Tehnički članak

PAdES digitalni potpisi u Delphi-ju: Potpisivanje i validacija pomoću PDFlibPas-a

Validacija jednog PAdES potpisa podrazumeva proveru tri nezavisne stvari, a zeleni znak potvrde u pregledaču govori vam samo o trećoj. Prvo, niz /ByteRange mora pokrivati prave bajtove: opsezi koje on imenuje moraju rekonstruisati tačan ulaz nad kojim je izračunat CMS sažetak (CMS digest), bez ikakvih potpisanih bajtova izvan njih. Drugo, sertifikat unutar CMS-a mora da se nadovezuje na koren (root) kome verujete i da nosi potpisani atribut sertifikata potpisnika (signing-certificate attribute) koji PAdES zahteva. Treće, ako profil zahteva vremenski žig, RFC 3161 token mora povezati vrednost potpisa sa trenutkom u vremenu pre nego što je sertifikat istekao. Acrobat spaja sve tri stvari u jednu ikonu. Alat za proveru usklađenosti ih drži odvojeno, a to bi trebalo da radi i kod koji generiše ove datoteke. losLab PDF biblioteka (PDFlibPas) pruža vam stranu potpisivanja, ponovno ugrađivanje vremenskog žiga i pozive za reviziju (audit) radi provere ByteRange-a pre nego što ukažete poverenje datoteci

Jedna razlika zbunjuje skoro svaku prvu PAdES implementaciju, pa je vredi navesti pre pisanja bilo kakvog koda. Potpis upisan pomoću /SubFilter /adbe.pkcs7.detached je potpuno ispravan potpis prema standardu ISO 32000-1 §12.8 koji će Acrobat prijaviti kao validan. Međutim, on istovremeno nije PAdES potpis, jer standard ETSI EN 319 142-1 zahteva ETSI.CAdES.detached na svakom osnovnom nivou (baseline level). eIDAS alat za proveru usklađenosti odbija prvi, a prihvata drugi, iako je kriptografija identična. Profil je tvrdnja koju dokument iznosi o samom sebi, a ispravno postavljanje te tvrdnje zahteva jedan jedini poziv u PDFlibPas-u

Šta pretvara PDF potpis u PAdES potpis

Standard ETSI EN 319 142-1 definiše četiri osnovna nivoa (baseline levels) naslagana na CMS formatu. PAdES-B-B je početna tačka: CAdES potpis u PDF polju za potpis sa SubFilter-om ETSI.CAdES.detached i potpisanim atributom sertifikata potpisnika. PAdES-B-T dodaje RFC 3161 vremenski žig preko vrednosti potpisa, dokazujući da je potpis postojao pre trenutka u vremenu koji niko ne može retroaktivno promeniti. PAdES-B-LT ugrađuje sertifikate, CRL liste i OCSP odgovore potrebne za validaciju u Document Security Store (DSS - skladište bezbednosti dokumenata), tako da datoteka ostaje proverljiva i nakon što izdavalac sertifikata (CA) ugasi svoju infrastrukturu. PAdES-B-LTA zatvara ovaj niz vremenskim žigom dokumenta (document timestamp) koji ponovo štiti prikupljene dokaze kako kriptografski algoritmi slabe

Biblioteka PDFlibPas mapira ove koncepte na svoj API za proces potpisivanja. Oznaka profila je metoda SetSignProcessCustomSubFilter. Ako vaša bezbednosna politika zahteva naznaku tipa obaveze (commitment-type indication - dokaz o poreklu, dokaz o odobrenju ili neki od drugih ETSI identifikatora numerisanih od 1 do 6), itu vrednost podešavate preko SetSignProcessCommitmentType. Eksplicitna politika potpisivanja (signature policy) se povezuje pomoću SetSignProcessSignaturePolicy, koja uzima OID politike i njen sažetak. Jedna podrazumevana vrednost zaslužuje pažnju: sa algoritmom sažetka ostavljenim na auto, biblioteka bira SHA-256 za ETSI i adbe.pkcs7.detached potpise, a vraća se na SHA-1 samo na nasleđenoj adbe.pkcs7.sha1 putanji. Ipak, podesite ga eksplicitno. Auditori pitaju koji ste heš koristili, a eksplicitnu vrednost u kodu je lakše odbraniti nego podrazumevanu vrednost za čije objašnjenje morate da čitate priručnik

