vatt'ghern jaskier's ballads

八張 eMudhra 中繼 CA 憑證的私鑰,每一把都只有單一位元組。Scott Helme 在文章裡寫,他在自己的筆電上把八把全數還原,花不到一秒。

八個位元的信任:eMudhra 中繼 CA

十月十七日是個週六,一位資安研究者向 Apple、Google、Microsoft、Mozilla 與 eMudhra 通報:eMudhra 旗下八張中繼 CA 憑證的私鑰,是弱到可以從公開憑證直接算出來的單一位元組金鑰,曲線是 P-384。三天後的十月二十日,Scott Helme 把這件事寫成一篇文章。這篇報導的事實幾乎全部來自那篇文章,也就是單一來源;文章裡轉述 Mozilla 公告與 eMudhra 說明的地方,下面都會標明是轉述,我自己的算術與推測也會用「我的算術」「合理的推測是」這類字眼隔開。順序按時間走:憑證什麼時候簽出去,弱點怎麼被確認,瀏覽器怎麼反應,以及到今天還有誰沒被保護到。

從憑證建立到通報,再到瀏覽器收口(時間間隔不成比例) 2025 年 4 月 八張中繼憑證建立 六張 TLS 發行 CA,兩張程式碼簽章 CA 2025 年 5 月 Sectigo 交叉簽發 AAA Certificate Services 根為每一張都簽了一次 約 17 個月,文章寫 there is no way to know 10 月 17 日 週六 研究者通報 對象:Apple、Google、Microsoft、Mozilla、eMudhra 同日晚間 Chrome 推出 CRLSet 更新 封鎖這八張中繼憑證 數小時後 Firefox 經 OneCRL 封鎖 Mozilla 的封鎖,文章寫在 Chrome 之後 10 月 20 日 週二 Mozilla 公開公告,發布 NSS 更新 移除 eMudhra 根;Chrome 確認根隨 Chrome Root Store v43 處理 Scott Helme 的文章同日發表 UTC 20 點 16 分 文章更新 轉述 eMudhra 在 Mozilla 事件單上的說明 2039 年 7 月 韌體凍結裝置的信任到期 文章寫這些裝置會一路信任到憑證到期
虛線是時間軸的斷點,不是比例尺。晚間與數小時後兩格,文章只給了這樣的相對說法,沒有給鐘點。

2025 年春天,八張憑證簽出去

按文章的說法,這八張中繼憑證是六張 TLS 發行 CA 與兩張程式碼簽章 CA。六張 TLS 發行 CA 涵蓋 DV、OV、EV 三種驗證等級,每種各有兩代。八張全部建立於 2025 年 4 月。到了 2025 年 5 月,Sectigo 的 AAA Certificate Services 根又為每一張都做了交叉簽發。從那時起,每一張中繼憑證至少有兩條通往根的路。其中通往 Sectigo 的那條,文章寫明了;另一條通往 eMudhra 自己的根,是我對中繼憑證一般結構的推測,文章談到移除 eMudhra 的根,卻沒有逐張交代誰簽了誰。

中繼 CA 的位置值得先停一下。終端用戶端通常只內建根憑證,真正替網站簽發憑證的是根底下的中繼 CA。誰握有一把中繼 CA 的私鑰,就握有簽出這條鏈上的用戶端都會相信的憑證的能力。這是 PKI 的一般常識,不是文章的新說法,放在這裡是要讓後面的數字有分量:八把私鑰,每一把都能簽,而且每一把都只有一個位元組。

Sectigo AAA Certificate Services 根,2025 年 5 月交叉簽發每一張 六張 TLS 發行 CA,DV、OV、EV 各兩代 DV世代一 DV世代二 OV世代一 OV世代二 EV世代一 EV世代二 暴露面:信任 CA bundle、不查撤銷也不查 CT 的非瀏覽器 TLS 用戶端 兩張程式碼簽章 CA 程式碼簽章第一張 程式碼簽章第二張 暴露面:文章寫 Windows 直接信任 eMudhra CS 根 文章說這不是 TLS,但是同一個缺陷 八張全部建立於 2025 年 4 月;Mozilla 公告附件有 16 張受影響憑證
世代一、世代二只是我為了區分每種各兩代而取的標籤,文章沒有替它們命名。16 張與 8 張之間的關係,文章沒有在我讀到的段落裡拆解。

