# 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/zh-tw/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 憑證授權單位。我在自己的 Mac 上執行了頁面提供的 curl 測試：

```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 環境也都需要更新。

**這些工具讀的都是自己那份受信任的 CA 清單，而不是瀏覽器的**。清單內建在工具裡的就更新工具，凡是會執行 pip 的地方就升級 certifi，剩下少數只能等廠商修補的，就持續追蹤之後的更新公告。

## eMudhra 的中繼 CA 發生了什麼事？

**eMudhra 營運的八個中繼憑證授權單位，私密金鑰任何人都能從公開的憑證算出來**。資安研究者 Scott Helme 不到一秒就還原了全部八把金鑰，因為這些金鑰的值都只是小於 256 的整數。

eMudhra 把問題追溯到 2025 年 4 月 30 日的金鑰產生儀式，也就是在硬體安全模組（HSM）裡正式產生 CA 金鑰的程序。當時有一個金鑰包裝步驟（用另一把金鑰把金鑰加密起來保存的處理），在強金鑰之外又多產生了一組弱金鑰，而最後被寫進中繼 CA 憑證、再由根憑證簽署的，正是這組弱金鑰。

*Figure — KeyCeremony: eMudhra 還沒有說明包裝步驟為什麼會產生弱金鑰，目前只知道被根憑證簽署的是弱金鑰那一組。*

emSign 是 eMudhra 自己的憑證授權單位品牌，`emSign ECC Root CA - G3` 就是 eMudhra 的根憑證。八個中繼 CA 之中，有六個用來簽發 TLS 憑證（網域驗證 DV、組織驗證 OV、延伸驗證 EV，各有 G3B 和 G3C 兩種），憑證鏈最終都連到這個根憑證，另外兩個則用於程式碼簽署。

這八個也都有交叉簽署的版本。交叉簽署是指另一家 CA 的根憑證，替同一把中繼 CA 金鑰再簽發一張憑證，讓這把金鑰能連到兩個根憑證。較新的 CA 常用這種方式，讓只認得舊根憑證的舊裝置也能信任它。這裡的另一家 CA 是 Sectigo，一家大型商業憑證授權單位，前身是 Comodo CA。它用的是較舊的 `AAA Certificate Services` 根憑證，Apple、Microsoft 和 macOS 的 `/etc/ssl/cert.pem` 都還收錄著它。

**因此，就算從信任存放區移除 emSign 根憑證，交叉簽署的那條憑證鏈仍然會被信任**。移除 emSign 的代價很小，因為這個根憑證底下只有 eMudhra 自己的憑證。反過來移除 AAA 根憑證則不可行，因為大量無關網站的憑證都連到它。所以瀏覽器和作業系統廠商的做法，是直接封鎖 eMudhra 的這八把中繼 CA 金鑰。

| 日期（2026 年） | 發生的事 |
| --- | --- |
| 10 月 17 日 | 一位只以 Wayne 具名的研究者向 Chrome 和 Mozilla 回報了這些金鑰。Chrome 的封鎖清單 CRLSet 在 19:45 UTC 前封鎖了六個 TLS 中繼 CA，Firefox 的封鎖清單 OneCRL 則在 22:59 UTC 生效 |
| 10 月 18 日 | eMudhra 撤銷了全部八個中繼 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 封鎖之前，公開的憑證紀錄 Certificate Transparency 裡，找不到任何一張經由這六個 TLS 中繼 CA 簽發的 TLS 憑證。eMudhra 也表示，這八個中繼 CA 都沒有簽發過葉憑證（也就是發給網站或程式的最終憑證，又稱終端實體憑證）。至於是否有人更早算出金鑰、並以不留紀錄的方式使用過，就無從得知了。

> **話說**
>
> **為什麼叫「葉」憑證？**
>
> 憑證之間的簽署關係像一棵倒過來的樹：根憑證在最上面，中繼 CA 是分支，發給網站或程式的憑證位在最末端，就像樹葉。葉憑證不能再替其他憑證簽署，所以憑證鏈走到它就結束了。

## 憑證鏈如何運作？還原出的中繼 CA 金鑰又能簽出什麼？

TLS 用戶端信任一個網站的憑證，看的是它由誰簽署。網站的憑證（葉憑證）由中繼 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 堆疊都不檢查撤銷狀態）。

由於 eMudhra 這八個中繼 CA 的私密金鑰任何人都能輕易算出，只要拿到其中一把，任何人都能替任何網域簽出憑證，**而仍然信任這條憑證鏈的用戶端都會把它當成真的**。攻擊者只要透過惡意的 Wi-Fi 或被汙染的 DNS 回應，介入用戶端和網站之間的連線，就能讓 `pip install` 或 `curl | sh` 收到被調包的內容，而且照樣能通過 TLS 驗證。

Certificate Transparency（CT）是瀏覽器在驗證憑證鏈之外多加的一層防護。它是一組公開的已簽發憑證紀錄，只能往後追加，不能修改或刪除。Chrome 和 Safari 會拒絕沒有登錄過的憑證，所以偽造的憑證一定會在公開紀錄裡留下痕跡。但**curl 不會檢查 CT**，因此拿來對付 curl 的偽造憑證，完全不會留下公開的痕跡。

### 路徑一：攔截連線（中間人攻擊，MITM）

位在用戶端和目標網站之間的攻擊者，可以直接攔截兩者之間的連線。公共 Wi-Fi 是最常見的例子，但被入侵的家用路由器、網路服務供應商或公司的代理伺服器，也都可能成為攻擊者攔截連線的地點。

當用戶端連往 `pypi.org` 時，攻擊者會冒充網站回應，出示一張偽造的 `pypi.org` 憑證，而這張憑證是用外洩的中繼 CA 金鑰簽署的。用戶端沿著憑證鏈追溯到信任存放區裡的 emSign 根憑證後，就會接受這張憑證，**攻擊者也就能讀取並竄改雙向的所有內容**。

*Figure — MitmRoute: 憑證鏈的終點是用戶端本來就信任的根憑證，所以偽造的憑證也能通過驗證。*

### 路徑二：DNS 汙染

DNS 是把網域名稱轉換成 IP 位址的查詢服務。只要能竄改 DNS 的回應，攻擊者甚至不需要待在連線路徑上。被汙染的 DNS 解析器，或是被入侵的路由器分配給用戶端的 DNS 伺服器，會告訴用戶端 `pypi.org` 位在攻擊者的 IP 位址，用戶端就會直接連過去。

如果沒有 eMudhra 外洩的金鑰，這種攻擊會在 TLS 這一關就被擋下來，因為攻擊者的伺服器拿不出 `pypi.org` 的有效憑證。但**只要用了 eMudhra 中繼 CA 簽出的偽造憑證，連這最後一道防線也能通過**，於是被調包的套件就這樣被安裝進來，過程中也不會跳出任何警告。

*Figure — DnsPoisoning: DNS 汙染不需要攻擊者待在連線路徑上，靠著外洩的金鑰就能突破 TLS。*

### 誰會暴露在風險中？

不管是攔截連線還是 DNS 汙染，攻擊者都需要能控制用戶端和網站之間的某一段網路。我的 Mac 上用的是 Cloudflare WARP，一種類似 VPN 的服務，它會把流量和 DNS 查詢都透過加密通道送到 Cloudflare。所以只要 WARP 開著，本地的 Wi-Fi 和路由器就既沒辦法攔截連線，也沒辦法竄改 DNS 回應。

**加密通道能縮小風險範圍，但無法消除風險**。CI 執行環境、伺服器和容器的連線都不會經過這條通道，而且只要通道一關，缺口就又出現了。更新信任存放區，才能在任何網路上都把這個缺口補起來。

## Mac 上的每個工具從哪裡取得 CA 清單？

一台 Mac 上至少有五份各自獨立的 CA 清單，**而且每一份都是由不同的廠商、在不同的日期修補的**。

*Figure — TrustStoreMap: 每份 CA 清單都是分開修補的，所以就算瀏覽器已經拒絕了 eMudhra 的憑證鏈，也不代表 curl 或 pip 同樣安全。*

- **macOS 平台驗證器**（macOS 內建的憑證驗證機制）供 Safari、透過鑰匙圈驗證的 App 使用，Go 程式在 macOS 上也會把驗證交給它來處理。Apple 10 月 19 日已經在驗證器上封鎖了這些中繼 CA。
- **`/etc/ssl/cert.pem`** 是一個 PEM 檔案（以文字格式保存憑證），內容不會自動更新，由系統內建的 `/usr/bin/curl`、`git`、`python3` 和 `openssl` 讀取。這些工具都不檢查憑證的撤銷狀態，而 macOS 26.7.1 上的這個檔案仍然包含 `emSign ECC Root CA - G3`。
- **編譯進執行檔的 CA 清單副本**：Node.js 和 Bun 都內建一份，用 Bun 建置的 Claude Code 也跟著帶有這份副本，而且只有新版本發布時才會改變。
- **certifi** 是一個 Python 套件，用 PEM 檔案保存 Mozilla 的根憑證清單，pip 和 `requests` 讀的就是它。pip 還在自己的套件裡內建了一份 certifi。
- **uv** 則透過 Rust 套件 `webpki-root-certs`，把 Mozilla 的根憑證編譯進執行檔。

除了上面這幾份清單，Chrome 和 Firefox 還各有自己的封鎖清單（CRLSet 和 OneCRL），所以瀏覽器才會最先變安全。

## 「That's bad!」的 curl 測試在檢查什麼？

測試主機回傳成功的回應，代表用戶端信任的憑證鏈經過了金鑰已外洩的中繼 CA。這個頁面提供四台主機：

| 主機 | 提供的憑證鏈 |
| --- | --- |
| `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 版本，再看它能不能連上用 eMudhra 金鑰簽署憑證的測試主機，所以能在任何環境重複使用：

