Short answer. Since March 2024 the CA/Browser Forum requires CAs to publish CRLs and makes OCSP optional. Let's Encrypt removed OCSP URLs from its certificates in May 2025 and switched its responders off in August 2025. Browsers no longer ask CAs about each certificate. Firefox checks a local CRLite filter, Chrome checks its pushed CRLSet, and Apple uses its own aggregated list. Most libraries and command-line tools (Java, .NET, Go, Python, Node.js, curl) do not check revocation at all by default. All of them accepted a revoked Let's Encrypt certificate in our tests. If your client must reject revoked certificates, enable CRL checking explicitly, and plan for short certificate lifetimes. The latter is where the industry is heading anyway.
Why revocation needed a rethink
A certificate can be revoked before it expires, typically because its key leaked or it was issued by mistake. The two classic ways to tell clients were:
- CRL (Certificate Revocation List, RFC 5280): the CA publishes a signed list of revoked serial numbers. The certificate points to it in its CRL Distribution Points extension.
- OCSP (RFC 6960): the client asks the CA's responder about one certificate and gets a signed "good", "revoked" or "unknown". The URL is in the Authority Information Access extension.
Both had problems on the web. CRLs could be large. OCSP leaked every site a user visited to the CA, added latency, and browsers had to "soft-fail" when the responder was unreachable. An attacker who can block the OCSP request makes the check useless. OCSP stapling fixed the privacy problem by having the server fetch and attach the response, but it never became universal. Must-Staple, a certificate flag requiring a stapled response, saw little browser support.
What the rules say now
Ballot SC-063 ("Make OCSP optional, require CRLs, and incentivize automation", effective 15 March 2024) changed the Baseline Requirements:
- CRLs are mandatory. A CA must publish a full or sharded CRL within 24 hours of issuing its first
certificate. It must update the CRL at least every 7 days if all its certificates carry an OCSP URL and every
4 days otherwise, and within 24 hours after revoking a certificate (§4.9.7). For subscriber certificates,
nextUpdatemay be at most 10 days afterthisUpdate(§7.2). - OCSP is optional. If a certificate has an OCSP URL, the responder must answer, and its responses must be valid for 8 hours to 10 days (§4.9.9). A certificate without an OCSP URL must carry a CRL Distribution Point (§7.1.2.11.2).
- Short-lived certificates need no revocation information. A "Short-lived Subscriber Certificate" is valid for at most 7 days if issued from 15 March 2026 (10 days before). The CA may support revoking it, and the CRL Distribution Point is optional.
- Revocation deadlines. 24 hours for key compromise and similar cases, 5 days for the rest (§4.9.1.1).
Let's Encrypt then dropped OCSP entirely, "primarily because it represents a considerable risk to privacy on the Internet":
- 30 January 2025: new Must-Staple requests fail.
- 7 May 2025: OCSP URLs are removed from new certificates, which carry CRL URLs instead.
- 6 August 2025: the OCSP responders are turned off.
What certificates carry today
Checking a few sites on 20 October 2026 for an OCSP URL, a CRL URL and a stapled OCSP response gives a mixed picture:
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 and Google Trust Services issue CRL-only certificates. The GitHub certificate from Sectigo is the mirror case, OCSP only, which the BR allows. A client that supports only one of the two methods cannot check all of these.
CRLs are not small, but they are manageable:
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 shards its CRLs: each certificate points to one of many numbered files, and the CRL's Issuing Distribution Point extension names that shard. The CRL decoder reads any of these files, checks its signature against the issuer, and tells you whether a given certificate is on it.
How browsers check revocation
None of the major browsers sends an OCSP request for an ordinary certificate any more:
- Firefox: CRLite. Firefox downloads a compressed encoding of all revoked certificates that appear in CT logs, updates it every 12 hours, and checks it locally. That is about 300 kB of data per day on average, according to Mozilla. It has been enabled for all desktop users since Firefox 137. Firefox 142 stopped using OCSP for domain-validated certificates, and Firefox for Android adopted CRLite in version 145.
- Chrome: CRLSets. Chrome pushes a list built from CA CRLs, but only "a subset of the certificates
identified as revoked", and "Online (i.e. OCSP and CRL) checks are not generally performed by Chrome"
(Chromium). In our test, Chromium 156 loaded
https://revoked-isrgrootx1.letsencrypt.org/with HTTP 200. - Apple: an aggregated list. Apple collects revocation data from CAs and distributes it to devices. In December 2025, Apple's developer technical support explained that certificates "only end up on Apple's 'naughty' list" when revoked for reasons such as key compromise, and that for Let's Encrypt certificates there is no secondary OCSP check. A certificate revoked for an ordinary reason may therefore still load in Safari.
What this means in practice: revoking a certificate is not a fast kill switch for browsers. Firefox picks up revocations through its 12-hourly CRLite updates. Chrome and Safari may never block an ordinary revocation. The real limit on a leaked key is the certificate's remaining lifetime, which is one reason for the move to 47-day certificates.
What libraries and tools check by default
Every client below connected to https://revoked-isrgrootx1.letsencrypt.org/ (revoked 7 October 2026) and
https://valid-isrgrootx1.letsencrypt.org/ on 20 October 2026. The revoked certificate has a CRL URL and no
OCSP URL.
| Client | Default on the revoked site | Revocation checking that worked |
|---|---|---|
| Java 25 (JSSE) | HTTP 200 | -Dcom.sun.net.ssl.checkRevocation=true -Dcom.sun.security.enableCRLDP=true |
.NET 10 HttpClient | HTTP 200 | SslOptions.CertificateRevocationCheckMode = X509RevocationMode.Online |
Go 1.27 crypto/tls | 200 OK | Your own check in tls.Config.VerifyConnection |
Python 3.13 ssl | handshake ok | VERIFY_CRL_CHECK_LEAF plus the CRL loaded into the context |
Node.js 24 tls | handshake ok | crl option with one CRL per array element, for every level of the chain |
| curl 8.14 (OpenSSL) | HTTP 200 | --crlfile with CRLs for every level of the chain |
| Chromium 156 | HTTP 200 | None (CRLSet only) |
Details per client follow.
Java
JSSE does not check revocation unless com.sun.net.ssl.checkRevocation is true. Even then it uses only
OCSP unless CRL Distribution Points are enabled too, which is why a Let's Encrypt certificate fails as
unknown rather than as revoked:
--- 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
With checking enabled, the PKIX validator fails hard when it cannot determine a status. That is safe, but a
CRL server outage then becomes your outage. java.security.cert.PKIXRevocationChecker lets you choose the
order of OCSP and CRL and soft-fail options per connection.
.NET
HttpClient with SocketsHttpHandler does not check revocation by default. Setting the mode to Online
makes .NET fetch the CRL or OCSP response itself. On Linux it does this through its own implementation over
OpenSSL:
var handler = new SocketsHttpHandler();
handler.SslOptions.CertificateRevocationCheckMode = X509RevocationMode.Online;
using var client = new HttpClient(handler);
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 verifies the chain and the name and never checks revocation. If you need it, check the CRL
yourself after verification. x509.ParseRevocationList and CheckSignatureFrom do the hard parts:
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}}}
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
The example checks the leaf only and fetches the CRL on every handshake. In production, cache each CRL until
its NextUpdate, and decide what happens when the CRL server is down.
Python, Node.js and curl
All three use OpenSSL's CRL checking and expect you to supply the CRLs. They never download them:
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's ssl.VERIFY_CRL_CHECK_LEAF checks only the leaf, so loading the leaf's CRL with
ctx.load_verify_locations(cafile=...) is enough. Node.js and curl check every certificate in the chain,
so a CRL for the leaf alone gives UNABLE_TO_GET_CRL (Node) or unable to get certificate CRL (curl) even on
a valid site. Give them the CRL of each certificate in the chain. For Node.js 24, pass them as an array with
one PEM string per CRL. A single buffer with three concatenated CRLs still failed with UNABLE_TO_GET_CRL in
our test:
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's two switches behave differently. --cert-status demands a stapled OCSP response, so it fails on every
server that doesn't staple, including all Let's Encrypt sites. --crlfile works with a file holding the CRLs
for the whole chain:
$ 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
This script collects the CRL of every certificate a server sends:
#!/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"
Checking a certificate by hand
With OpenSSL you can see where a certificate's status lives and check it directly. Look at the extensions first:
$ 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
Then download the CRL and verify the certificate against it. -untrusted supplies the intermediates the server
sent:
$ 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
For a CA that still runs OCSP, ask the responder directly or look at the stapled response:
$ 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 skips the check of the response signature, which is fine for a quick look but not for a decision.
The OCSP decoder parses requests and responses, verifies the
responder's signature and authority, and can query a responder for you.
What to do in your service
- Decide whether you need revocation checks at all. For outbound calls to public APIs, the browser model (short lifetimes, CT, CA incident response) is usually enough. For high-value connections, such as payment callbacks, mTLS between organizations or code and document signing, check revocation explicitly.
- Support CRLs first. Public CAs must publish them, and some no longer run OCSP. A client that only does OCSP will be unable to decide for Let's Encrypt and Google Trust Services certificates.
- Cache and refresh CRLs on their schedule. Fetch each CRL once, keep it until
nextUpdate, and refresh it in the background, so a TLS handshake never waits for a download. - Choose fail-open or fail-closed consciously. Fail-closed turns a CA's CDN problem into your outage. Fail-open makes the check optional for anyone who can block the CRL download. Log both outcomes.
- Drop Must-Staple and OCSP-stapling dependencies. Let's Encrypt rejects Must-Staple requests, and stapling helps only for CAs that still run OCSP.
- Private PKI is yours to design. Publish CRLs with a short
nextUpdate, or issue short-lived certificates and skip revocation, the way the BR now allows for certificates of 7 days or less.
Sources
- CA/Browser Forum Baseline Requirements v2.3.1: §4.9.1.1 (revocation deadlines), §4.9.7 (CRL frequency), §4.9.9 (OCSP), §7.1.2.11.2 (CRL distribution points), §7.2 (CRL profile), definition of Short-lived Subscriber Certificate. Ballot SC-063.
- RFC 5280 (CRLs) and RFC 6960 (OCSP).
- Let's Encrypt, Ending OCSP Support in 2025.
- Mozilla, CRLite: Fast, private, and comprehensive certificate revocation checking in Firefox (August 2025), and the Firefox for Android 145 release notes.
- Chromium, CRLSets.
- Apple Developer Forums, thread 808875 (December 2025).
Tests ran on 20 October 2026 in 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
and mcr.microsoft.com/playwright:v1.64.1-noble (Chromium 156).