Techninis straipsnis

OpenSSL AIA ir CRL gavimas PDF parašams Delphi

PDFium VCL dabar gali užbaigti PDF parašo grandinę ir patikrinti atšaukimą per tinklą savo OpenSSL galinėje pusėje: įjungus OnlineRetrieval, ConfigureSslCmsVerifier įdiegia patikrintuvą, kuris per AIA caIssuers URL atsisiunčia trūkstamus tarpinius sertifikatus, o CRL — iš CRL distribution points, viska suspaudęs į fiksuotą laiko, užklausų ir baitų biudžetą vienam patikrinimo kvietimui. Parsisiųsti sertifikatai yra tik grandinės statybinė medžiaga. Pasitikėjimas vis tiek ateina vien iš sistemos saugyklos ir jūsų sukonfigūruotų anchor

Plyšys, kurį tai uždaro, išryškėja pirmą kartą realaus pasaulio PDF tikrinant Linux serveryje. Nemaža dalis pasirašytojų į CMS įmeta tik savo lapinį sertifikatą, tad OpenSSL nepasiekia root, TrustStatus sugrįžta neteisėtas, o atšaukimas apskritai nevyksta, nes grandinė niekada netapo pasitikėtina. Iki v3.121.0 OpenSSL galinė pusė, aprašyta kaip tikrinti PDF parašus su OpenSSL PDFium VCL, buvo griežtai neprisijungusi, ir OnlineRetrieval jai nieko nereiškė. Vieną dalyką verta pasakyti iš anksto: pats PDFium variklis CMS patikros apskritai neatlieka, tad visos žemiau esančios taisyklės gyvena komponento PAdES sluoksnyje ir jo OpenSSL binding, kur jas galima perskaityti

Kokia tvarka OpenSSL galinė pusė tikrina, siunčiasi ir žiūri atšaukimą?

Pirma vientisumas, tada pasitikėjimas, paskui atšaukimas, o tinklas paliečiamas tik tarp žingsnių, kuriems jis reikalingas. VerifyCmsWithSsl tikrina CMS parašą ir pasirašytuosius atributus (RFC 5652) su užspaustu grandinės vertinimu, ir jei tai nepavyksta, grįžta tučtuojau, dar prieš egzistuojant bet kokiam siuntimosi seansui, tad dokumentas su sulaužytais baitais neiššaukia nė vienos išorinės užklausos. Tik jei grandinė tada krinta ir OnlineRetrieval įjungtas, jis seka AIA nuorodas ir tikrina dar kartą. CRL distribution points siunčiasi tik tada, kai grandinė pasitikėtina, nes kabančio ant nepasitikėtino kelio CRL nieko neįrodo. Trys verdiktai visame kelyje lieka atskiri: teisėtas parašas su neišbaigta grandine vis tiek pranešamas kaip teisėtas parašas

VerifyCmsWithSsl tvarka PDFium Component OpenSSL galinėje pusėje: CMS parašo tikra eina su užspaustu grandinės vertinimu, tad sulaužyti baitai niekada neliečia tinklo; RetrieveIntermediates seka AIA caIssuers URL tik po grandinės nesėkmės su įjungtu OnlineRetrieval, o RetrieveCrls parsisiunčia distribution point CRL į atskirą saugyklą, kai tik grandinė pasitikėtina
Vientisumas, tada pasitikėjimas, paskui atšaukimas: tinklas paliečiamas tik tarp žingsnių, kuriems jis reikalingas, o ant nepasitikėtino kelio kabančio CRL nieko neįrodo
uses
  PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;

var
  Pdf: TPdf;
  Probe: TPdfCmsVerifyOptions;
  Diags: TPdfSslVerifyDiagnostics;
  Trust: TPadesTrustValidationOptions;
  Verdict: TPadesValidationResult;
  I: Integer;
