電子署名の仕組みは、文書のハッシュ値を秘密鍵で署名し、受け取った側が公開鍵と電子証明書で検証するという流れでできています。この記事では、その流れをopensslのコマンドで1工程ずつ動かせる形にし、改ざんがどう検知されるか、X.509証明書チェーンで公開鍵の持ち主をどう確かめるか、PDFの電子署名がファイルのどこを覆っているかまでを実装者向けに整理します。電子署名の定義や当事者型と立会人型の違いといった全体像は電子署名とは?意味・仕組み・種類と法的効力の解説で扱っているため、ここで扱うのは「手元で確かめる」ための実装手順です。
まとめ:電子署名の仕組みはハッシュ・署名・証明書検証・時刻の4層で組み立てる
電子署名は4つの層の積み重ねです。1層目はハッシュ関数で、文書をSHA-256などで固定長の値に縮めます。2層目は署名で、そのハッシュ値を署名者の秘密鍵で署名値に変える処理です。3層目は証明書の検証で、署名に使われた公開鍵が誰のものかを認証局の発行したX.509証明書でたどり、失効していないかも確かめます。4層目は時刻で、タイムスタンプによって「その時点で署名が存在した」ことを固定し、証明書の有効期間が過ぎても検証できる状態を残します。
実装で詰まりやすいのは3層目と4層目です。署名値の数学的な検証だけなら数行のコマンドで終わりますが、それだけでは本人性も長期の有効性も出ません。受け取った署名済みPDFの検証と保管を自社システムに組み込むなら自前実装で十分に届き、取引先と契約を結ぶ場面は電子署名サービスのAPI連携に寄せる、というのが本稿の結論です。
電子署名の仕組みをハッシュ計算と秘密鍵署名・公開鍵検証の3工程で追う
署名者側の処理はSHA-256でダイジェストを作り秘密鍵で署名値に変える2段階
署名者が行う処理は2段階しかありません。まず文書全体からハッシュ値(ダイジェスト)を計算します。SHA-256なら文書が1ページでも1,000ページでも出力は32バイトです。次に、その32バイトを秘密鍵で署名値に変換します。文書そのものを暗号化するわけではない点が、暗号化通信との大きな違いです。
ハッシュを挟む理由は、署名演算のコストを文書の大きさから切り離すためと、少しでも内容が違えばまったく別の値になる性質を改ざん検知に使うためです。ハッシュ関数そのものの性質はハッシュ化の仕組みと改ざん検知の解説にまとめています。法務省も電子署名と認証の仕組みの解説で、文書を圧縮したメッセージダイジェストを秘密鍵で暗号化する流れとして説明しています。
検証者側は同じハッシュを再計算し公開鍵で署名値と照合して1ビットの差も検知
検証者は、受け取った文書から同じアルゴリズムでハッシュ値を計算し直します。並行して、署名値を署名者の公開鍵で検証し、署名時のハッシュ値と一致するかを確かめる処理です。一致すれば「この秘密鍵の持ち主が、この内容に署名した」ことが言え、不一致なら文書か署名値のどちらかが変わっています。
この照合は、電子署名法第2条が電子署名に求める2つの要件、つまり「その者が作成したことを示す」ことと「改変が行われていないかを確認できる」ことに、それぞれ対応しています。秘密鍵と公開鍵が対になる関係は公開鍵暗号方式と共通鍵暗号方式の違いで詳しく扱っています。
署名方式はFIPS 186-5が承認するRSA・ECDSA・EdDSAから鍵長で選ぶ
米国の署名標準FIPS 186-5(2023年2月公開)は、署名の生成と検証の方式としてRSA、ECDSA、EdDSAの3つを定めています。旧版にあったDSAは署名生成の方式から外れ、既存署名の検証用途に限られました。新しく作るシステムでDSAを選ぶ理由はありません。
| 方式 | 代表的な鍵 | 署名値の大きさ | 向く場面 |
|---|---|---|---|
| RSA | 3072ビット | 384バイト | 古い検証ソフトとの互換を優先 |
| ECDSA | P-256 | 約64〜72バイト | 電子証明書の発行元が対応済み |
| EdDSA | Ed25519 | 64バイト | 自社内で検証側も管理できる |
迷ったらECDSAのP-256を選びます。国内の認証局やPDFの検証ソフトで扱える範囲が広く、署名値も小さく収まります。Ed25519は性能面で優れていますが、PDFの電子署名では検証ソフト側の対応がまだ揃っておらず、社外に出す文書には向きません。
opensslで電子署名を作成し検証するコマンドと改ざん検知の実行手順
genpkeyでP-256の鍵ペアを作りdgstで契約書PDFに署名する手順
ここからのコマンドを実行する環境の前提はOpenSSL 3系です。2026年11月時点のOpenSSL公式の配布ページでは3.5系が長期サポート版で、3.5.9が2026年9月29日に出ています。署名と検証はopenssl dgstのマニュアルにある-signと-verifyで行えます。
# 署名者の鍵ペア(ECDSA P-256)を作る
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out signer.key
openssl pkey -in signer.key -pubout -out signer.pub
# SHA-256でハッシュを取り、秘密鍵で署名値を作る
openssl dgst -sha256 -sign signer.key -out contract.sig contract.pdf
# 公開鍵で検証する。成功すると「Verified OK」と表示される
openssl dgst -sha256 -verify signer.pub -signature contract.sig contract.pdf
contract.sigが署名値で、DER形式のECDSA署名が入ります。手元の任意のファイルに置き換えれば、そのまま試せます。秘密鍵のsigner.keyは暗号化されずに書き出されるため、検証用の作業ディレクトリ以外には置かないでください。
1バイト追記したPDFで検証が失敗し終了コード1になる改ざん検知の確認
改ざん検知は、文書に1バイト足すだけで確認できます。
cp contract.pdf tampered.pdf
printf 'x' >> tampered.pdf
# 同じ署名値で検証すると「Verification failure」になる
openssl dgst -sha256 -verify signer.pub -signature contract.sig tampered.pdf
echo $? # 1 が返る
終了コードが0以外になるので、シェルスクリプトやCIでは$?で判定できます。注意したいのは、この検証が示すのは「signer.pubに対応する秘密鍵で署名された」ことだけという点です。signer.pubが本当に署名者本人のものかは、この時点では何も保証されていません。そこを埋めるのが次の章の証明書です。
X.509証明書チェーンで公開鍵の持ち主を確かめる検証と失効確認の実装
生の公開鍵だけでは本人性が出ない理由とRFC 5280の証明書が持つ項目
公開鍵はただのバイト列です。誰でも鍵ペアを作り「山田太郎の公開鍵です」と添えて送れます。そこで認証局(CA)が本人確認をしたうえで、公開鍵と氏名や組織名をひも付けて署名したデータを発行します。これがX.509証明書で、その形式を定義しているのはRFC 5280です。
実装で読む項目は5つです。主体者(Subject)と発行者(Issuer)、有効期間(notBefore・notAfter)、鍵用途(keyUsage)、そして失効情報の取得先(CRL配布点とAIAのOCSP URL)です。マイナンバーカードや商業登記の電子証明書など、国内で使われる証明書の種類と有効期限は電子証明書の仕組み・種類・有効期限の解説にまとめています。
検証用のルートCA・署名者証明書をopensslで発行してチェーンを組む手順
手元で検証の流れを確かめるには、テスト用のルートCAを作り、そのCAで署名者の証明書を発行します。本番では認証局が発行した証明書を使うため、以下は検証環境専用です。
# テスト用のルートCA(10年)
openssl req -x509 -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
-keyout ca.key -out ca.crt -days 3650 -subj "/CN=Test Root CA"
# 署名者の証明書要求を作り、CAで1年の証明書を発行する
openssl req -new -key signer.key -out signer.csr -subj "/O=Example Inc./CN=Taro Yamada"
printf 'keyUsage=critical,digitalSignature,nonRepudiation\n' > signer.ext
openssl x509 -req -in signer.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-days 365 -extfile signer.ext -out signer.crt
# チェーンを検証する。成功すると「signer.crt: OK」
openssl verify -CAfile ca.crt signer.crt
中間CAがある実運用の構成では、openssl verifyのマニュアルのとおり、中間CAの証明書を-untrustedに渡し、信頼の起点にするルートだけを-CAfileに置きます。中間CAを-CAfileに混ぜると、ルートまでたどれない証明書まで信頼してしまうので避けてください。
CMS形式の分離署名をopenssl cmsで作り証明書チェーンごと検証する方法
前の章のdgstは署名値だけを出力しました。実務の電子署名は、署名値に署名者の証明書やハッシュ方式、署名時刻などの属性をまとめたCMS(RFC 5652)という入れ物で扱います。PDFの電子署名の中身もこの形式です。
# 本文とは別ファイルの分離署名をDERで作る。-binaryで改行コードの変換を止める
openssl cms -sign -binary -in contract.pdf -signer signer.crt -inkey signer.key \
-md sha256 -outform DER -out contract.p7s
# 本文と署名を別々に渡し、署名値と証明書チェーンをまとめて検証する
openssl cms -verify -binary -inform DER -in contract.p7s -content contract.pdf \
-CAfile ca.crt -out /dev/null
echo $? # 0 なら検証成功
-binaryを付け忘れると、テキストとして改行が正規化されてからハッシュが取られ、PDFのようなバイナリでは検証が通らなくなります。openssl cmsのマニュアルにあるとおり、-cadesを付けると署名者証明書を特定する属性が加わり、CAdES-BESの形になります。
有効期限内でも失効した証明書を見抜くCRLとOCSPの確認を検証に入れる
証明書は有効期間内でも、秘密鍵の漏えいや退職によって認証局が失効させることがあります。openssl verifyは既定では失効を確認しないため、失効リスト(CRL)か、RFC 6960で定められたOCSPの問い合わせを自分で組み込む必要があります。
# CRLで確認する(CRLは証明書のCRL配布点から取得したもの)
openssl verify -crl_check -CAfile ca.crt -CRLfile ca.crl.pem signer.crt
# OCSPで確認する。問い合わせ先は証明書のAIAから取り出す
OCSP_URL=$(openssl x509 -in signer.crt -noout -ocsp_uri)
openssl ocsp -issuer ca.crt -cert signer.crt -url "$OCSP_URL" -CAfile ca.crt
見落としやすいのは「いつの時点で失効していなかったか」という問いです。検証する今日に失効していても、署名した日に有効だったなら署名は有効と扱えます。その判断には署名時刻の証拠が要り、それが後半のタイムスタンプの役割になります。
PDFの電子署名の仕組みをByteRangeとPAdESの4レベルで読み解く
PDF署名はContents欄を除く全バイトをByteRangeで指定して覆う仕組み
PDFの電子署名は、ファイルの中に署名値を書き込みながら、そのファイル自体に署名するという入れ子の問題を抱えています。PDFはこれを、署名辞書のContents欄だけを空けておき、残りの全バイトに署名することで解く仕組みです。どこからどこまでが署名対象かはByteRangeという4つの数値で示されます。
[0 b c d]なら、先頭からbバイトと、cバイト目からdバイトが署名対象です。その間に挟まった16進文字列がCMS形式の署名で、本文とは分離された形式で入っています。署名後に注釈やページを足すと、PDFは元のバイトを残したまま末尾に追記(増分更新)するため、古い署名は追記前の範囲を覆ったまま残ります。
Pythonで署名PDFからCMSを取り出しopenssl cmsで検証する実装例
ByteRangeの仕組みが分かれば、専用ライブラリを使わずに署名を取り出して検証できます。次のPythonは標準ライブラリだけで、署名対象のバイト列とCMSを書き出します。
import hashlib
import re
import sys
def der_length(buf):
# 先頭のSEQUENCE(0x30)の長さを読み、ゼロ埋めを除いたDERの実長を返す
if buf[0] != 0x30:
raise ValueError("CMSのDERではありません")
first = buf[1]
if first < 0x80:
return 2 + first
n = first & 0x7F
return 2 + n + int.from_bytes(buf[2:2 + n], "big")
def extract(pdf_path):
data = open(pdf_path, "rb").read()
# 増分更新で署名が複数ある場合は、ファイル内で最後に現れる署名を対象にする
ranges = re.findall(rb"/ByteRange\s*\[\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*\]", data)
if not ranges:
raise ValueError("ByteRangeが見つかりません")
a, b, c, d = (int(x) for x in ranges[-1])
signed = data[a:a + b] + data[c:c + d]
# Contentsは山括弧で囲まれた16進文字列。両端の1文字ずつを外して復元する
cms = bytes.fromhex(data[a + b + 1:c - 1].decode("ascii"))
cms = cms[:der_length(cms)]
return signed, cms, c + d == len(data)
if __name__ == "__main__":
signed, cms, covers_all = extract(sys.argv[1])
open("signed_bytes.bin", "wb").write(signed)
open("signature.der", "wb").write(cms)
print("署名対象のSHA-256:", hashlib.sha256(signed).hexdigest())
print("署名がファイル末尾まで覆っているか:", covers_all)
python extract_pdf_sig.py signed.pdf
openssl cms -verify -binary -inform DER -in signature.der -content signed_bytes.bin \
-CAfile root.crt -purpose any -out /dev/null
covers_allがFalseなら、署名の後に何かが追記されています。タイムスタンプや失効情報の追加なら正常ですが、ページ内容の書き換えも同じ追記の形を取るため、業務システムで受け入れる際は「署名後の追記があればPDFビューアでの差分確認に回す」といった扱いを決めておきます。-purpose anyは、文書署名用の鍵用途を持つ証明書がS/MIME用途の確認で弾かれるのを避けるための指定です。
PAdESのB-B・B-T・B-LT・B-LTAの違いと業務で求めるべき署名レベル
PDFの電子署名の形式は欧州の規格ETSI EN 319 142-1(PAdES)で4段階に整理されています。上のレベルは下のレベルの要件をすべて含みます。
| レベル | 加わる要素 | 検証できる期間の目安 |
|---|---|---|
| B-B | CMS署名と署名者証明書 | 署名者証明書の有効期間内 |
| B-T | 署名値へのタイムスタンプ | 署名時刻を示せるが失効情報は外部頼み |
| B-LT | DSSに証明書チェーン・CRL・OCSP応答 | TSA証明書の有効期間内 |
| B-LTA | 文書全体へのドキュメントタイムスタンプ | タイムスタンプの更新を続ける限り |
契約書や請求書のように5年、7年と保存して後から真正性を問われる文書は、B-LT以上を受け入れ基準にします。社内の承認記録のように保存期間が短く、検証も社内で完結する文書ならB-Tで足りるという判断です。B-Bのまま長期保管すると、証明書が切れた時点で検証ソフトに「有効性不明」と表示されるようになります。
タイムスタンプで署名時刻を固定し証明書の期限切れ後も検証を残す長期署名
RFC 3161のタイムスタンプ要求をopenssl tsで作りTSAへ送る手順
タイムスタンプは、時刻認証局(TSA)が「このハッシュ値がこの時刻に存在した」ことに署名したデータで、要求と応答の形式はRFC 3161で決まっています。電子署名に付けるときは、文書ではなく署名値に対して取ります。
# 署名値に対するタイムスタンプ要求を作る(-certでTSA証明書を応答に含めてもらう)
openssl ts -query -data contract.sig -sha256 -cert -out contract.tsq
# RFC 3161のHTTP転送でTSAへ送る。TSA_URLは契約したTSAのものに置き換える
curl -s -H "Content-Type: application/timestamp-query" \
--data-binary @contract.tsq -o contract.tsr "$TSA_URL"
# 応答を検証する。TSAのルート証明書とTSA証明書を指定する
openssl ts -verify -data contract.sig -in contract.tsr -CAfile tsa_root.crt -untrusted tsa.crt
openssl tsのマニュアルには、要求をHTTPで自動送信する機能は無いと明記されています。そのため送信はcurlなどで別に行う手順です。認定タイムスタンプの位置付けや電子帳簿保存法での要否はタイムスタンプの仕組みと電子署名との違いで扱っています。
証明書の有効期間を越えて検証するためにDSSへ失効情報を埋め込む理由
タイムスタンプだけでは長期の検証は成り立ちません。10年後に検証するとき、署名者の証明書を発行した認証局のCRLやOCSPがもう引けない可能性があるからです。B-LTでは、署名時点で集めた証明書チェーンと失効情報を、PDF内のDSS(Document Security Store)という辞書に増分更新で書き込みます。これで、外部へ問い合わせなくても「署名時点で証明書は失効していなかった」ことを示せます。
B-LTAでは、さらにDSSまで含めた文書全体にドキュメントタイムスタンプを付けます。TSA証明書の期限が近づいたら、その時点の失効情報を足して新しいドキュメントタイムスタンプを重ねます。文書管理システムで長期保管するなら、この更新を期限の数か月前にまとめて走らせるバッチが必要です。
電子署名を業務システムに組み込むとき自前で書く範囲と任せる範囲の線引き
受け取った署名済みPDFの検証と保管だけなら自前実装で足りる条件
取引先から届いた署名済みPDFを検証して保管するだけなら、自前実装で十分です。必要な処理は、前の章のByteRangeの取り出し、CMSと証明書チェーンの検証、失効確認、検証結果と検証時刻の記録の4つで、秘密鍵を扱いません。鍵の管理責任を負わない分、事故の影響も限られます。
条件は2つあります。信頼するルート証明書の一覧を誰が更新するかを決めておくことと、検証結果をPDFと同じ保管単位で残すことです。検証済みの文書を版管理つきで保管する仕組みは、文書管理システム開発の範囲として一緒に設計すると、監査時の提出まで一本の流れで作れます。
秘密鍵をサーバーで持つ自社署名はHSMやKMSが無ければ見送る判断
自社名義で請求書や証明書類に署名を付ける場合、秘密鍵をサーバーに置くことになります。秘密鍵がファイルのままアプリケーションサーバーにある構成なら、その時点で採用を見送ります。サーバーへの侵入がそのまま「自社名義の署名を誰でも作れる」状態につながるためです。
進めるなら、鍵をハードウェアセキュリティモジュール(HSM)やクラウドの鍵管理サービスに置き、アプリケーションからは署名の要求だけを送る構成にします。Azureの場合の鍵の置き方とアクセス制御はAzure Key VaultとManaged HSMの違いで扱っています。法的な推定効が働く条件は電子署名法の第2条・第3条の解説を確認してください。
取引先との契約締結は電子署名サービスのAPI連携に寄せるべき場面
取引先に署名してもらう契約締結は、自前で作らず電子署名サービスに任せます。相手方の本人確認、署名依頼のメール送信、署名の順番管理、相手側の証明書発行まで自前で揃えるのは、受託開発の工数に見合いません。自社システムで作るのは、契約データから署名依頼を作る処理と、締結完了の通知を受けて署名済みPDFを保管する処理です。
連携の実装例はDocuSign APIのJWT認証と締結完了の受け取り方で解説しています。受け取った署名済みPDFは、前の2つのh3で述べた検証と保管の流れにそのまま乗せられます。
電子署名の仕組みと実装・検証について開発者から寄せられる質問
電子署名を実装する立場から出てくる質問を5つ取り上げます。
電子署名とハッシュ値だけの改ざん検知は何が違いますか?
ハッシュ値だけを保存しておく方法でも、文書が変わったことは検知できます。ただし、ハッシュ値ごと差し替えられると見抜けず、誰が作った文書かも示せません。電子署名はハッシュ値を秘密鍵で署名するため、秘密鍵を持たない人は辻褄の合う署名値を作れず、作成者も証明書でたどれます。社内のファイル整合性チェックならハッシュで足り、社外に向けて作成者を示す必要がある文書には署名が要ります。
自己署名証明書で作った電子署名に意味はありますか?
改ざん検知としては機能しますが、本人性の証明にはなりません。自己署名証明書は「自分で自分を保証した」状態のため、検証者がその証明書を別の経路で受け取り、信頼すると決めない限り意味を持ちません。社内システム間の署名で、検証側にルート証明書を配布して管理できる場合に限って使います。社外に出す文書には、認証局が発行した証明書を使います。
電子署名の有効期限が切れたPDFは無効になりますか?
署名そのものが消えるわけではありませんが、B-Bレベルの署名は証明書の有効期間が過ぎると検証ソフトで「有効性不明」と表示されます。署名時点で有効だったことを示すには、署名時刻のタイムスタンプと、署名時点の失効情報が必要です。長期保存する文書は、受け取った時点でB-LT以上かを確認し、足りなければ自社側で失効情報とドキュメントタイムスタンプを追加します。
RSAとECDSAのどちらで電子署名を実装すべきですか?
新規の実装ならECDSAのP-256を選びます。同等の安全性でRSAより鍵と署名値が小さく、国内外の認証局やPDFビューアの対応も広いためです。RSAを選ぶのは、検証側に古いソフトや機器が残っていて互換性を優先する場合です。その場合も鍵長は3072ビット以上にします。どちらもFIPS 186-5で承認された方式です。
opensslで作った署名はAdobe Acrobatで「有効」と表示されますか?
本稿のコマンドで作ったCMSの分離署名は、そのままではPDFに埋め込まれていないため、Acrobatでは表示されません。PDFに埋め込むには、署名辞書とByteRangeを書き込む処理が別途要り、通常はpyHankoやiTextなどのPDF署名ライブラリを使います。埋め込んだ場合も、署名者証明書のルートがAcrobatの信頼リストに入っていなければ「有効性不明」と表示されます。テスト用のCAで作った署名が有効と出ないのはこのためです。
関連記事
- 電子署名とは?意味・仕組み・種類と法的効力をわかりやすく解説【2026年時点】:電子署名の定義・種類・法的効力の全体像
- 電子証明書とは?仕組み・種類・有効期限と取得方法をわかりやすく解説【2026年時点】:署名者証明書の種類と取得方法
- タイムスタンプとは?仕組み・電子署名との違いと電子帳簿保存法での要否を解説【2026年時点】:長期署名に使うタイムスタンプの制度面
- 契約書の電子署名とは?当事者型・立会人型の選び分けと署名のやり方【2026年時点】:契約実務での署名方式の選び方
- mTLSとは?相互TLS認証の仕組みとクライアント証明書・導入判断を実装目線で解説:同じX.509証明書を通信の認証に使う実装