golang/go · error

tls: unexpected switch in encrypted client hello extension t

Error message

tls: unexpected switch in encrypted client hello extension type

What it means

ECH has an outer (encrypted) and inner (decrypted) extension type. The server tracks via hs.echContext.inner whether it is processing the inner or outer ClientHello. If the second ClientHello flips type inconsistently (outer when inner expected, or vice versa), it violates the ECH flow and the server sends decode_error.

Source

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

		c.sendAlert(alertUnexpectedMessage)
		return nil, unexpectedMessageError(clientHello, msg)
	}

	if hs.echContext != nil {
		if len(clientHello.encryptedClientHello) == 0 {
			c.sendAlert(alertMissingExtension)
			return nil, errors.New("tls: second client hello missing encrypted client hello extension")
		}

		echType, echCiphersuite, configID, encap, payload, err := parseECHExt(clientHello.encryptedClientHello)
		if err != nil {
			c.sendAlert(alertDecodeError)
			return nil, errors.New("tls: client sent invalid encrypted client hello extension")
		}

		if echType == outerECHExt && hs.echContext.inner || echType == innerECHExt && !hs.echContext.inner {
			c.sendAlert(alertDecodeError)
			return nil, errors.New("tls: unexpected switch in encrypted client hello extension type")
		}

		if echType == outerECHExt {
			if echCiphersuite != hs.echContext.ciphersuite || configID != hs.echContext.configID || len(encap) != 0 {
				c.sendAlert(alertIllegalParameter)
				return nil, errors.New("tls: second client hello encrypted client hello extension does not match")
			}

			encodedInner, err := decryptECHPayload(hs.echContext.hpkeContext, clientHello.original, payload)
			if err != nil {
				c.sendAlert(alertDecryptError)
				return nil, errors.New("tls: failed to decrypt second client hello encrypted client hello extension payload")
			}

			echInner, err := decodeInnerClientHello(clientHello, encodedInner)
			if err != nil {
				c.sendAlert(alertIllegalParameter)
				return nil, errors.New("tls: client sent invalid encrypted client hello extension")

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Keep the ECH extension type consistent between ClientHello1 and ClientHello2 as required by the ECH draft
  2. Update the ECH client implementation to handle HRR consistently
Defensive patterns

Strategy: try-catch

Try / catch

if err := tlsConn.Handshake(); err != nil {
    if strings.Contains(err.Error(), "unexpected switch in encrypted client hello extension type") {
        log.Printf("ECH type flip on retry from %v (possible MITM)", remote)
    }
    c.Close()
    return
}

Prevention

When it happens

Trigger: ClientHello1 was one ECH type, ClientHello2 is the opposite, contradicting the established echContext. E.g., outer in CH1 but inner in CH2.

Common situations: ECH client bug, a man-in-the-middle modifying the ECH type, fuzzers.

Understand the failure class

Related errors


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