跳到主要内容
ecdsa.com
← 全部文章

2026年的 OCSP 与 CRL:吊销检查如今如何工作

证书吊销已经悄然变了样。OCSP 成了可选项并逐渐淡出,CRL 重新回归,浏览器检查推送到本地的列表而不再询问 CA,而大多数 HTTP 客户端仍然什么都不检查,除非你明确要求。下文的每个客户端都用一张已吊销的证书测试过。

简短回答:自2024年3月起,CA/Browser Forum 要求 CA 发布 CRL,并把 OCSP 改为可选。Let's Encrypt 于2025年5月从其证书中移除了 OCSP URL,并于2025年8月关闭了 OCSP 响应器。浏览器不再就每张证书向 CA 查询。Firefox 检查本地的 CRLite 过滤器,Chrome 检查自己推送的 CRLSet,Apple 则使用自己汇总的列表。大多数库和命令行工具(Java、.NET、Go、Python、Node.js、curl)默认根本不检查吊销状态。在我们的测试中,它们全都接受了一张已吊销的 Let's Encrypt 证书。如果你的客户端必须拒绝已吊销的证书,就要显式启用 CRL 检查,并为较短的证书有效期做好规划。后者本来就是整个行业的发展方向。

为什么吊销机制需要重新设计

证书可以在过期之前被吊销,通常是因为密钥泄露或证书被误签发。通知客户端的经典方式有两种:

  • CRL(证书吊销列表,RFC 5280):CA 发布一份经过签名的列表,列出已吊销证书的序列号。证书在其 CRL 分发点(CRL Distribution Points)扩展中指向这份列表。
  • OCSP(RFC 6960):客户端就某一张证书向 CA 的响应器查询,得到经过签名的状态:“good”(正常)、“revoked”(已吊销)或“unknown”(未知)。响应器的 URL 位于颁发机构信息访问(Authority Information Access)扩展中。

这两种方式在 Web 上都有问题。CRL 可能很大。OCSP 会把用户访问的每个网站都泄露给 CA,还会增加延迟;而且响应器无法访问时,浏览器只能“软失败”(soft-fail)。能够拦截 OCSP 请求的攻击者可以让这项检查形同虚设。OCSP Stapling 由服务器获取响应并附在握手中发送,解决了隐私问题,但始终没有普及。Must-Staple 是要求必须提供 OCSP Stapling 响应的证书标志,浏览器对它的支持很少。

现行规则的规定

SC-063 提案(“OCSP 改为可选、强制要求 CRL、鼓励自动化”,2024年3月15日生效)修改了《基线要求》:

  • CRL 成为强制要求。CA 必须在签发第一张证书后的 24 小时内发布完整的或分片的 CRL。如果其所有证书都带有 OCSP URL,CA 必须至少每 7 天更新一次 CRL,否则至少每 4 天更新一次;吊销证书后,还必须在 24 小时内更新(§4.9.7)。对于订户证书,nextUpdate 最多只能比 thisUpdate 晚 10 天(§7.2)。
  • OCSP 改为可选。如果证书带有 OCSP URL,响应器就必须作答,且响应的有效期必须在 8 小时到 10 天之间(§4.9.9)。没有 OCSP URL 的证书必须包含 CRL 分发点(§7.1.2.11.2)。
  • 短期证书不需要吊销信息。2026年3月15日起签发的“短期订户证书”(Short-lived Subscriber Certificate)有效期最多 7 天(此前为 10 天)。CA 可以支持吊销这类证书,CRL 分发点也是可选的。
  • 吊销时限。密钥泄露及类似情况为 24 小时,其他情况为 5 天(§4.9.1.1)。

随后,Let's Encrypt 完全放弃了 OCSP,“主要是因为它对互联网上的隐私构成了相当大的风险”:

  • 2025年1月30日:新的 Must-Staple 证书申请会失败。
  • 2025年5月7日:新证书不再包含 OCSP URL,改为包含 CRL URL。
  • 2025年8月6日:OCSP 响应器关闭。

如今证书中带有什么

2026年10月20日检查了几个网站是否有 OCSP URL、CRL URL 和 OCSP Stapling 响应,得到的结果参差不齐:

ocsp-survey.sh,2026年10月20日(OpenSSL 3.5.7)
letsencrypt.org      Let's Encrypt            OCSP=-                                    CRL=http://ye2.c.lencr.org/96.crl
    staple: OCSP response: no response sent
