mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-30 02:46:30 +02:00
Update the isValidClientCertificate() method to consider any client-supplied intermediate certificates. Previously, in order to trust client certificates issued by an intermediate CA, users would need to include that intermediate CA's certificate directly in the client_ca setting. After this change, only the trusted root CA needs to be set: as long as the client can supply a set of certificates that chain back to this trusted root, the client's certificate will validate successfully. Rework the previous CRL checking logic to now consider CRLs for all issuers in the verified chains.
163 lines
4.1 KiB
Go
163 lines
4.1 KiB
Go
package evaluator
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
|
|
lru "github.com/hashicorp/golang-lru/v2"
|
|
|
|
"github.com/pomerium/pomerium/internal/log"
|
|
)
|
|
|
|
var isValidClientCertificateCache, _ = lru.New2Q[[4]string, bool](100)
|
|
|
|
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
|
|
}
|
|
|
|
cert := certInfo.Leaf
|
|
intermediates := certInfo.Intermediates
|
|
|
|
if cert == "" {
|
|
return false, nil
|
|
}
|
|
|
|
cacheKey := [4]string{ca, crl, cert, intermediates}
|
|
|
|
value, ok := isValidClientCertificateCache.Get(cacheKey)
|
|
if ok {
|
|
return value, nil
|
|
}
|
|
|
|
roots := x509.NewCertPool()
|
|
roots.AppendCertsFromPEM([]byte(ca))
|
|
|
|
intermediatesPool := x509.NewCertPool()
|
|
intermediatesPool.AppendCertsFromPEM([]byte(intermediates))
|
|
|
|
xcert, err := parseCertificate(cert)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
crls, err := parseCRLs([]byte(crl))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
verifyErr := verifyClientCertificate(xcert, roots, intermediatesPool, crls)
|
|
valid := verifyErr == nil
|
|
|
|
if verifyErr != nil {
|
|
log.Debug(context.Background()).Err(verifyErr).Msg("client certificate failed verification: %w")
|
|
}
|
|
|
|
isValidClientCertificateCache.Add(cacheKey, valid)
|
|
|
|
return valid, nil
|
|
}
|
|
|
|
func verifyClientCertificate(
|
|
cert *x509.Certificate,
|
|
roots *x509.CertPool,
|
|
intermediates *x509.CertPool,
|
|
crls map[string]*x509.RevocationList,
|
|
) error {
|
|
chains, err := cert.Verify(x509.VerifyOptions{
|
|
Roots: roots,
|
|
Intermediates: intermediates,
|
|
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// At least one of the verified chains must also pass revocation checking.
|
|
err = errors.New("internal error: no verified chains")
|
|
for _, chain := range chains {
|
|
err = validateClientCertificateChain(chain, crls)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Return an error from one of the chains that did not validate.
|
|
// (In the common case there will be at most one verified chain.)
|
|
return err
|
|
}
|
|
|
|
func validateClientCertificateChain(
|
|
chain []*x509.Certificate,
|
|
crls map[string]*x509.RevocationList,
|
|
) error {
|
|
// Consult CRLs for all CAs in the chain (that is, all certificates except
|
|
// for the first one). To match Envoy's behavior, if a CRL is provided for
|
|
// any CA in the chain, CRLs must be provided for all CAs in the chain (see
|
|
// https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/common.proto).
|
|
var anyIssuerHasCRL bool
|
|
var lastIssuerWithoutCRL *x509.Certificate
|
|
for i := 0; i < len(chain)-1; i++ {
|
|
cert, issuer := chain[i], chain[i+1]
|
|
crl := crls[string(issuer.RawSubject)]
|
|
if crl == nil {
|
|
lastIssuerWithoutCRL = issuer
|
|
continue
|
|
}
|
|
|
|
anyIssuerHasCRL = true
|
|
|
|
// Is the CRL signature itself valid?
|
|
if err := crl.CheckSignatureFrom(issuer); err != nil {
|
|
return fmt.Errorf("CRL signature verification failed for issuer %q: %w",
|
|
issuer.Subject, err)
|
|
}
|
|
|
|
// Is the certificate listed as revoked in the CRL?
|
|
for i := range crl.RevokedCertificates {
|
|
if cert.SerialNumber.Cmp(crl.RevokedCertificates[i].SerialNumber) == 0 {
|
|
return fmt.Errorf("certificate %q was revoked", cert.Subject)
|
|
}
|
|
}
|
|
}
|
|
|
|
if anyIssuerHasCRL && lastIssuerWithoutCRL != nil {
|
|
return fmt.Errorf("no CRL provided for issuer %q", lastIssuerWithoutCRL.Subject)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseCertificate(pemStr string) (*x509.Certificate, error) {
|
|
block, _ := pem.Decode([]byte(pemStr))
|
|
if block == nil {
|
|
return nil, fmt.Errorf("invalid certificate")
|
|
}
|
|
if block.Type != "CERTIFICATE" {
|
|
return nil, fmt.Errorf("unknown PEM type: %s", block.Type)
|
|
}
|
|
return x509.ParseCertificate(block.Bytes)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|