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.

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.

Appendix: 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)