Odborný článok

Sťahovanie AIA a CRL pre PDF podpisy v Delphi cez OpenSSL

PDFium VCL teraz dokáže na svojom OpenSSL backende dotvoriť reťaz PDF podpisu a skontrolovať revokáciu cez sieť: keď je zapnuté OnlineRetrieval, ConfigureSslCmsVerifier nainštaluje verifikátor, ktorý sťahuje chýbajúce intermediátne certifikáty z AIA caIssuers URL a CRL z CRL distribution points, v pevnom čase, počte požiadaviek a bajtovom rozpočte na jedno overovacie volanie. Stiahnuté certifikáty sú výhradne materiál reťaza. Trust stále prichádza výhradne zo systémového store a z kotiev, ktoré nakonfigurujete

Medzera, ktorú to zatvára, sa ukáže pri prvom overení reálnych PDF na Linuxovom serveri. Veľká časť podpisovateľov vkladá do CMS len vlastný leaf certifikát, takže OpenSSL sa nedostane ku koreňu, TrustStatus sa vráti ako invalid a revokácia sa ani nespustí, lebo reťaz sa nikdy nestal dôveryhodným. Pred v3.121.0 bol OpenSSL backend popísaný v overovaní PDF podpisov s OpenSSL v PDFium VCL striktne offline a OnlineRetrieval naňho nemal žiadny vplyv. Jedna vec sa oplatí povedať rovno: samotný PDFium engine nerobí CMS verifikáciu vôbec, takže všetky pravidlá nižšie žijú v PAdES vrstve komponentu a jeho OpenSSL bindingu, kde si ich môžete prečítať

V akom poradí OpenSSL backend overuje, sťahuje a kontroluje?

Najprv integrita, potom trust, potom revokácia a sieť sa dotkne len medzi krokmi, ktoré ju potrebujú. VerifyCmsWithSsl kontroluje CMS podpis a signed attributes (RFC 5652) s potlačeným vyhodnotením reťaza a keď to padne, vracia sa okamžite, skôr než fetch session vôbec existuje, takže dokument s pokorenými bajtmi nespustí žiadnu odchádzajúcu požiadavku. Až keď potom reťaz padne a OnlineRetrieval je zapnuté, nasleduje AIA odkazy a overenie znova. CRL distribution points sa sťahujú až vtedy, keď je reťaz dôveryhodný, lebo CRL visiaca na nedôveryhodnej ceste nič nedokazuje. Tri verdikty zostávajú oddelené po celý čas: platný podpis s neúplným reťazom sa aj naďalej hlási ako platný podpis

Poradie VerifyCmsWithSsl v OpenSSL backende PDFium Component: kontrola CMS podpisu beží s potlačeným vyhodnotením reťaza, takže pokorené bajty sa siete nikdy nedotknú; RetrieveIntermediates nasleduje AIA caIssuers URL až po páde reťaza so zapnutým OnlineRetrieval a RetrieveCrls sťahuje distribution point CRL do samostatného store, keď je reťaz dôveryhodný
Integrita, potom trust, potom revokácia: sieť sa dotkne len medzi krokmi, ktoré ju potrebujú, a CRL visiaca na nedôveryhodnej ceste nič nedokazuje
uses
  PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;

var
  Pdf: TPdf;
  Probe: TPdfCmsVerifyOptions;
  Diags: TPdfSslVerifyDiagnostics;
  Trust: TPadesTrustValidationOptions;
  Verdict: TPadesValidationResult;
  I: Integer;
begin
  ConfigureSslTrustAnchors(LoadCorporateRoots);   // DER; jediný extra trust
  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;  // predvolené ptnpOffline
  Trust.NetworkPolicy := ptnpOnline;
  Trust.CheckRevocation := True;
  Trust.UrlRetrievalTimeoutMs := 10000;           // na jedno overovacie volanie

  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;

Prečo sa stiahnuté certifikáty nikdy nepridávajú do trust store?

Pretože URL prichádzajú z certifikátu, ktorý sa práve overuje, a vybral ich podpisovateľ. Položka authorityInfoAccess caIssuers (RFC 5280 §4.2.2.1) je hint o tom, kde býva issuer, nič viac. Keby čokoľvek, čo na tú URL odpovie, skončilo v trust-anchor store, ktokoľvek by si mohol podpísať vlastnoručným kľúčom, namieriť AIA na vlastný server a dostať zelený verdikt. RetrieveIntermediates preto podáva každý vyparsovaný certifikát CMS_add1_cert, ktorý ho umiestni do untrusted množiny tejto jednej CMS štruktúry, a OpenSSL aj tak musí postaviť cestu z neho ku kotve, ktorú ste nakonfigurovali, alebo ktorú už drží systémový store. Je tu aj tichší dôvod: certifikátový argument CMS_verify nie je náhrada za certifikáty vložené v CMS, takže pridávanie do samotného CMS je spoľahlivá cesta

