eMudhra 有 8 张中间 CA 证书的 P-384 私钥是小于 256 的整数,Mozilla 和 Google 都把这件事定性为私钥失陷(private key compromise)。私钥几乎瞬间就能恢复,对还没撤销信任的客户端,拿到私钥就能签出冒充任意网站的证书。Go 1.26.10 已经按公钥哈希把这几张中间证书拉黑。我用一个 Spring Boot 4.1.1 小程序在 Debian 打包的 OpenJDK 21 上检查,JVM 默认信任库能把 12 张相关证书全部验到受信任的根。换成 Temurin 21.0.12.1,经 Sectigo 交叉签名的那一半找不到路径,经 emSign 自家根的另一半照样通过;10 月 20 日发布的 OpenJDK 27.0.1 自带的 cacerts 也全部放行。

这几张中间证书出了什么事

据 Mozilla Bug 2083449,2025 年 4 月 30 日那次密钥生成仪式产出 24 张中间证书,其中 B、C 两个系列的 P-384 私钥全是小于 256 的整数,除了最后一个字节基本全是 0。Google 的公告把原因概括为生成密钥时随机性不足。受影响的 8 张里,6 张是 TLS 用的 DV、OV、EV 各 G3B 和 G3C,另外 2 张是代码签名。eMudhra 在 10 月 18 日(UTC)把这 8 张以 caCompromise 为原因吊销。

麻烦在于每张 TLS 中间证书都有两个签发者。一份由 eMudhra 自己的 emSign Root TLS CA - G3 签发,这个 CA 又被 emSign ECC Root CA - G3 交叉签名;另一份由 Sectigo 的老根 AAA Certificate Services 交叉签名。所以一共 12 张证书,同一把弱私钥。

浏览器那边处理得很快。Chrome 在 10 月 17 日 19:45 UTC 通过 CRLSet 按 SPKI 哈希拉黑,并宣布把 emSign ECC Root CA - G3 移出 Chrome Root Store,预计 10 月 27 日(PDT)对所有依赖 Chrome Root Store 的设备生效。Mozilla 也决定紧急移除这个根。Sectigo 在 Bugzilla 里说明,AAA Certificate Services 早就不在 Chrome 和 Mozilla 的根库里了,但微软和苹果还信它。

服务端组件跟进得参差不齐。Go 在 10 月 19 日发布 1.26.10 和 1.27.3,对应 CVE-2026-111943,在 crypto/x509 的 Verify 里加了一张 8 个 SPKI 哈希的黑名单。Docker 29.9.2 的发布说明直接写了后果:镜像仓库的证书链如果用到这几张中间证书,pull 和 push 会在 TLS 校验那一步失败。Prometheus 3.15.1 则在容器镜像的 CA 证书里去掉了对 emSign ECC Root CA - G3 和 Comodo AAA Services root 的信任。

Java 这边,JDK 的信任来自 cacerts 文件,没有 Go 这次加的那种按公钥哈希过滤。所以我想知道的就是:Spring Boot 服务常见的几种 JDK,默认信任库会不会把这几张证书验到根。

实验怎么做

我没有去恢复私钥,也没有伪造任何证书。12 张中间证书都是公开的,从 crt.sh 按指纹下载,SHA-256 和 Bugzilla 里列的逐个核对过。另外下载了一张 emSign ECC Root CA - G3 给 emSign Root TLS CA - G3 签的证书,只用来补全链路。只要中间证书能被验到某个受信任的根,用它的私钥签出来的任何服务端证书都会走同一条路径,所以检查中间证书这一层就够说明问题。

应用是 start.spring.io 生成的 Spring Boot 4.1.1 项目,一个 CommandLineRunner。信任库留空时用 TrustManagerFactory.init(null),这正是 RestClient、WebClient 默认 SSLContext 背后用的那份 JVM 默认信任库:

TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
X509TrustManager tm = (X509TrustManager) tmf.getTrustManagers()[0];
Set<TrustAnchor> anchors = new HashSet<>();
for (X509Certificate c : tm.getAcceptedIssuers()) {
    anchors.add(new TrustAnchor(c, null));
}

然后对每张证书用 PKIX 建路径,把 emSign Root TLS CA - G3 那张交叉证书放进 CertStore,模拟服务端在握手时把中间证书一起发过来。吊销检查关掉,和 JSSE 默认行为一致,后面会讲为什么:

