跳到内容

八把 CA 私钥引发的连锁升级:eMudhra 证书泄露事件与全生态修复清单

一把 P-384 私钥的有效熵只剩几个比特,修复它却牵动了几乎所有主流语言生态。这次事故的关键词不是“吊销”,而是“重新构建”。

作者hopes78 分钟tls, pki, ca, emudhra, nodejs, go, java, 供应链安全

先说结论

如果你最近一周没刷到这条消息,值得补一下课:

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 通告给了三条路,按优先级:

  1. 升级独立安装的 JDK 到修复版本;
  2. 暂时升不了的,用官方提供的 blocked.certs 文件替换 $JDK_HOME/lib/security/blocked.certs(JDK 8 里叫 blacklisted.certs),注意先备份自己加过的条目;
  3. 开启吊销检查作为缓解:-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)到全线出修复版(一周内)的响应速度值得肯定,但前提是,你得看到公告。

趁弱密钥还没被滥用,升级吧。

参考

#tls#pki#ca#emudhra#nodejs#go#java#供应链安全

相关文章