Articol tehnic

Semnături digitale PDF și PAdES în Delphi cu HotPDF

O semnătură PDF este în mare parte o problemă de contabilitate a octeților, și acolo se strică lucrurile. Criptografia rulează pe cod care a fost auditat timp de două decenii, iar acea parte aproape că nu eșuează niciodată. Ce eșuează în producție este mai modest: un placeholder rezervat prea mic pentru semnătura reală, un hash calculat peste porțiunea greșită a fișierului sau o "salvare" după semnare care a rescris pe tăcute octeți pe care semnătura îi înghețase deja. Așezați octeții corect și bifa verde se rezolvă de la sine

HotPDF acoperă semnarea pentru Delphi și C++Builder la trei niveluri, iar alegerea dintre ele se face răspunzând la o singură întrebare: unde locuiește cheia privată? Un fișier PFX de pe disc are nevoie de un singur apel de funcție. O cheie blocată într-un HSM sau într-un serviciu de semnare la distanță are nevoie de succesiunea rezervă-hash-inserare, pentru că nicio bibliotecă nu poate ajunge într-un token și scoate cheia din el. O semnătură care trebuie să satisfacă reglementările europene are nevoie, peste toate acestea, de structurile de bază PAdES. Secțiunile de mai jos urmează această progresie

Diagramă decizională care alege între semnarea HotPDF PFX într-un singur apel, calea rezervare-hash-inserare când cheia stă într-un HSM sau serviciu la distanță, și structurile de bază PAdES pentru semnare europeană reglementată
Alege nivelul de semnare întrebând unde trăiește cheia privată; un fișier PFX lizibil colapsează semnarea într-un singur apel, în timp ce cheile deținute de token impun ocolirea la nivel de octeți, iar reglementarea europeană adaugă stratul PAdES

Cum fixează /ByteRange octeții semnați

O semnătură trebuie să locuiască în interiorul fișierului pe care îl semnează și nu se poate semna pe sine însăși. PDF ocolește acest paradox lăsând o gaură. Înainte de semnare, scriitorul rezervă o intrare /Contents de dimensiune fixă, plină cu zerouri, și înregistrează un tablou /ByteRange pentru cele două porțiuni de pe fiecare parte a ei: tot ce vine înainte de gaură, tot ce vine după. Semnatarul calculează hash-ul celor două porțiuni și scrie blob-ul CMS rezultat în gaură, sub formă hexazecimală. Capcana stă în cuvântul fixă. Vă angajați la dimensiunea acelei găuri înainte de a ști cât de mare va fi semnătura finală, așa că rezervarea trebuie să fie o supraestimare cu încredere. Opt kiloocteți acoperă confortabil o semnătură CMS detașată cu un lanț de certificate scurt

HotPDF împarte cele două cazuri în două apeluri, iar confundarea lor este o greșeală obișnuită la început. AddSignatureField plasează un câmp gol, vizibil, pentru ca o persoană să semneze mai târziu într-un vizualizator. AddSignedSignatureField creează câmpul și rezervă gaura /Contents, ceea ce este varianta pe care o doriți ori de câte ori codul, nu o persoană, va completa semnătura. Dați unui semnatar extern un câmp gol și acesta nu are nimic de completat

Calea cu un singur apel: semnarea dintr-un PFX

Atunci când certificatul și cheia sa privată se află într-un fișier PFX/PKCS#12 pe care procesul dumneavoastră îl poate citi, întregul flux se reduce la o funcție de clasă:

if THotPDF.SignPDFWithPFX('invoice-unsigned.pdf', 'invoice-signed.pdf',
    'company-cert.pfx', 'pfx-password') then
  Writeln('Signed: invoice-signed.pdf')
else
  raise Exception.Create('PFX signing failed');

Când acest lucru eșuează, PDF-ul este rareori problema. PFX-ul este problema. HotPDF citește containere protejate cu PBES2, adică derivare de cheie PBKDF2 peste AES-256-CBC. Un PFX exportat de un vrăjitor de certificate Windows mai vechi, sau de OpenSSL înainte de versiunea 3.0, este de obicei împachetat în schimb cu RC2 sau 3DES vechi și pur și simplu nu se va putea parsa. Soluția este să reexportați containerul o dată, cu protecție modernă; OpenSSL-ul de astăzi face acest lucru implicit, iar acest lucru nu este o modificare de cod. Așa că atunci când semnarea eșuează instantaneu pe un certificat care "funcționează peste tot în altă parte", uitați-vă la modul în care a fost creat PFX-ul înainte de a vă suspecta propriul cod

