技術記事

DelphiでのPDF署名とPAdESレベルの検査

署名付きのPDFを受け取り、ビューアに誰が署名したか、いつ署名されたか、署名がファイル全体をカバーしているか、そして長期的なコンプライアンスにどの程度適合しているかを表示する必要があるとします。DelphiおよびLazarus用のPDFiumコンポーネントは、読み取り専用の呼び出しによってこれら4つの疑問すべてに答えます。FPDF_GetSignatureCountFPDFSignatureObj_*ファミリーが署名辞書を公開し、TPdf.ValidatePadesがPAdESの基準レベルを分類します。これはPDFiumを使用したPDF署名に関する3つの記事の最初の部分です。それに続く2つの記事では、B-B署名の作成と長期タイムスタンプの追加について説明します。ただし、あらかじめ前提としておきますが、ここで紹介するのはあくまで「検査」であり、署名が主張している内容を読み取ることと、その暗号検証を行ったり署名者を信頼するかどうかを決定することは、全く別の作業です

これが重要である理由は、ネイティブDLLの失敗には3つの異なるパターンがあり、オペレーティングシステムの生テキストではこれらすべてが混同されるためです。アーキテクチャが間違っているか、配置されたバイナリがそれを呼び出すPascalバインディングに対して古すぎるか、あるいは2つのスレッドが同時にそれをバインドしようと競合している可能性があります。それぞれに異なる修正方法があり、v2.11.0のロードライフサイクル作業で追加された診断の主な目的は、実際にどれが発生しているかを特定することにあります

なぜPDF署名辞書は単なるバイトの塊ではないのか

PDF署名は不透明な添付ファイルではなく、辞書データであり、その最も重要な2つのエントリは、ファイルのどの部分が実際に保護されているかを示します。ISO 32000-1 §12.8は、/ByteRange/Contentsエントリを使用して署名辞書を定義しています。/Contentsは、署名者の証明書、署名付き属性、および署名値自体を保持する暗号エンベロープである16進エンコードされたCMS SignedData構造(RFC 5652)を保持します。/ByteRangeは開発者が過小評価しがちな部分です。これは、/Contentsの16進文字列を除くファイル全体をカバーする2つのオフセット長スパンの配列です。その隙間こそが署名バイトが配置される場所であり、その両側の2つのスパンが、まさに署名が保証する対象となります

ByteRangeの設計は、増分保存を監査可能にするためのものです。署名者はまだ存在しない署名バイトをハッシュ化できないため、ファイルは/Contentsプレースホルダーの前後で分割され、それ以外のすべてが署名ハッシュに組み込まれます。ByteRangeがファイルの末尾に達していない署名は警告シグナルです。カバーされている範囲の後に、後の増分更新によって追加されたコンテンツは、リーダーの表示内容を変更するとしても署名を破損させません。そのため、本格的な検査において最初に確認すべきは、誰が署名したかではなく、署名が保証しているように見えるバイトを実際にカバーしているかどうかです

PDFiumの読み取り専用APIを使用した署名辞書の読み取り

PDFiumコンポーネ实现は、SignatureCountSignature[Index]レコードという2つの読み取り専用メンバーを介して署名辞書を表面化させます。内部的には、これらはFPDF_GetSignatureCountFPDF_GetSignatureObject、および/SubFilter/ByteRange/Contents/Reason、署名時刻用のFPDFSignatureObj_*アクセッサを呼び出します。ここで重要なのは「読み取り専用」という言葉です。PDFiumは署名を列挙して読み取ることができますが、署名を作成または書き込むためのAPIは持っていません。そのため、このシリーズの署名作成側はPDFiumではなく、ライブラリ自体によって実装されています

var
  Pdf: TPdf;
  i: Integer;
  Sig: TPdfSignature;
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Open('sales-june.xlsx');
    // Render the active sheet's used range straight to report.pdf.
    if Book.SaveAsPDF('report.pdf') = 1 then
      Writeln('PDF written');
  finally
    Book.Free;
  end;
