Tehnični članak

Preklici podpisov PDF brez povezave na Windows v Delphiju

PDFium VCL preverja preklice podpisov PDF brez povezave na Windows tako, da k preklicnemu prehodu CertGetCertificateChain doda CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY, ker zastavica samo-iz-predpomnilnika, uporabljena pri gradnji verige, pridobivanja CRL ali OCSP sploh ne pokriva. Od v3.119.1 offline klic ValidatePadesTrust ostane izven omrežja in čist rezultat zahteva dejanske dokaze o preklicu na potrdilo. Preostanek te objave je o tem, zakaj je bilo treba popraviti obe polovici te povedi

Nastavitev, ki razkrije težavo, je vsakdanja. Validacijska storitev teče na zaklenjenem gostitelju Windows, TPadesTrustValidationOptions.NetworkPolicy je ptnpOffline (kar je tudi privzeto), operater pa pričakuje, da bo vsak odgovor prišel iz lokalnega predpomnilnika potrdil. Potem nekdo opazi odhodne zahteve k distribucijski točki CA v dnevniku požarnega zidu ali paketno opravilo, ki se zatakne za polne UrlRetrievalTimeoutMs 15.000 ms pri vsakem podpisu. Nič v kodi ni prosilo za omrežje. Windows je šel tja vseeno

Zakaj offline gradnja verige še vedno pridobi CRL na Windows?

Ker CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL omeji le pridobivanje URL, ki ga dela gradnja verige: pridobivanja izdajateljev AIA, posodobitve korenov in CTL. Microsoftova dokumentacija za CertGetCertificateChain izrecno pove, da zastavica ne velja za preverjanje preklicov. Preklic ima svoje stikalo, CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY ($80000000), in brez njega so ponudniki preklicov svobodni, da prenesejo CRL ali pošljejo zahtevo OCSP, čeprav obkrožajoči klic izgleda brez povezave. PDFium VCL zdaj to zastavico z OR združi s preklicnim prehodom, kadarkoli je OnlineRetrieval False, na vrhu zastavic verige, CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT in CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT. To šteje za več kot zakasnitev: zahteva OCSP pove odgovorjalcu, katero potrdilo gledate — točno to, česar se mora izogibati validator, odrezan od omrežja

Dve neodvisni stikali varujeta validacijo podpisov PDF brez povezave na Windows: CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL omeji le gradnjo verige, kot so pridobivanja izdajateljev AIA in korenov, preklic pa potrebuje CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY, ker brez nje ponudniki še vedno prenesejo CRL in pošljejo zahteve OCSP, ki razkrijejo, katero potrdilo se validira
PDFium VCL z OR združi zastavico preklica samo-iz-predpomnilnika s preklicnim prehodom, kadarkoli je OnlineRetrieval False, tako da validacija zaupanja ptnpOffline ostane izven omrežja za oba prehoda
uses
  PDFium, FPdfCrypto, FPdfPades;

var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
begin
  Options := TPadesTrustValidationOptions.Default; // ptnpOffline, 15000 ms
  Options.CheckRevocation := True;                 // privzeto False
  Options.CheckTimeStamps := True;
  // Offline zdaj pomeni offline tudi za preklic: le predpomnjeni CRL in OCSP
  // odgovori, kontrolna točka pcvstOnlineRetrieval se ne sproži
  Report := Pdf.ValidatePadesTrust(Options);
end;

Dve gradnji verige, dve polji napak

Zaledna stran Windows gradi verigo dvakrat in vsaka gradnja zdaj lasti svojo režo napake. Prvi klic CertGetCertificateChain teče brez zastavic preklica in CertVerifyCertificateChainPolicy napaja z osnovno politiko, kar proizvede TrustStatus in TrustError. Drugi klic doda zastavice preklica. Pred v3.119.1 je odpoved drugega klica zapisala GetLastError v TrustError, tako da je lahko veriga, pravkar potrjena kot zaupanja vredna, prišla nazaj videti nezaupanja vredna, ker je kihnil ponudnik preklicov. Popravek takoj prebere GetLastError in ga shrani v TPdfCmsVerifyResult.RevocationError, sodbo prvega prehoda pa pusti pri miru. In True, vrnjeno iz drugega klica, se ne obravnava kot uspeh; pomeni le, da je Windows vrnil kontekst verige, vreden pregleda

Kaj dejansko dokaže ničelna maska napak zaupanja?

Sam zase, nič. Združeni TrustStatus.dwErrorStatus nič po preklicnem prehodu pove, da ni bil dvignjen noben bit napake, veriga, kjer noben element sploh ni nosil informacije o preklicu, pa lahko proizvede točno to. Starejša koda je preslikala »noben bit preklica, noben neznan bit, noben offline bit« naravnost v veljavno — klasičen način, kako validator prijavi nepreverjeno potrdilo kot čisto. Nova rutina ReadWinRevocationEvidence obhodi vsako preprosto verigo in vsak element, zavrne strukture, katerih cbSize je premajhen za varno branje, in prijavi uspeh le, kadar obstaja vsaj en element, ki ni koren, in vsak tak element nosi CERT_REVOCATION_INFO, katerega dwRevocationResult je nič

Obhod dokazov, ki ga ReadWinRevocationEvidence opravi na vsakem elementu verige Windows v Delphiju: pRevocationInfo mora biti prisoten, cbSize mora biti dovolj velik za branje, dwRevocationResult mora biti nič, končni element pa je izključen le, kadar je označen kot samopodpisan ali CA-zaupan, tako da ničelna maska napak zaupanja ne more več skriti nepreverjenega potrdila
Čista sodba zahteva vsaj en element, ki ni koren, in odgovor vsakega zahtevanega elementa, RevocationError pa ohrani surov DWORD ponudnika, kot je CRYPT_E_REVOKED
// Skrčeno iz obhoda dokazov: element šteje le, kadar je zanj
// dejansko odgovoril ponudnik preklicov
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); // veriga samo s korenom ne dokaže nič

