golang/go · error
tls: second client hello encrypted client hello extension do
Error message
tls: second client hello encrypted client hello extension does not match
What it means
For outer ECH in ClientHello2, the ciphersuite and config_id must match what was in ClientHello1, and encap must be empty (the HPKE context is already established). Any mismatch triggers illegal_parameter per the ECH draft.
Source
Thrown at src/crypto/tls/handshake_server_tls13.go:613
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")
}
clientHello = echInner
}
}
View on GitHub (pinned to b6b368adc5)
Solutions
- Keep the ECH ciphersuite and config_id identical between ClientHello1 and ClientHello2
- Send an empty encap in ClientHello2's outer ECH extension as the draft requires
Defensive patterns
Strategy: try-catch
Validate before calling
// Client-side: assert outer ECH is consistent across both ClientHellos.
if echType1 != echType2 || suite1 != suite2 || configID1 != configID2 || len(encap2) != 0 {
return errors.New("ClientHello2 ECH must match ClientHello1 (and empty encap)")
} Try / catch
if err := tlsConn.Handshake(); err != nil {
if strings.Contains(err.Error(), "encrypted client hello extension does not match") {
log.Printf("ECH fields changed on retry from %v", remote)
}
c.Close()
return
} Prevention
- Keep ECH ciphersuite and config_id identical between ClientHello1 and ClientHello2
- Send an empty encap in ClientHello2's outer ECH
When it happens
Trigger: ClientHello2 changes the ECH ciphersuite or config_id versus ClientHello1, or includes unexpected non-empty encap bytes.
Common situations: ECH client bug; a replayed ClientHello with a different ECH config; fuzzers.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- tls: second client hello missing encrypted client hello exte
- tls: client sent invalid encrypted client hello extension
- tls: unexpected switch in encrypted client hello extension t
- 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/8a922e3ae7faf548.
Report an issue: GitHub.