Articol tehnic

Inspectarea semnăturilor PDF și a nivelurilor PAdES în Delphi

Ați primit un PDF semnat și trebuie să afișați, în vizualizatorul dvs., cine l-a semnat, când a fost semnat, dacă semnătura acoperă întregul fișier și în ce măsură respectă conformitatea pe termen lung. Pachetul PDFium Component pentru Delphi și Lazarus răspunde la toate cele patru întrebări prin apeluri read-only: FPDF_GetSignatureCount și familia FPDFSignatureObj_* expun dicționarul de semnătură, iar TPdf.ValidatePades clasifică nivelul de bază PAdES. Acesta este primul din cele trei articole dedicate semnăturilor PDF cu PDFium; următoarele două acoperă crearea unei semnături B-B și adăugarea de timestamp-uri pe termen lung. O limitare trebuie menționată de la început: tot ce este prezentat aici reprezintă inspecție, iar citirea informațiilor pe care le declară o semnătură reprezintă o sarcină diferită de verificarea criptografică sau de decizia de a acorda încredere semnatarului

De ce dicționarul unei semnături PDF nu este doar o înșiruire de octeți

O semnătură PDF este un dicționar, nu un atașament opac, iar cele mai importante două intrări ale sale indică ce parte din fișier este protejată efectiv. Standardul ISO 32000-1 §12.8 definește dicționarul de semnătură prin intrările /ByteRange și /Contents. Intrarea /Contents conține o structură CMS SignedData codificată hexazecimal (RFC 5652), plicul criptografic care transportă certificatul semnatarului, atributele semnate și valoarea semnăturii propriu-zise. Intrarea /ByteRange este partea pe care dezvoltatorii o subestimează: este un tablou format din două intervale offset-lungime care acoperă împreună întregul fișier, cu excepția șirului hexazecimal din /Contents. Această diferență reprezintă exact locul în care sunt plasați octeții semnăturii, iar cele două intervale laterale reprezintă conținutul pe care semnătura îl garantează

Structura ByteRange este cea care face ca o salvare incrementală să poată fi verificată. Deoarece un semnatar nu poate genera hash-ul pentru octeții de semnătură care nu există încă, fișierul este divizat în jurul substituentului /Contents, iar restul fișierului este inclus în hash-ul semnăturii. O semnătură al cărei ByteRange nu ajunge la sfârșitul fișierului este un semn de avertizare: conținutul adăugat după intervalul acoperit, printr-o salvare incrementală ulterioară, nu va anula semnătura, deși modifică ceea ce vede cititorul. Prin urmare, primul lucru pe care îl verifică un inspector nu este identitatea semnatarului, ci dacă semnătura acoperă octeții pe care pare să îi garanteze

Citirea dicționarului de semnătură cu API-ul read-only al PDFium

Componenta PDFium expune dicționarul de semnătură prin doi membri read-only: SignatureCount și înregistrarea Signature[Index]. În fundal, aceștia apelează FPDF_GetSignatureCount, FPDF_GetSignatureObject și metodele de acces FPDFSignatureObj_* pentru /SubFilter, /ByteRange, /Contents, /Reason și timpul de semnare. Conceptul „read-only” este esențial: PDFium poate enumera și citi semnăturile, dar nu are un API pentru crearea sau scrierea uneia, motiv pentru care procesul de semnare descris în continuare este implementat direct de bibliotecă, nu de 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);            // signer date string, e.g. D:20260708120000+02'00'
      Writeln('Reason    : ', Sig.Reason);
      Writeln('CMS length: ', Length(Sig.Content)); // raw DER SignedData taken from /Contents
      Writeln('DocMDP    : ', Sig.Permission);      // 0 = no certification, 1..3 = MDP level
    end;
  finally
    Pdf.Free;
  end;
end;

Care este diferența dintre PAdES B-B, B-T, B-LT și B-LTA?

Cele patru niveluri de bază PAdES formează o ierarhie de la o semnătură minim validă până la una concepută să reziste decenii în arhivă, fiecare nivel incluzând strict caracteristicile celui inferior. Standardul ETSI EN 319 142-1 le definește ca B-B, B-T, B-LT și B-LTA. Nivelul B-B (Basic) reprezintă semnătura plus atributele semnate obligatorii. Nivelul B-T (Timestamp) adaugă o marcă corporală (timestamp) de încredere conform RFC 3161 peste semnătură, astfel încât momentul semnării este atestat de o autoritate de marcare temporală, nu doar declarat de ceasul semnatarului. Nivelul B-LT (Long-Term) include materialul de validare — lanțul de certificate și, opțional, răspunsurile OCSP sau CRL — direct în fișier, permițând validarea semnăturii ani mai târziu, chiar și după ce infrastructura emitentă nu mai este activă. Nivelul B-LTA (Long-Term cu Timestamp de Arhivă) adaugă un timestamp de document peste aceste materiale, protejând datele pe termen lung și oferind posibilitatea de a reaplica mărci temporale înainte ca algoritmii criptografici să devină învechiți

În practică, diferența se referă la orizontul de timp. O semnătură B-B răspunde la întrebarea „a semnat cineva acest document”. Nivelul B-T răspunde „și când anume, în mod dovedit”. Nivelul B-LT răspunde „și dacă o mai pot verifica după expirarea certificatelor”. Nivelul B-LTA răspunde „și dacă această verificare va fi valabilă peste douăzeci de ani”. Profilurile de reglementare aleg un anumit nivel: multe contexte de facturare electronică și eIDAS impun cel puțin nivelul B-T, iar cerințele de arhivare solicită nivelurile B-LT sau B-LTA. Identificarea nivelului pe care îl atinge efectiv un document, înainte de a-l accepta sau respinge, reprezintă scopul acestei etape de inspecție

