eMudhra の中間 CA 鍵漏洩で Mac のトラストストアを棚卸しする — curl、pip、Node.js、Claude Code は別々の CA リストを読む

eMudhra の中間 CA 鍵漏洩を受け、Mac 上の curl、pip、certifi、uv、Node.js、Go、Claude Code がそれぞれ参照する CA リストと、その修正方法を整理します。

白いカードに青と黄のPythonの二匹の蛇のロゴと灰色のロゴタイプ
目次

はじめに

emudhra.we-have-a-problem.com というページを見つけました。侵害された eMudhra の認証局 8 つを、クライアントがまだ信頼しているかを確かめるページです。そこにある curl のテストを手元の Mac で実行しました。

ターミナルウィンドウ
入力
curl https://dv-g3b-emudhra.we-have-a-problem.com/
出力
Your client successfully connected to the site. That's bad!

Chrome と Firefox は、鍵が報告されたその日のうちにこれらの認証局をブロックしました。ほかのツールは対応が遅く、読んだニュースで更新が必要とされていたのは pip と uv だけでした。Claude Code にマシン上のほかのツールも調べてもらうと、curl、Claude Code 自身、そして 16 個の Python 環境も更新が必要だと分かりました。

これらのツールはブラウザのリストではなく、それぞれ独自の信頼済み認証局リストを読んでいます。リストを同梱しているツールは更新し、pip が動く場所ではすべて certifi を上げ、ベンダーの対応を待つしかないいくつかは今後の更新情報を追い続けます。

eMudhra の中間 CA に何が起きたか

eMudhra が運用する 8 つの中間認証局は、公開された証明書から誰でも計算できる秘密鍵で作られていました。セキュリティ研究者の Scott Helme は 8 つすべてを 1 秒未満で復元しています。鍵の値が 256 未満の整数にすぎなかったからです。

eMudhra は原因を 2025 年 4 月 30 日の鍵生成セレモニーに求めています。ハードウェアセキュリティモジュール(HSM)の中で CA の鍵を正式に生成する手続きです。そこでの鍵のラップ処理(鍵を別の鍵で暗号化して保管する処理)が、強い鍵とは別に弱い鍵をもう 1 セット生成し、ルートが署名した中間 CA 証明書に入ったのは弱い方でした。

弱い鍵がすべてのクライアントに届くまで2025 年 4 月 30 日の鍵生成セレモニーで、HSM の中で鍵が生成された。その後の鍵のラップ/アンラップ処理で 2 つのセットができた。公開された証明書には入っていない強い鍵のセットと、秘密鍵が 256 未満の整数である弱い鍵のセットである。弱い公開鍵は 8 枚の中間 CA 証明書に入り、emSign ECC Root CA - G3 が署名し、Sectigo AAA がクロス署名した。ルートストアはこれらのルートを信頼済みなので、どのクライアントも中間 CA を受け入れる。ラップ処理がどう誤ったかは調査中である。鍵生成セレモニー(2025 年 4 月 30 日)HSM の中で鍵を生成鍵のラップ/アンラップ処理どう誤ったかは調査中強い鍵のセット公開された証明書には入っていない弱い鍵のセット秘密鍵は 256 未満の整数emSign ECC Root CA - G3Sectigo AAA がクロス署名署名中間 CA 証明書 8 枚弱い公開鍵が入っているルートストアはこれらのルートを信頼済みそのため、どのクライアントも中間 CA を受け入れる
ラップ処理がなぜ弱い鍵を生んだのかは公表されておらず、分かっているのはルートが署名したのが弱い方だったということだけです。

emSign は eMudhra 自身の認証局ブランドで、emSign ECC Root CA - G3 は eMudhra のルート証明書です。8 つの中間 CA のうち 6 つは TLS 証明書を発行するもので(ドメイン認証 DV、組織認証 OV、EV 認証がそれぞれ G3B と G3C の 2 種類)、このルートにつながります。残りの 2 つはコード署名用です。

