技術記事

HotPDF の PAdES LTV 証拠とシード値

署名したばかりの PDF は、B-B 署名であってそれ以上のものではありません。誰が署名したか、バイトが動いていないことは証明しますが、署名者証明書が署名時点で有効だったことの証明は運びません。そのため何年も後の検証者は、もはや存在しないかもしれない失効データを探し回らなければなりません。このギャップを閉じるには、OCSP レスポンスと CRL を文書レベルの Document Security Store へ書き込むことであり、HotPDF ではそれは 1 回の呼び出しです。PopulatePAdESLTVEvidence はロードされたすべての署名を歩き、証明書集合から失効要求を導出し、供給されたトランスポートを通じて実行し、取得した素材と CMS チェーンを DSS へ書き込みます。証拠が着地した署名の数を返し、文書に署名フィールドがまったくなければマイナス 1 を返します

使う前に理解しておく価値のある設計判断は、ライブラリがソケットを決して開かないことです。ネットワークから届くすべてのバイトは、あなたが書いたコールバックを通じて届きます。用心のための用心ではありません。長期検証を実際に要求する環境の内部で、この機能が機能する唯一の方法だからです

ライブラリが自前の HTTP を行わない理由

B-LT 署名を要求する場所は、ネットワークをライブラリに任せられない場所だからです。署名サービスは企業ルートを持つ認証プロキシの背後で動きます。エアギャップされた署名層はレスポンダーへの経路を持たず、キャッシュされた証拠を供給されなければなりません。監査体制は、すべての外向き要求が依存関係に埋もれるのではなくアプリケーションによって記録されることを要求します。そしてテストスイートは決定論的な応答を必要とし、ライブラリが勝手に外へダイヤルするなら不可能です

トランスポートは固定の形状を持つ単純な関数参照であり、ポリシーはあなたのもののままです。HotPDF は、取得すべきものを正確に記述する要求レコードを渡します。コンテンツタイプと応答サイズ上限を含みます。あなたはバイトとステータスを返します

HotPDF の PopulatePAdESLTVEvidence フロー。呼び出し側供給の FetchEvidence トランスポート、要求レコードのフィールド、署名ごとのステータス結果。
すべてのネットワークバイトはあなたの FetchEvidence コールバックを通過し、各署名は独自のステータスを得ます。1 つのタイムアウトがパス全体を中断することはありません
function FetchEvidence(const Request: THPDFSignatureEvidenceRequest;
  Attempt: Integer; CancellationToken: THPDFCancellationToken;
  out Response: TBytes; out RetryAfterMS: Cardinal;
  out ErrorMessage: UnicodeString): THPDFSignatureEvidenceTransportStatus;
begin
  RetryAfterMS := 0;
  try
    // Request.Kind は OCSP POST か CRL GET かを示す。
    // Request.ContentType と Request.Body は準備済みであり、
    // Request.MaxResponseBytes は守るべき上限である
    Response := HttpExchange(Request.URI, Request.ContentType,
      Request.Body, Request.MaxResponseBytes);
    Result := setsSucceeded;
  except
    on E: Exception do
    begin
      ErrorMessage := E.Message;
      // setsRetry はリトライポリシーにバックオフさせる。404 や
      // 不正な URL には setsPermanentFailure を使う
      Result := setsRetry;
    end;
  end;
end;

// ロードしたファイルの全署名に対する 1 呼び出しの B-B から B-LT への
// アップグレード
var
  Pdf: THotPDF;
  Upgraded: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('signed.pdf');
    Upgraded := Pdf.PopulatePAdESLTVEvidence(FetchEvidence,
      THPDFSignatureEvidenceRetryPolicy.Default);
    if Upgraded > 0 then
      // 追記専用の保存:既存の署名がカバーするバイトは
      // 一字一句そのまま保存される
      Pdf.SaveIncrementalUpdate('signed-lt.pdf');
  finally
    Pdf.Free;
  end;
end;

失敗は文書ごとではなく署名ごとです。ある署名者のためにタイムアウトしたレスポンダーは、その署名者の素材をスキップし、パスの残りをそのまま残します。バッチで望まれる振る舞いです。部分的な証拠は中断された実行に勝り、戻り値は実際に改善した署名の数を教えます

CMS が含め忘れたチェーン

失効確認には発行者証明書が必要ですが、驚くほど多くの署名スタックが中間証明書を CMS コンテナから省いています。回復の経路は Authority Information Access 拡張、アクセスメソッド 1.3.6.1.5.5.7.48.2 です。これは発行者証明書をダウンロードできる URL を広告します。HPDFFetchAIAIntermediates はそれらの URL を同じトランスポートで歩き、各応答から DER を解析し、CMS がすでに運んでいない証明書だけを返します。DER ハッシュをキーにするため、重複とループが回り続けることはありません

実際の認証局に対してこれが機能するかを決める細部が 2 つあります。1 つ目はエンコーディングです。CA のエンドポイントは、証明書を生の DER で提供することが PEM アーマーで提供することと同じくらいよくあり、それらを区別する信頼できるコンテンツタイプは存在しません。頑健な探査は、まずテキストとして、次に構造として行います。-----BEGIN CERTIFICATE----- マーカーを探し、あればアーマーを剥いで base64 をデコードし、どちらの経路でも結果の最初のバイトが $30、すなわち SEQUENCE の DER タグであることを確認します。2 つ目は深さです。取得した中間証明書は自らの発行者向けの AIA URL を広告することがあり、歩行は新しい候補をキューへ追加し、2 ホップか 3 ホップ足りないチェーンを補完します。これは上限を設けなければならず、MaxFetch パラメータはそのためのものです

