Technical Article

PAdES digitalni potpisi u Delphi-ju: potpisivanje i validacija uz PDFlibPas

Validacija jednog PAdES potpisa podrazumeva proveru tri nezavisne stvari, a zelena oznaka potvrde u pregledaču govori vam samo o trećoj. Prvo, niz /ByteRange mora da pokriva ispravne bajtove: opsezi koje on imenuje moraju rekonstruisati tačan ulaz nad kojim je napravljen CMS rezime (digest), bez ikakvih potpisanih bajtova izvan njih. Drugo, sertifikat unutar CMS-a mora da se povezuje sa korenom (root) kojem verujete i da nosi potpisani atribut sertifikata potpisivanja koji PAdES zahteva. Treće, ako profil sadrži vremenski žig (timestamp), RFC 3161 token mora da poveže vrednost potpisa sa trenutkom u vremenu pre nego što je sertifikat istekao. Acrobat sažima sva tri u jednu ikonu; verifikator usaglašenosti ih drži odvojenim, a to bi trebalo da radi i kod koji proizvodi ove datoteke. losLab PDF biblioteka (PDFlibPas) pruža vam stranu potpisivanja tog procesa, ponovno ugrađivanje vremenskog žiga i revizorske pozive za ispitivanje ByteRange-a pre nego što ukažete poverenje datoteci.

Jedna razlika zadaje probleme skoro svakoj prvoj implementaciji PAdES-a, pa je vredi istaći pre bilo kakvog koda. Potpis upisan sa /SubFilter /adbe.pkcs7.detached je potpuno ispravan ISO 32000-1 §12.8 potpis koji će Acrobat prijaviti kao validan. Međutim, to nije PAdES potpis, jer ETSI EN 319 142-1 zahteva ETSI.CAdES.detached na svakom osnovnom nivou. eIDAS verifikator usaglašenosti odbacuje prvi, a prihvata drugi iako je kriptografija identična. Profil je tvrdnja koju dokument iznosi o samom sebi, a ispravno postavljanje te tvrdnje u PDFlibPas-u se postiže jednim jedinim pozivom.

Šta pretvara PDF potpis u PAdES potpis

ETSI EN 319 142-1 definiše četiri osnovna nivoa (baseline levels) izgrađena na CMS formatu. PAdES-B-B je polazna tačka: CAdES potpis u PDF polju potpisa sa ETSI.CAdES.detached SubFilter-om i potpisanim atributom sertifikata potpisivanja. 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-ove i OCSP odgovore potrebne za validaciju u skladište bezbednosti dokumenta (Document Security Store - DSS), tako da datoteka ostaje verifikabilna i nakon što izdavalac (CA) ugasi svoju infrastrukturu. PAdES-B-LTA kruniše ovaj skup vremenskim žigom dokumenta (document timestamp) koji ponovo štiti prikupljene dokaze kako algoritmi vremenom slabe.

PDFlibPas mapira ove koncepte na svoj API procesa potpisivanja. Oznaka profila je SetSignProcessCustomSubFilter. Ako vaša bezbednosna pravila zahtevaju indikaciju vrste obaveze (commitment-type indication) – to se definiše preko SetSignProcessCommitmentType. Eksplicitna politika potpisa se povezuje preko SetSignProcessSignaturePolicy, koja prima OID politike i njen rezime. Jedna podrazumevana vrednost zaslužuje pažnju: kada se algoritam rezimea ostavi na auto, biblioteka bira SHA-256 za ETSI i adbe.pkcs7.detached potpise, a na SHA-1 se vraća samo na zastareloj adbe.pkcs7.sha1 putanji. Ipak, postavite je eksplicitno. Revizori pitaju koji ste heš koristili, a eksplicitnu vrednost u kodu je lakše odbraniti nego podrazumevanu vrednost za čije objašnjenje morate da čitate dokumentaciju.

Kreiranje osnovnog potpisa

Ravan API vodi potpisivanje kao jednokratnu mašinu stanja: otvorite proces na izvornoj datoteci, konfigurišete ga, završite upis u izlaznu datoteku i pročitate kod rezultata. Redosled u nastavku proizvodi PAdES-B-B potpis pomoću SHA-256. Linija koja je najvažnija nema nikakve veze sa samim potpisom. To je namerno predimenzionisana rezervacija /Contents prostora, jer je to jedina stvar koju kasnije ne možete promeniti ako tom potpisu ikada bude trebalo 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;

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

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

PDFlibPas se ne isporučuje sa TSA klijentom, i to je svesno postavljena granica, a ne propust. Biblioteka izračunava heš koji autoritet za vremenski žig (TSA) mora da supotpiše i nakon toga ponovo ugrađuje prošireni CMS; HTTP razmena i sama modifikacija CMS-a između tih koraka pripadaju pozivaocu. Postoji jasan tehnički razlog za ovu podelu. Windows CryptoAPI kontrola koja nominalno dodaje nepotpisane atribute, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, otkazuje sa greškom CRYPT_E_INVALID_INDEX na odvojenom (detached) SignedData rasporedu koji PAdES koristi. Zato prošireni CMS mora doći iz CMS enkodera koji je pod vašom kontrolom. Nijedna biblioteka ne može tiho ugraditi token pomoću jednog sistemskog poziva, a svaka koja tvrdi da to radi obavlja tu modifikaciju negde gde je 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 ovde: 12 znači da imenovano polje potpisa ne postoji, 11 da postojeći CMS nije mogao biti analiziran, a 13 da prošireni CMS više ne staje u rezervisano mesto /Contents. Kod 13 je onaj koji stvara najviše problema jer je jedini lek ponovno potpisivanje: tipičan token vremenskog žiga sa svojim lancem sertifikata iznosi od 4 do 6 KB, a rezervacija od 8192 bajta napravljena tokom B-B koraka postoji upravo da bi ovaj korak imao dovoljno prostora da se upiše.

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

