Technical Article

PAdES digitalni potpisi u Delphiju: Potpisivanje i validacija s PDFlibPas-om

Validacija jednog PAdES potpisa znači provjeru triju neovisnih stvari, a zelena kvačica u pregledniku govori vam samo o trećoj. Prvo, polje /ByteRange mora pokrivati točne bajtove: rasponi koje imenuje moraju rekonstruirati točan unos nad kojim je napravljen sažetak CMS-a, bez potpisanih bajtova izvan njih. Drugo, certifikat unutar CMS-a mora se povezivati s korijenom (root) kojem vjerujete i nositi potpisani atribut certifikata za potpisivanje koji PAdES zahtijeva. Treće, ako profil zahtijeva vremenski žig, RFC 3161 token mora povezati vrijednost potpisa s trenutkom u vremenu prije nego što je certifikat istekao. Acrobat sažima sve tri stvari u jednu ikonu; provjera sukladnosti ih drži odvojenima, a to bi trebao raditi i kôd koji proizvodi te datoteke. losLab PDF Library (PDFlibPas) pruža vam stranu potpisivanja tog procesa, ponovno ugrađivanje vremenskog žiga i revizijske pozive za pregled ByteRange-a prije nego što povjerujete datoteci.

Jedna razlika zbunjuje gotovo svaku prvu PAdES implementaciju, pa ju je vrijedno navesti prije bilo kakvog koda. Potpis zapisan s /SubFilter /adbe.pkcs7.detached je savršeno ispravan potpis prema ISO 32000-1 §12.8 koji će Acrobat prijaviti kao valjan. Međutim, to nije PAdES potpis, jer ETSI EN 319 142-1 zahtijeva ETSI.CAdES.detached na svakoj osnovnoj razini. eIDAS provjera sukladnosti odbacuje prvi, a prihvaća drugi iako je kriptografija identična. Profil je tvrdnja koju dokument iznosi o samom sebi, a ispravno postavljanje te tvrdnje zahtijeva samo jedan poziv u PDFlibPas-u.

Što pretvara PDF potpis u PAdES potpis

ETSI EN 319 142-1 definira četiri osnovne razine (baseline levels) naslagane na CMS formatu. PAdES-B-B je polazna točka: CAdES potpis u PDF polju potpisa s ETSI.CAdES.detached SubFilterom i potpisanim atributom certifikata za potpisivanje. PAdES-B-T dodaje RFC 3161 vremenski žig preko vrijednosti potpisa, dokazujući da je potpis postojao prije točke u vremenu koju nitko ne može retroaktivno promijeniti. PAdES-B-LT ugrađuje certifikate, CRL-ove i OCSP odgovore potrebne za validaciju u Document Security Store (DSS), tako da datoteka ostaje provjerljiva i nakon što izdavateljski CA povuče svoju infrastrukturu. PAdES-B-LTA zatvara stog s vremenskim žigom dokumenta koji ponovno štiti prikupljene dokaze kako algoritmi slabe.

PDFlibPas preslikava ove koncepte na svoj API za proces potpisivanja (sign-process API). Oznaka profila je SetSignProcessCustomSubFilter. Ako vaša pravila trebaju naznaku vrste obveze (commitment-type indication – dokaz o podrijetlu, dokaz o odobrenju ili jedan od drugih ETSI identifikatora označenih brojevima od 1 do 6), to ide kroz SetSignProcessCommitmentType. Eksplicitna politika potpisa (signature policy) prilaže se s SetSignProcessSignaturePolicy, koja prima OID politike i njezin sažetak (digest). Jedna zadana postavka zaslužuje pozornost: s algoritmom sažetka postavljenim na automatski (auto), knjižnica odabire SHA-256 za ETSI i adbe.pkcs7.detached potpise, a vraća se na SHA-1 samo na zastarjeloj stazi adbe.pkcs7.sha1. Ipak to postavite eksplicitno. Revizori pitaju koji ste hash koristili, a eksplicitna vrijednost u kodu lakše se brani od zadane postavke za koju morate čitati priručnik kako biste je objasnili.

Izrada osnovnog potpisa

