ML-KEM

ML-KEM (Module-Lattice-Based Key-Encapsulation Mechanism) is the post-quantum key encapsulation mechanism standardized by NIST in FIPS 203. It is derived from CRYSTALS-Kyber.

This module generates, imports and exports ML-KEM keys. It does not offer encapsulation or decapsulation on their own: to encrypt data with an ML-KEM key, use HPKE (see Usage).

Its security is based on the hardness of the Module Learning With Errors problem (Module-LWE), which is believed to be hard even for quantum computers. Compared to ECC, keys and ciphertexts are much larger.

FIPS 203 defines three parameter sets:

Parameter set

Constant

NIST security category

Public key

Expanded private key

Ciphertext

ML-KEM-512

MLKEM.ML_KEM_512

1

800 bytes

1632 bytes

768 bytes

ML-KEM-768

MLKEM.ML_KEM_768

3

1184 bytes

2400 bytes

1088 bytes

ML-KEM-1024

MLKEM.ML_KEM_1024

5

1568 bytes

3168 bytes

1568 bytes

Pass one of these constants as the param_set argument. They are the members of the enum Crypto.PublicKey.MLKEM.ParamSet, and the value of each member is the name of its parameter set (for instance, 'ML-KEM-768'). If unsure, use ML-KEM-768, the most widely deployed. Some policies require ML-KEM-1024 (for instance, NSA’s CNSA 2.0).

A private key can be represented in two ways:

  • the seed (MLKEMKey.seed): the 64 random bytes the key is generated from. It is the most compact form, and the one recommended by RFC 9935.

  • the expanded private key (MLKEMKey.expanded_dk): the decapsulation key defined in FIPS 203. It cannot be converted back to the seed.

The public key is MLKEMKey.ek (the encapsulation key). The names follow the specifications (see Names in the specifications).

The implementation is based on mlkem-native.

Generation

You can generate a new ML-KEM key:

>>> from Crypto.PublicKey import MLKEM
>>>
>>> key = MLKEM.generate(param_set=MLKEM.ML_KEM_768)
>>> seed = key.seed       # 64 bytes, keep it secret
>>> ek = key.ek           # 1184 bytes, the public key

and rebuild it later from its seed (or from the expanded private key), or get the public key:

>>> key = MLKEM.construct(param_set=MLKEM.ML_KEM_768, seed=seed)
>>> pub_key = MLKEM.construct(param_set=MLKEM.ML_KEM_768, ek=ek)
>>> pub_key == key.public_key()
True

construct() checks that the key is valid (see the checks in its description).

To store a private key, prefer MLKEMKey.export_key() with a passphrase (see the next section): the seed on its own is not protected.

Import and Export

You can export an ML-KEM key as defined in RFC 9935: a public key as an X.509 SubjectPublicKeyInfo, a private key as a PKCS#8 OneAsymmetricKey, optionally protected with a passphrase. These are the usual formats for public and private keys, also used by OpenSSL (the keys of this module were tested with OpenSSL 4.0.3). The encoding is DER (binary, the default) or PEM (text, RFC 7468):

with open("myprivatekey.pem", "wt") as f:
    f.write(key.export_key(format="PEM", passphrase="secret"))

with open("mypublickey.der", "wb") as f:
    f.write(key.public_key().export_key())

and import it later (the format is detected automatically):

with open("myprivatekey.pem", "rt") as f:
    key = MLKEM.import_key(f.read(), "secret")

A private key is exported as its seed (or as the expanded private key, if the seed is not known). import_key() also accepts the other forms of RFC 9935 and X.509 certificates, and it checks that all the parts of the key match.

Usage

Currently, ML-KEM keys can only be used with HPKE (Crypto.Protocol.HPKE), alone or combined with ECDH in a hybrid KEM.

Names in the specifications

The specifications disagree on the meaning of the name dk, so this module never uses it on its own:

This module

FIPS 203

draft-ietf-hpke-pq

RFC 9935

seed

\(d \| z\)

dk

seed

expanded_dk

dk

