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

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

- Source: https://oharu121.com/ja/blog/emudhra-ca-compromise-trust-store-curl-pip-claude-code/
- Published: 2026-10-24T20:21:23+09:00
- Tags: セキュリティ, Python

---
## はじめに

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

```bash
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 証明書に入ったのは弱い方でした。

*Figure — KeyCeremony: ラップ処理がなぜ弱い鍵を生んだのかは公表されておらず、分かっているのはルートが署名したのが弱い方だったということだけです。*

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 が枝、サイトやプログラムに発行される証明書が末端の葉にあたります。リーフ証明書はほかの証明書に署名できないので、チェーンはそこで終わります。

## 証明書チェーンの仕組みと、復元された中間 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 ルートに行き着き、その証明書を受け入れます。こうして**攻撃者は双方向の通信をすべて読み取り、書き換えられるようになります**。

*Figure — MitmRoute: チェーンがクライアントの信頼済みのルートで終わるため、偽の証明書が検証を通ります。*

### 経路 2：DNS ポイズニング

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

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

*Figure — DnsPoisoning: DNS ポイズニングに経路上の位置は要りません。TLS を突破させるのは侵害された鍵です。*

### 影響を受けるのは誰か

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

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

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

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

*Figure — TrustStoreMap: 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-emudhra` | `dv-g3b-emudhra` と同じチェーンで、リーフを CT に記録していないもの |
| `dv-g3b-aaa-noct-emudhra` | `dv-g3b-aaa-emudhra` と同じクロス署名のチェーンで、リーフを CT に記録していないもの |

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

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

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

```python title="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))
```

```bash
uv run --no-project --with certifi==2026.7.22 python -I probe.py
```

| 手元の Mac のツール | `dv-g3b-emudhra` への結果 | 判定 |
| --- | --- | --- |
| `/usr/bin/curl` | `200`、接続成功 | eMudhra のチェーンをまだ信頼 |
| certifi 2026.7.22 | `CONNECTED` | eMudhra のチェーンをまだ信頼 |
| certifi 2026.10.19 | `CERTIFICATE_VERIFY_FAILED` | 拒否 |
| Node.js 26.12.1 | `CERT_REVOKED` | 拒否 |
| uv 0.13.5 | `invalid peer certificate: …(CertDistrusted)` | 拒否 |
| Go 1.26.4 / 1.27.3 | `x509: … 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 をすべて探してもらいました。

```bash
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` でこのバンドルを受け付けます。

```bash
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
```

> **気を付けて**
>
> **このワークアラウンドが効くのは curl だけ**
>
> `CURL_CA_BUNDLE` は `git`、`python3`、`openssl` には何の影響もありません。これらはそれぞれの設定を通じて `/etc/ssl/cert.pem` を読むからです。macOS のアップデートで `/etc/ssl/cert.pem` が置き換わったら、私は `CURL_CA_BUNDLE` を削除するつもりです。

## まとめ

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 のテストホストを使うのが一番手早い確認方法です**。

## 参考リンク

- [eMudhra 中間 CA の侵害：テストホストとツールごとの修正状況](https://emudhra.we-have-a-problem.com/)
- [The Hacker News：eMudhra の CA 鍵 8 つの侵害とブロックの時系列](https://thehackernews.com/2026/10/eight-emudhra-ca-keys-compromised.html)
- [BleepingComputer：Mozilla、Google、Apple が侵害された eMudhra の証明書をブロック](https://www.bleepingcomputer.com/news/security/mozilla-google-apple-block-compromised-emudhra-ca-certificates/)
- [Endor Labs：修正したトラストストアと、存在に気づいていなかったトラストストア](https://www.endorlabs.com/learn/emudhra-intermediate-ca-compromise-the-trust-stores-you-patched-and-the-ones-you-didnt-know-you-had)
- [StepSecurity：Python、pip、CI/CD の利用者が更新すべきもの](https://www.stepsecurity.io/blog/emudhra-emsign-ca-compromise-python-pip-ci-cd-update)
- [Mozilla から抽出した curl の CA 証明書バンドル](https://curl.se/docs/caextract.html)
