Un validator respinge o semnătură PAdES din unul dintre cele trei motive în aproape toate cazurile pe care le-am depanat cu PDFium Component în Delphi: tabloul /ByteRange conține în continuare înlocuitori cu zero (placeholders), ora semnării /M nu este un șir de dată PDF corect format, sau o actualizare incrementală a eliminat intrarea /Encrypt dintr-un document criptat. Toate cele trei produc fișiere care se deschid corect, se randează fără probleme, dar eșuează în momentul în care un validator conform citește dicționarul semnăturii
Scenariul care motivează acest articol este foarte specific. Semnați un contract utilizând fluxul de lucru din articolul tentang semnarea PAdES B-B, codul dvs. de inspecție raportează că semnătura este prezentă și corectă structural, toate testele locale sunt pe verde — iar apoi cealaltă parte încarcă fișierul pe platforma lor de validare și primește o eroare (red cross). Nimic din acest eșec nu este vizibil într-un vizualizator, deoarece niciunul dintre aceste trei defecte nu afectează conținutul paginii. Ele trăiesc exclusiv în dicționarul semnăturii și în trailer-ul fișierului, adică exact acolo unde caută validatoarele și unde vizualizatoarele, de regulă, nu caută
De ce este nevalid intervalul ByteRange al semnăturii mele PDF?
O respingere pe bază de ByteRange înseamnă aproape întotdeauna că cele patru câmpuri nu au fost completate niciodată, nu că intervalele sunt greșite în mod subtil. Tabloul /ByteRange [A B C D] declară două intervale, de la octetul A la A+B și de la C la C+D, iar standardul EN 319 142-1 §6.3 (cerința k) impune ca împreună să acopere întregul fișier, cu excepția șirului hexazecimal /Contents. În cifre: A este 0, C >= A+B, C+D este egal cu lungimea fișierului, iar spațiul dintre B și C conține exact șirul hexazecimal <...> — doi octeți de paranteze plus două caractere hexazecimale pentru fiecare octet de date CMS. Un validator care citește [0 0 0 0] concluzionează că semnătura nu acoperă nimic și o respinge, indiferent de cât de corectă este structura CMS din interiorul /Contents
Mecanismul prin care se întâmplă acest lucru merită înțeles deoarece același model există în orice sistem de semnare. Un program de semnare nu poate cunoaște offset-urile finale până când fișierul nu este structurat, așa că scrie tabloul cu înlocuitori (placeholders) de lungime fixă cu valoarea zero și îi completează după configurarea structurii. În versiunile PDFium Component anterioare 2.14.1, acea completare căuta modelul înlocuitorului pornind de la poziția /Contents — însă /ByteRange se află înaintea /Contents în dicționar, deci căutarea nu găsea nimic și toate cele trei înlocuiri eșuau în mod silențios. Rezumatul CMS era calculat pe intervalele corecte, deci criptografia era validă; declarația acelor intervale rămânea însă la zero, astfel încât orice validator conform respingea fișierul. Mărcile temporale de document PAdES B-LTA (Document Time-stamps), care utilizează aceeași structură de dicționar, eșuau în același mod — aspect relevant dacă vă bazați pe semnăturile pe termen lung B-LT și B-LTA. Versiunea 2.14.1 face completarea începând de la startul obiectului semnăturii, iar remedierea este acoperită de un test de regresie care analizează fișierul produs și verifică logica de mai jos
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); // doar prima semnătură
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: intervalele acoperă tot, cu excepția /Contents
Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
(F[2] + F[3] = Int64(Length(Raw)));
end;
De ce marchează validatoarele ora semnării /M ca fiind malformată?
Validatoarele stricte resping o oră a semnării care nu este un șir complet de dată PDF, iar cele două elemente care lipsesc cel mai des sunt prefixul D: și marcajul de offset UTC. ISO 32000-1 §7.9.4 definește formatul datei ca D:YYYYMMDDHHmmSS urmat de un offset — Z pentru UTC, sau o relație semnată +HH'mm' cu acesta. Un simplu 20260709143000 este citit ca dată de un parser permisiv, dar un validator care aplică gramatica în mod literal observă un șir malformat într-un câmp de format obligatoriu și respinge semnătura. Versiunile PDFium Component anterioare 2.14.4 scriau intrarea /M din TPdf.SignPades și SignPadesBytes exact sub acea formă simplă; începând cu 2.14.4, intrarea conține prefixul D: și marcajul Z, astfel încât ora declarată a semnării citește D:20260709143000Z
Două note practice se asociază acestui câmp. În primul rând, scrieți ora în format UTC și specificați acest lucru: o marcă temporală fără un marcaj de offset obligă validatorul să ghicească zona orară, iar §7.9.4 stabilește offset-ul ca parte a formatului, nu ca pe o simplă opțiune. În al doilea rând, rețineți ce reprezintă /M — ora declarată a semnată, o simplă afirmație și nu o dovadă. Un validator verifică formatul acesteia aici, nu veridicitatea ei; ora dovedită provine dintr-o marcă temporală RFC 3161 la PAdES B-T și versiunile ulterioare. Formatarea corectă a unui câmp și încrederea în el reprezintă decizii separate, iar validatoarele o impun doar pe prima
De ce generează SignPades eroarea EPadesCrypto pe un PDF criptat?
PDFium Component refuză să semneze un document criptat deoarece alternativa ar însemna producerea unui fișier pe care cititoarele conforme îl vor corupe la deschidere. O semnătură PAdES este adăugată ca o actualizare incrementală, iar ISO 32000-1 §7.5.6 cere ca noul trailer al unei secțiuni de actualizare să conțină fiecare intrare a trailer-ului anterior, cu excepția /Prev — într-un document criptat, aceasta include /Encrypt. Dacă o omiteți, ultimul trailer declară fișierul ca fiind necriptat, astfel încât un cititor conform analizează corpul criptat ca text simplu și obține date corupte. Mai rău, fluxurile și șirurile pe care le adaugă semnatarul ar trebui la rândul lor să fie criptate cu cheia documentului pentru a fi valide, lucru pe care un injector de semnătură în text simplu nu îl poate face. Nu există nicio modalitate de a adăuga o semnătură validă în text simplu la un fișier criptat, așa că începând cu versiunea 2.14.2, TPdf.SignPades, SignPadesBytes și InjectPadesDssMarkers generează eroarea EPadesCrypto în loc de a emite un rezultat corupt sau neverificabil
try
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
Writeln('Signing reported failure');
except
on E: EPadesCrypto do
begin
// ex. 'SignPadesBytes: documentul sursă este criptat;
// eliminați criptarea înainte de semnare'
Writeln('Cannot sign: ', E.Message);
end;
end;
Cum se pot valida reciproc două erori
Defectul ByteRange a rămas ascuns atât de mult timp deoarece propriul nostru validator era greșit într-un mod complementar, iar aceasta este cea mai importantă lecție din acest articol. Verificarea acoperirii din ValidatePadesCompliance cerea ca al doilea interval să înceapă exact la A+B — o diferență de zero —, ceea ce clasifică greșit structura standard în care spațiul conține șirul hexazecimal /Contents. Astfel, validatorul intern respingea exact structura conformă, iar generatorul intern nu o producea niciodată, conductele end-to-end de tip semnare-apoi-validare rămânând pe verde. Fiecare eroare a privat suita de teste de contraexemplul care ar fi expus-o pe cealaltă. Versiunea 2.14.1 a remediat ambele părți în aceeași versiune: generatorul completează acum toate cele patru câmpuri, iar validatorul acceptă C >= A+B cu C+D egal cu lungimea fișierului
Rezolvarea metodologică o reprezintă validarea încrucișată (cross-validation) cu o implementare pe care nu ați scris-o dvs. Un generator și un validator care partajează aceeași bază de cod, același autor sau doar un model mental al formatului pot fi de acord cu o interpretare greșită comună la nesfârșit; un validator independent rupe această simetrie. Treceți rezultatul semnat prin cel puțin un verificator extern de conformitate înainte de lansare și păstrați o auto-verificare structurală în sistem ca primă linie — TPdf.ValidatePades, descris în articolul despre inspecția semnăturilor, raportează eroarea de acoperire ca o problemă explicită
var
R: TPadesValidationResult;
begin
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
if ppeiByteRangeNotCoveringFile in R.Issues then
Writeln('ByteRange nu acoperă fișierul');
if not R.IsCompliant then
Writeln('Probleme structurale prezente: nu lansați acest fișier');
end;
O listă de verificare a respingerilor pentru fișierele semnate
Când o platformă respinge semnătura dvs. PAdES, verificați cauzele structurale simple înainte de a suspecta certificatele sau lanțurile de încredere. Citiți tabloul /ByteRange și verificați cele trei egalități: primul câmp zero, al doilea interval începând la sau după sfârșitul primului și al doilea interval terminându-se exact la sfârșitul fișierului. Citiți intrarea /M și verificați dacă este un șir complet de dată conform §7.9.4, cu prefixul D: și un marcaj de offset. Confirmați că documentul sursă nu a fost criptat când s-a adăugat semnătura — iar dacă sistemul dvs. l-a semnat oricum fără probleme, tratați acea tăcere ca pe o eroare în sistem. Toate cele trei verificări rulează pe octeții bruți în câteva milisecunde și, în experiența noastră, explică respingerea mult mai des decât orice cauză criptografică
Apelurile de semnare, inspecție și validare folosite aici — SignPades, ValidatePades și verificările de conformitate PAdES cu aritmetica corectată ByteRange, formatarea datei și oprirea la criptare — sunt livrate împreună cu PDFium Component pentru Delphi, C++Builder și Lazarus