Articol tehnic

De ce resping validatoarele semnăturile PAdES: PDFium în Delphi

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