www.google.com       Google Trust Services    OCSP=-                                    CRL=http://c.pki.goog/we2/xuzt3PU9F_w.crl
    staple: OCSP response: no response sent
github.com           Sectigo Limited          OCSP=http://ocsp.sectigo.com              CRL=-
    staple: OCSP response: no response sent
www.digicert.com     DigiCert Inc             OCSP=http://ocsp.digicert.com             CRL=http://crl3.digicert.com/DigiCertEVRSACAG2.crl
    staple: OCSP Response Status: successful (0x0)
www.microsoft.com    Microsoft Corporation    OCSP=http://oneocsp.microsoft.com/ocsp    CRL=http://www.microsoft.com/pkiops/crl/partition/Microsoft%20TLS%20G2%20RSA%20CA%20OCSP%2004_Partition00093.crl
    staple: OCSP Response Status: successful (0x0)
www.amazon.com       Amazon                   OCSP=http://ocsp.r2m04.amazontrust.com    CRL=http://crl.r2m04.amazontrust.com/r2m04.crl
    staple: OCSP Response Status: successful (0x0)

Let's Encrypt 和 Google Trust Services 签发的证书只带 CRL。GitHub 那张来自 Sectigo 的证书情况正好相反:只有 OCSP,这也是《基线要求》允许的。只支持其中一种方式的客户端,无法检查上面所有证书。

CRL 并不小,但还在可控范围内:

三个真实的 CRL(2026年10月20日)
http://yr1.c.lencr.org/46.crl                      192226 bytes    5094 entries  Oct 20 12:26:45 2026 GMT -> Oct 29 12:26:44 2026 GMT
http://c.pki.goog/we2/xuzt3PU9F_w.crl               87562 bytes    2456 entries  Oct 20 11:12:29 2026 GMT -> Oct 30 10:12:28 2026 GMT
http://crl3.digicert.com/DigiCertEVRSACAG2.crl     635286 bytes   17091 entries  Oct 20 06:24:08 2026 GMT -> Oct 27 06:24:08 2026 GMT

Let's Encrypt 对 CRL 做了分片:每张证书指向众多编号文件中的一个,CRL 的颁发分发点(Issuing Distribution Point)扩展则注明这是哪个分片。CRL 解码器可以读取其中任何一个文件,用颁发者证书验证它的签名,并告诉你某张证书是否在列表中。

浏览器如何检查吊销状态

主流浏览器都已不再为普通证书发送 OCSP 请求:

  • Firefox:CRLite。Firefox 会下载一份压缩编码的数据,涵盖出现在 CT 日志中的所有已吊销证书,每 12 小时更新一次,并在本地检查。据 Mozilla 介绍,平均每天的数据量约为 300 kB。自 Firefox 137 起,CRLite 已对所有桌面版用户启用。Firefox 142 不再对域名验证证书使用 OCSP,Android 版 Firefox 则从 145 版开始采用 CRLite。
  • Chrome:CRLSets。Chrome 推送一份根据 CA 的 CRL 构建的列表,但其中只包含“被认定为已吊销的证书中的一部分”,而且“Chrome 一般不执行在线检查(即 OCSP 和 CRL)”(Chromium)。在我们的测试中,Chromium 156 正常加载了 https://revoked-isrgrootx1.letsencrypt.org/,状态码为 HTTP 200。
  • Apple:汇总列表。Apple 从各家 CA 收集吊销数据,再分发到设备上。2025年12月,Apple 开发者技术支持解释说,只有因密钥泄露等原因被吊销的证书,才“会进入 Apple 的‘黑名单’”;对于 Let's Encrypt 证书,也不会再做 OCSP 二次检查。因此,因普通原因被吊销的证书在 Safari 中可能仍能正常加载。

这在实践中意味着:对浏览器来说,吊销证书并不是能立即生效的紧急开关。Firefox 通过每 12 小时一次的 CRLite 更新获知吊销。对于因普通原因被吊销的证书,Chrome 和 Safari 可能永远不会拦截。密钥泄露后,真正起限制作用的是证书剩余的有效期,这也是转向 47 天证书的原因之一。

各种库和工具默认检查什么

2026年10月20日,下面的每个客户端都分别连接了 https://revoked-isrgrootx1.letsencrypt.org/(2026年10月7日被吊销)和 https://valid-isrgrootx1.letsencrypt.org/。被吊销的证书带有 CRL URL,没有 OCSP URL。