```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 的平台驗證器。也就是說，**Apple 在 10 月 19 日的封鎖涵蓋了 Mac 上的所有 Go 版本**。

Go 1.27.3 和 1.26.10 在 `crypto/x509` 本身加入了這八把公開金鑰的封鎖清單。這份封鎖清單只在 Linux 和容器裡才有作用，因為 Go 在 Linux 和容器裡會直接讀取根憑證檔案。所以替 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 當作最低版本比較保險。

## 為什麼修補作業系統也影響不到 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
```

**16 份 certifi 全都比 2026.10.19 舊，11 份 pip 也全都比 26.2.2 舊**。這些 certifi 和 pip 分布在專案的虛擬環境、用 `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 的套件，不會為了符合鎖定檔而把它們刪掉。之後，鎖定檔的變更需要在每個 repo 各自提交，從這些 repo 建置的容器映像也需要跟著重新建置。

## 等待 Apple、Homebrew 或手動清理的項目

以下四項在 Apple 或 Homebrew 推出修補、或我自己手動清理之前，仍然暴露在危險中：

1. **`/usr/bin/curl`、`git`、`python3` 和 `openssl` 這些 macOS 內建工具**讀的是 `/etc/ssl/cert.pem`，而只有 macOS 更新才會替換這個檔案。到 10 月 21 日為止 Apple 都還沒有推出相對應的 macOS 更新。我寫這篇文章時，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` 這兩個 formula（Homebrew 的套件定義）之後才會更新。
4. **pre-commit（管理 git hook 的工具）建立的 hook 環境**，在重新建置之前都會停留在 pip 26.2.1。`pre-commit clean` 可以清掉這些 hook 環境，但我的 `PATH` 裡沒有 `pre-commit` 指令，所以必須手動刪除 `~/.cache/pre-commit` 底下的快取目錄。

