要点:2024年3月以降、CA/Browser Forum は CA に CRL の公開を義務付け、OCSP を任意としています。Let's Encrypt は 2025年5月に証明書から OCSP URL を削除し、2025年8月にレスポンダーを停止しました。ブラウザーは、もはや証明書ごとに CA へ問い合わせてはいません。Firefox はローカルの CRLite フィルターを、Chrome は配信される CRLSet を確認し、Apple は独自に集約したリストを使っています。ほとんどのライブラリとコマンドラインツール(Java、.NET、Go、Python、Node.js、curl)は、デフォルトでは失効をまったく確認しません。今回のテストでは、いずれも失効した Let's Encrypt の証明書を受け入れました。クライアントが失効した証明書を拒否しなければならない場合は、CRL チェックを明示的に有効にし、証明書の有効期間が短くなることも見越して計画を立ててください。業界はいずれにせよ、後者の方向に向かっています。
失効の仕組みに見直しが必要になった理由
証明書は、有効期限が切れる前に失効させることができます。典型的なのは、鍵が漏洩した場合や、誤って発行された場合です。それをクライアントに伝える従来の方法は、次の 2 つでした。
- CRL(証明書失効リスト、RFC 5280):CA が、失効したシリアル番号の署名付きリストを公開します。証明書は、CRL 配布ポイント(CRL Distribution Points)拡張でこのリストを指します。
- OCSP(RFC 6960):クライアントが CA のレスポンダーに 1 つの証明書について問い合わせ、署名付きの「有効(good)」「失効(revoked)」「不明(unknown)」のいずれかを受け取ります。その URL は、機関情報アクセス(Authority Information Access)拡張に記載されています。
どちらの方式も、Web では問題を抱えていました。CRL はサイズが大きくなることがありました。OCSP は、ユーザーが訪問したすべてのサイトを CA に知らせてしまい、遅延も増やしました。さらに、レスポンダーに到達できないとき、ブラウザーは「ソフトフェイル」せざるを得ませんでした。OCSP リクエストを遮断できる攻撃者がいれば、この確認は役に立ちません。OCSP ステープリングは、サーバーがレスポンスを取得して添付することでプライバシーの問題を解決しましたが、全面的に普及するには至りませんでした。ステープリングされたレスポンスを必須とする証明書のフラグ Must-Staple は、ブラウザーでほとんどサポートされませんでした。
現在のルール
投票 SC-063(「OCSP を任意とし、CRL を必須とし、自動化を促進する」、2024年3月15日発効)により、ベースライン要件は次のように変わりました。
- CRL は必須:CA は、最初の証明書を発行してから 24 時間以内に、完全な CRL またはシャードに分割した CRL を公開しなければなりません。CRL は、すべての証明書に OCSP URL が記載されている場合は少なくとも 7 日ごと、そうでない場合は少なくとも 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)。
- 短期証明書には失効情報が不要:「短期サブスクライバー証明書(Short-lived Subscriber Certificate)」の有効期間は、2026年3月15日以降に発行されたものでは最長 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 レスポンスの有無を確認したところ、結果はまちまちでした。
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 だけを記載した証明書を発行しています。Sectigo が発行した GitHub の証明書はその逆で、OCSP だけを記載しています。これも BR(ベースライン要件)で認められています。2 つの方式の一方にしか対応していないクライアントでは、これらすべてを確認することはできません。
CRL は小さくはありませんが、扱えないほどの大きさでもありません。
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 をシャードに分割しています。各証明書は番号付きの多数のファイルのうちの 1 つを指し、CRL の発行配布ポイント(Issuing Distribution Point)拡張がそのシャードを示します。CRL デコーダーは、これらのどのファイルでも読み込み、発行者の証明書で署名を検証して、特定の証明書が記載されているかどうかを示します。
ブラウザーによる失効確認の仕組み
主要なブラウザーはいずれも、通常の証明書について OCSP リクエストを送信しなくなりました。
- Firefox の CRLite:Firefox は、CT ログに記録されている失効したすべての証明書を圧縮してエンコードしたデータをダウンロードし、12 時間ごとに更新して、ローカルで確認します。Mozilla によると、データ量は平均で 1 日あたり約 300 kB です。Firefox 137 以降、すべてのデスクトップユーザーに対して有効になっています。Firefox 142 はドメイン検証(DV)証明書での OCSP の利用を停止し、Android 版 Firefox はバージョン 145 で CRLite を採用しました。
- Chrome の CRLSets:Chrome は CA の CRL から作成したリストを配信しますが、そこに含まれるのは「失効と判定された証明書の一部」だけです。また、「オンライン(つまり OCSP と CRL)の確認は、Chrome では基本的に行われません」(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 日の証明書へ移行する理由の 1 つです。
ライブラリとツールがデフォルトで確認する内容
以下のクライアントはすべて、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 HttpClient | HTTP 200 | SslOptions.CertificateRevocationCheckMode = X509RevocationMode.Online |
Go 1.27 crypto/tls | 200 OK | tls.Config.VerifyConnection での独自の確認 |
Python 3.13 ssl | ハンドシェイク成功 | VERIFY_CRL_CHECK_LEAF と、コンテキストに読み込んだ CRL |
Node.js 24 tls | ハンドシェイク成功 | crl オプション(配列の 1 要素に CRL を 1 つ、チェーンの全階層分) |
| curl 8.14(OpenSSL) | HTTP 200 | チェーンの全階層の CRL を含む --crlfile |
| Chromium 156 | HTTP 200 | なし(CRLSet のみ) |
以下、クライアントごとの詳細を説明します。
Java
JSSE は、com.sun.net.ssl.checkRevocation が true でない限り失効を確認しません。その場合でも、CRL 配布ポイントもあわせて有効にしない限り OCSP しか使いません。そのため、Let's Encrypt の証明書の検証は、失効ではなく状態不明として失敗します。
--- 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 の上に構築された独自の実装でこれを行います。
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 はチェーンと名前を検証しますが、失効は一切確認しません。必要な場合は、検証の後で自分で CRL を確認してください。難しい部分は x509.ParseRevocationList と CheckSignatureFrom が処理してくれます。
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
この例はリーフ証明書だけを確認し、ハンドシェイクのたびに CRL を取得します。本番環境では、各 CRL を NextUpdate までキャッシュし、CRL サーバーがダウンしたときの動作を決めておいてください。
Python、Node.js、curl
この 3 つはいずれも OpenSSL の CRL チェックを使い、CRL は利用者が用意する前提になっています。CRL を自らダウンロードすることはありません。
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 1 つにつき PEM 文字列 1 つを要素とする配列として渡します。今回のテストでは、3 つの CRL を連結した 1 つのバッファーでは、やはり UNABLE_TO_GET_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 の 2 つのオプションは動作が異なります。--cert-status はステープリングされた OCSP レスポンスを要求するため、ステープリングしないすべてのサーバーで失敗します。Let's Encrypt のサイトもすべてこれに該当します。--crlfile は、チェーン全体の CRL を含むファイルを指定すれば機能します。
$ 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 を収集します。
#!/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 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 で、サーバーが送信した中間証明書を指定します。
$ 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 の場合は、レスポンダーに直接問い合わせるか、ステープリングされたレスポンスを確認します。
$ 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 をもう運用していない CA もあります。OCSP にしか対応していないクライアントは、Let's Encrypt と Google Trust Services の証明書について失効の有無を判断できません。
- CRL をそのスケジュールに沿ってキャッシュ・更新する:各 CRL は一度だけ取得して
nextUpdateまで保持し、バックグラウンドで更新します。こうすれば、TLS ハンドシェイクがダウンロードを待つことはありません。 - フェイルオープンかフェイルクローズかを意識して選ぶ:フェイルクローズでは、CA の CDN の問題が自分のサービスの障害になります。フェイルオープンでは、CRL のダウンロードを遮断できる者なら誰でも、この確認を素通りできます。どちらの結果もログに記録してください。
- Must-Staple と OCSP ステープリングへの依存関係をなくす:Let's Encrypt は Must-Staple のリクエストを拒否します。また、ステープリングが役立つのは、まだ OCSP を運用している CA の場合だけです。
- プライベート PKI は自由に設計できる:
nextUpdateを短くした CRL を公開するか、短期証明書を発行して失効を省略します。後者は、現在の BR が有効期間 7 日以下の証明書に認めているやり方です。
出典
- CA/Browser Forum、ベースライン要件 v2.3.1:§4.9.1.1(失効の期限)、§4.9.7(CRL の更新頻度)、§4.9.9(OCSP)、§7.1.2.11.2(CRL 配布ポイント)、§7.2(CRL プロファイル)、短期サブスクライバー証明書(Short-lived Subscriber Certificate)の定義。投票 SC-063
- RFC 5280(CRL)と RFC 6960(OCSP)
- Let's Encrypt、2025年の OCSP サポート終了
- Mozilla、CRLite:Firefox における高速、プライベート、包括的な証明書失効確認(2025年8月)と、Android 版 Firefox 145 のリリースノート
- Chromium、CRLSets
- Apple のデベロッパーフォーラム、スレッド 808875(2025年12月)
テストは 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)で実行しました。