ML-KEM
Header: #include <cryptopp/mlkem.h> | Namespace: CryptoPP
Since: cryptopp-modern 2026.3.0
Thread Safety: Encapsulator/Decapsulator instances are not thread-safe; use one per thread. Treat key objects as immutable after load; do not mutate a key while it is in use by other threads.
ML-KEM (Module-Lattice Key Encapsulation Mechanism) is a NIST post-quantum cryptographic standard (FIPS 203) based on CRYSTALS-Kyber. It provides secure key exchange resistant to attacks by quantum computers.
Key Features
- Post-quantum secure - Resistant to attacks by quantum computers
- IND-CCA2 secure - Chosen ciphertext attack security
- Implicit rejection - Returns pseudorandom shared secret on invalid ciphertext (side-channel resistant)
- 3 parameter sets - Security levels 1, 3, and 5
- Efficient - Fast lattice-based operations with NTT
Parameter Sets
| Parameter Set | Security Level | Public Key | Secret Key | Ciphertext | Shared Secret |
|---|---|---|---|---|---|
| ML-KEM-512 | 1 (128-bit) | 800 bytes | 1632 bytes | 768 bytes | 32 bytes |
| ML-KEM-768 | 3 (192-bit) | 1184 bytes | 2400 bytes | 1088 bytes | 32 bytes |
| ML-KEM-1024 | 5 (256-bit) | 1568 bytes | 3168 bytes | 1568 bytes | 32 bytes |
Recommendation: Use ML-KEM-768 for most applications (good balance of security and performance).
Quick Example
#include <cryptopp/mlkem.h>
#include <cryptopp/osrng.h>
#include <cryptopp/secblock.h>
#include <cstring>
#include <iostream>
int main() {
using namespace CryptoPP;
AutoSeededRandomPool rng;
// === Recipient generates key pair ===
MLKEMDecapsulator<MLKEM_768> recipient(rng);
// Extract public key to share with sender
const auto& rkey = recipient.GetKey();
SecByteBlock publicKey(rkey.GetPublicKeySize());
std::memcpy(publicKey.begin(), rkey.GetPublicKeyBytePtr(), publicKey.size());
// === Sender encapsulates with recipient's public key ===
MLKEMEncapsulator<MLKEM_768> sender(publicKey.begin(), publicKey.size());
SecByteBlock ciphertext(sender.CiphertextLength());
SecByteBlock senderSecret(sender.SharedSecretLength());
sender.Encapsulate(rng, ciphertext.begin(), senderSecret.begin());
// === Recipient decapsulates to get shared secret ===
SecByteBlock recipientSecret(recipient.SharedSecretLength());
// Note: Decapsulate() always returns true due to implicit rejection
(void)recipient.Decapsulate(ciphertext.begin(), recipientSecret.begin());
// Both parties now have the same shared secret
std::cout << "Shared secrets match: "
<< ((senderSecret == recipientSecret) ? "YES" : "NO") << "\n";
return 0;
}Usage Guidelines
Do:
- Use ML-KEM-768 for most applications (NIST Level 3, 192-bit security)
- Use the shared secret as input to a KDF for deriving session keys
- Store private keys securely using SecByteBlock
- Consider X-Wing for defence in depth (hybrid with X25519)
Avoid:
- Using ML-KEM-512 unless size constraints require it (Level 1 is minimum security)
- Using the shared secret directly without context (prefer KDF with protocol-specific info)
- Reusing ciphertexts - generate fresh encapsulation for each session
When to Use ML-KEM
Use ML-KEM when:
- You need pure post-quantum key exchange
- Size constraints make X-Wing impractical
- You’re building a PQC-only system
Consider X-Wing instead when:
- You want defence in depth (secure if either X25519 or ML-KEM is broken)
- Interoperability with hybrid-aware systems (TLS, Signal, etc.)
- You’re uncertain about lattice-based cryptography long-term
Consider X25519 instead when:
- Quantum computers are not a concern for your threat model
- Minimum bandwidth/storage is critical
- You need maximum performance
Class: MLKEMEncapsulator
Encapsulate (generate shared secret and ciphertext) using recipient’s public key.
Template Parameter
template <class PARAMS>
struct MLKEMEncapsulator;PARAMS is one of: MLKEM_512, MLKEM_768, MLKEM_1024
Constants
CRYPTOPP_CONSTANT(PUBLIC_KEYLENGTH = PARAMS::PUBLIC_KEY_SIZE);
CRYPTOPP_CONSTANT(CIPHERTEXT_LENGTH = PARAMS::CIPHERTEXT_SIZE);
CRYPTOPP_CONSTANT(SHARED_SECRET_LENGTH = PARAMS::SHARED_SECRET_SIZE);Constructors
Default Constructor
MLKEMEncapsulator();Create an uninitialized encapsulator. Set the public key before calling Encapsulate().
Constructor with Public Key
MLKEMEncapsulator(const byte* publicKey, size_t publicKeyLen);Create an encapsulator from recipient’s public key.
Parameters:
publicKey- Pointer to public key bytespublicKeyLen- Length of public key (must match parameter set)
Example:
// Receive public key from recipient
SecByteBlock publicKey = /* received from recipient */;
MLKEMEncapsulator<MLKEM_768> encapsulator(publicKey.begin(), publicKey.size());Methods
Encapsulate
void Encapsulate(RandomNumberGenerator& rng,
byte* ciphertext,
byte* sharedSecret) const;Generate a random shared secret and encrypt it for the recipient.
Parameters:
rng- Random number generatorciphertext- Output buffer for ciphertext (CiphertextLength() bytes)sharedSecret- Output buffer for shared secret (SharedSecretLength() bytes)
Example:
SecByteBlock ciphertext(encapsulator.CiphertextLength());
SecByteBlock sharedSecret(encapsulator.SharedSecretLength());
encapsulator.Encapsulate(rng, ciphertext.begin(), sharedSecret.begin());
// Send ciphertext to recipient
AccessPublicKey / GetPublicKey
MLKEMPublicKey<PARAMS>& AccessPublicKey();
const MLKEMPublicKey<PARAMS>& GetPublicKey() const;Access the public key for setting or retrieving.
CiphertextLength / SharedSecretLength
size_t CiphertextLength() const; // Returns CIPHERTEXT_LENGTH
size_t SharedSecretLength() const; // Returns 32
AlgorithmName
std::string AlgorithmName() const;Get the algorithm name (e.g., “ML-KEM-768”). Useful for logging and runtime identification.
Class: MLKEMDecapsulator
Generate key pairs and decapsulate (recover shared secret from ciphertext).
Template Parameter
template <class PARAMS>
struct MLKEMDecapsulator;PARAMS is one of: MLKEM_512, MLKEM_768, MLKEM_1024
Constants
CRYPTOPP_CONSTANT(SECRET_KEYLENGTH = PARAMS::SECRET_KEY_SIZE);
CRYPTOPP_CONSTANT(PUBLIC_KEYLENGTH = PARAMS::PUBLIC_KEY_SIZE);
CRYPTOPP_CONSTANT(CIPHERTEXT_LENGTH = PARAMS::CIPHERTEXT_SIZE);
CRYPTOPP_CONSTANT(SHARED_SECRET_LENGTH = PARAMS::SHARED_SECRET_SIZE);Constructors
Default Constructor
MLKEMDecapsulator();Create an uninitialized decapsulator. Generate or load a key before calling Decapsulate().
Constructor with RNG (Generate New Key Pair)
MLKEMDecapsulator(RandomNumberGenerator& rng);Generate a new key pair.
Example:
AutoSeededRandomPool rng;
MLKEMDecapsulator<MLKEM_768> decapsulator(rng);
// Share public key with senders
Constructor with Private Key
MLKEMDecapsulator(const byte* privateKey, size_t privateKeyLen);Load an existing private key.
Example:
// Load stored private key
SecByteBlock storedKey = /* loaded from storage */;
MLKEMDecapsulator<MLKEM_768> decapsulator(storedKey.begin(), storedKey.size());Methods
Decapsulate
bool Decapsulate(const byte* ciphertext, byte* sharedSecret) const;Recover the shared secret from a ciphertext.
Parameters:
ciphertext- Ciphertext from encapsulator (CiphertextLength() bytes)sharedSecret- Output buffer for shared secret (SharedSecretLength() bytes)
Returns: true (always returns true for ML-KEM due to implicit rejection)
Details:
ML-KEM uses implicit rejection: if decapsulation fails internally, a pseudorandom shared secret is returned instead of the real one. This prevents side-channel attacks by ensuring constant-time behaviour regardless of ciphertext validity. The sharedSecret buffer is always written.
Important: Do not branch handshake behaviour based on ciphertext validity. Treat the return value as diagnostic only (for logging/metrics), not as a control-flow gate.
Example:
SecByteBlock sharedSecret(decapsulator.SharedSecretLength());
// Always proceed with the shared secret - implicit rejection ensures safety
(void)decapsulator.Decapsulate(ciphertext.begin(), sharedSecret.begin());
// Use sharedSecret with a KDF
AccessPrivateKey / GetPrivateKey / GetKey
MLKEMPrivateKey<PARAMS>& AccessPrivateKey();
const MLKEMPrivateKey<PARAMS>& GetPrivateKey() const;
const MLKEMPrivateKey<PARAMS>& GetKey() const;Access the private key.
AlgorithmName
std::string AlgorithmName() const;Get the algorithm name (e.g., “ML-KEM-768”).
Class: MLKEMPrivateKey
Holds the secret key material for decapsulation.
Methods
GetAlgorithmID
OID GetAlgorithmID() const;Get the algorithm OID for this key type. Used in ASN.1/DER encoding.
GenerateRandom
void GenerateRandom(RandomNumberGenerator& rng, const NameValuePairs& params);Generate a new random key pair.
Example:
MLKEMPrivateKey<MLKEM_768> privateKey;
privateKey.GenerateRandom(rng, g_nullNameValuePairs);GetPublicKeyBytePtr / GetPublicKeySize
const byte* GetPublicKeyBytePtr() const;
size_t GetPublicKeySize() const;Get the embedded public key.
Example:
SecByteBlock publicKey(privateKey.GetPublicKeySize());
std::memcpy(publicKey.begin(), privateKey.GetPublicKeyBytePtr(), publicKey.size());GetPrivateKeyBytePtr / GetPrivateKeySize
const byte* GetPrivateKeyBytePtr() const;
size_t GetPrivateKeySize() const;Get the private key bytes.
Save / Load
void Save(BufferedTransformation& bt) const;
void Load(BufferedTransformation& bt);Serialize/deserialize in ASN.1 DER format.
Class: MLKEMPublicKey
Holds the public key material for encapsulation.
Methods
SetPublicKey
void SetPublicKey(const byte* key, size_t len);Set the public key bytes.
GetPublicKeyBytePtr / GetPublicKeySize
const byte* GetPublicKeyBytePtr() const;
size_t GetPublicKeySize() const;Get the public key bytes.
Save / Load
void Save(BufferedTransformation& bt) const;
void Load(BufferedTransformation& bt);Serialize/deserialize in ASN.1 DER format.
Key Serialization
Requires: #include <cryptopp/files.h> for file I/O examples.
Saving Keys (Raw Bytes)
MLKEMDecapsulator<MLKEM_768> decapsulator(rng);
const auto& privKey = decapsulator.GetKey();
// Save private key
SecByteBlock privateKeyBytes(privKey.GetPrivateKeySize());
std::memcpy(privateKeyBytes.begin(),
privKey.GetPrivateKeyBytePtr(),
privateKeyBytes.size());
// Save public key
SecByteBlock publicKeyBytes(privKey.GetPublicKeySize());
std::memcpy(publicKeyBytes.begin(),
privKey.GetPublicKeyBytePtr(),
publicKeyBytes.size());Loading Keys (Raw Bytes)
// Load private key for decapsulation
MLKEMDecapsulator<MLKEM_768> decapsulator(
privateKeyBytes.begin(), privateKeyBytes.size());
// Load public key for encapsulation
MLKEMEncapsulator<MLKEM_768> encapsulator(
publicKeyBytes.begin(), publicKeyBytes.size());Saving Keys (ASN.1 DER)
// Save private key to file
FileSink privFile("mlkem_private.der");
decapsulator.GetKey().Save(privFile);
// Save public key to file
FileSink pubFile("mlkem_public.der");
MLKEMPublicKey<MLKEM_768> pubKey;
pubKey.SetPublicKey(decapsulator.GetKey().GetPublicKeyBytePtr(),
decapsulator.GetKey().GetPublicKeySize());
pubKey.Save(pubFile);Loading Keys (ASN.1 DER)
// Load private key from file
FileSource privFile("mlkem_private.der", true);
MLKEMDecapsulator<MLKEM_768> decapsulator;
decapsulator.AccessPrivateKey().Load(privFile);
// Load public key from file
FileSource pubFile("mlkem_public.der", true);
MLKEMEncapsulator<MLKEM_768> encapsulator;
encapsulator.AccessPublicKey().Load(pubFile);Using with a KDF
Prefer deriving keys from the shared secret using a KDF with context:
#include <cryptopp/mlkem.h>
#include <cryptopp/hkdf.h>
#include <cryptopp/sha3.h>
#include <cryptopp/secblock.h>
// Assumes decapsulator and ciphertext from earlier example
SecByteBlock sharedSecret(32);
(void)decapsulator.Decapsulate(ciphertext.begin(), sharedSecret.begin());
// Derive encryption and MAC keys using HKDF
HKDF<SHA3_256> hkdf;
SecByteBlock encryptionKey(32);
SecByteBlock macKey(32);
const byte info1[] = "encryption key";
hkdf.DeriveKey(encryptionKey.begin(), encryptionKey.size(),
sharedSecret.begin(), sharedSecret.size(),
nullptr, 0, // salt (optional)
info1, sizeof(info1) - 1);
const byte info2[] = "mac key";
hkdf.DeriveKey(macKey.begin(), macKey.size(),
sharedSecret.begin(), sharedSecret.size(),
nullptr, 0,
info2, sizeof(info2) - 1);Indicative Performance
| Operation | ML-KEM-512 | ML-KEM-768 | ML-KEM-1024 |
|---|---|---|---|
| Key Generation | ~50 µs | ~80 µs | ~120 µs |
| Encapsulation | ~60 µs | ~90 µs | ~130 µs |
| Decapsulation | ~70 µs | ~100 µs | ~150 µs |
Illustrative figures only. Performance varies by platform; benchmark in your target environment.
Security Considerations
Shared Secret Usage - Prefer feeding the shared secret into a KDF with context (HKDF, SHA-3, etc.) to derive exactly the keys you need. FIPS 203 permits direct use as a symmetric key, but KDF-with-context is usually safer and more flexible.
Implicit Rejection - ML-KEM returns a pseudorandom shared secret on invalid ciphertext instead of failing. This is intentional for side-channel resistance.
Private Key Storage - Store private keys using SecByteBlock which zeros memory on destruction.
Forward Secrecy - Generate fresh key pairs for each session to achieve forward secrecy.
Hybrid Mode - For defence in depth, combine ML-KEM with classical key exchange using X-Wing.
Object Identifiers (OIDs)
Library-provided OIDs for ASN.1/DER encoding:
| Parameter Set | OID | Accessor |
|---|---|---|
| ML-KEM-512 | 2.16.840.1.101.3.4.4.1 | ASN1::id_ml_kem_512() |
| ML-KEM-768 | 2.16.840.1.101.3.4.4.2 | ASN1::id_ml_kem_768() |
| ML-KEM-1024 | 2.16.840.1.101.3.4.4.3 | ASN1::id_ml_kem_1024() |
Usage:
// Get OID from key object
OID oid = privateKey.GetAlgorithmID();
// Get OID statically from parameter set
OID oid = MLKEM_768::StaticAlgorithmOID();
// Compare OIDs
if (oid == ASN1::id_ml_kem_768()) {
// ML-KEM-768 key
}Specification Compliance
This implementation follows NIST FIPS 203 (August 2024):
- Key Generation: Algorithm 16 with domain separation
- Encapsulation: Algorithm 20
- Decapsulation: Algorithm 21 with implicit rejection
- Hash Functions: SHA3-256, SHA3-512, SHAKE256
Test vectors from NIST ACVP validate compliance.
See Also
- FIPS 203 - ML-KEM Standard
- X-Wing - Hybrid KEM (X25519 + ML-KEM-768)
- ML-DSA - Post-quantum signatures
- PQC Overview - Algorithm comparison and guidance