证书透明度:浏览器如何及时发现假证书?

二十世纪末,浏览器与证书颁发机构(CA)之间的信任几乎完全依赖“名单制”:名单里列出哪些 CA 可信,浏览器就默认接受他们签发的一切证书。DigiNotar 事件等案例1提醒我们,这套模式太容易被误用或滥用,一张假证书可能在无人知晓的情况下就进入用户的 TLS 连接。Certificate Transparency(简称 CT)因此被提出,希望把签发过程晒在阳光下,让任何人都能验证“谁在什么时候为谁签了证”。

CT 的要求很直接:每一张被浏览器接受的证书,都需要写入公开日志。日志给出时间戳、原始证书内容,还附带一份可以独立验证的签名回执。域名持有者、监控服务或浏览器本身都可以据此对账,一旦发现不在计划内的证书,就能迅速追踪来源。

证书为什么需要被监督

要理解 CT 的价值,可以先回顾传统 TLS 信任链的三件麻烦事:

  1. 受信任的 CA 分布在全球各地,任何一家的配置失误或安全事故,都可能波及所有用户。
  2. 签发过程几乎没有公开信息,域名持有者往往事后才知道“有人帮我领了一张证书”。
  3. 纠错靠人工:上报、调查、吊销,往往以天为单位。

CT 给出的补救方案是“公开账本”——签发写入日志,日志返回一个 Signed Certificate Timestamp(SCT)作为凭证。浏览器在 TLS 握手时可以要求服务器一并提供 SCT,如果拿不到,就可以怀疑这张证书没有公开登记。

CT 的工作原理概览

协议细节很多,但从宏观角度可以把流程压缩成三步:

  1. 记账:CA 签发证书后,把原文提交到日志,换回一份 SCT。
  2. 校验:服务器在 TLS 握手时把证书和 SCT 一起交给浏览器。
  3. 审计:浏览器或第三方监控服务定期向日志查询,确认这张证书确实在记录里,而且日志没有“对不同人展示不同历史”。

这三步把证书生态里的参与者串成闭环,谁要是故意瞒报、伪造或篡改,就会在审核环节暴露。

日志里有哪些角色

CT 的生态通常包含四类参与者:

  • 日志(Log):维护一个只追加的记录簿,定期发布带有数字签名的树根(Signed Tree Head,STH)。
  • 证书颁发机构(CA):负责签发证书,并从日志获取 SCT。
  • 网站与浏览器:服务器把证书和 SCT 一起交给客户端;浏览器随机抽检日志,必要时还会拒绝缺少 SCT 的连接。
  • 监控者与审计者:独立的社区成员、域名持有者或安全团队,持续下载日志增量,检查是否出现陌生证书或不一致的树根。

这四类角色的职责相互制衡,形成了“有人记账、有人对账、有人抽查”的体系。

我为什么会关注 CT

我接触 CT 是因为在 Cloudflare 控制台里看到了 Certificate Transparency Monitoring2。完成域名验证之后,可以订阅“证书变更提醒”:每当有 CA 为我的域名签发新证书,Cloudflare 就会在几分钟内给我发邮件。第一次收到提醒时,我才意识到:即使证书由第三方代办,也能第一时间得知变更情况。

在企业环境里,类似的提醒会接入监控平台。有陌生证书出现就报警,必要时自动触发吊销或流量隔离。公开日志把证书安全从“等事故发生”变成“持续观测”的日常工作。

去哪里查公开日志

想要验证自己的解析器实现,需要先挑选一两个公开日志作为数据源。Chrome 安全团队维护着最新的已知日志列表入口3,里头列出了运营方、可用区间与访问地址。建议按以下方式挑选目标日志:

  1. 打开 Chrome 提供的 log list 页面,页面会注明当前版本的列表发布日期,并提供带签名的 JSON 下载链接。先确认这个列表的更新时间,再按照页面提示获取最新的 log_list.jsonlog_list.json.sig,确保手上的数据仍被 Chrome 信任。
  2. 针对感兴趣的运营商,查看对应日志条目的 url 字段,它就是 REST 接口的基础前缀。这些地址会随着批次和地理区域调整,因此务必以最新列表内容为准,而不是抄录过往示例。
  3. 如果需要长期监控,建议记录 temporal_interval 的起止时间。Chrome 会定期新增或下线日志,只要定期回到该页面重新获取列表,就能及时发现变化。

确认目标日志之后,可以先抓取最新的 Signed Tree Head,验证本地实现能正确校验签名,再逐步下载增量数据并喂给解析器。整个流程只依赖日志公开的 REST 接口,重复执行上述查询即可持续获取最新入口。

Merkle 树如何支撑公开日志

