eMudhra 中繼 CA 金鑰外洩後我盤點了 Mac 的信任存放區 — curl、pip、Node.js、Claude Code 各自管理著自己的 CA 清單
eMudhra 中繼 CA 金鑰外洩之後,本文將整理 Mac 上的 curl、pip、certifi、uv、Node.js、Go 與 Claude Code 分別讀取哪一份 CA 清單,以及該如何修補。

本頁目錄
引言
我偶然看到 emudhra.we-have-a-problem.com 這個頁面,它會檢查用戶端是否還信任八個私密金鑰已經外洩的 eMudhra 憑證授權單位。我在自己的 Mac 上執行了頁面提供的 curl 測試:
輸入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 憑證、再由根憑證簽署的,正是這組弱金鑰。
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 金鑰又能簽出什麼?
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 根憑證後,就會接受這張憑證,攻擊者也就能讀取並竄改雙向的所有內容。
路徑二:DNS 汙染
DNS 是把網域名稱轉換成 IP 位址的查詢服務。只要能竄改 DNS 的回應,攻擊者甚至不需要待在連線路徑上。被汙染的 DNS 解析器,或是被入侵的路由器分配給用戶端的 DNS 伺服器,會告訴用戶端 pypi.org 位在攻擊者的 IP 位址,用戶端就會直接連過去。
如果沒有 eMudhra 外洩的金鑰,這種攻擊會在 TLS 這一關就被擋下來,因為攻擊者的伺服器拿不出 pypi.org 的有效憑證。但只要用了 eMudhra 中繼 CA 簽出的偽造憑證,連這最後一道防線也能通過,於是被調包的套件就這樣被安裝進來,過程中也不會跳出任何警告。
誰會暴露在風險中?
不管是攔截連線還是 DNS 汙染,攻擊者都需要能控制用戶端和網站之間的某一段網路。我的 Mac 上用的是 Cloudflare WARP,一種類似 VPN 的服務,它會把流量和 DNS 查詢都透過加密通道送到 Cloudflare。所以只要 WARP 開著,本地的 Wi-Fi 和路由器就既沒辦法攔截連線,也沒辦法竄改 DNS 回應。
加密通道能縮小風險範圍,但無法消除風險。CI 執行環境、伺服器和容器的連線都不會經過這條通道,而且只要通道一關,缺口就又出現了。更新信任存放區,才能在任何網路上都把這個缺口補起來。
Mac 上的每個工具從哪裡取得 CA 清單?
一台 Mac 上至少有五份各自獨立的 CA 清單,而且每一份都是由不同的廠商、在不同的日期修補的。
- 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 金鑰簽署憑證的測試主機,所以能在任何環境重複使用:
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/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:
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' \) -print16 份 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 推出修補、或我自己手動清理之前,仍然暴露在危險中:
/usr/bin/curl、git、python3和openssl這些 macOS 內建工具讀的是/etc/ssl/cert.pem,而只有 macOS 更新才會替換這個檔案。到 10 月 21 日為止 Apple 都還沒有推出相對應的 macOS 更新。我寫這篇文章時,Mac 也還停在 26.7.1。- Xcode 和 Command Line Tools 裡的 pip 21.2.4 屬於 Apple,只有在 Apple 更新 Xcode 或 Command Line Tools 時才會換版本,所以最好不要直接使用
/usr/bin/pip3。 - Homebrew 的 Python 3.13 和 3.14 裡的 pip 26.0,要等 Homebrew 重新建置
python@3.13和python@3.14這兩個 formula(Homebrew 的套件定義)之後才會更新。 - 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 改用這份憑證包:
輸入curl -sS -o ~/.config/cacert.pem https://curl.se/ca/cacert.pemCURL_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 certificateMore details here: https://curl.se/docs/sslcerts.html總結
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 的測試主機是最快的方法。





