Ověřit jeden podpis PAdES znamená zkontrolovat tři nezávislé věci a zelená fajfka v prohlížeči vám říká jen o té třetí. Zaprvé, pole /ByteRange musí pokrývat správné bajty: rozsahy, které pojmenovává, musí zrekonstruovat přesně ten vstup, nad nímž byl otisk CMS pořízen, a mimo ně nesmí zůstat žádný podepsaný bajt. Zadruhé, certifikát uvnitř CMS musí vést řetězem ke kořeni, jemuž důvěřujete, a nést podepsaný atribut signing-certificate, který PAdES vyžaduje. Zatřetí, hlásí-li profil časové razítko, musí token podle RFC 3161 vázat hodnotu podpisu k okamžiku před vypršením certifikátu. Acrobat všechny tři věci slévá do jediné ikony; kontrolor shody je drží odděleně a stejně by je měl držet i kód, který tyto soubory vyrábí. losLab PDF Library (PDF Library for Delphi) vám dává podepisovací stranu, opětovné vložení časového razítka a auditní volání k prozkoumání pole ByteRange dřív, než mu uvěříte
Jeden rozdíl podráží nohy téměř každé první implementaci PAdES, takže stojí za zmínku ještě před jakýmkoli kódem. Podpis zapsaný s /SubFilter /adbe.pkcs7.detached je zcela v pořádku podle ISO 32000-1 §12.8 a Acrobat jej ohlásí jako platný. Zároveň to není podpis PAdES, protože ETSI EN 319 142-1 vyžaduje ETSI.CAdES.detached na každé baseline úrovni. Kontrolor shody podle eIDAS ten první odmítne a druhý přijme, přestože je kryptografie totožná. Profil je tvrzení, které dokument činí sám o sobě, a nastavit toto tvrzení správně je v PDF Library for Delphi jedno volání
Co dělá z podpisu v PDF podpis PAdES
ETSI EN 319 142-1 definuje čtyři baseline úrovně naskládané na formát CMS. PAdES-B-B je vstupní branou: podpis CAdES v podpisovém poli PDF se SubFilterem ETSI.CAdES.detached a podepsaným atributem signing-certificate. PAdES-B-T přidává časové razítko podle RFC 3161 nad hodnotou podpisu, čímž dokládá, že podpis existoval před okamžikem, který nikdo nemůže antedatovat. PAdES-B-LT vkládá certifikáty, seznamy CRL a odpovědi OCSP potřebné k ověření do úložiště Document Security Store, takže soubor zůstává ověřitelný i poté, co vydávající CA odstaví svou infrastrukturu. PAdES-B-LTA celou stavbu korunuje časovým razítkem dokumentu, jež nashromážděné důkazy znovu chrání ve chvíli, kdy algoritmy slábnou
PDF Library for Delphi tyto pojmy mapuje na své API podepisovacího procesu. Značkou profilu je SetSignProcessCustomSubFilter. Vyžaduje-li vaše politika údaj o typu závazku (doklad o původu, doklad o schválení nebo některý z dalších identifikátorů ETSI číslovaných 1 až 6), ten jde přes SetSignProcessCommitmentType. Explicitní politika podpisu se připojuje přes SetSignProcessSignaturePolicy, které bere OID politiky a její otisk. Jedno výchozí nastavení si zaslouží pozornost: je-li algoritmus otisku ponechán na automatice, volí knihovna pro podpisy ETSI a adbe.pkcs7.detached algoritmus SHA-256 a k SHA-1 sahá jen u starší cesty adbe.pkcs7.sha1. Nastavte jej stejně výslovně. Auditoři se ptají, jaký hash jste použili, a explicitní hodnota v kódu se obhajuje snáz než výchozí volba, kterou musíte jít vysvětlovat do manuálu
Výroba základního podpisu
Ploché API řídí podepisování jako jednorázový stavový automat: otevřete proces nad zdrojovým souborem, nakonfigurujete jej, dokončíte do výstupního souboru, přečtete výsledný kód. Sekvence níže vytvoří podpis PAdES-B-B se SHA-256. Řádek, na kterém záleží nejvíc, přitom se samotným podpisem nemá nic společného. Je to záměrně předimenzovaná rezervace /Contents, protože právě tu nelze později změnit, kdyby k tomuto podpisu mělo být někdy přidáno časové razítko
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); // místo pro pozdější razítko
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;
NewSignProcessFromFile vrací 0, když zdroj vůbec nelze otevřít. Poté GetSignProcessResult odlišuje způsoby selhání, které v produkci skutečně nastávají: 4 znamená špatné heslo k PDF, 7 špatné heslo k PFX, 9 soubor certifikátu bez soukromého klíče, 10 výstupní cestu, do níž nelze zapisovat, 11 selhání během vkládání bajtů podpisu. Zalogování číselného kódu vedle názvu vstupního souboru promění mlhavý požadavek podpory v minutovou diagnózu
Přidání časového razítka podle RFC 3161, které za vás knihovna nestáhne
PDF Library for Delphi nedodává žádného klienta TSA a jde o záměrnou hranici, ne o mezeru. Knihovna spočítá otisk, který musí autorita časových razítek spolupodepsat, a poté rozšířené CMS znovu vloží; výměna po HTTP a chirurgie nad CMS mezi tím patří volajícímu. Pro to rozdělení existuje tvrdý technický důvod. Řídicí prvek Windows CryptoAPI, který nominálně přidává nepodepsané atributy, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, selhává s CRYPT_E_INVALID_INDEX nad odpojeným rozvržením SignedData, jaké PAdES používá. Rozšířené CMS tedy musí vzejít z kodéru CMS pod vaší vlastní kontrolou. Žádná knihovna nedokáže token tiše přiložit jediným systémovým voláním a každá, která to tvrdí, provádí tu chirurgii někde, kam nevidíte
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);
// obě volání níže jsou aplikační kód: HTTP POST na vaši TSA
// a překódování CMS, které token připojí jako nepodepsaný atribut
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;
Hlídejte tady výsledné kódy: 12 znamená, že pojmenované podpisové pole neexistuje, 11 že stávající CMS nešlo rozebrat a 13 že se rozšířené CMS už nevejde do rezervovaného zástupného místa /Contents. Kód 13 je ten, co bolí, protože jedinou nápravou je podepsat znovu: typický token časového razítka s řetězcem certifikátů má 4 až 6 KB a rezervace 8192 bajtů udělaná v kroku B-B existuje právě proto, aby měl tento krok kam dosednout
Ověření začíná u ByteRange, ne u řetězce certifikátů
Zelená fajfka v prohlížeči je rozhodnutí o důvěře vůči úložišti certifikátů daného stroje, nikoli strukturální verdikt o souboru. Programové ověřování by mělo začínat níž, otázkou, kterou inkrementální aktualizace činí záludnou: které bajty vlastně každý podpis pokrývá? Každé vylepšení, o němž je tu řeč, ať jde o druhý podpis, slovník DSS nebo časové razítko dokumentu, přichází inkrementální aktualizací a každá taková aktualizace připojí bajty mimo /ByteRange dřívějšího podpisu. Tyto připojené bajty jsou legitimní. Validátor je přesto musí zařadit vůči politice úprav dokumentu a úroveň DocMDP pro jednotlivá pole, v níž tato politika žije, se čte přes GetSignatureDocMDPLevelByName
var
Doc: TPDFlibSignDoc;
Names: TStringList;
I: Integer;
B0, B1, B2, B3, FileSize: Int64;
begin
FileSize := TFile.GetSize('invoice-bt.pdf'); // před Open: SignDoc drží zámek sdílení
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 znamená skutečně podepsáno
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;
V této auditní cestě číhají dvě pasti. TPDFlibSignDoc.Open drží soubor výhradním zámkem sdílení, takže validátor, který chce zároveň hašovat surové bajty souboru kvůli ověření CMS, musí soubor načíst do paměti dřív, než jej otevře k auditu. Otočte to pořadí a čtení selže na zámku, který jste si nasadili sami. Druhá past je tichá, ne hlasitá: protějšek z plochého API GetSignProcessByteRange vrací Integer, zatímco podkladové offsety jsou Int64, takže nad 2 GB ploché volání bez námitek ořízne, a proto tento příklad tahá offsety přes auditní třídu. Za pojmenování stojí i jedna nepřítomnost. Plochá vrstva nemá vůbec žádnou obálku VerifySignature. Kryptografické verdikty přicházejí z třídní úrovně, z TPDFlibSignatureVerifier, jenž vrací vsValid, vsInvalid, nebo vsUnknown, případně od externího validátoru, jemuž vaše politika shody už důvěřuje
Dlouhodobé ověřování: DSS, VRI a časové razítko dokumentu
PAdES-B-LT existuje proto, že infrastruktura pro odvolávání certifikátů je smrtelná. ETSI EN 319 142-1 §5.4.2.2 specifikuje úložiště Document Security Store: slovník na úrovni dokumentu nesoucí certifikáty, seznamy CRL a odpovědi OCSP, volitelně indexovaný po jednotlivých podpisech přes položky VRI klíčované otiskem /Contents každého podpisu. Postup v PDF Library for Delphi zrcadlí návrh pro časové razítko. NewPAdESDSSProcessFromFile proces otevře; AddPAdESDSSCertificate, AddPAdESDSSCRL a AddPAdESDSSOCSP přijímají bloby v DER; AddPAdESDSSVRI naváže vybraný materiál na jeden podpis; EndPAdESDSSProcessToFile zapíše všechno jako inkrementální aktualizaci. Ta těžká část zůstává na vaší straně. Obstarat materiál o odvolání a posoudit, zda je dost čerstvý, aby stál za vložení, je úkolem volajícího. Knihovna zaručuje, že slovníky jsou strukturálně konformní; nemůže zaručit, že váš OCSP responder mluvil pravdu
Archivní koncový bod, B-LTA, přidává časové razítko dokumentu: samostatné podpisové pole, jehož typ je DocTimeStamp, nikoli Sig, vyráběné přes SetSignProcessDocTimeStamp s rezervovanou délkou podpisu. Nenahrazuje časové razítko podpisu z kroku B-T. Časové razítko podpisu dokládá, kdy existoval jeden konkrétní podpis; časové razítko dokumentu chrání celý soubor včetně důkazů v DSS a je tím prvkem, který dlouhodobý archiv každých pár let obnovuje, jak algoritmy slábnou. Zralý archivní profil nese obojí. Kvůli čtečkám starším než tyto struktury zapíše TPDFlibSignDoc.EnsurePAdESExtensions do katalogu dokumentu vývojářské rozšíření ESIC, čímž oznámí, že soubor používá prvky definované ETSI
Jedné reakci na tohle všechno stojí za to předejít, protože vypadá jako chyba, a není jí. Prohlížeč u souboru se zcela správnou strukturou PAdES často hlásí „platnost neznámá“. Důvěra a struktura jsou nezávislé osy. Prohlížeč prostě nedokáže podepisujícího navázat řetězem na kořen, jemuž na daném stroji důvěřuje, což je u privátních CA a testovacích certifikátů běžné, i když audit ByteRange i ověření CMS projdou. Nápravou je řádně distribuovat kořenový certifikát, nebo, je-li skutečným cílem kvalifikovaný status podle eIDAS, vyhodnocovat vůči důvěryhodným seznamům EU, nikoli sahat do podepisovacího kódu
Pohled ze strany auditu, tedy vyjmenování podpisových polí napříč korpusem, výpis rozvržení ByteRange a hromadné čtení úrovní DocMDP, najdete v doprovodném textu o pracovním prostředí pro shodu a podepisování. Podepsané dokumenty, které musí splnit i archivní politiku, patří do postupu popsaného v článku Preflight PDF/A a PDF/UA v Delphi. Kompletní dokumentace API a evaluační verze ke stažení jsou na produktové stránce losLab PDF Library for Delphi