Un workbench che concatena la validazione di conformità alla firma digitale deve coordinare quattro passi, in questo ordine, e tenerli legati a un unico insieme di byte per tutto il percorso. Esegue un preflight PDF/A o PDF/UA. Applica le correzioni richieste dai rilievi e salva una revisione corretta. Firma esattamente quella revisione. Poi rilegge il file firmato e conferma che la firma lo copra davvero. L'ordine non è un dettaglio estetico. Se saltate la rilettura state riponendo fiducia nel vostro stesso percorso di scrittura; se lasciate che il preflight giri sulla revisione sbagliata, il vostro rapporto di conformità descrive un file che non avete mai consegnato
La parte che la maggior parte delle pipeline artigianali sbaglia è la giunzione tra validazione e firma. Eseguitele come due strumenti separati con una fase di correzione nel mezzo e verranno alla luce almeno tre revisioni distinte del file, ciascuna con i propri byte. Il rapporto di preflight che consegnate a un revisore ne descrive una. La firma ne congela un'altra. Nulla nel file dichiara che siano la stessa revisione, e spesso non lo sono. PDF Library for Delphi, la losLab PDF Developer Library per Delphi e C++Builder, colloca preflight e firma PAdES dietro un'unica classe facade, così l'intera sequenza può vivere in un solo processo che non perde mai di vista quali byte sta trattando. Ogni chiamata mostrata di seguito esiste oggi nella libreria, e così ogni trappola segnalata accanto a essa
Tre revisioni di un solo documento, e come si apre il divario
Contate i salvataggi. L'originale arriva da monte. La fase di correzione lo carica, attiva una modalità di conformità e scrive una revisione corretta. La fase di firma aggiunge una firma come aggiornamento incrementale, che è una terza scrittura. Tre salvataggi, tre disposizioni di byte, e un rapporto di preflight non significa nulla se non dichiara quale delle tre copre. Uno SHA-256 del file, registrato accanto a ogni esecuzione del preflight e a ogni firma, è l'ancoraggio economico che vi permette di dimostrare che la revisione validata è la revisione firmata
Un comportamento della libreria irrigidisce ulteriormente questa disciplina. Le correzioni di conformità richieste tramite SetPDFAMode o SetPDFUAMode non hanno effetto nel momento in cui le chiamate. Vengono applicate durante il salvataggio. Le riparazioni automatiche, come forzare i flag di stampa delle annotazioni o assegnare un ordine di tabulazione PDF/UA, finiscono nel file di output e in nessun altro posto, quindi un controllo eseguito sul documento appena "corretto" in memoria non dice nulla sui byte diretti al firmatario. Salvate prima, poi eseguite il preflight sul file salvato. Lo stato in memoria è una bozza; solo il file su disco è reale
Preflight da disco, e lo zero che significa due cose
Il punto di ingresso flat del preflight è CheckFileCompliance(FileName, Password, ComplianceTest, Options). Il test 1 seleziona PDF/A (ISO 19005), il test 2 seleziona PDF/UA (ISO 14289). Apre il file tramite il lettore in streaming della libreria, quindi non serve chiamare prima LoadFromFile, e restituisce un handle di string list che porta un rilievo per voce:
var
PDF: TPDFlib;
ListID, I: Integer;
begin
PDF := TPDFlib.Create;
try
ListID := PDF.CheckFileCompliance('invoice-fixed.pdf', '', 1, 0); // 1 = PDF/A
if ListID = 0 then
begin
if PDF.LastErrorCode <> 0 then
raise Exception.Create('Preflight could not read the file')
else
Writeln('No PDF/A findings');
end
else
begin
for I := 0 to PDF.GetStringListCount(ListID) - 1 do
Writeln(PDF.GetStringListItem(ListID, I));
PDF.ReleaseStringList(ListID);
end;
finally
PDF.Free;
end;
end;
La trappola sta nel valore di ritorno, ed è del tipo che supera qualsiasi test sul percorso felice. Zero significa "nessun rilievo". Zero significa anche "il file non ha potuto essere aperto", perché l'implementazione restituisce 0 ogni volta che la lista dei risultati torna vuota, errore di lettura incluso. Un workbench che legge 0 come via libera approverà allegramente un file che qualche altro processo ha bloccato. Affiancare alla chiamata LastErrorCode, come sopra, è ciò che distingue i due casi. Il controllore apre inoltre il file in una modalità di condivisione che nega la scrittura, quindi se il vostro passo di correzione tiene ancora un handle in scrittura, il preflight fallisce per un motivo che non ha nulla a che vedere con la conformità e tutto a che vedere con uno stream che avete dimenticato di liberare
Quando a leggere i rilievi è una persona anziché una pipeline, CreatePreflightReport li rende come un rapporto leggibile. ComparePreflightReports confronta due esecuzioni, un modo ordinato per dimostrare che la correzione ha eliminato i rilievi originali senza introdurne silenziosamente di nuovi
Firmare la revisione controllata con un SignProcess
Una volta che la revisione salvata supera il preflight e il suo hash è agli atti, firmate esattamente quel file e nessun altro. L'API SignProcess si legge come un builder. Aprite un handle di processo, configuratelo riga per riga, confermate, poi rileggete il codice di risultato
ProcessID := PDF.NewSignProcessFromFile('invoice-fixed.pdf', '');
if ProcessID = 0 then
raise Exception.Create('Cannot open source for signing');
PDF.SetSignProcessField(ProcessID, 'ApprovalSig');
PDF.SetSignProcessPFXFromFile(ProcessID, 'company.pfx', PfxPassword);
PDF.SetSignProcessInfo(ProcessID, 'Invoice approval', 'Berlin', 'billing@example.com');
PDF.SetSignProcessCustomSubFilter(ProcessID, 'ETSI.CAdES.detached'); // baseline PAdES
PDF.SetSignProcessDigestAlgorithm(ProcessID, 2); // SHA-256
PDF.SetSignProcessReserveContentsBytes(ProcessID, 8192); // spazio per una marca temporale successiva
PDF.EndSignProcessToFile(ProcessID, 'invoice-signed.pdf');
if PDF.GetSignProcessResult(ProcessID) <> 1 then
Writeln('Sign failed, code ', PDF.GetSignProcessResult(ProcessID));
PDF.ReleaseSignProcess(ProcessID);
Due righe di quella sequenza pesano più di quanto sembri. SetSignProcessCustomSubFilter con ETSI.CAdES.detached sceglie una firma PAdES come profilata in ETSI EN 319 142-1 anziché la famiglia legacy adbe.pkcs7.detached, che è la differenza tra una firma che un validatore europeo accetta e una che segnala. SetSignProcessReserveContentsBytes riempie il segnaposto /Contents, e la dimensione che scegliete qui è una decisione sul futuro: se un giorno seguirà una marca temporale di firma, il CMS ingrandito dovrà stare nello spazio che riservate adesso, perché il segnaposto non può crescere in seguito senza rifirmare tutto quanto. Riservate con generosità e sprecherete qualche kilobyte. Riservate troppo stretto e il passo della marca temporale fallirà tra mesi con un overflow che farete fatica a ricollegare a questa singola riga
GetSignProcessResult risponde con un codice, non con un booleano, e vale la pena conservare quei codici. 1 è successo. 4 è una password PDF errata, 7 una password del certificato errata, 9 un PFX che non porta alcuna chiave privata, 11 un fallimento mentre la firma veniva applicata. Riduceteli a un vero/falso e butterete via l'unica informazione che distingue un caso di assistenza per password sbagliata da uno per chiave priva della parte privata. Registrate l'intero nel log
Rilettura: verificare il file appena prodotto
Nessun workbench dovrebbe fidarsi del percorso che ha scritto il file che sta per certificare. La classe di verifica TPDFlibSignDoc riapre l'output firmato e legge le voci del dizionario della firma direttamente da disco:
var
Doc: TPDFlibSignDoc;
Names: TStringList;
FS: TFileStream;
I: Integer;
SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
// Catturate la dimensione prima di Open: l'oggetto di verifica tiene un lock di condivisione sul file
FS := TFileStream.Create('invoice-signed.pdf', fmOpenRead or fmShareDenyNone);
SourceSize := FS.Size;
FS.Free;
Doc := TPDFlibSignDoc.Create;
Names := TStringList.Create;
try
if not Doc.Open('invoice-signed.pdf', '', False) then Exit;
Doc.GetSignatureFieldNames(Names);
for I := 0 to Names.Count - 1 do
if Doc.GetSignatureValueObjNum(Names[I]) > 0 then // > 0 significa che il campo è firmato
begin
RangeStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
GapStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
TailStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
TailLen := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
if (RangeStart = 0) and (TailStart + TailLen = SourceSize) then
Writeln(Names[I], ': signature covers the file to EOF')
else
Writeln(Names[I], ': earlier revision, or unusual ByteRange layout');
end;
Doc.Close;
finally
Names.Free;
Doc.Free;
end;
end;
Gli argomenti ValueKey corrispondono alle voci del dizionario. La chiave 0 restituisce il CMS grezzo da /Contents, le chiavi 2 e 3 i nomi /Filter e /SubFilter, e da 11 a 14 i quattro numeri del ByteRange. I valori testuali tornano invece tramite GetSignatureTextValueByName: la chiave 0 è l'ora di firma dichiarata, e la chiave 5 distingue una Sig ordinaria da un DocTimeStamp, cosa che conta quando un documento porta entrambi
La cattura della dimensione del file in cima a quell'esempio è portante, non ordinaria manutenzione. TPDFlibSignDoc.Open tiene il file sotto un lock di condivisione restrittivo per tutta la sua vita, quindi qualunque cosa abbia bisogno dei byte grezzi (calcolare l'hash dell'intervallo firmato, ricalcolare il digest CMS) deve leggere il file prima che Open venga chiamato. La demo SigningWorkbench della libreria stessa legge per prima cosa l'intero file in memoria proprio per questo motivo, e un workbench che ignora l'ordine fallisce a intermittenza, sulla macchina che di volta in volta perde la corsa
L'aritmetica del ByteRange che dimostra la copertura
Un file sano con una sola firma ha un ByteRange della forma [0 a b c]: la copertura inizia all'offset 0, salta il segnaposto esadecimale /Contents tra a e b, poi riprende fino al byte b+c. Quando b+c equivale alla dimensione del file, la firma copre tutto fino alla fine del file, che è il risultato desiderato. Quando resta sotto, qualcuno ha aggiunto un aggiornamento incrementale dopo che la firma era stata scritta. Questo è perfettamente legittimo secondo ISO 32000-1§12.8, poiché compilazioni successive di moduli, una seconda firma e un dizionario DSS arrivano tutti esattamente in questo modo. È anche precisamente il fatto che una pista di controllo dovrebbe registrare al momento della firma anziché ricostruire sotto pressione durante una contestazione
Fate attenzione alla larghezza degli interi mentre svolgete questa aritmetica. GetSignProcessByteRange dell'API flat restituisce un Integer a 32 bit, ma i valori sottostanti sono Int64, quindi su un file oltre i 2 GB l'accessore flat tronca silenziosamente. Rivolgetevi a TPDFlibSigner.GetByteRange del livello a classi, che restituisce Int64, oppure estraete i valori da GetSignatureValueByName come fa il codice di verifica qui sopra
Ciò che la libreria lascia a voi
Due limiti conviene conoscerli in fase di progettazione anziché nello sprint finale. L'API flat TPDFlib non porta alcun wrapper di verifica della firma. La verifica crittografica vive un livello più in basso, in TPDFlibSignatureVerifier, il cui VerifySignature risponde valido, non valido o sconosciuto. Non esiste nemmeno un client HTTP integrato per le autorità di marcatura temporale RFC 3161. La libreria calcola l'hash da inviare e reincorpora il CMS arricchito una volta tornato il token, ma il viaggio di rete verso la TSA sta a voi scriverlo. Entrambi sono semplici da avvolgere e davvero sgradevoli da scoprire mancanti la settimana prima di un rilascio, quindi progettateli fin dal primo schizzo
Vale la pena chiarire con franchezza una domanda sulla conformità, perché decide dove va l'ultimo cancello: aggiungere una firma rompe il PDF/A? Non di per sé. La firma arriva come aggiornamento incrementale, e ISO 19005-2 in poi consente esplicitamente i documenti firmati. Il punto delicato è l'aspetto della firma, che segue le stesse regole di qualsiasi altro contenuto di pagina, font incorporati e nessun colore dipendente dal dispositivo inclusi. Quindi il cancello finale nel workbench è un'ulteriore esecuzione del preflight, questa volta sull'output firmato. Trattate CheckFileCompliance come il controllo rapido interno alla pipeline e verificate comunque le release candidate con uno strumento indipendente come veraPDF, poiché i validatori implementano insiemi di regole sovrapposti ma non identici; quando i due sono in disaccordo, il testo del rilievo di solito nomina la clausola da andare a leggere
Da tutto questo discende un punto di sequenza. Firma e marcatura temporale non sono un unico passaggio: la firma baseline viene scritta per prima, poi un processo separato di marcatura temporale arricchisce il CMS dentro lo spazio /Contents riservato, il che è esattamente il motivo per cui la riga dei byte riservati vista prima pesava così tanto. Per i livelli di marcatura temporale e validazione a lungo termine che si costruiscono su questo workbench, la guida pratica alla firma e validazione PAdES porta la firma da baseline a B-LT, e la metà dedicata al preflight approfondisce nella guida al preflight PDF/A e PDF/UA. La documentazione API completa e i download di prova si trovano sulla pagina prodotto di PDF Library for Delphi