日志既要允许持续追加,又要让外部观察者快速验证“这条记录确实在里边”。RFC 6962 选择了 Merkle 树:

  1. 每条记录(也叫叶子)包含版本号、时间戳、条目类型和证书主体。叶子先经过哈希,得出固定长度摘要。
  2. 相邻的叶子两两组合继续哈希,层层向上,最后得到树根,也就是 STH 里的核心字段。
  3. 任何人都可以向日志请求“叶子到树根的哈希路径”。验证这条路径是否吻合,就能确认记录确实存在,或者比对不同时间点的树根,检查日志是否造假。

一旦某个观察者拿到了互相矛盾的树根,就可以证明日志对不同人展示了不同历史。Chrome、Safari 等浏览器正是因为有这个自证机制,才敢把 CT 当作硬性要求。

动手实现一个简单的解析器

想从日志里拿到真正的证书内容,需要读懂两块二进制数据:leaf_input(Merkle 树叶子的主体)和 extra_data(额外附带的信息)。下面按照日志的编码顺序,逐步拆解其中的结构,并给出可以直接套用的实现思路。

leaf_input 的开头是一个固定格式的头部,字段顺序如下:

字段长度说明
version1 字节日志协议版本,目前为 0 或 1
merkle_leaf_type1 字节固定为 0,表示是时间戳日志条目
timestamp8 字节毫秒级 Unix 时间戳
log_entry_type2 字节0 表示 X.509 证书,1 表示 Precertificate
entry剩余所有字节具体的证书数据或其他结构

解析头部时要顺序读取这些字段,并把未解析的部分保留在 entry 字段中,后面还要根据条目类型继续拆解。若你使用 Rust,可以写出类似下面的伪代码:

fn parse_merkle_tree_header(mut input: Bytes) -> (Header, Bytes) {
    let version = input.get_u8();
    let _leaf_type = input.get_u8();
    let timestamp = input.get_u64();
    let entry_type = input.get_u16();
    let entry = input.split_to(input.len());
    (Header { version, timestamp, entry_type }, entry)
}

完成这一步后,解析器就已经掌握了日志条目的“抬头”:版本号、时间戳和条目类型。

日志条目主要分成两类:

  • X.509:存放已经签发的正式证书;
  • Precertificate:存放预证书,方便外部提前审计。

解析器可以在读取完头部后,根据 entry_type 走不同的分支:如果是 X.509,就继续在 entry 中解析主体证书,并把 extra_data 里的字节当作证书链;如果是 Precertificate,就把 extra_data 当成一个包含主体和链条的结构整体解析。把分支逻辑集中放在一起,未来要支持新的条目类型时也能更容易地扩展。

CT 日志广泛使用 24 位大端整数表示长度,例如证书主体前面会写入三个字节的长度,紧跟着才是真实的 DER 数据。读取方法可以用一个 4 字节缓冲手动组装:

fn get_u24(input: &mut Bytes) -> u32 {
    let mut buf = [0u8; 4];
    buf[1..4].copy_from_slice(&input[..3]);
    input.advance(3);
    u32::from_be_bytes(buf)
}

读取长度之后要立刻检查是否越界,例如 length <= input.len()。这样可以提前发现日志损坏或实现错误,而不是等到 OpenSSL 解析失败才排查。

当你已经得到主体证书的长度,就可以把相应的字节切出来,交给 X.509 解析库(OpenSSL、rustls 或其他语言的标准库都可以)。典型的做法是把常用字段(序列号、颁发者、使用者、公钥算法、有效期、指纹等)抽取出来,再把原始 DER 一并保存,方便后续验证。

证书链的结构与主体类似,同样是“24 位长度 + 实际证书数据”的组合。解析链条时可以循环调用前面同样的读取函数,直到输入字节被完全消费。如果条目类型是 Precertificate,则要先解析出主体,再继续按照相同的方式解析链条。

完成上述步骤后,就可以把解析到的信息汇总成一个结构体,例如:

struct LogEntry {
    version: u8,
    timestamp: u64,
    entry_type: EntryType,
    certificate: ParsedCertificate,
    chain: Vec<ParsedCertificate>,
}

无论你使用哪种语言,实现思路都是:先拆头部确定类型,再按 24 位长度逐段读取证书和链条。只要把握住这个顺序,就能快速写出一个足够应对日常审计任务的 CT 日志解析器,用来验证告警邮件中的证书是否真实存在,或是把日志数据接入更完整的安全分析流程。

结语

CT 把证书签发过程变成了人人可查的公共记录:日志负责记账,CA 提供凭证,浏览器和审计者负责核对。理解日志中二进制数据的基本结构之后,实现一个轻量级解析器并不困难,它能帮助你把 CT 数据接入现有的监控体系,在第一时间发现异常证书。

延伸阅读


  1. 2011 年 DigiNotar 遭入侵,攻击者伪造了大量热门站点证书,并被浏览器接受。 ↩︎

  2. Cloudflare 的 CT Monitoring 服务:https://developers.cloudflare.com/ssl/edge-certificates/additional-options/certificate-transparency-monitoring/ ↩︎

  3. Chrome 团队维护的已知日志列表入口:https://googlechrome.github.io/CertificateTransparency/log_lists.html ↩︎