Per scoprire cosa è cambiato in un PDF dopo la firma, il PDFium Component per Delphi e Lazarus offre TPdf.AnalyzeSignatureRevisions, un analizzatore delle modifiche post-firma che ricostruisce ogni revisione incrementale dai byte originali del file, valuta ogni successiva modifica di oggetto contro le regole DocMDP e FieldMDP di quella firma, e riporta le definizioni di oggetti shadow come un rischio a parte. La situazione che mira è familiare a chiunque maneggi contratti: un modulo certificato parte, torna indietro con altri due salvataggi incrementali, e ogni firma continua a verificare. È quanto atteso, perché una firma copre solo i byte della propria revisione. La vera domanda è se quei salvataggi successivi erano permessi, e una spunta verde sulla firma non risponde a questa domanda
Perché l'API delle firme di PDFium non può mostrare cosa è cambiato dopo la firma?
L'API delle firme di PDFium non può mostrare le modifiche post-firma perché legge solo il dizionario della firma: /Contents, /ByteRange, /SubFilter e il valore di permesso DocMDP. PDFium non ha un grafo di revisioni incrementali, non parsa i parametri di transform FieldMDP, e non offre alcun diff a livello di oggetto tra revisioni, quindi l'analizzatore in FPdfPades.pas lavora direttamente sui byte grezzi. Questo ha una conseguenza pratica da tenere in conto nel progetto. TPdf.AnalyzeSignatureRevisions legge i byte trattenuti al caricamento del documento, mai una copia prodotta da SaveAs, perché un file riscritto ha perso proprio la struttura di revisioni che si sta analizzando. Se il documento arriva da una sorgente progressiva che non ha finito di scaricarsi, il report restituisce SourceStatus = pvssIncomplete e Status = prasIndeterminate invece di analizzare un file troncato
Ricostruire i confini di revisione da startxref, stream xref e /Prev
L'analizzatore ricostruisce i confini di revisione seguendo ogni startxref a ritroso attraverso tabelle xref classiche, stream di riferimenti incrociati, voci /XRefStm ibride e la catena /Prev, come definito per gli aggiornamenti incrementali in ISO 32000-1 §7.5.6 e §7.5.8. La lunghezza coperta da ciascuna firma è la fine del suo secondo span ByteRange, e l'analizzatore mappa quella lunghezza sulla revisione la cui sezione xref la contiene. Quando nessuna revisione combacia, la firma riceve prrCoveredRevisionNotFound e uno stato Indeterminate. Lo stato di ogni oggetto viene poi riprodotto fino alla revisione coperta, e ogni voce xref successiva viene confrontata con quello stato. Conta più di quanto sembri: alcuni writer riscrivono la tabella xref completa a ogni salvataggio incrementale, e una voce che punta ancora allo stesso oggetto immutato viene saltata invece che segnalata come modifica. Senza quel confronto, un riempimento di modulo perfettamente legittimo annegherebbe in centinaia di false modifiche
Le definizioni shadow sono il caso che merita più attenzione. Un corpo di oggetto che compare dentro il range di byte di una revisione successiva ma non è referenziato dall'xref di quella revisione è invisibile a un viewer normale, eppure è esattamente il tipo di messa in scena su cui contano gli shadow attack: il contenuto nascosto viene piantato prima o dopo la firma e attivato in seguito ribaltando un riferimento. AnalyzePadesSignatureRevisionsBytes registra un simile oggetto come modifica non autorevole con IsAuthoritative = False, lo valuta prdSuspicious a prescindere dal livello di permesso, e aggiunge prrUnreferencedObjectDefinition all'insieme dei rischi. Due rischi correlati coprono altri trucchi strutturali: prrDuplicateObjectDefinition scatta quando una sezione xref elenca lo stesso oggetto più di una volta, e prrSignatureObjectRedefined scatta quando una revisione successiva ridefinisce un oggetto firma esistente
uses
SysUtils, TypInfo, PDFium, FPdfPades;
const
ShadowTag: array[Boolean] of string = ('', ' (shadow)');
var
Pdf: TPdf;
Report: TPadesRevisionAnalysisReport;
i, j: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-returned.pdf';
Pdf.Active := True;
Report := Pdf.AnalyzeSignatureRevisions;
Writeln('Revisions: ', Report.RevisionCount,
' Signatures: ', Report.SignatureCount,
' Overall: ', GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus),
Ord(Report.Status)));
for i := 0 to High(Report.Signatures) do
with Report.Signatures[i] do
begin
Writeln(Format('Signature %d covers revision %d, %d later, P=%d, FieldMDP=%s',
[SignatureIndex, CoveredRevisionIndex, LaterRevisionCount,
DocMdpPermission,
GetEnumName(TypeInfo(TPadesFieldMdpAction), Ord(FieldMdpAction))]));
for j := 0 to High(Changes) do
Writeln(Format(' rev %d obj %d %s -> %s%s',
[Changes[j].RevisionIndex, Changes[j].ObjectNumber,
GetEnumName(TypeInfo(TPadesRevisionChangeKind), Ord(Changes[j].Kind)),
GetEnumName(TypeInfo(TPadesRevisionDecision), Ord(Changes[j].Decision)),
ShadowTag[not Changes[j].IsAuthoritative]]));
end;
finally
Pdf.Free;
end;
end;
Come vengono fatti rispettare DocMDP e FieldMDP per ogni firma?
DocMDP e FieldMDP vengono fatti rispettare separatamente per ogni firma, alla revisione coperta di quella stessa firma, così una firma di certificazione e una successiva firma di approvazione nello stesso file possono arrivare a verdetti diversi sulla stessa modifica. Ogni oggetto successivo viene prima classificato in un TPadesRevisionChangeKind dalle sue voci /Type, /Subtype e /FT e dal ruolo che gioca nei grafi di pagina, modulo, annotazione e DSS. Qualsiasi cosa porti /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia o /EmbeddedFile diventa prckActiveContent. La decisione poi segue ISO 32000-1 §12.8.2.2: con P=1 è vietato tutto tranne i dati di riferimenti incrociati e il materiale di validazione; P=2 consente il riempimento di moduli e ulteriori firme ma respinge le modifiche alle annotazioni; P=3 consente anche le annotazioni. Contenuto di pagina, struttura del documento, metadati, contenuto attivo e oggetti cancellati sono vietati a qualsiasi livello DocMDP, e valutati prdSuspicious quando la firma non porta alcun DocMDP, visto che una firma di approvazione non vieta formalmente nulla ma il lettore non vede più ciò che era stato firmato
FieldMDP, da ISO 32000-1 §12.8.2.4, stringe ulteriormente la decisione sui campi del modulo. pfmaAll blocca ogni campo, pfmaInclude blocca solo i campi elencati, e pfmaExclude blocca tutto tranne i campi elencati. Per applicare Include o Exclude, l'analizzatore risolve ogni campo modificato al suo nome pienamente qualificato attraverso la catena /Parent e lo confronta con la lista di blocco per match esatto, quindi elencate i nomi dei campi terminali invece di aspettarvi che un nome padre copra i figli. Quando un nome non si può risolvere o la transform usa un'azione che il parser non riconosce, la modifica diventa prdIndeterminate e viene sollevato prrFieldMdpUnresolved. Le decisioni per modifica poi si aggregano col peggiore al primo posto, con Suspicious sopra Disallowed, Disallowed sopra Indeterminate, e Indeterminate sopra Allowed, così un solo oggetto shadow pesa più di un numero qualsiasi di legittimi aggiornamenti di campi
Perché alcune modifiche tornano Indeterminate invece che sicure?
Le modifiche tornano Indeterminate ogni volta che l'analizzatore non riesce a dimostrare che una modifica è permessa, perché in un controllo di firma un'incognita non deve mai essere riportata come consentita. Un caso comune invece viene gestito con precisione: la validazione a lungo termine aggiunge un /DSS e riscrive il catalogo, cosa che altrimenti conterebbe come modifica strutturale sotto P=1. L'analizzatore toglie /DSS e /Extensions dai dizionari di catalogo vecchio e nuovo e confronta il resto; quando nulla altro differisce, la riscrittura è trattata come aggiornamento di materiale di validazione e permessa, così l'augmentation B-LT e B-LTA non rompe una firma di certificazione. Altri varchi restano aperti di proposito. Le voci di tipo 2 in uno stream di riferimenti incrociati puntano dentro object stream compressi, e l'analizzatore non espande gli object stream dentro questo confine di sicurezza, quindi quelle modifiche emergono come prckCompressedObject con prrCompressedObjectUnresolved, vietate sotto P=1 e Indeterminate altrimenti. Budget rigidi di 1024 revisioni, 1.000.000 di numeri di oggetto e 2.000.000 di modifiche riportate producono prrResourceLimitExceeded, e una catena xref rotta produce prrMalformedRevisionChain; entrambi finiscono come Indeterminate, mai come un passaggio
const
StructuralRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
prrSignatureObjectRedefined, prrResourceLimitExceeded];
function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
// Alcuni rischi vengono registrati senza cambiare Status, quindi testateli prima
if R.Risks * StructuralRisks <> [] then
Exit('review: structural risk in the revision chain');
case R.Status of
prasNoLaterChanges: Result := 'accept: nothing was added after signing';
prasAllowed: Result := 'accept: every later change is permitted';
prasDisallowed: Result := 'reject: a change violates DocMDP or FieldMDP';
prasSuspicious: Result := 'reject: shadow or unconstrained content change';
prasIndeterminate: Result := 'review: the analyzer could not decide';
else
Result := 'not checked: no signatures or no original bytes';
end;
end;
L'ordinamento di quel cancello è deliberato. prrDuplicateObjectDefinition viene aggiunto all'insieme dei rischi senza declassare da solo Status, e una transform FieldMDP non parsabile tocca lo stato solo quando un campo del modulo cambia davvero, quindi un cancello che guarda solo Status può perdere prove che il report contiene già. Tenete presente anche ciò che il report non rivendica. TPadesRevisionAnalysisReport non dice nulla sulla validità crittografica della firma CMS o sul fatto che il certificato del firmatario abbia una catena fino a una root di cui vi fidate. L'analisi delle revisioni risponde alla domanda su cosa è successo dopo la firma, e sta accanto alla validazione strutturale e di fiducia, non al loro posto
Scrivere seed value e lock MDP al momento della firma
Le stesse regole si possono scrivere al momento della firma tramite TPadesSignatureFieldOptions, che è il membro FieldOptions sia di TPadesSignOptions sia di TPadesRemoteSignOptions. PDFium sa creare un widget ma non sa scrivere /SV, /Lock, una transform FieldMDP o DocMDP, o il dizionario /Perms del catalogo, quindi il writer PAdES incrementale dello stesso componente produce questi oggetti dentro lo stesso aggiornamento xref della firma. FieldName imposta il nome del campo radice, RequiredSeedValues diventa i bit /Ff del dizionario di seed value descritto in ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations e AcceptableCertificates vincolano ciò che un firmatario successivo può scegliere, LockAction con LockFields scrive un /SigFieldLock indiretto, e CertificationPermission da 1 a 3 trasforma la firma in una firma di certificazione. Le transform DocMDP e FieldMDP vanno entrambe in un unico array /Reference sul valore della firma, ciascuna con /Data che punta al catalogo
var
Options: TPadesSignOptions;
begin
Options := TPadesSignOptions.Default;
Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
Options.Reason := 'Approved for release';
Options.FieldOptions.FieldName := 'Certification';
Options.FieldOptions.CertificationPermission := 2; // solo riempimento modulo e firma
Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
Options.FieldOptions.LockAction := pfmaInclude; // blocca solo questi campi
SetLength(Options.FieldOptions.LockFields, 2);
Options.FieldOptions.LockFields[0] := 'Total';
Options.FieldOptions.LockFields[1] := 'IBAN';
if not Pdf.SignPades('contract-certified.pdf', Options) then
Writeln('Signing failed');
end;
Qualche dettaglio è facile sbagliarlo se ve lo scrivete a mano. Il /Perms /DocMDP del catalogo deve referenziare il dizionario del valore della firma, non l'annotazione widget, e il writer tiene il valore della firma come oggetto indiretto a sé per quella ragione. Un dizionario /Perms esistente può già contenere diritti d'uso /UR3, quindi il writer lo copia e vi inserisce /DocMDP invece di sostituirlo, seguendo il dizionario dei permessi in ISO 32000-1 §12.8.4. Un documento che porta già /DocMDP rifiuta una seconda firma di certificazione con EPadesCrypto, e altrettanto fanno le opzioni incoerenti: un lock Include o Exclude senza nomi di campo, un lock All con una lista di campi, un'attestazione legale su una firma non di certificazione, o un punto nel nome del campo radice. La firma remota aggiunge un'ulteriore regola, perché il certificato firmatario è sconosciuto quando PreparePadesRemoteSignature gira: impostare lì CertificateRequired esige una lista esplicita AcceptableCertificates, mentre la firma locale può ripiegare sul certificato firmatario risolto
L'analisi delle revisioni completa la cassetta degli attrezzi delle firme invece di sostituirne una parte. Partite da ispezionare le firme PDF e i livelli PAdES con il PDFium Component per leggere il dizionario e il livello di base, guardate perché i validatori respingono le firme PAdES per i fallimenti strutturali che vengono prima di qualsiasi domanda sulle revisioni, e integrate il verdetto in un più ampio audit dei rischi di sicurezza PDF accanto ai controlli su JavaScript e file incorporati. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions e il writer PAdES incrementale mostrato qui viaggiano con il PDFium Component per Delphi, C++Builder e Lazarus