なぜデジタル署名を学ぶか
デジタル署名 (digital signature) は、「メッセージが本当にその送信者から来たこと(真正性)」「途中で改ざんされていないこと(完全性)」「送信者があとから『署名していない』と言い逃れできないこと(否認防止)」を数学的に保証する仕組みです。TLS のサーバ証明書、JWT の ES256 / RS256、SSH のホスト鍵認証、Git のコミット署名、Bitcoin / Ethereum のトランザクション、OS のコード署名 —— 現代インフラの信頼はほぼすべて署名の上に成り立っています。
署名は公開鍵暗号の応用です。 RSA 暗号 が素因数分解、 楕円曲線暗号 (ECC) が楕円曲線離散対数問題 (ECDLP) に依拠するのと同じ数学的困難性を、今度は「秘密鍵の持ち主だけが作れて、公開鍵で誰でも検証できるタグ」を作るために使います。本記事では、署名の一般モデルと安全性概念 (EUF-CMA) を整理したうえで、実務の三大方式 ECDSA / EdDSA (Ed25519) / RSA-PSS を数式で比較し、Python で教育用フルスクラッチ ECDSA を実際に実行して署名生成→検証成功→改ざん検知まで確認します。暗号領域全体の地図は 暗号技術ロードマップ を参照してください。
署名の基本モデル
デジタル署名方式は 3 つのアルゴリズムの組 \((\mathsf{Gen}, \mathsf{Sign}, \mathsf{Verify})\) で定義されます。
- 鍵生成 \(\mathsf{Gen}\) : セキュリティパラメータから鍵ペア \((sk, pk)\) を生成する。\(sk\) は署名鍵(秘密)、\(pk\) は検証鍵(公開)。
- 署名 \(\mathsf{Sign}(sk, m) \to \sigma\) : 秘密鍵とメッセージ \(m\) から署名 \(\sigma\) を生成する。
- 検証 \(\mathsf{Verify}(pk, m, \sigma) \to \{0, 1\}\) : 公開鍵で \((m, \sigma)\) の正当性を判定する。
正当な署名は必ず受理される、という正当性 (correctness) が要求されます。
\[ \Pr[\mathsf{Verify}(pk, m, \mathsf{Sign}(sk, m)) = 1] = 1 \tag{1} \]hash-and-sign パラダイム
実際の署名方式は、メッセージ \(m\) をそのまま署名するのではなく、まず暗号学的ハッシュ関数 \(H\) で固定長のダイジェスト \(e = H(m)\) に潰してから署名します。これを hash-and-sign と呼びます。
\[ \sigma = \mathsf{Sign}(sk, H(m)) \tag{2} \]hash-and-sign には 3 つの利点があります。
- 任意長メッセージ対応: 署名の数学は固定サイズの入力(RSA なら法 \(N\) 未満の整数、ECDSA なら位数 \(n\) 未満の整数)しか扱えないが、ハッシュで任意長を吸収できる。
- 性能: 大きなファイルでも 1 回のハッシュと 1 回の署名演算で済む。
- 安全性の切り分け: \(H\) が衝突耐性を持てば、署名方式の安全性を「短いダイジェストへの署名」に帰着できる。逆に \(H\) が壊れると署名も壊れる(MD5 の衝突で偽造 CA 証明書が作られた 2008 年の事件が典型)。
ハッシュの内部構造は ハッシュ関数の記事 や実際の ハッシュ生成ツール で確認できます。SHA-256 / SHA-512 が現行の標準です。
MAC との違い —— 否認防止
「メッセージが改ざんされていないこと」を保証する仕組みとしては、 AES / ChaCha20 の AEAD でも使われる MAC (Message Authentication Code)、たとえば HMAC があります。しかし MAC とデジタル署名は決定的に異なります。
| 観点 | MAC (HMAC など) | デジタル署名 |
|---|---|---|
| 鍵 | 共有秘密鍵(対称) | 秘密鍵で署名・公開鍵で検証(非対称) |
| 検証者 | 鍵を共有する者だけ | 公開鍵を持つ誰でも |
| 否認防止 | なし(両者が鍵を持つため) | あり(秘密鍵の持ち主しか作れない) |
| 主な用途 | セッション内の完全性・認証 | 証明書・契約・公開検証が必要な場面 |
決定的な差は否認防止 (non-repudiation) です。HMAC タグは送信者と受信者が同じ鍵を共有するため、受信者も同じタグを作れます。したがって「送信者が作った」と第三者に証明できません。一方デジタル署名は、署名を作れるのは秘密鍵の持ち主ただ一人で、検証は公開鍵だけあれば誰でもできます。だから「この人が確かに署名した」を裁判所や第三者にも示せます。証明書 (PKI) や電子契約が署名を使うのはこのためです。関連する信頼モデルは ゼロトラストセキュリティ や セキュリティ資格の記事 も参照してください。
安全性概念: EUF-CMA
署名方式の「安全」とは何かを厳密に定めるのが EUF-CMA (Existential Unforgeability under Chosen-Message Attack; 選択文書攻撃に対する存在的偽造困難性) です。次のゲームで攻撃者 \(\mathcal{A}\) が勝てないことを要求します。
- チャレンジャーが鍵ペア \((sk, pk)\) を生成し、\(pk\) を \(\mathcal{A}\) に渡す。
- \(\mathcal{A}\) は好きなメッセージ \(m_1, m_2, \dots\) を選んで署名オラクルに問い合わせ、署名 \(\sigma_i = \mathsf{Sign}(sk, m_i)\) を得られる(これが「選択文書攻撃」)。
- 最後に \(\mathcal{A}\) は、一度も問い合わせていない新しいメッセージ \(m^\*\) に対する有効な署名 \(\sigma^\*\) を出力しようとする。
この確率が無視できるほど小さければ EUF-CMA 安全です。「存在的 (existential)」とは「攻撃者にとって都合のよい特定のメッセージでなくても、何か一つでも新しい有効な署名を作れたら負け」という最も厳しい基準を指します。実務で使う署名方式(RSA-PSS, ECDSA, Ed25519)はいずれも、適切な仮定(RSA 仮定・ECDLP・ランダムオラクル)のもとで EUF-CMA 安全であることが証明されています。逆に、後述する ECDSA の nonce 再利用のような実装ミスは、この証明の前提を壊して偽造を可能にします。
ECDSA: 楕円曲線デジタル署名アルゴリズム
ECDSA (Elliptic Curve Digital Signature Algorithm) は、 ECC 上の署名方式で、TLS 証明書・SSH・Bitcoin / Ethereum で広く使われます。曲線のドメインパラメータ(素体 \(\mathbb{F}_p\) 、ベースポイント \(G\) 、位数 \(n\) )を固定します。
鍵生成:
- 秘密鍵 \(d \in [1, n-1]\) をランダムに選ぶ。
- 公開鍵 \(Q = dG\) ( スカラー倍算 )。
署名生成 —— メッセージハッシュ \(e = H(m)\) に対して:
- 一時乱数 (nonce) \(k \in [1, n-1]\) を選ぶ。
- \(R = kG\) を計算し、\(r = R_x \bmod n\) (\(r = 0\) ならやり直し)。
- 次を計算する(\(s = 0\) ならやり直し)。署名は \((r, s)\) 。
署名検証 —— \((r, s)\) が \([1, n-1]\) にあることを確認し:
\[ w = s^{-1} \bmod n, \quad u_1 = e w \bmod n, \quad u_2 = r w \bmod n \tag{5} \] \[ (x, y) = u_1 G + u_2 Q, \qquad x \bmod n \overset{?}{=} r \tag{6} \]正当性は代入で確認できます。\(s = k^{-1}(e + rd)\) より \(k = s^{-1}(e + rd) = w e + w r d = u_1 + u_2 d\) 。したがって
\[ u_1 G + u_2 Q = u_1 G + u_2 d G = (u_1 + u_2 d) G = k G = R \tag{7} \]となり、\(x\) 座標が \(r\) に一致します。
nonce k の再利用・バイアスは致命的
ECDSA 最大の落とし穴は nonce \(k\) です。\(k\) は署名ごとに一様ランダムかつ秘密でなければならず、少しでも漏れると秘密鍵が即座に復元されます。
(a) k の再利用: 同じ \(k\) で 2 つの異なるメッセージ \(m_1, m_2\) に署名すると、\(r\) は共通で、
\[ s_1 = k^{-1}(e_1 + rd), \quad s_2 = k^{-1}(e_2 + rd) \tag{8} \]の 2 式が立ちます。差を取ると \(s_1 - s_2 = k^{-1}(e_1 - e_2)\) から
\[ k = \frac{e_1 - e_2}{s_1 - s_2} \bmod n, \qquad d = \frac{s_1 k - e_1}{r} \bmod n \tag{9} \]と、nonce \(k\) と秘密鍵 \(d\) が完全に暴露されます。2010 年、Sony の PlayStation 3 はコード署名 ECDSA で \(k\) を定数にしていたため、fail0verflow / geohot によって署名鍵が抽出され、任意コードに正規署名を付けられる事態になりました。
(b) k のバイアス: \(k\) が完全な再利用でなくても、上位ビットが偏る・一部が既知といった部分的な漏れがあるだけで、複数署名から格子攻撃 (lattice attack; Hidden Number Problem を LLL / BKZ で解く) により \(d\) が復元されます。実際、乱数生成器の弱さや不完全な定数時間実装から鍵が抜かれる事故が繰り返し報告されています。
RFC 6979: 決定的 nonce
この構造的危険を断つのが RFC 6979 の決定的 (deterministic) ECDSA です。\(k\) を乱数生成器に頼らず、秘密鍵 \(d\) とメッセージハッシュ \(H(m)\) から HMAC-DRBG で決定的に導出します。
\[ k = \mathrm{HMAC\text{-}DRBG}(d, H(m)) \tag{10} \]こうすると、(1) 同じ \((d, m)\)
には常に同じ \(k\)
、異なる \(m\)
には(暗号学的に)独立な \(k\)
が対応し、再利用もバイアスも起きません。(2) 乱数生成器の品質に依存しないため、エントロピー不足の組み込み機器でも安全です。同じ署名が再現できるためテストも容易になります。現代の ECDSA 実装(OpenSSL, cryptography, Bitcoin ライブラリ等)は RFC 6979 を採用しています。ただし決定的 nonce でも、フォールトインジェクション対策として乱数を追加で混ぜる「hedged」方式も提案されています。
EdDSA / Ed25519
EdDSA (Edwards-curve Digital Signature Algorithm)、特に Curve25519 のツイスト Edwards 形上で SHA-512 を使う Ed25519 は、ECDSA の弱点を設計段階で潰した現代的な署名方式です。SSH・Signal・WireGuard・TLS 1.3・Git のデフォルト署名として普及しています。
鍵生成: 秘密の種 (seed) \(sk\) を SHA-512 でハッシュし、前半 32 byte をクランプしてスカラー \(a\) 、後半 32 byte を「prefix」\(\mathrm{pre}\) とする。公開鍵 \(A = aB\) (\(B\) はベースポイント)。
署名生成 —— メッセージ \(M\) に対して:
\[ r = H(\mathrm{pre} \,\|\, M) \bmod \ell, \quad R = rB \tag{11} \] \[ S = (r + H(R \,\|\, A \,\|\, M) \cdot a) \bmod \ell \tag{12} \]署名は \((R, S)\) (合計 64 byte)。
検証: \(8 S B \overset{?}{=} 8 R + 8 H(R \| A \| M) A\) を確認する。
ここが決定的に重要な点です。Ed25519 の \(r\) は、秘密の prefix とメッセージから決定的に導出されます(式 (11))。つまり ECDSA の nonce \(k\) にあたる値が、乱数生成器を一切使わずに計算されます。これにより:
- nonce 再利用・バイアスが構造的に不可能 —— RFC 6979 の思想を方式そのものに組み込んでいる。
- 実装ミス耐性が高い —— Curve25519 の 定数時間 Montgomery/Edwards 演算 でサイドチャネルにも強く、無効曲線攻撃も設計で排除。
- 高速 —— バッチ検証や高速な固定ベース乗算に最適化しやすく、同一セキュリティで ECDSA より速いことが多い。
| 観点 | ECDSA | Ed25519 (EdDSA) |
|---|---|---|
| nonce | 乱数(RFC 6979 で決定的化) | 常に決定的(方式に内蔵) |
| 実装ミス耐性 | 低い(nonce 事故が頻発) | 高い(misuse-resistant) |
| 曲線 | P-256 / secp256k1 など可変 | Curve25519 固定 |
| ハッシュ | 任意(SHA-256 等) | SHA-512 固定 |
| 署名長 | 約 64–72 byte (DER) | 64 byte 固定 |
| 速度 | 標準 | 一般に高速 |
新規プロトコルで曲線ベース署名を選ぶなら、迷わず Ed25519 が 2026 年のベストプラクティスです。ECDSA は既存互換性(TLS 証明書チェーン、Bitcoin の secp256k1 など)が必要な場面で残ります。
RSA 署名: PKCS#1 v1.5 と RSA-PSS
RSA でも署名できます。素朴には、秘密指数 \(d\) でハッシュを「復号」演算にかけ、公開指数 \(e\) で「暗号化」演算をかけ戻して検証します。
\[ \sigma = (H(m))^{d} \bmod N, \qquad \sigma^{e} \bmod N \overset{?}{=} H(m) \tag{13} \]ただし「教科書 RSA 署名」はそのままでは偽造可能なので、実務ではパディング方式が本質的に重要です。2 つの標準があります。
PKCS#1 v1.5
RSA PKCS#1 v1.5 署名 (RS256 など) は、ハッシュ値の前に固定のパディングバイト列とハッシュアルゴリズム識別子 (ASN.1 DigestInfo) を連結してから累乗します。決定的で実装は単純ですが、パディングが構造的(固定パターン)なため、実装が甘いと Bleichenbacher の署名偽造 (BB'06) のような攻撃を許した歴史があります(公開指数 \(e=3\)
かつ検証実装がパディングを厳密チェックしない場合、平方根を取るだけで署名を捏造できた)。理論的な帰着証明も弱く、「安全性が形式的に保証されていない」方式です。それでも JWT の RS256、古い TLS 証明書、多くのレガシーシステムで今なお広く使われています。
RSA-PSS
RSA-PSS (Probabilistic Signature Scheme; RS256 に対する PS256 等) は Bellare–Rogaway による確率的パディングで、ソルト (salt) を混ぜて署名ごとに異なる出力を作ります。MGF1(マスク生成関数)でハッシュを引き延ばし、EMSA-PSS エンコーディングを施してから累乗します。
\[ \mathrm{EM} = \mathrm{EMSA\text{-}PSS\text{-}ENCODE}(H(m), \text{salt}), \qquad \sigma = \mathrm{EM}^{d} \bmod N \tag{14} \]PSS が推奨される理由:
- 証明可能安全性: ランダムオラクルモデルで、RSA 仮定に厳密に帰着する EUF-CMA 安全性が証明されている。v1.5 にはこの保証がない。
- ソルトによる頑健性: 確率的なので、同じメッセージでも毎回異なる署名になり、決定的パディングを狙う攻撃面が消える。
- 標準の推奨: NIST FIPS 186-5、TLS 1.3、最新の PKI は PSS を推奨・要求する方向。
新規に RSA 署名を実装するなら PSS 一択です。PKCS#1 v1.5 は既存互換性のためだけに残すべきレガシーと位置づけられます。
三方式の比較
| 項目 | RSA-PSS (3072 bit) | ECDSA (P-256) | Ed25519 |
|---|---|---|---|
| 依拠する困難性 | 素因数分解 / RSA 仮定 | ECDLP | ECDLP (Curve25519) |
| 公開鍵長 | 384 byte | 33 byte (圧縮) | 32 byte |
| 署名長 | 384 byte | 約 64–72 byte (DER) | 64 byte |
| 署名速度 | 遅い | 速い | 非常に速い |
| 検証速度 | 非常に速い | 標準 | 速い(バッチ検証可) |
| 実装難易度 | 中(パディングが要) | 高(nonce 事故が致命的) | 低(misuse-resistant) |
| 決定的 | いいえ(PSS はソルト) | 乱数(RFC 6979 で決定的化) | はい(内蔵) |
| 主な用途 | レガシー TLS 証明書, JWT | TLS, SSH, 暗号通貨, JWT | SSH, Signal, WireGuard, Git |
大まかな使い分けの指針:
- 新規プロジェクト: 署名は Ed25519 を第一候補に。曲線互換や既存 PKI の都合があれば ECDSA (P-256)。
- RSA が必須の環境(HSM や古い認証局): RSA-PSS を選び、v1.5 は避ける。
- 検証が支配的なワークロード(証明書を大量検証するクライアント): RSA の検証は \(e = 65537\) の軽い累乗なので有利な場合がある。
- 暗号通貨: Bitcoin / Ethereum は secp256k1 上の ECDSA(Bitcoin は Taproot 以降 Schnorr も採用)。
用途例として、
OAuth 2.0 / OIDC
の JWT では RS256(RSA v1.5)・PS256(RSA-PSS)・ES256(P-256 ECDSA)・EdDSA(Ed25519)がアルゴリズム識別子として使われます。
Python 実装
1. 教育用フルスクラッチ ECDSA(実行検証済み)
小さな素体上の曲線 \(y^2 = x^3 + 2x + 2 \pmod{17}\) 、ベースポイント \(G = (5, 1)\) (位数 \(n = 19\) )で ECDSA を実装します。数式 (4)〜(6) をそのまま写し、 double-and-add でスカラー倍算します。このコードは実際に実行し、署名生成→検証成功→改ざん検知、さらに nonce 再利用による鍵復元まで確認済みです。
import hashlib
# --- 小さな素体上の曲線: y^2 = x^3 + 2x + 2 over F_17 ---
p = 17
a, b = 2, 2
G = (5, 1) # ベースポイント
n = 19 # G の位数 (この点は位数 19)
O = None # 無限遠点
def ec_add(P, Q):
"""楕円曲線上の点加算 (逆元は pow(x, -1, p))"""
if P is O:
return Q
if Q is O:
return P
x1, y1 = P
x2, y2 = Q
if x1 == x2 and (y1 + y2) % p == 0:
return O # P + (-P) = O
if P == Q:
lam = (3 * x1 * x1 + a) * pow(2 * y1, -1, p) % p # 接線
else:
lam = (y2 - y1) * pow(x2 - x1, -1, p) % p # 弦
x3 = (lam * lam - x1 - x2) % p
y3 = (lam * (x1 - x3) - y1) % p
return (x3, y3)
def scalar_mult(k, P):
"""double-and-add: O(log k) 回の点演算で kP"""
R, Q = O, P
while k:
if k & 1:
R = ec_add(R, Q)
Q = ec_add(Q, Q)
k >>= 1
return R
def hash_to_int(msg):
return int.from_bytes(hashlib.sha256(msg).digest(), "big") % n
def sign(d, msg, k):
"""署名生成: s = k^{-1}(e + rd) mod n … 式 (4)"""
e = hash_to_int(msg)
r = scalar_mult(k, G)[0] % n
assert r != 0
s = pow(k, -1, n) * (e + r * d) % n
assert s != 0
return (r, s)
def verify(Q, msg, sig):
"""署名検証: u1 G + u2 Q の x 座標が r か … 式 (5), (6)"""
r, s = sig
if not (1 <= r < n and 1 <= s < n):
return False
e = hash_to_int(msg)
w = pow(s, -1, n)
u1, u2 = e * w % n, r * w % n
X = ec_add(scalar_mult(u1, G), scalar_mult(u2, Q))
return X is not O and X[0] % n == r
# --- 鍵生成 ---
d = 7 # 秘密鍵
Q = scalar_mult(d, G) # 公開鍵 Q = dG
assert scalar_mult(n, G) is O # G の位数が n であることの確認
# --- 署名と検証 ---
msg = b"transfer 1 coin to Bob"
sig = sign(d, msg, k=11)
print("公開鍵 Q =", Q)
print("署名 (r, s) =", sig)
print("正当な検証 :", verify(Q, msg, sig)) # True
print("改ざん検知 :", verify(Q, b"transfer 1 coin to Eve", sig)) # False
# --- nonce 再利用 → 秘密鍵復元のデモ (式 (9)) ---
msg2 = b"transfer 2 coin to Bob"
sig2 = sign(d, msg2, k=11) # 同じ k を再利用 (危険!)
r, s1 = sig
_, s2 = sig2
e1, e2 = hash_to_int(msg), hash_to_int(msg2)
k_rec = (e1 - e2) * pow(s1 - s2, -1, n) % n
d_rec = (s1 * k_rec - e1) * pow(r, -1, n) % n
print("復元した nonce k =", k_rec, "(真値 11)")
print("復元した秘密鍵 d =", d_rec, "(真値 7)")
実行結果:
公開鍵 Q = (0, 6)
署名 (r, s) = (13, 14)
正当な検証 : True
改ざん検知 : False
復元した nonce k = 11 (真値 11)
復元した秘密鍵 d = 7 (真値 7)
正当な署名は受理され、1 文字書き換えたメッセージは棄却されます。さらに、同じ nonce \(k = 11\) を 2 回使っただけで、式 (9) の連立方程式から秘密鍵 \(d = 7\) が完全に復元されてしまうことが確認できます。これが PlayStation 3 事件の原理そのものです。なおこの実装は教育用で、実際の曲線の巨大素数・定数時間演算・DER エンコード・厳密な nonce 生成は省いています。
漏洩の幾何学的な理由は単純です。署名生成の 1 行目 r = scalar_mult(k, G)[0] % n が示すとおり、\(r\)
は \(k\)
だけから決まる点 \(R = kG\)
の \(x\)
座標です。同じ \(k\)
を使えば、メッセージが違っても \(R\)
(したがって \(r\)
)は完全に同一になります。下図は今回の小さな曲線 \(\mathbb{F}_{17}\)
上の全 18 点をプロットし、ベースポイント \(G\)
・公開鍵 \(Q = dG\)
・そして 2 回の署名が共有する \(R = kG\)
を示したものです。\(R\)
が 1 点に固定されることで、式 (8) の 2 本の方程式が \(k\)
について線形従属になり、引き算だけで \(k\)
、続いて \(d\)
が解けてしまいます。