Rezultat ponudnika ostane surov. RevocationError nosi DWORD dwRevocationResult točno tako, kot ga je vrnil ponudnik, pri čemer raje vzame napako preklicanega elementa, kadar ta obstaja (CRYPT_E_REVOKED je $80092010), in bitna maska zaupanja se nikoli ne obleče v domačo kodo napake. Preslikava v TPdfCmsRevocationReason je namerno groba: pcrrCertificateRevoked z pcvsInvalid za izrecen preklic, pcrrChainUntrusted, kadar veriga spodleti iz razlogov, nesorodnih preklicu, in pcrrUnknown za vse ostalo. Windows je lahko poskusil OCSP namesto CRL, zato se offline ali neznan rezultat ne prevede v pcrrCrlExpired. Zaledje za overjanje CMS OpenSSL lahko naredi razlike, specifične za CRL, ker ovrednoti le CRL, ki mu jih podate, zaledje SecTrust za macOS pa polja pusti na pcrrNone in nič, kar pomeni »brez podrobne diagnostike« in ne »uspešno«

Kje se izključitev korena ustavi

CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT upravičeno preskoči sidro, ker nihče ne objavlja CRL, ki bi preklical koren proti njemu samemu. Past je odločiti, kateri element je koren. PDFium VCL izključi zadnji element preproste verige le, kadar ga njegov dwInfoStatus označi kot samopodpisan ($00000008) ali izrecno CA-zaupan ($00004000). Gostitelj brez povezave pogosto ne more pridobiti manjkajočega izdajatelja, zato se veriga konča pri vmesnem; obravnavati zadnji element te delne verige kot koren bi tiho odvrglo tisto potrdilo, katerega status preklica je najbolj verjetno odsoten iz predpomnilnika. Ta element ostane v zahtevani množici, nima odgovora ponudnika in rezultat ostane pcvsIndeterminate

Kako se rezultati preklica podpisa in časovnega žiga držita ločeno?

Kot ločena polja, ki se nikoli ne prepišeta. Validator PAdES potrdi ločeno CMS podpisa dokumenta in priloženo CMS žetona časovnega žiga RFC 3161 v dveh neodvisnih klicih, v3.119.0 pa je vsakemu dal njegovo lastno diagnostiko na TPadesSignatureValidation: RevocationReason in NativeRevocationError za podpisovalca, TimeStampRevocationReason in NativeTimeStampRevocationError za TSA. Preklicano potrdilo TSA zato ne more nastopati kot preklican podpisovalec, odpoved časovnega žiga pa ne izbriše rezultata celovitosti, ki je že bil ugotovljen. Ko je CheckRevocation False ali validacija do te faze sploh ni prišla, polja ostanejo pcrrNone in 0, zato ju vedno berite poleg RevocationStatus in TimeStampRevocationStatus

Preklic podpisa in časovnega žiga ostajata ločena v PDFium VCL: ločeni CMS podpisa dokumenta napolni RevocationReason in NativeRevocationError, priloženi CMS žetona RFC 3161 napolni TimeStampRevocationReason in NativeTimeStampRevocationError, nepreverjene faze pa pustijo pcrrNone in nič poleg svojih polj statusa
Preklicano potrdilo TSA zato ne more nastopati kot preklican podpisovalec, odpoved časovnega žiga pa nikoli ne izbriše rezultata celovitosti, ki ga je overjanje podpisa že ugotovilo
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;

Poročilo dokazov sledi istemu pravilu. Izvoz CSV pripne revocationReason, nativeRevocationError in stolpce časovnega žiga do nativeTimeStampRevocationError na konec obstoječega vrstnega reda stolpcev, tako da starejši razčlenjevalniki še naprej delujejo, izvoz JSON pa doda ujemajoča se polja, ne da bi spremenil pomen starih. Če validacija brez povezave vztrajno prihaja nazaj kot nedoločena, je trajen popravek navzgor: zberite validacijski material ob podpisovanju, kot opisuje dolgoročni podpisi PDF s časovnimi žigi RFC 3161 in DSS, namesto da bi upali, da ima preverjalni stroj topel predpomnilnik

Kaj matrika preizkusov dokaže in česa ne

Matrika overjanja Windows je prestala 30 nadzorovanih scenarijev API-ja verig in en pravi offline dimni preizkus CMS na vsakem cilju Delphi in FPC Win32 ter Win64. Pravi dimni preizkus potrdi veljaven podpis pod nezaupanja vrednim zasebnim CA, medtem ko čiste in izrecno preklicane izide prinašajo zastavljeni (stub) odgovori CertGetCertificateChain in ne nameščena sidra zaupanja ali živo pridobivanje. To je iskrena meja, vredna izrekanja: ravnanje z zastavicami, izolacija napak in obhod dokazov so pripeti, kaj pa vsebuje predpomnilnik preklicov posameznega stroja na dani dan, je še vedno zadeva Windows, prazen predpomnilnik pa zdaj pravilno proizvede »neznano« namesto omrežne zahteve ali lažnega »veljavno«

Ravnanje s preklici brez povezave, diagnostika na polje in izvozi dokazov so del API-ja za validacijo podpisov PDF v PDFium VCL za Delphi in C++Builder, skupaj z zaledji OpenSSL in macOS za večplatformske namestitve