Teknisk artikel

AIA- och CRL-hämtning för PDF-signaturer i Delphi

PDFium VCL kan nu komplettera en PDF-signaturkedja och kontrollera spärrning över nätverket på sin OpenSSL-backend: när OnlineRetrieval är påslagen installerar ConfigureSslCmsVerifier en verifierare som laddar ner saknade intermediate-certifikat från AIA caIssuers-URL:erna och CRL:er från CRL-distributionspunkterna, inom en fast tid, begärande- och bytebudget per verifieringsanrop. Nerladdade certifikat är bara kedjematerial. Tilltron kommer fortfarande uteslutande från systemlagret och de ankare du konfigurerar

Luckan det här stänger visar sig första gången du validerar verkliga PDF:er på en Linux-server. En stor andel signerare bäddar bara in sitt eget lövcertifikat i CMS:en, så att OpenSSL inte når någon rot, TrustStatus kommer tillbaka ogiltigt, och spärrningen körs aldrig eftersom kedjan aldrig blev trovärdig. Före v3.121.0 var OpenSSL-backend som beskrivs i att verifiera PDF-signaturer med OpenSSL i PDFium VCL strikt offline och OnlineRetrieval hade ingen effekt på den. En sak är värd att säga på förhand: PDFium-motorn själv gör ingen CMS-verifiering alls, så varje regel nedan bor i komponentens PAdES-lager och dess OpenSSL-bindning, där du kan läsa den

I vilken ordning verifierar, hämtar och kontrollerar OpenSSL-backend?

Integritet först, sedan tilltro, sedan spärrning, och nätverket röras bara mellan stegen som behöver det. VerifyCmsWithSsl kontrollerar CMS-signaturen och signerade attribut (RFC 5652) med kedjeutvärdering undertryckt, och om det misslyckas returnerar den omedelbart, innan en hämtningssession ens finns, så att ett dokument med söndriga byte inte utlöser någon utgående begäran. Bara om kedjan sedan faller och OnlineRetrieval är på följer den AIA-länkar och verifierar igen. CRL-distributionspunkter hämtas bara när kedjan är betrodd, för en CRL som hänger på en obetrodd väg bevisar ingenting. De tre domen hålls isär genomgående: en giltig signatur med en ofullständig kedja rapporteras fortfarande som en giltig signatur

Ordningen i VerifyCmsWithSsl i PDFium Components OpenSSL-backend: CMS-signaturkontrollen körs med kedjeutvärdering undertryckt, så att söndriga byte aldrig rör nätverket; RetrieveIntermediates följer AIA caIssuers-URL:er bara efter ett kedjefel med OnlineRetrieval påslagen, och RetrieveCrls hämtar distributionspunkts-CRL:er till ett separat lager när kedjan är betrodd
Integritet, sedan tilltro, sedan spärrning: nätverket röras bara mellan stegen som behöver det, och en CRL som hänger på en obetrodd väg bevisar ingenting
uses
  PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;

var
  Pdf: TPdf;
  Probe: TPdfCmsVerifyOptions;
  Diags: TPdfSslVerifyDiagnostics;
  Trust: TPadesTrustValidationOptions;
  Verdict: TPadesValidationResult;
  I: Integer;
begin
  ConfigureSslTrustAnchors(LoadCorporateRoots);   // DER; den enda extra tilltron
  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 som default
  Trust.NetworkPolicy := ptnpOnline;
  Trust.CheckRevocation := True;
  Trust.UrlRetrievalTimeoutMs := 10000;           // per verifieringsanrop

  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;

Varför läggs nerladdade certifikat aldrig i trust-lagret?

För att URL:erna kommer från certifikatet som valideras, och signeraren valde dem. authorityInfoAccess caIssuers-posten (RFC 5280 §4.2.2.1) är en ledtråd om var utfärdaren bor, ingenting mer. Om vad som helst som svarar på den URL:n hamnade i trust-anchor-lagret, kunde vem som helst signera med en självskapad nyckel, peka AIA mot sin egen server och få grönt dom. RetrieveIntermediates skickar därför varje tolkat certifikat till CMS_add1_cert, som placerar det i den här CMS-strukturens obetrodda mängd, och OpenSSL måste fortfarande bygga en väg från det till ett ankare du konfigurerat eller som systemlagret redan har. Det finns också ett tystare skäl: certifikatargumentet hos CMS_verify är ingen drop-in-ersättning för certifikaten inbäddade i CMS:en, så att lägga till i CMS:en själv är den pålitliga vägen