客户端访问已吊销站点的默认结果实测有效的吊销检查方式
Java 25 (JSSE)HTTP 200-Dcom.sun.net.ssl.checkRevocation=true -Dcom.sun.security.enableCRLDP=true
.NET 10 HttpClientHTTP 200SslOptions.CertificateRevocationCheckMode = X509RevocationMode.Online
Go 1.27 crypto/tls200 OK在 tls.Config.VerifyConnection 中自行检查
Python 3.13 ssl握手成功VERIFY_CRL_CHECK_LEAF,并把 CRL 加载到上下文中
Node.js 24 tls握手成功crl 选项,数组中每个元素放一个 CRL,覆盖证书链的每一级
curl 8.14 (OpenSSL)HTTP 200--crlfile,包含证书链每一级的 CRL
Chromium 156HTTP 200无(仅 CRLSet)

下面逐一介绍各客户端的细节。

Java

除非把 com.sun.net.ssl.checkRevocation 设为 true,否则 JSSE 不检查吊销状态。即使设置了,只要没有同时启用 CRL 分发点,它就只使用 OCSP,所以 Let's Encrypt 证书的验证失败原因是状态未知,而不是已吊销:

JDK 25.0.4.1,revoked-isrgrootx1.letsencrypt.org
--- 1. default
HTTP 200
--- 2. -Dcom.sun.net.ssl.checkRevocation=true
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path validation failed: java.security.cert.CertPathValidatorException: Could not determine revocation status
--- 3. checkRevocation + -Dcom.sun.security.enableCRLDP=true (revoked site)
javax.net.ssl.SSLHandshakeException: (certificate_revoked) PKIX path validation failed: java.security.cert.CertPathValidatorException: Certificate has been revoked, reason: UNSPECIFIED, revocation date: Wed Oct 07 18:37:19 UTC 2026, authority: CN=YR1, O=Let's Encrypt, C=US, extension OIDs: []
--- 4. same, valid site
HTTP 200

启用检查后,PKIX 验证器在无法确定状态时会直接判定失败(硬失败)。这样做是安全的,但 CRL 服务器一旦故障,你的服务也会随之中断。使用 java.security.cert.PKIXRevocationChecker 可以按连接选择 OCSP 与 CRL 的先后顺序以及软失败选项。

.NET

使用 SocketsHttpHandler 的 HttpClient 默认不检查吊销状态。把模式设为 Online 后,.NET 会自行获取 CRL 或 OCSP 响应。在 Linux 上,它通过自己基于 OpenSSL 的实现完成这一步:

.NET 10:为单个 HttpClient 启用吊销检查
var handler = new SocketsHttpHandler();
handler.SslOptions.CertificateRevocationCheckMode = X509RevocationMode.Online;
using var client = new HttpClient(handler);
Ubuntu 24.04 上的 .NET SDK 10.0.402
https://revoked-isrgrootx1.letsencrypt.org/ revocation=default -> 200
https://revoked-isrgrootx1.letsencrypt.org/ revocation=Online -> The remote certificate is invalid because of errors in the certificate chain: Revoked
https://valid-isrgrootx1.letsencrypt.org/ revocation=default -> 200
https://valid-isrgrootx1.letsencrypt.org/ revocation=Online -> 200

Go

crypto/tls 会验证证书链和名称,但从不检查吊销状态。如果需要,就在验证完成后自行检查 CRL。最难的部分可以交给 x509.ParseRevocationList 和 CheckSignatureFrom:

Go 1.27:在 VerifyConnection 中检查 CRL
func checkCRL(cs tls.ConnectionState) error {
	chain := cs.VerifiedChains[0]
	leaf, issuer := chain[0], chain[1]
	if len(leaf.CRLDistributionPoints) == 0 {
		return errors.New("certificate has no CRL distribution point")
	}
	resp, err := http.Get(leaf.CRLDistributionPoints[0])
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	der, err := io.ReadAll(io.LimitReader(resp.Body, 64<<20))
	if err != nil {
		return err
	}
	crl, err := x509.ParseRevocationList(der)
	if err != nil {
		return err
	}
	if err := crl.CheckSignatureFrom(issuer); err != nil {
		return fmt.Errorf("CRL signature: %w", err)
	}
	if time.Now().After(crl.NextUpdate) {
		return errors.New("CRL is past its nextUpdate")
	}
	for _, entry := range crl.RevokedCertificateEntries {
		if entry.SerialNumber.Cmp(leaf.SerialNumber) == 0 {
			return fmt.Errorf("certificate %X revoked at %s", leaf.SerialNumber, entry.RevocationTime.UTC())
		}
	}
	return nil
}

