2、OCSP请求与响应格式详解
说真的,当年我第一次用Wireshark抓OCSP包时,看到那一堆十六进制数据,脑袋都是懵的。后来自己动手写了一套解析器,才把这格式彻底吃透。今天我把这些经验倒出来,省得你踩我踩过的坑。
2.1 整体结构——ASN.1与DER编码
OCSP的请求和响应都基于ASN.1定义,用DER编码传输。你想想看,SSL/TLS握手里那么多协议,为什么OCSP非要搞这么一套?其实是因为OCSP需要携带证书指纹、签名、时间戳等结构化信息,ASN.1正好干这个的。
我喜欢把整个结构想象成一个树形JSON,只是节点是二进制的。每个字段的类型、长度、值都紧挨着。解析的时候,任何一个字节错位,后面全废。
2.2 OCSP请求格式逐层拆解
一份OCSP请求的核心是OCSPRequest,它包含:
- version:目前固定为0(对应v1),我个人习惯直接跳过这个字段。
- requestorName:可选,一般是空。我曾见过有些实现会填客户端证书的DN,但这不是必须的。
- requestList:请求的核心列表,每个元素是Request。
- requestExtensions:可选扩展,最常用的是Nonce(随机数防重放)。
每个Request里面包含一个CertID,它的字段是这样的:
| 字段 | 描述 | 示例/注意 |
|---|---|---|
| hashAlgorithm | 哈希算法OID | 比如 1.3.14.3.2.26 (SHA-1) |
| issuerNameHash | 颁发者DN的哈希 | 20字节(SHA-1)或32字节(SHA-256) |
| issuerKeyHash | 颁发者公钥的哈希 | 同样长度 |
| serialNumber | 目标证书的序列号 | 必须去掉前导0?别信,保持原样 |
嗯,这里要注意serialNumber的编码:ASN.1里的整数是大端字节序,而且是有符号的。如果证书序列号最高位为1,DER会补一个0x00前缀,请求里也得一样——很多实现忽略这点,导致匹配失败。
2.3 OCSP响应格式逐层拆解
响应比请求丰富得多。它分为两层:外层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)
}
如果responseStatus不是successful(0),后面就没有responseBytes。我曾经遇到一个CA返回tryLater(3),原因是他们后端数据库连接超时——直接重试几次就过了。
当状态是successful时,responseBytes结构如下:
- responseType:OID,基本类型是 1.3.6.1.5.5.7.48.1.1
- response:DER编码的BasicOCSPResponse
BasicOCSPResponse是核心,包含:
| 组成部分 | 内容 | 注意 |
|---|---|---|
| tbsResponseData | 签名前的原始数据 | 版本、responderID、producedAt、responses、extensions |
| signatureAlgorithm | 签名算法OID | 常为sha256WithRSAEncryption |
| signature | 签名值 | BIT STRING,注意高位补0的问题 |
| certs | 可选证书链 | 一般不填,除非需要额外链 |
tbsResponseData里的responses字段是一个或多个SingleResponse,每个包含:
- certID:和请求里的CertID一模一样,用来关联
- certStatus:枚举(good、revoked、unknown),revoked时还会带上原因和时间
- thisUpdate / nextUpdate:时间戳(GeneralizedTime或UTCTime)
- singleExtensions:可选的扩展,比如nonce回声
2.4 核心扩展:Nonce(防重放)
Nonce是一个随机数,客户端发请求时带上,服务器响应时原样返回。攻击者就算截获了响应,也无法在其他请求中重放——因为随机数对不上。这是我很看重的安全机制。
Nonce的OID是 1.3.6.1.5.5.7.48.1.2,值是一个OCTET STRING。我曾经把长度写死了16字节,结果某CA服务器返回的Nonce只有8字节——我的客户端直接丢弃了响应。后来改成根据实际长度解析,这才稳定。
2.5 请求与响应格式对比
| 对比项 | 请求 | 响应 |
|---|---|---|
| 必须字段 | CertID | responseStatus + certStatus |
| 包含签名 | 否 | 是(BasicOCSPResponse内) |
| 时间戳 | 无 | thisUpdate / nextUpdate |
| 扩展 | Nonce(常用)、其他 | Nonce回声、服务端扩展 |
| 编码特点 | 结构简单,约100~300字节 | 可达数千字节(含证书链) |
2.6 实际项目中的避坑指南
除了前面提到的Nonce长度和serialNumber符号问题,还有几个坑值得记下来:
- 时间格式:thisUpdate用GeneralizedTime的时候,有的CA会用UTC时间(YYMMDDHHMMSSZ),有的用带毫秒的格式。最好统一转成UTC再比较。
- 签名验证:BasicOCSPResponse的签名是对tbsResponseData整体DER做的。我见过一个实现不小心对tbsResponseData重新编码(改变了排序或者去掉可选字段)导致验签失败。
- 扩展顺序:响应里的扩展顺序必须原样返回,但有些CA会调整顺序——如果客户端用下标索引而不是OID查找,就尴尬了。
嗯,OCSP请求和响应的格式就这些了。接下来你可能会问:这些格式如何实际应用在绕过攻击场景中?别急,后面我们会专门讲攻击手法,到时候格式知识就是你的地图。