Hämtningsloopen är medvetet smal. RetrieveIntermediates kör högst 4 varv, varav varje samlar caIssuers-URL:er från varje certifikat som nu finns i CMS:en, och stannar så snart ett varv inte lägger till något eller tidsbudgeten är förbrukad. Ett svar måste avkodas med d2i_X509 som ett enda DER-certifikat som konsumerar hela kroppen; eftersläpande byte avvisas, och ett PKCS#7-certs-paket serverat från en .p7c-URL hoppas över i stället för att packas upp. OCSP-åtkomstmetoden i samma AIA-utökning ignoreras, eftersom denna backend inte talar OCSP. På spärrningssidan läser RetrieveCrls bara fullName-URI:erna i varje DistributionPoint (RFC 5280 §4.2.1.13) ur CMS-certifikaten och de konfigurerade ankarna, och de nerladdade CRL:erna går till ett andra, oberoende X509_STORE med fullkedje-CRL-kontroll, så att en saknad eller inaktuell CRL ändrar RevocationStatus utan att någonsin röra TrustStatus

// Sammandraget ur VerifyCmsWithSsl (FPdfCryptoSsl.pas); BIO-uppsättning utelämnad.
// Varje _CMS_verify-anrop får en färsk content-BIO
if _CMS_verify(Cms, nil, nil, Bio, nil,
  CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
  Exit;                                   // söndrig signatur: ingen nätverk alls
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, endast untrusted
  // verifiera kedjan igen mot samma anchor-lager
end;

if Options.CheckRevocation and (FetchSession <> nil) and
   (Result.TrustStatus = pcvsValid) then
  RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// andra, separata lagret: konfigurerade CRL:er plus hämtade
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);

Vad kostar ett verifieringsanrop som mest?

Ett fast tak, drivet av en enda TPdfCryptoFetchSession som AIA- och CRL-stegen i ett enda verifieringsanrop delar. Gränserna är konstanter i FPdfCryptoHttp, inte önskemål:

  • Tid: UrlRetrievalTimeoutMs, som är 15000 som default i både TPdfCmsVerifyOptions.Default och TPadesTrustValidationOptions.Default; en session skapad med 0 faller tillbaka på 30000, och klockan startar när signaturen väl klarat, och täcker varje senare begäran
  • Begäranden: högst 8 per session, räknade innan transporten försöks, så att en död värd ändå förbrukar en plats
  • Byte: 1 MiB per svar och 4 MiB totalt, med URL:er längre än 2048 tecken avvisade före någon anslutning
De hårda taken för en TPdfCryptoFetchSession som delas av AIA- och CRL-stegen i ett verifieringsanrop i PDFium Component: UrlRetrievalTimeoutMs är 15000 ms som default med noll-falltillbaka på 30000, högst 8 begäranden per session, 1 MiB per svar och 4 MiB totalt med misslyckade svar som ändå räknas, och URL:er över 2048 tecken avvisade
Gränserna är konstanter, inte önskemål: byte från ett misslyckat svar tömmer ändå budgeten, och eftersom varje signatur och tidsstämpel verifieras separat växer värsta fallet med antalet signaturer

Redovisningen är strängare än den ser ut först. Byte mottagna från ett misslyckat svar räknas ändå mot totalen, så att en server som svarar 404 med en stor sida inte kan tömma budgeten gratis. Läsningen som passerar gränsen per svar avbryter nerladdningen i stället för att skicka en avhuggen kropp vidare till ASN.1-tolken, och en HTTP 200 med tom kropp avvisas rakt av, eftersom AIA-vägen annars skulle indexera Data[0] i en tom array. Bara rena http://- och https://-URL:er släpper igenom, utan omdirigeringar, kakor, inloggningsuppgifter eller automatisk proxyupptäckt, medan HTTPS behåller sina normala certifikat- och värdnamnskontroller. URL-dedupliceringen är medvetet avgränsad till ett anrop: nästa validering måste kunna se en färskpublicerad CRL. Budgeten är också per anrop, inte per dokument, och ValidatePadesTrust verifierar varje signatur och varje tidsstämpeltoken separat, så att värsta fallet växer med antalet signaturer