Retrieval cyklus je zámerne úzky. RetrieveIntermediates beží nanajvýš 4 kola, každé zbiera caIssuers URL z každého certifikátu, ktorý je teraz v CMS, a zastaví, hneď ako kolo nič nepridá alebo sa minie časový rozpočet. Odpoveď sa musí dekódovať cez d2i_X509 ako jediný DER certifikát, ktorý zožerie celé telo; trailing bajty sa odmietajú a PKCS#7 certs-only balík podávaný z URL .p7c sa preskočí namiesto rozbalenia. OCSP access method v tej istej AIA extenzií sa ignoruje, keďže tento backend nerozpráva OCSP. Na strane revokácie číta RetrieveCrls len fullName URI každého DistributionPoint (RFC 5280 §4.2.1.13) z CMS certifikátov a nakonfigurovaných kotiev a stiahnuté CRL idú do druhého, nezávislého X509_STORE s full-chain CRL kontrolou, takže chýbajúce alebo zastarané CRL zmení RevocationStatus, bez toho, aby sa kedykoľvek dotklo TrustStatus

// Zhrnuté z VerifyCmsWithSsl (FPdfCryptoSsl.pas); BIO setup vynechaný.
// Každé volanie _CMS_verify dostáva čerstvý content BIO
if _CMS_verify(Cms, nil, nil, Bio, nil,
  CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
  Exit;                                   // pokorený podpis: vôbec žiadna sieť
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, len untrusted
  // overte reťaz znova proti tomuto istému anchor store
end;

if Options.CheckRevocation and (FetchSession <> nil) and
   (Result.TrustStatus = pcvsValid) then
  RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// druhý, samostatný store: nakonfigurované CRL plus stiahnuté
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);

Koľko stojí jedno overovacie volanie nanajvýš?

Pevný strop, vynucovaný jediným TPdfCryptoFetchSession, ktorý zdieľajú kroky AIA a CRL jedného overovacieho volania. Limity sú konštanty v FPdfCryptoHttp, nie odporúčania:

  • Čas: UrlRetrievalTimeoutMs, ktorý sa v TPdfCmsVerifyOptions.Default aj TPadesTrustValidationOptions.Default predvolene rovná 15000; session vytvorená s 0 prepadne na 30000 a hodiny štartujú, keď podpis prešiel, a pokrývajú každú neskoršiu požiadavku
  • Požiadavky: najviac 8 na session, počítaných skôr, než sa transport skúsi, takže mŕtvy host aj tak spotrebuje slot
  • Bajty: 1 MiB na odpoveď a 4 MiB celkovo, s URL dlhšími než 2048 znakov odmietnutými pred akýmkoľvek spojením
Tvrdé stropy jednej TPdfCryptoFetchSession zdieľanej krokmi AIA a CRL overovacieho volania v PDFium Component: UrlRetrievalTimeoutMs sa predvolene rovná 15000 ms s nulovým fallbackom 30000, najviac 8 požiadaviek na session, 1 MiB na odpoveď a 4 MiB celkovo s tým, že aj zlyhané odpovede sa počítajú, a URL nad 2048 znakov sa odmietajú
Limity sú konštanty, nie odporúčania: bajty z zlyhanej odpovede tiež drainujú rozpočet a keďže každý podpis a každá timestamp sa overujú samostatne, najhorší prípad rastie s počtom podpisov

Účtovníctvo je prísnejšie, než vyzerá na prvý pohľad. Bajty prijaté z zlyhanej odpovede sa do celku tiež počítajú, takže server odpovedajúci 404 s veľkou stránkou nemôže rozpočet vypustiť zadarmo. Čítanie prekračujúce limit na odpoveď preruší sťahovanie namiesto toho, aby posunulo useknuté telo ASN.1 parseru, a HTTP 200 s prázdnym telom sa odmietne rovno, lebo by AIA cesta inak indexovala Data[0] prázdneho pola. Prejdu len holé URL http:// a https://, bez redirectov, cookies, credentials či automatického proxy discovery, zatiaľ čo HTTPS si drží svoje obvyklé certifikátové a hostname kontroly. Deduplikácia URL je zámerne v rozsahu jedného volania: ďalšia validácia musí mať šancu vidieť čerstvo publikované CRL. Rozpočet je tiež na volanie, nie na dokument a ValidatePadesTrust overuje každý podpis a každý timestamp token samostatne, takže najhorší prípad rastie s počtom podpisov

