Műszaki cikk

Offline aláírás-visszavonás-ellenőrzés Delphiben Windowson

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

Két független kapcsoló őrzi az offline PDF aláírásvalidálást Windowson: a CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL csak a lánchepezést korlátozza, például az AIA kiadó- és gyökérlehívásokat, a visszavonásnak pedig CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY kell, mert enélkül a szolgáltatók továbbra is letöltenek CRL-eket és OCSP kéréseket küldenek, amelyek felfedik, melyik tanúsítvány érvényesül
A PDFium VCL VAGY-olja a visszavonási cache-only jelzőt a visszavonási menetbe, valahányszor az OnlineRetrieval False, tehát egy ptnpOffline bizalmi validálás mindkét menetben a hálózaton kívül marad
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éta, amelyet a ReadWinRevocationEvidence minden Windows lánclemen elvégez Delphiben: a pRevocationInfo-nak jelen kell lennie, a cbSize-nak elég nagynak kell lennie az olvasáshoz, a dwRevocationResult-nak nullának, a záróelem pedig csak akkor zárható ki, ha önaláírtként vagy CA-bízottként van megjelölve, tehát egy nulla trust hibamaszk már nem rejthet el ellenőrizetlen tanúsítványt
A tiszta verdikthez legalább egy nem gyökér elem és válasz kell minden kötelező elemtől, a RevocationError pedig nyers szolgáltatói DWORD-et tart, például CRYPT_E_REVOKED-ot
// 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

Az aláírás és időbélyeg visszavonása elkülönül a PDFium VCL-ben: a dokumentumaláírás leválasztott CMS-e tölti a RevocationReason-t és a NativeRevocationError-t, az RFC 3161 token csatolt CMS-e a TimeStampRevocationReason-t és a NativeTimeStampRevocationError-t, az ellenőrizetlen szakaszok pedig pcrrNone-t és nullát hagynak a státuszmezőik mellett
Egy visszavont TSA tanúsítvány ezért nem maszkírozhatja magát visszavont aláírónak, és egy időbélyeghiba soha nem töröl el olyan integritási eredményt, amelyet az aláírásellenőrzés már kialakított
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