Teknisk artikel

Offline PDF-signatur-revokeringstjek på Windows i Delphi

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å

To uafhængige kontakter vogter offline PDF-signaturvalidering på Windows: CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL begrænser kun chain building som AIA-issuer- og rod-hentninger, mens revokering behøver CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY, for uden den downloader providers stadig CRL'er og sender OCSP-anmodninger, der afslører, hvilket certifikat der valideres
PDFium VCL OR'er revokerings-cache-only-flaget ind i revokeringsgennemløbet, når OnlineRetrieval er False, så en ptnpOffline tillidsvalidering forbliver væk fra netværket for begge gennemløb
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

Bevisgennemløbet, som ReadWinRevocationEvidence udfører på hvert Windows chain-element i Delphi: pRevocationInfo skal være til stede, cbSize skal være stor nok til at læse, dwRevocationResult skal være nul, og slut-elementet ekskluderes kun, når det er markeret self-signed eller CA-trusted, så en trust error-maske på nul ikke længere kan skjule et utjekket certifikat
En ren dom kræver mindst ét ikke-rodelement og et svar fra hvert krævet element, med RevocationError, der holder den rå provider DWORD som CRYPT_E_REVOKED
// 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

Signatur- og timestamp-revokering forbliver adskilt i PDFium VCL: dokument-signaturens detached CMS fylder RevocationReason og NativeRevocationError, RFC 3161-tokenets attached CMS fylder TimeStampRevocationReason og NativeTimeStampRevocationError, og utjekkede stadier efterlader pcrrNone og nul ved siden af deres statusfelter
Et revokeret TSA-certifikat kan derfor ikke udgive sig for at være en revokeret signerer, og en timestamp-fejl sletter aldrig et integritetsresultat, som signaturverifikationen allerede har etableret
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