golang/go · error

tls: server sent encrypted client hello retry configs after

Error message

tls: server sent encrypted client hello retry configs after accepting encrypted client hello

What it means

Encrypted Client Hello (ECH) specific: the server accepted ECH (hs.echContext.echRejected is false) but still included ECH retry configs in EncryptedExtensions. RFC 9460 only permits retry configs when ECH was rejected. Go sends `unsupported_extension`. Indicates a server misusing the ECH retry_config field.

Source

Thrown at src/crypto/tls/handshake_client_tls13.go:583

	if hs.hello.earlyData && !encryptedExtensions.earlyData {
		c.quicRejectedEarlyData()
	}
	if encryptedExtensions.earlyData {
		if hs.session.cipherSuite != c.cipherSuite {
			c.sendAlert(alertHandshakeFailure)
			return errors.New("tls: server accepted 0-RTT with the wrong cipher suite")
		}
		if hs.session.alpnProtocol != c.clientProtocol {
			c.sendAlert(alertHandshakeFailure)
			return errors.New("tls: server accepted 0-RTT with the wrong ALPN")
		}
	}
	if hs.echContext != nil {
		if hs.echContext.echRejected {
			hs.echContext.retryConfigs = encryptedExtensions.echRetryConfigs
		} else if encryptedExtensions.echRetryConfigs != nil {
			c.sendAlert(alertUnsupportedExtension)
			return errors.New("tls: server sent encrypted client hello retry configs after accepting encrypted client hello")
		}
	}

	return nil
}

func (hs *clientHandshakeStateTLS13) readServerCertificate() error {
	c := hs.c

	// Either a PSK or a certificate is always used, but not both.
	// See RFC 8446, Section 4.1.1.
	if hs.usingPSK {
		// Make sure the connection is still being verified whether or not this
		// is a resumption. Resumptions currently don't reverify certificates so
		// they don't call verifyServerCertificate. See Issue 31641.
		if c.config.VerifyConnection != nil {
			if err := c.config.VerifyConnection(c.connectionStateLocked()); err != nil {
				c.sendAlert(alertBadCertificate)

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Report to the server operator — retry_configs are rejection-only per RFC 9460.
  2. Confirm ECH is actually being accepted (check the ECH status after the handshake) on a known-good peer.
  3. Update the client and server ECH implementations to draft-compliant versions (ECH is still evolving).
  4. Disable ECH (do not set Config.EncryptedClientHelloConfigList) if the server implementation is unreliable.

Example fix

// before: ECH configured against a server with buggy retry-config handling
cfg := &tls.Config{EncryptedClientHelloConfigList: echList}

// after: disable ECH until the server is fixed
cfg := &tls.Config{}
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight: ECH is still maturing; only enable it against peers known to implement RFC 9460 correctly.
if !echCapablePeer {
    cfg.EncryptedClientHelloConfigList = nil
}

Try / catch

if err := conn.Handshake(); err != nil {
    if strings.Contains(err.Error(), "encrypted client hello retry configs after accepting") {
        cfg.EncryptedClientHelloConfigList = nil // disable ECH for this peer
        return retryHandshake(addr, cfg)
    }
}

Prevention

When it happens

Trigger: Client offered ECH, server accepted it (inner SNI used), yet encryptedExtensions.echRetryConfigs is non-nil. Reached in processEncryptedExtensions when ECH is in use.

Common situations: Experimental/early ECH server implementations, fuzzed handshakes, or a server that always includes retry_configs. Mainstream ECH-capable servers gate retry_configs on rejection.

Understand the failure class

Related errors


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