diff --git a/.gitignore b/.gitignore index 6d42084d..fe17c9f5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,13 @@ mutants.out*/ .idea/ .vscode/ + +# Claude Code: ignore personal/local state, but share team tooling +# (skills, slash commands, subagents, and project settings.json). +.claude/* +!.claude/settings.json +!.claude/skills/ +!.claude/commands/ +!.claude/agents/ +.claude/settings.local.json +.claude 2/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 82b379fe..f5b8c7a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ version = "0.1.3" # *** Internal Dependencies *** bouncycastle = { path = "./" } +bouncycastle-aes-lowmemory = { path = "./crypto/aes-lowmemory" } bouncycastle-base64 = { path = "./crypto/base64" } bouncycastle-core = { path = "crypto/core" } bouncycastle-core-test-framework = { path = "./crypto/core-test-framework" } @@ -41,6 +42,7 @@ version.workspace = true edition.workspace = true [dependencies] +bouncycastle-aes-lowmemory.workspace = true bouncycastle-base64.workspace = true bouncycastle-core.workspace = true bouncycastle-factory.workspace = true diff --git a/QUALITY_AND_STYLE.md b/QUALITY_AND_STYLE.md index 65f7e7e0..eeadbbab 100644 --- a/QUALITY_AND_STYLE.md +++ b/QUALITY_AND_STYLE.md @@ -63,7 +63,20 @@ which parts were done for a very specific reason and should not be changed on a ## Naming Conventions -All normal rust naming convensions from clippy apply. In addition, some library-specific naming conventions: +All normal rust naming conventions from clippy apply, with the following exceptions: + +* Many bouncycastle crates use `#[allow(non_snake_case)]`, `#[allow(non_upper_case_globals)]` + `#[allow(non_camel_case_types)]` and so forth, either locally or crate-wide to indicate a preference for keeping the + exact capitalization from a spec (FIPS, RFC, etc) over following rust convention. For example, a struct implementing + the Advanced Encryption Standard (AES) in Galois Counter Mode (GCM) should be named `struct AES_GCM` to match the spec + even though rust convention might be `struct AesGcm`. The same goes for variable and constant names, for example, if a + spec uses a notation where `A` is a matrix and `a` is vector, then it is perfectly acceptable to do + `let A = Matrix;` and + `let a = Vector`, or the global constant `Rcon` should keep that capitalization and not `RCON` or `R_CON`. The + intention is to speed up human code review by a human familiar with the spec is more important than following rust + convention. + +In addition, some library-specific naming conventions: * In constants, "LEN" is the length of a value in bytes (typically used for sizing arrays), whereas "SIZE" is a value in bits (typically used as a security parameter). For example SHA256 could have constants `HASH_SIZE = 256` and diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index 57f9e97c..5560df6b 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -2,8 +2,11 @@ ## Major features +* New crate `bouncycastle-aes-lowmemory` (`bouncycastle::aes_lowmemory`): AES-128/192/256 as a raw keyed block + permutation (NIST FIPS 197). + ## Minor features / bug fixes * bug fixes to the way SHA3/SHAKE handled absorbing and squeezing a partial final byte. * Design discussions about whether core::traits::XOF (in the abstract) should allow interleaving absorb -> squeeze -> - absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future. + absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future. \ No newline at end of file diff --git a/crypto/aes-lowmemory/Cargo.toml b/crypto/aes-lowmemory/Cargo.toml new file mode 100644 index 00000000..04266615 --- /dev/null +++ b/crypto/aes-lowmemory/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "bouncycastle-aes-lowmemory" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-utils.workspace = true + +[dev-dependencies] +bouncycastle-hex.workspace = true +bouncycastle-rng.workspace = true +criterion.workspace = true +serde_json = "1.0" # for parsing test vector files + +[[bench]] +name = "aes_benches" +harness = false diff --git a/crypto/aes-lowmemory/benches/aes_benches.rs b/crypto/aes-lowmemory/benches/aes_benches.rs new file mode 100644 index 00000000..3ca6495a --- /dev/null +++ b/crypto/aes-lowmemory/benches/aes_benches.rs @@ -0,0 +1,204 @@ +//! Criterion benchmarks for the bit-sliced AES engine. + +use bouncycastle_aes_lowmemory::{ + AES_128, AES_192, AES_256, AES128Params, AES192Params, AES256Params, AESParams, BLOCK_LEN, +}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::RNG; +use bouncycastle_rng as rng; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +/// 16 KiB of data, i.e. 1024 AES blocks. +const NUM_BLOCKS: usize = 1024; +const DATA_LEN: usize = NUM_BLOCKS * BLOCK_LEN; + +fn random_blocks() -> Vec<[u8; BLOCK_LEN]> { + let mut blocks = vec![[0u8; BLOCK_LEN]; NUM_BLOCKS]; + let mut generator = rng::DefaultRNG::default(); + for block in blocks.iter_mut() { + generator.next_bytes_out(block).unwrap(); + } + blocks +} + +fn key() -> KeyMaterial { + let mut bytes = [0u8; N]; + rng::DefaultRNG::default().next_bytes_out(&mut bytes).unwrap(); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() +} + +fn bench_key_expansion(c: &mut Criterion) { + let mut group = c.benchmark_group("aes_lowmemory::key expansion"); + + let key128 = key::<16>(); + group.throughput(Throughput::Bytes(::KEY_LEN as u64)); + group.bench_function("Aes128::new()", |b| { + b.iter(|| black_box(AES_128::new(black_box(&key128)).unwrap())) + }); + + let key192 = key::<24>(); + group.throughput(Throughput::Bytes(::KEY_LEN as u64)); + group.bench_function("Aes192::new()", |b| { + b.iter(|| black_box(AES_192::new(black_box(&key192)).unwrap())) + }); + + let key256 = key::<32>(); + group.throughput(Throughput::Bytes(::KEY_LEN as u64)); + group.bench_function("Aes256::new()", |b| { + b.iter(|| black_box(AES_256::new(black_box(&key256)).unwrap())) + }); + + group.finish(); +} + +fn bench_aes128(c: &mut Criterion) { + let aes = AES_128::new(&key::<16>()).unwrap(); + let mut blocks = random_blocks(); + + let mut group = c.benchmark_group("aes_lowmemory::Aes128"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB -- .encrypt_block() x1024", |b| { + b.iter(|| { + // So that we're not making copies within the measured loop, we'll just + // encrypt the ciphertext over and over again. + for block in blocks.iter_mut() { + aes.encrypt_block(black_box(block)); + } + black_box(&blocks); + }) + }); + + group.bench_function("16KiB -- .encrypt_blocks2() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for pair in buf.chunks_exact_mut(2) { + // `try_into` cannot fail: `chunks_exact_mut(2)` yields slices of length 2. + let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap(); + aes.encrypt_2blocks(black_box(pair)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .decrypt_block() x1024", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for block in buf.iter_mut() { + aes.decrypt_block(black_box(block)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .decrypt_blocks2() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for pair in buf.chunks_exact_mut(2) { + let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap(); + aes.decrypt_2blocks(black_box(pair)); + } + black_box(&buf); + }) + }); + + group.finish(); +} + +fn bench_aes192(c: &mut Criterion) { + let aes = AES_192::new(&key::<24>()).unwrap(); + let mut blocks = random_blocks(); + + let mut group = c.benchmark_group("aes_lowmemory::Aes192"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB -- .encrypt_block() x1024", |b| { + b.iter(|| { + // So that we're not making copies within the measured loop, we'll just + // encrypt the ciphertext over and over again. + for block in blocks.iter_mut() { + aes.encrypt_block(black_box(block)); + } + black_box(&blocks); + }) + }); + + group.bench_function("16KiB -- .encrypt_blocks2() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for pair in buf.chunks_exact_mut(2) { + let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap(); + aes.encrypt_2blocks(black_box(pair)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .decrypt_block() x1024", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for block in buf.iter_mut() { + aes.decrypt_block(black_box(block)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .decrypt_blocks2() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for pair in buf.chunks_exact_mut(2) { + let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap(); + aes.decrypt_2blocks(black_box(pair)); + } + black_box(&buf); + }) + }); + + group.finish(); +} + +fn bench_aes256(c: &mut Criterion) { + let aes = AES_256::new(&key::<32>()).unwrap(); + let mut blocks = random_blocks(); + + let mut group = c.benchmark_group("aes_lowmemory::Aes256"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB -- .encrypt_block() x1024", |b| { + b.iter(|| { + for block in blocks.iter_mut() { + aes.encrypt_block(black_box(block)); + } + black_box(&blocks); + }) + }); + + group.bench_function("16KiB -- .encrypt_blocks2() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for pair in buf.chunks_exact_mut(2) { + let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap(); + aes.encrypt_2blocks(black_box(pair)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .decrypt_blocks2() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for pair in buf.chunks_exact_mut(2) { + let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap(); + aes.decrypt_2blocks(black_box(pair)); + } + black_box(&buf); + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_key_expansion, bench_aes128, bench_aes192, bench_aes256); +criterion_main!(benches); diff --git a/crypto/aes-lowmemory/src/aes.rs b/crypto/aes-lowmemory/src/aes.rs new file mode 100644 index 00000000..d8c26382 --- /dev/null +++ b/crypto/aes-lowmemory/src/aes.rs @@ -0,0 +1,246 @@ +//! CIPHER() and INVCIPHER() (FIPS 197 §5.1 and §5.3), and the public types. + +use crate::bitslice::{Planes, pack, unpack}; +use crate::round::{add_round_key, inv_mix_columns, inv_shift_rows, mix_columns, shift_rows}; +use crate::sbox::{inv_sbox, sbox}; +use crate::schedule::{AES128Params, AES192Params, AES256Params, AESParams, expand, round_key}; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{Algorithm, SecurityStrength}; +use bouncycastle_utils::secret::Secret; + +/// The AES block length in bytes: 16 (FIPS 197 §5, Table 3, `Nb` = 4 words). +/// AES-128, AES-192, and AES-256 all use a block length of 16 bytes. +pub const BLOCK_LEN: usize = 16; + +/// One 16-byte AES block, in the order of FIPS 197 Eq (3.6): `block[r + 4c] == s[r,c]`. +pub type Block = [u8; BLOCK_LEN]; + +/// The AES keyed permutation, parameterised by key length. +/// +/// The core internal implementation of the ML-KEM algorithm. +/// This needs to be public for the compiler to be able to find it, +/// but is shouldn't ever need to be used directly. +/// Please use the named public types. +pub struct AES { + schedule: Secret, +} + +/// AES-128: 16-byte key, 10 rounds (FIPS 197 §5, Table 3). +pub type AES_128 = AES; +/// AES-192: 24-byte key, 12 rounds (FIPS 197 §5, Table 3). +pub type AES_192 = AES; +/// AES-256: 32-byte key, 14 rounds (FIPS 197 §5, Table 3). +pub type AES_256 = AES; + +impl AES

{ + /// Checks a key is fit to use before it is expanded: + /// + /// It must be: + /// * tagged [`KeyType::SymmetricCipherKey`], + /// * exactly `P::KEY_LEN` bytes, + /// * must carry a [`SecurityStrength`] at least equal to its own length. + /// + /// Takes `&dyn KeyMaterialTrait` so the three constructors, whose `KeyMaterial` capacities + /// differ, can share one implementation. + fn validate(key: &dyn KeyMaterialTrait) -> Result<(), SymmetricCipherError> { + if key.key_type() != KeyType::SymmetricCipherKey { + return Err(KeyMaterialError::InvalidKeyType( + "AES requires a key of type KeyType::SymmetricCipherKey.", + ) + .into()); + } + if key.key_len() != P::KEY_LEN { + return Err(KeyMaterialError::InvalidLength.into()); + } + if key.security_strength() < SecurityStrength::from_bytes(P::KEY_LEN) { + return Err(KeyMaterialError::SecurityStrength( + "The provided key has a lower security strength than the AES key length implies.", + ) + .into()); + } + Ok(()) + } + + /// CIPHER() (FIPS 197 Sec 5.1, Algorithm 1) acting on two blocks at once. + fn cipher2(&self, q: &mut Planes) { + // line 3: state = state XOR w[0..3] + add_round_key(q, &round_key::

(&self.schedule, 0)); + + // lines 4-9: for round from 1 to Nr - 1 + for round in 1..P::NR { + sbox(q); // line 5, SUBBYTES() + shift_rows(q); // line 6, SHIFTROWS() + mix_columns(q); // line 7, MIXCOLUMNS() + add_round_key(q, &round_key::

(&self.schedule, round)); // line 8 + } + + // lines 10-12: the final round has no MIXCOLUMNS() + sbox(q); + shift_rows(q); + add_round_key(q, &round_key::

(&self.schedule, P::NR)); + } + + /// INVCIPHER() (FIPS 197 Sec 5.3, Algorithm 3) acting on two blocks at once. + fn inv_cipher2(&self, q: &mut Planes) { + // line 3: state = state XOR w[4*Nr .. 4*Nr+3] + add_round_key(q, &round_key::

(&self.schedule, P::NR)); + + // lines 4-9: for round from Nr - 1 down to 1 + for round in (1..P::NR).rev() { + inv_shift_rows(q); // line 5, INVSHIFTROWS() + inv_sbox(q); // line 6, INVSUBBYTES() + add_round_key(q, &round_key::

(&self.schedule, round)); // line 7 + inv_mix_columns(q); // line 8, INVMIXCOLUMNS() + } + + // lines 10-12: the final inverse round has no INVMIXCOLUMNS() + inv_shift_rows(q); + inv_sbox(q); + add_round_key(q, &round_key::

(&self.schedule, 0)); + } + + /// Encrypts one block in place. + /// + /// The bit-sliced state always acts on two blocks, so this encrypts the provided block and a + /// dummy block. Use [`AES::encrypt_2blocks`] where two blocks are available. + // Dev note: This is acting on two copies of the provided block, which costs exactly what filling + // the unused half with zeros would, and it buys a free self-check: the two halves must come out + // equal, which `debug_assert` verifies. + pub fn encrypt_block(&self, block: &mut Block) { + let mut q = pack(block, block); + self.cipher2(&mut q); + let mut discard = [0u8; BLOCK_LEN]; + unpack(&q, block, &mut discard); + debug_assert_eq!(*block, discard, "the two interleaved halves must agree"); + } + + /// Encrypts two blocks in place. + /// + /// The internal bit-sliced state is constructed to act on two blocks simultaneously, so two + /// blocks cost almost exactly the same as one. + /// Prefer this over two [`AES::encrypt_block`] calls whenever two blocks are available and + /// independent -- which, for a mode of operation, means CTR, or the decryption direction of CBC + /// and CFB, but *not* CBC encryption, whose blocks are serially dependent. + pub fn encrypt_2blocks(&self, blocks: &mut [Block; 2]) { + let mut q = pack(&blocks[0], &blocks[1]); + self.cipher2(&mut q); + let (a, b) = blocks.split_at_mut(1); + unpack(&q, &mut a[0], &mut b[0]); + } + + /// Decrypts one block in place. See [`AES::encrypt_block`]. + pub fn decrypt_block(&self, block: &mut Block) { + let mut q = pack(block, block); + self.inv_cipher2(&mut q); + let mut discard = [0u8; BLOCK_LEN]; + unpack(&q, block, &mut discard); + debug_assert_eq!(*block, discard, "the two interleaved halves must agree"); + } + + /// Decrypts two blocks in place. See [`AES::encrypt_2blocks`]. + pub fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + let mut q = pack(&blocks[0], &blocks[1]); + self.inv_cipher2(&mut q); + let (a, b) = blocks.split_at_mut(1); + unpack(&q, &mut a[0], &mut b[0]); + } +} + +impl AES_128 { + /// Expands a 16-byte key into an AES-128 schedule. + /// + /// # Errors + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not [`KeyType::SymmetricCipherKey`]. + /// * [`KeyMaterialError::InvalidLength`] if the key is not 16 bytes long. + /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 128 bits. + pub fn new(key: &KeyMaterial<16>) -> Result { + Self::validate(key)?; + Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + } +} + +impl AES_192 { + /// Expands a 24-byte key into an AES-192 schedule. See [`AES_128::new`] for the error cases. + pub fn new(key: &KeyMaterial<24>) -> Result { + Self::validate(key)?; + Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + } +} + +impl AES_256 { + /// Expands a 32-byte key into an AES-256 schedule. See [`AES_128::new`] for the error cases. + pub fn new(key: &KeyMaterial<32>) -> Result { + Self::validate(key)?; + Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + } +} + +impl Algorithm for AES_128 { + const ALG_NAME: &'static str = AES128Params::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl Algorithm for AES_192 { + const ALG_NAME: &'static str = AES192Params::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; +} + +impl Algorithm for AES_256 { + const ALG_NAME: &'static str = AES256Params::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +} + +impl core::fmt::Debug for AES

