PDF電子署名の検証を実装する

PDF電子署名の検証を実装する

「電子署名の検証機能をつけてください」という要件は、そのままではシステムを作れません。検証という言葉が指しているのは、実際には4つの別々の判定だからです。どれを実装し、どれを実装しないのかを決めないまま発注すると、「署名は検証済みです」と表示しているのに証明書の発行者を誰も見ていない、といった穴のあるシステムができあがります。本記事は、契約書や申込書へのPDF署名を自社サービスに組み込もうとしている発注者・開発責任者の方に向けて、検証が答えるべき4つの質問と、それをOSSで手元に再現する手順、そしてマイナンバーカード(JPKI)の署名検証に持ち込んだときに何が技術問題から制度問題に変わるのかを整理します。確認日: 2026年7月18日。

この記事の要点

  • PDF署名の検証は intact(改ざん)・valid(署名値)・trusted(チェーン)・revoked(失効)の4つの独立した判定です。1本の「検証OK/NG」に潰すのが最も多い設計ミスです
  • pyHankoと自前のテストCAがあれば、4項目が別々に落ちる様子を実カードなしで再現できます。改ざんは intact のみ False、未知のCAは trusted のみ False という形で切り分けられます
  • 暗号的に正しい署名(valid: True)は、信頼できる署名という意味ではありません。オレオレCAで署名したPDFも valid は True になります
  • JPKIに置き換えると、intact と valid までは同じコードで済みますが、trusted と revoked の材料調達が制度の問題になります。失効情報は主務大臣認定を受けた署名検証者にしか提供されません

「検証済み」という表示の裏にある4つの質問

署名を検証する、という処理を分解すると、互いに独立した4つの問いになります。

質問確かめること失敗したときの意味
intact署名後に文書が変更されていないか改ざんされている
valid署名値が暗号的に正しいか署名データ自体が壊れている・偽造
trusted証明書チェーンが信頼できるCAに繋がるか知らない発行者の証明書
revoked証明書が失効していないか鍵漏えい等で無効化済み

重要なのは、この4つが別々に成立するという点です。「改ざんはされていないが、発行者が信頼できない」も、「暗号的には正しいが、証明書は失効済み」も、実務では普通に起こり得る状態です。

発注者の立場でこれを読み替えると、次のような要件の書き分けになります。intact と valid は「文書が本物か」を守る技術要件で、これを外す選択肢はありません。一方 trusted は「誰の署名を受け入れるか」というビジネス判断で、社内規程や取引ルールと結びつきます。revoked は「いつ時点の有効性を保証するか」という運用要件で、失効情報の取得元とタイミングを決める必要があります。この4つを要件定義書の別々の行として書けているかどうかが、後工程での認識ズレを防ぐ分岐点です。

なお、これらをどの順番で判定すべきかという実装レベルの論点は、署名検証の正しい順序で詳しく扱っています。本記事は「何を判定するのか」の側に集中します。

手元で確かめる環境をつくる

理屈だけでは、4項目が本当に別々に落ちるのか実感が湧きません。実カードや商用証明書を用意しなくても、OSSと自前のテストCAで一通り再現できます。

検証に使うのは pyHanko です。PDF署名まわりのデファクトOSSライブラリで、署名の付与も検証も扱えます。ここでの実行環境は macOS + Python 3.9 + pyHanko 0.33.0 です。CLIは別パッケージの pyhanko-cli に分離されている点だけ注意してください。

準備: テスト用PKIを作る

opensslでルートCAと署名者証明書を作ります。

openssl req -x509 -newkey rsa:2048 -sha256 -days 365 -nodes \
  -config openssl.cnf -extensions v3_ca \
  -keyout root-ca.key -out root-ca.pem -subj "/C=JP/O=Test Root CA/CN=Test Root CA"
openssl req -new -newkey rsa:2048 -nodes -config openssl.cnf \
  -keyout signer.key -out signer.csr -subj "/C=JP/O=Test Org/CN=Taro Yamada (test)"
openssl x509 -req -in signer.csr -CA root-ca.pem -CAkey root-ca.key -CAcreateserial \
  -days 180 -sha256 -extfile openssl.cnf -extensions v3_signer -out signer.pem

