Tekninen artikkeli

OpenSSL AIA- ja CRL-haku PDF-allekirjoituksille Delphissä

PDFium VCL voi nyt täydentää PDF-allekirjoitusketjun ja tarkistaa peruutuksen verkon yli OpenSSL-taustallaan: kun OnlineRetrieval on käytössä, ConfigureSslCmsVerifier asentaa varifoijan joka lataa puuttuvat välisertifiikatit AIA:n caIssuers-URLeista ja CRL:t CRL-jakelupisteistä, kiinteän ajan, pyyntö- ja tavubudjetin sisällä kutakin varifiointikutsua kohti. Ladatut sertifiikaatit ovat vain ja ainoastaan ketjumateriaalia. Luottamus tulee yhä yksinomaan järjestelmävarastosta ja ankkureista jotka konfiguroit

Kuilu jonka tämä sulkee näkyy ensimmäisellä kerralla kun varifoit oikean maailman PDF:eitä Linux-palvelimella. Suuri osa allekirjoittajista upottaa CMS:ään vain oman lehtisertifiikaattinsa, joten OpenSSL ei ulotu juureen, TrustStatus palautuu virheellisenä, ja peruutustarkistusta ei koskaan ajeta koska ketjusta ei koskaan tuli luotettavaa. Ennen v3.121.0:aa artikkelissa PDF-allekirjoitusten varifioinnista OpenSSL:llä PDFium VCL:ssä kuvattu OpenSSL-tausta oli ehdottoman offline ja OnlineRetrievalilla ei ollut vaikutusta siihen. Yksi asia kannattaa sanoa heti alkuun: PDFium-kone itse ei tee yhtään CMS-varifiointia, joten jokainen alla oleva sääntö asuu komponentin PAdES-kerroksessa ja sen OpenSSL-sidonnassa, missä voit lukea sen

Missä järjestyksessä OpenSSL-tausta varifoi, hakee ja tarkistaa?

Ensin eheys, sitten luottamus, sitten peruutus, ja verkkoa kosketaan vain niiden askelten välillä jotka sitä tarvitsevat. VerifyCmsWithSsl tarkistaa CMS-allekirjoituksen ja allekirjoitetut attribuutit (RFC 5652) ketjun evaluointi tukahdutettuna, ja jos se epäonnistuu, se palaa välittömästi ennen kuin hakuistunto edes on olemassa, joten rikki olevia tavuja kantava dokumentti ei laukaise yhtään lähtevää pyyntöä. Vain jos ketju sitten epäonnistuu ja OnlineRetrieval on päällä, se seuraa AIA-linkkejä ja varifoi uudelleen. CRL-jakelupisteet haetaan vasta kun ketju on luotettu, koska luottamattomaan polkuun roikkuva CRL ei todista mitään. Kolme tuomiota pysyvät erillään koko matkan: kelvollinen allekirjoitus puutteellisella ketjulla raportoidaan yhä kelvollisena allekirjoituksena

VerifyCmsWithSslin järjestys PDFium Componentin OpenSSL-taustassa: CMS-allekirjoituksen tarkistus ajetaan ketjun evaluointi tukahdutettuna, joten rikki olevat tavut eivät koske verkkoa; RetrieveIntermediates seuraa AIA:n caIssuers-URLeja vain ketjun epäonnistuttua OnlineRetrieval käytössä, ja RetrieveCrls hakee jakelupisteiden CRL:t erilliseen varastoon vasta kun ketju on luotettu
Eheys, sitten luottamus, sitten peruutus: verkkoa kosketaan vain niiden askelten välillä jotka sitä tarvitsevat, ja luottamattomaan polkuun roikkuva CRL ei todista mitään
uses
  PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;

var
  Pdf: TPdf;
  Probe: TPdfCmsVerifyOptions;
  Diags: TPdfSslVerifyDiagnostics;
  Trust: TPadesTrustValidationOptions;
  Verdict: TPadesValidationResult;
  I: Integer;
