第3章 OCSP请求格式:理解OCSP请求的结构、扩展字段
排查OCSP异常,光看响应码是不够的。很多时候问题出在请求本身——客户端发出去的请求就不对,服务器自然没法正确回应。我翻过不少抓包文件,发现80%的异常都跟请求格式有关。说白了,你得先知道正常请求长什么样,才能看出哪里不对劲。
3.1 OCSP请求的ASN.1结构概览
OCSP请求用ASN.1编码,传输层一般是DER或BER。整个结构像俄罗斯套娃:最外层是OCSPRequest,里面装TBSRequest,再里面是RequestList。我习惯把TBS理解为“待签名部分”,请求的核心信息都在这里。
| 字段 | 类型 | 说明 |
|---|---|---|
| tbsRequest | TBSRequest | 请求体,包含版本、请求列表、扩展 |
| optionalSignature | Signature | 可选,签名后的鉴别信息 |
TBSRequest内部又有这些家伙——
- version:默认0(v1),大部分时候用不到。
- requestList:核心数组,每个元素是一个
SingleRequest。 - requestExtensions:扩展字段,非常重要,后面单独说。
3.2 SingleRequest与CertID
每个SingleRequest对应一张要查询状态的证书。它包含一个CertID,以及可选的扩展。CertID的生成逻辑是OCSP排查的重灾区。
CertID ::= SEQUENCE {
hashAlgorithm AlgorithmIdentifier,
issuerNameHash OCTET STRING, -- 颁发者DN的哈希
issuerKeyHash OCTET STRING, -- 颁发者公钥的哈希
serialNumber CertificateSerialNumber
}
嗯,这里要注意:issuerNameHash和issuerKeyHash的哈希算法必须与hashAlgorithm一致。我排查过一个线上事故,客户端SDK用了SHA-256计算哈希,但hashAlgorithm字段填的是SHA-1 OID,服务器解析出来匹配不到证书,全部返回unknown。
为什么会这样?因为OCSP响应者会拿着你提供的issuerNameHash和issuerKeyHash去自己的缓存里找颁发者信息。哈希不一致,等于给了个错误的身份证。
3.3 扩展字段(Extensions)
OCSP请求的扩展字段藏在requestExtensions里。常用的有这几个——
| 扩展OID | 名称 | 用途 |
|---|---|---|
| 1.3.6.1.5.5.7.48.1.2 | nonce | 防重放攻击,请求带一个随机数,响应必须原样返回 |
| 1.3.6.1.5.5.7.48.1.3 | acceptableResponseTypes | 告诉服务器我期望哪种响应格式 |
| 1.3.6.1.5.5.7.48.1.6 | preferredSignatureAlgorithm | 指定响应签名的首选算法 |
Nonce扩展是最容易出问题的。如果客户端加了nonce,但服务器响应里没有原样返回,或者nonce值不匹配,客户端就会丢弃响应,表现成“连接中断”或“超时”。我记得有次SDK升级后,所有iOS设备都报OCSP失败,最后发现是客户端生成的nonce太长(超过255字节),服务器截断了,两边对不上。
acceptableResponseTypes这个扩展,说白了就是告诉服务器:“我只认basicOCSPResponse,别给我别的格式。”如果你的SDK只支持basic响应,但服务器默认返回了另一种,就会解析失败。我建议在扩展里显式指定,避免歧义。
3.4 实际请求抓包分析
下面是一个简化后的OCSP请求DER数据(十六进制),我标注了关键位置——
30 4B -- OCSPRequest (SEQUENCE, 75 bytes)
30 49 -- tbsRequest (SEQUENCE, 73 bytes)
A0 03 -- [0] (version, 3 bytes)
02 01 00 -- INTEGER 0 (v1)
31 24 -- requestList (SET, 36 bytes)
30 22 -- SingleRequest (SEQUENCE, 34 bytes)
30 20 -- CertID (SEQUENCE, 32 bytes)
30 0B -- hashAlgorithm (SEQUENCE, 11 bytes)
06 09 60 86 48 01 65 03 04 02 01 -- SHA-256 OID
04 14 -- issuerNameHash (OCTET STRING, 20 bytes)
AA BB CC DD ... -- 实际哈希值
04 14 -- issuerKeyHash (OCTET STRING, 20 bytes)
EE FF 00 11 ... -- 实际哈希值
02 08 -- serialNumber (INTEGER, 8 bytes)
12 34 56 78 9A BC DE F0
A2 1C -- [2] (requestExtensions, 28 bytes)
30 1A -- Extensions (SEQUENCE, 26 bytes)
30 18 -- Extension (SEQUENCE, 24 bytes)
06 0A -- extnID (OID, 10 bytes)
2B 06 01 05 05 07 30 01 02 -- nonce OID
04 0A -- extnValue (OCTET STRING, 10 bytes)
01 02 03 04 05 06 07 08 09 0A
你看,从30 4B开始逐层解析。只要有一层长度字段算错,整包就废了。SDK升级后如果发现请求长度超出预期(比如之前是70字节,现在变成了200字节),多半是加上了非必要的扩展,或者哈希长度变了。
openssl ocsp -reqout命令导出一份正常的请求文件,然后与新SDK发出的请求做二进制对比。差异一目了然。另外,在代码里加日志输出DER十六进制,比靠抓包省事得多。
这张图把整个请求结构串起来了。你排查的时候,就从最外层开始逐层验证:版本号对不对?requestList有没有漏掉证书?CertID里的哈希和序列号是否匹配?扩展字段有没有多出奇怪的OID?每一步都做二进制比对,基本就能定位问题了。