署名者証明書の keyUsage には digitalSignature,nonRepudiation を入れておきます。この2つのビットは「署名に使える鍵か」「否認防止の用途か」を証明書自身が宣言する部分で、検証側がここを見て用途外の証明書を弾く実装もあります。

署名する: PAdES形式で付与する

pyHankoでPDFにPAdES署名を付けます。

from pyhanko.sign import signers, fields
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter

signer = signers.SimpleSigner.load(
    "signer.key", "signer.pem", ca_chain_files=["root-ca.pem"],
)
with open("contract.pdf", "rb") as inf:
    w = IncrementalPdfFileWriter(inf)
    with open("contract-signed.pdf", "wb") as outf:
        signers.sign_pdf(
            w,
            signers.PdfSignatureMetadata(
                field_name="Signature1",
                subfilter=fields.SigSeedSubFilter.PADES,
            ),
            signer=signer,
            output=outf,
        )

形式にPAdESを選んでいるのは、実務のPDF署名がほぼこの形式に収れんしているからです。契約書のように数年単位で保存する文書では、署名時点の有効性を後から証明できる構造が要ります。その理由と設計は長期署名(PAdES/LTV)の考え方にまとめています。

検証する: ValidationContextが信頼の起点

検証の中心は ValidationContext です。「どのルートCAを信頼するか」をここで渡します。言い換えると、trusted の判定基準はライブラリが勝手に決めるのではなく、実装者が明示的に注入するということです。

from pyhanko.sign.validation import validate_pdf_signature
from pyhanko.pdf_utils.reader import PdfFileReader
from pyhanko_certvalidator import ValidationContext
from pyhanko.keys import load_cert_from_pemder

vc = ValidationContext(trust_roots=[load_cert_from_pemder("root-ca.pem")])
with open("contract-signed.pdf", "rb") as f:
    r = PdfFileReader(f)
    status = validate_pdf_signature(r.embedded_signatures[0], vc)
    print(status.intact, status.valid, status.trusted)

正常系の実行結果です。

intact(改ざんなし)   : True
valid(署名値の検証)  : True
trusted(チェーン検証): True

ここが基準線になります。以降はこの状態からわざと1つずつ壊して、どのフラグが落ちるかを見ていきます。

3つの実験: 4項目が別々に落ちることを確かめる

実験1: 署名後に文書を改ざんする

署名済みPDFの本文バイトを直接書き換えて(「Test contract」を「Fake contract」に)、同じ検証を通します。

intact: False / valid: True

注目すべきは valid が True のままである点です。署名値そのものは壊れていない、しかし文書のハッシュが署名時と一致しない(intact: False)という状態です。この区別ができていると、ユーザーに返すエラーを「署名データが不正です」と「署名後に文書が変更されています」に書き分けられます。前者は偽造の疑い、後者は運用事故の可能性が高く、社内での次のアクションがまったく違います。

実験2: 知らないCAが発行した証明書

信頼点を空にして(trust_roots=[])検証すると、こうなります。

intact: True / valid: True / trusted: False

暗号的には完全に正しい署名でも、チェーンの検証で落ちます。裏を返せば、誰でも作れる「オレオレCA」で署名したPDFも、暗号的には正しく見えるということです。intact と valid だけを見て「検証済み」と表示するUIは、自作証明書による偽装を素通しします。検証結果の画面設計をレビューするときは、trusted の判定結果が画面のどこに反映されているかを必ず確認してください。

実験3: 失効した証明書

ルートCAの鍵で署名者証明書を失効させたCRLを作り、検証に渡します。

vc = ValidationContext(
    trust_roots=[load_cert_from_pemder("root-ca.pem")],
    crls=[asn1_crl.CertificateList.load(crl_der)],
    revocation_mode="hard-fail",
    allow_fetching=False,
)

結果です。

intact : True / valid: True
trusted: False
revoked: True

