DEV Community

Cover image for "I Put ML-KEM-768 Post-Quantum Crypto in a Mesh Network — Here's What Broke"
Michael Muriithi
Michael Muriithi

Posted on

"I Put ML-KEM-768 Post-Quantum Crypto in a Mesh Network — Here's What Broke"

ML-KEM-768
pqcrypto
2026


2026 is being called the Year of Quantum Security. The Global Risk Institute estimates a 44% probability of a cryptographically-relevant quantum computer by 2030. NIST just released Hamming Quasi-Cyclic (HQC) as the fifth post-quantum algorithm.

And I decided to integrate ML-KEM-768 (formerly Kyber 768) into a production mesh network.

Here's what broke, what worked, and what I'd do differently.


Why Post-Quantum for a Mesh Network?

GhostWire is built for crisis communication. When disasters strike, infrastructure fails, or communities are remote — people need to talk securely.

The threat model includes "Harvest Now, Decrypt Later":

  1. Adversary records your encrypted traffic today
  2. Waits 5-10 years for a quantum computer
  3. Decrypts everything retroactively

For activists, journalists, and security teams, this isn't theoretical. If you're communicating about sensitive topics today, your messages could be decrypted tomorrow.

Post-quantum cryptography solves this. Even with a quantum computer, ML-KEM-768 remains secure.


The Integration

Step 1: Adding the Dependency

# Cargo.toml
pqcrypto-mlkem = { version = "0.1.1" }
pqcrypto-traits = { version = "0.3" }
Enter fullscreen mode Exit fullscreen mode

pqcrypto-mlkem is a Rust wrapper around the C reference implementation of ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism).

Step 2: Key Generation

use pqcrypto_mlkem::kem768;
use pqcrypto_traits::kem::{PublicKey, SecretKey, Ciphertext, SharedSecret};

// Generate ML-KEM-768 keypair
let (pk, sk) = kem768::keypair();

// Public key: 1,184 bytes (vs 32 bytes for X25519)
// Secret key: 2,400 bytes (vs 32 bytes for X25519)
println!("Public key size: {} bytes", pk.as_bytes().len());
println!("Secret key size: {} bytes", sk.as_bytes().len());
Enter fullscreen mode Exit fullscreen mode

First thing that broke: Key sizes.

Algorithm Public Key Secret Key
X25519 32 bytes 32 bytes
ML-KEM-768 1,184 bytes 2,400 bytes

ML-KEM-768 keys are 37x larger than X25519. In a mesh network where every byte counts, this matters.

Step 3: Key Encapsulation

// Encapsulate: create shared secret using recipient's public key
let (ct, ss_sender) = kem768::encapsulate(&pk);

// Decapsulate: recover shared secret using recipient's secret key
let ss_receiver = kem768::decapsulate(&ct, &sk);

// Both sides now have the same shared secret
assert_eq!(ss_sender.as_bytes(), ss_receiver.as_bytes());
Enter fullscreen mode Exit fullscreen mode

What broke: Performance.

Operation X25519 ML-KEM-768 Slowdown
Key generation 12μs 89μs 7.4x
Encapsulation N/A 134μs
Decapsulation N/A 178μs
Total handshake 24μs 401μs 16.7x

For a mesh network with hundreds of peer connections, a 16.7x slowdown in key exchange is significant. But it's still under 1ms — acceptable for most use cases.

Step 4: Hybrid Key Exchange

We didn't replace X25519. We combined it with ML-KEM-768:

// Hybrid key exchange: X25519 + ML-KEM-768
// Security = max(classical, post-quantum)
// If either is secure, the combined key is secure

// Step 1: X25519 key exchange (fast, well-studied)
let x25519_shared = x25519_dalek::diffie_hellman(&my_x25519_secret, &their_x25519_public);

// Step 2: ML-KEM-768 key encapsulation (post-quantum)
let (ciphertext, mlkem_shared) = kem768::encapsulate(&their_mlkem_public);

// Step 3: Combine both shared secrets via HKDF
let mut combined = Vec::with_capacity(
    x25519_shared.as_bytes().len() + mlkem_shared.as_bytes().len()
);
combined.extend_from_slice(x25519_shared.as_bytes());
combined.extend_from_slice(mlkem_shared.as_bytes());

let final_key = hkdf::Hkdf::<Sha256>::new(None, &combined);
let mut output = [0u8; 32];
final_key.expand(b"ghostwire-hybrid-key", &mut output).unwrap();
Enter fullscreen mode Exit fullscreen mode

Why hybrid?

  • If ML-KEM-768 has a hidden flaw, X25519 still protects you
  • If X25519 is broken by quantum computers, ML-KEM-768 still protects you
  • You get the best of both worlds

What Broke

1. Message Size Explosion

ML-KEM-768 ciphertexts are 1,088 bytes (vs 32 bytes for X25519). Every key exchange message grew by 1KB.

