PDFium VCL berie CMS overovanie ako vymeniteľný backend za rozhraním IPdfCmsVerifier, takže PAdES validátor beží na Windowse cez CryptoAPI, na macOS cez Keychain a všade, kde je OpenSSL prítomné, cez ConfigureSslCmsVerifier. Rozhranie je malé. Tri OpenSSL správania pod ním produkujú sebaisté, nesprávne odpovede, ak ho implementujete naivne
Motivácia je jasná, akonáhle Delphi aplikácia opustí Windows. Validácia podpisov je jedna z mála oblastí, kde platformový crypto stack nie je implementačný detail: rozhoduje, ktoré certifikáty sú dôveryhodné, ktoré algoritmy existujú a čo revokácia znamená. Zapečeniť jednu do kódu znamená, že kód neprenesiete. Zabstrahovať ju zle znamená, že každá platforma hlási odpoveď iného tvaru, ktorú volajúci nedokáže porovnávať
Čo musí abstrakcia reálne niesť
Dva tvary overenia a tri nezávislé verdikty. PDF podpis je detached: podpísaný obsah sú dva byte rozsahy po oboch stranách diery /Contents, takže VerifyDetached berie dva segmenty namiesto jedného bufferu. Token časovej pečiatky je attached, nesie vlastný obsah, takže VerifyAttached berie len DER
Výsledok sa delí na tri statusy, pretože odpovedajú na tri odlišné otázky a môžu si odporovať. SignatureStatus hovorí, či bajty podpísal kľúč z podpisového certifikátu. TrustStatus hovorí, či sa ten certifikát cez reťaz dotiahne k niečomu, čomu veríte. RevocationStatus hovorí, či bol certifikát stále platný v relevantnom čase. Dokument s matematicky dokonalým podpisom od certifikátu, o ktorom ste nikdy nepočuli, je validný, nedôveryhodný a neznámy, a zliatie toho do jediného booleanu je spôsob, akým validátory končia ako klamári
uses
FPdfCrypto, FPdfCryptoSsl;
var
Options: TPdfCmsVerifyOptions;
begin
if not SslAvailable then
raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER, môže byť prázdne
ConfigureSslCrls(LoadFreshCrls); // DER, môže byť prázdne
ConfigureSslCmsVerifier; // inštaluje 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 vyzerá ako kuriozita a nie je. Každý OpenSSL error kód a každý store flag prechádza hranicou ako C unsigned long, čo je na Windowse štyri bajty a na Linuxe a macOS osem. Deklarovať ho ako fixný 32-bitový typ znamená, že kód funguje na Windowse a potom mlčky číta polovicu hodnoty na LP64. Hlásenie predpokladaných šírok ako string, na ktorom sa dá v teste assertnúť, mení celú triedu platformového ABI driftu na jednoriadkovú kontrolu. Ktokoľvek, kto prešiel tým istým problémom s CK_ULONG v PKCS#11 viazaní, ho okamžite spozná; ten príbeh je v článku PKCS#11 struct packing a šírka CK_ULONG
Prečo druhý priebeh overenia vidí prázdny obsah?
Pretože CMS_verify číta detached content BIO až po koniec súboru a prečítaný BIO sa vám nikto nepretáča späť. Overovať v dvoch priebehoch je rozumný návrh, najprv samotný kryptografický podpis s potlačeným hodnotením reťaze, potom plné hodnotenie, a zlyháva to nezvyčajne klamným spôsobom, ak oba priebehy zdieľajú jeden BIO
Druhý priebeh dostane nula bajtov obsahu. V detached režime to nie je chyba, pretože prázdny content buffer je legálny vstup. Digest jednoducho nesedí a zlyhanie sa prejaví ako zlyhanie stavby reťaze, nie ako zlyhanie obsahu, takže idete skúmať certifikáty a trust store, kým skutočný problém je pozícia v streame. Zostavte memory BIO cez BIO_new_mem_buf pre každý priebeh. Stojí jednu alokáciu a túto možnosť odstráni úplne
Čo flag no-verify potláča a čo nie
CMS_NO_SIGNER_CERT_VERIFY potláča hodnotenie reťaze, nie vyhľadanie podpisového certifikátu. OpenSSL interne resolveuje a pripája podpisové certifikáty skôr, než sa pozrie na flag, takže po prvom priebehu s tým flagom je podpisovateľ už dostupný a jeho identifikátory algoritmov sa dajú čítať hneď. Netreba spúšťať druhé plné overovanie len preto, aby ste získali podpisový certifikát, čo je presne to, do čoho vás láka názov flagu
S tým súvisí jedno pravidlo vlastníctva. Referencia na podpisovateľa patrí CMS štruktúre a nesmie sa uvoľňovať samostatne. Platí presne tak dlho ako štruktúra a jej uvoľnenie vyprodukuje corrupt, ktorého príznak sa objaví niekde úplne inde, obvykle počas upratovania nesúvisiaceho objektu
Prečo zapnutie kontroly CRL odmietne každý podpis?
Pretože OpenSSL kontroluje CRL len voči tomu, čo store už drží, a sám nič nesťahuje. Nenasleduje CRL distribution points a nerozpráva OCSP. Nastavte X509_V_FLAG_CRL_CHECK na store bez jediného CRL a každá reťaz zlyhá s neschopnosťou získať certifikátové CRL. Výsledok vyzerá ako revokačná kontrola, ktorá funguje a nachádza problémy. Ide o revokačnú kontrolu, ktorá vôbec nezbehla
Backend preto nastavuje flag len vtedy, keď ConfigureSslCrls reálne dodal aspoň jedno CRL. Bez neho sa RevocationStatus vráti ako pcvsUnsupported, čo je úprimné vyhlásenie, že otázka nebola zodpovedaná. Z rovnakého dôvodu nemá OnlineRetrieval na tento backend žiadny efekt a checkpoint pcvstOnlineRetrieval sa nevysiela: niet cesty sťahovania, z ktorej by sa hlásil progres
Toto je dizajnová pozícia, ktorá sa oplatí brániť všeobecne. Validátor, ktorý nedokáže skontrolovať revokáciu, by to mal povedať. Hlásiť neskontrolovaný certifikát ako nezrevokovaný je najbežnejší spôsob, akým podpisové validačné nástroje klamú svojich používateľov, a presne táto trieda zmatenia je rozobratá v článku prečo validátory odmietajú PAdES podpisy
// Checkpointy nechajú UI ukázať, ktorá fáza beží, a povedia vám, ktoré
// fázy backend reálne vykonáva
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;
// Čítajte tri verdikty samostatne; smú si odporovať
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');
Viazanie na knižnicu, ktorú nedokážete pripnúť
OpenSSL premenovalo svoje stack accessory medzi 1.0 a 1.1, takže tá istá logická funkcia má dva možné exportné názvy podľa buildu, ktorý host náhodou má. Viazanie resolveuje najprv novší názov a siahne po staršom, keď nový nevyjde, a chýbajúci symbol zaznamená až vtedy, keď nevyjde ani jeden. To je správny tvar pre akékoľvek dynamické viazanie na knižnicu, ktorú nedodávate: preferujte aktuálne názvy, tolerujte historické a hláste len skutočnú neprítomnosť
SslMissingSymbols je to, čo mení neúspešné načítanie na diagnostikovateľnú udalosť. Neprázdny výsledok na hostiteľovi, ktorý zjavne má libcrypto nainštalované, znamená, že nainštalovaná verzia je staršia než API, na ktoré tento build cieľuje, čo je úplne iný support rozhovor než chýbajúca knižnica. ConfigureSslLibraryPath kryje druhý bežný prípad, hostiteľa s viacerými OpenSSL buildmi, kde ten na default search path nie je ten, ktorý chcete
Výber backendu podľa platformy
Praktické usporiadanie je vybrať pri štarte a zaznamenať, ktorý odpovedal. Na Windowse sa platformový backend integruje s certifikátovými store, ktoré podnik už spravuje, čo je obvykle to, čo chcete. Na macOS sedí Keychain backend s tým istým uvažovaním a je popísaný v článku overovanie podpisov s SecTrust na macOS. OpenSSL je prenositeľná voľba a je to aj správna voľba, keď potrebujete validačnú politiku identickú naprieč platformami namiesto takej, ktorá nasleduje trust store každej platformy
Čokoľvek nainštalujete, logujte PadesCmsVerificationBackendName vedľa každého verdiktu, ktorý zaznamenáte. Uložený validačný výsledok bez backendu, ktorý ho vyprodukoval, sa nedá neskôr reprodukovať, pretože tri statusové hodnoty znamenajú jemne odlišné veci podľa toho, ktorý stack odpovedal. Vrstva inšpekcie podpisov nad tým všetkým, vrátane toho, ako sa hlásia PAdES úrovne, je rozobratá v článku inšpekcia PDF digitálnych podpisov a úrovní PAdES
Všetko to prichádza ako zdroják s PDFium Delphi komponentom, čo tu záleží viac než obvykle: pre podpisový validátor je možnosť prečítať presne, ktoré flagy backend nastavuje a ktoré kontroly preskakuje, nie nice-to-have, je to jediný spôsob, ako zistiť, čo reálne tvrdí zelená fajka vo vašej aplikácii