Technical Article

Gradnja orodne vrstice za skladnost in podpisovanje v Delphiju s PDFlibPas

Delovna miza, ki povezuje preverjanje skladnosti z digitalnim podpisovanjem, mora usklajevati štiri korake v tem vrstnem redu in jih ves čas ohranjati povezane z istim naborom bajtov. Izvede preverjanje PDF/A ali PDF/UA. Uveljavi popravke, ki jih zahtevajo ugotovitve, in shrani popravljeno različico. Podpiše točno to različico. Nato prebere podpisano datoteko nazaj in potrdi, da jo podpis resnično pokriva. Vrstni red ni kozmetičen. Če izpustite branje nazaj, zaupate svoji lastni poti zapisovanja; če zaženete preverjanje na napačni različici, vaše poročilo o skladnosti opisuje datoteko, ki je niste nikoli poslali.

Del, ki ga večina domačih cevovodov naredi narobe, je stik med validacijo in podpisovanjem. Če jih zaženete kot dve ločeni orodji z vmesnim popravkom, nastanejo vsaj tri ločene različice datoteke, vsaka s svojimi bajti. Poročilo o preverjanju, ki ga predložite revizorju, opisuje eno od njih. Podpis zamrzne drugo. Nič v datoteki ne navaja, da gre za isto različico, in pogosto nista. PDFlibPas, losLab knjižnica za razvijalce PDF za Delphi in C++Builder, postavlja preverjanje in podpisovanje PAdES za en sam fasadni razred, tako da lahko celotno zaporedje živi v enem procesu, ki nikoli ne izgubi sledi o tem, o katerih bajtih govori. Vsak klic spodaj že obstaja v knjižnici, prav tako pa tudi vsaka past, navedena ob njem.

Tri različice enega dokumenta in kako nastane vrzel

Preštejte shranjevanja. Izvirnik prispe iz prejšnjega koraka. Popravljalni korak ga naloži, vklopi način skladnosti in zapiše popravljeno različico. Podpisovalni korak doda podpis kot inkrementalno posodobitev, kar je tretji zapis. Tri shranjevanja, tri postavitve bajtov in poročilo o preverjanju ne pomenijo ničesar, če ne navajajo, katero od treh različic pokrivajo. SHA-256 datoteke, zabeležen ob vsakem zagonu preverjanja in vsakem podpisu, je preprosto sidro, ki vam omogoča dokazovanje, da je preverjena različica enaka podpisani.

Eno vedenje knjižnice još dodatno zaostruje to disciplino. Popravki skladnosti, zahtevani prek SetPDFAMode ali SetPDFUAMode, ne stopijo v veljavo, ko jih pokličete. Uveljavijo se med shranjevanjem. Samodejna popravila, kot je prisilna nastavitev tiskalnih zastavic za komentarje ali dodelitev tabulatorskega vrstnega reda PDF/UA, pristanejo v izhodni datoteki in nikjer drugje, zato preverjanje dokumenta, ki ste ga pravkar "popravili" v pomnilniku, ne pove ničesar o bajtih, namenjenih podpisniku. Najprej shranite, nato preverite shranjeno datoteko. Stanje v pomnilniku je osnutek; le datoteka na disku je resnična.

Preverjanje z diska in ničla, ki pomeni dve stvari

Vstopna točka za preverjanje je CheckFileCompliance(FileName, Password, ComplianceTest, Options). Test 1 izbere PDF/A (ISO 19005), test 2 pa PDF/UA (ISO 14289). Datoteko odpre prek pretočnega bralnika knjižnice, zato je ni treba najprej naložiti z LoadFromFile, in vrne ročaj seznama nizov, ki vsebuje po eno ugotovitev na vnos:

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.GetStringItem(ListID, I));
      PDF.ReleaseStringList(ListID);
    end;
  finally
    PDF.Free;
  end;
end;

