eMudhraの中間CA秘密鍵は255通りで、公開証明書から復元できた
目次
TL;DR
事象 インドの認証局eMudhraの中間CA証明書8枚(TLS用6、コード署名用2)で、P-384の秘密鍵が1〜255の整数。公開されている証明書だけで誰でも鍵を計算できる状態
ブラウザ Chrome・Firefoxは2026年10月17日(UTC)に遮断。AppleとMicrosoftも19〜20日に遮断。遮断前のCTログ(証明書の公開ログ)に該当する証明書はなく、CTを要求するブラウザでは攻撃が成立しなかったとの説明
影響範囲 ブラウザ以外のTLSクライアント。手元のMacでは、macOS標準の /usr/bin/curl、Node.js v25.3.0、uv 0.13.3以前がテスト用サイトの危殆化した証明書チェーンでエラーを出さずに接続
対応 uv 0.13.4、certifi 2026.10.18以降、pip 26.2.2、Go 1.27.3 / 1.26.10、各ディストリビューションの ca-certificates 更新。Node.jsは2026年10月21日時点で修正待ち
インドの認証局(CA)eMudhraが発行した中間CA証明書8枚について、誰でも秘密鍵を計算できたことが2026年10月17日に報告された。
認証局はWebサイトの証明書に「このドメインを管理しているのはこの鍵の持ち主だ」と署名する組織で、ブラウザやOSはその署名を信じて接続先を本物と判断する。
署名に使う中間CAの秘密鍵を第三者が持てば、その中間CAに許された範囲で、任意のドメインの証明書を発行できてしまう。
P-384という暗号方式が破られたのではなく、鍵の作り方に問題があった。
P-384(384ビットの楕円曲線)の秘密鍵は本来ランダムな整数で、候補は約2の384乗通りある。それがMozillaの調査では、8本とも48バイトのうち最後の1バイト以外がすべてゼロで、値は256未満の整数だった(Mozilla Bugzilla 2083449)。
候補が255通りしかないので、総当たりでも一瞬で計算できる。
ブラウザは報告から数時間で遮断したが、パッケージマネージャーやCLIのほうはどうなのかが気になった。
そこで手元のM4 Mac miniで鍵を実際に復元し、いくつかのTLSクライアントがテスト用サイトの危殆化した証明書チェーンを受け入れるかを試した。
影響を受けた8枚の中間CA
ブラウザやOSは、サイトの証明書から中間CA、ルート証明書へと署名の連鎖(チェーン)をたどり、最後のルートを信頼しているかで接続先を判断する。
中間CAはルートの下にあり、サイトの証明書に実際に署名する役目を持つ。どのルートを信頼するかは、Chrome、Mozilla、Microsoft、Appleがそれぞれ運営するルートプログラム(信頼するルート証明書を審査して決める制度)で決まっている。
eMudhraは2025年4月30日に、24枚の中間CAをまとめて作る鍵生成セレモニー(立会人の下で決まった手順に沿って鍵を作る作業)を行っていて、このうちP-384鍵のB系列(G3B)とC系列(G3C)の8枚が対象になった。
| 種類 | 中間CA | 用途 | 信頼していた主なルートプログラム |
|---|---|---|---|
| TLS | EM DV / OV / EV TLS CA - G3B | Webサイト証明書 | Chrome、Mozilla、Microsoft、Apple |
| TLS | EM DV / OV / EV TLS CA - G3C | Webサイト証明書 | Chrome、Mozilla、Microsoft、Apple |
| コード署名 | EM OV CS CA - G3B / G3C | ソフトウェアの署名 | Microsoft |
TLS用の中間CAは、eMudhraの emSign Root TLS CA - G3 を経由して emSign ECC Root CA - G3 へつながる。
これとは別に、Sectigoの AAA Certificate Services ルートが同じ公開鍵へ署名したクロス署名証明書も8枚あった。クロス署名は、別のルートからも同じ中間CAを信頼させるための仕組みで、鍵が同じなのでこちらも一緒に危殆化(鍵が安全でなくなること)している。
Sectigo側は10月19日ごろに失効させた(Bugzilla 2083449 comment 13)。
graph TD
A["emSign ECC Root CA - G3<br/>(eMudhraのルート)"] --> B["emSign Root TLS CA - G3"]
B --> C["EM DV/OV/EV TLS CA<br/>G3B・G3C(6枚)"]
D["AAA Certificate Services<br/>(Sectigoのルート)"] --> E["同じ鍵のクロス署名証明書"]
C --> F["秘密鍵が1〜255の整数"]
E --> F
RSA鍵の中間CAは別の手順で作ったので影響しない、とeMudhraは説明している。
同じセレモニーで作ったP-384鍵でも、A系列とA-1系列の鍵は、Mozillaが探索を重ねても復元できなかった。
弱い秘密鍵が作られた経緯
eMudhraがBugzillaのコメント11で説明したところでは、鍵は鍵を外に出さずに保管・演算する専用機器(HSM)で作っていて、機種はFIPS 140-2 Level 3準拠のUtimaco製だった。
このとき、鍵をエクスポートするためのwrap/unwrap処理を試そうとして、通常のセレモニー手順から外れる作業を承認していた。その作業用のコードに不具合があり、HSM内に弱い鍵の組が2組目としてできてしまった。
さらに鍵のラベルが似ていたので弱い方を誤って使い、ルートで署名したうえでSectigoへクロス署名を求めていた。
eMudhraは「プロセスの複数の段階で人為的なミスがあった」と認めていて、すべての鍵生成セレモニーを確認し直している。
弱い鍵でも楕円曲線の秘密鍵としては数学的に正しい値なので、公開鍵の形式チェックや署名・検証のテストではエラーにならない。
証明書をどこまで発行していたかについては、eMudhraと報告者で書き方が違う。
eMudhraは、これらの中間CAからサイト向けの証明書(エンドエンティティ証明書)は発行していないとしている。一方、報告者の時系列では、G3BはOCSP(失効確認の応答)用の発行だけで、EM DV TLS CA - G3C は10月17日に停止されるまで動いていたと書かれている。
発覚から遮断までの時系列
2025年4月に鍵が作られてから2026年10月17日に遮断されるまで、約1年半のあいだ誰でも鍵を計算できた。
一方、報告から主要ブラウザの遮断までは5時間かかっていない。時刻はすべてUTC。
| 日時(UTC) | 出来事 | 出典 |
|---|---|---|
| 2025年4月30日 | 24枚の中間CAを作る鍵生成セレモニー | Bugzilla 2083449 |
| 2026年10月17日 18:05 | 研究者がChromeのルートプログラムへ報告 | dev-security-policy(Chrome) |
| 10月17日 19:45 | ChromeがCRLSet(Chromeが配る失効リスト)でTLS用6枚の公開鍵を遮断 | 同上 |
| 10月17日 20:06 | Mozillaが報告を受領 | Mozilla Security Blog |
| 10月17日 21:48 | Mozillaが独自に鍵を復元 | Bugzilla 2083449 |
| 10月17日 22:59 | FirefoxのOneCRL(Mozillaが配る失効リスト)で遮断完了 | 同上 |
| 10月18日 09:24〜09:38 | eMudhraが8枚を鍵の危殆化として失効 | 同上 |
| 10月19日 | Appleが遮断 | テスト用サイト |
| 10月20日 | Microsoftが遮断。NSS 3.131.1で emSign ECC Root CA - G3 を削除 | 同上、Mozilla Security Blog |
Certificate Transparency(CT)は、発行された証明書を公開ログへ記録させる仕組みで、CTを要求するクライアントはログに載っていない証明書を受け入れない。
Chromeによると、遮断前にこの6枚につながるTLS証明書はCTログに1件も記録されておらず、CTを要求するブラウザで偽造証明書が既定で信頼されることはなかった。テスト用サイトも、Firefox、Chrome、Edge、SafariはCTを要求するので攻撃が成立しなかったとしている。
ただし、ブラウザ以外の多くのクライアントはCTを要求していないので、そちらが狙われたかは分からないとも書かれている(テスト用サイト)。
Mozillaは、複数の人が公開の場で鍵を復元したことを理由に、emSign ECC Root CA - G3 の信頼を緊急で取り消した(Bug 2084101)。
NSSはFirefoxが使っているMozillaのTLSライブラリで、ルート証明書の一覧もここに入っている。ChromeもChrome Root Store(Chromeが信頼するルート証明書の一覧)のバージョン43でeMudhraのルートを外すとしている。
Firefoxは158.0.1と延長サポート版(ESR)の115.43.1、153.5.1で、OneCRLの更新を受け取れない設定の環境も同じように遮断した(MFSA 2026-111、CVE-2026-113470)。
eMudhraはG3ルートの下での発行を止め、利用者をG1系列の中間CAへ移している。移行が必要な証明書は約3万枚で、そのうち約97%はACME(Let’s Encryptなどと同じ自動発行の仕組み)で取得されたものだという。
公開証明書から8本の鍵を復元してみた
候補が255通りなので、手元のM4 Mac miniでも復元してみた。
| 項目 | 内容 |
|---|---|
| 機種 | Mac mini(M4) |
| OS | macOS 26.5.2 |
| Python | 3.14.4 |
| ライブラリ | cryptography 50.0.2 |
| 入力 | テスト用サイトの impacted-intermediates.json |
報告者が公開しているテスト用サイト(emudhra.we-have-a-problem.com)に、影響を受けた中間CAの証明書一覧 impacted-intermediates.json が置いてある。
証明書に入っている公開鍵は「秘密鍵 k × 楕円曲線の基準点 G」で決まる。公開鍵から k を効率よく逆算する方法は知られておらず、本来は候補が多すぎて全部試すこともできない。
今回は255通りしかないので、k = 1〜255の公開鍵を全部作り、証明書の公開鍵と同じものを探した。証明書に入っているのは公開鍵だけで、一致した候補の k がそのまま秘密鍵になる。
from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
fmt = (serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint)
table = {ec.derive_private_key(k, ec.SECP384R1()).public_key().public_bytes(*fmt): k
for k in range(1, 256)}
cert = x509.load_pem_x509_certificate(pem) # 証明書一覧のPEM
print(table.get(cert.public_key().public_bytes(*fmt)))
255通りの公開鍵は28.7ミリ秒で作れた。
8枚ともどれかの値と同じで、eMudhra側の証明書とSectigoのクロス署名証明書は同じ値になった。
| 中間CA | 用途 | 秘密鍵の値 |
|---|---|---|
| EM DV TLS CA - G3B | TLS | 156 |
| EM OV TLS CA - G3B | TLS | 155 |
| EM EV TLS CA - G3B | TLS | 115 |
| EM DV TLS CA - G3C | TLS | 230 |
| EM OV TLS CA - G3C | TLS | 4 |
| EM EV TLS CA - G3C | TLS | 56 |
| EM OV CS CA - G3B | コード署名 | 66 |
| EM OV CS CA - G3C | コード署名 | 237 |
EM OV TLS CA - G3Cの秘密鍵は4で、公開証明書と cryptography ライブラリだけで、CAに侵入しなくても署名鍵を計算できた。
これらの中間CAはすでに失効・遮断されていて、更新したクライアントは、この値で作った証明書を検証の段階で拒否する。
macOS上のcurl・Node.js・Python・uv・Goで危殆化したチェーンを試した
ブラウザはCRLSetやOneCRLで遮断したが、ブラウザ以外のソフトはそれぞれ独自に証明書を検証している。
テスト用サイトには、危殆化した中間CAで署名した証明書を返すホストがあり、dv-g3b-emudhra は emSign ECC Root CA - G3 へつながるチェーンを、dv-g3b-aaa-emudhra はSectigoの AAA Certificate Services 経由のチェーンを返す。
ここにエラーなしで接続できたクライアントは、危殆化した中間CAを信頼している。
2026年10月21日に、前節と同じM4 Mac mini(macOS 26.5.2)で試した。
この試験では「接続できた」「TLSの検証を通過」が危ない側の結果で、検証失敗やエラーになるのが正しい挙動だ。
| クライアント | 版 | eMudhraルート経由 | Sectigoルート経由 |
|---|---|---|---|
/usr/bin/curl(macOS標準) | 8.7.1(SecureTransport) | 接続できた(200) | 接続できた(200) |
Node.js fetch | v25.3.0 | 接続できた(200) | 接続できた(200) |
| Python + certifi | 2026.7.22 | 接続できた(200) | 検証失敗 |
| Python + certifi | 2026.10.18 / 2026.10.19 | 検証失敗 | 検証失敗 |
uv pip compile | 0.9.21(Homebrew) | TLSの検証を通過 | 未試験 |
uv pip compile | 0.13.3 | TLSの検証を通過 | 未試験 |
uv pip compile | 0.13.4 | invalid peer certificate: UnknownIssuer | 未試験 |
Go net/http | 1.26.2(macOS) | certificate is revoked | certificate is revoked |
最初に試したmacOS標準の curl は、Appleが10月19日に遮断したあとでも両方のホストへ接続できた。
テスト用サイトによると、/usr/bin/curl は失効確認をしないうえ、更新されていない /etc/ssl/cert.pem を使う。実際にこのMacの /etc/ssl/cert.pem を確かめると、emSign ECC Root CA - G3 と AAA Certificate Services がどちらも入っていた。
Node.js v25.3.0の fetch も、両方のホストへそのまま接続できた。Node.jsはMozillaのルート証明書一覧をバイナリに組み込んでいて、テスト用サイトの一覧では2026年10月21日時点でNode.jsとBunは修正待ち(pending)になっている。
Python + certifiは、2026.7.22でもSectigo経由のチェーンは検証に失敗した。
このcertifiにはもう AAA Certificate Services が入っておらず、emSign ECC Root CA - G3 だけが入っていた。2026.10.18以降はemSignのルートも外れている。
uvは最初、uv pip install --dry-run にテスト用ホストを --index-url で指定したが、仮想環境がないというエラーでTLSの手前で止まった。
そこで pip compile に切り替えると、Homebrewで入れていた0.9.21はTLSの検証を通ってHTTPのレスポンス(Unsupported Content-Type "text/plain")まで受け取った。uvx で0.13.4を取ってきて同じことをすると、TLSの段階で UnknownIssuer になり、一つ前の0.13.3では0.9.21と同じく通ってしまった。
uvは標準で webpki-root-certs から取り込んだルート証明書を本体に組み込んでいて、0.13.4でそこから emSign ECC Root CA - G3 が取り除かれた(GHSA-hq85-5cpj-mr62)。
brew info uv で確かめると、Homebrewでも0.13.4がすでに配布されていた。
Goは、macOSでは証明書の検証をOSのSecurityフレームワークで行うので、Appleの遮断がそのまま反映されて certificate is revoked になった。
Go 1.27.3と1.26.10は10月19日に出ていて、リリース履歴には crypto/x509 のセキュリティ修正とだけ書かれている(Go Release History)。
パッケージマネージャーはTLS以外の検証で防げるか
パッケージマネージャーがTLSとは別にファイルのハッシュや署名を照合していれば、TLSの検証が破られても偽のパッケージはそこで拒否される。
ただし、正しいハッシュや署名を前もって持っていなければ拒否できない。
| ツール | TLS以外の検証 | TLSが破られても防げる条件 |
|---|---|---|
| apt | リポジトリのメタデータにGPG署名がある(SecureApt) | 署名を検証する公開鍵があらかじめシステムに入っているので、通常は防げる |
| Go modules | チェックサムデータベース(sum.golang.org)と go.sum(Go Modules Reference) | go.sum か公開チェックサムDBで照合できるとき |
| npm / pnpm | ロックファイルの integrity(package-lock.json) | 既存のロックファイルにハッシュがあるとき |
| pip | --require-hashes のハッシュ照合(Secure installs) | ハッシュ付きの requirements.txt を使うとき |
| uv | uv.lock に記録したハッシュ | 既存のロックファイルがあるとき |
ロックファイルがない状態で依存関係を新しく解決する場合は、照合に使う正しいハッシュがまだない。
そこでTLSの検証が破られていると、パッケージ配布元(レジストリ)になりすました相手から届いたファイルを、そのまま正規のものとして受け取ってしまう。
古いuvやcertifiを入れたままのCI/CDのコンテナイメージや、ロックファイルなしで依存関係を追加するジョブには、TLSの検証以外に防ぐ方法がない。
Cloudflareは、Cloudflareのサービス上では悪用は確認されていないとしている(Cloudflare Blog)。ただ、これはCloudflareのサービスについての説明で、ほかの環境のことには触れていない。
同じ記事では、エラーを消すために証明書の検証を無効にすると、更新で取り戻そうとしている認証の保護がなくなること、SDKだけ更新しても内部のTLSライブラリやOSの信頼ストア(信頼するルート証明書の保管場所)が古いと検証の結果は変わらないことにも注意している。
2026年10月21日時点の修正済みの版
テスト用サイトの一覧から主なものを抜き出した。
| ソフトウェア | 修正済みの版 |
|---|---|
| uv | 0.13.4 |
| Python certifi | 2026.10.18以降(一覧の記載は2026.10.19) |
| pip | 26.2.2(中のcertifiを2026.10.19に更新) |
| Go | 1.27.3、1.26.10 |
| Rust webpki-roots / webpki-root-certs | 1.0.10 |
| Rust rustls-webpki | 0.103.16 |
| Composer ca-bundle | 1.4.16、1.6.0 |
| Electron | 44.9.1、43.8.1、42.11.16 |
| NSS | 3.131.1 |
| Ubuntu ca-certificates | 20260816ubuntu0.22.04.1 / 0.24.04.1 / 0.26.04.1 / 0.26.10.1 |
| Debian ca-certificates | 20250419~deb12u2、20250419+deb13u1、20260816+nmu1 |
| Alpine ca-certificates | 20261020-r0 |
| Node.js、Bun、FreeBSD ca_root_nss | 修正待ち(一覧では pending) |
Composerのca-bundleは10月20日に1.4.16と1.6.0が出ていて、危殆化した中間CAを含むチェーンを検出する CaBundle::findDistrustedCertificate も追加された(composer/ca-bundle 1.6.0)。
使っている環境がまだ危殆化した中間CAを信頼しているかは、テスト用サイトのホストへ接続して確かめられる。証明書エラーになれば、そのクライアントは信頼していない。
curl https://dv-g3b-emudhra.we-have-a-problem.com/
curl https://dv-g3b-aaa-emudhra.we-have-a-problem.com/
テスト用サイトには、/etc/ssl/cert.pem や /etc/ca-certificates.conf から2つのルートを外す手順も載っている。ただ、ルートごと外すとそのルートにつながる正規の証明書もすべて使えなくなるので、サイト自身は上級者向けの最後の手段としている。
手元のmacOS標準 curl は、2026年10月21日19時47分(JST)に試し直しても、両方のホストで200を返した。