Repository: snowflakedb/gosnowflake
Pull request #1465 (2025-07-14 13:31:03)

SNOW-2117152 Implement CRL algorithm
### Description

SNOW-2117152 Implemented the main CRL algorithm.

### Checklist
- [x] Created tests which fail without the change (if possible)
- [x] Extended the README / documentation, if necessary


codecov[bot]: ### :x: 1 Tests Failed:
| Tests completed | Failed | Passed | Skipped |
|---|---|---|---|
| 2794 | 1 | 2793 | 71 |
<details><summary>View the top 1 failed test(s) by shortest run time</summary>

> <pre><code class="language-python">github.com/snowflakedb/gosnowflake::TestLargeSetResultCancel</code></pre>
> <details><summary>Stack Traces | 3.01s run time</summary>
> 
> > <pre><code class="language-python">Failed</code></pre>
> 
> </details>

</details>

To view more test analytics, go to the [Test Analytics Dashboard](https://app.codecov.io/gh/snowflakedb/gosnowflake/tests/SNOW-2117152-crl)
<sub>📋 Got 3 mins? [Take this short survey](https://forms.gle/BpocVj23nhr2Y45G7) to help us improve Test Analytics.</sub>

Patch:
diff --git a/crl.go b/crl.go
new file mode 100644
index 0000000..21177ef
--- /dev/null
+++ b/crl.go
@@ -0,0 +1,423 @@
+package gosnowflake
+
+import (
+	"crypto/x509"
+	"encoding/asn1"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"os"
+	"path/filepath"
+	"sync"
+	"time"
+)
+
+var idpOID = asn1.ObjectIdentifier{2, 5, 29, 28}
+
+type distributionPointName struct {
+	FullName []asn1.RawValue `asn1:"optional,tag:0"`
+}
+
+type issuingDistributionPoint struct {
+	DistributionPoint distributionPointName `asn1:"optional,tag:0"`
+}
+
+type crlValidator struct {
+	certRevocationCheckMode        CertRevocationCheckMode
+	allowCertificatesWithoutCrlURL bool
+	cacheValidityTime              time.Duration
+	inMemoryCacheDisabled          bool
+	inMemoryCache                  map[string]*crlInMemoryCacheValueType
+	inMemoryCacheMutex             sync.Mutex
+	onDiskCacheDisabled            bool
+	onDiskCacheDir                 string
+	crlURLMutexes                  map[string]*sync.Mutex
+	httpClient                     *http.Client
+}
+
+type crlInMemoryCacheValueType struct {
+	crl          *x509.RevocationList
+	downloadTime *time.Time
+}
+
+func newCrlValidator(certRevocationCheckMode CertRevocationCheckMode, allowCertificatesWithoutCrlURL bool, cacheValidityTime time.Duration, inMemoryCacheDisabled, onDiskCacheDisabled bool, onDiskCacheDir string, httpClient *http.Client) *crlValidator {
+	var inMemoryCache map[string]*crlInMemoryCacheValueType
+	if !inMemoryCacheDisabled {
+		inMemoryCache = make(map[string]*crlInMemoryCacheValueType)
+	}
+	return &crlValidator{
+		certRevocationCheckMode:        certRevocationCheckMode,
+		allowCertificatesWithoutCrlURL: allowCertificatesWithoutCrlURL,
+		cacheValidityTime:              cacheValidityTime,
+		inMemoryCacheDisabled:          inMemoryCacheDisabled,
+		inMemoryCache:                  inMemoryCache,
+		onDiskCacheDisabled:            onDiskCacheDisabled,
+		onDiskCacheDir:                 onDiskCacheDir,
+		crlURLMutexes:                  make(map[string]*sync.Mutex),
+		httpClient:                     httpClient,
+	}
+}
+
+// CertRevocationCheckMode defines the modes for certificate revocation checks.
+type CertRevocationCheckMode int
+
+const (
+	// CertRevocationCheckDisabled means that certificate revocation checks are disabled.
+	CertRevocationCheckDisabled CertRevocationCheckMode = iota
+	// CertRevocationCheckAdvisory means that certificate revocation checks are advisory, and the driver will not fail if the checks end with error (cannot verify revocation status).
+	// Driver will fail only if a certicate is revoked.
+	CertRevocationCheckAdvisory
+	// CertRevocationCheckEnabled means that every certificate revocation check must pass, otherwise the driver will fail.
+	CertRevocationCheckEnabled
+)
+
+func (m CertRevocationCheckMode) String() string {
+	switch m {
+	case CertRevocationCheckDisabled:
+		return "CERT_REVOCATION_CHECK_DISABLED"
+	case CertRevocationCheckAdvisory:
+		return "CERT_REVOCATION_CHECK_ADVISORY"
+	case CertRevocationCheckEnabled:
+		return "CERT_REVOCATION_CHECK_ENABLED"
+	default:
+		return fmt.Sprintf("unknown CertRevocationCheckMode: %d", m)
+	}
+}
+
+type crlValidationResult int
+
+const (
+	crlRevoked crlValidationResult = iota
+	crlUnrevoked
+	crlError
+)
+
+type certValidationResult int
+
+const (
+	certRevoked certValidationResult = iota
+	certUnrevoked
+	certError
+)
+
+// TODO in following commits:
+// - clean up in memory cache and on-disk cache
+// - telemetry
+// - initialize into the main flow
+func (cv *crlValidator) verifyPeerCertificates(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+	if cv.certRevocationCheckMode == CertRevocationCheckDisabled {
+		logger.Debug("certificate revocation check is disabled, skipping CRL validation")
+		return nil
+	}
+	crlValidationResults := cv.validateChains(verifiedChains)
+
+	allRevoked := true
+	for _, result := range crlValidationResults {
+		if result == crlUnrevoked {
+			logger.Debug("found certificate chain with no revoked certificates")
+			return nil
+		}
+		if result != crlRevoked {
+			allRevoked = false
+		}
+	}
+
+	if allRevoked {
+		return fmt.Errorf("every verified certificate chain contained revoked certificates")
+	}
+
+	logger.Warn("some certificate chains didn't pass or driver wasn't able to peform the checks")
+	if cv.certRevocationCheckMode == CertRevocationCheckAdvisory {
+		logger.Warn("certificate revocation check is set to CERT_REVOCATION_CHECK_ADVISORY, so assuming that certificates are not revoked")
+		return nil
+	}
+	return fmt.Errorf("certificate revocation check failed")
+}
+
+func (cv *crlValidator) validateChains(chains [][]*x509.Certificate) []crlValidationResult {
+	crlValidationResults := make([]crlValidationResult, len(chains))
+	for i, chain := range chains {
+		crlValidationResults[i] = crlUnrevoked
+		chainStr := ""
+		for _, cert := range chain {
+			chainStr += fmt.Sprintf("%v -> ", cert.Subject)
+		}
+		logger.Debugf("validating certificate chain %d: %s", i, chainStr)
+		for j, cert := range chain {
+			if j == len(chain)-1 {
+				logger.Debugf("skipping root certificate %v for CRL validation", cert.Subject)
+				continue
+			}
+
+			if isShortLivedCertificate(cert) {
+				logger.Debugf("certificate %v is short-lived, skipping CRL validation", cert.Subject)
+				continue
+			}
+
+			if len(cert.CRLDistributionPoints) == 0 {
+				if cv.allowCertificatesWithoutCrlURL {
+					logger.Debugf("certificate %v has no CRL distribution points, skipping CRL validation", cert.Subject)
+					continue
+				}
+				logger.Warnf("certificate %v has no CRL distribution points, skipping CRL validation, but marking as error", cert.Subject)
+				crlValidationResults[i] = crlError
+				continue
+			}
+
+			certStatus := cv.validateCertificate(cert, chain[j+1])
+			if certStatus == certRevoked {
+				crlValidationResults[i] = crlRevoked
+				break
+			}
+
+			if certStatus == certError {
+				crlValidationResults[i] = crlError
+				continue
+			}
+		}
+
+		if crlValidationResults[i] == crlUnrevoked {
+			logger.Debugf("certificate chain %d is unrevoked, skipping remaining chains", i)
+			break
+		}
+	}
+
+	return crlValidationResults
+}
+
+func (cv *crlValidator) validateCertificate(cert *x509.Certificate, parent *x509.Certificate) certValidationResult {
+	for _, crlURL := range cert.CRLDistributionPoints {
+		result := cv.validateCrlAgainstCrlURL(cert, crlURL, parent)
+		if result == certRevoked || result == certError {
+			return result
+		}
+	}
+	return certUnrevoked
+}
+
+func (cv *crlValidator) validateCrlAgainstCrlURL(cert *x509.Certificate, crlURL string, parent *x509.Certificate) certValidationResult {
+	now := time.Now()
+
+	mu := cv.getOrCreateMutex(crlURL)
+	mu.Lock()
+	defer mu.Unlock()
+
+	crl, downloadTime := cv.getFromCache(crlURL)
+	needsFreshCrl := crl == nil || crl.NextUpdate.Before(now) || downloadTime.Add(cv.cacheValidityTime).Before(now)
+	shouldUpdateCrl := false
+
+	if needsFreshCrl {
+		newCrl, newDownloadTime, err := cv.downloadCrl(crlURL)
+		if err != nil {
+			logger.Warnf("failed to download CRL from %v: %v", crlURL, err)
+		}
+		shouldUpdateCrl = newCrl != nil && (crl == nil || newCrl.ThisUpdate.After(crl.ThisUpdate))
+		if shouldUpdateCrl {
+			logger.Debugf("Found updated CRL for %v", crlURL)
+			crl = newCrl
+			downloadTime = newDownloadTime
+		} else {
+			if crl != nil && crl.NextUpdate.After(now) {
+				logger.Debugf("CRL for %v is up-to-date, using cached version", crlURL)
+			} else {
+				logger.Warnf("CRL for %v is not available or outdated", crlURL)
+				return certError
+			}
+		}
+	}
+
+	logger.Debugf("CRL has %v entries, next update at %v", len(crl.RevokedCertificateEntries), crl.NextUpdate)
+	if err := cv.validateCrl(crl, parent, crlURL); err != nil {
+		return certError
+	}
+
+	if shouldUpdateCrl {
+		logger.Debugf("CRL for %v is valid, updating cache", crlURL)
+		cv.updateCache(crlURL, crl, downloadTime)
+	}
+
+	for _, rce := range crl.RevokedCertificateEntries {
+		if cert.SerialNumber.Cmp(rce.SerialNumber) == 0 {
+			logger.Warnf("certificate for %v (serial number %v) has been revoked at %v, reason: %v", cert.Subject, rce.SerialNumber, rce.RevocationTime, rce.ReasonCode)
+			return certRevoked
+		}
+	}
+
+	return certUnrevoked
+}
+
+func (cv *crlValidator) validateCrl(crl *x509.RevocationList, parent *x509.Certificate, crlURL string) error {
+	if crl.Issuer.String() != parent.Subject.String() {
+		err := fmt.Errorf("CRL issuer %v does not match parent certificate subject %v for %v", crl.Issuer, parent.Subject, crlURL)
+		logger.Warn(err)
+		return err
+	}
+	if err := crl.CheckSignatureFrom(parent); err != nil {
+		logger.Warnf("CRL signature verification failed for %v: %v", crlURL, err)
+		return err
+	}
+	if err := cv.verifyAgainstIdpExtension(crl, crlURL); err != nil {
+		logger.Warnf("CRL IDP extension verification failed for %v: %v", crlURL, err)
+		return err
+	}
+	return nil
+}
+
+func (cv *crlValidator) getFromCache(crlURL string) (*x509.RevocationList, *time.Time) {
+	if cv.inMemoryCacheDisabled {
+		logger.Debugf("in-memory cache is disabled")
+	} else {
+		cv.inMemoryCacheMutex.Lock()
+		cacheValue, exists := cv.inMemoryCache[crlURL]
+		cv.inMemoryCacheMutex.Unlock()
+		if exists {
+			logger.Debugf("found CRL in cache for %v", crlURL)
+			return cacheValue.crl, cacheValue.downloadTime
+		}
+	}
+	if cv.onDiskCacheDisabled {
+		logger.Debugf("CRL cache is disabled, not checking disk for %v", crlURL)
+		return nil, nil
+	}
+	crlFilePath := cv.crlURLToPath(crlURL)
+	fileHandle, err := os.Open(crlFilePath)
+	if err != nil {
+		logger.Debugf("cannot open CRL from disk for %v (%v): %v", crlURL, crlFilePath, err)
+		return nil, nil
+	}
+	defer func() {
+		if err := fileHandle.Close(); err != nil {
+			logger.Warnf("failed to close CRL file handle for %v (%v): %v", crlURL, crlFilePath, err)
+		}
+	}()
+	stat, err := fileHandle.Stat()
+	if err != nil {
+		logger.Debugf("cannot stat CRL file for %v (%v): %v", crlURL, crlFilePath, err)
+		return nil, nil
+	}
+	crlBytes, err := io.ReadAll(fileHandle)
+	if err != nil {
+		logger.Debugf("cannot read CRL from disk for %v (%v): %v", crlURL, crlFilePath, err)
+		return nil, nil
+	}
+	crl, err := x509.ParseRevocationList(crlBytes)
+	if err != nil {
+		logger.Warnf("cannot parse CRL from disk for %v (%v): %v", crlURL, crlFilePath, err)
+		return nil, nil
+	}
+	modTime := stat.ModTime()
+
+	if !cv.inMemoryCacheDisabled {
+		// promote CRL to in-memory cache
+		cv.inMemoryCacheMutex.Lock()
+		cv.inMemoryCache[crlURL] = &crlInMemoryCacheValueType{
+			crl: crl,
+			// modTime is not the exact time the CRL was downloaded, but rather the last modification time of the file
+			// still, it is good enough for our purposes
+			downloadTime: &modTime,
+		}
+		cv.inMemoryCacheMutex.Unlock()
+	}
+	return crl, &modTime
+}
+
+func (cv *crlValidator) updateCache(crlURL string, crl *x509.RevocationList, downloadTime *time.Time) {
+	if cv.inMemoryCacheDisabled {
+		logger.Debugf("in-memory cache is disabled, not updating")
+	} else {
+		cv.inMemoryCacheMutex.Lock()
+		cv.inMemoryCache[crlURL] = &crlInMemoryCacheValueType{
+			crl:          crl,
+			downloadTime: downloadTime,
+		}
+		cv.inMemoryCacheMutex.Unlock()
+	}
+	if cv.onDiskCacheDisabled {
+		logger.Debugf("CRL cache is disabled, not writing to disk for %v", crlURL)
+		return
+	}
+	crlFilePath := cv.crlURLToPath(crlURL)
+	if err := os.MkdirAll(filepath.Dir(crlFilePath), 0755); err != nil {
+		logger.Warnf("failed to create directory for CRL file %v: %v", crlFilePath, err)
+		return
+	}
+	if err := os.WriteFile(crlFilePath, crl.Raw, 0600); err != nil {
+		logger.Warnf("failed to write CRL to disk for %v (%v): %v", crlURL, crlFilePath, err)
+	}
+}
+
+func (cv *crlValidator) downloadCrl(crlURL string) (*x509.RevocationList, *time.Time, error) {
+	logger.Debugf("downloading CRL from %v", crlURL)
+	now := time.Now()
+	resp, err := cv.httpClient.Get(crlURL)
+	if err != nil {
+		return nil, nil, err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode >= 400 {
+		return nil, nil, fmt.Errorf("failed to download CRL from %v, status code: %v", crlURL, resp.StatusCode)
+	}
+	crlBytes, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, nil, err
+	}
+	logger.Debugf("downloaded %v bytes for CRL %v", len(crlBytes), crlURL)
+	crl, err := x509.ParseRevocationList(crlBytes)
+	if err != nil {
+		return nil, nil, err
+	}
+	return crl, &now, err
+}
+
+func (cv *crlValidator) crlURLToPath(crlURL string) string {
+	// Convert CRL URL to a file path, e.g., by replacing slashes with underscores
+	return filepath.Join(cv.onDiskCacheDir, url.QueryEscape(crlURL))
+}
+
+func (cv *crlValidator) verifyAgainstIdpExtension(crl *x509.RevocationList, distributionPoint string) error {
+	for _, ext := range append(crl.Extensions, crl.ExtraExtensions...) {
+		if ext.Id.Equal(idpOID) {
+			var idp issuingDistributionPoint
+			_, err := asn1.Unmarshal(ext.Value, &idp)
+			if err != nil {
+				return fmt.Errorf("failed to unmarshal IDP extension: %w", err)
+			}
+			for _, dp := range idp.DistributionPoint.FullName {
+				if string(dp.Bytes) == distributionPoint {
+					logger.Debugf("distribution point %v matches CRL IDP extension", distributionPoint)
+					return nil
+				}
+			}
+			return fmt.Errorf("distribution point %v not found in CRL IDP extension", distributionPoint)
+		}
+	}
+	return nil
+}
+
+func (cv *crlValidator) getOrCreateMutex(crlURL string) *sync.Mutex {
+	cv.inMemoryCacheMutex.Lock()
+	mu, ok := cv.crlURLMutexes[crlURL]
+	if !ok {
+		mu = &sync.Mutex{}
+		cv.crlURLMutexes[crlURL] = mu
+	}
+	cv.inMemoryCacheMutex.Unlock()
+	return mu
+}
+
+func isShortLivedCertificate(cert *x509.Certificate) bool {
+	// https://cabforum.org/working-groups/server/baseline-requirements/requirements/
+	// See Short-lived Subscriber Certificate section
+	if cert.NotBefore.Before(time.Date(2024, time.March, 15, 0, 0, 0, 0, time.UTC)) {
+		// Certificates issued before March 15, 2024 are not considered short-lived
+		return false
+	}
+	maximumValidityPeriod := 7 * 24 * time.Hour
+	if cert.NotBefore.Before(time.Date(2026, time.March, 15, 0, 0, 0, 0, time.UTC)) {
+		maximumValidityPeriod = 10 * 24 * time.Hour
+	}
+	maximumValidityPeriod += time.Minute // Fix inclusion start and end time
+	certValidityPeriod := cert.NotAfter.Sub(cert.NotBefore)
+	return maximumValidityPeriod > certValidityPeriod
+}


Test patch:
diff --git a/assert_test.go b/assert_test.go
index 7ff6ba0..e75d710 100644
--- a/assert_test.go
+++ b/assert_test.go
@@ -2,10 +2,12 @@ package gosnowflake
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"reflect"
 	"regexp"
 	"runtime"
+	"slices"
 	"strings"
 	"testing"
 )
@@ -26,6 +28,14 @@ func assertNotNilF(t *testing.T, actual any, descriptions ...string) {
 	fatalOnNonEmpty(t, validateNotNil(actual, descriptions...))
 }
 
+func assertErrIsF(t *testing.T, actual, expected error, descriptions ...string) {
+	fatalOnNonEmpty(t, validateErrIs(actual, expected, descriptions...))
+}
+
+func assertErrIsE(t *testing.T, actual, expected error, descriptions ...string) {
+	errorOnNonEmpty(t, validateErrIs(actual, expected, descriptions...))
+}
+
 func assertEqualE(t *testing.T, actual any, expected any, descriptions ...string) {
 	errorOnNonEmpty(t, validateEqual(actual, expected, descriptions...))
 }
@@ -130,6 +140,14 @@ func validateNotNil(actual any, descriptions ...string) string {
 	return fmt.Sprintf("expected to be not nil but was not. %s", desc)
 }
 
+func validateErrIs(actual, expected error, descriptions ...string) string {
+	if errors.Is(actual, expected) {
+		return ""
+	}
+	desc := joinDescriptions(descriptions...)
+	return fmt.Sprintf("expected %v to be %v. %s", actual, expected, desc)
+}
+
 func validateEqual(actual any, expected any, descriptions ...string) string {
 	if expected == actual {
 		return ""
@@ -235,7 +253,7 @@ func isNil(value any) bool {
 		return true
 	}
 	val := reflect.ValueOf(value)
-	return val.Kind() == reflect.Pointer && val.IsNil()
+	return slices.Contains([]reflect.Kind{reflect.Pointer, reflect.Slice, reflect.Map, reflect.Interface}, val.Kind()) && val.IsNil()
 }
 
 func thrownFrom() string {
diff --git a/crl_test.go b/crl_test.go
new file mode 100644
index 0000000..96c60e7
--- /dev/null
+++ b/crl_test.go
@@ -0,0 +1,799 @@
+package gosnowflake
+
+import (
+	"cmp"
+	"context"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/asn1"
+	"encoding/base64"
+	"fmt"
+	"math/big"
+	"net/http"
+	"os"
+	"sync"
+	"testing"
+	"time"
+)
+
+const testCrlServerPort = 56894
+
+var serialNumber = int64(0) // to be incremented
+
+type cacheValidityTimeType time.Duration
+
+type allowCertificatesWithoutCrlURLType bool
+
+type inMemoryCacheDisabledType bool
+type onDiskCacheDisabledType bool
+
+type notAfterType time.Time
+
+type revokedCert *x509.Certificate
+
+type thisUpdateType time.Time
+type nextUpdateType time.Time
+
+func newTestCrlValidator(t *testing.T, checkMode CertRevocationCheckMode, args ...any) *crlValidator {
+	httpClient := &http.Client{}
+	cacheValidityTime := 5 * time.Minute
+	allowCertificatesWithoutCrlURL := false
+	inMemoryCacheDisabled := false
+	onDiskCacheDisabled := false
+	for _, arg := range args {
+		switch v := arg.(type) {
+		case *http.Client:
+			httpClient = v
+		case cacheValidityTimeType:
+			cacheValidityTime = time.Duration(v)
+		case allowCertificatesWithoutCrlURLType:
+			allowCertificatesWithoutCrlURL = bool(v)
+		case inMemoryCacheDisabledType:
+			inMemoryCacheDisabled = bool(v)
+		case onDiskCacheDisabledType:
+			onDiskCacheDisabled = bool(v)
+		default:
+			t.Fatalf("unexpected argument type %T", v)
+		}
+	}
+	cacheDir := t.TempDir()
+	return newCrlValidator(checkMode, allowCertificatesWithoutCrlURL, cacheValidityTime, inMemoryCacheDisabled, onDiskCacheDisabled, cacheDir, httpClient)
+}
+
+func TestCrlCheckModeDisabled_NoHttpCall(t *testing.T) {
+	caKey, caCert := createCa(t, nil, nil, "root CA", "/rootCrl")
+	_, leafCert := createLeafCert(t, caCert, caKey, "/rootCrl")
+	crt := &countingRoundTripper{}
+	cv := newTestCrlValidator(t, CertRevocationCheckDisabled, &http.Client{Transport: crt})
+	err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+	assertNilE(t, err)
+	assertEqualE(t, crt.totalRequests(), 0, "no HTTP request should be made when check mode is disabled")
+}
+
+func TestCrlModes(t *testing.T) {
+	for _, checkMode := range []CertRevocationCheckMode{CertRevocationCheckEnabled, CertRevocationCheckAdvisory} {
+		t.Run(fmt.Sprintf("checkMode=%v", checkMode), func(t *testing.T) {
+			t.Run("ShortLivedCertDoesNotNeedCRL", func(t *testing.T) {
+				caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+				_, leafCert := createLeafCert(t, caCert, caPrivateKey, "", notAfterType(time.Now().Add(4*24*time.Hour)))
+
+				cv := newTestCrlValidator(t, checkMode, allowCertificatesWithoutCrlURLType(false))
+				err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+				assertNilE(t, err)
+			})
+
+			t.Run("LeafCertNotRevoked", func(t *testing.T) {
+				caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+				_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+				crl := createCrl(t, caCert, caPrivateKey)
+
+				server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+				defer closeServer(t, server)
+
+				cv := newTestCrlValidator(t, checkMode)
+				err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+				assertNilE(t, err)
+			})
+
+			t.Run("LeafCertRevoked", func(t *testing.T) {
+				caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+				_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+				crl := createCrl(t, caCert, caPrivateKey, revokedCert(leafCert))
+
+				server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+				defer closeServer(t, server)
+
+				cv := newTestCrlValidator(t, checkMode)
+				err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+				assertNotNilF(t, err)
+				assertEqualE(t, err.Error(), "every verified certificate chain contained revoked certificates")
+			})
+
+			t.Run("TestLeafNotRevokedAndRootDoesNotProvideCrl", func(t *testing.T) {
+				rootCaPrivateKey, rootCaCert := createCa(t, nil, nil, "root CA", "")
+				intermediateCaKey, intermediateCaCert := createCa(t, rootCaCert, rootCaPrivateKey, "intermediate CA", "")
+				_, leafCert := createLeafCert(t, intermediateCaCert, intermediateCaKey, "/intermediateCrl")
+				intermediateCrl := createCrl(t, intermediateCaCert, intermediateCaKey)
+
+				server := createCrlServer(t, newCrlEndpointDef("/intermediateCrl", intermediateCrl))
+				defer closeServer(t, server)
+
+				cv := newTestCrlValidator(t, checkMode)
+				err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, intermediateCaCert, rootCaCert}})
+				if checkMode == CertRevocationCheckEnabled {
+					assertEqualE(t, err.Error(), "certificate revocation check failed")
+				} else {
+					assertNilE(t, err)
+				}
+			})
+
+			t.Run("IntermediateRevokedAndIntermediateDoesNotProvideCrl", func(t *testing.T) {
+				rootCaPrivateKey, rootCaCert := createCa(t, nil, nil, "root CA", "")
+				intermediateCaKey, intermediateCaCert := createCa(t, rootCaCert, rootCaPrivateKey, "intermediate CA", "/rootCrl")
+				_, leafCert := createLeafCert(t, intermediateCaCert, intermediateCaKey, "")
+				rootCrl := createCrl(t, rootCaCert, rootCaPrivateKey, revokedCert(intermediateCaCert))
+
+				server := createCrlServer(t, newCrlEndpointDef("/rootCrl", rootCrl))
+				defer closeServer(t, server)
+
+				cv := newTestCrlValidator(t, checkMode)
+				err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, intermediateCaCert, rootCaCert}})
+				if checkMode == CertRevocationCheckEnabled {
+					assertEqualE(t, err.Error(), "every verified certificate chain contained revoked certificates")
+				} else {
+					assertEqualE(t, err.Error(), "every verified certificate chain contained revoked certificates")
+				}
+			})
+
+			t.Run("CrlSignatureInvalid", func(t *testing.T) {
+				caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "/rootCrl")
+				otherCaPrivateKey, _ := createCa(t, nil, nil, "other CA", "")
+				_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+				crl := createCrl(t, caCert, otherCaPrivateKey) // signed with wrong key
+
+				server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+				defer closeServer(t, server)
+
+				cv := newTestCrlValidator(t, checkMode)
+				err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+				if checkMode == CertRevocationCheckEnabled {
+					assertStringContainsE(t, err.Error(), "certificate revocation check failed")
+				} else {
+					assertNilE(t, err)
+				}
+			})
+
+			t.Run("CrlIssuerMismatch", func(t *testing.T) {
+				caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "/rootCrl")
+				otherKey, otherCert := createCa(t, nil, nil, "other CA", "")
+				_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+				crl := createCrl(t, otherCert, otherKey) // issued by other CA
+
+				server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+				defer closeServer(t, server)
+
+				cv := newTestCrlValidator(t, checkMode)
+				err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+				if checkMode == CertRevocationCheckEnabled {
+					assertStringContainsE(t, err.Error(), "certificate revocation check failed")
+				} else {
+					assertNilE(t, err)
+				}
+			})
+
+			t.Run("CertWithNoCrlDistributionPoints", func(t *testing.T) {
+				caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+				_, leafCert := createLeafCert(t, caCert, caPrivateKey, "")
+
+				cv := newTestCrlValidator(t, checkMode)
+				err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+				if checkMode == CertRevocationCheckEnabled {
+					assertEqualE(t, err.Error(), "certificate revocation check failed")
+				} else {
+					assertNilE(t, err)
+				}
+			})
+
+			t.Run("CertWithNoCrlDistributionPointsAllowed", func(t *testing.T) {
+				caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+				_, leafCert := createLeafCert(t, caCert, caPrivateKey, "")
+
+				cv := newTestCrlValidator(t, checkMode, allowCertificatesWithoutCrlURLType(true))
+				err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+				assertNilE(t, err)
+			})
+
+			t.Run("DownloadCrlFailsOnUnparsableCrl", func(t *testing.T) {
+				caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+				_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+
+				server := createCrlServer(t)
+				defer closeServer(t, server)
+
+				cv := newTestCrlValidator(t, checkMode, &http.Client{
+					Transport: &malformedCrlRoundTripper{},
+				})
+				err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+				if checkMode == CertRevocationCheckEnabled {
+					assertEqualE(t, err.Error(), "certificate revocation check failed")
+				} else {
+					assertNilE(t, err)
+				}
+			})
+
+			t.Run("DownloadCrlFailsOn404", func(t *testing.T) {
+				caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+				_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+
+				server := createCrlServer(t)
+				defer closeServer(t, server)
+
+				cv := newTestCrlValidator(t, checkMode)
+				err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+				if checkMode == CertRevocationCheckEnabled {
+					assertEqualE(t, err.Error(), "certificate revocation check failed")
+				} else {
+					assertNilE(t, err)
+				}
+			})
+
+			t.Run("VerifyAgainstIdpExtensionWithDistributionPointMatch", func(t *testing.T) {
+				caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "/rootCrl")
+				_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+
+				idpValue, err := asn1.Marshal(issuingDistributionPoint{
+					DistributionPoint: distributionPointName{
+						FullName: []asn1.RawValue{
+							{Bytes: []byte(fmt.Sprintf("http://localhost:%v/rootCrl", testCrlServerPort))},
+						},
+					},
+				})
+				assertNilF(t, err)
+				idpExtension := &pkix.Extension{
+					Id:    idpOID,
+					Value: idpValue,
+				}
+
+				crl := createCrl(t, caCert, caPrivateKey, idpExtension)
+
+				server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+				defer closeServer(t, server)
+
+				cv := newTestCrlValidator(t, checkMode)
+				err = cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+				assertNilE(t, err)
+			})
+
+			t.Run("TestVerifyAgainstIdpExtensionWithDistributionPointMismatch", func(t *testing.T) {
+				caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "/rootCrl")
+				_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+
+				idpValue, err := asn1.Marshal(issuingDistributionPoint{
+					DistributionPoint: distributionPointName{
+						FullName: []asn1.RawValue{
+							{Bytes: []byte(fmt.Sprintf("http://localhost:%v/otherCrl", testCrlServerPort))},
+						},
+					},
+				})
+				assertNilF(t, err)
+				idpExtension := &pkix.Extension{
+					Id:    idpOID,
+					Value: idpValue,
+				}
+
+				crl := createCrl(t, caCert, caPrivateKey, idpExtension)
+
+				server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+				defer closeServer(t, server)
+
+				cv := newTestCrlValidator(t, checkMode)
+				err = cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+				if checkMode == CertRevocationCheckEnabled {
+					assertNotNilF(t, err)
+					assertEqualE(t, err.Error(), "certificate revocation check failed")
+				} else {
+					assertNilE(t, err)
+				}
+			})
+
+			t.Run("AnyValidChainCausesSuccess", func(t *testing.T) {
+				caKey, caCert := createCa(t, nil, nil, "root CA", "/rootCrl")
+				_, revokedLeaf := createLeafCert(t, caCert, caKey, "/rootCrl")
+				_, validLeaf := createLeafCert(t, caCert, caKey, "/rootCrl")
+
+				// CRL revokes only the first leaf
+				crl := createCrl(t, caCert, caKey, revokedCert(revokedLeaf))
+				server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+				defer closeServer(t, server)
+
+				cv := newTestCrlValidator(t, checkMode)
+				// First chain: revoked, second chain: valid
+				err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{
+					{revokedLeaf, caCert},
+					{validLeaf, caCert},
+				})
+				assertNilE(t, err)
+			})
+
+			t.Run("OneChainIsRevokedAndOtherIsError", func(t *testing.T) {
+				caKey, caCert := createCa(t, nil, nil, "root CA", "/rootCrl")
+				_, revokedLeaf := createLeafCert(t, caCert, caKey, "/rootCrl")
+				_, errorLeaf := createLeafCert(t, caCert, caKey, "/missingCrl")
+
+				// CRL revokes only the first leaf
+				crl := createCrl(t, caCert, caKey, revokedCert(revokedLeaf))
+				server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+				defer closeServer(t, server)
+
+				cv := newTestCrlValidator(t, checkMode)
+				// First chain: revoked, second chain: valid
+				err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{
+					{revokedLeaf, caCert},
+					{errorLeaf, caCert},
+				})
+				if checkMode == CertRevocationCheckEnabled {
+					assertNotNilF(t, err)
+					assertEqualE(t, err.Error(), "certificate revocation check failed")
+				} else {
+					assertNilE(t, err)
+				}
+			})
+
+			t.Run("CacheTests", func(t *testing.T) {
+				t.Run("should use in-memory cache", func(t *testing.T) {
+					caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+					_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+					crl := createCrl(t, caCert, caPrivateKey)
+
+					server := createCrlServer(t)
+					defer closeServer(t, server)
+
+					crt := newCountingRoundTripper(snowflakeNoOcspTransport)
+					cv := newTestCrlValidator(t, checkMode, cacheValidityTimeType(10*time.Minute), &http.Client{
+						Transport: crt,
+					})
+					downloadTime := time.Now().Add(-1 * time.Minute)
+					cv.inMemoryCache[fullCrlURL("/rootCrl")] = &crlInMemoryCacheValueType{
+						crl:          crl,
+						downloadTime: &downloadTime,
+					}
+					err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+					assertNilE(t, err)
+					assertEqualE(t, crt.totalRequests(), 0)
+					_, err = os.Open(cv.crlURLToPath("/rootCrl"))
+					assertErrIsE(t, err, os.ErrNotExist, "CRL file should not be created in the cache directory")
+				})
+
+				t.Run("should promote on-disk cache to memory and not modify on-disk entry", func(t *testing.T) {
+					caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+					_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+					crl := createCrl(t, caCert, caPrivateKey)
+
+					server := createCrlServer(t)
+					defer closeServer(t, server)
+
+					crt := newCountingRoundTripper(snowflakeNoOcspTransport)
+					cv := newTestCrlValidator(t, checkMode, cacheValidityTimeType(10*time.Minute), &http.Client{
+						Transport: crt,
+					})
+
+					assertNilF(t, os.WriteFile(cv.crlURLToPath(fullCrlURL("/rootCrl")), crl.Raw, 0600)) // simulate a cached CRL
+					statBefore, err := os.Stat(cv.crlURLToPath(fullCrlURL("/rootCrl")))
+					assertNilF(t, err)
+
+					err = cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+					assertNilE(t, err)
+					assertEqualE(t, crt.totalRequests(), 0)
+					statAfter, err := os.Stat(cv.crlURLToPath(fullCrlURL("/rootCrl")))
+					assertNilF(t, err)
+					assertEqualE(t, statBefore.ModTime().Equal(statAfter.ModTime()), true, "CRL file should not be modified in the cache directory")
+				})
+
+				t.Run("should redownload when nextUpdate is reached", func(t *testing.T) {
+					caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+					_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+					oldCrl := createCrl(t, caCert, caPrivateKey, thisUpdateType(time.Now().Add(-2*time.Minute)), nextUpdateType(time.Now().Add(-1*time.Minute)))
+					newCrl := createCrl(t, caCert, caPrivateKey, thisUpdateType(time.Now()), nextUpdateType(time.Now().Add(time.Hour)))
+
+					server := createCrlServer(t, newCrlEndpointDef("/rootCrl", newCrl))
+					defer closeServer(t, server)
+
+					crt := newCountingRoundTripper(snowflakeNoOcspTransport)
+					cv := newTestCrlValidator(t, checkMode, cacheValidityTimeType(10*time.Minute), &http.Client{
+						Transport: crt,
+					})
+
+					previousDownloadTime := time.Now().Add(-1 * time.Minute)
+					cv.inMemoryCache[fullCrlURL("/rootCrl")] = &crlInMemoryCacheValueType{
+						crl:          oldCrl,
+						downloadTime: &previousDownloadTime,
+					}
+
+					err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+					assertNilE(t, err)
+
+					assertEqualE(t, crt.totalRequests(), 1)
+					fd, err := os.Open(cv.crlURLToPath(fullCrlURL("/rootCrl")))
+					assertNilE(t, err, "CRL file should be created in the cache directory")
+					defer fd.Close()
+					assertTrueE(t, cv.inMemoryCache[fullCrlURL("/rootCrl")].downloadTime.After(previousDownloadTime))
+					assertTrueE(t, cv.inMemoryCache[fullCrlURL("/rootCrl")].crl.NextUpdate.Equal(newCrl.NextUpdate))
+				})
+
+				t.Run("should redownload when evicted in cache", func(t *testing.T) {
+					caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+					_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+					oldCrl := createCrl(t, caCert, caPrivateKey, thisUpdateType(time.Now().Add(-2*time.Hour)), nextUpdateType(time.Now().Add(time.Hour)))
+					newCrl := createCrl(t, caCert, caPrivateKey, thisUpdateType(time.Now()), nextUpdateType(time.Now().Add(4*time.Hour)))
+
+					server := createCrlServer(t, newCrlEndpointDef("/rootCrl", newCrl))
+					defer closeServer(t, server)
+
+					crt := newCountingRoundTripper(snowflakeNoOcspTransport)
+					cv := newTestCrlValidator(t, checkMode, cacheValidityTimeType(10*time.Minute), &http.Client{
+						Transport: crt,
+					})
+
+					previousDownloadTime := time.Now().Add(-1 * time.Hour)
+					cv.inMemoryCache[fullCrlURL("/rootCrl")] = &crlInMemoryCacheValueType{
+						crl:          oldCrl,
+						downloadTime: &previousDownloadTime,
+					}
+
+					err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+					assertNilE(t, err)
+
+					assertEqualE(t, crt.totalRequests(), 1)
+					fd, err := os.Open(cv.crlURLToPath(fullCrlURL("/rootCrl")))
+					assertNilE(t, err, "CRL file should be created in the cache directory")
+					defer fd.Close()
+					assertTrueE(t, cv.inMemoryCache[fullCrlURL("/rootCrl")].downloadTime.After(previousDownloadTime))
+					assertTrueE(t, cv.inMemoryCache[fullCrlURL("/rootCrl")].crl.NextUpdate.Equal(newCrl.NextUpdate))
+				})
+
+				t.Run("should not save to on-disk cache when disabled", func(t *testing.T) {
+					caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+					_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+					crl := createCrl(t, caCert, caPrivateKey)
+
+					server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+					defer closeServer(t, server)
+
+					cv := newTestCrlValidator(t, checkMode, onDiskCacheDisabledType(true))
+					err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+					assertNilE(t, err)
+					_, err = os.Open(cv.crlURLToPath(fullCrlURL("/rootCrl")))
+					assertErrIsE(t, err, os.ErrNotExist, "CRL file should not be created in the cache directory when on-disk cache is disabled")
+					assertNotNilE(t, cv.inMemoryCache[fullCrlURL("/rootCrl")]) // in-memory cache should still be used
+				})
+
+				t.Run("should not read from on-disk cache when disabled", func(t *testing.T) {
+					caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+					_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+					oldCrl := createCrl(t, caCert, caPrivateKey, nextUpdateType(time.Now()))
+					newCrl := createCrl(t, caCert, caPrivateKey)
+
+					server := createCrlServer(t, newCrlEndpointDef("/rootCrl", newCrl))
+					defer closeServer(t, server)
+
+					crt := newCountingRoundTripper(snowflakeNoOcspTransport)
+					cv := newTestCrlValidator(t, checkMode, onDiskCacheDisabledType(true), &http.Client{
+						Transport: crt,
+					})
+					assertNilF(t, os.WriteFile(cv.crlURLToPath(fullCrlURL("/rootCrl")), oldCrl.Raw, 0600)) // simulate a cached CRL
+					statBefore, err := os.Stat(cv.crlURLToPath(fullCrlURL("/rootCrl")))
+					assertNilF(t, err)
+					err = cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+					assertNilE(t, err)
+					assertEqualE(t, crt.totalRequests(), 1, "CRL should be downloaded from the server")
+					assertNotNilE(t, cv.inMemoryCache[fullCrlURL("/rootCrl")]) // in-memory cache should still be used
+					statAfter, err := os.Stat(cv.crlURLToPath(fullCrlURL("/rootCrl")))
+					assertNilF(t, err)
+					assertTrueE(t, statBefore.ModTime().Equal(statAfter.ModTime()), "CRL file should be modified in the cache directory")
+				})
+
+				t.Run("should not use in-memory cache when disabled", func(t *testing.T) {
+					caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+					_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+					crl := createCrl(t, caCert, caPrivateKey)
+
+					server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+					defer closeServer(t, server)
+
+					cv := newTestCrlValidator(t, checkMode, inMemoryCacheDisabledType(true))
+					err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+					assertNilE(t, err)
+					assertNilE(t, cv.inMemoryCache, "in-memory cache should not be used when disabled")
+					fd, err := os.Open(cv.crlURLToPath(fullCrlURL("/rootCrl")))
+					assertNilE(t, err) // on-disk cache should still be used
+					defer fd.Close()
+				})
+
+				t.Run("should not use in-memory cache when disabled", func(t *testing.T) {
+					caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+					_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+					crl := createCrl(t, caCert, caPrivateKey)
+
+					server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+					defer closeServer(t, server)
+
+					cv := newTestCrlValidator(t, checkMode, inMemoryCacheDisabledType(true), onDiskCacheDisabledType(true))
+					err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+					assertNilE(t, err)
+					assertNilE(t, cv.inMemoryCache[fullCrlURL("/rootCrl")], "in-memory cache should not be used when disabled")
+					_, err = os.Open(cv.crlURLToPath(fullCrlURL("/rootCrl")))
+					assertErrIsE(t, err, os.ErrNotExist, "CRL file should not be created in the cache directory when on-disk cache is disabled")
+				})
+			})
+		})
+	}
+}
+
+func TestRealCrlWithIdpExtension(t *testing.T) {
+	crlBytes, err := base64.StdEncoding.DecodeString(`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`)
+	assertNilF(t, err)
+	crl, err := x509.ParseRevocationList(crlBytes)
+	assertNilF(t, err)
+	cv := newTestCrlValidator(t, CertRevocationCheckEnabled)
+	err = cv.verifyAgainstIdpExtension(crl, "http://c.pki.goog/we2/yK5nPhtHKQs.crl")
+	assertNilE(t, err)
+	err = cv.verifyAgainstIdpExtension(crl, "http://c.pki.goog/we2/other.crl")
+	assertNotNilF(t, err)
+	assertStringContainsE(t, err.Error(), "distribution point http://c.pki.goog/we2/other.crl not found in CRL IDP extension")
+}
+
+func TestParallelRequestToTheSameCrl(t *testing.T) {
+	caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "/rootCrl")
+	_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+	crl := createCrl(t, caCert, caPrivateKey)
+
+	server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+	defer closeServer(t, server)
+
+	brt := newBlockingRoundTripper(snowflakeNoOcspTransport, 100*time.Millisecond)
+	crt := newCountingRoundTripper(brt)
+	cv := newTestCrlValidator(t, CertRevocationCheckEnabled, &http.Client{
+		Transport: crt,
+	})
+
+	var wg sync.WaitGroup
+	for i := 0; i < 10; i++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+			assertNilE(t, err)
+		}()
+	}
+	wg.Wait()
+
+	assertEqualE(t, crt.totalRequests(), 1)
+}
+
+func TestIsShortLivedCertificate(t *testing.T) {
+	tests := []struct {
+		name     string
+		cert     *x509.Certificate
+		expected bool
+	}{
+		{
+			name: "Issued before March 15, 2024 (not short-lived)",
+			cert: &x509.Certificate{
+				NotBefore: time.Date(2024, time.March, 1, 0, 0, 0, 0, time.UTC),
+				NotAfter:  time.Date(2024, time.March, 10, 0, 0, 0, 0, time.UTC),
+			},
+			expected: false,
+		},
+		{
+			name: "Issued after March 15, 2024, validity less than 10, but more than 7 days (short-lived)",
+			cert: &x509.Certificate{
+				NotBefore: time.Date(2024, time.March, 16, 0, 0, 0, 0, time.UTC),
+				NotAfter:  time.Date(2024, time.March, 24, 0, 0, 0, 0, time.UTC),
+			},
+			expected: true,
+		},
+		{
+			name: "Issued after March 15, 2024, validity less than 7 days (short-lived)",
+			cert: &x509.Certificate{
+				NotBefore: time.Date(2024, time.March, 16, 0, 0, 0, 0, time.UTC),
+				NotAfter:  time.Date(2024, time.March, 22, 0, 0, 0, 0, time.UTC),
+			},
+			expected: true,
+		},
+		{
+			name: "Issued after March 15, 2024, validity exactly 10 days (short-lived)",
+			cert: &x509.Certificate{
+				NotBefore: time.Date(2024, time.March, 16, 0, 0, 0, 0, time.UTC),
+				NotAfter:  time.Date(2024, time.March, 26, 0, 0, 0, 0, time.UTC),
+			},
+			expected: true,
+		},
+		{
+			name: "Issued after March 15, 2024, validity more than 10 days (not short-lived)",
+			cert: &x509.Certificate{
+				NotBefore: time.Date(2024, time.March, 16, 0, 0, 0, 0, time.UTC),
+				NotAfter:  time.Date(2024, time.March, 27, 0, 0, 0, 0, time.UTC),
+			},
+			expected: false,
+		},
+		{
+			name: "Issued after March 15, 2026, validity less than 7 days (short-lived)",
+			cert: &x509.Certificate{
+				NotBefore: time.Date(2026, time.March, 16, 0, 0, 0, 0, time.UTC),
+				NotAfter:  time.Date(2026, time.March, 20, 0, 0, 0, 0, time.UTC),
+			},
+			expected: true,
+		},
+		{
+			name: "Issued after March 15, 2026, validity exactly 7 days (short-lived)",
+			cert: &x509.Certificate{
+				NotBefore: time.Date(2026, time.March, 16, 0, 0, 0, 0, time.UTC),
+				NotAfter:  time.Date(2026, time.March, 23, 0, 0, 0, 0, time.UTC),
+			},
+			expected: true,
+		},
+		{
+			name: "Issued after March 15, 2026, validity more than 7 days (not short-lived)",
+			cert: &x509.Certificate{
+				NotBefore: time.Date(2026, time.March, 16, 0, 0, 0, 0, time.UTC),
+				NotAfter:  time.Date(2026, time.March, 24, 0, 0, 0, 0, time.UTC),
+			},
+			expected: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assertEqualE(t, isShortLivedCertificate(tt.cert), tt.expected)
+		})
+	}
+}
+
+type malformedCrlRoundTripper struct {
+}
+
+func (m *malformedCrlRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	response := http.Response{
+		StatusCode: http.StatusOK,
+	}
+	response.Body = http.NoBody
+	return &response, nil
+}
+
+func createCa(t *testing.T, issuerCert *x509.Certificate, issuerPrivateKey *rsa.PrivateKey, cn string, crlEndpoint string) (*rsa.PrivateKey, *x509.Certificate) {
+	caTemplate := &x509.Certificate{
+		SerialNumber: big.NewInt(1),
+		Subject: pkix.Name{
+			Organization:       []string{"Snowflake"},
+			OrganizationalUnit: []string{"Drivers"},
+			Locality:           []string{"Warsaw"},
+			CommonName:         cn,
+		},
+		NotBefore:             time.Now(),
+		NotAfter:              time.Now().AddDate(10, 0, 0),
+		IsCA:                  true,
+		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+		BasicConstraintsValid: true,
+	}
+	return createCert(t, caTemplate, issuerCert, issuerPrivateKey, crlEndpoint)
+}
+
+func createLeafCert(t *testing.T, issuerCert *x509.Certificate, issuerPrivateKey *rsa.PrivateKey, crlEndpoint string, params ...any) (*rsa.PrivateKey, *x509.Certificate) {
+	notAfter := time.Now().AddDate(1, 0, 0)
+	for _, param := range params {
+		switch v := param.(type) {
+		case notAfterType:
+			notAfter = time.Time(v)
+		}
+	}
+	serialNumber++
+	certTemplate := &x509.Certificate{
+		SerialNumber: big.NewInt(serialNumber),
+		Subject: pkix.Name{
+			Organization:       []string{"Snowflake"},
+			OrganizationalUnit: []string{"Drivers"},
+			Locality:           []string{"Warsaw"},
+			CommonName:         "localhost",
+		},
+		NotBefore: time.Now(),
+		NotAfter:  notAfter,
+		IsCA:      false,
+	}
+	return createCert(t, certTemplate, issuerCert, issuerPrivateKey, crlEndpoint)
+}
+
+func createCert(t *testing.T, template, issuerCert *x509.Certificate, issuerPrivateKey *rsa.PrivateKey, crlEndpoint string) (*rsa.PrivateKey, *x509.Certificate) {
+	distributionPoints := []string{}
+	if crlEndpoint != "" {
+		distributionPoints = append(distributionPoints, fmt.Sprintf("http://localhost:%v%v", testCrlServerPort, crlEndpoint))
+		template.CRLDistributionPoints = distributionPoints
+	}
+	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	assertNilF(t, err)
+	signerPrivateKey := cmp.Or(issuerPrivateKey, privateKey)
+	issuerCertOrSelfSigned := cmp.Or(issuerCert, template)
+	certBytes, err := x509.CreateCertificate(rand.Reader, template, issuerCertOrSelfSigned, &privateKey.PublicKey, signerPrivateKey)
+	assertNilF(t, err)
+	cert, err := x509.ParseCertificate(certBytes)
+	assertNilF(t, err)
+	return privateKey, cert
+}
+
+func createCrl(t *testing.T, issuerCert *x509.Certificate, issuerPrivateKey *rsa.PrivateKey, args ...any) *x509.RevocationList {
+	var revokedCertEntries []x509.RevocationListEntry
+	var extensions []pkix.Extension
+	thisUpdate := time.Now().Add(-time.Hour)
+	nextUpdate := time.Now().Add(time.Hour)
+	for _, arg := range args {
+		switch v := arg.(type) {
+		case revokedCert:
+			revokedCertEntries = append(revokedCertEntries, x509.RevocationListEntry{
+				SerialNumber:   v.SerialNumber,
+				RevocationTime: time.Now().Add(-time.Hour * 24),
+			})
+		case *pkix.Extension:
+			extensions = append(extensions, *v)
+		case thisUpdateType:
+			thisUpdate = time.Time(v)
+		case nextUpdateType:
+			nextUpdate = time.Time(v)
+		default:
+			t.Fatalf("unexpected argument type: %T", arg)
+		}
+	}
+	crlTemplate := &x509.RevocationList{
+		Number:                    big.NewInt(1),
+		RevokedCertificateEntries: revokedCertEntries,
+		ExtraExtensions:           extensions,
+		ThisUpdate:                thisUpdate,
+		NextUpdate:                nextUpdate,
+	}
+	crlBytes, err := x509.CreateRevocationList(rand.Reader, crlTemplate, issuerCert, issuerPrivateKey)
+	assertNilF(t, err)
+	crl, err := x509.ParseRevocationList(crlBytes)
+	assertNilF(t, err)
+	return crl
+}
+
+type crlEndpointDef struct {
+	endpoint string
+	crl      *x509.RevocationList
+}
+
+func newCrlEndpointDef(endpoint string, crl *x509.RevocationList) *crlEndpointDef {
+	return &crlEndpointDef{
+		endpoint: endpoint,
+		crl:      crl,
+	}
+}
+
+func createCrlServer(t *testing.T, endpointDefs ...*crlEndpointDef) *http.Server {
+	mux := http.NewServeMux()
+	for _, endpointDef := range endpointDefs {
+		mux.HandleFunc(endpointDef.endpoint, func(responseWriter http.ResponseWriter, request *http.Request) {
+			responseWriter.WriteHeader(http.StatusOK)
+			_, err := responseWriter.Write(endpointDef.crl.Raw)
+			assertNilF(t, err)
+		})
+	}
+	server := &http.Server{
+		Addr:    fmt.Sprintf(":%v", testCrlServerPort),
+		Handler: mux,
+	}
+	go func() {
+		err := server.ListenAndServe()
+		assertErrIsF(t, err, http.ErrServerClosed)
+	}()
+	return server
+}
+
+func fullCrlURL(endpoint string) string {
+	return fmt.Sprintf("http://localhost:%v%v", testCrlServerPort, endpoint)
+}
+
+func closeServer(t *testing.T, server *http.Server) {
+	err := server.Shutdown(context.Background())
+	assertNilF(t, err)
+}
diff --git a/driver_test.go b/driver_test.go
index ddfb1eb..6b223a8 100644
--- a/driver_test.go
+++ b/driver_test.go
@@ -2166,3 +2166,20 @@ func (crt *countingRoundTripper) totalRequests() int {
 	}
 	return total
 }
+
+type blockingRoundTripper struct {
+	delegate  http.RoundTripper
+	blockTime time.Duration
+}
+
+func newBlockingRoundTripper(delegate http.RoundTripper, blockTime time.Duration) *blockingRoundTripper {
+	return &blockingRoundTripper{
+		delegate:  delegate,
+		blockTime: blockTime,
+	}
+}
+
+func (brt *blockingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	time.Sleep(brt.blockTime)
+	return brt.delegate.RoundTrip(req)
+}