Skip to main content

bouncycastle_mldsa_lowmemory/
lib.rs

1//! This crate implements the Module Lattice Digital Signature Algorithm (ML-DSA) as per FIPS 204 optimized
2//! to have the lowest-reasonable runtime memory footprint (aka peak memory usage).
3//!
4//! We achieve an approximate **1/10 the memory footprint** at a cost of approximately **3x
5//! the runtime for signing and no appreciable difference for keygen and verification**,
6//! compared with our un-optimized implementation in the \[bouncycastle_mldsa] crate.
7//! We are extremely happy with the result!
8//!
9//! # Philosophy of a low-memory implementation
10//!
11//! First, a little primer on the objects that make up an ML-DSA key pair.
12//! The "elements" of the lattice are polynomials of degree 256, represented in memory as
13//! arrays of 256 i32's. That's 1 kb per polynomial.
14//! Then the vectors and matrices composed of these polynomials are built.
15//! ML-DSA is parametrized as ML-DSA-k,l where k and l are the sizes of the vectors, and the matrices
16//! have size k x l.
17//! So ML-DSA-44 carries vectors of 4 polynomials and matrices of 4 x 4 = 16 polynomials.
18//! ML-DSA-65 is 6, 5 and 6 x 5 = 30 polynomials, and ML-DSA-87 is 8, 7, and 8 x 7 = 56 polynomials.
19//!
20//! A straightforward implementation of ML-DSA will start by un-compressing all the key material into
21//! memory, into the format that is needed to perform the computation.
22//! A ready-to-use private key consists of a `Vector<l>` and two `Vector<k>`s,
23//! while the public key is a `Vector<k>` and a `Matrix<k,l>`.
24//! For ML-DSA-65, it is expected to use 53 kb of RAM just for holding expanded key material, and then
25//! it is expected `.sign()` operation to require several multiples of that as variables for holding
26//! intermediate values as the computation proceeds.
27//! A well-written but not memory-optimized ML-DSA-65 can be expected to consume approximately 150 kb of RAM
28//! at the widest point of the `.sign()` operation.
29//!
30//! This crate strives to do better!
31//!
32//! The core observation that makes this implementation possible is that, by a careful examination of
33//! how the matrix multiplication works, the vectors and matrices never need to be fully
34//! expanded at the same time.
35//! In fact, it is possible to work one polynomial at a time.
36//! This is because the ML-DSA keygen algorithm starts with a single 32-byte seed and expands that
37//! into intermediate seeds `rho` (32 byte), `rho_prime`(64 byte), and `K` (32 byte), from which all
38//! of the vectors and matrices are derived via hash functions.
39//! The public matrix A can be derived in a random-access fashion from `rho` and the matrix index `i,j`.
40//! The various vectors cannot, but the polynomial compression algorithm given in FIPS 204 as part of
41//! the key encoding procedure can be used to hold the vectors in memory compressed and only un-compress
42//! a single polynomial entry at a time.
43//! The downside of this approach is that it costs performance:
44//! throughout this implementation, bits and pieces of matrices, that previously would be in memory, are
45//! instead being re-derived, used, and released.
46//!
47//! Furthermore, a surprising amount of memory-savings are achieved by simply following good coding hygiene:
48//! Using un-named scopes to tell the compiler when an intermediate variable is no longer needed and
49//! can be popped off the stack. This sometimes requires re-ordering the steps of the algorithms given in
50//! FIPS 204 so that variables can be created, used, and released in a self-contained block.
51//! Sometimes this is not possible, it is necessary to make a choice between keeping the variable around
52//! or releasing it and re-deriving it later.
53//! We also attempt to be clean about noting the last time a long-lived variable is used and
54//! re-using / re-naming / moving it to a new purpose rather than allocating an additional variable.
55//! These hygiene points can always be further improved with increasingly aggressive design choices.
56//! The authors feel that the trade-offs have hit the point of diminishing returns, maintaining an acceptable balance
57//! of memory footprint, performance, and code readability. That being said, we welcome pull requests if, for example,
58//! we've missed a polynomial that doesn't need to be created has been missed and could be eliminated.
59//!
60//! All this combined, the implementations achieves an approximate *1/10 the memory footprint* at a cost of approximately *3x
61//! the runtime for signing and no appreciable difference for keygen and verification*,
62//! compared with our un-optimized implementation in the \[bouncycastle_mldsa] crate, which we are extremely happy with!
63//!
64//! # Memory Footprint
65//!
66//! Below, find performance charts relative to the standard ML-DSA implementation in the \[bouncycastle_mldsa] crate.
67//!
68//! ## Keys sizes in memory and on disk
69//!
70//! This implementation greatly reduces the size of keys both on disk and in memory
71//! by only handling the matrices and vectors either as seeds or in their compressed representation
72//! expanding on-demand as part of a sign or verify operation rather than storing them in memory as part of a keygen or key load.
73//!
74//! | Key Object | PK size on disk | PK size in memory | SK Size on disk | SK size in memory |
75//! |------------|-----------------|-------------------|-----------------|-------------------|
76//! | ML-DSA-44  | 1312 (1312)     | 1312 (4128)       | 32 (2560)       | 176 (12464)       |
77//! | ML-DSA-65  | 1952 (1952)     | 1952 (6176)       | 32 (4032)       | 176 (17584)       |
78//! | ML-DSA-87  | 2592 (2592)     | 2592 (8224)       | 32 (4896)       | 176 (23728)       |
79//!
80//! All values are in bytes. The "in memory" sizes are measured by rust's `std::mem::size_of`.
81//! Values in parentheses are the usual sizes in our un-optimized implementation in the \[bouncycastle_mldsa] crate.
82//!
83//!
84//! ## Algorithm Peak Memory Usage
85//! The table below shows peak memory usage of the ML-DSA algorithms and the rough performanc (throughput) impact.
86//!
87//! Measuring peak application memory usage can be a bit tricky, and the numbers obtained depend heavily on how the
88//! measurement harness is designed. Here, we aim to provide a conservative measurement, meaning that we are aiming for an
89//! over-estimate so that any deployment within an existing application will use incrementally less additional memory
90//! than the amount stated here.
91//!
92//! Our measurement methodology is to compile a simple standalone HelloWorld application that only calls the function under test
93//! with as minimal as possible hard-coded data (such as keys or ciphertexts) and measure the peak memory usage of running
94//! the compiled binary using `valgrind --tool=massif --heap=no --stack=yes`. The flags for heap and stack
95//! reflect the fact that this is a `no_std` rust application and therefore the cryptographic functions use no heap memory.
96//! The measurements may over-estimate by as much as 3 kb since that that's the measured peak memory usage of a do-nothing
97//! HelloWorld rust application.
98//!
99//! | Algorithm                 | Peak swap memory usage (kB) | Throughput (ops/s)  |
100//! |---------------------------|-----------------------------|---------------------|
101//! | MLDSA44_lowmemory/KeyGen  | 12.6 (113.8)                | 11,800     (11,300) |
102//! | MLDSA65_lowmemory/KeyGen  | 15.0 (124.1)                | 5,500      (7.000)  |
103//! | MLDSA87_lowmemory/KeyGen  | 15.2 (197.8)                | 3,300      (4,200)  |
104//! | MLDSA44_lowmemory/Sign    | 24.8 (117.7)                | 850        (4,000)  |
105//! | MLDSA65_lowmemory/Sign    | 28.2 (159.6)                | 580        (2,900)  |
106//! | MLDSA87_lowmemory/Sign    | 31.1 (236.7)                | 315        (2,000)  |
107//! | MLDSA44_lowmemory/Verify  | 17.1 (73.0)                 | 10,100     (14,000) |
108//! | MLDSA65_lowmemory/Verify  | 18.0 (134.4)                | 6,300      (8,400)  |
109//! | MLDSA87_lowmemory/Verify  | 20.6 (211.6)                | 3,500      (5,000)  |
110//!
111//! Values in parentheses are the comparison values from the un-optimized implementation in the \[bouncycastle_mldsa] crate.
112//! Size numbers were collected with valgrind using a simple main program that calls only the measured function.
113//! Performance throughput numbers were collected on my laptop using the library's provided benchmarks, so
114//! performance they should be taken with an extreme grain of salt.
115//!
116//! Actual values may vary based on build configuration and target architecture.
117//!
118//! # Usage
119//!
120//! This crate has been designed to serve a wide range of use cases, from people dabbling in
121//! cryptography for the first time, to cryptographic protocol designers who need access to the advanced
122//! functionality of the ML-DSA algorithm, to embedded systems developers who want access to memory
123//! and performance optimized functions.
124//!
125//! This page gives examples of simple usage for generating keys and signatures, and verifying signatures.//!
126//!
127//! More examples on advanced usage can be found on the [`mldsa`] and [`hash_mldsa`] pages.
128//!
129//! ## Generating Keys
130//!
131//! ```rust
132//! use bouncycastle_mldsa_lowmemory::{MLDSA65, MLDSATrait};
133//!
134//! let (pk, sk) = MLDSA65::keygen().unwrap();
135//! ```
136//! That's it. That will use the library's default OS-backend RNG.
137//!
138//! Commonly with the ML-DSA algorithm, a 32-byte seed is used as the private key, and expanded into
139//! a full private key as needed. This is offered through the library's [`KeyMaterialTrait`] object:
140//!
141//! ```rust
142//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType, KeyMaterialTrait};
143//! use bouncycastle_hex as hex;
144//! use bouncycastle_mldsa_lowmemory::{MLDSA65, MLDSATrait};
145//!
146//! let seed = KeyMaterial256::from_bytes_as_type(
147//!     &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(),
148//!     KeyType::Seed,
149//! ).unwrap();
150//!
151//! let (pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap();
152//! ```
153//!
154//! See [`MLDSATrait`] and [`MLDSATrait::sign_mu_deterministic_from_seed`] for an API flow that uses a merged
155//! keygen-and-sign function to provide improved speed and memory performance compared with making
156//! separate calls to [`MLDSATrait::keygen_from_seed`] followed by [`Signer::sign`].
157//!
158//! ## Generating and Verifying Signatures
159//!
160//! ```rust
161//! use bouncycastle_core::errors::SignatureError;
162//! use bouncycastle_core::traits::{Signer, SignatureVerifier};
163//! use bouncycastle_mldsa_lowmemory::{MLDSA65, MLDSATrait};
164//!
165//! let msg = b"The quick brown fox";
166//!
167//! let (pk, sk) = MLDSA65::keygen().unwrap();
168//!
169//! let sig = MLDSA65::sign(&sk, msg, None).unwrap();
170//! // This is the signature value that can be saved to a file or whatever it is need.
171//!
172//! match MLDSA65::verify(&pk, msg, None, &sig) {
173//!     Ok(()) => println!("Signature is valid!"),
174//!     Err(SignatureError::SignatureVerificationFailed) => println!("Signature is invalid!"),
175//!     Err(e) => panic!("Something else went wrong: {:?}", e),
176//! }
177//!
178//! ```
179//! And that's the basic usage! There are lots more bells-and-whistles in the form of exposed algorithm
180//! parameters, streaming APIs and other goodies that can be found by poking around this documentation.
181//!
182//! # 🚨 Security 🚨
183//!
184//! This crate intends to expose only APIs that are secure to use.
185//! There are, however, a few exceptions that are worth mentioning.
186//!
187//! If using a [`MLDSA::keygen_from_seed`], then it is your responsibility to ensure that the seed is
188//! cryptographically random and unpredictable at a security strength that matches the MLDSA parameter set.
189//!
190//! ML-DSA and HashML-DSA take several parameters: `seed`, `mu`, `ph`, `ctx`, and `rnd`.
191//! They fall into two groups with very different failure modes.
192//!
193//!
194//! `seed` and `rnd`, however, are secret/entropy inputs and must be handled with care:
195//!
196//! - `seed` *is* the private key, i.e. the entire key is derived from it. It must be generated
197//!   with a strong cryptographically secure PRNG, it must be kept secret, and it must never reused.
198//!   A low-entropy, predictable, or disclosed seed yields a full key compromise,
199//!   not merely an unverifiable signature.
200//!
201//! - `rnd` is the signing randomizer. ML-DSA is designed to be nonce-misuse-resistant, i.e. the
202//!   signing mask is derived from a secret key value together with `rnd` and `mu`, so
203//!   reusing `rnd`, or using the all-zero "deterministic" mode, does NOT
204//!   leak the private key (unlike ECDSA). Deterministic signing is FIPS-approved and safe. The randomized
205//!   mode exists to add resistance to fault and side-channel attacks, so `rnd`
206//!   should come from a good RNG when that threat model applies.
207//!
208//! `mu`, `ph`, and `ctx` are binding values that the verifier must reproduce. This means that getting
209//! them wrong does not compromise security, it just yields a signature the intended
210//! verifier won't accept (a correctness/interoperability failure).
211//! One caveat: `ctx` can still be security-relevant at the protocol level (domain separation, replay and
212//! cross-protocol binding), so choosing it incorrectly can weaken those properties.
213
214#![no_std]
215#![forbid(unsafe_code)]
216#![forbid(missing_docs)]
217// These are because the code matching variable names exactly against FIPS 204, for example both 'K' and 'k',
218// or 'A' and 'a' are used and have specific meanings.
219// But need to tell the rust linter to not care.
220#![allow(non_snake_case)]
221#![allow(non_upper_case_globals)]
222// so that private traits can be used to hide internal stuff that needs to be generic within the
223// MLDSA implementation, but should not get accessed from outside, such as FIPS-internal functions.
224#![allow(private_bounds)]
225
226// imports needed just for docs
227#[allow(unused_imports)]
228use bouncycastle_core::key_material::KeyMaterialTrait;
229#[allow(unused_imports)]
230use bouncycastle_core::traits::{SignatureVerifier, Signer};
231
232mod aux_functions;
233pub mod hash_mldsa;
234mod low_memory_helpers;
235pub mod mldsa;
236mod mldsa_keys;
237mod polynomial;
238
239/*** Exported types ***/
240pub use hash_mldsa::{HashMLDSA44_with_SHA256, HashMLDSA65_with_SHA256, HashMLDSA87_with_SHA256};
241pub use hash_mldsa::{HashMLDSA44_with_SHA512, HashMLDSA65_with_SHA512, HashMLDSA87_with_SHA512};
242pub use mldsa::MuBuilder;
243pub use mldsa::{MLDSA, MLDSA44, MLDSA65, MLDSA87, MLDSATrait};
244pub use mldsa_keys::{
245    MLDSA44PrivateKey, MLDSA65PrivateKey, MLDSA87PrivateKey, MLDSASeedPrivateKey,
246};
247pub use mldsa_keys::{MLDSA44PublicKey, MLDSA65PublicKey, MLDSA87PublicKey, MLDSAPublicKey};
248pub use mldsa_keys::{MLDSAPrivateKeyTrait, MLDSAPublicKeyTrait};
249
250/*** Exported constants ***/
251pub use mldsa::ML_DSA_44_NAME;
252pub use mldsa::ML_DSA_65_NAME;
253pub use mldsa::ML_DSA_87_NAME;
254
255pub use hash_mldsa::HASH_ML_DSA_44_with_SHA256_NAME;
256pub use hash_mldsa::HASH_ML_DSA_65_WITH_SHA256_NAME;
257pub use hash_mldsa::HASH_ML_DSA_87_with_SHA256_NAME;
258
259pub use hash_mldsa::HASH_ML_DSA_44_with_SHA512_NAME;
260pub use hash_mldsa::HASH_ML_DSA_65_WITH_SHA512_NAME;
261pub use hash_mldsa::HASH_ML_DSA_87_WITH_SHA512_NAME;
262
263pub use mldsa::{MLDSA_MU_LEN, MLDSA_RND_LEN, MLDSA_TR_LEN};
264pub use mldsa::{MLDSA44_PK_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN};
265pub use mldsa::{MLDSA65_PK_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN};
266pub use mldsa::{MLDSA87_PK_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN};
267
268pub use mldsa::SUSPENDED_MU_BUILDER_STATE_LEN;
269
270// re-export just so it's visible to unit tests
271pub use polynomial::Polynomial;