接下來是文章最讓人不舒服的一段。弱金鑰在憑證建立的那一刻就已經存在,文章寫的是自 2025 年 4 月以來的任何時候,任何人都能還原這些金鑰,並稱之為「That's a 17 month window and there is no way to know whether someone did.」,也就是 17 個月的窗口,而且無從得知這段時間有沒有人利用過。

文章也提供了一個能縮小疑慮的線索:據文章說,沒有任何鏈到這幾個 CA 的 TLS 憑證出現在 Certificate Transparency 的紀錄裡,Chrome 因此表示預設下沒有偽造憑證可能被信任。這能排除的是簽出之後送進 CT 的濫用,排除不了從頭到尾沒進 CT 的偽造憑證,後面會看到,文章自己也沒有把 CT 當成萬靈丹。

一個位元組,只有 256 種可能

橢圓曲線金鑰的結構很簡單。文章這樣描述:公開金鑰,也就是放進憑證給全世界看的那個東西,是私鑰 d 乘上曲線的基點 G。「Going forwards is easy, but going backwards」,從 d 算出公開金鑰很容易,從公開金鑰推回 d 則被認為不切實際。文章把這稱為整個安全模型,並指出它成立的前提是 d 夠大。

這一次 d 不大。文章寫「A single byte has 256 possible values」,並且說這些 CA 能簽出的每一張憑證、信任過它們的每一個用戶端,全都建立在八個位元上(原文是 rested on eight bits)。對照之下,P-384 名稱裡的 384 指的是曲線的位元長度,正常的私鑰值域在 2 的 384 次方這個量級,這是一般密碼學常識,不是文章的數字。從 8 位元到 384 位元,候選數不是多 48 倍,而是多了 2 的 376 次方倍,這是我的算術。

怎麼還原?依我讀到的文章內容,做法就是從 d = 1、2、3 一路試下去,把算出來的點拿去跟憑證裡的公開金鑰比對。位元組只有 256 種值,最壞情況也只要試幾百次。文章給的結果是「I recovered all of them from their public certificates on my laptop just now, and it took less than a second.」。還原只需要公開憑證,不需要闖進任何系統。Mozilla 把受影響的憑證附在公告裡,文章作者寫「Mozilla attached the affected certificates to their announcement, so I pulled all 16」,並說「Every single one fell over instantly」。16 比 8 多一倍,合理的推測是八張中繼憑證加上八張交叉簽發的版本,但文章在我讀到的段落裡沒有這樣拆解。

這個做法有一個要留意的性質:整個檢查只用得到公開資訊。憑證本來就要送給每一個連線的用戶端,任何人拿到它,就能對它的公開金鑰重做同樣的試算。換句話說,驗證這件事不需要通報者的信任,也不需要 eMudhra 配合,文章作者自己在筆電上就重現了結果。反過來,合理的推測是,同樣的檢查如果在簽發當下做一次,應該很便宜;但文章沒有談簽發流程裡有沒有這類檢查,這一段純屬我的推論。

拖動位元長度與猜測速度,看窮舉的最壞情況 · 2 個滑桿

32
6
窮舉最壞要試 2 的 n 次方次,位元每加 1,候選數翻倍 文章的情況 單一位元組,8 位元 候選 256 個,最壞 約 256 微秒 你選的長度 32 位元,猜測速度 10^6 次每秒 候選 4,294,967,296 個,最壞 約 71.6 分鐘 對照 P-384 名稱裡的 384 位元(一般常識,非文章數字) 候選 約 3.94 × 10^115 個,最壞 約 1.2 × 10^102 年 猜測速度是你設的假設,不是文章量到的;長條長度與位元數成正比。
這是玩具推算:最壞情況等於把整個值域試完,平均約為一半。猜測速度由你設定,文章只說還原在筆電上花不到一秒,沒有給每秒猜幾次。

