]> Cypherpunks repositories - gostls13.git/commitdiff
[release-branch.go1.25] crypto/x509: excluded subdomain constraints preclude wildcard...
authorRoland Shoemaker <bracewell@google.com>
Mon, 24 Nov 2025 16:46:08 +0000 (08:46 -0800)
committerCherry Mui <cherryyz@google.com>
Tue, 25 Nov 2025 20:14:08 +0000 (12:14 -0800)
When evaluating name constraints in a certificate chain, the presence of
an excluded subdomain constraint (e.g., excluding "test.example.com")
should preclude the use of a wildcard SAN (e.g., "*.example.com").

Fixes #76442
Fixes #76464
Fixes CVE-2025-61727

Change-Id: I42a0da010cb36d2ec9d1239ae3f61cf25eb78bba
Reviewed-on: https://go-review.googlesource.com/c/go/+/724400
Reviewed-by: Nicholas Husin <nsh@golang.org>
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Neal Patel <nealpatel@google.com>
src/crypto/x509/name_constraints_test.go
src/crypto/x509/verify.go
src/crypto/x509/verify_test.go

index a5851845164d1021f417e937d08aef9e943fa346..bc91b28401fce530b426f1141588e99b822078ea 100644 (file)
@@ -1624,6 +1624,40 @@ var nameConstraintsTests = []nameConstraintsTest{
                },
                expectedError: "URI with IP",
        },
