Teknisk artikkel

OpenSSL AIA- og CRL-henting for PDF-signaturer i Delphi

PDFium VCL kan nå fullføre en PDF-signaturkjede og sjekke tilbakekalling over nettverket på OpenSSL-backend-en sin: når OnlineRetrieval er slått på, installerer ConfigureSslCmsVerifier en verifikator som laster ned manglende mellomsertifikater fra AIA caIssuers-URL-ene og CRL-er fra CRL-distribusjonspunktene, innenfor et fast tids-, forespørsels- og bytebudsjett per verifiseringskall. Nedlastede sertifikater er aldri noe annet enn kjedemateriale. Tillit kommer fortsatt utelukkende fra systembutikken og ankerne du konfigurerer

Gapet dette lukker, viser seg første gang du validerer ekte PDF-er på en Linux-server. En stor andel av signatarer legger bare inn sitt eget leaf-sertifikat i CMS-en, så OpenSSL når aldri en rot, TrustStatus kommer tilbake ugyldig, og tilbakekalling kjører aldri fordi kjeden aldri ble tiltrodd. Før v3.121.0 var OpenSSL-backend-en beskrevet i å verifisere PDF-signaturer med OpenSSL i PDFium VCL strengt offline og OnlineRetrieval hadde ingen virkning på den. Én ting er verdt å si oppfront: selve PDFium-motoren gjør ingen CMS-verifisering i det hele tatt, så hver regel under bor i komponentens PAdES-lag og dets OpenSSL-binding, der du kan lese den

I hvilken rekkefølge verifiserer, henter og sjekker OpenSSL-backend-en?

Integritet først, så tillit, så tilbakekalling, og nettverket berøres bare mellom trinnene som trenger det. VerifyCmsWithSsl sjekker CMS-signaturen og signed attributes (RFC 5652) med kjedeevaluering undertrykt, og hvis det feiler, returnerer den umiddelbart, før en fetch-økt i det hele tatt finnes, så et dokument med ødelagte byte utløser ingen utgående forespørsel. Bare hvis kjeden så feiler og OnlineRetrieval er på, følger den AIA-lenker og verifiserer igjen. CRL-distribusjonspunkter hentes bare når kjeden er tiltrodd, for en CRL som henger på en utrustet vei beviser ingenting. De tre dommene holder seg atskilt hele veien: en gyldig signatur med en ufullstendig kjede rapporteres fortsatt som en gyldig signatur

Rekkefølgen av VerifyCmsWithSsl i PDFium Components OpenSSL-backend: CMS-signatursjekken kjører med kjedeevaluering undertrykt, så ødelagte byte berører aldri nettverket; RetrieveIntermediates følger AIA caIssuers-URL-er bare etter et kjedefeil med OnlineRetrieval på, og RetrieveCrls henter distribusjonspunkt-CRL-er inn i en egen butikk når kjeden er tiltrodd
Integritet, så tillit, så tilbakekalling: nettverket berøres bare mellom trinnene som trenger det, og en CRL som henger på en utrustet vei beviser ingenting
uses
  PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;

var
  Pdf: TPdf;
  Probe: TPdfCmsVerifyOptions;
  Diags: TPdfSslVerifyDiagnostics;
  Trust: TPadesTrustValidationOptions;
  Verdict: TPadesValidationResult;
  I: Integer;
begin
  ConfigureSslTrustAnchors(LoadCorporateRoots);   // DER; den eneste ekstra tilliten
  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 standard
  Trust.NetworkPolicy := ptnpOnline;
  Trust.CheckRevocation := True;
  Trust.UrlRetrievalTimeoutMs := 10000;           // per verifiseringskall

  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;

Hvorfor blir nedlastede sertifikater aldri lagt til i trust-butikken?

For URL-ene kommer fra sertifikatet som valideres, og signataren valgte dem. authorityInfoAccess caIssuers-oppføringen (RFC 5280 §4.2.2.1) er et hint om der utstederen bor, ingenting mer. Gikk hva enn som svarer på den URL-en inn i trust-anchor-butikken, kunne hvem som helst signere med en selvlaget nøkkel, peke AIA mot sin egen server og motta en grønn dom. RetrieveIntermediates sender derfor hvert parsede sertifikat til CMS_add1_cert, som plasserer det i untrusted-settet til denne ene CMS-strukturen, og OpenSSL må fortsatt bygge en vei fra det til et anker du har konfigurert eller som systembutikken allerede holder. Det finnes også en stillere grunn: sertifikatargumentet til CMS_verify er ikke en drop-in-erstatning for sertifikatene innebygd i CMS-en, så å legge til i selve CMS-en er den pålitelige veien