begin
  ConfigureSslTrustAnchors(LoadCorporateRoots);   // DER; vienintelis papildomas pasitikėjimas
  ConfigureSslCmsVerifier;

  Probe := TPdfCmsVerifyOptions.Default;
  Probe.OnlineRetrieval := True;
  Probe.CheckRevocation := True;
  Diags := SslVerifyOptionsDiagnostics(Probe);
  if psvdOnlineRetrievalIgnored in Diags then
    Log('no HTTP transport or CMS_add1_cert: validation stays offline');

  Trust := TPadesTrustValidationOptions.Default;  // ptnpOffline pagal nutylėjimą
  Trust.NetworkPolicy := ptnpOnline;
  Trust.CheckRevocation := True;
  Trust.UrlRetrievalTimeoutMs := 10000;           // vienam patikrinimo kvietimui

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'signed-contract.pdf';
    Pdf.Active := True;
    Verdict := Pdf.ValidatePadesTrust(Trust);
    for I := 0 to High(Verdict.Signatures) do
      Log(Format('#%d trust=%d revocation=%d', [I,
        Ord(Verdict.Signatures[I].CertificateTrustStatus),
        Ord(Verdict.Signatures[I].RevocationStatus)]));
  finally
    Pdf.Free;
  end;
end;

Kodėl parsisiųsti sertifikatai niekada nepatenka į pasitikėjimo saugyklą?

Nes URL ateina iš tikrinamo sertifikato, o juos parinko pats pasirašytojas. authorityInfoAccess caIssuers įrašas (RFC 5280 §4.2.2.1) yra užuomina, kur gyvena leidėjas, ir nieko daugiau. Jei kas besiatsakantis tam URL patektų į trust-anchor saugyklą, bet kas galėtų pasirašyti savo pagamintu raktu, nukreipti AIA į savą serverį ir gauti žalią verdiktą. RetrieveIntermediates todėl kiekvieną išskaidytą sertifikatą perduoda CMS_add1_cert, kuris jį padeda į šios vienos CMS struktūros nepatikėtinųjų rinkinį, ir OpenSSL vis tiek turi nuo jo pastatyti kelią iki anchor, kurį sukonfigūravote, ar kurį jau laiko sistemos saugykla. Yra ir tylesnė priežastis: CMS_verify sertifikatų argumentas nėra tiesioginis pakaitalas CMS įmontuotiems sertifikatams, tad pridėjimas į patį CMS — patikimas kelias

Siuntimosi ciklas sąmoningai siauras. RetrieveIntermediates vyksta ne daugiau nei 4 ratus, kiekvieną kartą surinkdama caIssuers URL iš kiekvieno dabar CMS esančio sertifikato, ir stabo vos tik ratas nieko nepridėjo arba išseko laiko biudžetas. Atsakymas turi išskaidytis per d2i_X509 kaip vienas DER sertifikatas, prarijęs visą kūną; galiniai baitai atmetami, o PKCS#7 tik sertifikatų ryšulys, patiektas iš .p7c URL, praleidžiamas vietoj išpakavimo. OCSP prieigos metodas toje pačioje AIA plėtinyje ignoruojamas, nes ši galinė pusė OCSP nekalba. Atšaukimo pusėje RetrieveCrls skaito tik kiekvieno DistributionPoint (RFC 5280 §4.2.1.13) fullName URI iš CMS sertifikatų ir sukonfigūruotų anchor, o parsisiųstos CRL keliauja į antrą, nepriklausomą X509_STORE su visos grandinės CRL tikra, tad nesama ar pasenusi CRL pakeičia RevocationStatus neliesdama TrustStatus

// Sutraukta iš VerifyCmsWithSsl (FPdfCryptoSsl.pas); BIO paruošimas praleistas.
// Kiekvienas _CMS_verify kvietimas gauna šviežią content BIO
if _CMS_verify(Cms, nil, nil, Bio, nil,
  CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
  Exit;                                   // sulaužytas parašas: apskritai jokio tinklo
if Options.OnlineRetrieval and SslCapabilities.OnlineRetrieval then
  FetchSession := TPdfCryptoFetchSession.Create(Options.UrlRetrievalTimeoutMs);

Store := BuildStore(False, nil, RevocationChecked, CrlsMalformed);
if (_CMS_verify(Cms, nil, Store, Bio, nil, CMS_BINARY) <> 1) and
   (FetchSession <> nil) then
begin
  RetrieveIntermediates(Cms, FetchSession);  // CMS_add1_cert, tik nepatikėtinieji
  // grandinė tikrinama dar kartą prieš to paties anchor store
end;

if Options.CheckRevocation and (FetchSession <> nil) and
   (Result.TrustStatus = pcvsValid) then
  RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// antra, atskira saugykla: sukonfigūruotos CRL plus parsisiųstosios
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);