Varför kan en tidsutgången WinHTTP-begäran fortfarande skriva i ditt minne?

För att återvända vid tidsutgång inte avbryter de callbacks som redan är i färd. Windowstransporten driver WinHTTP asynkront och väntar på en event med sessionens återstående tid, och när den väntan ger upp kan begäran fortfarande fullborda en läsning och signalera efteråt. Rikta den asynkrona läsningen mot en stackbuffert och den sena fullbordan skriver i en ram som då tillhör någon orelaterad funktion. Fixen är ägarskap, inte tajming: eventen och 16 KB-läsbufferten bor i en heap-post med två referenser, en hållen av anroparen och en som släpps bara av sista HANDLE_CLOSING-callbacken, så att den sida som slutar sist frigör minnet

Varför en tidsutgången WinHTTP-begäran fortfarande kan skriva i minnet: att återvända vid tidsutgång lämnar callbacks i färd, så PDFium Component riktar den asynkrona läsningen mot en heapallokerad THttpState-post vars 16 KB-buffert och två referenser, en hållen av anroparen och en släppt av sista HANDLE_CLOSING-callbacken, frigörs först när sista sidan slutar
En sen fullbordan kan avsluta sin läsning efter att din väntan gett upp; heap-ägarskap med två referenser betyder att den skrivningen landar i minne som fortfarande lever
type
  PHttpState = ^THttpState;
  THttpState = record
    References: LongInt;               // anroparen + sista HANDLE_CLOSING-callbacken
    Event: THandle;
    Status, Count: DWORD;
    Buffer: array[0..16383] of Byte;   // asynkrona läsningar landar här, aldrig på en stack
  end;

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

// I status-callbacken: HANDLE_CLOSING är sista notifiering WinHTTP
// skickar för begäran, så den släpper den andra referensen
if Status = HttpHandleClosing then
begin
  ReleaseState(State);
  Exit;
end;

Vad libcurl måste erbjuda på FPC Unix

En asynkron resolver och ett trådsäkert bygge, annars förblir onlinehämtning avstängd. På FPC Unix går transporten genom libcurl, samma beroende bakom libcurl-tidsstämpelbackend för icke-Windows-mål, och bindningen avvisar varje bibliotek vars funktionsmask saknar antingen CURL_VERSION_ASYNCHDNS eller CURL_VERSION_THREADSAFE. Skälet är att CURLOPT_NOSIGNAL, som ett bibliotek inne i någon annans process måste sätta, kombinerat med en synkron resolver betyder att en DNS-uppslag helt enkelt kan överleva tidsutgången. Den andra fällan är avstängningen: curl_global_cleanup väntar inte på asynkrona DNS-trådar, så när libcurl väl initierats förblir modulen mappad tills processen avslutas i stället för att låta en bakgrundstråd springa in i avlänkad kod. När något av kraven faller är SslCapabilities.OnlineRetrieval False och SslVerifyOptionsDiagnostics rapporterar psvdOnlineRetrievalIgnored i stället för att låtsas att nätverket konsulterats

Vad resultatet garanterar och inte garanterar

En giltig RevocationStatus från denna backend betyder att aktuella CRL:er som täcker hela kedjan hittades, konfigurerades eller nerladdades, och att ingen listade ett certifikat i den; ingenting mer. Det finns ingen OCSP, så en CA som publicerar spärrning bara genom OCSP lämnar resultatet utan stöd, och ett nätverksfel ser exakt ut som en CA som inte publicerar något. Notera också att psvdNoCrlsConfigured bara beskriver de CRL:er du konfigurerat, så med onlinehämtning är det en ledtråd, inte en prognos om undergång. När ett revisionsspår måste vara reproducerbart utan nätåtkomst, låt NetworkPolicy stå på sitt ptnpOffline-default: ingen hämtningssession skapas och backend öppnar aldrig en anslutning, vilket matchar offline-kontraktet på CryptoAPI-sidan som beskrivs i offline-spärrningskontroller av PDF-signaturer på Windows

Hämtningskoden, budgetarna och transportbindningarna skeppas som källkod med PDFium Delphi-komponenten, så att du kan bekräfta exakt vilka URL:er en validering får kontakta och hur mycket den får ladda ner innan du slår på ptnpOnline på en server som hanterar obetrodda dokument