Hentingsløkken er bevisst smal. RetrieveIntermediates kjører høyst 4 runder, der hver samler caIssuers-URL-er fra hvert sertifikat som nå er i CMS-en, og stopper så snart en runde ikke legger til noe eller tidsbudsjettet er brukt opp. Et svar må dekode med d2i_X509 som et enkelt DER-sertifikat som konsumerer hele body-en; etterfølgende byte avvises, og en PKCS#7 certs-only-bundle servert fra en .p7c-URL hoppes over i stedet for å pakkes ut. OCSP-tilgangsmetoden i samme AIA-utvidelse ignoreres, siden denne backend-en ikke snakker OCSP. På tilbakekallingssiden leser RetrieveCrls bare fullName-URI-ene til hver DistributionPoint (RFC 5280 §4.2.1.13) fra CMS-sertifikatene og de konfigurerte ankerne, og de nedlastede CRL-ene går inn i en andre, uavhengig X509_STORE med fullkjede CRL-sjekking, så en manglende eller foreldet CRL endrer RevocationStatus uten noensinne å røre TrustStatus

// Komprimert fra VerifyCmsWithSsl (FPdfCryptoSsl.pas); BIO-oppsett utelatt.
// Hvert _CMS_verify-kall får en fersk content BIO
if _CMS_verify(Cms, nil, nil, Bio, nil,
  CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
  Exit;                                   // ødelagt signatur: intet nettverk i det hele tatt
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, bare untrusted
  // verifiser kjeden igjen mot samme ankerbutikk
end;

if Options.CheckRevocation and (FetchSession <> nil) and
   (Result.TrustStatus = pcvsValid) then
  RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// andre, egen butikk: konfigurerte CRL-er pluss hentede
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);

Hva koster ett verifiseringskall maksimalt?

Et fast tak, håndhevet av én TPdfCryptoFetchSession som AIA- og CRL-trinnene i ett enkelt verifiseringskall deler. Grensene er konstanter i FPdfCryptoHttp, ikke forslag:

  • Tid: UrlRetrievalTimeoutMs, som står til 15000 som standard i både TPdfCmsVerifyOptions.Default og TPadesTrustValidationOptions.Default; en økt laget med 0 faller tilbake på 30000, og klokken starter så snart signaturen har passert, og dekker enhver senere forespørsel
  • Forespørsler: høyst 8 per økt, talt før transporten forsøkes, så en død vert bruker fortsatt opp en plass
  • Byte: 1 MiB per respons og 4 MiB totalt, med URL-er lengre enn 2048 tegn avvist før noen tilkobling
De harde takene for én TPdfCryptoFetchSession som AIA- og CRL-trinnene i et verifiseringskall i PDFium Component deler: UrlRetrievalTimeoutMs står til 15000 ms som standard med null-fallback på 30000, høyst 8 forespørsler per økt, 1 MiB per respons og 4 MiB totalt med feilede responser som fortsatt teller, og URL-er over 2048 tegn avvist
Grensene er konstanter, ikke forslag: byte fra en feilet respons tapper fortsatt budsjettet, og etter som hver signatur og tidsstempel verifiseres hver for seg, vokser verste tilfellet med signaturantallet

Bokføringen er strengere enn den først ser ut. Byte mottatt fra en feilet respons teller fortsatt mot totalen, så en server som svarer 404 med en stor side, kan ikke tappe budsjettet gratis. Lesingen som krysser respons-grensen, avbryter nedlastingen i stedet for å sende en avkortet body videre til ASN.1-parseren, og en HTTP 200 med tom body avvises kontant, for AIA-veien ville ellers indeksert Data[0] i en tom array. Bare rene http:// og https://-URL-er slipper gjennom, uten redirecter, informasjonskapsler, legitimasjon eller automatisk proxy-oppdagelse, mens HTTPS beholder sine vanlige sertifikat- og vertsnavnsjekker. URL-deduplisering er med vilje avgrenset til ett kall: neste validering må kunne se en fersk publisert CRL. Budsjettet er også per kall, ikke per dokument, og ValidatePadesTrust verifiserer hver signatur og hvert tidsstempel-token hver for seg, så verste tilfellet vokser med antall signaturer

