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 that needs to be signed or verified is too big to fit in device memory all at once. No worries, we got you covered!
use bouncycastle_core::errors::SignatureError;
use bouncycastle_core::traits::{Signer, SignatureVerifier};
use bouncycastle_mldsa_lowmemory::{MLDSA65, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
let (pk, sk) = MLDSA65::keygen().unwrap();
// For illustration purposes, assume that this message was so long that it couldn't possibly
// be streamed in its entirety over a network, and therefore it needs to be 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 = signer.sign_final().unwrap();
// This is the signature value that can be saved to a file or whatever it is needed.
// 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::errors::SignatureError;
use bouncycastle_core::traits::{Signer, SignatureVerifier};
use bouncycastle_mldsa_lowmemory::{MLDSA65, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
let (pk, sk) = MLDSA65::keygen().unwrap();
// For illustration purposes, assume that this message was so long that it couldn't possibly
// be streamed in its entirety over a network, and therefore it needs to be 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 = 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 the user to know the public key that they 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, the 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::errors::SignatureError;
use bouncycastle_core::traits::{Signer, SignatureVerifier};
use bouncycastle_mldsa_lowmemory::{MLDSA65, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
let (pk, _) = MLDSA65::keygen().unwrap();
// Let's pretend this message was so long that it couldn't possibly
// streamed in its entirety over a network, and it needs to be pre-hashed.
let msg = b"The quick brown fox jumped over the lazy dog";
let mu: [u8; 64] = MuBuilder::compute_mu(&pk.compute_tr(), msg, None).unwrap();Note: binding a ctx value (explained below) needs to be done in MuBuilder::compute_mu.
If the message really is so huge that it can’t all be held in memory at once, then it might be preferable to use a streaming API for computing mu:
use bouncycastle_core::errors::SignatureError;
use bouncycastle_core::traits::{Signer, SignatureVerifier};
use bouncycastle_mldsa_lowmemory::{MLDSA65, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
let (pk, _) = MLDSA65::keygen().unwrap();
// Let's pretend this message was so long that it couldn't possibly
// streamed in its entirety over a network, and it needs to be 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, it is possible to compute a signature that verifies as normal (no mu’s required!):
use bouncycastle_core::errors::SignatureError;
use bouncycastle_core::traits::{Signer, SignatureVerifier};
use bouncycastle_mldsa_lowmemory::{MLDSA65, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
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 received by the user.
// Then the sender would have had to know pk!
let mu: [u8; 64] = MuBuilder::compute_mu(&pk.compute_tr(), msg, None).unwrap();
let sig = MLDSA65::sign_mu(&sk, &mu).unwrap();
// This is the signature value that can be saved to a file or whatever it is 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 allows setting the signing context value (ctx) and the signing nonce (rnd).
Here is an overview of 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, it is safe to ignore any property about a ctx object that is not well understood.
Example of signing and verifying with a ctx value:
use bouncycastle_core::errors::SignatureError;
use bouncycastle_core::traits::{Signer, SignatureVerifier};
use bouncycastle_mldsa_lowmemory::{MLDSA65, MLDSATrait};
let msg = b"The quick brown fox";
let ctx = b"FooTextDocumentFormat";
let (pk, sk) = MLDSA65::keygen().unwrap();
let sig = MLDSA65::sign(&sk, msg, Some(ctx)).unwrap();
// This is the signature value that can be saved to a file or whatever it is needed.
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 every time a signature is computed for the same message, it results in a different value
In general, the “deterministic” mode of ML-DSA (which usually uses an all-zero rnd) is considered
secure and safe to use, however, certain privacy properties may be lost. For example,
it becomes evident 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, the rnd can be set by the user
if necessary; for example if the function is run 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::errors::SignatureError;
use bouncycastle_core::traits::{Signer, SignatureVerifier};
use bouncycastle_mldsa_lowmemory::{MLDSA65, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
let msg = b"The quick brown fox jumped over the lazy dog";
let (pk, sk) = MLDSA65::keygen().unwrap();
// Assume this was computed somewhere else, then
// the party that computed it would have had to know pk
let mu: [u8; 64] = MuBuilder::compute_mu(&pk.compute_tr(), msg, None).unwrap();
// Typically, "deterministic" mode of ML-DSA will use an all-zero `rnd`,
// but here it is exposed it so it can be set any value, as needed.
let sig = MLDSA65::sign_mu_deterministic(&sk, &mu, [0u8; 32]).unwrap();
// This is the signature value that can saved to a file or whatever it is needed.
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 the entire ML-DSA private key does not need to be in memory at the same time. In fact, it is possible to merge the keygen() and sign() functions
The code provides 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, this has not been wrapped in all the beginner-friendly APIs. It is implied that a user that needs this functionality also knows how to use it and what they are doing
Example usage:
use bouncycastle_core::errors::SignatureError;
use bouncycastle_core::traits::{Signer, SignatureVerifier};
use bouncycastle_core::key_material::{KeyMaterial256, KeyType, KeyMaterialTrait};
use bouncycastle_hex as hex;
use bouncycastle_mldsa_lowmemory::{MLDSA44, MLDSA44_SIG_LEN, MLDSATrait, MLDSAPublicKeyTrait, MuBuilder};
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();
// The public key is computed so that the signature can be verified by anyone.
// It also computes the hash `tr` of the public key to later be used to bind the public key at the time of signing.
// There is no short-cut to efficiently computing the public key or `tr` from the seed;
// The full keygen need to be run in order to get the full private key, at least momentarily, then
// it can be discarded and 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, then
// the party that computed it would have had to know pk
let mu: [u8; 64] = MuBuilder::compute_mu(&tr, msg, None).unwrap();
let rnd: [u8; 32] = [0u8; 32]; // with this API, the user is responsible for their 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.
§Suspending and resuming execution via SerializableState
When signing or verifying a large message, it can be advantageous to be able to suspend the operation to a cache and resume it later; for example if waiting for the message to stream over a slow network connection.
This can bo accomplished for both the ML-DSA signer and verifier through the MuBuilder object.
Suspending an in-progress sign operation:
use bouncycastle_mldsa_lowmemory::{MLDSA65, MuBuilder, MLDSATrait, MLDSAPublicKeyTrait};
use bouncycastle_core::traits::{Signer, Suspendable};
let msg_part1 = b"The quick brown fox";
let msg_part2 = b" jumped over the lazy dog";
let (pk, sk) = MLDSA65::keygen().unwrap();
let mut mb = MuBuilder::do_init(&pk.compute_tr(), None).unwrap();
mb.do_update(msg_part1);
// here, we'll suspend while "waiting" for the second part of the message
let serialized_state = mb.suspend();
// ...
// do other things in the meantime
// ...
let mut mb_resumed = MuBuilder::from_suspended(serialized_state).unwrap();
mb_resumed.do_update(msg_part2);
let mu: [u8; 64] = mb_resumed.do_final();
// Now we'll do the actual sign_mu operation
let sig = MLDSA65::sign_mu(&sk, &mu).unwrap();Suspending an in-progress verify operation behaves exactly the same way:
use bouncycastle_mldsa_lowmemory::{MLDSA65, MuBuilder, MLDSATrait, MLDSAPublicKeyTrait};
use bouncycastle_core::traits::{Signer, Suspendable};
use bouncycastle_core::errors::SignatureError;
let (pk, sk) = MLDSA65::keygen().unwrap();
// first, let's generate a signature to verify
let sig = MLDSA65::sign(&sk, b"The quick brown fox jumped over the lazy dog", None).unwrap();
// Now we'll verify it with a suspension in the middle
let msg_part1 = b"The quick brown fox";
let msg_part2 = b" jumped over the lazy dog";
let mut mb = MuBuilder::do_init(&pk.compute_tr(), None).unwrap();
mb.do_update(msg_part1);
// here, we'll suspend while "waiting" for the second part of the message
let serialized_state = mb.suspend();
// ...
// do other things in the meantime
// ...
let mut mb_resumed = MuBuilder::from_suspended(serialized_state).unwrap();
mb_resumed.do_update(msg_part2);
let mu: [u8; 64] = mb_resumed.do_final();
// Now we'll do the actual verify_mu operation
match MLDSA65::verify_mu(&pk, &mu, &sig) {
Ok(()) => println!("Signature is valid!"),
Err(SignatureError::SignatureVerificationFailed) => println!("Signature is invalid!"),
Err(e) => panic!("Something else went wrong: {:?}", e),
}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 there shouldn’t ever be a 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_pkandMLDSATrait::compute_mu_from_trthat supports streaming large to-be-signed messages.
Constants§
- MLDS
A44_ FULL_ SK_ LEN - The length of the FIPS representation of the private key, which can be produced by
MLDSAPrivateKeyTrait::encode_full_sk - 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, which in this implementation is just a 32-byte seed.
- MLDS
A65_ FULL_ SK_ LEN - The length of the FIPS representation of the private key, which can be produced by
MLDSAPrivateKeyTrait::encode_full_sk - 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, which in this implementation is just a 32-byte seed.
- MLDS
A87_ FULL_ SK_ LEN - The length of the FIPS representation of the private key, which can be produced by
MLDSAPrivateKeyTrait::encode_full_sk - 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, which in this implementation is just a 32-byte seed.
- MLDSA_
MU_ LEN - Length of the [u8] holding a ML-DSA mu value.
- MLDSA_
RND_ LEN - Length of the [u8] holding a ML-DSA signing random value.
- MLDSA_
SEED_ LEN - Length of the [u8] holding an private key seed.
- MLDSA_
TR_ LEN - Length of the [u8] holding a ML-DSA tr value (which is the SHAKE256 hash of the public key).
- ML_
DSA_ 44_ NAME - ML_
DSA_ 65_ NAME - ML_
DSA_ 87_ NAME - SUSPENDED_
MU_ BUILDER_ STATE_ LEN - The length, in bytes, of a serialized state of a
MuBuilderobject.
Traits§
- MLDSA
Trait - Trait for all three of the ML-DSA algorithm variants.