Ravni API upravlja potpisivanjem kao jednokratnim strojem stanja (state machine): otvorite proces na izvornoj datoteci, konfigurirate ga, završite s izlaznom datotekom i pročitate povratni kôd rezultata. Slijed u nastavku proizvodi PAdES-B-B potpis sa SHA-256. Linija koja je najvažnija nema nikakve veze sa samim potpisom. To je namjerno predimenzionirana rezervacija /Contents, jer je to jedina stvar koju ne možete kasnije promijeniti ako se ovom potpisu ikada bude morao dodati 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;

Metoda NewSignProcessFromFile vraća 0 kada se izvor uopće ne može otvoriti. Nakon toga, GetSignProcessResult razdvaja načine neuspjeha koji se stvarno događaju u produkciji: 4 znači pogrešnu lozinku za PDF, 7 pogrešnu lozinku za PFX, 9 datoteku certifikata koja ne nosi privatni ključ, 10 izlaznu stazu na koju se ne može pisati, 11 neuspjeh tijekom primjene bajtova potpisa. Bilježenje numeričkog koda uz naziv ulazne datoteke pretvara nejasan korisnički upit u jednominutnu dijagnozu.

Dodavanje RFC 3161 vremenskog žiga koji knjižnica neće dohvatiti za vas

PDFlibPas ne isporučuje TSA klijent, i to je namjerna granica, a ne propust. Knjižnica izračunava hash koji autoritet za vremenske žige (TSA) mora supotpisati i nakon toga ponovno ugrađuje prošireni CMS; HTTP razmjena i CMS operacija u sredini pripadaju pozivatelju. Postoji čvrst tehnički razlog za ovu podjelu. Windows CryptoAPI kontrola koja nominalno dodaje nepotpisane atribute, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, ne uspijeva s pogreškom CRYPT_E_INVALID_INDEX na odvojenom SignedData rasporedu koji PAdES koristi. Stoga poboljšani CMS mora doći iz CMS kodera pod vašom vlastitom kontrolom. Nijedna knjižnica ne može tiho ugraditi token jednim sustavnim pozivom, a svaka koja to tvrdi provodi operaciju negdje gdje je ne možete vidjeti.

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;

Dva povratna koda ovdje imaju težinu: 12 znači da imenovano polje potpisa ne postoji, 11 da se postojeći CMS nije mogao raščlaniti, a 13 da prošireni CMS više ne stane u rezervirano mjesto /Contents. Kôd 13 je onaj koji boli, jer je jedini popravak ponovno potpisivanje: tipični token vremenskog žiga sa svojim lancem certifikata zauzima 4 do 6 KB, a rezervacija od 8192 bajta napravljena tijekom B-B koraka postoji upravo zato da bi ovaj korak imao mjesta za smještaj.

Validacija počinje na ByteRangeu, a ne na lancu certifikata

Zelena kvačica u pregledniku je odluka o povjerenju u odnosu na spremište certifikata tog računala, a ne strukturna presuda o datoteci. Programska validacija trebala bi započeti niže, s pitanjem koje inkrementalna ažuriranja čine suptilnim: koje bajtove svaki potpis stvarno pokriva? Svako poboljšanje o kojem se ovdje raspravlja, bilo da se radi o drugom potpisu, DSS rječniku ili vremenskom žigu dokumenta, dolazi putem inkrementalnog ažuriranja, a svako ažuriranje dodaje bajtove izvan /ByteRange-a ranijeg potpisa. Ti su dodani bajtovi legitimni. Validator ih i dalje mora klasificirati prema pravilima izmjene dokumenta, a razina DocMDP po polju u kojoj to pravilo živi čitljiva je 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;

