La domanda che rompe le pipeline di firma deboli raramente è crittografica. Un auditor chiede: "il vostro report preflight dice che questo batch di fatture è conforme PDF/A: è stato verificato prima o dopo l'applicazione della firma?" Quando validazione e firma girano come due tool separati con un passaggio di remediation in mezzo, esistono almeno tre revisioni del file, e il report descrive solo una di esse. PDFlibPas, losLab PDF Developer Library per Delphi e C++Builder, distribuisce preflight e firma PAdES dietro una sola facade class, rendendo pratico costruire un workbench in cui quella domanda ha una risposta dimostrabile.
Questo articolo percorre il pattern del workbench end to end: preflight sugli esatti byte che verranno firmati, firma applicata tramite la SignProcess API, e audit di rilettura che conferma che il ByteRange copra davvero il file. Ogni chiamata mostrata qui esiste oggi nella libreria, e così ogni trappola.
Tre revisioni di un documento, e come si apre il divario
Un workflow compliance-then-sign tocca il file almeno tre volte. L'originale arriva da monte. Un passaggio di remediation lo carica, abilita una modalità di conformità e salva una revisione corretta. Il passaggio di firma poi aggiunge una firma come aggiornamento incrementale. Ognuno di quei salvataggi cambia byte, quindi un report preflight è significativo solo se dichiara quale revisione descrive. Il modo più economico per ancorarlo è un hash SHA-256 del file registrato accanto a ogni run preflight e ogni firma.
Un comportamento della libreria rende l'ancoraggio più rigoroso di quanto si potrebbe pensare: le correzioni di conformità richieste tramite SetPDFAMode o SetPDFUAMode vengono applicate durante il salvataggio, non nel momento della chiamata. Auto-repair come forzare i print flag sulle annotazioni o assegnare un tab order PDF/UA finiscono solo nel file di output. Eseguire il checker contro il documento appena "corretto" in memoria non dimostra nulla sui byte che stai per firmare: riesegui sempre il preflight sul file salvato.
Preflight da disco, e lo zero che significa due cose
L'entry point preflight flat è CheckFileCompliance(FileName, Password, ComplianceTest, Options), dove test 1 seleziona PDF/A, ISO 19005, e test 2 seleziona PDF/UA, ISO 14289. Apre il file tramite il reader streaming della libreria, senza prima richiedere LoadFromFile, e restituisce un handle string-list con un finding per entry:
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 restituito. Zero significa "nessun finding", ma significa anche "il file non poteva essere aperto": l'implementazione restituisce 0 ogni volta che la lista risultati resta vuota, incluso un fallimento di lettura. Un workbench che tratta 0 come semaforo verde approverà un file bloccato da un altro processo. Abbina la chiamata a LastErrorCode, come sopra. Nota anche che il checker apre il file con share mode deny-write; se il passaggio di remediation mantiene ancora un writer handle, il preflight fallisce per un motivo che non ha nulla a che fare con la conformità.
Per la review umana, CreatePreflightReport renderizza gli stessi finding come report leggibile, e ComparePreflightReports confronta due run: un modo comodo per dimostrare che la remediation ha rimosso finding senza introdurne di nuovi.
Firmare la revisione controllata con un SignProcess
Una volta che la revisione salvata passa il preflight e il suo hash è registrato, firma esattamente quel file. La SignProcess API è un builder: apri un handle processo, configuralo, fai commit, poi leggi il result code.
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'); // PAdES baseline
PDF.SetSignProcessDigestAlgorithm(ProcessID, 2); // SHA-256
PDF.SetSignProcessReserveContentsBytes(ProcessID, 8192); // room for a later timestamp
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 configurazione meritano attenzione. SetSignProcessCustomSubFilter con ETSI.CAdES.detached seleziona una firma PAdES profilata in ETSI EN 319 142-1, invece della famiglia legacy adbe.pkcs7.detached. E SetSignProcessReserveContentsBytes riserva spazio nel placeholder /Contents: se intendi mai aggiungere un timestamp di firma, il CMS allargato deve entrare nello spazio riservato ora, perché il placeholder non può crescere dopo senza rifirmare.
GetSignProcessResult restituisce esiti codificati: 1 è successo, 4 significa password PDF errata, 7 password certificato errata, 9 un PFX senza chiave privata, 11 un fallimento durante l'applicazione della firma. Logga il codice invece di un boolean: gran parte dei casi di supporto sulla firma sono confusioni di credenziali che solo questi valori distinguono.
Rilettura: audit del file appena prodotto
Un workbench non dovrebbe mai fidarsi del proprio percorso di scrittura. La classe di audit TPDFlibSignDoc riapre l'output firmato ed espone direttamente le entry del dictionary di firma:
var
Doc: TPDFlibSignDoc;
Names: TStringList;
FS: TFileStream;
I: Integer;
SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
// Capture the size before Open: the audit object holds a share lock on the 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 means the field is signed
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 mappano alle entry del dictionary: 0 restituisce il CMS grezzo da /Contents, 2 e 3 i nomi /Filter e /SubFilter, e da 11 a 14 i quattro numeri ByteRange. I valori testuali passano da GetSignatureTextValueByName: la chiave 0 è il tempo di firma dichiarato, la chiave 5 distingue una Sig ordinaria da un DocTimeStamp.
La cattura della dimensione file all'inizio non è decorazione. TPDFlibSignDoc.Open mantiene il file con un lock di condivisione restrittivo per tutta la sua vita, quindi tutto ciò che ha bisogno dei byte grezzi, hash del range firmato o ricalcolo del digest CMS, deve leggere il file prima di Open. La demo SigningWorkbench della libreria legge prima l'intero file in memoria proprio per questo.
Aritmetica ByteRange che dimostra la copertura
Un file sano con una sola firma ha un ByteRange della forma [0 a b c]: la copertura parte da offset 0, salta il placeholder hex /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. Quando non equivale, qualcuno ha aggiunto un update incrementale dopo la firma. È legittimo secondo ISO 32000-1 §12.8, compilazioni successive di form, una seconda firma, un dictionary DSS arrivano tutti così, ma è proprio il fatto che un audit trail deve registrare subito invece di scoprirlo durante una disputa.
Fai attenzione alla larghezza degli interi durante il controllo. GetSignProcessByteRange della flat API restituisce un Integer a 32 bit, mentre i valori sottostanti sono Int64; su file oltre 2 GB l'accessor flat tronca. Usa il class-layer TPDFlibSigner.GetByteRange, che restituisce Int64, oppure analizza i valori da GetSignatureValueByName come fa il codice di audit sopra.
Cosa resta tua responsabilità
Sii chiaro sui confini. La flat TPDFlib API non ha alcun wrapper di verifica firma; la verifica crittografica vive nel class layer come TPDFlibSignatureVerifier, il cui VerifySignature risponde valid, invalid o unknown. Allo stesso modo non esiste un client HTTP built-in per timestamp authority RFC 3161: la libreria calcola l'hash da inviare e reinserisce il CMS aumentato, ma il round trip di rete appartiene al tuo codice. Pianifica entrambi nel design del workbench dall'inizio; entrambi sono facili da incapsulare e pessimi da scoprire mancanti nello sprint finale.
Domande frequenti
Aggiungere una firma rompe la conformità PDF/A? Non di per sé. La firma arriva come aggiornamento incrementale, e ISO 19005-2 in poi consente esplicitamente documenti firmati. L'apparenza della firma, però, segue le stesse regole di qualunque contenuto pagina, font incorporati e niente colore dipendente dal dispositivo, quindi il gate finale del workbench dovrebbe essere un altro run preflight sull'output firmato.
Perché il mio file passa qui ma fallisce in un validatore esterno? I validatori implementano set di regole sovrapposti ma non identici. Tratta CheckFileCompliance come gate veloce in pipeline e verifica i candidati release con uno strumento indipendente come veraPDF; quando i due non concordano, il testo del finding di solito nomina la clausola da leggere.
Posso firmare e timestampare in un solo passaggio? No: prima viene scritta la firma baseline, poi un processo timestamp separato aumenta il CMS dentro lo spazio /Contents riservato. Per questo la chiamata reserve-bytes nell'esempio di firma conta; dimensionarla per il token timestamp che ti aspetti.
Dove andare dopo
Per i livelli timestamp e long-term validation costruiti sopra questo workbench, vedi la guida a firma e validazione PAdES. La metà preflight è trattata più in profondità nella guida preflight PDF/A e PDF/UA.
Documentazione API completa e download trial sono nella pagina prodotto PDFlibPas.