golang/go · error

invalid PQ KEM for X25519 hybrid

Error message

invalid PQ KEM for X25519 hybrid

What it means

NewHybridPublicKey builds an ML-KEM + ECDH hybrid (draft-ietf-hpke-pq). For X25519 the only valid pairing is ML-KEM-768 (the X-Wing combiner). If the pq argument is not *mlkem.EncapsulationKey768 (e.g. it is EncapsulationKey1024 or another Encapsulator), the constructor rejects it.

Source

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

// NewHybridPublicKey returns a PublicKey implementing one of
//
//   - MLKEM768-X25519 (a.k.a. X-Wing)
//   - MLKEM768-P256
//   - MLKEM1024-P384
//
// from draft-ietf-hpke-pq, depending on the underlying curve of t
// ([ecdh.X25519], [ecdh.P256], or [ecdh.P384]) and the type of pq (either
// *[mlkem.EncapsulationKey768] or *[mlkem.EncapsulationKey1024]).
//
// This function is meant for applications that already have instantiated
// crypto/ecdh and crypto/mlkem public keys. Otherwise, applications should use
// the [KEM.NewPublicKey] method of e.g. [MLKEM768X25519].
func NewHybridPublicKey(pq crypto.Encapsulator, t *ecdh.PublicKey) (PublicKey, error) {
	switch t.Curve() {
	case ecdh.X25519():
		if _, ok := pq.(*mlkem.EncapsulationKey768); !ok {
			return nil, errors.New("invalid PQ KEM for X25519 hybrid")
		}
		return &hybridPublicKey{mlkem768X25519, t, pq}, nil
	case ecdh.P256():
		if _, ok := pq.(*mlkem.EncapsulationKey768); !ok {
			return nil, errors.New("invalid PQ KEM for P-256 hybrid")
		}
		return &hybridPublicKey{mlkem768P256, t, pq}, nil
	case ecdh.P384():
		if _, ok := pq.(*mlkem.EncapsulationKey1024); !ok {
			return nil, errors.New("invalid PQ KEM for P-384 hybrid")
		}
		return &hybridPublicKey{mlkem1024P384, t, pq}, nil
	default:
		return nil, errors.New("unsupported curve")
	}
}

func (kem *hybridKEM) NewPublicKey(data []byte) (PublicKey, error) {

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Pair X25519 only with *mlkem.EncapsulationKey768.
  2. Use MLKEM768X25519().NewPublicKey(data) for the combined parsing path, which selects types automatically.
  3. Type-check pq before calling NewHybridPublicKey: switch pq := pq.(type) { case *mlkem.EncapsulationKey768: ... }.

Example fix

// before
pq := mlkem.NewEncapsulationKey1024(pqBytes)
hpkePub, err := hpke.NewHybridPublicKey(pq, x25519Pub) // "invalid PQ KEM for X25519 hybrid"

// after
pq, _ := mlkem.NewEncapsulationKey768(pqBytes)
hpkePub, err := hpke.NewHybridPublicKey(pq, x25519Pub)
Defensive patterns

Strategy: type-guard

Validate before calling

func x25519HybridPub(pq crypto.Encapsulator, t *ecdh.PublicKey) (hpke.PublicKey, error) {
    if _, ok := pq.(*mlkem.EncapsulationKey768); !ok {
        return nil, fmt.Errorf("X25519 hybrid requires *mlkem.EncapsulationKey768, got %T", pq)
    }
    return hpke.NewHybridPublicKey(pq, t)
}

Type guard

func isMLKEM768Encapsulator(pq crypto.Encapsulator) bool {
    _, ok := pq.(*mlkem.EncapsulationKey768)
    return ok
}

Try / catch

pub, err := hpke.NewHybridPublicKey(pq, x25519Pub)
if err != nil && err.Error() == "invalid PQ KEM for X25519 hybrid" {
    return nil, fmt.Errorf("need *mlkem.EncapsulationKey768, got %T", pq)
}

Prevention

When it happens

Trigger: Calling hpke.NewHybridPublicKey(pq, x25519Pub) with pq being *mlkem.EncapsulationKey1024 or any non-EncapsulationKey768 Encapsulator.

Common situations: Copy-pasting hybrid setup code across X25519 and P-384 builds; instantiating ML-KEM-1024 and assuming it pairs with X25519; reading the wrong draft revision.

Related errors


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