DEV Community

Michael Muriithi
Michael Muriithi

Posted on

Post-Quantum Key Exchange in Rust: ML-KEM-768 in Practice

Quantum computing isn't here yet. But the data you encrypt today will still exist when it arrives. An attacker recording your traffic now can decrypt it later with a quantum computer — this is called "harvest now, decrypt later."

GhostWire implements ML-KEM-768 (formerly CRYSTALS-Kyber) as a post-quantum key exchange, hybridized with classical X25519. Here's why and how.

Why Post-Quantum Now?

The timeline is uncertain. Estimates range from 5 to 30 years for a cryptographically relevant quantum computer. But:

  1. Classified intelligence suggests nation-states are already harvesting encrypted traffic
  2. NIST finalized ML-KEM in 2024 — the standard is ready
  3. The performance cost is small — ML-KEM-768 adds ~1ms to key exchange

Waiting is not a strategy. Implementing now means your protocol is ready when quantum computers arrive.

ML-KEM-768: The Basics

ML-KEM is a Key Encapsulation Mechanism (KEM). It works differently from traditional key exchange:

Keygen:  (public_key, secret_key) = ML-KEM.Keygen()
Encaps:  (ciphertext, shared_secret) = ML-KEM.Encaps(public_key)
Decaps:  shared_secret = ML-KEM.Decaps(ciphertext, secret_key)
Enter fullscreen mode Exit fullscreen mode

The sender generates a random shared secret, encrypts it with the receiver's public key, and sends the ciphertext. The receiver decrypts to get the same shared secret. No back-and-forth needed — it's a one-shot operation.

Hybrid Approach

ML-KEM-768 alone isn't enough. If it's broken (mathematically or implementation-wise), you have no fallback. The solution is hybrid key exchange:

use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey};
use ml_kem::{MlKem768, Encaps, Decaps, Encapsulate, Decapsulate};
use ml_kem::kpem::KemCore;

fn hybrid_key_exchange(
    x25519_secret: &EphemeralSecret,
    x25519_peer: &X25519PublicKey,
    mlkem_pk: &MlKem768::PublicKey,
) -> ([u8; 32], Vec<u8>) {
    // Classical key exchange
    let classical = x25519_secret.diffie_hellman(x25519_peer);

    // Post-quantum encapsulation
    let (ciphertext, pq_secret) = mlkem_pk.encapsulate(&mut OsRng)
        .expect("encapsulation should not fail");

    // Combine both secrets
    let mut combined = [0u8; 64];
    combined[..32].copy_from_slice(classical.as_bytes());
    combined[32..].copy_from_slice(&pq_secret);

    // Derive final key
    let final_key = hkdf_expand(&combined, b"ghostwire-hybrid-v1");

    (final_key, ciphertext.to_vec())
}
Enter fullscreen mode Exit fullscreen mode

Why concatenate? Because an attacker needs to break both X25519 and ML-KEM-768 to get the key. If ML-KEM falls to quantum computing, X25519 still protects you (until quantum computers break that too). If ML-KEM has a mathematical weakness, X25519 is unaffected.

The Key Sizes

This is where post-quantum gets real:

X25519 ML-KEM-768 Combined
Public key 32 bytes 1,184 bytes 1,216 bytes
Ciphertext 32 bytes 1,088 bytes 1,120 bytes
Shared secret 32 bytes 32 bytes 32 bytes

The public key is 37x larger. The ciphertext is 34x larger. This matters for mesh networks where bandwidth is limited (especially over LoRa, which has ~250 byte packets).

Our solution: Key exchange happens once per session (or when keys rotate). The bulk data (messages, files) uses the derived 32-byte key with XChaCha20-Poly1305. The large keys are only on the wire during handshake.

Performance

Benchmarked on an x86-64 machine:

Operation Time
ML-KEM-768 Keygen 15μs
ML-KEM-768 Encaps 28μs
ML-KEM-768 Decaps 31μs
X25519 Keygen 12μs
X25519 ECDH 45μs
Total hybrid ~75μs

75 microseconds for post-quantum secure key exchange. That's fast enough to do on every connection.

On ESP32-S3 (no hardware acceleration):

Operation Time
ML-KEM-768 Keygen 280μs
ML-KEM-768 Encaps 520μs
ML-KEM-768 Decaps 580μs
Total hybrid ~1.2ms

1.2ms on embedded hardware. Acceptable for a key exchange that happens once per session.

The Crate

We're using the ml-kem crate from RustCrypto:

[dependencies]
ml-kem = "0.2"
x25519-dalek = { version = "2", features = ["static_secrets"] }
Enter fullscreen mode Exit fullscreen mode

The crate implements NIST FIPS 203 (ML-KEM). It's audited, constant-time, and no-std compatible (works on ESP32).

What's Next

In the next article, we'll cover AI-routed mesh networking — how GhostWire uses a 4-layer routing system (LightGBM → Gemma 4 → GNN → PRoPHET) to find optimal paths through a dynamic mesh.

GitHub: github.com/Phantomojo/GhostWire-secure-mesh-communication

Website: ghostwire.cc


Built from Nairobi, under RVC.

Top comments (0)