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