PDFium VCL kontroluje revokaci PDF podpisů offline na Windows přidáním CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY do revokačního průchodu CertGetCertificateChain, protože flag cache-only používaný pro stavbu řetězu CRL ani OCSP retrieval nepokrývá vůbec. Od v3.119.1 zůstává offline volání ValidatePadesTrust mimo síť a čistý výsledek vyžaduje skutečné revokační důkazy per certifikát. Zbytek tohohle článku je o tom, proč si obě poloviny té věty potřebovaly opravit
Sestavení, které problém vystaví na světlo, je obyčejné. Validační služba běží na zamknutém Windows hostu, TPadesTrustValidationOptions.NetworkPolicy je ptnpOffline (což je i default) a operátor očekává, že každá odpověď přijde z lokálního certifikátového cache. Pak si někdo všimne odchozích požadavků na CA distribution point v logu firewallu, nebo batch jobu, který se zasekne na celých UrlRetrievalTimeoutMs 15000 ms na každém podpisu. Kód o síť nežádal. Windows tam stejně odešly
Proč offline stavba řetězu pořád stahuje CRL na Windows?
Protože CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL omezuje jen URL retrieval, který dělá stavba řetězu: fetchování AIA issuerů, update root a CTL. Microsoft dokumentace k CertGetCertificateChain říká explicitně, že se flag na revokační kontroly nevztahuje. Revokace má vlastní přepínač, CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY ($80000000), a bez něj si mohou revokační providery dovolit stáhnout CRL nebo poslat OCSP požadavek, i když okolní volání vypadá offline. PDFium VCL teď ORuje tenhle flag do revokačního průchodu, kdykoli je OnlineRetrieval False, navrch flagů řetězu, CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT a CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT. Na tom záleží víc než kvůli latenci: OCSP požadavek prozradí responderovi, který certifikát sledujete — přesně to, čemu se má air-gapped validátor vyhýbat
uses
PDFium, FPdfCrypto, FPdfPades;
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
begin
Options := TPadesTrustValidationOptions.Default; // ptnpOffline, 15000 ms
Options.CheckRevocation := True; // defaultně False
Options.CheckTimeStamps := True;
// Offline teď znamená offline i pro revokaci: jen cachované CRL a OCSP
// odpovědi, checkpoint pcvstOnlineRetrieval se nezvedá
Report := Pdf.ValidatePadesTrust(Options);
end;
Dvě stavby řetězu, dvě pole chyb
Windows backend staví řetěz dvakrát a každá stavba teď vlastní svůj chybový slot. První volání CertGetCertificateChain běží bez revokačních flagů a krmí CertVerifyCertificateChainPolicy base politikou, což produkuje TrustStatus a TrustError. Druhé volání přidává revokační flagy. Před v3.119.1 zapsalo selhání toho druhého volání GetLastError do TrustError, takže řetěz právě ověřený jako důvěryhodný mohl vypadat jako nedůvěryhodný, protože revokační provider zakopl. Oprava čte GetLastError okamžitě a ukládá ho do TPdfCmsVerifyResult.RevocationError, takže verdikt z prvního průchodu zůstává nedotčený. A True návrat z druhého volání se ani nepovažuje za úspěch; znamená jen, že Windows vrátila chain context stojící za prohlédnutí
Co doopravdy dokazuje nulová maska trust chyb?
O svém, nic. Agregátní TrustStatus.dwErrorStatus nula po revokačním průchodu říká, že se nezvedl žádný chybový bit, a řetěz, ve kterém žádný element nenese revokační informace vůbec, může vyprodukovat přesně to. Dřívější kód mapoval „žádný revoked bit, žádný unknown bit, žádný offline bit“ rovnou na validní, což je klasický způsob, jak validátor nahlásí nezkontrolovaný certifikát jako čistý. Nová rutina ReadWinRevocationEvidence projde každý simple chain a každý element, odmítne struktury, jejichž cbSize je moc malé na bezpečné čtení, a hlásí úspěch jen tehdy, když existuje aspoň jeden non-root element a každý takový element nese CERT_REVOCATION_INFO, jehož dwRevocationResult je nula
// Zhuštěno z evidence walku: element počítá, jen když za něj
// revocation provider doopravdy odpověděl
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); // chain jen s kořenem nic nedokazuje
Výsledek provideru zůstává syrový. RevocationError drží DWORD dwRevocationResult přesně tak, jak ho provider vrátil, s předností chyby z revokovaného elementu, pokud existuje (CRYPT_E_REVOKED je $80092010), a trust bitmaska se nikdy nepřevléká za nativní chybový kód. Mapování na TPdfCmsRevocationReason je záměrně hrubé: pcrrCertificateRevoked s pcvsInvalid pro explicitní revokaci, pcrrChainUntrusted, když řetěz selhal z důvodů nesouvisejících s revokací, a pcrrUnknown pro všechno ostatní. Windows mohl zkusit OCSP místo CRL, takže offline nebo unknown výsledek se nepřekládá na pcrrCrlExpired. Backend verifikace CMS přes OpenSSL umí tyhle CRL-specifické rozdíly, protože vyhodnocuje jen CRL, které mu podáte, zatímco backend macOS SecTrust nechává pole na pcrrNone a nule, což znamená „žádnou detailní diagnostiku“, ne „prošlo“
Kde vyloučení kořene končí
CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT legitimně vynechává kotvu, protože nikdo nepublikuje CRL, která by revokovala root proti němu samotnému. Past je v rozhodnutí, který element je ten root. PDFium VCL vylučuje poslední element simple chain jen tehdy, když ho dwInfoStatus označí jako self-signed ($00000008) nebo explicitně CA-trusted ($00004000). Offline host často nemůže stáhnout chybějícího issuer, takže řetěz končí na intermediate; brát poslední element takového částečného řetězu jako root by potichu upustilo jediný certifikát, jehož revokační status je v cache nejpravděpodobněji chybí. Tenhle element zůstává v požadované sadě, nemá odpověď providera a výsledek zůstává pcvsIndeterminate
Jak se výsledky revokace podpisu a timestampu drží odděleně?
Jako oddělená pole, která se nikdy nepřepisují navzájem. Validátor PAdES verifikuje detached CMS podpisu dokumentu a attached CMS timestamp tokenu RFC 3161 ve dvou nezávislých voláních a v3.119.0 dala každému vlastní diagnostiku na TPadesSignatureValidation: RevocationReason a NativeRevocationError pro podepisujícího, TimeStampRevocationReason a NativeTimeStampRevocationError pro TSA. Revokovaný TSA certifikát se proto nemůže převléct za revokovaného podepisujícího a selhání timestampu nemaže výsledek integrity, který už byl ustavený. Když je CheckRevocation False, nebo validace do toho stupně nedošla, pole zůstávají na pcrrNone a 0, takže je vždy čtěte vedle 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;
Evidence report se drží téhož pravidla. CSV export přilepí revocationReason, nativeRevocationError a timestamp sloupce až po nativeTimeStampRevocationError na konec stávajícího pořadí sloupců, takže starší parsery fungují dál, a JSON export přidává odpovídající pole bez změny významu starých. Pokud offline validace pořád vychází jako indeterminate, trvalá oprava je po proudu: sesbírejte validační materiál už při podepisování, jak popisuje článek o dlouhodobých PDF podpisech s RFC 3161 timestampy a DSS, místo doufat, že verifikující stroj má teplý cache
Co testovací matice dokazuje a co ne
Windows verifikační matice prošla 30 řízenými scénáři chain API a jedním reálným offline CMS smoke na každém targetu Delphi i FPC, Win32 i Win64. Reálný smoke verifikuje platný podpis pod nedůvěryhodnou soukromou CA, zatímco čisté a explicitně revokované výstupy přicházejí ze stubovaných odpovědí CertGetCertificateChain místo instalovaných trust anchorů nebo živého stahování. Je to poctivá hranice, kterou stojí za to říct: obsluha flagů, izolace chyb a evidence walk jsou přichycené, ale to, co revokační cache konkrétního stroje obsahuje v daný den, je stále věc Windows, a prázdný cache teď správně produkuje „unknown“ místo síťového požadavku nebo falešného „validního“
Offline obsluha revokace, diagnostika per pole a evidence exporty jsou součástí PDF signature validačního API v PDFium VCL pro Delphi a C++Builder vedle backendů OpenSSL a macOS pro cross-platform nasazení