Repository: snowflakedb/gosnowflake
Pull request #1429 (2025-05-29 14:33:42)

SNOW-2117152 Implement CRL algorithm
### Description

SNOW-XXX Please explain the changes you made here.

### Checklist
- [ ] Created tests which fail without the change (if possible)
- [ ] Extended the README / documentation, if necessary


sfc-gh-snowflakedb-snyk-sa: ![](https://res.cloudinary.com/snyk/image/upload/r-d/scm-platform/snyk-pull-requests/pr-banner-default.svg)

### :tada: **Snyk checks have passed. No issues have been found so far.**

:white_check_mark: **security/snyk** check is complete. No issues have been found. [(View Details)](https://app.snyk.io/org/snowflakedb-sca-scanning-public-repo/pr-checks/11c1e7b0-fc6e-4479-87ee-2d9146573406?refs=c393449dfd36a9e55f32db9f0d3ca7777d035046&source=prChecksComment)

:white_check_mark: **license/snyk** check is complete. No issues have been found. [(View Details)](https://app.snyk.io/org/snowflakedb-sca-scanning-public-repo/pr-checks/11c1e7b0-fc6e-4479-87ee-2d9146573406/license?refs=c393449dfd36a9e55f32db9f0d3ca7777d035046&source=prChecksComment)




Patch:
diff --git a/crl.go b/crl.go
new file mode 100644
index 0000000..7ce8369
--- /dev/null
+++ b/crl.go
@@ -0,0 +1,131 @@
+package gosnowflake
+
+import (
+	"bytes"
+	"crypto/x509"
+	"encoding/asn1"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+)
+
+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 {
+	httpClient http.Client
+}
+
+func newCrlValidator(httpClient http.Client) *crlValidator {
+	return &crlValidator{
+		httpClient: httpClient,
+	}
+}
+
+// function to be set as custom TLS verification in the http client
+func (cv *crlValidator) verifyPeerCertificates(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+	var allErrors = errors.New("")
+	for _, verifiedChain := range verifiedChains {
+		isValidChain := true
+		for certPos, cert := range verifiedChain {
+			logger.Debugf("started CRL validation for %v", cert.Subject)
+			isSelfSigned := bytes.Equal(cert.RawSubject, cert.RawIssuer)
+			if isSelfSigned {
+				logger.Debugf("certificate %v is self signed, assuming root CA", cert.Subject)
+				break
+			}
+			if certPos == len(verifiedChain)-1 {
+				// Is it correct to assume that the last certificate in the chain is the self signed?
+				logger.Debugf("certificate %v is the last in the chain, assuming root CA", cert.Subject)
+				break
+			}
+			issuerCert := verifiedChain[certPos+1]
+			if err := cv.verifyCertificate(cert, issuerCert); err != nil {
+				logger.Debugf("CRL validation failed for %v: %v", cert.Subject, err)
+				allErrors = fmt.Errorf("%w%w", allErrors, err)
+				isValidChain = false
+				break
+			}
+			logger.Debugf("finished CRL validation for %v", cert.Subject)
+		}
+		if isValidChain {
+			return nil
+		}
+	}
+	return fmt.Errorf("no valid certificate chain found after CRL validation. errors: %w", allErrors)
+}
+
+func (cv *crlValidator) verifyCertificate(cert *x509.Certificate, issuerCert *x509.Certificate) error {
+	for _, distributionPoint := range cert.CRLDistributionPoints {
+		logger.Debugf("validating %v against %v", cert.Subject, distributionPoint)
+		crlBytes, err := cv.downloadCrl(distributionPoint)
+		if err != nil {
+			return fmt.Errorf("failed to download CRL from %v: %w", distributionPoint, err)
+		}
+		crl, err := x509.ParseRevocationList(crlBytes)
+		if err != nil {
+			return err
+		}
+		logger.Debugf("parsed CRL for %v, number of revoked certificates: %v", distributionPoint, len(crl.RevokedCertificateEntries))
+		if err = crl.CheckSignatureFrom(issuerCert); err != nil {
+			return fmt.Errorf("signature verification error for CRL %v: %w", distributionPoint, err)
+		}
+		if !bytes.Equal(crl.RawIssuer, cert.RawIssuer) {
+			logger.Debugf("failed to verify CRL issuer, got: %v, expected: %v", crl.Issuer, cert.Issuer)
+			return fmt.Errorf("failed to verify CRL issuer")
+		}
+		if err = cv.verifyAgainstIdpExtension(crl, distributionPoint); err != nil {
+			return err
+		}
+		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 fmt.Errorf("certificate for %v has been revoked", cert.Subject)
+			}
+		}
+	}
+	return nil
+}
+
+func (cv *crlValidator) downloadCrl(url string) ([]byte, error) {
+	logger.Debugf("downloading CRL from %v", url)
+	resp, err := cv.httpClient.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	crlBytes, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	logger.Debugf("downloaded %v bytes for CRL %v", len(crlBytes), url)
+	return crlBytes, err
+}
+
+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
+}


Test patch:
diff --git a/assert_test.go b/assert_test.go
index 7ff6ba0..db8c600 100644
--- a/assert_test.go
+++ b/assert_test.go
@@ -2,6 +2,7 @@ package gosnowflake
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"reflect"
 	"regexp"
