Skip to content
ecdsa.com

ML-KEM-768 encapsulation · Rust

How to encapsulate and decapsulate a shared secret with ML-KEM-768 in Rust

To encapsulate and decapsulate a shared secret with ML-KEM-768 in Rust, use RustCrypto's ml-kem crate: MlKem768::generate_keypair() creates the keys, encapsulation_key.encapsulate() returns the ciphertext and the secret, and decapsulation_key.decapsulate(&ciphertext) recovers the secret. This recipe also reads OpenSSL 3.5 keys from PEM and exchanges ciphertexts with OpenSSL in both directions.

Tested with ml-kem 0.3.3 (Rust 1.99.1, RustCrypto, pkcs8 0.11.0) and OpenSSL 3.5.7 (decapsulation of the recipe's ciphertext).

Cargo.toml — ml-kem with getrandom and PEM support, pkcs8
[dependencies]
ml-kem = { version = "=0.3.3", features = ["getrandom", "pem"] }
pkcs8 = { version = "=0.11.0", features = ["pem"] }
src/kem.rs — key pair, encapsulation, decapsulation
use ml_kem::MlKem768;
use ml_kem::kem::{Decapsulate, Encapsulate, Kem};
pub fn agree() {
// Receiver: a key pair from OS randomness ("getrandom" feature)
let (decapsulation_key, encapsulation_key) = MlKem768::generate_keypair();
// Sender: a fresh shared secret and the ciphertext that carries it
let (ciphertext, sender_secret) = encapsulation_key.encapsulate();
// Receiver: the same secret from the ciphertext
let receiver_secret = decapsulation_key.decapsulate(&ciphertext);
println!("ciphertext {} bytes, shared secret {} bytes", ciphertext.len(), sender_secret.len());
println!("shared secret agreed: {}", sender_secret == receiver_secret);
}

How it works

  1. MlKem768::generate_keypair() (from the kem::Kem trait) returns (decapsulation_key, encapsulation_key).
  2. encapsulation_key.encapsulate() returns (ciphertext, shared_secret) — 1088 and 32 bytes, in that order, which is the reverse of Python and Go.
  3. decapsulation_key.decapsulate(&ciphertext) returns the same secret; with a byte slice from the network use decapsulate_slice, which checks the length.
  4. EncapsulationKey768::from_public_key_pem and DecapsulationKey768::from_pkcs8_pem read OpenSSL's files; the run decapsulates OpenSSL's ciphertext to dee35810…30f9, and OpenSSL decapsulates the ciphertext Rust made.

Gotchas

  • Unlike ml-dsa, ml-kem has no randomness by default: generate_keypair and encapsulate need the getrandom feature, as in the Cargo.toml above.
  • A tampered ciphertext still decapsulates (Ok) to a different secret — implicit rejection. Only the length is checked: a 1000-byte slice gives Err(TryFromSliceError(())).
  • Secrets compare with == on the returned arrays; in a real protocol you do not compare them at all — each side derives keys from its own copy.

Related recipes