Tehnički članak

Izgradnja radnog stola za usklađenost i potpisivanje u Delphi-ju pomoću PDFlibPas-a

Radni sto koji povezuje proveru usklađenosti sa digitalnim potpisivanjem mora da koordinira četiri koraka, ovim redosledom, i da ih drži povezane sa jednim skupom bajtova tokom celog procesa. On pokreće PDF/A ili PDF/UA preflight proveru. Primenjuje sve ispravke koje nalazi zahtevaju i čuva korigovanu reviziju. Potpisuje tačno tu reviziju. Zatim ponovo čita potpisanu datoteku i potvrđuje da je potpis zaista pokriva. Ovaj redosled nije kozmetičke prirode. Ako preskočite ponovno čitanje, ukazujete poverenje sopstvenoj putanji pisanja. Ako dozvolite da se preflight provera pokrene nad pogrešnom revizijom, vaš izveštaj o usklađenosti će opisivati datoteku koju zapravo nikada niste isporučili

Deo u kom većina samostalno razvijenih procesnih linija (pipelines) greši jeste spoj između validacije i potpisivanja. Pokrenite ih kao dva odvojena alata sa korakom ispravljanja (remediation pass) između njih, i u opticaj ulaze najmanje tri različite revizije datoteke, svaka sa svojim bajtovima. Preflight izveštaj koji predajete auditoru opisuje jednu od njih. Potpis zamrzava drugu. Ništa u datoteci ne garantuje da su one ista revizija, a često to i nisu. PDFlibPas, losLab PDF razvojna biblioteka za Delphi i C++Builder, postavlja preflight proveru i PAdES potpisivanje iza jedne fasadne klase (facade class), tako da ceo niz može živeti u jednom procesu koji nikada ne gubi trag o tome o kojim bajtovima se radi. Svaki poziv opisan u nastavku danas postoji u biblioteci, kao i svaka zamka zabeležena uz njega

Tri revizije jednog dokumenta i kako nastaje jaz

Prebrojte čuvanja. Original stiže iz prethodnog procesa. Korak ispravljanja ga učitava, uključuje režim usklađenosti i upisuje ispravljenu reviziju. Korak potpisivanja dodaje potpis kao inkrementalno ažuriranje, što je treće upisivanje. Tri čuvanja, tri rasporeda bajtova, a preflight izveštaj ne znači ništa osim ako ne navede koju od ove tri revizije pokriva. SHA-256 datoteke, zabeležen pored svakog pokretanja preflight provere i svakog potpisa, jeste jednostavno sidro koje vam omogućava da dokažete da je revizija koju ste validirali ista ona koju ste potpisali

Jedno ponašanje biblioteke dodatno pooštrava tu disciplinu. Ispravke usklađenosti zahtevane preko SetPDFAMode ili SetPDFUAMode ne stupaju na snagu kada ih pozovete. One se primenjuju tokom čuvanja datoteke. Automatske popravke poput forsiranja zastavica štampanja anotacija ili dodeljivanja redosleda tabulatora za PDF/UA završavaju u izlaznoj datoteci i nigde drugde, tako da provera pokrenuta nad dokumentom koji ste upravo "popravili" u memoriji ne govori ništa o bajtovima koji idu ka potpisniku. Prvo sačuvajte datoteku, a zatim pokrenite preflight proveru nad sačuvanom datotekom. Stanje u memoriji je samo nacrt. Jedino je datoteka na disku stvarna

Preflight provera sa diska i nula koja znači dve stvari

Ulazna tačka ravnog (flat) API-ja za preflight je metoda CheckFileCompliance(FileName, Password, ComplianceTest, Options). Test 1 bira PDF/A (ISO 19005), a test 2 bira PDF/UA (ISO 14289). Metoda otvara datoteku preko strimujućeg čitača biblioteke, tako da nema potrebe za prvobitnim pozivanjem LoadFromFile, i vraća hendler liste stringova koja nosi po jedan nalaz po stavci:

