多运行时证书信任修补与验证清单
文章称 eMudhra 八个中间证书使用极小私钥,存在证书伪造风险,并列出 Node、Bun、Python、Java 等环境的修补路径。内容还提供证书链拒绝验证方法,以及托管运行时更新滞后时的临时防护方案。
AI 深度解读
一次 CA 私钥事故,为什么会让 Node、Bun、Python 和 Java 都需要排查?原文给出的答案是:同一台机器上的程序,可能各自信任不同的证书集合。浏览器拦住了危险证书,不代表后端请求、包管理器和云函数也已经安全。
作者围绕 eMudhra 事件整理了一份升级清单,并在 Bun、Vercel Functions 上做了连接测试。最有用的结论是:修复要落实到实际运行环境,再用明确的证书拒绝验证结果。下面的事件、版本和测试结果均按原文转述,未经独立核验,不能直接当作已确认的安全公告。
浏览器已经拦截,为什么后端还会接受
按原文描述,eMudhra 在 2025 年 4 月生成的八张中间 CA 证书,其私钥都是小于 256 的整数,其中一个就是数字 4。这不是密钥只有四个字节,而是私钥数值等于 4。
中间 CA 可以签发下级证书。攻击者猜出私钥后,就可能为任意域名伪造证书;只要客户端仍信任对应证书链,请求就可能通过验证。不过,伪造证书不等于自动截获流量,攻击者还需要处在客户端与服务端之间的网络路径上。
原文给出的处置时间线是:10 月 18 日 18:05 UTC,Chrome 根证书项目收到报告,约两小时后封禁相关中间证书,Mozilla 当晚加入 OneCRL;19 日 Apple 跟进,eMudhra 吊销全部八张证书;20 日 Microsoft 跟进,Python 打包团队发布公告;21 日 Node.js、Oracle 发布修复,OpenJDK 发布公告。原文还列出 Chrome 于 10 月 27 日完全移除 G3 根证书的计划,这在当前日期仍是未来安排。
问题卡在「吊销」和「客户端执行吊销检查」之间。原文指出,Node、Bun 和 Python 的 ssl 模块默认不检查证书吊销。服务器证书被吊销了,客户端也可能继续接受。
浏览器的拦截结果,不能替后端运行时证明安全。
作者称没有发现这些私钥被利用的证据,eMudhra 也称多数中间证书未进入生产环境。这些信息影响风险判断,但不能代替修复。
升级清单要追到 venv、容器和 CI 里面
原文列出的修复门槛如下,实际执行前应与对应项目的官方公告核对:
| 环境 | 原文给出的版本或措施 |
|---|---|
| Node.js | 22.23.4、24.22.1、26.12.1 |
| Bun | 1.4.4 |
| pip | 每个 venv 内升级到 26.2.2 |
| certifi | 2026.10.19 |
| uv | 0.13.4 |
| Java | Oracle 更新,或替换 blocked.certs |
| PHP | 更新系统及 ca-certificates |
Node 的修复还包含 Brotli 解码器等其他安全问题。原文称 Node 20 已结束支持,未获得此次修复。CI 也要检查:actions/setup-node 中的 node-version: 24 可能命中旧缓存,作者建议增加 check-latest: true,并在日志里打印 node --version。
Bun 的结论有作者实测支撑:1.3.13 和 1.4.3 都能连接测试站点,1.4.4 则返回 CERT_REVOKED。作者还称 Bun 忽略 SSL_CERT_FILE,所以仅替换这个环境变量指向的文件,不能证明 Bun 已修复。
Python 更容易出现「解释器检查通过,pip 仍有问题」。ssl 模块使用的证书文件取决于构建和配置,而原文称 pip 会同时加载自带的 certifi 与系统证书库,没有开关可以关闭前者。全局升级 pip,不会更新已有 venv 内的副本。
新环境也可能重新带入旧版本:python -m venv 从解释器附带的 ensurepip wheel 初始化 pip。作者建议把 pip install -U pip 设为新建环境后的第一条命令;另一种临时办法是替换 ensurepip/_bundled 中的 wheel 并修改版本字符串。原文将 Homebrew Python 列为例外,称其 pip 的 certifi 指向 Homebrew 维护的证书包。
容器同理。作者引用的情况是,截至 10 月 21 日,官方 slim Python 镜像仍包含 pip 26.2.1。因此要禁用缓存重建,并进入运行中的容器确认版本。
Java 部分,原文将问题编号写为 CVE-2026-91403,评分 9.1,受影响版本包括 Java SE 8u511、11.0.33、17.0.21、21.0.13、25.0.5、27.0.1,以及 GraalVM。Oracle 的修复在 JSSE 中取消对相关中间证书的信任;临时文件在 Java 8 中叫 blacklisted.certs,其他所述版本叫 blocked.certs。原文称 Linux 发行版的 OpenJDK 包通常读取系统证书库,但具体发行版和配置仍需确认。
PHP 同期版本是 8.5.12、8.4.27、8.3.36、8.2.35。不过作者指出,此问题更依赖 PHP OpenSSL 实际读取的系统证书库,不能只盯 PHP 版本。
云函数没升级时,应用能补上哪一层
作者在 10 月 21、22、23 日分别部署 Vercel Functions 测试函数,三次报告的都是 Node v24.21.0,并成功连接测试站点,返回 CONNECTED 200。这能说明作者当时测试的部署环境存在缺口,不能外推为所有区域、所有项目的统一状态。
原文利用 Node 24 的 tls.getCACertificates() 和 tls.setDefaultCACertificates(),在冷启动时移除指定根证书:
import * as tls from 'node:tls';
import { X509Certificate } from 'node:crypto';
const DISTRUSTED = ['emSign ECC Root CA - G3', 'AAA Certificate Services'];
export function dropEmudhraRoots(): number {
const all = tls.getCACertificates('default');
const keep = all.filter((pem) => {
const cn = new X509Certificate(pem).subject
.split('\n')
.find((line) => line.startsWith('CN='));
return !DISTRUSTED.some((name) => cn === `CN=${name}`);
});
if (keep.length !== all.length) tls.setDefaultCACertificates(keep);
return all.length - keep.length;
}
关键是调用 tls.setDefaultCACertificates(keep) 的那一行,它修改当前进程的默认信任集合。作者在 Next.js 的 instrumentation.ts 中通过 register() 调用该函数,记录移除数量;旧环境移除一个根证书后,测试请求变为 UNABLE_TO_GET_ISSUER_CERT_LOCALLY。
数组中的 AAA Certificate Services 是原文所述交叉签名链涉及的 Sectigo 根证书。作者称 Node 内置库没有它,这一项只是预防。作者还测试了 Stripe、Shopify、Anthropic 和 Vercel Blob,连接均正常,但这不是完整的业务兼容性验证。
修复最终要看连接行为。下面这组命令分别测试 curl、Node、Bun 和 Python:
curl https://dv-g3b-emudhra.we-have-a-problem.com # 安全结果为 error (60)
node -e "fetch('https://dv-g3b-emudhra.we-have-a-problem.com').then(() => console.log('EXPOSED'), (e) => console.log('rejected:', e.cause?.code ?? e.message))"
bun -e "fetch('https://dv-g3b-emudhra.we-have-a-problem.com').then(() => console.log('EXPOSED'), (e) => console.log('rejected:', e.code ?? e.message))"
python3 -c "import urllib.request as u; u.urlopen('https://dv-g3b-emudhra.we-have-a-problem.com'); print('EXPOSED')"
要看的是错误原因:DNS 失败和超时都不能算通过,curl 的错误也需要确认来自证书验证。测试站点的证书链是否仍符合预期,同样是这个探针成立的前提。
我的判断
我认同作者把检查单位从「机器」细化到「实际发请求的程序」。前端团队的 Node 构建任务、Next.js 服务端和部署平台,很可能运行着不同版本;Python 工具又可能藏在独立 venv 中。检查宿主机一次,覆盖不了这些环境。
但原文没有提供可核验的公告链接、完整实验环境或对照数据。尤其是版本门槛、Node 20 生命周期、pip 的证书加载行为,以及发行版 OpenJDK 的处理方式,都不应仅凭这篇文章直接进入生产变更单。uv 0.13.4 只出现在清单里,原文没有解释其修复机制或测试结果。
应用内删根证书可以作为临时措施,但会改变该进程的信任范围,也未必覆盖自行传入 CA 的客户端。它比等待平台升级多了一种应急选择,同时也比直接升级运行时承担更多维护责任。
行为探针比单看版本更接近真实结果,但一次探针不能覆盖所有 TLS 调用路径。
作者提到每天及包管理器升级后运行检查脚本,还扫描磁盘上的 certifi 包。这是有价值的维护经验,不过原文没有提供脚本,不能把描述当成可复现交付。
能拿来做什么
- 给 Node、Bun、Python 和 Java 各列出实际使用位置,至少覆盖本地、CI、容器和云函数;先确认是否使用,再对照官方公告决定修复范围。
- 在 CI 与部署诊断日志中输出真实运行时版本,并保留证书拒绝原因,避免只凭配置文件中的版本声明判断结果。
- 使用 Python 工具的团队应逐个检查 venv 中的 pip 和 certifi,并把升级步骤加入环境初始化流程,防止新环境重新带入旧证书包。
- 若托管平台暂未更新,可评估原文的进程内删根方案,在初始化阶段执行、回归关键外部接口,并在平台完成修复后移除临时代码。