A PDFium VCL Windowson azért tudja offline ellenőrizni a PDF aláírás-visszavonást, mert hozzáadja a CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY-t a CertGetCertificateChain visszavonási menetéhez, hiszen a lánchépezésre használt cache-only jelző a CRL vagy OCSP letöltést egyáltalán nem fedi le. A v3.119.1 óta egy offline ValidatePadesTrust hívás a hálózaton kívül marad, és egy tiszta eredményhez tényleges, tanúsítványonkénti visszavonási bizonyíték kell. A bejegyzés hátralévő része arról szól, miért szorult javításra mindkét fél
A helyzet, amely leleplezi a problémát, hétköznapi. Egy validáló szolgáltatás egy lezárt Windows gazdagépen fut, a TPadesTrustValidationOptions.NetworkPolicy ptnpOffline (ez az alapértelmezés is), és az operátor azt várja, hogy minden válasz a helyi tanúsítvány-gyorsítótárból jöjjön. Aztán valaki kimenő kéréseket vesz észre egy CA disztribúciós pont felé a tűzfal naplójában, vagy egy kötegelt feladat, amely minden aláírásnál kiállja a teljes UrlRetrievalTimeoutMs-t, 15000 ms-ot. A kód semmiért nem kért hálózatot. A Windows magától odament
Miért tölt le mégis CRL-eket egy offline lánchépezés Windowson?
Mert a CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL csak a lánchépezés URL-letöltéseit korlátozza: AIA kiadó-lehívásokat, gyökér- és CTL frissítéseket. A Microsoft dokumentációja a CertGetCertificateChain-hez kifejezetten kimondja, hogy a jelző nem vonatkozik a visszavonás-ellenőrzésre. A visszavonásnak saját kapcsolója van, a CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY ($80000000), és enélkül a visszavonási szolgáltatók szabadon letölthetnek egy CRL-t vagy OCSP kérést küldhetnek akkor is, ha a környező hívás offlinenak tűnik. A PDFium VCL mostantól minden alkalommal VAGY-olja ezt a jelzőt a visszavonási menetbe, amikor az OnlineRetrieval False, a lánchepezési jelzők, a CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT és a CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT fölé. Ez többet jelent latenciánál: egy OCSP kérés megmondja a válaszadónak, melyik tanúsítványt nézi, és pontosan ezt kellene elkerüljön egy légrésbe zárt validátor
uses
PDFium, FPdfCrypto, FPdfPades;
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
begin
Options := TPadesTrustValidationOptions.Default; // ptnpOffline, 15000 ms
Options.CheckRevocation := True; // alapból False
Options.CheckTimeStamps := True;
// Az offline mostantól a visszavonásra is offline: csak gyorsítótárazott CRL
// és OCSP válaszok, pcvstOnlineRetrieval ellenőrzőpont nem vetődik fel
Report := Pdf.ValidatePadesTrust(Options);
end;
Két lánchépezés, két hibamező
A Windows backend kétszer építi a láncot, és mostantól minden építés a saját hibahelyét birtokolja. Az első CertGetCertificateChain hívás visszavonási jelzők nélkül fut, és a CertVerifyCertificateChainPolicy-t az alapszabályzattal eteti, ami TrustStatus-t és TrustError-t ad. A második hívás hozzáteszi a visszavonási jelzőket. A v3.119.1 előtt a második hívás kudarca a GetLastError-t írta a TrustError-be, tehát egy épp most megbízhatónak igazolt lánc megbízhatatlanként térhetett vissza, mert egy visszavonási szolgáltató hibázott. A javítás azonnal olvassa a GetLastError-t, és a TPdfCmsVerifyResult.RevocationError-ben tárolja, az első menet verdiktjét békén hagyva. És a második hívás True visszatérése sem számít sikernek; csak annyit jelent, hogy a Windows egy vizsgálatra érdemes lánckontextust adott vissza
Mit bizonyít valójában egy nulla trust hibamaszk?
Önmagában semmit. Egy nulla aggregátumú TrustStatus.dwErrorStatus a visszavonási menet után azt mondja, nem merült fel hibabit, és egy olyan lánc, amelyben egyetlen elem sem hordozott egyáltalán visszavonási információt, pontosan ezt produkálhatja. A korábbi kód a „nincs revoked bit, nincs unknown bit, nincs offline bit" helyzetet egyenesen érvényesre képezte le, ami a klasszikus módja annak, ahogy egy validátor ellenőrizetlen tanúsítványt tisztaként jelent. Az új ReadWinRevocationEvidence rutin végigsétál minden egyszerű láncon és minden elemen, elutasítja azokat a struktúrákat, amelyek cbSize-a túl kicsi a biztonságos olvasáshoz, és csak akkor jelent sikert, ha legalább egy nem gyökér elem létezik, és minden ilyen elem hordoz egy CERT_REVOCATION_INFO-t, amelynek dwRevocationResult-a nulla
// A bizonyítéksétából sűrítve: egy elem csak akkor számít,
// ha egy visszavonási szolgáltató ténylegesen válaszolt rá
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); // egy csak gyökérből álló lánc semmit sem bizonyít
A szolgáltatói eredmény nyersen marad. A RevocationError a dwRevocationResult DWORD-et tartja pontosan úgy, ahogy a szolgáltató visszaadta, a visszavont elem hibáját előnyben részesítve, ha van olyan (CRYPT_E_REVOKED a $80092010), és a trust bitmaszkot soha nem öltöztetik natív hibakóddá. A TPdfCmsRevocationReason-re való leképezés szándékosan durva: pcrrCertificateRevoked pcvsInvalid-mal explicit visszavonásra, pcrrChainUntrusted, amikor a lánc a visszavonástól független okból bukik meg, pcrrUnknown minden másra. A Windows OCSP-t is próbálhatott a CRL helyett, tehát egy offline vagy ismeretlen eredmény nem fordítódik pcrrCrlExpired-re. A OpenSSL CMS ellenőrző backend megteheti azokat a CRL-specifikus megkülönböztetéseket, mert csak olyan CRL-eket értékel, amelyeket Ön ad neki, míg a macOS SecTrust backend a mezőket pcrrNone-on és nullán hagyja, ami azt jelenti: nincs részletes diagnosztika, nem pedig átment
Hol ér véget a gyökérkizárás
A CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT joggal hagyja ki a horgonyt, hiszen senki nem publikál olyan CRL-t, amely egy gyökeret önmaga ellen von vissza. A csapda annak eldöntése, melyik elem a gyökér. A PDFium VCL egy egyszerű lánc utolsó elemét csak akkor zárja ki, ha a dwInfoStatus-a önaláírtként ($00000008) vagy explicit CA-bízottként ($00004000) jelöli. Egy offline gazdagép gyakran nem tudja letölteni a hiányzó kiadót, tehát a lánc egy köztesnél ér véget; ha ennek a résznek a záróelemét gyökérként kezelnénk, csendben elejtenénk azt az egy tanúsítványt, amelynek visszavonási állapota a legvalószínűbb, hogy hiányzik a gyorsítótárból. Ez az elem a kötelező halmazban marad, nincs szolgáltatói válasza, és az eredmény pcvsIndeterminate marad
Hogyan tartják elkülönítve az aláírás és időbélyeg visszavonási eredményeit?
Külön mezőkként, amelyek soha nem írják felül egymást. A PAdES validátor a dokumentumaláírás leválasztott CMS-ét és az RFC 3161 időbélyeg token csatolt CMS-ét két független hívásban ellenőrzi, és a v3.119.0 mindegyiknek saját diagnosztikát adott a TPadesSignatureValidation-en: RevocationReason és NativeRevocationError az aláírónak, TimeStampRevocationReason és NativeTimeStampRevocationError a TSA-nak. Egy visszavont TSA tanúsítvány ezért nem maszkírozhatja magát visszavont aláírónak, és egy időbélyeghiba nem töröl el olyan integritási eredményt, amely már kialakult. Amikor a CheckRevocation False, vagy a validálás soha nem jutott el odáig, a mezők pcrrNone és 0 maradnak, tehát mindig a RevocationStatus és a TimeStampRevocationStatus mellett olvassa őket
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;
A bizonyítékjelentés ugyanezt a szabályt követi. A CSV export a meglévő oszlopsorrend végére fűzi a revocationReason-t, a nativeRevocationError-t és az időbélyeg oszlopokat a nativeTimeStampRevocationError-ig, tehát a régebbi elemzők tovább dolgoznak, a JSON export pedig illő mezőket ad anélkül, hogy a régiek jelentése változna. Ha az offline validálás makacsul indetermináltként tér vissza, a tartós javítás upstream: gyűjtse össze az érvényesítő anyagot aláíráskor, ahogy azt a hosszú távú PDF aláírások RFC 3161 időbélyegekkel és DSS-sel cikk írja le, ahelyett, hogy reménykedne abban, hogy az ellenőrző gép meleg gyorsítótárral bír
Mit bizonyít és mit nem a tesztmátrix
A Windows ellenőrzőmátrix 30 kontrollált chain-API szcenáriót és egy valódi offline CMS smoke tesztet teljesített minden Delphi és FPC Win32 és Win64 célon. A valódi smoke egy érvényes aláírást ellenőriz egy megbízhatatlan privát CA alatt, miközben a tiszta és explicit módon visszavont kimenetek stubelt CertGetCertificateChain válaszokból jönnek, telepített bizalmi horgonyok vagy élő letöltés helyett. Ez becsületes határ, amelyet érdemes kimondani: a jelzőkezelés, a hibaizoláció és a bizonyítékséta rögzítésre került, de az, hogy egy adott gép visszavonási gyorsítótára egy adott napon mit tartalmaz, továbbra is a Windows dolga, és egy üres gyorsítótár mostantól helyesen „ismeretlen"-t ad hálózati kérés vagy hamis „érvényes" helyett
Az offline visszavonáskezelés, a mezőnkénti diagnosztika és a bizonyítékexportok a PDF aláírásvalidálási API részét képezik a PDFium VCL Delphihez és C++Builderhez csomagban, az OpenSSL és macOS backendekkel együtt a platformok közötti telepítésekhez