PDFium VCL حالا میتواند روی بکاند OpenSSL خودش یک زنجیرهٔ امضای PDF را کامل و ابطال را از طریق شبکه بررسی کند: وقتی OnlineRetrieval فعال باشد، ConfigureSslCmsVerifier verifyکنندهای نصب میکند که گواهیهای میانی گمشده را از URLهای caIssuers مربوط به AIA و CRLها را از نقطههای توزیع CRL دانلود میکند؛ همهش درون بودجهٔ ثابت زمان، درخواست و بایت بهازای هر فراخوانی اعتبارسنجی. گواهیهای دانلودشده همیشه فقط مادهٔ زنجیرهاند. اعتماد همچنان منحصراً از انبار سیستم و anchorهایی که پیکربندی میکنید میآید
شکافی که این پر میکند اولین بار وقتی سراغش میروید که PDFهای دنیای واقعی را روی سرور لینوکسی اعتبارسنجی کنید خودش را نشان میدهد. سهم بزرگی از امضاکنندهها فقط گواهی leaf خودشان را در CMS جاسازی میکنند، پس OpenSSL نمیتواند به یک root برسد، TrustStatus نامعتبر برمیگردد و ابطال هرگز اجرا نمیشود چون زنجیره هرگز قابلاعتماد نشد. پیش از v3.121.0 بکاند OpenSSL که در verify کردن امضاهای PDF با OpenSSL در PDFium VCL توضیح داده شد کاملاً آفلاین بود و OnlineRetrieval رویش اثری نداشت. یک چیز را از اول بگوییم: خود موتور PDFium اصلاً verify مربوط به CMS را انجام نمیدهد، پس همهٔ قواعد زیر در لایهٔ PAdES کامپوننت و binding مربوط به OpenSSL آن زندگی میکنند، جایی که میتوانیدشان بخوانید
بکاند OpenSSL در چه ترتیبی verify میکند، میگیرد و بررسی میکند؟
اول یکپارچگی، بعد اعتماد، بعد ابطال، و شبکه فقط بین مراحلِ نیازدار لمس میشود. VerifyCmsWithSsl امضای CMS و ویژگیهای امضاشده (RFC 5652) را با ارزیابی زنجیرهٔ خاموش بررسی میکند و اگر آن شکست بخورد بلافاصله برمیگرداند، پیش از آنکه حتی نشست fetchای وجود داشته باشد، پس سندی با بایتهای شکسته هیچ درخواست خروجیای تحریک نمیکند. فقط اگر بعدش زنجیره افت کرد و OnlineRetrieval روشن بود، لینکهای AIA را دنبال و دوباره verify میکند. نقطههای توزیع CRL فقط وقتی زنجیره قابلاعتماد شد گرفته میشوند، چون CRLی که از مسیر غیرقابلاعتماد آویزان باشد هیچ چیزی را اثبات نمیکند. سه حکم سرتاسر جدا میمانند: امضای معتبر با زنجیرهٔ ناقص همچنان بهعنوان امضای معتبر گزارش میشود
uses
PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;
var
Pdf: TPdf;
Probe: TPdfCmsVerifyOptions;
Diags: TPdfSslVerifyDiagnostics;
Trust: TPadesTrustValidationOptions;
Verdict: TPadesValidationResult;
I: Integer;
begin
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER، تنها اعتماد اضافه
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;
چرا گواهیهای دانلودشده هرگز به انبار اعتماد اضافه نمیشوند؟
چون URLها از گواهیای میآیند که در حال اعتبارسنجیاش هستید و امضاکننده انتخابشان کرده. ورودی caIssuers در authorityInfoAccess (RFC 5280 §4.2.2.1) اشارهای است به اینکه issuer کجاست، همین و بس. اگر هر چیزی که به آن URL جواب میداد وارد انبار anchorهای اعتماد میشد، هر کسی میتوانست با کلیدی دستساز امضا کند، AIA را به سرور خودش اشاره دهد و حکم سبز بگیرد. پس RetrieveIntermediates هر گواهی parseشده را به CMS_add1_cert میدهد که آن را در مجموعهٔ غیرقابلاعتماد همین یک ساختار CMS میگذارد، و OpenSSL همچنان باید از آن مسیری به anchorای که پیکربندی کردهاید یا انبار سیستم از قبل دارد بسازد. دلیل آرامتری هم هست: آرگومان گواهی CMS_verify جانشین آمادهٔ گواهیهای جاسازیشده در CMS نیست، پس اضافه کردن به خود CMS مسیر مطمئن است
حلقهٔ بازیابی عمداً باریک است. RetrieveIntermediates حداکثر 4 دور اجرا میشود، هر دور URLهای caIssuers را از هر گواهی حالا موجود در CMS جمع میکند، و بهمحض آنکه دور چیزی اضافه نکرد یا بودجهٔ زمان تمام شد متوقف میشود. پاسخ باید با d2i_X509 بهصورت یک گواهی DER منفرد که کل بدنه را مصرف میکند decode شود؛ بایتهای دنباله رد میشوند و بستهٔ PKCS#7 فقط-گواهی که از URLای با پسوند .p7c سرو میشود skip میشود نه بازگشایی. روش دسترسی OCSP در همان افزونهٔ AIA نادیده گرفته میشود، چون این بکاند OCSP حرف نمیزند. سمت ابطال، RetrieveCrls فقط URIهای fullName هر DistributionPoint (RFC 5280 §4.2.1.13) را از گواهیهای CMS و anchorهای پیکربندیشده میخواند، و CRLهای دانلودشده به یک X509_STORE دوم و مستقل با بررسی 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، فقط غیرقابلاعتماد
// زنجیره را دوباره در برابر همان انبار anchor verify کن
end;
if Options.CheckRevocation and (FetchSession <> nil) and
(Result.TrustStatus = pcvsValid) then
RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// انبار دوم و جداست: CRLهای پیکربندیشده بهعلاوهٔ بازیابیشدهها
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);
یک فراخوانی اعتبارسنجی حداکثر چه چیزی خرج دارد؟
یک سقف ثابت، که با یک TPdfCryptoFetchSession اعمال میشود که مراحل AIA و CRL یک فراخوانی اعتبارسنجی واحد مشترکش دارند. محدودها ثابتهایی در FPdfCryptoHttp هستند، نه پیشنهاد:
- زمان:
UrlRetrievalTimeoutMsکه در هر دویTPdfCmsVerifyOptions.DefaultوTPadesTrustValidationOptions.Defaultپیشفرضش 15000 است؛ نشستی که با 0 ساخته شود به 30000 برمیگردد و ساعت از لحظهٔ قبول شدن امضا شروع میشود و همهٔ درخواستهای بعدی را پوشش میدهد - درخواستها: حداکثر 8 بهازای هر نشست، که پیش از تلاش برای transport شمرده میشوند، پس یک host مرده هم یک سهمیه را میبلعد
- بایتها: 1 MiB بهازای هر پاسخ و 4 MiB در کل، با URLهای بلندتر از 2048 کاراکتر که پیش از هر اتصال رد میشوند
حسابکردن از آنچه در نگاه اول به نظر میرسد سختگیرانهتر است. بایتهای دریافتی از پاسخ شکسته هم به کل شمرده میشوند، پس سروری که با صفحهٔ بزرگی 404 جواب دهد نمیتواند مجانی بودجه را بخورد. خواندنی که از حد هر پاسخ رد شود دانلود را قطع میکند بهجای آنکه بدنهٔ بریده را به پارسر ASN.1 بسپارد، و HTTP 200 با بدنهٔ خالی کلاً رد میشود، چون مسیر AIA وگرنه Data[0] یک آرایهٔ خالی را ایندکس میکرد. فقط URLهای سادهٔ http:// و https:// رد میشوند، بدون redirect، بدون کوکی، بدون اعتبارنامه و بدون کشف خودکار proxy، در حالی که HTTPS بررسیهای عادی گواهی و hostname خودش را نگه میدارد. حذف تکرار URL عمداً به یک فراخوانی محدود است: اعتبارسنجی بعدی باید بتواند CRL تازهمنتشرشده را ببیند. بودجه هم بهازای هر فراخوانی است نه هر سند، و ValidatePadesTrust هر امضا و هر توکن مهر زمان را جدا verify میکند، پس بدترین حالت با تعداد امضاها رشد میکند
چرا یک درخواست WinHTTP که timeout خورده هنوز میتواند در حافظهتان بنویسد؟
چون برگشتن روی timeout کالبکهای در پرواز را لغو نمیکند. transport ویندوزی WinHTTP را بهصورت ناهمگام میراند و روی رویدادی با زمان باقیماندهٔ نشست منتظر میماند، و وقتی آن انتظار تسلیم میشود، درخواست همچنان میتواند یک خواندن را تمام و بعداً سیگنال بدهد. خواندن ناهمگام را به بافری روی stack اشاره بدهید و همان تکمیل دیرهنگام در قابی مینویسد که تا آن موقع مال تابع بیربطی است. fix مالکیت است نه زمانبندی: رویداد و بافر خواندن 16 KB درون یک رکورد heap با دو ارجاع زندگی میکنند، یکی دست فراخواننده و یکی که فقط کالبک نهایی HANDLE_CLOSING رهایش میکند، پس هر طرفی که آخر تمام شود حافظه را آزاد میکند
type
PHttpState = ^THttpState;
THttpState = record
References: LongInt; // فراخواننده + کالبک نهایی HANDLE_CLOSING
Event: THandle;
Status, Count: DWORD;
Buffer: array[0..16383] of Byte; // خواندههای async اینجا فرود میآیند، هرگز روی stack
end;
procedure ReleaseState(State: PHttpState);
begin
if InterlockedDecrement(State.References) = 0 then
begin
CloseHandle(State.Event);
Dispose(State);
end;
end;
// در کالبک وضعیت: HANDLE_CLOSING آخرین اعلانی است که WinHTTP
// برای درخواست میفرستد، پس ارجاع دوم را رها میکند
if Status = HttpHandleClosing then
begin
ReleaseState(State);
Exit;
end;
libcurl روی FPC Unix چه چیزی باید فراهم کند
یک resolver ناهمگام و بیلدی thread-safe، وگرنه بازیابی آنلاین خاموش میماند. روی FPC Unix transport از libcurl میگذرد، همان وابستگی پشت بکاند مهر زمانی libcurl برای اهداف غیر ویندوزی، و binding هر کتابخانهای که ماسک ویژگیهایش نه CURL_VERSION_ASYNCHDNS داشته باشد نه CURL_VERSION_THREADSAFE را رد میکند. دلیلش این است که CURLOPT_NOSIGNAL — که کتابخانهای درون پروسهٔ دیگری باید ست کند — همراه با resolver همگام یعنی یک lookup مربوط به DNS میتواند بهسادگی از timeout عمرش بلندتر شود. دام دوم خاموشی است: curl_global_cleanup منتظر threadهای DNS ناهمگام نمیماند، پس وقتی یک بار libcurl مقداردهی اولیه شد ماژول تا خروج پروسه نگاشتشده میماند، بهجای آنکه بگذارد یک thread پسزمینه داخل کد unloadشده بدود. وقتی هر یک از این دو شرط برآورده نشود، SslCapabilities.OnlineRetrieval مقدار False میگیرد و SslVerifyOptionsDiagnostics بهجای ادعای مشورت با شبکه psvdOnlineRetrievalIgnored را گزارش میکند
نتیجه چه چیزی را تضمین میکند و چه چیزی را نه
یک RevocationStatus معتبر از این بکاند یعنی CRLهای جاری پوششدهندهٔ کل زنجیره پیدا، پیکربندی یا دانلود شدند و هیچکدام گواهیای در آن فهرست نکرده بود؛ همین و نه بیشتر. OCSPای در کار نیست، پس CAای که ابطال را فقط از طریق OCSP منتشر میکند نتیجه را پشتیبانینشده رها میکند و یک شکست شبکه دقیقاً شبیه CAای است که هیچ چیزی منتشر نمیکند. همچنین توجه کنید psvdNoCrlsConfigured فقط CRLهایی را توصیف میکند که شما پیکربندی کردهاید، پس با بازیابی آنلاین یک اشاره است نه پیشگویی شکست. وقتی مسیر حسابرسی باید بدون دسترسی شبکه بازتولیدپذیر باشد، NetworkPolicy را روی پیشفرض ptnpOffline رها کنید: هیچ نشست fetchای ساخته نمیشود و بکاند هرگز اتصالی باز نمیکند؛ همان قرارداد آفلاین سمت CryptoAPI که در بررسیهای آفلاین ابطال امضای PDF در ویندوز توضیح داده شده
کد بازیابی، بودجهها و bindingهای transport بهصورت سورس با کامپوننت دلفی PDFium عرضه میشوند، پس میتوانید پیش از فعال کردن ptnpOnline روی سروری که اسناد غیرقابلاعتماد را دست میچرخاند دقیقاً تأیید کنید چه URLهایی را یک اعتبارسنجی ممکن است صدا بزند و چقدر میتواند دانلود کند