]> Cypherpunks repositories - gostls13.git/commitdiff
[release-branch.go1.26] crypto/x509: fix full email constraint matching
authorRoland Shoemaker <bracewell@google.com>
Wed, 11 Feb 2026 23:16:38 +0000 (15:16 -0800)
committerGopher Robot <gobot@golang.org>
Fri, 6 Mar 2026 00:12:54 +0000 (16:12 -0800)
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.

Updates #77952
Fixes #77973
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-internal-review.googlesource.com/c/go/+/3620
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/752082
TryBot-Bypass: Gopher Robot <gobot@golang.org>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
src/crypto/x509/constraints.go
src/crypto/x509/name_constraints_test.go
src/crypto/x509/verify.go

index 5addedcf77196ffa92595589b6d90c63065787f5..3c260a9b96fc3d92ac233aa49babf354ee45896a 100644 (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
 }
index 9a796e100d4719f8cc8b47873344a43ce61f3bea..b325c8edb9c52cfbc116f52e3cebff4a216d65db 100644 (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) {
index 1301390bee632774e6a03bd39e8d4b5c813bba38..3d9c115dba9df316affce4bc8ce2b039550044fd 100644 (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