PDFium VCL tarkistaa PDF-allekirjoitusten mitätöinnin offline-tilassa Windowsissa lisäämällä CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLYin CertGetCertificateChainin mitätöintivaiheeseen, koska ketjun rakentamiseen käytetty vain välimuisti -lippu ei kata CRL- tai OCSP-hakuja lainkaan. Versiosta v3.119.1 lähtien offline-ValidatePadesTrust-kutsu pysyy verkoston ulkopuolella, ja puhdas tulos vaatii todellisen sertifikattikohtaisen mitätöintitodisteen. Loput tästä kirjoituksesta käsittelevät miksi molemmat lauseen puoliskot tarvitsivat korjausta
Asetus joka paljastaa ongelman on tavallinen. Validointipalvelu ajaa lukitussa Windows-isännässä, TPadesTrustValidationOptions.NetworkPolicy on ptnpOffline (mikä on myös oletus), ja operaattori odottaa jokaisen vastauksen tulevan paikallisesta sertifikaattivälimuistista. Sitten joku huomaa palomuurilokista lähtevät pyynnöt CA:n jakelupisteeseen, tai eräajon joka jumittuu koko UrlRetrievalTimeoutMsin 15000 ms:n ajoksi jokaisella allekirjoituksella. Mikään koodissa ei pyytänyt verkostoa. Windows meni sinne kuitenkin
Miksi offline-ketjun rakennus hakee yhä CRL:eitä Windowsissa?
Koska CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL rajoittaa vain URL-hakuja joita ketjun rakennus tekee: AIA-myöntäjähaut, juuri- ja CTL-päivitykset. Microsoftin dokumentaatio CertGetCertificateChainille sanoo eksplisiittisesti että lippu ei koske mitätöintitarkistusta. Mitätöinnillä on oma kytkimensä, CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY ($80000000), ja ilman sitä mitätöintipalvelut voivat vapaasti ladata CRL:n tai lähettää OCSP-pyynnön vaikka ympäröivä kutsu näyttää offline-tilalta. PDFium VCL OR:aa kyseisen lipun mitätöintivaiheeseen aina kun OnlineRetrieval on False, ketjulippujen, CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOTin ja CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUTin päälle. Sillä on enemmän väliä kuin viiveellä: OCSP-pyyntö kertoo vastaajalle mitä sertifikaattia katsot, mikä on juuri se mitä ilmatiivistetyn validoiijan kuuluu välttää
uses
PDFium, FPdfCrypto, FPdfPades;
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
begin
Options := TPadesTrustValidationOptions.Default; // ptnpOffline, 15000 ms
Options.CheckRevocation := True; // False oletuksena
Options.CheckTimeStamps := True;
// Offline tarkoittaa nyt offlinea myös mitätöinnille: vain välimuistissa olevat CRL- ja OCSP-
// vastaukset, pcvstOnlineRetrieval-tarkistuspistettä ei nosteta
Report := Pdf.ValidatePadesTrust(Options);
end;
Kaksi ketjurakennusta, kaksi virhekenttää
Windows-backend rakentaa ketjun kahdesti, ja kummallakin rakennuksella on nyt oma virhepaikkansa. Ensimmäinen CertGetCertificateChain-kutsu ajetaan ilman mitätöintilippuja ja syöttää CertVerifyCertificateChainPolicylle peruskäytännön, mikä tuottaa TrustStatusin ja TrustErrorin. Toinen kutsu lisää mitätöintiliput. Ennen v3.119.1 kyseisen toisen kutsun epäonnistuminen kirjoitti GetLastErrorin TrustErroriin, joten ketju joka oli juuri verifioitu luotettavaksi saattoi palata näyttäen luottamattomalta koska mitätöintipalvelu takelteli. Korjaus lukee GetLastErrorin heti ja tallentaa sen TPdfCmsVerifyResult.RevocationErroriin jättäen ensimmäisen kierroksen tuomion rauhaan. Ja toisen kutsun True-paluu ei myöskään tulkita onnistumiseksi; se tarkoittaa vain että Windows luovutti takaisin ketjukontekstin joka on tutkimisen arvoinen
Mitä nollan luottamusvirheen maski oikeasti todistaa?
Itsellään, ei mitään. Koostettu TrustStatus.dwErrorStatus nolla mitätöintivaiheen jälkeen sanoo ettei virhebittiä nostettu, ja ketju jossa yksikään elementti ei kantanut mitätöintitietoa lainkaan voi tuottaa täsmälleen sen. Aiempi koodi kuvasi ”ei kumottu-bittiä, ei tuntematon-bittiä, ei offline-bittiä” suoraan kelvolliseksi, mikä on klassinen tapa jolla validoija raportoi tarkistamattoman sertifikaatin puhtaana. Uusi ReadWinRevocationEvidence-rutiini kulkee jokaisen yksinkertaisen ketjun ja jokaisen elementin läpi, hylkää rakenteet joiden cbSize on liian pieni luettavaksi turvallisesti, ja raportoi onnistumisen vain kun vähintään yksi ei-juurielementti on olemassa ja jokainen tällainen elementti kantaa CERT_REVOCATION_INFOa jonka dwRevocationResult on nolla
// Tiivistetty todistekierroksesta: elementti lasketaan vain kun
// mitätöintipalvelu oikeasti vastasi sen puolesta
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); // pelkkä juuriketju ei todista mitään
Palvelun tulos säilytetään raakana. RevocationError pitää dwRevocationResult-DWORDin täsmälleen niin kuin palvelu sen palautti, suosien kumotun elementin virhettä jos sellainen on (CRYPT_E_REVOKED on $80092010), eikä luottamusbitimaskia koskaan puettaisi natiiviksi virhekoodiksi. Kuvaus arvoon TPdfCmsRevocationReason on tahallaan karkea: pcrrCertificateRevoked yhdessä pcvsInvalidin kanssa eksplisiittisestä mitätöinnistä, pcrrChainUntrusted kun ketju epäonnistui syistä jotka eivät liity mitätöintiin, ja pcrrUnknown kaikelle muulle. Windows on saattanut kokeilla OCSP:tä CRL:n sijaan, joten offline- tai tuntematon-tulosta ei käännetä arvoksi pcrrCrlExpired. OpenSSL CMS -verifiointibackend voi tehdä kyseiset CRL-kohtaiset erottelut koska se evaluoi vain CRL:eitä jotka sille annetaan, kun taas macOS SecTrust -backend jättää kentät arvoihin pcrrNone ja nolla, mikä tarkoittaa ”ei yksityiskohtaista diagnostiikkaa”, ei ”läpäisi”
Missä juuren poissulkeminen pysähtyy
CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT ohittaa ankkurin laillisesti, koska kukaan ei julkaise CRL:tä joka kumoaisi juuren sitä itseään vastaan. Ansa on päättää mikä elementti on juuri. PDFium VCL sulkee yksinkertaisen ketjun viimeisen elementin pois vain kun sen dwInfoStatus merkitsee sen itse allekirjoitetuksi ($00000008) tai eksplisiittisesti CA-luotetuksi ($00004000). Offline-isäntä ei usein voi hakea puuttuvaa myöntäjää, joten ketju päättyy väliedustajaan; kyseisen osittaisen ketjun viimeisen elementin käsitteleminen juurena pudottaisi hiljaa sen yhden sertifikaatin jonka mitätöintitila on todennäköisimmin poissa välimuistista. Kyseinen elementti jää vaadittuun joukkoon, sillä ei ole palvelun vastausta, ja tulos pysyy arvossa pcvsIndeterminate
Miten allekirjoituksen ja aikaleiman mitätöintitulokset pidetään erillään?
Erillisinä kenttinä jotka eivät koskaan ylikirjoita toisiaan. PAdES-validoija verifioi asiakirja-allekirjoituksen erillisen CMS:n ja RFC 3161 -aikaleimatokenin liitetyn CMS:n kahdessa riippumattomassa kutsussa, ja v3.119.0 antoi kummallekin omat diagnostiikkansa TPadesSignatureValidationissa: RevocationReason ja NativeRevocationError allekirjoittajalle, TimeStampRevocationReason ja NativeTimeStampRevocationError TSA:lle. Kumottu TSA-sertifikaatti ei siksi voi esiintyä kumottuna allekirjoittajana, eikä aikaleiman vika pyyhi pois eheystulosta joka on jo vakiintunut. Kun CheckRevocation on False, tai validointi ei koskaan ylettynyt kyseiseen vaiheeseen, kentät pysyvät arvoissa pcrrNone ja 0, joten lue ne aina RevocationStatusin ja TimeStampRevocationStatusin vierestä
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;
Todisteraportti noudattaa samaa sääntöä. CSV-export liittää revocationReasonin, nativeRevocationErrorin ja aikaleimasarakkeet aina nativeTimeStampRevocationErroriin asti olemassa olevan sarakejärjestyksen loppuun, joten vanhemmat jäsennykset jatkavat toimintaansa, ja JSON-export lisää vastaavat kentät muuttamatta mitä vanhemmat tarkoittavat. Jos offline-validointi palaa yhä epämääräisenä, kestävä korjaus on ylävirrassa: kokoa validointimateriaali allekirjoitushetkellä, kuten artikkelissa pitkäaikaiset PDF-allekirjoitukset RFC 3161 -aikaleimoin ja DSS:llä kuvataan, sen sijaan että toivoisit verifioivan koneen omavan lämmintä välimuistia
Mitä testimatriisi todistaa ja mitä ei
Windows-verifiointimatriisi läpäisi 30 kontrolloitua ketju-API-skenaariota ja yhden aidon offline-CMS-smokein jokaisella Delphi- ja FPC-Win32- ja Win64-kohteella. Aito smoke verifioi kelvollisen allekirjoituksen luottamattoman yksityisen CA:n alla, kun taas puhtaat ja eksplisiittisesti kumotut lopputulokset tulevat tukituista CertGetCertificateChain-vastauksista asennettujen luottamusankkurien tai elävien hakujen sijaan. Se on rehellinen raja jonka kannattaa sanoa ääneen: lipunkäsittely, virheiden eristäminen ja todistekierros on naulattu kiinni, mutta se mitä tietyn koneen mitätöintivälimuisti sisältää tiettynä päivänä on yhä Windowsin asia, ja tyhjä välimuisti tuottaa nyt oikein arvon ”tuntematon” verkkopyynnön tai vale ”kelvollinen” -tuloksen sijaan
Offline-mitätöinnin käsittely, kenttäkohtainen diagnostiikka ja todisteexportit kuuluvat PDF-allekirjoitusten validointi-API:hin PDFium VCL for Delphi and C++Builder -komponentissa OpenSSL- ja macOS-backendien rinnalla monialustaisia käyttöönottoja varten