Kreiranje osnovnog potpisa

Ravni (flat) API vodi potpisivanje kao jednokratnu mašinu stanja (one-shot state machine): otvorite proces nad izvornom datotekom, konfigurišete ga, završite ga u izlaznu datoteku i pročitate kod rezultata. Donji niz kreira PAdES-B-B potpis sa SHA-256. Linija koja je najvažnija nema nikakve veze sa samim potpisom. To je namerno predimenzionisana rezervacija /Contents, jer je to jedina stvar koju kasnije ne možete promeniti ako na ovaj potpis ikada bude morao da se doda vremenski žig

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);  // room for a timestamp later
    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;

Funkcija NewSignProcessFromFile vraća 0 kada izvor uopšte ne može da se otvori. Nakon toga, GetSignProcessResult razdvaja načine otkazivanja koji se stvarno dešavaju u produkciji: 4 označava pogrešnu PDF lozinku, 7 pogrešnu PFX lozinku, 9 datoteku sertifikata bez privatnog ključa, 10 neupisivu izlaznu putanju, a 11 grešku prilikom primene bajtova potpisa. Beleženje numeričkog koda pored naziva ulazne datoteke pretvara nejasan tiket podrške u dijagnozu od jednog minuta

Dodavanje RFC 3161 vremenskog žiga koji biblioteka neće preuzeti za vas

Biblioteka PDFlibPas ne sadrži TSA klijent, i to je namerna granica, a ne propust. Biblioteka izračunava heš koji autoritet za vremenske žigove (TSA) mora supotpisati i nakon toga ponovo ugrađuje prošireni CMS. HTTP razmena i CMS operacije između toga pripadaju pozivaocu. Postoji jasan tehnički razlog za ovu podelu. Kontrola Windows CryptoAPI koja nominalno dodaje nepotpisane atribute, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, ne uspeva sa greškom CRYPT_E_INVALID_INDEX na odvojenoj strukturi SignedData koju PAdES koristi. Zato poboljšani CMS mora doći iz CMS enkodera pod vašom sopstvenom kontrolom. Nijedna biblioteka ne može tiho da ubaci token pomoću jednog sistemskog poziva, a svaka koja to tvrdi vrši operaciju na mestu koje ne možete videti

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);
    // both calls below are application code: an HTTP POST to your TSA,
    // and a CMS re-encode that attaches the token as an unsigned attribute
    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;

Pratite kodove rezultata: 12 znači da imenovano polje potpisa ne postoji, 11 da postojeći CMS nije mogao biti parsiran, a 13 da prošireni CMS više ne može da stane u rezervisani prostor /Contents. Kod 13 je onaj koji stvara najviše problema jer je jedino rešenje ponovno potpisivanje: tipičan token vremenskog žiga sa svojim lancem sertifikata zauzima 4 do 6 KB, a rezervacija od 8192 bajta napravljena tokom B-B koraka postoji upravo da bi ovaj korak imao mesta da se smesti

Validacija počinje od ByteRange-a, a ne od lanca sertifikata

Zelena kvačica u pregledaču je odluka o poverenju u odnosu na skladište sertifikata te mašine, a ne strukturna presuda o samoj datoteci. Programska validacija bi trebalo da počne niže, sa pitanjem koje inkrementalna ažuriranja čine suptilnim: koje bajtove svaki potpis zapravo pokriva? Svako poboljšanje o kome se ovde govori, bilo da je u pitanju drugi potpis, DSS rečnik ili vremenski žig dokumenta, stiže putem inkrementalnog ažuriranja (incremental update), i svako ažuriranje dodaje bajtove izvan /ByteRange-a prethodnog potpisa. Ti dodati bajtovi su legitimni. Validator i dalje mora da ih klasifikuje u odnosu na politiku izmena dokumenta, a nivo DocMDP na nivou polja u kojem ta politika živi može se pročitati pomoću GetSignatureDocMDPLevelByName

var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // before Open: SignDoc holds a 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 means actually signed
        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;