{ + /// Prints the algorithm name only. The key schedule is secret and is never formatted. + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(P::ALG_NAME) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_size_constants_match_the_documented_memory_table() { + // The "Memory Usage" table in the crate docs quotes these, and the whole point of the + // crate is that they are this small: 4 * (Nr + 1) words of schedule, nothing else, and no + // tables anywhere. If the representation grows, the docs are wrong -- fix both. + assert_eq!(size_of::(), 176, "AES-128: 4 * (10 + 1) words"); + assert_eq!(size_of::(), 208, "AES-192: 4 * (12 + 1) words"); + assert_eq!(size_of::(), 240, "AES-256: 4 * (14 + 1) words"); + } + + #[test] + fn test_engine_size_is_exactly_the_schedule() { + // No round counter, no direction flag, no initialised marker: the schedule is all there + // is, which is what makes both directions available from one value at no extra cost. + assert_eq!(size_of::(), size_of::<::Schedule>()); + assert_eq!(size_of::(), size_of::<::Schedule>()); + assert_eq!(size_of::(), size_of::<::Schedule>()); + } + + #[test] + fn test_alg_names() { + assert_eq!(::ALG_NAME, "AES-128"); + assert_eq!(::ALG_NAME, "AES-192"); + assert_eq!(::ALG_NAME, "AES-256"); + } + + #[test] + fn test_max_security_strength_matches_the_key_length() { + assert_eq!( + ::MAX_SECURITY_STRENGTH, + SecurityStrength::from_bytes(AES128Params::KEY_LEN) + ); + assert_eq!( + ::MAX_SECURITY_STRENGTH, + SecurityStrength::from_bytes(AES192Params::KEY_LEN) + ); + assert_eq!( + ::MAX_SECURITY_STRENGTH, + SecurityStrength::from_bytes(AES256Params::KEY_LEN) + ); + } +} diff --git a/crypto/aes-lowmemory/src/bitslice.rs b/crypto/aes-lowmemory/src/bitslice.rs new file mode 100644 index 00000000..43c1ad2e --- /dev/null +++ b/crypto/aes-lowmemory/src/bitslice.rs @@ -0,0 +1,163 @@ +//! Conversion functions between AES blocks and the bit-sliced representation the round functions act on. +//! +//! The round functions in [`crate::round`] and the S-box in [`crate::sbox`] opperate accourding to +//! circuit `SLP_AES_113.txt` from +//! Peralta's circuit collection, described in J. Boyar and R. Peralta, "A new combinational logic +//! minimization technique with applications to cryptology", . +//! Its fundamental innovation is to take the 16 bytes of the state and effectively transpose it into +//! 8 u16's where the i'th u16 holds the i'th bit of each byte of the state. +//! That is what lets the S-box be a Boolean circuit: one +//! `&` or `^` on a plane applies that gate to all sixteen byte positions at once, and no memory +//! access is ever indexed by a secret value. +//! This implementation handles two input blocks at a time, so the planes are in fact u32's still with +//! 8 lanes. + +use crate::aes::Block; +use bouncycastle_utils::secret::Secret; + +/// The eight bit-planes holding two blocks. See the module docs for the layout. +pub(crate) type Planes = [u32; 8]; + +/// Transposes bytes into bit-planes, and back -- it is its own inverse. +/// +/// Translated from BearSSL `aes_ct.c:br_aes_ct_ortho` (the `SWAP2`/`SWAP4`/`SWAP8` macros). +pub(crate) fn ortho(q: &mut Planes) { + /// One masked swap: exchanges the `cl`-selected fields of `y` into `x` and the `ch`-selected + /// fields of `x` into `y`, moving them by `s` bit positions. + /// + /// `cl` and `ch` are complementary, and `s` is exactly the field width, so in each returned + /// word the two combined operands occupy disjoint bits: `(x & cl)` and `(y & cl) << s` cannot + /// both be set in the same position. + /// + /// Mutants note: `|` and `^` compute the same function here. + #[inline(always)] + fn swap(cl: u32, ch: u32, s: u32, x: u32, y: u32) -> (u32, u32) { + ((x & cl) | ((y & cl) << s), ((x & ch) >> s) | (y & ch)) + } + + // Stage 1: swap single bits between adjacent words (0x55 = even bits, 0xAA = odd bits). + for (a, b) in [(0, 1), (2, 3), (4, 5), (6, 7)] { + (q[a], q[b]) = swap(0x5555_5555, 0xAAAA_AAAA, 1, q[a], q[b]); + } + // Stage 2: swap 2-bit fields between words two apart. + for (a, b) in [(0, 2), (1, 3), (4, 6), (5, 7)] { + (q[a], q[b]) = swap(0x3333_3333, 0xCCCC_CCCC, 2, q[a], q[b]); + } + // Stage 3: swap nibbles between words four apart. + for (a, b) in [(0, 4), (1, 5), (2, 6), (3, 7)] { + (q[a], q[b]) = swap(0x0F0F_0F0F, 0xF0F0_F0F0, 4, q[a], q[b]); + } +} + +/// Loads two blocks into the bit-planes. +/// +/// Block `a` goes into the even words and block `b` into the odd words as little-endian `u32`s, +/// then [`ortho`] transposes them into planes. +/// +/// As this represents the working state of the block cipher, it is wrapped in [`Secret`]. +pub(crate) fn pack(a: &Block, b: &Block) -> Secret { + let mut q = Secret::<[u32; 8]>::new(); + for c in 0..4 { + // `try_into` cannot fail: the slice is a fixed 4-byte window of a 16-byte array. + q[2 * c] = u32::from_le_bytes(a[4 * c..4 * c + 4].try_into().unwrap()); + q[2 * c + 1] = u32::from_le_bytes(b[4 * c..4 * c + 4].try_into().unwrap()); + } + ortho(&mut q); + q +} + +/// Reads two blocks back out of the bit-planes; the exact inverse of [`pack`]. +pub(crate) fn unpack(q: &Planes, a: &mut Block, b: &mut Block) { + let mut q = *q; + ortho(&mut q); + for c in 0..4 { + a[4 * c..4 * c + 4].copy_from_slice(&q[2 * c].to_le_bytes()); + b[4 * c..4 * c + 4].copy_from_slice(&q[2 * c + 1].to_le_bytes()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A deterministic byte generator, so the tests do not depend on an RNG crate. + pub(crate) fn pseudo_random_block(seed: u32) -> Block { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + let mut out = [0u8; 16]; + for byte in out.iter_mut() { + // xorshift32; quality is irrelevant, only that it varies every bit position. + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + *byte = (state >> 24) as u8; + } + out + } + + #[test] + fn test_layout_matches_the_documented_table() { + // Pins the module doc table: q[k] bit (8r + 2c) is bit k of s[r,c] of block A, and + // bit (8r + 2c + 1) is bit k of s[r,c] of block B. Every mask in `round` depends on it. + let a = pseudo_random_block(1); + let b = pseudo_random_block(2); + let q = pack(&a, &b); + + for j in 0..16 { + let (r, c) = (j % 4, j / 4); + let pos = 8 * r + 2 * c; + for (k, plane) in q.iter().enumerate() { + assert_eq!( + (plane >> pos) & 1, + u32::from((a[j] >> k) & 1), + "block A: plane {k} bit {pos} should be bit {k} of byte {j}" + ); + assert_eq!( + (plane >> (pos + 1)) & 1, + u32::from((b[j] >> k) & 1), + "block B: plane {k} bit {} should be bit {k} of byte {j}", + pos + 1 + ); + } + } + } + + #[test] + fn test_ortho_is_an_involution() { + let mut q = [ + 0x0123_4567, 0x89AB_CDEF, 0xFEDC_BA98, 0x7654_3210, 0xDEAD_BEEF, 0x0000_0001, + 0xFFFF_FFFF, 0xA5A5_5A5A, + ]; + let original = q; + ortho(&mut q); + assert_ne!(q, original, "ortho should actually move bits"); + ortho(&mut q); + assert_eq!(q, original); + } + + #[test] + fn test_unpack_inverts_pack() { + for seed in 0..64 { + let a = pseudo_random_block(seed); + let b = pseudo_random_block(seed + 1000); + let mut out_a = [0u8; 16]; + let mut out_b = [0u8; 16]; + unpack(&pack(&a, &b), &mut out_a, &mut out_b); + assert_eq!(out_a, a); + assert_eq!(out_b, b); + } + } + + #[test] + fn test_the_two_halves_are_independent() { + // Changing block B must not disturb block A anywhere in the round-function pipeline; + // this pins that the interleave really is bit-parallel and not overlapping. + let a = pseudo_random_block(7); + let mut out_a1 = [0u8; 16]; + let mut out_a2 = [0u8; 16]; + let mut scratch = [0u8; 16]; + unpack(&pack(&a, &[0u8; 16]), &mut out_a1, &mut scratch); + unpack(&pack(&a, &pseudo_random_block(9)), &mut out_a2, &mut scratch); + assert_eq!(out_a1, out_a2); + assert_eq!(out_a1, a); + } +} diff --git a/crypto/aes-lowmemory/src/lib.rs b/crypto/aes-lowmemory/src/lib.rs new file mode 100644 index 00000000..60d2b1d9 --- /dev/null +++ b/crypto/aes-lowmemory/src/lib.rs @@ -0,0 +1,135 @@ +//! A constant-time, table-free AES block cipher implementation according to NIST FIPS 197. +//! +//! This crate provides the raw AES keyed permutation -- [`AES_128`], [`AES_192`] and [`AES_256`] -- +//! implemented as a Boolean circuit over bit-planes rather than as byte substitutions through a +//! lookup table. That makes it both smaller and constant-time; see [Design](#design). +//! +//! It is a *permutation*, not a cipher you can encrypt data with. See +//! [Security Considerations](#security-considerations). +//! +//! # Usage Examples +//! +//! ## Encrypting and decrypting a single block +//! +//! 🚨 Security Note 🚨 : This crate exposes only the single-block primitive (equivalent to ECB mode) +//! and is not generally secure to use on its own, but instead is a building block for higher-level +//! constructions such as AES_CBC or AES_GCM. +//! +//! ``` +//! use bouncycastle_aes_lowmemory::AES_128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type( +//! &[0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, +//! 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c], +//! KeyType::SymmetricCipherKey, +//! ).expect("a 16-byte symmetric cipher key"); +//! +//! // Instantiate the key schedule and create an object ready to encrypt or decrypt. +//! let aes = AES_128::new(&key).expect("a valid AES-128 key"); +//! +//! // This is not quite a "plaintext" since it has to be exactly one block (16 bytes) wide. +//! // Example from FIPS 197 Appendix B. +//! let mut block = [0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, +//! 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, 0x34]; +//! aes.encrypt_block(&mut block); +//! assert_eq!(block, [0x39, 0x25, 0x84, 0x1d, 0x02, 0xdc, 0x09, 0xfb, +//! 0xdc, 0x11, 0x85, 0x97, 0x19, 0x6a, 0x0b, 0x32]); +//! +//! // The same value decrypts, from the same schedule -- there is no separate decryptor. +//! aes.decrypt_block(&mut block); +//! assert_eq!(block, [0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, +//! 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, 0x34]); +//! ``` +//! +//! ## Two blocks at a time +//! +//! The bit-sliced state holds two blocks, so two independent blocks cost barely more than one. +//! Where a caller has two, [`AES::encrypt_2blocks`] is roughly twice the throughput of two +//! [`AES::encrypt_block`] calls: +//! +//! ``` +//! use bouncycastle_aes_lowmemory::AES_256; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! +//! let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! let aes = AES_256::new(&key).expect("a valid AES-256 key"); +//! +//! let mut blocks = [[0u8; 16], [1u8; 16]]; +//! aes.encrypt_2blocks(&mut blocks); +//! aes.decrypt_2blocks(&mut blocks); +//! assert_eq!(blocks, [[0u8; 16], [1u8; 16]]); +//! ``` +//! +//! # Design +//! +//! ## No lookup table! +//! +//! FIPS 197 Sec 5.1.1 presents the S-box as a table (Table 4), and most software AES +//! implementation store it as one. The trouble is that a table indexed by a byte of the state is +//! indexed by secret data, so on any CPU with a data cache the memory access pattern, and hence the +//! timing, depends on the key. That is a practical, repeatedly-demonstrated attack, and it is not +//! fixable with a lookup table-based implementation. +//! +//! This implementation uses instead the 113-gate straight-line S-box circuit `SLP_AES_113.txt` from +//! Peralta's circuit collection, described in J. Boyar and R. Peralta, "A new combinational logic +//! minimization technique with applications to cryptology", . +//! +//! See [Constant Time](#constant-time-properties) for more discussion. +//! +//! # Memory Usage +//! +//! This is an all-stack, no-heap implementation. The only persistent state within the [`AES`] struct +//! is the key schedule, which is `4 * (Nr + 1)` 32-bit words. +//! +//! | Type | Key | AES struct | +//! |---|---|---|---|---| +//! | [`AES_128`] | 16 B | 176 B | +//! | [`AES_192`] | 24 B | 208 B | +//! | [`AES_256`] | 32 B | 240 B | +//! +//! Measured with `cargo run --release -p mem_usage_benches --bin bench_aes_mem_usage`. +// dev todo: if we can improve our testing framework to measure stack usages this small, +// it would be nice to add columns for stack usage of the encrypt and decrypt functions. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! ## A block permutation is not a cipher +//! +//! The [`AES`] function implemented in this crate is a building block for safe and secure AES +//! constructions, **but it must not be used directly to encrypt data**. +//! +//! It transforms exactly 16 bytes where the same input +//! block always gives the same output block; a mode called Electronic Code Book (ECB). +//! This by itself offers almost no security because even though the permutation is unique per key, +//! once an attacker who knows the structure or partial content of the plaintext message con relatively +//! easily build a dictionary of plaintext blocks to ciphertext blocks and fully decrypt the message. +//! +//! ## Constant-time properties +//! +//! By construction there is no secret-dependent memory access and no secret-dependent branch, +//! in the cipher *or* in the key schedule. +//! The only branches are the round loops, which count over the public value `Nr`. +//! +//! As with all cryptography written in pure safe rust, the Rust compiler makes no guarantee it will +//! preserve constant-time behaviours through its optimizations. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] +// `AesParams` is deliberately sealed with a private supertrait so that no fourth parameter set can +// be added outside this crate; that is what triggers this lint. +#![allow(private_bounds)] +// Turn off clippy on names because we want names to match exactly FIPS 197, even if that goes against +// Rust convention. +#![allow(non_camel_case_types)] + +mod aes; +mod bitslice; +mod round; +mod sbox; +mod schedule; + +pub use aes::{AES, AES_128, AES_192, AES_256, BLOCK_LEN, Block}; +pub use schedule::{AES128Params, AES192Params, AES256Params, AESParams}; diff --git a/crypto/aes-lowmemory/src/round.rs b/crypto/aes-lowmemory/src/round.rs new file mode 100644 index 00000000..449122b3 --- /dev/null +++ b/crypto/aes-lowmemory/src/round.rs @@ -0,0 +1,476 @@ +//! Implements AddRoundKey(), ShiftRows(), and MixColumns() from FIPS 197. +//! +//! Everything here is XOR, AND with a constant mask, and rotation by a constant. No operation +//! depends on the data, so all of it is inherently constant-time. + +use crate::bitslice::Planes; + +/// ADDROUNDKEY(): XORs a round key into the state (FIPS 197 Sec 5.1.4, Eq 5.9). +/// +/// Eq 5.9 XORs word `w[4*round + c]` into column `c`. Here the round key has already been +/// bit-sliced into the same plane layout as the state by [`crate::schedule`], so the whole +/// transformation -- all four columns of both blocks -- is eight XORs. +/// +/// This is its own inverse, which is why FIPS 197 Sec 5.3.4 needs no separate INVADDROUNDKEY(). +#[inline(always)] +pub(crate) fn add_round_key(q: &mut Planes, round_key: &Planes) { + for (plane, key_plane) in q.iter_mut().zip(round_key.iter()) { + *plane ^= *key_plane; + } +} + +/// SHIFTROWS(): cyclically shifts row `r` left by `r` columns (FIPS 197 Sec 5.1.2, Eq 5.5). +/// +/// Eq 5.5 is `s'[r,c] = s[r,(c + r) mod 4]`. Row `r` occupies byte-lane `r` of every plane and +/// one column is two bit positions, so the new column `c` must take what is two-bits-times-`r` +/// further up the lane: a **rotate right by `2r` within lane `r`**. Rotating right, not left, +/// because taking from a higher column index means pulling data down towards bit 0. +/// +/// Written out per lane rather than as a loop, so the shift amounts stay compile-time constants: +/// +/// * lane 0 (`r = 0`): rotate by 0, so bits `0..8` pass through untouched. +/// * lane 1 (`r = 1`): rotate right by 2. Bits 10..16 drop to 8..14; bits 8..10 wrap to 14..16. +/// * lane 2 (`r = 2`): rotate right by 4. Bits 20..24 drop to 16..20; bits 16..20 wrap up. +/// * lane 3 (`r = 3`): rotate right by 6. Bits 30..32 drop to 24..26; bits 24..30 wrap up. +/// +/// Both interleaved blocks move together, since a column step of two positions carries the A and +/// B bits of that column as a pair. +/// +/// Translated from BearSSL `aes_ct_enc.c:shift_rows`. +#[inline(always)] +pub(crate) fn shift_rows(q: &mut Planes) { + for plane in q.iter_mut() { + let x = *plane; + *plane = (x & 0x0000_00FF) + | ((x & 0x0000_FC00) >> 2) + | ((x & 0x0000_0300) << 6) + | ((x & 0x00F0_0000) >> 4) + | ((x & 0x000F_0000) << 4) + | ((x & 0xC000_0000) >> 6) + | ((x & 0x3F00_0000) << 2); + } +} + +/// INVSHIFTROWS(): cyclically shifts row `r` right by `r` columns +/// (FIPS 197 Sec 5.3.1, Eq 5.12). +/// +/// Eq 5.12 is `s'[r,c] = s[r,(c - r) mod 4]`, so this is [`shift_rows`] with every lane rotation +/// reversed: **rotate left by `2r` within lane `r`**. The masks are the complementary halves of +/// the forward ones. +/// +/// Translated from BearSSL `aes_ct_dec.c:inv_shift_rows`. +#[inline(always)] +pub(crate) fn inv_shift_rows(q: &mut Planes) { + for plane in q.iter_mut() { + let x = *plane; + *plane = (x & 0x0000_00FF) + | ((x & 0x0000_3F00) << 2) + | ((x & 0x0000_C000) >> 6) + | ((x & 0x000F_0000) << 4) + | ((x & 0x00F0_0000) >> 4) + | ((x & 0x0300_0000) << 6) + | ((x & 0xFC00_0000) >> 2); + } +} + +/// MIXCOLUMNS(): multiplies every column by the fixed matrix of Eq 5.7 +/// (FIPS 197 Sec 5.1.3). +/// +/// # Derivation +/// +/// Eq 5.8 gives each output byte of a column. Collecting the four rows, and writing `s[r]` for +/// the byte in row `r` of the column being processed, every row obeys the same rule: +/// +/// ```text +/// s'[r] = {02}.s[r] ^ {03}.s[r+1] ^ s[r+2] ^ s[r+3] (rows mod 4) +/// = {02}.(s[r] ^ s[r+1]) ^ s[r+1] ^ s[r+2] ^ s[r+3] +/// ``` +/// +/// using `{03} = {02} ^ {01}`. Because "the next row" is `rotate_right(8)` and "two rows down" is +/// `rotate_right(16)` (see the module docs), with `p` the state planes and `r` = `p` rotated by 8: +/// +/// * `p[k]` is bit `k` of `s[r]`, `r[k]` is bit `k` of `s[r+1]`, +/// * `rotate_right(16)` of those two gives bit `k` of `s[r+2]` and of `s[r+3]`. +/// +/// So `s[r+2] ^ s[r+3]` is `(p[k] ^ r[k]).rotate_right(16)`, which is the `rotr16(..)` term in +/// every line below, and `s[r+1]` is the bare `r[k]`. +/// +/// The remaining `{02}.(s[r] ^ s[r+1])` is XTIMES() (Eq 4.5) in the plane basis. Multiplying by +/// `x` shifts every bit up one plane, and the degree-8 term that falls off the top is reduced by +/// XOR-ing `{1b} = 0b0001_1011` -- bits 0, 1, 3 and 4. So with `v[k] = p[k] ^ r[k]`, plane `k` of +/// `{02}.v` is: +/// +/// * `v[k-1]` from the shift, for `k >= 1` (plane 0 gets nothing from the shift), and +/// * `v[7]`, the reduction, for `k` in {0, 1, 3, 4} only. +/// +/// That is exactly where the extra `p[7] ^ r[7]` terms appear below: in the lines for planes 0, 1, +/// 3 and 4, and nowhere else. Plane 0 is the one line with no `p[k-1] ^ r[k-1]` term. +/// +/// Translated from BearSSL `aes_ct_enc.c:mix_columns`; the equivalence to Eq 5.8 is pinned by +/// `test_mix_columns_matches_equation_5_8`. +#[inline(always)] +pub(crate) fn mix_columns(q: &mut Planes) { + let p = *q; + // r[k] holds the same bit position of the next row down. + let r: Planes = core::array::from_fn(|k| p[k].rotate_right(8)); + + // The `p[7] ^ r[7]` term is the {1b} reduction, present only in planes 0, 1, 3 and 4. + q[0] = p[7] ^ r[7] ^ r[0] ^ (p[0] ^ r[0]).rotate_right(16); + q[1] = p[0] ^ r[0] ^ p[7] ^ r[7] ^ r[1] ^ (p[1] ^ r[1]).rotate_right(16); + q[2] = p[1] ^ r[1] ^ r[2] ^ (p[2] ^ r[2]).rotate_right(16); + q[3] = p[2] ^ r[2] ^ p[7] ^ r[7] ^ r[3] ^ (p[3] ^ r[3]).rotate_right(16); + q[4] = p[3] ^ r[3] ^ p[7] ^ r[7] ^ r[4] ^ (p[4] ^ r[4]).rotate_right(16); + q[5] = p[4] ^ r[4] ^ r[5] ^ (p[5] ^ r[5]).rotate_right(16); + q[6] = p[5] ^ r[5] ^ r[6] ^ (p[6] ^ r[6]).rotate_right(16); + q[7] = p[6] ^ r[6] ^ r[7] ^ (p[7] ^ r[7]).rotate_right(16); +} + +/// INVMIXCOLUMNS(): multiplies every column by the inverse matrix of Eq 5.14 +/// (FIPS 197 Sec 5.3.3). +/// +/// The same shape as [`mix_columns`] -- `r` is the next row down, `rotate_right(16)` reaches two +/// rows further -- but the defining word of Sec 4.3 is `[{0e},{09},{0d},{0b}]` (Eq 5.13) instead +/// of `[{02},{01},{01},{03}]` (Eq 5.6). Those have degree up to 3, so expanding each product +/// through XTIMES() +/// in the plane basis produces many more terms than the forward direction, and the per-plane term +/// lists below are that expansion of Eq 5.15 rather than something readable line by line. +/// +/// The reduction terms are not confined to planes 0, 1, 3 and 4 here, because the higher-degree +/// coefficients feed carries into every plane. +/// +/// Translated from BearSSL `aes_ct_dec.c:inv_mix_columns`. +#[inline(always)] +#[rustfmt::skip] +pub(crate) fn inv_mix_columns(q: &mut Planes) { + let p = *q; + let r: Planes = core::array::from_fn(|k| p[k].rotate_right(8)); + + q[0] = p[5] ^ p[6] ^ p[7] ^ r[0] ^ r[5] ^ r[7] + ^ (p[0] ^ p[5] ^ p[6] ^ r[0] ^ r[5]).rotate_right(16); + q[1] = p[0] ^ p[5] ^ r[0] ^ r[1] ^ r[5] ^ r[6] ^ r[7] + ^ (p[1] ^ p[5] ^ p[7] ^ r[1] ^ r[5] ^ r[6]).rotate_right(16); + q[2] = p[0] ^ p[1] ^ p[6] ^ r[1] ^ r[2] ^ r[6] ^ r[7] + ^ (p[0] ^ p[2] ^ p[6] ^ r[2] ^ r[6] ^ r[7]).rotate_right(16); + q[3] = p[0] ^ p[1] ^ p[2] ^ p[5] ^ p[6] ^ r[0] ^ r[2] ^ r[3] ^ r[5] + ^ (p[0] ^ p[1] ^ p[3] ^ p[5] ^ p[6] ^ p[7] ^ r[0] ^ r[3] ^ r[5] ^ r[7]).rotate_right(16); + q[4] = p[1] ^ p[2] ^ p[3] ^ p[5] ^ r[1] ^ r[3] ^ r[4] ^ r[5] ^ r[6] ^ r[7] + ^ (p[1] ^ p[2] ^ p[4] ^ p[5] ^ p[7] ^ r[1] ^ r[4] ^ r[5] ^ r[6]).rotate_right(16); + q[5] = p[2] ^ p[3] ^ p[4] ^ p[6] ^ r[2] ^ r[4] ^ r[5] ^ r[6] ^ r[7] + ^ (p[2] ^ p[3] ^ p[5] ^ p[6] ^ r[2] ^ r[5] ^ r[6] ^ r[7]).rotate_right(16); + q[6] = p[3] ^ p[4] ^ p[5] ^ p[7] ^ r[3] ^ r[5] ^ r[6] ^ r[7] + ^ (p[3] ^ p[4] ^ p[6] ^ p[7] ^ r[3] ^ r[6] ^ r[7]).rotate_right(16); + q[7] = p[4] ^ p[5] ^ p[6] ^ r[4] ^ r[6] ^ r[7] + ^ (p[4] ^ p[5] ^ p[7] ^ r[4] ^ r[7]).rotate_right(16); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bitslice::{pack, unpack}; + + /// Runs a plane transformation over one block placed in both halves, returning the A half. + fn apply(f: fn(&mut Planes), block: [u8; 16]) -> [u8; 16] { + let mut q = pack(&block, &block); + f(&mut q); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + assert_eq!(a, b, "the two interleaved blocks must transform identically"); + a + } + + /// A block whose bytes are all distinct, so any mask error that moves a byte to the wrong + /// position is visible. + fn distinct_block() -> [u8; 16] { + core::array::from_fn(|i| (i as u8).wrapping_mul(17).wrapping_add(3)) + } + + // ---- byte-wise references, written from the FIPS 197 equations ---------------------- + // These use `state[r + 4c] == s[r,c]` (Eq 3.6). They exist only to check the plane + // implementations and are deliberately naive. + + /// Eq 5.5: `s'[r,c] = s[r,(c + r) mod 4]`. + fn ref_shift_rows(s: &[u8; 16]) -> [u8; 16] { + let mut o = [0u8; 16]; + for r in 0..4 { + for c in 0..4 { + o[r + 4 * c] = s[r + 4 * ((c + r) % 4)]; + } + } + o + } + + /// Eq 5.12: `s'[r,c] = s[r,(c - r) mod 4]`. + fn ref_inv_shift_rows(s: &[u8; 16]) -> [u8; 16] { + let mut o = [0u8; 16]; + for r in 0..4 { + for c in 0..4 { + o[r + 4 * c] = s[r + 4 * ((c + 4 - r) % 4)]; + } + } + o + } + + /// Eq 4.5 XTIMES(): multiply by `{02}` in GF(2^8). + fn xtimes(b: u8) -> u8 { + (b << 1) ^ if b & 0x80 != 0 { 0x1b } else { 0 } + } + + /// General GF(2^8) multiplication. Test-only; it branches on `b` and must never see secrets. + fn gf_mul(mut a: u8, mut b: u8) -> u8 { + let mut product = 0u8; + for _ in 0..8 { + if b & 1 != 0 { + product ^= a; + } + b >>= 1; + a = xtimes(a); + } + product + } + + /// Multiplication of a column by a fixed matrix, exactly as FIPS 197 Sec 4.3 defines it. + /// + /// Eq 4.8 gives the output word `[d0,d1,d2,d3]` from the input word `[b0,b1,b2,b3]` and the + /// matrix word `[a0,a1,a2,a3]`: + /// + /// ```text + /// d0 = (a0.b0) + (a3.b1) + (a2.b2) + (a1.b3) + /// d1 = (a1.b0) + (a0.b1) + (a3.b2) + (a2.b3) + /// d2 = (a2.b0) + (a1.b1) + (a0.b2) + (a3.b3) + /// d3 = (a3.b0) + (a2.b1) + (a1.b2) + (a0.b3) + /// ``` + /// + /// so entry `(r,k)` of the matrix is `a[(r - k) mod 4]`, which is what the indexing below is. + /// Both MIXCOLUMNS() and INVMIXCOLUMNS() use this same convention; only the word differs. + fn ref_mix_columns(s: &[u8; 16], coeffs: [u8; 4]) -> [u8; 16] { + let mut o = [0u8; 16]; + for c in 0..4 { + for r in 0..4 { + let mut v = 0u8; + for k in 0..4 { + v ^= gf_mul(s[k + 4 * c], coeffs[(r + 4 - k) % 4]); + } + o[r + 4 * c] = v; + } + } + o + } + + /// Eq 5.6: `[a0, a1, a2, a3] = [{02}, {01}, {01}, {03}]`. + /// + /// Note the order: it is *not* `[{02},{03},{01},{01}]`, which is the first row of the matrix + /// in Eq 5.7 rather than the defining word. Feeding the matrix row in here instead of the + /// word silently transposes the matrix, which happens to leave INVMIXCOLUMNS() passing, so + /// this is a comment worth keeping. + const MIX_COEFFS: [u8; 4] = [0x02, 0x01, 0x01, 0x03]; + /// Eq 5.13: `[a0, a1, a2, a3] = [{0e}, {09}, {0d}, {0b}]`. + const INV_MIX_COEFFS: [u8; 4] = [0x0e, 0x09, 0x0d, 0x0b]; + + /// Eq 5.8, transcribed literally, as a cross-check on [`ref_mix_columns`]. + /// + /// ```text + /// s'0,c = ({02}.s0,c) + ({03}.s1,c) + s2,c + s3,c + /// s'1,c = s0,c + ({02}.s1,c) + ({03}.s2,c) + s3,c + /// s'2,c = s0,c + s1,c + ({02}.s2,c) + ({03}.s3,c) + /// s'3,c = ({03}.s0,c) + s1,c + s2,c + ({02}.s3,c) + /// ``` + #[rustfmt::skip] + fn ref_mix_columns_literal(s: &[u8; 16]) -> [u8; 16] { + let mut o = [0u8; 16]; + for c in 0..4 { + let (s0, s1, s2, s3) = (s[4 * c], s[4 * c + 1], s[4 * c + 2], s[4 * c + 3]); + o[4 * c] = gf_mul(0x02, s0) ^ gf_mul(0x03, s1) ^ s2 ^ s3; + o[4 * c + 1] = s0 ^ gf_mul(0x02, s1) ^ gf_mul(0x03, s2) ^ s3; + o[4 * c + 2] = s0 ^ s1 ^ gf_mul(0x02, s2) ^ gf_mul(0x03, s3); + o[4 * c + 3] = gf_mul(0x03, s0) ^ s1 ^ s2 ^ gf_mul(0x02, s3); + } + o + } + + /// Eq 5.15, transcribed literally, as a cross-check on [`ref_mix_columns`]. + /// + /// ```text + /// s'0,c = ({0e}.s0,c) + ({0b}.s1,c) + ({0d}.s2,c) + ({09}.s3,c) + /// s'1,c = ({09}.s0,c) + ({0e}.s1,c) + ({0b}.s2,c) + ({0d}.s3,c) + /// s'2,c = ({0d}.s0,c) + ({09}.s1,c) + ({0e}.s2,c) + ({0b}.s3,c) + /// s'3,c = ({0b}.s0,c) + ({0d}.s1,c) + ({09}.s2,c) + ({0e}.s3,c) + /// ``` + #[rustfmt::skip] + fn ref_inv_mix_columns_literal(s: &[u8; 16]) -> [u8; 16] { + let mut o = [0u8; 16]; + for c in 0..4 { + let (s0, s1, s2, s3) = (s[4 * c], s[4 * c + 1], s[4 * c + 2], s[4 * c + 3]); + o[4 * c] = gf_mul(0x0e, s0) ^ gf_mul(0x0b, s1) ^ gf_mul(0x0d, s2) ^ gf_mul(0x09, s3); + o[4 * c + 1] = gf_mul(0x09, s0) ^ gf_mul(0x0e, s1) ^ gf_mul(0x0b, s2) ^ gf_mul(0x0d, s3); + o[4 * c + 2] = gf_mul(0x0d, s0) ^ gf_mul(0x09, s1) ^ gf_mul(0x0e, s2) ^ gf_mul(0x0b, s3); + o[4 * c + 3] = gf_mul(0x0b, s0) ^ gf_mul(0x0d, s1) ^ gf_mul(0x09, s2) ^ gf_mul(0x0e, s3); + } + o + } + + // ---- tests -------------------------------------------------------------------------- + + #[test] + fn test_the_two_reference_forms_agree() { + // Eq 5.7 (matrix, via the Sec 4.3 convention) against Eq 5.8 (explicit bytes), and the + // same for Eq 5.14 against Eq 5.15. This is what pins the coefficient word order: get + // MIX_COEFFS wrong and these disagree, independently of the plane implementation. + for seed in 0..32u8 { + let block: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(37) ^ seed); + assert_eq!(ref_mix_columns(&block, MIX_COEFFS), ref_mix_columns_literal(&block)); + assert_eq!( + ref_mix_columns(&block, INV_MIX_COEFFS), + ref_inv_mix_columns_literal(&block) + ); + } + } + + #[test] + fn test_xtimes_reference_matches_the_spec_example() { + // FIPS 197 Sec 4.2 works through {57} . {13}; the intermediate XTIMES() chain from + // Eq 4.5 is {57}, {ae}, {47}, {8e}, {07}. + assert_eq!(xtimes(0x57), 0xae); + assert_eq!(xtimes(0xae), 0x47); + assert_eq!(xtimes(0x47), 0x8e); + assert_eq!(xtimes(0x8e), 0x07); + // and the product itself, {57} . {13} = {fe}. + assert_eq!(gf_mul(0x57, 0x13), 0xfe); + } + + #[test] + fn test_shift_rows_matches_equation_5_5() { + for seed in 0..32u8 { + let block: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(31) ^ seed); + assert_eq!(apply(shift_rows, block), ref_shift_rows(&block)); + } + assert_eq!(apply(shift_rows, distinct_block()), ref_shift_rows(&distinct_block())); + } + + #[test] + fn test_inv_shift_rows_matches_equation_5_12() { + for seed in 0..32u8 { + let block: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(31) ^ seed); + assert_eq!(apply(inv_shift_rows, block), ref_inv_shift_rows(&block)); + } + } + + #[test] + fn test_inv_shift_rows_inverts_shift_rows() { + let block = distinct_block(); + let mut q = pack(&block, &block); + shift_rows(&mut q); + inv_shift_rows(&mut q); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + assert_eq!(a, block); + } + + #[test] + fn test_shift_rows_is_a_bit_permutation() { + // Push a single set bit through and require exactly one bit out, with the induced map on + // bit positions a bijection. That is the real invariant behind the seven masked terms: + // their destination ranges are pairwise disjoint and together cover all 32 bits. + // + // It also explains a known `cargo mutants` result. The `| -> ^` mutants in [`shift_rows`] + // and [`inv_shift_rows`] survive, because on disjoint operands `|` and `^` compute the + // same function -- they are equivalent programs, not a gap in the tests, and no test can + // kill them. What *would* be a bug is masks that overlap or fail to cover, and this test + // is what rules that out. + for (name, f) in [ + ("shift_rows", shift_rows as fn(&mut Planes)), + ("inv_shift_rows", inv_shift_rows as fn(&mut Planes)), + ] { + let mut destinations = [false; 32]; + for bit in 0..32 { + let mut q: Planes = [1u32 << bit; 8]; + f(&mut q); + for plane in q { + assert_eq!( + plane.count_ones(), + 1, + "{name}: bit {bit} must map to exactly one bit, got {plane:#034b}" + ); + } + let dest = q[0].trailing_zeros() as usize; + assert!(!destinations[dest], "{name}: two source bits both map to bit {dest}"); + destinations[dest] = true; + } + assert!( + destinations.iter().all(|&hit| hit), + "{name}: the masks must cover all 32 bit positions" + ); + } + } + + #[test] + fn test_shift_rows_leaves_row_zero_alone() { + // Row 0 is bytes 0, 4, 8, 12 in the Eq 3.6 layout, and Eq 5.5 does not move it. + let block = distinct_block(); + let out = apply(shift_rows, block); + for c in 0..4 { + assert_eq!(out[4 * c], block[4 * c], "row 0, column {c}"); + } + } + + #[test] + fn test_mix_columns_matches_equation_5_8() { + for seed in 0..32u8 { + let block: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(37) ^ seed); + assert_eq!(apply(mix_columns, block), ref_mix_columns(&block, MIX_COEFFS)); + } + assert_eq!( + apply(mix_columns, distinct_block()), + ref_mix_columns(&distinct_block(), MIX_COEFFS) + ); + } + + #[test] + fn test_inv_mix_columns_matches_equation_5_15() { + for seed in 0..32u8 { + let block: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(37) ^ seed); + assert_eq!(apply(inv_mix_columns, block), ref_mix_columns(&block, INV_MIX_COEFFS)); + } + } + + #[test] + fn test_inv_mix_columns_inverts_mix_columns() { + let block = distinct_block(); + let mut q = pack(&block, &block); + mix_columns(&mut q); + inv_mix_columns(&mut q); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + assert_eq!(a, block); + } + + #[test] + fn test_add_round_key_is_its_own_inverse() { + let block = distinct_block(); + let key = pack(&[0xA5u8; 16], &[0x5Au8; 16]); + let mut q = pack(&block, &block); + add_round_key(&mut q, &key); + add_round_key(&mut q, &key); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + assert_eq!(a, block); + } + + #[test] + fn test_add_round_key_xors_the_expected_bytes() { + let block = distinct_block(); + let key_block = [0xA5u8; 16]; + let key = pack(&key_block, &key_block); + let mut q = pack(&block, &block); + add_round_key(&mut q, &key); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + for i in 0..16 { + assert_eq!(a[i], block[i] ^ key_block[i]); + } + } +} diff --git a/crypto/aes-lowmemory/src/sbox.rs b/crypto/aes-lowmemory/src/sbox.rs new file mode 100644 index 00000000..e676fdff --- /dev/null +++ b/crypto/aes-lowmemory/src/sbox.rs @@ -0,0 +1,331 @@ +//! SUBBYTES() and INVSUBBYTES() as a Boolean circuit (FIPS 197 Sec 5.1.1 and Sec 5.3.2). + +use crate::bitslice::Planes; + +/// SUBBYTES(): applies the AES S-box to every byte position of both blocks in `q` +/// (FIPS 197 Sec 5.1.1, the transformation tabulated in Table 4). +/// +/// The 113-gate Boyar-Peralta circuit, transcribed from `SLP_AES_113.txt`. See the module docs. +pub(crate) fn sbox(q: &mut Planes) { + // SLP inputs U0..U7, most-significant bit first, so U0 is the highest plane. + let u0 = q[7]; + let u1 = q[6]; + let u2 = q[5]; + let u3 = q[4]; + let u4 = q[3]; + let u5 = q[2]; + let u6 = q[1]; + let u7 = q[0]; + + let y14 = u3 ^ u5; + let y13 = u0 ^ u6; + let y9 = u0 ^ u3; + let y8 = u0 ^ u5; + let t0 = u1 ^ u2; + let y1 = t0 ^ u7; + let y4 = y1 ^ u3; + let y12 = y13 ^ y14; + let y2 = y1 ^ u0; + let y5 = y1 ^ u6; + let y3 = y5 ^ y8; + let t1 = u4 ^ y12; + let y15 = t1 ^ u5; + let y20 = t1 ^ u1; + let y6 = y15 ^ u7; + let y10 = y15 ^ t0; + let y11 = y20 ^ y9; + let y7 = u7 ^ y11; + let y17 = y10 ^ y11; + let y19 = y10 ^ y8; + let y16 = t0 ^ y11; + let y21 = y13 ^ y16; + let y18 = u0 ^ y16; + + let t2 = y12 & y15; + let t3 = y3 & y6; + let t4 = t3 ^ t2; + let t5 = y4 & u7; + let t6 = t5 ^ t2; + let t7 = y13 & y16; + let t8 = y5 & y1; + let t9 = t8 ^ t7; + let t10 = y2 & y7; + let t11 = t10 ^ t7; + let t12 = y9 & y11; + let t13 = y14 & y17; + let t14 = t13 ^ t12; + let t15 = y8 & y10; + let t16 = t15 ^ t12; + let t17 = t4 ^ y20; + let t18 = t6 ^ t16; + let t19 = t9 ^ t14; + let t20 = t11 ^ t16; + let t21 = t17 ^ t14; + let t22 = t18 ^ y19; + let t23 = t19 ^ y21; + let t24 = t20 ^ y18; + let t25 = t21 ^ t22; + let t26 = t21 & t23; + let t27 = t24 ^ t26; + let t28 = t25 & t27; + let t29 = t28 ^ t22; + let t30 = t23 ^ t24; + let t31 = t22 ^ t26; + let t32 = t31 & t30; + let t33 = t32 ^ t24; + let t34 = t23 ^ t33; + let t35 = t27 ^ t33; + let t36 = t24 & t35; + + // Mutants note: mutants reports the `^ -> |` mutant on the next line as surviving. That is a true + // equivalence, not a gap: `t36` and `t34` are never both 1 for any of the 256 possible input + // bytes, so XOR and OR agree here. It is the only one of the circuit's 77 XOR gates with that + // property. + let t37 = t36 ^ t34; + let t38 = t27 ^ t36; + let t39 = t29 & t38; + let t40 = t25 ^ t39; + let t41 = t40 ^ t37; + let t42 = t29 ^ t33; + let t43 = t29 ^ t40; + let t44 = t33 ^ t37; + let t45 = t42 ^ t41; + let z0 = t44 & y15; + let z1 = t37 & y6; + let z2 = t33 & u7; + let z3 = t43 & y16; + let z4 = t40 & y1; + let z5 = t29 & y7; + let z6 = t42 & y11; + let z7 = t45 & y17; + let z8 = t41 & y10; + let z9 = t44 & y12; + let z10 = t37 & y3; + let z11 = t33 & y4; + let z12 = t43 & y13; + let z13 = t40 & y5; + let z14 = t29 & y2; + let z15 = t42 & y9; + let z16 = t45 & y14; + let z17 = t41 & y8; + + let tc1 = z15 ^ z16; + let tc2 = z10 ^ tc1; + let tc3 = z9 ^ tc2; + let tc4 = z0 ^ z2; + let tc5 = z1 ^ z0; + let tc6 = z3 ^ z4; + let tc7 = z12 ^ tc4; + let tc8 = z7 ^ tc6; + let tc9 = z8 ^ tc7; + let tc10 = tc8 ^ tc9; + let tc11 = tc6 ^ tc5; + let tc12 = z3 ^ z5; + let tc13 = z13 ^ tc1; + let tc14 = tc4 ^ tc12; + let s3 = tc3 ^ tc11; + let tc16 = z6 ^ tc8; + let tc17 = z14 ^ tc10; + let tc18 = tc13 ^ tc14; + let s7 = !(z12 ^ tc18); + let tc20 = z15 ^ tc16; + let tc21 = tc2 ^ z11; + let s0 = tc3 ^ tc16; + let s6 = !(tc10 ^ tc18); + let s4 = tc14 ^ s3; + let s1 = !(s3 ^ tc16); + let tc26 = tc17 ^ tc20; + let s2 = !(tc26 ^ z17); + let s5 = tc21 ^ tc17; + + // SLP outputs S0..S7, most-significant bit first, mirroring the input mapping. + q[7] = s0; + q[6] = s1; + q[5] = s2; + q[4] = s3; + q[3] = s4; + q[2] = s5; + q[1] = s6; + q[0] = s7; +} + +/// INVSUBBYTES(): applies the inverse AES S-box to every byte position of both blocks in `q` +/// (FIPS 197 Sec 5.3.2, the transformation tabulated in Table 6). +/// +/// Rather than a second 113-gate circuit, this reuses [`sbox`] by conjugating it with the +/// inverse of its affine layer. Writing the S-box of Eq. 5.2 as `S(x) = A(I(x)) ^ {63}`, where +/// `I` is inversion in GF(2^8) and `A` the linear part, and letting `B` be the inverse of `A`: +/// +/// ```text +/// iS(x) = B(S(B(x ^ {63})) ^ {63}) +/// ``` +/// +/// which holds because `I` is an involution: +/// `iS(S(y)) = B(A(I(B(A(I(y)) ^ {63} ^ {63}))) ^ {63} ^ {63}) = y`. +/// +/// So applying [`inv_affine`], then the forward circuit, then [`inv_affine`] again yields the +/// inverse S-box, at the cost of 16 extra XORs and 8 complements instead of a whole second +/// circuit. Verified exhaustively against Table 6 by `test_inv_sbox_matches_fips197_table_6`. +/// +/// The derivation and the layer below are from BearSSL `aes_ct_dec.c` +/// (`br_aes_ct_bitslice_invSbox`). +pub(crate) fn inv_sbox(q: &mut Planes) { + inv_affine(q); + sbox(q); + inv_affine(q); +} + +/// `B(x ^ {63})`: the inverse of the affine layer of Eq. 5.2, composed with the constant. +/// +/// The complements on planes 0, 1, 5 and 6 are the `^ {63}`; the eight three-term XORs are `B`. +/// Translated from BearSSL `aes_ct_dec.c:br_aes_ct_bitslice_invSbox`. +fn inv_affine(q: &mut Planes) { + let q0 = !q[0]; + let q1 = !q[1]; + let q2 = q[2]; + let q3 = q[3]; + let q4 = q[4]; + let q5 = !q[5]; + let q6 = !q[6]; + let q7 = q[7]; + q[7] = q1 ^ q4 ^ q6; + q[6] = q0 ^ q3 ^ q5; + q[5] = q7 ^ q2 ^ q4; + q[4] = q6 ^ q1 ^ q3; + q[3] = q5 ^ q0 ^ q2; + q[2] = q4 ^ q7 ^ q1; + q[1] = q3 ^ q6 ^ q0; + q[0] = q2 ^ q5 ^ q7; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bitslice::{pack, unpack}; + + /// FIPS 197 Table 4 (SBOX), transcribed from the published PDF. Test-only: the + /// implementation evaluates the S-box as a Boolean circuit and never indexes a table. + #[rustfmt::skip] + const SBOX_TABLE_4: [u8; 256] = [ + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16, + ]; + + /// FIPS 197 Table 6 (INVSBOX), transcribed from the published PDF. Test-only. + #[rustfmt::skip] + const INVSBOX_TABLE_6: [u8; 256] = [ + 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, + 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, + 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, + 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, + 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, + 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, + 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, + 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, + 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, + 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, + 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, + 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, + 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, + 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, + 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d, + ]; + + /// Runs a plane transformation over a block placed in both halves, returning the A half. + /// + /// Filling both halves means a wrong interleave shows up as a difference between the two + /// blocks rather than silently passing. + fn apply(f: fn(&mut Planes), block: [u8; 16]) -> [u8; 16] { + let mut q = pack(&block, &block); + f(&mut q); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + assert_eq!(a, b, "the two interleaved blocks must transform identically"); + a + } + + #[test] + fn test_sbox_matches_fips197_table_4() { + // Exhaustive over the whole domain: this is the test that makes the 113 gates + // trustworthy, so it must stay exhaustive. + for x in 0..=255u8 { + let out = apply(sbox, [x; 16]); + assert!( + out.iter().all(|&b| b == out[0]), + "all 16 byte positions must substitute alike, x={x:#04x}" + ); + assert_eq!( + out[0], SBOX_TABLE_4[x as usize], + "SBOX({x:#04x}) should be {:#04x}", + SBOX_TABLE_4[x as usize] + ); + } + } + + #[test] + fn test_inv_sbox_matches_fips197_table_6() { + for x in 0..=255u8 { + let out = apply(inv_sbox, [x; 16]); + assert_eq!( + out[0], INVSBOX_TABLE_6[x as usize], + "INVSBOX({x:#04x}) should be {:#04x}", + INVSBOX_TABLE_6[x as usize] + ); + } + } + + #[test] + fn test_inv_sbox_inverts_sbox() { + for x in 0..=255u8 { + let mut q = pack(&[x; 16], &[x.wrapping_add(1); 16]); + sbox(&mut q); + inv_sbox(&mut q); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + assert_eq!(a, [x; 16]); + assert_eq!(b, [x.wrapping_add(1); 16]); + } + } + + #[test] + fn test_sbox_worked_example_from_section_5_1_1() { + // FIPS 197 Sec 5.1.1: "if s(r,c) = {53} ... s'(r,c) = {ed}". + assert_eq!(apply(sbox, [0x53; 16])[0], 0xed); + assert_eq!(SBOX_TABLE_4[0x53], 0xed); + } + + #[test] + fn test_the_two_spec_tables_are_inverses() { + // Guards the transcription of both tables against a typo in either one. + for x in 0..=255u8 { + assert_eq!(INVSBOX_TABLE_6[SBOX_TABLE_4[x as usize] as usize], x); + } + } + + #[test] + fn test_sbox_operates_on_each_byte_position_independently() { + // A block of distinct values, so a mask error that mixes byte positions is caught. + let block: [u8; 16] = core::array::from_fn(|i| (i as u8) * 17); + let out = apply(sbox, block); + for i in 0..16 { + assert_eq!(out[i], SBOX_TABLE_4[block[i] as usize], "byte position {i}"); + } + } +} diff --git a/crypto/aes-lowmemory/src/schedule.rs b/crypto/aes-lowmemory/src/schedule.rs new file mode 100644 index 00000000..5ef51fa3 --- /dev/null +++ b/crypto/aes-lowmemory/src/schedule.rs @@ -0,0 +1,436 @@ +//! KEYEXPANSION() (FIPS 197 Sec 5.2, Algorithm 2) and the per-key-length parameters. +//! +//! # Memory usage and representation +//! +//! The schedule is `4 * (Nr + 1)` words -- 44, 52 or 60 -- exactly as FIPS 197 Sec 5.2 defines +//! it, so 176, 208 or 240 bytes. It is stored in a **compressed** bit-sliced form: because +//! bit-slicing is a permutation of bits it does not change the size, and because both interleaved +//! blocks are encrypted under the same key the two halves of a bit-sliced round key are +//! identical, so only one of every pair of words needs keeping. [`round_key`] re-doubles a single +//! round key onto the stack when the round loop needs it. + +use crate::bitslice::{Planes, ortho}; +use crate::sbox::sbox; +use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; + +/// FIPS 197 Sec 5.2, Table 5: the round constants, `Rcon[j]` for `1 <= j <= 10`. +/// +/// Table 5 gives each as the word `[x, 00, 00, 00]`; only the leftmost byte is ever non-zero, and +/// words are held little-endian here, so the word `Rcon[j]` is just this byte. Indexing is shifted +/// by one against the spec: `RCON[j - 1]` is the spec's `Rcon[j]`, since the spec counts from 1. +#[allow(non_upper_case_globals)] +const Rcon: [u32; 10] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; + +/// A crate-private (aka "sealed") trait that prevents a new AES parameter set from being defined +/// outside this crate. +trait AESParamsInternalTrait {} + +/// The per-key-length constants of FIPS 197 §5, Table 3. +/// +/// Sealed via a private supertrait, so the three types below are the only implementations. +pub trait AESParams: AESParamsInternalTrait { + /// Key length in bytes: 16, 24 or 32 (FIPS 197 §5, Table 3). + const KEY_LEN: usize; + /// `Nk`, the key length in 32-bit words: 4, 6 or 8 (FIPS 197 §5, Table 3). + const NK: usize; + /// `Nr`, the number of rounds: 10, 12 or 14 (FIPS 197 §5, Table 3). + const NR: usize; + /// The algorithm name, as reported by `Algorithm::ALG_NAME`. + const ALG_NAME: &'static str; + /// The compressed schedule of size `[u32; 4 * (NR + 1)]`. See the module docs. + type Schedule: ZeroizablePrimitive + AsRef<[u32]> + AsMut<[u32]>; +} + +/// AES-128 parameters: 16-byte key, `Nk` = 4, `Nr` = 10 (FIPS 197 §5, Table 3). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AES128Params; +/// AES-192 parameters: 24-byte key, `Nk` = 6, `Nr` = 12 (FIPS 197 §5, Table 3). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AES192Params; +/// AES-256 parameters: 32-byte key, `Nk` = 8, `Nr` = 14 (FIPS 197 §5, Table 3). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AES256Params; + +impl AESParamsInternalTrait for AES128Params {} +impl AESParamsInternalTrait for AES192Params {} +impl AESParamsInternalTrait for AES256Params {} + +impl AESParams for AES128Params { + const KEY_LEN: usize = 16; + const NK: usize = 4; + const NR: usize = 10; + const ALG_NAME: &'static str = "AES-128"; + type Schedule = [u32; 44]; // 4 * (10 + 1) +} + +impl AESParams for AES192Params { + const KEY_LEN: usize = 24; + const NK: usize = 6; + const NR: usize = 12; + const ALG_NAME: &'static str = "AES-192"; + type Schedule = [u32; 52]; // 4 * (12 + 1) +} + +impl AESParams for AES256Params { + const KEY_LEN: usize = 32; + const NK: usize = 8; + const NR: usize = 14; + const ALG_NAME: &'static str = "AES-256"; + type Schedule = [u32; 60]; // 4 * (14 + 1) +} + +/// ROTWORD(): `[a0,a1,a2,a3] -> [a1,a2,a3,a0]` (FIPS 197 Sec 5.2, Eq 5.10). +/// +/// Words are held little-endian, so `a0` is the low byte. Moving `a1` down into the low byte and +/// wrapping `a0` to the top is a rotate right by 8 of the whole word. +#[inline(always)] +fn rot_word(word: u32) -> u32 { + word.rotate_right(8) +} + +/// SUBWORD(): applies the S-box to each of the four bytes of a word +/// (FIPS 197 Sec 5.2, Eq 5.11). +/// +/// It copies and acts on the provided word 8 times, which costs a full 8-word 113-gate S-box evaluation +/// which is wasteful, but it happens `Nr` or so times per key rather than per block. +/// +/// Translated from BearSSL `aes_ct.c:sub_word`. +fn sub_word(word: u32) -> u32 { + let mut q: Planes = [word; 8]; + ortho(&mut q); + sbox(&mut q); + ortho(&mut q); + + // Check that the redundant 8 copies of the word all came out the same. + debug_assert!( + q[0] == q[1] + && q[0] == q[2] + && q[0] == q[3] + && q[0] == q[4] + && q[0] == q[5] + && q[0] == q[6] + && q[0] == q[7] + ); + + q[0] +} + +/// KEYEXPANSION() (FIPS 197 Sec 5.2, Algorithm 2), returning the compressed bit-sliced schedule. +/// Algorithm 2 is followed literally. +/// +/// `key` must be exactly `P::KEY_LEN` bytes; [`crate::aes`] checks that before calling, so this +/// cannot fail and takes no `Result`. +pub(crate) fn expand(key: &[u8]) -> Secret { + debug_assert_eq!(key.len(), P::KEY_LEN); + + let mut schedule = Secret::::new(); + let w = (*schedule).as_mut(); + + // Algorithm 2 lines 2-6: w[i] = key[4i .. 4i+3] for i < Nk. + for i in 0..P::NK { + // Cannot fail: `key` is P::KEY_LEN == 4 * P::NK bytes, so this window is in bounds. + w[i] = u32::from_le_bytes(key[4 * i..4 * i + 4].try_into().unwrap()); + } + + // Algorithm 2 lines 7-16. + let mut temp = w[P::NK - 1]; // line 8, hoisted: w[i-1] is the temp from the previous pass + for i in P::NK..w.len() { + if i % P::NK == 0 { + // line 10: temp = SUBWORD(ROTWORD(temp)) XOR Rcon[i / Nk] + temp = sub_word(rot_word(temp)) ^ Rcon[i / P::NK - 1]; + } else if P::NK > 6 && i % P::NK == 4 { + // lines 11-12: the extra substitution that only AES-256 reaches + temp = sub_word(temp); + } + // line 14: w[i] = w[i - Nk] XOR temp + temp ^= w[i - P::NK]; + w[i] = temp; + } + + // Rewrite in place into the compressed bit-sliced form, one 4-word round key at a time. + // Both interleaved blocks use the same key, so each round key is bit-sliced with the word + // duplicated into both halves; the two halves are then identical and one bit of each pair is + // redundant, so the even-position bits of the first word and the odd-position bits of the + // second are packed into a single stored word. + for base in (0..w.len()).step_by(4) { + let mut q: Planes = [0u32; 8]; + for j in 0..4 { + q[2 * j] = w[base + j]; + q[2 * j + 1] = w[base + j]; + } + ortho(&mut q); + for j in 0..4 { + // The two masks are complementary, so the operands are disjoint and `|` and `^` agree. + // That is why `cargo mutants` reports the `| -> ^` mutant here as surviving. + w[base + j] = (q[2 * j] & 0x5555_5555) | (q[2 * j + 1] & 0xAAAA_AAAA); + } + } + + schedule +} + +/// Re-doubles round key `round` of a compressed schedule into its eight-plane form. +/// +/// The inverse of the packing at the end of [`expand`]: the even-position bits are spread back +/// over both positions of each pair, and likewise the odd-position bits, giving the two identical +/// halves that [`crate::round::add_round_key`] expects. Eight words of stack, built fresh each +/// round rather than stored. +/// +/// Translated from BearSSL `aes_ct.c:br_aes_ct_skey_expand`. +#[inline(always)] +pub(crate) fn round_key(schedule: &P::Schedule, round: usize) -> Planes { + debug_assert!(round <= P::NR); + let w = schedule.as_ref(); + let mut sk: Planes = [0u32; 8]; + for j in 0..4 { + let packed = w[4 * round + j]; + let even = packed & 0x5555_5555; + let odd = packed & 0xAAAA_AAAA; + // `even` occupies only even bit positions and `even << 1` only odd ones (and vice versa + // for `odd`), so both spreads combine disjoint operands and `|` and `^` agree. Hence the + // two `| -> ^` mutants `cargo mutants` reports here as surviving. + sk[2 * j] = even | (even << 1); + sk[2 * j + 1] = odd | (odd >> 1); + } + sk +} + +#[cfg(test)] +mod tests { + use super::*; + + /// FIPS 197 Appendix A.1: every w[i] of the AES-128 key expansion, as printed + /// (i.e. the byte sequence [a0,a1,a2,a3] read left to right). + #[rustfmt::skip] + const APPENDIX_A1_WORDS: [u32; 44] = [ + 0x2b7e1516, 0x28aed2a6, 0xabf71588, 0x09cf4f3c, + 0xa0fafe17, 0x88542cb1, 0x23a33939, 0x2a6c7605, + 0xf2c295f2, 0x7a96b943, 0x5935807a, 0x7359f67f, + 0x3d80477d, 0x4716fe3e, 0x1e237e44, 0x6d7a883b, + 0xef44a541, 0xa8525b7f, 0xb671253b, 0xdb0bad00, + 0xd4d1c6f8, 0x7c839d87, 0xcaf2b8bc, 0x11f915bc, + 0x6d88a37a, 0x110b3efd, 0xdbf98641, 0xca0093fd, + 0x4e54f70e, 0x5f5fc9f3, 0x84a64fb2, 0x4ea6dc4f, + 0xead27321, 0xb58dbad2, 0x312bf560, 0x7f8d292f, + 0xac7766f3, 0x19fadc21, 0x28d12941, 0x575c006e, + 0xd014f9a8, 0xc9ee2589, 0xe13f0cc8, 0xb6630ca6, + ]; + + /// FIPS 197 Appendix A.2: every w[i] of the AES-192 key expansion, as printed. + #[rustfmt::skip] + const APPENDIX_A2_WORDS: [u32; 52] = [ + 0x8e73b0f7, 0xda0e6452, 0xc810f32b, 0x809079e5, + 0x62f8ead2, 0x522c6b7b, 0xfe0c91f7, 0x2402f5a5, + 0xec12068e, 0x6c827f6b, 0x0e7a95b9, 0x5c56fec2, + 0x4db7b4bd, 0x69b54118, 0x85a74796, 0xe92538fd, + 0xe75fad44, 0xbb095386, 0x485af057, 0x21efb14f, + 0xa448f6d9, 0x4d6dce24, 0xaa326360, 0x113b30e6, + 0xa25e7ed5, 0x83b1cf9a, 0x27f93943, 0x6a94f767, + 0xc0a69407, 0xd19da4e1, 0xec1786eb, 0x6fa64971, + 0x485f7032, 0x22cb8755, 0xe26d1352, 0x33f0b7b3, + 0x40beeb28, 0x2f18a259, 0x6747d26b, 0x458c553e, + 0xa7e1466c, 0x9411f1df, 0x821f750a, 0xad07d753, + 0xca400538, 0x8fcc5006, 0x282d166a, 0xbc3ce7b5, + 0xe98ba06f, 0x448c773c, 0x8ecc7204, 0x01002202, + ]; + + /// FIPS 197 Appendix A.3: every w[i] of the AES-256 key expansion, as printed. + #[rustfmt::skip] + const APPENDIX_A3_WORDS: [u32; 60] = [ + 0x603deb10, 0x15ca71be, 0x2b73aef0, 0x857d7781, + 0x1f352c07, 0x3b6108d7, 0x2d9810a3, 0x0914dff4, + 0x9ba35411, 0x8e6925af, 0xa51a8b5f, 0x2067fcde, + 0xa8b09c1a, 0x93d194cd, 0xbe49846e, 0xb75d5b9a, + 0xd59aecb8, 0x5bf3c917, 0xfee94248, 0xde8ebe96, + 0xb5a9328a, 0x2678a647, 0x98312229, 0x2f6c79b3, + 0x812c81ad, 0xdadf48ba, 0x24360af2, 0xfab8b464, + 0x98c5bfc9, 0xbebd198e, 0x268c3ba7, 0x09e04214, + 0x68007bac, 0xb2df3316, 0x96e939e4, 0x6c518d80, + 0xc814e204, 0x76a9fb8a, 0x5025c02d, 0x59c58239, + 0xde136967, 0x6ccc5a71, 0xfa256395, 0x9674ee15, + 0x5886ca5d, 0x2e2f31d7, 0x7e0af1fa, 0x27cf73c3, + 0x749c47ab, 0x18501dda, 0xe2757e4f, 0x7401905a, + 0xcafaaae3, 0xe4d59b34, 0x9adf6ace, 0xbd10190d, + 0xfe4890d1, 0xe6188d0b, 0x046df344, 0x706c631e, + ]; + + /// Recovers the classical `w[i]` from a stored schedule. + /// + /// [`round_key`] undoes the pair-compression, and [`ortho`] then undoes the bit-slicing, + /// leaving the duplicated pre-slicing words with `w[4*round + j]` in position `2j`. This is + /// what lets the Appendix A vectors test the real [`expand`] output rather than a + /// reimplementation of it. + fn classical_word(schedule: &P::Schedule, i: usize) -> u32 { + let mut q = round_key::

(schedule, i / 4); + ortho(&mut q); + let j = i % 4; + assert_eq!(q[2 * j], q[2 * j + 1], "both interleaved halves hold the same round key"); + q[2 * j] + } + + /// Compares a whole expansion against an Appendix A table. + /// + /// Appendix A prints a word as the byte sequence `[a0,a1,a2,a3]` left to right, so the + /// tabulated `u32` has `a0` in its *most* significant byte; words are held little-endian + /// here, so `swap_bytes` is the conversion. + fn assert_expansion_matches(key: &[u8], expected: &[u32], label: &str) { + let schedule = expand::

