Rename the entire workspace:
- Crate packages: quicnprotochat-{core,proto,server,client,gui,p2p,mobile} -> quicproquo-*
- Binary names: quicnprotochat -> qpq, quicnprotochat-server -> qpq-server,
quicnprotochat-gui -> qpq-gui
- Default files: *-state.bin -> qpq-state.bin, *-server.toml -> qpq-server.toml,
*.db -> qpq.db
- Environment variable prefix: QUICNPROTOCHAT_* -> QPQ_*
- App identifier: chat.quicnproto.gui -> chat.quicproquo.gui
- Proto package: quicnprotochat.bench -> quicproquo.bench
- All documentation, Docker, CI, and script references updated
HKDF domain-separation strings and P2P ALPN remain unchanged for
backward compatibility with existing encrypted state and wire protocol.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
11 KiB
Post-Quantum Readiness
quicproquo includes a fully implemented and tested hybrid key encapsulation mechanism (KEM) combining X25519 (classical) with ML-KEM-768 (post-quantum). This page describes the current implementation, the integration plan, the security rationale, and the known gaps.
Source: crates/quicproquo-core/src/hybrid_kem.rs
Current State
The hybrid KEM is fully implemented and tested in quicproquo-core. The
implementation provides:
HybridKeypair::generate()-- generate a combined X25519 + ML-KEM-768 keypairhybrid_encrypt(recipient_pk, plaintext)-- encrypt to a hybrid public keyhybrid_decrypt(keypair, envelope)-- decrypt with the hybrid private key- Serialization/deserialization for both keypairs and public keys
- Comprehensive test coverage: round-trip, wrong-key rejection, tampering detection, version validation, large payload handling
The test suite in hybrid_kem.rs includes 10 tests covering:
- Basic encrypt/decrypt round-trip
- Wrong key decryption failure
- Tampered AEAD ciphertext detection
- Tampered ML-KEM ciphertext detection
- Tampered X25519 ephemeral public key detection
- Unsupported version rejection
- Envelope-too-short rejection
- Keypair serialization round-trip
- Public key serialization round-trip
- Large payload (50 KB) round-trip
ML-KEM-768 (FIPS 203)
ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism) is the NIST-standardized post-quantum KEM, published as FIPS 203. quicproquo uses ML-KEM-768, the middle parameter set:
| Parameter Set | NIST Level | Security (PQ) | EK Size | CT Size | SS Size |
|---|---|---|---|---|---|
| ML-KEM-512 | 1 | ~128-bit | 800 B | 768 B | 32 B |
| ML-KEM-768 | 3 | ~192-bit | 1184 B | 1088 B | 32 B |
| ML-KEM-1024 | 5 | ~256-bit | 1568 B | 1568 B | 32 B |
Why ML-KEM-768 (not 512 or 1024)?
- Level 3 provides a strong security margin. The ~192-bit post-quantum security level exceeds the 128-bit classical security of the rest of the system, providing headroom against future cryptanalytic advances.
- Moderate key and ciphertext sizes. The encapsulation key (1184 bytes) and ciphertext (1088 bytes) are large compared to X25519 (32 bytes each) but manageable for a messaging protocol. ML-KEM-1024 would add ~400 bytes to each with diminishing returns.
- Consistent with industry practice. Signal, Google Chrome, and Cloudflare have all deployed ML-KEM-768 (or its predecessor Kyber-768) in production.
The ml-kem 0.2 crate provides a pure-Rust implementation of FIPS 203 with all
three parameter sets compiled in by default.
Hybrid Construction
The hybrid KEM follows the combiner approach described in
draft-ietf-tls-hybrid-design. Both a classical and a post-quantum KEM are
executed, and their shared secrets are combined through a KDF:
Key Derivation
ikm = X25519_shared_secret(32 bytes) || ML-KEM_shared_secret(32 bytes)
key = HKDF-SHA256(salt=[], ikm, info="quicproquo-hybrid-v1", L=32)
nonce = HKDF-SHA256(salt=[], ikm, info="quicproquo-hybrid-nonce-v1", L=12)
The combined IKM (input key material) is wrapped in Zeroizing<Vec<u8>> and
cleared after HKDF expansion.
Why a Hybrid?
A pure ML-KEM deployment would be vulnerable if lattice-based cryptography is broken (which, while considered unlikely, cannot be ruled out for a newly standardized algorithm). A pure X25519 deployment provides no post-quantum protection. The hybrid approach provides a "belt and suspenders" guarantee:
- If ML-KEM-768 is broken, X25519 still provides 128-bit classical security.
- If X25519 is broken (by a quantum computer), ML-KEM-768 still provides ~192-bit post-quantum security.
- Both must be broken simultaneously to compromise the shared secret.
Wire Format
The hybrid envelope uses a versioned binary format:
Offset Length Field
------ ------ -----
0 1 Version byte (0x01)
1 32 X25519 ephemeral public key
33 1088 ML-KEM-768 ciphertext
1121 12 ChaCha20-Poly1305 nonce
1133 var ChaCha20-Poly1305 AEAD ciphertext (plaintext + 16-byte tag)
Total overhead: 1133 bytes of header + 16 bytes of AEAD tag = 1149 bytes per message (in addition to the plaintext length).
The version byte enables future algorithm agility. Version 0x01 denotes
X25519 + ML-KEM-768 + HKDF-SHA256 + ChaCha20-Poly1305.
Encryption Flow
Sender Recipient
------ ---------
HybridKeypair::generate()
├── x25519_sk, x25519_pk
└── mlkem_dk, mlkem_ek
│
public_key()│
│
┌─── HybridPublicKey ◄──────────────────────────────┘
│ (x25519_pk: 32B, mlkem_ek: 1184B)
│
│ hybrid_encrypt(pk, plaintext):
│ 1. eph_sk = EphemeralSecret::random()
│ 2. eph_pk = PublicKey::from(&eph_sk)
│ 3. x25519_ss = eph_sk.diffie_hellman(pk.x25519_pk)
│ 4. (mlkem_ct, mlkem_ss) = pk.mlkem_ek.encapsulate()
│ 5. ikm = x25519_ss || mlkem_ss
│ 6. (key, nonce) = HKDF(ikm, info)
│ 7. ct = ChaCha20Poly1305::encrypt(key, nonce, plaintext)
│ 8. envelope = ver || eph_pk || mlkem_ct || nonce || ct
│
└──────────── envelope ──────────────────────────────►
│
hybrid_decrypt(kp, envelope):
1. Parse ver, eph_pk, mlkem_ct, nonce, ct
2. x25519_ss = kp.x25519_sk.dh(eph_pk)
3. mlkem_ss = kp.mlkem_dk.decapsulate(mlkem_ct)
4. ikm = x25519_ss || mlkem_ss
5. (key, _) = HKDF(ikm, info)
6. plaintext = ChaCha20Poly1305::decrypt(key, nonce, ct)
Integration Plan (M5)
The hybrid KEM is currently a standalone module. Milestone M5 will integrate it
into the MLS pipeline by creating a custom OpenMlsCryptoProvider that uses the
hybrid KEM for HPKE init key exchange:
-
Custom crypto provider: Wrap the existing
StoreCryptowith a hybrid KEM layer that intercepts HPKE operations and replaces the classical X25519 DHKEM with the hybrid X25519 + ML-KEM-768 KEM. -
KeyPackage extension: Store the hybrid public key (1216 bytes: 32B X25519 + 1184B ML-KEM) in a custom MLS extension within the KeyPackage.
-
Welcome encryption: When creating a Welcome message, the hybrid KEM encrypts the group secrets instead of (or in addition to) the standard HPKE.
-
Backward compatibility: Groups can negotiate whether to use hybrid KEM via the MLS group context extensions. Classical-only clients can still participate in groups that do not require PQ protection.
The PQ Gap
There is an important asymmetry in quicproquo's post-quantum protection:
Layer Classical Protection Post-Quantum Protection
---------------------------------------------------------------------
QUIC/TLS 1.3 Yes (ECDHE) No
MLS content (M5+) Yes (X25519 DHKEM) Yes (hybrid KEM)
What this means:
-
Message content (the MLS application data) is protected against quantum adversaries from M5 onward. An attacker with a quantum computer cannot decrypt the message payload.
-
Transport metadata (who connects to the server, when, message sizes) is protected only by classical ECDHE. A quantum attacker who recorded the TLS handshake transcripts could, in theory, recover the transport session keys and observe the metadata.
This is the PQ gap: content is safe, but metadata is not.
Why not PQ transport?
Post-quantum TLS (via ML-KEM in the TLS 1.3 handshake) is being standardized by
the IETF and is supported by some TLS libraries, but rustls does not yet
support it in a stable release. When rustls adds ML-KEM support, quicproquo
will adopt it to close the PQ gap at the transport layer.
Harvest-Now, Decrypt-Later Risk
The "harvest-now, decrypt-later" (HNDL) threat model assumes an adversary who:
- Records all encrypted traffic today (inexpensive storage).
- Waits for a sufficiently powerful quantum computer (years or decades).
- Decrypts the recorded traffic retroactively.
In quicproquo's case:
-
Content is safe from M5 onward. The hybrid KEM wrapping MLS content uses ML-KEM-768, which resists quantum attacks. Even if the recorded traffic is decrypted at the transport layer, the MLS ciphertext inside is still protected.
-
Transport metadata is at risk. An HNDL attacker who records TLS handshakes today could, with a future quantum computer, recover the transport session keys and observe:
- Which clients connected to the server and when.
- Message sizes and timing patterns.
- The encrypted MLS blobs (which they still cannot decrypt if hybrid KEM is active).
-
Content before M5 is at risk. Messages sent before the hybrid KEM integration (M5) use classical-only MLS encryption. If the HPKE init key exchange used X25519-only DHKEM, a quantum attacker could recover the HPKE shared secret and decrypt the Welcome message, gaining access to the group state.
This risk is the primary motivation for deploying the hybrid KEM as early as possible.
Key Sizes and Performance
| Component | Key/Ciphertext | Size |
|---|---|---|
| X25519 public key | x25519_pk |
32 bytes |
| X25519 shared secret | DH result | 32 bytes |
| ML-KEM-768 encapsulation key | mlkem_ek |
1,184 bytes |
| ML-KEM-768 decapsulation key | mlkem_dk |
2,400 bytes |
| ML-KEM-768 ciphertext | mlkem_ct |
1,088 bytes |
| ML-KEM-768 shared secret | KEM result | 32 bytes |
| Hybrid public key | x25519_pk + mlkem_ek |
1,216 bytes |
| Hybrid envelope overhead | Header + AEAD tag | 1,149 bytes |
The ML-KEM-768 operations (keygen, encapsulate, decapsulate) are fast in software -- typically sub-millisecond on modern hardware. The primary cost is bandwidth, not computation.
Related Pages
- Cryptography Overview -- algorithm inventory including ML-KEM-768
- Key Lifecycle and Zeroization -- hybrid KEM key lifecycle
- Forward Secrecy -- how FS interacts with PQ protection
- Threat Model -- harvest-now-decrypt-later in context
- Hybrid KEM: X25519 + ML-KEM-768 -- protocol layer details