Kiek daugiausiai kainuoja vienas patikrinimo kvietimas?

Fiksuotos lubos, kurių laikosi vienas TPdfCryptoFetchSession, kurį vieno patikrinimo kvietimo AIA ir CRL žingsniai dalijasi. Riboženklės — konstantos FPdfCryptoHttp viduje, o ne pasiūlymai:

  • Laikas: UrlRetrievalTimeoutMs, kuris abiejuose TPdfCmsVerifyOptions.Default ir TPadesTrustValidationOptions.Default numatytai 15000; seansas, sukurtas su 0, grįžta prie 30000, o laikrodis startuoja vos parašui praeinus, dengdamas kiekvieną vėlesnę užklausą
  • Užklausos: ne daugiau nei 8 per seansą, skaičiuojamos dar prieš bandant transportą, tad negyvas serveris vis tiek sudegina vietą
  • Baitai: 1 MiB vienam atsakymui ir 4 MiB iš viso, o URL, ilgesni nei 2048 simbolių, atmetami dar prieš bet kokį prisijungimą
Vienos TPdfCryptoFetchSession, kurios PDFium Component patikrinimo kvietimo AIA ir CRL žingsniai dalijasi, kietos lubos: UrlRetrievalTimeoutMs numatytai 15000 ms su nuliniu atkritimu į 30000, ne daugiau nei 8 užklausos per seansą, 1 MiB atsakymui ir 4 MiB iš viso, nesėkmingi atsakymai vis tiek skaičiuojasi, o URL virš 2048 simbolių atmetami
Riboženklės yra konstantos, o ne pasiūlymai: nesėkmingo atsakymo baitai vis tiek išsenkina biudžetą, ir, kadangi kiekvienas parašas bei laiko žyma tikrinami atskirai, blogiausias atvejis auga su parašų skaičiumi

Apskaita griežtesnė, nei pirmu žvilgsniu atrodo. Nesėkmingo atsakymo baitai vis tiek skaitosi į bendrąją sumą, tad serveris, atsakantis 404 dideliu puslapiu, biudžeto neatlygintinai neišsenkins. Skaitymas, peržengęs vieno atsakymo ribą, nutraukia parsisiuntimą, vietoj to, kad perduotų nukirstą kūną ASN.1 parseriui, o HTTP 200 su tuščiu kūnu atmetamas iš karto, nes AIA kelias kitaip indeksuotų Data[0] tuščio masyvo. Praeina tik paprasti http:// ir https:// URL, be persiuntimų, slapukų, kredencialų ir automatinio proxy aptikimo, o HTTPS išlaiko įprastas sertifikatų ir hostname patikras. URL deduplikacija sąmoningai apibrėžta vienam kvietimui: kita validacija turi galėti išvygti šviežiai paskelbtą CRL. Biudžetas irgi vienam kvietimui, o ne dokumentui, ir ValidatePadesTrust kiekvieną parašą bei kiekvieną laiko žymos tokeną tikrina atskirai, tad blogiausias atvejis auga su parašų skaičiumi

Kodėl WinHTTP laiką viršijusi užklausa vis dar gali rašyti į jūsų atmintį?

Nes grįžimas pagal laiką neatšauka jau pakeliui esančių callback. Windows transportas vairuoja WinHTTP asinchroniškai ir laukia įvykio su seanso likusiu laiku, ir, kai tas laukimas pasiduoda, užklausa vis dar gali užbaigti skaitymą ir pasigirdėti vėliau. Nukreipkite asinchroninį skaitymą į stack buferį — ir tas vėlyvas užbaigimas rašo į kadrą, kuris tuo metu jau priklauso kokiai nors nesusijusiai funkcijai. Pataisymas — nuosavybė, o ne laikas: įvykis ir 16 KB skaitymo buferis gyvena heap įraše su dviem nuorodomis, vieną laiko kvietėjas, kitą atlaisvina tik galutinis HANDLE_CLOSING callback, tad kuriai pusei pasibaigus paskutinei, ta atlaisvina atmintį

