Articol tehnic

Semnături digitale PAdES în Delphi: semnare și validare cu PDF Library for Delphi

Validarea unei semnături PAdES înseamnă verificarea a trei lucruri independente, iar o bifă verde într-un vizualizator vă spune ceva doar despre al treilea. În primul rând, matricea /ByteRange trebuie să acopere octeții corecți: intervalele pe care le denumește trebuie să reconstituie exact intrarea peste care a fost calculat rezumatul CMS, fără niciun octet semnat rămas în afara lor. În al doilea rând, certificatul din interiorul CMS trebuie să se înlănțuie până la o rădăcină de încredere și să poarte atributul semnat signing-certificate cerut de PAdES. În al treilea rând, dacă profilul revendică o marcă temporală, un jeton RFC 3161 trebuie să lege valoarea semnăturii de un moment în timp anterior expirării certificatului. Acrobat comprimă toate cele trei într-o singură pictogramă; un verificator de conformitate le păstrează separate, iar codul care produce aceste fișiere ar trebui să facă la fel. losLab PDF Library (PDF Library for Delphi) vă oferă partea de semnare a acestui proces, reîncorporarea mărcii temporale și apelurile de audit pentru a inspecta un ByteRange înainte de a avea încredere în el

O distincție îngreunează aproape orice primă implementare PAdES, așa că merită enunțată înainte de a scrie vreun cod. O semnătură scrisă cu /SubFilter /adbe.pkcs7.detached este o semnătură perfect valabilă conform ISO 32000-1 §12.8, pe care Acrobat o va raporta ca fiind validă. Totuși, nu este o semnătură PAdES, deoarece ETSI EN 319 142-1 impune ETSI.CAdES.detached la fiecare nivel baseline. Un verificator de conformitate eIDAS o respinge pe prima și o acceptă pe a doua, deși criptografia este identică. Profilul este o afirmație pe care documentul o face despre sine, iar formularea corectă a acelei afirmații se rezolvă printr-un singur apel în PDF Library for Delphi

Ce transformă o semnătură PDF într-o semnătură PAdES

ETSI EN 319 142-1 definește patru niveluri baseline suprapuse peste formatul CMS. PAdES-B-B este punctul de intrare: o semnătură CAdES într-un câmp de semnătură PDF cu SubFilter-ul ETSI.CAdES.detached și un atribut signing-certificate semnat. PAdES-B-T adaugă o marcă temporală RFC 3161 peste valoarea semnăturii, dovedind că semnătura a existat înainte de un moment pe care nimeni nu îl poate falsifica retroactiv. PAdES-B-LT încorporează certificatele, CRL-urile și răspunsurile OCSP necesare validării într-un Document Security Store, astfel încât fișierul rămâne verificabil și după ce CA-ul emitent își retrage infrastructura. PAdES-B-LTA încheie stiva cu o marcă temporală de document care reprotejează dovezile acumulate pe măsură ce algoritmii slăbesc

PDF Library for Delphi transpune aceste concepte în API-ul său de proces de semnare. Marcatorul de profil este SetSignProcessCustomSubFilter. Dacă politica dumneavoastră necesită o indicație de tip commitment (dovadă de origine, dovadă de aprobare sau unul dintre ceilalți identificatori ETSI numerotați de la 1 la 6), aceasta se transmite prin SetSignProcessCommitmentType. O politică de semnătură explicită se atașează cu SetSignProcessSignaturePolicy, care primește OID-ul politicii și rezumatul acesteia. O valoare implicită merită atenție: cu algoritmul de rezumat lăsat pe auto, biblioteca selectează SHA-256 pentru semnăturile ETSI și adbe.pkcs7.detached și revine la SHA-1 doar pe calea veche adbe.pkcs7.sha1. Setați-l explicit oricum. Auditorii întreabă ce funcție hash ați folosit, iar o valoare explicită în cod este mai ușor de apărat decât o valoare implicită pe care trebuie să mergeți să citiți manualul ca să o explicați

