4、OCSP详解:请求/响应格式、Stapling技术与签名验证

OCSP,全称是在线证书状态协议。说白了,它就是用来替代CRL那个大文件的。你想想看,每次验证证书都要下载整个CRL列表,几百KB甚至几MB,这在移动端或者高并发场景下简直是灾难。OCSP的思路很简单——只查一张证书,不问别的。

我个人习惯把OCSP叫做“证书状态的实时查询接口”。客户端拿着证书序列号,去问CA的OCSP响应器:“这张证还活着吗?”响应器回答:“活着”或者“死了”。就这么简单。但简单背后,坑不少。今天咱们把OCSP的请求/响应格式、Stapling技术,还有签名验证,一个一个拆开讲。

4.1 OCSP请求格式

OCSP请求本身是用ASN.1编码的,DER格式。不过你不用手写ASN.1,OpenSSL和各大语言库都封装好了。我们看一个典型的请求结构:

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
}

这里最核心的是 CertID。它包含四个字段:

  • hashAlgorithm:哈希算法,通常是SHA-1或SHA-256
  • issuerNameHash:颁发者DN的哈希值
  • issuerKeyHash:颁发者公钥的哈希值
  • serialNumber:要查询的证书序列号

为什么要同时传颁发者名称哈希和公钥哈希?因为不同CA可能颁发相同序列号的证书(虽然概率极低),用两个哈希值做联合主键,确保唯一性。我在项目中遇到过一个问题:某个CA的OCSP响应器只认SHA-1哈希,客户端默认用SHA-256,结果一直返回“查询失败”。嗯,这就是兼容性坑。

小技巧: 构造OCSP请求时,建议先查一下目标CA的OCSP响应器支持哪些哈希算法。大部分现代CA同时支持SHA-1和SHA-256,但老系统可能只认SHA-1。

4.2 OCSP响应格式

OCSP响应比请求复杂一些。它包含响应状态、证书状态、以及可选的扩展信息。看结构:

OCSPResponse ::= SEQUENCE {
    responseStatus      OCSPResponseStatus,
    responseBytes       [0] EXPLICIT ResponseBytes OPTIONAL
}

OCSPResponseStatus ::= ENUMERATED {
    successful            (0),
    malformedRequest      (1),
    internalError         (2),
    tryLater              (3),
    sigRequired           (4),
    unauthorized          (5)
}

ResponseBytes ::= SEQUENCE {
    responseType   OBJECT IDENTIFIER,
    response       OCTET STRING
}

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   [1] EXPLICIT Extensions OPTIONAL
}

SingleResponse ::= SEQUENCE {
    certID               CertID,
    certStatus           CertStatus,
    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
}

响应状态有6种,但实际中你只会遇到两种:successful(0) 和 tryLater(3)。其他状态要么是请求格式错误,要么是权限不足。我曾经在生产环境遇到过 unauthorized(5),排查了半天才发现是OCSP请求里没有带必要的扩展字段——某些CA要求请求必须包含nonce扩展防重放。

证书状态有三种:

状态 含义 处理方式
good 证书有效,未被吊销 继续使用,信任该证书
revoked 证书已被吊销 拒绝连接,记录吊销信息
unknown CA不知道这张证书 建议视为不安全,走CRL兜底
注意: unknown 状态不等于 good。有些客户端把unknown当成good处理,这是严重的安全漏洞。正确的做法是:unknown状态下,要么走CRL二次确认,要么直接拒绝。

4.3 OCSP Stapling技术

OCSP有一个致命问题——隐私泄露。客户端每次查询OCSP,CA就知道你访问了哪个网站。而且客户端要额外发起一次HTTP请求,增加了延迟。OCSP Stapling就是来解决这两个问题的。

Stapling的原理很简单:服务器自己去查OCSP,把响应结果“钉”在TLS握手过程中发给客户端。客户端不再需要主动查询,直接验证服务器提供的OCSP响应即可。

我画了一张流程图,帮你理解这个过程:

OCSP Stapling 工作流程 客户端 Web服务器 CA/OCSP响应器 ① 定时查询OCSP ② 返回签名响应 ③ TLS握手 携带OCSP响应 ④ 验证OCSP签名 关键点: • 服务器定期(通常每小时)向CA查询自己证书的状态 • 服务器缓存OCSP响应,在TLS握手时直接发给客户端 • 客户端不需要再主动查询OCSP,隐私和性能都得到提升 • 响应有过期时间(nextUpdate),过期后服务器必须重新查询

Stapling的好处很明显:

  • 隐私保护:CA不知道客户端访问了哪些网站,因为查询是服务器做的
  • 性能提升:客户端省去了一次HTTP请求,TLS握手更快
  • 可靠性:即使CA的OCSP响应器暂时挂了,只要服务器缓存未过期,客户端仍能验证