Impact: On BLE (Bluetooth Low Energy) with 27-byte MTU, a single ML-KEM ciphertext requires 40 BLE packets. On LoRa with 250-byte packets, it requires 5 packets.

Fix: We batch key exchanges. Instead of rotating keys every message, we rotate every 1,000 messages. This amortizes the overhead.

2. Memory Pressure on Raspberry Pi

ML-KEM-768 operations allocate temporary buffers. On a Raspberry Pi with 1GB RAM running 100+ peer connections, this caused memory pressure.

Fix: We implemented a key exchange pool — pre-allocate buffers and reuse them. This reduced memory allocation overhead by 80%.

// Pre-allocated key exchange pool
pub struct KeyExchangePool {
    buffers: Vec<Mutex<Vec<u8>>>,
}

impl KeyExchangePool {
    pub fn new(size: usize) -> Self {
        Self {
            buffers: (0..size)
                .map(|_| Mutex::new(vec![0u8; 4096]))
                .collect(),
        }
    }

    pub fn acquire(&self) -> KeyExchangeGuard<'_> {
        // Find available buffer
        for buffer in &self.buffers {
            if let Ok(mut buf) = buffer.try_lock() {
                return KeyExchangeGuard { buffer: buf };
            }
        }
        // Allocate new if pool exhausted
        KeyExchangeGuard {
            buffer: Mutex::new(vec![0u8; 4096]).lock().unwrap(),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Compilation Time

Adding pqcrypto-mlkem pulled in the C reference implementation, which requires a C compiler and adds ~30 seconds to build time.

Fix: We pre-compile the C library and cache it in CI. Local builds use the cached version.

4. Cross-Compilation Nightmares

pqcrypto-mlkem uses C bindings. Cross-compiling for ARM (Raspberry Pi) and Android required setting up the C toolchain for each target.

Fix: We use cross (Docker-based cross-compilation) with pre-configured toolchains:

# Cross-compile for ARM64
cross build --target aarch64-unknown-linux-gnu --release

# Cross-compile for Android
cross build --target aarch64-linux-android --release
Enter fullscreen mode Exit fullscreen mode

Performance Results

After optimization:

Metric X25519 Only Hybrid (X25519 + ML-KEM) Overhead
Key exchange time 24μs 401μs 16.7x
Message size overhead 0 bytes 1,088 bytes per exchange +1KB
Memory per connection 64 bytes 3,648 bytes 57x
Throughput impact Baseline -2.3% Negligible
Security level Classical Classical + Post-Quantum Future-proof

The 2.3% throughput impact is acceptable for the security benefit.


The NIST Context

In March 2025, NIST released HQC (Hamming Quasi-Cyclic) as the fifth post-quantum algorithm, serving as a backup to ML-KEM-768. This means:

  1. ML-KEM-768 (Kyber) — Primary KEM standard
  2. ML-DSA (Dilithium) — Primary signature standard
  3. SLH-DSA (SPHINCS+) — Stateless hash-based signatures
  4. HQC — Backup KEM (lattice alternative)
  5. FN-DS — Backup signature

GhostWire uses ML-KEM-768 for key encapsulation. We're planning to add ML-DSA for signatures in the next release.


Quantum Timeline

Year Event
2025 NIST releases HQC as 5th PQC algorithm
2026 "Year of Quantum Security" — 44% probability of CRQC by 2030
2030 Estimated 50% probability of cryptographically-relevant quantum computer
2035 Estimated 90% probability

If you're building security-critical systems today, post-quantum isn't optional anymore. It's table stakes.


What I'd Do Differently

1. Start with Hybrid from Day One

Don't add post-quantum as an afterthought. Design your protocol for hybrid key exchange from the start. It's easier to disable it later than to add it later.

2. Use a Rust-Native Implementation

pqcrypto-mlkem uses C bindings. A pure Rust implementation would be easier to cross-compile and audit. We're watching the pqcrypto ecosystem for native Rust alternatives.

3. Benchmark Earlier

We didn't benchmark ML-KEM-768 performance until after integration. We should have tested on Raspberry Pi from day one. The 16.7x slowdown would have influenced our protocol design.

4. Plan for Key Size

1KB ciphertexts matter on BLE and LoRa. Design your protocol to handle large key exchange messages from the start.


Try It Yourself

git clone https://github.com/Phantomojo/GhostWire-secure-mesh-communication.git
cd GhostWire-secure-mesh-communication
cargo run --features security
Enter fullscreen mode Exit fullscreen mode

The hybrid key exchange is enabled by default in the security feature flag.


"The AI has to be faster than the crisis. The crypto has to be stronger than the quantum computer."

Built in Nairobi, for the world. 🇰🇪


About the Author: Michael (Phantomojo) is a Cybersecurity student at Open University of Kenya and Team Lead of Team GhostWire, competing in GCD4F 2026. He builds encrypted mesh networks, bio-adaptive honeypots, and offline AI assistants from Nairobi, Kenya.

Top comments (0)