Past je v povratni vrednosti in je tiste vrste, ki prestane vsak preprost test. Nič pomeni "brez ugotovitev". Nič pa pomeni tudi "datoteke ni bilo mogoče odpreti", saj implementacija vrne 0 vsakič, ko se vrne prazen seznam rezultatov, vključno z napako pri branju. Delovna miza, ki bere 0 kot zeleno luč, bo z veseljem odobrila datoteko, ki jo je zaklenil drug proces. Povezovanje klica z LastErrorCode, kot je prikazano zgoraj, loči ta dva primera. Preverjalnik odpre datoteko v načinu skupne rabe z zavrnitvijo pisanja (deny-write share mode), zato če vaš korak popravila še vedno drži ročaj za pisanje, preverjanje spodleti iz razloga, ki nima nobene zveze s skladnostjo in ima vse opraviti s tokom, ki ste ga pozabili sprostiti.

Ko mora ugotovitve prebrati oseba in ne avtomatiziran cevovod, jih CreatePreflightReport izriše kot berljivo poročilo. ComparePreflightReports primerja dva zagona, kart je urejen način za prikaz, da je popravilo odpravilo prvotne ugotovitve, ne da bi tiho uvedlo nove.

Podpisovanje preverjene različice s SignProcess

Ko shranjena različica prestane preverjanje in je njen zgoščeni naslov zabeležen, podpišite natanko to datoteko in nobene druge. API SignProcess deluje kot graditelj. Odprite ročaj procesa, ga konfigurirajte vrstico za vrstico, potrdite in nato preberite povratno kodo 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 vrstici v tem zaporedju imata večjo težo, kot je videti. SetSignProcessCustomSubFilter z ETSI.CAdES.detached izbere podpis PAdES, kot je profiliran v ETSI EN 319 142-1, nemesto starejše družine adbe.pkcs7.detached, kar predstavlja razliko med podpisom, ki ga evropski potrjevalec sprejme, in tistim, ki ga označi z zastavico. SetSignProcessReserveContentsBytes napolni ogrado /Contents, izbrana velikost pa je odločitev o prihodnosti: če bo podpisu kdaj sledil časovni žig, se mora povečan CMS prilegati prostoru, ki ga rezervirate zdaj, saj ograde pozneje ni mogoče povečati brez ponovnega podpisovanja celotne zadeve. Če rezervirate velikodušno, izgubite nekaj kilobajtov. Če rezervirate pretesno, korak časovnega žiga čez mesece spodleti z overflow napako, ki jo boste težko povezali s to vrstico.

GetSignProcessResult vrne kodo in ne logične vrednosti, te kode pa je vredno ohraniti. 1 pomeni uspeh. 4 je napačno geslo PDF, 7 napačno geslo certifikata, 9 PFX brez zasebnega ključa, 11 pa napaka med nanašanjem podpisa. Če jih združite v True/False, zavržete edini podatek, ki loči podporni primer napačnega gesla od primere ključa brez zasebnega dela. Zabeležite celo število.

Branje nazaj: revizija pravkar ustvarjene datoteke

Nobena delovna miza ne bi smela zaupati poti, ki je zapisala datoteko, ki jo namerava certificirati. Revizijski razred TPDFlibSignDoc ponovno odpre podpisani izhod in prebere vnose v slovarju podpisov neposredno z 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 unusual ByteRange layout');
      end;
    Doc.Close;
  finally
    Names.Free;
    Doc.Free;
  end;
end;

Argumenti ValueKey se preslikajo v vnose v slovarju. Ključ 0 vrne surovi CMS iz /Contents, ključa 2 in 3 vrneta imeni /Filter in /SubFilter, ključi od 11 do 14 pa štiri številke ByteRange. Tekstovne vrednosti se namesto tega vrnejo prek GetSignatureTextValueByName: ključ 0 je navedeni čas podpisovanja, ključ 5 pa loči običajen Sig od DocTimeStamp, kar je pomembno, ko dokument vsebuje oboje.