Dvije zamke žive na ovom revizijskom putu. TPDFlibSignDoc.Open drži datoteku s ekskluzivnim zaključavanjem dijeljenja, pa validator koji također želi raspršiti sirove bajtove datoteke za provjeru CMS-a mora pročitati datoteku u memoriju prije otvaranja za reviziju. Okrenite taj redoslijed i čitanje neće uspjeti zbog zaključavanja koje ste sami postavili. Druga zamka je tiha, a ne glasna: ekvivalent ravnog API-ja GetSignProcessByteRange vraća Integer dok su osnovni pomaci Int64, pa nakon 2 GB ravni poziv skraćuje vrijednost bez prigovora, zbog čega ovaj primjer povlači pomake kroz klasu revizije. Vrijedi spomenuti i jedan nedostatak: ravni sloj uopće nema omot VerifySignature. Kriptografske presude dolaze iz klasne razine TPDFlibSignatureVerifier, koja vraća vsValid, vsInvalid ili vsUnknown, ili iz vanjskog validatora kojem vaša pravila usklađenosti već vjeruju.

Dugoročna validacija: DSS, VRI i vremenski žig dokumenta

PAdES-B-LT postoji jer je opozivna infrastruktura smrtna. ETSI EN 319 142-1 §5.4.2.2 specificira Document Security Store: rječnik na razini dokumenta koji nosi certifikate, CRL-ove i OCSP odgovore, opcionalno indeksirane po potpisu kroz VRI unose s ključem koji je hash vrijednosti /Contents svakog potpisa. Tijek u PDFlibPas-u odražava dizajn vremenskog žiga. NewPAdESDSSProcessFromFile otvara proces; AddPAdESDSSCertificate, AddPAdESDSSCRL i AddPAdESDSSOCSP primaju DER blokove; AddPAdESDSSVRI povezuje odabrani materijal s jednim potpisom; EndPAdESDSSProcessToFile zapisuje sve kao inkrementalno ažuriranje. Težak dio ostaje na vašoj strani. Dohvaćanje materijala za opoziv i procjena je li dovoljno svjež da bi ga vrijedilo ugraditi posao je pozivatelja. Knjižnica jamči da su rječnici strukturno usklađeni; ne može jamčiti da je vaš OCSP responder rekao istinu.

Arhivska krajnja točka, B-LTA, dodaje vremenski žig dokumenta: zasebno polje potpisa čija je vrsta DocTimeStamp umjesto Sig, proizvedeno putem SetSignProcessDocTimeStamp s rezerviranom duljinom potpisa. Ono ne zamjenjuje vremenski žig potpisa iz B-T koraka. Vremenski žig potpisa dokazuje kada je određeni potpis postojao; vremenski žig dokumenta štiti cijelu datoteku, uključujući DSS dokaze, i to je element koji dugoročni arhiv obnavlja svakih nekoliko godina kako algoritmi slabe. Zreli arhivski profil nosi oboje. Za čitače koji su stariji od ovih struktura, TPDFlibSignDoc.EnsurePAdESExtensions bilježi ESIC proširenje za razvojne programere u katalogu dokumenta, najavljujući da datoteka koristi značajke definirane od strane ETSI-ja.

Jednu reakciju na sve ovo vrijedi spriječiti, jer izgleda kao pogreška, a nije. Preglednik često prijavljuje "nepoznatu valjanost" na datoteci čija je PAdES struktura potpuno ispravna. Povjerenje i struktura su neovisne osi. Preglednik jednostavno ne može povezati potpisnika s korijenom (root) kojem vjeruje na tom računalu, što je uobičajeno s privatnim CA-ovima i testnim certifikatima, čak i kada ByteRange revizija i CMS provjera prođu. Rješenje je ispravna distribucija korijenskog certifikata ili procjena prema popisima odabranih pouzdanih tijela EU (EU trusted lists) kada je kvalificirani eIDAS status stvarni cilj, umjesto mijenjanja koda za potpisivanje.

Za perspektivu na strani revizije, što znači popisivanje polja potpisa kroz skup dokumenata, ispis ByteRange rasporeda i masovno čitanje DocMDP razina, pogledajte popratni članak o radnom okruženju za usklađenost i potpisivanje. Potpisani dokumenti koji također moraju zadovoljiti arhivska pravila pripadaju tijeku rada opisanom u PDF/A i PDF/UA preflight u Delphiju. Cjelovita dokumentacija API-ja i preuzimanja za procjenu nalaze se na stranici proizvoda losLab PDF Library za Delphi.