Teknisk artikkel

Frakoblet PDF-signaturrevokering på Windows i Delphi

PDFium VCL sjekker PDF-signaturrevokering frakoblet på Windows ved å legge CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY til revokeringsspasset av CertGetCertificateChain, fordi cache-only-flagget som brukes til kjedebygging, ikke dekker CRL- eller OCSP-henting i det hele tatt. Siden v3.119.1 holder et frakoblet ValidatePadesTrust-kall seg unna nettverket, og et rent resultat krever faktisk revokeringsbevis per sertifikat. Resten av dette innlegget handler om hvorfor begge halvdeler av den setningen trengte å fikses

Oppsettet som avslører problemet, er helt vanlig. En valideringstjeneste kjører på en låst Windows-vert, TPadesTrustValidationOptions.NetworkPolicy er ptnpOffline (som også er standardverdien), og operatøren forventer at hvert svar kommer fra det lokale sertifikat-cachen. Så legger noen merke til utgående forespørsler til et CA-distribusjonspunkt i brannmursloggen, eller en batchjobb som står fast i hele UrlRetrievalTimeoutMs på 15000 ms for hver signatur. Ingenting i koden ba om nettverket. Windows dro dit likevel

Hvorfor henter et frakoblet kjedebygg fortsatt CRL-er på Windows?

Fordi CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL bare begrenser URL-hentingen som kjedebygging gjør: AIA-issuer-hentinger, rot- og CTL-oppdateringer. Microsofts dokumentasjon for CertGetCertificateChain sier eksplisitt at flagget ikke gjelder revokeringssjekking. Revokering har sin egen bryter, CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY ($80000000), og uten den står revokeringsleverandørene fritt til å laste ned en CRL eller sende en OCSP-forespørsel selv om det omkringliggende kallet ser frakoblet ut. PDFium VCL OR-er nå det flagget inn i revokeringsspasset når OnlineRetrieval er False, oppå kjedeflaggene, CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT og CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT. Det betyr noe for mer enn latens: en OCSP-forespørsel forteller responsenten hvilket sertifikat du ser på, noe som er nøyaktig det en luftskillet validator skal unngå

To uavhengige brytere vokter frakoblet PDF-signaturvalidering på Windows: CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL begrenser bare kjedebygging som AIA-issuer- og rothentinger, mens revokering trenger CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY, fordi uten den laster leverandører fortsatt ned CRL-er og sender OCSP-forespørsler som avslører hvilket sertifikat som valideres
PDFium VCL OR-er revokerings-cache-only-flagget inn i revokeringsspasset når OnlineRetrieval er False, så en ptnpOffline tillitsvalidering holder seg unna nettverket for begge pass
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;
  // Frakoblet betyr nå frakoblet for revokering også: cachete CRL- og OCSP
  // svarer alene, intet pcvstOnlineRetrieval-kontrollpunkt reises
  Report := Pdf.ValidatePadesTrust(Options);
end;

To kjedebygg, to feilfelt

Windows-backenden bygger kjeden to ganger, og hvert bygg eier nå sin egen feilplass. Det første CertGetCertificateChain-kallet kjører uten revokering flagg og mater CertVerifyCertificateChainPolicy med basispolicyen, noe som produserer TrustStatus og TrustError. Det andre kallet legger til revokeringsflaggene. Før v3.119.1 skrev en feil i det andre kallet GetLastError inn i TrustError, så en kjede som nettopp var verifisert som betrodd, kunne komme tilbake og se ut som utro fordi en revokeringsleverandør hikstet. Fiksen leser GetLastError umiddelbart og lagrer den i TPdfCmsVerifyResult.RevocationError, og lar første-pass-kjennelsen være i fred. Og en True-retur fra det andre kallet behandles heller ikke som suksess; det betyr bare at Windows ga tilbake en kjedekontekst verdt å se på

Hva beviser en null trust-feilmaske egentlig?

Alene: ingenting. En aggregert TrustStatus.dwErrorStatus på null etter revokeringsspasset sier at ingen feilbit ble reist, og en kjede der ingen element i det hele tatt bar revokeringsinformasjon, kan produsere nøyaktig det. Den tidligere koden mappet «ingen tilbakekalt-bit, ingen ukjent-bit, ingen frakoblet-bit» rett til gyldig, noe som er den klassiske måten en validator rapporterer et usjekket sertifikat som et rent. Den nye ReadWinRevocationEvidence-rutinen går gjennom hver enkle kjede og hvert element, avviser strukturer hvis cbSize er for liten til å lese trygt, og rapporterer suksess bare når minst ett ikke-rot-element finnes og hvert slikt element bærer en CERT_REVOCATION_INFO hvis dwRevocationResult er null

Bevisgangen ReadWinRevocationEvidence utfører på hvert Windows-kjedeelement i Delphi: pRevocationInfo må være til stede, cbSize må være stor nok til å lese, dwRevocationResult må være null, og slutt-elementet ekskluderes bare når det er merket selvsignert eller CA-betrodd, så en null trust-feilmaske kan ikke lenger skjule et usjekket sertifikat
En ren kjennelse krever minst ett ikke-rot-element og et svar fra hvert påkrevde element, med RevocationError som beholder rå leverandør-DWORD som CRYPT_E_REVOKED
// Komprimert fra bevisgangen: et element teller bare når en
// revokeringsleverandør faktisk svarte 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 kun-rot-kjede beviser ingenting

