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
- Pass exactly 32 bytes; if you have a larger secret, hash/expand it down to 32 bytes first.
- Use GenerateKey() to get a randomly-seeded key, or DeriveKeyPair(ikm) to derive from arbitrary input.
- 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
- Standardize on a 32-byte seed constant for hybrid keys.
- Prefer GenerateKey() for random keys and DeriveKeyPair(ikm) for arbitrary input.
- Round-trip keys through Bytes()/NewPrivateKey(Bytes()) to guarantee size correctness.
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
- invalid public key size
- invalid PQ KEM for X25519 hybrid
- invalid PQ KEM for P-256 hybrid
- invalid PQ KEM for P-384 hybrid
- unsupported curve
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/73aef09d3918e14a.
Report an issue: GitHub.