Dve zamke žive u ovoj revizionoj putanji. Poziv TPDFlibSignDoc.Open drži datoteku sa ekskluzivnim zaključavanjem deljenja (exclusive share lock), tako da validator koji takođe želi da hešira sirove bajtove datoteke radi CMS verifikacije mora da učita datoteku u memoriju pre nego što je otvori za reviziju. Obrnite taj redosled i čitanje neće uspeti zbog brave koju ste sami postavili. Druga zamka je tiha: pandan u ravnom API-ju, GetSignProcessByteRange, vraća Integer dok su u osnovi ofseti tipa Int64, pa preko 2 GB ravni poziv vrši skraćivanje (truncation) bez ikakve prijave greške, zbog čega ovaj primer povlači ofsete preko revizione klase. Vredi pomenuti još jedan nedostatak. Ravni sloj uopšte nema omotač (wrapper) VerifySignature. Kriptografske presude dolaze iz klase TPDFlibSignatureVerifier na nivou klase, koja vraća vsValid, vsInvalid ili vsUnknown, ili iz spoljnog validatora kome vaša bezbednosna politika već veruje

Dugotrajna validacija: DSS, VRI i vremenski žig dokumenta

PAdES-B-LT postoji jer je infrastruktura za opoziv sertifikata prolazna. Standard ETSI EN 319 142-1 §5.4.2.2 specifikuje Document Security Store (DSS): rečnik na nivou dokumenta koji nosi sertifikate, CRL liste i OCSP odgovore, opciono indeksirane po potpisu kroz VRI unose identifikovane hešom /Contents dela svakog potpisa. Tok u PDFlibPas-u prati dizajn vremenskog žiga. Metoda NewPAdESDSSProcessFromFile otvara proces. AddPAdESDSSCertificate, AddPAdESDSSCRL i AddPAdESDSSOCSP prihvataju DER binarne objekte. AddPAdESDSSVRI povezuje izabrani materijal sa jednim potpisom. EndPAdESDSSProcessToFile upisuje sve kao inkrementalno ažuriranje. Težak deo posla ostaje na vašoj strani. Pribavljanje materijala o opozivu i procena da li je on dovoljno svež da bi ga vredelo ugraditi jeste posao pozivaoca. Biblioteka garantuje da su rečnici strukturno usklađeni, ali ne može garantovati da je vaš OCSP server rekao istinu

Arhivska krajnja tačka, B-LTA, dodaje vremenski žig dokumenta: zasebno polje potpisa čiji je tip DocTimeStamp umesto Sig, koje se pravi preko metode SetSignProcessDocTimeStamp sa rezervisanom dužinom potpisa. Ono ne menja vremenski žig potpisa iz B-T koraka. Vremenski žig potpisa dokazuje kada je određeni potpis postojao. Vremenski žig dokumenta štiti celu datoteku, uključujući DSS dokaze, i predstavlja element koji dugotrajna arhiva obnavlja svakih nekoliko godina kako algoritmi slabe. Zreo arhivski profil nosi oba. Za čitače koji su stariji od ovih struktura, metoda TPDFlibSignDoc.EnsurePAdESExtensions upisuje ESIC programersku ekstenziju u katalog dokumenta, najavljujući da datoteka koristi funkcionalnosti definisane od strane ETSI organizacije

Jednu reakciju na sve ovo vredi preduprediti, jer izgleda kao greška, a zapravo nije. Pregledač često prijavljuje "validnost nepoznata" (validity unknown) na datoteci čija je PAdES struktura potpuno ispravna. Poverenje i struktura su nezavisne ose. Pregledač jednostavno ne može da poveže potpisnika sa korenom (root) kome veruje na toj mašini, što je uobičajeno sa privatnim izdavaocima (CA) i test sertifikatima, čak i kada ByteRange revizija i CMS verifikacija uspešno prođu. Rešenje je da se ispravno distribuira korenski sertifikat, ili da se vrši provera prema listama od poverenja Evropske unije (EU trusted lists) kada je kvalifikovani eIDAS status stvarni cilj, umesto da se dira kod za potpisivanje

Za revizorsku perspektivu, što znači popisivanje polja potpisa kroz korpus dokumenata, ispisivanje ByteRange rasporeda i grupno čitanje DocMDP nivoa, pogledajte prateći članak o revizorskom i potpisnom radnom stolu. Potpisani dokumenti koji takođe moraju da zadovolje arhivsku politiku spadaju u radni tok opisan u članku o PDF/A i PDF/UA preflight proveri u Delphi-ju. Kompletna dokumentacija o API-ju i datoteke za evaluaciju nalaze se na stranici proizvoda losLab PDF biblioteka za Delphi