Hai ricevuto un PDF firmato e devi mostrare, nel tuo visualizzatore, chi lo ha firmato, quando è stato firmato, se la firma copre l'intero file e quanto si avvicina alla conformità a lungo termine. Il componente PDFium per Delphi e Lazarus risponde a tutte e quattro le domande con chiamate di sola lettura: FPDF_GetSignatureCount e la famiglia FPDFSignatureObj_* espongono il dizionario delle firme, mentre TPdf.ValidatePades classifica il livello di baseline PAdES. Questo è il primo di tre articoli sulle firme PDF con PDFium; i due successivi coprono la creazione di una firma B-B e l'aggiunta di timestamp a lungo termine. Tuttavia, un limite va chiarito sin da subito: tutto ciò che viene trattato qui riguarda l'ispezione, e leggere ciò che una firma dichiara è un compito diverso dal verificarne la crittografia o dal decidere se fidarsi del firmatario
Perché un dizionario di firme PDF non è solo un ammasso di byte
Una firma PDF è un dizionario, non un allegato opaco, e le sue due voci più importanti indicano quanta parte del file è effettivamente protetta. La specifica ISO 32000-1 §12.8 definisce il dizionario delle firme con una voce /ByteRange e una /Contents. /Contents contiene una struttura CMS SignedData con codifica esadecimale (RFC 5652), la busta crittografica che trasporta il certificato del firmatario, gli attributi firmati e il valore della firma stessa. /ByteRange è la parte che gli sviluppatori sottovalutano: è un array di due intervalli offset-lunghezza che insieme coprono l'intero file ad eccezione della stringa esadecimale /Contents. Quello spazio vuoto è esattamente il punto in cui risiedono i byte della firma, e i due intervalli su ciascun lato rappresentano precisamente ciò a cui si vincola la firma
La struttura del ByteRange è ciò che rende controllabile un salvataggio incrementale. Poiché un firmatario non può calcolare l'hash dei byte della firma che non esistono ancora, il file viene suddiviso attorno al segnaposto /Contents e tutto il resto viene inserito nell'hash della firma. Una firma il cui ByteRange non raggiunge la fine del file è un segnale di avvertimento: il contenuto aggiunto dopo l'intervallo coperto, tramite un successivo aggiornamento incrementale, non comprometterebbe la firma pur modificando ciò che il lettore vede. Pertanto, la prima cosa che un controllore esperto verifica non è chi ha firmato, ma se la firma copre effettivamente i byte che dichiara di approvare
Leggere il dizionario delle firme con le API di sola lettura di PDFium
Il componente PDFium espone il dizionario delle firme tramite due membri di sola lettura: SignatureCount e il record Signature[Index]. Internamente chiamano FPDF_GetSignatureCount, FPDF_GetSignatureObject e gli accessori FPDFSignatureObj_* per /SubFilter, /ByteRange, /Contents, /Reason e l'ora della firma. "Sola lettura" è l'espressione chiave qui: PDFium può enumerare e leggere le firme ma non dispone di API per crearne o scriverne una, motivo per cui la parte relativa alla firma di questa serie è implementata dalla libreria stessa e non da PDFium
var
Pdf: TPdf;
i: Integer;
Sig: TPdfSignature;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
for i := 0 to Pdf.SignatureCount - 1 do
begin
Sig := Pdf.Signature[i];
Writeln('SubFilter : ', Sig.Encoding); // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
Writeln('Signed at : ', Sig.Time); // stringa data firmatario, ad es. D:20260708120000+02'00'
Writeln('Reason : ', Sig.Reason);
Writeln('CMS length: ', Length(Sig.Content)); // dati DER SignedData grezzi presi da /Contents
Writeln('DocMDP : ', Sig.Permission); // 0 = nessuna certificazione, 1..3 = livello MDP
end;
finally
Pdf.Free;
end;
end;
Ciascun record TPdfSignature si mappa direttamente sul dizionario. Encoding rappresenta il /SubFilter, il campo diagnostico più importante, perché indica il gestore della firma e separa immediatamente una firma moderna ETSI.CAdES.detached da una obsoleta o non consentita. Time è l'ora di firma dichiarata dall'autore come stringa di data PDF, ossia un'attestazione del firmatario e non un'ora attendibile. Content rappresenta i dati CMS SignedData grezzi, e Permission espone il livello di certificazione DocMDP (0 per una normale firma di approvazione, da 1 a 3 per una firma di certificazione che impedisce modifiche successive). L'unico campo non esposto dal record è il ByteRange analizzato, omissione intenzionale, poiché ValidatePades esegue per te il calcolo della copertura del ByteRange evitando di doverlo rifare manualmente
Qual è la differenza tra PAdES B-B, B-T, B-LT e B-LTA?
I quattro livelli di baseline PAdES formano una scala che va da una firma minimamente valida a una progettata per resistere a decenni di archiviazione, e ciascun livello include strettamente quello inferiore. La specifica ETSI EN 319 142-1 li definisce come B-B, B-T, B-LT e B-LTA. B-B (Basic) è la firma integrata con gli attributi firmati obbligatori e nulla più. B-T (Timestamp) aggiunge una marca temporale attendibile RFC 3161 sulla firma, in modo che il momento della firma sia attestato da un'autorità di marcatura temporale invece di essere semplicemente asserito dall'orologio del firmatario. B-LT (Long-Term) incorpora il materiale di convalida — la catena di certificati e opzionalmente le risposte OCSP o CRL — all'interno del file, cosicché la firma possa essere ancora convalidata a distanza di anni quando l'infrastruttura di emissione non sarà più attiva. B-LTA (Long-Term con marca temporale di archivio) avvolge una marca temporale del documento intorno a tale materiale, proteggendo i dati a lungo termine stessi e offrendo un punto per rinnovare la marca temporale prima che la crittografia sottostante diventi obsoleta
La lettura pratica riguarda l'orizzonte temporale. Una firma B-B risponde alla domanda "qualcuno ha firmato questo documento?". La firma B-T risponde a "e quando, in modo dimostrabile?". La firma B-LT risponde a "e posso ancora verificarla dopo la scadenza dei certificati?". La firma B-LTA risponde a "e questa verifica sarà ancora valida tra vent'anni?". I profili normativi scelgono un gradino specifico: molti contesti di fatturazione elettronica ed eIDAS richiedono almeno il livello B-T, mentre i mandati di archiviazione richiedono il livello B-LT o B-LTA. Sapere quale livello raggiunge effettivamente un documento, prima di accettarlo o rifiutarlo, è l'intero scopo della fase di ispezione
Rilevare il livello di baseline con TPdf.ValidatePades
Il componente PDFium riduce l'intera questione del livello a una singola chiamata. TPdf.ValidatePades restituisce un record TPadesValidationResult il cui campo Level è un TPadesLevel — plNone, plUnknown, plB_B, plB_T, plB_LT o plB_LTA — insieme a un insieme di problemi (issues), a un conteggio delle firme e a un conteggio delle marche temporali del documento. Il livello viene dedotto in modo monotonico: il convalidatore stabilisce prima il livello B-B, quindi passa a B-T se è presente una marca temporale della firma o del documento, a B-LT se il catalogo contiene un dizionario /DSS con i certificati e l'indicatore /Extensions /ESIC Level 1, e a B-LTA se sono presenti sia una marca temporale del documento sia l'indicatore ESIC Level 2. Due helper rendono il risultato immediatamente utilizzabile: IsCompliant è True solo quando il livello raggiunge almeno B-B e l'insieme dei problemi è vuoto, mentre IsCompliantAt ti consente di verificare una soglia di policy minima come ad esempio plB_T
var
Pdf: TPdf;
R: TPadesValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
case R.Level of
plNone: Writeln('No PAdES signature present');
plUnknown: Writeln('Signature present but level undeterminable');
plB_B: Writeln('PAdES B-B (basic)');
plB_T: Writeln('PAdES B-T (trusted timestamp)');
plB_LT: Writeln('PAdES B-LT (long-term material embedded)');
plB_LTA: Writeln('PAdES B-LTA (archive timestamp)');
end;
Writeln('Signatures : ', R.SignatureCount);
Writeln('DocTimeStamps: ', R.DocTimeStampCount);
if R.IsCompliantAt(plB_T) then
Writeln('Soddisfa la soglia di policy B-T')
else
Writeln('Al di sotto del livello B-T richiesto');
finally
Pdf.Free;
end;
end;
Perche adbe.pkcs7.sha1 è un SubFilter vietato?
Perché SHA-1 è compromesso e il gestore adbe.pkcs7.sha1 lo incorpora. Tale SubFilter esegue l'hash preliminare del documento con SHA-1 prima di racchiuderlo in PKCS#7, e SHA-1 è vulnerabile alle collisioni da anni, pertanto la clausola 6.3 di EN 319 142-1 lo vieta espressamente per una firma di baseline. Il metodo ValidatePades segnala ppeiForbiddenSubFilter quando rileva adbe.pkcs7.sha1 o adbe.x509.rsa_sha1, e segnala ppeiBadDigestAlgorithm quando il CMS stesso utilizza MD5 o SHA-1 como digest del messaggio (clausola 6.2.1). Si tratta di due controlli distinti che rilevano la stessa classe di debolezza a due livelli diversi
L'insieme dei problemi conta in totale 26 elementi, e quelli che riscontrerai più spesso si concentrano su struttura e copertura. ppeiByteRangeNotCoveringFile è il controllo di copertura descritto in precedenza. ppeiForbiddenCertKey si attiva quando il dizionario delle firme contiene una voce /Cert, vietata da PAdES in quanto la catena deve risiedere all'interno di SignedData.certificates del CMS. I problemi ppeiMissingSigningCertificate, ppeiMissingContentType e ppeiMissingMessageDigest segnalano l'assenza di attributi firmati obbligatori, mentre ppeiDetachedContentViolation rileva una firma che incorpora erroneamente il contenuto firmato anziché scollegarlo. L'enumerazione dell'insieme trasforma un semplice rifiuto in una diagnosi che puoi registrare nei log
var
R: TPadesValidationResult;
Issue: TPadesValidationIssue;
begin
R := Pdf.ValidatePades;
if R.Issues <> [] then
for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
if Issue in R.Issues then
Writeln('Issue: ',
GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;
Cosa non verifica ValidatePades
Il metodo ValidatePades convalida la struttura, non l'affidabilità, e scambiare l'una per l'altra rappresenta un errore pericoloso. Un risultato pari a plB_LTA indica che il documento contiene una firma B-LTA ben formata con tutti gli attributi, i materiali e le marche temporali prescritti nelle posizioni corrette — non significa che la firma sia crittograficamente valida, che la catena di certificati si colleghi a una radice attendibile o che nessun certificato della catena sia stato revocato. Il convalidatore non esegue deliberatamente alcuna verifica crittografica: non ricalcola la firma sul ByteRange, non compila né valuta la catena di attendibilità e non controlla lo stato di revoca OCSP o CRL. Questa distinzione è intenzionale e utile, poiché l'ispezione strutturale è rapida, completamente deterministica e non richiede chiavi, connessione di rete o crittografia di piattaforma, pertanto ValidatePades viene eseguito allo stesso modo su Windows, Linux e macOS come puro Pascal sui byte del file. La convalida della catena di attendibilità, al contrario, è inseparabile dalla policy — quali radici consideri attendibili, come recuperi le informazioni di revoca, quanto è rigida la tolleranza sulle marche temporali — e appartiene a una fase successiva che dipende dall'archivio certificati della piattaforma; pertanto, considera l'esito positivo di ValidatePades come il controllo preliminare necessario a confermare che una firma sia strutturata correttamente, per poi passare la firma strutturalmente valida a una reale verifica crittografica prima di farvi affidamento
Questo passaggio strutturale è il punto di partenza ideale, e si affianca naturalmente ai controlli di sola lettura più ampi trattati nella sezione sull'auditing dei rischi di sicurezza dei PDF con il componente PDFium e alle attività di conformità del formato come la convalida di documenti PDF/X pronti per la stampa. Una volta letta e classificata una firma, la fase successiva consiste nel produrne una: il secondo articolo di questa serie tratta della firma di PDF con PAdES B-B, mentre il terzo estende questo argomento alle marche temporali attendibili e alle firme a lungo termine B-LT e B-LTA. L'ispezione delle firme di sola lettura e il convalidatore ValidatePades mostrati qui fanno parte di PDFium Component per Delphi, C++Builder e Lazarus