var
  PDF: TPDFlib;
  ListID, I: Integer;
begin
  PDF := TPDFlib.Create;
  try
    ListID := PDF.CheckFileCompliance('invoice-fixed.pdf', '', 1, 0);  // 1 = PDF/A
    if ListID = 0 then
    begin
      if PDF.LastErrorCode <> 0 then
        raise Exception.Create('Preflight could not read the file')
      else
        Writeln('No PDF/A findings');
    end
    else
    begin
      for I := 0 to PDF.GetStringListCount(ListID) - 1 do
        Writeln(PDF.GetStringListItem(ListID, I));
      PDF.ReleaseStringList(ListID);
    end;
  finally
    PDF.Free;
  end;
end;

Zamka leži u povratnoj vrednosti, i to je ona vrsta zamke koja prolazi svaki test srećne putanje (happy-path). Nula znači "nema nalaza". Nula takođe znači "datoteka se ne može otvoriti", jer implementacija vraća 0 kad god je lista rezultata prazna, što uključuje i neuspeh pri čitanju. Radni sto koji čita vrednost 0 kao zeleno svetlo rado će odobriti datoteku koju je neki drugi proces zaključao. Uparivanje poziva sa LastErrorCode, kao što je prikazano iznad, jeste ono što razdvaja ova dva slučaja. Tester takođe otvara datoteku u režimu deljenja koji zabranjuje upisivanje (deny-write share mode), pa ako vaš korak ispravljanja još uvek drži hendler pisca, preflight provera ne uspeva iz razloga koji nema nikakve veze sa usklađenošću, već isključivo sa tokom podataka koji ste zaboravili da oslobodite

Kada čovek, a ne procesni lanac, treba da pročita nalaze, metoda CreatePreflightReport ih prikazuje kao čitljiv izveštaj. ComparePreflightReports upoređuje dva pokretanja, što je uredan način da se pokaže da je ispravka očistila prvobitne nalaze bez prećutnog uvođenja novih

Potpisivanje proverene revizije pomoću SignProcess-a

Kada sačuvana revizija prođe preflight proveru i njen heš bude zabeležen, potpišite tačno tu datoteku i nijednu drugu. SignProcess API se čita kao builder obrazac. Otvorite hendler procesa, konfigurišete ga liniju po liniju, potvrdite izmene i pročitate nazad kod rezultata:

ProcessID := PDF.NewSignProcessFromFile('invoice-fixed.pdf', '');
if ProcessID = 0 then
  raise Exception.Create('Cannot open source for signing');
PDF.SetSignProcessField(ProcessID, 'ApprovalSig');
PDF.SetSignProcessPFXFromFile(ProcessID, 'company.pfx', PfxPassword);
PDF.SetSignProcessInfo(ProcessID, 'Invoice approval', 'Berlin', 'billing@example.com');
PDF.SetSignProcessCustomSubFilter(ProcessID, 'ETSI.CAdES.detached');  // PAdES baseline
PDF.SetSignProcessDigestAlgorithm(ProcessID, 2);                      // SHA-256
PDF.SetSignProcessReserveContentsBytes(ProcessID, 8192);              // room for a later timestamp
PDF.EndSignProcessToFile(ProcessID, 'invoice-signed.pdf');
if PDF.GetSignProcessResult(ProcessID) <> 1 then
  Writeln('Sign failed, code ', PDF.GetSignProcessResult(ProcessID));
PDF.ReleaseSignProcess(ProcessID);

