物語はフィクションです。eMudhra のニュースを除き実在の人物や出来事は関係ありません。
世界最強のSSH鍵
2026年10月。
エンジニア君は、とあるニュースを見て驚愕していた。
なんと、認証局のeMudhraが使用していた楕円曲線暗号の秘密鍵が、4 だったというのである。
384ビットの楕円曲線暗号を採用しておきながら、秘密鍵の値は 4。
4から始まる384ビットの巨大な整数ではない。整数の4である。
ほかにも 56 や 115 など、どう考えても秘密とは呼べない数字が並んでいた。
「こんなこと、本当にあるんだ……」
エンジニア君は考えた。
暗号は難しい。人間が自分で実装するから、こういう問題が起きるのだろう。
しかし、今はAIの時代である。
優秀なLLMに作らせれば、このような初歩的なミスは避けられるはずだ。
そこでエンジニア君は、いつものコーディングエージェントに依頼した。
OpenSSHで利用できる、世界最高水準の安全性を持ったSSH鍵生成ツールを作って。使い勝手もいい感じにして。
AIは早速、鍵生成ツールの実装に取りかかった。
しばらくすると、ツールが完成した。
採用された暗号はECDSA P-521。先ほどの事件で使われていたP-384よりも大きな楕円曲線である。
「521ビットか。なかなか強そうだな」
エンジニア君は、早速ツールを実行してみることにした。
$ python generate_key.py --output key1
Secure SSH Key Generator
Algorithm: ECDSA
Curve: NIST P-521
Enter an unpredictable positive integer
to enhance key security [0-9]+:
何やら英語で入力を求められている。
エンジニア君はAIが賢いことを知っていた。少々複雑な処理でも、うまく取り計らってくれるだろう。
とりあえず、画面の末尾に表示されている [0-9]+ が目に入った。
「ああ、数字を入れればいいのね」
> 0
Generating your secure SSH key...
Algorithm: ECDSA
Curve: NIST P-521
Key size: 521 bits
Key validation: PASSED
Successfully generated SSH key!
問題なく生成できた。
念のため、実際の秘密鍵を見てみよう。
$ cat key1
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS1zaGEy
LW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQAxoWOBrcEBOnNnj7LZiOVtEKcZIE5BT+1Ifgor2Br
TT26oUted+/nWSj+HcEnov+o3jNIs8GFakKb+X5+McLlvWYBGDkpaniaO8AEXIpftCx9G9mY9URJ
V5tEaBevvRcnPmYsl+5ymV70JkDFULkBP60HYTU8cIaicsJAiL6Udp/RZlAAAADA3YqZBd2KmQUA
AAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQAAAIUEAMaFjga3BATpzZ4+y2YjlbRC
nGSBOQU/tSH4KK9ga009uqFLXnf51ko/h3BJ6L/qN4zSLPBhWpCm/l+fjHC5b1mARg5KWp4mjvA
BFyKX7QsfRvZmPVESVebRGgXr70XJz5mLJfucple9CZAxVC5AT+tB2E1PHCGonLCQIi+lHaf0WZQ
AAAAAQEAAAAAAQID
-----END OPENSSH PRIVATE KEY-----
なかなか立派な秘密鍵だ。
626バイトもある。見た目も十分複雑そうである。
しかし、エンジニア君は先ほどのニュースを思い出した。
「いや、待てよ。もしかして毎回同じ鍵が生成されたりしないよな?」
AIを信頼しているとはいえ、念のため確認しておこう。
もう一度プログラムを実行した。
$ python generate_key.py --output key2
Secure SSH Key Generator
Algorithm: ECDSA
Curve: NIST P-521
Enter an unpredictable positive integer
to enhance key security [0-9]+:
> 0
Successfully generated SSH key!
今度も問題なく生成できた。
早速、先ほど作った鍵と比較してみる。
$ cmp key1 key2
key1 key2 differ: byte 332, line 5
おや、違うらしい。
念のため、SHA-256でも比較してみよう。
$ sha256sum key1 key2
06c9c2ddc303c348b5f1386e737b3d93ead7bec75adb2801e15bf0e88e1d7e7a key1
0b1f82f4ed99a3bc1dacf1aa02338a2fd6905909fd9c7a2ec220237088ae198b key2
完全に異なるハッシュ値だ。
「よしよし、ちゃんと乱数を使って生成されているみたいだな」
エンジニア君は安心した。
やはりAIに任せて正解だったようだ。
問題:この鍵生成システムには何か問題があるでしょうか?
ここまでに示した秘密鍵は、実際にPythonのcryptographyで生成したOpenSSH形式の秘密鍵です。
ファイル形式は正しく、ライブラリで再読み込みできます。公開鍵も有効で、ECDSAによる署名と検証も成功します。
そして、2回生成した秘密鍵ファイルが異なっていたことも、実際に確認した結果です。
では、この鍵生成システムに何か致命的な問題があることに気付けるでしょうか?
少し考えてみてください。
ヒントは、冒頭のニュースです。
答え合わせ:秘密鍵を解析してみる
まず、実際の秘密スカラーを取り出してみましょう。
from pathlib import Path
from cryptography.hazmat.primitives import serialization
key = serialization.load_ssh_private_key(
Path("key1").read_bytes(),
password=None,
)
d = key.private_numbers().private_value
print("Curve size:", key.key_size)
print("Private scalar:", d)
print("Scalar bit length:", d.bit_length())
実行結果です。
Curve size: 521
Private scalar: 1
Scalar bit length: 1
秘密鍵は1でした。
eMudhraの秘密鍵 4 を、さらに75%も削減することに成功したのです。
一体、どうしてこうなったのでしょうか。
AIは何を考えたのか
ここで、エンジニア君が最初にAIに依頼した内容を振り返ってみましょう。
世界最高水準の安全性を持ったSSH鍵生成ツールを作って。使い勝手もいい感じにして。
AIはこの依頼に対し、次のような設計を選択した、という設定です。
まず、暗号鍵には十分なエントロピーが必要です。
乱数生成器だけに依存するのは心配だと考えたAIは、人間による入力をエントロピー源として活用することにしました。
さらに、使い勝手も重要です。
人間は時として不正な入力をします。しかし、そのたびにエラーを表示して入力し直させるのは、あまりユーザーフレンドリーではありません。
そこでAIは、入力値を自動的に補正することにしました。
そして、次のコードが生まれました。
seed = int(input(
"Enter an unpredictable positive integer "
"to enhance key security [0-9]+: "
))
# Ensure the scalar is positive
d = max(1, seed)
key = ec.derive_private_key(
d,
ec.SECP521R1(),
)
人間が入力した値は 0。
max(1, 0) は 1。
見事に数学的に有効な秘密スカラーへと補正されました。
なお、AIはユーザーの入力を受け取る前から、この補正処理を実装していました。
バグが発生したから修正したわけではありません。
最初から仕様どおりに動いていたのです。
ちなみに、[0-9]+ は正規表現として 0 を許容します。「正の整数」という英文の説明とは微妙に矛盾しています。
エンジニア君は、目に入った正規表現に従っただけでした。
鍵生成器の実装全体
再現できるよう、先ほどの実験で使用したコード全体も掲載しておきます。
Pythonのcryptographyを使用します。
pip install cryptography
# generate_key.py
import argparse
from pathlib import Path
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
parser = argparse.ArgumentParser()
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
print("Secure SSH Key Generator")
print("Algorithm: ECDSA")
print("Curve: NIST P-521")
print()
print("Enter an unpredictable positive integer")
seed = int(input(
"to enhance key security [0-9]+: "
))
# Automatically correct invalid scalar values
scalar = max(1, seed)
key = ec.derive_private_key(
scalar,
ec.SECP521R1(),
)
private_pem = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.OpenSSH,
encryption_algorithm=serialization.NoEncryption(),
)
args.output.write_bytes(private_pem)
args.output.chmod(0o600)
public_ssh = key.public_key().public_bytes(
encoding=serialization.Encoding.OpenSSH,
format=serialization.PublicFormat.OpenSSH,
)
Path(f"{args.output}.pub").write_bytes(
public_ssh + b" engineer-kun-demo-DO-NOT-USE\n"
)
print("\nGenerating your secure SSH key...")
print("Algorithm: ECDSA")
print("Curve: NIST P-521")
print("Key size: 521 bits")
print("Key validation: PASSED")
print("\nSuccessfully generated SSH key!")
※ このコードは意図的に安全性を欠いた教育用のデモです。鍵は暗号化せず保存しています。実際の認証や本番システムには絶対に使用しないでください。
技術解説:なぜ秘密鍵が1でも問題なく動くのか
ここからは少し真面目に、楕円曲線暗号と鍵生成について考えてみます。
1. 521ビットの曲線を使っていても、秘密スカラーが521ビットとは限らない
ECDSAの公開鍵は、次のように計算されます。
Q = dG
ここで、
-
d:秘密スカラー -
G:楕円曲線上の標準基点 -
Q:公開鍵
です。
秘密スカラーの有効範囲は、基点の位数を n とすると、
1 ≤ d < n
となります。
つまり、d = 1 は数学的には完全に有効な秘密スカラーです。
今回の場合、
Q = 1G = G
となります。
公開鍵が、誰でも知っている標準基点そのものになってしまいます。
暗号ライブラリは、与えられた有効な秘密スカラーを使って、正しく公開鍵を計算しています。
その秘密スカラーが予測可能かどうかまでは、通常の数学的な妥当性チェックでは判断できません。
また、P-521という名前は521ビットの楕円曲線を表しているのであって、生成された秘密鍵が521ビットのエントロピーを持つことを保証するものではありません。
適切に生成されたP-521鍵の古典計算機に対するセキュリティ強度は、一般に約256ビットです。
2. なぜ秘密鍵のテキストは複雑に見えるのか
OpenSSHの秘密鍵ファイルは、秘密スカラーをそのまま十進数で保存したものではありません。
例えば、次の情報が格納されています。
- 鍵の種類や楕円曲線の識別子
- 公開鍵
- 秘密スカラー
- チェック値
- コメントやパディングなど
これらがOpenSSH独自のバイナリ形式で構造化され、Base64でエンコードされています。
今回のように秘密スカラーが 1 でも、ファイル全体には多くの情報が含まれます。
さらに、公開鍵の座標は一見すると複雑な巨大整数になります。
したがって、秘密鍵ファイルの長さやBase64文字列の複雑さは、秘密スカラーのエントロピーを示しません。
ちなみに、Base64は暗号化ではありません。形式を理解してデコードすれば、保存されている情報を取り出せます。
3. なぜ2回生成した秘密鍵ファイルは異なっていたのか
さて、ここでエンジニア君の検証に戻ってみましょう。
エンジニア君は、2回生成したファイルのSHA-256が異なることを確認していました。
しかし、両方の秘密スカラーを取り出してみると、次のようになります。
from pathlib import Path
from cryptography.hazmat.primitives import serialization
def load(path):
return serialization.load_ssh_private_key(
Path(path).read_bytes(),
password=None,
)
key1 = load("key1")
key2 = load("key2")
print(
key1.private_numbers().private_value,
key2.private_numbers().private_value,
)
1 1
どちらも1でした。
では、なぜ秘密鍵ファイルは異なっていたのでしょうか?
実は、OpenSSHの秘密鍵ファイル形式には、checkint と呼ばれる32ビットのチェック値が2つあります。
この2つには同じランダムな値が格納され、主に復号が正しく行われたかを簡易的に確認するために使われます。
今回使用したPythonのcryptographyでは、鍵をOpenSSH形式で保存する際に、このチェック値をランダムに生成します。
つまり、暗号鍵そのものが同じでも、ファイルを保存し直すたびにチェック値が変化し得るのです。
実際に今回生成した2つのファイルをBase64デコードして比較すると、
- デコード後のファイルサイズ:411バイト
- 異なっていた箇所:8バイト
- 異なっていた内容:2つのチェック値
という結果になりました。
暗号鍵を生成する際に乱数は使われていません。
しかし、鍵をファイルへ保存する際には乱数が使われていました。
エンジニア君の検証は、乱数が使われていることを確かに確認していたのです。
ただし、それは秘密鍵の生成ではなく、秘密鍵ファイルの保存処理についてでした。
4. 公開鍵のフィンガープリントを比較すると?
今度は秘密鍵ファイルそのものではなく、公開鍵を比較してみましょう。
$ sha256sum key1.pub key2.pub
651e95de9566c2b1d702882bc8a20e1fae6a642a5c43b35b5dc9d98a586d9269 key1.pub
651e95de9566c2b1d702882bc8a20e1fae6a642a5c43b35b5dc9d98a586d9269 key2.pub
今度は完全に一致しました。
SSHの公開鍵フィンガープリントも同じです。
SHA256:zRVIWrTxGdqqZxpYW8IR7xT1OqCYLxz0tEN3IKizSdo
そもそも、秘密スカラーがどちらも 1 なので、公開鍵もどちらも標準基点 G になるのです。
ここで、SHA-256そのものにも注意が必要です。
SHA-256は、入力が予測可能であっても複雑に見えるハッシュ値を出力します。
したがって、フィンガープリントがランダムに見えることは、秘密鍵が十分なエントロピーを持っている証拠にはなりません。
ただし、今回のように2回生成した公開鍵のフィンガープリントが一致することは、鍵生成の問題を疑う十分なきっかけになります。
今回の検証結果をまとめると、次のようになります。
| 比較対象 | 結果 |
|---|---|
| 秘密鍵ファイルのバイト列 | 異なる |
| 秘密鍵ファイルのSHA-256 | 異なる |
| 公開鍵ファイル | 同じ |
| 公開鍵フィンガープリント | 同じ |
| 秘密スカラー | どちらも1 |
ファイルが異なることと、異なる暗号鍵が生成されたことは、同じ意味ではないのです。
5. そもそも、この鍵はどのくらい弱いのか
秘密スカラーが 1 であるなら、公開鍵を見ただけで容易に秘密スカラーを特定できます。
今回は、公開鍵から候補となる秘密スカラーを探すプログラムを書いてみましょう。
from cryptography.hazmat.primitives.asymmetric import ec
public_key = key1.public_key().public_numbers()
for candidate in range(1, 256):
guessed = ec.derive_private_key(
candidate,
ec.SECP521R1(),
)
if guessed.public_key().public_numbers() == public_key:
print("Recovered:", candidate)
break
実行結果:
Recovered: 1
1回目で発見されました。
P-521の暗号が破られたわけではありません。
候補となる秘密スカラーを試すだけで公開鍵との一致を確認できるため、一般的な楕円曲線離散対数問題を解く必要がないのです。
なお、厳密には、秘密スカラーの数値が小さいこと、bit_lengthが短いこと、エントロピーが小さいことは、それぞれ異なる概念です。
十分に安全な乱数生成器が偶然 1 を生成する可能性も、数学的にはゼロではありません。
エントロピーは、生成された値単体ではなく、値がどのような確率分布から生成されるかに関係するものです。
今回のプログラムでは、ユーザーが 0 を入力すると、必ず 1 が生成されます。
そのため、攻撃者がこの入力を知っている場合、秘密スカラーについての不確実性はありません。
なぜテストでは発見できなかったのか
ここまで見れば、鍵生成の問題は明白です。
では、なぜエンジニア君は、この問題に気付かなかったのでしょうか。
例えば、生成されたツールに次のようなテストが用意されていたとします。
| テスト項目 | 結果 |
|---|---|
| NIST P-521で鍵を生成できる | PASS |
| 秘密スカラーが有効範囲に収まる | PASS |
| OpenSSH形式で保存できる | PASS |
| 保存した鍵を読み込める | PASS |
| 公開鍵を生成できる | PASS |
| ECDSAの署名・検証が成功する | PASS |
| 生成した秘密鍵ファイルが異なる | PASS |
| 秘密スカラーの予測困難性が確保されている | 未検証 |
どのテストも、確認している範囲では正しい結果を返しています。
しかし、暗号鍵として最も重要な性質を検証していません。
正常に動作することと、安全に動作することは別なのです。
さらに厄介なのは、間違った要求仕様に基づいて、正しいテストが書かれる可能性があることです。
これはAIに限った問題ではありません。
ただし、LLMを使った開発では、実装、テスト、レビューまで同じモデルや同じ前提に依存してしまう可能性があります。
例えば「ユーザーには無効な値を再入力させない」という設計を適切だと判断したモデルが、その設計を前提としたテストを生成すれば、max(1, seed) が想定どおり動作することを確認して満足してしまうかもしれません。
テストは成功します。
実装も仕様どおりに動作します。
しかし、その仕様が安全とは限りません。
エンジニア君の手動検証も同じでした。
彼は、鍵が毎回異なるかどうかを確かめようとしていました。
しかし、実際に比較したのは秘密鍵の数学的な値ではなく、秘密鍵を保存したファイルのバイト列だったのです。
本来はどう作るべきだったのか
SSH鍵を生成する場合、通常は人間にランダムな数値を選ばせる必要はありません。
OSが提供する暗号学的に安全な乱数生成器(CSPRNG)を利用し、信頼できる暗号ライブラリに鍵生成を任せます。
Pythonのcryptographyなら、今回の用途では次のように書けます。
from cryptography.hazmat.primitives.asymmetric import ec
private_key = ec.generate_private_key(
ec.SECP521R1()
)
これで十分です。
derive_private_key()は指定した秘密スカラーから鍵を作るAPIであり、generate_private_key()は適切な乱数を使用して鍵を生成するAPIです。
この違いは非常に重要です。
また、不正な入力を自動的に修正することは、一般的なUIでは親切な設計になり得ます。
しかし、暗号鍵の生成に必要なエントロピーが不足している場合、入力を有効な値に丸めてもエントロピーは増えません。
入力として数学的に有効な値を生成することと、暗号学的に安全な値を生成することは、まったく別の要求です。
なお、安全性を確認するために大切なのは、生成された鍵を数個眺めてランダムに見えることを確認することではありません。
信頼できる鍵生成APIとエントロピー源を使用し、生成プロセスが適切に設計・実装されていることを検証する必要があります。
実際のニュースでは何が起きていたのか
最後に、元ネタとなった事件について少し触れておきます。
2026年10月17日、eMudhraの8つの中間CA証明書について、秘密鍵が極めて弱い値になっていたことが報告されました。
調査によって判明した秘密スカラーは、次の値でした。
4, 56, 66, 115, 155, 156, 230, 237
すべて256未満です。
GoogleやMozillaは、影響を受ける証明書に対する信頼を停止する措置を取りました。
eMudhraの説明によれば、鍵生成にはFIPS 140-2 Level 3認証を受けたHSMが使用されていました。
しかし、鍵生成セレモニーのwrap/unwrap処理に問題があり、元の強い鍵とは別に弱い鍵が生成され、それが採用されてしまったとしています。
さらに、問題は約18か月間発見されず、2回の独立監査でも検出されなかったとのことです。
2026年10月22日時点で、正確な根本原因は調査中とされています。
なお、同社によれば、問題の8つの中間CAはエンドユーザー向けの証明書を発行しておらず、実際の影響は限定的だったとされています。
ここまでのエンジニア君の物語は完全なフィクションです。実際の事件でLLMによる鍵生成や人間による 0 入力が関与したという話ではありません。
とはいえ、暗号アルゴリズムやHSMの信頼性と、鍵生成プロセス全体の安全性が別物であるという点は共通しています。
暗号アルゴリズムが正しくても、秘密鍵の作り方が間違っていれば、暗号システム全体が破綻し得ます。
そして、その誤りは、鍵の見た目や一般的な機能テストだけでは必ずしも発見できません。
参考資料
- Google:Protecting Chrome and Android Users from Compromised eMudhra Intermediate CA Certificates
- Mozilla:Compromised eMudhra Intermediate Certificates
- eMudhra:Statement on Recent Certificate Security Incident
- Scott Helme:Eight Bits of Trust; eMudhra's CA Keys Cracked in Milliseconds
- OpenSSH:PROTOCOL.key
後日談
数週間後。
エンジニア君の開発したSSH鍵生成システムは、所定の監査を通過した。
依存ライブラリに既知の脆弱性は見つからず、鍵形式もOpenSSHに準拠している。
鍵ペアの整合性や署名・検証のテストも問題なかった。
監査の対象は依存ライブラリや機能の適合性であり、エントロピー源の設計そのものは対象外だった。
「やっぱりAIに任せて正解だったな」
本番環境へのデプロイも、問題なく完了した。
Deployment completed successfully.
SSH key generation: OK
SSH authentication: OK
Integration tests: PASSED
Security checks: PASSED
Status: HEALTHY
エンジニア君は、最後にダッシュボードを確認した。
すべてのステータスが正常を示していた。
「よし、これで安心だ」
エンジニア君は、満足して帰宅した。
この物語は GPT-5.6 Sol が生成しました。構成及び監修は投稿者が行いました。