Skip to content

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 SetSecurity LevelPublic KeySecret KeyCiphertextShared Secret
ML-KEM-5121 (128-bit)800 bytes1632 bytes768 bytes32 bytes
ML-KEM-7683 (192-bit)1184 bytes2400 bytes1088 bytes32 bytes
ML-KEM-10245 (256-bit)1568 bytes3168 bytes1568 bytes32 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 bytes
  • publicKeyLen - 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 generator
  • ciphertext - 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

OperationML-KEM-512ML-KEM-768ML-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

  1. 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.

  2. Implicit Rejection - ML-KEM returns a pseudorandom shared secret on invalid ciphertext instead of failing. This is intentional for side-channel resistance.

  3. Private Key Storage - Store private keys using SecByteBlock which zeros memory on destruction.

  4. Forward Secrecy - Generate fresh key pairs for each session to achieve forward secrecy.

  5. 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 SetOIDAccessor
ML-KEM-5122.16.840.1.101.3.4.4.1ASN1::id_ml_kem_512()
ML-KEM-7682.16.840.1.101.3.4.4.2ASN1::id_ml_kem_768()
ML-KEM-10242.16.840.1.101.3.4.4.3ASN1::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