Skip to content

crypto/x509: TLS validation fails for FQDNs with trailing dot #75828

@nicholasngai

Description

@nicholasngai

Go version

go version go1.25.2 darwin/arm64

Output of go env in your module/workspace:

AR='ar'
CC='cc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='c++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/nngai/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/nngai/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/tmp/go-build1093866497=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/Users/nngai/Downloads/ssl/go.mod'
GOMODCACHE='/Users/nngai/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/nngai/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/opt/homebrew/Cellar/go/1.25.2/libexec'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/nngai/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/opt/homebrew/Cellar/go/1.25.2/libexec/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.25.2'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

  1. Generate a standards-compliant X.509 certificate for the SANs localhost, localhost.. Note that there are two valid SANs here: localhost and localhost.. The latter contains a trailing dot, which is a valid FQDN though not a valid SAN according to RFC 2459.
  2. Start a TLS server serving TLS traffic off of the newly generated cert and key:
openssl s_server -cert cert.pem -key privkey.pem -port 1234 -www
  1. Attempt to form a TLS client connection to the TLS server from Go 1.25.2.
    The following code reproduces the server/client behavior:
package main

import (
	"fmt"
	"net/http"
)

func main() {
	res, err := http.Get("https://localhost:1234/")
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	fmt.Println("Response received successfully")
}

Here is the openssl -noout -text output of the certificate used:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 7 (0x7)
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: CN=Nicholas Ngai’s CA, O=Roblox, ST=CA, C=US, L=San Mateo, emailAddress=nngai@roblox.com
        Validity
            Not Before: Oct  9 16:07:20 2025 GMT
            Not After : Oct  9 16:07:20 2026 GMT
        Subject: CN=localhost, C=US
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (384 bit)
                pub:
                    04:a6:36:6c:83:c9:71:fb:10:78:84:37:f4:9f:24:
                    43:96:0b:04:e7:2b:d5:1a:7f:8c:99:2b:18:84:b3:
                    c9:c5:8e:04:96:49:d5:5a:d0:a3:52:00:83:32:76:
                    3f:01:d4:88:b9:e2:8d:de:f8:86:3a:dc:d5:e6:d6:
                    3b:56:b2:da:e5:af:96:22:86:f3:3b:1f:49:c1:a4:
                    da:57:3d:25:ff:af:4f:b1:59:99:2b:91:e1:ac:b7:
                    8c:a7:7b:c9:28:5e:53
                ASN1 OID: secp384r1
                NIST CURVE: P-384
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication
            X509v3 Subject Key Identifier: 
                E4:C9:79:D8:F5:5A:E7:74:16:33:AD:3C:6C:CE:6A:7E:1A:0A:3F:09
            X509v3 Authority Key Identifier: 
                5B:4C:2B:2A:1E:EB:8D:5E:92:D1:DB:10:75:18:36:3F:7B:10:3C:C7
            X509v3 Subject Alternative Name: 
                DNS:localhost, DNS:localhost.
    Signature Algorithm: ecdsa-with-SHA256
    Signature Value:
        30:66:02:31:00:a7:cb:59:4a:db:62:8a:78:51:5e:fd:f4:ec:
        d7:cf:4e:05:11:d1:ea:eb:ca:5d:3b:e9:6c:c2:16:60:00:db:
        43:b1:bb:bb:56:01:86:0a:98:0b:66:a0:bc:f4:d4:bc:fe:02:
        31:00:91:e8:cc:88:49:15:47:bb:97:08:21:0e:5a:e9:68:22:
        f7:f8:12:09:95:bb:98:5c:7e:46:29:eb:38:af:79:d1:84:a7:
        cf:93:5c:6f:2c:45:57:d0:df:a3:44:23:d4:96

What did you see happen?

$ GOTOOLCHAIN=go1.24.7 go run main.go
Response received successfully

$ GOTOOLCHAIN=go1.25.1 go run main.go
Response received successfully

$ GOTOOLCHAIN=go1.24.8 go run main.go
panic: Get "https://localhost.:1234/": tls: failed to parse certificate from server: x509: SAN dNSName is malformed

goroutine 1 [running]:
main.main()
	/Users/nngai/Downloads/ssl/main.go:11 +0xcc
exit status 2

$ GOTOOLCHAIN=1.25.2 go run main.go
panic: Get "https://localhost.:1234/": tls: failed to parse certificate from server: x509: SAN dNSName is malformed

goroutine 1 [running]:
main.main()
	/Users/nngai/Downloads/ssl/main.go:11 +0xcc
exit status 2

What did you expect to see?

Go 1.24.8 and Go 1.25.2 should preserve observable validation behavior for SANs. While RFC 2459 disallows trailing dot by referencing RFC 1034’s “preferred name syntax” which doesn’t allow for trailing dot, this behavior is minor breaking behavior, and we should consider preserving it until Go 1.26.

Relevant issue: #75681
Relevant CL: https://go-review.googlesource.com/c/go/+/709854

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.CriticalA critical problem that affects the availability or correctness of production systems built using GoNeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions