golang/go · error

tls: Encrypted Client Hello cannot be used pre-TLS 1.3

Error message

tls: Encrypted Client Hello cannot be used pre-TLS 1.3

What it means

The server negotiated a TLS version below 1.3 while observing an outer Encrypted Client Hello (ECH) extension. Per the ECH specification and BoringSSL behavior mirrored here, ECH is only valid with TLS 1.3; using outer ECH with an older version is illegal_parameter. Backend servers accepting inner ECH at 1.2 are tolerated, but outer ECH at < 1.3 is rejected.

Source

Thrown at src/crypto/tls/handshake_server.go:209

	c.vers, ok = c.config.mutualVersion(roleServer, c.quic != nil, clientVersions)
	if !ok {
		c.sendAlert(alertProtocolVersion)
		return nil, nil, fmt.Errorf("tls: client offered only unsupported versions: %x", clientVersions)
	}
	c.haveVers = true
	c.in.version = c.vers
	c.out.version = c.vers

	// This check reflects some odd specification implied behavior. Client-facing servers
	// are supposed to reject hellos with outer ECH and inner ECH that offers 1.2, but
	// backend servers are allowed to accept hellos with inner ECH that offer 1.2, since
	// they cannot expect client-facing servers to behave properly. Since we act as both
	// a client-facing and backend server, we only enforce 1.3 being negotiated if we
	// saw a hello with outer ECH first. The spec probably should've made this an error,
	// but it didn't, and this matches the boringssl behavior.
	if c.vers != VersionTLS13 && (ech != nil && !ech.inner) {
		c.sendAlert(alertIllegalParameter)
		return nil, nil, errors.New("tls: Encrypted Client Hello cannot be used pre-TLS 1.3")
	}

	return clientHello, ech, nil
}

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

	hs.hello = new(serverHelloMsg)
	hs.hello.vers = c.vers

	foundCompression := false
	// We only support null compression, so check that the client offered it.
	for _, compression := range hs.clientHello.compressionMethods {
		if compression == compressionNone {
			foundCompression = true
			break
		}

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Ensure the client's tls.Config does not cap MaxVersion below VersionTLS13 when ECH is enabled.
  2. Investigate middleboxes or proxies that may strip the TLS 1.3 supported_versions entry.
  3. Update the client TLS stack to a version that correctly pairs ECH with TLS 1.3 negotiation.
  4. If ECH is not required, disable it on the client to allow fallback to a standard 1.2 handshake.

Example fix

// before
cfg := &tls.Config{
    MaxVersion: tls.VersionTLS12, // forces downgrade; ECH becomes illegal
    // ECH configured via ECHConfig list
}

// after
cfg := &tls.Config{
    MinVersion: tls.VersionTLS13,
    MaxVersion: tls.VersionTLS13,
}
Defensive patterns

Strategy: validation

Validate before calling

// Client: when enabling ECH, also require TLS 1.3.
if len(echConfigList) > 0 {
    cfg.MinVersion = tls.VersionTLS13
    cfg.MaxVersion = tls.VersionTLS13 // or omit, but never < 1.3
}

Try / catch

if err != nil && strings.Contains(err.Error(), "Encrypted Client Hello cannot be used pre-TLS 1.3") {
    cfg.MaxVersion = 0 // allow 1.3
    // retry handshake
}

Prevention

When it happens

Trigger: In the server's clientHello processing, c.vers != VersionTLS13 and an ECH extension was decoded with ech.inner == false (i.e. the outer SNI was encrypted). The client offered ECH but negotiated an older version.

Common situations: A client that sends ECH but advertises max_version of TLS 1.2, a downgrade forced by a middlebox stripping supported_versions, or a misconfigured client library combining ECH with legacy version caps.

Understand the failure class

Related errors


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