Prečo môže vypršaná WinHTTP požiadavka stále zapisovať do vašej pamäte?

Pretože návrat pri timeoute nezruší callbacky, ktoré už letia. Windows transport poháňa WinHTTP asynchrónne a čaká na event so zvyšným časom session a keď toto čakanie vzdá, požiadavka môže aj naďalej dokončiť čítanie a dať o sebe vedieť potom. Namierte asynchrónne čítanie na stack buffer a toto neskoré dokončenie zapíše do rámu, ktorý vtedy patrí nejakej úplne nesúvisiacej funkcii. Opravou je vlastníctvo, nie načasovanie: event aj 16 KB read buffer žijú v heap recorde s dvomi referenciami, jednu drží volajúci a jednu uvoľňuje až finálny callback HANDLE_CLOSING, takže ktorákoľvek strana skončí ako posledná, uvoľní pamäť

Prečo môže vypršaná požiadavka WinHTTP stále zapisovať do pamäte: návrat pri timeoute nechá callbacky letieť, takže PDFium Component nameriava asynchrónne čítanie na heap alokovaný record THttpState, ktorého 16 KB buffer a dve referencie, jednu drží volajúci a jednu uvoľňuje finálny callback HANDLE_CLOSING, sa uvoľnia až keď skončí posledná strana
Neskoré dokončenie môže dokočiť svoje čítanie, aj keď vaše čakanie už vzdalo; heap vlastníctvo s dvomi referenciami znamená, že tento zápis pristane v pamäti, ktorá ešte žije
type
  PHttpState = ^THttpState;
  THttpState = record
    References: LongInt;               // volajúci + finálny callback HANDLE_CLOSING
    Event: THandle;
    Status, Count: DWORD;
    Buffer: array[0..16383] of Byte;   // async čítania pristávajú tu, nikdy na stacku
  end;

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

// V status callbacku: HANDLE_CLOSING je posledné oznámenie, ktoré WinHTTP
// pre túto požiadavku posiela, takže odpočíta druhú referenciu
if Status = HttpHandleClosing then
begin
  ReleaseState(State);
  Exit;
end;

Čo musí libcurl dodať na FPC Unix

Asynchrónny resolver a thread-safe build, inak online retrieval ostáva vypnutý. Na FPC Unix vedie transport cez libcurl, tú istú závislosť za libcurl timestamp backendom pre ne-Windows ciele, a binding odmietne ľubovoľnú knižnicu, ktorej feature maska postráda CURL_VERSION_ASYNCHDNS alebo CURL_VERSION_THREADSAFE. Dôvodom je, že CURLOPT_NOSIGNAL, ktorý musí nastaviť knižnica žijúca v niečom cudzom procese, v kombinácii so synchrónnym resolverom znamená, že DNS lookup jednoducho môže prežiť timeout. Druhá pasca je shutdown: curl_global_cleanup nečaká na asynchrónne DNS thready, takže keď sa libcurl raz inicializuje, modul ostane namapovaný až do konca procesu, namiesto aby pustil background thread bežať do odmapovaného kódu. Keď niektorá z požiadaviek padne, SslCapabilities.OnlineRetrieval je False a SslVerifyOptionsDiagnostics hlási psvdOnlineRetrievalIgnored namiesto toho, aby predstieral, že sa siete niekto spýtal

Čo výsledok garantuje a čo nie

Platný RevocationStatus od tohto backendu znamená, že sa našli, nakonfigurovali alebo stiahli aktuálne CRL pokrývajúce celý reťaz a žiadne neuviedlo certifikát v ňom; nič viac. OCSP tu nie je, takže CA publikujúca revokáciu len cez OCSP nechá výsledok unsupported a sieťové zlyhanie vyzerá presne ako CA, ktorá nepublikuje nič. Všímajte si tiež, že psvdNoCrlsConfigured popisuje len CRL, ktoré ste nakonfigurovali vy, takže pri online retrieval je to hint, nie forecast zlyhania. Keď musí byť audit trail reprodukovateľný bez prístupu k sieti, nechajte NetworkPolicy na predvolenom ptnpOffline: žiadna fetch session sa nevytvorí a backend nikdy neotvorí spojenie, čo sedí s offline zmluvou na strane CryptoAPI popísanej v offline kontrolách revokácie PDF podpisov na Windows

Retrieval kód, rozpočty aj transport bindingy idú ako zdroják s PDFium Delphi component, takže si môžete potvrdiť presne to, ktoré URL môže validácia kontaktovať a koľko môže stiahnuť, skôr než zapnete ptnpOnline na serveri, ktorý spracováva nedôveryhodné dokumenty