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.
This commit is contained in:
Kenneth Jenkins 2023-08-03 15:59:11 -07:00 committed by GitHub
parent e91600c158
commit 9d4d31cb4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 352 additions and 92 deletions

View file

@ -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()),

View file

@ -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) {

View file

@ -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)
}

View file

@ -172,7 +172,7 @@ func TestEvaluator(t *testing.T) {
HTTP: RequestHTTP{
ClientCertificate: ClientCertificateInfo{
Presented: true,
Leaf: testUnsignedCert,
Leaf: testUntrustedCert,
},
},
})

View file

@ -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
}
}

View file

@ -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")
})
}

View file

@ -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 + "`" + `
)
`)
}

View file

@ -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) {

View file

@ -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)