Un convalidatore rifiuta una firma PAdES per uno dei seguenti tre motivi nella quasi totalità dei casi che abbiamo analizzato con il Componente PDFium in Delphi: l'array /ByteRange contiene ancora i segnaposto a zero, l'orario di firma /M non è una stringa di data PDF ben formata, oppure un aggiornamento incrementale ha omesso la voce /Encrypt da un documento crittografato. Tutti e tre i problemi producono file che si aprono e vengono visualizzati correttamente, ma falliscono nel momento in cui un convalidatore conforme analizza il dizionario delle firme
Lo scenario alla base di questo articolo è estremamente specifico. Firmi un contratto seguendo la procedura descritta nell'articolo sulla firma PAdES B-B, il tuo codice di verifica indica che la firma è presente e strutturalmente integra, tutti i test locali danno esito positivo — e poi la controparte carica il file sulla propria piattaforma di convalida che restituisce un errore. Nulla di questo malfunzionamento è visibile in un visualizzatore, poiché nessuno di questi tre difetti influisce sul contenuto della pagina. Risiedono interamente nel dizionario della firma e nel trailer del file, esattamente dove i convalidatori controllano e i visualizzatori solitamente no
Perché il ByteRange della mia firma PDF non è valido?
Un rifiuto dovuto a ByteRange indica quasi sempre che i quattro campi non sono mai stati compilati, e non che gli intervalli siano leggermente errati. L'array /ByteRange [A B C D] dichiara due intervalli, dal byte A a A+B e dal byte C a C+D, e lo standard EN 319 142-1 §6.3 (requisito k) richiede che insieme essi coprano l'intero file ad eccezione della stringa /Contents codificata in esadecimale. In sintesi: A è pari a 0, C >= A+B, C+D corrisponde alla lunghezza del file, e lo spazio tra B e C contiene esattamente la stringa esadecimale <...> — due byte di parentesi più due caratteri esadecimali per ciascun byte di dati CMS. Un convalidatore che legge [0 0 0 0] deduce che la firma non copra alcun dato e la rifiuta, a prescindere dalla correttezza della struttura CMS all'interno di /Contents
Il meccanismo con cui si verifica questo comportamento merita di essere compreso poiché lo stesso schema si ripresenta in ogni processo di firma. Chi firma non può conoscere gli offset finali finché il file non viene strutturato, per cui il modulo di scrittura genera l'array con segnaposto a zero di larghezza fissa e li compila successivamente. Nelle versioni del Componente PDFium antecedenti alla 2.14.1, tale compilazione successiva cercava lo schema dei segnaposto a partire dalla posizione di /Contents — ma /ByteRange precede /Contents nel dizionario, per cui la ricerca non produceva risultati e le tre sostituzioni fallivano silenziosamente. Il digest CMS veniva calcolato sugli intervalli corretti, per cui la crittografia era valida; la dichiarazione di tali intervalli rimaneva tuttavia a zero, spingendo ogni convalidatore conforme a rifiutare il file. Anche le marcature temporali del documento PAdES B-LTA, che utilizzano la stessa struttura di dizionario, fallivano nello stesso modo — dettaglio rilevante se utilizzi le firme a lungo termine B-LT e B-LTA. La versione 2.14.1 esegue la compilazione successiva a partire dall'inizio dell'oggetto firma, e la correzione è coperta da un test di regressione che analizza il file prodotto e verifica i calcoli sotto descritti
function SignedByteRangeCoversFile(const FileName: string): Boolean;
var
Raw: TBytes;
Text: AnsiString;
P, N: Integer;
F: array[0..3] of Int64;
begin
Raw := TFile.ReadAllBytes(FileName);
SetString(Text, PAnsiChar(@Raw[0]), Length(Raw));
P := Pos('/ByteRange', Text); // first signature only
Result := P > 0;
if not Result then Exit;
Inc(P, Length('/ByteRange'));
for N := 0 to 3 do
begin
while (P <= Length(Text)) and not (Text[P] in ['0'..'9']) do Inc(P);
F[N] := 0;
while (P <= Length(Text)) and (Text[P] in ['0'..'9']) do
begin
F[N] := F[N] * 10 + Ord(Text[P]) - Ord('0');
Inc(P);
end;
end;
// EN 319 142-1 §6.3 req k: spans cover everything except /Contents
Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
(F[2] + F[3] = Int64(Length(Raw)));
end;
Venti righe di codice RTL semplice, senza chiamate a librerie esterne, in grado di rilevare l'intera classe di errori al momento della generazione. Se desideri conservare una sola verifica da questo articolo, scegli questa: analizza il tuo output firmato e controlla le tre uguaglianze prima che il file lasci l'applicazione
Perché i convalidatori segnalano l'orario di firma /M come non valido?
I convalidatori più rigorosi rifiutano un orario di firma che non corrisponda a una stringa di data PDF completa, e i due elementi che mancano più frequentemente sono il prefisso D: e il marcatore di offset UTC. Lo standard ISO 32000-1 §7.9.4 definisce il formato della data come D:YYYYMMDDHHmmSS seguito da un offset — Z per UTC, o un valore con segno +HH'mm' che definisce la relazione con esso. Una stringa semplice come 20260709143000 viene interpretata come data da un lettore non restrittivo, ma un convalidatore che applica la grammatica alla lettera rileva una stringa non conforme in un campo a formato obbligatorio e segnala un errore nella firma. Le versioni del Componente PDFium antecedenti alla 2.14.4 scrivevano la voce /M di TPdf.SignPades e SignPadesBytes proprio in tale forma semplice; a partire dalla 2.14.4 la voce include il prefisso D: e il marcatore Z, per cui l'orario di firma dichiarato risulta D:20260709143000Z
A questo campo si associano due note pratiche. In primo luogo, scrivi in UTC e indicalo chiaramente: un timestamp privo di indicatore di offset costringe il convalidatore a ipotizzare il fuso orario, e la sezione §7.9.4 tratta l'offset como parte integrante del formato piuttosto che come opzione facoltativa. In secondo luogo, ricorda cos'è la voce /M — l'orario dichiarato da chi firma, un'asserzione e non una prova. Un convalidatore ne verifica la formattazione, non la veridicità; un orario verificabile è fornito da una marca temporale RFC 3161 a livello PAdES B-T e superiori. Formattare correttamente un campo e ritenerlo attendibile sono decisioni separate, e i convalidatori applicano solo la prima
Perché SignPades solleva EPadesCrypto su un PDF crittografato?
Il Componente PDFium rifiuta di firmare un documento crittografato poiché l'alternativa consisterebbe nel generare un file che i lettori conformi considereranno danneggiato all'apertura. Una firma PAdES viene aggiunta come aggiornamento incrementale, e lo standard ISO 32000-1 §7.5.6 richiede che il nuovo trailer di una sezione di aggiornamento contenga ogni voce del trailer precedente ad eccezione di /Prev — che in un documento crittografato include /Encrypt. Se si omette questa voce, il trailer più recente dichiarerà il file come non crittografato, per cui un lettore conforme interpreterà il corpo crittografato come testo normale, ottenendo dati corrotti. Inoltre, anche i flussi e le stringhe aggiunti da chi firma dovrebbero essere crittografati con la chiave del documento per essere validi, operazione che un iniettore di firme in formato testo semplice non può eseguire. Non è possibile aggiungere una firma in testo normale valida a un file crittografato, per cui a partire dalla versione 2.14.2 i metodi TPdf.SignPades, SignPadesBytes e InjectPadesDssMarkers sollevano l'eccezione EPadesCrypto anziché produrre un risultato corrotto o non verificabile
try
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
Writeln('Signing reported failure');
except
on E: EPadesCrypto do
begin
// e.g. 'SignPadesBytes: the source document is encrypted;
// remove encryption before signing'
Writeln('Cannot sign: ', E.Message);
end;
end;
L'eccezione rappresenta l'esito corretto, per cui si consiglia di strutturare il flusso di lavoro di conseguenza, anziché intercettare l'errore e riprovare. Decodifica prima il file con i permessi del proprietario, firma la copia in testo normale e, se il canale di distribuzione richiede la crittografia, considera che crittografare prima della firma o firmare prima della crittografia produce elementi diversi con differenti procedure di convalida. Una firma calcolata su byte in testo normale non può sopravvivere alla successiva crittografia di tali byte, per cui le opzioni praticabili sono un file in testo normale firmato o una scelta di criteri documentata accanto al codice
In che modo due bug possono convalidarsi a vicenda
Il difetto di ByteRange è rimasto nascosto a lungo poiché il nostro stesso convalidatore presentava un errore complementare, e questa rappresenta la lezione più importante di questo articolo. Il controllo di copertura in ValidatePadesCompliance richiedeva che il secondo intervallo iniziasse esattamente a A+B — definendo uno spazio vuoto pari a zero — il che interpretava in modo errato la struttura standard in cui lo spazio ospita la stringa esadecimale /Contents. Di conseguenza, il convalidatore interno rifiutava proprio la struttura conforme e il generatore interno non la produceva mai, mantenendo i test end-to-end apparentemente corretti. Ciascun bug impediva alla suite di test di rilevare il comportamento anomalo che avrebbe esposto l'altro errore. La versione 2.14.1 ha corretto entrambi i lati nella stessa release: il generatore compila tutti e quattro i campi, e il convalidatore accetta C >= A+B con C+D pari alla lunghezza del file
La correzione metodologica risiede nella convalida incrociata rispetto a un'implementazione esterna. Un generatore e un convalidatore che condividono la stessa base di codice, lo stesso autore o lo stesso modello concettuale del formato possono concordare indefinitamente su un errore comune; un convalidatore indipendente interrompe questa simmetria. Sottoponi il tuo output firmato ad almeno un convalidatore di conformità esterno prima del rilascio, e mantieni una verifica strutturale automatica come primo filtro — TPdf.ValidatePades, descritto nell'articolo sull'ispezione delle firme, segnala il fallimento della copertura come un problema specifico
var
R: TPadesValidationResult;
begin
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
if ppeiByteRangeNotCoveringFile in R.Issues then
Writeln('ByteRange does not cover the file');
if not R.IsCompliant then
Writeln('Structural issues present: do not ship this file');
end;
Una lista di controllo in caso di rifiuto dell'output firmato
Quando una piattaforma rifiuta la tua firma PAdES, verifica gli aspetti strutturali più semplici prima di sospettare dei certificati o delle catene di attendibilità. Leggi l'array /ByteRange e verifica le tre uguaglianze: primo campo a zero, secondo intervallo che inizia in corrispondenza o dopo la fine del primo, e secondo intervallo che termina esattamente con la fine del file. Leggi la voce /M e verifica che corrisponda a una stringa di data completa ai sensi di §7.9.4, completa di prefisso D: e di indicatore di offset. Conferma che il documento sorgente non fosse crittografato quando la firma è stata inserita — e se la tua libreria lo ha firmato comunque senza segnalare errori, considera questa mancanza come un bug. Tutti e tre i controlli vengono eseguiti sui byte grezzi in pochi millisecondi e, in base alla nostra esperienza, spiegano il rifiuto molto più frequentemente di qualsiasi causa legata alla crittografia
Le chiamate di firma, ispezione e convalida utilizzate qui — SignPades, ValidatePades e i controlli di conformità PAdES con i relativi calcoli ByteRange corretti, la formattazione della data e la gestione della crittografia — sono fornite con il Componente PDFium per Delphi, C++Builder e Lazarus