Un contratto cifrato AES e firmato altrove si carica con un valore di firma che decodifica in byte casuali, oppure, sotto AES-GCM, con un dizionario di firma che non si decifra. Fuori dal /ID del trailer e dal dizionario di cifratura, ISO 32000-2 §7.6.2 lascia non cifrata solo la /Contents della firma, e PDFlibPas la mantiene in chiaro in lettura e in scrittura mentre /Reason e /M restano cifrati
L'eccezione che ISO 32000-2 ha aggiunto alle regole di cifratura
ISO 32000-2 §7.6.2 dice che la cifratura copre ogni stringa e stream del file, con quattro eccezioni:
- i valori della voce
/IDdel trailer - le stringhe dentro il dizionario
/Encrypt - le stringhe dentro gli stream, che vengono cifrati nell'insieme
- qualsiasi stringa esadecimale che sia il valore di
/Contentsin un dizionario di firma
Le prime tre sono vecchie. ISO 32000-1 §7.6.1 ne elenca solo quelle, quindi un decryptor scritto sul testo del 2008 non ha alcuna istruzione di saltare qualcosa in un dizionario di firma. Gli strumenti di firma hanno comunque scritto il valore in chiaro, e la ragione è meccanica: il signer riserva un placeholder esadecimale, scrive l'intero file, fa l'hash di tutto tranne il placeholder, costruisce il CMS e incolla il suo esadecimale nel buco. Quell'hash copre i byte così stanno su disco, ciphertext incluso. Cifrare il valore della firma non proteggerebbe nulla, e costringerebbe il passo di patching a conoscere la file key e a far stare un IV più il padding in un placeholder dimensionato prima che l'hash esistesse. Il resto del dizionario segue le regole normali:
| Voce del dizionario di firma | Tipo PDF | Conservata cifrata? |
|---|---|---|
/Contents | stringa esadecimale | No, esente per ISO 32000-2 §7.6.2 |
/Reason, /Location, /ContactInfo, /Name | stringa di testo | Sì |
/M | stringa data | Sì |
/ByteRange | array di interi | No, gli interi non vengono mai cifrati |
/Type, /Filter, /SubFilter | name | No, i name non vengono mai cifrati |
Un reader che si perde l'eccezione fa girare il cipher su DER in chiaro. Quel che esce dipende dall'algoritmo. RC4 è una stream cipher, quindi l'output conserva la lunghezza e diventa rumore: il parser CMS vede un primo byte che non è più 0x30 e segnala una firma malformata. AES-CBC tratta i primi 16 byte come vettore di inizializzazione e il resto come ciphertext, il che finisce in un errore di padding o in un blob più corto di spazzatura. AES-GCM, definito per il PDF da ISO/TS 32003, è il caso più ostile: ogni stringa cifrata porta con sé il proprio nonce e il proprio tag di autenticazione, e un valore in chiaro non passa la verifica del tag. Un decryptor che considera fatale per l'intero dizionario anche un solo tag fallito perde allora /Reason e /M insieme al valore della firma
Come si distingue la /Contents di una firma da ogni altra /Contents?
Filtrare sul nome della chiave è il primo tentativo ovvio, e fallisce in entrambe le direzioni. I dizionari di annotazione hanno pure loro una voce /Contents — una stringa di testo con il commento scritto da chi ha revisionato — e quella stringa va cifrata: se la saltate, ogni post-it in un file confidenziale è leggibile senza password. Filtrare su /Type /Sig si rompe nell'altra direzione: la Tabella 255 di ISO 32000-2 rende /Type opzionale in un dizionario di firma, con Sig come default, e lo richiede solo per /DocTimeStamp, quindi un producer conforme può ometterlo. PDFlibPas tratta perciò una stringa /Contents come valore di firma solo quando lo stesso dizionario porta un array /ByteRange più un /Type pari a Sig o DocTimeStamp, oppure nessun /Type accanto a un name /Filter. Il /Filter è richiesto in ogni dizionario di firma e il /ByteRange in ogni firma che appartiene a un campo firma; un'annotazione non ha né l'uno né l'altro. La libreria esegue lo stesso test quando cifra al save e quando decifra al load, così la /Contents esce da un load con i byte scritti dal producer
Prima cifrate, poi firmate con PDFlibPas
Firmate per ultimi. Il digest della firma copre i byte come sono conservati, quindi qualunque operazione che riscriva il file dopo la firma rompe ogni firma al suo interno. Applicare la cifratura a un documento firmato ri-cifra ogni stringa e stream; toglierla li riscrive con pari profondità. La sequenza che funziona è: cifrare, salvare, poi firmare il file cifrato. Il sign process accoda un incremental update le cui nuove stringhe vengono cifrate con la file key esistente e il cui placeholder /Contents resta in chiaro. Aprite il file nel sign process con la owner password; la user password basta solo quando i permessi includono il bit 9 della Tabella 22 di ISO 32000-2, che nomina esplicitamente i campi firma. Encrypt e il sign process sono disponibili in ogni edizione di PDFlibPas
uses
SysUtils, PDFlibrary;
const
OwnerPwd = 'owner-secret';
UserPwd = 'reader-secret';
procedure EncryptCopy(const Source, Target: string);
var
Lib: TPDFlib;
Perms: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(Source, '') <> 1 then
raise Exception.CreateFmt('Load failed (%d)', [Lib.LastErrorCode]);
// stampa, riempimento campi (il bit 9 copre i campi firma), copia accessibile, stampa high-res
Perms := Lib.EncodePermissions(1, 0, 0, 0, 1, 1, 0, 1);
// Strength 4 = AES-256 revision 6: la file key si usa direttamente, senza /ID
if Lib.Encrypt(OwnerPwd, UserPwd, 4, Perms) <> 1 then
raise Exception.CreateFmt('Encrypt failed (%d)', [Lib.LastErrorCode]);
if Lib.SaveToFile(Target) <> 1 then
raise Exception.CreateFmt('Save failed (%d)', [Lib.LastErrorCode]);
finally
Lib.Free;
end;
end;
procedure SignField(Lib: TPDFlib; const Source, Target, FieldName,
PFXFile, PFXPassword: string);
var
SP, R: Integer;
begin
SP := Lib.NewSignProcessFromFile(Source, OwnerPwd);
if SP = 0 then
raise Exception.CreateFmt('Input not found: %s', [Source]);
try
Lib.SetSignProcessField(SP, FieldName); // creato invisibile se manca
Lib.SetSignProcessPFXFromFile(SP, PFXFile, PFXPassword);
Lib.SetSignProcessInfo(SP, 'Approved for release', 'Head office', '');
// margine nel placeholder per una firma con time stamp aggiunta dopo
Lib.SetSignProcessReserveContentsBytes(SP, 16384);
Lib.EndSignProcessToFile(SP, Target); // restituisce sempre 0
R := Lib.GetSignProcessResult(SP);
if R <> 1 then
raise Exception.CreateFmt('Signing %s failed (result %d)', [FieldName, R]);
finally
Lib.ReleaseSignProcess(SP);
end;
end;
Strength 4 seleziona AES-256 revision 6, e quella scelta elimina due modalità di fallimento. Con RC4 e AES-128, l'Algoritmo 1 (ISO 32000-1 §7.6.2) deriva una chiave separata per ogni oggetto dal suo numero oggetto e numero generazione. L'Algoritmo 2 (ISO 32000-1 §7.6.3.3, passo e) immette inoltre il primo elemento /ID nella file key. Un update di firma riscrive il dizionario AcroForm e l'array /Annots della pagina, quindi ogni oggetto riscritto deve conservare il suo numero generazione originale, e il trailer dell'update deve portare lo stesso primo elemento /ID. Sbagliate la generazione e quell'oggetto non si decifra più; sbagliate l'/ID e non si decifra più niente. La revision 6 usa la file key direttamente (ISO 32000-2 §7.6.3.3, Algoritmo 1.A) e la recupera senza guardare l'/ID (§7.6.4.3.3, Algoritmo 2.A). PDFlibPas conserva i numeri generazione sugli oggetti riscritti a ogni strength e deriva la chiave dall'/ID così com'è conservato, anche quando un file malformato porta /ID 7 0 R invece dell'array diretto che la Tabella 15 di ISO 32000-2 richiede; un /ID [] vuoto fallisce la verifica della password. Il percorso password-chiave è ricostruito più a fondo accanto a come aprire PDF cifrati con una file key grezza
Un editor di testo basta per vedere il risultato. Un dizionario di firma sta non compresso nell'update che l'ha aggiunto, perché il signer patcha /ByteRange e /Contents per offset di file: cercate /ByteRange e ci atterrati sopra. L'esadecimale della /Contents si apre tipicamente con 3082, l'header DER della SEQUENCE CMS SignedData, e finisce in una lunga sequenza di zeri dove il placeholder era più grande della firma. La /Reason è ciphertext che cambia da una sessione di firma all'altra anche quando il testo non cambia, perché AES tira un IV fresco per ogni stringa. Atteso con un qpdf recente: qpdf --password=owner-secret --show-object=14 signed.pdf stampa la /Reason decifrata accanto a una /Contents invariata. Usate per quel controllo un file strength 4. Forma attesa nel file grezzo, con offset e byte variabili:
14 0 obj
<</Type /Sig /Filter /Adobe.PPKLite
/ByteRange [0 48211 81093 2304]
/Contents <308240A206092A864886F70D010702A0...000000000000>
/Reason <7F21C9E04B...>
/M <D35A0F98...>
...>>
endobj
Quali firme si rompono quando si incolla un time stamp?
Ogni firma aggiunta dopo quella timbrata. Una signature time stamp PAdES è un token RFC 3161 conservato come attributo unsigned del firmatario (RFC 5652 §5.3, unsignedAttrs), quindi vive dentro il CMS nella /Contents. Aggiungerne una significa riscrivere quell'esadecimale sul posto, e in un file cifrato la riscrittura funziona solo perché la /Contents è in chiaro: niente ri-cifratura, niente nuovo oggetto — al token basta stare nello spazio riservato al momento della firma. La firma proprietaria sopravvive perché il suo /ByteRange salta quei byte. Le firme successive non godono di questa esenzione: ISO 32000-2 §12.8.1 fa partire il byte range di ogni firma dall'header %PDF- fino alla fine dell'incremental update che l'ha aggiunta, quindi una seconda firma, o un document time stamp, fa l'hash anche della /Contents della prima insieme a tutto il resto. Il buco appartiene a una firma sola: patchatelo dopo che un secondo firmatario ha firmato, e il digest della seconda firma non torna più. La richiesta del token fa parte di firma e validazione PAdES da Delphi; questa sezione riguarda solo quali byte si spostano
PDFlibPas controlla la sovrapposizione prima di fare l'hash di qualunque cosa. Il processo di signature time stamp legge la /ByteRange di ogni altra firma nel documento e confronta ogni coppia di span con il buco /Contents del target. Qualunque sovrapposizione ferma la preparazione con process result 17, e nessun file di output viene scritto. Il processo appartiene all'edizione Enterprise, che istanziate come TPDFlibEnterprise dall'unità PDFlibEnterprise; le altre edizioni conservano i metodi, che restituiscono 0 e impostano LastErrorCode a 900. Tenete d'occhio il numero 17 nei vostri log: da GetPAdESSignatureTimeStampProcessResult significa che una firma successiva copre questa; da GetSignProcessResult significa che un livello di certificazione o un field lock vieta la firma. Mappate i due separatamente. L'helper qui sotto usa SignField e OwnerPwd dal listato precedente
uses
SysUtils, PDFlibrary, PDFlibEnterprise;
const
STS_COVERED_BY_LATER_SIGNATURE = 17;
function StampSignature(Lib: TPDFlibEnterprise; const Source, Target,
FieldName: string): Integer;
var
STS: Integer;
begin
Result := 0;
STS := Lib.NewPAdESSignatureTimeStampProcessFromFile(Source, OwnerPwd);
if STS = 0 then
Exit;
try
Lib.SetPAdESSignatureTimeStampField(STS, FieldName);
if Lib.ApplyPAdESSignatureTimeStamp(STS, 0) <> 1 then
begin
Result := Lib.GetPAdESSignatureTimeStampProcessResult(STS);
if Result = 1 then
Result := -1; // valore con hash fatto, scambio TSA fallito: vedere GetTimeStampError
Exit;
end;
Result := Lib.EndPAdESSignatureTimeStampProcessToFile(STS, Target);
finally
Lib.ReleasePAdESSignatureTimeStampProcess(STS);
end;
end;
procedure SignAndStampInOrder(const EncryptedFile: string);
var
Lib: TPDFlibEnterprise;
R: Integer;
begin
Lib := TPDFlibEnterprise.Create;
try
Lib.SetTimeStampServer('https://tsa.example.com/tsa', '', '');
SignField(Lib, EncryptedFile, 'step1.pdf', 'Author', 'author.pfx', 'pfx-1');
R := StampSignature(Lib, 'step1.pdf', 'step1-ts.pdf', 'Author');
if R <> 1 then
raise Exception.CreateFmt('Author time stamp failed (%d)', [R]);
SignField(Lib, 'step1-ts.pdf', 'step2.pdf', 'Approver', 'approver.pfx', 'pfx-2');
R := StampSignature(Lib, 'step2.pdf', 'final.pdf', 'Approver');
if R <> 1 then
raise Exception.CreateFmt('Approver time stamp failed (%d)', [R]);
// Atteso: 17. Il byte range di Approver copre la /Contents di Author in
// step2.pdf, quindi il processo rifiuta e restamped.pdf non viene mai scritto
R := StampSignature(Lib, 'step2.pdf', 'restamped.pdf', 'Author');
if R <> STS_COVERED_BY_LATER_SIGNATURE then
raise Exception.CreateFmt('Unexpected result %d', [R]);
finally
Lib.Free;
end;
end;
Timbrate ogni firma prima che il firmatario successivo tocchi il file. Costa un round trip TSA per firmatario e dà a ogni firma il suo tempo fidato. Un file che arriva con più firme già in posto è un caso a parte: quelle precedenti non possono più prendere una signature time stamp. Aggiungete invece un document time stamp — una firma /Type /DocTimeStamp con /SubFilter /ETSI.RFC3161 in una nuova revisione, il cui byte range copre tutte le firme precedenti e prova che esistevano in quel momento. ApplyPAdESLongTermArchiveValidation ne aggiunge uno dopo aver allegato il materiale di validazione. Riservate spazio per una signature time stamp mentre firmate, tramite SetSignProcessReserveContentsBytes come nel primo listato. Un token che incapsula la catena di certificati TSA arriva facilmente a parecchi kilobyte, e un placeholder senza quel margine fa fallire l'augmentation successiva con result 13
Decifrare un file firmato e cosa hasha un verifier
I range firmati di un file cifrato sono ciphertext. Un verifier confronta un digest del ciphertext conservato con il digest dentro il CMS: la /ByteRange è fatta di interi, la /Contents è in chiaro, e il controllo di integrità non richiede alcuna chiave — lo richiedono solo il nome del campo e la /Reason. Lo stesso fatto spiega perché decifrare un file firmato e salvarlo in chiaro invalida ogni firma al suo interno anche se non è cambiato un pixel visibile. Conservate l'originale cifrato e firmato come copia di riferimento. Nemmeno i cross-reference stream vengono cifrati (ISO 32000-2 §7.5.8.2), quindi un update scritto con uno xref stream mantiene i propri offset leggibili prima che una chiave qualsiasi venga derivata. Il layout è descritto in object stream e cross-reference stream in Delphi. Quando un range firmato è troppo grande per essere letto in memoria per il confronto, PDFlibPas riporta il controllo di integrità come unknown anziché invalid, perché byte mai confrontati non hanno dimostrato di differire
Il valore di firma di un file cifrato resta scrivibile dopo la firma finché un'altra firma o un document time stamp non lo copre. Mettete il passo del time stamp dentro quella finestra, e la sequenza cifra-poi-firma del primo listato si applica senza cambiamenti ai file multi-firmatario in PDF Library per Delphi