8 つはどれもクロス署名版を持っています。クロス署名とは、別の CA のルートが同じ中間 CA の鍵に署名し、1 つの鍵が 2 つのルートにつながるようにすることです。新しい CA は、古いルートしか知らない古い端末にも信頼されるためにこの方法を使います。ここでの別の CA は Sectigo で、以前は Comodo CA という名前だった大手の商用認証局です。使われているのは古い AAA Certificate Services ルートで、Apple、Microsoft、macOS の /etc/ssl/cert.pem にはまだ収録されています。

そのため、トラストストアから emSign ルートを削除しても、クロス署名側のチェーンは信頼されたままです。emSign を外す代償は小さく、このルートの配下には eMudhra 自身の証明書しかありません。一方、AAA ルートを削除することはできません。無関係な多数のサイトの証明書がこのルートにつながっているため、ブラウザや OS のベンダーは 8 つの中間 CA の鍵そのものをブロックしています。

日付(2026 年)出来事
10 月 17 日Wayne とクレジットされた研究者が Chrome と Mozilla に鍵を報告。Chrome の CRLSet が 19:45 UTC までに TLS 用の 6 つをブロックし、Firefox の OneCRL によるブロックは 22:59 UTC に反映
10 月 18 日eMudhra が 8 つすべての中間 CA を失効
10 月 19 日Apple が自社プラットフォームでブロック。Android も Google Play 開発者サービス経由で追随
10 月 20 日Microsoft がブロック。Firefox 158.0.1、NSS 3.131.1、certifi 2026.10.19、pip 26.2.2、Go 1.27.3 / 1.26.10 がリリース
10 月 21 日鍵のブロックリストを入れた Node.js 26.12.1、24.22.1、22.23.4 がリリース

悪用は確認されていません。Chrome がブロックする以前に、TLS 用の 6 つの中間 CA につながる TLS 証明書は Certificate Transparency に 1 件も記録されておらず、eMudhra も 8 つのいずれもリーフ証明書(サイトやプログラムに発行される末端の証明書で、エンドエンティティ証明書とも呼ばれます)を発行していないと述べています。誰かがもっと早く鍵を計算し、記録に残らない形で使ったかどうかは知りようがありません。

証明書チェーンの仕組みと、復元された中間 CA の鍵で署名できるもの

TLS クライアントがサイトの証明書を信頼するかどうかは、誰が署名したかで決まります。サイトの証明書(リーフ)は中間 CA が署名し、中間 CA はルートが署名します。クライアントはトラストストア(受け入れるルート証明書のリスト)を持ち、リーフから署名をたどってそのどれかのルートまで行き着くかを確認します。

中間 CA を失効させても(CA がその証明書はもう有効ではないと公に宣言しても)、このチェーン自体は壊れません。ルートはどのトラストストアにも残り、中間 CA へのルートの署名も有効なままです。クライアントが失効した中間 CA を拒否するのは、失効を確認するか、その中間 CA を明示的にブロックしている場合だけです。セキュリティ企業の Endor Labs も「default TLS stacks in Python, Ruby, Go, Node.js, and most command-line tools do not check revocation」(Python、Ruby、Go、Node.js とほとんどのコマンドラインツールの標準 TLS スタックは失効を確認しない)と指摘しています。

この 8 つの中間 CA の秘密鍵は誰でも簡単に計算できるので、どれか 1 つを手にすれば、誰でも任意のドメインの証明書に署名でき、このチェーンを信頼したままのクライアントはそれを本物として受け入れます。悪意のある Wi-Fi や汚染された DNS 応答を通じてクライアントとサイトの間の通信に割り込んだ攻撃者は、pip install や curl | sh にすり替えた内容を渡せます。それでも TLS の検証は通ります。

