Articol tehnic

Semnarea PDF-urilor cu PAdES B-B în Delphi folosind PDFium

Componenta PDFium semnează un PDF cu o semnătură digitală PAdES B-B prin metoda sa SignPades: încarcă documentul, generează hash-ul intervalului de octeți semnat, construiește o structură CAdES CMS și adaugă semnătura ca o salvare incrementală. Backend-ul criptografic este disponibil exclusiv pe Windows, așa că protejați fiecare apel cu PadesCryptoAvailable înainte de a semna

Situația este una familiară. Aveți un contract în format PDF, departamentul juridic solicită semnarea lui digitală înainte de trimitere, iar dvs. apelați la aceeași versiune de PDFium pe care o folosiți pentru randarea și inspectarea documentelor, doar pentru a descoperi că PDFium nu poate scrie o semnătură. API-ul său de semnătură este strict read-only. Componenta PDFium rezolvă această problemă controlând întregul proces de semnare în Pascal, de la funcția hash până la injectarea la nivel de octet, iar acest articol parcurge acest flux de la un capăt la altul

De ce nu poate PDFium să scrie o semnătură digitală?

PDFium expune semnăturile ca obiecte read-only și nu oferă funcții pentru crearea uneia. Familia de metode FPDFSignatureObj_* vă permite să parcurgeți o semnătură existentă, să citiți intrarea /Contents și să inspectați /ByteRange, pero nu există o metodă similară care să construiască un dicționar de semnătură, să rezerve un slot /Contents sau să scrie un interval de octeți; salvarea incrementală este disponibilă (FPDF_SaveAsCopy cu FPDF_INCREMENTAL), dar nu include un punct de legătură pentru semnare. Orice componentă care realizează semnarea unui PDF folosind PDFium trebuie să genereze individual fiecare octet al semnăturii, motiv pentru care componenta PDFium construiește acest sistem folosind trei unități Pascal pure. FPC 3.2.2 include md5 și sha1, dar nu oferă suport pentru SHA-2, iar API-ul SHA-256 din System.Hash din Delphi nu este compatibil la nivel de sursă cu FPC. De aceea, FPdfSha256 este o implementare autonomă conform standardului FIPS 180-4, care menține toate căile de cod CMS pe un singur tip TSHA256Digest fără ramificări de compilare. FPdfAsn1 oferă codificatorul și cititorul DER necesare structurilor CMS, iar FPdfCms asamblează datele CAdES SignedData peste acestea

Cum se semnează digital un PDF în Delphi?

Încărcați documentul, apoi apelați SignPades cu o amprentă (thumbprint) de certificat. Componenta PDFium identifică acea amprentă în depozitul de certificate „MY” al utilizatorului curent (Current User), preia certificatul corespunzător și cheia sa privată și scrie o copie semnată la calea specificată

uses
  PDFium, FPdfCrypto;

procedure SignContract(const AThumbprint: string);
var
  Pdf: TPdf;
begin
  if not PadesCryptoAvailable then
    raise Exception.Create('PAdES signing requires the Windows CNG backend');

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';   // the document to sign
    Pdf.Active := True;
    // Second argument: SHA-1 thumbprint of a certificate in the Current
    // User "MY" store. First argument: destination for the signed copy.
    if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
      raise Exception.Create('Signing failed');
  finally
    Pdf.Free;
  end;
end;

Ce conține CMS-ul: atribute semnate și RFC 5652

O semnătură de bază PAdES nu este o simplă semnătură RSA aplicată fișierului; este o structură CAdES CMS SignedData care conține un set obligatoriu de atribute semnate, iar FPdfCms.BuildSignedData generează exact acest set: content-type, message-digest și signing-certificate-v2, atributul ESS care asociază semnătura cu certificatul semnatarului prin hash. Un detaliu de aici ridică probleme în aproape orice implementare CMS manuală. Standardul RFC 5652 §5.4 impune ca rezumatul atributelor semnate să fie calculat peste codificarea DER SET OF, eticheta (tag) 0x31, în timp ce aceleași atribute sunt transmise în SignerInfo sub eticheta IMPLICIT [0], 0xA0. Componenta PDFium codifică setul de atribute o singură dată, generează hash-ul pentru forma 0x31, apoi rescrie doar primul octet al etichetei în 0xA0 pentru transmitere, astfel încât un singur buffer deservește ambele roluri fără a parcurge structura a doua oară

var
  Pdf: TPdf;
  Opts: TPadesSignOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;

    Opts := TPadesSignOptions.Default;
    Opts.CertificateThumbprint := 'a1b2c3d4e5f6...';  // signer in the MY store
    Opts.Reason := 'I approve this agreement';
    Opts.Location := 'Berlin, DE';
    Opts.ContentsSize := 16384;                        // hex width of /Contents

    if not Pdf.SignPades('contract-signed.pdf', Opts) then
      raise Exception.Create('Signing failed');
  finally
    Pdf.Free;
  end;
