Convalidare una firma PAdES significa controllare tre aspetti indipendenti, e il segno di spunta verde in un visualizzatore informa solo sul terzo. Per prima cosa, l'array /ByteRange deve coprire i byte corretti: gli intervalli indicati devono ricostruire l'input esatto su cui è stato calcolato il digest CMS, senza byte firmati lasciati fuori. In secondo luogo, il certificato all'interno del CMS deve concatenarsi a una radice attendibile e contenere l'attributo firmato del certificato di firma richiesto da PAdES. Terzo, se il profilo dichiara una marca temporale, un token RFC 3161 deve legare il valore della firma a un momento precedente alla scadenza del certificato. Acrobat riunisce tutti e tre in un'unica icona; un verificatore di conformità li mantiene distinti e così dovrebbe fare il codice che produce questi file. losLab PDF Library (PDF Library for Delphi) fornisce il lato di firma, la reincorporazione della marca temporale e le chiamate di audit per ispezionare un ByteRange prima di considerarlo attendibile
Una distinzione trae in inganno quasi ogni prima implementazione PAdES, quindi merita di essere chiarita prima di qualsiasi codice. Una firma scritta con /SubFilter /adbe.pkcs7.detached è una firma ISO 32000-1 §12.8 perfettamente valida, che Acrobat segnalerà come valida. Non è però una firma PAdES, perché ETSI EN 319 142-1 richiede ETSI.CAdES.detached a ogni livello base. Un verificatore di conformità eIDAS rifiuta la prima e accetta la seconda anche se la crittografia è identica. Il profilo è un'affermazione che il documento fa su sé stesso e rendere corretta tale affermazione richiede una sola chiamata in PDF Library for Delphi
Cosa trasforma una firma PDF in una firma PAdES
ETSI EN 319 142-1 definisce quattro livelli base sovrapposti al formato CMS. PAdES-B-B è il punto di ingresso: una firma CAdES in un campo firma PDF con SubFilter ETSI.CAdES.detached e un attributo firmato del certificato di firma. PAdES-B-T aggiunge una marca temporale RFC 3161 sul valore della firma, dimostrando che la firma esisteva prima di un momento che nessuno può retrodatare. PAdES-B-LT incorpora nel Document Security Store i certificati, le CRL e le risposte OCSP necessarie alla convalida, così il file resta verificabile dopo che la CA emittente ha ritirato la propria infrastruttura. PAdES-B-LTA conclude la pila con una marca temporale del documento che protegge di nuovo le prove accumulate mentre gli algoritmi si indeboliscono
PDF Library for Delphi mappa questi concetti sulla propria API del processo di firma. Il marcatore del profilo è SetSignProcessCustomSubFilter. Se la politica richiede un'indicazione del tipo di impegno (prova di origine, prova di approvazione o uno degli altri identificatori ETSI numerati da 1 a 6), essa passa tramite SetSignProcessCommitmentType. Una politica di firma esplicita si collega con SetSignProcessSignaturePolicy, che accetta l'OID della politica e il relativo digest. Un valore predefinito merita attenzione: lasciando l'algoritmo di digest su automatico, la libreria seleziona SHA-256 per le firme ETSI e adbe.pkcs7.detached, e ricorre a SHA-1 solo nel percorso legacy adbe.pkcs7.sha1. Impostalo comunque in modo esplicito. I revisori chiedono quale hash sia stato usato e un valore esplicito nel codice è più facile da difendere di un valore predefinito che richiede la consultazione del manuale per essere spiegato
Produrre la firma di base
L'API piatta gestisce la firma come una macchina a stati in un'unica esecuzione: apre un processo sul file sorgente, lo configura, termina su un file di output e legge il codice risultato. La sequenza seguente produce una firma PAdES-B-B con SHA-256. La riga più importante non riguarda la firma in sé. È la riserva /Contents deliberatamente sovradimensionata, perché è l'unico elemento che non può più essere modificato in seguito se a questa firma dovrà mai essere aggiunta una marca temporale
var
Pdf: TPDFlib;
SignId: Integer;
begin
Pdf := TPDFlib.Create;
try
SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
if SignId = 0 then
raise Exception.Create('cannot open source PDF');
Pdf.SetSignProcessField(SignId, 'Sig1');
Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
Pdf.SetSignProcessDigestAlgorithm(SignId, 2); // SHA-256
Pdf.SetSignProcessReserveContentsBytes(SignId, 8192); // room for a timestamp later
Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
if Pdf.GetSignProcessResult(SignId) <> 1 then
raise Exception.CreateFmt('signing failed, code %d',
[Pdf.GetSignProcessResult(SignId)]);
Pdf.ReleaseSignProcess(SignId);
finally
Pdf.Free;
end;
end;
NewSignProcessFromFile restituisce 0 quando non è possibile aprire affatto l'origine. Dopo questo punto, GetSignProcessResult distingue le modalità di errore che si presentano realmente in produzione: 4 indica una password PDF errata, 7 una password PFX errata, 9 un file di certificato privo di chiave privata, 10 un percorso di output non scrivibile, 11 un errore nell'applicazione dei byte della firma. Registrare il codice numerico accanto al nome del file di input trasforma una vaga richiesta di supporto in una diagnosi di un minuto
Aggiungere la marca temporale RFC 3161 che la libreria non recupera per te
PDF Library for Delphi non include un client TSA, e si tratta di un confine deliberato anziché di una lacuna. La libreria calcola l'hash che l'autorità di marcatura temporale deve controfirmare e poi reincorpora il CMS aumentato; lo scambio HTTP e la modifica del CMS nel mezzo spettano al chiamante. Vi è una ragione tecnica inderogabile per questa separazione. Il controllo Windows CryptoAPI che nominalmente aggiunge attributi non firmati, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, fallisce con CRYPT_E_INVALID_INDEX nel layout SignedData separato usato da PAdES. Il CMS migliorato deve quindi provenire da un codificatore CMS sotto il tuo controllo. Nessuna libreria può inserire silenziosamente il token con una sola chiamata di sistema, e qualunque libreria affermi di farlo esegue la modifica in un punto che non puoi osservare
var
Pdf: TPDFlib;
StsId: Integer;
HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
Pdf := TPDFlib.Create;
try
StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
// le due chiamate qui sotto sono codice applicativo: un POST HTTP alla tua TSA,
// e una ricodifica CMS che aggancia il token come attributo unsigned
TstDer := RequestTimeStampToken(HashHex);
TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
raise Exception.Create('timestamp embedding failed');
Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
finally
Pdf.Free;
end;
end;
Osserva i codici risultato in questo punto: 12 indica che il campo firma nominato non esiste, 11 che il CMS esistente non può essere analizzato e 13 che il CMS aumentato non entra più nel segnaposto /Contents riservato. Il codice 13 è quello più doloroso, perché l'unica correzione consiste nel firmare di nuovo: un tipico token di marca temporale con la propria catena di certificati occupa da 4 a 6 KB, e la riserva di 8192 byte effettuata durante il passaggio B-B esiste proprio per lasciare spazio a questo passaggio
La convalida inizia dal ByteRange, non dalla catena di certificati
Un segno di spunta verde in un visualizzatore è una decisione di attendibilità rispetto all'archivio certificati di quella macchina, non un verdetto strutturale sul file. La convalida programmatica dovrebbe iniziare più in basso, con la domanda che gli aggiornamenti incrementali rendono delicata: quali byte copre effettivamente ogni firma? Ogni miglioramento descritto qui, sia esso una seconda firma, un dizionario DSS o una marca temporale del documento, arriva tramite aggiornamento incrementale e ogni aggiornamento aggiunge byte al di fuori del /ByteRange della firma precedente. Quei byte aggiunti sono legittimi. Un validatore deve comunque classificarli rispetto alla politica di modifica del documento e il livello DocMDP per campo in cui risiede tale politica è leggibile con GetSignatureDocMDPLevelByName
var
Doc: TPDFlibSignDoc;
Names: TStringList;
I: Integer;
B0, B1, B2, B3, FileSize: Int64;
begin
FileSize := TFile.GetSize('invoice-bt.pdf'); // before Open: SignDoc holds a share lock
Doc := TPDFlibSignDoc.Create;
try
if not Doc.Open('invoice-bt.pdf', '', False) then
raise Exception.Create('cannot open for audit');
Names := TStringList.Create;
try
Doc.GetSignatureFieldNames(Names);
for I := 0 to Names.Count - 1 do
if Doc.GetSignatureValueObjNum(Names[I]) > 0 then // >0 vuol dire firmato davvero
begin
B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
if (B0 = 0) and (B2 + B3 = FileSize) then
Writeln(Names[I], ': covers the file to EOF')
else
Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
end;
finally
Names.Free;
end;
Doc.Close;
finally
Doc.Free;
end;
end;
In questo percorso di audit ci sono due insidie. TPDFlibSignDoc.Open mantiene il file con un blocco di condivisione esclusivo, quindi un validatore che voglia anche calcolare l'hash dei byte grezzi del file per la verifica CMS deve leggere il file in memoria prima di aprirlo per l'audit. Inverti l'ordine e la lettura fallisce per un blocco impostato dal codice stesso. La seconda insidia è silenziosa anziché esplicita: la controparte dell'API piatta GetSignProcessByteRange restituisce Integer mentre gli offset sottostanti sono Int64, perciò oltre 2 GB la chiamata piatta tronca senza avviso; ecco perché questo esempio recupera gli offset tramite la classe di audit. Vale la pena nominare anche un'assenza. Il livello piatto non dispone affatto di un wrapper VerifySignature. I verdetti crittografici arrivano dalla classe TPDFlibSignatureVerifier, che restituisce vsValid, vsInvalid o vsUnknown, oppure da un validatore esterno di cui la tua politica di conformità si fida già
Convalida a lungo termine: DSS, VRI e marca temporale del documento
PAdES-B-LT esiste perché l'infrastruttura di revoca è mortale. ETSI EN 319 142-1 §5.4.2.2 specifica il Document Security Store: un dizionario a livello di documento che contiene certificati, CRL e risposte OCSP, facoltativamente indicizzati per firma tramite voci VRI basate sull'hash di /Contents di ciascuna firma. Il flusso PDF Library for Delphi rispecchia il progetto della marca temporale. NewPAdESDSSProcessFromFile apre il processo; AddPAdESDSSCertificate, AddPAdESDSSCRL e AddPAdESDSSOCSP accettano blob DER; AddPAdESDSSVRI collega il materiale selezionato a una firma; EndPAdESDSSProcessToFile scrive tutto come aggiornamento incrementale. La parte difficile resta dalla tua parte. Recuperare il materiale di revoca e giudicare se sia sufficientemente recente da meritare l'incorporazione è compito del chiamante. La libreria garantisce che i dizionari siano strutturalmente conformi; non può garantire che il responder OCSP abbia detto la verità
Il punto di arrivo archivistico, B-LTA, aggiunge una marca temporale del documento: un campo firma separato il cui tipo è DocTimeStamp anziché Sig, prodotto tramite SetSignProcessDocTimeStamp con una lunghezza di firma riservata. Non sostituisce la marca temporale della firma del passaggio B-T. La marca temporale della firma prova quando esisteva una firma specifica; quella del documento protegge l'intero file, comprese le prove DSS, ed è l'elemento che un archivio a lungo termine rinnova ogni pochi anni quando gli algoritmi si indeboliscono. Un profilo archivistico maturo contiene entrambe. Per i lettori precedenti a queste strutture, TPDFlibSignDoc.EnsurePAdESExtensions registra l'estensione sviluppatore ESIC nel catalogo del documento, annunciando che il file usa funzionalità definite da ETSI
Vale la pena prevenire una reazione a tutto questo, perché sembra un errore ma non lo è. Un visualizzatore spesso segnala «validità sconosciuta» per un file la cui struttura PAdES è del tutto corretta. Attendibilità e struttura sono assi indipendenti. Il visualizzatore semplicemente non riesce a concatenare il firmatario a una radice attendibile su quella macchina; è normale con CA private e certificati di test, anche quando l'audit ByteRange e la verifica CMS hanno entrambi esito positivo. La correzione consiste nel distribuire correttamente il certificato radice oppure nel valutare rispetto agli elenchi attendibili dell'UE quando lo status eIDAS qualificato è l'obiettivo effettivo, non nel modificare il codice di firma
Per la prospettiva di audit, cioè l'enumerazione dei campi firma in un corpus, il dump delle disposizioni ByteRange e la lettura in blocco dei livelli DocMDP, consulta l'articolo complementare sul banco di lavoro per conformità e firma. I documenti firmati che devono soddisfare anche una politica di archiviazione rientrano nel flusso descritto in il preflight PDF/A e PDF/UA in Delphi. La documentazione API completa e i download di valutazione sono nella pagina del prodotto losLab PDF Library per Delphi