golang/go · error
tls: client sent invalid encrypted client hello extension
Error message
tls: client sent invalid encrypted client hello extension
What it means
parseECHExt failed to parse the ECH extension bytes — the extension is structurally malformed (bad type, KEM/KDF/AEAD suite, config_id, encap, or payload layout). The server sends decode_error. Note: this same message is reused at line 625 for a different failure (inner ClientHello decode); here it is the outer parse.
Source
Thrown at src/crypto/tls/handshake_server_tls13.go:602
return nil, err
}
clientHello, ok := msg.(*clientHelloMsg)
if !ok {
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")
}View on GitHub (pinned to b6b368adc5)
Solutions
- Ensure the ECH extension is serialized per the draft (type, suite, config_id, enc, payload) the server expects
- Match the ECH draft revision between client and server
- Use a vetted ECH implementation instead of hand-encoding
Defensive patterns
Strategy: try-catch
Validate before calling
// Client-side sanity check before sending (conceptual):
if _, _, _, _, _, err := parseECHExt(echBytes); err != nil {
return fmt.Errorf("ECH extension malformed: %w", err)
} Try / catch
if err := tlsConn.Handshake(); err != nil {
if strings.Contains(err.Error(), "invalid encrypted client hello extension") {
log.Printf("malformed ECH extension from %v", remote)
}
c.Close()
return
} Prevention
- Match the ECH draft revision between client and server
- Serialize the ECH extension (type, suite, config_id, enc, payload) exactly per the draft
- Use a vetted ECH implementation rather than hand-encoding
When it happens
Trigger: Client sends a truncated or wrongly-encoded ECH extension. Hand-rolled ECH, an ECH draft-version mismatch with the server, or a fuzzer.
Common situations: Hand-rolled ECH serialization, client/server on different ECH draft revisions, buggy ECH library.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- tls: unexpected switch in encrypted client hello extension t
- 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: invalid reconstructed inner client hello
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/c0ad23b5813aa7c5.
Report an issue: GitHub.