From 87c1ae787e240101c73ea1eeb3f3495eba8276e9 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 15:59:33 +1100 Subject: [PATCH 01/57] sm3: from_suspended rejects a byte_count that disagrees with x_buf_off modulo 64 (as SHA-256 does) or exceeds 2^61 - 1 bytes, so a resumed state can no longer finalise to a digest of no message, wrap the length field, or trip the do_update debug assert; the message-length doc says 2^61 - 1 bytes rather than 2^64; a comment and a known answer (55 bytes + 7 bits, from an independent reference checked against openssl) pin the padding at l = 447 mod 512, where the draft's prose and closed-form k disagree; Usage Examples heading, draft-sca-cfrg-sm3-02 citation and a paren fixed; cargo mutants: 188 mutants, 171 caught, 10 unviable, 1 timeout and 6 missed, all documented equivalents (the MAX_MESSAGE_BYTES pair the review found is now caught) Assisted-by: Claude:claude-opus-5-5 --- crypto/sm3/src/lib.rs | 9 +++++---- crypto/sm3/src/sm3.rs | 18 +++++++++++++++--- crypto/sm3/tests/sm3_tests.rs | 29 ++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/crypto/sm3/src/lib.rs b/crypto/sm3/src/lib.rs index 6e8d1f92f..695ab2ffd 100644 --- a/crypto/sm3/src/lib.rs +++ b/crypto/sm3/src/lib.rs @@ -1,10 +1,10 @@ //! Implements the SM3 cryptographic hash function as per GB/T 32905-2016 (also ISO/IEC 10118-3:2018 -//! and IETF draft-shen-sm3-hash-01). +//! and IETF draft-sca-cfrg-sm3-02). //! //! SM3 is a 256-bit Merkle–Damgård hash with a 512-bit block, structurally similar to SHA-256 but //! with different message expansion, round compression functions and constants. //! -//! # Examples +//! # Usage Examples //! ## Hash //! Hash functionality is accessed via the [`Hash`] trait, which is implemented by [`SM3`]. //! @@ -69,12 +69,13 @@ //! //! * SM3 offers 128 bits of collision resistance and 256 bits of preimage resistance. //! * SM3 is a Merkle–Damgård construction and is therefore subject to length-extension: -//! `H(k || m)` is not a secure MAC. Use HMAC ([`sm3::hmac`](crate::hmac) for keyed hashing. +//! `H(k || m)` is not a secure MAC. Use HMAC ([`sm3::hmac`](crate::hmac)) for keyed hashing. //! * The chaining value and input buffer are held in [`bouncycastle_utils::secret::Secret`] and //! zeroized on drop. Transient copies (working variables and message schedule) in registers/stack //! locals during compression are not zeroized. //! * The implementation contains no data-dependent branches or table lookups. -//! * Messages up to 2^64 bytes are supported (the specification allows 2^64 bits). +//! * Message length: the specification permits messages shorter than 2^64 bits, so this +//! implementation supports 2^61 - 1 bytes. //! //! # Suspending and resuming execution //! diff --git a/crypto/sm3/src/sm3.rs b/crypto/sm3/src/sm3.rs index b351b13fb..cab5c79f5 100644 --- a/crypto/sm3/src/sm3.rs +++ b/crypto/sm3/src/sm3.rs @@ -70,7 +70,7 @@ fn p1(x: u32) -> u32 { pub struct SM3 { /// Chaining value V^(i), 8 big-endian words. v: Secret<[u32; 8]>, - /// Total number of message bytes absorbed so far. Supports messages up to 2^64 bytes. + /// Total number of message bytes absorbed so far, at most `MAX_MESSAGE_BYTES`. byte_count: u64, /// Buffered input that has not yet formed a whole block. x_buf: Secret<[u8; 64]>, @@ -189,8 +189,11 @@ impl SM3 { self.x_buf[self.x_buf_off] = pad_byte; self.x_buf_off += 1; - // ... then k zero bits so that l + 1 + k = 448 mod 512. If the 64-bit length field no longer - // fits in this block, zero-fill and compress, then start a fresh block. + // ... then k zero bits, "where k is the smallest non-negative number that satisfies + // l + 1 + k = 448 (mod 512)". If the 64-bit length field no longer fits in this block, + // zero-fill and compress, then start a fresh block. At l = 447 (mod 512) this gives k = 0; + // the draft's closed form k = 512 - (((l mod 512) + 1 + 64) mod 512) would give k = 512 and + // a different digest. The prose rule is followed. if self.x_buf_off > 56 { self.x_buf[self.x_buf_off..].fill(0x00); Self::compress(&mut self.v, slice::from_ref(&self.x_buf)); @@ -357,6 +360,15 @@ impl Suspendable for SM3 { if x_buf_off >= 64 { return Err(SuspendableError::InvalidData); } + // do_update keeps byte_count congruent to x_buf_off modulo the block length, so a state in + // which they disagree describes no message and must not resume. + if byte_count % 64 != x_buf_off as u64 { + return Err(SuspendableError::InvalidData); + } + // A longer message cannot be encoded in the 64-bit length field (s. 5.1, l < 2^64 bits). + if byte_count > MAX_MESSAGE_BYTES { + return Err(SuspendableError::InvalidData); + } Ok(SM3 { v, byte_count, x_buf, x_buf_off }) } diff --git a/crypto/sm3/tests/sm3_tests.rs b/crypto/sm3/tests/sm3_tests.rs index 47097ff46..c56d71598 100644 --- a/crypto/sm3/tests/sm3_tests.rs +++ b/crypto/sm3/tests/sm3_tests.rs @@ -169,7 +169,7 @@ mod sm3_tests { /// `partial_byte` (ASN.1 BIT STRING order). #[test] fn partial_bits_known_answers() { - let cases: [(&[u8], u8, usize, &str); 6] = [ + let cases: [(&[u8], u8, usize, &str); 7] = [ (b"", 0x80, 1, "985ffe9568be96328729b1c16631e9328d356432413d7556a646b9eefe479b9e"), (b"", 0xA8, 5, "469dd7b688a7b98d6362a8e2488a148cb4231bc196b796eee9652cb9044f3dcd"), (b"abc", 0xfe, 7, "5ad9f5745671e4a49f6704fdadff8cc2ff8a9683d1c7c0810a5dd7db367e9d74"), @@ -179,6 +179,14 @@ mod sm3_tests { 2, "65985be43230ee70a939d38e34a88198e0d63bb307081459d8d75541d54a382e", ), + // l = 447 (mod 512): s. 5.2's prose gives k = 0 and its closed-form k = 512; this is the + // prose digest. + ( + &[0x5a; 55], + 0xFE, + 7, + "e50c8288dfc79d82120e1f33765898380ea6fe6a685a145df0f836ab0e6dac76", + ), ( &[0x5a; 111], 0xA0, @@ -237,5 +245,24 @@ mod sm3_tests { Err(SuspendableError::InvalidData) => { /* good */ } _ => panic!("Expected an error"), } + + // byte_count must agree with x_buf_off modulo the block length, and stay within the + // longest message the 64-bit length field can encode, 2^61 - 1 bytes. + let with_counts = |byte_count: u64, x_buf_off: u8| { + let mut state = serialized_state; + state[3 + 32..3 + 40].copy_from_slice(&byte_count.to_le_bytes()); + state[3 + 104] = x_buf_off; + SM3::from_suspended(state) + }; + let off = serialized_state[3 + 104]; + assert!(with_counts(str.len() as u64 + 64, off).is_ok()); + assert!(matches!( + with_counts(str.len() as u64 + 1, off), + Err(SuspendableError::InvalidData) + )); + let max: u64 = (1 << 61) - 1; + assert!(with_counts(max, 63).is_ok()); + assert!(matches!(with_counts(max + 1, 0), Err(SuspendableError::InvalidData))); + assert!(matches!(with_counts(u64::MAX, 63), Err(SuspendableError::InvalidData))); } } From ce18b96f809098f9f357c0cca55859ecd80cd1d0 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 16:47:16 +1100 Subject: [PATCH 02/57] utils: Secret's byte-view equality now needs the new unsafe trait NoPadding (implemented for the primitives, bool, char and arrays of them), so a padded ZeroizablePrimitive defined in safe code can still be held and zeroized but no longer compared, which read its uninitialised padding through from_raw_parts (a compile_fail doctest pins it; ZeroizablePrimitive stays safe, so mlkem and mldsa keep forbid(unsafe_code); AESParams::Schedule gains the NoPadding bound its constant-time key comparison needs); Condition::is_bit_set returns FALSE for an index of BITS or more instead of panicking in debug and masking the shift in release (all four widths, with tests); add_lib_ver and check_lib_ver assert SERIALIZED_LEN >= LIB_VERSION_LEN at compile time, which removes check_lib_ver's runtime length check and its unwrap; parse_version_component fails the build on a component above 255 or an empty one whatever -C overflow-checks says, where a release build wrapped "300" to 44; doc fixes: check_lib_ver's stale return description, the negate example that assumed TRUE == 1, ct_eq_zero_bytes returning a public bool, bounded_usize's max, a tautological debug_assert in zeroize, two typos; cargo mutants: 395 mutants, 267 caught, 100 unviable, 1 timeout (+= as *= in the new parse loop never terminates), 27 missed, none in changed code: 7 | as ^ equivalents and Secret's drop as before, 19 in the suspendable_state Cursor, check_lib_ver and bounded_usize that the review lists as missing tests Assisted-by: Claude:claude-opus-5-5 --- crypto/aes/src/lib.rs | 6 +- crypto/utils/src/ct.rs | 91 +++++++++++---------------- crypto/utils/src/secret.rs | 60 ++++++++++++++---- crypto/utils/src/suspendable_state.rs | 82 +++++++++++++++++++----- crypto/utils/tests/ct_tests.rs | 24 +++++++ 5 files changed, 177 insertions(+), 86 deletions(-) diff --git a/crypto/aes/src/lib.rs b/crypto/aes/src/lib.rs index f203062ee..8c0460381 100644 --- a/crypto/aes/src/lib.rs +++ b/crypto/aes/src/lib.rs @@ -189,7 +189,7 @@ mod round; mod sbox; mod schedule; -use bouncycastle_utils::secret::ZeroizablePrimitive; +use bouncycastle_utils::secret::{NoPadding, ZeroizablePrimitive}; /// The AES block length in bytes: 16 (FIPS 197 Sec 3.4, `Nb` = 4 words). pub const AES_BLOCK_LEN: usize = 16; @@ -223,8 +223,8 @@ pub trait AESParams: AESParamsInternalTrait { /// The algorithm name, as reported by `Algorithm::ALG_NAME`. const ALG_NAME: &'static str; /// `[u32; 4 * (NR + 1)]` -- the bit-sliced schedule; its layout is - /// described in `schedule.rs`. - type Schedule: ZeroizablePrimitive + AsRef<[u32]> + AsMut<[u32]>; + /// described in `schedule.rs`. `NoPadding` lets keys compare their schedules in constant time. + type Schedule: ZeroizablePrimitive + NoPadding + AsRef<[u32]> + AsMut<[u32]>; } /// AES-128 parameters: 16-byte key, `Nk` = 4, `Nr` = 10 (FIPS 197 Table 3). diff --git a/crypto/utils/src/ct.rs b/crypto/utils/src/ct.rs index ab8ee9b97..b332e2b21 100644 --- a/crypto/utils/src/ct.rs +++ b/crypto/utils/src/ct.rs @@ -112,10 +112,15 @@ impl Condition { pub const fn from_lsb(value: i64) -> Self { Self(-(value & 1)) } - /// TRUE iff bit `bit` of `value` is set. The bit index must be public data - /// (the shift amount is timing-visible on some targets). + /// TRUE iff bit `bit` of `value` is set; FALSE for `bit >= i64::BITS`. The bit index must be + /// public data (the shift amount is timing-visible on some targets). pub const fn is_bit_set(value: i64, bit: u32) -> Self { - Self::from_lsb(value >> bit) + // Branches on the public index only: a plain `>>` panics in debug and masks the shift + // amount in release for an out-of-range index. + match value.checked_shr(bit) { + Some(shifted) => Self::from_lsb(shifted), + None => Self::FALSE, + } } /// TRUE iff `value < 0`, i.e. the sign (top) bit is set. The unsigned /// counterpart of this mask is `from_msb`, where the top bit carries a @@ -185,26 +190,8 @@ impl Condition { *dst = self.select(src, *dst); } - /// Conditionally negate the value. - /// - /// negate(-1) gives -3 - /// - /// `value` is `-1` (i.e., all bits are `1`, `...1111`) - /// - /// Condition `self.0` is 1 (`...0001`) (assuming `TRUE`) - /// - /// XOR operation was executed as `value ^ self.0` - /// - /// Then `...1111 XOR ...0001 = ...1110` (i.e., `-2`) - /// - /// Subtraction operation is `wrapping_sub(self.0)` - /// - /// Then `-2 - 1 = -3` - /// - /// As a result, `1`, which is the negation of `-1`, should be returned, but `-3` is output. - /// - /// Therefore, if the [`Self::TRUE`] constant value of the [`Condition`] implementation is changed to `-1`, - /// the test also runs normally. + /// Conditionally negate the value: returns `-value` (wrapping, so `MIN` stays `MIN`) if the + /// condition is true, otherwise `value`. pub fn negate(self, value: i64) -> i64 { let mask = self.barrier().0; (value ^ mask).wrapping_sub(mask) @@ -251,10 +238,15 @@ impl Condition { pub const fn from_lsb(value: i32) -> Self { Self(-(value & 1)) } - /// TRUE iff bit `bit` of `value` is set. The bit index must be public data - /// (the shift amount is timing-visible on some targets). + /// TRUE iff bit `bit` of `value` is set; FALSE for `bit >= i32::BITS`. The bit index must be + /// public data (the shift amount is timing-visible on some targets). pub const fn is_bit_set(value: i32, bit: u32) -> Self { - Self::from_lsb(value >> bit) + // Branches on the public index only: a plain `>>` panics in debug and masks the shift + // amount in release for an out-of-range index. + match value.checked_shr(bit) { + Some(shifted) => Self::from_lsb(shifted), + None => Self::FALSE, + } } /// TRUE iff `value < 0`, i.e. the sign (top) bit is set. The unsigned /// counterpart of this mask is `from_msb`, where the top bit carries a @@ -324,26 +316,8 @@ impl Condition { *dst = self.select(src, *dst); } - /// Conditionally negate the value. - /// - /// negate(-1) gives -3 - /// - /// `value` is `-1` (i.e., all bits are `1`, `...1111`) - /// - /// Condition `self.0` is 1 (`...0001`) (assuming `TRUE`) - /// - /// XOR operation was executed as `value ^ self.0` - /// - /// Then `...1111 XOR ...0001 = ...1110` (i.e., `-2`) - /// - /// Subtraction operation is `wrapping_sub(self.0)` - /// - /// Then `-2 - 1 = -3` - /// - /// As a result, `1`, which is the negation of `-1`, should be returned, but `-3` is output. - /// - /// Therefore, if the [`Self::TRUE`] constant value of the [`Condition`] implementation is changed to `-1`, - /// the test also runs normally. + /// Conditionally negate the value: returns `-value` (wrapping, so `MIN` stays `MIN`) if the + /// condition is true, otherwise `value`. pub fn negate(self, value: i32) -> i32 { let mask = self.barrier().0; (value ^ mask).wrapping_sub(mask) @@ -413,10 +387,15 @@ impl Condition { pub const fn from_msb(value: u64) -> Self { Self(0u64.wrapping_sub(value >> (u64::BITS - 1))) } - /// TRUE iff bit `bit` of `value` is set. The bit index must be public data - /// (the shift amount is timing-visible on some targets). + /// TRUE iff bit `bit` of `value` is set; FALSE for `bit >= u64::BITS`. The bit index must be + /// public data (the shift amount is timing-visible on some targets). pub const fn is_bit_set(value: u64, bit: u32) -> Self { - Self::from_lsb(value >> bit) + // Branches on the public index only: a plain `>>` panics in debug and masks the shift + // amount in release for an out-of-range index. + match value.checked_shr(bit) { + Some(shifted) => Self::from_lsb(shifted), + None => Self::FALSE, + } } /// TRUE iff `value != 0`. /// @@ -493,10 +472,15 @@ impl Condition { pub const fn from_msb(value: u32) -> Self { Self(0u32.wrapping_sub(value >> (u32::BITS - 1))) } - /// TRUE iff bit `bit` of `value` is set. The bit index must be public data - /// (the shift amount is timing-visible on some targets). + /// TRUE iff bit `bit` of `value` is set; FALSE for `bit >= u32::BITS`. The bit index must be + /// public data (the shift amount is timing-visible on some targets). pub const fn is_bit_set(value: u32, bit: u32) -> Self { - Self::from_lsb(value >> bit) + // Branches on the public index only: a plain `>>` panics in debug and masks the shift + // amount in release for an out-of-range index. + match value.checked_shr(bit) { + Some(shifted) => Self::from_lsb(shifted), + None => Self::FALSE, + } } /// TRUE iff `value != 0`. /// @@ -678,7 +662,8 @@ pub fn ct_eq_bytes(a: &[u8], b: &[u8]) -> bool { ct_eq_bytes_mask(a, b).to_bool() } -/// Constant-time check that every byte of `a` is zero. +/// Constant-time check that every byte of `a` is zero, as a `bool`, for callers that go on to +/// branch on the result at a public decision point. /// /// The runtime depends on the length of `a`, which is treated as public, but not on its contents /// or on the position of the first non-zero byte. Same construction as [`ct_eq_bytes_mask`] with diff --git a/crypto/utils/src/secret.rs b/crypto/utils/src/secret.rs index e86cec849..5480aca76 100644 --- a/crypto/utils/src/secret.rs +++ b/crypto/utils/src/secret.rs @@ -1,5 +1,5 @@ //! A transparent wrapper type, [`Secret`], which is a wrapper that holds a secret value and -//! guarantees it is securely zeroized on drop, and protected from accidintal logging by implementing +//! guarantees it is securely zeroized on drop, and protected from accidental logging by implementing //! redacting `fmt::Debug` and `fmt::Display`. //! //! # Why write_volatile @@ -89,6 +89,44 @@ impl ZeroizablePrimitive for [T; N] { const ZEROED: Self = [T::ZEROED; N]; } +/// A type with no padding bytes, so that every byte of every value is initialized. +/// +/// [`Secret`]'s equality compares the two values' `size_of::()` bytes, so it is defined only +/// for a `T` that implements this trait. It is implemented for the primitive integers, `bool`, +/// `char` and arrays of them. A type with padding can be held in a `Secret` but not compared: +/// +/// ```compile_fail +/// use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; +/// +/// #[derive(Clone, Copy)] +/// struct Padded { a: u8, b: u32 } +/// +/// impl ZeroizablePrimitive for Padded { +/// const ZEROED: Self = Padded { a: 0, b: 0 }; +/// } +/// +/// let x = Secret::::new(); +/// let y = Secret::::new(); +/// let _ = x == y; +/// ``` +/// +/// # Safety +/// +/// An implementor must guarantee that `Self` has no padding bytes. +pub unsafe trait NoPadding {} + +macro_rules! impl_no_padding { + ($($t:ty),+ $(,)?) => {$( + // SAFETY: a primitive scalar has no padding. + unsafe impl NoPadding for $t {} + )+}; +} + +impl_no_padding!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, bool, char); + +// SAFETY: an array has no padding between or around its elements, and `T` has none of its own. +unsafe impl NoPadding for [T; N] {} + /// A wrapper that holds a secret value of any primitive or fixed-size array type /// and guarantees it is securely zeroized on drop, and is /// protected from accidental logging by implementing redacting `fmt::Debug` and `fmt::Display`. @@ -159,7 +197,8 @@ impl ZeroizablePrimitive for [T; N] { /// /// Since [`ZeroizablePrimitive`] is a public trait, you can implement it on your own types and then /// trivially be able to wrap them in a `Secret`. The only requirement is that there is a -/// well-defined "ZEROED" value for the type. +/// well-defined "ZEROED" value for the type. Comparing two such secrets with `==` additionally +/// needs the `unsafe` trait [`NoPadding`]. /// /// Toy Example: /// @@ -204,7 +243,7 @@ impl ZeroizablePrimitive for [T; N] { /// ``` /// /// This will also work for custom types; let's say the `User` struct from the previous example had -/// impl'd Display and Debug; mhen you wrap them in `Secret` then `Secret`'s Display and Debug +/// impl'd Display and Debug; when you wrap them in `Secret` then `Secret`'s Display and Debug /// are invoked instead of `User`'s and you get the `""` output. /// /// # Memory Usage @@ -265,17 +304,11 @@ impl Secret { pub fn zeroize(&mut self) { // SAFETY: `&mut self.0` is a valid, properly aligned, mutable reference to an initialized // `T`, which is exactly the contract `write_volatile` requires. - // `T::ZEROED` is defined above for each supported primitive and primitive-array as the - // is the all-zero value of `T`, which is a valid and correctly-sized bit pattern - // for the primitive scalar/array being zeroized. + // `T::ZEROED` is a valid value of `T`, so it is a valid value to write. //`write_volatile` (rather than a plain store) is what forbids the compiler from eliding // this scrub as a dead write as per its contract: // https://doc.rust-lang.org/std/ptr/fn.write_volatile.html - // Just to make sure -- this should trigger on any unit tests for any instantiation of - // Secret that causes this assumption to be violated. - debug_assert_eq!(size_of::(), size_of_val(&T::ZEROED)); - unsafe { ptr::write_volatile(&mut self.0, T::ZEROED); } @@ -337,17 +370,18 @@ impl fmt::Display for Secret { /// Both operands are the same type `T`, so both views are exactly `size_of::()` bytes and the /// comparison never short-circuits: it always inspects every byte, avoiding a timing side channel /// that would otherwise leak how many leading bytes of two secrets match. -impl PartialEq for Secret { +impl PartialEq for Secret { fn eq(&self, other: &Self) -> bool { let len = size_of::(); // SAFETY: `self.0` / `other.0` are live, initialized `T` values, so the `len` bytes starting // at each address lie within that single object. `u8` has alignment 1, so every byte address // is well aligned, and the slices are read-only and used only within this call. `T: Copy` // (via `ZeroizablePrimitive`) means there is no interior mutability or drop glue to worry - // about. + // about. `T: NoPadding` means `T` has no padding, so all `len` bytes are initialized, as + // `from_raw_parts` requires. let a = unsafe { core::slice::from_raw_parts((&self.0 as *const T).cast::(), len) }; let b = unsafe { core::slice::from_raw_parts((&other.0 as *const T).cast::(), len) }; ct::ct_eq_bytes(a, b) } } -impl Eq for Secret {} +impl Eq for Secret {} diff --git a/crypto/utils/src/suspendable_state.rs b/crypto/utils/src/suspendable_state.rs index 9e676bd24..f6cbbf10b 100644 --- a/crypto/utils/src/suspendable_state.rs +++ b/crypto/utils/src/suspendable_state.rs @@ -98,13 +98,20 @@ impl From for [u8; 3] { /// Parse a decimal ASCII string (a Cargo version component) into a u8 at compile time. const fn parse_version_component(s: &str) -> u8 { let bytes = s.as_bytes(); + assert!(!bytes.is_empty(), "version component must not be empty"); let mut result: u8 = 0; let mut i = 0; while i < bytes.len() { let d = bytes[i]; assert!(d >= b'0' && d <= b'9', "version component must be numeric"); - // A component > 255 overflows u8 and fails the build (SemVer fields are u8 by design). - result = result * 10 + (d - b'0'); + // SemVer fields are u8 by design, so a component > 255 must fail the build. Plain `*` and + // `+` would wrap instead when overflow checks are off (const evaluation follows + // -C overflow-checks), so the overflow is checked explicitly. + let Some(shifted) = result.checked_mul(10) else { panic!("version component exceeds 255") }; + let Some(next) = shifted.checked_add(d - b'0') else { + panic!("version component exceeds 255") + }; + result = next; i += 1; } result @@ -130,34 +137,46 @@ pub const LIB_VERSION_LEN: usize = 3; /// Puts the library version into the first three bytes of the state array. /// -/// Hands back a slice to the same array, starting after the version tag. +/// Hands back a slice to the same array, starting after the version tag. `SERIALIZED_LEN` must be +/// at least [`LIB_VERSION_LEN`], checked at compile time: +/// +/// ```compile_fail +/// use bouncycastle_utils::suspendable_state::add_lib_ver; +/// +/// let mut state = [0u8; 2]; +/// add_lib_ver(&mut state); +/// ``` pub fn add_lib_ver(state: &mut [u8; SERIALIZED_LEN]) -> &mut [u8] { + const { + assert!( + SERIALIZED_LEN >= LIB_VERSION_LEN, + "the state must have room for the version header" + ) + }; state[..LIB_VERSION_LEN].copy_from_slice(&<[u8; 3]>::from(LIB_VERSION)); &mut state[LIB_VERSION_LEN..] } -/// A helper for deserializing an object's state -/// -/// The state_out array must have length at least SERIALIZED_LEN - 3. +/// A helper for deserializing an object's state: checks the version tag in its first three bytes. /// -/// Returns the number of bytes written to state_out, or a [`SuspendableError::IncompatibleVersion`] if -/// the version of the serialized state is earlier than the specified `not_before` version, or -/// is a future MAJOR or MINOR version (but future PATCH versions are ok). +/// Hands back a slice to the same array, starting after the version tag, or a +/// [`SuspendableError::IncompatibleVersion`] if the version of the serialized state is earlier than +/// the specified `not_before` version, or is a future MAJOR or MINOR version (but future PATCH +/// versions are ok). `SERIALIZED_LEN` must be at least [`LIB_VERSION_LEN`], checked at compile time. /// /// Note that for testability, this will always reject if the serialized state contains a version tag /// of `[0,0,0]`. -/// -/// Hands back a slice to the same array, starting after the version tag. pub fn check_lib_ver( state: &[u8; SERIALIZED_LEN], not_before: Option<[u8; 3]>, ) -> Result<&[u8], SuspendableError> { - // the .unwrap is infallible after the guard check - if state.len() < LIB_VERSION_LEN { - return Err(SuspendableError::InvalidData); - } - let ver_bytes: [u8; 3] = state[..LIB_VERSION_LEN].try_into().unwrap(); - let ver = SemVer::from(ver_bytes); + const { + assert!( + SERIALIZED_LEN >= LIB_VERSION_LEN, + "the state must have room for the version header" + ) + }; + let ver = SemVer::from([state[0], state[1], state[2]]); let not_before = SemVer::from(not_before.unwrap_or([0, 0, 0])); @@ -319,6 +338,8 @@ impl<'a> CursorMut<'a> { } /// A `usize` field read back from its `u64` encoding, refused if it is above `max`. +/// +/// `max` must be a real bound on the field: `usize::MAX` only narrows the width. pub fn bounded_usize(v: u64, max: usize) -> Result { if v > max as u64 { Err(SuspendableError::InvalidData) } else { Ok(v as usize) } } @@ -342,4 +363,31 @@ mod tests { assert_eq!(cmp([1, 1, 2], [1, 1, 1]), Ordering::Greater); assert_eq!(cmp([1, 1, 1], [1, 1, 1]), Ordering::Equal); } + + #[test] + fn test_parse_version_component() { + assert_eq!(parse_version_component("0"), 0); + assert_eq!(parse_version_component("3"), 3); + assert_eq!(parse_version_component("25"), 25); + assert_eq!(parse_version_component("255"), 255); + } + + #[test] + #[should_panic(expected = "version component exceeds 255")] + fn test_parse_version_component_256() { + parse_version_component("256"); + } + + #[test] + #[should_panic(expected = "version component exceeds 255")] + fn test_parse_version_component_300() { + // 30 * 10 overflows a u8 (in the multiply, not the add). + parse_version_component("300"); + } + + #[test] + #[should_panic(expected = "version component must not be empty")] + fn test_parse_version_component_empty() { + parse_version_component(""); + } } diff --git a/crypto/utils/tests/ct_tests.rs b/crypto/utils/tests/ct_tests.rs index 81a76d335..fe85e1385 100644 --- a/crypto/utils/tests/ct_tests.rs +++ b/crypto/utils/tests/ct_tests.rs @@ -183,6 +183,12 @@ mod unsigned_u64_tests { Condition::::from_msb(v).to_bool() ); } + // an index past the width gives FALSE, never a masked shift or a panic + for v in BOUNDARY { + for bit in [u64::BITS, u64::BITS + 1, u32::MAX] { + assert_canonical(Condition::::is_bit_set(v, bit), false); + } + } } #[test] @@ -332,6 +338,12 @@ mod unsigned_u32_tests { Condition::::from_msb(v).to_bool() ); } + // an index past the width gives FALSE, never a masked shift or a panic + for v in BOUNDARY { + for bit in [u32::BITS, u32::BITS + 1, u32::MAX] { + assert_canonical(Condition::::is_bit_set(v, bit), false); + } + } } #[test] @@ -464,6 +476,12 @@ mod signed_i64_tests { Condition::::is_negative(v).to_bool() ); } + // an index past the width gives FALSE, never a masked shift or a panic + for v in BOUNDARY { + for bit in [i64::BITS, i64::BITS + 1, u32::MAX] { + assert_canonical(Condition::::is_bit_set(v, bit), false); + } + } } #[test] @@ -665,6 +683,12 @@ mod signed_i32_tests { Condition::::is_negative(v).to_bool() ); } + // an index past the width gives FALSE, never a masked shift or a panic + for v in BOUNDARY { + for bit in [i32::BITS, i32::BITS + 1, u32::MAX] { + assert_canonical(Condition::::is_bit_set(v, bit), false); + } + } } #[test] From 5af9c4f24c168a1deab1dd85e5ea8be2e659fcb3 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 16:50:35 +1100 Subject: [PATCH 03/57] ascon: AsconHash256::do_final_partial_bits and _out follow the Hash contract as XOF128 and CXOF128 do, num_bits 0 is the byte-aligned final and above 7 is InvalidLength, where both refused every value with InvalidInput (the _out form now zeroes its output first); the decryptor's from_suspended refuses held-back bytes outside the Data phase, which no live decryptor holds and which a crafted Init-phase state could release as plaintext; Security Considerations state the SP 800-232 R6/R7 2^54-byte per-key data limit and the R5 2^96 decryption-failure limit, both the caller's to track, that a cloned encryptor reuses the keystream, and correct the partial-byte paragraph; tests for squeezes smaller than the buffered remainder and for AsconCXof128::hash_out's count; cargo mutants: 704 mutants, 598 caught, 98 unviable, 8 missed, the equivalents the review lists (the four test-gap survivors are now caught) Assisted-by: Claude:claude-opus-5-5 --- crypto/ascon/src/ascon_aead128.rs | 8 ++++-- crypto/ascon/src/ascon_hash256.rs | 39 ++++++++++++++++++++++----- crypto/ascon/src/lib.rs | 14 ++++++++-- crypto/ascon/tests/aead128_tests.rs | 13 +++++++++ crypto/ascon/tests/cxof128_tests.rs | 24 +++++++++++++++++ crypto/ascon/tests/hash256_tests.rs | 42 ++++++++++++++++++++++++++--- crypto/ascon/tests/xof128_tests.rs | 14 ++++++++++ 7 files changed, 140 insertions(+), 14 deletions(-) diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs index 69e019a0e..ddf475b1d 100644 --- a/crypto/ascon/src/ascon_aead128.rs +++ b/crypto/ascon/src/ascon_aead128.rs @@ -769,8 +769,8 @@ impl<'k> SuspendableKeyed<'k, SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN> out } - /// A state suspended by the encrypting side, or a held-back count above `TAG_LEN`, is - /// [`SuspendableError::InvalidData`]. + /// A state suspended by the encrypting side, a held-back count above `TAG_LEN`, or held-back + /// bytes before any ciphertext was seen, is [`SuspendableError::InvalidData`]. fn from_suspended( serialized_state: [u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN], key: &'k Self::Key, @@ -784,6 +784,10 @@ impl<'k> SuspendableKeyed<'k, SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN> if dec.held_len > TAG_LEN { return Err(SuspendableError::InvalidData); } + // Bytes are held back only by `decrypt_bytes`' callers, which move the phase to `Data`. + if dec.held_len != 0 && dec.phase != Phase::Data { + return Err(SuspendableError::InvalidData); + } Ok(dec) } } diff --git a/crypto/ascon/src/ascon_hash256.rs b/crypto/ascon/src/ascon_hash256.rs index 5717f774a..8ede9154d 100644 --- a/crypto/ascon/src/ascon_hash256.rs +++ b/crypto/ascon/src/ascon_hash256.rs @@ -86,19 +86,44 @@ impl Hash for AsconHash256 { fn do_final_partial_bits( self, - _partial_byte: u8, - _num_partial_bits: usize, + partial_byte: u8, + num_bits: usize, ) -> Result, HashError> { - Err(HashError::InvalidInput("Ascon-Hash256 does not support partial byte input")) + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-Hash256 does not support partial byte input", + )); + } + + // A zero-bit partial byte means the message is byte-aligned. + let _ = partial_byte; + Ok(self.do_final()) } fn do_final_partial_bits_out( self, - _partial_byte: u8, - _num_partial_bits: usize, - _output: &mut [u8], + partial_byte: u8, + num_bits: usize, + output: &mut [u8], ) -> Result { - Err(HashError::InvalidInput("Ascon-Hash256 does not support partial byte input")) + output.fill(0); + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-Hash256 does not support partial byte input", + )); + } + + // A zero-bit partial byte means the message is byte-aligned. + let _ = partial_byte; + Ok(self.do_final_out(output)) } } diff --git a/crypto/ascon/src/lib.rs b/crypto/ascon/src/lib.rs index 4d936ff58..36e5fafdd 100644 --- a/crypto/ascon/src/lib.rs +++ b/crypto/ascon/src/lib.rs @@ -183,12 +183,22 @@ //! //! - **Nonce uniqueness (SP 800-232 R3):** a (key, nonce) pair must never be reused for two //! different Ascon-AEAD128 encryptions. Nonce reuse breaks confidentiality. +//! - **Data limit (SP 800-232 R6, R7):** the total data processed under one key, encryption and +//! decryption together and including the nonces, must not exceed 2^54 bytes; the key must be +//! replaced before then. The limit spans every message under the key, so the caller must track +//! it; this crate does not count. +//! - **Decryption failures (SP 800-232 R5):** with the full 128-bit tag, no more than 2^96 +//! decryption failures may be accepted under one key. //! - **Tag length:** this crate always produces and verifies the full 128-bit tag. Truncated tags //! (SP 800-232 §4.2.1) are not exposed. +//! - **Cloning a cipher:** a clone of an [`Ascon_AEAD128`] continues with the same +//! key, nonce and state, so encrypting different data on the two copies reuses the keystream, +//! exactly as nonce reuse does. //! - **No partial-byte input:** Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 are byte-oriented; //! their `do_final_partial_bits`/`do_final_partial_bits_out` (and the equivalent XOF methods) -//! always return `HashError::InvalidInput`, including when reached through `HashFactory`. A -//! caller that needs a partial-byte final block should reach for SHA-3, which supports one. +//! return `HashError::InvalidInput` for 1 to 7 bits, including when reached through +//! `HashFactory`; 0 bits is an ordinary final. A caller that needs a partial-byte final block +//! should reach for SHA-3, which supports one. //! - **Decryption tag check failure:** a ciphertext decryption whose finalization returns //! `Err(SymmetricCipherError::AEADTagCheckFailed)` must be treated as tampered, and the entire //! plaintext rejected. The key's one-shots diff --git a/crypto/ascon/tests/aead128_tests.rs b/crypto/ascon/tests/aead128_tests.rs index 68bd3a461..3bae7855b 100644 --- a/crypto/ascon/tests/aead128_tests.rs +++ b/crypto/ascon/tests/aead128_tests.rs @@ -698,6 +698,19 @@ fn aead128_suspendable_keyed_state() { Err(SuspendableError::InvalidData) )); + // Bytes are held back only once ciphertext has been seen: a decryptor still taking AAD + // resumes with nothing held, and not with a held-back count. + let mut d_aad = key.create_decryptor(&NONCE); + d_aad.do_update_aad(ad).unwrap(); + let aad_state = d_aad.suspend(); + assert!(Ascon_AEAD128::::from_suspended(aad_state, &key).is_ok()); + let mut held_in_aad = aad_state; + held_in_aad[SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN - 1] = 5; + assert!(matches!( + Ascon_AEAD128::::from_suspended(held_in_aad, &key), + Err(SuspendableError::InvalidData) + )); + // A state suspended while encrypting must not resume as a decryptor, nor the reverse: the // call-state byte carries the direction and the resuming type checks it. let mut enc_as_dec = [0u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN]; diff --git a/crypto/ascon/tests/cxof128_tests.rs b/crypto/ascon/tests/cxof128_tests.rs index b8a4d3949..d7dee72c2 100644 --- a/crypto/ascon/tests/cxof128_tests.rs +++ b/crypto/ascon/tests/cxof128_tests.rs @@ -121,6 +121,20 @@ fn cxof128_prefix_property_and_streaming() { assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); + // Reads smaller than what is left of the last squeezed block are served from it. + for n in [1usize, 2, 3, 5, 7] { + let mut x = AsconCXof128::with_customization(z).unwrap(); + x.do_update(&msg); + let mut squeezer = x.into_squeezer(); + let mut piecewise = Vec::new(); + while piecewise.len() < full.len() { + let mut part = vec![0u8; n.min(full.len() - piecewise.len())]; + assert_eq!(squeezer.do_output_out(&mut part), part.len()); + piecewise.extend_from_slice(&part); + } + assert_eq!(piecewise, full, "squeeze in {n}-byte pieces mismatch"); + } + // Absorbing the message in chunks must equal absorbing it in one call. for chunk in [1usize, 8, 9, 64] { let mut xc = AsconCXof128::with_customization(z).unwrap(); @@ -144,6 +158,16 @@ fn cxof128_hash_view_metadata() { assert_eq!(x.block_bitlen(), 64); assert_eq!(x.output_len(), 32); assert_eq!(x.hash(b"").len(), 32); + + // hash_out writes the 32-byte Hash view and returns how many bytes it wrote. + let expected = AsconCXof128::new().hash(b"abc"); + let mut out = [0xEEu8; 40]; + assert_eq!(AsconCXof128::new().hash_out(b"abc", &mut out), 32); + assert_eq!(&out[..32], &expected[..]); + assert_eq!(&out[32..], &[0u8; 8]); + let mut short = [0u8; 10]; + assert_eq!(AsconCXof128::new().hash_out(b"abc", &mut short), 10); + assert_eq!(&short[..], &expected[..10]); } #[test] diff --git a/crypto/ascon/tests/hash256_tests.rs b/crypto/ascon/tests/hash256_tests.rs index 0fc25f4ad..19e86e9a9 100644 --- a/crypto/ascon/tests/hash256_tests.rs +++ b/crypto/ascon/tests/hash256_tests.rs @@ -4,6 +4,7 @@ //! streaming-equivalence, one-shot/trait-API, metadata, and unsupported-partial-op tests. use bouncycastle_ascon::AsconHash256; +use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Hash, HashAlgParams, HashOneShot}; use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_hex as hex; @@ -116,9 +117,44 @@ fn hash256_hash_out_zeroizes_past_output_len() { #[test] fn hash256_unsupported_partial_ops_return_err() { - assert!(AsconHash256::new().do_final_partial_bits(0, 3).is_err()); - let mut o = [0u8; 32]; - assert!(AsconHash256::new().do_final_partial_bits_out(0, 3, &mut o).is_err()); + // num_bits == 0 is byte-aligned input and must behave like ordinary finalization. + let data = b"abc"; + let expected = AsconHash256::hash(data); + let mut h = AsconHash256::new(); + h.do_update(data); + assert_eq!(h.do_final_partial_bits(0xFF, 0).unwrap(), expected); + let mut h = AsconHash256::new(); + h.do_update(data); + let mut o = [0xEEu8; 40]; + assert_eq!(h.do_final_partial_bits_out(0xFF, 0, &mut o).unwrap(), 32); + assert_eq!(&o[..32], &expected[..]); + assert_eq!(&o[32..], &[0u8; 8]); + + for num_bits in [1usize, 3, 7] { + assert!(matches!( + AsconHash256::new().do_final_partial_bits(0xA0, num_bits), + Err(HashError::InvalidInput(_)) + )); + let mut o = [0xEEu8; 32]; + assert!(matches!( + AsconHash256::new().do_final_partial_bits_out(0xA0, num_bits, &mut o), + Err(HashError::InvalidInput(_)) + )); + assert_eq!(o, [0u8; 32]); + } + + for num_bits in [8usize, 9] { + assert!(matches!( + AsconHash256::new().do_final_partial_bits(0xFF, num_bits), + Err(HashError::InvalidLength(_)) + )); + let mut o = [0xEEu8; 32]; + assert!(matches!( + AsconHash256::new().do_final_partial_bits_out(0xFF, num_bits, &mut o), + Err(HashError::InvalidLength(_)) + )); + assert_eq!(o, [0u8; 32]); + } } #[test] diff --git a/crypto/ascon/tests/xof128_tests.rs b/crypto/ascon/tests/xof128_tests.rs index 85d915e55..aea51f4c7 100644 --- a/crypto/ascon/tests/xof128_tests.rs +++ b/crypto/ascon/tests/xof128_tests.rs @@ -88,6 +88,20 @@ fn xof128_prefix_property_and_streaming() { assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); + // Reads smaller than what is left of the last squeezed block are served from it. + for n in [1usize, 2, 3, 5, 7] { + let mut x = AsconXof128::new(); + x.do_update(&msg); + let mut squeezer = x.into_squeezer(); + let mut piecewise = Vec::new(); + while piecewise.len() < full.len() { + let mut part = vec![0u8; n.min(full.len() - piecewise.len())]; + assert_eq!(squeezer.do_output_out(&mut part), part.len()); + piecewise.extend_from_slice(&part); + } + assert_eq!(piecewise, full, "squeeze in {n}-byte pieces mismatch"); + } + // Absorbing in chunks equals one-shot input. for chunk in [1usize, 8, 9, 64] { let mut xc = AsconXof128::new(); From f131301c7c58554da8e250860706a3c716afd371 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 18:04:03 +1100 Subject: [PATCH 04/57] sm3, factory, cli: HKDF-SM3 (the HKDF of the RFC 8998 TLS 1.3 SM suites): an sm3::hkdf module with the HKDF_SM3 alias, SUSPENDED_HKDF_SM3_STATE_LEN and HKDF_SM3_NAME (moved there from sm3::hmac, as sha2 keeps its HKDF names), a KDFFactory arm, an hkdf-sm3 CLI subcommand and a criterion bench; with no published HKDF-SM3 vectors, the known answers are RFC 5869 A.1-A.3's inputs through OpenSSL 3.0.13, whose SHA-256 output for the same inputs reproduces the RFC, checked for the PRK, the OKM, the streaming extract and the CLI, plus the KDF and suspendable-state frameworks; cargo mutants on kdf_factory.rs: 19 mutants, 6 caught, 11 unviable, 2 missed (default_256_bit as Default::default() is equivalent, default_128_bit's was untested before), the new HKDF-SM3 _out tests catching the four derive_key_out and derive_key_from_multiple_out survivors; sm3/src/hkdf.rs has nothing to mutate Assisted-by: Claude:claude-opus-5-5 --- cli/src/hkdf_cmd.rs | 8 ++ cli/src/main.rs | 56 +++++++++ cli/tests/test_hkdf.sh | 23 +++- crypto/factory/src/kdf_factory.rs | 11 ++ crypto/factory/tests/kdf_factory_tests.rs | 39 +++++++ crypto/sm3/Cargo.toml | 5 + crypto/sm3/benches/hkdf_sm3_benches.rs | 75 ++++++++++++ crypto/sm3/src/hkdf.rs | 51 ++++++++ crypto/sm3/src/hmac.rs | 3 +- crypto/sm3/src/lib.rs | 4 + crypto/sm3/tests/hkdf_sm3_tests.rs | 135 ++++++++++++++++++++++ 11 files changed, 407 insertions(+), 3 deletions(-) create mode 100644 crypto/sm3/benches/hkdf_sm3_benches.rs create mode 100644 crypto/sm3/src/hkdf.rs create mode 100644 crypto/sm3/tests/hkdf_sm3_tests.rs diff --git a/cli/src/hkdf_cmd.rs b/cli/src/hkdf_cmd.rs index 3fc0e1723..55564d53f 100644 --- a/cli/src/hkdf_cmd.rs +++ b/cli/src/hkdf_cmd.rs @@ -6,6 +6,7 @@ use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle::hex; use bouncycastle::hkdf; use bouncycastle::sha2::hkdf::{HKDF_SHA256, HKDF_SHA384, HKDF_SHA512}; +use bouncycastle::sm3::hkdf::HKDF_SM3; /// The salt is held in a fixed-size key buffer; the library itself puts no bound on it. const MAX_SALT_LEN: usize = 1024; @@ -85,6 +86,13 @@ pub(crate) fn hkdf_cmd( let prk = h.do_extract_final().unwrap(); HKDF_SHA512::expand_out(&prk, &info, len, &mut out_key) } + "HKDF-SM3" => { + let mut h = HKDF_SM3::new(); + h.do_extract_init(&salt_key).unwrap(); + h.do_extract_update_bytes(&ikm_bytes).unwrap(); + let prk = h.do_extract_final().unwrap(); + HKDF_SM3::expand_out(&prk, &info, len, &mut out_key) + } _ => { panic!("{} is not a supported HKDF variant.", hkdfname); } diff --git a/cli/src/main.rs b/cli/src/main.rs index c443f6fd3..9ca59b9b0 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1218,6 +1218,49 @@ enum Subcommands { x: bool, }, + /// Perform HKDF-SM3 (RFC 5869 extract-then-expand) of the provided input keying material: + /// OKM = HKDF-Expand(HKDF-Extract(salt, ikm), info, len) + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + HKDF_SM3 { + /// The salt value in hex. + /// The `salt_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + salt: Option, + + /// A file containing the salt value in binary. + /// If both salt and salt_file options are provided, the file will be used. + #[arg(short, long)] + salt_file: Option, + + /// An Input Keying Material in hex. + /// The `ikm_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + ikm: Option, + + /// A file containing the input keying material in binary. + /// If both ikm and ikm_file options are provided, the file will be used. + #[arg(short, long)] + ikm_file: Option, + + /// The HKDF `info` input (RFC 5869 Section 2.3) in hex. Optional; defaults to empty. + #[arg(long)] + additional_input: Option, + + /// A file containing the HKDF `info` input in binary. Optional; defaults to empty. + /// If both additional_input and additional_input_file options are provided, the file will be used. + #[arg(short, long)] + additional_input_file: Option, + + /// Length of output to produce, in bytes: at most 255 times the hash's output length. + #[arg(short, long)] + len: usize, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// Perform HMAC-SHA256 of the content provided on stdin. /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get @@ -1949,6 +1992,19 @@ fn run() { "HKDF-SHA512", salt, salt_file, ikm, ikm_file, additional_input, additional_input_file, *len, *x, ), + Some(Subcommands::HKDF_SM3 { + salt, + salt_file, + ikm, + ikm_file, + additional_input, + additional_input_file, + len, + x, + }) => hkdf_cmd::hkdf_cmd( + "HKDF-SM3", salt, salt_file, ikm, ikm_file, additional_input, additional_input_file, + *len, *x, + ), Some(Subcommands::HMAC_SHA256 { key, key_file, verify, x }) => { mac_cmd::mac_cmd(HMACVariant::SHA256, key, key_file, verify, *x) } diff --git a/cli/tests/test_hkdf.sh b/cli/tests/test_hkdf.sh index 740879144..4434c34d0 100755 --- a/cli/tests/test_hkdf.sh +++ b/cli/tests/test_hkdf.sh @@ -4,7 +4,8 @@ # the same output as the same values passed as hex. # # The known answers: HKDF-SHA256 is RFC 5869 Appendix A.1; HKDF-SHA384 and HKDF-SHA512 are -# tcId 11 of Wycheproof's testvectors_v1/hkdf_sha384_test.json and hkdf_sha512_test.json. +# tcId 11 of Wycheproof's testvectors_v1/hkdf_sha384_test.json and hkdf_sha512_test.json; +# HKDF-SM3, which has no published vectors, is RFC 5869 A.1's inputs through OpenSSL 3.0.13. source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" @@ -58,6 +59,18 @@ test_hkdf_sha512_matches_wycheproof_tc11() { "Wycheproof hkdf_sha512 tcId 11 OKM" } +test_hkdf_sm3_matches_openssl_on_rfc5869_a1_inputs() { + local got + got=$(hex_out "$BC_RUST" hkdf-sm3 \ + --salt 000102030405060708090a0b0c \ + --ikm 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b \ + --additional-input f0f1f2f3f4f5f6f7f8f9 \ + --len 42 -x) + assert_eq "$got" \ + "c69fe91b7aaee2dd5718d72dcaee0cce93f1b8e41f792da51261b6a517e68b36ed2c595572b01dfa359b" \ + "HKDF-SM3 OKM" +} + # ---- file inputs -------------------------------------------------------------------------- # `hkdf_files_match_hex SUBCOMMAND` derives from random inputs passed once as hex and once as @@ -86,6 +99,10 @@ test_hkdf_sha512_files_match_hex() { hkdf_files_match_hex hkdf-sha512 } +test_hkdf_sm3_files_match_hex() { + hkdf_files_match_hex hkdf-sm3 +} + # When a value is given both as hex and as a file, the file is used, as the help text says. test_hkdf_sha256_file_wins_over_hex() { local from_files with_both @@ -122,4 +139,8 @@ test_hkdf_sha512_one_octet_past_a_block() { hkdf_short_output_is_a_prefix hkdf-sha512 65 } +test_hkdf_sm3_one_octet_past_a_block() { + hkdf_short_output_is_a_prefix hkdf-sm3 33 +} + run_all diff --git a/crypto/factory/src/kdf_factory.rs b/crypto/factory/src/kdf_factory.rs index ff0866ee9..48076aa4d 100644 --- a/crypto/factory/src/kdf_factory.rs +++ b/crypto/factory/src/kdf_factory.rs @@ -60,6 +60,7 @@ use bouncycastle_sha3 as sha3; use bouncycastle_sha3::{ SHA3_224_NAME, SHA3_256_NAME, SHA3_384_NAME, SHA3_512_NAME, SHAKE128_NAME, SHAKE256_NAME, }; +use bouncycastle_sm3::hkdf::{HKDF_SM3, HKDF_SM3_NAME}; /// Wrapper object for all algorithms that impl [`KDF`]. #[non_exhaustive] @@ -73,6 +74,9 @@ pub enum KDFFactory { /// #[allow(non_camel_case_types)] HKDF_SHA512(HKDF_SHA512), + /// HKDF over SM3. + #[allow(non_camel_case_types)] + HKDF_SM3(HKDF_SM3), /// SHA3_224(sha3::SHA3_224), /// @@ -110,6 +114,7 @@ impl AlgorithmFactory for KDFFactory { HKDF_SHA256_NAME => Ok(Self::HKDF_SHA256(HKDF_SHA256::new())), HKDF_SHA384_NAME => Ok(Self::HKDF_SHA384(HKDF_SHA384::new())), HKDF_SHA512_NAME => Ok(Self::HKDF_SHA512(HKDF_SHA512::new())), + HKDF_SM3_NAME => Ok(Self::HKDF_SM3(HKDF_SM3::new())), SHA3_224_NAME => Ok(Self::SHA3_224(sha3::SHA3_224::new())), SHA3_256_NAME => Ok(Self::SHA3_256(sha3::SHA3_256::new())), SHA3_384_NAME => Ok(Self::SHA3_384(sha3::SHA3_384::new())), @@ -134,6 +139,7 @@ impl KDF for KDFFactory { Self::HKDF_SHA256(h) => h.derive_key(key, additional_input), Self::HKDF_SHA384(h) => h.derive_key(key, additional_input), Self::HKDF_SHA512(h) => h.derive_key(key, additional_input), + Self::HKDF_SM3(h) => h.derive_key(key, additional_input), Self::SHA3_224(h) => h.derive_key(key, additional_input), Self::SHA3_256(h) => h.derive_key(key, additional_input), Self::SHA3_384(h) => h.derive_key(key, additional_input), @@ -153,6 +159,7 @@ impl KDF for KDFFactory { Self::HKDF_SHA256(h) => h.derive_key_out(key, additional_input, output_key), Self::HKDF_SHA384(h) => h.derive_key_out(key, additional_input, output_key), Self::HKDF_SHA512(h) => h.derive_key_out(key, additional_input, output_key), + Self::HKDF_SM3(h) => h.derive_key_out(key, additional_input, output_key), Self::SHA3_224(h) => h.derive_key_out(key, additional_input, output_key), Self::SHA3_256(h) => h.derive_key_out(key, additional_input, output_key), Self::SHA3_384(h) => h.derive_key_out(key, additional_input, output_key), @@ -171,6 +178,7 @@ impl KDF for KDFFactory { Self::HKDF_SHA256(h) => h.derive_key_from_multiple(keys, additional_input), Self::HKDF_SHA384(h) => h.derive_key_from_multiple(keys, additional_input), Self::HKDF_SHA512(h) => h.derive_key_from_multiple(keys, additional_input), + Self::HKDF_SM3(h) => h.derive_key_from_multiple(keys, additional_input), Self::SHA3_224(h) => h.derive_key_from_multiple(keys, additional_input), Self::SHA3_256(h) => h.derive_key_from_multiple(keys, additional_input), Self::SHA3_384(h) => h.derive_key_from_multiple(keys, additional_input), @@ -196,6 +204,7 @@ impl KDF for KDFFactory { Self::HKDF_SHA512(h) => { h.derive_key_from_multiple_out(keys, additional_input, output_key) } + Self::HKDF_SM3(h) => h.derive_key_from_multiple_out(keys, additional_input, output_key), Self::SHA3_224(h) => h.derive_key_from_multiple_out(keys, additional_input, output_key), Self::SHA3_256(h) => h.derive_key_from_multiple_out(keys, additional_input, output_key), Self::SHA3_384(h) => h.derive_key_from_multiple_out(keys, additional_input, output_key), @@ -212,6 +221,7 @@ impl AlgorithmInfo for KDFFactory { Self::HKDF_SHA256(h) => h.alg_name(), Self::HKDF_SHA384(h) => h.alg_name(), Self::HKDF_SHA512(h) => h.alg_name(), + Self::HKDF_SM3(h) => h.alg_name(), Self::SHA3_224(h) => h.alg_name(), Self::SHA3_256(h) => h.alg_name(), Self::SHA3_384(h) => h.alg_name(), @@ -226,6 +236,7 @@ impl AlgorithmInfo for KDFFactory { Self::HKDF_SHA256(h) => h.max_security_strength(), Self::HKDF_SHA384(h) => h.max_security_strength(), Self::HKDF_SHA512(h) => h.max_security_strength(), + Self::HKDF_SM3(h) => h.max_security_strength(), Self::SHA3_224(h) => h.max_security_strength(), Self::SHA3_256(h) => h.max_security_strength(), Self::SHA3_384(h) => h.max_security_strength(), diff --git a/crypto/factory/tests/kdf_factory_tests.rs b/crypto/factory/tests/kdf_factory_tests.rs index 6ab26ee85..fbebc2b17 100644 --- a/crypto/factory/tests/kdf_factory_tests.rs +++ b/crypto/factory/tests/kdf_factory_tests.rs @@ -10,6 +10,7 @@ mod kdf_factory_tests { use bouncycastle_factory::AlgorithmFactory; use bouncycastle_factory::kdf_factory::KDFFactory; use bouncycastle_sha2::hkdf::HKDF_SHA384; + use bouncycastle_sm3::hkdf::HKDF_SM3; use bouncycastle_utils::ct; /// `AlgorithmInfo` answers for the algorithm the factory selected, not for the factory. @@ -21,10 +22,12 @@ mod kdf_factory_tests { SHA3_224_NAME, SHA3_256_NAME, SHA3_384_NAME, SHA3_512_NAME, SHAKE128_NAME, SHAKE256_NAME, }; + use bouncycastle_sm3::hkdf::HKDF_SM3_NAME; let table = [ (HKDF_SHA256_NAME, _128bit), (HKDF_SHA384_NAME, _192bit), (HKDF_SHA512_NAME, _256bit), + (HKDF_SM3_NAME, _128bit), (SHA3_224_NAME, _112bit), (SHA3_256_NAME, _128bit), (SHA3_384_NAME, _192bit), @@ -110,6 +113,42 @@ mod kdf_factory_tests { assert!(ct::ct_eq_bytes(derived_key.ref_to_bytes(), expected_key.ref_to_bytes())); assert_eq!(derived_key.key_len(), 48, "HKDF-SHA384 derives a hash-length key"); + /* HKDF-SM3 */ + // As for HKDF-SHA384: HKDF_SM3 is checked against known answers in bouncycastle-sm3, so + // here the factory's output is compared with the alias used directly. + let key_material = KeyMaterial256::from_bytes(&DUMMY_SEED[..32]).unwrap(); + let derived_key = + KDFFactory::new("HKDF-SM3").unwrap().derive_key(&key_material, b"info").unwrap(); + let expected_key = HKDF_SM3::new().derive_key(&key_material, b"info").unwrap(); + assert!(ct::ct_eq_bytes(derived_key.ref_to_bytes(), expected_key.ref_to_bytes())); + assert_eq!(derived_key.key_len(), 32, "HKDF-SM3 derives a hash-length key"); + + // The _out forms fill a longer buffer and report how much they wrote. + let mut out = KeyMaterial512::new(); + let n = KDFFactory::new("HKDF-SM3") + .unwrap() + .derive_key_out(&key_material, b"info", &mut out) + .unwrap(); + let mut expected_out = KeyMaterial512::new(); + let expected_n = + HKDF_SM3::new().derive_key_out(&key_material, b"info", &mut expected_out).unwrap(); + assert_eq!(n, 64); + assert_eq!(n, expected_n); + assert!(ct::ct_eq_bytes(out.ref_to_bytes(), expected_out.ref_to_bytes())); + + let salt = + KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[32..64], KeyType::MACKey).unwrap(); + let n = KDFFactory::new("HKDF-SM3") + .unwrap() + .derive_key_from_multiple_out(&[&salt, &key_material], b"info", &mut out) + .unwrap(); + let expected_n = HKDF_SM3::new() + .derive_key_from_multiple_out(&[&salt, &key_material], b"info", &mut expected_out) + .unwrap(); + assert_eq!(n, 64); + assert_eq!(n, expected_n); + assert!(ct::ct_eq_bytes(out.ref_to_bytes(), expected_out.ref_to_bytes())); + /* HKDF-SHA512 */ // Note: this value is not checked against any external reference implementation, // The value is hard-coded to ensure consistency. diff --git a/crypto/sm3/Cargo.toml b/crypto/sm3/Cargo.toml index 924fdf7a8..8f6415af4 100644 --- a/crypto/sm3/Cargo.toml +++ b/crypto/sm3/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true [dependencies] bouncycastle-core.workspace = true +bouncycastle-hkdf.workspace = true bouncycastle-hmac.workspace = true bouncycastle-utils.workspace = true @@ -21,3 +22,7 @@ harness = false [[bench]] name = "hmac_sm3_benches" harness = false + +[[bench]] +name = "hkdf_sm3_benches" +harness = false diff --git a/crypto/sm3/benches/hkdf_sm3_benches.rs b/crypto/sm3/benches/hkdf_sm3_benches.rs new file mode 100644 index 000000000..c9d506ec6 --- /dev/null +++ b/crypto/sm3/benches/hkdf_sm3_benches.rs @@ -0,0 +1,75 @@ +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterial256, KeyMaterial512, KeyMaterialTrait, KeyType, +}; +use bouncycastle_core::traits::RNG; +use bouncycastle_rng as rng; +use bouncycastle_sm3::hkdf::HKDF_SM3; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +fn bench_hkdf_sm3(c: &mut Criterion) { + let mut data_block = [0_u8; 1024]; + let mut output = KeyMaterial512::new(); + rng::DefaultRNG::default().next_bytes_out(&mut data_block).unwrap(); + + let key = KeyMaterial256::from_bytes_as_type(&data_block[..32], KeyType::MACKey).unwrap(); + + let mut big_data: Vec = vec![]; + for _ in 0..100 { + big_data.extend_from_slice(&data_block); + } + + let mut group = c.benchmark_group("HKDF_SM3::do_extract_update_key"); + group.throughput(Throughput::Bytes(key.key_len() as u64)); + group.bench_function(format!("{} bytes of IKM per call", key.key_len() as u64), |b| { + let mut hkdf = HKDF_SM3::default(); + hkdf.do_extract_init(&key).unwrap(); + b.iter(|| { + let bytes_written = hkdf.do_extract_update_key(&key).unwrap(); + black_box(&bytes_written); + }); + hkdf.do_extract_final_out(&mut output).unwrap(); + black_box(&output); + }); + group.finish(); + + let mut group = c.benchmark_group("HKDF_SM3::do_extract_update_bytes"); + group.throughput(Throughput::Bytes(big_data.len() as u64)); + group.bench_function(format!("{} bytes of IKM data", big_data.len() as u64), |b| { + let mut hkdf = HKDF_SM3::default(); + hkdf.do_extract_init(&key).unwrap(); + b.iter(|| { + let bytes_written = hkdf.do_extract_update_bytes(black_box(&big_data)).unwrap(); + black_box(&bytes_written); + }); + hkdf.do_extract_final_out(&mut output).unwrap(); + black_box(&output); + }); + group.finish(); + + let mut group = c.benchmark_group("HKDF_SM3::expand_out large info"); + group.throughput(Throughput::Bytes(big_data.len() as u64)); + group.bench_function(format!("{} bytes of additional data", big_data.len() as u64), |b| { + let mut output_key = KeyMaterial512::new(); + b.iter(|| { + HKDF_SM3::expand_out(&key, &big_data, 64, &mut output_key).unwrap(); + black_box(&output_key); + }); + }); + group.finish(); + + let mut group = c.benchmark_group("HKDF_SM3::expand_out max output size (255*HashLen)"); + group.throughput(Throughput::Bytes(255 * 32u64)); + group.bench_function(format!("{} bytes of output key material", 255 * 32u64), |b| { + let mut output_key = KeyMaterial::<8160>::new(); + b.iter(|| { + HKDF_SM3::extract_and_expand_out(&key, &key, &data_block, 255 * 32, &mut output_key) + .unwrap(); + black_box(&output_key); + }); + }); + group.finish(); +} + +criterion_group!(benches, bench_hkdf_sm3); +criterion_main!(benches); diff --git a/crypto/sm3/src/hkdf.rs b/crypto/sm3/src/hkdf.rs new file mode 100644 index 000000000..ac3583084 --- /dev/null +++ b/crypto/sm3/src/hkdf.rs @@ -0,0 +1,51 @@ +//! HMAC-based Extract-and-Expand Key Derivation Function (HKDF) over SM3, as per RFC 5869. +//! +//! Uses [`bouncycastle_hkdf`] to provide the HKDF-SM3 instantiation: [`HKDF_SM3`]. HKDF itself is +//! implemented generically in [`bouncycastle_hkdf`]; this module pins its const parameters to SM3 +//! and publishes the resulting type alias. See [`bouncycastle_hkdf`] for the extract and expand +//! phases, entropy tracking and suspend/resume, all of which apply here unchanged. +//! +//! # Usage Examples +//! +//! ``` +//! use bouncycastle_core::key_material::{KeyMaterial, KeyMaterial256, KeyType}; +//! use bouncycastle_sm3::hkdf::HKDF_SM3; +//! +//! let salt = KeyMaterial256::from_bytes_as_type(&[0x5a; 32], KeyType::MACKey).unwrap(); +//! let ikm = KeyMaterial256::from_bytes_as_type(&[0xa5; 32], KeyType::MACKey).unwrap(); +//! +//! let mut okm = KeyMaterial::<64>::new(); +//! HKDF_SM3::extract_and_expand_out(&salt, &ikm, b"context info", 64, &mut okm).unwrap(); +//! ``` +//! +//! # Memory Usage +//! +//! | Object | Size (bytes) | +//! |----------------------------|--------------| +//! | `HKDF_SM3` | 368 | +//! | Suspended `HKDF_SM3` state | 122 | +//! +//! The suspended state is the inner HMAC's (which is SM3's) plus 14 bytes; the salt is not part of +//! it and must be re-supplied on resume. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! * Resuming a suspended HKDF with a different salt cannot be detected and silently produces a +//! different PRK. +//! * The output key inherits the security strength of the inputs, at most SM3's 128 bits; HKDF +//! stretches key material but does not create entropy. + +use crate::hmac::SUSPENDED_HMAC_SM3_STATE_LEN; +use crate::{SM3, SUSPENDED_SM3_STATE_LEN}; +use bouncycastle_hkdf::HKDF; + +/// Algorithm name string for HKDF-SM3, as used by the factories and CLI. +pub const HKDF_SM3_NAME: &str = "HKDF-SM3"; + +/// Length in bytes of the serialized state of [`HKDF_SM3`]: the inner HMAC's, plus the 14 bytes +/// of HKDF's own bookkeeping (see the `SuspendableKeyed` impl in `bouncycastle-hkdf`). +pub const SUSPENDED_HKDF_SM3_STATE_LEN: usize = SUSPENDED_HMAC_SM3_STATE_LEN + 14; + +/// Public type for HKDF using SM3. +#[allow(non_camel_case_types)] +pub type HKDF_SM3 = HKDF; diff --git a/crypto/sm3/src/hmac.rs b/crypto/sm3/src/hmac.rs index 0d928fd72..e6060da70 100644 --- a/crypto/sm3/src/hmac.rs +++ b/crypto/sm3/src/hmac.rs @@ -41,6 +41,7 @@ //! * SM3 is a Merkle-Damgard construction and so is subject to length extension; `SM3(k || m)` is //! not a secure MAC and HMAC-SM3 is the right construction for keyed hashing over SM3. +use crate::hkdf::HKDF_SM3_NAME; use crate::{SM3, SUSPENDED_SM3_STATE_LEN}; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::security_strength::SecurityStrength; @@ -59,8 +60,6 @@ use bouncycastle_hmac::{HmacKey, MIN_FIPS_DIGEST_LEN}; /*** String constants ***/ /// Algorithm name string for HMAC-SM3, as used by the factories and CLI. pub const HMAC_SM3_NAME: &str = "HMAC-SM3"; -/// The name of HKDF over SM3, for `HMACParams::HKDF_ALG_NAME`. -pub const HKDF_SM3_NAME: &str = "HKDF-SM3"; /*** Type aliases ***/ /// Public type for HMAC using SM3. diff --git a/crypto/sm3/src/lib.rs b/crypto/sm3/src/lib.rs index 695ab2ffd..d6bb31f69 100644 --- a/crypto/sm3/src/lib.rs +++ b/crypto/sm3/src/lib.rs @@ -51,6 +51,9 @@ //! ## HMAC //! See [hmac]. //! +//! ## HKDF +//! See [hkdf]. +//! //! # Memory Usage //! //! No heap memory is used by the algorithm itself; the `Vec`-returning convenience methods @@ -110,6 +113,7 @@ mod sm3; +pub mod hkdf; pub mod hmac; pub use self::sm3::{SM3, SUSPENDED_SM3_STATE_LEN}; diff --git a/crypto/sm3/tests/hkdf_sm3_tests.rs b/crypto/sm3/tests/hkdf_sm3_tests.rs new file mode 100644 index 000000000..2fe63ef11 --- /dev/null +++ b/crypto/sm3/tests/hkdf_sm3_tests.rs @@ -0,0 +1,135 @@ +//! HKDF-SM3 tests. There are no published HKDF-SM3 vectors (RFC 5869 covers SHA-1 and SHA-256, +//! Wycheproof has no SM3 HKDF file), so the known answers are RFC 5869 Appendix A's three +//! SHA-256 test-case inputs run through OpenSSL 3.0.13 +//! (`openssl kdf -kdfopt digest:SM3 ... HKDF`, `mode:EXTRACT_ONLY` for the PRK), whose SHA-256 +//! output for the same inputs reproduces the RFC's values. + +#[cfg(test)] +mod hkdf_sm3_tests { + use bouncycastle_core::hazmat::do_hazardous_operations; + use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::{Algorithm, AlgorithmInfo}; + use bouncycastle_core_test_framework::DUMMY_SEED; + use bouncycastle_core_test_framework::kdf::TestFrameworkKDF; + use bouncycastle_hex as hex; + use bouncycastle_sm3::hkdf::{HKDF_SM3, HKDF_SM3_NAME, SUSPENDED_HKDF_SM3_STATE_LEN}; + + /// Key material of any length and content, including empty or all-zero, as the RFC 5869 + /// vectors need. + fn key(hex_str: &str, key_type: KeyType) -> KeyMaterial<100> { + let mut k = KeyMaterial::<100>::new(); + do_hazardous_operations(&mut k, |k| { + k.set_bytes_as_type(&hex::decode(hex_str).unwrap(), key_type) + }) + .unwrap(); + k + } + + #[test] + fn algorithm() { + assert_eq!(HKDF_SM3::ALG_NAME, HKDF_SM3_NAME); + assert_eq!(HKDF_SM3::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(HKDF_SM3::new().alg_name(), "HKDF-SM3"); + assert_eq!(SUSPENDED_HKDF_SM3_STATE_LEN, 122); + } + + /// `(ikm, salt, info, prk, okm)`, from RFC 5869 Appendix A.1, A.2 and A.3's inputs. + const KATS: [(&str, &str, &str, &str, &str); 3] = [ + ( + "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "000102030405060708090a0b0c", + "f0f1f2f3f4f5f6f7f8f9", + "e0d6f7b0bd056327b7659f1f39ad850561fbcf4fb10fb58e88eafa55cf7cd01e", + "c69fe91b7aaee2dd5718d72dcaee0cce93f1b8e41f792da51261b6a517e68b36ed2c595572b01dfa359b", + ), + ( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f", + "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + "1a43a7fedb2d111eb33babd0d256c272aa3262cdb12e6b43d4321ae8888485d5", + "c1226236bbdefa7921f9febe27b864f33e449201b436d8844ea53f58170dd6426defbd22ed1f3c5960f35523e62e3b6c0d657f2c61893436f539013199bfaef25aafd1e7726ede927623a9f5cbb8885c7e5d", + ), + ( + "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "", + "", + "004fc37143377d072d74e82ff480e8d7937ec607411bc1ec65dd34401871ff9c", + "c8c91a38ae2fb3b023a7c38ce9f0748f28230d59b6b950ba3ba949bf0d713a5774815778801741cb2034", + ), + ]; + + #[test] + fn known_answers() { + for (ikm, salt, info, prk, okm) in KATS { + let ikm = key(ikm, KeyType::CryptographicRandom); + let salt = key(salt, KeyType::MACKey); + let info = hex::decode(info).unwrap(); + let okm = hex::decode(okm).unwrap(); + + // Extract, then expand. + let mut prk_key = HKDF_SM3::extract(&salt, &ikm).unwrap(); + assert_eq!(prk_key.ref_to_bytes(), hex::decode(prk).unwrap()); + // Some inputs are too short to meet the seeding rules; the PRK is used as a key anyway. + do_hazardous_operations(&mut prk_key, |p| p.set_key_type(KeyType::MACKey)).unwrap(); + let mut out = KeyMaterial::<100>::new(); + HKDF_SM3::expand_out(&prk_key, &info, okm.len(), &mut out).unwrap(); + assert_eq!(out.ref_to_bytes(), &okm[..]); + + // The streaming extract gives the same PRK. + let mut hkdf = HKDF_SM3::new(); + hkdf.do_extract_init(&salt).unwrap(); + for chunk in ikm.ref_to_bytes().chunks(5) { + hkdf.do_extract_update_bytes(chunk).unwrap(); + } + assert_eq!(hkdf.do_extract_final().unwrap().ref_to_bytes(), hex::decode(prk).unwrap()); + } + } + + #[test] + fn as_kdf() { + // derive_key uses an all-zero salt and a hash-length output, which is the extract and + // expand of the same inputs truncated to 32 bytes. + let ikm = key(&hex::encode(&DUMMY_SEED[..64]), KeyType::MACKey); + let zero_salt = key(&"00".repeat(32), KeyType::MACKey); + let info = &DUMMY_SEED[64..80]; + + let mut expected = KeyMaterial::<64>::new(); + HKDF_SM3::extract_and_expand_out(&zero_salt, &ikm, info, 64, &mut expected).unwrap(); + let mut expected_truncated = expected.clone(); + expected_truncated.set_key_len(32).unwrap(); + + let framework = TestFrameworkKDF::new(); + framework.test_kdf_single_key::(&ikm, info, &expected_truncated); + framework.test_kdf_multiple_key::(&[&zero_salt, &ikm], info, &mut expected); + } + + #[test] + fn suspendable_keyed_state() { + use bouncycastle_core::traits::SuspendableKeyed; + use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableKeyedState; + + let salt = key("000102030405060708090a0b0c", KeyType::MACKey); + let (part1, part2) = DUMMY_SEED[..48].split_at(20); + + let mut hkdf = HKDF_SM3::new(); + hkdf.do_extract_init(&salt).unwrap(); + hkdf.do_extract_update_bytes(part1).unwrap(); + TestFrameworkSuspendableKeyedState::new().test(&hkdf, &salt); + + let mut uninterrupted = hkdf.clone(); + uninterrupted.do_extract_update_bytes(part2).unwrap(); + let expected = uninterrupted.do_extract_final().unwrap(); + + let state = hkdf.suspend(); + assert_eq!(state.len(), SUSPENDED_HKDF_SM3_STATE_LEN); + let mut resumed = HKDF_SM3::from_suspended(state, &salt).unwrap(); + resumed.do_extract_update_bytes(part2).unwrap(); + assert_eq!(resumed.do_extract_final().unwrap().ref_to_bytes(), expected.ref_to_bytes()); + } + + #[test] + fn struct_size() { + assert_eq!(size_of::(), 368); + } +} From 17681b1de9c4b2510d91c0321684eb3ca57475ef Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 18:07:05 +1100 Subject: [PATCH 05/57] sm3, mem_usage_benches: bench_sm3_mem_usage, in bench_ascon_mem_usage's shape (each entry point called through a black_boxed fn pointer under an #[inline(never)] measure closure, inputs built in sibling frames), and a per-entry-point stack table in sm3's Memory Usage section replacing the 272-byte message-schedule estimate: the deepest frame chain below each entry point from -C remark=prologepilog and --emit=asm after thin LTO, return addresses included and panic paths not followed, x86-64 Linux, rustc 1.97.0-nightly: do_update 320 B, do_final_out 336, do_final_partial_bits_out 464, hash_out 592, suspend 8, from_suspended 80, HmacKey::::from_key_material 992, HmacKey::::mac_out 928, HKDF_SM3::extract_and_expand_out 2560; every chain bottoms out in SM3::compress's 264-byte frame Assisted-by: Claude:claude-opus-5-5 --- crypto/sm3/src/lib.rs | 18 +- mem_usage_benches/Cargo.toml | 4 + mem_usage_benches/src/bench_sm3_mem_usage.rs | 242 +++++++++++++++++++ mem_usage_benches/src/lib.rs | 1 + 4 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 mem_usage_benches/src/bench_sm3_mem_usage.rs diff --git a/crypto/sm3/src/lib.rs b/crypto/sm3/src/lib.rs index d6bb31f69..8c9af863d 100644 --- a/crypto/sm3/src/lib.rs +++ b/crypto/sm3/src/lib.rs @@ -64,9 +64,21 @@ //! | `SM3` | 112 | //! | Suspended state | 108 | //! -//! The object holds the 8-word chaining value plus one 64-byte block of buffered input. The -//! compression function additionally uses a 68-word message schedule (272 bytes) on the stack for -//! the duration of a call. +//! The object holds the 8-word chaining value plus one 64-byte block of buffered input. +//! +//! Per-call stack usage does not depend on the data length. Measured as the deepest frame chain +//! below each entry point in the release build (x86-64 Linux, rustc 1.97.0-nightly, return +//! addresses included; see `mem_usage_benches/src/bench_sm3_mem_usage.rs`): +//! +//! | Entry point | Stack | +//! |-------------------------------------------------|-------------| +//! | `SM3::do_update` | 320 B | +//! | `SM3::do_final_out` | 336 B | +//! | `SM3::do_final_partial_bits_out` | 464 B | +//! | `SM3::hash_out` (one-shot) | 592 B | +//! | `SM3::suspend` / `SM3::from_suspended` | 8 / 80 B | +//! | `HmacKey::::from_key_material` / `mac_out` | 992 / 928 B | +//! | `HKDF_SM3::extract_and_expand_out` | 2560 B | //! //! # 🚨 Security Considerations 🚨 //! diff --git a/mem_usage_benches/Cargo.toml b/mem_usage_benches/Cargo.toml index 804cbe57a..1e06763dc 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -34,3 +34,7 @@ path = "src/bench_ccm_mem_usage.rs" [[bin]] name = "bench_ascon_mem_usage" path = "src/bench_ascon_mem_usage.rs" + +[[bin]] +name = "bench_sm3_mem_usage" +path = "src/bench_sm3_mem_usage.rs" diff --git a/mem_usage_benches/src/bench_sm3_mem_usage.rs b/mem_usage_benches/src/bench_sm3_mem_usage.rs new file mode 100644 index 000000000..5b906e4ad --- /dev/null +++ b/mem_usage_benches/src/bench_sm3_mem_usage.rs @@ -0,0 +1,242 @@ +//! Stack usage of the SM3 entry points, and the in-memory sizes printed by `print_struct_sizes`, +//! the source of the "Memory Usage" tables in the bouncycastle-sm3 crate docs. +//! +//! As with Ascon, a call needs a few hundred bytes, below the floor of the process's own start-up +//! that `bench_aes_mem_usage` describes, so massif cannot see it. The figures in the crate docs +//! are read from the compiler the way `bench_ascon_mem_usage` describes: `-C remark=prologepilog` +//! gives each function's frame and `--emit=asm` its calls, after LTO, and a figure is the deepest +//! chain of frames below the entry point plus 8 bytes of return address per call, not following +//! the panic paths. With every bench called from `main`: +//! +//! ```text +//! cargo rustc --release -p mem_usage_benches --bin bench_sm3_mem_usage -- -C remark=prologepilog --emit=asm +//! ``` +//! +//! Each entry point is called through a `black_box`ed function pointer, which gives it an +//! out-of-line body of its own, so the frame being read is the entry point's and not the +//! harness closure's. The inputs are built in `#[inline(never)]` helpers, so their frames are +//! siblings of the operation's. + +#![allow(dead_code)] +#![allow(unused_imports)] + +use core::hint::black_box; + +use bouncycastle::core::errors::{KDFError, MACError}; +use bouncycastle::core::key_material::{KeyMaterial, KeyMaterial256, KeyMaterialTrait, KeyType}; +use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle::core::traits::{Hash, HashOneShot, MAC, Suspendable}; +use bouncycastle::hmac::HmacKey; +use bouncycastle::sm3::hkdf::{HKDF_SM3, SUSPENDED_HKDF_SM3_STATE_LEN}; +use bouncycastle::sm3::hmac::{HMAC_SM3, SUSPENDED_HMAC_SM3_STATE_LEN}; +use bouncycastle::sm3::{SM3, SUSPENDED_SM3_STATE_LEN}; + +/// Two full 64-byte blocks and a partial one. +const MSG_LEN: usize = 150; + +/// This exists so /usr/bin/time can measure the base memory footprint of the harness itself. +#[inline(never)] +fn bench_do_nothing() { + eprintln!("DoNothing"); + + print!("{}", 1 + 1); +} + +/// Prints the in-memory size of each value a caller holds between calls, and the size of its +/// suspended state. Neither depends on how much data has been processed. +#[inline(never)] +fn print_struct_sizes() { + use core::mem::size_of; + + println!("size_of: {}", size_of::()); + println!("SUSPENDED_SM3_STATE_LEN: {}", SUSPENDED_SM3_STATE_LEN); + println!("size_of: {}", size_of::>()); + println!("size_of>: {}", size_of::>()); + println!("SUSPENDED_HMAC_SM3_STATE_LEN: {}", SUSPENDED_HMAC_SM3_STATE_LEN); + println!("size_of: {}", size_of::()); + println!("SUSPENDED_HKDF_SM3_STATE_LEN: {}", SUSPENDED_HKDF_SM3_STATE_LEN); +} + +/// Runs the operation in its own frame. Returns nothing, so no result crosses the boundary. +#[inline(never)] +fn measure(f: impl FnOnce()) { + f() +} + +/// A message filled at run time, so that its contents are not a constant the compiler could fold +/// the whole computation over. +#[inline(never)] +fn message(fill: u8) -> [u8; N] { + let mut m = [0u8; N]; + m.fill(black_box(fill)); + m +} + +/// An SM3 that has taken the message, so the final pads a partial block. +#[inline(never)] +fn sm3_after_data() -> SM3 { + let mut h = SM3::new(); + h.do_update(&message::(0xa5)); + h +} + +#[inline(never)] +fn key() -> KeyMaterial256 { + KeyMaterial256::from_bytes_as_type(&message::<32>(0x0b), KeyType::MACKey).unwrap() +} + +// ---- SM3 ---------------------------------------------------------------------------------- + +#[inline(never)] +fn bench_sm3_update() { + eprintln!("SM3::do_update, {MSG_LEN} B"); + let h = black_box(SM3::new()); + let msg = black_box(message::(0xa5)); + measure(move || { + let mut h = h; + let op = black_box(::do_update as fn(&mut SM3, &[u8])); + op(&mut h, &msg); + black_box(&h); + }); +} + +#[inline(never)] +fn bench_sm3_final() { + eprintln!("SM3::do_final_out"); + let h = black_box(sm3_after_data()); + measure(move || { + let mut out = [0u8; 32]; + let op = black_box(::do_final_out as fn(SM3, &mut [u8]) -> usize); + let n = op(h, &mut out); + print!("{:x?}", &out[..n]); + }); +} + +#[inline(never)] +fn bench_sm3_final_partial_bits() { + eprintln!("SM3::do_final_partial_bits_out"); + let h = black_box(sm3_after_data()); + measure(move || { + let mut out = [0u8; 32]; + let op = black_box( + ::do_final_partial_bits_out + as fn(SM3, u8, usize, &mut [u8]) -> Result, + ); + let n = op(h, 0xa0, 3, &mut out).unwrap(); + print!("{:x?}", &out[..n]); + }); +} + +#[inline(never)] +fn bench_sm3_oneshot() { + eprintln!("SM3::hash_out, {MSG_LEN} B"); + let msg = black_box(message::(0xa5)); + measure(move || { + let mut out = [0u8; 32]; + let op = black_box(::hash_out as fn(&[u8], &mut [u8]) -> usize); + let n = op(&msg, &mut out); + print!("{:x?}", &out[..n]); + }); +} + +#[inline(never)] +fn bench_sm3_suspend() { + eprintln!("SM3::suspend"); + let h = black_box(sm3_after_data()); + measure(move || { + let op = black_box( + >::suspend + as fn(SM3) -> [u8; SUSPENDED_SM3_STATE_LEN], + ); + let state = op(h); + print!("{:x?}", &state[..]); + }); +} + +#[inline(never)] +fn bench_sm3_from_suspended() { + eprintln!("SM3::from_suspended"); + let state = black_box(sm3_after_data().suspend()); + measure(move || { + let op = black_box( + >::from_suspended + as fn([u8; SUSPENDED_SM3_STATE_LEN]) -> Result, + ); + let h = op(state).unwrap(); + black_box(&h); + }); +} + +// ---- HMAC-SM3 and HKDF-SM3 ---------------------------------------------------------------- + +#[inline(never)] +fn hmac_sm3_key() -> HmacKey { + HmacKey::::from_key_material(&key()).unwrap() +} + +#[inline(never)] +fn bench_hmac_sm3_key() { + eprintln!("HmacKey::::from_key_material"); + let key = black_box(key()); + measure(move || { + let op = black_box( + as FromAnyKeyMaterial>::from_key_material + as fn(&dyn KeyMaterialTrait) -> Result, MACError>, + ); + let k = op(&key).unwrap(); + black_box(&k); + }); +} + +#[inline(never)] +fn bench_hmac_sm3_mac_out() { + eprintln!("HmacKey::::mac_out, {MSG_LEN} B"); + let k = black_box(hmac_sm3_key()); + let msg = black_box(message::(0xa5)); + measure(move || { + let mut out = [0u8; 32]; + let op = black_box( + as MacGenerationKey>::mac_out + as fn(&HmacKey, &[u8], &mut [u8]) -> Result, + ); + let n = op(&k, &msg, &mut out).unwrap(); + print!("{:x?}", &out[..n]); + }); +} + +#[inline(never)] +fn bench_hkdf_sm3_oneshot() { + eprintln!("HKDF_SM3::extract_and_expand_out, 64 B out"); + let salt = black_box(key()); + let ikm = black_box(key()); + let info = black_box(message::<16>(0x5a)); + measure(move || { + let mut okm = KeyMaterial::<64>::new(); + let op = black_box( + HKDF_SM3::extract_and_expand_out + as fn( + &KeyMaterial256, + &KeyMaterial256, + &[u8], + usize, + &mut KeyMaterial<64>, + ) -> Result, + ); + let n = op(&salt, &ikm, &info, 64, &mut okm).unwrap(); + black_box((n, &okm)); + }); +} + +fn main() { + print_struct_sizes() + // bench_do_nothing() + // bench_sm3_update() + // bench_sm3_final() + // bench_sm3_final_partial_bits() + // bench_sm3_oneshot() + // bench_sm3_suspend() + // bench_sm3_from_suspended() + // bench_hmac_sm3_key() + // bench_hmac_sm3_mac_out() + // bench_hkdf_sm3_oneshot() +} diff --git a/mem_usage_benches/src/lib.rs b/mem_usage_benches/src/lib.rs index 8e86dc3c5..231a1b730 100644 --- a/mem_usage_benches/src/lib.rs +++ b/mem_usage_benches/src/lib.rs @@ -5,3 +5,4 @@ mod bench_mldsa_mem_usage; mod bench_mlkem_mem_usage; mod bench_sha2_mem_usage; mod bench_sha3_mem_usage; +mod bench_sm3_mem_usage; From 74249cfd0d9ad03cee3953587a56f5d611387ebd Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 18:20:45 +1100 Subject: [PATCH 06/57] ascon: KEY_LEN, NONCE_LEN and TAG_LEN become ASCON_AEAD128_KEY_LEN, ASCON_AEAD128_NONCE_LEN and ASCON_AEAD128_TAG_LEN, so a glob import of the crate no longer collides with another crate's lengths and the name says which algorithm it is, as the AES and cipher crates prefix theirs (CCM_NONCE_LEN, GCM_NONCE_LEN); no aliases for the old names, the library being alpha; rename only, comments that it pushed past 100 columns reflowed; cargo mutants: 704 mutants, 598 caught, 98 unviable, 8 missed, the same equivalents as before Assisted-by: Claude:claude-opus-5-5 --- crypto/ascon/src/ascon_aead128.rs | 169 +++++++++++------- crypto/ascon/src/lib.rs | 6 +- crypto/ascon/tests/ascon_wycheproof.rs | 22 ++- .../src/bench_ascon_mem_usage.rs | 116 ++++++++---- 4 files changed, 202 insertions(+), 111 deletions(-) diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs index ddf475b1d..f40850e5a 100644 --- a/crypto/ascon/src/ascon_aead128.rs +++ b/crypto/ascon/src/ascon_aead128.rs @@ -36,11 +36,11 @@ use crate::ASCON_AEAD128_NAME; use crate::permutation::{AsconState, load_u64_le, p8, p12, store_u64_le}; /// Length in bytes of the Ascon-AEAD128 key. -pub const KEY_LEN: usize = 16; +pub const ASCON_AEAD128_KEY_LEN: usize = 16; /// Length in bytes of the Ascon-AEAD128 nonce. -pub const NONCE_LEN: usize = 16; +pub const ASCON_AEAD128_NONCE_LEN: usize = 16; /// Length in bytes of the Ascon-AEAD128 authentication tag. -pub const TAG_LEN: usize = 16; +pub const ASCON_AEAD128_TAG_LEN: usize = 16; const RATE: usize = 16; /// Ascon-AEAD128 initial value (SP 800-232 Table 14). @@ -116,17 +116,17 @@ impl Phase { /// /// The encrypting side implements [`AEADCipherEncryptor`] and [`SymmetricCipherEncryptor`]; the /// nonce is drawn by the key's `create_encryptor` and returned, or supplied to -/// `create_encryptor_nonce` by a caller who takes on its uniqueness. `FINAL_LEN` is `TAG_LEN`: -/// nothing is held back, so the -/// inline [`SymmetricCipherEncryptor::do_encrypt_final`] writes only the tag, and the detached +/// `create_encryptor_nonce` by a caller who takes on its uniqueness. `FINAL_LEN` is +/// `ASCON_AEAD128_TAG_LEN`: nothing is held back, so the inline +/// [`SymmetricCipherEncryptor::do_encrypt_final`] writes only the tag, and the detached /// [`AEADCipherEncryptor::do_encrypt_final_detached_out`] only zeroes its buffer. /// /// The decrypting side implements [`AEADCipherDecryptor`] and [`SymmetricCipherDecryptor`]. The -/// cipher emits every byte as it is seen, but this side holds back the last `TAG_LEN` bytes of -/// ciphertext, since until the stream ends it cannot know whether they are the inline tag +/// cipher emits every byte as it is seen, but this side holds back the last `ASCON_AEAD128_TAG_LEN` +/// bytes of ciphertext, since until the stream ends it cannot know whether they are the inline tag /// ([`SymmetricCipherDecryptor::do_decrypt_final`]) or ciphertext with the tag carried separately -/// ([`AEADCipherDecryptor::do_decrypt_final_detached_out`]). They are ciphertext, not plaintext, -/// so they need no [`Secret`] wrapper; the encrypting side carries the same fields unused. +/// ([`AEADCipherDecryptor::do_decrypt_final_detached_out`]). They are ciphertext, not plaintext, so +/// they need no [`Secret`] wrapper; the encrypting side carries the same fields unused. /// /// ```compile_fail /// # use bouncycastle_ascon::AsconAEAD128Key; @@ -151,8 +151,8 @@ pub struct Ascon_AEAD128 { // Where the call sequence is, for enforcing the call order. phase: Phase, /// Decrypting only: the most recent `held_len` bytes of ciphertext, not yet given to the - /// cipher. Always `min(TAG_LEN, total ciphertext seen)`. - held: [u8; TAG_LEN], + /// cipher. Always `min(ASCON_AEAD128_TAG_LEN, total ciphertext seen)`. + held: [u8; ASCON_AEAD128_TAG_LEN], held_len: usize, _dir: PhantomData, } @@ -176,7 +176,9 @@ impl Ascon_AEAD128 { /// Validate a [`KeyMaterial`] for use with Ascon-AEAD128 and return its key words. /// The key must be tagged as a [`KeyType::SymmetricCipherKey`] and carry at least the /// algorithm's 128-bit security strength (SP 800-232 R1/R2). - fn checked_key(key: &KeyMaterial) -> Result<[u64; 2], SymmetricCipherError> { + fn checked_key( + key: &KeyMaterial, + ) -> Result<[u64; 2], SymmetricCipherError> { if key.key_type() != KeyType::SymmetricCipherKey { return Err(KeyMaterialError::InvalidKeyType( "Ascon-AEAD128 requires a SymmetricCipherKey", @@ -190,14 +192,14 @@ impl Ascon_AEAD128 { .into()); } let bytes = key.ref_to_bytes(); - if bytes.len() != KEY_LEN { + if bytes.len() != ASCON_AEAD128_KEY_LEN { return Err(KeyMaterialError::InvalidLength.into()); } Ok([load_u64_le(bytes, 0), load_u64_le(bytes, 8)]) } /// Step 1 from already-validated key words: what [`AsconAEAD128Key`] calls per message. - fn from_key_words(key_words: &[u64; 2], nonce: &[u8; NONCE_LEN]) -> Self { + fn from_key_words(key_words: &[u64; 2], nonce: &[u8; ASCON_AEAD128_NONCE_LEN]) -> Self { let mut key_secret: Secret<[u64; 2]> = Secret::new(); *key_secret = *key_words; @@ -218,7 +220,7 @@ impl Ascon_AEAD128 { state, pos: 0, phase: Phase::Init, - held: [0u8; TAG_LEN], + held: [0u8; ASCON_AEAD128_TAG_LEN], held_len: 0, _dir: PhantomData, } @@ -312,14 +314,14 @@ impl Ascon_AEAD128 { // Finalization (SP 800-232 §4.1.1 step 4 / §4.1.2 step 4, Eq. 30-32 / 49-51): re-add the key, // permute with Ascon-p[12], and add the key again; the tag is the resulting last 128 bits. - fn finish_data(&mut self) -> [u8; TAG_LEN] { + fn finish_data(&mut self) -> [u8; ASCON_AEAD128_TAG_LEN] { self.state[2] ^= self.key[0]; self.state[3] ^= self.key[1]; p12(&mut self.state); self.state[3] ^= self.key[0]; self.state[4] ^= self.key[1]; - let mut tag = [0u8; TAG_LEN]; + let mut tag = [0u8; ASCON_AEAD128_TAG_LEN]; store_u64_le(&mut tag, 0, self.state[3]); store_u64_le(&mut tag, 8, self.state[4]); tag @@ -380,7 +382,7 @@ impl Ascon_AEAD128 { /// Finish encryption; returns the 128-bit tag (SP 800-232 §4.1.1 steps 3-4). Pads the final /// (possibly empty) plaintext block; no further bytes are emitted here since every /// plaintext/ciphertext byte was already written by `encrypt_bytes`. - fn finish_tag(mut self) -> [u8; TAG_LEN] { + fn finish_tag(mut self) -> [u8; ASCON_AEAD128_TAG_LEN] { self.check_data(); // Padding of the final (possibly empty) plaintext block (Eq. 27). self.xor_state_byte(self.pos, 0x01); @@ -413,7 +415,7 @@ impl Ascon_AEAD128 { } /// Finish decryption, checking `tag` in constant time (SP 800-232 §4.1.2 steps 3-4). - fn verify_tag(mut self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { + fn verify_tag(mut self, tag: &[u8; ASCON_AEAD128_TAG_LEN]) -> Result<(), SymmetricCipherError> { self.check_data(); // Padding of the final (possibly empty) ciphertext block (Eq. 47). self.xor_state_byte(self.pos, 0x01); @@ -431,7 +433,9 @@ impl Algorithm for Ascon_AEAD128 { const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; } -impl SymmetricCipherEncryptor for Ascon_AEAD128 { +impl SymmetricCipherEncryptor + for Ascon_AEAD128 +{ /// Ascon-AEAD128 never buffers: every byte given is a byte returned. fn do_encrypt_out_len(&self, input_len: usize) -> usize { input_len @@ -453,17 +457,21 @@ impl SymmetricCipherEncryptor for Ascon_AEAD128 } /// The inline layout: nothing is held back, so the final buffer is exactly the tag. - fn do_encrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { - Ok((self.finish_tag(), TAG_LEN)) + fn do_encrypt_final( + self, + ) -> Result<([u8; ASCON_AEAD128_TAG_LEN], usize), SymmetricCipherError> { + Ok((self.finish_tag(), ASCON_AEAD128_TAG_LEN)) } /// The ciphertext, which is as long as the plaintext, followed by the tag. fn encrypt_out_len(plaintext_len: usize) -> usize { - plaintext_len + TAG_LEN + plaintext_len + ASCON_AEAD128_TAG_LEN } } -impl AEADCipherEncryptor for Ascon_AEAD128 { +impl AEADCipherEncryptor + for Ascon_AEAD128 +{ fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { self.absorb_aad(aad) } @@ -471,17 +479,19 @@ impl AEADCipherEncryptor for Ascon_AEAD128 Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + ciphertext: &mut [u8; ASCON_AEAD128_TAG_LEN], + ) -> Result<(usize, [u8; ASCON_AEAD128_TAG_LEN]), SymmetricCipherError> { ciphertext.fill(0); Ok((0, self.finish_tag())) } } -impl SymmetricCipherDecryptor for Ascon_AEAD128 { - /// Everything but the last `TAG_LEN` bytes seen so far is released. +impl SymmetricCipherDecryptor + for Ascon_AEAD128 +{ + /// Everything but the last `ASCON_AEAD128_TAG_LEN` bytes seen so far is released. fn do_decrypt_out_len(&self, input_len: usize) -> usize { - (self.held_len + input_len).saturating_sub(TAG_LEN) + (self.held_len + input_len).saturating_sub(ASCON_AEAD128_TAG_LEN) } fn do_decrypt_out( @@ -503,8 +513,8 @@ impl SymmetricCipherDecryptor for Ascon_AEAD128 // Called even when `release` is 0: that is what ends the AAD phase, so a later non-empty // `do_update_aad` is refused however little ciphertext has been seen. self.decrypt_bytes(out); - // Keep the newest `TAG_LEN` (or fewer) bytes: what is left of `held`, then the tail of - // `ciphertext`. + // Keep the newest `ASCON_AEAD128_TAG_LEN` (or fewer) bytes: what is left of `held`, then + // the tail of `ciphertext`. let kept = self.held_len - from_held; self.held.copy_within(from_held..self.held_len, 0); let new_len = kept + ciphertext.len() - from_input; @@ -517,24 +527,28 @@ impl SymmetricCipherDecryptor for Ascon_AEAD128 /// release. /// /// # Errors - /// [`SymmetricCipherError::DecryptionFailed`] if fewer than `TAG_LEN` bytes were seen in all; - /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. - fn do_decrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { - if self.held_len < TAG_LEN { + /// [`SymmetricCipherError::DecryptionFailed`] if fewer than `ASCON_AEAD128_TAG_LEN` bytes were + /// seen in all; [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. + fn do_decrypt_final( + self, + ) -> Result<([u8; ASCON_AEAD128_TAG_LEN], usize), SymmetricCipherError> { + if self.held_len < ASCON_AEAD128_TAG_LEN { return Err(SymmetricCipherError::DecryptionFailed); } let tag = self.held; self.verify_tag(&tag)?; - Ok(([0u8; TAG_LEN], 0)) + Ok(([0u8; ASCON_AEAD128_TAG_LEN], 0)) } /// Everything but the trailing tag. fn decrypt_out_len(ciphertext_len: usize) -> usize { - ciphertext_len.saturating_sub(TAG_LEN) + ciphertext_len.saturating_sub(ASCON_AEAD128_TAG_LEN) } } -impl AEADCipherDecryptor for Ascon_AEAD128 { +impl AEADCipherDecryptor + for Ascon_AEAD128 +{ /// # Errors /// [`SymmetricCipherError::StateError`] if `aad` is non-empty and /// [`SymmetricCipherDecryptor::do_decrypt_out`] has already been called. @@ -547,8 +561,8 @@ impl AEADCipherDecryptor for Ascon_AEAD128 Result { plaintext.fill(0); let n = self.held_len; @@ -575,10 +589,10 @@ impl Ascon_AEAD128 { impl Ascon_AEAD128 { /// Decrypts in place, in one buffer: the in-place twin of /// [`SymmetricCipherDecryptor::do_decrypt_out`], with the same hold-back. The plaintext - /// released by this call, the oldest `(held + data.len()) - TAG_LEN` bytes of ciphertext seen - /// so far, is written to the front of `data` and its length returned; the newest `TAG_LEN` - /// bytes are held back for the final, so either final can follow. Whatever is in `data` past - /// the returned count is not plaintext. + /// released by this call, the oldest `(held + data.len()) - ASCON_AEAD128_TAG_LEN` bytes of + /// ciphertext seen so far, is written to the front of `data` and its length returned; the + /// newest `ASCON_AEAD128_TAG_LEN` bytes are held back for the final, so either final can + /// follow. Whatever is in `data` past the returned count is not plaintext. pub fn do_decrypt_inplace(&mut self, data: &mut [u8]) -> usize { let release = self.do_decrypt_out_len(data.len()); let from_held = release.min(self.held_len); @@ -588,7 +602,7 @@ impl Ascon_AEAD128 { // `data`, saved before the shift below overwrites the tail. let kept = self.held_len - from_held; let input_kept = data.len() - from_input; - let mut new_held = [0u8; TAG_LEN]; + let mut new_held = [0u8; ASCON_AEAD128_TAG_LEN]; new_held[..kept].copy_from_slice(&self.held[from_held..self.held_len]); new_held[kept..kept + input_kept].copy_from_slice(&data[from_input..]); @@ -645,21 +659,30 @@ impl PartialEq for AsconAEAD128Key { impl Eq for AsconAEAD128Key {} -impl FromKeyMaterial for AsconAEAD128Key { +impl FromKeyMaterial for AsconAEAD128Key { const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; /// # Errors /// * [`KeyMaterialError::InvalidKeyType`] if the key is not a `SymmetricCipherKey`. /// * [`KeyMaterialError::InvalidLength`] if the key is not 16 bytes long. /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 128 bits. - fn from_key_material(key: &KeyMaterial) -> Result { + fn from_key_material( + key: &KeyMaterial, + ) -> Result { let mut key_secret: Secret<[u64; 2]> = Secret::new(); *key_secret = Ascon_AEAD128::::checked_key(key)?; Ok(Self { key: key_secret }) } } -impl AeadEncryptionKey for AsconAEAD128Key { +impl + AeadEncryptionKey< + ASCON_AEAD128_KEY_LEN, + ASCON_AEAD128_NONCE_LEN, + ASCON_AEAD128_TAG_LEN, + ASCON_AEAD128_TAG_LEN, + > for AsconAEAD128Key +{ type Encryptor<'k> = Ascon_AEAD128 where @@ -670,26 +693,39 @@ impl AeadEncryptionKey for AsconAEAD128Key fn create_encryptor<'k>( &'k self, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { - let mut nonce = [0u8; NONCE_LEN]; + ) -> Result<(Self::Encryptor<'k>, [u8; ASCON_AEAD128_NONCE_LEN]), SymmetricCipherError> { + let mut nonce = [0u8; ASCON_AEAD128_NONCE_LEN]; rng.next_bytes_out(&mut nonce)?; Ok((Ascon_AEAD128::from_key_words(&self.key, &nonce), nonce)) } /// Under a nonce the caller supplies, who takes on the obligation that it never repeat /// under this key (SP 800-232 R3). - fn create_encryptor_nonce<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor<'k> { + fn create_encryptor_nonce<'k>( + &'k self, + nonce: &[u8; ASCON_AEAD128_NONCE_LEN], + ) -> Self::Encryptor<'k> { Ascon_AEAD128::from_key_words(&self.key, nonce) } } -impl AeadDecryptionKey for AsconAEAD128Key { +impl + AeadDecryptionKey< + ASCON_AEAD128_KEY_LEN, + ASCON_AEAD128_NONCE_LEN, + ASCON_AEAD128_TAG_LEN, + ASCON_AEAD128_TAG_LEN, + > for AsconAEAD128Key +{ type Decryptor<'k> = Ascon_AEAD128 where Self: 'k; - fn create_decryptor<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor<'k> { + fn create_decryptor<'k>( + &'k self, + nonce: &[u8; ASCON_AEAD128_NONCE_LEN], + ) -> Self::Decryptor<'k> { Ascon_AEAD128::from_key_words(&self.key, nonce) } } @@ -713,8 +749,9 @@ impl Display for Ascon_AEAD128 { pub const SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN: usize = CIPHER_STATE_LEN; /// Length in bytes of the serialized state of `Ascon_AEAD128`: the encrypting -/// side's layout, then the up to `TAG_LEN` bytes it holds back and their count. -pub const SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN: usize = CIPHER_STATE_LEN + TAG_LEN + 1; +/// side's layout, then the up to `ASCON_AEAD128_TAG_LEN` bytes it holds back and their count. +pub const SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN: usize = + CIPHER_STATE_LEN + ASCON_AEAD128_TAG_LEN + 1; /// The cipher core's serialized state: the whole of the encrypting side's and the head of the /// decrypting side's. @@ -762,15 +799,15 @@ impl<'k> SuspendableKeyed<'k, SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN> fn suspend(self) -> [u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN] { let mut out = [0u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN]; - out[CIPHER_STATE_LEN..CIPHER_STATE_LEN + TAG_LEN].copy_from_slice(&self.held); - debug_assert!(self.held_len <= TAG_LEN); - out[CIPHER_STATE_LEN + TAG_LEN] = self.held_len as u8; + out[CIPHER_STATE_LEN..CIPHER_STATE_LEN + ASCON_AEAD128_TAG_LEN].copy_from_slice(&self.held); + debug_assert!(self.held_len <= ASCON_AEAD128_TAG_LEN); + out[CIPHER_STATE_LEN + ASCON_AEAD128_TAG_LEN] = self.held_len as u8; out[..CIPHER_STATE_LEN].copy_from_slice(&self.suspend_as(DEC_DIR_BITS)); out } - /// A state suspended by the encrypting side, a held-back count above `TAG_LEN`, or held-back - /// bytes before any ciphertext was seen, is [`SuspendableError::InvalidData`]. + /// A state suspended by the encrypting side, a held-back count above `ASCON_AEAD128_TAG_LEN`, + /// or held-back bytes before any ciphertext was seen, is [`SuspendableError::InvalidData`]. fn from_suspended( serialized_state: [u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN], key: &'k Self::Key, @@ -779,9 +816,11 @@ impl<'k> SuspendableKeyed<'k, SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN> let cipher_state: [u8; CIPHER_STATE_LEN] = serialized_state[..CIPHER_STATE_LEN].try_into().unwrap(); let mut dec = Self::resume_as(cipher_state, key, DEC_DIR_BITS)?; - dec.held.copy_from_slice(&serialized_state[CIPHER_STATE_LEN..CIPHER_STATE_LEN + TAG_LEN]); - dec.held_len = serialized_state[CIPHER_STATE_LEN + TAG_LEN] as usize; - if dec.held_len > TAG_LEN { + dec.held.copy_from_slice( + &serialized_state[CIPHER_STATE_LEN..CIPHER_STATE_LEN + ASCON_AEAD128_TAG_LEN], + ); + dec.held_len = serialized_state[CIPHER_STATE_LEN + ASCON_AEAD128_TAG_LEN] as usize; + if dec.held_len > ASCON_AEAD128_TAG_LEN { return Err(SuspendableError::InvalidData); } // Bytes are held back only by `decrypt_bytes`' callers, which move the phase to `Data`. @@ -854,7 +893,7 @@ impl Ascon_AEAD128 { state: s, pos, phase, - held: [0u8; TAG_LEN], + held: [0u8; ASCON_AEAD128_TAG_LEN], held_len: 0, _dir: PhantomData, }) diff --git a/crypto/ascon/src/lib.rs b/crypto/ascon/src/lib.rs index 36e5fafdd..baeda293a 100644 --- a/crypto/ascon/src/lib.rs +++ b/crypto/ascon/src/lib.rs @@ -226,9 +226,9 @@ mod ascon_hash256; mod ascon_xof128; pub use ascon_aead128::{ - Ascon_AEAD128, AsconAEAD128Key, KEY_LEN, NONCE_LEN, - SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN, SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN, - TAG_LEN, + ASCON_AEAD128_KEY_LEN, ASCON_AEAD128_NONCE_LEN, ASCON_AEAD128_TAG_LEN, Ascon_AEAD128, + AsconAEAD128Key, SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN, + SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN, }; pub use ascon_cxof128::{AsconCXof128, AsconCXof128Squeezer, SUSPENDED_ASCON_CXOF128_STATE_LEN}; pub use ascon_hash256::{AsconHash256, SUSPENDED_ASCON_HASH256_STATE_LEN}; diff --git a/crypto/ascon/tests/ascon_wycheproof.rs b/crypto/ascon/tests/ascon_wycheproof.rs index 798a001f3..ebea69c8c 100644 --- a/crypto/ascon/tests/ascon_wycheproof.rs +++ b/crypto/ascon/tests/ascon_wycheproof.rs @@ -9,7 +9,10 @@ //! A `valid` case must encrypt to exactly `ct || tag` and decrypt back to `msg`. An `invalid` case //! must be rejected with `AEADTagCheckFailed`, leaving the output buffer zeroized. -use bouncycastle_ascon::{Ascon_AEAD128, AsconAEAD128Key, KEY_LEN, NONCE_LEN, TAG_LEN}; +use bouncycastle_ascon::{ + ASCON_AEAD128_KEY_LEN, ASCON_AEAD128_NONCE_LEN, ASCON_AEAD128_TAG_LEN, Ascon_AEAD128, + AsconAEAD128Key, +}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::do_hazardous_operations; @@ -21,9 +24,12 @@ use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wych /// Wraps the vector's key bytes as a cipher key, promoting them if `KeyMaterial`'s entropy /// heuristic declined to (as `ascon_bc-test-data.rs` does for the NIST KAT keys). -fn cipher_key(bytes: &[u8]) -> KeyMaterial { - let mut key = KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) - .expect("a 16-byte key"); +fn cipher_key(bytes: &[u8]) -> KeyMaterial { + let mut key = KeyMaterial::::from_bytes_as_type( + bytes, + KeyType::SymmetricCipherKey, + ) + .expect("a 16-byte key"); do_hazardous_operations(&mut key, |k| { k.set_key_type(KeyType::SymmetricCipherKey)?; k.set_security_strength(SecurityStrength::_128bit) @@ -44,7 +50,11 @@ fn wycheproof_ascon_aead128() { let (mut valid_count, mut invalid_count) = (0usize, 0usize); for group in doc.get("testGroups").and_then(Value::as_array).expect("testGroups") { - for (field, len) in [("keySize", KEY_LEN), ("ivSize", NONCE_LEN), ("tagSize", TAG_LEN)] { + for (field, len) in [ + ("keySize", ASCON_AEAD128_KEY_LEN), + ("ivSize", ASCON_AEAD128_NONCE_LEN), + ("tagSize", ASCON_AEAD128_TAG_LEN), + ] { let bits = group.get(field).and_then(Value::as_u64).expect(field); assert_eq!(bits as usize, 8 * len, "every group uses Ascon-AEAD128's fixed {field}"); } @@ -52,7 +62,7 @@ fn wycheproof_ascon_aead128() { for test in group.get("tests").and_then(Value::as_array).expect("tests") { let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); let key = aead_key(&hex_field(test, "key", tc_id)); - let nonce: [u8; NONCE_LEN] = + let nonce: [u8; ASCON_AEAD128_NONCE_LEN] = hex_field(test, "iv", tc_id).try_into().expect("a 16-byte nonce"); let aad = hex_field(test, "aad", tc_id); let msg = hex_field(test, "msg", tc_id); diff --git a/mem_usage_benches/src/bench_ascon_mem_usage.rs b/mem_usage_benches/src/bench_ascon_mem_usage.rs index 1bdfe8000..387e2511b 100644 --- a/mem_usage_benches/src/bench_ascon_mem_usage.rs +++ b/mem_usage_benches/src/bench_ascon_mem_usage.rs @@ -56,14 +56,14 @@ use core::hint::black_box; +use bouncycastle::ascon::{ + ASCON_AEAD128_KEY_LEN, ASCON_AEAD128_NONCE_LEN, ASCON_AEAD128_TAG_LEN, + SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN, SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN, +}; use bouncycastle::ascon::{Ascon_AEAD128, AsconAEAD128Key}; use bouncycastle::ascon::{AsconCXof128, AsconCXof128Squeezer, SUSPENDED_ASCON_CXOF128_STATE_LEN}; use bouncycastle::ascon::{AsconHash256, SUSPENDED_ASCON_HASH256_STATE_LEN}; use bouncycastle::ascon::{AsconXof128, AsconXof128Squeezer, SUSPENDED_ASCON_XOF128_STATE_LEN}; -use bouncycastle::ascon::{ - KEY_LEN, NONCE_LEN, SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN, - SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN, TAG_LEN, -}; use bouncycastle::cipher::{Decrypting, Encrypting}; use bouncycastle::core::errors::{HashError, RNGError, SymmetricCipherError}; use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; @@ -83,12 +83,12 @@ const MSG_LEN: usize = 40; /// One full AEAD block and a partial one. const AAD_LEN: usize = 20; /// The inline layout: ciphertext || tag. -const SEALED_LEN: usize = MSG_LEN + TAG_LEN; +const SEALED_LEN: usize = MSG_LEN + ASCON_AEAD128_TAG_LEN; -const KEY: [u8; KEY_LEN] = [ +const KEY: [u8; ASCON_AEAD128_KEY_LEN] = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, ]; -const NONCE: [u8; NONCE_LEN] = [ +const NONCE: [u8; ASCON_AEAD128_NONCE_LEN] = [ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, ]; @@ -115,7 +115,7 @@ impl RNG for FixedNonce { fn next_bytes_out(&mut self, out: &mut [u8]) -> Result { for (i, b) in out.iter_mut().enumerate() { - *b = NONCE[i % NONCE_LEN]; + *b = NONCE[i % ASCON_AEAD128_NONCE_LEN]; } Ok(out.len()) } @@ -178,8 +178,9 @@ fn measure(f: impl FnOnce()) { /// Wraps the hard-coded key. `#[inline(never)]` so the wrapping is a sibling frame of whatever /// uses the key, not part of it. #[inline(never)] -fn key() -> KeyMaterial { - KeyMaterial::::from_bytes_as_type(&KEY, KeyType::SymmetricCipherKey).unwrap() +fn key() -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(&KEY, KeyType::SymmetricCipherKey) + .unwrap() } /// The key object the workers are made from, likewise in its own frame. @@ -290,8 +291,8 @@ fn bench_aead_from_key_material() { let k = key(); measure(|| { let op = black_box( - >::from_key_material - as fn(&KeyMaterial) -> Result, + >::from_key_material + as fn(&KeyMaterial) -> Result, ); let key = op(&k).unwrap(); black_box(&key); @@ -305,11 +306,16 @@ fn bench_aead_encrypt_init() { let k = aead_key(); measure(|| { let op = black_box( - >::create_encryptor + >::create_encryptor as fn( &AsconAEAD128Key, &mut dyn RNG, - ) -> Result<(Enc, [u8; NONCE_LEN]), CipherErr>, + ) -> Result<(Enc, [u8; ASCON_AEAD128_NONCE_LEN]), CipherErr>, ); let (enc, nonce) = op(&k, &mut FixedNonce).unwrap(); black_box(&enc); @@ -325,8 +331,11 @@ fn bench_aead_encrypt_update_aad() { measure(move || { let mut enc = enc; let op = black_box( - >::do_update_aad - as fn(&mut Enc, &[u8]) -> Result<(), CipherErr>, + >::do_update_aad as fn(&mut Enc, &[u8]) -> Result<(), CipherErr>, ); op(&mut enc, &aad).unwrap(); black_box(&enc); @@ -342,7 +351,7 @@ fn bench_aead_encrypt_out() { let mut enc = enc; let mut ct = [0u8; MSG_LEN]; let op = black_box( - >::do_encrypt_out + >::do_encrypt_out as fn(&mut Enc, &[u8], &mut [u8]) -> Result, ); let n = op(&mut enc, &pt, &mut ct).unwrap(); @@ -357,8 +366,8 @@ fn bench_aead_encrypt_final() { let enc = black_box(encryptor_after_data()); measure(move || { let op = black_box( - >::do_encrypt_final - as fn(Enc) -> Result<([u8; TAG_LEN], usize), CipherErr>, + >::do_encrypt_final + as fn(Enc) -> Result<([u8; ASCON_AEAD128_TAG_LEN], usize), CipherErr>, ); let (tag, n) = op(enc).unwrap(); print!("{:x?}", &tag[..n]); @@ -370,10 +379,17 @@ fn bench_aead_encrypt_final_detached() { eprintln!("Ascon_AEAD128::do_encrypt_final_detached_out"); let enc = black_box(encryptor_after_data()); measure(move || { - let mut last = [0u8; TAG_LEN]; + let mut last = [0u8; ASCON_AEAD128_TAG_LEN]; let op = black_box( - >::do_encrypt_final_detached_out - as fn(Enc, &mut [u8; TAG_LEN]) -> Result<(usize, [u8; TAG_LEN]), CipherErr>, + >::do_encrypt_final_detached_out + as fn( + Enc, + &mut [u8; ASCON_AEAD128_TAG_LEN], + ) -> Result<(usize, [u8; ASCON_AEAD128_TAG_LEN]), CipherErr>, ); let (_, tag) = op(enc, &mut last).unwrap(); print!("{tag:x?}"); @@ -390,14 +406,19 @@ fn bench_aead_encrypt_oneshot() { measure(move || { let mut ct = [0u8; SEALED_LEN]; let op = black_box( - >::encrypt_with_aad_out + >::encrypt_with_aad_out as fn( &AsconAEAD128Key, &mut dyn RNG, &[u8], &[u8], &mut [u8], - ) -> Result<(usize, [u8; NONCE_LEN]), CipherErr>, + ) -> Result<(usize, [u8; ASCON_AEAD128_NONCE_LEN]), CipherErr>, ); let (n, _) = op(&k, &mut FixedNonce, &aad, &pt, &mut ct).unwrap(); print!("{:x?}", &ct[..n]); @@ -412,8 +433,13 @@ fn bench_aead_decrypt_init() { let k = aead_key(); measure(|| { let op = black_box( - >::create_decryptor - as fn(&AsconAEAD128Key, &[u8; NONCE_LEN]) -> Dec, + >::create_decryptor + as fn(&AsconAEAD128Key, &[u8; ASCON_AEAD128_NONCE_LEN]) -> Dec, ); let dec = op(&k, &NONCE); black_box(&dec); @@ -428,8 +454,11 @@ fn bench_aead_decrypt_update_aad() { measure(move || { let mut dec = dec; let op = black_box( - >::do_update_aad - as fn(&mut Dec, &[u8]) -> Result<(), CipherErr>, + >::do_update_aad as fn(&mut Dec, &[u8]) -> Result<(), CipherErr>, ); op(&mut dec, &aad).unwrap(); black_box(&dec); @@ -447,7 +476,7 @@ fn bench_aead_decrypt_out() { let mut dec = dec; let mut pt = [0u8; SEALED_LEN]; let op = black_box( - >::do_decrypt_out + >::do_decrypt_out as fn(&mut Dec, &[u8], &mut [u8]) -> Result, ); let n = op(&mut dec, &ct, &mut pt).unwrap(); @@ -462,8 +491,8 @@ fn bench_aead_decrypt_final() { let dec = black_box(decryptor_holding_tag()); measure(move || { let op = black_box( - >::do_decrypt_final - as fn(Dec) -> Result<([u8; TAG_LEN], usize), CipherErr>, + >::do_decrypt_final + as fn(Dec) -> Result<([u8; ASCON_AEAD128_TAG_LEN], usize), CipherErr>, ); let (_, n) = op(dec).unwrap(); print!("{n}"); @@ -475,12 +504,20 @@ fn bench_aead_decrypt_final() { fn bench_aead_decrypt_final_detached() { eprintln!("Ascon_AEAD128::do_decrypt_final_detached_out"); let dec = black_box(decryptor_holding_ciphertext()); - let tag: [u8; TAG_LEN] = black_box(sealed()[MSG_LEN..].try_into().unwrap()); + let tag: [u8; ASCON_AEAD128_TAG_LEN] = black_box(sealed()[MSG_LEN..].try_into().unwrap()); measure(move || { - let mut last = [0u8; TAG_LEN]; + let mut last = [0u8; ASCON_AEAD128_TAG_LEN]; let op = black_box( - >::do_decrypt_final_detached_out - as fn(Dec, &[u8; TAG_LEN], &mut [u8; TAG_LEN]) -> Result, + >::do_decrypt_final_detached_out + as fn( + Dec, + &[u8; ASCON_AEAD128_TAG_LEN], + &mut [u8; ASCON_AEAD128_TAG_LEN], + ) -> Result, ); let n = op(dec, &tag, &mut last).unwrap(); print!("{:x?}", &last[..n]); @@ -497,10 +534,15 @@ fn bench_aead_decrypt_oneshot() { measure(move || { let mut pt = [0u8; MSG_LEN]; let op = black_box( - >::decrypt_with_aad_out + >::decrypt_with_aad_out as fn( &AsconAEAD128Key, - &[u8; NONCE_LEN], + &[u8; ASCON_AEAD128_NONCE_LEN], &[u8], &[u8], &mut [u8], From e0781eeeb8f39c2352e5ec44e9472acdf3bb5129 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 18:20:46 +1100 Subject: [PATCH 07/57] hmac, sha2, sha3, sm3: the 32-bit HMAC truncation floor (MIN_FIPS_DIGEST_LEN) is cited to FIPS 140-3 IG C.L Resolution C.4, which carries SP 800-107r1 Section 5.3.3's requirement while NIST withdraws that document (announced 2022-12-20), in place of FIPS 140-2 IG A.8, and the error says FIPS 140-3; MIN_FIPS_DIGEST_LEN's doc also said 32 bits "exceeds" RFC 2104's 80-bit lower bound, where it is below it; the SP 800-107r1 citations with no new home yet (Section 5.3.4's min(K, 2C) key-strength ceiling, Table 1's digest strengths) stay, checked against the current text; docs and one error string only Assisted-by: Claude:claude-opus-5-5 --- crypto/hmac/src/lib.rs | 19 +++++++++---------- crypto/sha2/src/hmac.rs | 12 ++++++------ crypto/sha3/src/hmac.rs | 10 +++++----- crypto/sm3/src/hmac.rs | 10 +++++----- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index 9c7638859..d16764098 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -1,5 +1,5 @@ //! The generic Hash-Based Message Authentication Code (HMAC) construction, as specified in RFC 2104, -//! taking into account NIST Implementation Guidance in FIPS 140-2 IG A.8 and NIST SP 800-107-r1. +//! taking into account NIST Implementation Guidance in FIPS 140-3 IG C.L. //! //! This is a utility crate and is not intended to be used directly. It provides [`HmacKey`] -- the //! key object, which holds the two precomputed pad states RFC 2104 Section 4 describes and hands @@ -105,9 +105,9 @@ //! * Verification via [`MAC::verify`] / [`MAC::do_verify_final`] uses a constant-time comparison. //! Recomputing the MAC and comparing it with `==` leaks how many leading bytes matched. //! * [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is the shortest truncation this crate will produce, per -//! FIPS 140-2 IG A.8 / NIST SP 800-107-r1 Section 5.3.3. It is a floor, not a recommendation: -//! RFC 2104 Section 5 recommends that the output length "be not less than half the length of the -//! hash output ... and not less than 80 bits". +//! FIPS 140-3 IG C.L Resolution C.4 (formerly NIST SP 800-107r1 Section 5.3.3). It is a floor, +//! not a recommendation: RFC 2104 Section 5 recommends that the output length "be not less than +//! half the length of the hash output ... and not less than 80 bits". //! * The key is deliberately excluded from the suspended state and the [`HmacKey`] must be //! re-supplied on resume. Resuming with the wrong key cannot be detected and silently produces a //! wrong MAC, computed with different keys in the inner and outer pad. @@ -341,7 +341,7 @@ impl HmacKey { out.fill(0); if out.len() < MIN_FIPS_DIGEST_LEN { return Err(MACError::InvalidLength( - "HMAC truncation too short for FIPS 140-2 guidelines", + "HMAC truncation too short for FIPS 140-3 guidelines", )); } let mut ihash = vec![0u8; inner.output_len()]; @@ -403,11 +403,10 @@ impl Display for HMAC<'_, HASH> { const IPAD_BYTE: u8 = 0x36; const OPAD_BYTE: u8 = 0x5C; -/// Per FIPS 140-2 IG A.8 Use of a truncated HMAC (matching NIST SP 800-107-r1 -/// Section 5.3.3. Truncation of HMAC), says that the minimum truncation of a -/// HMAC for tagging should be 32 bits; this exceeds the lower bound set by -/// IETF RFC 2104 Section 5 Truncated output, which sets the lower bound to be -/// half of the hash's length and no fewer than 80 bits. +/// FIPS 140-3 IG C.L Resolution C.4, which carries over NIST SP 800-107r1 Section 5.3.3 while +/// that document is being withdrawn, says a truncated HMAC output "shall be no less than 32 bits"; +/// this is below the lower bound set by IETF RFC 2104 Section 5 Truncated output, which sets the +/// lower bound to be half of the hash's length and no fewer than 80 bits. /// /// However, as we feel there should be a minimum limit (and have an author /// work around this via explicit truncation manually afterwards), but not diff --git a/crypto/sha2/src/hmac.rs b/crypto/sha2/src/hmac.rs index 451ab92b8..5bcf834a5 100644 --- a/crypto/sha2/src/hmac.rs +++ b/crypto/sha2/src/hmac.rs @@ -1,5 +1,5 @@ -//! HMAC over the SHA-2 hashes, as specified in RFC 2104, with the truncation rules of NIST SP -//! 800-107 Rev 1. +//! HMAC over the SHA-2 hashes, as specified in RFC 2104, with the truncation rules of FIPS 140-3 +//! IG C.L. //! //! Uses [`bouncycastle_hmac`] to provide the HMAC-SHA2 instantiations: [`HMAC_SHA224`], //! [`HMAC_SHA256`], [`HMAC_SHA384`] and [`HMAC_SHA512`]. @@ -234,10 +234,10 @@ //! * Verify with [`MAC::verify`] or [`MAC::do_verify_final`] rather than computing the MAC yourself //! and comparing: those use a constant-time comparison, while `==` on the byte slices leaks how //! many leading bytes matched. -//! * Truncating the MAC output below [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is rejected, per FIPS 140-2 -//! IG A.8 / NIST SP 800-107-r1 Section 5.3.3. That is a floor, not a recommendation -- RFC 2104 -//! Section 5 recommends that the output length "be not less than half the length of the hash -//! output ... and not less than 80 bits". +//! * Truncating the MAC output below [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is rejected, per FIPS 140-3 +//! IG C.L Resolution C.4 (formerly NIST SP 800-107r1 Section 5.3.3). That is a floor, not a +//! recommendation -- RFC 2104 Section 5 recommends that the output length "be not less than half +//! the length of the hash output ... and not less than 80 bits". //! * Resuming a suspended HMAC with the wrong key cannot be detected and silently produces a wrong //! MAC; see the suspend/resume section above. //! * A key longer than the hash's block length is pre-hashed down to the output length (RFC 2104 diff --git a/crypto/sha3/src/hmac.rs b/crypto/sha3/src/hmac.rs index 7ec3bdf0f..67eb2f67a 100644 --- a/crypto/sha3/src/hmac.rs +++ b/crypto/sha3/src/hmac.rs @@ -1,5 +1,5 @@ //! HMAC over the SHA-3 hashes, as specified in RFC 2104, taking into account NIST Implementation -//! Guidance in FIPS 140-2 IG A.8 and NIST SP 800-107-r1. +//! Guidance in FIPS 140-3 IG C.L. //! //! Uses [`bouncycastle_hmac`] to provide the HMAC-SHA3 instantiations: [`HMAC_SHA3_224`], //! [`HMAC_SHA3_256`], [`HMAC_SHA3_384`] and [`HMAC_SHA3_512`]. @@ -234,10 +234,10 @@ //! * Verify with [`MAC::verify`] or [`MAC::do_verify_final`] rather than computing the MAC yourself //! and comparing: those use a constant-time comparison, while `==` on the byte slices leaks how //! many leading bytes matched. -//! * Truncating the MAC output below [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is rejected, per FIPS 140-2 -//! IG A.8 / NIST SP 800-107-r1 Section 5.3.3. That is a floor, not a recommendation -- RFC 2104 -//! Section 5 recommends that the output length "be not less than half the length of the hash -//! output ... and not less than 80 bits". +//! * Truncating the MAC output below [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is rejected, per FIPS 140-3 +//! IG C.L Resolution C.4 (formerly NIST SP 800-107r1 Section 5.3.3). That is a floor, not a +//! recommendation -- RFC 2104 Section 5 recommends that the output length "be not less than half +//! the length of the hash output ... and not less than 80 bits". //! * A suspended HMAC state can be inverted to the key and must be stored as securely as the key; //! and resuming with the wrong key cannot be detected and silently produces a wrong MAC. See the //! suspend/resume section above. diff --git a/crypto/sm3/src/hmac.rs b/crypto/sm3/src/hmac.rs index e6060da70..6b323c20b 100644 --- a/crypto/sm3/src/hmac.rs +++ b/crypto/sm3/src/hmac.rs @@ -1,5 +1,5 @@ //! HMAC over SM3, as specified in RFC 2104, taking into account NIST Implementation Guidance in -//! FIPS 140-2 IG A.8 and NIST SP 800-107-r1. +//! FIPS 140-3 IG C.L. //! //! Uses [`bouncycastle_hmac`] to provide the HMAC-SM3 instantiation: [`HMAC_SM3`]. //! @@ -32,10 +32,10 @@ //! * Verify with [`MAC::verify`] or [`MAC::do_verify_final`] rather than computing the MAC yourself //! and comparing: those use a constant-time comparison, while `==` on the byte slices leaks how //! many leading bytes matched. -//! * Truncating the MAC output below [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is rejected, per FIPS 140-2 -//! IG A.8 / NIST SP 800-107-r1 Section 5.3.3. That is a floor, not a recommendation -- RFC 2104 -//! Section 5 recommends that the output length "be not less than half the length of the hash -//! output ... and not less than 80 bits". +//! * Truncating the MAC output below [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is rejected, per FIPS 140-3 +//! IG C.L Resolution C.4 (formerly NIST SP 800-107r1 Section 5.3.3). That is a floor, not a +//! recommendation -- RFC 2104 Section 5 recommends that the output length "be not less than half +//! the length of the hash output ... and not less than 80 bits". //! * Resuming a suspended HMAC with the wrong key cannot be detected and silently produces a wrong //! MAC. //! * SM3 is a Merkle-Damgard construction and so is subject to length extension; `SM3(k || m)` is From d921046166505c8310b2c7e05db5da0f037115f3 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 18:27:22 +1100 Subject: [PATCH 08/57] ascon: into_squeezer ends the absorb phase there, through a new Sponge::finish_absorb that pads, clears the buffered message bytes and marks the sponge as squeezing with nothing buffered, so an AsconXof128Squeezer or AsconCXof128Squeezer suspended before its first output now resumes as a squeezer, where its state (not yet squeezing) was refused by the squeezer's from_suspended and accepted by the absorbing type's, the open item of the 2026-10-31 suspended-state audit; the output stream is unchanged, the first squeeze still beginning with the permutation, and AsconHash256 uses the same entry; the suspended-state layout is unchanged; tests for both XOFs fail on the old code; cargo mutants: 702 mutants, 596 caught, 98 unviable, 8 missed, the equivalents the review lists Assisted-by: Claude:claude-opus-5-5 --- crypto/ascon/src/ascon_cxof128.rs | 16 ++++++---------- crypto/ascon/src/ascon_hash256.rs | 2 +- crypto/ascon/src/ascon_xof128.rs | 16 ++++++---------- crypto/ascon/src/sponge.rs | 20 ++++++++++++++------ crypto/ascon/tests/cxof128_tests.rs | 12 ++++++++++++ crypto/ascon/tests/xof128_tests.rs | 10 ++++++++++ 6 files changed, 49 insertions(+), 27 deletions(-) diff --git a/crypto/ascon/src/ascon_cxof128.rs b/crypto/ascon/src/ascon_cxof128.rs index 0f1a76e51..d3ca3be3a 100644 --- a/crypto/ascon/src/ascon_cxof128.rs +++ b/crypto/ascon/src/ascon_cxof128.rs @@ -86,17 +86,10 @@ impl AsconCXof128 { Ok(Self { sponge }) } - /// Produces `output.len()` bytes from the XOF stream. - /// - /// The first call ends the message-absorb phase by padding and absorbing the - /// final message block. Subsequent calls continue the same output stream. + /// Produces the next `output.len()` bytes of the XOF stream; the absorb phase was ended by + /// [`XOF::into_squeezer`]. fn squeeze_into(&mut self, output: &mut [u8]) -> usize { output.fill(0); - - if !self.sponge.squeezing() { - self.sponge.pad_and_absorb(); - } - self.sponge.squeeze(output); output.len() } @@ -217,7 +210,10 @@ impl Hash for AsconCXof128 { impl XOF for AsconCXof128 { type Squeezer = AsconCXof128Squeezer; - fn into_squeezer(self) -> Self::Squeezer { + fn into_squeezer(mut self) -> Self::Squeezer { + // The squeezer is squeezing from the start, so a squeezer suspended before its first + // output resumes as a squeezer. + self.sponge.finish_absorb(); AsconCXof128Squeezer { xof: self } } diff --git a/crypto/ascon/src/ascon_hash256.rs b/crypto/ascon/src/ascon_hash256.rs index 8ede9154d..2f906febc 100644 --- a/crypto/ascon/src/ascon_hash256.rs +++ b/crypto/ascon/src/ascon_hash256.rs @@ -35,7 +35,7 @@ impl AsconHash256 { // Algorithm 5). The 32-byte digest is exactly RATE * 4 bytes, so a single generic // `Sponge::squeeze()` call over the whole output produces all four blocks with no leftover. fn squeeze_into(&mut self, output: &mut [u8; DIGEST_BYTES]) { - self.sponge.pad_and_absorb(); + self.sponge.finish_absorb(); self.sponge.squeeze(output); } } diff --git a/crypto/ascon/src/ascon_xof128.rs b/crypto/ascon/src/ascon_xof128.rs index 0d27ff2b1..afeb3dc73 100644 --- a/crypto/ascon/src/ascon_xof128.rs +++ b/crypto/ascon/src/ascon_xof128.rs @@ -39,17 +39,10 @@ impl AsconXof128 { } } - /// Produces `output.len()` bytes from the XOF stream. - /// - /// The first call ends the absorb phase by padding and absorbing the final - /// message block. Subsequent calls continue the same output stream. + /// Produces the next `output.len()` bytes of the XOF stream; the absorb phase was ended by + /// [`XOF::into_squeezer`]. fn squeeze_into(&mut self, output: &mut [u8]) -> usize { output.fill(0); - - if !self.sponge.squeezing() { - self.sponge.pad_and_absorb(); - } - self.sponge.squeeze(output); output.len() } @@ -170,7 +163,10 @@ impl Hash for AsconXof128 { impl XOF for AsconXof128 { type Squeezer = AsconXof128Squeezer; - fn into_squeezer(self) -> Self::Squeezer { + fn into_squeezer(mut self) -> Self::Squeezer { + // The squeezer is squeezing from the start, so a squeezer suspended before its first + // output resumes as a squeezer. + self.sponge.finish_absorb(); AsconXof128Squeezer { xof: self } } diff --git a/crypto/ascon/src/sponge.rs b/crypto/ascon/src/sponge.rs index c1618b6d6..60be607d8 100644 --- a/crypto/ascon/src/sponge.rs +++ b/crypto/ascon/src/sponge.rs @@ -122,15 +122,23 @@ impl Sponge { self.s[0] ^= 0x01u64 << final_bits; } - /// Squeeze `output.len()` bytes. May be called multiple times; the first call must follow - /// [`Sponge::pad_and_absorb`] and ends the absorb phase. + /// Ends the absorb phase: pads the final block and marks the sponge as squeezing with no + /// squeezed bytes buffered, so the first [`Sponge::squeeze`] begins with the permutation. The + /// buffered message bytes are cleared, since nothing reads them again. + pub(crate) fn finish_absorb(&mut self) { + self.pad_and_absorb(); + self.buf.fill(0); + self.buf_pos = RATE; + self.squeezing = true; + } + + /// Squeeze `output.len()` bytes. May be called multiple times, after + /// [`Sponge::finish_absorb`]. pub(crate) fn squeeze(&mut self, output: &mut [u8]) { + debug_assert!(self.squeezing, "squeeze before finish_absorb"); let mut output = output; - if !self.squeezing { - self.squeezing = true; - self.buf_pos = RATE; - } else if self.buf_pos < RATE { + if self.buf_pos < RATE { let available = RATE - self.buf_pos; if output.len() <= available { let end_pos = self.buf_pos + output.len(); diff --git a/crypto/ascon/tests/cxof128_tests.rs b/crypto/ascon/tests/cxof128_tests.rs index d7dee72c2..52d9b61dd 100644 --- a/crypto/ascon/tests/cxof128_tests.rs +++ b/crypto/ascon/tests/cxof128_tests.rs @@ -345,6 +345,18 @@ fn cxof128_suspendable_state() { combined.extend_from_slice(&tail); assert_eq!(combined, expected, "resuming mid-squeeze must continue the same output stream"); + // A squeezer suspended before its first output is a squeezing state too: it resumes as the + // squeezer, not as the absorbing type, and continues the same stream. + let mut fresh = AsconCXof128::with_customization(z).unwrap(); + fresh.do_update(&data); + let fresh_state = fresh.into_squeezer().suspend(); + assert!(matches!( + AsconCXof128::from_suspended(fresh_state), + Err(SuspendableError::InvalidData) + )); + let mut out = [0u8; 40]; + AsconCXof128Squeezer::from_suspended(fresh_state).unwrap().do_output_out(&mut out); + assert_eq!(out, expected, "a squeezer resumed before its first output must give the stream"); } #[test] diff --git a/crypto/ascon/tests/xof128_tests.rs b/crypto/ascon/tests/xof128_tests.rs index aea51f4c7..5701ac5b0 100644 --- a/crypto/ascon/tests/xof128_tests.rs +++ b/crypto/ascon/tests/xof128_tests.rs @@ -301,4 +301,14 @@ fn xof128_suspendable_state() { combined.extend_from_slice(&tail); assert_eq!(combined, expected, "resuming mid-squeeze must continue the same output stream"); + + // A squeezer suspended before its first output is a squeezing state too: it resumes as the + // squeezer, not as the absorbing type, and continues the same stream. + let mut fresh = AsconXof128::new(); + fresh.do_update(&data); + let fresh_state = fresh.into_squeezer().suspend(); + assert!(matches!(AsconXof128::from_suspended(fresh_state), Err(SuspendableError::InvalidData))); + let mut out = [0u8; 40]; + AsconXof128Squeezer::from_suspended(fresh_state).unwrap().do_output_out(&mut out); + assert_eq!(out, expected, "a squeezer resumed before its first output must give the stream"); } From fda953dcf5d28d2369baea7bf165a27489fda733 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 19:10:42 +1100 Subject: [PATCH 09/57] core: KeyMaterial::from_rng sets its metadata through the setters inside its window, the strength at min(RNG strength, what KEY_LEN bytes carry) per SP 800-90A r1 Section 8.4, so the one guard in set_security_strength covers it, where it wrote the three fields directly after the window (core review C1, whose cap the key-object rework had already added); a KeyMaterial<0> from an RNG is Zeroized rather than CryptographicRandom; set_key_type and zeroize drop the strength to None whenever the key becomes Zeroized or Unknown, as KeyType::Unknown's doc says it must, which removes the reachable panic in truncate (from_rng, then set_key_type(Unknown), then truncate) and turns its unwrap into an expect with a true justification; KeyMaterial's Clone is written out so a clone taken inside a do_hazardous_operations window has the window closed, where the derived Clone copied the open flag for good; cargo mutants on key_material.rs with the core and hmac tests: 129 mutants, 94 caught, 28 unviable, 7 missed, none in changed code (five KeyType TryFrom arms and allows_hazardous_operations untested before, truncate's > as >= equivalent) Assisted-by: Claude:claude-opus-5-5 --- crypto/core/src/key_material.rs | 108 +++++++++++------------- crypto/core/tests/key_material_tests.rs | 84 ++++++++++++++++++ 2 files changed, 135 insertions(+), 57 deletions(-) diff --git a/crypto/core/src/key_material.rs b/crypto/core/src/key_material.rs index ea41754e3..ebc05a219 100644 --- a/crypto/core/src/key_material.rs +++ b/crypto/core/src/key_material.rs @@ -221,7 +221,6 @@ pub trait KeyMaterialTrait: KeyMaterialInternalTrait { /// A wrapper for holding bytes-like key material (symmetric keys or seeds) which aims to apply a /// strict typing system to prevent many kinds of mis-use mistakes. /// The capacity of the internal buffer can be set at compile-time via the param. -#[derive(Clone)] pub struct KeyMaterial { buf: Secret<[u8; KEY_LEN]>, key_len: Secret, @@ -230,6 +229,20 @@ pub struct KeyMaterial { allow_hazardous_operations: bool, } +/// A clone never carries an open hazardous-operations window: the window belongs to the +/// `do_hazardous_operations` call that opened it, which closes only the original. +impl Clone for KeyMaterial { + fn clone(&self) -> Self { + Self { + buf: self.buf.clone(), + key_len: self.key_len.clone(), + key_type: self.key_type, + security_strength: self.security_strength, + allow_hazardous_operations: false, + } + } +} + // The explicit `#[repr(u8)]` discriminants are the stable on-the-wire encoding used by // `SerializableState` implementations (see the `TryFrom` impl below). Pin each value to its // variant name: reordering variants is fine, but never reuse or renumber an existing discriminant, @@ -305,23 +318,27 @@ impl KeyMaterial { } /// Creates a new instance of KeyMaterial containing random bytes from the provided random number generator. + /// + /// The key is tagged at the lesser of the RNG's strength and what `KEY_LEN` bytes can carry; + /// a `KeyMaterial<0>` comes back [`KeyType::Zeroized`]. pub fn from_rng(rng: &mut (impl RNG + ?Sized)) -> Result { let mut key = Self::new(); + if KEY_LEN == 0 { + return Ok(key); + } do_hazardous_operations(&mut key, |key| { - rng.next_bytes_out(&mut key.ref_to_bytes_mut().unwrap()) + rng.next_bytes_out(key.ref_to_bytes_mut()?) .map_err(|_| KeyMaterialError::GenericError("RNG failed."))?; - Ok(()) + key.set_key_len(KEY_LEN)?; + key.set_key_type(KeyType::CryptographicRandom)?; + // SP 800-90A r1 Section 8.4: "Security_strength_of_output = min(output_length, + // DRBG_security_strength)". + key.set_security_strength(*min( + &rng.security_strength(), + &SecurityStrength::from_bytes(KEY_LEN), + )) })?; - - *key.key_len = KEY_LEN; - key.key_type = KeyType::CryptographicRandom; - // The RNG's strength, capped at what `KEY_LEN` bytes can carry: a 16-byte key drawn from - // a 256-bit DRBG is a 128-bit key, and `set_security_strength` would refuse the higher - // figure. - let by_len = SecurityStrength::from_bits(KEY_LEN * 8); - let by_rng = rng.security_strength(); - key.security_strength = if by_rng < by_len { by_rng } else { by_len }; Ok(key) } @@ -461,49 +478,24 @@ impl KeyMaterialTrait for KeyMaterial { } fn set_key_type(&mut self, key_type: KeyType) -> Result<(), KeyMaterialError> { - if self.allow_hazardous_operations { - // just do it - self.key_type = key_type; - return Ok(()); - } - - match self.key_type { - KeyType::Zeroized => { - return Err(KeyMaterialError::ActingOnZeroizedKey); - } - KeyType::CryptographicRandom => { + if !self.allow_hazardous_operations { + match self.key_type { + KeyType::Zeroized => return Err(KeyMaterialError::ActingOnZeroizedKey), // raw full entropy can be safely converted to anything. - self.key_type = key_type; - } - KeyType::Unknown => match key_type { - KeyType::Unknown => { /* No change */ } - _ => { - return Err(KeyMaterialError::HazardousOperationNotPermitted); - } - }, - KeyType::MACKey => match key_type { - KeyType::MACKey => { /* No change */ } - // Else: Once a KeyMaterial is typed, it should stay that way. - _ => { - return Err(KeyMaterialError::HazardousOperationNotPermitted); - } - }, - KeyType::SymmetricCipherKey => match key_type { - KeyType::SymmetricCipherKey => { /* No change */ } - // Else: Once a KeyMaterial is typed, it should stay that way. - _ => { - return Err(KeyMaterialError::HazardousOperationNotPermitted); - } - }, - KeyType::Seed => match key_type { - KeyType::Seed => { /* No change */ } - // Else: Once a KeyMaterial is typed, it should stay that way. - _ => { + KeyType::CryptographicRandom => {} + // Else: once a KeyMaterial is typed, it should stay that way. + current if current != key_type => { return Err(KeyMaterialError::HazardousOperationNotPermitted); } - }, + _ => { /* No change */ } + } } + self.key_type = key_type; + // A Zeroized or Unknown key has no security strength (see `KeyType::Unknown`). + if key_type <= KeyType::Unknown { + self.security_strength = SecurityStrength::None; + } Ok(()) } @@ -576,6 +568,7 @@ impl KeyMaterialTrait for KeyMaterial { self.buf.zeroize(); self.key_len.zeroize(); self.key_type = KeyType::Zeroized; + self.security_strength = SecurityStrength::None; } fn equals(&self, other: &dyn KeyMaterialTrait) -> bool { @@ -599,16 +592,17 @@ impl KeyMaterialTrait for KeyMaterial { // set the metadata into.set_key_len(bytes_to_copy)?; into.set_key_type(self.key_type)?; - into.set_security_strength( - min(&self.security_strength(), &SecurityStrength::from_bytes(bytes_to_copy)) - .clone(), - )?; + into.set_security_strength(*min( + &self.security_strength(), + &SecurityStrength::from_bytes(bytes_to_copy), + ))?; Ok(()) }) - // Swallow all errors generated inside the hazardous operations closure. - // The set_* calls here are all infallible inside the hazardous closure. - .unwrap(); + // Infallible: inside the window the setters refuse only a strength the key cannot carry. + // The strength is capped at what `bytes_to_copy` bytes carry, and a Zeroized or Unknown + // source has strength None, because set_key_type and zeroize keep it so. + .expect("truncate's metadata is consistent by construction"); } } diff --git a/crypto/core/tests/key_material_tests.rs b/crypto/core/tests/key_material_tests.rs index dea104962..da5b605f3 100644 --- a/crypto/core/tests/key_material_tests.rs +++ b/crypto/core/tests/key_material_tests.rs @@ -233,6 +233,90 @@ mod test_key_material { assert_eq!(key.security_strength(), rng_strength, "64 bytes keep the RNG's strength"); } + /// SP 800-90A r1 Section 8.4: the strength of a draw is the lesser of the RNG's and the + /// draw's length, so a short key from a 256-bit RNG is tagged at what its length carries. + #[test] + fn from_rng_strength_is_capped_by_the_key_length() { + use bouncycastle_rng as rng; + + let mut rng = rng::DefaultRNG::default(); + use bouncycastle_core::traits::RNG; + assert_eq!(rng.security_strength(), SecurityStrength::_256bit); + let k32 = KeyMaterial256::from_rng(&mut rng).unwrap(); + assert_eq!(k32.security_strength(), SecurityStrength::_256bit); + let k24 = KeyMaterial::<24>::from_rng(&mut rng).unwrap(); + assert_eq!(k24.security_strength(), SecurityStrength::_192bit); + let k16 = KeyMaterial128::from_rng(&mut rng).unwrap(); + assert_eq!(k16.security_strength(), SecurityStrength::_128bit); + let k14 = KeyMaterial::<14>::from_rng(&mut rng).unwrap(); + assert_eq!(k14.security_strength(), SecurityStrength::_112bit); + let k13 = KeyMaterial::<13>::from_rng(&mut rng).unwrap(); + assert_eq!(k13.key_type(), KeyType::CryptographicRandom); + assert_eq!(k13.security_strength(), SecurityStrength::None); + let k0 = KeyMaterial0::from_rng(&mut rng).unwrap(); + assert_eq!(k0.key_len(), 0); + assert_eq!(k0.key_type(), KeyType::Zeroized); + assert_eq!(k0.security_strength(), SecurityStrength::None); + } + + /// A key re-typed or zeroized to Zeroized/Unknown carries no strength, so truncating it is + /// fine rather than a panic. + #[test] + fn retyping_to_unknown_drops_the_strength() { + use bouncycastle_rng as rng; + + let mut key = KeyMaterial256::from_rng(&mut rng::DefaultRNG::default()).unwrap(); + key.set_key_type(KeyType::Unknown).unwrap(); + assert_eq!(key.security_strength(), SecurityStrength::None); + let mut short = KeyMaterial128::new(); + key.clone().truncate(&mut short); + assert_eq!(short.key_type(), KeyType::Unknown); + assert_eq!(short.security_strength(), SecurityStrength::None); + assert_eq!(short.ref_to_bytes(), &key.ref_to_bytes()[..16]); + + // The same inside a hazardous window. + let mut key = KeyMaterial256::from_rng(&mut rng::DefaultRNG::default()).unwrap(); + do_hazardous_operations(&mut key, |k| k.set_key_type(KeyType::Unknown)).unwrap(); + assert_eq!(key.security_strength(), SecurityStrength::None); + + // And zeroize. + let mut key = KeyMaterial256::from_rng(&mut rng::DefaultRNG::default()).unwrap(); + key.zeroize(); + assert_eq!(key.security_strength(), SecurityStrength::None); + + // A typed key still refuses to be re-typed outside a window. + let mut key = + KeyMaterial256::from_bytes_as_type(&DUMMY_KEY[..32], KeyType::MACKey).unwrap(); + assert!(matches!( + key.set_key_type(KeyType::Unknown), + Err(KeyMaterialError::HazardousOperationNotPermitted) + )); + assert_eq!(key.security_strength(), SecurityStrength::_256bit); + } + + /// The window is closed on a clone taken inside it. + #[test] + fn a_clone_taken_inside_a_window_has_it_closed() { + let mut key = + KeyMaterial256::from_bytes_as_type(&DUMMY_KEY[..32], KeyType::MACKey).unwrap(); + let mut copy = None; + do_hazardous_operations(&mut key, |k| { + copy = Some(k.clone()); + Ok(()) + }) + .unwrap(); + let mut copy = copy.unwrap(); + assert!(matches!( + copy.set_key_type(KeyType::Seed), + Err(KeyMaterialError::HazardousOperationNotPermitted) + )); + assert!(matches!( + copy.ref_to_bytes_mut(), + Err(KeyMaterialError::HazardousOperationNotPermitted) + )); + assert_eq!(copy.ref_to_bytes(), key.ref_to_bytes()); + } + #[test] fn zeroize() { let mut key = KeyMaterial256::from_bytes(&DUMMY_KEY[..32]).unwrap(); From 967ae572c2c9635d47ed07fb7bb54104ca8476f3 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 23:43:29 +1100 Subject: [PATCH 10/57] hmac: a test that HmacKey::::generate works with the default 256-bit RNG, end to end: the merged key-object rework already tags the 28-byte draw at 192 bits (core review C2) and pins that rule on KeyMaterial, but no test generated and used an HMAC-SHA224 key Assisted-by: Claude:claude-opus-5-5 --- crypto/hmac/tests/hmac_key_tests.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crypto/hmac/tests/hmac_key_tests.rs b/crypto/hmac/tests/hmac_key_tests.rs index d30cbd02f..db460a95c 100644 --- a/crypto/hmac/tests/hmac_key_tests.rs +++ b/crypto/hmac/tests/hmac_key_tests.rs @@ -8,7 +8,7 @@ use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey, Mac use bouncycastle_core::traits::{MAC, RNG, SuspendableKeyed}; use bouncycastle_hmac::{HMAC, HMACParams, HmacKey}; use bouncycastle_rng::FixedSeedRNG; -use bouncycastle_sha2::{SHA256, SHA512, SUSPENDED_SHA256_STATE_LEN}; +use bouncycastle_sha2::{SHA224, SHA256, SHA512, SUSPENDED_SHA256_STATE_LEN}; use bouncycastle_sha3::SHA3_256; fn mac_key(fill: u8) -> KeyMaterial { @@ -176,6 +176,14 @@ fn generate_draws_a_key_of_the_hash_output_length_at_the_rng_strength() { HmacKey::::generate(&mut weak_rng), Err(MACError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) )); + + // A hash output shorter than the RNG's strength: the 28-byte SHA-224 key is tagged at what + // 28 bytes carry rather than refused for claiming the RNG's 256 bits. + let key = HmacKey::::generate(&mut FixedSeedRNG::new([0x5Au8; 32])) + .expect("a 256-bit RNG suffices for HMAC-SHA224"); + let tag = key.mac(MSG); + assert_eq!(tag.len(), 28); + assert!(key.verify(MSG, &tag)); } #[test] From bdb5f04e6c24eb914f6d55e97b4eaed0b3542c8b Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 19:12:38 +1100 Subject: [PATCH 11/57] base64: Base64Decoder::do_final refuses input that ends one symbol into a block with the new Base64Error::TruncatedInput, where it built two bytes from whatever an earlier block had left in the decoder (decode("QUJDR") gave [65, 66, 67, 69, 66]; RFC 4648 Section 4 allows a final quantum of two or three symbols only); padding is accepted only after two or three symbols of a block and may be followed only by the rest of that padding, and whitespace when skipped, anything else being the new Base64Error::InvalidPadding(index), where everything after the first "=" was silently dropped, a second message included (decode("QQ==QQ==") gave [65]; RFC 4648 Section 3.3: "MUST reject the encoded data if it contains characters outside the base alphabet"); missing padding is still tolerated; tests with RFC 4648 Section 10's vectors, padded, unpadded and with whitespace, and the reviewer's cases; cargo mutants: 136 mutants, 117 caught, 8 unviable, 3 timeouts (loop counters), 8 missed, all | as ^ on disjoint shifted bits Assisted-by: Claude:claude-opus-5-5 --- crypto/base64/src/lib.rs | 66 ++++++++++++++++++++------- crypto/base64/tests/base64_tests.rs | 71 +++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 16 deletions(-) diff --git a/crypto/base64/src/lib.rs b/crypto/base64/src/lib.rs index a6a4350f9..be2ad6a98 100644 --- a/crypto/base64/src/lib.rs +++ b/crypto/base64/src/lib.rs @@ -109,6 +109,14 @@ pub enum Base64Error { /// Input contained a character that was not in the base64 alphabet. The index of the illegal character is included in the output. InvalidB64Character(usize), + + /// Padding where the data cannot end, or something other than the rest of the padding (and, + /// when whitespace is skipped, whitespace) after it. The index of the offending character in + /// the input to [`Base64Decoder::do_final`] is included. + InvalidPadding(usize), + + /// The input ended one symbol into a block: six bits, which encode no whole byte. + TruncatedInput, } /// The stateful base64 encoder that supports streaming. @@ -255,14 +263,16 @@ impl Base64Decoder { /// Streaming API that performs Base64 encoding of the provided input, but does not apply /// the final padding and will hold an incomplete block while waiting for more input. pub fn do_update>(&mut self, input: T) -> Result, Base64Error> { - self.decode_internal(input, true) + self.decode_internal(input, true).map(|(out, _)| out) } + /// Decodes up to the first padding character, returning the output and, if there was one, the + /// padding character's index. fn decode_internal>( &mut self, input: T, rollback_if_padding: bool, - ) -> Result, Base64Error> { + ) -> Result<(Vec, Option), Base64Error> { // copy the current state so that we can restore it if we encounter a padding character. let starting_state: [u8; 4] = self.buf.clone(); let starting_vals_in_block: usize = self.vals_in_buf; @@ -296,7 +306,7 @@ impl Base64Decoder { self.vals_in_buf = starting_vals_in_block; return Err(Base64Error::PaddingEncounteredDuringDoUpdate); } - return Ok(out); + return Ok((out, Some(i))); } i += 1; @@ -313,14 +323,20 @@ impl Base64Decoder { } } - Ok(out) + Ok((out, None)) } /// As can be expected, do_final() consumes the object. + /// + /// Missing padding is tolerated, but padding must come where the data can end, after two or + /// three symbols of a block, and may be followed only by the rest of that padding (and + /// whitespace, when it is skipped): anything else is [`Base64Error::InvalidPadding`]. Input + /// ending one symbol into a block is [`Base64Error::TruncatedInput`]. pub fn do_final>(mut self, input: T) -> Result, Base64Error> { + let input = input.as_ref(); // process as much as we can the usual way. - let mut out = match self.decode_internal(input, false) { - Ok(out) => out, + let (mut out, pad_at) = match self.decode_internal(input, false) { + Ok(decoded) => decoded, Err(Base64Error::PaddingEncounteredDuringDoUpdate) => { panic!( "rollback_if_padding = false should not produce a Base64Error::PaddingEncounteredDuringDoUpdate" @@ -329,19 +345,37 @@ impl Base64Decoder { Err(e) => return Err(e), }; - // now, a single block containing padding remains to be dealt with. - if self.vals_in_buf != 0 { - // be tolerant of missing padding - // if it is not a complete block at the end, the infer the byte count from the number of leftover symbols - let pad_count: u8 = 3 - (self.vals_in_buf as u8 - 1); + // RFC 4648 Section 4: a final quantum of 8 bits is two symbols and "==", of 16 bits three + // symbols and "=". Section 3.3: implementations "MUST reject the encoded data if it + // contains characters outside the base alphabet", which includes anything after the + // padding, such as a second message. + if let Some(pad_at) = pad_at { + let pads_due = match self.vals_in_buf { + 2 => 2, + 3 => 1, + _ => return Err(Base64Error::InvalidPadding(pad_at)), + }; + let mut pads = 0; + for (offset, &c) in input[pad_at..].iter().enumerate() { + // Only padding and whitespace may follow, both public, so branching is fine. + match Self::ct_b64_to_bin(c) { + 0x81 if pads < pads_due => pads += 1, + 0x80 if self.skip_whitespace => {} + _ => return Err(Base64Error::InvalidPadding(pad_at + offset)), + } + } + } - out.push(self.buf[0] << 2 | self.buf[1] >> 4); - if pad_count != 2 { + // now, a single block containing padding remains to be dealt with. + match self.vals_in_buf { + 0 => {} + // Six bits encode no whole byte. + 1 => return Err(Base64Error::TruncatedInput), + 2 => out.push(self.buf[0] << 2 | self.buf[1] >> 4), + _ => { + out.push(self.buf[0] << 2 | self.buf[1] >> 4); out.push(self.buf[1] << 4 | self.buf[2] >> 2); } - if pad_count == 0 { - out.push(self.buf[2] << 6 | self.buf[3]); - } } Ok(out) diff --git a/crypto/base64/tests/base64_tests.rs b/crypto/base64/tests/base64_tests.rs index aa700dd62..f919ebc55 100644 --- a/crypto/base64/tests/base64_tests.rs +++ b/crypto/base64/tests/base64_tests.rs @@ -122,3 +122,74 @@ fn do_update_refuses_padding_and_leaves_the_state_restorable() { assert!(matches!(decoder.do_update("=="), Err(Base64Error::PaddingEncounteredDuringDoUpdate))); assert_eq!(decoder.do_final("==").unwrap(), b"D"); } + +/// RFC 4648 Section 10's test vectors, each also with its padding dropped (which `do_final` +/// tolerates) and with whitespace around and inside it. The two- and three-symbol tails carry +/// low bits in their last sextet. +#[test] +fn rfc4648_section_10_vectors() { + let vectors: [(&[u8], &str); 7] = [ + (b"", ""), + (b"f", "Zg=="), + (b"fo", "Zm8="), + (b"foo", "Zm9v"), + (b"foob", "Zm9vYg=="), + (b"fooba", "Zm9vYmE="), + (b"foobar", "Zm9vYmFy"), + ]; + for (raw, enc) in vectors { + assert_eq!(base64::encode(raw), enc); + assert_eq!(base64::decode(enc).unwrap(), raw, "{enc}"); + assert_eq!(base64::decode(enc.trim_end_matches('=')).unwrap(), raw, "{enc} unpadded"); + let spaced = format!(" {} \n", enc.replace('=', " = ")); + assert_eq!(base64::decode(&spaced).unwrap(), raw, "{spaced:?}"); + } +} + +/// A block of 4k + 1 symbols ends six bits into a byte, which is no whole byte: it is refused +/// rather than finished from whatever an earlier block left in the decoder. +#[test] +fn a_single_trailing_symbol_is_truncated_input() { + for input in ["R", "QUJDR", "QUJDR ", "QUJD R", "R="] { + assert!( + matches!( + base64::decode(input), + Err(base64::Base64Error::TruncatedInput | base64::Base64Error::InvalidPadding(_)) + ), + "{input:?}" + ); + } + assert!(matches!(base64::decode("QUJDR"), Err(base64::Base64Error::TruncatedInput))); +} + +/// Padding ends the data: RFC 4648 Section 3.3 requires rejecting anything after it other than +/// the rest of the padding, and padding is only valid after two or three symbols of a block. +#[test] +fn nothing_but_padding_and_whitespace_may_follow_the_padding() { + use base64::Base64Error::InvalidPadding; + + // (input, index of the offending character) + let cases = [ + ("QQ==garbage!!", 4), + ("QQ==QQ==", 4), + ("QUJD=RA==", 4), + ("QQ===", 4), + ("QUI==", 4), + ("=", 0), + ("QUJD=", 4), + ("Q=", 1), + ]; + for (input, at) in cases { + match base64::decode(input) { + Err(InvalidPadding(i)) => assert_eq!(i, at, "{input:?}"), + other => panic!("{input:?}: expected InvalidPadding({at}), got {other:?}"), + } + } + + // Whitespace after the padding is fine only when whitespace is skipped. + assert_eq!(base64::decode("QQ== \n").unwrap(), b"A"); + assert!(matches!( + Base64Decoder::new(false).do_final("QQ==\n"), + Err(base64::Base64Error::InvalidPadding(4)) + )); +} From ade7b31e9bbca4846f3920e132f1beb1cc916e74 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 19:30:39 +1100 Subject: [PATCH 12/57] sha2: SHA512t's check_t now also runs in OUTPUT_LEN and ALG_NAME, and from_suspended evaluates H0, so an invalid t is a compile error on the resume path too, where SHA512t::<384>, <0> and <512>::from_suspended built and hashed (<384> giving SHA-512 truncated to 384 bits under the name SHA512/384, which FIPS 180-4 Section 5.3.6 excludes) and <520> resumed and panicked in do_final; a compile_fail doctest on the resume path, which compiles against the old code, and the docs say the error comes at code generation; SHA-224/256 from_suspended refuses a byte_count above 2^61 - 1, the longest message the 64-bit length block encodes, where a state claiming 2^61 bytes finished to the empty-message digest; cargo mutants: 427 mutants, 344 caught, 61 unviable, 7 timeouts (loop counters as *=), 15 missed: 10 annotated | / ^ equivalents and the five SUSPENDED_HKDF_* "+ 14" constants the review lists, the MAX_MESSAGE_BYTES pair now caught Assisted-by: Claude:claude-opus-5-5 --- crypto/sha2/src/lib.rs | 24 ++++++++++++++++++++---- crypto/sha2/src/sha256.rs | 5 +++++ crypto/sha2/src/sha512.rs | 4 ++++ crypto/sha2/tests/sha2_tests.rs | 13 +++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/crypto/sha2/src/lib.rs b/crypto/sha2/src/lib.rs index a44e7f01c..db6003723 100644 --- a/crypto/sha2/src/lib.rs +++ b/crypto/sha2/src/lib.rs @@ -195,7 +195,8 @@ pub type SHA512 = SHA512Internal; /// initial hash value rather than one produced by the IV Generation Function below. Letting /// `T = 384` through here would derive a second, different 384-bit hash under a name already /// taken, so the standard reserves 384 for SHA-384 instead. Anything else is a compile error -/// naming the rule it broke: +/// naming the rule it broke, raised when the code using it is generated (`cargo build`, not +/// `cargo check`), whether the value is constructed or resumed: /// /// ```compile_fail /// use bouncycastle_sha2::SHA512t; @@ -204,6 +205,14 @@ pub type SHA512 = SHA512Internal; /// ``` /// /// ```compile_fail +/// use bouncycastle_core::traits::Suspendable; +/// use bouncycastle_sha2::{SHA512, SHA512t}; +/// // The same rule on the resume path, which never computes an initial hash value. +/// let state = SHA512::new().suspend(); +/// let _ = SHA512t::<384>::from_suspended(state); +/// ``` +/// +/// ```compile_fail /// use bouncycastle_sha2::SHA512t; /// // Not a whole number of bytes: this crate requires t to be a multiple of 8. /// let _ = SHA512t::<100>::new(); @@ -418,7 +427,10 @@ pub struct SHA512tParams; impl SHA512tParams { /// `"SHA512/t"` with `T` in decimal, NUL-padded; see [`Self::ALG_NAME_STR`]. - const ALG_NAME_BYTES: [u8; sha512::ALG_NAME_BUF_LEN] = sha512::alg_name_bytes(T); + const ALG_NAME_BYTES: [u8; sha512::ALG_NAME_BUF_LEN] = { + sha512::check_t(T); + sha512::alg_name_bytes(T) + }; /// The algorithm name, e.g. `"SHA512/224"`. Built at compile time from `T` because a const /// generic cannot be formatted into a `&'static str` directly. @@ -442,8 +454,12 @@ impl Algorithm for SHA512tParams { } impl HashAlgParams for SHA512tParams { /// FIPS 180-4 s. 6.6 / s. 6.7 exception 2: truncated to the left-most `T` bits. `T` is a - /// multiple of 8 (checked by `check_t` in `sha512.rs`), so this is exact. - const OUTPUT_LEN: usize = T / 8; + /// multiple of 8, so this is exact. `check_t` runs here as well as in `H0`, so that a value + /// resumed from a suspended state, which never reads `H0`, is held to the same rules. + const OUTPUT_LEN: usize = { + sha512::check_t(T); + T / 8 + }; const BLOCK_LEN: usize = 128; // FIPS 180-4 Figure 1: block size 1024 bits } impl SHA512InitValue for SHA512tParams { diff --git a/crypto/sha2/src/sha256.rs b/crypto/sha2/src/sha256.rs index 7b59d25a9..cfe22dd28 100644 --- a/crypto/sha2/src/sha256.rs +++ b/crypto/sha2/src/sha256.rs @@ -424,6 +424,11 @@ impl Suspendable for SHA256 if byte_count % 64 != x_buf_off as u64 { return Err(SuspendableError::InvalidData); } + // FIPS 180-4 s. 5.1.1 encodes the length in a 64-bit block, so a longer message cannot be + // finished correctly. + if byte_count > MAX_MESSAGE_BYTES { + return Err(SuspendableError::InvalidData); + } // Construct the object let state = Sha256State { _params: core::marker::PhantomData, h }; diff --git a/crypto/sha2/src/sha512.rs b/crypto/sha2/src/sha512.rs index 5e205843a..c08dc0b13 100644 --- a/crypto/sha2/src/sha512.rs +++ b/crypto/sha2/src/sha512.rs @@ -541,6 +541,10 @@ impl Suspendable for SHA512 fn from_suspended( serialized_state: [u8; SUSPENDED_SHA512_STATE_LEN], ) -> Result { + // Resuming builds the state without the initial hash value, so evaluate it here: for + // SHA-512/t that is what runs `check_t`, which makes an invalid `t` a compile error on + // this path too. + const { _ = PARAMS::H0 }; // check the version tag // At the moment, we have no not_before version to specify. // infallible: check_lib_ver returns a slice of exactly SUSPENDED_SHA512_STATE_LEN - 3 = 201 bytes. diff --git a/crypto/sha2/tests/sha2_tests.rs b/crypto/sha2/tests/sha2_tests.rs index 006a95d47..4394a829c 100644 --- a/crypto/sha2/tests/sha2_tests.rs +++ b/crypto/sha2/tests/sha2_tests.rs @@ -306,6 +306,19 @@ mod sha2_tests { _ => panic!("Expected an error"), } + // byte_count may not exceed 2^61 - 1 bytes, the longest message the 64-bit length block + // encodes (x_buf_off kept congruent so only the bound is tested). + let with_byte_count = |byte_count: u64| { + let mut state = serialized_state; + state[3 + 32..3 + 40].copy_from_slice(&byte_count.to_le_bytes()); + state[3 + 104] = (byte_count % 64) as u8; + SHA256::from_suspended(state) + }; + let max: u64 = (1 << 61) - 1; + assert!(with_byte_count(max).is_ok()); + assert!(matches!(with_byte_count(max + 1), Err(SuspendableError::InvalidData))); + assert!(matches!(with_byte_count(u64::MAX - 63), Err(SuspendableError::InvalidData))); + // SHA512 let mut sha512 = SHA512::new(); sha512.do_update(str.as_bytes()); From 996da8d2211452d223070a77784f5ed9d1adccaf Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 23:44:00 +1100 Subject: [PATCH 13/57] factory: MACFactory's 256-bit default (DEFAULT_256BIT_MAC_NAME, default_256_bit, "Default256Bit") is HMAC-SHA512, where it was HMAC-SHA256, which the library tags 128-bit and which accepted a 16-byte key that HMAC-SHA512 and KMAC256 refuse (factory review F1); a test that each default's worker meets its strength and that the 256-bit default refuses a 16-byte key; the review's F2 (MAC::new and new_allow_weak_key as unimplemented!() on the factory) is gone with the key-object rework, which took the constructors off the MAC trait Assisted-by: Claude:claude-opus-5-5 --- crypto/factory/src/mac_factory.rs | 8 +++---- crypto/factory/tests/mac_factory_tests.rs | 27 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/crypto/factory/src/mac_factory.rs b/crypto/factory/src/mac_factory.rs index b6df9e8b5..9500beca1 100644 --- a/crypto/factory/src/mac_factory.rs +++ b/crypto/factory/src/mac_factory.rs @@ -95,12 +95,12 @@ use bouncycastle_sm3 as sm3; use bouncycastle_sm3::hmac::HMAC_SM3_NAME; /*** Defaults ***/ -/// +/// The default MAC: HMAC-SHA256, a 128-bit MAC. pub const DEFAULT_MAC_NAME: &str = HMAC_SHA256_NAME; -/// +/// The default 128-bit MAC: HMAC-SHA256. pub const DEFAULT_128BIT_MAC_NAME: &str = HMAC_SHA256_NAME; -/// -pub const DEFAULT_256BIT_MAC_NAME: &str = HMAC_SHA256_NAME; +/// The default 256-bit MAC: HMAC-SHA512, as the 256-bit KDF default is HKDF-SHA512. +pub const DEFAULT_256BIT_MAC_NAME: &str = HMAC_SHA512_NAME; /// A MAC key selected by name: the key object of whichever algorithm was named, which hands out /// [`MACFactoryWorker`]s through [`MacGenerationKey`] and [`MacVerificationKey`]. diff --git a/crypto/factory/tests/mac_factory_tests.rs b/crypto/factory/tests/mac_factory_tests.rs index 76d1d9c34..7d0042211 100644 --- a/crypto/factory/tests/mac_factory_tests.rs +++ b/crypto/factory/tests/mac_factory_tests.rs @@ -8,6 +8,33 @@ mod hash_factory_tests { use bouncycastle_hex as hex; /// `AlgorithmInfo` answers for the algorithm the factory selected, not for the factory. + /// Each default is at least as strong as its name says, and so refuses a key below that. + #[test] + fn defaults_meet_their_strength() { + use bouncycastle_factory::mac_factory::{ + DEFAULT_128BIT_MAC_NAME, DEFAULT_256BIT_MAC_NAME, DEFAULT_MAC_NAME, + }; + let key32 = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::MACKey).unwrap(); + let key16 = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::MACKey).unwrap(); + + let k = MACFactory::default_256_bit(&key32).unwrap(); + let m = k.create_generator(); + assert_eq!(m.max_security_strength(), SecurityStrength::_256bit); + assert_eq!(m.alg_name(), DEFAULT_256BIT_MAC_NAME); + assert!(MACFactory::default_256_bit(&key16).is_err()); + assert!(MACFactory::new("Default256Bit", &key16).is_err()); + + let k = MACFactory::default_128_bit(&key16).unwrap(); + let m = k.create_generator(); + assert!(m.max_security_strength() >= SecurityStrength::_128bit); + assert_eq!(m.alg_name(), DEFAULT_128BIT_MAC_NAME); + + let k = MACFactory::default(&key16).unwrap(); + let m = k.create_generator(); + assert!(m.max_security_strength() >= SecurityStrength::_128bit); + assert_eq!(m.alg_name(), DEFAULT_MAC_NAME); + } + #[test] fn alg_name_and_strength_follow_the_variant() { use SecurityStrength::*; From 09cc458a3e36a9730601e79448faf24a778388f9 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 02:49:45 +1100 Subject: [PATCH 14/57] utils: ct::value_barrier is public, for a caller whose own arithmetic the optimiser would rewrite into a multiply on a secret (the AES round-key widening); the volatile store/load and its caveats are unchanged Assisted-by: Claude:claude-fable-5-1 --- crypto/utils/src/ct.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crypto/utils/src/ct.rs b/crypto/utils/src/ct.rs index b332e2b21..027894491 100644 --- a/crypto/utils/src/ct.rs +++ b/crypto/utils/src/ct.rs @@ -54,10 +54,16 @@ where // offer any guarantees for cryptographic or security purposes". // --------------------------------------------------------------------------------------------- -/// Returns `value` unchanged, via a volatile store to a stack slot and a volatile load back. The -/// section comment above says what that does and does not guarantee. +/// Returns `value` unchanged, via a volatile store to a stack slot and a volatile load back, so +/// the optimiser carries no facts about `value` across it: not its range, not which bits are +/// zero. Every constant-time construction in this module is built on it. Public for a caller +/// whose own arithmetic the optimiser would otherwise rewrite into something that is not +/// constant-time on every target; the AES crate widens a round-key word by shifts and ORs, which +/// LLVM turns into a multiply (a software routine on AVR and RISC-V without M) unless the word's +/// known-zero high bits are hidden first. Best effort, not a guarantee: see the section comment +/// above. #[inline(always)] -fn value_barrier(value: T) -> T { +pub fn value_barrier(value: T) -> T { let mut slot = value; // SAFETY: // * `&mut slot` must be a reference to an initialised, aligned `T` local on this stack frame, so From 7773e929750d21d1092e26ca77875ec40ba2c939 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 02:49:45 +1100 Subject: [PATCH 15/57] aes: the 2026-11-03 crate review (REVIEW-2026-11-03-aes-correctness-mythos.md, at 94d4e966) worked through: the round-key widening in the 2- and 4-block paths no longer folds into a multiply of a secret off x86-64/aarch64, expand takes the key array, the plane-agreement debug_assert goes, GCM_NONCE_LEN is re-exported, the CCM drawing in-place one-shot is tested, two doc nits A1 (hardening): `PlaneWord::splat` widens a round-key half-word into the 2- and 4-block lanes by shifts and ORs; LLVM knows the zero-extended word's high bits are zero, so it rewrites the disjoint ORs into a multiply by 0x0001_0001(_0001_0001). A no_std harness of the widening built with -Zbuild-std for five targets shows the fold as a `__mulsi3` / `__muldi3` call on AVR, `muls` / `__aeabi_lmul` on Cortex-M0 and `__muldi3` on RISC-V 32I (none on msp430 and x86-64; the review saw it on aarch64), i.e. a software multiply of key material where one is variable-time. Now the word passes through ct::value_barrier before the ORs, which hides the known-zero bits so the ORs stay ORs: the harness shows no multiply and no branch on any of the five. The barrier is skipped on x86-64 and aarch64 (a const, so both bodies compile everywhere), where the multiply is fixed-latency: with it, the 2-block path lost 60 % and the 4-block path 190 % of its throughput (one volatile round trip per widened word, 88 per 4-block call); without it the bench is within noise of before (AES-128 16 KiB: block 254 -> 267 us, 2 blocks 141 -> 143 us, 4 blocks 79 -> 78 us). The crate docs' constant-time section says which targets get which. A2: `expand` takes `&[u8; KEY_LEN]` with `KEY_LEN == P::KEY_LEN` asserted at compile time and reads the words with `as_chunks`, so the debug_assert on the length and the "cannot fail" unwrap go; the three constructors convert the validated slice once, mapping the impossible mismatch to InvalidLength rather than unwrapping. A3: the debug_assert over the eight broadcast planes in `sub_word` short-circuits on S-box output of key bytes in a debug build; dropped, the unit test already pins the property, and the docs now say debug builds are outside the constant-time claim. A5: `GCM_NONCE_LEN` is re-exported from the crate root beside CTR_NONCE_LEN, CCM_NONCE_LEN and CCM_TAG_LEN. A11: the inverse S-box costs 32 extra XORs (two inv_affine applications), not 16; the stack table is stated as x86-64 only. Mutants gap: `encrypt_detached_in_place(rng, ..)` on the CCM key was reached by no test (four survivors); `ccm_key_tests.rs` now checks it against the hazmat reference and the `_nonce` form under the drawn nonce at every length. Not changed: A4 (three naming grammars: a decision before the factory grows AES), A6/A7/A8/A9 (resolved by PR #172 and the from_rng fix on this branch: the bare constructors are pub(crate), MAC::new is gone, every mode has a key path), A10 (#[must_use], issue #74). Assisted-by: Claude:claude-fable-5-1 --- crypto/aes/src/bitslice.rs | 28 ++++++++++++----- crypto/aes/src/gcm.rs | 2 +- crypto/aes/src/hazmat/aes_internal.rs | 21 +++++++++++-- crypto/aes/src/lib.rs | 16 +++++++--- crypto/aes/src/sbox.rs | 2 +- crypto/aes/src/schedule.rs | 44 ++++++++++++++++----------- crypto/aes/tests/ccm_key_tests.rs | 19 ++++++++++++ 7 files changed, 99 insertions(+), 33 deletions(-) diff --git a/crypto/aes/src/bitslice.rs b/crypto/aes/src/bitslice.rs index dbf2fb6d1..b50455404 100644 --- a/crypto/aes/src/bitslice.rs +++ b/crypto/aes/src/bitslice.rs @@ -72,8 +72,14 @@ //! byte-lane, block B in the odd), which ties every mask to one width. Keeping each block in its //! own lane instead is what lets one set of `u16` patterns serve `u16`, `u32` and `u64` planes. +use bouncycastle_utils::ct; use core::ops::{BitAnd, BitOr, BitXor, BitXorAssign, Not, Shl, Shr}; +/// Targets whose integer multiply is fixed-latency, so the round-key widening in +/// [`PlaneWord::splat`] may be left for LLVM to fold into one. A `const` rather than a `cfg` on +/// the two bodies, so both compile on every target. +const MULTIPLY_IS_FIXED_LATENCY: bool = cfg!(any(target_arch = "x86_64", target_arch = "aarch64")); + /// One 16-byte AES block, in the order of FIPS 197 Eq (3.6): `block[r + 4c] == s[r,c]`. pub type Block = [u8; crate::AES_BLOCK_LEN]; @@ -190,12 +196,20 @@ impl PlaneWord for u32 { type Blocks = [Block; 2]; /// Shift-and-or rather than the equivalent `u32::from(pattern) * 0x0001_0001`, because - /// because integer multiplication is not constant-time on all architectures. - /// A compiler may still emit a multiply where its cost model - /// prefers one, as LLVM does for the `u64` version on x86-64, where `imul` is fixed-latency. + /// integer multiplication is not constant-time on every target. LLVM knows the + /// zero-extended `x` has no bits above 16, so the ORs are disjoint, so it may rewrite them + /// into that multiply anyway: it does on aarch64 and x86-64, where `mul` / `imul` are + /// fixed-latency and nothing is leaked, and (checked in a `no_std` harness, 2026-11-04) on + /// AVR, Cortex-M0 and RISC-V 32I, where the multiply is a `muls` or a `__mulsi3` call. So + /// everywhere but the two fixed-latency targets `x` first goes through + /// [`ct::value_barrier`], which hides its known-zero bits; the ORs then stay ORs (none of + /// the five harness targets emits a multiply or a branch). On x86-64 and aarch64 the + /// barrier would cost about 60 % of the 2-block and 190 % of the 4-block throughput + /// (measured), for a multiply that is already constant-time, so there the fold is allowed. #[inline(always)] fn splat(pattern: u16) -> Self { let x = u32::from(pattern); + let x = if MULTIPLY_IS_FIXED_LATENCY { x } else { ct::value_barrier(x) }; x | (x << 16) } @@ -224,13 +238,13 @@ impl PlaneWord for u32 { impl PlaneWord for u64 { type Blocks = [Block; 4]; - /// Shift-and-or equivalent of `u64::from(pattern) * 0x0001_0001_0001_0001`, - /// because integer multiplication is not constant-time on all architectures. - /// A compiler may still emit a multiply where its cost model - /// prefers one, as LLVM does for the `u64` version on x86-64, where `imul` is fixed-latency. + /// Shift-and-or equivalent of `u64::from(pattern) * 0x0001_0001_0001_0001`, with the same + /// barrier as the `u32` version, for the same reason (on AVR and RISC-V 32I the fold is a + /// `__muldi3` call). #[inline(always)] fn splat(pattern: u16) -> Self { let x = u64::from(pattern); + let x = if MULTIPLY_IS_FIXED_LATENCY { x } else { ct::value_barrier(x) }; x | (x << 16) | (x << 32) | (x << 48) } diff --git a/crypto/aes/src/gcm.rs b/crypto/aes/src/gcm.rs index 2df3dfc34..c95ba9b3e 100644 --- a/crypto/aes/src/gcm.rs +++ b/crypto/aes/src/gcm.rs @@ -6,7 +6,7 @@ use bouncycastle_cipher::modes::Gcm; // Imports needed for docs #[allow(unused_imports)] -use bouncycastle_cipher::modes::GCM_NONCE_LEN; +pub use bouncycastle_cipher::modes::GCM_NONCE_LEN; #[allow(unused_imports)] use bouncycastle_cipher::{Decrypting, Encrypting}; diff --git a/crypto/aes/src/hazmat/aes_internal.rs b/crypto/aes/src/hazmat/aes_internal.rs index 4437b4f10..561128d22 100644 --- a/crypto/aes/src/hazmat/aes_internal.rs +++ b/crypto/aes/src/hazmat/aes_internal.rs @@ -287,7 +287,12 @@ impl AES128Internal { /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 128 bits. pub(crate) fn new(key: &KeyMaterial<16>) -> Result { Self::validate(key)?; - Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + // `validate` checked key_len() == 16, which is exactly what ref_to_bytes() spans. + let bytes: &[u8; 16] = key + .ref_to_bytes() + .try_into() + .map_err(|_| SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidLength))?; + Ok(Self { schedule: expand::(bytes) }) } } @@ -295,7 +300,12 @@ impl AES192Internal { /// Expands a 24-byte key into an AES-192 schedule. See [`AES128Internal::new`] for the error cases. pub(crate) fn new(key: &KeyMaterial<24>) -> Result { Self::validate(key)?; - Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + // `validate` checked key_len() == 24, which is exactly what ref_to_bytes() spans. + let bytes: &[u8; 24] = key + .ref_to_bytes() + .try_into() + .map_err(|_| SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidLength))?; + Ok(Self { schedule: expand::(bytes) }) } } @@ -303,7 +313,12 @@ impl AES256Internal { /// Expands a 32-byte key into an AES-256 schedule. See [`AES128Internal::new`] for the error cases. pub(crate) fn new(key: &KeyMaterial<32>) -> Result { Self::validate(key)?; - Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + // `validate` checked key_len() == 32, which is exactly what ref_to_bytes() spans. + let bytes: &[u8; 32] = key + .ref_to_bytes() + .try_into() + .map_err(|_| SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidLength))?; + Ok(Self { schedule: expand::(bytes) }) } } diff --git a/crypto/aes/src/lib.rs b/crypto/aes/src/lib.rs index 8c0460381..52388f51d 100644 --- a/crypto/aes/src/lib.rs +++ b/crypto/aes/src/lib.rs @@ -102,8 +102,9 @@ //! Per-call stack usage is set by the plane width: 16, 32 or 64 bytes of bit-sliced state for //! one, two or four blocks, the same again for the round key widened from its stored one-block //! form, plus the S-box circuit's spills. Only key expansion depends on the key length. Measured -//! as the deepest frame chain below each entry point in the release build (x86-64, return -//! addresses included): +//! as the deepest frame chain below each entry point in the release build on x86-64 (return +//! addresses included); another target lays its frames out differently, and aarch64's are +//! smaller: //! //! | Entry point | AES-128 | AES-192 | AES-256 | //! |---|---|---|---| @@ -143,7 +144,14 @@ //! //! * The Rust compiler makes no guarantee it will preserve this. The code is written so that the //! natural code generation is straight-line, and `#![forbid(unsafe_code)]` rules out the usual -//! ways of forcing the issue, but the property is not contractual. +//! ways of forcing the issue, but the property is not contractual. The one place LLVM is known +//! to rewrite the arithmetic, the widening of a round-key word into the 2- and 4-block lanes, +//! folds into an integer multiply of a secret; on x86-64 and aarch64 that multiply is +//! fixed-latency and is allowed, everywhere else the word passes through +//! `bouncycastle_utils::ct::value_barrier` first so the widening stays shifts and ORs (see +//! `bitslice.rs`). Debug builds are not covered by any of this. +//! * The "no secret-dependent branch" claim is about release builds: a `debug_assert` can +//! short-circuit on a secret, so the crate keeps none over key-derived values. //! * The working state (16, 32 or 64 bytes, by the entry point) is not scrubbed after a call. //! Only the key schedule is wrapped in `Secret`, and so only it is guaranteed to be zeroized on //! drop. @@ -271,6 +279,6 @@ pub use cfb::{AES_CFB_128, AES_CFB_192, AES_CFB_256}; pub use cfb8::{AES_CFB8_128, AES_CFB8_192, AES_CFB8_256}; pub use cmac::{AES_CMAC_128, AES_CMAC_192, AES_CMAC_256}; pub use ctr::{AES_CTR_128, AES_CTR_192, AES_CTR_256, CTR_NONCE_LEN}; -pub use gcm::{AES_GCM_128, AES_GCM_192, AES_GCM_256}; +pub use gcm::{AES_GCM_128, AES_GCM_192, AES_GCM_256, GCM_NONCE_LEN}; pub use gmac::{AES_GMAC_128, AES_GMAC_192, AES_GMAC_256}; pub use key::{AES128Key, AES192Key, AES256Key, AESKey}; diff --git a/crypto/aes/src/sbox.rs b/crypto/aes/src/sbox.rs index e4df6d75d..2c513b6e6 100644 --- a/crypto/aes/src/sbox.rs +++ b/crypto/aes/src/sbox.rs @@ -222,7 +222,7 @@ pub(crate) fn sbox(q: &mut Planes) { /// `iS(S(y)) = B(A(I(B(A(I(y)) ^ {63} ^ {63}))) ^ {63} ^ {63}) = y`. /// /// So applying [`inv_affine`], then the forward circuit, then [`inv_affine`] again yields the -/// inverse S-box, at the cost of 16 extra XORs and 8 complements instead of a whole second +/// inverse S-box, at the cost of 32 extra XORs and 8 complements instead of a whole second /// circuit. Verified against Table 6 by the decryption known-answer tests in `tests/` /// (SP 800-38A F.1.2/4/6 and the ACVP decrypt vectors), and against [`sbox`] by the ECB /// conformance suite's inverse checks. diff --git a/crypto/aes/src/schedule.rs b/crypto/aes/src/schedule.rs index e0c38b653..24fa86c69 100644 --- a/crypto/aes/src/schedule.rs +++ b/crypto/aes/src/schedule.rs @@ -80,31 +80,37 @@ fn sub_word(word: u32) -> u32 { ortho(&mut q); sbox_out_of_line(&mut q); ortho(&mut q); - // The word was broadcast into all eight planes, so all eight must carry the same answer. - debug_assert!(q.iter().all(|&plane| plane == q[0]), "the eight broadcast planes must agree"); + // The word was broadcast into all eight planes, so all eight carry the same answer and plane + // 0 is it; `test_sub_word_fills_every_plane` below pins that. (Not a `debug_assert` over the + // planes: `Iterator::all` short-circuits, which is a branch on S-box output of key bytes in a + // debug build.) q[0] } /// KEYEXPANSION() (FIPS 197 Sec 5.2, Algorithm 2), returning the bit-sliced schedule. /// -/// `key` must be exactly `P::KEY_LEN` bytes; [`crate::hazmat::AESInternal`] checks that before calling, so this -/// cannot fail and takes no `Result`. +/// `key` is `P::KEY_LEN` bytes by its type (`KEY_LEN == P::KEY_LEN` is asserted at compile time), +/// so this cannot fail and takes no `Result`; [`crate::hazmat::AESInternal`]'s constructors hand +/// over the array once `validate` has passed. /// /// Algorithm 2 is followed literally -- lines 2-6 copy the key into `w[0..Nk]`, lines 7-16 derive /// the rest -- and then the finished schedule is rewritten in place into the storage form /// described in the module docs. Verified against the worked expansions in FIPS 197 /// Appendix A.1, A.2 and A.3 by the tests at the bottom of this file, which unpack the stored /// schedule and compare every `w[i]`. -pub(crate) fn expand(key: &[u8]) -> Secret { - debug_assert_eq!(key.len(), P::KEY_LEN); +pub(crate) fn expand( + key: &[u8; KEY_LEN], +) -> Secret { + const { assert!(KEY_LEN == P::KEY_LEN, "the key array must be P::KEY_LEN bytes") }; let mut schedule = Secret::::new(); let w = (*schedule).as_mut(); - // Algorithm 2 lines 2-6: w[i] = key[4i .. 4i+3] for i < Nk. - for i in 0..P::NK { - // Cannot fail: `key` is P::KEY_LEN == 4 * P::NK bytes, so this window is in bounds. - w[i] = u32::from_le_bytes(key[4 * i..4 * i + 4].try_into().unwrap()); + // Algorithm 2 lines 2-6: w[i] = key[4i .. 4i+3] for i < Nk. KEY_LEN == 4 * NK, so the + // array splits into exactly NK whole words with no remainder. + let (words, _) = key.as_chunks::<4>(); + for (w_i, bytes) in w[..P::NK].iter_mut().zip(words) { + *w_i = u32::from_le_bytes(*bytes); } // Algorithm 2 lines 7-16. @@ -252,8 +258,12 @@ mod tests { /// Appendix A prints a word as the byte sequence `[a0,a1,a2,a3]` left to right, so the /// tabulated `u32` has `a0` in its *most* significant byte; words are held little-endian /// here, so `swap_bytes` is the conversion. - fn assert_expansion_matches(key: &[u8], expected: &[u32], label: &str) { - let schedule = expand::

(key); + fn assert_expansion_matches( + key: &[u8; KEY_LEN], + expected: &[u32], + label: &str, + ) { + let schedule = expand::(key); assert_eq!(expected.len(), 4 * (P::NR + 1), "{label}: table length"); for (i, &want) in expected.iter().enumerate() { let got = classical_word::

(&schedule, i).swap_bytes(); @@ -267,7 +277,7 @@ mod tests { 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, ]; - assert_expansion_matches::(&key, &APPENDIX_A1_WORDS, "Appendix A.1"); + assert_expansion_matches::(&key, &APPENDIX_A1_WORDS, "Appendix A.1"); } #[test] @@ -276,7 +286,7 @@ mod tests { 0x8e, 0x73, 0xb0, 0xf7, 0xda, 0x0e, 0x64, 0x52, 0xc8, 0x10, 0xf3, 0x2b, 0x80, 0x90, 0x79, 0xe5, 0x62, 0xf8, 0xea, 0xd2, 0x52, 0x2c, 0x6b, 0x7b, ]; - assert_expansion_matches::(&key, &APPENDIX_A2_WORDS, "Appendix A.2"); + assert_expansion_matches::(&key, &APPENDIX_A2_WORDS, "Appendix A.2"); } #[test] @@ -286,7 +296,7 @@ mod tests { 0x77, 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, 0x09, 0x14, 0xdf, 0xf4, ]; - assert_expansion_matches::(&key, &APPENDIX_A3_WORDS, "Appendix A.3"); + assert_expansion_matches::(&key, &APPENDIX_A3_WORDS, "Appendix A.3"); } #[test] @@ -297,7 +307,7 @@ mod tests { 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, ]; - let schedule = expand::(&key); + let schedule = expand::(&key); for i in 0..AES128Params::NK { let got = classical_word::(&schedule, i); assert_eq!(got.to_le_bytes(), key[4 * i..4 * i + 4]); @@ -343,7 +353,7 @@ mod tests { 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, ]; - let schedule = expand::(&key); + let schedule = expand::(&key); // Recompute the classical schedule without the compression step. let mut w = [0u32; 44]; diff --git a/crypto/aes/tests/ccm_key_tests.rs b/crypto/aes/tests/ccm_key_tests.rs index 8776eb42c..5b47934cb 100644 --- a/crypto/aes/tests/ccm_key_tests.rs +++ b/crypto/aes/tests/ccm_key_tests.rs @@ -245,3 +245,22 @@ fn a_refused_update_leaves_no_payload_in_the_output_buffer() { )); assert_eq!(out, [0u8; 32], "a refused decrypt must leave only zeros behind"); } + +/// The drawing in-place one-shot, `encrypt_detached_in_place(rng, ..)`, is the `_nonce` form +/// under the nonce it drew: the one form the suite above does not reach. +#[test] +fn the_drawing_in_place_form_is_the_nonce_form_under_the_drawn_nonce() { + let km = material::<16>(); + let key = AES128Key::>::from_key_material(&km).unwrap(); + for len in LENS { + let msg = message(len); + let mut buf = msg.clone(); + let (nonce, tag) = key.encrypt_detached_in_place(&mut rng(), AAD, &mut buf).unwrap(); + assert_eq!(reference(&km, &nonce, &msg), (buf.clone(), tag), "len {len}"); + let mut again = msg.clone(); + assert_eq!(key.encrypt_detached_in_place_nonce(&nonce, AAD, &mut again).unwrap(), tag); + assert_eq!(again, buf, "len {len}: the nonce form agrees"); + key.decrypt_detached_in_place(&nonce, AAD, &mut buf, &tag).unwrap(); + assert_eq!(buf, msg, "len {len}: round trip"); + } +} From b02904353ff92b23ef292e00973fe2651cc32390 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 03:11:45 +1100 Subject: [PATCH 16/57] sha3, cli: the 2026-11-03 crate review's hardening items S2-S6 (REVIEW-2026-11-03-sha3-correctness-mythos.md, at 94d4e966; S1, the KMAC output ceiling, had landed with PR #172): TupleHash and ParallelHash bound output_len in new and on resume, SHA3 refuses a squeezing state on resume, the KDF entropy and ParallelHash block counters saturate and resume width-checked, squeeze cannot wrap its bit count on a 32-bit target; plus the review's two recommended tests S2: `TupleHashInternal::new` and `ParallelHashInternal::new` return `Result` and refuse an `output_len` above TUPLEHASH_MAX_OUTPUT_LEN / PARALLELHASH_MAX_OUTPUT_LEN (u64::MAX / 8, the KMAC cap), and `read_state` holds a resumed length to the same bound; past it `(n as u64) * 8` wrapped, and at exactly 2^61 bytes to zero, so `right_encode(0)` turned the fixed-length function into the XOF. The call sites (tests, benches, the sha3 CLI, which reports the length as an error) follow. S3: `SHA3Internal::from_suspended` refuses a state in the squeezing phase, as SHAKE, cSHAKE and ParallelHash already did; a SHA3 is only ever suspended while absorbing, and every finalisation of such a state failed on absorb_bits' phase check. S4: the KDF entropy counter is read back through `bounded_usize(.., usize::MAX)` (no truncation on a 32-bit target) and `mix_key_internal` adds to it with `saturating_add`, for SHA3 and SHAKE. S5: `ParallelState::absorb_block_digest` increments the block count with `saturating_add`; the resumed value is unbounded by design. S6: `KeccakInternal::squeeze` squeezes in slices of at most `usize::MAX / 8` bytes, so `out.len() << 3` cannot wrap (512 MiB on a 32-bit target wrapped the bound and the loop stopped early, writing only `(len * 8 mod 2^32) / 8` bytes); one iteration on 64-bit. Tests: the suspend-state suite covers the three new rejections and the two saturating counters on resume, the SHA3 suite the squeezing phase byte and a saturated entropy counter, the TupleHash and ParallelHash suites the constructor cap. The two tests the review's mutants pass asked for: TupleHash at L = 264 bits against Sec 5.3 written out by hand over cSHAKE128 (`right_encode(264)` is `01 08 02`, a two-byte value with a non-zero low byte, which no sample value exercises), and SHAKE's partial byte with all-ones against all-zeros in the unused low bits. Left from the review's notes: S-N1 (output_len 0 is accepted, as the spec allows), S-N2/S-N3 docs, S-N4/S-N5 API observations. Assisted-by: Claude:claude-fable-5-1 --- cli/src/sha3_cmd.rs | 24 ++++- crypto/sha3/benches/sha3_benches.rs | 6 +- crypto/sha3/src/keccak.rs | 27 +++++- crypto/sha3/src/parallelhash.rs | 31 ++++-- crypto/sha3/src/sha3.rs | 10 +- crypto/sha3/src/shake.rs | 3 +- crypto/sha3/src/tuplehash.rs | 27 ++++-- .../sha3/tests/parallelhash_bc-test-data.rs | 16 +-- crypto/sha3/tests/parallelhash_tests.rs | 75 +++++++++----- crypto/sha3/tests/sha3_tests.rs | 26 ++++- crypto/sha3/tests/shake_tests.rs | 18 ++++ crypto/sha3/tests/sp800_185_suspend_tests.rs | 59 ++++++++--- crypto/sha3/tests/tuplehash_bc-test-data.rs | 22 +++-- crypto/sha3/tests/tuplehash_tests.rs | 97 +++++++++++++++---- 14 files changed, 348 insertions(+), 93 deletions(-) diff --git a/cli/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index 69858e9cc..da4bd0218 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -60,8 +60,18 @@ pub(crate) fn tuplehash_cmd( let refs: Vec<&[u8]> = tuple.iter().map(|v| v.as_slice()).collect(); let out = match bit_len { - 128 => TupleHash128::new(s, output_len).hash_tuple(&refs), - 256 => TupleHash256::new(s, output_len).hash_tuple(&refs), + 128 => TupleHash128::new(s, output_len) + .unwrap_or_else(|_| { + eprintln!("Error: --output-len is larger than SP 800-185 allows here (Sec 7.2)."); + exit(-1); + }) + .hash_tuple(&refs), + 256 => TupleHash256::new(s, output_len) + .unwrap_or_else(|_| { + eprintln!("Error: --output-len is larger than SP 800-185 allows here (Sec 7.2)."); + exit(-1); + }) + .hash_tuple(&refs), _ => panic!("Unsupported algorithm: TupleHash-{bit_len}"), }; write_out(&out, output_hex); @@ -85,12 +95,18 @@ pub(crate) fn parallelhash_cmd( let s = customization.as_deref().unwrap_or("").as_bytes(); match bit_len { 128 => { - let mut p = ParallelHash128::new(block_size, s, output_len); + let mut p = ParallelHash128::new(block_size, s, output_len).unwrap_or_else(|_| { + eprintln!("Error: --output-len is larger than SP 800-185 allows here (Sec 7.2)."); + exit(-1); + }); stream_stdin(|chunk| p.do_update(chunk)); write_out(&p.do_final(), output_hex); } 256 => { - let mut p = ParallelHash256::new(block_size, s, output_len); + let mut p = ParallelHash256::new(block_size, s, output_len).unwrap_or_else(|_| { + eprintln!("Error: --output-len is larger than SP 800-185 allows here (Sec 7.2)."); + exit(-1); + }); stream_stdin(|chunk| p.do_update(chunk)); write_out(&p.do_final(), output_hex); } diff --git a/crypto/sha3/benches/sha3_benches.rs b/crypto/sha3/benches/sha3_benches.rs index a12c2e4e9..851ebdc4d 100644 --- a/crypto/sha3/benches/sha3_benches.rs +++ b/crypto/sha3/benches/sha3_benches.rs @@ -256,7 +256,7 @@ fn bench_tuplehash128(c: &mut Criterion) { group.throughput(Throughput::Bytes(big_data.len() as u64)); group.bench_function(format!("{} bytes in 4 elements -- .hash_tuple()", big_data.len()), |b| { b.iter(|| { - black_box(TupleHash128::new(b"", 32).hash_tuple(black_box(&tuple))); + black_box(TupleHash128::new(b"", 32).unwrap().hash_tuple(black_box(&tuple))); }) }); group.finish(); @@ -270,7 +270,9 @@ fn bench_parallelhash128(c: &mut Criterion) { group.throughput(Throughput::Bytes(big_data.len() as u64)); group.bench_function(format!("{} bytes, B = 8192 -- .hash_out()", big_data.len()), |b| { b.iter(|| { - ParallelHash128::new(block_size, b"", 32).hash_out(black_box(&big_data), &mut digest); + ParallelHash128::new(block_size, b"", 32) + .unwrap() + .hash_out(black_box(&big_data), &mut digest); black_box(&digest); }) }); diff --git a/crypto/sha3/src/keccak.rs b/crypto/sha3/src/keccak.rs index 2a3470424..86113dd61 100644 --- a/crypto/sha3/src/keccak.rs +++ b/crypto/sha3/src/keccak.rs @@ -2,6 +2,7 @@ use bouncycastle_core::errors::{HashError, SuspendableError}; use bouncycastle_core::key_material::KeyType; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::bounded_usize; /// `RC[ir]` for the 24 rounds of KECCAK-f[1600]: FIPS 202 Algorithm 6 (iota) steps 2-3, /// evaluated with the `rc(t)` LFSR of Algorithm 5 for `ir = 0..24` and stored as lanes. @@ -322,6 +323,18 @@ impl KeccakInternal { if !self.squeezing { self.pad_and_switch_to_squeezing_phase(); } + // The loop below counts in bits, so a slice whose bit length does not fit a `usize` (512 + // MiB on a 32-bit target) would wrap the bound and stop early; squeeze it in pieces that + // fit. On a 64-bit target the piece is larger than any buffer, so this is one iteration. + for piece in out.chunks_mut(usize::MAX >> 3) { + self.squeeze_into(piece); + } + out.len() + } + + /// [`Self::squeeze`]'s loop for a slice of fewer than `usize::MAX / 8` bytes, with the sponge + /// already in the squeezing phase. + fn squeeze_into(&mut self, out: &mut [u8]) { let output_length = out.len() << 3; let mut i = 0; @@ -340,7 +353,6 @@ impl KeccakInternal { self.bits_in_queue -= partial_block; i += partial_block; } - output_length >> 3 } #[inline(always)] @@ -529,9 +541,16 @@ pub(crate) fn deserialize_sha3_family_state( // KeyType and SecurityStrength each own their canonical 1-byte encoding (`as u8` / `TryFrom`). let kdf_key_type = KeyType::try_from(input[1 + KECCAK_SERIALIZED_LEN])?; let kdf_security_strength = SecurityStrength::try_from(input[1 + KECCAK_SERIALIZED_LEN + 1])?; - let kdf_entropy = u64::from_le_bytes( - input[1 + KECCAK_SERIALIZED_LEN + 2..1 + KECCAK_SERIALIZED_LEN + 10].try_into().unwrap(), - ) as usize; + // `usize::MAX` only narrows the width: any count is valid, but a 32-bit target must not + // truncate one that was written on a 64-bit one. + let kdf_entropy = bounded_usize( + u64::from_le_bytes( + input[1 + KECCAK_SERIALIZED_LEN + 2..1 + KECCAK_SERIALIZED_LEN + 10] + .try_into() + .unwrap(), + ), + usize::MAX, + )?; Ok((keccak, kdf_key_type, kdf_security_strength, kdf_entropy)) } diff --git a/crypto/sha3/src/parallelhash.rs b/crypto/sha3/src/parallelhash.rs index c6d42ee59..2f00cb45b 100644 --- a/crypto/sha3/src/parallelhash.rs +++ b/crypto/sha3/src/parallelhash.rs @@ -11,7 +11,7 @@ //! use bouncycastle_sha3::parallelhash::ParallelHash128; //! //! let block_size = NonZeroUsize::new(8192).unwrap(); -//! let output: Vec = ParallelHash128::new(block_size, b"", 32).hash(b"Hello, world!"); +//! let output: Vec = ParallelHash128::new(block_size, b"", 32).unwrap().hash(b"Hello, world!"); //! ``` //! //! # ParallelHashXOF @@ -37,7 +37,7 @@ //! let more: Vec = squeezer.do_output(1024); //! //! let bound: Vec = ParallelHashXOF128::new(block_size, b"").xof(b"Hello, world!", 32); -//! assert_eq!(bound, ParallelHash128::new(block_size, b"", 32).hash(b"Hello, world!")); +//! assert_eq!(bound, ParallelHash128::new(block_size, b"", 32).unwrap().hash(b"Hello, world!")); //! assert_ne!(bound[..16], first[..]); //! ``` @@ -76,6 +76,11 @@ const PARALLEL_STATE_LEN: usize = CSHAKE_COMPONENT_LEN + SHA3_FAMILY_STATE_LEN + /// The function-name string every ParallelHash binds, per SP 800-185 Sec 6.3. const PARALLELHASH_FUNCTION_NAME: &[u8] = b"ParallelHash"; +/// The largest `output_len` [`ParallelHashInternal::new`] accepts, and the bound a resumed state +/// is held to; see [`crate::tuplehash::TUPLEHASH_MAX_OUTPUT_LEN`] for the reasoning, which is the +/// same here (Sec 6.3.1 step 5 encodes `L` in bits). +pub const PARALLELHASH_MAX_OUTPUT_LEN: usize = (u64::MAX / 8) as usize; + /// ParallelHash128: the parallelisable hash of NIST SP 800-185 Sec 6, 128-bit strength. /// /// The block size `B` is part of the function, not a tuning knob: the same message under a @@ -126,7 +131,9 @@ impl ParallelState { let digest = &mut digest[..Self::INNER_LEN]; core::mem::take(&mut self.inner).xof_out(&[], digest); self.cshake.do_update(digest); - self.blocks += 1; + // Saturating: the count is bookkeeping for the final `right_encode(n)`, and a resumed + // state may carry any value, so `+=` could overflow on one a caller corrupted. + self.blocks = self.blocks.saturating_add(1); self.block_fill = 0; } @@ -235,8 +242,20 @@ impl Algorithm for ParallelHashInternal { impl ParallelHashInternal { /// A new ParallelHash over `block_size`-byte blocks, producing `output_len` bytes. - pub fn new(block_size: NonZeroUsize, customization: &[u8], output_len: usize) -> Self { - Self { state: ParallelState::new(block_size, customization), output_len } + /// + /// # Errors + /// [`HashError::InvalidLength`] if `output_len` is above [`PARALLELHASH_MAX_OUTPUT_LEN`]. + pub fn new( + block_size: NonZeroUsize, + customization: &[u8], + output_len: usize, + ) -> Result { + if output_len > PARALLELHASH_MAX_OUTPUT_LEN { + return Err(HashError::InvalidLength( + "ParallelHash output length in bits does not fit a u64", + )); + } + Ok(Self { state: ParallelState::new(block_size, customization), output_len }) } } @@ -256,7 +275,7 @@ impl<'k, PARAMS: SHAKEParams> SuspendableComponent<'k> for ParallelHashInternal< let (parallel, rest) = state.split_at(PARALLEL_STATE_LEN); let state = ParallelState::read_state(parallel, PARAMS::PARALLELHASH_STATE_TAG)?; let mut r = Cursor::new(rest); - let output_len = bounded_usize(r.u64(), usize::MAX)?; + let output_len = bounded_usize(r.u64(), PARALLELHASH_MAX_OUTPUT_LEN)?; debug_assert!(r.is_done()); Ok(Self { state, output_len }) } diff --git a/crypto/sha3/src/sha3.rs b/crypto/sha3/src/sha3.rs index 977a6aa39..a807b3f69 100644 --- a/crypto/sha3/src/sha3.rs +++ b/crypto/sha3/src/sha3.rs @@ -91,7 +91,8 @@ impl SHA3Internal { // track input entropy if key.is_full_entropy() { - self.kdf_entropy += key.key_len(); + // Saturating: the counter is bookkeeping, and a resumed state may carry any value. + self.kdf_entropy = self.kdf_entropy.saturating_add(key.key_len()); self.kdf_security_strength = *max(&self.kdf_security_strength, &key.security_strength()); self.kdf_security_strength = *min( @@ -307,6 +308,13 @@ impl Suspendable for SHA3Internal< let (keccak, kdf_key_type, kdf_security_strength, kdf_entropy) = deserialize_sha3_family_state(input, PARAMS::STATE_TAG, rate)?; + // A SHA3 is only ever suspended while absorbing: finalisation consumes it. A state in + // the squeezing phase is not one this type produced, and every finalisation of it would + // fail on absorb_bits' phase check, so refuse it here as SHAKE does. + if keccak.squeezing { + return Err(SuspendableError::InvalidData); + } + Ok(SHA3Internal { _params: core::marker::PhantomData, keccak, diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index bafe7a783..6b63f8336 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -131,7 +131,8 @@ impl SHAKEInternal { // track input entropy if key.is_full_entropy() { - self.kdf_entropy += key.key_len(); + // Saturating: the counter is bookkeeping, and a resumed state may carry any value. + self.kdf_entropy = self.kdf_entropy.saturating_add(key.key_len()); self.kdf_security_strength = *max(&self.kdf_security_strength, &key.security_strength()); self.kdf_security_strength = *min( diff --git a/crypto/sha3/src/tuplehash.rs b/crypto/sha3/src/tuplehash.rs index 3c94dde80..8d693cc4b 100644 --- a/crypto/sha3/src/tuplehash.rs +++ b/crypto/sha3/src/tuplehash.rs @@ -14,10 +14,10 @@ //! //! // .hash_tuple() takes tuples as an array of arrays //! let tuple: [&[u8]; 2] = [b"user id", b"session"]; -//! let output: Vec = TupleHash128::new(b"", 32).hash_tuple(&tuple); +//! let output: Vec = TupleHash128::new(b"", 32).unwrap().hash_tuple(&tuple); //! //! // The same computation, one element per .do_update() -//! let mut th = TupleHash128::new(b"", 32); +//! let mut th = TupleHash128::new(b"", 32).unwrap(); //! th.do_update(b"user id"); //! th.do_update(b"session"); //! assert_eq!(th.do_final(), output); @@ -44,7 +44,7 @@ //! let more: Vec = squeezer.do_output(1024); //! //! let bound: Vec = TupleHashXOF128::new(b"").xof(b"Hello, world!", 32); -//! assert_eq!(bound, TupleHash128::new(b"", 32).hash(b"Hello, world!")); +//! assert_eq!(bound, TupleHash128::new(b"", 32).unwrap().hash(b"Hello, world!")); //! assert_ne!(bound[..16], first[..]); //! ``` @@ -77,6 +77,13 @@ pub const SUSPENDED_TUPLEHASHXOF_STATE_LEN: usize = LIB_VERSION_LEN + CSHAKE_COM /// The function-name string every TupleHash binds, per SP 800-185 Sec 5.3. const TUPLEHASH_FUNCTION_NAME: &[u8] = b"TupleHash"; +/// The largest `output_len` [`TupleHashInternal::new`] accepts, and the bound a resumed state is +/// held to: the largest `L` in bits that fits the `u64` the final `right_encode(L)` (Sec 5.3.1 +/// step 3) and the suspended state carry. SP 800-185 Sec 5.3.1 allows any `L < 2^2040`, and Sec +/// 7.2 lets an implementation limit the lengths it produces; past this one the bit count would +/// wrap and the fixed-length function would silently become the XOF. +pub const TUPLEHASH_MAX_OUTPUT_LEN: usize = (u64::MAX / 8) as usize; + /// TupleHash128: the unambiguous tuple hash of NIST SP 800-185 Sec 5, 128-bit strength. /// /// Each [`Hash::do_update`] call appends one *tuple @@ -127,8 +134,16 @@ impl TupleHashInternal { /// /// `output_len` is `L` and is bound into the computation (Sec 5.3 step 4), so a different /// length is a different function rather than a longer or shorter view of the same one. - pub fn new(customization: &[u8], output_len: usize) -> Self { - Self { cshake: CSHAKEInternal::new(TUPLEHASH_FUNCTION_NAME, customization), output_len } + /// + /// # Errors + /// [`HashError::InvalidLength`] if `output_len` is above [`TUPLEHASH_MAX_OUTPUT_LEN`]. + pub fn new(customization: &[u8], output_len: usize) -> Result { + if output_len > TUPLEHASH_MAX_OUTPUT_LEN { + return Err(HashError::InvalidLength( + "TupleHash output length in bits does not fit a u64", + )); + } + Ok(Self { cshake: CSHAKEInternal::new(TUPLEHASH_FUNCTION_NAME, customization), output_len }) } /// Hashes a whole tuple in one call, the shape the specification is written in. @@ -156,7 +171,7 @@ impl<'k, PARAMS: SHAKEParams> SuspendableComponent<'k> for TupleHashInternal ParallelHash128::new(v.block_size, v.s.as_bytes(), want).hash(&v.msg), - 256 => ParallelHash256::new(v.block_size, v.s.as_bytes(), want).hash(&v.msg), + 128 => ParallelHash128::new(v.block_size, v.s.as_bytes(), want).unwrap().hash(&v.msg), + 256 => ParallelHash256::new(v.block_size, v.s.as_bytes(), want).unwrap().hash(&v.msg), other => panic!("COUNT {i}: unexpected strength {other}"), }; assert_eq!( @@ -144,7 +144,7 @@ fn do_final_binds_the_length_when_nothing_has_been_read() { match f.strength { 128 => check_do_final_binds_length( || ParallelHashXOF128::new(b, s), - |n| ParallelHash128::new(b, s, n).hash(&f.msg), + |n| ParallelHash128::new(b, s, n).unwrap().hash(&f.msg), &f.msg, &f.output, &x.output, @@ -152,7 +152,7 @@ fn do_final_binds_the_length_when_nothing_has_been_read() { ), 256 => check_do_final_binds_length( || ParallelHashXOF256::new(b, s), - |n| ParallelHash256::new(b, s, n).hash(&f.msg), + |n| ParallelHash256::new(b, s, n).unwrap().hash(&f.msg), &f.msg, &f.output, &x.output, @@ -349,15 +349,15 @@ fn hash_trait_view_agrees_with_the_sample_values() { let ctx = format!("COUNT {i}: ParallelHash{} B={b}", v.strength); match v.strength { 128 => check_fixed_view( - || ParallelHash128::new(b, s, n), - |m, o| ParallelHash128::new(b, s, n).hash_out(m, o), + || ParallelHash128::new(b, s, n).unwrap(), + |m, o| ParallelHash128::new(b, s, n).unwrap().hash_out(m, o), &v.msg, &v.output, &ctx, ), 256 => check_fixed_view( - || ParallelHash256::new(b, s, n), - |m, o| ParallelHash256::new(b, s, n).hash_out(m, o), + || ParallelHash256::new(b, s, n).unwrap(), + |m, o| ParallelHash256::new(b, s, n).unwrap().hash_out(m, o), &v.msg, &v.output, &ctx, diff --git a/crypto/sha3/tests/parallelhash_tests.rs b/crypto/sha3/tests/parallelhash_tests.rs index 137b597ea..f7fb86531 100644 --- a/crypto/sha3/tests/parallelhash_tests.rs +++ b/crypto/sha3/tests/parallelhash_tests.rs @@ -5,7 +5,8 @@ use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_sha3::parallelhash::{ - ParallelHash128, ParallelHash256, ParallelHashXOF128, ParallelHashXOF256, + PARALLELHASH_MAX_OUTPUT_LEN, ParallelHash128, ParallelHash256, ParallelHashXOF128, + ParallelHashXOF256, }; use core::num::NonZeroUsize; @@ -20,10 +21,10 @@ fn nz(n: usize) -> NonZeroUsize { #[test] fn chunking_does_not_change_the_result() { let msg: Vec = (0..=200u8).collect(); - let one = ParallelHash128::new(nz(8), b"S", 32).hash(&msg); + let one = ParallelHash128::new(nz(8), b"S", 32).unwrap().hash(&msg); for chunk in [1usize, 3, 7, 8, 9, 16, 64, 201] { - let mut p = ParallelHash128::new(nz(8), b"S", 32); + let mut p = ParallelHash128::new(nz(8), b"S", 32).unwrap(); for piece in msg.chunks(chunk) { p.do_update(piece); } @@ -36,9 +37,9 @@ fn chunking_does_not_change_the_result() { #[test] fn the_block_size_is_part_of_the_hash() { let msg: Vec = (0..=100u8).collect(); - let b8 = ParallelHash128::new(nz(8), b"", 32).hash(&msg); - let b12 = ParallelHash128::new(nz(12), b"", 32).hash(&msg); - let b16 = ParallelHash128::new(nz(16), b"", 32).hash(&msg); + let b8 = ParallelHash128::new(nz(8), b"", 32).unwrap().hash(&msg); + let b12 = ParallelHash128::new(nz(12), b"", 32).unwrap().hash(&msg); + let b16 = ParallelHash128::new(nz(16), b"", 32).unwrap().hash(&msg); assert_ne!(b8, b12); assert_ne!(b8, b16); assert_ne!(b12, b16); @@ -54,16 +55,16 @@ fn the_block_size_is_part_of_the_hash() { #[test] fn block_boundary_cases() { // exactly one full block, versus one full block plus one byte - let full = ParallelHash128::new(nz(8), b"", 32).hash(&[0xAAu8; 8]); - let plus = ParallelHash128::new(nz(8), b"", 32).hash(&[0xAAu8; 9]); + let full = ParallelHash128::new(nz(8), b"", 32).unwrap().hash(&[0xAAu8; 8]); + let plus = ParallelHash128::new(nz(8), b"", 32).unwrap().hash(&[0xAAu8; 9]); assert_ne!(full, plus); // two full blocks versus one short block: different block counts, so different output - let two = ParallelHash128::new(nz(8), b"", 32).hash(&[0xAAu8; 16]); + let two = ParallelHash128::new(nz(8), b"", 32).unwrap().hash(&[0xAAu8; 16]); assert_ne!(two, full); // an empty message is zero blocks, and must still produce a hash - let empty = ParallelHash128::new(nz(8), b"", 32).hash(b""); + let empty = ParallelHash128::new(nz(8), b"", 32).unwrap().hash(b""); assert_eq!(empty.len(), 32); assert_ne!(empty, full); } @@ -73,8 +74,8 @@ fn block_boundary_cases() { #[test] fn length_binding_differs_between_the_two() { let msg = b"parallel"; - let short = ParallelHash128::new(nz(4), b"", 16).hash(msg); - let long = ParallelHash128::new(nz(4), b"", 32).hash(msg); + let short = ParallelHash128::new(nz(4), b"", 16).unwrap().hash(msg); + let long = ParallelHash128::new(nz(4), b"", 32).unwrap().hash(msg); assert_ne!(&long[..16], &short[..], "ParallelHash: a different length is a different function"); let squeeze = |n| { @@ -90,7 +91,7 @@ fn length_binding_differs_between_the_two() { /// A partial final byte cannot be expressed: the block count and length encodings must follow. #[test] fn partial_final_byte_is_refused() { - let mut p = ParallelHash128::new(nz(8), b"", 32); + let mut p = ParallelHash128::new(nz(8), b"", 32).unwrap(); p.do_update(b"abc"); assert!(matches!(p.do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); @@ -111,13 +112,25 @@ fn algorithm_names() { /// length of the fixed forms. The generic checks elsewhere only require these to be positive. #[test] fn metadata() { - assert_eq!(ParallelHash128::new(nz(8), b"", 32).block_bitlen(), 1344, "cSHAKE128 rate"); - assert_eq!(ParallelHash256::new(nz(8), b"", 64).block_bitlen(), 1088, "cSHAKE256 rate"); + assert_eq!( + ParallelHash128::new(nz(8), b"", 32).unwrap().block_bitlen(), + 1344, + "cSHAKE128 rate" + ); + assert_eq!( + ParallelHash256::new(nz(8), b"", 64).unwrap().block_bitlen(), + 1088, + "cSHAKE256 rate" + ); assert_eq!(ParallelHashXOF128::new(nz(8), b"").block_bitlen(), 1344); assert_eq!(ParallelHashXOF256::new(nz(8), b"").block_bitlen(), 1088); - assert_eq!(ParallelHash128::new(nz(8), b"", 17).output_len(), 17, "whatever was asked for"); - assert_eq!(ParallelHash256::new(nz(8), b"", 100).output_len(), 100); + assert_eq!( + ParallelHash128::new(nz(8), b"", 17).unwrap().output_len(), + 17, + "whatever was asked for" + ); + assert_eq!(ParallelHash256::new(nz(8), b"", 100).unwrap().output_len(), 100); assert_eq!(ParallelHashXOF128::new(nz(8), b"").output_len(), 32, "the nominal length"); assert_eq!(ParallelHashXOF256::new(nz(8), b"").output_len(), 64); } @@ -131,11 +144,29 @@ fn output_buffers_of_every_length() { let framework = TestFrameworkHash::new(); let input = b"the quick brown fox jumps over the lazy dog"; - framework.test_hash_output_buffers(|| ParallelHash128::new(nz(8), b"", 32), input); - framework.test_hash_output_buffers(|| ParallelHash256::new(nz(8), b"", 64), input); + framework.test_hash_output_buffers(|| ParallelHash128::new(nz(8), b"", 32).unwrap(), input); + framework.test_hash_output_buffers(|| ParallelHash256::new(nz(8), b"", 64).unwrap(), input); // A block size that does not divide the input, a customization string, odd output lengths. - framework - .test_hash_output_buffers(|| ParallelHash128::new(nz(12), b"Parallel Data", 17), input); - framework.test_hash_output_buffers(|| ParallelHash256::new(nz(5), b"Parallel Data", 5), input); + framework.test_hash_output_buffers( + || ParallelHash128::new(nz(12), b"Parallel Data", 17).unwrap(), + input, + ); + framework.test_hash_output_buffers( + || ParallelHash256::new(nz(5), b"Parallel Data", 5).unwrap(), + input, + ); +} + +/// `L` is bound into the computation as a bit count in a `u64` (Sec 6.3.1 step 5): a byte length +/// whose bit count does not fit is refused by `new` rather than wrapped into a different +/// function. +#[test] +fn an_output_length_past_the_cap_is_refused() { + let b = NonZeroUsize::new(8).unwrap(); + assert!(matches!( + ParallelHash128::new(b, b"", PARALLELHASH_MAX_OUTPUT_LEN + 1), + Err(HashError::InvalidLength(_)) + )); + assert!(ParallelHash256::new(b, b"", PARALLELHASH_MAX_OUTPUT_LEN).is_ok(), "the cap is valid"); } diff --git a/crypto/sha3/tests/sha3_tests.rs b/crypto/sha3/tests/sha3_tests.rs index 0c6e63f79..6248938aa 100644 --- a/crypto/sha3/tests/sha3_tests.rs +++ b/crypto/sha3/tests/sha3_tests.rs @@ -419,7 +419,10 @@ mod sha3_tests { let str = "Colorless green ideas sleep furiously"; // A helper that exercises the full round-trip for one SHA3 variant. - fn round_trip + Clone>(mut hash: H, input: &[u8]) { + fn round_trip + Clone>( + mut hash: H, + input: &[u8], + ) { hash.do_update(input); // do the default trait-conformance tests @@ -444,6 +447,27 @@ mod sha3_tests { Err(SuspendableError::InvalidData) => { /* good */ } _ => panic!("Expected an error for a corrupt squeezing byte"), } + + // A well-formed phase byte that says "squeezing" is not a state a SHA3 produces + // either: it is only ever suspended while absorbing, and every finalisation of a + // squeezing one would fail. Refused on resume, as SHAKE refuses it. + let mut squeezing = serialized_state; + squeezing[3 + 1 + 400] = 1; + assert!( + matches!(H::from_suspended(squeezing), Err(SuspendableError::InvalidData)), + "a squeezing phase byte must be rejected" + ); + + // The KDF entropy counter (8 bytes after the key-type and strength bytes) may carry + // any value: the largest resumes, and mixing a further key saturates rather than + // overflowing. + let mut maxed = serialized_state; + let entropy = 3 + 1 + 401 + 2; + maxed[entropy..entropy + 8].copy_from_slice(&u64::MAX.to_le_bytes()); + let resumed = H::from_suspended(maxed).expect("any entropy count resumes"); + let key = KeyMaterial256::from_bytes_as_type(&[7u8; 32], KeyType::CryptographicRandom) + .unwrap(); + resumed.derive_key(&key, &[]).expect("a saturated counter still derives"); } round_trip(SHA3_224::new(), str.as_bytes()); diff --git a/crypto/sha3/tests/shake_tests.rs b/crypto/sha3/tests/shake_tests.rs index 3bdf84d95..0f74c82ff 100644 --- a/crypto/sha3/tests/shake_tests.rs +++ b/crypto/sha3/tests/shake_tests.rs @@ -18,6 +18,24 @@ mod shake_tests { /// Vector: NIST CAVP SHA3VS SHAKE128ShortMsg (bit-oriented), Len = 4, Msg = 08 (FIPS 202 B.1 /// packing: message bits 0001 in the low nibble, first bit in the LSB), i.e. 0x10 in the API's /// MSB-first order. + /// The low `8 - num_bits` bits of the partial byte are the BIT STRING's unused bits (X.690 + /// s. 8.6.2.1) and must not reach the sponge: all ones there and all zeros there give the + /// same output. + #[test] + fn the_unused_bits_of_a_partial_byte_are_ignored() { + for num_bits in 1..=7usize { + let mut all_ones = SHAKE128::new(); + all_ones.do_update(b"abc"); + let mut all_zeros = SHAKE128::new(); + all_zeros.do_update(b"abc"); + let used_mask = 0xffu8 << (8 - num_bits); + let a = all_ones.into_squeezer_partial_bits(0xff, num_bits).unwrap().do_output(32); + let b = + all_zeros.into_squeezer_partial_bits(used_mask, num_bits).unwrap().do_output(32); + assert_eq!(a, b, "num_bits = {num_bits}"); + } + } + #[test] fn into_squeezer_partial_bits_four_bits() { let shake = SHAKE128::new(); diff --git a/crypto/sha3/tests/sp800_185_suspend_tests.rs b/crypto/sha3/tests/sp800_185_suspend_tests.rs index f7a480c3b..f71b52429 100644 --- a/crypto/sha3/tests/sp800_185_suspend_tests.rs +++ b/crypto/sha3/tests/sp800_185_suspend_tests.rs @@ -115,8 +115,8 @@ fn kmacxof_round_trips() { #[test] fn tuplehash_round_trips() { - hash_round_trip(|| TupleHash128::new(b"", 32)); - hash_round_trip(|| TupleHash256::new(b"My Tuple App", 48)); + hash_round_trip(|| TupleHash128::new(b"", 32).unwrap()); + hash_round_trip(|| TupleHash256::new(b"My Tuple App", 48).unwrap()); hash_round_trip(|| TupleHashXOF128::new(b"")); hash_round_trip(|| TupleHashXOF256::new(b"My Tuple App")); squeezer_round_trip(|| TupleHashXOF128::new(b"")); @@ -128,8 +128,8 @@ fn parallelhash_round_trips() { // PART1 is 21 bytes: a block size of 8 suspends five bytes into a block, 7 suspends exactly // on a block boundary, and 64 suspends before the first block completes. for block_size in [nz(8), nz(7), nz(64)] { - hash_round_trip(|| ParallelHash128::new(block_size, b"", 32)); - hash_round_trip(|| ParallelHash256::new(block_size, b"Parallel Data", 64)); + hash_round_trip(|| ParallelHash128::new(block_size, b"", 32).unwrap()); + hash_round_trip(|| ParallelHash256::new(block_size, b"Parallel Data", 64).unwrap()); hash_round_trip(|| ParallelHashXOF128::new(block_size, b"")); hash_round_trip(|| ParallelHashXOF256::new(block_size, b"Parallel Data")); squeezer_round_trip(|| ParallelHashXOF128::new(block_size, b"")); @@ -168,11 +168,11 @@ fn each_type_rejects_the_others() { k.do_update(PART1); rejected::<_, TupleHash128>(k.clone().suspend(), "a KMAC128 state in TupleHash128"); rejected::<_, KMAC256>(k.suspend(), "a KMAC128 state in KMAC256"); - let mut t = TupleHash128::new(b"", 32); + let mut t = TupleHash128::new(b"", 32).unwrap(); t.do_update(PART1); rejected::<_, KMAC128>(t.suspend(), "a TupleHash128 state in KMAC128"); - let mut p = ParallelHash128::new(nz(8), b"", 32); + let mut p = ParallelHash128::new(nz(8), b"", 32).unwrap(); p.do_update(PART1); rejected::<_, ParallelHash256>(p.suspend(), "a ParallelHash128 state in ParallelHash256"); let mut p = ParallelHashXOF128::new(nz(8), b""); @@ -235,7 +235,7 @@ fn corrupt_fields_are_rejected() { const INNER_SQUEEZING_FLAG: usize = 416 + 1 + 400; const BLOCK_SIZE: usize = 416 + 412; const BLOCK_FILL: usize = BLOCK_SIZE + 8; - let mut p = ParallelHash128::new(nz(8), b"", 32); + let mut p = ParallelHash128::new(nz(8), b"", 32).unwrap(); p.do_update(PART1); let good = p.suspend(); assert_eq!(&good[BLOCK_SIZE..BLOCK_SIZE + 8], &8u64.to_le_bytes(), "layout check"); @@ -252,6 +252,43 @@ fn corrupt_fields_are_rejected() { let mut bad = good; bad[CUSTOMIZED] = 0; rejected::<_, ParallelHash128>(bad, "a ParallelHash claiming an empty function name"); + // The output length is the trailing u64, bounded as `new` bounds it. + let output_len = good.len() - 8; + let mut bad = good; + bad[output_len..].copy_from_slice(&u64::MAX.to_le_bytes()); + rejected::<_, ParallelHash128>( + bad, + "a ParallelHash whose output length in bits overflows a u64", + ); + let mut bad = good; + bad[output_len..].copy_from_slice(&(PARALLELHASH_MAX_OUTPUT_LEN as u64 + 1).to_le_bytes()); + rejected::<_, ParallelHash128>(bad, "a ParallelHash output length one above the cap"); + let mut edge = good; + edge[output_len..].copy_from_slice(&(PARALLELHASH_MAX_OUTPUT_LEN as u64).to_le_bytes()); + assert!(ParallelHash128::from_suspended(edge).is_ok(), "the cap itself resumes"); + // The block count may carry any value: the largest resumes, and a further block saturates + // the count rather than overflowing it. + const BLOCKS: usize = BLOCK_FILL + 8; + let mut maxed = good; + maxed[BLOCKS..BLOCKS + 8].copy_from_slice(&u64::MAX.to_le_bytes()); + let mut p = ParallelHash128::from_suspended(maxed).expect("any block count resumes"); + p.do_update(&[0u8; 16]); + assert_eq!(p.do_final().len(), 32); + + // TupleHash: cSHAKE 3..416, then output_len, bounded as `new` bounds it. + let mut t = TupleHash128::new(b"", 32).unwrap(); + t.do_update(PART1); + let good = t.suspend(); + let output_len = good.len() - 8; + let mut bad = good; + bad[output_len..].copy_from_slice(&u64::MAX.to_le_bytes()); + rejected::<_, TupleHash128>(bad, "a TupleHash whose output length in bits overflows a u64"); + let mut bad = good; + bad[output_len..].copy_from_slice(&(TUPLEHASH_MAX_OUTPUT_LEN as u64 + 1).to_le_bytes()); + rejected::<_, TupleHash128>(bad, "a TupleHash output length one above the cap"); + let mut edge = good; + edge[output_len..].copy_from_slice(&(TUPLEHASH_MAX_OUTPUT_LEN as u64).to_le_bytes()); + assert!(TupleHash128::from_suspended(edge).is_ok(), "the cap itself resumes"); } /// Every type in the crate writes a different variant tag, so no state can be misread as @@ -271,17 +308,17 @@ fn state_tags_are_distinct() { tag(CSHAKE128::new(b"", b"S")), tag(KMAC128Key::from_key_material(&key()).unwrap().create_generator()), tag(KMACXOF128::new(&key(), b"", false).unwrap()), - tag(TupleHash128::new(b"", 32)), + tag(TupleHash128::new(b"", 32).unwrap()), tag(TupleHashXOF128::new(b"")), - tag(ParallelHash128::new(nz(8), b"", 32)), + tag(ParallelHash128::new(nz(8), b"", 32).unwrap()), tag(ParallelHashXOF128::new(nz(8), b"")), tag(TupleHashXOF128::new(b"").into_squeezer()), tag(CSHAKE256::new(b"", b"S")), tag(KMAC256Key::from_key_material(&key()).unwrap().create_generator()), tag(KMACXOF256::new(&key(), b"", false).unwrap()), - tag(TupleHash256::new(b"", 32)), + tag(TupleHash256::new(b"", 32).unwrap()), tag(TupleHashXOF256::new(b"")), - tag(ParallelHash256::new(nz(8), b"", 32)), + tag(ParallelHash256::new(nz(8), b"", 32).unwrap()), tag(ParallelHashXOF256::new(nz(8), b"")), tag(TupleHashXOF256::new(b"").into_squeezer()), ]; diff --git a/crypto/sha3/tests/tuplehash_bc-test-data.rs b/crypto/sha3/tests/tuplehash_bc-test-data.rs index dc4f72f09..c7733c789 100644 --- a/crypto/sha3/tests/tuplehash_bc-test-data.rs +++ b/crypto/sha3/tests/tuplehash_bc-test-data.rs @@ -77,8 +77,8 @@ fn nist_sp800_185_tuplehash_sample_values() { let want = v.output_len / 8; let t = as_slices(&v.tuple); let got = match v.strength { - 128 => TupleHash128::new(v.s.as_bytes(), want).hash_tuple(&t), - 256 => TupleHash256::new(v.s.as_bytes(), want).hash_tuple(&t), + 128 => TupleHash128::new(v.s.as_bytes(), want).unwrap().hash_tuple(&t), + 256 => TupleHash256::new(v.s.as_bytes(), want).unwrap().hash_tuple(&t), other => panic!("COUNT {i}: unexpected strength {other}"), }; assert_eq!( @@ -139,7 +139,7 @@ fn do_final_binds_the_length_when_nothing_has_been_read() { match f.strength { 128 => check_do_final_binds_length( || TupleHashXOF128::new(s), - |n| TupleHash128::new(s, n).hash_tuple(&t), + |n| TupleHash128::new(s, n).unwrap().hash_tuple(&t), &t, &f.output, &x.output, @@ -147,7 +147,7 @@ fn do_final_binds_the_length_when_nothing_has_been_read() { ), 256 => check_do_final_binds_length( || TupleHashXOF256::new(s), - |n| TupleHash256::new(s, n).hash_tuple(&t), + |n| TupleHash256::new(s, n).unwrap().hash_tuple(&t), &t, &f.output, &x.output, @@ -380,8 +380,18 @@ fn hash_trait_view_agrees_with_the_sample_values() { let t = as_slices(&v.tuple); let ctx = format!("COUNT {i}: TupleHash{}", v.strength); match v.strength { - 128 => check_fixed_view(|| TupleHash128::new(v.s.as_bytes(), n), &t, &v.output, &ctx), - 256 => check_fixed_view(|| TupleHash256::new(v.s.as_bytes(), n), &t, &v.output, &ctx), + 128 => check_fixed_view( + || TupleHash128::new(v.s.as_bytes(), n).unwrap(), + &t, + &v.output, + &ctx, + ), + 256 => check_fixed_view( + || TupleHash256::new(v.s.as_bytes(), n).unwrap(), + &t, + &v.output, + &ctx, + ), other => panic!("COUNT {i}: unexpected strength {other}"), } } diff --git a/crypto/sha3/tests/tuplehash_tests.rs b/crypto/sha3/tests/tuplehash_tests.rs index b497e8f30..14f7d91ad 100644 --- a/crypto/sha3/tests/tuplehash_tests.rs +++ b/crypto/sha3/tests/tuplehash_tests.rs @@ -4,23 +4,26 @@ use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::hash::TestFrameworkHash; -use bouncycastle_sha3::tuplehash::{TupleHash128, TupleHash256, TupleHashXOF128, TupleHashXOF256}; +use bouncycastle_sha3::CSHAKE128; +use bouncycastle_sha3::tuplehash::{ + TUPLEHASH_MAX_OUTPUT_LEN, TupleHash128, TupleHash256, TupleHashXOF128, TupleHashXOF256, +}; /// Sec 5.1, the reason TupleHash exists: the boundaries between elements are part of the hash, so /// re-splitting the same bytes gives an unrelated result. Every other hash in this library has the /// opposite property, which is why it is worth pinning explicitly. #[test] fn the_tuple_boundaries_are_part_of_the_hash() { - let a = TupleHash128::new(b"", 32).hash_tuple(&[b"abc", b"d"]); - let b = TupleHash128::new(b"", 32).hash_tuple(&[b"ab", b"cd"]); - let c = TupleHash128::new(b"", 32).hash_tuple(&[b"abcd"]); + let a = TupleHash128::new(b"", 32).unwrap().hash_tuple(&[b"abc", b"d"]); + let b = TupleHash128::new(b"", 32).unwrap().hash_tuple(&[b"ab", b"cd"]); + let c = TupleHash128::new(b"", 32).unwrap().hash_tuple(&[b"abcd"]); assert_ne!(a, b, "the same bytes split differently must hash differently"); assert_ne!(a, c, "... and differently again from a single element"); assert_ne!(b, c); // An empty element is an element: dropping it changes the answer. - let with = TupleHash128::new(b"", 32).hash_tuple(&[b"a", b"", b"b"]); - let without = TupleHash128::new(b"", 32).hash_tuple(&[b"a", b"b"]); + let with = TupleHash128::new(b"", 32).unwrap().hash_tuple(&[b"a", b"", b"b"]); + let without = TupleHash128::new(b"", 32).unwrap().hash_tuple(&[b"a", b"b"]); assert_ne!(with, without, "an empty tuple element must still count"); } @@ -28,9 +31,9 @@ fn the_tuple_boundaries_are_part_of_the_hash() { #[test] fn hash_tuple_matches_successive_updates() { let tuple: [&[u8]; 3] = [b"first", b"second", b"third"]; - let one = TupleHash128::new(b"S", 32).hash_tuple(&tuple); + let one = TupleHash128::new(b"S", 32).unwrap().hash_tuple(&tuple); - let mut t = TupleHash128::new(b"S", 32); + let mut t = TupleHash128::new(b"S", 32).unwrap(); for element in tuple { t.do_update(element); } @@ -43,8 +46,8 @@ fn hash_tuple_matches_successive_updates() { fn length_binding_differs_between_the_two() { let t: [&[u8]; 2] = [b"x", b"y"]; - let short = TupleHash128::new(b"", 16).hash_tuple(&t); - let long = TupleHash128::new(b"", 32).hash_tuple(&t); + let short = TupleHash128::new(b"", 16).unwrap().hash_tuple(&t); + let long = TupleHash128::new(b"", 32).unwrap().hash_tuple(&t); assert_ne!(&long[..16], &short[..], "TupleHash: a different length is a different function"); let short = TupleHashXOF128::new(b"").output_for(&t).do_output(16); @@ -57,15 +60,15 @@ fn length_binding_differs_between_the_two() { fn customization_separates_the_functions() { let t: [&[u8]; 2] = [b"x", b"y"]; assert_ne!( - TupleHash128::new(b"", 32).hash_tuple(&t), - TupleHash128::new(b"My Application", 32).hash_tuple(&t), + TupleHash128::new(b"", 32).unwrap().hash_tuple(&t), + TupleHash128::new(b"My Application", 32).unwrap().hash_tuple(&t), ); } /// A partial final byte cannot be expressed: the length encoding has to follow the tuple. #[test] fn partial_final_byte_is_refused() { - let mut t = TupleHash128::new(b"", 32); + let mut t = TupleHash128::new(b"", 32).unwrap(); t.do_update(b"abc"); assert!(matches!(t.do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); @@ -86,13 +89,13 @@ fn algorithm_names() { /// length of the fixed forms. The generic checks elsewhere only require these to be positive. #[test] fn metadata() { - assert_eq!(TupleHash128::new(b"", 32).block_bitlen(), 1344, "cSHAKE128 rate"); - assert_eq!(TupleHash256::new(b"", 64).block_bitlen(), 1088, "cSHAKE256 rate"); + assert_eq!(TupleHash128::new(b"", 32).unwrap().block_bitlen(), 1344, "cSHAKE128 rate"); + assert_eq!(TupleHash256::new(b"", 64).unwrap().block_bitlen(), 1088, "cSHAKE256 rate"); assert_eq!(TupleHashXOF128::new(b"").block_bitlen(), 1344); assert_eq!(TupleHashXOF256::new(b"").block_bitlen(), 1088); - assert_eq!(TupleHash128::new(b"", 17).output_len(), 17, "whatever was asked for"); - assert_eq!(TupleHash256::new(b"", 100).output_len(), 100); + assert_eq!(TupleHash128::new(b"", 17).unwrap().output_len(), 17, "whatever was asked for"); + assert_eq!(TupleHash256::new(b"", 100).unwrap().output_len(), 100); assert_eq!(TupleHashXOF128::new(b"").output_len(), 32, "the nominal length"); assert_eq!(TupleHashXOF256::new(b"").output_len(), 64); } @@ -107,10 +110,62 @@ fn output_buffers_of_every_length() { let framework = TestFrameworkHash::new(); let input = b"the quick brown fox"; - framework.test_hash_output_buffers(|| TupleHash128::new(b"", 32), input); - framework.test_hash_output_buffers(|| TupleHash256::new(b"", 64), input); + framework.test_hash_output_buffers(|| TupleHash128::new(b"", 32).unwrap(), input); + framework.test_hash_output_buffers(|| TupleHash256::new(b"", 64).unwrap(), input); // Non-default lengths, and a customization string. - framework.test_hash_output_buffers(|| TupleHash128::new(b"My Tuple App", 17), input); - framework.test_hash_output_buffers(|| TupleHash256::new(b"My Tuple App", 5), input); + framework.test_hash_output_buffers(|| TupleHash128::new(b"My Tuple App", 17).unwrap(), input); + framework.test_hash_output_buffers(|| TupleHash256::new(b"My Tuple App", 5).unwrap(), input); +} + +/// `L` is bound into the computation as a bit count in a `u64` (Sec 5.3.1 step 3): a byte length +/// whose bit count does not fit is refused by `new` rather than wrapped into a different +/// function. +#[test] +fn an_output_length_past_the_cap_is_refused() { + assert!(matches!( + TupleHash128::new(b"", TUPLEHASH_MAX_OUTPUT_LEN + 1), + Err(HashError::InvalidLength(_)) + )); + assert!(TupleHash256::new(b"", TUPLEHASH_MAX_OUTPUT_LEN).is_ok(), "the cap itself is valid"); +} + +/// Sec 5.3 written out by hand over the public cSHAKE: `TupleHash128(X, L, S) = +/// cSHAKE128(encode_string(X[1]) || ... || encode_string(X[n]) || right_encode(L), L, +/// "TupleHash", S)`, with `left_encode` / `right_encode` / `encode_string` from Sec 2.3.1-2.3.3. +/// `L = 264` bits is chosen so that `right_encode(L)` is `01 08 02`, a two-byte value whose low +/// byte is non-zero; the SP 800-185 sample values only ever encode 256 and 512, whose low byte +/// is zero, and so cannot tell the digits' order apart. +#[test] +fn matches_the_cshake_definition_at_a_length_with_a_non_zero_low_encoding_byte() { + fn encode_string(s: &[u8]) -> Vec { + // left_encode(len(s) in bits): the digit count, then the big-endian digits. + let bits = (s.len() * 8) as u64; + let digits: Vec = bits.to_be_bytes().iter().copied().skip_while(|&b| b == 0).collect(); + let digits = if digits.is_empty() { vec![0] } else { digits }; + let mut out = vec![digits.len() as u8]; + out.extend_from_slice(&digits); + out.extend_from_slice(s); + out + } + fn right_encode(x: u64) -> Vec { + let mut digits: Vec = x.to_be_bytes().iter().copied().skip_while(|&b| b == 0).collect(); + if digits.is_empty() { + digits.push(0); + } + let n = digits.len() as u8; + digits.push(n); + digits + } + let (x1, x2, s, l_bytes) = (b"abc".as_slice(), b"de".as_slice(), b"S".as_slice(), 33usize); + + let mut z = encode_string(x1); + z.extend_from_slice(&encode_string(x2)); + z.extend_from_slice(&right_encode((l_bytes * 8) as u64)); + assert_eq!(&z[z.len() - 3..], &[0x01, 0x08, 0x02], "right_encode(264)"); + let mut c = CSHAKE128::new(b"TupleHash", s); + c.do_update(&z); + let by_definition = c.into_squeezer().do_output(l_bytes); + + assert_eq!(TupleHash128::new(s, l_bytes).unwrap().hash_tuple(&[x1, x2]), by_definition); } From 607ad4a2b2a49382ea1d2a4a367038ae3007224c Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 03:21:16 +1100 Subject: [PATCH 17/57] rng: Hash_DRBG now matches SP 800-90A Rev. 1 and passes all 960 CAVP Hash_DRBG vectors; the 2026-11-03 crate review's four bugs R1-R4 (REVIEW-2026-11-03-rng-correctness-mythos.md, at 94d4e966) R1: V and C are seedlen bits, 440 for SHA-256 and 888 for SHA-512 (Section 10.1 Table 2, 10.1.1.1), where both were 512. The parameter sets carry SEED_LEN (and OUTPUT_LEN); the backing arrays are sized for the larger and every use is `[..H::SEED_LEN]`, so Hash_df fills seedlen bytes, C is derived from 0x00 || V over seedlen bytes, and the `mod 2^seedlen` additions run over seedlen bytes. R2: Hash_df (Section 10.3.1 step 4.1) encodes no_of_bits_to_return as the 32-bit integer the text says, big-endian; it was little-endian. The counter is written as the one byte it is. R3: the generate algorithm's step 5 adds reseed_counter as an integer, i.e. its big-endian bytes at the low end of V; the little-endian bytes in that position added the byte-reversed counter (2^56 for 1). R4: step 4's H is an outlen-bit integer; for SHA-256 it was added through a 64-byte buffer, i.e. shifted left by 256 bits. The buffer is sliced to the hash's outlen before the addition, and Hashgen's data is seedlen bytes. Every output of the generator for a given seed changes. The crate's one known-answer test (test_hash_df) compared against a value the code itself had produced and anchored R2; its expected values are now computed independently in Python from Section 10.3.1, for SHA-256 and SHA-512. The new tests/cavp_hash_drbg.rs runs NIST's drbgtestvectors (CAVS 14.3, 2013-04-02) Hash_DRBG.rsp from drbgvectors_no_reseed and drbgvectors_pr_false, the SHA-256 and SHA-512 sections with and without personalization string and additional input: 240 + 240 vectors per file, all passing (they all failed before). The files are read from bc-test-data/crypto/cavp/drbg//Hash_DRBG.rsp through the shared loader, so a missing checkout skips with its warning; they need adding to bc-test-data (the three sets as NIST ships them, pr_true included for when prediction resistance is supported). The crate gains bouncycastle-hex as a dev-dependency for the parser. No other test in the workspace pinned DRBG output. Not changed: R5-R8 (hardening: prediction-resistance todo!(), the empty-nonce check, the pre-check allocation in generate, V copies as plain arrays) and the notes. Assisted-by: Claude:claude-fable-5-1 --- crypto/rng/Cargo.toml | 1 + crypto/rng/src/hash_drbg80090a.rs | 189 ++++++++++++++++------------- crypto/rng/tests/cavp_hash_drbg.rs | 135 +++++++++++++++++++++ 3 files changed, 238 insertions(+), 87 deletions(-) create mode 100644 crypto/rng/tests/cavp_hash_drbg.rs diff --git a/crypto/rng/Cargo.toml b/crypto/rng/Cargo.toml index 48e1a3c6d..a33cf3099 100644 --- a/crypto/rng/Cargo.toml +++ b/crypto/rng/Cargo.toml @@ -13,6 +13,7 @@ getrandom = "0.4.0-rc.1" [dev-dependencies] bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true criterion.workspace = true [[bench]] diff --git a/crypto/rng/src/hash_drbg80090a.rs b/crypto/rng/src/hash_drbg80090a.rs index eaf26fd6e..d7fe63db6 100644 --- a/crypto/rng/src/hash_drbg80090a.rs +++ b/crypto/rng/src/hash_drbg80090a.rs @@ -1,4 +1,5 @@ -//! Implements Hash_DRBG (Deterministic Random Bit Generator) from NIST SP 800-90Ar1. +//! Implements Hash_DRBG (Deterministic Random Bit Generator) from NIST SP 800-90A Rev. 1, +//! Sections 10.1.1 and 10.3.1, checked against the CAVP `Hash_DRBG.rsp` vectors in `tests/`. // This is here cause HashDRBG80090AParams is private on purpose so that people can't instantiate new parameter sets other than the ones prescribed by NIST. #![allow(private_bounds)] @@ -25,9 +26,13 @@ enum SupportedHash { // ie the structs defined below will be the only allowed ones. trait HashDRBG80090AParams { const HASH: SupportedHash; - // const OUT_LEN: usize; + /// `outlen` in bytes (SP 800-90A Rev. 1 Table 2). + const OUTPUT_LEN: usize; const MAX_SECURITY_STRENGTH: SecurityStrength; - // const SEED_LEN: usize; + /// `seedlen` in bytes: Table 2 gives 440 bits for SHA-256 and 888 for SHA-512. `V` and `C` + /// are this long (Section 10.1.1.1), and every `mod 2^seedlen` in the generate algorithm is + /// an addition over this many bytes. + const SEED_LEN: usize; const MAX_LENGTH: u64; const MAX_PERSONALIZATION_STRING_LENGTH: u64; const MAX_ADDITIONAL_INPUT_LENGTH: u64; @@ -41,7 +46,9 @@ pub struct HashDRBG80090AParams_SHA256 {} impl HashDRBG80090AParams for HashDRBG80090AParams_SHA256 { const HASH: SupportedHash = SupportedHash::SHA256; + const OUTPUT_LEN: usize = SHA256::OUTPUT_LEN; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; + const SEED_LEN: usize = 440 / 8; const MAX_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits const MAX_PERSONALIZATION_STRING_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits const MAX_ADDITIONAL_INPUT_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits @@ -54,7 +61,9 @@ impl HashDRBG80090AParams for HashDRBG80090AParams_SHA256 { pub struct HashDRBG80090AParams_SHA512 {} impl HashDRBG80090AParams for HashDRBG80090AParams_SHA512 { const HASH: SupportedHash = SupportedHash::SHA512; + const OUTPUT_LEN: usize = SHA512::OUTPUT_LEN; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; + const SEED_LEN: usize = 888 / 8; const MAX_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits const MAX_PERSONALIZATION_STRING_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits const MAX_ADDITIONAL_INPUT_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits @@ -62,16 +71,17 @@ impl HashDRBG80090AParams for HashDRBG80090AParams_SHA512 { const RESEED_INTERVAL: u64 = 1u64 << 48; // 2^48 requests } -// TODO: replace / simplify this once the generic_const_exprs feature lands in the stable rust compiler. -const LARGEST_HASHER_OUTPUT_LEN: usize = 64; +/// The backing arrays for `V` and `C` are sized for the largest `seedlen` (SHA-512's 111 bytes) +/// and used as `[..H::SEED_LEN]`, since stable Rust cannot size them by an associated constant. +const LARGEST_SEED_LEN: usize = 888 / 8; +/// The largest `outlen`, for the hash output buffers that are likewise sliced per hash. +const LARGEST_HASHER_OUTPUT_LEN: usize = SHA512::OUTPUT_LEN; #[allow(private_bounds)] /// Implementation of the Hash_DRBG algorithm as specified in NIST SP 800-90Ar1. pub struct HashDRBG80090A { _phantom: core::marker::PhantomData, - // TODO: replace / simplify this once the generic_const_exprs feature lands in the stable rust compiler. - // state: WorkingState, - state: WorkingState, + state: WorkingState, admin_info: AdministrativeInfo, } @@ -145,9 +155,9 @@ impl NewUninitialized for HashDRBG80090A { fn new_uninitialized() -> Self { Self { _phantom: core::marker::PhantomData, - state: WorkingState:: { - v: Secret::<[u8; LARGEST_HASHER_OUTPUT_LEN]>::new(), - c: Secret::<[u8; LARGEST_HASHER_OUTPUT_LEN]>::new(), + state: WorkingState:: { + v: Secret::<[u8; LARGEST_SEED_LEN]>::new(), + c: Secret::<[u8; LARGEST_SEED_LEN]>::new(), reseed_counter: Secret::new(), }, admin_info: AdministrativeInfo { @@ -228,31 +238,30 @@ impl Sp80090ADrbg for HashDRBG80090A { // 1. seed_material = entropy_input || nonce || personalization_string. // 2. seed = Hash_df (seed_material, seedlen). // 3. V = seed. + let v = &mut (*self.state.v)[..H::SEED_LEN]; match H::HASH { SupportedHash::SHA256 => hash_df::( seed.ref_to_bytes(), nonce.ref_to_bytes(), personalization_string, &[0u8; 0], - &mut *self.state.v, + v, ), SupportedHash::SHA512 => hash_df::( seed.ref_to_bytes(), nonce.ref_to_bytes(), personalization_string, &[0u8; 0], - &mut *self.state.v, + v, ), } // 4. C = Hash_df ((0x00 || V), seedlen). Comment: Precede V with a byte of zeros. + let v = &(*self.state.v)[..H::SEED_LEN]; + let c = &mut (*self.state.c)[..H::SEED_LEN]; match H::HASH { - SupportedHash::SHA256 => { - hash_df::(&[0u8], &*self.state.v, &[0u8; 0], &[0u8; 0], &mut *self.state.c) - } - SupportedHash::SHA512 => { - hash_df::(&[0u8], &*self.state.v, &[0u8; 0], &[0u8; 0], &mut *self.state.c) - } + SupportedHash::SHA256 => hash_df::(&[0u8], v, &[0u8; 0], &[0u8; 0], c), + SupportedHash::SHA512 => hash_df::(&[0u8], v, &[0u8; 0], &[0u8; 0], c), } // 5. reseed_counter = 1. @@ -308,33 +317,26 @@ impl Sp80090ADrbg for HashDRBG80090A { // 1. seed_material = 0x01 || V || entropy_input || additional_input. // 2. seed = Hash_df (seed_material, seedlen). // 3. V = seed. - match H::HASH { - SupportedHash::SHA256 => hash_df::( - &[0x01], - &*self.state.v.clone(), - seed.ref_to_bytes(), - additional_input, - &mut *self.state.v, - ), - SupportedHash::SHA512 => hash_df::( - &[0x01], - &*self.state.v.clone(), - seed.ref_to_bytes(), - additional_input, - &mut *self.state.v, - ), - } - - // 4. C = Hash_df ((0x00 || V), seedlen). Comment: Preceed with a byte of all zeros. + let old_v = self.state.v.clone(); + let old_v = &(*old_v)[..H::SEED_LEN]; + let v = &mut (*self.state.v)[..H::SEED_LEN]; match H::HASH { SupportedHash::SHA256 => { - hash_df::(&[0u8], &*self.state.v, &[0u8; 0], &[0u8; 0], &mut *self.state.c) + hash_df::(&[0x01], old_v, seed.ref_to_bytes(), additional_input, v) } SupportedHash::SHA512 => { - hash_df::(&[0u8], &*self.state.v, &[0u8; 0], &[0u8; 0], &mut *self.state.c) + hash_df::(&[0x01], old_v, seed.ref_to_bytes(), additional_input, v) } } + // 4. C = Hash_df ((0x00 || V), seedlen). Comment: Preceed with a byte of all zeros. + let v = &(*self.state.v)[..H::SEED_LEN]; + let c = &mut (*self.state.c)[..H::SEED_LEN]; + match H::HASH { + SupportedHash::SHA256 => hash_df::(&[0u8], v, &[0u8; 0], &[0u8; 0], c), + SupportedHash::SHA512 => hash_df::(&[0u8], v, &[0u8; 0], &[0u8; 0], c), + } + // 5. reseed_counter = 1. *self.state.reseed_counter = 1; @@ -384,27 +386,28 @@ impl Sp80090ADrbg for HashDRBG80090A { // 2. If (additional_input ≠ Null), then do // 2.1 w = Hash (0x02 || V || additional_input). // 2.2 V = (V + w) mod 2^seedlen. + let v = &mut (*self.state.v)[..H::SEED_LEN]; if additional_input.len() > 0 { match H::HASH { SupportedHash::SHA256 => { let mut h = SHA256::new(); h.do_update(&[0x02]); - h.do_update(&*self.state.v); + h.do_update(v); h.do_update(additional_input); let mut w = [0u8; SHA256::OUTPUT_LEN]; h.do_final_out(&mut w); - add_to_array(&mut *self.state.v, &w); + add_to_array(v, &w); } SupportedHash::SHA512 => { let mut h = SHA512::new(); h.do_update(&[0x02]); - h.do_update(&*self.state.v); + h.do_update(v); h.do_update(additional_input); let mut w = [0u8; SHA512::OUTPUT_LEN]; h.do_final_out(&mut w); - add_to_array(&mut *self.state.v, &w); + add_to_array(v, &w); } } } @@ -416,36 +419,38 @@ impl Sp80090ADrbg for HashDRBG80090A { // But we do want to continue below to roll the state and increment the request counter. match H::HASH { SupportedHash::SHA256 => { - hashgen::(&*self.state.v, out); + hashgen::(v, out); } SupportedHash::SHA512 => { - hashgen::(&*self.state.v, out); + hashgen::(v, out); } } } - // 4. H = Hash (0x03 || V). - // let mut h = [0u8; H::OUT_LEN]; - let mut h = [0u8; 64]; + // 4. H = Hash (0x03 || V). `h` is sliced to the hash's outlen: H is an outlen-bit + // integer, added at the low end of V in step 5. + let mut h = [0u8; LARGEST_HASHER_OUTPUT_LEN]; + let h = &mut h[..H::OUTPUT_LEN]; match H::HASH { SupportedHash::SHA256 => { let mut sha = SHA256::default(); sha.do_update(&[0x03]); - sha.do_update(&*self.state.v); - sha.do_final_out(&mut h); + sha.do_update(v); + sha.do_final_out(h); } SupportedHash::SHA512 => { let mut sha = SHA512::default(); sha.do_update(&[0x03]); - sha.do_update(&*self.state.v); - sha.do_final_out(&mut h); + sha.do_update(v); + sha.do_final_out(h); } }; - // 5. V = (V + H + C + reseed_counter) mod 2^seedlen. - add_to_array(&mut *self.state.v, &h); - add_to_array(&mut *self.state.v, &*self.state.c); - add_to_array(&mut *self.state.v, &self.state.reseed_counter.to_le_bytes()); + // 5. V = (V + H + C + reseed_counter) mod 2^seedlen. Every operand is a big-endian + // integer aligned at the low end of V; the counter's bytes are its big-endian ones. + add_to_array(v, h); + add_to_array(v, &(*self.state.c)[..H::SEED_LEN]); + add_to_array(v, &self.state.reseed_counter.to_be_bytes()); // 6. reseed_counter = reseed_counter + 1. *self.state.reseed_counter += 1; @@ -526,8 +531,11 @@ impl RNG for HashDRBG80090A { /*** Internal Helper Functions ***/ -/// 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. +/// `Hash_df` as defined in SP 800-90A Rev. 1 Section 10.3.1: `temp = temp || Hash(counter || +/// no_of_bits_to_return || input_string)` for `len = ceil(no_of_bits_to_return / outlen)` blocks, +/// with `counter` an 8-bit value starting at 1 and `no_of_bits_to_return` "represented as a +/// 32-bit integer", i.e. as its four big-endian bytes, which is what the CAVP vectors use. +/// `no_of_bits_to_return` is eight times 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. // To leave a parameter unused, simply provide an empty array &[0u8;0] fn hash_df( @@ -555,8 +563,8 @@ fn hash_df( // out of the loop and by merging i and counter into the same variable. for i in 1..len { let mut h = H::default(); - h.do_update(&counter.to_le_bytes()); - h.do_update(&no_of_bits_to_return.to_le_bytes()); + h.do_update(&[counter]); + h.do_update(&no_of_bits_to_return.to_be_bytes()); h.do_update(in1); h.do_update(in2); h.do_update(in3); @@ -573,15 +581,14 @@ fn hash_df( let remainder = out.len() - bytes_written; if remainder != 0 { let mut h = H::default(); - h.do_update(&counter.to_le_bytes()); - h.do_update(&no_of_bits_to_return.to_le_bytes()); + h.do_update(&[counter]); + h.do_update(&no_of_bits_to_return.to_be_bytes()); h.do_update(in1); h.do_update(in2); h.do_update(in3); h.do_update(in4); - // let mut temp = [0u8; H::OUTPUT_LEN]; - let mut temp = [0u8; 64]; + let mut temp = [0u8; LARGEST_HASHER_OUTPUT_LEN]; h.do_final_out(&mut temp); // Copy only what we need from the last block. @@ -589,26 +596,35 @@ fn hash_df( } } +/// Known answers computed independently in Python from Section 10.3.1 (`hashlib`, `counter` as +/// one byte from 1, `no_of_bits_to_return` as `to_bytes(4, "big")`), not from this function; the +/// previous expected value here was this function's own output and anchored a little-endian +/// length. The CAVP `Hash_DRBG.rsp` suites in `tests/` are the end-to-end check. #[test] fn test_hash_df() { - // success case + fn hex(s: &str) -> Vec { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() + } + // The four inputs concatenate to 01 02 03 04 05 06 07 08 09; 100 bytes of output. let mut out = [0u8; 100]; hash_df::(&[0x01, 0x02, 0x03], &[0x04, 0x05], &[0x06, 0x07], &[0x08, 0x09], &mut out); - assert_ne!(out, [0u8; 100]); - // repeatability test - // println!("out: {:?}", out); assert_eq!( - out, - [ - 150u8, 177u8, 87u8, 145u8, 138u8, 4u8, 164u8, 14u8, 162u8, 43u8, 159u8, 152u8, 121u8, - 117u8, 6u8, 18u8, 253u8, 84u8, 41u8, 64u8, 40u8, 209u8, 16u8, 176u8, 106u8, 115u8, - 172u8, 193u8, 246u8, 228u8, 208u8, 79u8, 37u8, 31u8, 134u8, 141u8, 200u8, 7u8, 42u8, - 199u8, 229u8, 236u8, 236u8, 186u8, 28u8, 87u8, 200u8, 14u8, 127u8, 36u8, 132u8, 23u8, - 36u8, 150u8, 23u8, 215u8, 247u8, 121u8, 175u8, 82u8, 99u8, 187u8, 235u8, 25u8, 213u8, - 18u8, 106u8, 22u8, 4u8, 99u8, 1u8, 184u8, 211u8, 160u8, 177u8, 67u8, 78u8, 181u8, 69u8, - 51u8, 117u8, 2u8, 72u8, 36u8, 134u8, 72u8, 2u8, 9u8, 105u8, 149u8, 136u8, 35u8, 81u8, - 114u8, 142u8, 80u8, 94u8, 42u8, 85u8, 155 - ] + out.to_vec(), + hex( + "abeaf302decd192379a95a7a11d248a6e42a631b776d290d4e3003ad6d773f2c3d3943c83dbf789f45629ea0\ + 3c48353bf16fc6d193d61422de4683a2b369a106aa4269a1a40acd90f572ef5206cd4370ac43366f0b8e9d85\ + 314eaac6352274add09c4097" + ) + ); + let mut out = [0u8; 100]; + hash_df::(&[0x01, 0x02, 0x03], &[0x04, 0x05], &[0x06, 0x07], &[0x08, 0x09], &mut out); + assert_eq!( + out.to_vec(), + hex( + "c056d412f5f174c371b71c63ca77eeaaacddca954ce583d09100504eed4f15e15439df83becf02b36967f51c\ + 70dd8fa6bf4263c23635cd3cc512d8442e63ed700ddec045ea860082843abc26291c1fc763117eb274d81581\ + 1a456eac69b0363ac6ecbc81" + ) ); // Test success with out.len() at the maximum allowed for SHA256 (255 * 32 = 8160) @@ -655,9 +671,9 @@ fn hashgen(v: &[u8], out: &mut [u8]) { let m = u32::div_ceil(out.len() as u32, H::OUTPUT_LEN as u32); - // requested_no_of_bits = out.len() - // let mut data= [0u8; H::OUTPUT_LEN]; - let mut data = [0u8; 64]; + // requested_no_of_bits = out.len(); `data` is seedlen bytes, as `v` is. + let mut data = [0u8; LARGEST_SEED_LEN]; + let data = &mut data[..v.len()]; data.copy_from_slice(v); // W = out @@ -667,10 +683,10 @@ fn hashgen(v: &[u8], out: &mut [u8]) { // 4.3 data = (data + 1) mod 2^seedlen. for i in 1..m { H::hash_out( - &data, + data, &mut out[((i - 1) as usize) * H::OUTPUT_LEN..(i as usize) * H::OUTPUT_LEN], ); - add_to_array(&mut data, &[0x01]); + add_to_array(data, &[0x01]); } // Handle the last block separately since not all of it will fit in the output buffer. @@ -679,9 +695,8 @@ fn hashgen(v: &[u8], out: &mut [u8]) { let bytes_written = (m - 1) as usize * H::OUTPUT_LEN; let remainder = out.len() - bytes_written; if remainder != 0 { - // let mut temp = [0u8; H::OUTPUT_LEN]; - let mut temp = [0u8; 64]; - H::hash_out(&data, &mut temp); + let mut temp = [0u8; LARGEST_HASHER_OUTPUT_LEN]; + H::hash_out(data, &mut temp); // Copy only what we need from the last block. out[bytes_written..].copy_from_slice(&temp[..remainder as usize]); diff --git a/crypto/rng/tests/cavp_hash_drbg.rs b/crypto/rng/tests/cavp_hash_drbg.rs new file mode 100644 index 000000000..32e27e014 --- /dev/null +++ b/crypto/rng/tests/cavp_hash_drbg.rs @@ -0,0 +1,135 @@ +//! NIST CAVP Hash_DRBG vectors (`drbgtestvectors.zip`, CAVS 14.3, 2013-04-02), from +//! `bc-test-data/crypto/cavp/drbg/`: `drbgvectors_no_reseed/Hash_DRBG.rsp` (instantiate, two +//! generates) and `drbgvectors_pr_false/Hash_DRBG.rsp` (instantiate, reseed, two generates), the +//! SHA-256 and SHA-512 sections, with and without a personalization string and additional input. +//! The second generate's output is `ReturnedBits`. `drbgvectors_pr_true` is not read: the DRBG +//! does not support prediction resistance. The other hashes' sections are skipped: SHA-256 and +//! SHA-512 are the two the crate implements. +//! +//! A missing `bc-test-data` checkout skips these with the loader's warning, as the other crates' +//! CAVP suites do. + +use bouncycastle_core::key_material::{KeyMaterial512, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rng::hazmat::NewUninitialized; +use bouncycastle_rng::{HashDRBG_SHA256, HashDRBG_SHA512, Sp80090ADrbg}; + +/// One `COUNT` block: the fields in file order, with the two `AdditionalInput` lines kept apart. +#[derive(Default)] +struct Record { + count: String, + entropy: Vec, + nonce: Vec, + personalization: Vec, + entropy_reseed: Option>, + additional_reseed: Vec, + additional: Vec>, + returned_bits: Vec, +} + +/// The records of every `[SHA-256]` and `[SHA-512]` section, tagged with the section's hash. +fn parse(contents: &str) -> Vec<(&'static str, Record)> { + let mut out = Vec::new(); + let mut hash: Option<&'static str> = None; + let mut record: Option = None; + for line in contents.lines().map(str::trim) { + if line.starts_with('#') { + continue; + } + if line.is_empty() { + if let (Some(h), Some(r)) = (hash, record.take()) { + out.push((h, r)); + } + continue; + } + if let Some(section) = line.strip_prefix('[') { + let section = section.trim_end_matches(']'); + if !section.contains('=') { + hash = match section { + "SHA-256" => Some("SHA-256"), + "SHA-512" => Some("SHA-512"), + _ => None, + }; + } + continue; + } + let (key, value) = line.split_once('=').expect("a `key = value` line"); + let (key, value) = (key.trim(), value.trim()); + let bytes = || hex_decode(value).expect("hex"); + if key == "COUNT" { + record = Some(Record { count: value.to_string(), ..Default::default() }); + continue; + } + let r = record.as_mut().expect("a field inside a COUNT block"); + match key { + "EntropyInput" => r.entropy = bytes(), + "Nonce" => r.nonce = bytes(), + "PersonalizationString" => r.personalization = bytes(), + "EntropyInputReseed" => r.entropy_reseed = Some(bytes()), + "AdditionalInputReseed" => r.additional_reseed = bytes(), + "AdditionalInput" => r.additional.push(bytes()), + "ReturnedBits" => r.returned_bits = bytes(), + other => panic!("unexpected field {other}"), + } + } + if let (Some(h), Some(r)) = (hash, record.take()) { + out.push((h, r)); + } + out +} + +fn seed(bytes: &[u8]) -> KeyMaterial512 { + KeyMaterial512::from_bytes_as_type(bytes, KeyType::Seed).expect("a seed") +} + +/// Drives one record as the CAVP procedure does and returns the second generate's output. +fn run(r: &Record, strength: SecurityStrength) -> Vec { + let mut drbg = D::new_uninitialized(); + drbg.instantiate(false, seed(&r.entropy), &seed(&r.nonce), &r.personalization, strength) + .unwrap_or_else(|e| panic!("COUNT {}: instantiate: {e:?}", r.count)); + if let Some(entropy_reseed) = &r.entropy_reseed { + drbg.reseed(&seed(entropy_reseed), &r.additional_reseed) + .unwrap_or_else(|e| panic!("COUNT {}: reseed: {e:?}", r.count)); + } + assert_eq!(r.additional.len(), 2, "COUNT {}: two AdditionalInput lines", r.count); + let n = r.returned_bits.len(); + drbg.generate(&r.additional[0], n) + .unwrap_or_else(|e| panic!("COUNT {}: first generate: {e:?}", r.count)); + drbg.generate(&r.additional[1], n) + .unwrap_or_else(|e| panic!("COUNT {}: second generate: {e:?}", r.count)) +} + +fn check_file(dir: &str) { + let Some(contents) = bc_test_data(&format!("crypto/cavp/drbg/{dir}"), "Hash_DRBG.rsp") else { + return; + }; + let records = parse(&contents); + let (mut sha256, mut sha512) = (0, 0); + for (hash, r) in &records { + let got = match *hash { + "SHA-256" => { + sha256 += 1; + run::(r, SecurityStrength::_128bit) + } + _ => { + sha512 += 1; + run::(r, SecurityStrength::_256bit) + } + }; + assert_eq!(got, r.returned_bits, "{dir} {hash} COUNT {}", r.count); + } + println!("{dir}: {sha256} SHA-256 and {sha512} SHA-512 Hash_DRBG vectors passed"); + assert!(sha256 >= 100 && sha512 >= 100, "{dir}: the SHA-256 and SHA-512 sections were read"); +} + +#[test] +fn cavp_hash_drbg_no_reseed() { + check_file("drbgvectors_no_reseed"); +} + +#[test] +fn cavp_hash_drbg_pr_false() { + check_file("drbgvectors_pr_false"); +} From 555a9f739b2606cf95391b65e2fa975b28497001 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 03:34:29 +1100 Subject: [PATCH 18/57] hkdf, sha2: the 2026-11-03 hmac/hkdf review's HKDF hardening items K2 and K3 (REVIEW-2026-11-03-hmac-hkdf-correctness-mythos.md, at 94d4e966): a resumed state's strength label and entropy counter are checked against the hash and the state tag, and a salt credits entropy only when the caller types it MACKey; K1 and the bug H1 had landed with PR #172 and 967ae572 K2: from_suspended refuses a security strength above the hash's cap (half the output, the min(K, 2C) rule credit_entropy already applies, now the tracker's MAX_STRENGTH) and refuses a non-zero entropy counter or a non-None strength on a state whose tag is Uninitialized, which new() never writes. Before, a pre-init state with those two bytes edited, holding no secret at all, resumed and labelled the PRK of HMAC(0^64, "") as CryptographicRandom at _256bit on HKDF-SHA256. K3: do_extract_init credits the salt only when it is typed MACKey. RFC 5869 Section 2.2 defines the salt as "a non-secret random value" and SP 800-133r2 Section 6.3 note (g) counts the component keys' entropy, not the salt's, so a CryptographicRandom salt (a nonce from an RNG) with an empty IKM gave a PRK anyone who knows the salt can compute, labelled full-entropy material at full strength; it is now Unknown at None. A MACKey salt is SP 800-56Cr2 Section 5.1's secret salt and is credited as before, which is how every existing test and the CLI type theirs. An Unknown-typed IKM stays accepted and uncredited, as the core KDF trait describes; the HKDF docs said it was refused and now say what happens. The crate's and sha2's Security Considerations state the salt rule. Tests: a_public_salt_credits_no_entropy (one-shot and streaming, the same PRK bytes either way) and a_forged_state_is_refused (strength above the cap, the cap itself, entropy or strength on a pre-init state, an honest pre-init state). Not changed from the review: K1 (saturating counter, width-checked on resume) and H1 (HmacKey::generate's strength cap) are already on this branch; H2 and H3 are hmac's; the notes K4-K10 and H4-H7. Assisted-by: Claude:claude-fable-5-1 --- crypto/hkdf/src/lib.rs | 51 +++++++++++++++++------ crypto/hkdf/tests/hkdf_tests.rs | 74 +++++++++++++++++++++++++++++++++ crypto/sha2/src/hkdf.rs | 4 +- 3 files changed, 116 insertions(+), 13 deletions(-) diff --git a/crypto/hkdf/src/lib.rs b/crypto/hkdf/src/lib.rs index bed49ab0e..05e65a1af 100644 --- a/crypto/hkdf/src/lib.rs +++ b/crypto/hkdf/src/lib.rs @@ -87,7 +87,10 @@ //! * HKDF is keyed by its `salt`, which keys the extract-phase HMAC. The salt is deliberately //! excluded from the suspended state and must be re-supplied on resume; resuming with a different //! salt cannot be detected and silently produces a different PRK. -//! * Entropy is credited only for input supplied via [`HKDF::do_extract_update_key`]. Bytes supplied +//! * Entropy is credited only for input supplied via [`HKDF::do_extract_update_key`], and for a +//! salt typed [`KeyType::MACKey`], which is how a caller marks it as the secret salt of SP +//! 800-56Cr2 Section 5.1. A salt of any other type is RFC 5869 Section 2.2's "non-secret random +//! value" and credits nothing: anyone who knows it can compute the PRK from the IKM. Bytes supplied //! via [`HKDF::do_extract_update_bytes`] are treated as uncredited context, so a PRK derived only //! from raw bytes will not be tagged as full-entropy key material even if those bytes were random. //! [`HKDF::do_extract_update_key`] is rejected after the first call to @@ -218,6 +221,10 @@ struct HkdfEntropyTracker { } impl HkdfEntropyTracker { + /// The most the label can say: HMAC's strength is bounded by half the hash output (the + /// `min(K, 2C)` rule), so no input, and no resumed state, raises it past this. + const MAX_STRENGTH: SecurityStrength = SecurityStrength::from_bytes(H::OUTPUT_LEN / 2); + fn new() -> Self { Self { _phantomhash: PhantomData, entropy: 0, security_strength: SecurityStrength::None } } @@ -229,8 +236,7 @@ impl HkdfEntropyTracker { // Saturating: the counter is bookkeeping, and a resumed state may carry any value. self.entropy = self.entropy.saturating_add(additional_entropy); self.security_strength = max(&self.security_strength, &key.security_strength()).clone(); - self.security_strength = - min(&self.security_strength, &SecurityStrength::from_bytes(H::OUTPUT_LEN / 2)).clone(); + self.security_strength = min(&self.security_strength, &Self::MAX_STRENGTH).clone(); additional_entropy } @@ -304,8 +310,11 @@ impl /// Output: /// PRK a pseudorandom key (of HashLen octets) /// - /// The KeyMaterial input parameters can be of any [`KeyType`] but [`KeyType::Unknown`], which is - /// refused as [`KeyMaterialError::InvalidKeyType`]; the type of the output is set from the inputs. + /// The salt can be of any [`KeyType`] but [`KeyType::Unknown`], which is refused as + /// [`KeyMaterialError::InvalidKeyType`]; the IKM can be of any type, and credits entropy only + /// when it is a full-entropy one. The salt credits entropy only when it is typed + /// [`KeyType::MACKey`], a secret salt; see the crate's Security Considerations. The type of the + /// output is set from what was credited. /// The output KeyMaterial will be of fixed size, with a capacity large enough to cover any /// underlying hash function, but the actual key length will be appropriate to the underlying hash function. /// @@ -358,8 +367,9 @@ impl /// therefore this function is provided only as expand_out(), filling the provided KeyMaterial object, /// and no analogous expand() is provided. /// - /// The KeyMaterial input parameters can be of any [`KeyType`] but [`KeyType::Unknown`], which is - /// refused as [`KeyMaterialError::InvalidKeyType`]; the type of the output is set from the inputs. + /// The salt can be of any [`KeyType`] but [`KeyType::Unknown`], which is refused as + /// [`KeyMaterialError::InvalidKeyType`]; the IKM can be of any type. The type of the output is + /// set from what was credited, as [`Self::extract_out`] says. /// /// L is the output length. [`KDFError::InvalidLength`] is returned if L exceeds 255 * HashLen, /// if `okm` cannot hold L bytes, or if `prk` is shorter than HashLen (RFC 5869 Section 2.3). @@ -554,10 +564,11 @@ impl /// This, together with [`HKDF::do_extract_update_key`], [`HKDF::do_extract_update_bytes`] and [`HKDF::do_extract_final`] /// provide a streaming interface for very long values of `ikm`. - /// In this mode, the entropy of `ikm` is untracked, and so only the entropy ef `salt` is taken into account - /// when computing the entropy of the output `prk`. - /// The KeyMaterial input parameters can be of any [`KeyType`] but [`KeyType::Unknown`], which is - /// refused as [`KeyMaterialError::InvalidKeyType`]; the type of the output is set from the inputs. + /// In this mode, entropy is credited for the keys given to [`HKDF::do_extract_update_key`] and + /// for a salt typed [`KeyType::MACKey`] (a secret salt); a salt of any other type credits + /// nothing, since RFC 5869 Section 2.2's salt is "a non-secret random value". + /// The salt can be of any [`KeyType`] but [`KeyType::Unknown`], which is refused as + /// [`KeyMaterialError::InvalidKeyType`]; the type of the output is set from what was credited. /// The output KeyMaterial will be of fixed size, with a capacity large enough to cover any /// underlying hash function, but the actual key length will be appropriate to the underlying hash function. /// @@ -576,7 +587,12 @@ impl let inner = hmac_hazmat::inner_state(&key); self.hmac = Some(HkdfExtract { key, inner }); - let additional_entropy = self.entropy.credit_entropy(salt); + // RFC 5869 Section 2.2 defines the salt as "a non-secret random value", and SP 800-133r2 + // Section 6.3 note (g) counts the component keys' entropy, not the salt's: a salt the + // caller has not marked as a key credits nothing, or a public salt alone would label the + // PRK full-entropy key material. A MACKey salt is SP 800-56Cr2 Section 5.1's secret salt. + let additional_entropy = + if salt.key_type() == KeyType::MACKey { self.entropy.credit_entropy(salt) } else { 0 }; self.state = HkdfStates::Initialized; Ok(additional_entropy) @@ -876,6 +892,17 @@ where usize::MAX, )?; let security_strength = SecurityStrength::try_from(state[13 + HASH_STATE_LEN])?; + // The bookkeeping is checked against what this type writes: the label never exceeds the + // hash's cap (credit_entropy applies it), and a state that has not been initialized + // carries no entropy and no strength (new() has none). Otherwise a state whose two + // bookkeeping bytes were edited would label a PRK derived from nothing as full-entropy + // material at a strength the hash cannot give. + if security_strength > HkdfEntropyTracker::::MAX_STRENGTH + || (hkdf_state == HkdfStates::Uninitialized + && (entropy != 0 || security_strength != SecurityStrength::None)) + { + return Err(SuspendableError::InvalidData); + } Ok(HKDF { hmac, diff --git a/crypto/hkdf/tests/hkdf_tests.rs b/crypto/hkdf/tests/hkdf_tests.rs index 7fa4bb569..f08d52ffa 100644 --- a/crypto/hkdf/tests/hkdf_tests.rs +++ b/crypto/hkdf/tests/hkdf_tests.rs @@ -963,6 +963,80 @@ mod hkdf_tests { /// The entropy counter is bookkeeping a resumed state may set to anything: the next credit /// saturates rather than overflowing. + /// A salt is RFC 5869 Section 2.2's "non-secret random value" unless the caller types it as a + /// key: a `CryptographicRandom` salt credits nothing, so a PRK made from it and an empty IKM, + /// which anyone who knows the salt can compute, is `Unknown`; the same bytes typed `MACKey` + /// are the secret salt of SP 800-56Cr2 Section 5.1 and are credited. + #[test] + fn a_public_salt_credits_no_entropy() { + let empty = KeyMaterial0::new(); + let public = + KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::CryptographicRandom) + .unwrap(); + let prk = HKDF_SHA256::extract(&public, &empty).unwrap(); + assert_eq!(prk.key_type(), KeyType::Unknown); + assert_eq!(prk.security_strength(), SecurityStrength::None); + + let secret = + KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::MACKey).unwrap(); + let prk_secret = HKDF_SHA256::extract(&secret, &empty).unwrap(); + assert_eq!(prk_secret.key_type(), KeyType::CryptographicRandom); + assert_eq!(prk_secret.security_strength(), SecurityStrength::_128bit); + assert_eq!(prk_secret.ref_to_bytes(), prk.ref_to_bytes(), "the same PRK bytes either way"); + + let mut hkdf = HKDF_SHA256::new(); + assert_eq!(hkdf.do_extract_init(&public).unwrap(), 0, "nothing credited for a public salt"); + assert_eq!(hkdf.get_entropy(), 0); + let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[32..64], KeyType::Seed).unwrap(); + assert_eq!(hkdf.do_extract_update_key(&seed).unwrap(), 32, "the IKM is credited"); + assert!(hkdf.is_fully_seeded()); + } + + /// The two bookkeeping fields of a suspended state are checked on resume: a strength label + /// above what the hash can give (half its output), or any entropy or strength on a state that + /// was never initialized, is refused, where it used to label a PRK derived from nothing as + /// `CryptographicRandom` at `_256bit`. + #[test] + fn a_forged_state_is_refused() { + use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core::traits::SuspendableKeyed; + use bouncycastle_sha2::hkdf::SUSPENDED_HKDF_SHA256_STATE_LEN; + + let salt = KeyMaterial128::from_bytes_as_type(&DUMMY_SEED[..16], KeyType::MACKey).unwrap(); + // Layout: [version(3) | present flag | hmac blob | state | entropy(8) | strength]. + let entropy_idx = SUSPENDED_HKDF_SHA256_STATE_LEN - 9; + let strength_idx = SUSPENDED_HKDF_SHA256_STATE_LEN - 1; + let refused = |state, what: &str| { + assert!( + matches!( + HKDF_SHA256::from_suspended(state, &salt), + Err(SuspendableError::InvalidData) + ), + "{what} must be refused" + ); + }; + + let mut hkdf = HKDF_SHA256::new(); + hkdf.do_extract_init(&salt).unwrap(); + let good = hkdf.suspend(); + assert_eq!(good[strength_idx], SecurityStrength::_128bit as u8, "layout check"); + let mut forged = good; + forged[strength_idx] = SecurityStrength::_256bit as u8; + refused(forged, "a strength above HMAC-SHA256's 128-bit cap"); + let mut edge = good; + edge[strength_idx] = SecurityStrength::_128bit as u8; + assert!(HKDF_SHA256::from_suspended(edge, &salt).is_ok(), "the cap itself resumes"); + + let pre_init = HKDF_SHA256::new().suspend(); + assert!(HKDF_SHA256::from_suspended(pre_init, &salt).is_ok(), "an honest pre-init state"); + let mut forged = pre_init; + forged[entropy_idx..entropy_idx + 8].copy_from_slice(&64u64.to_le_bytes()); + refused(forged, "entropy on a state that was never initialized"); + let mut forged = pre_init; + forged[strength_idx] = SecurityStrength::_128bit as u8; + refused(forged, "a strength on a state that was never initialized"); + } + #[test] fn a_resumed_entropy_counter_saturates() { use bouncycastle_core::traits::SuspendableKeyed; diff --git a/crypto/sha2/src/hkdf.rs b/crypto/sha2/src/hkdf.rs index f8a2589a0..640e26754 100644 --- a/crypto/sha2/src/hkdf.rs +++ b/crypto/sha2/src/hkdf.rs @@ -205,7 +205,9 @@ //! //! * Resuming a suspended HKDF with a different salt cannot be detected and silently produces a //! different PRK; see the suspend/resume section above. -//! * Entropy is only credited for input supplied via [`HKDF::do_extract_update_key`]. Bytes supplied +//! * Entropy is only credited for input supplied via [`HKDF::do_extract_update_key`], and for a salt +//! typed [`bouncycastle_core::key_material::KeyType::MACKey`] (a secret salt); any other salt is RFC +//! 5869's non-secret value and credits nothing. Bytes supplied //! via [`HKDF::do_extract_update_bytes`] are treated as uncredited context, so a PRK derived only //! from raw bytes will not be tagged as full-entropy key material even if those bytes were in fact //! random. From d8a04fc98d46e7cac5f219f42598a6c4e5482f19 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 03:45:56 +1100 Subject: [PATCH 19/57] sha3: the 2026-11-03 review's doc notes S-N2 and S-N3: the KDF impl is described as the plain hash of the concatenated inputs it is, not as SP 800-56C's one-step KDF (which puts a counter first); the Memory Usage rows for KMACXOF (448) and KMAC (456) match the bench; SHAKE's `new` doc says SHAKE Assisted-by: Claude:claude-fable-5-1 --- crypto/sha3/src/lib.rs | 10 +++++----- crypto/sha3/src/sha3.rs | 4 +++- crypto/sha3/src/shake.rs | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 5ce50b3b9..1e2650b74 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -108,8 +108,9 @@ //! SHA3 offers Key Derivation Functions in the form of KDF, which is accessed through the [`KDF`] trait, //! which is implemented by all SHA3 and SHAKE variants. //! [`KDF`] acts on [`KeyMaterial`] objects as both the input and output values. -//! In the case of SHA3, the [`KDF`] interfaces are simple wrapper functions around the underlying SHA3 or SHAKE -//! primitive that correctly maintains the length and entropy metadata of the key material that it is acting on. +//! In the case of SHA3, the [`KDF`] interfaces hash the concatenated input keys and additional input +//! with the underlying SHA3 or SHAKE primitive, maintaining the length and entropy metadata of the key +//! material; they are not the SP 800-56Cr2 one-step KDF, which puts a counter before the input. //! This is intended to act as a developer aid to prevent some classes of developer mistakes, such as //! deriving a cryptographic key from uninitialized (aka zeroized) input key material, or using low-entropy //! input key material to derive a MAC, symmetric, or asymmetric key. @@ -192,9 +193,8 @@ //! | Object | Size (bytes) | //! |-----------------------------------------------------------------|--------------| //! | `SHA3_224` .. `SHA3_512`, `SHAKE128/256`, `SHAKESqueezer` | 440 | -//! | `CSHAKE128/256`, `TUPLEHASHXOF128/256`, `CSHAKESqueezer` | 448 | -//! | `KMACXOF128/256`, `TUPLEHASH128/256` | 456 | -//! | `KMAC128/256` | 464 | +//! | `CSHAKE128/256`, `KMACXOF128/256`, `TUPLEHASHXOF128/256`, `CSHAKESqueezer` | 448 | +//! | `KMAC128/256`, `KMAC128Key`/`KMAC256Key`, `TUPLEHASH128/256` | 456 | //! | `PARALLELHASHXOF128/256` | 912 | //! | `PARALLELHASH128/256` | 920 | //! diff --git a/crypto/sha3/src/sha3.rs b/crypto/sha3/src/sha3.rs index a807b3f69..8a9f85773 100644 --- a/crypto/sha3/src/sha3.rs +++ b/crypto/sha3/src/sha3.rs @@ -225,7 +225,9 @@ impl Hash for SHA3Internal { } } -/// SHA3 is allowed to be used as a KDF in the form HASH(X) as per NIST SP 800-56C. +/// A plain hash of the concatenated inputs, `H(key_1 || .. || key_n || additional_input)`, with +/// the entropy bookkeeping of [`KeyMaterial`]. This is not the SP 800-56Cr2 Section 4 one-step +/// KDF, which hashes a 4-byte big-endian counter before `Z || FixedInfo` on every block. impl KDF for SHA3Internal { /// Returns a [`KeyMaterial`]. /// For the KDF to be considered "fully-seeded" and be capable of outputting full-entropy KeyMaterials, diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index 6b63f8336..ff82eab2e 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -35,7 +35,7 @@ impl Algorithm for SHAKEInternal { } impl SHAKEInternal { - /// Get a new SHA3 instance, ready for use. + /// Get a new SHAKE instance, ready for use. pub fn new() -> Self { Self { _phantomdata: core::marker::PhantomData, From c5a7dae424dedb4011d40e1b2037185107420d56 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 03:45:56 +1100 Subject: [PATCH 20/57] hmac: the 2026-11-03 review's doc notes H5 (b)-(d): the crate doc's instantiation table lists all six SHA-2 and four SHA-3 aliases and HMAC_SM3, the usage section has the heading QUALITY_AND_STYLE names, and Display/Debug print HMACParams::HMAC_ALG_NAME rather than "HMAC-" plus the hash's name Assisted-by: Claude:claude-fable-5-1 --- crypto/hmac/src/lib.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index d16764098..7dd5f7ef6 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -10,13 +10,14 @@ //! //! | Hash family | Instantiations | //! |-------------|------------------------------------------------------------------------------| -//! | SHA-2 | `bouncycastle_sha2::hmac` -- `HMAC_SHA224` .. `HMAC_SHA512_256` | -//! | SHA-3 | `bouncycastle_sha3::hmac` -- `HMAC_SHA3_224` .. `HMAC_SHA3_512` | +//! | SHA-2 | `bouncycastle_sha2::hmac` -- `HMAC_SHA224`, `HMAC_SHA256`, `HMAC_SHA384`, `HMAC_SHA512`, `HMAC_SHA512_224`, `HMAC_SHA512_256` | +//! | SHA-3 | `bouncycastle_sha3::hmac` -- `HMAC_SHA3_224`, `HMAC_SHA3_256`, `HMAC_SHA3_384`, `HMAC_SHA3_512` | +//! | SM3 | `bouncycastle_sm3::hmac` -- `HMAC_SM3` | //! //! Although users are free to implement [`Hash`] and [`HMACParams`] for a a hash function not included with the library, //! and will then be able to instantiate [`HMAC`] for it as well. //! -//! # Usage +//! # Usage Examples //! //! A key is built once, from key material of any length, and used for any number of messages: //! @@ -389,13 +390,13 @@ pub struct HMAC<'k, HASH: HMACParams> { impl Debug for HMAC<'_, HASH> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "HMAC-{} instance", HASH::ALG_NAME,) + write!(f, "{} instance", HASH::HMAC_ALG_NAME) } } impl Display for HMAC<'_, HASH> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "HMAC-{} instance", HASH::ALG_NAME,) + write!(f, "{} instance", HASH::HMAC_ALG_NAME) } } From 777401b9a612ea0f6c40de734a2ee05bae56439e Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 03:45:56 +1100 Subject: [PATCH 21/57] hkdf: the 2026-11-03 review's doc note K8: credited entropy is reported in bytes, the PRK is HashLen bytes, the SP 800-56Cr2 citation quotes Section 5.1's own sentence on RFC 5869 and says the expand layout is RFC 5869's rather than SP 800-108r1 Section 4.2's, the Drop comment gives the right reason (the hash types zeroize the pad states), two typos Assisted-by: Claude:claude-fable-5-1 --- crypto/hkdf/src/lib.rs | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/crypto/hkdf/src/lib.rs b/crypto/hkdf/src/lib.rs index 05e65a1af..c7f0d2ae4 100644 --- a/crypto/hkdf/src/lib.rs +++ b/crypto/hkdf/src/lib.rs @@ -1,5 +1,7 @@ //! The generic HMAC-based Extract-and-Expand Key Derivation Function (HKDF) construction, as per -//! RFC 5869, as allowed by NIST SP 800-56Cr2. +//! RFC 5869, which NIST SP 800-56Cr2 Section 5.1 describes as "a version of the above +//! extraction-then-expansion key-derivation procedure using HMAC for both the extraction and +//! expansion steps". //! //! This is a utility crate and is not intended to be used directly. It provides [`HKDF`] -- the //! construction, generic over any hash that implements [`HMACParams`], the extension point through @@ -179,7 +181,9 @@ struct HkdfExtract { inner: H, } -// Note: does not need to impl Drop because HKDF itself does not hold any sensitive state data. +// No Drop impl of its own: the sensitive state is the salt-keyed pad states inside `hmac` (RFC +// 2104 Section 4 says they "need to be treated and protected the same as secret keys"), and the +// hash types zeroize those in their own Drop. #[derive(Clone, Debug, PartialOrd, PartialEq)] #[repr(u8)] @@ -319,7 +323,7 @@ impl /// underlying hash function, but the actual key length will be appropriate to the underlying hash function. /// /// Salt is optional, which is indicated by providing an uninitialized KeyMaterial object of length zero, - /// the capacity is irrelevant, so KeyMateriol256::new() or KeyMaterial_internal::<0>::new() would both count as an absent salt. + /// the capacity is irrelevant, so KeyMaterial256::new() or KeyMaterial0::new() would both count as an absent salt. pub fn extract( salt: &impl KeyMaterialTrait, ikm: &impl KeyMaterialTrait, @@ -402,7 +406,7 @@ impl let hash_len = H::OUTPUT_LEN; if L > 255 * hash_len { return Err(KDFError::InvalidLength( - "HMAC can not produce more than 255*HashLen bytes out output", + "HKDF-Expand cannot produce more than 255*HashLen bytes of output", )); } @@ -509,7 +513,7 @@ impl } /// Salt is optional, which is indicated by providing an uninitialized KeyMaterial object of length zero, - /// the capacity is irrelevant, so KeyMateriol256::new() or KeyMaterial_internal::<0>::new() would both count as an absent salt. + /// the capacity is irrelevant, so KeyMaterial256::new() or KeyMaterial0::new() would both count as an absent salt. #[allow(non_snake_case)] pub fn extract_and_expand_out( salt: &impl KeyMaterialTrait, @@ -574,7 +578,7 @@ impl /// /// Salt is optional; to omit it, provide a KeyMaterial0, which will cause HKDF to use the default all-zero salt. /// - /// Returns the number of bits of entropy credited to this input key material. + /// Returns the number of bytes of entropy credited to this input key material. pub fn do_extract_init(&mut self, salt: &impl KeyMaterialTrait) -> Result { if self.state >= HkdfStates::Initialized { return Err(MACError::InvalidState("Initialized twice")); @@ -603,7 +607,7 @@ impl /// This function may be called zero or more times in a workflow. /// In particular, this function may be called multiple times to add more than one IKM. /// - /// Returns the number of bits of entropy credited to this input key material. + /// Returns the number of bytes of entropy credited to this input key material. pub fn do_extract_update_key( &mut self, ikm: &impl KeyMaterialTrait, @@ -636,7 +640,7 @@ impl /// State machine: this function must be called after [`HKDF::do_extract_init`], followed by /// zero or more calls of [`HKDF::do_extract_update_key`], and before [`HKDF::do_extract_final`]. /// - /// Returns the number of bits of entropy credited to this input key material, which is always 0 for this function. + /// Returns the number of bytes of entropy credited to this input key material, which is always 0 for this function. pub fn do_extract_update_bytes(&mut self, ikm_chunk: &[u8]) -> Result { if self.state == HkdfStates::Uninitialized { return Err(MACError::InvalidState( @@ -658,7 +662,7 @@ impl } /// Finish the HKDF-Extract phase and fill the provided `prk`. - /// Note that the provided KeyMaterial must be correctly sized to the HMAC block length. + /// The PRK is `HashLen` bytes (RFC 5869 Section 2.2), which the provided KeyMaterial always holds. #[allow(non_snake_case)] pub fn do_extract_final_out( self, @@ -699,8 +703,10 @@ impl } } -/// HKDF's extract-then-expand is the two-step key derivation of NIST SP 800-56Cr2 Section 5.1, with -/// HMAC as the auxiliary MAC. The default salt is empty: RFC 5869 Section 2.2 sets an absent salt +/// HKDF's extract-then-expand is RFC 5869's, which NIST SP 800-56Cr2 Section 5.1 names as "a +/// version of" its two-step key derivation "using HMAC for both the extraction and expansion +/// steps"; the expansion step's input layout (`T(i-1) || info || i`) is RFC 5869's own rather than +/// SP 800-108r1 Section 4.2's feedback mode. The default salt is empty: RFC 5869 Section 2.2 sets an absent salt /// to "a string of HashLen zeros", SP 800-56Cr2 Section 5.1 to an all-zero string of one hash input /// block, and HMAC zero-pads its key to a block, so the three key HMAC identically (see /// `salt_as_mac_key`). All key material and additional_input is mapped to HKDF's ikm input. From be9522c44db80bda4e289de089452e0fc2395c3c Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 03:45:56 +1100 Subject: [PATCH 22/57] rng: the 2026-11-03 review's doc notes R9, R10 and R16: the Sp80090ADrbg docs no longer promise an InsufficientSeedEntropy check the code never made (Section 8.4 limits the strength of a long output, not its length; the per-request limit is 2^19 bits as GenericError), say bytes where the code returns bytes, quote Section 8.6.7 for the nonce instead of "no entropy requirement", state the reseed policy (ReseedRequired after 2^48 requests until a full Section 10.1.1.3 reseed by the caller), that prediction resistance is unsupported and the OS-seeded constructors panic on an OS failure, and that the health test is not implemented; the four Table 2 limit constants are named for the bytes they hold; typos Assisted-by: Claude:claude-fable-5-1 --- crypto/rng/src/hash_drbg80090a.rs | 44 ++++++++++++----------- crypto/rng/src/lib.rs | 59 ++++++++++++++++++------------- 2 files changed, 57 insertions(+), 46 deletions(-) diff --git a/crypto/rng/src/hash_drbg80090a.rs b/crypto/rng/src/hash_drbg80090a.rs index d7fe63db6..1bdf8724e 100644 --- a/crypto/rng/src/hash_drbg80090a.rs +++ b/crypto/rng/src/hash_drbg80090a.rs @@ -33,10 +33,12 @@ trait HashDRBG80090AParams { /// are this long (Section 10.1.1.1), and every `mod 2^seedlen` in the generate algorithm is /// an addition over this many bytes. const SEED_LEN: usize; - const MAX_LENGTH: u64; - const MAX_PERSONALIZATION_STRING_LENGTH: u64; - const MAX_ADDITIONAL_INPUT_LENGTH: u64; - const MAX_NUMBER_OF_BITS_PER_REQUEST: u64; + /// Table 2's limits, in bytes: `max_length` of the entropy input, the personalization string + /// and the additional input are 2^35 bits, `max_number_of_bits_per_request` is 2^19 bits. + const MAX_SEED_LEN: u64; + const MAX_PERSONALIZATION_STRING_LEN: u64; + const MAX_ADDITIONAL_INPUT_LEN: u64; + const MAX_BYTES_PER_REQUEST: u64; const RESEED_INTERVAL: u64; } @@ -49,10 +51,10 @@ impl HashDRBG80090AParams for HashDRBG80090AParams_SHA256 { const OUTPUT_LEN: usize = SHA256::OUTPUT_LEN; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; const SEED_LEN: usize = 440 / 8; - const MAX_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits - const MAX_PERSONALIZATION_STRING_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits - const MAX_ADDITIONAL_INPUT_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits - const MAX_NUMBER_OF_BITS_PER_REQUEST: u64 = (1u64 << 19) / 8; // 2^19 bits + const MAX_SEED_LEN: u64 = (1u64 << 35) / 8; // 2^35 bits + const MAX_PERSONALIZATION_STRING_LEN: u64 = (1u64 << 35) / 8; // 2^35 bits + const MAX_ADDITIONAL_INPUT_LEN: u64 = (1u64 << 35) / 8; // 2^35 bits + const MAX_BYTES_PER_REQUEST: u64 = (1u64 << 19) / 8; // 2^19 bits const RESEED_INTERVAL: u64 = 1u64 << 48; // 2^48 requests } @@ -64,10 +66,10 @@ impl HashDRBG80090AParams for HashDRBG80090AParams_SHA512 { const OUTPUT_LEN: usize = SHA512::OUTPUT_LEN; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; const SEED_LEN: usize = 888 / 8; - const MAX_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits - const MAX_PERSONALIZATION_STRING_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits - const MAX_ADDITIONAL_INPUT_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits - const MAX_NUMBER_OF_BITS_PER_REQUEST: u64 = (1u64 << 19) / 8; // 2^19 bits + const MAX_SEED_LEN: u64 = (1u64 << 35) / 8; // 2^35 bits + const MAX_PERSONALIZATION_STRING_LEN: u64 = (1u64 << 35) / 8; // 2^35 bits + const MAX_ADDITIONAL_INPUT_LEN: u64 = (1u64 << 35) / 8; // 2^35 bits + const MAX_BYTES_PER_REQUEST: u64 = (1u64 << 19) / 8; // 2^19 bits const RESEED_INTERVAL: u64 = 1u64 << 48; // 2^48 requests } @@ -207,7 +209,7 @@ impl Sp80090ADrbg for HashDRBG80090A { todo!("Prediction resistance is not yet supported by Hash_DRBG80090A.") } - if personalization_string.len() as u64 > H::MAX_PERSONALIZATION_STRING_LENGTH { + if personalization_string.len() as u64 > H::MAX_PERSONALIZATION_STRING_LEN { return Err(RNGError::GenericError( "Personalization string exceeds the maximum length allowed by the DRBG instance", )); @@ -222,13 +224,13 @@ impl Sp80090ADrbg for HashDRBG80090A { "Provided seed must have a length that matches or exceeds the DRBG security strength.", ))?; } - if (seed.key_len() as u64) > H::MAX_LENGTH { + if (seed.key_len() as u64) > H::MAX_SEED_LEN { return Err(KeyMaterialError::SecurityStrength( "Provided seed exceeds the maximum seed length.", ))?; } // 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. + // because we assume it's pure entropy and hasn't been touched by any actual algorithms yet. if security_strength > H::MAX_SECURITY_STRENGTH { return Err(KeyMaterialError::SecurityStrength( "Requested security strength exceeds the maximum strength that this DRBG instance can provide.", @@ -291,7 +293,7 @@ impl Sp80090ADrbg for HashDRBG80090A { return Err(RNGError::Uninitialized); } - if additional_input.len() as u64 > H::MAX_ADDITIONAL_INPUT_LENGTH { + if additional_input.len() as u64 > H::MAX_ADDITIONAL_INPUT_LEN { return Err(RNGError::GenericError( "Additional input exceeds the maximum length allowed by the DRBG instance", )); @@ -301,14 +303,14 @@ impl Sp80090ADrbg for HashDRBG80090A { return Err(KeyMaterialError::InvalidKeyType("RNG seed must be KeyType::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. + // 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 algorithms yet. if (seed.key_len() as u32) < self.admin_info.strength.as_int() / 8 { return Err(KeyMaterialError::SecurityStrength( "Provided seed must have a length that matches or exceeds the DRBG security strength.", ))?; } - if (seed.key_len() as u64) > H::MAX_LENGTH { + if (seed.key_len() as u64) > H::MAX_SEED_LEN { return Err(KeyMaterialError::SecurityStrength( "Provided seed exceeds the maximum seed length.", ))?; @@ -365,12 +367,12 @@ impl Sp80090ADrbg for HashDRBG80090A { if !self.admin_info.instantiated { return Err(RNGError::Uninitialized); } - if out.len() as u64 > H::MAX_NUMBER_OF_BITS_PER_REQUEST { + if out.len() as u64 > H::MAX_BYTES_PER_REQUEST { return Err(RNGError::GenericError( - "Requested number of bits exceeds the maximum number of bits per request allowed by the DRBG instance", + "Requested number of bytes exceeds the maximum per request allowed by the DRBG instance", )); } - if additional_input.len() as u64 > H::MAX_ADDITIONAL_INPUT_LENGTH { + if additional_input.len() as u64 > H::MAX_ADDITIONAL_INPUT_LEN { return Err(RNGError::GenericError( "Additional input exceeds the maximum length allowed by the DRBG instance", )); diff --git a/crypto/rng/src/lib.rs b/crypto/rng/src/lib.rs index 8f854a2eb..d4af606b4 100644 --- a/crypto/rng/src/lib.rs +++ b/crypto/rng/src/lib.rs @@ -71,14 +71,18 @@ 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 -/// - health_test. -/// Note: this function implements Rust's Drop on the sensitive working state in place of the explicit -/// Uninstantiate function listed in SP 800-90Ar1. +/// The DRBG mechanism functions of SP 800-90A Rev. 1 Section 7.4: instantiate, generate and +/// reseed are the methods below; uninstantiate is Rust's `Drop` on the sensitive working state; +/// the health test is not implemented yet. `prediction_resistance` is not supported: only +/// `false` is accepted. A constructor that seeds from the OS (`new`, `new_from_os`, `Default`) +/// panics if the OS entropy source fails, since it cannot return an error. +/// +/// Reseeding is the caller's: after `reseed_interval` (2^48) requests every generating method, +/// the [`bouncycastle_core::traits::RNG`] ones included, returns [`RNGError::ReseedRequired`] until [`Self::reseed`] or +/// [`bouncycastle_core::traits::RNG::add_seed_keymaterial`] has run (Section 9.3.1 steps 7-8, with the "go back" loop left +/// to the application). Both are the full reseed of Section 10.1.1.3, so the seed they take must +/// carry at least the instance's security strength, as at instantiation; there is no automatic +/// reseed from the OS. pub trait Sp80090ADrbg { /// The input KeyMaterial must be of type [`KeyType::Seed`]. /// @@ -99,8 +103,13 @@ pub trait Sp80090ADrbg { /// 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 - /// benefits from the secure erasure and logging protections in the KeyMaterial object. + /// The nonce is Section 8.6.7's: "either: a. A value with at least (security_strength/2) bits + /// of entropy, or b. A value that is expected to repeat no more often than a + /// (security_strength/2)-bit random string would be expected to repeat", unique to the module + /// and "considered to be a critical security parameter" though it "need not be secret". It is + /// not checked here, which is why it is a KeyMaterial: so that it gets the erasure and logging + /// protections of one. An empty nonce is accepted and the seed then has to carry the whole + /// requirement on its own (`new_from_os` passes one, with a seed of the full strength). fn instantiate( &mut self, prediction_resistance: bool, @@ -126,26 +135,26 @@ pub trait Sp80090ADrbg { /// security strength provided in these returned bits is the minimum of the security strength /// supported by the DRBG and the length of the bit string returned" /// - /// As required by SP 800-90A section 8.4, `len` cannot exceed the initialized [`SecurityStrength`] - /// of this instance, although multiple calls to this function can be made, in which case it is the - /// application's responsibility to track that it is not expecting more entropy than the [`SecurityStrength`] - /// to which this instance was instantiated. For example, extracting two 128-bit values from an instance - /// instantiated to [`SecurityStrength::_128bit`] and then combining tem to form an AES-256 key would likely - /// not pass FIPS certification. + /// Section 8.4 does not limit `len`: "Security_strength_of_output = min(output_length, + /// DRBG_security_strength)", so a longer output is allowed and carries no more strength than + /// the instance has, and neither does a concatenation of outputs. Extracting two 128-bit + /// values from an instance instantiated at [`SecurityStrength::_128bit`] and combining them + /// into an AES-256 key gives a 128-bit key. What is limited is one request: `len` is bytes, + /// and more than `max_number_of_bits_per_request` (2^19 bits, Table 2) is refused as + /// [`RNGError::GenericError`]; so is an `additional_input` over 2^35 bits. /// - /// Throws a [`RNGError::InsufficientSeedEntropy`] if `len` exceeds [`SecurityStrength`]. + /// # Errors + /// [`RNGError::Uninitialized`] before [`Self::instantiate`]; [`RNGError::ReseedRequired`] after + /// `reseed_interval` requests; [`RNGError::GenericError`] for the length limits. fn generate(&mut self, additional_input: &[u8], len: usize) -> Result, RNGError>; - /// As per [`Sp80090ADrbg::generate`], but writes to the provided output slice. - /// The output slice is filled. - /// Throws a [`RNGError::InsufficientSeedEntropy`] if the length of the output slice exceeds [`SecurityStrength`]. - /// Retruns the number of bits output. + /// As per [`Sp80090ADrbg::generate`], but writes to the provided output slice, which is + /// filled (and zeroized first). Returns the number of bytes written, the slice's length. fn generate_out(&mut self, additional_input: &[u8], out: &mut [u8]) -> Result; - /// As per [`Sp80090ADrbg::generate`], but writes to the provided KeyMaterial. - /// The output [`KeyMaterialTrait`] is filled to capacity. - /// Throws a [`RNGError::InsufficientSeedEntropy`] if the capacity of the output KeyMaterial exceeds [`SecurityStrength`]. - /// Retruns the number of bits output. + /// As per [`Sp80090ADrbg::generate`], but writes to the provided KeyMaterial, which is filled + /// to capacity and tagged [`KeyType::CryptographicRandom`] at Section 8.4's + /// `min(output_length, DRBG_security_strength)`. Returns the number of bytes written. fn generate_keymaterial_out( &mut self, additional_input: &[u8], From cfa27824abea6d2dc2784f30adca35b2c7ce7fd6 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 07:54:35 +1100 Subject: [PATCH 23/57] cipher: the 2026-11-03 cipher review's items still open after PR #172 (REVIEW-2026-11-03-cipher-correctness-mythos.md, at 94d4e966): GCM refuses a resumed counter below 2, CCM bounds a resumed counter by the payload the mode can carry, an empty data call leaves GCM's AAD phase open as CCM's, the CMAC doc no longer claims an all-zero key is accepted; Ci1, Ci3, Ci4, Ci5, Ci7, Ci9 had landed with #172 and Ci10 went with the worker constructors Ci2: `Gcm::read_state` refuses a CTR counter below 2. GCTR starts at inc32(J0), counter 2 (SP 800-38D Algorithm 4 step 3); J0 itself, counter 1, is the tag mask (step 6) and 0 is never used, so a state claiming either would emit CIPH_K(J0) as keystream. The CTR component's own check was only the top of the range. Ci6: a CCM keystream resumes only with `next_ctr` in `1..=MAX_NEXT_CTR`, one past the last counter value the largest payload (`2^8q - 1` bytes) can use. The bound was `MAX_COUNTER`, the counter field's width, which for `q = 8` is `u64::MAX`: a resumed index there passed and the next increment overflowed, in release wrapping `next_ctr` to 0 (the tag mask's counter) and `remaining_blocks` with it. Ci11: `absorb_data` and the decryptor's `do_decrypt_out` treat an empty call as a no-op that leaves the AAD phase open, which is what CCM documents and does and what the AEAD suite in the test framework now holds the family to; a non-empty call still ends the phase even when every byte is held back as a possible tag. Ci8: the key route refuses a Zeroized key by type and resume goes through the key object, so the module doc's "(or an all-zero key, as HMAC does)" was stale. Tests: `gcm_refuses_a_resumed_counter_below_two` (0 and 1 refused, 2 resumes), `the_counter_index_is_held_to_its_range` moves to the new bound and adds `u64::MAX`. Assisted-by: Claude:claude-fable-5-1 --- crypto/cipher/src/modes/ccm.rs | 12 +++++++-- crypto/cipher/src/modes/cmac.rs | 3 ++- crypto/cipher/src/modes/gcm.rs | 20 +++++++++++++-- crypto/cipher/tests/ccm_suspend_tests.rs | 13 +++++++--- crypto/cipher/tests/suspend_tests.rs | 31 ++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 9 deletions(-) diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs index 2ad71174c..3f900bc38 100644 --- a/crypto/cipher/src/modes/ccm.rs +++ b/crypto/cipher/src/modes/ccm.rs @@ -683,6 +683,13 @@ where const MAX_COUNTER: u64 = if Self::Q_LEN >= 8 { u64::MAX } else { (1u64 << (8 * Self::Q_LEN)) - 1 }; + /// The largest `next_ctr` a state of this type can hold: the payload is at most `2^8q - 1` + /// bytes (`MAX_COUNTER` is that same value), so at most `ceil(that / BLOCK_LEN)` counter + /// values from `S1` are ever used and the index stops one past the last. Tighter than + /// `MAX_COUNTER` for `q = 8`, where that is `u64::MAX` and a resumed index at it would + /// overflow the next increment and `remaining_blocks`. + const MAX_NEXT_CTR: u64 = Self::MAX_COUNTER / (BLOCK_LEN as u64) + 2; + /// Formats the counter template and positions the keystream at `S1`. fn from_perm(perm: &'k P, nonce: &[u8; NONCE_LEN]) -> Self { // A.3, Tables 3 and 4: `Ctr_i` is `Flags || N || [i]_8q`, and its flags octet has both @@ -1529,11 +1536,12 @@ where let mut r = Cursor::new(state); let ctr_template = r.array::(); // A.3: the flags octet is `[q-1]_3` and nothing else, and the counter field is zero in - // the template; `next_ctr` starts at 1 (`S0` is the tag mask) and stops at `MAX_COUNTER`. + // the template; `next_ctr` starts at 1 (`S0` is the tag mask) and stops at + // `MAX_NEXT_CTR`, one past the last counter value the largest payload uses. let next_ctr = r.u64(); let flags_ok = ctr_template[0] == (Self::Q_LEN - 1) as u8; let counter_field_zero = ctr_template[BLOCK_LEN - Self::Q_LEN..].iter().all(|&b| b == 0); - let ctr_ok = next_ctr >= 1 && next_ctr - 1 <= Self::MAX_COUNTER; + let ctr_ok = next_ctr >= 1 && next_ctr <= Self::MAX_NEXT_CTR; if !(flags_ok && counter_field_zero && ctr_ok) { return Err(SuspendableError::InvalidData); } diff --git a/crypto/cipher/src/modes/cmac.rs b/crypto/cipher/src/modes/cmac.rs index dcdc91278..bd8daac7a 100644 --- a/crypto/cipher/src/modes/cmac.rs +++ b/crypto/cipher/src/modes/cmac.rs @@ -64,7 +64,8 @@ //! 2^21 with a 64-bit one. //! * **Key separation** (Sec 5.2): the key "shall be used exclusively for the CMAC mode of the //! chosen block cipher", so the key's `from_key_material` accepts only a [`KeyType::MACKey`] -//! key (or an all-zero key, as HMAC does), never a [`KeyType::SymmetricCipherKey`]. +//! key, never a [`KeyType::SymmetricCipherKey`] and not an all-zero (`Zeroized`) one; resume +//! goes through the same key object. //! * **Replay** (Appendix C): a valid tag says nothing about freshness; that is the protocol's job. //! //! [`KeyType::MACKey`]: bouncycastle_core::key_material::KeyType::MACKey diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs index 9a2f6823c..7b9dfec62 100644 --- a/crypto/cipher/src/modes/gcm.rs +++ b/crypto/cipher/src/modes/gcm.rs @@ -498,6 +498,11 @@ where /// plaintext into ciphertext in place) and the decryptor (which calls this *before* GCTR turns /// the ciphertext back into plaintext): either way GHASH must see ciphertext, never plaintext. fn absorb_data(&mut self, data: &[u8]) -> Result<(), SymmetricCipherError> { + // An empty call is a no-op that leaves the AAD phase open, the rule CCM and the AEAD + // suite in the test framework hold the family to; data begins with its first byte. + if data.is_empty() { + return Ok(()); + } self.data_len = self.data_len_after(data.len())?; self.begin_data_if_needed(); self.ghash.update(data); @@ -714,8 +719,11 @@ where // Data has started even if every byte is still held back as a possible tag, so the AAD // phase ends here rather than at the first byte released: otherwise a `do_update_aad` // after a first call shorter than `TAG_LEN` would be accepted, and absorbed as if it came - // before the ciphertext (Algorithm 5 absorbs `A` before `C`). - self.begin_data_if_needed(); + // before the ciphertext (Algorithm 5 absorbs `A` before `C`). An empty call, though, is + // a no-op that leaves the phase open (see `absorb_data`). + if !ciphertext.is_empty() { + self.begin_data_if_needed(); + } // Bytes of the old tail that are now known to be ciphertext, then bytes of the new input // that are also released this call. @@ -837,6 +845,14 @@ where gcm.ctr = as SuspendableComponent<'k>>::read_state( ctr, &key.perm, )?; + // The CTR state's own check is only the top of the counter's range. GCTR starts at + // inc32(J0), counter 2 (Algorithm 4 step 3); J0 itself, counter 1, is the tag mask + // (step 6) and 0 is never used, so a resumed counter below 2 would emit CIPH_K(J0) as + // keystream. The counter follows the nonce in the CTR state. + let mut counter = Cursor::new(&ctr[GCM_NONCE_LEN..]); + if counter.u64() < 2 { + return Err(SuspendableError::InvalidData); + } gcm.ghash.restore_state(ghash)?; let mut r = Cursor::new(rest); diff --git a/crypto/cipher/tests/ccm_suspend_tests.rs b/crypto/cipher/tests/ccm_suspend_tests.rs index 29367ed00..4c838b70d 100644 --- a/crypto/cipher/tests/ccm_suspend_tests.rs +++ b/crypto/cipher/tests/ccm_suspend_tests.rs @@ -126,15 +126,20 @@ fn owed_is_held_to_the_frame() { } /// The counter index runs from 1 (`S0` is the tag mask, step 7 starts the payload keystream at -/// `S1`) to one past the last counter value, which is where it stands once every block has been -/// used; 0 and anything beyond are not states the mode can have been in. +/// `S1`) to one past the last counter value the largest payload (`2^8q - 1` bytes, so +/// `ceil(LIMIT / 16)` blocks) can use, which is where it stands once every block has been used; +/// 0 and anything beyond are not states the mode can have been in. The bound is the payload's, +/// not the counter field's: for `q = 8` the field runs to `u64::MAX`, where one more increment +/// would overflow. #[test] fn the_counter_index_is_held_to_its_range() { let state = mid_payload(); + let last = LIMIT / BLOCK_LEN as u64 + 2; assert!(resumes(with_u64(state, NEXT_CTR, 1)).is_ok()); - assert!(resumes(with_u64(state, NEXT_CTR, LIMIT + 1)).is_ok(), "every counter used"); + assert!(resumes(with_u64(state, NEXT_CTR, last)).is_ok(), "every counter used"); assert_eq!(resumes(with_u64(state, NEXT_CTR, 0)), Err(SuspendableError::InvalidData)); - assert_eq!(resumes(with_u64(state, NEXT_CTR, LIMIT + 2)), Err(SuspendableError::InvalidData)); + assert_eq!(resumes(with_u64(state, NEXT_CTR, last + 1)), Err(SuspendableError::InvalidData)); + assert_eq!(resumes(with_u64(state, NEXT_CTR, u64::MAX)), Err(SuspendableError::InvalidData)); } /// A.3 Table 4 fixes the template's flags octet at `[q-1]_3` with every other bit zero, and the diff --git a/crypto/cipher/tests/suspend_tests.rs b/crypto/cipher/tests/suspend_tests.rs index 71256137b..690557d18 100644 --- a/crypto/cipher/tests/suspend_tests.rs +++ b/crypto/cipher/tests/suspend_tests.rs @@ -15,6 +15,7 @@ use bouncycastle_cipher::modes::{ }; use bouncycastle_cipher::padding::{PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SuspendableError; use bouncycastle_core::hazmat::{EcbDecryptionKey, EcbEncryptionKey, Mode}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::symmetric_key::{ @@ -27,6 +28,7 @@ use bouncycastle_core::traits::{ SymmetricCipherEncryptor, }; use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableKeyedState; +use bouncycastle_utils::suspendable_state::LIB_VERSION_LEN; #[path = "common/toy_block_cipher.rs"] mod toy_block_cipher; @@ -226,6 +228,35 @@ fn ctr_mid_block_both_directions() { assert_eq!(plain, msg[7..]); } +/// GCTR starts at counter 2 (SP 800-38D Algorithm 4 step 3, `inc32(J0)`); J0 itself, counter 1, +/// is the tag mask and 0 is never used, so a state claiming either would emit `CIPH_K(J0)` as +/// keystream. The counter is the little-endian u64 after the nonce in the CTR component. +#[test] +fn gcm_refuses_a_resumed_counter_below_two() { + let key = key_for::>(); + let (mut enc, _) = key.create_encryptor(&mut os_rng()).unwrap(); + enc.do_update_aad(b"aad").unwrap(); + let mut out = [0u8; 20]; + enc.do_encrypt_out(&message(20), &mut out).unwrap(); + let good: [u8; ToyGcm::::SUSPENDED_STATE_LEN] = enc.suspend(); + let counter_at = LIB_VERSION_LEN + 12; + assert_eq!(&good[counter_at..counter_at + 8], &4u64.to_le_bytes(), "20 bytes: 2 blocks used"); + for bad_counter in [0u64, 1] { + let mut bad = good; + bad[counter_at..counter_at + 8].copy_from_slice(&bad_counter.to_le_bytes()); + assert!( + matches!( + ToyGcm::::from_suspended(bad, key.state()), + Err(SuspendableError::InvalidData) + ), + "counter {bad_counter} must be refused" + ); + } + let mut edge = good; + edge[counter_at..counter_at + 8].copy_from_slice(&2u64.to_le_bytes()); + assert!(ToyGcm::::from_suspended(edge, key.state()).is_ok(), "2 is the start"); +} + #[test] fn gcm_both_directions_with_aad() { let msg = message(45); From d0c2481c2d4324c457a5d41d38480552b67c6569 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 07:54:35 +1100 Subject: [PATCH 24/57] core-test-framework: the AEAD suite checks that an empty data call is a no-op that leaves the AAD phase open, in both directions (the cipher review's Ci11: GCM closed it, CCM did not, and the trait left it unsaid) Assisted-by: Claude:claude-fable-5-1 --- crypto/core-test-framework/src/aead.rs | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crypto/core-test-framework/src/aead.rs b/crypto/core-test-framework/src/aead.rs index c5d1e5e8d..3370d8935 100644 --- a/crypto/core-test-framework/src/aead.rs +++ b/crypto/core-test-framework/src/aead.rs @@ -376,6 +376,37 @@ impl TestFrameworkAEADCipher { assert_eq!(pt, msg, "chunk {chunk}: streaming round trip"); } + // an empty data call is a no-op that leaves the AAD phase open, in both directions: AAD + // given after it is absorbed as if the call had not happened. The family's rule, so that + // a generic caller may forward an empty buffer without closing the phase. + { + let (first, second) = aad.split_at(aad.len() / 2); + let (mut enc, nonce) = key.create_encryptor(&mut fixed_rng()).unwrap(); + enc.do_update_aad(first).unwrap(); + assert_eq!(enc.do_encrypt_out(&[], &mut []).unwrap(), 0); + enc.do_update_aad(second).unwrap(); + let mut ct = vec![0u8; enc.do_encrypt_out_len(msg.len())]; + let n = enc.do_encrypt_out(msg, &mut ct).unwrap(); + ct.truncate(n); + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_encrypt_final_detached_out(&mut final_buf).unwrap(); + ct.extend_from_slice(&final_buf[..final_len]); + assert_eq!(ct, ct_ref, "an empty data call must leave the AAD phase open (encrypt)"); + assert_eq!(tag, tag_ref, "an empty data call must leave the AAD phase open (encrypt)"); + + let mut dec = key.create_decryptor(&nonce); + dec.do_update_aad(first).unwrap(); + assert_eq!(dec.do_decrypt_out(&[], &mut []).unwrap(), 0); + dec.do_update_aad(second).unwrap(); + let mut pt = vec![0u8; dec.do_decrypt_out_len(ct.len())]; + let n = dec.do_decrypt_out(&ct, &mut pt).unwrap(); + pt.truncate(n); + let mut final_buf = [0u8; FINAL_LEN]; + let final_len = dec.do_decrypt_final_detached_out(&tag, &mut final_buf).unwrap(); + pt.extend_from_slice(&final_buf[..final_len]); + assert_eq!(pt, msg, "an empty data call must leave the AAD phase open (decrypt)"); + } + // a wrong detached tag is refused at the final let mut wrong_tag = tag_ref; wrong_tag[0] ^= 0xFF; From 83bcbd3d19c7d12d49437d64d6621f233fc5e9cf Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 07:54:35 +1100 Subject: [PATCH 25/57] ascon: an empty data call leaves the AAD phase open, the family rule the test framework now checks; a non-empty decrypt call still closes it even while its bytes are held back as the tag Assisted-by: Claude:claude-fable-5-1 --- crypto/ascon/src/ascon_aead128.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs index f40850e5a..538bc44ea 100644 --- a/crypto/ascon/src/ascon_aead128.rs +++ b/crypto/ascon/src/ascon_aead128.rs @@ -359,6 +359,11 @@ impl Ascon_AEAD128 { /// Encrypt `data` in place (SP 800-232 §4.1.1 step 3). Every byte is transformed and emitted /// immediately; nothing is buffered across calls. fn encrypt_bytes(&mut self, data: &mut [u8]) { + // An empty call is a no-op that leaves the AAD phase open: the family's rule, held by + // the AEAD suite in the test framework; data begins with its first byte. + if data.is_empty() { + return; + } self.check_data(); let mut data = data; @@ -394,6 +399,11 @@ impl Ascon_AEAD128 { /// Decrypt `data` in place (SP 800-232 §4.1.2 step 3). Every byte is transformed and emitted /// immediately; the plaintext is **not** authenticated until `verify_tag` returns `Ok`. fn decrypt_bytes(&mut self, data: &mut [u8]) { + // An empty call is a no-op that leaves the AAD phase open: the family's rule, held by + // the AEAD suite in the test framework; data begins with its first byte. + if data.is_empty() { + return; + } self.check_data(); let mut data = data; @@ -510,8 +520,12 @@ impl SymmetricCipherDecryptor let out = &mut plaintext[..release]; out[..from_held].copy_from_slice(&self.held[..from_held]); out[from_held..].copy_from_slice(&ciphertext[..from_input]); - // Called even when `release` is 0: that is what ends the AAD phase, so a later non-empty - // `do_update_aad` is refused however little ciphertext has been seen. + // Data has started once any ciphertext has been seen, even while every byte of it is + // still held back as a possible tag, so the AAD phase ends here and a later non-empty + // `do_update_aad` is refused. An empty call, though, is a no-op that leaves it open. + if !ciphertext.is_empty() { + self.check_data(); + } self.decrypt_bytes(out); // Keep the newest `ASCON_AEAD128_TAG_LEN` (or fewer) bytes: what is left of `held`, then // the tail of `ciphertext`. From 474749ed2dcd630b73c0f094917875f82cd4aeea Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 07:58:29 +1100 Subject: [PATCH 26/57] rng: tests for the three gaps the scoped mutants run exposed: a third generate pinned against an independent Hash_DRBG reference (the CAVP files stop at the second, whose output does not yet depend on the reseed counter's increment), the 2^19-bit per-request limit at its boundary, and that add_seed_keymaterial changes the stream Assisted-by: Claude:claude-fable-5-1 --- crypto/rng/tests/hash_drbg80090a_tests.rs | 78 +++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/crypto/rng/tests/hash_drbg80090a_tests.rs b/crypto/rng/tests/hash_drbg80090a_tests.rs index f408fbb8d..0799babdd 100644 --- a/crypto/rng/tests/hash_drbg80090a_tests.rs +++ b/crypto/rng/tests/hash_drbg80090a_tests.rs @@ -332,4 +332,82 @@ mod tests { assert_ne!(key.ref_to_bytes(), [0u8; 32]); assert_eq!(key.security_strength(), rng.security_strength()); } + + fn hex(s: &str) -> Vec { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() + } + + /// The reseed counter is added into V at the end of every generate (Section 10.1.1.4 step 5) + /// and incremented (step 6); the CAVP files stop at the second generate, whose output does not + /// yet depend on the increment, so a third generate is pinned here. Expected values are from a + /// Hash_DRBG written independently in Python from Sections 10.1.1.2-10.1.1.4 and 10.3.1 and + /// checked against the CAVP no-reseed vectors first; the inputs are those vectors' COUNT 0. + #[test] + fn a_third_generate_matches_an_independent_reference() { + fn third( + entropy: &str, + nonce: &str, + strength: SecurityStrength, + len: usize, + ) -> Vec { + let mut drbg = D::new_uninitialized(); + let seed = KeyMaterial::<64>::from_bytes_as_type(&hex(entropy), KeyType::Seed).unwrap(); + let nonce = KeyMaterial::<64>::from_bytes_as_type(&hex(nonce), KeyType::Seed).unwrap(); + drbg.instantiate(false, seed, &nonce, &[], strength).unwrap(); + drbg.generate(&[], len).unwrap(); + drbg.generate(&[], len).unwrap(); + drbg.generate(&[], len).unwrap() + } + assert_eq!( + third::( + "a65ad0f345db4e0effe875c3a2e71f42c7129d620ff5c119a9ef55f05185e0fb", + "8581f9317517276e06e9607ddbcbcc2e", + SecurityStrength::_128bit, + 128 + ), + hex( + "c585885f220a9e616817c24192a6314f2170c6e066b3ebb7be43717a427d3bc36f83390195fe191a990a6926709ec3b59b6a88074dbab83a443ca69061c3e4a645597cd6733e068631b63c731250672134b50898579b17b6a4e9e0f121ec809c46135738e8009fde4f2464a0cd2893c3a1a1c1d96b1b36040271d6e71bb3d430" + ) + ); + assert_eq!( + third::( + "6b50a7d8f8a55d7a3df8bb40bcc3b722d8708de67fda010b03c4c84d72096f8c", + "3ec649cc6256d9fa31db7a2904aaf025", + SecurityStrength::_256bit, + 256 + ), + hex( + "356ad18ba9f67f3a5b5f56d9a20731a05f02f13aa1659eb3906d557e825078208c094ce00e403ec150cfdf98cea7825a54274c23e8194f3f400afe492731987b7e37cbe10eccddb32900b93e859a029ac9032e057428de67af2ad84a877776429ef6d325670be177b0ef1c0a15ad2907bd04cf6b910607abbb179e332b74645dc8d117c4d87da5242c2903216d3ada8ec01402144b61f90ed94b838e298c7a314fe9ebacae1a968dda321de4a9ae1681efa0e714d9f94845e2be898738c1d8b6e288f6599dddf3f4bd049b3512a5046e53b0b5e41276fb94ce63e24371cadc43353bf3dedd9e3b72697f6c10210b261945e052f3f0b1a9c533d4082d5414e61c" + ) + ); + } + + /// Table 2: `max_number_of_bits_per_request` is 2^19 bits, 65536 bytes; one more is refused. + #[test] + fn the_per_request_limit_is_enforced() { + let mut rng = HashDRBG_SHA256::new_from_os(); + assert_eq!(rng.generate(&[], 65536).unwrap().len(), 65536); + assert!(matches!(rng.generate(&[], 65537), Err(RNGError::GenericError(_)))); + let mut out = vec![0u8; 65537]; + assert!(matches!(rng.generate_out(&[], &mut out), Err(RNGError::GenericError(_)))); + } + + /// `add_seed_keymaterial` is the full reseed of Section 10.1.1.3: two instances from one seed + /// agree until one is reseeded, and disagree from then on. + #[test] + fn reseeding_changes_the_stream() { + fn instance() -> HashDRBG_SHA256 { + let mut rng = HashDRBG_SHA256::new_uninitialized(); + let seed = + KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::Seed).unwrap(); + rng.instantiate(false, seed, &KeyMaterial0::new(), &[], SecurityStrength::_128bit) + .unwrap(); + rng + } + let (mut a, mut b) = (instance(), instance()); + assert_eq!(a.next_bytes(32).unwrap(), b.next_bytes(32).unwrap()); + let fresh = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[32..64], KeyType::Seed).unwrap(); + b.add_seed_keymaterial(&fresh).unwrap(); + assert_ne!(a.next_bytes(32).unwrap(), b.next_bytes(32).unwrap()); + } } From 11405b419b9b2094acd68c662721ca50588ec817 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 07:58:29 +1100 Subject: [PATCH 27/57] sha3: the unused-bits test for a partial byte on the customized (cSHAKE) path too, which the scoped mutants run showed the SHAKE one does not reach Assisted-by: Claude:claude-fable-5-1 --- crypto/sha3/tests/cshake_tests.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crypto/sha3/tests/cshake_tests.rs b/crypto/sha3/tests/cshake_tests.rs index d420d4fe2..6ba652961 100644 --- a/crypto/sha3/tests/cshake_tests.rs +++ b/crypto/sha3/tests/cshake_tests.rs @@ -125,3 +125,19 @@ fn algorithm_names() { assert_eq!(CSHAKE128::ALG_NAME, "CSHAKE128"); assert_eq!(CSHAKE256::ALG_NAME, "CSHAKE256"); } + +/// As for SHAKE: the low `8 - num_bits` bits of the partial byte are the BIT STRING's unused bits +/// and must not reach the sponge, on the customized path too (it has its own suffix handling). +#[test] +fn the_unused_bits_of_a_partial_byte_are_ignored() { + for num_bits in 1..=7usize { + let mut all_ones = CSHAKE128::new(b"N", b"S"); + all_ones.do_update(b"abc"); + let mut all_zeros = CSHAKE128::new(b"N", b"S"); + all_zeros.do_update(b"abc"); + let used_mask = 0xffu8 << (8 - num_bits); + let a = all_ones.into_squeezer_partial_bits(0xff, num_bits).unwrap().do_output(32); + let b = all_zeros.into_squeezer_partial_bits(used_mask, num_bits).unwrap().do_output(32); + assert_eq!(a, b, "num_bits = {num_bits}"); + } +} From 3b2f570f4c5e26dabc7da62fc47512872b0a0d86 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 09:14:26 +1100 Subject: [PATCH 28/57] mlkem-common: barrett_reduce is valid for every i16 (the ML-KEM review's M2: for inputs at or below -9q the `t * q` product does not fit an i16 and the narrowed subtraction overflowed in debug builds; the subtraction is now done in i32 and the unit test covers the whole range), and Polynomial::from_msg takes the message by reference so a caller can keep it in a Secret Assisted-by: Claude:claude-fable-5-1 --- crypto/mlkem-common/src/polynomial.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/crypto/mlkem-common/src/polynomial.rs b/crypto/mlkem-common/src/polynomial.rs index e085203d3..b74e5d950 100644 --- a/crypto/mlkem-common/src/polynomial.rs +++ b/crypto/mlkem-common/src/polynomial.rs @@ -55,7 +55,7 @@ impl Polynomial { /// decryption noise margin. The mapping is computed branchlessly (constant-time) /// via a bit-derived all-ones / all-zeros mask, and bits are read LSB-first. This /// is the exact inverse of [`to_msg`]. - pub fn from_msg(m: [u8; 32]) -> Self { + pub fn from_msg(m: &[u8; 32]) -> Self { let mut w = Polynomial::new(); for (i, b) in m.iter().enumerate() { @@ -381,11 +381,13 @@ pub fn montgomery_reduce(a: i32) -> i16 { /// Barrett reduction into \[0, 𝑞]: the floor variant, not pq-crystals' centered one, which the /// compression code relies on for its unsigned casts. The result is 𝑞 rather than 0 for a -/// negative multiple of 𝑞; [`cond_sub_q`] folds that when a canonical value is needed. +/// negative multiple of 𝑞; [`cond_sub_q`] folds that when a canonical value is needed. Valid for +/// every `i16`: the subtraction is done in `i32`, since for `a <= -9q` the product `t * q` does +/// not fit an `i16` and a narrowed subtraction overflowed in debug builds. pub fn barrett_reduce(a: i16) -> i16 { - let v = (((1u32 << 26) + ((q / 2) as u32)) / (q as u32)) as i16; - let t = (((v as i32) * (a as i32)) >> 26) as i16; - a - (((t as i32) * q as i32) as i16) + let v = (((1u32 << 26) + ((q / 2) as u32)) / (q as u32)) as i32; + let t = (v * (a as i32)) >> 26; + (a as i32 - t * q as i32) as i16 } /// Folds 𝑞 to 0: for `a` in \[0, 𝑞], the canonical representative in \[0, 𝑞). @@ -416,11 +418,12 @@ mod tests { #[test] fn barrett_reduce_lands_in_zero_to_q_inclusive() { - // The floor variant: for the sums the NTT arithmetic produces, |a| < 8q, the result is - // congruent to a and in [0, q], reaching q only at negative multiples of q, which the - // compression masks fold and the encoders never see (their inputs are reduced sums of - // Montgomery products, each in (-q, q)). - for a in -(8 * q)..=(8 * q) { + // The floor variant, over every i16 (the NTT arithmetic produces |a| < 8q; the whole + // range is checked so that no input can overflow): the result is congruent to a and in + // [0, q], reaching q only at negative multiples of q, which the compression masks fold + // and the encoders never see (their inputs are reduced sums of Montgomery products, + // each in (-q, q)). + for a in i16::MIN..=i16::MAX { let r = barrett_reduce(a); assert!((0..=q).contains(&r), "barrett_reduce({a}) = {r} not in [0, q]"); assert_eq!((r as i32 - a as i32).rem_euclid(q as i32), 0, "barrett_reduce({a}) = {r}"); From 9f97213bb87a40c695fd80aa373456322bef1382 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 09:14:26 +1100 Subject: [PATCH 29/57] mlkem: the 2026-11-03 ML-KEM review's hardening items M1 and M3 and the note M5 (REVIEW-2026-11-03-mlkem-correctness-mythos.md, at 94d4e966): the k-term dot product is reduced before the inverse NTT, the decapsulation chain, the key-generation seeds and the private-key comparison keep their secrets in Secret buffers, and the seed error text names a key type that exists M1: `dot_product` reduces its sum before returning it. Each product coefficient is a sum of two Montgomery reductions in (-q, q), so the k-term sum is in (-2kq, 2kq), up to 16q for ML-KEM-1024, and `inv_ntt`'s first butterfly adds two of them in i16 (limit 9.84q). FIPS 203 Sec 3.3 allows deferring a reduction only while the result is right for every input; the margin here was empirical (the review's search reached 8.9q on a real key; an overflow would have made m' wrong and the honest decapsulation return K-bar), and bc-java and the pq-crystals reference reduce at this point. The reduction had been removed on purpose; its comment said so. Every known-answer suite is unchanged by it. M3: m', K', r', K-bar and the shared secret of decapsulation, the G output they are split from, m and K of encapsulation, and d and sigma of key generation are `Secret`, scrubbed when they drop (`sigma.fill(0)` had no barrier); `pke_encrypt`, `encaps_internal` and `pke_decrypt` pass the message by reference or return it in a `Secret`, and `PartialEq` on the private key encodes into `Secret` buffers rather than plain arrays of up to 3168 bytes. The hazmat `encaps_with_randomness` keeps its by-value contract and copies out. M5: "KeyType::BytesFullEntropy" in the seed error is "KeyType::CryptographicRandom", the variant the check accepts. Not changed: M4, M6-M13 (notes). Assisted-by: Claude:claude-fable-5-1 --- .../src/hazmat/encaps_with_randomness.rs | 4 +- crypto/mlkem/src/matrix.rs | 10 +- crypto/mlkem/src/mlkem.rs | 101 ++++++++++-------- crypto/mlkem/src/mlkem_keys.rs | 10 +- 4 files changed, 74 insertions(+), 51 deletions(-) diff --git a/crypto/mlkem/src/hazmat/encaps_with_randomness.rs b/crypto/mlkem/src/hazmat/encaps_with_randomness.rs index 7826ecc2c..125760761 100644 --- a/crypto/mlkem/src/hazmat/encaps_with_randomness.rs +++ b/crypto/mlkem/src/hazmat/encaps_with_randomness.rs @@ -65,6 +65,8 @@ impl< A_hat: Option<&P::MatrixA>, m: [u8; MLKEM_RND_LEN], ) -> ([u8; MLKEM_SS_LEN], [u8; CT_LEN]) { - Self::encaps_internal(ek, A_hat, m) + // The public contract returns `K` by value; the internal path keeps it in a `Secret`. + let (K, ct) = Self::encaps_internal(ek, A_hat, &m); + (*K, ct) } } diff --git a/crypto/mlkem/src/matrix.rs b/crypto/mlkem/src/matrix.rs index d92105946..5acb9b4dd 100644 --- a/crypto/mlkem/src/matrix.rs +++ b/crypto/mlkem/src/matrix.rs @@ -189,9 +189,13 @@ impl VectorTrait for Vector { let w1 = polynomial::base_mult_montgomery(&self[i], &v[i]); w.add(&w1); } - // Note: This function DOES NOT perform modular reduction, as the current - // construction of ML-KEM only reduces modulo q when it's necessary. - // w.poly_reduce(); + // Reduced before it is returned: each product coefficient is a sum of two Montgomery + // reductions in (-q, q), so the k-term sum is in (-2kq, 2kq), up to 16q for ML-KEM-1024, + // and the callers feed it to `inv_ntt`, whose first butterfly adds two coefficients in + // i16 (limit 9.84q). FIPS 203 Sec 3.3 allows deferring a reduction only while the result + // is right for every input; without this one the margin was empirical (a search reached + // 8.9q on a real key), and bc-java and the pq-crystals reference reduce at this point. + w.poly_reduce(); w } diff --git a/crypto/mlkem/src/mlkem.rs b/crypto/mlkem/src/mlkem.rs index abdea9b47..1deb2e609 100644 --- a/crypto/mlkem/src/mlkem.rs +++ b/crypto/mlkem/src/mlkem.rs @@ -293,7 +293,7 @@ impl< || seed.key_len() != 64 { return Err(KEMError::KeyGenError( - "Seed must be 64 bytes and KeyType::Seed or KeyType::BytesFullEntropy.", + "Seed must be 64 bytes and KeyType::Seed or KeyType::CryptographicRandom.", )); } @@ -304,7 +304,9 @@ impl< } // 1: (ekPKE, dkPKE) ← K-PKE.KeyGen(𝑑) - let (pk, s_hat) = Self::pke_keygen(&seed.ref_to_bytes()[..32].try_into().unwrap()); + let mut d = Secret::<[u8; 32]>::new(); + d.copy_from_slice(&seed.ref_to_bytes()[..32]); + let (pk, s_hat) = Self::pke_keygen(&d); // 2: ek ← ekPKE ▷ KEM encaps key is just the PKE encryption key // 3: dk ← (dkPKE‖ek‖H(ek)‖𝑧) ▷ KEM decaps key includes PKE decryption key @@ -329,15 +331,19 @@ impl< // ▷ expand 32+1 bytes to two pseudorandom 32-byte seeds1 // rho: public seed // sigma: noise seed - let (rho, mut sigma) = { + // sigma is the noise seed: secret, and scrubbed when it drops, with the G output it + // came from. rho is public (it is part of ek). + let (rho, sigma): ([u8; 32], Secret<[u8; 32]>) = { let mut g = G::new(); g.do_update(d); g.do_update(&[P::k as u8]); - let mut buf = [0u8; 64]; - let bytes_written = g.do_final_out(&mut buf); + let mut buf = Secret::<[u8; 64]>::new(); + let bytes_written = g.do_final_out(&mut *buf); debug_assert_eq!(bytes_written, 64); - (buf[..32].try_into().unwrap(), buf[32..64].try_into().unwrap()) + let mut sigma = Secret::<[u8; 32]>::new(); + sigma.copy_from_slice(&buf[32..64]); + (buf[..32].try_into().unwrap(), sigma) }; // 2: 𝑁 ← 0 @@ -387,9 +393,6 @@ impl< t_hat.add_vector_ntt(&e); } - // Clear the secret data before returning memory to the OS - sigma.fill(0u8); - // 19: ekPKE ← ByteEncode12(𝐭)‖𝜌 ▷ run ByteEncode12 𝑘 times, then append 𝐀-seed // 20: dkPKE ← ByteEncode12(𝐬)̂ ▷ run ByteEncode12 𝑘 times // Note: The encoding is skipped at this stage and left expanded for future efficiency when it's used. @@ -403,7 +406,7 @@ impl< /// Input: message 𝑚 ∈ 𝔹32 . /// Input: randomness 𝑟 ∈ 𝔹32 . /// Output: ciphertext 𝑐 ∈ 𝔹32(𝑑𝑢𝑘+𝑑𝑣). - fn pke_encrypt(ek: &PK, A_hat: &P::MatrixA, m: [u8; 32], r: &[u8; 32]) -> [u8; CT_LEN] { + fn pke_encrypt(ek: &PK, A_hat: &P::MatrixA, m: &[u8; 32], r: &[u8; 32]) -> [u8; CT_LEN] { // 1: 𝑁 ← 0 // since the number of loops here is static, the N values can be hard-coded rather than using a counter @@ -489,21 +492,24 @@ impl< pub(crate) fn encaps_internal( ek: &PK, A_hat: Option<&P::MatrixA>, - m: [u8; 32], - ) -> ([u8; 32], [u8; CT_LEN]) { + m: &[u8; 32], + ) -> (Secret<[u8; MLKEM_SS_LEN]>, [u8; CT_LEN]) { // 1: (𝐾, 𝑟) ← G(𝑚‖H(ek)) - // ▷ derive shared secret key 𝐾 and randomness 𝑟 - let K: [u8; MLKEM_SS_LEN]; - let r: [u8; 32]; - (K, r) = { + // ▷ derive shared secret key 𝐾 and randomness 𝑟. Both are secret and held in `Secret`, + // as is the G output they are split from. + let (K, r) = { let mut g = G::new(); - g.do_update(&m); + g.do_update(m); g.do_update(&ek.compute_hash()); - let mut buf = [0u8; 64]; - let bytes_written = g.do_final_out(&mut buf); + let mut buf = Secret::<[u8; 64]>::new(); + let bytes_written = g.do_final_out(&mut *buf); debug_assert_eq!(bytes_written, 64); - (buf[..32].try_into().unwrap(), buf[32..64].try_into().unwrap()) + let mut K = Secret::<[u8; MLKEM_SS_LEN]>::new(); + K.copy_from_slice(&buf[..32]); + let mut r = Secret::<[u8; 32]>::new(); + r.copy_from_slice(&buf[32..64]); + (K, r) }; // 2: 𝑐 ← K-PKE.Encrypt(ek, 𝑚, 𝑟) @@ -524,7 +530,7 @@ impl< /// Input: decryption key dkPKE ∈ 𝔹384𝑘. /// Input: ciphertext 𝑐 ∈ 𝔹32(𝑑𝑢𝑘+𝑑𝑣). /// Output: message 𝑚 ∈ 𝔹32 . - fn pke_decrypt(dk: &SK, ct: &[u8; CT_LEN]) -> [u8; 32] { + fn pke_decrypt(dk: &SK, ct: &[u8; CT_LEN]) -> Secret<[u8; 32]> { // 1: 𝑐1 ← 𝑐[0 ∶ 32𝑑𝑢𝑘] // 2: 𝑐2 ← 𝑐[32𝑑𝑢𝑘 ∶ 32(𝑑𝑢𝑘 + 𝑑𝑣)] // 3: 𝐮′ ← Decompress_𝑑𝑢(ByteDecode_𝑑𝑢(𝑐1)) @@ -552,8 +558,11 @@ impl< }; // 7: 𝑚 ← ByteEncode1(Compress1(𝑤)) - // ▷ decode plaintext 𝑚 from polynomial 𝑤 - w.to_msg() + // ▷ decode plaintext 𝑚 from polynomial 𝑤, into a `Secret`: it is an internal value + // (FIPS 203 Sec 3.3) that must not outlive the decapsulation. + let mut m = Secret::<[u8; 32]>::new(); + *m = w.to_msg(); + m } /// Algorithm 18 ML-KEM.Decaps_internal(dk, 𝑐) @@ -565,7 +574,7 @@ impl< dk: &SK, A_hat: Option<&P::MatrixA>, c: &[u8; CT_LEN], - ) -> [u8; MLKEM_SS_LEN] { + ) -> Secret<[u8; MLKEM_SS_LEN]> { // Structured to mirror the FIPS as closely as possible, with unnamed scopes // used to limit the number of live stack variables at any given time. @@ -580,17 +589,21 @@ impl< // Compute the trial shared secret key // 6: (𝐾′, 𝑟′) ← G(𝑚′‖ℎ)̄ - let K_prime: [u8; MLKEM_SS_LEN]; - let r_prime: [u8; 32]; - (K_prime, r_prime) = { + // K' and r' are the candidate shared secret and the re-encryption randomness: secret, + // held in `Secret` with the G output they are split from (FIPS 203 Sec 3.3, Sec 6.3). + let (K_prime, r_prime) = { let mut g = G::new(); - g.do_update(&m_prime); + g.do_update(&*m_prime); g.do_update(&dk.pk().compute_hash()); - let mut buf = [0u8; 64]; - let bytes_written = g.do_final_out(&mut buf); + let mut buf = Secret::<[u8; 64]>::new(); + let bytes_written = g.do_final_out(&mut *buf); debug_assert_eq!(bytes_written, 64); - (buf[..32].try_into().unwrap(), buf[32..64].try_into().unwrap()) + let mut K_prime = Secret::<[u8; MLKEM_SS_LEN]>::new(); + K_prime.copy_from_slice(&buf[..32]); + let mut r_prime = Secret::<[u8; 32]>::new(); + r_prime.copy_from_slice(&buf[32..64]); + (K_prime, r_prime) }; // 7: 𝐾_bar ← J(𝑧‖𝑐) @@ -599,13 +612,13 @@ impl< // This is because if the computation is conditional on the Fujisaki-Okamoto check failing, then // it will result in a timing difference between success and failure. - let K_bar: [u8; MLKEM_SS_LEN] = { + let K_bar: Secret<[u8; MLKEM_SS_LEN]> = { let mut j = J::new(); j.do_update(dk.z().as_ref()); j.do_update(c); - let mut buf = [0u8; MLKEM_SS_LEN]; + let mut buf = Secret::<[u8; MLKEM_SS_LEN]>::new(); let mut j = j.into_squeezer(); - let bytes_written = j.do_output_out(&mut buf); + let bytes_written = j.do_output_out(&mut *buf); debug_assert_eq!(bytes_written, MLKEM_SS_LEN); buf @@ -617,14 +630,14 @@ impl< // To allow for pre-computing A_hat for multiple encapsulations, we will either take // A_hat passed in, or compute it fresh. let c_prime = match A_hat { - Some(A_hat) => Self::pke_encrypt(dk.pk(), A_hat, m_prime, &r_prime), - None => Self::pke_encrypt(dk.pk(), &dk.pk().A_hat(), m_prime, &r_prime), + Some(A_hat) => Self::pke_encrypt(dk.pk(), A_hat, &m_prime, &r_prime), + None => Self::pke_encrypt(dk.pk(), &dk.pk().A_hat(), &m_prime, &r_prime), }; // 9: if 𝑐 ≠ 𝑐′ then // 10: 𝐾′ ← 𝐾_bar // ▷ if ciphertexts do not match, “implicitly reject" - let mut K_out = [0u8; MLKEM_SS_LEN]; + let mut K_out = Secret::<[u8; MLKEM_SS_LEN]>::new(); conditional_copy_bytes(&K_prime, &K_bar, &mut K_out, ct_eq_bytes_mask(c, &c_prime)); K_out @@ -740,13 +753,13 @@ impl< if rng.security_strength() < P::MAX_SECURITY_STRENGTH { Err(RNGError::SecurityStrengthInsufficientForAlgorithm)?; } - let mut m = [0u8; 32]; - rng.next_bytes_out(&mut m)?; + let mut m = Secret::<[u8; 32]>::new(); + rng.next_bytes_out(&mut *m)?; // 5: (𝐾, 𝑐) ← ML-KEM.Encaps_internal(ek, 𝑚) - let (K, ct) = Self::encaps_internal(ek, A_hat, m); + let (K, ct) = Self::encaps_internal(ek, A_hat, &m); - Ok((Self::shared_secret_key(K)?, ct)) + Ok((Self::shared_secret_key(&K)?, ct)) } /// Algorithm 21 ML-KEM.Decaps(dk, 𝑐) with the Section 7.3 input checks, over a private key @@ -767,12 +780,12 @@ impl< // 1: 𝐾′ ← ML-KEM.Decaps_internal(dk, 𝑐) let K = Self::decaps_internal(dk, A_hat, ct); - Self::shared_secret_key(K) + Self::shared_secret_key(&K) } /// The shared secret as [`KeyMaterial`]: full-entropy bytes at the parameter set's strength. - fn shared_secret_key(K: [u8; MLKEM_SS_LEN]) -> Result, KEMError> { - let mut key = KeyMaterial::::from_bytes_as_type(&K, KeyType::CryptographicRandom)?; + fn shared_secret_key(K: &[u8; MLKEM_SS_LEN]) -> Result, KEMError> { + let mut key = KeyMaterial::::from_bytes_as_type(K, KeyType::CryptographicRandom)?; do_hazardous_operations(&mut key, |key| { key.set_security_strength(P::MAX_SECURITY_STRENGTH) })?; diff --git a/crypto/mlkem/src/mlkem_keys.rs b/crypto/mlkem/src/mlkem_keys.rs index fd413b2e6..85fd611a4 100644 --- a/crypto/mlkem/src/mlkem_keys.rs +++ b/crypto/mlkem/src/mlkem_keys.rs @@ -645,9 +645,13 @@ impl< > PartialEq for MLKEMPrivateKey { fn eq(&self, other: &Self) -> bool { - let self_encoded = self.encode(); - let other_encoded = other.encode(); - bouncycastle_utils::ct::ct_eq_bytes(self_encoded.as_ref(), other_encoded.as_ref()) + // The encodings are private-key material; they live in `Secret` buffers for the + // comparison and are scrubbed when it returns. + let mut self_encoded = Secret::<[u8; SK_LEN]>::new(); + let mut other_encoded = Secret::<[u8; SK_LEN]>::new(); + self.encode_out(&mut self_encoded); + other.encode_out(&mut other_encoded); + bouncycastle_utils::ct::ct_eq_bytes(&*self_encoded, &*other_encoded) } } From 494893abb51051b5f04092911ed21feb6fbdeace Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 09:14:26 +1100 Subject: [PATCH 30/57] mlkem-lowmemory: the ML-KEM review's M1, M3 and M5 for the low-memory twin: the accumulated u_i row is reduced before its inverse NTT, m', r', m, K and the shared secret are Secret, the private-key comparison and the seed-and-encoding check encode into Secret buffers, and the seed error text names a key type that exists Assisted-by: Claude:claude-fable-5-1 --- .../src/hazmat/encaps_with_randomness.rs | 4 +- .../mlkem-lowmemory/src/low_memory_helpers.rs | 3 + crypto/mlkem-lowmemory/src/mlkem.rs | 66 +++++++++++-------- crypto/mlkem-lowmemory/src/mlkem_keys.rs | 11 ++-- 4 files changed, 52 insertions(+), 32 deletions(-) diff --git a/crypto/mlkem-lowmemory/src/hazmat/encaps_with_randomness.rs b/crypto/mlkem-lowmemory/src/hazmat/encaps_with_randomness.rs index 95d909151..f02a42bfb 100644 --- a/crypto/mlkem-lowmemory/src/hazmat/encaps_with_randomness.rs +++ b/crypto/mlkem-lowmemory/src/hazmat/encaps_with_randomness.rs @@ -55,6 +55,8 @@ impl< ek: &PK, m: [u8; MLKEM_RND_LEN], ) -> ([u8; MLKEM_SS_LEN], [u8; CT_LEN]) { - Self::encaps_internal(ek, m) + // The public contract returns `K` by value; the internal path keeps it in a `Secret`. + let (K, ct) = Self::encaps_internal(ek, &m); + (*K, ct) } } diff --git a/crypto/mlkem-lowmemory/src/low_memory_helpers.rs b/crypto/mlkem-lowmemory/src/low_memory_helpers.rs index a0990a767..c9c900fb8 100644 --- a/crypto/mlkem-lowmemory/src/low_memory_helpers.rs +++ b/crypto/mlkem-lowmemory/src/low_memory_helpers.rs @@ -78,6 +78,9 @@ pub(crate) fn compute_A_hat_dot_y_hat( A_ji.base_mult_montgomery(&y_j); u_i.add(&A_ji); } + // The k-term sum is in (-2kq, 2kq); `inv_ntt`'s first butterfly adds two coefficients in + // i16, so it is reduced first (see `dot_product` in bouncycastle-mlkem for the bound). + u_i.poly_reduce(); u_i.inv_ntt(); u_i diff --git a/crypto/mlkem-lowmemory/src/mlkem.rs b/crypto/mlkem-lowmemory/src/mlkem.rs index c14e1c135..44b756568 100644 --- a/crypto/mlkem-lowmemory/src/mlkem.rs +++ b/crypto/mlkem-lowmemory/src/mlkem.rs @@ -221,7 +221,7 @@ impl< fn pke_encrypt( t_hat_packed: &P::TPacked, rho: &[u8; 32], - m: [u8; 32], + m: &[u8; 32], r: &[u8; 32], ) -> [u8; CT_LEN] { let mut ct = [0u8; CT_LEN]; @@ -297,20 +297,26 @@ impl< /// Reachable from outside the crate only through /// [`EncapsWithRandomness`](crate::hazmat::EncapsWithRandomness), which carries the security /// notes on supplying `m`. - pub(crate) fn encaps_internal(ek: &PK, m: [u8; 32]) -> ([u8; 32], [u8; CT_LEN]) { + pub(crate) fn encaps_internal( + ek: &PK, + m: &[u8; 32], + ) -> (Secret<[u8; MLKEM_SS_LEN]>, [u8; CT_LEN]) { // 1: (𝐾, 𝑟) ← G(𝑚‖H(ek)) - // ▷ derive shared secret key 𝐾 and randomness 𝑟 - let K: [u8; MLKEM_SS_LEN]; - let r: [u8; 32]; - (K, r) = { + // ▷ derive shared secret key 𝐾 and randomness 𝑟. Both are secret and held in `Secret`, + // as is the G output they are split from. + let (K, r) = { let mut g = G::new(); - g.do_update(&m); + g.do_update(m); g.do_update(&ek.compute_hash()); - let mut buf = [0u8; 64]; - let bytes_written = g.do_final_out(&mut buf); + let mut buf = Secret::<[u8; 64]>::new(); + let bytes_written = g.do_final_out(&mut *buf); debug_assert_eq!(bytes_written, 64); - (buf[..32].try_into().unwrap(), buf[32..64].try_into().unwrap()) + let mut K = Secret::<[u8; MLKEM_SS_LEN]>::new(); + K.copy_from_slice(&buf[..32]); + let mut r = Secret::<[u8; 32]>::new(); + r.copy_from_slice(&buf[32..64]); + (K, r) }; // 2: 𝑐 ← K-PKE.Encrypt(ek, 𝑚, 𝑟) @@ -329,7 +335,7 @@ impl< fn pke_decrypt>( dk: &DK, ct: &[u8; CT_LEN], - ) -> [u8; 32] { + ) -> Secret<[u8; 32]> { // 1: 𝑐1 ← 𝑐[0 ∶ 32𝑑𝑢𝑘] // 3: 𝐮′ ← Decompress_𝑑𝑢(ByteDecode_𝑑𝑢(𝑐1)) @@ -380,8 +386,11 @@ impl< }; // 7: 𝑚 ← ByteEncode1(Compress1(𝑤)) - // ▷ decode plaintext 𝑚 from polynomial 𝑤 - w.to_msg() + // ▷ decode plaintext 𝑚 from polynomial 𝑤, into a `Secret`: it is an internal value + // (FIPS 203 Sec 3.3) that must not outlive the decapsulation. + let mut m = Secret::<[u8; 32]>::new(); + *m = w.to_msg(); + m } /// Algorithm 18 ML-KEM.Decaps_internal(dk, 𝑐) @@ -395,7 +404,7 @@ impl< >( dk: &DK, c: &[u8; CT_LEN], - ) -> [u8; MLKEM_SS_LEN] { + ) -> Secret<[u8; MLKEM_SS_LEN]> { // I have tried to keep this as clean as possible for correspondence with the FIPS, // but I have moved things around so that I can use unnamed scopes to limit how many // stack variables are alive at the same time. @@ -411,19 +420,19 @@ impl< // Compute the trial shared secret key // 6: (𝐾′, 𝑟′) ← G(𝑚′‖ℎ)̄ - let K_prime: Secret<[u8; MLKEM_SS_LEN]>; - let r_prime: [u8; 32]; - (K_prime, r_prime) = { + let (K_prime, r_prime) = { let mut buf: Secret<[u8; 64]> = Secret::new(); let mut g = G::new(); - g.do_update(&m_prime); + g.do_update(&*m_prime); g.do_update(dk.pk_hash()); let bytes_written = g.do_final_out(&mut *buf); debug_assert_eq!(bytes_written, 64); let mut K_prime: Secret<[u8; MLKEM_SS_LEN]> = Secret::new(); K_prime.copy_from_slice(&buf[..32]); - (K_prime, buf[32..64].try_into().unwrap()) + let mut r_prime: Secret<[u8; 32]> = Secret::new(); + r_prime.copy_from_slice(&buf[32..64]); + (K_prime, r_prime) }; // 7: 𝐾_bar ← J(𝑧‖𝑐) @@ -449,12 +458,12 @@ impl< // A seed key holds no 𝐭̂, so it derives one here (Algorithm 13 again, most of what makes // this crate's decapsulation slower than its encapsulation); an expanded key hands over // the copy it holds. - let c_prime = Self::pke_encrypt(&dk.t_hat_packed(), dk.rho(), m_prime, &r_prime); + let c_prime = Self::pke_encrypt(&dk.t_hat_packed(), dk.rho(), &m_prime, &r_prime); // 9: if 𝑐 ≠ 𝑐′ then // 10: 𝐾′ ← 𝐾_bar // ▷ if ciphertexts do not match, “implicitly reject" - let mut K_out = [0u8; MLKEM_SS_LEN]; + let mut K_out = Secret::<[u8; MLKEM_SS_LEN]>::new(); conditional_copy_bytes(&K_prime, &K_bar, &mut K_out, ct_eq_bytes_mask(c, &c_prime)); K_out @@ -502,7 +511,10 @@ impl< ) -> Result<(PK, SK), KEMError> { let (pk, sk) = Self::keygen_internal(seed)?; - if !ct_eq_bytes(&sk.encode_full_sk(), encoded_sk) { + // The full encoding is private-key material: made in a `Secret` buffer for the comparison. + let mut full = Secret::<[u8; FULL_SK_LEN]>::new(); + sk.encode_full_sk_out(&mut full); + if !ct_eq_bytes(&*full, encoded_sk) { return Err(KEMError::KeyGenError("Encoded key does not match generated key")); } @@ -600,13 +612,13 @@ impl< if rng.security_strength() < P::MAX_SECURITY_STRENGTH { Err(RNGError::SecurityStrengthInsufficientForAlgorithm)?; } - let mut m = [0u8; 32]; - rng.next_bytes_out(&mut m)?; + let mut m = Secret::<[u8; 32]>::new(); + rng.next_bytes_out(&mut *m)?; - let (ss_bytes, ct) = Self::encaps_internal(pk, m); + let (ss_bytes, ct) = Self::encaps_internal(pk, &m); let mut ss_keymaterial = - KeyMaterial::::from_bytes_as_type(&ss_bytes, KeyType::CryptographicRandom)?; + KeyMaterial::::from_bytes_as_type(&*ss_bytes, KeyType::CryptographicRandom)?; do_hazardous_operations(&mut ss_keymaterial, |ss_keymaterial| { ss_keymaterial.set_security_strength(P::MAX_SECURITY_STRENGTH) })?; @@ -670,7 +682,7 @@ impl< let ss_bytes = Self::decaps_internal(dk, ct); let mut ss_keymaterial = - KeyMaterial::::from_bytes_as_type(&ss_bytes, KeyType::CryptographicRandom)?; + KeyMaterial::::from_bytes_as_type(&*ss_bytes, KeyType::CryptographicRandom)?; do_hazardous_operations(&mut ss_keymaterial, |ss_keymaterial| { ss_keymaterial.set_security_strength(P::MAX_SECURITY_STRENGTH) })?; diff --git a/crypto/mlkem-lowmemory/src/mlkem_keys.rs b/crypto/mlkem-lowmemory/src/mlkem_keys.rs index d0c4784d3..a06bbb6ce 100644 --- a/crypto/mlkem-lowmemory/src/mlkem_keys.rs +++ b/crypto/mlkem-lowmemory/src/mlkem_keys.rs @@ -266,7 +266,7 @@ impl { fn eq(&self, other: &Self) -> bool { - let self_encoded = self.encode(); - let other_encoded = other.encode(); - bouncycastle_utils::ct::ct_eq_bytes(self_encoded.as_ref(), other_encoded.as_ref()) + // The encodings are the seed: held in `Secret` buffers for the comparison. + let mut self_encoded = Secret::<[u8; SK_LEN]>::new(); + let mut other_encoded = Secret::<[u8; SK_LEN]>::new(); + self.encode_out(&mut self_encoded); + other.encode_out(&mut other_encoded); + bouncycastle_utils::ct::ct_eq_bytes(&*self_encoded, &*other_encoded) } } From 7122745f37449ecd61a89d607898fd00936fe7a7 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 09:33:37 +1100 Subject: [PATCH 31/57] core: SignatureError gains RejectionLimitExceeded and PreHashTooWeak, and ConsistencyCheckFailed loses its empty parentheses (the 2026-11-03 ML-DSA review's M4 a, b and d: the bounded rejection loop and the weak pre-hash were GenericError strings, and the loop's told the deterministic variant to retry with a different nonce); the two ML-DSA crates and their tests use the variants, and the key, worker and crate docs say signing can fail that way Assisted-by: Claude:claude-fable-5-1 --- crypto/core/src/errors.rs | 10 ++++++++-- crypto/core/src/signature_key.rs | 9 +++++---- crypto/core/src/traits.rs | 5 +++-- crypto/mldsa-lowmemory/src/hash_mldsa.rs | 4 +--- crypto/mldsa-lowmemory/src/mldsa.rs | 6 ++---- crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs | 8 ++++---- crypto/mldsa-lowmemory/tests/mldsa_tests.rs | 8 ++++---- crypto/mldsa/src/hash_mldsa.rs | 4 +--- crypto/mldsa/src/mldsa.rs | 6 ++---- crypto/mldsa/tests/hash_mldsa_tests.rs | 8 ++++---- crypto/mldsa/tests/mldsa_tests.rs | 8 ++++---- 11 files changed, 38 insertions(+), 38 deletions(-) diff --git a/crypto/core/src/errors.rs b/crypto/core/src/errors.rs index 211cb9220..485550db8 100644 --- a/crypto/core/src/errors.rs +++ b/crypto/core/src/errors.rs @@ -137,8 +137,14 @@ pub use bouncycastle_utils::suspendable_state::SuspendableError; pub enum SignatureError { /// GenericError(&'static str), - /// - ConsistencyCheckFailed(), + /// The public and private key of a pair do not belong together. + ConsistencyCheckFailed, + /// The algorithm bounds its rejection-sampling loop (FIPS 204 Section 6.2 allows ML-DSA to) + /// and the bound was reached without a signature; nothing was written. The hedged variant + /// draws fresh randomness on a retry; the deterministic variant fails the same way again. + RejectionLimitExceeded, + /// The pre-hash is weaker than the parameter set (FIPS 204 Section 5.4 for HashML-DSA). + PreHashTooWeak, /// EncodingError(&'static str), /// diff --git a/crypto/core/src/signature_key.rs b/crypto/core/src/signature_key.rs index 1cedb7984..99d4435b3 100644 --- a/crypto/core/src/signature_key.rs +++ b/crypto/core/src/signature_key.rs @@ -91,7 +91,8 @@ pub trait SigningKey { /// written, always `SIG_LEN`. /// /// # Errors - /// Those of [`create_signer`](Self::create_signer) and of the signer's final step. + /// Those of [`create_signer`](Self::create_signer) and of the signer's final step ([`SignatureError::RejectionLimitExceeded`] + /// for an algorithm that bounds its rejection loop). fn sign_out( &self, rng: &mut dyn RNG, @@ -219,7 +220,7 @@ pub trait PreHashSigningKey: /// signs the digest. /// /// # Errors - /// As [`SigningKey::create_signer`]; also [`SignatureError::GenericError`] if the algorithm + /// As [`SigningKey::create_signer`]; also [`SignatureError::PreHashTooWeak`] if the algorithm /// does not accept `H` (FIPS 204 Section 5.4 requires a hash of at least the parameter set's /// strength for HashML-DSA). fn create_ph_signer<'k, H: PreHash>( @@ -274,7 +275,7 @@ pub trait DeterministicPreHashSigningKey( &'k self, ctx: Option<&[u8]>, @@ -324,7 +325,7 @@ pub trait PreHashVerifyingKey: /// and verifies over the digest. /// /// # Errors - /// As [`VerifyingKey::create_verifier`]; also [`SignatureError::GenericError`] if the + /// As [`VerifyingKey::create_verifier`]; also [`SignatureError::PreHashTooWeak`] if the /// algorithm does not accept `H`. fn create_ph_verifier<'k, H: PreHash>( &'k self, diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index fd4d965c4..15dbf5d71 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -865,8 +865,9 @@ pub trait SignatureSigner: AlgorithmInfo + Sized { /// Produces the signature. Consumes the worker. /// /// # Errors - /// Whatever the algorithm's own signing step can fail with; the key, the context and any - /// randomness were checked when the worker was made. + /// [`SignatureError::RejectionLimitExceeded`] for an algorithm that bounds its rejection loop, + /// and whatever else its signing step can fail with; the key, the context and any randomness + /// were checked when the worker was made. fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError>; /// As [`do_final`](Self::do_final), into `output`, which is zeroized first; returns diff --git a/crypto/mldsa-lowmemory/src/hash_mldsa.rs b/crypto/mldsa-lowmemory/src/hash_mldsa.rs index 58a9ac67a..c30095f7a 100644 --- a/crypto/mldsa-lowmemory/src/hash_mldsa.rs +++ b/crypto/mldsa-lowmemory/src/hash_mldsa.rs @@ -125,9 +125,7 @@ impl AlgorithmOID for HashMLDSA87_with_SHA512 { /// FIPS 204 Section 5.4: the hash must give at least the parameter set's strength. fn check_hash_strength() -> Result<(), SignatureError> { if H::MAX_SECURITY_STRENGTH < P::MAX_SECURITY_STRENGTH { - return Err(SignatureError::GenericError( - "HashML-DSA: the pre-hash is weaker than the ML-DSA parameter set (FIPS 204 Section 5.4)", - )); + return Err(SignatureError::PreHashTooWeak); } Ok(()) } diff --git a/crypto/mldsa-lowmemory/src/mldsa.rs b/crypto/mldsa-lowmemory/src/mldsa.rs index b46bae0cd..9fc71cfc5 100644 --- a/crypto/mldsa-lowmemory/src/mldsa.rs +++ b/crypto/mldsa-lowmemory/src/mldsa.rs @@ -536,7 +536,7 @@ impl MLDSATrait

for MLDSA

{ if bouncycastle_utils::ct::ct_eq_bytes(&sk.tr(), &pk.compute_tr()) { Ok(()) } else { - Err(SignatureError::ConsistencyCheckFailed()) + Err(SignatureError::ConsistencyCheckFailed) } } } @@ -610,9 +610,7 @@ pub(crate) fn sign_internal< // "Implementations may limit the number of iterations in this loop to not exceed a finite maximum value." // mutants note: there is no test for this because we don't have access to a KAT that will exceed this limit. if kappa > 1000 * P::k as u16 { - return Err(SignatureError::GenericError( - "Rejection sampling loop exceeded max iterations, try again with a different signing nonce.", - )); + return Err(SignatureError::RejectionLimitExceeded); } // 11-15: derive c_tilde without materializing y_hat or w as full vectors. diff --git a/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs index 21941b3fb..b1f20c65f 100644 --- a/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs @@ -61,19 +61,19 @@ mod hash_mldsa_tests { let ph = SHA256::hash(b"msg"); assert!(matches!( sk.create_ph_signer::(&mut HashDRBG_SHA512::new_from_os(), None), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); assert!(matches!( sk.sign_ph::(&mut HashDRBG_SHA512::new_from_os(), &ph, None), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); assert!(matches!( pk.create_ph_verifier::(None), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); assert!(matches!( pk.verify_ph::(&ph, None, &[0u8; MLDSA65_SIG_LEN]), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); } diff --git a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs index 6fe827290..de7c51079 100644 --- a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs @@ -844,11 +844,11 @@ mod mldsa_tests { // failure case: different but valid key let (pk2, sk2) = MLDSA44::keygen(&mut DefaultRNG::default()).unwrap(); match MLDSA44::keypair_consistency_check(&pk, &sk2) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; match MLDSA44::keypair_consistency_check(&pk2, &sk) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; @@ -857,7 +857,7 @@ mod mldsa_tests { pk_bytes[17] ^= 0x01; let pk2 = MLDSA44PublicKey::from_bytes(&pk_bytes).unwrap(); match MLDSA44::keypair_consistency_check(&pk2, &sk) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; @@ -865,7 +865,7 @@ mod mldsa_tests { sk_bytes[17] ^= 0x01; let sk2 = MLDSA44PrivateKey::from_bytes(&sk_bytes).unwrap(); match MLDSA44::keypair_consistency_check(&pk, &sk2) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; } diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index 6a55e8bec..31c4f9463 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -128,9 +128,7 @@ impl AlgorithmOID for HashMLDSA87_with_SHA512 { /// FIPS 204 Section 5.4: the hash must give at least the parameter set's strength. fn check_hash_strength() -> Result<(), SignatureError> { if H::MAX_SECURITY_STRENGTH < P::MAX_SECURITY_STRENGTH { - return Err(SignatureError::GenericError( - "HashML-DSA: the pre-hash is weaker than the ML-DSA parameter set (FIPS 204 Section 5.4)", - )); + return Err(SignatureError::PreHashTooWeak); } Ok(()) } diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs index fc070f8bb..31b1672ce 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -715,9 +715,7 @@ pub(crate) fn sign_internal< // mutants note: there is no test for this because, at this point, // we don't know of a KAT that will exceed this limit. if kappa > 1000 * P::k as u16 { - return Err(SignatureError::GenericError( - "Rejection sampling loop exceeded max iterations, try again with a different signing nonce.", - )); + return Err(SignatureError::RejectionLimitExceeded); } // 11: 𝐲 ∈ 𝑅^ℓ ← ExpandMask(𝜌″, 𝜅) @@ -981,7 +979,7 @@ impl MLDSATrait

for MLDSA

{ if derived_pk.compute_tr() == pk.compute_tr() { Ok(()) } else { - Err(SignatureError::ConsistencyCheckFailed()) + Err(SignatureError::ConsistencyCheckFailed) } } } diff --git a/crypto/mldsa/tests/hash_mldsa_tests.rs b/crypto/mldsa/tests/hash_mldsa_tests.rs index ea6c4c63e..9fb04f736 100644 --- a/crypto/mldsa/tests/hash_mldsa_tests.rs +++ b/crypto/mldsa/tests/hash_mldsa_tests.rs @@ -65,19 +65,19 @@ mod hash_mldsa_tests { let ph = SHA256::hash(b"msg"); assert!(matches!( sk.create_ph_signer::(&mut HashDRBG_SHA512::new_from_os(), None), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); assert!(matches!( sk.sign_ph::(&mut HashDRBG_SHA512::new_from_os(), &ph, None), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); assert!(matches!( pk.create_ph_verifier::(None), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); assert!(matches!( pk.verify_ph::(&ph, None, &[0u8; MLDSA65_SIG_LEN]), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); // ML-DSA-44 accepts it sk_44_accepts_sha256(); diff --git a/crypto/mldsa/tests/mldsa_tests.rs b/crypto/mldsa/tests/mldsa_tests.rs index de6badd53..e4b5cf187 100644 --- a/crypto/mldsa/tests/mldsa_tests.rs +++ b/crypto/mldsa/tests/mldsa_tests.rs @@ -947,11 +947,11 @@ mod mldsa_tests { // failure case: different but valid key let (pk2, sk2) = MLDSA44::keygen(&mut DefaultRNG::default()).unwrap(); match MLDSA44::keypair_consistency_check(&pk, &sk2) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; match MLDSA44::keypair_consistency_check(&pk2, &sk) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; @@ -960,7 +960,7 @@ mod mldsa_tests { pk_bytes[17] ^= 0x01; let pk2 = MLDSA44PublicKey::from_bytes(&pk_bytes).unwrap(); match MLDSA44::keypair_consistency_check(&pk2, &sk) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; @@ -968,7 +968,7 @@ mod mldsa_tests { sk_bytes[17] ^= 0x01; let sk2 = MLDSA44PrivateKey::from_bytes(&sk_bytes).unwrap(); match MLDSA44::keypair_consistency_check(&pk, &sk2) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; } From c312729c082c85afac1eb750f73668bc211680dc Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 09:34:11 +1100 Subject: [PATCH 32/57] mldsa-common: check_norm and make_hint are branch-free (the ML-DSA review's M3: the early return revealed the index of the first coefficient of a rejected candidate over the bound, and make_hint's short-circuit the sign of the centred low part, both inside the rejection loop; FIPS 204 asks for neither, and the reference implementations branch, but an OR-accumulated norm and a mask-form hint cost nothing); tests compare both with the comparisons they replace over every input Assisted-by: Claude:claude-fable-5-1 --- crypto/mldsa-common/src/aux_functions.rs | 14 ++-- crypto/mldsa-common/src/polynomial.rs | 23 ++--- crypto/mldsa-common/tests/rounding_tests.rs | 93 +++++++++++++++++++++ 3 files changed, 109 insertions(+), 21 deletions(-) create mode 100644 crypto/mldsa-common/tests/rounding_tests.rs diff --git a/crypto/mldsa-common/src/aux_functions.rs b/crypto/mldsa-common/src/aux_functions.rs index 636c20fce..b5c2f2de5 100644 --- a/crypto/mldsa-common/src/aux_functions.rs +++ b/crypto/mldsa-common/src/aux_functions.rs @@ -687,12 +687,14 @@ pub fn make_hint(low: i32, high: i32) -> i32 { // // 3: return [[𝑟1 ≠ 𝑣1]] // if r1 != v1 { 1 } else { 0 } - // mutants note: we do not have KATs that exercise all branches of this if - if low <= P::gamma2 || low > q - P::gamma2 || (low == q - P::gamma2 && high == 0) { - 0 - } else { - 1 - } + // Branch-free form of `low <= γ2 || low > q - γ2 || (low == q - γ2 && high == 0)`: each + // comparison is the sign bit of a difference of values in [0, q), so nothing overflows, and + // the combined mask is all ones exactly when the hint is 0. + let le = (low - P::gamma2 - 1) >> 31; // low <= γ2 + let gt = (q - P::gamma2 - low) >> 31; // low > q - γ2 + let eq = ((low ^ (q - P::gamma2)) - 1) >> 31; // low == q - γ2 + let high_zero = (high - 1) >> 31; // high == 0 + (le | gt | (eq & high_zero)) + 1 } /// Algorithm 40 UseHint(ℎ, 𝑟) diff --git a/crypto/mldsa-common/src/polynomial.rs b/crypto/mldsa-common/src/polynomial.rs index 009549768..4e187ee0f 100644 --- a/crypto/mldsa-common/src/polynomial.rs +++ b/crypto/mldsa-common/src/polynomial.rs @@ -132,13 +132,6 @@ impl Polynomial { /// type parameter cannot be used as a const generic argument. It is still a constant after /// monomorphization, so this costs nothing. pub fn check_norm(&self, bound: i32) -> bool { - // It is acceptable that this function is not constant-time (returns true early) - // The reason being because it is used in a rejection loop. - // That is, the early quit here leads to rejection, dropping the secret values and - // continuing to the top of the rejection loop with generating new secret values, - // or failing the signature validation. - // So the i32 that we just checked in a non-constant-time manner is about to get thrown away. - // Note: this formulation of the check_norm function usually requires this bounds check // if bound > (q - 1) / 8 { // return true; @@ -147,16 +140,16 @@ impl Polynomial { // instead to make sure the value is what we expect. debug_assert!(bound <= (q - 1) / 8); - let mut t: i32; + // Branch-free: the early return of the reference implementation reveals only that a + // candidate was rejected, but also the index of the first coefficient over the bound, + // a position-dependent fact about the rejected y + c*s1. |x| < 2^31 - 1 and bound > 0, so + // `bound - 1 - t` cannot overflow and is negative exactly when t >= bound. + let mut acc: i32 = 0; for x in self.coeffs.iter() { - t = *x >> 31; - t = *x - (t & (2 * *x)); - - if t >= bound { - return true; - } + let t = *x - ((*x >> 31) & (2 * *x)); + acc |= (bound - 1 - t) >> 31; } - false + acc != 0 } /// Multiplies every coefficient by 2^𝑑. diff --git a/crypto/mldsa-common/tests/rounding_tests.rs b/crypto/mldsa-common/tests/rounding_tests.rs new file mode 100644 index 000000000..fd1f2037d --- /dev/null +++ b/crypto/mldsa-common/tests/rounding_tests.rs @@ -0,0 +1,93 @@ +//! The branch-free forms of `make_hint` and `Polynomial::check_norm` against the comparisons they +//! replace, over every input the signer can feed them. + +use bouncycastle_mldsa_common::aux_functions::make_hint; +use bouncycastle_mldsa_common::params::{MLDSA44Params, MLDSA65Params, MLDSAParamsBase}; +use bouncycastle_mldsa_common::polynomial::Polynomial; +use bouncycastle_mldsa_common::q; + +/// `make_hint` as the comparisons of its doc comment, which are FIPS 204 Algorithm 39 in the +/// reference implementation's (low, high) form. +fn make_hint_by_comparison(low: i32, high: i32) -> i32 { + if low <= P::gamma2 || low > q - P::gamma2 || (low == q - P::gamma2 && high == 0) { + 0 + } else { + 1 + } +} + +fn make_hint_grid() { + let high_max = (q - 1) / (2 * P::gamma2); + for high in [0, 1, 2, high_max - 1, high_max] { + for low in 0..q { + assert_eq!( + make_hint::

(low, high), + make_hint_by_comparison::

(low, high), + "low {low} high {high}" + ); + } + } +} + +#[test] +fn make_hint_matches_the_comparisons_for_gamma2_88() { + make_hint_grid::(); +} + +#[test] +fn make_hint_matches_the_comparisons_for_gamma2_32() { + make_hint_grid::(); +} + +/// `check_norm` as FIPS 204 reads it: true when any |coefficient| >= bound. +fn check_norm_by_comparison(p: &Polynomial, bound: i32) -> bool { + (0..256).any(|i| p[i].abs() >= bound) +} + +#[test] +fn check_norm_matches_the_comparison_at_every_index_and_edge() { + let bounds = [ + MLDSA44Params::gamma1_minus_beta, + MLDSA44Params::gamma2_minus_beta, + MLDSA44Params::gamma2, + MLDSA65Params::gamma1_minus_beta, + MLDSA65Params::gamma2_minus_beta, + MLDSA65Params::gamma2, + 1, + ]; + for bound in bounds { + for i in 0..256 { + for v in [bound - 1, bound, bound + 1, -(bound - 1), -bound, -(bound + 1), 0] { + let mut p = Polynomial::new(); + p[i] = v; + assert_eq!( + p.check_norm(bound), + check_norm_by_comparison(&p, bound), + "bound {bound} index {i} value {v}" + ); + } + } + } +} + +#[test] +fn check_norm_matches_the_comparison_on_mixed_polynomials() { + // A small LCG so the test needs no RNG crate; values spread over (-q, q). + let mut x: u32 = 0x2545_F491; + let mut next = || { + x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + (x % (2 * q as u32)) as i32 - q + }; + let bound = MLDSA44Params::gamma1_minus_beta; + for _ in 0..200 { + let mut p = Polynomial::new(); + for i in 0..256 { + // Mostly in range, so that both outcomes occur. + p[i] = next() % bound; + } + if next() & 1 == 0 { + p[(next().unsigned_abs() % 256) as usize] = next(); + } + assert_eq!(p.check_norm(bound), check_norm_by_comparison(&p, bound)); + } +} From a2bc4794307a4c751a8e7a7b9ad83b68bd4a7cd4 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 09:34:11 +1100 Subject: [PATCH 33/57] mldsa: the ML-DSA review's M2 (rho' in key generation and rho'' in signing are Secret, so the error path wipes them too, in place of a plain fill on the success path; the zeroization doc says what stays plain), M4 c and e (the seed error names KeyType::CryptographicRandom; sig_decode returns Option), M5 b (the key-pair check compares tr in constant time) and M7's doc (a pre-hash of enough strength is accepted; that it is approved is the caller's) Assisted-by: Claude:claude-fable-5-1 --- crypto/mldsa/src/aux_functions.rs | 10 +++++----- crypto/mldsa/src/hash_mldsa.rs | 5 +++-- crypto/mldsa/src/lib.rs | 16 +++++++++++++--- crypto/mldsa/src/mldsa.rs | 19 ++++++++----------- crypto/mldsa/src/mldsa_keys.rs | 4 ++++ 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/crypto/mldsa/src/aux_functions.rs b/crypto/mldsa/src/aux_functions.rs index 56ffe1aa7..0b0c0b84a 100644 --- a/crypto/mldsa/src/aux_functions.rs +++ b/crypto/mldsa/src/aux_functions.rs @@ -70,7 +70,7 @@ pub(crate) type DecodedSig

= /// Output: (c_tilde, z, h) pub(crate) fn sig_decode( sig: &[u8; SIG_LEN], -) -> Result, ()> { +) -> Option> { debug_assert_eq!(SIG_LEN, P::SIG_LEN); let mut c_tilde = ::ZEROED; let mut z = P::VecL::new(); @@ -101,7 +101,7 @@ pub(crate) fn sig_decode( if sig[pos + (P::omega as usize) + i] < (idx as u8) || sig[pos + (P::omega as usize) + i] > P::omega as u8 { - return Err(()); + return None; } // 6: First ← Index @@ -112,7 +112,7 @@ pub(crate) fn sig_decode( // 9: if 𝑦[Index − 1] ≥ 𝑦[Index] then return ⊥ // ▷ malformed input if j > idx && sig[pos + j - 1] >= sig[pos + j] { - return Err(()); + return None; } // 12: 𝐡[𝑖]_𝑦[Index] ← 1 h.elems_mut()[i][sig[pos + j] as usize] = 1; @@ -127,11 +127,11 @@ pub(crate) fn sig_decode( // ▷ read any leftover bytes in the first 𝜔 bytes of 𝑦 for malformed (nonzero) bytes for j in idx..P::omega as usize { if sig[pos + j] != 0 { - return Err(()); + return None; } } - Ok((c_tilde, z, h)) + Some((c_tilde, z, h)) } /// Algorithm 32 ExpandA(𝜌) diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index 31c4f9463..e7cd8fd33 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -2,8 +2,9 @@ //! message, for the case where the message is hashed before the signing key is known or on a //! different machine. //! -//! The hash is a type parameter of the key's pre-hash methods, so any approved hash goes with any -//! ML-DSA key: +//! The hash is a type parameter of the key's pre-hash methods, so any hash of at least the +//! parameter set's strength goes with any ML-DSA key; that it is an approved one (FIPS 204 +//! Section 5.4) is the caller's to ensure: //! //! ```rust //! use bouncycastle_core::errors::SignatureError; diff --git a/crypto/mldsa/src/lib.rs b/crypto/mldsa/src/lib.rs index 17ca47679..10aa38e0c 100644 --- a/crypto/mldsa/src/lib.rs +++ b/crypto/mldsa/src/lib.rs @@ -120,13 +120,23 @@ //! 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. //! +//! ## When signing fails +//! +//! FIPS 204 Section 6.2 lets an implementation bound the rejection-sampling loop, and this one +//! does, at 1000·k iterations. Reaching it has negligible probability; when it happens, signing +//! returns [`SignatureError::RejectionLimitExceeded`] and writes nothing. The hedged variant +//! succeeds on a retry with fresh randomness; the deterministic one fails the same way again. +//! +//! [`SignatureError::RejectionLimitExceeded`]: bouncycastle_core::errors::SignatureError::RejectionLimitExceeded +//! //! ## What is zeroized //! //! FIPS 204 Section 3.6.3 asks that "any potentially sensitive intermediate data is destroyed as //! soon as it is no longer needed". Here the private key's `K`, `s1_hat` and `s2_hat` are `Secret` -//! and the seed is `KeyMaterial`, both wiped on drop, and the signing randomizer `rho''` is zeroized -//! by hand when signing finishes. The per-signature vectors `y`, `w`, `cs1`, `cs2`, `ct0` and `r0` -//! are plain stack values that are not wiped when their scope ends. +//! and the seed is `KeyMaterial`, and the key-generation seed `rho'` and the signing randomizer +//! `rho''` are `Secret`, all wiped on drop, on the error paths included. The per-signature vectors +//! `y`, `w`, `cs1`, `cs2`, `ct0` and `r0`, and the polynomials and buffer `sk_encode_out` packs +//! through, are plain stack values that are not wiped when their scope ends. #![no_std] #![forbid(unsafe_code)] diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs index 31b1672ce..f3488093d 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -565,7 +565,7 @@ impl MLDSA

{ || seed.key_len() != 32 { return Err(SignatureError::KeyGenError( - "Seed must be 32 bytes and KeyType::Seed or KeyType::BytesFullEntropy.", + "Seed must be 32 bytes and KeyType::Seed or KeyType::CryptographicRandom.", )); } @@ -589,8 +589,8 @@ impl MLDSA

{ let mut h = h.into_squeezer(); let bytes_written = h.do_output_out(&mut rho); debug_assert_eq!(bytes_written, 32); - let mut rho_prime: [u8; 64] = [0u8; 64]; - let bytes_written = h.do_output_out(&mut rho_prime); + let mut rho_prime: Secret<[u8; 64]> = Secret::new(); + let bytes_written = h.do_output_out(&mut *rho_prime); debug_assert_eq!(bytes_written, 64); let bytes_written = h.do_output_out(&mut *K); debug_assert_eq!(bytes_written, 32); @@ -681,16 +681,16 @@ pub(crate) fn sign_internal< // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀 ′, 64) // skip: mu has already been provided - let mut rho_p_p: [u8; 64] = { + let rho_p_p: Secret<[u8; 64]> = { // scope for h // 7: 𝜌″ ← H(𝐾||𝑟𝑛𝑑||𝜇, 64) let mut h = H::new(); h.do_update(&**sk.K()); h.do_update(&rnd); h.do_update(mu); - let mut rho_p_p = [0u8; 64]; + let mut rho_p_p: Secret<[u8; 64]> = Secret::new(); let mut h = h.into_squeezer(); - h.do_output_out(&mut rho_p_p); + h.do_output_out(&mut *rho_p_p); rho_p_p }; @@ -833,9 +833,6 @@ pub(crate) fn sign_internal< break; } - // zeroize rho_p_p before returning it to the OS - rho_p_p.fill(0u8); - // sig_encode does not necessarily write to all bytes of the output, so just to be safe: output.fill(0u8); @@ -866,7 +863,7 @@ pub(crate) fn verify_internal< // ▷ signer’s commitment hash c_tilde, response 𝐳, and hint 𝐡 // 3: if 𝐡 = ⊥ then return false let (c_tilde, z, h) = - sig_decode::(sig).map_err(|_| SignatureError::SignatureVerificationFailed)?; + sig_decode::(sig).ok_or(SignatureError::SignatureVerificationFailed)?; // 13 (first half) return [[ ||𝐳||∞ < 𝛾1 − 𝛽]] if z.check_norm(P::gamma1_minus_beta) { @@ -976,7 +973,7 @@ impl MLDSATrait

for MLDSA

{ ) -> Result<(), SignatureError> { // This is maybe a computationally heavy way to compare them, but it works let derived_pk = sk.derive_pk(); - if derived_pk.compute_tr() == pk.compute_tr() { + if bouncycastle_utils::ct::ct_eq_bytes(&derived_pk.compute_tr(), &pk.compute_tr()) { Ok(()) } else { Err(SignatureError::ConsistencyCheckFailed) diff --git a/crypto/mldsa/src/mldsa_keys.rs b/crypto/mldsa/src/mldsa_keys.rs index 20cd4b17d..1557db4a5 100644 --- a/crypto/mldsa/src/mldsa_keys.rs +++ b/crypto/mldsa/src/mldsa_keys.rs @@ -611,6 +611,10 @@ pub trait MLDSAPrivateKeyTrait: Sized + SignMuWithRnd

{ /// signature it verifies with a standard ML-DSA verifier, which need not know how `mu` was /// made. The hedged variant: `rnd` is drawn from `rng`. /// + /// # Errors + /// [`SignatureError::RejectionLimitExceeded`] if the bounded rejection loop (FIPS 204 + /// Section 6.2) runs out, which has negligible probability; nothing is written. + /// /// [`SigningKey::create_signer`]: bouncycastle_core::signature_key::SigningKey::create_signer fn sign_mu( &self, From e72aee4ed7aa032bee553f77243452023f5fff77 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 09:34:11 +1100 Subject: [PATCH 34/57] mldsa-lowmemory: the ML-DSA review's M1 (the iteration-cap return clears the caller's buffer, which held a rejected candidate's c~ and z: FIPS 204 s. 6.2 asks for "no other output"), M2 (rho'' is Secret; the zeroization doc says what stays plain), M4 c and e (the seed error text; unpack_z_row and compute_wp_approx_row return Option), M5 a, c and d (the public key compares in constant time, tr is returned by reference as in the full crate, MLDSA_SEED_LEN is re-exported), M6 (compute_z_component returns Option, and the signer doc's duplicated opening goes) and M7's doc Assisted-by: Claude:claude-fable-5-1 --- crypto/mldsa-lowmemory/src/aux_functions.rs | 4 ++-- crypto/mldsa-lowmemory/src/hash_mldsa.rs | 7 ++++--- crypto/mldsa-lowmemory/src/lib.rs | 18 ++++++++++++---- .../mldsa-lowmemory/src/low_memory_helpers.rs | 9 ++++---- crypto/mldsa-lowmemory/src/mldsa.rs | 21 ++++++++++--------- crypto/mldsa-lowmemory/src/mldsa_keys.rs | 19 ++++++++++------- crypto/mldsa-lowmemory/tests/mldsa_tests.rs | 2 +- 7 files changed, 48 insertions(+), 32 deletions(-) diff --git a/crypto/mldsa-lowmemory/src/aux_functions.rs b/crypto/mldsa-lowmemory/src/aux_functions.rs index 3570b9691..4ad8254c2 100644 --- a/crypto/mldsa-lowmemory/src/aux_functions.rs +++ b/crypto/mldsa-lowmemory/src/aux_functions.rs @@ -18,7 +18,7 @@ pub(crate) fn unpack_c_tilde(sig: &[u8]) -> P::SigCTilde { pub(crate) fn unpack_z_row( idx: usize, sig: &[u8; SIG_LEN], -) -> Result { +) -> Option { debug_assert!(idx < P::l); // skip to the start of the z's @@ -29,7 +29,7 @@ pub(crate) fn unpack_z_row( // Perform the norm check from // Alg 8; Line 13 (first half) return [[ ||𝐳||∞ < 𝛾1 − 𝛽]] - if z.check_norm(P::gamma1_minus_beta) { Err(()) } else { Ok(z) } + if z.check_norm(P::gamma1_minus_beta) { None } else { Some(z) } } /// Part of unpacking the sig value pub(crate) fn unpack_h_row( diff --git a/crypto/mldsa-lowmemory/src/hash_mldsa.rs b/crypto/mldsa-lowmemory/src/hash_mldsa.rs index c30095f7a..d6113561a 100644 --- a/crypto/mldsa-lowmemory/src/hash_mldsa.rs +++ b/crypto/mldsa-lowmemory/src/hash_mldsa.rs @@ -2,8 +2,9 @@ //! message, for the case where the message is hashed before the signing key is known or on a //! different machine. //! -//! The hash is a type parameter of the key's pre-hash methods, so any approved hash goes with any -//! ML-DSA key: +//! The hash is a type parameter of the key's pre-hash methods, so any hash of at least the +//! parameter set's strength goes with any ML-DSA key; that it is an approved one (FIPS 204 +//! Section 5.4) is the caller's to ensure: //! //! ```rust //! use bouncycastle_core::errors::SignatureError; @@ -224,7 +225,7 @@ fn sign_ph_with_rnd< check_hash_strength::()?; check_ph_len::(ph)?; // Algorithm 4, lines 1 and 23, and Algorithm 7, line 6 - let mu = compute_mu_ph(&sk.tr(), ctx, H::OID_DER, ph)?; + let mu = compute_mu_ph(sk.tr(), ctx, H::OID_DER, ph)?; // 24: 𝜎 ← ML-DSA.Sign_internal(𝑠𝑘, 𝑀′, 𝑟𝑛𝑑) sk.sign_mu_with_rnd_out(&mu, rnd, output) } diff --git a/crypto/mldsa-lowmemory/src/lib.rs b/crypto/mldsa-lowmemory/src/lib.rs index 80d05a946..3c7307c66 100644 --- a/crypto/mldsa-lowmemory/src/lib.rs +++ b/crypto/mldsa-lowmemory/src/lib.rs @@ -217,13 +217,23 @@ //! 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. //! +//! ## When signing fails +//! +//! FIPS 204 Section 6.2 lets an implementation bound the rejection-sampling loop, and this one +//! does, at 1000·k iterations. Reaching it has negligible probability; when it happens, signing +//! returns [`SignatureError::RejectionLimitExceeded`] and writes nothing. The hedged variant +//! succeeds on a retry with fresh randomness; the deterministic one fails the same way again. +//! +//! [`SignatureError::RejectionLimitExceeded`]: bouncycastle_core::errors::SignatureError::RejectionLimitExceeded +//! //! ## What is zeroized //! //! FIPS 204 Section 3.6.3 asks that "any potentially sensitive intermediate data is destroyed as //! soon as it is no longer needed". Here the private key's `K` and `rho'`, the packed `s1` and -//! `s2` it expands on demand, and the seed are `Secret` or `KeyMaterial`, wiped on drop. The -//! per-row vectors signing works through are plain stack values that are not wiped when their -//! scope ends. +//! `s2` it expands on demand, the seed, and the signing randomizer `rho''` are `Secret` or +//! `KeyMaterial`, wiped on drop, on the error paths included. The per-row vectors signing works +//! through, and the `s1` and `s2` rows unpacked for them, are plain stack values that are not +//! wiped when their scope ends. #![no_std] #![forbid(unsafe_code)] @@ -273,7 +283,7 @@ pub use hash_mldsa::HASH_ML_DSA_44_with_SHA512_NAME; pub use hash_mldsa::HASH_ML_DSA_65_with_SHA512_NAME; pub use hash_mldsa::HASH_ML_DSA_87_with_SHA512_NAME; -pub use mldsa::{MLDSA_MU_LEN, MLDSA_RND_LEN, MLDSA_TR_LEN}; +pub use mldsa::{MLDSA_MU_LEN, MLDSA_RND_LEN, MLDSA_SEED_LEN, MLDSA_TR_LEN}; pub use mldsa::{MLDSA44_PK_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN}; pub use mldsa::{MLDSA65_PK_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN}; pub use mldsa::{MLDSA87_PK_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN}; diff --git a/crypto/mldsa-lowmemory/src/low_memory_helpers.rs b/crypto/mldsa-lowmemory/src/low_memory_helpers.rs index b6235f1b1..cdb3d1f53 100644 --- a/crypto/mldsa-lowmemory/src/low_memory_helpers.rs +++ b/crypto/mldsa-lowmemory/src/low_memory_helpers.rs @@ -4,7 +4,6 @@ use crate::aux_functions::unpack_z_row; use crate::params::MLDSAParams; -use bouncycastle_core::errors::SignatureError; use bouncycastle_mldsa_common::aux_functions::{ bit_unpack_eta_out, expand_mask_poly, rej_ntt_poly, }; @@ -51,7 +50,7 @@ pub(crate) fn compute_wp_approx_row( t1: &Polynomial, c_hat: &Polynomial, idx: usize, -) -> Result { +) -> Option { // Algorithm 8: line 9: 𝐰′_approx ← NTT−1(𝐀_hat ∘ NTT(𝐳) − NTT(𝑐) ∘ NTT(𝐭1 ⋅ 2^𝑑)) // broken out for clarity: // NTT−1( @@ -99,7 +98,7 @@ pub(crate) fn compute_wp_approx_row( Az_acc.inv_ntt(); Az_acc.conditional_add_q(); - Ok(Az_acc) + Some(Az_acc) } pub(crate) fn compute_z_component( @@ -108,7 +107,7 @@ pub(crate) fn compute_z_component( c_hat: &Polynomial, kappa: u16, col: usize, -) -> Result, SignatureError> { +) -> Option { let y = expand_mask_poly::

(rho_p_p, kappa + col as u16); let mut s1_hat = *s1; s1_hat.ntt(); @@ -118,7 +117,7 @@ pub(crate) fn compute_z_component( let mut z = cs1; z.add_ntt(&y); - if z.check_norm(P::gamma1_minus_beta) { Ok(None) } else { Ok(Some(z)) } + if z.check_norm(P::gamma1_minus_beta) { None } else { Some(z) } } pub(crate) fn compute_w0cs2_component( diff --git a/crypto/mldsa-lowmemory/src/mldsa.rs b/crypto/mldsa-lowmemory/src/mldsa.rs index 9fc71cfc5..02d5a9256 100644 --- a/crypto/mldsa-lowmemory/src/mldsa.rs +++ b/crypto/mldsa-lowmemory/src/mldsa.rs @@ -533,7 +533,7 @@ impl MLDSATrait

for MLDSA

{ sk: &MLDSASeedPrivateKey

, ) -> Result<(), SignatureError> { // tr = H(pk) was computed when the key was built, so no derivation is needed here. - if bouncycastle_utils::ct::ct_eq_bytes(&sk.tr(), &pk.compute_tr()) { + if bouncycastle_utils::ct::ct_eq_bytes(sk.tr(), &pk.compute_tr()) { Ok(()) } else { Err(SignatureError::ConsistencyCheckFailed) @@ -586,14 +586,14 @@ pub(crate) fn sign_internal< // skip: mu has already been provided // Alg 7; 7: 𝜌″ ← H(𝐾||𝑟𝑛𝑑||𝜇, 64) - let rho_p_p: [u8; 64] = { + let rho_p_p: Secret<[u8; 64]> = { let mut h = H::new(); h.do_update(sk.K()); h.do_update(&rnd); h.do_update(mu); - let mut rho_p_p = [0u8; 64]; + let mut rho_p_p: Secret<[u8; 64]> = Secret::new(); let mut h = h.into_squeezer(); - h.do_output_out(&mut rho_p_p); + h.do_output_out(&mut *rho_p_p); rho_p_p }; @@ -610,6 +610,10 @@ pub(crate) fn sign_internal< // "Implementations may limit the number of iterations in this loop to not exceed a finite maximum value." // mutants note: there is no test for this because we don't have access to a KAT that will exceed this limit. if kappa > 1000 * P::k as u16 { + // "the signing algorithm shall return a constant that represents an error and no + // other output, destroying the results of the unsuccessful signing attempts" + // (FIPS 204 s. 6.2): the rejected candidate's c~ and z are in `output`. + output.fill(0); return Err(SignatureError::RejectionLimitExceeded); } @@ -655,7 +659,7 @@ pub(crate) fn sign_internal< &c_hat, kappa, col, - )? { + ) { Some(z) => z, None => { rejected = true; @@ -787,9 +791,9 @@ pub(crate) fn verify_internal< &c_hat, row, ) { - Ok(wp_approx) => wp_approx, + Some(wp_approx) => wp_approx, // means the norm check on z failed - Err(_) => return Err(SignatureError::SignatureVerificationFailed), + None => return Err(SignatureError::SignatureVerificationFailed), }; let h_i = match unpack_h_row::(row, sig) { @@ -875,9 +879,6 @@ pub(crate) fn draw_rnd(rng: &mut dyn RNG) -> Result<[u8; MLDSA_RND_LEN], Signatu Ok(rnd) } -/// An ML-DSA signature in progress: FIPS 204 Algorithm 7, with the message representative -/// (line 6) built as the message streams in. Made by [`SigningKey::create_signer`] on an ML-DSA -/// private key, which it borrows; the key is expanded from its seed only when the signature is /// An ML-DSA signature in progress: FIPS 204 Algorithm 7, with the message representative /// (line 6) built as the message streams in. Made by [`SigningKey::create_signer`] on an ML-DSA /// private key, which it borrows; the signing randomizer is fixed when the signer is made, and diff --git a/crypto/mldsa-lowmemory/src/mldsa_keys.rs b/crypto/mldsa-lowmemory/src/mldsa_keys.rs index 5bc7e7f86..347d853c7 100644 --- a/crypto/mldsa-lowmemory/src/mldsa_keys.rs +++ b/crypto/mldsa-lowmemory/src/mldsa_keys.rs @@ -209,7 +209,8 @@ impl Eq for MLDSAPublicKey

{} impl PartialEq for MLDSAPublicKey

{ fn eq(&self, other: &Self) -> bool { - self.rho == other.rho && self.t1_packed.as_ref() == other.t1_packed.as_ref() + bouncycastle_utils::ct::ct_eq_bytes(&self.rho, &other.rho) + & bouncycastle_utils::ct::ct_eq_bytes(self.t1_packed.as_ref(), other.t1_packed.as_ref()) } } @@ -259,7 +260,7 @@ pub trait MLDSAPrivateKeyTrait: Sized + SignMuWithRnd

{ fn seed(&self) -> Option<&KeyMaterial<32>>; /// Get a copy of the key hash `tr`, computed once when the key was built. - fn tr(&self) -> [u8; 64]; + fn tr(&self) -> &[u8; 64]; /// Derives the full public key from the seed: a key generation's worth of work. fn derive_pk(&self) -> MLDSAPublicKey

; /// This produces the full private key in the encoding specified in FIPS 204 Algorithm 24 skEncode() @@ -294,7 +295,7 @@ pub trait MLDSAPrivateKeyTrait: Sized + SignMuWithRnd

{ /// key's `tr` and to `ctx`, which then takes the message in chunks. `ctx` longer than 255 /// bytes is the error of FIPS 204 Algorithm 2, line 1. fn create_mu_builder(&self, ctx: Option<&[u8]>) -> Result { - MuBuilder::from_tr(&self.tr(), ctx) + MuBuilder::from_tr(self.tr(), ctx) } /// Signs an externally computed message representative `mu`: FIPS 204 Algorithm 7 with @@ -308,6 +309,10 @@ pub trait MLDSAPrivateKeyTrait: Sized + SignMuWithRnd

{ /// signature it verifies with a standard ML-DSA verifier, which need not know how `mu` was /// made. The hedged variant: `rnd` is drawn from `rng`. /// + /// # Errors + /// [`SignatureError::RejectionLimitExceeded`] if the bounded rejection loop (FIPS 204 + /// Section 6.2) runs out, which has negligible probability; nothing is written. + /// /// [`SigningKey::create_signer`]: bouncycastle_core::signature_key::SigningKey::create_signer fn sign_mu( &self, @@ -436,7 +441,7 @@ impl MLDSASeedPrivateKey

{ || seed.key_len() != 32 { return Err(SignatureError::KeyGenError( - "Seed must be 32 bytes and KeyType::Seed or KeyType::BytesFullEntropy.", + "Seed must be 32 bytes and KeyType::Seed or KeyType::CryptographicRandom.", )); } @@ -576,8 +581,8 @@ impl MLDSAPrivateKeyTrait

for MLDSASeedPrivateKey

{ Some(&self.seed) } - fn tr(&self) -> [u8; 64] { - self.tr + fn tr(&self) -> &[u8; 64] { + &self.tr } fn derive_pk(&self) -> MLDSAPublicKey

{ @@ -620,7 +625,7 @@ impl MLDSAPrivateKeyTrait

for MLDSASeedPrivateKey

{ out[0..32].copy_from_slice(&self.rho); // K is protected inside a Secret<[u8]>, but here we really do need to deref and copy it out. out[32..64].copy_from_slice(&*self.K); - out[64..128].copy_from_slice(&self.tr()); + out[64..128].copy_from_slice(self.tr()); off += 128; // 2: for 𝑖 from 0 to ℓ − 1 do diff --git a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs index de7c51079..80910c835 100644 --- a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs @@ -561,7 +561,7 @@ mod mldsa_tests { let tr: [u8; MLDSA_TR_LEN]; { let (_, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - tr = sk.tr(); + tr = *sk.tr(); } // BEGIN expected values From dda6bb34c308f0a22d81235a981063e8c3a0ebf9 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 09:49:21 +1100 Subject: [PATCH 35/57] sha3: SHAKE128 and SHAKE256 implement HashAlgParams as the fixed-length digests of RFC 8702 Section 3.1 (32 and 64 bytes under id-shake128 and id-shake256, the block length the sponge rate), the lengths their Hash impl already used, so that they serve as pre-hashes (FIPS 204 Algorithm 4's SHAKE128(M, 256)); a test pins the lengths and runs the framework's hash suite against the XOF at those lengths Assisted-by: Claude:claude-fable-5-1 --- crypto/sha3/src/lib.rs | 11 +++++++++++ crypto/sha3/tests/shake_tests.rs | 19 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 1e2650b74..4ce533057 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -508,3 +508,14 @@ impl AlgorithmOID for SHAKE256 { const OID_DER: &'static [u8] = &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0c]; } + +/// SHAKE as a fixed-length digest, for the algorithms that bind a hash's identity and length +/// into what they sign (HashML-DSA, PKCS#1): id-shake128 is 32 bytes and id-shake256 is 64 +/// (RFC 8702 Section 3.1: "the output length of SHAKE128 or SHAKE256 as the message digest +/// MUST be 32 or 64 bytes, respectively"), which is also FIPS 204 Algorithm 4's +/// SHAKE128(M, 256). The same lengths as [`Hash::output_len`] on these types; the block +/// length is the sponge rate. +impl HashAlgParams for SHAKEInternal { + const OUTPUT_LEN: usize = (PARAMS::SIZE as usize) / 4; + const BLOCK_LEN: usize = PARAMS::SIZE.rate_bits() / 8; +} diff --git a/crypto/sha3/tests/shake_tests.rs b/crypto/sha3/tests/shake_tests.rs index 0f74c82ff..46c275e65 100644 --- a/crypto/sha3/tests/shake_tests.rs +++ b/crypto/sha3/tests/shake_tests.rs @@ -7,8 +7,11 @@ mod shake_tests { KeyMaterial, KeyMaterial256, KeyMaterial512, KeyMaterialTrait, KeyType, }; use bouncycastle_core::security_strength::SecurityStrength; - use bouncycastle_core::traits::{AlgorithmInfo, Hash, HashOneShot, KDF, XOF, XOFSqueezer}; + use bouncycastle_core::traits::{ + AlgorithmInfo, Hash, HashAlgParams, HashOneShot, KDF, XOF, XOFSqueezer, + }; use bouncycastle_core_test_framework::DUMMY_SEED; + use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_core_test_framework::kdf::TestFrameworkKDF; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_sha3::{SHA3_256, SHAKE128, SHAKE256}; @@ -389,4 +392,18 @@ mod shake_tests { _ => panic!("Expected an error when loading a SHAKE256 state into SHA3-256"), } } + + /// RFC 8702 Section 3.1's fixed digests, which `HashAlgParams` carries: 32 bytes for SHAKE128 + /// and 64 for SHAKE256, the sponge rate as the block length (FIPS 202 Table 3), and the same + /// bytes as the XOF at that length, through the framework's hash suite. + #[test] + fn shake_as_a_fixed_length_digest() { + assert_eq!(SHAKE128::OUTPUT_LEN, 32); + assert_eq!(SHAKE256::OUTPUT_LEN, 64); + assert_eq!(SHAKE128::BLOCK_LEN, 168); + assert_eq!(SHAKE256::BLOCK_LEN, 136); + let tf = TestFrameworkHash::new(); + tf.test_hash::(&DUMMY_SEED[..512], &SHAKE128::new().xof(&DUMMY_SEED[..512], 32)); + tf.test_hash::(&DUMMY_SEED[..512], &SHAKE256::new().xof(&DUMMY_SEED[..512], 64)); + } } From cb587c05d91ecc3713826aaed4ce50efc38f54ec Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 09:49:21 +1100 Subject: [PATCH 36/57] mldsa: SHAKE128 and SHAKE256 as HashML-DSA pre-hashes (the ML-DSA review's M7): the module doc names them, and a test runs the framework's pre-hash suites with them, checks that SHAKE128 is refused above ML-DSA-44, and reproduces FIPS 204 Algorithm 4's SHAKE128 case by hand, M' with the OID bytes of line 18, through the external-mu path Assisted-by: Claude:claude-fable-5-1 --- crypto/mldsa/src/hash_mldsa.rs | 5 +-- crypto/mldsa/tests/hash_mldsa_tests.rs | 50 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index e7cd8fd33..e9c753ec7 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -3,8 +3,9 @@ //! different machine. //! //! The hash is a type parameter of the key's pre-hash methods, so any hash of at least the -//! parameter set's strength goes with any ML-DSA key; that it is an approved one (FIPS 204 -//! Section 5.4) is the caller's to ensure: +//! parameter set's strength goes with any ML-DSA key, SHAKE128 and SHAKE256 among them as the +//! fixed-length digests of RFC 8702 Section 3.1 (Algorithm 4's third case is SHAKE128); that it +//! is an approved one (FIPS 204 Section 5.4) is the caller's to ensure: //! //! ```rust //! use bouncycastle_core::errors::SignatureError; diff --git a/crypto/mldsa/tests/hash_mldsa_tests.rs b/crypto/mldsa/tests/hash_mldsa_tests.rs index 9fb04f736..23813c14d 100644 --- a/crypto/mldsa/tests/hash_mldsa_tests.rs +++ b/crypto/mldsa/tests/hash_mldsa_tests.rs @@ -16,10 +16,12 @@ mod hash_mldsa_tests { MLDSATrait, }; use bouncycastle_mldsa::{MLDSA44_SIG_LEN, MLDSA65_SIG_LEN, MLDSA87_SIG_LEN}; + use bouncycastle_mldsa::{MLDSAPrivateKeyTrait, MLDSAPublicKeyTrait}; use bouncycastle_rng::DefaultRNG; use bouncycastle_rng::FixedSeedRNG; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha2::{SHA256, SHA512}; + use bouncycastle_sha3::{SHAKE128, SHAKE256}; #[test] fn core_framework_signature() { @@ -511,4 +513,52 @@ mod hash_mldsa_tests { let sig = sk.sign_ph::(&mut HashDRBG_SHA512::new_from_os(), &ph512, None).unwrap(); pk.verify_ph::(&ph512, None, &sig).unwrap(); } + + /// FIPS 204 Algorithm 4, case SHAKE128 (lines 17 to 19): the pre-hash is SHAKE128(M, 256) + /// under OID 2.16.840.1.101.3.4.2.11, the 32-byte digest of RFC 8702 Section 3.1. Its + /// 128-bit strength fits ML-DSA-44 only; SHAKE256's 64-byte digest fits all three. + #[test] + fn shake_pre_hashes() { + let tf = TestFrameworkSignature::new(true); + tf.test_ph_signature::(|| MLDSA44::keygen(&mut DefaultRNG::default()), false); + tf.test_ph_signature::(|| MLDSA44::keygen(&mut DefaultRNG::default()), false); + tf.test_ph_signature::(|| MLDSA65::keygen(&mut DefaultRNG::default()), false); + tf.test_ph_signature::(|| MLDSA87::keygen(&mut DefaultRNG::default()), false); + + let (pk, sk) = MLDSA65::keygen(&mut DefaultRNG::default()).unwrap(); + assert!(matches!( + sk.create_ph_signer::(&mut DefaultRNG::default(), None), + Err(SignatureError::PreHashTooWeak) + )); + assert!(matches!( + pk.create_ph_verifier::(None), + Err(SignatureError::PreHashTooWeak) + )); + let (pk, sk) = MLDSA87::keygen(&mut DefaultRNG::default()).unwrap(); + assert!(matches!( + sk.create_ph_signer::(&mut DefaultRNG::default(), None), + Err(SignatureError::PreHashTooWeak) + )); + assert!(matches!( + pk.create_ph_verifier::(None), + Err(SignatureError::PreHashTooWeak) + )); + + // The signature is Algorithm 4's: M' = 0x01 || |ctx| || ctx || OID || SHAKE128(M, 256) + // (line 23, with the OID's DER bytes as line 18 prints them) and mu = H(tr || M', 64) + // (Algorithm 7 line 6), checked through the external-mu path. + let (pk, sk) = MLDSA44::keygen(&mut DefaultRNG::default()).unwrap(); + let (msg, ctx) = (b"pre-hashed with SHAKE128".as_slice(), b"ctx".as_slice()); + let ph: [u8; 32] = SHAKE128::hash(msg).try_into().unwrap(); + let mut tr_m_prime = pk.compute_tr().to_vec(); + tr_m_prime.extend_from_slice(&[0x01, ctx.len() as u8]); + tr_m_prime.extend_from_slice(ctx); + tr_m_prime + .extend_from_slice(&[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0B]); + tr_m_prime.extend_from_slice(&ph); + let mu: [u8; 64] = SHAKE256::hash(&tr_m_prime).try_into().unwrap(); + let by_mu: [u8; MLDSA44_SIG_LEN] = sk.sign_mu_deterministic(&mu).unwrap(); + assert_eq!(sk.sign_ph_deterministic::(&ph, Some(ctx)).unwrap(), by_mu); + pk.verify_ph::(&ph, Some(ctx), &by_mu).unwrap(); + } } From 20613a87c0eb20af472447581dbff0017852ee21 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 09:49:21 +1100 Subject: [PATCH 37/57] mldsa-lowmemory: SHAKE128 and SHAKE256 as HashML-DSA pre-hashes (the ML-DSA review's M7), the doc and the test of the full crate Assisted-by: Claude:claude-fable-5-1 --- crypto/mldsa-lowmemory/src/hash_mldsa.rs | 5 +- .../mldsa-lowmemory/tests/hash_mldsa_tests.rs | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/crypto/mldsa-lowmemory/src/hash_mldsa.rs b/crypto/mldsa-lowmemory/src/hash_mldsa.rs index d6113561a..29500a9fc 100644 --- a/crypto/mldsa-lowmemory/src/hash_mldsa.rs +++ b/crypto/mldsa-lowmemory/src/hash_mldsa.rs @@ -3,8 +3,9 @@ //! different machine. //! //! The hash is a type parameter of the key's pre-hash methods, so any hash of at least the -//! parameter set's strength goes with any ML-DSA key; that it is an approved one (FIPS 204 -//! Section 5.4) is the caller's to ensure: +//! parameter set's strength goes with any ML-DSA key, SHAKE128 and SHAKE256 among them as the +//! fixed-length digests of RFC 8702 Section 3.1 (Algorithm 4's third case is SHAKE128); that it +//! is an approved one (FIPS 204 Section 5.4) is the caller's to ensure: //! //! ```rust //! use bouncycastle_core::errors::SignatureError; diff --git a/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs index b1f20c65f..516a8e217 100644 --- a/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs @@ -19,9 +19,11 @@ mod hash_mldsa_tests { MLDSATrait, }; use bouncycastle_mldsa_lowmemory::{MLDSA44_SIG_LEN, MLDSA65_SIG_LEN, MLDSA87_SIG_LEN}; + use bouncycastle_mldsa_lowmemory::{MLDSAPrivateKeyTrait, MLDSAPublicKeyTrait}; use bouncycastle_rng::DefaultRNG; use bouncycastle_rng::FixedSeedRNG; use bouncycastle_sha2::{SHA256, SHA512}; + use bouncycastle_sha3::{SHAKE128, SHAKE256}; #[test] fn core_framework_signature() { @@ -342,4 +344,52 @@ mod hash_mldsa_tests { let sig = sk.sign_ph::(&mut HashDRBG_SHA512::new_from_os(), &ph512, None).unwrap(); pk.verify_ph::(&ph512, None, &sig).unwrap(); } + + /// FIPS 204 Algorithm 4, case SHAKE128 (lines 17 to 19): the pre-hash is SHAKE128(M, 256) + /// under OID 2.16.840.1.101.3.4.2.11, the 32-byte digest of RFC 8702 Section 3.1. Its + /// 128-bit strength fits ML-DSA-44 only; SHAKE256's 64-byte digest fits all three. + #[test] + fn shake_pre_hashes() { + let tf = TestFrameworkSignature::new(true); + tf.test_ph_signature::(|| MLDSA44::keygen(&mut DefaultRNG::default()), false); + tf.test_ph_signature::(|| MLDSA44::keygen(&mut DefaultRNG::default()), false); + tf.test_ph_signature::(|| MLDSA65::keygen(&mut DefaultRNG::default()), false); + tf.test_ph_signature::(|| MLDSA87::keygen(&mut DefaultRNG::default()), false); + + let (pk, sk) = MLDSA65::keygen(&mut DefaultRNG::default()).unwrap(); + assert!(matches!( + sk.create_ph_signer::(&mut DefaultRNG::default(), None), + Err(SignatureError::PreHashTooWeak) + )); + assert!(matches!( + pk.create_ph_verifier::(None), + Err(SignatureError::PreHashTooWeak) + )); + let (pk, sk) = MLDSA87::keygen(&mut DefaultRNG::default()).unwrap(); + assert!(matches!( + sk.create_ph_signer::(&mut DefaultRNG::default(), None), + Err(SignatureError::PreHashTooWeak) + )); + assert!(matches!( + pk.create_ph_verifier::(None), + Err(SignatureError::PreHashTooWeak) + )); + + // The signature is Algorithm 4's: M' = 0x01 || |ctx| || ctx || OID || SHAKE128(M, 256) + // (line 23, with the OID's DER bytes as line 18 prints them) and mu = H(tr || M', 64) + // (Algorithm 7 line 6), checked through the external-mu path. + let (pk, sk) = MLDSA44::keygen(&mut DefaultRNG::default()).unwrap(); + let (msg, ctx) = (b"pre-hashed with SHAKE128".as_slice(), b"ctx".as_slice()); + let ph: [u8; 32] = SHAKE128::hash(msg).try_into().unwrap(); + let mut tr_m_prime = pk.compute_tr().to_vec(); + tr_m_prime.extend_from_slice(&[0x01, ctx.len() as u8]); + tr_m_prime.extend_from_slice(ctx); + tr_m_prime + .extend_from_slice(&[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0B]); + tr_m_prime.extend_from_slice(&ph); + let mu: [u8; 64] = SHAKE256::hash(&tr_m_prime).try_into().unwrap(); + let by_mu: [u8; MLDSA44_SIG_LEN] = sk.sign_mu_deterministic(&mu).unwrap(); + assert_eq!(sk.sign_ph_deterministic::(&ph, Some(ctx)).unwrap(), by_mu); + pk.verify_ph::(&ph, Some(ctx), &by_mu).unwrap(); + } } From 4be9fda12fa478291ac7d7125dd70742a833b32f Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 10:03:35 +1100 Subject: [PATCH 38/57] cli: mlkem1024 keygen generates an ML-KEM-1024 key (it called the ML-KEM-512 keygen, so the key was 1632 bytes and no mlkem1024 command accepted it; the CLI review's L1), and the ML-KEM-768 public-key parser decodes a 768-sized hex window rather than a 512-sized one (L2) Assisted-by: Claude:claude-fable-5-1 --- cli/src/mlkem_cmd.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/mlkem_cmd.rs b/cli/src/mlkem_cmd.rs index 54c09e2ad..e0e92c590 100644 --- a/cli/src/mlkem_cmd.rs +++ b/cli/src/mlkem_cmd.rs @@ -301,7 +301,7 @@ pub(crate) fn mlkem1024_cmd( ) { match action { MLKEMAction::Keygen => { - let (_pk, sk) = MLKEM512::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); + let (_pk, sk) = MLKEM1024::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); write_bytes_or_hex(&sk.encode(), output_hex); } MLKEMAction::KeygenFromSeed => { @@ -518,7 +518,7 @@ fn parse_mlkem768_pk(bytes: &[u8]) -> Result { // try it as a hex'd full key if bytes.len() >= 2 * MLKEM768_PK_LEN { - let maybe_pk = hex::decode(&bytes[..2 * MLKEM512_PK_LEN]); + let maybe_pk = hex::decode(&bytes[..2 * MLKEM768_PK_LEN]); if maybe_pk.is_ok() { // it was hex let pk = MLKEM768PublicKey::from_bytes(&maybe_pk.unwrap()); From d125822155bf0093d9d4c486f6ed53b5cc692ead Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 10:10:35 +1100 Subject: [PATCH 39/57] cli: one body for the three ML-KEM and the three ML-DSA parameter sets (a ParameterSet table of the key types, lengths and key-pair functions replaces the six and three hand-copied command bodies and their twelve key parsers, the CLI review's L22), and the SHA-3, SHAKE, KMAC, TupleHash, ParallelHash and HKDF dispatches take an enum instead of a bit count or name with a panicking wildcard arm; the eight keygen-from-seed paths that unwrapped a bad seed now report it as mldsa44's did Assisted-by: Claude:claude-fable-5-1 --- cli/src/hkdf_cmd.rs | 23 +- cli/src/mac_cmd.rs | 10 +- cli/src/main.rs | 142 ++++- cli/src/mldsa_cmd.rs | 1298 +++++++----------------------------------- cli/src/mlkem_cmd.rs | 674 ++++++---------------- cli/src/sha3_cmd.rs | 56 +- 6 files changed, 540 insertions(+), 1663 deletions(-) diff --git a/cli/src/hkdf_cmd.rs b/cli/src/hkdf_cmd.rs index 55564d53f..b0c767ada 100644 --- a/cli/src/hkdf_cmd.rs +++ b/cli/src/hkdf_cmd.rs @@ -11,8 +11,16 @@ use bouncycastle::sm3::hkdf::HKDF_SM3; /// The salt is held in a fixed-size key buffer; the library itself puts no bound on it. const MAX_SALT_LEN: usize = 1024; +#[allow(non_camel_case_types)] +pub(crate) enum HKDFVariant { + SHA256, + SHA384, + SHA512, + SM3, +} + pub(crate) fn hkdf_cmd( - hkdfname: &str, + variant: HKDFVariant, salt: &Option, salt_file: &Option, ikm: &Option, @@ -64,38 +72,35 @@ pub(crate) fn hkdf_cmd( // streamed into the extract phase, so its length is not bounded by a KeyMaterial buffer, and the // additional input is the expand phase's `info`. The library refuses an L above 255 * HashLen. let mut out_key = KeyMaterial::<{ 255 * hkdf::MAX_HMAC_OUTPUT_LEN }>::new(); - let result = match hkdfname { - "HKDF-SHA256" => { + let result = match variant { + HKDFVariant::SHA256 => { let mut h = HKDF_SHA256::new(); h.do_extract_init(&salt_key).unwrap(); h.do_extract_update_bytes(&ikm_bytes).unwrap(); let prk = h.do_extract_final().unwrap(); HKDF_SHA256::expand_out(&prk, &info, len, &mut out_key) } - "HKDF-SHA384" => { + HKDFVariant::SHA384 => { let mut h = HKDF_SHA384::new(); h.do_extract_init(&salt_key).unwrap(); h.do_extract_update_bytes(&ikm_bytes).unwrap(); let prk = h.do_extract_final().unwrap(); HKDF_SHA384::expand_out(&prk, &info, len, &mut out_key) } - "HKDF-SHA512" => { + HKDFVariant::SHA512 => { let mut h = HKDF_SHA512::new(); h.do_extract_init(&salt_key).unwrap(); h.do_extract_update_bytes(&ikm_bytes).unwrap(); let prk = h.do_extract_final().unwrap(); HKDF_SHA512::expand_out(&prk, &info, len, &mut out_key) } - "HKDF-SM3" => { + HKDFVariant::SM3 => { let mut h = HKDF_SM3::new(); h.do_extract_init(&salt_key).unwrap(); h.do_extract_update_bytes(&ikm_bytes).unwrap(); let prk = h.do_extract_final().unwrap(); HKDF_SM3::expand_out(&prk, &info, len, &mut out_key) } - _ => { - panic!("{} is not a supported HKDF variant.", hkdfname); - } }; if let Err(e) = result { eprintln!("Error: {e:?}"); diff --git a/cli/src/mac_cmd.rs b/cli/src/mac_cmd.rs index 6cd942a39..d335c57d0 100644 --- a/cli/src/mac_cmd.rs +++ b/cli/src/mac_cmd.rs @@ -3,6 +3,7 @@ use std::io::Read; use std::process::exit; use crate::helpers::read_from_file_raw; +use crate::sha3_cmd::Strength; use bouncycastle::core::errors::MACError; use bouncycastle::core::hazmat::do_hazardous_operations; use bouncycastle::core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; @@ -92,7 +93,7 @@ pub(crate) fn mac_cmd( /// KMAC (NIST SP 800-185 Sec 4), which unlike HMAC takes a customization string and a caller- /// chosen tag length -- both are bound into the computation, so the verifier must use the same. pub(crate) fn kmac_cmd( - bit_len: usize, + strength: Strength, length: usize, customization: &Option, key: &Option, @@ -104,18 +105,17 @@ pub(crate) fn kmac_cmd( let s = customization.as_deref().unwrap_or("").as_bytes(); // The allow-weak form, as the HMAC commands use: a CLI is used for test vectors and // scripting, where a short or all-zero key is a legitimate thing to want. - match bit_len { - 128 => { + match strength { + Strength::Bits128 => { let kmac_key = KMAC128Key::with_params_allow_weak(&key, s, length) .unwrap_or_else(|e| kmac_key_error(e)); do_mac(kmac_key.create_generator(), verify_val, output_hex) } - 256 => { + Strength::Bits256 => { let kmac_key = KMAC256Key::with_params_allow_weak(&key, s, length) .unwrap_or_else(|e| kmac_key_error(e)); do_mac(kmac_key.create_generator(), verify_val, output_hex) } - _ => panic!("Unsupported algorithm: KMAC-{bit_len}"), } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 9ca59b9b0..bdda1cad9 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -17,9 +17,11 @@ mod sha2_cmd; mod sha3_cmd; mod sm3_cmd; +use crate::hkdf_cmd::HKDFVariant; use crate::mac_cmd::HMACVariant; use crate::mldsa_cmd::MLDSAAction; use crate::sha2_cmd::SHA2Variant; +use crate::sha3_cmd::{SHA3Variant, Strength}; use clap::{Parser, Subcommand}; use helpers::block_mode_helpers::CipherDirection; @@ -1939,13 +1941,40 @@ fn run() { encoders_cmd::base64_encode_cmd(); } Some(Subcommands::HashMLDSA44 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::hash_mldsa44_sha512_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); + mldsa_cmd::mldsa_cmd( + &mldsa_cmd::MLDSA44_SET, + action, + ctxfile, + skfile, + pkfile, + sigfile, + true, + *x, + ); } Some(Subcommands::HashMLDSA65 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::hash_mldsa65_sha512_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); + mldsa_cmd::mldsa_cmd( + &mldsa_cmd::MLDSA65_SET, + action, + ctxfile, + skfile, + pkfile, + sigfile, + true, + *x, + ); } Some(Subcommands::HashMLDSA87 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::hash_mldsa87_sha512_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); + mldsa_cmd::mldsa_cmd( + &mldsa_cmd::MLDSA87_SET, + action, + ctxfile, + skfile, + pkfile, + sigfile, + true, + *x, + ); } Some(Subcommands::HexDecode) => { encoders_cmd::hex_decode_cmd(); @@ -1963,8 +1992,15 @@ fn run() { len, x, }) => hkdf_cmd::hkdf_cmd( - "HKDF-SHA256", salt, salt_file, ikm, ikm_file, additional_input, additional_input_file, - *len, *x, + HKDFVariant::SHA256, + salt, + salt_file, + ikm, + ikm_file, + additional_input, + additional_input_file, + *len, + *x, ), Some(Subcommands::HKDF_SHA384 { salt, @@ -1976,8 +2012,15 @@ fn run() { len, x, }) => hkdf_cmd::hkdf_cmd( - "HKDF-SHA384", salt, salt_file, ikm, ikm_file, additional_input, additional_input_file, - *len, *x, + HKDFVariant::SHA384, + salt, + salt_file, + ikm, + ikm_file, + additional_input, + additional_input_file, + *len, + *x, ), Some(Subcommands::HKDF_SHA512 { salt, @@ -1989,8 +2032,15 @@ fn run() { len, x, }) => hkdf_cmd::hkdf_cmd( - "HKDF-SHA512", salt, salt_file, ikm, ikm_file, additional_input, additional_input_file, - *len, *x, + HKDFVariant::SHA512, + salt, + salt_file, + ikm, + ikm_file, + additional_input, + additional_input_file, + *len, + *x, ), Some(Subcommands::HKDF_SM3 { salt, @@ -2002,8 +2052,15 @@ fn run() { len, x, }) => hkdf_cmd::hkdf_cmd( - "HKDF-SM3", salt, salt_file, ikm, ikm_file, additional_input, additional_input_file, - *len, *x, + HKDFVariant::SM3, + salt, + salt_file, + ikm, + ikm_file, + additional_input, + additional_input_file, + *len, + *x, ), Some(Subcommands::HMAC_SHA256 { key, key_file, verify, x }) => { mac_cmd::mac_cmd(HMACVariant::SHA256, key, key_file, verify, *x) @@ -2021,47 +2078,74 @@ fn run() { mac_cmd::mac_cmd(HMACVariant::SM3, key, key_file, verify, *x) } Some(Subcommands::KMAC128 { length, customization, key, key_file, verify, x }) => { - mac_cmd::kmac_cmd(128, *length, customization, key, key_file, verify, *x) + mac_cmd::kmac_cmd(Strength::Bits128, *length, customization, key, key_file, verify, *x) } Some(Subcommands::KMAC256 { length, customization, key, key_file, verify, x }) => { - mac_cmd::kmac_cmd(256, *length, customization, key, key_file, verify, *x) + mac_cmd::kmac_cmd(Strength::Bits256, *length, customization, key, key_file, verify, *x) } Some(Subcommands::MLDSA44 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::mldsa44_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); + mldsa_cmd::mldsa_cmd( + &mldsa_cmd::MLDSA44_SET, + action, + ctxfile, + skfile, + pkfile, + sigfile, + false, + *x, + ); } Some(Subcommands::MLDSA65 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::mldsa65_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); + mldsa_cmd::mldsa_cmd( + &mldsa_cmd::MLDSA65_SET, + action, + ctxfile, + skfile, + pkfile, + sigfile, + false, + *x, + ); } Some(Subcommands::MLDSA87 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::mldsa87_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); + mldsa_cmd::mldsa_cmd( + &mldsa_cmd::MLDSA87_SET, + action, + ctxfile, + skfile, + pkfile, + sigfile, + false, + *x, + ); } Some(Subcommands::MLKEM512 { action, skfile, pkfile, ctfile, x }) => { - mlkem_cmd::mlkem512_cmd(action, skfile, pkfile, ctfile, *x); + mlkem_cmd::mlkem_cmd(&mlkem_cmd::MLKEM512_SET, action, skfile, pkfile, ctfile, *x); } Some(Subcommands::MLKEM768 { action, skfile, pkfile, ctfile, x }) => { - mlkem_cmd::mlkem768_cmd(action, skfile, pkfile, ctfile, *x); + mlkem_cmd::mlkem_cmd(&mlkem_cmd::MLKEM768_SET, action, skfile, pkfile, ctfile, *x); } Some(Subcommands::MLKEM1024 { action, skfile, pkfile, ctfile, x }) => { - mlkem_cmd::mlkem1024_cmd(action, skfile, pkfile, ctfile, *x); + mlkem_cmd::mlkem_cmd(&mlkem_cmd::MLKEM1024_SET, action, skfile, pkfile, ctfile, *x); } Some(Subcommands::PARALLELHASH128 { length, block_size, customization, x }) => { - sha3_cmd::parallelhash_cmd(128, *length, *block_size, customization, *x); + sha3_cmd::parallelhash_cmd(Strength::Bits128, *length, *block_size, customization, *x); } Some(Subcommands::PARALLELHASH256 { length, block_size, customization, x }) => { - sha3_cmd::parallelhash_cmd(256, *length, *block_size, customization, *x); + sha3_cmd::parallelhash_cmd(Strength::Bits256, *length, *block_size, customization, *x); } Some(Subcommands::RNG { len, x }) => rng_cmd::rng_cmd(*len, *x), Some(Subcommands::SHA3_224 { x }) => { - sha3_cmd::sha3_cmd(224, *x); + sha3_cmd::sha3_cmd(SHA3Variant::SHA3_224, *x); } Some(Subcommands::SHA3_256 { x }) => { - sha3_cmd::sha3_cmd(256, *x); + sha3_cmd::sha3_cmd(SHA3Variant::SHA3_256, *x); } Some(Subcommands::SHA3_384 { x }) => { - sha3_cmd::sha3_cmd(384, *x); + sha3_cmd::sha3_cmd(SHA3Variant::SHA3_384, *x); } Some(Subcommands::SHA3_512 { x }) => { - sha3_cmd::sha3_cmd(512, *x); + sha3_cmd::sha3_cmd(SHA3Variant::SHA3_512, *x); } Some(Subcommands::SHA224 { x }) => { sha2_cmd::sha2_cmd(SHA2Variant::SHA224, *x); @@ -2082,19 +2166,19 @@ fn run() { sha2_cmd::sha2_cmd(SHA2Variant::SHA512_256, *x); } Some(Subcommands::SHAKE128 { length, x }) => { - sha3_cmd::shake_cmd(128, *length, *x); + sha3_cmd::shake_cmd(Strength::Bits128, *length, *x); } Some(Subcommands::SHAKE256 { length, x }) => { - sha3_cmd::shake_cmd(256, *length, *x); + sha3_cmd::shake_cmd(Strength::Bits256, *length, *x); } Some(Subcommands::SM3 { x }) => { sm3_cmd::sm3_cmd(*x); } Some(Subcommands::TUPLEHASH128 { length, elements, customization, x }) => { - sha3_cmd::tuplehash_cmd(128, *length, elements, customization, *x); + sha3_cmd::tuplehash_cmd(Strength::Bits128, *length, elements, customization, *x); } Some(Subcommands::TUPLEHASH256 { length, elements, customization, x }) => { - sha3_cmd::tuplehash_cmd(256, *length, elements, customization, *x); + sha3_cmd::tuplehash_cmd(Strength::Bits256, *length, elements, customization, *x); } None => { eprintln!("No command provided. See -h") diff --git a/cli/src/mldsa_cmd.rs b/cli/src/mldsa_cmd.rs index b6697b357..54018ea4d 100644 --- a/cli/src/mldsa_cmd.rs +++ b/cli/src/mldsa_cmd.rs @@ -1,23 +1,28 @@ -//! Work in progress. -//! TODO: Use generic macros to eliminate duplicated code. +//! The `mldsa44`, `mldsa65`, `mldsa87` and `hash-mldsa*` subcommands: one body, [`mldsa_cmd`], +//! over the three parameter sets, which differ only in their key types, their lengths and the +//! functions that make and check a key pair. Those come in through a [`ParameterSet`], so a +//! length or a function can no longer be right for two sets and wrong for the third. use crate::helpers::{parse_seed, read_from_file, read_from_file_or_stdin, write_bytes_or_hex}; +use bouncycastle::core::errors::SignatureError; +use bouncycastle::core::key_material::KeyMaterial; use bouncycastle::core::signature_key::{ - PreHashSigningKey, PreHashVerifyingKey, SigningKey, VerifyingKey, + PreHashSigningKey, PreHashVerifyingKey, SignatureAlgorithm, SigningKey, VerifyingKey, }; use bouncycastle::core::traits::{ - SignaturePrivateKey, SignaturePublicKey, SignatureSigner, SignatureVerifier, + RNG, SignaturePrivateKey, SignaturePublicKey, SignatureSigner, SignatureVerifier, }; use bouncycastle::hex; use bouncycastle::mldsa::{ - MLDSA_SEED_LEN, MLDSA44, MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44PrivateKey, MLDSA44PublicKey, - MLDSA65, MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87, - MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPrivateKeyTrait, - MLDSATrait, + MLDSA_SEED_LEN, MLDSA44, MLDSA44_PK_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN, MLDSA44PrivateKey, + MLDSA44PublicKey, MLDSA65, MLDSA65_PK_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN, MLDSA65PrivateKey, + MLDSA65PublicKey, MLDSA87, MLDSA87_PK_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN, MLDSA87PrivateKey, + MLDSA87PublicKey, MLDSAPrivateKeyTrait, MLDSATrait, }; use bouncycastle::rng::HashDRBG_SHA512; use bouncycastle::sha2::SHA512; use clap::ValueEnum; +use core::marker::PhantomData; use std::io; use std::io::Read; use std::process::exit; @@ -42,17 +47,94 @@ pub(crate) enum MLDSAAction { Verify, } -pub(crate) fn mldsa44_cmd( +/// A parameter set as the command sees it: its name for messages, the algorithm marker the +/// signing traits are keyed by, and the key-pair operations the core traits do not carry. The +/// three sets below are the only instances. +pub(crate) struct ParameterSet< + A, + PK, + SK, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +> { + name: &'static str, + alg: PhantomData, + keygen: fn(&mut dyn RNG) -> Result<(PK, SK), SignatureError>, + keygen_from_seed: fn(&KeyMaterial) -> Result<(PK, SK), SignatureError>, + keypair_consistency_check: fn(&PK, &SK) -> Result<(), SignatureError>, + pk_of: fn(&SK) -> PK, +} + +pub(crate) const MLDSA44_SET: ParameterSet< + MLDSA44, + MLDSA44PublicKey, + MLDSA44PrivateKey, + MLDSA44_PK_LEN, + MLDSA44_SK_LEN, + MLDSA44_SIG_LEN, +> = ParameterSet { + name: "ML-DSA-44", + alg: PhantomData, + keygen: MLDSA44::keygen, + keygen_from_seed: MLDSA44::keygen_from_seed, + keypair_consistency_check: MLDSA44::keypair_consistency_check, + pk_of: |sk| sk.derive_pk(), +}; + +pub(crate) const MLDSA65_SET: ParameterSet< + MLDSA65, + MLDSA65PublicKey, + MLDSA65PrivateKey, + MLDSA65_PK_LEN, + MLDSA65_SK_LEN, + MLDSA65_SIG_LEN, +> = ParameterSet { + name: "ML-DSA-65", + alg: PhantomData, + keygen: MLDSA65::keygen, + keygen_from_seed: MLDSA65::keygen_from_seed, + keypair_consistency_check: MLDSA65::keypair_consistency_check, + pk_of: |sk| sk.derive_pk(), +}; + +pub(crate) const MLDSA87_SET: ParameterSet< + MLDSA87, + MLDSA87PublicKey, + MLDSA87PrivateKey, + MLDSA87_PK_LEN, + MLDSA87_SK_LEN, + MLDSA87_SIG_LEN, +> = ParameterSet { + name: "ML-DSA-87", + alg: PhantomData, + keygen: MLDSA87::keygen, + keygen_from_seed: MLDSA87::keygen_from_seed, + keypair_consistency_check: MLDSA87::keypair_consistency_check, + pk_of: |sk| sk.derive_pk(), +}; + +/// `prehash` selects HashML-DSA with SHA-512 (FIPS 204 Section 5.4) over pure ML-DSA. +pub(crate) fn mldsa_cmd< + A: SignatureAlgorithm, + PK: SignaturePublicKey + VerifyingKey + PreHashVerifyingKey, + SK: SignaturePrivateKey + SigningKey + PreHashSigningKey, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +>( + set: &ParameterSet, action: &MLDSAAction, ctxfile: &Option, skfile: &Option, pkfile: &Option, sigfile: &Option, + prehash: bool, output_hex: bool, ) { match action { MLDSAAction::Keygen => { - let (_pk, sk) = MLDSA44::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); + let (_pk, sk) = (set.keygen)(&mut HashDRBG_SHA512::new_from_os()).unwrap(); write_bytes_or_hex(&sk.encode(), output_hex); } MLDSAAction::KeygenFromSeed => { @@ -61,57 +143,23 @@ pub(crate) fn mldsa44_cmd( Ok(seed) => seed, Err(()) => { eprintln!( - "Error: input could not be parsed as a {} byte seed in either hex or bin", - MLDSA_SEED_LEN + "Error: input could not be parsed as a {MLDSA_SEED_LEN} byte seed in either hex or bin" ); exit(-1); } }; - - let (_pk, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); + let (_pk, sk) = (set.keygen_from_seed)(&seed).unwrap(); write_bytes_or_hex(&sk.encode(), output_hex); } MLDSAAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa44_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); + let sk = parse_sk(set, &read_from_file_or_stdin(skfile)); + write_bytes_or_hex(&(set.pk_of)(&sk).encode(), output_hex); } MLDSAAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa44_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa44_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLDSA44::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } + let sk = parse_sk(set, &read_from_file_or_stdin(skfile)); + let pk = parse_pk(set, &read_required_file(pkfile, "pkfile")); + match (set.keypair_consistency_check)(&pk, &sk) { + Ok(_) => crate::helpers::println_stdout("SUCCESS: pk and sk match."), Err(_) => { eprintln!("FAILURE: pk and sk do not match."); exit(-1); @@ -119,1081 +167,135 @@ pub(crate) fn mldsa44_cmd( } } MLDSAAction::Sign => { - // first, read the sk - let sk_bytes = if skfile.is_some() { - read_from_file(skfile.as_ref().unwrap()) - } else { - eprintln!("Error: no skfile provided."); - exit(-1); - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // and now sign, streaming the message from stdin - let sk = match parse_mldsa44_sk(&sk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - let mut signer = - sk.create_signer(&mut HashDRBG_SHA512::new_from_os(), Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); + let sk = parse_sk(set, &read_required_file(skfile, "skfile")); + let ctx = read_ctx(ctxfile); + let mut rng = HashDRBG_SHA512::new_from_os(); + if prehash { + sign_stream( + sk.create_ph_signer::(&mut rng, Some(&ctx)).unwrap(), + output_hex, + ); + } else { + sign_stream(sk.create_signer(&mut rng, Some(&ctx)).unwrap(), output_hex); } - - let sig = signer.do_final().unwrap(); - - write_bytes_or_hex(&sig, output_hex); } MLDSAAction::Verify => { - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa44_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // then read the sig - let sig = if sigfile.is_some() { - read_from_file(sigfile.as_ref().unwrap()) - } else { - eprintln!("Error: no sigfile provided."); - exit(-1); - }; - - // and now verify, streaming the message from stdin - let mut verifier = pk.create_verifier(Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - } - - let sig = verifier.do_verify_final(&sig); - - if sig.is_ok() { - crate::helpers::println_stdout("Signature is valid."); + let pk = parse_pk(set, &read_required_file(pkfile, "pkfile")); + let ctx = read_ctx(ctxfile); + let sig = read_required_file(sigfile, "sigfile"); + if prehash { + verify_stream(pk.create_ph_verifier::(Some(&ctx)).unwrap(), &sig); } else { - eprintln!("Signature is invalid."); - exit(-1); + verify_stream(pk.create_verifier(Some(&ctx)).unwrap(), &sig); } } } } -pub(crate) fn mldsa65_cmd( - action: &MLDSAAction, - ctxfile: &Option, - skfile: &Option, - pkfile: &Option, - sigfile: &Option, - output_hex: bool, -) { - match action { - MLDSAAction::Keygen => { - let (_pk, sk) = MLDSA65::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa65_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); - } - MLDSAAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa65_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa65_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLDSA65::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); - } - } - } - MLDSAAction::Sign => { - // first, read the sk - let sk_bytes = if skfile.is_some() { - read_from_file(skfile.as_ref().unwrap()) - } else { - eprintln!("Error: no skfile provided."); - exit(-1); - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // and now sign, streaming the message from stdin - let sk = match parse_mldsa65_sk(&sk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - let mut signer = - sk.create_signer(&mut HashDRBG_SHA512::new_from_os(), Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - } - - let sig = signer.do_final().unwrap(); - - write_bytes_or_hex(&sig, output_hex); - } - MLDSAAction::Verify => { - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa65_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // then read the sig - let sig = if sigfile.is_some() { - read_from_file(sigfile.as_ref().unwrap()) - } else { - eprintln!("Error: no sigfile provided."); - exit(-1); - }; - - // and now verify, streaming the message from stdin - let mut verifier = pk.create_verifier(Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - } - - let sig = verifier.do_verify_final(&sig); - - if sig.is_ok() { - crate::helpers::println_stdout("Signature is valid."); - } else { - eprintln!("Signature is invalid."); - exit(-1); - } - } - } -} -pub(crate) fn mldsa87_cmd( - action: &MLDSAAction, - ctxfile: &Option, - skfile: &Option, - pkfile: &Option, - sigfile: &Option, - output_hex: bool, -) { - match action { - MLDSAAction::Keygen => { - let (_pk, sk) = MLDSA87::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa87_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); - } - MLDSAAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa87_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa87_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLDSA87::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); - } - } - } - MLDSAAction::Sign => { - // first, read the sk - let sk_bytes = if skfile.is_some() { - read_from_file(skfile.as_ref().unwrap()) - } else { - eprintln!("Error: no skfile provided."); - exit(-1); - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // and now sign, streaming the message from stdin - let sk = match parse_mldsa87_sk(&sk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - let mut signer = - sk.create_signer(&mut HashDRBG_SHA512::new_from_os(), Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - } - - let sig = signer.do_final().unwrap(); - - write_bytes_or_hex(&sig, output_hex); - } - MLDSAAction::Verify => { - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa87_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // then read the sig - let sig = if sigfile.is_some() { - read_from_file(sigfile.as_ref().unwrap()) - } else { - eprintln!("Error: no sigfile provided."); - exit(-1); - }; - - // and now verify, streaming the message from stdin - let mut verifier = pk.create_verifier(Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - } - - let sig = verifier.do_verify_final(&sig); - - if sig.is_ok() { - crate::helpers::println_stdout("Signature is valid."); - } else { - eprintln!("Signature is invalid."); - exit(-1); - } - } - } -} - -pub(crate) fn hash_mldsa44_sha512_cmd( - action: &MLDSAAction, - ctxfile: &Option, - skfile: &Option, - pkfile: &Option, - sigfile: &Option, - output_hex: bool, -) { - match action { - MLDSAAction::Keygen => { - let (_pk, sk) = MLDSA44::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa44_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); - } - MLDSAAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa44_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa44_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLDSA44::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); - } - } - } - MLDSAAction::Sign => { - // first, read the sk - let sk_bytes = if skfile.is_some() { - read_from_file(skfile.as_ref().unwrap()) - } else { - eprintln!("Error: no skfile provided."); - exit(-1); - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // and now sign, streaming the message from stdin - let sk = match parse_mldsa44_sk(&sk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - let mut signer = sk - .create_ph_signer::(&mut HashDRBG_SHA512::new_from_os(), Some(&ctx)) - .unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - } - - let sig = signer.do_final().unwrap(); - - write_bytes_or_hex(&sig, output_hex); - } - MLDSAAction::Verify => { - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa44_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // then read the sig - let sig = if sigfile.is_some() { - read_from_file(sigfile.as_ref().unwrap()) - } else { - eprintln!("Error: no sigfile provided."); - exit(-1); - }; - - // and now verify, streaming the message from stdin - let mut verifier = pk.create_ph_verifier::(Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - } - - let sig = verifier.do_verify_final(&sig); - - if sig.is_ok() { - crate::helpers::println_stdout("Signature is valid."); - } else { - eprintln!("Signature is invalid."); - exit(-1); - } +/// Streams stdin into `signer` and writes the signature. +fn sign_stream(mut signer: impl SignatureSigner, output_hex: bool) { + let mut buf = [0u8; 1024]; + loop { + let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + if n == 0 { + break; } + signer.do_update(&buf[..n]); } + let sig = signer.do_final().unwrap(); + write_bytes_or_hex(&sig, output_hex); } -pub(crate) fn hash_mldsa65_sha512_cmd( - action: &MLDSAAction, - ctxfile: &Option, - skfile: &Option, - pkfile: &Option, - sigfile: &Option, - output_hex: bool, -) { - match action { - MLDSAAction::Keygen => { - let (_pk, sk) = MLDSA65::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa65_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); - } - MLDSAAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa65_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa65_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLDSA65::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); - } - } - } - MLDSAAction::Sign => { - // first, read the sk - let sk_bytes = if skfile.is_some() { - read_from_file(skfile.as_ref().unwrap()) - } else { - eprintln!("Error: no skfile provided."); - exit(-1); - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // and now sign, streaming the message from stdin - let sk = match parse_mldsa65_sk(&sk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - let mut signer = sk - .create_ph_signer::(&mut HashDRBG_SHA512::new_from_os(), Some(&ctx)) - .unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - } - - let sig = signer.do_final().unwrap(); - - write_bytes_or_hex(&sig, output_hex); - } - MLDSAAction::Verify => { - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa65_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // then read the sig - let sig = if sigfile.is_some() { - read_from_file(sigfile.as_ref().unwrap()) - } else { - eprintln!("Error: no sigfile provided."); - exit(-1); - }; - - // and now verify, streaming the message from stdin - let mut verifier = pk.create_ph_verifier::(Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - } - - let sig = verifier.do_verify_final(&sig); - - if sig.is_ok() { - crate::helpers::println_stdout("Signature is valid."); - } else { - eprintln!("Signature is invalid."); - exit(-1); - } +/// Streams stdin into `verifier` and reports whether `sig` verifies, through the exit status. +fn verify_stream(mut verifier: impl SignatureVerifier, sig: &[u8]) { + let mut buf = [0u8; 1024]; + loop { + let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + if n == 0 { + break; } + verifier.do_update(&buf[..n]); } -} -pub(crate) fn hash_mldsa87_sha512_cmd( - action: &MLDSAAction, - ctxfile: &Option, - skfile: &Option, - pkfile: &Option, - sigfile: &Option, - output_hex: bool, -) { - match action { - MLDSAAction::Keygen => { - let (_pk, sk) = MLDSA87::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa87_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); - } - MLDSAAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa87_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa87_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLDSA87::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); - } - } - } - MLDSAAction::Sign => { - // first, read the sk - let sk_bytes = if skfile.is_some() { - read_from_file(skfile.as_ref().unwrap()) - } else { - eprintln!("Error: no skfile provided."); - exit(-1); - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // and now sign, streaming the message from stdin - let sk = match parse_mldsa87_sk(&sk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - let mut signer = sk - .create_ph_signer::(&mut HashDRBG_SHA512::new_from_os(), Some(&ctx)) - .unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - } - - let sig = signer.do_final().unwrap(); - - write_bytes_or_hex(&sig, output_hex); - } - MLDSAAction::Verify => { - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa87_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // then read the sig - let sig = if sigfile.is_some() { - read_from_file(sigfile.as_ref().unwrap()) - } else { - eprintln!("Error: no sigfile provided."); - exit(-1); - }; - - // and now verify, streaming the message from stdin - let mut verifier = pk.create_ph_verifier::(Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - } - - let sig = verifier.do_verify_final(&sig); - - if sig.is_ok() { - crate::helpers::println_stdout("Signature is valid."); - } else { - eprintln!("Signature is invalid."); - exit(-1); - } - } + if verifier.do_verify_final(sig).is_ok() { + crate::helpers::println_stdout("Signature is valid."); + } else { + eprintln!("Signature is invalid."); + exit(-1); } } -fn parse_mldsa44_sk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLDSA44_SK_LEN { - let maybe_sk = hex::decode(&bytes[..2 * MLDSA44_SK_LEN]); - if maybe_sk.is_ok() { - // it was hex - let sk = MLDSA44PrivateKey::from_bytes(&maybe_sk.unwrap()); - if sk.is_ok() { - return Ok(sk.unwrap()); - } // else: keep trying things - } - } - - // try it as a binary full key - if bytes.len() == MLDSA44_SK_LEN { - let sk = MLDSA44PrivateKey::from_bytes(&bytes); - if sk.is_ok() { - return Ok(sk.unwrap()); - } - } // else: keep trying things - - // try it as a seed - let seed = parse_seed(bytes); - if seed.is_ok() { - let maybe_sk = MLDSA44::keygen_from_seed(&seed.unwrap()); - if maybe_sk.is_ok() { - let (_pk, sk) = maybe_sk.unwrap(); - return Ok(sk); - } // else: we're out of things to try - } - - Err("Error: couldn't parse the input as a valid ML-DSA-44 private key or seed.") +/// `--ctxfile`'s bytes, or the empty context (FIPS 204's default) without one. +fn read_ctx(ctxfile: &Option) -> Vec { + ctxfile.as_ref().map(|f| read_from_file(f)).unwrap_or_default() } -fn parse_mldsa44_pk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLDSA44_PK_LEN { - let maybe_pk = hex::decode(&bytes[..2 * MLDSA44_PK_LEN]); - if maybe_pk.is_ok() { - // it was hex - let pk = MLDSA44PublicKey::from_bytes(&maybe_pk.unwrap()); - if pk.is_ok() { - return Ok(pk.unwrap()); - } // else: keep trying things +/// `--skfile` and the like where the command cannot fall back to stdin. +fn read_required_file(file: &Option, flag: &str) -> Vec { + match file { + Some(file) => read_from_file(file), + None => { + eprintln!("Error: no {flag} provided."); + exit(-1); } } - - // try it as a binary full key - if bytes.len() == MLDSA44_PK_LEN { - let pk = MLDSA44PublicKey::from_bytes(&bytes); - if pk.is_ok() { - return Ok(pk.unwrap()); - } - } // else: we're out of things to try - - Err("Error: couldn't parse the input as a valid ML-DSA-44 public key.") } -fn parse_mldsa65_sk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLDSA65_SK_LEN { - let maybe_sk = hex::decode(&bytes[..2 * MLDSA65_SK_LEN]); - if maybe_sk.is_ok() { - // it was hex - let sk = MLDSA65PrivateKey::from_bytes(&maybe_sk.unwrap()); - if sk.is_ok() { - return Ok(sk.unwrap()); - } // else: keep trying things - } +/// A private key from its bytes: the hex of a full key, a full key, or a seed, tried in that +/// order (the hex case only arises when the file's hex-or-raw guess declined to decode it). +fn parse_sk< + A, + PK, + SK: SignaturePrivateKey, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +>( + set: &ParameterSet, + bytes: &[u8], +) -> SK { + if bytes.len() >= 2 * SK_LEN + && let Ok(decoded) = hex::decode(&bytes[..2 * SK_LEN]) + && let Ok(sk) = SK::from_bytes(&decoded) + { + return sk; } - - // try it as a binary full key - if bytes.len() == MLDSA65_SK_LEN { - let sk = MLDSA65PrivateKey::from_bytes(&bytes); - if sk.is_ok() { - return Ok(sk.unwrap()); - } - } // else: keep trying things - - // try it as a seed - let seed = parse_seed(bytes); - if seed.is_ok() { - let maybe_sk = MLDSA65::keygen_from_seed(&seed.unwrap()); - if maybe_sk.is_ok() { - let (_pk, sk) = maybe_sk.unwrap(); - return Ok(sk); - } // else: we're out of things to try + if bytes.len() == SK_LEN + && let Ok(sk) = SK::from_bytes(bytes) + { + return sk; } - - Err("Error: couldn't parse the input as a valid ML-DSA-65 private key or seed.") -} - -fn parse_mldsa65_pk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLDSA65_PK_LEN { - let maybe_pk = hex::decode(&bytes[..2 * MLDSA65_PK_LEN]); - if maybe_pk.is_ok() { - // it was hex - let pk = MLDSA65PublicKey::from_bytes(&maybe_pk.unwrap()); - if pk.is_ok() { - return Ok(pk.unwrap()); - } // else: keep trying things - } + if let Ok(seed) = parse_seed::(bytes) + && let Ok((_pk, sk)) = (set.keygen_from_seed)(&seed) + { + return sk; } - - // try it as a binary full key - if bytes.len() == MLDSA65_PK_LEN { - let pk = MLDSA65PublicKey::from_bytes(&bytes); - if pk.is_ok() { - return Ok(pk.unwrap()); - } - } // else: we're out of things to try - - Err("Error: couldn't parse the input as a valid ML-DSA-65 public key or seed.") + eprintln!("Error: couldn't parse the input as a valid {} private key or seed.", set.name); + exit(-1); } -fn parse_mldsa87_sk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLDSA87_SK_LEN { - let maybe_sk = hex::decode(&bytes[..2 * MLDSA87_SK_LEN]); - if maybe_sk.is_ok() { - // it was hex - let sk = MLDSA87PrivateKey::from_bytes(&maybe_sk.unwrap()); - if sk.is_ok() { - return Ok(sk.unwrap()); - } // else: keep trying things - } +/// A public key from its bytes: the hex of a key or the key itself. +fn parse_pk< + A, + PK: SignaturePublicKey, + SK, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +>( + set: &ParameterSet, + bytes: &[u8], +) -> PK { + if bytes.len() >= 2 * PK_LEN + && let Ok(decoded) = hex::decode(&bytes[..2 * PK_LEN]) + && let Ok(pk) = PK::from_bytes(&decoded) + { + return pk; } - - // try it as a binary full key - if bytes.len() == MLDSA87_SK_LEN { - let sk = MLDSA87PrivateKey::from_bytes(&bytes); - if sk.is_ok() { - return Ok(sk.unwrap()); - } - } // else: keep trying things - - // try it as a seed - let seed = parse_seed(bytes); - if seed.is_ok() { - let maybe_sk = MLDSA87::keygen_from_seed(&seed.unwrap()); - if maybe_sk.is_ok() { - let (_pk, sk) = maybe_sk.unwrap(); - return Ok(sk); - } // else: we're out of things to try + if bytes.len() == PK_LEN + && let Ok(pk) = PK::from_bytes(bytes) + { + return pk; } - - Err("Error: couldn't parse the input as a valid ML-DSA-87 private key.") -} - -fn parse_mldsa87_pk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLDSA87_PK_LEN { - let maybe_pk = hex::decode(&bytes[..2 * MLDSA87_PK_LEN]); - if maybe_pk.is_ok() { - // it was hex - let pk = MLDSA87PublicKey::from_bytes(&maybe_pk.unwrap()); - if pk.is_ok() { - return Ok(pk.unwrap()); - } // else: keep trying things - } - } - - // try it as a binary full key - if bytes.len() == MLDSA87_PK_LEN { - let pk = MLDSA87PublicKey::from_bytes(&bytes); - if pk.is_ok() { - return Ok(pk.unwrap()); - } - } // else: we're out of things to try - - Err("Error: couldn't parse the input as a valid ML-DSA-87 public key.") + eprintln!("Error: couldn't parse the input as a valid {} public key.", set.name); + exit(-1); } diff --git a/cli/src/mlkem_cmd.rs b/cli/src/mlkem_cmd.rs index e0e92c590..8fa406493 100644 --- a/cli/src/mlkem_cmd.rs +++ b/cli/src/mlkem_cmd.rs @@ -1,12 +1,17 @@ -//! Yup, this file is as absolutely atrocious mess of duplicate code that could be much improved -//! by using generics or macros. I just, haven't ... yet. +//! The `mlkem512`, `mlkem768` and `mlkem1024` subcommands: one body, [`mlkem_cmd`], over the +//! three parameter sets, which differ only in their key types, their lengths and the functions +//! that make and check a key pair. Those come in through a [`ParameterSet`], so a length or a +//! function can no longer be right for two sets and wrong for the third. use crate::helpers::{ parse_seed, read_from_file, read_from_file_or_stdin, write_bytes_or_hex, write_bytes_or_hex_to_file, }; -use bouncycastle::core::key_material::KeyMaterialTrait; -use bouncycastle::core::traits::{KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey}; +use bouncycastle::core::errors::KEMError; +use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait}; +use bouncycastle::core::traits::{ + KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, RNG, +}; use bouncycastle::hex; use bouncycastle::mlkem::{ MLKEM512, MLKEM512_CT_LEN, MLKEM512_PK_LEN, MLKEM512_SK_LEN, MLKEM512PrivateKey, @@ -18,11 +23,14 @@ use bouncycastle::rng::HashDRBG_SHA512; use clap::ValueEnum; use std::process::exit; +/// The ML-KEM seed is `d || z`, 64 bytes (FIPS 203 Algorithm 19). +const SEED_LEN: usize = 64; + #[derive(ValueEnum, Clone, Debug)] pub(crate) enum MLKEMAction { /// Generate and output a new private key Keygen, - /// Generate and output a private key from a seed read from stdin. + /// Generate and output a private key from a 64-byte seed (d || z) read from stdin. /// Accepts either binary or hex. KeygenFromSeed, /// Generate and output a new public key from a private key read from stdin. @@ -37,139 +45,95 @@ pub(crate) enum MLKEMAction { /// If no ctfile is provided then ct followed by ss are output to stdout in hex, separated by a newline. /// The shared secret is always output to stdout to prevent it /// staying in a file longer than it should and causing a vulnerability. - /// Accepts private key as full or seed, binary or hex. + /// Accepts the public key as binary or hex. Encaps, /// Perform a decapsulation with a private key and ciphertext and output the shared secret key to stdout either in /// binary or hex. /// The ciphertext can be read either from ctfile or stdin, in binary or hex. - /// Accepts the public key and signature as binary or hex. + /// Accepts the private key as full or seed, binary or hex. Decaps, } -pub(crate) fn mlkem512_cmd( - action: &MLKEMAction, - skfile: &Option, - pkfile: &Option, - ctfile: &Option, - output_hex: bool, -) { - match action { - MLKEMAction::Keygen => { - let (_pk, sk) = MLKEM512::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLKEMAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLKEM512::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLKEMAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mlkem512_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.pk().encode(), output_hex); - } - MLKEMAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mlkem512_sk(buf.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mlkem512_pk(pk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLKEM512::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); - } - } - } - MLKEMAction::Encaps => { - // first, read the pk - let pk_bytes = read_from_file_or_stdin(pkfile); - let pk = match parse_mlkem512_pk(pk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - let (ss, ct) = MLKEM512::encaps(&pk).unwrap(); - - if ctfile.is_some() { - // write ct to file and ss to stdout - write_bytes_or_hex_to_file(&ct, ctfile.as_ref().unwrap(), output_hex); - write_bytes_or_hex(ss.ref_to_bytes(), output_hex); - } else { - // write both to stdout in hex, separated by a newline. - write_bytes_or_hex(&ct, true); - crate::helpers::write_stdout(b"\n"); - write_bytes_or_hex(ss.ref_to_bytes(), true); - } - } - MLKEMAction::Decaps => { - // first, read the sk - let sk_bytes = read_from_file_or_stdin(skfile); - - let sk = match parse_mlkem512_sk(sk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - let ct_vec = read_from_file_or_stdin(ctfile); - if ct_vec.len() != MLKEM512_CT_LEN { - eprintln!("Error: ciphertexts is not the correct length."); - exit(-1); - } +/// A parameter set as the command sees it: its name for messages, and the operations the key +/// types and the `KEM*` traits do not carry on their own. The three sets below are the only +/// instances. +pub(crate) struct ParameterSet< + PK, + SK, + const PK_LEN: usize, + const SK_LEN: usize, + const CT_LEN: usize, + const SS_LEN: usize, +> { + name: &'static str, + keygen: fn(&mut dyn RNG) -> Result<(PK, SK), KEMError>, + keygen_from_seed: fn(&KeyMaterial) -> Result<(PK, SK), KEMError>, + keypair_consistency_check: fn(&PK, &SK) -> Result<(), KEMError>, + pk_of: fn(&SK) -> PK, + encaps: fn(&PK) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError>, + decaps: fn(&SK, &[u8]) -> Result, KEMError>, +} - let ct: [u8; MLKEM512_CT_LEN] = ct_vec.try_into().unwrap(); +pub(crate) const MLKEM512_SET: ParameterSet< + MLKEM512PublicKey, + MLKEM512PrivateKey, + MLKEM512_PK_LEN, + MLKEM512_SK_LEN, + MLKEM512_CT_LEN, + 32, +> = ParameterSet { + name: "ML-KEM-512", + keygen: MLKEM512::keygen, + keygen_from_seed: MLKEM512::keygen_from_seed, + keypair_consistency_check: MLKEM512::keypair_consistency_check, + pk_of: |sk| sk.pk().clone(), + encaps: MLKEM512::encaps, + decaps: MLKEM512::decaps, +}; - let ss = match MLKEM512::decaps(&sk, &ct) { - Ok(ss) => ss, - Err(estr) => { - eprintln!("{:?}", estr); - exit(-1); - } - }; +pub(crate) const MLKEM768_SET: ParameterSet< + MLKEM768PublicKey, + MLKEM768PrivateKey, + MLKEM768_PK_LEN, + MLKEM768_SK_LEN, + MLKEM768_CT_LEN, + 32, +> = ParameterSet { + name: "ML-KEM-768", + keygen: MLKEM768::keygen, + keygen_from_seed: MLKEM768::keygen_from_seed, + keypair_consistency_check: MLKEM768::keypair_consistency_check, + pk_of: |sk| sk.pk().clone(), + encaps: MLKEM768::encaps, + decaps: MLKEM768::decaps, +}; - write_bytes_or_hex(ss.ref_to_bytes(), output_hex); - } - } -} +pub(crate) const MLKEM1024_SET: ParameterSet< + MLKEM1024PublicKey, + MLKEM1024PrivateKey, + MLKEM1024_PK_LEN, + MLKEM1024_SK_LEN, + MLKEM1024_CT_LEN, + 32, +> = ParameterSet { + name: "ML-KEM-1024", + keygen: MLKEM1024::keygen, + keygen_from_seed: MLKEM1024::keygen_from_seed, + keypair_consistency_check: MLKEM1024::keypair_consistency_check, + pk_of: |sk| sk.pk().clone(), + encaps: MLKEM1024::encaps, + decaps: MLKEM1024::decaps, +}; -pub(crate) fn mlkem768_cmd( +pub(crate) fn mlkem_cmd< + PK: KEMPublicKey, + SK: KEMPrivateKey, + const PK_LEN: usize, + const SK_LEN: usize, + const CT_LEN: usize, + const SS_LEN: usize, +>( + set: &ParameterSet, action: &MLKEMAction, skfile: &Option, pkfile: &Option, @@ -178,180 +142,32 @@ pub(crate) fn mlkem768_cmd( ) { match action { MLKEMAction::Keygen => { - let (_pk, sk) = MLKEM768::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); + let (_pk, sk) = (set.keygen)(&mut HashDRBG_SHA512::new_from_os()).unwrap(); write_bytes_or_hex(&sk.encode(), output_hex); } MLKEMAction::KeygenFromSeed => { let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLKEM768::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLKEMAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mlkem768_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.pk().encode(), output_hex); - } - MLKEMAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mlkem768_sk(buf.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mlkem768_pk(pk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLKEM768::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); - } - } - } - MLKEMAction::Encaps => { - // first, read the pk - let pk_bytes = read_from_file_or_stdin(pkfile); - let pk = match parse_mlkem768_pk(pk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - let (ss, ct) = MLKEM768::encaps(&pk).unwrap(); - - if ctfile.is_some() { - // write ct to file and ss to stdout - write_bytes_or_hex_to_file(&ct, ctfile.as_ref().unwrap(), output_hex); - write_bytes_or_hex(ss.ref_to_bytes(), output_hex); - } else { - // write both to stdout in hex, separated by a newline. - write_bytes_or_hex(&ct, true); - crate::helpers::write_stdout(b"\n"); - write_bytes_or_hex(ss.ref_to_bytes(), true); - } - } - MLKEMAction::Decaps => { - // first, read the sk - let sk_bytes = read_from_file_or_stdin(skfile); - - let sk = match parse_mlkem768_sk(sk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); + let seed = match parse_seed::(&buf) { + Ok(seed) => seed, + Err(()) => { + eprintln!( + "Error: input could not be parsed as a {SEED_LEN} byte seed in either hex or bin" + ); exit(-1); } }; - - let ct_vec = read_from_file_or_stdin(ctfile); - if ct_vec.len() != MLKEM768_CT_LEN { - eprintln!("Error: ciphertexts is not the correct length."); - exit(-1); - } - - let ct: [u8; MLKEM768_CT_LEN] = ct_vec.try_into().unwrap(); - - let ss = match MLKEM768::decaps(&sk, &ct) { - Ok(ss) => ss, - Err(estr) => { - eprintln!("{:?}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(ss.ref_to_bytes(), output_hex); - } - } -} - -pub(crate) fn mlkem1024_cmd( - action: &MLKEMAction, - skfile: &Option, - pkfile: &Option, - ctfile: &Option, - output_hex: bool, -) { - match action { - MLKEMAction::Keygen => { - let (_pk, sk) = MLKEM1024::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLKEMAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLKEM1024::keygen_from_seed(&seed).unwrap(); + let (_pk, sk) = (set.keygen_from_seed)(&seed).unwrap(); write_bytes_or_hex(&sk.encode(), output_hex); } MLKEMAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mlkem1024_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.pk().encode(), output_hex); + let sk = parse_sk(set, &read_from_file_or_stdin(skfile)); + write_bytes_or_hex(&(set.pk_of)(&sk).encode(), output_hex); } MLKEMAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mlkem1024_sk(buf.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mlkem1024_pk(pk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLKEM1024::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } + let sk = parse_sk(set, &read_from_file_or_stdin(skfile)); + let pk = parse_pk(set, &read_required_file(pkfile, "pkfile")); + match (set.keypair_consistency_check)(&pk, &sk) { + Ok(_) => crate::helpers::println_stdout("SUCCESS: pk and sk match."), Err(_) => { eprintln!("FAILURE: pk and sk do not match."); exit(-1); @@ -359,21 +175,11 @@ pub(crate) fn mlkem1024_cmd( } } MLKEMAction::Encaps => { - // first, read the pk - let pk_bytes = read_from_file_or_stdin(pkfile); - let pk = match parse_mlkem1024_pk(pk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - let (ss, ct) = MLKEM1024::encaps(&pk).unwrap(); - - if ctfile.is_some() { + let pk = parse_pk(set, &read_from_file_or_stdin(pkfile)); + let (ss, ct) = (set.encaps)(&pk).unwrap(); + if let Some(ctfile) = ctfile { // write ct to file and ss to stdout - write_bytes_or_hex_to_file(&ct, ctfile.as_ref().unwrap(), output_hex); + write_bytes_or_hex_to_file(&ct, ctfile, output_hex); write_bytes_or_hex(ss.ref_to_bytes(), output_hex); } else { // write both to stdout in hex, separated by a newline. @@ -383,220 +189,88 @@ pub(crate) fn mlkem1024_cmd( } } MLKEMAction::Decaps => { - // first, read the sk - let sk_bytes = read_from_file_or_stdin(skfile); - - let sk = match parse_mlkem1024_sk(sk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - let ct_vec = read_from_file_or_stdin(ctfile); - if ct_vec.len() != MLKEM1024_CT_LEN { + let sk = parse_sk(set, &read_from_file_or_stdin(skfile)); + let ct = read_from_file_or_stdin(ctfile); + if ct.len() != CT_LEN { eprintln!("Error: ciphertexts is not the correct length."); exit(-1); } - - let ct: [u8; MLKEM1024_CT_LEN] = ct_vec.try_into().unwrap(); - - let ss = match MLKEM1024::decaps(&sk, &ct) { - Ok(ss) => ss, - Err(estr) => { - eprintln!("{:?}", estr); - exit(-1); - } - }; - + let ss = (set.decaps)(&sk, &ct).unwrap_or_else(|e| { + eprintln!("{:?}", e); + exit(-1); + }); write_bytes_or_hex(ss.ref_to_bytes(), output_hex); } } } -fn parse_mlkem512_sk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLKEM512_SK_LEN { - let maybe_sk = hex::decode(&bytes[..2 * MLKEM512_SK_LEN]); - if maybe_sk.is_ok() { - // it was hex - let sk = MLKEM512PrivateKey::from_bytes(&maybe_sk.unwrap()); - if sk.is_ok() { - return Ok(sk.unwrap()); - } // else: keep trying things +/// `--pkfile` and the like where the command cannot fall back to stdin. +fn read_required_file(file: &Option, flag: &str) -> Vec { + match file { + Some(file) => read_from_file(file), + None => { + eprintln!("Error: no {flag} provided."); + exit(-1); } } - - // try it as a binary full key - if bytes.len() == MLKEM512_SK_LEN { - let sk = MLKEM512PrivateKey::from_bytes(&bytes); - if sk.is_ok() { - return Ok(sk.unwrap()); - } - } // else: keep trying things - - // try it as a seed - let seed = parse_seed(bytes); - if seed.is_ok() { - let maybe_sk = MLKEM512::keygen_from_seed(&seed.unwrap()); - if maybe_sk.is_ok() { - let (_pk, sk) = maybe_sk.unwrap(); - return Ok(sk); - } // else: we're out of things to try - } - - Err("Error: couldn't parse the input as a valid ML-KEM-512 private key or seed.") } -fn parse_mlkem512_pk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLKEM512_PK_LEN { - let maybe_pk = hex::decode(&bytes[..2 * MLKEM512_PK_LEN]); - if maybe_pk.is_ok() { - // it was hex - let pk = MLKEM512PublicKey::from_bytes(&maybe_pk.unwrap()); - if pk.is_ok() { - return Ok(pk.unwrap()); - } // else: keep trying things - } +/// A private key from its bytes: the hex of a full key, a full key, or a seed, tried in that +/// order (the hex case only arises when the file's hex-or-raw guess declined to decode it). +fn parse_sk< + PK: KEMPublicKey, + SK: KEMPrivateKey, + const PK_LEN: usize, + const SK_LEN: usize, + const CT_LEN: usize, + const SS_LEN: usize, +>( + set: &ParameterSet, + bytes: &[u8], +) -> SK { + if bytes.len() >= 2 * SK_LEN + && let Ok(decoded) = hex::decode(&bytes[..2 * SK_LEN]) + && let Ok(sk) = SK::from_bytes(&decoded) + { + return sk; } - - // try it as a binary full key - if bytes.len() == MLKEM512_PK_LEN { - let pk = MLKEM512PublicKey::from_bytes(&bytes); - if pk.is_ok() { - return Ok(pk.unwrap()); - } - } // else: we're out of things to try - - Err("Error: couldn't parse the input as a valid ML-KEM-768 public key.") -} - -fn parse_mlkem768_sk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLKEM768_SK_LEN { - let maybe_sk = hex::decode(&bytes[..2 * MLKEM768_SK_LEN]); - if maybe_sk.is_ok() { - // it was hex - let sk = MLKEM768PrivateKey::from_bytes(&maybe_sk.unwrap()); - if sk.is_ok() { - return Ok(sk.unwrap()); - } // else: keep trying things - } + if bytes.len() == SK_LEN + && let Ok(sk) = SK::from_bytes(bytes) + { + return sk; } - - // try it as a binary full key - if bytes.len() == MLKEM768_SK_LEN { - let sk = MLKEM768PrivateKey::from_bytes(&bytes); - if sk.is_ok() { - return Ok(sk.unwrap()); - } - } // else: keep trying things - - // try it as a seed - let seed = parse_seed(bytes); - if seed.is_ok() { - let maybe_sk = MLKEM768::keygen_from_seed(&seed.unwrap()); - if maybe_sk.is_ok() { - let (_pk, sk) = maybe_sk.unwrap(); - return Ok(sk); - } // else: we're out of things to try - } - - Err("Error: couldn't parse the input as a valid ML-KEM-768 private key or seed.") -} - -fn parse_mlkem768_pk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLKEM768_PK_LEN { - let maybe_pk = hex::decode(&bytes[..2 * MLKEM768_PK_LEN]); - if maybe_pk.is_ok() { - // it was hex - let pk = MLKEM768PublicKey::from_bytes(&maybe_pk.unwrap()); - if pk.is_ok() { - return Ok(pk.unwrap()); - } // else: keep trying things - } + if let Ok(seed) = parse_seed::(bytes) + && let Ok((_pk, sk)) = (set.keygen_from_seed)(&seed) + { + return sk; } - - // try it as a binary full key - if bytes.len() == MLKEM768_PK_LEN { - let pk = MLKEM768PublicKey::from_bytes(&bytes); - if pk.is_ok() { - return Ok(pk.unwrap()); - } - } // else: we're out of things to try - - Err("Error: couldn't parse the input as a valid ML-KEM-768 public key.") + eprintln!("Error: couldn't parse the input as a valid {} private key or seed.", set.name); + exit(-1); } -fn parse_mlkem1024_sk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLKEM1024_SK_LEN { - let maybe_sk = hex::decode(&bytes[..2 * MLKEM1024_SK_LEN]); - if maybe_sk.is_ok() { - // it was hex - let sk = MLKEM1024PrivateKey::from_bytes(&maybe_sk.unwrap()); - if sk.is_ok() { - return Ok(sk.unwrap()); - } // else: keep trying things - } +/// A public key from its bytes: the hex of a key or the key itself. +fn parse_pk< + PK: KEMPublicKey, + SK: KEMPrivateKey, + const PK_LEN: usize, + const SK_LEN: usize, + const CT_LEN: usize, + const SS_LEN: usize, +>( + set: &ParameterSet, + bytes: &[u8], +) -> PK { + if bytes.len() >= 2 * PK_LEN + && let Ok(decoded) = hex::decode(&bytes[..2 * PK_LEN]) + && let Ok(pk) = PK::from_bytes(&decoded) + { + return pk; } - - // try it as a binary full key - if bytes.len() == MLKEM1024_SK_LEN { - let sk = MLKEM1024PrivateKey::from_bytes(&bytes); - if sk.is_ok() { - return Ok(sk.unwrap()); - } - } // else: keep trying things - - // try it as a seed - let seed = parse_seed(bytes); - if seed.is_ok() { - let maybe_sk = MLKEM1024::keygen_from_seed(&seed.unwrap()); - if maybe_sk.is_ok() { - let (_pk, sk) = maybe_sk.unwrap(); - return Ok(sk); - } // else: we're out of things to try - } - - Err("Error: couldn't parse the input as a valid ML-KEM-1024 private key or seed.") -} - -fn parse_mlkem1024_pk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLKEM1024_PK_LEN { - let maybe_pk = hex::decode(&bytes[..2 * MLKEM1024_PK_LEN]); - if maybe_pk.is_ok() { - // it was hex - let pk = MLKEM1024PublicKey::from_bytes(&maybe_pk.unwrap()); - if pk.is_ok() { - return Ok(pk.unwrap()); - } // else: keep trying things - } + if bytes.len() == PK_LEN + && let Ok(pk) = PK::from_bytes(bytes) + { + return pk; } - - // try it as a binary full key - if bytes.len() == MLKEM1024_PK_LEN { - let pk = MLKEM1024PublicKey::from_bytes(&bytes); - if pk.is_ok() { - return Ok(pk.unwrap()); - } - } // else: we're out of things to try - - Err("Error: couldn't parse the input as a valid ML-KEM-1024 public key.") + eprintln!("Error: couldn't parse the input as a valid {} public key.", set.name); + exit(-1); } diff --git a/cli/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index da4bd0218..8ab08d3aa 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -11,21 +11,35 @@ use std::process::exit; use crate::helpers::{stream_hash, stream_xof}; -pub(crate) fn sha3_cmd(bit_len: usize, output_hex: bool) { - match bit_len { - 224 => stream_hash(SHA3_224::new(), output_hex), - 256 => stream_hash(SHA3_256::new(), output_hex), - 384 => stream_hash(SHA3_384::new(), output_hex), - 512 => stream_hash(SHA3_512::new(), output_hex), - _ => panic!("Unsupported algorithm: SHA3-{}", bit_len), +#[allow(non_camel_case_types)] +pub(crate) enum SHA3Variant { + SHA3_224, + SHA3_256, + SHA3_384, + SHA3_512, +} + +/// The security strength that selects a SHAKE-based function's instance: SHAKE128 or SHAKE256, +/// and with them the 128- and 256-bit KMAC, TupleHash and ParallelHash. +#[derive(Clone, Copy)] +pub(crate) enum Strength { + Bits128, + Bits256, +} + +pub(crate) fn sha3_cmd(variant: SHA3Variant, output_hex: bool) { + match variant { + SHA3Variant::SHA3_224 => stream_hash(SHA3_224::new(), output_hex), + SHA3Variant::SHA3_256 => stream_hash(SHA3_256::new(), output_hex), + SHA3Variant::SHA3_384 => stream_hash(SHA3_384::new(), output_hex), + SHA3Variant::SHA3_512 => stream_hash(SHA3_512::new(), output_hex), } } -pub(crate) fn shake_cmd(bit_len: usize, output_len: usize, output_hex: bool) { - match bit_len { - 128 => stream_xof(SHAKE128::new(), output_len, output_hex), - 256 => stream_xof(SHAKE256::new(), output_len, output_hex), - _ => panic!("Unsupported algorithm: SHAKE-{}", bit_len), +pub(crate) fn shake_cmd(strength: Strength, output_len: usize, output_hex: bool) { + match strength { + Strength::Bits128 => stream_xof(SHAKE128::new(), output_len, output_hex), + Strength::Bits256 => stream_xof(SHAKE256::new(), output_len, output_hex), } } @@ -35,7 +49,7 @@ pub(crate) fn shake_cmd(bit_len: usize, output_len: usize, output_hex: bool) { /// hashed as a single-element tuple -- which is not the same as hashing those bytes with SHAKE, /// because the element is length-prefixed. pub(crate) fn tuplehash_cmd( - bit_len: usize, + strength: Strength, output_len: usize, elements: &[String], customization: &Option, @@ -59,20 +73,19 @@ pub(crate) fn tuplehash_cmd( }; let refs: Vec<&[u8]> = tuple.iter().map(|v| v.as_slice()).collect(); - let out = match bit_len { - 128 => TupleHash128::new(s, output_len) + let out = match strength { + Strength::Bits128 => TupleHash128::new(s, output_len) .unwrap_or_else(|_| { eprintln!("Error: --output-len is larger than SP 800-185 allows here (Sec 7.2)."); exit(-1); }) .hash_tuple(&refs), - 256 => TupleHash256::new(s, output_len) + Strength::Bits256 => TupleHash256::new(s, output_len) .unwrap_or_else(|_| { eprintln!("Error: --output-len is larger than SP 800-185 allows here (Sec 7.2)."); exit(-1); }) .hash_tuple(&refs), - _ => panic!("Unsupported algorithm: TupleHash-{bit_len}"), }; write_out(&out, output_hex); } @@ -82,7 +95,7 @@ pub(crate) fn tuplehash_cmd( /// The block size is part of the function, not a tuning knob -- the same input under a different /// block size gives an unrelated hash, so it must match on both sides. pub(crate) fn parallelhash_cmd( - bit_len: usize, + strength: Strength, output_len: usize, block_size: usize, customization: &Option, @@ -93,8 +106,8 @@ pub(crate) fn parallelhash_cmd( exit(-1); }; let s = customization.as_deref().unwrap_or("").as_bytes(); - match bit_len { - 128 => { + match strength { + Strength::Bits128 => { let mut p = ParallelHash128::new(block_size, s, output_len).unwrap_or_else(|_| { eprintln!("Error: --output-len is larger than SP 800-185 allows here (Sec 7.2)."); exit(-1); @@ -102,7 +115,7 @@ pub(crate) fn parallelhash_cmd( stream_stdin(|chunk| p.do_update(chunk)); write_out(&p.do_final(), output_hex); } - 256 => { + Strength::Bits256 => { let mut p = ParallelHash256::new(block_size, s, output_len).unwrap_or_else(|_| { eprintln!("Error: --output-len is larger than SP 800-185 allows here (Sec 7.2)."); exit(-1); @@ -110,7 +123,6 @@ pub(crate) fn parallelhash_cmd( stream_stdin(|chunk| p.do_update(chunk)); write_out(&p.do_final(), output_hex); } - _ => panic!("Unsupported algorithm: ParallelHash-{bit_len}"), } } From 36524babc79b61e8343c1eb68589330a8b936f32 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 10:23:47 +1100 Subject: [PATCH 40/57] cli: the CLI review's hardening and consistency items. One input rule for every subcommand: inline values are hex, and files and the stdin a command decodes are raw bytes, or hex text under the new global --hex-input, with nothing guessed from a file's contents (L11, L12, L13); keys, seeds, private keys, salts and IKM are read into a zeroized KeyMaterial buffer rather than a Vec (L20); a stdin read error, a missing or non-hex key, salt, IKM or verify value, a bad seed, a context over 255 bytes and an unwritable --ctfile are reported with exit 255 rather than a panic (L5 to L9); XOF output is squeezed to stdout in 1 KiB pieces, and the fixed-length KMAC, TupleHash and ParallelHash outputs are capped at 256 MiB with a pointer to the XOF forms (L10); MAC keys take any length up to 16 KiB, so RFC 4231's long-key cases run (L14); every failure line starts with "Error:", MAC verification, the signature check and the key-pair check included (L15); hkdf -x ends in a newline (L16); the SHA3-384/512 and ML-KEM action help, the HMAC verify text and the KMAC history warning (L18); #![forbid(unsafe_code)] and a clippy-clean package (L19). Flags: kmac* -k is the key file as for hmac-*, ascon-aead128 --ad is --aad with an --aad-file, ascon-cxof128 --customization is the string and --customization-hex the bytes, and the SP 800-185 commands gain --customization-hex. The shell tests follow the flag and input-rule changes. Assisted-by: Claude:claude-fable-5-1 --- cli/src/aes_ccm_cmd.rs | 46 +-- cli/src/aes_gcm_cmd.rs | 3 + cli/src/ascon_cmd.rs | 152 ++++----- cli/src/encoders_cmd.rs | 4 +- cli/src/helpers/aead_cipher_helpers.rs | 30 +- cli/src/helpers/block_mode_helpers.rs | 54 ++-- cli/src/helpers/mod.rs | 425 ++++++++++++++++++------- cli/src/hkdf_cmd.rs | 121 +++---- cli/src/mac_cmd.rs | 135 ++++---- cli/src/main.rs | 364 +++++++++++++-------- cli/src/mldsa_cmd.rs | 150 ++++----- cli/src/mlkem_cmd.rs | 86 ++--- cli/src/rng_cmd.rs | 6 +- cli/src/sha2_cmd.rs | 33 +- cli/src/sha3_cmd.rs | 135 ++++---- cli/src/sm3_cmd.rs | 20 +- cli/tests/test_aes_cbc.sh | 7 +- cli/tests/test_aes_cfb.sh | 7 +- cli/tests/test_aes_cfb8.sh | 7 +- cli/tests/test_aes_ctr.sh | 7 +- cli/tests/test_aes_ecb.sh | 12 +- cli/tests/test_ascon.sh | 12 +- cli/tests/test_kmac.sh | 7 +- 23 files changed, 1020 insertions(+), 803 deletions(-) diff --git a/cli/src/aes_ccm_cmd.rs b/cli/src/aes_ccm_cmd.rs index d2592dc08..cc2af0bcc 100644 --- a/cli/src/aes_ccm_cmd.rs +++ b/cli/src/aes_ccm_cmd.rs @@ -45,7 +45,6 @@ //! //! The output layout is Sec 6.1 step 8's own: `ciphertext || tag`. -use std::io::{self, Read}; use std::process::exit; use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; @@ -55,7 +54,6 @@ use bouncycastle::core::key_material::KeyMaterial; use bouncycastle::core::symmetric_key::{ AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, FromKeyMaterial, }; -use bouncycastle::hex; use crate::helpers; use crate::helpers::block_mode_helpers::{CipherDirection, load_key}; @@ -89,6 +87,7 @@ impl CcmKeys<32> for Aes256 { } /// AES-128 CCM. See the module docs and the subcommand help. +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aes128_ccm_cmd( action: &CipherDirection, key: &Option, @@ -113,6 +112,7 @@ pub(crate) fn aes128_ccm_cmd( } /// AES-192 CCM. See [`aes128_ccm_cmd`]. +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aes192_ccm_cmd( action: &CipherDirection, key: &Option, @@ -137,6 +137,7 @@ pub(crate) fn aes192_ccm_cmd( } /// AES-256 CCM. See [`aes128_ccm_cmd`]. +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aes256_ccm_cmd( action: &CipherDirection, key: &Option, @@ -165,10 +166,10 @@ pub(crate) fn aes256_ccm_cmd( /// Unlike the key there is no entropy question here: Sec 5.3 asks for uniqueness, not randomness, /// so an all-zero nonce is a perfectly valid *first* nonce and only a repeat is a problem. /// -/// `--nonce-file` reads raw bytes ([`helpers::read_from_file_raw`]), not the hex-or-raw guess -/// [`helpers::read_from_file`] uses for keys: a repeated nonce under one key is fatal for CCM (see -/// the module docs), so two distinct binary nonce files that happen to look like hex text of the -/// same value must not silently collapse to the same nonce. +/// `--nonce-file` reads raw bytes (hex only under the global `--hex-input`), never a guess from +/// the contents: a repeated nonce under one key is fatal for CCM (see the module docs), so two +/// distinct binary nonce files that happen to look like hex text of the same value must not +/// silently collapse to the same nonce. /// /// For the same reason a trailing newline is **not** stripped: a 13-byte file ending in `0x0a` and /// the 12-byte file without it are two different nonces, and silently treating them as one would @@ -178,8 +179,8 @@ pub(crate) fn aes256_ccm_cmd( /// is not a silent one; the bytes are still used exactly as they are. fn load_nonce(nonce: &Option, nonce_file: &Option) -> Vec { let bytes = if let Some(file) = nonce_file { - let bytes = helpers::read_from_file_raw(file); - if bytes.last() == Some(&b'\n') { + let bytes = helpers::read_input_file(file); + if !helpers::hex_input() && bytes.last() == Some(&b'\n') { eprintln!( "Warning: nonce file '{file}' ends with a newline byte (0x0a), which is used as \ part of the nonce." @@ -191,10 +192,7 @@ fn load_nonce(nonce: &Option, nonce_file: &Option) -> Vec { } bytes } else if let Some(v) = nonce { - hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: nonce is not valid hex."); - exit(-1) - }) + helpers::decode_hex_arg(v, "nonce") } else { eprintln!("Error: --nonce or --nonce-file must be supplied. CCM has no generated nonce;"); eprintln!(" see the subcommand help for why, and for the uniqueness requirement."); @@ -215,31 +213,20 @@ fn load_nonce(nonce: &Option, nonce_file: &Option) -> Vec { /// Loads the AAD from `--aad-file` (raw bytes, never hex-decoded) or `--aad` (hex); the file wins /// if both are given, as for `aes*-gcm`. Empty if neither is given. fn load_aad(aad: &Option, aad_file: &Option) -> Vec { - if let Some(path) = aad_file { - helpers::read_from_file_raw(path) - } else if let Some(v) = aad { - hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: associated data is not valid hex. Use --aad-file for raw bytes."); - exit(-1) - }) - } else { - Vec::new() + match (aad_file, aad) { + (Some(path), _) => helpers::read_input_file(path), + (None, Some(v)) => helpers::decode_hex_arg(v, "aad"), + (None, None) => Vec::new(), } } -/// Reads all of stdin. See the module docs on why this is not a streaming command. -fn read_all_stdin() -> Vec { - let mut input = Vec::new(); - io::stdin().read_to_end(&mut input).expect("Failed to read from stdin"); - input -} - /// Turns the runtime nonce and tag lengths into the mode's const generic parameters. /// /// `NONCE_LEN` and `TAG_LEN` are const parameters of the key type -- that is what makes A.1's /// length conditions compile-time checks rather than runtime ones -- so a command-line value has to /// be matched into one of the permitted instantiations. The two nested matches are the price of /// that, and they are exhaustive over A.1's sets: 7 nonce lengths x 7 tag lengths. +#[allow(clippy::too_many_arguments)] // the three key lengths' inputs, already parsed fn run, const KEY_LEN: usize>( action: &CipherDirection, key: &KeyMaterial, @@ -262,7 +249,8 @@ fn run, const KEY_LEN: usize>( let nonce_bytes = load_nonce(nonce, nonce_file); let aad = load_aad(aad, aad_file); - let input = read_all_stdin(); + // See the module docs on why this is not a streaming command. + let input = helpers::read_stdin_all(); let encrypt = matches!(action, CipherDirection::Encrypt); macro_rules! with_tag_len { diff --git a/cli/src/aes_gcm_cmd.rs b/cli/src/aes_gcm_cmd.rs index 5575c8d6d..0f6c7845c 100644 --- a/cli/src/aes_gcm_cmd.rs +++ b/cli/src/aes_gcm_cmd.rs @@ -19,6 +19,7 @@ use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; use bouncycastle::cipher::modes::GCM; use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey}; +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aes128_gcm_cmd( action: &CipherDirection, key: &Option, @@ -38,6 +39,7 @@ pub(crate) fn aes128_gcm_cmd( ); } +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aes192_gcm_cmd( action: &CipherDirection, key: &Option, @@ -57,6 +59,7 @@ pub(crate) fn aes192_gcm_cmd( ); } +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aes256_gcm_cmd( action: &CipherDirection, key: &Option, diff --git a/cli/src/ascon_cmd.rs b/cli/src/ascon_cmd.rs index a51ba01b5..e035d3511 100644 --- a/cli/src/ascon_cmd.rs +++ b/cli/src/ascon_cmd.rs @@ -1,3 +1,6 @@ +//! The Ascon subcommands (NIST SP 800-232): `ascon-hash256`, `ascon-xof128`, `ascon-cxof128` +//! and `ascon-aead128`. + use std::io::{self, Read}; use std::process::exit; @@ -13,54 +16,25 @@ use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, Fr use bouncycastle::core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; -use bouncycastle::hex; use crate::helpers; - -/// Load a hex string or a binary/hex file into bytes; exits with an error if neither is supplied. -fn load_bytes(value: &Option, value_file: &Option, label: &str) -> Vec { - if let Some(file) = value_file { - helpers::read_from_file(file) - } else if let Some(v) = value { - hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: {label} is not valid hex."); - exit(-1) - }) - } else { - eprintln!("Error: {label} must be supplied."); - exit(-1) - } -} - -fn load_optional_bytes( - value: &Option, - value_file: &Option, - label: &str, -) -> Option> { - if let Some(file) = value_file { - Some(helpers::read_from_file(file)) - } else { - value.as_ref().map(|v| { - hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: {label} is not valid hex."); - exit(-1) - }) - }) +use crate::helpers::{CHUNK_LEN, decode_hex_arg, load_secret, read_input_file, read_stdin_chunk}; + +/// Build the AEAD key from `--key-file` or `--key`, warning (and forcing usable metadata on the +/// material) only if the key turns out to be low-entropy (e.g. all-zero), the same way +/// `helpers::parse_seed` does. +fn load_aead_key(key: &Option, key_file: &Option) -> AsconAEAD128Key { + let secret = load_secret(key, key_file, "key"); + if secret.key_len() != 16 { + eprintln!("Error: the key must be exactly 16 bytes, got {}.", secret.key_len()); + exit(-1); } -} - -fn require_16(bytes: Vec, label: &str) -> [u8; 16] { - bytes.try_into().unwrap_or_else(|_: Vec| { - eprintln!("Error: {label} must be exactly 16 bytes."); - exit(-1) - }) -} - -/// Build the AEAD key, warning (and forcing usable metadata on the material) only if the key -/// turns out to be low-entropy (e.g. all-zero), the same way `helpers::parse_seed` does. -fn load_aead_key(key_bytes: &[u8; 16]) -> AsconAEAD128Key { let mut key = - KeyMaterial::<16>::from_bytes_as_type(key_bytes, KeyType::SymmetricCipherKey).unwrap(); + KeyMaterial::<16>::from_bytes_as_type(secret.ref_to_bytes(), KeyType::SymmetricCipherKey) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't load the key: {e:?}"); + exit(-1); + }); if key.key_type() == KeyType::Zeroized || key.security_strength() < SecurityStrength::_128bit { eprintln!( "Warning: low entropy key provided. We'll still process it, but it may be insecure." @@ -69,7 +43,10 @@ fn load_aead_key(key_bytes: &[u8; 16]) -> AsconAEAD128Key { k.set_key_type(KeyType::SymmetricCipherKey)?; k.set_security_strength(SecurityStrength::_128bit) }) - .unwrap(); + .unwrap_or_else(|e| { + eprintln!("Error: couldn't tag the key: {e:?}"); + exit(-1); + }); } AsconAEAD128Key::from_key_material(&key).unwrap_or_else(|e| { eprintln!("Error: couldn't load the key: {e:?}"); @@ -77,6 +54,19 @@ fn load_aead_key(key_bytes: &[u8; 16]) -> AsconAEAD128Key { }) } +/// The 16-byte nonce from `--nonce-file` or `--nonce` (hex), or none. +fn load_nonce(nonce: &Option, nonce_file: &Option) -> Option<[u8; 16]> { + let bytes = match (nonce_file, nonce) { + (Some(file), _) => read_input_file(file), + (None, Some(v)) => decode_hex_arg(v, "nonce"), + (None, None) => return None, + }; + Some(bytes.try_into().unwrap_or_else(|bytes: Vec| { + eprintln!("Error: the nonce must be exactly 16 bytes, got {}.", bytes.len()); + exit(-1) + })) +} + /// Ascon-Hash256 of stdin. Streaming update; 256-bit digest. pub(crate) fn hash256_cmd(output_hex: bool) { helpers::stream_hash(AsconHash256::new(), output_hex); @@ -87,17 +77,10 @@ pub(crate) fn xof128_cmd(output_len: usize, output_hex: bool) { helpers::stream_xof(AsconXof128::new(), output_len, output_hex); } -/// Ascon-CXOF128 of stdin with a hex customization string, producing `output_len` bytes. -pub(crate) fn cxof128_cmd(customization: &Option, output_len: usize, output_hex: bool) { - let z = match customization { - Some(v) => hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: customization is not valid hex."); - exit(-1) - }), - None => Vec::new(), - }; - let x = AsconCXof128::with_customization(&z).unwrap_or_else(|_| { - eprintln!("Error: customization string exceeds 256 bytes."); +/// Ascon-CXOF128 of stdin with a customization string, producing `output_len` bytes. +pub(crate) fn cxof128_cmd(customization: &[u8], output_len: usize, output_hex: bool) { + let x = AsconCXof128::with_customization(customization).unwrap_or_else(|_| { + eprintln!("Error: the customization string exceeds 256 bytes."); exit(-1) }); helpers::stream_xof(x, output_len, output_hex); @@ -117,31 +100,30 @@ pub(crate) fn cxof128_cmd(customization: &Option, output_len: usize, out /// ciphertext eagerly, before the tag is known; note that in the decryption direction, plaintext /// is likewise emitted before the tag has been checked, so it should not be treated as /// authentic until this command exits with status 0 (see the crate's "Security Considerations"). +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aead128_cmd( key: &Option, key_file: &Option, nonce: &Option, nonce_file: &Option, - ad: &Option, + aad: &Option, + aad_file: &Option, decrypt: bool, output_hex: bool, ) { - let key = load_aead_key(&require_16(load_bytes(key, key_file, "key"), "key")); - let nonce = - load_optional_bytes(nonce, nonce_file, "nonce").map(|bytes| require_16(bytes, "nonce")); - let ad_bytes = match ad { - Some(v) => hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: associated data is not valid hex."); - exit(-1) - }), - None => Vec::new(), + let key = load_aead_key(key, key_file); + let nonce = load_nonce(nonce, nonce_file); + let aad_bytes = match (aad_file, aad) { + (Some(file), _) => read_input_file(file), + (None, Some(v)) => decode_hex_arg(v, "aad"), + (None, None) => Vec::new(), }; - let ad_opt = if ad_bytes.is_empty() { None } else { Some(ad_bytes.as_slice()) }; + let aad_opt = if aad_bytes.is_empty() { None } else { Some(aad_bytes.as_slice()) }; if decrypt { - aead128_decrypt_stream(&key, nonce.as_ref(), ad_opt, output_hex); + aead128_decrypt_stream(&key, nonce.as_ref(), aad_opt, output_hex); } else { - aead128_encrypt_stream(&key, nonce.as_ref(), ad_opt, output_hex); + aead128_encrypt_stream(&key, nonce.as_ref(), aad_opt, output_hex); } } @@ -152,7 +134,7 @@ pub(crate) fn aead128_cmd( fn aead128_encrypt_stream( key: &AsconAEAD128Key, nonce: Option<&[u8; 16]>, - ad_opt: Option<&[u8]>, + aad_opt: Option<&[u8]>, output_hex: bool, ) { let (mut cipher, generated_nonce) = match nonce { @@ -165,23 +147,23 @@ fn aead128_encrypt_stream( exit(-1); }), }; - if let Some(ad) = ad_opt { + if let Some(aad) = aad_opt { // infallible: `do_update_aad` only refuses AAD once plaintext has been fed in, and none // has been yet. - cipher.do_update_aad(ad).unwrap(); + cipher.do_update_aad(aad).unwrap(); } if let Some(nonce) = generated_nonce { helpers::write_bytes_or_hex(&nonce, output_hex); } - let mut buf = [0u8; 1024]; + let mut buf = [0u8; CHUNK_LEN]; loop { - let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + let n = read_stdin_chunk(&mut buf); if n == 0 { break; } - let mut out = [0u8; 1024]; + let mut out = [0u8; CHUNK_LEN]; // infallible: `out` is as long as `buf`, so it cannot be shorter than the `n` bytes read // into it, which is the only length `OutputBufferTooSmall` could complain about. let written = cipher.do_encrypt_out(&buf[..n], &mut out).unwrap(); @@ -203,10 +185,9 @@ fn aead128_encrypt_stream( fn aead128_decrypt_stream( key: &AsconAEAD128Key, nonce: Option<&[u8; 16]>, - ad_opt: Option<&[u8]>, + aad_opt: Option<&[u8]>, output_hex: bool, ) { - const CHUNK: usize = 1024; let nonce = match nonce { Some(nonce) => *nonce, None => { @@ -214,29 +195,30 @@ fn aead128_decrypt_stream( if let Err(e) = io::stdin().read_exact(&mut nonce) { if e.kind() == io::ErrorKind::UnexpectedEof { eprintln!("Error: ciphertext is shorter than the 16-byte nonce."); - exit(-1); + } else { + eprintln!("Error: failed to read from stdin: {e}"); } - panic!("Failed to read from stdin: {e}"); + exit(-1); } nonce } }; let mut cipher = key.create_decryptor(&nonce); - if let Some(ad) = ad_opt { + if let Some(aad) = aad_opt { // infallible: as on the encrypt side, no ciphertext has been fed in yet. - cipher.do_update_aad(ad).unwrap(); + cipher.do_update_aad(aad).unwrap(); } - let mut buf = [0u8; CHUNK]; - let mut out = [0u8; CHUNK]; + let mut buf = [0u8; CHUNK_LEN]; + let mut out = [0u8; CHUNK_LEN]; loop { - let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + let n = read_stdin_chunk(&mut buf); if n == 0 { break; } // infallible: the decryptor releases at most what it has held back (16 bytes) plus what - // it is given, less the 16 it keeps, so never more than the `n <= CHUNK` bytes read. + // it is given, less the 16 it keeps, so never more than the `n <= CHUNK_LEN` bytes read. let written = cipher.do_decrypt_out(&buf[..n], &mut out).unwrap(); helpers::write_bytes_or_hex(&out[..written], output_hex); } diff --git a/cli/src/encoders_cmd.rs b/cli/src/encoders_cmd.rs index 6a9aaf8b7..6d939e973 100644 --- a/cli/src/encoders_cmd.rs +++ b/cli/src/encoders_cmd.rs @@ -94,7 +94,7 @@ pub(crate) fn base64_encode_cmd() { exit(-1); }); if n == 0 { - crate::helpers::write_stdout(encoder.do_final(&[]).as_bytes()); + crate::helpers::write_stdout(encoder.do_final([]).as_bytes()); return; } crate::helpers::write_stdout(encoder.do_update(&buf[..n]).as_bytes()); @@ -125,7 +125,7 @@ pub(crate) fn base64_decode_cmd() { let n = read_chunk(&mut buf); if n == 0 { // End of input with no padding seen: finish whatever partial block is held. - crate::helpers::write_stdout(&decoder.do_final(&[]).unwrap_or_else(|e| fail(e))); + crate::helpers::write_stdout(&decoder.do_final([]).unwrap_or_else(|e| fail(e))); return; } match decoder.do_update(&buf[..n]) { diff --git a/cli/src/helpers/aead_cipher_helpers.rs b/cli/src/helpers/aead_cipher_helpers.rs index 3574f6aa4..518822149 100644 --- a/cli/src/helpers/aead_cipher_helpers.rs +++ b/cli/src/helpers/aead_cipher_helpers.rs @@ -32,18 +32,19 @@ //! one call before any ciphertext, matching SP 800-38D Algorithm 4's requirement that AAD precede //! data. //! -//! `--aad-file` is read as raw bytes ([`read_from_file_raw`]), never hex-decoded. The -//! hex-or-raw guess `--key-file` uses would change what is authenticated without any error: a +//! `--aad-file` is read as raw bytes, or as hex text under the global `--hex-input`; nothing +//! is guessed from its contents. A guess would change what is authenticated without any error: a //! binary header that happens to parse as hex text (`cafe`, or sixteen zero bytes, which the hex -//! decoder skips) would be authenticated as its decoding, and the tag would not verify against any -//! other GCM implementation given the same file. +//! decoder skips) would be authenticated as its decoding, and the tag would not verify against +//! any other GCM implementation given the same file. -use crate::helpers::{flush_stdout, read_from_file_raw, write_bytes_or_hex, write_stdout}; +use crate::helpers::{ + decode_hex_arg, flush_stdout, read_input_file, write_bytes_or_hex, write_stdout, +}; use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey}; use bouncycastle::core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; -use bouncycastle::hex; use std::io; use std::io::{Read, Write}; use std::process::exit; @@ -57,18 +58,13 @@ const CHUNK_LEN: usize = 1024; /// calls the value the IV; the trait and this file's identifiers call it the nonce. const GCM_NONCE_LEN: usize = 12; -/// Loads the additional authenticated data from `--aad` (hex) or `--aad-file` (raw bytes; see the -/// module docs for why not hex). Empty if neither is given: AAD is optional, unlike the key. +/// Loads the additional authenticated data from `--aad` (hex) or `--aad-file` (raw bytes; hex +/// with `--hex-input`). Empty if neither is given: AAD is optional, unlike the key. pub fn load_aad(aad: &Option, aad_file: &Option) -> Vec { - if let Some(path) = aad_file { - read_from_file_raw(path) - } else if let Some(hex_str) = aad { - hex::decode(hex_str).unwrap_or_else(|_| { - eprintln!("Error: `--aad` must be hex. Use `--aad-file` for raw bytes."); - exit(-1); - }) - } else { - Vec::new() + match (aad_file, aad) { + (Some(path), _) => read_input_file(path), + (None, Some(hex_str)) => decode_hex_arg(hex_str, "aad"), + (None, None) => Vec::new(), } } diff --git a/cli/src/helpers/block_mode_helpers.rs b/cli/src/helpers/block_mode_helpers.rs index cc3b9ec71..a81bd91f4 100644 --- a/cli/src/helpers/block_mode_helpers.rs +++ b/cli/src/helpers/block_mode_helpers.rs @@ -55,8 +55,8 @@ //! ``` use crate::helpers::{ - flush_stdout, read_from_file, read_from_file_raw, strip_trailing_newline, write_bytes_or_hex, - write_stdout, + decode_hex_arg, flush_stdout, hex_input, load_secret, read_input_file, strip_trailing_newline, + write_bytes_or_hex, write_stdout, }; use bouncycastle::core::errors::{RNGError, SymmetricCipherError}; use bouncycastle::core::hazmat::do_hazardous_operations; @@ -64,7 +64,6 @@ use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle::core::security_strength::SecurityStrength; use bouncycastle::core::symmetric_key::FromKeyMaterial; use bouncycastle::core::traits::{RNG, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -use bouncycastle::hex; use bouncycastle::rng::HashDRBG_SHA512; use clap::ValueEnum; use std::io; @@ -88,7 +87,8 @@ pub(crate) enum CipherDirection { Decrypt, } -/// Loads the key from `--key` (hex) or `--key-file` (binary or hex), and checks its length. +/// Loads the key from `--key-file` (raw bytes; hex with `--hex-input`) or `--key` (hex), and +/// checks its length. /// /// `KEY_LEN` is exact: AES has three key lengths and the command selects one, so a key of the /// wrong length is a mistake rather than something to truncate or pad. @@ -97,24 +97,12 @@ pub(crate) fn load_key( key_file: &Option, alg: &str, ) -> KeyMaterial { - let key_bytes: Vec = if let Some(key_file) = key_file { - // A file may hold raw bytes or hex; `read_from_file` tries hex first, as the other - // commands do. - let bytes = read_from_file(key_file); - // `read_from_file` already ignores a trailing newline on a hex file. A *raw* key file may - // end in one too, which lengthens the key by a byte; strip it only when that leaves - // exactly the key, so a binary key whose last byte really is `0x0a` is not shortened. - let trimmed = strip_trailing_newline(&bytes); - if trimmed.len() == KEY_LEN { trimmed.to_vec() } else { bytes } - } else if let Some(key) = key { - hex::decode(key).unwrap_or_else(|_| { - eprintln!("Error: `--key` must be hex. Use `--key-file` for raw bytes."); - exit(-1); - }) - } else { - eprintln!("Error: either `--key` or `--key-file` must be supplied."); - exit(-1); - }; + let secret = load_secret(key, key_file, "key"); + // A raw key file may end in a newline, which lengthens the key by a byte; strip it only + // when that leaves exactly the key, so a binary key whose last byte really is `0x0a` is not + // shortened. + let trimmed = strip_trailing_newline(secret.ref_to_bytes()); + let key_bytes = if trimmed.len() == KEY_LEN { trimmed } else { secret.ref_to_bytes() }; if key_bytes.len() != KEY_LEN { eprintln!("Error: {alg} needs a {KEY_LEN}-byte key, got {} bytes.", key_bytes.len()); @@ -124,7 +112,7 @@ pub(crate) fn load_key( // `from_bytes_as_type` tags the key at the strength its length implies, which is exactly what // the engine requires -- except for an all-zero key, which it marks Zeroized instead. let mut key = - KeyMaterial::::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) + KeyMaterial::::from_bytes_as_type(key_bytes, KeyType::SymmetricCipherKey) .unwrap_or_else(|e| { eprintln!("Error: couldn't load the key: {e:?}"); exit(-1); @@ -209,19 +197,20 @@ impl RNG for FixedIv { } } -/// Loads `--iv-file` (raw bytes, never hex-decoded) or `--iv` (hex); the file wins if both are -/// given, as for `--key-file`. `None` when neither is given, which selects the generated, -/// in-band IV. The IV must be exactly `INIT_DATA_LEN` bytes. +/// Loads `--iv-file` (raw bytes; hex with `--hex-input`) or `--iv` (hex); the file wins if +/// both are given, as for `--key-file`. `None` when neither is given, which selects the +/// generated, in-band IV. The IV must be exactly `INIT_DATA_LEN` bytes. /// -/// As for CCM's `--nonce-file`, a trailing newline in the file is part of the IV, and is warned -/// about, because a file written with `echo` rather than `echo -n` is the usual way to get one. +/// As for CCM's `--nonce-file`, a trailing newline in a raw file is part of the IV, and is +/// warned about, because a file written with `echo` rather than `echo -n` is the usual way to +/// get one. pub(crate) fn load_iv( iv: &Option, iv_file: &Option, ) -> Option<[u8; INIT_DATA_LEN]> { let bytes = if let Some(file) = iv_file { - let bytes = read_from_file_raw(file); - if bytes.last() == Some(&b'\n') { + let bytes = read_input_file(file); + if !hex_input() && bytes.last() == Some(&b'\n') { eprintln!( "Warning: IV file '{file}' ends with a newline byte (0x0a), which is used as part \ of the IV. If that is not intended, write it with `printf` or `echo -n`." @@ -229,10 +218,7 @@ pub(crate) fn load_iv( } bytes } else if let Some(v) = iv { - hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: IV is not valid hex."); - exit(-1) - }) + decode_hex_arg(v, "iv") } else { return None; }; diff --git a/cli/src/helpers/mod.rs b/cli/src/helpers/mod.rs index eb56485d9..78b4903e5 100644 --- a/cli/src/helpers/mod.rs +++ b/cli/src/helpers/mod.rs @@ -1,3 +1,22 @@ +//! Shared plumbing: how files and stdin are read, how secrets are held, how output is written, +//! and the streaming loops the hash-like commands share. +//! +//! # Input conventions, the same for every subcommand +//! +//! - A value given inline (`--key`, `--iv`, `--nonce`, `--aad`, `--salt`, `--ikm`, `--verify`, +//! `--element`, `--customization-hex`, ...) is hex. +//! - A value given as a file (`--key-file`, `--iv-file`, `--skfile`, `--sigfile`, ...), or on +//! stdin where a command reads a key, seed, signature or ciphertext there, is the file's raw +//! bytes. With the global `--hex-input` flag it is hex text instead, with whitespace, `\x` +//! prefixes and a trailing newline ignored. There is no guessing: a raw file whose bytes happen +//! to be hex digits is still raw. +//! - A trailing newline is part of a raw value except where a command says it strips one. +//! - Keys, seeds, private keys, salts and IKM are read into [`SecretBytes`], a `KeyMaterial` +//! buffer that is zeroized when dropped, and never through a `Vec`; a secret longer than +//! [`MAX_SECRET_LEN`] is refused. +//! - A read or write failure, a malformed value and a wrong length are reported on stderr with +//! an `Error:` line and exit status 255; a panic is never the answer to input. + use bouncycastle::core::hazmat::do_hazardous_operations; use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle::core::security_strength::SecurityStrength; @@ -7,87 +26,249 @@ use std::fs::File; use std::io; use std::io::{Read, Write}; use std::process::exit; +use std::sync::atomic::{AtomicBool, Ordering}; pub mod aead_cipher_helpers; pub mod block_mode_helpers; pub mod stream_mode_helpers; -/// Reads a file's bytes exactly as they are, with no hex-or-raw guessing. -/// -/// Use this where a misread would silently change the *value* the caller asked for rather than -/// merely fail to match it -- a nonce is the reason this exists: two distinct binary nonce files -/// that happen to decode as hex to the same bytes must not collapse to one nonce (see -/// `aes_ccm_cmd::load_nonce`). [`read_from_file`]'s "try hex, fall back to raw" heuristic is fine -/// for a key, where a wrong guess only ever produces a mismatch, never a same-looking-different -/// value. -pub(crate) fn read_from_file_raw(filename: &str) -> Vec { +/// Bytes read from stdin per call, and squeezed per call for an XOF. +pub(crate) const CHUNK_LEN: usize = 1024; + +/// The most a secret read from a file, stdin or the command line may be: enough for an ML-DSA-87 +/// private key (4896 bytes) in hex with room to spare. +pub(crate) const MAX_SECRET_LEN: usize = 16 * 1024; + +/// A secret as read: a zeroized buffer holding up to [`MAX_SECRET_LEN`] bytes, tagged +/// [`KeyType::Unknown`] (or `Zeroized` when all zero); the command re-tags what it makes of it. +pub(crate) type SecretBytes = KeyMaterial; + +static HEX_INPUT: AtomicBool = AtomicBool::new(false); + +/// Records the global `--hex-input` flag for the readers below. +pub(crate) fn set_hex_input(on: bool) { + HEX_INPUT.store(on, Ordering::Relaxed); +} + +/// Whether files and stdin values are hex text rather than raw bytes. +pub(crate) fn hex_input() -> bool { + HEX_INPUT.load(Ordering::Relaxed) +} + +// ---- public (non-secret) input --------------------------------------------------------------- + +/// A file's bytes exactly as they are, regardless of `--hex-input`: for the data a command +/// authenticates or hashes rather than a value it decodes. +pub(crate) fn read_file(filename: &str) -> Vec { std::fs::read(filename).unwrap_or_else(|e| { eprintln!("Error: couldn't read file '{filename}': {e}"); exit(-1); }) } -/// `bytes` without one trailing line ending (`\n` or `\r\n`), if it has one. -/// -/// Files written by shell tools usually end in a newline that is not part of the value. Whether -/// stripping it is right depends on what the caller expects -- a binary key whose last byte -/// happens to be `0x0a` must not lose it -- so this only removes the bytes; the caller decides, -/// by the length it knows, whether to use the result. -pub(crate) fn strip_trailing_newline(bytes: &[u8]) -> &[u8] { - bytes.strip_suffix(b"\r\n").or_else(|| bytes.strip_suffix(b"\n")).unwrap_or(bytes) +/// A file's value: its raw bytes, or its hex decoding under `--hex-input`. +pub(crate) fn read_input_file(filename: &str) -> Vec { + let bytes = read_file(filename); + if !hex_input() { + return bytes; + } + hex::decode(&bytes).unwrap_or_else(|e| { + eprintln!("Error: file '{filename}' is not valid hex (given --hex-input): {e:?}"); + exit(-1); + }) +} + +/// Stdin's value, as [`read_input_file`] for a file. +pub(crate) fn read_input_stdin() -> Vec { + let bytes = read_stdin_all(); + if !hex_input() { + return bytes; + } + hex::decode(&bytes).unwrap_or_else(|e| { + eprintln!("Error: stdin is not valid hex (given --hex-input): {e:?}"); + exit(-1); + }) +} + +/// The value from the file when one is named, else from stdin. +pub(crate) fn read_input_file_or_stdin(filename: &Option) -> Vec { + match filename { + Some(filename) => read_input_file(filename), + None => read_input_stdin(), + } +} + +/// Decodes an inline hex argument, naming the flag in the error. +pub(crate) fn decode_hex_arg(value: &str, flag: &str) -> Vec { + hex::decode(value).unwrap_or_else(|_| { + eprintln!("Error: `--{flag}` must be hex."); + exit(-1); + }) } -/// The bytes a hex-or-raw input stands for: its hex decoding if it is hex -- with one trailing -/// line ending ignored, since files and pasted input usually end in one -- and the bytes -/// themselves, untouched, otherwise. A raw input keeps its trailing newline; only the caller -/// knows whether that byte is part of the value (see `block_mode_helpers::load_key`). -pub(crate) fn hex_or_raw(buf: Vec) -> Vec { - // Decide by the bytes themselves rather than by whether the decoder accepts them: it skips - // NUL bytes as well as whitespace, so an all-zero binary key would otherwise "decode" to an - // empty hex string. Hex text is hex digits, whitespace and `\x` escapes, and nothing else. - let looks_like_hex = buf.iter().any(u8::is_ascii_hexdigit) - && buf - .iter() - .all(|b| b.is_ascii_hexdigit() || b.is_ascii_whitespace() || *b == b'\\' || *b == b'x'); - if !looks_like_hex { - return buf; - } - match hex::decode(strip_trailing_newline(&buf)) { - Ok(decoded) => decoded, - Err(_) => buf, - } -} - -/// Reads either bin or hex -pub(crate) fn read_from_file(filename: &str) -> Vec { - let file = File::open(&filename); - if file.is_ok() { - let mut buf = Vec::::new(); - match file.unwrap().read_to_end(&mut buf) { - Ok(_bytes_read) => hex_or_raw(buf), - Err(_) => { - eprintln!("Error: couldn't open file '{}'", &filename); +// ---- secret input ---------------------------------------------------------------------------- + +/// Reads `source` to its end into a fresh [`SecretBytes`], refusing more than +/// [`MAX_SECRET_LEN`] bytes. `what` names the source for the messages. +fn read_secret_from(source: &mut impl Read, what: &str) -> SecretBytes { + let mut secret = SecretBytes::new(); + let mut len = 0usize; + let result = do_hazardous_operations(&mut secret, |secret| { + let buf = secret.ref_to_bytes_mut()?; + loop { + let n = source.read(&mut buf[len..]).unwrap_or_else(|e| { + eprintln!("Error: couldn't read {what}: {e}"); exit(-1); + }); + if n == 0 { + break; + } + len += n; + if len == buf.len() { + // Full: anything more is over the limit. (A read of 0 into an empty slice is + // indistinguishable from end of input, so probe explicitly.) + let mut probe = [0u8; 1]; + if source.read(&mut probe).unwrap_or(0) != 0 { + eprintln!( + "Error: {what} is longer than the {MAX_SECRET_LEN} bytes a secret may be." + ); + exit(-1); + } + break; } } - } else { - eprintln!("Error: couldn't open file '{}'", &filename); + secret.set_key_len(len)?; + secret.set_key_type(KeyType::Unknown) + }); + secret_tagging_error(result, what); + secret +} + +/// The hex decoding of `text`, into a fresh [`SecretBytes`]: the decoder writes straight into +/// the zeroized buffer, so the plaintext of the secret is never in a `Vec`. +fn decode_secret_hex(text: &[u8], what: &str) -> SecretBytes { + let mut secret = SecretBytes::new(); + let result = do_hazardous_operations(&mut secret, |secret| { + let out = secret.ref_to_bytes_mut()?; + let len = hex::decode_out(text, out).unwrap_or_else(|e| { + eprintln!("Error: {what} is not valid hex: {e:?}"); + exit(-1); + }); + secret.set_key_len(len)?; + secret.set_key_type(KeyType::Unknown) + }); + secret_tagging_error(result, what); + secret +} + +/// `set_key_len` and `set_key_type` refuse nothing a reader can cause except an all-zero value, +/// which they tag `Zeroized` and report; that is a value, not an error, so it passes. +fn secret_tagging_error( + result: Result<(), bouncycastle::core::errors::KeyMaterialError>, + what: &str, +) { + use bouncycastle::core::errors::KeyMaterialError; + match result { + Ok(()) | Err(KeyMaterialError::ActingOnZeroizedKey) => {} + Err(e) => { + eprintln!("Error: couldn't hold {what} as key material: {e:?}"); + exit(-1); + } + } +} + +/// A secret from a file: its raw bytes, or its hex decoding under `--hex-input`. +pub(crate) fn read_secret_file(filename: &str) -> SecretBytes { + let mut file = File::open(filename).unwrap_or_else(|e| { + eprintln!("Error: couldn't open file '{filename}': {e}"); exit(-1); + }); + let what = format!("file '{filename}'"); + let raw = read_secret_from(&mut file, &what); + if !hex_input() { + return raw; } + decode_secret_hex(raw.ref_to_bytes(), &what) } -/// Reads either bin or hex -pub(crate) fn read_from_file_or_stdin(filename: &Option) -> Vec { - if filename.is_some() { - // This already reads either bin or hex - return read_from_file(filename.as_ref().unwrap()); +/// A secret from stdin, as [`read_secret_file`] for a file. +pub(crate) fn read_secret_stdin() -> SecretBytes { + let raw = read_secret_from(&mut io::stdin(), "stdin"); + if !hex_input() { + return raw; } + decode_secret_hex(raw.ref_to_bytes(), "stdin") +} - let mut buf = Vec::::new(); - io::stdin().read_to_end(&mut buf).expect("Failed to read from stdin"); - hex_or_raw(buf) +/// The secret from the file when one is named, else from stdin. +pub(crate) fn read_secret_file_or_stdin(filename: &Option) -> SecretBytes { + match filename { + Some(filename) => read_secret_file(filename), + None => read_secret_stdin(), + } } +/// A secret given inline, in hex. The command line is visible to other users and kept in shell +/// history, which the help texts say; this only decodes what was given. +pub(crate) fn decode_hex_secret(value: &str, flag: &str) -> SecretBytes { + decode_secret_hex(value.as_bytes(), &format!("`--{flag}`")) +} + +/// A secret from `---file` or `--` (hex); the file wins when both are given, as the +/// help texts say. Neither is an error. +pub(crate) fn load_secret( + inline: &Option, + file: &Option, + flag: &str, +) -> SecretBytes { + match (file, inline) { + (Some(file), _) => read_secret_file(file), + (None, Some(value)) => decode_hex_secret(value, flag), + (None, None) => { + eprintln!("Error: either `--{flag}` or `--{flag}-file` must be supplied."); + exit(-1); + } + } +} + +/// `bytes` without one trailing line ending (`\n` or `\r\n`), if it has one. Only the caller +/// knows, by the length it expects, whether that byte is part of the value. +pub(crate) fn strip_trailing_newline(bytes: &[u8]) -> &[u8] { + bytes.strip_suffix(b"\r\n").or_else(|| bytes.strip_suffix(b"\n")).unwrap_or(bytes) +} + +// ---- stdin as data --------------------------------------------------------------------------- + +/// One read from stdin into `buf`; a read error is reported, not a panic. Returns 0 at the end. +pub(crate) fn read_stdin_chunk(buf: &mut [u8]) -> usize { + io::stdin().read(buf).unwrap_or_else(|e| { + eprintln!("Error: failed to read from stdin: {e}"); + exit(-1); + }) +} + +/// Feeds stdin to `sink` in pieces of at most [`CHUNK_LEN`] bytes, so a long input is never +/// held in memory. +pub(crate) fn stream_stdin(mut sink: impl FnMut(&[u8])) { + let mut buf = [0u8; CHUNK_LEN]; + loop { + let n = read_stdin_chunk(&mut buf); + if n == 0 { + return; + } + sink(&buf[..n]); + } +} + +/// All of stdin, where the whole input must be held anyway. +pub(crate) fn read_stdin_all() -> Vec { + let mut out = Vec::new(); + stream_stdin(|chunk| out.extend_from_slice(chunk)); + out +} + +// ---- output ---------------------------------------------------------------------------------- + /// Writes `bytes` to stdout, exiting quietly if the reader has gone away. /// /// A closed stdout -- `bc-rust ... | head -c 16` -- is the reader's choice, not a failure of ours, @@ -131,93 +312,107 @@ pub(crate) fn write_bytes_or_hex(bytes: &[u8], output_hex: bool) { } } +/// Writes `bytes`, raw or as hex, to a new file `filename`; a failure names the path. pub(crate) fn write_bytes_or_hex_to_file(bytes: &[u8], filename: &str, output_hex: bool) { - let mut file = File::create(filename).expect("Failed to create file"); - - if output_hex { - for b in bytes.iter() { - file.write_all(format!("{b:02x}").as_bytes()).unwrap(); + let result = File::create(filename).and_then(|mut file| { + if output_hex { + file.write_all(hex::encode(bytes).as_bytes()) + } else { + file.write_all(bytes) } - } else { - file.write_all(bytes).unwrap(); + }); + if let Err(e) = result { + eprintln!("Error: couldn't write file '{filename}': {e}"); + exit(-1); } } -/// Loads it as either hex or bytes -pub(crate) fn parse_seed(bytes: &[u8]) -> Result, ()> { - let bytes = if bytes.len() == 65 { &bytes[..64] } else { bytes }; - - // try decoding it as hex first - let seed_bytes: [u8; SEED_LEN] = match &hex::decode(&bytes) { - Ok(decoded_bytes) => { - if decoded_bytes.len() < SEED_LEN || decoded_bytes.len() > SEED_LEN + 1 { - // it was valid hex, but the wrong length - return Err(()); - } +// ---- seeds ----------------------------------------------------------------------------------- - decoded_bytes[..SEED_LEN].try_into().unwrap() - } - Err(_) => { - // it's not hex, so take the first SEED_LEN bytes of the raw binary - if bytes.len() < SEED_LEN || bytes.len() > SEED_LEN + 1 { - return Err(()); - } - - bytes[..SEED_LEN].try_into().unwrap() - } - }; - - // TODO: Verify that all error conditions have been checked - let mut seed = KeyMaterial::::from_bytes_as_type(&seed_bytes, KeyType::Seed).unwrap(); +/// Why a seed was refused: its length, which is the only thing checked. +pub(crate) struct SeedError { + pub(crate) got: usize, +} +/// A `SEED_LEN`-byte seed from its bytes, which the caller has already read (raw, or decoded +/// from hex under `--hex-input`). One trailing newline is tolerated, since a seed file written +/// with `echo` has one. +/// +/// A seed is required to be unpredictable at the parameter set's strength; the CLI cannot know +/// whether it is, so an all-zero seed is accepted with a warning, which is what test vectors +/// need. +pub(crate) fn parse_seed( + bytes: &[u8], +) -> Result, SeedError> { + let bytes = if bytes.len() == SEED_LEN + 1 { strip_trailing_newline(bytes) } else { bytes }; + if bytes.len() != SEED_LEN { + return Err(SeedError { got: bytes.len() }); + } + let mut seed = KeyMaterial::::from_bytes_as_type(bytes, KeyType::Seed) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't hold the seed as key material: {e:?}"); + exit(-1); + }); if seed.key_type() == KeyType::Zeroized || seed.security_strength() < SecurityStrength::_256bit { eprintln!( "Warning: low entropy seed provided. We'll still process it, but it may be insecure." ); - do_hazardous_operations(&mut seed, |seed| { seed.set_key_type(KeyType::Seed)?; seed.set_security_strength(SecurityStrength::_256bit) }) - .unwrap(); + .unwrap_or_else(|e| { + eprintln!("Error: couldn't tag the seed: {e:?}"); + exit(-1); + }); } - Ok(seed) } -/// Stream stdin through a [`Hash`] and write the digest to stdout (hex or binary), followed by a -/// newline. Used by both the SHA-3 and Ascon-Hash256 subcommands. -pub(crate) fn stream_hash(mut hasher: impl Hash, output_hex: bool) { - let mut buf: [u8; 1024] = [0u8; 1024]; - - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - - while bytes_read != 0 { - hasher.do_update(&buf[..bytes_read]); +/// Reports a refused seed and exits. +pub(crate) fn seed_error(e: SeedError) -> ! { + eprintln!( + "Error: the seed must be {SEED_LEN} bytes (raw, or hex with --hex-input), got {} bytes.", + e.got + ); + exit(-1); +} - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - } +// ---- streaming loops ------------------------------------------------------------------------- +/// Streams stdin through a [`Hash`] and writes the digest to stdout (hex or binary), followed by +/// a newline. Used by the SHA-2, SHA-3, SM3 and Ascon-Hash256 subcommands. +pub(crate) fn stream_hash(mut hasher: impl Hash, output_hex: bool) { + stream_stdin(|chunk| hasher.do_update(chunk)); let out = hasher.do_final(); write_bytes_or_hex(&out, output_hex); write_stdout(b"\n"); } -/// Stream stdin through an [`XOF`] and squeeze `output_len` bytes to stdout (hex or binary), -/// followed by a newline. Used by both the SHAKE and Ascon-XOF128/CXOF128 subcommands. +/// Streams stdin through an [`XOF`] and squeezes `output_len` bytes to stdout (hex or binary), +/// followed by a newline. Used by the SHAKE, cSHAKE, the SP 800-185 XOFs and Ascon-XOF128/CXOF128 +/// subcommands. pub(crate) fn stream_xof(mut xof: impl XOF, output_len: usize, output_hex: bool) { - let mut buf: [u8; 1024] = [0u8; 1024]; - - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - - while bytes_read != 0 { - xof.do_update(&buf[..bytes_read]); + stream_stdin(|chunk| xof.do_update(chunk)); + squeeze_to_stdout(xof.into_squeezer(), output_len, output_hex); +} - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); +/// Writes `output_len` bytes of `squeezer` to stdout in [`CHUNK_LEN`] pieces, followed by a +/// newline, so the output is never held whole and a very long one is a long run rather than an +/// allocation. +pub(crate) fn squeeze_to_stdout( + mut squeezer: impl XOFSqueezer, + output_len: usize, + output_hex: bool, +) { + let mut buf = [0u8; CHUNK_LEN]; + let mut left = output_len; + while left > 0 { + let n = left.min(CHUNK_LEN); + squeezer.do_output_out(&mut buf[..n]); + write_bytes_or_hex(&buf[..n], output_hex); + left -= n; } - - let out = xof.into_squeezer().do_output_final(output_len); - write_bytes_or_hex(&out, output_hex); write_stdout(b"\n"); } diff --git a/cli/src/hkdf_cmd.rs b/cli/src/hkdf_cmd.rs index b0c767ada..3ba6a1818 100644 --- a/cli/src/hkdf_cmd.rs +++ b/cli/src/hkdf_cmd.rs @@ -1,16 +1,18 @@ -use std::fs; +//! The `hkdf-*` subcommands: RFC 5869 extract-then-expand. + use std::process::exit; +use crate::helpers::{ + decode_hex_arg, load_secret, read_input_file, write_bytes_or_hex, write_stdout, +}; use bouncycastle::core::hazmat::do_hazardous_operations; use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle::hex; use bouncycastle::hkdf; +use bouncycastle::hkdf::HKDF; +use bouncycastle::hmac::HMACParams; use bouncycastle::sha2::hkdf::{HKDF_SHA256, HKDF_SHA384, HKDF_SHA512}; use bouncycastle::sm3::hkdf::HKDF_SM3; -/// The salt is held in a fixed-size key buffer; the library itself puts no bound on it. -const MAX_SALT_LEN: usize = 1024; - #[allow(non_camel_case_types)] pub(crate) enum HKDFVariant { SHA256, @@ -19,6 +21,7 @@ pub(crate) enum HKDFVariant { SM3, } +#[allow(clippy::too_many_arguments)] // one flag per RFC 5869 input, each inline or from a file pub(crate) fn hkdf_cmd( variant: HKDFVariant, salt: &Option, @@ -30,82 +33,60 @@ pub(crate) fn hkdf_cmd( len: usize, output_hex: bool, ) { - // Each value may come from hex on the command line or from a binary file; the file wins if - // both are given, as the subcommands' help says. - let salt_bytes = if let Some(file) = salt_file { - fs::read(file).unwrap() - } else if let Some(hex_str) = salt { - hex::decode(hex_str).unwrap() - } else { - eprintln!("Error: either `salt` or `salt-file` must be supplied."); - exit(-1) - }; - if salt_bytes.len() > MAX_SALT_LEN { - eprintln!("Error: The CLI only supports HKDF salts up to {MAX_SALT_LEN} bytes."); - exit(-1); - } - let mut salt_key = KeyMaterial::::from_bytes(&salt_bytes).unwrap(); - // force it just so the CLI behaves properly even with all-zero or zero-length keys - do_hazardous_operations(&mut salt_key, |salt_key| salt_key.set_key_type(KeyType::MACKey)) - .unwrap(); - - let ikm_bytes = if let Some(file) = ikm_file { - fs::read(file).unwrap() - } else if let Some(hex_str) = ikm { - hex::decode(hex_str).unwrap() - } else { - eprintln!("Error: either `ikm` or `ikm_file` must be supplied."); - exit(-1) - }; + // Each value may come from hex on the command line or from a file; the file wins if both + // are given, as the subcommands' help says. + let mut salt = load_secret(salt, salt_file, "salt"); + // Tagged as the secret salt of SP 800-56C: that is what credits its entropy, and an all-zero + // or empty salt is a legitimate thing to give on the command line. + do_hazardous_operations(&mut salt, |salt| salt.set_key_type(KeyType::MACKey)).unwrap_or_else( + |e| { + eprintln!("Error: couldn't load the salt: {e:?}"); + exit(-1); + }, + ); + let ikm = load_secret(ikm, ikm_file, "ikm"); // RFC 5869 Section 2.3: info is "optional context and application specific information (can // be a zero-length string)", so neither flag is required. - let info = if let Some(file) = additional_input_file { - fs::read(file).unwrap() - } else if let Some(hex_str) = additional_input { - hex::decode(hex_str).unwrap() - } else { - Vec::new() + let info = match (additional_input_file, additional_input) { + (Some(file), _) => read_input_file(file), + (None, Some(hex_str)) => decode_hex_arg(hex_str, "additional-input"), + (None, None) => Vec::new(), }; // RFC 5869: PRK = HKDF-Extract(salt, IKM), then OKM = HKDF-Expand(PRK, info, L). The IKM is - // streamed into the extract phase, so its length is not bounded by a KeyMaterial buffer, and the - // additional input is the expand phase's `info`. The library refuses an L above 255 * HashLen. + // streamed into the extract phase and the additional input is the expand phase's `info`. The + // library refuses an L above 255 * HashLen. let mut out_key = KeyMaterial::<{ 255 * hkdf::MAX_HMAC_OUTPUT_LEN }>::new(); let result = match variant { - HKDFVariant::SHA256 => { - let mut h = HKDF_SHA256::new(); - h.do_extract_init(&salt_key).unwrap(); - h.do_extract_update_bytes(&ikm_bytes).unwrap(); - let prk = h.do_extract_final().unwrap(); - HKDF_SHA256::expand_out(&prk, &info, len, &mut out_key) - } - HKDFVariant::SHA384 => { - let mut h = HKDF_SHA384::new(); - h.do_extract_init(&salt_key).unwrap(); - h.do_extract_update_bytes(&ikm_bytes).unwrap(); - let prk = h.do_extract_final().unwrap(); - HKDF_SHA384::expand_out(&prk, &info, len, &mut out_key) - } - HKDFVariant::SHA512 => { - let mut h = HKDF_SHA512::new(); - h.do_extract_init(&salt_key).unwrap(); - h.do_extract_update_bytes(&ikm_bytes).unwrap(); - let prk = h.do_extract_final().unwrap(); - HKDF_SHA512::expand_out(&prk, &info, len, &mut out_key) - } - HKDFVariant::SM3 => { - let mut h = HKDF_SM3::new(); - h.do_extract_init(&salt_key).unwrap(); - h.do_extract_update_bytes(&ikm_bytes).unwrap(); - let prk = h.do_extract_final().unwrap(); - HKDF_SM3::expand_out(&prk, &info, len, &mut out_key) - } + HKDFVariant::SHA256 => run(HKDF_SHA256::new(), &salt, &ikm, &info, len, &mut out_key), + HKDFVariant::SHA384 => run(HKDF_SHA384::new(), &salt, &ikm, &info, len, &mut out_key), + HKDFVariant::SHA512 => run(HKDF_SHA512::new(), &salt, &ikm, &info, len, &mut out_key), + HKDFVariant::SM3 => run(HKDF_SM3::new(), &salt, &ikm, &info, len, &mut out_key), }; if let Err(e) = result { - eprintln!("Error: {e:?}"); + eprintln!("Error: HKDF failed: {e}"); exit(-1); } - crate::helpers::write_bytes_or_hex(out_key.ref_to_bytes(), output_hex); + write_bytes_or_hex(out_key.ref_to_bytes(), output_hex); + if output_hex { + write_stdout(b"\n"); + } +} + +/// Extract then expand through `h`, into `out_key`. +fn run( + mut h: HKDF, + salt: &impl KeyMaterialTrait, + ikm: &impl KeyMaterialTrait, + info: &[u8], + len: usize, + out_key: &mut impl KeyMaterialTrait, +) -> Result { + h.do_extract_init(salt).map_err(|e| format!("{e:?}"))?; + h.do_extract_update_bytes(ikm.ref_to_bytes()).map_err(|e| format!("{e:?}"))?; + let prk = h.do_extract_final().map_err(|e| format!("{e:?}"))?; + HKDF::::expand_out(&prk, info, len, out_key) + .map_err(|e| format!("{e:?}")) } diff --git a/cli/src/mac_cmd.rs b/cli/src/mac_cmd.rs index d335c57d0..f17e391cb 100644 --- a/cli/src/mac_cmd.rs +++ b/cli/src/mac_cmd.rs @@ -1,19 +1,19 @@ -use std::io; -use std::io::Read; -use std::process::exit; +//! The `hmac-*` and `kmac*` subcommands: a MAC over stdin, computed or verified. -use crate::helpers::read_from_file_raw; -use crate::sha3_cmd::Strength; +use crate::helpers::{ + SecretBytes, decode_hex_arg, load_secret, stream_stdin, write_bytes_or_hex, write_stdout, +}; +use crate::sha3_cmd::{MAX_FIXED_OUTPUT_LEN, Strength}; use bouncycastle::core::errors::MACError; use bouncycastle::core::hazmat::do_hazardous_operations; -use bouncycastle::core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; +use bouncycastle::core::key_material::{KeyMaterialTrait, KeyType}; use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; use bouncycastle::core::traits::MAC; -use bouncycastle::hex; use bouncycastle::hmac::HmacKey; use bouncycastle::sha2::{SHA256, SHA512, SHA512_224, SHA512_256}; use bouncycastle::sha3::kmac::{KMAC128Key, KMAC256Key}; use bouncycastle::sm3::SM3; +use std::process::exit; #[allow(non_camel_case_types)] pub(crate) enum HMACVariant { @@ -24,29 +24,18 @@ pub(crate) enum HMACVariant { SM3, } -/// Loads a MAC key from `--key-file` (raw) or `--key` (hex), tagged as a MAC key. The file wins -/// when both are given, as the help text says and the other commands' loaders do. -fn load_mac_key(key: &Option, key_file: &Option) -> KeyMaterial512 { - let key_bytes: Vec = if let Some(key_file) = key_file { - read_from_file_raw(key_file) - } else if let Some(key) = key { - hex::decode(key).unwrap_or_else(|_| { - eprintln!("Error: `--key` must be hex. Use `--key-file` for raw bytes."); - exit(-1); - }) - } else { - eprintln!("Error: either `key` or `key-file` must be supplied."); - exit(-1) - }; - - if key_bytes.len() > 64 { - eprintln!("Error: CLI only supports MAC keys 64 bytes."); - exit(-1); +/// The MAC key from `--key-file` or `--key`, tagged as a MAC key. Any length the secret buffer +/// holds is accepted: HMAC hashes a key longer than its block (RFC 2104 Section 2), and KMAC +/// encodes the key's length (SP 800-185 Section 4.2). +fn load_mac_key(key: &Option, key_file: &Option) -> SecretBytes { + let mut key = load_secret(key, key_file, "key"); + if key.key_type() == KeyType::Zeroized { + // Same stance as the cipher loaders: warn, then do what was asked. A CLI is used for + // test vectors and scripting, where an all-zero key is a legitimate thing to want. + eprintln!( + "Warning: all-zero (or otherwise zeroized) key provided. Proceeding, but this is not secure." + ); } - let mut key = KeyMaterial512::from_bytes(&key_bytes).unwrap_or_else(|e| { - eprintln!("Error: couldn't load the key: {e:?}"); - exit(-1); - }); do_hazardous_operations(&mut key, |key| key.set_key_type(KeyType::MACKey)).unwrap_or_else( |e| { eprintln!("Error: couldn't load the key: {e:?}"); @@ -56,6 +45,11 @@ fn load_mac_key(key: &Option, key_file: &Option) -> KeyMaterial5 key } +fn mac_key_error(e: MACError) -> ! { + eprintln!("Error: couldn't build the MAC key: {e:?}"); + exit(-1) +} + pub(crate) fn mac_cmd( hmac_variant: HMACVariant, key: &Option, @@ -65,26 +59,32 @@ pub(crate) fn mac_cmd( ) { let key = load_mac_key(key, key_file); - // instantiate the MAC object and call do_mac() + // The allow-weak form: a CLI is used for test vectors and scripting, where a short or + // all-zero key is a legitimate thing to want. match hmac_variant { HMACVariant::SHA256 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key) + .unwrap_or_else(|e| mac_key_error(e)); do_mac(hmac_key.create_generator(), verify_val, output_hex); } HMACVariant::SHA512 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key) + .unwrap_or_else(|e| mac_key_error(e)); do_mac(hmac_key.create_generator(), verify_val, output_hex); } HMACVariant::SHA512_224 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key) + .unwrap_or_else(|e| mac_key_error(e)); do_mac(hmac_key.create_generator(), verify_val, output_hex); } HMACVariant::SHA512_256 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key) + .unwrap_or_else(|e| mac_key_error(e)); do_mac(hmac_key.create_generator(), verify_val, output_hex); } HMACVariant::SM3 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key) + .unwrap_or_else(|e| mac_key_error(e)); do_mac(hmac_key.create_generator(), verify_val, output_hex); } } @@ -95,58 +95,51 @@ pub(crate) fn mac_cmd( pub(crate) fn kmac_cmd( strength: Strength, length: usize, - customization: &Option, + customization: &[u8], key: &Option, key_file: &Option, verify_val: &Option, output_hex: bool, ) { + if length > MAX_FIXED_OUTPUT_LEN { + eprintln!( + "Error: a KMAC tag is computed whole; this command takes at most {MAX_FIXED_OUTPUT_LEN} bytes. Use kmacxof* for a long, streamed output." + ); + exit(-1); + } let key = load_mac_key(key, key_file); - let s = customization.as_deref().unwrap_or("").as_bytes(); - // The allow-weak form, as the HMAC commands use: a CLI is used for test vectors and - // scripting, where a short or all-zero key is a legitimate thing to want. match strength { Strength::Bits128 => { - let kmac_key = KMAC128Key::with_params_allow_weak(&key, s, length) - .unwrap_or_else(|e| kmac_key_error(e)); + let kmac_key = KMAC128Key::with_params_allow_weak(&key, customization, length) + .unwrap_or_else(|e| mac_key_error(e)); do_mac(kmac_key.create_generator(), verify_val, output_hex) } Strength::Bits256 => { - let kmac_key = KMAC256Key::with_params_allow_weak(&key, s, length) - .unwrap_or_else(|e| kmac_key_error(e)); + let kmac_key = KMAC256Key::with_params_allow_weak(&key, customization, length) + .unwrap_or_else(|e| mac_key_error(e)); do_mac(kmac_key.create_generator(), verify_val, output_hex) } } } -/// The key is already a MAC key, so what fails here is the tag length, which came from the -/// command line: report it the way the key errors are reported rather than through a panic. -fn kmac_key_error(e: MACError) -> ! { - eprintln!("Error: couldn't build the KMAC key: {e:?}"); - exit(-1) -} - -fn do_mac(mut mac: impl MAC, verify_val: &Option, output_hex: bool) { - // read the content to be MAC'd from stdin - let mut buf: [u8; 1024] = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - while bytes_read != 0 { - mac.do_update(&buf[..bytes_read]); - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - } +/// Streams stdin into `mac`, then writes the tag, or verifies `--verify` against it and reports +/// the outcome through the exit status (0 verified, 255 not) and an `Error:` line. +pub(crate) fn do_mac(mut mac: impl MAC, verify_val: &Option, output_hex: bool) { + stream_stdin(|chunk| mac.do_update(chunk)); - if verify_val.is_none() { - // compute a MAC value - let out = mac.do_final(); - - crate::helpers::write_bytes_or_hex(&out, output_hex); - crate::helpers::write_stdout(b"\n"); - } else { - // verify a MAC - let expected = hex::decode(verify_val.as_ref().unwrap()).unwrap_or_else(|_| { - eprintln!("Error: the MAC to verify must be hex."); - exit(-1); - }); - if mac.do_verify_final(&expected) { exit(0) } else { exit(-1) } + match verify_val { + None => { + let out = mac.do_final(); + write_bytes_or_hex(&out, output_hex); + write_stdout(b"\n"); + } + Some(verify_val) => { + let expected = decode_hex_arg(verify_val, "verify"); + if mac.do_verify_final(&expected) { + exit(0) + } + eprintln!("Error: MAC verification failed."); + exit(-1) + } } } diff --git a/cli/src/main.rs b/cli/src/main.rs index bdda1cad9..24539bbd3 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + mod aes_cbc_cmd; mod aes_ccm_cmd; mod aes_cfb8_cmd; @@ -28,11 +30,18 @@ use helpers::block_mode_helpers::CipherDirection; #[derive(Parser)] #[command(version, about, long_about=None, arg_required_else_help=true)] struct Cli { + /// Read the files a command decodes (keys, IVs and nonces, seeds, private and public keys, + /// signatures, ciphertexts, contexts, salts, IKM, AAD files), and stdin where it carries + /// one of those, as hex text rather than raw bytes. Values given inline (--key, --iv, ...) + /// are always hex. Without this flag a file is its raw bytes, however hex-like they look. + #[arg(long, global = true)] + hex_input: bool, + #[command(subcommand)] subcommands: Option, } -#[allow(non_camel_case_types)] +#[allow(non_camel_case_types, clippy::upper_case_acronyms)] #[derive(Subcommand)] enum Subcommands { /// AES-128 in CBC mode (NIST SP 800-38A Sec 6.2) with PKCS7 padding, streaming stdin to stdout. @@ -41,7 +50,7 @@ enum Subcommands { /// the output; on `decrypt` it is read back from the first 16 bytes of the input, so the two /// compose directly in a pipeline. /// - /// With `--iv` (hex) or `--iv-file` (raw bytes), the IV is supplied instead: `encrypt` uses it + /// With `--iv` (hex) or `--iv-file` (raw bytes; hex with --hex-input), the IV is supplied instead: `encrypt` uses it /// and writes ONLY the ciphertext, and `decrypt` reads ONLY ciphertext from stdin. This is for /// a format that carries the IV itself, and for reproducing published vectors. The IV must /// then be unpredictable to an attacker (SP 800-38A Appendix C) and never reused under one @@ -66,7 +75,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -76,8 +85,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex - /// decoding). If both iv and iv_file options are provided, the file will be used. + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -134,7 +142,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -143,7 +151,7 @@ enum Subcommands { #[arg(long)] nonce: Option, - /// A file containing the nonce, as raw bytes exactly as they are (no hex decoding). + /// A file containing the nonce, (raw bytes; hex with --hex-input). #[arg(long)] nonce_file: Option, @@ -174,7 +182,7 @@ enum Subcommands { /// the output; on `decrypt` it is read back from the first 16 bytes of the input, so the two /// compose directly in a pipeline. /// - /// With `--iv` (hex) or `--iv-file` (raw bytes), the IV is supplied instead: `encrypt` uses it + /// With `--iv` (hex) or `--iv-file` (raw bytes; hex with --hex-input), the IV is supplied instead: `encrypt` uses it /// and writes ONLY the ciphertext, and `decrypt` reads ONLY ciphertext from stdin. This is for /// a format that carries the IV itself, and for reproducing published vectors. The IV must /// then be unpredictable to an attacker (SP 800-38A Appendix C) and never reused under one @@ -199,7 +207,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -209,7 +217,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -230,7 +238,7 @@ enum Subcommands { /// the output; on `decrypt` it is read back from the first 16 bytes of the input, so the two /// compose directly in a pipeline. /// - /// With `--iv` (hex) or `--iv-file` (raw bytes), the IV is supplied instead: `encrypt` uses it + /// With `--iv` (hex) or `--iv-file` (raw bytes; hex with --hex-input), the IV is supplied instead: `encrypt` uses it /// and writes ONLY the ciphertext, and `decrypt` reads ONLY ciphertext from stdin. This is for /// a format that carries the IV itself, and for reproducing published vectors. The IV must /// then be unpredictable to an attacker (SP 800-38A Appendix C) and never reused under one @@ -255,7 +263,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -265,7 +273,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -285,7 +293,7 @@ enum Subcommands { /// on `decrypt` it is read back from the first 12 bytes of the input, so the two compose /// directly in a pipeline. Note that this is 12 bytes, not the 16 the other modes write. /// - /// With `--iv` (hex) or `--iv-file` (raw bytes), the 12-byte nonce is supplied instead: + /// With `--iv` (hex) or `--iv-file` (raw bytes; hex with --hex-input), the 12-byte nonce is supplied instead: /// `encrypt` uses it and writes ONLY the ciphertext, and `decrypt` reads ONLY ciphertext from /// stdin. The counter block is then `nonce || 00000000`, which is how an OpenSSL `-iv` whose /// low four bytes are zero lines up with it. The nonce MUST be unique per message under one @@ -311,7 +319,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -321,8 +329,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 12-byte nonce, as raw bytes exactly as they are (no hex - /// decoding). If both iv and iv_file options are provided, the file will be used. + /// A file containing the 12-byte nonce, (raw bytes; hex with --hex-input). If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -356,7 +363,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -376,13 +383,13 @@ enum Subcommands { /// back from the first 12 bytes of input and streams the rest, checking the tag once input is /// exhausted. (SP 800-38D calls this value the IV, and defines it as a nonce.) /// - /// With `--iv` (hex) or `--iv-file` (raw bytes), the IV is supplied instead: `encrypt` uses it + /// With `--iv` (hex) or `--iv-file` (raw bytes; hex with --hex-input), the IV is supplied instead: `encrypt` uses it /// and writes ONLY `ciphertext || tag`, and `decrypt` reads ONLY that from stdin. The IV MUST /// be unique per message under one key (Sec 8), and a repeat is worse here than merely /// unwise: it lets an attacker recover the hash subkey (Appendix A). The default takes care of /// that; with `--iv` it is on the caller. /// - /// `--aad` (hex) or `--aad-file` (raw bytes) supply the additional authenticated data, + /// `--aad` (hex) or `--aad-file` (raw bytes; hex with --hex-input) supply the additional authenticated data, /// which is covered by the tag but not encrypted; if neither is given, AAD is empty. /// /// Input may be ANY length: GCM needs no padding. @@ -403,7 +410,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -413,7 +420,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 12-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 12-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -445,7 +452,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -455,8 +462,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex - /// decoding). If both iv and iv_file options are provided, the file will be used. + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -478,7 +484,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -487,7 +493,7 @@ enum Subcommands { #[arg(long)] nonce: Option, - /// A file containing the nonce, as raw bytes exactly as they are (no hex decoding). + /// A file containing the nonce, (raw bytes; hex with --hex-input). #[arg(long)] nonce_file: Option, @@ -522,7 +528,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -532,7 +538,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -555,7 +561,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -565,7 +571,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -588,7 +594,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -598,8 +604,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 12-byte nonce, as raw bytes exactly as they are (no hex - /// decoding). If both iv and iv_file options are provided, the file will be used. + /// A file containing the 12-byte nonce, (raw bytes; hex with --hex-input). If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -621,7 +626,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -644,7 +649,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -654,7 +659,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 12-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 12-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -686,7 +691,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -696,8 +701,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex - /// decoding). If both iv and iv_file options are provided, the file will be used. + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -719,7 +723,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -728,7 +732,7 @@ enum Subcommands { #[arg(long)] nonce: Option, - /// A file containing the nonce, as raw bytes exactly as they are (no hex decoding). + /// A file containing the nonce, (raw bytes; hex with --hex-input). #[arg(long)] nonce_file: Option, @@ -763,7 +767,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -773,7 +777,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -796,7 +800,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -806,7 +810,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -829,7 +833,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -839,8 +843,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 12-byte nonce, as raw bytes exactly as they are (no hex - /// decoding). If both iv and iv_file options are provided, the file will be used. + /// A file containing the 12-byte nonce, (raw bytes; hex with --hex-input). If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -862,7 +865,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -885,7 +888,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -895,7 +898,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 12-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 12-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -941,7 +944,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 128-bit key in hex or binary. + /// A file containing the 128-bit key (raw bytes; hex with --hex-input). #[arg(long)] key_file: Option, @@ -950,13 +953,17 @@ enum Subcommands { #[arg(long)] nonce: Option, - /// A file containing a 128-bit nonce in hex or binary; the same hazardous override. + /// A file containing a 128-bit nonce (raw bytes; hex with --hex-input); the same hazardous override. #[arg(long)] nonce_file: Option, /// Associated data in hex (authenticated but not encrypted). #[arg(long)] - ad: Option, + aad: Option, + + /// A file holding the associated data (raw bytes; hex with --hex-input). + #[arg(long)] + aad_file: Option, #[arg(short, long)] direction: CipherDirection, @@ -972,10 +979,14 @@ enum Subcommands { /// Length of the output in bytes. length: usize, - /// Customization string in hex (optional). + /// Customization string Z, as the characters given (optional). #[arg(long)] customization: Option, + /// Customization string Z in hex, for bytes that are not text. + #[arg(long)] + customization_hex: Option, + #[arg(short)] /// Output in hex format. x: bool, @@ -1013,19 +1024,19 @@ enum Subcommands { action: MLDSAAction, #[arg(long)] - /// The file containing context string (in hex) for signing or verifying + /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, #[arg(long)] - /// The private key file (in hex or binary) for signing + /// The private key file (raw bytes; hex with --hex-input) for signing skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for verifying + /// The public key file (raw bytes; hex with --hex-input) for verifying pkfile: Option, #[arg(long)] - /// The signature value file (in hex or binary) for verifying + /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, #[arg(short)] @@ -1038,19 +1049,19 @@ enum Subcommands { action: MLDSAAction, #[arg(long)] - /// The file containing context string (in hex) for signing or verifying + /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, #[arg(long)] - /// The private key file (in hex or binary) for signing + /// The private key file (raw bytes; hex with --hex-input) for signing skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for verifying + /// The public key file (raw bytes; hex with --hex-input) for verifying pkfile: Option, #[arg(long)] - /// The signature value file (in hex or binary) for verifying + /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, #[arg(short)] @@ -1063,19 +1074,19 @@ enum Subcommands { action: MLDSAAction, #[arg(long)] - /// The file containing context string (in hex) for signing or verifying + /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, #[arg(long)] - /// The private key file (in hex or binary) for signing + /// The private key file (raw bytes; hex with --hex-input) for signing skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for verifying + /// The public key file (raw bytes; hex with --hex-input) for verifying pkfile: Option, #[arg(long)] - /// The signature value file (in hex or binary) for verifying + /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, #[arg(short)] @@ -1273,13 +1284,13 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the MAC key in binary. + /// A file containing the MAC key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, /// A MAC value to be verified. - /// The command will output either 0 for success or -1 for verification failure. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. #[arg(short, long)] verify: Option, @@ -1298,13 +1309,13 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the MAC key in binary. + /// A file containing the MAC key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, /// A MAC value to be verified. - /// The command will output either 0 for success or -1 for verification failure. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. #[arg(short, long)] verify: Option, @@ -1323,13 +1334,13 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the MAC key in binary. + /// A file containing the MAC key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, /// A MAC value to be verified. - /// The command will output either 0 for success or -1 for verification failure. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. #[arg(short, long)] verify: Option, @@ -1348,13 +1359,13 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the MAC key in binary. + /// A file containing the MAC key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, /// A MAC value to be verified. - /// The command will output either 0 for success or -1 for verification failure. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. #[arg(short, long)] verify: Option, @@ -1373,13 +1384,13 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the MAC key in binary. + /// A file containing the MAC key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, /// A MAC value to be verified. - /// The command will output either 0 for success or -1 for verification failure. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. #[arg(short, long)] verify: Option, @@ -1391,20 +1402,29 @@ enum Subcommands { /// Compute or verify a KMAC128 (NIST SP 800-185 Sec 4) over the content provided on stdin. /// The tag length and customization string are bound into the computation, so the verifier /// must use the same values. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. KMAC128 { /// Length of the tag in bytes, at least 4 (SP 800-185 Sec 8.4.2). length: usize, #[arg(short = 's', long)] - /// Customization string, domain-separating this use of KMAC from another. + /// Customization string S, as the characters given, domain-separating this use of KMAC + /// from another. customization: Option, - #[arg(short, long)] - /// The key, in hex. - key: Option, + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] - /// File containing the key, as raw bytes. + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] key_file: Option, #[arg(short, long)] @@ -1418,20 +1438,29 @@ enum Subcommands { /// Compute or verify a KMAC256 (NIST SP 800-185 Sec 4) over the content provided on stdin. /// See kmac128. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. KMAC256 { /// Length of the tag in bytes, at least 4 (SP 800-185 Sec 8.4.2). length: usize, #[arg(short = 's', long)] - /// Customization string, domain-separating this use of KMAC from another. + /// Customization string S, as the characters given, domain-separating this use of KMAC + /// from another. customization: Option, - #[arg(short, long)] - /// The key, in hex. - key: Option, + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] - /// File containing the key, as raw bytes. + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] key_file: Option, #[arg(short, long)] @@ -1448,19 +1477,19 @@ enum Subcommands { action: MLDSAAction, #[arg(long)] - /// The file containing context string (in hex) for signing or verifying + /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, #[arg(long)] - /// The private key file (in hex or binary) for signing + /// The private key file (raw bytes; hex with --hex-input) for signing skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for verifying + /// The public key file (raw bytes; hex with --hex-input) for verifying pkfile: Option, #[arg(long)] - /// The signature value file (in hex or binary) for verifying + /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, #[arg(short)] @@ -1473,19 +1502,19 @@ enum Subcommands { action: MLDSAAction, #[arg(long)] - /// The file containing context string (in hex) for signing or verifying + /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, #[arg(long)] - /// The private key file (in hex or binary) for signing + /// The private key file (raw bytes; hex with --hex-input) for signing skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for verifying + /// The public key file (raw bytes; hex with --hex-input) for verifying pkfile: Option, #[arg(long)] - /// The signature value file (in hex or binary) for verifying + /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, #[arg(short)] @@ -1498,19 +1527,19 @@ enum Subcommands { action: MLDSAAction, #[arg(long)] - /// The file containing context string (in hex) for signing or verifying + /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, #[arg(long)] - /// The private key file (in hex or binary) for signing + /// The private key file (raw bytes; hex with --hex-input) for signing skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for verifying + /// The public key file (raw bytes; hex with --hex-input) for verifying pkfile: Option, #[arg(long)] - /// The signature value file (in hex or binary) for verifying + /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, #[arg(short)] @@ -1523,15 +1552,15 @@ enum Subcommands { action: mlkem_cmd::MLKEMAction, #[arg(long)] - /// The private key file (in hex or binary) for decaps + /// The private key file (raw bytes; hex with --hex-input) for decaps skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for encaps + /// The public key file (raw bytes; hex with --hex-input) for encaps pkfile: Option, #[arg(long)] - /// The ciphertext value file (in hex or binary) either for encaps to output to, or for decaps to read from. + /// The ciphertext value file (raw bytes; hex with --hex-input) either for encaps to output to, or for decaps to read from. ctfile: Option, #[arg(short)] @@ -1544,15 +1573,15 @@ enum Subcommands { action: mlkem_cmd::MLKEMAction, #[arg(long)] - /// The private key file (in hex or binary) for decaps + /// The private key file (raw bytes; hex with --hex-input) for decaps skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for encaps + /// The public key file (raw bytes; hex with --hex-input) for encaps pkfile: Option, #[arg(long)] - /// The ciphertext value file (in hex or binary) either for encaps to output to, or for decaps to read from. + /// The ciphertext value file (raw bytes; hex with --hex-input) either for encaps to output to, or for decaps to read from. ctfile: Option, #[arg(short)] @@ -1565,15 +1594,15 @@ enum Subcommands { action: mlkem_cmd::MLKEMAction, #[arg(long)] - /// The private key file (in hex or binary) for decaps + /// The private key file (raw bytes; hex with --hex-input) for decaps skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for encaps + /// The public key file (raw bytes; hex with --hex-input) for encaps pkfile: Option, #[arg(long)] - /// The ciphertext value file (in hex or binary) either for encaps to output to, or for decaps to read from. + /// The ciphertext value file (raw bytes; hex with --hex-input) either for encaps to output to, or for decaps to read from. ctfile: Option, #[arg(short)] @@ -1594,9 +1623,13 @@ enum Subcommands { block_size: usize, #[arg(short = 's', long)] - /// Customization string. + /// Customization string S, as the characters given. customization: Option, + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + #[arg(short)] /// Output the hashes in hex format. x: bool, @@ -1612,9 +1645,13 @@ enum Subcommands { block_size: usize, #[arg(short = 's', long)] - /// Customization string. + /// Customization string S, as the characters given. customization: Option, + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + #[arg(short)] /// Output the hashes in hex format. x: bool, @@ -1648,7 +1685,7 @@ enum Subcommands { x: bool, }, - /// Perform SHA3-256 of the content provided on stdin. + /// Perform SHA3-384 of the content provided on stdin. /// Supports streaming update for low memory footprint. SHA3_384 { #[arg(short)] @@ -1656,7 +1693,7 @@ enum Subcommands { x: bool, }, - /// Perform SHA3-256 of the content provided on stdin. + /// Perform SHA3-512 of the content provided on stdin. /// Supports streaming update for low memory footprint. SHA3_512 { #[arg(short)] @@ -1754,9 +1791,13 @@ enum Subcommands { elements: Vec, #[arg(short = 's', long)] - /// Customization string. + /// Customization string S, as the characters given. customization: Option, + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + #[arg(short)] /// Output the hashes in hex format. x: bool, @@ -1772,9 +1813,13 @@ enum Subcommands { elements: Vec, #[arg(short = 's', long)] - /// Customization string. + /// Customization string S, as the characters given. customization: Option, + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + #[arg(short)] /// Output the hashes in hex format. x: bool, @@ -1793,6 +1838,7 @@ fn main() { fn run() { let cli = Cli::parse(); + helpers::set_hex_input(cli.hex_input); match &cli.subcommands { Some(Subcommands::AES128_CBC { direction, key, key_file, iv, iv_file, x }) => { @@ -1921,12 +1967,22 @@ fn run() { }) => { aes_gcm_cmd::aes256_gcm_cmd(direction, key, key_file, iv, iv_file, aad, aad_file, *x); } - Some(Subcommands::AsconAEAD128 { key, key_file, nonce, nonce_file, ad, direction, x }) => { + Some(Subcommands::AsconAEAD128 { + key, + key_file, + nonce, + nonce_file, + aad, + aad_file, + direction, + x, + }) => { let decrypt = matches!(direction, CipherDirection::Decrypt); - ascon_cmd::aead128_cmd(key, key_file, nonce, nonce_file, ad, decrypt, *x); + ascon_cmd::aead128_cmd(key, key_file, nonce, nonce_file, aad, aad_file, decrypt, *x); } - Some(Subcommands::AsconCXOF128 { length, customization, x }) => { - ascon_cmd::cxof128_cmd(customization, *length, *x); + Some(Subcommands::AsconCXOF128 { length, customization, customization_hex, x }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + ascon_cmd::cxof128_cmd(&s, *length, *x); } Some(Subcommands::AsconHash256 { x }) => { ascon_cmd::hash256_cmd(*x); @@ -2077,11 +2133,29 @@ fn run() { Some(Subcommands::HMAC_SM3 { key, key_file, verify, x }) => { mac_cmd::mac_cmd(HMACVariant::SM3, key, key_file, verify, *x) } - Some(Subcommands::KMAC128 { length, customization, key, key_file, verify, x }) => { - mac_cmd::kmac_cmd(Strength::Bits128, *length, customization, key, key_file, verify, *x) + Some(Subcommands::KMAC128 { + length, + customization, + customization_hex, + key, + key_file, + verify, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + mac_cmd::kmac_cmd(Strength::Bits128, *length, &s, key, key_file, verify, *x) } - Some(Subcommands::KMAC256 { length, customization, key, key_file, verify, x }) => { - mac_cmd::kmac_cmd(Strength::Bits256, *length, customization, key, key_file, verify, *x) + Some(Subcommands::KMAC256 { + length, + customization, + customization_hex, + key, + key_file, + verify, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + mac_cmd::kmac_cmd(Strength::Bits256, *length, &s, key, key_file, verify, *x) } Some(Subcommands::MLDSA44 { action, ctxfile, skfile, pkfile, sigfile, x }) => { mldsa_cmd::mldsa_cmd( @@ -2128,11 +2202,25 @@ fn run() { Some(Subcommands::MLKEM1024 { action, skfile, pkfile, ctfile, x }) => { mlkem_cmd::mlkem_cmd(&mlkem_cmd::MLKEM1024_SET, action, skfile, pkfile, ctfile, *x); } - Some(Subcommands::PARALLELHASH128 { length, block_size, customization, x }) => { - sha3_cmd::parallelhash_cmd(Strength::Bits128, *length, *block_size, customization, *x); + Some(Subcommands::PARALLELHASH128 { + length, + block_size, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::parallelhash_cmd(Strength::Bits128, *length, *block_size, &s, *x); } - Some(Subcommands::PARALLELHASH256 { length, block_size, customization, x }) => { - sha3_cmd::parallelhash_cmd(Strength::Bits256, *length, *block_size, customization, *x); + Some(Subcommands::PARALLELHASH256 { + length, + block_size, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::parallelhash_cmd(Strength::Bits256, *length, *block_size, &s, *x); } Some(Subcommands::RNG { len, x }) => rng_cmd::rng_cmd(*len, *x), Some(Subcommands::SHA3_224 { x }) => { @@ -2174,11 +2262,25 @@ fn run() { Some(Subcommands::SM3 { x }) => { sm3_cmd::sm3_cmd(*x); } - Some(Subcommands::TUPLEHASH128 { length, elements, customization, x }) => { - sha3_cmd::tuplehash_cmd(Strength::Bits128, *length, elements, customization, *x); + Some(Subcommands::TUPLEHASH128 { + length, + elements, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::tuplehash_cmd(Strength::Bits128, *length, elements, &s, *x); } - Some(Subcommands::TUPLEHASH256 { length, elements, customization, x }) => { - sha3_cmd::tuplehash_cmd(Strength::Bits256, *length, elements, customization, *x); + Some(Subcommands::TUPLEHASH256 { + length, + elements, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::tuplehash_cmd(Strength::Bits256, *length, elements, &s, *x); } None => { eprintln!("No command provided. See -h") diff --git a/cli/src/mldsa_cmd.rs b/cli/src/mldsa_cmd.rs index 54018ea4d..d8ba3628c 100644 --- a/cli/src/mldsa_cmd.rs +++ b/cli/src/mldsa_cmd.rs @@ -2,17 +2,22 @@ //! over the three parameter sets, which differ only in their key types, their lengths and the //! functions that make and check a key pair. Those come in through a [`ParameterSet`], so a //! length or a function can no longer be right for two sets and wrong for the third. +//! +//! Private keys and seeds are read through the zeroized secret path; public keys, signatures +//! and contexts through the plain one. All of them are raw bytes, or hex with `--hex-input`. -use crate::helpers::{parse_seed, read_from_file, read_from_file_or_stdin, write_bytes_or_hex}; +use crate::helpers::{ + parse_seed, read_input_file, read_secret_file, read_secret_file_or_stdin, seed_error, + stream_stdin, write_bytes_or_hex, +}; use bouncycastle::core::errors::SignatureError; -use bouncycastle::core::key_material::KeyMaterial; +use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait}; use bouncycastle::core::signature_key::{ PreHashSigningKey, PreHashVerifyingKey, SignatureAlgorithm, SigningKey, VerifyingKey, }; use bouncycastle::core::traits::{ RNG, SignaturePrivateKey, SignaturePublicKey, SignatureSigner, SignatureVerifier, }; -use bouncycastle::hex; use bouncycastle::mldsa::{ MLDSA_SEED_LEN, MLDSA44, MLDSA44_PK_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN, MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65, MLDSA65_PK_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN, MLDSA65PrivateKey, @@ -23,33 +28,30 @@ use bouncycastle::rng::HashDRBG_SHA512; use bouncycastle::sha2::SHA512; use clap::ValueEnum; use core::marker::PhantomData; -use std::io; -use std::io::Read; use std::process::exit; #[derive(ValueEnum, Clone, Debug)] pub(crate) enum MLDSAAction { /// Generate and output a new private key Keygen, - /// Generate and output a private key from a seed read from stdin. - /// Accepts either binary or hex. + /// Generate and output a private key from a 32-byte seed read from stdin (or --skfile). KeygenFromSeed, - /// Generate and output a new public key from a private key read from stdin. - /// Accepts either binary or hex. + /// Generate and output a new public key from a private key read from stdin (or --skfile). PkFromSk, - /// Accepts a sk and pk, and checks that they match. + /// Accepts a sk (stdin or --skfile) and a pk (--pkfile), and checks that they match. CheckConsistency, - /// Sign a message read from stdin with a private key file and output the signature. - /// Accepts private key as full or seed, binary or hex. + /// Sign a message read from stdin with the private key in --skfile and output the signature. + /// Accepts the private key as full or seed. Sign, - /// Verify a message read from stdin with a public key file and a signature file - /// Accepts the public key and signature as binary or hex. + /// Verify a message read from stdin against the public key in --pkfile and the signature in + /// --sigfile. Verify, } /// A parameter set as the command sees it: its name for messages, the algorithm marker the /// signing traits are keyed by, and the key-pair operations the core traits do not carry. The /// three sets below are the only instances. +#[allow(clippy::type_complexity)] // the function-pointer fields are the table's point pub(crate) struct ParameterSet< A, PK, @@ -115,6 +117,7 @@ pub(crate) const MLDSA87_SET: ParameterSet< }; /// `prehash` selects HashML-DSA with SHA-512 (FIPS 204 Section 5.4) over pure ML-DSA. +#[allow(clippy::too_many_arguments)] // one flag per file the actions take pub(crate) fn mldsa_cmd< A: SignatureAlgorithm, PK: SignaturePublicKey + VerifyingKey + PreHashVerifyingKey, @@ -134,49 +137,49 @@ pub(crate) fn mldsa_cmd< ) { match action { MLDSAAction::Keygen => { - let (_pk, sk) = (set.keygen)(&mut HashDRBG_SHA512::new_from_os()).unwrap(); + let (_pk, sk) = (set.keygen)(&mut HashDRBG_SHA512::new_from_os()) + .unwrap_or_else(|e| keygen_error(e)); write_bytes_or_hex(&sk.encode(), output_hex); } MLDSAAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = match parse_seed::(&buf) { - Ok(seed) => seed, - Err(()) => { - eprintln!( - "Error: input could not be parsed as a {MLDSA_SEED_LEN} byte seed in either hex or bin" - ); - exit(-1); - } - }; - let (_pk, sk) = (set.keygen_from_seed)(&seed).unwrap(); + let input = read_secret_file_or_stdin(skfile); + let seed = parse_seed::(input.ref_to_bytes()) + .unwrap_or_else(|e| seed_error::(e)); + let (_pk, sk) = (set.keygen_from_seed)(&seed).unwrap_or_else(|e| keygen_error(e)); write_bytes_or_hex(&sk.encode(), output_hex); } MLDSAAction::PkFromSk => { - let sk = parse_sk(set, &read_from_file_or_stdin(skfile)); + let sk = parse_sk(set, read_secret_file_or_stdin(skfile).ref_to_bytes()); write_bytes_or_hex(&(set.pk_of)(&sk).encode(), output_hex); } MLDSAAction::CheckConsistency => { - let sk = parse_sk(set, &read_from_file_or_stdin(skfile)); + let sk = parse_sk(set, read_secret_file_or_stdin(skfile).ref_to_bytes()); let pk = parse_pk(set, &read_required_file(pkfile, "pkfile")); match (set.keypair_consistency_check)(&pk, &sk) { Ok(_) => crate::helpers::println_stdout("SUCCESS: pk and sk match."), Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); + eprintln!("Error: the public and private keys do not match."); exit(-1); } } } MLDSAAction::Sign => { - let sk = parse_sk(set, &read_required_file(skfile, "skfile")); + let Some(skfile) = skfile else { + eprintln!("Error: no skfile provided."); + exit(-1); + }; + let sk = parse_sk(set, read_secret_file(skfile).ref_to_bytes()); let ctx = read_ctx(ctxfile); let mut rng = HashDRBG_SHA512::new_from_os(); if prehash { - sign_stream( - sk.create_ph_signer::(&mut rng, Some(&ctx)).unwrap(), - output_hex, - ); + let signer = sk + .create_ph_signer::(&mut rng, Some(&ctx)) + .unwrap_or_else(|e| signer_error(e)); + sign_stream(signer, output_hex); } else { - sign_stream(sk.create_signer(&mut rng, Some(&ctx)).unwrap(), output_hex); + let signer = + sk.create_signer(&mut rng, Some(&ctx)).unwrap_or_else(|e| signer_error(e)); + sign_stream(signer, output_hex); } } MLDSAAction::Verify => { @@ -184,55 +187,59 @@ pub(crate) fn mldsa_cmd< let ctx = read_ctx(ctxfile); let sig = read_required_file(sigfile, "sigfile"); if prehash { - verify_stream(pk.create_ph_verifier::(Some(&ctx)).unwrap(), &sig); + let verifier = + pk.create_ph_verifier::(Some(&ctx)).unwrap_or_else(|e| signer_error(e)); + verify_stream(verifier, &sig); } else { - verify_stream(pk.create_verifier(Some(&ctx)).unwrap(), &sig); + let verifier = pk.create_verifier(Some(&ctx)).unwrap_or_else(|e| signer_error(e)); + verify_stream(verifier, &sig); } } } } +fn keygen_error(e: SignatureError) -> ! { + eprintln!("Error: key generation failed: {e:?}"); + exit(-1); +} + +/// A signer or verifier refused to start: the context is over 255 bytes (FIPS 204 Algorithm 2 +/// line 1), or the RNG failed. +fn signer_error(e: SignatureError) -> ! { + eprintln!("Error: couldn't start the signature: {e:?}"); + exit(-1); +} + /// Streams stdin into `signer` and writes the signature. fn sign_stream(mut signer: impl SignatureSigner, output_hex: bool) { - let mut buf = [0u8; 1024]; - loop { - let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - if n == 0 { - break; - } - signer.do_update(&buf[..n]); - } - let sig = signer.do_final().unwrap(); + stream_stdin(|chunk| signer.do_update(chunk)); + let sig = signer.do_final().unwrap_or_else(|e| { + eprintln!("Error: signing failed: {e:?}"); + exit(-1); + }); write_bytes_or_hex(&sig, output_hex); } /// Streams stdin into `verifier` and reports whether `sig` verifies, through the exit status. fn verify_stream(mut verifier: impl SignatureVerifier, sig: &[u8]) { - let mut buf = [0u8; 1024]; - loop { - let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - if n == 0 { - break; - } - verifier.do_update(&buf[..n]); - } + stream_stdin(|chunk| verifier.do_update(chunk)); if verifier.do_verify_final(sig).is_ok() { crate::helpers::println_stdout("Signature is valid."); } else { - eprintln!("Signature is invalid."); + eprintln!("Error: the signature is invalid."); exit(-1); } } /// `--ctxfile`'s bytes, or the empty context (FIPS 204's default) without one. fn read_ctx(ctxfile: &Option) -> Vec { - ctxfile.as_ref().map(|f| read_from_file(f)).unwrap_or_default() + ctxfile.as_ref().map(|f| read_input_file(f)).unwrap_or_default() } -/// `--skfile` and the like where the command cannot fall back to stdin. +/// `--pkfile` and the like where the command cannot fall back to stdin. fn read_required_file(file: &Option, flag: &str) -> Vec { match file { - Some(file) => read_from_file(file), + Some(file) => read_input_file(file), None => { eprintln!("Error: no {flag} provided."); exit(-1); @@ -240,8 +247,7 @@ fn read_required_file(file: &Option, flag: &str) -> Vec { } } -/// A private key from its bytes: the hex of a full key, a full key, or a seed, tried in that -/// order (the hex case only arises when the file's hex-or-raw guess declined to decode it). +/// A private key from its bytes: a full key, or a seed. fn parse_sk< A, PK, @@ -253,12 +259,6 @@ fn parse_sk< set: &ParameterSet, bytes: &[u8], ) -> SK { - if bytes.len() >= 2 * SK_LEN - && let Ok(decoded) = hex::decode(&bytes[..2 * SK_LEN]) - && let Ok(sk) = SK::from_bytes(&decoded) - { - return sk; - } if bytes.len() == SK_LEN && let Ok(sk) = SK::from_bytes(bytes) { @@ -269,11 +269,14 @@ fn parse_sk< { return sk; } - eprintln!("Error: couldn't parse the input as a valid {} private key or seed.", set.name); + eprintln!( + "Error: couldn't parse the input as a valid {} private key or seed (raw bytes; hex needs --hex-input).", + set.name + ); exit(-1); } -/// A public key from its bytes: the hex of a key or the key itself. +/// A public key from its bytes. fn parse_pk< A, PK: SignaturePublicKey, @@ -285,17 +288,14 @@ fn parse_pk< set: &ParameterSet, bytes: &[u8], ) -> PK { - if bytes.len() >= 2 * PK_LEN - && let Ok(decoded) = hex::decode(&bytes[..2 * PK_LEN]) - && let Ok(pk) = PK::from_bytes(&decoded) - { - return pk; - } if bytes.len() == PK_LEN && let Ok(pk) = PK::from_bytes(bytes) { return pk; } - eprintln!("Error: couldn't parse the input as a valid {} public key.", set.name); + eprintln!( + "Error: couldn't parse the input as a valid {} public key (raw bytes; hex needs --hex-input).", + set.name + ); exit(-1); } diff --git a/cli/src/mlkem_cmd.rs b/cli/src/mlkem_cmd.rs index 8fa406493..eb5ca5faf 100644 --- a/cli/src/mlkem_cmd.rs +++ b/cli/src/mlkem_cmd.rs @@ -2,17 +2,19 @@ //! three parameter sets, which differ only in their key types, their lengths and the functions //! that make and check a key pair. Those come in through a [`ParameterSet`], so a length or a //! function can no longer be right for two sets and wrong for the third. +//! +//! Private keys and seeds are read through the zeroized secret path; public keys and +//! ciphertexts through the plain one. All of them are raw bytes, or hex with `--hex-input`. use crate::helpers::{ - parse_seed, read_from_file, read_from_file_or_stdin, write_bytes_or_hex, - write_bytes_or_hex_to_file, + parse_seed, read_input_file, read_input_file_or_stdin, read_secret_file_or_stdin, seed_error, + write_bytes_or_hex, write_bytes_or_hex_to_file, }; use bouncycastle::core::errors::KEMError; use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait}; use bouncycastle::core::traits::{ KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, RNG, }; -use bouncycastle::hex; use bouncycastle::mlkem::{ MLKEM512, MLKEM512_CT_LEN, MLKEM512_PK_LEN, MLKEM512_SK_LEN, MLKEM512PrivateKey, MLKEM512PublicKey, MLKEM768, MLKEM768_CT_LEN, MLKEM768_PK_LEN, MLKEM768_SK_LEN, @@ -57,6 +59,7 @@ pub(crate) enum MLKEMAction { /// A parameter set as the command sees it: its name for messages, and the operations the key /// types and the `KEM*` traits do not carry on their own. The three sets below are the only /// instances. +#[allow(clippy::type_complexity)] // the function-pointer fields are the table's point pub(crate) struct ParameterSet< PK, SK, @@ -142,41 +145,38 @@ pub(crate) fn mlkem_cmd< ) { match action { MLKEMAction::Keygen => { - let (_pk, sk) = (set.keygen)(&mut HashDRBG_SHA512::new_from_os()).unwrap(); + let (_pk, sk) = (set.keygen)(&mut HashDRBG_SHA512::new_from_os()) + .unwrap_or_else(|e| keygen_error(e)); write_bytes_or_hex(&sk.encode(), output_hex); } MLKEMAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = match parse_seed::(&buf) { - Ok(seed) => seed, - Err(()) => { - eprintln!( - "Error: input could not be parsed as a {SEED_LEN} byte seed in either hex or bin" - ); - exit(-1); - } - }; - let (_pk, sk) = (set.keygen_from_seed)(&seed).unwrap(); + let input = read_secret_file_or_stdin(skfile); + let seed = parse_seed::(input.ref_to_bytes()) + .unwrap_or_else(|e| seed_error::(e)); + let (_pk, sk) = (set.keygen_from_seed)(&seed).unwrap_or_else(|e| keygen_error(e)); write_bytes_or_hex(&sk.encode(), output_hex); } MLKEMAction::PkFromSk => { - let sk = parse_sk(set, &read_from_file_or_stdin(skfile)); + let sk = parse_sk(set, read_secret_file_or_stdin(skfile).ref_to_bytes()); write_bytes_or_hex(&(set.pk_of)(&sk).encode(), output_hex); } MLKEMAction::CheckConsistency => { - let sk = parse_sk(set, &read_from_file_or_stdin(skfile)); + let sk = parse_sk(set, read_secret_file_or_stdin(skfile).ref_to_bytes()); let pk = parse_pk(set, &read_required_file(pkfile, "pkfile")); match (set.keypair_consistency_check)(&pk, &sk) { Ok(_) => crate::helpers::println_stdout("SUCCESS: pk and sk match."), Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); + eprintln!("Error: the public and private keys do not match."); exit(-1); } } } MLKEMAction::Encaps => { - let pk = parse_pk(set, &read_from_file_or_stdin(pkfile)); - let (ss, ct) = (set.encaps)(&pk).unwrap(); + let pk = parse_pk(set, &read_input_file_or_stdin(pkfile)); + let (ss, ct) = (set.encaps)(&pk).unwrap_or_else(|e| { + eprintln!("Error: encapsulation failed: {e:?}"); + exit(-1); + }); if let Some(ctfile) = ctfile { // write ct to file and ss to stdout write_bytes_or_hex_to_file(&ct, ctfile, output_hex); @@ -189,14 +189,18 @@ pub(crate) fn mlkem_cmd< } } MLKEMAction::Decaps => { - let sk = parse_sk(set, &read_from_file_or_stdin(skfile)); - let ct = read_from_file_or_stdin(ctfile); + let sk = parse_sk(set, read_secret_file_or_stdin(skfile).ref_to_bytes()); + let ct = read_input_file_or_stdin(ctfile); if ct.len() != CT_LEN { - eprintln!("Error: ciphertexts is not the correct length."); + eprintln!( + "Error: the ciphertext is {} bytes; an {} ciphertext is {CT_LEN}.", + ct.len(), + set.name + ); exit(-1); } let ss = (set.decaps)(&sk, &ct).unwrap_or_else(|e| { - eprintln!("{:?}", e); + eprintln!("Error: decapsulation failed: {e:?}"); exit(-1); }); write_bytes_or_hex(ss.ref_to_bytes(), output_hex); @@ -204,10 +208,15 @@ pub(crate) fn mlkem_cmd< } } +fn keygen_error(e: KEMError) -> ! { + eprintln!("Error: key generation failed: {e:?}"); + exit(-1); +} + /// `--pkfile` and the like where the command cannot fall back to stdin. fn read_required_file(file: &Option, flag: &str) -> Vec { match file { - Some(file) => read_from_file(file), + Some(file) => read_input_file(file), None => { eprintln!("Error: no {flag} provided."); exit(-1); @@ -215,8 +224,7 @@ fn read_required_file(file: &Option, flag: &str) -> Vec { } } -/// A private key from its bytes: the hex of a full key, a full key, or a seed, tried in that -/// order (the hex case only arises when the file's hex-or-raw guess declined to decode it). +/// A private key from its bytes: a full key, or a seed. fn parse_sk< PK: KEMPublicKey, SK: KEMPrivateKey, @@ -228,12 +236,6 @@ fn parse_sk< set: &ParameterSet, bytes: &[u8], ) -> SK { - if bytes.len() >= 2 * SK_LEN - && let Ok(decoded) = hex::decode(&bytes[..2 * SK_LEN]) - && let Ok(sk) = SK::from_bytes(&decoded) - { - return sk; - } if bytes.len() == SK_LEN && let Ok(sk) = SK::from_bytes(bytes) { @@ -244,11 +246,14 @@ fn parse_sk< { return sk; } - eprintln!("Error: couldn't parse the input as a valid {} private key or seed.", set.name); + eprintln!( + "Error: couldn't parse the input as a valid {} private key or seed (raw bytes; hex needs --hex-input).", + set.name + ); exit(-1); } -/// A public key from its bytes: the hex of a key or the key itself. +/// A public key from its bytes. fn parse_pk< PK: KEMPublicKey, SK: KEMPrivateKey, @@ -260,17 +265,14 @@ fn parse_pk< set: &ParameterSet, bytes: &[u8], ) -> PK { - if bytes.len() >= 2 * PK_LEN - && let Ok(decoded) = hex::decode(&bytes[..2 * PK_LEN]) - && let Ok(pk) = PK::from_bytes(&decoded) - { - return pk; - } if bytes.len() == PK_LEN && let Ok(pk) = PK::from_bytes(bytes) { return pk; } - eprintln!("Error: couldn't parse the input as a valid {} public key.", set.name); + eprintln!( + "Error: couldn't parse the input as a valid {} public key (raw bytes; hex needs --hex-input).", + set.name + ); exit(-1); } diff --git a/cli/src/rng_cmd.rs b/cli/src/rng_cmd.rs index a05d9d751..b16f1a011 100644 --- a/cli/src/rng_cmd.rs +++ b/cli/src/rng_cmd.rs @@ -1,6 +1,7 @@ use bouncycastle::core::traits::RNG; use bouncycastle::factory::AlgorithmFactory; use bouncycastle::factory::rng_factory::RNGFactory; +use std::process::exit; use crate::helpers::write_bytes_or_hex; @@ -11,7 +12,10 @@ pub(crate) fn rng_cmd(len: Option, output_hex: bool) { let loop_forever = len.is_none(); let mut bytes_left_to_write = len.unwrap_or(u32::MAX) as usize; while loop_forever || bytes_left_to_write > 0 { - rng.next_bytes_out(&mut buf).unwrap(); + rng.next_bytes_out(&mut buf).unwrap_or_else(|e| { + eprintln!("Error: the DRBG refused to generate: {e:?}"); + exit(-1); + }); if bytes_left_to_write < buf.len() { buf.truncate(bytes_left_to_write); diff --git a/cli/src/sha2_cmd.rs b/cli/src/sha2_cmd.rs index f2fb889ad..73794ec60 100644 --- a/cli/src/sha2_cmd.rs +++ b/cli/src/sha2_cmd.rs @@ -1,7 +1,4 @@ -use bouncycastle::core::traits::Hash; -use std::io; -use std::io::Read; - +use crate::helpers::stream_hash; use bouncycastle::sha2::{SHA224, SHA256, SHA384, SHA512, SHA512_224, SHA512_256}; #[allow(non_camel_case_types)] @@ -16,27 +13,11 @@ pub(crate) enum SHA2Variant { pub(crate) fn sha2_cmd(variant: SHA2Variant, output_hex: bool) { match variant { - SHA2Variant::SHA224 => do_sha2(SHA224::new(), output_hex), - SHA2Variant::SHA256 => do_sha2(SHA256::new(), output_hex), - SHA2Variant::SHA384 => do_sha2(SHA384::new(), output_hex), - SHA2Variant::SHA512 => do_sha2(SHA512::new(), output_hex), - SHA2Variant::SHA512_224 => do_sha2(SHA512_224::new(), output_hex), - SHA2Variant::SHA512_256 => do_sha2(SHA512_256::new(), output_hex), + SHA2Variant::SHA224 => stream_hash(SHA224::new(), output_hex), + SHA2Variant::SHA256 => stream_hash(SHA256::new(), output_hex), + SHA2Variant::SHA384 => stream_hash(SHA384::new(), output_hex), + SHA2Variant::SHA512 => stream_hash(SHA512::new(), output_hex), + SHA2Variant::SHA512_224 => stream_hash(SHA512_224::new(), output_hex), + SHA2Variant::SHA512_256 => stream_hash(SHA512_256::new(), output_hex), } } - -fn do_sha2(mut sha2: impl Hash, output_hex: bool) { - let mut buf: [u8; 1024] = [0u8; 1024]; - - // read from stdin - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - while bytes_read != 0 { - sha2.do_update(&buf[..bytes_read]); - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - } - - let out = sha2.do_final(); - - crate::helpers::write_bytes_or_hex(&out, output_hex); - crate::helpers::write_stdout(b"\n"); -} diff --git a/cli/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index 8ab08d3aa..1185a0dc6 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -1,15 +1,20 @@ +//! The SHA-3 family subcommands: `sha3-*`, `shake*`, `tuplehash*` and `parallelhash*`. + use bouncycastle::core::traits::Hash; -use std::io; -use std::io::Read; -use bouncycastle::hex; use bouncycastle::sha3::parallelhash::{ParallelHash128, ParallelHash256}; use bouncycastle::sha3::tuplehash::{TupleHash128, TupleHash256}; use bouncycastle::sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256}; use core::num::NonZeroUsize; use std::process::exit; -use crate::helpers::{stream_hash, stream_xof}; +use crate::helpers::{decode_hex_arg, read_stdin_all, stream_hash, stream_stdin, stream_xof}; + +/// The most a fixed-length SP 800-185 function (KMAC, TupleHash, ParallelHash) will produce +/// here: their output length is bound into the computation before any output, so the library +/// computes it whole, and a request of gigabytes would be an allocation rather than a run. The +/// `*xof` subcommands stream any length. +pub(crate) const MAX_FIXED_OUTPUT_LEN: usize = 1 << 28; #[allow(non_camel_case_types)] pub(crate) enum SHA3Variant { @@ -27,6 +32,30 @@ pub(crate) enum Strength { Bits256, } +/// The customization string S of SP 800-185, from `--customization` (the characters) or +/// `--customization-hex` (bytes); empty when neither is given. Both given is an error, since +/// the one would silently win over the other. +pub(crate) fn load_customization(text: &Option, hex: &Option) -> Vec { + match (text, hex) { + (Some(_), Some(_)) => { + eprintln!("Error: give `--customization` or `--customization-hex`, not both."); + exit(-1); + } + (Some(text), None) => text.as_bytes().to_vec(), + (None, Some(hex)) => decode_hex_arg(hex, "customization-hex"), + (None, None) => Vec::new(), + } +} + +fn refuse_long_fixed_output(output_len: usize, xof_command: &str) { + if output_len > MAX_FIXED_OUTPUT_LEN { + eprintln!( + "Error: this output is computed whole; the command takes at most {MAX_FIXED_OUTPUT_LEN} bytes. Use {xof_command} for a long, streamed output." + ); + exit(-1); + } +} + pub(crate) fn sha3_cmd(variant: SHA3Variant, output_hex: bool) { match variant { SHA3Variant::SHA3_224 => stream_hash(SHA3_224::new(), output_hex), @@ -52,44 +81,38 @@ pub(crate) fn tuplehash_cmd( strength: Strength, output_len: usize, elements: &[String], - customization: &Option, + customization: &[u8], output_hex: bool, ) { - let s = customization.as_deref().unwrap_or("").as_bytes(); - - // Either the tuple came from flags, or stdin is the single element. - let tuple: Vec> = if elements.is_empty() { - vec![read_stdin()] - } else { - elements - .iter() - .map(|e| { - hex::decode(e).unwrap_or_else(|_| { - eprintln!("Error: --element must be hex."); - exit(-1); - }) - }) - .collect() - }; + refuse_long_fixed_output(output_len, "tuplehashxof*"); + let tuple = load_tuple(elements); let refs: Vec<&[u8]> = tuple.iter().map(|v| v.as_slice()).collect(); let out = match strength { - Strength::Bits128 => TupleHash128::new(s, output_len) - .unwrap_or_else(|_| { - eprintln!("Error: --output-len is larger than SP 800-185 allows here (Sec 7.2)."); - exit(-1); - }) + Strength::Bits128 => TupleHash128::new(customization, output_len) + .unwrap_or_else(|_| output_len_error()) .hash_tuple(&refs), - Strength::Bits256 => TupleHash256::new(s, output_len) - .unwrap_or_else(|_| { - eprintln!("Error: --output-len is larger than SP 800-185 allows here (Sec 7.2)."); - exit(-1); - }) + Strength::Bits256 => TupleHash256::new(customization, output_len) + .unwrap_or_else(|_| output_len_error()) .hash_tuple(&refs), }; write_out(&out, output_hex); } +/// The tuple: the `--element` flags decoded, or stdin as the single element. +pub(crate) fn load_tuple(elements: &[String]) -> Vec> { + if elements.is_empty() { + vec![read_stdin_all()] + } else { + elements.iter().map(|e| decode_hex_arg(e, "element")).collect() + } +} + +fn output_len_error() -> ! { + eprintln!("Error: the output length is larger than SP 800-185 allows here (Sec 7.2)."); + exit(-1); +} + /// ParallelHash (NIST SP 800-185 Sec 6): hashes stdin in `block_size`-byte blocks. /// /// The block size is part of the function, not a tuning knob -- the same input under a different @@ -98,57 +121,33 @@ pub(crate) fn parallelhash_cmd( strength: Strength, output_len: usize, block_size: usize, - customization: &Option, + customization: &[u8], output_hex: bool, ) { - let Some(block_size) = NonZeroUsize::new(block_size) else { - eprintln!("Error: --block-size must be greater than zero (SP 800-185 Sec 6.2)."); - exit(-1); - }; - let s = customization.as_deref().unwrap_or("").as_bytes(); + refuse_long_fixed_output(output_len, "parallelhashxof*"); + let block_size = require_block_size(block_size); match strength { Strength::Bits128 => { - let mut p = ParallelHash128::new(block_size, s, output_len).unwrap_or_else(|_| { - eprintln!("Error: --output-len is larger than SP 800-185 allows here (Sec 7.2)."); - exit(-1); - }); + let mut p = ParallelHash128::new(block_size, customization, output_len) + .unwrap_or_else(|_| output_len_error()); stream_stdin(|chunk| p.do_update(chunk)); write_out(&p.do_final(), output_hex); } Strength::Bits256 => { - let mut p = ParallelHash256::new(block_size, s, output_len).unwrap_or_else(|_| { - eprintln!("Error: --output-len is larger than SP 800-185 allows here (Sec 7.2)."); - exit(-1); - }); + let mut p = ParallelHash256::new(block_size, customization, output_len) + .unwrap_or_else(|_| output_len_error()); stream_stdin(|chunk| p.do_update(chunk)); write_out(&p.do_final(), output_hex); } } } -/// Reads all of stdin. Used where the whole input must be held anyway (a tuple element). -fn read_stdin() -> Vec { - let mut out = Vec::new(); - let mut buf = [0u8; 1024]; - loop { - let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - if n == 0 { - return out; - } - out.extend_from_slice(&buf[..n]); - } -} - -/// Feeds stdin to `sink` in 1 KiB pieces, so a long input is never held in memory. -fn stream_stdin(mut sink: impl FnMut(&[u8])) { - let mut buf = [0u8; 1024]; - loop { - let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - if n == 0 { - return; - } - sink(&buf[..n]); - } +/// SP 800-185 Sec 6.2: B must be greater than zero. +pub(crate) fn require_block_size(block_size: usize) -> NonZeroUsize { + NonZeroUsize::new(block_size).unwrap_or_else(|| { + eprintln!("Error: --block-size must be greater than zero (SP 800-185 Sec 6.2)."); + exit(-1); + }) } /// Writes the digest as raw bytes or hex, with the trailing newline the other commands emit. diff --git a/cli/src/sm3_cmd.rs b/cli/src/sm3_cmd.rs index c7bc5acdc..f713d2040 100644 --- a/cli/src/sm3_cmd.rs +++ b/cli/src/sm3_cmd.rs @@ -1,22 +1,6 @@ -use bouncycastle::core::traits::Hash; -use std::io; -use std::io::Read; - +use crate::helpers::stream_hash; use bouncycastle::sm3::SM3; pub(crate) fn sm3_cmd(output_hex: bool) { - let mut sm3 = SM3::new(); - let mut buf: [u8; 1024] = [0u8; 1024]; - - // read from stdin - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - while bytes_read != 0 { - sm3.do_update(&buf[..bytes_read]); - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - } - - let out = sm3.do_final(); - - crate::helpers::write_bytes_or_hex(&out, output_hex); - crate::helpers::write_stdout(b"\n"); + stream_hash(SM3::new(), output_hex); } diff --git a/cli/tests/test_aes_cbc.sh b/cli/tests/test_aes_cbc.sh index 3a32d1c28..5ad094392 100755 --- a/cli/tests/test_aes_cbc.sh +++ b/cli/tests/test_aes_cbc.sh @@ -94,7 +94,12 @@ test_key_file_accepts_binary_hex_and_a_trailing_newline() { "$BC_RUST" aes128-cbc -d encrypt --key-file "$TMP/key.bin" <"$TMP/pt" >"$TMP/ct" local form - for form in key.hex key.hex.nl key.bin.nl; do + # The hex forms need --hex-input: a key file is raw bytes unless the flag says otherwise. + for form in key.hex key.hex.nl; do + "$BC_RUST" --hex-input aes128-cbc -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" + done + for form in key.bin.nl; do "$BC_RUST" aes128-cbc -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" done diff --git a/cli/tests/test_aes_cfb.sh b/cli/tests/test_aes_cfb.sh index a8fa0f4b4..b404ed056 100755 --- a/cli/tests/test_aes_cfb.sh +++ b/cli/tests/test_aes_cfb.sh @@ -120,10 +120,13 @@ test_key_file_accepts_binary_hex_and_a_trailing_newline() { "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key.bin" <"$TMP/pt" >"$TMP/ct" local form - for form in key.hex key.hex.nl key.bin.nl; do - "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" + # The hex forms need --hex-input: a key file is raw bytes unless the flag says otherwise. + for form in key.hex key.hex.nl; do + "$BC_RUST" --hex-input aes128-cfb -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" done + "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/key.bin.nl" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "key.bin.nl must load as the same key as key.bin" } test_key_on_the_command_line_matches_the_key_file() { diff --git a/cli/tests/test_aes_cfb8.sh b/cli/tests/test_aes_cfb8.sh index 5592ae809..cf7be8c8d 100755 --- a/cli/tests/test_aes_cfb8.sh +++ b/cli/tests/test_aes_cfb8.sh @@ -104,10 +104,13 @@ test_key_file_accepts_binary_hex_and_a_trailing_newline() { "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key.bin" <"$TMP/pt" >"$TMP/ct" local form - for form in key.hex key.hex.nl key.bin.nl; do - "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" + # The hex forms need --hex-input: a key file is raw bytes unless the flag says otherwise. + for form in key.hex key.hex.nl; do + "$BC_RUST" --hex-input aes128-cfb8 -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" done + "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/key.bin.nl" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "key.bin.nl must load as the same key as key.bin" } test_key_on_the_command_line_matches_the_key_file() { diff --git a/cli/tests/test_aes_ctr.sh b/cli/tests/test_aes_ctr.sh index 1806b0d18..6f418ba9c 100755 --- a/cli/tests/test_aes_ctr.sh +++ b/cli/tests/test_aes_ctr.sh @@ -162,7 +162,12 @@ test_key_file_accepts_binary_hex_and_a_trailing_newline() { "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key.bin" <"$TMP/pt" >"$TMP/ct" local form - for form in key.hex key.hex.nl key.bin.nl; do + # The hex forms need --hex-input: a key file is raw bytes unless the flag says otherwise. + for form in key.hex key.hex.nl; do + "$BC_RUST" --hex-input aes128-ctr -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" + done + for form in key.bin.nl; do "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" done diff --git a/cli/tests/test_aes_ecb.sh b/cli/tests/test_aes_ecb.sh index d80eb76a5..0a56c8f20 100755 --- a/cli/tests/test_aes_ecb.sh +++ b/cli/tests/test_aes_ecb.sh @@ -135,10 +135,14 @@ test_key_file_accepts_hex_and_binary() { unhex "$F1_KEY_128" >"$TMP/key.bin" unhex "$F1_CT_128" >"$TMP/ct" unhex "$F1_PLAINTEXT" >"$TMP/pt" - for form in key.hex key.bin; do - "$BC_RUST" aes128-ecb -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" - assert_same "$TMP/pt" "$TMP/rec" "--key-file $form must decrypt the F.1.1 ciphertext" - done + "$BC_RUST" aes128-ecb -d decrypt --key-file "$TMP/key.bin" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "--key-file key.bin must decrypt the F.1.1 ciphertext" + # A hex key file needs --hex-input; without it the 32 ASCII bytes are a wrong-length key. + "$BC_RUST" --hex-input aes128-ecb -d decrypt --key-file "$TMP/key.hex" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "--hex-input --key-file key.hex must decrypt the F.1.1 ciphertext" + expect_fail "a hex key file without --hex-input is 32 raw bytes" \ + "$BC_RUST" aes128-ecb -d decrypt --key-file "$TMP/key.hex" <"$TMP/ct" + assert_stderr_has "got 32 bytes" } test_a_key_of_the_wrong_length_is_rejected() { diff --git a/cli/tests/test_ascon.sh b/cli/tests/test_ascon.sh index c3a6f575d..791041166 100755 --- a/cli/tests/test_ascon.sh +++ b/cli/tests/test_ascon.sh @@ -61,7 +61,7 @@ test_xof128_output_length_is_a_prefix_of_a_longer_squeeze() { test_cxof128_matches_the_kat() { local got unhex 00 >"$TMP/msg" - got=$(hex_out "$BC_RUST" ascon-cxof128 64 --customization 10 -x <"$TMP/msg") + got=$(hex_out "$BC_RUST" ascon-cxof128 64 --customization-hex 10 -x <"$TMP/msg") assert_eq "$got" \ "63fa8ba86382f2d544580f51322d080424b42c556eb74503cd73cf052bb993bd6f5210984c71c9c445f43ccc5b158226e509bd339cd634414377f79411aa8d5c" \ "customized squeeze" @@ -72,7 +72,7 @@ test_cxof128_matches_the_kat() { test_cxof128_with_no_customization_matches_an_empty_one() { local without with_empty without=$(hex_out "$BC_RUST" ascon-cxof128 64 -x "$TMP/key" rng 256 >"$TMP/pt" - "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" --ad deadbeef <"$TMP/pt" >"$TMP/ct" - "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" --ad deadbeef <"$TMP/ct" >"$TMP/rec" + "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "the same AD on both sides must round-trip" } @@ -135,9 +135,9 @@ test_aead128_each_invocation_uses_a_fresh_nonce() { test_aead128_wrong_associated_data_is_rejected() { rng 16 >"$TMP/key" rng 64 >"$TMP/pt" - "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" --ad deadbeef <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" >"$TMP/ct" expect_fail "different AD must fail the tag check" \ - "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" --ad cafebabe <"$TMP/ct" + "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" --aad cafebabe <"$TMP/ct" assert_stderr_has "authentication failed" } diff --git a/cli/tests/test_kmac.sh b/cli/tests/test_kmac.sh index 4c95e26c9..bcd78606d 100755 --- a/cli/tests/test_kmac.sh +++ b/cli/tests/test_kmac.sh @@ -31,7 +31,8 @@ kmac_round_trip() { "$BC_RUST" "$sub" 16 --key-file "$TMP/key" -v "${tag:0:32}" <"$TMP/msg" } -# `kmac_refused_length SUBCOMMAND LENGTH` checks that LENGTH is refused with an error message, +# `kmac_refused_length SUBCOMMAND LENGTH` checks that LENGTH is refused with an error message +# (the library's floor for a short one, the CLI's whole-output cap for a huge one), # not a panic trace, whether computing or verifying. kmac_refused_length() { local sub=$1 length=$2 @@ -40,12 +41,12 @@ kmac_refused_length() { expect_fail "tag length $length must be refused" \ "$BC_RUST" "$sub" "$length" --key-file "$TMP/key" -x <"$TMP/msg" - assert_stderr_has "Error: couldn't build the KMAC key" + assert_stderr_has "Error:" ! grep -q panicked "$LAST_STDERR" || fail "a refused tag length must not panic" expect_fail "tag length $length must be refused on verify" \ "$BC_RUST" "$sub" "$length" --key-file "$TMP/key" -v "" <"$TMP/msg" - assert_stderr_has "Error: couldn't build the KMAC key" + assert_stderr_has "Error:" ! grep -q panicked "$LAST_STDERR" || fail "a refused tag length must not panic" } From bf463f77338e75840ea8cd196b4537aa475b1572 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 10:31:07 +1100 Subject: [PATCH 41/57] cli: the subcommands the library had and the CLI did not (the CLI review's L17 and L21): hmac-sha224, hmac-sha384 and hmac-sha3-224/256/384/512; cshake128/256 with a function name and a customization string; kmacxof*, tuplehashxof* and parallelhashxof*, the SP 800-185 XOF forms, which stream any output length; aes128/192/256-cmac; aes128/192/256-gmac, with the nonce drawn and written ahead of the tag unless --iv gives it; --hash sha256|sha512|shake128|shake256 on hash-mldsa* (SHA-512 remains the default, and the library refuses a hash below the parameter set's strength); --deterministic on the ML-DSA sign actions; compute-mu, sign-mu and verify-mu for the external-mu interface, with --mufile; and rng --hash sha256|sha512 and the hazardous --seed-file, which instantiates the DRBG from a file for a reproducible stream Assisted-by: Claude:claude-fable-5-1 --- cli/src/helpers/block_mode_helpers.rs | 43 +- cli/src/mac_cmd.rs | 246 ++++++- cli/src/main.rs | 990 +++++++++++++++++++++++--- cli/src/mldsa_cmd.rs | 270 +++++-- cli/src/rng_cmd.rs | 67 +- cli/src/sha3_cmd.rs | 80 ++- 6 files changed, 1486 insertions(+), 210 deletions(-) diff --git a/cli/src/helpers/block_mode_helpers.rs b/cli/src/helpers/block_mode_helpers.rs index a81bd91f4..0c40ba5e9 100644 --- a/cli/src/helpers/block_mode_helpers.rs +++ b/cli/src/helpers/block_mode_helpers.rs @@ -96,6 +96,16 @@ pub(crate) fn load_key( key: &Option, key_file: &Option, alg: &str, +) -> KeyMaterial { + load_key_typed(key, key_file, alg, KeyType::SymmetricCipherKey) +} + +/// As [`load_key`], tagged `key_type`: `MACKey` for the CMAC and GMAC keys. +pub(crate) fn load_key_typed( + key: &Option, + key_file: &Option, + alg: &str, + key_type: KeyType, ) -> KeyMaterial { let secret = load_secret(key, key_file, "key"); // A raw key file may end in a newline, which lengthens the key by a byte; strip it only @@ -112,20 +122,19 @@ pub(crate) fn load_key( // `from_bytes_as_type` tags the key at the strength its length implies, which is exactly what // the engine requires -- except for an all-zero key, which it marks Zeroized instead. let mut key = - KeyMaterial::::from_bytes_as_type(key_bytes, KeyType::SymmetricCipherKey) - .unwrap_or_else(|e| { - eprintln!("Error: couldn't load the key: {e:?}"); - exit(-1); - }); + KeyMaterial::::from_bytes_as_type(key_bytes, key_type).unwrap_or_else(|e| { + eprintln!("Error: couldn't load the key: {e:?}"); + exit(-1); + }); - if key.key_type() != KeyType::SymmetricCipherKey { + if key.key_type() != key_type { // Same stance as `helpers::parse_seed`: warn, then do what was asked. A CLI is used for // test vectors and scripting, where an all-zero key is a legitimate thing to want. eprintln!( "Warning: all-zero (or otherwise zeroized) key provided. Proceeding, but this is not secure." ); do_hazardous_operations(&mut key, |key| { - key.set_key_type(KeyType::SymmetricCipherKey)?; + key.set_key_type(key_type)?; key.set_security_strength(SecurityStrength::from_bytes(KEY_LEN)) }) .unwrap_or_else(|e| { @@ -144,10 +153,22 @@ pub(crate) fn load_key_object, const KEY_LEN: usize> key_file: &Option, alg: &str, ) -> K { - K::from_key_material(&load_key::(key, key_file, alg)).unwrap_or_else(|e| { - eprintln!("Error: couldn't load the key: {e:?}"); - exit(-1); - }) + load_key_object_typed(key, key_file, alg, KeyType::SymmetricCipherKey) +} + +/// As [`load_key_object`], with the material tagged `key_type`. +pub(crate) fn load_key_object_typed, const KEY_LEN: usize>( + key: &Option, + key_file: &Option, + alg: &str, + key_type: KeyType, +) -> K { + K::from_key_material(&load_key_typed::(key, key_file, alg, key_type)).unwrap_or_else( + |e| { + eprintln!("Error: couldn't load the key: {e:?}"); + exit(-1); + }, + ) } /// An [`RNG`] that yields the caller's IV once, so `--iv` can reach a key whose only diff --git a/cli/src/mac_cmd.rs b/cli/src/mac_cmd.rs index f17e391cb..a5b48dedc 100644 --- a/cli/src/mac_cmd.rs +++ b/cli/src/mac_cmd.rs @@ -1,26 +1,40 @@ -//! The `hmac-*` and `kmac*` subcommands: a MAC over stdin, computed or verified. +//! The MAC subcommands: `hmac-*`, `kmac*` and `kmacxof*`, `aes*-cmac` and `aes*-gmac`. A MAC +//! over stdin, computed or verified. +use crate::helpers::block_mode_helpers::{load_iv, load_key_object_typed}; use crate::helpers::{ - SecretBytes, decode_hex_arg, load_secret, stream_stdin, write_bytes_or_hex, write_stdout, + SecretBytes, decode_hex_arg, load_secret, stream_stdin, stream_xof, write_bytes_or_hex, + write_stdout, }; use crate::sha3_cmd::{MAX_FIXED_OUTPUT_LEN, Strength}; +use bouncycastle::aes::hazmat::AESInternal; +use bouncycastle::aes::{AES128Key, AES192Key, AES256Key, AESKey, AESParams}; +use bouncycastle::cipher::modes::{CMAC, GMAC, ModeNames}; use bouncycastle::core::errors::MACError; -use bouncycastle::core::hazmat::do_hazardous_operations; +use bouncycastle::core::hazmat::{BlockPermutation, do_hazardous_operations}; use bouncycastle::core::key_material::{KeyMaterialTrait, KeyType}; -use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, FromKeyMaterial, MacGenerationKey}; use bouncycastle::core::traits::MAC; use bouncycastle::hmac::HmacKey; -use bouncycastle::sha2::{SHA256, SHA512, SHA512_224, SHA512_256}; -use bouncycastle::sha3::kmac::{KMAC128Key, KMAC256Key}; +use bouncycastle::rng::HashDRBG_SHA512; +use bouncycastle::sha2::{SHA224, SHA256, SHA384, SHA512, SHA512_224, SHA512_256}; +use bouncycastle::sha3::kmac::{KMAC128Key, KMAC256Key, KMACXOF128, KMACXOF256}; +use bouncycastle::sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512}; use bouncycastle::sm3::SM3; use std::process::exit; #[allow(non_camel_case_types)] pub(crate) enum HMACVariant { + SHA224, SHA256, + SHA384, SHA512, SHA512_224, SHA512_256, + SHA3_224, + SHA3_256, + SHA3_384, + SHA3_512, SM3, } @@ -50,6 +64,18 @@ fn mac_key_error(e: MACError) -> ! { exit(-1) } +/// HMAC over `H` with the allow-weak key form, as every `hmac-*` command uses: a CLI is used for +/// test vectors and scripting, where a short or all-zero key is a legitimate thing to want. +fn hmac( + key: &SecretBytes, + verify_val: &Option, + output_hex: bool, +) { + let hmac_key = + HmacKey::::from_key_material_allow_weak(key).unwrap_or_else(|e| mac_key_error(e)); + do_mac(hmac_key.create_generator(), verify_val, output_hex); +} + pub(crate) fn mac_cmd( hmac_variant: HMACVariant, key: &Option, @@ -58,35 +84,18 @@ pub(crate) fn mac_cmd( output_hex: bool, ) { let key = load_mac_key(key, key_file); - - // The allow-weak form: a CLI is used for test vectors and scripting, where a short or - // all-zero key is a legitimate thing to want. match hmac_variant { - HMACVariant::SHA256 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key) - .unwrap_or_else(|e| mac_key_error(e)); - do_mac(hmac_key.create_generator(), verify_val, output_hex); - } - HMACVariant::SHA512 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key) - .unwrap_or_else(|e| mac_key_error(e)); - do_mac(hmac_key.create_generator(), verify_val, output_hex); - } - HMACVariant::SHA512_224 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key) - .unwrap_or_else(|e| mac_key_error(e)); - do_mac(hmac_key.create_generator(), verify_val, output_hex); - } - HMACVariant::SHA512_256 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key) - .unwrap_or_else(|e| mac_key_error(e)); - do_mac(hmac_key.create_generator(), verify_val, output_hex); - } - HMACVariant::SM3 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key) - .unwrap_or_else(|e| mac_key_error(e)); - do_mac(hmac_key.create_generator(), verify_val, output_hex); - } + HMACVariant::SHA224 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA256 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA384 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA512 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA512_224 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA512_256 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA3_224 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA3_256 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA3_384 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA3_512 => hmac::(&key, verify_val, output_hex), + HMACVariant::SM3 => hmac::(&key, verify_val, output_hex), } } @@ -122,6 +131,175 @@ pub(crate) fn kmac_cmd( } } +/// KMACXOF (SP 800-185 Sec 4.3.1): KMAC with the length not bound in, so the output is a +/// stream of any length. There is nothing to verify against: a reader squeezes the same stream. +pub(crate) fn kmacxof_cmd( + strength: Strength, + output_len: usize, + customization: &[u8], + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let key = load_mac_key(key, key_file); + match strength { + Strength::Bits128 => { + let x = KMACXOF128::new(&key, customization, true).unwrap_or_else(|e| mac_key_error(e)); + stream_xof(x, output_len, output_hex); + } + Strength::Bits256 => { + let x = KMACXOF256::new(&key, customization, true).unwrap_or_else(|e| mac_key_error(e)); + stream_xof(x, output_len, output_hex); + } + } +} + +/// AES-CMAC (NIST SP 800-38B) under the key the command's length selects. +pub(crate) fn cmac_cmd + MacGenerationKey, const KEY_LEN: usize>( + alg: &str, + key: &Option, + key_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + let key: K = load_key_object_typed(key, key_file, alg, KeyType::MACKey); + do_mac(key.create_generator(), verify_val, output_hex); +} + +pub(crate) fn aes128_cmac_cmd( + key: &Option, + key_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + cmac_cmd::, 16>("AES-128", key, key_file, verify_val, output_hex); +} + +pub(crate) fn aes192_cmac_cmd( + key: &Option, + key_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + cmac_cmd::, 24>("AES-192", key, key_file, verify_val, output_hex); +} + +pub(crate) fn aes256_cmac_cmd( + key: &Option, + key_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + cmac_cmd::, 32>("AES-256", key, key_file, verify_val, output_hex); +} + +/// The GMAC nonce length: GCM's 12 bytes (SP 800-38D Sec 5.2.1.1's recommended IV length). +const GMAC_NONCE_LEN: usize = 12; +/// The GMAC tag length the `aes*-gmac` commands fix, as `aes*-gcm` do: the full 128 bits. +const GMAC_TAG_LEN: usize = 16; + +/// AES-GMAC (NIST SP 800-38D Sec 5.2): a GCM tag over the data as AAD. A tag is bound to a +/// nonce, so the shape is the MAC commands' with the IV flags of `aes*-gcm` added. Without +/// `--iv` the nonce is drawn from the OS-backed DRBG and written ahead of the tag, so the +/// output is `nonce || tag` and a reader has what it needs; with `--iv` only the tag is +/// written. `--verify` takes the tag, with `--iv`, or the 28-byte `nonce || tag` without it. +/// +/// The GMAC workers have no trait: their generator and verifier are inherent on the key, so +/// this is generic over the AES parameter set the way the key's own impl is. +fn gmac_run( + key: &AESKey>, + iv: Option<[u8; GMAC_NONCE_LEN]>, + verify_val: &Option, + output_hex: bool, +) where + AESInternal

: BlockPermutation<16> + ModeNames + Clone, +{ + match verify_val { + None => { + let (mut mac, generated) = match iv { + Some(iv) => (key.create_generator_nonce(&iv), None), + None => key + .create_generator(&mut HashDRBG_SHA512::new_from_os()) + .map(|(mac, nonce)| (mac, Some(nonce))) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't draw the nonce: {e:?}"); + exit(-1); + }), + }; + stream_stdin(|chunk| mac.do_update(chunk)); + let tag = mac.do_final(); + if let Some(nonce) = generated { + write_bytes_or_hex(&nonce, output_hex); + } + write_bytes_or_hex(&tag, output_hex); + write_stdout(b"\n"); + } + Some(verify_val) => { + let given = decode_hex_arg(verify_val, "verify"); + let (nonce, tag): ([u8; GMAC_NONCE_LEN], &[u8]) = match iv { + Some(iv) => (iv, &given), + None => { + if given.len() != GMAC_NONCE_LEN + GMAC_TAG_LEN { + eprintln!( + "Error: without --iv, --verify takes the {GMAC_NONCE_LEN}-byte nonce followed by the {GMAC_TAG_LEN}-byte tag." + ); + exit(-1); + } + let (nonce, tag) = given.split_at(GMAC_NONCE_LEN); + (nonce.try_into().expect("split at the nonce length"), tag) + } + }; + let tag: &[u8; GMAC_TAG_LEN] = tag.try_into().unwrap_or_else(|_| { + eprintln!("Error: the tag must be {GMAC_TAG_LEN} bytes, got {}.", tag.len()); + exit(-1); + }); + let mut mac = key.create_verifier(&nonce); + stream_stdin(|chunk| mac.do_update(chunk)); + if mac.do_verify_final(tag) { + exit(0) + } + eprintln!("Error: MAC verification failed."); + exit(-1) + } + } +} + +pub(crate) fn aes128_gmac_cmd( + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + let key: AES128Key = load_key_object_typed(key, key_file, "AES-128", KeyType::MACKey); + gmac_run(&key, load_iv::(iv, iv_file), verify_val, output_hex); +} + +pub(crate) fn aes192_gmac_cmd( + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + let key: AES192Key = load_key_object_typed(key, key_file, "AES-192", KeyType::MACKey); + gmac_run(&key, load_iv::(iv, iv_file), verify_val, output_hex); +} + +pub(crate) fn aes256_gmac_cmd( + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + let key: AES256Key = load_key_object_typed(key, key_file, "AES-256", KeyType::MACKey); + gmac_run(&key, load_iv::(iv, iv_file), verify_val, output_hex); +} + /// Streams stdin into `mac`, then writes the tag, or verifies `--verify` against it and reports /// the outcome through the exit status (0 verified, 255 not) and an `Error:` line. pub(crate) fn do_mac(mut mac: impl MAC, verify_val: &Option, output_hex: bool) { diff --git a/cli/src/main.rs b/cli/src/main.rs index 24539bbd3..289bd5b7a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -21,7 +21,8 @@ mod sm3_cmd; use crate::hkdf_cmd::HKDFVariant; use crate::mac_cmd::HMACVariant; -use crate::mldsa_cmd::MLDSAAction; +use crate::mldsa_cmd::{MLDSAAction, PreHashAlg}; +use crate::rng_cmd::DrbgHash; use crate::sha2_cmd::SHA2Variant; use crate::sha3_cmd::{SHA3Variant, Strength}; use clap::{Parser, Subcommand}; @@ -283,6 +284,31 @@ enum Subcommands { x: bool, }, + /// Compute or verify an AES-128 CMAC (NIST SP 800-38B) over the content provided on stdin: + /// a 16-byte tag. Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + AES128_CMAC { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + /// AES-128 in CTR mode (NIST SP 800-38A Sec 6.5), streaming stdin to stdout. /// /// The counter block is a 12-byte nonce followed by a 4-byte counter starting at zero, so one @@ -439,6 +465,44 @@ enum Subcommands { x: bool, }, + /// Compute or verify an AES-128 GMAC (NIST SP 800-38D Sec 5.2) over the content provided on + /// stdin: a 16-byte tag bound to a 12-byte nonce. Without `--iv` a fresh nonce is drawn and + /// written FIRST, so the output is `nonce || tag`; with `--iv` only the tag is written. To + /// verify, give `--verify` the tag along with `--iv`, or the 28-byte `nonce || tag` alone. + /// A nonce MUST NOT repeat under one key (SP 800-38D Sec 8). + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + AES128_GMAC { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 12-byte nonce in hex. Hazardous: supplying it makes uniqueness the caller's + /// responsibility. + #[arg(long)] + iv: Option, + + /// A file containing the 12-byte nonce (raw bytes; hex with --hex-input); the same + /// hazardous override. + #[arg(long)] + iv_file: Option, + + /// A MAC value to be verified: the tag (with --iv) or nonce || tag. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// AES-192 in CBC mode (NIST SP 800-38A Sec 6.2) with PKCS7 padding, streaming stdin to stdout. /// /// See `aes128-cbc` for the IV convention, padding and warnings; only the @@ -581,6 +645,31 @@ enum Subcommands { x: bool, }, + /// Compute or verify an AES-192 CMAC (NIST SP 800-38B) over the content provided on stdin: + /// a 16-byte tag. Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + AES192_CMAC { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + /// AES-192 in CTR mode (NIST SP 800-38A Sec 6.5), streaming stdin to stdout. /// /// See `aes128-ctr` for the nonce convention, input-length rule and warnings; only the key @@ -678,6 +767,44 @@ enum Subcommands { x: bool, }, + /// Compute or verify an AES-192 GMAC (NIST SP 800-38D Sec 5.2) over the content provided on + /// stdin: a 16-byte tag bound to a 12-byte nonce. Without `--iv` a fresh nonce is drawn and + /// written FIRST, so the output is `nonce || tag`; with `--iv` only the tag is written. To + /// verify, give `--verify` the tag along with `--iv`, or the 28-byte `nonce || tag` alone. + /// A nonce MUST NOT repeat under one key (SP 800-38D Sec 8). + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + AES192_GMAC { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 12-byte nonce in hex. Hazardous: supplying it makes uniqueness the caller's + /// responsibility. + #[arg(long)] + iv: Option, + + /// A file containing the 12-byte nonce (raw bytes; hex with --hex-input); the same + /// hazardous override. + #[arg(long)] + iv_file: Option, + + /// A MAC value to be verified: the tag (with --iv) or nonce || tag. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// AES-256 in CBC mode (NIST SP 800-38A Sec 6.2) with PKCS7 padding, streaming stdin to stdout. /// /// See `aes128-cbc` for the IV convention, padding and warnings; only the @@ -820,6 +947,31 @@ enum Subcommands { x: bool, }, + /// Compute or verify an AES-256 CMAC (NIST SP 800-38B) over the content provided on stdin: + /// a 16-byte tag. Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + AES256_CMAC { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + /// AES-256 in CTR mode (NIST SP 800-38A Sec 6.5), streaming stdin to stdout. /// /// See `aes128-ctr` for the nonce convention, input-length rule and warnings; only the key @@ -917,6 +1069,44 @@ enum Subcommands { x: bool, }, + /// Compute or verify an AES-256 GMAC (NIST SP 800-38D Sec 5.2) over the content provided on + /// stdin: a 16-byte tag bound to a 12-byte nonce. Without `--iv` a fresh nonce is drawn and + /// written FIRST, so the output is `nonce || tag`; with `--iv` only the tag is written. To + /// verify, give `--verify` the tag along with `--iv`, or the 28-byte `nonce || tag` alone. + /// A nonce MUST NOT repeat under one key (SP 800-38D Sec 8). + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + AES256_GMAC { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 12-byte nonce in hex. Hazardous: supplying it makes uniqueness the caller's + /// responsibility. + #[arg(long)] + iv: Option, + + /// A file containing the 12-byte nonce (raw bytes; hex with --hex-input); the same + /// hazardous override. + #[arg(long)] + iv_file: Option, + + /// A MAC value to be verified: the tag (with --iv) or nonce || tag. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// Ascon-AEAD128 authenticated encryption/decryption of the content provided on stdin /// (NIST SP 800-232). /// @@ -1019,10 +1209,62 @@ enum Subcommands { /// Supports streaming for low memory footprint and continuous processing from stdin to stdout. Base64Encode, - /// The HashML-DSA-44 signature algorithm. + /// Perform cSHAKE128 (NIST SP 800-185 Sec 3) of the content provided on stdin, under a + /// function name N and a customization string S; with both empty it is SHAKE128. Requires + /// the output length in bytes. Supports streaming update for low memory footprint. + CSHAKE128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'n', long)] + /// Function name N, as the characters given (SP 800-185 reserves it for NIST-defined + /// functions; leave it empty otherwise). + function_name: Option, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Perform cSHAKE256 (NIST SP 800-185 Sec 3). See cshake128. + CSHAKE256 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'n', long)] + /// Function name N, as the characters given. + function_name: Option, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// The HashML-DSA-44 signature algorithm, with the hash given by --hash (SHA-512 by default). HashMLDSA44 { action: MLDSAAction, + #[arg(long, value_enum, default_value_t = PreHashAlg::Sha512)] + /// The pre-hash (FIPS 204 Section 5.4); it must be at least as strong as the parameter + /// set, so sha256 and shake128 fit hash-mldsa44 only. + hash: PreHashAlg, + #[arg(long)] /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, @@ -1039,15 +1281,30 @@ enum Subcommands { /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, + #[arg(long)] + /// The message representative file (64 bytes; raw bytes; hex with --hex-input) for + /// sign-mu and verify-mu; stdin without it. + mufile: Option, + + #[arg(long)] + /// Sign with the deterministic variant (FIPS 204 Section 3.4: rnd all zero) rather than + /// the hedged default. + deterministic: bool, + #[arg(short)] /// Output in hex format. x: bool, }, - /// The HashML-DSA-65 signature algorithm. + /// The HashML-DSA-65 signature algorithm, with the hash given by --hash (SHA-512 by default). HashMLDSA65 { action: MLDSAAction, + #[arg(long, value_enum, default_value_t = PreHashAlg::Sha512)] + /// The pre-hash (FIPS 204 Section 5.4); it must be at least as strong as the parameter + /// set, so sha256 and shake128 fit hash-mldsa44 only. + hash: PreHashAlg, + #[arg(long)] /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, @@ -1064,15 +1321,30 @@ enum Subcommands { /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, + #[arg(long)] + /// The message representative file (64 bytes; raw bytes; hex with --hex-input) for + /// sign-mu and verify-mu; stdin without it. + mufile: Option, + + #[arg(long)] + /// Sign with the deterministic variant (FIPS 204 Section 3.4: rnd all zero) rather than + /// the hedged default. + deterministic: bool, + #[arg(short)] /// Output in hex format. x: bool, }, - /// The HashML-DSA87 signature algorithm. + /// The HashML-DSA-87 signature algorithm, with the hash given by --hash (SHA-512 by default). HashMLDSA87 { action: MLDSAAction, + #[arg(long, value_enum, default_value_t = PreHashAlg::Sha512)] + /// The pre-hash (FIPS 204 Section 5.4); it must be at least as strong as the parameter + /// set, so sha256 and shake128 fit hash-mldsa44 only. + hash: PreHashAlg, + #[arg(long)] /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, @@ -1089,6 +1361,16 @@ enum Subcommands { /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, + #[arg(long)] + /// The message representative file (64 bytes; raw bytes; hex with --hex-input) for + /// sign-mu and verify-mu; stdin without it. + mufile: Option, + + #[arg(long)] + /// Sign with the deterministic variant (FIPS 204 Section 3.4: rnd all zero) rather than + /// the hedged default. + deterministic: bool, + #[arg(short)] /// Output in hex format. x: bool, @@ -1274,11 +1556,11 @@ enum Subcommands { x: bool, }, - /// Perform HMAC-SHA256 of the content provided on stdin. + /// Perform HMAC-SHA224 of the content provided on stdin. /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. - HMAC_SHA256 { + HMAC_SHA224 { /// The MAC key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] @@ -1295,15 +1577,15 @@ enum Subcommands { verify: Option, #[arg(short)] - /// Output the hashes in hex format. + /// Output the tag in hex format. x: bool, }, - /// Perform HMAC-SHA512 of the content provided on stdin. + /// Perform HMAC-SHA256 of the content provided on stdin. /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. - HMAC_SHA512 { + HMAC_SHA256 { /// The MAC key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] @@ -1324,11 +1606,11 @@ enum Subcommands { x: bool, }, - /// Perform HMAC-SHA512/224 of the content provided on stdin. + /// Perform HMAC-SHA384 of the content provided on stdin. /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. - HMAC_SHA512_224 { + HMAC_SHA384 { /// The MAC key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] @@ -1345,15 +1627,15 @@ enum Subcommands { verify: Option, #[arg(short)] - /// Output the hashes in hex format. + /// Output the tag in hex format. x: bool, }, - /// Perform HMAC-SHA512/256 of the content provided on stdin. + /// Perform HMAC-SHA512 of the content provided on stdin. /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. - HMAC_SHA512_256 { + HMAC_SHA512 { /// The MAC key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] @@ -1374,11 +1656,11 @@ enum Subcommands { x: bool, }, - /// Perform HMAC-SM3 of the content provided on stdin. + /// Perform HMAC-SHA512/224 of the content provided on stdin. /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. - HMAC_SM3 { + HMAC_SHA512_224 { /// The MAC key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] @@ -1399,24 +1681,11 @@ enum Subcommands { x: bool, }, - /// Compute or verify a KMAC128 (NIST SP 800-185 Sec 4) over the content provided on stdin. - /// The tag length and customization string are bound into the computation, so the verifier - /// must use the same values. + /// Perform HMAC-SHA512/256 of the content provided on stdin. + /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. - KMAC128 { - /// Length of the tag in bytes, at least 4 (SP 800-185 Sec 8.4.2). - length: usize, - - #[arg(short = 's', long)] - /// Customization string S, as the characters given, domain-separating this use of KMAC - /// from another. - customization: Option, - - #[arg(long)] - /// Customization string S in hex, for bytes that are not text. - customization_hex: Option, - + HMAC_SHA512_256 { /// The MAC key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] @@ -1427,32 +1696,195 @@ enum Subcommands { #[arg(short, long)] key_file: Option, + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. #[arg(short, long)] - /// Verify against this tag (hex) instead of computing one. verify: Option, #[arg(short)] - /// Output the tag in hex format. + /// Output the hashes in hex format. x: bool, }, - /// Compute or verify a KMAC256 (NIST SP 800-185 Sec 4) over the content provided on stdin. - /// See kmac128. + /// Perform HMAC-SHA3-224 of the content provided on stdin. + /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. - KMAC256 { - /// Length of the tag in bytes, at least 4 (SP 800-185 Sec 8.4.2). - length: usize, - - #[arg(short = 's', long)] - /// Customization string S, as the characters given, domain-separating this use of KMAC - /// from another. - customization: Option, - - #[arg(long)] - /// Customization string S in hex, for bytes that are not text. - customization_hex: Option, - + HMAC_SHA3_224 { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + + /// Perform HMAC-SHA3-256 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + HMAC_SHA3_256 { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + + /// Perform HMAC-SHA3-384 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + HMAC_SHA3_384 { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + + /// Perform HMAC-SHA3-512 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + HMAC_SHA3_512 { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + + /// Perform HMAC-SM3 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + HMAC_SM3 { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Compute or verify a KMAC128 (NIST SP 800-185 Sec 4) over the content provided on stdin. + /// The tag length and customization string are bound into the computation, so the verifier + /// must use the same values. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + KMAC128 { + /// Length of the tag in bytes, at least 4 (SP 800-185 Sec 8.4.2). + length: usize, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given, domain-separating this use of KMAC + /// from another. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short, long)] + /// Verify against this tag (hex) instead of computing one. + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + + /// Compute or verify a KMAC256 (NIST SP 800-185 Sec 4) over the content provided on stdin. + /// See kmac128. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + KMAC256 { + /// Length of the tag in bytes, at least 4 (SP 800-185 Sec 8.4.2). + length: usize, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given, domain-separating this use of KMAC + /// from another. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + /// The MAC key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] @@ -1472,6 +1904,68 @@ enum Subcommands { x: bool, }, + /// Compute a KMACXOF128 (NIST SP 800-185 Sec 4.3.1) over the content provided on stdin: KMAC + /// as a XOF, with the output length not bound in, so any length streams. Requires the + /// output length in bytes. Nothing is verified; a reader squeezes the same stream. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + KMACXOF128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + /// The key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the key (raw bytes; hex with --hex-input). + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Compute a KMACXOF256 (NIST SP 800-185 Sec 4.3.1) over the content provided on stdin: KMAC + /// as a XOF, with the output length not bound in, so any length streams. Requires the + /// output length in bytes. Nothing is verified; a reader squeezes the same stream. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + KMACXOF256 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + /// The key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the key (raw bytes; hex with --hex-input). + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// The ML-DSA-44 signature algorithm. MLDSA44 { action: MLDSAAction, @@ -1492,6 +1986,16 @@ enum Subcommands { /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, + #[arg(long)] + /// The message representative file (64 bytes; raw bytes; hex with --hex-input) for + /// sign-mu and verify-mu; stdin without it. + mufile: Option, + + #[arg(long)] + /// Sign with the deterministic variant (FIPS 204 Section 3.4: rnd all zero) rather than + /// the hedged default. + deterministic: bool, + #[arg(short)] /// Output in hex format. x: bool, @@ -1517,6 +2021,16 @@ enum Subcommands { /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, + #[arg(long)] + /// The message representative file (64 bytes; raw bytes; hex with --hex-input) for + /// sign-mu and verify-mu; stdin without it. + mufile: Option, + + #[arg(long)] + /// Sign with the deterministic variant (FIPS 204 Section 3.4: rnd all zero) rather than + /// the hedged default. + deterministic: bool, + #[arg(short)] /// Output in hex format. x: bool, @@ -1542,6 +2056,16 @@ enum Subcommands { /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, + #[arg(long)] + /// The message representative file (64 bytes; raw bytes; hex with --hex-input) for + /// sign-mu and verify-mu; stdin without it. + mufile: Option, + + #[arg(long)] + /// Sign with the deterministic variant (FIPS 204 Section 3.4: rnd all zero) rather than + /// the hedged default. + deterministic: bool, + #[arg(short)] /// Output in hex format. x: bool, @@ -1657,13 +2181,69 @@ enum Subcommands { x: bool, }, - /// Generate cryptographically-secure random bytes, seeded from the operating system's entropy source (/dev/random or equivalent). - /// Uses the library's default 256-bit secure RNG algorithm. + /// Perform ParallelHashXOF128 (NIST SP 800-185 Sec 6.3.1): ParallelHash as a XOF, with + /// the output length not bound in. See parallelhash128 for the block size. + PARALLELHASHXOF128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'b', long)] + /// Block size B in bytes, for the parallel split. + block_size: usize, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Perform ParallelHashXOF256 (NIST SP 800-185 Sec 6.3.1): ParallelHash as a XOF, with + /// the output length not bound in. See parallelhash128 for the block size. + PARALLELHASHXOF256 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'b', long)] + /// Block size B in bytes, for the parallel split. + block_size: usize, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Generate cryptographically-secure random bytes from a Hash_DRBG (SP 800-90A Rev. 1), seeded + /// from the operating system's entropy source (/dev/random or equivalent) unless --seed-file. RNG { /// Number of bytes to generate. If omitted, it will stream continuously until the process is terminated. #[arg(short, long)] len: Option, + /// The hash under the DRBG: sha512 is a 256-bit instance, sha256 a 128-bit one. + #[arg(long, value_enum, default_value_t = DrbgHash::Sha512)] + hash: DrbgHash, + + /// HAZARDOUS: instantiate the DRBG from this file's bytes (raw; hex with --hex-input) + /// as its entropy input instead of the operating system, so the same file gives the + /// same stream. For reproducible test data only; the output is as secret as the file. + #[arg(long)] + seed_file: Option, + #[arg(short)] /// Output in hex format. x: bool, @@ -1824,6 +2404,52 @@ enum Subcommands { /// Output the hashes in hex format. x: bool, }, + + /// Perform TupleHashXOF128 (NIST SP 800-185 Sec 5.3.1): TupleHash as a XOF, with the + /// output length not bound in. See tuplehash128 for the tuple. + TUPLEHASHXOF128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'e', long = "element")] + /// A tuple element, in hex. Repeat for each element, in order. + elements: Vec, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Perform TupleHashXOF256 (NIST SP 800-185 Sec 5.3.1): TupleHash as a XOF, with the + /// output length not bound in. See tuplehash128 for the tuple. + TUPLEHASHXOF256 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'e', long = "element")] + /// A tuple element, in hex. Repeat for each element, in order. + elements: Vec, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, } fn main() { @@ -1996,39 +2622,69 @@ fn run() { Some(Subcommands::Base64Encode) => { encoders_cmd::base64_encode_cmd(); } - Some(Subcommands::HashMLDSA44 { action, ctxfile, skfile, pkfile, sigfile, x }) => { + Some(Subcommands::HashMLDSA44 { + action, + hash, + ctxfile, + skfile, + pkfile, + sigfile, + mufile, + deterministic, + x, + }) => { + let files = + mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; mldsa_cmd::mldsa_cmd( &mldsa_cmd::MLDSA44_SET, action, - ctxfile, - skfile, - pkfile, - sigfile, - true, + &files, + Some(*hash), + *deterministic, *x, ); } - Some(Subcommands::HashMLDSA65 { action, ctxfile, skfile, pkfile, sigfile, x }) => { + Some(Subcommands::HashMLDSA65 { + action, + hash, + ctxfile, + skfile, + pkfile, + sigfile, + mufile, + deterministic, + x, + }) => { + let files = + mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; mldsa_cmd::mldsa_cmd( &mldsa_cmd::MLDSA65_SET, action, - ctxfile, - skfile, - pkfile, - sigfile, - true, + &files, + Some(*hash), + *deterministic, *x, ); } - Some(Subcommands::HashMLDSA87 { action, ctxfile, skfile, pkfile, sigfile, x }) => { + Some(Subcommands::HashMLDSA87 { + action, + hash, + ctxfile, + skfile, + pkfile, + sigfile, + mufile, + deterministic, + x, + }) => { + let files = + mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; mldsa_cmd::mldsa_cmd( &mldsa_cmd::MLDSA87_SET, action, - ctxfile, - skfile, - pkfile, - sigfile, - true, + &files, + Some(*hash), + *deterministic, *x, ); } @@ -2157,41 +2813,47 @@ fn run() { let s = sha3_cmd::load_customization(customization, customization_hex); mac_cmd::kmac_cmd(Strength::Bits256, *length, &s, key, key_file, verify, *x) } - Some(Subcommands::MLDSA44 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::mldsa_cmd( - &mldsa_cmd::MLDSA44_SET, - action, - ctxfile, - skfile, - pkfile, - sigfile, - false, - *x, - ); + Some(Subcommands::MLDSA44 { + action, + ctxfile, + skfile, + pkfile, + sigfile, + mufile, + deterministic, + x, + }) => { + let files = + mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; + mldsa_cmd::mldsa_cmd(&mldsa_cmd::MLDSA44_SET, action, &files, None, *deterministic, *x); } - Some(Subcommands::MLDSA65 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::mldsa_cmd( - &mldsa_cmd::MLDSA65_SET, - action, - ctxfile, - skfile, - pkfile, - sigfile, - false, - *x, - ); + Some(Subcommands::MLDSA65 { + action, + ctxfile, + skfile, + pkfile, + sigfile, + mufile, + deterministic, + x, + }) => { + let files = + mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; + mldsa_cmd::mldsa_cmd(&mldsa_cmd::MLDSA65_SET, action, &files, None, *deterministic, *x); } - Some(Subcommands::MLDSA87 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::mldsa_cmd( - &mldsa_cmd::MLDSA87_SET, - action, - ctxfile, - skfile, - pkfile, - sigfile, - false, - *x, - ); + Some(Subcommands::MLDSA87 { + action, + ctxfile, + skfile, + pkfile, + sigfile, + mufile, + deterministic, + x, + }) => { + let files = + mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; + mldsa_cmd::mldsa_cmd(&mldsa_cmd::MLDSA87_SET, action, &files, None, *deterministic, *x); } Some(Subcommands::MLKEM512 { action, skfile, pkfile, ctfile, x }) => { mlkem_cmd::mlkem_cmd(&mlkem_cmd::MLKEM512_SET, action, skfile, pkfile, ctfile, *x); @@ -2222,7 +2884,9 @@ fn run() { let s = sha3_cmd::load_customization(customization, customization_hex); sha3_cmd::parallelhash_cmd(Strength::Bits256, *length, *block_size, &s, *x); } - Some(Subcommands::RNG { len, x }) => rng_cmd::rng_cmd(*len, *x), + Some(Subcommands::RNG { len, hash, seed_file, x }) => { + rng_cmd::rng_cmd(*len, *hash, seed_file, *x) + } Some(Subcommands::SHA3_224 { x }) => { sha3_cmd::sha3_cmd(SHA3Variant::SHA3_224, *x); } @@ -2282,6 +2946,126 @@ fn run() { let s = sha3_cmd::load_customization(customization, customization_hex); sha3_cmd::tuplehash_cmd(Strength::Bits256, *length, elements, &s, *x); } + Some(Subcommands::AES128_CMAC { key, key_file, verify, x }) => { + mac_cmd::aes128_cmac_cmd(key, key_file, verify, *x) + } + Some(Subcommands::AES128_GMAC { key, key_file, iv, iv_file, verify, x }) => { + mac_cmd::aes128_gmac_cmd(key, key_file, iv, iv_file, verify, *x) + } + Some(Subcommands::AES192_CMAC { key, key_file, verify, x }) => { + mac_cmd::aes192_cmac_cmd(key, key_file, verify, *x) + } + Some(Subcommands::AES192_GMAC { key, key_file, iv, iv_file, verify, x }) => { + mac_cmd::aes192_gmac_cmd(key, key_file, iv, iv_file, verify, *x) + } + Some(Subcommands::AES256_CMAC { key, key_file, verify, x }) => { + mac_cmd::aes256_cmac_cmd(key, key_file, verify, *x) + } + Some(Subcommands::AES256_GMAC { key, key_file, iv, iv_file, verify, x }) => { + mac_cmd::aes256_gmac_cmd(key, key_file, iv, iv_file, verify, *x) + } + Some(Subcommands::CSHAKE128 { + length, + function_name, + customization, + customization_hex, + x, + }) => { + let n = function_name.as_deref().unwrap_or("").as_bytes(); + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::cshake_cmd(Strength::Bits128, *length, n, &s, *x); + } + Some(Subcommands::KMACXOF128 { + length, + customization, + customization_hex, + key, + key_file, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + mac_cmd::kmacxof_cmd(Strength::Bits128, *length, &s, key, key_file, *x) + } + Some(Subcommands::TUPLEHASHXOF128 { + length, + elements, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::tuplehashxof_cmd(Strength::Bits128, *length, elements, &s, *x); + } + Some(Subcommands::PARALLELHASHXOF128 { + length, + block_size, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::parallelhashxof_cmd(Strength::Bits128, *length, *block_size, &s, *x); + } + Some(Subcommands::CSHAKE256 { + length, + function_name, + customization, + customization_hex, + x, + }) => { + let n = function_name.as_deref().unwrap_or("").as_bytes(); + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::cshake_cmd(Strength::Bits256, *length, n, &s, *x); + } + Some(Subcommands::KMACXOF256 { + length, + customization, + customization_hex, + key, + key_file, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + mac_cmd::kmacxof_cmd(Strength::Bits256, *length, &s, key, key_file, *x) + } + Some(Subcommands::TUPLEHASHXOF256 { + length, + elements, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::tuplehashxof_cmd(Strength::Bits256, *length, elements, &s, *x); + } + Some(Subcommands::PARALLELHASHXOF256 { + length, + block_size, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::parallelhashxof_cmd(Strength::Bits256, *length, *block_size, &s, *x); + } + Some(Subcommands::HMAC_SHA224 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA224, key, key_file, verify, *x) + } + Some(Subcommands::HMAC_SHA384 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA384, key, key_file, verify, *x) + } + Some(Subcommands::HMAC_SHA3_224 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA3_224, key, key_file, verify, *x) + } + Some(Subcommands::HMAC_SHA3_256 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA3_256, key, key_file, verify, *x) + } + Some(Subcommands::HMAC_SHA3_384 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA3_384, key, key_file, verify, *x) + } + Some(Subcommands::HMAC_SHA3_512 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA3_512, key, key_file, verify, *x) + } None => { eprintln!("No command provided. See -h") } diff --git a/cli/src/mldsa_cmd.rs b/cli/src/mldsa_cmd.rs index d8ba3628c..e8ee1cf88 100644 --- a/cli/src/mldsa_cmd.rs +++ b/cli/src/mldsa_cmd.rs @@ -3,29 +3,33 @@ //! functions that make and check a key pair. Those come in through a [`ParameterSet`], so a //! length or a function can no longer be right for two sets and wrong for the third. //! -//! Private keys and seeds are read through the zeroized secret path; public keys, signatures -//! and contexts through the plain one. All of them are raw bytes, or hex with `--hex-input`. +//! Private keys and seeds are read through the zeroized secret path; public keys, signatures, +//! contexts and message representatives through the plain one. All of them are raw bytes, or +//! hex with `--hex-input`. use crate::helpers::{ - parse_seed, read_input_file, read_secret_file, read_secret_file_or_stdin, seed_error, - stream_stdin, write_bytes_or_hex, + parse_seed, read_input_file, read_input_file_or_stdin, read_secret_file, + read_secret_file_or_stdin, seed_error, stream_stdin, write_bytes_or_hex, }; use bouncycastle::core::errors::SignatureError; use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait}; use bouncycastle::core::signature_key::{ - PreHashSigningKey, PreHashVerifyingKey, SignatureAlgorithm, SigningKey, VerifyingKey, + DeterministicPreHashSigningKey, DeterministicSigningKey, PreHashSigningKey, + PreHashVerifyingKey, SignatureAlgorithm, SigningKey, VerifyingKey, }; use bouncycastle::core::traits::{ RNG, SignaturePrivateKey, SignaturePublicKey, SignatureSigner, SignatureVerifier, }; use bouncycastle::mldsa::{ - MLDSA_SEED_LEN, MLDSA44, MLDSA44_PK_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN, MLDSA44PrivateKey, - MLDSA44PublicKey, MLDSA65, MLDSA65_PK_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN, MLDSA65PrivateKey, - MLDSA65PublicKey, MLDSA87, MLDSA87_PK_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN, MLDSA87PrivateKey, - MLDSA87PublicKey, MLDSAPrivateKeyTrait, MLDSATrait, + MLDSA_MU_LEN, MLDSA_SEED_LEN, MLDSA44, MLDSA44_PK_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN, + MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65, MLDSA65_PK_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN, + MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87, MLDSA87_PK_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN, + MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPrivateKeyTrait, MLDSAPublicKeyTrait, MLDSATrait, + MuBuilder, }; use bouncycastle::rng::HashDRBG_SHA512; -use bouncycastle::sha2::SHA512; +use bouncycastle::sha2::{SHA256, SHA512}; +use bouncycastle::sha3::{SHAKE128, SHAKE256}; use clap::ValueEnum; use core::marker::PhantomData; use std::process::exit; @@ -41,16 +45,48 @@ pub(crate) enum MLDSAAction { /// Accepts a sk (stdin or --skfile) and a pk (--pkfile), and checks that they match. CheckConsistency, /// Sign a message read from stdin with the private key in --skfile and output the signature. - /// Accepts the private key as full or seed. + /// Accepts the private key as full or seed. Hedged (FIPS 204 Section 3.4) unless + /// --deterministic. Sign, /// Verify a message read from stdin against the public key in --pkfile and the signature in /// --sigfile. Verify, + /// Output the 64-byte message representative mu (FIPS 204 Algorithm 7 line 6) of the + /// message on stdin under the public key in --pkfile and the context, for signing elsewhere + /// with sign-mu. Not for hash-mldsa*, whose representative frames the digest differently. + ComputeMu, + /// Sign a message representative (64 bytes, from --mufile or stdin) with the private key in + /// --skfile: the external-mu interface. The signature verifies as an ordinary one. + SignMu, + /// Verify the signature in --sigfile against a message representative (64 bytes, from + /// --mufile or stdin) under the public key in --pkfile. + VerifyMu, +} + +/// The pre-hash of HashML-DSA (FIPS 204 Section 5.4): the hash must carry at least the +/// parameter set's strength, so SHA-256 and SHAKE128 (RFC 8702's 32-byte digest) fit ML-DSA-44 +/// only. +#[derive(ValueEnum, Clone, Copy, Debug)] +pub(crate) enum PreHashAlg { + Sha256, + Sha512, + Shake128, + Shake256, +} + +/// The files a command names. +pub(crate) struct Files<'a> { + pub(crate) ctx: &'a Option, + pub(crate) sk: &'a Option, + pub(crate) pk: &'a Option, + pub(crate) sig: &'a Option, + pub(crate) mu: &'a Option, } /// A parameter set as the command sees it: its name for messages, the algorithm marker the -/// signing traits are keyed by, and the key-pair operations the core traits do not carry. The -/// three sets below are the only instances. +/// signing traits are keyed by, and the operations the core traits do not carry: the key pair's, +/// and the external-mu interface of the ML-DSA key traits. The three sets below are the only +/// instances. #[allow(clippy::type_complexity)] // the function-pointer fields are the table's point pub(crate) struct ParameterSet< A, @@ -66,6 +102,10 @@ pub(crate) struct ParameterSet< keygen_from_seed: fn(&KeyMaterial) -> Result<(PK, SK), SignatureError>, keypair_consistency_check: fn(&PK, &SK) -> Result<(), SignatureError>, pk_of: fn(&SK) -> PK, + mu_builder: fn(&PK, Option<&[u8]>) -> Result, + sign_mu: fn(&SK, &[u8; MLDSA_MU_LEN], &mut dyn RNG) -> Result<[u8; SIG_LEN], SignatureError>, + sign_mu_deterministic: fn(&SK, &[u8; MLDSA_MU_LEN]) -> Result<[u8; SIG_LEN], SignatureError>, + verify_mu: fn(&PK, &[u8; MLDSA_MU_LEN], &[u8; SIG_LEN]) -> Result<(), SignatureError>, } pub(crate) const MLDSA44_SET: ParameterSet< @@ -82,6 +122,10 @@ pub(crate) const MLDSA44_SET: ParameterSet< keygen_from_seed: MLDSA44::keygen_from_seed, keypair_consistency_check: MLDSA44::keypair_consistency_check, pk_of: |sk| sk.derive_pk(), + mu_builder: |pk, ctx| pk.create_mu_builder(ctx), + sign_mu: |sk, mu, rng| sk.sign_mu(mu, rng), + sign_mu_deterministic: |sk, mu| sk.sign_mu_deterministic(mu), + verify_mu: |pk, mu, sig| pk.verify_mu(mu, sig), }; pub(crate) const MLDSA65_SET: ParameterSet< @@ -98,6 +142,10 @@ pub(crate) const MLDSA65_SET: ParameterSet< keygen_from_seed: MLDSA65::keygen_from_seed, keypair_consistency_check: MLDSA65::keypair_consistency_check, pk_of: |sk| sk.derive_pk(), + mu_builder: |pk, ctx| pk.create_mu_builder(ctx), + sign_mu: |sk, mu, rng| sk.sign_mu(mu, rng), + sign_mu_deterministic: |sk, mu| sk.sign_mu_deterministic(mu), + verify_mu: |pk, mu, sig| pk.verify_mu(mu, sig), }; pub(crate) const MLDSA87_SET: ParameterSet< @@ -114,25 +162,31 @@ pub(crate) const MLDSA87_SET: ParameterSet< keygen_from_seed: MLDSA87::keygen_from_seed, keypair_consistency_check: MLDSA87::keypair_consistency_check, pk_of: |sk| sk.derive_pk(), + mu_builder: |pk, ctx| pk.create_mu_builder(ctx), + sign_mu: |sk, mu, rng| sk.sign_mu(mu, rng), + sign_mu_deterministic: |sk, mu| sk.sign_mu_deterministic(mu), + verify_mu: |pk, mu, sig| pk.verify_mu(mu, sig), }; -/// `prehash` selects HashML-DSA with SHA-512 (FIPS 204 Section 5.4) over pure ML-DSA. -#[allow(clippy::too_many_arguments)] // one flag per file the actions take +/// `prehash` selects HashML-DSA with that hash (FIPS 204 Section 5.4) over pure ML-DSA; +/// `deterministic` the variant with `rnd` all zero (Section 3.4) over the hedged one. pub(crate) fn mldsa_cmd< A: SignatureAlgorithm, PK: SignaturePublicKey + VerifyingKey + PreHashVerifyingKey, - SK: SignaturePrivateKey + SigningKey + PreHashSigningKey, + SK: SignaturePrivateKey + + SigningKey + + DeterministicSigningKey + + PreHashSigningKey + + DeterministicPreHashSigningKey, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, >( set: &ParameterSet, action: &MLDSAAction, - ctxfile: &Option, - skfile: &Option, - pkfile: &Option, - sigfile: &Option, - prehash: bool, + files: &Files, + prehash: Option, + deterministic: bool, output_hex: bool, ) { match action { @@ -142,19 +196,19 @@ pub(crate) fn mldsa_cmd< write_bytes_or_hex(&sk.encode(), output_hex); } MLDSAAction::KeygenFromSeed => { - let input = read_secret_file_or_stdin(skfile); + let input = read_secret_file_or_stdin(files.sk); let seed = parse_seed::(input.ref_to_bytes()) .unwrap_or_else(|e| seed_error::(e)); let (_pk, sk) = (set.keygen_from_seed)(&seed).unwrap_or_else(|e| keygen_error(e)); write_bytes_or_hex(&sk.encode(), output_hex); } MLDSAAction::PkFromSk => { - let sk = parse_sk(set, read_secret_file_or_stdin(skfile).ref_to_bytes()); + let sk = parse_sk(set, read_secret_file_or_stdin(files.sk).ref_to_bytes()); write_bytes_or_hex(&(set.pk_of)(&sk).encode(), output_hex); } MLDSAAction::CheckConsistency => { - let sk = parse_sk(set, read_secret_file_or_stdin(skfile).ref_to_bytes()); - let pk = parse_pk(set, &read_required_file(pkfile, "pkfile")); + let sk = parse_sk(set, read_secret_file_or_stdin(files.sk).ref_to_bytes()); + let pk = parse_pk(set, &read_required_file(files.pk, "pkfile")); match (set.keypair_consistency_check)(&pk, &sk) { Ok(_) => crate::helpers::println_stdout("SUCCESS: pk and sk match."), Err(_) => { @@ -164,47 +218,152 @@ pub(crate) fn mldsa_cmd< } } MLDSAAction::Sign => { - let Some(skfile) = skfile else { - eprintln!("Error: no skfile provided."); - exit(-1); - }; - let sk = parse_sk(set, read_secret_file(skfile).ref_to_bytes()); - let ctx = read_ctx(ctxfile); + let sk = parse_sk(set, read_required_secret(files.sk, "skfile").ref_to_bytes()); + let ctx = read_ctx(files.ctx); + let ctx = Some(ctx.as_slice()); let mut rng = HashDRBG_SHA512::new_from_os(); - if prehash { - let signer = sk - .create_ph_signer::(&mut rng, Some(&ctx)) - .unwrap_or_else(|e| signer_error(e)); - sign_stream(signer, output_hex); - } else { - let signer = - sk.create_signer(&mut rng, Some(&ctx)).unwrap_or_else(|e| signer_error(e)); - sign_stream(signer, output_hex); + match (prehash, deterministic) { + (None, false) => { + let s = sk.create_signer(&mut rng, ctx).unwrap_or_else(|e| signer_error(e)); + sign_stream(s, output_hex) + } + (None, true) => { + let s = sk.create_signer_deterministic(ctx).unwrap_or_else(|e| signer_error(e)); + sign_stream(s, output_hex) + } + (Some(PreHashAlg::Sha256), false) => { + let s = sk.create_ph_signer::(&mut rng, ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Sha512), false) => { + let s = sk.create_ph_signer::(&mut rng, ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Shake128), false) => { + let s = sk.create_ph_signer::(&mut rng, ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Shake256), false) => { + let s = sk.create_ph_signer::(&mut rng, ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Sha256), true) => { + let s = sk.create_ph_signer_deterministic::(ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Sha512), true) => { + let s = sk.create_ph_signer_deterministic::(ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Shake128), true) => { + let s = sk.create_ph_signer_deterministic::(ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Shake256), true) => { + let s = sk.create_ph_signer_deterministic::(ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } } } MLDSAAction::Verify => { - let pk = parse_pk(set, &read_required_file(pkfile, "pkfile")); - let ctx = read_ctx(ctxfile); - let sig = read_required_file(sigfile, "sigfile"); - if prehash { - let verifier = - pk.create_ph_verifier::(Some(&ctx)).unwrap_or_else(|e| signer_error(e)); - verify_stream(verifier, &sig); + let pk = parse_pk(set, &read_required_file(files.pk, "pkfile")); + let ctx = read_ctx(files.ctx); + let ctx = Some(ctx.as_slice()); + let sig = read_required_file(files.sig, "sigfile"); + match prehash { + None => { + let v = pk.create_verifier(ctx).unwrap_or_else(|e| signer_error(e)); + verify_stream(v, &sig) + } + Some(PreHashAlg::Sha256) => { + let v = pk.create_ph_verifier::(ctx); + verify_stream(v.unwrap_or_else(|e| signer_error(e)), &sig) + } + Some(PreHashAlg::Sha512) => { + let v = pk.create_ph_verifier::(ctx); + verify_stream(v.unwrap_or_else(|e| signer_error(e)), &sig) + } + Some(PreHashAlg::Shake128) => { + let v = pk.create_ph_verifier::(ctx); + verify_stream(v.unwrap_or_else(|e| signer_error(e)), &sig) + } + Some(PreHashAlg::Shake256) => { + let v = pk.create_ph_verifier::(ctx); + verify_stream(v.unwrap_or_else(|e| signer_error(e)), &sig) + } + } + } + MLDSAAction::ComputeMu => { + refuse_mu_with_prehash(prehash); + let pk = parse_pk(set, &read_required_file(files.pk, "pkfile")); + let ctx = read_ctx(files.ctx); + let mut mb = (set.mu_builder)(&pk, Some(&ctx)).unwrap_or_else(|e| signer_error(e)); + stream_stdin(|chunk| mb.do_update(chunk)); + write_bytes_or_hex(&mb.do_final(), output_hex); + } + MLDSAAction::SignMu => { + refuse_mu_with_prehash(prehash); + let sk = parse_sk(set, read_required_secret(files.sk, "skfile").ref_to_bytes()); + let mu = read_mu(files.mu); + let sig = if deterministic { + (set.sign_mu_deterministic)(&sk, &mu) + } else { + (set.sign_mu)(&sk, &mu, &mut HashDRBG_SHA512::new_from_os()) + }; + let sig = sig.unwrap_or_else(|e| { + eprintln!("Error: signing failed: {e:?}"); + exit(-1); + }); + write_bytes_or_hex(&sig, output_hex); + } + MLDSAAction::VerifyMu => { + refuse_mu_with_prehash(prehash); + let pk = parse_pk(set, &read_required_file(files.pk, "pkfile")); + let sig = read_required_file(files.sig, "sigfile"); + let sig: &[u8; SIG_LEN] = sig.as_slice().try_into().unwrap_or_else(|_| { + eprintln!( + "Error: the signature is {} bytes; an {} signature is {SIG_LEN}.", + sig.len(), + set.name + ); + exit(-1); + }); + let mu = read_mu(files.mu); + if (set.verify_mu)(&pk, &mu, sig).is_ok() { + crate::helpers::println_stdout("Signature is valid."); } else { - let verifier = pk.create_verifier(Some(&ctx)).unwrap_or_else(|e| signer_error(e)); - verify_stream(verifier, &sig); + eprintln!("Error: the signature is invalid."); + exit(-1); } } } } +/// The mu actions are pure ML-DSA's: HashML-DSA's representative frames the digest and the +/// hash's OID (FIPS 204 Algorithm 4 line 23), which `compute-mu` does not produce. +fn refuse_mu_with_prehash(prehash: Option) { + if prehash.is_some() { + eprintln!("Error: the mu actions are for the mldsa* commands, not hash-mldsa*."); + exit(-1); + } +} + +/// The 64-byte message representative from `--mufile` or stdin. +fn read_mu(mufile: &Option) -> [u8; MLDSA_MU_LEN] { + let mu = read_input_file_or_stdin(mufile); + mu.as_slice().try_into().unwrap_or_else(|_| { + eprintln!("Error: mu must be {MLDSA_MU_LEN} bytes, got {}.", mu.len()); + exit(-1); + }) +} + fn keygen_error(e: SignatureError) -> ! { eprintln!("Error: key generation failed: {e:?}"); exit(-1); } /// A signer or verifier refused to start: the context is over 255 bytes (FIPS 204 Algorithm 2 -/// line 1), or the RNG failed. +/// line 1), the pre-hash is too weak for the parameter set, or the RNG failed. fn signer_error(e: SignatureError) -> ! { eprintln!("Error: couldn't start the signature: {e:?}"); exit(-1); @@ -247,6 +406,17 @@ fn read_required_file(file: &Option, flag: &str) -> Vec { } } +/// `--skfile` where signing cannot take the key from stdin, which carries the message. +fn read_required_secret(file: &Option, flag: &str) -> crate::helpers::SecretBytes { + match file { + Some(file) => read_secret_file(file), + None => { + eprintln!("Error: no {flag} provided."); + exit(-1); + } + } +} + /// A private key from its bytes: a full key, or a seed. fn parse_sk< A, diff --git a/cli/src/rng_cmd.rs b/cli/src/rng_cmd.rs index b16f1a011..83bd6e50d 100644 --- a/cli/src/rng_cmd.rs +++ b/cli/src/rng_cmd.rs @@ -1,13 +1,66 @@ +//! The `rng` subcommand: bytes from a Hash_DRBG (SP 800-90A Rev. 1), seeded from the operating +//! system, or from a file for a reproducible stream. + +use bouncycastle::core::hazmat::do_hazardous_operations; +use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle::core::security_strength::SecurityStrength; use bouncycastle::core::traits::RNG; -use bouncycastle::factory::AlgorithmFactory; -use bouncycastle::factory::rng_factory::RNGFactory; +use bouncycastle::rng::hazmat::NewUninitialized; +use bouncycastle::rng::{HashDRBG_SHA256, HashDRBG_SHA512, Sp80090ADrbg}; +use clap::ValueEnum; use std::process::exit; -use crate::helpers::write_bytes_or_hex; +use crate::helpers::{CHUNK_LEN, read_secret_file, write_bytes_or_hex, write_stdout}; + +/// The hash under the DRBG: SHA-256 gives a 128-bit instance, SHA-512 a 256-bit one. +#[derive(ValueEnum, Clone, Copy, Debug)] +pub(crate) enum DrbgHash { + Sha256, + Sha512, +} + +pub(crate) fn rng_cmd( + len: Option, + hash: DrbgHash, + seed_file: &Option, + output_hex: bool, +) { + match (hash, seed_file) { + (DrbgHash::Sha256, None) => generate(HashDRBG_SHA256::new_from_os(), len, output_hex), + (DrbgHash::Sha512, None) => generate(HashDRBG_SHA512::new_from_os(), len, output_hex), + (DrbgHash::Sha256, Some(file)) => { + generate(seeded::(file, SecurityStrength::_128bit), len, output_hex) + } + (DrbgHash::Sha512, Some(file)) => { + generate(seeded::(file, SecurityStrength::_256bit), len, output_hex) + } + } +} + +/// A DRBG instantiated from the bytes of `file` as its entropy input, with an empty nonce and +/// personalization string (SP 800-90A Rev. 1 Section 10.1.1.2): the same file gives the same +/// stream, which is the point, and the reason the help calls it hazardous. The seed must carry +/// at least the instance's strength, which the library checks by its length. +fn seeded(file: &str, strength: SecurityStrength) -> D { + let mut seed = read_secret_file(file); + do_hazardous_operations(&mut seed, |seed| seed.set_key_type(KeyType::Seed)).unwrap_or_else( + |e| { + eprintln!("Error: couldn't use the seed file: {e:?}"); + exit(-1); + }, + ); + let mut drbg = D::new_uninitialized(); + drbg.instantiate(false, seed, &KeyMaterial::<0>::new(), &[], strength).unwrap_or_else(|e| { + eprintln!( + "Error: couldn't instantiate the DRBG from the seed file (it must carry {strength:?} of entropy, by its length): {e:?}" + ); + exit(-1); + }); + drbg +} -pub(crate) fn rng_cmd(len: Option, output_hex: bool) { - let mut rng = RNGFactory::default_256_bit(); - let mut buf = vec![0u8; 1024]; +fn generate(mut rng: impl RNG, len: Option, output_hex: bool) { + let mut buf = vec![0u8; CHUNK_LEN]; let loop_forever = len.is_none(); let mut bytes_left_to_write = len.unwrap_or(u32::MAX) as usize; @@ -26,6 +79,6 @@ pub(crate) fn rng_cmd(len: Option, output_hex: bool) { // Only a hex line gets a terminator: raw output is exactly `len` bytes, so that // `rng --len 16 > key.bin` is a 16-byte key and not a 17-byte one. if output_hex { - crate::helpers::write_stdout(b"\n"); + write_stdout(b"\n"); } } diff --git a/cli/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index 1185a0dc6..6babe84f4 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -1,14 +1,22 @@ -//! The SHA-3 family subcommands: `sha3-*`, `shake*`, `tuplehash*` and `parallelhash*`. +//! The SHA-3 family subcommands: `sha3-*`, `shake*`, `cshake*`, `tuplehash*`, `tuplehashxof*`, +//! `parallelhash*` and `parallelhashxof*`. use bouncycastle::core::traits::Hash; -use bouncycastle::sha3::parallelhash::{ParallelHash128, ParallelHash256}; -use bouncycastle::sha3::tuplehash::{TupleHash128, TupleHash256}; -use bouncycastle::sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256}; +use bouncycastle::core::traits::XOF; +use bouncycastle::sha3::parallelhash::{ + ParallelHash128, ParallelHash256, ParallelHashXOF128, ParallelHashXOF256, +}; +use bouncycastle::sha3::tuplehash::{TupleHash128, TupleHash256, TupleHashXOF128, TupleHashXOF256}; +use bouncycastle::sha3::{ + CSHAKE128, CSHAKE256, SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, +}; use core::num::NonZeroUsize; use std::process::exit; -use crate::helpers::{decode_hex_arg, read_stdin_all, stream_hash, stream_stdin, stream_xof}; +use crate::helpers::{ + decode_hex_arg, read_stdin_all, squeeze_to_stdout, stream_hash, stream_stdin, stream_xof, +}; /// The most a fixed-length SP 800-185 function (KMAC, TupleHash, ParallelHash) will produce /// here: their output length is bound into the computation before any output, so the library @@ -72,6 +80,68 @@ pub(crate) fn shake_cmd(strength: Strength, output_len: usize, output_hex: bool) } } +/// cSHAKE (NIST SP 800-185 Sec 3): SHAKE with a function name N and a customization string S +/// bound in; with both empty it is SHAKE itself. +pub(crate) fn cshake_cmd( + strength: Strength, + output_len: usize, + function_name: &[u8], + customization: &[u8], + output_hex: bool, +) { + match strength { + Strength::Bits128 => { + stream_xof(CSHAKE128::new(function_name, customization), output_len, output_hex) + } + Strength::Bits256 => { + stream_xof(CSHAKE256::new(function_name, customization), output_len, output_hex) + } + } +} + +/// TupleHashXOF (SP 800-185 Sec 5.3.1): TupleHash with the length not bound in, so the output +/// streams at any length. +pub(crate) fn tuplehashxof_cmd( + strength: Strength, + output_len: usize, + elements: &[String], + customization: &[u8], + output_hex: bool, +) { + let tuple = load_tuple(elements); + match strength { + Strength::Bits128 => { + let mut x = TupleHashXOF128::new(customization); + tuple.iter().for_each(|e| x.do_update(e)); + squeeze_to_stdout(x.into_squeezer(), output_len, output_hex); + } + Strength::Bits256 => { + let mut x = TupleHashXOF256::new(customization); + tuple.iter().for_each(|e| x.do_update(e)); + squeeze_to_stdout(x.into_squeezer(), output_len, output_hex); + } + } +} + +/// ParallelHashXOF (SP 800-185 Sec 6.3.1): ParallelHash with the length not bound in. +pub(crate) fn parallelhashxof_cmd( + strength: Strength, + output_len: usize, + block_size: usize, + customization: &[u8], + output_hex: bool, +) { + let block_size = require_block_size(block_size); + match strength { + Strength::Bits128 => { + stream_xof(ParallelHashXOF128::new(block_size, customization), output_len, output_hex) + } + Strength::Bits256 => { + stream_xof(ParallelHashXOF256::new(block_size, customization), output_len, output_hex) + } + } +} + /// TupleHash (NIST SP 800-185 Sec 5): hashes a *tuple* of strings unambiguously. /// /// The tuple comes from repeated `--element` flags, each a hex string. With none given, stdin is From 32b09ad40cf365e2dc51183562118bb69f7ca13a Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 10:36:49 +1100 Subject: [PATCH 42/57] cli/tests: the CLI review's eight known-answer scripts (sha2, sha3, sm3, hmac KATs, encoders, mlkem, mldsa, rng), adapted to the raw-or-hex input rule and extended for the new subcommands (cSHAKE, the SP 800-185 XOF forms, HMAC-SHA224/384/SHA3, the deterministic and external-mu ML-DSA actions, the pre-hash choice, the seeded rng); a CMAC/GMAC script against the ACVP files; GNU timeout and BSD wc padding handled for macOS; and a README example of the binary Assisted-by: Claude:claude-fable-5-1 --- README.md | 14 ++ cli/tests/lib.sh | 7 + cli/tests/test_aes_ctr.sh | 2 +- cli/tests/test_aes_gcm.sh | 2 +- cli/tests/test_aes_mac.sh | 98 +++++++++++++ cli/tests/test_encoders.sh | 136 ++++++++++++++++++ cli/tests/test_hmac_kat.sh | 180 +++++++++++++++++++++++ cli/tests/test_mldsa.sh | 282 +++++++++++++++++++++++++++++++++++++ cli/tests/test_mlkem.sh | 195 +++++++++++++++++++++++++ cli/tests/test_rng.sh | 79 +++++++++++ cli/tests/test_sha2.sh | 105 ++++++++++++++ cli/tests/test_sha3.sh | 257 +++++++++++++++++++++++++++++++++ cli/tests/test_sm3.sh | 31 ++++ 13 files changed, 1386 insertions(+), 2 deletions(-) create mode 100755 cli/tests/test_aes_mac.sh create mode 100755 cli/tests/test_encoders.sh create mode 100755 cli/tests/test_hmac_kat.sh create mode 100755 cli/tests/test_mldsa.sh create mode 100755 cli/tests/test_mlkem.sh create mode 100755 cli/tests/test_rng.sh create mode 100755 cli/tests/test_sha2.sh create mode 100755 cli/tests/test_sha3.sh create mode 100755 cli/tests/test_sm3.sh diff --git a/README.md b/README.md index ddc49928a..1830e39ae 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,20 @@ You can build the main library and the `bc-rust` command-line utility with: cargo build -p cli ``` +The utility exposes every primitive as a streaming stdin-to-stdout subcommand; `bc-rust --help` +lists them and `bc-rust --help` the flags. For example: + +``` +echo -n abc | target/debug/bc-rust sha256 -x +target/debug/bc-rust rng --len 32 > key.bin +target/debug/bc-rust aes256-gcm -d encrypt --key-file key.bin < plain > cipher +target/debug/bc-rust aes256-gcm -d decrypt --key-file key.bin < cipher +``` + +Values given inline (`--key`, `--iv`, ...) are hex; files are raw bytes, or hex text with the global +`--hex-input` flag. The shell tests under `cli/tests/` (`cli/tests/test_all.sh`) show every +subcommand in use. + Or you can build a single sub-crate by name, for example: ``` diff --git a/cli/tests/lib.sh b/cli/tests/lib.sh index 5e2876526..913987917 100644 --- a/cli/tests/lib.sh +++ b/cli/tests/lib.sh @@ -23,6 +23,13 @@ if [ ! -x "$BC_RUST" ]; then exit 2 fi +# `timeout` is GNU coreutils; macOS has none, but has perl, whose alarm does the same job. +if ! command -v timeout >/dev/null 2>&1; then + timeout() { + perl -e 'alarm shift; exec @ARGV' "$@" + } +fi + SCRATCH_ROOT=/tmp/bc-rust-cli-tests mkdir -p "$SCRATCH_ROOT" SCRATCH="$(mktemp -d "$SCRATCH_ROOT/$(basename "$0" .sh).XXXXXXXX")" diff --git a/cli/tests/test_aes_ctr.sh b/cli/tests/test_aes_ctr.sh index 6f418ba9c..e93f66673 100755 --- a/cli/tests/test_aes_ctr.sh +++ b/cli/tests/test_aes_ctr.sh @@ -148,7 +148,7 @@ test_each_invocation_uses_a_fresh_nonce() { "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "run $i still decrypts" done - assert_eq "$(sort -u "$TMP/nonces" | wc -l)" 8 "eight encryptions must draw eight distinct nonces" + assert_eq "$(sort -u "$TMP/nonces" | wc -l | tr -d ' ')" 8 "eight encryptions must draw eight distinct nonces" } # ---- keys ------------------------------------------------------------------------------------ diff --git a/cli/tests/test_aes_gcm.sh b/cli/tests/test_aes_gcm.sh index 98c32662a..53adbaa42 100755 --- a/cli/tests/test_aes_gcm.sh +++ b/cli/tests/test_aes_gcm.sh @@ -77,7 +77,7 @@ test_each_invocation_uses_a_fresh_nonce() { "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "run $i still decrypts" done - assert_eq "$(sort -u "$TMP/nonces" | wc -l)" 8 "eight encryptions must draw eight distinct nonces" + assert_eq "$(sort -u "$TMP/nonces" | wc -l | tr -d ' ')" 8 "eight encryptions must draw eight distinct nonces" } test_hex_output_composes_through_hex_decode() { diff --git a/cli/tests/test_aes_mac.sh b/cli/tests/test_aes_mac.sh new file mode 100755 index 000000000..0a76908ab --- /dev/null +++ b/cli/tests/test_aes_mac.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# The aes*-cmac and aes*-gmac subcommands against the ACVP known answers, and their round trips. +# +# Written for the 2026-11-03 CLI review (report 15) from the vector files named in each test; +# every expected value is copied from that file, none is typed by hand. Follows lib.sh's +# conventions: each test_* runs in its own subshell under `set -e`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# ACVP CMAC-AES (bc-test-data/crypto/aes_tdes_vectors/CMAC/CMAC-AES.4014544.{req,rsp}.json), +# tgId 6 tcId 41: AES-128, a 416-byte message, a 16-byte tag. +test_aes128_cmac_matches_acvp() { + local got + unhex 93D255B8DD1D4FEF87188DE84B0D011887DC20BB688B5C26C263D449CD2E34D0A155715EC8F22090C5EB3D1E9DE35A201BA964F571D28605301EABFEAE15DBC4372ACCA52E1CC2EBD1918EEE918D446D61825EA8F36CFA6EFA9F7B8ADDF22253DACEABE151BBA37A5257616C4181A5ABA888F0D068B593E30C1E60D197144233FECF6F3639EDF65152C75D864F068D5B71129E1F2520224A04E5CF480CF542AFDEB173CEDC16BE47A815F08BFF98C8DB676DC6D9C420E4719D4C4CF7349E6642535D1E18E2F4C837EB63400991C09651DC03E77338950DFCC955C2A44DAAAAE81E814C96E9A5DF4EE0FE6ED11DAEC75FF19AA1DF8785C49FF308BDC242A8365A345DB7581F87EB7EE36FF8FC3500048617044D8404DF46D6DA6B1DD523C2BDD9FBA4D12235DF643F1EA6BECE5B9E0B11DDE085008231FF0680EC3784C60A24A13E549A4111E1679D05CFB603E5C7C311F0E4DF4864FC2D96E5E3049C7C690ED5A9A3961EB0FBF5BC28E9FCAFC7E4E98489B3EED5DFCE45D4734B3D079E9086027F7EE8CF4B4CC5DEBABF16AAD0A1E123EBA22D036C8E539273E16B75DAB07C3F >"$TMP/msg" + assert_size "$TMP/msg" 416 "message length" + got=$(hex_out "$BC_RUST" aes128-cmac --key 94cd50a907d427b6b75a643361b8b5de -x <"$TMP/msg") + assert_eq "$got" "2c930aaccc775c99e421a0989621947c" "CMAC-AES128 tcId 41" + expect_ok "the tag verifies" "$BC_RUST" aes128-cmac --key 94cd50a907d427b6b75a643361b8b5de -v 2c930aaccc775c99e421a0989621947c <"$TMP/msg" + expect_fail "a flipped tag does not" "$BC_RUST" aes128-cmac --key 94cd50a907d427b6b75a643361b8b5de -v 3c930aaccc775c99e421a0989621947c <"$TMP/msg" + assert_stderr_has "MAC verification failed" +} + +# The same file, tgId 24 tcId 185: AES-256, a 416-byte message, a 16-byte tag. +test_aes256_cmac_matches_acvp() { + local got + unhex 9DB5CEAEC709883E16E4474BE66842D184026CDC0BCE6CCDB41187E9F59FD665F473B29B3B47E33695D21DF9E34E09517BE2E14760897C505109F5AB3F1E1034DD2A99EFBABEB4664ACC66881EBED6AABDC70806D3B4908E626D59EFAD2D02FC5A3BB7BAF8C61984E8B5FBF118DFBDABF9A3D9421B6B6F95E592B6D4429C1B45BD7E68DC69D04D960A24724349762019419C8CC873C07BC318218B465D23A85BF8446738F9FF47866B4C02972E87B670D814F7EAF8148A24B761DBB157B9E30CBBA1CB00E6F92D398685DCFEE43324F3D2E2FF413063FCC4454D322638CE0B484BE7CA537B8953D755284EF4D8CF72E988137F63012EF485F4468939D18361D479B5CD561CAC065245016F24E8F00BCE8703AA9AA385FE183EE6FF82DB99D9C54D36E5E39EFAEC32AD22E80047326CF4C865D2AA48AF0B5E996C5109DEB4752E01B75754E5D4345AADF249AEE497858FF0DE3651C7DA7CA48BCAB8D2549A24F7D1CC97CDC15CD2FFB69A84C5539629513FA6A403AC46FA988418B1B7D4EBF432B79D450DE8C53594E2E398C17301BD3C828CBD49CCDC9417DA8092F7F1B4E34E >"$TMP/msg" + got=$(hex_out "$BC_RUST" aes256-cmac --key 5cf7c611d3abddfdf070e0405ed5191610a26cb73951297eae18b9c6c22505c2 -x <"$TMP/msg") + assert_eq "$got" "c23b79870186e3294e7d9a2e2ef0993e" "CMAC-AES256 tcId 185" +} + +# CMAC round trips for the three key lengths: the tag is 16 bytes, verifies, and rejects another +# message, another key and a truncated tag; the key file form agrees with the inline form. +test_cmac_round_trips() { + local bits tag + rng 100 >"$TMP/msg" + rng 100 >"$TMP/other" + for bits in 128 192 256; do + rng $((bits / 8)) >"$TMP/key" + rng $((bits / 8)) >"$TMP/other_key" + tag=$(hex_out "$BC_RUST" "aes$bits-cmac" -k "$TMP/key" -x <"$TMP/msg") + assert_eq "${#tag}" 32 "aes$bits-cmac tag length" + assert_eq "$(hex_out "$BC_RUST" "aes$bits-cmac" --key "$(hex "$TMP/key")" -x <"$TMP/msg")" "$tag" "inline key agrees" + expect_ok "verifies" "$BC_RUST" "aes$bits-cmac" -k "$TMP/key" -v "$tag" <"$TMP/msg" + expect_fail "not another message" "$BC_RUST" "aes$bits-cmac" -k "$TMP/key" -v "$tag" <"$TMP/other" + expect_fail "not another key" "$BC_RUST" "aes$bits-cmac" -k "$TMP/other_key" -v "$tag" <"$TMP/msg" + expect_fail "not a truncated tag" "$BC_RUST" "aes$bits-cmac" -k "$TMP/key" -v "${tag:0:16}" <"$TMP/msg" + done + expect_fail "a wrong-length key" "$BC_RUST" aes128-cmac --key 0011 "$TMP/aad" + got=$(hex_out "$BC_RUST" aes128-gmac --key 209730e4737a56343a8bd50013e7cf9a --iv 2920dcb53254a95eaba3dd4e -x <"$TMP/aad") + assert_eq "$got" "349c7f0b588598c575bf10b29330a9fb" "GMAC-AES128 tcId 16" + expect_ok "the tag verifies with --iv" "$BC_RUST" aes128-gmac --key 209730e4737a56343a8bd50013e7cf9a --iv 2920dcb53254a95eaba3dd4e -v 349c7f0b588598c575bf10b29330a9fb <"$TMP/aad" + expect_ok "and as nonce || tag" "$BC_RUST" aes128-gmac --key 209730e4737a56343a8bd50013e7cf9a -v 2920dcb53254a95eaba3dd4e349c7f0b588598c575bf10b29330a9fb <"$TMP/aad" + expect_fail "not under another nonce" "$BC_RUST" aes128-gmac --key 209730e4737a56343a8bd50013e7cf9a --iv 2920dcb53254a95eaba3dd4f -v 349c7f0b588598c575bf10b29330a9fb <"$TMP/aad" + unhex 2920dcb53254a95eaba3dd4e >"$TMP/iv" + got=$(hex_out "$BC_RUST" aes128-gmac --key 209730e4737a56343a8bd50013e7cf9a --iv-file "$TMP/iv" -x <"$TMP/aad") + assert_eq "$got" "349c7f0b588598c575bf10b29330a9fb" "the nonce from a raw file" +} + +# The same file, tgId 9 tcId 121: AES-256, 32 bytes of AAD. +test_aes256_gmac_matches_acvp() { + local got + unhex 8AF7DADB6A185623BDDAB8637D6C9D3BAD39FAE4B6833AB41EC5DFB1BEE5A0DD >"$TMP/aad" + got=$(hex_out "$BC_RUST" aes256-gmac --key dc4185d2af1d50b6272e5e2b498200c8c351a16bde94a4033f4d58fdf0211215 --iv fdbba151a8245eaafa7ead03 -x <"$TMP/aad") + assert_eq "$got" "2f762326f3b2bf7e4971eac077958eb8" "GMAC-AES256 tcId 121" +} + +# Without --iv a fresh nonce is drawn and written ahead of the tag, so the output is 28 bytes, +# verifies as nonce || tag, and two runs draw two nonces; the tag is bound to the data. +test_gmac_generates_a_nonce_per_tag() { + local bits out + rng 100 >"$TMP/msg" + for bits in 128 192 256; do + rng $((bits / 8)) >"$TMP/key" + "$BC_RUST" "aes$bits-gmac" -k "$TMP/key" <"$TMP/msg" >"$TMP/out1" + assert_size "$TMP/out1" 29 "nonce || tag and a newline" + out=$(hex_out "$BC_RUST" "aes$bits-gmac" -k "$TMP/key" -x <"$TMP/msg") + assert_eq "${#out}" 56 "hex nonce || tag" + expect_ok "verifies as nonce || tag" "$BC_RUST" "aes$bits-gmac" -k "$TMP/key" -v "$out" <"$TMP/msg" + expect_ok "and as --iv plus the tag" "$BC_RUST" "aes$bits-gmac" -k "$TMP/key" --iv "${out:0:24}" -v "${out:24}" <"$TMP/msg" + expect_fail "not over other data" "$BC_RUST" "aes$bits-gmac" -k "$TMP/key" -v "$out" < <(rng 100) + [ "$out" != "$(hex_out "$BC_RUST" "aes$bits-gmac" -k "$TMP/key" -x <"$TMP/msg")" ] || fail "two runs must draw two nonces" + done + rng 16 >"$TMP/key" + expect_fail "a verify value of the wrong shape" "$BC_RUST" aes128-gmac -k "$TMP/key" -v 00 <"$TMP/msg" + assert_stderr_has "nonce" + expect_fail "an IV of the wrong length" "$BC_RUST" aes128-gmac -k "$TMP/key" --iv 0011 <"$TMP/msg" + assert_stderr_has "12" +} + +run_all diff --git a/cli/tests/test_encoders.sh b/cli/tests/test_encoders.sh new file mode 100755 index 000000000..db736e41e --- /dev/null +++ b/cli/tests/test_encoders.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# The hex-encode / hex-decode / base64-encode / base64-decode subcommands against RFC 4648 Sec 10 and the streaming boundaries. +# +# Generated for the 2026-11-03 CLI review (plan 15) from the vector files named in each test; +# every expected value is copied from that file, none is typed by hand. Follows lib.sh's +# conventions: each test_* runs in its own subshell under `set -e`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# RFC 4648 Sec 10 (standards/RFC-4648-base64-hex.txt l.652-664): BASE64 of "" .. "foobar", both ways. +test_base64_matches_rfc4648_section_10() { + local got + got=$(printf '%s' '' | "$BC_RUST" base64-encode) + assert_eq "$got" '' "BASE64('')" + got=$(printf '%s' '' | "$BC_RUST" base64-decode) + assert_eq "$got" '' "BASE64 decode of ''" + got=$(printf '%s' 'f' | "$BC_RUST" base64-encode) + assert_eq "$got" 'Zg==' "BASE64('f')" + got=$(printf '%s' 'Zg==' | "$BC_RUST" base64-decode) + assert_eq "$got" 'f' "BASE64 decode of 'Zg=='" + got=$(printf '%s' 'fo' | "$BC_RUST" base64-encode) + assert_eq "$got" 'Zm8=' "BASE64('fo')" + got=$(printf '%s' 'Zm8=' | "$BC_RUST" base64-decode) + assert_eq "$got" 'fo' "BASE64 decode of 'Zm8='" + got=$(printf '%s' 'foo' | "$BC_RUST" base64-encode) + assert_eq "$got" 'Zm9v' "BASE64('foo')" + got=$(printf '%s' 'Zm9v' | "$BC_RUST" base64-decode) + assert_eq "$got" 'foo' "BASE64 decode of 'Zm9v'" + got=$(printf '%s' 'foob' | "$BC_RUST" base64-encode) + assert_eq "$got" 'Zm9vYg==' "BASE64('foob')" + got=$(printf '%s' 'Zm9vYg==' | "$BC_RUST" base64-decode) + assert_eq "$got" 'foob' "BASE64 decode of 'Zm9vYg=='" + got=$(printf '%s' 'fooba' | "$BC_RUST" base64-encode) + assert_eq "$got" 'Zm9vYmE=' "BASE64('fooba')" + got=$(printf '%s' 'Zm9vYmE=' | "$BC_RUST" base64-decode) + assert_eq "$got" 'fooba' "BASE64 decode of 'Zm9vYmE='" + got=$(printf '%s' 'foobar' | "$BC_RUST" base64-encode) + assert_eq "$got" 'Zm9vYmFy' "BASE64('foobar')" + got=$(printf '%s' 'Zm9vYmFy' | "$BC_RUST" base64-decode) + assert_eq "$got" 'foobar' "BASE64 decode of 'Zm9vYmFy'" +} + +# RFC 4648 Sec 10 (l.701-713): BASE16 of "" .. "foobar". The RFC prints upper-case digits; the CLI +# emits lower-case (RFC 4648 Sec 8: "The encoded output ... case-insensitive" on decode), so the +# comparison lower-cases the RFC value and the decoder is given the upper-case form as printed. +test_base16_matches_rfc4648_section_10() { + local got + got=$(printf '%s' '' | "$BC_RUST" hex-encode) + assert_eq "$got" '' "BASE16('')" + got=$(printf '%s' '' | "$BC_RUST" hex-decode) + assert_eq "$got" '' "BASE16 decode of ''" + got=$(printf '%s' 'f' | "$BC_RUST" hex-encode) + assert_eq "$got" '66' "BASE16('f')" + got=$(printf '%s' '66' | "$BC_RUST" hex-decode) + assert_eq "$got" 'f' "BASE16 decode of '66'" + got=$(printf '%s' 'fo' | "$BC_RUST" hex-encode) + assert_eq "$got" '666f' "BASE16('fo')" + got=$(printf '%s' '666F' | "$BC_RUST" hex-decode) + assert_eq "$got" 'fo' "BASE16 decode of '666F'" + got=$(printf '%s' 'foo' | "$BC_RUST" hex-encode) + assert_eq "$got" '666f6f' "BASE16('foo')" + got=$(printf '%s' '666F6F' | "$BC_RUST" hex-decode) + assert_eq "$got" 'foo' "BASE16 decode of '666F6F'" + got=$(printf '%s' 'foob' | "$BC_RUST" hex-encode) + assert_eq "$got" '666f6f62' "BASE16('foob')" + got=$(printf '%s' '666F6F62' | "$BC_RUST" hex-decode) + assert_eq "$got" 'foob' "BASE16 decode of '666F6F62'" + got=$(printf '%s' 'fooba' | "$BC_RUST" hex-encode) + assert_eq "$got" '666f6f6261' "BASE16('fooba')" + got=$(printf '%s' '666F6F6261' | "$BC_RUST" hex-decode) + assert_eq "$got" 'fooba' "BASE16 decode of '666F6F6261'" + got=$(printf '%s' 'foobar' | "$BC_RUST" hex-encode) + assert_eq "$got" '666f6f626172' "BASE16('foobar')" + got=$(printf '%s' '666F6F626172' | "$BC_RUST" hex-decode) + assert_eq "$got" 'foobar' "BASE16 decode of '666F6F626172'" +} + +# A chunk boundary (1 KiB reads) may fall between the two digits of a byte, inside a `\x` escape +# or after a backslash; the decoder must carry the partial token into the next read. +test_hex_decode_carries_partial_tokens_across_the_chunk_boundary() { + local got + { printf '00%.0s' $(seq 511); printf '41'; } >"$TMP/pair-split" # '4' ends the first read + got=$("$BC_RUST" hex-decode <"$TMP/pair-split" | tail -c 1 | "$BC_RUST" hex-encode) + assert_eq "$got" "41" "digit pair split across reads" + { printf '00%.0s' $(seq 511); printf ' \\x41'; } >"$TMP/bs-last" # '\' ends the first read + got=$("$BC_RUST" hex-decode <"$TMP/bs-last" | tail -c 1 | "$BC_RUST" hex-encode) + assert_eq "$got" "41" "backslash last in a read" + { printf '00%.0s' $(seq 511); printf '\\x41'; } >"$TMP/x-last" # '\x' ends the first read + got=$("$BC_RUST" hex-decode <"$TMP/x-last" | tail -c 1 | "$BC_RUST" hex-encode) + assert_eq "$got" "41" "escape split after the x" + { printf '00%.0s' $(seq 510); printf ' \\x4'; printf '1'; } >"$TMP/digit-split" # '\x4' ends the first read + got=$("$BC_RUST" hex-decode <"$TMP/digit-split" | tail -c 1 | "$BC_RUST" hex-encode) + assert_eq "$got" "41" "escape split inside its digits" +} + +# Round trips of random data through both encoders at sizes around the 1 KiB read and the +# 3-byte / 2-digit group boundaries. +test_round_trips_across_chunk_boundaries() { + local size + for size in 0 1 2 3 4 1023 1024 1025 1026 3071 3072 3073 65536; do + rng "$size" >"$TMP/in" + "$BC_RUST" base64-encode <"$TMP/in" | "$BC_RUST" base64-decode >"$TMP/out" + assert_same "$TMP/in" "$TMP/out" "base64 round trip of $size bytes" + "$BC_RUST" hex-encode <"$TMP/in" | "$BC_RUST" hex-decode >"$TMP/out" + assert_same "$TMP/in" "$TMP/out" "hex round trip of $size bytes" + done +} + +test_malformed_hex_is_rejected() { + expect_fail "odd digit count" "$BC_RUST" hex-decode < <(printf '0') + assert_stderr_has "OddLengthInput" + expect_fail "non-hex character" "$BC_RUST" hex-decode < <(printf 'zz') + assert_stderr_has "InvalidHexCharacter" + expect_fail "lone trailing backslash" "$BC_RUST" hex-decode < <(printf '\\') + assert_stderr_has "backslash" +} + +# RFC 4648 Sec 3.3: "non-alphabet characters ... should be rejected"; and after the padding the +# message is over. Both must be rejected whether the offending bytes arrive in a later read or in +# the same read as the padding. +test_base64_rejects_data_after_the_padding() { + expect_fail "data after padding, next read" "$BC_RUST" base64-decode < <(printf 'QUJD%.0s' $(seq 256); printf 'QUI='; printf 'ZZZZ') + assert_stderr_has "not valid base64" + expect_fail "data after padding, same read" "$BC_RUST" base64-decode < <(printf 'QUI=XXXX') + assert_stderr_has "not valid base64" + expect_fail "two padded blocks in one read" "$BC_RUST" base64-decode < <(printf 'QUI=\nQUI=') +} + +# RFC 4648 Sec 3.3: a final quantum of a single symbol cannot be valid; it must be rejected rather +# than decoded to bytes made from stale bits. +test_base64_rejects_a_single_trailing_symbol() { + expect_fail "one symbol" "$BC_RUST" base64-decode < <(printf 'Q') + expect_fail "one symbol then padding" "$BC_RUST" base64-decode < <(printf 'Q===') +} + +run_all diff --git a/cli/tests/test_hmac_kat.sh b/cli/tests/test_hmac_kat.sh new file mode 100755 index 000000000..c2339aa80 --- /dev/null +++ b/cli/tests/test_hmac_kat.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# The hmac-* subcommands against RFC 4231 and Wycheproof known answers (test_hmac.sh covers the random round trips). +# +# Written for the 2026-11-03 CLI review (report 15) from the vector files named in each test; +# every expected value is copied from that file, none is typed by hand. Follows lib.sh's +# conventions: each test_* runs in its own subshell under `set -e`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# RFC 4231 Sec 4.2 Test Case 1 (standards/RFC-4231-HMAC-vectors.txt): 20-byte key, 8-byte data. +test_rfc4231_case_1() { + local got + unhex 4869205468657265 >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b -x <"$TMP/data") + assert_eq "$got" "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" "HMAC-SHA-256 case 1" + got=$(hex_out "$BC_RUST" hmac-sha512 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b -x <"$TMP/data") + assert_eq "$got" "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854" "HMAC-SHA-512 case 1" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha256 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b --verify b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7 <"$TMP/data" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha512 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b --verify 87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854 <"$TMP/data" + unhex 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b >"$TMP/key" + got=$(hex_out "$BC_RUST" hmac-sha256 -k "$TMP/key" -x <"$TMP/data") + assert_eq "$got" "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" "HMAC-SHA-256 case 1 with the key from a raw file" +} + +# RFC 4231 Sec 4.3 Test Case 2 (standards/RFC-4231-HMAC-vectors.txt): 4-byte key, 28-byte data. +test_rfc4231_case_2() { + local got + unhex 7768617420646f2079612077616e7420666f72206e6f7468696e673f >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key 4a656665 -x <"$TMP/data") + assert_eq "$got" "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843" "HMAC-SHA-256 case 2" + got=$(hex_out "$BC_RUST" hmac-sha512 --key 4a656665 -x <"$TMP/data") + assert_eq "$got" "164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737" "HMAC-SHA-512 case 2" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha256 --key 4a656665 --verify 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843 <"$TMP/data" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha512 --key 4a656665 --verify 164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737 <"$TMP/data" + unhex 4a656665 >"$TMP/key" + got=$(hex_out "$BC_RUST" hmac-sha256 -k "$TMP/key" -x <"$TMP/data") + assert_eq "$got" "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843" "HMAC-SHA-256 case 2 with the key from a raw file" +} + +# RFC 4231 Sec 4.4 Test Case 3 (standards/RFC-4231-HMAC-vectors.txt): 20-byte key, 50-byte data. +test_rfc4231_case_3() { + local got + unhex dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -x <"$TMP/data") + assert_eq "$got" "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe" "HMAC-SHA-256 case 3" + got=$(hex_out "$BC_RUST" hmac-sha512 --key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -x <"$TMP/data") + assert_eq "$got" "fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb" "HMAC-SHA-512 case 3" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha256 --key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --verify 773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe <"$TMP/data" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha512 --key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --verify fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb <"$TMP/data" + unhex aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >"$TMP/key" + got=$(hex_out "$BC_RUST" hmac-sha256 -k "$TMP/key" -x <"$TMP/data") + assert_eq "$got" "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe" "HMAC-SHA-256 case 3 with the key from a raw file" +} + +# RFC 4231 Sec 4.5 Test Case 4 (standards/RFC-4231-HMAC-vectors.txt): 25-byte key, 50-byte data. +test_rfc4231_case_4() { + local got + unhex cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key 0102030405060708090a0b0c0d0e0f10111213141516171819 -x <"$TMP/data") + assert_eq "$got" "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b" "HMAC-SHA-256 case 4" + got=$(hex_out "$BC_RUST" hmac-sha512 --key 0102030405060708090a0b0c0d0e0f10111213141516171819 -x <"$TMP/data") + assert_eq "$got" "b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd" "HMAC-SHA-512 case 4" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha256 --key 0102030405060708090a0b0c0d0e0f10111213141516171819 --verify 82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b <"$TMP/data" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha512 --key 0102030405060708090a0b0c0d0e0f10111213141516171819 --verify b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd <"$TMP/data" + unhex 0102030405060708090a0b0c0d0e0f10111213141516171819 >"$TMP/key" + got=$(hex_out "$BC_RUST" hmac-sha256 -k "$TMP/key" -x <"$TMP/data") + assert_eq "$got" "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b" "HMAC-SHA-256 case 4 with the key from a raw file" +} + +# RFC 4231 Sec 4.6 Test Case 5: the RFC truncates the output to 128 bits. The CLI has no truncation +# option, so the full tag's first 16 bytes must equal the RFC value; `--verify` of the truncated +# tag is expected to fail (the CLI compares full tags only). +test_rfc4231_case_5_truncated_output_is_a_prefix() { + local got + unhex 546573742057697468205472756e636174696f6e >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key 0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c -x <"$TMP/data") + assert_eq "${got:0:32}" "a3b6167473100ee06e0c796c2955552b" "HMAC-SHA-256 case 5 (first 128 bits)" + got=$(hex_out "$BC_RUST" hmac-sha512 --key 0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c -x <"$TMP/data") + assert_eq "${got:0:32}" "415fad6271580a531d4179bc891d87a6" "HMAC-SHA-512 case 5 (first 128 bits)" + expect_fail "a truncated tag is not a full tag" "$BC_RUST" hmac-sha256 --key 0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c --verify a3b6167473100ee06e0c796c2955552b <"$TMP/data" +} + +# RFC 4231 Sec 4.7 Test Case 6: a 131-byte key, longer than the SHA-256 block, which HMAC hashes +# first (RFC 2104 Sec 2). The CLI takes a key of any length up to 16 KiB. +test_rfc4231_case_6_long_key() { + local got key + key=$(printf 'aa%.0s' $(seq 131)) + unhex 54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a65204b6579202d2048617368204b6579204669727374 >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key "$key" -x <"$TMP/data") + assert_eq "$got" "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54" "HMAC-SHA-256 case 6" + got=$(hex_out "$BC_RUST" hmac-sha512 --key "$key" -x <"$TMP/data") + assert_eq "$got" "80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598" "HMAC-SHA-512 case 6" + unhex "$key" >"$TMP/key" + got=$(hex_out "$BC_RUST" hmac-sha256 -k "$TMP/key" -x <"$TMP/data") + assert_eq "$got" "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54" "HMAC-SHA-256 case 6 with the key from a raw file" +} + +# RFC 4231 Sec 4.8 Test Case 7: a 131-byte key and a 152-byte message, both over the SHA-512 block. +test_rfc4231_case_7_long_key_and_data() { + local got key + key=$(printf 'aa%.0s' $(seq 131)) + unhex 5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key "$key" -x <"$TMP/data") + assert_eq "$got" "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2" "HMAC-SHA-256 case 7" + got=$(hex_out "$BC_RUST" hmac-sha512 --key "$key" -x <"$TMP/data") + assert_eq "$got" "e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58" "HMAC-SHA-512 case 7" +} + +# RFC 4231 Sec 4.2 Test Case 1 for the hmac-sha224 and hmac-sha384 subcommands. +test_rfc4231_case_1_sha224_and_sha384() { + local got + unhex 4869205468657265 >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha224 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b -x <"$TMP/data") + assert_eq "$got" "896fb1128abbdf196832107cd49df33f47b4b1169912ba4f53684b22" "HMAC-SHA-224 case 1" + got=$(hex_out "$BC_RUST" hmac-sha384 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b -x <"$TMP/data") + assert_eq "$got" "afd03944d84895626b0825f4ab46907f15f9dadbe4101ec682aa034c7cebc59cfaea9ea9076ede7f4af152e8b2fa9cb6" "HMAC-SHA-384 case 1" + expect_ok "the tag must verify" "$BC_RUST" hmac-sha224 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b --verify 896fb1128abbdf196832107cd49df33f47b4b1169912ba4f53684b22 <"$TMP/data" +} + +# No published HMAC-SHA3 vector is in bc-test-data, so the four hmac-sha3-* subcommands are pinned +# by shape: the tag length, a verify round trip, and that they differ from each other and from +# HMAC-SHA2 of the same size. +test_hmac_sha3_variants_round_trip() { + local h len tag + rng 32 >"$TMP/key" + rng 300 >"$TMP/msg" + for h in 224 256 384 512; do + tag=$(hex_out "$BC_RUST" hmac-sha3-$h -k "$TMP/key" -x <"$TMP/msg") + assert_eq "${#tag}" $((h / 4)) "hmac-sha3-$h tag length" + expect_ok "hmac-sha3-$h verifies its own tag" "$BC_RUST" hmac-sha3-$h -k "$TMP/key" -v "$tag" <"$TMP/msg" + expect_fail "and not a flipped one" "$BC_RUST" hmac-sha3-$h -k "$TMP/key" -v "$(flip_hex "$tag" 0 1)" <"$TMP/msg" + done + assert_eq "$(hex_out "$BC_RUST" hmac-sha3-256 -k "$TMP/key" -x <"$TMP/msg" | cut -c1-16 | wc -c | tr -d ' ')" 17 "hex tag" + [ "$(hex_out "$BC_RUST" hmac-sha3-256 -k "$TMP/key" -x <"$TMP/msg")" != "$(hex_out "$BC_RUST" hmac-sha256 -k "$TMP/key" -x <"$TMP/msg")" ] || fail "HMAC-SHA3-256 must differ from HMAC-SHA-256" +} + +# Wycheproof testvectors_v1/hmac_sha512_224_test.json tcId 3 (keySize 224, tagSize 224): a +# full-length tag, since RFC 4231 has no hmac-sha512-224 vector. +test_hmac_sha512_224_matches_wycheproof_tc3() { + local got + unhex 2e >"$TMP/msg" + got=$(hex_out "$BC_RUST" hmac-sha512-224 --key 8648ee936c6ebc5ae4bb48c1139a54e3ac5d897beec492dc4d740752 -x <"$TMP/msg") + assert_eq "$got" "5979cc5dcdaf49daaadfc3388de3f5a04d183ebc896fd4f9dbdcc513" "hmac-sha512-224 tcId 3" + expect_ok "the tag must verify" "$BC_RUST" hmac-sha512-224 --key 8648ee936c6ebc5ae4bb48c1139a54e3ac5d897beec492dc4d740752 --verify 5979cc5dcdaf49daaadfc3388de3f5a04d183ebc896fd4f9dbdcc513 <"$TMP/msg" +} + +# Wycheproof testvectors_v1/hmac_sha512_256_test.json tcId 3 (keySize 256, tagSize 256): a +# full-length tag, since RFC 4231 has no hmac-sha512-256 vector. +test_hmac_sha512_256_matches_wycheproof_tc3() { + local got + unhex 77 >"$TMP/msg" + got=$(hex_out "$BC_RUST" hmac-sha512-256 --key 8159fd15133cd964c9a6964c94f0ea269a806fd9f43f0da58b6cd1b33d189b2a -x <"$TMP/msg") + assert_eq "$got" "aee7a277e0f36a042368ae52b1337ba13f1c00fdb2dfb1d5f0c304e485c3a7dc" "hmac-sha512-256 tcId 3" + expect_ok "the tag must verify" "$BC_RUST" hmac-sha512-256 --key 8159fd15133cd964c9a6964c94f0ea269a806fd9f43f0da58b6cd1b33d189b2a --verify aee7a277e0f36a042368ae52b1337ba13f1c00fdb2dfb1d5f0c304e485c3a7dc <"$TMP/msg" +} + +# Wycheproof testvectors_v1/hmac_sm3_test.json tcId 2 (keySize 256, tagSize 256): a +# full-length tag, since RFC 4231 has no hmac-sm3 vector. +test_hmac_sm3_matches_wycheproof_tc2() { + local got + unhex 77 >"$TMP/msg" + got=$(hex_out "$BC_RUST" hmac-sm3 --key 8159fd15133cd964c9a6964c94f0ea269a806fd9f43f0da58b6cd1b33d189b2a -x <"$TMP/msg") + assert_eq "$got" "51aaf64843b6d2fc7a244ab9cda9b7b57164ce6a865f9808c01931b46195411d" "hmac-sm3 tcId 2" + expect_ok "the tag must verify" "$BC_RUST" hmac-sm3 --key 8159fd15133cd964c9a6964c94f0ea269a806fd9f43f0da58b6cd1b33d189b2a --verify 51aaf64843b6d2fc7a244ab9cda9b7b57164ce6a865f9808c01931b46195411d <"$TMP/msg" +} + +# A key that is not hex, or a key file that does not exist, must be a reported error, not a panic. +test_bad_key_inputs_are_errors_not_panics() { + expect_fail "non-hex --key" "$BC_RUST" hmac-sha256 --key zz -x "$TMP/seed" + got=$(hex_out "$BC_RUST" mldsa44 keygen-from-seed -x <"$TMP/seed") + assert_eq "$got" "3a909812ef8d581a3118c2e9c33748943063daa58dbd96010428a7ae5d5d8d6412a9e06cd5963dfd8be96baf98b2bb61909b26006eafebd7d20dc9cb611ce7aa590a725e2bacb71eeafc4e057dc20539733b28a789108711f48aa4d947aa225dc4848a9de09ebd59365c287a4646650832225d2712eb0251cadc1f99daf981f101334803937108316843c464c9c44811a59194122d10c30c538401992642e2186ed0160053b6259908401c030ae2382482808c22a16c139524e102900819869128125b082e230772d4c2210c003141920da108212104809cb085e1380518356489c28080c23114314490846911390512a40c41164800823021962d81143061c830a0229022326ed240518824520241320a3782631649c91611c3149020394a024162d842661a132ecc34664a409004b62403074592906d01b64108b12c53a44c81c6619904010934511c1700014592e1b46c4ac610240184e4222520854023218a5c426ee0c44c94806998026d0c092101218c02982d62482420a16d099509400242a2b64942268c212609140044182042111342c0464693444a54a40c63128d00168a494665c2108808990c1b37110c3444d3a011a21460c3a205142844822409ca940d938404999441591870a3b84c8a4265181052e4922c020460c808129a904553944c1481619a04729c045298087022340959400ad0802914038a82182c1ab40554806960b841242670821490004162041020d4965098124122b445c0a08051c44c9ca21149c689c49088001450e4a4850a950919162a1b060212892492466081b24064980d843232da440104001093188511930122116d8a1210c3462ccb084c112604549468113685140484242144a4104150164a5cc00848308990420c113942e434508c90499c9869139721c1169121966580a83012100d63a88c0b054c8a2025d0a808a0043093944904223141b670c418708c924d99480c42188560100604446812100a42c86d08314dd94260d8c448daa82994040911458054b4310c49054920509c286100b070123011c8160e99126692126a02a585811644c334129a400e93944010b064190410889469d002721049000bb00d18074062402240122910162891b680d11231c2220e12008451966921176e923241440091d39081c2122c44460d90b051103805a19084188708e3802c24308049264449a89011c38c5a86901c82691215688a466e0bb88d83404148a65022920884c401a1108abb3902c1b31799a75924465953e9ff278a0bc840d829e053913e6385aa2df05cfde16dcf2c09f5faeacb062749174e6f61c4d264dbecca8a9dafeeaebe208e11a50fe8930a483f33e4ee21f0ade85ffc263f3995e4f71fe6d5fbea8f26c50f4782066dcd31116c46e6b0eb2aeb7ec06e487fec74f3a90f0ba8acd0769372f1ccadda23c5e0a5507e38ced0f3daf4033cef9e7c7c563950902be8b442b76400a3837121c753c7535dc6dd956cd75dd51943021a4432bab428c0df5ab33320c18187191e79f2a007410992e53bb9effbba1c54e03763846362d279cf46be4f8697a88db336127ddd7b20e54706e12cd2861ca6ae5f6ff14deaf77d29bad5b987ee6b3dc622b7dc757a44a43be1f9a69ab33547edfad87f643b18964faee9ce922fc21d8272d18152316e052f60f0441594fd6a4f0abb86ca298f9eddff9439470e95bf981f560f256d6519b920c4bdf9573507426e283b37dd3fe4002b31e85b4f25adf916277b6eab26f5ae8118c451953646e6b9f488bb977c17ecd57c5033e11dc168355a869411c6a78343bd6e071d69a4d7d2e7b5ade33508e22614b635817f035e7c1e16f07ca46c7eb98c448e320fd9b10208f82c394166208954850da4e4cd6d6e92737e3d6a99dbd46dbd30c3dc52e84db7f1ad95d7e579d3904d6bcbe6b3a05557962c8f74b09fe17c0e9ba4c48796b399de4ddb1ad15a1cf7294eb636fdac12a2c8d81f47a0b636b4e488d326dc51c94134e54e7d5960194e6f70cc156b312460092aa60f58dd91a607a495c19f053d7089ffe9962da0d9cfc1f528c743aa5f5f48d84ec660f4e225056d2e851c133cae930b8d49219b6aaeadc89ce6b30c3a1d84038db04f9cf969e93952a495818e36ae318f734353b696f4faaa2b59af0e98092b9f1ad5d21af96878af812bf79c697979c30a096deb1a54f3beeb998d679be2dd0009829ccb907a4e13204cf4943cc51562279d4904b959c2dfcec8e306ed613e1ab874dc805712345d1e552c49583526e3833a121ad301a5a677d8175d80a6cb4fdce3bc5c8d7f3747d75eca0f8ef45b2ce1876e34829409768d7438d79bc56fc51ccdbe0d62597faddd80151b541e32828bc287d9fa40eab868e07349c6b33a2f8b516b85e06d39474177a84db8165d4f27da09a91489a7f637e4ce30a7a14e2b7adc5de9c95372827f8eb4fa2f5436fc93113a001b1a99c269c0f372c7f19ef92d641915fc823d6bf96932cfd4b4c75341dcd1eed741a13ff69175c122cd9c7c80c805f996341a2e3c7366173ebe7aaee321e231390c07366ce36ce1da3c6391a908464f2adec37aa6b39146fe8b63fefea2bdd4915ab999f87f4343a9259518316c2cc1e0e3e3e4d3146da41f35ccf9a6faff2a85b1ceb00f6df76097ce8559c87a0ca47584084b02138eaed1a59909ad10948384a3d896b090ee6b4217344d96bb43e8ac58719ff19da362a11fcb5ee1b818e0c841eccf1976f50959d464209d87cb9d5864240ed023b6574f019b33469f56ac3cd507030b9af1b88dcd72a0deb0ad581d243d8e4678ed487d6fdbdba75738cdc4a5b899dad48f322b63d8d869b839e375f96fe2afc387cbf843cf4d232bb578cd86b5b06e911108660f06353948f10d79a325dc31c8fa9d0a373725dd1c92048b7b5cf0c802ceda0486b5dc6e6b6bc6a5256f4eb9f8432c570fbbd2722ac276881c42d8e190d8f6acb1b5b5901490fe42ede6198af204a0a84dd83b6572082c32371478d820bfebbe2507e25b770481192e06305dbf3d0f219d922fce665b058cc35c692fc05e3938485171a72482532e30445e9ade3970c5f35e291758dab31d8bba2e6c053164b62facb136703e211917b7c447b0a35e01e7879072686987c9da5b09a76cb2b2597bfb33486eff3cda9f34942bedcd91fb4243618b742ba738e466c9fc077fb70db57a60cc54b4a84849688bd8f4782562b257b81161e98d06a93e04ababf3b51bf2871bc49c651ce24b576e849d6d690f3de5dd22f453fbcce219461367613b0111d7b3188c41ca01fe90f74dcdf56308869217f080f561d787a065a69f7617d21225cc3819b2d93ceff1f6ad4bb7d1ae282323fb556fb7fbc92ad378e64a4e4d424c59c1449fb3bde3d2b2d784440da85456b8adc17d185a59ac69b3b38386245fcd8dae38d04817d38a5f3b1f71fb7d211eb83efb876345bcc99042b7e55bbed7b2af6a975f88d61f707501029634d5187ef4a118270026ef4e6aa417d6f586c8b158e2abdf7b84906b1f6198aea946a1c30e5a1d0e60bbcbc0a1b88d03ffebf88fa52255317ed94d7e79a5bc851d85e5ea5a1cdd8e0c78581320" "sk from a raw seed" + got=$(printf '%s' 61C5442C5C769EAC3021DCB9FFAB0D46223D456F0A72305E2F203DDD57BFFFCE | hex_out "$BC_RUST" --hex-input mldsa44 keygen-from-seed -x) + assert_eq "$got" "3a909812ef8d581a3118c2e9c33748943063daa58dbd96010428a7ae5d5d8d6412a9e06cd5963dfd8be96baf98b2bb61909b26006eafebd7d20dc9cb611ce7aa590a725e2bacb71eeafc4e057dc20539733b28a789108711f48aa4d947aa225dc4848a9de09ebd59365c287a4646650832225d2712eb0251cadc1f99daf981f101334803937108316843c464c9c44811a59194122d10c30c538401992642e2186ed0160053b6259908401c030ae2382482808c22a16c139524e102900819869128125b082e230772d4c2210c003141920da108212104809cb085e1380518356489c28080c23114314490846911390512a40c41164800823021962d81143061c830a0229022326ed240518824520241320a3782631649c91611c3149020394a024162d842661a132ecc34664a409004b62403074592906d01b64108b12c53a44c81c6619904010934511c1700014592e1b46c4ac610240184e4222520854023218a5c426ee0c44c94806998026d0c092101218c02982d62482420a16d099509400242a2b64942268c212609140044182042111342c0464693444a54a40c63128d00168a494665c2108808990c1b37110c3444d3a011a21460c3a205142844822409ca940d938404999441591870a3b84c8a4265181052e4922c020460c808129a904553944c1481619a04729c045298087022340959400ad0802914038a82182c1ab40554806960b841242670821490004162041020d4965098124122b445c0a08051c44c9ca21149c689c49088001450e4a4850a950919162a1b060212892492466081b24064980d843232da440104001093188511930122116d8a1210c3462ccb084c112604549468113685140484242144a4104150164a5cc00848308990420c113942e434508c90499c9869139721c1169121966580a83012100d63a88c0b054c8a2025d0a808a0043093944904223141b670c418708c924d99480c42188560100604446812100a42c86d08314dd94260d8c448daa82994040911458054b4310c49054920509c286100b070123011c8160e99126692126a02a585811644c334129a400e93944010b064190410889469d002721049000bb00d18074062402240122910162891b680d11231c2220e12008451966921176e923241440091d39081c2122c44460d90b051103805a19084188708e3802c24308049264449a89011c38c5a86901c82691215688a466e0bb88d83404148a65022920884c401a1108abb3902c1b31799a75924465953e9ff278a0bc840d829e053913e6385aa2df05cfde16dcf2c09f5faeacb062749174e6f61c4d264dbecca8a9dafeeaebe208e11a50fe8930a483f33e4ee21f0ade85ffc263f3995e4f71fe6d5fbea8f26c50f4782066dcd31116c46e6b0eb2aeb7ec06e487fec74f3a90f0ba8acd0769372f1ccadda23c5e0a5507e38ced0f3daf4033cef9e7c7c563950902be8b442b76400a3837121c753c7535dc6dd956cd75dd51943021a4432bab428c0df5ab33320c18187191e79f2a007410992e53bb9effbba1c54e03763846362d279cf46be4f8697a88db336127ddd7b20e54706e12cd2861ca6ae5f6ff14deaf77d29bad5b987ee6b3dc622b7dc757a44a43be1f9a69ab33547edfad87f643b18964faee9ce922fc21d8272d18152316e052f60f0441594fd6a4f0abb86ca298f9eddff9439470e95bf981f560f256d6519b920c4bdf9573507426e283b37dd3fe4002b31e85b4f25adf916277b6eab26f5ae8118c451953646e6b9f488bb977c17ecd57c5033e11dc168355a869411c6a78343bd6e071d69a4d7d2e7b5ade33508e22614b635817f035e7c1e16f07ca46c7eb98c448e320fd9b10208f82c394166208954850da4e4cd6d6e92737e3d6a99dbd46dbd30c3dc52e84db7f1ad95d7e579d3904d6bcbe6b3a05557962c8f74b09fe17c0e9ba4c48796b399de4ddb1ad15a1cf7294eb636fdac12a2c8d81f47a0b636b4e488d326dc51c94134e54e7d5960194e6f70cc156b312460092aa60f58dd91a607a495c19f053d7089ffe9962da0d9cfc1f528c743aa5f5f48d84ec660f4e225056d2e851c133cae930b8d49219b6aaeadc89ce6b30c3a1d84038db04f9cf969e93952a495818e36ae318f734353b696f4faaa2b59af0e98092b9f1ad5d21af96878af812bf79c697979c30a096deb1a54f3beeb998d679be2dd0009829ccb907a4e13204cf4943cc51562279d4904b959c2dfcec8e306ed613e1ab874dc805712345d1e552c49583526e3833a121ad301a5a677d8175d80a6cb4fdce3bc5c8d7f3747d75eca0f8ef45b2ce1876e34829409768d7438d79bc56fc51ccdbe0d62597faddd80151b541e32828bc287d9fa40eab868e07349c6b33a2f8b516b85e06d39474177a84db8165d4f27da09a91489a7f637e4ce30a7a14e2b7adc5de9c95372827f8eb4fa2f5436fc93113a001b1a99c269c0f372c7f19ef92d641915fc823d6bf96932cfd4b4c75341dcd1eed741a13ff69175c122cd9c7c80c805f996341a2e3c7366173ebe7aaee321e231390c07366ce36ce1da3c6391a908464f2adec37aa6b39146fe8b63fefea2bdd4915ab999f87f4343a9259518316c2cc1e0e3e3e4d3146da41f35ccf9a6faff2a85b1ceb00f6df76097ce8559c87a0ca47584084b02138eaed1a59909ad10948384a3d896b090ee6b4217344d96bb43e8ac58719ff19da362a11fcb5ee1b818e0c841eccf1976f50959d464209d87cb9d5864240ed023b6574f019b33469f56ac3cd507030b9af1b88dcd72a0deb0ad581d243d8e4678ed487d6fdbdba75738cdc4a5b899dad48f322b63d8d869b839e375f96fe2afc387cbf843cf4d232bb578cd86b5b06e911108660f06353948f10d79a325dc31c8fa9d0a373725dd1c92048b7b5cf0c802ceda0486b5dc6e6b6bc6a5256f4eb9f8432c570fbbd2722ac276881c42d8e190d8f6acb1b5b5901490fe42ede6198af204a0a84dd83b6572082c32371478d820bfebbe2507e25b770481192e06305dbf3d0f219d922fce665b058cc35c692fc05e3938485171a72482532e30445e9ade3970c5f35e291758dab31d8bba2e6c053164b62facb136703e211917b7c447b0a35e01e7879072686987c9da5b09a76cb2b2597bfb33486eff3cda9f34942bedcd91fb4243618b742ba738e466c9fc077fb70db57a60cc54b4a84849688bd8f4782562b257b81161e98d06a93e04ababf3b51bf2871bc49c651ce24b576e849d6d690f3de5dd22f453fbcce219461367613b0111d7b3188c41ca01fe90f74dcdf56308869217f080f561d787a065a69f7617d21225cc3819b2d93ceff1f6ad4bb7d1ae282323fb556fb7fbc92ad378e64a4e4d424c59c1449fb3bde3d2b2d784440da85456b8adc17d185a59ac69b3b38386245fcd8dae38d04817d38a5f3b1f71fb7d211eb83efb876345bcc99042b7e55bbed7b2af6a975f88d61f707501029634d5187ef4a118270026ef4e6aa417d6f586c8b158e2abdf7b84906b1f6198aea946a1c30e5a1d0e60bbcbc0a1b88d03ffebf88fa52255317ed94d7e79a5bc851d85e5ea5a1cdd8e0c78581320" "sk from a hex seed" + "$BC_RUST" mldsa44 keygen-from-seed <"$TMP/seed" >"$TMP/sk" + got=$(hex_out "$BC_RUST" mldsa44 pk-from-sk -x <"$TMP/sk") + assert_eq "$got" "3a909812ef8d581a3118c2e9c33748943063daa58dbd96010428a7ae5d5d8d64e8a2f9aea78b11ef18a2b2ea9d852ba6c5c0278b36cd899a5397189780d63eb3601dfddd6d127a900d68abcec8e3379b57d5bbc99591daa6da230935bb9a7ded40e76bda363f1a30fe9539adffcf3e882997d90b9426bfb25a28bbf3630f539f6819ae26b6705a33aef4ec09b6e5de2f1d83ad9ebb880f7a8cee31148829b9999d0f0e907ea28d8b3a14cd2071a9b2fcc14194bf82b4474976eb851eb9cb2209cbd1dd75d986231d01f0697bb65586cb0ef0d0fd8416b90e5c8cf9129924709fe998b5bfeb69dcc1d2fdf43422cfa1ad926506f8ca7674b6f4820cb76690524b9c1b9b3841cc375dd2861f1dd88b64d0e733ad8a5fd87078bfda207c489b813e12f8f24fb291dbd427de0ff01eab3b66f59fc81ccd1f81af9fa5a44880e57789c4bf6457adc20248aa6388bc8ba04cefad9c28431395cb8f6ea6084a38dee1613d988974f9c5dd3ca7578ecd4c688b73c600ac9c7d96dab748ff9faff9240ead4c854d868da3b3ea1a022866a36acfa432c9847a4dc631d67dfd9ed83869793c3064f0693c9fd63a3aca815540737ae24796841f7ecd94a2715f669dfeb294c4a0739e7f8e6092b41155f7701f95fc5761279d98aef6fc31a977aba678b8dd339b82b934dd3b74c9484791b00701ba1cf8ab369f2ed4f2995c6aeb0b1ad7181b3d5da4cb209d629bf621eaf9353263666e368402931b4ffd82f45e38c19ed7068046108cedf4dec5139bdd66b13ebcb7dacbaea770cefd994149f77e575dcda6b406cabc970c08c693844c3abec151972fe913514802fb974e396c5ba5f5493538185d2523569878032f8d128506804a12b36b5023122d01b86038cc9445317d8e8332d21c15ae298d73820362b097781f8d34803e7aff1ae929f0bba507ccc0b722750b8b1c7e5dd6b0194d677bb75d889a0061e48427f8c0709c227506ebf44d4e9cbec17f92a9ada7dd82d4a49a21b3664120cc6b6c023eabac72be1425ad6d0fd3f15c1f767a4af63e774447d52bda71f62accb9dc9e084b2baccb2e84be167b47f61b737d4862437aa4d7cbf078a90c35564ddf1535d365a4e9d8a789f311cc002ac1c7e377def9f231026ecbbcdaa4363f1942abf265350257b540079b559164d27c6eb21f2b7d184d33599ddf6ad082867c8751bf83ff2cec7322898de686bfc243f4a51b84b83a6e1bbc1f483bdbeb6991e8334eb299360434b1bb35245f46fc514a79fc9f54147636966a56a295e249bc2ca55e2f62f5513da77acf35f9d2006fd0104a8d5f70948ee0ca9b4e144aa498d597f6747cbe79ffb28aaa189588a79b1286cc9dcae708a3188d23ae0e10c012211fb48a40ab3c65afe2a4eaf99e79ff39e2352e2e4cbe98f6c6340d7697f82da0fbbb703c2c57f28651d1f83a83be015d0599c42bf55f1465f8a3007ef7179b5890524bcc0cf6b44adff3cb32593d8a05445f162a6226217389712401f85b6115366ada0122566302798ff92c0994d8e9e1ddfb24e6e46cc0cf64fc527a57f334443682a6343ce6ca6d2ee9accbe3ed1ba6cd9b9935b1d56564e35077073f5ced9a2ac9a4b3f0ca0684e69ac1d9dd83c3a661fbb5ac2853a382cb4842687ac16664bb8b82df9152df9210a0608de0483f7e1782ca932fd8f41d8f50ed10193ac0ad6263579d7261e443cddb279bc02b20e3f055169ee2a27566e6f47382e91d9241cfa7137732514fa460c8183b489efd8218c4f104351b9ff94da2952709ae7ed29ce88ff0debb45863265df7f1b536367816b57d42b4c8fccabd2bae3633d4a0af2faea7515f36babd3" "pk from the sk" + expect_ok "the pair must be consistent" "$BC_RUST" mldsa44 check-consistency --pkfile <(unhex 3A909812EF8D581A3118C2E9C33748943063DAA58DBD96010428A7AE5D5D8D64E8A2F9AEA78B11EF18A2B2EA9D852BA6C5C0278B36CD899A5397189780D63EB3601DFDDD6D127A900D68ABCEC8E3379B57D5BBC99591DAA6DA230935BB9A7DED40E76BDA363F1A30FE9539ADFFCF3E882997D90B9426BFB25A28BBF3630F539F6819AE26B6705A33AEF4EC09B6E5DE2F1D83AD9EBB880F7A8CEE31148829B9999D0F0E907EA28D8B3A14CD2071A9B2FCC14194BF82B4474976EB851EB9CB2209CBD1DD75D986231D01F0697BB65586CB0EF0D0FD8416B90E5C8CF9129924709FE998B5BFEB69DCC1D2FDF43422CFA1AD926506F8CA7674B6F4820CB76690524B9C1B9B3841CC375DD2861F1DD88B64D0E733AD8A5FD87078BFDA207C489B813E12F8F24FB291DBD427DE0FF01EAB3B66F59FC81CCD1F81AF9FA5A44880E57789C4BF6457ADC20248AA6388BC8BA04CEFAD9C28431395CB8F6EA6084A38DEE1613D988974F9C5DD3CA7578ECD4C688B73C600AC9C7D96DAB748FF9FAFF9240EAD4C854D868DA3B3EA1A022866A36ACFA432C9847A4DC631D67DFD9ED83869793C3064F0693C9FD63A3ACA815540737AE24796841F7ECD94A2715F669DFEB294C4A0739E7F8E6092B41155F7701F95FC5761279D98AEF6FC31A977ABA678B8DD339B82B934DD3B74C9484791B00701BA1CF8AB369F2ED4F2995C6AEB0B1AD7181B3D5DA4CB209D629BF621EAF9353263666E368402931B4FFD82F45E38C19ED7068046108CEDF4DEC5139BDD66B13EBCB7DACBAEA770CEFD994149F77E575DCDA6B406CABC970C08C693844C3ABEC151972FE913514802FB974E396C5BA5F5493538185D2523569878032F8D128506804A12B36B5023122D01B86038CC9445317D8E8332D21C15AE298D73820362B097781F8D34803E7AFF1AE929F0BBA507CCC0B722750B8B1C7E5DD6B0194D677BB75D889A0061E48427F8C0709C227506EBF44D4E9CBEC17F92A9ADA7DD82D4A49A21B3664120CC6B6C023EABAC72BE1425AD6D0FD3F15C1F767A4AF63E774447D52BDA71F62ACCB9DC9E084B2BACCB2E84BE167B47F61B737D4862437AA4D7CBF078A90C35564DDF1535D365A4E9D8A789F311CC002AC1C7E377DEF9F231026ECBBCDAA4363F1942ABF265350257B540079B559164D27C6EB21F2B7D184D33599DDF6AD082867C8751BF83FF2CEC7322898DE686BFC243F4A51B84B83A6E1BBC1F483BDBEB6991E8334EB299360434B1BB35245F46FC514A79FC9F54147636966A56A295E249BC2CA55E2F62F5513DA77ACF35F9D2006FD0104A8D5F70948EE0CA9B4E144AA498D597F6747CBE79FFB28AAA189588A79B1286CC9DCAE708A3188D23AE0E10C012211FB48A40AB3C65AFE2A4EAF99E79FF39E2352E2E4CBE98F6C6340D7697F82DA0FBBB703C2C57F28651D1F83A83BE015D0599C42BF55F1465F8A3007EF7179B5890524BCC0CF6B44ADFF3CB32593D8A05445F162A6226217389712401F85B6115366ADA0122566302798FF92C0994D8E9E1DDFB24E6E46CC0CF64FC527A57F334443682A6343CE6CA6D2EE9ACCBE3ED1BA6CD9B9935B1D56564E35077073F5CED9A2AC9A4B3F0CA0684E69AC1D9DD83C3A661FBB5AC2853A382CB4842687AC16664BB8B82DF9152DF9210A0608DE0483F7E1782CA932FD8F41D8F50ED10193AC0AD6263579D7261E443CDDB279BC02B20E3F055169EE2A27566E6F47382E91D9241CFA7137732514FA460C8183B489EFD8218C4F104351B9FF94DA2952709AE7ED29CE88FF0DEBB45863265DF7F1B536367816B57D42B4C8FCCABD2BAE3633D4A0AF2FAEA7515F36BABD3) <"$TMP/sk" >/dev/null + got=$(hex_out "$BC_RUST" hash-mldsa44 pk-from-sk -x <"$TMP/sk") + assert_eq "$got" "3a909812ef8d581a3118c2e9c33748943063daa58dbd96010428a7ae5d5d8d64e8a2f9aea78b11ef18a2b2ea9d852ba6c5c0278b36cd899a5397189780d63eb3601dfddd6d127a900d68abcec8e3379b57d5bbc99591daa6da230935bb9a7ded40e76bda363f1a30fe9539adffcf3e882997d90b9426bfb25a28bbf3630f539f6819ae26b6705a33aef4ec09b6e5de2f1d83ad9ebb880f7a8cee31148829b9999d0f0e907ea28d8b3a14cd2071a9b2fcc14194bf82b4474976eb851eb9cb2209cbd1dd75d986231d01f0697bb65586cb0ef0d0fd8416b90e5c8cf9129924709fe998b5bfeb69dcc1d2fdf43422cfa1ad926506f8ca7674b6f4820cb76690524b9c1b9b3841cc375dd2861f1dd88b64d0e733ad8a5fd87078bfda207c489b813e12f8f24fb291dbd427de0ff01eab3b66f59fc81ccd1f81af9fa5a44880e57789c4bf6457adc20248aa6388bc8ba04cefad9c28431395cb8f6ea6084a38dee1613d988974f9c5dd3ca7578ecd4c688b73c600ac9c7d96dab748ff9faff9240ead4c854d868da3b3ea1a022866a36acfa432c9847a4dc631d67dfd9ed83869793c3064f0693c9fd63a3aca815540737ae24796841f7ecd94a2715f669dfeb294c4a0739e7f8e6092b41155f7701f95fc5761279d98aef6fc31a977aba678b8dd339b82b934dd3b74c9484791b00701ba1cf8ab369f2ed4f2995c6aeb0b1ad7181b3d5da4cb209d629bf621eaf9353263666e368402931b4ffd82f45e38c19ed7068046108cedf4dec5139bdd66b13ebcb7dacbaea770cefd994149f77e575dcda6b406cabc970c08c693844c3abec151972fe913514802fb974e396c5ba5f5493538185d2523569878032f8d128506804a12b36b5023122d01b86038cc9445317d8e8332d21c15ae298d73820362b097781f8d34803e7aff1ae929f0bba507ccc0b722750b8b1c7e5dd6b0194d677bb75d889a0061e48427f8c0709c227506ebf44d4e9cbec17f92a9ada7dd82d4a49a21b3664120cc6b6c023eabac72be1425ad6d0fd3f15c1f767a4af63e774447d52bda71f62accb9dc9e084b2baccb2e84be167b47f61b737d4862437aa4d7cbf078a90c35564ddf1535d365a4e9d8a789f311cc002ac1c7e377def9f231026ecbbcdaa4363f1942abf265350257b540079b559164d27c6eb21f2b7d184d33599ddf6ad082867c8751bf83ff2cec7322898de686bfc243f4a51b84b83a6e1bbc1f483bdbeb6991e8334eb299360434b1bb35245f46fc514a79fc9f54147636966a56a295e249bc2ca55e2f62f5513da77acf35f9d2006fd0104a8d5f70948ee0ca9b4e144aa498d597f6747cbe79ffb28aaa189588a79b1286cc9dcae708a3188d23ae0e10c012211fb48a40ab3c65afe2a4eaf99e79ff39e2352e2e4cbe98f6c6340d7697f82da0fbbb703c2c57f28651d1f83a83be015d0599c42bf55f1465f8a3007ef7179b5890524bcc0cf6b44adff3cb32593d8a05445f162a6226217389712401f85b6115366ada0122566302798ff92c0994d8e9e1ddfb24e6e46cc0cf64fc527a57f334443682a6343ce6ca6d2ee9accbe3ed1ba6cd9b9935b1d56564e35077073f5ced9a2ac9a4b3f0ca0684e69ac1d9dd83c3a661fbb5ac2853a382cb4842687ac16664bb8b82df9152df9210a0608de0483f7e1782ca932fd8f41d8f50ed10193ac0ad6263579d7261e443cddb279bc02b20e3f055169ee2a27566e6f47382e91d9241cfa7137732514fa460c8183b489efd8218c4f104351b9ff94da2952709ae7ed29ce88ff0debb45863265df7f1b536367816b57d42b4c8fccabd2bae3633d4a0af2faea7515f36babd3" "hash-mldsa44 derives the same pk" +} + +# Wycheproof testvectors_v1/mldsa_44_verify_test.json tcId 1 ("baseline", external interface, empty +# context) must verify, and tcId 6 ("short signature") must not. The signature and key are given as +# hex files (--hex-input) and as binary ones; the message "Hello world" is stdin either way. +test_mldsa44_verify_matches_wycheproof() { + unhex 48656c6c6f20776f726c64 >"$TMP/msg" + printf '%s' db9ac67708f2ba0fac1f92bd802f9be89ecab966feef59872a1a9ac90b1111170a561290ae86b13968f2506023c014ba09fa449a26e4e9d35595e73986506cc8790e4d07a94d6c736f7ae78cc5e3e3cf025ce06a09252bef97fe92e94cbd107b1844d1a7c690d88bff9e9336f8f58e0bd5ee384de9c7ffbb149a6fcd87c77288601d8843e28e0c7a60149d02ebc57b183c39888d98b61cd8ad48135ddb8a1666743bb689f44c1a92d52017b6a8fa493eeb839dffb086a9a6c399b194a52f0e4164c96ff8a2a54337de24350a866b5fe4195257778e72511221778f1eae5fa93ed3532f696b9b0767aded85f62ea311027c7f5fc4182dcd2864b1c26bd6dcf72ebdedf70471327be0ea1c2ae53e46489c6dbefa512a78fdd7be0ad3ada16a7f7b1ece49817b44868a2cc234bfdba556c32cc92ec2c5e8a5d206f2e4ee372d41681e67d1b7e7b0061870c57f600fafca85f98aed8ce4ba76bba961f9ed56e563220d3ced853b6b28e7527da0e0912bc932a23c8bab811429bbb4d49b2770bcda44abb932b11c0a5866409fce39fed2b459c86c8f6e1ab0aefc5879503f4b21a49b4b2de6760c9b6aaf041144a656a26af39f4578e1d482ddc1360ef751d9784b860ec373d415360fe99f32e126a2ac1243430e8bed1bc90b19b3d219c2712edcf81c44b4331f6421088e662b695e1fd8fa5091f616ab60af70f159b63368f1ac60d77b279ed47ef7f24ec2044bb6c2bc76d933ecd568f7e663392afc1d335abac6c03670adf87747dde90052f5cd45f7d30f43a4dc3c500ceb658fce235c171240baca1b5a14733d774b9416c540f53eb83481afc98344b12a4309e6222b08d978430467497010314c6f6b8caf65361c216106395275a67d7500dbc120f7918c6f8db7aa63fa965b4a22c70dc88f727d768ce2bfc7597fd470184e1c59a6b2e1204cc8c3d052c594d5771e0ccc8cfb191f47038b1c0672f07caf4747562d3d76a9816fb1def1391cf0f05fcdbf2a0eb6c21ac24b26e74ee403133e80a79313ddb02c1fa386c6dd1d420195343e3a104aff6d60887f7304fa9e3bb59bb55f820dd85b1445c54e9a38dc1c7f3b88eb36a9f48d13455e51c934825ff3cd8bedb2b5422344120399eef83a360b83440ebdd8ea6e01c95159e3735bb4408500caa785ca4049891c7331c4ea31ad9060ece768fd339e6904f88e27bad3b28845687be2cc9314f300fda56fe3ff2508e54c59123b068f86fe00213d5af8da1b1735423ed688f097c306dbc121b81f532fcaf872d9f80596642295d6e4bead478644081618ab903b39e9b5e7cc0b5f2742d8337b18d4ad4788db7443e946cafc1762a5da84070e8c2fd86d6c633f0b44ee234ba11b9e1440c94a08d0437015279690405353059020fd2f58f15dab18754177244adfb81ceab79c7840bf3884a3d364afc8c453a425fd8c5378eaa7445f8c6256bfbd03a66c53e8cf27e2c52f14ef3294afe79cda408f5dff933ca0211a78a4e3be3d9a932558ed71ed19bbb57f87937fa3d4a78128491ff096a261045bdd186325c42caa8c7564195a4d2499a1c17d21a52d1aacd221d9c8a1866963a20390f2fd43dcf56b308a1c01c38091fd3e04c12b695de497d48bcc268d50cb0bed793b8e6937e8d533afd568521f1c9377a3804d38e785674d7ce868d289938e33dda6edc76d25b15fcb38852b7803cfe62f08d9fbd070957c4e6f134973964c9dc009985c8501e7d8f72e7ec285d5289fdd07f64d62acaa9737b039efa7a9d1d175577c6bcf9dddcf692877af38e75263bebe2453155be61f0723c274388a532abe29dd7023e327085f4c9dda41839b7b3357ab9d >"$TMP/pk.hex" + printf '%s' 1aa69cb5ed35204534f25f40a17eb0d767f8981f5e7cec46d3bf3252bfc78e09d02ef0c82da6dde973611c849472890106158cb15ffa6ca891615e888efa0d2d8a121b75ca440228ad32991be34249620f158ffd6f74d7b03bf919218ce259b500808ec157ead67b56b79e9e6607eafb9227b8a30adbec087d35bc1aec2f1a0c4dd126dfcfb9fbf0bd74fb1e092495fa994ab5a7cd1333281aafe834694a6dc11e889c762b5645638e172dfab3060031ebcdc1fd455d5de6050bf71b074a4dd34af5ebf15487651f0f13e5d3cee231b9b347810bfc418196df9d7231780c09171b9aae732bea27a1649d8c03220f417e30a016b08ccb1d55c9337b4812e20e04523f5d29a760a01b3a80d76285521206481ee1e44df09a76913ba54ae50c8eb973a3ced73950fbf39c4c0c1262a216821a442072c10cc82839ac57b898411be9e810f893272a2546ff7d1d920f146210efc2b4528bc98a099a398302d301fc1dea31b3d8ff78246a66f690ef536b68e02bd7ea23a5378930dde7f1beb51749896a7944e5a40e6fbbb1f76c3fda09e32e0a58062c24cac7ddb8c1d2cbb352a81e336425c5f551246db45ecd0ab29dac88cdebf51c60bebc2e27c974f56da12c1fec4f5745850429b607f5ed7cd821f2c91fc2dda8c2c8e8ec278d1b2a5bf50ec70c5623fc681b4d1dccff96b324cb53ff97470f9de177c2006a89af8f18603a6d4ea2625794695fe79cd30318a1e76307a4c2a353db1e076ad9b609a2489b94cb6dd821c3af31046bb7a5d43d190e09fce4969fe4e93c8393975e64cb2d9c2294fc427ef5191c40937929b3b0e1b037e6b84cc0299d5af2b5410118bfd88ef6491af6f21233390ca7a19f1576e6c5a10a673796905562075047896e3a2379f64dfbbc12b9bfe64939c2d05efbc5f6e4b5ca69ce1ed4b7d25e8c835b0612b33e13ed7a8a7233b4b3d58eead0bc4c841acb65a5ec0ed45e2584c23c2a162392b5789c62358e4038864e20c10e10c67d940ce78993178dbeb3de1fea1e50e7c29f4d7d938c3bfe50229ea040102f30d5b3a64cb8e13420065d54a1ac50a77383bdff3cae2340ebf15a1557fde897007c1b67d04f19431ca00cb0f08db87e90e166e0f4ce6fd69c6ecef1b3f70d9eb601b57a7bf931057c2afe2d3567b6bbec7891c664713385122fdd789c1d5a8a9cfd491f407c16d0b0c5dfc53a6862208e264b981bf2ddbdd1d7db9729b5265c4c3868a947c982880bc55b786153b89ef3324067b35a928c51236bcbe9f860ad9eee5644478f894a9fe78d26a5a17d482612f1cb9983b864e6fba84591c0f73b7b27918819d2121d4af640f533e2939d3da0f0aa6b9df80837a80165ae8b7579715192eb0f6cca78a43d8ad8d7abb56d816e3af2de59b88bfdcf6767abfb043d3ae24223d05001953faa292671c57ade1fe28988075ab8d14ac98363412bd694c40ae85b1f104afcd0f25aa590f57ba4f5dfdf613bf8594e3f54baadfdf50c0881af2475590758a23b7eee725513e4d1ea9f4630159c424a289f18a9879e5e173390f8e630f6ee2a6043d82a1983dc97c7acfea3b0c03e27e865d810d012daeec28dc454f59334edf24627d435701d329ff5e68d19bbdca5ef7d5e00204fa947d08f81cb6484cabe60989d2f61fbe70940f7e4f449b3fcb103a89143d74b15d72e7913dce9193a0b9c5a7b2a97bde6d7f396ae80b4b566f9f2e7345bc42ce3b002818e19f0f16416b850832cd02279ee8d58a381deaac09b1b4d4613f4d066805d2faea6716e015fe361c0526c6e4617a389ffdf930213c1dc0c4c905c3106a7517dce7abea7a9341132f8ad98de3e42f6b75809fff38f6eeafb97398e79d50a5622338763c4e45a88ddda7fb87ab7f5cec61109fdc1c5d4a16310275241fc34178028a49fd79581a05c3b6984eecd6cb9bd60a44da72600f8f2604a4ff4578126194fb2269c8e6e71447445e8e80bf8c6063dbbf29c7ded58abbc0d2eed347bf495b6a9cbe68585a594aa0e65834bfbcccc3f6bf42fe4ef42d86232c3fb1412dd0b5f8a0489958f5c3b883bf7851337a35b13dd2b6517626ff2d1064cd189beb402497dd6b8893d414dde7d1d51018c7a83766a2d8a29b80e8f428237732f7ee5ba878163f0aa8af5b60533b4d4621a38fdc54383acb3325b5e876e21483eddcc64c419596e656d1557be31b530ea66054f79d4f4a755f9ee33c84fed5d552ec5e2eccd061cf4c4b5c3c16a70a7baecccfc208d2430e78621c9ae5b0d080973b4e1df0a1f5c0415db6d3c85f9ea9041e9a9abdde71d6776a522aa957f708b14a33eda10ebbab93bb8ec2b7a04679f38eb44fc558ee698d3c6937e0e647dce898d7599fef6de32ae4d52adc722443610b2126559756aa36f3c79696b99be3d908f780fcbaef33b215693634d63ce2a0777dfbbc899d2be72efdbafbc10aefb26a1a63cdcfda00235b34db723c91624ee5f939024dfedba0863ad08f648767d41fc7fd6b317c51f4b1b87e21e07d488c9423581f9bbdae43e4d32b37e8283960524f9a601ab69f5cd7fb01c9a8a5c64fe863519a1e9f3426398f691a96e1748491b4e209fca2ab29481a674621c797614ba16fbfdfd1a4184e7f84667e720e6bcd9debef32c2b9e891a6e3c0423158f539838d8413e9fc707d5c65b23368b6edc95d9c3f8e20bbb844499311614945606c1487ef4015d1e260fa8239abaa071be572163132bdb06ef21e31be0f9d4b6747134e4842bedcd3bf53b0d6f054693bc428e9a715d5a32a79e6a3cb8b81faf2c04087d8816752637a2fe11eacb38341e024848562a29d4585e4ee56552ce9b1fad43b965a37bff8558921790ef0f4ac55bcac4327d1783f5e1e79bf01e96934bb4a8f5dd06c83bb70b2377189d622a106100f0cbdd38e34c0565900c616561161b3261859133f83893feb22b0bafc82cf4f0dbafde0648e2f86260e6e747034e5cb3ece98087fbf74179c6306f0c460b5d609b9b3a66761472ee0b9dabb5dfa872d5c6bc9b33461a27b5427bd8833f874f479ecc5f0a20304b9a75aacc82420a87af6469daaef53391ae8a25468e717dc47f464fce45a31147c0c4e12ac2f834567e4005b0827d13b3ec80cd8b7a907436e6624c6c8ac6a80add35cfa1a28872fb65cb3fa46894d116a052f19b5fe20c7ead10bd24d27ad2b5683f299d1193ec6d9ff3379f3b3e39cb9991831f194af2041085508da4dcb7b8785cbdc4e04cf4d826d1ef4a11036e4c5803c3aaa7669b4dd3bc12ff888984e9bbdace0e772ab59332b47300334757677578808e90929697a9cecfd6ed1b303d4250738a97a0aabac6d91e283a4b696f7181828493aec8cdcfe1eaecfc0227484957595badbcbfc8fa000000000000000000000000000000000000121f323e >"$TMP/sig.hex" + expect_ok "tcId 1 must verify" "$BC_RUST" --hex-input mldsa44 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig.hex" <"$TMP/msg" >/dev/null + unhex db9ac67708f2ba0fac1f92bd802f9be89ecab966feef59872a1a9ac90b1111170a561290ae86b13968f2506023c014ba09fa449a26e4e9d35595e73986506cc8790e4d07a94d6c736f7ae78cc5e3e3cf025ce06a09252bef97fe92e94cbd107b1844d1a7c690d88bff9e9336f8f58e0bd5ee384de9c7ffbb149a6fcd87c77288601d8843e28e0c7a60149d02ebc57b183c39888d98b61cd8ad48135ddb8a1666743bb689f44c1a92d52017b6a8fa493eeb839dffb086a9a6c399b194a52f0e4164c96ff8a2a54337de24350a866b5fe4195257778e72511221778f1eae5fa93ed3532f696b9b0767aded85f62ea311027c7f5fc4182dcd2864b1c26bd6dcf72ebdedf70471327be0ea1c2ae53e46489c6dbefa512a78fdd7be0ad3ada16a7f7b1ece49817b44868a2cc234bfdba556c32cc92ec2c5e8a5d206f2e4ee372d41681e67d1b7e7b0061870c57f600fafca85f98aed8ce4ba76bba961f9ed56e563220d3ced853b6b28e7527da0e0912bc932a23c8bab811429bbb4d49b2770bcda44abb932b11c0a5866409fce39fed2b459c86c8f6e1ab0aefc5879503f4b21a49b4b2de6760c9b6aaf041144a656a26af39f4578e1d482ddc1360ef751d9784b860ec373d415360fe99f32e126a2ac1243430e8bed1bc90b19b3d219c2712edcf81c44b4331f6421088e662b695e1fd8fa5091f616ab60af70f159b63368f1ac60d77b279ed47ef7f24ec2044bb6c2bc76d933ecd568f7e663392afc1d335abac6c03670adf87747dde90052f5cd45f7d30f43a4dc3c500ceb658fce235c171240baca1b5a14733d774b9416c540f53eb83481afc98344b12a4309e6222b08d978430467497010314c6f6b8caf65361c216106395275a67d7500dbc120f7918c6f8db7aa63fa965b4a22c70dc88f727d768ce2bfc7597fd470184e1c59a6b2e1204cc8c3d052c594d5771e0ccc8cfb191f47038b1c0672f07caf4747562d3d76a9816fb1def1391cf0f05fcdbf2a0eb6c21ac24b26e74ee403133e80a79313ddb02c1fa386c6dd1d420195343e3a104aff6d60887f7304fa9e3bb59bb55f820dd85b1445c54e9a38dc1c7f3b88eb36a9f48d13455e51c934825ff3cd8bedb2b5422344120399eef83a360b83440ebdd8ea6e01c95159e3735bb4408500caa785ca4049891c7331c4ea31ad9060ece768fd339e6904f88e27bad3b28845687be2cc9314f300fda56fe3ff2508e54c59123b068f86fe00213d5af8da1b1735423ed688f097c306dbc121b81f532fcaf872d9f80596642295d6e4bead478644081618ab903b39e9b5e7cc0b5f2742d8337b18d4ad4788db7443e946cafc1762a5da84070e8c2fd86d6c633f0b44ee234ba11b9e1440c94a08d0437015279690405353059020fd2f58f15dab18754177244adfb81ceab79c7840bf3884a3d364afc8c453a425fd8c5378eaa7445f8c6256bfbd03a66c53e8cf27e2c52f14ef3294afe79cda408f5dff933ca0211a78a4e3be3d9a932558ed71ed19bbb57f87937fa3d4a78128491ff096a261045bdd186325c42caa8c7564195a4d2499a1c17d21a52d1aacd221d9c8a1866963a20390f2fd43dcf56b308a1c01c38091fd3e04c12b695de497d48bcc268d50cb0bed793b8e6937e8d533afd568521f1c9377a3804d38e785674d7ce868d289938e33dda6edc76d25b15fcb38852b7803cfe62f08d9fbd070957c4e6f134973964c9dc009985c8501e7d8f72e7ec285d5289fdd07f64d62acaa9737b039efa7a9d1d175577c6bcf9dddcf692877af38e75263bebe2453155be61f0723c274388a532abe29dd7023e327085f4c9dda41839b7b3357ab9d >"$TMP/pk.bin"; unhex 1aa69cb5ed35204534f25f40a17eb0d767f8981f5e7cec46d3bf3252bfc78e09d02ef0c82da6dde973611c849472890106158cb15ffa6ca891615e888efa0d2d8a121b75ca440228ad32991be34249620f158ffd6f74d7b03bf919218ce259b500808ec157ead67b56b79e9e6607eafb9227b8a30adbec087d35bc1aec2f1a0c4dd126dfcfb9fbf0bd74fb1e092495fa994ab5a7cd1333281aafe834694a6dc11e889c762b5645638e172dfab3060031ebcdc1fd455d5de6050bf71b074a4dd34af5ebf15487651f0f13e5d3cee231b9b347810bfc418196df9d7231780c09171b9aae732bea27a1649d8c03220f417e30a016b08ccb1d55c9337b4812e20e04523f5d29a760a01b3a80d76285521206481ee1e44df09a76913ba54ae50c8eb973a3ced73950fbf39c4c0c1262a216821a442072c10cc82839ac57b898411be9e810f893272a2546ff7d1d920f146210efc2b4528bc98a099a398302d301fc1dea31b3d8ff78246a66f690ef536b68e02bd7ea23a5378930dde7f1beb51749896a7944e5a40e6fbbb1f76c3fda09e32e0a58062c24cac7ddb8c1d2cbb352a81e336425c5f551246db45ecd0ab29dac88cdebf51c60bebc2e27c974f56da12c1fec4f5745850429b607f5ed7cd821f2c91fc2dda8c2c8e8ec278d1b2a5bf50ec70c5623fc681b4d1dccff96b324cb53ff97470f9de177c2006a89af8f18603a6d4ea2625794695fe79cd30318a1e76307a4c2a353db1e076ad9b609a2489b94cb6dd821c3af31046bb7a5d43d190e09fce4969fe4e93c8393975e64cb2d9c2294fc427ef5191c40937929b3b0e1b037e6b84cc0299d5af2b5410118bfd88ef6491af6f21233390ca7a19f1576e6c5a10a673796905562075047896e3a2379f64dfbbc12b9bfe64939c2d05efbc5f6e4b5ca69ce1ed4b7d25e8c835b0612b33e13ed7a8a7233b4b3d58eead0bc4c841acb65a5ec0ed45e2584c23c2a162392b5789c62358e4038864e20c10e10c67d940ce78993178dbeb3de1fea1e50e7c29f4d7d938c3bfe50229ea040102f30d5b3a64cb8e13420065d54a1ac50a77383bdff3cae2340ebf15a1557fde897007c1b67d04f19431ca00cb0f08db87e90e166e0f4ce6fd69c6ecef1b3f70d9eb601b57a7bf931057c2afe2d3567b6bbec7891c664713385122fdd789c1d5a8a9cfd491f407c16d0b0c5dfc53a6862208e264b981bf2ddbdd1d7db9729b5265c4c3868a947c982880bc55b786153b89ef3324067b35a928c51236bcbe9f860ad9eee5644478f894a9fe78d26a5a17d482612f1cb9983b864e6fba84591c0f73b7b27918819d2121d4af640f533e2939d3da0f0aa6b9df80837a80165ae8b7579715192eb0f6cca78a43d8ad8d7abb56d816e3af2de59b88bfdcf6767abfb043d3ae24223d05001953faa292671c57ade1fe28988075ab8d14ac98363412bd694c40ae85b1f104afcd0f25aa590f57ba4f5dfdf613bf8594e3f54baadfdf50c0881af2475590758a23b7eee725513e4d1ea9f4630159c424a289f18a9879e5e173390f8e630f6ee2a6043d82a1983dc97c7acfea3b0c03e27e865d810d012daeec28dc454f59334edf24627d435701d329ff5e68d19bbdca5ef7d5e00204fa947d08f81cb6484cabe60989d2f61fbe70940f7e4f449b3fcb103a89143d74b15d72e7913dce9193a0b9c5a7b2a97bde6d7f396ae80b4b566f9f2e7345bc42ce3b002818e19f0f16416b850832cd02279ee8d58a381deaac09b1b4d4613f4d066805d2faea6716e015fe361c0526c6e4617a389ffdf930213c1dc0c4c905c3106a7517dce7abea7a9341132f8ad98de3e42f6b75809fff38f6eeafb97398e79d50a5622338763c4e45a88ddda7fb87ab7f5cec61109fdc1c5d4a16310275241fc34178028a49fd79581a05c3b6984eecd6cb9bd60a44da72600f8f2604a4ff4578126194fb2269c8e6e71447445e8e80bf8c6063dbbf29c7ded58abbc0d2eed347bf495b6a9cbe68585a594aa0e65834bfbcccc3f6bf42fe4ef42d86232c3fb1412dd0b5f8a0489958f5c3b883bf7851337a35b13dd2b6517626ff2d1064cd189beb402497dd6b8893d414dde7d1d51018c7a83766a2d8a29b80e8f428237732f7ee5ba878163f0aa8af5b60533b4d4621a38fdc54383acb3325b5e876e21483eddcc64c419596e656d1557be31b530ea66054f79d4f4a755f9ee33c84fed5d552ec5e2eccd061cf4c4b5c3c16a70a7baecccfc208d2430e78621c9ae5b0d080973b4e1df0a1f5c0415db6d3c85f9ea9041e9a9abdde71d6776a522aa957f708b14a33eda10ebbab93bb8ec2b7a04679f38eb44fc558ee698d3c6937e0e647dce898d7599fef6de32ae4d52adc722443610b2126559756aa36f3c79696b99be3d908f780fcbaef33b215693634d63ce2a0777dfbbc899d2be72efdbafbc10aefb26a1a63cdcfda00235b34db723c91624ee5f939024dfedba0863ad08f648767d41fc7fd6b317c51f4b1b87e21e07d488c9423581f9bbdae43e4d32b37e8283960524f9a601ab69f5cd7fb01c9a8a5c64fe863519a1e9f3426398f691a96e1748491b4e209fca2ab29481a674621c797614ba16fbfdfd1a4184e7f84667e720e6bcd9debef32c2b9e891a6e3c0423158f539838d8413e9fc707d5c65b23368b6edc95d9c3f8e20bbb844499311614945606c1487ef4015d1e260fa8239abaa071be572163132bdb06ef21e31be0f9d4b6747134e4842bedcd3bf53b0d6f054693bc428e9a715d5a32a79e6a3cb8b81faf2c04087d8816752637a2fe11eacb38341e024848562a29d4585e4ee56552ce9b1fad43b965a37bff8558921790ef0f4ac55bcac4327d1783f5e1e79bf01e96934bb4a8f5dd06c83bb70b2377189d622a106100f0cbdd38e34c0565900c616561161b3261859133f83893feb22b0bafc82cf4f0dbafde0648e2f86260e6e747034e5cb3ece98087fbf74179c6306f0c460b5d609b9b3a66761472ee0b9dabb5dfa872d5c6bc9b33461a27b5427bd8833f874f479ecc5f0a20304b9a75aacc82420a87af6469daaef53391ae8a25468e717dc47f464fce45a31147c0c4e12ac2f834567e4005b0827d13b3ec80cd8b7a907436e6624c6c8ac6a80add35cfa1a28872fb65cb3fa46894d116a052f19b5fe20c7ead10bd24d27ad2b5683f299d1193ec6d9ff3379f3b3e39cb9991831f194af2041085508da4dcb7b8785cbdc4e04cf4d826d1ef4a11036e4c5803c3aaa7669b4dd3bc12ff888984e9bbdace0e772ab59332b47300334757677578808e90929697a9cecfd6ed1b303d4250738a97a0aabac6d91e283a4b696f7181828493aec8cdcfe1eaecfc0227484957595badbcbfc8fa000000000000000000000000000000000000121f323e >"$TMP/sig.bin" + expect_ok "tcId 1 must verify from binary files" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk.bin" --sigfile "$TMP/sig.bin" <"$TMP/msg" >/dev/null + printf '%s' 1aa69cb5ed35204534f25f40a17eb0d767f8981f5e7cec46d3bf3252bfc78e09d02ef0c82da6dde973611c849472890106158cb15ffa6ca891615e888efa0d2d8a121b75ca440228ad32991be34249620f158ffd6f74d7b03bf919218ce259b500808ec157ead67b56b79e9e6607eafb9227b8a30adbec087d35bc1aec2f1a0c4dd126dfcfb9fbf0bd74fb1e092495fa994ab5a7cd1333281aafe834694a6dc11e889c762b5645638e172dfab3060031ebcdc1fd455d5de6050bf71b074a4dd34af5ebf15487651f0f13e5d3cee231b9b347810bfc418196df9d7231780c09171b9aae732bea27a1649d8c03220f417e30a016b08ccb1d55c9337b4812e20e04523f5d29a760a01b3a80d76285521206481ee1e44df09a76913ba54ae50c8eb973a3ced73950fbf39c4c0c1262a216821a442072c10cc82839ac57b898411be9e810f893272a2546ff7d1d920f146210efc2b4528bc98a099a398302d301fc1dea31b3d8ff78246a66f690ef536b68e02bd7ea23a5378930dde7f1beb51749896a7944e5a40e6fbbb1f76c3fda09e32e0a58062c24cac7ddb8c1d2cbb352a81e336425c5f551246db45ecd0ab29dac88cdebf51c60bebc2e27c974f56da12c1fec4f5745850429b607f5ed7cd821f2c91fc2dda8c2c8e8ec278d1b2a5bf50ec70c5623fc681b4d1dccff96b324cb53ff97470f9de177c2006a89af8f18603a6d4ea2625794695fe79cd30318a1e76307a4c2a353db1e076ad9b609a2489b94cb6dd821c3af31046bb7a5d43d190e09fce4969fe4e93c8393975e64cb2d9c2294fc427ef5191c40937929b3b0e1b037e6b84cc0299d5af2b5410118bfd88ef6491af6f21233390ca7a19f1576e6c5a10a673796905562075047896e3a2379f64dfbbc12b9bfe64939c2d05efbc5f6e4b5ca69ce1ed4b7d25e8c835b0612b33e13ed7a8a7233b4b3d58eead0bc4c841acb65a5ec0ed45e2584c23c2a162392b5789c62358e4038864e20c10e10c67d940ce78993178dbeb3de1fea1e50e7c29f4d7d938c3bfe50229ea040102f30d5b3a64cb8e13420065d54a1ac50a77383bdff3cae2340ebf15a1557fde897007c1b67d04f19431ca00cb0f08db87e90e166e0f4ce6fd69c6ecef1b3f70d9eb601b57a7bf931057c2afe2d3567b6bbec7891c664713385122fdd789c1d5a8a9cfd491f407c16d0b0c5dfc53a6862208e264b981bf2ddbdd1d7db9729b5265c4c3868a947c982880bc55b786153b89ef3324067b35a928c51236bcbe9f860ad9eee5644478f894a9fe78d26a5a17d482612f1cb9983b864e6fba84591c0f73b7b27918819d2121d4af640f533e2939d3da0f0aa6b9df80837a80165ae8b7579715192eb0f6cca78a43d8ad8d7abb56d816e3af2de59b88bfdcf6767abfb043d3ae24223d05001953faa292671c57ade1fe28988075ab8d14ac98363412bd694c40ae85b1f104afcd0f25aa590f57ba4f5dfdf613bf8594e3f54baadfdf50c0881af2475590758a23b7eee725513e4d1ea9f4630159c424a289f18a9879e5e173390f8e630f6ee2a6043d82a1983dc97c7acfea3b0c03e27e865d810d012daeec28dc454f59334edf24627d435701d329ff5e68d19bbdca5ef7d5e00204fa947d08f81cb6484cabe60989d2f61fbe70940f7e4f449b3fcb103a89143d74b15d72e7913dce9193a0b9c5a7b2a97bde6d7f396ae80b4b566f9f2e7345bc42ce3b002818e19f0f16416b850832cd02279ee8d58a381deaac09b1b4d4613f4d066805d2faea6716e015fe361c0526c6e4617a389ffdf930213c1dc0c4c905c3106a7517dce7abea7a9341132f8ad98de3e42f6b75809fff38f6eeafb97398e79d50a5622338763c4e45a88ddda7fb87ab7f5cec61109fdc1c5d4a16310275241fc34178028a49fd79581a05c3b6984eecd6cb9bd60a44da72600f8f2604a4ff4578126194fb2269c8e6e71447445e8e80bf8c6063dbbf29c7ded58abbc0d2eed347bf495b6a9cbe68585a594aa0e65834bfbcccc3f6bf42fe4ef42d86232c3fb1412dd0b5f8a0489958f5c3b883bf7851337a35b13dd2b6517626ff2d1064cd189beb402497dd6b8893d414dde7d1d51018c7a83766a2d8a29b80e8f428237732f7ee5ba878163f0aa8af5b60533b4d4621a38fdc54383acb3325b5e876e21483eddcc64c419596e656d1557be31b530ea66054f79d4f4a755f9ee33c84fed5d552ec5e2eccd061cf4c4b5c3c16a70a7baecccfc208d2430e78621c9ae5b0d080973b4e1df0a1f5c0415db6d3c85f9ea9041e9a9abdde71d6776a522aa957f708b14a33eda10ebbab93bb8ec2b7a04679f38eb44fc558ee698d3c6937e0e647dce898d7599fef6de32ae4d52adc722443610b2126559756aa36f3c79696b99be3d908f780fcbaef33b215693634d63ce2a0777dfbbc899d2be72efdbafbc10aefb26a1a63cdcfda00235b34db723c91624ee5f939024dfedba0863ad08f648767d41fc7fd6b317c51f4b1b87e21e07d488c9423581f9bbdae43e4d32b37e8283960524f9a601ab69f5cd7fb01c9a8a5c64fe863519a1e9f3426398f691a96e1748491b4e209fca2ab29481a674621c797614ba16fbfdfd1a4184e7f84667e720e6bcd9debef32c2b9e891a6e3c0423158f539838d8413e9fc707d5c65b23368b6edc95d9c3f8e20bbb844499311614945606c1487ef4015d1e260fa8239abaa071be572163132bdb06ef21e31be0f9d4b6747134e4842bedcd3bf53b0d6f054693bc428e9a715d5a32a79e6a3cb8b81faf2c04087d8816752637a2fe11eacb38341e024848562a29d4585e4ee56552ce9b1fad43b965a37bff8558921790ef0f4ac55bcac4327d1783f5e1e79bf01e96934bb4a8f5dd06c83bb70b2377189d622a106100f0cbdd38e34c0565900c616561161b3261859133f83893feb22b0bafc82cf4f0dbafde0648e2f86260e6e747034e5cb3ece98087fbf74179c6306f0c460b5d609b9b3a66761472ee0b9dabb5dfa872d5c6bc9b33461a27b5427bd8833f874f479ecc5f0a20304b9a75aacc82420a87af6469daaef53391ae8a25468e717dc47f464fce45a31147c0c4e12ac2f834567e4005b0827d13b3ec80cd8b7a907436e6624c6c8ac6a80add35cfa1a28872fb65cb3fa46894d116a052f19b5fe20c7ead10bd24d27ad2b5683f299d1193ec6d9ff3379f3b3e39cb9991831f194af2041085508da4dcb7b8785cbdc4e04cf4d826d1ef4a11036e4c5803c3aaa7669b4dd3bc12ff888984e9bbdace0e772ab59332b47300334757677578808e90929697a9cecfd6ed1b303d4250738a97a0aabac6d91e283a4b696f7181828493aec8cdcfe1eaecfc0227484957595badbcbfc8fa000000000000000000000000000000000000121f32 >"$TMP/sig-bad.hex" + expect_fail "tcId 6 must not verify" "$BC_RUST" --hex-input mldsa44 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig-bad.hex" <"$TMP/msg" + assert_stderr_has "invalid" + expect_fail "another message must not verify" "$BC_RUST" --hex-input mldsa44 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig.hex" < <(printf 'Hello World') +} + +# Signing is randomized (FIPS 204 Alg 2 draws rnd), so there is no signature known answer; the +# round trip pins the wiring: sign with the sk, verify with the derived pk, with and without a +# context, in the pure and the HashML-DSA (SHA-512) forms, and each form rejects the other's +# signature and the other context. +test_mldsa44_sign_verify_round_trip() { + "$BC_RUST" mldsa44 keygen >"$TMP/sk" + assert_size "$TMP/sk" 2560 "sk length of ML-DSA-44" + "$BC_RUST" mldsa44 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + rng 3000 >"$TMP/msg" + "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig" + assert_size "$TMP/sig" 2420 "signature length of ML-DSA-44" + expect_ok "pure signature verifies" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" >/dev/null + printf 'ctx' >"$TMP/ctx" + expect_fail "a context the signer did not use must not verify" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" --ctxfile "$TMP/ctx" <"$TMP/msg" + "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" --ctxfile "$TMP/ctx" <"$TMP/msg" >"$TMP/sig-ctx" + expect_ok "signature with context verifies with it" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ctx" --ctxfile "$TMP/ctx" <"$TMP/msg" >/dev/null + expect_fail "and not without it" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ctx" <"$TMP/msg" + "$BC_RUST" hash-mldsa44 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig-ph" + expect_ok "HashML-DSA signature verifies as HashML-DSA" "$BC_RUST" hash-mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ph" <"$TMP/msg" >/dev/null + expect_fail "and not as pure ML-DSA" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ph" <"$TMP/msg" + expect_fail "nor the pure one as HashML-DSA" "$BC_RUST" hash-mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" +} + +# ACVP ML-DSA keyGen (bc-test-data/pqc/crypto/mldsa/ML-DSA-keyGen.txt, ML-DSA-65 tcId 1): the 32-byte +# seed xi gives the vector's sk (`keygen-from-seed`) and pk (`pk-from-sk`), from raw and hex seeds +# (hex needs --hex-input). +test_mldsa65_keygen_from_seed_matches_acvp() { + local got + unhex A30C1FB71307C921442B8095B1FA6FB56D418073FB96014EDDB94BE47F1C590B >"$TMP/seed" + got=$(hex_out "$BC_RUST" mldsa65 keygen-from-seed -x <"$TMP/seed") + assert_eq "$got" "892da8b7b2c3b5fa066184e9491843fc3bd244f34c8d9f00ad2efd801e32cf45ee2d280dd9186625095236726efbc0e9d09f38d13c7ff92c894b026a23fb8a62e2abafcadf97cfbee688b6deed98da562aa0e0465ab9c82da0bdf73391ab32d1d6caa298a6f251ae72920054a50bccb44371e78cb21eecc11b42e2d6ebd89aba425008141512044350457338104086260085760805818887677628826675386877655463302458724413104357504433463681638635081416202227420265045656403887228586256253142331657414512376031243840167256003323287066454533601386741221351413735622287328338666424630384220216135137727336075685327387824102080607742316242411866331118585073381322353815061214040308853831303815008786606106575756475234833872076085878543638174411022578635758021510067461608816435348857546724876755672536240452261540581871481768768745373710504610843621851433432270100233584027345228651424568513480728154438717048561688473172725827316702708113142862458114060326606673738880672380747522788854843662100181051353141285548157158518447126460460001051141784268107477265386582313484558814471783417151416510147401168023471333176118543848715741771155571246745362007352023250154035187058044678578377838631585522218658213244382534223688427353657745643601325371676777312772111770070473132776710057242647641032380360043250886170431726153612420260006718070443034131123751544700757062682177822766573005050088825201660246666172284611010544821220007651176606638880576172512824653442411381334382242401866704176856347866574151178433227566682161677134325628005814002682338168807384483636176476721347120467833470225825221183326083748386468000558238346851243077403150015313428071538563800602363051342088734081018385243051000457514518205827470767041715835721317805836847100648507717826138225870478084646656704805533372436648757223161210438212355414474482075133156224860003735117882816467533247388185708661874510563177336864424337361584603038176641520756687043858552258611225280255028021318183374268742875132215454858401270306463734013763200026563065427682276437371877507182185632305183265004167364357640508117831177077516306770308327761514556248846033003703832573421814886162764015373467701508322570778512225126842866716506664128343833718476034878816154626566523667516423245516704702272472707222836551083041121223374447737884131482462377833376537753184831038545867626862707753527461568060746372021270872146673342460272608517761713806243316841242618240388042464285805355466122672754650122722386252243082760783276356613738722838240311313203124856474354236870266834678463478235082444861155222645412618508374810775462186750270510550813567113717642671654352108831288576557236052424804845831257781247840678373401316413588301661844353541748601438153585204687560287627607318683757145102401727210356874155015088750121757581406501447038323667347748771453703634816756433071274651277161375023431310880328545106606480808583010304442584861458681182281687302053251545032123848182608515460200681118841480427015311246876148253708432752612153233638800581554243678114425420815153073435685312308742523856074138277667161783438318268888211782548144507012886106672548051300557122413175773704457043734341367258bbfb3d95a547db281d08a8fbd8fe6c5496b7d05ce1342a6e89488a43697b0c19d554db4199ca5e7f6b3c0488e8227c700226d0763731193ce9ec54b30d9e6cf35dab538fa848d9707cfdfb6b0d6f892710bf86360f874e66bd674f726ba6710b8baee0f63525955a551d1d5ed111c2981a0bc8de651fb7079efee4a3569f09625f105d043ade4ac633108e0597c747ced1c1fb2a90a2d98fbe495704a8c582bcbcaea14b499651f831341ebb3671f2d15f66dfa6ee6ef1e0fc85f1a09a551c807dcebcefaf1f879e5dac5ff7cd2d8fa08cb96784c9e1e96f1dfff94d199d4739d0a73943bbf56fdfb81c2555a1c2be90a1ce0fd3fed2aa7f09d1b564bb99cdab0e800e6435d36ee8b57d46e0b37a1766de78b1e71c2b4c3b110d6d402e76610767c85e945fd3b8707db00bef4ccb156cd2959d1c7442212f74b35990cfaa4c38221c4c837f9090b901b2da53429276ef92911c88c1ba207c3b4a9a1a185864548b05f0fdfb65398206a7fa22fc568c0b2512e407566f64e550c75165e843e67783e34071f3c4004ef124600261098ff561ff3dc9569b20a2c7c7a37c7deb922443e2d61a18be0d3a5857adb5ef9f011e5bb8d36de7e3510990a70398aa1ae3e0f935fc78754cdf90e91e88c7ad3abbca4cd809421cb5e9eb014d4269884c6eb8d85c42f2676e39e9da30abbfc0807e2ead8ca177d9f27e6598bf9da29b999e3ba66bbeb6b27421013f67c389322e15353ddd3ad4f57c44e37be27be6e2311c1da0f4d3a6fdce318004306efe0424476dabdce43eee8acc0f5465f5c8d7bddbc372670660e36b97ac7ae17979b3ea76a8ab4e3035a2fb95dbf48d5ea69acd658550ec310e76860a4082fa7164b335dadd5756e794005b5e8d686243c32433988632c89745c037debd51204112154bc6d0b37842e609b5392619f44b5fd29f7ff36f80286c1bae439799660380fc6a207abebbd7c42473d8d5d5db12e18466d82e4720fe8a7af6b80aa38580cf71f72746b2e76f2630ba7438ab9944334f78d9450579fec825bee08c11e0e31bc762cbd69ab11705a5dc5bc1c559639d5fd85e55a078b2a017f49717b0b287d7cbb75547943992e2e5ea684b76d7753a2ca3159697b28be0ed12145c96f0294b2e4710e7b479a7aa4d27d7baf81a287fb73b259868599455b751522dc9813a1b1f908883b3607cf427bfc301b3b6d118a9d401fc9121fe0f1bd5965bc5c9ba3f7e29d877480a3cf047fc393313ce4468b491686ab7d91945ec276e0ab83da04cf3195d809c774d2aea049a98ba34b19e9e23c158b6694cdf596162788e27b1b075917415b1d4e1b55dbb5374f0c8b90c20cc05c9c35f9b6a3cdf91907e95829134eddacf6e78058629016344ed294c7158d00d639acd4057eb175ef4e409bba9d443cb20a8108f622361ee78a101189a033f7a1e82b75f9f66923eba5cabceffe3312fd1ce02454e64ee0feb9b6357c78bdb53a3e318f73b39253d4d122c35c71054e1516e157668c290bdfdb660329822579e0c7cca6c482da4d62a8f2e4177972d3f67f10c18f9eb5a61e815e09c3909b1f415141560be2ebbec7735b89ed2b12cc7b2028c027c95e6d293a92bea09bf354a69b6b3c0671c324d05a877f9f2088f9777d85d9ee83150446526fb13b9ae89b93c5ab20d07fac48fd93f1b10dfd5023deca84eacab6a8eb0d817ca27cef14a88c8dc3b432d470e88dc50c2c5c75ace5cf1710706b7ae3e9ebdbc247bdd89f08f7496f14111de054469450217b099901badb8e4c4c683240eee05cc55d23a68a0f2401b1913b95d9c8add5d54c1db013d8da5e3abec2993ff98160d3fc9b3786184cb9cd74e3f20ae33039ab7b1df2d430136b39ad434c8a901e8ad2c6017ce3a17383251ba51eaa11a48725265a3647bdc248a390eaf0114b5c85968f4cd2e7f37d652048a922d2535cd1f4408f999c64b00a31a097a7a66103d39e0613853b4dafa690350963ae9686cb3b9c0e7c7b1c3eaa857439cbf188e906b6ef2582f86d61fc1cbb166900983b17a0d130977a0785b38407738bcbdb0cd6d738b15c9eb4da0292dffbb0cf737983582ffcce249848c61369db2c3cbef61f5483f71d5736e1ca730a9b6e284bec23591a734e1b592759d91fb3f25ecb94bd9bafc8639cbb7a216766285ef0d00034094019ceb30739b5545717ee050ce900a78499cbf8c7b08d8e926f72133c427e0db77caaa1b47565d35583be4232677e87da2bf1eab31502a65c3c835a6ef89e0b85be54e9b97a636bd23cf982a3441996dd639320d8f65a28bc28c5288ac27abfb328d5718061e2197eec6e08e5c43aefc9cd736b75ac9b54135fd251d25faf242a3c407fdd7a19314dd992504bbfc7f8c2990441b21c7ccfd161005d2234aa1726e3b92e4efbcd78cb365a2e8ab64500ef190026f5eef017cd8f1257b70368a5bbf36e72cc86969fe968de45a48bab4f235263eb4e07730883e97a0e2012a6207d727e04affe6ea1ed00dda3cd4a91f515681282b7f2ab2458e6dd163c76f66ca6678de0f6b0c1afbc0abadbdeef00a19785131a60feb0eb0ba094c9fc648d4b1a22f2810bbca94547bed7710c97623c0c97a0d2bfa18c783a6638119a8f3b85c69c81012b49ebd6ec3a2520d719f787d39f7d25a24aa259e106a6c9557f7c9ac3e912f4e42840bf0b34704b962430f2e6ec9326651820a31fe2d1c397aa4fe7e6359acabf22acf701fb9762dc08fa83d4f524e4de41b6a56be8ecbe3456982f42eeb234564485bad145b44336dabc0d77737709021c29cc657d5d43d0c40fe219cdcfcf5d2c4384cb0e73bc76196943194f90aa2dc265a7ffe3da285a204690f2980e4c5f90ef5dfb14712d107cfde877a4723d8d30ceac2487f0bdb170410db6f39e6a68362c88727111e7a9f0c3f036b4a8b86f49f0db1f7d86264f9f6033166687da91a10829ae39170d635e7d3830bc065ec67c9557766c926db448ea13e5dbf18cd313e0bfc563cd11aef181cec2648e64cf11be76a935df752631e3779c41546ec9bfdadd2414b115ff456825766953f6420234c53235d228941df314a9fabaabac877208398bcaf27cc02796d2d0e5369766ea11a436017fa2d15db19d08eb7888c56abd17e1ba8d6aca2b76163313c8df4e607f7af4f8d4a0eb92cac8014c3249dfc17dec03971f811c943550c4dac1ce664a7eb0f4c109664bbb1c2087a5282284a3572aa9f6cd5e36ff156e5d7e7bf6e4aabbf70ab91defc5ca0cc1d7957fb3725b5980d3bbdf246a18ade7e81f318b1c9aa11a8fe5e6949be1002cf9a62e53d71df98cc39f5b2bd8729d3d5ae0d604dba3cbe357aa6aea5dfb513505c525068152ec52eced0c6e2efb4063279097882f3fc715560d309273d352d310900ef50191a4bcb7bfcbaf03a48a841389fe8132fb77a690f17049512c0795a25b7dcb6b702fd31c0789c5a42c0ed4a5df21eccc79e3c4da597d55b170768f5d168005e5f93c06d7f281697099080938b34c8e" "sk from a raw seed" + got=$(printf '%s' A30C1FB71307C921442B8095B1FA6FB56D418073FB96014EDDB94BE47F1C590B | hex_out "$BC_RUST" --hex-input mldsa65 keygen-from-seed -x) + assert_eq "$got" "892da8b7b2c3b5fa066184e9491843fc3bd244f34c8d9f00ad2efd801e32cf45ee2d280dd9186625095236726efbc0e9d09f38d13c7ff92c894b026a23fb8a62e2abafcadf97cfbee688b6deed98da562aa0e0465ab9c82da0bdf73391ab32d1d6caa298a6f251ae72920054a50bccb44371e78cb21eecc11b42e2d6ebd89aba425008141512044350457338104086260085760805818887677628826675386877655463302458724413104357504433463681638635081416202227420265045656403887228586256253142331657414512376031243840167256003323287066454533601386741221351413735622287328338666424630384220216135137727336075685327387824102080607742316242411866331118585073381322353815061214040308853831303815008786606106575756475234833872076085878543638174411022578635758021510067461608816435348857546724876755672536240452261540581871481768768745373710504610843621851433432270100233584027345228651424568513480728154438717048561688473172725827316702708113142862458114060326606673738880672380747522788854843662100181051353141285548157158518447126460460001051141784268107477265386582313484558814471783417151416510147401168023471333176118543848715741771155571246745362007352023250154035187058044678578377838631585522218658213244382534223688427353657745643601325371676777312772111770070473132776710057242647641032380360043250886170431726153612420260006718070443034131123751544700757062682177822766573005050088825201660246666172284611010544821220007651176606638880576172512824653442411381334382242401866704176856347866574151178433227566682161677134325628005814002682338168807384483636176476721347120467833470225825221183326083748386468000558238346851243077403150015313428071538563800602363051342088734081018385243051000457514518205827470767041715835721317805836847100648507717826138225870478084646656704805533372436648757223161210438212355414474482075133156224860003735117882816467533247388185708661874510563177336864424337361584603038176641520756687043858552258611225280255028021318183374268742875132215454858401270306463734013763200026563065427682276437371877507182185632305183265004167364357640508117831177077516306770308327761514556248846033003703832573421814886162764015373467701508322570778512225126842866716506664128343833718476034878816154626566523667516423245516704702272472707222836551083041121223374447737884131482462377833376537753184831038545867626862707753527461568060746372021270872146673342460272608517761713806243316841242618240388042464285805355466122672754650122722386252243082760783276356613738722838240311313203124856474354236870266834678463478235082444861155222645412618508374810775462186750270510550813567113717642671654352108831288576557236052424804845831257781247840678373401316413588301661844353541748601438153585204687560287627607318683757145102401727210356874155015088750121757581406501447038323667347748771453703634816756433071274651277161375023431310880328545106606480808583010304442584861458681182281687302053251545032123848182608515460200681118841480427015311246876148253708432752612153233638800581554243678114425420815153073435685312308742523856074138277667161783438318268888211782548144507012886106672548051300557122413175773704457043734341367258bbfb3d95a547db281d08a8fbd8fe6c5496b7d05ce1342a6e89488a43697b0c19d554db4199ca5e7f6b3c0488e8227c700226d0763731193ce9ec54b30d9e6cf35dab538fa848d9707cfdfb6b0d6f892710bf86360f874e66bd674f726ba6710b8baee0f63525955a551d1d5ed111c2981a0bc8de651fb7079efee4a3569f09625f105d043ade4ac633108e0597c747ced1c1fb2a90a2d98fbe495704a8c582bcbcaea14b499651f831341ebb3671f2d15f66dfa6ee6ef1e0fc85f1a09a551c807dcebcefaf1f879e5dac5ff7cd2d8fa08cb96784c9e1e96f1dfff94d199d4739d0a73943bbf56fdfb81c2555a1c2be90a1ce0fd3fed2aa7f09d1b564bb99cdab0e800e6435d36ee8b57d46e0b37a1766de78b1e71c2b4c3b110d6d402e76610767c85e945fd3b8707db00bef4ccb156cd2959d1c7442212f74b35990cfaa4c38221c4c837f9090b901b2da53429276ef92911c88c1ba207c3b4a9a1a185864548b05f0fdfb65398206a7fa22fc568c0b2512e407566f64e550c75165e843e67783e34071f3c4004ef124600261098ff561ff3dc9569b20a2c7c7a37c7deb922443e2d61a18be0d3a5857adb5ef9f011e5bb8d36de7e3510990a70398aa1ae3e0f935fc78754cdf90e91e88c7ad3abbca4cd809421cb5e9eb014d4269884c6eb8d85c42f2676e39e9da30abbfc0807e2ead8ca177d9f27e6598bf9da29b999e3ba66bbeb6b27421013f67c389322e15353ddd3ad4f57c44e37be27be6e2311c1da0f4d3a6fdce318004306efe0424476dabdce43eee8acc0f5465f5c8d7bddbc372670660e36b97ac7ae17979b3ea76a8ab4e3035a2fb95dbf48d5ea69acd658550ec310e76860a4082fa7164b335dadd5756e794005b5e8d686243c32433988632c89745c037debd51204112154bc6d0b37842e609b5392619f44b5fd29f7ff36f80286c1bae439799660380fc6a207abebbd7c42473d8d5d5db12e18466d82e4720fe8a7af6b80aa38580cf71f72746b2e76f2630ba7438ab9944334f78d9450579fec825bee08c11e0e31bc762cbd69ab11705a5dc5bc1c559639d5fd85e55a078b2a017f49717b0b287d7cbb75547943992e2e5ea684b76d7753a2ca3159697b28be0ed12145c96f0294b2e4710e7b479a7aa4d27d7baf81a287fb73b259868599455b751522dc9813a1b1f908883b3607cf427bfc301b3b6d118a9d401fc9121fe0f1bd5965bc5c9ba3f7e29d877480a3cf047fc393313ce4468b491686ab7d91945ec276e0ab83da04cf3195d809c774d2aea049a98ba34b19e9e23c158b6694cdf596162788e27b1b075917415b1d4e1b55dbb5374f0c8b90c20cc05c9c35f9b6a3cdf91907e95829134eddacf6e78058629016344ed294c7158d00d639acd4057eb175ef4e409bba9d443cb20a8108f622361ee78a101189a033f7a1e82b75f9f66923eba5cabceffe3312fd1ce02454e64ee0feb9b6357c78bdb53a3e318f73b39253d4d122c35c71054e1516e157668c290bdfdb660329822579e0c7cca6c482da4d62a8f2e4177972d3f67f10c18f9eb5a61e815e09c3909b1f415141560be2ebbec7735b89ed2b12cc7b2028c027c95e6d293a92bea09bf354a69b6b3c0671c324d05a877f9f2088f9777d85d9ee83150446526fb13b9ae89b93c5ab20d07fac48fd93f1b10dfd5023deca84eacab6a8eb0d817ca27cef14a88c8dc3b432d470e88dc50c2c5c75ace5cf1710706b7ae3e9ebdbc247bdd89f08f7496f14111de054469450217b099901badb8e4c4c683240eee05cc55d23a68a0f2401b1913b95d9c8add5d54c1db013d8da5e3abec2993ff98160d3fc9b3786184cb9cd74e3f20ae33039ab7b1df2d430136b39ad434c8a901e8ad2c6017ce3a17383251ba51eaa11a48725265a3647bdc248a390eaf0114b5c85968f4cd2e7f37d652048a922d2535cd1f4408f999c64b00a31a097a7a66103d39e0613853b4dafa690350963ae9686cb3b9c0e7c7b1c3eaa857439cbf188e906b6ef2582f86d61fc1cbb166900983b17a0d130977a0785b38407738bcbdb0cd6d738b15c9eb4da0292dffbb0cf737983582ffcce249848c61369db2c3cbef61f5483f71d5736e1ca730a9b6e284bec23591a734e1b592759d91fb3f25ecb94bd9bafc8639cbb7a216766285ef0d00034094019ceb30739b5545717ee050ce900a78499cbf8c7b08d8e926f72133c427e0db77caaa1b47565d35583be4232677e87da2bf1eab31502a65c3c835a6ef89e0b85be54e9b97a636bd23cf982a3441996dd639320d8f65a28bc28c5288ac27abfb328d5718061e2197eec6e08e5c43aefc9cd736b75ac9b54135fd251d25faf242a3c407fdd7a19314dd992504bbfc7f8c2990441b21c7ccfd161005d2234aa1726e3b92e4efbcd78cb365a2e8ab64500ef190026f5eef017cd8f1257b70368a5bbf36e72cc86969fe968de45a48bab4f235263eb4e07730883e97a0e2012a6207d727e04affe6ea1ed00dda3cd4a91f515681282b7f2ab2458e6dd163c76f66ca6678de0f6b0c1afbc0abadbdeef00a19785131a60feb0eb0ba094c9fc648d4b1a22f2810bbca94547bed7710c97623c0c97a0d2bfa18c783a6638119a8f3b85c69c81012b49ebd6ec3a2520d719f787d39f7d25a24aa259e106a6c9557f7c9ac3e912f4e42840bf0b34704b962430f2e6ec9326651820a31fe2d1c397aa4fe7e6359acabf22acf701fb9762dc08fa83d4f524e4de41b6a56be8ecbe3456982f42eeb234564485bad145b44336dabc0d77737709021c29cc657d5d43d0c40fe219cdcfcf5d2c4384cb0e73bc76196943194f90aa2dc265a7ffe3da285a204690f2980e4c5f90ef5dfb14712d107cfde877a4723d8d30ceac2487f0bdb170410db6f39e6a68362c88727111e7a9f0c3f036b4a8b86f49f0db1f7d86264f9f6033166687da91a10829ae39170d635e7d3830bc065ec67c9557766c926db448ea13e5dbf18cd313e0bfc563cd11aef181cec2648e64cf11be76a935df752631e3779c41546ec9bfdadd2414b115ff456825766953f6420234c53235d228941df314a9fabaabac877208398bcaf27cc02796d2d0e5369766ea11a436017fa2d15db19d08eb7888c56abd17e1ba8d6aca2b76163313c8df4e607f7af4f8d4a0eb92cac8014c3249dfc17dec03971f811c943550c4dac1ce664a7eb0f4c109664bbb1c2087a5282284a3572aa9f6cd5e36ff156e5d7e7bf6e4aabbf70ab91defc5ca0cc1d7957fb3725b5980d3bbdf246a18ade7e81f318b1c9aa11a8fe5e6949be1002cf9a62e53d71df98cc39f5b2bd8729d3d5ae0d604dba3cbe357aa6aea5dfb513505c525068152ec52eced0c6e2efb4063279097882f3fc715560d309273d352d310900ef50191a4bcb7bfcbaf03a48a841389fe8132fb77a690f17049512c0795a25b7dcb6b702fd31c0789c5a42c0ed4a5df21eccc79e3c4da597d55b170768f5d168005e5f93c06d7f281697099080938b34c8e" "sk from a hex seed" + "$BC_RUST" mldsa65 keygen-from-seed <"$TMP/seed" >"$TMP/sk" + got=$(hex_out "$BC_RUST" mldsa65 pk-from-sk -x <"$TMP/sk") + assert_eq "$got" "892da8b7b2c3b5fa066184e9491843fc3bd244f34c8d9f00ad2efd801e32cf4549a9de8b0311aec5f8a20c10754b587b9f554954016598b3b63fe771a426bae15850435158544e6e3824d03944680c568b95534d205308d3d357a3b3cb4c1e096913d0c1c5b1e24b3a68a95a6ef1055d3ebd172d8b5f5e3000314e54c98948f731cf1a21beb376c55eeef45101dcd99e95acd6a29239e7c6b48dea3a032fc6a4faa20cdae02a4a393c7fe10db764d81bff74d6971b391cd01cd9c79ce2c0b58172bb37cdce6941a722f89cc3fd18a1f839b2021671f082f5e97db049bb5424d5326ab21aa4249548297d1f9bfc9317098b85bc57a0eb3510eb718a70b334675773eb658439209d0179642db0c534d4bc67c549f1ebeb2214d726fb4c29798243e1e99a9464407912df26352019d6d224a7f130eb041bcebbdfd4811401d95426bc701965a5d51cadaf8d9567b4d5c62521527474f92f72098375a32246363ef8be4e6f36b1f42d26c127b75c2334264008f56731c711a525dff640b9379c2c818c09992ae87a8b36e9b5291cd61391aef2570e9c62355822e4447bbf56b6e0a8ee95cc2bd9ce6a6eb8e93dc480e8602084d839347df6c5889d164b5f0a82e27d2fc3a6ec6296eb3c1aa958cfa7999306ccc2a5af62f3b4e9107159abbf12960271b67d5571303abf5dc08b80d11bd6c6ea5e0abe6ddc9ba8495ff1c4e6609a73ad1092d39b94b826fc2b087766e6e57e42e468d56c4383970d764ddd9a61540a5b259db5b8d3c6647033fe52682d20a1cee15e5973f4a56953442705ded63ce29aa9e9650de636bc9d7bbd242db82bfc3ab2ab5be5d52e8ec4903ed71053c4dcef63fb77a53ff7f5f31d2e5cc88eacfc3a5cb6318d405a952164a986da2fc7a575cbbaa537802aeb795546637abdd664c49b5f031ced0b85752087af6ac7441ab3487ce713fd14dff7b1ee6fc06b5790bbc320b73dc076d5d0a061a96548f9f0a3b2721f843b63273519202910a6cf6a6620827b80cb5667ca7b5c3f224e8200bbf7c19b68e799155aeea414ec0322e305a31804c72da5983f77892e3d3878eab8e2f02f948a9291cd857a37b5fa938a5dc3d0d9a37ef6383d5aa3a43c8463a9367f023a67595f05468069ce284e50200d0ab5f9cea52e0ead067b98f84ad09c03ae049c0382b9f04c5fad161b8d2aabb4fbd00bd3277d01de5ead95dc68208fa1ce83d8a40fa8d4e5a177c7230259a25cc8d7120288fcf9214a3d86c5c77c64d6319e9c558d0d03b9b15ce2ddf9fce04b4646f18a86343f4f70f1df18780ea5b75e3bdf8cf292a6127f6dcb7ae9734c794daa57bd827bab26d9f3249b06a57081d153cdeb182ea503f4d7aaec22ec979634ed217f3e98813c0582baca597740404f560f08f203d26a44e8649f6e9459e269b5989ed145c4618a2a2ded9c1a7c052db8d03f11af032587aa15ae9900d229d08d1cc7d5f72b31e0bf6ca7dcf0efc3f7bd45fdfa6b2a8eb90718ad7eda033847fea9bd413b42242b9105bc63976f6f4d6f66a30e2e684e7255df93509ed18085d9124e72b7db2b1c8f2c967d9667b8756219fb8fc403730ed53247abec7e77659a079ba757db4656c5b0f38670a21242ea185912ae58d18b3ee5f5eec88a9e161f8d4a2341fdfc25bd0379c1ca8b8e4d94e3c0abc3287ccef79adf650f571c485e32e18794b0f8d137e35062c40465beadc58e91bc8b9a14e4ee88a027241737fa9bd903c44df7cda2983059f928fff7cd01de9477f5a2b5bf6f5d031837a355dc605bcf8c2803912a6ee273f9b8ec0c03e64ec2e12e4b66e7a3aacc2d1643e678304589bd9e46369741825f603fe9bf41e8811a1378ffb90f08a85f4efb2f2206354a6db32354caec96d7189f009c9f4b85eb230df5ef923db4667edccc082022b090c2df7e83987d15df7a2cb9bd393f0d87c8945d380e856acfca00ab7a2e698453e0441e916dd28b5859d88b5decd2a4905ab62bd29d244fe5770faa15fe79d1ee93c4f6fadfb035f81e6be9f56628ea0436e8160909335b462807e64181c67f18105cbd2931a3825d498b19098afc2bac5464f1bccf68fc30f3f192aa59497a49bea292d6188bd7ae5efc1abf25cf8129ebfe03e3b7cc996f306bdee24ae661bb3156d0a907b6346c9836b1a715e7073a2b0d38da54ea2e1a392208b365f136719146f67105ee152c64fe74eb3062b343f4031fb62510a4e12fed1d5e453159121c619ab0aaf51413ffcd28d4256ab80fc8989f45ead6590cd9ba6e9d8e1024d8fb4b7dc3cd20654813e4bb716d12494d7708271084a6d2e9ff544c477161ad2e3b956c18a6e2c2a619f773f4028f7bc0eb767c0445fd4c432ab2392e063cb84aa266c2fcfb5703bf7fc15de4af108a8b486e9901a60237f1a056bf6f10b99646cdaa55dbff355ad356d3a9850ba9a871b5a4642715837885da33daf8db247eb923c97ba268d375588a7fe0c7364f1b0e1756b21e637a872be537e205a5430b2f2ac2e706899ad9ce1f02ec768fb1abe7b144935bf27dbd39f883770b94c27785dac7deb4a6b53597e851362b2d4934fcb9aed5a145b328ce2c72fc90a52211894ea7b8133bb66ad8c563ac38356ba1255356e04c4c0f93e3acfa23627a72fa70fc51e5f1847332e0cc0ca7733230109de416db86b3f6791267aff619a582de4a6e4e8d8fb7c7025ad12880a143b8936d347eed61a17fbd1723b6327f2966510d3570d5ead382a24319a516" "pk from the sk" + expect_ok "the pair must be consistent" "$BC_RUST" mldsa65 check-consistency --pkfile <(unhex 892DA8B7B2C3B5FA066184E9491843FC3BD244F34C8D9F00AD2EFD801E32CF4549A9DE8B0311AEC5F8A20C10754B587B9F554954016598B3B63FE771A426BAE15850435158544E6E3824D03944680C568B95534D205308D3D357A3B3CB4C1E096913D0C1C5B1E24B3A68A95A6EF1055D3EBD172D8B5F5E3000314E54C98948F731CF1A21BEB376C55EEEF45101DCD99E95ACD6A29239E7C6B48DEA3A032FC6A4FAA20CDAE02A4A393C7FE10DB764D81BFF74D6971B391CD01CD9C79CE2C0B58172BB37CDCE6941A722F89CC3FD18A1F839B2021671F082F5E97DB049BB5424D5326AB21AA4249548297D1F9BFC9317098B85BC57A0EB3510EB718A70B334675773EB658439209D0179642DB0C534D4BC67C549F1EBEB2214D726FB4C29798243E1E99A9464407912DF26352019D6D224A7F130EB041BCEBBDFD4811401D95426BC701965A5D51CADAF8D9567B4D5C62521527474F92F72098375A32246363EF8BE4E6F36B1F42D26C127B75C2334264008F56731C711A525DFF640B9379C2C818C09992AE87A8B36E9B5291CD61391AEF2570E9C62355822E4447BBF56B6E0A8EE95CC2BD9CE6A6EB8E93DC480E8602084D839347DF6C5889D164B5F0A82E27D2FC3A6EC6296EB3C1AA958CFA7999306CCC2A5AF62F3B4E9107159ABBF12960271B67D5571303ABF5DC08B80D11BD6C6EA5E0ABE6DDC9BA8495FF1C4E6609A73AD1092D39B94B826FC2B087766E6E57E42E468D56C4383970D764DDD9A61540A5B259DB5B8D3C6647033FE52682D20A1CEE15E5973F4A56953442705DED63CE29AA9E9650DE636BC9D7BBD242DB82BFC3AB2AB5BE5D52E8EC4903ED71053C4DCEF63FB77A53FF7F5F31D2E5CC88EACFC3A5CB6318D405A952164A986DA2FC7A575CBBAA537802AEB795546637ABDD664C49B5F031CED0B85752087AF6AC7441AB3487CE713FD14DFF7B1EE6FC06B5790BBC320B73DC076D5D0A061A96548F9F0A3B2721F843B63273519202910A6CF6A6620827B80CB5667CA7B5C3F224E8200BBF7C19B68E799155AEEA414EC0322E305A31804C72DA5983F77892E3D3878EAB8E2F02F948A9291CD857A37B5FA938A5DC3D0D9A37EF6383D5AA3A43C8463A9367F023A67595F05468069CE284E50200D0AB5F9CEA52E0EAD067B98F84AD09C03AE049C0382B9F04C5FAD161B8D2AABB4FBD00BD3277D01DE5EAD95DC68208FA1CE83D8A40FA8D4E5A177C7230259A25CC8D7120288FCF9214A3D86C5C77C64D6319E9C558D0D03B9B15CE2DDF9FCE04B4646F18A86343F4F70F1DF18780EA5B75E3BDF8CF292A6127F6DCB7AE9734C794DAA57BD827BAB26D9F3249B06A57081D153CDEB182EA503F4D7AAEC22EC979634ED217F3E98813C0582BACA597740404F560F08F203D26A44E8649F6E9459E269B5989ED145C4618A2A2DED9C1A7C052DB8D03F11AF032587AA15AE9900D229D08D1CC7D5F72B31E0BF6CA7DCF0EFC3F7BD45FDFA6B2A8EB90718AD7EDA033847FEA9BD413B42242B9105BC63976F6F4D6F66A30E2E684E7255DF93509ED18085D9124E72B7DB2B1C8F2C967D9667B8756219FB8FC403730ED53247ABEC7E77659A079BA757DB4656C5B0F38670A21242EA185912AE58D18B3EE5F5EEC88A9E161F8D4A2341FDFC25BD0379C1CA8B8E4D94E3C0ABC3287CCEF79ADF650F571C485E32E18794B0F8D137E35062C40465BEADC58E91BC8B9A14E4EE88A027241737FA9BD903C44DF7CDA2983059F928FFF7CD01DE9477F5A2B5BF6F5D031837A355DC605BCF8C2803912A6EE273F9B8EC0C03E64EC2E12E4B66E7A3AACC2D1643E678304589BD9E46369741825F603FE9BF41E8811A1378FFB90F08A85F4EFB2F2206354A6DB32354CAEC96D7189F009C9F4B85EB230DF5EF923DB4667EDCCC082022B090C2DF7E83987D15DF7A2CB9BD393F0D87C8945D380E856ACFCA00AB7A2E698453E0441E916DD28B5859D88B5DECD2A4905AB62BD29D244FE5770FAA15FE79D1EE93C4F6FADFB035F81E6BE9F56628EA0436E8160909335B462807E64181C67F18105CBD2931A3825D498B19098AFC2BAC5464F1BCCF68FC30F3F192AA59497A49BEA292D6188BD7AE5EFC1ABF25CF8129EBFE03E3B7CC996F306BDEE24AE661BB3156D0A907B6346C9836B1A715E7073A2B0D38DA54EA2E1A392208B365F136719146F67105EE152C64FE74EB3062B343F4031FB62510A4E12FED1D5E453159121C619AB0AAF51413FFCD28D4256AB80FC8989F45EAD6590CD9BA6E9D8E1024D8FB4B7DC3CD20654813E4BB716D12494D7708271084A6D2E9FF544C477161AD2E3B956C18A6E2C2A619F773F4028F7BC0EB767C0445FD4C432AB2392E063CB84AA266C2FCFB5703BF7FC15DE4AF108A8B486E9901A60237F1A056BF6F10B99646CDAA55DBFF355AD356D3A9850BA9A871B5A4642715837885DA33DAF8DB247EB923C97BA268D375588A7FE0C7364F1B0E1756B21E637A872BE537E205A5430B2F2AC2E706899AD9CE1F02EC768FB1ABE7B144935BF27DBD39F883770B94C27785DAC7DEB4A6B53597E851362B2D4934FCB9AED5A145B328CE2C72FC90A52211894EA7B8133BB66AD8C563AC38356BA1255356E04C4C0F93E3ACFA23627A72FA70FC51E5F1847332E0CC0CA7733230109DE416DB86B3F6791267AFF619A582DE4A6E4E8D8FB7C7025AD12880A143B8936D347EED61A17FBD1723B6327F2966510D3570D5EAD382A24319A516) <"$TMP/sk" >/dev/null + got=$(hex_out "$BC_RUST" hash-mldsa65 pk-from-sk -x <"$TMP/sk") + assert_eq "$got" "892da8b7b2c3b5fa066184e9491843fc3bd244f34c8d9f00ad2efd801e32cf4549a9de8b0311aec5f8a20c10754b587b9f554954016598b3b63fe771a426bae15850435158544e6e3824d03944680c568b95534d205308d3d357a3b3cb4c1e096913d0c1c5b1e24b3a68a95a6ef1055d3ebd172d8b5f5e3000314e54c98948f731cf1a21beb376c55eeef45101dcd99e95acd6a29239e7c6b48dea3a032fc6a4faa20cdae02a4a393c7fe10db764d81bff74d6971b391cd01cd9c79ce2c0b58172bb37cdce6941a722f89cc3fd18a1f839b2021671f082f5e97db049bb5424d5326ab21aa4249548297d1f9bfc9317098b85bc57a0eb3510eb718a70b334675773eb658439209d0179642db0c534d4bc67c549f1ebeb2214d726fb4c29798243e1e99a9464407912df26352019d6d224a7f130eb041bcebbdfd4811401d95426bc701965a5d51cadaf8d9567b4d5c62521527474f92f72098375a32246363ef8be4e6f36b1f42d26c127b75c2334264008f56731c711a525dff640b9379c2c818c09992ae87a8b36e9b5291cd61391aef2570e9c62355822e4447bbf56b6e0a8ee95cc2bd9ce6a6eb8e93dc480e8602084d839347df6c5889d164b5f0a82e27d2fc3a6ec6296eb3c1aa958cfa7999306ccc2a5af62f3b4e9107159abbf12960271b67d5571303abf5dc08b80d11bd6c6ea5e0abe6ddc9ba8495ff1c4e6609a73ad1092d39b94b826fc2b087766e6e57e42e468d56c4383970d764ddd9a61540a5b259db5b8d3c6647033fe52682d20a1cee15e5973f4a56953442705ded63ce29aa9e9650de636bc9d7bbd242db82bfc3ab2ab5be5d52e8ec4903ed71053c4dcef63fb77a53ff7f5f31d2e5cc88eacfc3a5cb6318d405a952164a986da2fc7a575cbbaa537802aeb795546637abdd664c49b5f031ced0b85752087af6ac7441ab3487ce713fd14dff7b1ee6fc06b5790bbc320b73dc076d5d0a061a96548f9f0a3b2721f843b63273519202910a6cf6a6620827b80cb5667ca7b5c3f224e8200bbf7c19b68e799155aeea414ec0322e305a31804c72da5983f77892e3d3878eab8e2f02f948a9291cd857a37b5fa938a5dc3d0d9a37ef6383d5aa3a43c8463a9367f023a67595f05468069ce284e50200d0ab5f9cea52e0ead067b98f84ad09c03ae049c0382b9f04c5fad161b8d2aabb4fbd00bd3277d01de5ead95dc68208fa1ce83d8a40fa8d4e5a177c7230259a25cc8d7120288fcf9214a3d86c5c77c64d6319e9c558d0d03b9b15ce2ddf9fce04b4646f18a86343f4f70f1df18780ea5b75e3bdf8cf292a6127f6dcb7ae9734c794daa57bd827bab26d9f3249b06a57081d153cdeb182ea503f4d7aaec22ec979634ed217f3e98813c0582baca597740404f560f08f203d26a44e8649f6e9459e269b5989ed145c4618a2a2ded9c1a7c052db8d03f11af032587aa15ae9900d229d08d1cc7d5f72b31e0bf6ca7dcf0efc3f7bd45fdfa6b2a8eb90718ad7eda033847fea9bd413b42242b9105bc63976f6f4d6f66a30e2e684e7255df93509ed18085d9124e72b7db2b1c8f2c967d9667b8756219fb8fc403730ed53247abec7e77659a079ba757db4656c5b0f38670a21242ea185912ae58d18b3ee5f5eec88a9e161f8d4a2341fdfc25bd0379c1ca8b8e4d94e3c0abc3287ccef79adf650f571c485e32e18794b0f8d137e35062c40465beadc58e91bc8b9a14e4ee88a027241737fa9bd903c44df7cda2983059f928fff7cd01de9477f5a2b5bf6f5d031837a355dc605bcf8c2803912a6ee273f9b8ec0c03e64ec2e12e4b66e7a3aacc2d1643e678304589bd9e46369741825f603fe9bf41e8811a1378ffb90f08a85f4efb2f2206354a6db32354caec96d7189f009c9f4b85eb230df5ef923db4667edccc082022b090c2df7e83987d15df7a2cb9bd393f0d87c8945d380e856acfca00ab7a2e698453e0441e916dd28b5859d88b5decd2a4905ab62bd29d244fe5770faa15fe79d1ee93c4f6fadfb035f81e6be9f56628ea0436e8160909335b462807e64181c67f18105cbd2931a3825d498b19098afc2bac5464f1bccf68fc30f3f192aa59497a49bea292d6188bd7ae5efc1abf25cf8129ebfe03e3b7cc996f306bdee24ae661bb3156d0a907b6346c9836b1a715e7073a2b0d38da54ea2e1a392208b365f136719146f67105ee152c64fe74eb3062b343f4031fb62510a4e12fed1d5e453159121c619ab0aaf51413ffcd28d4256ab80fc8989f45ead6590cd9ba6e9d8e1024d8fb4b7dc3cd20654813e4bb716d12494d7708271084a6d2e9ff544c477161ad2e3b956c18a6e2c2a619f773f4028f7bc0eb767c0445fd4c432ab2392e063cb84aa266c2fcfb5703bf7fc15de4af108a8b486e9901a60237f1a056bf6f10b99646cdaa55dbff355ad356d3a9850ba9a871b5a4642715837885da33daf8db247eb923c97ba268d375588a7fe0c7364f1b0e1756b21e637a872be537e205a5430b2f2ac2e706899ad9ce1f02ec768fb1abe7b144935bf27dbd39f883770b94c27785dac7deb4a6b53597e851362b2d4934fcb9aed5a145b328ce2c72fc90a52211894ea7b8133bb66ad8c563ac38356ba1255356e04c4c0f93e3acfa23627a72fa70fc51e5f1847332e0cc0ca7733230109de416db86b3f6791267aff619a582de4a6e4e8d8fb7c7025ad12880a143b8936d347eed61a17fbd1723b6327f2966510d3570d5ead382a24319a516" "hash-mldsa65 derives the same pk" +} + +# Wycheproof testvectors_v1/mldsa_65_verify_test.json tcId 1 ("baseline", external interface, empty +# context) must verify, and tcId 6 ("short signature") must not. The signature and key are given as +# hex files (--hex-input) and as binary ones; the message "Hello world" is stdin either way. +test_mldsa65_verify_matches_wycheproof() { + unhex 48656c6c6f20776f726c64 >"$TMP/msg" + printf '%s' f5408337d0fee65c28851226a5fa81b58464632c78e2a9bef70d330f2e3a5f74d9cf676aedd1067c91a5dd5d4edc46f868a93ffec9f44e254e44f682a153aeadf228e8db7c5fcfed30cc3408e261ab896876bee56660d2a7c1d7eac20c5754255206a178f7156295065ce7876f90c48f44bc37f3a00e32eefd3a4bb1e298fe283d106eaef92a33a594253a2a0790976a1d04636f8672d28c06c852ea8bb43b84bff512996e7616963d5b9a2906466a152c7ea9be178be35405683b44367af85d2daad87630c1e21ba5490154f0141780f5ed0407cb0b975dd56d5930f9b26413b843b83f3693304b0038bd3e4bb398868060ea18c9c67099376470a50deb052e4056743fbcdf0341b192663bd1c21ba3b3d5666e0d0e29c4e1ed0759ab0bd9d1d355011b94e0ff0c049b03ddb7138640667144fcacd7265f55a07e5387f1abd30c037cf14d436aa855f827049215440d8007f61460500d943f57ffb6bfee6fedd2fcec52882d7d8da1aab29e892c8beac3df3234b4a7d2eca3a45c6623c52bbdd07c1c94314b706988a52029f8f8b06e874b741d72926652c78c6ace2cfd8864eadb2e4b39cafe6e03e4edbafa2747db9bc42f92af8b031e3e380846b1bfd15ade88c285d6a6fffe91eafc8b17de6cbc68575f323cc09fc20e49e8efd76f9568bec486b78df4245428d8d0d5f53873e11de65fda4c770b521a8c67f5c51d48cc26358954514447881fd9a42e5891dac7e1db5249d7861b322111e5fb929bee9ff5e9d5a2667ba93e63fc03040d2e82648f89e89dec1d1d2dfb9efeceb7940f7dcbebeb5a239cc1c54d8f7d52cba220d0634e15df46a58280bc5a48840bd39274cfde150f9ad9a40f6398d715350925f0e0501944409f32331a362bdaaafb3d8ce71c964332d6afb7e684f99951246d88081c86744ae68133f22c53a4b5ae258f230a98491d2d43a79a6d0f4d54a3b62013965ac7c82d0507125a38a0277f81cbc1d46cef2a131c6f51b88ec0baae0c82a6a0e72831cb06f9116cff5111d597e01057d32805a008f52c9aec3311139bfb35982789ff83bdd0c31e9f1080e8ed8eb99fde66bafb29e3357389fe3785b60c78e229ef073e1b65e34d848bd4d8a4f251551e2d38d2546afbc205d3c6dab34d2b962b1afb44f1d22fc10c6744fcd6b636afd3cb414b16c2e0d708fe9f51ff19120bde693b028b6d1e6dbe37b4b8b3bc7c6f7a842701603869d3ded572500f085502efc8d3cc62b30e5cdbcb5e86d9c0d42973bf755df539cc0aea58f9148386db67bd2bf70cd12ccd96d5c66fb271416b772465228dc44b079178f9b766370b66a79b871faca246ca6f8f63be9f0668297ac446cad5cf4a83318b1b00ecbd283f0eecee60a9a37a27abdbdbe382e307970002837dfc0bd3934ebd008918fd4bd383c02c9d37f694996e989a49075767ebc4a2981ef5275455e026cb0bd70946cdd1fadaf251381d324f9efbb860d1b280c29685bab97d010676273b45cca12ac3966aae342c84e2357eccf252577743b8787967b40b07ef2d3d9e6c1a3bcb059cba0fdb7f0d4f815c242b8e14acd3375e608e9230ba3cf8718f43882a3e1e661a2bbe81830d34741f33473e263b3790abe67acf29f5df44865b2ffbc96975fd62738a64112deda5a2534fb0a23b3b3024df986391badf9041c593c313a7ca1e1fcffcb65b07b9a99337b4a4acf616cbe1553eb9541f38aa6247342905995233a28172ca13396b2a9662970120f82b92a213f43de7a232ccca3268265c9ce042d50915430a6c455f32277da42f9962fb9163b623231ebc080fa7b8e9f9021fcf85b98f9c483e4d2226b9326a5bcb2e7449ef029ae142d3a0f0c28bd4f7e9c51a12e1336f24dfacbc3f808a8f7dd683027bc948763b808fb0037394b8b41bc9b2ec7887e67584e03d11b15ca203b2bcb43f8881638c4e4eee7f846d09c7f89b7739df22b2c3acc235032ba8f7ae27b5b9d25733143e80a4cdde6770719c1e66ec2ce683612233e88fafff84c0745a98aa1254c8219c6c556348c2b5d1beeb61532d6bf7bde153271dc647460beb65fe0055b33fd6480dcbb9d7d471952cfa5be260c39721a8c5c89b9e966ae2dc9036451ec9f2c49433b2225e13f23e20c2bfba81a7b3a555883449238f7d48213e9f10ce19e76f1bdcfc73ee5524bd7d8be0a4b46784e238233c04fb99383ec7726f9717e1179dd14fba9ad6c2ebd1699f0ab0e57e6cad23875b029e89cfda06f51266ecd2eed4edafb51e82f2a506d57ba74da611774ca5fa2fff4a976519de425885e7d09219cf815b1767d4fc5a72c18918991a285086a6a766614a4d245387da50f28dd778fb33ab88c0918feba3768c55bb1f07aec33cfeed33d6faa4d34fd7227b365533c1e67dbc89f0b20195cf1cbd480d333ade1c9bb28308085b72ced430268c1492a27050c43668adc9cf8b8509447cfcd3c8f8d8eb554f704101786aa9ebca86991d250776a37a1f56fbf7d08e591f978da49c3870625879f70e2418aec5cba32fa8c346fa9038baebc35ad0068a4d03537aee14c2e71570a87490377fa8dd66f995aa044a522f0c7025a7ab2dd5ad30a64268dc112b7f9fa156df64d631f55f1d6edc55cec570a9c7372e29e02c8d4867bae249431dcf6ed2794a0183f0f7501201feca4a81d334c642fc8d38e9a90fa77429665e09e214797dfa455ff47c4f219d3a2cb0176bc2236455123c1c5da714ad29d580fb194f87173a18dc >"$TMP/pk.hex" + printf '%s' 69da5aec6d5f58fbf29439c520bd68b966e3dd2ca633b68351c2862344713a1e9c086a44f9a870a3ccc14de62d6c12b278c354d7197c4d6d7f83d1422b29b250f5ee3fec118311d905e5db2b4b8b23b8d542202d6652f6dc3f9d7ed51f2463082d3f145cfd0fa7ac548a47e91c1ccb1a55b215e90ab355bfc6d67154287b1dfae0fb530264dbb841a7684b396e5ca0459d795216416a9d232bc89b32e0f9461f53107c78e66c8e876554e8ddd501867b55dcfc1fb33f102e03373cdd192640f1027a08ce277b468f6ed0fe80a9d6cd2d6b2f7a3738c8325d95b0ccc6e7b9fb000c923b92298e0867d4a9f6dd5513e8001033c633bb1641ee66349487224dd43386c7fcc29916332066a868100d46e2c5b8354c28f087a024cba27694afc4c1665e0d72b37686919ad55052cc63a144febe4e2a0c9ae416e064e289f9f69cbb883665d1130826b7b74e30c94a2b98b67b471663e3d66326db3b43bebf958e8665b68eda90e8c5d9494b0c7c9ec48800910dd6d906b1fcd47a0aac462ac87b126d21b5ba150df61f752257ddf5a063b4a5b150371d625535e3b2874b9fe548960ff67931cd6c12496e8213e2ace6fff48e6bdc60310e49389f62579db26b92ad73e9d3f23942cab51784f48b3660b6450caecbb0df2aa4c8e56577f5ea450d2f7f51aacc0b304a62250bf2cae7b99dcd955b6596625d06da1c67f730b706fdba630f00fd891830d251484640b7258ab364d6fd9986878fffa69b7c44b92e43143affae8b098e1d27716850f37553bf266cdfb561abbcdbfeb80752b364434e64b80429b54cc88693ce03dc0fa147f0741b215f0728499bdc25140aafc976ac99e910ba8a8a50d21b7bddaa28626b3b90a93fd44077068357c81d36e735eda4362930adead4951a0baa104f384fc70e842a9f329e1868b07b455e9cc3fecd54805c9052e70f88c3b92fe0fc6a4d7dda18cf5694e5398860e439a1e19d5a66f2fbc0aacdd1a498711bb16054796c015a715395ef6174e37b04eda589b673c4d5dda737817fb52f392caf7a72d7a3e84b2180cb5b75bc8af065bdc05c3e4040435a1b160081352ac43e09cbf2ead6e09c2b0be0e37894888fe2812f68806f957c13fce6ff167bcee21d4f412ec95a4847f3db7bf441223a4d4ca9ed69adb4de8a4b5b01c775f2721226e6c59ff26fc38e1bb78a384b30e7b55f082e264d8f25e31518619ddd6b6a9faf8aa6cdb5eab75ed59a33825d5ef8b93bde5d120ada773fcc0852b918f4f03e2d2a543b15363adb823eb1f6c533b98d940411e1f5c1cf521f9f63d5454697608326625fffe01bf87f44187dad631df2898effd2c291d98222e564abe3b042b75e90c9c54667842fa8ebb68a1244bf8e0c3ae3ee5f97d5ddeefd986c4bd3f99d877c2cc2381a89abdc61713d38cee58bf69805a485c288d21b15843147066b4a74c69dc25de878e21d35fdfe6746feb4c166606bf3219e42cf63581e7e6bd6570f40f8fae590cedf5106fe57037ccb2324b74fca6500f6ed3d0736cdcc67d04f8fa9e80054a5bd7c8459fc1abb1c4c78677d7f6b325af94a0e5c9c7db0a748e12c5265e8724947d9b5c4bab1a8b6faec827cc41ec115ef3c2d7348cddabddfbc8436f3b41765e13f3762b3b45ed23156f085831e726a55d4b83848b3d1d3352aab9edcc0ac2388f2383f6301ad813b917ee3f23734e057832ae4cf65e668c9ddd0bdd0f9d8b6693254649668aa91a1fa5eb7c59859bb6ddd36c25f4a2223f5d688b480d0388fa307ea69298f9bf7737f6b3dbfda87b331affd75cd8d88f0460e98ebc2890b217bd6d11000a3a088cd837f4f8859a43f76afaaab05a0c3007a149d4d6b9155cadc2c9b55003efdec5012b6272b87183694c505f0446ede55f35b8ab201f9eda974ff840eccb0f004fa3acf753acd0613f66e2a6ac82e322199d37b4af83cbb3d98371c31be79bb42331e819644cbad2ce27a04e4c517998692cd8331552892e199a01a6922bda4d38ac4c01f708809e529c3216eaab399ef25b350ea213ba47126f278140e17391ca7139bd13c56f415e6b74aed8dbfbf38c95dc6db366fd72aa863a27fa1ebf198716400b978a3709e35039731930406588ebdffd35fa230a9b75fce41d7acd214ca4f0029896c137495eade0cf4d10fe621c73f01061acb077de72177ff5dbc6f0c5bec681aa34668ca4fcdd727525068b0b0e9072971b84ef6ce11d5c3c6024da40966703dcc2b33ae04f677677635a55db508f34f1403cdbe37960c8577dac3d848b29f3b5c5c6c56fb74f34c8f4634c04b8cce9b218f1760ca00e6de87efd14087c633469c892bf3e319443336733bb60cfb44941bfa25229aa24384d812db90fe74e0f93fda005eea87400736cabc036f71421b6657b1674d4a8f76cbbf3a8b1c0af82f72973927752257c532db439d96762ad64f102551a9d03f9ce3d8cc850c393c128bf8054bb55bb92ea31ec0706f083a9cf90424c617f8ad2a21225d1913c30e8f47a6b7131304d536a85596ebfd987b64b6bf3c51638d6c839214b53c3c10aa52bd9c6eb77fcf80b5e3b724dec1381d0e02207a6adc73ff53d9d1ffcee1c4a28fa5445ce518eee937074ff7a402f5bbcb362ff090415f9dbd93b62ee56dc8c50e4d2e34c6c621650c0dffe311484e95d68de77170c909c815828946aeeec7ede56bcf433e22fc63a33f764ced1f9242f3d26dc7558686e471f30fbe9304d3d56af8b23e72a4088970b24b2f7e968c1d0392eeeb0b0f0ac8c176547a5383d948ed15484b79e21314a1f28ed624f61e5aaecf2269e5b027e1910ffddede52fad4e8da224e8a10b079548fa7cd44172f4991adfd7623d13e5a19c812824bcf990c07c9721ded9093be6ce7bc7da3ac8c932133a64396b822be92b088844991596df893625a4ef24543bf75a10d7d17ff70350ef62ce3a7758aebbf9b3977b08becb9ea28376082f607965f2cded28bbdb39dab7e00833b0488370d221742b66e27d9ee2d9dd07f401bc22a62c8a9d8d3a290c63804991496aafa47a32578f583cfb53d0c2199055973440d7535e0da6cb2957f4e04002ecea68f9c3ff76cade27ed15fd7835989d0abb197fe32f68636139a42710644bb25860ff33f539200e3ccb8a7738422ca0fa0c744b4c19d15c5d4a3cb082e20a78e20b5a4965b043595cbcacad500b5adbb6cd597e6a4b9c5ea6a1f2e653b5474da277f1818048094ac9e0e1e0b20068d1c1ce5a114a4db7195057a6ce4d221c336fdc29190fee8ff855cae8b7f7c02eec21f972c827066d9c6dcc4a4179bc44ea9b88abe5124bf78b071e09e9af43f739a6e1030091fc091e73edc447f25c68bf84b8df7aa8f091ab42662b93e02c27003afc7b0ca69efcfa60bd53d4d78ceb7c4d2c8fd5ed7e8b35024de849e06400ad145fdb28348d22b317ccec704c401f88db1af2a5348223f5cefd914e404c9d73805d0de77211881486f1bf4aadacadd3ae2588f0db7b5e6957fed50a374f541cfe5e4e923c82ec47e5b3d2c70ad6760c79cd5080b490bdc75f9ef5e1d17f0978b1e8770775f902b9463e6980e1683b2454751ba2dad4a2e6460924bd60ff49b03230cb11fcd04a0388e60874c35d3f6cfc4dd487665e1b16578751eaea89e126bf58044596e3188c7a9631017be1f2dcd7d612331832ff8755460dc496aa99a61ea053c78e72607a18213ff9ef4bb880903b91e9a43e0b1f0ed1511b2eca2f4253fcfbd7d0faebf3680fbf0a45df231544882c9c46505c726d56905d02fd046c1652d8fd06d15286a1a8f8b69fbd825ca421fd80f5e9ba1a23f924937ad049adeec60c78fea1adf9b1ef7e8ac4d1ded18f1a801b0bda8fe9a88098825ff3eef5c1fc68cbea143310b39543293f3f5fbcf4773b02054c0bc79f00554947c7604b36389c0c45f597a88f3713456b4cfd83b30cb6520b624aa09c812066a8cd542dc67e19e4c92b562b4e0f6799fe57d9d4f4f3e0b6fabff4b1fc190bf1e78775ebcbe3655d370ca6c08f48decf6153a4989eeab6921f8475f85197f51d651e563994257df57977e5f219b4879751de57ab0374b407a21adb4ba520bb35e7b7508675bf49f4e432190451423cbd529fc79b22baae9cb1d8660c3a49c456ac03bc06c0ef3b02f7d8acd40919315206fb38e715139c9bd6f89a58634fe683df03f5bda719764f6c38131bc5ba1c53244472ef73834ade04b86ca08dd753141ac0a9a230e246735060a044018bc9b75d50134b20e6219c13f8325b5a0201e9453f6f012fe72e829ee1c637fe30037a9212a31c6e713726a6cd4cf2dd66ffdba77f1e2800e717940f231d04aa2e4e88dea084754947d848c0271856bfe659922408449858a81fa6583f062d96898d18ec53664f0067eb9b9c40ad2579ba9802abd8d1bf287e49d94ae397e784db14b5f7010ee4fc42e6e3c8ba80370afc188fcecaf466ea830d7b16362e5c9329980b981decc7174f3ff70a35d8a180ee12ed0cbffd4e8d14eb503387e4959f702d4293109e922eb561371f9ab21475821f8555d92f0aa1c3d841a6f1eabd4e663993636c754ce2b3c3f6a6b6d0b161e777b8296d7dce7fd162970496494d4f60716244a5a7fb7cee40e1d565e6566697e8f9300000000000000000000000005101318212b >"$TMP/sig.hex" + expect_ok "tcId 1 must verify" "$BC_RUST" --hex-input mldsa65 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig.hex" <"$TMP/msg" >/dev/null + unhex f5408337d0fee65c28851226a5fa81b58464632c78e2a9bef70d330f2e3a5f74d9cf676aedd1067c91a5dd5d4edc46f868a93ffec9f44e254e44f682a153aeadf228e8db7c5fcfed30cc3408e261ab896876bee56660d2a7c1d7eac20c5754255206a178f7156295065ce7876f90c48f44bc37f3a00e32eefd3a4bb1e298fe283d106eaef92a33a594253a2a0790976a1d04636f8672d28c06c852ea8bb43b84bff512996e7616963d5b9a2906466a152c7ea9be178be35405683b44367af85d2daad87630c1e21ba5490154f0141780f5ed0407cb0b975dd56d5930f9b26413b843b83f3693304b0038bd3e4bb398868060ea18c9c67099376470a50deb052e4056743fbcdf0341b192663bd1c21ba3b3d5666e0d0e29c4e1ed0759ab0bd9d1d355011b94e0ff0c049b03ddb7138640667144fcacd7265f55a07e5387f1abd30c037cf14d436aa855f827049215440d8007f61460500d943f57ffb6bfee6fedd2fcec52882d7d8da1aab29e892c8beac3df3234b4a7d2eca3a45c6623c52bbdd07c1c94314b706988a52029f8f8b06e874b741d72926652c78c6ace2cfd8864eadb2e4b39cafe6e03e4edbafa2747db9bc42f92af8b031e3e380846b1bfd15ade88c285d6a6fffe91eafc8b17de6cbc68575f323cc09fc20e49e8efd76f9568bec486b78df4245428d8d0d5f53873e11de65fda4c770b521a8c67f5c51d48cc26358954514447881fd9a42e5891dac7e1db5249d7861b322111e5fb929bee9ff5e9d5a2667ba93e63fc03040d2e82648f89e89dec1d1d2dfb9efeceb7940f7dcbebeb5a239cc1c54d8f7d52cba220d0634e15df46a58280bc5a48840bd39274cfde150f9ad9a40f6398d715350925f0e0501944409f32331a362bdaaafb3d8ce71c964332d6afb7e684f99951246d88081c86744ae68133f22c53a4b5ae258f230a98491d2d43a79a6d0f4d54a3b62013965ac7c82d0507125a38a0277f81cbc1d46cef2a131c6f51b88ec0baae0c82a6a0e72831cb06f9116cff5111d597e01057d32805a008f52c9aec3311139bfb35982789ff83bdd0c31e9f1080e8ed8eb99fde66bafb29e3357389fe3785b60c78e229ef073e1b65e34d848bd4d8a4f251551e2d38d2546afbc205d3c6dab34d2b962b1afb44f1d22fc10c6744fcd6b636afd3cb414b16c2e0d708fe9f51ff19120bde693b028b6d1e6dbe37b4b8b3bc7c6f7a842701603869d3ded572500f085502efc8d3cc62b30e5cdbcb5e86d9c0d42973bf755df539cc0aea58f9148386db67bd2bf70cd12ccd96d5c66fb271416b772465228dc44b079178f9b766370b66a79b871faca246ca6f8f63be9f0668297ac446cad5cf4a83318b1b00ecbd283f0eecee60a9a37a27abdbdbe382e307970002837dfc0bd3934ebd008918fd4bd383c02c9d37f694996e989a49075767ebc4a2981ef5275455e026cb0bd70946cdd1fadaf251381d324f9efbb860d1b280c29685bab97d010676273b45cca12ac3966aae342c84e2357eccf252577743b8787967b40b07ef2d3d9e6c1a3bcb059cba0fdb7f0d4f815c242b8e14acd3375e608e9230ba3cf8718f43882a3e1e661a2bbe81830d34741f33473e263b3790abe67acf29f5df44865b2ffbc96975fd62738a64112deda5a2534fb0a23b3b3024df986391badf9041c593c313a7ca1e1fcffcb65b07b9a99337b4a4acf616cbe1553eb9541f38aa6247342905995233a28172ca13396b2a9662970120f82b92a213f43de7a232ccca3268265c9ce042d50915430a6c455f32277da42f9962fb9163b623231ebc080fa7b8e9f9021fcf85b98f9c483e4d2226b9326a5bcb2e7449ef029ae142d3a0f0c28bd4f7e9c51a12e1336f24dfacbc3f808a8f7dd683027bc948763b808fb0037394b8b41bc9b2ec7887e67584e03d11b15ca203b2bcb43f8881638c4e4eee7f846d09c7f89b7739df22b2c3acc235032ba8f7ae27b5b9d25733143e80a4cdde6770719c1e66ec2ce683612233e88fafff84c0745a98aa1254c8219c6c556348c2b5d1beeb61532d6bf7bde153271dc647460beb65fe0055b33fd6480dcbb9d7d471952cfa5be260c39721a8c5c89b9e966ae2dc9036451ec9f2c49433b2225e13f23e20c2bfba81a7b3a555883449238f7d48213e9f10ce19e76f1bdcfc73ee5524bd7d8be0a4b46784e238233c04fb99383ec7726f9717e1179dd14fba9ad6c2ebd1699f0ab0e57e6cad23875b029e89cfda06f51266ecd2eed4edafb51e82f2a506d57ba74da611774ca5fa2fff4a976519de425885e7d09219cf815b1767d4fc5a72c18918991a285086a6a766614a4d245387da50f28dd778fb33ab88c0918feba3768c55bb1f07aec33cfeed33d6faa4d34fd7227b365533c1e67dbc89f0b20195cf1cbd480d333ade1c9bb28308085b72ced430268c1492a27050c43668adc9cf8b8509447cfcd3c8f8d8eb554f704101786aa9ebca86991d250776a37a1f56fbf7d08e591f978da49c3870625879f70e2418aec5cba32fa8c346fa9038baebc35ad0068a4d03537aee14c2e71570a87490377fa8dd66f995aa044a522f0c7025a7ab2dd5ad30a64268dc112b7f9fa156df64d631f55f1d6edc55cec570a9c7372e29e02c8d4867bae249431dcf6ed2794a0183f0f7501201feca4a81d334c642fc8d38e9a90fa77429665e09e214797dfa455ff47c4f219d3a2cb0176bc2236455123c1c5da714ad29d580fb194f87173a18dc >"$TMP/pk.bin"; unhex 69da5aec6d5f58fbf29439c520bd68b966e3dd2ca633b68351c2862344713a1e9c086a44f9a870a3ccc14de62d6c12b278c354d7197c4d6d7f83d1422b29b250f5ee3fec118311d905e5db2b4b8b23b8d542202d6652f6dc3f9d7ed51f2463082d3f145cfd0fa7ac548a47e91c1ccb1a55b215e90ab355bfc6d67154287b1dfae0fb530264dbb841a7684b396e5ca0459d795216416a9d232bc89b32e0f9461f53107c78e66c8e876554e8ddd501867b55dcfc1fb33f102e03373cdd192640f1027a08ce277b468f6ed0fe80a9d6cd2d6b2f7a3738c8325d95b0ccc6e7b9fb000c923b92298e0867d4a9f6dd5513e8001033c633bb1641ee66349487224dd43386c7fcc29916332066a868100d46e2c5b8354c28f087a024cba27694afc4c1665e0d72b37686919ad55052cc63a144febe4e2a0c9ae416e064e289f9f69cbb883665d1130826b7b74e30c94a2b98b67b471663e3d66326db3b43bebf958e8665b68eda90e8c5d9494b0c7c9ec48800910dd6d906b1fcd47a0aac462ac87b126d21b5ba150df61f752257ddf5a063b4a5b150371d625535e3b2874b9fe548960ff67931cd6c12496e8213e2ace6fff48e6bdc60310e49389f62579db26b92ad73e9d3f23942cab51784f48b3660b6450caecbb0df2aa4c8e56577f5ea450d2f7f51aacc0b304a62250bf2cae7b99dcd955b6596625d06da1c67f730b706fdba630f00fd891830d251484640b7258ab364d6fd9986878fffa69b7c44b92e43143affae8b098e1d27716850f37553bf266cdfb561abbcdbfeb80752b364434e64b80429b54cc88693ce03dc0fa147f0741b215f0728499bdc25140aafc976ac99e910ba8a8a50d21b7bddaa28626b3b90a93fd44077068357c81d36e735eda4362930adead4951a0baa104f384fc70e842a9f329e1868b07b455e9cc3fecd54805c9052e70f88c3b92fe0fc6a4d7dda18cf5694e5398860e439a1e19d5a66f2fbc0aacdd1a498711bb16054796c015a715395ef6174e37b04eda589b673c4d5dda737817fb52f392caf7a72d7a3e84b2180cb5b75bc8af065bdc05c3e4040435a1b160081352ac43e09cbf2ead6e09c2b0be0e37894888fe2812f68806f957c13fce6ff167bcee21d4f412ec95a4847f3db7bf441223a4d4ca9ed69adb4de8a4b5b01c775f2721226e6c59ff26fc38e1bb78a384b30e7b55f082e264d8f25e31518619ddd6b6a9faf8aa6cdb5eab75ed59a33825d5ef8b93bde5d120ada773fcc0852b918f4f03e2d2a543b15363adb823eb1f6c533b98d940411e1f5c1cf521f9f63d5454697608326625fffe01bf87f44187dad631df2898effd2c291d98222e564abe3b042b75e90c9c54667842fa8ebb68a1244bf8e0c3ae3ee5f97d5ddeefd986c4bd3f99d877c2cc2381a89abdc61713d38cee58bf69805a485c288d21b15843147066b4a74c69dc25de878e21d35fdfe6746feb4c166606bf3219e42cf63581e7e6bd6570f40f8fae590cedf5106fe57037ccb2324b74fca6500f6ed3d0736cdcc67d04f8fa9e80054a5bd7c8459fc1abb1c4c78677d7f6b325af94a0e5c9c7db0a748e12c5265e8724947d9b5c4bab1a8b6faec827cc41ec115ef3c2d7348cddabddfbc8436f3b41765e13f3762b3b45ed23156f085831e726a55d4b83848b3d1d3352aab9edcc0ac2388f2383f6301ad813b917ee3f23734e057832ae4cf65e668c9ddd0bdd0f9d8b6693254649668aa91a1fa5eb7c59859bb6ddd36c25f4a2223f5d688b480d0388fa307ea69298f9bf7737f6b3dbfda87b331affd75cd8d88f0460e98ebc2890b217bd6d11000a3a088cd837f4f8859a43f76afaaab05a0c3007a149d4d6b9155cadc2c9b55003efdec5012b6272b87183694c505f0446ede55f35b8ab201f9eda974ff840eccb0f004fa3acf753acd0613f66e2a6ac82e322199d37b4af83cbb3d98371c31be79bb42331e819644cbad2ce27a04e4c517998692cd8331552892e199a01a6922bda4d38ac4c01f708809e529c3216eaab399ef25b350ea213ba47126f278140e17391ca7139bd13c56f415e6b74aed8dbfbf38c95dc6db366fd72aa863a27fa1ebf198716400b978a3709e35039731930406588ebdffd35fa230a9b75fce41d7acd214ca4f0029896c137495eade0cf4d10fe621c73f01061acb077de72177ff5dbc6f0c5bec681aa34668ca4fcdd727525068b0b0e9072971b84ef6ce11d5c3c6024da40966703dcc2b33ae04f677677635a55db508f34f1403cdbe37960c8577dac3d848b29f3b5c5c6c56fb74f34c8f4634c04b8cce9b218f1760ca00e6de87efd14087c633469c892bf3e319443336733bb60cfb44941bfa25229aa24384d812db90fe74e0f93fda005eea87400736cabc036f71421b6657b1674d4a8f76cbbf3a8b1c0af82f72973927752257c532db439d96762ad64f102551a9d03f9ce3d8cc850c393c128bf8054bb55bb92ea31ec0706f083a9cf90424c617f8ad2a21225d1913c30e8f47a6b7131304d536a85596ebfd987b64b6bf3c51638d6c839214b53c3c10aa52bd9c6eb77fcf80b5e3b724dec1381d0e02207a6adc73ff53d9d1ffcee1c4a28fa5445ce518eee937074ff7a402f5bbcb362ff090415f9dbd93b62ee56dc8c50e4d2e34c6c621650c0dffe311484e95d68de77170c909c815828946aeeec7ede56bcf433e22fc63a33f764ced1f9242f3d26dc7558686e471f30fbe9304d3d56af8b23e72a4088970b24b2f7e968c1d0392eeeb0b0f0ac8c176547a5383d948ed15484b79e21314a1f28ed624f61e5aaecf2269e5b027e1910ffddede52fad4e8da224e8a10b079548fa7cd44172f4991adfd7623d13e5a19c812824bcf990c07c9721ded9093be6ce7bc7da3ac8c932133a64396b822be92b088844991596df893625a4ef24543bf75a10d7d17ff70350ef62ce3a7758aebbf9b3977b08becb9ea28376082f607965f2cded28bbdb39dab7e00833b0488370d221742b66e27d9ee2d9dd07f401bc22a62c8a9d8d3a290c63804991496aafa47a32578f583cfb53d0c2199055973440d7535e0da6cb2957f4e04002ecea68f9c3ff76cade27ed15fd7835989d0abb197fe32f68636139a42710644bb25860ff33f539200e3ccb8a7738422ca0fa0c744b4c19d15c5d4a3cb082e20a78e20b5a4965b043595cbcacad500b5adbb6cd597e6a4b9c5ea6a1f2e653b5474da277f1818048094ac9e0e1e0b20068d1c1ce5a114a4db7195057a6ce4d221c336fdc29190fee8ff855cae8b7f7c02eec21f972c827066d9c6dcc4a4179bc44ea9b88abe5124bf78b071e09e9af43f739a6e1030091fc091e73edc447f25c68bf84b8df7aa8f091ab42662b93e02c27003afc7b0ca69efcfa60bd53d4d78ceb7c4d2c8fd5ed7e8b35024de849e06400ad145fdb28348d22b317ccec704c401f88db1af2a5348223f5cefd914e404c9d73805d0de77211881486f1bf4aadacadd3ae2588f0db7b5e6957fed50a374f541cfe5e4e923c82ec47e5b3d2c70ad6760c79cd5080b490bdc75f9ef5e1d17f0978b1e8770775f902b9463e6980e1683b2454751ba2dad4a2e6460924bd60ff49b03230cb11fcd04a0388e60874c35d3f6cfc4dd487665e1b16578751eaea89e126bf58044596e3188c7a9631017be1f2dcd7d612331832ff8755460dc496aa99a61ea053c78e72607a18213ff9ef4bb880903b91e9a43e0b1f0ed1511b2eca2f4253fcfbd7d0faebf3680fbf0a45df231544882c9c46505c726d56905d02fd046c1652d8fd06d15286a1a8f8b69fbd825ca421fd80f5e9ba1a23f924937ad049adeec60c78fea1adf9b1ef7e8ac4d1ded18f1a801b0bda8fe9a88098825ff3eef5c1fc68cbea143310b39543293f3f5fbcf4773b02054c0bc79f00554947c7604b36389c0c45f597a88f3713456b4cfd83b30cb6520b624aa09c812066a8cd542dc67e19e4c92b562b4e0f6799fe57d9d4f4f3e0b6fabff4b1fc190bf1e78775ebcbe3655d370ca6c08f48decf6153a4989eeab6921f8475f85197f51d651e563994257df57977e5f219b4879751de57ab0374b407a21adb4ba520bb35e7b7508675bf49f4e432190451423cbd529fc79b22baae9cb1d8660c3a49c456ac03bc06c0ef3b02f7d8acd40919315206fb38e715139c9bd6f89a58634fe683df03f5bda719764f6c38131bc5ba1c53244472ef73834ade04b86ca08dd753141ac0a9a230e246735060a044018bc9b75d50134b20e6219c13f8325b5a0201e9453f6f012fe72e829ee1c637fe30037a9212a31c6e713726a6cd4cf2dd66ffdba77f1e2800e717940f231d04aa2e4e88dea084754947d848c0271856bfe659922408449858a81fa6583f062d96898d18ec53664f0067eb9b9c40ad2579ba9802abd8d1bf287e49d94ae397e784db14b5f7010ee4fc42e6e3c8ba80370afc188fcecaf466ea830d7b16362e5c9329980b981decc7174f3ff70a35d8a180ee12ed0cbffd4e8d14eb503387e4959f702d4293109e922eb561371f9ab21475821f8555d92f0aa1c3d841a6f1eabd4e663993636c754ce2b3c3f6a6b6d0b161e777b8296d7dce7fd162970496494d4f60716244a5a7fb7cee40e1d565e6566697e8f9300000000000000000000000005101318212b >"$TMP/sig.bin" + expect_ok "tcId 1 must verify from binary files" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk.bin" --sigfile "$TMP/sig.bin" <"$TMP/msg" >/dev/null + printf '%s' 69da5aec6d5f58fbf29439c520bd68b966e3dd2ca633b68351c2862344713a1e9c086a44f9a870a3ccc14de62d6c12b278c354d7197c4d6d7f83d1422b29b250f5ee3fec118311d905e5db2b4b8b23b8d542202d6652f6dc3f9d7ed51f2463082d3f145cfd0fa7ac548a47e91c1ccb1a55b215e90ab355bfc6d67154287b1dfae0fb530264dbb841a7684b396e5ca0459d795216416a9d232bc89b32e0f9461f53107c78e66c8e876554e8ddd501867b55dcfc1fb33f102e03373cdd192640f1027a08ce277b468f6ed0fe80a9d6cd2d6b2f7a3738c8325d95b0ccc6e7b9fb000c923b92298e0867d4a9f6dd5513e8001033c633bb1641ee66349487224dd43386c7fcc29916332066a868100d46e2c5b8354c28f087a024cba27694afc4c1665e0d72b37686919ad55052cc63a144febe4e2a0c9ae416e064e289f9f69cbb883665d1130826b7b74e30c94a2b98b67b471663e3d66326db3b43bebf958e8665b68eda90e8c5d9494b0c7c9ec48800910dd6d906b1fcd47a0aac462ac87b126d21b5ba150df61f752257ddf5a063b4a5b150371d625535e3b2874b9fe548960ff67931cd6c12496e8213e2ace6fff48e6bdc60310e49389f62579db26b92ad73e9d3f23942cab51784f48b3660b6450caecbb0df2aa4c8e56577f5ea450d2f7f51aacc0b304a62250bf2cae7b99dcd955b6596625d06da1c67f730b706fdba630f00fd891830d251484640b7258ab364d6fd9986878fffa69b7c44b92e43143affae8b098e1d27716850f37553bf266cdfb561abbcdbfeb80752b364434e64b80429b54cc88693ce03dc0fa147f0741b215f0728499bdc25140aafc976ac99e910ba8a8a50d21b7bddaa28626b3b90a93fd44077068357c81d36e735eda4362930adead4951a0baa104f384fc70e842a9f329e1868b07b455e9cc3fecd54805c9052e70f88c3b92fe0fc6a4d7dda18cf5694e5398860e439a1e19d5a66f2fbc0aacdd1a498711bb16054796c015a715395ef6174e37b04eda589b673c4d5dda737817fb52f392caf7a72d7a3e84b2180cb5b75bc8af065bdc05c3e4040435a1b160081352ac43e09cbf2ead6e09c2b0be0e37894888fe2812f68806f957c13fce6ff167bcee21d4f412ec95a4847f3db7bf441223a4d4ca9ed69adb4de8a4b5b01c775f2721226e6c59ff26fc38e1bb78a384b30e7b55f082e264d8f25e31518619ddd6b6a9faf8aa6cdb5eab75ed59a33825d5ef8b93bde5d120ada773fcc0852b918f4f03e2d2a543b15363adb823eb1f6c533b98d940411e1f5c1cf521f9f63d5454697608326625fffe01bf87f44187dad631df2898effd2c291d98222e564abe3b042b75e90c9c54667842fa8ebb68a1244bf8e0c3ae3ee5f97d5ddeefd986c4bd3f99d877c2cc2381a89abdc61713d38cee58bf69805a485c288d21b15843147066b4a74c69dc25de878e21d35fdfe6746feb4c166606bf3219e42cf63581e7e6bd6570f40f8fae590cedf5106fe57037ccb2324b74fca6500f6ed3d0736cdcc67d04f8fa9e80054a5bd7c8459fc1abb1c4c78677d7f6b325af94a0e5c9c7db0a748e12c5265e8724947d9b5c4bab1a8b6faec827cc41ec115ef3c2d7348cddabddfbc8436f3b41765e13f3762b3b45ed23156f085831e726a55d4b83848b3d1d3352aab9edcc0ac2388f2383f6301ad813b917ee3f23734e057832ae4cf65e668c9ddd0bdd0f9d8b6693254649668aa91a1fa5eb7c59859bb6ddd36c25f4a2223f5d688b480d0388fa307ea69298f9bf7737f6b3dbfda87b331affd75cd8d88f0460e98ebc2890b217bd6d11000a3a088cd837f4f8859a43f76afaaab05a0c3007a149d4d6b9155cadc2c9b55003efdec5012b6272b87183694c505f0446ede55f35b8ab201f9eda974ff840eccb0f004fa3acf753acd0613f66e2a6ac82e322199d37b4af83cbb3d98371c31be79bb42331e819644cbad2ce27a04e4c517998692cd8331552892e199a01a6922bda4d38ac4c01f708809e529c3216eaab399ef25b350ea213ba47126f278140e17391ca7139bd13c56f415e6b74aed8dbfbf38c95dc6db366fd72aa863a27fa1ebf198716400b978a3709e35039731930406588ebdffd35fa230a9b75fce41d7acd214ca4f0029896c137495eade0cf4d10fe621c73f01061acb077de72177ff5dbc6f0c5bec681aa34668ca4fcdd727525068b0b0e9072971b84ef6ce11d5c3c6024da40966703dcc2b33ae04f677677635a55db508f34f1403cdbe37960c8577dac3d848b29f3b5c5c6c56fb74f34c8f4634c04b8cce9b218f1760ca00e6de87efd14087c633469c892bf3e319443336733bb60cfb44941bfa25229aa24384d812db90fe74e0f93fda005eea87400736cabc036f71421b6657b1674d4a8f76cbbf3a8b1c0af82f72973927752257c532db439d96762ad64f102551a9d03f9ce3d8cc850c393c128bf8054bb55bb92ea31ec0706f083a9cf90424c617f8ad2a21225d1913c30e8f47a6b7131304d536a85596ebfd987b64b6bf3c51638d6c839214b53c3c10aa52bd9c6eb77fcf80b5e3b724dec1381d0e02207a6adc73ff53d9d1ffcee1c4a28fa5445ce518eee937074ff7a402f5bbcb362ff090415f9dbd93b62ee56dc8c50e4d2e34c6c621650c0dffe311484e95d68de77170c909c815828946aeeec7ede56bcf433e22fc63a33f764ced1f9242f3d26dc7558686e471f30fbe9304d3d56af8b23e72a4088970b24b2f7e968c1d0392eeeb0b0f0ac8c176547a5383d948ed15484b79e21314a1f28ed624f61e5aaecf2269e5b027e1910ffddede52fad4e8da224e8a10b079548fa7cd44172f4991adfd7623d13e5a19c812824bcf990c07c9721ded9093be6ce7bc7da3ac8c932133a64396b822be92b088844991596df893625a4ef24543bf75a10d7d17ff70350ef62ce3a7758aebbf9b3977b08becb9ea28376082f607965f2cded28bbdb39dab7e00833b0488370d221742b66e27d9ee2d9dd07f401bc22a62c8a9d8d3a290c63804991496aafa47a32578f583cfb53d0c2199055973440d7535e0da6cb2957f4e04002ecea68f9c3ff76cade27ed15fd7835989d0abb197fe32f68636139a42710644bb25860ff33f539200e3ccb8a7738422ca0fa0c744b4c19d15c5d4a3cb082e20a78e20b5a4965b043595cbcacad500b5adbb6cd597e6a4b9c5ea6a1f2e653b5474da277f1818048094ac9e0e1e0b20068d1c1ce5a114a4db7195057a6ce4d221c336fdc29190fee8ff855cae8b7f7c02eec21f972c827066d9c6dcc4a4179bc44ea9b88abe5124bf78b071e09e9af43f739a6e1030091fc091e73edc447f25c68bf84b8df7aa8f091ab42662b93e02c27003afc7b0ca69efcfa60bd53d4d78ceb7c4d2c8fd5ed7e8b35024de849e06400ad145fdb28348d22b317ccec704c401f88db1af2a5348223f5cefd914e404c9d73805d0de77211881486f1bf4aadacadd3ae2588f0db7b5e6957fed50a374f541cfe5e4e923c82ec47e5b3d2c70ad6760c79cd5080b490bdc75f9ef5e1d17f0978b1e8770775f902b9463e6980e1683b2454751ba2dad4a2e6460924bd60ff49b03230cb11fcd04a0388e60874c35d3f6cfc4dd487665e1b16578751eaea89e126bf58044596e3188c7a9631017be1f2dcd7d612331832ff8755460dc496aa99a61ea053c78e72607a18213ff9ef4bb880903b91e9a43e0b1f0ed1511b2eca2f4253fcfbd7d0faebf3680fbf0a45df231544882c9c46505c726d56905d02fd046c1652d8fd06d15286a1a8f8b69fbd825ca421fd80f5e9ba1a23f924937ad049adeec60c78fea1adf9b1ef7e8ac4d1ded18f1a801b0bda8fe9a88098825ff3eef5c1fc68cbea143310b39543293f3f5fbcf4773b02054c0bc79f00554947c7604b36389c0c45f597a88f3713456b4cfd83b30cb6520b624aa09c812066a8cd542dc67e19e4c92b562b4e0f6799fe57d9d4f4f3e0b6fabff4b1fc190bf1e78775ebcbe3655d370ca6c08f48decf6153a4989eeab6921f8475f85197f51d651e563994257df57977e5f219b4879751de57ab0374b407a21adb4ba520bb35e7b7508675bf49f4e432190451423cbd529fc79b22baae9cb1d8660c3a49c456ac03bc06c0ef3b02f7d8acd40919315206fb38e715139c9bd6f89a58634fe683df03f5bda719764f6c38131bc5ba1c53244472ef73834ade04b86ca08dd753141ac0a9a230e246735060a044018bc9b75d50134b20e6219c13f8325b5a0201e9453f6f012fe72e829ee1c637fe30037a9212a31c6e713726a6cd4cf2dd66ffdba77f1e2800e717940f231d04aa2e4e88dea084754947d848c0271856bfe659922408449858a81fa6583f062d96898d18ec53664f0067eb9b9c40ad2579ba9802abd8d1bf287e49d94ae397e784db14b5f7010ee4fc42e6e3c8ba80370afc188fcecaf466ea830d7b16362e5c9329980b981decc7174f3ff70a35d8a180ee12ed0cbffd4e8d14eb503387e4959f702d4293109e922eb561371f9ab21475821f8555d92f0aa1c3d841a6f1eabd4e663993636c754ce2b3c3f6a6b6d0b161e777b8296d7dce7fd162970496494d4f60716244a5a7fb7cee40e1d565e6566697e8f930000000000000000000000000510131821 >"$TMP/sig-bad.hex" + expect_fail "tcId 6 must not verify" "$BC_RUST" --hex-input mldsa65 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig-bad.hex" <"$TMP/msg" + assert_stderr_has "invalid" + expect_fail "another message must not verify" "$BC_RUST" --hex-input mldsa65 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig.hex" < <(printf 'Hello World') +} + +# Signing is randomized (FIPS 204 Alg 2 draws rnd), so there is no signature known answer; the +# round trip pins the wiring: sign with the sk, verify with the derived pk, with and without a +# context, in the pure and the HashML-DSA (SHA-512) forms, and each form rejects the other's +# signature and the other context. +test_mldsa65_sign_verify_round_trip() { + "$BC_RUST" mldsa65 keygen >"$TMP/sk" + assert_size "$TMP/sk" 4032 "sk length of ML-DSA-65" + "$BC_RUST" mldsa65 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + rng 3000 >"$TMP/msg" + "$BC_RUST" mldsa65 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig" + assert_size "$TMP/sig" 3309 "signature length of ML-DSA-65" + expect_ok "pure signature verifies" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" >/dev/null + printf 'ctx' >"$TMP/ctx" + expect_fail "a context the signer did not use must not verify" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" --ctxfile "$TMP/ctx" <"$TMP/msg" + "$BC_RUST" mldsa65 sign --skfile "$TMP/sk" --ctxfile "$TMP/ctx" <"$TMP/msg" >"$TMP/sig-ctx" + expect_ok "signature with context verifies with it" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ctx" --ctxfile "$TMP/ctx" <"$TMP/msg" >/dev/null + expect_fail "and not without it" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ctx" <"$TMP/msg" + "$BC_RUST" hash-mldsa65 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig-ph" + expect_ok "HashML-DSA signature verifies as HashML-DSA" "$BC_RUST" hash-mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ph" <"$TMP/msg" >/dev/null + expect_fail "and not as pure ML-DSA" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ph" <"$TMP/msg" + expect_fail "nor the pure one as HashML-DSA" "$BC_RUST" hash-mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" +} + +# ACVP ML-DSA keyGen (bc-test-data/pqc/crypto/mldsa/ML-DSA-keyGen.txt, ML-DSA-87 tcId 1): the 32-byte +# seed xi gives the vector's sk (`keygen-from-seed`) and pk (`pk-from-sk`), from raw and hex seeds +# (hex needs --hex-input). +test_mldsa87_keygen_from_seed_matches_acvp() { + local got + unhex D67F9DB963E93302D0B7B996FDA537D31B1E7885F00C2675FE01E6F15595B4A6 >"$TMP/seed" + got=$(hex_out "$BC_RUST" mldsa87 keygen-from-seed -x <"$TMP/seed") + assert_eq "$got" "b09a7f9d1c3754bbc1470d2b91628d660630144bd75e7614bd210e4e9f1e14cb66a75a14a2a7734b671939b9fc5da5e8f3badf60b2ef9aea78c9c262959344f780455f7f719fbcdde674b0d50446651a9766c0f2e5cd3db90758cf7c58807ec47e0d3b95b9e8fcfadfc1b8d56543b5f7184d9148f32ad8c12391827a54df4c300ca9905a4469c0c4904092259b00068c162801120221364883340858b46c00a04de4227054a0641c090d0229690c116554466491140e5b184c11c84083c280e3a4711a262121312d18924c2334680bb78198342020a59101047041308c5aa06c10402d18854d01224a1c114848a465e0c42c031949a08204891632520488913201034610a03649929831e0824154424603318c5c0292489244c3886d2313204106609c404a1c4628e4168e62969094889104102d24c6440ab14de2246e5ca66810042d5a4840c1c0311a826d4cc84dc43645010868a208861323456220460a26051431081405852407425ca86801a2680a04501202246316120c3680d3b624d2885160b688d2c4691841650015710cc50d63062d1c162913c630d8224941a2410892259cb471cb428859824120a50154b001a3400ac912660b972ddba0910c438e21426024061254244692b26d9b084d43b809d33288e3a085a0b00c18b2089ab804e0b46003306a8b24688286710948899b147103300d91947020b40511b12d9a0048102004d2343184045151104ad9b0809ac80090c8240b078501050d8a4442822669c2a02904a28454880008396854b84122432419360a22028a04c980e2a42c12314a92326ce186210b344e04498523b26802c74014206661266920982083322c9c4882c0c88d13144152c84c0b32305a146a0191050a2782a2a66121448010a86501976918342610330e53000e214629192948241422da120554340109428018a40849c444e2466183a450d2266013436880402543b4856006081381114ca48ddb02842211406108644c48119c34921b480e03142999249100939153b20d41a2298104851a346422170a20b3015236505a106012a481e104702413505c82294b1465cbc4495410880ab70598922404c588203044cb0006c2002d233951023040811446c3c02900112a54980824066144468c1913020316121ba24c22b28411c730e30005491850501680cb366249926dd4284a51366cd120881346481b82254b98250ab909c3205063184988268251b00840160613478819c4041b156282c66191227192b2654a364a0a372593a245184066cc8481cc0609d8260a1920501b05900b050a9b3650222612cbb6649022814086418ca63120421218c68c13090c89144e4a4421e03072110645989604d0848521a0081b26115c827142848c48c6252443842431310c9910d144814b369162b805e0480a9224451aa38482b8109b845062c65193266c8ab685228284e2900141844cdc002c943092249889922802204101e2c0605bc48021100c5ab09101186463086523299263c86190b620098964d4b00c03097119869110276623310960c44c18b0215c3208c3a270110411a2b049492066424661d4008dc432681b9300a3b800419225da448d111169caa84d4a864088462d034872a2386a00b929931842198380524865cc460418c96849086ad33226c9c28c0111319912329886905a8604db00280a202d08998999106a58b60453a869cb9208c2c00549962509c86c93b42112202211b97189369208100254c049912611a40002c414621cb671189428c3066063124d0a1729134212d810811245440c160a62363001c6014ab8440b02720ba22c09b4219a244e820026984862193105db98899c406421484e8246660032642419492193618834611b0650c4246c19b208d828848ac2285b1682c2002604b68c2183449c169110088988105221388e0b41068cc25081a04004a38591902d11420d02a00883c86d044742134485cc346a44244e21c19061222193469162c00d8c265109878993100101988460c06921118101044d2248004cc0809310609908691a079022c665a0404423152c8c166924382191b850d2946500396d8280001b4981203020c336062029308c06918412225b206e5ab64422919164b845d8060e09228c9b806ddb30905b142522370d094904c9080662a891d9100060a885ca022e0cb471088628039491e3ee3d00690918fbe73f35a5eb0f08f223c6ec9e9b4d3cf388c99910caa16d496f16913bb024f0b437ddd80c16fd412fcff3c240a4148e5092b6ff9f8694d5d0eeabc8be4158f161bfb4d6ebee8b1a5290d4e811c1eb77766c08dcbac587f8724d65ad856a12daed257365e54dccacae742ebe4e963d493dde59aa5e674392ca6e899bd40f983fdb8edb53c02dfa84d81563f1756cb495932c516b189748089d021933989bbee17d964474c5235bb25940d0fc48e15f0facf4cf0257b1bb3764f0079ec0052e45ac10bb43b6cd413c30710a8d4c252387394d6e43bb37802658c23d8cd8e6f5ff5050e14b7865f9f1b3a236bb8ee5554bd47ded627bf0e747239cf27cc2f20958948392f8b0ee0de766784570b50c869afe4566728d2fe448f8d82e135821149e03993891e8253e603eec9ebc8b797183bceab097a5e31592ad9f3ce82b868f1b77a644a6c41c52a23fefbff968f29fa8314026573dcafc094a21f42447593b8950634ee52549d0b88af1f6385416d94cde1c54f30e4161a8902fb6becf55a6052d9b3f11ef79f0ad4452b6d7f888ff30bf9614a6309eae55615952af39f12078576ebc58933f32ca52aa6910f0873d05db7772e24a7565ec47e1d9475af913d9fbe33e2f3cd93a9e75f73f191c56f4e45780a4d4e5e513706f628bac1ae1d9d538872d712c359036181b69760a5fc1cff7d545e48aa6e96f4b89ae84708887f06ce09a78b83766184c2113de2694d24707adfe5cd0463699a4e8a0570083dc4f865f2243fe82bbb066ce024fb54124f2bf73e87a0144e43b3d0fc69e777b1ee8b969bb5321a0cb5de6487c1c4795ed8b52b2d70a7a1be574fadd2c97c31d48287a87f23a2cde106bdd17dff3b09a47cc3a2c0c8f380b175caf874bbda6a88f81742a3f6114acf882dd4df7fd149f063c89dc9429b3bbe92c5ac12747967afcf38f8a744141f5374c87b81c5f3cf5fdc8db5dbd2a0762f5df49fcf3d8b0b5fd4d810c923e29abf9a2015e25005236288dd00017184706e26eb72f16358a39e0ea5d73152c136ad9b43c411e79d34ad1f6ddac5ba00768c80d6f6f960d9edb1e79c2aaef8eb3eb957f80890792acf8b28d10b6f11f9a3b033e565ceeb989b6bb237bd79d032100dfebe20043bbbb4bd5a921a7bd51f1584444a17daf2dc36d377fbcb124b24baf1e792809c75d11a792a05c8d0f38ce1842b6a1cd18c99176d88eaab8669f34378304898f0cbc86c8955815676c619eb493764aeff4f4c96364a3d6f48cc3998fa9d0f9efa256080dfec909f570b871a2c65c9d90e7e67838a90036737a37b694cbd9bc0b4586b31ffc84cbe267fd574c4d655053eb87aa25b2d6cd3c173ad5de2303334e452f3065637ffb4ace7482745af9c62995c16c3b1f140b2a4af6c5e1d7a2d916b0c8dcb5f4fd2dcfaf51cc6252213b81e27b7a2c1eff21dba40bdf4c70f15560919e5db420ca3ea590f70729b7f1d59a880bb611c712112a92ba62fad5b392713aa8b7d28f265a458b234b84e4434405a5bc14aaa7e63a763a76bf75cfc4c105705de943541d99361d892eb049ca86f654195643a3b9fca77390ccca113668b2879a979fe0580d15a81a34d9983df92d923abbf8da71732127c0c3ab32b65c539dc8298bef3de901502c457a57cb3611a2f1298feb7219af1a4f465962e1e650547a9bab0b64956fefd71f28f87b907d14f7422134632326f79cfefafa877ae103d0e34754ad63cb4749e5bf7dad7fc4f05d924931ab2ec0d498fa1485d74c6d679d4f0d9ae5aad3077a841a2b8415d2fed5a3e831ee3c0d216ee867a2e4cb81584c3ea9d77b3b5fa30cfa19ab53369defae44b0267a8e0210d796698463e2a0ad06c5cbf9d197f3a954aaa96c4c41a7300d493cc617e7500d1c9f635fbb9a94f96c276aa418fd860f6b99a3e14114d051eac3ca51cd3509ca0ce8346d156ec0973e1d75d2ab7988f9d1e0d1329a4fb3841325849914354a24a589cb3be1acf54909bdf6306a292c553410b9fda00ba1f1b07f0979973ab0ce7cc8e68def5432233717f1be411aed5c06ff3b98f5888e31cde80dcc52ab038053fb50bb5bb244e56346324031a62c1cb492825fd1a0e3f293629eefe711656af2a283d693f54e30bf2e1afe4ae21aa1a439f5b2277ad49d28ba85b81ad0d86b8e6d62625f528818f2ce150160675fd39fc24f768c9fa6069d4a0d2b3893e0fa70c762a16e290553d4f9b094c7dc023a0e7c21345f80f60800eb5ee6d28d37f8bf4ad1f3a689092da39221c1fb539d5e98c146a29edadaa3454b9ff93e8a0dd7a3398315d6ee2899d10c870a5c4d1708d360f2944e7121703c8e2ea9ef54d73bc62019752fec5f2ccd38b203d625bbdd6b92d51540f9c55af9ec38f7a70e7de47fbbcca3788e07e4e64acf7806ad07454e57617965c2473258f9997387c20740ee28dc137f8283822d0efbe8b268a1fd170f095359d7814a51047344450cf58dc544e4f0b46497a847760819b345da05543ee76798cfb5b42e0f46510cf46f6bed7c089fb8816d663c38a4c27f29bc5f7cc210d0d49deae702c0cdce70d42360d78c18fd1cf065e50b6a0755a11b77c7e4d5ee62c2051d0155881939045b0fc33b0c46b5149054ebdd9da202d4cc7797aa43743f56746569fc1e7599a2ba18ba7352b2acef47ba3a2b8c7e1d1f6885cd290495888eedf290a699719b89d1535032bc1de33c60dbdc6ac0f401af899b92fcdf8748f19d902b191d52a253c46c15a47f075fef228561998b18e0f7f4a4e527c75509c59872d0b10022349ed5a6b113c9cdd75df0b5589761ff379b509eddf690f86afaca4d9fe85c44a0c8195b14b0d4b9c4d4bae2d59ab6c740d15b730b6290801ff66a5e6ee6d4c7356e0bc6e04987b2f833d25f4473b1cf663534a9023374b759afe1feaae799714c5cf94269c00634bec34f782be0d36039cb646decbca9542913817dcb559e7078f959d59a979d4318b88e05903aa9aef43e43e0a017616e097f3e026ff8bcbafca129b8efbd6806dd13c131bb3177e30851597806801ef16e2e58a427a7848f7627259a177c698cc7425d9b13964c3ec85c665fefd64b4b9098fee118d924c97bd9285ed52d3233d5510099bcf61e3558fb39260596cdeb3b01e0482f84d130d804d5e67622546bfd1c11548f814ecd61cb67507efa817afc86ff1249e910a065916658fc114fd513b9359edf8b33498f80584e880a827f2ebc979d36a82cc9789446f72ce491066f4abd4b03ee386d8eb813fe6eb606a1249ca6a592f12e7848fefb30294062926d953040c860fa851dac36ca8765082ba93e31eb1e2007df83252c8d062171a5e262a01297bd65d4ea3055721bd8afabc3ec1256df3b5149a426c5ce63866ce11f577b3faee2b3395d75ffd60025e34575b081c86989e9ec2789f7d8ec1044cae9599ee6b502edcdd6c00abaabd06b20aaea10ed998b8b80c65a6df8cd3a773ea80fc17093932d83bf38a8368a3917f8c2c9eb193f6f9587eb96bf69ed3384f107d7b28343b9f3705de33404c3770e40db07753ae554b764903a1a07319ad6d50c3bad320fff887bf902bd598b8574ce9428ccf5586e2954ac6bbaf2536211934737e5eaf0a1012903a543c024f34b58e4706cc55bf9687516bc96296a4e6adff507a0f0c72365796af72cadf785fc02fa0fa819f727953b29cf9bee2514217cf21f7fa15140cd7c4b519193f94c0d8efb753c6d76e1ade6ebfd71b5e521a6d6710b2f43035dc6000bafc71645f7f8a7da0bf3ff397a8098d2b924f36a046cb4d515387f57db5e32792df9e8a3488aac6e3960f1c9ea2cb16322e6bc94e44b13925671fe04e3a76729447ff18613feabf046b104e8f6b443822e92267e29a2b0ba7f6d415921c83137ab959681ed1aa6d3bcac9f53105a390b37482f1bb683d861757ab9cf4cfa8330809a5921a2167d7e97c877791ae0370e2dc2bdc3022cbcb35c2dcfd07088d91cabf15823bd057b8ac39b9bafeb88e8c17ac3a269855535a1e8d66b06b5497b8f6fbe125216c2c4ac4cece3baa85b3d8cc50a397195383cdde30580c0ec1b5098627e2af315a0dfe27b785a7579251b2f3c4b1dec5b6128c816482d0486b6af460e54fa38e50f5beabd3bfc9cdb2537a85bdcfef30886ce42a41c5f1416b656236cd52879e5f3a662dbe395f2d4fb9a70f0cb0a0682ac6dd03c8982233b15a2de1cd1ad4c29f1d2c60d4b50296ccd6d65516dc65fdd4e760f6c3825c3656aab20ec67a3bee64a0d05ed26b2d1212fde1e1ce421dd64e95740a7b12472332e47d468cfc0674d25cdebeb89902559ca82e07f2030240ac87ad65cc6a120f2e2cb6c6e091e17403b41f285ba82addbed90cec987e1e9091331330502429fb195c946925b5ae9f21b55612d5f6334945062dd51e1c2a16a17d09a1abd967008a26b6979c3b4afd87c2eafa259cf809ef86465f0717343280a0e8b0e48e756a10ec1aabb558e31b4baa40618c32d08797059d33d67a91d662d708e79b099d986531199195885615b8bc6a6ce331e7d96c31178ce2a0c8f5a6761a71dae83c4abf70ec5c4480c0cb9699df4031578546561397aed8e7d5c3d8339b0e59ec1ebcf75ab1b6d74c27ae6fa173aac297e3e6af4d52d9eea650d492b90885f75bf4aa82c3d747d4392ecab1f88f50100" "sk from a raw seed" + got=$(printf '%s' D67F9DB963E93302D0B7B996FDA537D31B1E7885F00C2675FE01E6F15595B4A6 | hex_out "$BC_RUST" --hex-input mldsa87 keygen-from-seed -x) + assert_eq "$got" "b09a7f9d1c3754bbc1470d2b91628d660630144bd75e7614bd210e4e9f1e14cb66a75a14a2a7734b671939b9fc5da5e8f3badf60b2ef9aea78c9c262959344f780455f7f719fbcdde674b0d50446651a9766c0f2e5cd3db90758cf7c58807ec47e0d3b95b9e8fcfadfc1b8d56543b5f7184d9148f32ad8c12391827a54df4c300ca9905a4469c0c4904092259b00068c162801120221364883340858b46c00a04de4227054a0641c090d0229690c116554466491140e5b184c11c84083c280e3a4711a262121312d18924c2334680bb78198342020a59101047041308c5aa06c10402d18854d01224a1c114848a465e0c42c031949a08204891632520488913201034610a03649929831e0824154424603318c5c0292489244c3886d2313204106609c404a1c4628e4168e62969094889104102d24c6440ab14de2246e5ca66810042d5a4840c1c0311a826d4cc84dc43645010868a208861323456220460a26051431081405852407425ca86801a2680a04501202246316120c3680d3b624d2885160b688d2c4691841650015710cc50d63062d1c162913c630d8224941a2410892259cb471cb428859824120a50154b001a3400ac912660b972ddba0910c438e21426024061254244692b26d9b084d43b809d33288e3a085a0b00c18b2089ab804e0b46003306a8b24688286710948899b147103300d91947020b40511b12d9a0048102004d2343184045151104ad9b0809ac80090c8240b078501050d8a4442822669c2a02904a28454880008396854b84122432419360a22028a04c980e2a42c12314a92326ce186210b344e04498523b26802c74014206661266920982083322c9c4882c0c88d13144152c84c0b32305a146a0191050a2782a2a66121448010a86501976918342610330e53000e214629192948241422da120554340109428018a40849c444e2466183a450d2266013436880402543b4856006081381114ca48ddb02842211406108644c48119c34921b480e03142999249100939153b20d41a2298104851a346422170a20b3015236505a106012a481e104702413505c82294b1465cbc4495410880ab70598922404c588203044cb0006c2002d233951023040811446c3c02900112a54980824066144468c1913020316121ba24c22b28411c730e30005491850501680cb366249926dd4284a51366cd120881346481b82254b98250ab909c3205063184988268251b00840160613478819c4041b156282c66191227192b2654a364a0a372593a245184066cc8481cc0609d8260a1920501b05900b050a9b3650222612cbb6649022814086418ca63120421218c68c13090c89144e4a4421e03072110645989604d0848521a0081b26115c827142848c48c6252443842431310c9910d144814b369162b805e0480a9224451aa38482b8109b845062c65193266c8ab685228284e2900141844cdc002c943092249889922802204101e2c0605bc48021100c5ab09101186463086523299263c86190b620098964d4b00c03097119869110276623310960c44c18b0215c3208c3a270110411a2b049492066424661d4008dc432681b9300a3b800419225da448d111169caa84d4a864088462d034872a2386a00b929931842198380524865cc460418c96849086ad33226c9c28c0111319912329886905a8604db00280a202d08998999106a58b60453a869cb9208c2c00549962509c86c93b42112202211b97189369208100254c049912611a40002c414621cb671189428c3066063124d0a1729134212d810811245440c160a62363001c6014ab8440b02720ba22c09b4219a244e820026984862193105db98899c406421484e8246660032642419492193618834611b0650c4246c19b208d828848ac2285b1682c2002604b68c2183449c169110088988105221388e0b41068cc25081a04004a38591902d11420d02a00883c86d044742134485cc346a44244e21c19061222193469162c00d8c265109878993100101988460c06921118101044d2248004cc0809310609908691a079022c665a0404423152c8c166924382191b850d2946500396d8280001b4981203020c336062029308c06918412225b206e5ab64422919164b845d8060e09228c9b806ddb30905b142522370d094904c9080662a891d9100060a885ca022e0cb471088628039491e3ee3d00690918fbe73f35a5eb0f08f223c6ec9e9b4d3cf388c99910caa16d496f16913bb024f0b437ddd80c16fd412fcff3c240a4148e5092b6ff9f8694d5d0eeabc8be4158f161bfb4d6ebee8b1a5290d4e811c1eb77766c08dcbac587f8724d65ad856a12daed257365e54dccacae742ebe4e963d493dde59aa5e674392ca6e899bd40f983fdb8edb53c02dfa84d81563f1756cb495932c516b189748089d021933989bbee17d964474c5235bb25940d0fc48e15f0facf4cf0257b1bb3764f0079ec0052e45ac10bb43b6cd413c30710a8d4c252387394d6e43bb37802658c23d8cd8e6f5ff5050e14b7865f9f1b3a236bb8ee5554bd47ded627bf0e747239cf27cc2f20958948392f8b0ee0de766784570b50c869afe4566728d2fe448f8d82e135821149e03993891e8253e603eec9ebc8b797183bceab097a5e31592ad9f3ce82b868f1b77a644a6c41c52a23fefbff968f29fa8314026573dcafc094a21f42447593b8950634ee52549d0b88af1f6385416d94cde1c54f30e4161a8902fb6becf55a6052d9b3f11ef79f0ad4452b6d7f888ff30bf9614a6309eae55615952af39f12078576ebc58933f32ca52aa6910f0873d05db7772e24a7565ec47e1d9475af913d9fbe33e2f3cd93a9e75f73f191c56f4e45780a4d4e5e513706f628bac1ae1d9d538872d712c359036181b69760a5fc1cff7d545e48aa6e96f4b89ae84708887f06ce09a78b83766184c2113de2694d24707adfe5cd0463699a4e8a0570083dc4f865f2243fe82bbb066ce024fb54124f2bf73e87a0144e43b3d0fc69e777b1ee8b969bb5321a0cb5de6487c1c4795ed8b52b2d70a7a1be574fadd2c97c31d48287a87f23a2cde106bdd17dff3b09a47cc3a2c0c8f380b175caf874bbda6a88f81742a3f6114acf882dd4df7fd149f063c89dc9429b3bbe92c5ac12747967afcf38f8a744141f5374c87b81c5f3cf5fdc8db5dbd2a0762f5df49fcf3d8b0b5fd4d810c923e29abf9a2015e25005236288dd00017184706e26eb72f16358a39e0ea5d73152c136ad9b43c411e79d34ad1f6ddac5ba00768c80d6f6f960d9edb1e79c2aaef8eb3eb957f80890792acf8b28d10b6f11f9a3b033e565ceeb989b6bb237bd79d032100dfebe20043bbbb4bd5a921a7bd51f1584444a17daf2dc36d377fbcb124b24baf1e792809c75d11a792a05c8d0f38ce1842b6a1cd18c99176d88eaab8669f34378304898f0cbc86c8955815676c619eb493764aeff4f4c96364a3d6f48cc3998fa9d0f9efa256080dfec909f570b871a2c65c9d90e7e67838a90036737a37b694cbd9bc0b4586b31ffc84cbe267fd574c4d655053eb87aa25b2d6cd3c173ad5de2303334e452f3065637ffb4ace7482745af9c62995c16c3b1f140b2a4af6c5e1d7a2d916b0c8dcb5f4fd2dcfaf51cc6252213b81e27b7a2c1eff21dba40bdf4c70f15560919e5db420ca3ea590f70729b7f1d59a880bb611c712112a92ba62fad5b392713aa8b7d28f265a458b234b84e4434405a5bc14aaa7e63a763a76bf75cfc4c105705de943541d99361d892eb049ca86f654195643a3b9fca77390ccca113668b2879a979fe0580d15a81a34d9983df92d923abbf8da71732127c0c3ab32b65c539dc8298bef3de901502c457a57cb3611a2f1298feb7219af1a4f465962e1e650547a9bab0b64956fefd71f28f87b907d14f7422134632326f79cfefafa877ae103d0e34754ad63cb4749e5bf7dad7fc4f05d924931ab2ec0d498fa1485d74c6d679d4f0d9ae5aad3077a841a2b8415d2fed5a3e831ee3c0d216ee867a2e4cb81584c3ea9d77b3b5fa30cfa19ab53369defae44b0267a8e0210d796698463e2a0ad06c5cbf9d197f3a954aaa96c4c41a7300d493cc617e7500d1c9f635fbb9a94f96c276aa418fd860f6b99a3e14114d051eac3ca51cd3509ca0ce8346d156ec0973e1d75d2ab7988f9d1e0d1329a4fb3841325849914354a24a589cb3be1acf54909bdf6306a292c553410b9fda00ba1f1b07f0979973ab0ce7cc8e68def5432233717f1be411aed5c06ff3b98f5888e31cde80dcc52ab038053fb50bb5bb244e56346324031a62c1cb492825fd1a0e3f293629eefe711656af2a283d693f54e30bf2e1afe4ae21aa1a439f5b2277ad49d28ba85b81ad0d86b8e6d62625f528818f2ce150160675fd39fc24f768c9fa6069d4a0d2b3893e0fa70c762a16e290553d4f9b094c7dc023a0e7c21345f80f60800eb5ee6d28d37f8bf4ad1f3a689092da39221c1fb539d5e98c146a29edadaa3454b9ff93e8a0dd7a3398315d6ee2899d10c870a5c4d1708d360f2944e7121703c8e2ea9ef54d73bc62019752fec5f2ccd38b203d625bbdd6b92d51540f9c55af9ec38f7a70e7de47fbbcca3788e07e4e64acf7806ad07454e57617965c2473258f9997387c20740ee28dc137f8283822d0efbe8b268a1fd170f095359d7814a51047344450cf58dc544e4f0b46497a847760819b345da05543ee76798cfb5b42e0f46510cf46f6bed7c089fb8816d663c38a4c27f29bc5f7cc210d0d49deae702c0cdce70d42360d78c18fd1cf065e50b6a0755a11b77c7e4d5ee62c2051d0155881939045b0fc33b0c46b5149054ebdd9da202d4cc7797aa43743f56746569fc1e7599a2ba18ba7352b2acef47ba3a2b8c7e1d1f6885cd290495888eedf290a699719b89d1535032bc1de33c60dbdc6ac0f401af899b92fcdf8748f19d902b191d52a253c46c15a47f075fef228561998b18e0f7f4a4e527c75509c59872d0b10022349ed5a6b113c9cdd75df0b5589761ff379b509eddf690f86afaca4d9fe85c44a0c8195b14b0d4b9c4d4bae2d59ab6c740d15b730b6290801ff66a5e6ee6d4c7356e0bc6e04987b2f833d25f4473b1cf663534a9023374b759afe1feaae799714c5cf94269c00634bec34f782be0d36039cb646decbca9542913817dcb559e7078f959d59a979d4318b88e05903aa9aef43e43e0a017616e097f3e026ff8bcbafca129b8efbd6806dd13c131bb3177e30851597806801ef16e2e58a427a7848f7627259a177c698cc7425d9b13964c3ec85c665fefd64b4b9098fee118d924c97bd9285ed52d3233d5510099bcf61e3558fb39260596cdeb3b01e0482f84d130d804d5e67622546bfd1c11548f814ecd61cb67507efa817afc86ff1249e910a065916658fc114fd513b9359edf8b33498f80584e880a827f2ebc979d36a82cc9789446f72ce491066f4abd4b03ee386d8eb813fe6eb606a1249ca6a592f12e7848fefb30294062926d953040c860fa851dac36ca8765082ba93e31eb1e2007df83252c8d062171a5e262a01297bd65d4ea3055721bd8afabc3ec1256df3b5149a426c5ce63866ce11f577b3faee2b3395d75ffd60025e34575b081c86989e9ec2789f7d8ec1044cae9599ee6b502edcdd6c00abaabd06b20aaea10ed998b8b80c65a6df8cd3a773ea80fc17093932d83bf38a8368a3917f8c2c9eb193f6f9587eb96bf69ed3384f107d7b28343b9f3705de33404c3770e40db07753ae554b764903a1a07319ad6d50c3bad320fff887bf902bd598b8574ce9428ccf5586e2954ac6bbaf2536211934737e5eaf0a1012903a543c024f34b58e4706cc55bf9687516bc96296a4e6adff507a0f0c72365796af72cadf785fc02fa0fa819f727953b29cf9bee2514217cf21f7fa15140cd7c4b519193f94c0d8efb753c6d76e1ade6ebfd71b5e521a6d6710b2f43035dc6000bafc71645f7f8a7da0bf3ff397a8098d2b924f36a046cb4d515387f57db5e32792df9e8a3488aac6e3960f1c9ea2cb16322e6bc94e44b13925671fe04e3a76729447ff18613feabf046b104e8f6b443822e92267e29a2b0ba7f6d415921c83137ab959681ed1aa6d3bcac9f53105a390b37482f1bb683d861757ab9cf4cfa8330809a5921a2167d7e97c877791ae0370e2dc2bdc3022cbcb35c2dcfd07088d91cabf15823bd057b8ac39b9bafeb88e8c17ac3a269855535a1e8d66b06b5497b8f6fbe125216c2c4ac4cece3baa85b3d8cc50a397195383cdde30580c0ec1b5098627e2af315a0dfe27b785a7579251b2f3c4b1dec5b6128c816482d0486b6af460e54fa38e50f5beabd3bfc9cdb2537a85bdcfef30886ce42a41c5f1416b656236cd52879e5f3a662dbe395f2d4fb9a70f0cb0a0682ac6dd03c8982233b15a2de1cd1ad4c29f1d2c60d4b50296ccd6d65516dc65fdd4e760f6c3825c3656aab20ec67a3bee64a0d05ed26b2d1212fde1e1ce421dd64e95740a7b12472332e47d468cfc0674d25cdebeb89902559ca82e07f2030240ac87ad65cc6a120f2e2cb6c6e091e17403b41f285ba82addbed90cec987e1e9091331330502429fb195c946925b5ae9f21b55612d5f6334945062dd51e1c2a16a17d09a1abd967008a26b6979c3b4afd87c2eafa259cf809ef86465f0717343280a0e8b0e48e756a10ec1aabb558e31b4baa40618c32d08797059d33d67a91d662d708e79b099d986531199195885615b8bc6a6ce331e7d96c31178ce2a0c8f5a6761a71dae83c4abf70ec5c4480c0cb9699df4031578546561397aed8e7d5c3d8339b0e59ec1ebcf75ab1b6d74c27ae6fa173aac297e3e6af4d52d9eea650d492b90885f75bf4aa82c3d747d4392ecab1f88f50100" "sk from a hex seed" + "$BC_RUST" mldsa87 keygen-from-seed <"$TMP/seed" >"$TMP/sk" + got=$(hex_out "$BC_RUST" mldsa87 pk-from-sk -x <"$TMP/sk") + assert_eq "$got" "b09a7f9d1c3754bbc1470d2b91628d660630144bd75e7614bd210e4e9f1e14cba4a6993379d25c4372a51cc76782ccb4bfd64c4f8b9a371e7e2bb6d4b4288b247c3a4ab2e2c96a4ee0c9ff8e388b543fa023067fb87255dfd537b16ddb7b50a83a4af98443fc0376ecebe059699ef8fc0b4605dd3b952ebc61c041da81f02af8ffa3ec95d292ce76493471b34dd2fc422015d8343baafb23338fc1b9e3742a38ae172c7674e3aab6c5dbaacee8143d342435a2ff3ea62038e907b9cb46e967f89b149a8d7b3f3088a1a77bc7d743fa26e2e238e68809771df6c6e6168f54fbff042be8af3ec36ee16d9f614154d329373107a240047350b9280f9f85423e70a87277bb3c0fec911d2f832d947ed75cbf0491545a98c710b8433ae0783c8f3533987d19da5d94b6eaccc8d19454e4d5d9bf1202fbde2e676e7d5dd6311bb75fef0231c7c866bfa0b5a43445d3daae7ce29934dd1e44ade1cd6f887bd480413178aae7a9f5fae7d8f47bd730b6695e49638bae0f9f047a3ff2eac96d633a7da388067917240ed2513684d7f3d43a3287e7a71ff8a50f4c1d83b427e3748ba11728c7c46cefdffd94d5a3cad19c98ae7d3c39cfd9cc57adb4d55efce506b6bce1d594b09a08138921570709821b7867bcb9a16369cd4988025d819152b42bb9be2cf1a0feaae3ba80a862aae4964503a3d7a1994f85813ff135e903e7eee2fffa9083e40166de96f312b2a27c3e91fd8b64590c06eb8541e30ddee206d37cdc200c202bc65464111eae3cee5f4a15fe14a84288099ddbcdec2d23bf456638d5471b0960f53d74b97bfd94441bf53246840a98a0120c8c78b266189d6992343581997147195cbf3b2c5f4c2de79f9a6421ec65f55af85e71be9a85a280dae87ac80835593d1461d9deb7e001378a018e637f6358683893432dc29ee98204a82d795427c0865df2e0cf71a65101f7f6f8e2f7f32359016299f121e29c07f488da275257f6433a6933b8c3df69669ba174ff6a1d9238d60702770c2b50d2c369d7149c0733996fddb1a866c475ade5a84f6a7d5fc7f795f03bd2f305fbc3fd770731eeb00041c20cd61f32c24f92ff3c20ae790de77615c189e2bc9bf41c12fcc066f6f3c6707987090d55b66ba8914b45ef834024be470f166f284e065834ab05550ea84e6addf82a502df243ee26351f6fe36826af07ccb8e92ce9354b8e42be9767bb03e4264aae3449d9084c955e75c9b1fceec4a314fde2203913a9ff1f0ec556d3130df20c3f6c8fcd6b1d33a7aa7f342b0c82327be9b5de468ea221f96bc3a146b24daf9ea27dbd55d4ae92ea1231ed0143da38a3bd857b411a16ef4d829116111d9c28dd5ee75825b9a5d96e517886ee49d9763942bb393108589720b2f132c2e1aab2d616f5986c885b76076cd4666b009672685fc3538a2872c96c62202a6ff7d00ef7b47401f731c079915541230a67085adf68258455972265ffe0e3a3c8cad8bb7e2e9255b70914d09b64f216435f1f65d89e41c623e9dbd02e849998d8fe49f53749205e0034dadd26f606b3dfe90c3f3a594c06e486ae0b9187bf1ff351379d3b456912957f27ef4c06bc8ed909996ffef694eb81c9d47e410acde2b81d51eed7d7539b70c93cd14bd0615b955c98fcb9ab2e441a61bd020760e6ddd5bea462eb60ac580a99dbad2f81d530b31d462f4b1d3e8b8029fcf50b7e9aa54fbac53820f1248c029fcb2f08f2cd66ed6b3d1d2da2b79dbc3b1a5f2c49b5a7b9902a8e1a9bb326303948c9b5797067b0fe6a8259f0ade7a3b309231a58c67d9d30f9b0e0045b403ec66788f9eaf603605f8bb6a45cd4f45b1767facf2c1063e5e199e24b23e59487cfc4f1e048bd7f76135f07b2898acf450089a746adb35efa41e97ea655475d2862ddb1ef9aba81a5fd34b855d691c291ec9da7752c4b0b05d45dfb122e168bae3263d7edcb1454897394afb1661320ef51c6750999de284c590a55477a0616023c924721557e37d2f7778706d925865595d4137d6d548112aad44f0a7715ffd87974b78665b98148b4c2546a68077893b0db40e5e94939933696b6499fc24a1cfddec19556775ab9e4b23b71a916d5e53f32923c3896f010c230e93f3651cf1d2a6cfba3e8e1a30a14895b2d58e3410fc603dde6a0b537a2fedc2480b98b236c00749fba3fb915130df64d9e5166bde7d204d9499a20ab4c5f57b2a84e86f6b2013a0355c98b82651459fe2304e89a85ee63300abe8a8d87157833d127ea8be07893b28540193d1b5690b230ad82f2f19c20a48846f0bd7b4f89b5c3087fc83d210a89ce60e1e94f2f0b46c4bb091fcc0cbbf8fea4baad75c8e97da61b20d02dd23666b97c9287e92ba41adda15859c285dfbb4ac6d4a1c9caae34eab338abf1f67ddc16722687b13999fcb43a64f807677f7b176ba1005ae388328d2e13adac44f1fb7061202c73513a21d6e862804c6ef084c61d627d2607d521204078ef50a6ea152ba7d50c7ca222fc4d8d315bc86e3dab25be0699cf038d927f97499799f1a80f894238386e1061cfca92fab77920eeb21a560786527431b17c2c9cd401cc36e7efaf86406f16716e0f48d113691366e730ff99d840707d3dba15048369e363823f28d5f754de09eaa22ae5d1f6b9e8e4ab8ee1be22d711a98a31a8ed7513eaf9dd4dd274215586d6fcf3df6eb1120a644e04c9463b87b745cfc2f3ab550dd0b7c2bdca5957e4f72f667dc18f467a0ae583943615f6e9c028c454410b25621200af24989473d75f2f8f1a32358fc3a2e7e5cfedcc7b023b0c3eba272ce5bcfa06d1c2c58a685303c920c848ae7f05fd25a900cd95a17cc3273d52871e2427d280b929cb92b999bcec0c23980d59832ecfe9456db5dd54ec63c2cfa9e444523cda66b136bd1140e87ced84704c7f8d3b9e65e5a56b2c7da3ffc702a8537e4b02d91108e8da31c2b5361bcbaa3b1a7c768903c2f2ac2996809dd12a827ec29fbcfd8de5dffd2033a0466b3ace84b68970f36217f9f9dd736e0bb53c81c759281e73307ba0ba958116a52937ce53b903fbe0fe8f97e50a4a540dea8794ccce67c5d843e4eaebe0248d27354e6c0fa526b789b2fd78a3a6bfd990e22d4e126ab952a716f6c13346ba8f602982a7060fa6683d518b9f59ac0c5218a956e2397bc8737f90ca844937ee3c3771216c887ed7a63fc5ad7e9aad75696503d37fe475ecf2794a4938d76fbcb973964fd32fbf4f1f00de680ce7e7fcf00c3411201f2767ae96af4a49f06df5b9b103b94f41618f0dcf932d10ab299f4888ea78d625053e380af3083ebffd1da44b050152204fa7499f165d4a7438d04976388175bf29ec6330d5b84a1d82e0f7def6bfa85738090bcbfa4c2d2f2fa1796ab5ddbdff9e87b8d3d855fb741d4ff5d2287335345df9c905c4d3fea7df69d500d2a634e79a2546ca980bf993e208064173b5ca7bddcf8af8ca3948c7beb4201fd7aad2d47cb0eab110e7dfc19fff1cea3374440db423ed6e4a882c2924c5dcf71b3b90edff3ec0578811702fa0d0a5cf3dceed14e3e883b9fd94dc89d7fadade73a9bc0650a38660f6d5bb0286b0e53c7e415aae21db4ae2c3f3f402e6c587a44c97aa9fe1bc64864f68f29d71c08672308f61daad0a7277544e7065" "pk from the sk" + expect_ok "the pair must be consistent" "$BC_RUST" mldsa87 check-consistency --pkfile <(unhex B09A7F9D1C3754BBC1470D2B91628D660630144BD75E7614BD210E4E9F1E14CBA4A6993379D25C4372A51CC76782CCB4BFD64C4F8B9A371E7E2BB6D4B4288B247C3A4AB2E2C96A4EE0C9FF8E388B543FA023067FB87255DFD537B16DDB7B50A83A4AF98443FC0376ECEBE059699EF8FC0B4605DD3B952EBC61C041DA81F02AF8FFA3EC95D292CE76493471B34DD2FC422015D8343BAAFB23338FC1B9E3742A38AE172C7674E3AAB6C5DBAACEE8143D342435A2FF3EA62038E907B9CB46E967F89B149A8D7B3F3088A1A77BC7D743FA26E2E238E68809771DF6C6E6168F54FBFF042BE8AF3EC36EE16D9F614154D329373107A240047350B9280F9F85423E70A87277BB3C0FEC911D2F832D947ED75CBF0491545A98C710B8433AE0783C8F3533987D19DA5D94B6EACCC8D19454E4D5D9BF1202FBDE2E676E7D5DD6311BB75FEF0231C7C866BFA0B5A43445D3DAAE7CE29934DD1E44ADE1CD6F887BD480413178AAE7A9F5FAE7D8F47BD730B6695E49638BAE0F9F047A3FF2EAC96D633A7DA388067917240ED2513684D7F3D43A3287E7A71FF8A50F4C1D83B427E3748BA11728C7C46CEFDFFD94D5A3CAD19C98AE7D3C39CFD9CC57ADB4D55EFCE506B6BCE1D594B09A08138921570709821B7867BCB9A16369CD4988025D819152B42BB9BE2CF1A0FEAAE3BA80A862AAE4964503A3D7A1994F85813FF135E903E7EEE2FFFA9083E40166DE96F312B2A27C3E91FD8B64590C06EB8541E30DDEE206D37CDC200C202BC65464111EAE3CEE5F4A15FE14A84288099DDBCDEC2D23BF456638D5471B0960F53D74B97BFD94441BF53246840A98A0120C8C78B266189D6992343581997147195CBF3B2C5F4C2DE79F9A6421EC65F55AF85E71BE9A85A280DAE87AC80835593D1461D9DEB7E001378A018E637F6358683893432DC29EE98204A82D795427C0865DF2E0CF71A65101F7F6F8E2F7F32359016299F121E29C07F488DA275257F6433A6933B8C3DF69669BA174FF6A1D9238D60702770C2B50D2C369D7149C0733996FDDB1A866C475ADE5A84F6A7D5FC7F795F03BD2F305FBC3FD770731EEB00041C20CD61F32C24F92FF3C20AE790DE77615C189E2BC9BF41C12FCC066F6F3C6707987090D55B66BA8914B45EF834024BE470F166F284E065834AB05550EA84E6ADDF82A502DF243EE26351F6FE36826AF07CCB8E92CE9354B8E42BE9767BB03E4264AAE3449D9084C955E75C9B1FCEEC4A314FDE2203913A9FF1F0EC556D3130DF20C3F6C8FCD6B1D33A7AA7F342B0C82327BE9B5DE468EA221F96BC3A146B24DAF9EA27DBD55D4AE92EA1231ED0143DA38A3BD857B411A16EF4D829116111D9C28DD5EE75825B9A5D96E517886EE49D9763942BB393108589720B2F132C2E1AAB2D616F5986C885B76076CD4666B009672685FC3538A2872C96C62202A6FF7D00EF7B47401F731C079915541230A67085ADF68258455972265FFE0E3A3C8CAD8BB7E2E9255B70914D09B64F216435F1F65D89E41C623E9DBD02E849998D8FE49F53749205E0034DADD26F606B3DFE90C3F3A594C06E486AE0B9187BF1FF351379D3B456912957F27EF4C06BC8ED909996FFEF694EB81C9D47E410ACDE2B81D51EED7D7539B70C93CD14BD0615B955C98FCB9AB2E441A61BD020760E6DDD5BEA462EB60AC580A99DBAD2F81D530B31D462F4B1D3E8B8029FCF50B7E9AA54FBAC53820F1248C029FCB2F08F2CD66ED6B3D1D2DA2B79DBC3B1A5F2C49B5A7B9902A8E1A9BB326303948C9B5797067B0FE6A8259F0ADE7A3B309231A58C67D9D30F9B0E0045B403EC66788F9EAF603605F8BB6A45CD4F45B1767FACF2C1063E5E199E24B23E59487CFC4F1E048BD7F76135F07B2898ACF450089A746ADB35EFA41E97EA655475D2862DDB1EF9ABA81A5FD34B855D691C291EC9DA7752C4B0B05D45DFB122E168BAE3263D7EDCB1454897394AFB1661320EF51C6750999DE284C590A55477A0616023C924721557E37D2F7778706D925865595D4137D6D548112AAD44F0A7715FFD87974B78665B98148B4C2546A68077893B0DB40E5E94939933696B6499FC24A1CFDDEC19556775AB9E4B23B71A916D5E53F32923C3896F010C230E93F3651CF1D2A6CFBA3E8E1A30A14895B2D58E3410FC603DDE6A0B537A2FEDC2480B98B236C00749FBA3FB915130DF64D9E5166BDE7D204D9499A20AB4C5F57B2A84E86F6B2013A0355C98B82651459FE2304E89A85EE63300ABE8A8D87157833D127EA8BE07893B28540193D1B5690B230AD82F2F19C20A48846F0BD7B4F89B5C3087FC83D210A89CE60E1E94F2F0B46C4BB091FCC0CBBF8FEA4BAAD75C8E97DA61B20D02DD23666B97C9287E92BA41ADDA15859C285DFBB4AC6D4A1C9CAAE34EAB338ABF1F67DDC16722687B13999FCB43A64F807677F7B176BA1005AE388328D2E13ADAC44F1FB7061202C73513A21D6E862804C6EF084C61D627D2607D521204078EF50A6EA152BA7D50C7CA222FC4D8D315BC86E3DAB25BE0699CF038D927F97499799F1A80F894238386E1061CFCA92FAB77920EEB21A560786527431B17C2C9CD401CC36E7EFAF86406F16716E0F48D113691366E730FF99D840707D3DBA15048369E363823F28D5F754DE09EAA22AE5D1F6B9E8E4AB8EE1BE22D711A98A31A8ED7513EAF9DD4DD274215586D6FCF3DF6EB1120A644E04C9463B87B745CFC2F3AB550DD0B7C2BDCA5957E4F72F667DC18F467A0AE583943615F6E9C028C454410B25621200AF24989473D75F2F8F1A32358FC3A2E7E5CFEDCC7B023B0C3EBA272CE5BCFA06D1C2C58A685303C920C848AE7F05FD25A900CD95A17CC3273D52871E2427D280B929CB92B999BCEC0C23980D59832ECFE9456DB5DD54EC63C2CFA9E444523CDA66B136BD1140E87CED84704C7F8D3B9E65E5A56B2C7DA3FFC702A8537E4B02D91108E8DA31C2B5361BCBAA3B1A7C768903C2F2AC2996809DD12A827EC29FBCFD8DE5DFFD2033A0466B3ACE84B68970F36217F9F9DD736E0BB53C81C759281E73307BA0BA958116A52937CE53B903FBE0FE8F97E50A4A540DEA8794CCCE67C5D843E4EAEBE0248D27354E6C0FA526B789B2FD78A3A6BFD990E22D4E126AB952A716F6C13346BA8F602982A7060FA6683D518B9F59AC0C5218A956E2397BC8737F90CA844937EE3C3771216C887ED7A63FC5AD7E9AAD75696503D37FE475ECF2794A4938D76FBCB973964FD32FBF4F1F00DE680CE7E7FCF00C3411201F2767AE96AF4A49F06DF5B9B103B94F41618F0DCF932D10AB299F4888EA78D625053E380AF3083EBFFD1DA44B050152204FA7499F165D4A7438D04976388175BF29EC6330D5B84A1D82E0F7DEF6BFA85738090BCBFA4C2D2F2FA1796AB5DDBDFF9E87B8D3D855FB741D4FF5D2287335345DF9C905C4D3FEA7DF69D500D2A634E79A2546CA980BF993E208064173B5CA7BDDCF8AF8CA3948C7BEB4201FD7AAD2D47CB0EAB110E7DFC19FFF1CEA3374440DB423ED6E4A882C2924C5DCF71B3B90EDFF3EC0578811702FA0D0A5CF3DCEED14E3E883B9FD94DC89D7FADADE73A9BC0650A38660F6D5BB0286B0E53C7E415AAE21DB4AE2C3F3F402E6C587A44C97AA9FE1BC64864F68F29D71C08672308F61DAAD0A7277544E7065) <"$TMP/sk" >/dev/null + got=$(hex_out "$BC_RUST" hash-mldsa87 pk-from-sk -x <"$TMP/sk") + assert_eq "$got" "b09a7f9d1c3754bbc1470d2b91628d660630144bd75e7614bd210e4e9f1e14cba4a6993379d25c4372a51cc76782ccb4bfd64c4f8b9a371e7e2bb6d4b4288b247c3a4ab2e2c96a4ee0c9ff8e388b543fa023067fb87255dfd537b16ddb7b50a83a4af98443fc0376ecebe059699ef8fc0b4605dd3b952ebc61c041da81f02af8ffa3ec95d292ce76493471b34dd2fc422015d8343baafb23338fc1b9e3742a38ae172c7674e3aab6c5dbaacee8143d342435a2ff3ea62038e907b9cb46e967f89b149a8d7b3f3088a1a77bc7d743fa26e2e238e68809771df6c6e6168f54fbff042be8af3ec36ee16d9f614154d329373107a240047350b9280f9f85423e70a87277bb3c0fec911d2f832d947ed75cbf0491545a98c710b8433ae0783c8f3533987d19da5d94b6eaccc8d19454e4d5d9bf1202fbde2e676e7d5dd6311bb75fef0231c7c866bfa0b5a43445d3daae7ce29934dd1e44ade1cd6f887bd480413178aae7a9f5fae7d8f47bd730b6695e49638bae0f9f047a3ff2eac96d633a7da388067917240ed2513684d7f3d43a3287e7a71ff8a50f4c1d83b427e3748ba11728c7c46cefdffd94d5a3cad19c98ae7d3c39cfd9cc57adb4d55efce506b6bce1d594b09a08138921570709821b7867bcb9a16369cd4988025d819152b42bb9be2cf1a0feaae3ba80a862aae4964503a3d7a1994f85813ff135e903e7eee2fffa9083e40166de96f312b2a27c3e91fd8b64590c06eb8541e30ddee206d37cdc200c202bc65464111eae3cee5f4a15fe14a84288099ddbcdec2d23bf456638d5471b0960f53d74b97bfd94441bf53246840a98a0120c8c78b266189d6992343581997147195cbf3b2c5f4c2de79f9a6421ec65f55af85e71be9a85a280dae87ac80835593d1461d9deb7e001378a018e637f6358683893432dc29ee98204a82d795427c0865df2e0cf71a65101f7f6f8e2f7f32359016299f121e29c07f488da275257f6433a6933b8c3df69669ba174ff6a1d9238d60702770c2b50d2c369d7149c0733996fddb1a866c475ade5a84f6a7d5fc7f795f03bd2f305fbc3fd770731eeb00041c20cd61f32c24f92ff3c20ae790de77615c189e2bc9bf41c12fcc066f6f3c6707987090d55b66ba8914b45ef834024be470f166f284e065834ab05550ea84e6addf82a502df243ee26351f6fe36826af07ccb8e92ce9354b8e42be9767bb03e4264aae3449d9084c955e75c9b1fceec4a314fde2203913a9ff1f0ec556d3130df20c3f6c8fcd6b1d33a7aa7f342b0c82327be9b5de468ea221f96bc3a146b24daf9ea27dbd55d4ae92ea1231ed0143da38a3bd857b411a16ef4d829116111d9c28dd5ee75825b9a5d96e517886ee49d9763942bb393108589720b2f132c2e1aab2d616f5986c885b76076cd4666b009672685fc3538a2872c96c62202a6ff7d00ef7b47401f731c079915541230a67085adf68258455972265ffe0e3a3c8cad8bb7e2e9255b70914d09b64f216435f1f65d89e41c623e9dbd02e849998d8fe49f53749205e0034dadd26f606b3dfe90c3f3a594c06e486ae0b9187bf1ff351379d3b456912957f27ef4c06bc8ed909996ffef694eb81c9d47e410acde2b81d51eed7d7539b70c93cd14bd0615b955c98fcb9ab2e441a61bd020760e6ddd5bea462eb60ac580a99dbad2f81d530b31d462f4b1d3e8b8029fcf50b7e9aa54fbac53820f1248c029fcb2f08f2cd66ed6b3d1d2da2b79dbc3b1a5f2c49b5a7b9902a8e1a9bb326303948c9b5797067b0fe6a8259f0ade7a3b309231a58c67d9d30f9b0e0045b403ec66788f9eaf603605f8bb6a45cd4f45b1767facf2c1063e5e199e24b23e59487cfc4f1e048bd7f76135f07b2898acf450089a746adb35efa41e97ea655475d2862ddb1ef9aba81a5fd34b855d691c291ec9da7752c4b0b05d45dfb122e168bae3263d7edcb1454897394afb1661320ef51c6750999de284c590a55477a0616023c924721557e37d2f7778706d925865595d4137d6d548112aad44f0a7715ffd87974b78665b98148b4c2546a68077893b0db40e5e94939933696b6499fc24a1cfddec19556775ab9e4b23b71a916d5e53f32923c3896f010c230e93f3651cf1d2a6cfba3e8e1a30a14895b2d58e3410fc603dde6a0b537a2fedc2480b98b236c00749fba3fb915130df64d9e5166bde7d204d9499a20ab4c5f57b2a84e86f6b2013a0355c98b82651459fe2304e89a85ee63300abe8a8d87157833d127ea8be07893b28540193d1b5690b230ad82f2f19c20a48846f0bd7b4f89b5c3087fc83d210a89ce60e1e94f2f0b46c4bb091fcc0cbbf8fea4baad75c8e97da61b20d02dd23666b97c9287e92ba41adda15859c285dfbb4ac6d4a1c9caae34eab338abf1f67ddc16722687b13999fcb43a64f807677f7b176ba1005ae388328d2e13adac44f1fb7061202c73513a21d6e862804c6ef084c61d627d2607d521204078ef50a6ea152ba7d50c7ca222fc4d8d315bc86e3dab25be0699cf038d927f97499799f1a80f894238386e1061cfca92fab77920eeb21a560786527431b17c2c9cd401cc36e7efaf86406f16716e0f48d113691366e730ff99d840707d3dba15048369e363823f28d5f754de09eaa22ae5d1f6b9e8e4ab8ee1be22d711a98a31a8ed7513eaf9dd4dd274215586d6fcf3df6eb1120a644e04c9463b87b745cfc2f3ab550dd0b7c2bdca5957e4f72f667dc18f467a0ae583943615f6e9c028c454410b25621200af24989473d75f2f8f1a32358fc3a2e7e5cfedcc7b023b0c3eba272ce5bcfa06d1c2c58a685303c920c848ae7f05fd25a900cd95a17cc3273d52871e2427d280b929cb92b999bcec0c23980d59832ecfe9456db5dd54ec63c2cfa9e444523cda66b136bd1140e87ced84704c7f8d3b9e65e5a56b2c7da3ffc702a8537e4b02d91108e8da31c2b5361bcbaa3b1a7c768903c2f2ac2996809dd12a827ec29fbcfd8de5dffd2033a0466b3ace84b68970f36217f9f9dd736e0bb53c81c759281e73307ba0ba958116a52937ce53b903fbe0fe8f97e50a4a540dea8794ccce67c5d843e4eaebe0248d27354e6c0fa526b789b2fd78a3a6bfd990e22d4e126ab952a716f6c13346ba8f602982a7060fa6683d518b9f59ac0c5218a956e2397bc8737f90ca844937ee3c3771216c887ed7a63fc5ad7e9aad75696503d37fe475ecf2794a4938d76fbcb973964fd32fbf4f1f00de680ce7e7fcf00c3411201f2767ae96af4a49f06df5b9b103b94f41618f0dcf932d10ab299f4888ea78d625053e380af3083ebffd1da44b050152204fa7499f165d4a7438d04976388175bf29ec6330d5b84a1d82e0f7def6bfa85738090bcbfa4c2d2f2fa1796ab5ddbdff9e87b8d3d855fb741d4ff5d2287335345df9c905c4d3fea7df69d500d2a634e79a2546ca980bf993e208064173b5ca7bddcf8af8ca3948c7beb4201fd7aad2d47cb0eab110e7dfc19fff1cea3374440db423ed6e4a882c2924c5dcf71b3b90edff3ec0578811702fa0d0a5cf3dceed14e3e883b9fd94dc89d7fadade73a9bc0650a38660f6d5bb0286b0e53c7e415aae21db4ae2c3f3f402e6c587a44c97aa9fe1bc64864f68f29d71c08672308f61daad0a7277544e7065" "hash-mldsa87 derives the same pk" +} + +# Wycheproof testvectors_v1/mldsa_87_verify_test.json tcId 1 ("baseline", external interface, empty +# context) must verify, and tcId 6 ("short signature") must not. The signature and key are given as +# hex files (--hex-input) and as binary ones; the message "Hello world" is stdin either way. +test_mldsa87_verify_matches_wycheproof() { + unhex 48656c6c6f20776f726c64 >"$TMP/msg" + printf '%s' 17a508179b35057099111733da28fd1a2265de7d8ab22d5279f13bca84cc42a5b8c9644c121e7e1b81723c5295be288fb6c36bfa188b6e08d913a152350947fa2c8ccc3fd01b319f65a2058a1dff54133946cfeb408d0b6dfde6bbebd7e0591cfe83b8b5452ceef6c855f7d33e06a0d269345089ed0d3ad67d84d8a4a34d16836004cff125469e8c3387abd788b620e30c1fc23909117a0e34c42a6631d9791347b1b2a3c9ab3082416211afb7bc3f6ce630a7019af19f736cdfacb1e7db66b65ef56844d2a2b0753d09283a7a0b66f77596384e95f7ceddd1c4ba20edc11f1eaab695bb963f6eda1c383754aa372a0d7729bfa6e0f142131c2367ba3f89ce3de6c357f9a7225b7cb85f6b3e8a3a122e8501fd1446b8152a415c19dda1d2e4590cd994f6664b4d1abd7381468c3a085abe2741a0cfbb81880664b271677245c4a471bf8bb8e0192eb32e4fb5e8560f3c50d6b19a353e486d0fcc2a35ac046286e707e095f61786d92212686a65d39b6863e0f8cec1e1997f2f845e4878ca9df650c746765296790863e51d012d32dffcbd746aa2276d04c0a57cd1b3d6ed06c0d66a0897aae5c49c97b6f19ae829baaafbfed28a52c05963c6eea9eff69528294207f8cda75280f7c486e6848791c8e37015479f2e13c28a9fe654dbde11689875203aaec51be3da7cab1cf31e4ec476c0c830cbdd04ac02167c0a6fbfdd6548b1fa525d235c7e3fca8d63e6427503b0a45c0bfddb428b837c32e8755441077bfe1c0142bac357b012a46545bf4148d465472dcf89c9d73b62357087e229f53a450d3cce41c8ee21a9d54b61e34a794f5b1406a70724ab0c3712c49df231ef30a956075e907c51b63dd1f9453dbe60e25b0f3cc0354dfd7c9119313919e77cb2c92f544d3e5302b8827603e936b567e99bfe9904932585a9f01a5a1b5bce07565f1d84c6b1c5c86259e1fefcff18cd06861122be6836be21e40be4eaf6bcabee8f634f95520aa914bb51c54dbd67d1b9dc5e38831e786c283979a963a3206b98e339edec4128b0502d4d47813869713e431a529a03c7f54b50123680f2b7f256f5d2b40642203259b9e85c62253d5670ce372193f28b5aa48ddd643c54756a2cff808c109f74772961d8db6bb8a17547c8f29c7f5ff3ea06740b867d84917e07f3978ad0281a20689eef58467e768b6178a9b36a567289fd39762bb3e4254031b2798a4550857f6af369d484392cddd7b48eaa2942e2cbfe754d5ee2da2b7fa71222e4a525ff5224d551a778ebd828e4e0499adc74ff0d59a5abc78ad6a8abafeedb3c99045a14423507f85597b1a7f540982f7d72ea13449110b442d54b78029b4c7fe3b49396dc6c3b7d58792538fa907963de10a4b724548142541cdf1512e0f7ff1b10a93de63541b8cc3268b4de20ed26739ee8973b6507ebe48965602c35fa3f7d4278146b598d7d7044e16e97e9351f7c51ac25573b7232ae2432638e9166190e7f7a7dcb5096ecb5d10017cdea2a82b4f56c7385041c6919a7e36e11beac77ec3f25df44e7b596c1542c1e376de3667c0e903fe25b57c338e9d93c5570c484f0ddab4f57d38f292b23599d9efc7a9fd9e078aaddca0acb1a196d6c45d3c8be6f39e8cdbe3299e370b262e0bf6fb5f005cae2b12879289d00bd8039de6a571c310d87557f5c9a4f64a0bde7177a8464722a04bf87fa2cb0e312d4fa6e536c61d65dc2c1baf144b0d1d1d75f4c860626ff773933efa9941d105c53a1d92c4f7c7bba4aa969590acef1e50901870f59715ac14d9846d83871a77367be57c63f88bc2c02eabafe678f44925a3e605979282fcd3f284736a1d346c033cb782dd615e886683fc37cd87a91422857774c63c6659096eba393c56225ed8c3485b4f89ecb07d53526281a6426ae7d67cda52fec5ac32320caae9b96000bcbe9e8782be88cb1ca6dcaffb74ef04c77e03a994bea2c89e4fcfa44cd0c9f4e30705a8b7b20df8c76b05a4479400e07db03d243e9fe4c90d34e9245f1e574be9a388f5355482077e4e98b919de024e666fdd7d51ed2a0d58a823e7497eb07303cf1d6d5f10a536be980220de5856727e5c13981839cfa19740988e7771a2b984f53ae3a5916ed881a4a90fe524f0bb3778355882864f8961fade32e656fcf9f524e748c8196a1f1bbc57bf8da7b36de9b0080f0c7bb8487a2b7bb7a81a8ff43a2539b367c9a48c70041520f05ca3dae316dbbe3118218216f52b7bcdba7557c4c9d861803a5e2ee01d3682e1261d7cae0a99fb8de909eb2bc1e112aa43cc2fa9c76a222bd85faaaba5d9ec2198ac45a295181a324a0592632b89e2752582cd5e01e1a610e7563faee10b76d853109e257e7c0c248a9fb7933f514b07b4f4e3a4a3d2cd22e8cc45ebda3bef5948aa050f01eff85ae98d19f69c51e67ff89f2df0c5268acfdd325e84591317e05cab4f9e6358f249c4ddf4019fbc8f511549a733898a50efa9e0793083de0b15b5bf78d9f63d8df830d42df2fefa27b89e0ede2a702eb9467118fc0ed44edc63ad1b1935877c34843fea06fdf388bbf83e501723a13cc6cc2efbb9691fe28fc1d45270591e5bdf7aa1c82673544ee29d9e6c9da3328f21e9729bffd7f4e56de585909679a74037105fdac3f51ae35f69d9763d2e4cfeb1d4a8fdce99bf1aa21f866a9f523b2a9549e12258a4d19900cf5db37b67da19b23563bd1d701c6106fccb28e4689c62e1a6cf1abd763d7239c2258b765610d4478be9f1650cb8d18923592ad0024076e52f9bd0a3894fe97bc0a1646b4c37f62c27f32d0df270260f47c49a5caf110e4cf80168a7d54b1c70bed9bd5d9a143ce869a05cd44ee266aecd6bfedb39be79e7c7d5c11a99575ebc0f389cc55a4fe1469a2d61b70bfe4b74e3e27521a037d2b9f4fdb377231e2ceb214ba90f6953865c683215203ce963875c6524c01b789e0389a9f0c386eb236f0dfba6c95df4f28ccc7ae7cd473f9dcd20817cccdd211bcbc78b064e936e4ba2813df531128428ddf410e6ca07044aeb4cfcc0a16c995ec51c8af16a541ce18dbeb69a26635632dcc24ee52a5eedce38c502cd0e356ec31341c893f92e6063c3a160a53d34b85e92357a8ebaaad8f206771be43ee48cc409825a7094bda529ee18776d9e67f1fa1c1419514309d70ba2443be2f63b6943478d6c0f56dd058731e53de4c30bfc7d915e9284a56248e81944392881666680d4991f04269ec9a83b24b458ed59a6c274de452ab3013c103a4920543e6a7d22dadfd764f6ea39d49b910ee0dc216e547aa5fb4382a72a568ebe83ec00416fb5830dc21c24ae72416602870cb52c3a8a1c4c12a4b287b9b800d31c287ca161f404a9e598a5358d28b3aae43e534846bcd0d7a9c7652ae01e6698c79e315aca8198f36de45af7084b1cb21ca2ba0ee3a547a7343a10ef9e3fd17b0a4060badd1409a0562cba25b84fd578268fac53cfbca08e6cf6e5419f57262eb5813c1d1324e0df1d483ade08d8f6c62498e262485ac7c2872b11b42e5c1b797fc12e838b38a711d364d45cd1ed35f7faffdf4b0fb0eaa312fc3d5af77909b0649cbbacea10c9831273922b5b05172face9ce6cf324edf6e2f5f5fa0a9f0463eee938b30adf3e55664f94d274cd87dea901a7e08e805 >"$TMP/pk.hex" + printf '%s' ba4275ff54c22d2d09ea1937a0667362acd44925c6d6965fad350b111d1cbcce68ddbd0e576d1a8810eb4e71623781f32f747d44c8e693749df191682f588906949d97617a4b0ec54ad966818dee88b95f0f28ca24bfc5bfe0c316140b0662c43093ae48b899cc71e5739e9d67095ed987a79b6a0e7aac960c3c4125f0e92bc9435d10bfffae34bb3af05e977ebe0bafcbeb2381c5afe3379667b4c201aebf162dbd0a4bd1baa88fb2f88fa970499a848737d3cf94cc8ce278880a169cad91f304e4e8f1091d4cf39d9a3ab9f88dcc6f3bc4df311a5be0cba290365b3e879527e2a77f0cb6eccc9d85a5e592fb00f3a2e925a26d295a6b82746d7f534c83c35bc4826ee4910216b9a2867032698996fb0e1669b539ccd2ec74d181f4844e8f4d28f9c174316c12dadb54cb1dde7338238a20731c2565bb959f8e3086273ed03abc7ac515728750633083c0b397b29d385d13f5afd2529b32f00dce66c9dc8ea93d99c8b61c5e0ab2fc70de2a8dabdcaf290d53e8fca7561bda8c516ad475e4ec6c7cd2603aa3c8a71d9fa5dc7efc33cf318bebc1f1594e6ea25c69b8f9ce34a65b8ab0ae8dd3538bd267d86c584b8f354d7e4776ed4dd59a73f9e70a1df572f033b69b3eafa5a901e02515472e37258608875ca469de07db71cd6b8dc7edeb3d866ed2d219e44fcb133a066e89d8e3013569ca6f1fee7bf4ae56a6d32a5f3e5a530819c31aadabc8a88503edbea9cdfa3171762e1e8bcebaf9bb6af7e540102d5fc810bbcf1e02ae564e04d9dc55dab0a9392d6c95a317730d9793954da2cb16544d15403d0db01e85881e2d4f1b9b98458e1af0985f98b014f08f200558f2fed7a70c352a27423bedeebb3775c0a1ede9d461d2aea303c09c8b1f73fc37a5b3a01fb24a131574f7eac90c78baa38cebe81e2335dafda20299f76d6a0ba77d2954d11381674f4069f45e133886d64222d92583d5908e3ab6eb6a72cfb41f7dc3e71c1383888f61624bdcaf12fb716de98232ee329af1dd045f30d377234db7bfd11ddab0b3108329e16ce568c8bada39a98df5a5f72a72f063fa4253313a806013f61ce5adbe54cf42180ebf6e496bd4b42aeacf069aaa8e1c231fd037b394d78a69d1742b45bc5784ab8593a198077423c4c357d734f9899cfe9b3b62b6b9c3f4d781b8484a3fd0eea7e8d6945f4ba2b046fee079b7f032bebbc402918daa2aa1c9433dc3bcfbd7f49a5d7a293833c21c1bade3e8a7ec3c83d485529de51b5993cdffe23b770e25acab0ab9fb3059f14952d9464ebfa36a6274d8da317a7b07c2afe38ba28ca942cf7bfaee4020e59911c047b2aa24d1787baae1bc3546364b782358676d75c7698769f1d4a0d5dccade7fcf80e308437f7fc24fdfbf72625abf1b0f534da62cf860da1efd986950e3e19a085999f4d008fce38459c673befcdf2287c1766e106beb4f3175e97812da141330dcb2beb3265e38c8423c19dd50a655c9e8dc969f6367f3383b644d53a26875d53cea26de429266b506e70e7e6832886ed06d81738b0b482bd23bda396eb674ddfdb64803d6c4fae2f040170b5a28923279838b7b876220d02dc7478666f7c3287b1ba4a2f8228e8c491a55ba459805c601b986caea27ee9436f63383351c74d673643b15f6007fc6dc49e337a65a8a96ecd7eef4ad730bf3dc1972fc396703ee26af1156dc4beb46c47d99bb69ebdaf81c2d738d0f70e57a2c7162f5a55242255675f22082aa43f5b3dd862b535b2a15bb815b4e9faf16a302552cd6098d40930dfad7a7c609d6aeade814242a8bf0721c1c26d0d3daa7a638880a6411c6538d80c259d31b639aadaea49563a8d7f5ddb64291d6b086c80d72bfe7d26802cbd20fd6ba5495011e42da1c82483cbe8e37838e73c48f79f65b205476bee600399feda6aaaa939eee12cb34be4e6bdbd18032c85b54a2713551de677a16ca0142ff97b77bc963e8f3c57d91df9b49474b01ca514641842893abc3181caee3f49b635d17daf41bcc4aeb1116ec4b3e78ff1480cf3a5d9c6a154384b88516834d19196976e2ba97cee91f7a0d73f7f8146e58dc0fc8f510511d39d82a7ba531a4abcce6b624035d753a37c5980343cfc7724cf83efb0c33fc4abb5b002241bf57a46b67cb5a4cbd637b2f19bc93b368d97e13c6a62c8443c8222e0a90c3ed1972cc739b824fdf729ed8eca02ad96bd78bf6d2b3d2853e24fa93199ff41635176b31aab2207013d0a9317fa49668dfeb672b8129e6175a2998642ab8e74f0823e3b5480ae80180ca395f5348744a3c7b344891f008aab65914760b5fe852615ad6425216b1c5e777db1a46517cd01a77b277cfa2c6f250c2d68c495fef28feee6e0716817d6b30716f5ca48001805042133eca17a41c2219784ce0f12858ccfd371c77b90966ea04c3996851edf31aef962946628007de5531b06fddf3449f6c552eaf6e16b3e9160e265900b8c8e414732505e02660e123d45a3d6f1b15e56fd759d38e821e27e84967e95c4b0d2a48e008897e655d1b65b76299f1e3074209abe44b37f8786b02df23aa4dcebe512e6312f1a3d6d781d749635247aca897626f5ee688f517df6cfe947ddead820f07fed4bcbe7f1bee3f19117b5666dc123d528f2bf03db346d6afb53804b0681aea98a9479fc6de5ae974d2c0d2055f07ba8a1d2d8b6a7e08a6805bd8634cc190c4988b502f475c36d78c7b3dda0057c818835bbf21c5a7c13bb6fd91cd3cfd76abe4c2ff908a08a3000b9021500ff94b297cec0fb3e51b1cae7026ab4347bfd5b6a641a3d347f45a2a2aacd22e521e00da0c3986c67672c5d7d7e61e7edafc15d42aa42cb37fa8621f79e9096b092efe853ab318b3e4bbd90f20165a1be1d3aa5b3de43751b65abbd952599869778393c4351ab8f534fd49e16547df01c40bfaf56de60b4fb0019bc34177cc8e2236d219fb3c0b84dedae6b88e134280eabd1823420cd1afe6d929774967fd7d885fbad33d89ed9dc5e0eb978eab5d96c50bed5887aa8277880c7b06bf2780cee82ce639a1e3344c88a25102edcc17a4cb48989c4ad6a998726ede31deb0b98107f40858bad7864983f6ecdf0c9761a42751b19d5360daa7fddbbd2e2292638b95c763ca1e747eedef0dd387e9d9ec9e5afee207d8c45703c5befc2de3878d313655ce85ad984250cdf054360f33d41dc193060d42cf9528b1fb91d6ca3395199e25a1a7739eba9a6a4ac2c417ae615940b3eb1b746dd0ecc7b2f7acdff887110115629f70877dafcd7a6625fa1b9e256bc8fe1d66005dbcf12fde0a5fcda5d4f23d58ece91d60eb91274df8d9d17d4a39e63533acf1b317db979b04ca3ab9a0bcba652d0010ca3fcd33ed8f8a62faf42f78b37912d3adb410f20bf16b31359cacc25fb783083a3f065f0a2dd6fe58b8f594e11a87bf0f4c5f5493f334c18b03ebdefebce50228937ec13a8c221b617450486291071f3c14f64f66c927dd4bd623c214ac35433b8a6875cdf00916476eac0f196858aa1484bc1cd45b726d33a965619829b8deaa9d9fa0c3c210f23967ce26a4bdd939cff8aa662f70fb0af97ee44bcb9e2755000a195741d8919e4dcb1a5caae21009f686fa1489c72f16f9fee76b5410ece7f406947f4a19f394a5121da79f3216777b0fee5423328156ecf0b4548dbd5b3f7b526d6b9cfd57576f67dd521c314c2d37474ed0cf732c3b073a101c735a4c4e6b33c9aaa12c91d147ad1075a20287d36c388657614a9c648d8e49cce8cfa282b2a2e8e9da6e4444e4b6aed1bd0ada5009a335cd0500bdae9a01b97f7e8cfe8372398e750de92ee4a524393a19826d19de5e762fb53a88b9b4657e9c6d7d01a4124e3e39532f614aec5cb88d982b78b2ff568017c92f6a1ce5298b5f323b5ee61695038ef0c3a7a339cfac31cf7875a4563046a40e7b35cef1d37d811b342fcbb5373122415befc23cb656a619f7c262c443403b23ba20e341a079918dec6f4f801b92781179ad7ac1951f39ddb0b1f1fb95b28c0f4593a04486f0e0e86bb3b014674879aad10f41e0bad34d40bc817b6fd43c1dde8547882d82e5111e208107e9c9736d16ca77ac7453d7b6c7976a7dd6a4c6c0252185bc9948660a07b66151b09b980d8572ae829ab2f6d900cc63066c4ecb3a0317e8a9acaa8e22a216721f3d69c67843df2fbedd88bc424f761cdf420de4ff2c55b6925b1826c1c4134090d2aae82a7f64a8d428efe1b21097a6fdbc9b8a31c4d46c7b32d478bf5b9181bef8af4d3486958d7c198c46a5ad771090bb64e7fc2bd7677ae7618b861c83c13177075063517eff40cfd52ae56650e9e7035f783bfe8920c754e98470b327909ac2a407e07864710544b8adbc075502c75b5f0fdbca0cde5d94b013141c96b55ae1b60ab63f9e792641690af41b05a78233935fd7f82852ebb2602a10459a709256c41d108c2aeb71925089cc79b121eabd5edc54f3f7c8929685b88ab29700c7fb2f247cd7962c8ce6dc9f79f9358d6a7989d24ccc17d0dbaa0b7fbe73479164181ae7d6b6a02678cce46f74ea2bd387ecb73041817b429a0220e1c3635fe492f5f3a8e2b65c086fe24c375563d7220856dd8f970716d548492964a156554dc88810c3c4f81dcc3ae80243e19679a3be9c9b1b8c707b416e9166c54a568bb9d84728c1a283d9231a12b13688ce2362901342652d66bf44cec223f561d2735d977c6adcf57e9066220d0770fc77fdf6ff367a2f36ae20062887beb88d1b453f267aef9bc763ab716bacb9214c38b95f4f2f3f6c236aa71aef83c1ae4b26133678884476c1d7c6d3e7b99f13e028ac6cebddfae4793f9ae975f3d66b725b500e7c7d2f664eaa0c358deb91cb2d6173394b306749d2bfb2684b985769bdb682e922b75555d38dad1899057a64ef6ea361e4712244d02d0dfec8c3d40770122770c2538d6a14a8462ef18eb705c16e5ba30aa5366447c94869060e4df155f7d01ffda04c1ced0ad5fbe5fdd85856e1e49320319687d31a6e4c7145479f45f43a9b8e9ffe4cbacfad4a21e5445e119df2996cce8b11d0f224efb4f18b544d456e2fb1d96fcd99fc319dbd86720621ac25b490f3611f7e5655bb3940a503c07dbf41f4b87593595a66008808744668371a54ce1b9dfdaa16f90415e57470bc23898d13d8351ba34369e96347da13d012b4eab32ada90668654c5ed2b433716b1b4170f640cf4b40659efcf4150237bbc25f72b248be85cd482b55ce2f5f73e8f02efd2465805b37d12487465ec1085bc6602b71862771af13e13baece4916b1ddef9ce016c92db9fb9e82aee5e1c4e22c45090ad1c19801ce1c541ff3902baea7a12dbcac6ec2d128ac7acb203463921ca6ce1d182d60d553ddecc4a3175eec2e924e9191e0d69aa49ca0b653495b8c62b802e443e669220f2a4047a56ed5cb7431a3387a435070795e6e63d242a74555e97371989c6d0040748e89ac316618d5d6eb7bff8d91e953afbe00464df4e4f6380c273b7ac5934cacb6c3be4da6649c8a5ea12bdf9afa1ec1e5053db7668c10ae2df75c4b3b14525369bf33741525a7630158aca3d3c7da5c1d71d3f63c0c2948a43236968d623c6c163eb757f0c78d6ae682ff4e4b673be07193e8d6c106c92851b393f0523491d5152e06de675fa22ae7bded329836a8ca0b955a59cb575395952c6ea0cb1644f2cada196b96b44ee12115ff9668e32886103d8f8109fac4a2287738ee1d2d4c1c19dab94eaa2757ea32016df60d7286099eb010ba570b5791ce4c54d860b15d56c53a5ddb543b0b602b3a87ca5213aa9647e51b1cb1736697506081240f4f7163a646f2be30e62c617d7572d820a113f96320834a2f43842160511923bd1ef4f723a2ab9b242fffe97cf0199c9f2ebca266a63beea0af279f2d18651b2ea9789d03025857c8aefc5f8bc6ee92ab7c6ffed2606f9c7ef25cb6c96140256a08cc58218869c52f3e5074fe5ace87a86057cc29fc845ff275f1b2dbc5991cbebe4c556b29ce3da3415958f0c9c68a5c664a1da60365ce56b9a4df24ec5d69c6f5a2e685885f6973c0ccd4caf7e000346bdea502b1418c8819e05bb7bee31bbd235819b49c892f7dadade576cb8ca68bda916a598c32ec03cceba2d01a7960f38680facccdaaf05e254dd8c3631a7222d1561c998c54b16f17a425f3247232dca035d545202e1b180340219ce31c66987bc86f983240e7fd2c926e8af77e6ea9f4139ff1edc0c323b67ceadead8f64a8004164d942f6aa65a28544d7205d41aafe62c5bfc6fd9fc322e2a8b620532d00d1add7a0d25a9dbaff90e19de14ac2ca97e83a2d77e63888ba0f7eaf9b18a7f2e5f148d16887580e55e894ae79024385f439acd071494e6469b77d07e763553652a470f3b4bd8bb7968052f3a969dacced51572bd125870849ce3e55359b2e17eabc3153b5f62868ea0ca1f40738488dbe0020c103f53678b9eb1c10000000000000000000000000000000000000000000000000000070a111518202731 >"$TMP/sig.hex" + expect_ok "tcId 1 must verify" "$BC_RUST" --hex-input mldsa87 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig.hex" <"$TMP/msg" >/dev/null + unhex 17a508179b35057099111733da28fd1a2265de7d8ab22d5279f13bca84cc42a5b8c9644c121e7e1b81723c5295be288fb6c36bfa188b6e08d913a152350947fa2c8ccc3fd01b319f65a2058a1dff54133946cfeb408d0b6dfde6bbebd7e0591cfe83b8b5452ceef6c855f7d33e06a0d269345089ed0d3ad67d84d8a4a34d16836004cff125469e8c3387abd788b620e30c1fc23909117a0e34c42a6631d9791347b1b2a3c9ab3082416211afb7bc3f6ce630a7019af19f736cdfacb1e7db66b65ef56844d2a2b0753d09283a7a0b66f77596384e95f7ceddd1c4ba20edc11f1eaab695bb963f6eda1c383754aa372a0d7729bfa6e0f142131c2367ba3f89ce3de6c357f9a7225b7cb85f6b3e8a3a122e8501fd1446b8152a415c19dda1d2e4590cd994f6664b4d1abd7381468c3a085abe2741a0cfbb81880664b271677245c4a471bf8bb8e0192eb32e4fb5e8560f3c50d6b19a353e486d0fcc2a35ac046286e707e095f61786d92212686a65d39b6863e0f8cec1e1997f2f845e4878ca9df650c746765296790863e51d012d32dffcbd746aa2276d04c0a57cd1b3d6ed06c0d66a0897aae5c49c97b6f19ae829baaafbfed28a52c05963c6eea9eff69528294207f8cda75280f7c486e6848791c8e37015479f2e13c28a9fe654dbde11689875203aaec51be3da7cab1cf31e4ec476c0c830cbdd04ac02167c0a6fbfdd6548b1fa525d235c7e3fca8d63e6427503b0a45c0bfddb428b837c32e8755441077bfe1c0142bac357b012a46545bf4148d465472dcf89c9d73b62357087e229f53a450d3cce41c8ee21a9d54b61e34a794f5b1406a70724ab0c3712c49df231ef30a956075e907c51b63dd1f9453dbe60e25b0f3cc0354dfd7c9119313919e77cb2c92f544d3e5302b8827603e936b567e99bfe9904932585a9f01a5a1b5bce07565f1d84c6b1c5c86259e1fefcff18cd06861122be6836be21e40be4eaf6bcabee8f634f95520aa914bb51c54dbd67d1b9dc5e38831e786c283979a963a3206b98e339edec4128b0502d4d47813869713e431a529a03c7f54b50123680f2b7f256f5d2b40642203259b9e85c62253d5670ce372193f28b5aa48ddd643c54756a2cff808c109f74772961d8db6bb8a17547c8f29c7f5ff3ea06740b867d84917e07f3978ad0281a20689eef58467e768b6178a9b36a567289fd39762bb3e4254031b2798a4550857f6af369d484392cddd7b48eaa2942e2cbfe754d5ee2da2b7fa71222e4a525ff5224d551a778ebd828e4e0499adc74ff0d59a5abc78ad6a8abafeedb3c99045a14423507f85597b1a7f540982f7d72ea13449110b442d54b78029b4c7fe3b49396dc6c3b7d58792538fa907963de10a4b724548142541cdf1512e0f7ff1b10a93de63541b8cc3268b4de20ed26739ee8973b6507ebe48965602c35fa3f7d4278146b598d7d7044e16e97e9351f7c51ac25573b7232ae2432638e9166190e7f7a7dcb5096ecb5d10017cdea2a82b4f56c7385041c6919a7e36e11beac77ec3f25df44e7b596c1542c1e376de3667c0e903fe25b57c338e9d93c5570c484f0ddab4f57d38f292b23599d9efc7a9fd9e078aaddca0acb1a196d6c45d3c8be6f39e8cdbe3299e370b262e0bf6fb5f005cae2b12879289d00bd8039de6a571c310d87557f5c9a4f64a0bde7177a8464722a04bf87fa2cb0e312d4fa6e536c61d65dc2c1baf144b0d1d1d75f4c860626ff773933efa9941d105c53a1d92c4f7c7bba4aa969590acef1e50901870f59715ac14d9846d83871a77367be57c63f88bc2c02eabafe678f44925a3e605979282fcd3f284736a1d346c033cb782dd615e886683fc37cd87a91422857774c63c6659096eba393c56225ed8c3485b4f89ecb07d53526281a6426ae7d67cda52fec5ac32320caae9b96000bcbe9e8782be88cb1ca6dcaffb74ef04c77e03a994bea2c89e4fcfa44cd0c9f4e30705a8b7b20df8c76b05a4479400e07db03d243e9fe4c90d34e9245f1e574be9a388f5355482077e4e98b919de024e666fdd7d51ed2a0d58a823e7497eb07303cf1d6d5f10a536be980220de5856727e5c13981839cfa19740988e7771a2b984f53ae3a5916ed881a4a90fe524f0bb3778355882864f8961fade32e656fcf9f524e748c8196a1f1bbc57bf8da7b36de9b0080f0c7bb8487a2b7bb7a81a8ff43a2539b367c9a48c70041520f05ca3dae316dbbe3118218216f52b7bcdba7557c4c9d861803a5e2ee01d3682e1261d7cae0a99fb8de909eb2bc1e112aa43cc2fa9c76a222bd85faaaba5d9ec2198ac45a295181a324a0592632b89e2752582cd5e01e1a610e7563faee10b76d853109e257e7c0c248a9fb7933f514b07b4f4e3a4a3d2cd22e8cc45ebda3bef5948aa050f01eff85ae98d19f69c51e67ff89f2df0c5268acfdd325e84591317e05cab4f9e6358f249c4ddf4019fbc8f511549a733898a50efa9e0793083de0b15b5bf78d9f63d8df830d42df2fefa27b89e0ede2a702eb9467118fc0ed44edc63ad1b1935877c34843fea06fdf388bbf83e501723a13cc6cc2efbb9691fe28fc1d45270591e5bdf7aa1c82673544ee29d9e6c9da3328f21e9729bffd7f4e56de585909679a74037105fdac3f51ae35f69d9763d2e4cfeb1d4a8fdce99bf1aa21f866a9f523b2a9549e12258a4d19900cf5db37b67da19b23563bd1d701c6106fccb28e4689c62e1a6cf1abd763d7239c2258b765610d4478be9f1650cb8d18923592ad0024076e52f9bd0a3894fe97bc0a1646b4c37f62c27f32d0df270260f47c49a5caf110e4cf80168a7d54b1c70bed9bd5d9a143ce869a05cd44ee266aecd6bfedb39be79e7c7d5c11a99575ebc0f389cc55a4fe1469a2d61b70bfe4b74e3e27521a037d2b9f4fdb377231e2ceb214ba90f6953865c683215203ce963875c6524c01b789e0389a9f0c386eb236f0dfba6c95df4f28ccc7ae7cd473f9dcd20817cccdd211bcbc78b064e936e4ba2813df531128428ddf410e6ca07044aeb4cfcc0a16c995ec51c8af16a541ce18dbeb69a26635632dcc24ee52a5eedce38c502cd0e356ec31341c893f92e6063c3a160a53d34b85e92357a8ebaaad8f206771be43ee48cc409825a7094bda529ee18776d9e67f1fa1c1419514309d70ba2443be2f63b6943478d6c0f56dd058731e53de4c30bfc7d915e9284a56248e81944392881666680d4991f04269ec9a83b24b458ed59a6c274de452ab3013c103a4920543e6a7d22dadfd764f6ea39d49b910ee0dc216e547aa5fb4382a72a568ebe83ec00416fb5830dc21c24ae72416602870cb52c3a8a1c4c12a4b287b9b800d31c287ca161f404a9e598a5358d28b3aae43e534846bcd0d7a9c7652ae01e6698c79e315aca8198f36de45af7084b1cb21ca2ba0ee3a547a7343a10ef9e3fd17b0a4060badd1409a0562cba25b84fd578268fac53cfbca08e6cf6e5419f57262eb5813c1d1324e0df1d483ade08d8f6c62498e262485ac7c2872b11b42e5c1b797fc12e838b38a711d364d45cd1ed35f7faffdf4b0fb0eaa312fc3d5af77909b0649cbbacea10c9831273922b5b05172face9ce6cf324edf6e2f5f5fa0a9f0463eee938b30adf3e55664f94d274cd87dea901a7e08e805 >"$TMP/pk.bin"; unhex ba4275ff54c22d2d09ea1937a0667362acd44925c6d6965fad350b111d1cbcce68ddbd0e576d1a8810eb4e71623781f32f747d44c8e693749df191682f588906949d97617a4b0ec54ad966818dee88b95f0f28ca24bfc5bfe0c316140b0662c43093ae48b899cc71e5739e9d67095ed987a79b6a0e7aac960c3c4125f0e92bc9435d10bfffae34bb3af05e977ebe0bafcbeb2381c5afe3379667b4c201aebf162dbd0a4bd1baa88fb2f88fa970499a848737d3cf94cc8ce278880a169cad91f304e4e8f1091d4cf39d9a3ab9f88dcc6f3bc4df311a5be0cba290365b3e879527e2a77f0cb6eccc9d85a5e592fb00f3a2e925a26d295a6b82746d7f534c83c35bc4826ee4910216b9a2867032698996fb0e1669b539ccd2ec74d181f4844e8f4d28f9c174316c12dadb54cb1dde7338238a20731c2565bb959f8e3086273ed03abc7ac515728750633083c0b397b29d385d13f5afd2529b32f00dce66c9dc8ea93d99c8b61c5e0ab2fc70de2a8dabdcaf290d53e8fca7561bda8c516ad475e4ec6c7cd2603aa3c8a71d9fa5dc7efc33cf318bebc1f1594e6ea25c69b8f9ce34a65b8ab0ae8dd3538bd267d86c584b8f354d7e4776ed4dd59a73f9e70a1df572f033b69b3eafa5a901e02515472e37258608875ca469de07db71cd6b8dc7edeb3d866ed2d219e44fcb133a066e89d8e3013569ca6f1fee7bf4ae56a6d32a5f3e5a530819c31aadabc8a88503edbea9cdfa3171762e1e8bcebaf9bb6af7e540102d5fc810bbcf1e02ae564e04d9dc55dab0a9392d6c95a317730d9793954da2cb16544d15403d0db01e85881e2d4f1b9b98458e1af0985f98b014f08f200558f2fed7a70c352a27423bedeebb3775c0a1ede9d461d2aea303c09c8b1f73fc37a5b3a01fb24a131574f7eac90c78baa38cebe81e2335dafda20299f76d6a0ba77d2954d11381674f4069f45e133886d64222d92583d5908e3ab6eb6a72cfb41f7dc3e71c1383888f61624bdcaf12fb716de98232ee329af1dd045f30d377234db7bfd11ddab0b3108329e16ce568c8bada39a98df5a5f72a72f063fa4253313a806013f61ce5adbe54cf42180ebf6e496bd4b42aeacf069aaa8e1c231fd037b394d78a69d1742b45bc5784ab8593a198077423c4c357d734f9899cfe9b3b62b6b9c3f4d781b8484a3fd0eea7e8d6945f4ba2b046fee079b7f032bebbc402918daa2aa1c9433dc3bcfbd7f49a5d7a293833c21c1bade3e8a7ec3c83d485529de51b5993cdffe23b770e25acab0ab9fb3059f14952d9464ebfa36a6274d8da317a7b07c2afe38ba28ca942cf7bfaee4020e59911c047b2aa24d1787baae1bc3546364b782358676d75c7698769f1d4a0d5dccade7fcf80e308437f7fc24fdfbf72625abf1b0f534da62cf860da1efd986950e3e19a085999f4d008fce38459c673befcdf2287c1766e106beb4f3175e97812da141330dcb2beb3265e38c8423c19dd50a655c9e8dc969f6367f3383b644d53a26875d53cea26de429266b506e70e7e6832886ed06d81738b0b482bd23bda396eb674ddfdb64803d6c4fae2f040170b5a28923279838b7b876220d02dc7478666f7c3287b1ba4a2f8228e8c491a55ba459805c601b986caea27ee9436f63383351c74d673643b15f6007fc6dc49e337a65a8a96ecd7eef4ad730bf3dc1972fc396703ee26af1156dc4beb46c47d99bb69ebdaf81c2d738d0f70e57a2c7162f5a55242255675f22082aa43f5b3dd862b535b2a15bb815b4e9faf16a302552cd6098d40930dfad7a7c609d6aeade814242a8bf0721c1c26d0d3daa7a638880a6411c6538d80c259d31b639aadaea49563a8d7f5ddb64291d6b086c80d72bfe7d26802cbd20fd6ba5495011e42da1c82483cbe8e37838e73c48f79f65b205476bee600399feda6aaaa939eee12cb34be4e6bdbd18032c85b54a2713551de677a16ca0142ff97b77bc963e8f3c57d91df9b49474b01ca514641842893abc3181caee3f49b635d17daf41bcc4aeb1116ec4b3e78ff1480cf3a5d9c6a154384b88516834d19196976e2ba97cee91f7a0d73f7f8146e58dc0fc8f510511d39d82a7ba531a4abcce6b624035d753a37c5980343cfc7724cf83efb0c33fc4abb5b002241bf57a46b67cb5a4cbd637b2f19bc93b368d97e13c6a62c8443c8222e0a90c3ed1972cc739b824fdf729ed8eca02ad96bd78bf6d2b3d2853e24fa93199ff41635176b31aab2207013d0a9317fa49668dfeb672b8129e6175a2998642ab8e74f0823e3b5480ae80180ca395f5348744a3c7b344891f008aab65914760b5fe852615ad6425216b1c5e777db1a46517cd01a77b277cfa2c6f250c2d68c495fef28feee6e0716817d6b30716f5ca48001805042133eca17a41c2219784ce0f12858ccfd371c77b90966ea04c3996851edf31aef962946628007de5531b06fddf3449f6c552eaf6e16b3e9160e265900b8c8e414732505e02660e123d45a3d6f1b15e56fd759d38e821e27e84967e95c4b0d2a48e008897e655d1b65b76299f1e3074209abe44b37f8786b02df23aa4dcebe512e6312f1a3d6d781d749635247aca897626f5ee688f517df6cfe947ddead820f07fed4bcbe7f1bee3f19117b5666dc123d528f2bf03db346d6afb53804b0681aea98a9479fc6de5ae974d2c0d2055f07ba8a1d2d8b6a7e08a6805bd8634cc190c4988b502f475c36d78c7b3dda0057c818835bbf21c5a7c13bb6fd91cd3cfd76abe4c2ff908a08a3000b9021500ff94b297cec0fb3e51b1cae7026ab4347bfd5b6a641a3d347f45a2a2aacd22e521e00da0c3986c67672c5d7d7e61e7edafc15d42aa42cb37fa8621f79e9096b092efe853ab318b3e4bbd90f20165a1be1d3aa5b3de43751b65abbd952599869778393c4351ab8f534fd49e16547df01c40bfaf56de60b4fb0019bc34177cc8e2236d219fb3c0b84dedae6b88e134280eabd1823420cd1afe6d929774967fd7d885fbad33d89ed9dc5e0eb978eab5d96c50bed5887aa8277880c7b06bf2780cee82ce639a1e3344c88a25102edcc17a4cb48989c4ad6a998726ede31deb0b98107f40858bad7864983f6ecdf0c9761a42751b19d5360daa7fddbbd2e2292638b95c763ca1e747eedef0dd387e9d9ec9e5afee207d8c45703c5befc2de3878d313655ce85ad984250cdf054360f33d41dc193060d42cf9528b1fb91d6ca3395199e25a1a7739eba9a6a4ac2c417ae615940b3eb1b746dd0ecc7b2f7acdff887110115629f70877dafcd7a6625fa1b9e256bc8fe1d66005dbcf12fde0a5fcda5d4f23d58ece91d60eb91274df8d9d17d4a39e63533acf1b317db979b04ca3ab9a0bcba652d0010ca3fcd33ed8f8a62faf42f78b37912d3adb410f20bf16b31359cacc25fb783083a3f065f0a2dd6fe58b8f594e11a87bf0f4c5f5493f334c18b03ebdefebce50228937ec13a8c221b617450486291071f3c14f64f66c927dd4bd623c214ac35433b8a6875cdf00916476eac0f196858aa1484bc1cd45b726d33a965619829b8deaa9d9fa0c3c210f23967ce26a4bdd939cff8aa662f70fb0af97ee44bcb9e2755000a195741d8919e4dcb1a5caae21009f686fa1489c72f16f9fee76b5410ece7f406947f4a19f394a5121da79f3216777b0fee5423328156ecf0b4548dbd5b3f7b526d6b9cfd57576f67dd521c314c2d37474ed0cf732c3b073a101c735a4c4e6b33c9aaa12c91d147ad1075a20287d36c388657614a9c648d8e49cce8cfa282b2a2e8e9da6e4444e4b6aed1bd0ada5009a335cd0500bdae9a01b97f7e8cfe8372398e750de92ee4a524393a19826d19de5e762fb53a88b9b4657e9c6d7d01a4124e3e39532f614aec5cb88d982b78b2ff568017c92f6a1ce5298b5f323b5ee61695038ef0c3a7a339cfac31cf7875a4563046a40e7b35cef1d37d811b342fcbb5373122415befc23cb656a619f7c262c443403b23ba20e341a079918dec6f4f801b92781179ad7ac1951f39ddb0b1f1fb95b28c0f4593a04486f0e0e86bb3b014674879aad10f41e0bad34d40bc817b6fd43c1dde8547882d82e5111e208107e9c9736d16ca77ac7453d7b6c7976a7dd6a4c6c0252185bc9948660a07b66151b09b980d8572ae829ab2f6d900cc63066c4ecb3a0317e8a9acaa8e22a216721f3d69c67843df2fbedd88bc424f761cdf420de4ff2c55b6925b1826c1c4134090d2aae82a7f64a8d428efe1b21097a6fdbc9b8a31c4d46c7b32d478bf5b9181bef8af4d3486958d7c198c46a5ad771090bb64e7fc2bd7677ae7618b861c83c13177075063517eff40cfd52ae56650e9e7035f783bfe8920c754e98470b327909ac2a407e07864710544b8adbc075502c75b5f0fdbca0cde5d94b013141c96b55ae1b60ab63f9e792641690af41b05a78233935fd7f82852ebb2602a10459a709256c41d108c2aeb71925089cc79b121eabd5edc54f3f7c8929685b88ab29700c7fb2f247cd7962c8ce6dc9f79f9358d6a7989d24ccc17d0dbaa0b7fbe73479164181ae7d6b6a02678cce46f74ea2bd387ecb73041817b429a0220e1c3635fe492f5f3a8e2b65c086fe24c375563d7220856dd8f970716d548492964a156554dc88810c3c4f81dcc3ae80243e19679a3be9c9b1b8c707b416e9166c54a568bb9d84728c1a283d9231a12b13688ce2362901342652d66bf44cec223f561d2735d977c6adcf57e9066220d0770fc77fdf6ff367a2f36ae20062887beb88d1b453f267aef9bc763ab716bacb9214c38b95f4f2f3f6c236aa71aef83c1ae4b26133678884476c1d7c6d3e7b99f13e028ac6cebddfae4793f9ae975f3d66b725b500e7c7d2f664eaa0c358deb91cb2d6173394b306749d2bfb2684b985769bdb682e922b75555d38dad1899057a64ef6ea361e4712244d02d0dfec8c3d40770122770c2538d6a14a8462ef18eb705c16e5ba30aa5366447c94869060e4df155f7d01ffda04c1ced0ad5fbe5fdd85856e1e49320319687d31a6e4c7145479f45f43a9b8e9ffe4cbacfad4a21e5445e119df2996cce8b11d0f224efb4f18b544d456e2fb1d96fcd99fc319dbd86720621ac25b490f3611f7e5655bb3940a503c07dbf41f4b87593595a66008808744668371a54ce1b9dfdaa16f90415e57470bc23898d13d8351ba34369e96347da13d012b4eab32ada90668654c5ed2b433716b1b4170f640cf4b40659efcf4150237bbc25f72b248be85cd482b55ce2f5f73e8f02efd2465805b37d12487465ec1085bc6602b71862771af13e13baece4916b1ddef9ce016c92db9fb9e82aee5e1c4e22c45090ad1c19801ce1c541ff3902baea7a12dbcac6ec2d128ac7acb203463921ca6ce1d182d60d553ddecc4a3175eec2e924e9191e0d69aa49ca0b653495b8c62b802e443e669220f2a4047a56ed5cb7431a3387a435070795e6e63d242a74555e97371989c6d0040748e89ac316618d5d6eb7bff8d91e953afbe00464df4e4f6380c273b7ac5934cacb6c3be4da6649c8a5ea12bdf9afa1ec1e5053db7668c10ae2df75c4b3b14525369bf33741525a7630158aca3d3c7da5c1d71d3f63c0c2948a43236968d623c6c163eb757f0c78d6ae682ff4e4b673be07193e8d6c106c92851b393f0523491d5152e06de675fa22ae7bded329836a8ca0b955a59cb575395952c6ea0cb1644f2cada196b96b44ee12115ff9668e32886103d8f8109fac4a2287738ee1d2d4c1c19dab94eaa2757ea32016df60d7286099eb010ba570b5791ce4c54d860b15d56c53a5ddb543b0b602b3a87ca5213aa9647e51b1cb1736697506081240f4f7163a646f2be30e62c617d7572d820a113f96320834a2f43842160511923bd1ef4f723a2ab9b242fffe97cf0199c9f2ebca266a63beea0af279f2d18651b2ea9789d03025857c8aefc5f8bc6ee92ab7c6ffed2606f9c7ef25cb6c96140256a08cc58218869c52f3e5074fe5ace87a86057cc29fc845ff275f1b2dbc5991cbebe4c556b29ce3da3415958f0c9c68a5c664a1da60365ce56b9a4df24ec5d69c6f5a2e685885f6973c0ccd4caf7e000346bdea502b1418c8819e05bb7bee31bbd235819b49c892f7dadade576cb8ca68bda916a598c32ec03cceba2d01a7960f38680facccdaaf05e254dd8c3631a7222d1561c998c54b16f17a425f3247232dca035d545202e1b180340219ce31c66987bc86f983240e7fd2c926e8af77e6ea9f4139ff1edc0c323b67ceadead8f64a8004164d942f6aa65a28544d7205d41aafe62c5bfc6fd9fc322e2a8b620532d00d1add7a0d25a9dbaff90e19de14ac2ca97e83a2d77e63888ba0f7eaf9b18a7f2e5f148d16887580e55e894ae79024385f439acd071494e6469b77d07e763553652a470f3b4bd8bb7968052f3a969dacced51572bd125870849ce3e55359b2e17eabc3153b5f62868ea0ca1f40738488dbe0020c103f53678b9eb1c10000000000000000000000000000000000000000000000000000070a111518202731 >"$TMP/sig.bin" + expect_ok "tcId 1 must verify from binary files" "$BC_RUST" mldsa87 verify --pkfile "$TMP/pk.bin" --sigfile "$TMP/sig.bin" <"$TMP/msg" >/dev/null + printf '%s' ba4275ff54c22d2d09ea1937a0667362acd44925c6d6965fad350b111d1cbcce68ddbd0e576d1a8810eb4e71623781f32f747d44c8e693749df191682f588906949d97617a4b0ec54ad966818dee88b95f0f28ca24bfc5bfe0c316140b0662c43093ae48b899cc71e5739e9d67095ed987a79b6a0e7aac960c3c4125f0e92bc9435d10bfffae34bb3af05e977ebe0bafcbeb2381c5afe3379667b4c201aebf162dbd0a4bd1baa88fb2f88fa970499a848737d3cf94cc8ce278880a169cad91f304e4e8f1091d4cf39d9a3ab9f88dcc6f3bc4df311a5be0cba290365b3e879527e2a77f0cb6eccc9d85a5e592fb00f3a2e925a26d295a6b82746d7f534c83c35bc4826ee4910216b9a2867032698996fb0e1669b539ccd2ec74d181f4844e8f4d28f9c174316c12dadb54cb1dde7338238a20731c2565bb959f8e3086273ed03abc7ac515728750633083c0b397b29d385d13f5afd2529b32f00dce66c9dc8ea93d99c8b61c5e0ab2fc70de2a8dabdcaf290d53e8fca7561bda8c516ad475e4ec6c7cd2603aa3c8a71d9fa5dc7efc33cf318bebc1f1594e6ea25c69b8f9ce34a65b8ab0ae8dd3538bd267d86c584b8f354d7e4776ed4dd59a73f9e70a1df572f033b69b3eafa5a901e02515472e37258608875ca469de07db71cd6b8dc7edeb3d866ed2d219e44fcb133a066e89d8e3013569ca6f1fee7bf4ae56a6d32a5f3e5a530819c31aadabc8a88503edbea9cdfa3171762e1e8bcebaf9bb6af7e540102d5fc810bbcf1e02ae564e04d9dc55dab0a9392d6c95a317730d9793954da2cb16544d15403d0db01e85881e2d4f1b9b98458e1af0985f98b014f08f200558f2fed7a70c352a27423bedeebb3775c0a1ede9d461d2aea303c09c8b1f73fc37a5b3a01fb24a131574f7eac90c78baa38cebe81e2335dafda20299f76d6a0ba77d2954d11381674f4069f45e133886d64222d92583d5908e3ab6eb6a72cfb41f7dc3e71c1383888f61624bdcaf12fb716de98232ee329af1dd045f30d377234db7bfd11ddab0b3108329e16ce568c8bada39a98df5a5f72a72f063fa4253313a806013f61ce5adbe54cf42180ebf6e496bd4b42aeacf069aaa8e1c231fd037b394d78a69d1742b45bc5784ab8593a198077423c4c357d734f9899cfe9b3b62b6b9c3f4d781b8484a3fd0eea7e8d6945f4ba2b046fee079b7f032bebbc402918daa2aa1c9433dc3bcfbd7f49a5d7a293833c21c1bade3e8a7ec3c83d485529de51b5993cdffe23b770e25acab0ab9fb3059f14952d9464ebfa36a6274d8da317a7b07c2afe38ba28ca942cf7bfaee4020e59911c047b2aa24d1787baae1bc3546364b782358676d75c7698769f1d4a0d5dccade7fcf80e308437f7fc24fdfbf72625abf1b0f534da62cf860da1efd986950e3e19a085999f4d008fce38459c673befcdf2287c1766e106beb4f3175e97812da141330dcb2beb3265e38c8423c19dd50a655c9e8dc969f6367f3383b644d53a26875d53cea26de429266b506e70e7e6832886ed06d81738b0b482bd23bda396eb674ddfdb64803d6c4fae2f040170b5a28923279838b7b876220d02dc7478666f7c3287b1ba4a2f8228e8c491a55ba459805c601b986caea27ee9436f63383351c74d673643b15f6007fc6dc49e337a65a8a96ecd7eef4ad730bf3dc1972fc396703ee26af1156dc4beb46c47d99bb69ebdaf81c2d738d0f70e57a2c7162f5a55242255675f22082aa43f5b3dd862b535b2a15bb815b4e9faf16a302552cd6098d40930dfad7a7c609d6aeade814242a8bf0721c1c26d0d3daa7a638880a6411c6538d80c259d31b639aadaea49563a8d7f5ddb64291d6b086c80d72bfe7d26802cbd20fd6ba5495011e42da1c82483cbe8e37838e73c48f79f65b205476bee600399feda6aaaa939eee12cb34be4e6bdbd18032c85b54a2713551de677a16ca0142ff97b77bc963e8f3c57d91df9b49474b01ca514641842893abc3181caee3f49b635d17daf41bcc4aeb1116ec4b3e78ff1480cf3a5d9c6a154384b88516834d19196976e2ba97cee91f7a0d73f7f8146e58dc0fc8f510511d39d82a7ba531a4abcce6b624035d753a37c5980343cfc7724cf83efb0c33fc4abb5b002241bf57a46b67cb5a4cbd637b2f19bc93b368d97e13c6a62c8443c8222e0a90c3ed1972cc739b824fdf729ed8eca02ad96bd78bf6d2b3d2853e24fa93199ff41635176b31aab2207013d0a9317fa49668dfeb672b8129e6175a2998642ab8e74f0823e3b5480ae80180ca395f5348744a3c7b344891f008aab65914760b5fe852615ad6425216b1c5e777db1a46517cd01a77b277cfa2c6f250c2d68c495fef28feee6e0716817d6b30716f5ca48001805042133eca17a41c2219784ce0f12858ccfd371c77b90966ea04c3996851edf31aef962946628007de5531b06fddf3449f6c552eaf6e16b3e9160e265900b8c8e414732505e02660e123d45a3d6f1b15e56fd759d38e821e27e84967e95c4b0d2a48e008897e655d1b65b76299f1e3074209abe44b37f8786b02df23aa4dcebe512e6312f1a3d6d781d749635247aca897626f5ee688f517df6cfe947ddead820f07fed4bcbe7f1bee3f19117b5666dc123d528f2bf03db346d6afb53804b0681aea98a9479fc6de5ae974d2c0d2055f07ba8a1d2d8b6a7e08a6805bd8634cc190c4988b502f475c36d78c7b3dda0057c818835bbf21c5a7c13bb6fd91cd3cfd76abe4c2ff908a08a3000b9021500ff94b297cec0fb3e51b1cae7026ab4347bfd5b6a641a3d347f45a2a2aacd22e521e00da0c3986c67672c5d7d7e61e7edafc15d42aa42cb37fa8621f79e9096b092efe853ab318b3e4bbd90f20165a1be1d3aa5b3de43751b65abbd952599869778393c4351ab8f534fd49e16547df01c40bfaf56de60b4fb0019bc34177cc8e2236d219fb3c0b84dedae6b88e134280eabd1823420cd1afe6d929774967fd7d885fbad33d89ed9dc5e0eb978eab5d96c50bed5887aa8277880c7b06bf2780cee82ce639a1e3344c88a25102edcc17a4cb48989c4ad6a998726ede31deb0b98107f40858bad7864983f6ecdf0c9761a42751b19d5360daa7fddbbd2e2292638b95c763ca1e747eedef0dd387e9d9ec9e5afee207d8c45703c5befc2de3878d313655ce85ad984250cdf054360f33d41dc193060d42cf9528b1fb91d6ca3395199e25a1a7739eba9a6a4ac2c417ae615940b3eb1b746dd0ecc7b2f7acdff887110115629f70877dafcd7a6625fa1b9e256bc8fe1d66005dbcf12fde0a5fcda5d4f23d58ece91d60eb91274df8d9d17d4a39e63533acf1b317db979b04ca3ab9a0bcba652d0010ca3fcd33ed8f8a62faf42f78b37912d3adb410f20bf16b31359cacc25fb783083a3f065f0a2dd6fe58b8f594e11a87bf0f4c5f5493f334c18b03ebdefebce50228937ec13a8c221b617450486291071f3c14f64f66c927dd4bd623c214ac35433b8a6875cdf00916476eac0f196858aa1484bc1cd45b726d33a965619829b8deaa9d9fa0c3c210f23967ce26a4bdd939cff8aa662f70fb0af97ee44bcb9e2755000a195741d8919e4dcb1a5caae21009f686fa1489c72f16f9fee76b5410ece7f406947f4a19f394a5121da79f3216777b0fee5423328156ecf0b4548dbd5b3f7b526d6b9cfd57576f67dd521c314c2d37474ed0cf732c3b073a101c735a4c4e6b33c9aaa12c91d147ad1075a20287d36c388657614a9c648d8e49cce8cfa282b2a2e8e9da6e4444e4b6aed1bd0ada5009a335cd0500bdae9a01b97f7e8cfe8372398e750de92ee4a524393a19826d19de5e762fb53a88b9b4657e9c6d7d01a4124e3e39532f614aec5cb88d982b78b2ff568017c92f6a1ce5298b5f323b5ee61695038ef0c3a7a339cfac31cf7875a4563046a40e7b35cef1d37d811b342fcbb5373122415befc23cb656a619f7c262c443403b23ba20e341a079918dec6f4f801b92781179ad7ac1951f39ddb0b1f1fb95b28c0f4593a04486f0e0e86bb3b014674879aad10f41e0bad34d40bc817b6fd43c1dde8547882d82e5111e208107e9c9736d16ca77ac7453d7b6c7976a7dd6a4c6c0252185bc9948660a07b66151b09b980d8572ae829ab2f6d900cc63066c4ecb3a0317e8a9acaa8e22a216721f3d69c67843df2fbedd88bc424f761cdf420de4ff2c55b6925b1826c1c4134090d2aae82a7f64a8d428efe1b21097a6fdbc9b8a31c4d46c7b32d478bf5b9181bef8af4d3486958d7c198c46a5ad771090bb64e7fc2bd7677ae7618b861c83c13177075063517eff40cfd52ae56650e9e7035f783bfe8920c754e98470b327909ac2a407e07864710544b8adbc075502c75b5f0fdbca0cde5d94b013141c96b55ae1b60ab63f9e792641690af41b05a78233935fd7f82852ebb2602a10459a709256c41d108c2aeb71925089cc79b121eabd5edc54f3f7c8929685b88ab29700c7fb2f247cd7962c8ce6dc9f79f9358d6a7989d24ccc17d0dbaa0b7fbe73479164181ae7d6b6a02678cce46f74ea2bd387ecb73041817b429a0220e1c3635fe492f5f3a8e2b65c086fe24c375563d7220856dd8f970716d548492964a156554dc88810c3c4f81dcc3ae80243e19679a3be9c9b1b8c707b416e9166c54a568bb9d84728c1a283d9231a12b13688ce2362901342652d66bf44cec223f561d2735d977c6adcf57e9066220d0770fc77fdf6ff367a2f36ae20062887beb88d1b453f267aef9bc763ab716bacb9214c38b95f4f2f3f6c236aa71aef83c1ae4b26133678884476c1d7c6d3e7b99f13e028ac6cebddfae4793f9ae975f3d66b725b500e7c7d2f664eaa0c358deb91cb2d6173394b306749d2bfb2684b985769bdb682e922b75555d38dad1899057a64ef6ea361e4712244d02d0dfec8c3d40770122770c2538d6a14a8462ef18eb705c16e5ba30aa5366447c94869060e4df155f7d01ffda04c1ced0ad5fbe5fdd85856e1e49320319687d31a6e4c7145479f45f43a9b8e9ffe4cbacfad4a21e5445e119df2996cce8b11d0f224efb4f18b544d456e2fb1d96fcd99fc319dbd86720621ac25b490f3611f7e5655bb3940a503c07dbf41f4b87593595a66008808744668371a54ce1b9dfdaa16f90415e57470bc23898d13d8351ba34369e96347da13d012b4eab32ada90668654c5ed2b433716b1b4170f640cf4b40659efcf4150237bbc25f72b248be85cd482b55ce2f5f73e8f02efd2465805b37d12487465ec1085bc6602b71862771af13e13baece4916b1ddef9ce016c92db9fb9e82aee5e1c4e22c45090ad1c19801ce1c541ff3902baea7a12dbcac6ec2d128ac7acb203463921ca6ce1d182d60d553ddecc4a3175eec2e924e9191e0d69aa49ca0b653495b8c62b802e443e669220f2a4047a56ed5cb7431a3387a435070795e6e63d242a74555e97371989c6d0040748e89ac316618d5d6eb7bff8d91e953afbe00464df4e4f6380c273b7ac5934cacb6c3be4da6649c8a5ea12bdf9afa1ec1e5053db7668c10ae2df75c4b3b14525369bf33741525a7630158aca3d3c7da5c1d71d3f63c0c2948a43236968d623c6c163eb757f0c78d6ae682ff4e4b673be07193e8d6c106c92851b393f0523491d5152e06de675fa22ae7bded329836a8ca0b955a59cb575395952c6ea0cb1644f2cada196b96b44ee12115ff9668e32886103d8f8109fac4a2287738ee1d2d4c1c19dab94eaa2757ea32016df60d7286099eb010ba570b5791ce4c54d860b15d56c53a5ddb543b0b602b3a87ca5213aa9647e51b1cb1736697506081240f4f7163a646f2be30e62c617d7572d820a113f96320834a2f43842160511923bd1ef4f723a2ab9b242fffe97cf0199c9f2ebca266a63beea0af279f2d18651b2ea9789d03025857c8aefc5f8bc6ee92ab7c6ffed2606f9c7ef25cb6c96140256a08cc58218869c52f3e5074fe5ace87a86057cc29fc845ff275f1b2dbc5991cbebe4c556b29ce3da3415958f0c9c68a5c664a1da60365ce56b9a4df24ec5d69c6f5a2e685885f6973c0ccd4caf7e000346bdea502b1418c8819e05bb7bee31bbd235819b49c892f7dadade576cb8ca68bda916a598c32ec03cceba2d01a7960f38680facccdaaf05e254dd8c3631a7222d1561c998c54b16f17a425f3247232dca035d545202e1b180340219ce31c66987bc86f983240e7fd2c926e8af77e6ea9f4139ff1edc0c323b67ceadead8f64a8004164d942f6aa65a28544d7205d41aafe62c5bfc6fd9fc322e2a8b620532d00d1add7a0d25a9dbaff90e19de14ac2ca97e83a2d77e63888ba0f7eaf9b18a7f2e5f148d16887580e55e894ae79024385f439acd071494e6469b77d07e763553652a470f3b4bd8bb7968052f3a969dacced51572bd125870849ce3e55359b2e17eabc3153b5f62868ea0ca1f40738488dbe0020c103f53678b9eb1c10000000000000000000000000000000000000000000000000000070a1115182027 >"$TMP/sig-bad.hex" + expect_fail "tcId 6 must not verify" "$BC_RUST" --hex-input mldsa87 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig-bad.hex" <"$TMP/msg" + assert_stderr_has "invalid" + expect_fail "another message must not verify" "$BC_RUST" --hex-input mldsa87 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig.hex" < <(printf 'Hello World') +} + +# Signing is randomized (FIPS 204 Alg 2 draws rnd), so there is no signature known answer; the +# round trip pins the wiring: sign with the sk, verify with the derived pk, with and without a +# context, in the pure and the HashML-DSA (SHA-512) forms, and each form rejects the other's +# signature and the other context. +test_mldsa87_sign_verify_round_trip() { + "$BC_RUST" mldsa87 keygen >"$TMP/sk" + assert_size "$TMP/sk" 4896 "sk length of ML-DSA-87" + "$BC_RUST" mldsa87 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + rng 3000 >"$TMP/msg" + "$BC_RUST" mldsa87 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig" + assert_size "$TMP/sig" 4627 "signature length of ML-DSA-87" + expect_ok "pure signature verifies" "$BC_RUST" mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" >/dev/null + printf 'ctx' >"$TMP/ctx" + expect_fail "a context the signer did not use must not verify" "$BC_RUST" mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" --ctxfile "$TMP/ctx" <"$TMP/msg" + "$BC_RUST" mldsa87 sign --skfile "$TMP/sk" --ctxfile "$TMP/ctx" <"$TMP/msg" >"$TMP/sig-ctx" + expect_ok "signature with context verifies with it" "$BC_RUST" mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ctx" --ctxfile "$TMP/ctx" <"$TMP/msg" >/dev/null + expect_fail "and not without it" "$BC_RUST" mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ctx" <"$TMP/msg" + "$BC_RUST" hash-mldsa87 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig-ph" + expect_ok "HashML-DSA signature verifies as HashML-DSA" "$BC_RUST" hash-mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ph" <"$TMP/msg" >/dev/null + expect_fail "and not as pure ML-DSA" "$BC_RUST" mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ph" <"$TMP/msg" + expect_fail "nor the pure one as HashML-DSA" "$BC_RUST" hash-mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" +} + +# FIPS 204 Alg 2 step 1: a context longer than 255 bytes is rejected. The CLI must report it +# (exit 255, an `Error:` line), not panic; and a seed of the wrong length likewise, for every +# parameter set and both forms. +test_bad_inputs_are_errors_not_panics() { + local cmd + head -c 256 /dev/zero >"$TMP/ctx256" + head -c 255 /dev/zero >"$TMP/ctx255" + rng 31 >"$TMP/seed31" + for cmd in mldsa44 mldsa65 mldsa87 hash-mldsa44 hash-mldsa65 hash-mldsa87; do + "$BC_RUST" $cmd keygen >"$TMP/sk" + expect_ok "$cmd: a 255-byte context is allowed" "$BC_RUST" $cmd sign --skfile "$TMP/sk" --ctxfile "$TMP/ctx255" /dev/null + expect_fail "$cmd: a 256-byte context" "$BC_RUST" $cmd sign --skfile "$TMP/sk" --ctxfile "$TMP/ctx256" 255 must not panic: $(head -1 "$LAST_STDERR")" + assert_stderr_has "Error" + expect_fail "$cmd: a 31-byte seed" "$BC_RUST" $cmd keygen-from-seed <"$TMP/seed31" + grep -q panicked "$LAST_STDERR" && fail "$cmd: a bad seed must not panic: $(head -1 "$LAST_STDERR")" + assert_stderr_has "Error" + done +} + +# A signature file of the wrong length, a truncated public key and a missing file are errors. +test_bad_files_are_errors() { + "$BC_RUST" mldsa44 keygen >"$TMP/sk" + "$BC_RUST" mldsa44 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + printf 'hello' | "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" >"$TMP/sig" + head -c 100 "$TMP/sig" >"$TMP/sig-short" + expect_fail "short signature" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-short" < <(printf 'hello') + assert_stderr_has "invalid" + head -c 100 "$TMP/pk" >"$TMP/pk-short" + expect_fail "short pk" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk-short" --sigfile "$TMP/sig" < <(printf 'hello') + assert_stderr_has "Error" + expect_fail "missing sig file" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/nope" < <(printf 'hello') + assert_stderr_has "Error" +} + +# FIPS 204 Section 3.4: the deterministic variant signs with rnd all zero, so two signatures of +# one message agree, and it verifies like any other; the hedged default draws rnd, so two differ. +test_deterministic_signatures_repeat_and_hedged_ones_do_not() { + "$BC_RUST" mldsa44 keygen >"$TMP/sk" + "$BC_RUST" mldsa44 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + rng 100 >"$TMP/msg" + "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" --deterministic <"$TMP/msg" >"$TMP/sig1" + "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" --deterministic <"$TMP/msg" >"$TMP/sig2" + assert_same "$TMP/sig1" "$TMP/sig2" "deterministic signatures must repeat" + expect_ok "a deterministic signature verifies" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig1" <"$TMP/msg" >/dev/null + "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig3" + "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig4" + assert_differs "$TMP/sig3" "$TMP/sig4" "hedged signatures must differ" + "$BC_RUST" hash-mldsa44 sign --skfile "$TMP/sk" --deterministic <"$TMP/msg" >"$TMP/sig5" + "$BC_RUST" hash-mldsa44 sign --skfile "$TMP/sk" --deterministic <"$TMP/msg" >"$TMP/sig6" + assert_same "$TMP/sig5" "$TMP/sig6" "deterministic HashML-DSA signatures must repeat" +} + +# The external-mu interface: compute-mu gives the 64-byte representative of FIPS 204 Algorithm 7 +# line 6, sign-mu signs it, and the result verifies both as an ordinary signature over the message +# and with verify-mu; a context is part of mu, so the wrong one fails. +test_external_mu_round_trip() { + "$BC_RUST" mldsa65 keygen >"$TMP/sk" + "$BC_RUST" mldsa65 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + rng 1000 >"$TMP/msg" + printf 'ctx' >"$TMP/ctx" + "$BC_RUST" mldsa65 compute-mu --pkfile "$TMP/pk" --ctxfile "$TMP/ctx" <"$TMP/msg" >"$TMP/mu" + assert_size "$TMP/mu" 64 "mu length" + "$BC_RUST" mldsa65 sign-mu --skfile "$TMP/sk" --mufile "$TMP/mu" "$TMP/sig" + assert_size "$TMP/sig" 3309 "signature length of ML-DSA-65" + expect_ok "a mu signature verifies over the message" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" --ctxfile "$TMP/ctx" <"$TMP/msg" >/dev/null + expect_ok "and with verify-mu" "$BC_RUST" mldsa65 verify-mu --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/mu" >/dev/null + expect_fail "not under another context" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" + "$BC_RUST" mldsa65 sign-mu --skfile "$TMP/sk" --deterministic <"$TMP/mu" >"$TMP/sig-det" + expect_ok "a deterministic mu signature verifies" "$BC_RUST" mldsa65 verify-mu --pkfile "$TMP/pk" --sigfile "$TMP/sig-det" --mufile "$TMP/mu" /dev/null + expect_fail "mu is 64 bytes" "$BC_RUST" mldsa65 sign-mu --skfile "$TMP/sk" < <(rng 63) + assert_stderr_has "64 bytes" + expect_fail "the mu actions are not HashML-DSA's" "$BC_RUST" hash-mldsa65 compute-mu --pkfile "$TMP/pk" <"$TMP/msg" + assert_stderr_has "hash-mldsa" +} + +# FIPS 204 Section 5.4: the pre-hash must be at least as strong as the parameter set. --hash +# selects it; sha256 and shake128 (RFC 8702's 32-byte digest) fit hash-mldsa44 only, sha512 and +# shake256 fit all three, and a signature only verifies under the hash it was made with. +test_hash_mldsa_pre_hash_choice() { + local h + "$BC_RUST" mldsa44 keygen >"$TMP/sk" + "$BC_RUST" mldsa44 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + rng 500 >"$TMP/msg" + for h in sha256 sha512 shake128 shake256; do + "$BC_RUST" hash-mldsa44 sign --skfile "$TMP/sk" --hash $h <"$TMP/msg" >"$TMP/sig-$h" + expect_ok "hash-mldsa44 with $h verifies" "$BC_RUST" hash-mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-$h" --hash $h <"$TMP/msg" >/dev/null + done + expect_fail "a sha256 signature is not a sha512 one" "$BC_RUST" hash-mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-sha256" <"$TMP/msg" + "$BC_RUST" mldsa65 keygen >"$TMP/sk65" + "$BC_RUST" mldsa65 pk-from-sk --skfile "$TMP/sk65" >"$TMP/pk65" + for h in sha256 shake128; do + expect_fail "hash-mldsa65 refuses $h" "$BC_RUST" hash-mldsa65 sign --skfile "$TMP/sk65" --hash $h <"$TMP/msg" + assert_stderr_has "PreHashTooWeak" + expect_fail "and on verify" "$BC_RUST" hash-mldsa65 verify --pkfile "$TMP/pk65" --sigfile "$TMP/sig-sha256" --hash $h <"$TMP/msg" + assert_stderr_has "PreHashTooWeak" + done + "$BC_RUST" hash-mldsa65 sign --skfile "$TMP/sk65" --hash shake256 <"$TMP/msg" >"$TMP/sig65" + expect_ok "hash-mldsa65 with shake256 verifies" "$BC_RUST" hash-mldsa65 verify --pkfile "$TMP/pk65" --sigfile "$TMP/sig65" --hash shake256 <"$TMP/msg" >/dev/null +} + +run_all diff --git a/cli/tests/test_mlkem.sh b/cli/tests/test_mlkem.sh new file mode 100755 index 000000000..33acfd26b --- /dev/null +++ b/cli/tests/test_mlkem.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash +# The mlkem512 / mlkem768 / mlkem1024 subcommands against the ACVP ML-KEM known answers. +# +# Written for the 2026-11-03 CLI review (report 15) from the vector files named in each test; +# every expected value is copied from that file, none is typed by hand. Follows lib.sh's +# conventions: each test_* runs in its own subshell under `set -e`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# ACVP ML-KEM keyGen (bc-test-data/pqc/crypto/mlkem/ML-KEM-keyGen.txt, ML-KEM-512 tcId 1): the +# 64-byte seed is d || z (FIPS 203 Alg 19 draws d then z; Alg 16 takes (d, z)); `keygen-from-seed` +# must give the vector's dk and `pk-from-sk` its ek, from the seed as raw bytes and as hex. +test_mlkem512_keygen_from_seed_matches_acvp() { + local got + unhex 8A3D5F46A59A1B14E87C963A355D12D353074754FF15000172E6C8AE00A3AC1D5449ACE5765E87F252DD14DED32FD1973DF5B5DE53751A3723D5A52C8C6DF2FD >"$TMP/seed" + assert_size "$TMP/seed" 64 "seed length" + got=$(hex_out "$BC_RUST" mlkem512 keygen-from-seed -x <"$TMP/seed") + assert_eq "$got" "d2facfe0ab6bc16109fc167cabb16605052fd5e70b757ba6c3d70712929716d2be43b2c7baa2631b4c374218899a0ca1e3b370d518aa992b37cda8767a8728fc51a43abc2728775a82674fc5eabf6e79218864395b6cce4531983d88874890682c9c41e302ab6882bfa94c37dea34fc0d977d5a7cca52420396cbffc89a562c47cfe858360d0cf3d89349df0a4da26bbdbcc28cda811a885423dd130a3cc78ffe85257e030298a4c5c2c6d22924802a72e722c079b10b0c0806ca62c667f533c305b8445b8a64d8cbe05111d1ae798fa4816293a61a01975105b64318932b5c8c83e805e2a3678271c77b4918f8caa15063b9b4e264615ba32a3db4e3510714b566375396926e873693743bde8a4c6280f66054df9f2930d045bdebcb1752581a10241024558e6163b6324a7c13784e3d28e78352164ab137a54a3a5650eb2399c20d14c1f0ccca4cc3aae37217b791aae8c74fa085e72249313e8746de6316b664262a253f0a100caf38de2889a465264bdca258bf28ed660c6f27a93c6d6c6d9f9c10be80605126ff17a1b5ee3b2c25c76ce58ab21450cbac36aed192cc096006b5c0ca43640d11aa9b1587dedb06506c9c2dd882d30b73df318a57c09af614a4cd066586319771a106146469e21526a41b4a3a4c3386e490160ca9d2ab55ea6d401bcd01fa4a949283a878ae2105bc11cfd705e231018e4fc84c4e24bee263393d76b5cdb6309841aaefb609149af19b492ec4628fe6090ee5349eac519e1a226050244f70c5dd87b1a8c8c11c00ccf8d3844557c1333a02dbea5a690ac367c325094c53945fa31b168508951a19547bc422bcbd02a8db8ca9366cb617ad13236607968c93aab0c78e50217c96a0f76d64db09b4deca4a3aa574ef9210c3a6c7b21d5729ee07c781378680648a24aa3bef461e6ca012df9be515856dadb67d7835edeac4610c58f38fb4115831dbf29ba1aa980b7eacc7c430f70970256b1029ef27b6ca4a61f5093326ac4128a0536cc1bbb005b1158b0322899759761e0c5b9752c045c2cc3ffd81e526b86f0b03ca42355daf5a7f1c19a0dd517eb1744084c1a6f228372bb63e07632561746cd310ba0c09fcfac838556bf598168ee7a9e784c0f428a5aa6685bb23574d9d1330d2aaab4aa5cd3da3c90a9557140af72e813e17868e60b5e80d3c1afa7ae32a141a6a48fb05878f9b7cfa6e4939c4c8c240a8e337579e7958c59605f82e4088b705bf530451a968ff83c388615bf8c0c3470e52010e1226fc83c22d64167b35e0b946ddf40a17be90ac062c5d4a8587711c192b096adc64962dc99a3aa260d19143c99919e6bb7b6e129ff288b121b96d25b7822852fc6dcbbb47bbe47534ff58a80f7cc43a86bb96dac2001a60d8c95cd1926259f662f4a79160435378c37209092bdabba796beba290ab49585294dd6481deaa348a62a95dca8262197654278c6e06a1fedb9168249ae6cbc20310abd964279d477906cabdde6a9d3f2789a02a2e609a5d8d768ad4761c23ab5003c61145e715249560321a06b49461536b2f84f320a9741ccf33a09695694b102350baae6d085823608794f728c1e7bd35f3a8b3b68b4cd2aa7017310eda95db058f6983a366882e394949d4c8756fda372eec65219849caf069cf5b95bd0c43cbe00fa0125ea705bbfad35480c85d192035576abfe475c2ed5c2ee37ca705036e4ce92075b27943189442164b96816b63d6b3f8886af6dcb57d385206803b81d638965b6b90baac8c9c8c3db8587a7036c02b24cc5201bda802febb7eeba607df69568f8c7f44006ec95529ce297a48c8cad08120ff836135b3aefeb96b6c8248b5918a49d84e56766555198f730403abf840283106bc001ef92cbcac194c7873c675ca142e790711a38be1e73f94d40f8e4257d3741718f9452765757d1916b92acfd429b48ac00ad31c5212730a6a26987627c988db224e74afcb03a05be5aea29422a52788fcfa14a4eb847895c3aad81c90d65d480b53b910672a65c35c69a23130a862542b502c2e84a092ed32bdd8c703e4ab27fc3cca014269a3a0930505b1922c54eb8340e8132e17c36a2512872aa98f64f691f268326e083a4c488d861a74bddb78ead962daf9cbd1807c8cdb0b22456778698e634b0c0572b5d50627001eee88e35a3e01b7d3c81b948bbc338de3294a574aa46f45ce4c795400b47203c0abc212cd4230f2a993d12a11a0ac6e761a8d479f3b961d8e79ff834dbb3b5449ace5765e87f252dd14ded32fd1973df5b5de53751a3723d5a52c8c6df2fd" "dk from a raw seed" + got=$(printf '%s' 8A3D5F46A59A1B14E87C963A355D12D353074754FF15000172E6C8AE00A3AC1D5449ACE5765E87F252DD14DED32FD1973DF5B5DE53751A3723D5A52C8C6DF2FD | hex_out "$BC_RUST" --hex-input mlkem512 keygen-from-seed -x) + assert_eq "$got" "d2facfe0ab6bc16109fc167cabb16605052fd5e70b757ba6c3d70712929716d2be43b2c7baa2631b4c374218899a0ca1e3b370d518aa992b37cda8767a8728fc51a43abc2728775a82674fc5eabf6e79218864395b6cce4531983d88874890682c9c41e302ab6882bfa94c37dea34fc0d977d5a7cca52420396cbffc89a562c47cfe858360d0cf3d89349df0a4da26bbdbcc28cda811a885423dd130a3cc78ffe85257e030298a4c5c2c6d22924802a72e722c079b10b0c0806ca62c667f533c305b8445b8a64d8cbe05111d1ae798fa4816293a61a01975105b64318932b5c8c83e805e2a3678271c77b4918f8caa15063b9b4e264615ba32a3db4e3510714b566375396926e873693743bde8a4c6280f66054df9f2930d045bdebcb1752581a10241024558e6163b6324a7c13784e3d28e78352164ab137a54a3a5650eb2399c20d14c1f0ccca4cc3aae37217b791aae8c74fa085e72249313e8746de6316b664262a253f0a100caf38de2889a465264bdca258bf28ed660c6f27a93c6d6c6d9f9c10be80605126ff17a1b5ee3b2c25c76ce58ab21450cbac36aed192cc096006b5c0ca43640d11aa9b1587dedb06506c9c2dd882d30b73df318a57c09af614a4cd066586319771a106146469e21526a41b4a3a4c3386e490160ca9d2ab55ea6d401bcd01fa4a949283a878ae2105bc11cfd705e231018e4fc84c4e24bee263393d76b5cdb6309841aaefb609149af19b492ec4628fe6090ee5349eac519e1a226050244f70c5dd87b1a8c8c11c00ccf8d3844557c1333a02dbea5a690ac367c325094c53945fa31b168508951a19547bc422bcbd02a8db8ca9366cb617ad13236607968c93aab0c78e50217c96a0f76d64db09b4deca4a3aa574ef9210c3a6c7b21d5729ee07c781378680648a24aa3bef461e6ca012df9be515856dadb67d7835edeac4610c58f38fb4115831dbf29ba1aa980b7eacc7c430f70970256b1029ef27b6ca4a61f5093326ac4128a0536cc1bbb005b1158b0322899759761e0c5b9752c045c2cc3ffd81e526b86f0b03ca42355daf5a7f1c19a0dd517eb1744084c1a6f228372bb63e07632561746cd310ba0c09fcfac838556bf598168ee7a9e784c0f428a5aa6685bb23574d9d1330d2aaab4aa5cd3da3c90a9557140af72e813e17868e60b5e80d3c1afa7ae32a141a6a48fb05878f9b7cfa6e4939c4c8c240a8e337579e7958c59605f82e4088b705bf530451a968ff83c388615bf8c0c3470e52010e1226fc83c22d64167b35e0b946ddf40a17be90ac062c5d4a8587711c192b096adc64962dc99a3aa260d19143c99919e6bb7b6e129ff288b121b96d25b7822852fc6dcbbb47bbe47534ff58a80f7cc43a86bb96dac2001a60d8c95cd1926259f662f4a79160435378c37209092bdabba796beba290ab49585294dd6481deaa348a62a95dca8262197654278c6e06a1fedb9168249ae6cbc20310abd964279d477906cabdde6a9d3f2789a02a2e609a5d8d768ad4761c23ab5003c61145e715249560321a06b49461536b2f84f320a9741ccf33a09695694b102350baae6d085823608794f728c1e7bd35f3a8b3b68b4cd2aa7017310eda95db058f6983a366882e394949d4c8756fda372eec65219849caf069cf5b95bd0c43cbe00fa0125ea705bbfad35480c85d192035576abfe475c2ed5c2ee37ca705036e4ce92075b27943189442164b96816b63d6b3f8886af6dcb57d385206803b81d638965b6b90baac8c9c8c3db8587a7036c02b24cc5201bda802febb7eeba607df69568f8c7f44006ec95529ce297a48c8cad08120ff836135b3aefeb96b6c8248b5918a49d84e56766555198f730403abf840283106bc001ef92cbcac194c7873c675ca142e790711a38be1e73f94d40f8e4257d3741718f9452765757d1916b92acfd429b48ac00ad31c5212730a6a26987627c988db224e74afcb03a05be5aea29422a52788fcfa14a4eb847895c3aad81c90d65d480b53b910672a65c35c69a23130a862542b502c2e84a092ed32bdd8c703e4ab27fc3cca014269a3a0930505b1922c54eb8340e8132e17c36a2512872aa98f64f691f268326e083a4c488d861a74bddb78ead962daf9cbd1807c8cdb0b22456778698e634b0c0572b5d50627001eee88e35a3e01b7d3c81b948bbc338de3294a574aa46f45ce4c795400b47203c0abc212cd4230f2a993d12a11a0ac6e761a8d479f3b961d8e79ff834dbb3b5449ace5765e87f252dd14ded32fd1973df5b5de53751a3723d5a52c8c6df2fd" "dk from a hex seed" + "$BC_RUST" mlkem512 keygen-from-seed <"$TMP/seed" >"$TMP/dk" + got=$(hex_out "$BC_RUST" mlkem512 pk-from-sk -x <"$TMP/dk") + assert_eq "$got" "084c1a6f228372bb63e07632561746cd310ba0c09fcfac838556bf598168ee7a9e784c0f428a5aa6685bb23574d9d1330d2aaab4aa5cd3da3c90a9557140af72e813e17868e60b5e80d3c1afa7ae32a141a6a48fb05878f9b7cfa6e4939c4c8c240a8e337579e7958c59605f82e4088b705bf530451a968ff83c388615bf8c0c3470e52010e1226fc83c22d64167b35e0b946ddf40a17be90ac062c5d4a8587711c192b096adc64962dc99a3aa260d19143c99919e6bb7b6e129ff288b121b96d25b7822852fc6dcbbb47bbe47534ff58a80f7cc43a86bb96dac2001a60d8c95cd1926259f662f4a79160435378c37209092bdabba796beba290ab49585294dd6481deaa348a62a95dca8262197654278c6e06a1fedb9168249ae6cbc20310abd964279d477906cabdde6a9d3f2789a02a2e609a5d8d768ad4761c23ab5003c61145e715249560321a06b49461536b2f84f320a9741ccf33a09695694b102350baae6d085823608794f728c1e7bd35f3a8b3b68b4cd2aa7017310eda95db058f6983a366882e394949d4c8756fda372eec65219849caf069cf5b95bd0c43cbe00fa0125ea705bbfad35480c85d192035576abfe475c2ed5c2ee37ca705036e4ce92075b27943189442164b96816b63d6b3f8886af6dcb57d385206803b81d638965b6b90baac8c9c8c3db8587a7036c02b24cc5201bda802febb7eeba607df69568f8c7f44006ec95529ce297a48c8cad08120ff836135b3aefeb96b6c8248b5918a49d84e56766555198f730403abf840283106bc001ef92cbcac194c7873c675ca142e790711a38be1e73f94d40f8e4257d3741718f9452765757d1916b92acfd429b48ac00ad31c5212730a6a26987627c988db224e74afcb03a05be5aea29422a52788fcfa14a4eb847895c3aad81c90d65d480b53b910672a65c35c69a23130a862542b502c2e84a092ed32bdd8c703e4ab27fc3cca014269a3a0930505b1922c54eb8340e8132e17c36a2512872aa98f64f691f268326e083a4c488d861a74bddb78ead962daf9cbd1807c8cdb0b22456778698e634b0c0572b5d50627001eee88e35a3e01b7d3c81b948bbc338de3294a574aa46f45ce4c795400b472" "ek from the dk" + expect_ok "the pair must be consistent" "$BC_RUST" mlkem512 check-consistency --pkfile <(unhex 084C1A6F228372BB63E07632561746CD310BA0C09FCFAC838556BF598168EE7A9E784C0F428A5AA6685BB23574D9D1330D2AAAB4AA5CD3DA3C90A9557140AF72E813E17868E60B5E80D3C1AFA7AE32A141A6A48FB05878F9B7CFA6E4939C4C8C240A8E337579E7958C59605F82E4088B705BF530451A968FF83C388615BF8C0C3470E52010E1226FC83C22D64167B35E0B946DDF40A17BE90AC062C5D4A8587711C192B096ADC64962DC99A3AA260D19143C99919E6BB7B6E129FF288B121B96D25B7822852FC6DCBBB47BBE47534FF58A80F7CC43A86BB96DAC2001A60D8C95CD1926259F662F4A79160435378C37209092BDABBA796BEBA290AB49585294DD6481DEAA348A62A95DCA8262197654278C6E06A1FEDB9168249AE6CBC20310ABD964279D477906CABDDE6A9D3F2789A02A2E609A5D8D768AD4761C23AB5003C61145E715249560321A06B49461536B2F84F320A9741CCF33A09695694B102350BAAE6D085823608794F728C1E7BD35F3A8B3B68B4CD2AA7017310EDA95DB058F6983A366882E394949D4C8756FDA372EEC65219849CAF069CF5B95BD0C43CBE00FA0125EA705BBFAD35480C85D192035576ABFE475C2ED5C2EE37CA705036E4CE92075B27943189442164B96816B63D6B3F8886AF6DCB57D385206803B81D638965B6B90BAAC8C9C8C3DB8587A7036C02B24CC5201BDA802FEBB7EEBA607DF69568F8C7F44006EC95529CE297A48C8CAD08120FF836135B3AEFEB96B6C8248B5918A49D84E56766555198F730403ABF840283106BC001EF92CBCAC194C7873C675CA142E790711A38BE1E73F94D40F8E4257D3741718F9452765757D1916B92ACFD429B48AC00AD31C5212730A6A26987627C988DB224E74AFCB03A05BE5AEA29422A52788FCFA14A4EB847895C3AAD81C90D65D480B53B910672A65C35C69A23130A862542B502C2E84A092ED32BDD8C703E4AB27FC3CCA014269A3A0930505B1922C54EB8340E8132E17C36A2512872AA98F64F691F268326E083A4C488D861A74BDDB78EAD962DAF9CBD1807C8CDB0B22456778698E634B0C0572B5D50627001EEE88E35A3E01B7D3C81B948BBC338DE3294A574AA46F45CE4C795400B472) <"$TMP/dk" >/dev/null +} + +# ACVP ML-KEM encapDecap, decapsulation (ML-KEM-encapDecap.txt, ML-KEM-512 tcId 26): dk and c give k. +test_mlkem512_decaps_matches_acvp() { + local got + unhex E4B91341E794B495639F98AA78A0A786DC5172213DE6866909E2050ED265C04927EB8805A1A2B9CADB0C16DB67DC4AB77D72A8A2F31EE723A38B214F1D1455EAB0866084C10A811AA806B9E9DC5757107F46A04AB8EC5C41888E40D385879B4B52C288B4044725F36FA3F43841D42ADF34AB9816022663108B821671D6A56EA2204A93C55E669CE66CB118F35858396B63A9CD08EBCE703293C40BAB2FC5485B8A0B595A7EA941C82FC361AA093237B9AC36D44AD25A785D596127B4B46627BC8237A29ED37FB888AB7CC84469FA972395701F864A4CC83A6397AC23255425DC1CA030B07DB09EB8B50DBFE0BE34C49F489BA3FEA2B2F13853679AB8B4D4456E688D972CB4063B5289D64347C81F256B91563AB443BABC59984643BC45CE34A0CEABB445BB0B96808F5972972E0C7458A655EBA95696354EC0422730302D322A2B0E60CE8593C054715E946116F0C69B81F9C26C64AD8A557C987651905505085581EB708D952018AF1CAE67ACCBA26882D8F6A6290A02F619775AB795494C0F12D2C73A28BA08BBA703074E941956CC032DA50B468ACB26A4DA82F8C507687AC50339384A4C233EE9B678E4964F63A704A0B4513B6C451BB0A8B92A54A188D5D74ECE9AAE90AB1C0841001E443CD0FBB4F907879B048A2E63025E160BC805CBDD0B0457D2A74BB440DDBCA3AD0B86FDEBB3BE1B215A3559992A76DC270E47DA63DF9C8DFFC530D51C88C7F9BB2FB0AEB29558B6152B57DB564F647E5109A510604F79BA4A3515BDB1F9150837560441535776273948878D1555161943547A48C3293E40261F5112A0D93664E280775CC7B83FFC673C4BB47E6856F72C62B05A4C98239E28944E9AB59B6B86B2AEA92DAAC92642354B18E9C1871A57ECAA06F6386EDCD092C7151AC965CB90893AC9F079CEA5CC02FBAABF90A95845CC6A9A426E0670D49C0CEFE6A350129BB8E72EE9A7229B87309305237D50C6F634686507ACF652C12D848C77D1683908498FA8203C08C7DFAB5CAF6A67F7D88CD58150997651EA74946B6B05BBF80E09470B5AD696CD426CF8C67E68D570C7E753E00A4B522403468534A253596F8B531CB4ADA263CC3EFB87D8E571AA699DB3D27D5961BA4AA5212914BF1747CC601544330CB1315796DBD46FBDAB4BA7A4199E8327C3E2472DF3C6060B0AF54A2E206485FC34B4DF372D2B586DBDD8B73367CB23DAC857562380A513DB6BAF68E0170C75293BA1A592F023BF2C1E66437CBE789584495ADD43B3905001F2270E21C622D9D256314B4CB731A9FAC841C0C547D7C5B396014B826A6831C2CF4EECBFB09B36F5356CB1E02D2AF988C63B69AA9B0F3D5683974C06C65847E3EC3C8D7458257978EF063BC2A23320C785CDE71DAAE7448AE2CAC9A65820BC957406A9C7D1812417C54FA813B55103F869CEE5C6229C4540CF77CCE125298A1133FBC8CD6081B437613CDB56A5563A7F3313B76BE42554A157B9F7BB1D37459ED849C4D6968CBC5443376296F1A8F2617406E40575906E12B42D1F974A9A3496301763B3F133B8107F08466F5381A4C0C41F48A993B0594741A4B0824A4C87695AAB32CAAAC3A9462C51B366B451B0A7C94281FDA1092BE5C4CF754829E26C7EE66B779627EC9BA08BC6093E7682701CCD6E847F774A9A2E791C2BCBC6ED74A8A6CC3762A520DD208887D75C4BEA035E8ACEF82C8B259BA8B91A832716A136C56C6CC397187010DFC603037388304BB622B1458A0B6DC0E108135C3A00D460E3225AFBB272A2DBB39C909F893A86654957D4A9684BC44FCA358C6430B37202CD0B43585A575E8D01536E930AFA668CF9C23FEF7A2B91B5CA5BD26CA4912E1AB20D6D176F58C61A25BB21E3569E6A3B3DEAA03253861ECD5A379E9819152612A0E7B6B380806C74310BA78F73CC2B6963468DC270388B02DD87B8181688F0304F46244403715395EB3B67659C19291615D4259A086304D993BA050459C04D433C88495B52492357BDC30AFCCC73922709479A0D0DB2A5C3720D4F12852C217BD954CEFACABD74A4CFC7C90DE360A1148A2C6D5644FB8C2490F29FF1009815EB5770B0B40B3A46E552C089E9B1AE0C0A27589A6191CCB054258EAA650936C3E7D2247582873F396A8357A09CD0A43DB4CC9207849E7AAC29E973FB05268CF468D8E5C2FA6E0291D4F3972EA189F4FAC1E15894798D2D2FC73D1D4F67538DAA1D7654AA2BAB8ECE6944BDB57FCD362D24717F0255E845A5D231E9ED0D32308DC009AB9B81A2E945FE72C1F7E30AC712617137E56D11C9A49D4B89 >"$TMP/dk" + unhex F120796127DA7ACC2C980427DFB6EEB2B229315CC4BB364B7A06B02780F98BB3EFFA46153E75B6364231BED99E846C3BBFA842FDE9738E3579E849B0E22FF9A2323978BA2BB8280AB052F83AE2D653E3CCD8A218130250BD02C373DCADE7D01D67E788E418E14E3C41BD26B01B6021472E983F2AE1D02BEC32DCB164942994853FBA123D4AF93B56234A5AC7F0AFC709A9AF10F568F02893835A32E2A81E88F74FF9A52D8A6F6EDB7A2AEC6A05E2F287EFC99CEC1C6FA934C40C53C97FFC58E6DA5673E716935C9EBCEDF361706A69E0048F0C7FE3C7465E008C2B2E5194DE124C4CD6EE0D43F02CEAA8D2997C56C95721D22247A579430BA08FF30FF6A4B26CD5F0B71DAD4B832B0D05BFF1177DA99910B544D0184DD66DC615C6C885842D2232A89E772B2C3F067682F6440F92936043B7F2D987D1FCDE7B5BD8803D9F4209577C7873532903B7C5D2631D6B930728FBF5740EDD77988D3AAE2A1ED4129B03584DD12ABD844BE7BFAA7C5449013982D18900B40D9CE6D9C6E186233E8D320AAD122895DE21B68ECD8578326475D42D1F3814265A6050E2A6E599A3417698064BB2EA003399001532E19ACD7D31C9DD653E6572301D3F5920FCBD4925AFEB88131E93D16CF62C74B269DDFDB3B0DC161A063BAFAF3FF23F438F7CE36A2F66558DB145A6A955AD72B56A1588315F091A6912EFF884B694A613DCA643B70D097571E553AACFC32C9066B0E767B4799C3C229AEFB0547DEA6D847B10DB38DBF91EE58C72863D859E9DCEC07B70F304A772EAFF549DAB568108C4201ABB4E81DEC2C070996976C88E64A9206CE2756BF1771C181EBF929470F56D148FE788AE33D24A06B1AB17C254B864ED3082B8484736D490AC89719D75DD0DFAD2813B50435FC07B5BCD8CFF2E60EFF7F0E7204F3A34A7EBE58AE93629D16866880BFC946DAC044002411A0871569E25BEDA6306F3301DDC10E83D51824E4E0BC70107FAB287C194513BAFE257CCC9E6FC86B07DCFF1B0BFC030A68A7757E842C6EA8F39BD6D37809DA7EED8FDA747E42296B6675B6132EAEC9874A55548DC9700F851A64A4B >"$TMP/c" + got=$(hex_out "$BC_RUST" mlkem512 decaps --skfile "$TMP/dk" -x <"$TMP/c") + assert_eq "$got" "afb1789cf2bf24b4d0cd0e94570fe0da6dcc7a082286cfad97c59195679cedda" "shared secret (ct on stdin)" + got=$(hex_out "$BC_RUST" mlkem512 decaps --skfile "$TMP/dk" --ctfile "$TMP/c" -x "$TMP/dk.hex" + got=$(hex "$TMP/c" | hex_out "$BC_RUST" --hex-input mlkem512 decaps --skfile "$TMP/dk.hex" -x) + assert_eq "$got" "afb1789cf2bf24b4d0cd0e94570fe0da6dcc7a082286cfad97c59195679cedda" "shared secret (hex dk file and hex ct, with --hex-input)" +} + +# `keygen` draws from the OS; its output must be a ML-KEM-512 key (FIPS 203 Table 3 dk length), and +# encaps/decaps with it must agree on the shared secret through --ctfile and through stdout. +test_mlkem512_keygen_encaps_decaps_round_trip() { + local ss_enc ss_dec lines + "$BC_RUST" mlkem512 keygen >"$TMP/dk" + assert_size "$TMP/dk" 1632 "dk length of ML-KEM-512" + "$BC_RUST" mlkem512 pk-from-sk --skfile "$TMP/dk" >"$TMP/ek" + assert_size "$TMP/ek" 800 "ek length of ML-KEM-512" + expect_ok "the pair must be consistent" "$BC_RUST" mlkem512 check-consistency --skfile "$TMP/dk" --pkfile "$TMP/ek" /dev/null + # `-x` would also render the --ctfile as hex ("the ciphertext to file also in binary or hex"), so the + # secret is hex-encoded here and the ciphertext file stays binary. + ss_enc=$("$BC_RUST" mlkem512 encaps --pkfile "$TMP/ek" --ctfile "$TMP/ct" "$TMP/seed" + assert_size "$TMP/seed" 64 "seed length" + got=$(hex_out "$BC_RUST" mlkem768 keygen-from-seed -x <"$TMP/seed") + assert_eq "$got" "e0963a4ca3b67b75caf8650d12193989e9b0ee23b72c00c53f835fa5a5986c5a9d6b730e23c7720ba08641cac0f9489d0fcc39b7b3b54b33be045c4d5e54c3f1cb1eb8d722ecfab2c2209b3a8574fa4615d6e626d0c15ef392cda4f4153d1b55be83241f97a79b8a1bb6daab46d64948988c23ebcfe57134a66606b139b821b6a857c825d21080ce6a183799210f33cc65b7127c7cc071932dc80415701865966137c9bb52d8e14756b976775c97feb58702922c64f65a8c4c9fb378af484576c855322f5c7163e356c3386ce9f21ab7253b2d3a99126b207f6086a388cf89e94cfd89942907ca9a4510e02b707f5b32119186db2769e2daa07cc4a34950508aa1b0fea5a4d3833e9e2b11837202e7bb88bdb9842c78b56a8320ae950acd19c135f36c49cb20267c6b67a10b2d0256289bc90c6996c67198963149a7620dd7e1b7ac2c9c228c4e60419e6932c74e799df9085d92493b910ccd0b837b55f82c73da044b0305cb52bf54da86ebbaba245624cc78ba3296bd8a204a6d0ac9d039b8c2f45036f711c6bc516b936403fb051fd09be8c7c8fb9aa2c22cb9e0d312fd972271e56de755ab479bb9f2b183818b6cbbbaba1bd704812a3f1fa0c6066875caa83a5c926b4b88561066bb6924b44b97ce16d5b160f001bc66289e49cdf843856f3cab938c71308553c84a0fa0e53331fa38dd642c2c284b76c135d845003e1b7e18c59a91e43da9050fbf74324b110ec3d756b02031b0cb3ef70186dcc42947f160cd8891acfc169b802243b9b27df799722b6477c093b0252a047a6a1fe65c7fb7c2c853b4a81376ea0651186242bb295e8355c47f868c35d5862adac4f8427b63745140f35570a58553e962ece42619922fd0953ad5cac2efca6b86e68389b51e663348f49631ae06a9969b9086bac11ad742837940267a187229018517a658777a4964674ccc8e3c2b8bb615cc7885af2cf5865193725023180558258f48b619b80032f7c521b95ff6325ee7acb22edaa772e5a5cc166bfbf439e762405c84987530735ba7ba4b1b583a6bc504d28e62c448fc6a541b5a5a3f382c356a3b2bc35e831255fa902196b669d7a12bbeaa02930434d75c01a02801f8f9aecb885ccf8727448854ca450cc561c394917c7dc58dae989cd5620b18f657ed193822b676fccb49cf705055ec62cc9800e3546e1f78253594a563176338f49484aaa7a6faa402c659a320a975f824e3da1313d1ca588b00e9e2685e690fb31a867deb5fac69610fd99631555bef428f8fe29509caa04af6638d0a200d05ba94c626c97b4c26d550caf1b489f590e8a99332f5b712a8872022381df32b28951ffd71450cf28f82cc0ed5d9240114917d18219498513cbb30c980576039228551c78153a5d9a8008e559246c645508657be02a48cb0c6e826ca2ce076ec76b915c40c5134ac626735d7b21cbcc6984b8c80f6a8ca616b547a666c8c8c9cdd0ba0b38a4465ea23820140b6744dc1f8a78558b6b699af0fe96158dc206161bf0adc04273947dae81af241a61352847eccadf48b6bed0196b6c63e7a33022adb1ce4e968fedab7d6f65824732ffcb325483599a6f886b79b5cbcb0477f8906c535ace37b784bc58d18dc9cf6973257a12a7aeb62cd91944dd173f85c571d64277b7263fa37b0f8b5aaffaa7419998442e55fd0bc1af7b0287ada9ab174ae4fa78f2266158bc83e0ee33aaa7086cf3c010a4a4d56b42ed5172169827729293745844ae757af6e95c2a6744797710f2fe9c78bfa15c13aaee8e9b69b021f142144d398600a579cc9cc2c7520b7259c2528a9966e8cc5efba8668d885145b052bdc0ad4f0be38bc9144b20509f25d942a88c9789650c76d6806791109c647bb842e314946f251dcb5b71cfab81ad81908472cb7307e87e4a29cc48ae0b3cb7e82a57dc92fb95126988b11d506c35e40228687099af3aef53baf6b3686ce3695b4270692567afb367cdb7129b2b83551d47420a1c938016f4671a354259787f69534798839eb1464d749dc883ad8d4072561056501c76bf2a4c8385897302acc76cf5609ccdad74b9bea745b78321c975d0f72789feb32da404ced223f7b838d10fa6bf7057384cb1e5ec863f2d906e274c4eee6b9388b5ac9e4575d48747d371cecd58dadf256ba9b278fd8a1535708d8b78862b27f080c7b8a057fc08c231a41bf428941be82a307644297d825a386182ff32f8f3c30493638e16b8a8edc9346183262e50a4474416402628be4a7ccb3431ab70d324910a39198a25468eb95ccdad0948d450213b06bb83430283538c9234cb1181fd6689cc07c8d125cb45fb2b78de6071d8895b614cd47e7a253aaa1ce31bd955974117809f9391cd8a9113178273c5a8a65e7b66ca77b0710ab112144e77058e5da498ce0199967c39b8a447aab7f8b714ddcd694c2f28b2295762a3ba880acba260384ff8a7c64a8211e7c4f81165723e3bb261198a2a1c8086662f7940abc4ba1ef10460526621a79470db22427bb10f0115c17b79763c5488bdb3d305a4e411797cb620b29c3a7c076bd3c405db2b81fec85adfe7ac9ba7702fbc3bebdc0b308149f327c18c7037ee5234c37f581dfd94837554673a39379e03ac6aaa38ca87ea2b940478811edd41c07968b15c112cf64b667b3bd6dd5bcdd5a5cb065787aba70d6926b30259c77e3b4ef2cc8b25153fbb38c1cdc8ef81358893861a888851b54256e37a0e8360ce22c9ccc3822b718becd7bc83422cc61ea9f918b986f15b6d8e1b32d472d394ca40ed7a5c5611548e294c5bc09b64367fcc86f5fd417786c3c7b864c4f7b24ecab8a32c40a993769093b36fb2b465b2924816314fc1063d9606d7277a8fff5b5dba5769c5c8986e4adae1965b652064e0570d6a36dc28a45df2c4c5f6aa201ea7d8d672a7222648b32672dcba0a68459dcb484b14c18603831003d15da1b4a87d0579d196bb069477ff3bef0059ad38c78d7da94413a69f8274e93726dca420f75043e55236abc4921f6489c14043d2cbab64d68a0083bb0d2257a4750459780c623a12d8eb522b8b62214c75bcaa41899b8cbe36b1a071200652bae06e25808296151964c89a3bb7d5b88e8b23260964b3cd23f6bb7655b0834b9ecc9bcac1185603d0af11216a9aae4ca80ee0c714618bf0afc73293c9d592872ca72c887d4447e65b751368ea081294a43777cb599c416516cb09ebc436f352ba11a3962fd5745214133b72b67ea855f380e4661920c003ce639a7f02609835f00f09ab1a80f0e1cd0d5b5fd8ae3c90829b99d1f0a55382eaf301bb5f288c67687a2eb61060b3aafd1bbe680371c1ca2a5b29d73a2cdd1955dc19785d8c3f313bc8c65e1edde23293bc0724496" "dk from a raw seed" + got=$(printf '%s' 4A39718BC71DA57200A3458849B66A584B1654517FC01CD71F73936EC637AFDB1C1CA2A5B29D73A2CDD1955DC19785D8C3F313BC8C65E1EDDE23293BC0724496 | hex_out "$BC_RUST" --hex-input mlkem768 keygen-from-seed -x) + assert_eq "$got" "e0963a4ca3b67b75caf8650d12193989e9b0ee23b72c00c53f835fa5a5986c5a9d6b730e23c7720ba08641cac0f9489d0fcc39b7b3b54b33be045c4d5e54c3f1cb1eb8d722ecfab2c2209b3a8574fa4615d6e626d0c15ef392cda4f4153d1b55be83241f97a79b8a1bb6daab46d64948988c23ebcfe57134a66606b139b821b6a857c825d21080ce6a183799210f33cc65b7127c7cc071932dc80415701865966137c9bb52d8e14756b976775c97feb58702922c64f65a8c4c9fb378af484576c855322f5c7163e356c3386ce9f21ab7253b2d3a99126b207f6086a388cf89e94cfd89942907ca9a4510e02b707f5b32119186db2769e2daa07cc4a34950508aa1b0fea5a4d3833e9e2b11837202e7bb88bdb9842c78b56a8320ae950acd19c135f36c49cb20267c6b67a10b2d0256289bc90c6996c67198963149a7620dd7e1b7ac2c9c228c4e60419e6932c74e799df9085d92493b910ccd0b837b55f82c73da044b0305cb52bf54da86ebbaba245624cc78ba3296bd8a204a6d0ac9d039b8c2f45036f711c6bc516b936403fb051fd09be8c7c8fb9aa2c22cb9e0d312fd972271e56de755ab479bb9f2b183818b6cbbbaba1bd704812a3f1fa0c6066875caa83a5c926b4b88561066bb6924b44b97ce16d5b160f001bc66289e49cdf843856f3cab938c71308553c84a0fa0e53331fa38dd642c2c284b76c135d845003e1b7e18c59a91e43da9050fbf74324b110ec3d756b02031b0cb3ef70186dcc42947f160cd8891acfc169b802243b9b27df799722b6477c093b0252a047a6a1fe65c7fb7c2c853b4a81376ea0651186242bb295e8355c47f868c35d5862adac4f8427b63745140f35570a58553e962ece42619922fd0953ad5cac2efca6b86e68389b51e663348f49631ae06a9969b9086bac11ad742837940267a187229018517a658777a4964674ccc8e3c2b8bb615cc7885af2cf5865193725023180558258f48b619b80032f7c521b95ff6325ee7acb22edaa772e5a5cc166bfbf439e762405c84987530735ba7ba4b1b583a6bc504d28e62c448fc6a541b5a5a3f382c356a3b2bc35e831255fa902196b669d7a12bbeaa02930434d75c01a02801f8f9aecb885ccf8727448854ca450cc561c394917c7dc58dae989cd5620b18f657ed193822b676fccb49cf705055ec62cc9800e3546e1f78253594a563176338f49484aaa7a6faa402c659a320a975f824e3da1313d1ca588b00e9e2685e690fb31a867deb5fac69610fd99631555bef428f8fe29509caa04af6638d0a200d05ba94c626c97b4c26d550caf1b489f590e8a99332f5b712a8872022381df32b28951ffd71450cf28f82cc0ed5d9240114917d18219498513cbb30c980576039228551c78153a5d9a8008e559246c645508657be02a48cb0c6e826ca2ce076ec76b915c40c5134ac626735d7b21cbcc6984b8c80f6a8ca616b547a666c8c8c9cdd0ba0b38a4465ea23820140b6744dc1f8a78558b6b699af0fe96158dc206161bf0adc04273947dae81af241a61352847eccadf48b6bed0196b6c63e7a33022adb1ce4e968fedab7d6f65824732ffcb325483599a6f886b79b5cbcb0477f8906c535ace37b784bc58d18dc9cf6973257a12a7aeb62cd91944dd173f85c571d64277b7263fa37b0f8b5aaffaa7419998442e55fd0bc1af7b0287ada9ab174ae4fa78f2266158bc83e0ee33aaa7086cf3c010a4a4d56b42ed5172169827729293745844ae757af6e95c2a6744797710f2fe9c78bfa15c13aaee8e9b69b021f142144d398600a579cc9cc2c7520b7259c2528a9966e8cc5efba8668d885145b052bdc0ad4f0be38bc9144b20509f25d942a88c9789650c76d6806791109c647bb842e314946f251dcb5b71cfab81ad81908472cb7307e87e4a29cc48ae0b3cb7e82a57dc92fb95126988b11d506c35e40228687099af3aef53baf6b3686ce3695b4270692567afb367cdb7129b2b83551d47420a1c938016f4671a354259787f69534798839eb1464d749dc883ad8d4072561056501c76bf2a4c8385897302acc76cf5609ccdad74b9bea745b78321c975d0f72789feb32da404ced223f7b838d10fa6bf7057384cb1e5ec863f2d906e274c4eee6b9388b5ac9e4575d48747d371cecd58dadf256ba9b278fd8a1535708d8b78862b27f080c7b8a057fc08c231a41bf428941be82a307644297d825a386182ff32f8f3c30493638e16b8a8edc9346183262e50a4474416402628be4a7ccb3431ab70d324910a39198a25468eb95ccdad0948d450213b06bb83430283538c9234cb1181fd6689cc07c8d125cb45fb2b78de6071d8895b614cd47e7a253aaa1ce31bd955974117809f9391cd8a9113178273c5a8a65e7b66ca77b0710ab112144e77058e5da498ce0199967c39b8a447aab7f8b714ddcd694c2f28b2295762a3ba880acba260384ff8a7c64a8211e7c4f81165723e3bb261198a2a1c8086662f7940abc4ba1ef10460526621a79470db22427bb10f0115c17b79763c5488bdb3d305a4e411797cb620b29c3a7c076bd3c405db2b81fec85adfe7ac9ba7702fbc3bebdc0b308149f327c18c7037ee5234c37f581dfd94837554673a39379e03ac6aaa38ca87ea2b940478811edd41c07968b15c112cf64b667b3bd6dd5bcdd5a5cb065787aba70d6926b30259c77e3b4ef2cc8b25153fbb38c1cdc8ef81358893861a888851b54256e37a0e8360ce22c9ccc3822b718becd7bc83422cc61ea9f918b986f15b6d8e1b32d472d394ca40ed7a5c5611548e294c5bc09b64367fcc86f5fd417786c3c7b864c4f7b24ecab8a32c40a993769093b36fb2b465b2924816314fc1063d9606d7277a8fff5b5dba5769c5c8986e4adae1965b652064e0570d6a36dc28a45df2c4c5f6aa201ea7d8d672a7222648b32672dcba0a68459dcb484b14c18603831003d15da1b4a87d0579d196bb069477ff3bef0059ad38c78d7da94413a69f8274e93726dca420f75043e55236abc4921f6489c14043d2cbab64d68a0083bb0d2257a4750459780c623a12d8eb522b8b62214c75bcaa41899b8cbe36b1a071200652bae06e25808296151964c89a3bb7d5b88e8b23260964b3cd23f6bb7655b0834b9ecc9bcac1185603d0af11216a9aae4ca80ee0c714618bf0afc73293c9d592872ca72c887d4447e65b751368ea081294a43777cb599c416516cb09ebc436f352ba11a3962fd5745214133b72b67ea855f380e4661920c003ce639a7f02609835f00f09ab1a80f0e1cd0d5b5fd8ae3c90829b99d1f0a55382eaf301bb5f288c67687a2eb61060b3aafd1bbe680371c1ca2a5b29d73a2cdd1955dc19785d8c3f313bc8c65e1edde23293bc0724496" "dk from a hex seed" + "$BC_RUST" mlkem768 keygen-from-seed <"$TMP/seed" >"$TMP/dk" + got=$(hex_out "$BC_RUST" mlkem768 pk-from-sk -x <"$TMP/dk") + assert_eq "$got" "4bc58d18dc9cf6973257a12a7aeb62cd91944dd173f85c571d64277b7263fa37b0f8b5aaffaa7419998442e55fd0bc1af7b0287ada9ab174ae4fa78f2266158bc83e0ee33aaa7086cf3c010a4a4d56b42ed5172169827729293745844ae757af6e95c2a6744797710f2fe9c78bfa15c13aaee8e9b69b021f142144d398600a579cc9cc2c7520b7259c2528a9966e8cc5efba8668d885145b052bdc0ad4f0be38bc9144b20509f25d942a88c9789650c76d6806791109c647bb842e314946f251dcb5b71cfab81ad81908472cb7307e87e4a29cc48ae0b3cb7e82a57dc92fb95126988b11d506c35e40228687099af3aef53baf6b3686ce3695b4270692567afb367cdb7129b2b83551d47420a1c938016f4671a354259787f69534798839eb1464d749dc883ad8d4072561056501c76bf2a4c8385897302acc76cf5609ccdad74b9bea745b78321c975d0f72789feb32da404ced223f7b838d10fa6bf7057384cb1e5ec863f2d906e274c4eee6b9388b5ac9e4575d48747d371cecd58dadf256ba9b278fd8a1535708d8b78862b27f080c7b8a057fc08c231a41bf428941be82a307644297d825a386182ff32f8f3c30493638e16b8a8edc9346183262e50a4474416402628be4a7ccb3431ab70d324910a39198a25468eb95ccdad0948d450213b06bb83430283538c9234cb1181fd6689cc07c8d125cb45fb2b78de6071d8895b614cd47e7a253aaa1ce31bd955974117809f9391cd8a9113178273c5a8a65e7b66ca77b0710ab112144e77058e5da498ce0199967c39b8a447aab7f8b714ddcd694c2f28b2295762a3ba880acba260384ff8a7c64a8211e7c4f81165723e3bb261198a2a1c8086662f7940abc4ba1ef10460526621a79470db22427bb10f0115c17b79763c5488bdb3d305a4e411797cb620b29c3a7c076bd3c405db2b81fec85adfe7ac9ba7702fbc3bebdc0b308149f327c18c7037ee5234c37f581dfd94837554673a39379e03ac6aaa38ca87ea2b940478811edd41c07968b15c112cf64b667b3bd6dd5bcdd5a5cb065787aba70d6926b30259c77e3b4ef2cc8b25153fbb38c1cdc8ef81358893861a888851b54256e37a0e8360ce22c9ccc3822b718becd7bc83422cc61ea9f918b986f15b6d8e1b32d472d394ca40ed7a5c5611548e294c5bc09b64367fcc86f5fd417786c3c7b864c4f7b24ecab8a32c40a993769093b36fb2b465b2924816314fc1063d9606d7277a8fff5b5dba5769c5c8986e4adae1965b652064e0570d6a36dc28a45df2c4c5f6aa201ea7d8d672a7222648b32672dcba0a68459dcb484b14c18603831003d15da1b4a87d0579d196bb069477ff3bef0059ad38c78d7da94413a69f8274e93726dca420f75043e55236abc4921f6489c14043d2cbab64d68a0083bb0d2257a4750459780c623a12d8eb522b8b62214c75bcaa41899b8cbe36b1a071200652bae06e25808296151964c89a3bb7d5b88e8b23260964b3cd23f6bb7655b0834b9ecc9bcac1185603d0af11216a9aae4ca80ee0c714618bf0afc73293c9d592872ca72c887d4447e65b751368ea081294a43777cb599c416516cb09ebc436f352ba11a3962fd5745214133b72b67ea855f380e4661920c003ce639a7f02609835f00f09ab1a80f0e1cd0d5b5fd8a" "ek from the dk" + expect_ok "the pair must be consistent" "$BC_RUST" mlkem768 check-consistency --pkfile <(unhex 4BC58D18DC9CF6973257A12A7AEB62CD91944DD173F85C571D64277B7263FA37B0F8B5AAFFAA7419998442E55FD0BC1AF7B0287ADA9AB174AE4FA78F2266158BC83E0EE33AAA7086CF3C010A4A4D56B42ED5172169827729293745844AE757AF6E95C2A6744797710F2FE9C78BFA15C13AAEE8E9B69B021F142144D398600A579CC9CC2C7520B7259C2528A9966E8CC5EFBA8668D885145B052BDC0AD4F0BE38BC9144B20509F25D942A88C9789650C76D6806791109C647BB842E314946F251DCB5B71CFAB81AD81908472CB7307E87E4A29CC48AE0B3CB7E82A57DC92FB95126988B11D506C35E40228687099AF3AEF53BAF6B3686CE3695B4270692567AFB367CDB7129B2B83551D47420A1C938016F4671A354259787F69534798839EB1464D749DC883AD8D4072561056501C76BF2A4C8385897302ACC76CF5609CCDAD74B9BEA745B78321C975D0F72789FEB32DA404CED223F7B838D10FA6BF7057384CB1E5EC863F2D906E274C4EEE6B9388B5AC9E4575D48747D371CECD58DADF256BA9B278FD8A1535708D8B78862B27F080C7B8A057FC08C231A41BF428941BE82A307644297D825A386182FF32F8F3C30493638E16B8A8EDC9346183262E50A4474416402628BE4A7CCB3431AB70D324910A39198A25468EB95CCDAD0948D450213B06BB83430283538C9234CB1181FD6689CC07C8D125CB45FB2B78DE6071D8895B614CD47E7A253AAA1CE31BD955974117809F9391CD8A9113178273C5A8A65E7B66CA77B0710AB112144E77058E5DA498CE0199967C39B8A447AAB7F8B714DDCD694C2F28B2295762A3BA880ACBA260384FF8A7C64A8211E7C4F81165723E3BB261198A2A1C8086662F7940ABC4BA1EF10460526621A79470DB22427BB10F0115C17B79763C5488BDB3D305A4E411797CB620B29C3A7C076BD3C405DB2B81FEC85ADFE7AC9BA7702FBC3BEBDC0B308149F327C18C7037EE5234C37F581DFD94837554673A39379E03AC6AAA38CA87EA2B940478811EDD41C07968B15C112CF64B667B3BD6DD5BCDD5A5CB065787ABA70D6926B30259C77E3B4EF2CC8B25153FBB38C1CDC8EF81358893861A888851B54256E37A0E8360CE22C9CCC3822B718BECD7BC83422CC61EA9F918B986F15B6D8E1B32D472D394CA40ED7A5C5611548E294C5BC09B64367FCC86F5FD417786C3C7B864C4F7B24ECAB8A32C40A993769093B36FB2B465B2924816314FC1063D9606D7277A8FFF5B5DBA5769C5C8986E4ADAE1965B652064E0570D6A36DC28A45DF2C4C5F6AA201EA7D8D672A7222648B32672DCBA0A68459DCB484B14C18603831003D15DA1B4A87D0579D196BB069477FF3BEF0059AD38C78D7DA94413A69F8274E93726DCA420F75043E55236ABC4921F6489C14043D2CBAB64D68A0083BB0D2257A4750459780C623A12D8EB522B8B62214C75BCAA41899B8CBE36B1A071200652BAE06E25808296151964C89A3BB7D5B88E8B23260964B3CD23F6BB7655B0834B9ECC9BCAC1185603D0AF11216A9AAE4CA80EE0C714618BF0AFC73293C9D592872CA72C887D4447E65B751368EA081294A43777CB599C416516CB09EBC436F352BA11A3962FD5745214133B72B67EA855F380E4661920C003CE639A7F02609835F00F09AB1A80F0E1CD0D5B5FD8A) <"$TMP/dk" >/dev/null +} + +# ACVP ML-KEM encapDecap, decapsulation (ML-KEM-encapDecap.txt, ML-KEM-768 tcId 26): dk and c give k. +test_mlkem768_decaps_matches_acvp() { + local got + unhex 94AAA7ABEC0BAB887A8C485D3E33AA35305B8088B25CCB4487EC7B2DB4AB29F440BA5231BD724A7A397F9AD3813D393A1786271CB49068A6451FDC230AB3186BBC0290E2362F5AB1EBE58CBE1B3371A0818E26A0A58C59DC96B90C3C45B483ABBA410208F78D275203F4A40EB7DBC42D191553341F98672057D3CEBE668B6C397BC9707092776FCC450FFDA31C5E2C929C253FFCC184D2D98C25F11AACA44145BC1AAC54077F77AAA77B9EC35BBC9856CE30E70FEB77BFD1F42F0421B9F3F55719E78502141263B4CF432830B943BF1CE83F1BF12DA931505A293000D977B6E33C25584869009BD4CA44F3BC19CE041984D0CE9110735D637D7E027606940E6A05BE090078532065750A5FD953302C44A530B432DBBC73E946076512A90142C16130BA9E2413A9F0C526737137B5BB113524D4B5747D79A325298F1B9819A0C2058D199BF93781098B1363243B6110C0EDB1BD750B92B3513482E62572C66687652B34819F0FF0557F022381B39D7797C14D8C8011F96C39CB44D0B039D3745A71C541D7937C4C7BA75B3470961117D6B22E24BC52660158150B654C220F4C5992891720CA5779F89426F78A3949397B7D10977F18909B247A5848096F43689BE199AA7A2D16927C54E6191241CE27706B7444674CC10050E14197A11623370C15372466716BDF6825361BBD4642A277458C80532DA4213A1510A57425CD6C5B21282B168DDA6A095B122A4A175F95B8311A1151133F44D57A78BC7A934BCAA38097D3808B7BCA17B7EB4F20165EB4983EE7D588C2B19F3CF5790CFC9B4688379E4C91DBF7878DC626AE11308AF19D73A59675E8A0D9E5662529BFCA457848861FBC5392C9453C33434CA8A307EC30854F50115A6C939991AAE3DB749C689208772864C9AD091389F3030F1AE45E3A528B5F9248F47727507389A40270E8EC7085133642282102C3992E829CEC8976B5C102082B911A6126F99A35E5834BDC2A3C58E96F5D44B6FAC845B113573FE9580E469E4393B3520695E22113AB02014981A647B1B2B7B8935FD53565B6B83CA1CB285899D8968C37E866904A6DDDE1A0F3C17CE7C702BDB520CF7696052B5F74525B2C8A740D4B3BB85ABB8EE5081B77B789B25ACA006C3C75A57DDAA47D742283C9BABD5B35D424789822AC6821A1BF6A4D5EB7CB70B2013FDA913C6C02DDE856CC624B875587042C1B82C8946D6123EE07572BF4BF6B4381074BCB96693879A87E13640F102A5A838B26C5BAC121C4BD63E44EE2121F21EC6016351B4FD6BCE835B67EB55D0E24895FBC2F92F447D05855BFA9BACF2A044AC32E2F6B7367458C1AF74F24025F4DC1B647B82578E29D93317BCF6A00C1892FC712BB61328102EB6A23491210408E1418B31BC4ADBF287889414FFAD3C654A3B0E473832D1355364C8303259142D0207FF302C7FC7238398F9249C9DDF69D21A57630B120CFAA3E0A3142AD481194D471B8F919EE131615369D77D033C65AAC2CC1AD87F582AA8C1FCBEB7C010CBD422C0A9BE9844ECA5A0FCAAF4AC2ACCF50760C341492A6B0CE712B7B362BE397AB892B9DFB064A60F396AB963F9CE837C483456D055DE3F25545139F06696780377BF6C5813A607E4039820364598108920300822E1C2CDB97899AA2A9EB4409CEA5A24F959CD204A12DE532066AB5619897408655E6259D7579C6A509314CA2C210F2224CC9691C734F1929974CB5B3859683322AAD9D3953521C607110386939329F92122EC03E0F02961FAB5578741C600307CEACB54B21C4BAE847C8A479577077E6920E933C4E3882B515BBC03934BA86D4BE3E96895FB3CBA7E9B4AC54C2731B678CA908C7502500D8A782B73C79FAC137F56193662FB0D30AC90131ABF6B109A2B4AED62BF94A923C808D6EE9ACCE4B2466B2B0A10552C0645C7D271A9D71A9025117E6E46FE1D66F4BD306D1075898627AA3B3BC70C1B1ABCC694A86180498B17F9147091172E019734E732CFB6A3744591246D5575351A7F7AB593F5A7C1876009C46516D95C7408A78A17548E5BB2313DA178D027175496AFC59266DEA825C8140FBD51494D0CBC9F7457DD57237C5CFF4CCAB4E04A0FA4141F0422E7E85CD0A94274BDB3C6CF10F6415BB48606DFF5C98FE551BD96B4A06459F45C358A4284EA87844534189077C10899331C134230478C0E7A35442CC13C6D4AB0D928041156E6F7A70F6438EE1746196E245B2F31AE07221AD9C3CCC542A79D960D9178259F51315371D85428B227228B7F38B6C543C51074B31184C16E143E0F18E4004668BB32DF9386E347395F47C38A857ABBBE342C761326F713B5730BD241200A230C4ECB4C5E05A0D0F5C28B42B9D715386DEC74E5DC9373E7BCE096C56CCD8C0D48910A3D96D61449B71B1A319B1B3A0826AB289B5A66280593C437DA08A5A610D50EA4BC401542CC83DB5436882810BA60C5B673A1175258563542EFFA3146339A8461A343DF59A06B362D5E762053962E2B441CA1742CB92304F6B1F104BCCE1783566C6CD56F0BF1C15460CD3B32574386DB122E9F23F2715230622C45306033BC9501357B9BAB6822772823412447D965080C04F29AB36D49A0CF37C893202846EB9A1EFB86A23EA3269D2769601C67962488AF0A61937CCB0AC892DB2A28710935A5C82B929C732B3226A746923AB1ADFB81ED165692041BE874C7D8E1A056ED2CF5BE8BD87D5C595521E2BF65749E98D27003A6C000C3F87B544C224343763272047376B420DB7688E710B4308953D792B060C30AFB7044BAC134D77C306B365DF1B315EE536F223663A023507C23A6BAAB46BBB1BC40BA408E9309D430006178517D41F9D1364A6C722C7B737BC62BB270202F2799F0CA50B5853A60ED789A2FC4742F6CFFFBBAA400A2F8CA01ACA044E2830AD65B78BCDD8CEDAEC451DD093C0627FBA3C365C2A47EDFA6582BB1DE899355E0C64820646E07703B4390A6DF27633654490BA927699981A34528EB7427ED70FBB9C3A4BE5BFCEB70516768A5222C08079352D6A181BBCB69FAC13917AB0F3B221505B350754B7614C4120E01A9167091E84AC48A7C00ABA212672535C8608B5531364BC656386B42B8B41C777B82DD808FBF76559764D01D55F35CB594C19AA0918571F9C4AAC65595949135DDCB8AE8C7C4ECB2220D51C56655CCF13A7BF88784DC74C33C4390658CE75A59BA24776EE96686231AF8AD007BDD3CC6657674998719D2C7180481518F809855C114B8D8F80EF081ECB4CDF02F3634147386FF2C8FDC972FA395AE103C61030221B90D5B8400C1B570298312D9E8057FBAB908D88E3F7E826900C7FD53615ABAD0EE1227965788DE2A02017CA6828D3BEC5F665D9FF36CE804564AF8DA275FC4634 >"$TMP/dk" + unhex FA014F168757DD2CEE22644E799A9592644E886C29BF70E3ABFF5C540F6DAF1561DEE71948A3782CB834CF96FDB205E6BCE9B5B151C8F01AE6326868B57C2CCBDE03FC23F9A3DD7422BD0DA2C53BC961821E927941854C3D55DE42643422F1BD31AEEFF355B88391E0A4A8416DE41328D71EA2E36FA6839C800BCC2212B91B2D485267960085A20A088FD2C48F748BFB56C285E83E1A1C0A2E4A2138595F857B8EBBB2B43FD2584BC24BFB1AF8BB5D2D4BFCFD5D610F0E73D12E817D4D00A643BFD9AD53587EB5F610BAEB81EC59A93F24E81B16C0258A56A3FDF4BC5FF67BE67CFAAA2C38CF5E4FF909763BE8060610E74445050CA99D21C1505CE717B615084D37CFA10827986F91C719695284B5EBFA7720D0C3A4099576C50E9BE2A71DB8DBDC6B247A47E9130FFA5E4007BCB8D095998EAA826BE2D3EAD7F787CFE08ED6394DB72EDC160B0D3EDA203674B4F96A5A2DBF052AD8F803CAF6572BC5A2A540F72927F05BFF5EC2DDE529C982E379E12D4260C3780C7F70DE4B6E6CA451117045ADBFA6D77F7538807F221B3AEEA18263E649ABEB3143F60A8FDCA3A6414537F5824932CEC4F5859C4827F9CBC30DCB21DBE909BB1188FF8553875D0A4328094E7A6DE57EB72407F4870FC2771201F70F19A796F8F8907E1DFB1080659389D8B900235D5C53398D5CA487B5D5A86686D6A016B4FAE247BE9D473474FD04FB7BA6DC54D00824779965022A4946C0418760ED5DCC97AD4B68EB4C29A279A9DF962CCC7ED81259D4B1EA6E772532E16925648C5C04B08272FEB81EDF280FBECCC5CD62B21A74EAA828A4473C20873A1668873464C57129C2BBA570B6037508AB3A6AE2702052759F9FBE620D775FEBCDEA9B8F0FDBAB0FBE730E9D43E414A5B29D949E27BDE8E08E9B2260B9CA274924224386505DB5CB43689CF5A6C495824DC7E4C43D970D7636CBD0B4A7B3664C13D383897989DA503ED18238BF37514D0E426EABAC4F5DC85172DFC0260EAA9A83DC72A22A88CF8EEC7DC1AD771BC6ED5C6D45F1203C86E80DCD38F4C3F5959A1EC45B1035A6C5263D026B3016A51F1D8216DC6517C38667AC13BB2C42FFAEB5464533B3BAD89325F1DB37774D9136B0F5DB38FDAEB4D3104D08657FD380441156097AD602C46F54F27E41C2F9451087776E0FF7BBB90F5F497B9241F9FB4AA598E76BFC62D9C59F1EA4EAC29E3B03E46C0D66D134D3B451315CBA69E0EF05E49C4211B2941F779FE2DC4E65AE37D2FA5A3AC117CC6A3439B0ACDA4255DD57347AF7A9FA6F995AC9811CAF7BFE64427335C1D3DED4C0A57DF4F03AD1166D17163CA2EC16BCE65B47DB0243326D143058FC8D8D619EA7D9B08C28A66B1D87D05FF336CED546627AB8AE8904942E3448B01F9030D415DDBAFFEBE0E5141FFB43DACB29A626C7942E8A8F3D539FEF5FDB9CF3E5DE83105C4E154971403A125FEE8F42E2EF7783B7C3ADD9047F661469C61048ECFD2A7945179C773AF46DFD70E5BB09F45A7498403CDDDBA9E2A58A1DC9A2470B >"$TMP/c" + got=$(hex_out "$BC_RUST" mlkem768 decaps --skfile "$TMP/dk" -x <"$TMP/c") + assert_eq "$got" "f347ebcf8e03cc1195b1a7a02899249b7ff9f5fc48144013989bb94b9ab8d1d3" "shared secret (ct on stdin)" + got=$(hex_out "$BC_RUST" mlkem768 decaps --skfile "$TMP/dk" --ctfile "$TMP/c" -x "$TMP/dk.hex" + got=$(hex "$TMP/c" | hex_out "$BC_RUST" --hex-input mlkem768 decaps --skfile "$TMP/dk.hex" -x) + assert_eq "$got" "f347ebcf8e03cc1195b1a7a02899249b7ff9f5fc48144013989bb94b9ab8d1d3" "shared secret (hex dk file and hex ct, with --hex-input)" +} + +# `keygen` draws from the OS; its output must be a ML-KEM-768 key (FIPS 203 Table 3 dk length), and +# encaps/decaps with it must agree on the shared secret through --ctfile and through stdout. +test_mlkem768_keygen_encaps_decaps_round_trip() { + local ss_enc ss_dec lines + "$BC_RUST" mlkem768 keygen >"$TMP/dk" + assert_size "$TMP/dk" 2400 "dk length of ML-KEM-768" + "$BC_RUST" mlkem768 pk-from-sk --skfile "$TMP/dk" >"$TMP/ek" + assert_size "$TMP/ek" 1184 "ek length of ML-KEM-768" + expect_ok "the pair must be consistent" "$BC_RUST" mlkem768 check-consistency --skfile "$TMP/dk" --pkfile "$TMP/ek" /dev/null + # `-x` would also render the --ctfile as hex ("the ciphertext to file also in binary or hex"), so the + # secret is hex-encoded here and the ciphertext file stays binary. + ss_enc=$("$BC_RUST" mlkem768 encaps --pkfile "$TMP/ek" --ctfile "$TMP/ct" "$TMP/seed" + assert_size "$TMP/seed" 64 "seed length" + got=$(hex_out "$BC_RUST" mlkem1024 keygen-from-seed -x <"$TMP/seed") + assert_eq "$got" "e40b1a2c116bfac97fb37b0d20c13f436c0ba5e348eab3815cb61b2118970779ad1a539abeb3948fac2e5748a3e2866df4fa7c83b277a4b102eb6911b6642589107060d67779d59628665951168e2748b2a4ab56716887366427b31777912c70421380299619a4a64e61a60c12376885f6cd5d98c80ac35d1f293d2c643d90e790144b1d673496d9c98f4a44047215989a18655b767352e1049bc71fb6e375b16a2ca15559c7ec12396b08445731b7f497e6b02411b18d56a2cf0e099e8611cdff86b778720225d72726f8122d6b2b360014f7d8cdac58171c78af0aa37e1624509f435a5e4984dbb254f13bbe1deb0d4e04047bd14e341788308358c2453df02b879a16590cd61709d2c77fdbc4a5ccbb817cb70649255e050f12441507d442c78262ab98030a13311ad81a406474cbc729b4c2459d2ab0f0ea0911448ef456c37cac70ab890ebde72369628f14235a2838afe3c6912256a970279cb61c8234226f1f153d5f3b924b10b154bbb5ac576ab7f7482cc06e6fd533f367328bd4a4340c06f8c639a6303165f40fce564d3cb187a51a5224e94bbfb817a0e03cefab77bb1c5ed887a9a00c51f2b34741f078dd625fd7a49fbc0c7e5544455e599ede0a92e9fbc583f132b62b8c8286cfd4c81d65e785b7b44fec127dd6a46885d409fe254cfb787107b201ba80ca63e85abc5a7e49f421e7a6b34b62ce9bd6ce55c28153fc7f55f00b4169a574bcb61ca08f02f17b10dc298e5ba19aa8c6a4c74c5fb9a196caac201937dee779c2d11fe6fa9d6ff5570f70cfae93c216484ff0634dbd1647f40ccd164ab1cfb67216b5a6eb2b3417078ab2c386a63471003aaf233cacc4679c684ca557ba3a275577582103af34257d01806a166b57229c63c040778678776a9ca981b7fa62451e82476f44ce07fb0d115328159469c88a3f2a3a47dd3484f48885802948e6853706757bedfa8f95565dccb1685732b601879572f45eca5a4baf84b8b72623d4450c34c7c92255932580c18d71cf3d8255efc38aab4b5370cc044d6c3b322a6d8f79355c229b674b8a3bea8670433985f7b5de01aa95332cbd1c2706ea716c904f45e88419a04241ea3d32d5c30f732a13071b34f33b91c742c14662e752865bf63e65f3bdcae19a8aa5408ebb9bacbb6e20a8062e1b954692a61668755209890f191dbd0ca3b3d11260e0560ab72787507221cb1a38e0a48d7539c7e249ddb519cfc0974a242b81a5469dab08cb50c3884c14ed8bc177c78e397782e544ab889c42cfa78bc263c0682470d6908f930903a0e719fd701b60fb3218c84609738099d09aea5c142ea443cf536980d72e37b16b4aa8a81cc16ea9b7b4e5904f97375ab883be601ca0932974cdc60574f2c50222a38015157818281aa32d24cc1126238260ba8d60b3a3d38912c558ac3134ac4a8298c5052661b71a430aacd6d7bec0f70425726071f2b39cb5030e71007d5257c588200dfbb915557943b629bbf3ae55f0cd546013ed7ccecc6475b5fc60adf6633b63537f4c95db609358515da9217daef1c30e5455a66b30d4b1b1a1d4746e62caba887d7341807a9c99bbf4880a5896e225b13b443ec99a2726f3bd9473a813913846d2b67c403018867781eac0b8045ae0e02e03160f865b8cba0140f83744dbd70bdaabbea4f31911565deee4aef6b3ac32eb65793ac115e7a798e80a2a856731f56f50218b6e35a5d882032732a62906bbf00a82c6249fe7824664b3b3f8633dbb7aa83fc3c0a6812efc6a238b449aa8494ee0dc28e36a42d4ca509f62038e4b2f73e3a603096777161289542c83f64abbd70c810a3ccc31a81be9b6a45a4b333540b554c0ddf423360491e22998b22b1ff582545ae43c41651aa95b31f7892d54dc2c9f1024d768c70d6597919c749ed9b45e62aab143ae1e4c7334d07a9c235247bbadb7c04b188a7bb8706ca7263f76191e99c5675a547acd1a746bb09a6aa4c8dcd844efd5b321c7486bcb6d299719b6388574166722516249ecb678b390629ba45339a746da0532bc60cce08fcaf0c4048c951a035057c6b594ba94a5071db32284dab54e1c664835438370fa535835b97ad4093668949a156667926890d42adb587e501c496a903b0e66c5be619ad7c165ba920a9069b69c004d99170eca975b8ad48f9f3132e47784ead421e056238af4916f5b326529800584098ee0b4c6d2430f02296826c09c987fa75061a543abecdc6b3287acec29cb818b336b2c65fa099684a179c9c7b24d5c00a4a09cac4300536430d1f05a9eb4ba31682aef0a91adbb0555680c112c5709b8b0c52683651c0df4d8c9f6c09d74d45465da78743455b9b2803bea86ca41531f556128cccfd8c8ae70b057e090ac2a347f57d185326c2bee8c3819d340301071a077ad84d296c095359c97010ec6a42693340636268d48204f707610387862152c805bac1370bd2c99a3f3c2842ae17c69b98152c112e98aa703b9526e8c138875a873f598e689bc22b6cb51f7a7c7b760507b36c6cbbe0052030d593fb3b9073f446ec0a010616a0aac2baeaeda262e9bcca8c7911273ccbe3832937707ec22591a9bc62d2114bcfa1614bb85b3c7934d53c47695a1a4087e85d00b301cad6ffb70d38836bf916015757b38fa60dada5f0a99892fea429443b17c5715c9c75726f63d528565ea587d1b5a3065344f4660466c7c80f8c4213f5246a3c75b03624cf11967926a21297c13929baaa0639dc3128633d34e081c5c4792374bc58e24c6375e0314b79947e6794a255852ac67300bb8086e5088df849ce4722cfe0420c8b18bd25c9da378874542b33e233f1ef37eb7b7167349cdd1e70c50a115082824e8d4963a022691136be606695fc385794bcff394bb42754be21228b7c32456d5bde7bb5ce4ca2e419c2f0f5c3d53058393105eb89359400a87a1e8c57037551461a09653990762651e964552599c115578ebd1926a64ccdca75ce358991058cd57a5b8719468d69a53d7304ddf69c4bdd0c15a94015cf54009054f41dc4346235742ab4ea261952642c579b204a6c2a6c436554fc605d9f9cb43099c4fba5b978437391b3cfd73a89528c92ca69a7f7b30e2263eeb462c3b530b88206e990030076aa6ec386f7bf517002ba3c4180c3d75362e188e3a0372ecc87267e9ac40ba7b5b79c55980aec82bc2b9921c84f2643b298ed0dc2b5dcb0a3ee15e7ef959ff6272c6d10b299102b13959dd8bb1129c8d38862bddc75e16a3b4cd097d3ec2848a28aa3f0bc742d1ba2afb478a0635275775f245be96a8494d62120c7069f3d21a19f40879a521df87b1cb465da07a69ebc0c6b43626dc1c225ba97fc09b0d6c7189b1f669da9ace3c2a7b3151741643cee5d35df471a26e053ad2474521f5ae56e062b8727e206571263465fafc9103aa4b556438e8a9a31c65a22f0249d081538fd63ecf15ce400224bd5654717b0e977a2d802486c989c4ca23bf0ff24415622e4f6b63f2047c2aa0a57d0753347a455e605aeb5392466a52c2e5a73789bd96c46a86f1552ce511b7a1a64f7a215b582b987a344005224c771b885301797608cdc2a89505a47946715072754599377e06139361cadeeba59a948a9a36ca2ccb3da1d040bee237c8d81c35d0b2381666a2351a373230c22393dac770ca62403c6a925ef2bd629638ec5478cf2c4cfa5b715cdb7256d365971183ed1320874bc56428cd12412dc9fa790e71aeec077e07d91f268bc38f334e6b6068888818171a71fd360a6e7505c5d37eb0b3101290a689414878b62629217cbe0125474a4d90a59d50093dcce45f2ebb551f6205fffb7b16b0c952fb72ddc723aeb9cb1fa074a1e07d2ef4c4a7612b5f14a4d2e807da7349a035c7cc85b332f263cef387a5d36bfb55930ef96a04fa2bdbf508c94aa071e7699ba96b59798c0096b0a491a0125c0179a8940a8795c7bc6a44f34a7af212f5e5244a92a41a0b339d63377208151154408d3c3c127a5641e1002246a747678694a4c316d7951bd370d9104a15c7584ee60c019ca9f0a965fb0c7517a1c333f470bac01c252a8ef075664dd1173f2829b854b6f75b535ddc09314b31fd62370e676bdd569948215aba79b1afa11c47344e21f02dbc60351607bc22aab412d024d08002be73a49677c77fe7ac4872645166b1b9963641a106e9bb7092ac1a16498cdbf37de579aa95c9227baaa32ae74bc2b1240ab50bfba4645bf7682b1ca6e424adb9e1c56e7b7380ba32ffdb2b314ab46517bfe501a7d1f60a06b53ea82308a3fa69036603dbc45f61f9be4f171016212491c2c6f48327ef32cc9c28b4939c3d17a6f8643d51c76c84c6eddfac7a1d03818d3fb856ff960fe9ce96fe16d651fd545328c20d5a2c0e139a49460a9f63cb77cea230165de90fcb5f5aba64ba7d101c1f1215068d3665997c3446b16c0ce29d95f0ed17434e620bb95a4045" "dk from a raw seed" + got=$(printf '%s' 07262F4FF6B3CC09E050D438FF648D9C8AAD96B3479EE81D4AD88E6B7E1829ACBA7D101C1F1215068D3665997C3446B16C0CE29D95F0ED17434E620BB95A4045 | hex_out "$BC_RUST" --hex-input mlkem1024 keygen-from-seed -x) + assert_eq "$got" "e40b1a2c116bfac97fb37b0d20c13f436c0ba5e348eab3815cb61b2118970779ad1a539abeb3948fac2e5748a3e2866df4fa7c83b277a4b102eb6911b6642589107060d67779d59628665951168e2748b2a4ab56716887366427b31777912c70421380299619a4a64e61a60c12376885f6cd5d98c80ac35d1f293d2c643d90e790144b1d673496d9c98f4a44047215989a18655b767352e1049bc71fb6e375b16a2ca15559c7ec12396b08445731b7f497e6b02411b18d56a2cf0e099e8611cdff86b778720225d72726f8122d6b2b360014f7d8cdac58171c78af0aa37e1624509f435a5e4984dbb254f13bbe1deb0d4e04047bd14e341788308358c2453df02b879a16590cd61709d2c77fdbc4a5ccbb817cb70649255e050f12441507d442c78262ab98030a13311ad81a406474cbc729b4c2459d2ab0f0ea0911448ef456c37cac70ab890ebde72369628f14235a2838afe3c6912256a970279cb61c8234226f1f153d5f3b924b10b154bbb5ac576ab7f7482cc06e6fd533f367328bd4a4340c06f8c639a6303165f40fce564d3cb187a51a5224e94bbfb817a0e03cefab77bb1c5ed887a9a00c51f2b34741f078dd625fd7a49fbc0c7e5544455e599ede0a92e9fbc583f132b62b8c8286cfd4c81d65e785b7b44fec127dd6a46885d409fe254cfb787107b201ba80ca63e85abc5a7e49f421e7a6b34b62ce9bd6ce55c28153fc7f55f00b4169a574bcb61ca08f02f17b10dc298e5ba19aa8c6a4c74c5fb9a196caac201937dee779c2d11fe6fa9d6ff5570f70cfae93c216484ff0634dbd1647f40ccd164ab1cfb67216b5a6eb2b3417078ab2c386a63471003aaf233cacc4679c684ca557ba3a275577582103af34257d01806a166b57229c63c040778678776a9ca981b7fa62451e82476f44ce07fb0d115328159469c88a3f2a3a47dd3484f48885802948e6853706757bedfa8f95565dccb1685732b601879572f45eca5a4baf84b8b72623d4450c34c7c92255932580c18d71cf3d8255efc38aab4b5370cc044d6c3b322a6d8f79355c229b674b8a3bea8670433985f7b5de01aa95332cbd1c2706ea716c904f45e88419a04241ea3d32d5c30f732a13071b34f33b91c742c14662e752865bf63e65f3bdcae19a8aa5408ebb9bacbb6e20a8062e1b954692a61668755209890f191dbd0ca3b3d11260e0560ab72787507221cb1a38e0a48d7539c7e249ddb519cfc0974a242b81a5469dab08cb50c3884c14ed8bc177c78e397782e544ab889c42cfa78bc263c0682470d6908f930903a0e719fd701b60fb3218c84609738099d09aea5c142ea443cf536980d72e37b16b4aa8a81cc16ea9b7b4e5904f97375ab883be601ca0932974cdc60574f2c50222a38015157818281aa32d24cc1126238260ba8d60b3a3d38912c558ac3134ac4a8298c5052661b71a430aacd6d7bec0f70425726071f2b39cb5030e71007d5257c588200dfbb915557943b629bbf3ae55f0cd546013ed7ccecc6475b5fc60adf6633b63537f4c95db609358515da9217daef1c30e5455a66b30d4b1b1a1d4746e62caba887d7341807a9c99bbf4880a5896e225b13b443ec99a2726f3bd9473a813913846d2b67c403018867781eac0b8045ae0e02e03160f865b8cba0140f83744dbd70bdaabbea4f31911565deee4aef6b3ac32eb65793ac115e7a798e80a2a856731f56f50218b6e35a5d882032732a62906bbf00a82c6249fe7824664b3b3f8633dbb7aa83fc3c0a6812efc6a238b449aa8494ee0dc28e36a42d4ca509f62038e4b2f73e3a603096777161289542c83f64abbd70c810a3ccc31a81be9b6a45a4b333540b554c0ddf423360491e22998b22b1ff582545ae43c41651aa95b31f7892d54dc2c9f1024d768c70d6597919c749ed9b45e62aab143ae1e4c7334d07a9c235247bbadb7c04b188a7bb8706ca7263f76191e99c5675a547acd1a746bb09a6aa4c8dcd844efd5b321c7486bcb6d299719b6388574166722516249ecb678b390629ba45339a746da0532bc60cce08fcaf0c4048c951a035057c6b594ba94a5071db32284dab54e1c664835438370fa535835b97ad4093668949a156667926890d42adb587e501c496a903b0e66c5be619ad7c165ba920a9069b69c004d99170eca975b8ad48f9f3132e47784ead421e056238af4916f5b326529800584098ee0b4c6d2430f02296826c09c987fa75061a543abecdc6b3287acec29cb818b336b2c65fa099684a179c9c7b24d5c00a4a09cac4300536430d1f05a9eb4ba31682aef0a91adbb0555680c112c5709b8b0c52683651c0df4d8c9f6c09d74d45465da78743455b9b2803bea86ca41531f556128cccfd8c8ae70b057e090ac2a347f57d185326c2bee8c3819d340301071a077ad84d296c095359c97010ec6a42693340636268d48204f707610387862152c805bac1370bd2c99a3f3c2842ae17c69b98152c112e98aa703b9526e8c138875a873f598e689bc22b6cb51f7a7c7b760507b36c6cbbe0052030d593fb3b9073f446ec0a010616a0aac2baeaeda262e9bcca8c7911273ccbe3832937707ec22591a9bc62d2114bcfa1614bb85b3c7934d53c47695a1a4087e85d00b301cad6ffb70d38836bf916015757b38fa60dada5f0a99892fea429443b17c5715c9c75726f63d528565ea587d1b5a3065344f4660466c7c80f8c4213f5246a3c75b03624cf11967926a21297c13929baaa0639dc3128633d34e081c5c4792374bc58e24c6375e0314b79947e6794a255852ac67300bb8086e5088df849ce4722cfe0420c8b18bd25c9da378874542b33e233f1ef37eb7b7167349cdd1e70c50a115082824e8d4963a022691136be606695fc385794bcff394bb42754be21228b7c32456d5bde7bb5ce4ca2e419c2f0f5c3d53058393105eb89359400a87a1e8c57037551461a09653990762651e964552599c115578ebd1926a64ccdca75ce358991058cd57a5b8719468d69a53d7304ddf69c4bdd0c15a94015cf54009054f41dc4346235742ab4ea261952642c579b204a6c2a6c436554fc605d9f9cb43099c4fba5b978437391b3cfd73a89528c92ca69a7f7b30e2263eeb462c3b530b88206e990030076aa6ec386f7bf517002ba3c4180c3d75362e188e3a0372ecc87267e9ac40ba7b5b79c55980aec82bc2b9921c84f2643b298ed0dc2b5dcb0a3ee15e7ef959ff6272c6d10b299102b13959dd8bb1129c8d38862bddc75e16a3b4cd097d3ec2848a28aa3f0bc742d1ba2afb478a0635275775f245be96a8494d62120c7069f3d21a19f40879a521df87b1cb465da07a69ebc0c6b43626dc1c225ba97fc09b0d6c7189b1f669da9ace3c2a7b3151741643cee5d35df471a26e053ad2474521f5ae56e062b8727e206571263465fafc9103aa4b556438e8a9a31c65a22f0249d081538fd63ecf15ce400224bd5654717b0e977a2d802486c989c4ca23bf0ff24415622e4f6b63f2047c2aa0a57d0753347a455e605aeb5392466a52c2e5a73789bd96c46a86f1552ce511b7a1a64f7a215b582b987a344005224c771b885301797608cdc2a89505a47946715072754599377e06139361cadeeba59a948a9a36ca2ccb3da1d040bee237c8d81c35d0b2381666a2351a373230c22393dac770ca62403c6a925ef2bd629638ec5478cf2c4cfa5b715cdb7256d365971183ed1320874bc56428cd12412dc9fa790e71aeec077e07d91f268bc38f334e6b6068888818171a71fd360a6e7505c5d37eb0b3101290a689414878b62629217cbe0125474a4d90a59d50093dcce45f2ebb551f6205fffb7b16b0c952fb72ddc723aeb9cb1fa074a1e07d2ef4c4a7612b5f14a4d2e807da7349a035c7cc85b332f263cef387a5d36bfb55930ef96a04fa2bdbf508c94aa071e7699ba96b59798c0096b0a491a0125c0179a8940a8795c7bc6a44f34a7af212f5e5244a92a41a0b339d63377208151154408d3c3c127a5641e1002246a747678694a4c316d7951bd370d9104a15c7584ee60c019ca9f0a965fb0c7517a1c333f470bac01c252a8ef075664dd1173f2829b854b6f75b535ddc09314b31fd62370e676bdd569948215aba79b1afa11c47344e21f02dbc60351607bc22aab412d024d08002be73a49677c77fe7ac4872645166b1b9963641a106e9bb7092ac1a16498cdbf37de579aa95c9227baaa32ae74bc2b1240ab50bfba4645bf7682b1ca6e424adb9e1c56e7b7380ba32ffdb2b314ab46517bfe501a7d1f60a06b53ea82308a3fa69036603dbc45f61f9be4f171016212491c2c6f48327ef32cc9c28b4939c3d17a6f8643d51c76c84c6eddfac7a1d03818d3fb856ff960fe9ce96fe16d651fd545328c20d5a2c0e139a49460a9f63cb77cea230165de90fcb5f5aba64ba7d101c1f1215068d3665997c3446b16c0ce29d95f0ed17434e620bb95a4045" "dk from a hex seed" + "$BC_RUST" mlkem1024 keygen-from-seed <"$TMP/seed" >"$TMP/dk" + got=$(hex_out "$BC_RUST" mlkem1024 pk-from-sk -x <"$TMP/dk") + assert_eq "$got" "ca975b8ad48f9f3132e47784ead421e056238af4916f5b326529800584098ee0b4c6d2430f02296826c09c987fa75061a543abecdc6b3287acec29cb818b336b2c65fa099684a179c9c7b24d5c00a4a09cac4300536430d1f05a9eb4ba31682aef0a91adbb0555680c112c5709b8b0c52683651c0df4d8c9f6c09d74d45465da78743455b9b2803bea86ca41531f556128cccfd8c8ae70b057e090ac2a347f57d185326c2bee8c3819d340301071a077ad84d296c095359c97010ec6a42693340636268d48204f707610387862152c805bac1370bd2c99a3f3c2842ae17c69b98152c112e98aa703b9526e8c138875a873f598e689bc22b6cb51f7a7c7b760507b36c6cbbe0052030d593fb3b9073f446ec0a010616a0aac2baeaeda262e9bcca8c7911273ccbe3832937707ec22591a9bc62d2114bcfa1614bb85b3c7934d53c47695a1a4087e85d00b301cad6ffb70d38836bf916015757b38fa60dada5f0a99892fea429443b17c5715c9c75726f63d528565ea587d1b5a3065344f4660466c7c80f8c4213f5246a3c75b03624cf11967926a21297c13929baaa0639dc3128633d34e081c5c4792374bc58e24c6375e0314b79947e6794a255852ac67300bb8086e5088df849ce4722cfe0420c8b18bd25c9da378874542b33e233f1ef37eb7b7167349cdd1e70c50a115082824e8d4963a022691136be606695fc385794bcff394bb42754be21228b7c32456d5bde7bb5ce4ca2e419c2f0f5c3d53058393105eb89359400a87a1e8c57037551461a09653990762651e964552599c115578ebd1926a64ccdca75ce358991058cd57a5b8719468d69a53d7304ddf69c4bdd0c15a94015cf54009054f41dc4346235742ab4ea261952642c579b204a6c2a6c436554fc605d9f9cb43099c4fba5b978437391b3cfd73a89528c92ca69a7f7b30e2263eeb462c3b530b88206e990030076aa6ec386f7bf517002ba3c4180c3d75362e188e3a0372ecc87267e9ac40ba7b5b79c55980aec82bc2b9921c84f2643b298ed0dc2b5dcb0a3ee15e7ef959ff6272c6d10b299102b13959dd8bb1129c8d38862bddc75e16a3b4cd097d3ec2848a28aa3f0bc742d1ba2afb478a0635275775f245be96a8494d62120c7069f3d21a19f40879a521df87b1cb465da07a69ebc0c6b43626dc1c225ba97fc09b0d6c7189b1f669da9ace3c2a7b3151741643cee5d35df471a26e053ad2474521f5ae56e062b8727e206571263465fafc9103aa4b556438e8a9a31c65a22f0249d081538fd63ecf15ce400224bd5654717b0e977a2d802486c989c4ca23bf0ff24415622e4f6b63f2047c2aa0a57d0753347a455e605aeb5392466a52c2e5a73789bd96c46a86f1552ce511b7a1a64f7a215b582b987a344005224c771b885301797608cdc2a89505a47946715072754599377e06139361cadeeba59a948a9a36ca2ccb3da1d040bee237c8d81c35d0b2381666a2351a373230c22393dac770ca62403c6a925ef2bd629638ec5478cf2c4cfa5b715cdb7256d365971183ed1320874bc56428cd12412dc9fa790e71aeec077e07d91f268bc38f334e6b6068888818171a71fd360a6e7505c5d37eb0b3101290a689414878b62629217cbe0125474a4d90a59d50093dcce45f2ebb551f6205fffb7b16b0c952fb72ddc723aeb9cb1fa074a1e07d2ef4c4a7612b5f14a4d2e807da7349a035c7cc85b332f263cef387a5d36bfb55930ef96a04fa2bdbf508c94aa071e7699ba96b59798c0096b0a491a0125c0179a8940a8795c7bc6a44f34a7af212f5e5244a92a41a0b339d63377208151154408d3c3c127a5641e1002246a747678694a4c316d7951bd370d9104a15c7584ee60c019ca9f0a965fb0c7517a1c333f470bac01c252a8ef075664dd1173f2829b854b6f75b535ddc09314b31fd62370e676bdd569948215aba79b1afa11c47344e21f02dbc60351607bc22aab412d024d08002be73a49677c77fe7ac4872645166b1b9963641a106e9bb7092ac1a16498cdbf37de579aa95c9227baaa32ae74bc2b1240ab50bfba4645bf7682b1ca6e424adb9e1c56e7b7380ba32ffdb2b314ab46517bfe501a7d1f60a06b53ea82308a3fa69036603dbc45f61f9be4f171016212491c2c6f48327ef32cc9c28b4939c3d17a6f8643d51c76c84c6eddfac7a1d03818d3fb856ff960fe9ce96fe16" "ek from the dk" + expect_ok "the pair must be consistent" "$BC_RUST" mlkem1024 check-consistency --pkfile <(unhex CA975B8AD48F9F3132E47784EAD421E056238AF4916F5B326529800584098EE0B4C6D2430F02296826C09C987FA75061A543ABECDC6B3287ACEC29CB818B336B2C65FA099684A179C9C7B24D5C00A4A09CAC4300536430D1F05A9EB4BA31682AEF0A91ADBB0555680C112C5709B8B0C52683651C0DF4D8C9F6C09D74D45465DA78743455B9B2803BEA86CA41531F556128CCCFD8C8AE70B057E090AC2A347F57D185326C2BEE8C3819D340301071A077AD84D296C095359C97010EC6A42693340636268D48204F707610387862152C805BAC1370BD2C99A3F3C2842AE17C69B98152C112E98AA703B9526E8C138875A873F598E689BC22B6CB51F7A7C7B760507B36C6CBBE0052030D593FB3B9073F446EC0A010616A0AAC2BAEAEDA262E9BCCA8C7911273CCBE3832937707EC22591A9BC62D2114BCFA1614BB85B3C7934D53C47695A1A4087E85D00B301CAD6FFB70D38836BF916015757B38FA60DADA5F0A99892FEA429443B17C5715C9C75726F63D528565EA587D1B5A3065344F4660466C7C80F8C4213F5246A3C75B03624CF11967926A21297C13929BAAA0639DC3128633D34E081C5C4792374BC58E24C6375E0314B79947E6794A255852AC67300BB8086E5088DF849CE4722CFE0420C8B18BD25C9DA378874542B33E233F1EF37EB7B7167349CDD1E70C50A115082824E8D4963A022691136BE606695FC385794BCFF394BB42754BE21228B7C32456D5BDE7BB5CE4CA2E419C2F0F5C3D53058393105EB89359400A87A1E8C57037551461A09653990762651E964552599C115578EBD1926A64CCDCA75CE358991058CD57A5B8719468D69A53D7304DDF69C4BDD0C15A94015CF54009054F41DC4346235742AB4EA261952642C579B204A6C2A6C436554FC605D9F9CB43099C4FBA5B978437391B3CFD73A89528C92CA69A7F7B30E2263EEB462C3B530B88206E990030076AA6EC386F7BF517002BA3C4180C3D75362E188E3A0372ECC87267E9AC40BA7B5B79C55980AEC82BC2B9921C84F2643B298ED0DC2B5DCB0A3EE15E7EF959FF6272C6D10B299102B13959DD8BB1129C8D38862BDDC75E16A3B4CD097D3EC2848A28AA3F0BC742D1BA2AFB478A0635275775F245BE96A8494D62120C7069F3D21A19F40879A521DF87B1CB465DA07A69EBC0C6B43626DC1C225BA97FC09B0D6C7189B1F669DA9ACE3C2A7B3151741643CEE5D35DF471A26E053AD2474521F5AE56E062B8727E206571263465FAFC9103AA4B556438E8A9A31C65A22F0249D081538FD63ECF15CE400224BD5654717B0E977A2D802486C989C4CA23BF0FF24415622E4F6B63F2047C2AA0A57D0753347A455E605AEB5392466A52C2E5A73789BD96C46A86F1552CE511B7A1A64F7A215B582B987A344005224C771B885301797608CDC2A89505A47946715072754599377E06139361CADEEBA59A948A9A36CA2CCB3DA1D040BEE237C8D81C35D0B2381666A2351A373230C22393DAC770CA62403C6A925EF2BD629638EC5478CF2C4CFA5B715CDB7256D365971183ED1320874BC56428CD12412DC9FA790E71AEEC077E07D91F268BC38F334E6B6068888818171A71FD360A6E7505C5D37EB0B3101290A689414878B62629217CBE0125474A4D90A59D50093DCCE45F2EBB551F6205FFFB7B16B0C952FB72DDC723AEB9CB1FA074A1E07D2EF4C4A7612B5F14A4D2E807DA7349A035C7CC85B332F263CEF387A5D36BFB55930EF96A04FA2BDBF508C94AA071E7699BA96B59798C0096B0A491A0125C0179A8940A8795C7BC6A44F34A7AF212F5E5244A92A41A0B339D63377208151154408D3C3C127A5641E1002246A747678694A4C316D7951BD370D9104A15C7584EE60C019CA9F0A965FB0C7517A1C333F470BAC01C252A8EF075664DD1173F2829B854B6F75B535DDC09314B31FD62370E676BDD569948215ABA79B1AFA11C47344E21F02DBC60351607BC22AAB412D024D08002BE73A49677C77FE7AC4872645166B1B9963641A106E9BB7092AC1A16498CDBF37DE579AA95C9227BAAA32AE74BC2B1240AB50BFBA4645BF7682B1CA6E424ADB9E1C56E7B7380BA32FFDB2B314AB46517BFE501A7D1F60A06B53EA82308A3FA69036603DBC45F61F9BE4F171016212491C2C6F48327EF32CC9C28B4939C3D17A6F8643D51C76C84C6EDDFAC7A1D03818D3FB856FF960FE9CE96FE16) <"$TMP/dk" >/dev/null +} + +# ACVP ML-KEM encapDecap, decapsulation (ML-KEM-encapDecap.txt, ML-KEM-1024 tcId 26): dk and c give k. +test_mlkem1024_decaps_matches_acvp() { + local got + unhex A2E32D4F2C80368140FCBCA14F841FE27635A1384B402C9D79037A58F04546C7A1EF3A610AA21E977214BD97BBE9840294E267F2A97FD8AB6220789CCAB5C710A9ABFCB34D89936182D52C63C273FAD72C59606B06B4C0AE73897C0BAD2893775930C108016EE573944E8934C7707330A5B44EE854F2F82DF2F06E4AB97FEE4C3DFB966911A729E8697C57642D19283B040983ECF9361D566B48B702642CA0FA756BA03C13BE11ADE7C20422F07DE40A6507253D8F82756FD29D6F601624B8A832461436F441A5BC249A466F880B6377593B340514995A1A8D74BC41C449ED5A53818A8DA62021323CBEBD81BA7D1B6B0E28B66D284791F76FD7494741504BFAF319F009359A80654007BC45563C26B4444F29C9ADEC967C4C439A88B9CCE165A0DB48FEF80F7C770D9799B5B330405E819FCF85CF01F22C63E518C376281C3C35FB8A2CF929502720261467093051CD0DCC3BEEAA987C5A0D60226981B1865FC3B5D0476B0FC17BD8F647FC28336DFC605A231A1FFAB13933AC5D9A82D7A1ACD672CB02653964C10193949F9EF91F93105ED0822D8CA37BF6B74A8DC63739872F3F273360AA352BEB2C931B65C6A5A45175557E72561121B4038958C4C52459D890BDC2CB4E841CCFAC20B9233730C6A5D6864683397D40333D7B537373C953F6725E2FE40B938381293B4D7ACA6982E6CF4E982958188296EC531F535433B13C19BB8AD091AF648495DE7BCEA7282112C74CD868B086C0408EF60357059232526EC4871B2070632A151424B6CE1E53BAB2A62E63018915B1A40FA57E6A1A623B963FF3DA42FCE6B682FA0EE243165E62907067CE3FA331AA796034C5532866C70581A1012446DD1A1BDDEC4A37F24365E33A7EFA23333B869461C697111BEDA6CC24571E7C18BD99D95933348B4D4098E7C592A91A1A7193AD893C0A91790F04588315D98D6169685B63A845C148EFC318D1D9315366A0FB93B5EED25839A25FC05AB985E8B2CA46CB238B57C77217389667F80A209C8B7D7DAA1B63A4A549A57A05294A9443688A755F66D9A45F225B1475C4F8CA7DAF5A63EA55A49E2342B1DB9191B1CBF7F6536BF40A15710E9ED00CF2891378F5850024B8B3284D726C8F95989A253BB0C49AC7EBEAC6B8ACB966F74FCDD6078F1BBEB02651B019C68B1C27622564B833A3046768EAC5C37E17C51286890D52A7A6763444BC7BFF24322498896765202E71CF10416170E483DD6189AAD3A83CB3B66FC7489EF00D38C3C741492D2730760706B3BE8C5ACE9372C1AA78127B309AF753DCB3A565C78045019B4F809F6326C3A3166228C57F87E3B423536C650355B9828FBB712E0CC9C163810A4E4A5B32F310F5A2510E41A6AFBB8C2B6C11F8E91DC0D38A831868ED12A3CFC25B38F88B17708C99F86B31CC6CCEAB8FCB265E239BA0B1165A4D9B1D0F858E756A2388C3C2F9CC4D8B37C0A913B8C961CC6848B371F3A86A9191B9548C30E502FAE3051523B3867219AE91C8F5F4C8A75B8115F5BC8CA69EAC900C260A214DB6C657564DFFC968F335886B89224E3A45A78C047BC17749305230C9B7E864304D328E13B26300042851840AF3098C51A911EEA6377A5C70853800835A0FA72B4137BA885CB905966660D8987646E7BCBB68A2A1C85EE07C103F702D5FF87025E70AF4DA765458328C415A2FEA9164A087C9904AA132BC96D08C38D5AB17947822BA6894FB772B05898A734CF6DC173BD03D35DAA51067C76E200420AC87E672AFD98053BC943493F35068926D01F9A8112756773090547807A2EC8C573799D00817121B29D01626B5937B2180B0A63AC7645436E5291ABD3083E582AB13E3882293C3D47B8B89C8007134736E703276F3BD95D899C04B0F0046AB6FDC88B098C3114438A412C91991727E8A223DDB9C23D54046F92B1E642BAA497C2031434E5999B9B51E18549E86F08AF368CA818A1A295498BDA5AA90A3B38CCC8B7E051C1FC207A9A619D8305EFF1824F59283B0634460B67A4B2B30C80636745A5A4CE7321700403833B6A85978469B04FF819B19A87A205C0C70B263D8FC514958968CE8603A9643627847A1131646C6307BFA3B55D7A9DC23495537BD32070D17E39327C12C399B7E047762320C4CD74C98B041019EC5CC1A451E7C0120532394C3349057F508F3129367EC75B4574E18457B9D245C5D5B480F514D01D6895E81B82A37A9AD83C4A120B9F86CA0810BBC30D29EF6E13E9C1A6EA7D833B2E7B5AB855C6B1B33202510EC0A2B50D8CDB1B583514B990EE8062E18CA261A6DDE7342DBFA637F645B27C7645A497A43D10CBFE5C60D2A4B2135BC6710C7B6F07443E369A6F14858BB24EB4A7FA741920B8B6E88000FD4F1383A5385BCAB90FD6719C9DBCBFE9B271F5B37B94054D706ACCFA164A4C8656A33B6BAE9697159398729C7B817A0EA14CD41931D02E0669FC680B2802CED844436E1C1E6F2A3E0C2C53619944A8B1B81E942989125FECC1ACA166B5E5777D4C543A8B5448B2428475A969D7C6F7297C19E307887FB4FE470213B102351447FCE5728DD8A30883ACD62E903DD9C68179C0B1EB99D0BF5B200323730EA0408C463BB7AA009CA0474E26A9B354CC29C1CEC6147D68CA29EA130CA29C8AE74C5AA850325494B52437960489896872C38D3841EC9A00B25CD5C906BB55214C0E1302C7B1C3F416BAC2861A40BAF3D97B570982CAFB39D05561783298758D434288014025572CD932DDCAA25E3227E1382CCB337A9EE6CCC573595E1BC5FBCE6C1EEE71D68E1BA39C337D6F92A966C76433158EF06BF7FD959F22969DDD418BA432B06D01E5B0886F7D7BFB462728E0BBEE5152264965DA944C4D87097A1CB3D4539990A75666FE32457927711A22999145502080DE5D13E25D1907009058CF6C73906CE81F7C9CF08449BD78BB6ECAE19F8AE511B70B12268251499D1244BA253026501A2C3BA24C71A4101ECBD497BC3E9A57C8E1657A0499F1F6B0DB25455B23C88D9FB3F2316472E696EA30A4EA2A008A5DABEB3EA5A81A384795008DFDBB21A6274931B5A86A5962ED44DEA007C9F62AFD891AD39CC66B5CB2C2998297E349DEC6A4140F922F3851649377A6E685EEB5B423D52C15586C10288AF9A4935D27C1F92AB8ADF38080656BB9EAB9D57C3414A1A130F6C90773375757C433479AA41D64F65F31D23774AA4289924099713C76AADACBB93F441FF24AD09B99940E49942E1B1A088293C58815CA72832B007D0708E103BAD61899DCF203E419A72BBEC614AE098171B89BCD2231725A2C5C38AA2E4C2F0C1C7F164579F5C75ABC3B0ACD60F5F38A720420934B5CDF02C7B77678681203F3386AD4AC72354F4A37DC6B8BF1ACFD0305B30560E04E531705876052A14B5E298A5D34226A0ACCEF92287B2B567055527D906BAA388A4262F447C78338B92CF5957266228AA747F9ABA9F0D57A32AB6C37BC464BC8922E7781FD6607CAC5134F9C5CC1784BFC68B7B93596D20958B1F977DC9D580CC461841F029F1C4C45D0554D3747796399464F337A9218293ABA0D3677F44B608B8C9A66EEAB7311C8BE8B45C8E6AC064096AF6D15573D4B0FEA5B1412B6E40F7B950A62C864B1FDC774498F1AD2D868077F0C8FC920517E77D844639A5B515CF602135889C20084EDEABC1ACCAAE5DDC724F875F64474CC2873635B48D7F1BBA69E64683A4A739AACEA8EBBE919683CF7B7B581996555A9721D14A1F542D08641D8F74C333E38AE3B30DDBD9974592923715772F4B0760C3B0FF8CC977E433779620D9CB21AE036443C4AAFE1C1B15209866B20F4E2407575A8F94C344AF1794861C6F89B52B9B1A5B14D39127C74F66AA185B482D14C933DC95C235FB026E617250187C606A1A81280079062C72AB9326D371526427EA7061F8C4AF8E4346B80112AD5BBE307290FD2A40E29CB5EC6A4582396AA3B68E03144EBB5497EB04B286B68C9D35B1FF2B6C86B1105CF3C6FEE47D4A07CB897990C5685E4A08828A9ACF05991C37A685480ACB7A4A7403CC56BC332291C81BD9F344BD13824A9111BA72CDC0228677B8B433E693C1C87E4E2505776AC38699A5C8D80CFD4A11BE047519978E35E34D4D52A7B3593FEA2756E95155C1D794EA00783594633F135B8B28648E3616597CC194C7A522F11E34842F7EA868CDB7340BF2B2CA45BCE0F83D17FCA935399B2DB2C4D686460AE6A4297092FF4633C5D181FC86AABDEB5ED07B634E11A22DD080F5A851CB7AAF4A23327F55BCB908A516981641B85E0550A29413CBB60024066A26BA0B53E949142CB89070D4A1F47A7BE9A1CDD640982E344C998B79B69867F6A3FC6DA567B342E2EFAD0434B5202A86047159F0C1011B08FB226C23F73AC5E8BD666F91A9ACADCDB47D9CBE921EAEDF0E8B4AAC3318C0D8407ADFAAD51CCACC4ABD50AFEB9B6C216EDC3098DBDC65CD0792669DB8E48544638E097D >"$TMP/dk" + unhex B5A1AC0A3BBAE8F621A52CA15EE6C0B21FECB8E7D3C788CDAB60FF155D2C2FB58AC61BED65BE35800A9E82BFFC89BCC11398DFAAED1074B3DC59DC87AC13A16B3C4883ED188F181DD041EC6F9BF678F5E27FF4323B4A6D57CAC1C5C70679E295D4F3DFEE539BDDC2DFAD183F0A3F5A3B12A13B39410CA529095CE7183AEC29ABBE6EC303A100AC821C02291A39B4728C9F38FC3E0AE80B718246158C31A0E35FA5F6398946A79682AAFE0E3E7663668A56B7FCAD85A7A74BECAEF6CB70B2E8E645B9807061CDEAA78D3A9A818E7DAA4B2E4FE735165FF7F0097A506AE603E9B630FF2F0701662B8CB6F4D5374C3E017BB2678C9CF4C0564B96D5FE23C5051D93F420A404277B0181AD4171E0C888DCCE329E700461A6250DD7E335B8FB6E27793789775F54F31104919E0C73C3802966C0A1D867CFB1E2D02B9A6D0E919D509F1622E3DF865A5E5F907A702918949094A785F7778E6EEA66D2E9239F42BC8F83ED58B7480B0B60951181C550F6FFDD10491483C2D342C848E5216F7465FD58657646263A9159970D2019BB603BAB375A18F82C763D640262C0B7DEB01ADA00D03CA6AD9A1B65FA3491A43A6E5D4FA366259766E46A5C0C44C8CCE94D787A7511EE5E7E303AE2C386E6D7E0A5DA21504A90D68B5EDDEDE1788ABD426DC43D071CE84DE34BF6354355E4223F6EDEED6DCE2C8851412133FD498DC5BCEE5C9B59AC3588EE96D8362D7835DAA90132EDCB1F6AAF99551F1C8271BF23D773CF336F334081EE1CC8E37E4EACE29E756902A9BBA77E6E32208B1791C41124B8F4DC7096B5084DADC55ABD9AE533FDFFD75AE2EE0A43CD99D4950C548FFBB46ED3135E70C0DFC0E9EE3EA5BF71E2C3B37675E83F4954A78F7F79899812764DB8560EFD303FF3928894436CF0D2AFC0F50F718C65645DC6BADD9C948F3B1C84E58B85BFA7F902FA45CA800A86D544FBABA14B0A809985CEA155C93FA3E9823BC7A418D70EB0AC9A259DC673E4287EAEA83DC85480EC79CEEBC4A5D00925B1C444059203F940647CFA9C26A6762BC9DFE45C0A33D505F712ADEBC20DA9C03970750966C078465EA2A76ED8158158EB198E9377FE4E3D15F0FFD3F0A92115028666C073C13DE3AD8679E8CF8FA5921FCE9A11660942E6379807FEDB3EC07F1658150160DC4217FCCFEE39F1B1FF91A4A5C081E10C009CBF166E47E0CAD337EA7FC0571B9A44DBE10A8B4A8BCDB7025370D865AB551FCB1AC4B7DDE85D751F37B91DA3CB75FC8ED7BF0BF557DB563CE4DE93159051E132AD44F68B18A29DD76EDB8BB03B0318C79A72EE67501867AE577BE1E36D76534E1A52ED1B1FD8CEEFA84F5781D8AB555DD41A31665B48F7699A04DCD1AB2ED59726116F37808486061DA4A9C99455CF1D1B773FD8802C5CC7189C14806119595E6990862F2B0D576031D5D76FF4E7DB0AF6977638DCD4C777BB281645666B59CDA5C10BAED0900C684DDD38D6BDB20425B2D31851F71048DA3F26CB890C9FB8940ADD4B2DCA8C0925A6FCA11CEE9A813A43BCC416B34D12770D9B64A01C917BDE03AD0C7075A148F3A61C40B468B26304F3EEEC1B96CC7B13A271D4DF33471F834CAD503FDA8EECEF125054B81ED1FE1F02B4D70CFAEBD7FD5402EEBA35E321B602C9919F9ED26548E1A144A5A0136752060B72E93A75E8BC6FB8FABCE5B9D84CB0E167C5A448BD16AD19AB52AD529EBB2EFAB32BEAAA5167CE1411C7ABC31195299F7F895D4FE301D4B1568DDA730CF8EAB35A973E64F6B96E8E411A892D547B466496273261FFA11EF56122D43B41E0540CE278C19E8312C8DCBB2DC9704F2C2D1AF7ED04676F61D31B51185DE079ED085A259E4C74974E6FD539416A7FE45C6B835769C7D8153841D8A906C45FF8EB151D1FE9C88C21B89D2C2B1938D41BCF468D5C935701F99E5930419FFF3337A2684FBE508EAC73471A1F87059DE5E49E9FE5E65DE1D10615F3127DC633360BF99AD6111FE21F15B2D6A258E63BB4138B97D85731FC0D3D5F291117A5F7E0D5F56CA325183A63EE489FA4C81811905741B35E702D741F7B8FA7D719D3727C097D889430DCEAF4097055ED2005A2BED2912C4DCABF98EFF609362925CDD8CED954131B54D3D041D7A3B06EBAF21FBB85A30A380CD1A112A6FA4890543B3273FAD3AED569BB4CA37799904DCEC77765AF6EADB6DF2A2AD8A4424A79D678172354 >"$TMP/c" + got=$(hex_out "$BC_RUST" mlkem1024 decaps --skfile "$TMP/dk" -x <"$TMP/c") + assert_eq "$got" "09f3db05409636d3d9e3ebc09790fc6feb7b37b23a321a7f3fb9e10ebb1c5c0a" "shared secret (ct on stdin)" + got=$(hex_out "$BC_RUST" mlkem1024 decaps --skfile "$TMP/dk" --ctfile "$TMP/c" -x "$TMP/dk.hex" + got=$(hex "$TMP/c" | hex_out "$BC_RUST" --hex-input mlkem1024 decaps --skfile "$TMP/dk.hex" -x) + assert_eq "$got" "09f3db05409636d3d9e3ebc09790fc6feb7b37b23a321a7f3fb9e10ebb1c5c0a" "shared secret (hex dk file and hex ct, with --hex-input)" +} + +# `keygen` draws from the OS; its output must be a ML-KEM-1024 key (FIPS 203 Table 3 dk length), and +# encaps/decaps with it must agree on the shared secret through --ctfile and through stdout. +test_mlkem1024_keygen_encaps_decaps_round_trip() { + local ss_enc ss_dec lines + "$BC_RUST" mlkem1024 keygen >"$TMP/dk" + assert_size "$TMP/dk" 3168 "dk length of ML-KEM-1024" + "$BC_RUST" mlkem1024 pk-from-sk --skfile "$TMP/dk" >"$TMP/ek" + assert_size "$TMP/ek" 1568 "ek length of ML-KEM-1024" + expect_ok "the pair must be consistent" "$BC_RUST" mlkem1024 check-consistency --skfile "$TMP/dk" --pkfile "$TMP/ek" /dev/null + # `-x` would also render the --ctfile as hex ("the ciphertext to file also in binary or hex"), so the + # secret is hex-encoded here and the ciphertext file stays binary. + ss_enc=$("$BC_RUST" mlkem1024 encaps --pkfile "$TMP/ek" --ctfile "$TMP/ct" "$TMP/dk" + rng 700 >"$TMP/short-ct" + expect_fail "short ct" "$BC_RUST" mlkem512 decaps --skfile "$TMP/dk" <"$TMP/short-ct" + assert_stderr_has "Error" + rng 100 >"$TMP/junk" + expect_fail "junk dk" "$BC_RUST" mlkem512 pk-from-sk --skfile "$TMP/junk" "$TMP/seed33" + expect_fail "33-byte seed" "$BC_RUST" mlkem512 keygen-from-seed <"$TMP/seed33" + grep -q panicked "$LAST_STDERR" && fail "a bad seed must not panic: $(head -1 "$LAST_STDERR")" + assert_stderr_has "Error" +} + +# A hex key file is read with --hex-input; without the flag it is raw bytes, however hex-like, +# and is refused as a key of the wrong length rather than guessed at. A trailing newline on the +# hex is fine; a comment line is not hex and is refused cleanly. +test_hex_key_files_need_the_hex_input_flag() { + local cmd + for cmd in mlkem512 mlkem768 mlkem1024; do + "$BC_RUST" $cmd keygen-from-seed < <(rng 64) >"$TMP/dk" + "$BC_RUST" $cmd pk-from-sk --skfile "$TMP/dk" >"$TMP/ek" + { hex "$TMP/ek"; printf '\n'; } >"$TMP/ek.hex" + expect_ok "$cmd encaps with a hex ek and --hex-input" "$BC_RUST" --hex-input $cmd encaps --pkfile "$TMP/ek.hex" --ctfile "$TMP/ct" /dev/null + expect_fail "$cmd encaps with a hex ek and no --hex-input" "$BC_RUST" $cmd encaps --pkfile "$TMP/ek.hex" --ctfile "$TMP/ct" "$TMP/ek.commented" + expect_fail "$cmd: a comment line is not hex" "$BC_RUST" --hex-input $cmd encaps --pkfile "$TMP/ek.commented" --ctfile "$TMP/ct" "$TMP/out" + assert_size "$TMP/out" "$len" "rng --len $len" + done +} + +test_hex_output_is_two_chars_per_byte_plus_a_newline() { + local len + for len in 0 1 1024 1025; do + "$BC_RUST" rng --len "$len" -x >"$TMP/out" + assert_size "$TMP/out" $((2 * len + 1)) "rng --len $len -x" + [ "$(tail -c 1 "$TMP/out" | od -An -c | tr -d ' ')" = '\n' ] || fail "hex output must end in a newline" + [ "$(head -c $((2 * len)) "$TMP/out" | tr -d '0-9a-f' | wc -c | tr -d ' ')" -eq 0 ] || fail "hex output must be lower-case hex" + done +} + +test_successive_draws_differ() { + "$BC_RUST" rng --len 32 >"$TMP/a" + "$BC_RUST" rng --len 32 >"$TMP/b" + assert_differs "$TMP/a" "$TMP/b" "two 32-byte draws" +} + +test_len_is_validated_by_clap() { + expect_fail "negative" "$BC_RUST" rng --len -1 + expect_fail "non-numeric" "$BC_RUST" rng --len abc + expect_fail "over u32" "$BC_RUST" rng --len 4294967296 +} + +# Without --len the command streams until the reader goes away; a closed pipe is a quiet exit 0. +test_streaming_until_the_reader_closes_exits_zero() { + local status + "$BC_RUST" rng 2>"$TMP/err" | head -c 100000 >"$TMP/out" + status=${PIPESTATUS[0]} + assert_eq "$status" 0 "exit status on a closed pipe" + assert_size "$TMP/out" 100000 "the reader got what it asked for" +} + +# --seed-file instantiates the DRBG from the file (SP 800-90A Rev. 1 Section 10.1.1.2 with the +# file as the entropy input), so the same file gives the same bytes, another file different ones, +# and a file too short for the instance's strength is refused with a message. +test_seed_file_gives_a_reproducible_stream() { + rng 32 >"$TMP/seed" + rng 32 >"$TMP/seed2" + "$BC_RUST" rng --len 1000 --seed-file "$TMP/seed" >"$TMP/a" + "$BC_RUST" rng --len 1000 --seed-file "$TMP/seed" >"$TMP/b" + assert_same "$TMP/a" "$TMP/b" "the same seed file must give the same stream" + "$BC_RUST" rng --len 1000 --seed-file "$TMP/seed2" >"$TMP/c" + assert_differs "$TMP/a" "$TMP/c" "another seed file must give another stream" + assert_eq "$(hex_out "$BC_RUST" --hex-input rng --len 16 --seed-file <(hex "$TMP/seed") -x)" "$(head -c 16 "$TMP/a" | hex_out "$BC_RUST" hex-encode)" "a hex seed file with --hex-input is the same seed" + rng 16 >"$TMP/short" + expect_fail "16 bytes is below the 256-bit instance" "$BC_RUST" rng --len 16 --seed-file "$TMP/short" + assert_stderr_has "Error" + "$BC_RUST" rng --len 16 --hash sha256 --seed-file "$TMP/short" >"$TMP/d" + assert_size "$TMP/d" 16 "16 bytes seeds the 128-bit SHA-256 instance" +} + +test_hash_selects_the_drbg() { + "$BC_RUST" rng --len 100 --hash sha256 >"$TMP/a" + assert_size "$TMP/a" 100 "rng --hash sha256" + "$BC_RUST" rng --len 100 --hash sha512 >"$TMP/b" + assert_size "$TMP/b" 100 "rng --hash sha512" + expect_fail "an unknown hash" "$BC_RUST" rng --len 1 --hash md5 +} + +run_all diff --git a/cli/tests/test_sha2.sh b/cli/tests/test_sha2.sh new file mode 100755 index 000000000..065dc6d5c --- /dev/null +++ b/cli/tests/test_sha2.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# The sha224 / sha256 / sha384 / sha512 / sha512-224 / sha512-256 subcommands against the CAVP SHS known answers. +# +# Generated for the 2026-11-03 CLI review (plan 15) from the vector files named in each test; +# every expected value is copied from that file, none is typed by hand. Follows lib.sh's +# conventions: each test_* runs in its own subshell under `set -e`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# CAVP SHS byte-oriented SHA224ShortMsg.rsp (bc-test-data/crypto/sha2/byte-oriented), Len = 0 and Len = 64. +test_sha224_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha224 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha224 -x <"$TMP/msg") + assert_eq "$got" "bdf21ff325f754157ccf417f4855360a72e8fd117d28c8fe7da3ea38" "sha224 of an 8-byte message" +} + +# CAVP SHS byte-oriented SHA256ShortMsg.rsp (bc-test-data/crypto/sha2/byte-oriented), Len = 0 and Len = 64. +test_sha256_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha256 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha256 -x <"$TMP/msg") + assert_eq "$got" "963bb88f27f512777aab6c8b1a02c70ec0ad651d428f870036e1917120fb48bf" "sha256 of an 8-byte message" +} + +# CAVP SHS byte-oriented SHA384ShortMsg.rsp (bc-test-data/crypto/sha2/byte-oriented), Len = 0 and Len = 64. +test_sha384_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha384 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha384 -x <"$TMP/msg") + assert_eq "$got" "a3d861d866c1362423eb21c6bec8e44b74ce993c55baa2b6640567560ebecdaeda07183dbbbd95e0f522caee5ddbdaf0" "sha384 of an 8-byte message" +} + +# CAVP SHS byte-oriented SHA512ShortMsg.rsp (bc-test-data/crypto/sha2/byte-oriented), Len = 0 and Len = 64. +test_sha512_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha512 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha512 -x <"$TMP/msg") + assert_eq "$got" "a3941def2803c8dfc08f20c06ba7e9a332ae0c67e47ae57365c243ef40059b11be22c91da6a80c2cff0742a8f4bcd941bdee0b861ec872b215433ce8dcf3c031" "sha512 of an 8-byte message" +} + +# CAVP SHS byte-oriented SHA512_224ShortMsg.rsp (bc-test-data/crypto/sha2/byte-oriented), Len = 0 and Len = 64. +test_sha512_224_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha512-224 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha512-224 -x <"$TMP/msg") + assert_eq "$got" "21f6c373637e6a5e89d6e88811110c5c3fa12e497144912914c546e1" "sha512-224 of an 8-byte message" +} + +# CAVP SHS byte-oriented SHA512_256ShortMsg.rsp (bc-test-data/crypto/sha2/byte-oriented), Len = 0 and Len = 64. +test_sha512_256_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha512-256 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha512-256 -x <"$TMP/msg") + assert_eq "$got" "d25265bf9cbc0dd2f108a2f5e8f69db7d15e5b8fe9100fe887dae20b6e054fe8" "sha512-256 of an 8-byte message" +} + +# CAVP SHA256LongMsg.rsp, the first message of at least 3 KiB (Len = 25064 bits = 3133 bytes), so the +# 1 KiB stdin chunking (`cli/src/sha2_cmd.rs:29`) is crossed several times; fed both from a file and +# through a pipe. +test_sha256_matches_cavp_long_msg_across_chunk_boundaries() { + local got + unhex df753c3c6eb6f4e34dde6ee5388b5b818196c4e7a951439e2d0d7223a2e0a4d304a5d30a4c43da8d4033e4258f3c258d1d81c88e9cbe28695ccf427564726b09c14d3e9c1e33d92b95a349dcc59142e02569139d9752da0d41b0f46198ecb8ebbd77653a0d0c5887f436835001131a4240578f5cc493c15283b8712f67d7f53fa12a302c4158c16703f57bc96c48157b16522313490b45ffcaef9f27f8b989ce146f87b9e19e7b946de719912eba9c525062b7094e42cfdec05e6791741ddc60bf4d522b1567eed6d81446ef6ef11590968f3487fcbfc5b50b0cb1b4028856bd4acc79c8073a82f4ed85e82a5474eafd8fe280d57a0a914d530166d9cb845fac5cf4e4d9dc9f9fb85341e88b0acb8161e462843bf1ab996e41466c48f6985ff69dce7ca5bf1eade8b2502f5ab57e44843d182801892a47df7ce2ad49ac4dbe63b7612776d640144db4bba4df8015992e400a556e6910026aaddd10241529e4bb4fb5ce92498c3cafff892e619119ea269842f1cbbae531d57c407c2058b639c610d35a42418b8ed63bc2b72a10129e35ebc8e560a32f3ee99012db869e7c264c9fc2b1cee6c37089116f268026861d23e2a21a754d78906e925b149e0e021114a78abd7e5fbe4b92289763acfbffcfb824da7752e0478f2c29230fb0d1d063e7ce34649ad50488b72a4255f5b6041cbb5a78e33f8325a3ebfe73b4040e6e858dbcc790696f1b8dc1bc78e6a57f1e6cf62b8248835e993472b952ebe2ee65ddce1a4b2834568dc4778842fd0380d3272f40028d679a2baa8a01c99c91993bd57290c91443b1e293a336a78256deaeb52f40ff9a335f636dfb7edfaf5a367bd440c5473cdea9a0640b6eb05ee7c4347ebed482572fd4cae58dd99c0c85edaa410162d0884d66519e78d76477fe58efeba3a492aca222e77b07ba089b5ae852867d0e69e0d70f7ca9b319cbb6e2c26662a8573781d670429533c82c45204cfa0a7c721c2cc6ccaaed81dded03dd2b214c939579be11e7649dd7ac0ff021442b35c636f05460c762d2adcbd34735bba9122d08614b9cafd4f9bc0eb985a3f56c6f2201ee40ee252cc0fd8a84683399851a3e56f610bfef1a13aa9433edef8a45205a7b1cd8b711901f3bc308501fee3dce8e2741f5c1d0bf5169e9840e4d293eb0344d840505b117572d1d68e541e15a95e42448e3ec794a1dc0b3b4d204fbe088791f2ead1e2d48d09c41ab11e09b63d60af415c0861243de789bcc205e27b20ef19820b94baeacf24fd6d9bb4643e0fbc640b076d8c533fb5948d3dfa1d53dab63616412d45cc9261085fbcbb902596be1b01feb2ab8a1de2b63d1f9162f07c79822029e015e2ad2a5f617288200578e23eb6c09c41315eeb693cf55055a9acb598200471add5460d7d038bce96461cc539a9fb6edff1793c281d3dbb6817e1d6c2968f46600d1366ebb636e3557008d3b64bedceea4303516d4e87370ca392799c0428d2ef027be3261a226b000bb39a1f2d3620f29e73c7b3513057d5d5550aaef94f9bad4e15eeb67fccca0881a384eebe53098b6c511b94c5e7923635aff655d682252d5848060787a494e16a5f20af8fd2ed175511a98c0d0b7ab04ce9e94b3c5ef3e1b9a8b5a3a228302d3e5d09cc12244028c13c0e03a71a85d673b94fd98b448c5cdabe7a155fe6304e66138ceed5c5a7223936b58614a3280b5284969b53b1531133dfbfd7216d19b78a1936625b3586a635a84c9de623e5e151e259fb2653ec816da31363970b610b12e4a2d1030263273ef71fb58c2b53db0a490693637acd3fe09f0135116f8abb88fc3eb57daac127cda9cb678707d66ae42f28de9bb4599f353a15d631081b4b64c6868a1bad352a6a46bc2e67680213680b3d474f8f2641c3eaadc16643773d2eea06b97d6813918d448e649dca4b570e73b067ff69c7aa1547458131c043035fcd2bee1389f10fbf29aeb49a8d36c85fef2ebb1be2924ab1d67990140cf3206fe682726e8161f7686c99a8fe4f48c9d4d414ab35783aa6220eb6689d680c26e7dc96f0eecccaeb2193db9c397ef5edf5d8eed2cc99054ede5698953e8fe2895005d5e434eab05d7325d108dcaac2a74b5429ac51d3a6a74f447c8067d33aca20f8cbb6a169a6c0c5a93699384123884dec61a265a673321d6efec4a9e0696be32be502e4a802a7918478a80cd0bcf365e8135f68801f81a12c7bdc9525f2d10f25be334f4478847f4df3cfaad7f38b2bfdab2deb0ef018730390228a126bac1d6cb3d271bd6ce9e76c447a92d54bc21883b5b85d8199691b8ac8512934385f8677626adc81544a5275e73375473a1ab420bf6940b67ff6033880c2d319fd6de2bc5656e02ec31858e1588492bc42f7774b14cb24ec1831b293460218220f59bac1ec414d143df6534524d8ab82fb0aea6972588ab0f6cea201a49978eaf295672ea09443d02946e9f7b8b9f059d6a77612449bd293900f6a2a18e375e35bc370392fda84f1199c859fa0a331af4a6fb2bc07ffad02788673631f9a8f998f467e97c68e80e58369aea3592ecfd1eac618fd390e7a9c24b656532509367c21a0eac1212ac83c0b20cd896eb72b801c4d212c5452bbbf09317b50c5c9fb1997553d2bbc29bb42f5748ad83dc6e538736763eff646f0f9aa6a5b028e575de074f5fe5de425dab8e6b73f0662f88d49749749ae7b55aaba9cd38deecb3bcf6f3ee5a6de5589b70c637b82aa6aa67451847f5aee60295990350f8c6c18d1d02449b6bf037cdfd09bd033de2ab16b4adf47620036511c7e192770cf0103c4009e49ebaf83c682a805d172aa0dd3a1615121f1e20caab99893c8a9ce43f89cc13ba3f700c5cf3cbdd8dbaa8eda4e036845a89168d7e98b39bebd0d22bb8396a29aec6b5b5ff3487263435df4f68cebb02b4fe31d3eaa0bc1e8692d44ce117c151a87ab0798df507653cd0f65091cd4e9808c49779758b5d1bf518cacf24ea7a9590a58ae36efcf2a18cd3157887a5974c3e246f0786203d9920aadd3de8793cfd4e8d6a780a11f1e30c86664eb21e3d283e66e106872805a0ef90341c948db090cb7a79c2cafeac32a4310e5a7b00e934f0d217fddf6c5c394a11f9f792ee7d56edb6df48148aca963c1b3824481242773db03674f6b1eb6a1977515349644c579d88c392517cf417bc8d0a35821df1f90b4769f334c2593d440866ba21b7c59cc4374d29ea9dd1be0ccfac1492e99dc244e0d60301664fbab30d60b382ae8f2c480ca79445b50a9215294749de3eefdbcec3a52bc56475e84ecd67c7d413530f134ff65bcc8c469bbde3035dd0e48f4b9338fff4e88572cffe92a17c7facb84e0b486507e6e92ee2ed4407d7cfa2241a79870ce5d291ef77a9a2395deb4267f01a3e191ffa95696e8e957c3f424256506af7f386eb5093e9384655ef819c07fd277b3d2b7259a048861f891e938acff64dd1dd5f9c89072c94c61ebca784417e1945f7b3b5fb7b76bf2757fa29433414095ef1f94b06f248a36abb4bc6c0bd7adae56f402b43a1021ff80e5e6c3530b088e358db628dfd464f7a5424471922f951a3cf593cfbbbf390d9d5cfc42769d4b1ab398d47e7d02d7f3d13d09057190366c63c8750e97052f911d4d799e287876cfdbd9864dc12051bfc1ed60b4249a10198e051cfd7692641e934fd532f33d2a1200b831f336bd60aff9dbe1fa15bda0a5fe33f218b4e7752a91dc95ad415bb385d4dd6e2b685a9368eb911333ef6b769646dd9aecdc64e1399b6c41799d44d1afca81a142b058586f19955b1dfc33e07efa4a8df6779b26c002875f048188d2d4546d61c5b9673e26f6715004d6979ca47b3331a1f10ab45289a654ea78b855a7f4f92640ede7a325248d6885091709bb6c002c8bf33418657351a9a80d33c8af4ebd6530b85a25ee06622b1afe32a6382f49e520d897d18d6211e3a9998baa3ea402b511c34f34f62e980e33406af08f4768e1ed2bb66e1ed85f998ba83088eaa7918c6079376eb1ff97986a5a30774546f5a96d57004cae389311aad3b2d347cbc261a9549321b61bc9402cb613b8dea068b21e05160bb0257502a3969317ca731dbb68eff2fc86e5d59bf6ee9513dfd64a826b906819d9014de2f25b51d4d7502c200ba5c76d89002502656e2546ad7b0390f29367056d6eb61913ac6f8912c546061e1090e350cd4029d4af549febe13c743f88933d01c0263b74045ca2f15523f42caadffc52dfaf68d14ca3ae0fbb5d92aeea9d4f1aa816b0bffd99b0f7821e6093ef152723a9cb45f7a082ef8d6bdf72cd33b5aa3c79102f43e2b74199decdd20057d0e227ae4c57945582e2e9653a9b16eeacecdbc5aaedac7e35c35cbd9adede7f83bbf36f8b0453d61416a85a17821885b3757d203fa2560a85c4b4c10 >"$TMP/msg" + assert_size "$TMP/msg" 3133 "message length" + got=$(hex_out "$BC_RUST" sha256 -x <"$TMP/msg") + assert_eq "$got" "176b0c71e213031a29f56009aac7ebec591ba24a8b162d80506b2df8f59e11a2" "from a file" + got=$(cat "$TMP/msg" | hex_out "$BC_RUST" sha256 -x) + assert_eq "$got" "176b0c71e213031a29f56009aac7ebec591ba24a8b162d80506b2df8f59e11a2" "through a pipe" +} + +# Binary output is the same digest without the hex rendering; both end in one newline. +test_sha256_binary_output_matches_hex_output() { + rng 100 >"$TMP/msg" + "$BC_RUST" sha256 <"$TMP/msg" >"$TMP/bin" + assert_size "$TMP/bin" 33 "32 digest bytes plus a newline" + assert_eq "$(head -c 32 "$TMP/bin" | "$BC_RUST" hex-encode)" "$(hex_out "$BC_RUST" sha256 -x <"$TMP/msg")" "binary and hex agree" +} + +test_the_subcommands_are_listed_in_help() { + "$BC_RUST" --help >"$TMP/help" + local cmd + for cmd in sha224 sha256 sha384 sha512 sha512-224 sha512-256; do + grep -q "$cmd" "$TMP/help" || fail "--help should list $cmd" + done +} + +run_all diff --git a/cli/tests/test_sha3.sh b/cli/tests/test_sha3.sh new file mode 100755 index 000000000..eb41a306c --- /dev/null +++ b/cli/tests/test_sha3.sh @@ -0,0 +1,257 @@ +#!/usr/bin/env bash +# The sha3-*, shake*, cshake*, kmac*, kmacxof*, tuplehash*, tuplehashxof*, parallelhash* and +# parallelhashxof* subcommands against the CAVP and SP 800-185 known answers. +# +# Written for the 2026-11-03 CLI review (report 15) from the vector files named in each test; +# every expected value is copied from that file, none is typed by hand. Follows lib.sh's +# conventions: each test_* runs in its own subshell under `set -e`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# CAVP SHA3_224ShortMsg.rsp (bc-test-data/crypto/sha3/byte-oriented), Len = 0 and Len = 64. +test_sha3_224_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha3-224 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha3-224 -x <"$TMP/msg") + assert_eq "$got" "7dd1a8e3ffe8c99cc547a69af14bd63b15ac26bd3d36b8a99513e89e" "sha3-224 of an 8-byte message" +} + +# CAVP SHA3_256ShortMsg.rsp (bc-test-data/crypto/sha3/byte-oriented), Len = 0 and Len = 64. +test_sha3_256_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha3-256 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha3-256 -x <"$TMP/msg") + assert_eq "$got" "dbb8be5dec1d715bd117b24566dc3f24f2cc0c799795d0638d9537481ef1e03e" "sha3-256 of an 8-byte message" +} + +# CAVP SHA3_384ShortMsg.rsp (bc-test-data/crypto/sha3/byte-oriented), Len = 0 and Len = 64. +test_sha3_384_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha3-384 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha3-384 -x <"$TMP/msg") + assert_eq "$got" "60ad40f964d0edcf19281e415f7389968275ff613199a069c916a0ff7ef65503b740683162a622b913d43a46559e913c" "sha3-384 of an 8-byte message" +} + +# CAVP SHA3_512ShortMsg.rsp (bc-test-data/crypto/sha3/byte-oriented), Len = 0 and Len = 64. +test_sha3_512_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha3-512 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha3-512 -x <"$TMP/msg") + assert_eq "$got" "03c2ac02de1765497a0a6af466fb64758e3283ed83d02c0edb3904fd3cf296442e790018d4bf4ce55bc869cebb4aa1a799afc9d987e776fef5dfe6628e24de97" "sha3-512 of an 8-byte message" +} + +# CAVP SHA3_256LongMsg.rsp, the first message of at least 3 KiB (Len = 25200 bits), crossing the 1 KiB +# chunking of `helpers::stream_hash` and the 136-byte rate many times. +test_sha3_256_matches_cavp_long_msg() { + local got + unhex d9c7762f278a6ecb070bca56c4f7d7fea31590df217906d47dfb058c76e7f4e056f6fd632f7d6e3b65e55f306c5b9603d3c8a70182045fd7404763a878e0155d3c29b73d8abad3bdceddda99a9420b23f1f496dbf98c024112a5cce7518f51ca9348ede2bfa765f84bdb82b80214ff070480a6970e79b5b8fbfd86718b5e6fcf643ae87d56aeeb95e3c7a1b6ff393a5714541c5a493341e40437da6dadb43913b6e9ed34d8362f3b9f897dba281a84ba2a58434f33226e6f343b100340f8753f913c472fcca6f79385095eed061da5d84c74629b53af03fe94f1705dcb94ecfafd1b3c97ba680c45a0308e7720ab645a8590c0693140ca3c2a4142a0d6ef66ed036e16942ae336f8f5e4547ffe2d8ae8da94a6df563f89ce0014cdf7ea71abc0aa1d1b4da57f3c548e0ef72d2909df2955685c254912095f1e505a888e82821afb1194ebb2a4e8037297c0aa28a92bc6fdf42a64922312958adf317b4a8ab4a3fc30c895daba00aaa965f71e83733666da2158c4bad86c184ea79af9a6f10a04b7630174a4294df43c62e4b1c3d1c8b2f5d52d6c489bde917292dd2a2b1f49e5349385b0985a97863274ce896f2aa85255f9f285c4d331a8fc874135607d3ced7aa69e703eb3a60b9385ffd10fe59fed0276f036b7e72d04f66d0f42cd71aac5918691dc1f9d4129677cbdaf2c6c752b05326ca8a8419a4e672e907bfb645a158119a91ec2813288b741514b4d26f2b66517b1021f48402d58b1090671bf158452492d5bafc53fd18abc03cefa7bdd332a0c066da464e74ad0dec50bb7e8a3ba0dfc64be6fd331ace9d51a60bbd3004d5df8b211c0fd564cd79d0bb35649cc60ba1c976c8911cfc0db74e028199621aa05c5fe15fa7b56dc75d62225d548581e5f900f9085e9e3b668819b4f9b2c09f22a5a32a2db47afa2b371538abc4f0e9b06401150ecc2333598e494fccafe80ced49f96dfeac729459856e60a94c5b780b614e8d4450389e6748513582c724ee60c7c71f5af648b6e2d6e23cce4121b7478f4db451816ab71034c5f8b4bf13ae1d9d90d0bb2869fc4799f51f9349d022053c831cbee62617d4e22c2bcafe40d67449eb04a7c962bf084d2bab80dd0342b4f78338d4d4f75b25bed8214deb18f2254b3a3da94faf89956f0a432f512783e74ec29b4c045adba3497e8ba62c288b711002ee2821cce68f8df588f76cc9801cb0d5b67ccacd33ae1063cd6c37dc0d1836e988acf63750571891ef618645a1b5bc110cffbecaddd6824c692874cff16b3e32bfc0236b417c9d43d8f624387352cf19114d46d0448d3d7cd1438960c2ea8482d5da3ff544608aaff83dcd1e7f64786275ddf989f262a099b845dc2b0c26a86e7d83a251e3c37f2aafa0e764107b36618d2a5d3481d73a1760b7f3ab37a0283a1925010d79e5e94871b819b5e0f787bac9dad87c5d5b887a7d12565ddfd7729a3b66c274a178377de0fbca607b79fab2de37f1ddff800a376fdd7abf5f4d15f346a17d43e4db085f7fe470102a72fe0e1cfa4fb5e2b54dd2ab71e74c506190c9dd6d87f7ae8eca5190fab12178630011286a38b0a18bb1d0d29802813dc561a2724378ec79140bf8e6a6f4310fdabf606330434ab673d4b6578872fa81d90701779bc6aedf0b2bc9c381bfbb4b3a6a705fc505d08c0e24f7bcfbbf24c72cff6b800f07bb4ac4d828ca138a1ca512cfc59090e70eac9b3119754a3d7c6831c9451de5a29e46d16faa6cc4d9f1b7e42dd4983c6d4192db779012fc747e0f77e32293e83dd44dd170c31e2e25c6fe3f2321c9201f15d92f6700089afab6077feb88946f1fd9da257a1770a7f408d2743e31b5d452f7ea179dcdd8ae4690faaa30eb91e2ef3b764de9fbe6dadb33ff33ed88e0d1df6430147a7e159edebb539751d01efc2255accb5f04f32f31d4befe9f9bdc90eae755a68b7511f52971acb7ccba9e133fd4e81e37a7282dae67ac24e2fefbaa2bfdc56e5f678039cd70509c101f81b0be15a2c70276e85e976941575606d086091409bc54a9d2e5212409a4ba837c6d13b487565dd8d62a836618f647580dad08c69d1109d07c7fcaf912822a6af090e72de03c98615429f8ca9d925576e587b191fa1cfd6fff72218c1584fe5a50d49eee57fa4094f4c57e1339ac0ec7face89e29b309ddcde64db498d4d94b64f90a172490fe4e83a9579ec87b3b1efdfb9f8550aec69ab9bb04ebdd44c8c233f6da5565dfe03df98027293024a0d42e7029345c3a0ef50b9fd0255a4f2531592c3f106e1451bef8fa50ec333cf7f65c300d151500ead60ab938cad59ef77f1fc2c3b008d65ab9c93f594d986c466bcd66d38e42a86125ff32c44217d94696cde1ee9dfbcd0216b576cab6295b971dc9380409adda7cca3231fcf6e8a71a5dabb0473f0fa251bd02c82c8eacb1f1f39ace27ed1c3d42adebdf694c0fb07cbc005856ff0cfd4f96f85c053dea185dd58751c4a25879bbb6726392ad569d739b1600a343b985058ae15be02bab36e1abd7e964902937efce0ee6ff82831608e233d1b774dfe37a7f03ec42c0b7b3885dd3bd8e1f561fe7db099039315fc24279a0f319fb46bfb65ea838b564d53857d4c3f6cab7a9d8aac6a988c724b24a2949a58d513eb6bfc2101666c07fcf6d58f6efbba0c0e80b407837f731c14455c53889c7e8a7f78ea57c959fec796bf992ac9e3599a2dc5fb2d8fa040072dc8c1f2f96ca659dd0d267896f0bbfba72850ddf198c3cc54d7d07cd3cd8bb9e44973e1712be50870b341121ead1c3aaabb8d275a9e71191030c4741439aeaac421d2ad646cb32cf6e5baa2b501a80b1e03be858946ea16fc5b57a1b597316069fc6ec567a1791dcc2f03a8ee70d7d38d7b5576e224b73c55c20a943c48bc613223c8b7df6cdf816ce9aca34f8d342a559cf28a19da310fc91f4dca0cc09823b95240f350113ece55c026004ca9357622e3162d74ba1fe7d2ad9191cbb16d2e6be298165e411f7b132cf07e6a77645d51e820dfa766ac7afef796a9ed7d93f7c67f1e8f5e8f1a0e219aac4c2d5a49368f16a32f16429f2f9aa79d60f6a7b506556244ccc52d86fe0d93ba1809d44c9826e93d7a45d0408c521846555ba238e0a546bb5d46df0ed143a29a509477e6f8a5fa207fbab23907a1d40205910fa7fd1286e2cf48061a1e4041dd8431b7c0eea8a42c7de95af127f66550e03b328ad239cd4b814d25314b9febe431761632d09b719213562d0173ec46c383cedd4a0697645621cacedccfcf2b5ed6d605e554fb0cf1ce85bad261eff2da4ea0426de097f6b31f556575313a6a9df2e61920d243c0dc0dfc86fc787de3f5cf64147f16d7feb2d8518ba3168e2431f40ac2addf0227b1603f13d82e12b4b6849ec5f61fb3e705eb53a3eec61702bb78d894c4d1b2bc0253a1d40e095acf8ceca4d95b41d83a0cd6e030a0e681708aefc4021e60d1f84f66d95a068092aef9860ab12743a17238196bd471fe49e820d7418d02f2fc6718f85a284a3a66cd7cef550fe28b9c3a28216ae1ca53dce42dbdf5a81b1e7e72dfa688a1417d43c597670828059044dc5d9d71eb22ada6ff020244f5ef29e4c603f00957a4efc601d9ab88ac08e827d338dc0573035254934e52b3c8c7cbdbbb364330ddbd084a1ca41b7b0e74f9b1d95a6b8a0bf4f8523b431dc6c36d7aafdcd73eb386223d6c70874543b1d25b7f3922da94087fe62c61b0c1f268289cfda58730f6034d1589a813a777589963ec9d3297a80afcd25fb0f7edcb1981c9319f96d87cb7c6274e2da9b08a4542c9865ffa213a8604134883890fb56a1185d845a683bc1605c9166be9ff011a56f2e8fbfa05c00c1e4f8354049645dc50397e1236c138c084916efd34f1504a5e66611bb2c3dcffb3dd9ccccb2ff0e2b67a8f9fc82f7739db06d45c1b260c47c19a9b29a4d9e5f35d4401387d1b5b0fccbf8948bb0cfbd471afd3031d0b99b51906039050c67f6983293d416b06c8d5fb8ef7a7b0083b32d0e2e78cbb209cf4cd4f16fa36f80dd4cde3bd524570deb7db5f572727afb9d781468e4365f36a9976cc61c1d81d15c7860a1df57cc81137e4a724518251fc34fb4133d1bdf2a7157e045081a4742ba44412cb95719ae4d89ffcebb1709d86cdb77974b213bcb6fb7b9d8fa43cbbc41d908432dcd467ee1c65d719c142e0ce144e45dd6f17a03ebdd7ee6bb401425375f3d50016159d8165eebc3b0ba9da46bb184cba632a4b92cf1b7d714b82a67e5fd69bafeba5ca33579312dbfae5d89504add4a828dc2d57b1cec12ca53166893dc13e9ec8156c9863eb815909b76a1d839794334aadb3e2085a601a8badf128924c345935d503151d607e4ec27f1d64da3e01c8230f7de63566c0b75958d4b0efc0136baf751c3de69a7039bfad50ff5b3983f35c81e9c8bb4c798 >"$TMP/msg" + got=$(hex_out "$BC_RUST" sha3-256 -x <"$TMP/msg") + assert_eq "$got" "20e819aa8988e4a3c491673504b59ca44db6703f01094e0d87a85ce9c4d9ac4f" "long message" +} + +# CAVP SHAKE128ShortMsg.rsp ([Outputlen = 128]), Len = 0 and Len = 64, and SHAKE128VariableOut.rsp's +# longest output (COUNT = 1116, Outputlen = 1120 bits = 140 bytes): the +# positional length argument selects the squeeze length. +test_shake128_matches_cavp() { + local got + got=$(hex_out "$BC_RUST" shake128 16 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" shake128 16 -x <"$TMP/msg") + assert_eq "$got" "5ef5578b89c50532131b7843de7329a3" "shake128 of an 8-byte message" + unhex a8ded9816defca8327c194a48a88ae4e >"$TMP/msg" + got=$(hex_out "$BC_RUST" shake128 140 -x <"$TMP/msg") + assert_eq "$got" "ed7397b2215c6c412bf444b1b96fc55c531aef025c6dd13fb4ab53fcc20c91917d82c8d6710a8d7b4c24d18b54150490e98ee01b4a4b9790d1878810a8392d3fa203b066327c0c67cace3a08c57d0d30b62ba43121d8d715637884f055c55ad6689692249885dce01c96979f1a2b309943e14abb0ba8e227bceb381667579e10694bd442b99064aa88501431" "shake128 variable-length output" +} + +# CAVP SHAKE256ShortMsg.rsp ([Outputlen = 256]), Len = 0 and Len = 64, and SHAKE256VariableOut.rsp's +# longest output (COUNT = 1240, Outputlen = 2000 bits = 250 bytes): the +# positional length argument selects the squeeze length. +test_shake256_matches_cavp() { + local got + got=$(hex_out "$BC_RUST" shake256 32 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" shake256 32 -x <"$TMP/msg") + assert_eq "$got" "54f5dddb85f62dba7dc4727d502bdee959fb665bd482bd0ce31cbdd1a042e4b5" "shake256 of an 8-byte message" + unhex dc886df3f69c49513de3627e9481db5871e8ee88eb9f99611541930a8bc885e0 >"$TMP/msg" + got=$(hex_out "$BC_RUST" shake256 250 -x <"$TMP/msg") + assert_eq "$got" "00648afbc5e651649db1fd82936b00dbbc122fb4c877860d385c4950d56de7e096d613d7a3f27ed8f26334b0ccc1407b41dccb23dfaa529818d1125cd5348092524366b85fabb97c6cd1e6066f459bcc566da87ec9b7ba36792d118ac39a4ccef6192bbf3a54af18e57b0c146101f6aeaa822bc4b4c9708b09f0b3bab41bcce964d999d1107bd7c27af989ebe1e104a35478df362ec4c9628f5ea29cc1164b92960d42068a59b1ddcb8875722ed59df36ea654d97b6bf39e5075121ec93f8a8ea6ee5d923997ae8a9d4c315a3d074e3ff83dfa26490f0f5b00a3cc3f110f0f761eaac885a3d1eee302014191733bf77b4d58cca49258897d6460" "shake256 variable-length output" +} + +# SP 800-185 KMAC sample, bc-test-data/crypto/sp800-185/KMAC.rsp COUNT = 0 (kmac128, S = "", +# 32-byte tag). The key is the sample's 32-byte key, passed as hex; the customization string is +# passed as the characters themselves (`-s`), since the CLI takes it as a string. +test_kmac128_sample_count_0() { + local got + unhex 00010203 >"$TMP/msg" + got=$(hex_out "$BC_RUST" kmac128 32 --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f -x <"$TMP/msg") + assert_eq "$got" "e5780b0d3ea6f7d3a429c5706aa43a00fadbd7d49628839e3187243f456ee14e" "KMAC128 COUNT 0" + expect_ok "the sample tag must verify" "$BC_RUST" kmac128 32 --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f --verify "e5780b0d3ea6f7d3a429c5706aa43a00fadbd7d49628839e3187243f456ee14e" <"$TMP/msg" + expect_fail "a different length must not verify the same tag" "$BC_RUST" kmac128 33 --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f --verify "e5780b0d3ea6f7d3a429c5706aa43a00fadbd7d49628839e3187243f456ee14e" <"$TMP/msg" +} + +# SP 800-185 KMAC sample, bc-test-data/crypto/sp800-185/KMAC.rsp COUNT = 1 (kmac128, S = 'My Tagged Application', +# 32-byte tag). The key is the sample's 32-byte key, passed as hex; the customization string is +# passed as the characters themselves (`-s`), since the CLI takes it as a string. +test_kmac128_sample_count_1() { + local got + unhex 00010203 >"$TMP/msg" + got=$(hex_out "$BC_RUST" kmac128 32 -s 'My Tagged Application' --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f -x <"$TMP/msg") + assert_eq "$got" "3b1fba963cd8b0b59e8c1a6d71888b7143651af8ba0a7070c0979e2811324aa5" "KMAC128 COUNT 1" + expect_ok "the sample tag must verify" "$BC_RUST" kmac128 32 -s 'My Tagged Application' --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f --verify "3b1fba963cd8b0b59e8c1a6d71888b7143651af8ba0a7070c0979e2811324aa5" <"$TMP/msg" + expect_fail "a different length must not verify the same tag" "$BC_RUST" kmac128 33 -s 'My Tagged Application' --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f --verify "3b1fba963cd8b0b59e8c1a6d71888b7143651af8ba0a7070c0979e2811324aa5" <"$TMP/msg" +} + +# SP 800-185 KMAC sample, bc-test-data/crypto/sp800-185/KMAC.rsp COUNT = 3 (kmac256, S = 'My Tagged Application', +# 64-byte tag). The key is the sample's 32-byte key, passed as hex; the customization string is +# passed as the characters themselves (`-s`), since the CLI takes it as a string. +test_kmac256_sample_count_3() { + local got + unhex 00010203 >"$TMP/msg" + got=$(hex_out "$BC_RUST" kmac256 64 -s 'My Tagged Application' --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f -x <"$TMP/msg") + assert_eq "$got" "20c570c31346f703c9ac36c61c03cb64c3970d0cfc787e9b79599d273a68d2f7f69d4cc3de9d104a351689f27cf6f5951f0103f33f4f24871024d9c27773a8dd" "KMAC256 COUNT 3" + expect_ok "the sample tag must verify" "$BC_RUST" kmac256 64 -s 'My Tagged Application' --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f --verify "20c570c31346f703c9ac36c61c03cb64c3970d0cfc787e9b79599d273a68d2f7f69d4cc3de9d104a351689f27cf6f5951f0103f33f4f24871024d9c27773a8dd" <"$TMP/msg" + expect_fail "a different length must not verify the same tag" "$BC_RUST" kmac256 65 -s 'My Tagged Application' --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f --verify "20c570c31346f703c9ac36c61c03cb64c3970d0cfc787e9b79599d273a68d2f7f69d4cc3de9d104a351689f27cf6f5951f0103f33f4f24871024d9c27773a8dd" <"$TMP/msg" +} + +# SP 800-185 TupleHash sample, TupleHash.rsp COUNT = 0 (tuplehash128, S = "", 32 bytes). +test_tuplehash128_sample_count_0() { + local got + got=$(hex_out "$BC_RUST" tuplehash128 32 -e 000102 -e 101112131415 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" parallelhash128 32 -b 8 -x <"$TMP/msg") + assert_eq "$got" "ba8dc1d1d979331d3f813603c67f72609ab5e44b94a0b8f9af46514454a2b4f5" "ParallelHash128 COUNT 0" +} + +# SP 800-185 ParallelHash sample, ParallelHash.rsp COUNT = 1 (parallelhash128, B = 8, S = 'Parallel Data', 32 bytes). +test_parallelhash128_sample_count_1() { + local got + unhex 000102030405060710111213141516172021222324252627 >"$TMP/msg" + got=$(hex_out "$BC_RUST" parallelhash128 32 -b 8 -s 'Parallel Data' -x <"$TMP/msg") + assert_eq "$got" "fc484dcb3f84dceedc353438151bee58157d6efed0445a81f165e495795b7206" "ParallelHash128 COUNT 1" +} + +# SP 800-185 ParallelHash sample, ParallelHash.rsp COUNT = 3 (parallelhash256, B = 8, S = "", 64 bytes). +test_parallelhash256_sample_count_3() { + local got + unhex 000102030405060710111213141516172021222324252627 >"$TMP/msg" + got=$(hex_out "$BC_RUST" parallelhash256 64 -b 8 -x <"$TMP/msg") + assert_eq "$got" "bc1ef124da34495e948ead207dd9842235da432d2bbc54b4c110e64c451105531b7f2a3e0ce055c02805e7c2de1fb746af97a1dd01f43b824e31b87612410429" "ParallelHash256 COUNT 3" +} + +# Shorter XOF output is a prefix of longer output (FIPS 202 Sec 6.2 defines SHAKE as a function of +# the output length d: the first d bits). +test_shake128_output_is_a_prefix_of_a_longer_squeeze() { + local full short + full=$(hex_out "$BC_RUST" shake128 200 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" cshake128 32 -s 'Email Signature' -x <"$TMP/msg") + assert_eq "$got" "c1c36925b6409a04f1b504fcbca9d82b4017277cb5ed2b2065fc1d3814d5aaf5" "cSHAKE128 COUNT 0" + got=$(hex_out "$BC_RUST" cshake256 64 -s 'Email Signature' -x <"$TMP/msg") + assert_eq "$got" "d008828e2b80ac9d2218ffee1d070c48b8e4c87bff32c9699d5b6896eee0edd164020e2be0560858d9c00c037e34a96937c561a74c412bb4c746469527281c8c" "cSHAKE256 COUNT 2" + got=$(hex_out "$BC_RUST" cshake128 32 --customization-hex 456d61696c205369676e6174757265 -x <"$TMP/msg") + assert_eq "$got" "c1c36925b6409a04f1b504fcbca9d82b4017277cb5ed2b2065fc1d3814d5aaf5" "the customization given in hex" + assert_eq "$(hex_out "$BC_RUST" cshake128 32 -x <"$TMP/msg")" "$(hex_out "$BC_RUST" shake128 32 -x <"$TMP/msg")" "cSHAKE128 with empty N and S is SHAKE128" + assert_eq "$(hex_out "$BC_RUST" cshake256 32 -x <"$TMP/msg")" "$(hex_out "$BC_RUST" shake256 32 -x <"$TMP/msg")" "cSHAKE256 with empty N and S is SHAKE256" + expect_fail "both customization forms at once" "$BC_RUST" cshake128 32 -s a --customization-hex 61 <"$TMP/msg" +} + +# SP 800-185 KMACXOF sample, KMACXOF.rsp COUNT = 0 (kmacxof128, S = "", 256 bits), and that the XOF +# form is not the fixed-length KMAC of the same length (Sec 4.3.1 encodes L = 0). +test_kmacxof128_sample_count_0() { + local got + unhex 00010203 >"$TMP/msg" + got=$(hex_out "$BC_RUST" kmacxof128 32 --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f -x <"$TMP/msg") + assert_eq "$got" "cd83740bbd92ccc8cf032b1481a0f4460e7ca9dd12b08a0c4031178bacd6ec35" "KMACXOF128 COUNT 0" + [ "$got" != "$(hex_out "$BC_RUST" kmac128 32 --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f -x <"$TMP/msg")" ] || fail "KMACXOF128 must differ from KMAC128" + assert_eq "${got:0:32}" "$(hex_out "$BC_RUST" kmacxof128 16 --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f -x <"$TMP/msg")" "a shorter KMACXOF output is a prefix" +} + +# SP 800-185 TupleHashXOF sample, TupleHashXOF.rsp COUNT = 0 (tuplehashxof128, S = "", 256 bits). +test_tuplehashxof128_sample_count_0() { + local got + got=$(hex_out "$BC_RUST" tuplehashxof128 32 -e 000102 -e 101112131415 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" parallelhashxof128 32 -b 8 -x <"$TMP/msg") + assert_eq "$got" "fe47d661e49ffe5b7d999922c062356750caf552985b8e8ce6667f2727c3c8d3" "ParallelHashXOF128 COUNT 0" +} + +# A XOF squeezes to stdout in pieces, so a long output is a long run rather than an allocation: +# 3 MB from shake128 arrives as exactly that many bytes, and the fixed-length SP 800-185 forms, +# which are computed whole, refuse a request over the command's cap with a pointer to the XOF form. +test_long_xof_output_streams_and_fixed_output_is_capped() { + "$BC_RUST" shake128 3000000 "$TMP/out" + assert_size "$TMP/out" 3000001 "3 MB and a newline" + "$BC_RUST" kmacxof256 2000000 --key 00 "$TMP/out" + assert_size "$TMP/out" 2000001 "2 MB and a newline" + expect_fail "kmac over the cap" "$BC_RUST" kmac128 268435457 --key 00 "$TMP/msg" + "$BC_RUST" sm3 <"$TMP/msg" >"$TMP/bin" + assert_size "$TMP/bin" 33 "32 digest bytes plus a newline" + assert_eq "$(head -c 32 "$TMP/bin" | "$BC_RUST" hex-encode)" "$(hex_out "$BC_RUST" sm3 -x <"$TMP/msg")" "binary and hex agree across chunk boundaries" +} + +run_all From ea159faa1fc385a5686bbc65d135a15754d885a9 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:44:51 +1100 Subject: [PATCH 43/57] ascon: do_decrypt_inplace ends the AAD phase on its first non-empty call, as do_decrypt_out does (verification P190-ascon-1), so the two decrypt entry points run one phase machine; a test drives both with all the data held back and an empty call first Assisted-by: Claude:claude-fable-5-1 --- crypto/ascon/src/ascon_aead128.rs | 6 +++++- crypto/ascon/tests/aead128_tests.rs | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs index 538bc44ea..fe97d8e86 100644 --- a/crypto/ascon/src/ascon_aead128.rs +++ b/crypto/ascon/src/ascon_aead128.rs @@ -608,6 +608,11 @@ impl Ascon_AEAD128 { /// newest `ASCON_AEAD128_TAG_LEN` bytes are held back for the final, so either final can /// follow. Whatever is in `data` past the returned count is not plaintext. pub fn do_decrypt_inplace(&mut self, data: &mut [u8]) -> usize { + // The same phase rule as `do_decrypt_out`: data has started once any ciphertext has + // been seen, even while all of it is held back, and an empty call is a no-op. + if !data.is_empty() { + self.check_data(); + } let release = self.do_decrypt_out_len(data.len()); let from_held = release.min(self.held_len); let from_input = release - from_held; @@ -624,7 +629,6 @@ impl Ascon_AEAD128 { // the input moves right to make room for the held-back bytes ahead of it. data.copy_within(..from_input, from_held); data[..from_held].copy_from_slice(&self.held[..from_held]); - // Called even when `release` is 0, which is what ends the AAD phase. self.decrypt_bytes(&mut data[..release]); self.held = new_held; diff --git a/crypto/ascon/tests/aead128_tests.rs b/crypto/ascon/tests/aead128_tests.rs index 3bae7855b..09d813ecc 100644 --- a/crypto/ascon/tests/aead128_tests.rs +++ b/crypto/ascon/tests/aead128_tests.rs @@ -495,6 +495,29 @@ fn aead128_key_enforces_the_material_policy() { .test::<16, AsconAEAD128Key>(ASCON_AEAD128_NAME, SecurityStrength::_128bit); } +/// The AAD phase ends with the first non-empty data call on either decrypt entry point, even +/// while every byte of it is still held back as a possible tag, and an empty call on either +/// leaves it open: `do_decrypt_inplace` and `do_decrypt_out` run one phase machine, so a caller +/// mixing them sees the same answer from `do_update_aad` whichever it used. +#[test] +fn both_decrypt_entry_points_close_the_aad_phase_on_the_first_non_empty_call() { + let km = aead_key(&KEY); + let held = [0x5au8; 16]; + + let mut d = km.create_decryptor(&NONCE); + assert_eq!(d.do_decrypt_inplace(&mut []), 0); + d.do_update_aad(b"still open").unwrap(); + assert_eq!(d.do_decrypt_inplace(&mut held.clone()), 0, "all held back"); + assert!(matches!(d.do_update_aad(b"late"), Err(SymmetricCipherError::StateError(_)))); + + let mut d = km.create_decryptor(&NONCE); + let mut out = [0u8; 16]; + assert_eq!(d.do_decrypt_out(&[], &mut out).unwrap(), 0); + d.do_update_aad(b"still open").unwrap(); + assert_eq!(d.do_decrypt_out(&held, &mut out).unwrap(), 0, "all held back"); + assert!(matches!(d.do_update_aad(b"late"), Err(SymmetricCipherError::StateError(_)))); +} + /// The two tag layouts must agree byte for byte: `direct_ciphertext || direct_tag`, produced by /// streaming [`Ascon_AEAD128`] and taking the tag from `do_encrypt_final_detached_out`, /// must equal what the inline layout produces for the same key, nonce (driven by the same RNG From 169e499ec52ab989c62388b8c23dda28bfe4d838 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:51:31 +1100 Subject: [PATCH 44/57] utils: the NoPadding safety contract says every byte is initialized, not only that there is no padding (P190-utils-1) Assisted-by: Claude:claude-fable-5-1 --- crypto/utils/src/secret.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crypto/utils/src/secret.rs b/crypto/utils/src/secret.rs index 5480aca76..8d0d4bacd 100644 --- a/crypto/utils/src/secret.rs +++ b/crypto/utils/src/secret.rs @@ -112,7 +112,8 @@ impl ZeroizablePrimitive for [T; N] { /// /// # Safety /// -/// An implementor must guarantee that `Self` has no padding bytes. +/// An implementor must guarantee that every byte of every value of `Self` is initialized: no +/// padding bytes, and no `MaybeUninit` or union field that may hold uninitialized bytes. pub unsafe trait NoPadding {} macro_rules! impl_no_padding { From 9d13f4f23234b0eb7894d397264e1db0d83b6d10 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:51:31 +1100 Subject: [PATCH 45/57] core: the do_update_aad docs carry the empty-call rule the AEAD suite enforces, and from_rng's doc says a zero-capacity key never consults the RNG (P190-cipher-3, P190-core-1) Assisted-by: Claude:claude-fable-5-1 --- crypto/core/src/key_material.rs | 3 ++- crypto/core/src/traits.rs | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crypto/core/src/key_material.rs b/crypto/core/src/key_material.rs index ebc05a219..bf585fc62 100644 --- a/crypto/core/src/key_material.rs +++ b/crypto/core/src/key_material.rs @@ -320,7 +320,8 @@ impl KeyMaterial { /// Creates a new instance of KeyMaterial containing random bytes from the provided random number generator. /// /// The key is tagged at the lesser of the RNG's strength and what `KEY_LEN` bytes can carry; - /// a `KeyMaterial<0>` comes back [`KeyType::Zeroized`]. + /// a `KeyMaterial<0>` comes back [`KeyType::Zeroized`] without consulting the RNG, so an RNG + /// that would refuse (reseed required) is not reported for it. pub fn from_rng(rng: &mut (impl RNG + ?Sized)) -> Result { let mut key = Self::new(); if KEY_LEN == 0 { diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 15dbf5d71..4859c05db 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -54,8 +54,9 @@ pub trait AEADCipherDecryptor Result<(), SymmetricCipherError>; /// Finishes the decryption with the tag detached, consuming the decryptor: decrypts whatever @@ -178,9 +179,10 @@ pub trait AEADCipherEncryptor Result<(), SymmetricCipherError>; From 5c97f57f4f2ad0fcfe4c5fe251a3bed185bc5668 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:51:31 +1100 Subject: [PATCH 46/57] sha3: the squeeze loop comment allows for an empty output (P190-sha3-2) Assisted-by: Claude:claude-fable-5-1 --- crypto/sha3/src/keccak.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crypto/sha3/src/keccak.rs b/crypto/sha3/src/keccak.rs index 86113dd61..04ac5f147 100644 --- a/crypto/sha3/src/keccak.rs +++ b/crypto/sha3/src/keccak.rs @@ -325,7 +325,8 @@ impl KeccakInternal { } // The loop below counts in bits, so a slice whose bit length does not fit a `usize` (512 // MiB on a 32-bit target) would wrap the bound and stop early; squeeze it in pieces that - // fit. On a 64-bit target the piece is larger than any buffer, so this is one iteration. + // fit. On a 64-bit target the piece is larger than any buffer, so this is at most one + // iteration (none for an empty `out`, which has nothing to write). for piece in out.chunks_mut(usize::MAX >> 3) { self.squeeze_into(piece); } From 62897b03edc9a3d9621b25b7d3acf780112a7221 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:51:31 +1100 Subject: [PATCH 47/57] sm3: the HKDF-SM3 test header records the verification review's independent reproduction with OpenSSL 3.6.3 and bc-java 1.86 (P190-sm3-2) Assisted-by: Claude:claude-fable-5-1 --- crypto/sm3/tests/hkdf_sm3_tests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crypto/sm3/tests/hkdf_sm3_tests.rs b/crypto/sm3/tests/hkdf_sm3_tests.rs index 2fe63ef11..9cfd5818a 100644 --- a/crypto/sm3/tests/hkdf_sm3_tests.rs +++ b/crypto/sm3/tests/hkdf_sm3_tests.rs @@ -2,7 +2,8 @@ //! Wycheproof has no SM3 HKDF file), so the known answers are RFC 5869 Appendix A's three //! SHA-256 test-case inputs run through OpenSSL 3.0.13 //! (`openssl kdf -kdfopt digest:SM3 ... HKDF`, `mode:EXTRACT_ONLY` for the PRK), whose SHA-256 -//! output for the same inputs reproduces the RFC's values. +//! output for the same inputs reproduces the RFC's values. The 2026-11-04 verification review +//! reproduced all six PRK and OKM values independently with OpenSSL 3.6.3 and bc-java 1.86. #[cfg(test)] mod hkdf_sm3_tests { From 3ad654019cdf16d825109bd0dfcb8ec9fe1050e8 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:51:31 +1100 Subject: [PATCH 48/57] hkdf: the resume check's comment records that the credited label is bounded only by the cap once initialized, so the next audit does not re-derive it (P190-hkdf-1) Assisted-by: Claude:claude-fable-5-1 --- crypto/hkdf/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crypto/hkdf/src/lib.rs b/crypto/hkdf/src/lib.rs index c7f0d2ae4..a42aae2c2 100644 --- a/crypto/hkdf/src/lib.rs +++ b/crypto/hkdf/src/lib.rs @@ -902,7 +902,10 @@ where // hash's cap (credit_entropy applies it), and a state that has not been initialized // carries no entropy and no strength (new() has none). Otherwise a state whose two // bookkeeping bytes were edited would label a PRK derived from nothing as full-entropy - // material at a strength the hash cannot give. + // material at a strength the hash cannot give. A state past Uninitialized whose entropy + // and strength were edited within the cap still resumes with that label: whether a key + // was credited is not recorded in the state, so the rule cannot be tighter than the cap + // without a new state field (2026-11-04 verification, P190-hkdf-1). if security_strength > HkdfEntropyTracker::::MAX_STRENGTH || (hkdf_state == HkdfStates::Uninitialized && (entropy != 0 || security_strength != SecurityStrength::None)) From 7a25f217b1d118767254a110ba582dcb23b0cc65 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:51:31 +1100 Subject: [PATCH 49/57] base64: the impossible padding arm of do_final is unreachable! rather than panic! (P190-base64-1) Assisted-by: Claude:claude-fable-5-1 --- crypto/base64/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto/base64/src/lib.rs b/crypto/base64/src/lib.rs index be2ad6a98..1b8cf3644 100644 --- a/crypto/base64/src/lib.rs +++ b/crypto/base64/src/lib.rs @@ -338,7 +338,7 @@ impl Base64Decoder { let (mut out, pad_at) = match self.decode_internal(input, false) { Ok(decoded) => decoded, Err(Base64Error::PaddingEncounteredDuringDoUpdate) => { - panic!( + unreachable!( "rollback_if_padding = false should not produce a Base64Error::PaddingEncounteredDuringDoUpdate" ); } From d7b64d287224d5fff677ffe282f0585b08923d84 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:51:31 +1100 Subject: [PATCH 50/57] rng: the nonce doc says new_from_os seeds with twice the strength, SP 800-90A Rev. 1 Section 8.6.7's (3/2 security_strength) alternative, and the CAVP test header notes the LF line endings of the bc-test-data copies (P190-rng-1, P190-rng-2) Assisted-by: Claude:claude-fable-5-1 --- crypto/rng/src/lib.rs | 4 +++- crypto/rng/tests/cavp_hash_drbg.rs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crypto/rng/src/lib.rs b/crypto/rng/src/lib.rs index d4af606b4..fb2df49e2 100644 --- a/crypto/rng/src/lib.rs +++ b/crypto/rng/src/lib.rs @@ -109,7 +109,9 @@ pub trait Sp80090ADrbg { /// and "considered to be a critical security parameter" though it "need not be secret". It is /// not checked here, which is why it is a KeyMaterial: so that it gets the erasure and logging /// protections of one. An empty nonce is accepted and the seed then has to carry the whole - /// requirement on its own (`new_from_os` passes one, with a seed of the full strength). + /// requirement on its own: `new_from_os` passes one, with a seed of twice the strength, which + /// covers Section 8.6.7's alternative of an entropy input of `(3/2 security_strength)` bits + /// in place of a nonce. Neither is checked here (the deferred R6). fn instantiate( &mut self, prediction_resistance: bool, diff --git a/crypto/rng/tests/cavp_hash_drbg.rs b/crypto/rng/tests/cavp_hash_drbg.rs index 32e27e014..eaeb12b4e 100644 --- a/crypto/rng/tests/cavp_hash_drbg.rs +++ b/crypto/rng/tests/cavp_hash_drbg.rs @@ -1,5 +1,6 @@ //! NIST CAVP Hash_DRBG vectors (`drbgtestvectors.zip`, CAVS 14.3, 2013-04-02), from -//! `bc-test-data/crypto/cavp/drbg/`: `drbgvectors_no_reseed/Hash_DRBG.rsp` (instantiate, two +//! `bc-test-data/crypto/cavp/drbg/` (the copies there have LF line endings where the zip has +//! CRLF; the content is otherwise identical and the parser trims): `drbgvectors_no_reseed/Hash_DRBG.rsp` (instantiate, two //! generates) and `drbgvectors_pr_false/Hash_DRBG.rsp` (instantiate, reseed, two generates), the //! SHA-256 and SHA-512 sections, with and without a personalization string and additional input. //! The second generate's output is `ReturnedBits`. `drbgvectors_pr_true` is not read: the DRBG From a86b3b726284a45f95443e689c8adc9ce6b56b34 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:51:31 +1100 Subject: [PATCH 51/57] aes: the round-key widening's cross-target assembly check is in the tree and in CI -- dev_scripts/aes_ct_harness holds the two splat bodies with and without the barrier, dev_scripts/check_aes_ct_asm.sh builds it on nightly with -Zbuild-std=core for thumbv6m and riscv32i and greps the barriered bodies for a multiply, call or branch (none, in 10/20 and 7/14 instructions) and the plain ones for the multiply the barrier prevents, and aes-ct-asm.yml runs it on every pull request; the splat comment records that the architecture-only gate makes 32-bit Arm, x86 and wasm32 pay the barrier too, and the dead allow(unused_imports) above the GCM_NONCE_LEN re-export goes (P190-aes-1, P190-aes-2, P190-aes-3) Assisted-by: Claude:claude-fable-5-1 --- .github/workflows/aes-ct-asm.yml | 26 ++++++++++++ .gitignore | 1 + CLAUDE.md | 8 +++- crypto/aes/src/bitslice.rs | 4 ++ crypto/aes/src/gcm.rs | 3 +- dev_scripts/aes_ct_harness/Cargo.toml | 13 ++++++ dev_scripts/aes_ct_harness/src/lib.rs | 38 +++++++++++++++++ dev_scripts/check_aes_ct_asm.sh | 61 +++++++++++++++++++++++++++ 8 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/aes-ct-asm.yml create mode 100644 dev_scripts/aes_ct_harness/Cargo.toml create mode 100644 dev_scripts/aes_ct_harness/src/lib.rs create mode 100755 dev_scripts/check_aes_ct_asm.sh diff --git a/.github/workflows/aes-ct-asm.yml b/.github/workflows/aes-ct-asm.yml new file mode 100644 index 000000000..88a6df8db --- /dev/null +++ b/.github/workflows/aes-ct-asm.yml @@ -0,0 +1,26 @@ +name: AES Constant-Time Assembly + +on: + pull_request: + +permissions: + contents: read + +concurrency: + group: aes-ct-asm-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + aes-ct-asm: + if: github.repository == 'bcgit/bc-rust' + name: aes-ct-asm + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install nightly with rust-src + run: rustup toolchain install nightly --profile minimal --component rust-src + + - name: Check the round-key widening assembly on Cortex-M0 and RISC-V 32I + run: ./dev_scripts/check_aes_ct_asm.sh diff --git a/.gitignore b/.gitignore index bdc86bd8b..41d845b5e 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ custom_mutants_output/ !.claude/agents/ .claude/settings.local.json .claude 2/ +dev_scripts/aes_ct_harness/Cargo.lock diff --git a/CLAUDE.md b/CLAUDE.md index a5583b749..b19324e89 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,8 @@ Revisit this section at the first non-alpha release. - Builds on Rust **stable**: there is no toolchain pin, and no crate enables a `#![feature(...)]` gate, so nightly-only tooling (`-Z` flags and the like) is not available. CI builds, tests and docs on stable; only the - `rustfmt` job installs nightly. + `rustfmt` job and the AES assembly check (`-Zbuild-std=core` for targets without a shipped `core`) install + nightly. - 2024 edition (set workspace-wide in the root `Cargo.toml`). - Minimum Rust is 1.88: `rust-version` in the root `Cargo.toml`, inherited by `bouncycastle-utils` (the crate that needs it, for `slice::as_chunks`) and so enforced for every crate that depends on it. @@ -326,12 +327,15 @@ external vector suites — is specified in QUALITY_AND_STYLE.md and CONTRIBUTING ## CI -Five workflows under `.github/workflows/`, all triggered by pull requests: +Six workflows under `.github/workflows/`, all triggered by pull requests: - `rust-build.yml`: `cargo build --workspace --all-targets --all-features` on stable. - `rust-test.yml`: `cargo test --all` on stable. - `rust-style.yml`: `cargo fmt --all --check` on **nightly** rustfmt, so format before every commit. - `rust-docs.yml`: `cargo doc --all` on stable. +- `aes-ct-asm.yml`: `dev_scripts/check_aes_ct_asm.sh` on **nightly** with `rust-src`: builds + `dev_scripts/aes_ct_harness` for thumbv6m and riscv32i and greps the assembly of the AES round-key + widening for a multiply, call or branch (see the comment on `PlaneWord::splat` in the aes crate). - `publish_doc_benches_to_ghpages.yaml`: builds rustdoc and runs `quality_stats.sh`; on `main` it also publishes docs and code stats to GitHub Pages (`https://bcgit.github.io/bc-rust/`). Its bench step is commented out because the benches are too slow on the hosted runner, so benchmark figures come from a diff --git a/crypto/aes/src/bitslice.rs b/crypto/aes/src/bitslice.rs index b50455404..258c581eb 100644 --- a/crypto/aes/src/bitslice.rs +++ b/crypto/aes/src/bitslice.rs @@ -206,6 +206,10 @@ impl PlaneWord for u32 { /// the five harness targets emits a multiply or a branch). On x86-64 and aarch64 the /// barrier would cost about 60 % of the 2-block and 190 % of the 4-block throughput /// (measured), for a multiply that is already constant-time, so there the fold is allowed. + /// The gate is by architecture alone, so 32-bit Arm, x86 and wasm32 pay the barrier too, + /// whether or not their multiply is fixed-latency: conservative, and cheap off the hot + /// 64-bit targets. `dev_scripts/check_aes_ct_asm.sh` rebuilds the harness and greps its + /// assembly; CI runs it on the Cortex-M0 and RISC-V 32I targets. #[inline(always)] fn splat(pattern: u16) -> Self { let x = u32::from(pattern); diff --git a/crypto/aes/src/gcm.rs b/crypto/aes/src/gcm.rs index c95ba9b3e..e0f7a6234 100644 --- a/crypto/aes/src/gcm.rs +++ b/crypto/aes/src/gcm.rs @@ -4,9 +4,8 @@ use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; use bouncycastle_cipher::modes::Gcm; -// Imports needed for docs -#[allow(unused_imports)] pub use bouncycastle_cipher::modes::GCM_NONCE_LEN; +// Imported for the docs #[allow(unused_imports)] use bouncycastle_cipher::{Decrypting, Encrypting}; diff --git a/dev_scripts/aes_ct_harness/Cargo.toml b/dev_scripts/aes_ct_harness/Cargo.toml new file mode 100644 index 000000000..bbe398f69 --- /dev/null +++ b/dev_scripts/aes_ct_harness/Cargo.toml @@ -0,0 +1,13 @@ +# The AES round-key widening, out of line, for `../check_aes_ct_asm.sh` to build on the targets +# whose multiply is not fixed-latency and grep. Its own workspace, so the root `cargo build +# --workspace` never builds it for the host. +[package] +name = "aes_ct_harness" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +bouncycastle-utils = { path = "../../crypto/utils" } + +[workspace] diff --git a/dev_scripts/aes_ct_harness/src/lib.rs b/dev_scripts/aes_ct_harness/src/lib.rs new file mode 100644 index 000000000..8f1670d47 --- /dev/null +++ b/dev_scripts/aes_ct_harness/src/lib.rs @@ -0,0 +1,38 @@ +//! The two bodies of `PlaneWord::splat` from `crypto/aes/src/bitslice.rs`, with the barrier the +//! crate applies off x86-64 and aarch64 and without it, each out of line so that it lands in the +//! assembly as one symbol. `../check_aes_ct_asm.sh` builds this for the Cortex-M0 and RISC-V 32I +//! targets and checks that the barriered bodies hold no multiply, call or branch, and that the +//! plain ones do show the multiply LLVM folds the shifts and ORs into, which is what the check +//! exists to catch. Keep the bodies the same as the crate's. +#![no_std] +#![forbid(unsafe_code)] + +use bouncycastle_utils::ct; + +/// `u32::splat` as the crate compiles it off the fixed-latency targets. +#[inline(never)] +pub fn splat32_barrier(pattern: u16) -> u32 { + let x = ct::value_barrier(u32::from(pattern)); + x | (x << 16) +} + +/// `u64::splat` as the crate compiles it off the fixed-latency targets. +#[inline(never)] +pub fn splat64_barrier(pattern: u16) -> u64 { + let x = ct::value_barrier(u64::from(pattern)); + x | (x << 16) | (x << 32) | (x << 48) +} + +/// The same without the barrier: the positive control, which LLVM may fold into a multiply. +#[inline(never)] +pub fn splat32_plain(pattern: u16) -> u32 { + let x = u32::from(pattern); + x | (x << 16) +} + +/// The same without the barrier: the positive control, which LLVM may fold into a multiply. +#[inline(never)] +pub fn splat64_plain(pattern: u16) -> u64 { + let x = u64::from(pattern); + x | (x << 16) | (x << 32) | (x << 48) +} diff --git a/dev_scripts/check_aes_ct_asm.sh b/dev_scripts/check_aes_ct_asm.sh new file mode 100755 index 000000000..fb00a8f29 --- /dev/null +++ b/dev_scripts/check_aes_ct_asm.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Checks that the AES round-key widening (`PlaneWord::splat` in crypto/aes/src/bitslice.rs) +# compiles to no multiply, call or branch on the targets whose multiply is not fixed-latency. +# Builds dev_scripts/aes_ct_harness with the nightly toolchain and `-Zbuild-std=core` (the +# targets have no shipped `core`), emits the assembly and greps the four `splat*` bodies: the +# barriered pair must be clean, and the plain pair must show the multiply the barrier prevents, +# so that a grep that matches nothing is noticed. Targets may be given as arguments. +# +# rustup toolchain install nightly --profile minimal --component rust-src +# ./dev_scripts/check_aes_ct_asm.sh # thumbv6m-none-eabi riscv32i-unknown-none-elf +# ./dev_scripts/check_aes_ct_asm.sh thumbv6m-none-eabi +set -euo pipefail + +cd "$(dirname "$0")/aes_ct_harness" +TARGETS=${*:-"thumbv6m-none-eabi riscv32i-unknown-none-elf"} +status=0 + +# Prints the instruction lines of the function whose mangled symbol contains $2 in $1. +body() { + awk -v f="$2" '$0 ~ f && /:$/ { on = 1 } on { print } on && /^\.Lfunc_end/ { exit }' "$1" | + grep -vE '^\s*(\.|@|#|//)' | grep -vE ':\s*$' || true +} + +for t in $TARGETS; do + case "$t" in + thumb*|arm*) + bad='\b(mul|muls|smull|umull|mla|bl|blx|b(eq|ne|cs|hs|cc|lo|mi|pl|vs|vc|hi|ls|ge|lt|gt|le)|cbz|cbnz|it|ite|itt)\b|__aeabi_' + mul='\b(mul|muls|smull|umull)\b|__aeabi_lmul|__aeabi_.*mul' ;; + riscv*) + bad='\b(mul|mulh|mulhu|mulhsu|call|jal|jalr|tail|b(eq|ne|lt|ge|ltu|geu|nez|eqz|lez|gez|ltz|gtz))\b|__mul' + mul='\bmul|__mul' ;; + avr*) + bad='\b(mul|muls|mulsu|fmul|call|rcall|icall|br(eq|ne|cs|cc|sh|lo|mi|pl|ge|lt|hs|vs|vc|ie|id|ts|tc)|sbrc|sbrs|cpse)\b|__mul' + mul='\bmul|__mul' ;; + *) echo "no mnemonic table for $t"; exit 2 ;; + esac + + rm -rf "target/$t" + cargo +nightly rustc --quiet --release --target "$t" -Zbuild-std=core -- --emit asm + asm=$(ls "target/$t/release/deps/"aes_ct_harness-*.s) + + for f in splat32_barrier splat64_barrier; do + hits=$(body "$asm" "$f" | grep -cE "$bad" || true) + n=$(body "$asm" "$f" | wc -l) + if [ "$hits" -ne 0 ]; then + echo "FAIL $t $f: $hits multiply/call/branch line(s) in $n instructions" + body "$asm" "$f" | grep -E "$bad" + status=1 + else + echo "ok $t $f: no multiply, call or branch in $n instructions" + fi + done + control=$(for f in splat32_plain splat64_plain; do body "$asm" "$f"; done | grep -cE "$mul" || true) + if [ "$control" -eq 0 ]; then + echo "FAIL $t: the unbarriered bodies show no multiply, so the grep proves nothing" + status=1 + else + echo "ok $t control: the unbarriered bodies show the multiply ($control line(s))" + fi +done +exit $status From def27dac43139b32ab07698acbbf0bb3f75982a1 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:51:31 +1100 Subject: [PATCH 52/57] cipher: GCM's resumed-counter rule reads the counter through CtrKeyStream::counter_from_state rather than by its byte offset, and the CCM counter-index test adds the q = 8 case (a 7-byte nonce) that motivated the bound (P190-cipher-1, P190-cipher-2) Assisted-by: Claude:claude-fable-5-1 --- crypto/cipher/src/modes/gcm.rs | 5 ++-- .../cipher/src/modes/hazmat/ctr_key_stream.rs | 9 +++++++ crypto/cipher/tests/ccm_suspend_tests.rs | 25 +++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs index 7b9dfec62..7daf12888 100644 --- a/crypto/cipher/src/modes/gcm.rs +++ b/crypto/cipher/src/modes/gcm.rs @@ -848,9 +848,8 @@ where // The CTR state's own check is only the top of the counter's range. GCTR starts at // inc32(J0), counter 2 (Algorithm 4 step 3); J0 itself, counter 1, is the tag mask // (step 6) and 0 is never used, so a resumed counter below 2 would emit CIPH_K(J0) as - // keystream. The counter follows the nonce in the CTR state. - let mut counter = Cursor::new(&ctr[GCM_NONCE_LEN..]); - if counter.u64() < 2 { + // keystream. Read through the keystream's own layout, after its own range check. + if CtrKeyStream::<'k, P, 16, GCM_NONCE_LEN>::counter_from_state(ctr) < 2 { return Err(SuspendableError::InvalidData); } gcm.ghash.restore_state(ghash)?; diff --git a/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs index 2919bd1e6..6b000c088 100644 --- a/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs +++ b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs @@ -109,6 +109,15 @@ where Cursor::new(state).array::() } + /// The next counter value of a suspended keystream, read out of the same state: the `u64` + /// after the nonce. For a composite whose rule on the counter is tighter than this type's + /// (GCM starts at 2), so that the rule is tied to this layout and not to a copy of it. + pub(crate) fn counter_from_state(state: &[u8]) -> u64 { + let mut r = Cursor::new(state); + r.array::(); + r.u64() + } + /// `Tj = N | [j]m`: the nonce followed by the counter `j`, big-endian, in the trailing /// `CTR_LEN` bytes. /// diff --git a/crypto/cipher/tests/ccm_suspend_tests.rs b/crypto/cipher/tests/ccm_suspend_tests.rs index 4c838b70d..cf733c49f 100644 --- a/crypto/cipher/tests/ccm_suspend_tests.rs +++ b/crypto/cipher/tests/ccm_suspend_tests.rs @@ -142,6 +142,31 @@ fn the_counter_index_is_held_to_its_range() { assert_eq!(resumes(with_u64(state, NEXT_CTR, u64::MAX)), Err(SuspendableError::InvalidData)); } +/// The same bound at `q = 8` (a 7-byte nonce), where `MAX_COUNTER` is `u64::MAX` and a check of +/// the form `next_ctr - 1 <= MAX_COUNTER` holds for every value: the index stops one past the +/// last of the `ceil(u64::MAX / 16)` counter values a payload can use, and `u64::MAX` itself, +/// which the next increment would overflow, is refused. +#[test] +fn the_counter_index_is_held_to_its_range_at_q_8() { + const NONCE_LEN_7: usize = 7; + type Enc7<'k> = + CcmPacketEncryptor<'k, ToyBlockCipher, BLOCK_LEN, NONCE_LEN_7, 16, AAD_LEN, DATA_LEN>; + const N7: usize = Enc7::SUSPENDED_STATE_LEN; + let key: ToyKey> = + ToyKey::from_key_material(&key()).unwrap(); + let mut enc = key.create_encryptor_nonce(&[0x24u8; NONCE_LEN_7]); + enc.do_update_aad(b"aad").unwrap(); + let mut out = [0u8; 16]; + enc.do_encrypt_out(&[0x5Au8; 16], &mut out).unwrap(); + let state: [u8; N7] = enc.suspend(); + let resumes = |state: [u8; N7]| Enc7::from_suspended(state, key.state()).map(|_| ()); + let last = u64::MAX / BLOCK_LEN as u64 + 2; + assert!(resumes(with_u64(state, NEXT_CTR, 1)).is_ok()); + assert!(resumes(with_u64(state, NEXT_CTR, last)).is_ok(), "every counter used"); + assert_eq!(resumes(with_u64(state, NEXT_CTR, last + 1)), Err(SuspendableError::InvalidData)); + assert_eq!(resumes(with_u64(state, NEXT_CTR, u64::MAX)), Err(SuspendableError::InvalidData)); +} + /// A.3 Table 4 fixes the template's flags octet at `[q-1]_3` with every other bit zero, and the /// counter field is zero in the template because the index is written over it per block. Each /// is checked on its own: the flags octet with the right `q` but a stray bit, a wrong `q`, and a From 0f8c9e6c5ffd033ed2b3c46a53e33ac1c051f730 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:51:31 +1100 Subject: [PATCH 53/57] mlkem-common: poly_reduce's doc gives the closed range [0, q] that barrett_reduce documents and tests Assisted-by: Claude:claude-fable-5-1 --- crypto/mlkem-common/src/polynomial.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto/mlkem-common/src/polynomial.rs b/crypto/mlkem-common/src/polynomial.rs index b74e5d950..93b2a4e1b 100644 --- a/crypto/mlkem-common/src/polynomial.rs +++ b/crypto/mlkem-common/src/polynomial.rs @@ -146,7 +146,7 @@ impl Polynomial { } } - /// Barrett-reduces every coefficient into \[0, 𝑞). + /// Barrett-reduces every coefficient into \[0, 𝑞] (see [`barrett_reduce`] for the closed end). pub fn poly_reduce(&mut self) { for i in 0..N { self[i] = barrett_reduce(self[i]); From b2774202fdf53a9f7106844be6ffd1086b32bc6f Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:51:31 +1100 Subject: [PATCH 54/57] mlkem: a unit test that dot_product returns reduced coefficients, which pins the reduction before inv_ntt that M1 restored (P190-mlkem-2) Assisted-by: Claude:claude-fable-5-1 --- crypto/mlkem/src/matrix.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crypto/mlkem/src/matrix.rs b/crypto/mlkem/src/matrix.rs index 5acb9b4dd..9d62754ec 100644 --- a/crypto/mlkem/src/matrix.rs +++ b/crypto/mlkem/src/matrix.rs @@ -466,4 +466,32 @@ mod tests { fn compress_and_decompress_match_fips_203_for_du_11() { check::(); } + + /// `dot_product` returns reduced coefficients, in [0, q]: the k-term sum of Montgomery + /// products is in (-2kq, 2kq) and the callers' `inv_ntt` adds two coefficients in i16, so + /// the reduction at the end of `dot_product` is what keeps the sum in range (the review's + /// M1, which arose from that reduction being commented out). An LCG spreads the inputs over + /// (-q, q), the range NTT-domain coefficients take. + #[test] + fn dot_product_is_reduced() { + let mut x: u32 = 0x2545_F491; + let mut next = || { + x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + ((x % (2 * q as u32 - 1)) as i32 - (q as i32 - 1)) as i16 + }; + for _ in 0..64 { + let mut a = Vector::<4>::new(); + let mut b = Vector::<4>::new(); + for i in 0..4 { + for j in 0..N { + a[i][j] = next(); + b[i][j] = next(); + } + } + let w = a.dot_product(&b); + for j in 0..N { + assert!((0..=q).contains(&w[j]), "coefficient {j} is {}", w[j]); + } + } + } } From 4013ddefd9ca21ce8462ab9e70416edd65e58e1f Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:51:31 +1100 Subject: [PATCH 55/57] mldsa-common: the rounding tests say they pin the value, not the branch-freeness, which only the assembly shows (P190-mldsa-2) Assisted-by: Claude:claude-fable-5-1 --- crypto/mldsa-common/tests/rounding_tests.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crypto/mldsa-common/tests/rounding_tests.rs b/crypto/mldsa-common/tests/rounding_tests.rs index fd1f2037d..e3f5847dc 100644 --- a/crypto/mldsa-common/tests/rounding_tests.rs +++ b/crypto/mldsa-common/tests/rounding_tests.rs @@ -1,5 +1,7 @@ //! The branch-free forms of `make_hint` and `Polynomial::check_norm` against the comparisons they -//! replace, over every input the signer can feed them. +//! replace, over every input the signer can feed them. These pin the value, which the comparison +//! forms also gave, so they pass on the old code too: that the new forms compile without a +//! branch on the secret is shown by reading the release assembly, which no in-tree test can do. use bouncycastle_mldsa_common::aux_functions::make_hint; use bouncycastle_mldsa_common::params::{MLDSA44Params, MLDSA65Params, MLDSAParamsBase}; From 1108f34cc4c9182ca60251c047f9f8383925d741 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 12:51:31 +1100 Subject: [PATCH 56/57] cli: comments on the process-wide --hex-input flag and on why the base64 command repeats the data-after-padding check for later chunks (P190-cli-3, P190-base64-2) Assisted-by: Claude:claude-fable-5-1 --- cli/src/encoders_cmd.rs | 5 ++++- cli/src/helpers/mod.rs | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cli/src/encoders_cmd.rs b/cli/src/encoders_cmd.rs index 6d939e973..f6ab129cf 100644 --- a/cli/src/encoders_cmd.rs +++ b/cli/src/encoders_cmd.rs @@ -135,7 +135,10 @@ pub(crate) fn base64_decode_cmd() { crate::helpers::write_stdout( &decoder.do_final(&buf[..n]).unwrap_or_else(|e| fail(e)), ); - // Padding ends the message. Anything but whitespace after it is not base64. + // Padding ends the message. Anything but whitespace after it is not base64. The + // library refuses data after the padding within the chunk it is given; what later + // chunks hold it never sees, since `do_final` consumed the decoder, so the check + // is repeated here for them. loop { let n = read_chunk(&mut buf); if n == 0 { diff --git a/cli/src/helpers/mod.rs b/cli/src/helpers/mod.rs index 78b4903e5..e82ac8431 100644 --- a/cli/src/helpers/mod.rs +++ b/cli/src/helpers/mod.rs @@ -43,6 +43,8 @@ pub(crate) const MAX_SECRET_LEN: usize = 16 * 1024; /// [`KeyType::Unknown`] (or `Zeroized` when all zero); the command re-tags what it makes of it. pub(crate) type SecretBytes = KeyMaterial; +/// The parsed `--hex-input` flag, process-wide: a CLI has one invocation, and `main` stores it +/// before any reader below runs, so no reader can see the default by mistake. static HEX_INPUT: AtomicBool = AtomicBool::new(false); /// Records the global `--hex-input` flag for the readers below. From c3c5e9d313ea690f1a767db28c4d85bcee370a5f Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 4 Nov 2026 13:38:53 +1100 Subject: [PATCH 57/57] aes: the assembly check pins cargo's build directory to target and finds the emitted .s by search, since the nightly on CI keeps intermediate artifacts in a separate build directory Assisted-by: Claude:claude-fable-5-1 --- dev_scripts/check_aes_ct_asm.sh | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/dev_scripts/check_aes_ct_asm.sh b/dev_scripts/check_aes_ct_asm.sh index fb00a8f29..4804d6c86 100755 --- a/dev_scripts/check_aes_ct_asm.sh +++ b/dev_scripts/check_aes_ct_asm.sh @@ -35,9 +35,18 @@ for t in $TARGETS; do *) echo "no mnemonic table for $t"; exit 2 ;; esac - rm -rf "target/$t" - cargo +nightly rustc --quiet --release --target "$t" -Zbuild-std=core -- --emit asm - asm=$(ls "target/$t/release/deps/"aes_ct_harness-*.s) + rm -rf target + # `--emit asm` writes beside the intermediate artifacts, which newer cargo keeps in a build + # directory separate from the target directory; pinning both to `target` keeps the file + # findable on every cargo, and `find` does not depend on the exact layout within it. + cargo +nightly rustc --release --target "$t" -Zbuild-std=core \ + --config 'build.build-dir="target"' -- --emit asm + asm=$(find target -name 'aes_ct_harness-*.s' -path "*$t*" | head -n 1) + if [ -z "$asm" ]; then + echo "FAIL $t: no aes_ct_harness-*.s under $(pwd)/target" + find target -name '*.s' | head + exit 2 + fi for f in splat32_barrier splat64_barrier; do hits=$(body "$asm" "$f" | grep -cE "$bad" || true)