Skip to content

Commit 70682f3

Browse files
thatnealpatelrickystewart
authored andcommitted
[release-branch.go1.24] crypto/x509: improve domain name verification
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 golang#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 golang#75681 Fixes golang#75714 Change-Id: I6ebce847dcbe5fc63ef2f9a74f53f11c4c56d3d1 Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2820 Reviewed-by: Damien Neil <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2982 Reviewed-by: Nicholas Husin <[email protected]> Reviewed-on: https://go-review.googlesource.com/c/go/+/709839 Auto-Submit: Michael Pratt <[email protected]> Reviewed-by: Carlos Amedee <[email protected]> TryBot-Bypass: Michael Pratt <[email protected]>
1 parent 7422bfb commit 70682f3

File tree

3 files changed

+53
-100
lines changed

3 files changed

+53
-100
lines changed

src/crypto/x509/name_constraints_test.go

Lines changed: 6 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1456,63 +1456,7 @@ var nameConstraintsTests = []nameConstraintsTest{
14561456
expectedError: "incompatible key usage",
14571457
},
14581458

1459-
// An invalid DNS SAN should be detected only at validation time so
1460-
// that we can process CA certificates in the wild that have invalid SANs.
1461-
// See https://github.com/golang/go/issues/23995
1462-
1463-
// #77: an invalid DNS or mail SAN will not be detected if name constraint
1464-
// checking is not triggered.
1465-
{
1466-
roots: make([]constraintsSpec, 1),
1467-
intermediates: [][]constraintsSpec{
1468-
{
1469-
{},
1470-
},
1471-
},
1472-
leaf: leafSpec{
1473-
sans: []string{"dns:this is invalid", "email:this @ is invalid"},
1474-
},
1475-
},
1476-
1477-
// #78: an invalid DNS SAN will be detected if any name constraint checking
1478-
// is triggered.
1479-
{
1480-
roots: []constraintsSpec{
1481-
{
1482-
bad: []string{"uri:"},
1483-
},
1484-
},
1485-
intermediates: [][]constraintsSpec{
1486-
{
1487-
{},
1488-
},
1489-
},
1490-
leaf: leafSpec{
1491-
sans: []string{"dns:this is invalid"},
1492-
},
1493-
expectedError: "cannot parse dnsName",
1494-
},
1495-
1496-
// #79: an invalid email SAN will be detected if any name constraint
1497-
// checking is triggered.
1498-
{
1499-
roots: []constraintsSpec{
1500-
{
1501-
bad: []string{"uri:"},
1502-
},
1503-
},
1504-
intermediates: [][]constraintsSpec{
1505-
{
1506-
{},
1507-
},
1508-
},
1509-
leaf: leafSpec{
1510-
sans: []string{"email:this @ is invalid"},
1511-
},
1512-
expectedError: "cannot parse rfc822Name",
1513-
},
1514-
1515-
// #80: if several EKUs are requested, satisfying any of them is sufficient.
1459+
// #77: if several EKUs are requested, satisfying any of them is sufficient.
15161460
{
15171461
roots: make([]constraintsSpec, 1),
15181462
intermediates: [][]constraintsSpec{
@@ -1527,7 +1471,7 @@ var nameConstraintsTests = []nameConstraintsTest{
15271471
requestedEKUs: []ExtKeyUsage{ExtKeyUsageClientAuth, ExtKeyUsageEmailProtection},
15281472
},
15291473

1530-
// #81: EKUs that are not asserted in VerifyOpts are not required to be
1474+
// #78: EKUs that are not asserted in VerifyOpts are not required to be
15311475
// nested.
15321476
{
15331477
roots: make([]constraintsSpec, 1),
@@ -1546,7 +1490,7 @@ var nameConstraintsTests = []nameConstraintsTest{
15461490
},
15471491
},
15481492

1549-
// #82: a certificate without SANs and CN is accepted in a constrained chain.
1493+
// #79: a certificate without SANs and CN is accepted in a constrained chain.
15501494
{
15511495
roots: []constraintsSpec{
15521496
{
@@ -1563,7 +1507,7 @@ var nameConstraintsTests = []nameConstraintsTest{
15631507
},
15641508
},
15651509

1566-
// #83: a certificate without SANs and with a CN that does not parse as a
1510+
// #80: a certificate without SANs and with a CN that does not parse as a
15671511
// hostname is accepted in a constrained chain.
15681512
{
15691513
roots: []constraintsSpec{
@@ -1582,7 +1526,7 @@ var nameConstraintsTests = []nameConstraintsTest{
15821526
},
15831527
},
15841528

1585-
// #84: a certificate with SANs and CN is accepted in a constrained chain.
1529+
// #81: a certificate with SANs and CN is accepted in a constrained chain.
15861530
{
15871531
roots: []constraintsSpec{
15881532
{
@@ -1600,14 +1544,7 @@ var nameConstraintsTests = []nameConstraintsTest{
16001544
},
16011545
},
16021546

1603-
// #85: .example.com is an invalid DNS name, it should not match the
1604-
// constraint example.com.
1605-
{
1606-
roots: []constraintsSpec{{ok: []string{"dns:example.com"}}},
1607-
leaf: leafSpec{sans: []string{"dns:.example.com"}},
1608-
expectedError: "cannot parse dnsName \".example.com\"",
1609-
},
1610-
// #86: URIs with IPv6 addresses with zones and ports are rejected
1547+
// #82: URIs with IPv6 addresses with zones and ports are rejected
16111548
{
16121549
roots: []constraintsSpec{
16131550
{

src/crypto/x509/parser.go

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -379,10 +379,14 @@ func parseSANExtension(der cryptobyte.String) (dnsNames, emailAddresses []string
379379
if err := isIA5String(email); err != nil {
380380
return errors.New("x509: SAN rfc822Name is malformed")
381381
}
382+
parsed, ok := parseRFC2821Mailbox(email)
383+
if !ok || (ok && !domainNameValid(parsed.domain, false)) {
384+
return errors.New("x509: SAN rfc822Name is malformed")
385+
}
382386
emailAddresses = append(emailAddresses, email)
383387
case nameTypeDNS:
384388
name := string(data)
385-
if err := isIA5String(name); err != nil {
389+
if err := isIA5String(name); err != nil || (err == nil && !domainNameValid(name, false)) {
386390
return errors.New("x509: SAN dNSName is malformed")
387391
}
388392
dnsNames = append(dnsNames, string(name))
@@ -392,14 +396,9 @@ func parseSANExtension(der cryptobyte.String) (dnsNames, emailAddresses []string
392396
return errors.New("x509: SAN uniformResourceIdentifier is malformed")
393397
}
394398
uri, err := url.Parse(uriStr)
395-
if err != nil {
399+
if err != nil || (err == nil && uri.Host != "" && !domainNameValid(uri.Host, false)) {
396400
return fmt.Errorf("x509: cannot parse URI %q: %s", uriStr, err)
397401
}
398-
if len(uri.Host) > 0 {
399-
if _, ok := domainToReverseLabels(uri.Host); !ok {
400-
return fmt.Errorf("x509: cannot parse URI %q: invalid domain", uriStr)
401-
}
402-
}
403402
uris = append(uris, uri)
404403
case nameTypeIP:
405404
switch len(data) {
@@ -559,15 +558,7 @@ func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandle
559558
return nil, nil, nil, nil, errors.New("x509: invalid constraint value: " + err.Error())
560559
}
561560

562-
trimmedDomain := domain
563-
if len(trimmedDomain) > 0 && trimmedDomain[0] == '.' {
564-
// constraints can have a leading
565-
// period to exclude the domain
566-
// itself, but that's not valid in a
567-
// normal domain name.
568-
trimmedDomain = trimmedDomain[1:]
569-
}
570-
if _, ok := domainToReverseLabels(trimmedDomain); !ok {
561+
if !domainNameValid(domain, true) {
571562
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse dnsName constraint %q", domain)
572563
}
573564
dnsNames = append(dnsNames, domain)
@@ -608,12 +599,7 @@ func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandle
608599
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse rfc822Name constraint %q", constraint)
609600
}
610601
} else {
611-
// Otherwise it's a domain name.
612-
domain := constraint
613-
if len(domain) > 0 && domain[0] == '.' {
614-
domain = domain[1:]
615-
}
616-
if _, ok := domainToReverseLabels(domain); !ok {
602+
if !domainNameValid(constraint, true) {
617603
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse rfc822Name constraint %q", constraint)
618604
}
619605
}
@@ -629,15 +615,7 @@ func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandle
629615
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse URI constraint %q: cannot be IP address", domain)
630616
}
631617

632-
trimmedDomain := domain
633-
if len(trimmedDomain) > 0 && trimmedDomain[0] == '.' {
634-
// constraints can have a leading
635-
// period to exclude the domain itself,
636-
// but that's not valid in a normal
637-
// domain name.
638-
trimmedDomain = trimmedDomain[1:]
639-
}
640-
if _, ok := domainToReverseLabels(trimmedDomain); !ok {
618+
if !domainNameValid(domain, true) {
641619
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse URI constraint %q", domain)
642620
}
643621
uriDomains = append(uriDomains, domain)
@@ -1229,3 +1207,40 @@ func ParseRevocationList(der []byte) (*RevocationList, error) {
12291207

12301208
return rl, nil
12311209
}
1210+
1211+
// domainNameValid does minimal domain name validity checking. In particular it
1212+
// enforces the following properties:
1213+
// - names cannot have the trailing period
1214+
// - names can only have a leading period if constraint is true
1215+
// - names must be <= 253 characters
1216+
// - names cannot have empty labels
1217+
// - names cannot labels that are longer than 63 characters
1218+
//
1219+
// Note that this does not enforce the LDH requirements for domain names.
1220+
func domainNameValid(s string, constraint bool) bool {
1221+
if len(s) == 0 && constraint {
1222+
return true
1223+
}
1224+
if len(s) == 0 || (!constraint && s[0] == '.') || s[len(s)-1] == '.' || len(s) > 253 {
1225+
return false
1226+
}
1227+
lastDot := -1
1228+
if constraint && s[0] == '.' {
1229+
s = s[1:]
1230+
}
1231+
1232+
for i := 0; i <= len(s); i++ {
1233+
if i == len(s) || s[i] == '.' {
1234+
labelLen := i
1235+
if lastDot >= 0 {
1236+
labelLen -= lastDot + 1
1237+
}
1238+
if labelLen == 0 || labelLen > 63 {
1239+
return false
1240+
}
1241+
lastDot = i
1242+
}
1243+
}
1244+
1245+
return true
1246+
}

src/crypto/x509/verify.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {
360360
// domainToReverseLabels converts a textual domain name like foo.example.com to
361361
// the list of labels in reverse order, e.g. ["com", "example", "foo"].
362362
func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {
363+
reverseLabels = make([]string, 0, strings.Count(domain, ".")+1)
363364
for len(domain) > 0 {
364365
if i := strings.LastIndexByte(domain, '.'); i == -1 {
365366
reverseLabels = append(reverseLabels, domain)

0 commit comments

Comments
 (0)