HotPDF の AIA チェーン補完の図。caIssuers URL の取得、PEM と DER の探査、DER ハッシュによる重複排除、MaxFetch の深さ上限。
HPDFFetchAIAIntermediates は caIssuers の URL を同じトランスポートで歩き、PEM アーマーを探査し、キューを MaxFetch で上限管理します

署名シード値とは何か、そしてなぜ黙って失敗するのか

シード値は、文書作者が署名フィールドに付け、署名者にどの種類の署名が許容されるかを伝える制約です。どの SubFilter、どのダイジェストアルゴリズム、どの理由、どの最小 PDF バージョン、失効情報の埋め込みが必須かどうか。フィールド上の /SV 辞書に存在し、ISO 32000-1 §12.7.5.5 で定義されています。HotPDF は AttachPAdESSeedValue で書き込み、CheckLoadedSignatureSeedValue で確認します。この関数は、フィールドに制約がないか、存在するすべての制約が通るとき True を返し、False のときは最初に失敗した制約を出力パラメータ経由で名指します。エラーメッセージへそのまま入れられます

シード値を誤りやすくする仕掛けが、§12.7.5.5.3 に記述された /Ff フラグエントリです。セットされたビットはその制約を必須として印を付けます。不一致はエラーであり、署名者は拒否しなければなりません。クリアされたビットは同じ制約を選好として印を付けます。値は UI が提供すべきものをフィルタするだけで、それ以上ではありません。ここから 2 つの罠が続きます。1 つ目に、/Ff はウィジェット注釈上ではなく /SV 辞書の内側に存在するため、フィールドレベルの /Ff を読むコードは永遠に空の答えを得て、何も強制されていないと結論します。2 つ目に、ビットの割り当ては 1、2、4、8 の単純な並びではありません。HotPDF のライターは SubFilter に 2、MinVersion に 4、AddRevInfo に 32、DigestMethod に 64 を出力します。連続するビットを仮定するリーダーは、すべての制約をオプションとしてデコードし、重要な唯一のテスト以外のすべてのテストに通ります

HotPDF の PAdES 署名におけるシード値フラグビット表。Ff ビットの 2、4、32、64 と、必須と選好の制約処理の違いを示す。
/Ff エントリは /SV の内側に存在し、各ビット位置が、不一致が強い拒否か UI 上の選好かを決めます
var
  Violation: AnsiString;
begin
  // 署名しようとしているプロフィールが許されるかフィールドに問う
  if not Pdf.CheckLoadedSignatureSeedValue(0, 'ETSI.CAdES.detached',
       'SHA256', 'Approved for payment', 1, Violation) then
    raise Exception.Create('Signature field rejects this profile: ' +
      String(Violation));
  // 制約を満たした:署名パスを続行する
end;

元のデコードのバグを暴露したテストは、正のテストではありませんでした。強制された不一致は拒否されなければならないというアサーションでした。そしてこの種の欠陥を捕まえられるのはこの種のテストだけです。誤った辞書や誤ったビット位置を読むデコーダは、すべての入力に対して「違反なし」を生みます。意図的に 1 つを違反するまで、それは正しい振る舞いとまったく同じに見えます

LTV の階梯のどこに位置するか

階段は 4 段あり、各段はその下の段を必要とします。B-B は裸の署名です。B-T は信頼されたタイムスタンプを追加し、署名時刻を固定します。検証者がどの時点に対して失効を評価すべきか分かるようにするためです。B-LT は失効証拠を DSS へ追加します。これが PopulatePAdESLTVEvidence が自動化するものです。B-LTA は、前のものが弱まる前に更新される文書タイムスタンプを追加し、有効性を無期限に延ばします。HotPDF はこれを RenewPAdESLTATimestamp として公開します。新しいタイムスタンプをインクリメンタルリビジョンとして追記し、それ以前のすべての署名、タイムスタンプ、DSS エントリを無変更のまま保ちます

インクリメンタル更新モデルは、署名済み文書へ証拠を追加する唯一の正しい方法です。ファイルを書き直すと、既存の署名がカバーするバイト範囲が壊れるからです。リビジョン間で何が変わったか、それらの変更が署名が許す類のものかを考察する必要があるなら、その分析はDocMDP と FieldMDP のリビジョン分析で別途扱っています。証明書ソースやバイト順の落とし穴を含む署名パイプライン自体はPAdES 署名のウォークスルーに、検証側はロードした文書の署名検証にあります

順序について 1 つの実務上の警告があります。証拠は署名の直後に、できれば同じジョブで収集してください。証明書に応答できるレスポンダーは、証明書が現行の間はオンラインにあり、何年も後には姿を消します。したがって B-B のままパイプラインを出る文書は、二度とアップグレードできないかもしれません。HotPDF は Delphi と C++Builder 向けのネイティブ VCL コンポーネントとして動き、証拠パス全体はあなた自身のトランスポートを除きプロセス内です。対応するプロフィールは、HotPDF Delphi PDF component の製品ページに一覧があります