X509CertSelector target = new X509CertSelector();
target.setCertificate(cert);
PKIXBuilderParameters params = new PKIXBuilderParameters(anchors, target);
params.setRevocationEnabled(false);
params.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(List.of(cert, cross))));
PKIXCertPathBuilderResult r = (PKIXCertPathBuilderResult) CertPathBuilder.getInstance("PKIX").build(params);

Go 那边写了一个等价的小程序,用 Debian 的系统 CA bundle 当根,同样把交叉证书放进 Intermediates,分别用 Go 1.26.9 和 1.26.10 跑。所有场景在一个 verify.sh 里一次跑完:

### 环境
2026-10-22 08:27:46 CST
spring-boot 4.1.1
ca-certificates 20250419
被检查的证书(crt.sh 下载,SHA-256 与 Mozilla Bug 2083449 列出的一致):
DV-G3B-aaa  45A883F349AEDFD5
DV-G3B-emsign  42C4ACCD12C4F595
DV-G3C-aaa  62B7F9868263F72F
DV-G3C-emsign  0164D7CEAE5BB909
EV-G3B-aaa  579480AEA166290B
EV-G3B-emsign  E7568FE6E12331F4
EV-G3C-aaa  FD9D5C2DBBD4995A
EV-G3C-emsign  432EC25F0D28DE90
OV-G3B-aaa  5F1DED4DD590AF74
OV-G3B-emsign  98B896625C0C230A
OV-G3C-aaa  7485B51C0ED19287
OV-G3C-emsign  CB6E15165AC802E9
交叉证书(不在 Bugzilla 列表里,只放进 CertStore/Intermediates 供建链):
emSign-Root-TLS-CA-G3-by-ECC-G3  3094025792039A68

三份信任库里装了哪些根

### 三份信任库里有没有这两个根
$ keytool -list -keystore /etc/ssl/certs/java/cacerts | 查 AAA Certificate Services 与 emSign ECC Root CA - G3
AAA Certificate Services=1  emSign ECC Root CA - G3=1
$ keytool -list -keystore tools/jdk-21.0.12.1+1/lib/security/cacerts | 查 AAA Certificate Services 与 emSign ECC Root CA - G3
AAA Certificate Services=0  emSign ECC Root CA - G3=1
$ keytool -list -keystore tools/jdk-27.0.1/lib/security/cacerts | 查 AAA Certificate Services 与 emSign ECC Root CA - G3
AAA Certificate Services=1  emSign ECC Root CA - G3=1

Debian 的 ca-certificates 20250419 生成的 Java cacerts 里两个根都在。Temurin 21.0.12.1 自带的 cacerts 没有 AAA Certificate Services,有 emSign ECC Root CA - G3。OpenJDK 27.0.1 是 10 月 20 日跟着 Oracle 季度更新发布的版本,自带的 cacerts 两个都有。

Debian 的 OpenJDK 21:12 张全部通过

### A: Debian 的 OpenJDK 21,JVM 默认信任库
$ java -jar target/trustcheck-0.0.1-SNAPSHOT.jar --check.certs-dir=certs
java=21.0.12.1 vendor=Debian truststore=JVM 默认 anchors=150
DV-G3B-aaa  通过(根=AAA Certificate Services)
DV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
DV-G3C-aaa  通过(根=AAA Certificate Services)
DV-G3C-emsign  通过(根=emSign ECC Root CA - G3)
EV-G3B-aaa  通过(根=AAA Certificate Services)
EV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
EV-G3C-aaa  通过(根=AAA Certificate Services)
EV-G3C-emsign  通过(根=emSign ECC Root CA - G3)
OV-G3B-aaa  通过(根=AAA Certificate Services)
OV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
OV-G3C-aaa  通过(根=AAA Certificate Services)
OV-G3C-emsign  通过(根=emSign ECC Root CA - G3)

这是在 Debian 上装 openjdk-21-jdk-headless(21.0.12.1+1-1~deb13u1)之后的默认状态。12 张全部能验到根,经 Sectigo 的 6 张落在 AAA Certificate Services,经 emSign 的 6 张落在 emSign ECC Root CA - G3。

Temurin 只挡住一半

### B: Temurin 21.0.12.1,JVM 默认信任库
$ tools/jdk-21.0.12.1+1/bin/java -jar target/trustcheck-0.0.1-SNAPSHOT.jar --check.certs-dir=certs
java=21.0.12.1 vendor=Eclipse Adoptium truststore=JVM 默认 anchors=118
DV-G3B-aaa  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
DV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
DV-G3C-aaa  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
DV-G3C-emsign  通过(根=emSign ECC Root CA - G3)
EV-G3B-aaa  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
EV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
EV-G3C-aaa  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
EV-G3C-emsign  通过(根=emSign ECC Root CA - G3)
OV-G3B-aaa  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
OV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
OV-G3C-aaa  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
OV-G3C-emsign  通过(根=emSign ECC Root CA - G3)

