PDFium VCLはいまや、OpenSSLバックエンド上でPDF署名チェーンの補完と失効確認をネットワーク越しに行えます。OnlineRetrievalを有効にすると、ConfigureSslCmsVerifierが、AIAのcaIssuers URLから不足している中間証明書を、CRL配布ポイントからCRLを、検証呼び出しごとの固定の時間・リクエスト・バイト予算の中でダウンロードする検証器をインストールします。ダウンロードされた証明書はあくまでチェーン素材です。トラストは今も、システムストアと設定したアンカーからのみ来ます
この修正が塞ぐ隙間は、Linuxサーバーで現実世界のPDFを検証した最初の日に出てきます。署名者の多くはCMSに自分のリーフ証明書しか埋め込まず、OpenSSLはルートに届かず、TrustStatusはinvalidで戻り、チェーンが一度もトラスト可能にならないため失効確認は永遠に走りません。v3.121.0より前、PDFium VCLでOpenSSLを使ってPDF署名を検証するで述べたOpenSSLバックエンドは徹底してオフラインで、OnlineRetrievalは効き目がありませんでした。最初に言っておきたいことが1つあります。PDFiumエンジン自身はCMS検証をまったく行いません。だから以下の規則はすべて、コンポーネントのPAdESレイヤーとそのOpenSSLバインディングの中、つまり読める場所に住んでいます
OpenSSLバックエンドはどんな順で検証し、取得し、確認するのか
整合性が先、次にトラスト、最後に失効。ネットワークに触れるのは必要なステップの間だけです。VerifyCmsWithSslはチェーン評価を抑えた状態でCMS署名とsigned attributes(RFC 5652)を検査し、失敗したら即座に返ります。フェッチセッションが存在するよりも前なので、バイト列の壊れた文書は外向きのリクエストを一切起こしません。チェーンがその後失敗し、かつOnlineRetrievalが有効なときにだけ、AIAリンクをたどって再検証します。CRL配布ポイントを取得するのはチェーンがトラストされた後だけです。信頼できない経路にぶら下がるCRLは何も証明しないからです。3つの判定は最後まで独立しています。チェーンが不完全でも、署名が有効なら有効な署名として報告されます
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の出所は検証対象の証明書であり、選んだのは署名者だからです。authorityInfoAccessのcaIssuersエントリ(RFC 5280 §4.2.2.1)は発行者がどこに住むかの手がかりであって、それ以上のものではありません。そのURLに応答する何者かがトラストアンカーストアに入る仕様だったら、誰でも自作鍵で署名し、AIAを自分のサーバーに向け、グリーンの判定を受け取れます。RetrieveIntermediatesはそのため、パースした各証明書をCMS_add1_certへ渡します。これは当該CMS構造体の非トラスト集合へ置くものであり、OpenSSLはそこから、設定したアンカーかシステムストアが既に持つアンカーへの経路をやはり組まなければなりません。もっと静かな理由もあります。CMS_verifyの証明書引数は、CMSに埋め込まれた証明書の差し替え品にはならないので、CMSそのものへ追加するのが確実な経路です
取得ループは意図的に狭く作ってあります。RetrieveIntermediatesは最大4ラウンドで、各ラウンドはいまCMSにいる全証明書からcaIssuers URLを集め、1ラウンドで何も増えなかったか時間予算を使い切った時点で止まります。レスポンスはd2i_X509で、本文全体を消費する単一のDER証明書としてデコードできなければなりません。後続バイトは拒否し、.p7c URLから配られたPKCS#7の証明書のみバンドルは、展開せずスキップします。同じAIA拡張内のOCSPアクセスメソッドは無視します。このバックエンドはOCSPを話さないからです。失効側では、RetrieveCrlsはCMS証明書と設定済みアンカーから各DistributionPoint(RFC 5280 §4.2.1.13)のfullName URIだけを読み、ダウンロードしたCRLはフルチェーンCRL検査付きの、2つ目の独立したX509_STOREへ入ります。欠落や古いCRLはTrustStatusに一切触れずにRevocationStatusを変えるのです
// 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、非トラスト専用
// 同じアンカーストアに対してチェーンを再検証する
end;
if Options.CheckRevocation and (FetchSession <> nil) and
(Result.TrustStatus = pcvsValid) then
RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// 2つ目の独立ストア:設定済みCRL+取得CRL
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);
検証呼び出し1回の最大コスト
固定の天井です。1回の検証呼び出しのAIAステップとCRLステップが共有する、1つのTPdfCryptoFetchSessionが強制します。上限はFPdfCryptoHttpの定数であって、提案ではありません
- 時間:
UrlRetrievalTimeoutMs。TPdfCmsVerifyOptions.DefaultとTPadesTrustValidationOptions.Defaultのどちらでもデフォルトは15000です。0で作ったセッションは30000へフォールバックし、時計は署名の合格後から始まり、それ以降の全リクエストをカバーします - リクエスト:セッションあたり最大8。トランスポートを試みる前に数えるので、死んだホストでもスロットを1つ消費します
- バイト:レスポンスあたり1 MiB、合計4 MiB。2048文字超のURLは接続の前に拒否します
会計は一見より厳密です。失敗レスポンスから受け取ったバイトも合計に算入されるので、大きなページ付きの404を返すサーバーは予算をただで吸い取れません。レスポンスあたり上限を跨ぐ読み取りは、切り詰められた本文をASN.1パーサーへ渡す代わりにダウンロードを打ち切ります。空本文のHTTP 200は一蹴して拒否します。そうしないとAIA経路が空配列のData[0]を参照しかねないからです。通すのは素のhttp://とhttps://のURLだけで、リダイレクト、Cookie、資格情報、自動プロキシ発見はありません。一方HTTPSは通常の証明書とホスト名検査を保ちます。URLの重複排除は意図的に1呼びに限定します。次の検証が、新しく公開されたCRLを見えなければならないからです。予算も呼びごとであって文書ごとではなく、ValidatePadesTrustは各署名と各タイムスタンプトークンを個別に検証するので、最悪ケースは署名数とともに膨らみます
タイムアウトしたWinHTTPリクエストがメモリに書き続けられる理由
タイムアウトで返っても、飛行中のコールバックはキャンセルされないからです。WindowsトランスポートはWinHTTPを非同期で駆動し、セッションの残り時間だけイベントで待ちます。その待ちが諦めた後でも、リクエストは読み取りを完了して後から通知できます。非同期読み取りの先をスタックバッファに向けていれば、その遅れた完了は、その頃には無関係な関数のものになっているフレームへ書き込みます。修正は所有権であって、タイミングではありません。イベントと16 KBの読み取りバッファは、参照を2つ持つヒープレコードに住みます。1つは呼び出し元が持ち、もう1つは最後のHANDLE_CLOSINGコールバックだけが解放します。どちらが最後に終わっても、メモリを解放するのはその側です
type
PHttpState = ^THttpState;
THttpState = record
References: LongInt; // 呼び出し元+最後のHANDLE_CLOSINGコールバック
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;
// ステータスコールバック内:HANDLE_CLOSINGはWinHTTPがリクエストに
// 送る最後の通知なので、2つ目の参照を落とす
if Status = HttpHandleClosing then
begin
ReleaseState(State);
Exit;
end;
FPC Unixでlibcurlに求める条件
非同期リゾルバとスレッドセーフなビルドです。どちらかがなければオンライン取得はオフのままです。FPC Unixではトランスポートはlibcurlを通ります。非Windowsターゲット向けlibcurlタイムスタンプバックエンドの背後にいるのと同じ依存です。バインディングは、機能マスクにCURL_VERSION_ASYNCHDNSかCURL_VERSION_THREADSAFEのどちらでも欠くライブラリを拒否します。理由はこうです。他人のプロセス内で動くライブラリが設定せねばならないCURLOPT_NOSIGNALと、同期リゾルバの組み合わせは、DNSルックアップがタイムアウトを悠々と生き延びることを許します。2つ目の罠はシャットダウンです。curl_global_cleanupは非同期DNSスレッドを待たないので、libcurlを初期化した後は、バックグラウンドスレッドをアンロード済みコードへ走らせる代わりに、モジュールはプロセス終了までマップされたままになります。どちらかの要件が満たされないとき、SslCapabilities.OnlineRetrievalはFalseとなり、SslVerifyOptionsDiagnosticsは、ネットワークに問い合わせたふりをする代わりにpsvdOnlineRetrievalIgnoredを報告します
結果が保証することとしないこと
このバックエンドの有効なRevocationStatusが意味するのは、チェーン全体をカバーする現行のCRLが見つかり、設定またはダウンロードされ、どれも内の証明書を挙げていなかったこと、それだけです。OCSPはありません。失効情報をOCSPだけで公開するCAは結果が未サポートのままになり、ネットワーク障害は何も公開しないCAとまったく同じに見えます。もう1つ、psvdNoCrlsConfiguredは設定したCRLについてだけを述べるので、オンライン取得ありではヒントであって失敗の予報ではありません。監査証跡をネットワークなしで再現可能にしなければならないときは、NetworkPolicyをデフォルトのptnpOfflineに置いてください。フェッチセッションは作られず、バックエンドは接続を一切開きません。WindowsでのオフラインPDF署名失効確認で述べたCryptoAPI側のオフライン契約と一致します
取得コード、予算、トランスポートバインディングは、PDFium Delphiコンポーネントとともにソースで届きます。信用できない文書を扱うサーバーでptnpOnlineを有効にする前に、検証がどのURLに接触し得て、どれだけダウンロードし得るかを正確に確認できます