ML-DSA (Module-Lattice-Based Digital Signature Algorithm) Support
Introduction
The IPWorks Encrypt development library supports Module-Lattice-Based Digital Signature Algorithm (ML-DSA) cryptography via the MLDSA component. ML-DSA is the NIST-standardized post-quantum signature algorithm defined in FIPS 204 (based on the CRYSTALS-Dilithium algorithm), designed to remain secure even against an attacker with a quantum computer. Using the MLDSA component, one party can sign data with a private key, and any party holding the corresponding public key can verify that signature.
This guide covers creating ML-DSA keys, signing, and verifying, including the pre-hashed HashML-DSA variant.
ML-DSA Keys
The MLDSA component supports three parameter sets, selected via the ParamSet config:
- 44 (default)
- 65
- 87
Higher parameter sets provide a larger security margin, at the cost of larger keys/signatures and more computation.
To generate a new key pair, set ParamSet as needed and call CreateKey. The generated public and private key values are then available, as raw bytes, from the Key property:
MLDSA mldsa = new MLDSA();
mldsa.Config("ParamSet=65");
mldsa.CreateKey();
byte[] publicKey = mldsa.Key.PublicKeyB;
byte[] privateKey = mldsa.Key.PrivateKeyB;
Signing
To sign data, load the private key into Key and supply the data to sign. If the data is held in a file, set InputFile to the appropriate file path. If data is held in memory, set InputMessage (or InputMessageB for raw bytes). Alternatively, you may supply the data from a stream via SetInputStream.
Once the key and input data are set, call Sign. The resulting signature is available, as raw bytes, from the SignatureB property.
signer.Key.PrivateKeyB = privateKey;
signer.InputMessage = "Hello, World!";
signer.Sign();
byte[] signature = signer.SignatureB;
Verifying
To verify a signature, load the corresponding public key into Key, supply the same data that was signed (via InputFile, InputMessage/InputMessageB, or SetInputStream), and set Signature to the signature being checked. Calling Verify then returns true or false.
verifier.Key.PublicKeyB = publicKey;
verifier.InputMessage = "Hello, World!";
verifier.SignatureB = signature;
bool isValid = verifier.Verify();
Signature is raw bytes by default. Set UseHex to true on both the signer and verifier to work with Signature as a hex-encoded string instead.
HashML-DSA
By default, the component uses pure ML-DSA, which processes the full input message during signing and verification. Setting PreHash to true switches to HashML-DSA instead, which pre-hashes the input using the algorithm selected by HashAlgorithm (SHA256 by default, or SHA384/SHA512) before signing. This allows the signer and verifier to process the input in a single pass, which is useful for large or streamed input. PreHash and HashAlgorithm must be set identically on both the signer and the verifier.
signer.PreHash = true;
signer.HashAlgorithm = MLDSAHashAlgorithms.mhaSHA384;
signer.CreateKey();
signer.InputMessage = "Hello, World!";
signer.Sign();
verifier.PreHash = true;
verifier.HashAlgorithm = MLDSAHashAlgorithms.mhaSHA384;
verifier.Key.PublicKeyB = signer.Key.PublicKeyB;
verifier.InputMessage = "Hello, World!";
verifier.SignatureB = signer.SignatureB;
bool isValid = verifier.Verify();
Context String
Both pure ML-DSA and HashML-DSA support an optional context string, via the Context property (or ContextB for raw bytes). It is empty by default, with a maximum length of 255 bytes, and the same value must be supplied when signing and when verifying. A mismatched context will cause verification to fail.
signer.Context = "example-context";
signer.CreateKey();
signer.InputMessage = "Hello, World!";
signer.Sign();
verifier.Context = "example-context";
verifier.Key.PublicKeyB = signer.Key.PublicKeyB;
verifier.InputMessage = "Hello, World!";
verifier.SignatureB = signer.SignatureB;
bool isValid = verifier.Verify();
We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@nsoftware.com.