diff --git a/.gitignore b/.gitignore index 6d42084..1fae103 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ mutants.out*/ .idea/ .vscode/ + +.claude/* \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 6e2ed3f..88f5868 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2024" # *** Internal Dependencies *** bouncycastle = { path = "./", version = "0.1.2" } +bouncycastle-aes = { path = "./crypto/aes", version = "0.1.2" } bouncycastle-base64 = { path = "./crypto/base64", version = "0.1.2" } bouncycastle-core = { path = "crypto/core", version = "0.1.2" } bouncycastle-core-test-framework = { path = "./crypto/core-test-framework", version = "0.1.2" } @@ -40,6 +41,7 @@ version = "0.1.2" edition.workspace = true [dependencies] +bouncycastle-aes.workspace = true bouncycastle-base64.workspace = true bouncycastle-core.workspace = true bouncycastle-factory.workspace = true diff --git a/cli/src/mldsa_cmd.rs b/cli/src/mldsa_cmd.rs index beb92c6..070af7e 100644 --- a/cli/src/mldsa_cmd.rs +++ b/cli/src/mldsa_cmd.rs @@ -1,4 +1,4 @@ -//! Work in progress. +//! Work in progress. //! TODO: Use generic macros to eliminate duplicated code. use crate::helpers::{parse_seed, read_from_file, read_from_file_or_stdin, write_bytes_or_hex}; diff --git a/crypto/aes/Cargo.toml b/crypto/aes/Cargo.toml new file mode 100644 index 0000000..c72068c --- /dev/null +++ b/crypto/aes/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "bouncycastle-aes" +version = "0.1.2" +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-utils.workspace = true + +[dev-dependencies] +bouncycastle-hex.workspace = true +criterion.workspace = true + +[[bench]] +name = "aes_benches" +harness = false diff --git a/crypto/aes/benches/aes_benches.rs b/crypto/aes/benches/aes_benches.rs new file mode 100644 index 0000000..303d67a --- /dev/null +++ b/crypto/aes/benches/aes_benches.rs @@ -0,0 +1,152 @@ +//! Criterion benchmarks for the AES block cipher engine. +//! +//! There are three things worth measuring separately, because they have genuinely different +//! performance characteristics: +//! +//! * `KeyExpansion` -- [`AES::new`], ie FIPS 197 Algorithm 2. Paid once per key. +//! * `EncryptBlock` -- CIPHER(), Algorithm 1. Paid once per block. +//! * `DecryptBlock` -- INVCIPHER(), Algorithm 3. Slower than encryption, because INVMIXCOLUMNS() +//! multiplies by {09}, {0b}, {0d} and {0e} rather than MIXCOLUMNS()'s {02} and {03}. +//! +//! The one-shot APIs ([`AES::encrypt_single_block`] and friends) are deliberately not benchmarked +//! separately: they are a key expansion plus a block operation over the same implementation, so +//! their cost is the sum of the two benchmarks above. +//! +//! The block benchmarks run a fixed number of blocks per iteration and report throughput in bytes, +//! so the numbers are directly comparable with the other symmetric primitives in the library, and +//! so that the per-call overhead of criterion does not dominate a ~10ns operation. + +use bouncycastle_aes::{ + AES_BLOCK_LEN, AES128, AES128_KEY_LEN, AES128Key, AES192, AES192_KEY_LEN, AES192Key, AES256, + AES256_KEY_LEN, AES256Key, AESEngine, +}; +use bouncycastle_core::key_material::KeyType; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +/// How many blocks each block-level benchmark iteration processes. +const NUM_BLOCKS: usize = 256; + +/// Builds the set of blocks to process. Each block differs from the others so that the CPU cannot +/// cache or branch-predict its way to an unrealistically good result. +fn make_blocks() -> [[u8; AES_BLOCK_LEN]; NUM_BLOCKS] { + let mut blocks = [[0u8; AES_BLOCK_LEN]; NUM_BLOCKS]; + for (i, block) in blocks.iter_mut().enumerate() { + for (j, byte) in block.iter_mut().enumerate() { + *byte = (i as u8).wrapping_mul(31).wrapping_add(j as u8); + } + } + blocks +} + +/// The FIPS 197 Appendix A keys, so the benchmarks are keyed with the same material as the tests. +fn keys() -> (AES128Key, AES192Key, AES256Key) { + const KEY_BYTES: [u8; AES256_KEY_LEN] = [ + 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, + ]; + + let key128 = + AES128Key::from_bytes_as_type(&KEY_BYTES[..AES128_KEY_LEN], KeyType::SymmetricCipherKey) + .unwrap(); + let key192 = + AES192Key::from_bytes_as_type(&KEY_BYTES[..AES192_KEY_LEN], KeyType::SymmetricCipherKey) + .unwrap(); + let key256 = AES256Key::from_bytes_as_type(&KEY_BYTES, KeyType::SymmetricCipherKey).unwrap(); + + (key128, key192, key256) +} + +fn bench_key_expansion(c: &mut Criterion) { + let mut group = c.benchmark_group("KeyExpansion"); + let (key128, key192, key256) = keys(); + + group.throughput(criterion::Throughput::Elements(1)); + + group.bench_function("AES-128", |b| b.iter(|| black_box(AES128::new(&key128)).unwrap())); + group.bench_function("AES-192", |b| b.iter(|| black_box(AES192::new(&key192)).unwrap())); + group.bench_function("AES-256", |b| b.iter(|| black_box(AES256::new(&key256)).unwrap())); + + group.finish(); +} + +fn bench_encrypt_block(c: &mut Criterion) { + let mut group = c.benchmark_group("EncryptBlock"); + let (key128, key192, key256) = keys(); + let blocks = make_blocks(); + + // Key expansion happens outside the timing loop: this measures CIPHER() only. + let aes128 = AES128::new(&key128).unwrap(); + let aes192 = AES192::new(&key192).unwrap(); + let aes256 = AES256::new(&key256).unwrap(); + + group.throughput(criterion::Throughput::Bytes((NUM_BLOCKS * AES_BLOCK_LEN) as u64)); + + group.bench_function("AES-128", |b| { + b.iter(|| { + for block in blocks.iter() { + black_box(aes128.encrypt_block(block)); + } + }) + }); + + group.bench_function("AES-192", |b| { + b.iter(|| { + for block in blocks.iter() { + black_box(aes192.encrypt_block(block)); + } + }) + }); + + group.bench_function("AES-256", |b| { + b.iter(|| { + for block in blocks.iter() { + black_box(aes256.encrypt_block(block)); + } + }) + }); + + group.finish(); +} + +fn bench_decrypt_block(c: &mut Criterion) { + let mut group = c.benchmark_group("DecryptBlock"); + let (key128, key192, key256) = keys(); + let blocks = make_blocks(); + + let aes128 = AES128::new(&key128).unwrap(); + let aes192 = AES192::new(&key192).unwrap(); + let aes256 = AES256::new(&key256).unwrap(); + + group.throughput(criterion::Throughput::Bytes((NUM_BLOCKS * AES_BLOCK_LEN) as u64)); + + group.bench_function("AES-128", |b| { + b.iter(|| { + for block in blocks.iter() { + black_box(aes128.decrypt_block(block)); + } + }) + }); + + group.bench_function("AES-192", |b| { + b.iter(|| { + for block in blocks.iter() { + black_box(aes192.decrypt_block(block)); + } + }) + }); + + group.bench_function("AES-256", |b| { + b.iter(|| { + for block in blocks.iter() { + black_box(aes256.decrypt_block(block)); + } + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_key_expansion, bench_encrypt_block, bench_decrypt_block); +criterion_main!(benches); diff --git a/crypto/aes/src/aes.rs b/crypto/aes/src/aes.rs new file mode 100644 index 0000000..35d5eca --- /dev/null +++ b/crypto/aes/src/aes.rs @@ -0,0 +1,518 @@ +//! The AES block cipher engine: CIPHER() and INVCIPHER() from FIPS 197 Section 5, together with +//! the key expansion that drives them. +//! +//! This module provides the keyed permutation on a single 128-bit block and nothing more. See the +//! crate-level docs for what that does and does not give you, and for why there is no mode of +//! operation here. + +use crate::key_schedule::key_expansion; +use crate::state::{ + AES_BLOCK_LEN, NB, inv_mix_columns, inv_shift_rows, inv_sub_bytes, mix_columns, shift_rows, + sub_bytes, +}; +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; +use core::fmt; + +/* *** Algorithm names *** */ + +/// The library-wide name for AES with a 128-bit key. +pub const AES_128_NAME: &str = "AES-128"; +/// The library-wide name for AES with a 192-bit key. +pub const AES_192_NAME: &str = "AES-192"; +/// The library-wide name for AES with a 256-bit key. +pub const AES_256_NAME: &str = "AES-256"; + +/* *** Parameters from FIPS 197 Table 3 (Key-Block-Round Combinations) *** */ + +/// The AES-128 key length in bytes; `Nk = 4` words. +pub const AES128_KEY_LEN: usize = 16; +/// The AES-192 key length in bytes; `Nk = 6` words. +pub const AES192_KEY_LEN: usize = 24; +/// The AES-256 key length in bytes; `Nk = 8` words. +pub const AES256_KEY_LEN: usize = 32; + +/// The number of rounds `Nr` for AES-128. +pub const AES128_NUM_ROUNDS: usize = 10; +/// The number of rounds `Nr` for AES-192. +pub const AES192_NUM_ROUNDS: usize = 12; +/// The number of rounds `Nr` for AES-256. +pub const AES256_NUM_ROUNDS: usize = 14; + +/// The length of the AES-128 key schedule, in 32-bit words: `4 * (Nr + 1)` (FIPS 197 Section 5.2). +/// +/// Counted in words rather than bytes, hence `WORDS` and not the library's usual `LEN` suffix. +pub const AES128_KEY_SCHEDULE_WORDS: usize = 4 * (AES128_NUM_ROUNDS + 1); +/// The length of the AES-192 key schedule, in 32-bit words: `4 * (Nr + 1)`. +pub const AES192_KEY_SCHEDULE_WORDS: usize = 4 * (AES192_NUM_ROUNDS + 1); +/// The length of the AES-256 key schedule, in 32-bit words: `4 * (Nr + 1)`. +pub const AES256_KEY_SCHEDULE_WORDS: usize = 4 * (AES256_NUM_ROUNDS + 1); + +/* *** Key types *** */ + +/// The [`KeyMaterial`] type that [`AES128::new`] takes: a 128-bit AES key. +/// +/// Using a fixed-capacity key type means a key of the wrong size for the variant is a compile +/// error rather than a runtime one. +pub type AES128Key = KeyMaterial; +/// The [`KeyMaterial`] type that [`AES192::new`] takes: a 192-bit AES key. +pub type AES192Key = KeyMaterial; +/// The [`KeyMaterial`] type that [`AES256::new`] takes: a 256-bit AES key. +pub type AES256Key = KeyMaterial; + +/* *** The three variants specified by FIPS 197 *** */ + +/// AES with a 128-bit key: 10 rounds (FIPS 197 Table 3). +pub type AES128 = AES; +/// AES with a 192-bit key: 12 rounds (FIPS 197 Table 3). +pub type AES192 = AES; +/// AES with a 256-bit key: 14 rounds (FIPS 197 Table 3). +pub type AES256 = AES; + +impl Algorithm for AES128 { + const ALG_NAME: &'static str = AES_128_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl Algorithm for AES192 { + const ALG_NAME: &'static str = AES_192_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; +} + +impl Algorithm for AES256 { + const ALG_NAME: &'static str = AES_256_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +} + +// Deliberately no `AlgorithmOID` impl: NIST's Computer Security Objects Register assigns AES OIDs +// per *mode* (id-aes128-CBC, id-aes128-GCM, ...), not to the bare block cipher, so the OIDs belong +// on the mode-of-operation types rather than here. + +/// The AES block cipher engine, holding one expanded key schedule. +/// +/// You almost certainly want one of the three named variants -- [`AES128`], [`AES192`] or +/// [`AES256`] -- rather than naming this type directly. It is generic only so that the three +/// variants share one implementation, with the key schedule sized exactly at compile time. +/// +/// The const parameters are: +/// +/// * `KEY_LEN`: cipher key length in bytes, ie `4 * Nk`. +/// * `NR`: number of rounds `Nr`. +/// * `W_WORDS`: key schedule length in 32-bit words, ie `4 * (Nr + 1)`. +/// +/// Only the three combinations in FIPS 197 Table 3 are usable: everything on this type is gated on +/// `Self: Algorithm`, and [`Algorithm`] is implemented only for [`AES128`], [`AES192`] and +/// [`AES256`]. Because both that trait and this struct are foreign to downstream crates, the orphan +/// rule prevents anyone adding a fourth combination, so a nonsense parameterization such as +/// `AES<16, 3, 44>` has no constructor and no methods. (FIPS 197 Section 6.3 notes that future +/// revisions might add parameter values; adding one here means adding a variant, its `Algorithm` +/// impl, and its test vectors, which is exactly the review that such a change deserves.) +/// +/// # ๐Ÿšจ Security ๐Ÿšจ +/// +/// An instance of this type *is* key material: the key schedule it holds is invertible back to the +/// cipher key. It is stored in a [`Secret`], so it is scrubbed when the engine is dropped, and the +/// [`fmt::Debug`] impl never prints it. Do not clone engines you do not need to clone. +#[derive(Clone)] +pub struct AES { + /// The key schedule, `w` in FIPS 197 Section 5.2, as `4 * (Nr + 1)` big-endian words. + w: Secret<[u32; W_WORDS]>, +} + +impl AES +where + Self: Algorithm, +{ + /// A compile-time check that this instantiation is one of the three parameter sets of + /// FIPS 197 Table 3, and that the derived sizes are self-consistent. + /// + /// The `Self: Algorithm` bound above already restricts instantiation to the three variants; + /// this is a second, independent guard that catches a typo *inside this crate* -- for example + /// defining [`AES192`] with a 50-word schedule -- at compile time rather than as a wrong answer + /// at run time. + const VALID_PARAMS: () = { + assert!( + KEY_LEN == 16 || KEY_LEN == 24 || KEY_LEN == 32, + "AES key length must be 16, 24 or 32 bytes (FIPS 197 Table 3)" + ); + // Table 3: Nr = Nk + 6, where Nk = KEY_LEN / 4. + assert!(NR == KEY_LEN / 4 + 6, "AES round count must be Nk + 6 (FIPS 197 Table 3)"); + // Section 5.2: the key schedule holds 4 * (Nr + 1) words. + assert!( + W_WORDS == 4 * (NR + 1), + "AES key schedule must hold 4 * (Nr + 1) words (FIPS 197 Section 5.2)" + ); + }; + + /// Creates an engine from a cipher key by running KEYEXPANSION() (FIPS 197 Algorithm 2). + /// + /// Expanding the key is the only expensive part of AES, so construct one engine and reuse it + /// for as many blocks as you have; the per-block operations take `&self`. + /// + /// The key must be a [`KeyMaterial`] of exactly `KEY_LEN` bytes whose [`KeyType`] is + /// [`KeyType::SymmetricCipherKey`] or [`KeyType::CryptographicRandom`], and whose + /// [`SecurityStrength`] is at least this variant's strength. In other words: use a full-entropy + /// key of the right length, and tell the library that is what it is. + /// + /// # Errors + /// + /// * [`SymmetricCipherError::KeyMaterialError`] wrapping + /// [`KeyMaterialError::InvalidKeyType`] if the key is not tagged as a symmetric cipher key or + /// full-entropy random. Note that an all-zero buffer arrives tagged + /// [`KeyType::Zeroized`] and is rejected here -- using one is possible, but only by + /// deliberately retagging it inside a + /// [`do_hazardous_operations`](bouncycastle_core::key_material::do_hazardous_operations) + /// closure. + /// * ... wrapping [`KeyMaterialError::InvalidLength`] if the key holds fewer than `KEY_LEN` + /// bytes. (It can never hold more: the capacity is `KEY_LEN`.) + /// * ... wrapping [`KeyMaterialError::SecurityStrength`] if the key is tagged at a lower + /// security strength than the variant requires; for example a 128-bit-strength key handed to + /// [`AES256`]. + pub fn new(key: &KeyMaterial) -> Result { + // Force the compile-time parameter check for this instantiation. Costs nothing at runtime. + let () = Self::VALID_PARAMS; + + // Wrong kind of key: refuse to use, say, a MAC key or an unclassified buffer as a cipher + // key. Key separation between algorithms is a security property, not a formality. + if !(key.key_type() == KeyType::SymmetricCipherKey + || key.key_type() == KeyType::CryptographicRandom) + { + return Err(KeyMaterialError::InvalidKeyType( + "AES::new(): key must be a SymmetricCipherKey or CryptographicRandom KeyType", + ) + .into()); + } + + // The KeyMaterial capacity is KEY_LEN, so the only way to be wrong is to be short, which + // would mean silently using a key padded with zeros. + if key.key_len() != KEY_LEN { + return Err(KeyMaterialError::InvalidLength.into()); + } + + // A key tagged weaker than the algorithm means the caller's key does not actually deliver + // the security level they think they are getting from this variant. + if key.security_strength() < Self::MAX_SECURITY_STRENGTH { + return Err(KeyMaterialError::SecurityStrength( + "AES::new(): key security strength is lower than the AES variant requires", + ) + .into()); + } + + // Copy the key into a fixed-size, scrubbed-on-drop buffer so that KEYEXPANSION() can take + // it as a compile-time-sized array. The length check above guarantees that ref_to_bytes() + // is exactly KEY_LEN bytes long, so copy_from_slice() cannot panic here. + let mut key_bytes = Secret::<[u8; KEY_LEN]>::new(); + key_bytes.copy_from_slice(key.ref_to_bytes()); + + Ok(Self { w: key_expansion::(&key_bytes) }) + } + + /// One-shot API: expands `key` and encrypts exactly one block with it. + /// + /// Convenient for a single block, but it runs the full key expansion on every call. If you have + /// more than one block, build an engine with [`AES::new`] once and call + /// [`AESEngine::encrypt_block`] repeatedly. + /// + /// # Errors + /// + /// As [`AES::new`]. + pub fn encrypt_single_block( + key: &KeyMaterial, + plaintext: &[u8; AES_BLOCK_LEN], + ) -> Result<[u8; AES_BLOCK_LEN], SymmetricCipherError> { + // The engine, and with it the key schedule, is scrubbed when it drops at the end of this + // expression. + Ok(Self::new(key)?.encrypt_block(plaintext)) + } + + /// One-shot API: expands `key` and decrypts exactly one block with it. + /// + /// # Errors + /// + /// As [`AES::new`]. + pub fn decrypt_single_block( + key: &KeyMaterial, + ciphertext: &[u8; AES_BLOCK_LEN], + ) -> Result<[u8; AES_BLOCK_LEN], SymmetricCipherError> { + Ok(Self::new(key)?.decrypt_block(ciphertext)) + } + + /// ADDROUNDKEY(): XORs round key `round` of the schedule into the state + /// (FIPS 197 Section 5.1.4). + /// + /// Eq (5.9) is: + /// + /// ```text + /// [s'(0,c), s'(1,c), s'(2,c), s'(3,c)] = [s(0,c), s(1,c), s(2,c), s(3,c)] XOR w[4*round + c] + /// ``` + /// + /// Column `c` of the state is the contiguous run `state[4c .. 4c+4]` and the bytes of `w[i]` + /// are its big-endian bytes `[a0, a1, a2, a3]`, so this is a straight byte-wise XOR with no + /// index arithmetic beyond the column offset. + /// + /// `round` ranges over `0 ..= Nr`, and `w` has `4 * (Nr + 1)` words, so `4 * round + c` is + /// always in bounds (Section 5.1.4: ADDROUNDKEY() is invoked `Nr + 1` times). + #[inline(always)] + fn add_round_key(state: &mut [u8; AES_BLOCK_LEN], w: &[u32; W_WORDS], round: usize) { + for c in 0..NB { + let round_key_word = w[4 * round + c].to_be_bytes(); + state[4 * c] ^= round_key_word[0]; + state[4 * c + 1] ^= round_key_word[1]; + state[4 * c + 2] ^= round_key_word[2]; + state[4 * c + 3] ^= round_key_word[3]; + } + } + + /// CIPHER(): FIPS 197 Algorithm 1, the forward permutation on one block. + /// + /// The line numbers in the comments are Algorithm 1's. + fn cipher(&self, input: &[u8; AES_BLOCK_LEN], output: &mut [u8; AES_BLOCK_LEN]) { + // 2: state <- in + // Section 3.4 Eq (3.6) is s[r, c] = in[r + 4c], which in this layout is a plain copy. + // Held in a Secret so that the intermediate round states are scrubbed on the way out. + let mut state = Secret::<[u8; AES_BLOCK_LEN]>::new(); + *state = *input; + + // 3: state <- ADDROUNDKEY(state, w[0..3]) + Self::add_round_key(&mut state, &self.w, 0); + + // 4: for round from 1 to Nr - 1 + for round in 1..NR { + sub_bytes(&mut state); // 5 + shift_rows(&mut state); // 6 + mix_columns(&mut state); // 7 + Self::add_round_key(&mut state, &self.w, round); // 8 + } // 9: end for + + // 10-12: the final round, which differs in that MIXCOLUMNS() is omitted. + sub_bytes(&mut state); // 10 + shift_rows(&mut state); // 11 + Self::add_round_key(&mut state, &self.w, NR); // 12 + + // 13: return state -- Eq (3.7), out[r + 4c] = s[r, c], again a plain copy. + *output = *state; + } + + /// INVCIPHER(): FIPS 197 Algorithm 3, the inverse permutation on one block. + /// + /// This is the straight inverse cipher, not the equivalent inverse cipher of Section 5.3.5: + /// EQINVCIPHER() would let decryption reuse the encryption round structure, but it needs a + /// separate, INVMIXCOLUMNS()-transformed key schedule (Algorithm 5), which would mean either + /// storing a second schedule per engine or deriving one per call. Algorithm 3 reuses the one + /// schedule we already have. + /// + /// The line numbers in the comments are Algorithm 3's. + fn inv_cipher(&self, input: &[u8; AES_BLOCK_LEN], output: &mut [u8; AES_BLOCK_LEN]) { + // 2: state <- in + let mut state = Secret::<[u8; AES_BLOCK_LEN]>::new(); + *state = *input; + + // 3: state <- ADDROUNDKEY(state, w[4*Nr .. 4*Nr+3]) + Self::add_round_key(&mut state, &self.w, NR); + + // 4: for round from Nr - 1 downto 1 + for round in (1..NR).rev() { + inv_shift_rows(&mut state); // 5 + inv_sub_bytes(&mut state); // 6 + Self::add_round_key(&mut state, &self.w, round); // 7 + inv_mix_columns(&mut state); // 8 + } // 9: end for + + // 10-12: the final iteration, which omits INVMIXCOLUMNS(). + inv_shift_rows(&mut state); // 10 + inv_sub_bytes(&mut state); // 11 + Self::add_round_key(&mut state, &self.w, 0); // 12 + + // 13: return state + *output = *state; + } +} + +/// The block-level interface shared by [`AES128`], [`AES192`] and [`AES256`]. +/// +/// Every method here operates on exactly one 128-bit block and cannot fail: a block cipher is a +/// permutation of blocks (FIPS 197 Section 1), so with the key already validated by [`AES::new`] +/// there is nothing left to reject. The block length is fixed by the type system rather than +/// checked, which is why none of these return a `Result`. +/// +/// This trait exists so that code layered on top of AES -- a mode of operation, say -- can be +/// written once against `E: AESEngine` instead of being repeated per variant. +/// +/// # ๐Ÿšจ Security ๐Ÿšจ +/// +/// These are raw block operations. Encrypting more than one block by calling +/// [`AESEngine::encrypt_block`] in a loop is ECB mode, which leaks equality of plaintext blocks and +/// is not a secure way to encrypt data. See the crate-level "Security Considerations". +pub trait AESEngine: Algorithm + Sized { + /// The cipher key length in bytes for this variant: 16, 24 or 32 (FIPS 197 Table 3). + const KEY_LEN: usize; + + /// The number of rounds `Nr` for this variant: 10, 12 or 14 (FIPS 197 Table 3). + const NUM_ROUNDS: usize; + + /// The block length in bytes. 128 bits for every AES variant (FIPS 197 Table 3). + const BLOCK_LEN: usize = AES_BLOCK_LEN; + + /// Applies CIPHER() (FIPS 197 Algorithm 1) to one block, returning the result. + fn encrypt_block(&self, plaintext: &[u8; AES_BLOCK_LEN]) -> [u8; AES_BLOCK_LEN]; + + /// As [`AESEngine::encrypt_block`], but writes into a caller-provided buffer. + /// + /// Returns the number of bytes written, which is always [`AES_BLOCK_LEN`]; it is returned for + /// consistency with the `_out` conventions elsewhere in the library. + fn encrypt_block_out( + &self, + plaintext: &[u8; AES_BLOCK_LEN], + ciphertext: &mut [u8; AES_BLOCK_LEN], + ) -> usize; + + /// Applies INVCIPHER() (FIPS 197 Algorithm 3) to one block, returning the result. + fn decrypt_block(&self, ciphertext: &[u8; AES_BLOCK_LEN]) -> [u8; AES_BLOCK_LEN]; + + /// As [`AESEngine::decrypt_block`], but writes into a caller-provided buffer. + /// + /// Returns the number of bytes written, which is always [`AES_BLOCK_LEN`]. + fn decrypt_block_out( + &self, + ciphertext: &[u8; AES_BLOCK_LEN], + plaintext: &mut [u8; AES_BLOCK_LEN], + ) -> usize; +} + +impl AESEngine + for AES +where + Self: Algorithm, +{ + const KEY_LEN: usize = KEY_LEN; + const NUM_ROUNDS: usize = NR; + + fn encrypt_block(&self, plaintext: &[u8; AES_BLOCK_LEN]) -> [u8; AES_BLOCK_LEN] { + let mut ciphertext = [0u8; AES_BLOCK_LEN]; + self.cipher(plaintext, &mut ciphertext); + ciphertext + } + + fn encrypt_block_out( + &self, + plaintext: &[u8; AES_BLOCK_LEN], + ciphertext: &mut [u8; AES_BLOCK_LEN], + ) -> usize { + self.cipher(plaintext, ciphertext); + AES_BLOCK_LEN + } + + fn decrypt_block(&self, ciphertext: &[u8; AES_BLOCK_LEN]) -> [u8; AES_BLOCK_LEN] { + let mut plaintext = [0u8; AES_BLOCK_LEN]; + self.inv_cipher(ciphertext, &mut plaintext); + plaintext + } + + fn decrypt_block_out( + &self, + ciphertext: &[u8; AES_BLOCK_LEN], + plaintext: &mut [u8; AES_BLOCK_LEN], + ) -> usize { + self.inv_cipher(ciphertext, plaintext); + AES_BLOCK_LEN + } +} + +/// Redacting: prints the variant but never any part of the key schedule, so an engine cannot leak +/// key material into a log line or a panic message. +impl fmt::Debug + for AES +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AES {{ key_size: {} bits, rounds: {}, key_schedule: }}", + KEY_LEN * 8, + NR + ) + } +} + +// No `Drop` impl is needed: the only field is a `Secret`, which volatile-scrubs itself on drop, and +// adding a `Drop` here would only risk getting that ordering wrong. + +#[cfg(test)] +mod tests { + use super::*; + + /// FIPS 197 Appendix B, "Cipher Example": the round-key column of that worked example is the + /// AES-128 key schedule for key 2b7e151628aed2a6abf7158809cf4f3c, regrouped into round keys. + /// This checks that [`AES::add_round_key`] reads the schedule with the round indexing of + /// Eq (5.9) -- an off-by-one or a transposed word here would still produce a self-consistent + /// cipher that simply computed the wrong answer. + #[test] + fn add_round_key_uses_the_round_key_of_eq_5_9() { + // w[0..4] of Appendix A.1, ie the key itself, is round key 0. + let mut w = [0u32; AES128_KEY_SCHEDULE_WORDS]; + w[0] = 0x2b7e1516; + w[1] = 0x28aed2a6; + w[2] = 0xabf71588; + w[3] = 0x09cf4f3c; + // w[4..8] of Appendix A.1 is round key 1. + w[4] = 0xa0fafe17; + w[5] = 0x88542cb1; + w[6] = 0x23a33939; + w[7] = 0x2a6c7605; + + // XOR-ing round key 0 into the all-zero state must reproduce the round key itself... + let mut state = [0u8; AES_BLOCK_LEN]; + AES128::add_round_key(&mut state, &w, 0); + assert_eq!( + state, + [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, + 0x4f, 0x3c + ] + ); + + // ... and round 1 must use w[4..8], not w[0..4] again. + let mut state = [0u8; AES_BLOCK_LEN]; + AES128::add_round_key(&mut state, &w, 1); + assert_eq!( + state, + [ + 0xa0, 0xfa, 0xfe, 0x17, 0x88, 0x54, 0x2c, 0xb1, 0x23, 0xa3, 0x39, 0x39, 0x2a, 0x6c, + 0x76, 0x05 + ] + ); + + // ADDROUNDKEY() is its own inverse (Section 5.3.4). + AES128::add_round_key(&mut state, &w, 1); + assert_eq!(state, [0u8; AES_BLOCK_LEN]); + } + + /// The initial ADDROUNDKEY() of CIPHER() (Algorithm 1 line 3) is the first thing that happens + /// to a block, so its output is the plaintext XOR the key. The NIST intermediate-value file for + /// ECB-AES128 prints this as the first "KeyAddition" line. + #[test] + fn initial_add_round_key_matches_nist_intermediate_value() { + let mut w = [0u32; AES128_KEY_SCHEDULE_WORDS]; + w[0] = 0x2b7e1516; + w[1] = 0x28aed2a6; + w[2] = 0xabf71588; + w[3] = 0x09cf4f3c; + + // plaintext = 6BC1BEE2 2E409F96 E93D7E11 7393172A + let mut state = [ + 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, + 0x17, 0x2a, + ]; + AES128::add_round_key(&mut state, &w, 0); + + // expected = 40BFABF4 06EE4D30 42CA6B99 7A5C5816 + assert_eq!( + state, + [ + 0x40, 0xbf, 0xab, 0xf4, 0x06, 0xee, 0x4d, 0x30, 0x42, 0xca, 0x6b, 0x99, 0x7a, 0x5c, + 0x58, 0x16 + ] + ); + } +} diff --git a/crypto/aes/src/gf.rs b/crypto/aes/src/gf.rs new file mode 100644 index 0000000..bfdc759 --- /dev/null +++ b/crypto/aes/src/gf.rs @@ -0,0 +1,211 @@ +//! Arithmetic in the finite field GF(2^8), as specified in FIPS 197 Section 4. +//! +//! Every byte of the AES state is interpreted as an element of GF(2^8); ie as the polynomial +//! (FIPS 197 Eq 4.1): +//! +//! ```text +//! b(x) = b7*x^7 + b6*x^6 + b5*x^5 + b4*x^4 + b3*x^3 + b2*x^2 + b1*x + b0 +//! ``` +//! +//! Addition in the field is the bitwise XOR of the two bytes (Section 4.1), so this module only +//! needs to provide multiplication. +//! +//! Multiplication (Section 4.2) is polynomial multiplication reduced modulo the fixed polynomial +//! (Eq 4.3): +//! +//! ```text +//! m(x) = x^8 + x^4 + x^3 + x + 1 +//! ``` +//! +//! # Why only these specific multipliers? +//! +//! The cipher never needs a general field multiplication: MIXCOLUMNS() multiplies only by +//! {02} and {03} (Eq 5.6) and INVMIXCOLUMNS() only by {09}, {0b}, {0d} and {0e} (Eq 5.13). +//! Each of those is expressed here as a short chain of [`xtimes`] calls plus XORs, exactly as +//! suggested by FIPS 197 Section 4.2 ("Multiplication by higher powers of x ... can be implemented +//! by the repeated application of xTimes()"). A general multiply routine would either need a +//! data-dependent loop or a log/antilog table, both of which leak the multiplicand through +//! timing or cache state; the fixed chains below are branch-free and table-free. +//! +//! # Constant-time behaviour +//! +//! All functions in this module are branch-free and index-free: they consist only of shifts, XORs +//! and masks over the input byte, so neither their execution time nor their memory access pattern +//! depends on the (secret) value being multiplied. + +/// Multiplies `b` by {02} in GF(2^8); ie FIPS 197 Eq (4.5) xTimes(b). +/// +/// The spec writes this as a conditional on the high bit of `b`: +/// +/// ```text +/// xTimes(b) = {b6 b5 b4 b3 b2 b1 b0 0} if b7 = 0 +/// xTimes(b) = {b6 b5 b4 b3 b2 b1 b0 0} XOR {0 0 0 1 1 0 1 1} if b7 = 1 +/// ``` +/// +/// We evaluate both arms unconditionally and select between them with a mask, so that the timing +/// and the branch-predictor state do not depend on the secret value of `b`. Do not "simplify" this +/// back into an `if`. +#[inline(always)] +pub(crate) const fn xtimes(b: u8) -> u8 { + // `b >> 7` is exactly b7, so it is 0 or 1; `wrapping_neg()` maps 1 -> 0xFF and 0 -> 0x00. + let b7_mask = (b >> 7).wrapping_neg(); + + // `b << 1` is the polynomial multiplication by x. It discards b7, which is the degree-8 term + // that the modular reduction has to remove. + // {1b} == {0 0 0 1 1 0 1 1} is m(x) (Eq 4.3) with its x^8 term dropped, so XOR-ing it in is + // the reduction "mod m(x)" -- and it must only happen when a degree-8 term was actually + // produced, which is what the mask selects. + (b << 1) ^ (b7_mask & 0x1b) +} + +/// Multiplies `b` by {02}, one of the two MIXCOLUMNS() coefficients (FIPS 197 Eq 5.6). +/// +/// This is just [`xtimes`] under the name used by Eq (5.8), for readability at the call site. +#[inline(always)] +pub(crate) const fn mul_02(b: u8) -> u8 { + xtimes(b) +} + +/// Multiplies `b` by {03}, the other MIXCOLUMNS() coefficient (FIPS 197 Eq 5.6). +/// +/// {03} = {02} XOR {01} (ie x + 1), and multiplication distributes over field addition, so +/// b*{03} = xTimes(b) XOR b. +#[inline(always)] +pub(crate) const fn mul_03(b: u8) -> u8 { + xtimes(b) ^ b +} + +/// Multiplies `b` by {09}, an INVMIXCOLUMNS() coefficient (FIPS 197 Eq 5.13). +/// +/// {09} = {08} XOR {01}, ie x^3 + 1, so b*{09} = xTimes^3(b) XOR b. +#[inline(always)] +pub(crate) const fn mul_09(b: u8) -> u8 { + let x8 = xtimes(xtimes(xtimes(b))); + x8 ^ b +} + +/// Multiplies `b` by {0b}, an INVMIXCOLUMNS() coefficient (FIPS 197 Eq 5.13). +/// +/// {0b} = {08} XOR {02} XOR {01}, ie x^3 + x + 1. +#[inline(always)] +pub(crate) const fn mul_0b(b: u8) -> u8 { + let x2 = xtimes(b); + let x8 = xtimes(xtimes(x2)); + x8 ^ x2 ^ b +} + +/// Multiplies `b` by {0d}, an INVMIXCOLUMNS() coefficient (FIPS 197 Eq 5.13). +/// +/// {0d} = {08} XOR {04} XOR {01}, ie x^3 + x^2 + 1. +#[inline(always)] +pub(crate) const fn mul_0d(b: u8) -> u8 { + let x4 = xtimes(xtimes(b)); + let x8 = xtimes(x4); + x8 ^ x4 ^ b +} + +/// Multiplies `b` by {0e}, an INVMIXCOLUMNS() coefficient (FIPS 197 Eq 5.13). +/// +/// {0e} = {08} XOR {04} XOR {02}, ie x^3 + x^2 + x. Note there is no XOR of `b` itself here, +/// because {0e} has no constant term. +#[inline(always)] +pub(crate) const fn mul_0e(b: u8) -> u8 { + let x2 = xtimes(b); + let x4 = xtimes(x2); + let x8 = xtimes(x4); + x8 ^ x4 ^ x2 +} + +/// A general GF(2^8) multiplication, used only to cross-check the fixed multipliers above and to +/// derive the S-box from its mathematical definition in the unit tests. +/// +/// This is deliberately **not** available outside of tests: the loop below branches on the bits of +/// `b`, so it is not constant-time and must never be used on secret data. +#[cfg(test)] +pub(crate) fn gf_mul(a: u8, b: u8) -> u8 { + let mut product = 0u8; + let mut a_shifted = a; + let mut b_remaining = b; + + // Section 4.2: multiply the polynomials, accumulating a*x^i for each set bit i of b, reducing + // mod m(x) as we go (which the spec notes may be applied to intermediate steps). + while b_remaining != 0 { + if b_remaining & 1 == 1 { + product ^= a_shifted; + } + a_shifted = xtimes(a_shifted); + b_remaining >>= 1; + } + + product +} + +/// Raises `b` to the power `exponent` in GF(2^8) by repeated multiplication. Tests only. +#[cfg(test)] +pub(crate) fn gf_pow(b: u8, exponent: u32) -> u8 { + let mut acc = 1u8; + for _ in 0..exponent { + acc = gf_mul(acc, b); + } + acc +} + +#[cfg(test)] +mod tests { + use super::*; + + /// FIPS 197 Eq (4.6): the worked example of repeated xTimes() applied to b = {57}. + #[test] + fn xtimes_matches_fips197_eq_4_6() { + assert_eq!(xtimes(0x57), 0xae); // {57} * {02} + assert_eq!(xtimes(0xae), 0x47); // {57} * {04} + assert_eq!(xtimes(0x47), 0x8e); // {57} * {08} + assert_eq!(xtimes(0x8e), 0x07); // {57} * {10} + assert_eq!(xtimes(0x07), 0x0e); // {57} * {20} + assert_eq!(xtimes(0x0e), 0x1c); // {57} * {40} + assert_eq!(xtimes(0x1c), 0x38); // {57} * {80} + } + + /// FIPS 197 Eq (4.7): {57} * {13} = {57} XOR {ae} XOR {07} = {fe}. + #[test] + fn gf_mul_matches_fips197_eq_4_7() { + assert_eq!(gf_mul(0x57, 0x13), 0xfe); + // Also check the decomposition the spec uses to get there: {13} = {01} + {02} + {10}. + assert_eq!(0x57 ^ 0xae ^ 0x07, 0xfe); + } + + /// The fixed multipliers must agree with a general field multiplication for every input byte. + /// This is what pins the xTimes() chains in [`mul_09`] .. [`mul_0e`] to the coefficients that + /// FIPS 197 Eq (5.6) and Eq (5.13) actually specify. + #[test] + fn fixed_multipliers_match_general_multiplication() { + for b in 0..=u8::MAX { + assert_eq!(mul_02(b), gf_mul(b, 0x02), "mul_02({b:#04x})"); + assert_eq!(mul_03(b), gf_mul(b, 0x03), "mul_03({b:#04x})"); + assert_eq!(mul_09(b), gf_mul(b, 0x09), "mul_09({b:#04x})"); + assert_eq!(mul_0b(b), gf_mul(b, 0x0b), "mul_0b({b:#04x})"); + assert_eq!(mul_0d(b), gf_mul(b, 0x0d), "mul_0d({b:#04x})"); + assert_eq!(mul_0e(b), gf_mul(b, 0x0e), "mul_0e({b:#04x})"); + } + } + + /// FIPS 197 Eq (4.10) and (4.11): b^254 is the multiplicative inverse of every non-zero b. + /// This is the property that the S-box is built on, so it is worth pinning independently. + #[test] + fn gf_pow_254_is_the_multiplicative_inverse() { + for b in 1..=u8::MAX { + assert_eq!(gf_mul(b, gf_pow(b, 254)), 0x01, "inverse of {b:#04x}"); + } + } + + /// The field has no zero divisors, and {01} is the identity. + #[test] + fn gf_mul_basic_properties() { + for b in 0..=u8::MAX { + assert_eq!(gf_mul(b, 0x00), 0x00); + assert_eq!(gf_mul(b, 0x01), b); + // Commutativity, spot-checked across the whole range. + assert_eq!(gf_mul(b, 0x57), gf_mul(0x57, b)); + } + } +} diff --git a/crypto/aes/src/key_schedule.rs b/crypto/aes/src/key_schedule.rs new file mode 100644 index 0000000..d28e267 --- /dev/null +++ b/crypto/aes/src/key_schedule.rs @@ -0,0 +1,271 @@ +//! KEYEXPANSION(): derivation of the AES round keys from the cipher key +//! (FIPS 197 Section 5.2, Algorithm 2). +//! +//! # Word representation +//! +//! FIPS 197 Section 3.5 defines a word as a sequence of four bytes `[a0, a1, a2, a3]` with `a0` +//! leftmost. We hold each key schedule word as a `u32` whose big-endian byte order is that same +//! sequence, ie `a0` is the most significant byte. That makes: +//! +//! * ROTWORD() (Eq 5.10) a single `rotate_left(8)`, +//! * the round constants of Table 5 plain `u32` XOR operands, and +//! * ADDROUNDKEY() (Eq 5.9) a XOR of `w[i].to_be_bytes()` into a column of the state, since a +//! column of the state and the bytes of a word are indexed the same way. +//! +//! # ๐Ÿšจ Security ๐Ÿšจ +//! +//! The expanded key schedule is key material: every round key is derived from the cipher key by an +//! invertible transformation, so recovering any four consecutive words of the schedule recovers the +//! cipher key. It is therefore returned wrapped in a [`Secret`], which scrubs it on drop. + +use crate::tables::{RCON, SBOX}; +use bouncycastle_utils::secret::Secret; + +/// SUBWORD(): applies the S-box to each of the four bytes of a word (FIPS 197 Eq 5.11). +#[inline(always)] +const fn sub_word(word: u32) -> u32 { + let [a0, a1, a2, a3] = word.to_be_bytes(); + u32::from_be_bytes([SBOX[a0 as usize], SBOX[a1 as usize], SBOX[a2 as usize], SBOX[a3 as usize]]) +} + +/// ROTWORD(): cyclically permutes the four bytes of a word (FIPS 197 Eq 5.10). +/// +/// `ROTWORD([a0, a1, a2, a3]) = [a1, a2, a3, a0]`. With `a0` held as the most significant byte, +/// moving every byte one position left in the sequence is a rotate *left* by 8 bits. +#[inline(always)] +const fn rot_word(word: u32) -> u32 { + word.rotate_left(8) +} + +/// KEYEXPANSION(): expands the cipher key into the `4 * (Nr + 1)`-word key schedule `w` +/// (FIPS 197 Algorithm 2). +/// +/// * `KEY_LEN` is the cipher key length in bytes, so `Nk = KEY_LEN / 4` words (Table 3). +/// * `W_WORDS` is the length of the key schedule in 32-bit words, ie `4 * (Nr + 1)`. +/// +/// Both are compile-time parameters, so this function cannot be called with a mismatched key +/// length or output size, and it has no failure mode: every 16-, 24- or 32-byte input is a valid +/// AES key (FIPS 197 Section 6.2 imposes no keying restrictions). +/// +/// The caller ([`crate::aes::AES::new`]) is responsible for checking that the parameter triple is +/// one of the three combinations in Table 3. +pub(crate) fn key_expansion( + key: &[u8; KEY_LEN], +) -> Secret<[u32; W_WORDS]> { + // Nk: the number of 32-bit words comprising the key (Section 2.3). + let nk = KEY_LEN / 4; + + // Allocate the schedule zeroed and fill it in place, so no unprotected copy of the round keys + // is ever materialized (see the Secret docs in bouncycastle-utils). + let mut w = Secret::<[u32; W_WORDS]>::new(); + + // Alg 2 lines 2-6: the first Nk words of the expanded key are the key itself. + // w[i] <- key[4*i .. 4*i+3] + let mut i = 0; + while i < nk { + w[i] = u32::from_be_bytes([key[4 * i], key[4 * i + 1], key[4 * i + 2], key[4 * i + 3]]); + i += 1; + } + + // Alg 2 lines 7-16. The loop bound `i <= 4*Nr + 3` is `i < W_WORDS`, because + // W_WORDS == 4 * (Nr + 1) == 4*Nr + 4. + // On entry i == Nk, as the spec notes at line 6. + while i < W_WORDS { + // 8: temp <- w[i-1] + let mut temp = w[i - 1]; + + // 9-10: if i mod Nk = 0 then temp <- SUBWORD(ROTWORD(temp)) XOR Rcon[i/Nk] + if i % nk == 0 { + // Rcon is 1-indexed in Table 5 and 0-indexed here, hence the -1. + // i/nk >= 1 here because i >= nk, so this cannot underflow. + temp = sub_word(rot_word(temp)) ^ RCON[i / nk - 1]; + } + // 11-12: else if Nk > 6 and i mod Nk = 4 then temp <- SUBWORD(temp) + // This extra substitution applies only to AES-256 (Nk = 8); see Figure 8. + else if nk > 6 && i % nk == 4 { + temp = sub_word(temp); + } + + // 14: w[i] <- w[i-Nk] XOR temp + w[i] = w[i - nk] ^ temp; + + // 15: i <- i + 1 + i += 1; + } + + // Note: the branches above are decided entirely by `i` and `Nk`, which are public parameters + // of the algorithm, never by key material. The expansion is therefore key-independent in its + // control flow. + + // 17: return w + w +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::aes::{ + AES128_KEY_LEN, AES128_KEY_SCHEDULE_WORDS, AES192_KEY_LEN, AES192_KEY_SCHEDULE_WORDS, + AES256_KEY_LEN, AES256_KEY_SCHEDULE_WORDS, + }; + + /// FIPS 197 Eq (5.10), the worked ROTWORD() example implied by Appendix A.1 at i = 4: + /// ROTWORD(09cf4f3c) = cf4f3c09. + #[test] + fn rot_word_matches_fips197() { + assert_eq!(rot_word(0x09cf4f3c), 0xcf4f3c09); + // Rotating four times returns the original word. + let mut word = 0x09cf4f3c; + for _ in 0..4 { + word = rot_word(word); + } + assert_eq!(word, 0x09cf4f3c); + } + + /// FIPS 197 Appendix A.1 at i = 4: SUBWORD(cf4f3c09) = 8a84eb01. + #[test] + fn sub_word_matches_fips197() { + assert_eq!(sub_word(0xcf4f3c09), 0x8a84eb01); + // Appendix A.3 at i = 8: SUBWORD(14dff409) = fa9ebf01. + assert_eq!(sub_word(0x14dff409), 0xfa9ebf01); + } + + /// FIPS 197 Appendix A.1: the complete expansion of the 128-bit key + /// `2b7e151628aed2a6abf7158809cf4f3c`, all 44 words. + #[test] + fn key_expansion_matches_fips197_appendix_a1() { + const KEY: [u8; AES128_KEY_LEN] = [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, + 0x4f, 0x3c, + ]; + #[rustfmt::skip] + const EXPECTED: [u32; AES128_KEY_SCHEDULE_WORDS] = [ + // w[0..4] are the key itself + 0x2b7e1516, 0x28aed2a6, 0xabf71588, 0x09cf4f3c, + 0xa0fafe17, 0x88542cb1, 0x23a33939, 0x2a6c7605, // i = 4..7 + 0xf2c295f2, 0x7a96b943, 0x5935807a, 0x7359f67f, // i = 8..11 + 0x3d80477d, 0x4716fe3e, 0x1e237e44, 0x6d7a883b, // i = 12..15 + 0xef44a541, 0xa8525b7f, 0xb671253b, 0xdb0bad00, // i = 16..19 + 0xd4d1c6f8, 0x7c839d87, 0xcaf2b8bc, 0x11f915bc, // i = 20..23 + 0x6d88a37a, 0x110b3efd, 0xdbf98641, 0xca0093fd, // i = 24..27 + 0x4e54f70e, 0x5f5fc9f3, 0x84a64fb2, 0x4ea6dc4f, // i = 28..31 + 0xead27321, 0xb58dbad2, 0x312bf560, 0x7f8d292f, // i = 32..35 + 0xac7766f3, 0x19fadc21, 0x28d12941, 0x575c006e, // i = 36..39 + 0xd014f9a8, 0xc9ee2589, 0xe13f0cc8, 0xb6630ca6, // i = 40..43 + ]; + + let w = key_expansion::(&KEY); + for (i, expected) in EXPECTED.iter().enumerate() { + assert_eq!(w[i], *expected, "w[{i}]"); + } + } + + /// FIPS 197 Appendix A.2: the expansion of the 192-bit key + /// `8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b`. + /// + /// The first six words are the key, and i = 6, 7, 8 exercise the `i mod Nk = 0` branch and the + /// two words that follow it. The remaining words of this schedule are covered end to end by + /// the AES-192 known-answer tests in tests/aes_tests.rs, which use this exact key. + #[test] + fn key_expansion_matches_fips197_appendix_a2() { + const KEY: [u8; AES192_KEY_LEN] = [ + 0x8e, 0x73, 0xb0, 0xf7, 0xda, 0x0e, 0x64, 0x52, 0xc8, 0x10, 0xf3, 0x2b, 0x80, 0x90, + 0x79, 0xe5, 0x62, 0xf8, 0xea, 0xd2, 0x52, 0x2c, 0x6b, 0x7b, + ]; + #[rustfmt::skip] + const EXPECTED_PREFIX: [u32; 9] = [ + // w[0..6] are the key itself + 0x8e73b0f7, 0xda0e6452, 0xc810f32b, 0x809079e5, 0x62f8ead2, 0x522c6b7b, + 0xfe0c91f7, // i = 6 + 0x2402f5a5, // i = 7 + 0xec12068e, // i = 8 + ]; + + let w = key_expansion::(&KEY); + for (i, expected) in EXPECTED_PREFIX.iter().enumerate() { + assert_eq!(w[i], *expected, "w[{i}]"); + } + } + + /// FIPS 197 Appendix A.3: the expansion of the 256-bit key + /// `603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4`. + /// + /// Words 8 through 22 cover both of AES-256's special cases: the `i mod Nk = 0` branch at + /// i = 8 and i = 16, and the AES-256-only `Nk > 6 and i mod Nk = 4` branch at i = 12 and + /// i = 20. The rest of the schedule is covered end to end by the AES-256 known-answer tests in + /// tests/aes_tests.rs, which use this exact key. + #[test] + fn key_expansion_matches_fips197_appendix_a3() { + const KEY: [u8; AES256_KEY_LEN] = [ + 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, + ]; + #[rustfmt::skip] + const EXPECTED_PREFIX: [u32; 23] = [ + // w[0..8] are the key itself + 0x603deb10, 0x15ca71be, 0x2b73aef0, 0x857d7781, + 0x1f352c07, 0x3b6108d7, 0x2d9810a3, 0x0914dff4, + 0x9ba35411, 0x8e6925af, 0xa51a8b5f, 0x2067fcde, // i = 8..11 + 0xa8b09c1a, 0x93d194cd, 0xbe49846e, 0xb75d5b9a, // i = 12..15 + 0xd59aecb8, 0x5bf3c917, 0xfee94248, 0xde8ebe96, // i = 16..19 + 0xb5a9328a, 0x2678a647, 0x98312229, // i = 20..22 + ]; + + let w = key_expansion::(&KEY); + for (i, expected) in EXPECTED_PREFIX.iter().enumerate() { + assert_eq!(w[i], *expected, "w[{i}]"); + } + } + + /// The expansion must be deterministic, and a single-bit change anywhere in the key must + /// change every round key from the point of injection onward (the avalanche the recursion of + /// Alg 2 is designed to produce). A schedule that ignored part of the key -- a plausible + /// off-by-one in the `w[i - nk]` term -- would fail this. + #[test] + fn key_expansion_depends_on_every_key_byte() { + const KEY: [u8; AES128_KEY_LEN] = [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, + 0x4f, 0x3c, + ]; + let baseline = key_expansion::(&KEY); + + // Deterministic. + let again = key_expansion::(&KEY); + assert_eq!(*baseline, *again); + + for flipped_byte in 0..AES128_KEY_LEN { + let mut key = KEY; + key[flipped_byte] ^= 0x01; + let w = key_expansion::(&key); + + assert_ne!(*w, *baseline, "flipping key byte {flipped_byte} changed nothing"); + // The last round key must always differ: every key byte feeds it. + assert_ne!( + w[AES128_KEY_SCHEDULE_WORDS - 4..], + baseline[AES128_KEY_SCHEDULE_WORDS - 4..], + "flipping key byte {flipped_byte} left the final round key unchanged" + ); + } + } + + /// The first `Nk` words must be the key verbatim for all three variants (Alg 2 lines 2-6), and + /// no word of the schedule may be left at its zero-initialized value for these test keys. + #[test] + fn key_expansion_copies_the_key_then_fills_the_schedule() { + let key128 = [0x11u8; AES128_KEY_LEN]; + let w = key_expansion::(&key128); + assert_eq!(w[0..4], [0x1111_1111u32; 4]); + assert!(w.iter().all(|word| *word != 0), "an unwritten word remained zero"); + + let key192 = [0x22u8; AES192_KEY_LEN]; + let w = key_expansion::(&key192); + assert_eq!(w[0..6], [0x2222_2222u32; 6]); + assert!(w.iter().all(|word| *word != 0), "an unwritten word remained zero"); + + let key256 = [0x33u8; AES256_KEY_LEN]; + let w = key_expansion::(&key256); + assert_eq!(w[0..8], [0x3333_3333u32; 8]); + assert!(w.iter().all(|word| *word != 0), "an unwritten word remained zero"); + } +} diff --git a/crypto/aes/src/lib.rs b/crypto/aes/src/lib.rs new file mode 100644 index 0000000..1162b7b --- /dev/null +++ b/crypto/aes/src/lib.rs @@ -0,0 +1,261 @@ +//! This crate implements the Advanced Encryption Standard (AES) block cipher engine as per +//! FIPS 197 (updated May 2023). +//! +//! All three key sizes of the Standard are provided: [`AES128`], [`AES192`] and [`AES256`]. +//! +//! # What a block cipher engine is (and is not) +//! +//! A block cipher is "a family of permutations of blocks that is parameterized by a sequence of +//! bits called the key" (FIPS 197 Section 1). This crate provides exactly that permutation, on one +//! 128-bit block at a time: +//! +//! * [`AESEngine::encrypt_block`] is `CIPHER()`, FIPS 197 Algorithm 1. +//! * [`AESEngine::decrypt_block`] is `INVCIPHER()`, FIPS 197 Algorithm 3. +//! +//! It is a building block, not something you should encrypt a message with directly. Encrypting +//! data requires a **mode of operation** layered on top -- CBC, CTR, GCM and so on -- which is what +//! turns a 16-byte permutation into something that can safely handle a message of any length. FIPS +//! 197 itself is explicit about this: "The algorithm shall be used in conjunction with a +//! FIPS-approved or NIST-recommended mode of operation" (announcement section 8), and the modes are +//! specified separately in the NIST SP 800-38 series. +//! +//! Consequently this crate deliberately does **not** implement the library's +//! [`SymmetricCipher`](bouncycastle_core::traits::SymmetricCipher), +//! [`BlockCipher`](bouncycastle_core::traits::BlockCipher) or +//! [`AEADCipher`](bouncycastle_core::traits::AEADCipher) traits, and there is no `bc-rust` CLI +//! subcommand for it. Those interfaces are about encrypting *data*: they generate and carry +//! initialization vectors, handle padding, and authenticate. Those are properties of a mode, not of +//! the permutation, and the only mode that a raw engine can offer -- ECB -- must not be presented +//! as a general-purpose way to encrypt (see "Security Considerations" below). When the +//! mode-of-operation crates land, they will be the types that implement those traits, take the CLI +//! subcommands, and carry the per-mode algorithm OIDs. +//! +//! # Usage Examples +//! +//! ## Encrypting and decrypting a single block +//! +//! ```rust +//! use bouncycastle_aes::{AES128, AES128Key, AESEngine}; +//! use bouncycastle_core::key_material::KeyType; +//! +//! // The key and plaintext from the worked example in FIPS 197 Appendix B. +//! let key = AES128Key::from_bytes_as_type( +//! &[ +//! 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, +//! 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, +//! ], +//! KeyType::SymmetricCipherKey, +//! ) +//! .unwrap(); +//! +//! // Expanding the key is the expensive part, so do it once... +//! let engine = AES128::new(&key).unwrap(); +//! +//! // ... then use the engine for as many blocks as you like. +//! let plaintext = [ +//! 0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, +//! 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, 0x34, +//! ]; +//! let ciphertext = engine.encrypt_block(&plaintext); +//! +//! assert_eq!( +//! ciphertext, +//! [ +//! 0x39, 0x25, 0x84, 0x1d, 0x02, 0xdc, 0x09, 0xfb, +//! 0xdc, 0x11, 0x85, 0x97, 0x19, 0x6a, 0x0b, 0x32, +//! ] +//! ); +//! assert_eq!(engine.decrypt_block(&ciphertext), plaintext); +//! ``` +//! +//! ## The one-shot API +//! +//! For a single block there is a static take-data-return-result form that expands the key, uses it, +//! and scrubs it again: +//! +//! ```rust +//! use bouncycastle_aes::{AES256, AES256Key}; +//! use bouncycastle_core::key_material::KeyType; +//! use bouncycastle_hex as hex; +//! +//! let key = AES256Key::from_bytes_as_type( +//! &hex::decode("603deb1015ca71be2b73aef0857d7781 +//! 1f352c073b6108d72d9810a30914dff4") +//! .unwrap(), +//! KeyType::SymmetricCipherKey, +//! ) +//! .unwrap(); +//! +//! let plaintext: [u8; 16] = +//! hex::decode("6bc1bee22e409f96e93d7e117393172a").unwrap().try_into().unwrap(); +//! +//! let ciphertext = AES256::encrypt_single_block(&key, &plaintext).unwrap(); +//! assert_eq!(hex::encode(&ciphertext), "f3eed1bdb5d2a03c064b5a7e3db181f8"); +//! ``` +//! +//! ## Writing into a caller-owned buffer +//! +//! Every operation has an `_out` form that writes into a buffer you provide, for callers that want +//! to control where the bytes land: +//! +//! ```rust +//! use bouncycastle_aes::{AES192, AES192Key, AESEngine, AES_BLOCK_LEN}; +//! use bouncycastle_core::key_material::KeyType; +//! use bouncycastle_hex as hex; +//! +//! let key = AES192Key::from_bytes_as_type( +//! &hex::decode("8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b").unwrap(), +//! KeyType::SymmetricCipherKey, +//! ) +//! .unwrap(); +//! let engine = AES192::new(&key).unwrap(); +//! +//! let plaintext: [u8; AES_BLOCK_LEN] = +//! hex::decode("6bc1bee22e409f96e93d7e117393172a").unwrap().try_into().unwrap(); +//! +//! let mut ciphertext = [0u8; AES_BLOCK_LEN]; +//! let bytes_written = engine.encrypt_block_out(&plaintext, &mut ciphertext); +//! +//! assert_eq!(bytes_written, AES_BLOCK_LEN); +//! assert_eq!(hex::encode(&ciphertext), "bd334f1d6e45f25ff712a214571fa5cc"); +//! ``` +//! +//! ## Writing code that is generic over the key size +//! +//! [`AESEngine`] is implemented by all three variants, so code that only needs the block +//! permutation -- a mode of operation, for instance -- can be written once: +//! +//! ```rust +//! use bouncycastle_aes::{AES128, AES128Key, AESEngine, AES_BLOCK_LEN}; +//! use bouncycastle_core::key_material::KeyType; +//! use bouncycastle_core::traits::Algorithm; +//! +//! /// Encrypts each block of `blocks` in place. (This is ECB mode: see the security notes!) +//! fn encrypt_all(engine: &E, blocks: &mut [[u8; AES_BLOCK_LEN]]) { +//! for block in blocks.iter_mut() { +//! *block = engine.encrypt_block(block); +//! } +//! } +//! +//! let key = AES128Key::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let engine = AES128::new(&key).unwrap(); +//! +//! let mut blocks = [[0u8; AES_BLOCK_LEN]; 4]; +//! encrypt_all(&engine, &mut blocks); +//! +//! assert_eq!(AES128::ALG_NAME, "AES-128"); +//! assert_eq!(::NUM_ROUNDS, 10); +//! ``` +//! +//! # Memory Usage +//! +//! An engine holds nothing but its expanded key schedule: `4 * (Nr + 1)` 32-bit words +//! (FIPS 197 Section 5.2). There is no other state, so an engine costs the same whether it has +//! encrypted no blocks or a billion. +//! +//! | Engine | Key schedule | Struct size (`size_of`) | +//! |-----------|--------------|-------------------------| +//! | [`AES128`] | 44 words | 176 bytes | +//! | [`AES192`] | 52 words | 208 bytes | +//! | [`AES256`] | 60 words | 240 bytes | +//! +//! Those sizes are asserted by a test in `tests/aes_tests.rs`, so this table cannot drift away from +//! the code. +//! +//! Stack usage of the block operations is a small constant: one 16-byte working state plus the +//! 16-byte output block, no heap allocation, no recursion, and no variable-length buffers. It does +//! not vary with the key size, the number of blocks processed, or anything an attacker controls. +//! That is why this crate has no harness in `/mem_usage_benches`: there is nothing to measure that +//! the table above does not already tell you. A mode of operation that buffers partial blocks will +//! be a different story. +//! +//! For the same reason there is no +//! [`Suspendable`](bouncycastle_core::traits::Suspendable) or +//! [`SuspendableKeyed`](bouncycastle_core::traits::SuspendableKeyed) implementation: those exist so +//! that an algorithm carrying state across a `do_update()`/`do_final()` sequence can be paused and +//! resumed. An engine carries no such state -- every block operation takes `&self` and completes -- +//! so the only thing there would be to serialize is the key schedule, and serializing key material +//! is exactly what those traits are designed to avoid. A streaming mode of operation, which does +//! carry state between calls, is where that becomes relevant. +//! +//! # Security Considerations +//! +//! ## Do not use a raw block cipher to encrypt data +//! +//! Calling [`AESEngine::encrypt_block`] once per block *is* ECB mode. ECB encrypts equal plaintext +//! blocks to equal ciphertext blocks, which leaks the structure of the plaintext (this is the +//! famous "ECB penguin"), and it provides no integrity protection whatsoever, so an attacker can +//! reorder, duplicate or splice your blocks undetected. Use an authenticated mode of operation. +//! +//! If you find yourself reaching for this crate directly to protect data, that is the signal that +//! you want a mode instead. +//! +//! ## Key hygiene +//! +//! Keys are passed as [`KeyMaterial`](bouncycastle_core::key_material::KeyMaterial) so that the +//! library can check that the bytes you handed over really are a full-entropy symmetric cipher key +//! of the right length, and so that they are scrubbed from memory when dropped. [`AES::new`] +//! rejects a key that is too short, tagged for a different algorithm, tagged at a lower security +//! strength than the variant needs, or all-zero (which arrives tagged +//! [`KeyType::Zeroized`](bouncycastle_core::key_material::KeyType::Zeroized)). Every one of those +//! rejections corresponds to a real, repeatedly-made deployment bug. +//! +//! The expanded key schedule is as sensitive as the key itself: the expansion is invertible, so any +//! four consecutive words of it recover the cipher key. It is held in a +//! [`Secret`](bouncycastle_utils::secret::Secret), which volatile-scrubs it when the engine drops +//! and refuses to print it in [`Debug`](core::fmt::Debug) output. The per-block working state is +//! scrubbed the same way. +//! +//! ## Cache-timing side channels +//! +//! This is a table-driven implementation: SUBBYTES() and the key expansion index a 256-byte S-box +//! with values derived from the key and the data. On a processor with a data cache, *which* cache +//! lines that touches depends on those secret values, and an attacker who can observe cache state +//! -- typically a process co-resident on the same machine -- may be able to recover key material. +//! FIPS 197 Section 6.4 names this attack explicitly, and it applies to every table-driven AES. +//! +//! What this crate does about it: +//! +//! * The GF(2^8) arithmetic and the state permutations are branch-free and table-free: no +//! secret-dependent branches, no secret-dependent indices (see the module docs in `gf.rs`). +//! * Only the 256-byte S-box is used. The common 4 KiB "T-table" optimization, which folds +//! MIXCOLUMNS() into the lookup, is faster but spreads each lookup across many more cache lines +//! and measurably widens this side channel. +//! * The key expansion's control flow depends only on public parameters (`Nk` and the word index), +//! never on key bytes. +//! +//! What it does not do: eliminate the S-box lookups. Doing so requires either a bitsliced +//! implementation or the CPU's AES instructions (AES-NI, ARMv8 Crypto Extensions), the latter being +//! unavailable to us since this library contains no assembly or intrinsics and forbids +//! `unsafe`. If you need resistance to a co-resident attacker, that is the direction to look. +//! +//! As with the rest of bc-rust: the constant-time properties claimed here are a best effort in safe +//! Rust, and Rust makes no guarantee that the optimizer preserves them. +//! +//! ## Keying restrictions +//! +//! There are none, beyond using a properly generated key: "When a cryptographic key has been +//! generated appropriately ... no restriction is imposed when the resulting key is used for the AES +//! algorithm" (FIPS 197 Section 6.2). There are no weak keys to avoid, and no key check values to +//! compute. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +pub mod aes; +mod gf; +mod key_schedule; +mod state; +mod tables; + +/*** Exported types ***/ +pub use aes::{AES, AES128, AES192, AES256, AESEngine}; +pub use aes::{AES128Key, AES192Key, AES256Key}; + +/*** Exported constants ***/ +pub use aes::{AES_128_NAME, AES_192_NAME, AES_256_NAME}; +pub use aes::{AES128_KEY_LEN, AES192_KEY_LEN, AES256_KEY_LEN}; +pub use aes::{AES128_KEY_SCHEDULE_WORDS, AES192_KEY_SCHEDULE_WORDS, AES256_KEY_SCHEDULE_WORDS}; +pub use aes::{AES128_NUM_ROUNDS, AES192_NUM_ROUNDS, AES256_NUM_ROUNDS}; +pub use state::AES_BLOCK_LEN; diff --git a/crypto/aes/src/state.rs b/crypto/aes/src/state.rs new file mode 100644 index 0000000..fc030f2 --- /dev/null +++ b/crypto/aes/src/state.rs @@ -0,0 +1,389 @@ +//! The four byte-oriented transformations of the AES state, and their inverses. +//! +//! | Function | FIPS 197 | Inverse | FIPS 197 | +//! |----------|----------|---------|----------| +//! | [`sub_bytes`] | Sec 5.1.1, Eq 5.2-5.4 | [`inv_sub_bytes`] | Sec 5.3.2 | +//! | [`shift_rows`] | Sec 5.1.2, Eq 5.5 | [`inv_shift_rows`] | Sec 5.3.1, Eq 5.12 | +//! | [`mix_columns`] | Sec 5.1.3, Eq 5.7-5.8 | [`inv_mix_columns`] | Sec 5.3.3, Eq 5.14-5.15 | +//! +//! ADDROUNDKEY() is not here because it needs the key schedule; it lives with the engine in +//! [`crate::aes`]. +//! +//! # The state layout, and why it is a flat 16-byte array +//! +//! FIPS 197 Section 3.4 defines the state as a 4x4 array of bytes `s[r, c]`, filled from the input +//! block by Eq (3.6): +//! +//! ```text +//! s[r, c] = in[r + 4c] for 0 <= r < 4 and 0 <= c < 4 +//! ``` +//! +//! We store the state as a flat `[u8; 16]` in exactly that order, so `state[r + 4 * c]` *is* +//! `s[r, c]`, and copying a block in or out (Eq 3.6 and 3.7) is a plain 16-byte copy with no +//! transposition. In this layout a *column* is a contiguous 4-byte run -- `state[4c .. 4c + 4]` -- +//! which is what MIXCOLUMNS() and ADDROUNDKEY() operate on, and it also matches the byte order of +//! a key schedule word (Section 3.5), so those two functions stay index-free. +//! +//! The tradeoff is that a *row* is strided by 4, which only SHIFTROWS() cares about; it is written +//! out longhand below. +//! +//! Every function here takes `&mut [u8; AES_BLOCK_LEN]`, so the block length is enforced by the +//! compiler rather than checked at runtime, and none of them can fail. + +use crate::gf::{mul_0b, mul_0d, mul_0e, mul_02, mul_03, mul_09}; +use crate::tables::{INVSBOX, SBOX}; + +/// The AES block length in bytes. Every AES variant has a 128-bit block (FIPS 197 Table 3). +pub const AES_BLOCK_LEN: usize = 16; + +/// The number of columns of the state, `Nb` in FIPS 197. This Standard fixes `Nb = 4` +/// (Section 2.3); Rijndael in general allows other values, which is why the spec keeps a name for +/// it at all. +pub(crate) const NB: usize = 4; + +/// SUBBYTES(): applies the S-box to each byte of the state independently (FIPS 197 Section 5.1.1). +#[inline(always)] +pub(crate) fn sub_bytes(state: &mut [u8; AES_BLOCK_LEN]) { + for byte in state.iter_mut() { + // s'[r, c] = SBOX(s[r, c]). The transformation is per-byte and position-independent, so + // iterating the flat array in any order is equivalent to the row/column form in Figure 2. + *byte = SBOX[*byte as usize]; + } +} + +/// INVSUBBYTES(): the inverse of [`sub_bytes`], applying INVSBOX() to each byte +/// (FIPS 197 Section 5.3.2). +#[inline(always)] +pub(crate) fn inv_sub_bytes(state: &mut [u8; AES_BLOCK_LEN]) { + for byte in state.iter_mut() { + *byte = INVSBOX[*byte as usize]; + } +} + +/// SHIFTROWS(): cyclically shifts row `r` of the state left by `r` bytes +/// (FIPS 197 Section 5.1.2). +/// +/// Eq (5.5) is `s'[r, c] = s[r, (c + r) mod 4]`. Substituting the flat layout `s[r, c] == +/// state[r + 4c]` gives `new[r + 4c] = old[r + 4 * ((c + r) mod 4)]`, which for each row is a +/// left-rotation of that row's four (stride-4) bytes by `r` positions -- the leftward movement +/// drawn in Figure 3. +/// +/// Written out per row rather than as a loop over a copy of the state, so that no second copy of +/// the state is created (see the crate docs on scrubbing intermediate state). +#[inline(always)] +pub(crate) fn shift_rows(state: &mut [u8; AES_BLOCK_LEN]) { + // Row 0 (r = 0) is unchanged, per Eq (5.5) with r = 0. + + // Row 1: rotate [s(1,0), s(1,1), s(1,2), s(1,3)] left by 1. + let row1_c0 = state[1]; + state[1] = state[5]; // s'(1,0) = s(1,1) + state[5] = state[9]; // s'(1,1) = s(1,2) + state[9] = state[13]; // s'(1,2) = s(1,3) + state[13] = row1_c0; // s'(1,3) = s(1,0) + + // Row 2: rotate left by 2, ie swap the two halves of the row. + let row2_c0 = state[2]; + let row2_c1 = state[6]; + state[2] = state[10]; // s'(2,0) = s(2,2) + state[6] = state[14]; // s'(2,1) = s(2,3) + state[10] = row2_c0; // s'(2,2) = s(2,0) + state[14] = row2_c1; // s'(2,3) = s(2,1) + + // Row 3: rotate left by 3, which is the same as rotating right by 1. + let row3_c3 = state[15]; + state[15] = state[11]; // s'(3,3) = s(3,2) + state[11] = state[7]; // s'(3,2) = s(3,1) + state[7] = state[3]; // s'(3,1) = s(3,0) + state[3] = row3_c3; // s'(3,0) = s(3,3) +} + +/// INVSHIFTROWS(): the inverse of [`shift_rows`], cyclically shifting row `r` right by `r` bytes +/// (FIPS 197 Section 5.3.1). +/// +/// Eq (5.12) is `s'[r, c] = s[r, (c - r) mod 4]`; in the flat layout that is a right-rotation of +/// each row by `r`, ie the rightward movement drawn in Figure 9. +#[inline(always)] +pub(crate) fn inv_shift_rows(state: &mut [u8; AES_BLOCK_LEN]) { + // Row 0 (r = 0) is unchanged. + + // Row 1: rotate right by 1. + let row1_c3 = state[13]; + state[13] = state[9]; // s'(1,3) = s(1,2) + state[9] = state[5]; // s'(1,2) = s(1,1) + state[5] = state[1]; // s'(1,1) = s(1,0) + state[1] = row1_c3; // s'(1,0) = s(1,3) + + // Row 2: rotate right by 2 -- identical to rotating left by 2, so this is its own inverse. + let row2_c0 = state[2]; + let row2_c1 = state[6]; + state[2] = state[10]; // s'(2,0) = s(2,2) + state[6] = state[14]; // s'(2,1) = s(2,3) + state[10] = row2_c0; // s'(2,2) = s(2,0) + state[14] = row2_c1; // s'(2,3) = s(2,1) + + // Row 3: rotate right by 3, which is the same as rotating left by 1. + let row3_c0 = state[3]; + state[3] = state[7]; // s'(3,0) = s(3,1) + state[7] = state[11]; // s'(3,1) = s(3,2) + state[11] = state[15]; // s'(3,2) = s(3,3) + state[15] = row3_c0; // s'(3,3) = s(3,0) +} + +/// MIXCOLUMNS(): multiplies each column of the state by the fixed matrix of Eq (5.7) +/// (FIPS 197 Section 5.1.3). +/// +/// The four output bytes of each column are Eq (5.8) transcribed literally, with the GF(2^8) +/// products supplied by [`crate::gf`]: +/// +/// ```text +/// s'(0,c) = ({02} . s(0,c)) + ({03} . s(1,c)) + s(2,c) + s(3,c) +/// s'(1,c) = s(0,c) + ({02} . s(1,c)) + ({03} . s(2,c)) + s(3,c) +/// s'(2,c) = s(0,c) + s(1,c) + ({02} . s(2,c)) + ({03} . s(3,c)) +/// s'(3,c) = ({03} . s(0,c)) + s(1,c) + s(2,c) + ({02} . s(3,c)) +/// ``` +/// +/// where `.` is GF(2^8) multiplication and `+` is XOR (Section 4.1). +#[inline(always)] +pub(crate) fn mix_columns(state: &mut [u8; AES_BLOCK_LEN]) { + for c in 0..NB { + // A column is contiguous in this layout: state[4c + r] == s(r, c). + let s0 = state[4 * c]; + let s1 = state[4 * c + 1]; + let s2 = state[4 * c + 2]; + let s3 = state[4 * c + 3]; + + state[4 * c] = mul_02(s0) ^ mul_03(s1) ^ s2 ^ s3; + state[4 * c + 1] = s0 ^ mul_02(s1) ^ mul_03(s2) ^ s3; + state[4 * c + 2] = s0 ^ s1 ^ mul_02(s2) ^ mul_03(s3); + state[4 * c + 3] = mul_03(s0) ^ s1 ^ s2 ^ mul_02(s3); + } +} + +/// INVMIXCOLUMNS(): the inverse of [`mix_columns`], multiplying each column by the inverse matrix +/// of Eq (5.14) (FIPS 197 Section 5.3.3). +/// +/// This is Eq (5.15) transcribed literally: +/// +/// ```text +/// s'(0,c) = ({0e} . s(0,c)) + ({0b} . s(1,c)) + ({0d} . s(2,c)) + ({09} . s(3,c)) +/// s'(1,c) = ({09} . s(0,c)) + ({0e} . s(1,c)) + ({0b} . s(2,c)) + ({0d} . s(3,c)) +/// s'(2,c) = ({0d} . s(0,c)) + ({09} . s(1,c)) + ({0e} . s(2,c)) + ({0b} . s(3,c)) +/// s'(3,c) = ({0b} . s(0,c)) + ({0d} . s(1,c)) + ({09} . s(2,c)) + ({0e} . s(3,c)) +/// ``` +#[inline(always)] +pub(crate) fn inv_mix_columns(state: &mut [u8; AES_BLOCK_LEN]) { + for c in 0..NB { + let s0 = state[4 * c]; + let s1 = state[4 * c + 1]; + let s2 = state[4 * c + 2]; + let s3 = state[4 * c + 3]; + + state[4 * c] = mul_0e(s0) ^ mul_0b(s1) ^ mul_0d(s2) ^ mul_09(s3); + state[4 * c + 1] = mul_09(s0) ^ mul_0e(s1) ^ mul_0b(s2) ^ mul_0d(s3); + state[4 * c + 2] = mul_0d(s0) ^ mul_09(s1) ^ mul_0e(s2) ^ mul_0b(s3); + state[4 * c + 3] = mul_0b(s0) ^ mul_0d(s1) ^ mul_09(s2) ^ mul_0e(s3); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Builds a state from the four 32-bit words that NIST's intermediate-value files print for it. + /// + /// Those files print the state as `state[0..4] state[4..8] state[8..12] state[12..16]`, each + /// group as a big-endian hex word -- ie the four *columns* of the state in order, which is the + /// same as the flat byte order of this implementation (see the module docs). + const fn state_of(w0: u32, w1: u32, w2: u32, w3: u32) -> [u8; AES_BLOCK_LEN] { + let (a, b, c, d) = (w0.to_be_bytes(), w1.to_be_bytes(), w2.to_be_bytes(), w3.to_be_bytes()); + [ + a[0], a[1], a[2], a[3], b[0], b[1], b[2], b[3], c[0], c[1], c[2], c[3], d[0], d[1], + d[2], d[3], + ] + } + + /* The intermediate values below are from the NIST "AES Core" ECB-AES128 intermediate value + * file, first block: key = 2B7E1516 28AED2A6 ABF71588 09CF4F3C, + * plaintext = 6BC1BEE2 2E409F96 E93D7E11 7393172A. + * Round 1 and round 2 are enough to pin every transformation; the remaining rounds, and the + * AES-192/AES-256 variants, are covered end to end by the known-answer tests in + * tests/aes_tests.rs. */ + + /// Round 1 input, ie the state after the initial ADDROUNDKEY() ("KeyAddition" in the file). + const R1_START: [u8; AES_BLOCK_LEN] = state_of(0x40BFABF4, 0x06EE4D30, 0x42CA6B99, 0x7A5C5816); + /// Round 1 after SUBBYTES() ("Substitution"). + const R1_SUB: [u8; AES_BLOCK_LEN] = state_of(0x090862BF, 0x6F28E304, 0x2C747FEE, 0xDA4A6A47); + /// Round 1 after SHIFTROWS() ("ShiftRow"). + const R1_SHIFT: [u8; AES_BLOCK_LEN] = state_of(0x09287F47, 0x6F746ABF, 0x2C4A6204, 0xDA08E3EE); + /// Round 1 after MIXCOLUMNS() ("MixColumn"). + const R1_MIX: [u8; AES_BLOCK_LEN] = state_of(0x529F16C2, 0x978615CA, 0xE01AAE54, 0xBA1A2659); + + /// Round 2 input, ie the state after round 1's ADDROUNDKEY(). + const R2_START: [u8; AES_BLOCK_LEN] = state_of(0xF265E8D5, 0x1FD2397B, 0xC3B9976D, 0x9076505C); + /// Round 2 after SUBBYTES(). + const R2_SUB: [u8; AES_BLOCK_LEN] = state_of(0x894D9B03, 0xC0B51221, 0x2E56883C, 0x6038534A); + /// Round 2 after SHIFTROWS(). + const R2_SHIFT: [u8; AES_BLOCK_LEN] = state_of(0x89B5884A, 0xC0565303, 0x2E389B21, 0x604D123C); + /// Round 2 after MIXCOLUMNS(). + const R2_MIX: [u8; AES_BLOCK_LEN] = state_of(0x0F31E929, 0x319A3558, 0xAEC95893, 0x39F04D87); + + #[test] + fn sub_bytes_matches_nist_intermediate_values() { + let mut state = R1_START; + sub_bytes(&mut state); + assert_eq!(state, R1_SUB); + + let mut state = R2_START; + sub_bytes(&mut state); + assert_eq!(state, R2_SUB); + } + + #[test] + fn shift_rows_matches_nist_intermediate_values() { + let mut state = R1_SUB; + shift_rows(&mut state); + assert_eq!(state, R1_SHIFT); + + let mut state = R2_SUB; + shift_rows(&mut state); + assert_eq!(state, R2_SHIFT); + } + + #[test] + fn mix_columns_matches_nist_intermediate_values() { + let mut state = R1_SHIFT; + mix_columns(&mut state); + assert_eq!(state, R1_MIX); + + let mut state = R2_SHIFT; + mix_columns(&mut state); + assert_eq!(state, R2_MIX); + } + + /* The inverse transformations are pinned against the same NIST values, read in the other + * direction. (The decryption traces in those files apply INVSUBBYTES() and INVSHIFTROWS() in + * the opposite order to Algorithm 3 -- the two commute, since one is per-byte and the other is + * a permutation of positions -- so running the encryption values backwards is the unambiguous + * way to pin these.) */ + + #[test] + fn inv_sub_bytes_matches_nist_intermediate_values() { + let mut state = R1_SUB; + inv_sub_bytes(&mut state); + assert_eq!(state, R1_START); + + let mut state = R2_SUB; + inv_sub_bytes(&mut state); + assert_eq!(state, R2_START); + } + + #[test] + fn inv_shift_rows_matches_nist_intermediate_values() { + let mut state = R1_SHIFT; + inv_shift_rows(&mut state); + assert_eq!(state, R1_SUB); + + let mut state = R2_SHIFT; + inv_shift_rows(&mut state); + assert_eq!(state, R2_SUB); + } + + #[test] + fn inv_mix_columns_matches_nist_intermediate_values() { + let mut state = R1_MIX; + inv_mix_columns(&mut state); + assert_eq!(state, R1_SHIFT); + + let mut state = R2_MIX; + inv_mix_columns(&mut state); + assert_eq!(state, R2_SHIFT); + } + + /// Each transformation composed with its inverse must be the identity, for a spread of states + /// including the two degenerate ones (all-zero and all-ones) that a vector-only test set can + /// easily miss. + #[test] + fn every_transformation_round_trips() { + let mut states = [[0u8; AES_BLOCK_LEN]; 4]; + states[1] = [0xFF; AES_BLOCK_LEN]; + // A state with every byte distinct catches transposition and off-by-one row errors. + for (i, byte) in states[2].iter_mut().enumerate() { + *byte = i as u8; + } + states[3] = R1_START; + + for original in states.iter() { + let mut state = *original; + + sub_bytes(&mut state); + inv_sub_bytes(&mut state); + assert_eq!(&state, original, "sub_bytes round trip"); + + shift_rows(&mut state); + inv_shift_rows(&mut state); + assert_eq!(&state, original, "shift_rows round trip"); + + mix_columns(&mut state); + inv_mix_columns(&mut state); + assert_eq!(&state, original, "mix_columns round trip"); + } + } + + /// SHIFTROWS() must leave row 0 alone and must be a pure permutation of the other rows: it can + /// neither change any byte's value nor move a byte out of its row. + #[test] + fn shift_rows_permutes_within_rows_only() { + let mut state = [0u8; AES_BLOCK_LEN]; + for (i, byte) in state.iter_mut().enumerate() { + // Encode the row in the low nibble and the column in the high nibble. + *byte = ((i / NB) as u8) << 4 | (i % NB) as u8; + } + let original = state; + shift_rows(&mut state); + + for r in 0..4 { + for c in 0..NB { + let moved = state[r + 4 * c]; + // The row index (low nibble here, since i % NB == r for index r + 4c) is preserved. + assert_eq!(moved & 0x0f, (r as u8) & 0x0f, "byte left its row at ({r},{c})"); + } + } + // Row 0 is untouched. + for c in 0..NB { + assert_eq!(state[4 * c], original[4 * c], "row 0 changed at column {c}"); + } + // And every other row genuinely moved (r = 1, 2, 3 all have non-zero shifts). + for r in 1..4 { + assert_ne!( + [state[r], state[r + 4], state[r + 8], state[r + 12]], + [original[r], original[r + 4], original[r + 8], original[r + 12]], + "row {r} did not move" + ); + } + } + + /// MIXCOLUMNS() treats the four columns independently (Section 5.1.3: it "mixes their data + /// independently of one another"), so changing one column must not disturb the others. + #[test] + fn mix_columns_keeps_columns_independent() { + let mut baseline = R1_SHIFT; + mix_columns(&mut baseline); + + for c in 0..NB { + let mut perturbed = R1_SHIFT; + perturbed[4 * c] ^= 0xFF; + mix_columns(&mut perturbed); + + for other in 0..NB { + if other == c { + continue; + } + assert_eq!( + perturbed[4 * other..4 * other + 4], + baseline[4 * other..4 * other + 4], + "column {c} leaked into column {other}" + ); + } + } + } +} diff --git a/crypto/aes/src/tables.rs b/crypto/aes/src/tables.rs new file mode 100644 index 0000000..2c72a10 --- /dev/null +++ b/crypto/aes/src/tables.rs @@ -0,0 +1,204 @@ +//! The AES substitution tables and key expansion round constants. +//! +//! * [`SBOX`] is FIPS 197 Table 4, used by SUBBYTES() (Section 5.1.1) and SUBWORD() (Eq 5.11). +//! * [`INVSBOX`] is FIPS 197 Table 6, used by INVSUBBYTES() (Section 5.3.2). +//! * [`RCON`] is FIPS 197 Table 5, used by KEYEXPANSION() (Section 5.2). +//! +//! # Why tables and not computed values? +//! +//! The S-box is mathematically defined (FIPS 197 Eq 5.2 and 5.3) as the multiplicative inverse in +//! GF(2^8) followed by an affine transformation. Computing that per byte would cost roughly 250 +//! field multiplications, which is far too slow for a block cipher, so -- like every practical +//! implementation, and like the tabulated form the standard itself provides -- we store the 256 +//! precomputed values. +//! +//! The unit tests at the bottom of this file re-derive the whole table from Eq (5.2) and (5.3) and +//! compare it against the stored bytes, so a transcription error in the table cannot survive +//! `cargo test`. +//! +//! # ๐Ÿšจ Security ๐Ÿšจ +//! +//! Indexing a 256-byte table with a secret byte is not a constant-time operation on a CPU with a +//! data cache: which cache lines get touched depends on the value of the index. This is the +//! cache-timing exposure that FIPS 197 Section 6.4 calls out, and it is inherent to any +//! table-driven AES. See the crate-level "Security Considerations" docs for what this means in +//! practice and what the alternatives are. +//! +//! Note that we deliberately use only the 256-byte S-box, and *not* the common 4 KiB "T-tables" +//! that fold MIXCOLUMNS() into the substitution step. T-tables are faster but spread each lookup +//! over far more cache lines, which measurably widens exactly this side channel. + +/// FIPS 197 Table 4: `SBOX()` substitution values for the byte `xy` in hexadecimal. +/// +/// Indexed as `SBOX[0xXY]`, ie the table is stored row-major with `x` as the high nibble of the +/// index and `y` as the low nibble. The 16 rows below are therefore `x = 0x0 ..= 0xf` and the 16 +/// columns are `y = 0x0 ..= 0xf`, so this reads exactly like Table 4 -- which is the point, and why +/// the grid is held to `rustfmt`'s `skip`: a reviewer has to be able to compare it against the +/// printed standard row by row. +#[rustfmt::skip] +pub(crate) const SBOX: [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()` substitution values for the byte `xy` in hexadecimal. +/// +/// This is [`SBOX`] with the roles of input and output exchanged, laid out and indexed the same +/// way: 16 rows of `x = 0x0 ..= 0xf`, 16 columns of `y = 0x0 ..= 0xf`. +#[rustfmt::skip] +pub(crate) const INVSBOX: [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, +]; + +/// FIPS 197 Table 5: the round constants `Rcon[j]` for 1 <= j <= 10. +/// +/// The spec indexes these from 1, so `Rcon[j]` is `RCON[j - 1]` here; [`crate::key_schedule`] is +/// the only caller and does that adjustment at the single point of use. +/// +/// Each round constant is the word `[x^(j-1), {00}, {00}, {00}]`, held here as a big-endian `u32` +/// so it can be XOR-ed straight into a key schedule word. The leftmost byte therefore runs +/// {01}, {02}, {04} ... doubling in GF(2^8) each step, which is why the ninth and tenth entries +/// are {1b} and {36} rather than {100} and {200}. +pub(crate) const RCON: [u32; 10] = [ + 0x0100_0000, // j=1 -> x^0 = {01} + 0x0200_0000, // j=2 -> x^1 = {02} + 0x0400_0000, // j=3 -> x^2 = {04} + 0x0800_0000, // j=4 -> x^3 = {08} + 0x1000_0000, // j=5 -> x^4 = {10} + 0x2000_0000, // j=6 -> x^5 = {20} + 0x4000_0000, // j=7 -> x^6 = {40} + 0x8000_0000, // j=8 -> x^7 = {80} + 0x1b00_0000, // j=9 -> x^8 = {1b} (x^8 reduced mod m(x)) + 0x3600_0000, // j=10 -> x^9 = {36} +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::gf::{gf_mul, gf_pow, xtimes}; + + /// Re-derives one S-box entry from its mathematical definition, FIPS 197 Section 5.1.1. + fn sbox_from_definition(b: u8) -> u8 { + // Step 1, Eq (5.2): b~ = {00} if b == {00}, otherwise the multiplicative inverse of b. + // Eq (4.11) gives that inverse as b^254. + let b_tilde = if b == 0x00 { 0x00 } else { gf_pow(b, 254) }; + + // Step 2, Eq (5.3): b'_i = b~_i XOR b~_(i+4 mod 8) XOR b~_(i+5 mod 8) + // XOR b~_(i+6 mod 8) XOR b~_(i+7 mod 8) XOR c_i + // where c is the constant byte {01100011} = {63}. + const C: u8 = 0x63; + let bit = |value: u8, i: u32| (value >> (i % 8)) & 1; + + let mut result = 0u8; + for i in 0..8u32 { + let b_prime_i = bit(b_tilde, i) + ^ bit(b_tilde, i + 4) + ^ bit(b_tilde, i + 5) + ^ bit(b_tilde, i + 6) + ^ bit(b_tilde, i + 7) + ^ bit(C, i); + result |= b_prime_i << i; + } + result + } + + /// Every entry of the stored [`SBOX`] must equal the value that FIPS 197 Eq (5.2) and (5.3) + /// define, which makes a typo in the transcribed Table 4 impossible to miss. + #[test] + fn sbox_matches_its_mathematical_definition() { + for b in 0..=u8::MAX { + assert_eq!(SBOX[b as usize], sbox_from_definition(b), "SBOX[{b:#04x}]"); + } + } + + /// Two spot checks straight out of the prose of FIPS 197. + #[test] + fn sbox_matches_documented_examples() { + // Section 5.1.1: "if s_rc = {53} ... so that s'_rc = {ed}". + assert_eq!(SBOX[0x53], 0xed); + // Section 5.1.1: SBOX({00}) is the affine transform of {00}, ie the constant {63}. + assert_eq!(SBOX[0x00], 0x63); + } + + /// [`INVSBOX`] must be the exact inverse permutation of [`SBOX`] (Section 5.3.2), in both + /// directions. Checking both directions also proves each table is a bijection. + #[test] + fn invsbox_inverts_sbox() { + for b in 0..=u8::MAX { + assert_eq!(INVSBOX[SBOX[b as usize] as usize], b, "INVSBOX(SBOX({b:#04x}))"); + assert_eq!(SBOX[INVSBOX[b as usize] as usize], b, "SBOX(INVSBOX({b:#04x}))"); + } + } + + /// The S-box has no fixed points (SBOX(b) != b) and no "opposite" fixed points + /// (SBOX(b) != !b). These are design properties of Rijndael's affine constant, so they are a + /// cheap independent sanity check on the table. + #[test] + fn sbox_has_no_fixed_points() { + for b in 0..=u8::MAX { + assert_ne!(SBOX[b as usize], b, "SBOX has a fixed point at {b:#04x}"); + assert_ne!(SBOX[b as usize], !b, "SBOX has an opposite fixed point at {b:#04x}"); + } + } + + /// FIPS 197 Section 5.2: "the value of the left-most byte of Rcon[j] in polynomial form is + /// x^(j-1) ... these bytes may be generated by successively applying xTimes()". + #[test] + fn rcon_leftmost_bytes_are_successive_powers_of_x() { + let mut power_of_x = 0x01u8; // x^0 + for (index, rcon) in RCON.iter().enumerate() { + let expected = u32::from_be_bytes([power_of_x, 0x00, 0x00, 0x00]); + assert_eq!(*rcon, expected, "RCON[{index}] (ie Rcon[{}])", index + 1); + power_of_x = xtimes(power_of_x); + } + } + + /// Cross-check of the same property using the general field multiplication: Rcon[j] is + /// {02}^(j-1). + #[test] + fn rcon_leftmost_bytes_are_powers_of_two_in_the_field() { + for (index, rcon) in RCON.iter().enumerate() { + let expected_byte = gf_pow(0x02, index as u32); + assert_eq!((*rcon >> 24) as u8, expected_byte, "RCON[{index}]"); + // The other three bytes of every round constant are zero. + assert_eq!(*rcon & 0x00ff_ffff, 0, "RCON[{index}] low bytes"); + } + } + + /// {1b} and {36} are the reduced forms of x^8 and x^9, per Eq (4.3)'s modulus. Pinning this + /// separately documents why the doubling sequence "wraps" where it does. + #[test] + fn rcon_wrap_values_are_reduced_powers() { + assert_eq!(gf_mul(0x80, 0x02), 0x1b); // x^7 * x = x^8 = {1b} mod m(x) + assert_eq!(gf_mul(0x1b, 0x02), 0x36); // x^8 * x = x^9 = {36} mod m(x) + } +} diff --git a/crypto/aes/tests/aes_tests.rs b/crypto/aes/tests/aes_tests.rs new file mode 100644 index 0000000..785c4ff --- /dev/null +++ b/crypto/aes/tests/aes_tests.rs @@ -0,0 +1,476 @@ +//! Known-answer tests for the AES block cipher engine. +//! +//! The vectors come from two places: +//! +//! * **FIPS 197 Appendix B** ("Cipher Example"), the single-block AES-128 worked example. +//! * The NIST **"AES Core" intermediate value files** for ECB-AES128, ECB-AES192 and ECB-AES256 +//! (the same four-block plaintext and keys that appear in NIST SP 800-38A Appendix F.1). Only the +//! block-level inputs and outputs are checked here; the per-round intermediate values from those +//! same files are checked against the individual transformations by the unit tests in +//! `src/state.rs`, `src/key_schedule.rs` and `src/aes.rs`. +//! +//! Note that "ECB" in the names of those files simply means each block is enciphered independently +//! with no chaining, which is exactly the raw permutation this crate exposes. This crate does not +//! implement ECB (or any other) mode of operation -- see the crate docs. + +use bouncycastle_aes::{ + AES, AES_BLOCK_LEN, AES128, AES128_KEY_LEN, AES128_KEY_SCHEDULE_WORDS, AES128_NUM_ROUNDS, + AES128Key, AES192, AES192_KEY_LEN, AES192_KEY_SCHEDULE_WORDS, AES192_NUM_ROUNDS, AES256, + AES256_KEY_LEN, AES256_KEY_SCHEDULE_WORDS, AES256_NUM_ROUNDS, AES256Key, AESEngine, +}; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{Algorithm, SecurityStrength}; +use bouncycastle_hex as hex; + +/* *** Test vectors *** */ + +/// The AES-128 key of FIPS 197 Appendix A.1 and Appendix B, and of the ECB-AES128 vector file. +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +/// The AES-192 key of FIPS 197 Appendix A.2 and of the ECB-AES192 vector file. +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +/// The AES-256 key of FIPS 197 Appendix A.3 and of the ECB-AES256 vector file. +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; + +/// The four plaintext blocks used by all three ECB-AES vector files. +const PLAINTEXTS: [&str; 4] = [ + "6bc1bee22e409f96e93d7e117393172a", + "ae2d8a571e03ac9c9eb76fac45af8e51", + "30c81c46a35ce411e5fbc1191a0a52ef", + "f69f2445df4f9b17ad2b417be66c3710", +]; + +/// The ciphertexts of [`PLAINTEXTS`] under [`KEY_128`]. +const CIPHERTEXTS_128: [&str; 4] = [ + "3ad77bb40d7a3660a89ecaf32466ef97", + "f5d3d58503b9699de785895a96fdbaaf", + "43b1cd7f598ece23881b00e3ed030688", + "7b0c785e27e8ad3f8223207104725dd4", +]; + +/// The ciphertexts of [`PLAINTEXTS`] under [`KEY_192`]. +const CIPHERTEXTS_192: [&str; 4] = [ + "bd334f1d6e45f25ff712a214571fa5cc", + "974104846d0ad3ad7734ecb3ecee4eef", + "ef7afd2270e2e60adce0ba2face6444e", + "9a4b41ba738d6c72fb16691603c18e0e", +]; + +/// The ciphertexts of [`PLAINTEXTS`] under [`KEY_256`]. +const CIPHERTEXTS_256: [&str; 4] = [ + "f3eed1bdb5d2a03c064b5a7e3db181f8", + "591ccb10d410ed26dc5ba74a31362870", + "b6ed21b99ca6f4f9f153e7b1beafed1d", + "23304b7a39f9f3ff067d8d8f9e24ecc7", +]; + +/* *** Helpers *** */ + +/// Wraps a hex-encoded key as a [`KeyMaterial`] of the exact size the given AES variant takes. +fn key_from_hex(key_hex: &str) -> KeyMaterial { + let bytes = hex::decode(key_hex).unwrap(); + assert_eq!(bytes.len(), KEY_LEN, "test key is the wrong length for this variant"); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() +} + +/// Decodes a hex-encoded block into a fixed-size array. +fn block_from_hex(block_hex: &str) -> [u8; AES_BLOCK_LEN] { + hex::decode(block_hex).unwrap().try_into().expect("test block is not 16 bytes") +} + +/// Runs a full known-answer test for one variant, in both directions, through every API the crate +/// offers: the streaming engine, the `_out` forms, and the one-shot statics. +/// +/// Generic over the variant's parameters so that all three variants are exercised by identical +/// code; the `Algorithm` bound is what restricts this to the three parameter sets of FIPS 197 +/// Table 3 (and incidentally proves that downstream crates can write such generic code). +fn check_known_answers( + key_hex: &str, + expected_ciphertexts: [&str; 4], +) where + AES: Algorithm, +{ + let key = key_from_hex::(key_hex); + let engine = AES::::new(&key).unwrap(); + + for (plaintext_hex, ciphertext_hex) in PLAINTEXTS.iter().zip(expected_ciphertexts.iter()) { + let plaintext = block_from_hex(plaintext_hex); + let ciphertext = block_from_hex(ciphertext_hex); + + // CIPHER() + assert_eq!( + engine.encrypt_block(&plaintext), + ciphertext, + "{} encrypt of {plaintext_hex}", + AES::::ALG_NAME + ); + + // INVCIPHER() + assert_eq!( + engine.decrypt_block(&ciphertext), + plaintext, + "{} decrypt of {ciphertext_hex}", + AES::::ALG_NAME + ); + + // The _out forms must agree with the returning forms, byte for byte. + let mut out = [0u8; AES_BLOCK_LEN]; + assert_eq!(engine.encrypt_block_out(&plaintext, &mut out), AES_BLOCK_LEN); + assert_eq!(out, ciphertext); + assert_eq!(engine.decrypt_block_out(&ciphertext, &mut out), AES_BLOCK_LEN); + assert_eq!(out, plaintext); + + // ... and so must the one-shot statics, which re-expand the key each call. + assert_eq!( + AES::::encrypt_single_block(&key, &plaintext).unwrap(), + ciphertext + ); + assert_eq!( + AES::::decrypt_single_block(&key, &ciphertext).unwrap(), + plaintext + ); + } +} + +/* *** Known-answer tests *** */ + +/// FIPS 197 Appendix B, the step-by-step example: a 16-byte key and a 16-byte input, with the +/// expected output read out of the final state matrix. +#[test] +fn fips197_appendix_b_cipher_example() { + let key = key_from_hex::(KEY_128); + let engine = AES128::new(&key).unwrap(); + + let plaintext = block_from_hex("3243f6a8885a308d313198a2e0370734"); + let ciphertext = engine.encrypt_block(&plaintext); + + assert_eq!(hex::encode(ciphertext), "3925841d02dc09fbdc118597196a0b32"); + assert_eq!(engine.decrypt_block(&ciphertext), plaintext); +} + +#[test] +fn aes128_known_answers() { + check_known_answers::( + KEY_128, CIPHERTEXTS_128, + ); +} + +#[test] +fn aes192_known_answers() { + check_known_answers::( + KEY_192, CIPHERTEXTS_192, + ); +} + +#[test] +fn aes256_known_answers() { + check_known_answers::( + KEY_256, CIPHERTEXTS_256, + ); +} + +/// The same plaintext under the three different key sizes must give three different ciphertexts. +/// This catches an engine that ignored part of the key, or that used the wrong round count. +#[test] +fn the_three_variants_are_distinct() { + assert_ne!(CIPHERTEXTS_128[0], CIPHERTEXTS_192[0]); + assert_ne!(CIPHERTEXTS_128[0], CIPHERTEXTS_256[0]); + assert_ne!(CIPHERTEXTS_192[0], CIPHERTEXTS_256[0]); +} + +/* *** Behavioural properties *** */ + +/// Encryption must be a permutation: every block must round-trip, including the degenerate +/// all-zero and all-ones blocks, and no two distinct blocks may encrypt to the same ciphertext. +#[test] +fn every_variant_round_trips_and_is_injective() { + let key128 = key_from_hex::(KEY_128); + let key192 = key_from_hex::(KEY_192); + let key256 = key_from_hex::(KEY_256); + + let aes128 = AES128::new(&key128).unwrap(); + let aes192 = AES192::new(&key192).unwrap(); + let aes256 = AES256::new(&key256).unwrap(); + + // 258 blocks: all-zero, all-ones, and one block filled with each byte value. + let mut blocks = Vec::new(); + blocks.push([0x00u8; AES_BLOCK_LEN]); + blocks.push([0xFFu8; AES_BLOCK_LEN]); + for filler in 0..=u8::MAX { + let mut block = [filler; AES_BLOCK_LEN]; + // Vary within the block too, so a bug that only shows up on non-uniform blocks is caught. + block[0] = filler.wrapping_mul(31); + block[AES_BLOCK_LEN - 1] = !filler; + blocks.push(block); + } + + let mut ciphertexts_128 = Vec::with_capacity(blocks.len()); + for block in blocks.iter() { + let ct128 = aes128.encrypt_block(block); + let ct192 = aes192.encrypt_block(block); + let ct256 = aes256.encrypt_block(block); + + assert_eq!(&aes128.decrypt_block(&ct128), block, "AES-128 round trip"); + assert_eq!(&aes192.decrypt_block(&ct192), block, "AES-192 round trip"); + assert_eq!(&aes256.decrypt_block(&ct256), block, "AES-256 round trip"); + + // A block must never encrypt to itself, and the three variants must disagree. + assert_ne!(&ct128, block); + assert_ne!(ct128, ct192); + assert_ne!(ct128, ct256); + + ciphertexts_128.push(ct128); + } + + // Injectivity: distinct inputs give distinct outputs. + let mut sorted = ciphertexts_128.clone(); + sorted.sort_unstable(); + sorted.dedup(); + assert_eq!(sorted.len(), ciphertexts_128.len(), "two distinct blocks collided"); +} + +/// The engine is deterministic and stateless across calls: the same block encrypts to the same +/// ciphertext every time, and interleaving encryptions and decryptions changes nothing. +/// +/// This is a property of the raw permutation, not a bug -- but it is also precisely why using the +/// engine directly on multi-block data (ie ECB) leaks plaintext structure, so it is worth pinning +/// as documented behaviour. +#[test] +fn the_engine_is_stateless_and_deterministic() { + let key = key_from_hex::(KEY_128); + let engine = AES128::new(&key).unwrap(); + + let a = block_from_hex(PLAINTEXTS[0]); + let b = block_from_hex(PLAINTEXTS[1]); + + let first = engine.encrypt_block(&a); + let _ = engine.encrypt_block(&b); + let _ = engine.decrypt_block(&first); + let second = engine.encrypt_block(&a); + + assert_eq!(first, second); + assert_eq!(hex::encode(first), CIPHERTEXTS_128[0]); +} + +/// Flipping a single bit of the plaintext must change roughly half the ciphertext bits, and must +/// never leave the ciphertext unchanged (the avalanche property). A cheap end-to-end check that all +/// the diffusion layers are actually wired in. +#[test] +fn flipping_one_plaintext_bit_avalanches() { + let key = key_from_hex::(KEY_128); + let engine = AES128::new(&key).unwrap(); + + let plaintext = block_from_hex(PLAINTEXTS[0]); + let baseline = engine.encrypt_block(&plaintext); + + for bit in 0..(AES_BLOCK_LEN * 8) { + let mut flipped = plaintext; + flipped[bit / 8] ^= 1 << (bit % 8); + let ciphertext = engine.encrypt_block(&flipped); + + assert_ne!(ciphertext, baseline, "flipping plaintext bit {bit} changed nothing"); + + let differing_bits: u32 = + ciphertext.iter().zip(baseline.iter()).map(|(a, b)| (a ^ b).count_ones()).sum(); + // Expect ~64 of 128 bits to change. A generous window: anything outside it means the + // diffusion is broken, not that we got unlucky. + assert!( + (32..=96).contains(&differing_bits), + "flipping plaintext bit {bit} changed {differing_bits} of 128 ciphertext bits" + ); + } +} + +/// The same avalanche property for a single-bit change in the *key*, which exercises the key +/// expansion rather than the round function. +#[test] +fn flipping_one_key_bit_avalanches() { + let key_bytes = hex::decode(KEY_128).unwrap(); + let plaintext = block_from_hex(PLAINTEXTS[0]); + + let baseline = { + let key = AES128Key::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey).unwrap(); + AES128::new(&key).unwrap().encrypt_block(&plaintext) + }; + + for bit in 0..(AES128_KEY_LEN * 8) { + let mut flipped_bytes = key_bytes.clone(); + flipped_bytes[bit / 8] ^= 1 << (bit % 8); + let key = + AES128Key::from_bytes_as_type(&flipped_bytes, KeyType::SymmetricCipherKey).unwrap(); + let ciphertext = AES128::new(&key).unwrap().encrypt_block(&plaintext); + + assert_ne!(ciphertext, baseline, "flipping key bit {bit} changed nothing"); + + let differing_bits: u32 = + ciphertext.iter().zip(baseline.iter()).map(|(a, b)| (a ^ b).count_ones()).sum(); + assert!( + (32..=96).contains(&differing_bits), + "flipping key bit {bit} changed {differing_bits} of 128 ciphertext bits" + ); + } +} + +/* *** Key validation *** */ + +/// A key tagged for a different algorithm must be refused: key separation between primitives is a +/// security property. +#[test] +fn rejects_a_key_of_the_wrong_type() { + let key_bytes = hex::decode(KEY_128).unwrap(); + + for wrong_type in [KeyType::MACKey, KeyType::Seed, KeyType::Unknown] { + let key = AES128Key::from_bytes_as_type(&key_bytes, wrong_type).unwrap(); + match AES128::new(&key) { + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) => { + /* good */ + } + other => panic!("expected InvalidKeyType for {wrong_type:?}, got {other:?}"), + } + } + + // The two accepted types must in fact be accepted. + for right_type in [KeyType::SymmetricCipherKey, KeyType::CryptographicRandom] { + let key = AES128Key::from_bytes_as_type(&key_bytes, right_type).unwrap(); + assert!(AES128::new(&key).is_ok(), "{right_type:?} should be accepted"); + } +} + +/// An all-zero key arrives tagged [`KeyType::Zeroized`] and must be refused, so that a caller who +/// accidentally passes an uninitialized buffer gets an error rather than a working cipher under a +/// guessable key. +#[test] +fn rejects_an_all_zero_key() { + // from_bytes_as_type() reports the all-zero input and tags the key Zeroized. + let mut key = AES128Key::new(); + let flagged = key.set_bytes_as_type(&[0u8; AES128_KEY_LEN], KeyType::SymmetricCipherKey); + assert!(flagged.is_err(), "KeyMaterial should flag an all-zero key"); + + match AES128::new(&key) { + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) => { + /* good */ + } + other => panic!("expected InvalidKeyType for an all-zero key, got {other:?}"), + } +} + +/// A key holding fewer bytes than the variant needs must be refused rather than silently +/// zero-padded. (It cannot hold more: the [`KeyMaterial`] capacity is the key length.) +#[test] +fn rejects_a_short_key() { + let short = AES256Key::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); + assert_eq!(short.key_len(), 16); + + match AES256::new(&short) { + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidLength)) => { + /* good */ + } + other => panic!("expected InvalidLength for a short key, got {other:?}"), + } +} + +/// A key tagged at a lower security strength than the variant provides must be refused: otherwise +/// an application could believe it had 256-bit security while keying AES-256 from 128 bits of +/// entropy. +#[test] +fn rejects_a_key_weaker_than_the_algorithm() { + let mut key = key_from_hex::(KEY_256); + assert_eq!(key.security_strength(), SecurityStrength::_256bit); + + // Lowering the recorded strength does not need a hazardous-operations closure; only raising it + // does. This models a 32-byte key that was derived from a weaker source. + key.set_security_strength(SecurityStrength::_128bit).unwrap(); + + match AES256::new(&key) { + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) => { + /* good */ + } + other => panic!("expected SecurityStrength for a weak key, got {other:?}"), + } + + // The same key material is fine for AES-128, whose strength requirement it does meet. + let mut key128 = key_from_hex::(KEY_128); + assert_eq!(key128.security_strength(), SecurityStrength::_128bit); + assert!(AES128::new(&key128).is_ok()); + + key128.set_security_strength(SecurityStrength::_112bit).unwrap(); + assert!(AES128::new(&key128).is_err(), "AES-128 must require 128-bit key strength"); +} + +/* *** Metadata, memory and hygiene *** */ + +/// The [`Algorithm`] and [`AESEngine`] constants must match FIPS 197 Table 3. +#[test] +fn algorithm_metadata_matches_fips197_table_3() { + assert_eq!(AES128::ALG_NAME, "AES-128"); + assert_eq!(AES192::ALG_NAME, "AES-192"); + assert_eq!(AES256::ALG_NAME, "AES-256"); + + assert_eq!(AES128::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(AES192::MAX_SECURITY_STRENGTH, SecurityStrength::_192bit); + assert_eq!(AES256::MAX_SECURITY_STRENGTH, SecurityStrength::_256bit); + + // Key lengths: Nk = 4, 6, 8 words. + assert_eq!(::KEY_LEN, 16); + assert_eq!(::KEY_LEN, 24); + assert_eq!(::KEY_LEN, 32); + + // Round counts: Nr = 10, 12, 14. + assert_eq!(::NUM_ROUNDS, 10); + assert_eq!(::NUM_ROUNDS, 12); + assert_eq!(::NUM_ROUNDS, 14); + + // Every variant has a 128-bit block. + assert_eq!(AES_BLOCK_LEN, 16); + assert_eq!(::BLOCK_LEN, AES_BLOCK_LEN); + assert_eq!(::BLOCK_LEN, AES_BLOCK_LEN); + assert_eq!(::BLOCK_LEN, AES_BLOCK_LEN); + + // The key schedule holds 4 * (Nr + 1) words. + assert_eq!(AES128_KEY_SCHEDULE_WORDS, 44); + assert_eq!(AES192_KEY_SCHEDULE_WORDS, 52); + assert_eq!(AES256_KEY_SCHEDULE_WORDS, 60); +} + +/// Pins the struct sizes quoted in the crate's "Memory Usage" documentation, so that the table +/// cannot silently go stale. +#[test] +fn struct_sizes_match_the_documented_memory_usage() { + assert_eq!(size_of::(), 176); + assert_eq!(size_of::(), 208); + assert_eq!(size_of::(), 240); + + // ie exactly the key schedule and nothing else. + assert_eq!(size_of::(), AES128_KEY_SCHEDULE_WORDS * size_of::()); + assert_eq!(size_of::(), AES192_KEY_SCHEDULE_WORDS * size_of::()); + assert_eq!(size_of::(), AES256_KEY_SCHEDULE_WORDS * size_of::()); +} + +/// Debug formatting must never expose the key schedule, which is recoverable back to the cipher +/// key. A regression here would leak keys into logs and panic messages. +#[test] +fn debug_output_does_not_leak_key_material() { + let key = key_from_hex::(KEY_128); + let engine = AES128::new(&key).unwrap(); + + let rendered = format!("{engine:?}"); + assert!(rendered.contains("redacted"), "Debug output should say it is redacted: {rendered}"); + + // None of the key bytes, and no word of the first round key, may appear in any form. + for byte_pair in ["2b7e", "28ae", "abf7", "09cf", "a0fa", "fe17"] { + assert!(!rendered.contains(byte_pair), "Debug output leaked {byte_pair}: {rendered}"); + } +} + +/// A cloned engine must behave identically to the original: cloning duplicates the key schedule and +/// nothing else. +#[test] +fn a_cloned_engine_encrypts_identically() { + let key = key_from_hex::(KEY_192); + let engine = AES192::new(&key).unwrap(); + let clone = engine.clone(); + + let plaintext = block_from_hex(PLAINTEXTS[2]); + assert_eq!(engine.encrypt_block(&plaintext), clone.encrypt_block(&plaintext)); + assert_eq!(hex::encode(clone.encrypt_block(&plaintext)), CIPHERTEXTS_192[2]); +} diff --git a/crypto/aes/tests/wycheproof.rs b/crypto/aes/tests/wycheproof.rs new file mode 100644 index 0000000..758b880 --- /dev/null +++ b/crypto/aes/tests/wycheproof.rs @@ -0,0 +1,28 @@ +//! Test against the project wycheproof repo available at: +//! https://github.com/C2SP/wycheproof +//! Requires that the wycheproof repository is cloned and available for testing at "../wycheproof" +//! relative to the root of this git project. +//! +//! TODO: port the wycheproof vectors for AES. Nothing is wired up here yet -- this file is a +//! placeholder so that the crate has the standard test layout, and so that whoever picks this up +//! has the context in one place. +//! +//! Notes for whoever does the port: +//! +//! * Wycheproof has no test set for the bare AES permutation; its AES coverage is all +//! mode-of-operation based (`aes_cbc_pkcs5_test.json`, `aes_gcm_test.json`, `aes_ccm_test.json`, +//! `aes_siv_cmac_test.json`, `aes_eax_test.json`, `aes_cmac_test.json`, ...). Those vectors +//! belong to the crates that implement those modes, not to this crate, and most of them exercise +//! exactly the things a raw engine has no opinion about: padding, tag checks, and nonce handling. +//! * The one thing worth extracting here is any test group that pins raw single-block behaviour, +//! which in practice means using a mode's vectors with a single full block and no padding as an +//! indirect check. +//! * Until the mode crates exist, the block-level coverage in `aes_tests.rs` (FIPS 197 Appendix B +//! plus the NIST "AES Core" ECB-AES128/192/256 vectors, in both directions) is the authoritative +//! set for this crate. +//! * See `crypto/mlkem/tests/wycheproof.rs` for the established pattern: locate the repo at either +//! `../wycheproof/testvectors_v1` or `../../../wycheproof/testvectors_v1`, skip the tests with a +//! printed warning if it is absent, and drive each test group from a parsed struct. +//! +//! The AES known-answer tests against the bc-test-data repo are a separate, similarly outstanding +//! task; see `crypto/mlkem/tests/bc_test_data.rs` for that pattern. diff --git a/crypto/core/src/key_material.rs b/crypto/core/src/key_material.rs index dc3244d..fb95d71 100644 --- a/crypto/core/src/key_material.rs +++ b/crypto/core/src/key_material.rs @@ -547,7 +547,7 @@ impl KeyMaterialTrait for KeyMaterial { } self.security_strength = strength; - + Ok(()) } diff --git a/crypto/factory/src/rng_factory.rs b/crypto/factory/src/rng_factory.rs index c316475..492efec 100644 --- a/crypto/factory/src/rng_factory.rs +++ b/crypto/factory/src/rng_factory.rs @@ -30,7 +30,7 @@ //! let output: Vec = h.hash(data); //! ``` //! Equivalently, it may be invoked by passing a string instead of using the constant: -//! +//! //! ``` //! use bouncycastle_factory::AlgorithmFactory; //! use bouncycastle_core::traits::Hash; diff --git a/crypto/hex/src/lib.rs b/crypto/hex/src/lib.rs index 3b53b45..923151d 100644 --- a/crypto/hex/src/lib.rs +++ b/crypto/hex/src/lib.rs @@ -3,14 +3,14 @@ //! This one is implemented using constant-time operations in the conversions //! from Strings to byte values, so it is safe to use on cryptographic secret values. //! -//! It should just work as expected: -//! encode takes any bytes-like rust type and returns a String, +//! It should just work as expected: +//! encode takes any bytes-like rust type and returns a String, //! decode takes a String (which can be in any bytes-like container) and returns a `Vec`. //! //! Moreover, the API of this crate is intended to mirror that of the public `hex` crate, //! so you should generally be able to swap `use hex` for `use bouncycastle_hex` and all the function //! calls and behaviours should work as expected. -//! +//! //! ``` //! use bouncycastle_hex as hex; //! @@ -72,7 +72,7 @@ pub fn encode_out>(input: T, out: &mut [u8]) -> Result::is_within_range(c as i64, 0, 9); let in_af = Condition::::is_within_range(c as i64, 10, 15); - // TODO: redo this once we have ct::u8 implemented + // TODO: redo this once we have ct::u8 implemented // The i64 is wasteful let c_09: i64 = '0' as i64 + (c as i64); @@ -111,7 +111,7 @@ pub fn decode_out>(input: T, out: &mut [u8]) -> Result { @@ -157,7 +157,7 @@ pub fn decode_out>(input: T, out: &mut [u8]) -> Result::is_within_range(b as i64, 65, 70); - // TODO: redo this once we have ct::u8 implemented + // TODO: redo this once we have ct::u8 implemented // The i64 is wasteful let c_09: i64 = b as i64 - ('0' as i64); diff --git a/crypto/mldsa-lowmemory/src/aux_functions.rs b/crypto/mldsa-lowmemory/src/aux_functions.rs index 4d91dc5..2726420 100644 --- a/crypto/mldsa-lowmemory/src/aux_functions.rs +++ b/crypto/mldsa-lowmemory/src/aux_functions.rs @@ -471,7 +471,7 @@ pub(crate) fn sample_in_ball( let mut j = [0u8]; for i in (N - TAU as usize)..N { // 7: (ctx, ๐‘—) โ† H.Squeeze(ctx, 1) - // Note: At first, it might seem to be faster to pre-squeeze a buffer outside the loop. + // Note: At first, it might seem to be faster to pre-squeeze a buffer outside the loop. // However, after experimentation and testing, the difference is not noticeable. h.squeeze_out(&mut j); @@ -567,7 +567,7 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2] // SHAKE is fairly inefficient if only 3 bytes are squeezed at a time, so the implementation does a block instead. // size is not a limitation as long as it is a multiple of 3. // 312 seems to be the sweet spot after some experimentation - // which is possibly also related with the average rejection rate. + // which is possibly also related with the average rejection rate. // Also, 312 is a multiple of 8 (efficient for SHAKE) let mut z_arr = [0u8; 312]; h.squeeze_out(&mut z_arr); diff --git a/crypto/mldsa-lowmemory/src/polynomial.rs b/crypto/mldsa-lowmemory/src/polynomial.rs index 2e672b5..95e3387 100644 --- a/crypto/mldsa-lowmemory/src/polynomial.rs +++ b/crypto/mldsa-lowmemory/src/polynomial.rs @@ -95,7 +95,7 @@ impl Polynomial { pub(crate) fn check_norm(&self) -> bool { // Fine that this is not constant-time (returns true early) because it is used in a rejection loop. - // IE the early quit here leads to rejection and continuing to the top of the rejection loop, or failing + // IE the early quit here leads to rejection and continuing to the top of the rejection loop, or failing // the signature validation. // So the i32 that was just checked in a non-constant-time manner is about to get thrown away. diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index af38aff..5e1b535 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -1097,7 +1097,7 @@ impl< /// Note that the PH expected here *is not the same* as the `mu` computed by [`MuBuilder`]. /// To make use of this function, the user needs to compute a straight hash of the message using - /// the same hash function as the indicated in the HashML-DSA variant; + /// the same hash function as the indicated in the HashML-DSA variant; /// for example: SHA256 for HashMDSA44_with_SHA256; SHA512 for HashMLDSA65_with_SHA512; etc. fn sign_ph_out( sk: &SK, diff --git a/crypto/mldsa/src/lib.rs b/crypto/mldsa/src/lib.rs index 91852f1..15fa740 100644 --- a/crypto/mldsa/src/lib.rs +++ b/crypto/mldsa/src/lib.rs @@ -116,11 +116,10 @@ //! //! `mu`, `ph`, and `ctx` are binding values that the verifier must reproduce. This means that getting //! them wrong does not compromise security, it just yields a signature the intended -//! verifier won't accept (a correctness/interoperability failure). +//! verifier won't accept (a correctness/interoperability failure). //! One caveat: `ctx` can still be security-relevant at the protocol level (domain separation, replay and //! cross-protocol binding), so choosing it incorrectly can weaken those properties. - #![no_std] #![forbid(unsafe_code)] #![forbid(missing_docs)] diff --git a/crypto/mldsa/tests/mldsa_tests.rs b/crypto/mldsa/tests/mldsa_tests.rs index a07d2e9..33aa8f9 100644 --- a/crypto/mldsa/tests/mldsa_tests.rs +++ b/crypto/mldsa/tests/mldsa_tests.rs @@ -1083,9 +1083,9 @@ struct Kat { } // generated by hand against bc-java -// This is almost tgId=1, tcId=1 from +// This is almost tgId=1, tcId=1 from // https://raw.githubusercontent.com/bcgit/bc-test-data/refs/heads/main/pqc/crypto/mldsa/ML-DSA-sigGen.txt -// with the difference being that an empty ctx is added, instead of testing sign_internal directly, +// with the difference being that an empty ctx is added, instead of testing sign_internal directly, // and the signature value is run against bc-java const MLDSA44_KAT1: Kat = Kat { _parameter_set: "ML-DSA-44", @@ -1096,9 +1096,9 @@ const MLDSA44_KAT1: Kat = Kat { signature: "2bddcee4a9ac1b9d19bc1531365c5613e48b95a530339c52f5fc0b671ee01b6587fe08b290c191f82eace640fae216cca90f40fa93bb309e5ff53afc5a042050bffeb69d4e0041c34fd334a7b576c6ebae68042b315fe78f84300dc011cdb144252a06f35c9a2b0a275d00552f890361d1e7b7439572097d1f3d5833b98b751412deb3c0df23ee3a30782919444bcbcc9ac25c645180c8d1a9fe693086cba4779d6e31b1e5fd1a0ccaeb055c471ef065a273676d78c6e7feebfc607c5578107ac27765345363af57f77431e306d9407ce365708d813e44d3107c108f8c5f2848913a4099f6a0ce2ebf80c7414236d4edc07fee79efe1ab6fe70217bec958ea48221fb6b87cef6f5762b41a9c698fbb45f5994969bee21e40f20c95f9a18389e8b49d6ecccdae9f568313448b5162c981bc9ff320753aede977b15f7ddc68bba076a07deb68ac36826e70d80752fac0356d507b3e283b350a17b015f76c71314f7c2086e44601c4d9c707c99a36e55716fd34384a218242a69876c8dea9f6ec28c40189ee2b8608040a96a813f38240dc85a511b3cebab1ea893270e730aef2e29406fcc1f6826101f1361168ceb3f1632e8ee505143e0f031e744928c1e41eab923ebbfce5e9f159fd160db737b013759382274a1d9409554ab06eb72b5ce2a710d8b08f163df991c956abc823d21b0a6d9d7ae484d6cdb2e04de7a282ef317f488e20d40e4e67cbf05d1528c0ce261e3a65163ad518661989484bc964da21122a6c95ef7036b3273f93833d068d9a2c7933e19ad2fde8378c286ad57f7e22c8aa4153718f356b46b34ca4a986e6e9ef3ddb206693acf3b08c0aa5118c8efbb6a99a0fc0674f6228e2f6f53e229129d4b6c0fd6455e28587f0f169270d044398e2c377ab6f985f7f2235d68192d031f39251f9f0270451beda2d29b654511b73bce0f30174a606a1100f4fc984959704ba0e2df2bdf642ba3b0241ca9889009ae575540c71747246835018ba8faeb473b39e4ede5a6fbe0165a4db2e90739cb051f3e5c1c0ce159539758569ecc8b67a759a2e786d5b4e96834db0dd460ab1c5c7ac8f3cf19596979108dfbdc6c8798e7342026b7571ba64fa86d68e1d5179af5768a7e22db76f3a319193d04eeea626d03d4be7dd83ed6c46e2149a1e2428e60f6566833726bcc93d0b342ff4245665b54167b013b02165d29fb64055112c5d800e853bf28bf149da2284e49dc1c5f478f490ce1002f16431564534244cd9c1e0bd12ea9209efe572afb7effc0153e5b2066b1af0dd05a2d9da13308c90a0244ad95a33a64d07a75d7596e46cdcfd154e18f7677ef16b979a1f431eb59279c65a3a31c5429ceebfcc869cf538722a82e6e765bd6a3042dc825684ad4163e054c113b47276b894580db17c92f0859c240b6812716e408a557c1d7ac8cfc3abf9b43f759bb2a13c903bba936206496fed37696171717cb8995b837e869052c9c25348698617c70e98966d30f56bb5bf41369c0131de4178637c5684a64992b50f9ab9a16b51752e14ea521d5bd42bacdfcf100085629796d22bcaa6a7830f9b5859d75b290a0db031bd51b77fa7911eb17d46dcf16d45e3b1ce8fb7dc19ba969a724e43ccce9487c143302c79fb208b2ab0ea1060af3809d5397c11cc0a79ed39ab06c0ef7bceb8e094ec0e3ea52937442ccc176a31b07a52d7de84c0599dbe736cecfacbb41cac206ec3dc20603606a14a1db3ccad8037a540f3213ee9337b529f447ed2c2e287760973b8175f70c78423ace6179757c519c14a0d7b81af00646afde573a35e909ef9660623cd3651b63b79be508698888e1d3bc87344067dcc099416616c273d5ea0e41a8723d11c5ec49e0c6019f6576a2e87d0237c3f64d49afbaf09fc818829d5d0fcb4336e08503d570dc3c96fa71fd7b2d4f51d0d1acc515c2c4dad23896273b2616f7d848816c5cc17f7bb2ed5f952a708c38eb13f0fdd8f14ec71893d3f19ccc8aa15fed848583093f4d8246f36db300c9cdd5d63b507641e4d2ba9ec284d17f4a05696be00da371bd5bab0ba56a13451278dbdeed02b4093e482b96269707b09a10dba9ca1dffb4e4185f9e392ebabcea1f9d26c037d9c0dcd71f2bc13faf559d195ef0d8be7baa0cf8aa105a8069356aa2287ec1003e0759f27b02772d4855bf0b7e0b3e97b7f77bd3119669695cc706090991e6a542eb7a28547978ba8a05d9103a90789108fa15898d14982bd0d5d098019700b37939f9f0d66e78af49c6318b886ba2a9101c072bb1e7bdbdb7319eb38e3320b4fbb1fcb28e8e7fac0bd493faf3c0547214465d55ca212dae99dc53addc3378b7e7b93acbdc1c9787149ed0214cd9846852ed7c18b23ab0ae8b7afbb725d44997a38422ff17b1dbeb22e2387094e0bc59496786114d0bb398f36fdfb06c70ff0ce47e9c3eb8c32c22062ccd5306026b606a9e9c628a377f0efd71087c95b3c1ffbdec8a91f311fbba4793d3d3fbf350e6a4a491d74ab7fb0cb66afa0d66177853df464f0175cdee4f97a4e620366aa18c2d04ebab82ff31ec07722fd53e0b4926973dd41d10422747261d8772b18ab55e0bbdcb89e224a5fc2679c2b729aaa4e1a78f95cf68af3562b98f0586d02134464f87dd15b843451a9160c5f4704c994a32259ca623c937431cfe55ee97d736916ac3e7ef831a1b6978539ac6c3304de2f43b3b208f73d071d17fd5cae631f617929468fc59d529deab1c0080a85ded180f9b7029376059c5ca3ba2eab9ee556a74373eadb5983f5990146b04bd255f2380450864e33c478cfe42072eeaabf8032f2c22fbf111407bf2cc41f6cc55596cca62a69303089c3ec231f42a8358d8bce315debce8cd4cea4aa478fba3476b5252e2d64da6b70d6ea0c1b4a99abebbd194e62e25442e7ecffc788710ac6dd1da815a0a5ef8dcef34ebcc90c6f29372875d664c2a06f3485dad8bd00d94837f417412399f9f085d8666fcaf38d38620897e2d06c7399eb28c5db0ffa42a233fdc6732c3e525bd49771aad03348aad068a5e729565c10d343a10c5cd6e530994c9a400354de3af3b39d20cf2b54fc0c9bde4b6f520688694d9b53c3628f1744b61271383d852219d8afae7a284d2f0b042f2fb70778b53d30876b5a031904a241e5a1741ff2e88bf8faa60472ba66111f0560ef127ee86d24f2c502fcff7575696f7f450109776f0f5e1bbd77a48952697fb2f8657516cc061de2bcc6aff9b861356b97e576f78abab1d3acb70d14a84b7858c22b2a0ee17e1231a2da3738018205091b263c42597ea6d3d7d9dae3fa030a13162243494c4e7f8fa1a2d8e2ff11153a41474a63646770717487919da9bac9d2d3e3070f23455d686a7dbcc9de00000000000000000000000000000000000f1f343f", }; -// This is almost tgId=1, tcId=1 from +// This is almost tgId=1, tcId=1 from // https://raw.githubusercontent.com/bcgit/bc-test-data/refs/heads/main/pqc/crypto/mldsa/ML-DSA-sigGen.txt -// with the difference being that an empty ctx is added, instead of testing sign_internal directly, +// with the difference being that an empty ctx is added, instead of testing sign_internal directly, // and the signature value is run against bc-java const MLDSA65_KAT1: Kat = Kat { _parameter_set: "ML-DSA-65", @@ -1110,9 +1110,9 @@ const MLDSA65_KAT1: Kat = Kat { }; // generated by hand against bc-java -// This is almost tgId=1, tcId=1 from +// This is almost tgId=1, tcId=1 from // https://raw.githubusercontent.com/bcgit/bc-test-data/refs/heads/main/pqc/crypto/mldsa/ML-DSA-sigGen.txt -// with the difference being that an empty ctx is added, instead of testing sign_internal directly, +// with the difference being that an empty ctx is added, instead of testing sign_internal directly, // and the signature value is run against bc-java const MLDSA87_KAT1: Kat = Kat { _parameter_set: "ML-DSA-87", diff --git a/crypto/mlkem-lowmemory/src/aux_functions.rs b/crypto/mlkem-lowmemory/src/aux_functions.rs index 7df893d..b548f44 100644 --- a/crypto/mlkem-lowmemory/src/aux_functions.rs +++ b/crypto/mlkem-lowmemory/src/aux_functions.rs @@ -62,7 +62,7 @@ pub(crate) fn byte_decode(B: &[u8; PACK_L for j in 0..d { // select the next bit, according to bitcount, then shift it up by j // there is supposed to be a `mod m` here, but that shouldn't matter as they are being checked below - F[i] |= (((B[(i * d + j) / 8] >> (i * d + j) % 8) & 1) as i16) << j; + F[i] |= (((B[(i * d + j) / 8] >> (i * d + j) % 8) & 1) as i16) << j; } // assert the mod m // These are relaxed because they are being checked above in MLKEMPublicKey::pk_decode() diff --git a/crypto/mlkem-lowmemory/src/lib.rs b/crypto/mlkem-lowmemory/src/lib.rs index 207da32..ae93345 100644 --- a/crypto/mlkem-lowmemory/src/lib.rs +++ b/crypto/mlkem-lowmemory/src/lib.rs @@ -208,16 +208,16 @@ //! There are, however, a few exceptions worth mentioning. //! //! If using a [`MLKEM::keygen_from_seed`], then it is your responsibility to ensure that the seed is -//! cryptographically random and unpredictable at a security strength that matches the MLKEM parameter set. +//! cryptographically random and unpredictable at a security strength that matches the MLKEM parameter set. //! //! Also, [`MLKEM::encaps_internal`] requires the encapsulation randomness to be provided, so the ciphertext //! will only be as strong as the randomness that you provide. -//! +//! //! A note about cryptographic side-channel attacks: considerable effort has been expended to attempt //! to make this implementation constant-time, which generally means that the core mathematical algorithm //! code that handles secret data uses bitshift-and-xor type constructions instead of if-and-loop //! constructions. That should give this implementation reasonably good resistance to timing and -//! power analysis key extraction attacks, however: +//! power analysis key extraction attacks, however: //! A) this is a "best-effort" and not formally verified, and //! B) the Rust compiler does not guarantee constant-time behaviour no matter how good the design is code, //! so like all Safe Rust code (ie Rust code that does not include inline assembly), @@ -227,7 +227,7 @@ #![no_std] #![forbid(missing_docs)] #![forbid(unsafe_code)] -// These are because variable names need to be matched exactly against FIPS 204, +// These are because variable names need to be matched exactly against FIPS 204, // for example both 'K' and 'k', or 'A' and 'a' are used and have specific meanings. // linter needs to be instructed to ignore these cases #![allow(non_snake_case)] diff --git a/crypto/mlkem/src/aux_functions.rs b/crypto/mlkem/src/aux_functions.rs index 88966d7..0999837 100644 --- a/crypto/mlkem/src/aux_functions.rs +++ b/crypto/mlkem/src/aux_functions.rs @@ -301,9 +301,8 @@ pub(crate) fn barrett_reduce(a: i16) -> i16 { a - (((t as i32) * q as i32) as i16) } - -// Not currently used. It is left here as a reference since it's useful for debugging if it's -// necessary to output values that are normalized to [0,q] to compare against intermediate results +// Not currently used. It is left here as a reference since it's useful for debugging if it's +// necessary to output values that are normalized to [0,q] to compare against intermediate results // from other libraries. // pub(super) fn cond_sub_q(a: i16) -> i16 { // let tmp = a - q; diff --git a/crypto/mlkem/src/polynomial.rs b/crypto/mlkem/src/polynomial.rs index 6617e46..4002528 100644 --- a/crypto/mlkem/src/polynomial.rs +++ b/crypto/mlkem/src/polynomial.rs @@ -9,7 +9,7 @@ use crate::mlkem::{N, q}; /// A polynomial over the ML-KEM ring. /// -/// Dev note: The following structure does not necessarily need to be declared as public. +/// Dev note: The following structure does not necessarily need to be declared as public. /// There is no real scenario where this function needs to be called directly. /// However, in order to test the Debug and Display traits, it is necessary to use STD, so those /// can't be tested from inline tests in this file and the real unit tests are in a different crate. @@ -46,8 +46,8 @@ impl Polynomial { Self { coeffs: [0i16; N] } } - /// Encodes a 32-byte message `m` into a `Polynomial`, implementing the message - /// encoding step of K-PKE.Encrypt `Decompress_1(ByteDecode_1(m))`, + /// Encodes a 32-byte message `m` into a `Polynomial`, implementing the message + /// encoding step of K-PKE.Encrypt `Decompress_1(ByteDecode_1(m))`, /// (FIPS 203, Alg. 14). Each message bit becomes one coefficient: `Decompress_1` /// (ยง4.2.1) maps bit `1` to `โŒˆq/2โŒ‰ = (q + 1) / 2 = 1665` (for `q = 3329`) and bit /// `0` to `0`, placing a set bit at the point farthest from `0` to maximize the @@ -67,25 +67,25 @@ impl Polynomial { w } - /// Decodes a `Polynomial` into its 32-byte message `m`, implementing the message + /// Decodes a `Polynomial` into its 32-byte message `m`, implementing the message /// recovery step of K-PKE.Decrypt `ByteEncode_1(Compress_1(self))`, /// (FIPS 203, Alg. 15). Each coefficient yields one message bit: `Compress_1` /// (ยง4.2.1) sets the bit when the coefficient lies nearer `q/2` than `0`, i.e. in /// the central interval `[833, 2496]` for `q = 3329`. The decision is computed - /// branchlessly and the bits are packed LSB-first. - /// Coefficients are expected to already be canonical in `[0, q]`: the unsigned - /// interval test is not periodic mod `q`, so the caller reduces beforehand (`poly_reduce()` + /// branchlessly and the bits are packed LSB-first. + /// Coefficients are expected to already be canonical in `[0, q]`: the unsigned + /// interval test is not periodic mod `q`, so the caller reduces beforehand (`poly_reduce()` /// in `pke_decrypt`) and no reduction is repeated here. pub(crate) fn to_msg(self) -> [u8; 32] { - const LOWER: i32 = q as i32 >> 2; // โŒŠq/4โŒ‹ = 832 - const UPPER: i32 = q as i32 - LOWER; // q - โŒŠq/2โŒ‹ = 2497 + const LOWER: i32 = q as i32 >> 2; // โŒŠq/4โŒ‹ = 832 + const UPPER: i32 = q as i32 - LOWER; // q - โŒŠq/2โŒ‹ = 2497 let mut msg = [0u8; 32]; // Using full reduce() might be expected here. - // However, this function is only called by pke_decrypt (see mlkem.rs), which performs a + // However, this function is only called by pke_decrypt (see mlkem.rs), which performs a // reduction on every coefficient of the polynomial immediately prior to the call. - // For completeness, testing against the bc-test-data set of KATs shows that everything passes + // For completeness, testing against the bc-test-data set of KATs shows that everything passes // without modular reduction. // self.cond_sub_q(); @@ -101,8 +101,8 @@ impl Polynomial { msg } - // Not currently used. It is left here as a reference since it's useful for debugging if it's - // necessary to output values that are normalized to [0,q] to compare against intermediate results + // Not currently used. It is left here as a reference since it's useful for debugging if it's + // necessary to output values that are normalized to [0,q] to compare against intermediate results // from other libraries. // pub(crate) fn conditional_add_q(&mut self) { // for x in self.0.iter_mut() { @@ -157,7 +157,7 @@ impl Polynomial { // bc-java has a cond_sub_q() here, however, it is not needed // The reason for this is because a modular reduction is performed immediately // prior to calling pack_ciphertext in mlkem.rs - // This can be corroborated by running the corresponding unit tests + // This can be corroborated by running the corresponding unit tests // let mut s = self.clone(); // s.cond_sub_q(); @@ -250,8 +250,8 @@ impl Polynomial { v } - // Not currently used. It is left here as a reference since it's useful for debugging if it's - // necessary to output values that are normalized to [0,q] to compare against intermediate results + // Not currently used. It is left here as a reference since it's useful for debugging if it's + // necessary to output values that are normalized to [0,q] to compare against intermediate results // from other libraries. // pub(crate) fn cond_sub_q(&mut self) { // for i in 0..N { @@ -357,8 +357,8 @@ pub fn base_mult_montgomery(a: &Polynomial, b: &Polynomial) -> Polynomial { r } -// Not currently used. It is left here as a reference since it's useful for debugging if it's -// necessary to output values that are normalized to [0,q] to compare against intermediate results +// Not currently used. It is left here as a reference since it's useful for debugging if it's +// necessary to output values that are normalized to [0,q] to compare against intermediate results // from other libraries. // /// if a is in \[-q..0], then it shifts it up by q to be in \[0..q] // pub(crate) fn conditional_add_q(a: i16) -> i16 { diff --git a/crypto/mlkem/tests/mlkem_key_tests.rs b/crypto/mlkem/tests/mlkem_key_tests.rs index b3e1e4f..24930d2 100644 --- a/crypto/mlkem/tests/mlkem_key_tests.rs +++ b/crypto/mlkem/tests/mlkem_key_tests.rs @@ -61,7 +61,6 @@ mod mlkem_key_tests { // 2) whether it calculates H(ek) properly from a private key // 3) whether it rejects a private key if the H(ek) is wrong - let seed = KeyMaterial512::from_bytes_as_type( &hex::decode( "000102030405060708090a0b0c0d0e0f diff --git a/crypto/rng/src/hash_drbg80090a.rs b/crypto/rng/src/hash_drbg80090a.rs index 5dc1013..be70cb8 100644 --- a/crypto/rng/src/hash_drbg80090a.rs +++ b/crypto/rng/src/hash_drbg80090a.rs @@ -216,7 +216,7 @@ impl Sp80090ADrbg for HashDRBG80090A { "Provided seed exceeds the maximum seed length.", ))?; } - // On purpose not checking the SecurityStrength field of the seed, + // On purpose not checking the SecurityStrength field of the seed, // because we assume it's pure entropy and hasn't been touched by any actual algoritms yet. if security_strength > H::MAX_SECURITY_STRENGTH { return Err(KeyMaterialError::SecurityStrength( @@ -527,7 +527,7 @@ impl RNG for HashDRBG80090A { /// the hash_df function as defined in SP 800-90Ar1 section 10.3.1. /// no_of_bits_to_return is the length of the provided output buffer. -/// Because array concatenation is not available in a no_std / no_alloc build, this takes many input parameters. +/// Because array concatenation is not available in a no_std / no_alloc build, this takes many input parameters. // To leave a parameter unused, simply provide an empty array &[0u8;0] fn hash_df( in1: &[u8], @@ -550,7 +550,7 @@ fn hash_df( let len = u32::div_ceil(out.len() as u32, H::OUTPUT_LEN as u32); let mut counter: u8 = 0x01; - // note: this could probably be performance optimized a tiny bit by pulling no_of_bits_to_return.to_le_bytes() + // note: this could probably be performance optimized a tiny bit by pulling no_of_bits_to_return.to_le_bytes() // out of the loop and by merging i and counter into the same variable. for i in 1..len { let mut h = H::default(); @@ -566,7 +566,7 @@ fn hash_df( } // Handle the last block separately since not all of it will fit in the output buffer. - // TODO: Check whether it is necessary to do a last block, + // TODO: Check whether it is necessary to do a last block, // or was the requested number of bits already a multiple of the output length let bytes_written = (len - 1) as usize * H::OUTPUT_LEN; let remainder = out.len() - bytes_written; @@ -673,7 +673,7 @@ fn hashgen(v: &[u8], out: &mut [u8]) { } // Handle the last block separately since not all of it will fit in the output buffer. - // TODO: Check whether it is necessary to do a last block, + // TODO: Check whether it is necessary to do a last block, // or was the requested number of bits already a multiple of the output length let bytes_written = (m - 1) as usize * H::OUTPUT_LEN; let remainder = out.len() - bytes_written; diff --git a/crypto/rng/src/lib.rs b/crypto/rng/src/lib.rs index 1840f4c..3040358 100644 --- a/crypto/rng/src/lib.rs +++ b/crypto/rng/src/lib.rs @@ -66,13 +66,13 @@ pub type Default128BitRNG = HashDRBG_SHA256; /// The library's default RNG at the 256-bit security level. pub type Default256BitRNG = HashDRBG_SHA512; -/// Implements the five functions specified in SP 800-90A section 7.4 are -/// - instantate, -/// - generate, -/// - reseed, -/// - uninstantiate, and +/// Implements the five functions specified in SP 800-90A section 7.4 are +/// - instantate, +/// - generate, +/// - reseed, +/// - uninstantiate, and /// - health_test. -/// Note: this function implements Rust's Drop on the sensitive working state in place of the explicit +/// Note: this function implements Rust's Drop on the sensitive working state in place of the explicit /// Uninstantiate function listed in SP 800-90Ar1. pub trait Sp80090ADrbg { /// The input KeyMaterial must be of type [`KeyType::Seed`]. @@ -91,7 +91,7 @@ pub trait Sp80090ADrbg { /// required. /// """ /// - /// This function takes ownership of the seed KeyMaterial object, + /// This function takes ownership of the seed KeyMaterial object, /// to reduce the likelihood of its reuse in a second function call. /// /// There is no entropy requirement on the nonce, but it is expected as a KeyMaterial so that it @@ -106,7 +106,7 @@ pub trait Sp80090ADrbg { ) -> Result<(), RNGError>; /// Reseeds the DRBG with the provided seed. - /// TODO: this needs to be redesigned to take some sort of EntropySource object that will work well + /// TODO: this needs to be redesigned to take some sort of EntropySource object that will work well // with DRBGs that require frequent reseeding. fn reseed( &mut self, diff --git a/crypto/rng/tests/hash_drbg80090a_tests.rs b/crypto/rng/tests/hash_drbg80090a_tests.rs index 840dacf..4a8967b 100644 --- a/crypto/rng/tests/hash_drbg80090a_tests.rs +++ b/crypto/rng/tests/hash_drbg80090a_tests.rs @@ -83,11 +83,11 @@ mod tests { _ => panic!("Expected KeyMaterialError error"), } - // Skipping tests for max lengths of seeds and personalization strings - // because they are on the order of a gigabyte in size. + // Skipping tests for max lengths of seeds and personalization strings + // because they are on the order of a gigabyte in size. // Testing would blow up the test suite. - // Error case: security strength requested at init is higher than the underlying + // Error case: security strength requested at init is higher than the underlying // hash function's max security strength let mut rng = HashDRBG_SHA256::new_unititialized(); let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::Seed).unwrap(); @@ -96,7 +96,7 @@ mod tests { _ => panic!("Expected KeyMaterialError error"), } - // Success case: security strength requested at init is lower than the underlying + // Success case: security strength requested at init is lower than the underlying // hash function's max security strength // ... 112 bit let mut rng = HashDRBG_SHA256::new_unititialized(); @@ -156,10 +156,9 @@ mod tests { _ => panic!("Expected KeyMaterialError error"), } - // Skipping tests for max lengths of seeds and personalization strings - // because they are on the order of a gigabyte in size. + // Skipping tests for max lengths of seeds and personalization strings + // because they are on the order of a gigabyte in size. // Testing would blow up the test suite. - } #[test] @@ -194,8 +193,8 @@ mod tests { _ => panic!("Expected Uninitialized error"), } - // Skipping tests for max lengths of seeds and personalization strings - // because they are on the order of a gigabyte in size. + // Skipping tests for max lengths of seeds and personalization strings + // because they are on the order of a gigabyte in size. // Testing would blow up the test suite. // TODO: Tests for ReseedRequired. Investigate how this gets triggered. The limits are in the exobyte range. @@ -240,8 +239,8 @@ mod tests { _ => panic!("Expected Uninitialized error"), } - // Skipping tests for max lengths of seeds and personalization strings - // because they are on the order of a gigabyte in size. + // Skipping tests for max lengths of seeds and personalization strings + // because they are on the order of a gigabyte in size. // Testing would blow up the test suite. // TODO: tests for ReseedRequired. Investigate how this gets triggered. The limits are in the exobyte range. @@ -291,8 +290,8 @@ mod tests { Ok(_) => panic!("Expected Uninitialized error"), } - // Skipping tests for max lengths of seeds and personalization strings - // because they are on the order of a gigabyte in size. + // Skipping tests for max lengths of seeds and personalization strings + // because they are on the order of a gigabyte in size. // Testing would blow up the test suite. // TODO: tests for ReseedRequired. Investigate how this gets triggered. The limits are in the exobyte range. diff --git a/crypto/sha2/src/sha256.rs b/crypto/sha2/src/sha256.rs index 7cee95c..34d0977 100644 --- a/crypto/sha2/src/sha256.rs +++ b/crypto/sha2/src/sha256.rs @@ -150,7 +150,7 @@ pub struct SHA256Internal { byte_count: u64, x_buf: Secret<[u8; 64]>, x_buf_off: usize, - // TODO: Investigate whether maximum message size (according to FIPS 180-4) should be added + // TODO: Investigate whether maximum message size (according to FIPS 180-4) should be added // (2^64 for SHA256 and 2^128 for SHA512) } diff --git a/crypto/sha2/src/sha512.rs b/crypto/sha2/src/sha512.rs index 60207eb..c31e306 100644 --- a/crypto/sha2/src/sha512.rs +++ b/crypto/sha2/src/sha512.rs @@ -161,7 +161,7 @@ pub struct SHA512Internal { _params: std::marker::PhantomData, state: Sha512State, // NOTE The code currently only supports 2^67 bits, not the full 2^128 - byte_count: u64, + byte_count: u64, x_buf: Secret<[u8; 128]>, x_buf_off: usize, } diff --git a/crypto/sha3/src/sha3.rs b/crypto/sha3/src/sha3.rs index 0393507..3da20b0 100644 --- a/crypto/sha3/src/sha3.rs +++ b/crypto/sha3/src/sha3.rs @@ -169,14 +169,14 @@ impl Hash for SHA3Internal { output } - // TODO: investigate why this doesn't take a &mut [u8; HASH_LEN] + // TODO: investigate why this doesn't take a &mut [u8; HASH_LEN] // Being able to do so would improve ergonomics fn do_final_out(mut self, output: &mut [u8]) -> usize { output.fill(0); - // this shouldn't fail because, by construction, the function is only called once, + // this shouldn't fail because, by construction, the function is only called once, // and this is the only way to absorb partial bits. - self.keccak.absorb_bits(0x02, 2).expect("do_final_out: keccak.absorb_bits failed."); + self.keccak.absorb_bits(0x02, 2).expect("do_final_out: keccak.absorb_bits failed."); let bytes_written = if output.len() <= self.output_len() { self.keccak.squeeze(output) @@ -210,7 +210,7 @@ impl Hash for SHA3Internal { ) -> Result { output.fill(0); - // Mutants note: This is just bit-setting into empty space. + // Mutants note: This is just bit-setting into empty space. // It works the same regardless of whether it's OR or XOR. let mut final_input: u16 = ((partial_byte as u16) & ((1 << num_partial_bits) - 1)) | (0x02 << num_partial_bits); diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index 1bd91dc..6ba2a88 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -92,7 +92,7 @@ impl SHAKEInternal { mut self, additional_input: &[u8], ) -> Result, KDFError> { - // At the moment, oversized KeyMaterial is returned for most cases. + // At the moment, oversized KeyMaterial is returned for most cases. let mut output_key = KeyMaterial::<64>::new(); self.derive_key_out_final_internal(additional_input, &mut output_key)?; @@ -307,7 +307,7 @@ impl XOF for SHAKEInternal { if !(1..=7).contains(&num_partial_bits) { return Err(HashError::InvalidLength("must be in the range [0,7]")); } - // Mutants note: This is just bit-setting into empty space. + // Mutants note: This is just bit-setting into empty space. // It works the same regardless of whether it's OR or XOR. let mut final_input: u16 = ((partial_byte as u16) & ((1 << num_partial_bits) - 1)) | (0x0F << num_partial_bits); diff --git a/src/lib.rs b/src/lib.rs index b46df8c..c705213 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub use bouncycastle_aes as aes; pub use bouncycastle_base64 as base64; pub use bouncycastle_core as core; pub use bouncycastle_factory as factory;