Scara nivelurilor de bază PAdES B-B, B-T, B-LT și B-LTA construite cu PDF Library for Delphi, arătând cum fiecare nivel adaugă timestamps, dovezi DSS sau un timestamp de document reînnoibil peste nucleul ETSI.CAdES.detached
Fiecare nivel de bază ETSI suprapune o garanție în plus peste același nucleu CAdES, de la atribute semnate până la un timestamp de document reînnoibil

Producerea semnăturii baseline

API-ul flat controlează semnarea ca pe o mașină de stări cu o singură trecere: deschideți un proces pe fișierul sursă, îl configurați, îl finalizați către un fișier de ieșire, citiți codul de rezultat. Secvența de mai jos produce o semnătură PAdES-B-B cu SHA-256. Linia cea mai importantă nu are nicio legătură cu semnătura în sine. Este rezervarea deliberat supradimensionată a /Contents, pentru că acesta este singurul lucru pe care nu îl mai puteți schimba ulterior dacă vreodată trebuie adăugată o marcă temporală acestei semnături

var
  Pdf: TPDFlib;
  SignId: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
    if SignId = 0 then
      raise Exception.Create('cannot open source PDF');
    Pdf.SetSignProcessField(SignId, 'Sig1');
    Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
    Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
    Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
    Pdf.SetSignProcessDigestAlgorithm(SignId, 2);          // SHA-256
    Pdf.SetSignProcessReserveContentsBytes(SignId, 8192);  // spațiu pentru o marcă temporală ulterioară
    Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
    if Pdf.GetSignProcessResult(SignId) <> 1 then
      raise Exception.CreateFmt('signing failed, code %d',
        [Pdf.GetSignProcessResult(SignId)]);
    Pdf.ReleaseSignProcess(SignId);
  finally
    Pdf.Free;
  end;
end;

NewSignProcessFromFile returnează 0 când sursa nu poate fi deschisă deloc. După aceea, GetSignProcessResult separă modurile de eșec care apar efectiv în producție: 4 înseamnă o parolă PDF greșită, 7 o parolă PFX greșită, 9 un fișier certificat fără cheie privată, 10 o cale de ieșire nescriptibilă, 11 un eșec la aplicarea octeților semnăturii. Înregistrarea codului numeric alături de numele fișierului de intrare transformă un tichet de suport vag într-un diagnostic de un minut

Adăugarea mărcii temporale RFC 3161 pe care biblioteca nu o va prelua pentru dumneavoastră

PDF Library for Delphi nu livrează niciun client TSA, iar aceasta este o graniță deliberată, nu o lacună. Biblioteca calculează rezumatul pe care autoritatea de marcare temporală trebuie să îl contrasemneze și reîncorporează ulterior CMS-ul augmentat; schimbul HTTP și intervenția asupra CMS-ului dintre aceste două etape revin apelantului. Există un motiv tehnic solid pentru această separare. Controlul CryptoAPI din Windows care ar trebui în mod nominal să adauge atribute nesemnate, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, eșuează cu CRYPT_E_INVALID_INDEX pe structura SignedData detașată folosită de PAdES. Așadar CMS-ul îmbunătățit trebuie să provină dintr-un codor CMS aflat sub controlul dumneavoastră. Nicio bibliotecă nu poate integra discret jetonul printr-un singur apel de sistem, iar oricare care pretinde că o face realizează intervenția undeva unde nu o puteți vedea

