2. CRL结构详解
CRL(证书吊销列表)的结构,说白了就是一张“黑名单”+一张“身份证”。刚接触时你觉得它不就是几个字段拼在一起吗?但等真上手解析的时候,每一个字节都藏着坑。我个人习惯拿到一个CRL后,先看它的版本号——嗯,这个决定后面怎么解析。
2.1 基本外壳与版本号
每个CRL都是一个ASN.1 DER编码的SignedData对象。最外层的包裹是签名值,里面才是真正的TBSCertList。我当年第一次写解析器时,直接跳过外层签名字段去读吊销列表,结果一个字节都没对上……
TBSCertList的第一个字段就是版本号。目前有两种主流版本:
| 版本 | 枚举值 | 说明 |
|---|---|---|
| v1 | 0 | 基础版,不含扩展字段 |
| v2 | 1 | 支持扩展项,如吊销理由、签发者密钥标识符等 |
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不过期——但非常危险。
2.3 吊销列表项
这是CRL的核心——一个有序集合,每个元素包含两项必需信息和可选扩展:
- 用户证书序列号(必选)——要吊销的证书ID。
- 吊销日期(必选)——证书被撤销的时间。
- 吊销扩展(可选)——比如吊销理由、无效日期等。
序列号是CA自己分配的,同一CRL内不能重复。我调试过一个奇怪的bug:两个条目序列号相同但吊销日期不同,客户端只匹配第一个,导致第二个永远不被处理。你想想看,这种问题根本不会报错,只有事后审计才能发现。
2.4 字段汇总表
把整个TBSCertList的关键字段整理一下,方便你对照:
| 字段 | 可选/必选 | 我的备注 |
|---|---|---|
| 版本 | 必选 | v2才有扩展支持 |
| 签名算法 | 必选 | 必须和外层签名算法一致 |
| 签发者 | 必选 | 空格、大小写都敏感 |
| 本次更新 | 必选 | UTC或GeneralizedTime,注意时区 |
| 下次更新 | 可选 | 缺失时视为不过期 |
| 吊销列表项 | 可选 | 空列表表示无吊销 |
| 扩展 | 可选 | 如吊销理由、CRL编号 |
| 签名值(外层) | 必选 | 对上述所有字段的签名 |
看张图会更清楚整体结构。
2.5 容易踩的坑
收尾前,我再唠叨几句实战中常见的雷:
- DN 比较问题:不同编码(如PrintableString和UTF8String)即便内容一致,字节也不同。我用openssl asn1parse看过一个实际CRL,签发者字段里面混了一个空格字符……
- 时间格式坑:thisUpdate如果是UTCTime,年份只支持00-99;GeneralizedTime才能支持四位年份。建议统一转换成UTC再比较。
- 空吊销列表:CRL的吊销列表项可以为空(SEQUENCE OF 长度为0)。但有些旧库解析会抛异常——你最好放过这个case。
- 签名算法一致性:TBSCertList里的signature字段必须和外层签名算法字段一致。不一致的话,直接拒绝。
openssl crl -in crl.der -text -noout把结构打出来,肉眼过一遍关键字段。这个方法虽然原始,但能发现很多自动化工具遗漏的异常。
理解了这个结构,后面讲CRL验证和完整性保障时,你就能快速定位问题出在哪个字段上了。