Certificate Transparency(CT)は、ブラウザがチェーンの検証に加えて重ねている安全策で、発行された証明書を記録する、公開された追記専用のログです。Chrome と Safari はログに記録されていない証明書を拒否するので、偽造証明書は必ず公開の記録に痕跡を残します。curl は CT を確認しません。そのため、curl に対して使われた偽造証明書は、公開された痕跡をまったく残しません。

経路 1:通信の傍受(中間者攻撃、MITM)

クライアントとサイトの間のネットワークにいる攻撃者は、通信そのものを横取りできます。よくあるのは公衆 Wi-Fi ですが、乗っ取られた家庭用ルーター、ISP、社内のプロキシも同じ位置にいます。

クライアントが pypi.org に接続すると、攻撃者がサイトになりすまして応答し、侵害された中間 CA の鍵で署名した偽の pypi.org 証明書を提示します。クライアントはチェーンをたどってトラストストアの emSign ルートに行き着き、その証明書を受け入れます。こうして攻撃者は双方向の通信をすべて読み取り、書き換えられるようになります。

経路 1:通信の傍受左から、クライアント、Wi-Fi やルーター、ISP、プロキシなど経路上の攻撃者、本物の pypi.org が並ぶ。クライアントが pypi.org に接続すると攻撃者がそれを横取りし、漏洩した鍵で署名した偽の pypi.org 証明書で応答する。チェーンがトラストストアの emSign ルートに届くため、クライアントはそれを受け入れる。攻撃者は本物のサイトに中継しながら、通信を読み取り書き換える。クライアントpip install、curl | sh経路上の攻撃者Wi-Fi、ルーター、ISP、プロキシpypi.org本物のサイト1241. クライアントが pypi.org に接続し、攻撃者がそれを横取りする2. 攻撃者が、漏洩した鍵で署名した偽の pypi.org 証明書で応答する3. チェーンがトラストストアの emSign ルートに届くので、クライアントは受け入れる4. 攻撃者は本物のサイトに中継しながら、通信を読み取り書き換える
チェーンがクライアントの信頼済みのルートで終わるため、偽の証明書が検証を通ります。

経路 2:DNS ポイズニング

DNS は、ドメイン名を IP アドレスに変換する問い合わせの仕組みです。DNS の応答を書き換えられるなら、攻撃者は通信経路上にいる必要すらありません。汚染されたリゾルバーや、乗っ取られたルーターが配る DNS サーバーが、pypi.org の場所として攻撃者の IP アドレスを返し、クライアントはそこへ直接接続します。

侵害された鍵がなければ、この攻撃は TLS で止まります。攻撃者のサーバーは pypi.org の有効な証明書を出せないからです。しかしeMudhra の中間 CA で署名した偽の証明書があれば、この最後のチェックも通ってしまい、すり替えられたパッケージが警告なしにインストールされます。

経路 2:DNS ポイズニングクライアント、汚染された DNS サーバー、接続されることのない本物の pypi.org、インターネット上のどこにでも置ける攻撃者のサーバーが並ぶ。クライアントが DNS に pypi.org の場所を問い合わせると、汚染された DNS は攻撃者の IP アドレスを返し、クライアントは攻撃者のサーバーに直接接続する。漏洩した鍵で署名した偽の pypi.org 証明書が、そのまま TLS の検証を通る。クライアントpip install、curl | sh汚染された DNSリゾルバー、ルーターが配る DNSpypi.org接続されない攻撃者のサーバーインターネット上のどこでも1231. クライアントが DNS に pypi.org の場所を問い合わせる2. 汚染された DNS が攻撃者の IP アドレスを返す3. クライアントは攻撃者のサーバーに直接接続する4. 漏洩した鍵で署名した偽の pypi.org 証明書が TLS の検証を通る
DNS ポイズニングに経路上の位置は要りません。TLS を突破させるのは侵害された鍵です。

影響を受けるのは誰か

