4、证书验证与使用:证书路径验证、CRL/OCSP在线状态检查、TLS握手中的证书协商

证书拿到手,不是终点,而是起点。我见过太多团队把证书部署上去就以为万事大吉,结果线上出了大问题才回头排查——嗯,证书验证这个环节,其实才是真正考验功底的地方。

这一章我们聚焦三个核心问题:怎么证明证书链是可信的?怎么知道证书还没被吊销?TLS握手时证书是怎么协商出来的?说白了,就是回答「这张证书到底能不能用」。

4.1 证书路径验证:信任是怎么传递的?

你想想看,浏览器里预装了上百个根证书,但你的服务器证书又不是根CA直接签的。中间可能隔了两三层。浏览器凭什么信任你?

靠的就是证书路径构建与验证(Path Validation)。

我习惯把这个过程拆成两步:

  1. 路径构建:从终端实体证书往上找,直到找到一个信任锚(根证书)。
  2. 路径验证:沿着这条链,逐级检查签名、有效期、密钥用法、名称约束等。

核心原则:信任是单向传递的。根CA信任中间CA,中间CA信任终端实体。但反过来不行——终端实体不能代表中间CA做任何事。

具体验证时,RFC 5280 规定了至少要做这几项检查:

检查项 说明 我踩过的坑
签名验证 用父证书的公钥验子证书的签名 有一次中间CA证书的签名算法是SHA1,但终端证书是SHA256,链直接断了
有效期检查 每个证书的当前时间必须在notBefore和notAfter之间 跨时区服务器时间不同步,导致证书「提前过期」
密钥用法检查 CA证书必须有keyCertSign,终端证书不能有 有人把终端证书配了keyCertSign,直接被OpenSSL拒绝
名称约束检查 中间CA签发的证书域名必须在允许范围内 某次私有CA没配nameConstraints,子CA签了不该签的域名
基本约束检查 CA证书的pathLenConstraint限制了链深度 超过深度限制的链,验证直接失败

一个小技巧:我写验证代码时,习惯先构建完整的证书链,再逐级验证。不要边构建边验证——路径可能有多条,先找到最优路径再验证更可靠。

4.2 CRL与OCSP:证书的「死亡证明」

证书还没过期,但私钥泄露了怎么办?或者员工离职了,证书得立刻作废。这时候就需要吊销检查。

业界有两种主流方案:CRL 和 OCSP。我分别说说。

4.2.1 CRL(证书吊销列表)

CRL 说白了就是一张黑名单。CA 定期发布一个列表,里面是所有被吊销证书的序列号。验证方下载这个列表,查一下目标证书在不在里面。

优点:简单,离线也能查。
缺点:列表会越来越大,实时性差。

我曾经在一个大客户现场遇到过一个问题:他们的 CRL 文件已经超过 50MB,每次下载都要好几秒,TLS 握手直接超时。后来我们切到了 OCSP 才解决。

注意:CRL 有有效期(nextUpdate)。如果验证方拿到的 CRL 已经过期,必须拒绝信任。我见过有人缓存了过期的 CRL 没刷新,结果吊销的证书还在用。

4.2.2 OCSP(在线证书状态协议)

OCSP 是实时查询。验证方向 OCSP 响应器发一个请求,问「这张证书现在什么状态?」响应器返回 good、revoked 或 unknown。

优点:实时、轻量。
缺点:OCSP 响应器可能挂掉,或者被中间人攻击。

为了解决 OCSP 响应器单点故障的问题,业界搞了个OCSP Stapling。什么意思呢?服务器自己定期去 OCSP 响应器拿一个带时间戳的响应,然后在 TLS 握手时「钉」给客户端。客户端不用再主动查 OCSP,省了一次网络开销。

我建议:生产环境优先用 OCSP Stapling。既保证了实时性,又避免了客户端直连 OCSP 响应器的性能瓶颈和隐私泄露问题。

下面这张图展示了 CRL 和 OCSP 的核心流程对比:

CRL 与 OCSP 流程对比 CRL 流程 1. 客户端下载 CRL 文件 2. 检查 CRL 有效期 3. 查找证书序列号 4. 返回 good / revoked OCSP 流程 1. 客户端发送 OCSP 请求 2. OCSP 响应器查询数据库 3. 响应器签名响应 4. 返回 good / revoked / unknown 关键区别: CRL:客户端主动下载列表,离线可用,但实时性差,列表体积大。 OCSP:客户端实时查询,响应小,但依赖响应器可用性。OCSP Stapling 可缓解此问题。

4.3 TLS握手中的证书协商

终于到了最实际的部分。TLS 握手时,证书是怎么被选出来、发出去、被验证的?

我以 TLS 1.3 为例,把流程简化成几个关键步骤:

  1. ClientHello:客户端告诉服务器自己支持的密码套件、TLS 版本,以及可选的 SNI(服务器名称指示)。
  2. ServerHello + 证书:服务器根据 SNI 选出一张合适的证书,连同完整的证书链(不含根)一起发给客户端。
  3. 证书验证:客户端收到证书链后,执行我们前面说的路径验证 + 吊销检查。
  4. 密钥交换:验证通过后,继续完成握手,生成会话密钥。

一个常见的坑:服务器配置了多张证书,但 SNI 匹配逻辑写错了。我曾经排查过一个案例,客户端发来 SNI 是 www.example.com,服务器却返回了 *.example.com 的通配符证书。虽然也能用,但浏览器弹了安全警告——因为通配符证书不匹配具体子域名。

关于证书协商,有几个关键点我特别想强调:

  • 证书链必须完整:服务器发证书链时,一定要包含中间证书,不能只发终端证书。否则客户端没有中间证书,构建不了路径。我见过太多运维只配了服务器证书,忘了配中间证书链。
  • OCSP Stapling 的协商:服务器在 Certificate 消息之后,可以带上一个 CertificateStatus 消息,里面就是 OCSP Stapling 响应。客户端如果支持,就直接用这个响应,不再主动查 OCSP。
  • 证书类型匹配:ECC 证书和 RSA 证书的密码套件不同。服务器要根据客户端支持的密码套件,选择合适的证书类型。如果客户端只支持 ECDHE 套件,服务器却配了 RSA 证书,握手会失败。

我个人的习惯:在 Nginx 或 Apache 里配置证书时,我会用 OpenSSL 命令行先验证一遍证书链:openssl verify -CAfile root.pem -untrusted intermediate.pem server.pem。这条命令能提前发现链不完整、顺序不对、签名算法不兼容等问题。

最后说一个真实案例。有一次我帮一个客户排查 TLS 握手失败的问题。客户端报错「certificate unknown」,但证书链看起来没问题。折腾了半天,发现是 OCSP 响应器的 URL 配错了——证书里的 Authority Information Access 扩展指向了一个内网地址,客户端访问不到。嗯,这种问题,光看证书本身是看不出来的,必须抓包看扩展字段。

证书验证这件事,细节决定成败。路径验证、吊销检查、握手协商,环环相扣。任何一个环节出问题,用户看到的都是「连接不安全」或者「无法建立安全连接」。所以,我建议你在上线前,把这三个环节都走一遍端到端测试,别等到线上出问题再回头查。