-aaa 的 6 张找不到路径,因为 Temurin 的 cacerts 里没有 Sectigo 那个老根。-emsign 的 6 张照样通过,对端在握手时发来的只要是 emSign 这条链,Temurin 的默认信任库同样能验到根。

OpenJDK 27.0.1 的 cacerts 也全部放行

### C: Temurin 运行,信任库换成 OpenJDK 27.0.1 自带的 cacerts
$ tools/jdk-21.0.12.1+1/bin/java -jar target/trustcheck-0.0.1-SNAPSHOT.jar --check.certs-dir=certs --check.truststore=tools/jdk-27.0.1/lib/security/cacerts
java=21.0.12.1 vendor=Eclipse Adoptium truststore=tools/jdk-27.0.1/lib/security/cacerts anchors=110
DV-G3B-aaa  通过(根=AAA Certificate Services)
DV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
DV-G3C-aaa  通过(根=AAA Certificate Services)
DV-G3C-emsign  通过(根=emSign ECC Root CA - G3)
EV-G3B-aaa  通过(根=AAA Certificate Services)
EV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
EV-G3C-aaa  通过(根=AAA Certificate Services)
EV-G3C-emsign  通过(根=emSign ECC Root CA - G3)
OV-G3B-aaa  通过(根=AAA Certificate Services)
OV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
OV-G3C-aaa  通过(根=AAA Certificate Services)
OV-G3C-emsign  通过(根=emSign ECC Root CA - G3)

运行时还是 Temurin 21,只是把信任库换成 OpenJDK 27.0.1 自带的那份。结果和 Debian 一样,12 张全部通过。这一版 10 月 20 日发布,自带的 cacerts 里这两个根都还在。

用 blocked.certs 可以按证书挡住

JDK 的 lib/security/blocked.certs 是一份按证书 SHA-256 指纹拉黑的列表,PKIX 校验时会查。我复制了一份 Temurin,把 12 张证书的指纹追加进去,信任库用 Debian 那份全都放行的 cacerts。先看不改 blocked.certs 时的对照:

### D0: Temurin 运行,信任库换成 Debian 的 cacerts(blocked.certs 未改)
$ tools/jdk-21.0.12.1+1/bin/java -jar target/trustcheck-0.0.1-SNAPSHOT.jar --check.certs-dir=certs --check.truststore=/etc/ssl/certs/java/cacerts
java=21.0.12.1 vendor=Eclipse Adoptium truststore=/etc/ssl/certs/java/cacerts anchors=150
DV-G3B-aaa  通过(根=AAA Certificate Services)
DV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
DV-G3C-aaa  通过(根=AAA Certificate Services)
DV-G3C-emsign  通过(根=emSign ECC Root CA - G3)
EV-G3B-aaa  通过(根=AAA Certificate Services)
EV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
EV-G3C-aaa  通过(根=AAA Certificate Services)
EV-G3C-emsign  通过(根=emSign ECC Root CA - G3)
OV-G3B-aaa  通过(根=AAA Certificate Services)
OV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
OV-G3C-aaa  通过(根=AAA Certificate Services)
OV-G3C-emsign  通过(根=emSign ECC Root CA - G3)

再看追加指纹之后:

### D: 把 12 张证书的指纹追加进 JDK 的 lib/security/blocked.certs,信任库仍用 Debian 的
blocked.certs 行数 39 -> 51
$ tools/jdk-blocked/bin/java -jar target/trustcheck-0.0.1-SNAPSHOT.jar --check.certs-dir=certs --check.truststore=/etc/ssl/certs/java/cacerts
java=21.0.12.1 vendor=Eclipse Adoptium truststore=/etc/ssl/certs/java/cacerts anchors=150
DV-G3B-aaa  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
DV-G3B-emsign  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
DV-G3C-aaa  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
DV-G3C-emsign  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
EV-G3B-aaa  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
EV-G3B-emsign  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
EV-G3C-aaa  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
EV-G3C-emsign  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
OV-G3B-aaa  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
OV-G3B-emsign  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
OV-G3C-aaa  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)
OV-G3C-emsign  拒绝(SunCertPathBuilderException: unable to find valid certification path to requested target)

