Techninis straipsnis

PDF parašų atšaukimų tikrinimas be tinklo Windows Delphi

PDFium VCL Windows tikrina PDF parašų atšaukimus atsijungęs, pridėdamas CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY prie CertGetCertificateChain atšaukimų žingsnio, nes grandinės statybai naudojama cache-only žymė apskritai nedengia CRL ar OCSP parsisiuntimo. Nuo v3.119.1 atsijungęs ValidatePadesTrust kvietimas lieka už tinklo ribų, o švarus rezultatas reikalauja tikro kiekvieno sertifikato atšaukimų įrodymo. Likusi šio straipsnio dalis — apie tai, kodėl abi šio sakinio pusės reikalavo pataisymo

Nustatymas, atskleidžiantis problemą, paprastas. Patikros servisas dirba užrakintame Windows kompiuteryje, TPadesTrustValidationOptions.NetworkPolicy yra ptnpOffline (tai ir numatytoji reikšmė), o operatorius tikisi, kad kiekvienas atsakymas atkeliaus iš vietinio sertifikatų podėlio. Tada kas nors pastebi ugniasienės žurnale išorinius užklausimus į CA platinimo tašką arba paketinę užduotį, stingstantį pilnam UrlRetrievalTimeoutMs — 15000 ms — prie kiekvieno parašo. Niekas kode neprašė tinklo. Windows ten vis tiek nuėjo

Kodėl grandinės statymas atsijungus vis tiek parsiunčia CRL Windows?

Nes CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL riboja tik URL parsisiuntimą, kurį atlieka grandinės statymas: AIA leidėjo paėmimus, šaknies ir CTL atnaujinimus. Microsoft dokumentacija CertGetCertificateChain aiškiai sako, kad žymė negalioja atšaukimų tikrinimui. Atšaukimai turi savąjį jungiklį, CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY ($80000000), ir be jo atšaukimų tiekėjai laisvai gali parsisiųsti CRL ar išsiųsti OCSP užklausą, nors supantis kvietimas atrodo atsijungęs. PDFium VCL dabar tą žymę OR operacija įmaišo į atšaukimų žingsnį kaskart, kai OnlineRetrieval False, ant grandinės žymių, CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT ir CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT. Tai svarbiau nei vėlavimas: OCSP užklausa atsakikliui pasako, kurį sertifikatą žiūrite, o būtent to ir turi vengti oro tarpu izoliuotas validatorius

Du nepriklausomi jungikliai saugo atsijungusio PDF parašų patikrą Windows: CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL riboja tik grandinės statymą, tokį kaip AIA leidėjo ir šaknies paėmimai, o atšaukimams reikia CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY, nes be jo tiekėjai vis tiek parsisiunčia CRL ir siunčia OCSP užklausas, atskleidžiančias, kuris sertifikatas tikrinamas
PDFium VCL atšaukimų cache-only žymę OR operacija įmaišo į atšaukimų žingsnį kaskart, kai OnlineRetrieval False, tad ptnpOffline patikėtumo patikra abiem žingsniais lieka už tinklo
uses
  PDFium, FPdfCrypto, FPdfPades;

var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
begin
  Options := TPadesTrustValidationOptions.Default; // ptnpOffline, 15000 ms
  Options.CheckRevocation := True;                 // False pagal nutylėjimą
  Options.CheckTimeStamps := True;
  // Atsijungus dabar reiškia atsijungus ir atšaukimams: tik podėlyje
  // sukaupti CRL ir OCSP atsakymai, pcvstOnlineRetrieval taškas nekeliamas
  Report := Pdf.ValidatePadesTrust(Options);
end;

Du grandinės statymai, du klaidų laukai