Hvorfor kan en tidsavbrutt WinHTTP-forespørsel fortsatt skrive inn i minnet ditt?

For å returnere ved tidsavbrudd kansellerer ikke callback-ene allerede i luften. Windows-transporten driver WinHTTP asynkront og venter på et event med øktens gjenværende tid, og når det ventet gir seg, kan forespørselen fortsatt fullføre en lesing og signalere etterpå. Peker du den asynkrone lesingen mot en stack-buffer, skriver den sene fullføringen inn i et ramme-område som da tilhører en helt annen funksjon. Fiksen er eierskap, ikke timing: event-en og 16 KB lesebufferen bor i en heap-record med to referanser, én holdt av kalleren og én sluppet bare av den siste HANDLE_CLOSING-callback-en, så uansett hvilken side som blir ferdig sist, frigjør den minnet

Hvorfor en tidsavbrutt WinHTTP-forespørsel fortsatt kan skrive inn i minnet: å returnere ved tidsavbrudd etterlater callbacks i luften, så PDFium Component peker den asynkrone lesingen mot en heap-allokert THttpState-record hvis 16 KB buffer og to referanser, én holdt av kalleren og én sluppet av den siste HANDLE_CLOSING-callback-en, frigjøres bare når siste side blir ferdig
En sen fullføring kan gjøre ferdig lesingen sin etter at ventingen din har gitt seg; heap-eierskap med to referanser betyr at skrivingen lander i minne som fortsatt lever
type
  PHttpState = ^THttpState;
  THttpState = record
    References: LongInt;               // kaller + siste HANDLE_CLOSING-callback
    Event: THandle;
    Status, Count: DWORD;
    Buffer: array[0..16383] of Byte;   // async-lesinger lander her, aldri 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 er siste varsling WinHTTP
// sender for forespørselen, så den slipper den andre referansen
if Status = HttpHandleClosing then
begin
  ReleaseState(State);
  Exit;
end;

Hva libcurl må tilby på FPC Unix

En asynkron resolver og et trådsikkert bygg, ellers forblir netthenting av. På FPC Unix går transporten gjennom libcurl, samme avhengighet bak libcurl timestamp-backend for ikke-Windows-mål, og bindingen nekter ethvert bibliotek hvis feature-maske mangler enten CURL_VERSION_ASYNCHDNS eller CURL_VERSION_THREADSAFE. Grunnen er at CURLOPT_NOSIGNAL, som et bibliotek inne i noens annen prosess må sette, kombinert med en synkron resolver betyr at et DNS-oppslag ganske enkelt kan overleve tidsavbruddet. Den andre fellen er nedstenging: curl_global_cleanup venter ikke på asynkrone DNS-tråder, så når libcurl først er initialisert, forblir modulen mappet til prosessen avslutter, i stedet for å la en bakgrunnstråd løpe inn i avlastet kode. Feiler ett av kravene, er SslCapabilities.OnlineRetrieval False og SslVerifyOptionsDiagnostics rapporterer psvdOnlineRetrievalIgnored i stedet for å late som nettverket ble konsultert

Hva resultatet garanterer og ikke garanterer

En gyldig RevocationStatus fra denne backend-en betyr at aktuelle CRL-er som dekker hele kjeden ble funnet, konfigurert eller lastet ned, og ingen listet opp et sertifikat i den; ingenting mer. Det finnes ingen OCSP, så en CA som publiserer tilbakekalling bare gjennom OCSP, etterlater resultatet ustøttet, og en nettverksfeil ser nøyaktig ut som en CA som ikke publiserer noe. Merk også at psvdNoCrlsConfigured beskriver bare CRL-ene du konfigurerte, så med netthenting er det et hint, ikke en spådom om feil. Når et revisjonsspor må være reproduserbart uten nettverkstilgang, la NetworkPolicy stå på ptnpOffline-standardverdien sin: ingen fetch-økt opprettes og backend-en åpner aldri en tilkobling, noe som matcher den offline-kontrakten på CryptoAPI-siden beskrevet i offline PDF-signatur tilbakekallingssjekker på Windows

Hentingskoden, budsjettene og transportbindingene følger med som kildekode med PDFium Delphi-komponenten, så du kan bekrefte nøyaktig hvilke URL-er en validering kan kontakte og hvor mye den kan laste ned, før du slår på ptnpOnline på en server som håndterer utrustede dokumenter