Conductă pentru adăugarea unui timestamp RFC 3161 unei semnături PAdES în Delphi, separând hashing-ul și încorporarea PDF Library for Delphi de cererea TSA a apelantului și re-codarea CMS în spațiul rezervat /Contents
Biblioteca face hash și reîncorporează, în timp ce codul tău aduce token-ul și efectuează operația pe CMS, iar rezultatul trebuie să aterizeze în interiorul rezervației /Contents de 8192 de octeți
var
  Pdf: TPDFlib;
  StsId: Integer;
  HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
  Pdf := TPDFlib.Create;
  try
    StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
    Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
    Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
    HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
    // ambele apeluri de mai jos sunt cod al aplicației: un HTTP POST către TSA-ul dumneavoastră,
    // și o reîncodare CMS care atașează jetonul ca atribut nesemnat
    TstDer := RequestTimeStampToken(HashHex);
    TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
    AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
    Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
    Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
    if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
      raise Exception.Create('timestamp embedding failed');
    Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
  finally
    Pdf.Free;
  end;
end;

Urmăriți codurile de rezultat aici: 12 înseamnă că respectivul câmp de semnătură nu există, 11 că CMS-ul existent nu a putut fi analizat, iar 13 că CMS-ul augmentat nu mai încape în rezervarea /Contents. Codul 13 este cel dureros, pentru că singura remediere este resemnarea: un jeton de marcă temporală tipic, împreună cu lanțul său de certificate, ocupă între 4 și 6 KB, iar rezervarea de 8192 de octeți făcută în etapa B-B există tocmai pentru ca această etapă să aibă loc unde să se așeze

Validarea începe de la ByteRange, nu de la lanțul de certificate

O bifă verde într-un vizualizator este o decizie de încredere raportată la depozitul de certificate al acelui calculator, nu un verdict structural despre fișier. Validarea programatică ar trebui să înceapă mai jos, cu întrebarea pe care actualizările incrementale o fac subtilă: ce octeți acoperă efectiv fiecare semnătură? Fiecare îmbunătățire discutată aici, fie că este o a doua semnătură, un dicționar DSS sau o marcă temporală de document, sosește printr-o actualizare incrementală, iar fiecare actualizare adaugă octeți în afara /ByteRange-ului semnăturii anterioare. Acei octeți adăugați sunt legitimi. Un validator tot trebuie să îi clasifice în raport cu politica de modificare a documentului, iar nivelul DocMDP per câmp în care trăiește acea politică poate fi citit cu GetSignatureDocMDPLevelByName

Auditul aspectului în octeți al unui PDF semnat în Delphi, arătând intervalele acoperite de ByteRange, octeții /Contents excluși, actualizările incrementale adăugate în afara intervalului și verdictele de acoperire raportate la dimensiunea fișierului
Două intervale acoperite cu octeții propriei semnături excluși spun adevărata poveste a acoperirii, iar actualizările adăugate sunt clasificate conform politicii DocMDP, nu temute
var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // înainte de Open: SignDoc deține un blocaj de tip share lock
  Doc := TPDFlibSignDoc.Create;
  try
    if not Doc.Open('invoice-bt.pdf', '', False) then
      raise Exception.Create('cannot open for audit');
    Names := TStringList.Create;
    try
      Doc.GetSignatureFieldNames(Names);
      for I := 0 to Names.Count - 1 do
        if Doc.GetSignatureValueObjNum(Names[I]) > 0 then   // >0 înseamnă că este efectiv semnat
        begin
          B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
          B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
          B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
          B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
          if (B0 = 0) and (B2 + B3 = FileSize) then
            Writeln(Names[I], ': covers the file to EOF')
          else
            Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
        end;
    finally
      Names.Free;
    end;
    Doc.Close;
  finally
    Doc.Free;
  end;
end;

Pe această cale de audit există două capcane. TPDFlibSignDoc.Open menține fișierul cu un blocaj exclusiv de tip share lock, astfel încât un validator care dorește și să calculeze rezumatul octeților bruți ai fișierului pentru verificarea CMS trebuie să citească fișierul în memorie înainte de a-l deschide pentru audit. Inversați această ordine și citirea eșuează pe un blocaj pe care vi l-ați impus singuri. A doua capcană este tăcută, nu zgomotoasă: echivalentul din API-ul flat, GetSignProcessByteRange, returnează Integer în timp ce offset-urile de bază sunt Int64, astfel încât dincolo de 2 GB apelul flat trunchiază fără nicio avertizare, motiv pentru care acest exemplu extrage offset-urile prin clasa de audit în loc de asta. Merită menționată și o absență. Stratul flat nu are deloc un wrapper VerifySignature. Verdictele criptografice provin din clasa TPDFlibSignatureVerifier, care returnează vsValid, vsInvalid sau vsUnknown, sau de la un validator extern în care politica dumneavoastră de conformitate are deja încredere