2. 実用ライブラリ: cryptography による ECDSA / Ed25519 / RSA-PSS(実行検証済み)
本番では検証済みライブラリを使います。以下は cryptography (v49.0) での三方式の署名・検証例で、実際に pip install cryptography 環境で実行し、3 方式すべての署名が正しく検証できることを確認済みです。
# pip install cryptography で導入。以下は実際に実行し出力を確認済み。
from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
msg = b"contract: pay 100 USD"
# --- (a) ECDSA (P-256): nonce は RFC 6979 系で内部的に安全生成 ---
ec_key = ec.generate_private_key(ec.SECP256R1())
ec_sig = ec_key.sign(msg, ec.ECDSA(hashes.SHA256()))
ec_key.public_key().verify(ec_sig, msg, ec.ECDSA(hashes.SHA256()))
# --- (b) Ed25519: 決定的署名・API は極めてミニマル ---
ed_key = ed25519.Ed25519PrivateKey.generate()
ed_sig = ed_key.sign(msg) # ハッシュ指定も不要
ed_key.public_key().verify(ed_sig, msg) # 失敗時 InvalidSignature
# --- (c) RSA-PSS: v1.5 ではなく PSS + MGF1 を使う ---
rsa_key = rsa.generate_private_key(public_exponent=65537, key_size=3072)
pss = padding.PSS(mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH)
rsa_sig = rsa_key.sign(msg, pss, hashes.SHA256())
try:
rsa_key.public_key().verify(rsa_sig, msg, pss, hashes.SHA256())
print("all signatures verified")
except InvalidSignature:
print("verification failed")
実行結果:
all signatures verified
3 方式とも署名生成 → 検証成功まで実際に確認できました(署名長は ECDSA が 71 byte (DER, P-256)、Ed25519 が 64 byte、RSA-PSS (3072 bit) が 384 byte)。ポイント: (a) cryptography の ECDSA は nonce 生成を内部で安全に処理するため、利用側で \(k\)
を扱う余地がありません。(b) Ed25519 はハッシュ・曲線・nonce の選択肢すら無く、sign(msg) / verify(sig, msg) だけの最小 API で「間違えようがない」設計です。(c) RSA では padding.PSS(...) を明示し、レガシーな padding.PKCS1v15() を新規では避けます。フルスクラッチ実装が踏む地雷(nonce 事故・パディングオラクル)をライブラリ層が吸収してくれるのが、本番でライブラリを使う最大の理由です。
3. RSA-PSS のソルトは毎回異なる署名を作る(実行検証)
本文で述べたとおり、RSA-PSS は署名のたびにランダムなソルトを混ぜるため、同じメッセージ・同じ鍵でも署名バイト列は毎回変わります。これは PKCS#1 v1.5 との決定的な違いです。次のコードで、同じメッセージを PSS で 3 回・PKCS#1 v1.5 で 3 回署名し、実際にバイト列を比較します。
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
msg = b"contract: pay 100 USD"
key = rsa.generate_private_key(public_exponent=65537, key_size=3072)
pss = padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH)
pkcs = padding.PKCS1v15()
pss_sigs = [key.sign(msg, pss, hashes.SHA256()) for _ in range(3)]
pkcs_sigs = [key.sign(msg, pkcs, hashes.SHA256()) for _ in range(3)]
print("PSS: 署名1本目と2本目が同一か =", pss_sigs[0] == pss_sigs[1])
print("PSS: 署名2本目と3本目が同一か =", pss_sigs[1] == pss_sigs[2])
for i, s in enumerate(pss_sigs):
print(f"PSS 署名{i+1} 先頭16byte =", s[:16].hex())
print("PKCS#1v1.5: 3本とも同一か =", pkcs_sigs[0] == pkcs_sigs[1] == pkcs_sigs[2])
print("PKCS#1v1.5 署名 先頭16byte =", pkcs_sigs[0][:16].hex())
pub = key.public_key()
for s in pss_sigs:
pub.verify(s, msg, pss, hashes.SHA256())
print("3本のPSS署名すべてが検証成功 = True")
実行結果:
PSS: 署名1本目と2本目が同一か = False
PSS: 署名2本目と3本目が同一か = False
PSS 署名1 先頭16byte = 8cdb06bda9d86ba627170766e2a8f18c
PSS 署名2 先頭16byte = 64700c4da2807f7118e925c1d3beda94
PSS 署名3 先頭16byte = 231abcedf07f9e46e73337903a07107f
PKCS#1v1.5: 3本とも同一か = True
PKCS#1v1.5 署名 先頭16byte = 71585006d01054e887ceb8dd1f42f458
同じ秘密鍵・同じメッセージにもかかわらず、PSS の 3 つの署名は先頭バイトからして完全に異なります。一方 PKCS#1 v1.5 は 3 回ともバイト単位で完全一致します(決定的パディングだから当然ですが、これが構造化パディングを狙う攻撃の足がかりになります)。そして PSS の 3 署名はいずれも verify() に成功しており、「毎回形が変わるのに、どれも正当な署名として受理される」という PSS の確率的安全性が実測で確認できました。
4. 三方式の署名・検証速度をベンチマーク(実行検証)
最後に、同一メッセージに対する RSA-PSS (3072 bit) / ECDSA (P-256) / Ed25519 の署名・検証時間を実測します(time.perf_counter()、ウォームアップ20回のあと 200 回計測した中央値、Python 3.14 / cryptography 49.0、単一マシンでの計測につき絶対値はハードウェア依存)。
import time, statistics
from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa
from cryptography.hazmat.primitives import hashes
msg = b"contract: pay 100 USD"
N, WARMUP = 200, 20
def bench(sign_fn, verify_fn):
for _ in range(WARMUP):
verify_fn(sign_fn())
t_sign = []
sig = None
for _ in range(N):
t0 = time.perf_counter()
sig = sign_fn()
t_sign.append(time.perf_counter() - t0)
t_verify = []
for _ in range(N):
t0 = time.perf_counter()
verify_fn(sig)
t_verify.append(time.perf_counter() - t0)
return statistics.median(t_sign) * 1000, statistics.median(t_verify) * 1000
rsa_key = rsa.generate_private_key(public_exponent=65537, key_size=3072)
pss = padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH)
rsa_s, rsa_v = bench(lambda: rsa_key.sign(msg, pss, hashes.SHA256()),
lambda sig: rsa_key.public_key().verify(sig, msg, pss, hashes.SHA256()))
ec_key = ec.generate_private_key(ec.SECP256R1())
ec_s, ec_v = bench(lambda: ec_key.sign(msg, ec.ECDSA(hashes.SHA256())),
lambda sig: ec_key.public_key().verify(sig, msg, ec.ECDSA(hashes.SHA256())))
ed_key = ed25519.Ed25519PrivateKey.generate()
ed_s, ed_v = bench(lambda: ed_key.sign(msg), lambda sig: ed_key.public_key().verify(sig, msg))
print(f"RSA-PSS(3072): sign={rsa_s:.4f} ms verify={rsa_v:.4f} ms")
print(f"ECDSA(P-256): sign={ec_s:.4f} ms verify={ec_v:.4f} ms")
print(f"Ed25519: sign={ed_s:.4f} ms verify={ed_v:.4f} ms")
実行結果:
RSA-PSS(3072): sign=2.6224 ms verify=0.1067 ms
ECDSA(P-256): sign=0.0256 ms verify=0.0701 ms
Ed25519: sign=0.0919 ms verify=0.2025 ms

