Pracovní prostředí, které řetězí kontrolu shody s digitálním podepisováním, musí zkoordinovat čtyři kroky, a to právě v tomto pořadí, a po celou dobu je udržet svázané s jedinou sadou bajtů. Spustí preflight podle PDF/A nebo PDF/UA. Provede opravy, které si nálezy vyžádají, a uloží opravenou revizi. Podepíše přesně tuto revizi. Nakonec podepsaný soubor znovu načte a ověří, že jej podpis skutečně pokrývá. To pořadí není kosmetické. Vynecháte-li zpětné načtení, důvěřujete vlastní zápisové cestě; necháte-li preflight běžet nad špatnou revizí, popisuje váš protokol o shodě soubor, který jste nikdy nevydali
Nejčastější chyba doma psaných pipeline leží ve švu mezi validací a podpisem. Spusťte je jako dva samostatné nástroje s opravným průchodem mezi nimi a vzniknou nejméně tři odlišné revize souboru, každá s vlastními bajty. Protokol z preflightu, který předáte auditorovi, popisuje jednu z nich. Podpis zmrazí jinou. Nic v souboru neříká, že jde o tutéž revizi, a často to tak není. PDF Library for Delphi, knihovna losLab PDF Developer Library pro Delphi a C++Builder, staví preflight i podepisování PAdES za jedinou fasádní třídu, takže celá sekvence může běžet v jednom procesu, který nikdy neztratí přehled o tom, o kterých bajtech mluví. Každé volání níže v knihovně dnes existuje a stejně tak i každá past, na kterou u něj upozorňujeme
Tři revize jednoho dokumentu a jak vzniká mezera
Spočítejte uložení. Originál přijde z nadřazeného systému. Opravný průchod jej načte, zapne režim shody a zapíše opravenou revizi. Podepisovací průchod připojí podpis jako inkrementální aktualizaci, což je třetí zápis. Tři uložení, tři rozložení bajtů, a protokol z preflightu neznamená nic, dokud nepojmenuje, kterou ze tří revizí pokrývá. Otisk SHA-256 souboru, zaznamenaný u každého běhu preflightu i u každého podpisu, je levná kotva, díky níž doložíte, že revize, kterou jste validovali, je tatáž revize, kterou jste podepsali
Jedno chování knihovny tuto disciplínu ještě utahuje. Opravy shody vyžádané přes SetPDFAMode nebo SetPDFUAMode se neprojeví ve chvíli, kdy je zavoláte. Uplatní se až při ukládání. Automatické opravy, jako je vynucení příznaků tisku u anotací nebo přiřazení pořadí tabulátoru podle PDF/UA, se propíšou do výstupního souboru a nikam jinam, takže kontrola spuštěná nad dokumentem, který jste právě „opravili“ v paměti, neříká nic o bajtech mířících k podepisovači. Nejprve uložte, teprve pak spusťte preflight nad uloženým souborem. Stav v paměti je jen koncept; skutečný je pouze soubor na disku
Preflight z disku a nula, která znamená dvě věci
Plochým vstupním bodem preflightu je CheckFileCompliance(FileName, Password, ComplianceTest, Options). Test 1 volí PDF/A (ISO 19005), test 2 volí PDF/UA (ISO 14289). Soubor otevře přes proudový čtecí modul knihovny, takže není nutné nejdřív volat LoadFromFile, a vrátí handle seznamu řetězců s jedním nálezem na položku:
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;
Past sedí v návratové hodnotě a je z toho druhu, který projde každým testem šťastné cesty. Nula znamená „žádné nálezy“. Nula ale také znamená „soubor se nepodařilo otevřít“, protože implementace vrací 0 pokaždé, když se výsledný seznam vrátí prázdný, chybu čtení nevyjímaje. Pracovní prostředí, které čte 0 jako zelenou, s klidem schválí soubor, jejž si zamkl nějaký jiný proces. Právě spárování volání s LastErrorCode, jak je uvedeno výše, oba případy odliší. Kontrolní modul navíc otevírá soubor v režimu sdílení, který zakazuje zápis, takže pokud váš opravný krok stále drží zapisovací handle, preflight selže z důvodu, který nemá nic společného se shodou a všechno se streamem, jejž jste zapomněli uvolnit
Když nálezy potřebuje číst člověk, a ne pipeline, CreatePreflightReport je vykreslí jako čitelný protokol. ComparePreflightReports porovná dva běhy, což je úhledný způsob, jak ukázat, že oprava odstranila původní nálezy, aniž by potichu zavedla nové
Podepsání zkontrolované revize pomocí SignProcess
Jakmile uložená revize projde preflightem a její otisk je zaznamenán, podepište přesně tento soubor a žádný jiný. API SignProcess se čte jako builder. Otevřete handle procesu, nakonfigurujte jej řádek po řádku, potvrďte a nakonec si přečtěte výsledný kód
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'); // základní profil PAdES
PDF.SetSignProcessDigestAlgorithm(ProcessID, 2); // SHA-256
PDF.SetSignProcessReserveContentsBytes(ProcessID, 8192); // místo pro pozdější časové razítko
PDF.EndSignProcessToFile(ProcessID, 'invoice-signed.pdf');
if PDF.GetSignProcessResult(ProcessID) <> 1 then
Writeln('Sign failed, code ', PDF.GetSignProcessResult(ProcessID));
PDF.ReleaseSignProcess(ProcessID);
Dva řádky té sekvence váží víc, než vypadají. SetSignProcessCustomSubFilter s hodnotou ETSI.CAdES.detached volí podpis PAdES podle profilu ETSI EN 319 142-1 namísto starší rodiny adbe.pkcs7.detached, a v tom je rozdíl mezi podpisem, který evropský validátor přijme, a podpisem, který označí za vadný. SetSignProcessReserveContentsBytes vypodloží zástupné místo /Contents a velikost, kterou tu zvolíte, je rozhodnutí o budoucnosti: má-li kdy následovat časové razítko podpisu, musí se zvětšená struktura CMS vejít do prostoru, který rezervujete teď, protože zástupné místo už později nelze zvětšit bez opětovného podepsání celého dokumentu. Rezervujte štědře a přijdete o pár kilobajtů. Rezervujte příliš těsně a krok s časovým razítkem selže za několik měsíců přetečením, které budete jen těžko spojovat s tímto jediným řádkem
GetSignProcessResult odpovídá kódem, nikoli logickou hodnotou, a ty kódy stojí za uchování. 1 je úspěch. 4 je špatné heslo k PDF, 7 špatné heslo k certifikátu, 9 soubor PFX bez soukromého klíče, 11 selhání během aplikace podpisu. Sesypte je do true/false a zahodíte jedinou informaci, která odliší případ podpory se špatným heslem od případu s klíčem bez soukromé části. Logujte celé číslo
Zpětné načtení: audit souboru, který jste právě vyrobili
Žádné pracovní prostředí by nemělo důvěřovat cestě, jež zapsala soubor, který se chystá certifikovat. Auditní třída TPDFlibSignDoc znovu otevře podepsaný výstup a čte položky slovníku podpisu přímo z disku:
var
Doc: TPDFlibSignDoc;
Names: TStringList;
FS: TFileStream;
I: Integer;
SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
// Velikost zjistěte před voláním Open: auditní objekt drží na souboru zámek sdílení
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 znamená, že pole je podepsané
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;
Argumenty ValueKey se mapují na položky slovníku. Klíč 0 vrací surovou strukturu CMS z /Contents, klíče 2 a 3 názvy /Filter a /SubFilter a klíče 11 až 14 čtyři čísla pole ByteRange. Textové hodnoty se místo toho získávají přes GetSignatureTextValueByName: klíč 0 je deklarovaný čas podpisu a klíč 5 odliší běžný Sig od DocTimeStamp, což začne být důležité ve chvíli, kdy dokument nese obojí
Zjištění velikosti souboru na začátku toho příkladu je nosný prvek, ne úklid. TPDFlibSignDoc.Open drží soubor pod restriktivním zámkem sdílení po celou dobu své existence, takže cokoli, co potřebuje surové bajty (hašování podepsaného rozsahu, přepočet otisku CMS), musí soubor přečíst dřív, než se Open zavolá. Ukázka SigningWorkbench dodávaná s knihovnou načítá celý soubor do paměti právě z tohoto důvodu a pracovní prostředí, které toto pořadí ignoruje, selhává nahodile, a to na tom stroji, jenž zrovna prohraje závod
Aritmetika ByteRange, která doloží pokrytí
Zdravý soubor s jedním podpisem má ByteRange ve tvaru [0 a b c]: pokrytí začíná na offsetu 0, přeskočí hexadecimální zástupné místo /Contents mezi offsety a a b a pak pokračuje až po bajt b+c. Když se b+c rovná velikosti souboru, podpis pokrývá všechno až do konce souboru, což je výsledek, který chcete. Když zaostane, někdo po zápisu podpisu připojil inkrementální aktualizaci. To je podle ISO 32000-1§12.8 zcela legitimní, protože pozdější vyplnění formuláře, druhý podpis i slovník DSS přicházejí přesně touto cestou. A je to zároveň přesně ten fakt, který by auditní stopa měla zaznamenat v okamžiku podpisu, místo aby jej pod tlakem rekonstruovala během sporu
Při této aritmetice hlídejte šířku celých čísel. Funkce GetSignProcessByteRange z plochého API vrací 32bitový Integer, jenže podkladové hodnoty jsou Int64, takže u souboru nad 2 GB plochý přístupový prvek tiše ořízne. Sáhněte po TPDFlibSigner.GetByteRange z třídní vrstvy, která vrací Int64, nebo hodnoty rozeberte z GetSignatureValueByName tak, jak to dělá auditní kód výše
Co knihovna nechává na vás
Dvě hranice je lepší poznat ve fázi návrhu než v posledním sprintu. Ploché API TPDFlib neobsahuje vůbec žádnou obálku pro ověření podpisu. Kryptografické ověření sídlí o vrstvu níž, ve třídě TPDFlibSignatureVerifier, jejíž metoda VerifySignature odpovídá platný, neplatný, nebo neznámý. Chybí také vestavěný klient HTTP pro autority časových razítek podle RFC 3161. Knihovna spočítá otisk k odeslání a po návratu tokenu znovu vloží rozšířenou strukturu CMS, ale síťové kolečko k TSA si musíte napsat sami. Obojí se obaluje snadno a obojí je vysloveně nepříjemné objevit jako chybějící týden před vydáním, takže s nimi počítejte hned od prvního náčrtu
Jednu otázku ke shodě stojí za to vyřešit rovnou, protože rozhoduje o tom, kam patří poslední brána: rozbije přidání podpisu PDF/A? Samo o sobě ne. Podpis přichází jako inkrementální aktualizace a ISO 19005-2 a novější podepsané dokumenty výslovně povolují. Háček je ve vzhledu podpisu, který hraje podle stejných pravidel jako jakýkoli jiný obsah stránky, včetně vložených písem a zákazu barev závislých na zařízení. Poslední branou pracovního prostředí je tedy ještě jeden běh preflightu, tentokrát nad podepsaným výstupem. Berte CheckFileCompliance jako rychlou kontrolu uvnitř pipeline a kandidáty na vydání přesto ověřte nezávislým nástrojem, například veraPDF, protože validátory implementují překrývající se, avšak nikoli totožné sady pravidel; když se ty dva neshodnou, text nálezu obvykle pojmenuje článek, který si máte přečíst
Ze všeho výše plyne jeden bod o pořadí. Podepsání a orazítkování nejsou jeden průchod: nejprve se zapíše základní podpis a teprve pak samostatný proces časového razítka rozšíří strukturu CMS uvnitř rezervovaného prostoru /Contents, což je přesně důvod, proč dřívější řádek s rezervovanými bajty vážil tolik. Vrstvám časového razítka a dlouhodobé validace, které na tomto pracovním prostředí staví, se věnuje průvodce podepisováním a validací PAdES, jenž dovede podpis od základního profilu k B-LT, a preflightové polovině jde více do hloubky příručka preflightu PDF/A a PDF/UA. Kompletní dokumentaci API a zkušební verze ke stažení najdete na produktové stránce PDF Library for Delphi