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)——一种确定性编码。

关键点: OCSP 报文使用 DER 编码的 ASN.1 结构。DER 保证同一数据只有唯一编码结果,这对签名验证至关重要。

我有个习惯:遇到不确定的 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)
避坑提示: 我曾经遇到过客户端把 requestList 传空的情况。虽然 RFC 没有明确禁止,但大多数 OCSP 响应器收到空列表会直接返回 malformedRequest。嗯,这条得记住。

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 核心逻辑:一张图看懂

下面这张图概括了请求到响应的核心流转,以及重放检测点都藏在哪。

OCSP 请求与响应核心结构及重放检测点 客户端 发起请求 OCSP 响应器 处理并签名响应 OCSPRequest ├─ tbsRequest (必填) ├─ version ├─ requestorName (可选) └─ requestList → CertID ├─ issuerNameHash ├─ issuerKeyHash └─ serialNumber OCSPResponse ├─ responseStatus (枚举) └─ responseBytes └─ BasicOCSPResponse ├─ tbsResponseData │ ├─ producedAt │ ├─ responses → SingleResponse │ │ ├─ thisUpdate │ │ └─ nextUpdate │ └─ nonce (扩展) ├─ signatureAlgorithm └─ signature 🔍 重放检测点 nonce / producedAt / thisUpdate

从图上你能看到:请求端(客户端)打包 CertID,发送给响应器。响应器生成响应,填入 producedAt(生产时间)、thisUpdate(当前更新)、nextUpdate(下次更新),以及可选的 nonce(随机数)。这些字段,正是我们检测重放攻击的武器库。

2.6 重放攻击为什么能成功?

说白了,OCSP 响应是一段被签名的数据。签名本身防篡改,但不防重放。攻击者只要抓到一个合法的“证书状态良好”响应,稍后找个时机重新发给客户端,客户端如果不检查时间窗口或 nonce,就会相信这个过期的结果。

我曾经在一个实际项目里测试过:用一个 3 天前的 OCSP 响应发给一个配置不当的客户端,它竟然接受了。嗯,这就是忽略了响应结构里时间字段的后果。

2.7 小结一下

  • 请求很简单:核心是 CertID,三要素确保唯一标识证书
  • 响应有层次:外层看状态,内层看数据、签名、时间
  • 时间字段都是重点:producedAt、thisUpdate、nextUpdate 一个都不能漏检
  • nonce 是可选但强大的武器:请求端植入,响应端原样返回,一锤子买卖
注意: nonce 虽然好,但不是所有 OCSP 响应器都支持。RFC 6960 说它是“应该”实现的,不是“必须”。碰到不支持 nonce 的响应器,时间和 freshness 扩展就是底线了。