PDFium VCL kan nu fuldende en PDF-signaturkæde og tjekke revokering over netværket på sin OpenSSL-backend: når OnlineRetrieval er slået til, installerer ConfigureSslCmsVerifier en verifier, der downloader manglende intermediate-certifikater fra AIA caIssuers-URL'er og CRL'er fra CRL distribution points, inden for et fast tids-, request- og bytebudget pr. verifikationskald. Downloadede certifikater er for altid kun kædemateriale. Trust kommer fortsat udelukkende fra system store og de anchors, du konfigurerer
Hullet, der lukkes her, viser sig, første gang du validerer virkelighedens PDF'er på en Linux-server. En stor andel af signere embedder kun deres eget leaf-certifikat i CMS'en, så OpenSSL kan ikke nå en root, TrustStatus kommer tilbage invalid, og revokering kører aldrig, fordi kæden aldrig blev trustworthy. Før v3.121.0 var OpenSSL-backend, der er beskrevet i verifying PDF signatures with OpenSSL in PDFium VCL, strengt offline, og OnlineRetrieval havde ingen effekt på den. Én ting er værd at slå fast forrest: PDFium-motoren selv laver slet ingen CMS-verificering, så alle reglerne nedenfor bor i komponentens PAdES-lag og dets OpenSSL-binding, hvor du kan læse dem
I hvilken rækkefølge verificerer, henter og tjekker OpenSSL-backend?
Integritet først, så trust, så revokering, og netværket røres kun mellem de trin, der har brug for det. VerifyCmsWithSsl tjekker CMS-signaturen og signed attributes (RFC 5652) med kæeevaluering undertrykt, og fejler det, returnerer den straks, før en fetch-session overhovedet findes, så et dokument med brudte bytes udløser ingen udgående request. Først hvis kæden derefter fejler, og OnlineRetrieval er slået til, følger den AIA-links og verificerer igen. CRL distribution points hentes kun, når kæden er trusted, for en CRL, der hænger på en utrustet vej, beviser ingenting. De tre kendelser forbliver adskilt hele vejen: en gyldig signatur med en ufuldstændig kæde rapporteres stadig som en gyldig signatur
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 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; // ptnpOffline som default
Trust.NetworkPolicy := ptnpOnline;
Trust.CheckRevocation := True;
Trust.UrlRetrievalTimeoutMs := 10000; // pr. verifikationskald
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 tilføjes downloadede certifikater aldrig til trust store?
Fordi URL'erne kommer fra det certifikat, der valideres, og signed valgte dem. authorityInfoAccess caIssuers-entryen (RFC 5280 §4.2.2.1) er et hint om, hvor issuer bor, ikke mere. Havde hvad som helst, der svarede på den URL, kunnet ryge i trust-anchor store, kunne enhver signere med en selvbygget nøgle, pege AIA mod sin egen server og få en grøn kendelse. RetrieveIntermediates giver derfor hvert parset certifikat til CMS_add1_cert, som placerer det i denne ene CMS-strukturs utruste sæt, og OpenSSL skal stadig bygge en vej fra det til en anchor, du har konfigureret, eller system store allerede holder. Der er også en mere stille grund: certifikatargumentet i CMS_verify er ikke en drop-in-erstatning for de certifikater, der er embeddet i CMS'en, så tilføjelse til CMS'en selv er den pålidelige vej
Hentningsløkken er bevidst snæver. RetrieveIntermediates kører højst 4 runder, der hver samler caIssuers-URL'er fra alle de certifikater, som nu er i CMS'en, og stopper, så snart en runde ikke tilføjer noget, eller tidsbudgettet er brugt. Et svar skal dekode med d2i_X509 som ét enkelt DER-certifikat, der opsluger hele body'en; afsluttende bytes afvises, og en PKCS#7 certs-only-pakke serveret fra en .p7c-URL springes over frem for at blive pakket ud. OCSP access-metoden i samme AIA-udvidelse ignoreres, da denne backend ikke taler OCSP. På revokeringssiden læser RetrieveCrls kun fullName-URI'erne i hver DistributionPoint (RFC 5280 §4.2.1.13) fra CMS-certifikaterne og de konfigurerede anchors, og de downloadede CRL'er ryger i en anden, uafhængig X509_STORE med full-chain CRL-tjek, så en manglende eller forældet CRL ændrer RevocationStatus uden nogensinde at røre TrustStatus
// Komprimeret fra VerifyCmsWithSsl (FPdfCryptoSsl.pas); BIO-opsætning udeladt.
// Hvert _CMS_verify-kald får en frisk content BIO
if _CMS_verify(Cms, nil, nil, Bio, nil,
CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
Exit; // brudt signatur: slet intet netværk
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, kun untrusted
// verificér kæden igen mod samme anchor store
end;
if Options.CheckRevocation and (FetchSession <> nil) and
(Result.TrustStatus = pcvsValid) then
RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// anden, separat store: konfigurerede CRL'er plus hentede
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);
Hvad koster ét verifikationskald højst?
Et fast loft, håndhævet af én TPdfCryptoFetchSession, som AIA- og CRL-trinene i et enkelt verifikationskald deler. Grænserne er konstanter i FPdfCryptoHttp, ikke anbefalinger:
- Tid:
UrlRetrievalTimeoutMs, som default er 15000 i bådeTPdfCmsVerifyOptions.DefaultogTPadesTrustValidationOptions.Default; en session oprettet med 0 falder tilbage til 30000, og uret starter, først når signaturen er bestået, og dækker alle senere requests - Requests: højst 8 pr. session, talt før transporten forsøges, så en død host bruger stadig en plads
- Bytes: 1 MiB pr. svar og 4 MiB i alt, med URL'er længere end 2048 tegn afvist før nogen forbindelse
Bogholderiet er strengere, end det ser ud. Bytes modtaget fra et fejlet svar tæller stadig med i totalen, så en server, der svarer 404 med en stor side, kan ikke tømme budgettet gratis. Læsningen, der krydser grænsen pr. svar, afbryder downloaden i stedet for at sende en trunkeret body videre til ASN.1-parseren, og en HTTP 200 med tom body afvises på stedet, for AIA-vejen ville ellers indeksere Data[0] i et tomt array. Kun almindelige http://- og https://-URL'er slipper igennem, uden redirects, cookies, credentials eller automatisk proxy-discovery, mens HTTPS beholder sine normale certifikat- og hostname-tjek. URL-deduplikering er bevidst afgrænset til ét kald: næste validering skal kunne se en nyligt publiceret CRL. Budgettet er også pr. kald, ikke pr. dokument, og ValidatePadesTrust verificerer hver signatur og hvert timestamp-token separat, så værst-tilfældet vokser med antallet af signaturer
Hvorfor kan et timed-out WinHTTP-request stadig skrive i din hukommelse?
Fordi at returnere ved timeout ikke annullerer de callbacks, der allerede er i luften. Windows-transporten driver WinHTTP asynkront og venter på et event med sessionens resterende tid, og når det vent opgiver, kan requestet stadig nå at fuldføre en læsning og signale bagefter. Peg den asynkrone læsning på en stack-buffer, og den sene fuldførelse skriver i en frame, der på det tidspunkt tilhører en helt anden funktion. Fixet er ownership, ikke timing: eventet og den 16 KB store læsebuffer bor i en heap-record med to referencer, én holdt af kalderen og én frigivet kun af den afsluttende HANDLE_CLOSING-callback, så uanset hvilken side slutter sidst, frigøres hukommelsen
type
PHttpState = ^THttpState;
THttpState = record
References: LongInt; // caller + afsluttende HANDLE_CLOSING-callback
Event: THandle;
Status, Count: DWORD;
Buffer: array[0..16383] of Byte; // async-reads lander her, 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 er den sidste notifikation, WinHTTP
// sender for requestet, så den dropper den anden reference
if Status = HttpHandleClosing then
begin
ReleaseState(State);
Exit;
end;
Hvad libcurl skal levere på FPC Unix
En asynkron resolver og en thread-safe build, ellers forbliver online retrieval slukket. På FPC Unix går transporten gennem libcurl, samme afhængighed som bag libcurl timestamp-backend til non-Windows-targets, og bindingen afviser ethvert bibliotek, hvis featuremaske mangler enten CURL_VERSION_ASYNCHDNS eller CURL_VERSION_THREADSAFE. Grunden er, at CURLOPT_NOSIGNAL, som et bibliotek inde i en andens proces må sætte, kombineret med en synkron resolver betyder, at et DNS-lookup simpelthen kan overleve timeouten. Den anden fælde er nedlukning: curl_global_cleanup venter ikke på asynkrone DNS-tråde, så når libcurl først er initialiseret, forbliver modulet mapped, til processen afsluttes, frem for at lade en baggrundstråd løbe ind i uindlæst kode. Fejler et af kravene, er SslCapabilities.OnlineRetrieval False, og SslVerifyOptionsDiagnostics rapporterer psvdOnlineRetrievalIgnored i stedet for at lade som om, netværket var konsulteret
Hvad resultatet garanterer og ikke garanterer
En gyldig RevocationStatus fra denne backend betyder, at der blev fundet, konfigureret eller downloadet aktuelle CRL'er, der dækker hele kæden, og at ingen af dem listede et certifikat i den; ikke mere. Der er ingen OCSP, så en CA, der kun publicerer revokering gennem OCSP, efterlader resultatet unsupported, og en netværksfejl ser præcis ud som en CA, der ikke publicerer noget. Bemærk også, at psvdNoCrlsConfigured kun beskriver de CRL'er, du har konfigureret, så med online retrieval er det et hint, ikke en forudsigelse af fiasko. Skal et audit trail kunne reproduceres uden netværksadgang, så lad NetworkPolicy stå på sin ptnpOffline-default: der oprettes ingen fetch-session, og backend åbner aldrig en forbindelse, hvilket matcher den offline-kontrakt på CryptoAPI-siden, der er beskrevet i offline PDF signature revocation checks on Windows
Hentningskoden, budgetterne og transportbindingerne skibes som kildekode med PDFium Delphi component, så du kan konstatere præcis, hvilke URL'er en validering må kontakte, og hvor meget den må downloade, før du slår ptnpOnline til på en server, der håndterer utruste dokumenter