From f73142b136914eaf4d0ce16850340e8975bd821e Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 26 Aug 2026 13:21:59 +1000 Subject: [PATCH 1/5] sha2: return errors for partial-bit input, compile-time IVs, docs and cleanups - do_final_partial_bits[_out] previously hit unimplemented!() (a panic behind a Result-returning API). Bit-oriented messages remain unsupported, but the methods now return errors: num_partial_bits in 1..=7 -> HashError::InvalidInput ("not supported"), > 7 -> HashError::InvalidLength, and 0 behaves exactly as do_final_out. do_final_out and this path share a single spec-commented finalize(). - Initial hash values moved to `const H0` on new Sha256Family / Sha512Family param traits, removing the runtime match-on-OUTPUT_LEN and its panic! arm. - Blanket `impl HashAlgParams for SHA256Internal

` / `SHA512Internal

` forwarding to the params struct, replacing four hand-duplicated impls. - std::marker::PhantomData -> core::marker::PhantomData (no_std goal). - Crate docs: fixed SHA3/SHAKE copy-paste, documented the partial-bits limitation, added Memory Usage and Security Considerations sections, documented *_NAME consts. - Resolved byte_count TODOs with a stated 2^64-byte message limit. Tests: partial_bits_are_rejected covers the 0 / 1..=7 / >7 cases for all four variants without panicking. Co-Authored-By: Claude Fable 5 --- crypto/sha2/src/lib.rs | 114 ++++++++++++++++++----- crypto/sha2/src/sha256.rs | 147 ++++++++++++++--------------- crypto/sha2/src/sha512.rs | 158 ++++++++++++++++---------------- crypto/sha2/tests/sha2_tests.rs | 37 +++++++- 4 files changed, 280 insertions(+), 176 deletions(-) diff --git a/crypto/sha2/src/lib.rs b/crypto/sha2/src/lib.rs index 6906e0c6..79f489c7 100644 --- a/crypto/sha2/src/lib.rs +++ b/crypto/sha2/src/lib.rs @@ -14,7 +14,7 @@ //! let output: Vec = sha2::SHA256::new().hash(data); //! ``` //! -//! More advanced usage will require creating a SHA3 or SHAKE object to hold state between successive calls, +//! More advanced usage will require creating a SHA2 object to hold state between successive calls, //! for example if input is received in chunks and not all available at the same time: //! //! ``` @@ -34,6 +34,41 @@ //! let output: Vec = sha2.do_final(); //! ``` //! +//! Bit-oriented messages (a final byte with fewer than 8 bits, FIPS 180-4 s. 5.1) are not supported: +//! [`Hash::do_final_partial_bits`] returns [`HashError::InvalidInput`] for `num_partial_bits` in `1..=7` +//! (and behaves as [`Hash::do_final`] for `0`). +//! +//! # Memory Usage +//! +//! No heap memory is used by the algorithms themselves; the `Vec`-returning convenience methods +//! allocate only the output buffer, and the `*_out` variants allocate nothing. +//! +//! | Object | Size (bytes) | +//! |-------------------------------------|--------------| +//! | `SHA224`, `SHA256` | 112 | +//! | `SHA384`, `SHA512` | 208 | +//! | Suspended `SHA224`/`SHA256` state | 108 | +//! | Suspended `SHA384`/`SHA512` state | 204 | +//! +//! The object holds the 8-word chaining value plus one block of buffered input. The compression +//! function additionally uses a 64-word (SHA-256 family, 256 bytes) or 80-word (SHA-512 family, +//! 640 bytes) message schedule on the stack for the duration of a call. +//! +//! # Security Considerations +//! +//! * SHA-224/256/384/512 offer 112/128/192/256 bits of collision resistance respectively. +//! * SHA-2 is a Merkle–Damgård construction and is therefore subject to length-extension: +//! `H(k || m)` is not a secure MAC. Use HMAC (`bouncycastle-hmac`) for keyed hashing. +//! * SHA-384 and SHA-224 are truncations of SHA-512 and SHA-256 with distinct initial values, and +//! are not vulnerable to length extension in the same direct way, but should still not be used as +//! `H(k || m)` MACs. +//! * 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 (FIPS 180-4 permits 2^64 bits for SHA-224/256 and +//! 2^128 bits for SHA-384/512; the SHA-512 family limit here is 2^67 bits). +//! //! # Suspending and resuming execution //! //! When hashing a large message, it can be advantageous to be able to suspend the operation @@ -78,16 +113,18 @@ use bouncycastle_core::traits::{Algorithm, AlgorithmOID, HashAlgParams, Security /*** Imports needed for docs ***/ #[allow(unused_imports)] -use bouncycastle_core::traits::Suspendable; +use bouncycastle_core::errors::HashError; +#[allow(unused_imports)] +use bouncycastle_core::traits::{Hash, Suspendable}; /*** String constants ***/ -/// +/// Algorithm name string for SHA224, as used by the factories and CLI. pub const SHA224_NAME: &str = "SHA224"; -/// +/// Algorithm name string for SHA256, as used by the factories and CLI. pub const SHA256_NAME: &str = "SHA256"; -/// +/// Algorithm name string for SHA384, as used by the factories and CLI. pub const SHA384_NAME: &str = "SHA384"; -/// +/// Algorithm name string for SHA512, as used by the factories and CLI. pub const SHA512_NAME: &str = "SHA512"; /*** pub types ***/ @@ -104,11 +141,30 @@ pub type SHA512 = SHA512Internal; /// Private trait on purpose so that only the NIST-approved params can be used. trait SHA2Params: HashAlgParams {} -/*** SHA224 ***/ -impl HashAlgParams for SHA224 { - const OUTPUT_LEN: usize = 28; - const BLOCK_LEN: usize = 64; +/// Parameters for the SHA-256 family (SHA-224, SHA-256): 32-bit words, 512-bit blocks. +/// `H0` is the initial hash value from FIPS 180-4 s. 5.3.2 / 5.3.3. +trait Sha256Family: SHA2Params { + const H0: [u32; 8]; +} + +/// Parameters for the SHA-512 family (SHA-384, SHA-512): 64-bit words, 1024-bit blocks. +/// `H0` is the initial hash value from FIPS 180-4 s. 5.3.4 / 5.3.5. +trait Sha512Family: SHA2Params { + const H0: [u64; 8]; +} + +/// The public hash types expose the same parameters as their `*Params` marker, so the constants +/// are defined exactly once (on the params struct) and forwarded here. +impl HashAlgParams for SHA256Internal { + const OUTPUT_LEN: usize = PARAMS::OUTPUT_LEN; + const BLOCK_LEN: usize = PARAMS::BLOCK_LEN; +} +impl HashAlgParams for SHA512Internal { + const OUTPUT_LEN: usize = PARAMS::OUTPUT_LEN; + const BLOCK_LEN: usize = PARAMS::BLOCK_LEN; } + +/*** SHA224 ***/ /// The parameters for SHA224. #[derive(Clone)] pub struct SHA224Params; @@ -127,12 +183,14 @@ impl AlgorithmOID for SHA224 { &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04]; } impl SHA2Params for SHA224Params {} +/// FIPS 180-4 s. 5.3 initial hash value for SHA224. +impl Sha256Family for SHA224Params { + const H0: [u32; 8] = [ + 0xC1059ED8, 0x367CD507, 0x3070DD17, 0xF70E5939, 0xFFC00B31, 0x68581511, 0x64F98FA7, 0xBEFA4FA4, + ]; +} /*** SHA256 ***/ -impl HashAlgParams for SHA256 { - const OUTPUT_LEN: usize = 32; - const BLOCK_LEN: usize = 64; -} /// The parameters for SHA256. #[derive(Clone)] pub struct SHA256Params; @@ -151,12 +209,14 @@ impl HashAlgParams for SHA256Params { const BLOCK_LEN: usize = 64; } impl SHA2Params for SHA256Params {} +/// FIPS 180-4 s. 5.3 initial hash value for SHA256. +impl Sha256Family for SHA256Params { + const H0: [u32; 8] = [ + 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19, + ]; +} /*** SHA384 ***/ -impl HashAlgParams for SHA384 { - const OUTPUT_LEN: usize = 48; - const BLOCK_LEN: usize = 128; -} /// The parameters for SHA384. #[derive(Clone)] pub struct SHA384Params; @@ -175,15 +235,18 @@ impl HashAlgParams for SHA384Params { const BLOCK_LEN: usize = 128; } impl SHA2Params for SHA384Params {} +/// FIPS 180-4 s. 5.3 initial hash value for SHA384. +impl Sha512Family for SHA384Params { + const H0: [u64; 8] = [ + 0xCBBB9D5DC1059ED8, 0x629A292A367CD507, 0x9159015A3070DD17, 0x152FECD8F70E5939, + 0x67332667FFC00B31, 0x8EB44A8768581511, 0xDB0C2E0D64F98FA7, 0x47B5481DBEFA4FA4, + ]; +} /*** SHA512 ***/ /// The parameters for SHA512. #[derive(Clone)] pub struct SHA512Params; -impl HashAlgParams for SHA512 { - const OUTPUT_LEN: usize = 64; - const BLOCK_LEN: usize = 128; -} impl Algorithm for SHA512Params { const ALG_NAME: &'static str = SHA512_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; @@ -199,6 +262,13 @@ impl AlgorithmOID for SHA512 { &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03]; } impl SHA2Params for SHA512Params {} +/// FIPS 180-4 s. 5.3 initial hash value for SHA512. +impl Sha512Family for SHA512Params { + const H0: [u64; 8] = [ + 0x6A09E667F3BCC908, 0xBB67AE8584CAA73B, 0x3C6EF372FE94F82B, 0xA54FF53A5F1D36F1, + 0x510E527FADE682D1, 0x9B05688C2B3E6C1F, 0x1F83D9ABFB41BD6B, 0x5BE0CD19137E2179, + ]; +} pub use sha256::SUSPENDED_SHA256_STATE_LEN; pub use sha512::SUSPENDED_SHA512_STATE_LEN; diff --git a/crypto/sha2/src/sha256.rs b/crypto/sha2/src/sha256.rs index 34d09775..041193c9 100644 --- a/crypto/sha2/src/sha256.rs +++ b/crypto/sha2/src/sha256.rs @@ -1,4 +1,4 @@ -use crate::SHA2Params; +use crate::Sha256Family; use bouncycastle_core::errors::{HashError, SuspendableError}; use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, Suspendable}; @@ -47,31 +47,17 @@ fn theta1(x: u32) -> u32 { } #[derive(Clone)] -pub(crate) struct Sha256State { +pub(crate) struct Sha256State { _params: core::marker::PhantomData, h: Secret<[u32; 8]>, } -impl Sha256State { +impl Sha256State { pub(crate) fn new() -> Self { + // FIPS 180-4 s. 5.3: initial hash value H(0), supplied per-variant by the params type. let mut h = Secret::<[u32; 8]>::new(); - match PARAMS::OUTPUT_LEN * 8 { - 224 => { - h.copy_from_slice(&[ - 0xC1059ED8, 0x367CD507, 0x3070DD17, 0xF70E5939, 0xFFC00B31, 0x68581511, - 0x64F98FA7, 0xBEFA4FA4, - ]); - Self { _params: core::marker::PhantomData, h } - } - 256 => { - h.copy_from_slice(&[ - 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, - 0x1F83D9AB, 0x5BE0CD19, - ]); - Self { _params: std::marker::PhantomData, h } - } - _ => panic!("Invalid SHA-2 bit size: {}", PARAMS::OUTPUT_LEN), - } + h.copy_from_slice(&PARAMS::H0); + Self { _params: core::marker::PhantomData, h } } fn compress(&mut self, blocks: &[[u8; 64]]) { @@ -144,17 +130,15 @@ impl Sha256State { /// This uses a private bound so that you cannot instantiate it directly and have to use the /// provided and NIST-approved parameters. #[derive(Clone)] -pub struct SHA256Internal { +pub struct SHA256Internal { _params: core::marker::PhantomData, state: Sha256State, byte_count: u64, x_buf: Secret<[u8; 64]>, x_buf_off: usize, - // TODO: Investigate whether maximum message size (according to FIPS 180-4) should be added - // (2^64 for SHA256 and 2^128 for SHA512) } -impl SHA256Internal { +impl SHA256Internal { /// Creates a new SHA256 instance, ready for use. pub fn new() -> Self { Self { @@ -167,18 +151,61 @@ impl SHA256Internal { } } -impl Default for SHA256Internal { +impl SHA256Internal { + /// Pads and compresses the final block(s) as per FIPS 180-4 s. 5.1.1, then writes the digest. + /// + /// Returns the number of bytes written (`min(output.len(), OUTPUT_LEN)`); a shorter output buffer + /// truncates the digest, a longer one is zero-filled past the digest. + fn finalize(mut self, output: &mut [u8]) -> usize { + output.fill(0); + + let n = *min(&output.len(), &PARAMS::OUTPUT_LEN); + + // FIPS 180-4 s. 5.1.1: append the "1" bit (as the byte 0x80, since messages are whole bytes). + self.x_buf[self.x_buf_off] = 0x80; + self.x_buf_off += 1; + + // If the length field no longer fits in this block, zero-fill and compress, then start a fresh block. + if self.x_buf_off > 56 { + self.x_buf[self.x_buf_off..].fill(0x00); + self.state.compress(slice::from_ref(&self.x_buf)); + self.x_buf_off = 0; + } + + self.x_buf[self.x_buf_off..56].fill(0x00); + // FIPS 180-4 s. 5.1.1: append the 64-bit big-endian message length in bits. byte_count is a + // byte counter, so the length in bits is byte_count << 3. + let bit_len: u64 = self.byte_count << 3; + self.x_buf[56..64].copy_from_slice(&bit_len.to_be_bytes()); + self.state.compress(slice::from_ref(&self.x_buf)); + + // FIPS 180-4 s. 6.x.2: the digest is H0 || H1 || ... (big-endian words), truncated to OUTPUT_LEN + // (and further to the caller's buffer if that is shorter). + let h = &self.state.h; + for i in 0..(n / 4) { + output[i * 4..i * 4 + 4].copy_from_slice(&h[i].to_be_bytes()); + } + if !n.is_multiple_of(4) { + output[((n / 4) * 4)..((n / 4) * 4) + (n % 4)] + .copy_from_slice(&h[n / 4].to_be_bytes()[0..(n % 4)]); + } + + n + } +} + +impl Default for SHA256Internal { fn default() -> Self { Self::new() } } -impl Algorithm for SHA256Internal { +impl Algorithm for SHA256Internal { const ALG_NAME: &'static str = PARAMS::ALG_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; } -impl Hash for SHA256Internal { +impl Hash for SHA256Internal { /// As per FIPS 180-4 Figure 1 fn block_bitlen(&self) -> usize { 512 @@ -204,8 +231,8 @@ impl Hash for SHA256Internal { fn do_update(&mut self, block: &[u8]) { let len = block.len(); - // TODO: Check there is enough space left in 'byte_count' to allow this operation, - // TODO: although overflowing a u64 is unlikely to happen in practice, and rust will throw an error anyway. + // byte_count is a u64 byte counter, so this supports messages up to 2^64 bytes (2^67 bits). + // Exceeding it is infeasible in practice; in debug builds the add panics, in release it wraps. self.byte_count += len as u64; let available = 64 - self.x_buf_off; @@ -240,63 +267,37 @@ impl Hash for SHA256Internal { output } - fn do_final_out(mut self, output: &mut [u8]) -> usize { - output.fill(0); - - let n = *min(&output.len(), &PARAMS::OUTPUT_LEN); - - let bit_len: u64 = self.byte_count << 3; - - self.x_buf[self.x_buf_off] = 0x80; - self.x_buf_off += 1; - - if self.x_buf_off > 56 { - self.x_buf[self.x_buf_off..].fill(0x00); - self.state.compress(slice::from_ref(&self.x_buf)); - self.x_buf_off = 0; - } - - self.x_buf[self.x_buf_off..56].fill(0x00); - self.x_buf[56..64].copy_from_slice(&bit_len.to_be_bytes()); - self.state.compress(slice::from_ref(&self.x_buf)); - - let h = &self.state.h; - - // let n = output.len(); - for i in 0..(n / 4) { - output[i * 4..i * 4 + 4].copy_from_slice(&h[i].to_be_bytes()); - } - if !n.is_multiple_of(4) { - output[((n / 4) * 4)..((n / 4) * 4) + (n % 4)] - .copy_from_slice(&h[n / 4].to_be_bytes()[0..(n % 4)]); - } - - n + fn do_final_out(self, output: &mut [u8]) -> usize { + self.finalize(output) } - /// TODO: This is defined in FIPS 180-4 s. 5.1.2 - /// TODO: - /// TODO: It can be implemented if required - #[allow(unused)] fn do_final_partial_bits( self, partial_byte: u8, num_partial_bits: usize, ) -> Result, HashError> { - unimplemented!() + let mut output = vec![0u8; PARAMS::OUTPUT_LEN]; + self.do_final_partial_bits_out(partial_byte, num_partial_bits, &mut output)?; + Ok(output) } - /// TODO: This is defined in FIPS 180-4 s. 5.1.2 - /// TODO: - /// TODO: It can be implemented if required - #[allow(unused)] + /// Bit-oriented (partial final byte) messages are defined by FIPS 180-4 s. 5.1 but are not + /// supported by this implementation, which processes whole bytes only. `num_partial_bits == 0` + /// is the whole-byte case and behaves exactly like [`Hash::do_final_out`]; any other value returns + /// an error rather than panicking. fn do_final_partial_bits_out( self, - partial_byte: u8, + _partial_byte: u8, num_partial_bits: usize, output: &mut [u8], ) -> Result { - unimplemented!() + match num_partial_bits { + 0 => Ok(self.finalize(output)), + 1..=7 => Err(HashError::InvalidInput( + "bit-oriented (partial final byte) messages are not supported by this SHA-2 implementation", + )), + _ => Err(HashError::InvalidLength("num_partial_bits must be in the range [0,7]")), + } } fn max_security_strength(&self) -> SecurityStrength { @@ -307,7 +308,7 @@ impl Hash for SHA256Internal { /// Length in bytes of the serialized state of SHA224 and SHA256. pub const SUSPENDED_SHA256_STATE_LEN: usize = 108; -impl Suspendable for SHA256Internal { +impl Suspendable for SHA256Internal { fn suspend(self) -> [u8; SUSPENDED_SHA256_STATE_LEN] { debug_assert_eq!(SUSPENDED_SHA256_STATE_LEN, 108); diff --git a/crypto/sha2/src/sha512.rs b/crypto/sha2/src/sha512.rs index c31e3065..ecd3f076 100644 --- a/crypto/sha2/src/sha512.rs +++ b/crypto/sha2/src/sha512.rs @@ -1,4 +1,4 @@ -use crate::SHA2Params; +use crate::Sha512Family; use bouncycastle_core::errors::{HashError, SuspendableError}; use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, Suspendable}; @@ -58,34 +58,18 @@ fn theta1(x: u64) -> u64 { x.rotate_right(19) ^ x.rotate_right(61) ^ (x >> 6) } -// todo -- cleanup -// #[derive(Clone, Copy)] #[derive(Clone)] -pub(crate) struct Sha512State { - _params: std::marker::PhantomData, +pub(crate) struct Sha512State { + _params: core::marker::PhantomData, h: Secret<[u64; 8]>, } -impl Sha512State { +impl Sha512State { pub(crate) fn new() -> Self { + // FIPS 180-4 s. 5.3: initial hash value H(0), supplied per-variant by the params type. let mut h = Secret::<[u64; 8]>::new(); - match PARAMS::OUTPUT_LEN * 8 { - 384 => { - h.copy_from_slice(&[ - 0xCBBB9D5DC1059ED8, 0x629A292A367CD507, 0x9159015A3070DD17, 0x152FECD8F70E5939, - 0x67332667FFC00B31, 0x8EB44A8768581511, 0xDB0C2E0D64F98FA7, 0x47B5481DBEFA4FA4, - ]); - Self { _params: std::marker::PhantomData, h } - } - 512 => { - h.copy_from_slice(&[ - 0x6A09E667F3BCC908, 0xBB67AE8584CAA73B, 0x3C6EF372FE94F82B, 0xA54FF53A5F1D36F1, - 0x510E527FADE682D1, 0x9B05688C2B3E6C1F, 0x1F83D9ABFB41BD6B, 0x5BE0CD19137E2179, - ]); - Self { _params: std::marker::PhantomData, h } - } - _ => panic!("Invalid SHA-2 bit size"), - } + h.copy_from_slice(&PARAMS::H0); + Self { _params: core::marker::PhantomData, h } } fn compress(&mut self, blocks: &[[u8; 128]]) { @@ -157,20 +141,20 @@ impl Sha512State { /// This uses a private bound so that you cannot instantiate it directly and have to use the /// provided and NIST-approved parameters. #[derive(Clone)] -pub struct SHA512Internal { - _params: std::marker::PhantomData, +pub struct SHA512Internal { + _params: core::marker::PhantomData, state: Sha512State, - // NOTE The code currently only supports 2^67 bits, not the full 2^128 + // NOTE: FIPS 180-4 allows messages up to 2^128 bits; this counter supports 2^67 bits (2^64 bytes). byte_count: u64, x_buf: Secret<[u8; 128]>, x_buf_off: usize, } -impl SHA512Internal { +impl SHA512Internal { /// Creates a new SHA512 instance, ready for use. pub fn new() -> Self { Self { - _params: std::marker::PhantomData, + _params: core::marker::PhantomData, state: Sha512State::::new(), byte_count: 0, x_buf: Secret::new(), @@ -179,18 +163,63 @@ impl SHA512Internal { } } -impl Default for SHA512Internal { +impl SHA512Internal { + /// Pads and compresses the final block(s) as per FIPS 180-4 s. 5.1.2, then writes the digest. + /// + /// Returns the number of bytes written (`min(output.len(), OUTPUT_LEN)`); a shorter output buffer + /// truncates the digest, a longer one is zero-filled past the digest. + fn finalize(mut self, output: &mut [u8]) -> usize { + output.fill(0); + + let n = *min(&output.len(), &PARAMS::OUTPUT_LEN); + + // FIPS 180-4 s. 5.1.2: append the "1" bit (as the byte 0x80, since messages are whole bytes). + self.x_buf[self.x_buf_off] = 0x80; + self.x_buf_off += 1; + + // If the length field no longer fits in this block, zero-fill and compress, then start a fresh block. + if self.x_buf_off > 112 { + self.x_buf[self.x_buf_off..].fill(0x00); + self.state.compress(slice::from_ref(&self.x_buf)); + self.x_buf_off = 0; + } + + self.x_buf[self.x_buf_off..112].fill(0x00); + // FIPS 180-4 s. 5.1.2: append the 128-bit big-endian message length in bits. byte_count is a + // byte counter so the high 64 bits are byte_count >> 61 and the low 64 bits are byte_count << 3. + let bit_len_hi: u64 = self.byte_count >> 61; + let bit_len_lo: u64 = self.byte_count << 3; + self.x_buf[112..120].copy_from_slice(&bit_len_hi.to_be_bytes()); + self.x_buf[120..128].copy_from_slice(&bit_len_lo.to_be_bytes()); + self.state.compress(slice::from_ref(&self.x_buf)); + + // FIPS 180-4 s. 6.x.2: the digest is H0 || H1 || ... (big-endian words), truncated to OUTPUT_LEN + // (and further to the caller's buffer if that is shorter). + let h = &self.state.h; + for i in 0..(n / 8) { + output[i * 8..i * 8 + 8].copy_from_slice(&h[i].to_be_bytes()); + } + if !n.is_multiple_of(8) { + output[((n / 8) * 8)..((n / 8) * 8) + (n % 8)] + .copy_from_slice(&h[n / 8].to_be_bytes()[0..(n % 8)]); + } + + n + } +} + +impl Default for SHA512Internal { fn default() -> Self { Self::new() } } -impl Algorithm for SHA512Internal { +impl Algorithm for SHA512Internal { const ALG_NAME: &'static str = PARAMS::ALG_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; } -impl Hash for SHA512Internal { +impl Hash for SHA512Internal { /// As per FIPS 180-4 Figure 1 fn block_bitlen(&self) -> usize { 1024 @@ -216,8 +245,8 @@ impl Hash for SHA512Internal { fn do_update(&mut self, block: &[u8]) { let len = block.len(); - // TODO: Check there is enough space left in 'byte_count' to allow this operation, - // TODO: although overflowing a u64 is unlikely to happen in practice, and rust will throw an error anyway. + // byte_count is a u64 byte counter, so this supports messages up to 2^64 bytes (2^67 bits). + // Exceeding it is infeasible in practice; in debug builds the add panics, in release it wraps. self.byte_count += len as u64; let available = 128 - self.x_buf_off; @@ -251,64 +280,37 @@ impl Hash for SHA512Internal { output } - fn do_final_out(mut self, output: &mut [u8]) -> usize { - output.fill(0); - - let n = *min(&output.len(), &PARAMS::OUTPUT_LEN); - - let bit_len_hi: u64 = self.byte_count >> 61; - let bit_len_lo: u64 = self.byte_count << 3; - - self.x_buf[self.x_buf_off] = 0x80; - self.x_buf_off += 1; - - if self.x_buf_off > 112 { - self.x_buf[self.x_buf_off..].fill(0x00); - self.state.compress(slice::from_ref(&self.x_buf)); - self.x_buf_off = 0; - } - - self.x_buf[self.x_buf_off..112].fill(0x00); - self.x_buf[112..120].copy_from_slice(&bit_len_hi.to_be_bytes()); - self.x_buf[120..128].copy_from_slice(&bit_len_lo.to_be_bytes()); - self.state.compress(slice::from_ref(&self.x_buf)); - - let h = &self.state.h; - - for i in 0..(n / 8) { - output[i * 8..i * 8 + 8].copy_from_slice(&h[i].to_be_bytes()); - } - if !n.is_multiple_of(8) { - output[((n / 8) * 8)..((n / 8) * 8) + (n % 8)] - .copy_from_slice(&h[n / 8].to_be_bytes()[0..(n % 8)]); - } - - n + fn do_final_out(self, output: &mut [u8]) -> usize { + self.finalize(output) } - /// TODO: This is defined in FIPS 180-4 s. 5.1.2 - /// TODO: - /// TODO: It can be implemented if required - #[allow(unused)] fn do_final_partial_bits( self, partial_byte: u8, num_partial_bits: usize, ) -> Result, HashError> { - unimplemented!() + let mut output = vec![0u8; PARAMS::OUTPUT_LEN]; + self.do_final_partial_bits_out(partial_byte, num_partial_bits, &mut output)?; + Ok(output) } - /// TODO: This is defined in FIPS 180-4 s. 5.1.2 - /// TODO: - /// TODO: It can be implemented if required - #[allow(unused)] + /// Bit-oriented (partial final byte) messages are defined by FIPS 180-4 s. 5.1 but are not + /// supported by this implementation, which processes whole bytes only. `num_partial_bits == 0` + /// is the whole-byte case and behaves exactly like [`Hash::do_final_out`]; any other value returns + /// an error rather than panicking. fn do_final_partial_bits_out( self, - partial_byte: u8, + _partial_byte: u8, num_partial_bits: usize, output: &mut [u8], ) -> Result { - unimplemented!() + match num_partial_bits { + 0 => Ok(self.finalize(output)), + 1..=7 => Err(HashError::InvalidInput( + "bit-oriented (partial final byte) messages are not supported by this SHA-2 implementation", + )), + _ => Err(HashError::InvalidLength("num_partial_bits must be in the range [0,7]")), + } } fn max_security_strength(&self) -> SecurityStrength { @@ -319,7 +321,7 @@ impl Hash for SHA512Internal { /// Length in bytes of the serialized state of SHA384 and SHA512. pub const SUSPENDED_SHA512_STATE_LEN: usize = 204; -impl Suspendable for SHA512Internal { +impl Suspendable for SHA512Internal { fn suspend(self) -> [u8; SUSPENDED_SHA512_STATE_LEN] { debug_assert_eq!(SUSPENDED_SHA512_STATE_LEN, 204); diff --git a/crypto/sha2/tests/sha2_tests.rs b/crypto/sha2/tests/sha2_tests.rs index 23667eb2..6943b9b1 100644 --- a/crypto/sha2/tests/sha2_tests.rs +++ b/crypto/sha2/tests/sha2_tests.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod sha2_tests { - use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core::errors::{HashError, SuspendableError}; use bouncycastle_core::traits::{Algorithm, Hash, HashAlgParams, SecurityStrength}; use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_sha2::*; @@ -56,6 +56,37 @@ mod sha2_tests { } } + /// Bit-oriented messages are not supported by the SHA-2 implementation: 0 partial bits must equal + /// do_final(); 1..=7 returns InvalidInput; more than 7 returns InvalidLength. None of them panic. + #[test] + fn partial_bits_are_rejected() { + fn check() { + let mut a = H::default(); + a.do_update(b"abc"); + assert_eq!(a.do_final_partial_bits(0xFF, 0).unwrap(), H::default().hash(b"abc")); + + for n in 1..=7usize { + let mut h = H::default(); + h.do_update(b"abc"); + assert!(matches!(h.do_final_partial_bits(0xFF, n), Err(HashError::InvalidInput(_))), "n={n}"); + let mut out = [0u8; 64]; + assert!(matches!( + H::default().do_final_partial_bits_out(0xFF, n, &mut out), + Err(HashError::InvalidInput(_)) + )); + } + for bad in [8usize, 9, 16, 64, usize::MAX] { + let mut h = H::default(); + h.do_update(b"abc"); + assert!(matches!(h.do_final_partial_bits(0xFF, bad), Err(HashError::InvalidLength(_))), "n={bad}"); + } + } + check::(); + check::(); + check::(); + check::(); + } + #[test] fn test_constants() { assert_eq!(SHA224::OUTPUT_LEN, 28); @@ -118,7 +149,7 @@ mod sha2_tests { assert_eq!(output, output2); // also, give it a busted x_buf_off, just to satisfy mutants that that's been tested - let mut busted_state = serialized_state.clone(); + let mut busted_state = serialized_state; busted_state[3 + 104] = 65; match SHA256::from_suspended(busted_state) { Err(SuspendableError::InvalidData) => { /* good */ } @@ -146,7 +177,7 @@ mod sha2_tests { assert_eq!(output, output2); // also, give it a busted x_buf_off, just to satisfy mutants that that's been tested - let mut busted_state = serialized_state.clone(); + let mut busted_state = serialized_state; busted_state[3 + 200] = 129; match SHA512::from_suspended(busted_state) { Err(SuspendableError::InvalidData) => { /* good */ } From 8c89b779e838ab4210cbee67ec7d799967b25ad4 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 26 Aug 2026 15:25:07 +1000 Subject: [PATCH 2/5] sha2: implement bit-oriented (partial final byte) messages per FIPS 180-4 s. 5.1 Replaces the InvalidInput error path with a real implementation, bringing SHA-2 to parity with SHA3 for non-byte-aligned input. - finalize(partial_byte, num_partial_bits, output): the num_partial_bits least significant bits of partial_byte are placed MSB-first in the final message byte (FIPS 180-4 s. 3.1 bit numbering), immediately followed by the "1" pad bit, and the 64/128-bit length field is (byte_count << 3) | num_partial_bits. do_final_out is the zero-bit case. - do_final_partial_bits_out validates num_partial_bits <= 7 (InvalidLength) and delegates; nothing panics. - Crate docs: partial-bits usage example replaces the "not supported" note. Tests: the core-test-framework partial-input tests are now enabled for all four variants; `partial_bits` checks 0-bit equivalence, range validation, low-bit-only influence and second-block spill; `partial_bits_known_answers` holds 10 SHA-256/SHA-512 bit-length digests generated from an independent pure-Python FIPS 180-4 reference (validated against hashlib on byte inputs). Co-Authored-By: Claude Fable 5 --- crypto/sha2/src/lib.rs | 17 ++++-- crypto/sha2/src/sha256.rs | 47 ++++++++++------- crypto/sha2/src/sha512.rs | 47 ++++++++++------- crypto/sha2/tests/sha2_tests.rs | 91 +++++++++++++++++++++++++-------- 4 files changed, 140 insertions(+), 62 deletions(-) diff --git a/crypto/sha2/src/lib.rs b/crypto/sha2/src/lib.rs index 79f489c7..d7d1dca3 100644 --- a/crypto/sha2/src/lib.rs +++ b/crypto/sha2/src/lib.rs @@ -34,9 +34,18 @@ //! let output: Vec = sha2.do_final(); //! ``` //! -//! Bit-oriented messages (a final byte with fewer than 8 bits, FIPS 180-4 s. 5.1) are not supported: -//! [`Hash::do_final_partial_bits`] returns [`HashError::InvalidInput`] for `num_partial_bits` in `1..=7` -//! (and behaves as [`Hash::do_final`] for `0`). +//! It is also possible to provide input where the final byte contains fewer than 8 bits of data +//! (a bit-oriented message, FIPS 180-4 s. 5.1); the partial bits are taken from the least significant +//! bits of the supplied byte. The following hashes 16 bytes plus 3 bits: +//! ``` +//! use bouncycastle_core::traits::Hash; +//! use bouncycastle_sha2 as sha2; +//! +//! let data: &[u8] = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x05"; +//! let mut sha2 = sha2::SHA256::new(); +//! sha2.do_update(&data[..16]); +//! let output: Vec = sha2.do_final_partial_bits(data[16], 3).expect("num_partial_bits is in 0..=7"); +//! ``` //! //! # Memory Usage //! @@ -113,8 +122,6 @@ use bouncycastle_core::traits::{Algorithm, AlgorithmOID, HashAlgParams, Security /*** Imports needed for docs ***/ #[allow(unused_imports)] -use bouncycastle_core::errors::HashError; -#[allow(unused_imports)] use bouncycastle_core::traits::{Hash, Suspendable}; /*** String constants ***/ diff --git a/crypto/sha2/src/sha256.rs b/crypto/sha2/src/sha256.rs index 041193c9..c30c09f5 100644 --- a/crypto/sha2/src/sha256.rs +++ b/crypto/sha2/src/sha256.rs @@ -154,15 +154,28 @@ impl SHA256Internal { impl SHA256Internal { /// Pads and compresses the final block(s) as per FIPS 180-4 s. 5.1.1, then writes the digest. /// + /// `num_partial_bits` (0..=7, validated by the caller) trailing message bits are taken from the + /// least significant bits of `partial_byte`. FIPS 180-4 s. 3.1 numbers message bits from the most + /// significant bit of each byte, so those bits are shifted to the top of the final message byte + /// and the mandatory "1" padding bit follows them immediately in the same byte. + /// /// Returns the number of bytes written (`min(output.len(), OUTPUT_LEN)`); a shorter output buffer /// truncates the digest, a longer one is zero-filled past the digest. - fn finalize(mut self, output: &mut [u8]) -> usize { + fn finalize(mut self, partial_byte: u8, num_partial_bits: usize, output: &mut [u8]) -> usize { + debug_assert!(num_partial_bits <= 7); output.fill(0); let n = *min(&output.len(), &PARAMS::OUTPUT_LEN); - // FIPS 180-4 s. 5.1.1: append the "1" bit (as the byte 0x80, since messages are whole bytes). - self.x_buf[self.x_buf_off] = 0x80; + // FIPS 180-4 s. 5.1.1: final message byte = [partial bits, MSB-first] [1] [0...]. + // With no partial bits this is the familiar 0x80. Shifts are done in u16 so that the 8-bit + // shift for num_partial_bits == 0 cannot overflow; the masked value is < 2^num_partial_bits so + // the result always fits back into a u8. + let mask: u8 = ((1u16 << num_partial_bits) - 1) as u8; + let message_bits = ((partial_byte & mask) as u16) << (8 - num_partial_bits); + let pad_byte = (message_bits as u8) | (0x80u8 >> num_partial_bits); + + self.x_buf[self.x_buf_off] = pad_byte; self.x_buf_off += 1; // If the length field no longer fits in this block, zero-fill and compress, then start a fresh block. @@ -173,9 +186,10 @@ impl SHA256Internal { } self.x_buf[self.x_buf_off..56].fill(0x00); - // FIPS 180-4 s. 5.1.1: append the 64-bit big-endian message length in bits. byte_count is a - // byte counter, so the length in bits is byte_count << 3. - let bit_len: u64 = self.byte_count << 3; + // FIPS 180-4 s. 5.1.1: append the 64-bit big-endian message length l in bits. byte_count is a + // byte counter, so l = (byte_count << 3) | num_partial_bits (the low three bits of + // byte_count << 3 are zero). + let bit_len: u64 = (self.byte_count << 3) | (num_partial_bits as u64); self.x_buf[56..64].copy_from_slice(&bit_len.to_be_bytes()); self.state.compress(slice::from_ref(&self.x_buf)); @@ -268,7 +282,8 @@ impl Hash for SHA256Internal { } fn do_final_out(self, output: &mut [u8]) -> usize { - self.finalize(output) + // A whole-byte message is the zero-partial-bits case of the general padding. + self.finalize(0, 0, output) } fn do_final_partial_bits( @@ -281,23 +296,19 @@ impl Hash for SHA256Internal { Ok(output) } - /// Bit-oriented (partial final byte) messages are defined by FIPS 180-4 s. 5.1 but are not - /// supported by this implementation, which processes whole bytes only. `num_partial_bits == 0` - /// is the whole-byte case and behaves exactly like [`Hash::do_final_out`]; any other value returns - /// an error rather than panicking. + /// FIPS 180-4 s. 5.1: bit-oriented messages. The `num_partial_bits` least significant bits of + /// `partial_byte` are appended to the message before padding. `num_partial_bits == 0` behaves + /// exactly like [`Hash::do_final_out`]. fn do_final_partial_bits_out( self, - _partial_byte: u8, + partial_byte: u8, num_partial_bits: usize, output: &mut [u8], ) -> Result { - match num_partial_bits { - 0 => Ok(self.finalize(output)), - 1..=7 => Err(HashError::InvalidInput( - "bit-oriented (partial final byte) messages are not supported by this SHA-2 implementation", - )), - _ => Err(HashError::InvalidLength("num_partial_bits must be in the range [0,7]")), + if num_partial_bits > 7 { + return Err(HashError::InvalidLength("num_partial_bits must be in the range [0,7]")); } + Ok(self.finalize(partial_byte, num_partial_bits, output)) } fn max_security_strength(&self) -> SecurityStrength { diff --git a/crypto/sha2/src/sha512.rs b/crypto/sha2/src/sha512.rs index ecd3f076..c24251be 100644 --- a/crypto/sha2/src/sha512.rs +++ b/crypto/sha2/src/sha512.rs @@ -166,15 +166,28 @@ impl SHA512Internal { impl SHA512Internal { /// Pads and compresses the final block(s) as per FIPS 180-4 s. 5.1.2, then writes the digest. /// + /// `num_partial_bits` (0..=7, validated by the caller) trailing message bits are taken from the + /// least significant bits of `partial_byte`. FIPS 180-4 s. 3.1 numbers message bits from the most + /// significant bit of each byte, so those bits are shifted to the top of the final message byte + /// and the mandatory "1" padding bit follows them immediately in the same byte. + /// /// Returns the number of bytes written (`min(output.len(), OUTPUT_LEN)`); a shorter output buffer /// truncates the digest, a longer one is zero-filled past the digest. - fn finalize(mut self, output: &mut [u8]) -> usize { + fn finalize(mut self, partial_byte: u8, num_partial_bits: usize, output: &mut [u8]) -> usize { + debug_assert!(num_partial_bits <= 7); output.fill(0); let n = *min(&output.len(), &PARAMS::OUTPUT_LEN); - // FIPS 180-4 s. 5.1.2: append the "1" bit (as the byte 0x80, since messages are whole bytes). - self.x_buf[self.x_buf_off] = 0x80; + // FIPS 180-4 s. 5.1.2: final message byte = [partial bits, MSB-first] [1] [0...]. + // With no partial bits this is the familiar 0x80. Shifts are done in u16 so that the 8-bit + // shift for num_partial_bits == 0 cannot overflow; the masked value is < 2^num_partial_bits so + // the result always fits back into a u8. + let mask: u8 = ((1u16 << num_partial_bits) - 1) as u8; + let message_bits = ((partial_byte & mask) as u16) << (8 - num_partial_bits); + let pad_byte = (message_bits as u8) | (0x80u8 >> num_partial_bits); + + self.x_buf[self.x_buf_off] = pad_byte; self.x_buf_off += 1; // If the length field no longer fits in this block, zero-fill and compress, then start a fresh block. @@ -185,10 +198,11 @@ impl SHA512Internal { } self.x_buf[self.x_buf_off..112].fill(0x00); - // FIPS 180-4 s. 5.1.2: append the 128-bit big-endian message length in bits. byte_count is a - // byte counter so the high 64 bits are byte_count >> 61 and the low 64 bits are byte_count << 3. + // FIPS 180-4 s. 5.1.2: append the 128-bit big-endian message length l in bits. byte_count is a + // byte counter, so the high 64 bits are byte_count >> 61 and the low 64 bits are + // (byte_count << 3) | num_partial_bits (the low three bits of byte_count << 3 are zero). let bit_len_hi: u64 = self.byte_count >> 61; - let bit_len_lo: u64 = self.byte_count << 3; + let bit_len_lo: u64 = (self.byte_count << 3) | (num_partial_bits as u64); self.x_buf[112..120].copy_from_slice(&bit_len_hi.to_be_bytes()); self.x_buf[120..128].copy_from_slice(&bit_len_lo.to_be_bytes()); self.state.compress(slice::from_ref(&self.x_buf)); @@ -281,7 +295,8 @@ impl Hash for SHA512Internal { } fn do_final_out(self, output: &mut [u8]) -> usize { - self.finalize(output) + // A whole-byte message is the zero-partial-bits case of the general padding. + self.finalize(0, 0, output) } fn do_final_partial_bits( @@ -294,23 +309,19 @@ impl Hash for SHA512Internal { Ok(output) } - /// Bit-oriented (partial final byte) messages are defined by FIPS 180-4 s. 5.1 but are not - /// supported by this implementation, which processes whole bytes only. `num_partial_bits == 0` - /// is the whole-byte case and behaves exactly like [`Hash::do_final_out`]; any other value returns - /// an error rather than panicking. + /// FIPS 180-4 s. 5.1: bit-oriented messages. The `num_partial_bits` least significant bits of + /// `partial_byte` are appended to the message before padding. `num_partial_bits == 0` behaves + /// exactly like [`Hash::do_final_out`]. fn do_final_partial_bits_out( self, - _partial_byte: u8, + partial_byte: u8, num_partial_bits: usize, output: &mut [u8], ) -> Result { - match num_partial_bits { - 0 => Ok(self.finalize(output)), - 1..=7 => Err(HashError::InvalidInput( - "bit-oriented (partial final byte) messages are not supported by this SHA-2 implementation", - )), - _ => Err(HashError::InvalidLength("num_partial_bits must be in the range [0,7]")), + if num_partial_bits > 7 { + return Err(HashError::InvalidLength("num_partial_bits must be in the range [0,7]")); } + Ok(self.finalize(partial_byte, num_partial_bits, output)) } fn max_security_strength(&self) -> SecurityStrength { diff --git a/crypto/sha2/tests/sha2_tests.rs b/crypto/sha2/tests/sha2_tests.rs index 6943b9b1..36d412b3 100644 --- a/crypto/sha2/tests/sha2_tests.rs +++ b/crypto/sha2/tests/sha2_tests.rs @@ -12,8 +12,7 @@ mod sha2_tests { #[test] fn sha224() { - let mut test_framework = TestFrameworkHash::new(); - test_framework.enable_partial_final_input_tests = false; + let test_framework = TestFrameworkHash::new(); test_framework.test_hash::(b"", b"\xd1\x4a\x02\x8c\x2a\x3a\x2b\xc9\x47\x61\x02\xbb\x28\x82\x34\xc4\x15\xa2\xb0\x1f\x82\x8e\xa6\x2a\xc5\xb3\xe4\x2f"); test_framework.test_hash::(b"a", b"\xab\xd3\x75\x34\xc7\xd9\xa2\xef\xb9\x46\x5d\xe9\x31\xcd\x70\x55\xff\xdb\x88\x79\x56\x3a\xe9\x80\x78\xd6\xd6\xd5"); test_framework.test_hash::(b"abc", b"\x23\x09\x7d\x22\x34\x05\xd8\x22\x86\x42\xa4\x77\xbd\xa2\x55\xb3\x2a\xad\xbc\xe4\xbd\xa0\xb3\xf7\xe3\x6c\x9d\xa7"); @@ -24,8 +23,7 @@ mod sha2_tests { #[test] fn sha256() { - let mut test_framework = TestFrameworkHash::new(); - test_framework.enable_partial_final_input_tests = false; + let test_framework = TestFrameworkHash::new(); test_framework.test_hash::(b"", b"\xe3\xb0\xc4\x42\x98\xfc\x1c\x14\x9a\xfb\xf4\xc8\x99\x6f\xb9\x24\x27\xae\x41\xe4\x64\x9b\x93\x4c\xa4\x95\x99\x1b\x78\x52\xb8\x55"); test_framework.test_hash::(b"a", b"\xca\x97\x81\x12\xca\x1b\xbd\xca\xfa\xc2\x31\xb3\x9a\x23\xdc\x4d\xa7\x86\xef\xf8\x14\x7c\x4e\x72\xb9\x80\x77\x85\xaf\xee\x48\xbb"); test_framework.test_hash::(b"abc", b"\xba\x78\x16\xbf\x8f\x01\xcf\xea\x41\x41\x40\xde\x5d\xae\x22\x23\xb0\x03\x61\xa3\x96\x17\x7a\x9c\xb4\x10\xff\x61\xf2\x00\x15\xad"); @@ -35,8 +33,7 @@ mod sha2_tests { #[test] fn sha384() { - let mut test_framework = TestFrameworkHash::new(); - test_framework.enable_partial_final_input_tests = false; + let test_framework = TestFrameworkHash::new(); test_framework.test_hash::(b"", b"\x38\xb0\x60\xa7\x51\xac\x96\x38\x4c\xd9\x32\x7e\xb1\xb1\xe3\x6a\x21\xfd\xb7\x11\x14\xbe\x07\x43\x4c\x0c\xc7\xbf\x63\xf6\xe1\xda\x27\x4e\xde\xbf\xe7\x6f\x65\xfb\xd5\x1a\xd2\xf1\x48\x98\xb9\x5b"); test_framework.test_hash::(b"a", b"\x54\xa5\x9b\x9f\x22\xb0\xb8\x08\x80\xd8\x42\x7e\x54\x8b\x7c\x23\xab\xd8\x73\x48\x6e\x1f\x03\x5d\xce\x9c\xd6\x97\xe8\x51\x75\x03\x3c\xaa\x88\xe6\xd5\x7b\xc3\x5e\xfa\xe0\xb5\xaf\xd3\x14\x5f\x31"); test_framework.test_hash::(b"abc", b"\xcb\x00\x75\x3f\x45\xa3\x5e\x8b\xb5\xa0\x3d\x69\x9a\xc6\x50\x07\x27\x2c\x32\xab\x0e\xde\xd1\x63\x1a\x8b\x60\x5a\x43\xff\x5b\xed\x80\x86\x07\x2b\xa1\xe7\xcc\x23\x58\xba\xec\xa1\x34\xc8\x25\xa7"); @@ -46,8 +43,7 @@ mod sha2_tests { #[test] fn sha512() { - let mut test_framework = TestFrameworkHash::new(); - test_framework.enable_partial_final_input_tests = false; + let test_framework = TestFrameworkHash::new(); test_framework.test_hash::(b"", b"\xcf\x83\xe1\x35\x7e\xef\xb8\xbd\xf1\x54\x28\x50\xd6\x6d\x80\x07\xd6\x20\xe4\x05\x0b\x57\x15\xdc\x83\xf4\xa9\x21\xd3\x6c\xe9\xce\x47\xd0\xd1\x3c\x5d\x85\xf2\xb0\xff\x83\x18\xd2\x87\x7e\xec\x2f\x63\xb9\x31\xbd\x47\x41\x7a\x81\xa5\x38\x32\x7a\xf9\x27\xda\x3e"); test_framework.test_hash::(b"a", b"\x1f\x40\xfc\x92\xda\x24\x16\x94\x75\x09\x79\xee\x6c\xf5\x82\xf2\xd5\xd7\xd2\x8e\x18\x33\x5d\xe0\x5a\xbc\x54\xd0\x56\x0e\x0f\x53\x02\x86\x0c\x65\x2b\xf0\x8d\x56\x02\x52\xaa\x5e\x74\x21\x05\x46\xf3\x69\xfb\xbb\xce\x8c\x12\xcf\xc7\x95\x7b\x26\x52\xfe\x9a\x75"); test_framework.test_hash::(b"abc", b"\xdd\xaf\x35\xa1\x93\x61\x7a\xba\xcc\x41\x73\x49\xae\x20\x41\x31\x12\xe6\xfa\x4e\x89\xa9\x7e\xa2\x0a\x9e\xee\xe6\x4b\x55\xd3\x9a\x21\x92\x99\x2a\x27\x4f\xc1\xa8\x36\xba\x3c\x23\xa3\xfe\xeb\xbd\x45\x4d\x44\x23\x64\x3c\xe8\x0e\x2a\x9a\xc9\x4f\xa5\x4c\xa4\x9f"); @@ -56,29 +52,46 @@ mod sha2_tests { } } - /// Bit-oriented messages are not supported by the SHA-2 implementation: 0 partial bits must equal - /// do_final(); 1..=7 returns InvalidInput; more than 7 returns InvalidLength. None of them panic. + /// FIPS 180-4 s. 5.1: bit-oriented messages. Zero partial bits must equal the byte-oriented + /// digest; more than 7 partial bits is rejected; only the low bits of the partial byte matter; + /// and the pad byte spilling into a second block must not break. Known answers are in + /// `partial_bits_known_answers`. #[test] - fn partial_bits_are_rejected() { + fn partial_bits() { fn check() { + // 0 partial bits == do_final let mut a = H::default(); a.do_update(b"abc"); assert_eq!(a.do_final_partial_bits(0xFF, 0).unwrap(), H::default().hash(b"abc")); - for n in 1..=7usize { + // out of range -> InvalidLength, never a panic + for bad in [8usize, 9, 16, 64, usize::MAX] { let mut h = H::default(); h.do_update(b"abc"); - assert!(matches!(h.do_final_partial_bits(0xFF, n), Err(HashError::InvalidInput(_))), "n={n}"); - let mut out = [0u8; 64]; - assert!(matches!( - H::default().do_final_partial_bits_out(0xFF, n, &mut out), - Err(HashError::InvalidInput(_)) - )); + assert!(matches!(h.do_final_partial_bits(0xFF, bad), Err(HashError::InvalidLength(_)))); } - for bad in [8usize, 9, 16, 64, usize::MAX] { + + // only the low num_partial_bits bits of partial_byte may influence the result + for n in 1..=7usize { + let mask = ((1u16 << n) - 1) as u8; + let x = H::default().do_final_partial_bits(0xA5, n).unwrap(); + let y = H::default().do_final_partial_bits(0xA5 & mask, n).unwrap(); + let z = H::default().do_final_partial_bits(0xA5 ^ 1, n).unwrap(); + assert_eq!(x, y, "n={n}"); + assert_ne!(x, z, "n={n}: low bit must change the digest"); + // and a bit-message is distinct from byte-messages of nearby length + assert_ne!(x, H::default().hash(&[]), "n={n}"); + assert_ne!(x, H::default().hash(&[0xA5 & mask]), "n={n}"); + } + + // the partial-bit path must also work when the pad byte spills into a second block + for len in [55usize, 56, 63, 64, 111, 112, 119, 127, 128] { + let msg = vec![0x5Au8; len]; let mut h = H::default(); - h.do_update(b"abc"); - assert!(matches!(h.do_final_partial_bits(0xFF, bad), Err(HashError::InvalidLength(_))), "n={bad}"); + h.do_update(&msg); + let mut out = vec![0u8; 64]; + let written = h.do_final_partial_bits_out(0x03, 2, &mut out).unwrap(); + assert!(written > 0); } } check::(); @@ -87,6 +100,42 @@ mod sha2_tests { check::(); } + /// Bit-oriented known answers (FIPS 180-4 s. 5.1). Expected values were produced by an + /// independent pure-Python implementation of FIPS 180-4 with bit-length padding, itself checked + /// against `hashlib` for byte-aligned inputs. `(prefix_len, fill, partial_byte, bits, digest)`. + #[test] + fn partial_bits_known_answers() { + fn hex(s: &str) -> Vec { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() + } + fn check(cases: &[(usize, u8, u8, usize, &str)]) { + for &(prefix_len, fill, partial_byte, bits, expected) in cases { + let mut h = H::default(); + h.do_update(&vec![fill; prefix_len]); + assert_eq!(h.do_final_partial_bits(partial_byte, bits).unwrap(), hex(expected), "{prefix_len}/{bits}"); + } + } + check::(&[ + (0, 0, 0x01, 1, "b9debf7d52f36e6468a54817c1fa071166c3a63d384850e1575b42f702dc5aa1"), + (0, 0, 0x15, 5, "9a6eb6cad1c1017a060c4cc9d1be5c9404397e4d05c8e6c91f6347db8591c1a9"), + (55, 0x5a, 0x03, 2, "f9f22d1e48f4d6fe0f84db4a04bef65d4be116e4f182845b8a827c897b05723a"), + (111, 0x5a, 0x05, 3, "bf63c89e04968fba3fc26ccf8908e0b2d05221834a17f912b48d9816d821be6d"), + ]); + let mut h = SHA256::new(); + h.do_update(b"abc"); + assert_eq!(h.do_final_partial_bits(0x7f, 7).unwrap(), hex("9f5893e1b85faf8d646489927b5bc22b7394e2a14bbd47da00bbce3a1b27a5ba")); + + check::(&[ + (0, 0, 0x01, 1, "5f72ee8494a425ba13fc8c48ac0a05cbaae7e932e471e948cb524333745aa432c1851c0c43682b0e67d64626f8f45cf165f6b538a94c63be98224e969e75d7ed"), + (0, 0, 0x15, 5, "dcaab1be5ce172f510ebe2da22f6488bd2f706c8124d6bb16de5cfb3432f0dd6e7262dd35206d500180b70563c419e142c354b6ac155ca8a3f0f0fdb88d567e9"), + (55, 0x5a, 0x03, 2, "4fe3a857ce5d8abc5dcc7ea0d3f97ff7bb0db06001e1f37c2c2c9d48bd4c609af169b0f5d200d1b9033af31819095a4679b62d87b15673a85ac75c8ecbc2bd57"), + (111, 0x5a, 0x05, 3, "f0af9c9852d733b024e097ae6aa9e7959c84c05a666b04f3c0df368e2ea93bcccf9136aefa54b0c4db432217742dec7d77365b3f5a6b63fe46c9fc259b8f0101"), + ]); + let mut h = SHA512::new(); + h.do_update(b"abc"); + assert_eq!(h.do_final_partial_bits(0x7f, 7).unwrap(), hex("ec168db3beb4379ddd4dd854461ac533f047f69ebf4770dec59442994a8320a4f240eeb0d808f8b7dc8d23d0428af5f095cc2ded70c516aef86ca68e99f8ffe6")); + } + #[test] fn test_constants() { assert_eq!(SHA224::OUTPUT_LEN, 28); From 59cc7002ea4ee8418521d10d2a29b3267d9e817c Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 26 Aug 2026 16:02:00 +1000 Subject: [PATCH 3/5] sha2: rustfmt Co-Authored-By: Claude Fable 5 --- crypto/sha2/src/lib.rs | 6 ++-- crypto/sha2/tests/sha2_tests.rs | 63 ++++++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/crypto/sha2/src/lib.rs b/crypto/sha2/src/lib.rs index d7d1dca3..1a6bfc96 100644 --- a/crypto/sha2/src/lib.rs +++ b/crypto/sha2/src/lib.rs @@ -193,7 +193,8 @@ impl SHA2Params for SHA224Params {} /// FIPS 180-4 s. 5.3 initial hash value for SHA224. impl Sha256Family for SHA224Params { const H0: [u32; 8] = [ - 0xC1059ED8, 0x367CD507, 0x3070DD17, 0xF70E5939, 0xFFC00B31, 0x68581511, 0x64F98FA7, 0xBEFA4FA4, + 0xC1059ED8, 0x367CD507, 0x3070DD17, 0xF70E5939, 0xFFC00B31, 0x68581511, 0x64F98FA7, + 0xBEFA4FA4, ]; } @@ -219,7 +220,8 @@ impl SHA2Params for SHA256Params {} /// FIPS 180-4 s. 5.3 initial hash value for SHA256. impl Sha256Family for SHA256Params { const H0: [u32; 8] = [ - 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19, + 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, + 0x5BE0CD19, ]; } diff --git a/crypto/sha2/tests/sha2_tests.rs b/crypto/sha2/tests/sha2_tests.rs index 36d412b3..ed89f3cd 100644 --- a/crypto/sha2/tests/sha2_tests.rs +++ b/crypto/sha2/tests/sha2_tests.rs @@ -68,7 +68,10 @@ mod sha2_tests { for bad in [8usize, 9, 16, 64, usize::MAX] { let mut h = H::default(); h.do_update(b"abc"); - assert!(matches!(h.do_final_partial_bits(0xFF, bad), Err(HashError::InvalidLength(_)))); + assert!(matches!( + h.do_final_partial_bits(0xFF, bad), + Err(HashError::InvalidLength(_)) + )); } // only the low num_partial_bits bits of partial_byte may influence the result @@ -112,28 +115,70 @@ mod sha2_tests { for &(prefix_len, fill, partial_byte, bits, expected) in cases { let mut h = H::default(); h.do_update(&vec![fill; prefix_len]); - assert_eq!(h.do_final_partial_bits(partial_byte, bits).unwrap(), hex(expected), "{prefix_len}/{bits}"); + assert_eq!( + h.do_final_partial_bits(partial_byte, bits).unwrap(), + hex(expected), + "{prefix_len}/{bits}" + ); } } check::(&[ (0, 0, 0x01, 1, "b9debf7d52f36e6468a54817c1fa071166c3a63d384850e1575b42f702dc5aa1"), (0, 0, 0x15, 5, "9a6eb6cad1c1017a060c4cc9d1be5c9404397e4d05c8e6c91f6347db8591c1a9"), (55, 0x5a, 0x03, 2, "f9f22d1e48f4d6fe0f84db4a04bef65d4be116e4f182845b8a827c897b05723a"), - (111, 0x5a, 0x05, 3, "bf63c89e04968fba3fc26ccf8908e0b2d05221834a17f912b48d9816d821be6d"), + ( + 111, + 0x5a, + 0x05, + 3, + "bf63c89e04968fba3fc26ccf8908e0b2d05221834a17f912b48d9816d821be6d", + ), ]); let mut h = SHA256::new(); h.do_update(b"abc"); - assert_eq!(h.do_final_partial_bits(0x7f, 7).unwrap(), hex("9f5893e1b85faf8d646489927b5bc22b7394e2a14bbd47da00bbce3a1b27a5ba")); + assert_eq!( + h.do_final_partial_bits(0x7f, 7).unwrap(), + hex("9f5893e1b85faf8d646489927b5bc22b7394e2a14bbd47da00bbce3a1b27a5ba") + ); check::(&[ - (0, 0, 0x01, 1, "5f72ee8494a425ba13fc8c48ac0a05cbaae7e932e471e948cb524333745aa432c1851c0c43682b0e67d64626f8f45cf165f6b538a94c63be98224e969e75d7ed"), - (0, 0, 0x15, 5, "dcaab1be5ce172f510ebe2da22f6488bd2f706c8124d6bb16de5cfb3432f0dd6e7262dd35206d500180b70563c419e142c354b6ac155ca8a3f0f0fdb88d567e9"), - (55, 0x5a, 0x03, 2, "4fe3a857ce5d8abc5dcc7ea0d3f97ff7bb0db06001e1f37c2c2c9d48bd4c609af169b0f5d200d1b9033af31819095a4679b62d87b15673a85ac75c8ecbc2bd57"), - (111, 0x5a, 0x05, 3, "f0af9c9852d733b024e097ae6aa9e7959c84c05a666b04f3c0df368e2ea93bcccf9136aefa54b0c4db432217742dec7d77365b3f5a6b63fe46c9fc259b8f0101"), + ( + 0, + 0, + 0x01, + 1, + "5f72ee8494a425ba13fc8c48ac0a05cbaae7e932e471e948cb524333745aa432c1851c0c43682b0e67d64626f8f45cf165f6b538a94c63be98224e969e75d7ed", + ), + ( + 0, + 0, + 0x15, + 5, + "dcaab1be5ce172f510ebe2da22f6488bd2f706c8124d6bb16de5cfb3432f0dd6e7262dd35206d500180b70563c419e142c354b6ac155ca8a3f0f0fdb88d567e9", + ), + ( + 55, + 0x5a, + 0x03, + 2, + "4fe3a857ce5d8abc5dcc7ea0d3f97ff7bb0db06001e1f37c2c2c9d48bd4c609af169b0f5d200d1b9033af31819095a4679b62d87b15673a85ac75c8ecbc2bd57", + ), + ( + 111, + 0x5a, + 0x05, + 3, + "f0af9c9852d733b024e097ae6aa9e7959c84c05a666b04f3c0df368e2ea93bcccf9136aefa54b0c4db432217742dec7d77365b3f5a6b63fe46c9fc259b8f0101", + ), ]); let mut h = SHA512::new(); h.do_update(b"abc"); - assert_eq!(h.do_final_partial_bits(0x7f, 7).unwrap(), hex("ec168db3beb4379ddd4dd854461ac533f047f69ebf4770dec59442994a8320a4f240eeb0d808f8b7dc8d23d0428af5f095cc2ded70c516aef86ca68e99f8ffe6")); + assert_eq!( + h.do_final_partial_bits(0x7f, 7).unwrap(), + hex( + "ec168db3beb4379ddd4dd854461ac533f047f69ebf4770dec59442994a8320a4f240eeb0d808f8b7dc8d23d0428af5f095cc2ded70c516aef86ca68e99f8ffe6" + ) + ); } #[test] From 3743d8653e0e7f4a880191eefdb0298d18a49545 Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 27 Aug 2026 12:52:35 +1000 Subject: [PATCH 4/5] sha2: add NIST CAVP SHAVS test harness (bit- and byte-oriented, Monte) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reads the SHAVS .rsp files from ../bc-test-data/crypto/sha2/{bit-oriented, byte-oriented}/ using the same lookup/skip-with-warning convention as the mldsa, mlkem and sha3 crates, and runs ShortMsg, LongMsg and Monte Carlo (SHAVS s. 6.4) for SHA-224/256/384/512 — 24 tests, ~12k message cases of which ~5.4k are bit-length messages exercising do_final_partial_bits(). CAVP left-justifies the trailing message bits in the final Msg byte while the Hash API takes them in the least significant bits, so the harness shifts the last byte right by (8 - n). SHA-512/224 and SHA-512/256 vectors are present in bc-test-data but not implemented by this crate, so are skipped. Adds bouncycastle-hex as a dev-dependency. Requested in PR #88 review. Co-Authored-By: Claude Fable 5 --- crypto/sha2/Cargo.toml | 1 + crypto/sha2/tests/cavp_tests.rs | 199 ++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 crypto/sha2/tests/cavp_tests.rs diff --git a/crypto/sha2/Cargo.toml b/crypto/sha2/Cargo.toml index 7ff2e037..558da22a 100644 --- a/crypto/sha2/Cargo.toml +++ b/crypto/sha2/Cargo.toml @@ -11,6 +11,7 @@ bouncycastle-utils.workspace = true criterion.workspace = true bouncycastle-core-test-framework.workspace = true bouncycastle-rng.workspace = true +bouncycastle-hex.workspace = true [[bench]] name = "sha2_benches" diff --git a/crypto/sha2/tests/cavp_tests.rs b/crypto/sha2/tests/cavp_tests.rs new file mode 100644 index 00000000..7b30bb85 --- /dev/null +++ b/crypto/sha2/tests/cavp_tests.rs @@ -0,0 +1,199 @@ +//! NIST CAVP SHAVS test vectors for SHA-224/256/384/512. +//! +//! Vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), which must be +//! cloned alongside this repo at "../bc-test-data" (same convention as the mldsa/mlkem/sha3 crates), +//! under `crypto/sha2/{bit-oriented,byte-oriented}/`. If it is not present the tests print a warning +//! and pass vacuously. +//! +//! Three SHAVS test types are exercised (SHAVS s. 6): +//! +//! * ShortMsg / LongMsg — `Len` (bits), `Msg`, `MD`. In the bit-oriented files `Len` is not a +//! multiple of 8 for most cases; the trailing bits are packed MSB-first in the final `Msg` byte +//! (SHAVS s. 6.2, "the message is left-justified"), whereas [`Hash::do_final_partial_bits`] takes +//! them in the least significant bits, hence the `>> (8 - n)` when feeding the last byte. +//! * Monte — SHAVS s. 6.4 pseudo-random message test: `MD0 = MD1 = MD2 = Seed`, +//! `MDi = SHA(MDi-3 || MDi-2 || MDi-1)` for i in 3..=1002, `MD = MD1002`, then reseed with `MD` +//! for the next COUNT. 100 counts per file. +//! +//! SHA-512/224 and SHA-512/256 files are present in bc-test-data but those algorithms are not +//! implemented by this crate, so they are not exercised here. + +use bouncycastle_core::traits::Hash; +use bouncycastle_hex as hex; +use bouncycastle_sha2::{SHA224, SHA256, SHA384, SHA512}; +use std::fs; +use std::path::Path; +use std::sync::Once; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/crypto/sha2"; +const TEST_DATA_PATH: &str = "../bc-test-data/crypto/sha2"; + +static TEST_DATA_CHECK: Once = Once::new(); + +/// Returns the contents of `/` from bc-test-data, or `None` (after a one-time +/// warning) if the repo is not checked out. +fn get_test_data(orientation: &str, filename: &str) -> Option { + let dir = [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH].into_iter().find(|d| Path::new(d).exists()); + TEST_DATA_CHECK.call_once(|| match dir { + Some(d) => println!("bc-test-data found at: {d:?}"), + None => println!("WARNING: bc-test-data directory not found; CAVP tests will be skipped"), + }); + let dir = dir?; + Some( + fs::read_to_string(format!("{dir}/{orientation}/{filename}")) + .expect("failed to read CAVP test vector file"), + ) +} + +/// Splits a `Key = value` line from a `.rsp` file. +fn kv(line: &str) -> Option<(&str, &str)> { + let (k, v) = line.split_once('=')?; + Some((k.trim(), v.trim())) +} + +struct MsgCase { + len_bits: usize, + msg: Vec, + md: Vec, +} + +/// Parses a ShortMsg/LongMsg `.rsp` file into `(Len, Msg, MD)` triples. +fn parse_msg_file(content: &str) -> Vec { + let mut cases = vec![]; + let (mut len_bits, mut msg) = (None, None); + for line in content.lines() { + let Some((k, v)) = kv(line) else { continue }; + match k { + "Len" => len_bits = Some(v.parse::().expect("bad Len")), + "Msg" => msg = Some(hex::decode(v).expect("bad Msg hex")), + "MD" => cases.push(MsgCase { + len_bits: len_bits.take().expect("MD without Len"), + msg: msg.take().expect("MD without Msg"), + md: hex::decode(v).expect("bad MD hex"), + }), + _ => {} + } + } + cases +} + +/// Hashes the first `len_bits` bits of `msg` (CAVP MSB-first packing) with `H`. +fn hash_bits(msg: &[u8], len_bits: usize) -> Vec { + let whole_bytes = len_bits / 8; + let partial_bits = len_bits % 8; + if partial_bits == 0 { + // Note: CAVP writes `Msg = 00` for Len = 0, so always slice rather than using msg directly. + H::default().hash(&msg[..whole_bytes]) + } else { + let mut h = H::default(); + h.do_update(&msg[..whole_bytes]); + // CAVP left-justifies the trailing bits in the last byte; the API wants them in the LSBs. + let partial_byte = msg[whole_bytes] >> (8 - partial_bits); + h.do_final_partial_bits(partial_byte, partial_bits).expect("partial_bits is in 1..=7") + } +} + +fn run_msg_file(orientation: &str, filename: &str) { + let Some(content) = get_test_data(orientation, filename) else { return }; + let cases = parse_msg_file(&content); + assert!(!cases.is_empty(), "{orientation}/{filename}: no test cases parsed"); + let mut partial_cases = 0; + for c in &cases { + if c.len_bits % 8 != 0 { + partial_cases += 1; + } + assert_eq!( + hash_bits::(&c.msg, c.len_bits), + c.md, + "{orientation}/{filename}: Len = {}", + c.len_bits + ); + } + if orientation == "bit-oriented" { + assert!(partial_cases > 0, "{orientation}/{filename}: expected bit-length cases"); + } + println!("{orientation}/{filename}: {} cases ({partial_cases} bit-length)", cases.len()); +} + +struct MonteFile { + seed: Vec, + mds: Vec>, +} + +/// Parses a Monte `.rsp` file into the seed and the per-COUNT expected digests. +fn parse_monte_file(content: &str) -> MonteFile { + let mut seed = None; + let mut mds = vec![]; + for line in content.lines() { + let Some((k, v)) = kv(line) else { continue }; + match k { + "Seed" => seed = Some(hex::decode(v).expect("bad Seed hex")), + "MD" => mds.push(hex::decode(v).expect("bad MD hex")), + _ => {} + } + } + MonteFile { seed: seed.expect("Monte file without Seed"), mds } +} + +/// SHAVS s. 6.4 Monte Carlo test. +fn run_monte_file(orientation: &str, filename: &str) { + let Some(content) = get_test_data(orientation, filename) else { return }; + let MonteFile { mut seed, mds } = parse_monte_file(&content); + assert_eq!(mds.len(), 100, "{orientation}/{filename}: expected 100 COUNTs"); + for (count, expected) in mds.iter().enumerate() { + // MD0 = MD1 = MD2 = Seed + let mut md = [seed.clone(), seed.clone(), seed.clone()]; + // for i = 3 to 1002: Mi = MDi-3 || MDi-2 || MDi-1; MDi = SHA(Mi) + for _ in 3..=1002 { + let mut m = Vec::with_capacity(3 * seed.len()); + m.extend_from_slice(&md[0]); + m.extend_from_slice(&md[1]); + m.extend_from_slice(&md[2]); + let next = H::default().hash(&m); + md.rotate_left(1); + md[2] = next; + } + // MDj = MD1002; Seed = MDj + assert_eq!(&md[2], expected, "{orientation}/{filename}: COUNT = {count}"); + seed = md[2].clone(); + } + println!("{orientation}/{filename}: {} counts", mds.len()); +} + +macro_rules! cavp_tests { + ($mod:ident, $hash:ty, $prefix:literal) => { + mod $mod { + use super::*; + + #[test] + fn bit_oriented_short_msg() { + run_msg_file::<$hash>("bit-oriented", concat!($prefix, "ShortMsg.rsp")); + } + #[test] + fn bit_oriented_long_msg() { + run_msg_file::<$hash>("bit-oriented", concat!($prefix, "LongMsg.rsp")); + } + #[test] + fn bit_oriented_monte() { + run_monte_file::<$hash>("bit-oriented", concat!($prefix, "Monte.rsp")); + } + #[test] + fn byte_oriented_short_msg() { + run_msg_file::<$hash>("byte-oriented", concat!($prefix, "ShortMsg.rsp")); + } + #[test] + fn byte_oriented_long_msg() { + run_msg_file::<$hash>("byte-oriented", concat!($prefix, "LongMsg.rsp")); + } + #[test] + fn byte_oriented_monte() { + run_monte_file::<$hash>("byte-oriented", concat!($prefix, "Monte.rsp")); + } + } + }; +} + +cavp_tests!(sha224, SHA224, "SHA224"); +cavp_tests!(sha256, SHA256, "SHA256"); +cavp_tests!(sha384, SHA384, "SHA384"); +cavp_tests!(sha512, SHA512, "SHA512"); From f94559c025ccdb3a0ad05f732fa3b3b26bebe5c5 Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 27 Aug 2026 13:33:19 +1000 Subject: [PATCH 5/5] Add 0.1.3 release notes for the SHA-2 changes (PR #88) Co-Authored-By: Claude Fable 5 --- alpha_0.1.3_release_notes.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index 210a5aeb..6e4058c9 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -3,3 +3,26 @@ ## Major features ## Minor features / bug fixes + +SHA-2 (PR #88): + +* `Hash::do_final_partial_bits()` / `do_final_partial_bits_out()` are now implemented for SHA-224/256/384/512 + (FIPS 180-4 s. 5.1), bringing SHA-2 to parity with SHA-3 for messages whose length is not a multiple of 8 bits. + Previously these methods hit `unimplemented!()` -- a panic behind a `Result`-returning API. `num_partial_bits` may be + 0..=7 (0 behaves exactly as `do_final_out()`); larger values return `HashError::InvalidLength`. The trailing bits are + taken from the least significant bits of `partial_byte`, the same convention as SHA-3 (see the `Hash` trait docs). +* Initial hash values are now compile-time constants (`const H0` on the params traits), removing a runtime + match-on-`OUTPUT_LEN` and its `panic!` arm. `HashAlgParams` for the public types is forwarded from the `*Params` + structs, so `OUTPUT_LEN` / `BLOCK_LEN` are defined once. +* Crate docs: fixed SHA-3/SHAKE copy-paste text, added a partial-bits usage example, "Memory Usage" and + "Security Considerations" sections, and documented the `*_NAME` constants. The 2^64-byte message-length limit is + now stated. + +Testing: + +* SHA-2 now runs the NIST CAVP SHAVS vector sets from bc-test-data (`crypto/sha2`: ShortMsg, LongMsg and Monte Carlo; + bit- and byte-oriented, ~12k cases of which ~5.4k are bit-length messages) using the same `../bc-test-data` lookup + convention as the mldsa/mlkem crates; the tests skip with a warning if the repo is not checked out. The SHAVS files + pack trailing message bits MSB-first, so the harness shifts them into the LSB convention used by the API. Note that + `cargo mutants` runs in a copied tree where `../bc-test-data` does not resolve, so these tests do not contribute to + mutation coverage.