// client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{VerifyConnection: checkCRL}}}
Go 1.27.3:先用默认客户端,再加上 checkCRL
https://revoked-isrgrootx1.letsencrypt.org/ -> 200 OK
https://revoked-isrgrootx1.letsencrypt.org/ -> Get "https://revoked-isrgrootx1.letsencrypt.org/": certificate 67E51010C975E496C8EB800F3EF1BAB3F2B revoked at 2026-10-07 18:37:19 +0000 UTC
https://valid-isrgrootx1.letsencrypt.org/ -> 200 OK
https://valid-isrgrootx1.letsencrypt.org/ -> 200 OK

这个示例只检查叶子证书,而且每次握手都会下载 CRL。在生产环境中,应把每个 CRL 缓存到它的 NextUpdate 时间,并决定 CRL 服务器宕机时如何处理。

Python、Node.js 和 curl

三者都使用 OpenSSL 的 CRL 检查,并要求你自己提供 CRL。它们从不自行下载 CRL:

Python 3.13(Debian 13)和 Node.js 24.22.0,revoked-isrgrootx1.letsencrypt.org
python 3.13.5 default: handshake ok, TLSv1.3
python 3.13.5 VERIFY_CRL_CHECK_LEAF: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate revoked (_ssl.c:1029)
node v24.22.0 default: handshake ok
node v24.22.0 crl: UNABLE_TO_GET_CRL unable to get certificate CRL

Python 的 ssl.VERIFY_CRL_CHECK_LEAF 只检查叶子证书,因此用 ctx.load_verify_locations(cafile=...) 加载叶子证书的 CRL 就够了。Node.js 和 curl 会检查证书链中的每一张证书,所以只提供叶子证书的 CRL 时,即使访问有效站点也会报 UNABLE_TO_GET_CRL(Node)或 unable to get certificate CRL(curl)。要为证书链中的每张证书都提供对应的 CRL。对于 Node.js 24,应以数组形式传入,每个 CRL 一个 PEM 字符串。在我们的测试中,把三个 CRL 拼接成一个 buffer 传入时,仍然报 UNABLE_TO_GET_CRL:

Node.js 24.22.0,证书链中全部三张证书的 CRL
revoked-isrgrootx1.letsencrypt.org (one buffer): UNABLE_TO_GET_CRL unable to get certificate CRL
revoked-isrgrootx1.letsencrypt.org (array): CERT_REVOKED certificate revoked
valid-isrgrootx1.letsencrypt.org (one buffer): UNABLE_TO_GET_CRL unable to get certificate CRL
valid-isrgrootx1.letsencrypt.org (array): handshake ok

curl 的两个开关表现不同。--cert-status 要求服务器提供 OCSP Stapling 响应,因此在所有不提供 OCSP Stapling 的服务器上都会报错,包括所有 Let's Encrypt 站点。--crlfile 配合包含整条证书链 CRL 的文件即可正常工作:

curl 8.14.1(OpenSSL 3.5.7)
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" https://revoked-isrgrootx1.letsencrypt.org/
HTTP 200
$ curl -sS -o /dev/null --cert-status https://revoked-isrgrootx1.letsencrypt.org/
curl: (91) No OCSP response received
$ curl -sS -o /dev/null --crlfile revoked-chain-crls.pem https://revoked-isrgrootx1.letsencrypt.org/
curl: (60) SSL certificate problem: certificate revoked
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" --crlfile valid-chain-crls.pem https://valid-isrgrootx1.letsencrypt.org/
HTTP 200
$ curl -sS -o /dev/null --crlfile leaf-crl-only.pem https://valid-isrgrootx1.letsencrypt.org/
curl: (60) SSL certificate problem: unable to get certificate CRL

下面的脚本会收集服务器发送的每张证书对应的 CRL:

fetch-crls.sh HOST OUT:以 PEM 格式获取服务器整条证书链的 CRL
#!/bin/bash
host="$1"; out="$2"
dir=$(mktemp -d)
openssl s_client -connect "$host:443" -servername "$host" -showcerts </dev/null 2>/dev/null \
  | awk -v d="$dir" '/BEGIN CERTIFICATE/{n++} n>0{print > (d "/cert" n ".pem")}'
: > "$out"
for cert in "$dir"/cert*.pem; do
  url=$(openssl x509 -in "$cert" -noout -ext crlDistributionPoints 2>/dev/null | grep -o 'http[^ ]*' | head -1)
  [ -n "$url" ] && curl -s "$url" | openssl crl -inform DER >> "$out"