12 张全部被拒。这个办法有两个限制。它按整张证书的指纹拉黑,同一把私钥如果再出现在第三份交叉证书里就挡不住,Go 按 SPKI 哈希拉黑就没有这个问题。它改的是 JDK 目录里的文件,升级 JDK 或者换基础镜像就会被覆盖,得写进镜像构建步骤。更干脆的做法是从信任库里删掉 emSign ECC Root CA - G3 和 AAA Certificate Services,代价是链到这两个根的正常证书也会失效。Chrome 和 Mozilla 这次移除的就是 emSign ECC Root CA - G3,AAA Certificate Services 早已不在它们的根库里。

如果服务用 Spring Boot 的 SSL bundle 给 RestClient 配了自己的信任库,那要检查的是那份 bundle 指向的文件,JVM 默认 cacerts 管不到它。这一块之前在 Spring Boot 热换 HTTPS 证书时私钥没跟着换,握手会全部失败 里讲过配置方式。

Go 1.26.10 拒绝了全部 12 张

### E: Go 1.26.9,系统 CA bundle
$ env GOTOOLCHAIN=local ../tools/go1.26.9/bin/go run . ../certs /etc/ssl/certs/ca-certificates.crt
go=go1.26.9 roots=/etc/ssl/certs/ca-certificates.crt
DV-G3B-aaa  通过(根=AAA Certificate Services)
DV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
DV-G3C-aaa  通过(根=AAA Certificate Services)
DV-G3C-emsign  通过(根=emSign ECC Root CA - G3)
EV-G3B-aaa  通过(根=AAA Certificate Services)
EV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
EV-G3C-aaa  通过(根=AAA Certificate Services)
EV-G3C-emsign  通过(根=emSign ECC Root CA - G3)
OV-G3B-aaa  通过(根=AAA Certificate Services)
OV-G3B-emsign  通过(根=emSign ECC Root CA - G3)
OV-G3C-aaa  通过(根=AAA Certificate Services)
OV-G3C-emsign  通过(根=emSign ECC Root CA - G3)

### F: Go 1.26.10,同一个 CA bundle
$ env GOTOOLCHAIN=local ../tools/go1.26.10/bin/go run . ../certs /etc/ssl/certs/ca-certificates.crt
go=go1.26.10 roots=/etc/ssl/certs/ca-certificates.crt
DV-G3B-aaa  拒绝(x509: no valid chains built)
DV-G3B-emsign  拒绝(x509: no valid chains built)
DV-G3C-aaa  拒绝(x509: no valid chains built)
DV-G3C-emsign  拒绝(x509: no valid chains built)
EV-G3B-aaa  拒绝(x509: no valid chains built)
EV-G3B-emsign  拒绝(x509: no valid chains built)
EV-G3C-aaa  拒绝(x509: no valid chains built)
EV-G3C-emsign  拒绝(x509: no valid chains built)
OV-G3B-aaa  拒绝(x509: no valid chains built)
OV-G3B-emsign  拒绝(x509: no valid chains built)
OV-G3C-aaa  拒绝(x509: no valid chains built)
OV-G3C-emsign  拒绝(x509: no valid chains built)

同一个 CA bundle,Go 1.26.9 全部通过,Go 1.26.10 全部拒绝。拒绝的原因就是 verify.go 里新加的 containsDistrustedSPKIHash,它对链上每张证书算 RawSubjectPublicKeyInfo 的 SHA-256,命中那 8 个哈希就把这条链丢掉。所以升级到 Docker 29.9.2、Prometheus 3.15.1 之后,这些 Go 写的组件不再认这几张证书,同一台机器上用默认信任库的 Java 服务还认。

吊销为什么指望不上

eMudhra 和 Sectigo 都已经吊销了这些证书,但 JSSE 默认不查吊销。Temurin 21.0.12.1 源码里 sun.security.validator.PKIXValidator 是这么写的:

private static final boolean checkTLSRevocation = GetBooleanAction
        .privilegedGetProperty("com.sun.net.ssl.checkRevocation");

用 JDK 默认的 TrustManager 时,TLS 场景下只有显式设置 -Dcom.sun.net.ssl.checkRevocation=true 才会查。就算打开,还要能连上 crl.comodoca.com、crl.emsign.com 这些 CRL 地址。吊销检查没有实测,实验时访问 crl.comodoca.com 超时。

参考来源

— 感谢阅读 —
广告

一起交流

分享你的思考,让讨论更进一步。