把位元長度拉到 32、速度設在每秒一百萬次,最壞情況從不到一毫秒變成約一個多小時,多出來的 24 位元就是 2 的 24 次方倍。再把速度拉到最快的每秒十億次,384 位元的最壞情況仍落在 10 的 99 次方年這個量級,這是我用小工具算的,不是文章的數字。八個位元和三百八十四個位元之間的距離,說明了為什麼這件事不需要任何人去偷金鑰。

文章對這個差異的說法,大意是:一般的 CA 金鑰事故,是有人把金鑰偷走;這一次沒有人需要偷任何東西,金鑰從憑證存在的第一天起就等於公開。這裡是轉述大意,不是逐字引文。一般的 CA 金鑰事故有一個被偷的時間點,可以倒推影響範圍;這裡影響範圍的起點,就是憑證建立的那一天。文章另外引了一句「the issue cannot be contained」,據文章,這是 Ian Carroll 在 Mozilla 討論串中的留言。

這個差異對應變流程的影響很具體。平常的 CA 事故,封鎖名單和撤銷機制處理的是被偷之後的憑證;這裡沒有被偷的時間點,所以要處理的是從簽發起的所有憑證。合理的推測是,這也是那句 cannot be contained 的來由:封鎖名單只能影響收到它的用戶端,收不到的用戶端不會因此知道這八把金鑰早已公開。這是我對那句評語的讀法,文章引述的上下文我沒有完整讀到。

通報當晚,瀏覽器先動手

通報發生在 10 月 17 日。文章寫 Chrome 在「that evening」推出 CRLSet 更新封鎖這幾張中繼憑證,Mozilla 則在「a few hours later」透過 OneCRL 在 Firefox 封鎖。依文章的語序,我讀成兩者都在通報當天,但文章沒有給鐘點,幾個小時只能照這個說法抄。CRLSet 與 OneCRL 是兩家瀏覽器各自推送封鎖名單的機制,讓它們不必等憑證到期,也不必等用戶端逐一查詢撤銷狀態,這是我補的背景,不是文章的描述。

接著是根本身。文章寫週二,也就是 10 月 20 日,「Mozilla announced it publicly and shipped an NSS update removing the eMudhra root」,Chrome 則確認這個根會隨 Chrome Root Store v43 處理(原句是 the root is going in Chrome Root Store v43,我讀成移除)。NSS 是 Mozilla 的信任存放區,不少下游軟體的 CA bundle 以它為來源之一,這是一般背景,文章在我讀到的段落沒有寫到這一層。從封鎖中繼憑證到移除根,是兩個不同層次的動作:前者讓已知的壞憑證失效,後者讓整個 CA 失去信任錨點。

瀏覽器這邊還有第二層保險。文章寫「Chrome and Firefox block the intermediates directly and both require CT, and Safari requires CT too.」,並下了判斷:「Mainstream and updated browsers are likely absolutely fine.」換成工程語言:在更新過的主流瀏覽器上,一張偽造憑證要被接受,得先過封鎖名單,再過要求 CT 的檢查,而文章說沒有相關憑證出現在 CT。這個判斷的限定詞是 updated,沒更新的瀏覽器不在範圍內;Safari 有沒有同樣的封鎖,文章只說它要求 CT,其餘我沒有讀到。CT 的功能,文章講得很保守:「CT detects mis-issuance though, it doesn't prevent it, and a lot of software doesn't check CT at all.」

把這幾個動作排在一起看,節奏很不一樣。封鎖名單以小時計:Chrome 在通報當晚,Firefox 隔了數小時。根的移除以天計:10 月 20 日 Mozilla 發布 NSS 更新,Chrome 的處理綁在 Chrome Root Store v43 這個版本上。到了 CA bundle 與韌體,文章給的就不再是日期,而是更新的建議與不能更新的難處。這是我整理文章敘述得到的觀察,不是文章自己的結論:越靠近使用者手上那份靜態的信任清單,反應越慢,也越依賴有人去動手。

瀏覽器以外,沒有人替你推更新

麻煩從這裡開始。文章接著寫「Everything else is a different story, though.」這一類用戶端,文章列舉了 curl、Python、Node、Go、Java、git 與套件管理器,共通點是「they trust a CA bundle on disk, they don't check revocation by default, and they don't check CT.」信任的來源是磁碟上的一個 CA bundle 檔案,沒有人替它推送封鎖名單,也沒有 CT 這道後備。要讓它安全,只剩一條路:換掉 bundle。