比較表で述べた「RSA は署名が遅く検証が速い」「ECDSA / Ed25519 は署名が速い」という傾向は、この実測でも裏付けられました。RSA-PSS の署名(2.62 ms)は ECDSA(0.026 ms)やEd25519(0.092 ms)よりおよそ 30〜100 倍遅い一方、検証(0.107 ms)は 3 方式中最速に近い値です。なお ECDSA と Ed25519 の大小関係は実装・CPU・OpenSSL のバージョンに強く依存します。今回の環境(OpenSSL バックエンドの cryptography)では P-256 の署名が Ed25519 よりわずかに速い結果になりましたが、これは OpenSSL の P-256 に高度に最適化されたアセンブリ実装があるためで、「Ed25519 は理論上つねに ECDSA より速い」という単純な主張はできません。実務での選定は、この記事で述べた実装ミス耐性(misuse-resistance)や決定性の観点を優先し、性能差は二次的な判断材料とするのが妥当です。
最近の研究動向 (2024–2025)
ECDSA の nonce 問題は理論上の懸念にとどまらず、2024–2025 年も現実の脆弱性・攻撃研究として動き続けています。
- CVE-2024-31497 (Bäumer & Brinkmann, 2024): Ruhr 大学ボーフムの研究者が、SSH クライアント PuTTY (v0.68–0.80) の NIST P-521 ECDSA 署名で nonce の上位 9 ビットが常にゼロというバイアスを発見しました。約 60 個の署名(Git のコミット署名や公開サーバとの通信から収集可能)があれば秘密鍵を完全に復元できると報告されており、同じ nonce 生成コードを使う FileZilla・WinSCP・TortoiseGit・TortoiseSVN も影響を受けました。本記事で解説した「(b) k のバイアス」が理論ではなく2024年に実際に発見・悪用可能だった実例です。PuTTY 0.81 で RFC 6979 方式へ切り替えて修正されています。
- Gao, Wang, Hu, & He (2024): “Attacking ECDSA with Nonce Leakage by Lattice Sieving” (ASIACRYPT 2024) は、nonce の部分的な漏洩(Hidden Number Problem)に対するフーリエ解析ベースの攻撃と格子ふるい (lattice sieving) を組み合わせ、従来より少ない漏洩ビット数・少ない署名数で秘密鍵回復を可能にする手法を示しました。本記事の格子攻撃の説明を裏付ける最新の攻撃研究です。
- Dinu (2025): “Migration to Post-Quantum Cryptography: From ECDSA to ML-DSA” (IACR ePrint 2025/2025) は、セキュアブート・TPM・ディスク暗号化などで広く使われる ECDSA 署名を、NIST が標準化した耐量子署名 ML-DSA (FIPS 204) へ移行する際の実装・サイドチャネル対策上の課題を論じています。NIST は 2024 年 8 月に ML-DSA (FIPS 204) と SLH-DSA (FIPS 205) を耐量子デジタル署名の標準として正式に発行しており、ECDSA / Ed25519 / RSA-PSS からの移行計画は今後数年の暗号実装における主要テーマになります。
これらはいずれも「nonce の乱数性・決定性が署名方式の安全性の生命線である」という本記事の主張を裏付けており、PQC 移行後も同様の実装規律(決定的導出・定数時間演算・サイドチャネル対策)が要求され続けることを示しています。
まとめ
- デジタル署名は \((\mathsf{Gen}, \mathsf{Sign}, \mathsf{Verify})\) の 3 アルゴリズムで、hash-and-sign により任意長メッセージを固定長ダイジェストに潰してから署名する。
- MAC と違い否認防止を提供する(秘密鍵の持ち主だけが署名でき、公開鍵で誰でも検証できる)。安全性の基準は EUF-CMA。
- ECDSA は式 (4)〜(6) で成立するが、nonce \(k\) の再利用・バイアスは秘密鍵の即時漏洩に直結する(PlayStation 3 事件・格子攻撃・2024年の CVE-2024-31497)。RFC 6979 の決定的 nonce で構造的に防ぐ。
- Ed25519 (EdDSA) は nonce を方式に内蔵して決定化し、実装ミス耐性で ECDSA を上回る(実測ベンチマークでは署名・検証速度の優劣は実装依存)。新規プロトコルの第一候補。
- RSA 署名は PKCS#1 v1.5 ではなく、証明可能安全性を持つ RSA-PSS を推奨。ソルトにより同じメッセージでも毎回異なる署名になることを実行検証した。
- 用途に応じて RSA-PSS / ECDSA / Ed25519 を使い分ける。TLS・JWT・SSH・暗号通貨での実例は比較表を参照。ECDSA から耐量子署名 ML-DSA への移行も今後の課題。
- Python では、学習に教育用フルスクラッチ ECDSA(nonce 再利用による鍵復元まで実行検証済み)、本番に
cryptographyのec/ed25519/padding.PSS(署名・検証・ベンチマークまで実行検証済み)を使う。
関連記事
- 暗号技術ロードマップ:DLP・素因数分解・楕円曲線から PQC まで - 暗号領域全体の学習マップ。署名は公開鍵暗号の主要応用。
- 楕円曲線暗号(ECC)の数学と Python 実装 - ECDSA / Ed25519 が乗る楕円曲線の点演算・ECDLP を詳解。
- SHA-256とHMACの理論とPython実装 - 本記事の署名方式が「メッセージそのものではなくハッシュに署名する」際に前提とするハッシュ関数の安全性を詳解しています。
- TLS 1.3ハンドシェイク解剖 - 本記事の署名方式がハンドシェイクのCertificateVerifyステップでどう使われるかの文脈を解説しています。
- SSH鍵交換の暗号技術:Diffie-HellmanからX25519までPythonで解説 - SSHのホスト鍵認証で本記事のECDSA/Ed25519署名がどう使われるかを、鍵交換ハッシュの検証ステップとして解説しています。
- RSA 暗号の理論・鍵生成・暗号化・復号を Python 実装 - RSA-PSS / PKCS#1 v1.5 署名の土台となる公開鍵暗号。
- Diffie-Hellman 鍵交換の理論と実装 - 署名と対をなす鍵交換。TLS ハンドシェイクで署名と併用される。
- AES / ChaCha20 対称鍵暗号の理論と Python 実装 - AEAD の MAC と署名の違いの対比。
- 繰り返し二乗法による高速べき乗剰余計算の Python 実装 - スカラー倍算 double-and-add と RSA 累乗の基礎。
- 楕円曲線 ElGamal 暗号の原理と Python による簡易実装 - 楕円曲線上の暗号化。ECDSA の隣接領域。
- OAuth 2.0/OIDC 入門 - JWT の署名アルゴリズム (RS256 / PS256 / ES256 / EdDSA) の実例。
- ゼロトラストセキュリティの概要と導入 - 署名ベースの PKI / mTLS を組織設計に組み込む文脈。
- セキュリティ資格比較:CISSP vs 情報処理安全確保支援士 - 署名・PKI を含む暗号分野が出題される資格。
関連ツール
- ハッシュ生成ツール (DevToolBox) - hash-and-sign のダイジェスト \(H(m)\) の計算に。
- 進数変換ツール (CalcBox) - 署名・鍵の 16 進表記の確認に。
参考文献
- Johnson, D., Menezes, A., & Vanstone, S. (2001). “The Elliptic Curve Digital Signature Algorithm (ECDSA)”. International Journal of Information Security, 1(1), 36–63.
- Bernstein, D. J., Duif, N., Lange, T., Schwabe, P., & Yang, B.-Y. (2012). “High-speed high-security signatures”. Journal of Cryptographic Engineering, 2(2), 77–89.
- Josefsson, S., & Liusvaara, I. (2017). “Edwards-Curve Digital Signature Algorithm (EdDSA)”. RFC 8032.
- Pornin, T. (2013). “Deterministic Usage of DSA and ECDSA”. RFC 6979.
- Bellare, M., & Rogaway, P. (1996). “The Exact Security of Digital Signatures — How to Sign with RSA and Rabin”. EUROCRYPT ‘96.
- Moriarty, K. et al. (2016). “PKCS #1: RSA Cryptography Specifications Version 2.2”. RFC 8017.
- NIST (2023). “Digital Signature Standard (DSS)”. FIPS 186-5.
- Bäumer, F., & Brinkmann, M. (2024). “CVE-2024-31497: Secret Key Recovery of NIST P-521 Private Keys Through Biased ECDSA Nonces in PuTTY Client”. Ruhr-Universität Bochum.
- Gao, Y., Wang, J., Hu, H., & He, B. (2024). “Attacking ECDSA with Nonce Leakage by Lattice Sieving: Bridging the Gap with Fourier Analysis-Based Attacks”. ASIACRYPT 2024.
- Dinu, D. (2025). “Migration to Post-Quantum Cryptography: From ECDSA to ML-DSA”. Cryptology ePrint Archive, Paper 2025/2025.
- NIST (2024). “Module-Lattice-Based Digital Signature Standard”. FIPS 204.