Articol tehnic

Verificarea semnăturilor digitale PDF în Delphi cu HotPDF

HotPDF verifică semnăturile digitale din documentele PDF încărcate prin intermediul a trei metode THotPDF: GetLoadedSignatureInfo, VerifyLoadedSignature și VerifyLoadedSignatureEx, introduse în v2.259.0. Componenta recalculează hash-ul segmentelor /ByteRange din fișierul original, verifică atributul CMS messageDigest și execută o verificare RSA PKCS#1 v1.5 pe baza certificatului de semnatar încorporat, returnând svValid atunci când octeții documentului sunt intacți

Scenariul este unul comun, însă miza nu este deloc nesemnificativă. O altă parte returnează un contract semnat, fluxul dvs. de lucru trebuie să îl înregistreze și cineva pune singura întrebare care contează: este acesta documentul pe care l-am trimis, octet cu octet, semnat cu certificatul declarat? Răspunsul la această întrebare în cod reprezintă partea de verificare a poveștii semnăturii; partea de semnare, adică generarea și încorporarea semnăturilor PAdES, este acoperită în articolul asociat despre crearea semnăturilor digitale PAdES cu HotPDF. Acest articol se referă la cealaltă direcție: un PDF sosește deja semnat, iar dvs. doriți un verdict programatic, nu o captură de ecran cu marcajul verde din Acrobat

Cum dovedește un PDF semnat că nu a fost modificat neautorizat?

O semnătură PDF protejează intervale specifice de octeți din fișier, nu o noțiune abstractă de „document”. ISO 32000-1 §12.8 definește acest mecanism: câmpul formularului de semnătură conține un dicționar a cărui intrare /Contents deține un container CMS SignedData (RFC 5652) și al cărui tablou /ByteRange specifică regiunile exacte ale fișierului acoperite de semnătură, conform §12.8.1. Tabloul este o listă de perechi de decalaj și lungime, practic două segmente: tot ceea ce se află înainte de șirul hexazecimal /Contents și tot ceea ce se află după acesta. Valoarea semnăturii nu se poate acoperi pe sine, așa că fișierul este transpus în hash ocolind acel gol

Această structură are o consecință care definește întregul API: verificarea trebuie să ruleze hash pe octeții serializați originali, exact așa cum se află pe disc. Un model de obiecte analizat este inutil în acest scop, deoarece reserializarea chiar și a unui document neschimbat produce octeți diferiți. Prin urmare, HotPDF realizează verificarea pe baza fișierului sursă din care a fost încărcat documentul sau a unui flux TStream de octeți bruți pe care îl furnizați, niciodată pe baza reprezentării sale din memorie

Citirea metadatelor semnăturii înainte de realizarea oricărei verificări

Metoda GetLoadedSignatureInfo analizează dicționarul de semnătură și containerul său CMS fără a atinge niciun octet din document, ceea ce o face apelul inițial potrivit atunci când doriți doar să afișați cine a semnat și când. Câmpurile de semnătură sunt indexate de la 0 în ordinea câmpurilor de formular, iar GetLoadedSignatureFieldCount specifică numărul celor existente. Înregistrarea THPDFSignatureInfo returnată conține numele câmpului, /SubFilter, numele comun al certificatului semnatarului, numele distincte (DN) ale subiectului și emitentului, numărul de serie, datele de valabilitate, momentul semnării (din atributul semnat când este prezent, altfel din intrarea /M a dicționarului), numele algoritmului de digest și șirurile /Reason, /Location și /ContactInfo. Membrul său Status rămâne svNotVerified, o etichetă corectă pentru „analizat, neverificat”

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('signed-contract.pdf');
    for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
    begin
      Info := Pdf.GetLoadedSignatureInfo(I);
      Writeln('Field:     ', Info.FieldName);
      Writeln('Signer:    ', Info.SignerName);
      Writeln('Issuer:    ', Info.IssuerDN);
      Writeln('Algorithm: ', Info.HashAlgorithm);
      Writeln('SubFilter: ', Info.SubFilter);
    end;
  finally
    Pdf.Free;
  end;
end;

Rularea verificării criptografice

Metoda VerifyLoadedSignatureEx realizează verificarea completă a unui document încărcat din fișier și returnează înregistrarea cu informații completate într-un singur apel: redeschide fișierul sursă, rulează hash pe segmentele /ByteRange cu algoritmul de digest SignerInfo, compară rezultatul cu atributul semnat messageDigest (RFC 5652 §5.4), apoi verifică semnătura RSA pe baza recodificării DER SET a atributelor semnate. Când o semnătură nu conține atribute semnate, verificarea RSA se execută direct pe baza hash-ului documentului. Semnăturile acceptate sunt RSA PKCS#1 v1.5 cu rezumate SHA-1, SHA-256, SHA-384 sau SHA-512, acoperind subfiltrele adbe.pkcs7.detached și ETSI.CAdES.detached produse de instrumentele populare de semnare

var
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  Status := Pdf.VerifyLoadedSignatureEx(0, Info);
  case Status of
    svValid:
      if Info.CoversWholeDocument then
        Writeln('Valid; signature covers the whole file')
      else
        Writeln('Valid; file was extended after signing');
    svDigestMismatch:
      Writeln('Document bytes changed after signing');
    svSignatureInvalid:
      Writeln('RSA check failed over signed attributes');
    svUnsupportedAlgorithm:
      Writeln('Non-RSA key or unknown digest algorithm');
    svMalformed:
      Writeln('CMS container could not be parsed');
    svSourceUnavailable:
      Writeln('No source bytes; use the TStream overload');
  end;
end;