(key); + assert_eq!(expected.len(), 4 * (P::NR + 1), "{label}: table length"); + for (i, &want) in expected.iter().enumerate() { + let got = classical_word::

(&schedule, i).swap_bytes(); + assert_eq!(got, want, "{label}: w[{i}] should be {want:#010x}, got {got:#010x}"); + } + } + + #[test] + fn test_key_expansion_matches_fips197_appendix_a1() { + let key = [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, + 0x4f, 0x3c, + ]; + assert_expansion_matches::(&key, &APPENDIX_A1_WORDS, "Appendix A.1"); + } + + #[test] + fn test_key_expansion_matches_fips197_appendix_a2() { + let key = [ + 0x8e, 0x73, 0xb0, 0xf7, 0xda, 0x0e, 0x64, 0x52, 0xc8, 0x10, 0xf3, 0x2b, 0x80, 0x90, + 0x79, 0xe5, 0x62, 0xf8, 0xea, 0xd2, 0x52, 0x2c, 0x6b, 0x7b, + ]; + assert_expansion_matches::(&key, &APPENDIX_A2_WORDS, "Appendix A.2"); + } + + #[test] + fn test_key_expansion_matches_fips197_appendix_a3() { + let key = [ + 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, 0x7d, + 0x77, 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, + 0x09, 0x14, 0xdf, 0xf4, + ]; + assert_expansion_matches::(&key, &APPENDIX_A3_WORDS, "Appendix A.3"); + } + + #[test] + fn test_the_first_nk_schedule_words_are_the_key_itself() { + // Algorithm 2 lines 2-6, and a check that the expansion is reading the key + // little-endian consistently with how Appendix A prints it. + let key = [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, + 0x4f, 0x3c, + ]; + let schedule = expand::(&key); + for i in 0..AES128Params::NK { + let got = classical_word::(&schedule, i); + assert_eq!(got.to_le_bytes(), key[4 * i..4 * i + 4]); + } + } + + #[test] + fn test_rot_word_matches_equation_5_10() { + // FIPS 197 Eq 5.10 on the byte sequence [a0,a1,a2,a3] = [0x09,0xcf,0x4f,0x3c], which is + // the temp at i = 4 of Appendix A.1, whose ROTWORD() the appendix gives as cf4f3c09. + let word = u32::from_le_bytes([0x09, 0xcf, 0x4f, 0x3c]); + assert_eq!(rot_word(word).to_le_bytes(), [0xcf, 0x4f, 0x3c, 0x09]); + } + + #[test] + fn test_sub_word_matches_the_appendix_a1_example() { + // Appendix A.1, i = 4: "After ROTWORD()" is cf4f3c09 and "After SUBWORD()" is 8a84eb01. + // The appendix prints a word as the byte sequence [a0,a1,a2,a3]; words are held + // little-endian here, so `a0` is the low byte. + let after_rot = u32::from_le_bytes([0xcf, 0x4f, 0x3c, 0x09]); + assert_eq!(sub_word(after_rot).to_le_bytes(), [0x8a, 0x84, 0xeb, 0x01]); + } + + #[test] + fn test_sub_word_fills_every_plane() { + // The doc comment claims all eight planes end up holding SUBWORD(word); if that ever + // stopped being true, picking q[0] would be an arbitrary choice rather than a correct one. + let word = 0x1234_5678u32; + let mut q: Planes = [word; 8]; + ortho(&mut q); + sbox(&mut q); + ortho(&mut q); + assert!(q.iter().all(|&plane| plane == q[0])); + assert_eq!(q[0], sub_word(word)); + } + + #[test] + fn test_round_key_inverts_the_compression() { + // Round-tripping a known schedule: expand(), then round_key() for every round, and check + // the recovered planes match bit-slicing the classical words directly. + let key = [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, + 0x4f, 0x3c, + ]; + let schedule = expand::(&key); + + // Recompute the classical schedule without the compression step. + let mut w = [0u32; 44]; + for i in 0..4 { + w[i] = u32::from_le_bytes(key[4 * i..4 * i + 4].try_into().unwrap()); + } + let mut temp = w[3]; + for i in 4..44 { + if i % 4 == 0 { + temp = sub_word(rot_word(temp)) ^ Rcon[i / 4 - 1]; + } + temp ^= w[i - 4]; + w[i] = temp; + } + + for round in 0..=AES128Params::NR { + let got = round_key::(&schedule, round); + let mut expected: Planes = [0u32; 8]; + for j in 0..4 { + expected[2 * j] = w[4 * round + j]; + expected[2 * j + 1] = w[4 * round + j]; + } + ortho(&mut expected); + assert_eq!(got, expected, "round {round}"); + } + } + + #[test] + fn test_schedule_lengths_match_four_times_nr_plus_one() { + // FIPS 197 Sec 5.2: the schedule is 4 * (Nr + 1) words. The array types are written out + // by hand per parameter set, so this guards against a typo in one of them. + assert_eq!( + size_of::<::Schedule>() / 4, + 4 * (AES128Params::NR + 1) + ); + assert_eq!( + size_of::<::Schedule>() / 4, + 4 * (AES192Params::NR + 1) + ); + assert_eq!( + size_of::<::Schedule>() / 4, + 4 * (AES256Params::NR + 1) + ); + } + + #[test] + fn test_key_len_is_four_times_nk() { + // FIPS 197 §5, Table 3 ties the two together; both are declared independently above. + assert_eq!(AES128Params::KEY_LEN, 4 * AES128Params::NK); + assert_eq!(AES192Params::KEY_LEN, 4 * AES192Params::NK); + assert_eq!(AES256Params::KEY_LEN, 4 * AES256Params::NK); + } + + #[test] + fn test_rcon_table_5_values() { + // FIPS 197 Sec 5.2: "for j > 0, these bytes may be generated by successively applying + // XTIMES() to the byte represented by x^(j-1)". Derive the table and compare, so a typo + // in the transcription of Table 5 shows up here. + let mut expected = [0u32; 10]; + let mut v: u8 = 0x01; + for slot in expected.iter_mut() { + *slot = u32::from(v); + v = (v << 1) ^ if v & 0x80 != 0 { 0x1b } else { 0 }; + } + assert_eq!(Rcon, expected); + // Spot-check the two values from Table 5 that are not plain powers of two. + assert_eq!(Rcon[8], 0x1b); + assert_eq!(Rcon[9], 0x36); + } +} diff --git a/crypto/aes-lowmemory/tests/bc-test-data.rs b/crypto/aes-lowmemory/tests/bc-test-data.rs new file mode 100644 index 00000000..a5e26555 --- /dev/null +++ b/crypto/aes-lowmemory/tests/bc-test-data.rs @@ -0,0 +1,266 @@ +//! Known-answer tests against the NIST ACVP `ACVP-AES-ECB` vectors from the `bc-test-data` repo. +//! +//! Requires `bc-test-data` to be cloned alongside this repository, i.e. at `../bc-test-data` +//! relative to the root of this git project. If it is absent the tests print a warning and pass, +//! matching the convention used by the ML-KEM and ML-DSA test suites -- `cargo test` must stay +//! green for someone who has only cloned this repository. +//! +//! # Why ACVP ECB vectors +//! +//! ECB applies the raw permutation to each block independently, so an ECB test vector *is* a +//! block-permutation test vector -- which is the only reason ECB is mentioned in this crate. See +//! the crate docs on why you must never use ECB to encrypt data. +//! +//! The response file records `key`, `pt` and `ct` for every test case regardless of the group's +//! declared direction, so each case is checked in **both** directions: encrypting `pt` must give +//! `ct` and decrypting `ct` must give `pt`. That is strictly stronger than honouring the declared +//! direction, and it means the group metadata in the request file is not needed. +//! +//! # Coverage and one gap +//! +//! The AFT (Algorithm Functional Test) groups cover all three key lengths in both directions, +//! including cases whose plaintext spans several blocks. The six MCT (Monte Carlo Test) groups +//! are **not** implemented: their expected output is a `resultsArray` produced by a chained +//! key/plaintext update rule defined in the ACVP AES specification rather than in FIPS 197, and +//! implementing it from anything other than that specification would be guesswork. The test +//! reports how many it skipped so the gap is visible rather than silent. + +use bouncycastle_aes_lowmemory::{AES_128, AES_192, AES_256, BLOCK_LEN}; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::traits::SecurityStrength; +use bouncycastle_hex as hex; +use serde_json::Value; +use std::fs; +use std::path::{Path, PathBuf}; + +/// Candidate locations, covering `cargo test` run from the crate root or from the repo root. +const TEST_DATA_PATHS: [&str; 2] = [ + "../../../bc-test-data/crypto/aes_tdes_vectors/AES", + "../bc-test-data/crypto/aes_tdes_vectors/AES", +]; + +const RESPONSE_FILE: &str = "ACVP-AES-ECB.4014527.rsp.json"; + +/// Locates the bc-test-data AES directory, or `None` if `bc-test-data` is not checked out. +fn test_data_dir() -> Option { + for candidate in TEST_DATA_PATHS { + let path = Path::new(candidate); + if path.join(RESPONSE_FILE).exists() { + return Some(path.to_path_buf()); + } + } + println!( + "WARNING: bc-test-data not found (looked in {TEST_DATA_PATHS:?}); \ + ACVP AES-ECB tests will be skipped" + ); + None +} + +/// Builds a `KeyMaterial` from raw ACVP key bytes, including the all-zero keys. +/// +/// The ACVP set deliberately includes an all-zero key (the GFSbox-style groups vary only the +/// plaintext under a zero key). `KeyMaterial` tags an all-zero buffer as [`KeyType::Zeroized`] +/// and will not promote it outside a [`do_hazardous_operations`] closure, which is the right +/// default -- an all-zero key normally means a broken RNG, and `Aes128::new` rejecting it is +/// tested in `fips197_tests.rs`. Here the zero key is deliberate and comes from NIST, so this +/// opts in explicitly rather than the library weakening its guard. +fn cipher_key(bytes: &[u8]) -> KeyMaterial { + assert_eq!(bytes.len(), N, "key length should match the parameter set"); + let mut key = KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) + .expect("ACVP key bytes fit the buffer"); + + if key.key_type() != KeyType::SymmetricCipherKey { + do_hazardous_operations(&mut key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::from_bytes(N)) + }) + .expect("promoting a NIST all-zero test key"); + } + + key +} + +/// A single-block transformation, resolved once per test case rather than per block. +type BlockTransform = Box; + +/// Encrypts or decrypts `data` block by block, i.e. ECB, dispatching on the key length. +fn ecb(key: &[u8], data: &[u8], encrypt: bool) -> Vec { + assert_eq!(data.len() % BLOCK_LEN, 0, "ACVP ECB data must be block-aligned"); + + let transform: BlockTransform = match key.len() { + 16 => { + let km = cipher_key::<16>(key); + let aes = AES_128::new(&km).expect("valid AES-128 key"); + if encrypt { + Box::new(move |b| aes.encrypt_block(b)) + } else { + Box::new(move |b| aes.decrypt_block(b)) + } + } + 24 => { + let km = cipher_key::<24>(key); + let aes = AES_192::new(&km).expect("valid AES-192 key"); + if encrypt { + Box::new(move |b| aes.encrypt_block(b)) + } else { + Box::new(move |b| aes.decrypt_block(b)) + } + } + 32 => { + let km = cipher_key::<32>(key); + let aes = AES_256::new(&km).expect("valid AES-256 key"); + if encrypt { + Box::new(move |b| aes.encrypt_block(b)) + } else { + Box::new(move |b| aes.decrypt_block(b)) + } + } + other => panic!("ACVP AES vectors should only use 16, 24 or 32 byte keys, got {other}"), + }; + + let mut out = Vec::with_capacity(data.len()); + for chunk in data.chunks(BLOCK_LEN) { + // Cannot fail: the length is asserted block-aligned above. + let mut block: [u8; BLOCK_LEN] = chunk.try_into().unwrap(); + transform(&mut block); + out.extend_from_slice(&block); + } + out +} + +/// The same, using the two-block entry points where a pair is available. +fn ecb_pairwise(key: &[u8], data: &[u8], encrypt: bool) -> Vec { + assert_eq!(data.len() % BLOCK_LEN, 0, "ACVP ECB data must be block-aligned"); + let mut blocks: Vec<[u8; BLOCK_LEN]> = + data.chunks(BLOCK_LEN).map(|c| c.try_into().unwrap()).collect(); + + match key.len() { + 16 => { + let km = cipher_key::<16>(key); + let aes = AES_128::new(&km).unwrap(); + run_pairwise(&mut blocks, encrypt, |p, e| { + if e { aes.encrypt_2blocks(p) } else { aes.decrypt_2blocks(p) } + }); + } + 24 => { + let km = cipher_key::<24>(key); + let aes = AES_192::new(&km).unwrap(); + run_pairwise(&mut blocks, encrypt, |p, e| { + if e { aes.encrypt_2blocks(p) } else { aes.decrypt_2blocks(p) } + }); + } + 32 => { + let km = cipher_key::<32>(key); + let aes = AES_256::new(&km).unwrap(); + run_pairwise(&mut blocks, encrypt, |p, e| { + if e { aes.encrypt_2blocks(p) } else { aes.decrypt_2blocks(p) } + }); + } + other => panic!("ACVP AES vectors should only use 16, 24 or 32 byte keys, got {other}"), + } + + blocks.concat() +} + +/// Walks `blocks` two at a time, leaving a trailing odd block to a duplicated pair. +fn run_pairwise( + blocks: &mut [[u8; BLOCK_LEN]], + encrypt: bool, + transform: impl Fn(&mut [[u8; BLOCK_LEN]; 2], bool), +) { + let mut chunks = blocks.chunks_exact_mut(2); + for pair in &mut chunks { + // Cannot fail: `chunks_exact_mut(2)` yields slices of length 2. + let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap(); + transform(pair, encrypt); + } + // An odd trailing block still has to go through the two-block path. + if let [last] = chunks.into_remainder() { + let mut pair = [*last, *last]; + transform(&mut pair, encrypt); + *last = pair[0]; + } +} + +#[test] +fn acvp_aes_ecb_known_answer_tests() { + let Some(dir) = test_data_dir() else { return }; + + let contents = fs::read_to_string(dir.join(RESPONSE_FILE)).expect("readable response file"); + let parsed: Value = serde_json::from_str(&contents).expect("valid ACVP JSON"); + + // The ACVP file is an array: element 0 is the version header, element 1 the vector set. + let groups = parsed + .get(1) + .and_then(|set| set.get("testGroups")) + .and_then(Value::as_array) + .expect("testGroups array"); + + let mut checked = 0usize; + let mut skipped_mct = 0usize; + let mut by_key_len = [0usize; 3]; // 128, 192, 256 + + for group in groups { + let tests = group.get("tests").and_then(Value::as_array).expect("tests array"); + for test in tests { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + + // Monte Carlo groups carry a chained resultsArray instead of a single pt/ct pair. + if test.get("resultsArray").is_some() { + skipped_mct += 1; + continue; + } + + let get = |name: &str| -> Vec { + let s = test + .get(name) + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("tcId {tc_id}: missing field {name}")); + hex::decode(s).unwrap_or_else(|_| panic!("tcId {tc_id}: bad hex in {name}")) + }; + + let key = get("key"); + let pt = get("pt"); + let ct = get("ct"); + + assert_eq!(pt.len(), ct.len(), "tcId {tc_id}: pt and ct differ in length"); + + assert_eq!(ecb(&key, &pt, true), ct, "tcId {tc_id}: AES-{} encrypt", key.len() * 8); + assert_eq!(ecb(&key, &ct, false), pt, "tcId {tc_id}: AES-{} decrypt", key.len() * 8); + + // The two-block path must agree with the single-block path on real vectors too. + assert_eq!( + ecb_pairwise(&key, &pt, true), + ct, + "tcId {tc_id}: AES-{} encrypt via encrypt_blocks2", + key.len() * 8 + ); + assert_eq!( + ecb_pairwise(&key, &ct, false), + pt, + "tcId {tc_id}: AES-{} decrypt via decrypt_blocks2", + key.len() * 8 + ); + + by_key_len[match key.len() { + 16 => 0, + 24 => 1, + _ => 2, + }] += 1; + checked += 1; + } + } + + println!( + "ACVP AES-ECB: {checked} test cases checked in both directions \ + (AES-128: {}, AES-192: {}, AES-256: {}); {skipped_mct} MCT cases skipped", + by_key_len[0], by_key_len[1], by_key_len[2] + ); + + // Guard against a silently-empty run: the published vector set has thousands of AFT cases + // across all three key lengths. + assert!(checked > 1000, "expected the full ACVP AFT set, only checked {checked}"); + assert!(by_key_len.iter().all(|&n| n > 0), "every key length should be covered"); +} diff --git a/crypto/aes-lowmemory/tests/fips197_tests.rs b/crypto/aes-lowmemory/tests/fips197_tests.rs new file mode 100644 index 00000000..2dd84e92 --- /dev/null +++ b/crypto/aes-lowmemory/tests/fips197_tests.rs @@ -0,0 +1,230 @@ +//! Known-answer tests from NIST FIPS 197 itself. +//! +//! Appendix B -- the worked single-block AES-128 encryption -- plus its inverse, the two-block +//! path, and key-handling behaviour. +//! +//! The Appendix A key expansions are **not** tested here. The key schedule is deliberately not +//! public API (it is a `Secret` field), and a round-trip through the cipher cannot check it: a +//! wrong `w[i]` is used by encryption and decryption alike, so the round trip still succeeds. +//! Every word of all three expansions is instead checked against Appendix A inside +//! `src/schedule.rs`, where the stored schedule can be decompressed and compared directly. +//! +//! Known-answer coverage for AES-192 and AES-256, which Appendix B does not reach, is in +//! `sp800_38a_tests.rs` and `acvp_tests.rs`. +//! +//! All values here are transcribed from the published FIPS 197 (Update 1) PDF. + +use bouncycastle_aes_lowmemory::{AES_128, AES_192, AES_256}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::SecurityStrength; + +/// Appendix A.1 / Appendix B key: `2b7e151628aed2a6abf7158809cf4f3c`. +const KEY_128: [u8; 16] = [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, +]; + +/// Appendix A.2 key: `8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b`. +const KEY_192: [u8; 24] = [ + 0x8e, 0x73, 0xb0, 0xf7, 0xda, 0x0e, 0x64, 0x52, 0xc8, 0x10, 0xf3, 0x2b, 0x80, 0x90, 0x79, 0xe5, + 0x62, 0xf8, 0xea, 0xd2, 0x52, 0x2c, 0x6b, 0x7b, +]; + +/// Appendix A.3 key: +/// `603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4`. +const KEY_256: [u8; 32] = [ + 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, 0x7d, 0x77, 0x81, + 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, 0x09, 0x14, 0xdf, 0xf4, +]; + +fn key_material(bytes: &[u8; N]) -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +#[test] +fn appendix_b_encrypts_the_documented_block() { + // Appendix B: Input = 32 43 f6 a8 88 5a 30 8d 31 31 98 a2 e0 37 07 34 + // Key = 2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c + // The final state printed as "output" reads, column by column (Eq 3.7): + // 39 25 84 1d 02 dc 09 fb dc 11 85 97 19 6a 0b 32 + let aes = AES_128::new(&key_material(&KEY_128)).unwrap(); + + let mut block = [ + 0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, + 0x34, + ]; + aes.encrypt_block(&mut block); + assert_eq!( + block, + [ + 0x39, 0x25, 0x84, 0x1d, 0x02, 0xdc, 0x09, 0xfb, 0xdc, 0x11, 0x85, 0x97, 0x19, 0x6a, + 0x0b, 0x32 + ] + ); +} + +#[test] +fn appendix_b_decrypts_back_to_the_documented_input() { + let aes = AES_128::new(&key_material(&KEY_128)).unwrap(); + + let mut block = [ + 0x39, 0x25, 0x84, 0x1d, 0x02, 0xdc, 0x09, 0xfb, 0xdc, 0x11, 0x85, 0x97, 0x19, 0x6a, 0x0b, + 0x32, + ]; + aes.decrypt_block(&mut block); + assert_eq!( + block, + [ + 0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, + 0x07, 0x34 + ] + ); +} + +#[test] +fn appendix_b_two_block_path_agrees_with_the_single_block_path() { + let aes = AES_128::new(&key_material(&KEY_128)).unwrap(); + let input = [ + 0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, + 0x34, + ]; + let expected = [ + 0x39, 0x25, 0x84, 0x1d, 0x02, 0xdc, 0x09, 0xfb, 0xdc, 0x11, 0x85, 0x97, 0x19, 0x6a, 0x0b, + 0x32, + ]; + + // Pairing the Appendix B block with an unrelated one must not disturb either half. + let other = [0xAAu8; 16]; + let mut other_alone = other; + aes.encrypt_block(&mut other_alone); + + let mut pair = [input, other]; + aes.encrypt_2blocks(&mut pair); + assert_eq!(pair[0], expected); + assert_eq!(pair[1], other_alone); + + // ...and in the other slot, which is a different bit position in the interleave. + let mut pair = [other, input]; + aes.encrypt_2blocks(&mut pair); + assert_eq!(pair[0], other_alone); + assert_eq!(pair[1], expected); +} + +/// Encryption and decryption are inverses, under each Appendix A key. +/// +/// This checks `decrypt_block` really inverts `encrypt_block` from the same stored schedule, +/// which is the load-bearing claim of following FIPS 197 Algorithm 3 rather than Sec 5.3.5. It +/// deliberately makes no claim about the schedule being *correct* -- see the module docs. +#[test] +fn encryption_and_decryption_are_inverses_for_all_three_key_lengths() { + let aes128 = AES_128::new(&key_material(&KEY_128)).unwrap(); + let aes192 = AES_192::new(&key_material(&KEY_192)).unwrap(); + let aes256 = AES_256::new(&key_material(&KEY_256)).unwrap(); + + for block in [[0u8; 16], [0xFFu8; 16], core::array::from_fn(|i| i as u8)] { + let mut b = block; + aes128.encrypt_block(&mut b); + assert_ne!(b, block, "AES-128 must actually transform the block"); + aes128.decrypt_block(&mut b); + assert_eq!(b, block, "AES-128 round trip with the Appendix A.1 key"); + + let mut b = block; + aes192.encrypt_block(&mut b); + assert_ne!(b, block, "AES-192 must actually transform the block"); + aes192.decrypt_block(&mut b); + assert_eq!(b, block, "AES-192 round trip with the Appendix A.2 key"); + + let mut b = block; + aes256.encrypt_block(&mut b); + assert_ne!(b, block, "AES-256 must actually transform the block"); + aes256.decrypt_block(&mut b); + assert_eq!(b, block, "AES-256 round trip with the Appendix A.3 key"); + } +} + +/// The three key lengths must give different results for the same input. +/// +/// Guards against a parameter set silently using another set's `Nr` or `Nk`. +#[test] +fn the_three_key_lengths_are_distinct_permutations() { + // A key whose first 16 bytes are shared, so only Nk/Nr and the extra key bytes differ. + let shared = [0x11u8; 32]; + let aes128 = AES_128::new(&key_material::<16>(&shared[..16].try_into().unwrap())).unwrap(); + let aes192 = AES_192::new(&key_material::<24>(&shared[..24].try_into().unwrap())).unwrap(); + let aes256 = AES_256::new(&key_material(&shared)).unwrap(); + + let block = [0x42u8; 16]; + let mut b128 = block; + let mut b192 = block; + let mut b256 = block; + aes128.encrypt_block(&mut b128); + aes192.encrypt_block(&mut b192); + aes256.encrypt_block(&mut b256); + + assert_ne!(b128, b192); + assert_ne!(b192, b256); + assert_ne!(b128, b256); +} + +// ---- key handling ----------------------------------------------------------------------- + +#[test] +fn a_key_of_the_wrong_type_is_rejected() { + // KeyType::Seed is not a cipher key: a seed reused directly as an AES key is a real mistake + // and the type system tracks enough to catch it. + let key = KeyMaterial::<16>::from_bytes_as_type(&[0x01; 16], KeyType::Seed).unwrap(); + assert!(AES_128::new(&key).is_err()); + + let key = KeyMaterial::<16>::from_bytes_as_type(&[0x01; 16], KeyType::MACKey).unwrap(); + assert!(AES_128::new(&key).is_err()); +} + +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + // The capacity is right but only part of it is populated, so `key_len()` disagrees with the + // parameter set. This is the one length error the const generic cannot catch by itself. + let key = + KeyMaterial::<32>::from_bytes_as_type(&[0x01; 16], KeyType::SymmetricCipherKey).unwrap(); + assert!(AES_256::new(&key).is_err()); +} + +#[test] +fn a_key_carrying_too_low_a_security_strength_is_rejected() { + // A full-length key whose material was only ever derived at a lower security strength must + // not be usable at the strength its length implies. `from_bytes_as_type` tags a 32-byte key + // as 256-bit, so lower it deliberately -- lowering does not need a hazardous closure, only + // raising does. + let mut key = + KeyMaterial::<32>::from_bytes_as_type(&[0x01; 32], KeyType::SymmetricCipherKey).unwrap(); + assert_eq!(key.security_strength(), SecurityStrength::_256bit); + + key.set_security_strength(SecurityStrength::_128bit).unwrap(); + assert!( + AES_256::new(&key).is_err(), + "AES-256 must reject a 32-byte key only derived at the 128-bit strength" + ); + + // The same key at its full strength is fine, so the rejection is about the strength tag and + // not about anything else having gone wrong with the key. + let good = + KeyMaterial::<32>::from_bytes_as_type(&[0x01; 32], KeyType::SymmetricCipherKey).unwrap(); + assert!(AES_256::new(&good).is_ok()); +} + +#[test] +fn a_correctly_typed_key_of_each_length_is_accepted() { + assert!(AES_128::new(&key_material(&KEY_128)).is_ok()); + assert!(AES_192::new(&key_material(&KEY_192)).is_ok()); + assert!(AES_256::new(&key_material(&KEY_256)).is_ok()); +} + +#[test] +fn debug_does_not_print_the_key_schedule() { + // The schedule is secret; `Debug` must not be a way to leak it. + let aes = AES_128::new(&key_material(&KEY_128)).unwrap(); + let rendered = format!("{aes:?}"); + assert_eq!(rendered, "AES-128"); + // No byte of the key should appear as hex in the output. + assert!(!rendered.contains("2b")); + assert!(!rendered.contains("7e")); +} diff --git a/crypto/aes-lowmemory/tests/sp800_38a_tests.rs b/crypto/aes-lowmemory/tests/sp800_38a_tests.rs new file mode 100644 index 00000000..5629c64f --- /dev/null +++ b/crypto/aes-lowmemory/tests/sp800_38a_tests.rs @@ -0,0 +1,176 @@ +//! Known-answer tests from NIST SP 800-38A Appendix F.1, "ECB Example Vectors". +//! +//! These are the only NIST-published known-answer vectors for AES-192 and AES-256 that live in a +//! specification document rather than a separate vector file -- FIPS 197 Appendix B only covers +//! AES-128, and FIPS 197 (Update 1) removed the Appendix C example vectors in favour of a pointer +//! to the CSRC website. `acvp_tests.rs` covers far more cases, but only when the `bc-test-data` +//! repository is present, so these vectors are the always-available known-answer floor. +//! +//! ECB applies the raw permutation to each block independently, so an ECB example vector *is* a +//! block-permutation test vector. (That is the only reason ECB appears in this crate; see the +//! crate docs on why you must not use it to encrypt anything.) +//! +//! The keys are the same three keys as FIPS 197 Appendix A.1, A.2 and A.3, so these vectors also +//! pin each key expansion against a NIST-published answer, in both directions. +//! +//! Transcribed from the published SP 800-38A PDF, sections F.1.1 through F.1.6. + +use bouncycastle_aes_lowmemory::{AES_128, AES_192, AES_256, BLOCK_LEN}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_hex as hex; + +/// The four plaintext blocks shared by every F.1 subsection. +const PLAINTEXTS: [&str; 4] = [ + "6bc1bee22e409f96e93d7e117393172a", + "ae2d8a571e03ac9c9eb76fac45af8e51", + "30c81c46a35ce411e5fbc1191a0a52ef", + "f69f2445df4f9b17ad2b417be66c3710", +]; + +/// F.1.1 / F.1.2 key. +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +/// F.1.1 ECB-AES128.Encrypt output blocks. +const CIPHERTEXTS_128: [&str; 4] = [ + "3ad77bb40d7a3660a89ecaf32466ef97", + "f5d3d58503b9699de785895a96fdbaaf", + "43b1cd7f598ece23881b00e3ed030688", + "7b0c785e27e8ad3f8223207104725dd4", +]; + +/// F.1.3 / F.1.4 key. +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +/// F.1.3 ECB-AES192.Encrypt output blocks. +const CIPHERTEXTS_192: [&str; 4] = [ + "bd334f1d6e45f25ff712a214571fa5cc", + "974104846d0ad3ad7734ecb3ecee4eef", + "ef7afd2270e2e60adce0ba2face6444e", + "9a4b41ba738d6c72fb16691603c18e0e", +]; + +/// F.1.5 / F.1.6 key. +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; +/// F.1.5 ECB-AES256.Encrypt output blocks. +const CIPHERTEXTS_256: [&str; 4] = [ + "f3eed1bdb5d2a03c064b5a7e3db181f8", + "591ccb10d410ed26dc5ba74a31362870", + "b6ed21b99ca6f4f9f153e7b1beafed1d", + "23304b7a39f9f3ff067d8d8f9e24ecc7", +]; + +fn block(hex_str: &str) -> [u8; BLOCK_LEN] { + hex::decode(hex_str).expect("valid hex").try_into().expect("16 bytes") +} + +fn key_material(hex_str: &str) -> KeyMaterial { + let bytes = hex::decode(hex_str).expect("valid hex"); + assert_eq!(bytes.len(), N, "key length"); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +// ---- F.1.1 / F.1.2 ECB-AES128 ------------------------------------------------------------- + +#[test] +fn f_1_1_ecb_aes128_encrypt() { + let aes = AES_128::new(&key_material::<16>(KEY_128)).unwrap(); + for (i, (pt, ct)) in PLAINTEXTS.iter().zip(CIPHERTEXTS_128.iter()).enumerate() { + let mut b = block(pt); + aes.encrypt_block(&mut b); + assert_eq!(b, block(ct), "F.1.1 block #{}", i + 1); + } +} + +#[test] +fn f_1_2_ecb_aes128_decrypt() { + let aes = AES_128::new(&key_material::<16>(KEY_128)).unwrap(); + for (i, (pt, ct)) in PLAINTEXTS.iter().zip(CIPHERTEXTS_128.iter()).enumerate() { + let mut b = block(ct); + aes.decrypt_block(&mut b); + assert_eq!(b, block(pt), "F.1.2 block #{}", i + 1); + } +} + +// ---- F.1.3 / F.1.4 ECB-AES192 ------------------------------------------------------------- + +#[test] +fn f_1_3_ecb_aes192_encrypt() { + let aes = AES_192::new(&key_material::<24>(KEY_192)).unwrap(); + for (i, (pt, ct)) in PLAINTEXTS.iter().zip(CIPHERTEXTS_192.iter()).enumerate() { + let mut b = block(pt); + aes.encrypt_block(&mut b); + assert_eq!(b, block(ct), "F.1.3 block #{}", i + 1); + } +} + +#[test] +fn f_1_4_ecb_aes192_decrypt() { + let aes = AES_192::new(&key_material::<24>(KEY_192)).unwrap(); + for (i, (pt, ct)) in PLAINTEXTS.iter().zip(CIPHERTEXTS_192.iter()).enumerate() { + let mut b = block(ct); + aes.decrypt_block(&mut b); + assert_eq!(b, block(pt), "F.1.4 block #{}", i + 1); + } +} + +// ---- F.1.5 / F.1.6 ECB-AES256 ------------------------------------------------------------- + +#[test] +fn f_1_5_ecb_aes256_encrypt() { + let aes = AES_256::new(&key_material::<32>(KEY_256)).unwrap(); + for (i, (pt, ct)) in PLAINTEXTS.iter().zip(CIPHERTEXTS_256.iter()).enumerate() { + let mut b = block(pt); + aes.encrypt_block(&mut b); + assert_eq!(b, block(ct), "F.1.5 block #{}", i + 1); + } +} + +#[test] +fn f_1_6_ecb_aes256_decrypt() { + let aes = AES_256::new(&key_material::<32>(KEY_256)).unwrap(); + for (i, (pt, ct)) in PLAINTEXTS.iter().zip(CIPHERTEXTS_256.iter()).enumerate() { + let mut b = block(ct); + aes.decrypt_block(&mut b); + assert_eq!(b, block(pt), "F.1.6 block #{}", i + 1); + } +} + +// ---- the two-block path against the same vectors ------------------------------------------- + +/// The two-block entry points must produce exactly the single-block answers. +/// +/// This is the test that pins the interleave: a mistake in which bit of each pair belongs to +/// which block shows up here and nowhere in the single-block tests, because a single-block call +/// puts the same data in both halves. +#[test] +fn two_block_path_matches_the_f_1_vectors() { + let aes = AES_128::new(&key_material::<16>(KEY_128)).unwrap(); + + // Blocks 1 and 2 as a pair, then 3 and 4. + for chunk in 0..2 { + let (i, j) = (chunk * 2, chunk * 2 + 1); + let mut pair = [block(PLAINTEXTS[i]), block(PLAINTEXTS[j])]; + aes.encrypt_2blocks(&mut pair); + assert_eq!(pair[0], block(CIPHERTEXTS_128[i]), "pair {chunk} slot 0"); + assert_eq!(pair[1], block(CIPHERTEXTS_128[j]), "pair {chunk} slot 1"); + + aes.decrypt_2blocks(&mut pair); + assert_eq!(pair[0], block(PLAINTEXTS[i])); + assert_eq!(pair[1], block(PLAINTEXTS[j])); + } +} + +/// Swapping the two slots must swap the two results, and nothing else. +#[test] +fn two_block_path_is_slot_symmetric() { + let aes = AES_256::new(&key_material::<32>(KEY_256)).unwrap(); + + let mut forward = [block(PLAINTEXTS[0]), block(PLAINTEXTS[1])]; + let mut reversed = [block(PLAINTEXTS[1]), block(PLAINTEXTS[0])]; + aes.encrypt_2blocks(&mut forward); + aes.encrypt_2blocks(&mut reversed); + + assert_eq!(forward[0], reversed[1]); + assert_eq!(forward[1], reversed[0]); + assert_eq!(forward[0], block(CIPHERTEXTS_256[0])); + assert_eq!(forward[1], block(CIPHERTEXTS_256[1])); +} diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index 57fc0ee1..6e1c8534 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -6,7 +6,8 @@ use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; use bouncycastle_core::traits::{ - AEADCipher, BlockCipher, SecurityStrength, StreamCipher, SymmetricCipher, + AEADCipher, BlockCipherDecryptor, BlockCipherEncryptor, SecurityStrength, StreamCipher, + SymmetricCipher, }; /// Instance of the test framework. @@ -124,7 +125,8 @@ impl TestFrameworkBlockCipher { const KEY_LEN: usize, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize, - C: BlockCipher, + E: BlockCipherEncryptor, + D: BlockCipherDecryptor, >( &self, ) { @@ -135,42 +137,89 @@ impl TestFrameworkBlockCipher { .unwrap(); // to test blocks, we'll chunk our dummy seed - let (mut encryptor, iv) = C::do_encrypt_init(&key).unwrap(); - let mut decryptor = C::do_decrypt_init(&key, &iv).unwrap(); + let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); + let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); + // one block at a time (N = 1) for msg_chunk in DUMMY_SEED.as_chunks::().0.iter() { - let ct = encryptor.do_encrypt_block(msg_chunk).unwrap(); - let pt = decryptor.do_decrypt_block(&ct).unwrap(); + let ct = encryptor.do_encrypt_blocks(&[*msg_chunk]).unwrap(); + let [pt] = decryptor.do_decrypt_blocks(&ct).unwrap(); assert_eq!(msg_chunk, &pt); } // do it again using the _out versions - let (mut encryptor, iv) = C::do_encrypt_init(&key).unwrap(); - let mut decryptor = C::do_decrypt_init(&key, &iv).unwrap(); + let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); + let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); - let mut ct = [0u8; BLOCK_LEN]; - let mut pt = [0u8; BLOCK_LEN]; + let mut ct = [[0u8; BLOCK_LEN]; 1]; + let mut pt = [[0u8; BLOCK_LEN]; 1]; for msg_chunk in DUMMY_SEED.as_chunks::().0.iter() { - let ct_bytes_written = encryptor.do_encrypt_block_out(msg_chunk, &mut ct).unwrap(); + let ct_bytes_written = encryptor.do_encrypt_blocks_out(&[*msg_chunk], &mut ct).unwrap(); assert_eq!(ct_bytes_written, BLOCK_LEN); - let pt_bytes_written = decryptor.do_decrypt_block_out(&ct, &mut pt).unwrap(); + let pt_bytes_written = decryptor.do_decrypt_blocks_out(&ct, &mut pt).unwrap(); assert_eq!(pt_bytes_written, BLOCK_LEN); - assert_eq!(msg_chunk, &pt); + assert_eq!(msg_chunk, &pt[0]); } + // multi-block (N = 2): blocks encrypted together must decrypt both together and one at a time, + // and blocks encrypted one at a time must decrypt together. + let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); + let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); + + let mut ct = [[0u8; BLOCK_LEN]; 2]; + let mut pt = [[0u8; BLOCK_LEN]; 2]; + for msg_pair in DUMMY_SEED.as_chunks::().0.as_chunks::<2>().0.iter() { + // encrypt together, decrypt together (by value) + let ct_by_value = encryptor.do_encrypt_blocks(msg_pair).unwrap(); + let pt_by_value = decryptor.do_decrypt_blocks(&ct_by_value).unwrap(); + assert_eq!(msg_pair, &pt_by_value); + + // encrypt together (_out), decrypt one at a time + let ct_bytes_written = encryptor.do_encrypt_blocks_out(msg_pair, &mut ct).unwrap(); + assert_eq!(ct_bytes_written, 2 * BLOCK_LEN); + for (msg_chunk, ct_chunk) in msg_pair.iter().zip(ct.iter()) { + let [pt] = decryptor.do_decrypt_blocks(&[*ct_chunk]).unwrap(); + assert_eq!(msg_chunk, &pt); + } + + // encrypt one at a time, decrypt together (_out) + for (msg_chunk, ct_chunk) in msg_pair.iter().zip(ct.iter_mut()) { + let [c] = encryptor.do_encrypt_blocks(&[*msg_chunk]).unwrap(); + *ct_chunk = c; + } + let pt_bytes_written = decryptor.do_decrypt_blocks_out(&ct, &mut pt).unwrap(); + assert_eq!(pt_bytes_written, 2 * BLOCK_LEN); + assert_eq!(msg_pair, &pt); + } + + // one-shot API: must agree with the streaming API for the same key, and round-trip + let two_blocks: &[[u8; BLOCK_LEN]; 2] = + &DUMMY_SEED.as_chunks::().0.as_chunks::<2>().0[0]; + let (iv, ct) = E::encrypt_blocks(&key, two_blocks).unwrap(); + assert_eq!(D::decrypt_blocks(&key, &iv, &ct).unwrap(), *two_blocks); + let mut streamed = D::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(streamed.do_decrypt_blocks(&ct).unwrap(), *two_blocks); + + let mut ct = [[0u8; BLOCK_LEN]; 2]; + let mut pt = [[0u8; BLOCK_LEN]; 2]; + let (iv, n) = E::encrypt_blocks_out(&key, two_blocks, &mut ct).unwrap(); + assert_eq!(n, 2 * BLOCK_LEN); + assert_eq!(D::decrypt_blocks_out(&key, &iv, &ct, &mut pt).unwrap(), 2 * BLOCK_LEN); + assert_eq!(pt, *two_blocks); + // test that the iv is random (ie not the same on two runs) - let (_encryptor, iv1) = C::do_encrypt_init(&key).unwrap(); - let (_encryptor, iv2) = C::do_encrypt_init(&key).unwrap(); + let (_encryptor, iv1) = E::do_encrypt_init(&key).unwrap(); + let (_encryptor, iv2) = E::do_encrypt_init(&key).unwrap(); assert_ne!(iv1, iv2); // error case: KeyMaterial of wrong type let mac_key = KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], KeyType::MACKey) .unwrap(); - match C::do_encrypt_init(&mac_key) { + match E::do_encrypt_init(&mac_key) { Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } _ => panic!("Unexpected error"), }; @@ -194,15 +243,15 @@ impl TestFrameworkBlockCipher { // (and bypasses the key-length guard) without complaining. do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); - match C::do_encrypt_init(&key) { + match E::do_encrypt_init(&key) { Ok(_) => { - if ss >= &C::MAX_SECURITY_STRENGTH { /* good */ + if ss >= &E::MAX_SECURITY_STRENGTH { /* good */ } else { panic!("Should have been a strong enough key"); } } Err(SymmetricCipherError::KeyMaterialError(_)) => { - if ss < &C::MAX_SECURITY_STRENGTH { /* good */ + if ss < &E::MAX_SECURITY_STRENGTH { /* good */ } else { panic!("Should not have accepted a key weaker than algorithm"); } diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index f839a27e..495eb1fc 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -80,73 +80,138 @@ pub trait SymmetricCipher: Alg ) -> Result; } -/// The basic functions of a block cipher. +/// Metadata shared by [`BlockCipherEncryptor`] and [`BlockCipherDecryptor`]. +pub trait BlockCipher { + /// Maximum security strength supported by the algorithm; keys tagged with a lower strength are + /// rejected by the `_init` constructors. + const MAX_SECURITY_STRENGTH: SecurityStrength; +} + +/// The encryption half of a block cipher's streaming API. Strictly block-aligned: whole blocks in, whole +/// blocks out, no finalization step. Padding of non-block-aligned data is handled by a separate layer +/// (`PaddedEncryptor` / `PaddedDecryptor`) built on top of this trait. +/// +/// Encryption and decryption are separate traits (as with [`KEMEncapsulator`] / [`KEMDecapsulator`]) so +/// that the direction can be encoded in the type, and so that a policy can permit decryption of an +/// algorithm while forbidding new encryptions. +/// /// This trait allows for a block cipher to generate initialization data, such as an Initialization Vector (IV) or Counter (CTR) /// which is not technically part of the ciphertext, but must be transmitted along with the ciphertext in order for the /// recipient to perform successful decryption. The length of the initialization data is specified by the implementing struct /// via the `INIT_DATA_LEN` constant. -/// In order for these one-shot APIs to be usable securely in all contexts, the init data will be generated +/// In order for these APIs to be usable securely in all contexts, the init data will be generated /// securely by the block cipher implementation and returned along with the ciphertext, and there is no API for the /// user to provide the init data. If you require this functionality, see the documentation for the underlying implementation. -pub trait BlockCipher: - SymmetricCipher + Sized +pub trait BlockCipherEncryptor< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +>: BlockCipher + Sized { - /// Constructor that begins a flow of the streaming API for encrypting one block at a time. - /// Allows for the implementation to return init data such as an IV which is generated prior to encrypting the first block. + /// Begins a streaming encryption flow, returning the generated init data (e.g. IV). + /// Sources randomness from the library's default OS-backed RNG. fn do_encrypt_init( key: &KeyMaterial, ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; - /// Encrypts a single block of plaintext. - fn do_encrypt_block( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Encrypts a single block of plaintext and writes the ciphertext to the provided buffer. - fn do_encrypt_block_out( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ciphertext: &mut [u8; BLOCK_LEN], - ) -> Result; - /// Encrypts the final block of plaintext. - fn do_encrypt_final( + /// As [`BlockCipherEncryptor::do_encrypt_init`], but sources randomness from the provided RNG. + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; + /// Encrypts `N` consecutive blocks of plaintext. A sequence of calls is equivalent to one call over + /// the concatenation. + fn do_encrypt_blocks( &mut self, - plaintext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Encrypts the final block of plaintext and writes the ciphertext to the provided buffer. - fn do_encrypt_final_out( + plaintext: &[[u8; BLOCK_LEN]; N], + ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError>; + /// Encrypts `N` consecutive blocks of plaintext into the provided buffer. Returns `N * BLOCK_LEN`. + fn do_encrypt_blocks_out( &mut self, - plaintext: &[u8; BLOCK_LEN], - ciphertext: &mut [u8; BLOCK_LEN], + plaintext: &[[u8; BLOCK_LEN]; N], + ciphertext: &mut [[u8; BLOCK_LEN]; N], ) -> Result; - /// Constructor that begins a flow of the streaming API for decryption one block at a time. + + /// One-shot: encrypts `N` blocks under a fresh init. Returns the generated init data and the ciphertext. + fn encrypt_blocks( + key: &KeyMaterial, + plaintext: &[[u8; BLOCK_LEN]; N], + ) -> Result<([u8; INIT_DATA_LEN], [[u8; BLOCK_LEN]; N]), SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init(key)?; + Ok((init_data, enc.do_encrypt_blocks(plaintext)?)) + } + /// As [`BlockCipherEncryptor::encrypt_blocks`], but sources randomness from the provided RNG. + fn encrypt_blocks_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + plaintext: &[[u8; BLOCK_LEN]; N], + ) -> Result<([u8; INIT_DATA_LEN], [[u8; BLOCK_LEN]; N]), SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; + Ok((init_data, enc.do_encrypt_blocks(plaintext)?)) + } + /// One-shot: encrypts `N` blocks under a fresh init into the provided buffer. + /// Returns the generated init data and `N * BLOCK_LEN`. + fn encrypt_blocks_out( + key: &KeyMaterial, + plaintext: &[[u8; BLOCK_LEN]; N], + ciphertext: &mut [[u8; BLOCK_LEN]; N], + ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init(key)?; + Ok((init_data, enc.do_encrypt_blocks_out(plaintext, ciphertext)?)) + } + /// As [`BlockCipherEncryptor::encrypt_blocks_out`], but sources randomness from the provided RNG. + fn encrypt_blocks_out_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + plaintext: &[[u8; BLOCK_LEN]; N], + ciphertext: &mut [[u8; BLOCK_LEN]; N], + ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; + Ok((init_data, enc.do_encrypt_blocks_out(plaintext, ciphertext)?)) + } +} + +/// The decryption half of a block cipher's streaming API; see [`BlockCipherEncryptor`]. +pub trait BlockCipherDecryptor< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +>: BlockCipher + Sized +{ + /// Begins a streaming decryption flow from the init data returned by [`BlockCipherEncryptor::do_encrypt_init`]. fn do_decrypt_init( key: &KeyMaterial, init_data: &[u8; INIT_DATA_LEN], ) -> Result; - /// Decrypts a single block of ciphertext. - fn do_decrypt_block( + /// Decrypts `N` consecutive blocks of ciphertext. A sequence of calls is equivalent to one call over + /// the concatenation. + fn do_decrypt_blocks( &mut self, - ciphertext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Decrypts a single block of ciphertext and writes the plaintext to the provided buffer. - fn do_decrypt_block_out( + ciphertext: &[[u8; BLOCK_LEN]; N], + ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError>; + /// Decrypts `N` consecutive blocks of ciphertext into the provided buffer. Returns `N * BLOCK_LEN`. + fn do_decrypt_blocks_out( &mut self, - ciphertext: &[u8; BLOCK_LEN], - plaintext: &mut [u8; BLOCK_LEN], - ) -> Result; - /// Decrypts the final block of ciphertext. - /// This is the decryption counterpart to [`BlockCipher::do_encrypt_final`] and is where an - /// implementation validates and strips any padding (or otherwise finalizes the flow). - fn do_decrypt_final( - &mut self, - ciphertext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Decrypts the final block of ciphertext and writes the plaintext to the provided buffer. - fn do_decrypt_final_out( - &mut self, - ciphertext: &[u8; BLOCK_LEN], - plaintext: &mut [u8; BLOCK_LEN], + ciphertext: &[[u8; BLOCK_LEN]; N], + plaintext: &mut [[u8; BLOCK_LEN]; N], ) -> Result; + + /// One-shot: decrypts `N` blocks from the given init data. + fn decrypt_blocks( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ciphertext: &[[u8; BLOCK_LEN]; N], + ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError> { + Self::do_decrypt_init(key, init_data)?.do_decrypt_blocks(ciphertext) + } + /// One-shot: decrypts `N` blocks from the given init data into the provided buffer. Returns `N * BLOCK_LEN`. + fn decrypt_blocks_out( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ciphertext: &[[u8; BLOCK_LEN]; N], + plaintext: &mut [[u8; BLOCK_LEN]; N], + ) -> Result { + Self::do_decrypt_init(key, init_data)?.do_decrypt_blocks_out(ciphertext, plaintext) + } } /// The basic functions of an Authenticated Encryption with Addititional Data cipher. @@ -170,7 +235,7 @@ pub trait AEADCipher, @@ -178,7 +243,7 @@ pub trait AEADCipher Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError>; - /// All AEAD ciphers will also be either a [`BlockCipher`] or a [`StreamCipher`], and so will already + /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) or a [`StreamCipher`], and so will already /// have a streaming API. /// This allows you to finish either style of streaming API flow with AEAD specific do_final() /// that computes and returns the authentication tag. @@ -207,7 +272,7 @@ pub trait AEADCipher Result; - /// All AEAD ciphers will also be either a [`BlockCipher`] or a [`StreamCipher`], and so will already + /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) or a [`StreamCipher`], and so will already /// have a streaming API. /// This allows you to finish either style of streaming API flow with AEAD specific do_final() /// that computes and returns the authentication tag. diff --git a/crypto/mldsa/src/aux_functions.rs b/crypto/mldsa/src/aux_functions.rs index 9eadbb89..37d6aa7b 100644 --- a/crypto/mldsa/src/aux_functions.rs +++ b/crypto/mldsa/src/aux_functions.rs @@ -1,14 +1,14 @@ //! Implements auxiliary functions for ML-DSA as defined in Section 7 of FIPS 204. -use crate::matrix::{Matrix, Vector}; -use crate::mldsa::{G, H, q_inv}; -use crate::mldsa::{ - MLDSA44_GAMMA1, MLDSA44_GAMMA2, MLDSA65_GAMMA1, MLDSA65_GAMMA2, N, POLY_T0PACKED_LEN, - POLY_T1PACKED_LEN, d, q, +use crate::matrix::{MatrixTrait, VectorTrait}; +use crate::mldsa::{G, H, N, POLY_T0PACKED_LEN, POLY_T1PACKED_LEN, d, q, q_inv}; +use crate::params::{ + GAMMA1_2_POW_17, GAMMA1_2_POW_19, GAMMA2_Q_MINUS_1_OVER_32, GAMMA2_Q_MINUS_1_OVER_88, + MLDSAParams, }; use crate::polynomial::Polynomial; use bouncycastle_core::traits::XOF; -use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; /// Algorithm 14 CoeffFromThreeBytes(𝑏0, 𝑏1, 𝑏2) /// Output: An integer modulo 𝑞 or ⊥. @@ -34,8 +34,8 @@ pub(crate) fn coeff_from_three_bytes(b: &[u8; 3]) -> Result { /// Input: Integer 𝑏 ∈ {0, 1, … , 15}. /// Output: An integer between −𝜂 and 𝜂, or ⊥. #[inline(always)] -pub(crate) fn coeff_from_half_byte(b: u8) -> Result { - if ETA == 2 && b < 15 { +pub(crate) fn coeff_from_half_byte(b: u8) -> Result { + if P::ETA == 2 && b < 15 { // Original code is bad because '%' is not constant-time. // Ok(2 - (b % 5) as i32) // I'm still not convinced this is constant-time, but maybe it's closer? And I can't come up with anything better. @@ -46,7 +46,7 @@ pub(crate) fn coeff_from_half_byte(b: u8) -> Result { }; Ok(2 - b as i32) } else { - if ETA == 4 && b < 9 { Ok(4 - b as i32) } else { Err(()) } + if P::ETA == 4 && b < 9 { Ok(4 - b as i32) } else { Err(()) } } } @@ -84,13 +84,13 @@ pub const fn bitlen_eta(eta: usize) -> usize { // the hope here is that the compiler will aggressively inline this function, // and optimize away the branching. #[inline(always)] -pub(crate) fn bit_pack_eta(w: &Polynomial, r: &mut [u8]) { - debug_assert!(r.len() >= bitlen_eta(ETA)); +pub(crate) fn bit_pack_eta(w: &Polynomial, r: &mut [u8]) { + debug_assert!(r.len() >= bitlen_eta(P::ETA)); // temp swap space let mut t: [u8; 8] = [0; 8]; - match ETA { + match P::ETA { // MLDSA44 and MLDSA87 2 => { let eta: i32 = 2; @@ -166,19 +166,21 @@ pub(crate) fn bit_pack_t0(t0: &Polynomial) -> [u8; POLY_T0PACKED_LEN] { } /// A variant of Algorithm 17 specific to packing z in the signature value in \[−𝛾1 + 1, 𝛾1]. -pub(crate) fn bitpack_gamma1( - z: &Polynomial, -) -> [u8; POLY_Z_PACKED_LEN] { - let mut r = [0u8; POLY_Z_PACKED_LEN]; +pub(crate) fn bitpack_gamma1(z: &Polynomial) -> P::PolyZPacked { + let mut out = ::ZEROED; + let r = out.as_mut(); let mut t: [u32; 4] = [0; 4]; - match GAMMA1 { - MLDSA44_GAMMA1 => { + // One layout per distinct 𝛾1 rather than one per parameter set; `P::GAMMA1` is a constant + // after monomorphization, so only the matching arm survives. + match P::GAMMA1 { + // MLDSA-44 + GAMMA1_2_POW_17 => { for i in 0..N / 4 { - t[0] = (GAMMA1 - z[4 * i]) as u32; - t[1] = (GAMMA1 - z[4 * i + 1]) as u32; - t[2] = (GAMMA1 - z[4 * i + 2]) as u32; - t[3] = (GAMMA1 - z[4 * i + 3]) as u32; + t[0] = (P::GAMMA1 - z[4 * i]) as u32; + t[1] = (P::GAMMA1 - z[4 * i + 1]) as u32; + t[2] = (P::GAMMA1 - z[4 * i + 2]) as u32; + t[3] = (P::GAMMA1 - z[4 * i + 3]) as u32; r[9 * i] = t[0] as u8; r[9 * i + 1] = (t[0] >> 8) as u8; @@ -191,11 +193,11 @@ pub(crate) fn bitpack_gamma1( r[9 * i + 8] = (t[3] >> 10) as u8; } } - // MLDSA-65 and 87 have the same GAMMA1 value - MLDSA65_GAMMA1 => { + // MLDSA-65 and -87 have the same GAMMA1 value + GAMMA1_2_POW_19 => { for i in 0..N / 2 { - t[0] = (GAMMA1 - z[2 * i]) as u32; - t[1] = (GAMMA1 - z[2 * i + 1]) as u32; + t[0] = (P::GAMMA1 - z[2 * i]) as u32; + t[1] = (P::GAMMA1 - z[2 * i + 1]) as u32; r[5 * i] = t[0] as u8; r[5 * i + 1] = (t[0] >> 8) as u8; @@ -209,7 +211,7 @@ pub(crate) fn bitpack_gamma1( } } - r + out } /// A specific instantiation of Algorithm 18 SimpleBitUnpack(v, 𝑏) with the constants set for unpacking the t1 vector @@ -241,12 +243,12 @@ pub(crate) fn simple_bit_unpack_t1(v: &[u8; POLY_T1PACKED_LEN]) -> Polynomial { // the hope here is that the compiler will aggressively inline this function, // and optimize away the branching. #[inline(always)] -pub(crate) fn bit_unpack_eta(v: &[u8]) -> Polynomial { - debug_assert_eq!(v.len(), bitlen_eta(ETA)); +pub(crate) fn bit_unpack_eta(v: &[u8]) -> Polynomial { + debug_assert_eq!(v.len(), bitlen_eta(P::ETA)); let mut w = Polynomial::new(); - match ETA { + match P::ETA { // MLDSA44 and MLDSA87 2 => { let eta: i32 = 2; @@ -331,11 +333,12 @@ pub(crate) fn bit_unpack_t0(a: &[u8; POLY_T0PACKED_LEN]) -> Polynomial { /// When 𝑎 + 𝑏 + 1 is a power of 2, the coefficients are in [−𝑎, 𝑏]. /// /// Note: caller is responsible for ensuring correct input array size -pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { +pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { let mut w = Polynomial::new(); - match GAMMA1 { - MLDSA44_GAMMA1 => { + match P::GAMMA1 { + // MLDSA-44 + GAMMA1_2_POW_17 => { for i in 0..N / 4 { w[4 * i] = (((v[9 * i] as i32) | ((v[9 * i + 1] as i32) << 8)) | ((v[9 * i + 2] as i32) << 16)) @@ -350,14 +353,14 @@ pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { | ((v[9 * i + 8] as i32) << 10)) & 0x3FFFF; - w[4 * i] = GAMMA1 - w[4 * i]; - w[4 * i + 1] = GAMMA1 - w[4 * i + 1]; - w[4 * i + 2] = GAMMA1 - w[4 * i + 2]; - w[4 * i + 3] = GAMMA1 - w[4 * i + 3]; + w[4 * i] = P::GAMMA1 - w[4 * i]; + w[4 * i + 1] = P::GAMMA1 - w[4 * i + 1]; + w[4 * i + 2] = P::GAMMA1 - w[4 * i + 2]; + w[4 * i + 3] = P::GAMMA1 - w[4 * i + 3]; } } - // MLDSA-65 and 87 have the same GAMMA1 value - MLDSA65_GAMMA1 => { + // MLDSA-65 and -87 have the same GAMMA1 value + GAMMA1_2_POW_19 => { for i in 0..N / 2 { w[2 * i] = (((v[5 * i] as i32) | ((v[5 * i + 1] as i32) << 8)) | ((v[5 * i + 2] as i32) << 16)) @@ -366,8 +369,8 @@ pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { | ((v[5 * i + 4] as i32) << 12)) & 0xFFFFF; - w[2 * i] = GAMMA1 - w[2 * i]; - w[2 * i + 1] = GAMMA1 - w[2 * i + 1]; + w[2 * i] = P::GAMMA1 - w[2 * i]; + w[2 * i + 1] = P::GAMMA1 - w[2 * i + 1]; } } _ => { @@ -384,43 +387,36 @@ pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { /// Output: Signature 𝜎 ∈ 𝔹𝜆/4+ℓ⋅32⋅(1+bitlen (𝛾1−1))+𝜔+𝑘. /// /// Returns the number of bytes written to the output buffer. -pub(crate) fn sig_encode< - const GAMMA1: i32, - const k: usize, - const l: usize, - const LAMBDA_over_4: usize, - const OMEGA: i32, - const POLY_Z_PACKED_LEN: usize, - const SIG_LEN: usize, ->( - c_tilde: &[u8; LAMBDA_over_4], - z: &Vector, - h: &Vector, +pub(crate) fn sig_encode( + c_tilde: &P::SigCTilde, + z: &P::VecL, + h: &P::VecK, output: &mut [u8; SIG_LEN], ) -> usize { + debug_assert_eq!(SIG_LEN, P::SIG_LEN); output.fill(0); let mut pos = 0; - output[..LAMBDA_over_4].copy_from_slice(c_tilde); - pos += LAMBDA_over_4; + output[..P::C_TILDE_LEN].copy_from_slice(c_tilde.as_ref()); + pos += P::C_TILDE_LEN; - for i in 0..l { - output[pos..pos + POLY_Z_PACKED_LEN] - .copy_from_slice(&bitpack_gamma1::(&z.elems[i])); - pos += POLY_Z_PACKED_LEN; + for i in 0..P::l { + output[pos..pos + P::POLY_Z_PACKED_LEN] + .copy_from_slice(bitpack_gamma1::

