第2章|OCSP请求与响应格式详解

说实话,很多做SSL/TLS的工程师都把OCSP当黑盒用。调个openssl命令,返回“good”就算完事。但我告诉你,一旦线上出现超时,你不懂报文结构,根本无从下手。

我刚接手PKI运维时也吃过这个亏。明明CA没问题,客户端却一直报“OCSP response verification failed”。查了一整天,最后发现是请求里少了关键扩展——Nonce。所以本章,我把OCSP请求与响应的完整结构掰开揉碎了讲给你听。

2.1 请求格式:客户端到底问了什么?

OCSP请求用ASN.1 DER编码。底层走HTTP GET或POST。结构上,核心就是“你帮我查查这张证书还活着没”。

请求由三个部分组成:

  • 版本号:目前只有0(v1);
  • 请求者名称:可选,一般是空的;
  • 请求列表:真正干活的部分,包含待查询的证书标识;
  • 扩展:可选的附加信息,但实战中几乎必带。

每个待查询的证书标识又包含:

  1. 哈希算法:默认SHA-1,但现在推荐用SHA-256;
  2. 颁发者公钥哈希:CA公钥的哈希值;
  3. 序列号:证书本身的序列号。
个人习惯:我总是在开发环境先打印出请求的hexdump。用openssl ocsp -reqout导出,再配合dumpasn1解析。看一眼就知道哈希算法对不对,序列号有没有写错位。

下面是典型的OCSP请求结构,我用ASN.1的伪代码表示:

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
}

CertID ::= SEQUENCE {
    hashAlgorithm       AlgorithmIdentifier,
    issuerNameHash      OCTET STRING,
    issuerKeyHash       OCTET STRING,
    serialNumber        CertificateSerialNumber
}

2.2 响应格式:CA怎么回答你?

响应的结构同样遵循ASN.1。最上层是OCSPResponse,它包含一个ResponseStatus和可选的ResponseBytes。

ResponseStatus有六种取值,我直接放一张对比表,你一看就明白:

状态码 含义 常见原因
0 successful 正常,响应体里有具体状态
1 malformedRequest 客户端请求格式不对,DER解析失败
2 internalError CA那边崩了,比如数据库连不上
3 tryLater CA太忙,让你过会儿再问
5 sigRequired CA要求签名请求,但你没签名
6 unauthorized CA不认你,没权限查询
我曾经踩过坑:线上环境收到tryLater(状态码3),客户端默认会重试三次,每次间隔30秒。但我们的Nginx配置的OCSP超时只有5秒……结果就是每次刷新都报“OCSP auth error”,用户直接看到502。

如果状态码是0,则ResponseBytes里会包含真正有效的BasicOCSPResponse。它由三块组成:

  • tbsResponseData:响应的主体,含版本、时间、证书状态;
  • signatureAlgorithm:签名用的算法;
  • signature:CA的数字签名;
  • certs:帮助验证的中间证书。

tbsResponseData里最核心的是SingleResponse,它告诉你单张证书的生死:

SingleResponse ::= SEQUENCE {
    certID              CertID,
    certStatus          CertStatus,
    thisUpdate          GeneralizedTime,
    nextUpdate          GeneralizedTime OPTIONAL,
    singleExtensions    [0] EXPLICIT Extensions OPTIONAL
}

CertStatus ::= CHOICE {
    good                [0] IMPLICIT NULL,
    revoked             [1] IMPLICIT RevokedInfo,
    unknown             [2] IMPLICIT NULL
}

三种状态:

  • good:证书有效,未被吊销;
  • revoked:证书被吊销,附带吊销时间、原因;
  • unknown:CA不认识这张证书。

这里还有一个关键时间字段——thisUpdate和nextUpdate。它们共同决定了OCSP响应在客户端能缓存多久。如果nextUpdate没填,客户端就默认不缓存,每次都得重新查。你想想看,高并发场景下,这得多消耗CA资源。

2.3 扩展:Nonce和Staple的秘密

扩展字段在请求和响应里都能出现。我最常用的扩展是两个:

  • Nonce:客户端生成一个随机数,CA把它原样签回响应。避免重放攻击。我建议所有生产环境都启用。
  • Staple:严格来说是服务器用OCSP响应来“钉住”证书状态。原理就是把响应提前缓存,客户端握手时服务器直接返回签过名的响应,省去外发请求。
经验之谈:如果你在做OCSP Stapling,记得检查响应里的nextUpdate。我见过有人缓存了过期的响应,结果证书吊销快一天了,浏览器还在显示绿色小锁。

Nonce的OID是1.3.6.1.5.5.7.48.1.2。如果CA支持Nonce,响应里也会原样返回。不符合时客户端应该验证不通过。但现实是,有些CA就是不支持Nonce,服务端得做降级处理。

2.4 核心流程图:请求与响应的握手

为了让你把整个过程串起来,我画了一张图。记住这张图,以后排错就有了路线图。

OCSP请求-响应核心流程 客户端 构建请求 版本 + 序列号 哈希算法 可选 Nonce OCSP响应者(CA) 验证请求 解析DER 查证书状态 生成响应 HTTP POST / GET 返回响应 状态码 + 签名 thisUpdate nextUpdate 接收并验证 验证签名 检查Nonce 缓存/使用 DER编码响应 排错核心:检查Nonce是否匹配、nextUpdate是否过晚、签名算法是否同意

记住了,OCSP所有问题最后都落在这三个维度:

  1. 请求构造:序列号、哈希算法对不对?Nonce带没带?
  2. 网络传输:HTTP超时、GET/POST被代理拦截?
  3. 响应解析:状态码含义、签名验证、缓存策略是否合理?

2.5 实战排错:从报文看问题

我知道了,光讲理论你肯定不过瘾。讲一个真实案例。有一次客户反馈说Nginx启了OCSP Stapling,但浏览器偶尔还会弹出“证书已吊销”的告警。

我抓包一看,响应里的nextUpdate是当前时间之前的一个小时。也就是说,CA返回的响应本身就过期了。客户端拿个过期响应去钉,浏览器当然不认账。

解决方案也简单:返回报文里,CA把nextUpdate时间戳写错了。找CA供应商修了后,一切正常。

所以,读懂OCSP请求与响应的每一比特,是你排查超时问题的基本功。不做黑盒工程师,从拆开ASN.1报文开始。

最后一条避坑指南:千万不要在生产环境直接用openssl ocsp -no_nonce做测试。我见过有人用这个命令测了一周,没发现问题。部署上线后,客户端强制校验Nonce,结果全线超时。测试环境一定要模拟客户端的真实行为。