Skip to content

Commit 95c5ec6

Browse files
committed
crypto/x509: treat certificate names with trailing dots as invalid
Trailing dots are not allowed in certificate fields like CN and SANs (while they are allowed and ignored as inputs to verification APIs). Move to considering names with trailing dots in certificates as invalid hostnames. Following the rule of CL 231378, these invalid names lose wildcard processing, but can still match if there is a 1:1 match, trailing dot included, with the VerifyHostname input. They also become ignored Common Name values regardless of the GODEBUG=x509ignoreCN=X value, because we have to ignore invalid hostnames in Common Name for #24151. The error message automatically accounts for this, and doesn't suggest the environment variable. You don't get to use a legacy deprecated field AND invalid hostnames. (While at it, also consider wildcards in VerifyHostname inputs as invalid hostnames, not that it should change any observed behavior.) Change-Id: Iecdee8927df50c1d9daf904776b051de9f5e76ad Reviewed-on: https://go-review.googlesource.com/c/go/+/231380 Run-TryBot: Filippo Valsorda <filippo@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Katie Hockman <katie@golang.org>
1 parent d65e1b2 commit 95c5ec6

File tree

3 files changed

+40
-31
lines changed

3 files changed

+40
-31
lines changed

src/crypto/x509/verify.go

+14-10
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,11 @@ func (h HostnameError) Error() string {
110110
c := h.Certificate
111111

112112
if !c.hasSANExtension() && matchHostnames(c.Subject.CommonName, h.Host) {
113-
if !ignoreCN && !validHostname(c.Subject.CommonName) {
113+
if !ignoreCN && !validHostnamePattern(c.Subject.CommonName) {
114114
// This would have validated, if it weren't for the validHostname check on Common Name.
115115
return "x509: Common Name is not a valid hostname: " + c.Subject.CommonName
116116
}
117-
if ignoreCN && validHostname(c.Subject.CommonName) {
117+
if ignoreCN && validHostnamePattern(c.Subject.CommonName) {
118118
// This would have validated if x509ignoreCN=0 were set.
119119
return "x509: certificate relies on legacy Common Name field, " +
120120
"use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0"
@@ -902,12 +902,16 @@ func (c *Certificate) buildChains(cache map[*Certificate][][]*Certificate, curre
902902
return
903903
}
904904

905+
func validHostnamePattern(host string) bool { return validHostname(host, true) }
906+
func validHostnameInput(host string) bool { return validHostname(host, false) }
907+
905908
// validHostname reports whether host is a valid hostname that can be matched or
906909
// matched against according to RFC 6125 2.2, with some leniency to accommodate
907910
// legacy values.
908-
func validHostname(host string) bool {
909-
host = strings.TrimSuffix(host, ".")
910-
911+
func validHostname(host string, isPattern bool) bool {
912+
if !isPattern {
913+
host = strings.TrimSuffix(host, ".")
914+
}
911915
if len(host) == 0 {
912916
return false
913917
}
@@ -917,7 +921,7 @@ func validHostname(host string) bool {
917921
// Empty label.
918922
return false
919923
}
920-
if i == 0 && part == "*" {
924+
if isPattern && i == 0 && part == "*" {
921925
// Only allow full left-most wildcards, as those are the only ones
922926
// we match, and matching literal '*' characters is probably never
923927
// the expected behavior.
@@ -957,7 +961,7 @@ func validHostname(host string) bool {
957961
// constraints if there is no risk the CN would be matched as a hostname.
958962
// See NameConstraintsWithoutSANs and issue 24151.
959963
func (c *Certificate) commonNameAsHostname() bool {
960-
return !ignoreCN && !c.hasSANExtension() && validHostname(c.Subject.CommonName)
964+
return !ignoreCN && !c.hasSANExtension() && validHostnamePattern(c.Subject.CommonName)
961965
}
962966

963967
func matchExactly(hostA, hostB string) bool {
@@ -968,7 +972,7 @@ func matchExactly(hostA, hostB string) bool {
968972
}
969973

970974
func matchHostnames(pattern, host string) bool {
971-
pattern = toLowerCaseASCII(strings.TrimSuffix(pattern, "."))
975+
pattern = toLowerCaseASCII(pattern)
972976
host = toLowerCaseASCII(strings.TrimSuffix(host, "."))
973977

974978
if len(pattern) == 0 || len(host) == 0 {
@@ -1061,15 +1065,15 @@ func (c *Certificate) VerifyHostname(h string) error {
10611065
}
10621066

10631067
candidateName := toLowerCaseASCII(h) // Save allocations inside the loop.
1064-
validCandidateName := validHostname(candidateName)
1068+
validCandidateName := validHostnameInput(candidateName)
10651069

10661070
for _, match := range names {
10671071
// Ideally, we'd only match valid hostnames according to RFC 6125 like
10681072
// browsers (more or less) do, but in practice Go is used in a wider
10691073
// array of contexts and can't even assume DNS resolution. Instead,
10701074
// always allow perfect matches, and only apply wildcard and trailing
10711075
// dot processing to valid hostnames.
1072-
if validCandidateName && validHostname(match) {
1076+
if validCandidateName && validHostnamePattern(match) {
10731077
if matchHostnames(match, candidateName) {
10741078
return nil
10751079
}

src/crypto/x509/verify_test.go

+22-17
Original file line numberDiff line numberDiff line change
@@ -1987,26 +1987,31 @@ CCqGSM49BAMCA0gAMEUCIQClA3d4tdrDu9Eb5ZBpgyC+fU1xTZB0dKQHz6M5fPZA
19871987

19881988
func TestValidHostname(t *testing.T) {
19891989
tests := []struct {
1990-
host string
1991-
want bool
1990+
host string
1991+
validInput, validPattern bool
19921992
}{
1993-
{"example.com", true},
1994-
{"eXample123-.com", true},
1995-
{"-eXample123-.com", false},
1996-
{"", false},
1997-
{".", false},
1998-
{"example..com", false},
1999-
{".example.com", false},
2000-
{"*.example.com", true},
2001-
{"*foo.example.com", false},
2002-
{"foo.*.example.com", false},
2003-
{"exa_mple.com", true},
2004-
{"foo,bar", false},
2005-
{"project-dev:us-central1:main", true},
1993+
{host: "example.com", validInput: true, validPattern: true},
1994+
{host: "eXample123-.com", validInput: true, validPattern: true},
1995+
{host: "-eXample123-.com"},
1996+
{host: ""},
1997+
{host: "."},
1998+
{host: "example..com"},
1999+
{host: ".example.com"},
2000+
{host: "example.com.", validInput: true},
2001+
{host: "*.example.com."},
2002+
{host: "*.example.com", validPattern: true},
2003+
{host: "*foo.example.com"},
2004+
{host: "foo.*.example.com"},
2005+
{host: "exa_mple.com", validInput: true, validPattern: true},
2006+
{host: "foo,bar"},
2007+
{host: "project-dev:us-central1:main", validInput: true, validPattern: true},
20062008
}
20072009
for _, tt := range tests {
2008-
if got := validHostname(tt.host); got != tt.want {
2009-
t.Errorf("validHostname(%q) = %v, want %v", tt.host, got, tt.want)
2010+
if got := validHostnamePattern(tt.host); got != tt.validPattern {
2011+
t.Errorf("validHostnamePattern(%q) = %v, want %v", tt.host, got, tt.validPattern)
2012+
}
2013+
if got := validHostnameInput(tt.host); got != tt.validInput {
2014+
t.Errorf("validHostnameInput(%q) = %v, want %v", tt.host, got, tt.validInput)
20102015
}
20112016
}
20122017
}

src/crypto/x509/x509_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -369,10 +369,10 @@ var matchHostnamesTests = []matchHostnamesTest{
369369
{".", "", false},
370370
{".", ".", false},
371371
{"example.com", "example.com.", true},
372-
{"example.com.", "example.com", true},
373-
{"example.com.", "example.com.", true},
374-
{"*.com.", "example.com.", true},
375-
{"*.com.", "example.com", true},
372+
{"example.com.", "example.com", false},
373+
{"example.com.", "example.com.", true}, // perfect matches allow trailing dots in patterns
374+
{"*.com.", "example.com.", false},
375+
{"*.com.", "example.com", false},
376376
{"*.com", "example.com", true},
377377
{"*.com", "example.com.", true},
378378
{"foo:bar", "foo:bar", true},

0 commit comments

Comments
 (0)