Dve linije u tom nizu nose veću težinu nego što se čini. Poziv SetSignProcessCustomSubFilter sa argumentom ETSI.CAdES.detached bira PAdES potpis profilisan prema standardu ETSI EN 319 142-1 umesto nasleđene adbe.pkcs7.detached porodice, što čini razliku između potpisa koji evropski validator prihvata i onog koji označava kao problematičan. Metoda SetSignProcessReserveContentsBytes popunjava mesto (placeholder) /Contents, a veličina koju ovde izaberete predstavlja odluku o budućnosti: ako vremenski žig potpisa ikada bude sledio, prošireni CMS mora da stane u prostor koji sada rezervišete, jer rezervisano mesto ne može naknadno rasti bez ponovnog potpisivanja celog dokumenta. Rezervišite velikodušno i potrošićete nekoliko kilobajta. Rezervišite preusko i korak postavljanja vremenskog žiga neće uspeti mesecima od danas sa prekoračenjem koje ćete teško povezati sa ovom jednom linijom koda

Metoda GetSignProcessResult odgovara kodom, a ne logičkom vrednošću (boolean), i te kodove vredi sačuvati. 1 označava uspeh. 4 je pogrešna PDF lozinka, 7 pogrešna lozinka sertifikata, 9 PFX koji ne nosi privatni ključ, a 11 neuspeh tokom primene potpisa. Ako ih spojite u običnu tačno/netačno vrednost, odbacujete jedinu informaciju koja razlikuje prijavu pogrešne lozinke od sertifikata bez privatnog ključa. Zabeležite ceo broj

Ponovno čitanje: revizija datoteke koju ste upravo napravili

Nijedan radni sto ne bi trebalo da veruje putanji koja je upisala datoteku koju namerava da sertifikuje. Klasa revizije TPDFlibSignDoc ponovo otvara potpisanu izlaznu datoteku i čita unose rečnika potpisa direktno sa diska:

var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  FS: TFileStream;
  I: Integer;
  SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
  // Capture the size before Open: the audit object holds a share lock on the file
  FS := TFileStream.Create('invoice-signed.pdf', fmOpenRead or fmShareDenyNone);
  SourceSize := FS.Size;
  FS.Free;
  Doc := TPDFlibSignDoc.Create;
  Names := TStringList.Create;
  try
    if not Doc.Open('invoice-signed.pdf', '', False) then Exit;
    Doc.GetSignatureFieldNames(Names);
    for I := 0 to Names.Count - 1 do
      if Doc.GetSignatureValueObjNum(Names[I]) > 0 then  // > 0 means the field is signed
      begin
        RangeStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
        GapStart   := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
        TailStart  := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
        TailLen    := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
        if (RangeStart = 0) and (TailStart + TailLen = SourceSize) then
          Writeln(Names[I], ': signature covers the file to EOF')
        else
          Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
      end;
    Doc.Close;
  finally
    Names.Free;
    Doc.Free;
  end;
end;

Argumenti ValueKey se mapiraju na unose rečnika. Ključ 0 vraća sirovi CMS iz /Contents, ključevi 2 i 3 nazive /Filter i /SubFilter, a od 11 do 14 četiri broja iz ByteRange-a. Tekstualne vrednosti se umesto toga vraćaju preko GetSignatureTextValueByName: ključ 0 je deklarisano vreme potpisivanja, a ključ 5 razlikuje običan Sig od DocTimeStamp-a, što postaje važno kada dokument nosi oba unosa

Hvatanje veličine datoteke na vrhu tog primera je ključno za rad (load-bearing), a ne samo čišćenje koda. Metoda TPDFlibSignDoc.Open drži datoteku pod restriktivnim zaključavanjem deljenja tokom celog svog životnog veka, tako da sve što zahteva sirove bajtove (heširanje potpisanog opsega, ponovno izračunavanje CMS sažetka) mora pročitati datoteku pre nego što se pozove Open. Demo projekat same biblioteke pod nazivom SigningWorkbench prvo učitava čitavu datoteku u memoriju upravo iz tog razloga. Radni sto koji ignoriše ovaj redosled puca povremeno, u zavisnosti od toga koja mašina izgubi trku u izvršavanju

ByteRange aritmetika koja dokazuje pokrivenost