end;

Suprasolicitarea opțiunilor adăugă metadatele dicționarului de semnătură definite de ISO 32000-1 §12.8.1: Reason, Location, ContactInfo și Name, toate fiind opționale și scrise în dicționarul valorii semnăturii. O limitare poate fi ușor omisă. Dacă setați CommitmentTypeOid pentru a adăuga un atribut semnat de tip indicator al angajamentului CAdES, nu setați și Reason; standardul ETSI EN 319 142-1 §6.3 interzice includerea ambelor elemente, deoarece cele două exprimă aceeași intenție prin mijloace diferite

Cum interacționează elementul ByteRange și slotul /Contents?

O semnătură trebuie să acopere întregul fișier, cu excepția octeților care conțin semnătura propriu-zisă, iar PAdES rezolvă această circularitate folosind un substituent cu lățime fixă gestionat de SignPadesBytes. Acesta rezervă un șir hexazecimal /Contents de ContentsSize octeți (16384 implicit, valoare mai mare decât o structură tipică CMS SignedData), serializează salvarea incrementală pentru a localiza offsetul exact al slotului, apoi calculează /ByteRange ca două intervale care încadrează slotul, adică tot ce se află înainte de delimitatorul de deschidere al șirului hexazecimal și tot ce se află după cel de închidere. Hash-ul SHA-256 se calculează doar peste aceste două intervale. CMS-ul finalizat este codificat hexazecimal în slotul rezervat, completat cu zerouri până la dimensiunea fixă, iar actualizarea tabelei de referințe este adăugată la sfârșit. Deoarece lățimea este fixată de la început, completarea slotului nu decalează octeții următori, motiv pentru care intervalul de octeți rămâne valid; octeții documentului original sunt păstrați intacti, astfel încât o semnătură anterioară aplicată pe același fișier rămâne validă, exact așa cum impune salvarea incrementală din ISO 32000-1 §12.8.1

Backend-ul Windows CNG și limitele sale

Componenta PDFium realizează semnarea exclusiv pe Windows, iar această limită este stabilită în mod deliberat. Modulul FPdfCryptoWin încarcă dinamic fișierele crypt32.dll și ncrypt.dll, fără a adăuga o dependență la compilare, iar procesul de semnare folosește serviciile standard CNG: deschide depozitul „MY”, caută certificatul după hash, obține handle-ul cheii sale private prin CryptAcquireCertificatePrivateKey și apelează NCryptSignHash. Sunt acceptate protocoalele RSA cu PKCS#1 v1.5, RSA-PSS și ECDSA. Algoritmul ECDSA necesită o corecție suplimentară pe care ceilalți nu o solicită, deoarece NCryptSignHash returnevara perechea brută r și s conform IEEE P1363, în timp ce structura CMS necesită o secvență DER ECDSA-Sig-Value SEQUENCE, astfel încât backend-ul o re-codifică conform standardului RFC 5480

Consecința practică este că cheia privată trebuie să fie stocată în depozitul de certificate din Windows. Un certificat stocat într-un fișier PFX poate fi utilizat doar după ce îl importați în depozitul Current User, moment în care amprenta sa devine valoarea pe care o veți transmite către SignPades. Această versiune nu oferă suport pentru PKCS#11, module HSM sau chei stocate în fișiere software, astfel încât atunci când PadesCryptoAvailable returnează False, semnarea nu este disponibilă pe acel sistem

Unde se oprește PAdES B-B

Nivelul PAdES B-B reprezintă baza celor patru niveluri PAdES: dovedește identitatea semnatarului și faptul că octeții nu au fost modificați ulterior, fără alte elemente suplimentare. O semnătură B-B nu conține un timestamp de încredere, deci nu poate dovedi momentul semnării, și nu include date de revocare, astfel încât un validator va trebui să obțină individual lanțul de certificate și starea acestora peste ani. Aceste lipsuri sunt acoperite de nivelurile superioare. Când aveți nevoie de un timp de semnare acceptat la audit, adăugarea unui timestamp RFC 3161 și a DSS pentru validarea pe termen lung promovează semnătura la nivelul B-T și mai sus; când doriți să citiți o semnătură finalizată și să confirmați nivelul atins, inspectarea semnăturilor PDF și a nivelurilor PAdES reprezintă instrumentul asociat; iar înainte de a semna orice document, analiza riscurilor de securitate PDF vă arată ce urmează să semnați

Metodele SignPades prezentate aici sunt incluse în pachetul PDFium Component pentru Delphi și C++Builder, alături de modulul de inspecție read-only a semnăturilor oferit implicit de PDFium