bigywater.tech
Java · 412

证书透明度:如何验证证书发布记录

证书透明度(CT)把“证书已经公开登记”变成可验证的事实。这篇用真实的证书、真实的日志列表和真实的日志服务器,在 Java 里完成它的三步验证:SCT 的签名、日志的 STH 签名、Merkle 包含证明,并确认 JDK 的 TLS 默认不做这些。

5 / 5
能在日志列表里找到日志的 SCT,签名验证为 true 的个数(篡改后全部 false)
32 个哈希
Argon2026h2 日志(约 34 亿条)里一个叶子的审计路径长度;包含证明验证通过
不检查
Java 对没有任何 SCT 的本地证书的 TLS 握手结果:握手成功,没有证书透明度检查

证书里的 SCT

实测 · 本机执行输出
  Google 的公开日志列表 log_list.json:版本 93.4,时间戳 2026-10-04T13:39:14Z,10 个运营者、26 个日志,按状态 {'usable': 21, 'readonly': 2, 'retired': 3}
  取回的证书数(每个站点,含叶子与中间证书):{'github.com': 3, 'www.wikipedia.org': 4, 'example.com': 4}
  github.com:叶子证书主题 CN=github.com,颁发者 Sectigo Public Server Authentication CA DV E36,有效期 2026-09-01 至 2026-11-29,扩展 OID 1.3.6.1.4.1.11129.2.4.2 存在 = true,内嵌 SCT 个数 2
      SCT:version 0,日志 ID d76d7d10d1a7…,Google 'Argon2026h2' log(运营者 Google,状态 usable),时间戳 2026-09-01T00:10:32.017Z,签名算法 hash=4 sig=3,签名 71 字节
      SCT:version 0,日志 ID c8a3c47fc7b3…,Sectigo 'Tiger2026h2'(运营者 Sectigo,状态 usable),时间戳 2026-09-01T00:10:31.981Z,签名算法 hash=4 sig=3,签名 71 字节
  www.wikipedia.org:叶子证书主题 CN=*.wikipedia.org,颁发者 YE2,有效期 2026-08-05 至 2026-11-03,扩展 OID 1.3.6.1.4.1.11129.2.4.2 存在 = true,内嵌 SCT 个数 2
      SCT:version 0,日志 ID d76d7d10d1a7…,Google 'Argon2026h2' log(运营者 Google,状态 usable),时间戳 2026-08-05T20:14:11.646Z,签名算法 hash=4 sig=3,签名 70 字节
      SCT:version 0,日志 ID 1a8b9d6b0ffe…,不在当前日志列表里,时间戳 2026-08-05T20:14:11.986Z,签名算法 hash=4 sig=3,签名 71 字节
  example.com:叶子证书主题 CN=example.com,颁发者 Cloudflare TLS Issuing ECC CA 3,有效期 2026-09-26 至 2026-12-25,扩展 OID 1.3.6.1.4.1.11129.2.4.2 存在 = true,内嵌 SCT 个数 2
      SCT:version 0,日志 ID 944e4387faec…,DigiCert 'Sphinx2026h2'(运营者 DigiCert,状态 usable),时间戳 2026-09-26T22:59:12.866Z,签名算法 hash=4 sig=3,签名 71 字节
      SCT:version 0,日志 ID cb38f715897c…,Cloudflare 'Nimbus2026'(运营者 Cloudflare,状态 usable),时间戳 2026-09-26T22:59:12.894Z,签名算法 hash=4 sig=3,签名 71 字节

验证 SCT 的签名

实测 · 本机执行输出
  github.com:重建的预证书 TBS 656 字节(去掉 SCT 扩展之后;原始叶子 TBS 920 字节)
      Google 'Argon2026h2' log:SCT 签名用日志公钥验证 = true;改 TBS 的 1 位 = false;错误的颁发者哈希 = false;时间戳加 1 ms = false
      Sectigo 'Tiger2026h2':SCT 签名用日志公钥验证 = true;改 TBS 的 1 位 = false;错误的颁发者哈希 = false;时间戳加 1 ms = false
  www.wikipedia.org:重建的预证书 TBS 1243 字节(去掉 SCT 扩展之后;原始叶子 TBS 1514 字节)
      Google 'Argon2026h2' log:SCT 签名用日志公钥验证 = true;改 TBS 的 1 位 = false;错误的颁发者哈希 = false;时间戳加 1 ms = false
      SCT 的日志不在列表里,跳过
  example.com:重建的预证书 TBS 649 字节(去掉 SCT 扩展之后;原始叶子 TBS 913 字节)
      DigiCert 'Sphinx2026h2':SCT 签名用日志公钥验证 = true;改 TBS 的 1 位 = false;错误的颁发者哈希 = false;时间戳加 1 ms = false
      Cloudflare 'Nimbus2026':SCT 签名用日志公钥验证 = true;改 TBS 的 1 位 = false;错误的颁发者哈希 = false;时间戳加 1 ms = false

Merkle 包含证明

实测 · 本机执行输出
  Google 'Argon2026h2' log:get-sth → tree_size 3414504644,时间戳 2026-10-05T10:50:01.110Z,根哈希 c613645227b5877d…
      STH 签名用日志公钥验证 = true
      叶子哈希 8a388d102a405120…;get-proof-by-hash:leaf_index = 2820279912,审计路径 32 个哈希;按 RFC 9162 验证包含证明:算出的根与 STH 的根相同 = true
      把叶子哈希改 1 位后重新计算:根与 STH 的根相同 = false
      get-entries 取回该下标的条目:leaf_input 705 字节,与本地重建的 MerkleTreeLeaf 逐字节相同 = true
  Google 'Argon2026h2' log:get-sth → tree_size 3414504757,时间戳 2026-10-05T10:50:04.114Z,根哈希 53fcf9a8e08826eb…
      STH 签名用日志公钥验证 = true
      叶子哈希 060cbd5c6f38fdf5…;get-proof-by-hash:leaf_index = 2353258003,审计路径 32 个哈希;按 RFC 9162 验证包含证明:算出的根与 STH 的根相同 = true
      把叶子哈希改 1 位后重新计算:根与 STH 的根相同 = false
      get-entries 取回该下标的条目:leaf_input 1292 字节,与本地重建的 MerkleTreeLeaf 逐字节相同 = true

JDK 做不做 CT

实测 · 本机执行输出
  [把本地 CA 加入信任库] 握手成功:协议 TLSv1.3,对端证书 CN=localhost,内嵌 SCT 扩展 = false,TLS 握手里收到的 SCT(SSLSession 没有相关 API)
  [只用 Java 默认的信任库(对照)] 握手失败:SSLHandshakeException:(certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to f
  这张本地证书里 “CT Precertificate SCTs” 出现 = False

分步演示

证书透明度验证
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12 + BouncyCastle 1.78.1(只用来解析与重组 TBSCertificate)与 Jackson;证书由 openssl 4.0.3 s_client 经本机代理取回,日志列表来自 gstatic.com,日志请求(get-sth / get-proof-by-hash / get-entries)发往 ct.googleapis.com;数字是 2026-10-05 当天取到的真实数据
    • 没有验证:外部嵌入 SCT 之外的传递方式(TLS 扩展、OCSP 装订)、静态 CT API(Sunlight / tiles)的日志、一致性证明(get-sth-consensus)、监控与域名告警、SCT 数量策略(Chrome / Apple 的要求)、日志列表之外的日志(wikipedia 的第二个 SCT)
    京ICP备2026067502号-1京公网安备11010502063487号