end;

PAdES B-B、B-T、B-LT、B-LTAの違いとは

4つのPAdES基準レベルは、最小限有効な署名から、数十年間のアーカイブに耐えられるように構築された署名までの段階を形成しており、各レベルは下位のレベルを厳密に包含しています。ETSI EN 319 142-1はこれらをB-B、B-T、B-LT、およびB-LTAとして定義しています。B-B(Basic)は署名と必須の署名付き属性のみで構成されます。B-T(Timestamp)は、署名の上に信頼されたRFC 3161タイムスタンプを追加します。これにより、署名の瞬間は署名者の時計によって主張されるのではなく、タイムスタンプ局によって証明されます。B-LT(Long-Term)は、ファイル内部に検証資料(証明書チェーン、およびオプションのOCSPまたはCRL応答)を埋め込みます。これにより、発行インフラがなくなった何年も後でも署名を検証できます。B-LTA(Long-Term with Archive timestamp)は、これらの資料をドキュメントタイムスタンプでラップし、長期データ自体を保護し、基盤となる暗号が陳腐化する前に再タイムスタンプを設定するポイントを提供します

実用的な解釈としては時間軸に関係します。B-B署名は「誰かがこれに署名したか」に答えます。B-Tは「そしていつ、証明可能な形で」に答えます。B-LTは「証明書が期限切れになった後でも検証可能か」に答えます。B-LTAは「20年後でもその検証が有効か」に答えます。規制プロファイルは段階を選択します。多くの電子請求書やeIDASの文脈では少なくともB-Tが必要であり、アーカイブの義務付けではB-LTまたはB-LTAが求められます。ドキュメントを受け入れるか拒否するかの前に、実際にどの段階に達しているかを知ることが、検査ステップの主な目的です

TPdf.ValidatePadesを使用した基準レベルの検出

PDFiumコンポーネントは、レベルに関する疑問全体を1回の呼び出しに凝縮します。TPdf.ValidatePadesは、LevelフィールドがTPadesLevelplNoneplUnknownplB_BplB_TplB_LT、またはplB_LTA)であるTPadesValidationResultレコードを、問題のセット、署名数、およびドキュメントタイムスタンプ数とともに返します。レベルは段階的に推測されます。バリデータは最初にB-Bを確立し、署名タイムスタンプまたはドキュメントタイムスタンプが存在する場合はB-Tに昇格させ、カタログに証明書付きの/DSS/Extensions /ESICレベル1マーカーがある場合はB-LTに、ドキュメントタイムスタンプとESICレベル2マーカーの両方が存在する場合はB-LTAに昇格させます。結果を実用的にする2つのヘルパーがあります。IsCompliantは、レベルが少なくともB-Bに達し、問題セットが空の場合にのみTrueになります。また、IsCompliantAtを使用すると、plB_Tなどのポリシーの最低ラインを主張できます

var
  Pdf: TPdf;
  R: TPadesValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePades;
    case R.Level of
      plNone:    Writeln('No PAdES signature present');
      plUnknown: Writeln('Signature present but level undeterminable');
      plB_B:     Writeln('PAdES B-B   (basic)');
      plB_T:     Writeln('PAdES B-T   (trusted timestamp)');
      plB_LT:    Writeln('PAdES B-LT  (long-term material embedded)');
      plB_LTA:   Writeln('PAdES B-LTA (archive timestamp)');
    end;
    Writeln('Signatures   : ', R.SignatureCount);
    Writeln('DocTimeStamps: ', R.DocTimeStampCount);
    if R.IsCompliantAt(plB_T) then
      Writeln('Meets the B-T policy floor')
    else
      Writeln('Below the required B-T level');
  finally
    Pdf.Free;
  end;
end;

なぜadbe.pkcs7.sha1が禁止されたSubFilterなのか

