技術記事

HotPDFでDelphiからポスト量子とEdDSAのPDF署名を行う

HotPDFは読み込んだPDFドキュメントの中のML-DSA-44、ML-DSA-65、ML-DSA-87、Ed25519、Ed448のCMS署名を検証し、プラグイン可能なプロバイダ経由で署名を行うため、秘密鍵がDelphiプロセスの中に置かれる必要はない。後半の部分こそ、ほとんどのチームが最初に必要とするものだ。ハードウェアトークンも、リモート署名サービスも、国民電子身分証明書も、鍵を渡すことを拒否する。署名パイプラインがキーストアから切り離されるまで、どれも使えないままだ

その分割こそがTHPDFSignatureProviderの要点だ。HotPDFは自分が持つべき部分——CMSのパース、SignedDataの構築、/ByteRangeの配置——を手元に残し、持つことのできない1つの操作、つまり見ることを許されない鍵でダイジェストを署名に変える操作だけを委譲する。以下のすべては、その分割から導かれる

なぜ妥当なML-DSA署名が検証に失敗するのか

HotPDFが、そのための拡張を宣言していない読み込み済みドキュメント上のML-DSAを拒否するからだ。ML-DSA、つまりFIPS 204として標準化された格子署名方式であり、人が「ポスト量子PDF」と言うときの理由は、まだISO 32000-2への登録を持たない。これを持つPDFは基本規格が名指ししないアルゴリズムを使っており、名前のないアルゴリズムを黙って使うファイルは、誰にも再現できない評定を持つファイルだ

だからHotPDFは主張を明示的にする。EnsureMLDSAExtensionsは許可された範囲でドキュメントをPDF 2.0に引き上げ、Catalogに/Extensions /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >>を書き込む。読み取り側ではLoadedDocumentDeclaresMLDSAExtensionがその宣言が生き残ったかを返し、VerifyLoadedSignatureWithOptionsOptions.AllowMLDSAを尊重する前に同じ検査を適用する。宣言されていないドキュメントでフラグをセットしても、オンのままにはならない。このオプションはポリシーを緩めることはできても、構造上の要件を緩めることは決してない

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'contract-pq.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 720, 0, 'Supply agreement 2026-114');
    Pdf.EnsureMLDSAExtensions;   // declare before the signature is written
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

保存の後ではなく前に呼ぶこと。宣言は署名されたバイト範囲の一部であり、後からパッチされたCatalogは、署名済みファイルへの未署名の変更か、バリデータが変更として報告する第2リビジョンのどちらかになる

3つのアルゴリズムファミリ、1つの検証エントリポイント

3つのファミリはすべてVerifyLoadedSignatureWithOptionsを経由する。これは署名インデックス、ソースストリーム、THPDFCMSVerifyOptionsレコード、署名詳細用のoutパラメータを取る。レコードはちょうど3つのフィールドを持ち、それぞれがかつてはリビルドを必要とした問いに答える

SignatureProviderは、組み込みのプラットフォームプロバイダを自前のプロバイダで置き換える。OpenSSLLibraryPathはOpenSSL 3ライブラリを選択し、これがWindows CNGがどこでも提供するわけではないピュアモードのEd25519とEd448検証を供給する。AllowMLDSAは上記の拡張検査を前提として、格子アルゴリズムにオプトインする。認識された正確なアルゴリズムOIDはTHPDFSignatureInfo.SignatureAlgorithmOIDに戻るので、監査ログは要求されたものではなく検証されたものを記録できる

var
  Opts: THPDFCMSVerifyOptions;
  Info: THPDFSignatureInfo;
  Status: THPDFSignatureVerifyStatus;
  Src: TFileStream;