ログには「CRL indicates the end-entity certificate was revoked … due to a compromised key」と失効理由まで出力されます。ここで実務上の設計問題が姿を現します。このCRL(またはOCSP)をどこから、いつ取得するのかです。revocation_mode を hard-fail にすれば失効情報が取れないときに検証を失敗させられますが、それは失効情報の配布元が落ちているときに自社の契約フローが止まることを意味します。可用性と厳格性のどちらを優先するかは、扱う文書の性質に応じた経営判断です。

JPKIに翻訳すると、どこが制度の壁になるか

ここまでの構造は、マイナンバーカードの署名用電子証明書による署名を検証する場合でもそのまま成立します。ただし2点が大きく異なります。

  • 信頼点(trusted): trust_roots に入るのは、J-LIS(地方公共団体情報システム機構)の公的個人認証サービスの認証局証明書です。自前のテストCAを差し替える形になります
  • 失効確認(revoked): JPKIの失効情報には、誰でも自由に取得できるCRL配布点があるわけではありません。主務大臣の認定を受けた署名検証者にのみ提供されます

つまり、intact と valid はここまで書いてきたコードと同じ実装で済むのに、trusted と revoked は材料の調達方法が制度によって規定される、という切り分けになります。自前で revoked を判定したいなら、認定を取得するか、認定済みのプラットフォーム事業者を経由するかの二択です。この認定の構造についてはJPKIの民間利用と認定の壁で詳しく解説しています。

もう一点、設計上見落とされやすいのが証明書の使い分けです。マイナンバーカードには署名用電子証明書と利用者証明用電子証明書の2種類が入っており、どちらを検証するかで参照する認証局も、そもそも実現できることも変わります。この違いはマイナンバーカードの2つの電子証明書にまとめました。本人確認方式全体のなかでJPKIがどこに位置づくのかを俯瞰したい場合は、eKYC完全マップ2026から入るのが早道です。

発注前チェックリスト: 検証要件を4行に分解する

RFPや要件定義のレビューで、次の項目が明記されているかを確認してください。埋まらない欄がある状態で見積もりを取ると、後から「そこまでは含んでいません」となりがちな箇所です。

  • intact/valid をどう扱うか: 検証失敗時のエラー種別を「署名データの不正」と「署名後の改ざん」に書き分けるか。ログに残す情報は何か
  • trusted の信頼点は誰が決めるか: 受け入れる認証局のリストを誰が管理し、どの頻度で更新するか。ルート証明書の入れ替え手順は運用手順書にあるか
  • revoked の取得元とタイミング: CRLかOCSPか、取得は署名検証のたびか定期キャッシュか。取得できないときに検証を通すのか止めるのか(hard-fail 相当にするか)を明文化しているか
  • 検証時点の記録: いつの時点で有効と判定したのかを、後から第三者に示せる形で保存するか。長期保存する文書なら長期署名の設計とセットで検討する
  • JPKI署名を受け入れるか: 受け入れるなら、失効確認をどう調達するか(自社認定か、プラットフォーム事業者経由か)を方式選定の段階で決めておく
  • 表示文言: 画面に「検証済み」と出す条件は4項目すべてTrueか。部分的にしか判定していないなら、その旨がユーザーに伝わる文言になっているか

このシリーズの記事をどの順で読むと全体像が掴めるかは、eKYC・本人確認記事の読み方ガイドに整理しています。

まとめ

  • 検証は intact(改ざん)・valid(署名値)・trusted(チェーン)・revoked(失効)の4つの独立した質問です。1本のOK/NGに潰さないことが設計の起点になります
  • pyHankoと自前PKIがあれば、4項目が別々に落ちる様子を実カードなしで再現できます。改ざんは intact: False、未知のCAは trusted: False、失効は revoked: True として個別に検出されます
  • 暗号的に正しい署名(valid: True)は、信頼できる署名という意味ではありません。trusted と revoked まで見て初めて「検証済み」と言えます
  • trusted の信頼点は実装者が明示的に注入するものです。誰の署名を受け入れるかはビジネス判断であり、要件定義に書くべき項目です
  • JPKI署名の検証では、失効情報の取得に署名検証者の認定が必要です。検証の技術そのものは同じでも、材料調達が制度の問題に変わります
  • 発注前に、4項目それぞれの扱い・失効情報の取得方針・「検証済み」表示の条件を要件として言語化しておくと、後工程での認識ズレを防げます