SHA-1は安全ではなく、adbe.pkcs7.sha1ハンドラーがそれを組み込んでいるためです。このSubFilterは、文書をPKCS#7でラップする前にSHA-1で事前ハッシュ化しますが、SHA-1は長年にわたり衝突脆弱性が指摘されているため、EN 319 142-1の6.3項は基準署名においてこれを完全に禁止しています。ValidatePadesは、adbe.pkcs7.sha1またはadbe.x509.rsa_sha1を検出したときにppeiForbiddenSubFilterを発生させ、CMS自体がメッセージダイジェストとしてMD5またはSHA-1を使用している場合(6.2.1項)にppeiBadDigestAlgorithmを発生させます。これらは、2つの異なるレイヤーで同じ種類の弱点をキャッチする2つの独立したチェックです

問題セットには合計26個のメンバーがあり、最も頻繁に遭遇するものは構造とカバレッジに関連しています。ppeiByteRangeNotCoveringFile是、前述のカバレッジチェックです。ppeiForbiddenCertKeyは、署名辞書に/Certエントリが含まれている場合に発生します。PAdESではチェーンがCMSのSignedData.certificatesの内部に存在しなければならないため、これは禁止されています。ppeiMissingSigningCertificateppeiMissingContentType、およびppeiMissingMessageDigestは、必須の署名付き属性が欠落していることを示し、ppeiDetachedContentViolationは、署名されたコンテンツを切り離さずに誤って埋め込んでいる署名をキャッチします。セットを列挙することで、単なる拒否をログに記録可能な診断情報に変換できます

var
  R: TPadesValidationResult;
  Issue: TPadesValidationIssue;
begin
  R := Pdf.ValidatePades;
  if R.Issues <> [] then
    for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
      if Issue in R.Issues then
        Writeln('Issue: ',
          GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;

ValidatePadesがチェックしないもの

ValidatePadesは信頼性ではなく構造を検証します。これらを混同することは重大なエラーとなります。plB_LTAという結果は、ドキュメントにすべての必須属性、資料、タイムスタンプが正しい場所に配置された、正しく構成されたB-LTA署名が含まれていることを意味するだけであり、署名が暗号的に有効であること、証明書が信頼するルートに接続されていること、またはチェーン内の証明書が失効していないことを意味するものではありません。バリデータは意図的に暗号検証を実行しません。ByteRange上で署名を再計算せず、信頼チェーンを構築または評価せず、OCSPまたはCRLの失効状態を確認しません。この区分は意図的かつ有用です。構造検査は高速で完全に決定論的であり、キー、ネットワーク、およびプラットフォームの暗号機能を必要としないため、ValidatePadesはWindows、Linux、およびmacOSでファイルバイトに対する純粋なPascalとして同様に動作します。対照的に、信頼チェーンの検証はポリシー(どのルートを信頼するか、どのように失効情報を取得するか、タイムスタンプの許容度がどの程度厳しいか)と不可分であり、プラットフォームの証明書ストアに依存する後の段階に属します。そのため、合格したValidatePadesは署名が正しく形成されていることを示す必要なゲートとして扱い、依存する前に構造的に健全な署名を実際の暗号検証に渡すようにしてください

その構造的パスは適切な開始点であり、PDFiumコンポーネントによるPDFセキュリティリスクの監査におけるより広範な読み取り専用チェックや、印刷可能PDF/Xドキュメントの検証などのフォーマット準拠作業と自然に組み合わさります。一度署名を読み取って分類できたら、次のステップは署名を作成することです。このシリーズの2番目の記事では、PAdES B-BによるPDF署名について扱い、3番目の記事では、信頼されたタイムスタンプと長期的なB-LTおよびB-LTA署名へと拡張します。ここで紹介した読み取り専用の署名検査とValidatePades分類器は、Delphi、C++Builder、およびLazarus用のPDFiumコンポーネントの一部です