Ispravna datoteka sa jednim potpisom ima ByteRange u obliku [0 a b c]: pokrivenost počinje na ofsetu 0, preskače heksadecimalno rezervisano mesto /Contents između a i b, a zatim se nastavlja kroz bajt b+c. Kada je b+c jednako veličini datoteke, potpis pokriva sve do kraja datoteke, što je rezultat koji želite. Kada to nije slučaj, neko je dodao inkrementalno ažuriranje nakon što je potpis prijavljen. To je potpuno legitimno prema standardu ISO 32000-1 §12.8, jer kasnija popunjavanja obrazaca, drugi potpis i DSS rečnik stižu upravo na ovaj način. To je takođe činjenica koju bi revizorski trag trebalo da zabeleži u trenutku potpisivanja, umesto da je rekonstruiše pod pritiskom tokom spora

Pazite na širinu celog broja dok radite ovu aritmetiku. Pandan ravnog API-ja GetSignProcessByteRange vraća 32-bitni Integer, ali su vrednosti u osnovi tipa Int64, pa na datoteci većoj od 2 GB ravni pristupnik vrši prećutno skraćivanje (truncation). Opredelite se za klasni nivo TPDFlibSigner.GetByteRange, koji vraća Int64, ili parsirajte vrednosti iz GetSignatureValueByName na način na koji to radi gornji revizorski kod

Ono što vam biblioteka prepušta

Dve granice je bolje naučiti tokom dizajna nego u poslednjem sprintu pre isporuke. Ravni TPDFlib API uopšte ne sadrži omotač za verifikaciju potpisa. Kriptografska verifikacija živi jedan sloj niže, u klasi TPDFlibSignatureVerifier, čija metoda VerifySignature daje odgovore: validan, nevalidan ili nepoznat. Takođe, ne postoji ugrađeni HTTP klijent za RFC 3161 autoritete za vremenske žigove (TSA). Biblioteka izračunava heš za slanje i ponovo ugrađuje prošireni CMS kada se token vrati, ali mrežni prenos do TSA servera morate napisati sami. Oba elementa je lako omotati, ali ih je izuzetno neprijatno otkriti kao nedostajuće nedelju dana pre izlaska softvera, stoga ih projektujte od prve skice

Jedno pitanje o usklađenosti vredi jasno rešiti jer ono odlučuje o tome gde ide poslednja kontrola: da li dodavanje potpisa narušava PDF/A status? Ne samo po sebi. Potpis stiže kao inkrementalno ažuriranje, a standard ISO 19005-2 i noviji eksplicitno dozvoljavaju potpisane dokumente. Zamka je u izgledu samog potpisa, koji igra po istim pravilima kao i bilo koji drugi sadržaj stranice, što uključuje ugrađene fontove i odsustvo boja zavisnih od uređaja. Zato je poslednji kontrolni prolaz u radnom stolu još jedno pokretanje preflight provere, ovog puta nad potpisanim izlazom. Tretirajte CheckFileCompliance kao brzu proveru unutar procesnog lanca, ali i dalje verifikujte kandidate za izdanje pomoću nezavisnog alata kao što je veraPDF, jer validatori implementiraju preklapajuće, ali ne i identične skupove pravila. Kada se ova dva alata ne slažu, tekst nalaza obično imenuje klauzulu koju treba pročitati

Jedna sekvencijalna tačka proizilazi iz svega ovoga. Potpisivanje i dodavanje vremenskog žiga nisu jedan isti prolaz: osnovni potpis se piše prvi, a zatim zaseban proces vremenskog žiga proširuje CMS unutar rezervisanog prostora /Contents, što je upravo razlog zašto je linija za rezervaciju bajtova ranije imala toliku važnost. Za slojeve vremenskog žiga i dugotrajne validacije koji se nadograđuju na ovaj radni sto, pregled PAdES potpisivanja i validacije vodi potpis od osnovnog do B-LT nivoa, dok preflight polovina ide dublje u vodiču za PDF/A i PDF/UA preflight proveru. Kompletna dokumentacija za API i probne verzije nalaze se na stranici proizvoda PDFlibPas