OCSP 协议原理

讲 OCSP Stapling 之前,得先把 OCSP 协议本身摸透。我早年刚接触 PKI 时,觉得 OCSP 就是个在线查证书状态的“黑盒”,直到被线上故障折腾过几次才老老实实啃了一遍 RFC 6960。说白了,OCSP 的核心就是两样东西:请求和响应,再加上签名和防重放机制。咱们一个一个拆开看。

1. 为什么需要 OCSP?

传统方案是用 CRL(证书吊销列表),客户端自己定期去拉一个巨大的列表。我在一个金融项目里见过 CRL 文件超过 50 MB,每次下载慢得要命,还经常过期。OCSP 的出现就是为了解决这个痛点——按需查询,实时响应。你问一张证书,它只回那张证书的状态,量小速度快。

2. OCSP 请求长什么样?

OCSP 请求是 ASN.1 结构,经过 DER 编码后通过 HTTP POST 发送。核心内容就是告诉响应者:“帮我查一下这张证书现在到底有没有被吊销?”

字段 必选/可选 说明
version 可选(默认 v1) 协议版本,目前只有 v1
requestList 必选 包含一个或多个 CertID,每个 CertID 由哈希算法、发行者信息、序列号组成
requestExtensions 可选 最常用的是 nonce 扩展

我刚入行时犯过一个低级错误——忘记在请求里带上 Nonce,导致生产环境被重放攻击趁虚而入。嗯,这个坑后面会细说。

注意:CertID 中的哈希不匹配会导致响应者找不到对应证书。我在对接某家 CA 时就碰到过他们用 SHA-256,而客户端发的是 SHA-1,结果一直返回 unauthorized。

3. OCSP 响应格式

响应比请求复杂一点,因为它要包含签名。整体结构分两部分:响应状态(ResponseStatus)和 基本响应(BasicOCSPResponse)。

ResponseStatus 含义
successful (0) 正常,后面跟着具体状态
malformedRequest (1) 请求格式错误
internalError (2) 响应者内部出问题
tryLater (3) 暂时查不了,让客户端过会儿再试
sigRequired (5) 要求请求也必须签名(很少遇到)
unauthorized (6) 客户端不被授权查询

当 status 为 0(successful)时,响应体才包含基本响应。基本响应内部有:

  • tbsResponseData:包含版本、响应者、生产时间、证书状态列表(good / revoked / unknown)和扩展(如 Nonce)。
  • signatureAlgorithm:签名算法,比如 sha256WithRSAEncryption。
  • signature:签名值,用响应者的私钥对 tbsResponseData 签名。
  • certs[]:可选,把签名证书也带在响应里,方便客户端验证。

我的建议:生产环境一定要让响应者把签名证书嵌入到 certs[] 中。否则客户端还得额外去拉 CA 证书链,多一次 HTTP 查询,延迟就上去了。

4. 签名机制——为什么非签不可?

你想想看,如果响应不签名,中间人完全可以篡改“good”为“revoked”,直接让你的服务不可用。OCSP 响应必须由受信任的实体签名:证书的签发 CA,或者 CA 授权的专用响应者(Authorized Responder)。

我记得有一次排查一个诡异的证书验证失败案例:客户端总是报“revoked”,但手动查 CA 的 OCSP 却是 good。最后发现是响应者的签名证书过期了,客户端验证签名失败,降级处理成了 revoked。所以大家在部署时一定要给响应者的签名证书加上监控,别让它静悄悄过期。

5. Nonce——防重放的小把戏

Nonce 是一个随机数,由客户端生成放到请求扩展里,响应者原封不动地把它拷贝回响应。这样客户端就能确认收到的响应是针对自己刚才那个请求的,而不是攻击者事先录好的旧响应。

这里有一个容易踩的坑——Nonce 是可选扩展。有些 OCSP 响应者不支持 Nonce,直接忽略,返回的响应里没有 Nonce。客户端如果强制校验 Nonce 存在且匹配,就会失败。我在一个跨国项目中碰到过:客户端是刚升级的 Nginx,默认开启 Nonce 验证,而对方的 OCSP 响应者版本老旧,不支持 Nonce,结果 TLS 握手一直失败。解决方法是在 Nginx 上配 ssl_stapling_verify on 时加一条 ssl_trusted_certificate 链,同时调整 Nonce 超时策略。

避坑指南:我曾经遇到过第三方 OCSP 响应者返回的 Nonce 长度跟请求不一致(少了一个字节),导致底层 ASN.1 解析报错。解决方案:如果响应者和客户端不在同一团队控制,建议在客户端做“Nonce 存在则校验,不存在则放过”的宽松策略,而不是一刀切拒绝。

6. 实战中的交互全貌

把上面这些串起来,一次完整的 OCSP 会话就是:

  1. 客户端构造 CertID + 随机 Nonce → 封装成请求 DER 字节。
  2. 通过 HTTP POST 发给 OCSP 响应者(默认路径 /ocsp)。
  3. 响应者查数据库或 CRL,得到证书状态。
  4. 响应者用私钥签名 tbsResponseData(内含状态、生产时间、Nonce)。
  5. 客户端收到响应,验证签名、Nonce、生产时间是否在允许的偏移内。
  6. 如果一切正常,信任该状态,继续 TLS 握手。

下面这张图把请求/响应流程和关键元素画在一起,方便你对照理解。

OCSP 请求/响应交互流程 客户端(验证者) 构造 CertID + Nonce OCSP 响应者 查状态 + 签名 HTTP POST (DER 请求) 包含 Nonce HTTP 200 (DER 响应) 包含签名 + Nonce 请求 DER 关键字段 • version (默认0) • requestList[0].CertID - hashAlgorithm: sha256 - issuerNameHash, serialNumber 响应 DER 关键字段 • responseStatus: successful (0) • tbsResponseData - producedAt, thisUpdate, nextUpdate - certStatus: good / revoked / unknown • signature (256字节 RSA) 客户端验证:签名 → Nonce 匹配 → 时间窗口 → 证书状态

7. 几个容易被忽略的细节

时间窗口:响应的 producedAt、thisUpdate、nextUpdate 三个时间戳决定了响应是否新鲜。如果客户端时钟和服务器差太多,即使签名无误也会被判为无效。我在跨国部署时吃过这个亏——客户端时区没同步,差了 5 分钟,结果一直报“stale response”。后来统一用 NTP 解决。

扩展字段:除了 Nonce,还有 AcceptableResponses、ServiceLocator 等扩展。不过日常用到的就只有 Nonce,其他的可以忽略。

响应大小:一个带签名的 OCSP 响应通常在 500~2000 字节之间,比起 CRL 动辄几十 MB 小太多了。但注意如果每次查询都加签名证书链,响应可能超过 4 KB,对 HTTP 头长度有影响。我倾向于在响应里只加签名证书本身,父证书让客户端通过信任锚自行补齐。

个人经验:我习惯在测试环境用 OpenSSL 手动模拟一次 OCSP 请求,确认格式无误后再集成到 Nginx 或 Apache。命令很简单:
openssl ocsp -issuer ca.pem -cert server.pem -url http://ocsp.example.com -text
加 -respout 可以把响应 DER 存下来慢慢查。

再次提醒:生产环境务必开启 OCSP 响应的签名验证,并且定期更新签名证书。不要像我一个同事那样,把响应者的私钥存到默认路径还不设权限,被运维扫到直接通报漏洞。

好了,OCSP 请求/响应的原理就这些。搞懂了 Nonce 和签名,你就能理解为什么 Stapling 要强调“自验证”了——下一层自然过渡。实际用的时候多留意版本兼容性和时间同步,基本不会出大问题。