Windows backendas grandinę stato du kartus, ir kiekvienas statymas dabar turi savą klaidos vietą. Pirmasis CertGetCertificateChain kvietimas vyksta be atšaukimų žymių ir maitina CertVerifyCertificateChainPolicy bazine politika, kuri duoda TrustStatus ir TrustError. Antrasis kvietimas prideda atšaukimų žymes. Iki v3.119.1 antrojo kvietimo nesėkmė GetLastError rašydavo į TrustError, tad grandinė, ką tik patvirtinta kaip patikima, galėjo grįžti atrodanti nepatikima, nes atšaukimų tiekėjas užstrigdavo. Pataisa GetLastError perskaito nedelsiant ir saugo TPdfCmsVerifyResult.RevocationError, palikdama pirmojo žingsnio verdiktą ramybėje. Ir True grąžinimas iš antrojo kvietimo taip pat nėra laikomas sėkme; jis reiškia tik tai, kad Windows atidavė vertą apžiūros grandinės kontekstą

Ką iš tikrųjų įrodo nulinė patikėtumo klaidos kaukė?

Pats savaime — nieko. Agreguotas TrustStatus.dwErrorStatus lygus nuliui po atšaukimų žingsnio sako, kad jokio klaidos bito neiškilo, ir grandinė, kurioje nė vienas elementas apskritai neneša atšaukimų informacijos, gali duoti būtent tai. Ankstesnis kodas „nėra atšaukto bito, nė nežinomo bito, nė atsijungimo bito“ tiesiai atvaizduodavo į teisėtą, o tai klasikinis būdas, kuriuo valdytojas nepatikrintą sertifikatą praneša kaip švarų. Naujoji ReadWinRevocationEvidence procedūra apeina kiekvieną paprastąją grandinę ir kiekvieną elementą, atmeta struktūras, kurių cbSize per mažas saugiam skaitymui, ir sėkmę praneša tik tada, kai egzistuoja bent vienas ne šakninis elementas ir kiekvienas toks elementas neša CERT_REVOCATION_INFO, kurio dwRevocationResult nulinis

Įrodymų apėjimas, kurį ReadWinRevocationEvidence atlieka kiekvienam Windows grandinės elementui Delphi: pRevocationInfo privalo būti, cbSize privalo būti pakankamai didelis skaitymui, dwRevocationResult privalo būti nulinis, o galutinis elementas atmetamas tik tada, kai pažymėtas savarankiškai pasirašytu ar CA patikėtu, tad nulinė patikėtumo klaidos kaukė daugiau negali paslėpti nepatikrinto sertifikato
Švarus verdiktas reikalauja bent vieno ne šakninio elemento ir atsakymo iš kiekvieno privalomo elemento, o RevocationError laiko žaliąjį tiekėjo DWORD, tokį kaip CRYPT_E_REVOKED
// Sutrumpinta iš įrodymų apėjimo: elementas skaitomas tik tada, kai
// atšaukimų tiekėjas iš tikrųjų jam atsakė
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); // tik šaknimi besibaigianti grandinė nieko neįrodo

Tiekėjo rezultatas laikomas žalias. RevocationError laiko dwRevocationResult DWORD lygiai tokį, kokį grąžino tiekėjas, teikdamas pirmenybę atšaukto elemento klaidai, kai toks yra (CRYPT_E_REVOKED yra $80092010), o patikėtumo bitmaška niekada neapvilka natyvaus klaidos kodo. Atvaizdavimas į TPdfCmsRevocationReason sąmoningai stambus: pcrrCertificateRevoked su pcvsInvalid aiškiam atšaukimui, pcrrChainUntrusted, kai grandinė žlugo dėl atšaukimams nesusijusių priežasčių, ir pcrrUnknown visam kitam. Windows galėjo bandyti OCSP vietoj CRL, tad atsijungęs ar nežinomas rezultatas neverčiamas į pcrrCrlExpired. OpenSSL CMS patikros backend tuos CRL specifinius skirtumus gali daryti, nes jis vertina tik jam duotas CRL, o macOS SecTrust backend laukus palieka pcrrNone ir nulinius, kas reiškia „nėra detalios diagnostikos“, o ne „praėjo“

Kur baigiasi šaknies išimimas

CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT pagrįstai praleidžia inkarą, nes niekas neskelbia CRL, atšaukiančio šaknį pačios atžvilgiu. Spąstai — nuspręsti, kuris elementas yra šaknis. PDFium VCL paprastosios grandinės paskutinį elementą atmeta tik tada, kai jo dwInfoStatus jį pažymi savarankiškai pasirašytu ($00000008) arba aiškiai CA patikėtu ($00004000). Atsijungęs kompiuteris dažnai nesugeba parsisiųsti trūkstamo leidėjo, tad grandinė baigiasi tarpiniu; laikant tos dalinės grandinės galutinį elementą šaknimi, tyliai būtų prarastas tas vienintelis sertifikatas, kurio atšaukimų būsena labiausiai tikėtina, kad podėlyje nėra. Tas elementas lieka privalomojoje aibėje, atsakymo iš tiekėjo neturi, o rezultatas lieka pcvsIndeterminate

Kaip parašo ir laiko žymos atšaukimų rezultatai laikomi atskirai?

Kaip atskiri laukai, kurie niekada neperrašo vieni kitų. PAdES valdytojas patvirtina dokumento parašo atskirtąjį CMS ir RFC 3161 laiko žymos tokeno prijungtąjį CMS dviem nepriklausomais kvietimais, o v3.119.0 kiekvienam dav savas diagnostikas TPadesSignatureValidation: RevocationReason ir NativeRevocationError pasirašiusiajam, TimeStampRevocationReason ir NativeTimeStampRevocationError TSA. Atšauktas TSA sertifikatas todėl negali apsimesti atšauktu pasirašiusiuoju, o laiko žymos nesėkmė neištrina jau nustatytos vientisumo rezultato. Kai CheckRevocation False arba patikra to etapo niekada nepasiekė, laukai lieka pcrrNone ir 0, tad skaitykite juos visada šalia RevocationStatus ir TimeStampRevocationStatus

Parašo ir laiko žymos atšaukimai lieka atskirai PDFium VCL: dokumento parašo atskirtasis CMS užpildo RevocationReason ir NativeRevocationError, RFC 3161 tokeno prijungtasis CMS užpildo TimeStampRevocationReason ir NativeTimeStampRevocationError, o nepatikrinti etapai palieka pcrrNone ir nulį šalia savo būsenų laukų
Atšauktas TSA sertifikatas todėl negali apsimesti atšauktu pasirašiusiuoju, o laiko žymos nesėkmė niekada neištrina vientisumo rezultato, kurį parašo patikra jau yra nustatiusi
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;

Įrodymų ataskaita laikosi tos pačios taisyklės. CSV eksportas revocationReason, nativeRevocationError ir laiko žymų stulpelius iki nativeTimeStampRevocationError prijungia esamos stulpelių tvarkos gale, tad senesni parseriai dirba toliau, o JSON eksportas prideda atitinkamus laukus nekeisdamas to, ką senieji reiškia. Jei atsijungusio patikra vis grįžta neapibrėžta, patvari pataisa yra aukščiau: surinkite patikros medžiagą pasirašymo metu, kaip aprašyta ilgalaikiuose PDF parašuose su RFC 3161 laiko žymomis ir DSS, o ne vilkitės, kad tikrinanti mašina turi šiltą podėlį

Ką testų matrica įrodo ir ko neįrodo

Windows patikros matrica praeino 30 valdomų grandinės API scenarijų ir vieną tikrą atsijungusio CMS smoke kiekviename Delphi ir FPC Win32 bei Win64 tiksluose. Tikrasis smoke patvirtina teisėtą parašą po nepatikima privačia CA, o švarūs ir aiškiai atšaukti rezultatai ateina iš imituotų CertGetCertificateChain atsakymų, o ne iš įdiegtų patikėtumo inkarų ar gyvų parsisiuntimų. Tai sąžininga riba, verta pasakyti: žymių apdorojimas, klaidų izoliacija ir įrodymų apėjimas yra užfiksuoti, bet tai, ką konkrečios mašinos atšaukimų podėlis turi tam tikrą dieną, vis dar Windows reikalas, o tuščias podėlis dabar teisingai duoda „nežinoma“ vietoj tinklo užklausos ar netikro „teisėta“

Atsijungusio atšaukimų apdorojimas, atskirų laukų diagnostika ir įrodymų eksportai yra PDF parašų patikros API dalis PDFium VCL for Delphi and C++Builder, šalia OpenSSL ir macOS backendų kryžminėms platformoms diegimams