Expand description
This page documents advanced features of the Module Lattice Digital Signature Algorithm (ML-DSA) available in this crate.
§Streaming APIs
Sometimes the message you need to sign or verify is too big to fit in device memory all at once. No worries, we got you covered!
use bouncycastle_core_interface::errors::SignatureError;
use bouncycastle_mldsa::{MLDSA65, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
use bouncycastle_core_interface::traits::Signature;
let (pk, sk) = MLDSA65::keygen().unwrap();
// Let's pretend this message was so long that you couldn't possibly
// stream the whole thing over a network, and you need it pre-hashed.
let msg_chunk1 = b"The quick brown fox ";
let msg_chunk2 = b"jumped over the lazy dog";
let mut signer = MLDSA65::sign_init(&sk, None).unwrap();
signer.sign_update(msg_chunk1);
signer.sign_update(msg_chunk2);
let sig: Vec<u8> = signer.sign_final().unwrap();
// This is the signature value that you can save to a file or whatever you need.
// This is compatible with a verifies that takes the whole message as one chunk:
let msg = b"The quick brown fox jumped over the lazy dog";
match MLDSA65::verify(&pk, msg, None, &sig) {
Ok(()) => println!("Signature is valid!"),
Err(SignatureError::SignatureVerificationFailed) => println!("Signature is invalid!"),
Err(e) => panic!("Something else went wrong: {:?}", e),
}
// But of course there's also a streaming API for the verifier!
let mut verifier = MLDSA65::verify_init(&pk, None).unwrap();
verifier.verify_update(msg_chunk1);
verifier.verify_update(msg_chunk2);
match verifier.verify_final(&sig.as_slice()) {
Ok(()) => println!("Signature is valid!"),
Err(SignatureError::SignatureVerificationFailed) => println!("Signature is invalid!"),
Err(e) => panic!("Something else went wrong: {:?}", e),
}Note that the streaming API also supports setting the signing context ctx and signing nonce rnd,
which are explained in more detail below.
use bouncycastle_core_interface::errors::SignatureError;
use bouncycastle_mldsa::{MLDSA65, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
use bouncycastle_core_interface::traits::Signature;
let (pk, sk) = MLDSA65::keygen().unwrap();
// Let's pretend this message was so long that you couldn't possibly
// stream the whole thing over a network, and you need it pre-hashed.
let msg_chunk1 = b"The quick brown fox ";
let msg_chunk2 = b"jumped over the lazy dog";
let mut signer = MLDSA65::sign_init(&sk, Some(b"signing ctx value")).unwrap();
signer.set_signer_rnd([0u8; 32]); // an all-zero rnd is the "deterministic" mode of ML-DSA
signer.sign_update(msg_chunk1);
signer.sign_update(msg_chunk2);
let sig: Vec<u8> = signer.sign_final().unwrap();§External Mu mode
Here, mu refers to the message digest which is computed internally to the ML-DSA algorithm:
𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀′, 64) ▷ message representative that may optionally be computed in a different cryptographic module
The External Mu mode of ML-DSA fulfills a similar function to hash_mldsa in that it allows large messages to be pre-digested outside of the cryptographic module that holds the private key, but it does it in a way that is compatible with the ML-DSA verification function. In other works, whereas hash_mldsa represents a different signature algorithm, the external mu mode of ML-DSA is simply internal implementation detail of how the signature was computed and produces signatures that are indistinguishable from “direct” ML-DSA mode.
The one potential complication with external mu mode – that hash_mldsa does not have –
is that it requires you to know the public key that you are about to sign the message with.
Or, more specifically, the hash of the public key tr.
tr is a public value (derivable from the public key), so there is no harm in, for example,
sending it down to a client device so that it can pre-hash a large message and only send the
64-byte mu value up to the server to be signed.
But in some contexts, the message has to be pre-hashed for performance reasons but
the public key that will be used for signing cannot be known in advance.
For those use cases, your only choice is to use hash_mldsa.
This library exposes MuBuilder which can be used to pre-hash a large to-be-signed message
along with the public key hash tr:
use bouncycastle_core_interface::errors::SignatureError;
use bouncycastle_mldsa::{MLDSA65, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
use bouncycastle_core_interface::traits::Signature;
let (pk, _) = MLDSA65::keygen().unwrap();
// Let's pretend this message was so long that you couldn't possibly
// stream the whole thing over a network, and you need it pre-hashed.
let msg = b"The quick brown fox jumped over the lazy dog";
let mu: [u8; 64] = MuBuilder::compute_mu(msg, None, &pk.compute_tr()).unwrap();Note: if you are going to bind a ctx value (explained below), then you need to do in in MuBuilder::compute_mu.
If the message really is so huge that you can’t hold it all in memory at once, then you might prefer a streaming API for computing mu:
use bouncycastle_core_interface::errors::SignatureError;
use bouncycastle_mldsa::{MLDSA65, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
use bouncycastle_core_interface::traits::Signature;
let (pk, _) = MLDSA65::keygen().unwrap();
// Let's pretend this message was so long that you couldn't possibly
// stream the whole thing over a network, and you need it pre-hashed.
let msg_chunk1 = b"The quick brown fox ";
let msg_chunk2 = b"jumped over the lazy dog";
let mut mb = MuBuilder::do_init(&pk.compute_tr(), None).unwrap();
mb.do_update(msg_chunk1);
mb.do_update(msg_chunk2);
let mu = mb.do_final();Given a mu value, you can compute a signature that verifies as normal (no mu’s required!):
use bouncycastle_core_interface::errors::SignatureError;
use bouncycastle_mldsa::{MLDSA65, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
use bouncycastle_core_interface::traits::Signature;
let msg = b"The quick brown fox jumped over the lazy dog";
let (pk, sk) = MLDSA65::keygen().unwrap();
// Assume this was computed somewhere else and sent to you.
// They would have had to know pk!
let mu: [u8; 64] = MuBuilder::compute_mu(msg, None, &pk.compute_tr()).unwrap();
let sig = MLDSA65::sign_mu(&sk, &mu).unwrap();
// This is the signature value that you can save to a file or whatever you need.
match MLDSA65::verify(&pk, msg, None, &sig) {
Ok(()) => println!("Signature is valid!"),
Err(SignatureError::SignatureVerificationFailed) => println!("Signature is invalid!"),
Err(e) => panic!("Something else went wrong: {:?}", e),
}
§Ctx and Rnd params
Various functions in this crate let you set the signing context value (ctx) and the signing nonce (rnd).
Let’s talk about them both:
§ctx
The ctx value allows the signer to bind the signature value to an extra piece of information
(up to 255 bytes long) that must also be known to the verifier in order to successfully verify the signature.
This optional parameter allows cryptographic protocol designers to get additional binding properties
from the ML-DSA signature.
The ctx value should be something that is known to both the signer and verifier,
does not necessarily need to be a secret, but should not go over the wire as part of the not-yet-verified message.
Examples of uses of the ctx could include binding the application data type (ex: FooEmailData) in order
to disambiguate other data types that share an encoding (ex: FooTextDocumentData) and might otherwise be possible for an
attacker to trick a verifier into accepting one in place of the other.
In a network protocol, ctx could be used to bind a transaction ID or protocol nonce in order to strongly
protect against replay attacks.
Generally, ctx is one of those things that if you don’t know what it does, then you’re probably
fine to ignore it.
Example of signing and verifying with a ctx value:
use bouncycastle_core_interface::errors::SignatureError;
use bouncycastle_mldsa::{MLDSA65, MLDSATrait};
use bouncycastle_core_interface::traits::Signature;
let msg = b"The quick brown fox";
let ctx = b"FooTextDocumentFormat";
let (pk, sk) = MLDSA65::keygen().unwrap();
let sig: Vec<u8> = MLDSA65::sign(&sk, msg, Some(ctx)).unwrap();
// This is the signature value that you can save to a file or whatever you need.
match MLDSA65::verify(&pk, msg, Some(ctx), &sig) {
Ok(()) => println!("Signature is valid!"),
Err(SignatureError::SignatureVerificationFailed) => println!("Signature is invalid!"),
Err(e) => panic!("Something else went wrong: {:?}", e),
}§rnd
This is the signature nonce, whose purpose is to ensure that you get different signature values if you sign the same message with the same public key multiple times.
In general, the “deterministic” mode of ML-DSA (which usually uses an all-zero rnd) is considered
secure and safe to use but you may lose certain privacy properties, because, for example,
it becomes obvious that multiple identical signatures means that the same message was signed multiple times
by the same private key.
The default mode of ML-DSA uses a rnd generated by the library’s OS-backed RNG, but you can set the rnd
if you need to; for example if you are running on an embedded device that does not have access to an RNG.
Note that in order to avoid combinatorial explosion of API functions, setting the rnd value is only
available in conjunction with external mu or streaming modes. The example of setting rnd on the streaming
API was shown above.
Here is an example of using the MLDSA::sign_mu_deterministic function:
use bouncycastle_core_interface::errors::SignatureError;
use bouncycastle_mldsa::{MLDSA65, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
use bouncycastle_core_interface::traits::Signature;
let msg = b"The quick brown fox jumped over the lazy dog";
let (pk, sk) = MLDSA65::keygen().unwrap();
// Assume this was computed somewhere else and sent to you.
// They would have had to know pk!
let mu: [u8; 64] = MuBuilder::compute_mu(msg, None, &pk.compute_tr()).unwrap();
// Typically, "deterministic" mode of ML-DSA will use an all-zero rnd,
// but we've exposed it so you can set any value you need to.
let sig = MLDSA65::sign_mu_deterministic(&sk, &mu, [0u8; 32]).unwrap();
// This is the signature value that you can save to a file or whatever you need.
match MLDSA65::verify(&pk, msg, None, &sig) {
Ok(()) => println!("Signature is valid!"),
Err(SignatureError::SignatureVerificationFailed) => println!("Signature is invalid!"),
Err(e) => panic!("Something else went wrong: {:?}", e),
}§sign_from_seed
This mode is intended for users with extreme performance or resource-limitation requirements.
A very careful analysis of the ML-DSA signing algorithm will show that you don’t actually need the entire ML-DSA private key to be in memory at the same time. In fact, it is possible to merge the keygen() and sign() functions
We provide MLDSA::sign_mu_deterministic_from_seed which implements such an algorithm. It has a significantly lower peak-memory-footprint than the regular signing API (although there’s always room for more optimization), and according to our benchmarks it is only around 25% slower than signing with a fully-expanded private key – which is still faster than performing a full keygen followed by a regular sign since there are intermediate values common to keygen and sign that the merged function is able to only compute once.
Since this is intended for hard-core embedded systems people, we have not wrapped this in all the beginner-friendly APIs. If you need this, then we assume you know what you’re doing!
Example usage:
use bouncycastle_core_interface::errors::SignatureError;
use bouncycastle_mldsa::{MLDSA44, MLDSA44_SIG_LEN, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
use bouncycastle_core_interface::traits::Signature;
use bouncycastle_core_interface::traits::KeyMaterial;
use bouncycastle_core_interface::key_material::{KeyMaterial256, KeyType};
let msg = b"The quick brown fox jumped over the lazy dog";
let seed = KeyMaterial256::from_bytes_as_type(
&hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(),
KeyType::Seed,
).unwrap();
// At some point, you'll need to compute the public key, both to get `tr`, and so other
// people can verify your signature.
// There's no possible short-cut to efficiently computing the public key or `tr` from the seed;
// you have to run the full keygen to get the full private key, at least momentarily, then
// you can discard it in only keep `tr` and `seed`.
let (pk, _) = MLDSA44::keygen_from_seed(&seed).unwrap();
let tr: [u8; 64] = pk.compute_tr();
// Assume this was computed somewhere else and sent to you.
// They would have had to know pk!
let mu: [u8; 64] = MuBuilder::compute_mu(msg, None, &tr).unwrap();
let rnd: [u8; 32] = [0u8; 32]; // with this API, you're responsible for your own nonce
// because in the cases where this level of memory optimization
// is needed, our RNG probably won't work anyway.
let mut sig = [0u8; MLDSA44_SIG_LEN];
let bytes_written = MLDSA44::sign_mu_deterministic_from_seed_out(&seed, &mu, rnd, &mut sig).unwrap();
// it can be verified normally
match MLDSA44::verify(&pk, msg, None, &sig) {
Ok(()) => println!("Signature is valid!"),
Err(SignatureError::SignatureVerificationFailed) => println!("Signature is invalid!"),
Err(e) => panic!("Something else went wrong: {:?}", e),
}While this is currently only supported when operating from a seed-based private key, something analogous could be done that merges the sk_decode() and sign() routines when working with the standardized private key encoding (which is often called the “semi-expanded format” since the in-memory representation is still larger). Contact us if you need such a thing implemented.
Structs§
- MLDSA
- The core internal implementation of the ML-DSA algorithm. This needs to be public for the compiler to be able to find it, but you shouldn’t ever need to use this directly. Please use the named public types.
- MuBuilder
- Implements parts of Algorithm 2 and Line 6 of Algorithm 7 of FIPS 204. Provides a stateful version of MLDSATrait::compute_mu_from_pk and MLDSATrait::compute_mu_from_tr that supports streaming large to-be-signed messages.
Constants§
- MLDS
A44_ PK_ LEN - Length of the [u8] holding a ML-DSA-44 public key.
- MLDS
A44_ SIG_ LEN - Length of the [u8] holding a ML-DSA-44 signature value.
- MLDS
A44_ SK_ LEN - Length of the [u8] holding a ML-DSA-44 private key.
- MLDS
A65_ PK_ LEN - Length of the [u8] holding a ML-DSA-65 public key.
- MLDS
A65_ SIG_ LEN - Length of the [u8] holding a ML-DSA-65 signature value.
- MLDS
A65_ SK_ LEN - Length of the [u8] holding a ML-DSA-65 private key.
- MLDS
A87_ PK_ LEN - Length of the [u8] holding a ML-DSA-87 public key.
- MLDS
A87_ SIG_ LEN - Length of the [u8] holding a ML-DSA-87 signature value.
- MLDS
A87_ SK_ LEN - Length of the [u8] holding a ML-DSA-87 private key.
- ML_
DSA_ 44_ NAME - ML_
DSA_ 65_ NAME - ML_
DSA_ 87_ NAME - MU_LEN
- Length of the [u8] holding an ML-DSA mu value.
- RND_LEN
- Length of the [u8] holding an ML-DSA signing random value.
- SEED_
LEN - Length of the [u8] holding an ML-DSA seed value.
- TR_LEN
- Length of the [u8] holding an ML-DSA tr value (which is the SHAKE256 hash of the public key).
Traits§
- MLDSA
Trait - Trait for all three of the ML-DSA algorithm variants.