Odborný článok

Overovanie PDF podpisov cez OpenSSL v PDFium VCL

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

Diagram OpenSSL CMS verifikátora PDFium VCL s tromi pastami: zdieľaný content BIO prečítaný po koniec súboru nechá druhý priebeh overenia s nula bajtmi, CMS_NO_SIGNER_CERT_VERIFY potláča hodnotenie reťaze, nie vyhľadanie podpisovateľa, a kontrola CRL na prázdnom store odmietne každú reťaz, aniž by revokácia kedy zbehla
Každá pasta dáva sebaistý nesprávny verdikt: pozícia v streame sa vydáva za zlyhanie trustu, flag no-verify potláča menej, než sľubuje jeho názov, a revokácia, ktorá nikdy nezbehla, vyzerá ako revokácia, ktorá našla problémy

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

Diagram abstrakcie IPdfCmsVerifier v PDFium VCL nesúcej VerifyDetached nad dvomi byte rozsahmi okolo diery Contents a VerifyAttached pre tokeny časových pečiatok, tri nezávislé verdikty SignatureStatus, TrustStatus a RevocationStatus a backendy vyberané pri štarte podľa platformy cez CryptoAPI, SecTrust alebo ConfigureSslCmsVerifier
Rozhranie nesie dva tvary overenia a tri verdikty, pretože odpovedajú na odlišné otázky a môžu si odporovať, a nainštalovaný backend sa zaznamenáva vedľa každého verdiktu, aby sa uložené výsledky dali reprodukovať

Č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