(&z.elems()[i]).as_ref()); + pos += P::POLY_Z_PACKED_LEN; } // This inlines Algorithm 20 HintBitPack(𝐡) let mut m: usize = 0; - for i in 0..k { + for i in 0..P::k { for j in 0..N { - if h.elems[i][j] != 0 { + if h.elems()[i][j] != 0 { output[pos + m] = j as u8; m += 1; } - output[pos + OMEGA as usize + i] = m as u8; + output[pos + P::OMEGA as usize + i] = m as u8; } } @@ -432,29 +428,22 @@ pub(crate) fn sig_encode< /// Input: Signature 𝜎 ∈ 𝔹𝜆/4+ℓ⋅32⋅(1+bitlen (𝛾1−1))+𝜔+𝑘. /// Output: 𝑐 ∈ 𝔹𝜆/4, 𝐳 ∈ 𝑅ℓ with coefficients in \[−𝛾1 + 1, 𝛾1], 𝐡 ∈ 𝑅𝑘, or ⊥. /// Output: (c_tilde, z, h) -pub(crate) fn sig_decode< - const GAMMA1: i32, - const k: usize, - const l: usize, - const LAMBDA_over_4: usize, - const OMEGA: i32, - const POLY_Z_PACKED_LEN: usize, - const SIG_LEN: usize, ->( +pub(crate) fn sig_decode( sig: &[u8; SIG_LEN], -) -> Result<([u8; LAMBDA_over_4], Vector, Vector), ()> { - let mut c_tilde = [0u8; LAMBDA_over_4]; - let mut z: Vector = Vector::::new(); - let mut h: Vector = Vector::::new(); +) -> Result<(P::SigCTilde, P::VecL, P::VecK), ()> { + debug_assert_eq!(SIG_LEN, P::SIG_LEN); + let mut c_tilde = ::ZEROED; + let mut z = P::VecL::new(); + let mut h = P::VecK::new(); let mut pos: usize = 0; - c_tilde.copy_from_slice(&sig[..LAMBDA_over_4]); - pos += LAMBDA_over_4; + c_tilde.as_mut().copy_from_slice(&sig[..P::C_TILDE_LEN]); + pos += P::C_TILDE_LEN; - for i in 0..l { - z.elems[i] = bit_unpack_gamma1::(&sig[pos..pos + POLY_Z_PACKED_LEN]); - pos += POLY_Z_PACKED_LEN; + for i in 0..P::l { + z.elems_mut()[i] = bit_unpack_gamma1::

(&sig[pos..pos + P::POLY_Z_PACKED_LEN]); + pos += P::POLY_Z_PACKED_LEN; } // This inlines Algorithm 21 HintBitUnpack(𝑦) @@ -465,12 +454,12 @@ pub(crate) fn sig_decode< // 3: for 𝑖 from 0 to 𝑘 − 1 do // ▷ reconstruct 𝐡[𝑖] - for i in 0..k { + for i in 0..P::k { // 4: if 𝑦[𝜔 + 𝑖] < Index or 𝑦[𝜔 + 𝑖] > 𝜔 then return ⊥ // todo: this needs a specific test for malformed signature values. Maybe crucible coveres this case? // ... could hide an assert here and see if it triggers. - if sig[pos + (OMEGA as usize) + i] < (idx as u8) - || sig[pos + (OMEGA as usize) + i] > OMEGA as u8 + if sig[pos + (P::OMEGA as usize) + i] < (idx as u8) + || sig[pos + (P::OMEGA as usize) + i] > P::OMEGA as u8 { return Err(()); } @@ -478,7 +467,7 @@ pub(crate) fn sig_decode< // 6: First ← Index // 7: while Index < 𝑦[𝜔 + 𝑖] do // ▷ 𝑦[𝜔 + 𝑖] says how far one can advance Index - for j in idx..sig[pos + OMEGA as usize + i] as usize { + for j in idx..sig[pos + P::OMEGA as usize + i] as usize { // 8: if Index > First then // 9: if 𝑦[Index − 1] ≥ 𝑦[Index] then return ⊥ // ▷ malformed input @@ -486,17 +475,17 @@ pub(crate) fn sig_decode< return Err(()); } // 12: 𝐡[𝑖]_𝑦[Index] ← 1 - h.elems[i][sig[pos + j] as usize] = 1; + h.elems_mut()[i][sig[pos + j] as usize] = 1; // 13: Index ← Index + 1 // > done by for loop } - idx = sig[pos + OMEGA as usize + i] as usize; + idx = sig[pos + P::OMEGA as usize + i] as usize; } // ▷ read any leftover bytes in the first 𝜔 bytes of 𝑦 for malformed (nonzero) bytes - for j in idx..OMEGA as usize { + for j in idx..P::OMEGA as usize { if sig[pos + j] != 0 { return Err(()); } @@ -509,9 +498,7 @@ pub(crate) fn sig_decode< /// Samples a polynomial 𝑐 ∈ 𝑅 with coefficients from {−1, 0, 1} and Hamming weight 𝜏 ≤ 64. /// Input: A seed 𝜌 ∈ 𝔹𝜆/4 /// Output: A polynomial 𝑐 in 𝑅. -pub(crate) fn sample_in_ball( - rho: &[u8; LAMBDA_over_4], -) -> Polynomial { +pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { // 1: 𝑐 ← 0 let mut c = Polynomial::new(); @@ -519,7 +506,7 @@ pub(crate) fn sample_in_ball( // 3: ctx ← H.Absorb(ctx, 𝜌) // 4: (ctx, 𝑠) ← H.Squeeze(ctx, 8) let mut h = H::new(); - h.absorb(rho).expect("absorb before squeeze is infallible"); + h.absorb(rho.as_ref()).expect("absorb before squeeze is infallible"); let mut s = [0u8; 8]; h.squeeze_out(&mut s); @@ -535,7 +522,7 @@ pub(crate) fn sample_in_ball( // let mut pos = 8; // let mut b; let mut j = [0u8]; - for i in (N - TAU as usize)..N { + for i in (N - P::TAU as usize)..N { // 7: (ctx, 𝑗) ← H.Squeeze(ctx, 1) // Note: Even though it may appear that pre-squeezing a buffer outside the loop would be faster, // testing it both ways doesn't make a noticeable difference, so this has been left as is @@ -624,7 +611,7 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { /// This is supposed to take a rho: [u8; 66], which is: 𝜌||IntegerToBytes(𝑠, 1)||IntegerToBytes(𝑟, 1) /// but to avoid needing to copy bytes and allocate more memory, /// that is split into a [u8;64] and a [u8;2] -pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) -> Polynomial { +pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) -> Polynomial { let mut a = Polynomial::new(); let mut j: usize = 0; let mut h = H::new(); @@ -640,8 +627,8 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2] let mut idx: usize = 0; while j < N { - let z0 = coeff_from_half_byte::(z_arr[idx] & 0x0F); // equiv to % 16 (but faster, and more importantly, constant-time) - let z1 = coeff_from_half_byte::(z_arr[idx] >> 4); // equiv to div_floor(16) (but faster, and more importantly, constant-time) + let z0 = coeff_from_half_byte::

(z_arr[idx] & 0x0F); // equiv to % 16 (but faster, and more importantly, constant-time) + let z1 = coeff_from_half_byte::