done
rm -r "$dir"
echo "$host: $(grep -c 'BEGIN X509 CRL' "$out") CRLs"

手动检查证书

用 OpenSSL 可以查看证书的状态信息存放在哪里,并直接检查。先查看扩展:

已吊销测试证书的扩展(OpenSSL 3.5.7)
$ openssl x509 -in leaf.pem -noout -ext crlDistributionPoints,authorityInfoAccess
Authority Information Access: 
    CA Issuers - URI:http://yr1.i.lencr.org/
X509v3 CRL Distribution Points: 
    Full Name:
      URI:http://yr1.c.lencr.org/44.crl

然后下载 CRL,并用它验证证书。-untrusted 用于提供服务器发送的中间证书:

openssl verify -crl_check(OpenSSL 3.5.7)
$ curl -s -o crl.der http://yr1.c.lencr.org/44.crl
$ openssl crl -inform DER -in crl.der -out crl.pem
$ openssl verify -crl_check -CRLfile crl.pem -untrusted chain.pem leaf.pem
CN=revoked-isrgrootx1.letsencrypt.org
error 23 at 0 depth lookup: certificate revoked
error leaf.pem: verification failed
$ openssl crl -in crl.pem -noout -text | grep -A1 "Serial Number: 067E51010C975E496C8EB800F3EF1BAB3F2B"
    Serial Number: 067E51010C975E496C8EB800F3EF1BAB3F2B
        Revocation Date: Oct  7 18:37:19 2026 GMT

对于仍在运行 OCSP 的 CA,可以直接查询响应器,或者查看 OCSP Stapling 响应:

openssl ocsp 与 s_client -status,www.digicert.com(输出已用 grep 筛选)
$ openssl ocsp -issuer issuer.pem -cert leaf.pem -url http://ocsp.digicert.com -resp_text -noverify
    OCSP Response Status: successful (0x0)
    Cert Status: good
    This Update: Oct 19 22:09:00 2026 GMT
    Next Update: Oct 26 21:09:00 2026 GMT
leaf.pem: good
	This Update: Oct 19 22:09:00 2026 GMT
	Next Update: Oct 26 21:09:00 2026 GMT
$ openssl s_client -connect www.digicert.com:443 -servername www.digicert.com -status </dev/null
    OCSP Response Status: successful (0x0)
    Cert Status: good

-noverify 会跳过对响应签名的检查,快速查看时没问题,但不能据此做决定。OCSP 解码器可以解析请求和响应,验证响应器的签名及其授权,还可以替你向响应器发起查询。

你的服务该怎么做

  • 先判断是否真的需要吊销检查。对于调用公共 API 的出站请求,浏览器的模式(短有效期、CT、CA 事件响应)通常就够了。对于高价值的连接,例如支付回调、组织之间的 mTLS,或者代码签名和文档签名,应显式检查吊销状态。
  • 优先支持 CRL。公共 CA 必须发布 CRL,而且有些已不再运行 OCSP。只支持 OCSP 的客户端无法判断 Let's Encrypt 和 Google Trust Services 证书的吊销状态。
  • 按 CRL 自身的周期缓存和刷新。每个 CRL 只下载一次,保留到 nextUpdate,并在后台刷新,这样 TLS 握手就永远不用等待下载。
  • 有意识地选择 fail-open 或 fail-closed。采用 fail-closed(无法完成检查时拒绝连接)时,CA 的 CDN 故障会变成你的服务中断。采用 fail-open(无法完成检查时照常放行)时,对任何能拦截 CRL 下载的人来说,这项检查都变得可有可无。两种结果都要记录到日志中。
  • 去掉对 Must-Staple 和 OCSP Stapling 的依赖。Let's Encrypt 会拒绝 Must-Staple 申请,而 OCSP Stapling 只对仍在运行 OCSP 的 CA 有用。
  • 私有 PKI 由你自己设计。可以发布 nextUpdate 较短的 CRL,也可以签发短期证书并省去吊销,就像《基线要求》现在对有效期不超过 7 天的证书所允许的那样。

参考资料

测试于2026年10月20日运行,环境为 node:24.22.0-trixie(OpenSSL 3.5.7、curl 8.14.1、Node.js 24.22.0、Python 3.13.5)、eclipse-temurin:25.0.4.1_1-jdk-noble、golang:1.27.3-trixie、mcr.microsoft.com/dotnet/sdk:10.0.402-noble 以及 mcr.microsoft.com/playwright:v1.64.1-noble(Chromium 156)。