通信の傍受も DNS ポイズニングも、攻撃者がクライアントとサイトの間のネットワークのどこかを支配している必要があります。私は Mac で、VPN に似たサービスである Cloudflare WARP を使っています。WARP は通信と DNS の問い合わせを暗号化したトンネルで Cloudflare に送るので、有効な間はローカルの Wi-Fi やルーターが通信を横取りすることも、DNS の応答を書き換えることもできません。

トンネルはリスクの範囲を狭めますが、なくしはしません。CI の実行環境、サーバー、コンテナはこのトンネルの外にあり、トンネルを切れば穴はまた開きます。トラストストアを更新して初めて、どのネットワークでも穴がふさがります。

Mac 上の各ツールはどこから CA リストを得ているか

Mac には少なくとも 5 つの独立した CA リストがあり、それぞれ別のベンダーが別の日に修正しました。

1 台の Mac にある 5 つの CA リスト5 行の表。各行はツール群、そのツールが参照する CA リスト、リストを修正した主体を対応させる。Safari、キーチェーン利用アプリ、Go が使う macOS の証明書検証は 10 月 19 日に Apple がブロックし、テストホストを拒否した。システムの curl、git、python3、openssl が読む /etc/ssl/cert.pem は更新がなく、まだ信頼していた。Node.js と Claude Code に組み込まれたリストは更新後に拒否した。pip と requests が読む certifi は、各 venv で更新するまで古いままだった。uv の組み込みリストは拒否した。ツール参照する CA リスト手元の MacSafari、キーチェーン利用アプリmacOS 上の GomacOS の証明書検証Apple がブロック(10/19)拒否/usr/bin/curl、gitpython3、openssl/etc/ssl/cert.pemmacOS の更新はまだまだ信頼Node.jsClaude Code(Bun)バイナリに組み込みNode.js 26.12.1、Claude Code 2.1.304 以降更新後に拒否pip、requestscertifi(venv ごとに 1 つ)certifi 2026.10.19(10/20)更新するまで古いままuvwebpki-root-certs(組み込み)uv 0.13.4拒否Chrome と Firefox は独自のブロックリスト(CRLSet、OneCRL)も持つ。
CA リストはそれぞれ別々に修正されるため、ブラウザがチェーンを拒否しても、curl や pip が安全だとは言えません。
  • macOS の証明書検証機能は、Safari、キーチェーンで検証するアプリ、そして Go のプログラムが使います。Go は macOS では検証を OS に任せます。Apple は 10 月 19 日にここで中間 CA をブロックしました。
  • /etc/ssl/cert.pem は、システムの /usr/bin/curl、git、python3、openssl が読む PEM ファイル(証明書をテキスト形式で保存したもの)で、中身は自動では更新されません。これらは失効を確認せず、macOS 26.7.1 のこのファイルにはまだ emSign ECC Root CA - G3 が入っています。
  • バイナリに組み込まれたコピーは、Node.js と Bun に同梱されています。Claude Code は Bun で作られているので、Claude Code にも入っています。変わるのは新しいリリースが出たときだけです。
  • certifi は Mozilla のルートリストを PEM ファイルとして持つ Python パッケージで、pip と requests が読みます。pip は pip パッケージの中にも certifi のコピーを同梱しています。
  • uv は Rust のクレート webpki-root-certs を通じて Mozilla のルートを組み込んでいます。

Chrome と Firefox はこれらとは別に、独自のブロックリスト(CRLSet と OneCRL)を持っています。ブラウザが最初に安全になったのはそのためです。

「That’s bad!」の curl テストは何を確認しているか

テストホストから正常な応答が返るということは、そのクライアントが侵害された中間 CA を経由するチェーンを信頼しているということです。ページは 4 つのホストを用意しています。

ホスト返すチェーン
dv-g3b-emudhraリーフ → EM DV TLS CA - G3B → emSign ECC Root CA - G3。CT に記録済み
dv-g3b-aaa-emudhra同じ中間 CA を、Sectigo の AAA Certificate Services 配下でクロス署名したもの
dv-g3b-noct-emudhradv-g3b-emudhra と同じチェーンで、リーフを CT に記録していないもの
dv-g3b-aaa-noct-emudhradv-g3b-aaa-emudhra と同じクロス署名のチェーンで、リーフを CT に記録していないもの

