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

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