Zelena oznaka potvrde u pregledaču je odluka o poverenju na osnovu skladišta sertifikata tog računara, a ne strukturni zaključak 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 proširenje o kome je ovde reč, bilo da je u pitanju drugi potpis, DSS rečnik ili vremenski žig dokumenta, stiže putem inkrementalnog ažuriranja, a svako ažuriranje dodaje bajtove izvan /ByteRange-a prethodnog potpisa. Ti dodati bajtovi su legitimni. Verifikator i dalje mora da ih klasifikuje u odnosu na pravila izmena dokumenta (modification policy), a DocMDP nivo po polju u kom to pravilo živi čita se 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 vrebaju na ovoj revizorskoj putanji. TPDFlibSignDoc.Open drži datoteku pod ekskluzivnim zaključavanjem deljenja (share lock), tako da verifikator koji takođe želi da hešira sirove bajtove datoteke radi provere CMS-a mora da učita datoteku u memoriju pre nego što je otvori za reviziju. Obrnite taj redosled i čitanje će otkazati zbog zaključavanja koje ste sami postavili. Druga zamka je tiha, a ne zvučna: pandan iz ravnog API-ja GetSignProcessByteRange vraća Integer dok su osnovni ofseti tipa Int64, pa preko 2 GB ravan poziv odseca vrednost bez upozorenja, zbog čega ovaj primer povlači ofsete kroz revizorsku klasu. Vredi pomenuti i jedan nedostatak: ravan sloj uopšte nema omotač za VerifySignature. Kriptografske presude dolaze iz klasnog nivoa TPDFlibSignatureVerifier, koji vraća vsValid, vsInvalid ili vsUnknown, ili iz eksternog verifikatora kome vaša bezbednosna politika već veruje.

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

ETSI EN 319 142-1 definiše skladište bezbednosti dokumenta (Document Security Store): rečnik na nivou dokumenta koji nosi sertifikate, CRL-ove i OCSP odgovore, opciono indeksirane po potpisu kroz VRI unose koji su ključem povezani sa hešom /Contents svakog potpisa. Tok u PDFlibPas-u prati dizajn vremenskog žiga. Funkcija NewPAdESDSSProcessFromFile otvara proces; AddPAdESDSSCertificate, AddPAdESDSSCRL i AddPAdESDSSOCSP prihvataju DER binarne podatke; AddPAdESDSSVRI povezuje izabrani materijal sa jednim potpisom; EndPAdESDSSProcessToFile upisuje sve kao inkrementalno ažuriranje. Teži deo posla ostaje na vašoj strani. Pribavljanje materijala o opozivu i procena da li je dovoljno svež da bi ga vredelo ugraditi zadatak je pozivaoca. Biblioteka garantuje da su rečnici strukturno usaglašeni; ona ne može da garantuje da je vaš OCSP server za proveru statusa 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 SetSignProcessDocTimeStamp sa rezervisanom dužinom potpisa. Ono ne zamenjuje 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 dugoročna arhiva obnavlja svakih nekoliko godina kako kriptografski algoritmi slabe. Zreo arhivski profil nosi oba. Za čitače koji su stariji od ovih struktura, TPDFlibSignDoc.EnsurePAdESExtensions upisuje ESIC programersko proširenje (developer extension) u katalog dokumenta, objavljujući da datoteka koristi ETSI definisane funkcije.

Jednu reakciju na sve ovo vredi preduprediti jer izgleda kao greška, a zapravo nije. Pregledač često prijavljuje „nepoznatu validnost” (validity unknown) za datoteku č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 tom računaru, što je uobičajeno sa privatnim telima za sertifikaciju (CA) i test sertifikatima, čak i kada ByteRange revizija i CMS verifikacija uspešno prođu. Rešenje je ispravna distribucija korenskog sertifikata, ili validacija u odnosu na zvanične EU liste poverenja kada je kvalifikovani eIDAS status stvarni cilj, umesto izmena na kodu za potpisivanje.

Kuda dalje

Za perspektivu sa revizorske strane (što podrazumeva popisivanje polja potpisa kroz skup dokumenata, izbacivanje ByteRange rasporeda i grupno čitanje DocMDP nivoa), pogledajte prateći članak o radnom okruženju za usaglašenost i potpisivanje. Potpisani dokumenti koji takođe moraju da zadovolje arhivsku politiku pripadaju radnom toku opisanom u članku o PDF/A i PDF/UA preflight proverama u Delphi-ju. Kompletna dokumentacija za API i probne verzije dostupne su na stranici proizvoda losLab PDF Library for Delphi.