Detectarea nivelului de bază cu TPdf.ValidatePades

Componenta PDFium reduce întreaga analiză la un singur apel. TPdf.ValidatePades returnează o înregistrare TPadesValidationResult în care câmpul Level este de tip TPadesLevelplNone, plUnknown, plB_B, plB_T, plB_LT sau plB_LTA —, alături de un set de probleme, un contor de semnături și un contor de timestamp-uri de document. Nivelul este determinat incremental: validatorul stabilește mai întâi nivelul B-B, apoi îl promovează la B-T dacă există un timestamp de semnătură sau de document, la B-LT dacă registrul catalog conține o intrare /DSS cu certificate și marcatorul /Extensions /ESIC de Nivel 1 și la B-LTA dacă există atât un timestamp de document, cât și marcatorul ESIC de Nivel 2. Două funcții ajutătoare fac rezultatul ușor de utilizat: IsCompliant este True doar când nivelul atinge cel puțin B-B și setul de probleme este gol, iar IsCompliantAt vă permite să impuneți o limită de politică, cum ar fi 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('Meets the B-T policy floor')
    else
      Writeln('Below the required B-T level');
  finally
    Pdf.Free;
  end;
end;

De ce este adbe.pkcs7.sha1 un SubFilter interzis?

Deoarece algoritmul SHA-1 este nesigur, iar handlerul adbe.pkcs7.sha1 se bazează pe acesta. Acest SubFilter generează hash-ul documentului folosind SHA-1 înainte de a-l include în PKCS#7, iar SHA-1 este vulnerabil la coliziuni de mulți ani, motiv pentru care clauza 6.3 din standardul EN 319 142-1 îl interzice complet pentru o semnătură de bază. ValidatePades raportează ppeiForbiddenSubFilter când întâlnește adbe.pkcs7.sha1 sau adbe.x509.rsa_sha1 și raportează ppeiBadDigestAlgorithm când structura CMS folosește MD5 sau SHA-1 ca algoritm hash (clauza 6.2.1). Acestea sunt două verificări separate care identifică aceeași clasă de vulnerabilități la niveluri diferite

Setul de probleme conține 26 de membri în total, iar cele mai frecvente vizează structura și acoperirea. ppeiByteRangeNotCoveringFile reprezintă verificarea acoperirii descrisă anterior. ppeiForbiddenCertKey se declanșează când dicționarul de semnătură conține o intrare /Cert, interzisă de PAdES deoarece lanțul trebuie să fie inclus în SignedData.certificates din CMS. Indicatorii ppeiMissingSigningCertificate, ppeiMissingContentType și ppeiMissingMessageDigest semnalează absența unor atribute semnate obligatorii, iar ppeiDetachedContentViolation identifică o semnătură care include greșit conținutul semnat în loc să îl detașeze. Parcurgerea acestui set vă oferă un diagnostic clar pe care îl puteți stoca în jurnal

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;

Ce nu verifică ValidatePades

Metoda ValidatePades validează structura, nu încrederea (trust), iar confundarea acestor aspecte este o eroare periculoasă. Un rezultat plB_LTA înseamnă că documentul conține o semnătură B-LTA corect structurată, cu toate atributele, materialele și timestamp-urile necesare plasate corect — nu înseamnă că semnătura este validă criptografic, că lanțul de certificate duce la o rădăcină de încredere sau că niciun certificat din lanț nu a fost revocat. Validatorul nu efectuează în mod deliberat nicio verificare criptografică: nu recalculează semnătura peste ByteRange, nu construiește și nu evaluează lanțul de încredere și nu verifică starea de revocare prin OCSP sau CRL. Această diferențiere este intenționată și utilă, deoarece inspecția structurală este rapidă, deterministă și nu necesită chei criptografice, conexiune la rețea sau algoritmi de criptare ai platformei, permițând ca ValidatePades să ruleze identic pe Windows, Linux și macOS ca cod Pascal pur peste octeții fișierului. Validarea lanțului de încredere, în schimb, depinde direct de politicile specifice — ce rădăcini acceptați, cum obțineți informațiile de revocare, ce toleranțe de timp aveți — și aparține unei etape ulterioare dependente de depozitul de certificate al platformei; prin urmare, tratați succesul ValidatePades ca pe o confirmare obligatorie că semnătura are o structură corectă, urmând ca apoi să trimiteți semnătura validată structural către o verificare criptografică reală înainte de a vă baza pe ea

Această verificare structurală reprezintă punctul corect de pornire și se integrează perfect cu verificările read-only din analiza riscurilor de securitate PDF cu PDFium Component și cu verificările de conformitate a formatelor, cum ar fi validarea documentelor PDF/X gata de tipar. Odată ce puteți citi și clasifica o semnătură, pasul următor este crearea uneia: al doilea articol din această serie acoperă semnarea PDF-urilor cu PAdES B-B, iar al treilea extinde discuția la timestamp-uri de încredere și semnături pe termen lung B-LT și B-LTA. Modulul de inspecție read-only a semnăturilor și validatorul ValidatePades prezentate aici fac parte din pachetul PDFium Component pentru Delphi, C++Builder și Lazarus