PDFium VCL вече може да завърши веригата на PDF подпис и да провери revocation по мрежата на своя OpenSSL backend: когато OnlineRetrieval е включен, ConfigureSslCmsVerifier инсталира верификатор, който сваля липсващи междинни сертификати от AIA caIssuers URL-ите и CRL от CRL distribution points, вътре в фиксиран бюджет за време, заявки и байтове на всяко извикване за верификация. Свалените сертификати са винаги само материал за веригата. Trust продължава да идва изключително от системния store и котвите, които сте настроили
Дупката, която това затваря, се показва при първата валидация на истински PDF на Linux сървър. Голяма част от подписващите вграждат в CMS само собствения си leaf сертификат, така че OpenSSL не стига до root, TrustStatus се връща invalid, а revocation изобщо не тръгва, защото веригата никога не става доверена. Преди v3.121.0 OpenSSL backend-ът, описан в верификацията на PDF подписи с OpenSSL в PDFium VCL, беше изцяло офлайн и OnlineRetrieval нямаше никакъв ефект върху него. Едно нещо си заслужава да се каже отначало: самият PDFium engine изобщо не прави CMS верификация, така че всяко правило по-долу живее в PAdES слоя на компонента и в OpenSSL обвързката му, където можете да го прочетете
В какъв ред OpenSSL backend-ът верифицира, издирва и проверява?
Първо integrity, после trust, после revocation, а мрежата се пипа само между стъпките, които имат нужда от нея. VerifyCmsWithSsl проверява CMS подписа и signed attributes (RFC 5652) с потисната оценка на веригата и ако това провали, връща веднага — преди изобщо да съществува fetch сесия — така че документ с разбити байтове не задейства нито един изходящ запрос. Само ако веригата после се провали и OnlineRetrieval е включен, той следва AIA връзките и верифицира отново. CRL distribution points се теглят само щом веригата стане доверена, защото CRL, висящ на недоверен път, не доказва нищо. Трите присъди си остават отделни през цялото време: валиден подпис с непълна верига все пак се докладва като валиден подпис
uses
PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;
var
Pdf: TPdf;
Probe: TPdfCmsVerifyOptions;
Diags: TPdfSslVerifyDiagnostics;
Trust: TPadesTrustValidationOptions;
Verdict: TPadesValidationResult;
I: Integer;
begin
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER; единственото допълнително 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 по подразбиране
Trust.NetworkPolicy := ptnpOnline;
Trust.CheckRevocation := True;
Trust.UrlRetrievalTimeoutMs := 10000; // на извикване за верификация
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;
Защо свалените сертификати никога не попадат в trust store?
Защото URL-ите идват от сертификата, който се валидира, и ги е избрал подписващият. Записът authorityInfoAccess caIssuers (RFC 5280 §4.2.2.1) е намек къде живее issuer-ът, нищо повече. Ако каквото и да отговори на този URL влезе в trust-anchor store, всеки би могъл да подпише с направен от себе си ключ, да насочи AIA към свой сървър и да получи зелена присъда. Затова RetrieveIntermediates подава всеки парснат сертификат на CMS_add1_cert, който го поставя в недовереното множество на тази една CMS структура, а OpenSSL пак трябва да изгради път от него към котва, която сте настроили, или която системният store вече държи. Има и по-тиха причина: аргументът за сертификати на CMS_verify не е заместващ еквивалент на вградените в CMS сертификати, така че добавянето в самия CMS е надеждният път
Цикълът на издирване е нарочно тесен. RetrieveIntermediates върви най-много 4 рунда, всеки от които събира caIssuers URL-и от всеки сертификат, вече намиращ се в CMS, и спира щом рунд не добави нищо или бюджетът за време изтече. Отговор трябва да се декодира с d2i_X509 като един DER сертификат, изяждащ цялото тяло; байтове в края се отхвърлят, а PKCS#7 пакет само с сертификати, сервирани от .p7c URL, се прескача вместо да се разопакова. OCSP access методът в същото AIA разширение се игнорира, тъй като този backend не говори OCSP. От страната на revocation RetrieveCrls чете само fullName URI-ите на всеки DistributionPoint (RFC 5280 §4.2.1.13) от CMS сертификатите и настроените котви, а свалените CRL отиват във втори, независим X509_STORE с full-chain CRL проверка, така че липсващ или остарял CRL мени RevocationStatus, без изобщо да пипа TrustStatus
// Сгънато от VerifyCmsWithSsl (FPdfCryptoSsl.pas); BIO настройката е пропусната.
// Всяко _CMS_verify извикване получава свеж content BIO
if _CMS_verify(Cms, nil, nil, Bio, nil,
CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
Exit; // счупен подпис: никаква мрежа изобщо
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, само untrusted
// верифицирай веригата пак срещу същия anchor store
end;
if Options.CheckRevocation and (FetchSession <> nil) and
(Result.TrustStatus = pcvsValid) then
RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// втори, отделен store: настроените CRL плюс свалените
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);
Какво струва едно извикване за верификация най-много?
Фиксиран таван, наложен от един TPdfCryptoFetchSession, който AIA и CRL стъпките на едно извикване за верификация споделят. Лимитите са константи в FPdfCryptoHttp, не пожелания:
- Време:
UrlRetrievalTimeoutMs, което по подразбиране е 15000 и вTPdfCmsVerifyOptions.Default, и вTPadesTrustValidationOptions.Default; сесия, създадена с 0, пада обратно на 30000, а часовникът тръгва щом подписът мине и покрива всеки по-късен запрос - Заявки: най-много 8 на сесия, броени преди опита за транспорт, така че мъртъв хост пак изяжда слот
- Байтове: 1 MiB на отговор и 4 MiB общо, а URL по-дълги от 2048 символа се отказват преди каквото и да е свързване
Сметките са по-строги, отколкото изглеждат на пръв поглед. Байтовете, получени от провален отговор, пак се броят в общата сума, така че сървър, отговарящ на 404 с голяма страница, не може да изтощи бюджета безплатно. Четенето, което прекоси лимита на отговор, прекъсва свалянето вместо да предаде отрязано тяло на ASN.1 parser-а, а HTTP 200 с празно тяло се отхвърля изцяло, защото AIA пътят иначе би индексирал Data[0] на празен масив. Минават само чисти http:// и https:// URL-и, без redirects, cookies, credentials или автоматично откриване на proxy, докато HTTPS си пази нормалните проверки за сертификат и hostname. Дедупликацията на URL е нарочно ограничена до едно извикване: следващата валидация трябва да може да види публикуван току-що CRL. Бюджетът е също на извикване, не на документ, а ValidatePadesTrust верифицира всеки подпис и всеки timestamp token поотделно, така че най-лошият случай расте с броя на подписите
Защо WinHTTP запрос, изтекъл по време, все още може да пише в паметта ви?
Защото връщането при изтекло време не отменя callback-ите, вече в полет. Windows транспортът задвижва WinHTTP асинхронно и чака на event с оставащото време на сесията, а когато това чакане се откаже, запросът пак може да завърши четене и да сигнализира после. Насочите ли асинхронното четене към стеков буфер, това късно завършване пише във frame, който дотогава принадлежи на някаква съвсем друга функция. Fix-ът е собственост, не тайминг: event-ът и 16 KB буферът за четене живеят в heap запис с две референции — една държана от извикващия и една освободена само от финалния callback HANDLE_CLOSING — така че коя страна свърши последна, тя освобождава паметта
type
PHttpState = ^THttpState;
THttpState = record
References: LongInt; // извикващ + финален HANDLE_CLOSING callback
Event: THandle;
Status, Count: DWORD;
Buffer: array[0..16383] of Byte; // асинхронните четения кацат тук, никога на стек
end;
procedure ReleaseState(State: PHttpState);
begin
if InterlockedDecrement(State.References) = 0 then
begin
CloseHandle(State.Event);
Dispose(State);
end;
end;
// В status callback-а: HANDLE_CLOSING е последното уведомление, което WinHTTP
// изпраща за запроса, така че то пуска втората референция
if Status = HttpHandleClosing then
begin
ReleaseState(State);
Exit;
end;
Какво трябва да даде libcurl на FPC Unix
Асинхронен resolver и thread-safe build, или online издирването остава изключено. На FPC Unix транспортът минава през libcurl — същата зависимост зад libcurl timestamp backend-а за не-Windows цели — а обвързката отказва всяка библиотека, чиято feature маска е без CURL_VERSION_ASYNCHDNS или CURL_VERSION_THREADSAFE. Причината е, че CURLOPT_NOSIGNAL, което библиотека в чужд процес задължително вдига, в комбинация със синхронен resolver значи, че DNS lookup просто може да надживее timeout-а. Вторият капан е shutdown: curl_global_cleanup не чака асинхронните DNS нишки, така че щом libcurl бъде инициализиран, модулът остава мапнат до края на процеса, вместо да позволи background нишка да влезе в разтоварен код. Когато което и да е изискване се провали, SslCapabilities.OnlineRetrieval е False, а SslVerifyOptionsDiagnostics докладва psvdOnlineRetrievalIgnored, вместо да се преструва, че мрежата е била питана
Какво резултатът гарантира и какво не
Валиден RevocationStatus от този backend значи, че са намерени, настроени или свалени текущи CRL, покриващи цялата верига, и нито един не изброява сертификат от нея; нищо повече. Няма OCSP, така че CA, публикуващ revocation само през OCSP, оставя резултата unsupported, а мрежов провал изглежда точно като CA, който не публикува нищо. Обърнете внимание също, че psvdNoCrlsConfigured описва само CRL, които вие сте настроили, така че при online издирване той е намек, не прогноза за провал. Когато audit trail трябва да е възпроизводим без мрежов достъп, оставете NetworkPolicy на default-а му ptnpOffline: не се създава fetch сесия и backend-ът никога не отваря връзка — същият офлайн договор като от страната на CryptoAPI, описан в офлайн проверките за revocation на PDF подписи в Windows
Кодът за издирване, бюджетите и транспортните обвързки се доставят като source с PDFium Delphi компонента, така че можете да проверите точно кои URL може да контактува една валидация и колко може да свали, преди да включите ptnpOnline на сървър, обработващ недоверени документи