From 9d4d31cb4f3666b6df171fa32f7188cfaadab3b5 Mon Sep 17 00:00:00 2001 From: Kenneth Jenkins <51246568+kenjenkins@users.noreply.github.com> Date: Thu, 3 Aug 2023 15:59:11 -0700 Subject: [PATCH] authorize: implement client certificate CRL check (#4439) Update isValidClientCertificate() to also consult the configured certificate revocation lists. Update existing test cases and add a new unit test to exercise the revocation support. Restore the skipped integration test case. Generate new test certificates and CRLs using a new `go run`-able source file. --- authorize/authorize.go | 6 + authorize/evaluator/config.go | 8 ++ authorize/evaluator/evaluator.go | 5 +- authorize/evaluator/evaluator_test.go | 2 +- authorize/evaluator/functions.go | 110 ++++++++++++++++-- authorize/evaluator/functions_test.go | 159 +++++++++++++------------- authorize/evaluator/gen-test-certs.go | 140 +++++++++++++++++++++++ config/options.go | 12 ++ integration/policy_test.go | 2 - 9 files changed, 352 insertions(+), 92 deletions(-) create mode 100644 authorize/evaluator/gen-test-certs.go diff --git a/authorize/authorize.go b/authorize/authorize.go index b2ac52ed2..e2956021c 100644 --- a/authorize/authorize.go +++ b/authorize/authorize.go @@ -99,6 +99,11 @@ func newPolicyEvaluator(opts *config.Options, store *store.Store) (*evaluator.Ev return nil, fmt.Errorf("authorize: invalid client CA: %w", err) } + clientCRL, err := opts.GetClientCRL() + if err != nil { + return nil, fmt.Errorf("authorize: invalid client CRL: %w", err) + } + authenticateURL, err := opts.GetInternalAuthenticateURL() if err != nil { return nil, fmt.Errorf("authorize: invalid authenticate url: %w", err) @@ -112,6 +117,7 @@ func newPolicyEvaluator(opts *config.Options, store *store.Store) (*evaluator.Ev return evaluator.New(ctx, store, evaluator.WithPolicies(opts.GetAllPolicies()), evaluator.WithClientCA(clientCA), + evaluator.WithClientCRL(clientCRL), evaluator.WithSigningKey(signingKey), evaluator.WithAuthenticateURL(authenticateURL.String()), evaluator.WithGoogleCloudServerlessAuthenticationServiceAccount(opts.GetGoogleCloudServerlessAuthenticationServiceAccount()), diff --git a/authorize/evaluator/config.go b/authorize/evaluator/config.go index 9c7ecab2d..2d96b3e49 100644 --- a/authorize/evaluator/config.go +++ b/authorize/evaluator/config.go @@ -7,6 +7,7 @@ import ( type evaluatorConfig struct { policies []config.Policy clientCA []byte + clientCRL []byte signingKey []byte authenticateURL string googleCloudServerlessAuthenticationServiceAccount string @@ -38,6 +39,13 @@ func WithClientCA(clientCA []byte) Option { } } +// WithClientCRL sets the client CRL in the config. +func WithClientCRL(clientCRL []byte) Option { + return func(cfg *evaluatorConfig) { + cfg.clientCRL = clientCRL + } +} + // WithSigningKey sets the signing key and algorithm in the config. func WithSigningKey(signingKey []byte) Option { return func(cfg *evaluatorConfig) { diff --git a/authorize/evaluator/evaluator.go b/authorize/evaluator/evaluator.go index 09c0feb77..ab5798719 100644 --- a/authorize/evaluator/evaluator.go +++ b/authorize/evaluator/evaluator.go @@ -93,6 +93,7 @@ type Evaluator struct { policyEvaluators map[uint64]*PolicyEvaluator headersEvaluators *HeadersEvaluator clientCA []byte + clientCRL []byte } // New creates a new Evaluator. @@ -112,6 +113,7 @@ func New(ctx context.Context, store *store.Store, options ...Option) (*Evaluator } e.clientCA = cfg.clientCA + e.clientCRL = cfg.clientCRL e.policyEvaluators = make(map[uint64]*PolicyEvaluator) for i := range cfg.policies { @@ -209,7 +211,8 @@ func (e *Evaluator) evaluatePolicy(ctx context.Context, req *Request) (*PolicyRe return nil, err } - isValidClientCertificate, err := isValidClientCertificate(clientCA, req.HTTP.ClientCertificate) + isValidClientCertificate, err := + isValidClientCertificate(clientCA, string(e.clientCRL), req.HTTP.ClientCertificate) if err != nil { return nil, fmt.Errorf("authorize: error validating client certificate: %w", err) } diff --git a/authorize/evaluator/evaluator_test.go b/authorize/evaluator/evaluator_test.go index c1f695c09..afaf940a5 100644 --- a/authorize/evaluator/evaluator_test.go +++ b/authorize/evaluator/evaluator_test.go @@ -172,7 +172,7 @@ func TestEvaluator(t *testing.T) { HTTP: RequestHTTP{ ClientCertificate: ClientCertificateInfo{ Presented: true, - Leaf: testUnsignedCert, + Leaf: testUntrustedCert, }, }, }) diff --git a/authorize/evaluator/functions.go b/authorize/evaluator/functions.go index 987a22d49..76f631361 100644 --- a/authorize/evaluator/functions.go +++ b/authorize/evaluator/functions.go @@ -4,6 +4,7 @@ import ( "context" "crypto/x509" "encoding/pem" + "errors" "fmt" lru "github.com/hashicorp/golang-lru/v2" @@ -11,9 +12,9 @@ import ( "github.com/pomerium/pomerium/internal/log" ) -var isValidClientCertificateCache, _ = lru.New2Q[[2]string, bool](100) +var isValidClientCertificateCache, _ = lru.New2Q[[3]string, bool](100) -func isValidClientCertificate(ca string, certInfo ClientCertificateInfo) (bool, error) { +func isValidClientCertificate(ca, crl string, certInfo ClientCertificateInfo) (bool, error) { // when ca is the empty string, client certificates are not required if ca == "" { return true, nil @@ -25,25 +26,29 @@ func isValidClientCertificate(ca string, certInfo ClientCertificateInfo) (bool, return false, nil } - cacheKey := [2]string{ca, cert} + cacheKey := [3]string{ca, crl, cert} value, ok := isValidClientCertificateCache.Get(cacheKey) if ok { return value, nil } - roots := x509.NewCertPool() - roots.AppendCertsFromPEM([]byte(ca)) + roots, err := parseCertificates([]byte(ca)) + if err != nil { + return false, err + } xcert, err := parseCertificate(cert) if err != nil { return false, err } - _, verifyErr := xcert.Verify(x509.VerifyOptions{ - Roots: roots, - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - }) + crls, err := parseCRLs([]byte(crl)) + if err != nil { + return false, err + } + + verifyErr := verifyClientCertificate(xcert, roots, crls) valid := verifyErr == nil if verifyErr != nil { @@ -55,6 +60,55 @@ func isValidClientCertificate(ca string, certInfo ClientCertificateInfo) (bool, return valid, nil } +var errCertificateRevoked = errors.New("certificate revoked") + +func verifyClientCertificate( + cert *x509.Certificate, + roots map[string]*x509.Certificate, + crls map[string]*x509.RevocationList, +) error { + rootPool := x509.NewCertPool() + for _, root := range roots { + rootPool.AddCert(root) + } + + if _, err := cert.Verify(x509.VerifyOptions{ + Roots: rootPool, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }); err != nil { + return err + } + + // Consult any CRL for the presented certificate's Issuer. + issuer := string(cert.RawIssuer) + crl := crls[issuer] + if crl == nil { + return nil + } + + // Do we have a corresponding trusted CA certificate? + root, ok := roots[issuer] + if !ok { + return fmt.Errorf("could not check CRL: no matching trusted CA for issuer %s", + cert.Issuer) + } + + // Is the CRL signature itself valid? + if err := crl.CheckSignatureFrom(root); err != nil { + return fmt.Errorf("could not check CRL for issuer %s: signature verification "+ + "error: %w", cert.Issuer, err) + } + + // Is the client certificate listed as revoked in this CRL? + for i := range crl.RevokedCertificates { + if cert.SerialNumber.Cmp(crl.RevokedCertificates[i].SerialNumber) == 0 { + return errCertificateRevoked + } + } + + return nil +} + func parseCertificate(pemStr string) (*x509.Certificate, error) { block, _ := pem.Decode([]byte(pemStr)) if block == nil { @@ -65,3 +119,41 @@ func parseCertificate(pemStr string) (*x509.Certificate, error) { } return x509.ParseCertificate(block.Bytes) } + +func parseCertificates(certs []byte) (map[string]*x509.Certificate, error) { + m := make(map[string]*x509.Certificate) + for { + var block *pem.Block + block, certs = pem.Decode(certs) + if block == nil { + return m, nil + } + if block.Type != "CERTIFICATE" { + continue + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + m[string(cert.RawSubject)] = cert + } +} + +func parseCRLs(crl []byte) (map[string]*x509.RevocationList, error) { + m := make(map[string]*x509.RevocationList) + for { + var block *pem.Block + block, crl = pem.Decode(crl) + if block == nil { + return m, nil + } + if block.Type != "X509 CRL" { + continue + } + l, err := x509.ParseRevocationList(block.Bytes) + if err != nil { + return nil, err + } + m[string(l.RawIssuer)] = l + } +} diff --git a/authorize/evaluator/functions_test.go b/authorize/evaluator/functions_test.go index e8ae50994..f87ec953a 100644 --- a/authorize/evaluator/functions_test.go +++ b/authorize/evaluator/functions_test.go @@ -6,106 +6,84 @@ import ( "github.com/stretchr/testify/assert" ) +// These certificates can be regenerated by running: +// +// go run ./gen-test-certs.go +// +// (Copy and paste the output here.) const ( testCA = ` -----BEGIN CERTIFICATE----- -MIIEtjCCAx6gAwIBAgIRAJFkXxMjoQzoojykk6CiiGkwDQYJKoZIhvcNAQELBQAw -czEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSQwIgYDVQQLDBtjYWxl -YkBwb3Atb3MgKENhbGViIERveHNleSkxKzApBgNVBAMMIm1rY2VydCBjYWxlYkBw -b3Atb3MgKENhbGViIERveHNleSkwHhcNMjAwNDI0MTY1MzEwWhcNMzAwNDI0MTY1 -MzEwWjBzMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExJDAiBgNVBAsM -G2NhbGViQHBvcC1vcyAoQ2FsZWIgRG94c2V5KTErMCkGA1UEAwwibWtjZXJ0IGNh -bGViQHBvcC1vcyAoQ2FsZWIgRG94c2V5KTCCAaIwDQYJKoZIhvcNAQEBBQADggGP -ADCCAYoCggGBAL2QSyQGjaGD97K7HSExJfMcuyEoh+ewAkPZ/HZR4n12zwAn1sLK -RqusKSfMe8qG6KgsojXrJ9AXEkD7x3bmK5j/4M/lwlNGulg+k5MSu3leoLpOZwfX -JQTu+HDzWubu5cjy7taHyeZc35VbOBWEaDJgVxmJvE9TJIOr8POZ7DD/rlkbgQas -s6G/8cg2mRX0Rh3O20/1bvi9Uen/kraBgGMOyG5MfuiiTl3KsrGST848Q+jiSbu3 -5F5MAzdO4tlR6kqEZk/Igog6OPkTb82vMli/R+mR37JYncQcj0WNYS4PkfjofVpb -FwrHtfdkVYJ9T2yNvQnJVu6MF9fhj9FqWQbsdbYKlUDow5KwI+BxmCAmGwgzmCOy -ONkglj76fPKFkoF4s+DSFocbAwhdazaViAcCB+x6yohOUjgG7H9NJo0MasPHuqUO -8d56Bf0BTXfNX6nOgYYisrOoEATCbs729vHMaQ/7pG2zf9dnEuw95gZTSr9Rv3dx -2NjmM6+tNOMCzwIDAQABo0UwQzAOBgNVHQ8BAf8EBAMCAgQwEgYDVR0TAQH/BAgw -BgEB/wIBADAdBgNVHQ4EFgQUShofXNkcXh2q4wnnWZ2bco24XEQwDQYJKoZIhvcN -AQELBQADggGBAJQzfmr84xtvoUgnq8T4ND0Q166dlnbDnASRFhbmyZcxzvDJsPs4 -N45HbIg0xOsXOaBaE+jTSV4GmZ/vtyP8WbQrhabL2zpnjnLF1d9B0qx/RlIzoDEa -e/0zc0R6RAd64/aE/jHNDhfTNXD/NmnI25RqgnsZXXXRVMTl+PzQ1A8XQghZVWHN -vbyFFd3GE5Qs+vxMzwKCqp6f3MI8KyI2aM4hZZ+zULdEuSw0hWzMOkeZY6LC0flW -/rpkT+GLA3uZ357iehSISLqnkIozw92ldov5oZDthoy3i1I6gIDkngk7BGKr42pD -L2sWi1MEEIhymy4K1DnRkGre3mqzus2y/nE4ruuJlctq6QXcCSnko717vukVtoE8 -o5SkW4usivU8yZeBLt56sySRyCpe/T1XAFTQZ5Q4S5ssGmNpOLS9Aa5iOUz9/62S -uvjFyvOEE3yqd/d3py8qm6olcjaMooVA8j5G+QF/UiH951azGIez6/Ui1lg1m0T6 -+YLkPqNIt0o9dQ== +MIIBZzCCAQ6gAwIBAgICEAAwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl +ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzMwNzMxMTUzMzE5WjAaMRgw +FgYDVQQDEw9UcnVzdGVkIFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC +AARGMVCBvgbkVB3OPltnBHAy9s9rtog2rlnzZ4BKzPBbLEM0uPYTOZa0LLxSMtCj +N+Bu3wfGPgHU6/pJ2uEky7/Uo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUXep6D8FTP6+5ZdR/HjP3pYfmxkwwCgYIKoZIzj0E +AwIDRwAwRAIgSS5J6ii/n0gf2/UAMFb+UVG8n0nb1dcBCG55fSlWlVECIENVK+X3 +6SfUhfYSVBvOdS08AzMVvOM7aZbWaY9UirIf -----END CERTIFICATE----- ` testValidCert = ` -----BEGIN CERTIFICATE----- -MIIESDCCArCgAwIBAgIQG/h9GflpINqLLv4Tde9+STANBgkqhkiG9w0BAQsFADBz -MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExJDAiBgNVBAsMG2NhbGVi -QHBvcC1vcyAoQ2FsZWIgRG94c2V5KTErMCkGA1UEAwwibWtjZXJ0IGNhbGViQHBv -cC1vcyAoQ2FsZWIgRG94c2V5KTAeFw0xOTA2MDEwMDAwMDBaFw0zMDA1MjAyMDM4 -NDRaME8xJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEk -MCIGA1UECwwbY2FsZWJAcG9wLW9zIChDYWxlYiBEb3hzZXkpMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5ouz2dlXHALdxiLcLwAvxg02CN/Jdcrmyyzm -bzKHqIpknotZSlbPgE/mp5wMwIoyMqFIEm3IzXFEf3cjFYYG4b6wp4zlFrx7jCOa -vhEHpH3yM71xt1I/BME6VrmX7sRKO90dwpTxCOadx9aGEn1AlHuPfhMMm/WTLynD -d5hbsHKp7eZMYHvQnferTelq5NnBySBP/HaAtF76qTSQzHev5K/cgioDZAaM0dnP -bicl0Zay+f5INrDr9XtQo/FHwGI/YLMW5TWXYmHjYmdD8s4Tg/KUoRMgJp4mlkkF -9t1pwArbNFU/4wQWPbpWBLh1gcnQxojSZ3a6aI+V+REDzV/PVQIDAQABo3wwejAO -BgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMAwG -A1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUShofXNkcXh2q4wnnWZ2bco24XEQwGgYD -VR0RBBMwEYIPZXhhbXBsZS1zdWJqZWN0MA0GCSqGSIb3DQEBCwUAA4IBgQC78S2n -6jcKfWbm24g/U5tkWiBVnBk1jphH7Ct69Lw2JNstGLtNs4AiE9lKmXDQ82MiAFYg -gaeiHRhTebkOTF9Kx3Jwq7bwhhzONqPp5a0SkY4EWjZ7c5k/fZc8DkrRE71hOgMf -rFbRBZCywBVtGbXIA1uMsCijTe4sQF1ZA918NmfjhpIhRHljQJM16RJ753s+0CZ8 -WomOW4JrtjJefRuV97PRADvRNQbtZYelnoTfbp1afGhbQpKjyylCDGlpJS4mGrSA -lPaRVhEB+wI8gA3lzpa6adXsc1yueZ19++dxQNYxAawCMQNjjxy3aLWzy8aPWxxq -Qo/Q9rqjre3SpJfARLOV9ezQNbqsXvJW+5DcoG5dx8s6jAhMusNjUHpf6oVgnv65 -3Bvl124bZyf9q4lW9g8pvZkrgQ3Fx2IahqhXhyF5zrqf2r9+1l0fXocIUP2GQ+Fr -b9j9bWWhov5aidEjPwpFeTmzcGqCWQBEA4H+yo/4YaIN0sOfE2yaAmc3gcU= +MIIBYTCCAQigAwIBAgICEAEwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl +ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzMwNzMxMTUzMzE5WjAeMRww +GgYDVQQDExN0cnVzdGVkIGNsaWVudCBjZXJ0MFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAEfAYP3ZwiKJgk9zXpR/CMHYlAxjweJaMJihIS2FTA5gb0xBcTEe5AGpNF +CHWPk4YCB25VeHg9GmY9Q1+qDD1hdqM4MDYwEwYDVR0lBAwwCgYIKwYBBQUHAwIw +HwYDVR0jBBgwFoAUXep6D8FTP6+5ZdR/HjP3pYfmxkwwCgYIKoZIzj0EAwIDRwAw +RAIgProROtxpvKS/qjrjonSvacnhdU0JwoXj2DgYvF/qjrUCIAXlHkdEzyXmTLuu +/YxuOibV35vlaIzj21GRj4pYmVR1 -----END CERTIFICATE----- ` - testUnsignedCert = ` + testUntrustedCert = ` -----BEGIN CERTIFICATE----- -MIIESTCCArGgAwIBAgIRAIE9860UHBIVofXB5cu/aWAwDQYJKoZIhvcNAQELBQAw -czEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSQwIgYDVQQLDBtjYWxl -YkBwb3Atb3MgKENhbGViIERveHNleSkxKzApBgNVBAMMIm1rY2VydCBjYWxlYkBw -b3Atb3MgKENhbGViIERveHNleSkwHhcNMTkwNjAxMDAwMDAwWhcNMzAwNTIwMjIw -NDAxWjBPMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUx -JDAiBgNVBAsMG2NhbGViQHBvcC1vcyAoQ2FsZWIgRG94c2V5KTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAKPgWHAJ58p7ZZ6MHA6QHA9rQQWKSvYbN9zz -fCCURqHFbQHCCJs2D39XPioo9EMZcD6J7ldwEOJsdSNw3+dzBCvIl7wP6fqtbo/3 -SNgRaLAB+Mb4S8oek6P6zHkjuOXzodhCZjLO7oxY9pjGREy6hC/SjylJFgw9mKEG -SYmsyCqeP5BfW9DghRgd5uJe0HtwlBZLPS91Mk5whn7YOxnWslS/REwZdd12s3DI -WQdmvGhMakIAiMKmx+LX9qS3Ua2gUArHnSFXcOAg9iK+MM68T1KsQTCYnRZVK4v5 -Na4qEjiPhmkzzEExZa787ClL6UXfoXB+jXy2sXu0CDD4tv2D7R8CAwEAAaN8MHow -DgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAM -BgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFH8wenPOF2tE2EIksItmlkWfgEMkMBoG -A1UdEQQTMBGCD2ludmFsaWQtc3ViamVjdDANBgkqhkiG9w0BAQsFAAOCAYEAJCdl -c6J/x/UY6vEDzplwR8iZ5s7dyKKF7bwNdjEvBREgkTY6GmwDC9HOmWWPs7vENqEX -jUwHEK+v7A7AUIS4WeJrJgogzEDPI7ZlVtzQNviqMavzk/I1Us00WYtMQQFb1Sgz -xIRskug5wH6vPcR4XbCftx6NP9UFG8pJLPTJ67ZUaTP23ccsToMM/Dd17LFrtleE -9xAvdqA54vcBiJ99uts+xWlQznjIgdauNC6sOmL3JAflyj6aBy+Dcos9R35ERIXz -3rRl25yXjtidPDo8YxmtHs+Ijw4R3iJ44NCcc/+LfACYUcua0cBF2Ixk2JrFYx8n -wwRJukrHXI+RFBmSOlUripyyJH92H5vXvj8lO5wM8wVVVe8anr5TOvxFOAjNC5a3 -vJByvJQTUEkx8rT7zZi8eSQJHP3Eoqr9g4ajqIU22yrCxiiQXpZLJ4JFQQEgyD9A -Y+E5W+FKfIBv9yvdNBYZsL6IZ0Yh1ctKwB5gnajO8+swx5BeaCIbBrCtOBSB +MIIBZzCCAQygAwIBAgICEAEwCgYIKoZIzj0EAwIwHDEaMBgGA1UEAxMRVW50cnVz +dGVkIFJvb3QgQ0EwIBgPMDAwMTAxMDEwMDAwMDBaFw0zMzA3MzExNTMzMTlaMCAx +HjAcBgNVBAMTFXVudHJ1c3RlZCBjbGllbnQgY2VydDBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABBG2Qo/l0evcNKjwaJsi04BJJh7ec064lRiKaRMNRUK+UxkKmfbn +0FobVtlioTmzeWCX8OJFPfO7y7/VLMiGVr+jODA2MBMGA1UdJQQMMAoGCCsGAQUF +BwMCMB8GA1UdIwQYMBaAFCd2l26OflZF3LTFUEBB54ZQV3AUMAoGCCqGSM49BAMC +A0kAMEYCIQCYEk3D4nHevIlKFg6f7O2/GdptzKU6F05pz4B3Aa8ahAIhAJcBGUNm +cqQQJNOelJJmMeFOzmmTk7oNFxCGEC00wlGn -----END CERTIFICATE----- +` + testRevokedCert = ` +-----BEGIN CERTIFICATE----- +MIIBYzCCAQigAwIBAgICEAIwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl +ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzMwNzMxMTUzMzE5WjAeMRww +GgYDVQQDExNyZXZva2VkIGNsaWVudCBjZXJ0MFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAEoN/gKhZgyKhTmiC3qLHDQ54TIpgXBTvGKrdIRHO616XMkzj0lFZMHG5u +LGK3qo8wJtyoalOFTkSck0kl3PD/9qM4MDYwEwYDVR0lBAwwCgYIKwYBBQUHAwIw +HwYDVR0jBBgwFoAUXep6D8FTP6+5ZdR/HjP3pYfmxkwwCgYIKoZIzj0EAwIDSQAw +RgIhAK6/oLtzvrK2Vrt1MRZJ6aGU2Cz28X0Y/4TOwFSvCK9AAiEAm4XPQXy6L0PE +vfXoV8RW/RnndDhf8iDALvAaAuS82fU= +-----END CERTIFICATE----- +` + testCRL = ` +-----BEGIN X509 CRL----- +MIHfMIGFAgEBMAoGCCqGSM49BAMCMBoxGDAWBgNVBAMTD1RydXN0ZWQgUm9vdCBD +QRgPMDAwMTAxMDEwMDAwMDBaMBUwEwICEAIXDTIzMDgwMzE1MzMxOVqgMDAuMB8G +A1UdIwQYMBaAFF3qeg/BUz+vuWXUfx4z96WH5sZMMAsGA1UdFAQEAgIgADAKBggq +hkjOPQQDAgNJADBGAiEApMG/hJxlMe9QNF8cCVjOFyTfVVBkfKtrFQDmElO46x4C +IQCX9SYteNaaW+NVmGED6QfHXRWnDqHnXfe/mLxmnPVWzA== +-----END X509 CRL----- ` ) func Test_isValidClientCertificate(t *testing.T) { t.Run("no ca", func(t *testing.T) { - valid, err := isValidClientCertificate("", ClientCertificateInfo{Leaf: "WHATEVER!"}) + valid, err := isValidClientCertificate("", "", ClientCertificateInfo{Leaf: "WHATEVER!"}) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") }) t.Run("no cert", func(t *testing.T) { - valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{}) + valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{}) assert.NoError(t, err, "should not return an error") assert.False(t, valid, "should return false") }) t.Run("valid cert", func(t *testing.T) { - valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{ + valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCert, }) @@ -113,19 +91,42 @@ func Test_isValidClientCertificate(t *testing.T) { assert.True(t, valid, "should return true") }) t.Run("unsigned cert", func(t *testing.T) { - valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{ + valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, - Leaf: testUnsignedCert, + Leaf: testUntrustedCert, }) assert.NoError(t, err, "should not return an error") assert.False(t, valid, "should return false") }) t.Run("not a cert", func(t *testing.T) { - valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{ + valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: "WHATEVER!", }) assert.Error(t, err, "should return an error") assert.False(t, valid, "should return false") }) + t.Run("revoked cert", func(t *testing.T) { + revokedCertInfo := ClientCertificateInfo{ + Presented: true, + Leaf: testRevokedCert, + } + + // The "revoked cert" should otherwise be valid (when no CRL is specified). + valid, err := isValidClientCertificate(testCA, "", revokedCertInfo) + assert.NoError(t, err, "should not return an error") + assert.True(t, valid, "should return true") + + valid, err = isValidClientCertificate(testCA, testCRL, revokedCertInfo) + assert.NoError(t, err, "should not return an error") + assert.False(t, valid, "should return false") + + // Specifying a CRL containing the revoked cert should not affect other certs. + valid, err = isValidClientCertificate(testCA, testCRL, ClientCertificateInfo{ + Presented: true, + Leaf: testValidCert, + }) + assert.NoError(t, err, "should not return an error") + assert.True(t, valid, "should return true") + }) } diff --git a/authorize/evaluator/gen-test-certs.go b/authorize/evaluator/gen-test-certs.go new file mode 100644 index 000000000..171473509 --- /dev/null +++ b/authorize/evaluator/gen-test-certs.go @@ -0,0 +1,140 @@ +//go:build ignore + +package main + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "log" + "math/big" + "time" +) + +// Returns a new self-signed certificate, as both PEM data and an +// *x509.Certificate, along with the corresponding private key. +func newSelfSignedCertificate(template *x509.Certificate) ( + string, *x509.Certificate, *ecdsa.PrivateKey, +) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + log.Fatalln(err) + } + der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) + if err != nil { + log.Fatalln(err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + log.Fatalln(err) + } + return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})), cert, key +} + +// Returns a new certificate, as both PEM data and an *x509.Certificate, along +// with the new certificate's corresponding private key. +func newCertificate(template, issuer *x509.Certificate, issuerKey *ecdsa.PrivateKey) ( + string, *x509.Certificate, *ecdsa.PrivateKey, +) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + log.Fatalln(err) + } + der, err := x509.CreateCertificate(rand.Reader, template, issuer, key.Public(), issuerKey) + if err != nil { + log.Fatalln(err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + log.Fatalln(err) + } + return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})), cert, key +} + +// Returns a new CRL in PEM format. +func newCRL( + template *x509.RevocationList, issuer *x509.Certificate, issuerKey *ecdsa.PrivateKey, +) string { + der, err := x509.CreateRevocationList(rand.Reader, template, issuer, issuerKey) + if err != nil { + log.Fatalln(err) + } + return string(pem.EncodeToMemory(&pem.Block{Type: "X509 CRL", Bytes: der})) +} + +// Generates new test certificates and CRLs. +func main() { + notAfter := time.Now().Add(3650 * 24 * time.Hour) + + rootPEM, rootCA, rootKey := newSelfSignedCertificate(&x509.Certificate{ + SerialNumber: big.NewInt(0x1000), + Subject: pkix.Name{ + CommonName: "Trusted Root CA", + }, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + }) + + trustedClientCertPEM, _, _ := newCertificate(&x509.Certificate{ + SerialNumber: big.NewInt(0x1001), + Subject: pkix.Name{ + CommonName: "trusted client cert", + }, + NotAfter: notAfter, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }, rootCA, rootKey) + + _, untrustedCA, untrustedCAKey := newSelfSignedCertificate(&x509.Certificate{ + SerialNumber: big.NewInt(0x1000), + Subject: pkix.Name{ + CommonName: "Untrusted Root CA", + }, + NotAfter: notAfter, + BasicConstraintsValid: true, + IsCA: true, + }) + + untrustedClientCertPEM, _, _ := newCertificate(&x509.Certificate{ + SerialNumber: big.NewInt(0x1001), + Subject: pkix.Name{ + CommonName: "untrusted client cert", + }, + NotAfter: notAfter, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }, untrustedCA, untrustedCAKey) + + revokedClientCertPEM, revokedClientCert, _ := newCertificate(&x509.Certificate{ + SerialNumber: big.NewInt(0x1002), + Subject: pkix.Name{ + CommonName: "revoked client cert", + }, + NotAfter: notAfter, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }, rootCA, rootKey) + + crlPEM := newCRL(&x509.RevocationList{ + Number: big.NewInt(0x2000), + RevokedCertificates: []pkix.RevokedCertificate{ + { + SerialNumber: revokedClientCert.SerialNumber, + RevocationTime: time.Now(), + }, + }, + }, rootCA, rootKey) + + fmt.Println(` +const ( + testCA = ` + "`\n" + rootPEM + "`" + ` + testValidCert = ` + "`\n" + trustedClientCertPEM + "`" + ` + testUntrustedCert = ` + "`\n" + untrustedClientCertPEM + "`" + ` + testRevokedCert = ` + "`\n" + revokedClientCertPEM + "`" + ` + testCRL = ` + "`\n" + crlPEM + "`" + ` +) +`) +} diff --git a/config/options.go b/config/options.go index fb18787e4..48d23d76a 100644 --- a/config/options.go +++ b/config/options.go @@ -966,6 +966,18 @@ func (o *Options) GetClientCA() ([]byte, error) { return nil, nil } +// GetClientCRL returns the client certificate revocation list bundle. If +// neither client_crl nor client_crl_file is specified nil will be returned. +func (o *Options) GetClientCRL() ([]byte, error) { + if o.ClientCRL != "" { + return base64.StdEncoding.DecodeString(o.ClientCRL) + } + if o.ClientCRLFile != "" { + return os.ReadFile(o.ClientCRLFile) + } + return nil, nil +} + // GetDataBrokerCertificate gets the optional databroker certificate. This method will return nil if no certificate is // specified. func (o *Options) GetDataBrokerCertificate() (*tls.Certificate, error) { diff --git a/integration/policy_test.go b/integration/policy_test.go index 88eab948f..05475b75f 100644 --- a/integration/policy_test.go +++ b/integration/policy_test.go @@ -393,8 +393,6 @@ func TestDownstreamClientCA(t *testing.T) { assert.Equal(t, "/", result.Path) }) t.Run("revoked client cert", func(t *testing.T) { - t.Skip("CRL support must be reimplemented first") - // Configure an http.Client with a revoked client certificate. cert := loadCertificate(t, "downstream-1-client-revoked") client, transport := getClientWithTransport(t)