caddyserver/caddy · error

invalid KEM ID

Error message

invalid KEM ID: %d

What it means

During ECH config decoding, the 2-byte KEM identifier is validated against the hpke package's known KEMs. Caddy only ever generates KEM_X25519_HKDF_SHA256 (0x0020); any other value in stored data — corrupt file, hand-crafted config, or a KEM the linked hpke library doesn't know — fails here. The message prints the offending numeric ID.

Solutions

  1. Note the reported KEM ID and compare with 0x20 (X25519) which Caddy writes.
  2. Delete the malformed ECH config entries under ech/configs/ in storage; Caddy regenerates with X25519 automatically.
  3. If importing external ECH configs, re-create them with the X25519 KEM.
Defensive patterns

Strategy: validation

Validate before calling

// Expected KEM: X25519-HKDF-SHA256 = 0x0020 in the ECHConfig wire format (after config_id byte).
func likelyValidKEM(configBin []byte) bool {
    // version(2) + len(2) + id(1) then 2-byte KEM
    return len(configBin) >= 7 && binary.BigEndian.Uint16(configBin[5:7]) == 0x0020
}

Try / catch

if err != nil && strings.Contains(err.Error(), "invalid KEM ID") {
    // prune malformed stored ECH configs; regenerate with X25519
}

Prevention

When it happens

Trigger: UnmarshalBinary of an ECH config whose KEM ID field is not a registered hpke.KEM (e.g. P-256/0x0017 if the hpke build lacks it, or random bytes after corruption).

Common situations: Corrupted ech/configs/<id>/config.bin; importing an ECH config produced by another implementation using a KEM Caddy's hpke dependency doesn't support; partial writes from storage failures.

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/d26e7c9860b54a35. Report an issue: GitHub.

Appendix: source

Thrown at modules/caddytls/ech.go:996

	if !b.ReadUint16LengthPrefixed(&content) || !b.Empty() {
		return errInvalidLen
	}

	var t cryptobyte.String
	var pk []byte

	if !content.ReadUint8(&echCfg.ConfigID) ||
		!content.ReadUint16((*uint16)(&echCfg.KEMID)) ||
		!content.ReadUint16LengthPrefixed(&t) ||
		!t.ReadBytes(&pk, len(t)) ||
		!content.ReadUint16LengthPrefixed(&t) ||
		len(t)%4 != 0 /* the length of (KDFs and AEADs) must be divisible by 4 */ {
		return errInvalidLen
	}

	if !echCfg.KEMID.IsValid() {
		return fmt.Errorf("invalid KEM ID: %d", echCfg.KEMID)
	}

	var err error
	if echCfg.PublicKey, err = echCfg.KEMID.Scheme().UnmarshalBinaryPublicKey(pk); err != nil {
		return fmt.Errorf("parsing public_key: %w", err)
	}

	echCfg.CipherSuites = echCfg.CipherSuites[:0]

	for !t.Empty() {
		var hpkeKDF, hpkeAEAD uint16
		if !t.ReadUint16(&hpkeKDF) || !t.ReadUint16(&hpkeAEAD) {
			// we have already checked that the length is divisible by 4
			panic("this must not happen")
		}
		if !hpke.KDF(hpkeKDF).IsValid() {
			return fmt.Errorf("invalid KDF ID: %d", hpkeKDF)
		}

View on GitHub (pinned to 50e54ee279)