begin
  Opts := THPDFCMSVerifyOptions.Default;
  Opts.OpenSSLLibraryPath := 'C:\openssl3\libcrypto-3-x64.dll';
  Opts.AllowMLDSA := Pdf.LoadedDocumentDeclaresMLDSAExtension;
  Src := TFileStream.Create('contract-pq.pdf', fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignatureWithOptions(0, Src, Opts, Info);
    if Status = svValid then
      Memo1.Lines.Add('signed with OID ' + string(Info.SignatureAlgorithmOID));
  finally
    Src.Free;
  end;
end;

Ed25519とEd448は拡張宣言を必要としない。ISO 32000-2がすでに両者を認めているからだ。ただし両者を実装するプロバイダは必要で、ほとんどのWindowsデプロイでは、マシンにたまたまあるものではなく、自社が出荷して制御するライブラリにOpenSSLLibraryPathを向けることを意味する

署名プロバイダは実際に何を約束するのか

プロバイダが約束するのは1つのことだけだ。リクエストを与えられたらステータスを返し、署名時にはバイトを返す、ということ。THPDFSignatureProviderRequestはアルゴリズムとそのOID、ダイジェストOID、PSSソルト長、入力がメッセージかすでに計算済みのダイジェストか、入力自身、公開鍵または証明書、鍵識別子、操作識別子を運ぶ。このレコードの中にHotPDF固有のものは何もない。トークンドライバや署名サービスがすでに話している語彙だ

3つの実装がライブラリに同梱される。THPDFCallbackSignatureProviderは無名メソッドを包み、社内の既存署名ルーチンから動くPDF署名までの最短経路となる。THPDFRemoteSignatureProviderは転送コールバックを、再試行回数制限、キャンセルレジストリ、入力・署名サイズ上限とともに包み、ハングしたHSMがハングしたアプリケーションにならないようにする。THPDFPKCS11SignatureProviderは、呼び出し側が所有しすでに認証済みのPKCS#11セッションと秘密鍵ハンドルに対してRSA操作をシリアライズする。HotPDFは決してログインせず、PINを見ず、自分が開かなかったセッションを閉じない

var
  Provider: THPDFRemoteSignatureProvider;
begin
  Provider := THPDFRemoteSignatureProvider.Create(
    function(const Req: THPDFSignatureProviderRequest; Attempt: Integer;
      out Signature: TBytes): THPDFSignatureProviderStatus
    begin
      // POST Req.Input to the signing service; Req.KeyIdentifier selects the key
      if PostToSigningService(Req.KeyIdentifier, Req.Input, Signature) then
        Result := spsValid
      else
        Result := spsProviderError;
    end,
    3,          // RetryLimit
    1048576,    // MaxInputBytes
    65536);     // MaxSignatureBytes
  try
    // hand Provider to the signing call
  finally
    Provider.Free;
  end;
end;

なぜステータス列挙がブール値ではなく6つの値を持つのか

THPDFSignatureProviderStatusspsValidspsInvalidspsUnsupportedspsMalformedspsProviderErrorspsCancelledを区別し、これらを潰せば正しく行動する能力を失う。暗号学的に間違っている署名(spsInvalid)はセキュリティイベントだ。プロバイダが実装しないアルゴリズム(spsUnsupported)はデプロイのギャップだ。転送の失敗(spsProviderError)は再試行する価値があり、ユーザーがキャンセルしたトークンプロンプト(spsCancelled)はまったく再試行する価値がない

署名のルールは狭い。署名プロバイダはspsValidを空でない署名とともにのみ返す。検証プロバイダはspsValidspsInvalidを返し、他の4つは両方のパスで区別されたままになる。プロバイダを書くなら、認識しないものを何でもspsInvalidに写像したくなる衝動に抗うこと。それは、顧客の署名が偽造されているという報告に、行方不明のDLLを変換してしまう

署名がファイル内のどこに着地するのか

2つの関数がプロバイダを実際のPDFバイトにつなぐ。HPDFCMSBuildSignedDataWithProviderはドキュメントのSHA-256ダイジェストから分離型CMSを構築し、ワークフローがダイジェストをよそで計算する場合に正しいエントリポイントとなる。HPDFCMSSignPDFStreamWithProviderはPDFストリーム内の既存の署名プレースホルダに署名し、標準の/ByteRangeパイプラインを保存する。HotPDFがプレースホルダ自身を配置した場合に正しいエントリポイントだ

このパイプラインを保存することは見かけ以上に重要だ。/ByteRangeの慣習、つまり16進の署名ウィンドウをスキップする2つの範囲は、すべてのバリデータが最初にチェックするものであり、これを書き換えるプロバイダベースのパスは、暗号がどれだけ健全であってもPAdES適合を壊すだろう。HotPDFはレイアウトを組み込み署名パスと同一に保つので、PKCS#11トークン経由で署名されたドキュメントは、PFXファイルから署名されたものと同じ署名検証コードで検証される。アルゴリズムの選択の上に乗るプロファイルルールについては、DelphiでのPAdESベースライン署名の解説を参照のこと。また、このプロバイダモデルに先行するECDSA固有のエンコーディングの罠については、ECDSA CMS検証とP1363署名フォーマットのノートを参照のこと

ドキュメントを立ち往生させない移行順序

ポスト量子への準備はスイッチではなく、スケジュールの問題だ。今日ML-DSAを検証するデプロイ済みPDFビューアはほぼ存在せず、それだけで署名されたドキュメントは、読者から見れば検証不可能な署名を持つドキュメントになる。実地のアーカイブと接触して生き残る順序はこうだ。バリデータが判定する署名としてRSAかECDSAを保ち、ポリシーが量子耐性のある証拠を要求する場所で拡張宣言と第2のML-DSA署名を追加し、消費側システムが追いついたときだけ主署名を移行する

HotPDFが今日与えてくれるのは、同じコードから両方を書き、検証する能力だ。アルゴリズムはファイルにも検証結果にも正直に記録される。HotPDFはDelphiとC++Builder向けのネイティブVCL PDFコンポーネントであり、外部PDFランタイムを持たない。だから署名と検証のパスは、アプリケーションの隣ではなく内部に出荷される。完全な機能一覧とトライアルダウンロードはHotPDF Delphi PDFコンポーネントページを参照のこと