PDFium VCL kontroluje revokáciu PDF podpisov offline na Windows pridaním CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY k revokačnému prieskumu CertGetCertificateChain, pretože cache-only flag používaný na stavbu reťaze nepokrýva CRL ani OCSP retrieval vôbec. Od v3.119.1 zostáva offline volanie ValidatePadesTrust mimo siete a čistý výsledok vyžaduje reálne revokačné dôkazy na certifikát. Zvyšok tohto príspevku je o tom, prečo obe polovice tej vety potrebovali opravu
Setup, ktorý odhalí problém, je obyčajný. Validačná služba beží na uzamknutom Windows hostiteľovi, TPadesTrustValidationOptions.NetworkPolicy je ptnpOffline (čo je aj default) a operátor čaká, že každá odpoveď príde z lokálneho certifikátového cache. Potom si niekto všimne odchádzajúce požiadavky na CA distribution point v firewall logu, alebo batch job, ktorý sa zasekne na celých UrlRetrievalTimeoutMs 15000 ms pri každom podpise. Nič v kóde o sieť nepožiadalo. Windows tam šiel aj tak
Prečo offline stavba reťaze stále čerpá CRL na Windows?
Pretože CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL obmedzuje len URL retrieval, ktorý robí stavba reťaze: fetch AIA issuer, aktualizácie root a CTL. Dokumentácia Microsoftu pre CertGetCertificateChain hovorí explicitne, že flag sa nevzťahuje na revokačnú kontrolu. Revokácia má vlastný prepínač, CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY ($80000000) a bez neho sú revokační provideri slobodní stiahnuť CRL alebo poslať OCSP požiadavku, hoci okolité volanie vyzerá offline. PDFium VCL teraz OR-uje ten flag do revokačného prieskumu vždy, keď OnlineRetrieval je False, navrchu k chain flagom, CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT a CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT. To má význam pre viac než latenciu: OCSP požiadavka povie responderovi, na ktorý certifikát sa pozeráte, čoho sa má air-gapped validátor presne vyvarovať
uses
PDFium, FPdfCrypto, FPdfPades;
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
begin
Options := TPadesTrustValidationOptions.Default; // ptnpOffline, 15000 ms
Options.CheckRevocation := True; // False defaultne
Options.CheckTimeStamps := True;
// Offline teraz znamená offline aj pre revokáciu: len cache CRL a OCSP
// odpovede, žiadny checkpoint pcvstOnlineRetrieval sa nezvýši
Report := Pdf.ValidatePadesTrust(Options);
end;
Dve stavby reťaze, dve chybové polia
Windows backend stavia reťaz dvakrát a každá stavba teraz vlastní vlastný chybový slot. Prvé volanie CertGetCertificateChain beží bez revokačných flagov a kŕmi CertVerifyCertificateChainPolicy base politikou, čo produkuje TrustStatus a TrustError. Druhé volanie pridáva revokačné flagy. Pred v3.119.1 zapísalo zlyhanie toho druhého volania GetLastError do TrustError, takže reťaz, ktorá bola práve overená ako dôveryhodná, sa mohla vrátiť pôsobiaca nedôveryhodne, pretože revokačný provider zaváhal. Oprava číta GetLastError okamžite a ukladá ho do TPdfCmsVerifyResult.RevocationError, ponechávajúc verdikt prvého prieskumu pokoji. A True návrat z druhého volania sa tiež neberie ako úspech; znamená len, že Windows odovzdal späť chain context stojaci za inšpekciu
Čo vlastne dokazuje nulová maska trust chyby?
Samé o sebe, nič. Agregát TrustStatus.dwErrorStatus nula po revokačnom prieskume hovorí, že sa nezvýšil žiadny chybový bit, a reťaz, v ktorej žiadny element neonesol vôbec revokačnú informáciu, môže produkovať presne to. Skorší kód mapoval „žiadny revoked bit, žiadny unknown bit, žiadny offline bit“ priamo na valid, čo je klasický spôsob, akým validátor hlási nepreverený certifikát ako čistý. Nová rutina ReadWinRevocationEvidence prechádza každú simple chain a každý element, odmieta štruktúry, ktorých cbSize je príliš malé na bezpečné čítanie, a hlási úspech len vtedy, keď existuje aspoň jeden non-root element a každý taký element nesie CERT_REVOCATION_INFO, ktorého dwRevocationResult je nula
// Skondenzované z prechodu dôkazov: element sa počíta len vtedy, keď
// ho revokačný provider reálne odpovedal
for J := 0 to ElementCount - 1 do
begin
Element := Elements[J];
ExcludedRoot := (J = ElementCount - 1) and
((Element^.TrustStatus.dwInfoStatus and
(CERT_TRUST_IS_SELF_SIGNED or CERT_TRUST_IS_CA_TRUSTED)) <> 0);
InfoPresent := (Element^.pRevocationInfo <> nil) and
(Element^.pRevocationInfo^.cbSize >= SizeOf(TCERT_REVOCATION_INFO));
if not ExcludedRoot then
begin
Inc(RequiredCount);
if not InfoPresent or
(Element^.pRevocationInfo^.dwRevocationResult <> 0) then
Complete := False;
end;
end;
Complete := Complete and (RequiredCount > 0); // reťaz len s rootom nič nedokazuje
Výsledok providera zostáva surový. RevocationError drží DWORD dwRevocationResult presne tak, ako ho provider vrátil, s prednosťou chyby z revoked elementu, keď existuje (CRYPT_E_REVOKED je $80092010), a trust bitmaska sa nikdy neoblečie za natívny chybový kód. Mapovanie do TPdfCmsRevocationReason je zámerne hrubé: pcrrCertificateRevoked s pcvsInvalid pre explicitnú revokáciu, pcrrChainUntrusted, keď reťaz zlyhala z dôvodov nesúvisiacich s revokáciou, a pcrrUnknown pre všetko ostatné. Windows mohol skúsiť OCSP namiesto CRL, takže offline alebo neznámy výsledok sa neprekladá do pcrrCrlExpired. OpenSSL CMS verification backend dokáže robiť tie CRL-špecifické rozdiely, pretože vyhodnocuje len CRL, ktoré mu podáte, kým macOS SecTrust backend necháva polia na pcrrNone a nule, čo znamená „žiadna detailná diagnostika“, nie „prešlo“
Kde končí vylúčenie rootu
CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT oprávnene preskočí kotvu, keďže nikto nepublikuje CRL, ktorá by revokovala root proti nemu samému. Pasca je rozhodnúť, ktorý element je root. PDFium VCL vylúči posledný element simple chain len vtedy, keď ho jeho dwInfoStatus označí ako self-signed ($00000008) alebo explicitne CA-trusted ($00004000). Offline hostiteľ často nedokáže fetchnúť chýbajúceho issuer, takže reťaz končí na intermediáte; brať posledný element tej čiastočnej reťaze ako root by poticho zahodilo ten jediný certifikát, ktorého revokačný status je najpravdepodobnejšie neprítomný v cache. Ten element ostáva v povinnej sade, nemá odpoveď providera a výsledok ostáva pcvsIndeterminate
Ako sa výsledky revokácie podpisu a timestampu držia oddelene?
Ako oddelené polia, ktoré sa nikdy neprepisujú navzájom. PAdES validátor verifikuje detached CMS podpisu dokumentu a attached CMS timestamp tokenu RFC 3161 v dvoch nezávislých volaniach a v3.119.0 dalo každému vlastnú diagnostiku na TPadesSignatureValidation: RevocationReason a NativeRevocationError pre podpisujúceho, TimeStampRevocationReason a NativeTimeStampRevocationError pre TSA. Revokovaný TSA certifikát preto nedokáže vydávať sa za revokovaného podpisujúceho a zlyhanie timestampu nemaze integrity výsledok, ktorý už bol nadviazaný. Keď CheckRevocation je False, alebo validácia nikdy nedosiahla tú etapu, polia ostanú pcrrNone a 0, takže ich čítajte vždy vedľa RevocationStatus a TimeStampRevocationStatus
for I := 0 to High(Report.Signatures) do
begin
S := Report.Signatures[I];
case S.RevocationStatus of
pcsInvalid:
Log(Format('sig %d: signer revoked, provider 0x%.8x',
[I, S.NativeRevocationError]));
pcsIndeterminate:
Log(Format('sig %d: revocation unknown, reason %d, provider 0x%.8x',
[I, Ord(S.RevocationReason), S.NativeRevocationError]));
pcsNotChecked:
Log(Format('sig %d: revocation not checked', [I]));
end;
if S.TimeStampRevocationStatus = pcsIndeterminate then
Log(Format('sig %d: TSA revocation unknown, provider 0x%.8x',
[I, S.NativeTimeStampRevocationError]));
end;
Report dôkazov nasleduje to isté pravidlo. CSV export pripojí revocationReason, nativeRevocationError a timestampové stĺpce cez nativeTimeStampRevocationError na koniec existujúceho poradia stĺpcov, takže staršie parsery fungujú ďalej, a JSON export pridá zodpovedajúce polia bez zmeny významu tých starých. Ak offline validácia stále vracia indeterminate, trvalá oprava je po prúde: nazbierajte validačný materiál pri podpisovaní, ako popisuje dlhodobé PDF podpisy s RFC 3161 timestampami a DSS, namiesto dúfania, že verifikujúci stroj má teplý cache
Čo testovacia matica dokazuje a čo nie
Windows verifikačná matica prešla 30 riadenými chain-API scenármi a jedným reálnym offline CMS smokeom na každom cieľi Delphi aj FPC, Win32 aj Win64. Reálny smoke verifikuje platný podpis pod nedôveryhodnou privátnou CA, kým čisté a explicitne revoked výstupy pochádzajú zo stubnutých odpovedí CertGetCertificateChain, nie z inštalovaných trust anchor alebo živého retrieval. To je úprimná hranica, ktorá stojí za vyslovenie: obsluha flagov, izolácia chýb a prechod dôkazov sú pripnuté, ale to, čo revokačný cache konkrétneho stroja obsahuje v daný deň, je stále vec Windows a prázdny cache teraz správne produkuje „unknown“ namiesto sieťovej požiadavky alebo falošného „valid“
Offline revokačné spracovanie, per-field diagnostika a exporty dôkazov sú súčasťou API validácie PDF podpisov v PDFium VCL for Delphi and C++Builder, vedľa OpenSSL a macOS backendov pre cross-platform nasadenia