pqtransport logopqtransport

Hybrid Groups

RFC 10024 hybrid key_share concatenation. ML-KEM-first for X25519MLKEM768. ECDHE-first for NIST-curve groups.

The #1 interop bug in this stack is concatenating hybrid shares in the wrong order. pqtransport encodes, decodes, and live-handshakes three RFC 10024 groups.

Why the name lies#

RFC 9954-style names read classical-then-PQ. X25519MLKEM768 does not follow that order on the wire. ML-KEM encapsulation key / ciphertext comes first; X25519 follows. NIST-curve groups (SecP256r1MLKEM768, SecP384r1MLKEM1024) do put the uncompressed ECDHE share first, including the 0x04 prefix.

package:pqforge's PqForgeCombiner.combine() is always classical || PQ. Using it for X25519MLKEM768 would reverse the RFC. Concat uses concatenateSharedSecrets after length / all-zero checks.

Wire sizes#

GroupIANAClientServerShared secretOrder
X25519MLKEM7680x11EC1216112064kem-first
SecP256r1MLKEM7680x11EB1249115364ecdhe-first
SecP384r1MLKEM10240x11ED1665166580ecdhe-first

ML-KEM-768: ek 1184, ct 1088, ss 32. ML-KEM-1024: ek/ct 1568, ss 32. X25519: 32. P-256 uncompressed: 65. P-384 uncompressed: 97.

Live vs fail-closed#

PathStatus
Encode / decode / combine all three groupsDone, byte-exact tests
Live X25519 + ML-KEM-768 + ML-DSA-65 + AES-256-GCMDone
Live P-256 ECDH (SecP256r1MLKEM768, balanced) Done (pqforge 0.4.4)
Live P-384 ECDH (SecP384r1MLKEM1024, maximum) Done (pqforge 0.4.4)
Profile / group mismatchFail-closed (requireGroup)

Fail closed means the handshake refuses a profile/group mismatch. It does not silently skip the classical share. See doc/PQFORGE_EXPORTS.md.

Profile footgun#

PqForgeProfile.maximum selects ML-KEM-1024 + ML-DSA-87. ML-KEM-768 groups belong with balanced / compact. Constructing PqTransportCrypto(profile: PqForgeProfile.maximum) with the default X25519MLKEM768 group is refused (requireGroup; OPEN-03 closed).

SecP384r1MLKEM1024 is the group that belongs with ML-KEM-1024.

Tests that pin this#

  • Combiner order differs for X25519 vs P-256.
  • Client 1216 / server 1120 / ss 64, ML-KEM first.
  • P-256 1249 / 1153, ECDHE first, leading 0x04.
  • P-384 1665 / 1665 / 80.
  • Wrong-length encapsulation key is illegal_parameter.
  • Bad-modulus encapsulation key is illegal_parameter without catch.
  • maximum + ML-KEM-768 and balanced + P-384 are refused.