PDFium VCL bere CMS verifikaci jako vyměnitelný backend za interface IPdfCmsVerifier, takže PAdES validátor může běžet na Windows přes CryptoAPI, na macOS přes Keychain a kdekoli, kde je OpenSSL, přes ConfigureSslCmsVerifier. Interface je malý. Tři chování OpenSSL pod ním produkují sebejistě špatné odpovědi, pokud ho implementujete naivně
Motivace je jasná, jakmile Delphi aplikace opustí Windows. Validace podpisů je jedna z mála oblastí, kde platformní crypto stack není implementační detail: rozhoduje, které certifikáty jsou důvěryhodné, které algoritmy existují a co znamená revokace. Natvrdo zapsat jednu znamená, že kód neportuje. Abstrahovat ji špatně znamená, že každá platforma hlásí jinak tvarovanou odpověď, kterou si volající nemůže srovnat
Co musí abstrakce doopravdy nést
Dvě podoby verifikace a tři nezávislé verdikty. PDF podpis je detached: podepsaný obsah jsou dva bajtové rozsahy po stranách díry /Contents, takže VerifyDetached bere dva segmenty, ne jeden buffer. Timestamp token je attached, nese vlastní obsah, takže VerifyAttached bere jen DER
Výsledek se dělí na tři statusy, protože odpovídají na tři různé otázky a můžou si protiřečit. SignatureStatus říká, zda bajty podepsal klíč v certifikátu podepisujícího. TrustStatus říká, zda ten certifikát chainuje k něčemu, čemu věříte. RevocationStatus říká, zda byl certifikát stále platný v relevantní době. Dokument s matematicky dokonalým podpisem od certifikátu, o němž jste nikdy neslyšeli, je validní, nedůvěryhodný a neznámý a slepit tohle do jediného booleanu je způsob, jak validátory končí tím, že lžou uživatelům
uses
FPdfCrypto, FPdfCryptoSsl;
var
Options: TPdfCmsVerifyOptions;
begin
if not SslAvailable then
raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER, může být prázdné
ConfigureSslCrls(LoadFreshCrls); // DER, může být prázdné
ConfigureSslCmsVerifier; // instaluje backend
Writeln('backend : ', PadesCmsVerificationBackendName);
Writeln('library : ', SslLibraryPath, ' ', SslLibraryVersion);
Writeln('ABI : ', SslAbiLayout); // ulong=<n> long=<n>
Options := TPdfCmsVerifyOptions.Default;
Options.CheckRevocation := True;
Options.CollectChainCertificates := True;
end;
SslAbiLayout vypadá jako kuriozita a není. Každý error kód OpenSSL a každý store flag překračuje hranici jako C unsigned long, který je na Windows čtyři bajty a na Linuxu a macOS osm. Deklarovat ho jako fixní 32bit typ znamená, že kód funguje na Windows a pak na LP64 tiše čte polovinu hodnoty. Hlásit předpokládané šířky jako string, na který se v testu můžete assertnout, promění celou třídu platformního ABI driftu v jednořádkovou kontrolu. Kdo kdy procházel stejný problém s CK_ULONG v bindingu PKCS#11, okamžitě ho pozná; tenhle příběh je v článku PKCS#11 struct packing a šířka CK_ULONG
Proč druhý průchod verifikace vidí prázdný obsah?
Protože CMS_verify čte detached content BIO až do konce souboru a BIO, které bylo čteno, vám nikdo nezaskočí zpět. Verifikovat ve dvou průchodech je rozumný návrh, nejdřív samotný kryptografický podpis s potlačeným vyhodnocením chainu, pak plné vyhodnocení, a selže to neobvykle klamavým způsobem, sdílí-li oba průchody jedno BIO
Druhý průchod dostane nula bajtů obsahu. V detached módu to není chyba, protože prázdný content buffer je legální vstup. Digest se prostě neschoduje a selhání se projeví jako selhání stavby chainu, ne jako selhání obsahu, což vás pošle zkoumat certifikáty a trust store, zatímco skutečný problém je pozice ve streamu. Postavte memory BIO přes BIO_new_mem_buf pro každý průchod znovu. Stojí jednu alokaci a možnost odstraní úplně
Co flag no-verify potlačí a co ne
CMS_NO_SIGNER_CERT_VERIFY potlačuje vyhodnocení chainu, ne dohledání certifikátu podepisujícího. Interně resolveuje a připojuje OpenSSL certifikáty podepisujícího dřív, než flag konzultuje, takže po prvním průchodu nesoucím tenhle flag je podepisující už dostupný a jeho algorithm identifiery lze číst hned. Není potřeba pouštět druhou plnou verifikaci jen kvůli získání certifikátu podepisujícího, což je to, do čeho vás jméno flagu láká
Jedno ownership pravidlo s tím souvisí. Reference na podepisujícího patří CMS struktuře a nesmí se uvolňovat samostatně. Platí, dokud platí struktura, a její uvolnění vyprodukuje corrupt, jejíž symptom se objeví úplně jinde, obvykle při cleanupu nesouvisejícího objektu
Proč zapnutí kontroly CRL odmítne každý podpis?
Protože OpenSSL kontroluje CRL jen proti tomu, co store už drží, a nic vlastními silami nestahuje. Nenasleduje CRL distribution points a nemluví OCSP. Nastavte X509_V_FLAG_CRL_CHECK na store bez jediného CRL a každý chain selže s neschopností získat certifikátové CRL. Výsledek vypadá jako revokační kontrola, která funguje a nachází problémy. Je to revokační kontrola, která nikdy vůbec neběžela
Backend proto nastavuje flag jen tehdy, když ConfigureSslCrls doopravdy dodal aspoň jedno CRL. Bez něj se RevocationStatus vrátí jako pcvsUnsupported, což je upřímné tvrzení, že otázka nezodpovězena byla. Ze stejného důvodu nemá OnlineRetrieval na tenhle backend žádný efekt a neemituje se žádný checkpoint pcvstOnlineRetrieval: není tu žádná stahovací cesta, ze které by se hlásil progres
Tohle je designová pozice, která stojí za obhajobu obecně. Validátor, který nedokáže zkontrolovat revokaci, by to měl říct. Hlásit nezkontrolovaný certifikát jako nerevokovaný je ten nejčastější způsob, jakým nástroje na validaci podpisů klamou své uživatele, a je to přesně ta třída zmatení, kterou rozebírá článek proč validátory odmítají PAdES podpisy
// Checkpointy umožní UI ukázat, která stage běží, a řeknou vám,
// které stage backend doopravdy provádí
type
TSignatureProbe = class
procedure Checkpoint(Stage: TPdfCmsVerifyStage);
end;
procedure TSignatureProbe.Checkpoint(Stage: TPdfCmsVerifyStage);
begin
case Stage of
pcvstCryptographicSignature: Status('checking the signature');
pcvstChainBuild: Status('building the certificate chain');
pcvstOnlineRetrieval: Status('fetching validation data');
pcvstRevocationCheck: Status('checking revocation');
end;
end;
// Tři verdikty čtěte odděleně; smí si protiřečit
if Result.SignatureStatus = pcvsValid then
case Result.TrustStatus of
pcvsValid: Report('signed and trusted');
pcvsInvalid: Report('signed, chain rejected');
pcvsUnsupported,
pcvsIndeterminate: Report('signed, trust not established');
end;
if Result.RevocationStatus = pcvsUnsupported then
Report('revocation was not checked on this backend');
Vázání na knihovnu, kterou nemůžete zafixovat
OpenSSL přejmenovalo své stack accessory mezi 1.0 a 1.1, takže tatáž logická funkce má dvě možná exportní jména podle buildu, který host zrovna má. Binding resolveuje nejdřív novější jméno a spadne zpět na starší a chybějící symbol zaznamená, jen když nevyjde ani jedno. To je správná podoba pro jakékoli dynamické vázání na knihovnu, kterou nedodáváte: preferujte současná jména, tolerujte historická a hlásit jen skutečnou absenci
SslMissingSymbols je to, co ze selhaného loadu udělá diagnostikovatelnou událost. Neprázdný výsledek na hostu, který zjevně má libcrypto nainstalované, znamená, že nainstalovaná verze je starší než API, na které tenhle build míří, což je úplně jiný support rozhovor než chybějící knihovna. ConfigureSslLibraryPath pokrývá druhý běžný případ, host s několika OpenSSL buildy, kde ten na defaultní search path není ten, který chcete
Výběr backendu per platforma
Praktická úprava je vybrat při startu a zaznamenat, který odpověděl. Na Windows se platformní backend integruje s certificate store, které podnik už spravuje, což je normálně to, co chcete. Na macOS sedí Keychain backend do téže úvahy a popisuje ho článek ověřování podpisů se SecTrust na macOS. OpenSSL je přenosná volba a je to taky správná volba, když potřebujete validační politiku identickou napříč platformami, místo jedné, která následuje trust store každé platformy
Cokoli nainstalujete, logujte PadesCmsVerificationBackendName vedle každého verdiktu, který zaznamenáte. Uložený validační výsledek bez backendu, který ho vyprodukoval, nejde později reprodukovat, protože tři statusové hodnoty znamenají subtílně různé věci podle toho, který stack odpověděl. Vrstvu inspekce podpisů nad tímhle vším, včetně toho, jak se hlásí PAdES úrovně, pokrývá článek inspekce PDF digitálních podpisů a PAdES úrovní
Všechno tohle se dodává jako source s PDFium Delphi komponentou, což je tady důležitější než obvykle: pro validátor podpisů je možnost přečíst si přesně, které flagy backend nastavuje a které kontroly přeskakuje, ne nice-to-have, ale jediný způsob, jak vědět, co zelená fajfka ve vaší aplikaci doopravdy tvrdí