(z_arr[idx] >> 4); // equiv to div_floor(16) (but faster, and more importantly, constant-time) if z0.is_ok() { a[j] = z0.unwrap(); @@ -667,12 +654,12 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2] /// in other words: derives the public matrix from the public seed. /// Input: A seed 𝜌 ∈ 𝔹32 .̂ /// Output: Matrix  ∈ (𝑇𝑞)𝑘×ℓ . -pub(crate) fn expandA(rho: &[u8; 32]) -> Matrix { - let mut A_hat = Matrix::::new(); +pub(crate) fn expandA(rho: &[u8; 32]) -> P::MatrixA { + let mut A_hat = P::MatrixA::new(); - for r in 0..k { - for s in 0..l { - A_hat.elems[r][s] = rej_ntt_poly(rho, &[s as u8, r as u8]); + for r in 0..P::k { + for s in 0..P::l { + A_hat.set_elem(r, s, rej_ntt_poly(rho, &[s as u8, r as u8])); } } @@ -685,18 +672,16 @@ pub(crate) fn expandA(rho: &[u8; 32]) -> Matrix< /// Input: A seed 𝜌 ∈ 𝔹64 . /// Output: Vectors 𝐬1, 𝐬2 of secret polynomials in 𝑅 /// Note that this returns Secret> because s1, s2 are always part of a private key. -pub(crate) fn expandS( - rho: &[u8; 64], -) -> (Secret>, Secret>) { - let mut s1: Secret> = Secret::new(); - let mut s2: Secret> = Secret::new(); - - for r in 0..l { - s1.elems[r] = rej_bounded_poly::(rho, &(r as u16).to_le_bytes()); +pub(crate) fn expandS(rho: &[u8; 64]) -> (Secret, Secret) { + let mut s1: Secret = Secret::new(); + let mut s2: Secret = Secret::new(); + + for r in 0..P::l { + s1.elems_mut()[r] = rej_bounded_poly::

