本文へスキップ
ecdsa.com
← すべての記事

PDF 署名の仕組み:ByteRange、CMS、PAdES、LTV

PDF 署名は、署名そのものを収める穴を除いて、ファイルのすべてのバイトに署名します。紛らわしい判定の多くは、その穴の外側にあるもの、つまり後から追加されたリビジョン、信頼するルートの欠如、タイムスタンプの欠如に起因します。以下の例はすべて、pyHanko と OpenSSL で署名し、検証したものです。

要点:PDF 署名は、署名辞書の /Contents エントリに格納された CMS(PKCS #7)署名です。署名の対象は /ByteRange に列挙されたバイトで、これは署名時点のファイル全体から /Contents の値だけを除いたものです。検証ツールは 3 つの独立した問いに答えます。完全性:署名されたバイトは今も一致していますか。信頼:署名者の証明書チェーンは、検証ツールが信頼するルートまでつながっていますか。変更:署名後に何が追加され、それは許可されたものですか。Acrobat で黄色い疑問符が付く「有効だが信頼されていない」という状態は、ほぼ例外なく、バイトは改変されていないものの、署名者の CA が PDF リーダーの信頼リストに含まれていないことを意味します。PAdES(ETSI EN 319 142-1)は、B-B から B-LTA までのレベルを定義しています。RFC 3161 タイムスタンプ(B-T)は署名がいつ存在していたかを証明し、埋め込まれた検証データと文書タイムスタンプ(B-LT、B-LTA)によって、証明書の有効期限が切れた後も署名を検証できる状態が保たれます。

署名済み PDF の中身

以下のテストでは、pyHanko 0.38(CLI 0.6.0)を使い、小規模なテスト用 CA の P-256 鍵と、DigiCert の公開 TSA によるタイムスタンプで、1 ページの請求書に署名します。

pyhanko sign addsig(pyHanko 0.38.0、2026年10月20日)
$ pyhanko sign addsig --field Sig1 --use-pades --timestamp-url http://timestamp.digicert.com --reason Approved \
    pemder --key signer.key --cert signer.pem --chain root.pem --no-pass invoice.pdf invoice-signed.pdf

署名ツールはファイルを書き換えません。元の %%EOF の後ろに、増分更新として新しいオブジェクト、新しい相互参照セクション、新しいトレーラーを追加します。元の 627 バイトのファイルは、署名済みファイルの先頭に 1 バイトも変わらずに残っています。この更新で、署名フィールドと署名辞書が追加されます。次の例は同じコマンドを別に実行したときのものなので、数値は以下で調べるファイルとは異なります(/Contents の 16 進値は短縮しています)。

pyHanko が書き込んだ署名辞書(別の実行)
9 0 obj
<<
/Contents <30821CFC06092A864886F70D010702A0821CED3...(zeros)...0000000>
/ByteRange [0 1331 23601 1118]
/Type /Sig
/Filter /Adobe.PPKLite
/SubFilter /ETSI.CAdES.detached
/M (D\07220261020140517Z)
/Reason (Approved)
/Prop_Build <<
/App <<
/Name /pyHanko#20CLI
/REx (0\05638\0560)
>>
>>
>>
endobj

/M の値は、PDF の日付形式で書かれた D:20261020140517Z です。\072 はエスケープされたコロンです。/ByteRange と /Contents のサイズを読み取る小さなスクリプトを使うと、このページで以降に扱うファイルの数値は次のようになります。

invoice-signed.pdf の署名レイアウト
file size: 24869 bytes, revisions (%%EOF markers): 2
/ByteRange [0 1331 23751 1118]
   signed: bytes 0-1330 and 23751-24868; the 22420-byte gap is the /Contents value
   /Type /Sig  /SubFilter /ETSI.CAdES.detached
   /Contents: 11209 bytes reserved, 7472 bytes used by the CMS object
   range ends at end of file: True

各要素の関係は次のとおりです。

  • /ByteRange [a b c d]:2 つのバイト範囲を示します。a から始まる b バイトと、c から始まる d バイトです。その間の隙間は、区切り文字を含めた /Contents の 16 進文字列とぴったり一致します。署名時点のファイルのそれ以外の部分は、すべてハッシュの対象です。署名が入るまでファイルサイズは確定しないため、署名ツールはまずプレースホルダーを書き込み、/ByteRange を埋め、ハッシュを計算して署名し、最後に予約した領域に署名を書き込みます。
  • /Contents:DER エンコードされた CMS の SignedData を、16 進エンコードしてゼロでパディングした形で格納します。pyHanko は 11,209 バイトを予約し、そのうち 7,472 バイトを使いました。残りはパディングです。署名のサイズがわかる前に領域を予約しなければならないため、このパディングが生じます。
  • /SubFilter:形式を示します。ETSI.CAdES.detached は PAdES の形式で、adbe.pkcs7.detached は ISO 32000-1 の旧形式です。どちらも、ByteRange のバイトに対する分離形式の CMS 署名です。
  • /M:署名者が主張する署名時刻です。PAdES では時刻をここに記録します。ベースラインプロファイルでは、CMS の signing-time 属性はどのレベルでも「存在してはならない」と定められています。鍵を持っていれば誰でも任意の時刻を書き込めるため、これは主張にすぎません。

CMS オブジェクト自体はごく普通のもので、抽出したバイト範囲に対して OpenSSL で検証できます。

/Contents に対する openssl cms -verify(OpenSSL 3.5.7)
CMS Verification successful
      1 object: contentType (1.2.840.113549.1.9.3)
      1 object: id-smime-aa-signingCertificateV2 (1.2.840.113549.1.9.16.2.47)
      1 object: id-smime-aa-timeStampToken (1.2.840.113549.1.9.16.2.14)
      1 object: messageDigest (1.2.840.113549.1.9.4)

署名付き属性は、コンテンツタイプ、ByteRange のバイトの messageDigest、signingCertificateV2 です。最後の属性は署名者の証明書のハッシュ値で、同じ鍵を持つ別の証明書に差し替えられるのを防ぎます。非署名属性の timeStampToken は、署名値に対する RFC 3161 タイムスタンプです。PDF 署名検証ツールでは、任意の PDF について、各署名がどのバイトを対象としているかを含め、この内訳を確認できます。タイムスタンプインスペクターは、埋め込まれたトークンをデコードします。

検証ツールが答える 3 つの問い

pyHanko のレポートはこの 3 つを分けて示すので、よい教材になります。テスト用 CA のルートを信頼済みとして指定した場合の結果は次のとおりです。

pyhanko sign validate --pretty-print --trust root.pem invoice-signed.pdf(抜粋)
Trust anchor: "Organization: Example GmbH, Common Name: Example Signing Root CA"
The signer's certificate is trusted.

Integrity
---------
The signature is cryptographically sound.

Signing time
------------
Signing time as reported by signer: 2026-10-20T14:04:36+00:00

Signature timestamp token: 2026-10-20T14:04:37+00:00
The token is guaranteed to be newer than the signature.
This timestamp is backed by a time stamping authority.
The timestamp token is cryptographically sound.
TSA certificate subject: "Common Name: DigiCert SHA256 RSA4096 Timestamp Responder 2026 1; Organization: DigiCert, Inc.; Country: US"
The TSA certificate is trusted.

Modifications
-------------
The signature covers the entire file.

Bottom line
-----------
The signature is judged VALID.

有効だが信頼されていない

同じファイルを、テスト用のルートを指定せずに検証した結果です。

同じファイル、署名者のルートを信頼しない場合(抜粋)
Trust anchor: "No path to trust anchor found."
The signer's certificate is untrusted.

Integrity
---------
The signature is cryptographically sound.
...
Bottom line
-----------
The signature is judged INVALID.

完全性は保たれていますが、信頼が欠けています。Acrobat はこの状態を「Signature validity is UNKNOWN」と表示し、署名者の ID が不明であると注記します。署名済み PDF に関する苦情で最も多いのがこれですが、たいていの場合、文書自体に問題はありません。PDF リーダーは、それぞれ独自のリストに基づいて署名証明書を信頼します。Acrobat は Adobe Approved Trust List(AATL)と、オプションで EU のトラステッドリストを使用します。社内 CA の証明書、リーダーが認識していない国民 ID カードの証明書、自己署名証明書は、この状態になります。対処方法は、閲覧側(発行元の CA を信頼する、または EU のトラステッドリストを有効にする)か、署名側(受信者が信頼する CA の証明書で署名する)にあります。TSA 証明書には別の判定が出ている点にも注目してください。DigiCert の TSA はシステムの証明書ストアにあるルートまでチェーンがつながっているため、署名者が信頼されていなくても、タイムスタンプは信頼されます。

署名範囲内のバイトの変更

ファイル内の EUR 1,250.00 を直接 EUR 9,250.00 に書き換えた結果です。

金額 1 か所をその場で編集(抜粋)
Integrity
---------
The signature is cryptographically unsound.
...
Bottom line
-----------
The signature is judged INVALID.

これを回避する方法はありません。範囲内のすべてのバイトがハッシュの対象なので、ファイルを直接編集すれば、どこを変更しても署名は壊れます。

署名後に追加された変更

注目すべき攻撃は、署名されたバイトには手を触れず、新しいリビジョンを追加します。PDF は増分更新を認めており、フォームへの入力、注釈の追加、署名の追加では正当な手段です。テストでは、ページの内容を新しい金額に置き換えるリビジョンを追加します。

署名後の増分更新で内容を置き換え
file size: 26337 bytes, revisions (%%EOF markers): 3
/ByteRange [0 1331 23751 1118]
   range ends at end of file: False
...
The signature is cryptographically sound.
...
Modifications
-------------
The signature does not cover the entire file.
Some modifications may be illegitimate, and they appear to be incompatible with the current document modification policy.

Bottom line
-----------
The signature is judged INVALID.

署名されたリビジョンには手が加えられていないため、署名は暗号的には依然として正しいままです。しかし、読み手が目にするのは新しいリビジョンです。これを検出できるのは差分分析、つまり署名後に変更された内容を許可されている変更と照らし合わせる処理だけです。「署名が正しく、署名者が信頼されている」ことしか確認しないビューアーは、有効な署名の下に EUR 9,250 を表示します。PDF ビューアーに関する 2019年以降の研究では、まさにこうした欠陥が多くのアプリケーションで見つかっています(増分保存攻撃とシャドウ攻撃)。一方、2 つ目の署名の追加は許可された変更です。

最初の署名の後に 2 つ目の署名を追加:Sig1 の判定
Modifications
-------------
The signature does not cover the entire file.
All modifications relate to signing and form filling operations, and they appear to be compatible with the current document modification policy.

Bottom line
-----------
The signature is judged VALID.

作成者署名(certification signature)は、以降の変更に関する作成者のポリシーを DocMDP の権限として記録します(pyHanko では --certify、Acrobat では「証明」)。変更不可、フォーム入力のみ、フォーム入力と注釈のいずれかです。検証ツールは、差分分析の中でこのポリシーを適用します。

時刻:RFC 3161 タイムスタンプ

署名に使う証明書には有効期限があり、失効することもあります。署名がいつ行われたかを示す独立した証拠がなければ、検証ツールは現時点を基準に証明書を確認するしかありません。証明書の有効期限が切れたり、理由を問わず失効したりすると、その署名は、盗まれた鍵で昨日作られた署名と同じくらい疑わしく見えます。/M エントリは署名者が自由に決められるため、役に立ちません。

信頼できるタイムスタンプ局(TSA)が発行する RFC 3161 タイムスタンプが、この問題を解決します。TSA は、署名値のハッシュ値と自身の時刻を含むトークンに署名します。テストでは、DigiCert の TSA が UTC で 14時04分37秒のトークンを返しました。主張された署名時刻の 1 秒後です。これにより、証明書が有効だったその時点に署名が存在していたことが証明されます。これが PAdES のレベル B-T です。タイムスタンプは、独立した文書タイムスタンプとして追加することもできます。これは /Type /DocTimeStamp と /SubFilter /ETSI.RFC3161 を持つ署名辞書で、その時点の文書全体にタイムスタンプを付けます。

長期検証:B-LT と B-LTA

10 年後に検証するには、タイムスタンプだけでは足りません。CA の CRL や OCSP レスポンダーはなくなっているかもしれず、アルゴリズムの安全性が低下している可能性もあります。ETSI EN 319 142-1(V1.2.1、2024年1月)は、次のレベルを定義しています。

レベル追加するもの目的
B-B必須の署名付き属性を含む CMS 署名基本的な署名
B-T信頼できるタイムスタンプある時点で署名が存在していたことの証明
B-LTすべての検証データ(文書の DSS に格納した証明書、CRL、OCSP レスポンス)外部から何も取得しない検証
B-LTAその検証データを含む文書全体に対する文書タイムスタンプ(定期的に更新)証明書が期限切れになり、アルゴリズムが古くなっても、検証データとタイムスタンプの信頼性を維持

検証データはドキュメントセキュリティストア(文書カタログの /DSS)に格納され、署名後の増分更新で追加されます。テスト用 CA が提供する CRL を使って、同じ請求書に B-LTA で署名した結果です。

pyhanko sign addsig --use-pades-lta --with-validation-info(抜粋)
file size: 54674 bytes, revisions (%%EOF markers): 4
/ByteRange [0 1331 23745 1099]
   /Type /Sig  /SubFilter /ETSI.CAdES.detached
   range ends at end of file: False
/ByteRange [0 34471 52463 668]
   /Type /DocTimeStamp  /SubFilter /ETSI.RFC3161
   range ends at end of file: False
catalog /DSS: True
   /Certs present: True
   /CRLs present: True
   /OCSPs present: True
   /VRI present: True
...
Modifications
-------------
The signature does not cover the entire file.
All modifications relate to signature maintenance, and they appear to be compatible with the current document modification policy.

Bottom line
-----------
The signature is judged VALID.

リビジョンは 4 つあります。元のファイル、署名、検証データを含む DSS を追加して文書タイムスタンプで終わる更新(このタイムスタンプのバイト範囲は 53,131 バイト目まで)、そしてタイムスタンプの後ろに追加された 1,543 バイトの最後の更新です。この最後の更新を、検証ツールは署名の保守(signature maintenance)とみなします。同じ方法で署名したファイルの DSS を一覧すると、「すべての検証データ」が実際に何を指すのかがわかります。署名者のチェーンとテスト用 CA の CRL に加えて、DigiCert のタイムスタンプレスポンダーのチェーンと 2 つの OCSP レスポンスも含まれています。タイムスタンプも検証できる状態を保たなければならないからです。

B-LTA ファイルのドキュメントセキュリティストア(pyHanko と asn1crypto で読み取り)
certs:
   Organization: Example GmbH, Common Name: Example Signing Root CA
   Organization: Example GmbH, Common Name: Alice Example
   Common Name: DigiCert Trusted Root G4, Organizational Unit: www.digicert.com, Organization: DigiCert Inc, Country: US
   Common Name: DigiCert Trusted G4 TimeStamping RSA4096 SHA256 2025 CA1; Organization: DigiCert, Inc.; Country: US
   Common Name: DigiCert SHA256 RSA4096 Timestamp Responder 2026 1; Organization: DigiCert, Inc.; Country: US
CRLs:
   Organization: Example GmbH, Common Name: Example Signing Root CA
OCSP responses:
   ef6f534ae9e4067c7acae29056f62fd449eccb4e
   ecd7e382d2715d644cdf2e673fe7ba98ae1c0f4f

OCSP レスポンダーは鍵のハッシュ値で識別されています。文書タイムスタンプも、それ自体の TSA 証明書の有効期限が切れる前に更新する必要があり、pyhanko sign ltaupdate で新しいものを追加します。検証データが埋め込まれた署名を、Acrobat は「LTV enabled」と表示します。

どのレベルで署名するか

  • 必ずタイムスタンプを付ける:B-B で十分なのは、証明書の有効期間内に読まれて保管される文書だけです。その場合でも、タイムスタンプのコストは TSA への HTTP リクエスト 1 回にすぎません。最低でも B-T を使ってください。
  • アーカイブする文書には B-LTA を使う:何年も後に確認できなければならない契約書、規制対象の記録、請求書には、埋め込まれた検証データと文書タイムスタンプが必要です。ltaupdate の実行は、最新の文書タイムスタンプの TSA 証明書の有効期限が切れるより十分前に、またそのアルゴリズムが弱いとみなされる前に行うよう計画してください。
  • 法的効力は別の問題:EU の eIDAS 規則(EU 規則 No 910/2014)では、適格電子署名には、EU のトラステッドリストに掲載されたトラストサービスプロバイダーが発行する適格証明書と、適格署名生成装置が必要です。PAdES が定めるのはファイル形式であり、その法的なレベルではありません。pyHanko の --eutl-territories オプションと --require-qualified オプションは、トラステッドリスト側を確認します。
  • 読み手が信頼する CA で署名する:外部の受信者向けなら、AATL または EU のトラステッドリストに掲載された CA を使います。すべての読み手でその CA を信頼するよう設定できるなら、社内 CA でもかまいません。

署名済み PDF の確認方法

  1. すべての署名とリビジョンを確認する:緑のバナーを見るだけでは不十分です。最後の署名の後に続くリビジョンはいくつあり、それぞれ何を変更していますか。
  2. 判定を分けて考える:完全性の失敗 → 署名されたバイトが変更されています。信頼の失敗 → 閲覧側の信頼の問題です。変更の指摘 → 署名後にコンテンツが追加されているので、その内容を確認する必要があります。
  3. 時刻の証拠を確認する:信頼する TSA によるタイムスタンプがありますか。それとも署名者の /M だけですか。
  4. アーカイブでは LTV を確認する:検証データは埋め込まれていますか。最新の文書タイムスタンプは、まだその TSA 証明書の有効期間内にありますか。
  5. これらすべてを報告する検証ツールを使う:スクリプトでは、--trust を明示した pyhanko sign validate --pretty-print、または AdES 形式の pyhanko sign adesverify を使います。pyHanko 0.38 は、オペレーティングシステムのトラストストアへのフォールバックについて「is deprecated and will be removed in a future release」(非推奨であり、将来のリリースで削除される)と警告するため、信頼するルートは明示的に指定してください。ブラウザーでは、PDF 署名検証ツールが、文書をアップロードせずに同じ確認を行います。

よくある間違い

  • 目に見える署名の画像を信頼する:ページ上の印影は飾りにすぎません。署名とは CMS オブジェクトのことであり、画像は何も証明しません。
  • 「署名は有効」を「文書は変更されていない」と受け取る:それを意味するのは、「ファイル全体を対象としている」という結果か、問題のない差分分析だけです。
  • 警告を消すために署名者自身の証明書を信頼済みとして追加する:発行元の CA を確認したうえで信頼するか、まったく信頼しないかのどちらかにしてください。
  • タイムスタンプがない:TSA のタイムスタンプがなければ、署名の証拠としての価値は、証明書の有効期限が切れた時点で失われます。
  • ファイルを書き換えるツールで署名済み PDF を保存し直す:多くのエディターは増分保存ではなく完全保存を行うため、既存の署名がすべて壊れます。

出典

  • ISO 32000-2(PDF 2.0)、§12.8 デジタル署名。ISO 32000-1 は adbe.pkcs7.detached と署名辞書を定義しています。
  • ETSI EN 319 142-1 V1.2.1(2024年1月):PAdES の構成要素とベースラインレベル、文書タイムスタンプ。
  • RFC 5652(CMS)、RFC 3161(タイムスタンプ)、RFC 5035(signingCertificateV2)。
  • pyHanko 0.38.0 のドキュメントと CLI。
  • PDF Insecurity:増分保存攻撃、署名ラッピング攻撃、シャドウ攻撃に関する研究。

このページのファイルはすべて、2026年10月20日に node:24.22.0-trixie 上で、virtualenv にインストールした pyHanko 0.38.0(pyhanko-cli 0.6.0、pyhanko-certvalidator 0.33.0)と OpenSSL 3.5.7 を使って作成しました。タイムスタンプは http://timestamp.digicert.com から取得しています。