八把 CA 私钥引发的连锁升级:eMudhra 证书泄露事件与全生态修复清单
一把 P-384 私钥的有效熵只剩几个比特,修复它却牵动了几乎所有主流语言生态。这次事故的关键词不是“吊销”,而是“重新构建”。
先说结论
如果你最近一周没刷到这条消息,值得补一下课:
2026 年 10 月 17 日,有安全研究者发现 CA 机构 eMudhra(emSign)的 8 个中间 CA 证书,其私钥可以被第三方直接推导出来。这些密钥生成于 2025 年 4 月 30 日的一场密钥仪式,由于流程缺陷,本应具备 384 位安全强度的 P-384 密钥,实际有效熵只剩几个比特。
这不是“某张 HTTPS 证书需要吊销”级别的常规事故。私钥可推导,意味着攻击者理论上可以为任意域名签发浏览器会信任的证书,安静地做中间人;其中的代码签名中间证书还能给恶意软件披上合法外衣。
于是过去一周我们看到了一次罕见的全生态连锁响应:
- Chrome、Mozilla、Apple 拉黑了 6 个受影响的 TLS 中间证书,Microsoft 拉黑了 2 个代码签名中间证书;
- Go 发布 1.27.3 / 1.26.10,把拒绝名单硬编码进了
crypto/x509(CVE-2026-111943); - Node.js 发布 10 月 21 日安全版 22.23.4 / 24.22.1 / 26.12.1;
- Python 的 certifi、pip、uv,Rust 的 rustls-webpki、webpki-roots,PHP 的 composer/ca-bundle 全部跟进;
- Oracle 在 10 月 21 日到 22 日为 Java 发布带外安全告警 CVE-2026-91403(CVSS 9.1);
- Electron、curl、Debian/Ubuntu/Alpine 的证书包同步更新。
好消息是,截至 Talos 10 月 21 日发布分析时,尚未在野外发现这些弱密钥被实际滥用,Chrome 在 Certificate Transparency 日志里也没有找到伪造证书。
但“还没被滥用”从来不是“不用升级”的理由——这波紧急修复的意义,就是抢在滥用开始之前把门关上。
事故是怎么回事
把时间线摊开看,问题出在一年半之前。
2025 年 4 月 30 日,eMudhra 为 24 个下级 CA 举行密钥生成仪式。仪式中的 wrap/unwrap 环节存在缺陷,产生了一组强度严重不足的 P-384 密钥。按照 Cisco Talos 的分析,这组密钥的有效熵只有 3 到 8 个比特——而规范要求是 384 位。其中 8 把弱密钥被误用到了 8 张中间证书上:6 张 TLS 证书(EV/OV/DV 的 G3B、G3C 变体)和 2 张代码签名证书,全部挂在 emSign Root TLS CA – G3 与 Root CS CA – G3 之下,另有一条 cross-signed 到 Comodo AAA 的路径。
有效熵只剩几个比特意味着什么?意味着私钥不在“猜不出来”的范畴里,而在“暴力枚举一会儿就能算出来”的范畴里。据报道,其中一把私钥被研究者复原后,值竟然接近一个小到离谱的整数。
这样的密钥在野外存在了大约 17 个月,直到 2026 年 10 月 17 日才被发现。eMudhra 随后吊销了受影响证书,并停止从 G3 根签发新证书。Mozilla Bugzilla #2083449 记录了整个处置过程,Talos 在 10 月 21 日发布了完整分析。
为什么这次的修复方式这么“重”
常规的证书事故,处置是“吊销 + 换证书”,应用层几乎无感。
这次不一样。私钥本身可推导,所以光吊销不够——必须让全世界的 TLS 客户端拒绝这些公钥,不管它们出现在哪张证书里。
麻烦在于,信任这些公钥的地方远不止浏览器:
- 有的运行时把根证书库编译进了二进制(Go 标准库、Rust 的 webpki-roots);
- 有的发行渠道各自维护信任库快照(Python certifi、PHP composer/ca-bundle、各家 Linux 发行版);
- 有的干脆把自己的信任库随 JDK 一起分发。
所以各生态的修复姿势出奇地一致:把 8 个被攻陷证书的 SPKI SHA-256 哈希写进拒绝名单,随紧急版本发出去。Go 团队甚至直接把名单硬编码进了 crypto/x509。
这也带来了这次事故最重要的关键词:重新构建。
升级工具链只是第一步。已经编译好的 Go 二进制里,旧的拒绝名单(或者根本没有名单)还在;已经发布出去的容器镜像里,旧的 ca-certificates 还在;已经分发到用户手里的 Electron 应用,框架层还是旧的。这些东西不会因为 你升级了开发机 就自动修复——它们必须重新构建、重新分发。
各生态修复版本对照表
按 10 月 21 日到 22 日各生态的公告整理:
| 生态 | 修复版本 | 说明 |
|---|---|---|
| Go | 1.27.3 / 1.26.10(CVE-2026-111943) | crypto/x509 内置 8 个被攻陷证书的 SPKI 拒绝名单;已编译的二进制必须重新构建 |
| Node.js | 22.23.4 / 24.22.1 / 26.12.1(10-21 安全版) | TLS 拒绝包含被拉黑公钥的证书链,并更新内置 CA 证书 |
| Python | certifi 2026.10.19;pip 26.2.2;uv 0.13.4 | 从信任库移除 emSign G3 根 |
| Rust | rustls-webpki 0.103.16;webpki-roots 1.0.10 | 更新编译进二进制的根证书数据 |
| PHP | composer/ca-bundle 1.6.0 / 1.4.16 | 不再信任这 8 个中间 CA |
| Java | CVE-2026-91403(OpenJDK 10-21 通告) | 升级 JDK,或替换 blocked.certs,或强制开启吊销检查 |
| Electron | 44.9.1 等 | 升级框架后应用需要重新打包分发 |
| curl / Linux 发行版 | 各家 ca-certificates 更新 | 容器镜像需要重建 |
| Bun / FreeBSD | 截至 10 月 21 日尚未发布修复 | 留意跟进,修了立刻升 |
Java:比想象中更需要动手的一环
Java 这次值得单独拎出来讲,因为它的暴露面和处置方式都容易被忽略。
10 月 21 日,OpenJDK 发布官方通告(CVE-2026-91403,CVSS 9.1,问题出在 javax.net.ssl):JDK 自带的信任库里包含这些被攻陷的 eMudhra 中间 CA,所以独立安装的运行时会信任所有能链到它们——也就是能被伪造——的证书。受影响版本覆盖面很大:
- JDK 27.0.1、25.0.5、21.0.13、17.0.21、11.0.32.1、8u504 及更早版本。
Oracle 同步发布了带外安全告警,10 月 22 日开始各家 CERT 陆续跟进通报。
OpenJDK 通告给了三条路,按优先级:
- 升级独立安装的 JDK 到修复版本;
- 暂时升不了的,用官方提供的
blocked.certs文件替换$JDK_HOME/lib/security/blocked.certs(JDK 8 里叫blacklisted.certs),注意先备份自己加过的条目; - 开启吊销检查作为缓解:
-Dcom.sun.net.ssl.checkRevocation=true -Dcom.sun.security.enableCRLDP=true,签名 JAR 用jarsigner -verify -revcheck -strict校验。
两个容易踩的细节:用系统包管理器装的 JDK(Arch、Debian、Fedora、RHEL、Ubuntu 等)用的是操作系统证书库,更新系统的 ca-certificates 就够;但 FreeBSD 和 Homebrew 的构建要单独更新。另外,一台机器上装了几个 JDK 就要处理几遍,通告里特意强调了这句。
Node.js 这批更新里还藏着一个 RCE
如果你只盯着 eMudhra 事件,可能会漏掉一个对你更直接的风险:Node.js 10 月 21 日这批安全版一共修了约 10 个漏洞,其中 4 个 High,最重的一个不是证书问题,而是:
CVE-2026-111497:Brotli 解码器越界写入,可导致远程代码执行,社区给它起了个和 React2Shell 呼应的名字——“Node2Shell”。三个 LTS 线全部受影响。这个漏洞由 OpenAI Codex Security 的研究员报告。
同一批还修了几个值得留意的问题:
- 两个 TLS 证书主机名校验绕过(全角数字写的 IP、宽松解析到 IP 的主机名),High;
dns.resolve对含 NUL 字符的名称截断处理,High;crypto.randomUUID()/randomInt()的熵缓存被存进了启动快照和 SEA——每个进程会重复输出相同的“随机”值,Medium。如果你的服务靠它生成 token 或 ID,这条不小;- keep-alive socket 处理乱序响应、非规范 IPv4 主机名的证书校验问题,等等。
修复版本:22.23.4 / 24.22.1 / 26.12.1,按你所在的大版本线升级即可。所有已 EOL 的版本都受影响,没有侥幸选项。
你现在应该做什么
按你的技术栈对号入座:
所有人:
# 升级本地与 CI 的工具链(示例:Node)
nvm install 24.22.1 && nvm alias default 24.22.1
然后把构建产物重新走一遍流水线:重新编译、重新打包、重新发布。容器镜像记得拉取新的基础镜像再 build,别命中旧 layer 的缓存。
Go: 升级到 1.27.3 / 1.26.10 后,重新 go build 并重新部署所有服务。只更新 CI 里的 Go 版本而不重新编译发布,等于没修。
Python: 升级 pip 26.2.2、certifi 2026.10.19、uv 0.13.4;CI 和容器里同理。
Rust: cargo update -p rustls-webpki -p webpki-roots,然后重新发布。
PHP: composer update composer/ca-bundle。
Java: 按 OpenJDK 通告升级,或替换 blocked.certs;数一下机器上有几套 JDK。
Electron 应用: 升级框架版本后重新打包、重新分发——已安装的旧包不受新版本保护。
还没修的: Bun 和 FreeBSD 截至 10 月 21 日还没发布修复,留意官方渠道,修了第一时间升。
本站是怎么自测的
写这篇文章的当天,我们也把身边的门关上了:
- 本地开发机的 Node 从 24.14.1(受影响)升级到 24.22.1(修复版);
- 仓库的
.nvmrc从 22.22.2 升到 22.23.4,engines.node同步升到>=22.23.4,让每个clone 这个仓库的人拿到的都是修复后的工具链; - 在新工具链上完整跑了一遍
pnpm install、pnpm lint、pnpm typecheck、pnpm build,全部通过。
运行时方面,本站部署在 Cloudflare Workers 上,出站 TLS 的信任链由平台侧维护,不依赖本机的 ca-certificates;但本地开发和构建环境该升的照样要升——毕竟构建产物的信任边界是从开发机开始的。
整个过程中最直观的感受是:这类系统级升级,pnpm audit 之类的工具完全照不到。它只看你 package.json 里的依赖,不看你的工具链、基础镜像和 JDK。
写在最后
这次事故我想记下三点。
第一,数字信任的底座是熵和流程。384 位的安全强度,因为一次仪式缺陷变成 3 到 8 位,差异是十几个数量级;而这个错误潜伏了 17 个月才被看见。PKI 体系里,“随机数不够随机”从来不是小概率的倒霉事,而是需要仪式、审计和验证去主动防守的单点。
第二,修复的单位从“升级”变成了“重建”。过去我们说“升级修复”,默认改的是源状态;这次,工具链、二进制、容器镜像、已分发的应用,每一层都要显式地重建一遍。供应链越深,重建清单越长。
第三,别把安全检查外包给单一工具。audit、dependabot、renovate 盯着的是应用依赖这薄薄一层;CA 信任库、语言工具链、系统证书包这些“更下面”的东西,目前还得靠人盯着公告。这波各生态从发现(10-17)到全线出修复版(一周内)的响应速度值得肯定,但前提是,你得看到公告。
趁弱密钥还没被滥用,升级吧。
参考
- Cisco Talos: Compromised eMudhra intermediate certificates
- Node.js: Wednesday, October 21, 2026 Security Releases
- OpenJDK Vulnerability Advisory (CVE-2026-91403), 2026-10-21
- Oracle Security Alert Advisory for CVE-2026-91403
- The Hacker News: Eight eMudhra CA Keys Compromised
- OpenVPN: What happened and what to update now
- Mozilla Bugzilla #2083449