add mTLS UserPrincipalName SAN match (#5177)

Add a new 'user_principal_name' type to the downstream mTLS
match_subject_alt_names option. This corresponds to the 'OtherName' type
with type-id 1.3.6.1.4.1.311.20.2.3 and a UTF8String value.
Add support for UserPrincipalName SAN matching to the policy evaluator.
This commit is contained in:
Kenneth Jenkins 2024-07-26 10:23:19 -07:00 committed by GitHub
parent b0606d9283
commit dbedfc586f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 416 additions and 84 deletions

View file

@ -3,14 +3,18 @@ package evaluator
import (
"context"
"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"
@ -135,6 +139,16 @@ func verifyClientCertificate(
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,
@ -242,6 +256,17 @@ func validateClientCertificateSANs(chain []*x509.Certificate, matchers SANMatche
}
}
}
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
}
@ -256,3 +281,56 @@ func parseCertificate(pemStr string) (*x509.Certificate, error) {
}
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
}