ML-DSA

ML-DSA (Module-Lattice-Based Digital Signature Algorithm) is the post-quantum signature algorithm standardized by NIST in FIPS 204. It is derived from CRYSTALS-Dilithium.

Its security is based on the hardness of lattice problems (Module-LWE and Module-SIS), which are believed to be hard even for quantum computers. Compared to RSA and ECC, keys and signatures are much larger.

FIPS 204 defines three parameter sets:

Parameter set

Constant

NIST security category

Public key

Expanded private key

Signature

ML-DSA-44

MLDSA.ML_DSA_44

2

1312 bytes

2560 bytes

2420 bytes

ML-DSA-65

MLDSA.ML_DSA_65

3

1952 bytes

4032 bytes

3309 bytes

ML-DSA-87

MLDSA.ML_DSA_87

5

2592 bytes

4896 bytes

4627 bytes

Pass one of these constants as the param_set argument. They are the members of the enum Crypto.PublicKey.MLDSA.ParamSet, and the value of each member is the name of its parameter set (for instance, 'ML-DSA-65').

A private key can be represented in two ways:

  • the seed: the 32 random bytes the key is generated from (\(\xi\) in FIPS 204). It is the most compact form, and the one recommended by RFC 9881.

  • the expanded private key: the encoding defined in FIPS 204 (Algorithm 24). It cannot be converted back to the seed.

With this module you can generate a new ML-DSA key:

>>> from Crypto.PublicKey import MLDSA
>>>
>>> key = MLDSA.generate(param_set=MLDSA.ML_DSA_65)
>>> seed = key.seed       # 32 bytes, keep it secret
>>> pk = key.pk           # 1952 bytes

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

>>> key = MLDSA.construct(param_set=MLDSA.ML_DSA_65, seed=seed)
>>> pub_key = MLDSA.construct(param_set=MLDSA.ML_DSA_65, pk=pk)
>>> pub_key == key.public_key()
True

You can export an ML-DSA key as defined in RFC 9881: a public key as an X.509 SubjectPublicKeyInfo, a private key as a PKCS#8 OneAsymmetricKey, optionally protected with a passphrase. 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 = MLDSA.import_key(f.read(), "secret")

The private key in the OneAsymmetricKey is the seed or, if the seed is not known, the expanded private key. It does not contain the public key (the optional publicKey field of OneAsymmetricKey version 2), which can always be computed again. import_key() accepts all the variants of RFC 9881 (the seed, the expanded private key, or both, with or without the public key), and it checks that the seed, the expanded private key and the public key match.

To sign messages and verify signatures with these keys, use Crypto.Signature.mldsa.

The implementation is based on mldsa-native,

ML-DSA keys (FIPS 204)

class Crypto.PublicKey.MLDSA.MLDSAKey(*, param_set, seed=None, sk=None, pk=None)

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

Variables:

param_set (ParamSet) – The parameter set.

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

Export this ML-DSA key, as defined in RFC 9881.

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 32-byte seed (the format recommended by RFC 9881), 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.

has_private()

True if this key can be used for making signatures.

Return type:

bool

property pk: bytes

The public key, encoded as in FIPS 204 (Algorithm 22).

public_key()

A matching ML-DSA public key.

Return type:

MLDSAKey

Returns:

a new MLDSAKey object

property seed: bytes

The 32-byte seed of the private key (\(\xi\) in FIPS 204).

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

property sk: bytes

The expanded private key, encoded as in FIPS 204 (Algorithm 24).

Crypto.PublicKey.MLDSA.ML_DSA_44 = ParamSet.ML_DSA_44

Same as ParamSet.ML_DSA_44

Crypto.PublicKey.MLDSA.ML_DSA_65 = ParamSet.ML_DSA_65

Same as ParamSet.ML_DSA_65

Crypto.PublicKey.MLDSA.ML_DSA_87 = ParamSet.ML_DSA_87

Same as ParamSet.ML_DSA_87

class Crypto.PublicKey.MLDSA.ParamSet(value)

The ML-DSA parameter sets (FIPS 204, Section 4). The value of each member is its name in FIPS 204.

ML_DSA_44 = 'ML-DSA-44'

NIST security category 2

ML_DSA_65 = 'ML-DSA-65'

NIST security category 3

ML_DSA_87 = 'ML-DSA-87'

NIST security category 5

Crypto.PublicKey.MLDSA.construct(*, param_set, seed=None, sk=None, pk=None)

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

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

  • seed (bytes) – The 32-byte seed of a private key (\(\xi\) in FIPS 204).

  • sk (bytes) – The expanded private key, encoded as in FIPS 204 (Algorithm 24). Its consistency is checked (but not whether it was generated correctly).

  • pk (bytes) – The public key, encoded as in FIPS 204 (Algorithm 22).

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

Returns:

a new ML-DSA key object

Return type:

MLDSAKey

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

Generate a new ML-DSA private key (FIPS 204, Algorithm 1).

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

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

Returns:

a new private key

Return type:

MLDSAKey

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

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

Parameters:
  • encoded (bytes or string) –

    The ML-DSA 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.

    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-DSA key object

Return type:

MLDSAKey

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-DSA key, or its components do not match.