Zajem velikosti datoteke na vrhu tega primera je ključnega pomena in ne le čiščenje. Metoda TPDFlibSignDoc.Open drži datoteko pod omejevalnim zaklepom skupne rabe celoten življenjski cikel, zato mora vse, kar potrebuje surove bajte (zgoščevanje podpisanega obsega, ponovni izračun CMS povzetka), prebrati datoteko pred klicem Open. Predstavitveni program SigningWorkbench v knjižnici iz prav tega razloga najprej prebere celotno datoteko v pomnilnik, delovna miza, ki ignorira ta vrstni red, pa občasno spodleti na tistem računalniku, ki izgubi tekmo.

Aritmetika ByteRange, ki dokazuje pokritost

Zdrava datoteka z enim podpisom ima ByteRange v obliki [0 a b c]: pokritost se začne pri odmiku 0, preskoči heksadecimalno ogrado /Contents med a in b ter se nadaljuje skozi bajt b+c. Ko je b+c gradbeno enak velikosti datoteke, podpis pokriva vse do konca datoteke, kar je želeni rezultat. Če je krajši, je nekdo dodal inkrementalno posodobitev po tem, ko je bil podpis že zapisan. To je popolnoma zakonito po standardu ISO 32000-1§12.8, saj kasnejša izpolnjevanja obrazcev, drugi podpis in slovar DSS prispejo natanko na ta način. To je tudi dejstvo, ki bi ga morala revizijska sled zabeležiti v času podpisovanja, namesto da se rekonstruira pod pritiskom med sporom.

Pri izvajanju te aritmetike pazite na širino celih števil. Funkcija GetSignProcessByteRange v ravnem API-ju vrne 32-bitno celo število, vendar so osnovne vrednosti Int64, zato na datoteki, večji od 2 GB, ravni dostopnik tiho odreže podatke. Uporabite razredni nivo TPDFlibSigner.GetByteRange, ki vrne Int64, ali pa razčlenite vrednosti iz GetSignatureValueByName, kot to počne revizijska koda zgoraj.

Kaj knjižnica prepušča vam

Dve omejitvi je bolje spoznati v času načrtovanja kot v zadnjem sprintu. Ravni API TPDFlib sploh ne vsebuje ovoja za preverjanje podpisov. Kriptografsko preverjanje živi raven nižje, v TPDFlibSignatureVerifier, katerega metoda VerifySignature vrne veljavno, neveljavno ali neznano. Prav tako ni vgrajenega odjemalca HTTP za avtoritete časovnih žigov RFC 3161. Knjižnica izračuna zgoščeno vrednost za pošiljanje in ponovno vgradi razširjeni CMS, ko se vrne žeton, vendar morate omrežno potovanje do TSA napisati sami. Obe zadevi je enostavno oviti, a zelo neprijetno ugotoviti, da manjkata teden dni pred izdajo, zato ju načrtujte od samega začetka.

Eno vprašanje o skladnosti je vredno jasno razjasniti, saj določa, kam gre zadnji prehod: ali dodajanje podpisa krši PDF/A? Ne samo po sebi. Podpis prispe kot inkrementalna posodobitev, standard ISO 19005-2 in novejši pa izrecno dovoljujejo podpisane dokumente. Past je vizualni izgled podpisa, ki igra po enakih pravilih kot katera koli druga vsebina strani, vključno z vgrajenimi pisavami in brez barv, odvisnih od naprave. Zato je končni prehod v delovni mizi još en zagon preverjanja, tokrat na podpisani izhodni datoteki. Obravnavajte CheckFileCompliance kot hitro preverjanje znotraj cevovoda, kandidate za izdajo pa še vedno preverite z neodvisnim orodjem, kot je veraPDF, saj potrjevalci izvajajo prekrivajoče se, a ne povsem enake nabor pravil; ko se ne strinjata, besedilo ugotovitve običajno poimenuje člen, ki ga je treba prebrati.

Za časovne žige in dolgoročne potrjevalne sloje, ki gradijo na tej delovni mizi, vodnik PAdES podpisovanje in potrjevanje popelje podpis od izhodišča do B-LT, del o preverjanju pa gre globlje v priročniku PDF/A in PDF/UA preverjanje v Delphiju. Celotna dokumentacija API in poskusni prenosi so na voljo na produktni strani PDFlibPas.