但Stapling也有坑。我记得有一次排查线上问题,发现Nginx配置了Stapling,但客户端始终收不到stapled响应。查了半天,原来是OCSP响应器的地址配错了——证书里的Authority Information Access字段指向的是旧CA的OCSP地址。嗯,这种配置错误很隐蔽,建议你部署后一定要用 openssl s_client -status 验证一下。

验证Stapling是否生效的命令:
openssl s_client -connect example.com:443 -status -tlsextdebug 2>&1 | grep -A 20 "OCSP response"

如果看到 OCSP Response Status: successful (0x0),说明Stapling正常工作。

4.4 OCSP响应签名验证

OCSP响应是签过名的。客户端收到响应后,必须验证签名,否则攻击者可以伪造一个“good”状态欺骗你。签名验证分三步:

  1. 获取签名证书:OCSP响应里可以内嵌签名证书(certs字段),也可以从外部获取
  2. 验证签名证书的合法性:签名证书必须由受信任的CA签发,且未被吊销
  3. 验证响应签名:用签名证书的公钥验证OCSP响应体的签名

这里有一个容易忽略的点:签名证书不一定是CA自己的证书。CA可以委托一个专门的OCSP响应器,用独立的证书签名。这种情况下,签名证书的Extended Key Usage必须包含 OCSP Signing 扩展。我见过一个案例:某CA的OCSP响应器证书没有这个扩展,导致所有客户端验证失败——嗯,这是CA自己的配置失误。

验证签名的代码示例(用OpenSSL命令行模拟):

# 1. 获取OCSP响应并保存到文件
openssl ocsp -issuer ca.pem -cert server.pem \
    -url http://ocsp.example.com -respout ocsp.der

# 2. 验证OCSP响应签名
openssl ocsp -respin ocsp.der -issuer ca.pem \
    -verify_other ocsp_signer.pem -CAfile ca.pem

在代码层面,以Python为例:

from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import requests

# 构造OCSP请求
builder = x509.ocsp.OCSPRequestBuilder()
builder = builder.add_certificate(server_cert, issuer_cert, hashes.SHA256())
req = builder.build()

# 发送请求
url = server_cert.ocsp_url
resp = requests.post(url, data=req.public_bytes(serialization.Encoding.DER),
                     headers={'Content-Type': 'application/ocsp-request'})

# 解析响应
ocsp_resp = x509.ocsp.load_der_ocsp_response(resp.content)

if ocsp_resp.response_status == x509.ocsp.OCSPResponseStatus.SUCCESSFUL:
    # 验证签名
    signer_cert = ocsp_resp.certs[0]  # 获取签名证书
    public_key = signer_cert.public_key()
    
    # 验证签名证书链
    # ... (省略证书链验证代码)
    
    # 验证OCSP响应签名
    public_key.verify(
        ocsp_resp.signature,
        ocsp_resp.tbs_response_bytes,
        padding.PKCS1v15(),
        ocsp_resp.signature_hash_algorithm
    )
    
    # 检查证书状态
    if ocsp_resp.certificate_status == x509.ocsp.OCSPCertStatus.GOOD:
        print("证书状态:有效")
    elif ocsp_resp.certificate_status == x509.ocsp.OCSPCertStatus.REVOKED:
        print(f"证书已吊销,时间:{ocsp_resp.revocation_time}")
    else:
        print("证书状态未知")
else:
    print(f"OCSP请求失败,状态码:{ocsp_resp.response_status}")
安全警告: 验证OCSP响应签名时,一定要检查签名证书的合法性。如果签名证书本身是伪造的,那签名验证就失去了意义。建议使用系统信任存储或CA证书链进行验证。

4.5 避坑指南

最后,分享几个我在项目中踩过的坑:

  • OCSP响应缓存时间:nextUpdate字段告诉客户端这个响应什么时候过期。有些CA把缓存时间设得很长(比如7天),这会导致证书吊销后客户端迟迟收不到更新。我建议客户端强制设置最大缓存时间,比如24小时。
  • Nonce扩展:某些OCSP响应器要求请求包含nonce防重放。但有些响应器不支持nonce。这就尴尬了——你加了nonce,它报错;你不加,它又要求。解决办法是:先不加nonce试一次,如果返回malformedRequest,再加nonce重试。
  • Stapling的过期处理:服务器缓存的OCSP响应过期后,如果CA暂时不可用,服务器应该怎么办?我建议的做法是:继续使用过期响应(但记录告警),而不是直接中断TLS握手。毕竟,过期的good状态比没有状态要安全一些。
  • OCSP Must-Staple扩展:这是证书里的一个扩展,告诉客户端“这张证书必须使用Stapling”。如果服务器没有提供Stapling,客户端应该拒绝连接。这个扩展在Let's Encrypt的证书里已经普及了,建议你签发证书时加上。

OCSP这块内容不少,但核心就三件事:请求什么格式、响应怎么解析、签名怎么验。Stapling是锦上添花,但现在已经成了标配。你想想看,没有Stapling的TLS握手,就像出门不锁门——能用,但总感觉不踏实。