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 憑證授權單位。我在自己的 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 憑證、再由根憑證簽署的,正是這組弱金鑰。

弱金鑰如何傳到每一個用戶端2025 年 4 月 30 日的金鑰產生儀式在 HSM 裡產生了金鑰。接著的金鑰包裝/解包步驟產生了兩組金鑰:一組是沒有出現在公開憑證裡的強金鑰,另一組是私密金鑰為小於 256 整數的弱金鑰。弱公開金鑰被放進 8 張中繼 CA 憑證,由 emSign ECC Root CA - G3 簽署,並由 Sectigo AAA 交叉簽署。由於各家根憑證存放區本來就信任這些根憑證,每個用戶端都會接受這些中繼 CA。包裝步驟出錯的原因仍在調查中。金鑰產生儀式(2025 年 4 月 30 日)在 HSM 裡產生金鑰金鑰包裝/解包步驟出錯的原因仍在調查中強金鑰組沒有出現在公開的憑證裡弱金鑰組私密金鑰是小於 256 的整數emSign ECC Root CA - G3由 Sectigo AAA 交叉簽署簽署8 張中繼 CA 憑證裡面放的是弱公開金鑰各家根憑證存放區本來就信任這些根憑證,所以每個用戶端都會接受這些中繼 CA
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 金鑰又能簽出什麼?

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 根憑證後,就會接受這張憑證,攻擊者也就能讀取並竄改雙向的所有內容。

路徑一:攔截連線由左到右是用戶端、位在連線路徑上的攻擊者(例如 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. 攻擊者把流量轉送到真正的網站,同時讀取並竄改內容
憑證鏈的終點是用戶端本來就信任的根憑證,所以偽造的憑證也能通過驗證。

路徑二:DNS 汙染

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

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

路徑二: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 上用的是 Cloudflare WARP,一種類似 VPN 的服務,它會把流量和 DNS 查詢都透過加密通道送到 Cloudflare。所以只要 WARP 開著,本地的 Wi-Fi 和路由器就既沒辦法攔截連線,也沒辦法竄改 DNS 回應。

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

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

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

一台 Mac 上的五份 CA 清單五列表格,每列把一組工具對應到它讀取的 CA 清單,以及修補該清單的一方。Safari、使用鑰匙圈的 App 和 Go 所用的 macOS 平台驗證器,在 Apple 於 10 月 19 日封鎖之後拒絕了測試主機。系統的 curl、git、python3、openssl 讀取的 /etc/ssl/cert.pem 沒有更新,仍然信任測試主機。Node.js 和 Claude Code 內建的清單在更新後拒絕了測試主機。pip 和 requests 讀取的 certifi,在每個 venv 升級之前都還是舊版。uv 內建的清單也拒絕了測試主機。工具讀取的 CA 清單我的 Mac 上Safari、使用鑰匙圈的 AppmacOS 上的 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 各一份)certifi 2026.10.19(10/20)升級前仍是舊版uvwebpki-root-certs(編譯進執行檔)uv 0.13.4拒絕Chrome 和 Firefox 另外有自己的封鎖清單(CRLSet、OneCRL)。
每份 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 金鑰簽署憑證的測試主機,所以能在任何環境重複使用:

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.22CONNECTED仍然信任 eMudhra 的憑證鏈
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 的平台驗證器。也就是說,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' \) -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 改用這份憑證包:

終端機視窗
輸入
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 這八個中繼 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 的測試主機是最快的方法。

參考連結

分享這篇文章