+       // #87: subdomain excluded constraints preclude wildcard names
+       {
+               roots: []constraintsSpec{
+                       {
+                               bad: []string{"dns:foo.example.com"},
+                       },
+               },
+               intermediates: [][]constraintsSpec{
+                       {
+                               {},
+                       },
+               },
+               leaf: leafSpec{
+                       sans: []string{"dns:*.example.com"},
+               },
+               expectedError: "\"*.example.com\" is excluded by constraint \"foo.example.com\"",
+       },
+       // #88: wildcard names are not matched by subdomain permitted constraints
+       {
+               roots: []constraintsSpec{
+                       {
+                               ok: []string{"dns:foo.example.com"},
+                       },
+               },
+               intermediates: [][]constraintsSpec{
+                       {
+                               {},
+                       },
+               },
+               leaf: leafSpec{
+                       sans: []string{"dns:*.example.com"},
+               },
+               expectedError: "\"*.example.com\" is not permitted",
+       },
 }
 
 func makeConstraintsCACert(constraints constraintsSpec, name string, key *ecdsa.PrivateKey, parent *Certificate, parentKey *ecdsa.PrivateKey) (*Certificate, error) {
index bf7e7ec058db2bd59aae0cdf26a01e1c8e63bafa..9175fa4dc147a2bdbb970c4349d6daad89bf8e8c 100644 (file)
@@ -429,7 +429,7 @@ func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {
        return reverseLabels, true
 }
 
-func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string, reversedDomainsCache map[string][]string, reversedConstraintsCache map[string][]string) (bool, error) {
+func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string, excluded bool, reversedDomainsCache map[string][]string, reversedConstraintsCache map[string][]string) (bool, error) {
        // If the constraint contains an @, then it specifies an exact mailbox
        // name.
        if strings.Contains(constraint, "@") {
@@ -442,10 +442,10 @@ func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string, reversedDom
 
        // Otherwise the constraint is like a DNS constraint of the domain part
        // of the mailbox.
-       return matchDomainConstraint(mailbox.domain, constraint, reversedDomainsCache, reversedConstraintsCache)
+       return matchDomainConstraint(mailbox.domain, constraint, excluded, reversedDomainsCache, reversedConstraintsCache)
 }
 
-func matchURIConstraint(uri *url.URL, constraint string, reversedDomainsCache map[string][]string, reversedConstraintsCache map[string][]string) (bool, error) {
+func matchURIConstraint(uri *url.URL, constraint string, excluded bool, reversedDomainsCache map[string][]string, reversedConstraintsCache map[string][]string) (bool, error) {
        // From RFC 5280, Section 4.2.1.10:
        // “a uniformResourceIdentifier that does not include an authority
        // component with a host name specified as a fully qualified domain
@@ -474,7 +474,7 @@ func matchURIConstraint(uri *url.URL, constraint string, reversedDomainsCache ma
                return false, fmt.Errorf("URI with IP (%q) cannot be matched against constraints", uri.String())
        }
 
-       return matchDomainConstraint(host, constraint, reversedDomainsCache, reversedConstraintsCache)
+       return matchDomainConstraint(host, constraint, excluded, reversedDomainsCache, reversedConstraintsCache)
 }
 
 func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) {
@@ -491,7 +491,7 @@ func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) {
        return true, nil
 }
 
-func matchDomainConstraint(domain, constraint string, reversedDomainsCache map[string][]string, reversedConstraintsCache map[string][]string) (bool, error) {
+func matchDomainConstraint(domain, constraint string, excluded bool, reversedDomainsCache map[string][]string, reversedConstraintsCache map[string][]string) (bool, error) {
        // The meaning of zero length constraints is not specified, but this
        // code follows NSS and accepts them as matching everything.
        if len(constraint) == 0 {
@@ -508,6 +508,11 @@ func matchDomainConstraint(domain, constraint string, reversedDomainsCache map[s
                reversedDomainsCache[domain] = domainLabels
        }
 
+       wildcardDomain := false
+       if len(domain) > 0 && domain[0] == '*' {
+               wildcardDomain = true
+       }
+
        // RFC 5280 says that a leading period in a domain name means that at
        // least one label must be prepended, but only for URI and email
        // constraints, not DNS constraints. The code also supports that
@@ -534,6 +539,11 @@ func matchDomainConstraint(domain, constraint string, reversedDomainsCache map[s
                return false, nil
        }
 
+       if excluded && wildcardDomain && len(domainLabels) > 1 && len(constraintLabels) > 0 {
+               domainLabels = domainLabels[:len(domainLabels)-1]
+               constraintLabels = constraintLabels[:len(constraintLabels)-1]
+       }
+
        for i, constraintLabel := range constraintLabels {
                if !strings.EqualFold(constraintLabel, domainLabels[i]) {
                        return false, nil
@@ -553,7 +563,7 @@ func (c *Certificate) checkNameConstraints(count *int,
        nameType string,
        name string,
        parsedName any,
-       match func(parsedName, constraint any) (match bool, err error),
+       match func(parsedName, constraint any, excluded bool) (match bool, err error),
        permitted, excluded any) error {
 
        excludedValue := reflect.ValueOf(excluded)
@@ -565,7 +575,7 @@ func (c *Certificate) checkNameConstraints(count *int,
 
        for i := 0; i < excludedValue.Len(); i++ {
                constraint := excludedValue.Index(i).Interface()
-               match, err := match(parsedName, constraint)
+               match, err := match(parsedName, constraint, true)
                if err != nil {
                        return CertificateInvalidError{c, CANotAuthorizedForThisName, err.Error()}
                }
@@ -587,7 +597,7 @@ func (c *Certificate) checkNameConstraints(count *int,
                constraint := permittedValue.Index(i).Interface()
 
                var err error
-               if ok, err = match(parsedName, constraint); err != nil {
+               if ok, err = match(parsedName, constraint, false); err != nil {
                        return CertificateInvalidError{c, CANotAuthorizedForThisName, err.Error()}
                }
 
@@ -679,8 +689,8 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
                                        }
 
                                        if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "email address", name, mailbox,
-                                               func(parsedName, constraint any) (bool, error) {
-                                                       return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string), reversedDomainsCache, reversedConstraintsCache)
+                                               func(parsedName, constraint any, excluded bool) (bool, error) {
+                                                       return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string), excluded, reversedDomainsCache, reversedConstraintsCache)
                                                }, c.PermittedEmailAddresses, c.ExcludedEmailAddresses); err != nil {
                                                return err
                                        }
@@ -692,8 +702,8 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
                                        }
 
                                        if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "DNS name", name, name,
-                                               func(parsedName, constraint any) (bool, error) {
-                                                       return matchDomainConstraint(parsedName.(string), constraint.(string), reversedDomainsCache, reversedConstraintsCache)
+                                               func(parsedName, constraint any, excluded bool) (bool, error) {
+                                                       return matchDomainConstraint(parsedName.(string), constraint.(string), excluded, reversedDomainsCache, reversedConstraintsCache)
                                                }, c.PermittedDNSDomains, c.ExcludedDNSDomains); err != nil {
                                                return err
                                        }
@@ -706,8 +716,8 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
                                        }
 
                                        if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "URI", name, uri,
-                                               func(parsedName, constraint any) (bool, error) {
-                                                       return matchURIConstraint(parsedName.(*url.URL), constraint.(string), reversedDomainsCache, reversedConstraintsCache)
+                                               func(parsedName, constraint any, excluded bool) (bool, error) {
+                                                       return matchURIConstraint(parsedName.(*url.URL), constraint.(string), excluded, reversedDomainsCache, reversedConstraintsCache)
                                                }, c.PermittedURIDomains, c.ExcludedURIDomains); err != nil {
                                                return err
                                        }
@@ -719,7 +729,7 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
                                        }
 
                                        if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "IP address", ip.String(), ip,
-                                               func(parsedName, constraint any) (bool, error) {
+                                               func(parsedName, constraint any, _ bool) (bool, error) {
                                                        return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet))
                                                }, c.PermittedIPRanges, c.ExcludedIPRanges); err != nil {
                                                return err
index 60a4cea9146adf051922fcdce616c941866514c8..6a394e46e94f5a766c77aedf764a3416cd62fb7c 100644 (file)
@@ -1352,7 +1352,7 @@ var nameConstraintTests = []struct {
 
 func TestNameConstraints(t *testing.T) {
        for i, test := range nameConstraintTests {
-               result, err := matchDomainConstraint(test.domain, test.constraint, map[string][]string{}, map[string][]string{})
+               result, err := matchDomainConstraint(test.domain, test.constraint, false, map[string][]string{}, map[string][]string{})
 
                if err != nil && !test.expectError {
                        t.Errorf("unexpected error for test #%d: domain=%s, constraint=%s, err=%s", i, test.domain, test.constraint, err)