begin
  ConfigureSslTrustAnchors(LoadCorporateRoots);   // DER; ainoa ylimääräinen luottamus
  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 oletuksena
  Trust.NetworkPolicy := ptnpOnline;
  Trust.CheckRevocation := True;
  Trust.UrlRetrievalTimeoutMs := 10000;           // kutakin varifiointikutsua kohti

  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;

Miksi ladattuja sertifiikaatteja ei koskaan lisätä luottamusvarastoon?

Koska URLit tulevat validoitavasta sertifiikaatista, ja allekirjoittaja valitsi ne. authorityInfoAccessin caIssuers-merkintä (RFC 5280 §4.2.2.1) on vihje siitä missä myöntäjä asuu, ei mitään muuta. Jos mitä ikinä vastaa kyseiseen URLiin menisi luottamusankkurivarastoon, kuka tahansa voisi allekirjoittaa itsetehdylle avaimelle, osoittaa AIA:n omaan palvelimeensa ja saada vihreän tuomion. RetrieveIntermediates välittää siksi jokaisen jäsennetyn sertifiikaatin CMS_add1_certille, joka sijoittaa sen tämän yhden CMS-rakenteen luottamattomaan joukkoon, ja OpenSSL:n on yhä rakennettava polku siitä ankkuriin jonka konfiguroit tai jonka järjestelmävarasto jo kantaa. On olemassa myös hiljaisempi syy: CMS_verifyin sertifiikaattiargumentti ei ole pudotusvalmis korvike CMS:ään upotetuille sertifiikaateille, joten CMS:ään itsensä lisääminen on luotettava polku

Hakusilmukka on tarkoituksella kapea. RetrieveIntermediates ajaa enintään 4 kierrosta, joista jokainen kerää caIssuers-URLit jokaisesta nyt CMS:ssä olevasta sertifiikaatista, ja pysähtyy heti kun kierros ei lisää mitään tai aikabudjetti on kulunut. Vastauksen on jäsennyttävä d2i_X509illa yksittäiseksi DER-sertifiikaatiksi joka kuluttaa koko rungon; perässä roikkuvat tavut hylätään, ja PKCS#7 certs-only -nipun joka tarjoillaan .p7c-URLista ohitetaan sen sijaan että purettaisiin. Saman AIA-laajennuksen OCSP-hakutapa ohitetaan, koska kyseinen tausta ei puhu OCSP:tä. Peruutuspuolella RetrieveCrls lukee vain jokaisen DistributionPointin (RFC 5280 §4.2.1.13) fullName-URIt CMS-sertifiikaateista ja konfiguroiduista ankkureista, ja ladatut CRL:t menevät toiseen, itsenäiseen X509_STOREiin täyden ketjun CRL-tarkistuksella, joten puuttuva tai vanhentunut CRL muuttaa RevocationStatusia koskematta koskaan TrustStatusiin

// Tiivistetty funktiosta VerifyCmsWithSsl (FPdfCryptoSsl.pas); BIO:n pystytys jätetty pois.
// Jokainen _CMS_verify-kutsu saa tuoreen content-BIO:n
if _CMS_verify(Cms, nil, nil, Bio, nil,
  CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
  Exit;                                   // rikki oleva allekirjoitus: ei verkkoa lainkaan
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, vain luottamaton
  // varifoi ketju uudelleen samaa ankkurivarastoa vasten
end;

if Options.CheckRevocation and (FetchSession <> nil) and
   (Result.TrustStatus = pcvsValid) then
  RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// toinen, erillinen varasto: konfiguroidut CRL:t plus haetut
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);

Mitä yksi varifiointikutsu maksaa enintään?