(rho, &(r as u16).to_le_bytes()); } - for r in 0..k { - s2.elems[r] = rej_bounded_poly::(rho, &(r as u16 + l as u16).to_le_bytes()); + for r in 0..P::k { + s2.elems_mut()[r] = rej_bounded_poly::

(rho, &(r as u16 + P::l as u16).to_le_bytes()); } (s1, s2) @@ -704,13 +689,13 @@ pub(crate) fn expandS( /// Implements the meta-function described in FIPS 204 section 7.4 for applying power_2_round to a vector. /// ((𝐫1\[𝑖])𝑗, (𝐫0\[𝑖])𝑗) = Power2Round((𝐫\[𝑖])𝑗). -pub(crate) fn power_2_round_vec(v: &Vector) -> (Vector, Vector) { - let mut r1 = Vector::::new(); - let mut r0 = Vector::::new(); +pub(crate) fn power_2_round_vec(v: &V) -> (V, V) { + let mut r1 = V::new(); + let mut r0 = V::new(); - for i in 0..LEN { + for i in 0..V::LEN { for j in 0..N { - (r1.elems[i][j], r0.elems[i][j]) = power_2_round(v.elems[i][j]); + (r1.elems_mut()[i][j], r0.elems_mut()[i][j]) = power_2_round(v.elems()[i][j]); } } @@ -721,17 +706,15 @@ pub(crate) fn power_2_round_vec(v: &Vector) -> (Vector( - rho: &[u8; 64], - mu: u16, -) -> Vector { - let mut y = Vector::::new(); +pub(crate) fn expand_mask(rho: &[u8; 64], mu: u16) -> P::VecL { + let mut y = P::VecL::new(); // 1: 𝑐 ← 1 + bitlen (𝛾1 − 1) // ▷ 𝛾1 is always a power of 2 - // 32c = GAMMA1_MASK_LEN; + // The 32𝑐 bytes squeezed on line 4 are exactly `P::POLY_Z_PACKED_LEN`, so the buffer for them + // is `P::PolyZPacked`; see the docs on [`MLDSAParams::POLY_Z_PACKED_LEN`]. - for r in 0..l { + for r in 0..P::l { // 3: 𝜌′ ← 𝜌||IntegerToBytes(𝜇 + 𝑟, 2) // 4: 𝑣 ← H(𝜌′, 32𝑐) let v = { @@ -739,13 +722,13 @@ pub(crate) fn expand_mask::ZEROED; + h.squeeze_out(v.as_mut()); v }; // 5: 𝐲[𝑟] ← BitUnpack(𝑣, 𝛾1 − 1, 𝛾1) - y.elems[r] = bit_unpack_gamma1::(&v); + y.elems_mut()[r] = bit_unpack_gamma1::

(v.as_ref()); } y @@ -788,7 +771,7 @@ fn test_power_2_round() { /// Decomposes 𝑟 into (𝑟1, 𝑟0) such that 𝑟 ≡ 𝑟1(2𝛾2) + 𝑟0 mod 𝑞. /// Input: 𝑟 ∈ ℤ𝑞. /// Output: Integers (𝑟1, 𝑟0). -pub(crate) fn decompose(r: i32) -> (i32, i32) { +pub(crate) fn decompose(r: i32) -> (i32, i32) { // 1: 𝑟+ ← 𝑟 mod 𝑞 // 2: 𝑟0 ← 𝑟+ mod±(2𝛾2) // 3: if 𝑟+ − 𝑟0 = 𝑞 − 1 then @@ -803,14 +786,15 @@ pub(crate) fn decompose(r: i32) -> (i32, i32) { let mut r1: i32; let mut r0 = (r + 127) >> 7; - match GAMMA2 { - MLDSA44_GAMMA2 => { + match P::GAMMA2 { + // MLDSA-44 + GAMMA2_Q_MINUS_1_OVER_88 => { // (q - 1) / 88 r0 = (r0 * 11275 + (1 << 23)) >> 24; r0 ^= ((43 - r0) >> 31) & r0; } - // ML-DSA65 and 87 have the same GAMMA2 - MLDSA65_GAMMA2 => { + // ML-DSA-65 and -87 have the same GAMMA2 + GAMMA2_Q_MINUS_1_OVER_32 => { // (q - 1) / 32; r0 = (r0 * 1025 + (1 << 21)) >> 22; r0 &= 15; @@ -821,7 +805,7 @@ pub(crate) fn decompose(r: i32) -> (i32, i32) { } } - r1 = r - r0 * 2 * GAMMA2; + r1 = r - r0 * 2 * P::GAMMA2; // mutants note: the choice of (q - 1) is a bit arbitrary in that after doing the bit-shifting, // this seems to work out mathematically equivalent if doing q/2, or (q+3)/2, but here it is left as (q-1)/2 @@ -835,10 +819,10 @@ pub(crate) fn decompose(r: i32) -> (i32, i32) { /// Returns 𝑟1 from the output of Decompose (𝑟). /// Input: 𝑟 ∈ ℤ𝑞. /// Output: Integer 𝑟1. -pub(crate) fn high_bits(r: i32) -> i32 { +pub(crate) fn high_bits(r: i32) -> i32 { // 1: (𝑟1, 𝑟0) ← Decompose(𝑟) // 2: return 𝑟1 - let (r1, _) = decompose::(r); + let (r1, _) = decompose::

(r); r1 } @@ -846,10 +830,10 @@ pub(crate) fn high_bits(r: i32) -> i32 { /// Returns 𝑟0 from the output of Decompose (𝑟). /// Input: 𝑟 ∈ ℤ𝑞. /// Output: Integer 𝑟0. -pub(crate) fn low_bits(r: i32) -> i32 { +pub(crate) fn low_bits(r: i32) -> i32 { // 1: (𝑟1, 𝑟0) ← Decompose(𝑟) // 2: return 𝑟0 - let (_, r0) = decompose::(r); + let (_, r0) = decompose::

(r); r0 } @@ -857,32 +841,29 @@ pub(crate) fn low_bits(r: i32) -> i32 { /// Computes hint bit indicating whether adding 𝑧 to 𝑟 alters the high bits of 𝑟. /// Input: 𝑧, 𝑟 ∈ ℤ𝑞. /// Output: Boolean. -pub(crate) fn make_hint(z: i32, r: i32) -> i32 { +pub(crate) fn make_hint(z: i32, r: i32) -> i32 { // // 1: 𝑟1 ← HighBits(𝑟) - // let r1 = high_bits::(r); + // let r1 = high_bits::

(r); // // // 2: 𝑣1 ← HighBits(𝑟 + 𝑧) - // let v1 = high_bits::(r + z); + // let v1 = high_bits::

(r + z); // // // 3: return [[𝑟1 ≠ 𝑣1]] // if r1 != v1 { 1 } else { 0 } // By the powers of someone much more clever than me, this is equivalent. // mutants note: we do not have KATs that exercise all branches of this if - if z <= GAMMA2 || z > q - GAMMA2 || (z == q - GAMMA2 && r == 0) { 0 } else { 1 } + if z <= P::GAMMA2 || z > q - P::GAMMA2 || (z == q - P::GAMMA2 && r == 0) { 0 } else { 1 } } /// Creates the hint vector from two Vector's, and also returns its hamming weight (ie the number of 1's). -pub(crate) fn make_hint_vecs( - r: &Vector, - s: &Vector, -) -> (Vector, i32) { - let mut out = Vector::::new(); +pub(crate) fn make_hint_vecs(r: &P::VecK, s: &P::VecK) -> (P::VecK, i32) { + let mut out = P::VecK::new(); let mut count = 0i32; - for i in 0..k { - let (w, c) = r.elems[i].make_hint::(&s.elems[i]); - out.elems[i] = w; + for i in 0..P::k { + let (w, c) = r.elems()[i].make_hint::

(&s.elems()[i]); + out.elems_mut()[i] = w; // mutants note: this chains up to hint_hamming_weight > OMEGA and there is no test KAT that triggers this branch count += c; @@ -895,8 +876,8 @@ pub(crate) fn make_hint_vecs( /// Returns the high bits of 𝑟 adjusted according to hint ℎ. /// Input: Boolean ℎ, 𝑟 ∈ ℤ𝑞. /// Output: 𝑟1 ∈ ℤ with 0 ≤ 𝑟1 ≤ (𝑞−1) / 2*gamma2). -pub(super) fn use_hint(a: i32, hint: i32) -> i32 { - let (a0, a1) = decompose::(a); +pub(super) fn use_hint(a: i32, hint: i32) -> i32 { + let (a0, a1) = decompose::

(a); if hint == 0 { return a0; @@ -904,8 +885,8 @@ pub(super) fn use_hint(a: i32, hint: i32) -> i32 { debug_assert!(hint == 1); - match GAMMA2 { - MLDSA44_GAMMA2 => { + match P::GAMMA2 { + GAMMA2_Q_MINUS_1_OVER_88 => { // mutants note: this passes unit tests if it's a1 >= 0 // it is left like this because it matches the spec if a1 > 0 { @@ -915,7 +896,7 @@ pub(super) fn use_hint(a: i32, hint: i32) -> i32 { } } // ML-DSA65 and 87 have the same GAMMA2 - MLDSA65_GAMMA2 => { + GAMMA2_Q_MINUS_1_OVER_32 => { // mutants note: this passes unit tests if it's a1 >= 0 // it is left like this because it matches the spec if a1 > 0 { (a0 + 1) & 15 } else { (a0 - 1) & 15 } @@ -926,23 +907,22 @@ pub(super) fn use_hint(a: i32, hint: i32) -> i32 { } } -pub(crate) fn use_hint_polys( +pub(crate) fn use_hint_polys( wp_approx: &Polynomial, h: &Polynomial, out: &mut Polynomial, ) { for i in 0..N { - out[i] = use_hint::(wp_approx[i], h[i]); + out[i] = use_hint::

(wp_approx[i], h[i]); } } -pub(crate) fn use_hint_vecs( - h: &Vector, - wp_approx: &Vector, -) -> Vector { - let mut out = Vector::::new(); - for i in 0..k { - use_hint_polys::(&wp_approx.elems[i], &h.elems[i], &mut out.elems[i]); +pub(crate) fn use_hint_vecs(h: &P::VecK, wp_approx: &P::VecK) -> P::VecK { + let mut out = P::VecK::new(); + for i in 0..P::k { + let hint = h.elems()[i]; + let approx = wp_approx.elems()[i]; + use_hint_polys::

(&approx, &hint, &mut out.elems_mut()[i]); } out diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index 5e1b535a..a4018adb 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -66,29 +66,15 @@ //! But a simple [`HashMLDSA::keygen`] is provided. use crate::mldsa::{H, MLDSA_MU_LEN, MLDSA_RND_LEN, MLDSATrait}; -use crate::mldsa::{ - MLDSA44_BETA, MLDSA44_C_TILDE, MLDSA44_ETA, MLDSA44_GAMMA1, MLDSA44_GAMMA1_MASK_LEN, - MLDSA44_GAMMA1_MINUS_BETA, MLDSA44_GAMMA2, MLDSA44_GAMMA2_MINUS_BETA, MLDSA44_LAMBDA, - MLDSA44_LAMBDA_over_4, MLDSA44_OMEGA, MLDSA44_PK_LEN, MLDSA44_POLY_W1_PACKED_LEN, - MLDSA44_POLY_Z_PACKED_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN, MLDSA44_TAU, MLDSA44_k, MLDSA44_l, -}; -use crate::mldsa::{ - MLDSA65_BETA, MLDSA65_C_TILDE, MLDSA65_ETA, MLDSA65_GAMMA1, MLDSA65_GAMMA1_MASK_LEN, - MLDSA65_GAMMA1_MINUS_BETA, MLDSA65_GAMMA2, MLDSA65_GAMMA2_MINUS_BETA, MLDSA65_LAMBDA, - MLDSA65_LAMBDA_over_4, MLDSA65_OMEGA, MLDSA65_PK_LEN, MLDSA65_POLY_W1_PACKED_LEN, - MLDSA65_POLY_Z_PACKED_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN, MLDSA65_TAU, MLDSA65_k, MLDSA65_l, -}; -use crate::mldsa::{ - MLDSA87_BETA, MLDSA87_C_TILDE, MLDSA87_ETA, MLDSA87_GAMMA1, MLDSA87_GAMMA1_MASK_LEN, - MLDSA87_GAMMA1_MINUS_BETA, MLDSA87_GAMMA2, MLDSA87_GAMMA2_MINUS_BETA, MLDSA87_LAMBDA, - MLDSA87_LAMBDA_over_4, MLDSA87_OMEGA, MLDSA87_PK_LEN, MLDSA87_POLY_W1_PACKED_LEN, - MLDSA87_POLY_Z_PACKED_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN, MLDSA87_TAU, MLDSA87_k, MLDSA87_l, -}; +use crate::mldsa::{MLDSA44_PK_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN}; +use crate::mldsa::{MLDSA65_PK_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN}; +use crate::mldsa::{MLDSA87_PK_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN}; use crate::mldsa_keys::{MLDSAPrivateKeyInternalTrait, MLDSAPublicKeyInternalTrait}; +use crate::params::{MLDSA44Params, MLDSA65Params, MLDSA87Params, MLDSAParams}; use crate::{ MLDSA, MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPrivateKeyExpanded, MLDSAPrivateKeyTrait, - MLDSAPublicKeyExpanded, MLDSAPublicKeyTrait, Matrix, + MLDSAPublicKeyExpanded, MLDSAPublicKeyTrait, }; use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::KeyMaterial; @@ -124,29 +110,14 @@ pub const HASH_ML_DSA_87_WITH_SHA512_NAME: &str = "HashML-DSA-87_with_SHA512"; /// The HashML-DSA-44_with_SHA512 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA44_with_SHA256 = HashMLDSA< + MLDSA44Params, SHA256, + MLDSA44PublicKey, + MLDSA44PrivateKey, 32, MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44_SIG_LEN, - MLDSA44PublicKey, - MLDSA44PrivateKey, - MLDSA44_TAU, - MLDSA44_LAMBDA, - MLDSA44_GAMMA1, - MLDSA44_GAMMA2, - MLDSA44_k, - MLDSA44_l, - MLDSA44_ETA, - MLDSA44_BETA, - MLDSA44_OMEGA, - MLDSA44_C_TILDE, - MLDSA44_POLY_Z_PACKED_LEN, - MLDSA44_POLY_W1_PACKED_LEN, - MLDSA44_LAMBDA_over_4, - MLDSA44_GAMMA1_MINUS_BETA, - MLDSA44_GAMMA2_MINUS_BETA, - MLDSA44_GAMMA1_MASK_LEN, >; impl Algorithm for HashMLDSA44_with_SHA256 { @@ -157,29 +128,14 @@ impl Algorithm for HashMLDSA44_with_SHA256 { /// The HashML-DSA-65_with_SHA256 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA65_with_SHA256 = HashMLDSA< + MLDSA65Params, SHA256, + MLDSA65PublicKey, + MLDSA65PrivateKey, 32, MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65_SIG_LEN, - MLDSA65PublicKey, - MLDSA65PrivateKey, - MLDSA65_TAU, - MLDSA65_LAMBDA, - MLDSA65_GAMMA1, - MLDSA65_GAMMA2, - MLDSA65_k, - MLDSA65_l, - MLDSA65_ETA, - MLDSA65_BETA, - MLDSA65_OMEGA, - MLDSA65_C_TILDE, - MLDSA65_POLY_Z_PACKED_LEN, - MLDSA65_POLY_W1_PACKED_LEN, - MLDSA65_LAMBDA_over_4, - MLDSA65_GAMMA1_MINUS_BETA, - MLDSA65_GAMMA2_MINUS_BETA, - MLDSA65_GAMMA1_MASK_LEN, >; impl Algorithm for HashMLDSA65_with_SHA256 { @@ -190,29 +146,14 @@ impl Algorithm for HashMLDSA65_with_SHA256 { /// The HashML-DSA-87_with_SHA256 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA87_with_SHA256 = HashMLDSA< + MLDSA87Params, SHA256, + MLDSA87PublicKey, + MLDSA87PrivateKey, 32, MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87_SIG_LEN, - MLDSA87PublicKey, - MLDSA87PrivateKey, - MLDSA87_TAU, - MLDSA87_LAMBDA, - MLDSA87_GAMMA1, - MLDSA87_GAMMA2, - MLDSA87_k, - MLDSA87_l, - MLDSA87_ETA, - MLDSA87_BETA, - MLDSA87_OMEGA, - MLDSA87_C_TILDE, - MLDSA87_POLY_Z_PACKED_LEN, - MLDSA87_POLY_W1_PACKED_LEN, - MLDSA87_LAMBDA_over_4, - MLDSA87_GAMMA1_MINUS_BETA, - MLDSA87_GAMMA2_MINUS_BETA, - MLDSA87_GAMMA1_MASK_LEN, >; impl Algorithm for HashMLDSA87_with_SHA256 { @@ -223,29 +164,14 @@ impl Algorithm for HashMLDSA87_with_SHA256 { /// The HashML-DSA-44_with_SHA512 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA44_with_SHA512 = HashMLDSA< + MLDSA44Params, SHA512, + MLDSA44PublicKey, + MLDSA44PrivateKey, 64, MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44_SIG_LEN, - MLDSA44PublicKey, - MLDSA44PrivateKey, - MLDSA44_TAU, - MLDSA44_LAMBDA, - MLDSA44_GAMMA1, - MLDSA44_GAMMA2, - MLDSA44_k, - MLDSA44_l, - MLDSA44_ETA, - MLDSA44_BETA, - MLDSA44_OMEGA, - MLDSA44_C_TILDE, - MLDSA44_POLY_Z_PACKED_LEN, - MLDSA44_POLY_W1_PACKED_LEN, - MLDSA44_LAMBDA_over_4, - MLDSA44_GAMMA1_MINUS_BETA, - MLDSA44_GAMMA2_MINUS_BETA, - MLDSA44_GAMMA1_MASK_LEN, >; impl Algorithm for HashMLDSA44_with_SHA512 { @@ -262,29 +188,14 @@ impl AlgorithmOID for HashMLDSA44_with_SHA512 { /// The HashML-DSA-65_with_SHA512 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA65_with_SHA512 = HashMLDSA< + MLDSA65Params, SHA512, + MLDSA65PublicKey, + MLDSA65PrivateKey, 64, MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65_SIG_LEN, - MLDSA65PublicKey, - MLDSA65PrivateKey, - MLDSA65_TAU, - MLDSA65_LAMBDA, - MLDSA65_GAMMA1, - MLDSA65_GAMMA2, - MLDSA65_k, - MLDSA65_l, - MLDSA65_ETA, - MLDSA65_BETA, - MLDSA65_OMEGA, - MLDSA65_C_TILDE, - MLDSA65_POLY_Z_PACKED_LEN, - MLDSA65_POLY_W1_PACKED_LEN, - MLDSA65_LAMBDA_over_4, - MLDSA65_GAMMA1_MINUS_BETA, - MLDSA65_GAMMA2_MINUS_BETA, - MLDSA65_GAMMA1_MASK_LEN, >; impl Algorithm for HashMLDSA65_with_SHA512 { @@ -301,29 +212,14 @@ impl AlgorithmOID for HashMLDSA65_with_SHA512 { /// The HashML-DSA-87_with_SHA512 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA87_with_SHA512 = HashMLDSA< + MLDSA87Params, SHA512, + MLDSA87PublicKey, + MLDSA87PrivateKey, 64, MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87_SIG_LEN, - MLDSA87PublicKey, - MLDSA87PrivateKey, - MLDSA87_TAU, - MLDSA87_LAMBDA, - MLDSA87_GAMMA1, - MLDSA87_GAMMA2, - MLDSA87_k, - MLDSA87_l, - MLDSA87_ETA, - MLDSA87_BETA, - MLDSA87_OMEGA, - MLDSA87_C_TILDE, - MLDSA87_POLY_Z_PACKED_LEN, - MLDSA87_POLY_W1_PACKED_LEN, - MLDSA87_LAMBDA_over_4, - MLDSA87_GAMMA1_MINUS_BETA, - MLDSA87_GAMMA2_MINUS_BETA, - MLDSA87_GAMMA1_MASK_LEN, >; impl Algorithm for HashMLDSA87_with_SHA512 { @@ -344,32 +240,16 @@ impl AlgorithmOID for HashMLDSA87_with_SHA512 { /// by specifying the hash function to use (in the verifier), and specifying the bytes of the OID to /// to use as its domain separator in constructing the message representative M'. pub struct HashMLDSA< + P: MLDSAParams, HASH: Hash + AlgorithmOID + Default, - const HASH_LEN: usize, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, + const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, > { - _phantom: PhantomData<(PK, SK)>, + _phantom: PhantomData<(P, PK, SK)>, signer_rnd: Option<[u8; MLDSA_RND_LEN]>, @@ -392,56 +272,15 @@ pub struct HashMLDSA< } impl< + P: MLDSAParams, HASH: Hash + AlgorithmOID + Default, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MASK_LEN: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, -> - HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> HashMLDSA { /// Generate a keypair, sourcing randomness from bouncycastle's default os-backed RNG. /// @@ -450,60 +289,16 @@ impl< /// Keygen, and keys in general, are interchangeable between MLDSA and HashMLDSA. /// Error condition: basically only on RNG failures. pub fn keygen() -> Result<(PK, SK), SignatureError> { - MLDSA::< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::keygen() + MLDSA::::keygen() } /// Imports a secret key from a seed. pub fn keygen_from_seed(seed: &KeyMaterial<32>) -> Result<(PK, SK), SignatureError> { - MLDSA::< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::keygen_internal(seed) + MLDSA::::keygen_internal(seed) } /// Same as [`Signer::sign`], but signs from an [`MLDSAPrivateKeyExpanded`]. pub fn sign_with_expanded_key( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; SIG_LEN], SignatureError> { @@ -514,7 +309,7 @@ impl< } /// Same as [`Signer::sign_out`], but signs from an [`MLDSAPrivateKeyExpanded`]. pub fn sign_with_expanded_key_out( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN], @@ -527,7 +322,7 @@ impl< } /// Same as [`PHSigner::sign_ph`], but signs from an [`MLDSAPrivateKeyExpanded`]. pub fn sign_ph_with_expanded_key( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, ph: &[u8; PH_LEN], ctx: Option<&[u8]>, ) -> Result<[u8; SIG_LEN], SignatureError> { @@ -538,7 +333,7 @@ impl< } /// Same as [`PHSigner::sign_ph_out`], but signs from an [`MLDSAPrivateKeyExpanded`]. pub fn sign_ph_with_expanded_key_out( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, ph: &[u8; PH_LEN], ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN], @@ -564,7 +359,7 @@ impl< /// prevent accidental nonce reuse, this function moves `rnd`. pub fn sign_ph_deterministic( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, ctx: Option<&[u8]>, ph: &[u8; PH_LEN], rnd: [u8; 32], @@ -587,7 +382,7 @@ impl< /// Returns the number of bytes written to the output buffer. Can be called with an oversized buffer. pub fn sign_ph_deterministic_out( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, ctx: Option<&[u8]>, ph: &[u8; PH_LEN], rnd: [u8; 32], @@ -625,29 +420,9 @@ impl< }; // 24: 𝜎 ← ML-DSA.Sign_internal(𝑠𝑘, 𝑀', 𝑟𝑛𝑑) - let bytes_written = MLDSA::< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::sign_mu_deterministic_out(sk, A_hat, &mu, rnd, output)?; + let bytes_written = MLDSA::::sign_mu_deterministic_out( + sk, A_hat, &mu, rnd, output, + )?; Ok(bytes_written) } @@ -695,7 +470,7 @@ impl< } /// Same as [`SignatureVerifier::verify`], but verifies from an [`MLDSAPublicKeyExpanded`]. pub fn verify_with_expanded_key( - pk: &MLDSAPublicKeyExpanded, + pk: &MLDSAPublicKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8], @@ -708,7 +483,7 @@ impl< fn verify_ph_internal( pk: &PK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, ph: &[u8; PH_LEN], ctx: Option<&[u8]>, sig: &[u8], @@ -747,107 +522,32 @@ impl< }; match A_hat { - Some(A_hat) => MLDSA::< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::verify_mu(pk, Some(A_hat), &mu, sig_sized), - None => MLDSA::< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::verify_mu(pk, Some(&pk.A_hat()), &mu, sig_sized), + Some(A_hat) => MLDSA::::verify_mu( + pk, + Some(A_hat), + &mu, + sig_sized, + ), + None => MLDSA::::verify_mu( + pk, + Some(&pk.A_hat()), + &mu, + sig_sized, + ), } } } impl< + P: MLDSAParams, HASH: Hash + AlgorithmOID + Default, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> Signer - for HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> Signer for HashMLDSA { /// Algorithm 4 HashML-DSA.Sign(𝑠𝑘, 𝑀 , 𝑐𝑡𝑥, PH) /// Generate a “pre-hash” ML-DSA signature. @@ -946,56 +646,16 @@ impl< } impl< + P: MLDSAParams, HASH: Hash + AlgorithmOID + Default, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, > SignatureVerifier - for HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > + for HashMLDSA { fn verify(pk: &PK, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8]) -> Result<(), SignatureError> { let mut ph_m = [0u8; PH_LEN]; @@ -1033,56 +693,16 @@ impl< } impl< + P: MLDSAParams, HASH: Hash + AlgorithmOID + Default, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MASK_LEN: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, > PHSigner - for HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > + for HashMLDSA { fn sign_ph( sk: &SK, @@ -1114,56 +734,16 @@ impl< } impl< + P: MLDSAParams, HASH: Hash + AlgorithmOID + Default, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MASK_LEN: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, > PHSignatureVerifier - for HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > + for HashMLDSA { fn verify_ph( pk: &PK, diff --git a/crypto/mldsa/src/lib.rs b/crypto/mldsa/src/lib.rs index 2bea9873..20711aa6 100644 --- a/crypto/mldsa/src/lib.rs +++ b/crypto/mldsa/src/lib.rs @@ -148,6 +148,7 @@ pub mod hash_mldsa; mod matrix; pub mod mldsa; mod mldsa_keys; +mod params; mod polynomial; /*** Exported types ***/ @@ -187,4 +188,4 @@ pub use mldsa::{MLDSA87_PK_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN}; pub use mldsa::SUSPENDED_MU_BUILDER_STATE_LEN; -pub use matrix::Matrix; +pub use params::{MLDSA44Params, MLDSA65Params, MLDSA87Params}; diff --git a/crypto/mldsa/src/matrix.rs b/crypto/mldsa/src/matrix.rs index 916fe47d..e08bb62f 100644 --- a/crypto/mldsa/src/matrix.rs +++ b/crypto/mldsa/src/matrix.rs @@ -3,11 +3,32 @@ use crate::aux_functions::multiply_ntt; use crate::mldsa::H; +use crate::params::MLDSAParams; use crate::polynomial::Polynomial; use bouncycastle_core::traits::XOF; use bouncycastle_utils::secret::ZeroizablePrimitive; use core::ops::{Index, IndexMut}; +/// The operations this crate performs on the public matrix 𝐀̂. +/// +/// [`Matrix`] is the only implementation; see the module docs for why the trait exists. +pub(crate) trait MatrixTrait: Sized + Clone { + /// The vector this matrix can be applied to: an element of 𝑅^ℓ. + type VecL: VectorTrait; + /// The vector applying this matrix produces: an element of 𝑅^𝑘. + type VecK: VectorTrait; + + /// A matrix with every coefficient set to zero. + fn new() -> Self; + + /// Overwrites the polynomial at `elems[row][col]`. + fn set_elem(&mut self, row: usize, col: usize, p: Polynomial); + + /// Algorithm 48 MatrixVectorNTT(𝐌, 𝐯) + /// Computes the product 𝐌 ∘̂ 𝐯_hat of a matrix 𝐌_hat and a vector 𝐯_hat over 𝑇𝑞. + fn matrix_vector_ntt(&self, v: &Self::VecL) -> Self::VecK; +} + /// A matrix over the ML-DSA ring. #[derive(Clone)] pub struct Matrix { @@ -47,8 +68,88 @@ impl Matrix { } } +impl MatrixTrait for Matrix { + type VecL = Vector; + type VecK = Vector; + + fn new() -> Self { + Matrix::new() + } + + fn set_elem(&mut self, row: usize, col: usize, p: Polynomial) { + self.elems[row][col] = p; + } + + fn matrix_vector_ntt(&self, v: &Vector) -> Vector { + Matrix::matrix_vector_ntt(self, v) + } +} + +/// The operations this crate performs on a vector of polynomials, i.e. on an element of 𝑅^LEN. +/// +/// [`Vector`] is the only implementation; the trait exists so that code generic over a parameter +/// set can operate on [`MLDSAParams::VecK`] and [`MLDSAParams::VecL`] without knowing their length. +pub trait VectorTrait: + Sized + Copy + ZeroizablePrimitive + Index + IndexMut +{ + /// The number of polynomial coordinates, i.e. 𝑘 or ℓ. + const LEN: usize; + + /// A vector with every coefficient set to zero. + fn new() -> Self; + + /// The coordinates, for iteration and chunking. + fn elems(&self) -> &[Polynomial]; + /// The coordinates, for iteration and chunking. + fn elems_mut(&mut self) -> &mut [Polynomial]; + + /// Algorithm 46 AddVectorNTT(𝐯, 𝐰)̂ + /// Computes the sum 𝐯_hat + 𝐰_hat of two vectors 𝐯_hat, 𝐰_hat over 𝑇𝑞. + fn add_vector_ntt(&mut self, s: &Self); + + /// Subtracts another vector from this one, coordinatewise. + fn sub_vector(&self, s: &Self) -> Self; + + /// Algorithm 47 ScalarVectorNTT(𝑐,̂ 𝐯)̂ + /// Computes the product 𝑐_hat * 𝐯_hat of a scalar 𝑐_hat and a vector 𝐯_hat over 𝑇𝑞. + fn scalar_vector_ntt(&self, w: &Polynomial) -> Self; + + /// Adds 𝑞 to every negative coefficient. + fn conditional_add_q(&mut self); + + /// Montgomery-reduces every coefficient. + fn reduce(&mut self); + + /// Applies Algorithm 41 NTT(𝑤) to every coordinate. + fn ntt(&mut self); + + /// Applies Algorithm 42 NTT−1(𝑤_hat) to every coordinate. + fn inv_ntt(&mut self); + + /// Applies Algorithm 37 HighBits(𝑟) coefficientwise. + fn high_bits(&self) -> Self; + + /// Applies Algorithm 38 LowBits(𝑟) coefficientwise. + fn low_bits(&self) -> Self; + + /// Multiplies every coefficient by 2^𝑑. + fn shift_left_d(&self) -> Self; + + /// Tests whether any coefficient of any coordinate has absolute value at least `bound`. + /// See `Polynomial::check_norm` for why `bound` is not a const generic. + fn check_norm(&self, bound: i32) -> bool; + + /// Algorithm 28 w1Encode(𝐰1), fed straight into `h` rather than into a buffer. + fn w1_encode_and_hash(&self, h: &mut H); +} + +/// A vector of `LEN` polynomials, i.e. an element of 𝑅^LEN. +/// +/// Public only because it is the value of [`MLDSAParams::VecK`] and [`MLDSAParams::VecL`]; its +/// fields and operations are crate-private, so from outside it is an opaque handle. Reach it +/// through [`VectorTrait`]. #[derive(Clone, Copy)] -pub(crate) struct Vector { +pub struct Vector { pub(crate) elems: [Polynomial; LEN], } @@ -75,21 +176,37 @@ impl Vector { pub(crate) const fn new() -> Self { Self { elems: [Polynomial::new(); LEN] } } +} + +impl VectorTrait for Vector { + const LEN: usize = LEN; + + fn new() -> Self { + Vector::new() + } + + fn elems(&self) -> &[Polynomial] { + &self.elems + } + + fn elems_mut(&mut self) -> &mut [Polynomial] { + &mut self.elems + } /// Algorithm 46 AddVectorNTT(𝐯, 𝐰)̂ /// Computes the sum 𝐯_hat + 𝐰_hat of two vectors 𝐯_hat, 𝐰_hat over 𝑇𝑞. /// Input: ℓ ∈ ℕ, v_hat ∈ T^ℓ, w_hat ∈ 𝑇^ℓ /// Output: u_hat ∈ T^ℓ_𝑞. /// Add another vector to this vector - pub(crate) fn add_vector_ntt(&mut self, s: &Self) { + fn add_vector_ntt(&mut self, s: &Self) { for i in 0..LEN { // perform montgomery addition of each polynomial in the vector self[i].add_ntt(&s[i]); } } - pub(crate) fn sub_vector(&self, s: &Self) -> Self { - let mut out = self.clone(); + fn sub_vector(&self, s: &Self) -> Self { + let mut out = *self; for i in 0..LEN { out[i].sub(&s[i]); } @@ -100,8 +217,8 @@ impl Vector { /// Computes the product 𝑐_hat * 𝐯_hat of a scalar 𝑐_hat and a vector 𝐯_hat over 𝑇𝑞. /// Input: 𝑐_hat ∈ 𝑇𝑞, ℓ ∈ ℕ, 𝐯_hat ∈ 𝑇^ℓ /// Output: 𝑞 . - pub(crate) fn scalar_vector_ntt(&self, w: &Polynomial) -> Self { - let mut s_hat = Self::new(); + fn scalar_vector_ntt(&self, w: &Polynomial) -> Self { + let mut s_hat = Vector::::new(); for i in 0..LEN { s_hat[i] = multiply_ntt(&self[i], &w); } @@ -109,63 +226,63 @@ impl Vector { s_hat } - pub(crate) fn conditional_add_q(&mut self) { + fn conditional_add_q(&mut self) { for i in 0..LEN { self[i].conditional_add_q(); } } - pub(crate) fn reduce(&mut self) { + fn reduce(&mut self) { for i in 0..LEN { self[i].reduce(); } } - pub(crate) fn ntt(&mut self) { + fn ntt(&mut self) { for i in 0..LEN { self[i].ntt(); } } - pub(crate) fn inv_ntt(&mut self) { + fn inv_ntt(&mut self) { for i in 0..LEN { self[i].inv_ntt(); } } - pub(crate) fn high_bits(&self) -> Self { - let mut s = Self::new(); + fn high_bits(&self) -> Self { + let mut s = Vector::::new(); for i in 0..LEN { - s[i] = self[i].high_bits::(); + s[i] = self[i].high_bits::

(); } s } - pub(crate) fn low_bits(&self) -> Self { - let mut s = Self::new(); + fn low_bits(&self) -> Self { + let mut s = Vector::::new(); for i in 0..LEN { - s[i] = self[i].low_bits::(); + s[i] = self[i].low_bits::

(); } s } - pub(crate) fn shift_left(&self) -> Self { - let mut out = self.clone(); + fn shift_left_d(&self) -> Self { + let mut out = *self; for i in 0..LEN { - out[i].shift_left::(); + out[i].shift_left_d(); } out } - pub(crate) fn check_norm(&self) -> bool { + fn check_norm(&self, bound: i32) -> bool { // Fine that this is not constant-time because it is used in a rejection loop -- the early quit leads to rejection. for x in self.elems.iter() { - if x.check_norm::() { + if x.check_norm(bound) { return true; } } @@ -177,7 +294,7 @@ impl Vector { /// Input: 𝐰1 ∈ 𝑅𝑘 whose polynomial coordinates have coefficients in \[0, (𝑞 − 1)/(2𝛾2) − 1]. /// Output: A byte string representation 𝐰1_tilde ∈ 𝔹32𝑘⋅bitlen ((𝑞−1)/(2𝛾2)−1) /// Optimized from FIPS 204 to feed into the hash one row at a time to reduce overall memory footprint. - pub(crate) fn w1_encode_and_hash(&self, h: &mut H) { + fn w1_encode_and_hash(&self, h: &mut H) { // 1: 𝐰̃1 ← () // Nothing needs to be allocated since it is being fed into the hash row-wise @@ -185,8 +302,7 @@ impl Vector { // 3: 𝐰̃1 ← 𝐰̃1 || SimpleBitPack (𝐰1[𝑖], (𝑞 − 1)/(2𝛾2) − 1) // 4: end for for w in self.elems.iter() { - h.absorb(&w.w1_encode::()) - .expect("absorb before squeeze is infallible"); + h.absorb(w.w1_encode::

().as_ref()).expect("absorb before squeeze is infallible"); } } } diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs index 842841a4..2e58854f 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -479,9 +479,10 @@ use crate::aux_functions::{ expand_mask, expandA, expandS, make_hint_vecs, power_2_round_vec, sample_in_ball, sig_decode, sig_encode, use_hint_vecs, }; -use crate::matrix::{Matrix, Vector}; +use crate::matrix::{MatrixTrait, VectorTrait}; use crate::mldsa_keys::{MLDSAPrivateKeyInternalTrait, MLDSAPrivateKeyTrait}; use crate::mldsa_keys::{MLDSAPublicKeyInternalTrait, MLDSAPublicKeyTrait}; +use crate::params::{MLDSA44Params, MLDSA65Params, MLDSA87Params, MLDSAParams}; use crate::{ MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPrivateKeyExpanded, MLDSAPublicKeyExpanded, @@ -493,7 +494,7 @@ use bouncycastle_core::traits::{ }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN}; -use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; use core::marker::PhantomData; // imports needed just for docs @@ -529,95 +530,28 @@ pub const MLDSA_MU_LEN: usize = 64; pub(crate) const POLY_T0PACKED_LEN: usize = 416; pub(crate) const POLY_T1PACKED_LEN: usize = 320; -/* ML-DSA-44 params */ +/*** Re-exporting length constants that a caller will need instead of the entire Params objects which contains a bunch of internal algorithm detail ***/ /// Length of the \[u8] holding a ML-DSA-44 public key. -pub const MLDSA44_PK_LEN: usize = 1312; +pub const MLDSA44_PK_LEN: usize = MLDSA44Params::PK_LEN; /// Length of the \[u8] holding a ML-DSA-44 private key. -pub const MLDSA44_SK_LEN: usize = 2560; +pub const MLDSA44_SK_LEN: usize = MLDSA44Params::SK_LEN; /// Length of the \[u8] holding a ML-DSA-44 signature value. -pub const MLDSA44_SIG_LEN: usize = 2420; -pub(crate) const MLDSA44_TAU: i32 = 39; -pub(crate) const MLDSA44_LAMBDA: i32 = 128; -pub(crate) const MLDSA44_GAMMA1: i32 = 1 << 17; -pub(crate) const MLDSA44_GAMMA2: i32 = (q - 1) / 88; // mutants note: because of the bitshifting, the "- 1" ends up not mattering -pub(crate) const MLDSA44_k: usize = 4; -pub(crate) const MLDSA44_l: usize = 4; -pub(crate) const MLDSA44_ETA: usize = 2; -pub(crate) const MLDSA44_BETA: i32 = 78; -pub(crate) const MLDSA44_OMEGA: i32 = 80; - -// Useful derived values -pub(crate) const MLDSA44_C_TILDE: usize = 32; -pub(crate) const MLDSA44_POLY_Z_PACKED_LEN: usize = 576; -pub(crate) const MLDSA44_POLY_W1_PACKED_LEN: usize = 192; -pub(crate) const MLDSA44_LAMBDA_over_4: usize = 128 / 4; -pub(crate) const MLDSA44_GAMMA1_MINUS_BETA: i32 = MLDSA44_GAMMA1 - MLDSA44_BETA; -pub(crate) const MLDSA44_GAMMA2_MINUS_BETA: i32 = MLDSA44_GAMMA2 - MLDSA44_BETA; - -// Alg 32 -// 1: 𝑐 ← 1 + bitlen (𝛾1 − 1) -pub(crate) const MLDSA44_GAMMA1_MASK_LEN: usize = 576; // 32*(1 + bitlen (𝛾1 − 1) ) - -/* ML-DSA-65 params */ +pub const MLDSA44_SIG_LEN: usize = MLDSA44Params::SIG_LEN; /// Length of the \[u8] holding a ML-DSA-65 public key. -pub const MLDSA65_PK_LEN: usize = 1952; +pub const MLDSA65_PK_LEN: usize = MLDSA65Params::PK_LEN; /// Length of the \[u8] holding a ML-DSA-65 private key. -pub const MLDSA65_SK_LEN: usize = 4032; +pub const MLDSA65_SK_LEN: usize = MLDSA65Params::SK_LEN; /// Length of the \[u8] holding a ML-DSA-65 signature value. -pub const MLDSA65_SIG_LEN: usize = 3309; -pub(crate) const MLDSA65_TAU: i32 = 49; -pub(crate) const MLDSA65_LAMBDA: i32 = 192; -pub(crate) const MLDSA65_GAMMA1: i32 = 1 << 19; -pub(crate) const MLDSA65_GAMMA2: i32 = (q - 1) / 32; // mutants note: because of the bitshifting, the "- 1" ends up not mattering -pub(crate) const MLDSA65_k: usize = 6; -pub(crate) const MLDSA65_l: usize = 5; -pub(crate) const MLDSA65_ETA: usize = 4; -pub(crate) const MLDSA65_BETA: i32 = 196; -pub(crate) const MLDSA65_OMEGA: i32 = 55; - -// Useful derived values -pub(crate) const MLDSA65_C_TILDE: usize = 48; -pub(crate) const MLDSA65_POLY_Z_PACKED_LEN: usize = 640; -pub(crate) const MLDSA65_POLY_W1_PACKED_LEN: usize = 128; -pub(crate) const MLDSA65_LAMBDA_over_4: usize = 192 / 4; -pub(crate) const MLDSA65_GAMMA1_MINUS_BETA: i32 = MLDSA65_GAMMA1 - MLDSA65_BETA; -pub(crate) const MLDSA65_GAMMA2_MINUS_BETA: i32 = MLDSA65_GAMMA2 - MLDSA65_BETA; - -// Alg 32 -// 1: 𝑐 ← 1 + bitlen (𝛾1 − 1) -pub(crate) const MLDSA65_GAMMA1_MASK_LEN: usize = 640; - -/* ML-DSA-87 params */ +pub const MLDSA65_SIG_LEN: usize = MLDSA65Params::SIG_LEN; /// Length of the \[u8] holding a ML-DSA-87 public key. -pub const MLDSA87_PK_LEN: usize = 2592; +pub const MLDSA87_PK_LEN: usize = MLDSA87Params::PK_LEN; /// Length of the \[u8] holding a ML-DSA-87 private key. -pub const MLDSA87_SK_LEN: usize = 4896; +pub const MLDSA87_SK_LEN: usize = MLDSA87Params::SK_LEN; /// Length of the \[u8] holding a ML-DSA-87 signature value. -pub const MLDSA87_SIG_LEN: usize = 4627; -pub(crate) const MLDSA87_TAU: i32 = 60; -pub(crate) const MLDSA87_LAMBDA: i32 = 256; -pub(crate) const MLDSA87_GAMMA1: i32 = 1 << 19; -pub(crate) const MLDSA87_GAMMA2: i32 = (q - 1) / 32; // mutants note: because of the bitshifting, the "- 1" ends up not mattering -pub(crate) const MLDSA87_k: usize = 8; -pub(crate) const MLDSA87_l: usize = 7; -pub(crate) const MLDSA87_ETA: usize = 2; -pub(crate) const MLDSA87_BETA: i32 = 120; -pub(crate) const MLDSA87_OMEGA: i32 = 75; - -// Useful derived values -pub(crate) const MLDSA87_C_TILDE: usize = 64; -pub(crate) const MLDSA87_POLY_Z_PACKED_LEN: usize = 640; -pub(crate) const MLDSA87_POLY_W1_PACKED_LEN: usize = 128; -pub(crate) const MLDSA87_LAMBDA_over_4: usize = 256 / 4; -pub(crate) const MLDSA87_GAMMA1_MINUS_BETA: i32 = MLDSA87_GAMMA1 - MLDSA87_BETA; -pub(crate) const MLDSA87_GAMMA2_MINUS_BETA: i32 = MLDSA87_GAMMA2 - MLDSA87_BETA; - -// Alg 32 -// 1: 𝑐 ← 1 + bitlen (𝛾1 − 1) -pub(crate) const MLDSA87_GAMMA1_MASK_LEN: usize = 640; +pub const MLDSA87_SIG_LEN: usize = MLDSA87Params::SIG_LEN; // Typedefs just to make the algorithms look more like the FIPS 204 sample code. pub(crate) type H = SHAKE256; @@ -627,110 +561,63 @@ pub(crate) type G = SHAKE128; /// The ML-DSA-44 algorithm. pub type MLDSA44 = MLDSA< + MLDSA44Params, + MLDSA44PublicKey, + MLDSA44PrivateKey, MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44_SIG_LEN, - MLDSA44PublicKey, - MLDSA44PrivateKey, - MLDSA44_TAU, - MLDSA44_LAMBDA, - MLDSA44_GAMMA1, - MLDSA44_GAMMA2, - MLDSA44_k, - MLDSA44_l, - MLDSA44_ETA, - MLDSA44_BETA, - MLDSA44_OMEGA, - MLDSA44_C_TILDE, - MLDSA44_POLY_Z_PACKED_LEN, - MLDSA44_POLY_W1_PACKED_LEN, - MLDSA44_LAMBDA_over_4, - MLDSA44_GAMMA1_MINUS_BETA, - MLDSA44_GAMMA2_MINUS_BETA, - MLDSA44_GAMMA1_MASK_LEN, >; -impl Algorithm for MLDSA44 { - const ALG_NAME: &'static str = ML_DSA_44_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} -/// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-44 { sigAlgs 17 } -impl AlgorithmOID for MLDSA44 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 17]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x11]; -} - /// The ML-DSA-65 algorithm. pub type MLDSA65 = MLDSA< + MLDSA65Params, + MLDSA65PublicKey, + MLDSA65PrivateKey, MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65_SIG_LEN, - MLDSA65PublicKey, - MLDSA65PrivateKey, - MLDSA65_TAU, - MLDSA65_LAMBDA, - MLDSA65_GAMMA1, - MLDSA65_GAMMA2, - MLDSA65_k, - MLDSA65_l, - MLDSA65_ETA, - MLDSA65_BETA, - MLDSA65_OMEGA, - MLDSA65_C_TILDE, - MLDSA65_POLY_Z_PACKED_LEN, - MLDSA65_POLY_W1_PACKED_LEN, - MLDSA65_LAMBDA_over_4, - MLDSA65_GAMMA1_MINUS_BETA, - MLDSA65_GAMMA2_MINUS_BETA, - MLDSA65_GAMMA1_MASK_LEN, >; -impl Algorithm for MLDSA65 { - const ALG_NAME: &'static str = ML_DSA_65_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; -} -/// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-65 { sigAlgs 18 } -impl AlgorithmOID for MLDSA65 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 18]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12]; -} - /// The ML-DSA-87 algorithm. pub type MLDSA87 = MLDSA< + MLDSA87Params, + MLDSA87PublicKey, + MLDSA87PrivateKey, MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87_SIG_LEN, - MLDSA87PublicKey, - MLDSA87PrivateKey, - MLDSA87_TAU, - MLDSA87_LAMBDA, - MLDSA87_GAMMA1, - MLDSA87_GAMMA2, - MLDSA87_k, - MLDSA87_l, - MLDSA87_ETA, - MLDSA87_BETA, - MLDSA87_OMEGA, - MLDSA87_C_TILDE, - MLDSA87_POLY_Z_PACKED_LEN, - MLDSA87_POLY_W1_PACKED_LEN, - MLDSA87_LAMBDA_over_4, - MLDSA87_GAMMA1_MINUS_BETA, - MLDSA87_GAMMA2_MINUS_BETA, - MLDSA87_GAMMA1_MASK_LEN, >; -impl Algorithm for MLDSA87 { - const ALG_NAME: &'static str = ML_DSA_87_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +/// The name and claimed strength of an ML-DSA algorithm are properties of its parameter set, so +/// one impl covers all three; `MLDSAParams` is sealed, so those are the only three that exist. +impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +> Algorithm for MLDSA +{ + const ALG_NAME: &'static str = P::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -/// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-87 { sigAlgs 19 } -impl AlgorithmOID for MLDSA87 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 19]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x13]; + +/// The OIDs NIST assigned in the Computer Security Objects Register: id-ml-dsa-44 { sigAlgs 17 }, +/// id-ml-dsa-65 { sigAlgs 18 } and id-ml-dsa-87 { sigAlgs 19 }. As with [`Algorithm`], the values +/// belong to the parameter set, so one impl covers all three. +impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +> AlgorithmOID for MLDSA +{ + const OID: &'static [u32] = P::OID; + const OID_DER: &'static [u8] = P::OID_DER; } /// The core internal implementation of the ML-DSA algorithm. @@ -738,30 +625,14 @@ impl AlgorithmOID for MLDSA87 { /// but it shouldn't ever need to be used directly. /// Please use the named public types [`MLDSA44`], [`MLDSA65`], [`MLDSA87`] instead. pub struct MLDSA< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_VEC_H_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, > { - _phantom: PhantomData<(PK, SK)>, + _phantom: PhantomData<(P, PK, SK)>, /// used for streaming the message for both signing and verifying mu_builder: MuBuilder, @@ -779,52 +650,13 @@ pub struct MLDSA< } impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> - MLDSA< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> MLDSA { /// Implements Algorithm 6 of FIPS 204 /// Note: NIST has made a special exception in the FIPS 204 FAQ that this _internal function @@ -844,7 +676,7 @@ impl< )); } - if seed.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if seed.security_strength() < SecurityStrength::from_bits(P::LAMBDA as usize) { return Err(SignatureError::KeyGenError( "Seed SecurityStrength must match algorithm security strength", )); @@ -859,8 +691,8 @@ impl< // scope for h let mut h = H::default(); h.absorb(seed.ref_to_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(l as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); + h.absorb(&(P::k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); + h.absorb(&(P::l as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); let bytes_written = h.squeeze_out(&mut rho); debug_assert_eq!(bytes_written, 32); let mut rho_prime: [u8; 64] = [0u8; 64]; @@ -870,7 +702,7 @@ impl< debug_assert_eq!(bytes_written, 32); // 4: (𝐬1, 𝐬2) ← ExpandS(𝜌′) - let (mut s1, s2) = expandS::(&rho_prime); + let (mut s1, s2) = expandS::

(&rho_prime); s1.ntt(); (s1, s2) @@ -879,7 +711,7 @@ impl< let t_hat = { // scope for s1_hat // 3: 𝐀_hat ← ExpandA(𝜌) ▷ 𝐀 is generated and stored in NTT representation as 𝐀 - let A_hat = expandA::(&rho); + let A_hat = expandA::

(&rho); // 5: 𝐭 ← NTT−1(𝐀 ∘ NTT(𝐬1)) + 𝐬2 // ▷ compute 𝐭 = 𝐀𝐬1 + 𝐬2 @@ -896,7 +728,7 @@ impl< // 6: (𝐭1, 𝐭0) ← Power2Round(𝐭) // ▷ compress 𝐭 // ▷ PowerTwoRound is applied componentwise (see explanatory text in Section 7.4) - power_2_round_vec::(&t) + power_2_round_vec(&t) }; // 8: 𝑝𝑘 ← pkEncode(𝜌, 𝐭1) @@ -928,7 +760,7 @@ impl< /// modified to take an externally-computed mu instead of M', and to take the public matrix A_hat fn sign_internal( sk: &SK, - A_hat: &Matrix, + A_hat: &P::MatrixA, mu: &[u8; 64], rnd: [u8; 32], output: &mut [u8; SIG_LEN], @@ -972,22 +804,22 @@ impl< // ▷ rejection sampling loop // these need to be outside the loop because they form the encoded signature value - let mut sig_val_c_tilde = [0u8; LAMBDA_over_4]; - let mut sig_val_z: Vector; - let mut sig_val_h: Vector; + let mut sig_val_c_tilde = ::ZEROED; + let mut sig_val_z: P::VecL; + let mut sig_val_h: P::VecK; loop { // FIPS 204 s. 6.2 allows: // "Implementations may limit the number of iterations in this loop to not exceed a finite maximum value." // mutants note: there is no test for this because, at this point, // we don't know of a KAT that will exceed this limit. - if kappa > 1000 * k as u16 { + if kappa > 1000 * P::k as u16 { return Err(SignatureError::GenericError( "Rejection sampling loop exceeded max iterations, try again with a different signing nonce.", )); } // 11: 𝐲 ∈ 𝑅^ℓ ← ExpandMask(𝜌″, 𝜅) - let mut y = expand_mask::(&rho_p_p, kappa); + let mut y = expand_mask::

(&rho_p_p, kappa); let w = { // scope for y_hat @@ -1002,7 +834,7 @@ impl< // 13: 𝐰1 ← HighBits(𝐰) // ▷ signer’s commitment - let w1 = w.high_bits::(); + let w1 = w.high_bits::

(); { // scope for h @@ -1010,15 +842,15 @@ impl< // ▷ commitment hash let mut hash = H::new(); hash.absorb(mu).expect("absorb before squeeze is infallible"); - w1.w1_encode_and_hash::(&mut hash); - hash.squeeze_out(&mut sig_val_c_tilde); + w1.w1_encode_and_hash::

(&mut hash); + hash.squeeze_out(sig_val_c_tilde.as_mut()); } // 16: 𝑐 ∈ 𝑅𝑞 ← SampleInBall(c_tilde) // ▷ verifier’s challenge let c_hat = { // scope for c - let mut c = sample_in_ball::(&sig_val_c_tilde); + let mut c = sample_in_ball::

(&sig_val_c_tilde); // 17: 𝑐_hat ← NTT(𝑐) c.ntt(); @@ -1038,8 +870,8 @@ impl< // ▷ validity checks // This is done out-of-order on purpose for performance reasons: // rejection sampling check is done before any extra heavy computation - if sig_val_z.check_norm::() { - kappa += l as u16; + if sig_val_z.check_norm(P::GAMMA1_MINUS_BETA) { + kappa += P::l as u16; continue; }; @@ -1048,7 +880,7 @@ impl< cs2.inv_ntt(); // 21: 𝐫0 ← LowBits(𝐰 − ⟨⟨𝑐𝐬2⟩⟩) - let mut r0 = w.sub_vector(&cs2).low_bits::(); + let mut r0 = w.sub_vector(&cs2).low_bits::

(); // 23 (second half): if ||𝐳||∞ ≥ 𝛾1 − 𝛽 or ||𝐫0||∞ ≥ 𝛾2 − 𝛽 then (z, h) ← ⊥ // ▷ validity checks @@ -1058,8 +890,8 @@ impl< // and checking whether ‖r0‖∞ < γ2 − β and r1 = w1, it is equivalent to just check that // ‖w0 − cs2‖∞ < γ2 − β, where w0 is the low part of w. If this check passes, w0 − cs2 // is the low part of w − cs2." - if r0.check_norm::() { - kappa += l as u16; + if r0.check_norm(P::GAMMA2_MINUS_BETA) { + kappa += P::l as u16; continue; }; @@ -1071,8 +903,8 @@ impl< // This is done out-of-order on purpose for performance reasons: // rejection sampling check is done before any extra heavy computation // mutants note: there is currently no unit test that triggers this branch - if ct0.check_norm::() { - kappa += l as u16; + if ct0.check_norm(P::GAMMA2) { + kappa += P::l as u16; continue; }; @@ -1083,15 +915,15 @@ impl< let hint_hamming_weight: i32; sig_val_h = { // scope for hint - let (hint, inner_hint_hamming_weight) = make_hint_vecs::(&r0, &w1); + let (hint, inner_hint_hamming_weight) = make_hint_vecs::

(&r0, &w1); hint_hamming_weight = inner_hint_hamming_weight; hint }; // 28 (second half): if ||⟨⟨𝑐𝐭0⟩⟩||∞ ≥ 𝛾2 or the number of 1’s in 𝐡 is greater than 𝜔, then (z, h) ← ⊥ // mutants note: there is no test KAT that triggers this branch - if hint_hamming_weight > OMEGA { - kappa += l as u16; + if hint_hamming_weight > P::OMEGA { + kappa += P::l as u16; continue; }; @@ -1106,9 +938,7 @@ impl< // 33: 𝜎 ← sigEncode(𝑐, 𝐳̃ mod±𝑞, 𝐡) let bytes_written = - sig_encode::( - &sig_val_c_tilde, &sig_val_z, &sig_val_h, output, - ); + sig_encode::(&sig_val_c_tilde, &sig_val_z, &sig_val_h, output); Ok(bytes_written) } @@ -1119,7 +949,7 @@ impl< /// Input: Signature 𝜎 ∈ 𝔹𝜆/4+ℓ⋅32⋅(1+bitlen (𝛾1−1))+𝜔+𝑘. fn verify_internal( pk: &PK, - A_hat: &Matrix, + A_hat: &P::MatrixA, mu: &[u8; 64], sig: &[u8; SIG_LEN], ) -> Result<(), SignatureError> { @@ -1129,12 +959,11 @@ impl< // 2: (𝑐_tilde, 𝐳, 𝐡) ← sigDecode(𝜎) // ▷ signer’s commitment hash c_tilde, response 𝐳, and hint 𝐡 // 3: if 𝐡 = ⊥ then return false - let (c_tilde, z, h) = - sig_decode::(&sig) - .map_err(|_| SignatureError::SignatureVerificationFailed)?; + let (c_tilde, z, h) = sig_decode::(&sig) + .map_err(|_| SignatureError::SignatureVerificationFailed)?; // 13 (first half) return [[ ||𝐳||∞ < 𝛾1 − 𝛽]] - if z.check_norm::() { + if z.check_norm(P::GAMMA1_MINUS_BETA) { return Err(SignatureError::SignatureVerificationFailed); } @@ -1151,7 +980,7 @@ impl< // 8: 𝑐 ∈ 𝑅𝑞 ← SampleInBall(c_tilde) let c_hat = { - let mut c = sample_in_ball::(&c_tilde); + let mut c = sample_in_ball::

(&c_tilde); c.ntt(); c @@ -1173,7 +1002,7 @@ impl< }; let ct1 = { // potential optimization -- pre-compute this on key load? - let mut t1_shift_hat = pk.t1().shift_left::(); + let mut t1_shift_hat = pk.t1().shift_left_d(); t1_shift_hat.ntt(); t1_shift_hat.scalar_vector_ntt(&c_hat) }; @@ -1183,23 +1012,23 @@ impl< // 10: 𝐰1′ ← UseHint(𝐡, 𝐰'_approx) // ▷ reconstruction of signer’s commitment - use_hint_vecs::(&h, &wp_approx) + use_hint_vecs::

(&h, &wp_approx) }; // 12: 𝑐_tilde_p ← H(𝜇||w1Encode(𝐰1'), 𝜆/4) // ▷ hash it; this should match 𝑐_tilde let c_tilde_p = { - let mut c_tilde_p = [0u8; LAMBDA_over_4]; + let mut c_tilde_p = ::ZEROED; let mut hash = H::new(); hash.absorb(mu).expect("absorb before squeeze is infallible"); - w1p.w1_encode_and_hash::(&mut hash); - hash.squeeze_out(&mut c_tilde_p); + w1p.w1_encode_and_hash::

(&mut hash); + hash.squeeze_out(c_tilde_p.as_mut()); c_tilde_p }; // verification probably doesn't technically need to be constant-time, but why not? // 13 (second half): return [[ ||𝐳||∞ < 𝛾1 − 𝛽]] and [[𝑐 ̃ = 𝑐′ ]] - if bouncycastle_utils::ct::ct_eq_bytes(&c_tilde, &c_tilde_p) { + if bouncycastle_utils::ct::ct_eq_bytes(c_tilde.as_ref(), c_tilde_p.as_ref()) { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) @@ -1208,52 +1037,13 @@ impl< } impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> MLDSATrait - for MLDSA< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> MLDSATrait for MLDSA { fn keygen_from_seed(seed: &KeyMaterial<32>) -> Result<(PK, SK), SignatureError> { Self::keygen_internal(seed) @@ -1290,21 +1080,21 @@ impl< MuBuilder::compute_mu(tr, msg, ctx) } fn compute_mu_from_pk( - pk: &impl MLDSAPublicKeyTrait, + pk: &impl MLDSAPublicKeyTrait, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; 64], SignatureError> { MuBuilder::compute_mu(&pk.compute_tr(), msg, ctx) } fn compute_mu_from_sk( - sk: &impl MLDSAPrivateKeyTrait, + sk: &impl MLDSAPrivateKeyTrait, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; 64], SignatureError> { MuBuilder::compute_mu(&sk.tr(), msg, ctx) } fn sign_with_expanded_key( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; SIG_LEN], SignatureError> { @@ -1313,7 +1103,7 @@ impl< } fn sign_with_expanded_key_out( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, out: &mut [u8; SIG_LEN], @@ -1326,7 +1116,7 @@ impl< fn sign_mu( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], ) -> Result<[u8; SIG_LEN], SignatureError> { let mut out: [u8; SIG_LEN] = [0u8; SIG_LEN]; @@ -1336,7 +1126,7 @@ impl< } fn sign_mu_out( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], output: &mut [u8; SIG_LEN], ) -> Result { @@ -1348,8 +1138,8 @@ impl< Self::sign_mu_deterministic_out(sk, A_hat, mu, rnd, output) } fn sign_mu_with_expanded_key( - sk: &MLDSAPrivateKeyExpanded, - A_hat: Option<&Matrix>, + sk: &MLDSAPrivateKeyExpanded, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], ) -> Result<[u8; SIG_LEN], SignatureError> { let mut out: [u8; SIG_LEN] = [0u8; SIG_LEN]; @@ -1358,8 +1148,8 @@ impl< Ok(out) } fn sign_mu_with_expanded_key_out( - sk: &MLDSAPrivateKeyExpanded, - A_hat: Option<&Matrix>, + sk: &MLDSAPrivateKeyExpanded, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], out: &mut [u8; SIG_LEN], ) -> Result { @@ -1370,7 +1160,7 @@ impl< fn sign_mu_deterministic( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], rnd: [u8; 32], ) -> Result<[u8; SIG_LEN], SignatureError> { @@ -1381,7 +1171,7 @@ impl< } fn sign_mu_deterministic_out( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], rnd: [u8; 32], output: &mut [u8; SIG_LEN], @@ -1409,7 +1199,7 @@ impl< /// This is a middle ground between keygen_from_seed()+sign_mu() and /// the fully streamed low-memory implementation. // TODO: benchmark peak memory + runtime against - // keygen_from_seed() + sign_mu_deterministic() to confirm the separate path earns being kept. + // keygen_from_seed() + sign_mu_deterministic() to confirm the separate path earns being kept. // Note: this path intentionally avoids the public key entirely // (no pkEncode / tr = H(pk)) since μ is supplied externally. fn sign_mu_deterministic_from_seed_out( @@ -1436,7 +1226,7 @@ impl< )); } - if seed.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if seed.security_strength() < SecurityStrength::from_bits(P::LAMBDA as usize) { return Err(SignatureError::KeyGenError( "Seed SecurityStrength must match algorithm security strength: 128-bit (ML-DSA-44), 192-bit (ML-DSA-65), or 256-bit (ML-DSA-87).", )); @@ -1453,8 +1243,8 @@ impl< let (rho, rho_prime, K) = { let mut h = H::default(); h.absorb(seed.ref_to_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(l as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); + h.absorb(&(P::k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); + h.absorb(&(P::l as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); let mut rho = [0u8; 32]; let bytes_written = h.squeeze_out(&mut rho); debug_assert_eq!(bytes_written, 32); @@ -1481,7 +1271,7 @@ impl< }; // 4: (𝐬1, 𝐬2) ← ExpandS(𝜌′) - let (s1, s2) = expandS::(&rho_prime); + let (s1, s2) = expandS::

(&rho_prime); (rho, rho_p_p, s1, s2) }; @@ -1494,7 +1284,7 @@ impl< // as 20 or even 80 times. So moving expandA() inside the loop would be a pretty drastic speed-for-memory tradeoff // whose generality falls out of the scope of this implementation. // It is left as an optimization that can be made by users that require further reduction of memory usage - let A_hat = expandA::(&rho); + let A_hat = expandA::

(&rho); // Alg 7; 8: 𝜅 ← 0 // ▷ initialize counter 𝜅 @@ -1507,21 +1297,21 @@ impl< // ▷ rejection sampling loop // these need to be outside the loop because they form the encoded signature value - let mut sig_val_c_tilde = [0u8; LAMBDA_over_4]; - let mut sig_val_z: Vector; - let mut sig_val_h: Vector; + let mut sig_val_c_tilde = ::ZEROED; + let mut sig_val_z: P::VecL; + let mut sig_val_h: P::VecK; loop { // FIPS 204 s. 6.2 allows: // "Implementations may limit the number of iterations in this loop to not exceed a finite maximum value." // mutants note: there is no test for this because we don't know of a KAT that will exceed this limit. - if kappa > 1000 * k as u16 { + if kappa > 1000 * P::k as u16 { return Err(SignatureError::GenericError( "Rejection sampling loop exceeded max iterations, try again with a different signing nonce.", )); } // Alg 7; 11: 𝐲 ∈ 𝑅^ℓ ← ExpandMask(𝜌″, 𝜅) - let mut y = expand_mask::(&rho_p_p, kappa); + let mut y = expand_mask::

(&rho_p_p, kappa); let w = { // scope for y_hat @@ -1536,7 +1326,7 @@ impl< // Alg 7; 13: 𝐰1 ← HighBits(𝐰) // ▷ signer’s commitment - let w1 = w.high_bits::(); + let w1 = w.high_bits::

(); { // scope for h @@ -1544,22 +1334,22 @@ impl< // ▷ commitment hash let mut hash = H::new(); hash.absorb(mu).expect("absorb before squeeze is infallible"); - w1.w1_encode_and_hash::(&mut hash); - hash.squeeze_out(&mut sig_val_c_tilde); + w1.w1_encode_and_hash::

(&mut hash); + hash.squeeze_out(sig_val_c_tilde.as_mut()); } // Alg 7; 16: 𝑐 ∈ 𝑅𝑞 ← SampleInBall(c_tilde) // ▷ verifier’s challenge let c_hat = { // scope for c - let mut c = sample_in_ball::(&sig_val_c_tilde); + let mut c = sample_in_ball::

(&sig_val_c_tilde); // 17: 𝑐_hat ← NTT(𝑐) c.ntt(); c }; - let t_hat: Vector; + let t_hat: P::VecK; sig_val_z = { // scope for s1_hat, cs1 // Alg 7; 2: 𝐬1̂_hat ← NTT(𝐬1) @@ -1591,13 +1381,13 @@ impl< // ▷ validity checks // This is done out-of-order on purpose for performance reasons: // rejection sampling check is done before any extra heavy computation - if sig_val_z.check_norm::() { - kappa += l as u16; + if sig_val_z.check_norm(P::GAMMA1_MINUS_BETA) { + kappa += P::l as u16; continue; }; - let t0: Vector; - let mut r0: Vector = { + let t0: P::VecK; + let mut r0: P::VecK = { // scope for s2_hat and cs2 // 3: 𝐬2̂_hat ← NTT(𝐬2) let mut s2_hat = s2.clone(); @@ -1608,7 +1398,7 @@ impl< cs2.inv_ntt(); // 21: 𝐫0 ← LowBits(𝐰 − ⟨⟨𝑐𝐬2⟩⟩) - let r0 = w.sub_vector(&cs2).low_bits::(); + let r0 = w.sub_vector(&cs2).low_bits::

(); // while s2_hat is in scope, derive t0 let mut t = t_hat; @@ -1619,7 +1409,7 @@ impl< // 6: (𝐭1, 𝐭0) ← Power2Round(𝐭) // ▷ compress 𝐭 // ▷ PowerTwoRound is applied componentwise (see explanatory text in Section 7.4) - let (_t1tmp, t0tmp) = power_2_round_vec::(&t); + let (_t1tmp, t0tmp) = power_2_round_vec(&t); t0 = t0tmp; r0 @@ -1627,14 +1417,14 @@ impl< // Alg 7; 23 (second half): if ||𝐳||∞ ≥ 𝛾1 − 𝛽 or ||𝐫0||∞ ≥ 𝛾2 − 𝛽 then (z, h) ← ⊥ // ▷ validity checks - if r0.check_norm::() { + if r0.check_norm(P::GAMMA2_MINUS_BETA) { // mutants note: mutants thinks this can be replaced with -=, but in practice that makes // the rejection sampling loop go forever, so is a false positive. - kappa += l as u16; + kappa += P::l as u16; continue; }; - let ct0: Vector = { + let ct0: P::VecK = { // scope for t0_hat // 4: 𝐭0̂_hat ← NTT(𝐭0)̂ let mut t0_hat = t0.clone(); @@ -1650,8 +1440,8 @@ impl< // out-of-order on purpose for performance reasons: // might as well do the rejection sampling check before any extra heavy computation // mutants note: there is currently no unit test that triggers this branch - if ct0.check_norm::() { - kappa += l as u16; + if ct0.check_norm(P::GAMMA2) { + kappa += P::l as u16; continue; }; @@ -1662,15 +1452,15 @@ impl< let hint_hamming_weight: i32; sig_val_h = { // scope for hint - let (hint, inner_hint_hamming_weight) = make_hint_vecs::(&r0, &w1); + let (hint, inner_hint_hamming_weight) = make_hint_vecs::

(&r0, &w1); hint_hamming_weight = inner_hint_hamming_weight; hint }; // Alg 7; 28 (second half): if ||⟨⟨𝑐𝐭0⟩⟩||∞ ≥ 𝛾2 or the number of 1’s in 𝐡 is greater than 𝜔, then (z, h) ← ⊥ // mutants note: there is currently no unit test that triggers this branch - if hint_hamming_weight > OMEGA { - kappa += l as u16; + if hint_hamming_weight > P::OMEGA { + kappa += P::l as u16; continue; }; @@ -1686,9 +1476,7 @@ impl< // Alg 7; 33: 𝜎 ← sigEncode(𝑐, 𝐳̃ mod±𝑞, 𝐡) let bytes_written = - sig_encode::( - &sig_val_c_tilde, &sig_val_z, &sig_val_h, output, - ); + sig_encode::(&sig_val_c_tilde, &sig_val_z, &sig_val_h, output); Ok(bytes_written) } @@ -1711,7 +1499,7 @@ impl< } fn verify_with_expanded_key( - pk: &MLDSAPublicKeyExpanded, + pk: &MLDSAPublicKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8], @@ -1725,7 +1513,7 @@ impl< fn verify_mu( pk: &PK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], sig: &[u8; SIG_LEN], ) -> Result<(), SignatureError> { @@ -1738,16 +1526,12 @@ impl< /// Trait for all three of the ML-DSA algorithm variants. pub trait MLDSATrait< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const LAMBDA: i32, - const k: usize, - const l: usize, - const ETA: usize, >: Sized { /// Runs a key generation using the library's default RNG, seeded from the OS. @@ -1762,7 +1546,7 @@ pub trait MLDSATrait< // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), SignatureError> { // Source the seed from the provided RNG - if rng.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if rng.security_strength() < SecurityStrength::from_bits(P::LAMBDA as usize) { return Err(RNGError::SecurityStrengthInsufficientForAlgorithm)?; } let mut seed = KeyMaterial256::new(); @@ -1829,26 +1613,26 @@ pub trait MLDSATrait< ) -> Result<[u8; 64], SignatureError>; /// Same as [`MLDSATrait::compute_mu_from_tr`], but extracts tr from the public key. fn compute_mu_from_pk( - pk: &impl MLDSAPublicKeyTrait, + pk: &impl MLDSAPublicKeyTrait, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; 64], SignatureError>; /// Same as [`MLDSATrait::compute_mu_from_tr`], but extracts tr from the private key. // dev note: defined sk this way so that it accepts either MLDSAPrivateKey or MLDSAPRivateKeyExpanded fn compute_mu_from_sk( - sk: &impl MLDSAPrivateKeyTrait, + sk: &impl MLDSAPrivateKeyTrait, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; 64], SignatureError>; /// Same as [`Signer::sign`], but signs from an [`MLDSAPrivateKeyExpanded`]. fn sign_with_expanded_key( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; SIG_LEN], SignatureError>; /// Same as [`MLDSATrait::sign_with_expanded_key`], but takes an output array. fn sign_with_expanded_key_out( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, out: &mut [u8; SIG_LEN], @@ -1861,7 +1645,7 @@ pub trait MLDSATrait< /// Optionally, takes a pre-expanded public matrix `A_hat`. fn sign_mu( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], ) -> Result<[u8; SIG_LEN], SignatureError>; /// Performs an ML-DSA signature using the provided external message representative `mu`. @@ -1876,20 +1660,20 @@ pub trait MLDSATrait< /// Returns the number of bytes written to the output buffer. Can be called with an oversized buffer. fn sign_mu_out( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], output: &mut [u8; SIG_LEN], ) -> Result; /// Same as [`MLDSATrait::sign_mu`], but signs from an [`MLDSAPrivateKeyExpanded`]. fn sign_mu_with_expanded_key( - sk: &MLDSAPrivateKeyExpanded, - A_hat: Option<&Matrix>, + sk: &MLDSAPrivateKeyExpanded, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], ) -> Result<[u8; SIG_LEN], SignatureError>; /// Same as [`MLDSATrait::sign_mu_out`], but signs from an [`MLDSAPrivateKeyExpanded`]. fn sign_mu_with_expanded_key_out( - sk: &MLDSAPrivateKeyExpanded, - A_hat: Option<&Matrix>, + sk: &MLDSAPrivateKeyExpanded, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], output: &mut [u8; SIG_LEN], ) -> Result; @@ -1916,7 +1700,7 @@ pub trait MLDSATrait< /// prevent accidental nonce reuse, this function moves `rnd`. fn sign_mu_deterministic( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], rnd: [u8; 32], ) -> Result<[u8; SIG_LEN], SignatureError>; @@ -1945,7 +1729,7 @@ pub trait MLDSATrait< /// Returns the number of bytes written to the output buffer. Can be called with an oversized buffer. fn sign_mu_deterministic_out( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], rnd: [u8; 32], output: &mut [u8; SIG_LEN], @@ -1978,7 +1762,7 @@ pub trait MLDSATrait< ) -> Result; /// Same as [`SignatureVerifier::verify`], but signs from an expanded key object. fn verify_with_expanded_key( - pk: &MLDSAPublicKeyExpanded, + pk: &MLDSAPublicKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8], @@ -1989,59 +1773,20 @@ pub trait MLDSATrait< /// Optionally, takes a pre-expanded public matrix `A_hat`. fn verify_mu( pk: &PK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], sig: &[u8; SIG_LEN], ) -> Result<(), SignatureError>; } impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> Signer - for MLDSA< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> Signer for MLDSA { fn sign(sk: &SK, msg: &[u8], ctx: Option<&[u8]>) -> Result<[u8; SIG_LEN], SignatureError> { let mut out = [0u8; SIG_LEN]; @@ -2125,52 +1870,13 @@ impl< } impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> SignatureVerifier - for MLDSA< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> SignatureVerifier for MLDSA { fn verify(pk: &PK, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8]) -> Result<(), SignatureError> { let mu = MuBuilder::compute_mu(&pk.compute_tr(), msg, ctx)?; diff --git a/crypto/mldsa/src/mldsa_keys.rs b/crypto/mldsa/src/mldsa_keys.rs index 456a26c8..f84f199a 100644 --- a/crypto/mldsa/src/mldsa_keys.rs +++ b/crypto/mldsa/src/mldsa_keys.rs @@ -2,13 +2,10 @@ use crate::aux_functions::{ bit_pack_eta, bit_pack_t0, bit_unpack_eta, bit_unpack_t0, bitlen_eta, expandA, power_2_round_vec, simple_bit_pack_t1, simple_bit_unpack_t1, }; -use crate::matrix::{Matrix, Vector}; +use crate::matrix::{MatrixTrait, VectorTrait}; use crate::mldsa::H; -use crate::mldsa::{MLDSA44_ETA, MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44_k, MLDSA44_l}; -use crate::mldsa::{MLDSA65_ETA, MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65_k, MLDSA65_l}; -use crate::mldsa::{MLDSA87_ETA, MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87_k, MLDSA87_l}; use crate::mldsa::{POLY_T0PACKED_LEN, POLY_T1PACKED_LEN}; -use crate::{ML_DSA_44_NAME, ML_DSA_65_NAME, ML_DSA_87_NAME}; +use crate::params::{MLDSA44Params, MLDSA65Params, MLDSA87Params, MLDSAParams}; use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey, XOF}; @@ -25,71 +22,83 @@ use crate::polynomial::Polynomial; /* Pub Types */ /// ML-DSA-44 Public Key -pub type MLDSA44PublicKey = MLDSAPublicKey; +pub type MLDSA44PublicKey = MLDSAPublicKey; /// ML-DSA-44 Private Key pub type MLDSA44PrivateKey = - MLDSAPrivateKey; + MLDSAPrivateKey; /// ML-DSA-65 Public Key -pub type MLDSA65PublicKey = MLDSAPublicKey; +pub type MLDSA65PublicKey = MLDSAPublicKey; /// ML-DSA-65 Private Key pub type MLDSA65PrivateKey = - MLDSAPrivateKey; + MLDSAPrivateKey; /// ML-DSA-87 Public Key -pub type MLDSA87PublicKey = MLDSAPublicKey; +pub type MLDSA87PublicKey = MLDSAPublicKey; /// ML-DSA-87 Private Key pub type MLDSA87PrivateKey = - MLDSAPrivateKey; + MLDSAPrivateKey; /* Pre-expanded keys for repeated operations */ /// ML-DSA-44 Public Key with a pre-expanded public matrix A for repeated encaps operations. pub type MLDSA44PublicKeyExpanded = - MLDSAPublicKeyExpanded; + MLDSAPublicKeyExpanded; /// ML-DSA-44 Private Key with a pre-expanded public matrix A for repeated decaps operations. pub type MLDSA44PrivateKeyExpanded = MLDSAPrivateKeyExpanded< - MLDSA44_k, - MLDSA44_l, - MLDSA44_ETA, + MLDSA44Params, MLDSA44PublicKey, MLDSA44PrivateKey, - MLDSA44_SK_LEN, - MLDSA44_PK_LEN, + { MLDSA44Params::SK_LEN }, + { MLDSA44Params::PK_LEN }, >; /// ML-DSA-65 Public Key with a pre-expanded public matrix A for repeated encaps operations. pub type MLDSA65PublicKeyExpanded = - MLDSAPublicKeyExpanded; + MLDSAPublicKeyExpanded; /// ML-DSA-65 Private Key with a pre-expanded public matrix A for repeated decaps operations. pub type MLDSA65PrivateKeyExpanded = MLDSAPrivateKeyExpanded< - MLDSA65_k, - MLDSA65_l, - MLDSA65_ETA, + MLDSA65Params, MLDSA65PublicKey, MLDSA65PrivateKey, - MLDSA65_SK_LEN, - MLDSA65_PK_LEN, + { MLDSA65Params::SK_LEN }, + { MLDSA65Params::PK_LEN }, >; /// ML-DSA-87 Public Key with a pre-expanded public matrix A for repeated encaps operations. pub type MLDSA87PublicKeyExpanded = - MLDSAPublicKeyExpanded; + MLDSAPublicKeyExpanded; /// ML-DSA-87 Private Key with a pre-expanded public matrix A for repeated decaps operations. pub type MLDSA87PrivateKeyExpanded = MLDSAPrivateKeyExpanded< - MLDSA87_k, - MLDSA87_l, - MLDSA87_ETA, + MLDSA87Params, MLDSA87PublicKey, MLDSA87PrivateKey, - MLDSA87_SK_LEN, - MLDSA87_PK_LEN, + { MLDSA87Params::SK_LEN }, + { MLDSA87Params::PK_LEN }, >; /// An ML-DSA public key. -#[derive(Clone)] -pub struct MLDSAPublicKey { +/// +/// `PK_LEN` duplicates [`MLDSAParams::PK_LEN`]; it has to be carried separately because +/// [`SignaturePublicKey`] takes the encoded length as a const generic parameter, and an associated +/// const of a type parameter may not be used as a const generic argument. The two are checked +/// against each other at compile time -- see the `ASSERT_PK_LEN` const on this type. +pub struct MLDSAPublicKey { rho: [u8; 32], - t1: Vector, + t1: P::VecK, +} + +// Written out rather than derived: `#[derive(Clone)]` would demand `P: Clone`, and `P` is a +// marker for the parameter set that is never stored, only used to name the field types. +impl Clone for MLDSAPublicKey { + fn clone(&self) -> Self { + Self { rho: self.rho, t1: self.t1 } + } } -impl MLDSAPublicKey { +impl MLDSAPublicKey { + /// Fails to evaluate, and so fails the build, if this type is instantiated with a `PK_LEN` + /// that is not the parameter set's own. Referenced from every constructor so that it is + /// reached during monomorphization. + const ASSERT_PK_LEN: () = + assert!(PK_LEN == P::PK_LEN, "PK_LEN does not match MLDSAParams::PK_LEN"); + /// Algorithm 22 pkEncode(𝜌, 𝐭1) /// Encodes a public key for ML-DSA into a byte string. /// Input:𝜌 ∈ 𝔹32, 𝐭1 ∈ 𝑅𝑘 with coefficients in [0, 2bitlen (𝑞−1)−𝑑 − 1]. @@ -102,11 +111,11 @@ impl MLDSAPublicKey(); // that should divide evenly the remainder of the array - debug_assert_eq!(pk_chunks.len(), k); + debug_assert_eq!(pk_chunks.len(), P::k); debug_assert_eq!(last_chunk.len(), 0); - for (pk_chunk, t1_i) in pk_chunks.into_iter().zip(&self.t1.elems) { - pk_chunk.copy_from_slice(&simple_bit_pack_t1(&t1_i)); + for (pk_chunk, t1_i) in pk_chunks.into_iter().zip(self.t1.elems()) { + pk_chunk.copy_from_slice(&simple_bit_pack_t1(t1_i)); } PK_LEN @@ -114,7 +123,7 @@ impl MLDSAPublicKey: +pub trait MLDSAPublicKeyTrait: SignaturePublicKey { /// Algorithm 23 pkDecode(𝑝𝑘) @@ -124,7 +133,7 @@ pub trait MLDSAPublicKeyTrait Self; /// Get a copy of the expanded public matrix A_hat - fn A_hat(&self) -> Matrix; + fn A_hat(&self) -> P::MatrixA; /// Compute the public key hash (tr) from the public key. /// @@ -135,43 +144,43 @@ pub trait MLDSAPublicKeyTrait [u8; 64]; } -pub(crate) trait MLDSAPublicKeyInternalTrait: +pub(crate) trait MLDSAPublicKeyInternalTrait: SignaturePublicKey { /// Not exposing a constructor publicly because you should have to get an instance either by /// running a keygen, or by decoding an existing key. - fn new(rho: [u8; 32], t1: Vector) -> Self; + fn new(rho: [u8; 32], t1: P::VecK) -> Self; /// Get a ref to t1 - fn t1(&self) -> &Vector; + fn t1(&self) -> &P::VecK; } -impl MLDSAPublicKeyTrait - for MLDSAPublicKey +impl MLDSAPublicKeyTrait + for MLDSAPublicKey { // todo: block a t1 of all zeros? Maybe add to consistency_check() ? fn pk_decode(pk: &[u8; PK_LEN]) -> Self { let rho = pk[0..32].try_into().unwrap(); - let mut t1 = Vector::::new(); + let mut t1 = P::VecK::new(); let (pk_chunks, last_chunk) = pk[32..].as_chunks::(); // that should divide evenly the remainder of the array - debug_assert_eq!(pk_chunks.len(), k); + debug_assert_eq!(pk_chunks.len(), P::k); debug_assert_eq!(last_chunk.len(), 0); - for (t1_i, pk_chunk) in t1.elems.iter_mut().zip(pk_chunks) { + for (t1_i, pk_chunk) in t1.elems_mut().iter_mut().zip(pk_chunks) { // 3: 𝐭1[𝑖] ← SimpleBitUnpack(𝑧𝑖, 2bitlen (𝑞−1)−𝑑 − 1) // ▷ This is always in the correct range // Therefore, we don't need to check that the coeeffs are in range t1_i.coeffs.copy_from_slice(&simple_bit_unpack_t1(pk_chunk).coeffs); } - Self::new(rho, t1) + >::new(rho, t1) } - fn A_hat(&self) -> Matrix { - expandA::(&self.rho) + fn A_hat(&self) -> P::MatrixA { + expandA::

(&self.rho) } fn compute_tr(&self) -> [u8; 64] { @@ -182,21 +191,20 @@ impl MLDSAPublicKeyTrait MLDSAPublicKeyInternalTrait - for MLDSAPublicKey +impl MLDSAPublicKeyInternalTrait + for MLDSAPublicKey { - fn new(rho: [u8; 32], t1: Vector) -> Self { + fn new(rho: [u8; 32], t1: P::VecK) -> Self { + let () = Self::ASSERT_PK_LEN; Self { rho, t1 } } - fn t1(&self) -> &Vector { + fn t1(&self) -> &P::VecK { &self.t1 } } -impl SignaturePublicKey - for MLDSAPublicKey -{ +impl SignaturePublicKey for MLDSAPublicKey { fn encode(&self) -> [u8; PK_LEN] { let mut pk = [0u8; PK_LEN]; let bytes_written = self.encode_out(&mut pk); @@ -218,15 +226,13 @@ impl SignaturePublicKey>::pk_decode(&bytes_sized)) } } -impl Eq for MLDSAPublicKey {} +impl Eq for MLDSAPublicKey {} -impl PartialEq - for MLDSAPublicKey -{ +impl PartialEq for MLDSAPublicKey { fn eq(&self, other: &Self) -> bool { let self_encoded = self.encode(); let other_encoded = other.encode(); @@ -234,50 +240,54 @@ impl PartialEq } } -impl Debug for MLDSAPublicKey { +impl Debug for MLDSAPublicKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; - write!(f, "MLDSAPublicKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", alg, self.compute_tr(),) + write!( + f, + "MLDSAPublicKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", + P::ALG_NAME, + >::compute_tr(self), + ) } } -impl Display for MLDSAPublicKey { +impl Display for MLDSAPublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; - write!(f, "MLDSAPublicKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", alg, self.compute_tr(),) + write!( + f, + "MLDSAPublicKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", + P::ALG_NAME, + >::compute_tr(self), + ) } } /// A fully expanded ML-DSA public key that includes the intermediate values needed for performing /// multiple verification operations against the same public key, which causes the public key struct /// to take up more memory, but results in more efficient repeated verify() operations. -#[derive(Clone)] pub struct MLDSAPublicKeyExpanded< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, const PK_LEN: usize, > { pub(crate) pk: PK, - pub(crate) A_hat: Matrix, + pub(crate) A_hat: P::MatrixA, +} + +/// See the note on [`MLDSAPublicKey`]'s `Clone` for why this is not derived. +impl, const PK_LEN: usize> Clone + for MLDSAPublicKeyExpanded +{ + fn clone(&self) -> Self { + Self { pk: self.pk.clone(), A_hat: self.A_hat.clone() } + } } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> SignaturePublicKey for MLDSAPublicKeyExpanded +> SignaturePublicKey for MLDSAPublicKeyExpanded { fn encode(&self) -> [u8; PK_LEN] { self.pk.encode() @@ -296,16 +306,15 @@ impl< )); } let bytes_sized: [u8; PK_LEN] = bytes[..PK_LEN].try_into().unwrap(); - Ok(Self::pk_decode(&bytes_sized)) + Ok(>::pk_decode(&bytes_sized)) } } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> PartialEq for MLDSAPublicKeyExpanded +> PartialEq for MLDSAPublicKeyExpanded { fn eq(&self, other: &Self) -> bool { self.pk.eq(&other.pk) @@ -313,66 +322,50 @@ impl< } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> Eq for MLDSAPublicKeyExpanded +> Eq for MLDSAPublicKeyExpanded { } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> Debug for MLDSAPublicKeyExpanded +> Debug for MLDSAPublicKeyExpanded { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLDSAPublicKeyExpanded {{ alg: {}, pub_key_hash (tr): {:x?} }}", - alg, - self.compute_tr(), + P::ALG_NAME, + self.pk.compute_tr(), ) } } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> Display for MLDSAPublicKeyExpanded +> Display for MLDSAPublicKeyExpanded { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLDSAPublicKeyExpanded {{ alg: {}, pub_key_hash (tr): {:x?} }}", - alg, - self.compute_tr(), + P::ALG_NAME, + self.pk.compute_tr(), ) } } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> From<&PK> for MLDSAPublicKeyExpanded +> From<&PK> for MLDSAPublicKeyExpanded { /// Fully expands the intermediate values needed for performing multiple encaps operations /// against the same public key, which causes the MLKEMPublicKey struct to take up @@ -384,11 +377,10 @@ impl< } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> MLDSAPublicKeyTrait for MLDSAPublicKeyExpanded +> MLDSAPublicKeyTrait for MLDSAPublicKeyExpanded { fn pk_decode(pk: &[u8; PK_LEN]) -> Self { let pk1 = PK::pk_decode(pk); @@ -396,7 +388,7 @@ impl< Self { pk: pk1, A_hat } } - fn A_hat(&self) -> Matrix { + fn A_hat(&self) -> P::MatrixA { self.A_hat.clone() } @@ -407,15 +399,10 @@ impl< /// An ML-DSA private key. /// +/// See [`MLDSAPublicKey`] for why `SK_LEN` and `PK_LEN` are carried alongside `P`. +// // Dev note: This will automatically inherit the [`Secret`] protections because [`Polynomial`] wraps the underlying data with [`Secret`]. -#[derive(Clone)] -pub struct MLDSAPrivateKey< - const k: usize, - const l: usize, - const eta: usize, - const SK_LEN: usize, - const PK_LEN: usize, -> { +pub struct MLDSAPrivateKey { rho: [u8; 32], K: Secret<[u8; 32]>, tr: [u8; 64], @@ -425,16 +412,31 @@ pub struct MLDSAPrivateKey< // So we are going to hold them as s1_hat, s2_hat, and t0_hat. // Note: these are not necessarily in their reduced form; so you'll need to reduce them before // inv_ntt()'ing them or hashing them. - s1_hat: Secret>, - s2_hat: Secret>, - t0_hat: Vector, + s1_hat: Secret, + s2_hat: Secret, + t0_hat: P::VecK, // note: KeyMaterial is inherently Secret seed: Option>, } -impl - MLDSAPrivateKey +/// See the note on [`MLDSAPublicKey`]'s `Clone` for why this is not derived. +impl Clone + for MLDSAPrivateKey { + fn clone(&self) -> Self { + Self { + rho: self.rho, + K: self.K.clone(), + tr: self.tr, + s1_hat: self.s1_hat.clone(), + s2_hat: self.s2_hat.clone(), + t0_hat: self.t0_hat, + seed: self.seed.clone(), + } + } +} + +impl MLDSAPrivateKey { /// Algorithm 24 skEncode(𝜌, 𝐾, 𝑡𝑟, 𝐬1, 𝐬2, 𝐭0) /// Encodes a secret key for ML-DSA into a byte string. /// Input: 𝜌 ∈ 𝔹32, 𝐾 ∈ 𝔹32, 𝑡𝑟 ∈ 𝔹64 , 𝐬1 ∈ 𝑅ℓ with coefficients in [−𝜂, 𝜂], 𝐬2 ∈ 𝑅𝑘 with @@ -452,47 +454,44 @@ impl(&s1_i, &mut buf); + bit_pack_eta::

(&s1_i, &mut buf); sk_chunk.copy_from_slice(&buf[..eta_pack_len]); } - off += l * bitlen_eta(eta); + off += P::l * eta_pack_len; - let sk_chunks = out[off..off + k * bitlen_eta(eta)].chunks_mut(bitlen_eta(eta)); - debug_assert_eq!(sk_chunks.len(), k); - for (sk_chunk, s2_hat_i) in sk_chunks.into_iter().zip(&self.s2_hat.elems) { + let sk_chunks = out[off..off + P::k * eta_pack_len].chunks_mut(eta_pack_len); + debug_assert_eq!(sk_chunks.len(), P::k); + for (sk_chunk, s2_hat_i) in sk_chunks.into_iter().zip(self.s2_hat.elems()) { // Deviation from the FIPS: // We are holding these in ntt form, so need to convert back to standard form - let mut s2_hat_i = s2_hat_i.clone(); - s2_hat_i.reduce(); - s2_hat_i.inv_ntt(); - let s2_i = s2_hat_i; + let mut s2_i = *s2_hat_i; + s2_i.reduce(); + s2_i.inv_ntt(); - bit_pack_eta::(&s2_i, &mut buf); + bit_pack_eta::

(&s2_i, &mut buf); sk_chunk.copy_from_slice(&buf[..eta_pack_len]); } - off += k * bitlen_eta(eta); + off += P::k * eta_pack_len; - let sk_chunks = out[off..off + k * POLY_T0PACKED_LEN].chunks_mut(POLY_T0PACKED_LEN); - debug_assert_eq!(sk_chunks.len(), k); - for (sk_chunk, t0_hat_i) in sk_chunks.into_iter().zip(&self.t0_hat.elems) { + let sk_chunks = out[off..off + P::k * POLY_T0PACKED_LEN].chunks_mut(POLY_T0PACKED_LEN); + debug_assert_eq!(sk_chunks.len(), P::k); + for (sk_chunk, t0_hat_i) in sk_chunks.into_iter().zip(self.t0_hat.elems()) { // Deviation from the FIPS: // We are holding these in ntt form, so need to convert back to standard form - let mut t0_hat_i = t0_hat_i.clone(); - t0_hat_i.reduce(); - t0_hat_i.inv_ntt(); - let t0_i = t0_hat_i; + let mut t0_i = *t0_hat_i; + t0_i.reduce(); + t0_i.inv_ntt(); sk_chunk.copy_from_slice(&bit_pack_t0(&t0_i)); } @@ -502,13 +501,8 @@ impl: SignaturePrivateKey +pub trait MLDSAPrivateKeyTrait: + SignaturePrivateKey { /// Get a ref to the seed, if there is one stored with this private key fn seed(&self) -> Option<&KeyMaterial<32>>; @@ -517,10 +511,10 @@ pub trait MLDSAPrivateKeyTrait< fn tr(&self) -> &[u8; 64]; /// Get the public matrix A_hat. - fn A_hat(&self) -> Matrix; + fn A_hat(&self) -> P::MatrixA; /// This is a partial implementation of keygen_internal(), and probably not allowed in FIPS mode. - fn derive_pk(&self) -> MLDSAPublicKey; + fn derive_pk(&self) -> MLDSAPublicKey; /// Algorithm 25 skDecode(𝑠𝑘) /// Reverses the procedure skEncode. /// Input: Private key 𝑠𝑘 ∈ 𝔹32+32+64+32⋅((ℓ+𝑘)⋅bitlen (2𝜂)+𝑑𝑘). @@ -533,9 +527,7 @@ pub trait MLDSAPrivateKeyTrait< } pub(crate) trait MLDSAPrivateKeyInternalTrait< - const k: usize, - const l: usize, - const eta: usize, + P: MLDSAParams, const SK_LEN: usize, const PK_LEN: usize, > @@ -546,23 +538,23 @@ pub(crate) trait MLDSAPrivateKeyInternalTrait< rho: [u8; 32], K: Secret<[u8; 32]>, tr: [u8; 64], - s1_hat: Secret>, - s2_hat: Secret>, - t0_hat: Vector, + s1_hat: Secret, + s2_hat: Secret, + t0_hat: P::VecK, seed: Option>, ) -> Self; /// Get a ref to K fn K(&self) -> &Secret<[u8; 32]>; /// Get a ref to s1 - fn s1_hat(&self) -> &Vector; + fn s1_hat(&self) -> &P::VecL; /// Get a ref to s2 - fn s2_hat(&self) -> &Vector; + fn s2_hat(&self) -> &P::VecK; /// Get a ref to t0 - fn t0_hat(&self) -> &Vector; + fn t0_hat(&self) -> &P::VecK; } -impl - MLDSAPrivateKeyTrait for MLDSAPrivateKey +impl + MLDSAPrivateKeyTrait for MLDSAPrivateKey { fn seed(&self) -> Option<&KeyMaterial<32>> { match self.seed { @@ -575,18 +567,18 @@ impl Matrix { - expandA::(&self.rho) + fn A_hat(&self) -> P::MatrixA { + expandA::

(&self.rho) } - fn derive_pk(&self) -> MLDSAPublicKey { + fn derive_pk(&self) -> MLDSAPublicKey { // 5: 𝐭 ← NTT−1(𝐀 ∘ NTT(𝐬1)) + 𝐬2 // ▷ compute 𝐭 = 𝐀𝐬1 + 𝐬2 let mut t = { // scope for A_hat // 3: 𝐀 ← ExpandA(𝜌) // ▷ 𝐀 is generated and stored in NTT representation as 𝐀 - let A_hat = expandA::(&self.rho); + let A_hat = expandA::

(&self.rho); let mut t_ntt = A_hat.matrix_vector_ntt(&self.s1_hat); t_ntt.inv_ntt(); @@ -596,7 +588,7 @@ impl(&t); + let (t1, _) = power_2_round_vec(&t); - MLDSAPublicKey::::new(self.rho.clone(), t1) + as MLDSAPublicKeyInternalTrait>::new(self.rho, t1) } fn sk_decode(sk: &[u8; SK_LEN]) -> Result { // Construct the (Secret-protected) key up front and unpack each field directly into it, @@ -621,23 +613,25 @@ impl::new(), + t0_hat: P::VecK::new(), seed: None, }; key.K.copy_from_slice(&sk[32..64]); let mut off = 128; + let eta_pack_len = bitlen_eta(P::ETA); + let eta = P::ETA as i32; // unpack s1 directly into key.s1_hat so that we don't make additional non-Secret copies. - let sk_chunks = sk[off..off + (l * bitlen_eta(eta))].chunks(bitlen_eta(eta)); - debug_assert_eq!(sk_chunks.len(), l); - for (s1_i, sk_chunk) in key.s1_hat.elems.iter_mut().zip(sk_chunks) { + let sk_chunks = sk[off..off + (P::l * eta_pack_len)].chunks(eta_pack_len); + debug_assert_eq!(sk_chunks.len(), P::l); + for (s1_i, sk_chunk) in key.s1_hat.elems_mut().iter_mut().zip(sk_chunks) { // 3: 𝐬1[𝑖] ← BitUnpack(𝑦𝑖, 𝜂, 𝜂) // ▷ this may lie outside [−𝜂, 𝜂] if input is malformed - s1_i.coeffs.copy_from_slice(&bit_unpack_eta::(&sk_chunk).coeffs); + s1_i.coeffs.copy_from_slice(&bit_unpack_eta::

(sk_chunk).coeffs); // check that the coefficients are within the expected range for coeff in s1_i.coeffs.iter() { - if *coeff < -(eta as i32) || *coeff > (eta as i32) { + if *coeff < -eta || *coeff > eta { return Err(SignatureError::DecodingError("Invalid or corrupted key")); } } @@ -645,19 +639,19 @@ impl(&sk_chunk).coeffs); + s2_i.coeffs.copy_from_slice(&bit_unpack_eta::

(sk_chunk).coeffs); // check that the coefficients are within the expected range for coeff in s2_i.coeffs.iter() { - if *coeff < -(eta as i32) || *coeff > (eta as i32) { + if *coeff < -eta || *coeff > eta { return Err(SignatureError::DecodingError("Invalid or corrupted key")); } } @@ -665,17 +659,17 @@ impl(); + sk[off..off + (P::k * POLY_T0PACKED_LEN)].as_chunks::(); // that should divide evenly the remainder of the array - debug_assert_eq!(sk_chunks.len(), k); + debug_assert_eq!(sk_chunks.len(), P::k); debug_assert_eq!(last_chunk.len(), 0); - for (t0_i, sk_chunk) in key.t0_hat.elems.iter_mut().zip(sk_chunks) { + for (t0_i, sk_chunk) in key.t0_hat.elems_mut().iter_mut().zip(sk_chunks) { t0_i.coeffs.copy_from_slice(&bit_unpack_t0(sk_chunk).coeffs); } // Deviation from the FIPS: @@ -686,49 +680,40 @@ impl - MLDSAPrivateKeyInternalTrait - for MLDSAPrivateKey +impl + MLDSAPrivateKeyInternalTrait for MLDSAPrivateKey { fn new( rho: [u8; 32], K: Secret<[u8; 32]>, tr: [u8; 64], - s1_hat: Secret>, - s2_hat: Secret>, - t0_hat: Vector, + s1_hat: Secret, + s2_hat: Secret, + t0_hat: P::VecK, seed: Option>, ) -> Self { - Self { - rho: rho.clone(), - K: K.clone(), - tr: tr.clone(), - s1_hat: s1_hat.clone(), - s2_hat: s2_hat.clone(), - t0_hat: t0_hat.clone(), - seed: seed.clone(), - } + Self { rho, K, tr, s1_hat, s2_hat, t0_hat, seed } } fn K(&self) -> &Secret<[u8; 32]> { &self.K } - fn s1_hat(&self) -> &Vector { + fn s1_hat(&self) -> &P::VecL { &self.s1_hat } - fn s2_hat(&self) -> &Vector { + fn s2_hat(&self) -> &P::VecK { &self.s2_hat } - fn t0_hat(&self) -> &Vector { + fn t0_hat(&self) -> &P::VecK { &self.t0_hat } } -impl - SignaturePrivateKey for MLDSAPrivateKey +impl SignaturePrivateKey + for MLDSAPrivateKey { fn encode(&self) -> [u8; SK_LEN] { let mut out = [0u8; SK_LEN]; @@ -752,17 +737,17 @@ impl>::sk_decode(&bytes_sized) } } -impl Eq - for MLDSAPrivateKey +impl Eq + for MLDSAPrivateKey { } -impl - PartialEq for MLDSAPrivateKey +impl PartialEq + for MLDSAPrivateKey { fn eq(&self, other: &Self) -> bool { let self_encoded = self.encode(); @@ -772,20 +757,14 @@ impl - fmt::Debug for MLDSAPrivateKey +impl fmt::Debug + for MLDSAPrivateKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLDSAPrivateKey {{ alg: {}, pub_key_hash (tr): {:x?}, has_seed: {} }}", - alg, + P::ALG_NAME, self.tr, self.seed.is_some(), ) @@ -793,20 +772,14 @@ impl - Display for MLDSAPrivateKey +impl Display + for MLDSAPrivateKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLDSAPrivateKey {{ alg: {}, pub_key_hash (tr): {:x?}, has_seed: {} }}", - alg, + P::ALG_NAME, self.tr, self.seed.is_some(), ) @@ -816,32 +789,39 @@ impl, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, > { _phantom: core::marker::PhantomData, pub(crate) sk: SK, - pub(crate) A_hat: Matrix, + pub(crate) A_hat: P::MatrixA, } +/// See the note on [`MLDSAPublicKey`]'s `Clone` for why this is not derived. impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> PartialEq for MLDSAPrivateKeyExpanded +> Clone for MLDSAPrivateKeyExpanded +{ + fn clone(&self) -> Self { + Self { _phantom: core::marker::PhantomData, sk: self.sk.clone(), A_hat: self.A_hat.clone() } + } +} + +impl< + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, + const SK_LEN: usize, + const PK_LEN: usize, +> PartialEq for MLDSAPrivateKeyExpanded { fn eq(&self, other: &Self) -> bool { self.sk.eq(&other.sk) @@ -849,40 +829,28 @@ impl< } impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> Eq for MLDSAPrivateKeyExpanded +> Eq for MLDSAPrivateKeyExpanded { } impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> Debug for MLDSAPrivateKeyExpanded +> Debug for MLDSAPrivateKeyExpanded { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLDSAPrivateKeyExpanded {{ alg: {}, pub_key_hash (tr): {:x?}, has_seed: {} }}", - alg, + P::ALG_NAME, self.sk.tr(), self.sk.seed().is_some(), ) @@ -890,27 +858,18 @@ impl< } impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> Display for MLDSAPrivateKeyExpanded +> Display for MLDSAPrivateKeyExpanded { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLDSAPrivateKeyExpanded {{ alg: {}, pub_key_hash (tr): {:x?}, has_seed: {} }}", - alg, + P::ALG_NAME, self.sk.tr(), self.sk.seed().is_some(), ) @@ -918,35 +877,30 @@ impl< } impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> From<&SK> for MLDSAPrivateKeyExpanded +> From<&SK> for MLDSAPrivateKeyExpanded { /// Fully expands the intermediate values needed for performing multiple encaps operations /// against the same public key, which causes the MLKEMPublicKey struct to take up fn from(sk: &SK) -> Self { - let A_hat = sk.derive_pk().A_hat(); + let A_hat = + as MLDSAPublicKeyTrait>::A_hat(&sk.derive_pk()); Self { _phantom: core::marker::PhantomData, sk: sk.clone(), A_hat } } } impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> SignaturePrivateKey for MLDSAPrivateKeyExpanded +> SignaturePrivateKey for MLDSAPrivateKeyExpanded { fn encode(&self) -> [u8; SK_LEN] { self.sk.encode() @@ -965,16 +919,12 @@ impl< } impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> MLDSAPrivateKeyTrait - for MLDSAPrivateKeyExpanded +> MLDSAPrivateKeyTrait for MLDSAPrivateKeyExpanded { fn seed(&self) -> Option<&KeyMaterial<32>> { self.sk.seed() @@ -984,17 +934,18 @@ impl< self.sk.tr() } - fn A_hat(&self) -> Matrix { + fn A_hat(&self) -> P::MatrixA { self.sk.A_hat() } - fn derive_pk(&self) -> MLDSAPublicKey { + fn derive_pk(&self) -> MLDSAPublicKey { self.sk.derive_pk() } fn sk_decode(sk: &[u8; SK_LEN]) -> Result { let sk1 = SK::sk_decode(sk)?; - let A_hat = sk1.derive_pk().A_hat(); + let A_hat = + as MLDSAPublicKeyTrait>::A_hat(&sk1.derive_pk()); Ok(Self { _phantom: core::marker::PhantomData, sk: sk1, A_hat }) } diff --git a/crypto/mldsa/src/params.rs b/crypto/mldsa/src/params.rs new file mode 100644 index 00000000..2e394302 --- /dev/null +++ b/crypto/mldsa/src/params.rs @@ -0,0 +1,548 @@ +//! The three ML-DSA parameter sets of FIPS 204, Section 4, as a sealed trait with one type per set. +//! +//! # Derived parameters +//! +//! FIPS 204, Table 1 assigns eight independent values per set (𝜏, 𝜆, 𝛾1, 𝛾2, (𝑘, ℓ), 𝜂, 𝜔) plus the +//! three sizes of Table 2. Everything else this implementation needs is a function of those, so it +//! is written once as a defaulted associated const rather than three times as a hand-computed +//! number. `params::tests` checks every derivation against the values tabulated in FIPS 204. + +use crate::hash_mldsa::{ + HASH_ML_DSA_44_with_SHA256_NAME, HASH_ML_DSA_44_with_SHA512_NAME, + HASH_ML_DSA_65_WITH_SHA256_NAME, HASH_ML_DSA_65_WITH_SHA512_NAME, + HASH_ML_DSA_87_WITH_SHA512_NAME, HASH_ML_DSA_87_with_SHA256_NAME, +}; +use crate::matrix::{Matrix, MatrixTrait, Vector, VectorTrait}; +use crate::mldsa::{ML_DSA_44_NAME, ML_DSA_65_NAME, ML_DSA_87_NAME, q}; +use bouncycastle_core::traits::{Algorithm, AlgorithmOID, Hash, HashAlgParams, SecurityStrength}; +use bouncycastle_sha2::{SHA256, SHA512}; +use bouncycastle_utils::secret::ZeroizablePrimitive; + +/// `bitlen 𝑥`, the length of the binary expansion of 𝑥 (FIPS 204, Section 2.3). +/// +/// `bitlen 0` is 0; every use below has a positive argument. +pub(crate) const fn bitlen(x: u32) -> usize { + if x == 0 { 0 } else { x.ilog2() as usize + 1 } +} + +/// A fixed-size byte buffer whose length depends on the parameter set. +/// +/// [`ZeroizablePrimitive`] rather than [`Default`] supplies the all-zero value, because `Default` +/// for arrays stops at 32 elements and every buffer here is longer than that. +// todo -- why pub? Are we returning it somewhere in the public API surface? +pub trait ByteBuffer: ZeroizablePrimitive + AsRef<[u8]> + AsMut<[u8]> {} +impl ByteBuffer for [u8; N] {} + +/// A crate-private (aka "sealed") trait that prevents a new ML-DSA parameter set from being defined +/// outside this crate. +trait MLDSAParamsInternalTrait {} + +/// One ML-DSA parameter set: the values of FIPS 204, Table 1 and Table 2, and the types whose size +/// they determine. +/// +/// Sealed via a private supertrait, so [`MLDSA44Params`], [`MLDSA65Params`] and [`MLDSA87Params`] +/// are the only implementations. +pub trait MLDSAParams: MLDSAParamsInternalTrait { + /* FIPS 204, Table 1: the values assigned by each parameter set. */ + + /// 𝜏, the number of ±1's in the polynomial 𝑐. + const TAU: i32; + /// 𝜆, the collision strength of 𝑐̃, in bits. + const LAMBDA: i32; + /// 𝛾1, the coefficient range of 𝐲. Always a power of two. + const GAMMA1: i32; + /// 𝛾2, the low-order rounding range. + const GAMMA2: i32; + /// 𝑘, the number of rows of 𝐀. + const k: usize; + /// ℓ, the number of columns of 𝐀. + const l: usize; + /// 𝜂, the private key range. + const ETA: usize; + /// 𝜔, the maximum number of 1's in the hint 𝐡. + const OMEGA: i32; + + /* FIPS 204, Table 2: sizes in bytes of keys and signatures. */ + + /// The length of an encoded public key. + const PK_LEN: usize; + /// The length of an encoded private key. + const SK_LEN: usize; + /// The length of a signature. + const SIG_LEN: usize; + + /* Algorithm meta-data. */ + + /// The algorithm name, as reported by `Algorithm::ALG_NAME`. + const ALG_NAME: &'static str; + /// The strength claimed for this parameter set, as reported by `Algorithm::MAX_SECURITY_STRENGTH`. + const MAX_SECURITY_STRENGTH: SecurityStrength; + /// The OID in component form, as reported by `AlgorithmOID::OID`. + const OID: &'static [u32]; + /// The DER encoding of [`MLDSAParams::OID`], as reported by `AlgorithmOID::OID_DER`. + const OID_DER: &'static [u8]; + + /* Derived. Never written out per parameter set -- see the module docs. */ + + /// 𝛽, which FIPS 204, Table 1 defines as "𝛽 = 𝜏 ⋅ 𝜂". + const BETA: i32 = Self::TAU * Self::ETA as i32; + + /// The length of the commitment hash 𝑐̃, which FIPS 204, Algorithm 26 (sigEncode) gives as + /// 𝑐̃ ∈ 𝔹^(𝜆/4). + const C_TILDE_LEN: usize = Self::LAMBDA as usize / 4; + + /// The packed length of one coordinate of 𝐳: FIPS 204, Algorithm 26 (sigEncode) writes each of + /// the ℓ coordinates as 𝔹^(32⋅(1+bitlen (𝛾1−1))). + /// + /// This is also the number of bytes ExpandMask squeezes per coordinate: FIPS 204, + /// Algorithm 34, line 1 sets 𝑐 ← 1 + bitlen (𝛾1 − 1) and line 4 squeezes 32𝑐 bytes. + const POLY_Z_PACKED_LEN: usize = 32 * (1 + bitlen(Self::GAMMA1 as u32 - 1)); + + /// The packed length of one coordinate of 𝐰1: FIPS 204, Algorithm 28 (w1Encode) outputs + /// 𝔹^(32𝑘⋅bitlen ((𝑞−1)/(2𝛾2)−1)) for all 𝑘 coordinates together. + const POLY_W1_PACKED_LEN: usize = 32 * bitlen(((q - 1) / (2 * Self::GAMMA2)) as u32 - 1); + + /// 𝛾1 − 𝛽, the rejection bound on ‖𝐳‖∞ (FIPS 204, Algorithm 7, line 23). + const GAMMA1_MINUS_BETA: i32 = Self::GAMMA1 - Self::BETA; + + /// 𝛾2 − 𝛽, the rejection bound on ‖𝐫0‖∞ (FIPS 204, Algorithm 7, line 23). + const GAMMA2_MINUS_BETA: i32 = Self::GAMMA2 - Self::BETA; + + /* Types whose size depends on the parameter set. */ + + /// A vector of 𝑘 polynomials, i.e. an element of 𝑅^𝑘. + type VecK: VectorTrait; + /// A vector of ℓ polynomials, i.e. an element of 𝑅^ℓ. + type VecL: VectorTrait; + /// The 𝑘 × ℓ public matrix 𝐀̂. + type MatrixA: MatrixTrait; + + /// The commitment hash 𝑐̃, of [`MLDSAParams::C_TILDE_LEN`] bytes. + type SigCTilde: ByteBuffer; + /// One packed coordinate of 𝐳, of [`MLDSAParams::POLY_Z_PACKED_LEN`] bytes. + /// + /// ExpandMask squeezes into a buffer of this same length; see + /// [`MLDSAParams::POLY_Z_PACKED_LEN`]. + type PolyZPacked: ByteBuffer; + /// One packed coordinate of 𝐰1, of [`MLDSAParams::POLY_W1_PACKED_LEN`] bytes. + type PolyW1Packed: ByteBuffer; +} + +/// The ML-DSA-44 parameter set (FIPS 204, Table 1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLDSA44Params; +/// The ML-DSA-65 parameter set (FIPS 204, Table 1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLDSA65Params; +/// The ML-DSA-87 parameter set (FIPS 204, Table 1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLDSA87Params; + +impl MLDSAParamsInternalTrait for MLDSA44Params {} +impl MLDSAParamsInternalTrait for MLDSA65Params {} +impl MLDSAParamsInternalTrait for MLDSA87Params {} + +impl MLDSAParams for MLDSA44Params { + const TAU: i32 = 39; + const LAMBDA: i32 = 128; + const GAMMA1: i32 = 1 << 17; + // mutants note: because 𝛾1 is applied by bit-shifting, the "- 1" ends up not mattering. + const GAMMA2: i32 = (q - 1) / 88; + const k: usize = 4; + const l: usize = 4; + const ETA: usize = 2; + const OMEGA: i32 = 80; + + const PK_LEN: usize = 1312; + const SK_LEN: usize = 2560; + const SIG_LEN: usize = 2420; + + const ALG_NAME: &'static str = ML_DSA_44_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; + /// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-44 { sigAlgs 17 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 17]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x11]; + + type VecK = Vector<4>; + type VecL = Vector<4>; + type MatrixA = Matrix<4, 4>; + + type SigCTilde = [u8; 32]; // 𝜆/4 = 128/4 + type PolyZPacked = [u8; 576]; // 32 * (1 + bitlen(2^17 - 1)) = 32 * 18 + type PolyW1Packed = [u8; 192]; // 32 * bitlen(44 - 1) = 32 * 6 +} + +impl MLDSAParams for MLDSA65Params { + const TAU: i32 = 49; + const LAMBDA: i32 = 192; + const GAMMA1: i32 = 1 << 19; + // mutants note: because 𝛾1 is applied by bit-shifting, the "- 1" ends up not mattering. + const GAMMA2: i32 = (q - 1) / 32; + const k: usize = 6; + const l: usize = 5; + const ETA: usize = 4; + const OMEGA: i32 = 55; + + const PK_LEN: usize = 1952; + const SK_LEN: usize = 4032; + const SIG_LEN: usize = 3309; + + const ALG_NAME: &'static str = ML_DSA_65_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; + /// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-65 { sigAlgs 18 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 18]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12]; + + type VecK = Vector<6>; + type VecL = Vector<5>; + type MatrixA = Matrix<6, 5>; + + type SigCTilde = [u8; 48]; // 𝜆/4 = 192/4 + type PolyZPacked = [u8; 640]; // 32 * (1 + bitlen(2^19 - 1)) = 32 * 20 + type PolyW1Packed = [u8; 128]; // 32 * bitlen(16 - 1) = 32 * 4 +} + +impl MLDSAParams for MLDSA87Params { + const TAU: i32 = 60; + const LAMBDA: i32 = 256; + const GAMMA1: i32 = 1 << 19; + // mutants note: because 𝛾1 is applied by bit-shifting, the "- 1" ends up not mattering. + const GAMMA2: i32 = (q - 1) / 32; + const k: usize = 8; + const l: usize = 7; + const ETA: usize = 2; + const OMEGA: i32 = 75; + + const PK_LEN: usize = 2592; + const SK_LEN: usize = 4896; + const SIG_LEN: usize = 4627; + + const ALG_NAME: &'static str = ML_DSA_87_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; + /// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-87 { sigAlgs 19 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 19]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x13]; + + type VecK = Vector<8>; + type VecL = Vector<7>; + type MatrixA = Matrix<8, 7>; + + type SigCTilde = [u8; 64]; // 𝜆/4 = 256/4 + type PolyZPacked = [u8; 640]; // 32 * (1 + bitlen(2^19 - 1)) = 32 * 20 + type PolyW1Packed = [u8; 128]; // 32 * bitlen(16 - 1) = 32 * 4 +} + +/// The two distinct values 𝛾1 takes across the three parameter sets (FIPS 204, Table 1). +/// +/// The bit-packing routines have one layout per distinct 𝛾1, so they dispatch on these rather than +/// on the parameter set. ML-DSA-65 and ML-DSA-87 share the second value. +pub(crate) const GAMMA1_2_POW_17: i32 = MLDSA44Params::GAMMA1; +/// See [`GAMMA1_2_POW_17`]. +pub(crate) const GAMMA1_2_POW_19: i32 = MLDSA65Params::GAMMA1; + +/// The two distinct values 𝛾2 takes across the three parameter sets (FIPS 204, Table 1). +/// +/// As with 𝛾1, the routines that depend on 𝛾2 have one form per distinct value rather than one per +/// parameter set. ML-DSA-65 and ML-DSA-87 share the second value. +pub(crate) const GAMMA2_Q_MINUS_1_OVER_88: i32 = MLDSA44Params::GAMMA2; +/// See [`GAMMA2_Q_MINUS_1_OVER_88`]. +pub(crate) const GAMMA2_Q_MINUS_1_OVER_32: i32 = MLDSA65Params::GAMMA2; + +// todo -- do we really need these? Surely the general test vectors will fail if there are problems here? +/// The weaker of two security strengths. +/// +/// [`SecurityStrength`]'s discriminants are assigned in increasing order of strength, so comparing +/// them as integers orders them. A `const fn` because the strength of a HashML-DSA pairing is a +/// defaulted associated const. +const fn weaker_of(a: SecurityStrength, b: SecurityStrength) -> SecurityStrength { + if (a as u8) <= (b as u8) { a } else { b } +} + +/// A crate-private (aka "sealed") trait that prevents a new HashML-DSA pairing from being defined +/// outside this crate. +trait HashMLDSAParamsInternalTrait {} + +/// One HashML-DSA algorithm: an ML-DSA parameter set paired with a pre-hash function. +/// +/// FIPS 204, Algorithm 4 (HashML-DSA.Sign) leaves the choice of PH open, so an implementation is a +/// pairing rather than a single parameter set. Everything that varies across the pairings lives +/// here, so the algorithm itself takes one type rather than a parameter set plus a hash plus a +/// digest length. +/// +/// Sealed via a private supertrait, so the six types below are the only implementations. +pub trait HashMLDSAParams: HashMLDSAParamsInternalTrait { + /// The ML-DSA parameter set underneath. + type MLDSA: MLDSAParams; + /// PH, the pre-hash function. + type PreHash: Hash + HashAlgParams + AlgorithmOID + Default; + + /// The algorithm name, as reported by `Algorithm::ALG_NAME`. + /// + /// Written out per pairing rather than derived: it is the two component names spliced + /// together, and `&'static str` cannot be concatenated in a const context. + const ALG_NAME: &'static str; + + /* Derived. Never written out per pairing. */ + + /// The length of the pre-hash `ph`, which is just PH's output length. + const PH_LEN: usize = ::OUTPUT_LEN; + + /// The strength claimed for the pairing, as reported by `Algorithm::MAX_SECURITY_STRENGTH`. + /// + /// A HashML-DSA signature is no stronger than either of its two components, so this is the + /// weaker of the two. That is what caps, for example, HashML-DSA-87_with_SHA256 at 128 bits. + const MAX_SECURITY_STRENGTH: SecurityStrength = weaker_of( + ::MAX_SECURITY_STRENGTH, + ::MAX_SECURITY_STRENGTH, + ); +} + +/// The HashML-DSA-44_with_SHA256 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA44_with_SHA256Params; +/// The HashML-DSA-65_with_SHA256 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA65_with_SHA256Params; +/// The HashML-DSA-87_with_SHA256 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA87_with_SHA256Params; +/// The HashML-DSA-44_with_SHA512 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA44_with_SHA512Params; +/// The HashML-DSA-65_with_SHA512 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA65_with_SHA512Params; +/// The HashML-DSA-87_with_SHA512 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA87_with_SHA512Params; + +impl HashMLDSAParamsInternalTrait for HashMLDSA44_with_SHA256Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA65_with_SHA256Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA87_with_SHA256Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA44_with_SHA512Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA65_with_SHA512Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA87_with_SHA512Params {} + +impl HashMLDSAParams for HashMLDSA44_with_SHA256Params { + type MLDSA = MLDSA44Params; + type PreHash = SHA256; + const ALG_NAME: &'static str = HASH_ML_DSA_44_with_SHA256_NAME; +} +impl HashMLDSAParams for HashMLDSA65_with_SHA256Params { + type MLDSA = MLDSA65Params; + type PreHash = SHA256; + const ALG_NAME: &'static str = HASH_ML_DSA_65_WITH_SHA256_NAME; +} +impl HashMLDSAParams for HashMLDSA87_with_SHA256Params { + type MLDSA = MLDSA87Params; + type PreHash = SHA256; + const ALG_NAME: &'static str = HASH_ML_DSA_87_with_SHA256_NAME; +} +impl HashMLDSAParams for HashMLDSA44_with_SHA512Params { + type MLDSA = MLDSA44Params; + type PreHash = SHA512; + const ALG_NAME: &'static str = HASH_ML_DSA_44_with_SHA512_NAME; +} +impl HashMLDSAParams for HashMLDSA65_with_SHA512Params { + type MLDSA = MLDSA65Params; + type PreHash = SHA512; + const ALG_NAME: &'static str = HASH_ML_DSA_65_WITH_SHA512_NAME; +} +impl HashMLDSAParams for HashMLDSA87_with_SHA512Params { + type MLDSA = MLDSA87Params; + type PreHash = SHA512; + const ALG_NAME: &'static str = HASH_ML_DSA_87_WITH_SHA512_NAME; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mldsa::d; + + /// FIPS 204, Table 1, transcribed column by column: the eight values each parameter set + /// assigns. `(tau, lambda, gamma1, gamma2, k, l, eta, omega)`. + const TABLE_1: [(i32, i32, i32, i32, usize, usize, usize, i32); 3] = [ + (39, 128, 131072, (q - 1) / 88, 4, 4, 2, 80), + (49, 192, 524288, (q - 1) / 32, 6, 5, 4, 55), + (60, 256, 524288, (q - 1) / 32, 8, 7, 2, 75), + ]; + + /// FIPS 204, Table 2, transcribed row by row: `(private key, public key, signature)` in bytes. + const TABLE_2: [(usize, usize, usize); 3] = + [(2560, 1312, 2420), (4032, 1952, 3309), (4896, 2592, 4627)]; + + /// FIPS 204, Table 1 also tabulates 𝛽, which it labels "𝛽 = 𝜏 ⋅ 𝜂". + const TABLE_1_BETA: [i32; 3] = [78, 196, 120]; + + fn check_table_1(i: usize) { + let (tau, lambda, gamma1, gamma2, k, l, eta, omega) = TABLE_1[i]; + assert_eq!(P::TAU, tau, "{}: 𝜏", P::ALG_NAME); + assert_eq!(P::LAMBDA, lambda, "{}: 𝜆", P::ALG_NAME); + assert_eq!(P::GAMMA1, gamma1, "{}: 𝛾1", P::ALG_NAME); + assert_eq!(P::GAMMA2, gamma2, "{}: 𝛾2", P::ALG_NAME); + assert_eq!(P::k, k, "{}: 𝑘", P::ALG_NAME); + assert_eq!(P::l, l, "{}: ℓ", P::ALG_NAME); + assert_eq!(P::ETA, eta, "{}: 𝜂", P::ALG_NAME); + assert_eq!(P::OMEGA, omega, "{}: 𝜔", P::ALG_NAME); + assert_eq!(P::BETA, TABLE_1_BETA[i], "{}: 𝛽 = 𝜏 ⋅ 𝜂", P::ALG_NAME); + } + + fn check_table_2(i: usize) { + let (sk_len, pk_len, sig_len) = TABLE_2[i]; + assert_eq!(P::SK_LEN, sk_len, "{}: private key size", P::ALG_NAME); + assert_eq!(P::PK_LEN, pk_len, "{}: public key size", P::ALG_NAME); + assert_eq!(P::SIG_LEN, sig_len, "{}: signature size", P::ALG_NAME); + } + + /// Each of the three sizes of Table 2 also has a formula in FIPS 204, and the two must agree. + /// Table 2 is what is written down above; this is what re-derives it. + fn check_table_2_formulas() { + // Algorithm 22 (pkEncode): 𝑝𝑘 ∈ 𝔹^(32+32𝑘(bitlen (𝑞−1)−𝑑)). + let pk_len = 32 + 32 * P::k * (bitlen((q - 1) as u32) - d as usize); + assert_eq!(P::PK_LEN, pk_len, "{}: Algorithm 22 output size", P::ALG_NAME); + + // Algorithm 24 (skEncode): 𝑠𝑘 ∈ 𝔹^(32+32+64+32⋅((𝑘+ℓ)⋅bitlen (2𝜂)+𝑑𝑘)). + let sk_len = + 32 + 32 + 64 + 32 * ((P::k + P::l) * bitlen(2 * P::ETA as u32) + d as usize * P::k); + assert_eq!(P::SK_LEN, sk_len, "{}: Algorithm 24 output size", P::ALG_NAME); + + // Algorithm 26 (sigEncode): 𝜎 ∈ 𝔹^(𝜆/4+ℓ⋅32⋅(1+bitlen (𝛾1−1))+𝜔+𝑘). + let sig_len = P::LAMBDA as usize / 4 + + P::l * 32 * (1 + bitlen(P::GAMMA1 as u32 - 1)) + + P::OMEGA as usize + + P::k; + assert_eq!(P::SIG_LEN, sig_len, "{}: Algorithm 26 output size", P::ALG_NAME); + } + + /// The associated types must be exactly as long as the consts that describe them; they are + /// written out by hand per parameter set, so this guards against a typo in one of them. + fn check_associated_type_sizes() { + assert_eq!( + size_of::(), + P::C_TILDE_LEN, + "{}: SigCTilde vs C_TILDE_LEN", + P::ALG_NAME + ); + assert_eq!( + size_of::(), + P::POLY_Z_PACKED_LEN, + "{}: PolyZPacked vs POLY_Z_PACKED_LEN", + P::ALG_NAME + ); + assert_eq!( + size_of::(), + P::POLY_W1_PACKED_LEN, + "{}: PolyW1Packed vs POLY_W1_PACKED_LEN", + P::ALG_NAME + ); + assert_eq!(::LEN, P::k, "{}: VecK vs 𝑘", P::ALG_NAME); + assert_eq!(::LEN, P::l, "{}: VecL vs ℓ", P::ALG_NAME); + } + + #[test] + fn test_parameter_sets_match_fips204_table_1() { + check_table_1::(0); + check_table_1::(1); + check_table_1::(2); + } + + #[test] + fn test_sizes_match_fips204_table_2() { + check_table_2::(0); + check_table_2::(1); + check_table_2::(2); + } + + #[test] + fn test_table_2_sizes_agree_with_the_encoding_formulas() { + check_table_2_formulas::(); + check_table_2_formulas::(); + check_table_2_formulas::(); + } + + #[test] + fn test_associated_types_are_the_length_their_consts_claim() { + check_associated_type_sizes::(); + check_associated_type_sizes::(); + check_associated_type_sizes::(); + } + + #[test] + fn test_derived_lengths_match_the_values_they_replaced() { + // These three were hand-written per parameter set before the derivations above existed. + // They are pinned here so that a change to a `bitlen` formula cannot silently move them. + assert_eq!( + [MLDSA44Params::C_TILDE_LEN, MLDSA65Params::C_TILDE_LEN, MLDSA87Params::C_TILDE_LEN], + [32, 48, 64] + ); + assert_eq!( + [ + MLDSA44Params::POLY_Z_PACKED_LEN, + MLDSA65Params::POLY_Z_PACKED_LEN, + MLDSA87Params::POLY_Z_PACKED_LEN + ], + [576, 640, 640] + ); + assert_eq!( + [ + MLDSA44Params::POLY_W1_PACKED_LEN, + MLDSA65Params::POLY_W1_PACKED_LEN, + MLDSA87Params::POLY_W1_PACKED_LEN + ], + [192, 128, 128] + ); + assert_eq!( + [ + MLDSA44Params::GAMMA1_MINUS_BETA, + MLDSA65Params::GAMMA1_MINUS_BETA, + MLDSA87Params::GAMMA1_MINUS_BETA + ], + [131072 - 78, 524288 - 196, 524288 - 120] + ); + assert_eq!( + [ + MLDSA44Params::GAMMA2_MINUS_BETA, + MLDSA65Params::GAMMA2_MINUS_BETA, + MLDSA87Params::GAMMA2_MINUS_BETA + ], + [(q - 1) / 88 - 78, (q - 1) / 32 - 196, (q - 1) / 32 - 120] + ); + } + + #[test] + fn test_bitlen_matches_its_definition() { + // FIPS 204 Section 2.3 defines bitlen 𝑥 as the length of the binary expansion of 𝑥. + assert_eq!(bitlen(0), 0); + assert_eq!(bitlen(1), 1); + assert_eq!(bitlen(2), 2); + assert_eq!(bitlen(3), 2); + assert_eq!(bitlen(4), 3); + // The two arguments the derivations above actually use, plus bitlen(𝑞 − 1) = 23. + assert_eq!(bitlen((1 << 17) - 1), 17); + assert_eq!(bitlen((1 << 19) - 1), 19); + assert_eq!(bitlen((q - 1) as u32), 23); + } + + #[test] + fn test_gamma_dispatch_constants_cover_every_parameter_set() { + // The packing routines dispatch on these; a parameter set whose 𝛾 is neither value would + // fall through to a panic at runtime rather than fail to compile, so pin them here. + for gamma1 in [MLDSA44Params::GAMMA1, MLDSA65Params::GAMMA1, MLDSA87Params::GAMMA1] { + assert!(gamma1 == GAMMA1_2_POW_17 || gamma1 == GAMMA1_2_POW_19); + } + for gamma2 in [MLDSA44Params::GAMMA2, MLDSA65Params::GAMMA2, MLDSA87Params::GAMMA2] { + assert!(gamma2 == GAMMA2_Q_MINUS_1_OVER_88 || gamma2 == GAMMA2_Q_MINUS_1_OVER_32); + } + assert_ne!(GAMMA1_2_POW_17, GAMMA1_2_POW_19); + assert_ne!(GAMMA2_Q_MINUS_1_OVER_88, GAMMA2_Q_MINUS_1_OVER_32); + } +} diff --git a/crypto/mldsa/src/polynomial.rs b/crypto/mldsa/src/polynomial.rs index 98d47125..6c25c925 100644 --- a/crypto/mldsa/src/polynomial.rs +++ b/crypto/mldsa/src/polynomial.rs @@ -3,7 +3,9 @@ use crate::aux_functions::{ ZETAS, conditional_add_q, high_bits, low_bits, make_hint, montgomery_reduce, }; -use crate::mldsa::{MLDSA44_POLY_W1_PACKED_LEN, MLDSA65_POLY_W1_PACKED_LEN, N, q}; +use crate::mldsa::{N, d, q}; +use crate::params::{GAMMA2_Q_MINUS_1_OVER_32, GAMMA2_Q_MINUS_1_OVER_88, MLDSAParams}; +use bouncycastle_utils::secret::ZeroizablePrimitive; use core::ops::{Index, IndexMut}; /// A polynomial over the ML-DSA ring. @@ -12,8 +14,11 @@ use core::ops::{Index, IndexMut}; /// Polynomials themselves are not inherently secret since sometimes they are part of public keys /// and sometimes private keys. /// It is the responsibility of the caller to wrap sensitive instances in `Secret`. +/// +/// Public only because it appears in [`crate::VectorTrait`]'s signatures; its fields and +/// operations are crate-private, so from outside it is an opaque handle. #[derive(Clone, Copy)] -pub(crate) struct Polynomial { +pub struct Polynomial { pub(crate) coeffs: [i32; N], } @@ -34,7 +39,7 @@ impl IndexMut for Polynomial { impl Polynomial { /// Create a new polynomial with all coefficients set to zero. - pub const fn new() -> Self { + pub(crate) const fn new() -> Self { Self { coeffs: [0i32; N] } } @@ -65,25 +70,31 @@ impl Polynomial { } } - pub(crate) fn high_bits(&self) -> Self { + pub(crate) fn high_bits(&self) -> Self { let mut w = Self::new(); for i in 0..N { - w[i] = high_bits::(self[i]); + w[i] = high_bits::

(self[i]); } w } - pub(crate) fn low_bits(&self) -> Self { + pub(crate) fn low_bits(&self) -> Self { let mut w = Self::new(); for i in 0..N { - w[i] = low_bits::(self[i]); + w[i] = low_bits::

(self[i]); } w } - pub(crate) fn check_norm(&self) -> bool { + /// Tests whether any coefficient has absolute value at least `bound`. + /// + /// `bound` is a runtime argument rather than a const generic because every call site passes a + /// value derived from the parameter set (𝛾1 − 𝛽, 𝛾2 − 𝛽, or 𝛾2), and an associated const of a + /// type parameter cannot be used as a const generic argument. It is still a constant after + /// monomorphization, so this costs nothing. + pub(crate) fn check_norm(&self, bound: i32) -> bool { // It is acceptable that this function is not constant-time (returns true early) // The reason being because it is used in a rejection loop. // That is, the early quit here leads to rejection, dropping the secret values and @@ -95,33 +106,38 @@ impl Polynomial { // if bound > (q - 1) / 8 { // return true; // } - // but since BOUND is a constant here, a debug_assert is performed to make sure the value is what we expect. - debug_assert!(BOUND <= (q - 1) / 8); + // but since every caller passes a parameter-set constant, a debug_assert is performed + // instead to make sure the value is what we expect. + debug_assert!(bound <= (q - 1) / 8); let mut t: i32; for x in self.coeffs.iter() { t = *x >> 31; t = *x - (t & (2 * *x)); - if t >= BOUND { + if t >= bound { return true; } } false } - pub(crate) fn shift_left(&mut self) { + /// Multiplies every coefficient by 2^𝑑. + /// + /// 𝑑 is 13 for all three parameter sets (FIPS 204, Table 1), so it is read from the global + /// constant rather than being passed in. + pub(crate) fn shift_left_d(&mut self) { for x in self.coeffs.iter_mut() { *x <<= d; } } /// Creates the hint vector, and also returns its hamming weight (i.e. the number of 1's). - pub(crate) fn make_hint(&self, r: &Self) -> (Self, i32) { + pub(crate) fn make_hint(&self, r: &Self) -> (Self, i32) { let mut out = Polynomial::new(); let mut count = 0i32; for i in 0..N { - let x = make_hint::(self[i], r[i]); + let x = make_hint::

(self[i], r[i]); out[i] = x; // mutants note: this chains up to hint_hamming_weight > OMEGA and there is no test KAT that triggers this branch @@ -131,19 +147,24 @@ impl Polynomial { (out, count) } - pub(crate) fn w1_encode(&self) -> [u8; POLY_W1_PACKED_LEN] { - let mut r = [0u8; POLY_W1_PACKED_LEN]; + /// SimpleBitPack(𝐰1[𝑖], (𝑞 − 1)/(2𝛾2) − 1), the per-coordinate body of + /// FIPS 204, Algorithm 28 (w1Encode), line 3. + pub(crate) fn w1_encode(&self) -> P::PolyW1Packed { + let mut out = ::ZEROED; + let r = out.as_mut(); - match POLY_W1_PACKED_LEN { - MLDSA44_POLY_W1_PACKED_LEN => { + match P::GAMMA2 { + // ML-DSA-44: (𝑞 − 1)/(2𝛾2) − 1 = 43, so four 6-bit coefficients pack into three bytes. + GAMMA2_Q_MINUS_1_OVER_88 => { for i in 0..N / 4 { r[3 * i] = ((self[4 * i]) as u8) | ((self[4 * i + 1] << 6) as u8); r[3 * i + 1] = ((self[4 * i + 1] >> 2) as u8) | ((self[4 * i + 2] << 4) as u8); r[3 * i + 2] = ((self[4 * i + 2] >> 4) as u8) | ((self[4 * i + 3] << 2) as u8); } } - // ML-DSA65 and 87 share a POLY_W1_PACKED_LEN value - MLDSA65_POLY_W1_PACKED_LEN => { + // ML-DSA-65 and ML-DSA-87 share this 𝛾2: (𝑞 − 1)/(2𝛾2) − 1 = 15, so two 4-bit + // coefficients pack into one byte. + GAMMA2_Q_MINUS_1_OVER_32 => { for i in 0..N / 2 { r[i] = ((self[2 * i]) | (self[2 * i + 1] << 4)) as u8; } @@ -153,7 +174,7 @@ impl Polynomial { } } - r + out } /// Algorithm 41 NTT(𝑤) diff --git a/crypto/mldsa/tests/mldsa_tests.rs b/crypto/mldsa/tests/mldsa_tests.rs index 33aa8f9f..aebd3a06 100644 --- a/crypto/mldsa/tests/mldsa_tests.rs +++ b/crypto/mldsa/tests/mldsa_tests.rs @@ -1071,6 +1071,46 @@ mod mldsa_tests { _ => panic!("Expected an error when loading a SHAKE128 state into a MuBuilder"), } } + + #[test] + fn algorithm_names_and_oids() { + use bouncycastle_core::traits::{Algorithm, AlgorithmOID}; + + // `Algorithm` and `AlgorithmOID` are implemented once, generically over the parameter set, + // so nothing else states these per algorithm. Pinned here so that a wrong wiring of the + // blanket impls, or a typo in a parameter set, is a test failure rather than a silently + // mislabelled algorithm or an unparseable OID. + assert_eq!(MLDSA44::ALG_NAME, "ML-DSA-44"); + assert_eq!(MLDSA65::ALG_NAME, "ML-DSA-65"); + assert_eq!(MLDSA87::ALG_NAME, "ML-DSA-87"); + + assert_eq!(MLDSA44::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(MLDSA65::MAX_SECURITY_STRENGTH, SecurityStrength::_192bit); + assert_eq!(MLDSA87::MAX_SECURITY_STRENGTH, SecurityStrength::_256bit); + + // NIST's Computer Security Objects Register: id-ml-dsa-44 { sigAlgs 17 }, + // id-ml-dsa-65 { sigAlgs 18 }, id-ml-dsa-87 { sigAlgs 19 }, under + // joint-iso-itu-t(2) country(16) us(840) organization(1) gov(101) csor(3) nistAlgorithm(4) + // sigAlgs(3). + assert_eq!(MLDSA44::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 17]); + assert_eq!(MLDSA65::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 18]); + assert_eq!(MLDSA87::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 19]); + + // The DER encodings must be the OBJECT IDENTIFIER (tag 0x06) encodings of those arcs: + // 9 content bytes, the first being 40*2 + 16 = 0x60, then 840 as the two-byte 0x86 0x48. + for (oid, der) in [ + (MLDSA44::OID, MLDSA44::OID_DER), + (MLDSA65::OID, MLDSA65::OID_DER), + (MLDSA87::OID, MLDSA87::OID_DER), + ] { + assert_eq!(der[0], 0x06, "DER tag must be OBJECT IDENTIFIER"); + assert_eq!(der[1] as usize, der.len() - 2, "DER length must match the content"); + assert_eq!( + &der[2..], + &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, *oid.last().unwrap() as u8] + ); + } + } } struct Kat { diff --git a/mem_usage_benches/Cargo.toml b/mem_usage_benches/Cargo.toml index a3623aac..5d3e1aed 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -18,3 +18,7 @@ path = "bench_mlkem_mem_usage.rs" [[bin]] name = "bench_sha3_mem_usage" path = "bench_sha3_mem_usage.rs" + +[[bin]] +name = "bench_aes_mem_usage" +path = "bench_aes_mem_usage.rs" diff --git a/mem_usage_benches/bench_aes_mem_usage.rs b/mem_usage_benches/bench_aes_mem_usage.rs new file mode 100644 index 00000000..23002fa3 --- /dev/null +++ b/mem_usage_benches/bench_aes_mem_usage.rs @@ -0,0 +1,127 @@ +//! The purpose of this binary is to perform a single run of the primitive under test so that +//! its peak memory usage can be measured with: +//! +//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_aes_mem_usage > /dev/null +//! +//! ms_print massif.out.835000 +//! +//! or, shoved all into one line: +//! +//! clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_aes_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! +//! Make sure you build in release mode! +//! +//! Note: print!() is used to force the compiler not to optimize away the actual code. +//! The important stuff for benchmarking goes to stderr so the junk can be piped to /dev/null. +//! +//! Main is at the bottom, and controls which of these actually runs -- measure one at a time, +//! because massif reports the peak across the whole process. +//! +//! # What to expect +//! +//! Peak usage is a small constant plus the key schedule. +//! The numbers worth recording in the crate docs are the ones `print_struct_sizes` prints -- the +//! persistent size of each engine -- and the confirmation that per-block work is a fixed, small +//! amount of stack independent of key length. + +#![allow(dead_code)] +#![allow(unused_imports)] + +use bouncycastle::aes_lowmemory::{AES_128, AES_192, AES_256}; +use bouncycastle::core::key_material::{KeyMaterial, KeyType}; + +/// This exists so /usr/bin/time can measure the base memory footprint of the harness itself. +fn bench_do_nothing() { + eprintln!("DoNothing"); + + print!("{}", 1 + 1); +} + +/// Prints the in-memory size of each engine, i.e. the persistent cost of holding a key schedule. +fn print_struct_sizes() { + use core::mem::size_of; + + // FIPS 197 Sec 5.2: the schedule is 4 * (Nr + 1) words, so 176 / 208 / 240 bytes. The + // bit-sliced form is stored compressed, so bit-slicing adds nothing to these. + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); +} + +fn key() -> KeyMaterial { + // A fixed non-zero key: an all-zero buffer would be tagged KeyType::Zeroized and rejected. + let mut bytes = [0u8; N]; + for (i, b) in bytes.iter_mut().enumerate() { + *b = (i as u8).wrapping_mul(7).wrapping_add(1); + } + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() +} + +fn bench_aes128_key_expansion() { + eprintln!("Aes128::new (key expansion)"); + + let aes = AES_128::new(&key::<16>()).unwrap(); + print!("{aes:?}"); +} + +fn bench_aes192_key_expansion() { + eprintln!("Aes192::new (key expansion)"); + + let aes = AES_192::new(&key::<24>()).unwrap(); + print!("{aes:?}"); +} + +fn bench_aes256_key_expansion() { + eprintln!("Aes256::new (key expansion)"); + + let aes = AES_256::new(&key::<32>()).unwrap(); + print!("{aes:?}"); +} + +fn bench_aes128_encrypt_block() { + eprintln!("Aes128::encrypt_block"); + + let aes = AES_128::new(&key::<16>()).unwrap(); + let mut block = [0x11u8; 16]; + aes.encrypt_block(&mut block); + print!("{block:x?}"); +} + +fn bench_aes256_encrypt_block() { + eprintln!("Aes256::encrypt_block"); + + let aes = AES_256::new(&key::<32>()).unwrap(); + let mut block = [0x11u8; 16]; + aes.encrypt_block(&mut block); + print!("{block:x?}"); +} + +fn bench_aes256_decrypt_block() { + eprintln!("Aes256::decrypt_block"); + + let aes = AES_256::new(&key::<32>()).unwrap(); + let mut block = [0x11u8; 16]; + aes.decrypt_block(&mut block); + print!("{block:x?}"); +} + +fn bench_aes256_encrypt_blocks2() { + eprintln!("Aes256::encrypt_blocks2"); + + let aes = AES_256::new(&key::<32>()).unwrap(); + let mut blocks = [[0x11u8; 16], [0x22u8; 16]]; + aes.encrypt_2blocks(&mut blocks); + print!("{blocks:x?}"); +} + +fn main() { + print_struct_sizes() + // bench_do_nothing() + // bench_aes128_key_expansion() + // bench_aes192_key_expansion() + // bench_aes256_key_expansion() + // bench_aes128_encrypt_block() + // bench_aes256_encrypt_block() + // bench_aes256_decrypt_block() + // bench_aes256_encrypt_blocks2() +} diff --git a/mem_usage_benches/lib.rs b/mem_usage_benches/lib.rs index a281a8b2..0445bb89 100644 --- a/mem_usage_benches/lib.rs +++ b/mem_usage_benches/lib.rs @@ -1,3 +1,4 @@ +mod bench_aes_mem_usage; mod bench_mldsa_mem_usage; mod bench_mlkem_mem_usage; mod bench_sha3_mem_usage; diff --git a/src/lib.rs b/src/lib.rs index b46df8cd..ca2fb145 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub use bouncycastle_aes_lowmemory as aes_lowmemory; pub use bouncycastle_base64 as base64; pub use bouncycastle_core as core; pub use bouncycastle_factory as factory;