よくある質問

Q. PDFの電子署名検証では何をチェックするのですか?

独立した4項目をチェックします。intact(署名後に文書が変更されていないか)、valid(署名値が暗号的に正しいか)、trusted(証明書チェーンが信頼できるCAに繋がるか)、revoked(証明書が失効していないか)です。この4つは別々に成立するため、「改ざんはないが発行者が信頼できない」「暗号的には正しいが失効済み」といった状態があり得ます。1本の検証OK/NGにまとめてしまうと、こうした違いが表現できなくなります。

Q. 署名が「有効(valid)」なら信頼してよいのですか?

いいえ。valid は署名値が暗号的に正しいことだけを意味します。誰でも作れる自己署名の認証局(いわゆるオレオレCA)で署名したPDFも valid は True になります。信頼できる発行者かを見る trusted と、証明書が失効していないかを見る revoked まで確認して初めて「検証済み」と言えます。

Q. PDF署名の検証はOSSで実装できますか?

できます。PythonのpyHankoがPDF署名のデファクトOSSライブラリで、署名の付与と検証の両方を扱えます。opensslで自前のテストCAと署名者証明書を作れば、実カードや商用証明書なしで改ざん・未知のCA・失効の3パターンを手元で再現し、4項目が別々に落ちることを確認できます。

Q. マイナンバーカードの署名検証も同じ実装でできますか?

intact と valid までは同じ実装で成立します。異なるのは残り2つです。trusted の信頼点にはJ-LIS(地方公共団体情報システム機構)の公的個人認証サービスの認証局証明書が入ります。revoked については、JPKIの失効情報が誰でも取れるCRL配布点として公開されておらず、主務大臣の認定を受けた署名検証者にのみ提供されます。自前で失効判定を行うには認定を取得するか、認定済みのプラットフォーム事業者を経由する必要があります。

Q. 失効情報が取得できないとき、検証は通すべきですか止めるべきですか?

扱う文書の性質に応じた判断になります。pyHankoでは revocation_mode を hard-fail にすることで、失効情報が取得できない場合に検証を失敗させられます。厳格側に倒すと安全ですが、失効情報の配布元が落ちているときに契約フローが止まります。どちらを選ぶにせよ、方針を要件として明文化し、止まったときの代替手順まで運用手順書に書いておくことが重要です。

Q. 検証機能を発注するとき、要件定義に何を書けばよいですか?

4項目それぞれの扱いを分けて書きます。具体的には、intact/valid の失敗をエラー種別として書き分けるか、trusted の信頼点リストを誰がどう管理するか、revoked の取得元(CRLかOCSPか)と取得タイミング、取得失敗時に検証を通すか止めるか、検証時点の記録をどう残すか、そして画面に「検証済み」と表示する条件は何か、の6点です。ここが空欄のまま見積もりを取ると、後から追加費用の交渉になりやすい箇所です。

主な参考資料: pyHanko公式ドキュメント公的個人認証サービス ポータルサイト総務省 公的個人認証サービスによる電子証明書(民間事業者向け)

TodoONada株式会社では、マイナンバーカード認証(JPKI・eKYC)を組み込んだWebサービス・スマホアプリの受託開発を行っています。電子署名の付与・検証システムについては、検証要件を4項目に分解する要件定義の段階から、JPKI署名検証の方式選定(自社認定かプラットフォーム事業者経由か)まで一貫してご支援できます。デジタル庁「デジタル認証アプリ」の利用申請・審査対応も、書類作成から指摘対応まで伴走します。料金の目安は認証機能組込みプラン150万円〜、認証込みアプリ開発プラン400万円〜、大規模・個別要件は1,000万円〜です。「うちの契約書はどこまで検証すべきか」といった検討段階のご相談も、マイナンバーシステム導入支援またはお問い合わせからお気軽にどうぞ。


この記事に関連するサービス

TodoONada株式会社では、マイナンバーカード認証を組み込んだシステム開発を行っています。

導入・開発のご相談はお問い合わせからお気軽にどうぞ。検討段階のご相談も歓迎です。

出典・参照

技術ブログ一覧へ戻る