@@ -26,6 +27,10 @@ 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 assertEqualE(t *testing.T, actual any, expected any, descriptions ...string) {
 	errorOnNonEmpty(t, validateEqual(actual, expected, descriptions...))
 }
@@ -130,6 +135,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 ""
diff --git a/crl_test.go b/crl_test.go
new file mode 100644
index 0000000..4bf7fec
--- /dev/null
+++ b/crl_test.go
@@ -0,0 +1,327 @@
+package gosnowflake
+
+import (
+	"cmp"
+	"context"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/asn1"
+	"encoding/base64"
+	"fmt"
+	"math/big"
+	"net/http"
+	"testing"
+	"time"
+)
+
+const testCrlServerPort = 56894
+
+var serialNumber = int64(0) // to be incremented
+
+func TestLeafCertNotRevoked(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 := newCrlValidator(http.Client{})
+	err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+	assertNilE(t, err)
+}
+
+func TestLeafCertRevoked(t *testing.T) {
+	caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+	_, leafCert := createLeafCert(t, caCert, caPrivateKey, "/rootCrl")
+	crl := createCrl(t, caCert, caPrivateKey, leafCert)
+
+	server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+	defer closeServer(t, server)
+
+	cv := newCrlValidator(http.Client{})
+	err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+	assertNotNilF(t, err)
+	assertEqualE(t, err.Error(), "no valid certificate chain found after CRL validation. errors: certificate for CN=localhost,OU=Drivers,O=Snowflake,L=Warsaw has been revoked")
+}
+
+func TestLeafNotRevokedAndRootDoesNotProvideCrl(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 := newCrlValidator(http.Client{})
+	err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, intermediateCaCert, rootCaCert}})
+	assertNilE(t, err)
+}
+
+func TestIntermediateCertRevoked(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, intermediateCaCert)
+
+	server := createCrlServer(t, newCrlEndpointDef("/rootCrl", rootCrl))
+	defer closeServer(t, server)
+
+	cv := newCrlValidator(http.Client{})
+	err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, intermediateCaCert, rootCaCert}})
+	assertEqualE(t, err.Error(), "no valid certificate chain found after CRL validation. errors: certificate for CN=intermediate CA,OU=Drivers,O=Snowflake,L=Warsaw has been revoked")
+}
+
+func TestCrlSignatureInvalid(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 := newCrlValidator(http.Client{})
+	err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+	assertStringContainsE(t, err.Error(), "signature verification error")
+}
+
+func TestCrlIssuerMismatch(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 := newCrlValidator(http.Client{})
+	err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+	assertStringContainsE(t, err.Error(), "signature verification error for CRL")
+}
+
+func TestCertWithNoCrlDistributionPoints(t *testing.T) {
+	caPrivateKey, caCert := createCa(t, nil, nil, "root CA", "")
+	_, leafCert := createLeafCert(t, caCert, caPrivateKey, "")
+
+	cv := newCrlValidator(http.Client{})
+	err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+	assertNilE(t, err)
+}
+
+func TestCrlDownloadFails(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 := newCrlValidator(http.Client{})
+	err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+	assertNotNilF(t, err)
+}
+
+func TestVerifyAgainstIdpExtensionWithDistributionPointMatch(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 := newCrlValidator(http.Client{})
+	err = cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+	assertNilE(t, err)
+}
+
+func TestVerifyAgainstIdpExtensionWithDistributionPointMismatch(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 := newCrlValidator(http.Client{})
+	err = cv.verifyPeerCertificates(nil, [][]*x509.Certificate{{leafCert, caCert}})
+	assertNotNilF(t, err)
+	assertEqualE(t, err.Error(), fmt.Sprintf("no valid certificate chain found after CRL validation. errors: distribution point http://localhost:%v/rootCrl not found in CRL IDP extension", testCrlServerPort))
+}
+
+func TestAnyValidChainCausesSuccess(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, revokedLeaf)
+	server := createCrlServer(t, newCrlEndpointDef("/rootCrl", crl))
+	defer closeServer(t, server)
+
+	cv := newCrlValidator(http.Client{})
+	// First chain: revoked, second chain: valid
+	err := cv.verifyPeerCertificates(nil, [][]*x509.Certificate{
+		{revokedLeaf, caCert},
+		{validLeaf, caCert},
+	})
+	assertNilE(t, err)
+}
+
+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 := newCrlValidator(http.Client{})
+	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 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) (*rsa.PrivateKey, *x509.Certificate) {
+	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:  time.Now().AddDate(0, 12, 0),
+		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
+	for _, arg := range args {
+		switch v := arg.(type) {
+		case *x509.Certificate:
+			revokedCertEntries = append(revokedCertEntries, x509.RevocationListEntry{
+				SerialNumber:   v.SerialNumber,
+				RevocationTime: time.Now().Add(-time.Hour * 24),
+			})
+		case *pkix.Extension:
+			extensions = append(extensions, *v)
+		}
+	}
+	crlTemplate := &x509.RevocationList{
+		Number:                    big.NewInt(1),
+		RevokedCertificateEntries: revokedCertEntries,
+		ExtraExtensions:           extensions,
+	}
+	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 closeServer(t *testing.T, server *http.Server) {
+	err := server.Shutdown(context.Background())
+	assertNilF(t, err)
+}