PDFium VCL sada može na svom OpenSSL backendu dovršiti lanac PDF potpisa i provjeriti revokaciju preko mreže: kad je OnlineRetrieval uključen, ConfigureSslCmsVerifier instalira verifiera koji iz AIA caIssuers URL-ova skida nedostajuće međucertifikate, a CRL-ove s CRL distribution points, unutar fiksnog budžeta vremena, zahtjeva i bajtova po pozivu vrednovanja. Skinuti certifikati su samo i isključivo materijal lanca. Trust i dalje dolazi isključivo iz sistemskog storea i sidara koje konfigurirate
Jaz koji se time zatvara pokazuje se prvog trenutka kad na Linux serveru validirate PDF-ove iz stvarnog svijeta. Velik udio potpisnika u CMS ugrađuje samo vlastiti leaf certifikat, pa OpenSSL ne može doći do korijena, TrustStatus vraća se nevaljan, a revokacija se uopće ne pokrene jer lanac nikad nije postao pouzdan. Prije v3.121.0 OpenSSL backend opisan u članku o vrednovanju PDF potpisa OpenSSL-om u PDFium VCL bio je strogo offline i OnlineRetrieval na njega nije djelovao. Jedno vrijedi reći unaprijed: sam PDFium engine ne radi nikakvu CMS verifikaciju, pa sva pravila dolje žive u PAdES sloju komponente i u njezinu OpenSSL bindingu, gdje ih možete i pročitati
U kojem redoslijedu OpenSSL backend verificira, dohvaća i provjerava?
Najprije integritet, zatim trust, pa revokacija, i mreža se dira samo među koracima kojima treba. VerifyCmsWithSsl provjerava CMS potpis i potpisane atribute (RFC 5652) s prigušenom evaluacijom lanca, i ako to padne, vraća se odmah, prije nego fetch sesija uopće postoji, pa dokument sa slomljenim bajtovima ne okida nikakav odlazni zahtjev. Samo ako lanac zatim padne a OnlineRetrieval je uključen, slijedi AIA poveznice i verificira ponovno. CRL distribution points dohvaćaju se tek kad je lanac pouzdan, jer CRL koja visi o nepouzdanoj putanji ne dokazuje ništa. Tri presude ostaju odvojene kroz cijeli tok: valjan potpis s nepotpunim lancem i dalje se javlja kao valjan potpis
uses
PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;
var
Pdf: TPdf;
Probe: TPdfCmsVerifyOptions;
Diags: TPdfSslVerifyDiagnostics;
Trust: TPadesTrustValidationOptions;
Verdict: TPadesValidationResult;
I: Integer;
begin
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER; jedini dodatni 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 po zadanim
Trust.NetworkPolicy := ptnpOnline;
Trust.CheckRevocation := True;
Trust.UrlRetrievalTimeoutMs := 10000; // po pozivu vrednovanja
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;
Zašto skinuti certifikati nikad ne ulaze u trust store?
Jer URL-ovi dolaze od certifikata koji se vrednuje, a odabrao ih je potpisnik. authorityInfoAccess caIssuers unos (RFC 5280 §4.2.2.1) je naznaka gdje stanuje izdavatelj, ništa više. Da bi ono što odgovori na taj URL završilo u trust-anchor storeu, svatko bi mogao potpisati vlastitim domaćim ključem, uperiti AIA u vlastiti server i primiti zelenu presudu. RetrieveIntermediates stoga svaki parsirani certifikat predaje CMS_add1_cert, koji ga smješta u nepouzdani skup ove jedne CMS strukture, a OpenSSL i dalje mora iz njega sagraditi putanju do sidra koje ste konfigurirali ili koje već drži sistemski store. Postoji i tiši razlog: argument certifikata u CMS_verify nije drop-in zamjena za certifikate ugrađene u CMS, pa je dodavanje u sam CMS pouzdan put
Dohvatna petlja namjerno je uska. RetrieveIntermediates radi najviše 4 krugova, svaki skuplja caIssuers URL-ove sa svih certifikata koji su tada u CMS-u, i staje čim krug ništa ne doda ili je vremenski budžet potrošen. Odgovor se mora dekodirati s d2i_X509 kao jedan DER certifikat koji troši cijelo tijelo; repni bajtovi se odbacuju, a PKCS#7 certs-only paket poslužen s .p7c URL-a preskače se umjesto da se raspakira. OCSP metoda pristupa u istom AIA proširenju ignorira se, jer ovaj backend ne govori OCSP. Na strani revokacije RetrieveCrls čita samo fullName URI-je svakog DistributionPointa (RFC 5280 §4.2.1.13) iz CMS certifikata i konfiguriranih sidara, a skinute CRL-ove ostavljaju u drugom, neovisnom X509_STORE s provjerom CRL-ova cijelog lanca, pa nedostajuća ili zastarjela CRL mijenja RevocationStatus a da nikad ne dira TrustStatus
// Sažeto iz VerifyCmsWithSsl (FPdfCryptoSsl.pas); BIO postava izostavljena.
// Svaki _CMS_verify poziv dobiva svježi content BIO
if _CMS_verify(Cms, nil, nil, Bio, nil,
CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
Exit; // slomljen potpis: bez mreže uopće
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, samo nepouzdano
// verificiraj lanac ponovno prema istom anchor storeu
end;
if Options.CheckRevocation and (FetchSession <> nil) and
(Result.TrustStatus = pcvsValid) then
RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// drugi, odvojeni store: konfigurirane CRL-ove plus dohvaćene
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);
Koliko jedan poziv vrednovanja košta najviše?
Fiksni strop, nametnut jednim TPdfCryptoFetchSession koji AIA i CRL koraci jednog poziva vrednovanja dijele. Granice su konstante u FPdfCryptoHttp, a ne preporuke:
- Vrijeme:
UrlRetrievalTimeoutMs, koji u obaTPdfCmsVerifyOptions.DefaultiTPadesTrustValidationOptions.Defaultima zadanu 15000; sesija stvorena s 0 pada na 30000, a sat kreće čim potpis prođe, pokrivajući svaki kasniji zahtjev - Zahtjevi: najviše 8 po sesiji, brojano prije nego se transport i pokuša, pa i mrtvi host potroši slot
- Bajtovi: 1 MiB po odgovoru i 4 MiB ukupno, s URL-ovima duljim od 2048 znakova odbijenima prije bilo kakve veze
Računovodstvo je strože nego što na prvi pogled izgleda. Bajtovi primljeni iz neuspjelog odgovora i dalje računaju u ukupno, pa server koji odgovori 404 s velikom stranicom ne može besplatno isušiti budžet. Čitanje koje preskoči granicu po odgovoru prekida preuzimanje umjesto da ASN.1 parseru proslijedi odrezano tijelo, a HTTP 200 s praznim tijelom odbacuje se odmah, jer bi AIA put inače indeksirao Data[0] praznog niza. Prolaze samo obični http:// i https:// URL-ovi, bez preusmjeravanja, kolačića, vjerodajnica ili automatskog otkrivanja proxyja, dok HTTPS zadržava svoje uobičajene provjere certifikata i imena hosta. Deduplikacija URL-ova namjerno je ograničena na jedan poziv: sljedeća validacija mora moći vidjeti svježe objavljenu CRL. Budžet je također po pozivu, ne po dokumentu, a ValidatePadesTrust verificira svaki potpis i svaki vremenski žig zasebno, pa najgori slučaj raste s brojem potpisa
Zašto WinHTTP zahtjev čiji je isteklo vrijeme može i dalje pisati u Vašu memoriju?
Jer povratak na istek vremena ne otkazuje callbackove koji su već u letu. Windows transport pokreće WinHTTP asinkrono i čeka na eventu s preostalim vremenom sesije, i kad taj wait odustane, zahtjev može i dalje dovršiti čitanje i signalizirati poslije. Uperite asinkrono čitanje u stack buffer i taj kasni dovršetak zapisuje u okvir koji tada pripada nekoj sasvim drugoj funkciji. Popravak je vlasništvo, ne tajming: event i 16 KB read buffer žive u heap zapisu s dvije reference, jednu drži pozivatelj a drugu otpušta samo konačni HANDLE_CLOSING callback, pa slobodu memorije dobije strana koja zadnja završi
type
PHttpState = ^THttpState;
THttpState = record
References: LongInt; // pozivatelj + konačni HANDLE_CLOSING callback
Event: THandle;
Status, Count: DWORD;
Buffer: array[0..16383] of Byte; // asinkrona čitanja slijeću ovdje, nikad na stack
end;
procedure ReleaseState(State: PHttpState);
begin
if InterlockedDecrement(State.References) = 0 then
begin
CloseHandle(State.Event);
Dispose(State);
end;
end;
// U status callbacku: HANDLE_CLOSING je zadnja obavijest koju WinHTTP
// šalje za zahtjev, pa ona otpušta drugu referencu
if Status = HttpHandleClosing then
begin
ReleaseState(State);
Exit;
end;
Što libcurl mora dati na FPC Unixu
Asinkroni resolver i thread-safe build, ili online dohvat ostaje isključen. Na FPC Unixu transport ide kroz libcurl, istu ovisnost iza libcurl timestamp backendea za ne-Windows ciljeve, i binding odbija svaku biblioteku čija feature maska nema i CURL_VERSION_ASYNCHDNS i CURL_VERSION_THREADSAFE. Razlog je što CURLOPT_NOSIGNAL, koji biblioteka u nečijem tuđem procesu mora postaviti, u kombinaciji sa sinkronim resolverom znači da DNS lookup može slobodno nadživjeti istek vremena. Druga je zamka gašenje: curl_global_cleanup ne čeka asinkrone DNS niti, pa jednom inicijaliziran libcurl ostaje mapiran dok proces ne izađe, umjesto da pozadinska nit zaleti se u istovareni kod. Kad bilo koji uvjet padne, SslCapabilities.OnlineRetrieval je False a SslVerifyOptionsDiagnostics javlja psvdOnlineRetrievalIgnored umjesto da se pretvara da je mreža bila konzultirana
Što rezultat jamči, a što ne
Valjan RevocationStatus s ovog backendea znači da su nađene, konfigurirane ili skinute tekuće CRL-ove koje pokrivaju cijeli lanac i da nijedna nije stavila certifikat iz njega na popis; ništa više. OCSP-a nema, pa CA koji revokaciju objavljuje samo kroz OCSP ostavlja rezultat nepodržanim, a mrežna greška izgleda točno kao CA koji ništa ne objavljuje. Imajte na umu i da psvdNoCrlsConfigured opisuje samo CRL-ove koje ste konfigurirali, pa je uz online dohvat to naznaka, a ne prognoza pada. Kad revizijski trag mora biti reproduciv bez pristupa mreži, ostavite NetworkPolicy na zadanom ptnpOffline: fetch sesija se ne stvara i backend nikad ne otvara vezu, što se poklapa s offline ugovorom na CryptoAPI strani opisanim u offline provjerama revokacije PDF potpisa na Windowsima
Dohvatni kod, budžeti i transportni bindingi isporučuju se kao izvor uz PDFium Delphi komponentu, pa možete potvrditi točno koje URL-ove validacija smije kontaktirati i koliko smije skinuti prije nego uključite ptnpOnline na serveru koji rukuje nepouzdanim dokumentima