From 04db77a423cac75bb82cc9a6859991ae9c016344 Mon Sep 17 00:00:00 2001
From: Roland Shoemaker <bracewell@google.com>
Date: Mon, 24 Nov 2025 08:46:08 -0800
Subject: [PATCH] [release-branch.go1.24] crypto/x509: excluded subdomain
 constraints preclude wildcard SANs

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 #76463
Fixes CVE-2025-61727

Change-Id: I42a0da010cb36d2ec9d1239ae3f61cf25eb78bba
Reviewed-on: https://go-review.googlesource.com/c/go/+/724401
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: Nicholas Husin <nsh@golang.org>
Reviewed-by: Neal Patel <nealpatel@google.com>

Upstream-Status: Backport [https://github.com/golang/go/commit/04db77a423cac75bb82cc9a6859991ae9c016344]
CVE: CVE-2025-61727
Signed-off-by: Vijay Anusuri <vanusuri@mvista.com>
---
 src/crypto/x509/name_constraints_test.go | 34 ++++++++++++++++++++
 src/crypto/x509/verify.go                | 40 +++++++++++++++---------
 src/crypto/x509/verify_test.go           |  2 +-
 3 files changed, 60 insertions(+), 16 deletions(-)

diff --git a/src/crypto/x509/name_constraints_test.go b/src/crypto/x509/name_constraints_test.go
index c59a7dc..963bc5a 100644
--- a/src/crypto/x509/name_constraints_test.go
+++ b/src/crypto/x509/name_constraints_test.go
@@ -1595,6 +1595,40 @@ var nameConstraintsTests = []nameConstraintsTest{
 			cn:   "foo.bar",
 		},
 	},
+	// #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) {
diff --git a/src/crypto/x509/verify.go b/src/crypto/x509/verify.go
index 99f38a0..88260ee 100644
--- a/src/crypto/x509/verify.go
+++ b/src/crypto/x509/verify.go
@@ -390,7 +390,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, "@") {
@@ -403,10 +403,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
@@ -433,7 +433,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) {
@@ -450,7 +450,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 {
@@ -467,6 +467,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
@@ -493,6 +498,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
@@ -512,7 +522,7 @@ func (c *Certificate) checkNameConstraints(count *int,
 	nameType string,
 	name string,
 	parsedName interface{},
-	match func(parsedName, constraint interface{}) (match bool, err error),
+	match func(parsedName, constraint interface{}, excluded bool) (match bool, err error),
 	permitted, excluded interface{}) error {
 
 	excludedValue := reflect.ValueOf(excluded)
@@ -524,7 +534,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()}
 		}
@@ -546,7 +556,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()}
 		}
 
@@ -633,8 +643,8 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
 				}
 
 				if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "email address", name, mailbox,
-					func(parsedName, constraint interface{}) (bool, error) {
-						return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string), reversedDomainsCache, reversedConstraintsCache)
+					func(parsedName, constraint interface{}, excluded bool) (bool, error) {
+						return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string), excluded, reversedDomainsCache, reversedConstraintsCache)
 					}, c.PermittedEmailAddresses, c.ExcludedEmailAddresses); err != nil {
 					return err
 				}
@@ -646,8 +656,8 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
 				}
 
 				if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "DNS name", name, name,
-					func(parsedName, constraint interface{}) (bool, error) {
-						return matchDomainConstraint(parsedName.(string), constraint.(string), reversedDomainsCache, reversedConstraintsCache)
+					func(parsedName, constraint interface{}, excluded bool) (bool, error) {
+						return matchDomainConstraint(parsedName.(string), constraint.(string), excluded, reversedDomainsCache, reversedConstraintsCache)
 					}, c.PermittedDNSDomains, c.ExcludedDNSDomains); err != nil {
 					return err
 				}
@@ -660,8 +670,8 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
 				}
 
 				if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "URI", name, uri,
-					func(parsedName, constraint interface{}) (bool, error) {
-						return matchURIConstraint(parsedName.(*url.URL), constraint.(string), reversedDomainsCache, reversedConstraintsCache)
+					func(parsedName, constraint interface{}, excluded bool) (bool, error) {
+						return matchURIConstraint(parsedName.(*url.URL), constraint.(string), excluded, reversedDomainsCache, reversedConstraintsCache)
 					}, c.PermittedURIDomains, c.ExcludedURIDomains); err != nil {
 					return err
 				}
@@ -673,7 +683,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 interface{}) (bool, error) {
+					func(parsedName, constraint interface{}, _ bool) (bool, error) {
 						return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet))
 					}, c.PermittedIPRanges, c.ExcludedIPRanges); err != nil {
 					return err
diff --git a/src/crypto/x509/verify_test.go b/src/crypto/x509/verify_test.go
index 31e8149..5f7c834 100644
--- a/src/crypto/x509/verify_test.go
+++ b/src/crypto/x509/verify_test.go
@@ -1648,7 +1648,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)
-- 
2.25.1

