2、OCSP请求与响应格式详解

说真的,当年我第一次用Wireshark抓OCSP包时,看到那一堆十六进制数据,脑袋都是懵的。后来自己动手写了一套解析器,才把这格式彻底吃透。今天我把这些经验倒出来,省得你踩我踩过的坑。

2.1 整体结构——ASN.1与DER编码

OCSP的请求和响应都基于ASN.1定义,用DER编码传输。你想想看,SSL/TLS握手里那么多协议,为什么OCSP非要搞这么一套?其实是因为OCSP需要携带证书指纹、签名、时间戳等结构化信息,ASN.1正好干这个的。

我喜欢把整个结构想象成一个树形JSON,只是节点是二进制的。每个字段的类型、长度、值都紧挨着。解析的时候,任何一个字节错位,后面全废。

⚠ 踩坑提醒:我曾经遇到过一台老设备返回的OCSP响应居然用BER编码(允许不定长形式),标准DER是不允许的。解析时如果不做容错,直接抛异常——那台设备就变成了“不可用”状态,排查了整整两天。

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前缀,请求里也得一样——很多实现忽略这点,导致匹配失败。

💡 我的小技巧:写请求解析器时,先读requestList的SEQUENCE长度,再逐个解析里面的CertID。别直接跳长度,宁可多读几个字节也不漏。

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 请求与响应格式对比

对比项请求响应
必须字段CertIDresponseStatus + certStatus
包含签名否是(BasicOCSPResponse内)
时间戳无thisUpdate / nextUpdate
扩展Nonce(常用)、其他Nonce回声、服务端扩展
编码特点结构简单,约100~300字节可达数千字节(含证书链)
OCSP请求与响应ASN.1结构对比 OCSPRequest version (INTEGER, 固定0) requestorName [optional] requestList: SEQUENCE of Request requestExtensions [optional] CertID 内部 hashAlgorithm OID issuerNameHash issuerKeyHash serialNumber OCSPResponse responseStatus (ENUMERATED) responseBytes (若successful) BasicOCSPResponse - tbsResponseData - signatureAlgorithm + signature 💡 tbsResponseData 内含 producedAt、responses、extensions SingleResponse 含 certStatus、thisUpdate、nextUpdate 请求与响应通过CertID关联

2.6 实际项目中的避坑指南

除了前面提到的Nonce长度和serialNumber符号问题,还有几个坑值得记下来:

  • 时间格式:thisUpdate用GeneralizedTime的时候,有的CA会用UTC时间(YYMMDDHHMMSSZ),有的用带毫秒的格式。最好统一转成UTC再比较。
  • 签名验证:BasicOCSPResponse的签名是对tbsResponseData整体DER做的。我见过一个实现不小心对tbsResponseData重新编码(改变了排序或者去掉可选字段)导致验签失败。
  • 扩展顺序:响应里的扩展顺序必须原样返回,但有些CA会调整顺序——如果客户端用下标索引而不是OID查找,就尴尬了。
🔑 核心原则:写OCSP解析器时,不要对DER做任何“规范化”处理。原字节进来,原字节验签。任何自作聪明的“修复”都可能引入安全漏洞。

嗯,OCSP请求和响应的格式就这些了。接下来你可能会问:这些格式如何实际应用在绕过攻击场景中?别急,后面我们会专门讲攻击手法,到时候格式知识就是你的地图。