golang/go · error

tls: client used the legacy version field to negotiate TLS 1

Error message

tls: client used the legacy version field to negotiate TLS 1.3

What it means

RFC 8446 §4.2.1 requires a TLS 1.3 client to advertise its supported versions via the supported_versions extension, NOT the legacy ClientHello.version field (which must stay 0x0303). This error fires in processClientHello when hs.clientHello.supportedVersions is empty — the client tried to negotiate TLS 1.3 through the legacy field. The server sends an illegal_parameter alert and aborts.

Source

Thrown at src/crypto/tls/handshake_server_tls13.go:119

	c.isHandshakeComplete.Store(true)

	return nil
}

func (hs *serverHandshakeStateTLS13) processClientHello() error {
	c := hs.c

	hs.hello = new(serverHelloMsg)

	// TLS 1.3 froze the ServerHello.legacy_version field, and uses
	// supported_versions instead. See RFC 8446, sections 4.1.3 and 4.2.1.
	hs.hello.vers = VersionTLS12
	hs.hello.supportedVersion = c.vers

	if len(hs.clientHello.supportedVersions) == 0 {
		c.sendAlert(alertIllegalParameter)
		return errors.New("tls: client used the legacy version field to negotiate TLS 1.3")
	}

	// Abort if the client is doing a fallback and landing lower than what we
	// support. See RFC 7507, which however does not specify the interaction
	// with supported_versions. The only difference is that with
	// supported_versions a client has a chance to attempt a [TLS 1.2, TLS 1.4]
	// handshake in case TLS 1.3 is broken but 1.2 is not. Alas, in that case,
	// it will have to drop the TLS_FALLBACK_SCSV protection if it falls back to
	// TLS 1.2, because a TLS 1.3 server would abort here. The situation before
	// supported_versions was not better because there was just no way to do a
	// TLS 1.4 handshake without risking the server selecting TLS 1.3.
	for _, id := range hs.clientHello.cipherSuites {
		if id == TLS_FALLBACK_SCSV {
			// Use c.vers instead of max(supported_versions) because an attacker
			// could defeat this by adding an arbitrary high version otherwise.
			if c.vers < c.config.maxSupportedVersion(roleServer, c.quic != nil) {
				c.sendAlert(alertInappropriateFallback)
				return errors.New("tls: client using inappropriate protocol fallback")

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Ensure the client sends the supported_versions extension listing 0x0304 (TLS 1.3)
  2. Keep ClientHello.legacy_version pinned at 0x0303 (TLS 1.2) as mandated by RFC 8446 §4.1.2
  3. Replace a hand-rolled TLS client with an RFC 8446-compliant library (Go's crypto/tls, current OpenSSL/BoringSSL)

Example fix

// before (non-compliant client)
hello.version = 0x0304 // TLS 1.3 via legacy field
// supported_versions omitted

// after (compliant)
hello.version = 0x0303
hello.supportedVersions = []uint16{0x0304, 0x0303}
Defensive patterns

Strategy: try-catch

Validate before calling

// Server-side: you cannot pre-validate a remote ClientHello.
// Ensure YOUR server config does not force TLS 1.3-only in a way that
// rejects compliant clients. Default config is fine.
cfg := &tls.Config{ /* leave MinVersion/MaxVersion at defaults */ }

Try / catch

ln, err := listener.Accept()
if err != nil { log.Printf("accept: %v", err); continue }
go func(c net.Conn) {
    tlsConn := tls.Server(c, cfg)
    if err := tlsConn.Handshake(); err != nil {
        if strings.Contains(err.Error(), "legacy version field") {
            log.Printf("non-compliant client (no supported_versions): %v", err)
        }
        c.Close()
        return
    }
    handle(tlsConn)
}(ln)

Prevention

When it happens

Trigger: A ClientHello arrives with an empty supported_versions list. This happens when a hand-rolled or non-compliant TLS client sets legacy_version directly to 0x0304 and omits the supported_versions extension, or when an old TLS stack predating RFC 8446 is used.

Common situations: Custom/in-house TLS client implementations, security fuzzers generating malformed ClientHellos, old TLS libraries that predate RFC 8446, or middleboxes/proxies that rewrite and strip the ClientHello.

Understand the failure class

Related errors


AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12). Data as JSON: /api/errors/3406d0de3260b6be. Report an issue: GitHub.