golang/go · error

hpke: invalid hybrid KEM secret length

Error message

hpke: invalid hybrid KEM secret length

What it means

hybridKEM.NewPrivateKey derives the PQ and ECDH subkeys from a single 32-byte seed via SHAKE256 (draft-ietf-hpke-pq DeriveKeyPair). The seed length is fixed at 32 bytes; any other length is rejected because the KDF input size is part of the combiner's contract.

Source

Thrown at src/crypto/hpke/pq.go:256

//
// This function is meant for applications that already have instantiated
// crypto/ecdh and crypto/mlkem private keys, or another implementation of a
// [ecdh.KeyExchanger] and [crypto.Decapsulator] (e.g. a hardware key).
// Otherwise, applications should use the [KEM.NewPrivateKey] method of e.g.
// [MLKEM768X25519].
func NewHybridPrivateKey(pq crypto.Decapsulator, t ecdh.KeyExchanger) (PrivateKey, error) {
	return newHybridPrivateKey(pq, t, nil)
}

func (kem *hybridKEM) GenerateKey() (PrivateKey, error) {
	seed := make([]byte, 32)
	drbg.Read(seed)
	return kem.NewPrivateKey(seed)
}

func (kem *hybridKEM) NewPrivateKey(priv []byte) (PrivateKey, error) {
	if len(priv) != 32 {
		return nil, errors.New("hpke: invalid hybrid KEM secret length")
	}

	s := sha3.NewSHAKE256()
	s.Write(priv)

	seedPQ := make([]byte, mlkem.SeedSize)
	s.Read(seedPQ)
	pq, err := kem.pqNewPrivateKey(seedPQ)
	if err != nil {
		return nil, err
	}

	seedT := make([]byte, kem.curveSeedSize)
	for {
		s.Read(seedT)
		var k ecdh.KeyExchanger
		fips140.WithoutEnforcement(func() { // Hybrid of ML-KEM, which is Approved.
			k, err = kem.curve.NewPrivateKey(seedT)

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Pass exactly 32 bytes; if you have a larger secret, hash/expand it down to 32 bytes first.
  2. Use GenerateKey() to get a randomly-seeded key, or DeriveKeyPair(ikm) to derive from arbitrary input.
  3. Round-trip keys via Bytes()/NewPrivateKey(Bytes()) to guarantee correct sizing.

Example fix

// before
seed := make([]byte, 64) // wrong size
sk, err := hpke.MLKEM768X25519().NewPrivateKey(seed) // "invalid hybrid KEM secret length"

// after
seed := make([]byte, 32)
crand.Read(seed)
sk, err := hpke.MLKEM768X25519().NewPrivateKey(seed)
Defensive patterns

Strategy: validation

Validate before calling

const hybridSeedLen = 32
func newHybridSecret(kem hpke.KEM, seed []byte) (hpke.PrivateKey, error) {
    if len(seed) != hybridSeedLen {
        return nil, fmt.Errorf("hybrid seed must be %d bytes, got %d", hybridSeedLen, len(seed))
    }
    // Use the unexported path via GenerateKey/DeriveKeyPair by passing through NewPrivateKey if exposed.
    return kem.NewPrivateKey(seed)
}

Try / catch

sk, err := kem.NewPrivateKey(seed)
if err != nil && err.Error() == "hpke: invalid hybrid KEM secret length" {
    h := sha3.NewShake256()
    h.Write(seed)
    out := make([]byte, 32)
    h.Read(out)
    sk, err = kem.NewPrivateKey(out)
}
if err != nil {
    return nil, err
}

Prevention

When it happens

Trigger: Calling (*hybridKEM).NewPrivateKey(priv) directly with priv of length != 32. Indirectly via DeriveKeyPair(ikm) which derives a 32-byte dk first, so the direct call is the usual culprit.

Common situations: Passing a 64-byte X25519-style secret, an empty slice, or the raw ML-KEM seed instead of the combined 32-byte hybrid seed.

Related errors


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