2. OCSP 请求与响应格式:ASN.1 编码、请求结构、响应结构
说到 OCSP(在线证书状态协议),咱们得先聊聊它的数据格式。
我最早接触这个协议时,第一反应是:“这不就是个查证书状态的请求吗?能有多复杂?”后来真动手抓包分析,才明白里面的门道其实不少。
OCSP 使用 ASN.1 编码,这个选择不是随便定的。原因很简单——X.509 证书体系本来就是基于 ASN.1 的。既然要跟证书打交道,用同一种数据描述语言,能省去不少格式转换的麻烦。
2.1 ASN.1 编码基础
ASN.1(Abstract Syntax Notation One)说白了就是一种数据描述语言。它定义“长什么样”,不管“怎么存”。真正的编码方式有很多种,OCSP 用的是 DER(Distinguished Encoding Rules)——一种确定性编码。
我有个习惯:遇到不确定的 ASN.1 结构,直接上 dumpasn1 工具看一眼。解码出来的树形结构一目了然,比看文档快多了。你想想看,一个嵌套了四五层的结构,光靠人脑推理容易漏掉字段。
OCSP 的 ASN.1 定义在 RFC 6960 里。核心的结构就三个:OCSPRequest、OCSPResponse 和 CertID。每个结构都是嵌套的 SEQUENCE(类似其他语言里的“结构体”)。
2.2 请求结构
OCSP 请求很简单,就是一个 OCSPRequest 结构。我把它拆成两部分:请求信息 和 可选签名。大部分时间我们只看请求信息,签名在请求端很少用。
具体结构如下:
OCSPRequest ::= SEQUENCE {
tbsRequest TBSRequest, -- 必填
optionalSignature [0] EXPLICIT Signature OPTIONAL -- 选填
}
TBSRequest ::= SEQUENCE {
version [0] EXPLICIT Version DEFAULT v1, -- 版本
requestorName [1] EXPLICIT GeneralName OPTIONAL, -- 请求方
requestList SEQUENCE OF Request, -- 请求列表(核心)
requestExtensions [2] EXPLICIT Extensions OPTIONAL -- 扩展
}
Request ::= SEQUENCE {
reqCert CertID, -- 要查的证书
singleRequestExtensions [0] EXPLICIT Extensions OPTIONAL
}
看懂了吗?真正干活的是 requestList,里面放着一个或多个 Request 对象。每个 Request 包含一个 CertID——也就是要查询的证书标识。
| 字段 | 数据类型 | 说明 |
|---|---|---|
| version | INTEGER | 目前只用 v1(值为 0) |
| requestorName | GeneralName | 可选,标识谁发起的请求 |
| requestList | SEQUENCE OF Request | 证书查询列表,至少一个 |
| requestExtensions | Extensions | 扩展字段(如 nonce) |
2.3 CertID:如何唯一标识一张证书
CertID 是请求中最核心的部分。它用了三个字段来唯一锁定一张证书,而不是直接放证书本身。为什么?因为 OCSP 请求往往很小巧,传输效率要高。
CertID ::= SEQUENCE {
hashAlgorithm AlgorithmIdentifier, -- 哈希算法
issuerNameHash OCTET STRING, -- 签发者名称哈希
issuerKeyHash OCTET STRING, -- 签发者公钥哈希
serialNumber CertificateSerialNumber -- 证书序列号
}
注意这个组合:
- issuerNameHash:签发者 DN(可分辨名称)的哈希值
- issuerKeyHash:签发者公钥的哈希值
- serialNumber:待查询证书的序列号
这三个一起用,基本上不会撞车。
哈希算法通常用 SHA-1 或 SHA-256。我个人的建议是:能选 SHA-256 就别用 SHA-1。虽然 OCSP 本身不校验这个哈希的强度,但 SHA-1 现在已经被认为是弱哈希了。
2.4 响应结构
响应比请求复杂一些。它分两层:外层的 OCSPResponse 和内层的 BasicOCSPResponse。
OCSPResponse ::= SEQUENCE {
responseStatus OCSPResponseStatus, -- 状态
responseBytes [0] EXPLICIT ResponseBytes OPTIONAL
}
OCSPResponseStatus ::= ENUMERATED {
successful (0), -- 响应正常
malformedRequest (1), -- 请求格式错误
internalError (2), -- 服务器内部错误
tryLater (3), -- 暂不提供服务
sigRequired (5), -- 需要签名
unauthorized (6) -- 无权查询
}
ResponseBytes ::= SEQUENCE {
responseType OBJECT IDENTIFIER, -- 响应类型 OID
response OCTET STRING -- 实际响应数据
}
BasicOCSPResponse ::= SEQUENCE {
tbsResponseData ResponseData, -- 响应数据(核心)
signatureAlgorithm AlgorithmIdentifier,-- 签名算法
signature BIT STRING, -- 签名值
certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL -- 证书链
}
我最先看到这个嵌套时,觉得设计得不太直观。后来想通了:responseStatus 不等于 successful 时,就不用解析后面的复杂结构了。省电省带宽。
接下来是 ResponseData,这里藏着重放攻击的关键信息:
ResponseData ::= SEQUENCE {
version [0] EXPLICIT Version DEFAULT v1,
responderID ResponderID, -- 响应者标识
producedAt GeneralizedTime, -- 响应生产时间(关键!)
responses SEQUENCE OF SingleResponse, -- 单证书状态列表
responseExtensions [2] EXPLICIT Extensions OPTIONAL
}
SingleResponse ::= SEQUENCE {
certID CertID, -- 证书标识
certStatus CertStatus, -- 状态:good/revoked/unknown
thisUpdate GeneralizedTime,-- 本更新时间
nextUpdate [0] EXPLICIT GeneralizedTime OPTIONAL, -- 下次更新时间
singleExtensions [1] EXPLICIT Extensions OPTIONAL
}
CertStatus ::= CHOICE {
good [0] IMPLICIT NULL,
revoked [1] IMPLICIT RevokedInfo,
unknown [2] IMPLICIT NULL
}
| 字段 | 说明 | 防重放价值 |
|---|---|---|
| producedAt | 响应者生成响应的时间 | 高 |
| thisUpdate | 本次状态更新发布时间 | 中 |
| nextUpdate | 下次状态更新时间 | 中 |
| nonce 扩展 | 请求端植入的随机数 | 最高 |
2.5 核心逻辑:一张图看懂
下面这张图概括了请求到响应的核心流转,以及重放检测点都藏在哪。
从图上你能看到:请求端(客户端)打包 CertID,发送给响应器。响应器生成响应,填入 producedAt(生产时间)、thisUpdate(当前更新)、nextUpdate(下次更新),以及可选的 nonce(随机数)。这些字段,正是我们检测重放攻击的武器库。
2.6 重放攻击为什么能成功?
说白了,OCSP 响应是一段被签名的数据。签名本身防篡改,但不防重放。攻击者只要抓到一个合法的“证书状态良好”响应,稍后找个时机重新发给客户端,客户端如果不检查时间窗口或 nonce,就会相信这个过期的结果。
我曾经在一个实际项目里测试过:用一个 3 天前的 OCSP 响应发给一个配置不当的客户端,它竟然接受了。嗯,这就是忽略了响应结构里时间字段的后果。
2.7 小结一下
- 请求很简单:核心是 CertID,三要素确保唯一标识证书
- 响应有层次:外层看状态,内层看数据、签名、时间
- 时间字段都是重点:producedAt、thisUpdate、nextUpdate 一个都不能漏检
- nonce 是可选但强大的武器:请求端植入,响应端原样返回,一锤子买卖