From 287017acebd27203aa3218abbd11ed65c2280cf8 Mon Sep 17 00:00:00 2001 From: Roland Shoemaker Date: Mon, 24 Nov 2025 08:46:08 -0800 Subject: [PATCH] [release-branch.go1.25] 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 #76464 Fixes CVE-2025-61727 Change-Id: I42a0da010cb36d2ec9d1239ae3f61cf25eb78bba Reviewed-on: https://go-review.googlesource.com/c/go/+/724400 Reviewed-by: Nicholas Husin Reviewed-by: Nicholas Husin Reviewed-by: Daniel McCarney LUCI-TryBot-Result: Go LUCI Reviewed-by: Neal Patel --- 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 a585184516..bc91b28401 100644 --- a/src/crypto/x509/name_constraints_test.go +++ b/src/crypto/x509/name_constraints_test.go @@ -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) { diff --git a/src/crypto/x509/verify.go b/src/crypto/x509/verify.go index bf7e7ec058..9175fa4dc1 100644 --- a/src/crypto/x509/verify.go +++ b/src/crypto/x509/verify.go @@ -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 diff --git a/src/crypto/x509/verify_test.go b/src/crypto/x509/verify_test.go index 60a4cea914..6a394e46e9 100644 --- a/src/crypto/x509/verify_test.go +++ b/src/crypto/x509/verify_test.go @@ -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) -- 2.52.0