PDFium VCL proverava opoziv PDF potpisa offline na Windows-u dodavanjem CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY revokacionom prolazu CertGetCertificateChain-a, jer zastavica samo-iz-keša korišćena za gradnju lanca uopšte ne pokriva CRL ili OCSP dohvate. Od v3.119.1 offline poziv ValidatePadesTrust ostaje van mreže, i čist rezultat traži stvarne dokaze revokacije po sertifikatu. Ostatak ovog teksta je o tome zašto je obe polovine te rečenice morale da se poprave
Postavka koja razotkriva problem je obična. Servis validacije radi na zaključanom Windows host-u, TPadesTrustValidationOptions.NetworkPolicy je ptnpOffline (što je ujedno i podrazumevano), i operater očekuje da svaki odgovor dođe iz lokalnog keša sertifikata. Pa neko primeti odlazne zahteve ka CA distribucionoj tački u logu firewall-a, ili batch posao koji zastane punih UrlRetrievalTimeoutMs od 15000 ms na svakom potpisu. Ništa u kodu nije tražilo mrežu. Windows je ionako otišao tamo
Zašto offline gradnja lanca i dalje dohvata CRL-ove na Windows-u?
Jer CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL ograničava samo URL dohvate koje gradnja lanca radi: AIA dohvate izdavaoca, update-ove korena i CTL. Microsoft dokumentacija za CertGetCertificateChain kaže izričito da se zastavica ne odnosi na proveru revokacije. Revokacija ima svoj prekidač, CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY ($80000000), i bez njega su revokacioni provajderi slobodni da skinu CRL ili pošalju OCSP zahtev iako okolni poziv deluje offline. PDFium VCL sada OR-uje tu zastavicu u revokacioni prolaz kad god je OnlineRetrieval False, preko zastavica lanca, CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT i CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT. To je bitno za više od latencije: OCSP zahtev govori responder-u koji sertifikat gledate, a baš to air-gapped validator treba da izbegava
uses
PDFium, FPdfCrypto, FPdfPades;
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
begin
Options := TPadesTrustValidationOptions.Default; // ptnpOffline, 15000 ms
Options.CheckRevocation := True; // False po podrazumevanju
Options.CheckTimeStamps := True;
// Offline sada znači offline i za revokaciju: keširani CRL i OCSP
// odgovori samo, ne podiže se pcvstOnlineRetrieval checkpoint
Report := Pdf.ValidatePadesTrust(Options);
end;
Dve gradnje lanca, dva polja za grešku
Windows backend gradi lanac dvaput, i svaka gradnja sada poseduje svoj slot za grešku. Prvi poziv CertGetCertificateChain radi bez revokacionih zastavica i hrani CertVerifyCertificateChainPolicy baznom politikom, što proizvodi TrustStatus i TrustError. Drugi poziv dodaje revokacione zastavice. Pre v3.119.1 neuspeh tog drugog poziva upisivao je GetLastError u TrustError, pa lanac tek verifikovan kao poverljiv mogao se vratiti delujući nepoverljivo jer je neki revokacioni provajder štrajknuo. Popravka čita GetLastError odmah i čuva ga u TPdfCmsVerifyResult.RevocationError, ostavljajući presudu prvog prolaza na miru. I True povratak sa drugog poziva se ne tretira kao uspeh; znači samo da je Windows vratio kontekst lanca vredan pregleda
Šta maska greške poverenja nula zaista dokazuje?
Sama za sebe, ništa. Zbirni TrustStatus.dwErrorStatus od nule posle revokacionog prolaza kaže da nijedan bit greške nije podignut, i lanac u kojem nijedan element uopšte nije nosio informaciju o revokaciji može proizvesti baš to. Raniji kod mapirao je „nema bita opozvanog, nema bita nepoznatog, nema bita offline“ pravo u validno, što je klasičan način da validator prijavi neproveren sertifikat kao čist. Nova rutina ReadWinRevocationEvidence šeta svaki simple lanac i svaki element, odbija strukture čiji je cbSize premalen da se bezbedno pročita, i prijavljuje uspeh tek kad postoji bar jedan element koji nije koren i svaki takav element nosi CERT_REVOCATION_INFO čiji je dwRevocationResult nula
// Sažeto iz šetnje dokazima: element se računa samo kad mu je
// revokacioni provajder zaista odgovorio
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); // lanac samo sa korenom ne dokazuje ništa
Rezultat provajdera čuva se sirov. RevocationError drži DWORD dwRevocationResult tačno kako ga je provajder vratio, više voleći grešku sa opozvanog elementa kad postoji (CRYPT_E_REVOKED je $80092010), i bitmaska poverenja nikada se ne oblači kao nativni kod greške. Mapiranje na TPdfCmsRevocationReason je namerno grubo: pcrrCertificateRevoked sa pcvsInvalid za eksplicitnu revokaciju, pcrrChainUntrusted kad lanac padne iz razloga nesrodnih revokaciji, i pcrrUnknown za sve ostalo. Windows je možda probao OCSP umesto CRL-a, pa se offline ili nepoznat rezultat ne prevodi u pcrrCrlExpired. OpenSSL CMS backend verifikacije može da pravi te CRL-specifične razlike jer evaluira samo CRL-ove koje mu Vi date, dok macOS SecTrust backend ostavlja polja na pcrrNone i nuli, što znači „nema detaljne dijagnostike“, ne „prošlo je“
Gde isključenje korena staje
CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT legitimno preskače sidro, jer niko ne objavljuje CRL koji opoziva koren naspram samog sebe. Zamka je odlučiti koji je element koren. PDFium VCL isključuje poslednji element simple lanca samo kad ga dwInfoStatus označi kao samopotpisan ($00000008) ili eksplicitno CA-poverljiv ($00004000). Offline host često ne može da dohvata nedostajućeg izdavaoca, pa se lanac završava na posredniku; tretirati poslednji element tog delimičnog lanca kao koren tiho bi odbacilo onaj sertifikat čiji status revokacije je najverovatnije odsutan iz keša. Taj element ostaje u tražanom skupu, nema odgovor provajdera, i rezultat ostaje pcvsIndeterminate
Kako se rezultati revokacije potpisa i vremenske oznake drže odvojenima?
Kao odvojena polja koja se nikada ne pregaze međusobno. PAdES validator verifikuje odvojeni CMS potpisa dokumenta i priloženi CMS RFC 3161 tokena vremenske oznake u dva nezavisna poziva, i v3.119.0 dala je svakom sopstvenu dijagnostiku na TPadesSignatureValidation: RevocationReason i NativeRevocationError za potpisoca, TimeStampRevocationReason i NativeTimeStampRevocationError za TSA. Opozvan TSA sertifikat zato ne može da se pravi kao opozvan potpisnik, i neuspeh vremenske oznake ne briše rezultat integriteta koji je već uspostavljen. Kad je CheckRevocation False, ili validacija nikada nije došla do te faze, polja ostaju pcrrNone i 0, pa ih uvek čitajte uz 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;
Izveštaj dokaza prati isto pravilo. CSV izvoz dodaje revocationReason, nativeRevocationError i kolone vremenske oznake do nativeTimeStampRevocationError na kraj postojećeg redosleda kolona, pa stariji parseri nastavljaju da rade, i JSON izvoz dodaje odgovarajuća polja ne menjajući značenje starih. Ako offline validacija i dalje vraća indeterminatno, trajna popravka je uzvodno: skupite validacioni materijal u trenutku potpisivanja, kako opisuje dugoročne PDF potpise sa RFC 3161 vremenskim oznakama i DSS-om, umesto da se nadate da mašina koja verifikuje ima topao keš
Šta test matrica dokazuje, a šta ne
Windows matrica verifikacije prošla je 30 kontrolisanih chain-API scenarija i jedan stvarni offline CMS smoke na svakom Delphi i FPC Win32 i Win64 cilju. Stvarni smoke verifikuje validan potpis pod nepoverljivim privatnim CA-om, dok čisti i eksplicitno opozvani ishodi dolaze iz stub-ovanih CertGetCertificateChain odgovora umesto instaliranih trust sidara ili živog dohvata. To je iskrena granica vredna izjave: rukovanje zastavicama, izolacija grešaka i šetnja dokazima su prikovani, ali ono što keš revokacije konkretne mašine sadrži datog dana je i dalje Windows-ova stvar, i prazan keš sada ispravno proizvodi „nepoznato“ umesto mrežnog zahteva ili lažnog „validno“
Rukovanje offline revokacijom, dijagnostika po polju i izvozi dokaza deo su API-ja validacije PDF potpisa u PDFium VCL-u za Delphi i C++Builder, uz OpenSSL i macOS backendove za višeplatformska raspoređivanja