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

  1. Keep the ECH ciphersuite and config_id identical between ClientHello1 and ClientHello2
  2. 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

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

Related errors


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