Merită știute două detalii de implementare, deoarece explică eșecuri care par misterioase din exterior. În primul rând, verificarea atributelor semnate este strictă în ceea ce privește codificarea: în interiorul fișierului, atributele sunt etichetate [0] IMPLICIT, însă semnătura a fost calculată pe baza formei lor DER SET OF, astfel încât verificatorul le reetichetează înainte de a rula hash, exact conform cerințelor RFC 5652 §5.4. Un verificator artizanal care rulează hash pe octeți exact așa cum apar aceștia în fișier va respinge orice document semnat corect. În al doilea rând, intrarea /Contents este prin convenție umplută cu zerouri (zero-padded) conform unui buget de octeți rezervat, astfel încât verificatorul trunchiază blob-ul DER la lungimea reală a secvenței sale exterioare SEQUENCE înainte de analiză; zerourile de umplere de la final sunt normale, nu reprezintă o corupere a datelor. Aceeași categorie de riscuri de analiză sintactică ASN.1, pe partea de import de certificate, face obiectul articolului despre securizarea PKCS#12 și ASN.1 în HotPDF

Ce garantează de fapt o semnătură validă?

Starea svValid înseamnă exact acest lucru: octeții specificați de /ByteRange corespund prin hash valorii semnate de semnatar, iar semnătura este validată sub cheia publică a certificatului încorporat în containerul CMS. Aceasta reprezintă integritatea octeților și asocierea cheii, nimic mai mult. Validarea lanțului de certificate și a încrederii se află în mod explicit în afara scopului verificatorului HotPDF: acesta nu parcurge lanțul până la o rădăcină, nu verifică revocarea și nu consultă nicio stocare de încredere. Un certificat auto-semnat al unui atacator care a resemnat un document modificat se va valida ca fiind svValid, deoarece calculele sunt coerente la nivel intern. Dacă semnatarul este cel care pretinde și dacă ar trebui să aveți încredere în el este o decizie de politică de securitate care aparține unui strat separat, fie că este vorba despre lista de certificate permise a organizației dvs., stocarea de certificate Windows sau o autoritate de validare

Marcajul CoversWholeDocument protejează împotriva unei probleme mai subtile. O semnătură acoperă întotdeauna doar intervalul său /ByteRange, iar mecanismul de actualizare incrementală al PDF-ului permite adăugarea de conținut după o semnătură fără a o invalida, lucru prevăzut prin proiectare care asigură funcționarea fluxurilor cu mai multe semnături. Marcajul este calculat în timpul verificării și are valoarea true doar atunci când cele două segmente plus spațiul /Contents acoperă întregul fișier. Când se returnează svValid cu CoversWholeDocument având valoarea false, versiunea semnată este intactă, însă fișierul conține adăugiri ulterioare, iar fluxul dvs. de lucru ar trebui să decidă dacă acceptă sau nu acele modificări

Documentele încărcate din flux și cele criptate au nevoie de proprii octeți sursă

Metodele fără parametri VerifyLoadedSignature și VerifyLoadedSignatureEx depind de memorarea de către componentă a fișierului din care provine documentul. Dacă încărcați documentul dintr-un flux, nu există niciun nume de fișier de redeschis; același lucru este valabil și după procesul de reîncărcare cu parolă utilizat pentru documentele criptate, flux de lucru descris în articolul despre criptarea PDF AES-256 în HotPDF. În ambele cazuri, supraîncărcările bazate pe fișiere returnează svSourceUnavailable în loc să facă presupuneri. Soluția este supraîncărcarea cu TStream, care vă permite să furnizați octeții bruți originali de unde i-ați stocat — un fișier pe care îl dețineți în continuare, un buffer de memorie sau un blob de bază de date

var
  Src: TFileStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // Stream-loaded document: the component holds no source
  // file name, so supply the original bytes yourself.
  Src := TFileStream.Create('signed-contract.pdf',
    fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignature(0, Src, Info);
    if Status <> svValid then
      Writeln('Verification failed: ', Ord(Status));
  finally
    Src.Free;
  end;
end;

Raportarea a ceea ce nu puteți verifica

Un verificator care recunoaște doar stările „valid” și „invalid” va raporta eronat documentele pe care pur și simplu nu le înțelege, astfel încât enumerarea stărilor separă cazurile pe care interfața dvs. de utilizator ar trebui să le distingă. Starea svDigestMismatch indică modificarea octeților documentului după semnare, semnalul clasic de alterare. Starea svSignatureInvalid înseamnă că octeții au hash-ul corect, însă verificarea RSA a eșuat, ceea ce indică o valoare a semnăturii coruptă sau falsificată. Starea svUnsupportedAlgorithm este răspunsul corect pentru cheile ECDSA și rezumatele necunoscute: semnătura poate fi perfect validă, însă HotPDF nu o poate verifica, iar raportarea acesteia ca „invalidă” ar discredita un document corect. Starea svMalformed semnalează un container CMS care nu a putut fi analizat sintactic deloc. Pentru verificări de tip poartă (gate), VerifyAllLoadedSignatures returnează true doar atunci când există cel puțin un câmp de semnătură și fiecare dintre acestea se validează ca fiind svValid, o valoare booleană unică convenabilă pentru o linie de procesare de arhivă care nu acceptă nimic mai puțin

Verificarea semnăturii, semnarea PAdES, criptarea AES-256 și API-ul de editare a documentelor încărcate sunt toate livrate în aceeași bibliotecă nativă VCL pentru Delphi și C++Builder, fără dependențe de DLL-uri externe; lista completă de caracteristici și versiunile IDE acceptate se găsesc pe pagina produsului HotPDF Component