Articol tehnic

Semnare PAdES la distanță în PDFium VCL: HSM și cloud

PDFiumPas împarte semnarea PAdES în două apeluri, astfel încât cheia privată nu trebuie niciodată să fie în procesul tău. PreparePadesRemoteSignature scrie o actualizare incrementală cu un placeholder /Contents gol, cu lățime fixă, și returnează o înregistrare de cerere care poartă digest-ul documentului SHA-256, ByteRange-ul exact și o amprentă a fișierului pregătit. CompletePadesRemoteSignature preia CMS-ul detașat pe care serviciul tău de semnare îl returnează și îl introduce în acel spațiu rezervat

Între cele două apeluri pot trece minute sau ore, procesul se poate reporni, iar munca se poate muta pe altă mașină. Acel interval este întregul motiv pentru care API-ul este proiectat așa

De ce nu poate o cheie la distanță să folosească apelul obișnuit de semnare?

Pentru că SignPadesBytes presupune că operația de semnare se întâmplă în interiorul apelului. Construiește actualizarea incrementală, calculează digest-ul peste ByteRange, semnează, și scrie rezultatul, toate înainte de a returna. Asta este exact corect atunci când cheia trăiește în magazinul de certificate Windows sau într-un fișier PKCS#12 pe care l-ai încărcat

Este imposibil atunci când cheia trăiește într-un HSM de rețea, un dispozitiv calificat de creare a semnăturii operat de un furnizor de servicii de încredere, sau un API de semnare cloud care cere utilizatorului să confirme pe telefon. În aceste cazuri secvența nu este o funcție, este o conversație: trimiți un digest, altceva autentifică un om, iar un CMS revine mai târziu. Un API sincron nu poate exprima „mai târziu” fără să blocheze un thread pe o operație care poate avea nevoie de un al doilea factor

Protocolul în două faze

Faza unu pregătește documentul. PDFiumPas adaugă câmpul și dicționarul de valoare al semnăturii, rezervă ContentsSize bytes de spațiu codificat hex în /Contents, calculează ByteRange-ul în jurul acelei rezervări, și produce un TPadesRemoteSigningRequest conținând FormatVersion, PreparedFingerprint, DocumentDigest, ByteRange-ul cu patru elemente, ContentsHexOffset și ContentsSize

Singura valoare de care are nevoie serviciul tău de semnare este DocumentDigest: SHA-256-ul pe care CAdES SignedData returnat trebuie să îl poarte ca digest de mesaj. Tot restul din înregistrare există pentru ca faza a doua să poată dovedi că fișierul pe care îl completează este fișierul din care a fost calculat acel digest

uses
  FPdfPades;

var
  Options: TPadesRemoteSignOptions;
  Request: TPadesRemoteSigningRequest;
  Source, Prepared, Session: TFileStream;
begin
  Options := TPadesRemoteSignOptions.Default;
  Options.Reason := 'Approved by finance';
  Options.Location := 'Lisbon';
  Options.Name := 'A. Moreira';
  Options.SigningTimeUtc := NowUtc;
  Options.ContentsSize := 16384;   // bytes hex rezervați pentru CMS

  Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
  try
    PreparePadesRemoteSignature(Source, Prepared, Options, Request);
  finally
    Prepared.Free;
    Source.Free;
  end;

  // Persistă sesiunea astfel încât o rulare ulterioară - sau altă mașină - o poate termina
  Session := TFileStream.Create('contract.signreq', fmCreate);
  try
    SavePadesRemoteSigningRequest(Session, Request);
  finally
    Session.Free;
  end;

  SendDigestToSigningService(Request.DocumentDigest);
end;

Ce refuză Complete, și de ce există fiecare verificare?

Completarea este locul unde un design de semnare la distanță de obicei o ia razna, așa că validarea este deliberat neiertătoare. CompletePadesRemoteSignature respinge un PDF pregătit a cărui amprentă nu mai corespunde cererii, un ByteRange care nu corespunde coordonatelor placeholder-ului înregistrat, delimitatori /Contents modificați, un placeholder care nu mai este gol, un CMS mai mare decât rezervarea, un CMS care nu este exact o singură valoare DER, o formă SignedData nesuportată, un atribut signing-certificate-v2 lipsă, și un CMS al cărui digest de mesaj nu este egal cu digest-ul documentului pregătit

Fiecare dintre acestea corespunde unui eșec real. Verificările de amprentă și ByteRange prind cazul în care cineva a regenerat fișierul pregătit între faze, ceea ce ar produce o semnătură care se validează peste bytes pe care nimeni nu îi are. Verificarea placeholder-ului gol prinde completarea dublă, unde un al doilea CMS este scris peste o semnătură care există deja. Verificarea digest-ului de mesaj prinde cel mai periculos caz dintre toate: un CMS corect format semnat peste un document diferit, ceea ce obții atunci când o coadă amestecă două sesiuni de semnare concurente. Fără ea ai produce un fișier care arată semnat și eșuează validarea peste tot, sau mai rău, care poartă aprobarea altcuiva

