4、证书路径验证算法(RFC 5280)

好,咱们进入正题。证书链验证,说白了就是回答一个问题:“我手里这张证书,到底能不能信?” 而 RFC 5280 就是回答这个问题的标准答案。我当年第一次啃这个文档的时候,说实话,头大。但后来在项目里踩过几次坑,才真正明白它为什么这么设计。

今天我就带你把这个算法拆开揉碎。核心就三件事:基本约束、名称约束、密钥用法。咱们一个一个来。

4.1 算法总览:验证的骨架

先看整体流程。RFC 5280 的路径验证,不是上来就检查叶子证书,而是从信任锚开始,一层一层往下推。我习惯把它想象成一个“信任接力赛”——每一级 CA 都要证明“我授权了下一级”。

核心原则:路径验证是从根到叶的。信任锚是起点,叶子证书是终点。中间每一跳都必须满足所有约束。

下面这张图,是我自己项目里常用的流程图。你一看就明白。

证书路径验证算法流程图 RFC 5280 证书路径验证流程 信任锚(自签名根CA) 验证中间CA证书(n=1..N-1) ① 基本约束检查 ② 名称约束检查 ③ 密钥用法检查 全部 通过? 是 继续下一级/验证叶子 否 → 验证失败 注意:叶子证书不检查 basicConstraints

嗯,这里要注意:叶子证书不检查 basicConstraints。为什么?因为叶子证书就是最终实体,它不需要再签发下级证书。这个细节,我见过好几个项目搞反了。

4.2 基本约束(Basic Constraints)

这个扩展,是 CA 证书的“身份证”。它告诉验证者:“我是不是一个 CA?如果是,我能签发多少级?”

RFC 5280 定义得很清楚:

  • cA 布尔值:必须为 true,否则该证书不能作为 CA 使用。
  • pathLenConstraint:允许的中间 CA 数量。注意,它不包括当前证书本身,也不包括叶子证书。

我的经验:有一次客户反馈说“证书链明明配对了,但设备就是拒绝”。我查了半天,发现是中间 CA 的 pathLenConstraint 设成了 0。这意味着它只能直接签发叶子证书,不能再有下级 CA。而客户的部署里偏偏多了一级。这就是典型的“约束过死”。

检查逻辑其实很简单:

// 伪代码:基本约束检查
if (cert.isCA == false) {
    // 如果证书不是 CA,但被用来签发其他证书 → 失败
    if (cert.isUsedToSign) return FAIL;
}

if (pathLenConstraint 存在) {
    // 当前路径中剩余的中间 CA 数量
    remaining = pathLenConstraint - (当前路径深度 - 1);
    if (remaining < 0) return FAIL;
}

你想想看,这个设计其实很巧妙。它防止了 CA 被滥用——一个只允许签一级的 CA,永远无法变成根 CA 那样的“超级管理员”。

4.3 名称约束(Name Constraints)

这个扩展,是 CA 的“活动范围”。它限制了 CA 能签发证书的域名、IP 地址或目录名。

我遇到过最典型的坑:名称约束只对下级 CA 生效,对当前 CA 自身不生效。 什么意思?就是父 CA 给子 CA 加了个名称约束“只能签发 example.com 的证书”,但父 CA 自己依然可以签发其他域名的证书。这个不对称性,很多人一开始会搞混。

约束类型 含义 示例
permittedSubtrees 允许的命名空间 permittedDNSDomains = ".example.com"
excludedSubtrees 禁止的命名空间 excludedDNSDomains = ".evil.com"

验证时,算法会做两件事:

  1. 检查下级证书的 subject 是否在 permitted 范围内。
  2. 检查下级证书的 subject 是否不在 excluded 范围内。

注意:名称约束是累积的。路径上每一级 CA 的约束都要叠加。我曾经在调试一个智能家居项目时,发现设备死活连不上云。最后定位到是三级 CA 链,每一级都加了不同的名称约束,叠加后把合法的域名给排除了。这种问题,日志里根本看不出来,只能手动算。

4.4 密钥用法(Key Usage)

这个扩展,是证书的“功能开关”。它告诉验证者:这把密钥能用来干什么。

RFC 5280 定义了 9 种用法,但和证书链验证最相关的是这几个:

  • keyCertSign:用于签发其他证书。CA 证书必须要有这个。
  • cRLSign:用于签发 CRL。如果 CA 用同一把密钥签 CRL,这个位必须设。
  • digitalSignature:用于数字签名(比如 TLS 中的实体认证)。

检查逻辑很直接:

// 伪代码:密钥用法检查
if (证书用于签发下级证书) {
    if (!keyUsage.has(keyCertSign)) return FAIL;
}
if (证书用于签发 CRL) {
    if (!keyUsage.has(cRLSign)) return FAIL;
}

嗯,这里有个细节:如果密钥用法扩展不存在,默认所有用法都允许。 但一旦存在,就必须严格匹配。我建议你在产品里永远显式设置密钥用法,不要依赖默认值。为什么?因为不同平台的默认行为可能不一样,你想想看,这就会导致兼容性问题。

最佳实践:根 CA 证书至少设置 keyCertSign 和 cRLSign。中间 CA 证书设置 keyCertSign。叶子证书根据用途设置 digitalSignature 或 keyEncipherment。

4.5 三个扩展的协同检查

这三个扩展不是独立工作的。它们共同构成了一个“约束网络”。我画个表格帮你理清关系:

扩展 检查对象 失败后果 常见坑
Basic Constraints 所有 CA 证书 路径被拒绝 pathLenConstraint 算错层级
Name Constraints 下级证书的 subject 路径被拒绝 累积约束导致合法域名被排除
Key Usage 当前证书的用途 操作被拒绝 依赖默认值导致跨平台问题

在实际项目中,我习惯把这三个检查写成一个统一的验证函数。每次验证中间 CA 时,按顺序跑一遍。顺序不重要,但一个都不能少。

4.6 避坑指南

最后,分享几个我亲身踩过的坑:

  • 路径深度从 0 开始还是从 1 开始? RFC 5280 说 pathLenConstraint 是“后续中间 CA 的数量”。如果根 CA 直接签叶子,pathLenConstraint 可以是 0。如果根 CA 签了一个中间 CA,中间 CA 再签叶子,那根 CA 的 pathLenConstraint 至少是 1。我见过有人把根 CA 自己也算进去,结果路径长度永远多 1。
  • 名称约束中的 DNS 域名匹配是“后缀匹配”。比如 ".example.com" 匹配 "sub.example.com",但不匹配 "example.com"(注意前面的点)。这个细节,RFC 5280 第 4.2.1.10 节写得很清楚,但实现时很容易忽略。
  • 密钥用法中的 keyCertSign 和 digitalSignature 可以共存。有些 CA 用同一把密钥既签证书又签 OCSP 响应,那就得两个位都设。不要想当然地认为“CA 只能有 keyCertSign”。

我的建议:如果你在写证书验证代码,一定要准备一套边界测试用例。比如 pathLenConstraint=0 的链、名称约束有重叠的链、密钥用法缺失的链。这些用例能帮你提前发现 90% 的兼容性问题。