Kodėl laiką viršijusi WinHTTP užklausa vis dar gali rašyti į atmintį: grįžimas pagal laiką palieka callback pakeliui, tad PDFium Component asinchroninį skaitymą nukreipia į heap skirtą THttpState įrašą, kurio 16 KB buferis ir dvi nuorodos, vieną laiko kvietėjas, kitą atlaisvina galutinis HANDLE_CLOSING callback, atlaisvinami tik paskutinei pusei pasibaigus
Vėlyvas užbaigimas gali užbaigti skaitymą, kai jūsų laukimas jau pasidavė; heap nuosavybė su dviem nuorodomis reiškia, kad tas rašymas nukrenta į vis dar gyvą atmintį
type
  PHttpState = ^THttpState;
  THttpState = record
    References: LongInt;               // kvietėjas + galutinis HANDLE_CLOSING callback
    Event: THandle;
    Status, Count: DWORD;
    Buffer: array[0..16383] of Byte;   // čia nusileidžia asinchroniniai skaitymai, niekada ant stack
  end;

procedure ReleaseState(State: PHttpState);
begin
  if InterlockedDecrement(State.References) = 0 then
  begin
    CloseHandle(State.Event);
    Dispose(State);
  end;
end;

// Statuso callback viduje: HANDLE_CLOSING yra paskutinė pranešimo užklausa,
// kurią WinHTTP siunčia prašymui, tad ji numeta antrąją nuorodą
if Status = HttpHandleClosing then
begin
  ReleaseState(State);
  Exit;
end;

Ką libcurl turi duoti FPC Unix

Asinchroninis resolver ir gijoms saugus build, kitaip online gavimas lieka išjungtas. FPC Unix transportas eina per libcurl — tą pačią priklausomybę už libcurl laiko žymų galinę pusę ne Windows tikslams, — ir binding atsisako bet kurios bibliotekos, kurios feature maskai trūksta CURL_VERSION_ASYNCHDNS ar CURL_VERSION_THREADSAFE. Priežastis ta, jog CURLOPT_NOSIGNAL, kurį privalo nustatyti biblioteka, gyvenanti kieno nors kito procese, sudėtas su sinchroniniu resolver reiškia, kad DNS paieška gali paprasčiausiai išgyventi ilgiau už timeout. Antra spąstai — išjungimas: curl_global_cleanup nelaukia asinchroninių DNS gijų, tad kai libcurl kartą inicijuotas, modulis lieka užkeltas iki proceso išėjimo, vietoj to, kad fono gija įbėgtų į jau nuimtą kodą. Kai kuri iš reikalavimų krenta, SslCapabilities.OnlineRetrieval yra False, o SslVerifyOptionsDiagnostics praneša psvdOnlineRetrievalIgnored, vietoj to, kad apsimestų, jog tinklas buvo klaustas

Ką rezultatas garantuoja ir ko negarantuoja

Teisėtas RevocationStatus iš šios galinės pusės reiškia, jog rasti, sukonfigūruoti ar parsisiųsti dabartiniai CRL, dengiantys visą grandinę, ir nė vienas jų jame sertifikato neužrašė; nieko daugiau. OCSP nėra, tad CA, skelbianti atšaukimą tik per OCSP, palieka rezultatą nepalaikomu, o tinklo nesėkmė atrodo lygiai taip, kaip CA, neskelbiančios nieko. Pastebėkite ir tai, jog psvdNoCrlsConfigured apibūdina tik jūsų sukonfigūruotas CRL, tad su online gavimu tai užuomina, o ne nesėkmės prognozė. Kai audito pėdsakas turi būti atkuriamas be tinklo, palikite NetworkPolicy prie jo ptnpOffline numatytosios: joks siuntimosi seansas nesukuriamas, o galinė pusė niekada neatveria ryšio — tai sutampa su CryptoAPI pusės neprisijungimo kontraktu, aprašytu neprisijungusiuose PDF parašo atšaukimo tikrinimuose Windows

Gavimo kodas, biudžetai ir transporto binding keliauja kaip šaltinis kartu su PDFium Delphi komponentu, tad galite patys patikslinti, kokiais URL validacija gali kreiptis ir kiek ji gali parsisiųsti, dar prieš įjungdami ptnpOnline serveryje, kuris tvarko nepatikėtinus dokumentus