2. CRL结构详解

CRL(证书吊销列表)的结构,说白了就是一张“黑名单”+一张“身份证”。刚接触时你觉得它不就是几个字段拼在一起吗?但等真上手解析的时候,每一个字节都藏着坑。我个人习惯拿到一个CRL后,先看它的版本号——嗯,这个决定后面怎么解析。

2.1 基本外壳与版本号

每个CRL都是一个ASN.1 DER编码的SignedData对象。最外层的包裹是签名值,里面才是真正的TBSCertList。我当年第一次写解析器时,直接跳过外层签名字段去读吊销列表,结果一个字节都没对上……

TBSCertList的第一个字段就是版本号。目前有两种主流版本:

版本枚举值说明
v10基础版,不含扩展字段
v21支持扩展项,如吊销理由、签发者密钥标识符等
经验之谈:现在99%的CA都发v2版本的CRL。如果遇到v1,先怀疑是老旧系统或者测试证书。

2.2 签发者与更新时段

接下来是签发者(Issuer)字段。它是一个DN(可分辨名称),必须和签发该CRL的CA证书里的Subject字段完全一致。我在项目中遇到过CA换发新证书后,DN里多了一个空格,结果CRL验证直接失败了——因为X.509对比DN时是按原始字节比对的。

本次更新(thisUpdate)和下次更新(nextUpdate)是时间戳。这两个字段定义了一个时间窗口:从thisUpdate开始,到nextUpdate结束,客户端只在这个窗口内信任该CRL。我曾经在生产环境看到一个CA把nextUpdate设成了过去的时间,所有客户端立刻将所有证书视为不可信……

  • thisUpdate:签发时间,必须≤当前时间。
  • nextUpdate:过期时间,必须>thisUpdate。
  • 如果nextUpdate缺失(v1允许),客户端通常默认CRL不过期——但非常危险。
警告:不要跳过nextUpdate的校验。很多安全事件就是利用“无过期CRL”进行的重放攻击。

2.3 吊销列表项

这是CRL的核心——一个有序集合,每个元素包含两项必需信息和可选扩展:

  1. 用户证书序列号(必选)——要吊销的证书ID。
  2. 吊销日期(必选)——证书被撤销的时间。
  3. 吊销扩展(可选)——比如吊销理由、无效日期等。

序列号是CA自己分配的,同一CRL内不能重复。我调试过一个奇怪的bug:两个条目序列号相同但吊销日期不同,客户端只匹配第一个,导致第二个永远不被处理。你想想看,这种问题根本不会报错,只有事后审计才能发现。

建议:解析吊销列表项时,务必强制检查序列号唯一性。标准没说必须唯一,但实践中重复序列号就是人为错误。

2.4 字段汇总表

把整个TBSCertList的关键字段整理一下,方便你对照:

字段可选/必选我的备注
版本必选v2才有扩展支持
签名算法必选必须和外层签名算法一致
签发者必选空格、大小写都敏感
本次更新必选UTC或GeneralizedTime,注意时区
下次更新可选缺失时视为不过期
吊销列表项可选空列表表示无吊销
扩展可选如吊销理由、CRL编号
签名值(外层)必选对上述所有字段的签名

看张图会更清楚整体结构。

CRL结构图 CRL结构图(TBSCertList + 签名) 外部签名(SignedData包壳) TBSCertList 版本 (v1/v2) 签名算法 签发者 (DN) 本次更新 下次更新 (可选) 吊销列表项 序列号 | 吊销日期 | 扩展 扩展 (可选) 签名值(外层) 签名值是对整个TBSCertList的签名 扩展可以为空,但版本如果为v2则必须包含扩展字段

2.5 容易踩的坑

收尾前,我再唠叨几句实战中常见的雷:

  • DN 比较问题:不同编码(如PrintableString和UTF8String)即便内容一致,字节也不同。我用openssl asn1parse看过一个实际CRL,签发者字段里面混了一个空格字符……
  • 时间格式坑:thisUpdate如果是UTCTime,年份只支持00-99;GeneralizedTime才能支持四位年份。建议统一转换成UTC再比较。
  • 空吊销列表:CRL的吊销列表项可以为空(SEQUENCE OF 长度为0)。但有些旧库解析会抛异常——你最好放过这个case。
  • 签名算法一致性:TBSCertList里的signature字段必须和外层签名算法字段一致。不一致的话,直接拒绝。
个人习惯:每拿到一个CRL,我都会先用openssl crl -in crl.der -text -noout把结构打出来,肉眼过一遍关键字段。这个方法虽然原始,但能发现很多自动化工具遗漏的异常。

理解了这个结构,后面讲CRL验证和完整性保障时,你就能快速定位问题出在哪个字段上了。