Kiinteä katto, jota yksi TPdfCryptoFetchSession valvoo ja jota yhden varifiointikutsun AIA- ja CRL-askleet jakavat. Rajat ovat vakioita tiedostossa FPdfCryptoHttp, ei ehdotuksia:

  • Aika: UrlRetrievalTimeoutMs, jonka oletus on 15000 sekä TPdfCmsVerifyOptions.Defaultissa että TPadesTrustValidationOptions.Defaultissa; 0:lla luotu istunto palautuu arvoon 30000, ja kello käynnistyy kun allekirjoitus on päässyt läpi, kattaen jokaisen myöhemmän pyynnön
  • Pyynnöt: enintään 8 istuntoa kohden, laskettuna ennen kuin kuljetusta yritetään, joten kuollut isäntäkin kuluttaa paikan
  • Tavut: 1 MiB vastausta kohden ja 4 MiB yhteensä, ja yli 2048 merkin URLit hylätään ennen yhtään yhteyttä
Yhden TPdfCryptoFetchSessionin tiukat katot jonka varifiointikutsun AIA- ja CRL-askleet jakavat PDFium Componentissa: UrlRetrievalTimeoutMs oletus 15000 ms nollalla palautuen arvoon 30000, enintään 8 pyyntöä istuntoa kohden, 1 MiB vastausta kohden ja 4 MiB yhteensä niin että epäonnistuneetkin vastaukset laskevat, ja yli 2048 merkin URLit hylätään
Rajat ovat vakioita, ei ehdotuksia: epäonnistuneen vastauksen tavutkin kuluttavat budjettia, ja koska jokainen allekirjoitus ja aikaleima varifoidaan erikseen, pahin tapa kasvaa allekirjoitusten määrän mukana

Kirjanpito on tiukempi kuin ensisilmäykseltä näyttää. Epäonnistuneesta vastauksesta vastaanotetut tavutkin laskevat yhteensä vastaan, joten 404:llä suurella sivulla vastaava palvelin ei voi kuluttaa budjettia ilmaiseksi. Vastausrajaa ylittävä luku keskeyttää latauksen sen sijaan että välittäisi typistetyn rungon ASN.1-jäsentimelle, ja tyhjä runko kantava HTTP 200 hylätään suoraan, koska AIA-polku indeksoisi muuten tyhjän taulukon Data[0]in. Vain pelkät http://- ja https://-URLit menevät läpi, ilman uudelleenohjauksia, evästeitä, tunnistetietoja tai automaattista proxyn löytämistä, kun taas HTTPS pitää tavalliset sertifiikkaatti- ja isäntänimetarkistuksensa. URL-deduplikoinnin laajuus on yksi kutsu tahallaan: seuraavan validoinnin on pystyttävä näkemään tuoreelleen julkaistu CRL. Budjetti on myös kutsua kohden, ei dokumenttia kohden, ja ValidatePadesTrust varifoi jokaisen allekirjoituksen ja jokaisen aikaleimatokenin erikseen, joten pahin tapa kasvaa allekirjoitusten määrän mukaan

Miksi aikakatkaistu WinHTTP-pyyntö voi yhä kirjoittaa muistiisi?

Koska paluu aikakatkaisussa ei peruuta jo lennossa olevia callbackeja. Windows-kuljetus ajaa WinHTTP:tä asynkronisesti ja odottaa eventtiä istunnon jäljellä olevalla ajalla, ja kun kyseinen odotus luovuttaa, pyyntö voi yhä viimeistellä luvun ja signaloida sen jälkeen. Osoita asynkroninen luku pinopuskuriin ja kyseinen myöhäinen viimeistely kirjoittaa kehykseen joka kuuluu siihen mennessä jollekin ihan toiselle funktiolle. Korjaus on omistajuus, ei ajoitus: eventti ja 16 kilotavun lukupuskuri asuvat keossa olevassa tietueessa jolla on kaksi viittausta, toisen pitää soittaja ja toisen vapauttaa vain viimeinen HANDLE_CLOSING-callback, joten kumpi puoli viimeistelee viimeisenä vapauttaa muistin

