ML-DSA
Header: #include <cryptopp/mldsa.h> | Namespace: CryptoPP
Since: cryptopp-modern 2026.3.0
Thread Safety: Signer/Verifier instances are not thread-safe; use one per thread. If sharing key bytes, load them into separate instances per thread.
ML-DSA (Module-Lattice Digital Signature Algorithm) is a NIST post-quantum cryptographic standard (FIPS 204) based on CRYSTALS-Dilithium. It provides digital signatures resistant to attacks by quantum computers.
Key Features
- Post-quantum secure - Resistant to attacks by quantum computers
- EUF-CMA secure - Existential unforgeability under chosen-message attack
- Probabilistic signing - Uses RNG with rejection sampling; signature output may vary
- 3 parameter sets - Security levels 2, 3, and 5
- Efficient - Fast lattice-based operations with NTT
Parameter Sets
| Parameter Set | Security Level | Public Key | Secret Key | Signature |
|---|---|---|---|---|
| ML-DSA-44 | 2 (128-bit) | 1312 bytes | 2560 bytes | 2420 bytes |
| ML-DSA-65 | 3 (192-bit) | 1952 bytes | 4032 bytes | 3309 bytes |
| ML-DSA-87 | 5 (256-bit) | 2592 bytes | 4896 bytes | 4627 bytes |
Recommendation: Use ML-DSA-65 for most applications (good balance of security and performance).
Quick Example
#include <cryptopp/mldsa.h>
#include <cryptopp/osrng.h>
#include <cryptopp/secblock.h>
#include <string>
#include <iostream>
int main() {
using namespace CryptoPP;
AutoSeededRandomPool rng;
// === Generate key pair and create signer ===
MLDSASigner<MLDSA_65> signer(rng);
// === Create verifier from signer's public key ===
MLDSAVerifier<MLDSA_65> verifier(signer);
// === Sign a message ===
std::string message = "Hello, post-quantum world!";
SecByteBlock signature(signer.SignatureLength());
size_t sigLen = signer.SignMessage(
rng,
(const byte*)message.data(),
message.size(),
signature.begin()
);
// === Verify the signature ===
bool valid = verifier.VerifyMessage(
(const byte*)message.data(),
message.size(),
signature.begin(),
sigLen
);
std::cout << "Signature valid: " << (valid ? "YES" : "NO") << std::endl;
return 0;
}Usage Guidelines
Do:
- Use ML-DSA-65 for most applications (NIST Level 3, 192-bit security)
- Store private keys securely using SecByteBlock
- Consider hybrid signatures combining ML-DSA with Ed25519 for defence in depth
- Use ML-DSA-87 for high-security applications (government, long-term documents)
Avoid:
- Using ML-DSA-44 unless size constraints require it (Level 2 is minimum security)
- Modifying signatures or messages (verification will fail)
- Exposing private keys in logs or error messages
- Using the same key pair across different security domains
When to Use ML-DSA
Use ML-DSA when:
- You need post-quantum secure digital signatures
- Signing code, certificates, or long-lived documents
- Preparing for “harvest now, decrypt later” threats
- Building post-quantum secure authentication systems
Consider SLH-DSA instead when:
- You want hash-based security (no lattice assumptions)
- Conservative security requirements demand different mathematical basis
- Larger signatures are acceptable for your use case
Consider Ed25519 instead when:
- Quantum computers are not a concern for your threat model
- Minimum signature size is critical (64 bytes vs 3309+ bytes)
- Maximum verification speed is required
Class: MLDSASigner
Sign messages using a private key.
Template Parameter
template <class PARAMS>
struct MLDSASigner;PARAMS is one of: MLDSA_44, MLDSA_65, MLDSA_87
Constants
CRYPTOPP_CONSTANT(SECRET_KEYLENGTH = PARAMS::SECRET_KEY_SIZE);
CRYPTOPP_CONSTANT(PUBLIC_KEYLENGTH = PARAMS::PUBLIC_KEY_SIZE);
CRYPTOPP_CONSTANT(SIGNATURE_LENGTH = PARAMS::SIGNATURE_SIZE);Constructors
Default Constructor
MLDSASigner();Create an empty signer. Use AccessKey().GenerateRandom(rng, params) to generate a key.
Constructor with RNG (Generate New Key Pair)
MLDSASigner(RandomNumberGenerator &rng);Generate a new key pair.
Example:
AutoSeededRandomPool rng;
MLDSASigner<MLDSA_65> signer(rng);
// Ready to sign messages
Constructor with Private Key
MLDSASigner(const byte *privateKey, size_t privateKeyLen);Load an existing private key.
Example:
// Load stored private key
SecByteBlock storedKey = /* loaded from storage */;
MLDSASigner<MLDSA_65> signer(storedKey.begin(), storedKey.size());Methods
SignMessage (Convenience Method)
size_t SignMessage(RandomNumberGenerator &rng,
const byte *message, size_t messageLen,
byte *signature) const;Sign a message in one call (inherited from PK_Signer).
Parameters:
rng- Random number generatormessage- Message to signmessageLen- Length of messagesignature- Output buffer for signature (SignatureLength() bytes)
Returns: Actual signature length
Example:
std::string message = "Document to sign";
SecByteBlock signature(signer.SignatureLength());
size_t sigLen = signer.SignMessage(rng,
(const byte*)message.data(), message.size(),
signature.begin());SignatureLength
size_t SignatureLength() const;Get the signature size in bytes.
GetKey / AccessKey
const MLDSAPrivateKey<PARAMS>& GetKey() const;
MLDSAPrivateKey<PARAMS>& AccessKey();Access the private key.
AlgorithmName
std::string AlgorithmName() const;Get the algorithm name (e.g., “ML-DSA-65”). Useful for logging and runtime identification.
Class: MLDSAVerifier
Verify signatures using a public key.
Template Parameter
template <class PARAMS>
struct MLDSAVerifier;PARAMS is one of: MLDSA_44, MLDSA_65, MLDSA_87
Constants
CRYPTOPP_CONSTANT(PUBLIC_KEYLENGTH = PARAMS::PUBLIC_KEY_SIZE);
CRYPTOPP_CONSTANT(SIGNATURE_LENGTH = PARAMS::SIGNATURE_SIZE);Constructors
Default Constructor
MLDSAVerifier();Create an empty verifier. Use AccessKey().SetPublicKey() to set the key.
Constructor from Signer
MLDSAVerifier(const MLDSASigner<PARAMS> &signer);Create a verifier from a signer’s public key.
Example:
MLDSASigner<MLDSA_65> signer(rng);
MLDSAVerifier<MLDSA_65> verifier(signer);Constructor with Public Key
MLDSAVerifier(const byte *publicKey, size_t publicKeyLen);Load a public key.
Example:
SecByteBlock publicKey = /* received from signer */;
MLDSAVerifier<MLDSA_65> verifier(publicKey.begin(), publicKey.size());Methods
VerifyMessage (Convenience Method)
bool VerifyMessage(const byte *message, size_t messageLen,
const byte *signature, size_t signatureLen) const;Verify a signature in one call (inherited from PK_Verifier).
Parameters:
message- Original messagemessageLen- Length of messagesignature- Signature to verifysignatureLen- Length of signature
Returns: true if signature is valid, false otherwise
Example:
bool valid = verifier.VerifyMessage(
(const byte*)message.data(), message.size(),
signature.begin(), signature.size());
if (valid) {
std::cout << "Signature verified!" << std::endl;
}SignatureLength
size_t SignatureLength() const;Get the expected signature size in bytes.
GetKey / AccessKey
const MLDSAPublicKey<PARAMS>& GetKey() const;
MLDSAPublicKey<PARAMS>& AccessKey();Access the public key.
AlgorithmName
std::string AlgorithmName() const;Get the algorithm name (e.g., “ML-DSA-65”).
Class: MLDSAPrivateKey
Holds the secret key material for signing.
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 ¶ms);Generate a new random key pair.
Example:
MLDSAPrivateKey<MLDSA_65> privateKey;
privateKey.GenerateRandom(rng, g_nullNameValuePairs);GetPublicKeyBytePtr / GetPublicKeySize
const byte* GetPublicKeyBytePtr() const;
size_t GetPublicKeySize() const;Get the embedded public key.
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 (PKCS#8 OneAsymmetricKey).
Class: MLDSAPublicKey
Holds the public key material for verification.
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 (X.509 SubjectPublicKeyInfo).
Key Serialization
Requires: #include <cryptopp/files.h> for file I/O, #include <cstring> for std::memcpy.
Note: This implementation buffers the entire message before signing/verification. For very large messages, consider hashing first and signing the digest.
Saving Keys (Raw Bytes)
// Generate key pair
MLDSASigner<MLDSA_65> signer(rng);
const auto& privKey = signer.GetKey();
// Save private key (includes secret and public parts)
SecByteBlock privateKeyBytes(privKey.GetPrivateKeySize());
std::memcpy(privateKeyBytes.begin(),
privKey.GetPrivateKeyBytePtr(),
privateKeyBytes.size());
// Save public key (for distribution)
SecByteBlock publicKeyBytes(privKey.GetPublicKeySize());
std::memcpy(publicKeyBytes.begin(),
privKey.GetPublicKeyBytePtr(),
publicKeyBytes.size());Loading Keys (Raw Bytes)
// Load private key (for signing)
MLDSASigner<MLDSA_65> signer(
privateKeyBytes.begin(), privateKeyBytes.size());
// Load public key only (for verification)
MLDSAVerifier<MLDSA_65> verifier(
publicKeyBytes.begin(), publicKeyBytes.size());Saving Keys (ASN.1 DER)
// Save private key to file
FileSink privFile("mldsa_private.der");
signer.GetKey().Save(privFile);
// Save public key to file
FileSink pubFile("mldsa_public.der");
MLDSAPublicKey<MLDSA_65> pubKey;
pubKey.SetPublicKey(signer.GetKey().GetPublicKeyBytePtr(),
signer.GetKey().GetPublicKeySize());
pubKey.Save(pubFile);Loading Keys (ASN.1 DER)
// Load private key from file
FileSource privFile("mldsa_private.der", true);
MLDSASigner<MLDSA_65> signer;
signer.AccessKey().Load(privFile);
// Load public key from file
FileSource pubFile("mldsa_public.der", true);
MLDSAVerifier<MLDSA_65> verifier;
verifier.AccessKey().Load(pubFile);Convenience Type Aliases
// ML-DSA-44 (Level 2, 128-bit security)
typedef MLDSASigner<MLDSA_44> MLDSA_44_Signer;
typedef MLDSAVerifier<MLDSA_44> MLDSA_44_Verifier;
// ML-DSA-65 (Level 3, 192-bit security)
typedef MLDSASigner<MLDSA_65> MLDSA_65_Signer;
typedef MLDSAVerifier<MLDSA_65> MLDSA_65_Verifier;
// ML-DSA-87 (Level 5, 256-bit security)
typedef MLDSASigner<MLDSA_87> MLDSA_87_Signer;
typedef MLDSAVerifier<MLDSA_87> MLDSA_87_Verifier;Example using convenience types:
MLDSA_65_Signer signer(rng);
MLDSA_65_Verifier verifier(signer);Hybrid Signature Example
For defence in depth, combine ML-DSA with classical signatures:
#include <cryptopp/mldsa.h>
#include <cryptopp/xed25519.h>
#include <cryptopp/sha3.h>
struct HybridSignature {
SecByteBlock ed25519Sig; // 64 bytes
SecByteBlock mldsaSig; // 3309 bytes for ML-DSA-65
};
HybridSignature HybridSign(RandomNumberGenerator& rng,
const ed25519Signer& ed25519Signer,
const MLDSASigner<MLDSA_65>& mldsaSigner,
const byte* message, size_t messageLen)
{
HybridSignature sig;
// Ed25519 signature
sig.ed25519Sig.resize(64);
ed25519Signer.SignMessage(rng, message, messageLen, sig.ed25519Sig.begin());
// ML-DSA signature
sig.mldsaSig.resize(mldsaSigner.SignatureLength());
mldsaSigner.SignMessage(rng, message, messageLen, sig.mldsaSig.begin());
return sig;
}
bool HybridVerify(const ed25519Verifier& ed25519Verifier,
const MLDSAVerifier<MLDSA_65>& mldsaVerifier,
const byte* message, size_t messageLen,
const HybridSignature& sig)
{
// Both signatures must be valid
bool ed25519Valid = ed25519Verifier.VerifyMessage(
message, messageLen, sig.ed25519Sig.begin(), sig.ed25519Sig.size());
bool mldsaValid = mldsaVerifier.VerifyMessage(
message, messageLen, sig.mldsaSig.begin(), sig.mldsaSig.size());
return ed25519Valid && mldsaValid;
}Context Strings
FIPS 204 supports an optional context string (up to 255 bytes) that binds a signature to a specific application or protocol. This prevents cross-protocol signature reuse.
Context strings are set on the message accumulator before signing or verifying:
#include <cryptopp/mldsa.h>
#include <cryptopp/osrng.h>
#include <cryptopp/secblock.h>
#include <string>
using namespace CryptoPP;
AutoSeededRandomPool rng;
MLDSASigner<MLDSA_65> signer(rng);
MLDSAVerifier<MLDSA_65> verifier(signer);
std::string message = "Important document";
const byte ctx[] = "myapp-v1";
// Sign with context
auto* signAcc = signer.NewSignatureAccumulator(rng);
auto* mldsaSignAcc = dynamic_cast<MLDSA_MessageAccumulator<MLDSA_65>*>(signAcc);
mldsaSignAcc->SetContext(ctx, sizeof(ctx) - 1);
mldsaSignAcc->Update((const byte*)message.data(), message.size());
SecByteBlock signature(signer.SignatureLength());
size_t sigLen = signer.SignAndRestart(rng, *signAcc, signature.begin());
delete signAcc;
// Verify with same context
auto* verAcc = verifier.NewVerificationAccumulator();
auto* mldsaVerAcc = dynamic_cast<MLDSA_MessageAccumulator<MLDSA_65>*>(verAcc);
mldsaVerAcc->SetContext(ctx, sizeof(ctx) - 1);
mldsaVerAcc->Update((const byte*)message.data(), message.size());
verifier.InputSignature(*verAcc, signature.begin(), sigLen);
bool valid = verifier.VerifyAndRestart(*verAcc);
delete verAcc;
// valid == true (context matches)
Notes:
- Context must be identical for signing and verification; a mismatch will cause verification to fail
- Empty context (the default) is valid and interoperable with implementations that don’t set a context
- Maximum context length is 255 bytes;
SetContextthrowsInvalidArgumentif exceeded
Indicative Performance
| Operation | ML-DSA-44 | ML-DSA-65 | ML-DSA-87 |
|---|---|---|---|
| Key Generation | ~100 µs | ~150 µs | ~200 µs |
| Signing | ~200 µs | ~300 µs | ~400 µs |
| Verification | ~100 µs | ~150 µs | ~200 µs |
Illustrative figures only. Performance varies by platform; benchmark in your target environment.
Notes:
- Signing uses rejection sampling, so times can vary
- Verification is typically faster than signing
- ML-DSA-87 provides highest security but largest signatures
Security Considerations
Private Key Protection - Store private keys using SecByteBlock which zeros memory on destruction. Never log or expose private key bytes.
Key Lifetime - Consider key rotation policies. Long-lived signing keys have increased exposure risk.
Signature Verification - Always verify signatures before trusting signed data. Invalid signatures should be treated as a security event.
Quantum Timeline - ML-DSA is designed for “harvest now, decrypt later” threats. Consider migrating now for long-lived signatures (code signing, certificates).
Hybrid Mode - For critical applications, combine ML-DSA with Ed25519 or ECDSA. Security is maintained as long as either algorithm remains secure.
Side Channels - The implementation uses constant-time operations where feasible. Avoid timing measurements on signing operations.
Object Identifiers (OIDs)
Library-provided OIDs for ASN.1/DER encoding:
| Parameter Set | OID | Accessor |
|---|---|---|
| ML-DSA-44 | 2.16.840.1.101.3.4.3.17 | ASN1::id_ml_dsa_44() |
| ML-DSA-65 | 2.16.840.1.101.3.4.3.18 | ASN1::id_ml_dsa_65() |
| ML-DSA-87 | 2.16.840.1.101.3.4.3.19 | ASN1::id_ml_dsa_87() |
Usage:
// Get OID from key object
OID oid = privateKey.GetAlgorithmID();
// Get OID statically from parameter set
OID oid = MLDSA_65::StaticAlgorithmOID();
// Compare OIDs
if (oid == ASN1::id_ml_dsa_65()) {
// ML-DSA-65 key
}Specification Compliance
This implementation follows NIST FIPS 204 (August 2024):
- Key Generation: Uses SHAKE256 for matrix and secret vector expansion
- Signing: Implements rejection sampling loop with proper hint generation
- Verification: Reconstructs commitment and validates challenge hash
- Primitives: SHAKE256 for all XOF operations, SHA-3 for hashing
- Parameters: All three parameter sets (44, 65, 87) match FIPS 204 specifications
FIPS 204 Internal Parameters:
| Parameter Set | k | l | eta | gamma1 | gamma2 | omega |
|---|---|---|---|---|---|---|
| ML-DSA-44 | 4 | 4 | 2 | 2^17 | (q-1)/88 | 80 |
| ML-DSA-65 | 6 | 5 | 4 | 2^19 | (q-1)/32 | 55 |
| ML-DSA-87 | 8 | 7 | 2 | 2^19 | (q-1)/32 | 75 |
See Also
- FIPS 204 - ML-DSA Standard
- ML-KEM - Post-quantum key encapsulation
- SLH-DSA - Hash-based digital signatures
- X-Wing - Hybrid KEM (X25519 + ML-KEM-768)
- PQC Overview - Algorithm comparison and guidance