pomerium/authorize/evaluator/functions.go
Caleb Doxsey d2c14cd6d2
logging: remove ctx from global log methods (#5337)
* log: remove warn

* log: update debug

* log: update info

* remove level, log

* remove contextLogger function
2024-10-23 14:18:52 -06:00

335 lines
9.2 KiB
Go

package evaluator
import (
"crypto/x509"
"encoding/asn1"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"regexp"
"slices"
"strings"
lru "github.com/hashicorp/golang-lru/v2"
"golang.org/x/crypto/cryptobyte"
cb_asn1 "golang.org/x/crypto/cryptobyte/asn1"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/pkg/cryptutil"
)
// ClientCertConstraints contains additional constraints to validate when
// verifying a client certificate.
type ClientCertConstraints struct {
// MaxVerifyDepth is the maximum allowed certificate chain depth (not
// counting the leaf certificate). A value of 0 indicates no maximum.
MaxVerifyDepth uint32
// SANMatchers is a map of SAN type to regex match expression. When
// non-empty, a client certificate must contain at least one Subject
// Alternative Name that matches one of the expessions.
SANMatchers SANMatchers
}
// SANMatchers is a map of SAN type to regex match expression.
type SANMatchers = map[config.SANType]*regexp.Regexp
// ClientCertConstraintsFromConfig populates a new ClientCertConstraints struct
// based on the provided configuration.
func ClientCertConstraintsFromConfig(
cfg *config.DownstreamMTLSSettings,
) (*ClientCertConstraints, error) {
constraints := &ClientCertConstraints{
MaxVerifyDepth: cfg.GetMaxVerifyDepth(),
}
// Combine all SAN match patterns for a given type into one expression.
patternsByType := make(map[config.SANType][]string)
for i := range cfg.MatchSubjectAltNames {
m := &cfg.MatchSubjectAltNames[i]
patternsByType[m.Type] = append(patternsByType[m.Type], m.Pattern)
}
matchers := make(SANMatchers)
for k, v := range patternsByType {
var s strings.Builder
s.WriteString("^(")
s.WriteString(v[0])
for _, p := range v[1:] {
s.WriteString(")|(")
s.WriteString(p)
}
s.WriteString(")$")
r, err := regexp.Compile(s.String())
if err != nil {
return nil, err
}
matchers[k] = r
}
if len(matchers) > 0 {
constraints.SANMatchers = matchers
}
return constraints, nil
}
var isValidClientCertificateCache, _ = lru.New2Q[[5]string, bool](100)
func isValidClientCertificate(
ca, crl string, certInfo ClientCertificateInfo, constraints ClientCertConstraints,
) (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
}
constraintsJSON, err := json.Marshal(constraints)
if err != nil {
return false, fmt.Errorf("internal error: failed to serialize constraints: %w", err)
}
cacheKey := [5]string{ca, crl, cert, intermediates, string(constraintsJSON)}
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 := cryptutil.ParseCRLs([]byte(crl))
if err != nil {
return false, err
}
verifyErr := verifyClientCertificate(xcert, roots, intermediatesPool, crls, constraints)
valid := verifyErr == nil
if verifyErr != nil {
log.Debug().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,
constraints ClientCertConstraints,
) error {
// If a SubjectAltName extension is:
// - marked as critical, and
// - contains only name types that are not recognized by the Go standard
// library (i.e. no DNS, email address, IP address, or URI names)
// then the Go parsing code will add it to the UnhandleCriticalExtensions
// field of the Certificate struct. This will fail the Verify() call below.
// Because we support other SAN matching checks, let's avoid this behavior.
cert.UnhandledCriticalExtensions = slices.DeleteFunc(cert.UnhandledCriticalExtensions,
func(oid asn1.ObjectIdentifier) bool { return oid.Equal(oidSubjectAltName) })
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
// and satisfy any additional constraints.
err = errors.New("internal error: no verified chains")
for _, chain := range chains {
err = validateClientCertificateChain(chain, crls, constraints)
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,
constraints ClientCertConstraints,
) error {
if constraints.MaxVerifyDepth > 0 {
if d := uint32(len(chain) - 1); d > constraints.MaxVerifyDepth {
return fmt.Errorf("chain depth %d exceeds max_verify_depth %d",
d, constraints.MaxVerifyDepth)
}
}
if err := validateClientCertificateSANs(chain, constraints.SANMatchers); err != nil {
return err
}
// Consult CRLs only for the first CA in the chain, to match Envoy's
// behavior when the only_verify_leaf_cert_crl option is set (see
// https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/common.proto).
if len(chain) < 2 {
return nil
}
cert, issuer := chain[0], chain[1]
crl := crls[string(issuer.RawSubject)]
if crl == nil {
return nil
}
// 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)
}
}
return nil
}
var errNoSANMatch = errors.New("no matching Subject Alternative Name")
func validateClientCertificateSANs(chain []*x509.Certificate, matchers SANMatchers) error {
if len(matchers) == 0 {
return nil
} else if len(chain) == 0 {
return errors.New("internal error: no certificates in verified chain")
}
cert := chain[0]
if r := matchers[config.SANTypeDNS]; r != nil {
for _, name := range cert.DNSNames {
if r.MatchString(name) {
return nil
}
}
}
if r := matchers[config.SANTypeEmail]; r != nil {
for _, email := range cert.EmailAddresses {
if r.MatchString(email) {
return nil
}
}
}
if r := matchers[config.SANTypeIPAddress]; r != nil {
for _, ip := range cert.IPAddresses {
if r.MatchString(ip.String()) {
return nil
}
}
}
if r := matchers[config.SANTypeURI]; r != nil {
for _, uri := range cert.URIs {
if r.MatchString(uri.String()) {
return nil
}
}
}
if r := matchers[config.SANTypeUserPrincipalName]; r != nil {
names, err := getUserPrincipalNamesFromCert(cert)
if err != nil {
return err
}
for _, name := range names {
if r.MatchString(name) {
return nil
}
}
}
return errNoSANMatch
}
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)
}
var (
oidSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17}
oidUserPrincipalName = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 20, 2, 3}
otherNameTag = cb_asn1.Tag(0).Constructed().ContextSpecific()
otherNameValueTag = cb_asn1.Tag(0).Constructed().ContextSpecific()
)
func getUserPrincipalNamesFromSAN(raw []byte) ([]string, error) {
san := cryptobyte.String(raw)
var generalNames cryptobyte.String
if !san.ReadASN1(&generalNames, cb_asn1.SEQUENCE) {
return nil, errors.New("error reading GeneralNames sequence")
}
var upns []string
for !generalNames.Empty() {
var name cryptobyte.String
var tag cb_asn1.Tag
if !generalNames.ReadAnyASN1(&name, &tag) {
return nil, errors.New("error reading GeneralName")
} else if tag != otherNameTag {
continue
}
var oid asn1.ObjectIdentifier
if !name.ReadASN1ObjectIdentifier(&oid) {
return nil, errors.New("error reading OtherName type ID")
} else if !oid.Equal(oidUserPrincipalName) {
continue
}
var value cryptobyte.String
if !name.ReadASN1(&value, otherNameValueTag) {
return nil, errors.New("error reading UserPrincipalName value")
}
var utf8string cryptobyte.String
if !value.ReadASN1(&utf8string, cb_asn1.UTF8String) {
return nil, errors.New("error reading UserPrincipalName: expected UTF8String")
}
upns = append(upns, string(utf8string))
}
return upns, nil
}
func getUserPrincipalNamesFromCert(cert *x509.Certificate) ([]string, error) {
for _, ext := range cert.Extensions {
if ext.Id.Equal(oidSubjectAltName) {
return getUserPrincipalNamesFromSAN(ext.Value)
}
}
return nil, nil
}