noct ホストに接続できるということは、チェーンを信頼したうえで CT の確認も省いているということです。ページはこうしたクライアントを「can be attacked without anyone noticing」(誰にも気づかれずに攻撃できる)と書いています。手元の /usr/bin/curl は noct ホストにも接続し、同じ「That’s bad!」が返ってきました。

各ツールを修正済みバージョンと照らし合わせる

バージョン番号はツールが何を同梱しているかを示し、テストホストへのプローブはツールが何を受け入れるかを示します。そこで Claude Code に両方を実行してもらいました。Python のプローブは検証する certifi のバージョンを固定するので、どの環境にも使い回せます。

probe.py
import ssl, certifi, urllib.request
ctx = ssl.create_default_context(cafile=certifi.where())
for host in ["dv-g3b-emudhra", "dv-g3b-aaa-emudhra"]:
url = f"https://{host}.we-have-a-problem.com/"
try:
urllib.request.urlopen(url, context=ctx, timeout=15)
print(host, "CONNECTED")
except Exception as e:
print(host, "ERR", getattr(e, "reason", e))
ターミナルウィンドウ
uv run --no-project --with certifi==2026.7.22 python -I probe.py
手元の Mac のツールdv-g3b-emudhra への結果判定
/usr/bin/curl200、接続成功eMudhra のチェーンをまだ信頼
certifi 2026.7.22CONNECTEDeMudhra のチェーンをまだ信頼
certifi 2026.10.19CERTIFICATE_VERIFY_FAILED拒否
Node.js 26.12.1CERT_REVOKED拒否
uv 0.13.5invalid peer certificate: …(CertDistrusted)拒否
Go 1.26.4 / 1.27.3x509: … certificate is blocked拒否

Go 1.26.4 も 1.27.3 と同じように dv-g3b-emudhra を拒否します。macOS では certificate is blocked というエラーは Apple の証明書検証機能から来ているので、10 月 19 日の Apple のブロックが Mac 上のすべての Go バージョンをカバーしていました。

Go 1.27.3 と 1.26.10 は、8 つの公開鍵のブロックリストを crypto/x509 自体に追加しています。これが効くのは、Go がルートファイルを直接読む Linux やコンテナです。そうした環境向けにビルドしたバイナリは、Go 1.27.3 か 1.26.10 で再ビルドが必要です。

Node.js 26.12.1 と uv 0.13.5 はすでに修正済みのリリースでした。Claude Code は 2.1.292 で、emudhra.we-have-a-problem.com が挙げる 2.1.303 より古かったため、Claude Code を更新しました。Anthropic の 2.1.303 の変更履歴は証明書に触れていませんが、コミュニティのテストでは 2.1.304 以降が emSign ルート経由と AAA クロス署名経由の両方のチェーンを拒否すると報告されています。そのため、2.1.304 を下限と考えるのが安全です。

OS を修正しても pip に届かない理由

pip はシステムのトラストストアに加えて、同梱の certifi を常に読み込みます。これを止める方法はありません。--cert や REQUESTS_CA_BUNDLE を設定しても認証局が追加されるだけで、certifi にもともと入っているリストはそのまま残ります。そのため、仮想環境ごとに更新すべき certifi があります。

Claude Code に、ホームディレクトリ、Homebrew、Command Line Tools の Python から、インストール済みの certifi と pip をすべて探してもらいました。

ターミナルウィンドウ
find ~ /opt/homebrew /Library/Developer/CommandLineTools/Library/Frameworks \
\( -path ~/Library/Caches -o -path ~/.cache/uv \) -prune -o \
-type d \( -name 'certifi-*.dist-info' -o -name 'pip-*.dist-info' \) -print