expanded_dk

expandedKey

ek

ek

ek

public key

API

ML-KEM keys (FIPS 203): the parameter sets, the key class, and the functions to generate, build and import keys.

class Crypto.PublicKey.MLKEM.ParamSet(value)

The ML-KEM parameter sets (FIPS 203, Section 8). The value of each member is its name in FIPS 203.

ML_KEM_512 = 'ML-KEM-512'

NIST security category 1

ML_KEM_768 = 'ML-KEM-768'

NIST security category 3

ML_KEM_1024 = 'ML-KEM-1024'

NIST security category 5

Crypto.PublicKey.MLKEM.ML_KEM_512 = ParamSet.ML_KEM_512

Same as ParamSet.ML_KEM_512

Crypto.PublicKey.MLKEM.ML_KEM_768 = ParamSet.ML_KEM_768

Same as ParamSet.ML_KEM_768

Crypto.PublicKey.MLKEM.ML_KEM_1024 = ParamSet.ML_KEM_1024

Same as ParamSet.ML_KEM_1024

class Crypto.PublicKey.MLKEM.MLKEMKey(*, param_set, seed=None, expanded_dk=None, ek=None)

Class defining an ML-KEM key (private or public). Do not instantiate directly. Use generate() or construct() instead.

Variables:

param_set (ParamSet) – The parameter set.

has_private()

True if this is a private key.

Return type:

bool

property seed: bytes

The 64-byte seed of the private key (\(d \| z\) in FIPS 203).

Not available if the private key was created from the expanded form only.

property expanded_dk: bytes

The expanded decapsulation key (dk in FIPS 203, Algorithm 16).

property ek: bytes

The encapsulation key, that is, the public key (ek in FIPS 203, Algorithm 16).

public_key()

A matching ML-KEM public key.

Return type:

MLKEMKey

Returns:

a new MLKEMKey object

export_key(*, format='DER', passphrase=None, protection=None, prot_params=None, randfunc=None, threads=1)

Export this ML-KEM key, as defined in RFC 9935.

A public key is encoded as an X.509 SubjectPublicKeyInfo.

A private key is encoded as a PKCS#8 OneAsymmetricKey (in the clear, or encrypted if passphrase is set). It contains the 64-byte seed (the format recommended by RFC 9935), or the expanded private key if the seed of this key is not known (see seed).

