مقاله فنی

بازیابی AIA و CRL مربوط به OpenSSL برای امضاهای PDF در دلفی

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ی که از مسیر غیرقابل‌اعتماد آویزان باشد هیچ چیزی را اثبات نمی‌کند. سه حکم سرتاسر جدا می‌مانند: امضای معتبر با زنجیرهٔ ناقص همچنان به‌عنوان امضای معتبر گزارش می‌شود

ترتیب VerifyCmsWithSsl در بک‌اند OpenSSL مربوط به PDFium Component: بررسی امضای CMS با ارزیابی زنجیرهٔ خاموش اجرا می‌شود پس بایت‌های شکسته هرگز به شبکه دست نمی‌زنند؛ RetrieveIntermediates فقط بعد از شکست زنجیره با OnlineRetrieval فعال، URLهای caIssuers مربوط به AIA را دنبال می‌کند و RetrieveCrls وقتی زنجیره قابل‌اعتماد شد 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 کاراکتر که پیش از هر اتصال رد می‌شوند
سقف‌های سخت یک TPdfCryptoFetchSession که مراحل AIA و CRL یک فراخوانی اعتبارسنجی در PDFium Component مشترکش هستند: UrlRetrievalTimeoutMs پیش‌فرض 15000 میلی‌ثانیه با جایگزین صفر 30000، حداکثر 8 درخواست به‌ازای هر نشست، 1 MiB به‌ازای هر پاسخ و 4 MiB در کل که پاسخ‌های شکسته هم می‌شمارند، و URLهای بلندتر از 2048 کاراکتر رد می‌شوند
محدودها ثابت‌اند نه پیشنهاد: بایت‌های یک پاسخ شکسته هم بودجه را می‌خورند، و چون هر امضا و هر مهر زمان جدا verify می‌شود بدترین حالت با تعداد امضاها رشد می‌کند

حساب‌کردن از آن‌چه در نگاه اول به نظر می‌رسد سخت‌گیرانه‌تر است. بایت‌های دریافتی از پاسخ شکسته هم به کل شمرده می‌شوند، پس سروری که با صفحهٔ بزرگی 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 رهایش می‌کند، پس هر طرفی که آخر تمام شود حافظه را آزاد می‌کند

چرا یک درخواست WinHTTP که timeout خورده هنوز می‌تواند در حافظه بنویسد: برگشتن روی timeout کال‌بک‌ها را در پرواز رها می‌کند، پس PDFium Component خواندن ناهمگام را به رکورد THttpState گرفته‌شده از heap اشاره می‌دهد که بافر 16 KB و دو ارجاعش، یکی دست فراخواننده و یکی آزادشده توسط کال‌بک نهایی HANDLE_CLOSING، فقط وقتی آخرین طرف تمام شود آزاد می‌شوند
یک تکمیل دیرهنگام ممکن است بعد از تسلیم شدن انتظار شما خواندنش را تمام کند؛ مالکیت heap با دو ارجاع یعنی آن نوشتن در حافظه‌ای می‌نشیند که هنوز زنده است
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هایی را یک اعتبارسنجی ممکن است صدا بزند و چقدر می‌تواند دانلود کند