PDFium VCL tjekker PDF-signaturrevokering offline på Windows ved at tilføje CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY til revokeringsgennemløbet af CertGetCertificateChain, fordi cache-only-flaget, der bruges til chain building, slet ikke dækker CRL- eller OCSP-hentning. Siden v3.119.1 forbliver et offline ValidatePadesTrust-kald væk fra netværket, og et rent resultat kræver faktisk revokeringsbevis pr. certifikat. Resten af dette indlæg handler om, hvorfor begge halvdele af den sætning trængte til at blive rettet
Opsætningen, der eksponerer problemet, er almindelig. En valideringsservice kører på en låst Windows-vært, TPadesTrustValidationOptions.NetworkPolicy er ptnpOffline (som også er standarden), og operatøren forventer, at alle svar kommer fra det lokale certifikat-cache. Så lægger nogen mærke til udgående anmodninger til et CA-distributionspunkt i firewall-loggen, eller et batch-job, der går i stå i hele UrlRetrievalTimeoutMs på 15000 ms ved hver signatur. Intet i koden bad om netværket. Windows gik der alligevel
Hvorfor henter et offline chain build stadig CRL'er på Windows?
Fordi CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL kun begrænser den URL-hentning, som chain building laver: AIA-issuer-hentninger, rod- og CTL-opdateringer. Microsofts dokumentation for CertGetCertificateChain siger eksplicit, at flaget ikke gælder revokeringstjek. Revokering har sin egen kontakt, CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY ($80000000), og uden den står revokeringsproviderne frit til at downloade en CRL eller sende en OCSP-anmodning, selvom det omgivende kald ser offline ud. PDFium VCL OR'er nu det flag ind i revokeringsgennemløbet, når OnlineRetrieval er False, oven på chain-flagene, CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT og CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT. Det tæller for mere end latenstid: en OCSP-anmodning fortæller responderen, hvilket certifikat du kigger på, hvilket er præcis det, en air-gapped validator skal undgå
uses
PDFium, FPdfCrypto, FPdfPades;
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
begin
Options := TPadesTrustValidationOptions.Default; // ptnpOffline, 15000 ms
Options.CheckRevocation := True; // False som standard
Options.CheckTimeStamps := True;
// Offline betyder nu også offline for revokering: cached CRL og OCSP
// kun svar, intet pcvstOnlineRetrieval-checkpoint rejses
Report := Pdf.ValidatePadesTrust(Options);
end;
To chain builds, to fejlfelter
Windows-backenden bygger kæden to gange, og hvert build ejer nu sit eget fejlfelt. Det første CertGetCertificateChain-kald kører uden revokeringsflag og føder CertVerifyCertificateChainPolicy med base-politikken, hvilket producerer TrustStatus og TrustError. Det andet kald tilføjer revokeringsflagene. Før v3.119.1 skrev en fejl i det andet kald GetLastError ind i TrustError, så en kæde, der netop var verificeret som betroet, kunne komme tilbage og se utro ud, fordi en revokeringsprovider snublede. Fixet læser GetLastError straks og gemmer den i TPdfCmsVerifyResult.RevocationError og lader første-gennemløbs-dommen være. Og en True-retur fra det andet kald behandles heller ikke som succes; det betyder blot, at Windows afleverede en chain context, der er værd at inspicere
Hvad beviser en trust error-maske på nul reelt?
I sig selv: intet. En aggregeret TrustStatus.dwErrorStatus på nul efter revokeringsgennemløbet siger, at ingen fejlbit blev rejst, og en kæde, hvor intet element overhovedet bar revokeringsinformation, kan producere netop det. Den tidligere kode mappede "ingen revoked-bit, ingen unknown-bit, ingen offline-bit" direkte til gyldig, hvilket er den klassiske måde, en validator rapporterer et utjekket certifikat som et rent. Den nye ReadWinRevocationEvidence-rutine gennemløber hver simple chain og hvert element, afviser strukturer, hvis cbSize er for lille til at læse sikkert, og rapporterer succes kun, når mindst ét ikke-rodelement eksisterer, og hvert sådant element bærer en CERT_REVOCATION_INFO, hvis dwRevocationResult er nul
// Komprimeret fra bevisgennemløbet: et element tæller kun, når en
// revokeringsprovider reelt svarede for det
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); // en kæde med kun roden beviser intet
Provider-resultatet holdes råt. RevocationError holder dwRevocationResult-DWORD'en præcis, som provideren returnerede den, foretrækker fejlen fra det revokede element, når der er ét (CRYPT_E_REVOKED er $80092010), og trust-bitmasken klædes aldrig ud som en native fejlskode. Mappingen til TPdfCmsRevocationReason er bevidst grov: pcrrCertificateRevoked med pcvsInvalid for eksplicit revokering, pcrrChainUntrusted, når kæden fejlede af grunde uden relation til revokering, og pcrrUnknown for alt andet. Windows kan have prøvet OCSP frem for en CRL, så et offline eller ukendt resultat oversættes ikke til pcrrCrlExpired. OpenSSL CMS-verifikationsbackenden kan lave de CRL-specifikke skelnelser, for den evaluerer kun CRL'er, man giver den, mens macOS SecTrust-backenden lader felterne stå på pcrrNone og nul, hvilket betyder "ingen detaljeret diagnostik", ikke "bestået"
Hvor rod-eksklusionen stopper
CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT springer med rette ankeret over, da ingen udgiver en CRL, der revokerer en rod mod den selv. Fælden er at afgøre, hvilket element der er roden. PDFium VCL ekskluderer det sidste element af en simple chain kun, når dets dwInfoStatus markerer det som self-signed ($00000008) eller eksplicit CA-trusted ($00004000). En offline vært kan ofte ikke hente en manglende issuer, så kæden ender ved en intermediate; at behandle den delvise kædes sidste element som en rod ville stille droppe det certifikat, hvis revokeringsstatus mest sandsynligt mangler i cachen. Det element forbliver i det krævede sæt, har intet provider-svar, og resultatet forbliver pcvsIndeterminate
Hvordan holdes signatur- og timestamp-revokeringsresultater adskilt?
Som separate felter, der aldrig overskriver hinanden. PAdES-validatoren verificerer dokument-signaturens detached CMS og RFC 3161 timestamp-tokenets attached CMS i to uafhængige kald, og v3.119.0 gav hver sine egne diagnostikker på TPadesSignatureValidation: RevocationReason og NativeRevocationError for signereren, TimeStampRevocationReason og NativeTimeStampRevocationError for TSA'en. Et revokeret TSA-certifikat kan derfor ikke udgive sig for at være en revokeret signerer, og en timestamp-fejl sletter ikke et integritetsresultat, der allerede er etableret. Er CheckRevocation False, eller nåede valideringen aldrig det stadie, forbliver felterne pcrrNone og 0, så læs dem altid ved siden af RevocationStatus og 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;
Bevisrapporten følger samme regel. CSV-eksporten føjer revocationReason, nativeRevocationError og timestamp-kolonnerne gennem nativeTimeStampRevocationError til sidst i den eksisterende kolonnerækkefølge, så ældre parsere fortsætter med at virke, og JSON-eksporten tilføjer matchende felter uden at ændre, hvad de gamle betyder. Kommer offline-validering vedvarende tilbage som indeterminate, er det holdbare fix upstream: saml valideringsmaterialet ved signeringstid, som beskrevet i langtids-PDF-signaturer med RFC 3161 timestamps og DSS, frem for at håbe på, at den verificerende maskine har en varm cache
Hvad testmatricen beviser og ikke beviser
Windows-verifikationsmatricen bestod 30 kontrollerede chain-API-scenarier og én reel offline CMS-smoke på hvert Delphi- og FPC Win32- og Win64-mål. Den reelle smoke verificerer en gyldig signatur under en utro privat CA, mens rene og eksplicit revokerede udfald kommer fra stubbede CertGetCertificateChain-svar frem for installerede trust anchors eller live-hentning. Det er en ærlig grænse, der er værd at nævne: flag-håndteringen, fejlisolationen og bevisgennemløbet er fastlåst, men hvad en bestemt maskines revokeringscache indeholder en given dag, er stadig Windows' sag, og en tom cache producerer nu korrekt "unknown" i stedet for en netværksanmodning eller et falsk "valid"
Den offline revokeringshåndtering, de pr.-felt-diagnostikker og beviseksporterne er en del af PDF-signaturvaliderings-API'en i PDFium VCL til Delphi og C++Builder, ved siden af OpenSSL- og macOS-backenderne til cross-platform-udrulninger