crypto/x509: fix full email constraint matching

For full email addresses (local@domain), we stored a map between the
case sensitive local portion to the case insensitive domain portion, and
used that to check if a email SAN matched the constraint. This could be
abused, because it was a map[string]string, meaning if any two
constraints had the same local portion but different domains, the second
would overwrite the first.

Change the map from map[string]string to map[rfc2821Mailbox]struct{},
where the domain portion of the mailbox is lowercased. When checking for
a match we then check the parsed mailbox against the map, lowercasing
the domain portion of the query when we initially parse the address.
This gives us the same functionality as before, but without the
possibility of one constraint overwriting another.

Thanks to Jakub Ciolek for reporting this issue.

Fixes #77952
Fixes CVE-2026-27137

Change-Id: Ia405209be6f3b87cf4ac220a645467418dc41805
Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/3440
Reviewed-by: Neal Patel <nealpatel@google.com>
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/752182
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
TryBot-Bypass: Cherry Mui <cherryyz@google.com>
This commit is contained in:
Roland Shoemaker
2026-02-11 15:16:38 -08:00
committed by Cherry Mui
parent fb16297ae5
commit 2ebb157295
3 changed files with 67 additions and 32 deletions

View File

@@ -58,11 +58,11 @@ import (
// of nameConstraintsSet, to handle constraints which define full email
// addresses (i.e. 'test@example.com'). For bare domain constraints, we use the
// dnsConstraints type described above, querying the domain portion of the email
// address. For full email addresses, we also hold a map of email addresses that
// map the local portion of the email to the domain. When querying full email
// addresses we then check if the local portion of the email is present in the
// map, and if so case insensitively compare the domain portion of the
// email.
// address. For full email addresses, we also hold a map of email addresses with
// the domain portion of the email lowercased, since it is case insensitive. When
// looking up an email address in the constraint set, we first check the full
// email address map, and if we don't find anything, we check the domain portion
// of the email address against the dnsConstraints.
type nameConstraintsSet[T *net.IPNet | string, V net.IP | string] struct {
set []T
@@ -387,16 +387,22 @@ func (dnc *dnsConstraints) query(s string) (string, bool) {
type emailConstraints struct {
dnsConstraints interface{ query(string) (string, bool) }
fullEmails map[string]string
// fullEmails is map of rfc2821Mailboxs that are fully specified in the
// constraints, which we need to check for separately since they don't
// follow the same matching rules as the domain-based constraints. The
// domain portion of the rfc2821Mailbox has been lowercased, since the
// domain portion is case insensitive. When checking the map for an email,
// the domain portion of the query should also be lowercased.
fullEmails map[rfc2821Mailbox]struct{}
}
func newEmailConstraints(l []string, permitted bool) interface {
query(parsedEmail) (string, bool)
query(rfc2821Mailbox) (string, bool)
} {
if len(l) == 0 {
return nil
}
exactMap := map[string]string{}
exactMap := map[rfc2821Mailbox]struct{}{}
var domains []string
for _, c := range l {
if !strings.ContainsRune(c, '@') {
@@ -411,7 +417,8 @@ func newEmailConstraints(l []string, permitted bool) interface {
// certificate since parsing.
continue
}
exactMap[parsed.local] = parsed.domain
parsed.domain = strings.ToLower(parsed.domain)
exactMap[parsed] = struct{}{}
}
ec := &emailConstraints{
fullEmails: exactMap,
@@ -422,16 +429,16 @@ func newEmailConstraints(l []string, permitted bool) interface {
return ec
}
func (ec *emailConstraints) query(s parsedEmail) (string, bool) {
if len(ec.fullEmails) > 0 && strings.ContainsRune(s.email, '@') {
if domain, ok := ec.fullEmails[s.mailbox.local]; ok && strings.EqualFold(domain, s.mailbox.domain) {
return ec.fullEmails[s.email] + "@" + s.mailbox.domain, true
func (ec *emailConstraints) query(s rfc2821Mailbox) (string, bool) {
if len(ec.fullEmails) > 0 {
if _, ok := ec.fullEmails[s]; ok {
return fmt.Sprintf("%s@%s", s.local, s.domain), true
}
}
if ec.dnsConstraints == nil {
return "", false
}
constraint, found := ec.dnsConstraints.query(s.mailbox.domain)
constraint, found := ec.dnsConstraints.query(s.domain)
return constraint, found
}
@@ -441,7 +448,7 @@ type constraints[T any, V any] struct {
excluded interface{ query(V) (T, bool) }
}
func checkConstraints[T string | *net.IPNet, V any, P string | net.IP | parsedURI | parsedEmail](c constraints[T, V], s V, p P) error {
func checkConstraints[T string | *net.IPNet, V any, P string | net.IP | parsedURI | rfc2821Mailbox](c constraints[T, V], s V, p P) error {
if c.permitted != nil {
if _, found := c.permitted.query(s); !found {
return fmt.Errorf("%s %q is not permitted by any constraint", c.constraintType, p)
@@ -459,13 +466,13 @@ type chainConstraints struct {
ip constraints[*net.IPNet, net.IP]
dns constraints[string, string]
uri constraints[string, string]
email constraints[string, parsedEmail]
email constraints[string, rfc2821Mailbox]
index int
next *chainConstraints
}
func (cc *chainConstraints) check(dns []string, uris []parsedURI, emails []parsedEmail, ips []net.IP) error {
func (cc *chainConstraints) check(dns []string, uris []parsedURI, emails []rfc2821Mailbox, ips []net.IP) error {
for _, ip := range ips {
if err := checkConstraints(cc.ip, ip, ip); err != nil {
return err
@@ -488,8 +495,8 @@ func (cc *chainConstraints) check(dns []string, uris []parsedURI, emails []parse
}
}
for _, e := range emails {
if !domainNameValid(e.mailbox.domain, false) {
return fmt.Errorf("x509: cannot parse rfc822Name %q", e.mailbox)
if !domainNameValid(e.domain, false) {
return fmt.Errorf("x509: cannot parse rfc822Name %q", e)
}
if err := checkConstraints(cc.email, e, e); err != nil {
return err
@@ -509,7 +516,7 @@ func checkChainConstraints(chain []*Certificate) error {
ip: constraints[*net.IPNet, net.IP]{"IP address", newIPNetConstraints(c.PermittedIPRanges), newIPNetConstraints(c.ExcludedIPRanges)},
dns: constraints[string, string]{"DNS name", newDNSConstraints(c.PermittedDNSDomains, true), newDNSConstraints(c.ExcludedDNSDomains, false)},
uri: constraints[string, string]{"URI", newDNSConstraints(c.PermittedURIDomains, true), newDNSConstraints(c.ExcludedURIDomains, false)},
email: constraints[string, parsedEmail]{"email address", newEmailConstraints(c.PermittedEmailAddresses, true), newEmailConstraints(c.ExcludedEmailAddresses, false)},
email: constraints[string, rfc2821Mailbox]{"email address", newEmailConstraints(c.PermittedEmailAddresses, true), newEmailConstraints(c.ExcludedEmailAddresses, false)},
index: i,
}
if currentConstraints == nil {
@@ -592,24 +599,15 @@ func parseURIs(uris []*url.URL) ([]parsedURI, error) {
return parsed, nil
}
type parsedEmail struct {
email string
mailbox *rfc2821Mailbox
}
func (e parsedEmail) String() string {
return e.mailbox.local + "@" + e.mailbox.domain
}
func parseMailboxes(emails []string) ([]parsedEmail, error) {
parsed := make([]parsedEmail, 0, len(emails))
func parseMailboxes(emails []string) ([]rfc2821Mailbox, error) {
parsed := make([]rfc2821Mailbox, 0, len(emails))
for _, email := range emails {
mailbox, ok := parseRFC2821Mailbox(email)
if !ok {
return nil, fmt.Errorf("cannot parse rfc822Name %q", email)
}
mailbox.domain = strings.ToLower(mailbox.domain)
parsed = append(parsed, parsedEmail{strings.ToLower(email), &mailbox})
parsed = append(parsed, mailbox)
}
return parsed, nil
}

View File

@@ -1612,6 +1612,39 @@ var nameConstraintsTests = []nameConstraintsTest{
sans: []string{"dns:testexample.com"},
},
},
{
name: "excluded email constraint, multiple email with matching local portion",
roots: []constraintsSpec{
{
bad: []string{"email:a@example.com", "email:a@test.com"},
},
},
intermediates: [][]constraintsSpec{
{
{},
},
},
leaf: leafSpec{
sans: []string{"email:a@example.com"},
},
expectedError: "\"a@example.com\" is excluded by constraint \"a@example.com\"",
},
{
name: "email_case_check",
roots: []constraintsSpec{
{
ok: []string{"email:a@example.com"},
},
},
intermediates: [][]constraintsSpec{
{
{},
},
},
leaf: leafSpec{
sans: []string{"email:a@ExAmple.com"},
},
},
}
func makeConstraintsCACert(constraints constraintsSpec, name string, key *ecdsa.PrivateKey, parent *Certificate, parentKey *ecdsa.PrivateKey) (*Certificate, error) {

View File

@@ -253,6 +253,10 @@ type rfc2821Mailbox struct {
local, domain string
}
func (s rfc2821Mailbox) String() string {
return fmt.Sprintf("%s@%s", s.local, s.domain)
}
// parseRFC2821Mailbox parses an email address into local and domain parts,
// based on the ABNF for a “Mailbox” from RFC 2821. According to RFC 5280,
// Section 4.2.1.6 that's correct for an rfc822Name from a certificate: “The