curl 倒是有個權宜做法，可以在每次執行時指定憑證包。curl 專案發布的 CA 憑證包是 10 月 20 日從 Mozilla 擷取的，已經不包含 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 這八個中繼 CA 的私密金鑰任何人都能算出，但是瀏覽器安全了，並不代表同一台 Mac 上的 curl、pip 等其他工具也安全。在 Mac 上，需要個別處理的 CA 清單有這些：

- `/usr/bin/curl` 讀取的 `/etc/ssl/cert.pem`。要等 Apple 修補，在那之前可以用 `CURL_CA_BUNDLE` 應急。
- 編譯進 Node.js 和 Claude Code 的 CA 清單副本，各自更新就能修好。
- certifi。不管作業系統信任什麼，pip 都會載入它，所以每個虛擬環境和 `uv.lock` 都要升級。

macOS 上的 Go 已經被 Apple 的封鎖涵蓋，所以 Go 的更新只對用 Go 建置的 Linux 執行檔有意義。**要檢查任何一份 CA 清單，emudhra.we-have-a-problem.com 的測試主機是最快的方法**。

## 參考連結

- [eMudhra 中繼 CA 金鑰外洩事件：測試主機與各工具的修補狀態](https://emudhra.we-have-a-problem.com/)
- [The Hacker News：八把 eMudhra CA 金鑰外洩與封鎖時間軸](https://thehackernews.com/2026/10/eight-emudhra-ca-keys-compromised.html)
- [BleepingComputer：Mozilla、Google 和 Apple 封鎖金鑰外洩的 eMudhra 中繼 CA 憑證](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)