點選用戶端,看偽造憑證闖過哪幾關 · 8 種用戶端

同一張偽造憑證遇到的三道關卡 curl 偽造的 葉憑證 ① 封鎖名單 放行 沒有推送 靠自己更新 CA bundle ② CT 檢查 放行 不檢查 CT 文章寫預設不查 ③ 撤銷檢查 放行 預設不檢查 文章寫預設不查 文章實測,curl 對偽造憑證回報驗證通過。 依據:本地伺服器加 api.stripe.com 偽造憑證的測試,curl 回報 SSL certificate verify ok。 關卡狀態只採文章明說的內容,標「未說明」的格子不代表沒有這道關卡。
每一格都來自文章的明說。Go 與 Java 的格子是依文章把它們歸入同一類而填,文章列出的實測結果只有 curl、Python 與 Node 三個。

文章不只推理,還做了測試。作者寫他在自己的機器上,用本地伺服器與一張 api.stripe.com 的偽造憑證測試,結果是「curl said」SSL certificate verify ok,Python 完成了 TLS 握手,Node 回報 authorized 為 true。三個用戶端對一張沒有任何合法來源的憑證,都給了通過的答案。上面的關卡圖把差別攤開:同一張偽造憑證,在更新過的瀏覽器面前要連闖封鎖名單與 CT 兩關,在這些用戶端面前一關都沒有。有兩點要記得:文章測的只有 curl、Python 與 Node,Go 與 Java 是被歸在同一類,沒有列實測;這些測試描述的是尚未更新 bundle 的用戶端,這是我對測試情境的讀法。

三個不同生態系的用戶端給出同樣的結論,合理的推測是問題出在它們共同信任的 CA 清單內容,而不是某一個函式庫的 bug;文章沒有逐一檢查每個函式庫用的是哪份 bundle,這個推測也只能到此為止。

文章有一句直接點出後果:「A forged certificate here means running the attacker's code, often as root.」這句話的前後文我讀到的片段沒有逐字附上,從語意看,它談的是靠這類用戶端下載並執行東西的場景,比如安裝程式,這個讀法是我的推測。把它和上面的測試放在一起,風險的樣子就清楚了:問題在於用戶端接受的是什麼,而不是攻擊者要花多少力氣。

更新 bundle 能解的是能更新的機器。文章寫「Their bundles are frozen until 2039」,並說這就是前面 cannot be contained 那一點變成了現實。路由器、印表機、電視與韌體凍結的家電,文章寫它們會一路信任這些憑證直到到期,也就是 2039 年 7 月(原句是 until expiry, which is July 2039)。「Devices you can't update are the hard part.」文章這樣收這一段。另外一條線在程式碼簽章:文章寫「Windows trusts the eMudhra CS root directly.」,並強調 It isn't TLS,但是同一個缺陷。

2039 年 7 月離這次通報有多遠?從 2026 年 10 月算起大約 12 年又 9 個月,這是我的算術。一台在這段時間內都不更新韌體的裝置,信任這八把金鑰的時間,比 17 個月的暴露窗口長得多。

前面提到 Sectigo 的交叉簽發,它和只移除 eMudhra 的根夠不夠這個問題有關。交叉簽發的意思是同一把中繼金鑰有兩張憑證,由不同的根簽出,用戶端只要信任其中一個根,就能建出通往這把中繼金鑰的路徑。所以假如某個用戶端移除了 eMudhra 的根、卻還留著 Sectigo 的 AAA 根,路徑仍然存在,除非這八張中繼憑證另外被封鎖。這是我從交叉簽發的定義推出來的,文章沒有分析各家 bundle 的實際內容,也沒有說 Sectigo 的根該不該動。下面的小工具是這個推理的玩具版本。

勾選用戶端的信任設定,看信任路徑通不通 · 3 個開關

eMudhra 根 用戶端是否信任 Sectigo AAA 根 用戶端是否信任 推測的原簽發 2025 年 5 月交叉簽發 八張中繼 CA 私鑰只有一個位元組 用還原的私鑰簽 攻擊者簽出的 偽造葉憑證 接受 兩條路徑都成立 用戶端只要信任任何一個根,就能建出通往弱金鑰的路徑。
這是依交叉簽發定義寫的玩具邏輯,不代表任何真實 bundle 的內容。eMudhra 根到中繼憑證那條線是我的推測,文章只明說了 Sectigo 的交叉簽發。

