2、OCSP响应流程:详解客户端发起请求、服务端验证、响应生成的全过程
OCSP的全称是 Online Certificate Status Protocol,说白了就是在线查证书状态。以前用CRL(证书吊销列表)的时候,你得下载一个大列表,然后自己比对——又慢又容易过期。OCSP就好比打一个电话问CA:「这个证书现在还好使吗?」CA在电话那头实时回答你。
但问题来了:这个“打电话”的过程,到底包含了多少步骤?哪个环节可能卡住?我当年接手一个SSL握手频繁超时的项目,日志里全是「OCSP response not received」,查了好几天才发现是CA端的一个缓存配置搞的鬼。嗯,我们一起把OCSP的完整响应流程拆一遍,你就知道该盯哪些点了。
2.1 从客户端发起一次OCSP请求说起
客户端到底是怎么构造OCSP请求的?不是随便发个「ping」就完事。我给你看一个典型的请求结构:
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
}
Request ::= SEQUENCE {
reqCert CertID,
singleRequestExtensions [0] EXPLICIT Extensions OPTIONAL
}
CertID ::= SEQUENCE {
hashAlgorithm AlgorithmIdentifier,
issuerNameHash OCTET STRING, -- Hash of issuer's DN
issuerKeyHash OCTET STRING, -- Hash of issuer's public key
serialNumber CertificateSerialNumber
}
你想想看,这里面最关键的就是 CertID。它不像我们平时发请求那样传一个「证书文件本身」,而是用哈希来标识:签发者名称的哈希、签发者公钥的哈希、证书序列号。三个字段唯一确定一张证书。我一开始也犯过迷糊,以为请求里直接带证书,结果发现请求包其实很小,只有几百字节。
请求生成的步骤:
- 解析目标证书,提取 Issuer DN 和 Issuer Key
- 分别计算 SHA-1(或更安全的算法)哈希
- 组合 SerialNumber 形成 CertID
- 放入 RequestList(可以同时查多张证书)
- 封装成 DER 编码,再套一层 HTTP(或直接 ASN.1 over TCP)
至于发送方式,大多数场景走 HTTP POST,Content-Type 为 application/ocsp-request。也有 GET 方式,把请求放到 URL 后面,但我不推荐——长度有限制,而且日志里容易截断,查起来费劲。
| 发送方式 | URI 示例 | 适用场景 |
|---|---|---|
| POST | http://ocsp.example.com/ | 标准方式,无长度限制 |
| GET | http://ocsp.example.com/MEMwQTA/... | CDN 友好,但日志难跟踪 |
openssl ocsp -reqin 可以解析出来,方便验证客户端组装的 CertID 是否正确。
2.2 服务端:接收、解析、验证
请求到了 CA 或 OCSP 响应器,它会做什么?不是直接查数据库就完事了,这里有个容易被忽略的步骤:
- 解析请求:解码 ASN.1,提取 CertID 列表
- 查找签发者:用 IssuerNameHash + IssuerKeyHash 匹配内部的签发者记录
- 状态查询:根据 SerialNumber 查当前证书状态(good/revoked/unknown)
- 签名准备:收集必要字段,准备构建响应
我印象最深的一次性能问题出在第二步——查找签发者。有些老系统把签发者信息放在远程数据库里,每次请求都要跨机房查,延迟直接飙到 200ms。你想想看,TLS 握手等一个 OCSP 来回就多了半秒,用户不投诉才怪。
服务端常见的瓶颈点:
- 哈希碰撞查找:IssuerKeyHash 如果分布不均匀,索引失效
- 状态库并发:大规模 CA 每秒可能要处理上万条查询,数据库连接池扛不住
- 签名计算:每个响应都要重新签名,RSA 2048 位签名一次大概 1-3ms,量一上来 CPU 就吃满了
2.3 响应生成与返回
服务端验证完状态之后,要构造一个 OCSPResponse 返回给客户端。响应的灵魂在 BasicOCSPResponse 里:
OCSPResponse ::= SEQUENCE {
responseStatus OCSPResponseStatus,
responseBytes [0] EXPLICIT ResponseBytes OPTIONAL
}
ResponseBytes ::= SEQUENCE {
responseType OBJECT IDENTIFIER,
response OCTET STRING -- DER-encoded BasicOCSPResponse
}
BasicOCSPResponse ::= SEQUENCE {
tbsResponseData ResponseData,
signatureAlgorithm AlgorithmIdentifier,
signature BIT STRING,
certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL
}
ResponseData ::= SEQUENCE {
version [0] EXPLICIT Version DEFAULT v1,
responderID ResponderID,
producedAt GeneralizedTime,
responses SEQUENCE OF SingleResponse,
responseExtensions [0] EXPLICIT Extensions OPTIONAL
}
SingleResponse ::= SEQUENCE {
certID CertID,
certStatus CertStatus,
thisUpdate GeneralizedTime,
nextUpdate GeneralizedTime OPTIONAL,
singleExtensions [0] EXPLICIT Extensions OPTIONAL
}
注意到 certStatus 有且只有三种取值:
| 状态 | 含义 | 响应体行为 |
|---|---|---|
| good | 证书目前有效且未被吊销 | 仅标识 good,无额外信息 |
| revoked | 证书已被吊销 | 包含吊销时间、原因代码 |
| unknown | 响应器无法确定证书状态 | 客户端应视为证书不可信 |
为什么会有 unknown?我见过最离谱的,是 CA 把证书信息迁移到新数据库,旧数据库还在但只读,OCSP 服务查不到新发证书,全部返回 unknown——客户端全部拒绝连接,事故持续了 40 分钟。所以你得记得,unknown 不等于「安全」,它意味着查询链路中断了。
响应返回后,客户端要验证签名、检查 producedAt 和 thisUpdate 的时间窗口是否在可接受范围内(一般往前 10 分钟叫 tolerated skew)。如果签名验证失败或响应过期,客户端必须当作证书无效处理。
好,整个流程走下来就是这样。我们画一张图把它串起来,以后你看日志时就知道:这一步慢了,大概卡在哪个环节。
这张图上,请求和响应都是双边走完的。看到哪个框慢了,就去看日志里对应的阶段耗时。我一般在调优时最关注「状态查询」和「签名」这两个格子——机器能扛多少并发,基本由它们决定。