Miksi aikakatkaistu WinHTTP-pyyntö voi yhä kirjoittaa muistiin: paluu aikakatkaisussa jättää callbackit lentoon, joten PDFium Component osoittaa asynkronisen luvun keoon varattuun THttpState-tietueeseen jonka 16 kilotavun puskuria ja kahta viittausta, toista pitää soittaja ja toisen vapauttaa viimeinen HANDLE_CLOSING-callback, vapautetaan vasta kun viimeinen puoli viimeistelee
Myöhäinen viimeistely voi saattaa lukunsa loppuun sen jälkeen kun odotuksesi on luovuttanut; keo-omistajuus kahdella viittauksella tarkoittaa että kyseinen kirjoitus laskeutuu yhä elossa olevaan muistiin
type
  PHttpState = ^THttpState;
  THttpState = record
    References: LongInt;               // soittaja + viimeinen HANDLE_CLOSING-callback
    Event: THandle;
    Status, Count: DWORD;
    Buffer: array[0..16383] of Byte;   // asynkroniset luvut laskeutuvat tähän, ei koskaan pinoon
  end;

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

// Status-callbackissa: HANDLE_CLOSING on viimeinen ilmoitus jonka WinHTTP
// lähettää pyynnölle, joten se pudottaa toisen viittauksen
if Status = HttpHandleClosing then
begin
  ReleaseState(State);
  Exit;
end;

Mitä libcurlin on tarjottava FPC Unixilla

Asynkroninen resolveri ja säieturvallinen buildi, tai verkkohaku pysyy pois. FPC Unixilla kuljetus kulkee libcurlin kautta, sama riippuvuus jonka takana on libcurl-aikaleimatausta ei-Windows-kohteille, ja sidonta hylkää minkä tahansa kirjaston jonka feature-maskilta puuttuu joko CURL_VERSION_ASYNCHDNS tai CURL_VERSION_THREADSAFE. Syy on se, että CURLOPT_NOSIGNAL, jonka toisen prosessin sisällä elävän kirjaston on asetettava, yhdistettynä synkroniseen resolveriin tarkoittaa että DNS-haku voi yksinkertaisesti elää aikakatkaisun yli. Toinen ansa on sammutus: curl_global_cleanup ei odota asynkronisia DNS-säikeitä, joten kun libcurl on kerran alustettu, moduuli pysyy mmapattuna prosessin päättymiseen asti sen sijaan että taustasäie pääsisi juoksemaan purettuun koodiin. Kun kumpikaan vaatimus epäonnistuu, SslCapabilities.OnlineRetrieval on False ja SslVerifyOptionsDiagnostics raportoi psvdOnlineRetrievalIgnoredin sen sijaan että esittäisi verkkoon oltiin yhteydessä

Mitä tulos takaa ja mitä ei

Kelvollinen RevocationStatus kyseiseltä taustalta tarkoittaa että koko ketjun kattavia ajantasaisia CRL:eitä löydettiin, konfiguroitiin tai ladattiin, eikä yksikään listannut siinä olevaa sertifiikaattia; ei mitään muuta. OCSP:tä ei ole, joten CA joka julkaisee peruutukset vain OCSP:n kautta jättää tuloksen ei-tuetuksi, ja verkkovirhe näyttää täsmälleen samalta kuin CA joka ei julkaise mitään. Huomaa myös että psvdNoCrlsConfigured kuvaa vain konfiguroimiasi CRL:eitä, joten verkkohakua käytettäessä se on vihje, ei ennuste epäonnistumisesta. Kun auditointijäljen on oltava toistettavissa ilman verkkoyhteyttä, jätä NetworkPolicy ptnpOffline-oletukseensa: hakuistuntoa ei luoda ja tausta ei koskaan avaa yhteyttä, mikä vastaa CryptoAPI-puolen offline-sopimusta jota käsittelee artikkeli offline-PDF-allekirjoitusten peruutustarkistuksista Windowsissa

Hakukoodi, budjetit ja kuljetussidonnat toimitetaan lähdekoodina mukana PDFium Delphi -komponentin, joten voit varmistaa täsmälleen mitkä URLit validointi saa kontaktoida ja kuinka paljon se saa ladata ennen kuin otat ptnpOnlinein käyttöön palvelimella joka käsittelee luottamattomia dokumentteja