文章給的處理清單很短。「Update everything that ships a CA bundle」,點名的有 OS 的 ca-certificates 套件、certifi、Node 與 Homebrew;容器映像則是「rebuild your container images once the base images pick up the change.」,先等基底映像跟上,再重建。這份清單我沒有看到更多條目,所以也不替它補。至於不能更新的那一批,文章沒有提出解法,只把它們標成難處。

eMudhra 的說明,以及還沒答完的問題

文章發表後不久,Mozilla 把事件單公開了(原句是 Shortly after publishing, Mozilla made their incident ticket public),作者因此在 UTC 時間 10 月 20 日 20 點 16 分加了一則更新。更新轉述 eMudhra 在事件單上的說明:「It seems there was a bug in the script used during the ceremony, a second, weak key pair was created on the HSM」,另有「a human error lead to the wrong keys being used for the CSR.」換成白話:金鑰儀式用的腳本似乎有 bug,在 HSM 上多產生了第二組弱金鑰,人為失誤又讓錯的那組被拿去做憑證簽署請求。這是文章作者轉述 eMudhra 的說法,開頭還有 It seems 的保留,我沒有讀到事件單原文,無法獨立核實。作者自己的結論是「Nothing in the text of my article above has changed as a result of this new information.」

我的讀法是,這個說明回答的是弱金鑰從哪裡來,還沒有回答為什麼沒有人在 17 個月裡發現。前者是儀式與流程的問題,後者牽涉到憑證簽發後有沒有人檢查金鑰長度,這兩件事事件單裡有沒有談,我不知道,所以只當作還沒答完的部分。

把更新放回整件事的因果裡:eMudhra 的說明講的是弱金鑰如何出現,不改變金鑰已經弱了 17 個月、八張憑證仍受影響的事實。作者在更新裡也是這個立場,他說原文沒有因為新資訊改動。對維運的人來說,原因影響的是 eMudhra 之後怎麼補救,不影響你這週要更新哪些 bundle。

更新裡還有一句和方法有關:「I can also confirm that a simple prompt in an AI agent allowed me to recover the private keys of the CA.」作者說,他用一個 AI agent、一次簡單的提示就還原了 CA 私鑰。這句話能支撐的結論有限:它說明門檻低到不需要專門的工具鏈。至於 AI 工具能不能還原一把正常長度的 P-384 私鑰,我的推論是不能,因為這類金鑰的安全性靠的是 d 夠大,而這幾把的 d 只有一個位元組;這是我的判斷,文章沒有討論。

還沒有答案的部分,我分開列。有沒有人實際利用過:文章的答案是無從得知,沒有 CT 紀錄只能排除一部分情況。更新時程:文章給了 NSS 與 Chrome 的動作,沒有給 OS 發行版、certifi、Node 這些何時跟上的日期。韌體凍結的設備:文章給的期限是 2039 年 7 月,中間沒有補救辦法。eMudhra 說明裡 bug 與人為失誤的細節:寫在 Mozilla 的事件單上,我沒有讀到。

如果你維運的環境裡有 curl、Python、Node 或 Go 寫的服務,這件事的實務意義很直接:先查清楚你的 CA bundle 是哪個套件提供的,更新它;容器映像則等基底映像跟上之後重建。文章列的清單就是這些。不更新的機器能不能靠其他手段擋下,文章沒有討論,我也不替它編。

這篇報導本身的限制,我也要說清楚。事實幾乎全部來自 Scott Helme 的單篇文章,他引用了研究者的通報、Mozilla 的公告與 eMudhra 的說明,但我沒有分別讀過那些原件;eMudhra 的說明更是兩層轉述。所以凡是某個機構做了什麼的句子,都應該讀成文章說該機構做了什麼。

What still bites:瀏覽器在通報當天就擋下八張中繼憑證,但只信 CA bundle 的用戶端沒有人替它們推更新,韌體凍結的裝置更是要信任這些憑證到 2039 年 7 月。