Calea rezervă-hash-inserare pentru HSM-uri și tokenuri

Calea cu un singur apel presupune că procesul dumneavoastră poate citi cheia ca fișier. Tot mai des nu poate. Cheia se află într-un HSM, pe un token USB sau în spatele API-ului unui serviciu de semnare, și nu există nicio modalitate prin care o bibliotecă să ajungă direct la ea. HotPDF gestionează acest lucru descompunând semnarea în pași la nivel de octeți: scrie un document placeholder, cere bibliotecii intervalele de hash, transmite intrarea de hash oricărui lucru care deține cheia, apoi îmbină CMS-ul returnat înapoi în gaură

HotPDF: conductă în patru pași de rezervare-hash-inserare peste placeholder.pdf, arătând golul rezervat /Contents între cele două intervale ByteRange și un HSM care schimbă rezumatul pentru hex CMS
HotPDF rezervă gaura și raportează ambele intervale ByteRange, deținătorul tău de chei le semnează în exterior, iar CMS-ul returnat este cusut înapoi octet cu octet fără a atinge vreun octet înghețat
var
  Doc: THotPDF;
  Fs: TFileStream;
  PdfBytes, HashInput, SigHex: AnsiString;
  R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
  // 1. Scrie documentul cu o gaură /Contents rezervată
  Doc := THotPDF.Create(nil);
  try
    Doc.FileName := 'placeholder.pdf';
    Doc.BeginDoc;
    Doc.CurrentPage.AddSignedSignatureField('Sig1',
      Rect(50, 100, 350, 150), 8192, 'adbe.pkcs7.detached',
      'Contract approval', 'Boston, MA', 'legal@example.com');
    Doc.EndDoc;
  finally
    Doc.Free;
  end;

  // 2. Încarcă octeții salvați; offset-urile returnate sunt indexate de la 0
  Fs := TFileStream.Create('placeholder.pdf', fmOpenRead);
  try
    SetLength(PdfBytes, Fs.Size);
    Fs.ReadBuffer(PdfBytes[1], Fs.Size);
  finally
    Fs.Free;
  end;
  THotPDF.PreparePDFForSigning(PdfBytes, R1Start, R1Len, R2Start, R2Len,
    CStart, CLen);

  // 3. Calculează hash-ul ambelor porțiuni și semnează extern (HSM, token, serviciu)
  HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
               Copy(PdfBytes, R2Start + 1, R2Len);
  SigHex := SignWithHsm(HashInput);  // integrarea dumneavoastră: returnează CMS ca hex

  // 4. Îmbină semnătura în gaura rezervată
  THotPDF.InsertSignatureHex(PdfBytes, SigHex);
  Fs := TFileStream.Create('signed.pdf', fmCreate);
  try
    Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
  finally
    Fs.Free;
  end;
end;

Două detalii din această succesiune provoacă majoritatea eșecurilor intermitente. Primul este că PreparePDFForSigning lucrează pe octeții unui fișier finalizat. Placeholder-ul trebuie scris și salvat complet înainte ca offset-urile să însemne ceva; calculați-le pe un flux încă în curs de asamblare și ele nu se vor alinia cu octeții pe care îi veți calcula ulterior prin hash. Al doilea este, din nou, dimensiunea rezervării. Cei 8192 de octeți pe care i-ați cerut trebuie să încapă CMS-ul final, iar o semnătură care poartă certificate intermediare, sau una pe care un serviciu o decorează cu atribute semnate, poate depăși această dimensiune. InsertSignatureHex nu va mări gaura ca să facă loc. Semnul este un flux care semnează bine cu un certificat și eșuează cu următorul; remediul este să regenerați placeholder-ul cu o rezervare măsurată dintr-o semnătură reală produsă de semnatarul efectiv, nu estimată

Nivelurile de bază PAdES și marcajele de timp care mențin o semnătură vie

Dacă semnați conform regulilor europene, standardul aplicabil este ETSI EN 319 142-1, care stivuiește patru niveluri de bază PAdES. B-B este semnătura simplă. B-T adaugă un marcaj de timp de încredere care dovedește când a fost făcută. B-LT înglobează materialul de validare, certificatele și datele de revocare, în interiorul documentului, astfel încât să poată fi verificat și peste ani. B-LTA stratifică marcaje de timp periodice ale documentului deasupra celorlalte, astfel încât dovada supraviețuiește algoritmilor pe care a fost construită. HotPDF emite structurile de pe partea documentului pentru fiecare nivel:

HotPDF: niveluri de bază PAdES stivate, de la B-B prin B-T și B-LT până la B-LTA, cu o cronologie de reînnoire arătând timestamp-urile periodice de document menținând o semnătură verificabilă zeci de ani mai târziu
Fiecare nivel suprapune o protecție nouă peste cea anterioară; B-LTA reaplică în permanență timestamp-uri de document, astfel încât dovezile să supraviețuiască algoritmilor pe care a fost construită inițial
// Câmp de semnătură de bază PAdES (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
  'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
  'Contract approval', 'Boston, MA', 'legal@example.com');

// Marcaj de timp al documentului: rezervare mai mare pentru tokenul TSA și lanț
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);

Rezervarea de 16384 de octeți pentru marcajul de timp este deliberată. O autoritate de marcare temporală returnează un token care își aduce propriul lanț de certificate, astfel încât are de obicei nevoie de mai mult spațiu decât cei 8 KB cu care se mulțumește o semnătură simplă. Aceste marcaje de timp ale documentului sunt și mecanismul din spatele B-LTA: re-marcarea temporală a unei semnături arhivate la fiecare câțiva ani, cu algoritmi încă actuali, este ceea ce menține verificabil în 2040 un document semnat de dumneavoastră în 2026

Un cuvânt despre șirurile de motiv, locație și contact pe care le acceptă ambele apeluri de câmp: acestea sunt metadate de conveniență și nimic mai mult. HotPDF le stochează ca intrări simple de dicționar și le pictează în aspectul vizibil al semnăturii, dar niciun validator nu le verifică față de nimic. Completați-le consecvent din datele fluxului dumneavoastră de lucru, pentru că auditorii chiar le citesc, dar nu le confundați niciodată cu dovezi. Afirmația criptografică efectivă trăiește în întregime în CMS și în lanțul său de certificate, iar un verificator ignoră complet textul vizibil

După semnare, fișierul poate doar să crească

Din momentul în care o semnătură există, octeții din intervalele ei sunt înghețați. Singurul mod legitim de a modifica fișierul ulterior este o actualizare incrementală conform ISO 32000-1 §7.5.6, care adaugă obiecte noi și modificate după octeții originali și înlănțuiește o secțiune proaspătă de referințe încrucișate înapoi la ele. Procedat astfel, semnătura rămâne validă pentru revizia ei, iar un vizualizator raportează starea onestă: revizia semnată este intactă, documentul a fost extins ulterior. Reserializați în schimb întregul fișier și rescrieți porțiunile semnate, ceea ce distruge semnătura chiar și atunci când nimic vizibil nu s-a schimbat. Același mecanism de revizie este și modul în care un singur document poartă mai multe semnături: fiecare semnătură nouă ajunge în propria actualizare incrementală, iar intervalele ei acoperă tot ce vine înainte de ea, inclusiv semnăturile anterioare. Mecanica append-only, și momentul în care este sigur să le compactați, sunt tratate în articolul despre fluxuri de obiecte și actualizări incrementale

Merită să țineți minte două limite în timp ce proiectați. Modul de ieșire PDF/A al HotPDF respinge direct câmpurile de semnătură, așa că o conformitate arhivistică și o semnătură înglobată trebuie livrate ca fișiere separate. Iar semnarea nu spune nimic despre secretizare: dovedește cine a produs un document și că acesta nu s-a schimbat de atunci, dar oricine îl poate citi în continuare. Ascunderea conținutului este o sarcină separată, tratată de criptarea AES-256 și politica de permisiuni

Orice ați construi, testați-l cu altceva decât codul care a scris fișierul. Deschideți rezultatul în panoul de semnătură al Acrobat și confirmați trei lucruri: semnătura este validă, identitatea se înlănțuie la rădăcina pe care o așteptați, iar panoul nu raportează nicio modificare de la semnare încoace. Apoi schimbați un singur octet în interiorul intervalului semnat al unei copii de unică folosință și confirmați că panoul consideră acum documentul alterat. Un flux de semnare pe care nu l-ați văzut niciodată respingând un fișier falsificat este unul a cărui verificare nu a fost cu adevărat testată

Toate cele trei niveluri de semnare vin incluse în HotPDF Delphi Component pentru Delphi și C++Builder; pagina de produs face legătura către referința completă a API-ului de semnare