PDFium VCL verifică offline revocarea semnăturilor PDF pe Windows adăugând CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY la trecerea de revocare a lui CertGetCertificateChain, pentru că flag-ul cache-only folosit la construirea lanțului nu acoperă deloc recuperarea CRL sau OCSP. Din v3.119.1, un apel ValidatePadesTrust offline rămâne în afara rețelei, iar un rezultat curat cere dovezi reale de revocare per certificat. Restul postării acestuia este despre de ce ambele jumătăți ale propoziției aceleia au avut nevoie de reparare
Montura care expune problema este obișnuită. Un serviciu de validare rulează pe o gazdă Windows închisă strâns, TPadesTrustValidationOptions.NetworkPolicy este ptnpOffline (care este și implicitul), iar operatorul se așteaptă ca fiecare răspuns să vină din cache-ul local de certificate. Apoi cineva observă cereri outbound către un punct de distribuție CA în jurnalul firewall-ului, sau un job de batch care se blochează toți UrlRetrievalTimeoutMs-ii de 15000 ms la fiecare semnătură. Nimic în cod nu a cerut rețeaua. Windows a plecat acolo oricum
De ce o construire de lanț offline încă aduce CRL-uri pe Windows?
Pentru că CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL restrânge doar recuperarea de URL pe care o face construirea lanțului: aducerile de emitent AIA, actualizările de rădăcină și CTL. Documentația Microsoft pentru CertGetCertificateChain spune explicit că flag-ul nu se aplică verificării de revocare. Revocarea are propriul ei comutator, CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY ($80000000), iar fără el furnizorii de revocare sunt liberi să descarce un CRL sau să trimită o cerere OCSP chiar dacă apelul din jur arată offline. PDFium VCL OR-ează acum flag-ul acela în trecerea de revocare ori de câte ori OnlineRetrieval este False, peste flag-urile de lanț, CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT și CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT. Asta contează pentru mai mult decât latența: o cerere OCSP îi spune responder-ului la ce certificat priviți, ceea ce este exact ceea ce un validator izolat aerian trebuie să evite
uses
PDFium, FPdfCrypto, FPdfPades;
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
begin
Options := TPadesTrustValidationOptions.Default; // ptnpOffline, 15000 ms
Options.CheckRevocation := True; // False implicit
Options.CheckTimeStamps := True;
// Offline înseamnă acum offline și pentru revocare: doar răspunsuri CRL și OCSP
// din cache, niciun checkpoint pcvstOnlineRetrieval nu este ridicat
Report := Pdf.ValidatePadesTrust(Options);
end;
Două construcții de lanț, două câmpuri de eroare
Backend-ul Windows construiește lanțul de două ori, iar fiecare construcție își deține acum propriul slot de eroare. Primul apel CertGetCertificateChain rulează fără flag-uri de revocare și îi dă lui CertVerifyCertificateChainPolicy politica de bază, ceea ce produce TrustStatus și TrustError. Al doilea apel adaugă flag-urile de revocare. Înainte de v3.119.1, un eșec al celui de-al doilea apel scria GetLastError în TrustError, astfel încât un lanț tocmai confirmat de încredere putea reveni cu aspect de neîncredere pentru că un furnizor de revocare a tușit. Reparația citește GetLastError imediat și îl stochează în TPdfCmsVerifyResult.RevocationError, lăsând în pace verdictul primei treceri. Și un True întors de cel de-al doilea apel nu este tratat nici el ca succes; înseamnă doar că Windows a predat înapoi un context de lanț care merită inspectat
Ce dovedește efectiv o mască de eroare de încredere zero?
De una singură, nimic. Un TrustStatus.dwErrorStatus agregat de zero după trecerea de revocare spune că niciun bit de eroare nu a fost ridicat, iar un lanț în care niciun element nu a purtat deloc informație de revocare poate produce exact asta. Codul de dinainte mapa „niciun bit de revocat, niciun bit de necunoscut, niciun bit offline" direct la valid, ceea ce este felul clasic în care un validator raportează un certificat neverificat ca unul curat. Noua rutină ReadWinRevocationEvidence plimbă prin fiecare lanț simplu și prin fiecare element, respinge structurile al căror cbSize este prea mic ca să fie citit în siguranță și raportează succes doar când există cel puțin un element non-rădăcină și fiecare asemenea element poartă un CERT_REVOCATION_INFO al cărui dwRevocationResult este zero
// Condensat din plimbarea de dovezi: un element contează doar când un
// furnizor de revocare i-a răspuns efectiv
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); // un lanț doar-rădăcină nu dovedește nimic
Rezultatul furnizorului este ținut brut. RevocationError ține DWORD-ul dwRevocationResult exact cum l-a întors furnizorul, preferând eroarea de la elementul revocat când există unul (CRYPT_E_REVOKED este $80092010), iar bitmask-ul de încredere nu este niciodată costumat în cod de eroare nativ. Maparea la TPdfCmsRevocationReason este deliberată grosieră: pcrrCertificateRevoked cu pcvsInvalid pentru revocare explicită, pcrrChainUntrusted când lanțul a eșuat din motive fără legătură cu revocarea, și pcrrUnknown pentru tot restul. Windows ar fi putut încerca OCSP în loc de un CRL, deci un rezultat offline sau necunoscut nu este tradus în pcrrCrlExpired. Backend-ul de verificare CMS OpenSSL poate face distincțiile acelea specifice CRL pentru că evaluează doar CRL-urile pe care i le predați, în timp ce backend-ul SecTrust din macOS lasă câmpurile pe pcrrNone și zero, ceea ce înseamnă „fără diagnostic detaliat", nu „trecut"
Unde se oprește excluderea rădăcinii
CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT sare în mod legitim peste ancoră, deoarece nimeni nu publică un CRL care să revoke o rădăcină contra ei înșiși. Capcana este deciderea care element este rădăcina. PDFium VCL exclude ultimul element al unui lanț simplu doar când dwInfoStatus-ul lui îl marchează autosemnat ($00000008) sau explicit CA-avizat ($00004000). O gazdă offline nu poate aduce adesea un emitent lipsă, deci lanțul se termină la un intermediar; tratarea elementului final al lanțului parțial acela ca rădăcină ar arunca în tăcere unicul certificat al cărui status de revocare are cele mai multe șanse să lipsească din cache. Elementul acela rămâne în mulțimea cerută, nu are răspuns de la furnizor, iar rezultatul rămâne pcvsIndeterminate
Cum sunt ținute separat rezultatele de revocare ale semnăturii și ale timestamp-ului?
Ca câmpuri separate care nu se suprascriu niciodată. Validatorul PAdES verifică CMS-ul detașat al semnăturii documentului și CMS-ul atașat al token-ului de timestamp RFC 3161 în două apeluri independente, iar v3.119.0 i-a dat fiecăruia propriile diagnostice pe TPadesSignatureValidation: RevocationReason și NativeRevocationError pentru semnatar, TimeStampRevocationReason și NativeTimeStampRevocationError pentru TSA. Un certificat TSA revocat nu se poate de aceea deghiza într-un semnatar revocat, iar un eșec de timestamp nu șterge un rezultat de integritate deja stabilit. Când CheckRevocation este False, sau validarea nu a ajuns niciodată la etapa aceea, câmpurile rămân pcrrNone și 0, deci citiți-le întotdeauna lângă RevocationStatus și 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;
Raportul de dovezi urmează aceeași regulă. Exportul CSV adaugă revocationReason, nativeRevocationError și coloanele de timestamp până la nativeTimeStampRevocationError la sfârșitul ordinii existente de coloane, astfel încât parserele mai vechi continuă să funcționeze, iar exportul JSON adaugă câmpuri potrivite fără să schimbe ce înseamnă cele vechi. Dacă validarea offline continuă să revină indeterminată, reparația durabilă este în amonte: adunați materialul de validare la momentul semnării, așa cum descrie semnăturile PDF pe termen lung cu timestamp-uri RFC 3161 și DSS, în loc să sperați că mașina care verifică are un cache cald
Ce dovedește și ce nu dovedește matricea de teste
Matricea de verificare Windows a trecut 30 de scenarii controlate de API de lanț și un singur smoke CMS offline real pe fiecare țintă Delphi și FPC Win32 și Win64. Smoke-ul real verifică o semnătură validă sub un CA privat neavizat, în timp ce rezultatele curate și explicit revocate vin din răspunsuri CertGetCertificateChain stub-uite în loc de ancore de încredere instalate sau recuperare în direct. Aceasta este o graniță onestă care merită enunțată: tratarea flag-urilor, izolarea erorilor și plimbarea de dovezi sunt fixate, dar ceea ce conține cache-ul de revocare al unei anumite mașini într-o anumită zi rămâne treaba Windows, iar un cache gol produce acum corect „necunoscut" în loc de o cerere de rețea sau un „valid" fals
Tratarea offline a revocării, diagnosticele per câmp și exporturile de dovezi fac parte din API-ul de validare a semnăturilor PDF din PDFium VCL pentru Delphi și C++Builder, alături de backend-urile OpenSSL și macOS pentru implementări cross-platform