Leverandørresultatet beholdes rått. RevocationError holder dwRevocationResult-DWORD-en nøyaktig slik leverandøren returnerte den, med preferanse for feilen fra det tilbakekalte elementet når det finnes ett (CRYPT_E_REVOKED er $80092010), og tillitsbitmasken kles aldri ut som en native feilkode. Mappingen til TPdfCmsRevocationReason er bevisst grov: pcrrCertificateRevoked med pcvsInvalid for eksplisitt tilbakekalling, pcrrChainUntrusted når kjeden feiler av grunner urelatert til revokering, og pcrrUnknown for alt annet. Windows kan ha prøvd OCSP i stedet for en CRL, så et frakoblet eller ukjent resultat oversettes ikke til pcrrCrlExpired. OpenSSL CMS-verifiserings-backend kan gjøre de CRL-spesifikke skillene fordi den bare evaluerer CRL-ene du gir den, mens macOS SecTrust-backend lar feltene stå på pcrrNone og null, noe som betyr «ingen detaljert diagnose», ikke «bestått»

Hvor rot-eksklusionen stopper

CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT hopper over ankeret med rette, siden ingen publiserer en CRL som tilbakekaller en rot mot seg selv. Fellene er å avgjøre hvilket element som er roten. PDFium VCL ekskluderer det siste elementet i en enkel kjede bare når dwInfoStatus dets merker det som selvsignert ($00000008) eller eksplisitt CA-betrodd ($00004000). En frakoblet vert kan ofte ikke hente en manglende utsteder, så kjeden ender på et intermediært element; å behandle det delvise kjedens siste element som en rot ville stille miste det ene sertifikatet hvis revokeringsstatus mest sannsynlig mangler i cachen. Det elementet forblir i det påkrevde settet, har intet leverandørsvar, og resultatet forblir pcvsIndeterminate

Hvordan holdes signatur- og tidsstempel-revokering atskilt?

Som separate felt som aldri overskriver hverandre. PAdES-validatoren verifiserer den løsrevne CMS-en til dokumentsignaturen og den tilknyttede CMS-en til RFC 3161-tidsstemplet-tokenet i to uavhengige kall, og v3.119.0 ga hver sine egne diagnostikker på TPadesSignatureValidation: RevocationReason og NativeRevocationError for signataren, TimeStampRevocationReason og NativeTimeStampRevocationError for TSA-en. Et tilbakekalt TSA-sertifikat kan derfor ikke utgi seg for en tilbakekalt signatar, og en tidsstempel-feil utesletter ikke et integritetsresultat som allerede er etablert. Når CheckRevocation er False, eller valideringen aldri nådde det stadiet, forblir feltene pcrrNone og 0, så les dem alltid ved siden av RevocationStatus og TimeStampRevocationStatus

Signatur- og tidsstempel-revokering holdes atskilt i PDFium VCL: den løsrevne CMS-en til dokumentsignaturen fyller RevocationReason og NativeRevocationError, den tilknyttede CMS-en til RFC 3161-tokenet fyller TimeStampRevocationReason og NativeTimeStampRevocationError, og usjekkede stadier etterlater pcrrNone og null ved siden av statusfeltene sine
Et tilbakekalt TSA-sertifikat kan derfor ikke utgi seg for en tilbakekalt signatar, og en tidsstempel-feil utesletter aldri et integritetsresultat som signaturverifiseringen allerede har etablert
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 legger til revocationReason, nativeRevocationError og tidsstempel-kolonnene gjennom nativeTimeStampRevocationError på slutten av den eksisterende kolonnerekkefølgen, så eldre parsere fortsetter å virke, og JSON-eksporten legger til matchende felt uten å endre hva de gamle betyr. Kommer frakoblet validering stadig tilbake som indeterminert, er den varige fiksen oppstrøms: samle valideringsmaterialet ved signeringstid, som beskrevet i langsiktige PDF-signaturer med RFC 3161-tidsstempler og DSS, i stedet for å håpe på at verifiseringsmaskinen har en varm cache

Hva testmatrisen beviser og ikke beviser

Windows-verifiseringsmatrisen besto 30 kontrollerte kjede-API-scenarier og én ekte frakoblet CMS-smoke på hvert Delphi- og FPC-mål for Win32 og Win64. Den ekte smoken verifiserer en gyldig signatur under en utro privat CA, mens rene og eksplisitt tilbakekalte utfall kommer fra stubbete CertGetCertificateChain-svar i stedet for installerte tillitsankre eller live-henting. Det er en ærlig grense verdt å si: flagghåndteringen, feilisoleringen og bevisgangen er festet, men hva en gitt maskins revokeringscache inneholder en gitt dag, er fortsatt Windows' affære, og en tom cache produserer nå korrekt «ukjent» i stedet for en nettverksforespørsel eller en falsk «gyldig»

Frakoblet revokeringshåndtering, per-felt-diagnostikkene og beviseksportene er del av PDF-signaturvaliderings-API-et i PDFium VCL for Delphi og C++Builder, ved siden av OpenSSL- og macOS-backendene for plattformuavhengige utrullinger