I maintain Qeli, a self-hosted VPN whose core and server are written in Rust. For the 0.7.x line I added a hybrid post-quantum key exchange to the inner handshake, and wired the same primitive into the non-Rust clients. Here is how it is built and what bit me.
Why hybrid, not pure PQ
The threat is "harvest now, decrypt later": traffic captured today, decrypted once a large quantum computer exists. Classical X25519 does not survive that; pure ML-KEM is young, and I do not want one new primitive to be the only thing between you and plaintext. So the handshake runs both and mixes the results - you are safe unless both X25519 and ML-KEM-768 fall.
The shape of the handshake
- Classical: X25519 ephemeral ECDH.
- Post-quantum: ML-KEM-768 (FIPS 203), via the RustCrypto stack - one side encapsulates to the other's ephemeral KEM public key.
- The two shared secrets are concatenated and run through HKDF-SHA256 to derive the session keys. The data plane is ChaCha20-Poly1305; password-derived secrets use Argon2id.
Mixing both secrets in the KDF (instead of picking one) is what makes it hybrid: an attacker has to break both to recover the key.
One PQ core across Rust, C# and Kotlin
Qeli has native clients on Windows/macOS (C#) and Android (Kotlin). Rather than reimplement ML-KEM three times, the Rust implementation is the single source of truth, and the other clients call into a small native core over FFI (C#) and JNI (Android). Same wire format everywhere, and the PQ code gets reviewed once.
Things that bit me
- Wire compatibility. Turning on PQ changed the handshake bytes. I kept it wire-compatible within 0.7.x, but it is a breaking change versus older clients - versioning the handshake from day one would have saved pain.
- Message sizes. ML-KEM-768 public keys and ciphertexts are ~1.1 KB, far larger than X25519's 32 bytes. Worth accounting for in your framing and MTU assumptions.
- Keep the classical path. It is tempting to drop X25519 once PQ works, but hybrid is the whole point - the KDF step has to bind both.
Try it
Code is open (AGPL-3.0 core, MPL-2.0 clients): https://github.com/litvinovtd/qeli - and there is a project site at https://qeli.ru. Feedback on the handshake and transport code is very welcome.
Top comments (0)