golang/go · error

tls: EncryptedClientHelloConfigList contains no valid config

Error message

tls: EncryptedClientHelloConfigList contains no valid configs

What it means

Thrown after parsing Config.EncryptedClientHelloConfigList when pickECHConfig(echConfigs) returns a nil config. The list parsed successfully (parseECHConfigList did not error) but no entry had a usable combination of HPKE KEM/KDF/AEAD that this Go build supports. ECH (Encrypted Client Hello, draft-ietf-tls-esni) wraps the real ClientHello inside an HPKE-sealed inner hello to hide the SNI.

Source

Thrown at src/crypto/tls/handshake_client.go:188

		}
		hello.quicTransportParameters = p
	}

	var ech *echClientContext
	if c.config.EncryptedClientHelloConfigList != nil {
		if c.config.MinVersion != 0 && c.config.MinVersion < VersionTLS13 {
			return nil, nil, nil, errors.New("tls: MinVersion must be >= VersionTLS13 if EncryptedClientHelloConfigList is populated")
		}
		if c.config.MaxVersion != 0 && c.config.MaxVersion <= VersionTLS12 {
			return nil, nil, nil, errors.New("tls: MaxVersion must be >= VersionTLS13 if EncryptedClientHelloConfigList is populated")
		}
		echConfigs, err := parseECHConfigList(c.config.EncryptedClientHelloConfigList)
		if err != nil {
			return nil, nil, nil, err
		}
		echConfig, echPK, kdf, aead := pickECHConfig(echConfigs)
		if echConfig == nil {
			return nil, nil, nil, errors.New("tls: EncryptedClientHelloConfigList contains no valid configs")
		}
		ech = &echClientContext{config: echConfig, kdfID: kdf.ID(), aeadID: aead.ID()}
		hello.encryptedClientHello = []byte{1} // indicate inner hello
		// We need to explicitly set these 1.2 fields to nil, as we do not
		// marshal them when encoding the inner hello, otherwise transcripts
		// will later mismatch.
		hello.supportedPoints = nil
		hello.ticketSupported = false
		hello.secureRenegotiationSupported = false
		hello.extendedMasterSecret = false

		info := append([]byte("tls ech\x00"), ech.config.raw...)
		ech.encapsulatedKey, ech.hpkeContext, err = hpke.NewSender(echPK, kdf, aead, info)
		if err != nil {
			return nil, nil, nil, err
		}
	}

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Refetch the ECH config at runtime from the authoritative DNS HTTPS/SVCB ech= parameter (base64-decoded) rather than caching it long-term.
  2. Verify you are passing only the ech= value (base64-decoded bytes), not the entire HTTPS RR or the quoted string.
  3. Upgrade Go to a version whose crypto/internal/hpke supports the KEM/KDF/AEAD in the config (X25519 + HKDF-SHA256 + AES-128-GCM is the baseline).
  4. If ECH cannot be made reliable, set Config.EncryptedClientHelloConfigList to nil and fall back to plaintext SNI rather than failing every handshake.

Example fix

// before: stale hardcoded list
cfg := &tls.Config{EncryptedClientHelloConfigList: cachedECHBytes}
// after: fetch fresh at connection time and tolerate absence
echBytes, err := dnshttps.ECHConfig(serverName) // resolves type 65
cfg := &tls.Config{}
if err == nil && len(echBytes) > 0 {
    cfg.EncryptedClientHelloConfigList = echBytes
}
Defensive patterns

Strategy: validation

Validate before calling

// Parse the ECH config list the same way the library will, and check that
// at least one config is acceptable before dialing.
import "crypto/internal/fips140/hpke" // not publicly exposed; mirror the supported suites

func hasUsableECHConfig(list []byte) bool {
    // Walk 4-byte version + 1-byte length-prefixed ECHConfig entries (draft-ietf-tls-esni).
    for len(list) > 0 {
        if len(list) < 4 { return false }
        // version is uint16 at list[0:2]; config_id length follows
        n := int(list[3])
        if 4+n > len(list) { return false }
        cfg := list[4 : 4+n]
        if echSupported(cfg) { return true }
        list = list[4+n:]
    }
    return false
}
// echSupported should check the HPKE KEM/KDF/AEAD triple is one your Go supports.
// In practice: fetch a fresh config list at runtime and rely on Go's own parsing.

Type guard

// No type guard: errors are untyped strings. Match on the message prefix.
func isECHNoValidConfigs(err error) bool {
    return err != nil && strings.HasPrefix(err.Error(), "tls: EncryptedClientHelloConfigList contains no valid configs")
}

Try / catch

echBytes, err := resolveECHConfig(host)
if err == nil { cfg.EncryptedClientHelloConfigList = echBytes }
if _, derr := tls.Dial("tcp", addr, cfg); derr != nil {
    if isECHNoValidConfigs(derr) {
        // retry once with ECH disabled rather than failing permanently
        cfg.EncryptedClientHelloConfigList = nil
        _, derr = tls.Dial("tcp", addr, cfg)
    }
}

Prevention

When it happens

Trigger: Calling tls.Dial / tls.DialTLS / http transport with Config.EncryptedClientHelloConfigList set to bytes where every entry uses an unsupported KEM (e.g. a post-quantum KEM this Go version lacks), uses an ECHConfigVersion other than 0xfffd, or has a public_name/key_config that pickECHConfig rejects. Common when the caller passes the whole DNS HTTPS RR instead of just the ech= base64 payload.

Common situations: Stale ECH config cached locally after the server rotated its HPKE key; fetching the ECH config from DNS HTTPS/SVCB records on a Go version older than the suite the server advertises; passing raw bytes that contain non-ECH SvcParamKeys; empty list after filtering malformed entries.

Understand the failure class

Related errors


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