Validarea pe termen lung: DSS, VRI și marca temporală de document

PAdES-B-LT există pentru că infrastructura de revocare este muritoare. ETSI EN 319 142-1 §5.4.2.2 specifică Document Security Store: un dicționar la nivel de document care poartă certificate, CRL-uri și răspunsuri OCSP, indexate opțional per semnătură prin intrări VRI identificate prin rezumatul câmpului /Contents al fiecărei semnături. Fluxul PDF Library for Delphi oglindește proiectarea mărcii temporale. NewPAdESDSSProcessFromFile deschide procesul; AddPAdESDSSCertificate, AddPAdESDSSCRL și AddPAdESDSSOCSP acceptă blob-uri DER; AddPAdESDSSVRI leagă materialul selectat de o singură semnătură; EndPAdESDSSProcessToFile scrie totul ca o actualizare incrementală. Partea dificilă rămâne în sarcina dumneavoastră. Preluarea materialului de revocare și evaluarea prospețimii sale, pentru a decide dacă merită încorporat, este sarcina apelantului. Biblioteca garantează că dicționarele sunt conforme structural; ea nu poate garanta că respondentul dumneavoastră OCSP a spus adevărul

Punctul terminal de arhivare, B-LTA, adaugă o marcă temporală de document: un câmp de semnătură separat al cărui tip este DocTimeStamp și nu Sig, produs prin SetSignProcessDocTimeStamp cu o lungime de semnătură rezervată. Aceasta nu înlocuiește marca temporală a semnăturii din etapa B-T. Marca temporală a semnăturii dovedește când a existat o anumită semnătură; marca temporală de document protejează întregul fișier, inclusiv dovezile DSS, și este elementul pe care o arhivă pe termen lung îl reînnoiește la câțiva ani, pe măsură ce algoritmii slăbesc. Un profil de arhivare matur le poartă pe amândouă. Pentru vizualizatoarele PDF anterioare acestor structuri, TPDFlibSignDoc.EnsurePAdESExtensions înregistrează extensia de dezvoltator ESIC în catalogul documentului, anunțând că fișierul folosește funcționalități definite de ETSI

O reacție la toate acestea merită anticipată, pentru că pare un bug, dar nu este. Un vizualizator raportează adesea „validitate necunoscută” pe un fișier a cărui structură PAdES este complet corectă. Încrederea și structura sunt axe independente. Vizualizatorul pur și simplu nu poate înlănțui semnatarul până la o rădăcină de încredere pe acel calculator, ceea ce este obișnuit cu CA-uri private și certificate de test, chiar dacă atât auditul ByteRange, cât și verificarea CMS trec cu succes. Remediul este distribuirea corectă a certificatului rădăcină sau evaluarea față de listele de încredere ale UE atunci când obiectivul real este statutul eIDAS calificat, nu modificarea codului de semnare

Pentru perspectiva din partea de audit, adică enumerarea câmpurilor de semnătură într-un corpus, extragerea structurilor ByteRange și citirea nivelurilor DocMDP în masă, consultați articolul complementar despre workbench-ul de conformitate și semnare. Documentele semnate care trebuie să satisfacă și politica de arhivare se încadrează în fluxul descris în verificarea preliminară PDF/A și PDF/UA în Delphi. Documentația API completă și descărcările de evaluare se găsesc pe pagina de produs losLab PDF Library for Delphi