certifi は 16 個すべてが 2026.10.19 より古く、pip は 11 個すべてが 26.2.2 より古いものでした。見つかったのは、プロジェクトの仮想環境、uv tool でインストールしたツール、Google Cloud SDK 専用の環境、pre-commit のキャッシュ、そして Homebrew、Command Line Tools、uv が管理する Python 本体です。修正方法は、それぞれの環境を何が管理しているかで決まります。

環境コマンド
uv のプロジェクトuv lock --upgrade-package certifi && uv sync --inexact
素の venv<venv>/bin/python -m pip install -U pip certifi
uv tool でインストールしたツールuv tool upgrade --all

--upgrade-package certifi は uv.lock の中で certifi だけを更新し、ほかのピンはそのままにします。--inexact は、venv に手動で入れたパッケージを、ロックファイルに合わせて削除せずに残します。その後、ロックファイルの変更はリポジトリごとにコミットが必要で、それらのリポジトリからビルドしたコンテナイメージも再ビルドが必要です。

Apple、Homebrew、手作業での片付けを待つもの

次の 4 つは、Apple や Homebrew が修正を出すか、自分で手作業で片付けるまで無防備なままです。

  1. /usr/bin/curl、git、python3、openssl といった macOS 標準のツールは /etc/ssl/cert.pem を読みます。このファイルを置き換えられるのは macOS のアップデートだけで、10 月 21 日の時点で Apple はまだ出していませんでした。この記事を書いた時点でも、私の Mac は 26.7.1 のままです。
  2. Xcode と Command Line Tools に入っている pip 21.2.4 は Apple のもので、Apple が Xcode や Command Line Tools を更新したときにしかバージョンが変わりません。/usr/bin/pip3 は使わないでおくのが無難です。
  3. Homebrew の Python 3.13 と 3.14 に入っている pip 26.0 は、Homebrew が python@3.13 と python@3.14 のフォーミュラ(Homebrew のパッケージ定義)を再ビルドするまで更新されません。
  4. pre-commit(git フックを管理するツール)が作ったフック環境は、再構築されるまで pip 26.2.1 のままです。pre-commit clean でこのフック環境を消せますが、私の PATH には pre-commit コマンドがなかったので、~/.cache/pre-commit 以下のキャッシュディレクトリを手で削除する必要があります。

curl には、呼び出しごとに使えるワークアラウンドがあります。10 月 20 日に Mozilla から抽出された curl 自身の CA バンドルには emSign ルートが入っておらず、/usr/bin/curl は環境変数 CURL_CA_BUNDLE でこのバンドルを受け付けます。

ターミナルウィンドウ
入力
curl -sS -o ~/.config/cacert.pem https://curl.se/ca/cacert.pem
CURL_CA_BUNDLE=~/.config/cacert.pem curl https://dv-g3b-emudhra.we-have-a-problem.com/
出力
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

まとめ

eMudhra の 8 つの中間 CA の秘密鍵は誰でも計算できる状態でした。ブラウザが安全だからといって、同じ Mac 上の curl や pip などのツールまで安全だとは限りませんでした。Mac で個別に対応が必要だった CA リストは次のとおりです。

  • /usr/bin/curl が読む /etc/ssl/cert.pem。Apple の修正待ちで、それまでは CURL_CA_BUNDLE で回避できます。
  • Node.js と Claude Code に組み込まれた CA リストのコピー。それぞれを更新すれば直ります。
  • certifi。pip は OS が何を信頼しているかに関係なくこれを読み込むので、すべての仮想環境と uv.lock で上げる必要があります。

macOS 上の Go は Apple のブロックでカバーされていたので、Go の更新が意味を持つのは Linux 向けにビルドするバイナリの場合です。どの CA リストについても、emudhra.we-have-a-problem.com のテストホストを使うのが一番手早い確認方法です。

参考リンク

この記事をシェア