Parameters:
  • format (string) –

    • 'DER' (default): binary encoding, as bytes.

    • 'PEM': textual encoding of RFC 7468, as a string, with the label PUBLIC KEY, PRIVATE KEY or ENCRYPTED PRIVATE KEY.

  • passphrase (bytes or string) – (Private keys only) The passphrase to protect the private key with PBES2 (PKCS#8 EncryptedPrivateKeyInfo).

  • protection (string) – (Private keys only) The encryption scheme, when passphrase is set. For all possible protection schemes, refer to the encryption parameters of PKCS#8. The default is 'PBKDF2WithHMAC-SHA256AndAES256-CBC'.

  • prot_params (dict) – (Private keys only) The parameters to derive the encryption key from the passphrase, when passphrase is set. For all possible values, refer to the encryption parameters of PKCS#8. The defaults meet the OWASP recommendations (with the default protection): there is no need to set them.

  • randfunc (callable) – (Private keys only) The RNG for the salt and the IV of the encryption. If None, Crypto.Random.get_random_bytes() is used.

  • threads (integer) – (Private keys only) The maximum number of threads used to derive the encryption key with scrypt (default: 1, no extra threads; 0 for all CPU cores). Only useful with a scrypt protection and a parallelization larger than 1 in prot_params. The output does not depend on the number of threads.

Warning

If you don’t provide a passphrase, the private key will be exported in the clear!

Return type:

Union[bytes, str]

Returns:

bytes (for 'DER') or a multi-line string (for 'PEM') with the encoded key.

Crypto.PublicKey.MLKEM.generate(*, param_set, randfunc=None)

Generate a new ML-KEM private key (FIPS 203, Algorithm 19).

Parameters:
  • param_set (ParamSet) – Mandatory. The parameter set, for instance MLKEM.ML_KEM_768.

  • randfunc (callable) – Optional. The RNG to read the 64-byte seed from. If None, Crypto.Random.get_random_bytes() is used.

Returns:

a new private key

Return type:

MLKEMKey

Crypto.PublicKey.MLKEM.construct(*, param_set, seed=None, expanded_dk=None, ek=None)

Build an ML-KEM key (private or public) from its components.

Parameters:
  • param_set (ParamSet) – Mandatory. The parameter set, for instance MLKEM.ML_KEM_768.

  • seed (bytes) – The 64-byte seed of a private key: \(d \| z\) in FIPS 203 (the input of ML-KEM.KeyGen_internal, Algorithm 16).

  • expanded_dk (bytes) – The expanded decapsulation key (dk in FIPS 203). It must pass the hash check of FIPS 203 (Section 7.3), and a pairwise consistency check (an encapsulation and a decapsulation), unless the seed is also present.

  • ek (bytes) – The encapsulation key, that is, the public key (ek in FIPS 203). It must pass the modulus check of FIPS 203 (Section 7.2).

At least one of seed, expanded_dk and ek must be present. If more than one is present, they must match, otherwise ValueError is raised.

Returns:

a new ML-KEM key object

Return type:

MLKEMKey

Crypto.PublicKey.MLKEM.import_key(encoded, passphrase=None, *, max_iteration_count=None, threads=1, max_memory=None)

Import an ML-KEM key (public or private), as defined in RFC 9935.

Parameters:
  • encoded (bytes or string) –

    The ML-KEM key to import, in DER format (bytes), or in PEM format (RFC 7468, as a string or as bytes). It can be:

    • For a public key: an X.509 SubjectPublicKeyInfo, or an X.509 certificate (the signature of the certificate is not checked).

    • For a private key: a PKCS#8 OneAsymmetricKey (version 1 or 2, in the clear or encrypted). The private key can contain the seed, the expanded private key or both.

    The parameters of the AlgorithmIdentifier must be absent (or NULL, which is tolerated).

    In PEM format, the label must match the content: PUBLIC KEY, CERTIFICATE (or X509 CERTIFICATE, X.509 CERTIFICATE), PRIVATE KEY or ENCRYPTED PRIVATE KEY. The legacy PEM encryption (with the Proc-Type header) is not supported.

    The same checks as in construct() apply. If the private key contains more than one among the seed, the expanded private key and the public key (in the publicKey field of a OneAsymmetricKey version 2), they must match.

  • passphrase (bytes or string) – The passphrase to decrypt an encrypted private key (PKCS#8 EncryptedPrivateKeyInfo). It is ignored if the key is not encrypted.

  • max_iteration_count (integer) – The maximum value allowed for the PBKDF2 iteration count or the scrypt cost N·r·p/8 (that is, N for the usual r=8 and p=1) of an encrypted private key. A malicious key could require an extremely high number of iterations, tying up the CPU for hours. The default value is None: a limit for each KDF, which keeps the key derivation to about one minute at most (see the limits for decryption). Set it to 0 to disable the check.

  • threads (integer) – The maximum number of threads used to derive the decryption key with scrypt (default: 1, no extra threads; 0 for all CPU cores). Only a private key protected with scrypt and a parallelization parameter larger than 1 can use them.

  • max_memory (integer) – The maximum memory (in bytes) for the key derivation of an encrypted private key, as a limit on the total of all threads together. The default value is None (a limit of 256 MiB). Set it to 0 to disable the check.

Returns:

a new ML-KEM key object

Return type:

MLKEMKey

Raises:
  • PassphraseRequiredError – (a ValueError) when the private key is encrypted, but no passphrase was provided.

  • IncorrectPassphraseError – (a ValueError) when the private key cannot be decrypted with the passphrase (most likely, the passphrase is wrong).

  • ValueError – when the key cannot be parsed, it is not an ML-KEM key, it fails the checks, or its components do not match.