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
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ådeTPdfCmsVerifyOptions.DefaultochTPadesTrustValidationOptions.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
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
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