Cerința signing-certificate-v2 este o chestiune de conformitate PAdES, nu una de integritate. ETSI EN 319 142 cere ca certificatul de semnare să fie legat în atributele semnate, iar un CMS căruia îi lipsește acel atribut nu este o semnătură PAdES chiar dacă se verifică criptografic. Respingerea lui la completare înseamnă că afli asta aici, nu într-un raport de validator de la un client, subiect explorat mai departe în de ce validatorii resping semnăturile PAdES

var
  Request: TPadesRemoteSigningRequest;
  Session, Prepared, Dest: TFileStream;
  CmsDer: TBytes;
begin
  Session := TFileStream.Create('contract.signreq', fmOpenRead);
  try
    Request := LoadPadesRemoteSigningRequest(Session);
  finally
    Session.Free;
  end;

  CmsDer := FetchDetachedCmsFromService;   // returnat de HSM sau TSP

  Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
  Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
  try
    try
      CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
    except
      on E: EPadesCrypto do
        // Fiecare respingere poartă un motiv specific; loghează-l cuvânt cu cuvânt
        FailSession(E.Message);
    end;
  finally
    Dest.Free;
    Prepared.Free;
  end;
end;

Trecerea granițelor de proces și de mașină

SavePadesRemoteSigningRequest și LoadPadesRemoteSigningRequest serializează sesiunea printr-un format binar versionat stabil, ceea ce face designul practic, nu doar corect. O aplicație web poate pregăti un document într-o cerere, stoca PDF-ul pregătit și blob-ul de sesiune, returna un digest browser-ului pentru o semnătură cu smart-card, și completa fișierul într-un handler de cerere complet diferit

Câmpul FormatVersion este ce menține asta sigur peste actualizări. O sesiune scrisă de un build mai vechi și încărcată de unul mai nou este recunoscută sau respinsă explicit, în loc să fie citită greșit ca o înregistrare de altă formă. Dacă coada ta poate păstra sesiuni zile întregi, tratează versiunea de format ca pe un fapt operațional demn de logat, nu un detaliu de implementare

Dimensionarea placeholder-ului

ContentsSize este singurul parametru la care trebuie să te gândești, pentru că este fixat înainte ca CMS-ul să existe. Numără rezervarea codificată hex, așa că un CMS DER de 6 KB are nevoie de cel puțin 12 KB de spațiu, iar implementarea plafonează rezervarea la 64 MiB

Rezervă prea puțin și completarea eșuează cu o eroare de CMS supradimensionat după ce serviciul tău de semnare și-a făcut deja treaba, ceea ce pe un serviciu de semnătură calificată taxat înseamnă o operație irosită. Rezervă prea mult și fiecare document semnat poartă padding-ul pentru totdeauna. Abordarea sensibilă este să măsori: semnează un document cu lanțul tău real de certificate, uită-te la lungimea DER, dublează-o pentru hex, apoi adaugă un spațiu generos pentru token-ul de timestamp dacă intenționezi să treci la o semnătură de nivel T. Lanțurile cu mai multe intermediare și un răspuns OCSP lung cresc mai repede decât se așteaptă oamenii

Ce urmează după semnătură

O semnătură la distanță completată este PAdES B-B. Validarea pe termen lung are nevoie de un timestamp și de materialul de validare, care este o actualizare incrementală separată ce adaugă un DSS și dicționarele lui VRI per-semnătură, descrisă în semnături pe termen lung cu timestamp-uri RFC 3161 și DSS. Acel pas este local: adaugă certificate, răspunsuri OCSP și CRL-uri, dintre care niciunul nu are nevoie de cheia privată

Înainte de a livra, verifică ce ai produs cu același traseu de cod pe care l-ar folosi o parte care se bazează pe semnătură, acoperit în inspectarea semnăturilor digitale PDF și a nivelurilor PAdES. Semnarea și verificarea sunt cod diferit, iar un pipeline de semnare la distanță este exact locul unde cele două pot devia una de cealaltă fără ca nimeni să observe până când un validator extern o spune

PDFiumPas este o componentă Delphi și Lazarus în jurul motorului PDFium, cu o stivă PAdES nativă în Pascal, astfel încât semnarea, timestamping-ul și validarea funcționează fără instrumente externe de linie de comandă. Documentația API completă și o versiune de test sunt pe pagina componentei PDFium Delphi