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 会话就是:
- 客户端构造 CertID + 随机 Nonce → 封装成请求 DER 字节。
- 通过 HTTP POST 发给 OCSP 响应者(默认路径 /ocsp)。
- 响应者查数据库或 CRL,得到证书状态。
- 响应者用私钥签名 tbsResponseData(内含状态、生产时间、Nonce)。
- 客户端收到响应,验证签名、Nonce、生产时间是否在允许的偏移内。
- 如果一切正常,信任该状态,继续 TLS 握手。
下面这张图把请求/响应流程和关键元素画在一起,方便你对照理解。
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 要强调“自验证”了——下一层自然过渡。实际用的时候多留意版本兼容性和时间同步,基本不会出大问题。