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
- Keep the ECH extension type consistent between ClientHello1 and ClientHello2 as required by the ECH draft
- 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
- Keep the ECH extension type (outer/inner) consistent across ClientHello1 and ClientHello2
- Treat a type flip as suspicious — it can indicate tampering
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
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- tls: client sent invalid encrypted client hello extension
- tls: second client hello missing encrypted client hello exte
- tls: second client hello encrypted client hello extension do
- tls: failed to decrypt second client hello encrypted client
- tls: server sent encrypted client hello retry configs after
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/b7b8ad00ccf81ced.
Report an issue: GitHub.