]> Cypherpunks repositories - gostls13.git/commitdiff
[release-branch.go1.25] crypto/x509: improve domain name verification
authorNeal Patel <nealpatel@google.com>
Mon, 15 Sep 2025 20:31:22 +0000 (16:31 -0400)
committerGopher Robot <gobot@golang.org>
Tue, 7 Oct 2025 18:02:15 +0000 (11:02 -0700)
Don't use domainToReverseLabels to check if domain names are valid,
since it is not particularly performant, and can contribute to DoS
vectors. Instead just iterate over the name and enforce the properties
we care about.

This also enforces that DNS names, both in SANs and name constraints,
are valid. We previously allowed invalid SANs, because some
intermediates had these weird names (see #23995), but there are
currently no trusted intermediates that have this property, and since we
target the web PKI, supporting this particular case is not a high
priority.

Thank you to Jakub Ciolek for reporting this issue.

Fixes CVE-2025-58187
For #75681
Fixes #75715

Change-Id: I6ebce847dcbe5fc63ef2f9a74f53f11c4c56d3d1
Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2820
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Roland Shoemaker <bracewell@google.com>
Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2981
Commit-Queue: Roland Shoemaker <bracewell@google.com>
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/709848
Auto-Submit: Michael Pratt <mpratt@google.com>
TryBot-Bypass: Michael Pratt <mpratt@google.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
src/crypto/x509/name_constraints_test.go
src/crypto/x509/parser.go
src/crypto/x509/parser_test.go
src/crypto/x509/verify.go

index a5851845164d1021f417e937d08aef9e943fa346..831fcbc8d2eb8286ed8fa088b76c389c529b80f3 100644 (file)
@@ -1456,63 +1456,7 @@ var nameConstraintsTests = []nameConstraintsTest{
                expectedError: "incompatible key usage",
        },
 
-       // An invalid DNS SAN should be detected only at validation time so
-       // that we can process CA certificates in the wild that have invalid SANs.
-       // See https://github.com/golang/go/issues/23995
-
-       // #77: an invalid DNS or mail SAN will not be detected if name constraint
-       // checking is not triggered.
-       {
-               roots: make([]constraintsSpec, 1),
-               intermediates: [][]constraintsSpec{
-                       {
-                               {},
-                       },
-               },
-               leaf: leafSpec{
-                       sans: []string{"dns:this is invalid", "email:this @ is invalid"},
-               },
-       },
-
-       // #78: an invalid DNS SAN will be detected if any name constraint checking
-       // is triggered.
-       {
-               roots: []constraintsSpec{
-                       {
-                               bad: []string{"uri:"},
-                       },
-               },
-               intermediates: [][]constraintsSpec{
-                       {
-                               {},
-                       },
-               },
-               leaf: leafSpec{
-                       sans: []string{"dns:this is invalid"},
-               },
-               expectedError: "cannot parse dnsName",
-       },
-
-       // #79: an invalid email SAN will be detected if any name constraint
-       // checking is triggered.
-       {
-               roots: []constraintsSpec{
-                       {
-                               bad: []string{"uri:"},
-                       },
-               },
-               intermediates: [][]constraintsSpec{
-                       {
-                               {},
-                       },
-               },
-               leaf: leafSpec{
-                       sans: []string{"email:this @ is invalid"},
-               },
-               expectedError: "cannot parse rfc822Name",
-       },
-
-       // #80: if several EKUs are requested, satisfying any of them is sufficient.
+       // #77: if several EKUs are requested, satisfying any of them is sufficient.
        {
                roots: make([]constraintsSpec, 1),
                intermediates: [][]constraintsSpec{
@@ -1527,7 +1471,7 @@ var nameConstraintsTests = []nameConstraintsTest{
                requestedEKUs: []ExtKeyUsage{ExtKeyUsageClientAuth, ExtKeyUsageEmailProtection},
        },
 
-       // #81: EKUs that are not asserted in VerifyOpts are not required to be
+       // #78: EKUs that are not asserted in VerifyOpts are not required to be
        // nested.
        {
                roots: make([]constraintsSpec, 1),
@@ -1546,7 +1490,7 @@ var nameConstraintsTests = []nameConstraintsTest{
                },
        },
 
-       // #82: a certificate without SANs and CN is accepted in a constrained chain.
+       // #79: a certificate without SANs and CN is accepted in a constrained chain.
        {
                roots: []constraintsSpec{
                        {
@@ -1563,7 +1507,7 @@ var nameConstraintsTests = []nameConstraintsTest{
                },
        },
 
-       // #83: a certificate without SANs and with a CN that does not parse as a
+       // #80: a certificate without SANs and with a CN that does not parse as a
        // hostname is accepted in a constrained chain.
        {
                roots: []constraintsSpec{
@@ -1582,7 +1526,7 @@ var nameConstraintsTests = []nameConstraintsTest{
                },
        },
 
-       // #84: a certificate with SANs and CN is accepted in a constrained chain.
+       // #81: a certificate with SANs and CN is accepted in a constrained chain.
        {
                roots: []constraintsSpec{
                        {
@@ -1600,14 +1544,7 @@ var nameConstraintsTests = []nameConstraintsTest{
                },
        },
 
-       // #85: .example.com is an invalid DNS name, it should not match the
-       // constraint example.com.
-       {
-               roots:         []constraintsSpec{{ok: []string{"dns:example.com"}}},
-               leaf:          leafSpec{sans: []string{"dns:.example.com"}},
-               expectedError: "cannot parse dnsName \".example.com\"",
-       },
-       // #86: URIs with IPv6 addresses with zones and ports are rejected
+       // #82: URIs with IPv6 addresses with zones and ports are rejected
        {
                roots: []constraintsSpec{
                        {
index 4abcc1b7b590e2442d4b35989b0da84baa2b2d94..9d6bfd6e95f94980c07e948ca5ba39386c28b1b5 100644 (file)
@@ -413,10 +413,14 @@ func parseSANExtension(der cryptobyte.String) (dnsNames, emailAddresses []string
                        if err := isIA5String(email); err != nil {
                                return errors.New("x509: SAN rfc822Name is malformed")
                        }
+                       parsed, ok := parseRFC2821Mailbox(email)
+                       if !ok || (ok && !domainNameValid(parsed.domain, false)) {
+                               return errors.New("x509: SAN rfc822Name is malformed")
+                       }
                        emailAddresses = append(emailAddresses, email)
                case nameTypeDNS:
                        name := string(data)
-                       if err := isIA5String(name); err != nil {
+                       if err := isIA5String(name); err != nil || (err == nil && !domainNameValid(name, false)) {
                                return errors.New("x509: SAN dNSName is malformed")
                        }
                        dnsNames = append(dnsNames, string(name))
@@ -426,14 +430,9 @@ func parseSANExtension(der cryptobyte.String) (dnsNames, emailAddresses []string
                                return errors.New("x509: SAN uniformResourceIdentifier is malformed")
                        }
                        uri, err := url.Parse(uriStr)
-                       if err != nil {
+                       if err != nil || (err == nil && uri.Host != "" && !domainNameValid(uri.Host, false)) {
                                return fmt.Errorf("x509: cannot parse URI %q: %s", uriStr, err)
                        }
-                       if len(uri.Host) > 0 {
-                               if _, ok := domainToReverseLabels(uri.Host); !ok {
-                                       return fmt.Errorf("x509: cannot parse URI %q: invalid domain", uriStr)
-                               }
-                       }
                        uris = append(uris, uri)
                case nameTypeIP:
                        switch len(data) {
@@ -598,15 +597,7 @@ func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandle
                                        return nil, nil, nil, nil, errors.New("x509: invalid constraint value: " + err.Error())
                                }
 
-                               trimmedDomain := domain
-                               if len(trimmedDomain) > 0 && trimmedDomain[0] == '.' {
-                                       // constraints can have a leading
-                                       // period to exclude the domain
-                                       // itself, but that's not valid in a
-                                       // normal domain name.
-                                       trimmedDomain = trimmedDomain[1:]
-                               }
-                               if _, ok := domainToReverseLabels(trimmedDomain); !ok {
+                               if !domainNameValid(domain, true) {
                                        return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse dnsName constraint %q", domain)
                                }
                                dnsNames = append(dnsNames, domain)
@@ -647,12 +638,7 @@ func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandle
                                                return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse rfc822Name constraint %q", constraint)
                                        }
                                } else {
-                                       // Otherwise it's a domain name.
-                                       domain := constraint
-                                       if len(domain) > 0 && domain[0] == '.' {
-                                               domain = domain[1:]
-                                       }
-                                       if _, ok := domainToReverseLabels(domain); !ok {
+                                       if !domainNameValid(constraint, true) {
                                                return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse rfc822Name constraint %q", constraint)
                                        }
                                }
@@ -668,15 +654,7 @@ func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandle
                                        return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse URI constraint %q: cannot be IP address", domain)
                                }
 
-                               trimmedDomain := domain
-                               if len(trimmedDomain) > 0 && trimmedDomain[0] == '.' {
-                                       // constraints can have a leading
-                                       // period to exclude the domain itself,
-                                       // but that's not valid in a normal
-                                       // domain name.
-                                       trimmedDomain = trimmedDomain[1:]
-                               }
-                               if _, ok := domainToReverseLabels(trimmedDomain); !ok {
+                               if !domainNameValid(domain, true) {
                                        return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse URI constraint %q", domain)
                                }
                                uriDomains = append(uriDomains, domain)
@@ -1317,3 +1295,40 @@ func ParseRevocationList(der []byte) (*RevocationList, error) {
 
        return rl, nil
 }
+
+// domainNameValid does minimal domain name validity checking. In particular it
+// enforces the following properties:
+//   - names cannot have the trailing period
+//   - names can only have a leading period if constraint is true
+//   - names must be <= 253 characters
+//   - names cannot have empty labels
+//   - names cannot labels that are longer than 63 characters
+//
+// Note that this does not enforce the LDH requirements for domain names.
+func domainNameValid(s string, constraint bool) bool {
+       if len(s) == 0 && constraint {
+               return true
+       }
+       if len(s) == 0 || (!constraint && s[0] == '.') || s[len(s)-1] == '.' || len(s) > 253 {
+               return false
+       }
+       lastDot := -1
+       if constraint && s[0] == '.' {
+               s = s[1:]
+       }
+
+       for i := 0; i <= len(s); i++ {
+               if i == len(s) || s[i] == '.' {
+                       labelLen := i
+                       if lastDot >= 0 {
+                               labelLen -= lastDot + 1
+                       }
+                       if labelLen == 0 || labelLen > 63 {
+                               return false
+                       }
+                       lastDot = i
+               }
+       }
+
+       return true
+}
index 3b9d9aed826b01bbfbf4e2cab6f4a3d2c743f637..1b553e362e48a0a4ae4170e820d4404ae51d6e7b 100644 (file)
@@ -8,6 +8,7 @@ import (
        "encoding/asn1"
        "encoding/pem"
        "os"
+       "strings"
        "testing"
 
        cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
@@ -251,3 +252,45 @@ d5l1tRhScKu2NBgm74nYmJxJYgvuTA38wGhRrGU=
                }
        }
 }
+
+func TestDomainNameValid(t *testing.T) {
+       for _, tc := range []struct {
+               name       string
+               dnsName    string
+               constraint bool
+               valid      bool
+       }{
+               {"empty name, name", "", false, false},
+               {"empty name, constraint", "", true, true},
+               {"empty label, name", "a..a", false, false},
+               {"empty label, constraint", "a..a", true, false},
+               {"period, name", ".", false, false},
+               {"period, constraint", ".", true, false}, // TODO(roland): not entirely clear if this is a valid constraint (require at least one label?)
+               {"valid, name", "a.b.c", false, true},
+               {"valid, constraint", "a.b.c", true, true},
+               {"leading period, name", ".a.b.c", false, false},
+               {"leading period, constraint", ".a.b.c", true, true},
+               {"trailing period, name", "a.", false, false},
+               {"trailing period, constraint", "a.", true, false},
+               {"bare label, name", "a", false, true},
+               {"bare label, constraint", "a", true, true},
+               {"254 char label, name", strings.Repeat("a.a", 84) + "aaa", false, false},
+               {"254 char label, constraint", strings.Repeat("a.a", 84) + "aaa", true, false},
+               {"253 char label, name", strings.Repeat("a.a", 84) + "aa", false, false},
+               {"253 char label, constraint", strings.Repeat("a.a", 84) + "aa", true, false},
+               {"64 char single label, name", strings.Repeat("a", 64), false, false},
+               {"64 char single label, constraint", strings.Repeat("a", 64), true, false},
+               {"63 char single label, name", strings.Repeat("a", 63), false, true},
+               {"63 char single label, constraint", strings.Repeat("a", 63), true, true},
+               {"64 char label, name", "a." + strings.Repeat("a", 64), false, false},
+               {"64 char label, constraint", "a." + strings.Repeat("a", 64), true, false},
+               {"63 char label, name", "a." + strings.Repeat("a", 63), false, true},
+               {"63 char label, constraint", "a." + strings.Repeat("a", 63), true, true},
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       if tc.valid != domainNameValid(tc.dnsName, tc.constraint) {
+                               t.Errorf("domainNameValid(%q, %t) = %v; want %v", tc.dnsName, tc.constraint, !tc.valid, tc.valid)
+                       }
+               })
+       }
+}
index 755c1db96c1edbfa83323427446ecd619485e976..058153fbe73461295e8d5cdc4c3eedaf13b6a811 100644 (file)
@@ -391,6 +391,7 @@ func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {
 // domainToReverseLabels converts a textual domain name like foo.example.com to
 // the list of labels in reverse order, e.g. ["com", "example", "foo"].
 func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {
+       reverseLabels = make([]string, 0, strings.Count(domain, ".")+1)
        for len(domain) > 0 {
                if i := strings.LastIndexByte(domain, '.'); i == -1 {
                        reverseLabels = append(reverseLabels, domain)