From 4053f215208609baa05a6a237f57443a35743e1a Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 15:06:06 +1000 Subject: [PATCH 01/22] core, sha3: XOF extends Hash, so SHAKE128 and SHAKE256 are hashes; squeezing becomes its own type --- cli/src/sha3_cmd.rs | 7 +- crypto/core-test-framework/src/xof.rs | 386 ++++++++---------- crypto/core/src/traits.rs | 139 +++---- crypto/factory/src/xof_factory.rs | 162 ++++++-- crypto/mldsa-lowmemory/src/aux_functions.rs | 36 +- crypto/mldsa-lowmemory/src/hash_mldsa.rs | 34 +- crypto/mldsa-lowmemory/src/mldsa.rs | 42 +- crypto/mldsa-lowmemory/src/mldsa_keys.rs | 17 +- crypto/mldsa-lowmemory/tests/bc_test_data.rs | 19 +- crypto/mldsa-lowmemory/tests/mldsa_tests.rs | 9 +- crypto/mldsa/src/aux_functions.rs | 37 +- crypto/mldsa/src/hash_mldsa.rs | 34 +- crypto/mldsa/src/matrix.rs | 4 +- crypto/mldsa/src/mldsa.rs | 76 ++-- crypto/mldsa/tests/bc_test_data.rs | 16 +- crypto/mldsa/tests/mldsa_tests.rs | 9 +- crypto/mlkem-lowmemory/src/aux_functions.rs | 25 +- crypto/mlkem-lowmemory/src/mlkem.rs | 8 +- crypto/mlkem-lowmemory/tests/mlkem_tests.rs | 12 +- crypto/mlkem/src/aux_functions.rs | 25 +- crypto/mlkem/src/mlkem.rs | 8 +- crypto/mlkem/tests/mlkem_tests.rs | 12 +- crypto/sha3/src/lib.rs | 30 +- crypto/sha3/src/shake.rs | 325 ++++++++++----- crypto/sha3/tests/bc-test-data.rs | 24 +- crypto/sha3/tests/shake_tests.rs | 227 +++------- mem_usage_benches/src/bench_sha3_mem_usage.rs | 12 +- 27 files changed, 902 insertions(+), 833 deletions(-) diff --git a/cli/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index b6107e0c..b620e9c1 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -1,4 +1,4 @@ -use bouncycastle::core::traits::{Hash, XOF}; +use bouncycastle::core::traits::{Hash, XOF, XofOutput}; use std::io; use std::io::{Read, Write}; @@ -49,11 +49,12 @@ fn do_shake(mut shake: impl XOF, output_len: usize, output_hex: bool) { // read from stdin let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); while bytes_read != 0 { - shake.absorb(&buf[..bytes_read]).expect("absorb before squeeze is infallible"); + shake.do_update(&buf[..bytes_read]); bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); } - let out = shake.squeeze(output_len); + let mut shake = shake.into_output(); + let out = shake.do_output(output_len); if output_hex { for b in out.iter() { print!("{b:02x}"); diff --git a/crypto/core-test-framework/src/xof.rs b/crypto/core-test-framework/src/xof.rs index fbbe7006..46edb466 100644 --- a/crypto/core-test-framework/src/xof.rs +++ b/crypto/core-test-framework/src/xof.rs @@ -1,12 +1,12 @@ //! Generic behaviour tests for anything that implements [`XOF`]. use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{XOF, XofOutput}; /// Instance of the test framework. pub struct TestFrameworkXOF { // Put any config options here - /// Can be disabled for XOFs that don't implement [`XOF::absorb_last_partial_byte`]. + /// Can be disabled for XOFs that don't support a partial final byte of input. pub enable_partial_byte_tests: bool, } @@ -16,239 +16,199 @@ impl TestFrameworkXOF { Self { enable_partial_byte_tests: true } } - /// Test the absorb-after-squeeze members of trait XOF against the given input-output pair. - /// This is not exhaustive; it covers the rules laid out in the "State and Absorb-after-Squeeze" - /// section of the [`XOF`] docs: an XOF is an absorb phase followed by a squeeze phase, once - /// squeezing has begun any further absorb returns [`HashError::InvalidState`], and a rejected - /// absorb leaves the object usable for further squeezing. - /// `expected_output` is the result of squeezing `expected_output.len()` bytes after absorbing - /// `input`. + /// Exercises the trait against a known input-output pair. + /// + /// `expected_output` is the result of reading `expected_output.len()` bytes after absorbing + /// `input`. There is deliberately no absorb-after-squeeze test: [`XOF::into_output`] consumes + /// the XOF, so absorbing afterwards is not expressible and there is no runtime rule left to + /// check. That guarantee is asserted instead by `compile_fail` doctests on the implementors. pub fn test_xof(&self, input: &[u8], expected_output: &[u8]) { - /*** fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> ***/ - // Absorbing is fine, repeatedly, right up until the first squeeze. + /*** fn do_update(&mut self, data: &[u8]) ***/ + // Feeding the input in pieces must equal feeding it in one go. let mut xof = X::default(); for chunk in input.chunks(16) { - xof.absorb(chunk).expect("absorb() before any squeeze must succeed"); + xof.do_update(chunk); } + assert_eq!( + xof.into_output().do_output(expected_output.len()), + expected_output, + "chunked input must equal a single update" + ); - // "once the XOF has begun squeezing, attempting to absorb more will return - // HashError::InvalidState" - // squeeze() begins squeezing ... + /*** fn do_output(&mut self, num_bytes: usize) -> Vec ***/ let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(expected_output.len()); - assert!( - matches!(xof.absorb(b"more input"), Err(HashError::InvalidState(_))), - "absorb() after squeeze() must return InvalidState" + xof.do_update(input); + assert_eq!( + xof.into_output().do_output(expected_output.len()), + expected_output, + "do_output must produce the expected bytes" ); - // ... and so does squeeze_out() + /*** fn do_output_out(&mut self, output: &mut [u8]) -> usize ***/ + // Pre-filled so that the documented zeroization is observable. + let mut output = vec![0xFFu8; expected_output.len()]; let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let mut output = vec![0u8; expected_output.len()]; - xof.squeeze_out(&mut output); - assert!( - matches!(xof.absorb(b"more input"), Err(HashError::InvalidState(_))), - "absorb() after squeeze_out() must return InvalidState" - ); + xof.do_update(input); + let n = xof.into_output().do_output_out(&mut output); + assert_eq!(n, expected_output.len(), "do_output_out must report what it wrote"); + assert_eq!(output, expected_output, "do_output_out must agree with do_output"); - /*** fn squeeze(&mut self, num_bytes: usize) -> Vec ***/ - /*** fn squeeze_out(&mut self, output: &mut [u8]) -> usize ***/ - // "... and leave the object usable for further squeezing" - // So squeezing the output in two halves around a rejected absorb must give exactly the same - // stream as one clean squeeze: a rejected absorb must not consume, pad, or otherwise - // disturb the sponge. + // One output stream: reading it in two goes equals reading it in one. let split = expected_output.len() / 2; - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let first_half = xof.squeeze(split); - assert!(xof.absorb(b"more input").is_err()); - let mut second_half = vec![0u8; expected_output.len() - split]; - xof.squeeze_out(&mut second_half); - + xof.do_update(input); + let mut out = xof.into_output(); + let first = out.do_output(split); + let mut second = vec![0u8; expected_output.len() - split]; + out.do_output_out(&mut second); assert_eq!( - first_half.as_slice(), - &expected_output[..split], - "Incorrect output for input / the output stream must be unchanged by a rejected absorb" + [first, second].concat(), + expected_output, + "successive reads must continue one stream" ); + + /*** fn hash_xof(self, data: &[u8], result_len: usize) -> Vec ***/ assert_eq!( - second_half.as_slice(), - &expected_output[split..], - "Incorrect output for input / the output stream must continue as if the rejected absorb never happened" + X::default().hash_xof(input, expected_output.len()), + expected_output, + "the one-shot must equal update-then-output" ); + let mut output = vec![0xFFu8; expected_output.len()]; + let n = X::default().hash_xof_out(input, &mut output); + assert_eq!(n, expected_output.len()); + assert_eq!(output, expected_output, "hash_xof_out must agree with hash_xof"); + + /*** the Hash half: a XOF is a hash ***/ + self.test_xof_as_hash::(input, expected_output); + if self.enable_partial_byte_tests { - /*** fn absorb_last_partial_byte(&mut self, partial_byte: u8, num_bits: usize) -> Result<(), HashError> ***/ - // The same phase rule applies to absorb_last_partial_byte() once squeezing has begun. + self.test_xof_partial_bits::(input, expected_output); + } + } + + /// The inherited [`Hash`] surface. `XOF: Hash`, so SHAKE can be used wherever a hash is wanted; + /// these checks pin that the inherited methods agree with the XOF ones. + fn test_xof_as_hash(&self, input: &[u8], expected_output: &[u8]) { + let xof = X::default(); + let output_len = xof.output_len(); + assert!(output_len > 0, "output_len must be positive"); + assert!(xof.block_bitlen() > 0, "block_bitlen must be positive"); + assert!( + xof.block_bitlen().is_multiple_of(8), + "block_bitlen must be a whole number of bytes" + ); + + // do_final is do_output at the nominal length: the same stream, truncated. + let mut a = X::default(); + a.do_update(input); + let via_hash = a.do_final(); + assert_eq!(via_hash.len(), output_len, "do_final must produce output_len bytes"); + + let mut b = X::default(); + b.do_update(input); + assert_eq!( + via_hash, + b.into_output().do_output(output_len), + "do_final must equal do_output(output_len)" + ); + + // ... and it is a prefix of the longer output, because a XOF cannot diversify by length. + if expected_output.len() >= output_len { + assert_eq!( + &via_hash[..], + &expected_output[..output_len], + "do_final must be a prefix of the longer output" + ); + } + + // do_final_out fills the caller's buffer, zeroizing it first. + let mut buf = vec![0xFFu8; output_len]; + let mut c = X::default(); + c.do_update(input); + let n = c.do_final_out(&mut buf); + assert_eq!(n, output_len); + assert_eq!(buf, via_hash, "do_final_out must agree with do_final"); + + // The one-shot Hash entry points. + assert_eq!(X::default().hash(input), via_hash, "hash must equal update-then-do_final"); + let mut buf = vec![0xFFu8; output_len]; + assert_eq!(X::default().hash_out(input, &mut buf), output_len); + assert_eq!(buf, via_hash, "hash_out must agree with hash"); + } + + /// A partial final byte of input, in both the XOF and the Hash spelling. + fn test_xof_partial_bits(&self, input: &[u8], expected_output: &[u8]) { + // num_bits = 0 means the message ended on a byte boundary, so it must match plain input. + let mut xof = X::default(); + xof.do_update(input); + assert_eq!( + xof.into_output_partial_bits(0, 0) + .expect("0 is in range") + .do_output(expected_output.len()), + expected_output, + "num_bits = 0 must equal a byte-aligned message" + ); + + // A real partial byte must change the output, and both spellings must agree. + for num_bits in 1..=7usize { + let mut a = X::default(); + a.do_update(input); + let with_bits = a + .into_output_partial_bits(0xFE, num_bits) + .expect("num_bits is in 1..=7") + .do_output(expected_output.len()); + assert_ne!( + with_bits, expected_output, + "a partial byte must change the output / num_bits: {num_bits}" + ); + + let mut b = X::default(); + b.do_update(input); + let via_hash = b.do_final_partial_bits(0xFE, num_bits).expect("num_bits is in 1..=7"); + assert_eq!( + via_hash, + with_bits[..via_hash.len()], + "do_final_partial_bits must be the same stream / num_bits: {num_bits}" + ); + + let mut buf = vec![0xFFu8; via_hash.len()]; + let mut c = X::default(); + c.do_update(input); + let n = c + .do_final_partial_bits_out(0xFE, num_bits, &mut buf) + .expect("num_bits is in 1..=7"); + assert_eq!(n, via_hash.len()); + assert_eq!(buf, via_hash, "the _out form must agree / num_bits: {num_bits}"); + } + + // "num_bits must be in 0..=7; larger values return HashError::InvalidLength." + for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] { let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(expected_output.len()); + xof.do_update(input); assert!( - matches!(xof.absorb_last_partial_byte(0x01, 3), Err(HashError::InvalidState(_))), - "absorb_last_partial_byte() after squeeze() must return InvalidState" + matches!( + xof.into_output_partial_bits(0xFF, num_bits), + Err(HashError::InvalidLength(_)) + ), + "into_output_partial_bits must reject num_bits = {num_bits}" ); - // "Unlike XOF::absorb, this switches the XOF from Absorbing mode into Squeezing mode - // because absorbing more input after absorbing a partial byte is undefined - // behaviour." - // So it leaves the object in the same state a squeeze does, for every valid num_bits, - // with no squeeze having happened at all. - for num_bits in 0..=7 { - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - xof.absorb_last_partial_byte(0xFF, num_bits) - .expect("absorb_last_partial_byte() must succeed for num_bits in 0..=7"); - let expected_partial_output = xof.squeeze(expected_output.len()); - - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - xof.absorb_last_partial_byte(0xFF, num_bits) - .expect("absorb_last_partial_byte() must succeed for num_bits in 0..=7"); - - assert!( - matches!(xof.absorb(b"more input"), Err(HashError::InvalidState(_))), - "absorb() after absorb_last_partial_byte() must return InvalidState / num_bits: {num_bits}" - ); - assert!( - matches!( - xof.absorb_last_partial_byte(0xFF, num_bits), - Err(HashError::InvalidState(_)) - ), - "a second absorb_last_partial_byte() must return InvalidState / num_bits: {num_bits}" - ); - - // ... and, again, the rejections must leave the object usable for further squeezing. - assert_eq!( - xof.squeeze(expected_output.len()), - expected_partial_output, - "the output stream must be unchanged by a rejected absorb / num_bits: {num_bits}" - ); - } - - // Helper: the output stream of `input` finished with the top `num_bits` bits of - // `partial_byte`. - let partial_absorb_output = |partial_byte: u8, num_bits: usize| -> Vec { - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - xof.absorb_last_partial_byte(partial_byte, num_bits) - .expect("absorb_last_partial_byte() must succeed for num_bits in 0..=7"); - xof.squeeze(expected_output.len()) - }; - - // "0 is a valid value and means the message ends on a byte boundary (equivalent to - // XOF::absorb)." - // So the message is still just `input`, whatever the discarded bits of partial_byte are. - for partial_byte in [0x00u8, 0x01, 0x80, 0xA5, 0xFF] { - assert_eq!( - partial_absorb_output(partial_byte, 0), - expected_output, - "num_bits = 0 must leave the message byte-aligned / partial_byte: {partial_byte:#04X}" - ); - } - - // "the num_bits message bits are the most significant bits of partial_byte ... and the - // low 8 - num_bits bits (the BIT STRING's "unused bits") are ignored". - // So the unused low bits are not part of the message and must not change the output. - for num_bits in 0..=7 { - // the used bits are the top num_bits; built in u16 so that num_bits == 0 cannot overflow - let mask = (0xFF00u16 >> num_bits) as u8; - for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] { - assert_eq!( - partial_absorb_output(partial_byte, num_bits), - partial_absorb_output(partial_byte & mask, num_bits), - "the low 8 - num_bits = {} bits must be ignored / partial_byte: {partial_byte:#04X}", - 8 - num_bits - ); - } - } - - // "num_bits must be in 0..=7; larger values return HashError::InvalidLength." - // Checked on an absorbing object, so that it is the range check rejecting the call and - // not the phase check above. - for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] { - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - assert!( - matches!( - xof.absorb_last_partial_byte(0xFF, num_bits), - Err(HashError::InvalidLength(_)) - ), - "absorb_last_partial_byte() must reject num_bits = {num_bits} with InvalidLength" - ); - } - - /*** fn squeeze_partial_byte_final(self, num_bits: usize) -> Result ***/ - /*** fn squeeze_partial_byte_final_out(self, num_bits: usize, output: &mut u8) -> Result<(), HashError> ***/ - // "in the most significant num_bits bits of the returned u8, first output bit first, with - // the low 8 - num_bits "unused" bits zero." - // They are the first bits of the next byte of the output stream, which `expected_output` - // gives us: after squeezing `split` bytes, the next byte is expected_output[split]. In - // that byte the first output bit is the LSB (FIPS 202 B.1 / the byte-oriented stream), so - // the expected partial byte is the bit-reversal of it, masked to the top num_bits bits. - let split = expected_output.len() / 2; - for num_bits in 0..=7 { - // the used bits are the top num_bits; built in u16 so that num_bits == 0 cannot overflow - let mask = (0xFF00u16 >> num_bits) as u8; - - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(split); - let partial_byte = xof - .squeeze_partial_byte_final(num_bits) - .expect("squeeze_partial_byte_final() must succeed for num_bits in 0..=7"); - - assert_eq!( - partial_byte, - expected_output[split].reverse_bits() & mask, - "the squeezed bits must be the first bits of the next output byte, MSB-first / num_bits: {num_bits}" - ); - assert_eq!( - partial_byte & !mask, - 0x00, - "the unused low bits of the result must be zero / num_bits: {num_bits}" - ); - - // "The same as XOF::squeeze_partial_byte_final, but writes into the provided output - // byte. The output byte is zeroized before the result is written." - // Pre-filled with 0xFF so that the zeroization is observable. - let mut output_byte = 0xFFu8; - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(split); - xof.squeeze_partial_byte_final_out(num_bits, &mut output_byte) - .expect("squeeze_partial_byte_final_out() must succeed for num_bits in 0..=7"); - assert_eq!( - output_byte, partial_byte, - "squeeze_partial_byte_final_out() must agree with squeeze_partial_byte_final() / num_bits: {num_bits}" - ); - } - - // "num_bits must be in 0..=7; larger values return HashError::InvalidLength." - for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] { - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(split); - assert!( - matches!( - xof.squeeze_partial_byte_final(num_bits), - Err(HashError::InvalidLength(_)) - ), - "squeeze_partial_byte_final() must reject num_bits = {num_bits} with InvalidLength" - ); - - let mut output_byte = 0u8; - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(split); - assert!( - matches!( - xof.squeeze_partial_byte_final_out(num_bits, &mut output_byte), - Err(HashError::InvalidLength(_)) - ), - "squeeze_partial_byte_final_out() must reject num_bits = {num_bits} with InvalidLength" - ); - } + let mut xof = X::default(); + xof.do_update(input); + assert!( + matches!( + xof.do_final_partial_bits(0xFF, num_bits), + Err(HashError::InvalidLength(_)) + ), + "do_final_partial_bits must reject num_bits = {num_bits}" + ); } } } + +impl Default for TestFrameworkXOF { + fn default() -> Self { + Self::new() + } +} diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 7ad51967..f8f4f19c 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -1743,91 +1743,78 @@ where } } -/// Extensible Output Functions (XOFs) are similar to hash functions, except that they can produce output of arbitrary length. -/// The naming used for the functions of this trait are borrowed from the SHA3-style sponge constructions that split XOF operation -/// into two phases: an absorb phase in which an arbitrary amount of input is provided to the XOF, -/// and then a squeeze phase in which an arbitrary amount of output is extracted. -/// Once squeezing begins, no more input can be absorbed. +/// The squeezing phase of an [`XOF`]: a value that produces output and can no longer take input. /// -/// XOFs are _similar to_ hash functions, but are not hash functions for one technical but important reason: -/// since the amount of output to produce is not provided to the XOF in advance, it cannot be used to -/// diversify the XOF output streams. -/// In other words, the overlapping parts of their outputs will be the same! -/// For example, consider two XOFs that absorb the same input data, one that is squeezed to produce 32 bytes, -/// and the other to produce 1 kb; both outputs will be identical in their first 32 bytes. -/// This could lead to loss of security in a number of ways, for example distinguishing attacks where -/// it is sufficient for the attacker to know that two values came from the same input, even if the -/// attacker cannot learn what that input was. This is attack is often sufficient, for example, -/// to break anonymity-preserving technology. -/// Applications that require the arbitrary-length output of an XOF, but also care about these -/// distinguishing attacks should consider adding a cryptographic salt to diversify the inputs. +/// This is the type [`XOF::into_output`] hands back. Absorbing and squeezing are separate types +/// rather than separate states of one type, so "no more input once output has begun" is a fact the +/// compiler enforces rather than a rule the documentation asks callers to follow. BC Java draws the +/// same line at run time, throwing `IllegalStateException` from `KeccakDigest.absorb`. /// -/// # State and Absorb-after-Squeeze -/// This trait makes the design choice that an XOF consists of an absorb phase followed by a squeeze phase. -/// This means that once the XOF has begun squeezing, attempting to absorb more will return -/// [`HashError::InvalidState`] and leave the object usable for further squeezing. +/// Output is one continuous stream: successive calls continue where the last left off, so reading +/// 16 bytes twice gives the same 32 bytes as reading 32 once. +pub trait XofOutput { + /// Produces the next `num_bytes` bytes of the output stream. + /// + /// BC Java's `Xof.doOutput(out, outOff, outLen)`. + fn do_output(&mut self, num_bytes: usize) -> Vec; + + /// As [`do_output`](Self::do_output), filling the caller's buffer, which is zeroized first. + /// Returns the number of bytes written. + fn do_output_out(&mut self, output: &mut [u8]) -> usize; +} + +/// Extendable-Output Functions (XOFs): hashes whose output length is chosen by the caller. /// -/// Without this restriction, the [`XOF::absorb_last_partial_byte`] API cannot function correctly. +/// `XOF: Hash`, so SHAKE128 and SHAKE256 *are* hashes and can be used wherever one is wanted. This +/// is the relationship BC Java draws with `Xof extends ExtendedDigest extends Digest`. As a hash, a +/// XOF has a nominal output length -- [`Hash::output_len`], which for SHAKE is +/// `fixedOutputLength / 4`, matching `SHAKEDigest.getDigestSize()` -- and [`Hash::do_final`] +/// produces exactly that many bytes. This trait adds the ability to ask for a different number. /// -/// If Absorb-after-Squeeze becomes necessary to support in the future, then these design choices can be revisited. -pub trait XOF: Default { - /// A static one-shot API that digests the input data and produces `result_len` bytes of output. - fn hash_xof(self, data: &[u8], result_len: usize) -> Vec; - - /// A static one-shot API that digests the input data and produces `result_len` bytes of output. - /// Fills the provided output slice. - /// The entire output buffer is zeroized before the output is written. - fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize; +/// # Absorb, then squeeze +/// +/// A sponge takes input, then produces output, and cannot go back. Here that is expressed in the +/// types: [`into_output`](Self::into_output) consumes the XOF and returns an [`XofOutput`], so +/// after output has begun there is no value left on which to call [`Hash::do_update`]. Nothing +/// returns an "absorbed after squeezing" error because nothing can reach that state. +/// +/// # A XOF is not a hash, cryptographically +/// +/// It satisfies the trait, but the output length is not an input to the computation, so it cannot +/// diversify the output. Two XOFs given the same input, one read for 32 bytes and one for 1 KiB, +/// agree on their first 32 bytes. An attacker who only needs to know that two values came from the +/// same input -- enough to break an anonymity property -- learns it from the overlap. Where that +/// matters, salt the input. +pub trait XOF: Hash { + /// The squeezing state this XOF turns into. + type Output: XofOutput; - /// Absorb some amount of input. - fn absorb(&mut self, data: &[u8]) -> Result<(), HashError>; + /// Ends the input phase and begins producing output. + /// + /// BC Java's `Xof.doOutput` in effect, but the phase change is in the type: what comes back + /// takes no more input. + fn into_output(self) -> Self::Output; - /// The same as [`XOF::absorb`], but allows for supplying a partial byte as the last input. - /// The partial byte is taken as it arrives in the final octet of an ASN.1 BIT STRING - /// (X.690 s. 8.6.2.1): the `num_bits` message bits are the most significant bits of - /// `partial_byte`, leading bit first, and the low `8 - num_bits` bits (the BIT STRING's "unused - /// bits") are ignored. This is the same convention as [`Hash::do_final_partial_bits`]; see there - /// for the relationship to the FIPS 202 Appendix B.1 bit order and to the NIST test vector files. - /// 0 is a valid value and means the message ends on a byte boundary (equivalent to [`XOF::absorb`]). - /// `num_bits` must be in `0..=7`; larger values return [`HashError::InvalidLength`]. + /// As [`into_output`](Self::into_output), with a final partial **byte** of input. /// - /// Unlike [`XOF::absorb`], this switches the XOF from Absorbing mode into Squeezing mode because - /// absorbing more input after absorbing a partial byte is undefined behaviour. - fn absorb_last_partial_byte( - &mut self, + /// The partial byte arrives as the final octet of an ASN.1 BIT STRING (X.690 s. 8.6.2.1): the + /// `num_bits` message bits are the most significant bits of `partial_byte`, leading bit first, + /// and the low `8 - num_bits` "unused" bits are ignored. Same convention as + /// [`Hash::do_final_partial_bits`]. `num_bits` of 0 means the message ended on a byte boundary + /// and is equivalent to [`into_output`](Self::into_output). + /// + /// # Errors + /// [`HashError::InvalidLength`] if `num_bits` is not in `0..=7`. + fn into_output_partial_bits( + self, partial_byte: u8, num_bits: usize, - ) -> Result<(), HashError>; - - /// Can be called multiple times. - fn squeeze(&mut self, num_bytes: usize) -> Vec; - - /// Can be called multiple times. - /// Fills the provided output slice. - /// The entire output buffer is zeroized before the output is written. - fn squeeze_out(&mut self, output: &mut [u8]) -> usize; - - /// Squeezes a partial byte (`num_bits` in `0..=7`) from the XOF. - /// The bits are returned as they would be placed in the final octet of an ASN.1 BIT STRING - /// (X.690 s. 8.6.2.1): in the most significant `num_bits` bits of the returned u8, first output - /// bit first, with the low `8 - num_bits` "unused" bits zero. This matches the input convention of - /// [`XOF::absorb_last_partial_byte`]. (FIPS 202 Appendix B.1 orders the bits of an output byte - /// LSB-first; the implementation converts.) - /// 0 is a valid value and requests no bits, so the result is `0x00`. - /// `num_bits` must be in `0..=7`; larger values return [`HashError::InvalidLength`]. - /// This is a final call and consumes self. - fn squeeze_partial_byte_final(self, num_bits: usize) -> Result; + ) -> Result; - /// The same as [`XOF::squeeze_partial_byte_final`], but writes into the provided output byte. - /// The output byte is zeroized before the result is written. - fn squeeze_partial_byte_final_out( - self, - num_bits: usize, - output: &mut u8, - ) -> Result<(), HashError>; + /// One-shot: absorbs `data` and produces `result_len` bytes. + fn hash_xof(self, data: &[u8], result_len: usize) -> Vec; - /// Returns the maximum security strength that this KDF is capable of supporting, based on the underlying primitives. - // todo: we should do a refactor to make [Algorithm] be a `security_strength()` function instead of constant, - // then have `RNG: Algorithm`, then delete this function. - fn max_security_strength(&self) -> SecurityStrength; + /// One-shot: absorbs `data` and fills `output`, which is zeroized first. Returns the number of + /// bytes written. + fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize; } diff --git a/crypto/factory/src/xof_factory.rs b/crypto/factory/src/xof_factory.rs index c3d97473..cb36e2ca 100644 --- a/crypto/factory/src/xof_factory.rs +++ b/crypto/factory/src/xof_factory.rs @@ -5,7 +5,7 @@ //! //! Example usage: //! ``` -//! use bouncycastle_core::traits::XOF; +//! use bouncycastle_core::traits::{Hash, XOF, XofOutput}; //! use bouncycastle_factory::AlgorithmFactory; //! use bouncycastle_factory::xof_factory::XOFFactory; //! use bouncycastle_sha3 as sha3; @@ -13,9 +13,11 @@ //! let data: &[u8] = b"Hello, world!"; //! //! let mut h = XOFFactory::new(sha3::SHAKE128_NAME).unwrap(); -//! h.absorb(data); -//! let output: Vec = h.squeeze(16); +//! h.do_update(data); +//! let output: Vec = h.into_output().do_output(16); //! ``` +//! `XOFFactory` implements [`Hash`] too, so it can be used wherever a hash is wanted; `do_final` +//! then produces the nominal 32 or 64 bytes. //! Equivalently, it may be invoked by passing a string instead of using the constant: //! //! ``` @@ -35,7 +37,7 @@ use crate::{AlgorithmFactory, FactoryError}; use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{KDF, SecurityStrength, XOF}; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XofOutput}; use bouncycastle_sha3 as sha3; use bouncycastle_sha3::{SHAKE128_NAME, SHAKE256_NAME}; @@ -82,81 +84,161 @@ impl AlgorithmFactory for XOFFactory { } } } -impl XOF for XOFFactory { - fn hash_xof(self, data: &[u8], result_len: usize) -> Vec { +/// `Hash` requires it, and the factory does not know which algorithm it holds until it is +/// constructed, so the constants are placeholders -- the same stance `HashFactory` takes. The +/// per-value answers come from [`Hash::output_len`] and [`Hash::max_security_strength`], which +/// dispatch on the variant. +impl Algorithm for XOFFactory { + const ALG_NAME: &'static str = "TODO"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; +} + +/// The squeezing phase of whichever XOF the factory selected. +/// +/// [`XOF::into_output`] consumes the factory value, so this enum is what remains; like +/// [`XOFFactory`] itself it dispatches on the variant. +pub enum XOFFactoryOutput { + /// SHAKE128 output. + SHAKE128(::Output), + /// SHAKE256 output. + SHAKE256(::Output), +} + +impl XofOutput for XOFFactoryOutput { + fn do_output(&mut self, num_bytes: usize) -> Vec { match self { - Self::SHAKE128(h) => h.hash_xof(data, result_len), - Self::SHAKE256(h) => h.hash_xof(data, result_len), + Self::SHAKE128(o) => o.do_output(num_bytes), + Self::SHAKE256(o) => o.do_output(num_bytes), } } - fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { - output.fill(0); + fn do_output_out(&mut self, output: &mut [u8]) -> usize { + match self { + Self::SHAKE128(o) => o.do_output_out(output), + Self::SHAKE256(o) => o.do_output_out(output), + } + } +} +impl Hash for XOFFactory { + fn block_bitlen(&self) -> usize { match self { - Self::SHAKE128(h) => h.hash_xof_out(data, output), - Self::SHAKE256(h) => h.hash_xof_out(data, output), + Self::SHAKE128(h) => h.block_bitlen(), + Self::SHAKE256(h) => h.block_bitlen(), } } - fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> { + fn output_len(&self) -> usize { match self { - Self::SHAKE128(h) => h.absorb(data), - Self::SHAKE256(h) => h.absorb(data), + Self::SHAKE128(h) => h.output_len(), + Self::SHAKE256(h) => h.output_len(), } } - fn absorb_last_partial_byte( - &mut self, - partial_byte: u8, - num_partial_bits: usize, - ) -> Result<(), HashError> { + fn hash(self, data: &[u8]) -> Vec { match self { - Self::SHAKE128(h) => h.absorb_last_partial_byte(partial_byte, num_partial_bits), - Self::SHAKE256(h) => h.absorb_last_partial_byte(partial_byte, num_partial_bits), + Self::SHAKE128(h) => h.hash(data), + Self::SHAKE256(h) => h.hash(data), } } - fn squeeze(&mut self, num_bytes: usize) -> Vec { + fn hash_out(self, data: &[u8], output: &mut [u8]) -> usize { match self { - Self::SHAKE128(h) => h.squeeze(num_bytes), - Self::SHAKE256(h) => h.squeeze(num_bytes), + Self::SHAKE128(h) => h.hash_out(data, output), + Self::SHAKE256(h) => h.hash_out(data, output), } } - fn squeeze_out(&mut self, output: &mut [u8]) -> usize { - output.fill(0); + fn do_update(&mut self, data: &[u8]) { + match self { + Self::SHAKE128(h) => h.do_update(data), + Self::SHAKE256(h) => h.do_update(data), + } + } + fn do_final(self) -> Vec { match self { - Self::SHAKE128(h) => h.squeeze_out(output), - Self::SHAKE256(h) => h.squeeze_out(output), + Self::SHAKE128(h) => h.do_final(), + Self::SHAKE256(h) => h.do_final(), } } - fn squeeze_partial_byte_final(self, num_bits: usize) -> Result { + fn do_final_out(self, output: &mut [u8]) -> usize { match self { - Self::SHAKE128(h) => h.squeeze_partial_byte_final(num_bits), - Self::SHAKE256(h) => h.squeeze_partial_byte_final(num_bits), + Self::SHAKE128(h) => h.do_final_out(output), + Self::SHAKE256(h) => h.do_final_out(output), } } - fn squeeze_partial_byte_final_out( + fn do_final_partial_bits( self, + partial_byte: u8, num_bits: usize, - output: &mut u8, - ) -> Result<(), HashError> { - *output = 0; + ) -> Result, HashError> { + match self { + Self::SHAKE128(h) => h.do_final_partial_bits(partial_byte, num_bits), + Self::SHAKE256(h) => h.do_final_partial_bits(partial_byte, num_bits), + } + } + fn do_final_partial_bits_out( + self, + partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { match self { - Self::SHAKE128(h) => h.squeeze_partial_byte_final_out(num_bits, output), - Self::SHAKE256(h) => h.squeeze_partial_byte_final_out(num_bits, output), + Self::SHAKE128(h) => h.do_final_partial_bits_out(partial_byte, num_bits, output), + Self::SHAKE256(h) => h.do_final_partial_bits_out(partial_byte, num_bits, output), } } fn max_security_strength(&self) -> SecurityStrength { match self { - Self::SHAKE128(h) => KDF::max_security_strength(h), - Self::SHAKE256(h) => XOF::max_security_strength(h), + Self::SHAKE128(h) => Hash::max_security_strength(h), + Self::SHAKE256(h) => Hash::max_security_strength(h), + } + } +} + +impl XOF for XOFFactory { + type Output = XOFFactoryOutput; + + fn into_output(self) -> Self::Output { + match self { + Self::SHAKE128(h) => XOFFactoryOutput::SHAKE128(h.into_output()), + Self::SHAKE256(h) => XOFFactoryOutput::SHAKE256(h.into_output()), + } + } + + fn into_output_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result { + Ok(match self { + Self::SHAKE128(h) => { + XOFFactoryOutput::SHAKE128(h.into_output_partial_bits(partial_byte, num_bits)?) + } + Self::SHAKE256(h) => { + XOFFactoryOutput::SHAKE256(h.into_output_partial_bits(partial_byte, num_bits)?) + } + }) + } + + fn hash_xof(self, data: &[u8], result_len: usize) -> Vec { + match self { + Self::SHAKE128(h) => h.hash_xof(data, result_len), + Self::SHAKE256(h) => h.hash_xof(data, result_len), + } + } + + fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { + output.fill(0); + + match self { + Self::SHAKE128(h) => h.hash_xof_out(data, output), + Self::SHAKE256(h) => h.hash_xof_out(data, output), } } } diff --git a/crypto/mldsa-lowmemory/src/aux_functions.rs b/crypto/mldsa-lowmemory/src/aux_functions.rs index 5eaf55f3..488045b5 100644 --- a/crypto/mldsa-lowmemory/src/aux_functions.rs +++ b/crypto/mldsa-lowmemory/src/aux_functions.rs @@ -7,7 +7,7 @@ use crate::params::{ MLDSAParams, }; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{Hash, XOF, XofOutput}; use bouncycastle_utils::secret::ZeroizablePrimitive; /// Algorithm 14 CoeffFromThreeBytes(𝑏0, 𝑏1, 𝑏2) @@ -433,9 +433,10 @@ pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { // 3: ctx ← H.Absorb(ctx, 𝜌) // 4: (ctx, 𝑠) ← H.Squeeze(ctx, 8) let mut h = H::new(); - h.absorb(rho.as_ref()).expect("absorb before squeeze is infallible"); + h.do_update(rho.as_ref()); let mut s = [0u8; 8]; - h.squeeze_out(&mut s); + let mut h = h.into_output(); + h.do_output_out(&mut s); // 5: β„Ž ← BytesToBits(𝑠) // β–· β„Ž is a bit string of length 64 @@ -453,13 +454,13 @@ pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { // 7: (ctx, 𝑗) ← H.Squeeze(ctx, 1) // Note: At first, it might seem to be faster to pre-squeeze a buffer outside the loop. // However, after experimentation and testing, the difference is not noticeable. - h.squeeze_out(&mut j); + h.do_output_out(&mut j); // 8: while 𝑗 > 𝑖 do while j[0] as usize > i { // β–· rejection sampling in {0, … , 𝑖} // 9: (ctx, 𝑗) ← H.Squeeze(ctx, 1) - h.squeeze_out(&mut j); + h.do_output_out(&mut j); } // 11: 𝑐𝑖 ← 𝑐𝑗 @@ -496,8 +497,8 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { let mut w_hat = Polynomial::new(); let mut j: usize = 0; let mut g = G::new(); - g.absorb(rho).expect("absorb before squeeze is infallible"); - g.absorb(nonce).expect("absorb before squeeze is infallible"); + g.do_update(rho); + g.do_update(nonce); // SHAKE is fairly inefficient if only 3 bytes are squeezed at a time, so the implementation does a block instead. // size is not a limitation, so long as it's a multiple of 3. @@ -505,12 +506,13 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // It's probably around the average rejection rate, and 288 is a multiple of both 3 (required for this alg) // and 8 (efficient for SHAKE). let mut s = [0u8; 288]; - g.squeeze_out(&mut s); + let mut g = g.into_output(); + g.do_output_out(&mut s); let mut idx: usize = 0; while j < N { if idx == s.len() { - g.squeeze_out(&mut s); + g.do_output_out(&mut s); idx = 0; } w_hat[j] = match coeff_from_three_bytes(&s[idx..idx + 3].try_into().unwrap()) { @@ -541,8 +543,8 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) let mut a = Polynomial::new(); let mut j: usize = 0; let mut h = H::new(); - h.absorb(rho).expect("absorb before squeeze is infallible"); - h.absorb(nonce).expect("absorb before squeeze is infallible"); + h.do_update(rho); + h.do_update(nonce); // SHAKE is fairly inefficient if only 3 bytes are squeezed at a time, so the implementation does a block instead. // size is not a limitation as long as it is a multiple of 3. @@ -550,7 +552,8 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) // which is possibly also related with the average rejection rate. // Also, 312 is a multiple of 8 (efficient for SHAKE) let mut z_arr = [0u8; 312]; - h.squeeze_out(&mut z_arr); + let mut h = h.into_output(); + h.do_output_out(&mut z_arr); let mut idx: usize = 0; while j < N { @@ -568,7 +571,7 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) idx += 1; if idx == z_arr.len() { - h.squeeze_out(&mut z_arr); + h.do_output_out(&mut z_arr); idx = 0; } } @@ -588,10 +591,11 @@ pub(crate) fn expand_mask_poly(rho: &[u8; 64], nonce: u16) -> Po // The 32𝑐 bytes squeezed on line 4 are exactly `P::POLY_Z_PACKED_LEN`, so the buffer for them // is `P::PolyZPacked`; see the docs on `MLDSAParams::POLY_Z_PACKED_LEN`. let mut h = H::new(); - h.absorb(rho).expect("absorb before squeeze is infallible"); - h.absorb(&nonce.to_le_bytes()).expect("absorb before squeeze is infallible"); + h.do_update(rho); + h.do_update(&nonce.to_le_bytes()); let mut v = ::ZEROED; - h.squeeze_out(v.as_mut()); + let mut h = h.into_output(); + h.do_output_out(v.as_mut()); bit_unpack_gamma1::

(v.as_ref()) } diff --git a/crypto/mldsa-lowmemory/src/hash_mldsa.rs b/crypto/mldsa-lowmemory/src/hash_mldsa.rs index 9b8599d7..0a0ac0b6 100644 --- a/crypto/mldsa-lowmemory/src/hash_mldsa.rs +++ b/crypto/mldsa-lowmemory/src/hash_mldsa.rs @@ -83,7 +83,7 @@ use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, PHSignatureVerifier, PHSigner, RNG, SecurityStrength, - SignatureVerifier, Signer, XOF, + SignatureVerifier, Signer, XOF, XofOutput, }; use bouncycastle_rng::HashDRBG_SHA512; use core::marker::PhantomData; @@ -342,19 +342,19 @@ impl< // Algorithm 7 // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀', 64) let mut h = H::new(); - h.absorb(&sk.tr()).expect("absorb before squeeze is infallible"); + h.do_update(&sk.tr()); // Algorithm 4 // 23: 𝑀' ← BytesToBits(IntegerToBytes(1, 1) βˆ₯ IntegerToBytes(|𝑐𝑑π‘₯|, 1) βˆ₯ 𝑐𝑑π‘₯ βˆ₯ OID βˆ₯ PH𝑀) // all done together - h.absorb(&[1u8]).expect("absorb before squeeze is infallible"); - h.absorb(&[ctx.len() as u8]).expect("absorb before squeeze is infallible"); - h.absorb(ctx).expect("absorb before squeeze is infallible"); - h.absorb(::OID_DER) - .expect("absorb before squeeze is infallible"); - h.absorb(ph).expect("absorb before squeeze is infallible"); + h.do_update(&[1u8]); + h.do_update(&[ctx.len() as u8]); + h.do_update(ctx); + h.do_update(::OID_DER); + h.do_update(ph); let mut mu = [0u8; MLDSA_MU_LEN]; - let bytes_written = h.squeeze_out(&mut mu); + let mut h = h.into_output(); + let bytes_written = h.do_output_out(&mut mu); debug_assert_eq!(bytes_written, MLDSA_MU_LEN); // 24: 𝜎 ← ML-DSA.Sign_internal(π‘ π‘˜, 𝑀', π‘Ÿπ‘›π‘‘) @@ -631,19 +631,19 @@ impl< // Algorithm 7 // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀', 64) let mut h = H::new(); - h.absorb(&pk.compute_tr()).expect("absorb before squeeze is infallible"); + h.do_update(&pk.compute_tr()); // Algorithm 4 // 23: 𝑀 ← BytesToBits(IntegerToBytes(1, 1) βˆ₯ IntegerToBytes(|𝑐𝑑π‘₯|, 1) βˆ₯ 𝑐𝑑π‘₯ βˆ₯ OID βˆ₯ PH𝑀) // all done together - h.absorb(&[1u8]).expect("absorb before squeeze is infallible"); - h.absorb(&[ctx.len() as u8]).expect("absorb before squeeze is infallible"); - h.absorb(ctx).expect("absorb before squeeze is infallible"); - h.absorb(::OID_DER) - .expect("absorb before squeeze is infallible"); - h.absorb(ph).expect("absorb before squeeze is infallible"); + h.do_update(&[1u8]); + h.do_update(&[ctx.len() as u8]); + h.do_update(ctx); + h.do_update(::OID_DER); + h.do_update(ph); let mut mu = [0u8; MLDSA_MU_LEN]; - _ = h.squeeze_out(&mut mu); + let mut h = h.into_output(); + _ = h.do_output_out(&mut mu); MLDSA::::verify_mu( pk, &mu, sig_sized, diff --git a/crypto/mldsa-lowmemory/src/mldsa.rs b/crypto/mldsa-lowmemory/src/mldsa.rs index b1658579..fa2c4b51 100644 --- a/crypto/mldsa-lowmemory/src/mldsa.rs +++ b/crypto/mldsa-lowmemory/src/mldsa.rs @@ -399,7 +399,8 @@ use crate::{ use bouncycastle_core::errors::{RNGError, SignatureError, SuspendableError}; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{ - Algorithm, AlgorithmOID, RNG, SecurityStrength, SignatureVerifier, Signer, Suspendable, XOF, + Algorithm, AlgorithmOID, Hash, RNG, SecurityStrength, SignatureVerifier, Signer, Suspendable, + XOF, XofOutput, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN}; @@ -787,11 +788,12 @@ impl< // Alg 7; 7: πœŒβ€³ ← H(𝐾||π‘Ÿπ‘›π‘‘||πœ‡, 64) let rho_p_p: [u8; 64] = { let mut h = H::new(); - h.absorb(sk.K()).expect("absorb before squeeze is infallible"); - h.absorb(&rnd).expect("absorb before squeeze is infallible"); - h.absorb(mu).expect("absorb before squeeze is infallible"); + h.do_update(sk.K()); + h.do_update(&rnd); + h.do_update(mu); let mut rho_p_p = [0u8; 64]; - h.squeeze_out(&mut rho_p_p); + let mut h = h.into_output(); + h.do_output_out(&mut rho_p_p); rho_p_p }; @@ -817,15 +819,15 @@ impl< let sig_val_c_tilde = { // scope for hash let mut hash = H::new(); - hash.absorb(mu).expect("absorb before squeeze is infallible"); + hash.do_update(mu); for row in 0..P::k { let mut w = compute_w_row::

(&sk.rho(), &rho_p_p, kappa, row); w.high_bits::

(); - hash.absorb(w.w1_encode::

().as_ref()) - .expect("absorb before squeeze is infallible"); + hash.do_update(w.w1_encode::

().as_ref()); } let mut sig_val_c_tilde = ::ZEROED; - hash.squeeze_out(sig_val_c_tilde.as_mut()); + let mut hash = hash.into_output(); + hash.do_output_out(sig_val_c_tilde.as_mut()); sig_val_c_tilde }; // 16: 𝑐 ∈ π‘…π‘ž ← SampleInBall(c_tilde) @@ -1013,7 +1015,7 @@ impl< // 12: 𝑐_tilde_p ← H(πœ‡||w1Encode(𝐰1'), πœ†/4) // β–· hash it; this should match 𝑐_tilde let mut hash = H::new(); - hash.absorb(mu).expect("absorb before squeeze is infallible"); + hash.do_update(mu); for row in 0..P::k { let mut wp_approx = match { @@ -1034,12 +1036,12 @@ impl< // 10: 𝐰1β€² ← UseHint(𝐑, 𝐰'_approx) // β–· reconstruction of signer’s commitment wp_approx.use_hint::

(&h_i); - hash.absorb(wp_approx.w1_encode::

().as_ref()) - .expect("absorb before squeeze is infallible"); + hash.do_update(wp_approx.w1_encode::

().as_ref()); } let mut c_tilde_p = ::ZEROED; - hash.squeeze_out(c_tilde_p.as_mut()); + let mut hash = hash.into_output(); + hash.do_output_out(c_tilde_p.as_mut()); // Verification is also done in constant time // 13 (second half): return [[ ||𝐳||∞ < 𝛾1 βˆ’ 𝛽]] and [[𝑐 Μƒ = 𝑐′ ]] @@ -1446,14 +1448,14 @@ impl MuBuilder { // Algorithm 7 // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀', 64) let mut mb = Self { h: H::new() }; - mb.h.absorb(tr).expect("absorb before squeeze is infallible"); + mb.h.do_update(tr); // Algorithm 2 // 10: 𝑀′ ← BytesToBits(IntegerToBytes(0, 1) βˆ₯ IntegerToBytes(|𝑐𝑑π‘₯|, 1) βˆ₯ 𝑐𝑑π‘₯) βˆ₯ 𝑀 // all done together - mb.h.absorb(&[0u8]).expect("absorb before squeeze is infallible"); - mb.h.absorb(&[ctx.len() as u8]).expect("absorb before squeeze is infallible"); - mb.h.absorb(ctx).expect("absorb before squeeze is infallible"); + mb.h.do_update(&[0u8]); + mb.h.do_update(&[ctx.len() as u8]); + mb.h.do_update(ctx); // now ready to absorb M Ok(mb) @@ -1461,16 +1463,16 @@ impl MuBuilder { /// Stream a chunk of the message. pub fn do_update(&mut self, msg_chunk: &[u8]) { - self.h.absorb(msg_chunk).expect("absorb before squeeze is infallible"); + self.h.do_update(msg_chunk); } /// Finalize and return the mu value. - pub fn do_final(mut self) -> [u8; 64] { + pub fn do_final(self) -> [u8; 64] { // Completion of // Algorithm 7 // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀 β€², 64) let mut mu = [0u8; 64]; - self.h.squeeze_out(&mut mu); + self.h.into_output().do_output_out(&mut mu); mu } diff --git a/crypto/mldsa-lowmemory/src/mldsa_keys.rs b/crypto/mldsa-lowmemory/src/mldsa_keys.rs index 76477c63..9aebec2d 100644 --- a/crypto/mldsa-lowmemory/src/mldsa_keys.rs +++ b/crypto/mldsa-lowmemory/src/mldsa_keys.rs @@ -11,7 +11,9 @@ use crate::params::{MLDSA44Params, MLDSA65Params, MLDSA87Params, MLDSAParams}; use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{SecurityStrength, SignaturePrivateKey, SignaturePublicKey, XOF}; +use bouncycastle_core::traits::{ + Hash, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, XOF, XofOutput, +}; use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; use core::fmt; use core::fmt::{Debug, Display, Formatter}; @@ -337,14 +339,15 @@ impl = Secret::new(); let mut h = H::default(); - h.absorb(seed.ref_to_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::l as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - let bytes_written = h.squeeze_out(&mut rho); + h.do_update(seed.ref_to_bytes()); + h.do_update(&(P::k as u8).to_le_bytes()); + h.do_update(&(P::l as u8).to_le_bytes()); + let mut h = h.into_output(); + let bytes_written = h.do_output_out(&mut rho); debug_assert_eq!(bytes_written, 32); - let bytes_written = h.squeeze_out(rho_prime.deref_mut()); + let bytes_written = h.do_output_out(rho_prime.deref_mut()); debug_assert_eq!(bytes_written, 64); - let bytes_written = h.squeeze_out(K.deref_mut()); + let bytes_written = h.do_output_out(K.deref_mut()); debug_assert_eq!(bytes_written, 32); (rho, rho_prime, K) diff --git a/crypto/mldsa-lowmemory/tests/bc_test_data.rs b/crypto/mldsa-lowmemory/tests/bc_test_data.rs index b2f71bdb..c5438be5 100644 --- a/crypto/mldsa-lowmemory/tests/bc_test_data.rs +++ b/crypto/mldsa-lowmemory/tests/bc_test_data.rs @@ -1,9 +1,9 @@ +use bouncycastle_core::traits::{Hash, XOF, XofOutput}; // Test against the bc-test-data repo // Requires that the bc-test-data repository is cloned and available for testing at "../bc-test-data" // relative to the root of this git project. use bouncycastle_core::errors::SignatureError; -use bouncycastle_core::traits::XOF; use bouncycastle_sha3::SHAKE256; #[allow(unused_imports)] @@ -19,7 +19,8 @@ mod bc_test_data { use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ - Hash, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, + Hash, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, XOF, + XofOutput, }; use bouncycastle_hex as hex; use bouncycastle_mldsa_lowmemory::{ @@ -964,14 +965,14 @@ impl BustedMuBuilder { // Algorithm 7 // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀', 64) let mut mb = Self { h: SHAKE256::new() }; - mb.h.absorb(tr).expect("absorb before squeeze is infallible"); + mb.h.do_update(tr); // Algorithm 2 // 10: 𝑀′ ← BytesToBits(IntegerToBytes(0, 1) βˆ₯ IntegerToBytes(|𝑐𝑑π‘₯|, 1) βˆ₯ 𝑐𝑑π‘₯) βˆ₯ 𝑀 // all done together - // mb.h.absorb(&[0u8]); // these are the busted lines -- bc-java just doesn't do these in the test code - // mb.h.absorb(&[ctx.len() as u8]); - // mb.h.absorb(ctx); + // mb.h.do_update(&[0u8]); // these are the busted lines -- bc-java just doesn't do these in the test code + // mb.h.do_update(&[ctx.len() as u8]); + // mb.h.do_update(ctx); // now ready to absorb M Ok(mb) @@ -979,16 +980,16 @@ impl BustedMuBuilder { /// Stream a chunk of the message. pub fn do_update(&mut self, msg_chunk: &[u8]) { - self.h.absorb(msg_chunk).expect("absorb before squeeze is infallible"); + self.h.do_update(msg_chunk); } /// Finalize and return the mu value. - pub fn do_final(mut self) -> [u8; 64] { + pub fn do_final(self) -> [u8; 64] { // Completion of // Algorithm 7 // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀 β€², 64) let mut mu = [0u8; 64]; - self.h.squeeze_out(&mut mu); + self.h.into_output().do_output_out(&mut mu); mu } diff --git a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs index 69832aa2..bc7c643f 100644 --- a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs @@ -6,8 +6,8 @@ mod mldsa_tests { use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ - RNG, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, Signer, - Suspendable, + Hash, RNG, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, + Signer, Suspendable, }; use bouncycastle_core_test_framework::DUMMY_SEED; use bouncycastle_core_test_framework::FixedSeedRNG; @@ -867,7 +867,6 @@ mod mldsa_tests { #[test] fn serializable_state_mubuilder_rejects_wrong_variant() { - use bouncycastle_core::traits::XOF; use bouncycastle_sha3::SHAKE128; // A MuBuilder is always backed by SHAKE256. A serialized SHAKE128 state has the same length @@ -875,9 +874,7 @@ mod mldsa_tests { // variant tag weren't checked -- SHAKE128 (tag 5) must be rejected by MuBuilder (SHAKE256, // tag 6). let mut shake128 = SHAKE128::new(); - shake128 - .absorb(b"Colorless green ideas sleep furiously") - .expect("absorb before squeeze is infallible"); + shake128.do_update(b"Colorless green ideas sleep furiously"); let serialized_128 = shake128.suspend(); match MuBuilder::from_suspended(serialized_128) { diff --git a/crypto/mldsa/src/aux_functions.rs b/crypto/mldsa/src/aux_functions.rs index bf0c2f91..b7dc7865 100644 --- a/crypto/mldsa/src/aux_functions.rs +++ b/crypto/mldsa/src/aux_functions.rs @@ -7,7 +7,7 @@ use crate::params::{ MLDSAParams, }; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{Hash, XOF, XofOutput}; use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; /// Algorithm 14 CoeffFromThreeBytes(𝑏0, 𝑏1, 𝑏2) @@ -500,9 +500,10 @@ pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { // 3: ctx ← H.Absorb(ctx, 𝜌) // 4: (ctx, 𝑠) ← H.Squeeze(ctx, 8) let mut h = H::new(); - h.absorb(rho.as_ref()).expect("absorb before squeeze is infallible"); + h.do_update(rho.as_ref()); let mut s = [0u8; 8]; - h.squeeze_out(&mut s); + let mut h = h.into_output(); + h.do_output_out(&mut s); // 5: β„Ž ← BytesToBits(𝑠) // β–· β„Ž is a bit string of length 64 @@ -521,13 +522,13 @@ pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { // Note: Even though it may appear that pre-squeezing a buffer outside the loop would be faster, // testing it both ways doesn't make a noticeable difference, so this has been left as is // for better correspondence with the FIPS sample algorithm. - h.squeeze_out(&mut j); + h.do_output_out(&mut j); // 8: while 𝑗 > 𝑖 do while j[0] as usize > i { // β–· rejection sampling in {0, … , 𝑖} // 9: (ctx, 𝑗) ← H.Squeeze(ctx, 1) - h.squeeze_out(&mut j); + h.do_output_out(&mut j); } // 11: 𝑐𝑖 ← 𝑐𝑗 @@ -564,8 +565,8 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { let mut w_hat = Polynomial::new(); let mut j: usize = 0; let mut g = G::new(); - g.absorb(rho).expect("absorb before squeeze is infallible"); - g.absorb(nonce).expect("absorb before squeeze is infallible"); + g.do_update(rho); + g.do_update(nonce); // SHAKE is fairly inefficient if only 3 bytes are squeezed at a time, so instead this implementation does a block. // Size is not a limitation, so long as it's a multiple of 3. @@ -573,12 +574,13 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // It's probably around the average rejection rate, and 288 is a multiple of both 3 (required for this alg) // and 8 (efficient for SHAKE). let mut s = [0u8; 288]; - g.squeeze_out(&mut s); + let mut g = g.into_output(); + g.do_output_out(&mut s); let mut idx: usize = 0; while j < N { if idx == s.len() { - g.squeeze_out(&mut s); + g.do_output_out(&mut s); idx = 0; } w_hat[j] = match coeff_from_three_bytes(&s[idx..idx + 3].try_into().unwrap()) { @@ -609,15 +611,16 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) let mut a = Polynomial::new(); let mut j: usize = 0; let mut h = H::new(); - h.absorb(rho).expect("absorb before squeeze is infallible"); - h.absorb(nonce).expect("absorb before squeeze is infallible"); + h.do_update(rho); + h.do_update(nonce); // size doesn't really matter // 312 seemed to be the sweet spot from playing with benchmarks // maybe something to do with the average rejection rate? // Also, 312 is a multiple of 8 (efficient for SHAKE) let mut z_arr = [0u8; 312]; - h.squeeze_out(&mut z_arr); + let mut h = h.into_output(); + h.do_output_out(&mut z_arr); let mut idx: usize = 0; while j < N { @@ -635,7 +638,7 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) idx += 1; if idx == z_arr.len() { - h.squeeze_out(&mut z_arr); + h.do_output_out(&mut z_arr); idx = 0; } } @@ -713,11 +716,11 @@ pub(crate) fn expand_mask(rho: &[u8; 64], mu: u16) -> P::VecL { // 4: 𝑣 ← H(πœŒβ€², 32𝑐) let v = { let mut h = H::new(); - h.absorb(rho).expect("absorb before squeeze is infallible"); - h.absorb(&(mu + (r as u16)).to_le_bytes()) - .expect("absorb before squeeze is infallible"); + h.do_update(rho); + h.do_update(&(mu + (r as u16)).to_le_bytes()); let mut v = ::ZEROED; - h.squeeze_out(v.as_mut()); + let mut h = h.into_output(); + h.do_output_out(v.as_mut()); v }; diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index 35747605..bd4f67b1 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -84,7 +84,7 @@ use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, PHSignatureVerifier, PHSigner, RNG, SecurityStrength, - SignatureVerifier, Signer, XOF, + SignatureVerifier, Signer, XOF, XofOutput, }; use bouncycastle_rng::HashDRBG_SHA512; use core::marker::PhantomData; @@ -384,19 +384,19 @@ impl< // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀', 64) let mu = { let mut h = H::new(); - h.absorb(sk.tr()).expect("absorb before squeeze is infallible"); + h.do_update(sk.tr()); // Algorithm 4 // 23: 𝑀' ← BytesToBits(IntegerToBytes(1, 1) βˆ₯ IntegerToBytes(|𝑐𝑑π‘₯|, 1) βˆ₯ 𝑐𝑑π‘₯ βˆ₯ OID βˆ₯ PH𝑀) // all done together - h.absorb(&[1u8]).expect("absorb before squeeze is infallible"); - h.absorb(&[ctx.len() as u8]).expect("absorb before squeeze is infallible"); - h.absorb(ctx).expect("absorb before squeeze is infallible"); - h.absorb(::OID_DER) - .expect("absorb before squeeze is infallible"); - h.absorb(ph).expect("absorb before squeeze is infallible"); + h.do_update(&[1u8]); + h.do_update(&[ctx.len() as u8]); + h.do_update(ctx); + h.do_update(::OID_DER); + h.do_update(ph); let mut mu = [0u8; MLDSA_MU_LEN]; - let bytes_written = h.squeeze_out(&mut mu); + let mut h = h.into_output(); + let bytes_written = h.do_output_out(&mut mu); debug_assert_eq!(bytes_written, MLDSA_MU_LEN); mu @@ -489,19 +489,19 @@ impl< // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀', 64) let mu = { let mut h = H::new(); - h.absorb(&pk.compute_tr()).expect("absorb before squeeze is infallible"); + h.do_update(&pk.compute_tr()); // Algorithm 4 // 23: 𝑀 ← BytesToBits(IntegerToBytes(1, 1) βˆ₯ IntegerToBytes(|𝑐𝑑π‘₯|, 1) βˆ₯ 𝑐𝑑π‘₯ βˆ₯ OID βˆ₯ PH𝑀) // all done together - h.absorb(&[1u8]).expect("absorb before squeeze is infallible"); - h.absorb(&[ctx.len() as u8]).expect("absorb before squeeze is infallible"); - h.absorb(ctx).expect("absorb before squeeze is infallible"); - h.absorb(::OID_DER) - .expect("absorb before squeeze is infallible"); - h.absorb(ph).expect("absorb before squeeze is infallible"); + h.do_update(&[1u8]); + h.do_update(&[ctx.len() as u8]); + h.do_update(ctx); + h.do_update(::OID_DER); + h.do_update(ph); let mut mu = [0u8; MLDSA_MU_LEN]; - _ = h.squeeze_out(&mut mu); + let mut h = h.into_output(); + _ = h.do_output_out(&mut mu); mu }; diff --git a/crypto/mldsa/src/matrix.rs b/crypto/mldsa/src/matrix.rs index e08bb62f..b3391332 100644 --- a/crypto/mldsa/src/matrix.rs +++ b/crypto/mldsa/src/matrix.rs @@ -5,7 +5,7 @@ use crate::aux_functions::multiply_ntt; use crate::mldsa::H; use crate::params::MLDSAParams; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::Hash; use bouncycastle_utils::secret::ZeroizablePrimitive; use core::ops::{Index, IndexMut}; @@ -302,7 +302,7 @@ impl VectorTrait for Vector { // 3: 𝐰̃1 ← 𝐰̃1 || SimpleBitPack (𝐰1[𝑖], (π‘ž βˆ’ 1)/(2𝛾2) βˆ’ 1) // 4: end for for w in self.elems.iter() { - h.absorb(w.w1_encode::

().as_ref()).expect("absorb before squeeze is infallible"); + h.do_update(w.w1_encode::

().as_ref()); } } } diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs index 9e003579..6533fb61 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -490,7 +490,8 @@ use crate::{ use bouncycastle_core::errors::{RNGError, SignatureError, SuspendableError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterial256, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ - Algorithm, AlgorithmOID, RNG, SecurityStrength, SignatureVerifier, Signer, Suspendable, XOF, + Algorithm, AlgorithmOID, Hash, RNG, SecurityStrength, SignatureVerifier, Signer, Suspendable, + XOF, XofOutput, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN}; @@ -690,15 +691,16 @@ impl< let (s1_hat, mut s2) = { // scope for h let mut h = H::default(); - h.absorb(seed.ref_to_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::l as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - let bytes_written = h.squeeze_out(&mut rho); + h.do_update(seed.ref_to_bytes()); + h.do_update(&(P::k as u8).to_le_bytes()); + h.do_update(&(P::l as u8).to_le_bytes()); + let mut h = h.into_output(); + 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.squeeze_out(&mut rho_prime); + let bytes_written = h.do_output_out(&mut rho_prime); debug_assert_eq!(bytes_written, 64); - let bytes_written = h.squeeze_out(&mut *K); + let bytes_written = h.do_output_out(&mut *K); debug_assert_eq!(bytes_written, 32); // 4: (𝐬1, 𝐬2) ← ExpandS(πœŒβ€²) @@ -784,11 +786,12 @@ impl< // scope for h // 7: πœŒβ€³ ← H(𝐾||π‘Ÿπ‘›π‘‘||πœ‡, 64) let mut h = H::new(); - h.absorb(&**sk.K()).expect("absorb before squeeze is infallible"); - h.absorb(&rnd).expect("absorb before squeeze is infallible"); - h.absorb(mu).expect("absorb before squeeze is infallible"); + h.do_update(&**sk.K()); + h.do_update(&rnd); + h.do_update(mu); let mut rho_p_p = [0u8; 64]; - h.squeeze_out(&mut rho_p_p); + let mut h = h.into_output(); + h.do_output_out(&mut rho_p_p); rho_p_p }; @@ -841,9 +844,10 @@ impl< // 15: 𝑐_tilde ← H(πœ‡||w1Encode(𝐰1), πœ†/4) // β–· commitment hash let mut hash = H::new(); - hash.absorb(mu).expect("absorb before squeeze is infallible"); + hash.do_update(mu); w1.w1_encode_and_hash::

(&mut hash); - hash.squeeze_out(sig_val_c_tilde.as_mut()); + let mut hash = hash.into_output(); + hash.do_output_out(sig_val_c_tilde.as_mut()); } // 16: 𝑐 ∈ π‘…π‘ž ← SampleInBall(c_tilde) @@ -1019,9 +1023,10 @@ impl< let c_tilde_p = { let mut c_tilde_p = ::ZEROED; let mut hash = H::new(); - hash.absorb(mu).expect("absorb before squeeze is infallible"); + hash.do_update(mu); w1p.w1_encode_and_hash::

(&mut hash); - hash.squeeze_out(c_tilde_p.as_mut()); + let mut hash = hash.into_output(); + hash.do_output_out(c_tilde_p.as_mut()); c_tilde_p }; @@ -1242,17 +1247,18 @@ impl< // β–· expand seed let (rho, rho_prime, K) = { let mut h = H::default(); - h.absorb(seed.ref_to_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::l as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); + h.do_update(seed.ref_to_bytes()); + h.do_update(&(P::k as u8).to_le_bytes()); + h.do_update(&(P::l as u8).to_le_bytes()); let mut rho = [0u8; 32]; - let bytes_written = h.squeeze_out(&mut rho); + let mut h = h.into_output(); + let bytes_written = h.do_output_out(&mut rho); debug_assert_eq!(bytes_written, 32); let mut rho_prime = [0u8; 64]; - let bytes_written = h.squeeze_out(&mut rho_prime); + let bytes_written = h.do_output_out(&mut rho_prime); debug_assert_eq!(bytes_written, 64); let mut K: [u8; 32] = [0u8; 32]; - let bytes_written = h.squeeze_out(&mut K); + let bytes_written = h.do_output_out(&mut K); debug_assert_eq!(bytes_written, 32); (rho, rho_prime, K) @@ -1261,11 +1267,12 @@ impl< // Alg 7; 7: πœŒβ€³ ← H(𝐾||π‘Ÿπ‘›π‘‘||πœ‡, 64) let rho_p_p = { let mut h = H::new(); - h.absorb(&K).expect("absorb before squeeze is infallible"); - h.absorb(&rnd).expect("absorb before squeeze is infallible"); - h.absorb(mu).expect("absorb before squeeze is infallible"); + h.do_update(&K); + h.do_update(&rnd); + h.do_update(mu); let mut rho_p_p = [0u8; 64]; - h.squeeze_out(&mut rho_p_p); + let mut h = h.into_output(); + h.do_output_out(&mut rho_p_p); rho_p_p }; @@ -1333,9 +1340,10 @@ impl< // 15: 𝑐_tilde ← H(πœ‡||w1Encode(𝐰1), πœ†/4) // β–· commitment hash let mut hash = H::new(); - hash.absorb(mu).expect("absorb before squeeze is infallible"); + hash.do_update(mu); w1.w1_encode_and_hash::

(&mut hash); - hash.squeeze_out(sig_val_c_tilde.as_mut()); + let mut hash = hash.into_output(); + hash.do_output_out(sig_val_c_tilde.as_mut()); } // Alg 7; 16: 𝑐 ∈ π‘…π‘ž ← SampleInBall(c_tilde) @@ -1961,14 +1969,14 @@ impl MuBuilder { // Algorithm 7 // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀', 64) let mut mb = Self { h: H::new() }; - mb.h.absorb(tr).expect("absorb before squeeze is infallible"); + mb.h.do_update(tr); // Algorithm 2 // 10: 𝑀′ ← BytesToBits(IntegerToBytes(0, 1) βˆ₯ IntegerToBytes(|𝑐𝑑π‘₯|, 1) βˆ₯ 𝑐𝑑π‘₯) βˆ₯ 𝑀 // all done together - mb.h.absorb(&[0u8]).expect("absorb before squeeze is infallible"); - mb.h.absorb(&[ctx.len() as u8]).expect("absorb before squeeze is infallible"); - mb.h.absorb(ctx).expect("absorb before squeeze is infallible"); + mb.h.do_update(&[0u8]); + mb.h.do_update(&[ctx.len() as u8]); + mb.h.do_update(ctx); // now ready to absorb M Ok(mb) @@ -1976,16 +1984,16 @@ impl MuBuilder { /// Stream a chunk of the message. pub fn do_update(&mut self, msg_chunk: &[u8]) { - self.h.absorb(msg_chunk).expect("absorb before squeeze is infallible"); + self.h.do_update(msg_chunk); } /// Finalize and return the mu value. - pub fn do_final(mut self) -> [u8; 64] { + pub fn do_final(self) -> [u8; 64] { // Completion of // Algorithm 7 // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀 β€², 64) let mut mu = [0u8; 64]; - self.h.squeeze_out(&mut mu); + self.h.into_output().do_output_out(&mut mu); mu } diff --git a/crypto/mldsa/tests/bc_test_data.rs b/crypto/mldsa/tests/bc_test_data.rs index e82df129..1625a89b 100644 --- a/crypto/mldsa/tests/bc_test_data.rs +++ b/crypto/mldsa/tests/bc_test_data.rs @@ -5,7 +5,7 @@ #![allow(dead_code)] use bouncycastle_core::errors::SignatureError; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{Hash, XOF, XofOutput}; use bouncycastle_sha3::SHAKE256; #[cfg(test)] @@ -966,14 +966,14 @@ impl BustedMuBuilder { // Algorithm 7 // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀', 64) let mut mb = Self { h: SHAKE256::new() }; - mb.h.absorb(tr).expect("absorb before squeeze is infallible"); + mb.h.do_update(tr); // Algorithm 2 // 10: 𝑀′ ← BytesToBits(IntegerToBytes(0, 1) βˆ₯ IntegerToBytes(|𝑐𝑑π‘₯|, 1) βˆ₯ 𝑐𝑑π‘₯) βˆ₯ 𝑀 // all done together - // mb.h.absorb(&[0u8]); // these are the busted lines -- bc-java just doesn't do these in the test code - // mb.h.absorb(&[ctx.len() as u8]); - // mb.h.absorb(ctx); + // mb.h.do_update(&[0u8]); // these are the busted lines -- bc-java just doesn't do these in the test code + // mb.h.do_update(&[ctx.len() as u8]); + // mb.h.do_update(ctx); // now ready to absorb M Ok(mb) @@ -981,16 +981,16 @@ impl BustedMuBuilder { /// Stream a chunk of the message. pub fn do_update(&mut self, msg_chunk: &[u8]) { - self.h.absorb(msg_chunk).expect("absorb before squeeze is infallible"); + self.h.do_update(msg_chunk); } /// Finalize and return the mu value. - pub fn do_final(mut self) -> [u8; 64] { + pub fn do_final(self) -> [u8; 64] { // Completion of // Algorithm 7 // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀 β€², 64) let mut mu = [0u8; 64]; - self.h.squeeze_out(&mut mu); + self.h.into_output().do_output_out(&mut mu); mu } diff --git a/crypto/mldsa/tests/mldsa_tests.rs b/crypto/mldsa/tests/mldsa_tests.rs index aebd3a06..010f06ed 100644 --- a/crypto/mldsa/tests/mldsa_tests.rs +++ b/crypto/mldsa/tests/mldsa_tests.rs @@ -7,8 +7,8 @@ mod mldsa_tests { KeyMaterial256, KeyMaterialTrait, KeyType, do_hazardous_operations, }; use bouncycastle_core::traits::{ - RNG, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, Signer, - Suspendable, + Hash, RNG, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, + Signer, Suspendable, }; use bouncycastle_core_test_framework::DUMMY_SEED; use bouncycastle_core_test_framework::FixedSeedRNG; @@ -1053,7 +1053,6 @@ mod mldsa_tests { #[test] fn serializable_state_mubuilder_rejects_wrong_variant() { - use bouncycastle_core::traits::XOF; use bouncycastle_sha3::SHAKE128; // A MuBuilder is always backed by SHAKE256. A serialized SHAKE128 state has the same length @@ -1061,9 +1060,7 @@ mod mldsa_tests { // variant tag weren't checked -- SHAKE128 (tag 5) must be rejected by MuBuilder (SHAKE256, // tag 6). let mut shake128 = SHAKE128::new(); - shake128 - .absorb(b"Colorless green ideas sleep furiously") - .expect("absorb before squeeze is infallible"); + shake128.do_update(b"Colorless green ideas sleep furiously"); let serialized_128 = shake128.suspend(); match MuBuilder::from_suspended(serialized_128) { diff --git a/crypto/mlkem-lowmemory/src/aux_functions.rs b/crypto/mlkem-lowmemory/src/aux_functions.rs index 406ef47a..9fda6722 100644 --- a/crypto/mlkem-lowmemory/src/aux_functions.rs +++ b/crypto/mlkem-lowmemory/src/aux_functions.rs @@ -2,7 +2,7 @@ use crate::mlkem::{N, q, q_inv}; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{Hash, XOF, XofOutput}; use bouncycastle_sha3::{SHAKE128, SHAKE256}; /// Algorithm 5 ByteEncode_d(𝐹) @@ -83,8 +83,8 @@ pub(crate) fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // 1: ctx ← XOF.Init() // 2: ctx ← XOF.Absorb(ctx, 𝐡) β–· input the given byte array into XOF let mut xof = SHAKE128::new(); - xof.absorb(rho).expect("absorb before squeeze is infallible"); - xof.absorb(nonce).expect("absorb before squeeze is infallible"); + xof.do_update(rho); + xof.do_update(nonce); // 3: 𝑗 ← 0 let mut j = 0usize; @@ -95,7 +95,8 @@ pub(crate) fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // It's likely around the average rejection rate, and 216 is a multiple of both 3 (required for this alg) // and 8 (efficient for SHAKE). let mut C = [0u8; 216]; - xof.squeeze_out(&mut C); + let mut xof = xof.into_output(); + xof.do_output_out(&mut C); let mut idx: usize = 0; // 4: while 𝑗 < 256 do @@ -103,7 +104,7 @@ pub(crate) fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // 5: (ctx, 𝐢) ← XOF.Squeeze(ctx, 3) // β–· get a fresh 3-byte array 𝐢 from XOF if idx == C.len() { - xof.squeeze_out(&mut C); + xof.do_output_out(&mut C); idx = 0; } @@ -200,11 +201,12 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { 2 => { let buf = { let mut xof = SHAKE256::new(); - xof.absorb(b).expect("absorb before squeeze is infallible"); - xof.absorb(&n.to_le_bytes()).expect("absorb before squeeze is infallible"); + xof.do_update(b); + xof.do_update(&n.to_le_bytes()); let mut buf = [0u8; 2 * 64]; - xof.squeeze_out(&mut buf); + let mut xof = xof.into_output(); + xof.do_output_out(&mut buf); buf }; @@ -213,10 +215,11 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { 3 => { let buf = { let mut xof = SHAKE256::new(); - xof.absorb(b).expect("absorb before squeeze is infallible"); - xof.absorb(&n.to_le_bytes()).expect("absorb before squeeze is infallible"); + xof.do_update(b); + xof.do_update(&n.to_le_bytes()); let mut buf = [0u8; 3 * 64]; - xof.squeeze_out(&mut buf); + let mut xof = xof.into_output(); + xof.do_output_out(&mut buf); buf }; diff --git a/crypto/mlkem-lowmemory/src/mlkem.rs b/crypto/mlkem-lowmemory/src/mlkem.rs index da61c593..25617d38 100644 --- a/crypto/mlkem-lowmemory/src/mlkem.rs +++ b/crypto/mlkem-lowmemory/src/mlkem.rs @@ -19,6 +19,7 @@ use bouncycastle_core::key_material::{ }; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, SecurityStrength, XOF, + XofOutput, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHA3_256, SHA3_512, SHAKE256}; @@ -431,9 +432,10 @@ impl< K_bar = { let mut K_bar: Secret<[u8; MLKEM_SS_LEN]> = Secret::new(); let mut j = J::new(); - j.absorb(dk.z()).expect("absorb before squeeze is infallible"); - j.absorb(&c).expect("absorb before squeeze is infallible"); - let bytes_written = j.squeeze_out(&mut *K_bar); + j.do_update(dk.z()); + j.do_update(&c); + let mut j = j.into_output(); + let bytes_written = j.do_output_out(&mut *K_bar); debug_assert_eq!(bytes_written, MLKEM_SS_LEN); K_bar diff --git a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs index 74cd7c17..bf2b7e9f 100644 --- a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs +++ b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs @@ -6,7 +6,8 @@ mod mlkem_tests { KeyMaterial512, KeyMaterialTrait, KeyType, do_hazardous_operations, }; use bouncycastle_core::traits::{ - KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength, XOF, + Hash, KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength, XOF, + XofOutput, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; @@ -434,12 +435,11 @@ mod mlkem_tests { // J is SHAKE256(𝑠, 8*32) let mut shake = SHAKE256::new(); - shake - .absorb(&seed.ref_to_bytes()[32..64]) - .expect("absorb before squeeze is infallible"); - shake.absorb(&busted_ciphertext).expect("absorb before squeeze is infallible"); + shake.do_update(&seed.ref_to_bytes()[32..64]); + shake.do_update(&busted_ciphertext); let mut buf = [0u8; 32]; - _ = shake.squeeze_out(&mut buf); + let mut shake = shake.into_output(); + _ = shake.do_output_out(&mut buf); assert_eq!(ss.ref_to_bytes(), buf); } diff --git a/crypto/mlkem/src/aux_functions.rs b/crypto/mlkem/src/aux_functions.rs index 3dbb8683..97f20e6f 100644 --- a/crypto/mlkem/src/aux_functions.rs +++ b/crypto/mlkem/src/aux_functions.rs @@ -4,7 +4,7 @@ use crate::matrix::{MatrixTrait, VectorTrait}; use crate::mlkem::{N, q, q_inv}; use crate::params::MLKEMParams; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{Hash, XOF, XofOutput}; use bouncycastle_sha3::{SHAKE128, SHAKE256}; pub(crate) fn expandA(rho: &[u8; 32]) -> P::MatrixA { @@ -92,8 +92,8 @@ pub fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // 1: ctx ← XOF.Init() // 2: ctx ← XOF.Absorb(ctx, 𝐡) β–· input the given byte array into XOF let mut xof = SHAKE128::new(); - xof.absorb(rho).expect("absorb before squeeze is infallible"); - xof.absorb(nonce).expect("absorb before squeeze is infallible"); + xof.do_update(rho); + xof.do_update(nonce); // 3: 𝑗 ← 0 let mut j = 0usize; @@ -104,7 +104,8 @@ pub fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // It's probably around the average rejection rate, and 216 is a multiple of both 3 (required for this alg) // and 8 (efficient for SHAKE). let mut C = [0u8; 216]; - xof.squeeze_out(&mut C); + let mut xof = xof.into_output(); + xof.do_output_out(&mut C); let mut idx: usize = 0; // 4: while 𝑗 < 256 do @@ -112,7 +113,7 @@ pub fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // 5: (ctx, 𝐢) ← XOF.Squeeze(ctx, 3) // β–· get a fresh 3-byte array 𝐢 from XOF if idx == C.len() { - xof.squeeze_out(&mut C); + xof.do_output_out(&mut C); idx = 0; } @@ -209,11 +210,12 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { 2 => { let buf = { let mut xof = SHAKE256::new(); - xof.absorb(b).expect("absorb before squeeze is infallible"); - xof.absorb(&n.to_le_bytes()).expect("absorb before squeeze is infallible"); + xof.do_update(b); + xof.do_update(&n.to_le_bytes()); let mut buf = [0u8; 2 * 64]; - xof.squeeze_out(&mut buf); + let mut xof = xof.into_output(); + xof.do_output_out(&mut buf); buf }; @@ -222,10 +224,11 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { 3 => { let buf = { let mut xof = SHAKE256::new(); - xof.absorb(b).expect("absorb before squeeze is infallible"); - xof.absorb(&n.to_le_bytes()).expect("absorb before squeeze is infallible"); + xof.do_update(b); + xof.do_update(&n.to_le_bytes()); let mut buf = [0u8; 3 * 64]; - xof.squeeze_out(&mut buf); + let mut xof = xof.into_output(); + xof.do_output_out(&mut buf); buf }; diff --git a/crypto/mlkem/src/mlkem.rs b/crypto/mlkem/src/mlkem.rs index 6490a521..afd76c19 100644 --- a/crypto/mlkem/src/mlkem.rs +++ b/crypto/mlkem/src/mlkem.rs @@ -151,6 +151,7 @@ use bouncycastle_core::key_material::{ }; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, SecurityStrength, XOF, + XofOutput, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHA3_256, SHA3_512, SHAKE256}; @@ -635,10 +636,11 @@ impl< let K_bar: [u8; MLKEM_SS_LEN]; K_bar = { let mut j = J::new(); - j.absorb(dk.z().as_ref()).expect("absorb before squeeze is infallible"); - j.absorb(&c).expect("absorb before squeeze is infallible"); + j.do_update(dk.z().as_ref()); + j.do_update(&c); let mut buf = [0u8; MLKEM_SS_LEN]; - let bytes_written = j.squeeze_out(&mut buf); + let mut j = j.into_output(); + let bytes_written = j.do_output_out(&mut buf); debug_assert_eq!(bytes_written, MLKEM_SS_LEN); buf diff --git a/crypto/mlkem/tests/mlkem_tests.rs b/crypto/mlkem/tests/mlkem_tests.rs index 4faf8498..733f1861 100644 --- a/crypto/mlkem/tests/mlkem_tests.rs +++ b/crypto/mlkem/tests/mlkem_tests.rs @@ -5,7 +5,8 @@ mod mlkem_tests { use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ - KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength, XOF, + Hash, KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength, XOF, + XofOutput, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; @@ -469,12 +470,11 @@ mod mlkem_tests { // J is SHAKE256(𝑠, 8*32) let mut shake = SHAKE256::new(); - shake - .absorb(&seed.ref_to_bytes()[32..64]) - .expect("absorb before squeeze is infallible"); - shake.absorb(&busted_ciphertext).expect("absorb before squeeze is infallible"); + shake.do_update(&seed.ref_to_bytes()[32..64]); + shake.do_update(&busted_ciphertext); let mut buf = [0u8; 32]; - _ = shake.squeeze_out(&mut buf); + let mut shake = shake.into_output(); + _ = shake.do_output_out(&mut buf); assert_eq!(ss.ref_to_bytes(), buf); } diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 4f276df0..695f73c0 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -60,7 +60,7 @@ //! ## XOF //! SHA3 offers Extendable-Output Functions in the form of SHAKE, which is accessed through the [`XOF`] trait, //! which is implemented by [`SHAKE128`] and [`SHAKE256`]. -//! The difference from [`Hash`] is that SHAKE can produce output of any length. +//! [`XOF`] extends [`Hash`] -- SHAKE *is* a hash -- and adds the ability to choose the output length. //! //! The simplest usage is via the static functions. The following example produces a 16 byte (128-bit) and 16KiB output: //!``` @@ -72,27 +72,35 @@ //! let output_16KiB: Vec = sha3::SHAKE128::new().hash_xof(data, 16 * 1024); //! ``` //! -//! As with [`Hash`] above, the [`XOF`] trait has streaming APIs in the form of [`XOF::absorb`] and [`XOF::squeeze`]. -//! Unlike [`Hash::do_final`], [`XOF::squeeze`] can be called multiple times. -//! Note, however, that once you start squeezing, you can no longer absorb more input -- [`XOF::absorb`] -//! will throw a [`HashError::InvalidState`], but the SHAKE object will still be usable for squeezing -//! as if the erroneous `absorb` call never happened. +//! [`XOF`] extends [`Hash`], so SHAKE takes input through [`Hash::do_update`] like any other hash. +//! Output is where they differ: [`XOF::into_output`] ends the input phase and returns an +//! [`XofOutput`](bouncycastle_core::traits::XofOutput), whose +//! [`do_output`](bouncycastle_core::traits::XofOutput::do_output) can be called as many times as you +//! like, each call continuing one stream. +//! +//! Absorbing after output has begun is not an error you can make: `into_output` consumes the +//! SHAKE, so there is no value left to call [`Hash::do_update`] on. //! //! The following code produces the same output as the previous example: //!``` -//! use bouncycastle_core::traits::XOF; +//! use bouncycastle_core::traits::{Hash, XOF, XofOutput}; //! use bouncycastle_sha3 as sha3; //! //! let data: &[u8] = b"Hello, world!"; //! let mut shake = sha3::SHAKE128::new(); -//! shake.absorb(data).expect("infallible before squeeze"); -//! let output_16byte: Vec = shake.squeeze(16); +//! shake.do_update(data); +//! let output_16byte: Vec = shake.into_output().do_output(16); //! -//! let mut shake = sha3::SHAKE128::new(); +//! let mut shake = sha3::SHAKE128::new().into_output(); //! let mut output_16KiB: Vec = vec![]; -//! for i in 0..16 { output_16KiB.extend_from_slice(&shake.squeeze(1024)) } +//! for i in 0..16 { output_16KiB.extend_from_slice(&shake.do_output(1024)) } //! ``` //! +//! Because [`XOF`] extends [`Hash`], SHAKE can also be used wherever a hash is wanted: +//! [`Hash::do_final`] produces the nominal digest size, 32 bytes for SHAKE128 and 64 for SHAKE256 +//! (the length at which the output carries the full security level), and the one-shot +//! [`Hash::hash`] does the same. +//! //! ## KDF //! 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. diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index 263cb0cc..3c02a33d 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -7,7 +7,9 @@ use bouncycastle_core::errors::{HashError, KDFError, SuspendableError}; use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; -use bouncycastle_core::traits::{Algorithm, KDF, SecurityStrength, Suspendable, XOF}; +use bouncycastle_core::traits::{ + Algorithm, Hash, KDF, SecurityStrength, Suspendable, XOF, XofOutput, +}; use bouncycastle_utils::{max, min}; /// Internal struct for SHAKE. @@ -53,32 +55,27 @@ impl SHAKEInternal { } } - /// Swallows errors and simply returns an empty Vec if the hashes fails for whatever reason. fn hash_internal(mut self, data: &[u8], result_len: usize) -> Vec { - // The absorb fails if this object has already begun squeezing, which the caller is free to - // have done: these one-shot APIs take `self`, they do not require a fresh object. - if self.absorb(data).is_err() { - return Vec::new(); - } - self.squeeze(result_len) + self.keccak.absorb(data); + self.into_output().do_output(result_len) } - /// Swallows errors and simply returns 0, leaving `output` zeroized, if the hashes fails for - /// whatever reason. fn hash_internal_out(mut self, data: &[u8], output: &mut [u8]) -> usize { - output.fill(0); + self.keccak.absorb(data); + self.into_output().do_output_out(output) + } - // The absorb fails if this object has already begun squeezing, which the caller is free to - // have done: these one-shot APIs take `self`, they do not require a fresh object. - if self.absorb(data).is_err() { - return 0; + /// Produces the next bytes of the output stream, applying the SHAKE "1111" domain separator + /// (FIPS 202 s. 6.2) on the first call. Reached only through [`SHAKEOutput`], so the caller + /// cannot interleave this with absorbing. + fn squeeze_internal_out(&mut self, output: &mut [u8]) -> usize { + output.fill(0); + if !self.keccak.squeezing { + self.keccak.absorb_bits(0x0F, 4).expect("Absorb_bits failed"); } - self.squeeze_out(output) + self.keccak.squeeze(output) } - /// Returns [`KDFError::HashError`] wrapping a [`HashError::InvalidState`] if this object has - /// already begun squeezing, since key material absorbed after that point would not contribute - /// to the derived key. fn mix_key_internal(&mut self, key: &impl KeyMaterialTrait) -> Result<(), KDFError> { // track the strongest input key type self.kdf_key_type = *max(&self.kdf_key_type, &key.key_type()); @@ -94,9 +91,8 @@ impl SHAKEInternal { ); } - // The absorb fails if this object has already begun squeezing, which the caller is free to - // have done: the KDF entry points take `self`, they do not require a fresh object. - Ok(self.absorb(key.ref_to_bytes())?) + self.keccak.absorb(key.ref_to_bytes()); + Ok(()) } fn derive_key_final_internal( @@ -132,12 +128,11 @@ impl SHAKEInternal { self.kdf_security_strength = SecurityStrength::None; // BytesLowEntropy can't have a securtiy level. } - // As in mix_key_internal(): the absorb fails if this object has already begun squeezing. - self.absorb(additional_input)?; + self.keccak.absorb(additional_input); let mut bytes_written: usize = 0; key_material::do_hazardous_operations(output_key, |output_key| { - bytes_written = self.squeeze_out( + bytes_written = self.squeeze_internal_out( output_key.ref_to_bytes_mut().expect("Infallible within do_hazardous_operations"), ); output_key.set_key_len(bytes_written) @@ -191,6 +186,14 @@ impl Suspendable for SHAKEInterna let (keccak, kdf_key_type, kdf_security_strength, kdf_entropy) = deserialize_sha3_family_state(input, PARAMS::STATE_TAG, rate)?; + // A SHAKEInternal accepts input, so it must never be rebuilt in the squeezing phase -- + // that is the invariant `Hash::do_update` relies on. A suspended squeezing sponge is a + // SHAKEOutput; resume it as one. + if keccak.squeezing { + // InvalidData rather than a new variant: for this type the phase byte is simply wrong. + return Err(SuspendableError::InvalidData); + } + Ok(SHAKEInternal { _phantomdata: core::marker::PhantomData, keccak, @@ -274,122 +277,222 @@ impl Default for SHAKEInternal { } } -impl XOF for SHAKEInternal { - fn hash_xof(self, data: &[u8], result_len: usize) -> Vec { - self.hash_internal(data, result_len) +/// The squeezing half of SHAKE: what [`XOF::into_output`] hands back. +/// +/// It owns the sponge, so the absorbing value is gone by the time this exists. That is the whole +/// point: [`Hash::do_update`] cannot be called on a SHAKE that has begun producing output, because +/// there is no longer a SHAKE to call it on. +pub struct SHAKEOutput { + shake: SHAKEInternal, +} + +impl XofOutput for SHAKEOutput { + fn do_output(&mut self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.do_output_out(&mut out); + out } - fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { - // hash_internal_out zeroizes `output` before writing. - self.hash_internal_out(data, output) + fn do_output_out(&mut self, output: &mut [u8]) -> usize { + self.shake.squeeze_internal_out(output) } +} - /// This can throw a [`HashError::InvalidState`] if called after squeezing has begun, - /// but is safe to consider infallible otherwise -- IE feel free to use `.unwrap()` or `.expect()` - /// on the result if you are confident that your code cannot call `absorb` after squeezing. - /// - /// A rejected call leaves the SHAKE object untouched so the output stream continues consistently. - /// IE it is safe to attempt to feed in more input and do nothing if the absorb fails - /// ("safe" in the sense that it won't panic, but it may still produce an incorrect output which - /// could be insecure in the sense of being predictable or low-entropy). - fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> { - // A sponge XOF cannot return to absorbing once squeezing has begun (FIPS 202 defines SHAKE as - // a single function of the whole message; re-absorbing would be an unapproved duplex). - if self.keccak.squeezing { - return Err(HashError::InvalidState("cannot absorb after squeezing has begun")); - } - self.keccak.absorb(data); - Ok(()) +impl Clone for SHAKEOutput { + fn clone(&self) -> Self { + Self { shake: self.shake.clone() } } +} - /// Switches to squeezing. - fn absorb_last_partial_byte( - &mut self, - partial_byte: u8, - num_partial_bits: usize, - ) -> Result<(), HashError> { - // Same phase rule as absorb(): reject a partial-byte absorb once squeezing has begun. Checked - // before any state mutation so a rejected call leaves the sponge untouched. - if self.keccak.squeezing { - return Err(HashError::InvalidState("cannot absorb after squeezing has begun")); - } - // A partial byte has at most 7 bits; 0 means the message ends on a byte boundary. - if num_partial_bits > 7 { - return Err(HashError::InvalidLength("num_partial_bits must be in the range [0,7]")); - } - // Mutants note: This is just bit-setting into empty space. - // It works the same regardless of whether it's OR or XOR. - // The public convention puts the message bits in the most significant bits of partial_byte, - // leading bit first (ASN.1 BIT STRING order, X.690 s. 8.6.2.1). Keccak absorbs a byte - // LSB-first: FIPS 202 Algorithm 10 (h2b) step 3 sets message bit T[8i + j] = b_ij, the bit - // of weight 2^j in byte i. So reverse the bit order and keep the low num_partial_bits bits. - let message_bits = (partial_byte.reverse_bits() as u16) & ((1 << num_partial_bits) - 1); - let mut final_input: u16 = message_bits | (0x0F << num_partial_bits); - let mut final_bits = num_partial_bits + 4; +/// The squeezing phase suspends and resumes just as the absorbing phase does, so a long output +/// stream can be paused. The serialized form is the same one [`SHAKEInternal`] writes -- the +/// keccak state records which phase it is in -- so the two `from_suspended` implementations +/// accept exactly the states the other rejects. +impl Suspendable for SHAKEOutput { + fn suspend(self) -> [u8; SUSPENDED_SHA3_STATE_LEN] { + self.shake.suspend() + } - if final_bits >= 8 { - self.keccak.absorb(&[final_input as u8]); - final_bits -= 8; - final_input >>= 8; + fn from_suspended( + serialized_state: [u8; SUSPENDED_SHA3_STATE_LEN], + ) -> Result { + let input: &[u8; SHA3_FAMILY_STATE_LEN] = + check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + let rate = 1600 - ((PARAMS::SIZE as usize) << 1); + let (keccak, kdf_key_type, kdf_security_strength, kdf_entropy) = + deserialize_sha3_family_state(input, PARAMS::STATE_TAG, rate)?; + + // The mirror of the check in `SHAKEInternal::from_suspended`: a state that had not begun + // producing output is still absorbing, and resuming it here would skip the domain suffix. + if !keccak.squeezing { + return Err(SuspendableError::InvalidData); } - // Infallible: guarded above (not squeezing), the queue is byte-aligned here, and final_bits is - // in 0..=7 by construction. - self.keccak.absorb_bits(final_input as u8, final_bits).expect("Absorb failed."); + Ok(Self { + shake: SHAKEInternal { + _phantomdata: core::marker::PhantomData, + keccak, + kdf_key_type, + kdf_security_strength, + kdf_entropy, + }, + }) + } +} - Ok(()) +impl Hash for SHAKEInternal { + /// The sponge rate in bits: `1600 - 2c`, where the capacity `c` is twice the security level + /// (FIPS 202 Table 3 -- 1344 bits for SHAKE128, 1088 for SHAKE256). + fn block_bitlen(&self) -> usize { + 1600 - ((PARAMS::SIZE as usize) << 1) } - fn squeeze(&mut self, num_bytes: usize) -> Vec { - let mut out: Vec = vec![0u8; num_bytes]; - self.squeeze_out(&mut out); + /// The nominal digest size: 32 bytes for SHAKE128, 64 for SHAKE256. + /// + /// A XOF has no inherent output length, so this is a convention rather than a property of the + /// function. It is BC Java's: `SHAKEDigest.getDigestSize()` returns `fixedOutputLength / 4`, + /// which is the length at which the output carries the full security level. + fn output_len(&self) -> usize { + (PARAMS::SIZE as usize) / 4 + } + + fn hash(self, data: &[u8]) -> Vec { + let result_len = self.output_len(); + self.hash_internal(data, result_len) + } + + fn hash_out(self, data: &[u8], output: &mut [u8]) -> usize { + // hash_internal_out zeroizes `output` before writing. + self.hash_internal_out(data, output) + } + + /// Infallible, and this is a fact about the type rather than a promise. + /// + /// Absorbing after squeezing has begun would be wrong -- FIPS 202 defines SHAKE as a single + /// function of the whole message, so re-absorbing would be an unapproved duplex -- and it cannot + /// be expressed: producing output goes through [`XOF::into_output`], which consumes the value, + /// and every `KDF` entry point takes `self` by value too. A `SHAKEInternal` a caller can still + /// name has therefore never squeezed. + fn do_update(&mut self, data: &[u8]) { + // Pins the invariant the doc above argues for, so a future change that lets a squeezing + // SHAKE escape fails the test suite rather than silently corrupting the sponge. + debug_assert!(!self.keccak.squeezing, "a reachable SHAKEInternal has never squeezed"); + self.keccak.absorb(data); + } + + /// Produces [`output_len`](Self::output_len) bytes and ends the object, as BC Java's + /// `Digest.doFinal(out, outOff)` does via `doFinal(out, outOff, getDigestSize())`. + fn do_final(self) -> Vec { + let n = self.output_len(); + let mut out = vec![0u8; n]; + self.do_final_out(&mut out); out } - fn squeeze_out(&mut self, output: &mut [u8]) -> usize { - output.fill(0); + fn do_final_out(self, output: &mut [u8]) -> usize { + self.into_output().do_output_out(output) + } - if !self.keccak.squeezing { - self.keccak.absorb_bits(0x0F, 4).expect("Absorb_bits failed"); - }; + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let mut out = vec![0u8; self.output_len()]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } - self.keccak.squeeze(output) + fn do_final_partial_bits_out( + self, + partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + // Validated before anything is written, so a rejected call leaves `output` untouched. + Ok(self.into_output_partial_bits(partial_byte, num_bits)?.do_output_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::from_bits(PARAMS::SIZE as usize) } +} - fn squeeze_partial_byte_final(self, num_bits: usize) -> Result { - let mut output: u8 = 0; - self.squeeze_partial_byte_final_out(num_bits, &mut output)?; - Ok(output) +/// The absorb-then-squeeze rule, as a compile error rather than a runtime one. +/// +/// ```compile_fail +/// use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +/// use bouncycastle_sha3::SHAKE128; +/// +/// let mut shake = SHAKE128::new(); +/// shake.do_update(b"abc"); +/// let mut out = shake.into_output(); +/// let _ = out.do_output(32); +/// shake.do_update(b"more"); // `shake` was moved by into_output() +/// ``` +/// +/// The same value used correctly: +/// +/// ``` +/// use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +/// use bouncycastle_sha3::SHAKE128; +/// +/// let mut shake = SHAKE128::new(); +/// shake.do_update(b"abc"); +/// let mut out = shake.into_output(); +/// assert_eq!(out.do_output(32).len(), 32); +/// ``` +impl XOF for SHAKEInternal { + type Output = SHAKEOutput; + + fn into_output(mut self) -> Self::Output { + // The SHAKE domain separator, "1111" (FIPS 202 s. 6.2), applied as the sponge switches to + // squeezing. Infallible: this value has never squeezed (see `do_update`), so the queue is + // byte-aligned and `absorb_bits` cannot reject it. + self.keccak.absorb_bits(0x0F, 4).expect("a SHAKE that has not squeezed can absorb bits"); + SHAKEOutput { shake: self } } - /// Result is the number of bits squezed into `output`. - fn squeeze_partial_byte_final_out( + fn into_output_partial_bits( mut self, + partial_byte: u8, num_bits: usize, - output: &mut u8, - ) -> Result<(), HashError> { - // A partial byte has at most 7 bits; 0 means no bits are requested. Checked before the shift - // below, which would overflow for num_bits >= 8. + ) -> Result { + // A partial byte has at most 7 bits; 0 means the message ends on a byte boundary. + // Checked before any state change, so a rejected call leaves the sponge untouched. if num_bits > 7 { return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); } + // Mutants note: this is bit-setting into empty space, so OR and XOR behave identically. + // The public convention puts the message bits in the most significant bits of partial_byte, + // leading bit first (ASN.1 BIT STRING order, X.690 s. 8.6.2.1). Keccak absorbs a byte + // LSB-first: FIPS 202 Algorithm 10 (h2b) step 3 sets message bit T[8i + j] = b_ij, the bit + // of weight 2^j in byte i. So reverse the bit order and keep the low num_bits bits. + let message_bits = (partial_byte.reverse_bits() as u16) & ((1 << num_bits) - 1); + let mut final_input: u16 = message_bits | (0x0F << num_bits); + let mut final_bits = num_bits + 4; + + if final_bits >= 8 { + self.keccak.absorb(&[final_input as u8]); + final_bits -= 8; + final_input >>= 8; + } - *output = 0; + // Infallible: this value has never squeezed, the queue is byte-aligned here, and final_bits + // is in 0..=7 by construction. + self.keccak.absorb_bits(final_input as u8, final_bits).expect("Absorb failed."); - // Via squeeze_out() so the SHAKE "1111" suffix (FIPS 202 s. 6.2) is applied on a first squeeze. - let mut buf = [0u8; 1]; - self.squeeze_out(&mut buf); + // The "1111" suffix is already folded into final_input above, so the sponge is finished + // absorbing; wrap it without applying the suffix a second time. + Ok(SHAKEOutput { shake: self }) + } - // Keccak emits the bits of an output byte LSB-first (FIPS 202 Algorithm 11, b2h: output bit - // T[8i + j] has weight 2^j), and the public convention returns them as the final octet of an - // ASN.1 BIT STRING (X.690 s. 8.6.2.1): first bit in the MSB, unused low bits zero. So reverse - // the bit order and keep the top num_bits bits. The mask is built in u16 so that num_bits == 0 - // cannot overflow (0xFF00 >> 0 truncates to 0x00). - *output = buf[0].reverse_bits() & ((0xFF00u16 >> num_bits) as u8); - Ok(()) + fn hash_xof(self, data: &[u8], result_len: usize) -> Vec { + self.hash_internal(data, result_len) } - fn max_security_strength(&self) -> SecurityStrength { - SecurityStrength::from_bits(PARAMS::SIZE as usize) + fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { + // hash_internal_out zeroizes `output` before writing. + self.hash_internal_out(data, output) } } diff --git a/crypto/sha3/tests/bc-test-data.rs b/crypto/sha3/tests/bc-test-data.rs index 334bb6f9..147d7c91 100644 --- a/crypto/sha3/tests/bc-test-data.rs +++ b/crypto/sha3/tests/bc-test-data.rs @@ -25,7 +25,7 @@ //! `Outputlen = minoutbytes + (rightmost 16 bits of Output as big-endian integer) mod //! (maxoutbytes - minoutbytes + 1)` bytes; report `Output`/`Outputlen` per COUNT. -use bouncycastle_core::traits::{Hash, XOF}; +use bouncycastle_core::traits::{Hash, XOF, XofOutput}; use bouncycastle_hex as hex; use bouncycastle_sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256}; use std::fs; @@ -168,19 +168,19 @@ fn run_sha3_monte_file(orientation: &str, filename: &str) { fn shake_bits(msg: &[u8], len_bits: usize, out_bits: usize) -> Vec { let mut x = X::default(); let (whole, partial) = (len_bits / 8, len_bits % 8); - x.absorb(&msg[..whole]).expect("absorb before squeeze is infallible"); - if partial != 0 { - x.absorb_last_partial_byte(msg[whole].reverse_bits(), partial) - .expect("partial is in 1..=7"); - } + x.do_update(&msg[..whole]); + let mut out_stream = if partial != 0 { + x.into_output_partial_bits(msg[whole].reverse_bits(), partial).expect("partial is in 1..=7") + } else { + x.into_output() + }; let (out_whole, out_partial) = (out_bits / 8, out_bits % 8); - let mut out = x.squeeze(out_whole); + let mut out = out_stream.do_output(out_whole + usize::from(out_partial != 0)); if out_partial != 0 { - out.push( - x.squeeze_partial_byte_final(out_partial) - .expect("out_partial is in 1..=7") - .reverse_bits(), - ); + // FIPS 202 B.1: an output of `out_bits` bits occupies the low `out_partial` bits of its + // final octet, so the unused high bits of the byte the sponge gave us are dropped. + let last = out.len() - 1; + out[last] &= (1u8 << out_partial) - 1; } out } diff --git a/crypto/sha3/tests/shake_tests.rs b/crypto/sha3/tests/shake_tests.rs index 3d2f5fba..2f9fe83a 100644 --- a/crypto/sha3/tests/shake_tests.rs +++ b/crypto/sha3/tests/shake_tests.rs @@ -7,174 +7,54 @@ mod shake_tests { use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterial256, KeyMaterial512, KeyMaterialTrait, KeyType, }; - use bouncycastle_core::traits::{KDF, SecurityStrength, XOF}; + use bouncycastle_core::traits::{Hash, KDF, SecurityStrength, XOF, XofOutput}; use bouncycastle_core_test_framework::DUMMY_SEED; use bouncycastle_core_test_framework::kdf::TestFrameworkKDF; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_sha3::{SHA3_256, SHAKE128, SHAKE256}; - #[test] - fn test_xof_partial_bit_output() { - // The 4th ([3]) byte of the output of SHA128(\x00\x01\x02\x03\x04) is known to be 0xFF - // That fact is used to test partial byte output. - - let output = SHAKE128::new().hash_xof(&[0u8, 1u8, 2u8, 3u8, 4u8], 4); - assert_eq!(output[3], 0xFF); - - // just for comparison - let mut output2 = vec![0u8; 4]; - SHAKE128::new().hash_xof_out(&[0u8, 1u8, 2u8, 3u8, 4u8], &mut output2); - assert_eq!(output, output2); - - // test bounds - // 0 is in range: it requests no bits, so the result is 0x00. - let mut shake = SHAKE128::new(); - shake.absorb(&[0u8, 1u8, 2u8, 3u8, 4u8]).expect("absorb before squeeze is infallible"); - let _throwaway = shake.squeeze(3); - assert_eq!(shake.squeeze_partial_byte_final(0).expect("Squeeze failed"), 0x00); - - // 8 and above are out of range. - for bad in [8usize, 9, 15, 16, 64, usize::MAX] { - let mut shake = SHAKE128::new(); - shake.absorb(&[0u8, 1u8, 2u8, 3u8, 4u8]).expect("absorb before squeeze is infallible"); - let _throwaway = shake.squeeze(3); - assert!( - matches!(shake.squeeze_partial_byte_final(bad), Err(HashError::InvalidLength(_))), - "num_bits={bad}" - ); - } - - for i in 0..=7 { - let mut shake = SHAKE128::new(); - shake.absorb(&[0u8, 1u8, 2u8, 3u8, 4u8]).expect("absorb before squeeze is infallible"); - _ = shake.squeeze(3); - let out: u8 = shake.squeeze_partial_byte_final(i).expect("Squeeze failed"); - // byte [3] of the stream is 0xFF, so its first `i` bits, returned MSB-first, are the top - // `i` set bits. - assert_eq!(out, (0xFF00u16 >> i) as u8); - } - - // success case -- output slice version - let mut shake = SHAKE128::new(); - shake.absorb(&[0u8, 1u8, 2u8, 3u8, 4u8]).expect("absorb before squeeze is infallible"); - _ = shake.squeeze(3); - let mut out = 0u8; - shake.squeeze_partial_byte_final_out(1, &mut out).expect("Squeeze failed"); - assert_eq!(out, 0x80); - } - - /// Regression: squeeze_partial_byte_final() as the *first* squeeze must apply the SHAKE "1111" - /// domain suffix (previously it bypassed it and returned raw Keccak output), and must return the - /// first `num_bits` bits of the next output byte (its low bits, FIPS 202 B.1 bit ordering) in the - /// top `num_bits` bits of the result (ASN.1 BIT STRING order), with the unused low bits zero. - #[test] - fn partial_bit_output_as_first_squeeze_matches_full_output() { - let msg = b"abc"; - for skip in [0usize, 1, 5] { - let mut shake = SHAKE256::new(); - shake.absorb(msg).unwrap(); - let full = shake.squeeze(skip + 1)[skip]; - // pick a byte that is not all-ones/all-zeros so bit selection is actually tested - assert!( - full != 0x00 && full != 0xFF, - "test vector byte must be non-uniform: {full:#x}" - ); - - for n in 0..=7usize { - let mut shake = SHAKE256::new(); - shake.absorb(msg).unwrap(); - if skip > 0 { - _ = shake.squeeze(skip); - } - let got = shake.squeeze_partial_byte_final(n).unwrap(); - assert_eq!( - got, - full.reverse_bits() & ((0xFF00u16 >> n) as u8), - "skip={skip} n={n}" - ); - assert_eq!(got & (0xFFu8 >> n), 0, "unused low bits must be zero"); - } - } - } - /// Regression: when the 4 trailing message bits plus the SHAKE "1111" suffix exactly fill a byte, /// the sponge must still switch to squeezing, otherwise the first squeeze appended a second suffix. /// 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. #[test] - fn absorb_last_partial_byte_four_bits() { - let mut shake = SHAKE128::new(); - shake.absorb_last_partial_byte(0x10, 4).unwrap(); + fn into_output_partial_bits_four_bits() { + let shake = SHAKE128::new(); + let mut out = shake.into_output_partial_bits(0x10, 4).unwrap(); assert_eq!( - shake.squeeze(16), + out.do_output(16), bouncycastle_hex::decode("d40238024b040a954d9c2c89daf480e5").unwrap(), "SHAKE128 of the 4-bit message 0001" ); } - /// absorb_last_partial_byte() must validate num_partial_bits before shifting: 0 is allowed + /// into_output_partial_bits() must validate num_bits before shifting: 0 is allowed /// (finalize with no partial byte), 8+ is rejected with InvalidLength rather than panicking. #[test] - fn absorb_last_partial_byte_validates_range() { + fn into_output_partial_bits_validates_range() { for bad in [8usize, 9, 15, 16, 64, usize::MAX] { let mut shake = SHAKE128::new(); - shake.absorb(b"abc").unwrap(); + shake.do_update(b"abc"); assert!( matches!( - shake.absorb_last_partial_byte(0xFF, bad), + shake.into_output_partial_bits(0xFF, bad), Err(HashError::InvalidLength(_)) ), - "num_partial_bits={bad}" + "num_bits={bad}" ); } let mut a = SHAKE128::new(); - a.absorb(b"abc").unwrap(); - a.absorb_last_partial_byte(0xFF, 0).unwrap(); - assert_eq!(a.squeeze(32), SHAKE128::new().hash_xof(b"abc", 32)); + a.do_update(b"abc"); + let mut a = a.into_output_partial_bits(0xFF, 0).unwrap(); + assert_eq!(a.do_output(32), SHAKE128::new().hash_xof(b"abc", 32)); // Upper boundary: 7 bits is the largest valid partial byte and must be accepted, and must // actually change the output relative to the byte-aligned message. let mut b = SHAKE128::new(); - b.absorb(b"abc").unwrap(); - b.absorb_last_partial_byte(0xFE, 7).unwrap(); - assert_ne!(b.squeeze(32), SHAKE128::new().hash_xof(b"abc", 32)); - } - - /// Once squeezing has begun, a SHAKE cannot return to absorbing (FIPS 202 defines SHAKE as a - /// single function of the whole message). Both absorb entry points must reject a post-squeeze call - /// with `HashError::InvalidState` rather than panicking, and a rejected call must leave the sponge - /// untouched so the output stream continues consistently. - #[test] - fn absorb_after_squeeze_is_rejected() { - use bouncycastle_core::errors::HashError; - - // absorb() after squeeze() -> InvalidState. - let mut shake = SHAKE128::new(); - shake.absorb(b"input").expect("absorb before squeeze is infallible"); - let _ = shake.squeeze(16); - assert!(matches!(shake.absorb(b"more"), Err(HashError::InvalidState(_)))); - - // absorb_last_partial_byte() after squeeze() -> InvalidState. - let mut shake = SHAKE256::new(); - shake.absorb(b"input").expect("absorb before squeeze is infallible"); - let _ = shake.squeeze(16); - assert!(matches!(shake.absorb_last_partial_byte(0x01, 3), Err(HashError::InvalidState(_)))); - - // A rejected absorb must not corrupt state: the output stream continues as if it never - // happened. Squeezing 16 + 16 bytes around a rejected absorb must equal a clean squeeze of 32. - let mut a = SHAKE128::new(); - a.absorb(b"input").expect("absorb before squeeze is infallible"); - let first = a.squeeze(16); - assert!(a.absorb(b"more").is_err()); - let second = a.squeeze(16); - - let mut b = SHAKE128::new(); - b.absorb(b"input").expect("absorb before squeeze is infallible"); - let clean = b.squeeze(32); - - assert_eq!(first.as_slice(), &clean[..16]); - assert_eq!(second.as_slice(), &clean[16..]); + b.do_update(b"abc"); + let mut b = b.into_output_partial_bits(0xFE, 7).unwrap(); + assert_ne!(b.do_output(32), SHAKE128::new().hash_xof(b"abc", 32)); } #[test] @@ -343,9 +223,9 @@ mod shake_tests { #[test] fn security_strength() { assert_eq!(KDF::max_security_strength(&SHAKE128::default()), SecurityStrength::_128bit); - assert_eq!(XOF::max_security_strength(&SHAKE128::default()), SecurityStrength::_128bit); + assert_eq!(Hash::max_security_strength(&SHAKE128::default()), SecurityStrength::_128bit); assert_eq!(KDF::max_security_strength(&SHAKE256::default()), SecurityStrength::_256bit); - assert_eq!(XOF::max_security_strength(&SHAKE256::default()), SecurityStrength::_256bit); + assert_eq!(Hash::max_security_strength(&SHAKE256::default()), SecurityStrength::_256bit); } #[test] @@ -369,36 +249,58 @@ mod shake_tests { let str = "Colorless green ideas sleep furiously"; // A helper that exercises the full round-trip for one SHAKE variant. - fn round_trip + Clone>(mut shake: X, input: &[u8]) { - shake.absorb(input).expect("absorb before squeeze is infallible"); + // Each phase suspends as its own type: an absorbing state resumes as `X`, a squeezing one + // as `X::Output`, and each rejects the other's phase. + fn round_trip(mut shake: X, input: &[u8]) + where + X: XOF + Suspendable + Clone, + X::Output: Suspendable + Clone, + { + shake.do_update(input); // do the default trait-conformance tests TestFrameworkSuspendableState::new().test(&shake); // Test #1 - // serialize the in-progress (absorbing) state, then squeeze from the original and compare - let serialized_state = shake.clone().suspend(); - let expected = shake.squeeze(64); + // serialize the in-progress (absorbing) state, then read from the original and compare + let absorbing_state = shake.clone().suspend(); + let mut out = shake.into_output(); + let expected = out.do_output(64); // rebuild from the serialized state and confirm it produces the same output - let mut from_state = X::from_suspended(serialized_state).unwrap(); - assert_eq!(expected, from_state.squeeze(64)); + let from_state = + X::from_suspended(absorbing_state).expect("an absorbing state resumes as the XOF"); + assert_eq!(expected, from_state.into_output().do_output(64)); // Test #2 - // serialize the in-progress (squeezing) state, then squeeze more from the original and compare - let serialized_state = shake.clone().suspend(); - let expected = shake.squeeze(64); + // serialize the in-progress (squeezing) state, then read more from the original and compare + let squeezing_state = out.clone().suspend(); + let expected = out.do_output(64); // rebuild from the serialized state and confirm it produces the same output - let mut from_state = X::from_suspended(serialized_state).unwrap(); - assert_eq!(expected, from_state.squeeze(64)); + let mut from_state = X::Output::from_suspended(squeezing_state) + .expect("a squeezing state resumes as the output"); + assert_eq!(expected, from_state.do_output(64)); + + // The phase is part of the state, so each type refuses the other's. + assert!( + matches!(X::from_suspended(squeezing_state), Err(SuspendableError::InvalidData)), + "a squeezing state must not resume as an absorbing XOF" + ); + assert!( + matches!( + X::Output::from_suspended(absorbing_state), + Err(SuspendableError::InvalidData) + ), + "an absorbing state must not resume as an output" + ); // a corrupt `squeezing` byte (last byte of the keccak state) must be rejected. // Layout: 3 version bytes + variant tag(1) + [u64;25](200) + data_queue(192) // + bits_in_queue(8) + squeezing(1) - let mut busted = serialized_state; + let mut busted = squeezing_state; busted[3 + 1 + 400] = 42; - match X::from_suspended(busted) { + match X::Output::from_suspended(busted) { Err(SuspendableError::InvalidData) => { /* good */ } _ => panic!("Expected an error for a corrupt squeezing byte"), } @@ -411,7 +313,7 @@ mod shake_tests { // variant tag). The SHAKE256 -> SHA3-256 case is the important one: they share the same rate // (1088), so only the variant tag distinguishes them. let mut shake128 = SHAKE128::new(); - shake128.absorb(str.as_bytes()).expect("absorb before squeeze is infallible"); + shake128.do_update(str.as_bytes()); let serialized_128 = shake128.suspend(); match SHAKE256::from_suspended(serialized_128) { Err(SuspendableError::InvalidData) => { /* good */ } @@ -419,7 +321,7 @@ mod shake_tests { } let mut shake256 = SHAKE256::new(); - shake256.absorb(str.as_bytes()).expect("absorb before squeeze is infallible"); + shake256.do_update(str.as_bytes()); let serialized_256 = shake256.suspend(); match SHA3_256::from_suspended(serialized_256) { Err(SuspendableError::InvalidData) => { /* good */ } @@ -446,16 +348,15 @@ mod shake_tests { let output: Vec; if partial_bits == 0 { - shake.absorb(tc.msg.as_slice()).expect("absorb before squeeze is infallible"); - output = shake.squeeze(tc.output.len()); + shake.do_update(tc.msg.as_slice()); + let mut shake = shake.into_output(); + output = shake.do_output(tc.output.len()); } else { - shake - .absorb(&tc.msg[..(tc.msg.len() - 1)]) - .expect("absorb before squeeze is infallible"); - shake - .absorb_last_partial_byte(tc.msg[tc.msg.len() - 1], partial_bits) - .expect("Absorb failed"); - output = shake.squeeze(tc.output.len()); + shake.do_update(&tc.msg[..(tc.msg.len() - 1)]); + let mut shake = shake + .into_output_partial_bits(tc.msg[tc.msg.len() - 1], partial_bits) + .expect("partial_bits is in 1..=7"); + output = shake.do_output(tc.output.len()); } assert_eq!(tc.output, output); diff --git a/mem_usage_benches/src/bench_sha3_mem_usage.rs b/mem_usage_benches/src/bench_sha3_mem_usage.rs index b08e2c3e..7a0b3c63 100644 --- a/mem_usage_benches/src/bench_sha3_mem_usage.rs +++ b/mem_usage_benches/src/bench_sha3_mem_usage.rs @@ -25,7 +25,7 @@ #![allow(dead_code)] #![allow(unused_imports)] -use bouncycastle::core::traits::{Hash, Suspendable, XOF}; +use bouncycastle::core::traits::{Hash, Suspendable, XOF, XofOutput}; use bouncycastle::sha3::{ SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN, }; @@ -85,9 +85,10 @@ fn bench_shake128_xof() { eprintln!("SHAKE128/absorb+squeeze_out"); let mut x = SHAKE128::new(); - x.absorb(&MSG).expect("absorb before squeeze is infallible"); + x.do_update(&MSG); let mut out = [0u8; 512]; - x.squeeze_out(&mut out); + let mut x = x.into_output(); + x.do_output_out(&mut out); println!("{:x?}", out); } @@ -95,9 +96,10 @@ fn bench_shake256_xof() { eprintln!("SHAKE256/absorb+squeeze_out"); let mut x = SHAKE256::new(); - x.absorb(&MSG).expect("absorb before squeeze is infallible"); + x.do_update(&MSG); let mut out = [0u8; 512]; - x.squeeze_out(&mut out); + let mut x = x.into_output(); + x.do_output_out(&mut out); println!("{:x?}", out); } From 86819d7412c94b16610c9c323580a62ca2e50679 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 15:29:01 +1000 Subject: [PATCH 02/22] sha3: pin the SHAKE block_bitlen and output_len values, which three mutants survived --- crypto/sha3/tests/shake_tests.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crypto/sha3/tests/shake_tests.rs b/crypto/sha3/tests/shake_tests.rs index 2f9fe83a..590fcc14 100644 --- a/crypto/sha3/tests/shake_tests.rs +++ b/crypto/sha3/tests/shake_tests.rs @@ -57,6 +57,27 @@ mod shake_tests { assert_ne!(b.do_output(32), SHAKE128::new().hash_xof(b"abc", 32)); } + /// The two `Hash` metadata methods, pinned to their actual values. + /// + /// The generic framework can only check that these are positive and byte-aligned, which every + /// plausible mis-derivation also satisfies -- `cargo mutants` survived three separate mutations + /// of them until this test existed. + /// + /// `block_bitlen` is the sponge rate, `1600 - 2c`: FIPS 202 Table 3 gives 1344 bits for + /// SHAKE128 and 1088 for SHAKE256. `output_len` is the nominal digest size, which BC Java's + /// `SHAKEDigest.getDigestSize()` defines as `fixedOutputLength / 4`: 32 and 64 bytes. + #[test] + fn metadata_matches_fips202_and_bc_java() { + assert_eq!(SHAKE128::new().block_bitlen(), 1344, "SHAKE128 rate, FIPS 202 Table 3"); + assert_eq!(SHAKE256::new().block_bitlen(), 1088, "SHAKE256 rate, FIPS 202 Table 3"); + assert_eq!(SHAKE128::new().output_len(), 32, "SHAKEDigest.getDigestSize() for SHAKE128"); + assert_eq!(SHAKE256::new().output_len(), 64, "SHAKEDigest.getDigestSize() for SHAKE256"); + + // and do_final actually produces that many bytes + assert_eq!(SHAKE128::new().hash(b"abc").len(), 32); + assert_eq!(SHAKE256::new().hash(b"abc").len(), 64); + } + #[test] fn test_update_bytes() { for tc in read_test_vectors("SHAKETestVectors.txt") { From 24ae9b419ee24e7dc15503ddcc81d8da053f2178 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 17:19:46 +1000 Subject: [PATCH 03/22] core: XofOutput gains do_final and do_final_out, matching BC Java's doFinal after doOutput --- crypto/core-test-framework/src/xof.rs | 29 +++++++++++++++++++++++++++ crypto/core/src/traits.rs | 26 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/crypto/core-test-framework/src/xof.rs b/crypto/core-test-framework/src/xof.rs index 46edb466..8dbf6bcb 100644 --- a/crypto/core-test-framework/src/xof.rs +++ b/crypto/core-test-framework/src/xof.rs @@ -67,6 +67,35 @@ impl TestFrameworkXOF { "successive reads must continue one stream" ); + /*** fn do_final(self, num_bytes: usize) -> Vec ***/ + // do_final reads what do_output would read at the same point; it only ends the stream. + let mut xof = X::default(); + xof.do_update(input); + assert_eq!( + xof.into_output().do_final(expected_output.len()), + expected_output, + "do_final must read what do_output reads" + ); + + // ... including part-way through a stream, not just at the start. + let mut xof = X::default(); + xof.do_update(input); + let mut out = xof.into_output(); + let head = out.do_output(split); + let tail = out.do_final(expected_output.len() - split); + assert_eq!( + [head, tail].concat(), + expected_output, + "do_final must continue the stream, not restart it" + ); + + let mut buf = vec![0xFFu8; expected_output.len()]; + let mut xof = X::default(); + xof.do_update(input); + let n = xof.into_output().do_final_out(&mut buf); + assert_eq!(n, expected_output.len()); + assert_eq!(buf, expected_output, "do_final_out must agree with do_final"); + /*** fn hash_xof(self, data: &[u8], result_len: usize) -> Vec ***/ assert_eq!( X::default().hash_xof(input, expected_output.len()), diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index f8f4f19c..052e10c9 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -1761,6 +1761,32 @@ pub trait XofOutput { /// As [`do_output`](Self::do_output), filling the caller's buffer, which is zeroized first. /// Returns the number of bytes written. fn do_output_out(&mut self, output: &mut [u8]) -> usize; + + /// The last output: produces `num_bytes` bytes and ends the stream. + /// + /// This is BC Java's `Xof.doFinal(out, outOff, outLen)` called after `doOutput`, which is + /// `doOutput` followed by `reset()` (`SHAKEDigest.java`). Here the reset is taking `self` by + /// value: the handle is gone afterwards, and dropping it zeroizes the sponge. So this is + /// exactly [`do_output`](Self::do_output) plus the end of the value's life, provided as a + /// separate name so a call site can say which read is its last. + /// + /// It reads the same bytes [`do_output`](Self::do_output) would at the same point in the + /// stream; the difference is only that nothing can follow it. + fn do_final(mut self, num_bytes: usize) -> Vec + where + Self: Sized, + { + self.do_output(num_bytes) + } + + /// As [`do_final`](Self::do_final), filling the caller's buffer, which is zeroized first. + /// Returns the number of bytes written. + fn do_final_out(mut self, output: &mut [u8]) -> usize + where + Self: Sized, + { + self.do_output_out(output) + } } /// Extendable-Output Functions (XOFs): hashes whose output length is chosen by the caller. From 517cd5902f6e77ad1a120c8c430890a1c5436174 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 18:01:37 +1000 Subject: [PATCH 04/22] sha3: add cSHAKE128 and cSHAKE256 (SP 800-185 Sec 3) with the Sec 2.3 encodings and cshake CLI subcommands --- cli/src/main.rs | 48 ++++++++ cli/src/sha3_cmd.rs | 24 +++- crypto/sha3/src/cshake.rs | 190 ++++++++++++++++++++++++++++++ crypto/sha3/src/lib.rs | 27 ++++- crypto/sha3/src/shake.rs | 72 ++++++++--- crypto/sha3/src/xof_utils.rs | 121 +++++++++++++++++++ crypto/sha3/tests/cshake_tests.rs | 187 +++++++++++++++++++++++++++++ 7 files changed, 648 insertions(+), 21 deletions(-) create mode 100644 crypto/sha3/src/cshake.rs create mode 100644 crypto/sha3/src/xof_utils.rs create mode 100644 crypto/sha3/tests/cshake_tests.rs diff --git a/cli/src/main.rs b/cli/src/main.rs index 2b26315b..fc7866c8 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -158,6 +158,48 @@ enum Subcommands { x: bool, }, + /// Perform cSHAKE128 (NIST SP 800-185) of the content provided on stdin. Requires the output + /// length in bytes. With no customization string this is exactly SHAKE128. + /// Supports streaming update for low memory footprint. + CSHAKE128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 's', long)] + /// Customization string. Two cSHAKEs with different customization strings produce + /// unrelated output, so this domain-separates one use of the function from another. + customization: Option, + + #[arg(short = 'n', long)] + /// Function-name string. Reserved by NIST for functions it defines (SP 800-185 Sec 3.4); + /// use --customization for your own domain separation. + function_name: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform cSHAKE256 (NIST SP 800-185) of the content provided on stdin. Requires the output + /// length in bytes. With no customization string this is exactly SHAKE256. + /// Supports streaming update for low memory footprint. + CSHAKE256 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 's', long)] + /// Customization string. See cshake128. + customization: Option, + + #[arg(short = 'n', long)] + /// Function-name string, reserved by NIST. See cshake128. + function_name: Option, + + #[arg(short)] + /// Output the hashes 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 @@ -1051,6 +1093,12 @@ fn main() { Some(Subcommands::SHAKE256 { length, x }) => { sha3_cmd::shake_cmd(256, *length, *x); } + Some(Subcommands::CSHAKE128 { length, customization, function_name, x }) => { + sha3_cmd::cshake_cmd(128, *length, function_name, customization, *x); + } + Some(Subcommands::CSHAKE256 { length, customization, function_name, x }) => { + sha3_cmd::cshake_cmd(256, *length, function_name, customization, *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/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index b620e9c1..1f5205aa 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -2,7 +2,9 @@ use bouncycastle::core::traits::{Hash, XOF, XofOutput}; use std::io; use std::io::{Read, Write}; -use bouncycastle::sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256}; +use bouncycastle::sha3::{ + CSHAKE128, CSHAKE256, SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, +}; pub(crate) fn sha3_cmd(bit_len: usize, output_hex: bool) { match bit_len { @@ -44,6 +46,26 @@ pub(crate) fn shake_cmd(bit_len: usize, output_len: usize, output_hex: bool) { } } +/// cSHAKE (NIST SP 800-185 Sec 3): SHAKE bound to a function name and a customization string. +/// +/// Both strings default to empty, and with both empty cSHAKE is defined to be plain SHAKE +/// (Sec 3.3 step 1), so `cshake128 32` and `shake128 32` agree. +pub(crate) fn cshake_cmd( + bit_len: usize, + output_len: usize, + function_name: &Option, + customization: &Option, + output_hex: bool, +) { + let n = function_name.as_deref().unwrap_or("").as_bytes(); + let s = customization.as_deref().unwrap_or("").as_bytes(); + match bit_len { + 128 => do_shake(CSHAKE128::new(n, s), output_len, output_hex), + 256 => do_shake(CSHAKE256::new(n, s), output_len, output_hex), + _ => panic!("Unsupported algorithm: cSHAKE-{}", bit_len), + } +} + fn do_shake(mut shake: impl XOF, output_len: usize, output_hex: bool) { let mut buf: [u8; 1024] = [0u8; 1024]; // read from stdin diff --git a/crypto/sha3/src/cshake.rs b/crypto/sha3/src/cshake.rs new file mode 100644 index 00000000..b809303e --- /dev/null +++ b/crypto/sha3/src/cshake.rs @@ -0,0 +1,190 @@ +//! cSHAKE, the customizable SHAKE of NIST SP 800-185 Sec 3. + +use crate::SHAKEParams; +use crate::shake::{SHAKEInternal, SHAKEOutput}; +use crate::xof_utils::left_encode; +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XofOutput}; + +/// The domain separator cSHAKE absorbs in place of SHAKE's `1111`: the `00` of SP 800-185 Sec 3.3, +/// two zero bits, which is what keeps a customized instance separate from plain SHAKE. +const CSHAKE_SUFFIX: (u8, usize) = (0x00, 2); + +/// Internal struct for cSHAKE. Use [`crate::CSHAKE128`] or [`crate::CSHAKE256`]. +/// +/// cSHAKE is SHAKE with two extra inputs bound to the front of the message: a function-name string +/// `N`, reserved for NIST, and a customization string `S`, chosen by the caller. SP 800-185 Sec 3.1 +/// puts it as strong typing -- two instances with different `N` or `S` produce unrelated output, so +/// a key fingerprint and an email signature computed over the same bytes cannot collide. +/// +/// # The empty case is SHAKE, exactly +/// +/// SP 800-185 Sec 3.3 step 1: when `N` and `S` are both empty, cSHAKE *is* SHAKE, including its +/// `1111` domain separator. This is a required special case, not something that falls out of the +/// general construction -- feeding empty strings through the `bytepad` branch would absorb a +/// non-empty prefix and use a different separator, giving a different function. [`Self::new`] +/// branches on it, and there is a test that the two agree. +pub struct CSHAKEInternal { + shake: SHAKEInternal, + /// False when `N` and `S` are both empty, in which case this is plain SHAKE. + customized: bool, +} + +impl Algorithm for CSHAKEInternal { + const ALG_NAME: &'static str = PARAMS::CSHAKE_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl CSHAKEInternal { + /// A new cSHAKE bound to the function name `n` and customization string `s`. + /// + /// Both may be empty; if both are, this is plain SHAKE (Sec 3.3 step 1). + /// + /// `n` is reserved for NIST-defined functions -- Sec 3.4 asks callers not to invent their own, + /// because a value NIST later assigns would then collide. Customization belongs in `s`. + pub fn new(n: &[u8], s: &[u8]) -> Self { + let mut shake = SHAKEInternal::::new(); + let customized = !n.is_empty() || !s.is_empty(); + if customized { + // Sec 3.3: bytepad(encode_string(N) || encode_string(S), rate). Absorbed rather than + // built in a buffer, so no allocation and no bound on the length of N or S. + let rate = PARAMS::RATE_BYTES; + let mut written = absorb_left_encode(&mut shake, rate as u64); + written += absorb_encoded_string(&mut shake, n); + written += absorb_encoded_string(&mut shake, s); + // ... then zero bytes up to a whole number of rate-sized blocks. + absorb_zeros(&mut shake, written.next_multiple_of(rate) - written); + } + Self { shake, customized } + } +} + +/// Absorbs `left_encode(value)`, returning how many bytes went in. +fn absorb_left_encode(shake: &mut SHAKEInternal, value: u64) -> usize { + let (buf, len) = left_encode(value); + shake.do_update(&buf[..len]); + len +} + +/// Absorbs `encode_string(s)` -- `left_encode(len(s))` then `s` -- returning how many bytes went +/// in. SP 800-185 Sec 2.3.2 counts the length in bits. +fn absorb_encoded_string( + shake: &mut SHAKEInternal, + s: &[u8], +) -> usize { + let n = absorb_left_encode(shake, (s.len() as u64) * 8); + shake.do_update(s); + n + s.len() +} + +/// Absorbs `count` zero bytes, the padding of `bytepad` (Sec 2.3.3 step 3). +fn absorb_zeros(shake: &mut SHAKEInternal, mut count: usize) { + const ZEROS: [u8; 64] = [0u8; 64]; + while count > 0 { + let n = count.min(ZEROS.len()); + shake.do_update(&ZEROS[..n]); + count -= n; + } +} + +impl Default for CSHAKEInternal { + /// An uncustomized cSHAKE, which by Sec 3.3 step 1 is plain SHAKE. + fn default() -> Self { + Self::new(&[], &[]) + } +} + +impl Hash for CSHAKEInternal { + fn block_bitlen(&self) -> usize { + self.shake.block_bitlen() + } + + fn output_len(&self) -> usize { + self.shake.output_len() + } + + fn hash(self, data: &[u8]) -> Vec { + let n = self.output_len(); + let mut out = vec![0u8; n]; + self.hash_out(data, &mut out); + out + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.into_output().do_output_out(output) + } + + fn do_update(&mut self, data: &[u8]) { + self.shake.do_update(data); + } + + fn do_final(self) -> Vec { + let n = self.output_len(); + self.into_output().do_output(n) + } + + fn do_final_out(self, output: &mut [u8]) -> usize { + self.into_output().do_output_out(output) + } + + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let mut out = vec![0u8; self.output_len()]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } + + fn do_final_partial_bits_out( + self, + partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + Ok(self.into_output_partial_bits(partial_byte, num_bits)?.do_output_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + Hash::max_security_strength(&self.shake) + } +} + +impl XOF for CSHAKEInternal { + type Output = SHAKEOutput; + + fn into_output(self) -> Self::Output { + if self.customized { + let (suffix, bits) = CSHAKE_SUFFIX; + self.shake.into_output_with_suffix(suffix, bits) + } else { + // Sec 3.3 step 1: with no N and no S this is SHAKE, separator included. + self.shake.into_output() + } + } + + fn into_output_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result { + if self.customized { + let (suffix, bits) = CSHAKE_SUFFIX; + self.shake.into_output_partial_bits_with_suffix(partial_byte, num_bits, suffix, bits) + } else { + self.shake.into_output_partial_bits(partial_byte, num_bits) + } + } + + fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { + self.do_update(data); + self.into_output().do_output(result_len) + } + + fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.into_output().do_output_out(output) + } +} diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 695f73c0..937a439d 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -201,9 +201,11 @@ use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::traits::{Hash, KDF, MAC, Suspendable, XOF}; // end of doc-only imports +mod cshake; mod keccak; mod sha3; mod shake; +mod xof_utils; pub mod hmac; @@ -220,10 +222,26 @@ pub const SHA3_512_NAME: &str = "SHA3-512"; pub const SHAKE128_NAME: &str = "SHAKE128"; /// Algorithm name string for SHAKE256, as used by the factories and CLI. pub const SHAKE256_NAME: &str = "SHAKE256"; +/// The name of the cSHAKE128 algorithm (NIST SP 800-185 Sec 3). +pub const CSHAKE128_NAME: &str = "CSHAKE128"; +/// The name of the cSHAKE256 algorithm (NIST SP 800-185 Sec 3). +pub const CSHAKE256_NAME: &str = "CSHAKE256"; /*** pub types ***/ +pub use cshake::CSHAKEInternal; pub use sha3::SHA3Internal; -pub use shake::SHAKEInternal; + +/// cSHAKE128: the customizable SHAKE128 of NIST SP 800-185 Sec 3, at a 128-bit security strength. +/// +/// Construct with [`CSHAKEInternal::new`], passing the function-name string `N` (reserved for +/// NIST, normally empty) and the customization string `S`. With both empty this is exactly +/// [`SHAKE128`]. +pub type CSHAKE128 = CSHAKEInternal; +/// cSHAKE256: the customizable SHAKE256 of NIST SP 800-185 Sec 3, at a 256-bit security strength. +/// +/// See [`CSHAKE128`]. +pub type CSHAKE256 = CSHAKEInternal; +pub use shake::{SHAKEInternal, SHAKEOutput}; pub use keccak::SUSPENDED_SHA3_STATE_LEN; @@ -350,6 +368,11 @@ trait SHAKEParams: Algorithm { const SIZE: KeccakSize; /// See [`SHA3Params::STATE_TAG`]. Must be distinct from every SHA3 *and* SHAKE variant's tag. const STATE_TAG: u8; + /// The sponge rate in bytes: `(1600 - 2c) / 8`, 168 for SHAKE128 and 136 for SHAKE256. + /// SP 800-185 Sec 3.3 pads cSHAKE's encoded strings to a multiple of it. + const RATE_BYTES: usize = (1600 - ((Self::SIZE as usize) << 1)) / 8; + /// The name of the cSHAKE built on this parameter set. + const CSHAKE_ALG_NAME: &'static str; } /// The parameters for SHAKE128. #[derive(Clone)] @@ -361,6 +384,7 @@ impl Algorithm for SHAKE128Params { impl SHAKEParams for SHAKE128Params { const SIZE: KeccakSize = KeccakSize::_128; const STATE_TAG: u8 = 5; + const CSHAKE_ALG_NAME: &'static str = CSHAKE128_NAME; } /// Assigned by NIST in the Computer Security Objects Register: id-shake128 { hashAlgs 11 } impl AlgorithmOID for SHAKE128 { @@ -378,6 +402,7 @@ impl Algorithm for SHAKE256Params { impl SHAKEParams for SHAKE256Params { const SIZE: KeccakSize = KeccakSize::_256; const STATE_TAG: u8 = 6; + const CSHAKE_ALG_NAME: &'static str = CSHAKE256_NAME; } /// Assigned by NIST in the Computer Security Objects Register: id-shake256 { hashAlgs 12 } impl AlgorithmOID for SHAKE256 { diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index 3c02a33d..ec6c3bec 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -65,6 +65,25 @@ impl SHAKEInternal { self.into_output().do_output_out(output) } + /// Ends absorbing with a caller-chosen domain separator and returns the squeezing half. + /// + /// SHAKE uses "1111" (FIPS 202 s. 6.2), but cSHAKE uses "00" (SP 800-185 s. 3.3, the `00` in + /// the `KECCAK[c](... || X || 00, L)` branch), so the suffix cannot be baked in here. Crate + /// internal: callers outside pick a function, and the function picks its own separator. + /// + /// Infallible for the same reason [`Hash::do_update`] is: a `SHAKEInternal` a caller can name + /// has never squeezed, so the queue is byte-aligned and `absorb_bits` cannot reject it. + pub(crate) fn into_output_with_suffix( + mut self, + suffix: u8, + num_bits: usize, + ) -> SHAKEOutput { + self.keccak + .absorb_bits(suffix, num_bits) + .expect("a sponge that has not squeezed can absorb a domain separator"); + SHAKEOutput { shake: self } + } + /// Produces the next bytes of the output stream, applying the SHAKE "1111" domain separator /// (FIPS 202 s. 6.2) on the first call. Reached only through [`SHAKEOutput`], so the caller /// cannot interleave this with absorbing. @@ -445,19 +464,43 @@ impl Hash for SHAKEInternal { impl XOF for SHAKEInternal { type Output = SHAKEOutput; - fn into_output(mut self) -> Self::Output { - // The SHAKE domain separator, "1111" (FIPS 202 s. 6.2), applied as the sponge switches to - // squeezing. Infallible: this value has never squeezed (see `do_update`), so the queue is - // byte-aligned and `absorb_bits` cannot reject it. - self.keccak.absorb_bits(0x0F, 4).expect("a SHAKE that has not squeezed can absorb bits"); - SHAKEOutput { shake: self } + fn into_output(self) -> Self::Output { + // The SHAKE domain separator, "1111" (FIPS 202 s. 6.2). + self.into_output_with_suffix(0x0F, 4) } fn into_output_partial_bits( - mut self, + self, partial_byte: u8, num_bits: usize, ) -> Result { + // The SHAKE domain separator, "1111" (FIPS 202 s. 6.2). + self.into_output_partial_bits_with_suffix(partial_byte, num_bits, 0x0F, 4) + } + + fn hash_xof(self, data: &[u8], result_len: usize) -> Vec { + self.hash_internal(data, result_len) + } + + fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { + // hash_internal_out zeroizes `output` before writing. + self.hash_internal_out(data, output) + } +} + +impl SHAKEInternal { + /// [`XOF::into_output_partial_bits`] with a caller-chosen domain separator, for cSHAKE. + /// + /// The message's trailing bits and the separator are absorbed together, so the separator + /// cannot simply be applied afterwards -- hence the suffix travels in rather than being + /// hardcoded. See [`Self::into_output_with_suffix`]. + pub(crate) fn into_output_partial_bits_with_suffix( + mut self, + partial_byte: u8, + num_bits: usize, + suffix: u8, + suffix_bits: usize, + ) -> Result, HashError> { // A partial byte has at most 7 bits; 0 means the message ends on a byte boundary. // Checked before any state change, so a rejected call leaves the sponge untouched. if num_bits > 7 { @@ -469,8 +512,8 @@ impl XOF for SHAKEInternal { // LSB-first: FIPS 202 Algorithm 10 (h2b) step 3 sets message bit T[8i + j] = b_ij, the bit // of weight 2^j in byte i. So reverse the bit order and keep the low num_bits bits. let message_bits = (partial_byte.reverse_bits() as u16) & ((1 << num_bits) - 1); - let mut final_input: u16 = message_bits | (0x0F << num_bits); - let mut final_bits = num_bits + 4; + let mut final_input: u16 = message_bits | ((suffix as u16) << num_bits); + let mut final_bits = num_bits + suffix_bits; if final_bits >= 8 { self.keccak.absorb(&[final_input as u8]); @@ -482,17 +525,8 @@ impl XOF for SHAKEInternal { // is in 0..=7 by construction. self.keccak.absorb_bits(final_input as u8, final_bits).expect("Absorb failed."); - // The "1111" suffix is already folded into final_input above, so the sponge is finished + // The suffix is already folded into final_input above, so the sponge is finished // absorbing; wrap it without applying the suffix a second time. Ok(SHAKEOutput { shake: self }) } - - fn hash_xof(self, data: &[u8], result_len: usize) -> Vec { - self.hash_internal(data, result_len) - } - - fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { - // hash_internal_out zeroizes `output` before writing. - self.hash_internal_out(data, output) - } } diff --git a/crypto/sha3/src/xof_utils.rs b/crypto/sha3/src/xof_utils.rs new file mode 100644 index 00000000..6322e0f1 --- /dev/null +++ b/crypto/sha3/src/xof_utils.rs @@ -0,0 +1,121 @@ +//! The integer and string encodings of NIST SP 800-185 Sec 2.3. +//! +//! These are shared by every SHA-3-derived function in the Recommendation: cSHAKE uses +//! `encode_string` and `bytepad` to bind its function-name and customization strings, and KMAC and +//! TupleHash add `right_encode` to bind the key and the requested output length. +//! +//! Lengths in the Recommendation are counted in **bits**, while this crate's API is byte-oriented, +//! so callers pass byte counts and the helpers multiply where the spec says `len(S)`. + +/// The widest encoding these functions produce: a length byte plus up to eight value bytes. +/// +/// SP 800-185 Sec 2.3.1 permits integers up to `2^2040 - 1`, which would need 255 value bytes. A +/// `u64` covers every length this library can be handed -- an input of `2^64` bits is 2 exabytes -- +/// so the buffer is sized for that rather than for the spec's theoretical maximum. +pub(crate) const MAX_ENCODED_LEN: usize = 9; + +/// `left_encode(x)`: SP 800-185 Sec 2.3.1. +/// +/// Encodes `value` so that it can be parsed unambiguously *from the beginning*: the number of +/// value bytes comes first, then the value itself, big-endian. Returns the buffer and how much of +/// it is used. +/// +/// The spec's example: `left_encode(0)` is `10000000 00000000`, which in this document's +/// low-order-bit-first notation is the bytes `01 00`. +pub(crate) fn left_encode(value: u64) -> ([u8; MAX_ENCODED_LEN], usize) { + let mut buf = [0u8; MAX_ENCODED_LEN]; + // Step 1: n is the smallest positive integer with 2^(8n) > value. Zero still takes one byte, + // which is why the count starts at 1 rather than 0. + let n = value_bytes(value); + buf[0] = n as u8; + // Steps 2-4: the base-256 digits of value, most significant first. + for i in 0..n { + buf[1 + i] = (value >> (8 * (n - 1 - i))) as u8; + } + (buf, n + 1) +} + +/// `right_encode(x)`: SP 800-185 Sec 2.3.1. +/// +/// Unused until KMAC and TupleHash land, which bind the requested output length with it. +/// +/// As [`left_encode`], but the length byte comes *last*, so the encoding can be parsed from the end +/// of a string. The spec's example: `right_encode(0)` is the bytes `00 01`. +#[allow(dead_code)] // used by KMAC and TupleHash +pub(crate) fn right_encode(value: u64) -> ([u8; MAX_ENCODED_LEN], usize) { + let mut buf = [0u8; MAX_ENCODED_LEN]; + let n = value_bytes(value); + for i in 0..n { + buf[i] = (value >> (8 * (n - 1 - i))) as u8; + } + buf[n] = n as u8; + (buf, n + 1) +} + +/// The number of base-256 digits in `value`: the spec's `n`, the smallest positive integer with +/// `2^(8n) > value`. Positive, so zero encodes as one byte. +fn value_bytes(value: u64) -> usize { + let mut n = 1; + let mut v = value; + while { + v >>= 8; + v != 0 + } { + n += 1; + } + n +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The two worked examples in SP 800-185 Sec 2.3.1, in the byte spelling of Sec 2 + /// ("bytes are written with the low-order bit first" in binary, high-order digit first in hex). + #[test] + fn spec_examples() { + let (b, n) = right_encode(0); + assert_eq!(&b[..n], &[0x00, 0x01], "right_encode(0) = 00000000 10000000"); + + let (b, n) = left_encode(0); + assert_eq!(&b[..n], &[0x01, 0x00], "left_encode(0) = 10000000 00000000"); + } + + /// The encodings that appear in the NIST cSHAKE sample file: `left_encode(168)` opens the + /// bytepad block, and `left_encode(120)` prefixes the 15-character "Email Signature". + #[test] + fn cshake_sample_encodings() { + let (b, n) = left_encode(168); + assert_eq!(&b[..n], &[0x01, 0xA8], "left_encode(168), the cSHAKE128 rate"); + + let (b, n) = left_encode(120); + assert_eq!(&b[..n], &[0x01, 0x78], "left_encode(15 * 8), for \"Email Signature\""); + } + + /// The length byte grows with the value, and the value is big-endian after it. + #[test] + fn multi_byte_values() { + let (b, n) = left_encode(0x0100); + assert_eq!(&b[..n], &[0x02, 0x01, 0x00]); + let (b, n) = right_encode(0x0100); + assert_eq!(&b[..n], &[0x01, 0x00, 0x02]); + + let (b, n) = left_encode(u64::MAX); + assert_eq!(&b[..n], &[0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); + let (b, n) = right_encode(u64::MAX); + assert_eq!(&b[..n], &[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x08]); + } + + /// Every boundary where the number of value bytes increases. + #[test] + fn byte_count_boundaries() { + for n in 1..=8u32 { + let just_under = if n == 8 { u64::MAX } else { (1u64 << (8 * n)) - 1 }; + assert_eq!(left_encode(just_under).1, n as usize + 1, "2^{} - 1", 8 * n); + assert_eq!(right_encode(just_under).1, n as usize + 1, "2^{} - 1", 8 * n); + if n < 8 { + assert_eq!(left_encode(1u64 << (8 * n)).1, n as usize + 2, "2^{}", 8 * n); + } + } + } +} diff --git a/crypto/sha3/tests/cshake_tests.rs b/crypto/sha3/tests/cshake_tests.rs new file mode 100644 index 00000000..32783850 --- /dev/null +++ b/crypto/sha3/tests/cshake_tests.rs @@ -0,0 +1,187 @@ +//! cSHAKE against the NIST SP 800-185 sample values. +//! +//! The vectors live in the `bc-test-data` repo, which must be cloned alongside this one at +//! `../bc-test-data` (the same convention as the ML-KEM, ML-DSA and SHA-3 suites). If it is not +//! present these tests print a warning and pass vacuously. + +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XofOutput}; +use bouncycastle_hex as hex; +use bouncycastle_sha3::{CSHAKE128, CSHAKE256, SHAKE128, SHAKE256}; +use std::fs; +use std::path::Path; + +/// One `COUNT` block of a `.rsp` file. +struct Vector { + strength: usize, + n: String, + s: String, + output_len: usize, + msg: Vec, + output: Vec, +} + +fn read_vectors(filename: &str) -> Option> { + let path = Path::new("../../../bc-test-data/crypto/sp800-185").join(filename); + let Ok(content) = fs::read_to_string(&path) else { + println!( + "warning: {} not found; skipping. Clone bc-test-data alongside this repo.", + path.display() + ); + return None; + }; + + let mut out = Vec::new(); + let mut cur: Vec<(String, String)> = Vec::new(); + let finish = |cur: &mut Vec<(String, String)>, out: &mut Vec| { + if cur.is_empty() { + return; + } + let get = |k: &str| cur.iter().find(|(a, _)| a == k).map(|(_, b)| b.clone()); + out.push(Vector { + strength: get("Strength").expect("Strength").parse().expect("a number"), + n: get("N").unwrap_or_default(), + s: get("S").unwrap_or_default(), + output_len: get("Outputlen").expect("Outputlen").parse().expect("a number"), + msg: hex::decode(get("Msg").unwrap_or_default()).expect("hex"), + output: hex::decode(get("Output").expect("Output")).expect("hex"), + }); + cur.clear(); + }; + + for line in content.lines() { + let line = line.trim_end(); + if line.starts_with('#') || line.is_empty() { + continue; + } + let Some((k, v)) = line.split_once(" = ") else { continue }; + if k == "COUNT" { + finish(&mut cur, &mut out); + } else { + cur.push((k.to_string(), v.to_string())); + } + } + finish(&mut cur, &mut out); + Some(out) +} + +/// Every published cSHAKE sample value, at both strengths. +#[test] +fn nist_sp800_185_sample_values() { + let Some(vectors) = read_vectors("cSHAKE.rsp") else { return }; + assert!(!vectors.is_empty(), "the vector file must not be empty"); + + for (i, v) in vectors.iter().enumerate() { + assert!(v.output_len.is_multiple_of(8), "COUNT {i}: byte-aligned outputs only"); + let want = v.output_len / 8; + + let got = match v.strength { + 128 => { + let mut c = CSHAKE128::new(v.n.as_bytes(), v.s.as_bytes()); + c.do_update(&v.msg); + c.into_output().do_output(want) + } + 256 => { + let mut c = CSHAKE256::new(v.n.as_bytes(), v.s.as_bytes()); + c.do_update(&v.msg); + c.into_output().do_output(want) + } + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!(got, v.output, "COUNT {i}: cSHAKE{} S={:?}", v.strength, v.s); + } + println!("cSHAKE: {} sample values", vectors.len()); +} + +/// SP 800-185 Sec 3.3 step 1: with `N` and `S` both empty, cSHAKE *is* SHAKE. +/// +/// This is a special case in the definition rather than a consequence of the general construction: +/// the customized branch absorbs a `bytepad` prefix and uses the `00` domain separator, where SHAKE +/// absorbs nothing and uses `1111`. Getting it wrong would leave cSHAKE self-consistent but +/// incompatible with SHAKE, which no sample value would catch, since every published sample has a +/// non-empty `S`. +#[test] +fn empty_name_and_customization_is_plain_shake() { + for msg in [b"".as_slice(), b"abc", &[0u8; 200], b"Hello, world!"] { + for len in [1usize, 16, 32, 168, 200] { + assert_eq!( + CSHAKE128::new(b"", b"").hash_xof(msg, len), + SHAKE128::new().hash_xof(msg, len), + "cSHAKE128 with no N or S must equal SHAKE128 / len {len}" + ); + assert_eq!( + CSHAKE256::new(b"", b"").hash_xof(msg, len), + SHAKE256::new().hash_xof(msg, len), + "cSHAKE256 with no N or S must equal SHAKE256 / len {len}" + ); + } + } +} + +/// Sec 3.1: two instances with different `N` or `S` must produce unrelated output. That is the +/// whole point of customization, so a customized instance must also differ from plain SHAKE. +#[test] +fn customization_separates_the_functions() { + let msg = b"the same message"; + let plain = SHAKE128::new().hash_xof(msg, 32); + let email = CSHAKE128::new(b"", b"Email Signature").hash_xof(msg, 32); + let finger = CSHAKE128::new(b"", b"key fingerprint").hash_xof(msg, 32); + let named = CSHAKE128::new(b"KMAC", b"").hash_xof(msg, 32); + + assert_ne!(plain, email, "a customized cSHAKE must differ from SHAKE"); + assert_ne!(email, finger, "different S must give unrelated output"); + assert_ne!(plain, named, "a function name alone must customize"); + assert_ne!(email, named, "N and S must not be interchangeable"); +} + +/// `N` and `S` are separate inputs, and `encode_string` length-prefixes each, so moving bytes from +/// one to the other must change the result. Without the prefixes, ("AB", "") and ("A", "B") would +/// collide -- the ambiguity Sec 2.3.2 exists to prevent. +#[test] +fn the_boundary_between_n_and_s_is_unambiguous() { + let msg = b"x"; + assert_ne!( + CSHAKE128::new(b"AB", b"").hash_xof(msg, 32), + CSHAKE128::new(b"A", b"B").hash_xof(msg, 32), + "the split between N and S must be part of the computation" + ); +} + +/// Chunked input must equal a single update, and the output must be one continuous stream. +#[test] +fn streaming_matches_one_shot() { + let msg: Vec = (0..=255u8).collect(); + let one = CSHAKE128::new(b"", b"Email Signature").hash_xof(&msg, 64); + + let mut c = CSHAKE128::new(b"", b"Email Signature"); + for chunk in msg.chunks(7) { + c.do_update(chunk); + } + let mut out = c.into_output(); + let head = out.do_output(20); + let tail = out.do_final(44); + assert_eq!([head, tail].concat(), one, "chunked in, split out, must equal the one-shot"); +} + +/// cSHAKE is a `Hash`, so `do_final` gives the nominal digest size and is a prefix of the stream. +#[test] +fn cshake_is_a_hash() { + let mut c = CSHAKE128::new(b"", b"Email Signature"); + c.do_update(b"abc"); + let digest = c.do_final(); + assert_eq!(digest.len(), 32, "cSHAKE128's nominal output length"); + assert_eq!(CSHAKE128::new(b"", b"Email Signature").hash(b"abc"), digest); + + let long = CSHAKE128::new(b"", b"Email Signature").hash_xof(b"abc", 64); + assert_eq!(&long[..32], &digest[..], "do_final must be a prefix of the longer output"); + + let mut c = CSHAKE256::new(b"", b"Email Signature"); + c.do_update(b"abc"); + assert_eq!(c.do_final().len(), 64, "cSHAKE256's nominal output length"); +} + +/// The algorithm names, so the factory and any registry agree with the specification's spelling. +#[test] +fn algorithm_names() { + assert_eq!(CSHAKE128::ALG_NAME, "CSHAKE128"); + assert_eq!(CSHAKE256::ALG_NAME, "CSHAKE256"); +} From 61d3d8eece82dba4c1d32950536535e196568843 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 18:13:38 +1000 Subject: [PATCH 05/22] docs: record the cargo mutants scoping flags, the bc-test-data conventions and the commit message style in CLAUDE.md --- CLAUDE.md | 21 ++++++++++++++++++++- crypto/sha3/tests/cshake_tests.rs | 17 +++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 66c3592f..2a285ddc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -158,7 +158,15 @@ Rules when working from the downloaded copy: - **Quote exactly, and locate precisely.** Comments and commit messages should name the document with its revision (e.g. "FIPS 203, Algorithm 13 (ML-KEM.Encaps_internal), step 2", "RFC 5869 Β§2.2"), and quote the spec verbatim where a quote is clearer than a paraphrase. Verify every section/algorithm/step number against the file you just downloaded β€” including numbers already present in the code, which may predate a spec revision. - **The specification is the source of truth for correct behaviour** β€” not the C/Java/Go implementation you have seen, not the BC Java or BC C# port, and not another crate. When an existing implementation appears to disagree with the spec, re-read the spec, and if the disagreement is real, follow the spec and note the discrepancy in the PR description rather than silently copying the other implementation. - **Optimizations are allowed, provided externally-visible behaviour is identical.** Restructuring loops, fusing steps, precomputing tables, constant-time rewrites, and in-place buffer reuse are all fine β€” the spec constrains observable outputs (and, for this library, timing behaviour on secret data), not the shape of the code. Any such deviation from the spec's literal steps gets a comment saying which spec steps it implements and why it is equivalent. -- **Test vectors come from the spec or its official companion files** (NIST CAVP / ACVP vectors, RFC test-vector appendices), downloaded the same way. Never hand-write an "expected" value from recall. +- **Test vectors come from the spec or its official companion files** (NIST CAVP / ACVP vectors, RFC test-vector appendices, the NIST "Examples with Intermediate Values" sample files). Never hand-write an "expected" value from recall. + +### Test vector data + +Vectors live in the **`bc-test-data`** repo, cloned alongside this one at `../bc-test-data`; suites read from it by relative path and print a warning and pass vacuously if it is absent (see `crypto/sha3/tests/cavp_tests.rs` for the pattern). Symlink it to `/tmp/bc-test-data` before running `cargo mutants`, whose build directories are elsewhere. + +- Commit the vectors there, not here, and not as PDFs β€” that repo holds `.rsp`, `.txt` and `.json`, and has no PDFs at all. Extract what a harness needs into the CAVP-style `.rsp` shape already used by `crypto/sha3/`. +- Every new directory gets a `README.md` giving provenance: upstream URL, licence or copyright status, retrieval date, and the SHA-256 of each source document so a refresh can be checked. `crypto/wycheproof/` and `crypto/sp800-185/` are the examples. +- **Validate an extraction against declared lengths, not just that it parses.** NIST sample-value PDFs split hex blocks across page boundaries, and the continuation line then begins with a form feed rather than spaces, so an "indented hex lines" pattern stops at the break and silently truncates. The result is still well-formed hex. Check each value against the length the file states (`Outputlen`, `Length of data is`, `Length of Key is`), and cross-check against BC Java's expected values where an equivalent test exists. ## Notes on testing @@ -168,9 +176,20 @@ external vector suites β€” is specified in QUALITY_AND_STYLE.md and CONTRIBUTING - `cargo mutants` is expected to be run on each crate; surviving mutants must be investigated but not all need to die (e.g. XOR/OR equivalences in crypto code are acceptable). Config lives in `.cargo/mutants.toml` (output dir `custom_mutants_output/`). - Integration tests in `tests/` are preferred over in-file `#[cfg(test)] mod tests` blocks β€” see "Unit tests vs integration tests" in QUALITY_AND_STYLE.md for the reasoning and the exceptions. A unit test is justified for high-risk code that has known-answer values and cannot be reached through the public API; when you write one, all of its helpers go inside that `mod tests`. - A property that can be asserted at compile time (`const _: () = assert!(...)`) stays a compile-time assertion even when a test also covers it: `cargo mutants` cannot see a const assertion fail, so pair the two rather than trading the guarantee for the coverage. +- Scoping a mutation run: **`--file` is silently ignored** by the installed cargo-mutants β€” it accepts the flag, filters nothing, and runs the whole package, so a run reported as covering one file may have covered the crate. Use **`-F `**, which matches the mutant names `--list` prints, and confirm the scope with `--list` first. `--test-workspace` needs an explicit value (`--test-workspace=true`), and is required whenever the mutated code is a `core` trait used by other crates. +- `--in-diff` finds nothing for a change that is mostly trait declarations, renamed call sites and documentation, because the executable code in impl bodies is unchanged. File-scoped runs are the useful gate for that shape of change; do not read "no mutants to filter" as "nothing to test". +- Behaviour-critical private functions can use in-file `#[cfg(test)] mod tests` blocks when they can't be exercised from outside the crate. - For traits in `core`, the canonical tests live in `core-test-framework` and are invoked from each implementor's integration tests β€” don't duplicate them per-implementation. - The per-width `impl Condition` blocks in `crypto/utils/src/ct.rs` (and their test modules) are deliberately duplicated rather than macro-generated: `cargo mutants` cannot see into `macro_rules!` bodies, so a macro would hide the mask identities from mutation testing. Do not fold them back into a macro. Any change to one width in a group (i64/i32, u64/u32) must be applied to every width in that group. +## Commit messages + +One-line subject only: no body, no "Squashed commits" list, and **no `Co-Authored-By` trailer**. This overrides the usual default of adding one. It applies on the release branches and on feature branches alike, so `git commit -m ""` is the whole of it β€” put in the subject what the body would have said. + +Subjects are `: `, and a change spanning several crates is normally split into one commit per crate, including that crate's factory and CLI wiring. Split only where each commit still builds: a trait change that every implementor must follow cannot be split that way and belongs in one commit. + +Do not strip `Co-Authored-By` from commits written in earlier sessions when rewording them during a rebase β€” that removes someone else's attribution. + ## CI The only workflow is `.github/workflows/publish_doc_benches_to_ghpages.yaml`: on every PR it builds rustdoc and runs `quality_stats.sh`; on `main` it additionally runs `cargo bench --all` and publishes docs, code stats, and benchmark results to GitHub Pages (`https://bcgit.github.io/bc-rust/`). There is no separate CI test/lint job β€” local `cargo test --workspace` is the gate, and nothing but a developer running it stands between a broken test and `main`. \ No newline at end of file diff --git a/crypto/sha3/tests/cshake_tests.rs b/crypto/sha3/tests/cshake_tests.rs index 32783850..346f6195 100644 --- a/crypto/sha3/tests/cshake_tests.rs +++ b/crypto/sha3/tests/cshake_tests.rs @@ -20,15 +20,20 @@ struct Vector { output: Vec, } +/// Two candidates, as in `cavp_tests.rs`: the first is relative to the crate directory (where cargo +/// runs an integration test), the second to the workspace root. +const DATA_DIRS: [&str; 2] = + ["../../../bc-test-data/crypto/sp800-185", "../bc-test-data/crypto/sp800-185"]; + fn read_vectors(filename: &str) -> Option> { - let path = Path::new("../../../bc-test-data/crypto/sp800-185").join(filename); - let Ok(content) = fs::read_to_string(&path) else { - println!( - "warning: {} not found; skipping. Clone bc-test-data alongside this repo.", - path.display() - ); + let Some(dir) = DATA_DIRS.into_iter().find(|d| Path::new(d).exists()) else { + println!("WARNING: bc-test-data not found; cSHAKE sample-value tests skipped"); return None; }; + let path = Path::new(dir).join(filename); + let content = fs::read_to_string(&path).unwrap_or_else(|e| { + panic!("bc-test-data is present but {} is unreadable: {e}", path.display()) + }); let mut out = Vec::new(); let mut cur: Vec<(String, String)> = Vec::new(); From 63ca4335263caf840f1294e16c9233e59bf42e12 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 18:22:09 +1000 Subject: [PATCH 06/22] sha3: add KMAC128 and KMAC256 (SP 800-185 Sec 4) with KMACXOF, MACFactory registration and kmac CLI subcommands --- cli/src/mac_cmd.rs | 52 ++++++- cli/src/main.rs | 61 ++++++++ crypto/factory/src/mac_factory.rs | 28 ++++ crypto/sha3/src/cshake.rs | 35 ++++- crypto/sha3/src/kmac.rs | 175 ++++++++++++++++++++++ crypto/sha3/src/lib.rs | 21 +++ crypto/sha3/tests/kmac_tests.rs | 238 ++++++++++++++++++++++++++++++ 7 files changed, 594 insertions(+), 16 deletions(-) create mode 100644 crypto/sha3/src/kmac.rs create mode 100644 crypto/sha3/tests/kmac_tests.rs diff --git a/cli/src/mac_cmd.rs b/cli/src/mac_cmd.rs index f80fa0ba..e9a3f82e 100644 --- a/cli/src/mac_cmd.rs +++ b/cli/src/mac_cmd.rs @@ -8,6 +8,7 @@ use bouncycastle::core::key_material::{ use bouncycastle::core::traits::MAC; use bouncycastle::hex; use bouncycastle::sha2::hmac::{HMAC_SHA256, HMAC_SHA512, HMAC_SHA512_224, HMAC_SHA512_256}; +use bouncycastle::sha3::{KMAC128, KMAC256}; use bouncycastle::sm3::hmac::HMAC_SM3; #[allow(non_camel_case_types)] @@ -19,14 +20,8 @@ pub(crate) enum HMACVariant { SM3, } -pub(crate) fn mac_cmd( - hmac_variant: HMACVariant, - key: &Option, - key_file: &Option, - verify_val: &Option, - output_hex: bool, -) { - // load the key +/// Loads a MAC key from `--key` (hex) or `--key-file` (raw), tagged as a MAC key. +fn load_mac_key(key: &Option, key_file: &Option) -> KeyMaterial512 { let key_bytes: Vec = if key.is_some() { hex::decode(key.as_ref().unwrap()).unwrap() } else if key_file.is_some() { @@ -42,6 +37,17 @@ pub(crate) fn mac_cmd( } let mut key = KeyMaterial512::from_bytes(&key_bytes).unwrap(); do_hazardous_operations(&mut key, |key| key.set_key_type(KeyType::MACKey)).unwrap(); + key +} + +pub(crate) fn mac_cmd( + hmac_variant: HMACVariant, + key: &Option, + key_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + let key = load_mac_key(key, key_file); // instantiate the MAC object and call do_mac() match hmac_variant { @@ -68,6 +74,36 @@ 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, + length: usize, + customization: &Option, + key: &Option, + key_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + let key = load_mac_key(key, key_file); + let s = customization.as_deref().unwrap_or("").as_bytes(); + // new_allow_weak_key, as the HMAC commands do: 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 => do_mac( + KMAC128::new_with_params(&key, s, length, true).expect("a valid MAC key"), + verify_val, + output_hex, + ), + 256 => do_mac( + KMAC256::new_with_params(&key, s, length, true).expect("a valid MAC key"), + verify_val, + output_hex, + ), + _ => panic!("Unsupported algorithm: KMAC-{bit_len}"), + } +} + 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]; diff --git a/cli/src/main.rs b/cli/src/main.rs index fc7866c8..6257325b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -158,6 +158,61 @@ 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. + KMAC128 { + /// Length of the tag in bytes. + length: usize, + + #[arg(short = 's', long)] + /// Customization string, domain-separating this use of KMAC from another. + customization: Option, + + #[arg(short, long)] + /// The key, in hex. + key: Option, + + #[arg(long)] + /// File containing the key, as raw bytes. + 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. + KMAC256 { + /// Length of the tag in bytes. + length: usize, + + #[arg(short = 's', long)] + /// Customization string, domain-separating this use of KMAC from another. + customization: Option, + + #[arg(short, long)] + /// The key, in hex. + key: Option, + + #[arg(long)] + /// File containing the key, as raw bytes. + 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, + }, + /// Perform cSHAKE128 (NIST SP 800-185) of the content provided on stdin. Requires the output /// length in bytes. With no customization string this is exactly SHAKE128. /// Supports streaming update for low memory footprint. @@ -1096,6 +1151,12 @@ fn main() { Some(Subcommands::CSHAKE128 { length, customization, function_name, x }) => { sha3_cmd::cshake_cmd(128, *length, function_name, customization, *x); } + Some(Subcommands::KMAC128 { length, customization, key, key_file, verify, x }) => { + mac_cmd::kmac_cmd(128, *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) + } Some(Subcommands::CSHAKE256 { length, customization, function_name, x }) => { sha3_cmd::cshake_cmd(256, *length, function_name, customization, *x); } diff --git a/crypto/factory/src/mac_factory.rs b/crypto/factory/src/mac_factory.rs index bdda273d..62adeca5 100644 --- a/crypto/factory/src/mac_factory.rs +++ b/crypto/factory/src/mac_factory.rs @@ -83,6 +83,7 @@ use bouncycastle_sha3 as sha3; use bouncycastle_sha3::hmac::{ HMAC_SHA3_224_NAME, HMAC_SHA3_256_NAME, HMAC_SHA3_384_NAME, HMAC_SHA3_512_NAME, }; +use bouncycastle_sha3::{KMAC128, KMAC128_NAME, KMAC256, KMAC256_NAME}; use bouncycastle_sm3 as sm3; use bouncycastle_sm3::hmac::HMAC_SM3_NAME; @@ -101,6 +102,13 @@ pub const DEFAULT_256BIT_MAC_NAME: &str = HMAC_SHA256_NAME; /// instead they have a constructor that takes a [`KeyMaterialTrait`] and can return an error. #[non_exhaustive] pub enum MACFactory { + /// KMAC128 with no customization string and a 32-byte tag (NIST SP 800-185 Sec 4). + /// For a customization string or a different output length, construct + /// `bouncycastle_sha3::KMAC128` directly -- the factory selects by name alone and has no + /// channel for those parameters. + KMAC128(KMAC128), + /// KMAC256 with no customization string and a 64-byte tag. See [`MACFactory::KMAC128`]. + KMAC256(KMAC256), /// HMAC_SHA224(sha2::hmac::HMAC_SHA224), /// @@ -144,6 +152,8 @@ impl MACFactory { DEFAULT => Self::default(key), DEFAULT_128_BIT => Self::default_128_bit(key), DEFAULT_256_BIT => Self::default_256_bit(key), + KMAC128_NAME => Ok(Self::KMAC128(KMAC128::new(key)?)), + KMAC256_NAME => Ok(Self::KMAC256(KMAC256::new(key)?)), HMAC_SHA224_NAME => Ok(Self::HMAC_SHA224(sha2::hmac::HMAC_SHA224::new(key)?)), HMAC_SHA256_NAME => Ok(Self::HMAC_SHA256(sha2::hmac::HMAC_SHA256::new(key)?)), HMAC_SHA384_NAME => Ok(Self::HMAC_SHA384(sha2::hmac::HMAC_SHA384::new(key)?)), @@ -180,6 +190,8 @@ impl MAC for MACFactory { fn output_len(&self) -> usize { match self { + Self::KMAC128(h) => h.output_len(), + Self::KMAC256(h) => h.output_len(), Self::HMAC_SHA224(h) => h.output_len(), Self::HMAC_SHA256(h) => h.output_len(), Self::HMAC_SHA384(h) => h.output_len(), @@ -196,6 +208,8 @@ impl MAC for MACFactory { fn mac(self, data: &[u8]) -> Vec { match self { + Self::KMAC128(h) => h.mac(data), + Self::KMAC256(h) => h.mac(data), Self::HMAC_SHA224(h) => h.mac(data), Self::HMAC_SHA256(h) => h.mac(data), Self::HMAC_SHA384(h) => h.mac(data), @@ -214,6 +228,8 @@ impl MAC for MACFactory { out.fill(0); match self { + Self::KMAC128(h) => h.mac_out(data, out), + Self::KMAC256(h) => h.mac_out(data, out), Self::HMAC_SHA224(h) => h.mac_out(data, out), Self::HMAC_SHA256(h) => h.mac_out(data, out), Self::HMAC_SHA384(h) => h.mac_out(data, out), @@ -230,6 +246,8 @@ impl MAC for MACFactory { fn verify(self, data: &[u8], mac: &[u8]) -> bool { match self { + Self::KMAC128(h) => h.verify(data, mac), + Self::KMAC256(h) => h.verify(data, mac), Self::HMAC_SHA224(h) => h.verify(data, mac), Self::HMAC_SHA256(h) => h.verify(data, mac), Self::HMAC_SHA384(h) => h.verify(data, mac), @@ -246,6 +264,8 @@ impl MAC for MACFactory { fn do_update(&mut self, data: &[u8]) { match self { + Self::KMAC128(h) => h.do_update(data), + Self::KMAC256(h) => h.do_update(data), Self::HMAC_SHA224(h) => h.do_update(data), Self::HMAC_SHA256(h) => h.do_update(data), Self::HMAC_SHA384(h) => h.do_update(data), @@ -262,6 +282,8 @@ impl MAC for MACFactory { fn do_final(self) -> Vec { match self { + Self::KMAC128(h) => h.do_final(), + Self::KMAC256(h) => h.do_final(), Self::HMAC_SHA224(h) => h.do_final(), Self::HMAC_SHA256(h) => h.do_final(), Self::HMAC_SHA384(h) => h.do_final(), @@ -280,6 +302,8 @@ impl MAC for MACFactory { out.fill(0); match self { + Self::KMAC128(h) => h.do_final_out(&mut out), + Self::KMAC256(h) => h.do_final_out(&mut out), Self::HMAC_SHA224(h) => h.do_final_out(&mut out), Self::HMAC_SHA256(h) => h.do_final_out(&mut out), Self::HMAC_SHA384(h) => h.do_final_out(&mut out), @@ -296,6 +320,8 @@ impl MAC for MACFactory { fn do_verify_final(self, mac: &[u8]) -> bool { match self { + Self::KMAC128(h) => h.do_verify_final(mac), + Self::KMAC256(h) => h.do_verify_final(mac), Self::HMAC_SHA224(h) => h.do_verify_final(mac), Self::HMAC_SHA256(h) => h.do_verify_final(mac), Self::HMAC_SHA384(h) => h.do_verify_final(mac), @@ -312,6 +338,8 @@ impl MAC for MACFactory { fn max_security_strength(&self) -> SecurityStrength { match self { + Self::KMAC128(h) => h.max_security_strength(), + Self::KMAC256(h) => h.max_security_strength(), Self::HMAC_SHA224(h) => h.max_security_strength(), Self::HMAC_SHA256(h) => h.max_security_strength(), Self::HMAC_SHA384(h) => h.max_security_strength(), diff --git a/crypto/sha3/src/cshake.rs b/crypto/sha3/src/cshake.rs index b809303e..33969886 100644 --- a/crypto/sha3/src/cshake.rs +++ b/crypto/sha3/src/cshake.rs @@ -46,19 +46,38 @@ impl CSHAKEInternal { let mut shake = SHAKEInternal::::new(); let customized = !n.is_empty() || !s.is_empty(); if customized { - // Sec 3.3: bytepad(encode_string(N) || encode_string(S), rate). Absorbed rather than - // built in a buffer, so no allocation and no bound on the length of N or S. - let rate = PARAMS::RATE_BYTES; - let mut written = absorb_left_encode(&mut shake, rate as u64); - written += absorb_encoded_string(&mut shake, n); - written += absorb_encoded_string(&mut shake, s); - // ... then zero bytes up to a whole number of rate-sized blocks. - absorb_zeros(&mut shake, written.next_multiple_of(rate) - written); + // Sec 3.3: bytepad(encode_string(N) || encode_string(S), rate). + absorb_bytepad(&mut shake, &[n, s]); } Self { shake, customized } } } +/// Absorbs `bytepad(encode_string(s[0]) || ... || encode_string(s[n]), rate)`, the padding of +/// SP 800-185 Sec 2.3.3 over the string encodings of Sec 2.3.2. +/// +/// Absorbed straight into the sponge rather than built in a buffer, so there is no allocation and +/// no bound on the length of the strings. +fn absorb_bytepad(shake: &mut SHAKEInternal, strings: &[&[u8]]) { + let rate = PARAMS::RATE_BYTES; + // Step 1: the encoding of the block size comes first. + let mut written = absorb_left_encode(shake, rate as u64); + for s in strings { + written += absorb_encoded_string(shake, s); + } + // Step 3: zero bytes up to a whole number of rate-sized blocks. + absorb_zeros(shake, written.next_multiple_of(rate) - written); +} + +/// [`absorb_bytepad`] against a cSHAKE, for the functions layered on top of it: KMAC binds its key +/// this way (Sec 4.3 step 1) as a second bytepad block inside cSHAKE's message. +pub(crate) fn absorb_bytepad_strings( + cshake: &mut CSHAKEInternal, + strings: &[&[u8]], +) { + absorb_bytepad(&mut cshake.shake, strings); +} + /// Absorbs `left_encode(value)`, returning how many bytes went in. fn absorb_left_encode(shake: &mut SHAKEInternal, value: u64) -> usize { let (buf, len) = left_encode(value); diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs new file mode 100644 index 00000000..dc10c06e --- /dev/null +++ b/crypto/sha3/src/kmac.rs @@ -0,0 +1,175 @@ +//! KMAC, the Keccak Message Authentication Code of NIST SP 800-185 Sec 4. + +use crate::SHAKEParams; +use crate::cshake::CSHAKEInternal; +use crate::shake::SHAKEOutput; +use crate::xof_utils::right_encode; +use bouncycastle_core::errors::{KeyMaterialError, MACError}; +use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{Algorithm, Hash, MAC, SecurityStrength, XOF, XofOutput}; +use bouncycastle_utils::ct; + +/// The function-name string every KMAC binds, per SP 800-185 Sec 4.3. Fixed by the specification: +/// it is what separates KMAC from any other cSHAKE-derived function. +const KMAC_FUNCTION_NAME: &[u8] = b"KMAC"; + +/// Internal struct for KMAC. Use [`crate::KMAC128`] or [`crate::KMAC256`]. +/// +/// KMAC is cSHAKE with the function name `"KMAC"`, the key bound to the front of the message and +/// the requested output length bound to the end (Sec 4.3): +/// +/// ```text +/// KMAC128(K, X, L, S) = cSHAKE128(bytepad(encode_string(K), 168) || X || right_encode(L), +/// L, "KMAC", S) +/// ``` +/// +/// # Two functions, not one function truncated +/// +/// The output length is *absorbed*, so KMAC at one length is unrelated to KMAC at another -- +/// Sec 1 puts it as "any change in the requested output length completely changes the function". +/// That is why [`Self::new_with_params`] takes the length up front and [`MAC::do_final`] produces +/// exactly that many bytes. +/// +/// [`Self::into_output`] is the separate function of Sec 4.3.1, KMACXOF, which binds +/// `right_encode(0)` instead and then produces as much output as asked for. Its bytes are *not* a +/// prefix of the fixed-length KMAC over the same inputs, and are not meant to be. +pub struct KMACInternal { + cshake: CSHAKEInternal, + output_len: usize, + strength: SecurityStrength, +} + +impl Algorithm for KMACInternal { + const ALG_NAME: &'static str = PARAMS::KMAC_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl KMACInternal { + /// A new KMAC with a customization string and an output length of the caller's choosing. + /// + /// `output_len` is `L` in bytes and is bound into the computation, so it must be the length the + /// verifier will use. `customization` may be empty. [`MAC::new`] is this with no customization + /// and the nominal output length. + /// + /// Sec 8.4.1 requires the key to be at least as long as the security strength for approved use; + /// that is enforced through the key's [`SecurityStrength`] tag, exactly as `HMAC` does, and + /// [`MAC::new_allow_weak_key`] is the escape hatch. + /// + /// # Errors + /// [`MACError::KeyMaterialError`] if the key is not tagged as a MAC key, or -- unless + /// `allow_weak_key` -- if it is tagged below this KMAC's security strength. + pub fn new_with_params( + key: &impl KeyMaterialTrait, + customization: &[u8], + output_len: usize, + allow_weak_key: bool, + ) -> Result { + // Same stance as HMAC: an all-zero key is Zeroized rather than MACKey, and is allowed + // through so callers are not forced to re-tag it. + if !(key.key_type() == KeyType::Zeroized || key.key_type() == KeyType::MACKey) { + return Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType( + "Key type must be a MAC key.", + ))); + } + let strength = SecurityStrength::from_bits(PARAMS::SIZE as usize); + if !allow_weak_key && key.security_strength() < strength { + Err(KeyMaterialError::SecurityStrength( + "KMAC::new(): provided key has a lower security strength than the instantiated KMAC", + ))? + } + + let mut cshake = CSHAKEInternal::::new(KMAC_FUNCTION_NAME, customization); + // Sec 4.3 step 1: bytepad(encode_string(K), rate), absorbed rather than materialised. + crate::cshake::absorb_bytepad_strings(&mut cshake, &[key.ref_to_bytes()]); + + Ok(Self { cshake, output_len, strength }) + } + + /// KMACXOF (Sec 4.3.1): ends the input phase binding `right_encode(0)` and returns the output + /// stream, which will produce as many bytes as asked for. + /// + /// This is a *different function* from [`MAC::do_final`], not a longer view of it -- see the + /// type-level documentation. BC Java reaches both through one `doFinal`/`doOutput` pair guarded + /// by a `firstOutput` flag; here they are separate methods and the flag cannot be got wrong, + /// because this one consumes the KMAC. + pub fn into_output(mut self) -> SHAKEOutput { + self.absorb_right_encode(0); + self.cshake.into_output() + } + + /// Absorbs `right_encode(value)`, the length binding of Sec 4.3 step 1. + fn absorb_right_encode(&mut self, value: u64) { + let (buf, len) = right_encode(value); + self.cshake.do_update(&buf[..len]); + } +} + +impl MAC for KMACInternal { + /// A KMAC with no customization string, producing the nominal output length -- 32 bytes for + /// KMAC128 and 64 for KMAC256. Use [`Self::new_with_params`] to choose either. + fn new(key: &impl KeyMaterialTrait) -> Result { + let len = (PARAMS::SIZE as usize) / 4; + Self::new_with_params(key, &[], len, false) + } + + fn new_allow_weak_key(key: &impl KeyMaterialTrait) -> Result { + let len = (PARAMS::SIZE as usize) / 4; + Self::new_with_params(key, &[], len, true) + } + + fn output_len(&self) -> usize { + self.output_len + } + + fn mac(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn mac_out(mut self, data: &[u8], out: &mut [u8]) -> Result { + out.fill(0); + self.do_update(data); + self.do_final_out(out) + } + + fn verify(mut self, data: &[u8], mac: &[u8]) -> bool { + self.do_update(data); + self.do_verify_final(mac) + } + + fn do_update(&mut self, data: &[u8]) { + self.cshake.do_update(data); + } + + fn do_final(mut self) -> Vec { + let n = self.output_len; + // Sec 4.3 step 1: the requested length is bound into the input before any output. + self.absorb_right_encode((n as u64) * 8); + self.cshake.into_output().do_output(n) + } + + fn do_final_out(mut self, out: &mut [u8]) -> Result { + if out.len() < self.output_len { + return Err(MACError::InvalidLength( + "output buffer is smaller than the KMAC output length", + )); + } + let n = self.output_len; + self.absorb_right_encode((n as u64) * 8); + Ok(self.cshake.into_output().do_output_out(&mut out[..n])) + } + + /// Compares in constant time, and only against the full output length: a caller must not be + /// able to pass verification by supplying a shorter prefix. + fn do_verify_final(self, mac: &[u8]) -> bool { + if mac.len() != self.output_len { + return false; + } + let computed = self.do_final(); + ct::ct_eq_bytes(&computed, mac) + } + + fn max_security_strength(&self) -> SecurityStrength { + self.strength + } +} diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 937a439d..098bcb6c 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -203,6 +203,7 @@ use bouncycastle_core::traits::{Hash, KDF, MAC, Suspendable, XOF}; mod cshake; mod keccak; +mod kmac; mod sha3; mod shake; mod xof_utils; @@ -226,9 +227,14 @@ pub const SHAKE256_NAME: &str = "SHAKE256"; pub const CSHAKE128_NAME: &str = "CSHAKE128"; /// The name of the cSHAKE256 algorithm (NIST SP 800-185 Sec 3). pub const CSHAKE256_NAME: &str = "CSHAKE256"; +/// The name of the KMAC128 algorithm (NIST SP 800-185 Sec 4). +pub const KMAC128_NAME: &str = "KMAC128"; +/// The name of the KMAC256 algorithm (NIST SP 800-185 Sec 4). +pub const KMAC256_NAME: &str = "KMAC256"; /*** pub types ***/ pub use cshake::CSHAKEInternal; +pub use kmac::KMACInternal; pub use sha3::SHA3Internal; /// cSHAKE128: the customizable SHAKE128 of NIST SP 800-185 Sec 3, at a 128-bit security strength. @@ -241,6 +247,17 @@ pub type CSHAKE128 = CSHAKEInternal; /// /// See [`CSHAKE128`]. pub type CSHAKE256 = CSHAKEInternal; + +/// KMAC128: the Keccak MAC of NIST SP 800-185 Sec 4, at a 128-bit security strength. +/// +/// [`bouncycastle_core::traits::MAC::new`] gives the common case -- no customization, 32-byte +/// output. [`KMACInternal::new_with_params`] chooses the customization string and output length, +/// and [`KMACInternal::into_output`] is KMACXOF (Sec 4.3.1). +pub type KMAC128 = KMACInternal; +/// KMAC256: the Keccak MAC of NIST SP 800-185 Sec 4, at a 256-bit security strength. +/// +/// See [`KMAC128`]. The nominal output length is 64 bytes. +pub type KMAC256 = KMACInternal; pub use shake::{SHAKEInternal, SHAKEOutput}; pub use keccak::SUSPENDED_SHA3_STATE_LEN; @@ -373,6 +390,8 @@ trait SHAKEParams: Algorithm { const RATE_BYTES: usize = (1600 - ((Self::SIZE as usize) << 1)) / 8; /// The name of the cSHAKE built on this parameter set. const CSHAKE_ALG_NAME: &'static str; + /// The name of the KMAC built on this parameter set. + const KMAC_ALG_NAME: &'static str; } /// The parameters for SHAKE128. #[derive(Clone)] @@ -385,6 +404,7 @@ impl SHAKEParams for SHAKE128Params { const SIZE: KeccakSize = KeccakSize::_128; const STATE_TAG: u8 = 5; const CSHAKE_ALG_NAME: &'static str = CSHAKE128_NAME; + const KMAC_ALG_NAME: &'static str = KMAC128_NAME; } /// Assigned by NIST in the Computer Security Objects Register: id-shake128 { hashAlgs 11 } impl AlgorithmOID for SHAKE128 { @@ -403,6 +423,7 @@ impl SHAKEParams for SHAKE256Params { const SIZE: KeccakSize = KeccakSize::_256; const STATE_TAG: u8 = 6; const CSHAKE_ALG_NAME: &'static str = CSHAKE256_NAME; + const KMAC_ALG_NAME: &'static str = KMAC256_NAME; } /// Assigned by NIST in the Computer Security Objects Register: id-shake256 { hashAlgs 12 } impl AlgorithmOID for SHAKE256 { diff --git a/crypto/sha3/tests/kmac_tests.rs b/crypto/sha3/tests/kmac_tests.rs new file mode 100644 index 00000000..ec458a70 --- /dev/null +++ b/crypto/sha3/tests/kmac_tests.rs @@ -0,0 +1,238 @@ +//! KMAC against the NIST SP 800-185 sample values. +//! +//! Vectors come from the `bc-test-data` repo cloned alongside this one; see `cshake_tests.rs`. + +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{Algorithm, MAC, XofOutput}; +use bouncycastle_hex as hex; +use bouncycastle_sha3::{KMAC128, KMAC256}; +use std::fs; +use std::path::Path; + +const DATA_DIRS: [&str; 2] = + ["../../../bc-test-data/crypto/sp800-185", "../bc-test-data/crypto/sp800-185"]; + +/// One `COUNT` block of a `.rsp` file. +struct Vector { + strength: usize, + key: Vec, + s: String, + output_len: usize, + msg: Vec, + output: Vec, +} + +fn read_vectors(filename: &str) -> Option> { + let Some(dir) = DATA_DIRS.into_iter().find(|d| Path::new(d).exists()) else { + println!("WARNING: bc-test-data not found; KMAC sample-value tests skipped"); + return None; + }; + let path = Path::new(dir).join(filename); + let content = fs::read_to_string(&path).unwrap_or_else(|e| { + panic!("bc-test-data is present but {} is unreadable: {e}", path.display()) + }); + + let mut out = Vec::new(); + let mut cur: Vec<(String, String)> = Vec::new(); + let finish = |cur: &mut Vec<(String, String)>, out: &mut Vec| { + if cur.is_empty() { + return; + } + let get = |k: &str| cur.iter().find(|(a, _)| a == k).map(|(_, b)| b.clone()); + out.push(Vector { + strength: get("Strength").expect("Strength").parse().expect("a number"), + key: hex::decode(get("Key").expect("Key")).expect("hex"), + s: get("S").unwrap_or_default(), + output_len: get("Outputlen").expect("Outputlen").parse().expect("a number"), + msg: hex::decode(get("Msg").unwrap_or_default()).expect("hex"), + output: hex::decode(get("Output").expect("Output")).expect("hex"), + }); + cur.clear(); + }; + for line in content.lines() { + let line = line.trim_end(); + if line.starts_with('#') || line.is_empty() { + continue; + } + let Some((k, v)) = line.split_once(" = ") else { continue }; + if k == "COUNT" { + finish(&mut cur, &mut out); + } else { + cur.push((k.to_string(), v.to_string())); + } + } + finish(&mut cur, &mut out); + Some(out) +} + +/// Every published sample key is 32 bytes, which carries a 256-bit strength and so satisfies both +/// KMAC128 and KMAC256 without the weak-key escape hatch. +fn key_material(bytes: &[u8]) -> KeyMaterial<32> { + assert_eq!(bytes.len(), 32, "the sample keys are all 32 bytes"); + KeyMaterial::<32>::from_bytes_as_type(bytes, KeyType::MACKey).expect("a valid MAC key") +} + +/// KMAC (Sec 4.3): the requested output length is bound into the input. +#[test] +fn nist_sp800_185_kmac_sample_values() { + let Some(vectors) = read_vectors("KMAC.rsp") else { return }; + assert!(!vectors.is_empty()); + + for (i, v) in vectors.iter().enumerate() { + assert!(v.output_len.is_multiple_of(8), "COUNT {i}: byte-aligned outputs only"); + let want = v.output_len / 8; + let key = key_material(&v.key); + + let got = match v.strength { + 128 => KMAC128::new_with_params(&key, v.s.as_bytes(), want, false) + .expect("a valid key") + .mac(&v.msg), + 256 => KMAC256::new_with_params(&key, v.s.as_bytes(), want, false) + .expect("a valid key") + .mac(&v.msg), + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!(got, v.output, "COUNT {i}: KMAC{} S={:?}", v.strength, v.s); + } + println!("KMAC: {} sample values", vectors.len()); +} + +/// KMACXOF (Sec 4.3.1): `right_encode(0)` in place of the length, then arbitrary output. +#[test] +fn nist_sp800_185_kmacxof_sample_values() { + let Some(vectors) = read_vectors("KMACXOF.rsp") else { return }; + assert!(!vectors.is_empty()); + + for (i, v) in vectors.iter().enumerate() { + let want = v.output_len / 8; + let key = key_material(&v.key); + + let got = match v.strength { + 128 => { + let mut k = KMAC128::new_with_params(&key, v.s.as_bytes(), want, false) + .expect("a valid key"); + k.do_update(&v.msg); + k.into_output().do_output(want) + } + 256 => { + let mut k = KMAC256::new_with_params(&key, v.s.as_bytes(), want, false) + .expect("a valid key"); + k.do_update(&v.msg); + k.into_output().do_output(want) + } + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!(got, v.output, "COUNT {i}: KMACXOF{} S={:?}", v.strength, v.s); + } + println!("KMACXOF: {} sample values", vectors.len()); +} + +/// Sec 4.3.1 versus Sec 4.3: with identical key, message, customization *and* length, KMAC and +/// KMACXOF are different functions, because one binds `right_encode(L)` and the other +/// `right_encode(0)`. The published samples use the same inputs for both, so this is checkable +/// directly against them -- and it is the property that would break if `into_output` bound the +/// length by mistake. +#[test] +fn kmacxof_is_not_kmac_truncated() { + let (Some(fixed), Some(xof)) = (read_vectors("KMAC.rsp"), read_vectors("KMACXOF.rsp")) else { + return; + }; + assert_eq!(fixed.len(), xof.len(), "the two sample files pair up"); + + for (i, (f, x)) in fixed.iter().zip(xof.iter()).enumerate() { + assert_eq!(f.key, x.key, "COUNT {i}: the sample pairs share a key"); + assert_eq!(f.msg, x.msg, "COUNT {i}: ... and a message"); + assert_eq!(f.output_len, x.output_len, "COUNT {i}: ... and an output length"); + assert_ne!( + f.output, x.output, + "COUNT {i}: KMAC and KMACXOF must not agree on the same inputs" + ); + } +} + +/// The output length is absorbed, so asking for a different length is a different function -- not +/// a prefix. Sec 1: "any change in the requested output length completely changes the function". +#[test] +fn output_length_changes_the_function() { + let key = key_material(&[0x42u8; 32]); + let short = KMAC128::new_with_params(&key, b"", 16, false).unwrap().mac(b"abc"); + let long = KMAC128::new_with_params(&key, b"", 32, false).unwrap().mac(b"abc"); + + assert_eq!(short.len(), 16); + assert_eq!(long.len(), 32); + assert_ne!(&long[..16], &short[..], "a longer KMAC must not extend a shorter one"); +} + +/// The customization string separates one use of KMAC from another (Sec 4.2). +#[test] +fn customization_separates_the_functions() { + let key = key_material(&[0x42u8; 32]); + let plain = KMAC128::new_with_params(&key, b"", 32, false).unwrap().mac(b"abc"); + let custom = + KMAC128::new_with_params(&key, b"My Tagged Application", 32, false).unwrap().mac(b"abc"); + assert_ne!(plain, custom, "a customization string must change the output"); +} + +/// Streaming input must equal the one-shot, and `verify` must accept only the right tag. +#[test] +fn streaming_and_verification() { + let key = key_material(&[0x11u8; 32]); + let msg: Vec = (0..=255u8).collect(); + + let one = KMAC128::new_with_params(&key, b"", 32, false).unwrap().mac(&msg); + + let mut k = KMAC128::new_with_params(&key, b"", 32, false).unwrap(); + for chunk in msg.chunks(13) { + k.do_update(chunk); + } + assert_eq!(k.do_final(), one, "chunked input must equal the one-shot"); + + assert!( + KMAC128::new_with_params(&key, b"", 32, false).unwrap().verify(&msg, &one), + "the correct tag must verify" + ); + + let mut wrong = one.clone(); + wrong[0] ^= 1; + assert!( + !KMAC128::new_with_params(&key, b"", 32, false).unwrap().verify(&msg, &wrong), + "a corrupted tag must not verify" + ); + assert!( + !KMAC128::new_with_params(&key, b"", 32, false).unwrap().verify(&msg, &one[..16]), + "a truncated tag must not verify" + ); +} + +/// Sec 8.4.1 wants the key at least as long as the security strength; the tag on the key material +/// is how that is enforced, so a key tagged too weak must be refused unless explicitly allowed. +#[test] +fn weak_keys_are_refused_unless_allowed() { + let weak = KeyMaterial::<16>::from_bytes_as_type(&[0x01u8; 16], KeyType::MACKey) + .expect("a valid 16-byte MAC key"); + assert!(weak.security_strength() < bouncycastle_core::traits::SecurityStrength::_256bit); + + assert!(KMAC256::new(&weak).is_err(), "a 128-bit key must not instantiate KMAC256"); + assert!(KMAC256::new_allow_weak_key(&weak).is_ok(), "... unless explicitly allowed"); + assert!(KMAC128::new(&weak).is_ok(), "but it is enough for KMAC128"); +} + +/// The default constructor: no customization, nominal output length. +#[test] +fn default_constructor_uses_the_nominal_length() { + let key = key_material(&[0x42u8; 32]); + assert_eq!(KMAC128::new(&key).unwrap().output_len(), 32); + assert_eq!(KMAC256::new(&key).unwrap().output_len(), 64); + + // ... and agrees with spelling the same thing out in full. + assert_eq!( + KMAC128::new(&key).unwrap().mac(b"abc"), + KMAC128::new_with_params(&key, b"", 32, false).unwrap().mac(b"abc"), + ); +} + +#[test] +fn algorithm_names() { + assert_eq!(KMAC128::ALG_NAME, "KMAC128"); + assert_eq!(KMAC256::ALG_NAME, "KMAC256"); +} From 3adbfc1495612134137d34ab80e93429c59160d3 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 18:50:55 +1000 Subject: [PATCH 07/22] core: drop the Default supertrait from Hash, so keyed constructions can implement it --- crypto/core/src/traits.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 052e10c9..fbcc6359 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -410,7 +410,18 @@ pub trait ElectronicCodeBook: /// * Collision resistance: finding two inputs that yield the same output is computationally difficult. /// * Preimage resistance: from a given output, finding an input that generates it is computationally difficult. /// * Second preimage resistance: given an input, finding another input that yields the same output is computationally difficult. -pub trait Hash: Algorithm + Default { +/// +/// # Construction is not part of this trait +/// +/// There is deliberately no `Default` supertrait. Feeding bytes in and finalising is one concern; +/// making an instance is another, and not every implementor has a canonical zero-argument one -- +/// a keyed construction such as KMAC (SP 800-185 Sec 4) has no meaningful default, and requiring +/// one would exclude it from this trait and from [`XOF`] with it. +/// +/// Generic code that needs to *build* a hasher asks for it: `fn digest(..)`. +/// That is what `HMAC` and the shared test framework already do, so the bound sits where the +/// requirement actually is rather than on every implementor. +pub trait Hash: Algorithm { /// The size of the internal block in bits -- needed by functions such as HMAC to compute security parameters. fn block_bitlen(&self) -> usize; From b1fff3cc22508a062eec47e47d2c21ae4ebb61ab Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 18:57:06 +1000 Subject: [PATCH 08/22] sha3: KMACXOF128 and KMACXOF256 as keyed XOFs, now that Hash no longer requires Default --- crypto/sha3/src/kmac.rs | 177 +++++++++++++++++++++++++++++--- crypto/sha3/src/lib.rs | 22 +++- crypto/sha3/tests/kmac_tests.rs | 64 +++++++++--- 3 files changed, 232 insertions(+), 31 deletions(-) diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs index dc10c06e..a70228fc 100644 --- a/crypto/sha3/src/kmac.rs +++ b/crypto/sha3/src/kmac.rs @@ -4,7 +4,7 @@ use crate::SHAKEParams; use crate::cshake::CSHAKEInternal; use crate::shake::SHAKEOutput; use crate::xof_utils::right_encode; -use bouncycastle_core::errors::{KeyMaterialError, MACError}; +use bouncycastle_core::errors::{HashError, KeyMaterialError, MACError}; use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{Algorithm, Hash, MAC, SecurityStrength, XOF, XofOutput}; use bouncycastle_utils::ct; @@ -30,8 +30,8 @@ const KMAC_FUNCTION_NAME: &[u8] = b"KMAC"; /// That is why [`Self::new_with_params`] takes the length up front and [`MAC::do_final`] produces /// exactly that many bytes. /// -/// [`Self::into_output`] is the separate function of Sec 4.3.1, KMACXOF, which binds -/// `right_encode(0)` instead and then produces as much output as asked for. Its bytes are *not* a +/// [`KMACXOFInternal`] is the separate function of Sec 4.3.1, KMACXOF, which binds +/// `right_encode(0)` instead and produces as much output as asked for. Its bytes are *not* a /// prefix of the fixed-length KMAC over the same inputs, and are not meant to be. pub struct KMACInternal { cshake: CSHAKEInternal, @@ -85,18 +85,6 @@ impl KMACInternal { Ok(Self { cshake, output_len, strength }) } - /// KMACXOF (Sec 4.3.1): ends the input phase binding `right_encode(0)` and returns the output - /// stream, which will produce as many bytes as asked for. - /// - /// This is a *different function* from [`MAC::do_final`], not a longer view of it -- see the - /// type-level documentation. BC Java reaches both through one `doFinal`/`doOutput` pair guarded - /// by a `firstOutput` flag; here they are separate methods and the flag cannot be got wrong, - /// because this one consumes the KMAC. - pub fn into_output(mut self) -> SHAKEOutput { - self.absorb_right_encode(0); - self.cshake.into_output() - } - /// Absorbs `right_encode(value)`, the length binding of Sec 4.3 step 1. fn absorb_right_encode(&mut self, value: u64) { let (buf, len) = right_encode(value); @@ -173,3 +161,162 @@ impl MAC for KMACInternal { self.strength } } + +/// Internal struct for KMACXOF. Use [`crate::KMACXOF128`] or [`crate::KMACXOF256`]. +/// +/// KMACXOF is the arbitrary-output-length function of SP 800-185 Sec 4.3.1: KMAC with +/// `right_encode(0)` bound in place of the output length. +/// +/// ```text +/// KMACXOF128(K, X, L, S) = cSHAKE128(bytepad(encode_string(K), 168) || X || right_encode(0), +/// L, "KMAC", S) +/// ``` +/// +/// # Why this is a separate type from [`KMACInternal`] +/// +/// The Recommendation defines them as two functions, and they are: over identical inputs KMAC and +/// KMACXOF produce unrelated output, which the published sample values demonstrate directly. They +/// also want different traits -- KMAC's length is fixed at construction and bound into the +/// computation, which is `MAC`; KMACXOF's is not bound at all, which is `XOF`. Since `MAC` and +/// `Hash` share five method names (`do_update`, `do_final`, `output_len` and two more), one type +/// implementing both would make every one of those calls ambiguous, so they are separate types. +/// +/// Because the length is *not* bound here, output at one length really is a prefix of output at a +/// longer one -- the opposite of fixed-length KMAC -- so [`Hash::do_final`] is the first +/// [`Hash::output_len`] bytes of the same stream [`XOF::into_output`] produces. +pub struct KMACXOFInternal { + cshake: CSHAKEInternal, + strength: SecurityStrength, +} + +impl Algorithm for KMACXOFInternal { + const ALG_NAME: &'static str = PARAMS::KMACXOF_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl KMACXOFInternal { + /// A new KMACXOF under `key`, optionally customized by `customization`. + /// + /// The key requirements are [`KMACInternal::new_with_params`]'s: tagged as a MAC key, and at + /// least the security strength unless `allow_weak_key`. + /// + /// # Errors + /// [`MACError::KeyMaterialError`] if the key is not a MAC key, or is tagged too weak. + pub fn new( + key: &impl KeyMaterialTrait, + customization: &[u8], + allow_weak_key: bool, + ) -> Result { + // The key binding is identical to KMAC's; only the length encoding differs, and that is + // applied when output begins. + let kmac = KMACInternal::::new_with_params(key, customization, 0, allow_weak_key)?; + Ok(Self { cshake: kmac.cshake, strength: kmac.strength }) + } + + /// Absorbs `right_encode(0)`, the Sec 4.3.1 length binding, ending the input phase. + fn bind_zero_length(&mut self) { + let (buf, len) = right_encode(0); + self.cshake.do_update(&buf[..len]); + } +} + +impl Hash for KMACXOFInternal { + fn block_bitlen(&self) -> usize { + self.cshake.block_bitlen() + } + + /// The nominal length, 32 or 64 bytes. Unlike [`KMACInternal`] this is not bound into the + /// computation -- it is only how many bytes [`Hash::do_final`] takes from the stream. + fn output_len(&self) -> usize { + self.cshake.output_len() + } + + fn hash(mut self, data: &[u8]) -> Vec { + let n = self.output_len(); + self.do_update(data); + self.into_output().do_output(n) + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.into_output().do_output_out(output) + } + + fn do_update(&mut self, data: &[u8]) { + self.cshake.do_update(data); + } + + fn do_final(self) -> Vec { + let n = self.output_len(); + self.into_output().do_output(n) + } + + fn do_final_out(self, output: &mut [u8]) -> usize { + self.into_output().do_output_out(output) + } + + /// # Errors + /// Always [`HashError::InvalidLength`] for a non-zero `num_bits`: `right_encode(0)` has to + /// follow the message, and a partial final byte would leave the sponge unable to absorb it + /// byte-aligned. `num_bits` of 0 means the message ended on a byte boundary and is accepted. + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let n = self.output_len(); + let mut out = vec![0u8; n]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "KMACXOF cannot take a partial final byte: right_encode(0) must follow the message", + )); + } + Ok(self.do_final_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + self.strength + } +} + +impl XOF for KMACXOFInternal { + type Output = SHAKEOutput; + + fn into_output(mut self) -> Self::Output { + self.bind_zero_length(); + self.cshake.into_output() + } + + fn into_output_partial_bits( + self, + _partial_byte: u8, + num_bits: usize, + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "KMACXOF cannot take a partial final byte: right_encode(0) must follow the message", + )); + } + Ok(self.into_output()) + } + + fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { + self.do_update(data); + self.into_output().do_output(result_len) + } + + fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.into_output().do_output_out(output) + } +} diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 098bcb6c..5bd43afd 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -231,10 +231,14 @@ pub const CSHAKE256_NAME: &str = "CSHAKE256"; pub const KMAC128_NAME: &str = "KMAC128"; /// The name of the KMAC256 algorithm (NIST SP 800-185 Sec 4). pub const KMAC256_NAME: &str = "KMAC256"; +/// The name of the KMACXOF128 algorithm (NIST SP 800-185 Sec 4.3.1). +pub const KMACXOF128_NAME: &str = "KMACXOF128"; +/// The name of the KMACXOF256 algorithm (NIST SP 800-185 Sec 4.3.1). +pub const KMACXOF256_NAME: &str = "KMACXOF256"; /*** pub types ***/ pub use cshake::CSHAKEInternal; -pub use kmac::KMACInternal; +pub use kmac::{KMACInternal, KMACXOFInternal}; pub use sha3::SHA3Internal; /// cSHAKE128: the customizable SHAKE128 of NIST SP 800-185 Sec 3, at a 128-bit security strength. @@ -252,12 +256,22 @@ pub type CSHAKE256 = CSHAKEInternal; /// /// [`bouncycastle_core::traits::MAC::new`] gives the common case -- no customization, 32-byte /// output. [`KMACInternal::new_with_params`] chooses the customization string and output length, -/// and [`KMACInternal::into_output`] is KMACXOF (Sec 4.3.1). +/// [`KMACXOF128`] is the separate arbitrary-length function of Sec 4.3.1. pub type KMAC128 = KMACInternal; /// KMAC256: the Keccak MAC of NIST SP 800-185 Sec 4, at a 256-bit security strength. /// /// See [`KMAC128`]. The nominal output length is 64 bytes. pub type KMAC256 = KMACInternal; + +/// KMACXOF128: the arbitrary-output-length KMAC of NIST SP 800-185 Sec 4.3.1. +/// +/// A keyed [`XOF`]. Distinct from [`KMAC128`], and not a longer +/// view of it: over the same inputs the two produce unrelated output. +pub type KMACXOF128 = KMACXOFInternal; +/// KMACXOF256: the arbitrary-output-length KMAC of NIST SP 800-185 Sec 4.3.1. +/// +/// See [`KMACXOF128`]. +pub type KMACXOF256 = KMACXOFInternal; pub use shake::{SHAKEInternal, SHAKEOutput}; pub use keccak::SUSPENDED_SHA3_STATE_LEN; @@ -392,6 +406,8 @@ trait SHAKEParams: Algorithm { const CSHAKE_ALG_NAME: &'static str; /// The name of the KMAC built on this parameter set. const KMAC_ALG_NAME: &'static str; + /// The name of the KMACXOF built on this parameter set. + const KMACXOF_ALG_NAME: &'static str; } /// The parameters for SHAKE128. #[derive(Clone)] @@ -405,6 +421,7 @@ impl SHAKEParams for SHAKE128Params { const STATE_TAG: u8 = 5; const CSHAKE_ALG_NAME: &'static str = CSHAKE128_NAME; const KMAC_ALG_NAME: &'static str = KMAC128_NAME; + const KMACXOF_ALG_NAME: &'static str = KMACXOF128_NAME; } /// Assigned by NIST in the Computer Security Objects Register: id-shake128 { hashAlgs 11 } impl AlgorithmOID for SHAKE128 { @@ -424,6 +441,7 @@ impl SHAKEParams for SHAKE256Params { const STATE_TAG: u8 = 6; const CSHAKE_ALG_NAME: &'static str = CSHAKE256_NAME; const KMAC_ALG_NAME: &'static str = KMAC256_NAME; + const KMACXOF_ALG_NAME: &'static str = KMACXOF256_NAME; } /// Assigned by NIST in the Computer Security Objects Register: id-shake256 { hashAlgs 12 } impl AlgorithmOID for SHAKE256 { diff --git a/crypto/sha3/tests/kmac_tests.rs b/crypto/sha3/tests/kmac_tests.rs index ec458a70..bac88a59 100644 --- a/crypto/sha3/tests/kmac_tests.rs +++ b/crypto/sha3/tests/kmac_tests.rs @@ -3,9 +3,9 @@ //! Vectors come from the `bc-test-data` repo cloned alongside this one; see `cshake_tests.rs`. use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{Algorithm, MAC, XofOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, MAC, XOF}; use bouncycastle_hex as hex; -use bouncycastle_sha3::{KMAC128, KMAC256}; +use bouncycastle_sha3::{KMAC128, KMAC256, KMACXOF128, KMACXOF256}; use std::fs; use std::path::Path; @@ -108,18 +108,12 @@ fn nist_sp800_185_kmacxof_sample_values() { let key = key_material(&v.key); let got = match v.strength { - 128 => { - let mut k = KMAC128::new_with_params(&key, v.s.as_bytes(), want, false) - .expect("a valid key"); - k.do_update(&v.msg); - k.into_output().do_output(want) - } - 256 => { - let mut k = KMAC256::new_with_params(&key, v.s.as_bytes(), want, false) - .expect("a valid key"); - k.do_update(&v.msg); - k.into_output().do_output(want) - } + 128 => KMACXOF128::new(&key, v.s.as_bytes(), false) + .expect("a valid key") + .hash_xof(&v.msg, want), + 256 => KMACXOF256::new(&key, v.s.as_bytes(), false) + .expect("a valid key") + .hash_xof(&v.msg, want), other => panic!("COUNT {i}: unexpected strength {other}"), }; assert_eq!(got, v.output, "COUNT {i}: KMACXOF{} S={:?}", v.strength, v.s); @@ -236,3 +230,45 @@ fn algorithm_names() { assert_eq!(KMAC128::ALG_NAME, "KMAC128"); assert_eq!(KMAC256::ALG_NAME, "KMAC256"); } + +/// The counterpart to `output_length_changes_the_function`: because KMACXOF binds +/// `right_encode(0)` rather than the length, output at one length *is* a prefix of output at a +/// longer one, and `do_final` is simply the first `output_len` bytes of that same stream. +#[test] +fn kmacxof_output_is_one_stream() { + let key = key_material(&[0x42u8; 32]); + let long = KMACXOF128::new(&key, b"", false).unwrap().hash_xof(b"abc", 64); + + let short = KMACXOF128::new(&key, b"", false).unwrap().hash_xof(b"abc", 16); + assert_eq!(&long[..16], &short[..], "KMACXOF at a shorter length must be a prefix"); + + let mut k = KMACXOF128::new(&key, b"", false).unwrap(); + k.do_update(b"abc"); + let via_hash = k.do_final(); + assert_eq!(via_hash.len(), 32, "the nominal output length"); + assert_eq!(&long[..32], &via_hash[..], "do_final must be a prefix of the stream"); +} + +/// A partial final byte cannot be expressed: `right_encode(0)` has to follow the message, and the +/// sponge cannot absorb byte-aligned data after a partial byte. +#[test] +fn kmacxof_rejects_a_partial_final_byte() { + let key = key_material(&[0x42u8; 32]); + let mut k = KMACXOF128::new(&key, b"", false).unwrap(); + k.do_update(b"abc"); + assert!(matches!( + k.into_output_partial_bits(0xF0, 4), + Err(bouncycastle_core::errors::HashError::InvalidLength(_)) + )); + + // ... but zero bits means the message ended on a byte boundary, which is fine. + let mut k = KMACXOF128::new(&key, b"", false).unwrap(); + k.do_update(b"abc"); + assert!(k.into_output_partial_bits(0, 0).is_ok()); +} + +#[test] +fn kmacxof_algorithm_names() { + assert_eq!(KMACXOF128::ALG_NAME, "KMACXOF128"); + assert_eq!(KMACXOF256::ALG_NAME, "KMACXOF256"); +} From 9efbf5faac693f2edb455dd0e0e1ac090bccd3fb Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 19:01:01 +1000 Subject: [PATCH 09/22] core-test-framework: the XOF suite takes a constructor closure, so keyed XOFs can use it --- crypto/core-test-framework/src/xof.rs | 57 +++++++++++++++------------ crypto/sha3/tests/cshake_tests.rs | 15 +++++++ crypto/sha3/tests/kmac_tests.rs | 23 +++++++++++ crypto/sha3/tests/shake_tests.rs | 4 +- 4 files changed, 71 insertions(+), 28 deletions(-) diff --git a/crypto/core-test-framework/src/xof.rs b/crypto/core-test-framework/src/xof.rs index 8dbf6bcb..5b0f5400 100644 --- a/crypto/core-test-framework/src/xof.rs +++ b/crypto/core-test-framework/src/xof.rs @@ -22,10 +22,10 @@ impl TestFrameworkXOF { /// `input`. There is deliberately no absorb-after-squeeze test: [`XOF::into_output`] consumes /// the XOF, so absorbing afterwards is not expressible and there is no runtime rule left to /// check. That guarantee is asserted instead by `compile_fail` doctests on the implementors. - pub fn test_xof(&self, input: &[u8], expected_output: &[u8]) { + pub fn test_xof(&self, make: impl Fn() -> X, input: &[u8], expected_output: &[u8]) { /*** fn do_update(&mut self, data: &[u8]) ***/ // Feeding the input in pieces must equal feeding it in one go. - let mut xof = X::default(); + let mut xof = make(); for chunk in input.chunks(16) { xof.do_update(chunk); } @@ -36,7 +36,7 @@ impl TestFrameworkXOF { ); /*** fn do_output(&mut self, num_bytes: usize) -> Vec ***/ - let mut xof = X::default(); + let mut xof = make(); xof.do_update(input); assert_eq!( xof.into_output().do_output(expected_output.len()), @@ -47,7 +47,7 @@ impl TestFrameworkXOF { /*** fn do_output_out(&mut self, output: &mut [u8]) -> usize ***/ // Pre-filled so that the documented zeroization is observable. let mut output = vec![0xFFu8; expected_output.len()]; - let mut xof = X::default(); + let mut xof = make(); xof.do_update(input); let n = xof.into_output().do_output_out(&mut output); assert_eq!(n, expected_output.len(), "do_output_out must report what it wrote"); @@ -55,7 +55,7 @@ impl TestFrameworkXOF { // One output stream: reading it in two goes equals reading it in one. let split = expected_output.len() / 2; - let mut xof = X::default(); + let mut xof = make(); xof.do_update(input); let mut out = xof.into_output(); let first = out.do_output(split); @@ -69,7 +69,7 @@ impl TestFrameworkXOF { /*** fn do_final(self, num_bytes: usize) -> Vec ***/ // do_final reads what do_output would read at the same point; it only ends the stream. - let mut xof = X::default(); + let mut xof = make(); xof.do_update(input); assert_eq!( xof.into_output().do_final(expected_output.len()), @@ -78,7 +78,7 @@ impl TestFrameworkXOF { ); // ... including part-way through a stream, not just at the start. - let mut xof = X::default(); + let mut xof = make(); xof.do_update(input); let mut out = xof.into_output(); let head = out.do_output(split); @@ -90,7 +90,7 @@ impl TestFrameworkXOF { ); let mut buf = vec![0xFFu8; expected_output.len()]; - let mut xof = X::default(); + let mut xof = make(); xof.do_update(input); let n = xof.into_output().do_final_out(&mut buf); assert_eq!(n, expected_output.len()); @@ -98,28 +98,28 @@ impl TestFrameworkXOF { /*** fn hash_xof(self, data: &[u8], result_len: usize) -> Vec ***/ assert_eq!( - X::default().hash_xof(input, expected_output.len()), + make().hash_xof(input, expected_output.len()), expected_output, "the one-shot must equal update-then-output" ); let mut output = vec![0xFFu8; expected_output.len()]; - let n = X::default().hash_xof_out(input, &mut output); + let n = make().hash_xof_out(input, &mut output); assert_eq!(n, expected_output.len()); assert_eq!(output, expected_output, "hash_xof_out must agree with hash_xof"); /*** the Hash half: a XOF is a hash ***/ - self.test_xof_as_hash::(input, expected_output); + self.test_xof_as_hash(&make, input, expected_output); if self.enable_partial_byte_tests { - self.test_xof_partial_bits::(input, expected_output); + self.test_xof_partial_bits(&make, input, expected_output); } } /// The inherited [`Hash`] surface. `XOF: Hash`, so SHAKE can be used wherever a hash is wanted; /// these checks pin that the inherited methods agree with the XOF ones. - fn test_xof_as_hash(&self, input: &[u8], expected_output: &[u8]) { - let xof = X::default(); + fn test_xof_as_hash(&self, make: impl Fn() -> X, input: &[u8], expected_output: &[u8]) { + let xof = make(); let output_len = xof.output_len(); assert!(output_len > 0, "output_len must be positive"); assert!(xof.block_bitlen() > 0, "block_bitlen must be positive"); @@ -129,12 +129,12 @@ impl TestFrameworkXOF { ); // do_final is do_output at the nominal length: the same stream, truncated. - let mut a = X::default(); + let mut a = make(); a.do_update(input); let via_hash = a.do_final(); assert_eq!(via_hash.len(), output_len, "do_final must produce output_len bytes"); - let mut b = X::default(); + let mut b = make(); b.do_update(input); assert_eq!( via_hash, @@ -153,23 +153,28 @@ impl TestFrameworkXOF { // do_final_out fills the caller's buffer, zeroizing it first. let mut buf = vec![0xFFu8; output_len]; - let mut c = X::default(); + let mut c = make(); c.do_update(input); let n = c.do_final_out(&mut buf); assert_eq!(n, output_len); assert_eq!(buf, via_hash, "do_final_out must agree with do_final"); // The one-shot Hash entry points. - assert_eq!(X::default().hash(input), via_hash, "hash must equal update-then-do_final"); + assert_eq!(make().hash(input), via_hash, "hash must equal update-then-do_final"); let mut buf = vec![0xFFu8; output_len]; - assert_eq!(X::default().hash_out(input, &mut buf), output_len); + assert_eq!(make().hash_out(input, &mut buf), output_len); assert_eq!(buf, via_hash, "hash_out must agree with hash"); } /// A partial final byte of input, in both the XOF and the Hash spelling. - fn test_xof_partial_bits(&self, input: &[u8], expected_output: &[u8]) { + fn test_xof_partial_bits( + &self, + make: impl Fn() -> X, + input: &[u8], + expected_output: &[u8], + ) { // num_bits = 0 means the message ended on a byte boundary, so it must match plain input. - let mut xof = X::default(); + let mut xof = make(); xof.do_update(input); assert_eq!( xof.into_output_partial_bits(0, 0) @@ -181,7 +186,7 @@ impl TestFrameworkXOF { // A real partial byte must change the output, and both spellings must agree. for num_bits in 1..=7usize { - let mut a = X::default(); + let mut a = make(); a.do_update(input); let with_bits = a .into_output_partial_bits(0xFE, num_bits) @@ -192,7 +197,7 @@ impl TestFrameworkXOF { "a partial byte must change the output / num_bits: {num_bits}" ); - let mut b = X::default(); + let mut b = make(); b.do_update(input); let via_hash = b.do_final_partial_bits(0xFE, num_bits).expect("num_bits is in 1..=7"); assert_eq!( @@ -202,7 +207,7 @@ impl TestFrameworkXOF { ); let mut buf = vec![0xFFu8; via_hash.len()]; - let mut c = X::default(); + let mut c = make(); c.do_update(input); let n = c .do_final_partial_bits_out(0xFE, num_bits, &mut buf) @@ -213,7 +218,7 @@ impl TestFrameworkXOF { // "num_bits must be in 0..=7; larger values return HashError::InvalidLength." for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] { - let mut xof = X::default(); + let mut xof = make(); xof.do_update(input); assert!( matches!( @@ -223,7 +228,7 @@ impl TestFrameworkXOF { "into_output_partial_bits must reject num_bits = {num_bits}" ); - let mut xof = X::default(); + let mut xof = make(); xof.do_update(input); assert!( matches!( diff --git a/crypto/sha3/tests/cshake_tests.rs b/crypto/sha3/tests/cshake_tests.rs index 346f6195..17dfc9ce 100644 --- a/crypto/sha3/tests/cshake_tests.rs +++ b/crypto/sha3/tests/cshake_tests.rs @@ -5,6 +5,7 @@ //! present these tests print a warning and pass vacuously. use bouncycastle_core::traits::{Algorithm, Hash, XOF, XofOutput}; +use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_hex as hex; use bouncycastle_sha3::{CSHAKE128, CSHAKE256, SHAKE128, SHAKE256}; use std::fs; @@ -190,3 +191,17 @@ fn algorithm_names() { assert_eq!(CSHAKE128::ALG_NAME, "CSHAKE128"); assert_eq!(CSHAKE256::ALG_NAME, "CSHAKE256"); } + +/// cSHAKE through the shared `XOF` conformance suite, with a published sample value as the +/// expected output -- conformance and a NIST vector in one. +#[test] +fn test_framework_xof() { + let Some(vectors) = read_vectors("cSHAKE.rsp") else { return }; + let v = vectors.first().expect("at least one sample"); + // The partial-byte input path is cSHAKE's own (it inherits SHAKE's), so leave it enabled. + TestFrameworkXOF::new().test_xof( + || CSHAKE128::new(v.n.as_bytes(), v.s.as_bytes()), + &v.msg, + &v.output, + ); +} diff --git a/crypto/sha3/tests/kmac_tests.rs b/crypto/sha3/tests/kmac_tests.rs index bac88a59..612f09d8 100644 --- a/crypto/sha3/tests/kmac_tests.rs +++ b/crypto/sha3/tests/kmac_tests.rs @@ -4,6 +4,7 @@ use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{Algorithm, Hash, MAC, XOF}; +use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_hex as hex; use bouncycastle_sha3::{KMAC128, KMAC256, KMACXOF128, KMACXOF256}; use std::fs; @@ -272,3 +273,25 @@ fn kmacxof_algorithm_names() { assert_eq!(KMACXOF128::ALG_NAME, "KMACXOF128"); assert_eq!(KMACXOF256::ALG_NAME, "KMACXOF256"); } + +/// KMACXOF through the shared `XOF` conformance suite. +/// +/// This is what the constructor-closure form of the framework buys: a keyed XOF has no `Default`, +/// so before it the suite could only be pointed at unkeyed functions. The expected output is taken +/// from a published sample value, so this checks conformance and a NIST vector at once. +#[test] +fn test_framework_xof() { + let Some(vectors) = read_vectors("KMACXOF.rsp") else { return }; + let v = vectors.first().expect("at least one sample"); + let key = key_material(&v.key); + + // Partial-byte input is not expressible for KMACXOF -- right_encode(0) has to follow the + // message -- so that part of the suite is switched off. + let mut framework = TestFrameworkXOF::new(); + framework.enable_partial_byte_tests = false; + framework.test_xof( + || KMACXOF128::new(&key, v.s.as_bytes(), false).expect("a valid key"), + &v.msg, + &v.output, + ); +} diff --git a/crypto/sha3/tests/shake_tests.rs b/crypto/sha3/tests/shake_tests.rs index 590fcc14..6d921c97 100644 --- a/crypto/sha3/tests/shake_tests.rs +++ b/crypto/sha3/tests/shake_tests.rs @@ -257,8 +257,8 @@ mod shake_tests { #[test] fn test_framework_xof() { let test_framework = TestFrameworkXOF::new(); - test_framework.test_xof::(&DUMMY_SEED[..512], b"\x88\x90\xED\x20\x4D\x22\x89\xE1\x72\xE9\xAE\x68\x48\x18\x23\x77\x08\x20\x90\x80\x60\xA4\xDF\x33\x51\xA3\xF1\x84\xEB\xB6\xDD\x0F\x9D\x23\x15\x60\x68\x0F\x2C\x65\x8A\xC4\x84\x97\xAD\xB5\xA4\x83\x99\x36\xA3\x16\x55\x16\xFA\x5E\x13\xBF\x8A\x15\xBA\xBC\x14\x1F"); - test_framework.test_xof::(&DUMMY_SEED[..512], b"\xA1\xD7\x18\x85\xB0\xA8\x41\xF0\x3D\x1D\xC7\xF2\x73\x8A\x15\xCC\x98\x40\x71\xA1\x7F\xFE\xD5\xEC\xAC\xB9\xF5\x87\x20\xA4\x73\xBE\x1F\x2D\x28\xB9\x6D\x54\x3A\x36\x7C\x81\x11\x42\x06\xF5\xAF\x37\x18\xE7\x31\x5B\x57\xF2\x90\xB6\x4D\x8D\x29\xCF\x43\x7E\x40\x4C"); + test_framework.test_xof(SHAKE128::new, &DUMMY_SEED[..512], b"\x88\x90\xED\x20\x4D\x22\x89\xE1\x72\xE9\xAE\x68\x48\x18\x23\x77\x08\x20\x90\x80\x60\xA4\xDF\x33\x51\xA3\xF1\x84\xEB\xB6\xDD\x0F\x9D\x23\x15\x60\x68\x0F\x2C\x65\x8A\xC4\x84\x97\xAD\xB5\xA4\x83\x99\x36\xA3\x16\x55\x16\xFA\x5E\x13\xBF\x8A\x15\xBA\xBC\x14\x1F"); + test_framework.test_xof(SHAKE256::new, &DUMMY_SEED[..512], b"\xA1\xD7\x18\x85\xB0\xA8\x41\xF0\x3D\x1D\xC7\xF2\x73\x8A\x15\xCC\x98\x40\x71\xA1\x7F\xFE\xD5\xEC\xAC\xB9\xF5\x87\x20\xA4\x73\xBE\x1F\x2D\x28\xB9\x6D\x54\x3A\x36\x7C\x81\x11\x42\x06\xF5\xAF\x37\x18\xE7\x31\x5B\x57\xF2\x90\xB6\x4D\x8D\x29\xCF\x43\x7E\x40\x4C"); } #[test] From 3913b551ef0c4a49b604fd22eec6dcd4ef27259c Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 19:11:21 +1000 Subject: [PATCH 10/22] sha3: add TupleHash and TupleHashXOF (SP 800-185 Sec 5), where each update appends one tuple element --- crypto/sha3/src/cshake.rs | 10 + crypto/sha3/src/lib.rs | 31 ++++ crypto/sha3/src/tuplehash.rs | 267 +++++++++++++++++++++++++++ crypto/sha3/tests/tuplehash_tests.rs | 209 +++++++++++++++++++++ 4 files changed, 517 insertions(+) create mode 100644 crypto/sha3/src/tuplehash.rs create mode 100644 crypto/sha3/tests/tuplehash_tests.rs diff --git a/crypto/sha3/src/cshake.rs b/crypto/sha3/src/cshake.rs index 33969886..acf95fab 100644 --- a/crypto/sha3/src/cshake.rs +++ b/crypto/sha3/src/cshake.rs @@ -78,6 +78,16 @@ pub(crate) fn absorb_bytepad_strings( absorb_bytepad(&mut cshake.shake, strings); } +/// Absorbs `encode_string(s)` into a cSHAKE, for the functions layered on top: TupleHash encodes +/// each tuple element this way (Sec 5.3 step 3), which is what makes the tuple boundaries part of +/// the hash. +pub(crate) fn absorb_encoded_string_into( + cshake: &mut CSHAKEInternal, + s: &[u8], +) { + absorb_encoded_string(&mut cshake.shake, s); +} + /// Absorbs `left_encode(value)`, returning how many bytes went in. fn absorb_left_encode(shake: &mut SHAKEInternal, value: u64) -> usize { let (buf, len) = left_encode(value); diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 5bd43afd..51d23cc4 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -206,6 +206,7 @@ mod keccak; mod kmac; mod sha3; mod shake; +mod tuplehash; mod xof_utils; pub mod hmac; @@ -235,11 +236,20 @@ pub const KMAC256_NAME: &str = "KMAC256"; pub const KMACXOF128_NAME: &str = "KMACXOF128"; /// The name of the KMACXOF256 algorithm (NIST SP 800-185 Sec 4.3.1). pub const KMACXOF256_NAME: &str = "KMACXOF256"; +/// The name of the TupleHash128 algorithm (NIST SP 800-185 Sec 5). +pub const TUPLEHASH128_NAME: &str = "TupleHash128"; +/// The name of the TupleHash256 algorithm (NIST SP 800-185 Sec 5). +pub const TUPLEHASH256_NAME: &str = "TupleHash256"; +/// The name of the TupleHashXOF128 algorithm (NIST SP 800-185 Sec 5.3.1). +pub const TUPLEHASHXOF128_NAME: &str = "TupleHashXOF128"; +/// The name of the TupleHashXOF256 algorithm (NIST SP 800-185 Sec 5.3.1). +pub const TUPLEHASHXOF256_NAME: &str = "TupleHashXOF256"; /*** pub types ***/ pub use cshake::CSHAKEInternal; pub use kmac::{KMACInternal, KMACXOFInternal}; pub use sha3::SHA3Internal; +pub use tuplehash::{TupleHashInternal, TupleHashXOFInternal}; /// cSHAKE128: the customizable SHAKE128 of NIST SP 800-185 Sec 3, at a 128-bit security strength. /// @@ -272,6 +282,19 @@ pub type KMACXOF128 = KMACXOFInternal; /// /// See [`KMACXOF128`]. pub type KMACXOF256 = KMACXOFInternal; + +/// TupleHash128: the unambiguous tuple hash of NIST SP 800-185 Sec 5, 128-bit strength. +/// +/// Each [`Hash::do_update`] call appends one *tuple +/// element*, not a run of bytes -- so unlike every other hash here, the chunking is part of the +/// input. See [`TupleHashInternal`]. +pub type TUPLEHASH128 = TupleHashInternal; +/// TupleHash256: see [`TUPLEHASH128`]. +pub type TUPLEHASH256 = TupleHashInternal; +/// TupleHashXOF128: the arbitrary-output-length TupleHash of Sec 5.3.1. +pub type TUPLEHASHXOF128 = TupleHashXOFInternal; +/// TupleHashXOF256: see [`TUPLEHASHXOF128`]. +pub type TUPLEHASHXOF256 = TupleHashXOFInternal; pub use shake::{SHAKEInternal, SHAKEOutput}; pub use keccak::SUSPENDED_SHA3_STATE_LEN; @@ -408,6 +431,10 @@ trait SHAKEParams: Algorithm { const KMAC_ALG_NAME: &'static str; /// The name of the KMACXOF built on this parameter set. const KMACXOF_ALG_NAME: &'static str; + /// The name of the TupleHash built on this parameter set. + const TUPLEHASH_ALG_NAME: &'static str; + /// The name of the TupleHashXOF built on this parameter set. + const TUPLEHASHXOF_ALG_NAME: &'static str; } /// The parameters for SHAKE128. #[derive(Clone)] @@ -422,6 +449,8 @@ impl SHAKEParams for SHAKE128Params { const CSHAKE_ALG_NAME: &'static str = CSHAKE128_NAME; const KMAC_ALG_NAME: &'static str = KMAC128_NAME; const KMACXOF_ALG_NAME: &'static str = KMACXOF128_NAME; + const TUPLEHASH_ALG_NAME: &'static str = TUPLEHASH128_NAME; + const TUPLEHASHXOF_ALG_NAME: &'static str = TUPLEHASHXOF128_NAME; } /// Assigned by NIST in the Computer Security Objects Register: id-shake128 { hashAlgs 11 } impl AlgorithmOID for SHAKE128 { @@ -442,6 +471,8 @@ impl SHAKEParams for SHAKE256Params { const CSHAKE_ALG_NAME: &'static str = CSHAKE256_NAME; const KMAC_ALG_NAME: &'static str = KMAC256_NAME; const KMACXOF_ALG_NAME: &'static str = KMACXOF256_NAME; + const TUPLEHASH_ALG_NAME: &'static str = TUPLEHASH256_NAME; + const TUPLEHASHXOF_ALG_NAME: &'static str = TUPLEHASHXOF256_NAME; } /// Assigned by NIST in the Computer Security Objects Register: id-shake256 { hashAlgs 12 } impl AlgorithmOID for SHAKE256 { diff --git a/crypto/sha3/src/tuplehash.rs b/crypto/sha3/src/tuplehash.rs new file mode 100644 index 00000000..a98cf652 --- /dev/null +++ b/crypto/sha3/src/tuplehash.rs @@ -0,0 +1,267 @@ +//! TupleHash, the tuple-hashing function of NIST SP 800-185 Sec 5. + +use crate::SHAKEParams; +use crate::cshake::{CSHAKEInternal, absorb_encoded_string_into}; +use crate::shake::SHAKEOutput; +use crate::xof_utils::right_encode; +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XofOutput}; + +/// The function-name string every TupleHash binds, per SP 800-185 Sec 5.3. +const TUPLEHASH_FUNCTION_NAME: &[u8] = b"TupleHash"; + +/// Internal struct for TupleHash. Use [`crate::TUPLEHASH128`] or [`crate::TUPLEHASH256`]. +/// +/// TupleHash hashes a *sequence of strings* unambiguously (Sec 5.1): each element is length- +/// prefixed with `encode_string` before absorption, so the boundaries between elements are part of +/// the computation. `("abc", "d")` and `("ab", "cd")` therefore hash differently, even though the +/// concatenations are identical -- which is the whole point of the function. +/// +/// ```text +/// TupleHash128(X, L, S) = cSHAKE128(encode_string(X[0]) || ... || right_encode(L), +/// L, "TupleHash", S) +/// ``` +/// +/// # `do_update` appends an element, it does not append bytes +/// +/// This is the one place TupleHash departs from the usual [`Hash`] contract. For every other hash, +/// feeding the input in pieces gives the same answer as feeding it at once; here each +/// [`Hash::do_update`] call is one tuple element, so the chunking *is* the input. BC Java draws the +/// same line -- its `TupleHash.update` encodes each call with `XofUtils.encode` before passing it +/// on -- but it is worth stating plainly, because code that treats a `TupleHash` as an +/// interchangeable `Hash` and re-chunks its input will silently compute something else. +/// +/// [`TupleHashXOFInternal`] is the arbitrary-output-length function of Sec 5.3.1. +pub struct TupleHashInternal { + cshake: CSHAKEInternal, + output_len: usize, +} + +impl Algorithm for TupleHashInternal { + const ALG_NAME: &'static str = PARAMS::TUPLEHASH_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl TupleHashInternal { + /// A new TupleHash producing `output_len` bytes, optionally customized. + /// + /// `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 } + } + + /// Hashes a whole tuple in one call, the shape the specification is written in. + pub fn hash_tuple(mut self, tuple: &[&[u8]]) -> Vec { + for element in tuple { + self.do_update(element); + } + self.do_final() + } +} + +impl Hash for TupleHashInternal { + fn block_bitlen(&self) -> usize { + self.cshake.block_bitlen() + } + + fn output_len(&self) -> usize { + self.output_len + } + + /// Hashes `data` as a one-element tuple. For more than one element use + /// [`Self::hash_tuple`] or successive [`Hash::do_update`] calls. + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.do_final_out(output) + } + + /// Appends **one tuple element**. See the note on the type: this is not byte-wise streaming. + fn do_update(&mut self, data: &[u8]) { + absorb_encoded_string_into(&mut self.cshake, data); + } + + fn do_final(mut self) -> Vec { + let n = self.output_len; + let (buf, len) = right_encode((n as u64) * 8); + self.cshake.do_update(&buf[..len]); + self.cshake.into_output().do_output(n) + } + + fn do_final_out(mut self, output: &mut [u8]) -> usize { + let n = self.output_len; + let (buf, len) = right_encode((n as u64) * 8); + self.cshake.do_update(&buf[..len]); + self.cshake.into_output().do_output_out(&mut output[..n]) + } + + /// # Errors + /// Always [`HashError::InvalidLength`] for a non-zero `num_bits`: `right_encode(L)` has to + /// follow the tuple, which a partial final byte would prevent. + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let mut out = vec![0u8; self.output_len]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "TupleHash cannot take a partial final byte: the length encoding must follow", + )); + } + Ok(self.do_final_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::from_bits(PARAMS::SIZE as usize) + } +} + +/// Internal struct for TupleHashXOF. Use [`crate::TUPLEHASHXOF128`] or [`crate::TUPLEHASHXOF256`]. +/// +/// The arbitrary-output-length TupleHash of Sec 5.3.1: `right_encode(0)` in place of the length. +/// As with KMAC, it is a *different function* from the fixed-length one, not a longer view of it, +/// and it is a separate type for the same reason -- but here the length not being bound means +/// output at one length really is a prefix of output at a longer one. +/// +/// [`Hash::do_update`] appends one tuple element, exactly as for [`TupleHashInternal`]. +pub struct TupleHashXOFInternal { + cshake: CSHAKEInternal, +} + +impl Algorithm for TupleHashXOFInternal { + const ALG_NAME: &'static str = PARAMS::TUPLEHASHXOF_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl TupleHashXOFInternal { + /// A new TupleHashXOF, optionally customized. + pub fn new(customization: &[u8]) -> Self { + Self { cshake: CSHAKEInternal::new(TUPLEHASH_FUNCTION_NAME, customization) } + } + + /// Hashes a whole tuple and returns the output stream. + pub fn output_for(mut self, tuple: &[&[u8]]) -> SHAKEOutput { + for element in tuple { + self.do_update(element); + } + self.into_output() + } +} + +impl Hash for TupleHashXOFInternal { + fn block_bitlen(&self) -> usize { + self.cshake.block_bitlen() + } + + /// The nominal length, 32 or 64 bytes. Not bound into the computation -- see + /// [`TupleHashXOFInternal`]. + fn output_len(&self) -> usize { + self.cshake.output_len() + } + + fn hash(mut self, data: &[u8]) -> Vec { + let n = self.output_len(); + self.do_update(data); + self.into_output().do_output(n) + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.into_output().do_output_out(output) + } + + /// Appends **one tuple element**. + fn do_update(&mut self, data: &[u8]) { + absorb_encoded_string_into(&mut self.cshake, data); + } + + fn do_final(self) -> Vec { + let n = self.output_len(); + self.into_output().do_output(n) + } + + fn do_final_out(self, output: &mut [u8]) -> usize { + self.into_output().do_output_out(output) + } + + /// # Errors + /// Always [`HashError::InvalidLength`] for a non-zero `num_bits`; see + /// [`TupleHashInternal::do_final_partial_bits`]. + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let mut out = vec![0u8; self.output_len()]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "TupleHashXOF cannot take a partial final byte: right_encode(0) must follow", + )); + } + Ok(self.do_final_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::from_bits(PARAMS::SIZE as usize) + } +} + +impl XOF for TupleHashXOFInternal { + type Output = SHAKEOutput; + + fn into_output(mut self) -> Self::Output { + // Sec 5.3.1 step 4: right_encode(0) rather than the length. + let (buf, len) = right_encode(0); + self.cshake.do_update(&buf[..len]); + self.cshake.into_output() + } + + fn into_output_partial_bits( + self, + _partial_byte: u8, + num_bits: usize, + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "TupleHashXOF cannot take a partial final byte: right_encode(0) must follow", + )); + } + Ok(self.into_output()) + } + + fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { + self.do_update(data); + self.into_output().do_output(result_len) + } + + fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.into_output().do_output_out(output) + } +} diff --git a/crypto/sha3/tests/tuplehash_tests.rs b/crypto/sha3/tests/tuplehash_tests.rs new file mode 100644 index 00000000..263e898b --- /dev/null +++ b/crypto/sha3/tests/tuplehash_tests.rs @@ -0,0 +1,209 @@ +//! TupleHash against the NIST SP 800-185 sample values. +//! +//! Vectors come from the `bc-test-data` repo cloned alongside this one; see `cshake_tests.rs`. + +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XofOutput}; +use bouncycastle_hex as hex; +use bouncycastle_sha3::{TUPLEHASH128, TUPLEHASH256, TUPLEHASHXOF128, TUPLEHASHXOF256}; +use std::fs; +use std::path::Path; + +const DATA_DIRS: [&str; 2] = + ["../../../bc-test-data/crypto/sp800-185", "../bc-test-data/crypto/sp800-185"]; + +/// One `COUNT` block of a `.rsp` file. +struct Vector { + strength: usize, + s: String, + output_len: usize, + tuple: Vec>, + output: Vec, +} + +fn read_vectors(filename: &str) -> Option> { + let Some(dir) = DATA_DIRS.into_iter().find(|d| Path::new(d).exists()) else { + println!("WARNING: bc-test-data not found; TupleHash sample-value tests skipped"); + return None; + }; + let path = Path::new(dir).join(filename); + let content = fs::read_to_string(&path).unwrap_or_else(|e| { + panic!("bc-test-data is present but {} is unreadable: {e}", path.display()) + }); + + let mut out = Vec::new(); + let mut cur: Vec<(String, String)> = Vec::new(); + let finish = |cur: &mut Vec<(String, String)>, out: &mut Vec| { + if cur.is_empty() { + return; + } + let get = |k: &str| cur.iter().find(|(a, _)| a == k).map(|(_, b)| b.clone()); + let count: usize = get("Count").expect("Count").parse().expect("a number"); + let tuple = (1..=count) + .map(|i| hex::decode(get(&format!("Tuple{i}")).expect("a tuple element")).expect("hex")) + .collect(); + out.push(Vector { + strength: get("Strength").expect("Strength").parse().expect("a number"), + s: get("S").unwrap_or_default(), + output_len: get("Outputlen").expect("Outputlen").parse().expect("a number"), + tuple, + output: hex::decode(get("Output").expect("Output")).expect("hex"), + }); + cur.clear(); + }; + for line in content.lines() { + let line = line.trim_end(); + if line.starts_with('#') || line.is_empty() { + continue; + } + let Some((k, v)) = line.split_once(" = ") else { continue }; + if k == "COUNT" { + finish(&mut cur, &mut out); + } else { + cur.push((k.to_string(), v.to_string())); + } + } + finish(&mut cur, &mut out); + Some(out) +} + +fn as_slices(tuple: &[Vec]) -> Vec<&[u8]> { + tuple.iter().map(|v| v.as_slice()).collect() +} + +/// TupleHash (Sec 5.3): the output length is bound into the input. +#[test] +fn nist_sp800_185_tuplehash_sample_values() { + let Some(vectors) = read_vectors("TupleHash.rsp") else { return }; + assert!(!vectors.is_empty()); + + for (i, v) in vectors.iter().enumerate() { + 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), + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!( + got, + v.output, + "COUNT {i}: TupleHash{} with {} elements, S={:?}", + v.strength, + v.tuple.len(), + v.s + ); + } + println!("TupleHash: {} sample values", vectors.len()); +} + +/// TupleHashXOF (Sec 5.3.1): `right_encode(0)` in place of the length. +#[test] +fn nist_sp800_185_tuplehashxof_sample_values() { + let Some(vectors) = read_vectors("TupleHashXOF.rsp") else { return }; + assert!(!vectors.is_empty()); + + for (i, v) in vectors.iter().enumerate() { + let want = v.output_len / 8; + let t = as_slices(&v.tuple); + let got = match v.strength { + 128 => TUPLEHASHXOF128::new(v.s.as_bytes()).output_for(&t).do_output(want), + 256 => TUPLEHASHXOF256::new(v.s.as_bytes()).output_for(&t).do_output(want), + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!(got, v.output, "COUNT {i}: TupleHashXOF{} S={:?}", v.strength, v.s); + } + println!("TupleHashXOF: {} sample values", vectors.len()); +} + +/// The two are different functions on identical inputs, as for KMAC. +#[test] +fn tuplehashxof_is_not_tuplehash_truncated() { + let (Some(fixed), Some(xof)) = + (read_vectors("TupleHash.rsp"), read_vectors("TupleHashXOF.rsp")) + else { + return; + }; + assert_eq!(fixed.len(), xof.len()); + for (i, (f, x)) in fixed.iter().zip(xof.iter()).enumerate() { + assert_eq!(f.tuple, x.tuple, "COUNT {i}: the sample pairs share a tuple"); + assert_eq!(f.output_len, x.output_len, "COUNT {i}: ... and an output length"); + assert_ne!(f.output, x.output, "COUNT {i}: the two functions must differ"); + } +} + +/// 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"]); + 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"]); + assert_ne!(with, without, "an empty tuple element must still count"); +} + +/// `hash_tuple` and successive `do_update` calls must agree, since each update is one element. +#[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 mut t = TUPLEHASH128::new(b"S", 32); + for element in tuple { + t.do_update(element); + } + assert_eq!(t.do_final(), one, "do_update per element must equal hash_tuple"); +} + +/// The output length is bound for the fixed-length function and not for the XOF, so they have +/// opposite behaviour when the length changes -- the same split as KMAC. +#[test] +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); + assert_ne!(&long[..16], &short[..], "TupleHash: a different length is a different function"); + + let short = TUPLEHASHXOF128::new(b"").output_for(&t).do_output(16); + let long = TUPLEHASHXOF128::new(b"").output_for(&t).do_output(32); + assert_eq!(&long[..16], &short[..], "TupleHashXOF: one stream, so shorter is a prefix"); +} + +/// The customization string separates one use from another (Sec 5.2). +#[test] +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), + ); +} + +/// 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); + t.do_update(b"abc"); + assert!(matches!(t.do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); + + let mut t = TUPLEHASHXOF128::new(b""); + t.do_update(b"abc"); + assert!(matches!(t.into_output_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); +} + +#[test] +fn algorithm_names() { + assert_eq!(TUPLEHASH128::ALG_NAME, "TupleHash128"); + assert_eq!(TUPLEHASH256::ALG_NAME, "TupleHash256"); + assert_eq!(TUPLEHASHXOF128::ALG_NAME, "TupleHashXOF128"); + assert_eq!(TUPLEHASHXOF256::ALG_NAME, "TupleHashXOF256"); +} From 90bd17976d4568eb28a8a7d2ae3b4c69217b8634 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 19:18:27 +1000 Subject: [PATCH 11/22] sha3: add ParallelHash and ParallelHashXOF (SP 800-185 Sec 6), completing the Recommendation --- crypto/sha3/src/cshake.rs | 9 + crypto/sha3/src/lib.rs | 30 +++ crypto/sha3/src/parallelhash.rs | 311 ++++++++++++++++++++++++ crypto/sha3/tests/parallelhash_tests.rs | 220 +++++++++++++++++ 4 files changed, 570 insertions(+) create mode 100644 crypto/sha3/src/parallelhash.rs create mode 100644 crypto/sha3/tests/parallelhash_tests.rs diff --git a/crypto/sha3/src/cshake.rs b/crypto/sha3/src/cshake.rs index acf95fab..9f5a3aa8 100644 --- a/crypto/sha3/src/cshake.rs +++ b/crypto/sha3/src/cshake.rs @@ -88,6 +88,15 @@ pub(crate) fn absorb_encoded_string_into( absorb_encoded_string(&mut cshake.shake, s); } +/// Absorbs `left_encode(value)` into a cSHAKE, for the functions layered on top: ParallelHash +/// binds its block size this way (Sec 6.3 step 2). +pub(crate) fn absorb_left_encode_into( + cshake: &mut CSHAKEInternal, + value: u64, +) { + absorb_left_encode(&mut cshake.shake, value); +} + /// Absorbs `left_encode(value)`, returning how many bytes went in. fn absorb_left_encode(shake: &mut SHAKEInternal, value: u64) -> usize { let (buf, len) = left_encode(value); diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 51d23cc4..df2c67ba 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -204,6 +204,7 @@ use bouncycastle_core::traits::{Hash, KDF, MAC, Suspendable, XOF}; mod cshake; mod keccak; mod kmac; +mod parallelhash; mod sha3; mod shake; mod tuplehash; @@ -244,10 +245,19 @@ pub const TUPLEHASH256_NAME: &str = "TupleHash256"; pub const TUPLEHASHXOF128_NAME: &str = "TupleHashXOF128"; /// The name of the TupleHashXOF256 algorithm (NIST SP 800-185 Sec 5.3.1). pub const TUPLEHASHXOF256_NAME: &str = "TupleHashXOF256"; +/// The name of the ParallelHash128 algorithm (NIST SP 800-185 Sec 6). +pub const PARALLELHASH128_NAME: &str = "ParallelHash128"; +/// The name of the ParallelHash256 algorithm (NIST SP 800-185 Sec 6). +pub const PARALLELHASH256_NAME: &str = "ParallelHash256"; +/// The name of the ParallelHashXOF128 algorithm (NIST SP 800-185 Sec 6.3.1). +pub const PARALLELHASHXOF128_NAME: &str = "ParallelHashXOF128"; +/// The name of the ParallelHashXOF256 algorithm (NIST SP 800-185 Sec 6.3.1). +pub const PARALLELHASHXOF256_NAME: &str = "ParallelHashXOF256"; /*** pub types ***/ pub use cshake::CSHAKEInternal; pub use kmac::{KMACInternal, KMACXOFInternal}; +pub use parallelhash::{ParallelHashInternal, ParallelHashXOFInternal}; pub use sha3::SHA3Internal; pub use tuplehash::{TupleHashInternal, TupleHashXOFInternal}; @@ -295,6 +305,18 @@ pub type TUPLEHASH256 = TupleHashInternal; pub type TUPLEHASHXOF128 = TupleHashXOFInternal; /// TupleHashXOF256: see [`TUPLEHASHXOF128`]. pub type TUPLEHASHXOF256 = TupleHashXOFInternal; + +/// 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 +/// different `B` hashes differently. See [`ParallelHashInternal`]. +pub type PARALLELHASH128 = ParallelHashInternal; +/// ParallelHash256: see [`PARALLELHASH128`]. +pub type PARALLELHASH256 = ParallelHashInternal; +/// ParallelHashXOF128: the arbitrary-output-length ParallelHash of Sec 6.3.1. +pub type PARALLELHASHXOF128 = ParallelHashXOFInternal; +/// ParallelHashXOF256: see [`PARALLELHASHXOF128`]. +pub type PARALLELHASHXOF256 = ParallelHashXOFInternal; pub use shake::{SHAKEInternal, SHAKEOutput}; pub use keccak::SUSPENDED_SHA3_STATE_LEN; @@ -435,6 +457,10 @@ trait SHAKEParams: Algorithm { const TUPLEHASH_ALG_NAME: &'static str; /// The name of the TupleHashXOF built on this parameter set. const TUPLEHASHXOF_ALG_NAME: &'static str; + /// The name of the ParallelHash built on this parameter set. + const PARALLELHASH_ALG_NAME: &'static str; + /// The name of the ParallelHashXOF built on this parameter set. + const PARALLELHASHXOF_ALG_NAME: &'static str; } /// The parameters for SHAKE128. #[derive(Clone)] @@ -451,6 +477,8 @@ impl SHAKEParams for SHAKE128Params { const KMACXOF_ALG_NAME: &'static str = KMACXOF128_NAME; const TUPLEHASH_ALG_NAME: &'static str = TUPLEHASH128_NAME; const TUPLEHASHXOF_ALG_NAME: &'static str = TUPLEHASHXOF128_NAME; + const PARALLELHASH_ALG_NAME: &'static str = PARALLELHASH128_NAME; + const PARALLELHASHXOF_ALG_NAME: &'static str = PARALLELHASHXOF128_NAME; } /// Assigned by NIST in the Computer Security Objects Register: id-shake128 { hashAlgs 11 } impl AlgorithmOID for SHAKE128 { @@ -473,6 +501,8 @@ impl SHAKEParams for SHAKE256Params { const KMACXOF_ALG_NAME: &'static str = KMACXOF256_NAME; const TUPLEHASH_ALG_NAME: &'static str = TUPLEHASH256_NAME; const TUPLEHASHXOF_ALG_NAME: &'static str = TUPLEHASHXOF256_NAME; + const PARALLELHASH_ALG_NAME: &'static str = PARALLELHASH256_NAME; + const PARALLELHASHXOF_ALG_NAME: &'static str = PARALLELHASHXOF256_NAME; } /// Assigned by NIST in the Computer Security Objects Register: id-shake256 { hashAlgs 12 } impl AlgorithmOID for SHAKE256 { diff --git a/crypto/sha3/src/parallelhash.rs b/crypto/sha3/src/parallelhash.rs new file mode 100644 index 00000000..fdfd602e --- /dev/null +++ b/crypto/sha3/src/parallelhash.rs @@ -0,0 +1,311 @@ +//! ParallelHash, the parallelisable hash of NIST SP 800-185 Sec 6. + +use crate::SHAKEParams; +use crate::cshake::{CSHAKEInternal, absorb_left_encode_into}; +use crate::shake::{SHAKEInternal, SHAKEOutput}; +use crate::xof_utils::right_encode; +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XofOutput}; + +/// The function-name string every ParallelHash binds, per SP 800-185 Sec 6.3. +const PARALLELHASH_FUNCTION_NAME: &[u8] = b"ParallelHash"; + +/// The shared machinery of [`ParallelHashInternal`] and [`ParallelHashXOFInternal`]: the outer +/// cSHAKE, the block buffer, and the count of blocks hashed so far. +struct ParallelState { + cshake: CSHAKEInternal, + block_size: usize, + /// The partial block still being filled. Bounded by `block_size`, which the caller chooses at + /// construction, so this cannot be a const-sized array. + buffer: Vec, + blocks: u64, +} + +impl ParallelState { + /// Each block is hashed to `2c` bits -- 256 for ParallelHash128, 512 for ParallelHash256 + /// (Sec 6.3 step 3, the `256` and `512` in the inner cSHAKE calls). + const INNER_LEN: usize = (PARAMS::SIZE as usize) / 4; + + fn new(block_size: usize, customization: &[u8]) -> Self { + assert!(block_size > 0, "SP 800-185 Sec 6.2: the block size B must be positive"); + let mut cshake = CSHAKEInternal::new(PARALLELHASH_FUNCTION_NAME, customization); + // Step 2: z = left_encode(B). + absorb_left_encode_into(&mut cshake, block_size as u64); + Self { cshake, block_size, buffer: Vec::new(), blocks: 0 } + } + + /// Step 3 for one whole block: hash it and absorb the digest into the outer cSHAKE. + /// + /// The inner call is `cSHAKE(block, 2c, "", "")`, which by Sec 3.3 step 1 is plain SHAKE -- + /// so SHAKE is what is used here. + fn absorb_block(&mut self, block: &[u8]) { + let inner = SHAKEInternal::::new().hash_xof(block, Self::INNER_LEN); + self.cshake.do_update(&inner); + self.blocks += 1; + } + + fn do_update(&mut self, mut data: &[u8]) { + // Top up a partial block first, then take whole blocks straight from `data` so that a + // caller feeding block-aligned input never copies. + if !self.buffer.is_empty() { + let need = self.block_size - self.buffer.len(); + let take = need.min(data.len()); + self.buffer.extend_from_slice(&data[..take]); + data = &data[take..]; + if self.buffer.len() == self.block_size { + let block = core::mem::take(&mut self.buffer); + self.absorb_block(&block); + } + } + while data.len() >= self.block_size { + let (block, rest) = data.split_at(self.block_size); + self.absorb_block(block); + data = rest; + } + self.buffer.extend_from_slice(data); + } + + /// Flushes the short final block, then binds the block count and the length (steps 3 and 4). + /// + /// `length_bits` is `right_encode`'s argument: the requested output length for the + /// fixed-length function, or 0 for the XOF (Sec 6.3.1). + fn finish(mut self, length_bits: u64) -> CSHAKEInternal { + if !self.buffer.is_empty() { + let block = core::mem::take(&mut self.buffer); + self.absorb_block(&block); + } + // Step 4: z = z || right_encode(n) || right_encode(L). + for value in [self.blocks, length_bits] { + let (buf, len) = right_encode(value); + self.cshake.do_update(&buf[..len]); + } + self.cshake + } +} + +/// Internal struct for ParallelHash. Use [`crate::PARALLELHASH128`] or [`crate::PARALLELHASH256`]. +/// +/// ParallelHash splits the message into `B`-byte blocks, hashes each independently, and hashes the +/// concatenated digests (Sec 6.1). The point is that the per-block hashes can be computed in +/// parallel on long inputs; this implementation is sequential, which gives identical output. +/// +/// ```text +/// ParallelHash128(X, B, L, S) = cSHAKE128(left_encode(B) || SHAKE128(X[0], 256) || ... +/// || right_encode(n) || right_encode(L), +/// L, "ParallelHash", S) +/// ``` +/// +/// # The block size is part of the hash +/// +/// `B` is bound by `left_encode(B)`, so the same message under a different block size gives an +/// unrelated result. It is a parameter of the function, not a tuning knob. +/// +/// Unlike [`crate::TUPLEHASH128`], `do_update` here *is* ordinary byte-wise streaming: the block +/// boundaries come from `B`, not from how the caller chunks its calls. +pub struct ParallelHashInternal { + state: ParallelState, + output_len: usize, +} + +impl Algorithm for ParallelHashInternal { + const ALG_NAME: &'static str = PARAMS::PARALLELHASH_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl ParallelHashInternal { + /// A new ParallelHash over `block_size`-byte blocks, producing `output_len` bytes. + /// + /// # Panics + /// If `block_size` is zero, which Sec 6.2 forbids (`0 < B`). + pub fn new(block_size: usize, customization: &[u8], output_len: usize) -> Self { + Self { state: ParallelState::new(block_size, customization), output_len } + } +} + +impl Hash for ParallelHashInternal { + fn block_bitlen(&self) -> usize { + self.state.cshake.block_bitlen() + } + + fn output_len(&self) -> usize { + self.output_len + } + + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.do_final_out(output) + } + + fn do_update(&mut self, data: &[u8]) { + self.state.do_update(data); + } + + fn do_final(self) -> Vec { + let n = self.output_len; + self.state.finish((n as u64) * 8).into_output().do_output(n) + } + + fn do_final_out(self, output: &mut [u8]) -> usize { + let n = self.output_len; + self.state.finish((n as u64) * 8).into_output().do_output_out(&mut output[..n]) + } + + /// # Errors + /// Always [`HashError::InvalidLength`] for a non-zero `num_bits`: the block count and length + /// encodings have to follow the message, which a partial final byte would prevent. + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let mut out = vec![0u8; self.output_len]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "ParallelHash cannot take a partial final byte: the encodings must follow", + )); + } + Ok(self.do_final_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::from_bits(PARAMS::SIZE as usize) + } +} + +/// Internal struct for ParallelHashXOF (Sec 6.3.1). Use [`crate::PARALLELHASHXOF128`] or +/// [`crate::PARALLELHASHXOF256`]. +/// +/// Binds `right_encode(0)` in place of the output length, so -- as for KMACXOF and TupleHashXOF -- +/// it is a different function from the fixed-length one, and its output at one length is a prefix +/// of its output at a longer one. +pub struct ParallelHashXOFInternal { + state: ParallelState, +} + +impl Algorithm for ParallelHashXOFInternal { + const ALG_NAME: &'static str = PARAMS::PARALLELHASHXOF_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl ParallelHashXOFInternal { + /// A new ParallelHashXOF over `block_size`-byte blocks. + /// + /// # Panics + /// If `block_size` is zero (Sec 6.2). + pub fn new(block_size: usize, customization: &[u8]) -> Self { + Self { state: ParallelState::new(block_size, customization) } + } +} + +impl Hash for ParallelHashXOFInternal { + fn block_bitlen(&self) -> usize { + self.state.cshake.block_bitlen() + } + + /// The nominal length, 32 or 64 bytes; not bound into the computation. + fn output_len(&self) -> usize { + self.state.cshake.output_len() + } + + fn hash(mut self, data: &[u8]) -> Vec { + let n = self.output_len(); + self.do_update(data); + self.into_output().do_output(n) + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.into_output().do_output_out(output) + } + + fn do_update(&mut self, data: &[u8]) { + self.state.do_update(data); + } + + fn do_final(self) -> Vec { + let n = self.output_len(); + self.into_output().do_output(n) + } + + fn do_final_out(self, output: &mut [u8]) -> usize { + self.into_output().do_output_out(output) + } + + /// # Errors + /// Always [`HashError::InvalidLength`] for a non-zero `num_bits`; see + /// [`ParallelHashInternal::do_final_partial_bits`]. + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let mut out = vec![0u8; self.output_len()]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "ParallelHashXOF cannot take a partial final byte: the encodings must follow", + )); + } + Ok(self.do_final_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::from_bits(PARAMS::SIZE as usize) + } +} + +impl XOF for ParallelHashXOFInternal { + type Output = SHAKEOutput; + + fn into_output(self) -> Self::Output { + // Sec 6.3.1 step 4: right_encode(0) rather than the length. + self.state.finish(0).into_output() + } + + fn into_output_partial_bits( + self, + _partial_byte: u8, + num_bits: usize, + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "ParallelHashXOF cannot take a partial final byte: the encodings must follow", + )); + } + Ok(self.into_output()) + } + + fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { + self.do_update(data); + self.into_output().do_output(result_len) + } + + fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.into_output().do_output_out(output) + } +} diff --git a/crypto/sha3/tests/parallelhash_tests.rs b/crypto/sha3/tests/parallelhash_tests.rs new file mode 100644 index 00000000..f9d44742 --- /dev/null +++ b/crypto/sha3/tests/parallelhash_tests.rs @@ -0,0 +1,220 @@ +//! ParallelHash against the NIST SP 800-185 sample values. +//! +//! Vectors come from the `bc-test-data` repo cloned alongside this one; see `cshake_tests.rs`. + +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Algorithm, Hash, XOF}; +use bouncycastle_hex as hex; +use bouncycastle_sha3::{PARALLELHASH128, PARALLELHASH256, PARALLELHASHXOF128, PARALLELHASHXOF256}; +use std::fs; +use std::path::Path; + +const DATA_DIRS: [&str; 2] = + ["../../../bc-test-data/crypto/sp800-185", "../bc-test-data/crypto/sp800-185"]; + +struct Vector { + strength: usize, + block_size: usize, + s: String, + output_len: usize, + msg: Vec, + output: Vec, +} + +fn read_vectors(filename: &str) -> Option> { + let Some(dir) = DATA_DIRS.into_iter().find(|d| Path::new(d).exists()) else { + println!("WARNING: bc-test-data not found; ParallelHash sample-value tests skipped"); + return None; + }; + let path = Path::new(dir).join(filename); + let content = fs::read_to_string(&path).unwrap_or_else(|e| { + panic!("bc-test-data is present but {} is unreadable: {e}", path.display()) + }); + + let mut out = Vec::new(); + let mut cur: Vec<(String, String)> = Vec::new(); + let finish = |cur: &mut Vec<(String, String)>, out: &mut Vec| { + if cur.is_empty() { + return; + } + let get = |k: &str| cur.iter().find(|(a, _)| a == k).map(|(_, b)| b.clone()); + out.push(Vector { + strength: get("Strength").expect("Strength").parse().expect("a number"), + block_size: get("B").expect("B").parse().expect("a number"), + s: get("S").unwrap_or_default(), + output_len: get("Outputlen").expect("Outputlen").parse().expect("a number"), + msg: hex::decode(get("Msg").expect("Msg")).expect("hex"), + output: hex::decode(get("Output").expect("Output")).expect("hex"), + }); + cur.clear(); + }; + for line in content.lines() { + let line = line.trim_end(); + if line.starts_with('#') || line.is_empty() { + continue; + } + let Some((k, v)) = line.split_once(" = ") else { continue }; + if k == "COUNT" { + finish(&mut cur, &mut out); + } else { + cur.push((k.to_string(), v.to_string())); + } + } + finish(&mut cur, &mut out); + Some(out) +} + +/// ParallelHash (Sec 6.3): the output length is bound into the input. +#[test] +fn nist_sp800_185_parallelhash_sample_values() { + let Some(vectors) = read_vectors("ParallelHash.rsp") else { return }; + assert!(!vectors.is_empty()); + + for (i, v) in vectors.iter().enumerate() { + let want = v.output_len / 8; + let got = match v.strength { + 128 => 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), + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!( + got, v.output, + "COUNT {i}: ParallelHash{} B={} S={:?}", + v.strength, v.block_size, v.s + ); + } + println!("ParallelHash: {} sample values", vectors.len()); +} + +/// ParallelHashXOF (Sec 6.3.1): `right_encode(0)` in place of the length. +#[test] +fn nist_sp800_185_parallelhashxof_sample_values() { + let Some(vectors) = read_vectors("ParallelHashXOF.rsp") else { return }; + assert!(!vectors.is_empty()); + + for (i, v) in vectors.iter().enumerate() { + let want = v.output_len / 8; + let got = match v.strength { + 128 => PARALLELHASHXOF128::new(v.block_size, v.s.as_bytes()).hash_xof(&v.msg, want), + 256 => PARALLELHASHXOF256::new(v.block_size, v.s.as_bytes()).hash_xof(&v.msg, want), + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!( + got, v.output, + "COUNT {i}: ParallelHashXOF{} B={} S={:?}", + v.strength, v.block_size, v.s + ); + } + println!("ParallelHashXOF: {} sample values", vectors.len()); +} + +/// The two are different functions on identical inputs. +#[test] +fn parallelhashxof_is_not_parallelhash_truncated() { + let (Some(fixed), Some(xof)) = + (read_vectors("ParallelHash.rsp"), read_vectors("ParallelHashXOF.rsp")) + else { + return; + }; + assert_eq!(fixed.len(), xof.len()); + for (i, (f, x)) in fixed.iter().zip(xof.iter()).enumerate() { + assert_eq!(f.msg, x.msg, "COUNT {i}: the sample pairs share a message"); + assert_eq!(f.block_size, x.block_size, "COUNT {i}: ... and a block size"); + assert_ne!(f.output, x.output, "COUNT {i}: the two functions must differ"); + } +} + +/// Unlike TupleHash, ParallelHash *is* ordinary byte-wise streaming: the blocks come from `B`, not +/// from how the caller chunks its `do_update` calls. Chunkings that straddle block boundaries are +/// the interesting ones, so this walks a range of chunk sizes against a block size of 8. +#[test] +fn chunking_does_not_change_the_result() { + let msg: Vec = (0..=200u8).collect(); + let one = PARALLELHASH128::new(8, b"S", 32).hash(&msg); + + for chunk in [1usize, 3, 7, 8, 9, 16, 64, 201] { + let mut p = PARALLELHASH128::new(8, b"S", 32); + for piece in msg.chunks(chunk) { + p.do_update(piece); + } + assert_eq!(p.do_final(), one, "chunk size {chunk} must not change the result"); + } +} + +/// Sec 6.2: `B` is a parameter of the function. The same message under a different block size is a +/// different hash, not a re-arrangement of the same work. +#[test] +fn the_block_size_is_part_of_the_hash() { + let msg: Vec = (0..=100u8).collect(); + let b8 = PARALLELHASH128::new(8, b"", 32).hash(&msg); + let b12 = PARALLELHASH128::new(12, b"", 32).hash(&msg); + let b16 = PARALLELHASH128::new(16, b"", 32).hash(&msg); + assert_ne!(b8, b12); + assert_ne!(b8, b16); + assert_ne!(b12, b16); +} + +/// A short final block, an exactly-full final block, and an empty message are the boundary cases +/// of the block loop. +/// +/// This test matters more than it looks: **every published ParallelHash sample value has a +/// block-aligned message** (24 bytes at B = 8, 72 at B = 12), so the NIST vectors never exercise a +/// short final block at all. Deleting the flush of the partial buffer passes all twelve of them +/// and fails only here. +#[test] +fn block_boundary_cases() { + // exactly one full block, versus one full block plus one byte + let full = PARALLELHASH128::new(8, b"", 32).hash(&[0xAAu8; 8]); + let plus = PARALLELHASH128::new(8, b"", 32).hash(&[0xAAu8; 9]); + assert_ne!(full, plus); + + // two full blocks versus one short block: different block counts, so different output + let two = PARALLELHASH128::new(8, b"", 32).hash(&[0xAAu8; 16]); + assert_ne!(two, full); + + // an empty message is zero blocks, and must still produce a hash + let empty = PARALLELHASH128::new(8, b"", 32).hash(b""); + assert_eq!(empty.len(), 32); + assert_ne!(empty, full); +} + +/// The XOF's output at one length is a prefix of its output at a longer one; the fixed-length +/// function's is not. +#[test] +fn length_binding_differs_between_the_two() { + let msg = b"parallel"; + let short = PARALLELHASH128::new(4, b"", 16).hash(msg); + let long = PARALLELHASH128::new(4, b"", 32).hash(msg); + assert_ne!(&long[..16], &short[..], "ParallelHash: a different length is a different function"); + + let short = PARALLELHASHXOF128::new(4, b"").hash_xof(msg, 16); + let long = PARALLELHASHXOF128::new(4, b"").hash_xof(msg, 32); + assert_eq!(&long[..16], &short[..], "ParallelHashXOF: one stream, so shorter is a prefix"); +} + +/// 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(8, b"", 32); + p.do_update(b"abc"); + assert!(matches!(p.do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); + + let mut p = PARALLELHASHXOF128::new(8, b""); + p.do_update(b"abc"); + assert!(matches!(p.into_output_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); +} + +/// Sec 6.2 forbids a zero block size. +#[test] +#[should_panic(expected = "block size B must be positive")] +fn zero_block_size_is_rejected() { + let _ = PARALLELHASH128::new(0, b"", 32); +} + +#[test] +fn algorithm_names() { + assert_eq!(PARALLELHASH128::ALG_NAME, "ParallelHash128"); + assert_eq!(PARALLELHASH256::ALG_NAME, "ParallelHash256"); + assert_eq!(PARALLELHASHXOF128::ALG_NAME, "ParallelHashXOF128"); + assert_eq!(PARALLELHASHXOF256::ALG_NAME, "ParallelHashXOF256"); +} From 5c6302a1aeca7217b0a169ed84f9a91102229609 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 19:29:46 +1000 Subject: [PATCH 12/22] cli: add tuplehash and parallelhash subcommands, completing SP 800-185 on the command line --- cli/src/main.rs | 89 +++++++++++++++++++++++++++++++++++ cli/src/sha3_cmd.rs | 111 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 199 insertions(+), 1 deletion(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 6257325b..bf734077 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -158,6 +158,83 @@ enum Subcommands { x: bool, }, + /// Perform TupleHash128 (NIST SP 800-185 Sec 5) over a tuple of strings. The tuple is given + /// by repeated --element flags, each in hex; with none, stdin is hashed as a single element. + /// The boundaries between elements are part of the hash. + TUPLEHASH128 { + /// 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. + customization: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform TupleHash256 (NIST SP 800-185 Sec 5). See tuplehash128. + TUPLEHASH256 { + /// 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. + customization: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform ParallelHash128 (NIST SP 800-185 Sec 6) of the content provided on stdin. + /// The block size is part of the function: the same input under a different block size gives + /// an unrelated hash, so both sides must use the same value. + /// Supports streaming update for low memory footprint. + PARALLELHASH128 { + /// 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. + customization: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform ParallelHash256 (NIST SP 800-185 Sec 6). See parallelhash128. + PARALLELHASH256 { + /// 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. + customization: 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. @@ -1151,6 +1228,18 @@ fn main() { Some(Subcommands::CSHAKE128 { length, customization, function_name, x }) => { sha3_cmd::cshake_cmd(128, *length, function_name, customization, *x); } + Some(Subcommands::TUPLEHASH128 { length, elements, customization, x }) => { + sha3_cmd::tuplehash_cmd(128, *length, elements, customization, *x); + } + Some(Subcommands::TUPLEHASH256 { length, elements, customization, x }) => { + sha3_cmd::tuplehash_cmd(256, *length, elements, customization, *x); + } + Some(Subcommands::PARALLELHASH128 { length, block_size, customization, x }) => { + sha3_cmd::parallelhash_cmd(128, *length, *block_size, customization, *x); + } + Some(Subcommands::PARALLELHASH256 { length, block_size, customization, x }) => { + sha3_cmd::parallelhash_cmd(256, *length, *block_size, customization, *x); + } Some(Subcommands::KMAC128 { length, customization, key, key_file, verify, x }) => { mac_cmd::kmac_cmd(128, *length, customization, key, key_file, verify, *x) } diff --git a/cli/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index 1f5205aa..2835e122 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -2,9 +2,12 @@ use bouncycastle::core::traits::{Hash, XOF, XofOutput}; use std::io; use std::io::{Read, Write}; +use bouncycastle::hex; use bouncycastle::sha3::{ - CSHAKE128, CSHAKE256, SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, + CSHAKE128, CSHAKE256, PARALLELHASH128, PARALLELHASH256, SHA3_224, SHA3_256, SHA3_384, SHA3_512, + SHAKE128, SHAKE256, TUPLEHASH128, TUPLEHASH256, }; +use std::process::exit; pub(crate) fn sha3_cmd(bit_len: usize, output_hex: bool) { match bit_len { @@ -66,6 +69,112 @@ pub(crate) fn cshake_cmd( } } +/// 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 +/// 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, + output_len: usize, + elements: &[String], + customization: &Option, + 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() + }; + 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), + _ => panic!("Unsupported algorithm: TupleHash-{bit_len}"), + }; + write_out(&out, output_hex); +} + +/// 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 +/// block size gives an unrelated hash, so it must match on both sides. +pub(crate) fn parallelhash_cmd( + bit_len: usize, + output_len: usize, + block_size: usize, + customization: &Option, + output_hex: bool, +) { + if block_size == 0 { + 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(); + match bit_len { + 128 => { + let mut p = PARALLELHASH128::new(block_size, s, output_len); + 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); + stream_stdin(|chunk| p.do_update(chunk)); + write_out(&p.do_final(), output_hex); + } + _ => panic!("Unsupported algorithm: ParallelHash-{bit_len}"), + } +} + +/// 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]); + } +} + +/// Writes the digest as raw bytes or hex, with the trailing newline the other commands emit. +fn write_out(out: &[u8], output_hex: bool) { + if output_hex { + for b in out { + print!("{b:02x}"); + } + } else { + io::stdout().write_all(out).expect("Failed to write to stdout"); + } + println!(); +} + fn do_shake(mut shake: impl XOF, output_len: usize, output_hex: bool) { let mut buf: [u8; 1024] = [0u8; 1024]; // read from stdin From 68a3dbcef63341d88845905f99113e05a448a880 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 08:45:56 +1000 Subject: [PATCH 13/22] sha3: pin the Hash and XOF trait views of TupleHash, ParallelHash and KMAC against the sample values, plus KMAC's key-type and buffer-length checks; kills the 88 mutants the SP 800-185 suites had missed --- crypto/sha3/tests/kmac_tests.rs | 141 +++++++++++++++++++++++ crypto/sha3/tests/parallelhash_tests.rs | 119 ++++++++++++++++++++ crypto/sha3/tests/tuplehash_tests.rs | 142 ++++++++++++++++++++++++ 3 files changed, 402 insertions(+) diff --git a/crypto/sha3/tests/kmac_tests.rs b/crypto/sha3/tests/kmac_tests.rs index 612f09d8..a8a58600 100644 --- a/crypto/sha3/tests/kmac_tests.rs +++ b/crypto/sha3/tests/kmac_tests.rs @@ -2,6 +2,7 @@ //! //! Vectors come from the `bc-test-data` repo cloned alongside this one; see `cshake_tests.rs`. +use bouncycastle_core::errors::{KeyMaterialError, MACError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{Algorithm, Hash, MAC, XOF}; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; @@ -295,3 +296,143 @@ fn test_framework_xof() { &v.output, ); } + +/// `mac_out` and `do_final_out` against one sample value. The sample-value test above goes through +/// `mac` only, so these two, their returned lengths, and the buffer-length check in `do_final_out` +/// were all invisible to `cargo mutants`. +fn check_out_variants(make: impl Fn() -> M, msg: &[u8], expected: &[u8], ctx: &str) { + let n = expected.len(); + + let mut out = vec![0xFFu8; n]; + assert_eq!(make().mac_out(msg, &mut out).unwrap(), n, "{ctx}: mac_out returns the length"); + assert_eq!(out, expected, "{ctx}: mac_out"); + + // mac_out zero-fills the whole buffer first, so a longer one ends in zeros + let mut out = vec![0xFFu8; n + 5]; + assert_eq!(make().mac_out(msg, &mut out).unwrap(), n); + assert_eq!(&out[..n], expected, "{ctx}: mac_out, oversized buffer"); + assert_eq!(&out[n..], &[0u8; 5], "{ctx}: mac_out zeroizes past the tag"); + + let mut m = make(); + msg.chunks(7).for_each(|c| m.do_update(c)); + let mut out = vec![0xFFu8; n]; + assert_eq!(m.do_final_out(&mut out).unwrap(), n, "{ctx}: do_final_out returns the length"); + assert_eq!(out, expected, "{ctx}: do_final_out"); + + // do_final_out writes exactly output_len bytes and leaves the rest alone + let mut m = make(); + m.do_update(msg); + let mut out = vec![0xFFu8; n + 5]; + assert_eq!(m.do_final_out(&mut out).unwrap(), n); + assert_eq!(&out[..n], expected, "{ctx}: do_final_out, oversized buffer"); + assert_eq!(&out[n..], &[0xFFu8; 5], "{ctx}: do_final_out leaves bytes past the tag"); + + // a buffer one byte short is refused, by both + let mut out = vec![0u8; n - 1]; + assert!( + matches!(make().do_final_out(&mut out), Err(MACError::InvalidLength(_))), + "{ctx}: do_final_out must refuse a short buffer" + ); + assert!( + matches!(make().mac_out(msg, &mut out), Err(MACError::InvalidLength(_))), + "{ctx}: mac_out must refuse a short buffer" + ); +} + +#[test] +fn mac_out_and_do_final_out_agree_with_the_sample_values() { + let Some(vectors) = read_vectors("KMAC.rsp") else { return }; + for (i, v) in vectors.iter().enumerate() { + let n = v.output_len / 8; + let key = key_material(&v.key); + let s = v.s.as_bytes(); + let ctx = format!("COUNT {i}: KMAC{} S={:?}", v.strength, v.s); + match v.strength { + 128 => check_out_variants( + || KMAC128::new_with_params(&key, s, n, false).unwrap(), + &v.msg, + &v.output, + &ctx, + ), + 256 => check_out_variants( + || KMAC256::new_with_params(&key, s, n, false).unwrap(), + &v.msg, + &v.output, + &ctx, + ), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + } +} + +/// `new_allow_weak_key` is `new` without the strength check: same customization, same nominal +/// length, same tag. +#[test] +fn new_allow_weak_key_uses_the_nominal_length() { + let key = key_material(&[0x42u8; 32]); + + let k = KMAC128::new_allow_weak_key(&key).unwrap(); + assert_eq!(k.output_len(), 32); + assert_eq!(k.mac(b"abc"), KMAC128::new(&key).unwrap().mac(b"abc")); + + let k = KMAC256::new_allow_weak_key(&key).unwrap(); + assert_eq!(k.output_len(), 64); + assert_eq!(k.mac(b"abc"), KMAC256::new(&key).unwrap().mac(b"abc")); +} + +/// The same stance as HMAC: a key tagged `MACKey` or `Zeroized` is accepted, anything else is +/// refused as the wrong type. A zeroized key carries no security strength, so it also needs +/// `allow_weak_key`. +#[test] +fn key_type_is_checked() { + let cipher_key = + KeyMaterial::<32>::from_bytes_as_type(&[0x42u8; 32], KeyType::SymmetricCipherKey).unwrap(); + assert!(matches!( + KMAC128::new(&cipher_key), + Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) + )); + assert!(matches!( + KMAC128::new_with_params(&cipher_key, b"", 32, true), + Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) + )); + assert!(matches!( + KMACXOF128::new(&cipher_key, b"", true), + Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) + )); + + let zero = KeyMaterial::<32>::new(); + assert_eq!(zero.key_type(), KeyType::Zeroized); + assert!(KMAC128::new(&zero).is_err(), "a zeroized key has no security strength"); + assert!(KMAC128::new_with_params(&zero, b"", 32, true).is_ok(), "... but is the right type"); + assert!(KMAC128::new_allow_weak_key(&zero).is_ok()); + assert!(KMACXOF128::new(&zero, b"", true).is_ok()); +} + +/// The `Hash` view of the partial-byte entry points on KMACXOF: zero bits is the byte-aligned case +/// and yields the same bytes as `do_final`; anything else is refused. The test above only covers +/// the `XOF` entry point, `into_output_partial_bits`. +#[test] +fn kmacxof_hash_view_partial_bits() { + let key = key_material(&[0x42u8; 32]); + let fresh = || { + let mut k = KMACXOF128::new(&key, b"", false).unwrap(); + k.do_update(b"abc"); + k + }; + let expected = fresh().do_final(); + assert_eq!(expected.len(), 32); + + assert_eq!(fresh().do_final_partial_bits(0, 0).unwrap(), expected); + let mut out = vec![0u8; 32]; + assert_eq!(fresh().do_final_partial_bits_out(0, 0, &mut out).unwrap(), 32); + assert_eq!(out, expected); + + assert!(matches!( + fresh().do_final_partial_bits(0xF0, 4), + Err(bouncycastle_core::errors::HashError::InvalidLength(_)) + )); + assert!(matches!( + fresh().do_final_partial_bits_out(0xF0, 4, &mut out), + Err(bouncycastle_core::errors::HashError::InvalidLength(_)) + )); +} diff --git a/crypto/sha3/tests/parallelhash_tests.rs b/crypto/sha3/tests/parallelhash_tests.rs index f9d44742..9d0fec90 100644 --- a/crypto/sha3/tests/parallelhash_tests.rs +++ b/crypto/sha3/tests/parallelhash_tests.rs @@ -218,3 +218,122 @@ fn algorithm_names() { assert_eq!(PARALLELHASHXOF128::ALG_NAME, "ParallelHashXOF128"); assert_eq!(PARALLELHASHXOF256::ALG_NAME, "ParallelHashXOF256"); } + +/// Sponge rates from FIPS 202 Table 3, the nominal lengths of the XOF forms, and the constructed +/// length of the fixed forms. The generic checks elsewhere only require these to be positive. +#[test] +fn metadata() { + assert_eq!(PARALLELHASH128::new(8, b"", 32).block_bitlen(), 1344, "cSHAKE128 rate"); + assert_eq!(PARALLELHASH256::new(8, b"", 64).block_bitlen(), 1088, "cSHAKE256 rate"); + assert_eq!(PARALLELHASHXOF128::new(8, b"").block_bitlen(), 1344); + assert_eq!(PARALLELHASHXOF256::new(8, b"").block_bitlen(), 1088); + + assert_eq!(PARALLELHASH128::new(8, b"", 17).output_len(), 17, "whatever was asked for"); + assert_eq!(PARALLELHASH256::new(8, b"", 100).output_len(), 100); + assert_eq!(PARALLELHASHXOF128::new(8, b"").output_len(), 32, "the nominal length"); + assert_eq!(PARALLELHASHXOF256::new(8, b"").output_len(), 64); +} + +/// Every `Hash` entry point of the fixed-length form, against one sample value. +/// +/// The sample-value test above goes through `hash` only, which left `hash_out` and +/// `do_final_out` unexercised: `cargo mutants` could replace each with a constant, and change the +/// `* 8` in the `right_encode(L)` that `do_final_out` binds, without a test noticing. +fn check_fixed_view(make: impl Fn() -> H, msg: &[u8], expected: &[u8], ctx: &str) { + let n = expected.len(); + assert_eq!(make().output_len(), n, "{ctx}: output_len"); + + let mut out = vec![0u8; n]; + assert_eq!(make().hash_out(msg, &mut out), n, "{ctx}: hash_out returns the length"); + assert_eq!(out, expected, "{ctx}: hash_out"); + + let mut h = make(); + msg.chunks(5).for_each(|c| h.do_update(c)); + let mut out = vec![0u8; n]; + assert_eq!(h.do_final_out(&mut out), n, "{ctx}: do_final_out returns the length"); + assert_eq!(out, expected, "{ctx}: do_final_out"); + + // a longer buffer is only written up to the output length + let mut h = make(); + h.do_update(msg); + let mut out = vec![0xFFu8; n + 7]; + assert_eq!(h.do_final_out(&mut out), n); + assert_eq!(&out[..n], expected, "{ctx}: do_final_out, oversized buffer"); + assert_eq!(&out[n..], &[0xFFu8; 7], "{ctx}: bytes past the output length are untouched"); +} + +/// Every `Hash` and `XOF` entry point of the XOF form, against one sample value. The samples ask +/// for the nominal length, so `do_final` and `hash` must reproduce them exactly. +fn check_xof_view(make: impl Fn() -> X, msg: &[u8], expected: &[u8], ctx: &str) { + let n = expected.len(); + assert_eq!(make().output_len(), n, "{ctx}: the samples ask for the nominal length"); + + assert_eq!(make().hash(msg), expected, "{ctx}: hash"); + + let mut out = vec![0u8; n]; + assert_eq!(make().hash_out(msg, &mut out), n, "{ctx}: hash_out returns the length"); + assert_eq!(out, expected, "{ctx}: hash_out"); + + let mut x = make(); + msg.chunks(5).for_each(|c| x.do_update(c)); + assert_eq!(x.do_final(), expected, "{ctx}: do_final"); + + let mut x = make(); + x.do_update(msg); + let mut out = vec![0u8; n]; + assert_eq!(x.do_final_out(&mut out), n, "{ctx}: do_final_out returns the length"); + assert_eq!(out, expected, "{ctx}: do_final_out"); + + // zero partial bits is the byte-aligned case and must be accepted; any other count refused + let mut x = make(); + x.do_update(msg); + assert_eq!(x.do_final_partial_bits(0, 0).unwrap(), expected, "{ctx}: do_final_partial_bits(0)"); + + let mut x = make(); + x.do_update(msg); + let mut out = vec![0u8; n]; + assert_eq!(x.do_final_partial_bits_out(0, 0, &mut out).unwrap(), n, "{ctx}: ..._out length"); + assert_eq!(out, expected, "{ctx}: do_final_partial_bits_out(0)"); + + assert!(matches!(make().do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); + let mut out = vec![0u8; n]; + assert!(matches!( + make().do_final_partial_bits_out(0xF0, 4, &mut out), + Err(HashError::InvalidLength(_)) + )); + + assert_eq!(make().hash_xof(msg, n / 2), &expected[..n / 2], "{ctx}: hash_xof, shorter"); + + let mut out = vec![0u8; n]; + assert_eq!(make().hash_xof_out(msg, &mut out), n, "{ctx}: hash_xof_out returns the length"); + assert_eq!(out, expected, "{ctx}: hash_xof_out"); +} + +#[test] +fn hash_trait_view_agrees_with_the_sample_values() { + let Some(vectors) = read_vectors("ParallelHash.rsp") else { return }; + for (i, v) in vectors.iter().enumerate() { + let n = v.output_len / 8; + let (b, s) = (v.block_size, v.s.as_bytes()); + let ctx = format!("COUNT {i}: ParallelHash{} B={b}", v.strength); + match v.strength { + 128 => check_fixed_view(|| PARALLELHASH128::new(b, s, n), &v.msg, &v.output, &ctx), + 256 => check_fixed_view(|| PARALLELHASH256::new(b, s, n), &v.msg, &v.output, &ctx), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + } +} + +#[test] +fn xof_trait_view_agrees_with_the_sample_values() { + let Some(vectors) = read_vectors("ParallelHashXOF.rsp") else { return }; + for (i, v) in vectors.iter().enumerate() { + let (b, s) = (v.block_size, v.s.as_bytes()); + let ctx = format!("COUNT {i}: ParallelHashXOF{} B={b}", v.strength); + match v.strength { + 128 => check_xof_view(|| PARALLELHASHXOF128::new(b, s), &v.msg, &v.output, &ctx), + 256 => check_xof_view(|| PARALLELHASHXOF256::new(b, s), &v.msg, &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 263e898b..a4a164c3 100644 --- a/crypto/sha3/tests/tuplehash_tests.rs +++ b/crypto/sha3/tests/tuplehash_tests.rs @@ -207,3 +207,145 @@ fn algorithm_names() { assert_eq!(TUPLEHASHXOF128::ALG_NAME, "TupleHashXOF128"); assert_eq!(TUPLEHASHXOF256::ALG_NAME, "TupleHashXOF256"); } + +/// Sponge rates from FIPS 202 Table 3, the nominal lengths of the XOF forms, and the constructed +/// 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!(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!(TUPLEHASHXOF128::new(b"").output_len(), 32, "the nominal length"); + assert_eq!(TUPLEHASHXOF256::new(b"").output_len(), 64); +} + +/// Every `Hash` entry point of the fixed-length form, against one sample value. +/// +/// The sample-value test above goes through `hash_tuple` only, which left `hash`, `hash_out` and +/// `do_final_out` unexercised: `cargo mutants` could replace each with a constant, and change the +/// `* 8` in the `right_encode(L)` that `do_final_out` absorbs, without a test noticing. +fn check_fixed_view(make: impl Fn() -> H, tuple: &[&[u8]], expected: &[u8], ctx: &str) { + let n = expected.len(); + assert_eq!(make().output_len(), n, "{ctx}: output_len"); + + // do_final_out into an exact buffer + let mut h = make(); + tuple.iter().for_each(|e| h.do_update(e)); + let mut out = vec![0u8; n]; + assert_eq!(h.do_final_out(&mut out), n, "{ctx}: do_final_out returns the length"); + assert_eq!(out, expected, "{ctx}: do_final_out"); + + // ... and into a longer one, which is only written up to the output length + let mut h = make(); + tuple.iter().for_each(|e| h.do_update(e)); + let mut out = vec![0xFFu8; n + 7]; + assert_eq!(h.do_final_out(&mut out), n); + assert_eq!(&out[..n], expected, "{ctx}: do_final_out, oversized buffer"); + assert_eq!(&out[n..], &[0xFFu8; 7], "{ctx}: bytes past the output length are untouched"); + + // hash and hash_out take one element: the last, after the rest have been fed in + let Some((last, rest)) = tuple.split_last() else { return }; + let mut h = make(); + rest.iter().for_each(|e| h.do_update(e)); + assert_eq!(h.hash(last), expected, "{ctx}: hash as the final element"); + + let mut h = make(); + rest.iter().for_each(|e| h.do_update(e)); + let mut out = vec![0u8; n]; + assert_eq!(h.hash_out(last, &mut out), n, "{ctx}: hash_out returns the length"); + assert_eq!(out, expected, "{ctx}: hash_out"); +} + +/// Every `Hash` and `XOF` entry point of the XOF form, against one sample value. The samples ask +/// for the nominal length, so `do_final` and `hash` must reproduce them exactly. +fn check_xof_view(make: impl Fn() -> X, tuple: &[&[u8]], expected: &[u8], ctx: &str) { + let n = expected.len(); + assert_eq!(make().output_len(), n, "{ctx}: the samples ask for the nominal length"); + + let mut x = make(); + tuple.iter().for_each(|e| x.do_update(e)); + assert_eq!(x.do_final(), expected, "{ctx}: do_final"); + + let mut x = make(); + tuple.iter().for_each(|e| x.do_update(e)); + let mut out = vec![0u8; n]; + assert_eq!(x.do_final_out(&mut out), n, "{ctx}: do_final_out returns the length"); + assert_eq!(out, expected, "{ctx}: do_final_out"); + + // zero partial bits is the byte-aligned case and must be accepted; any other count refused + let mut x = make(); + tuple.iter().for_each(|e| x.do_update(e)); + assert_eq!(x.do_final_partial_bits(0, 0).unwrap(), expected, "{ctx}: do_final_partial_bits(0)"); + + let mut x = make(); + tuple.iter().for_each(|e| x.do_update(e)); + let mut out = vec![0u8; n]; + assert_eq!(x.do_final_partial_bits_out(0, 0, &mut out).unwrap(), n, "{ctx}: ..._out length"); + assert_eq!(out, expected, "{ctx}: do_final_partial_bits_out(0)"); + + assert!(matches!(make().do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); + let mut out = vec![0u8; n]; + assert!(matches!( + make().do_final_partial_bits_out(0xF0, 4, &mut out), + Err(HashError::InvalidLength(_)) + )); + + // the one-shots take one element: the last, after the rest have been fed in + let Some((last, rest)) = tuple.split_last() else { return }; + let mut x = make(); + rest.iter().for_each(|e| x.do_update(e)); + assert_eq!(x.hash(last), expected, "{ctx}: hash"); + + let mut x = make(); + rest.iter().for_each(|e| x.do_update(e)); + let mut out = vec![0u8; n]; + assert_eq!(x.hash_out(last, &mut out), n, "{ctx}: hash_out returns the length"); + assert_eq!(out, expected, "{ctx}: hash_out"); + + let mut x = make(); + rest.iter().for_each(|e| x.do_update(e)); + assert_eq!(x.hash_xof(last, n), expected, "{ctx}: hash_xof"); + + let mut x = make(); + rest.iter().for_each(|e| x.do_update(e)); + assert_eq!(x.hash_xof(last, n / 2), &expected[..n / 2], "{ctx}: hash_xof, shorter"); + + let mut x = make(); + rest.iter().for_each(|e| x.do_update(e)); + let mut out = vec![0u8; n]; + assert_eq!(x.hash_xof_out(last, &mut out), n, "{ctx}: hash_xof_out returns the length"); + assert_eq!(out, expected, "{ctx}: hash_xof_out"); +} + +#[test] +fn hash_trait_view_agrees_with_the_sample_values() { + let Some(vectors) = read_vectors("TupleHash.rsp") else { return }; + for (i, v) in vectors.iter().enumerate() { + let n = v.output_len / 8; + 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), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + } +} + +#[test] +fn xof_trait_view_agrees_with_the_sample_values() { + let Some(vectors) = read_vectors("TupleHashXOF.rsp") else { return }; + for (i, v) in vectors.iter().enumerate() { + let t = as_slices(&v.tuple); + let ctx = format!("COUNT {i}: TupleHashXOF{}", v.strength); + match v.strength { + 128 => check_xof_view(|| TUPLEHASHXOF128::new(v.s.as_bytes()), &t, &v.output, &ctx), + 256 => check_xof_view(|| TUPLEHASHXOF256::new(v.s.as_bytes()), &t, &v.output, &ctx), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + } +} From 50c125b292660de4a657f07bfdeffef34daf6d06 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 08:45:56 +1000 Subject: [PATCH 14/22] factory: replace the todo stub in xof_factory_tests with a differential suite against the SHAKE types; of 29 missed mutants only the equivalent default_128_bit one survives --- crypto/factory/tests/xof_factory_tests.rs | 150 +++++++++++++++++++++- 1 file changed, 147 insertions(+), 3 deletions(-) diff --git a/crypto/factory/tests/xof_factory_tests.rs b/crypto/factory/tests/xof_factory_tests.rs index 7e414f94..ac1ea32d 100644 --- a/crypto/factory/tests/xof_factory_tests.rs +++ b/crypto/factory/tests/xof_factory_tests.rs @@ -1,4 +1,148 @@ -#[cfg(test)] -mod tests { - // todo +//! `XOFFactory` is a pass-through to the SHAKE types in `bouncycastle-sha3`, so the oracle for +//! every method is the same call on the underlying type. Each check below runs the factory and the +//! direct type side by side on the same input; nothing here is an expected value written by hand. + +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +use bouncycastle_core_test_framework::xof::TestFrameworkXOF; +use bouncycastle_factory::xof_factory::XOFFactory; +use bouncycastle_factory::{AlgorithmFactory, FactoryError}; +use bouncycastle_sha3::{SHAKE128, SHAKE128_NAME, SHAKE256, SHAKE256_NAME}; + +const MSG: &[u8] = b"The quick brown fox jumps over the lazy dog"; + +/// Every `Hash`, `XOF` and `XofOutput` method of the factory against the direct type `S`. +fn check_against(make: impl Fn() -> XOFFactory, ctx: &str) { + let n = S::default().output_len(); + + // metadata + assert_eq!(make().block_bitlen(), S::default().block_bitlen(), "{ctx}: block_bitlen"); + assert_eq!(make().output_len(), n, "{ctx}: output_len"); + assert_eq!( + Hash::max_security_strength(&make()), + Hash::max_security_strength(&S::default()), + "{ctx}: max_security_strength" + ); + + // the Hash view + let expected = S::default().hash(MSG); + assert_eq!(expected.len(), n); + assert_eq!(make().hash(MSG), expected, "{ctx}: hash"); + + let mut out = vec![0u8; n]; + assert_eq!(make().hash_out(MSG, &mut out), n, "{ctx}: hash_out returns the length"); + assert_eq!(out, expected, "{ctx}: hash_out"); + + let mut f = make(); + MSG.chunks(5).for_each(|c| f.do_update(c)); + assert_eq!(f.do_final(), expected, "{ctx}: do_update then do_final"); + + let mut f = make(); + f.do_update(MSG); + let mut out = vec![0u8; n]; + assert_eq!(f.do_final_out(&mut out), n, "{ctx}: do_final_out returns the length"); + assert_eq!(out, expected, "{ctx}: do_final_out"); + + // partial final byte, which SHAKE accepts + let mut s = S::default(); + s.do_update(MSG); + let expected_bits = s.do_final_partial_bits(0x05, 3).unwrap(); + assert_ne!(expected_bits, expected, "three more bits must change the digest"); + + let mut f = make(); + f.do_update(MSG); + assert_eq!(f.do_final_partial_bits(0x05, 3).unwrap(), expected_bits, "{ctx}: partial bits"); + + let mut f = make(); + f.do_update(MSG); + let mut out = vec![0u8; n]; + assert_eq!(f.do_final_partial_bits_out(0x05, 3, &mut out).unwrap(), n, "{ctx}: ..._out length"); + assert_eq!(out, expected_bits, "{ctx}: do_final_partial_bits_out"); + + let mut f = make(); + f.do_update(MSG); + assert!( + matches!(f.do_final_partial_bits(0xFF, 8), Err(HashError::InvalidLength(_))), + "{ctx}: eight partial bits is not a partial byte" + ); + + // the XOF view: one stream, of which the Hash view is the first output_len bytes + let mut s = S::default(); + s.do_update(MSG); + let long = s.into_output().do_output(3 * n); + assert_eq!(&long[..n], &expected[..], "the direct type's hash is a prefix of its stream"); + + let mut f = make(); + f.do_update(MSG); + let mut fo = f.into_output(); + assert_eq!(fo.do_output(n), &long[..n], "{ctx}: do_output"); + let mut buf = vec![0u8; 2 * n]; + assert_eq!(fo.do_output_out(&mut buf), 2 * n, "{ctx}: do_output_out returns the length"); + assert_eq!(buf, &long[n..], "{ctx}: do_output_out continues the stream"); + + let mut s = S::default(); + s.do_update(MSG); + let want = s.into_output_partial_bits(0x05, 3).unwrap().do_output(n); + let mut f = make(); + f.do_update(MSG); + assert_eq!( + f.into_output_partial_bits(0x05, 3).unwrap().do_output(n), + want, + "{ctx}: into_output_partial_bits" + ); + let mut f = make(); + f.do_update(MSG); + assert!(matches!(f.into_output_partial_bits(0xFF, 8), Err(HashError::InvalidLength(_)))); + + // the one-shots + assert_eq!(make().hash_xof(MSG, 3 * n), long, "{ctx}: hash_xof"); + let mut out = vec![0xFFu8; 3 * n]; + assert_eq!(make().hash_xof_out(MSG, &mut out), 3 * n, "{ctx}: hash_xof_out returns the length"); + assert_eq!(out, long, "{ctx}: hash_xof_out"); +} + +#[test] +fn shake128_by_name_matches_the_direct_type() { + check_against::(|| XOFFactory::new(SHAKE128_NAME).unwrap(), "SHAKE128 by constant"); + check_against::(|| XOFFactory::new("SHAKE128").unwrap(), "SHAKE128 by string"); +} + +#[test] +fn shake256_by_name_matches_the_direct_type() { + check_against::(|| XOFFactory::new(SHAKE256_NAME).unwrap(), "SHAKE256 by constant"); + check_against::(|| XOFFactory::new("SHAKE256").unwrap(), "SHAKE256 by string"); +} + +/// The configured defaults: SHAKE128 for the general and 128-bit defaults, SHAKE256 for 256-bit. +#[test] +fn defaults() { + check_against::(XOFFactory::default, "default()"); + check_against::(XOFFactory::default_128_bit, "default_128_bit()"); + check_against::(XOFFactory::default_256_bit, "default_256_bit()"); +} + +#[test] +fn unknown_names_are_refused() { + for name in ["SHAKE512", "shake128", "", "cSHAKE128"] { + assert!( + matches!(XOFFactory::new(name), Err(FactoryError::UnsupportedAlgorithm(_))), + "{name:?} must not construct a XOF" + ); + } +} + +/// The shared `XOF` conformance suite, with the expected stream taken from the direct type. +#[test] +fn test_framework_xof() { + let framework = TestFrameworkXOF::new(); + framework.test_xof( + || XOFFactory::new(SHAKE128_NAME).unwrap(), + MSG, + &SHAKE128::new().hash_xof(MSG, 100), + ); + framework.test_xof( + || XOFFactory::new(SHAKE256_NAME).unwrap(), + MSG, + &SHAKE256::new().hash_xof(MSG, 100), + ); } From b95dd0c70d778ddf673840b50b3dc2643982ab68 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 9 Sep 2026 15:45:58 +1000 Subject: [PATCH 15/22] core: Hash gains Clone as a supertrait, so a hash mid-stream can be forked and finished several ways from one absorbed prefix; the SP 800-185 types and the factory enums derive it, the sha2 and sha3 params traits require it, and the framework hash and XOF suites check a clone finishes like its original and diverges on different input --- crypto/core-test-framework/src/hash.rs | 34 ++++++++++++++++++++++++++ crypto/core-test-framework/src/xof.rs | 31 +++++++++++++++++++++++ crypto/core/src/traits.rs | 12 ++++++++- crypto/factory/src/hash_factory.rs | 1 + crypto/factory/src/xof_factory.rs | 1 + crypto/sha2/src/lib.rs | 9 ++++--- crypto/sha3/src/cshake.rs | 1 + crypto/sha3/src/kmac.rs | 1 + crypto/sha3/src/lib.rs | 4 +-- crypto/sha3/src/parallelhash.rs | 3 +++ crypto/sha3/src/tuplehash.rs | 2 ++ 11 files changed, 93 insertions(+), 6 deletions(-) diff --git a/crypto/core-test-framework/src/hash.rs b/crypto/core-test-framework/src/hash.rs index 44037462..0a552c90 100644 --- a/crypto/core-test-framework/src/hash.rs +++ b/crypto/core-test-framework/src/hash.rs @@ -205,6 +205,40 @@ impl TestFrameworkHash { ); } + /*** Clone: a hash mid-stream can be forked ***/ + // A clone continues from the same absorbed prefix, so finishing the two on the same tail + // must give the same digest, and finishing them on different tails must not. + let (prefix, tail) = input.split_at(input.len() / 2); + let mut original = H::default(); + original.do_update(prefix); + let mut forked = original.clone(); + original.do_update(tail); + forked.do_update(tail); + assert_eq!( + original.do_final(), + expected_output, + "the original must be unaffected by cloning" + ); + assert_eq!( + forked.do_final(), + expected_output, + "a clone must continue from the same absorbed prefix" + ); + + let mut original = H::default(); + original.do_update(prefix); + let mut forked = original.clone(); + original.do_update(tail); + forked.do_update(&[0xA5]); + forked.do_update(tail); + let original_out = original.do_final(); + assert_eq!(original_out, expected_output); + assert_ne!( + forked.do_final(), + original_out, + "a clone must have its own state, not share the original's" + ); + // check that if you feed it an output slice that's bigger than it needs, that it doesn't touch the extra bytes. let mut message_digest = H::default(); let mut buf = vec![0u8; 2 * H::OUTPUT_LEN]; diff --git a/crypto/core-test-framework/src/xof.rs b/crypto/core-test-framework/src/xof.rs index 5b0f5400..f74e7727 100644 --- a/crypto/core-test-framework/src/xof.rs +++ b/crypto/core-test-framework/src/xof.rs @@ -108,6 +108,37 @@ impl TestFrameworkXOF { assert_eq!(n, expected_output.len()); assert_eq!(output, expected_output, "hash_xof_out must agree with hash_xof"); + /*** Clone: a XOF mid-absorb can be forked ***/ + // The clone continues from the same absorbed prefix and owns its own sponge. + let (prefix, tail) = input.split_at(input.len() / 2); + let mut original = make(); + original.do_update(prefix); + let mut forked = original.clone(); + original.do_update(tail); + forked.do_update(tail); + assert_eq!( + original.into_output().do_output(expected_output.len()), + expected_output, + "the original must be unaffected by cloning" + ); + assert_eq!( + forked.into_output().do_output(expected_output.len()), + expected_output, + "a clone must continue from the same absorbed prefix" + ); + + let mut original = make(); + original.do_update(prefix); + let mut forked = original.clone(); + original.do_update(tail); + forked.do_update(&[0xA5]); + forked.do_update(tail); + assert_ne!( + forked.into_output().do_output(expected_output.len()), + original.into_output().do_output(expected_output.len()), + "a clone must have its own state, not share the original's" + ); + /*** the Hash half: a XOF is a hash ***/ self.test_xof_as_hash(&make, input, expected_output); diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index fbcc6359..8f32e359 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -421,7 +421,17 @@ pub trait ElectronicCodeBook: /// Generic code that needs to *build* a hasher asks for it: `fn digest(..)`. /// That is what `HMAC` and the shared test framework already do, so the bound sits where the /// requirement actually is rather than on every implementor. -pub trait Hash: Algorithm { +/// +/// # Forking is part of this trait +/// +/// `Clone` *is* a supertrait: a hash mid-stream can be copied, and the copy continues independently +/// from the same absorbed prefix. That is how a running hash of a common prefix is finished several +/// ways -- a transcript hash checkpointed at each handshake message, HMAC's inner and outer states +/// held ready across many MACs under one key, or a Merkle node whose prefix is shared by its +/// siblings -- without re-absorbing the prefix each time. Every implementor is a fixed-size state +/// plus a small buffer, so the derive is the right implementation; the shared test framework checks +/// that a clone and its original finish to the same digest, and diverge once fed different input. +pub trait Hash: Algorithm + Clone { /// The size of the internal block in bits -- needed by functions such as HMAC to compute security parameters. fn block_bitlen(&self) -> usize; diff --git a/crypto/factory/src/hash_factory.rs b/crypto/factory/src/hash_factory.rs index 9c89fa40..3e6646ee 100644 --- a/crypto/factory/src/hash_factory.rs +++ b/crypto/factory/src/hash_factory.rs @@ -42,6 +42,7 @@ use bouncycastle_sm3::SM3_NAME; /// Wrapper object for all algorithms that impl [`Hash`]. /// Note: no SHAKE because SHAKE is not NIST approved as a hash function. See FIPS 202 section A.2. #[non_exhaustive] +#[derive(Clone)] pub enum HashFactory { /// SHA224(sha2::SHA224), diff --git a/crypto/factory/src/xof_factory.rs b/crypto/factory/src/xof_factory.rs index cb36e2ca..75a075f6 100644 --- a/crypto/factory/src/xof_factory.rs +++ b/crypto/factory/src/xof_factory.rs @@ -51,6 +51,7 @@ pub const DEFAULT_256BIT_XOF_NAME: &str = SHAKE256_NAME; /// Wrapper object for all algorithms that impl [`XOF`]. #[non_exhaustive] +#[derive(Clone)] pub enum XOFFactory { /// SHAKE128(sha3::SHAKE128), diff --git a/crypto/sha2/src/lib.rs b/crypto/sha2/src/lib.rs index 3c1200a8..8f75811a 100644 --- a/crypto/sha2/src/lib.rs +++ b/crypto/sha2/src/lib.rs @@ -248,7 +248,10 @@ pub type SHA512_256 = SHA512t<256>; /// /// Crate-private (aka "sealed") on purpose: it cannot be implemented outside this crate, so the /// only parameter sets that exist are the NIST-approved ones below. -trait SHA256InitValue: HashAlgParams { +/// +/// `Clone` because [`Hash`] requires it: a hash mid-stream can be forked and finished several +/// ways from one absorbed prefix. +trait SHA256InitValue: HashAlgParams + Clone { /// The initial hash value H(0), FIPS 180-4 s. 5.3.2 / 5.3.3. const H0: [u32; 8]; } @@ -256,8 +259,8 @@ trait SHA256InitValue: HashAlgParams { /// The SHA-512 family (SHA-384, SHA-512, SHA-512/t) shares one compression function and differs /// only in the initial hash value and the output truncation, so each member supplies its H(0) here. /// -/// Crate-private for the same reason as [`SHA256InitValue`]. -trait SHA512InitValue: HashAlgParams { +/// Crate-private for the same reason as [`SHA256InitValue`], and `Clone` for the same reason. +trait SHA512InitValue: HashAlgParams + Clone { /// The initial hash value H(0), FIPS 180-4 s. 5.3.4 / 5.3.5 / 5.3.6. const H0: [u64; 8]; diff --git a/crypto/sha3/src/cshake.rs b/crypto/sha3/src/cshake.rs index 9f5a3aa8..7149d842 100644 --- a/crypto/sha3/src/cshake.rs +++ b/crypto/sha3/src/cshake.rs @@ -24,6 +24,7 @@ const CSHAKE_SUFFIX: (u8, usize) = (0x00, 2); /// general construction -- feeding empty strings through the `bytepad` branch would absorb a /// non-empty prefix and use a different separator, giving a different function. [`Self::new`] /// branches on it, and there is a test that the two agree. +#[derive(Clone)] pub struct CSHAKEInternal { shake: SHAKEInternal, /// False when `N` and `S` are both empty, in which case this is plain SHAKE. diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs index a70228fc..81ddf821 100644 --- a/crypto/sha3/src/kmac.rs +++ b/crypto/sha3/src/kmac.rs @@ -184,6 +184,7 @@ impl MAC for KMACInternal { /// Because the length is *not* bound here, output at one length really is a prefix of output at a /// longer one -- the opposite of fixed-length KMAC -- so [`Hash::do_final`] is the first /// [`Hash::output_len`] bytes of the same stream [`XOF::into_output`] produces. +#[derive(Clone)] pub struct KMACXOFInternal { cshake: CSHAKEInternal, strength: SecurityStrength, diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index df2c67ba..3104d410 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -337,7 +337,7 @@ pub type SHAKE256 = SHAKEInternal; /*** Param traits ***/ /// Private trait on purpose so that only the NIST-approved params can be used. -trait SHA3Params: HashAlgParams { +trait SHA3Params: HashAlgParams + Clone { const SIZE: KeccakSize; /// A tag, unique across all SHA3 *and* SHAKE variants, identifying which variant produced a /// serialized state. Distinguishing same-rate variants (e.g. SHA3-256 vs SHAKE256) requires @@ -440,7 +440,7 @@ impl AlgorithmOID for SHA3_512 { &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0a]; } -trait SHAKEParams: Algorithm { +trait SHAKEParams: Algorithm + Clone { const SIZE: KeccakSize; /// See [`SHA3Params::STATE_TAG`]. Must be distinct from every SHA3 *and* SHAKE variant's tag. const STATE_TAG: u8; diff --git a/crypto/sha3/src/parallelhash.rs b/crypto/sha3/src/parallelhash.rs index fdfd602e..aa7d99ba 100644 --- a/crypto/sha3/src/parallelhash.rs +++ b/crypto/sha3/src/parallelhash.rs @@ -12,6 +12,7 @@ const PARALLELHASH_FUNCTION_NAME: &[u8] = b"ParallelHash"; /// The shared machinery of [`ParallelHashInternal`] and [`ParallelHashXOFInternal`]: the outer /// cSHAKE, the block buffer, and the count of blocks hashed so far. +#[derive(Clone)] struct ParallelState { cshake: CSHAKEInternal, block_size: usize, @@ -102,6 +103,7 @@ impl ParallelState { /// /// Unlike [`crate::TUPLEHASH128`], `do_update` here *is* ordinary byte-wise streaming: the block /// boundaries come from `B`, not from how the caller chunks its calls. +#[derive(Clone)] pub struct ParallelHashInternal { state: ParallelState, output_len: usize, @@ -193,6 +195,7 @@ impl Hash for ParallelHashInternal { /// Binds `right_encode(0)` in place of the output length, so -- as for KMACXOF and TupleHashXOF -- /// it is a different function from the fixed-length one, and its output at one length is a prefix /// of its output at a longer one. +#[derive(Clone)] pub struct ParallelHashXOFInternal { state: ParallelState, } diff --git a/crypto/sha3/src/tuplehash.rs b/crypto/sha3/src/tuplehash.rs index a98cf652..d28305e0 100644 --- a/crypto/sha3/src/tuplehash.rs +++ b/crypto/sha3/src/tuplehash.rs @@ -32,6 +32,7 @@ const TUPLEHASH_FUNCTION_NAME: &[u8] = b"TupleHash"; /// interchangeable `Hash` and re-chunks its input will silently compute something else. /// /// [`TupleHashXOFInternal`] is the arbitrary-output-length function of Sec 5.3.1. +#[derive(Clone)] pub struct TupleHashInternal { cshake: CSHAKEInternal, output_len: usize, @@ -140,6 +141,7 @@ impl Hash for TupleHashInternal { /// output at one length really is a prefix of output at a longer one. /// /// [`Hash::do_update`] appends one tuple element, exactly as for [`TupleHashInternal`]. +#[derive(Clone)] pub struct TupleHashXOFInternal { cshake: CSHAKEInternal, } From 67070028d45be9e0c5f47943e0726b3aecf9d9e6 Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 10 Sep 2026 22:23:20 +1000 Subject: [PATCH 16/22] core: XOF gains default hash_xof and hash_xof_out bodies so only SHAKE overrides them, XofOutput is renamed XOFOutput to match the spec capitalisation used everywhere else, Hash::output_len documents that a XOF's length is nominal rather than part of the function, and the BC Java asides come out of the Hash and XOF docs --- cli/src/sha3_cmd.rs | 2 +- crypto/core-test-framework/src/xof.rs | 2 +- crypto/core/src/traits.rs | 66 +++++++++++++------ crypto/factory/src/xof_factory.rs | 6 +- crypto/factory/tests/xof_factory_tests.rs | 4 +- crypto/mldsa-lowmemory/src/aux_functions.rs | 2 +- crypto/mldsa-lowmemory/src/hash_mldsa.rs | 2 +- crypto/mldsa-lowmemory/src/mldsa.rs | 2 +- crypto/mldsa-lowmemory/src/mldsa_keys.rs | 2 +- crypto/mldsa-lowmemory/tests/bc_test_data.rs | 4 +- crypto/mldsa/src/aux_functions.rs | 2 +- crypto/mldsa/src/hash_mldsa.rs | 2 +- crypto/mldsa/src/mldsa.rs | 2 +- crypto/mldsa/tests/bc_test_data.rs | 2 +- crypto/mlkem-lowmemory/src/aux_functions.rs | 2 +- crypto/mlkem-lowmemory/src/mlkem.rs | 2 +- crypto/mlkem-lowmemory/tests/mlkem_tests.rs | 2 +- crypto/mlkem/src/aux_functions.rs | 2 +- crypto/mlkem/src/mlkem.rs | 2 +- crypto/mlkem/tests/mlkem_tests.rs | 2 +- crypto/sha3/src/cshake.rs | 12 +--- crypto/sha3/src/kmac.rs | 12 +--- crypto/sha3/src/lib.rs | 6 +- crypto/sha3/src/parallelhash.rs | 12 +--- crypto/sha3/src/shake.rs | 15 ++--- crypto/sha3/src/tuplehash.rs | 19 ++---- crypto/sha3/tests/bc-test-data.rs | 2 +- crypto/sha3/tests/cshake_tests.rs | 2 +- crypto/sha3/tests/shake_tests.rs | 12 ++-- crypto/sha3/tests/tuplehash_tests.rs | 2 +- mem_usage_benches/src/bench_sha3_mem_usage.rs | 2 +- 31 files changed, 95 insertions(+), 113 deletions(-) diff --git a/cli/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index 2835e122..c6841128 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -1,4 +1,4 @@ -use bouncycastle::core::traits::{Hash, XOF, XofOutput}; +use bouncycastle::core::traits::{Hash, XOF, XOFOutput}; use std::io; use std::io::{Read, Write}; diff --git a/crypto/core-test-framework/src/xof.rs b/crypto/core-test-framework/src/xof.rs index f74e7727..a11803d9 100644 --- a/crypto/core-test-framework/src/xof.rs +++ b/crypto/core-test-framework/src/xof.rs @@ -1,7 +1,7 @@ //! Generic behaviour tests for anything that implements [`XOF`]. use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{XOF, XofOutput}; +use bouncycastle_core::traits::{XOF, XOFOutput}; /// Instance of the test framework. pub struct TestFrameworkXOF { diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 8f32e359..7d52f9b6 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -436,6 +436,19 @@ pub trait Hash: Algorithm + Clone { fn block_bitlen(&self) -> usize; /// The size of the output in bytes. + /// + /// # This is not always part of the function's identity + /// + /// For most hashes the length is bound into the computation, so asking for a different length + /// gives a different function rather than more or fewer bytes of the same one. TupleHash and + /// KMAC are built that way deliberately -- SP 800-185 absorbs `right_encode(L)` before + /// squeezing. + /// + /// A [`XOF`] is the exception. Its length is chosen at the point of output and is *not* an + /// input to the computation, so this returns a nominal length only -- 32 bytes for SHAKE128 -- + /// and two outputs of different lengths share their leading bytes. Generic code over `Hash` + /// must therefore not infer "different `output_len` implies unrelated output"; see the + /// discussion on [`XOF`]. fn output_len(&self) -> usize; /// A static one-shot API that hashes the provided data. @@ -1768,15 +1781,13 @@ where /// /// This is the type [`XOF::into_output`] hands back. Absorbing and squeezing are separate types /// rather than separate states of one type, so "no more input once output has begun" is a fact the -/// compiler enforces rather than a rule the documentation asks callers to follow. BC Java draws the -/// same line at run time, throwing `IllegalStateException` from `KeccakDigest.absorb`. +/// compiler enforces rather than a rule the documentation asks callers to follow, and so there is +/// no "absorbed after squeezing" error to raise or to test for. /// /// Output is one continuous stream: successive calls continue where the last left off, so reading /// 16 bytes twice gives the same 32 bytes as reading 32 once. -pub trait XofOutput { +pub trait XOFOutput { /// Produces the next `num_bytes` bytes of the output stream. - /// - /// BC Java's `Xof.doOutput(out, outOff, outLen)`. fn do_output(&mut self, num_bytes: usize) -> Vec; /// As [`do_output`](Self::do_output), filling the caller's buffer, which is zeroized first. @@ -1785,11 +1796,9 @@ pub trait XofOutput { /// The last output: produces `num_bytes` bytes and ends the stream. /// - /// This is BC Java's `Xof.doFinal(out, outOff, outLen)` called after `doOutput`, which is - /// `doOutput` followed by `reset()` (`SHAKEDigest.java`). Here the reset is taking `self` by - /// value: the handle is gone afterwards, and dropping it zeroizes the sponge. So this is - /// exactly [`do_output`](Self::do_output) plus the end of the value's life, provided as a - /// separate name so a call site can say which read is its last. + /// Ending the stream is taking `self` by value: the handle is gone afterwards, and dropping it + /// zeroizes the sponge. So this is exactly [`do_output`](Self::do_output) plus the end of the + /// value's life, provided as a separate name so a call site can say which read is its last. /// /// It reads the same bytes [`do_output`](Self::do_output) would at the same point in the /// stream; the difference is only that nothing can follow it. @@ -1812,16 +1821,15 @@ pub trait XofOutput { /// Extendable-Output Functions (XOFs): hashes whose output length is chosen by the caller. /// -/// `XOF: Hash`, so SHAKE128 and SHAKE256 *are* hashes and can be used wherever one is wanted. This -/// is the relationship BC Java draws with `Xof extends ExtendedDigest extends Digest`. As a hash, a -/// XOF has a nominal output length -- [`Hash::output_len`], which for SHAKE is -/// `fixedOutputLength / 4`, matching `SHAKEDigest.getDigestSize()` -- and [`Hash::do_final`] -/// produces exactly that many bytes. This trait adds the ability to ask for a different number. +/// `XOF: Hash`, so SHAKE128 and SHAKE256 *are* hashes and can be used wherever one is wanted. As a +/// hash, a XOF has a nominal output length -- [`Hash::output_len`], which for SHAKE is twice the +/// security strength, 32 bytes for SHAKE128 and 64 for SHAKE256 -- and [`Hash::do_final`] produces +/// exactly that many bytes. This trait adds the ability to ask for a different number. /// /// # Absorb, then squeeze /// /// A sponge takes input, then produces output, and cannot go back. Here that is expressed in the -/// types: [`into_output`](Self::into_output) consumes the XOF and returns an [`XofOutput`], so +/// types: [`into_output`](Self::into_output) consumes the XOF and returns an [`XOFOutput`], so /// after output has begun there is no value left on which to call [`Hash::do_update`]. Nothing /// returns an "absorbed after squeezing" error because nothing can reach that state. /// @@ -1834,12 +1842,11 @@ pub trait XofOutput { /// matters, salt the input. pub trait XOF: Hash { /// The squeezing state this XOF turns into. - type Output: XofOutput; + type Output: XOFOutput; /// Ends the input phase and begins producing output. /// - /// BC Java's `Xof.doOutput` in effect, but the phase change is in the type: what comes back - /// takes no more input. + /// The phase change is in the type: what comes back takes no more input. fn into_output(self) -> Self::Output; /// As [`into_output`](Self::into_output), with a final partial **byte** of input. @@ -1859,9 +1866,26 @@ pub trait XOF: Hash { ) -> Result; /// One-shot: absorbs `data` and produces `result_len` bytes. - fn hash_xof(self, data: &[u8], result_len: usize) -> Vec; + /// + /// The default absorbs and squeezes in the obvious way; override it only where the type can do + /// better, as SHAKE does. + fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec + where + Self: Sized, + { + self.do_update(data); + self.into_output().do_output(result_len) + } /// One-shot: absorbs `data` and fills `output`, which is zeroized first. Returns the number of /// bytes written. - fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize; + /// + /// Defaulted as [`hash_xof`](Self::hash_xof) is. + fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize + where + Self: Sized, + { + self.do_update(data); + self.into_output().do_output_out(output) + } } diff --git a/crypto/factory/src/xof_factory.rs b/crypto/factory/src/xof_factory.rs index 75a075f6..b3749a66 100644 --- a/crypto/factory/src/xof_factory.rs +++ b/crypto/factory/src/xof_factory.rs @@ -5,7 +5,7 @@ //! //! Example usage: //! ``` -//! use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +//! use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; //! use bouncycastle_factory::AlgorithmFactory; //! use bouncycastle_factory::xof_factory::XOFFactory; //! use bouncycastle_sha3 as sha3; @@ -37,7 +37,7 @@ use crate::{AlgorithmFactory, FactoryError}; use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XofOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFOutput}; use bouncycastle_sha3 as sha3; use bouncycastle_sha3::{SHAKE128_NAME, SHAKE256_NAME}; @@ -105,7 +105,7 @@ pub enum XOFFactoryOutput { SHAKE256(::Output), } -impl XofOutput for XOFFactoryOutput { +impl XOFOutput for XOFFactoryOutput { fn do_output(&mut self, num_bytes: usize) -> Vec { match self { Self::SHAKE128(o) => o.do_output(num_bytes), diff --git a/crypto/factory/tests/xof_factory_tests.rs b/crypto/factory/tests/xof_factory_tests.rs index ac1ea32d..8beb93a7 100644 --- a/crypto/factory/tests/xof_factory_tests.rs +++ b/crypto/factory/tests/xof_factory_tests.rs @@ -3,7 +3,7 @@ //! direct type side by side on the same input; nothing here is an expected value written by hand. use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_factory::xof_factory::XOFFactory; use bouncycastle_factory::{AlgorithmFactory, FactoryError}; @@ -11,7 +11,7 @@ use bouncycastle_sha3::{SHAKE128, SHAKE128_NAME, SHAKE256, SHAKE256_NAME}; const MSG: &[u8] = b"The quick brown fox jumps over the lazy dog"; -/// Every `Hash`, `XOF` and `XofOutput` method of the factory against the direct type `S`. +/// Every `Hash`, `XOF` and `XOFOutput` method of the factory against the direct type `S`. fn check_against(make: impl Fn() -> XOFFactory, ctx: &str) { let n = S::default().output_len(); diff --git a/crypto/mldsa-lowmemory/src/aux_functions.rs b/crypto/mldsa-lowmemory/src/aux_functions.rs index 488045b5..93c2b490 100644 --- a/crypto/mldsa-lowmemory/src/aux_functions.rs +++ b/crypto/mldsa-lowmemory/src/aux_functions.rs @@ -7,7 +7,7 @@ use crate::params::{ MLDSAParams, }; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; use bouncycastle_utils::secret::ZeroizablePrimitive; /// Algorithm 14 CoeffFromThreeBytes(𝑏0, 𝑏1, 𝑏2) diff --git a/crypto/mldsa-lowmemory/src/hash_mldsa.rs b/crypto/mldsa-lowmemory/src/hash_mldsa.rs index 0a0ac0b6..f4f0ba59 100644 --- a/crypto/mldsa-lowmemory/src/hash_mldsa.rs +++ b/crypto/mldsa-lowmemory/src/hash_mldsa.rs @@ -83,7 +83,7 @@ use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, PHSignatureVerifier, PHSigner, RNG, SecurityStrength, - SignatureVerifier, Signer, XOF, XofOutput, + SignatureVerifier, Signer, XOF, XOFOutput, }; use bouncycastle_rng::HashDRBG_SHA512; use core::marker::PhantomData; diff --git a/crypto/mldsa-lowmemory/src/mldsa.rs b/crypto/mldsa-lowmemory/src/mldsa.rs index fa2c4b51..4e69002c 100644 --- a/crypto/mldsa-lowmemory/src/mldsa.rs +++ b/crypto/mldsa-lowmemory/src/mldsa.rs @@ -400,7 +400,7 @@ use bouncycastle_core::errors::{RNGError, SignatureError, SuspendableError}; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, RNG, SecurityStrength, SignatureVerifier, Signer, Suspendable, - XOF, XofOutput, + XOF, XOFOutput, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN}; diff --git a/crypto/mldsa-lowmemory/src/mldsa_keys.rs b/crypto/mldsa-lowmemory/src/mldsa_keys.rs index 9aebec2d..b578c939 100644 --- a/crypto/mldsa-lowmemory/src/mldsa_keys.rs +++ b/crypto/mldsa-lowmemory/src/mldsa_keys.rs @@ -12,7 +12,7 @@ use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ - Hash, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, XOF, XofOutput, + Hash, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, XOF, XOFOutput, }; use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; use core::fmt; diff --git a/crypto/mldsa-lowmemory/tests/bc_test_data.rs b/crypto/mldsa-lowmemory/tests/bc_test_data.rs index c5438be5..966590dd 100644 --- a/crypto/mldsa-lowmemory/tests/bc_test_data.rs +++ b/crypto/mldsa-lowmemory/tests/bc_test_data.rs @@ -1,4 +1,4 @@ -use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; // Test against the bc-test-data repo // Requires that the bc-test-data repository is cloned and available for testing at "../bc-test-data" // relative to the root of this git project. @@ -20,7 +20,7 @@ mod bc_test_data { use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ Hash, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, XOF, - XofOutput, + XOFOutput, }; use bouncycastle_hex as hex; use bouncycastle_mldsa_lowmemory::{ diff --git a/crypto/mldsa/src/aux_functions.rs b/crypto/mldsa/src/aux_functions.rs index b7dc7865..1f7add2a 100644 --- a/crypto/mldsa/src/aux_functions.rs +++ b/crypto/mldsa/src/aux_functions.rs @@ -7,7 +7,7 @@ use crate::params::{ MLDSAParams, }; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; /// Algorithm 14 CoeffFromThreeBytes(𝑏0, 𝑏1, 𝑏2) diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index bd4f67b1..137025cd 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -84,7 +84,7 @@ use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, PHSignatureVerifier, PHSigner, RNG, SecurityStrength, - SignatureVerifier, Signer, XOF, XofOutput, + SignatureVerifier, Signer, XOF, XOFOutput, }; use bouncycastle_rng::HashDRBG_SHA512; use core::marker::PhantomData; diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs index 6533fb61..da49457a 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -491,7 +491,7 @@ use bouncycastle_core::errors::{RNGError, SignatureError, SuspendableError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterial256, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, RNG, SecurityStrength, SignatureVerifier, Signer, Suspendable, - XOF, XofOutput, + XOF, XOFOutput, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN}; diff --git a/crypto/mldsa/tests/bc_test_data.rs b/crypto/mldsa/tests/bc_test_data.rs index 1625a89b..f7e9e6a2 100644 --- a/crypto/mldsa/tests/bc_test_data.rs +++ b/crypto/mldsa/tests/bc_test_data.rs @@ -5,7 +5,7 @@ #![allow(dead_code)] use bouncycastle_core::errors::SignatureError; -use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; use bouncycastle_sha3::SHAKE256; #[cfg(test)] diff --git a/crypto/mlkem-lowmemory/src/aux_functions.rs b/crypto/mlkem-lowmemory/src/aux_functions.rs index 9fda6722..507dfbb1 100644 --- a/crypto/mlkem-lowmemory/src/aux_functions.rs +++ b/crypto/mlkem-lowmemory/src/aux_functions.rs @@ -2,7 +2,7 @@ use crate::mlkem::{N, q, q_inv}; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; use bouncycastle_sha3::{SHAKE128, SHAKE256}; /// Algorithm 5 ByteEncode_d(𝐹) diff --git a/crypto/mlkem-lowmemory/src/mlkem.rs b/crypto/mlkem-lowmemory/src/mlkem.rs index 25617d38..d1bb1224 100644 --- a/crypto/mlkem-lowmemory/src/mlkem.rs +++ b/crypto/mlkem-lowmemory/src/mlkem.rs @@ -19,7 +19,7 @@ use bouncycastle_core::key_material::{ }; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, SecurityStrength, XOF, - XofOutput, + XOFOutput, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHA3_256, SHA3_512, SHAKE256}; diff --git a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs index bf2b7e9f..e1b661b4 100644 --- a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs +++ b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs @@ -7,7 +7,7 @@ mod mlkem_tests { }; use bouncycastle_core::traits::{ Hash, KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength, XOF, - XofOutput, + XOFOutput, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; diff --git a/crypto/mlkem/src/aux_functions.rs b/crypto/mlkem/src/aux_functions.rs index 97f20e6f..2292e1c8 100644 --- a/crypto/mlkem/src/aux_functions.rs +++ b/crypto/mlkem/src/aux_functions.rs @@ -4,7 +4,7 @@ use crate::matrix::{MatrixTrait, VectorTrait}; use crate::mlkem::{N, q, q_inv}; use crate::params::MLKEMParams; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; use bouncycastle_sha3::{SHAKE128, SHAKE256}; pub(crate) fn expandA(rho: &[u8; 32]) -> P::MatrixA { diff --git a/crypto/mlkem/src/mlkem.rs b/crypto/mlkem/src/mlkem.rs index afd76c19..8a3d88f8 100644 --- a/crypto/mlkem/src/mlkem.rs +++ b/crypto/mlkem/src/mlkem.rs @@ -151,7 +151,7 @@ use bouncycastle_core::key_material::{ }; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, SecurityStrength, XOF, - XofOutput, + XOFOutput, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHA3_256, SHA3_512, SHAKE256}; diff --git a/crypto/mlkem/tests/mlkem_tests.rs b/crypto/mlkem/tests/mlkem_tests.rs index 733f1861..65331ae2 100644 --- a/crypto/mlkem/tests/mlkem_tests.rs +++ b/crypto/mlkem/tests/mlkem_tests.rs @@ -6,7 +6,7 @@ mod mlkem_tests { use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ Hash, KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength, XOF, - XofOutput, + XOFOutput, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; diff --git a/crypto/sha3/src/cshake.rs b/crypto/sha3/src/cshake.rs index 7149d842..6268efd1 100644 --- a/crypto/sha3/src/cshake.rs +++ b/crypto/sha3/src/cshake.rs @@ -4,7 +4,7 @@ use crate::SHAKEParams; use crate::shake::{SHAKEInternal, SHAKEOutput}; use crate::xof_utils::left_encode; use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XofOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFOutput}; /// The domain separator cSHAKE absorbs in place of SHAKE's `1111`: the `00` of SP 800-185 Sec 3.3, /// two zero bits, which is what keeps a customized instance separate from plain SHAKE. @@ -216,14 +216,4 @@ impl XOF for CSHAKEInternal { self.shake.into_output_partial_bits(partial_byte, num_bits) } } - - fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { - self.do_update(data); - self.into_output().do_output(result_len) - } - - fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { - self.do_update(data); - self.into_output().do_output_out(output) - } } diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs index 81ddf821..0600edcb 100644 --- a/crypto/sha3/src/kmac.rs +++ b/crypto/sha3/src/kmac.rs @@ -6,7 +6,7 @@ use crate::shake::SHAKEOutput; use crate::xof_utils::right_encode; use bouncycastle_core::errors::{HashError, KeyMaterialError, MACError}; use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{Algorithm, Hash, MAC, SecurityStrength, XOF, XofOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, MAC, SecurityStrength, XOF, XOFOutput}; use bouncycastle_utils::ct; /// The function-name string every KMAC binds, per SP 800-185 Sec 4.3. Fixed by the specification: @@ -310,14 +310,4 @@ impl XOF for KMACXOFInternal { } Ok(self.into_output()) } - - fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { - self.do_update(data); - self.into_output().do_output(result_len) - } - - fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { - self.do_update(data); - self.into_output().do_output_out(output) - } } diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 3104d410..5cfc112f 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -74,8 +74,8 @@ //! //! [`XOF`] extends [`Hash`], so SHAKE takes input through [`Hash::do_update`] like any other hash. //! Output is where they differ: [`XOF::into_output`] ends the input phase and returns an -//! [`XofOutput`](bouncycastle_core::traits::XofOutput), whose -//! [`do_output`](bouncycastle_core::traits::XofOutput::do_output) can be called as many times as you +//! [`XOFOutput`](bouncycastle_core::traits::XOFOutput), whose +//! [`do_output`](bouncycastle_core::traits::XOFOutput::do_output) can be called as many times as you //! like, each call continuing one stream. //! //! Absorbing after output has begun is not an error you can make: `into_output` consumes the @@ -83,7 +83,7 @@ //! //! The following code produces the same output as the previous example: //!``` -//! use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +//! use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; //! use bouncycastle_sha3 as sha3; //! //! let data: &[u8] = b"Hello, world!"; diff --git a/crypto/sha3/src/parallelhash.rs b/crypto/sha3/src/parallelhash.rs index aa7d99ba..8ae43c9d 100644 --- a/crypto/sha3/src/parallelhash.rs +++ b/crypto/sha3/src/parallelhash.rs @@ -5,7 +5,7 @@ use crate::cshake::{CSHAKEInternal, absorb_left_encode_into}; use crate::shake::{SHAKEInternal, SHAKEOutput}; use crate::xof_utils::right_encode; use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XofOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFOutput}; /// The function-name string every ParallelHash binds, per SP 800-185 Sec 6.3. const PARALLELHASH_FUNCTION_NAME: &[u8] = b"ParallelHash"; @@ -301,14 +301,4 @@ impl XOF for ParallelHashXOFInternal { } Ok(self.into_output()) } - - fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { - self.do_update(data); - self.into_output().do_output(result_len) - } - - fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { - self.do_update(data); - self.into_output().do_output_out(output) - } } diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index ec6c3bec..9339ed73 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -8,7 +8,7 @@ use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_core::traits::{ - Algorithm, Hash, KDF, SecurityStrength, Suspendable, XOF, XofOutput, + Algorithm, Hash, KDF, SecurityStrength, Suspendable, XOF, XOFOutput, }; use bouncycastle_utils::{max, min}; @@ -305,7 +305,7 @@ pub struct SHAKEOutput { shake: SHAKEInternal, } -impl XofOutput for SHAKEOutput { +impl XOFOutput for SHAKEOutput { fn do_output(&mut self, num_bytes: usize) -> Vec { let mut out = vec![0u8; num_bytes]; self.do_output_out(&mut out); @@ -369,8 +369,8 @@ impl Hash for SHAKEInternal { /// The nominal digest size: 32 bytes for SHAKE128, 64 for SHAKE256. /// /// A XOF has no inherent output length, so this is a convention rather than a property of the - /// function. It is BC Java's: `SHAKEDigest.getDigestSize()` returns `fixedOutputLength / 4`, - /// which is the length at which the output carries the full security level. + /// function: it is twice the security strength, the length at which the output carries the + /// full security level. fn output_len(&self) -> usize { (PARAMS::SIZE as usize) / 4 } @@ -399,8 +399,7 @@ impl Hash for SHAKEInternal { self.keccak.absorb(data); } - /// Produces [`output_len`](Self::output_len) bytes and ends the object, as BC Java's - /// `Digest.doFinal(out, outOff)` does via `doFinal(out, outOff, getDigestSize())`. + /// Produces [`output_len`](Self::output_len) bytes and ends the object. fn do_final(self) -> Vec { let n = self.output_len(); let mut out = vec![0u8; n]; @@ -440,7 +439,7 @@ impl Hash for SHAKEInternal { /// The absorb-then-squeeze rule, as a compile error rather than a runtime one. /// /// ```compile_fail -/// use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +/// use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; /// use bouncycastle_sha3::SHAKE128; /// /// let mut shake = SHAKE128::new(); @@ -453,7 +452,7 @@ impl Hash for SHAKEInternal { /// The same value used correctly: /// /// ``` -/// use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +/// use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; /// use bouncycastle_sha3::SHAKE128; /// /// let mut shake = SHAKE128::new(); diff --git a/crypto/sha3/src/tuplehash.rs b/crypto/sha3/src/tuplehash.rs index d28305e0..66dc9c42 100644 --- a/crypto/sha3/src/tuplehash.rs +++ b/crypto/sha3/src/tuplehash.rs @@ -5,7 +5,7 @@ use crate::cshake::{CSHAKEInternal, absorb_encoded_string_into}; use crate::shake::SHAKEOutput; use crate::xof_utils::right_encode; use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XofOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFOutput}; /// The function-name string every TupleHash binds, per SP 800-185 Sec 5.3. const TUPLEHASH_FUNCTION_NAME: &[u8] = b"TupleHash"; @@ -26,10 +26,9 @@ const TUPLEHASH_FUNCTION_NAME: &[u8] = b"TupleHash"; /// /// This is the one place TupleHash departs from the usual [`Hash`] contract. For every other hash, /// feeding the input in pieces gives the same answer as feeding it at once; here each -/// [`Hash::do_update`] call is one tuple element, so the chunking *is* the input. BC Java draws the -/// same line -- its `TupleHash.update` encodes each call with `XofUtils.encode` before passing it -/// on -- but it is worth stating plainly, because code that treats a `TupleHash` as an -/// interchangeable `Hash` and re-chunks its input will silently compute something else. +/// [`Hash::do_update`] call is one tuple element, so the chunking *is* the input. It is worth +/// stating plainly, because code that treats a `TupleHash` as an interchangeable `Hash` and +/// re-chunks its input will silently compute something else. /// /// [`TupleHashXOFInternal`] is the arbitrary-output-length function of Sec 5.3.1. #[derive(Clone)] @@ -256,14 +255,4 @@ impl XOF for TupleHashXOFInternal { } Ok(self.into_output()) } - - fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { - self.do_update(data); - self.into_output().do_output(result_len) - } - - fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { - self.do_update(data); - self.into_output().do_output_out(output) - } } diff --git a/crypto/sha3/tests/bc-test-data.rs b/crypto/sha3/tests/bc-test-data.rs index 147d7c91..b3e312b2 100644 --- a/crypto/sha3/tests/bc-test-data.rs +++ b/crypto/sha3/tests/bc-test-data.rs @@ -25,7 +25,7 @@ //! `Outputlen = minoutbytes + (rightmost 16 bits of Output as big-endian integer) mod //! (maxoutbytes - minoutbytes + 1)` bytes; report `Output`/`Outputlen` per COUNT. -use bouncycastle_core::traits::{Hash, XOF, XofOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; use bouncycastle_hex as hex; use bouncycastle_sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256}; use std::fs; diff --git a/crypto/sha3/tests/cshake_tests.rs b/crypto/sha3/tests/cshake_tests.rs index 17dfc9ce..552b8e7f 100644 --- a/crypto/sha3/tests/cshake_tests.rs +++ b/crypto/sha3/tests/cshake_tests.rs @@ -4,7 +4,7 @@ //! `../bc-test-data` (the same convention as the ML-KEM, ML-DSA and SHA-3 suites). If it is not //! present these tests print a warning and pass vacuously. -use bouncycastle_core::traits::{Algorithm, Hash, XOF, XofOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFOutput}; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_hex as hex; use bouncycastle_sha3::{CSHAKE128, CSHAKE256, SHAKE128, SHAKE256}; diff --git a/crypto/sha3/tests/shake_tests.rs b/crypto/sha3/tests/shake_tests.rs index 6d921c97..226adbdb 100644 --- a/crypto/sha3/tests/shake_tests.rs +++ b/crypto/sha3/tests/shake_tests.rs @@ -7,7 +7,7 @@ mod shake_tests { use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterial256, KeyMaterial512, KeyMaterialTrait, KeyType, }; - use bouncycastle_core::traits::{Hash, KDF, SecurityStrength, XOF, XofOutput}; + use bouncycastle_core::traits::{Hash, KDF, SecurityStrength, XOF, XOFOutput}; use bouncycastle_core_test_framework::DUMMY_SEED; use bouncycastle_core_test_framework::kdf::TestFrameworkKDF; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; @@ -64,14 +64,14 @@ mod shake_tests { /// of them until this test existed. /// /// `block_bitlen` is the sponge rate, `1600 - 2c`: FIPS 202 Table 3 gives 1344 bits for - /// SHAKE128 and 1088 for SHAKE256. `output_len` is the nominal digest size, which BC Java's - /// `SHAKEDigest.getDigestSize()` defines as `fixedOutputLength / 4`: 32 and 64 bytes. + /// SHAKE128 and 1088 for SHAKE256. `output_len` is the nominal digest size, twice the security + /// strength: 32 and 64 bytes. #[test] - fn metadata_matches_fips202_and_bc_java() { + fn metadata_matches_fips202() { assert_eq!(SHAKE128::new().block_bitlen(), 1344, "SHAKE128 rate, FIPS 202 Table 3"); assert_eq!(SHAKE256::new().block_bitlen(), 1088, "SHAKE256 rate, FIPS 202 Table 3"); - assert_eq!(SHAKE128::new().output_len(), 32, "SHAKEDigest.getDigestSize() for SHAKE128"); - assert_eq!(SHAKE256::new().output_len(), 64, "SHAKEDigest.getDigestSize() for SHAKE256"); + assert_eq!(SHAKE128::new().output_len(), 32, "nominal digest size for SHAKE128"); + assert_eq!(SHAKE256::new().output_len(), 64, "nominal digest size for SHAKE256"); // and do_final actually produces that many bytes assert_eq!(SHAKE128::new().hash(b"abc").len(), 32); diff --git a/crypto/sha3/tests/tuplehash_tests.rs b/crypto/sha3/tests/tuplehash_tests.rs index a4a164c3..9bd3076f 100644 --- a/crypto/sha3/tests/tuplehash_tests.rs +++ b/crypto/sha3/tests/tuplehash_tests.rs @@ -3,7 +3,7 @@ //! Vectors come from the `bc-test-data` repo cloned alongside this one; see `cshake_tests.rs`. use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{Algorithm, Hash, XOF, XofOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFOutput}; use bouncycastle_hex as hex; use bouncycastle_sha3::{TUPLEHASH128, TUPLEHASH256, TUPLEHASHXOF128, TUPLEHASHXOF256}; use std::fs; diff --git a/mem_usage_benches/src/bench_sha3_mem_usage.rs b/mem_usage_benches/src/bench_sha3_mem_usage.rs index 7a0b3c63..4e9db510 100644 --- a/mem_usage_benches/src/bench_sha3_mem_usage.rs +++ b/mem_usage_benches/src/bench_sha3_mem_usage.rs @@ -25,7 +25,7 @@ #![allow(dead_code)] #![allow(unused_imports)] -use bouncycastle::core::traits::{Hash, Suspendable, XOF, XofOutput}; +use bouncycastle::core::traits::{Hash, Suspendable, XOF, XOFOutput}; use bouncycastle::sha3::{ SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN, }; From b466d95ab2439703731a080a51ab7e487d4fc95c Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 10 Sep 2026 22:49:27 +1000 Subject: [PATCH 17/22] core-test-framework: add test_hash_output_buffers, a closure-built Hash suite covering short, exact and over-long output buffers, for the implementors that take constructor arguments and so cannot reach test_hash --- crypto/core-test-framework/src/hash.rs | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/crypto/core-test-framework/src/hash.rs b/crypto/core-test-framework/src/hash.rs index 0a552c90..2b6b0c1d 100644 --- a/crypto/core-test-framework/src/hash.rs +++ b/crypto/core-test-framework/src/hash.rs @@ -16,6 +16,64 @@ impl TestFrameworkHash { Self { enable_partial_byte_tests: true } } + /// Checks [`Hash::do_final_out`] and [`Hash::hash_out`] against every buffer length, for a + /// hash whose output length is bound into the computation. + /// + /// [`test_hash`](Self::test_hash) covers this too, but only for a `Default + HashAlgParams` + /// implementor. The SP 800-185 functions take constructor arguments and so cannot reach it; + /// `TupleHash` and `ParallelHash` both panicked on a short buffer until this existed. + /// + /// Not for XOFs. A XOF's [`Hash::output_len`] is nominal rather than bound, and its + /// `do_final_out` fills whatever buffer it is handed rather than stopping at `output_len`, so + /// the over-long case below does not describe one. Use `TestFrameworkXOF` for those. + pub fn test_hash_output_buffers(&self, make: impl Fn() -> H, input: &[u8]) { + let expected = { + let mut h = make(); + h.do_update(input); + h.do_final() + }; + let n = make().output_len(); + assert_eq!(expected.len(), n, "do_final() must produce output_len() bytes"); + + // Short: the buffer is filled and the digest truncated to it. + for length in 1..n { + let mut buf = vec![0xAA_u8; length]; + let mut h = make(); + h.do_update(input); + let written = h.do_final_out(&mut buf); + assert_eq!(written, length, "a {length}-byte buffer must take {length} bytes"); + assert_eq!(buf, expected[..length], "short buffer must truncate the digest"); + + // hash_out is the one-shot spelling of the same thing. + let mut buf = vec![0xAA_u8; length]; + let written = make().hash_out(input, &mut buf); + assert_eq!(written, length, "hash_out must agree with do_final_out"); + assert_eq!(buf, expected[..length], "hash_out must truncate the digest"); + } + + // Exact. + let mut buf = vec![0xAA_u8; n]; + let mut h = make(); + h.do_update(input); + assert_eq!(h.do_final_out(&mut buf), n); + assert_eq!(buf, expected, "an exactly-sized buffer must take the whole digest"); + + // Long: the digest lands in the first output_len bytes and the rest is zeroized. + for extra in [1, n, 2 * n + 1] { + let mut buf = vec![0xAA_u8; n + extra]; + let mut h = make(); + h.do_update(input); + let written = h.do_final_out(&mut buf); + assert_eq!(written, n, "a long buffer must still write only output_len bytes"); + assert_eq!(&buf[..n], &expected[..], "the digest must land at the start"); + assert!( + buf[n..].iter().all(|&b| b == 0), + "bytes past output_len must be zeroized, buffer was {} bytes", + n + extra + ); + } + } + /// Test all the members of trait Hash against the given input-output pair. /// This gives good baseline test coverage, but is not exhaustive; for example it does not test /// do_final_partial_bits() or do_final_partial_bits_out() From 0e8b2f74540f9b287eb09b8c59f72002d5ffa7b5 Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 10 Sep 2026 22:49:42 +1000 Subject: [PATCH 18/22] sha3: TupleHash and ParallelHash panicked on an output buffer shorter than output_len instead of truncating, and neither they nor KMAC zeroized past the digest as the Hash and MAC contracts require; the two suites that had pinned the old behaviour are corrected and all three types now run the framework's buffer-length checks --- crypto/sha3/src/kmac.rs | 3 +++ crypto/sha3/src/parallelhash.rs | 8 +++++++- crypto/sha3/src/tuplehash.rs | 8 +++++++- crypto/sha3/tests/kmac_tests.rs | 5 +++-- crypto/sha3/tests/parallelhash_tests.rs | 22 +++++++++++++++++++++- crypto/sha3/tests/tuplehash_tests.rs | 23 ++++++++++++++++++++++- 6 files changed, 63 insertions(+), 6 deletions(-) diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs index 0600edcb..0f437ba9 100644 --- a/crypto/sha3/src/kmac.rs +++ b/crypto/sha3/src/kmac.rs @@ -144,6 +144,9 @@ impl MAC for KMACInternal { } let n = self.output_len; self.absorb_right_encode((n as u64) * 8); + // MAC::do_final_out zeroizes the entire buffer, as HMAC does, so a longer one comes back + // with zeros after the MAC rather than whatever the caller left there. + out[n..].fill(0); Ok(self.cshake.into_output().do_output_out(&mut out[..n])) } diff --git a/crypto/sha3/src/parallelhash.rs b/crypto/sha3/src/parallelhash.rs index 8ae43c9d..af8493e3 100644 --- a/crypto/sha3/src/parallelhash.rs +++ b/crypto/sha3/src/parallelhash.rs @@ -154,7 +154,13 @@ impl Hash for ParallelHashInternal { fn do_final_out(self, output: &mut [u8]) -> usize { let n = self.output_len; - self.state.finish((n as u64) * 8).into_output().do_output_out(&mut output[..n]) + // Per Hash::do_final_out: a short buffer is filled and the digest truncated, a long one + // takes the digest in its first output_len bytes and zeros after it. `n` is bound into the + // computation either way -- the buffer's length never reaches the length encoding, so a + // truncated read is this ParallelHash cut short, not the ParallelHash of a shorter length. + let written = n.min(output.len()); + output[written..].fill(0); + self.state.finish((n as u64) * 8).into_output().do_output_out(&mut output[..written]) } /// # Errors diff --git a/crypto/sha3/src/tuplehash.rs b/crypto/sha3/src/tuplehash.rs index 66dc9c42..5b6a5704 100644 --- a/crypto/sha3/src/tuplehash.rs +++ b/crypto/sha3/src/tuplehash.rs @@ -97,7 +97,13 @@ impl Hash for TupleHashInternal { let n = self.output_len; let (buf, len) = right_encode((n as u64) * 8); self.cshake.do_update(&buf[..len]); - self.cshake.into_output().do_output_out(&mut output[..n]) + // Per Hash::do_final_out: a short buffer is filled and the digest truncated, a long one + // takes the digest in its first output_len bytes and zeros after it. `n` is bound into the + // computation either way -- the buffer's length never reaches right_encode above, so a + // truncated read is this TupleHash cut short, not the TupleHash of a shorter length. + let written = n.min(output.len()); + output[written..].fill(0); + self.cshake.into_output().do_output_out(&mut output[..written]) } /// # Errors diff --git a/crypto/sha3/tests/kmac_tests.rs b/crypto/sha3/tests/kmac_tests.rs index a8a58600..37e4020f 100644 --- a/crypto/sha3/tests/kmac_tests.rs +++ b/crypto/sha3/tests/kmac_tests.rs @@ -319,13 +319,14 @@ fn check_out_variants(make: impl Fn() -> M, msg: &[u8], expected: &[u8], assert_eq!(m.do_final_out(&mut out).unwrap(), n, "{ctx}: do_final_out returns the length"); assert_eq!(out, expected, "{ctx}: do_final_out"); - // do_final_out writes exactly output_len bytes and leaves the rest alone + // do_final_out writes output_len bytes and zeroizes the rest, as mac_out above does -- the two + // used to disagree, mac_out zero-filling and do_final_out leaving the caller's bytes in place. let mut m = make(); m.do_update(msg); let mut out = vec![0xFFu8; n + 5]; assert_eq!(m.do_final_out(&mut out).unwrap(), n); assert_eq!(&out[..n], expected, "{ctx}: do_final_out, oversized buffer"); - assert_eq!(&out[n..], &[0xFFu8; 5], "{ctx}: do_final_out leaves bytes past the tag"); + assert_eq!(&out[n..], &[0u8; 5], "{ctx}: do_final_out zeroizes past the tag"); // a buffer one byte short is refused, by both let mut out = vec![0u8; n - 1]; diff --git a/crypto/sha3/tests/parallelhash_tests.rs b/crypto/sha3/tests/parallelhash_tests.rs index 9d0fec90..de0de3b4 100644 --- a/crypto/sha3/tests/parallelhash_tests.rs +++ b/crypto/sha3/tests/parallelhash_tests.rs @@ -4,6 +4,7 @@ use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Algorithm, Hash, XOF}; +use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_hex as hex; use bouncycastle_sha3::{PARALLELHASH128, PARALLELHASH256, PARALLELHASHXOF128, PARALLELHASHXOF256}; use std::fs; @@ -259,7 +260,9 @@ fn check_fixed_view(make: impl Fn() -> H, msg: &[u8], expected: &[u8], let mut out = vec![0xFFu8; n + 7]; assert_eq!(h.do_final_out(&mut out), n); assert_eq!(&out[..n], expected, "{ctx}: do_final_out, oversized buffer"); - assert_eq!(&out[n..], &[0xFFu8; 7], "{ctx}: bytes past the output length are untouched"); + // Hash::do_final_out zeroizes the whole buffer, so the tail is 0 rather than what the caller + // left there -- the same as SHA3, which is the contract these fixed-length types share. + assert_eq!(&out[n..], &[0u8; 7], "{ctx}: bytes past the output length are zeroized"); } /// Every `Hash` and `XOF` entry point of the XOF form, against one sample value. The samples ask @@ -337,3 +340,20 @@ fn xof_trait_view_agrees_with_the_sample_values() { } } } + +/// Every output-buffer length, at both strengths and a non-default output length. +/// +/// As for TupleHash: `output_len` is bound into the computation, so a short buffer truncates this +/// ParallelHash rather than computing a shorter one, and must not panic. +#[test] +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(8, b"", 32), input); + framework.test_hash_output_buffers(|| PARALLELHASH256::new(8, b"", 64), input); + + // A block size that does not divide the input, a customization string, odd output lengths. + framework.test_hash_output_buffers(|| PARALLELHASH128::new(12, b"Parallel Data", 17), input); + framework.test_hash_output_buffers(|| PARALLELHASH256::new(5, b"Parallel Data", 5), input); +} diff --git a/crypto/sha3/tests/tuplehash_tests.rs b/crypto/sha3/tests/tuplehash_tests.rs index 9bd3076f..f285258c 100644 --- a/crypto/sha3/tests/tuplehash_tests.rs +++ b/crypto/sha3/tests/tuplehash_tests.rs @@ -4,6 +4,7 @@ use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFOutput}; +use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_hex as hex; use bouncycastle_sha3::{TUPLEHASH128, TUPLEHASH256, TUPLEHASHXOF128, TUPLEHASHXOF256}; use std::fs; @@ -245,7 +246,9 @@ fn check_fixed_view(make: impl Fn() -> H, tuple: &[&[u8]], expected: &[ let mut out = vec![0xFFu8; n + 7]; assert_eq!(h.do_final_out(&mut out), n); assert_eq!(&out[..n], expected, "{ctx}: do_final_out, oversized buffer"); - assert_eq!(&out[n..], &[0xFFu8; 7], "{ctx}: bytes past the output length are untouched"); + // Hash::do_final_out zeroizes the whole buffer, so the tail is 0 rather than what the caller + // left there -- the same as SHA3, which is the contract these fixed-length types share. + assert_eq!(&out[n..], &[0u8; 7], "{ctx}: bytes past the output length are zeroized"); // hash and hash_out take one element: the last, after the rest have been fed in let Some((last, rest)) = tuple.split_last() else { return }; @@ -349,3 +352,21 @@ fn xof_trait_view_agrees_with_the_sample_values() { } } } + +/// Every output-buffer length, at both strengths and a non-default output length. +/// +/// `output_len` is bound into the computation, so a short buffer must truncate this TupleHash +/// rather than compute the TupleHash of a shorter length -- and must not panic, which it did +/// before this test existed. +#[test] +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); + + // 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); +} From ad3fa52fa587f4ace64751131757f3f5a8c85bea Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 10 Sep 2026 23:24:45 +1000 Subject: [PATCH 19/22] core: drop XOFOutput::do_final and do_final_out, which no implementor overrode and nothing outside their own tests called; a squeeze has nothing to finalize, so ending the stream is dropping the value, and the XOF suite now checks do_output_out zeroizes the buffer where it had checked the alias agreed with do_final --- crypto/core-test-framework/src/xof.rs | 28 ++++---------------------- crypto/core/src/traits.rs | 29 +++++---------------------- crypto/sha3/tests/cshake_tests.rs | 2 +- 3 files changed, 10 insertions(+), 49 deletions(-) diff --git a/crypto/core-test-framework/src/xof.rs b/crypto/core-test-framework/src/xof.rs index a11803d9..ec25090b 100644 --- a/crypto/core-test-framework/src/xof.rs +++ b/crypto/core-test-framework/src/xof.rs @@ -67,34 +67,14 @@ impl TestFrameworkXOF { "successive reads must continue one stream" ); - /*** fn do_final(self, num_bytes: usize) -> Vec ***/ - // do_final reads what do_output would read at the same point; it only ends the stream. - let mut xof = make(); - xof.do_update(input); - assert_eq!( - xof.into_output().do_final(expected_output.len()), - expected_output, - "do_final must read what do_output reads" - ); - - // ... including part-way through a stream, not just at the start. - let mut xof = make(); - xof.do_update(input); - let mut out = xof.into_output(); - let head = out.do_output(split); - let tail = out.do_final(expected_output.len() - split); - assert_eq!( - [head, tail].concat(), - expected_output, - "do_final must continue the stream, not restart it" - ); - + // do_output_out zeroizes the caller's buffer before writing, so a dirty one still comes + // back holding exactly the output. let mut buf = vec![0xFFu8; expected_output.len()]; let mut xof = make(); xof.do_update(input); - let n = xof.into_output().do_final_out(&mut buf); + let n = xof.into_output().do_output_out(&mut buf); assert_eq!(n, expected_output.len()); - assert_eq!(buf, expected_output, "do_final_out must agree with do_final"); + assert_eq!(buf, expected_output, "do_output_out must zeroize before writing"); /*** fn hash_xof(self, data: &[u8], result_len: usize) -> Vec ***/ assert_eq!( diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 7d52f9b6..e40a76f2 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -1786,6 +1786,11 @@ where /// /// Output is one continuous stream: successive calls continue where the last left off, so reading /// 16 bytes twice gives the same 32 bytes as reading 32 once. +/// +/// There is no `do_final` here, unlike [`Hash`] and [`MAC`]. On those it is load-bearing -- the +/// only way to get output, and it must consume the value because finalizing pads the state. A +/// squeeze has nothing to finalize, so such a method would only say "this read is my last", which +/// ownership already says: drop the value, or let it fall out of scope. pub trait XOFOutput { /// Produces the next `num_bytes` bytes of the output stream. fn do_output(&mut self, num_bytes: usize) -> Vec; @@ -1793,30 +1798,6 @@ pub trait XOFOutput { /// As [`do_output`](Self::do_output), filling the caller's buffer, which is zeroized first. /// Returns the number of bytes written. fn do_output_out(&mut self, output: &mut [u8]) -> usize; - - /// The last output: produces `num_bytes` bytes and ends the stream. - /// - /// Ending the stream is taking `self` by value: the handle is gone afterwards, and dropping it - /// zeroizes the sponge. So this is exactly [`do_output`](Self::do_output) plus the end of the - /// value's life, provided as a separate name so a call site can say which read is its last. - /// - /// It reads the same bytes [`do_output`](Self::do_output) would at the same point in the - /// stream; the difference is only that nothing can follow it. - fn do_final(mut self, num_bytes: usize) -> Vec - where - Self: Sized, - { - self.do_output(num_bytes) - } - - /// As [`do_final`](Self::do_final), filling the caller's buffer, which is zeroized first. - /// Returns the number of bytes written. - fn do_final_out(mut self, output: &mut [u8]) -> usize - where - Self: Sized, - { - self.do_output_out(output) - } } /// Extendable-Output Functions (XOFs): hashes whose output length is chosen by the caller. diff --git a/crypto/sha3/tests/cshake_tests.rs b/crypto/sha3/tests/cshake_tests.rs index 552b8e7f..ecaa70c6 100644 --- a/crypto/sha3/tests/cshake_tests.rs +++ b/crypto/sha3/tests/cshake_tests.rs @@ -164,7 +164,7 @@ fn streaming_matches_one_shot() { } let mut out = c.into_output(); let head = out.do_output(20); - let tail = out.do_final(44); + let tail = out.do_output(44); assert_eq!([head, tail].concat(), one, "chunked in, split out, must equal the one-shot"); } From 8810ae78ab2ec0c742424e8dd712fc738f733b89 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 14 Sep 2026 14:20:16 +1000 Subject: [PATCH 20/22] core, core-test-framework, sha3, factory, mldsa, mlkem, cli: rename the XOF squeezing vocabulary, so XOFOutput becomes XOFSqueezer with SHAKEOutput and XOFFactoryOutput following it, XOF::Output becomes XOF::Squeezer, into_output and into_output_partial_bits become into_squeezer and into_squeezer_partial_bits, and the one-shots hash_xof and hash_xof_out become xof and xof_out; mechanical throughout, with no behaviour change --- cli/src/sha3_cmd.rs | 4 +- crypto/core-test-framework/src/xof.rs | 40 +++++------ crypto/core/src/traits.rs | 28 ++++---- crypto/factory/src/xof_factory.rs | 44 ++++++------ crypto/factory/tests/hash_factory_tests.rs | 4 +- crypto/factory/tests/xof_factory_tests.rs | 26 +++---- crypto/mldsa-lowmemory/src/aux_functions.rs | 10 +-- crypto/mldsa-lowmemory/src/hash_mldsa.rs | 6 +- crypto/mldsa-lowmemory/src/mldsa.rs | 10 +-- crypto/mldsa-lowmemory/src/mldsa_keys.rs | 6 +- crypto/mldsa-lowmemory/tests/bc_test_data.rs | 6 +- crypto/mldsa/src/aux_functions.rs | 10 +-- crypto/mldsa/src/hash_mldsa.rs | 6 +- crypto/mldsa/src/mldsa.rs | 18 ++--- crypto/mldsa/src/mldsa_keys.rs | 2 +- crypto/mldsa/tests/bc_test_data.rs | 4 +- crypto/mlkem-lowmemory/src/aux_functions.rs | 8 +-- crypto/mlkem-lowmemory/src/mlkem.rs | 4 +- crypto/mlkem-lowmemory/tests/mlkem_tests.rs | 4 +- crypto/mlkem/src/aux_functions.rs | 8 +-- crypto/mlkem/src/mlkem.rs | 4 +- crypto/mlkem/tests/mlkem_tests.rs | 4 +- crypto/sha3/benches/sha3_benches.rs | 8 +-- crypto/sha3/src/cshake.rs | 28 ++++---- crypto/sha3/src/kmac.rs | 30 ++++---- crypto/sha3/src/lib.rs | 20 +++--- crypto/sha3/src/parallelhash.rs | 30 ++++---- crypto/sha3/src/shake.rs | 68 +++++++++---------- crypto/sha3/src/tuplehash.rs | 32 ++++----- crypto/sha3/tests/bc-test-data.rs | 9 +-- crypto/sha3/tests/cshake_tests.rs | 32 ++++----- crypto/sha3/tests/kmac_tests.rs | 24 +++---- crypto/sha3/tests/parallelhash_tests.rs | 16 ++--- crypto/sha3/tests/shake_tests.rs | 38 +++++------ crypto/sha3/tests/tuplehash_tests.rs | 12 ++-- mem_usage_benches/src/bench_sha3_mem_usage.rs | 6 +- 36 files changed, 305 insertions(+), 304 deletions(-) diff --git a/cli/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index c6841128..7c0ae4c6 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -1,4 +1,4 @@ -use bouncycastle::core::traits::{Hash, XOF, XOFOutput}; +use bouncycastle::core::traits::{Hash, XOF, XOFSqueezer}; use std::io; use std::io::{Read, Write}; @@ -184,7 +184,7 @@ fn do_shake(mut shake: impl XOF, output_len: usize, output_hex: bool) { bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); } - let mut shake = shake.into_output(); + let mut shake = shake.into_squeezer(); let out = shake.do_output(output_len); if output_hex { for b in out.iter() { diff --git a/crypto/core-test-framework/src/xof.rs b/crypto/core-test-framework/src/xof.rs index ec25090b..d78af1e6 100644 --- a/crypto/core-test-framework/src/xof.rs +++ b/crypto/core-test-framework/src/xof.rs @@ -1,7 +1,7 @@ //! Generic behaviour tests for anything that implements [`XOF`]. use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{XOF, XOFOutput}; +use bouncycastle_core::traits::{XOF, XOFSqueezer}; /// Instance of the test framework. pub struct TestFrameworkXOF { @@ -19,7 +19,7 @@ impl TestFrameworkXOF { /// Exercises the trait against a known input-output pair. /// /// `expected_output` is the result of reading `expected_output.len()` bytes after absorbing - /// `input`. There is deliberately no absorb-after-squeeze test: [`XOF::into_output`] consumes + /// `input`. There is deliberately no absorb-after-squeeze test: [`XOF::into_squeezer`] consumes /// the XOF, so absorbing afterwards is not expressible and there is no runtime rule left to /// check. That guarantee is asserted instead by `compile_fail` doctests on the implementors. pub fn test_xof(&self, make: impl Fn() -> X, input: &[u8], expected_output: &[u8]) { @@ -30,7 +30,7 @@ impl TestFrameworkXOF { xof.do_update(chunk); } assert_eq!( - xof.into_output().do_output(expected_output.len()), + xof.into_squeezer().do_output(expected_output.len()), expected_output, "chunked input must equal a single update" ); @@ -39,7 +39,7 @@ impl TestFrameworkXOF { let mut xof = make(); xof.do_update(input); assert_eq!( - xof.into_output().do_output(expected_output.len()), + xof.into_squeezer().do_output(expected_output.len()), expected_output, "do_output must produce the expected bytes" ); @@ -49,7 +49,7 @@ impl TestFrameworkXOF { let mut output = vec![0xFFu8; expected_output.len()]; let mut xof = make(); xof.do_update(input); - let n = xof.into_output().do_output_out(&mut output); + let n = xof.into_squeezer().do_output_out(&mut output); assert_eq!(n, expected_output.len(), "do_output_out must report what it wrote"); assert_eq!(output, expected_output, "do_output_out must agree with do_output"); @@ -57,7 +57,7 @@ impl TestFrameworkXOF { let split = expected_output.len() / 2; let mut xof = make(); xof.do_update(input); - let mut out = xof.into_output(); + let mut out = xof.into_squeezer(); let first = out.do_output(split); let mut second = vec![0u8; expected_output.len() - split]; out.do_output_out(&mut second); @@ -72,21 +72,21 @@ impl TestFrameworkXOF { let mut buf = vec![0xFFu8; expected_output.len()]; let mut xof = make(); xof.do_update(input); - let n = xof.into_output().do_output_out(&mut buf); + let n = xof.into_squeezer().do_output_out(&mut buf); assert_eq!(n, expected_output.len()); assert_eq!(buf, expected_output, "do_output_out must zeroize before writing"); - /*** fn hash_xof(self, data: &[u8], result_len: usize) -> Vec ***/ + /*** fn xof(self, data: &[u8], result_len: usize) -> Vec ***/ assert_eq!( - make().hash_xof(input, expected_output.len()), + make().xof(input, expected_output.len()), expected_output, "the one-shot must equal update-then-output" ); let mut output = vec![0xFFu8; expected_output.len()]; - let n = make().hash_xof_out(input, &mut output); + let n = make().xof_out(input, &mut output); assert_eq!(n, expected_output.len()); - assert_eq!(output, expected_output, "hash_xof_out must agree with hash_xof"); + assert_eq!(output, expected_output, "xof_out must agree with xof"); /*** Clone: a XOF mid-absorb can be forked ***/ // The clone continues from the same absorbed prefix and owns its own sponge. @@ -97,12 +97,12 @@ impl TestFrameworkXOF { original.do_update(tail); forked.do_update(tail); assert_eq!( - original.into_output().do_output(expected_output.len()), + original.into_squeezer().do_output(expected_output.len()), expected_output, "the original must be unaffected by cloning" ); assert_eq!( - forked.into_output().do_output(expected_output.len()), + forked.into_squeezer().do_output(expected_output.len()), expected_output, "a clone must continue from the same absorbed prefix" ); @@ -114,8 +114,8 @@ impl TestFrameworkXOF { forked.do_update(&[0xA5]); forked.do_update(tail); assert_ne!( - forked.into_output().do_output(expected_output.len()), - original.into_output().do_output(expected_output.len()), + forked.into_squeezer().do_output(expected_output.len()), + original.into_squeezer().do_output(expected_output.len()), "a clone must have its own state, not share the original's" ); @@ -149,7 +149,7 @@ impl TestFrameworkXOF { b.do_update(input); assert_eq!( via_hash, - b.into_output().do_output(output_len), + b.into_squeezer().do_output(output_len), "do_final must equal do_output(output_len)" ); @@ -188,7 +188,7 @@ impl TestFrameworkXOF { let mut xof = make(); xof.do_update(input); assert_eq!( - xof.into_output_partial_bits(0, 0) + xof.into_squeezer_partial_bits(0, 0) .expect("0 is in range") .do_output(expected_output.len()), expected_output, @@ -200,7 +200,7 @@ impl TestFrameworkXOF { let mut a = make(); a.do_update(input); let with_bits = a - .into_output_partial_bits(0xFE, num_bits) + .into_squeezer_partial_bits(0xFE, num_bits) .expect("num_bits is in 1..=7") .do_output(expected_output.len()); assert_ne!( @@ -233,10 +233,10 @@ impl TestFrameworkXOF { xof.do_update(input); assert!( matches!( - xof.into_output_partial_bits(0xFF, num_bits), + xof.into_squeezer_partial_bits(0xFF, num_bits), Err(HashError::InvalidLength(_)) ), - "into_output_partial_bits must reject num_bits = {num_bits}" + "into_squeezer_partial_bits must reject num_bits = {num_bits}" ); let mut xof = make(); diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index e40a76f2..94b7b411 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -1779,7 +1779,7 @@ where /// The squeezing phase of an [`XOF`]: a value that produces output and can no longer take input. /// -/// This is the type [`XOF::into_output`] hands back. Absorbing and squeezing are separate types +/// This is the type [`XOF::into_squeezer`] hands back. Absorbing and squeezing are separate types /// rather than separate states of one type, so "no more input once output has begun" is a fact the /// compiler enforces rather than a rule the documentation asks callers to follow, and so there is /// no "absorbed after squeezing" error to raise or to test for. @@ -1791,7 +1791,7 @@ where /// only way to get output, and it must consume the value because finalizing pads the state. A /// squeeze has nothing to finalize, so such a method would only say "this read is my last", which /// ownership already says: drop the value, or let it fall out of scope. -pub trait XOFOutput { +pub trait XOFSqueezer { /// Produces the next `num_bytes` bytes of the output stream. fn do_output(&mut self, num_bytes: usize) -> Vec; @@ -1810,7 +1810,7 @@ pub trait XOFOutput { /// # Absorb, then squeeze /// /// A sponge takes input, then produces output, and cannot go back. Here that is expressed in the -/// types: [`into_output`](Self::into_output) consumes the XOF and returns an [`XOFOutput`], so +/// types: [`into_squeezer`](Self::into_squeezer) consumes the XOF and returns an [`XOFSqueezer`], so /// after output has begun there is no value left on which to call [`Hash::do_update`]. Nothing /// returns an "absorbed after squeezing" error because nothing can reach that state. /// @@ -1823,50 +1823,50 @@ pub trait XOFOutput { /// matters, salt the input. pub trait XOF: Hash { /// The squeezing state this XOF turns into. - type Output: XOFOutput; + type Squeezer: XOFSqueezer; /// Ends the input phase and begins producing output. /// /// The phase change is in the type: what comes back takes no more input. - fn into_output(self) -> Self::Output; + fn into_squeezer(self) -> Self::Squeezer; - /// As [`into_output`](Self::into_output), with a final partial **byte** of input. + /// As [`into_squeezer`](Self::into_squeezer), with a final partial **byte** of input. /// /// The partial byte arrives as the final octet of an ASN.1 BIT STRING (X.690 s. 8.6.2.1): the /// `num_bits` message bits are the most significant bits of `partial_byte`, leading bit first, /// and the low `8 - num_bits` "unused" bits are ignored. Same convention as /// [`Hash::do_final_partial_bits`]. `num_bits` of 0 means the message ended on a byte boundary - /// and is equivalent to [`into_output`](Self::into_output). + /// and is equivalent to [`into_squeezer`](Self::into_squeezer). /// /// # Errors /// [`HashError::InvalidLength`] if `num_bits` is not in `0..=7`. - fn into_output_partial_bits( + fn into_squeezer_partial_bits( self, partial_byte: u8, num_bits: usize, - ) -> Result; + ) -> Result; /// One-shot: absorbs `data` and produces `result_len` bytes. /// /// The default absorbs and squeezes in the obvious way; override it only where the type can do /// better, as SHAKE does. - fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec + fn xof(mut self, data: &[u8], result_len: usize) -> Vec where Self: Sized, { self.do_update(data); - self.into_output().do_output(result_len) + self.into_squeezer().do_output(result_len) } /// One-shot: absorbs `data` and fills `output`, which is zeroized first. Returns the number of /// bytes written. /// - /// Defaulted as [`hash_xof`](Self::hash_xof) is. - fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize + /// Defaulted as [`xof`](Self::xof) is. + fn xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize where Self: Sized, { self.do_update(data); - self.into_output().do_output_out(output) + self.into_squeezer().do_output_out(output) } } diff --git a/crypto/factory/src/xof_factory.rs b/crypto/factory/src/xof_factory.rs index b3749a66..27cc5a5e 100644 --- a/crypto/factory/src/xof_factory.rs +++ b/crypto/factory/src/xof_factory.rs @@ -5,7 +5,7 @@ //! //! Example usage: //! ``` -//! use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; +//! use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; //! use bouncycastle_factory::AlgorithmFactory; //! use bouncycastle_factory::xof_factory::XOFFactory; //! use bouncycastle_sha3 as sha3; @@ -14,7 +14,7 @@ //! //! let mut h = XOFFactory::new(sha3::SHAKE128_NAME).unwrap(); //! h.do_update(data); -//! let output: Vec = h.into_output().do_output(16); +//! let output: Vec = h.into_squeezer().do_output(16); //! ``` //! `XOFFactory` implements [`Hash`] too, so it can be used wherever a hash is wanted; `do_final` //! then produces the nominal 32 or 64 bytes. @@ -37,7 +37,7 @@ use crate::{AlgorithmFactory, FactoryError}; use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFSqueezer}; use bouncycastle_sha3 as sha3; use bouncycastle_sha3::{SHAKE128_NAME, SHAKE256_NAME}; @@ -96,16 +96,16 @@ impl Algorithm for XOFFactory { /// The squeezing phase of whichever XOF the factory selected. /// -/// [`XOF::into_output`] consumes the factory value, so this enum is what remains; like +/// [`XOF::into_squeezer`] consumes the factory value, so this enum is what remains; like /// [`XOFFactory`] itself it dispatches on the variant. -pub enum XOFFactoryOutput { +pub enum XOFFactorySqueezer { /// SHAKE128 output. - SHAKE128(::Output), + SHAKE128(::Squeezer), /// SHAKE256 output. - SHAKE256(::Output), + SHAKE256(::Squeezer), } -impl XOFOutput for XOFFactoryOutput { +impl XOFSqueezer for XOFFactorySqueezer { fn do_output(&mut self, num_bytes: usize) -> Vec { match self { Self::SHAKE128(o) => o.do_output(num_bytes), @@ -203,43 +203,43 @@ impl Hash for XOFFactory { } impl XOF for XOFFactory { - type Output = XOFFactoryOutput; + type Squeezer = XOFFactorySqueezer; - fn into_output(self) -> Self::Output { + fn into_squeezer(self) -> Self::Squeezer { match self { - Self::SHAKE128(h) => XOFFactoryOutput::SHAKE128(h.into_output()), - Self::SHAKE256(h) => XOFFactoryOutput::SHAKE256(h.into_output()), + Self::SHAKE128(h) => XOFFactorySqueezer::SHAKE128(h.into_squeezer()), + Self::SHAKE256(h) => XOFFactorySqueezer::SHAKE256(h.into_squeezer()), } } - fn into_output_partial_bits( + fn into_squeezer_partial_bits( self, partial_byte: u8, num_bits: usize, - ) -> Result { + ) -> Result { Ok(match self { Self::SHAKE128(h) => { - XOFFactoryOutput::SHAKE128(h.into_output_partial_bits(partial_byte, num_bits)?) + XOFFactorySqueezer::SHAKE128(h.into_squeezer_partial_bits(partial_byte, num_bits)?) } Self::SHAKE256(h) => { - XOFFactoryOutput::SHAKE256(h.into_output_partial_bits(partial_byte, num_bits)?) + XOFFactorySqueezer::SHAKE256(h.into_squeezer_partial_bits(partial_byte, num_bits)?) } }) } - fn hash_xof(self, data: &[u8], result_len: usize) -> Vec { + fn xof(self, data: &[u8], result_len: usize) -> Vec { match self { - Self::SHAKE128(h) => h.hash_xof(data, result_len), - Self::SHAKE256(h) => h.hash_xof(data, result_len), + Self::SHAKE128(h) => h.xof(data, result_len), + Self::SHAKE256(h) => h.xof(data, result_len), } } - fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { + fn xof_out(self, data: &[u8], output: &mut [u8]) -> usize { output.fill(0); match self { - Self::SHAKE128(h) => h.hash_xof_out(data, output), - Self::SHAKE256(h) => h.hash_xof_out(data, output), + Self::SHAKE128(h) => h.xof_out(data, output), + Self::SHAKE256(h) => h.xof_out(data, output), } } } diff --git a/crypto/factory/tests/hash_factory_tests.rs b/crypto/factory/tests/hash_factory_tests.rs index 8d90be83..22f5a3b4 100644 --- a/crypto/factory/tests/hash_factory_tests.rs +++ b/crypto/factory/tests/hash_factory_tests.rs @@ -160,8 +160,8 @@ mod hash_factory_tests { #[test] fn sha3_xof_tests() { - assert_eq!(XOFFactory::new("SHAKE128").unwrap().hash_xof(&DUMMY_SEED[..512], 32), b"\x88\x90\xed\x20\x4d\x22\x89\xe1\x72\xe9\xae\x68\x48\x18\x23\x77\x08\x20\x90\x80\x60\xa4\xdf\x33\x51\xa3\xf1\x84\xeb\xb6\xdd\x0f"); - assert_eq!(XOFFactory::new("SHAKE256").unwrap().hash_xof(&DUMMY_SEED[..512], 32), b"\xa1\xd7\x18\x85\xb0\xa8\x41\xf0\x3d\x1d\xc7\xf2\x73\x8a\x15\xcc\x98\x40\x71\xa1\x7f\xfe\xd5\xec\xac\xb9\xf5\x87\x20\xa4\x73\xbe"); + assert_eq!(XOFFactory::new("SHAKE128").unwrap().xof(&DUMMY_SEED[..512], 32), b"\x88\x90\xed\x20\x4d\x22\x89\xe1\x72\xe9\xae\x68\x48\x18\x23\x77\x08\x20\x90\x80\x60\xa4\xdf\x33\x51\xa3\xf1\x84\xeb\xb6\xdd\x0f"); + assert_eq!(XOFFactory::new("SHAKE256").unwrap().xof(&DUMMY_SEED[..512], 32), b"\xa1\xd7\x18\x85\xb0\xa8\x41\xf0\x3d\x1d\xc7\xf2\x73\x8a\x15\xcc\x98\x40\x71\xa1\x7f\xfe\xd5\xec\xac\xb9\xf5\x87\x20\xa4\x73\xbe"); } #[test] diff --git a/crypto/factory/tests/xof_factory_tests.rs b/crypto/factory/tests/xof_factory_tests.rs index 8beb93a7..bea0ca87 100644 --- a/crypto/factory/tests/xof_factory_tests.rs +++ b/crypto/factory/tests/xof_factory_tests.rs @@ -3,7 +3,7 @@ //! direct type side by side on the same input; nothing here is an expected value written by hand. use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_factory::xof_factory::XOFFactory; use bouncycastle_factory::{AlgorithmFactory, FactoryError}; @@ -11,7 +11,7 @@ use bouncycastle_sha3::{SHAKE128, SHAKE128_NAME, SHAKE256, SHAKE256_NAME}; const MSG: &[u8] = b"The quick brown fox jumps over the lazy dog"; -/// Every `Hash`, `XOF` and `XOFOutput` method of the factory against the direct type `S`. +/// Every `Hash`, `XOF` and `XOFSqueezer` method of the factory against the direct type `S`. fn check_against(make: impl Fn() -> XOFFactory, ctx: &str) { let n = S::default().output_len(); @@ -69,12 +69,12 @@ fn check_against(make: impl Fn() -> XOFFactory, ctx: &str) { // the XOF view: one stream, of which the Hash view is the first output_len bytes let mut s = S::default(); s.do_update(MSG); - let long = s.into_output().do_output(3 * n); + let long = s.into_squeezer().do_output(3 * n); assert_eq!(&long[..n], &expected[..], "the direct type's hash is a prefix of its stream"); let mut f = make(); f.do_update(MSG); - let mut fo = f.into_output(); + let mut fo = f.into_squeezer(); assert_eq!(fo.do_output(n), &long[..n], "{ctx}: do_output"); let mut buf = vec![0u8; 2 * n]; assert_eq!(fo.do_output_out(&mut buf), 2 * n, "{ctx}: do_output_out returns the length"); @@ -82,23 +82,23 @@ fn check_against(make: impl Fn() -> XOFFactory, ctx: &str) { let mut s = S::default(); s.do_update(MSG); - let want = s.into_output_partial_bits(0x05, 3).unwrap().do_output(n); + let want = s.into_squeezer_partial_bits(0x05, 3).unwrap().do_output(n); let mut f = make(); f.do_update(MSG); assert_eq!( - f.into_output_partial_bits(0x05, 3).unwrap().do_output(n), + f.into_squeezer_partial_bits(0x05, 3).unwrap().do_output(n), want, - "{ctx}: into_output_partial_bits" + "{ctx}: into_squeezer_partial_bits" ); let mut f = make(); f.do_update(MSG); - assert!(matches!(f.into_output_partial_bits(0xFF, 8), Err(HashError::InvalidLength(_)))); + assert!(matches!(f.into_squeezer_partial_bits(0xFF, 8), Err(HashError::InvalidLength(_)))); // the one-shots - assert_eq!(make().hash_xof(MSG, 3 * n), long, "{ctx}: hash_xof"); + assert_eq!(make().xof(MSG, 3 * n), long, "{ctx}: xof"); let mut out = vec![0xFFu8; 3 * n]; - assert_eq!(make().hash_xof_out(MSG, &mut out), 3 * n, "{ctx}: hash_xof_out returns the length"); - assert_eq!(out, long, "{ctx}: hash_xof_out"); + assert_eq!(make().xof_out(MSG, &mut out), 3 * n, "{ctx}: xof_out returns the length"); + assert_eq!(out, long, "{ctx}: xof_out"); } #[test] @@ -138,11 +138,11 @@ fn test_framework_xof() { framework.test_xof( || XOFFactory::new(SHAKE128_NAME).unwrap(), MSG, - &SHAKE128::new().hash_xof(MSG, 100), + &SHAKE128::new().xof(MSG, 100), ); framework.test_xof( || XOFFactory::new(SHAKE256_NAME).unwrap(), MSG, - &SHAKE256::new().hash_xof(MSG, 100), + &SHAKE256::new().xof(MSG, 100), ); } diff --git a/crypto/mldsa-lowmemory/src/aux_functions.rs b/crypto/mldsa-lowmemory/src/aux_functions.rs index 93c2b490..7f5c702a 100644 --- a/crypto/mldsa-lowmemory/src/aux_functions.rs +++ b/crypto/mldsa-lowmemory/src/aux_functions.rs @@ -7,7 +7,7 @@ use crate::params::{ MLDSAParams, }; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle_utils::secret::ZeroizablePrimitive; /// Algorithm 14 CoeffFromThreeBytes(𝑏0, 𝑏1, 𝑏2) @@ -435,7 +435,7 @@ pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { let mut h = H::new(); h.do_update(rho.as_ref()); let mut s = [0u8; 8]; - let mut h = h.into_output(); + let mut h = h.into_squeezer(); h.do_output_out(&mut s); // 5: β„Ž ← BytesToBits(𝑠) @@ -506,7 +506,7 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // It's probably around the average rejection rate, and 288 is a multiple of both 3 (required for this alg) // and 8 (efficient for SHAKE). let mut s = [0u8; 288]; - let mut g = g.into_output(); + let mut g = g.into_squeezer(); g.do_output_out(&mut s); let mut idx: usize = 0; @@ -552,7 +552,7 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) // which is possibly also related with the average rejection rate. // Also, 312 is a multiple of 8 (efficient for SHAKE) let mut z_arr = [0u8; 312]; - let mut h = h.into_output(); + let mut h = h.into_squeezer(); h.do_output_out(&mut z_arr); let mut idx: usize = 0; @@ -594,7 +594,7 @@ pub(crate) fn expand_mask_poly(rho: &[u8; 64], nonce: u16) -> Po h.do_update(rho); h.do_update(&nonce.to_le_bytes()); let mut v = ::ZEROED; - let mut h = h.into_output(); + let mut h = h.into_squeezer(); h.do_output_out(v.as_mut()); bit_unpack_gamma1::

(v.as_ref()) } diff --git a/crypto/mldsa-lowmemory/src/hash_mldsa.rs b/crypto/mldsa-lowmemory/src/hash_mldsa.rs index f4f0ba59..a095a699 100644 --- a/crypto/mldsa-lowmemory/src/hash_mldsa.rs +++ b/crypto/mldsa-lowmemory/src/hash_mldsa.rs @@ -83,7 +83,7 @@ use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, PHSignatureVerifier, PHSigner, RNG, SecurityStrength, - SignatureVerifier, Signer, XOF, XOFOutput, + SignatureVerifier, Signer, XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use core::marker::PhantomData; @@ -353,7 +353,7 @@ impl< h.do_update(::OID_DER); h.do_update(ph); let mut mu = [0u8; MLDSA_MU_LEN]; - let mut h = h.into_output(); + let mut h = h.into_squeezer(); let bytes_written = h.do_output_out(&mut mu); debug_assert_eq!(bytes_written, MLDSA_MU_LEN); @@ -642,7 +642,7 @@ impl< h.do_update(::OID_DER); h.do_update(ph); let mut mu = [0u8; MLDSA_MU_LEN]; - let mut h = h.into_output(); + let mut h = h.into_squeezer(); _ = h.do_output_out(&mut mu); MLDSA::::verify_mu( diff --git a/crypto/mldsa-lowmemory/src/mldsa.rs b/crypto/mldsa-lowmemory/src/mldsa.rs index 4e69002c..d145f714 100644 --- a/crypto/mldsa-lowmemory/src/mldsa.rs +++ b/crypto/mldsa-lowmemory/src/mldsa.rs @@ -400,7 +400,7 @@ use bouncycastle_core::errors::{RNGError, SignatureError, SuspendableError}; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, RNG, SecurityStrength, SignatureVerifier, Signer, Suspendable, - XOF, XOFOutput, + XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN}; @@ -792,7 +792,7 @@ impl< h.do_update(&rnd); h.do_update(mu); let mut rho_p_p = [0u8; 64]; - let mut h = h.into_output(); + let mut h = h.into_squeezer(); h.do_output_out(&mut rho_p_p); rho_p_p @@ -826,7 +826,7 @@ impl< hash.do_update(w.w1_encode::

().as_ref()); } let mut sig_val_c_tilde = ::ZEROED; - let mut hash = hash.into_output(); + let mut hash = hash.into_squeezer(); hash.do_output_out(sig_val_c_tilde.as_mut()); sig_val_c_tilde }; @@ -1040,7 +1040,7 @@ impl< } let mut c_tilde_p = ::ZEROED; - let mut hash = hash.into_output(); + let mut hash = hash.into_squeezer(); hash.do_output_out(c_tilde_p.as_mut()); // Verification is also done in constant time @@ -1472,7 +1472,7 @@ impl MuBuilder { // Algorithm 7 // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀 β€², 64) let mut mu = [0u8; 64]; - self.h.into_output().do_output_out(&mut mu); + self.h.into_squeezer().do_output_out(&mut mu); mu } diff --git a/crypto/mldsa-lowmemory/src/mldsa_keys.rs b/crypto/mldsa-lowmemory/src/mldsa_keys.rs index b578c939..9f293e83 100644 --- a/crypto/mldsa-lowmemory/src/mldsa_keys.rs +++ b/crypto/mldsa-lowmemory/src/mldsa_keys.rs @@ -12,7 +12,7 @@ use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ - Hash, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, XOF, XOFOutput, + Hash, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, XOF, XOFSqueezer, }; use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; use core::fmt; @@ -97,7 +97,7 @@ impl MLDSAPublicKeyTrait fn compute_tr(&self) -> [u8; 64] { let mut tr = [0u8; 64]; - H::new().hash_xof_out(&self.encode(), &mut tr); + H::new().xof_out(&self.encode(), &mut tr); tr } @@ -342,7 +342,7 @@ impl(rho: &P::SigCTilde) -> Polynomial { let mut h = H::new(); h.do_update(rho.as_ref()); let mut s = [0u8; 8]; - let mut h = h.into_output(); + let mut h = h.into_squeezer(); h.do_output_out(&mut s); // 5: β„Ž ← BytesToBits(𝑠) @@ -574,7 +574,7 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // It's probably around the average rejection rate, and 288 is a multiple of both 3 (required for this alg) // and 8 (efficient for SHAKE). let mut s = [0u8; 288]; - let mut g = g.into_output(); + let mut g = g.into_squeezer(); g.do_output_out(&mut s); let mut idx: usize = 0; @@ -619,7 +619,7 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) // maybe something to do with the average rejection rate? // Also, 312 is a multiple of 8 (efficient for SHAKE) let mut z_arr = [0u8; 312]; - let mut h = h.into_output(); + let mut h = h.into_squeezer(); h.do_output_out(&mut z_arr); let mut idx: usize = 0; @@ -719,7 +719,7 @@ pub(crate) fn expand_mask(rho: &[u8; 64], mu: u16) -> P::VecL { h.do_update(rho); h.do_update(&(mu + (r as u16)).to_le_bytes()); let mut v = ::ZEROED; - let mut h = h.into_output(); + let mut h = h.into_squeezer(); h.do_output_out(v.as_mut()); v }; diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index 137025cd..aad5d55f 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -84,7 +84,7 @@ use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, PHSignatureVerifier, PHSigner, RNG, SecurityStrength, - SignatureVerifier, Signer, XOF, XOFOutput, + SignatureVerifier, Signer, XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use core::marker::PhantomData; @@ -395,7 +395,7 @@ impl< h.do_update(::OID_DER); h.do_update(ph); let mut mu = [0u8; MLDSA_MU_LEN]; - let mut h = h.into_output(); + let mut h = h.into_squeezer(); let bytes_written = h.do_output_out(&mut mu); debug_assert_eq!(bytes_written, MLDSA_MU_LEN); @@ -500,7 +500,7 @@ impl< h.do_update(::OID_DER); h.do_update(ph); let mut mu = [0u8; MLDSA_MU_LEN]; - let mut h = h.into_output(); + let mut h = h.into_squeezer(); _ = h.do_output_out(&mut mu); mu diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs index da49457a..82ed65a5 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -491,7 +491,7 @@ use bouncycastle_core::errors::{RNGError, SignatureError, SuspendableError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterial256, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, RNG, SecurityStrength, SignatureVerifier, Signer, Suspendable, - XOF, XOFOutput, + XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN}; @@ -694,7 +694,7 @@ impl< h.do_update(seed.ref_to_bytes()); h.do_update(&(P::k as u8).to_le_bytes()); h.do_update(&(P::l as u8).to_le_bytes()); - let mut h = h.into_output(); + 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]; @@ -790,7 +790,7 @@ impl< h.do_update(&rnd); h.do_update(mu); let mut rho_p_p = [0u8; 64]; - let mut h = h.into_output(); + let mut h = h.into_squeezer(); h.do_output_out(&mut rho_p_p); rho_p_p @@ -846,7 +846,7 @@ impl< let mut hash = H::new(); hash.do_update(mu); w1.w1_encode_and_hash::

(&mut hash); - let mut hash = hash.into_output(); + let mut hash = hash.into_squeezer(); hash.do_output_out(sig_val_c_tilde.as_mut()); } @@ -1025,7 +1025,7 @@ impl< let mut hash = H::new(); hash.do_update(mu); w1p.w1_encode_and_hash::

(&mut hash); - let mut hash = hash.into_output(); + let mut hash = hash.into_squeezer(); hash.do_output_out(c_tilde_p.as_mut()); c_tilde_p @@ -1251,7 +1251,7 @@ impl< h.do_update(&(P::k as u8).to_le_bytes()); h.do_update(&(P::l as u8).to_le_bytes()); let mut rho = [0u8; 32]; - let mut h = h.into_output(); + 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 = [0u8; 64]; @@ -1271,7 +1271,7 @@ impl< h.do_update(&rnd); h.do_update(mu); let mut rho_p_p = [0u8; 64]; - let mut h = h.into_output(); + let mut h = h.into_squeezer(); h.do_output_out(&mut rho_p_p); rho_p_p @@ -1342,7 +1342,7 @@ impl< let mut hash = H::new(); hash.do_update(mu); w1.w1_encode_and_hash::

(&mut hash); - let mut hash = hash.into_output(); + let mut hash = hash.into_squeezer(); hash.do_output_out(sig_val_c_tilde.as_mut()); } @@ -1993,7 +1993,7 @@ impl MuBuilder { // Algorithm 7 // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀 β€², 64) let mut mu = [0u8; 64]; - self.h.into_output().do_output_out(&mut mu); + self.h.into_squeezer().do_output_out(&mut mu); mu } diff --git a/crypto/mldsa/src/mldsa_keys.rs b/crypto/mldsa/src/mldsa_keys.rs index 5d4dee7d..3516ed6c 100644 --- a/crypto/mldsa/src/mldsa_keys.rs +++ b/crypto/mldsa/src/mldsa_keys.rs @@ -179,7 +179,7 @@ impl MLDSAPublicKeyTrait fn compute_tr(&self) -> [u8; 64] { let mut tr = [0u8; 64]; - H::new().hash_xof_out(&self.encode(), &mut tr); + H::new().xof_out(&self.encode(), &mut tr); tr } diff --git a/crypto/mldsa/tests/bc_test_data.rs b/crypto/mldsa/tests/bc_test_data.rs index f7e9e6a2..878bb56a 100644 --- a/crypto/mldsa/tests/bc_test_data.rs +++ b/crypto/mldsa/tests/bc_test_data.rs @@ -5,7 +5,7 @@ #![allow(dead_code)] use bouncycastle_core::errors::SignatureError; -use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle_sha3::SHAKE256; #[cfg(test)] @@ -990,7 +990,7 @@ impl BustedMuBuilder { // Algorithm 7 // 6: πœ‡ ← H(BytesToBits(π‘‘π‘Ÿ)||𝑀 β€², 64) let mut mu = [0u8; 64]; - self.h.into_output().do_output_out(&mut mu); + self.h.into_squeezer().do_output_out(&mut mu); mu } diff --git a/crypto/mlkem-lowmemory/src/aux_functions.rs b/crypto/mlkem-lowmemory/src/aux_functions.rs index 507dfbb1..874e5ca9 100644 --- a/crypto/mlkem-lowmemory/src/aux_functions.rs +++ b/crypto/mlkem-lowmemory/src/aux_functions.rs @@ -2,7 +2,7 @@ use crate::mlkem::{N, q, q_inv}; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle_sha3::{SHAKE128, SHAKE256}; /// Algorithm 5 ByteEncode_d(𝐹) @@ -95,7 +95,7 @@ pub(crate) fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // It's likely around the average rejection rate, and 216 is a multiple of both 3 (required for this alg) // and 8 (efficient for SHAKE). let mut C = [0u8; 216]; - let mut xof = xof.into_output(); + let mut xof = xof.into_squeezer(); xof.do_output_out(&mut C); let mut idx: usize = 0; @@ -205,7 +205,7 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { xof.do_update(&n.to_le_bytes()); let mut buf = [0u8; 2 * 64]; - let mut xof = xof.into_output(); + let mut xof = xof.into_squeezer(); xof.do_output_out(&mut buf); buf }; @@ -218,7 +218,7 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { xof.do_update(b); xof.do_update(&n.to_le_bytes()); let mut buf = [0u8; 3 * 64]; - let mut xof = xof.into_output(); + let mut xof = xof.into_squeezer(); xof.do_output_out(&mut buf); buf }; diff --git a/crypto/mlkem-lowmemory/src/mlkem.rs b/crypto/mlkem-lowmemory/src/mlkem.rs index d1bb1224..dd4e64c8 100644 --- a/crypto/mlkem-lowmemory/src/mlkem.rs +++ b/crypto/mlkem-lowmemory/src/mlkem.rs @@ -19,7 +19,7 @@ use bouncycastle_core::key_material::{ }; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, SecurityStrength, XOF, - XOFOutput, + XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHA3_256, SHA3_512, SHAKE256}; @@ -434,7 +434,7 @@ impl< let mut j = J::new(); j.do_update(dk.z()); j.do_update(&c); - let mut j = j.into_output(); + let mut j = j.into_squeezer(); let bytes_written = j.do_output_out(&mut *K_bar); debug_assert_eq!(bytes_written, MLKEM_SS_LEN); diff --git a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs index e1b661b4..81a9c844 100644 --- a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs +++ b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs @@ -7,7 +7,7 @@ mod mlkem_tests { }; use bouncycastle_core::traits::{ Hash, KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength, XOF, - XOFOutput, + XOFSqueezer, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; @@ -438,7 +438,7 @@ mod mlkem_tests { shake.do_update(&seed.ref_to_bytes()[32..64]); shake.do_update(&busted_ciphertext); let mut buf = [0u8; 32]; - let mut shake = shake.into_output(); + let mut shake = shake.into_squeezer(); _ = shake.do_output_out(&mut buf); assert_eq!(ss.ref_to_bytes(), buf); diff --git a/crypto/mlkem/src/aux_functions.rs b/crypto/mlkem/src/aux_functions.rs index 2292e1c8..18de14a5 100644 --- a/crypto/mlkem/src/aux_functions.rs +++ b/crypto/mlkem/src/aux_functions.rs @@ -4,7 +4,7 @@ use crate::matrix::{MatrixTrait, VectorTrait}; use crate::mlkem::{N, q, q_inv}; use crate::params::MLKEMParams; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle_sha3::{SHAKE128, SHAKE256}; pub(crate) fn expandA(rho: &[u8; 32]) -> P::MatrixA { @@ -104,7 +104,7 @@ pub fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // It's probably around the average rejection rate, and 216 is a multiple of both 3 (required for this alg) // and 8 (efficient for SHAKE). let mut C = [0u8; 216]; - let mut xof = xof.into_output(); + let mut xof = xof.into_squeezer(); xof.do_output_out(&mut C); let mut idx: usize = 0; @@ -214,7 +214,7 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { xof.do_update(&n.to_le_bytes()); let mut buf = [0u8; 2 * 64]; - let mut xof = xof.into_output(); + let mut xof = xof.into_squeezer(); xof.do_output_out(&mut buf); buf }; @@ -227,7 +227,7 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { xof.do_update(b); xof.do_update(&n.to_le_bytes()); let mut buf = [0u8; 3 * 64]; - let mut xof = xof.into_output(); + let mut xof = xof.into_squeezer(); xof.do_output_out(&mut buf); buf }; diff --git a/crypto/mlkem/src/mlkem.rs b/crypto/mlkem/src/mlkem.rs index 8a3d88f8..9136e425 100644 --- a/crypto/mlkem/src/mlkem.rs +++ b/crypto/mlkem/src/mlkem.rs @@ -151,7 +151,7 @@ use bouncycastle_core::key_material::{ }; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, SecurityStrength, XOF, - XOFOutput, + XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHA3_256, SHA3_512, SHAKE256}; @@ -639,7 +639,7 @@ impl< j.do_update(dk.z().as_ref()); j.do_update(&c); let mut buf = [0u8; MLKEM_SS_LEN]; - let mut j = j.into_output(); + let mut j = j.into_squeezer(); let bytes_written = j.do_output_out(&mut buf); debug_assert_eq!(bytes_written, MLKEM_SS_LEN); diff --git a/crypto/mlkem/tests/mlkem_tests.rs b/crypto/mlkem/tests/mlkem_tests.rs index 65331ae2..fb210165 100644 --- a/crypto/mlkem/tests/mlkem_tests.rs +++ b/crypto/mlkem/tests/mlkem_tests.rs @@ -6,7 +6,7 @@ mod mlkem_tests { use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ Hash, KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength, XOF, - XOFOutput, + XOFSqueezer, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; @@ -473,7 +473,7 @@ mod mlkem_tests { shake.do_update(&seed.ref_to_bytes()[32..64]); shake.do_update(&busted_ciphertext); let mut buf = [0u8; 32]; - let mut shake = shake.into_output(); + let mut shake = shake.into_squeezer(); _ = shake.do_output_out(&mut buf); assert_eq!(ss.ref_to_bytes(), buf); diff --git a/crypto/sha3/benches/sha3_benches.rs b/crypto/sha3/benches/sha3_benches.rs index e2006a6a..b7555c80 100644 --- a/crypto/sha3/benches/sha3_benches.rs +++ b/crypto/sha3/benches/sha3_benches.rs @@ -125,7 +125,7 @@ fn bench_shake128_64b(c: &mut Criterion) { format!("input: {} bytes, output: {} bytes -- ::hashes()", big_data.len(), digest.len()), |b| { b.iter(|| { - SHAKE128::new().hash_xof_out(black_box(&big_data), &mut digest); + SHAKE128::new().xof_out(black_box(&big_data), &mut digest); black_box(&digest); }) }, @@ -149,7 +149,7 @@ fn bench_shake128_64k(c: &mut Criterion) { format!("input: {} bytes, output: {} bytes -- ::hashes()", big_data.len(), digest.len()), |b| { b.iter(|| { - SHAKE128::new().hash_xof_out(black_box(&big_data), &mut digest); + SHAKE128::new().xof_out(black_box(&big_data), &mut digest); black_box(&digest); }) }, @@ -173,7 +173,7 @@ fn bench_shake256_64b(c: &mut Criterion) { format!("input: {} bytes, output: {} bytes -- ::hashes()", big_data.len(), digest.len()), |b| { b.iter(|| { - SHAKE256::new().hash_xof_out(black_box(&big_data), &mut digest); + SHAKE256::new().xof_out(black_box(&big_data), &mut digest); black_box(&digest); }) }, @@ -197,7 +197,7 @@ fn bench_shake256_64k(c: &mut Criterion) { format!("input: {} bytes, output: {} bytes -- ::hashes()", big_data.len(), digest.len()), |b| { b.iter(|| { - SHAKE128::new().hash_xof_out(black_box(&big_data), &mut digest); + SHAKE128::new().xof_out(black_box(&big_data), &mut digest); black_box(&digest); }) }, diff --git a/crypto/sha3/src/cshake.rs b/crypto/sha3/src/cshake.rs index 6268efd1..dfc21100 100644 --- a/crypto/sha3/src/cshake.rs +++ b/crypto/sha3/src/cshake.rs @@ -1,10 +1,10 @@ //! cSHAKE, the customizable SHAKE of NIST SP 800-185 Sec 3. use crate::SHAKEParams; -use crate::shake::{SHAKEInternal, SHAKEOutput}; +use crate::shake::{SHAKEInternal, SHAKESqueezer}; use crate::xof_utils::left_encode; use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFSqueezer}; /// The domain separator cSHAKE absorbs in place of SHAKE's `1111`: the `00` of SP 800-185 Sec 3.3, /// two zero bits, which is what keeps a customized instance separate from plain SHAKE. @@ -151,7 +151,7 @@ impl Hash for CSHAKEInternal { fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { self.do_update(data); - self.into_output().do_output_out(output) + self.into_squeezer().do_output_out(output) } fn do_update(&mut self, data: &[u8]) { @@ -160,11 +160,11 @@ impl Hash for CSHAKEInternal { fn do_final(self) -> Vec { let n = self.output_len(); - self.into_output().do_output(n) + self.into_squeezer().do_output(n) } fn do_final_out(self, output: &mut [u8]) -> usize { - self.into_output().do_output_out(output) + self.into_squeezer().do_output_out(output) } fn do_final_partial_bits( @@ -183,7 +183,7 @@ impl Hash for CSHAKEInternal { num_bits: usize, output: &mut [u8], ) -> Result { - Ok(self.into_output_partial_bits(partial_byte, num_bits)?.do_output_out(output)) + Ok(self.into_squeezer_partial_bits(partial_byte, num_bits)?.do_output_out(output)) } fn max_security_strength(&self) -> SecurityStrength { @@ -192,28 +192,28 @@ impl Hash for CSHAKEInternal { } impl XOF for CSHAKEInternal { - type Output = SHAKEOutput; + type Squeezer = SHAKESqueezer; - fn into_output(self) -> Self::Output { + fn into_squeezer(self) -> Self::Squeezer { if self.customized { let (suffix, bits) = CSHAKE_SUFFIX; - self.shake.into_output_with_suffix(suffix, bits) + self.shake.into_squeezer_with_suffix(suffix, bits) } else { // Sec 3.3 step 1: with no N and no S this is SHAKE, separator included. - self.shake.into_output() + self.shake.into_squeezer() } } - fn into_output_partial_bits( + fn into_squeezer_partial_bits( self, partial_byte: u8, num_bits: usize, - ) -> Result { + ) -> Result { if self.customized { let (suffix, bits) = CSHAKE_SUFFIX; - self.shake.into_output_partial_bits_with_suffix(partial_byte, num_bits, suffix, bits) + self.shake.into_squeezer_partial_bits_with_suffix(partial_byte, num_bits, suffix, bits) } else { - self.shake.into_output_partial_bits(partial_byte, num_bits) + self.shake.into_squeezer_partial_bits(partial_byte, num_bits) } } } diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs index 0f437ba9..3e8706a4 100644 --- a/crypto/sha3/src/kmac.rs +++ b/crypto/sha3/src/kmac.rs @@ -2,11 +2,11 @@ use crate::SHAKEParams; use crate::cshake::CSHAKEInternal; -use crate::shake::SHAKEOutput; +use crate::shake::SHAKESqueezer; use crate::xof_utils::right_encode; use bouncycastle_core::errors::{HashError, KeyMaterialError, MACError}; use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{Algorithm, Hash, MAC, SecurityStrength, XOF, XOFOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, MAC, SecurityStrength, XOF, XOFSqueezer}; use bouncycastle_utils::ct; /// The function-name string every KMAC binds, per SP 800-185 Sec 4.3. Fixed by the specification: @@ -133,7 +133,7 @@ impl MAC for KMACInternal { let n = self.output_len; // Sec 4.3 step 1: the requested length is bound into the input before any output. self.absorb_right_encode((n as u64) * 8); - self.cshake.into_output().do_output(n) + self.cshake.into_squeezer().do_output(n) } fn do_final_out(mut self, out: &mut [u8]) -> Result { @@ -147,7 +147,7 @@ impl MAC for KMACInternal { // MAC::do_final_out zeroizes the entire buffer, as HMAC does, so a longer one comes back // with zeros after the MAC rather than whatever the caller left there. out[n..].fill(0); - Ok(self.cshake.into_output().do_output_out(&mut out[..n])) + Ok(self.cshake.into_squeezer().do_output_out(&mut out[..n])) } /// Compares in constant time, and only against the full output length: a caller must not be @@ -186,7 +186,7 @@ impl MAC for KMACInternal { /// /// Because the length is *not* bound here, output at one length really is a prefix of output at a /// longer one -- the opposite of fixed-length KMAC -- so [`Hash::do_final`] is the first -/// [`Hash::output_len`] bytes of the same stream [`XOF::into_output`] produces. +/// [`Hash::output_len`] bytes of the same stream [`XOF::into_squeezer`] produces. #[derive(Clone)] pub struct KMACXOFInternal { cshake: CSHAKEInternal, @@ -238,12 +238,12 @@ impl Hash for KMACXOFInternal { fn hash(mut self, data: &[u8]) -> Vec { let n = self.output_len(); self.do_update(data); - self.into_output().do_output(n) + self.into_squeezer().do_output(n) } fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { self.do_update(data); - self.into_output().do_output_out(output) + self.into_squeezer().do_output_out(output) } fn do_update(&mut self, data: &[u8]) { @@ -252,11 +252,11 @@ impl Hash for KMACXOFInternal { fn do_final(self) -> Vec { let n = self.output_len(); - self.into_output().do_output(n) + self.into_squeezer().do_output(n) } fn do_final_out(self, output: &mut [u8]) -> usize { - self.into_output().do_output_out(output) + self.into_squeezer().do_output_out(output) } /// # Errors @@ -294,23 +294,23 @@ impl Hash for KMACXOFInternal { } impl XOF for KMACXOFInternal { - type Output = SHAKEOutput; + type Squeezer = SHAKESqueezer; - fn into_output(mut self) -> Self::Output { + fn into_squeezer(mut self) -> Self::Squeezer { self.bind_zero_length(); - self.cshake.into_output() + self.cshake.into_squeezer() } - fn into_output_partial_bits( + fn into_squeezer_partial_bits( self, _partial_byte: u8, num_bits: usize, - ) -> Result { + ) -> Result { if num_bits != 0 { return Err(HashError::InvalidLength( "KMACXOF cannot take a partial final byte: right_encode(0) must follow the message", )); } - Ok(self.into_output()) + Ok(self.into_squeezer()) } } diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 5cfc112f..8d1b99c3 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -68,30 +68,30 @@ //! use bouncycastle_sha3 as sha3; //! //! let data: &[u8] = b"Hello, world!"; -//! let output_16byte: Vec = sha3::SHAKE128::new().hash_xof(data, 16); -//! let output_16KiB: Vec = sha3::SHAKE128::new().hash_xof(data, 16 * 1024); +//! let output_16byte: Vec = sha3::SHAKE128::new().xof(data, 16); +//! let output_16KiB: Vec = sha3::SHAKE128::new().xof(data, 16 * 1024); //! ``` //! //! [`XOF`] extends [`Hash`], so SHAKE takes input through [`Hash::do_update`] like any other hash. -//! Output is where they differ: [`XOF::into_output`] ends the input phase and returns an -//! [`XOFOutput`](bouncycastle_core::traits::XOFOutput), whose -//! [`do_output`](bouncycastle_core::traits::XOFOutput::do_output) can be called as many times as you +//! Output is where they differ: [`XOF::into_squeezer`] ends the input phase and returns an +//! [`XOFSqueezer`](bouncycastle_core::traits::XOFSqueezer), whose +//! [`do_output`](bouncycastle_core::traits::XOFSqueezer::do_output) can be called as many times as you //! like, each call continuing one stream. //! -//! Absorbing after output has begun is not an error you can make: `into_output` consumes the +//! Absorbing after output has begun is not an error you can make: `into_squeezer` consumes the //! SHAKE, so there is no value left to call [`Hash::do_update`] on. //! //! The following code produces the same output as the previous example: //!``` -//! use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; +//! use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; //! use bouncycastle_sha3 as sha3; //! //! let data: &[u8] = b"Hello, world!"; //! let mut shake = sha3::SHAKE128::new(); //! shake.do_update(data); -//! let output_16byte: Vec = shake.into_output().do_output(16); +//! let output_16byte: Vec = shake.into_squeezer().do_output(16); //! -//! let mut shake = sha3::SHAKE128::new().into_output(); +//! let mut shake = sha3::SHAKE128::new().into_squeezer(); //! let mut output_16KiB: Vec = vec![]; //! for i in 0..16 { output_16KiB.extend_from_slice(&shake.do_output(1024)) } //! ``` @@ -317,7 +317,7 @@ pub type PARALLELHASH256 = ParallelHashInternal; pub type PARALLELHASHXOF128 = ParallelHashXOFInternal; /// ParallelHashXOF256: see [`PARALLELHASHXOF128`]. pub type PARALLELHASHXOF256 = ParallelHashXOFInternal; -pub use shake::{SHAKEInternal, SHAKEOutput}; +pub use shake::{SHAKEInternal, SHAKESqueezer}; pub use keccak::SUSPENDED_SHA3_STATE_LEN; diff --git a/crypto/sha3/src/parallelhash.rs b/crypto/sha3/src/parallelhash.rs index af8493e3..5d99f401 100644 --- a/crypto/sha3/src/parallelhash.rs +++ b/crypto/sha3/src/parallelhash.rs @@ -2,10 +2,10 @@ use crate::SHAKEParams; use crate::cshake::{CSHAKEInternal, absorb_left_encode_into}; -use crate::shake::{SHAKEInternal, SHAKEOutput}; +use crate::shake::{SHAKEInternal, SHAKESqueezer}; use crate::xof_utils::right_encode; use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFSqueezer}; /// The function-name string every ParallelHash binds, per SP 800-185 Sec 6.3. const PARALLELHASH_FUNCTION_NAME: &[u8] = b"ParallelHash"; @@ -40,7 +40,7 @@ impl ParallelState { /// The inner call is `cSHAKE(block, 2c, "", "")`, which by Sec 3.3 step 1 is plain SHAKE -- /// so SHAKE is what is used here. fn absorb_block(&mut self, block: &[u8]) { - let inner = SHAKEInternal::::new().hash_xof(block, Self::INNER_LEN); + let inner = SHAKEInternal::::new().xof(block, Self::INNER_LEN); self.cshake.do_update(&inner); self.blocks += 1; } @@ -149,7 +149,7 @@ impl Hash for ParallelHashInternal { fn do_final(self) -> Vec { let n = self.output_len; - self.state.finish((n as u64) * 8).into_output().do_output(n) + self.state.finish((n as u64) * 8).into_squeezer().do_output(n) } fn do_final_out(self, output: &mut [u8]) -> usize { @@ -160,7 +160,7 @@ impl Hash for ParallelHashInternal { // truncated read is this ParallelHash cut short, not the ParallelHash of a shorter length. let written = n.min(output.len()); output[written..].fill(0); - self.state.finish((n as u64) * 8).into_output().do_output_out(&mut output[..written]) + self.state.finish((n as u64) * 8).into_squeezer().do_output_out(&mut output[..written]) } /// # Errors @@ -234,12 +234,12 @@ impl Hash for ParallelHashXOFInternal { fn hash(mut self, data: &[u8]) -> Vec { let n = self.output_len(); self.do_update(data); - self.into_output().do_output(n) + self.into_squeezer().do_output(n) } fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { self.do_update(data); - self.into_output().do_output_out(output) + self.into_squeezer().do_output_out(output) } fn do_update(&mut self, data: &[u8]) { @@ -248,11 +248,11 @@ impl Hash for ParallelHashXOFInternal { fn do_final(self) -> Vec { let n = self.output_len(); - self.into_output().do_output(n) + self.into_squeezer().do_output(n) } fn do_final_out(self, output: &mut [u8]) -> usize { - self.into_output().do_output_out(output) + self.into_squeezer().do_output_out(output) } /// # Errors @@ -288,23 +288,23 @@ impl Hash for ParallelHashXOFInternal { } impl XOF for ParallelHashXOFInternal { - type Output = SHAKEOutput; + type Squeezer = SHAKESqueezer; - fn into_output(self) -> Self::Output { + fn into_squeezer(self) -> Self::Squeezer { // Sec 6.3.1 step 4: right_encode(0) rather than the length. - self.state.finish(0).into_output() + self.state.finish(0).into_squeezer() } - fn into_output_partial_bits( + fn into_squeezer_partial_bits( self, _partial_byte: u8, num_bits: usize, - ) -> Result { + ) -> Result { if num_bits != 0 { return Err(HashError::InvalidLength( "ParallelHashXOF cannot take a partial final byte: the encodings must follow", )); } - Ok(self.into_output()) + Ok(self.into_squeezer()) } } diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index 9339ed73..834cd9fd 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -8,7 +8,7 @@ use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_core::traits::{ - Algorithm, Hash, KDF, SecurityStrength, Suspendable, XOF, XOFOutput, + Algorithm, Hash, KDF, SecurityStrength, Suspendable, XOF, XOFSqueezer, }; use bouncycastle_utils::{max, min}; @@ -57,12 +57,12 @@ impl SHAKEInternal { fn hash_internal(mut self, data: &[u8], result_len: usize) -> Vec { self.keccak.absorb(data); - self.into_output().do_output(result_len) + self.into_squeezer().do_output(result_len) } fn hash_internal_out(mut self, data: &[u8], output: &mut [u8]) -> usize { self.keccak.absorb(data); - self.into_output().do_output_out(output) + self.into_squeezer().do_output_out(output) } /// Ends absorbing with a caller-chosen domain separator and returns the squeezing half. @@ -73,19 +73,19 @@ impl SHAKEInternal { /// /// Infallible for the same reason [`Hash::do_update`] is: a `SHAKEInternal` a caller can name /// has never squeezed, so the queue is byte-aligned and `absorb_bits` cannot reject it. - pub(crate) fn into_output_with_suffix( + pub(crate) fn into_squeezer_with_suffix( mut self, suffix: u8, num_bits: usize, - ) -> SHAKEOutput { + ) -> SHAKESqueezer { self.keccak .absorb_bits(suffix, num_bits) .expect("a sponge that has not squeezed can absorb a domain separator"); - SHAKEOutput { shake: self } + SHAKESqueezer { shake: self } } /// Produces the next bytes of the output stream, applying the SHAKE "1111" domain separator - /// (FIPS 202 s. 6.2) on the first call. Reached only through [`SHAKEOutput`], so the caller + /// (FIPS 202 s. 6.2) on the first call. Reached only through [`SHAKESqueezer`], so the caller /// cannot interleave this with absorbing. fn squeeze_internal_out(&mut self, output: &mut [u8]) -> usize { output.fill(0); @@ -207,7 +207,7 @@ impl Suspendable for SHAKEInterna // A SHAKEInternal accepts input, so it must never be rebuilt in the squeezing phase -- // that is the invariant `Hash::do_update` relies on. A suspended squeezing sponge is a - // SHAKEOutput; resume it as one. + // SHAKESqueezer; resume it as one. if keccak.squeezing { // InvalidData rather than a new variant: for this type the phase byte is simply wrong. return Err(SuspendableError::InvalidData); @@ -296,16 +296,16 @@ impl Default for SHAKEInternal { } } -/// The squeezing half of SHAKE: what [`XOF::into_output`] hands back. +/// The squeezing half of SHAKE: what [`XOF::into_squeezer`] hands back. /// /// It owns the sponge, so the absorbing value is gone by the time this exists. That is the whole /// point: [`Hash::do_update`] cannot be called on a SHAKE that has begun producing output, because /// there is no longer a SHAKE to call it on. -pub struct SHAKEOutput { +pub struct SHAKESqueezer { shake: SHAKEInternal, } -impl XOFOutput for SHAKEOutput { +impl XOFSqueezer for SHAKESqueezer { fn do_output(&mut self, num_bytes: usize) -> Vec { let mut out = vec![0u8; num_bytes]; self.do_output_out(&mut out); @@ -317,7 +317,7 @@ impl XOFOutput for SHAKEOutput { } } -impl Clone for SHAKEOutput { +impl Clone for SHAKESqueezer { fn clone(&self) -> Self { Self { shake: self.shake.clone() } } @@ -327,7 +327,7 @@ impl Clone for SHAKEOutput { /// stream can be paused. The serialized form is the same one [`SHAKEInternal`] writes -- the /// keccak state records which phase it is in -- so the two `from_suspended` implementations /// accept exactly the states the other rejects. -impl Suspendable for SHAKEOutput { +impl Suspendable for SHAKESqueezer { fn suspend(self) -> [u8; SUSPENDED_SHA3_STATE_LEN] { self.shake.suspend() } @@ -389,7 +389,7 @@ impl Hash for SHAKEInternal { /// /// Absorbing after squeezing has begun would be wrong -- FIPS 202 defines SHAKE as a single /// function of the whole message, so re-absorbing would be an unapproved duplex -- and it cannot - /// be expressed: producing output goes through [`XOF::into_output`], which consumes the value, + /// be expressed: producing output goes through [`XOF::into_squeezer`], which consumes the value, /// and every `KDF` entry point takes `self` by value too. A `SHAKEInternal` a caller can still /// name has therefore never squeezed. fn do_update(&mut self, data: &[u8]) { @@ -408,7 +408,7 @@ impl Hash for SHAKEInternal { } fn do_final_out(self, output: &mut [u8]) -> usize { - self.into_output().do_output_out(output) + self.into_squeezer().do_output_out(output) } fn do_final_partial_bits( @@ -428,7 +428,7 @@ impl Hash for SHAKEInternal { output: &mut [u8], ) -> Result { // Validated before anything is written, so a rejected call leaves `output` untouched. - Ok(self.into_output_partial_bits(partial_byte, num_bits)?.do_output_out(output)) + Ok(self.into_squeezer_partial_bits(partial_byte, num_bits)?.do_output_out(output)) } fn max_security_strength(&self) -> SecurityStrength { @@ -439,67 +439,67 @@ impl Hash for SHAKEInternal { /// The absorb-then-squeeze rule, as a compile error rather than a runtime one. /// /// ```compile_fail -/// use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; +/// use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; /// use bouncycastle_sha3::SHAKE128; /// /// let mut shake = SHAKE128::new(); /// shake.do_update(b"abc"); -/// let mut out = shake.into_output(); +/// let mut out = shake.into_squeezer(); /// let _ = out.do_output(32); -/// shake.do_update(b"more"); // `shake` was moved by into_output() +/// shake.do_update(b"more"); // `shake` was moved by into_squeezer() /// ``` /// /// The same value used correctly: /// /// ``` -/// use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; +/// use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; /// use bouncycastle_sha3::SHAKE128; /// /// let mut shake = SHAKE128::new(); /// shake.do_update(b"abc"); -/// let mut out = shake.into_output(); +/// let mut out = shake.into_squeezer(); /// assert_eq!(out.do_output(32).len(), 32); /// ``` impl XOF for SHAKEInternal { - type Output = SHAKEOutput; + type Squeezer = SHAKESqueezer; - fn into_output(self) -> Self::Output { + fn into_squeezer(self) -> Self::Squeezer { // The SHAKE domain separator, "1111" (FIPS 202 s. 6.2). - self.into_output_with_suffix(0x0F, 4) + self.into_squeezer_with_suffix(0x0F, 4) } - fn into_output_partial_bits( + fn into_squeezer_partial_bits( self, partial_byte: u8, num_bits: usize, - ) -> Result { + ) -> Result { // The SHAKE domain separator, "1111" (FIPS 202 s. 6.2). - self.into_output_partial_bits_with_suffix(partial_byte, num_bits, 0x0F, 4) + self.into_squeezer_partial_bits_with_suffix(partial_byte, num_bits, 0x0F, 4) } - fn hash_xof(self, data: &[u8], result_len: usize) -> Vec { + fn xof(self, data: &[u8], result_len: usize) -> Vec { self.hash_internal(data, result_len) } - fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { + fn xof_out(self, data: &[u8], output: &mut [u8]) -> usize { // hash_internal_out zeroizes `output` before writing. self.hash_internal_out(data, output) } } impl SHAKEInternal { - /// [`XOF::into_output_partial_bits`] with a caller-chosen domain separator, for cSHAKE. + /// [`XOF::into_squeezer_partial_bits`] with a caller-chosen domain separator, for cSHAKE. /// /// The message's trailing bits and the separator are absorbed together, so the separator /// cannot simply be applied afterwards -- hence the suffix travels in rather than being - /// hardcoded. See [`Self::into_output_with_suffix`]. - pub(crate) fn into_output_partial_bits_with_suffix( + /// hardcoded. See [`Self::into_squeezer_with_suffix`]. + pub(crate) fn into_squeezer_partial_bits_with_suffix( mut self, partial_byte: u8, num_bits: usize, suffix: u8, suffix_bits: usize, - ) -> Result, HashError> { + ) -> Result, HashError> { // A partial byte has at most 7 bits; 0 means the message ends on a byte boundary. // Checked before any state change, so a rejected call leaves the sponge untouched. if num_bits > 7 { @@ -526,6 +526,6 @@ impl SHAKEInternal { // The suffix is already folded into final_input above, so the sponge is finished // absorbing; wrap it without applying the suffix a second time. - Ok(SHAKEOutput { shake: self }) + Ok(SHAKESqueezer { shake: self }) } } diff --git a/crypto/sha3/src/tuplehash.rs b/crypto/sha3/src/tuplehash.rs index 5b6a5704..1eb3be28 100644 --- a/crypto/sha3/src/tuplehash.rs +++ b/crypto/sha3/src/tuplehash.rs @@ -2,10 +2,10 @@ use crate::SHAKEParams; use crate::cshake::{CSHAKEInternal, absorb_encoded_string_into}; -use crate::shake::SHAKEOutput; +use crate::shake::SHAKESqueezer; use crate::xof_utils::right_encode; use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFSqueezer}; /// The function-name string every TupleHash binds, per SP 800-185 Sec 5.3. const TUPLEHASH_FUNCTION_NAME: &[u8] = b"TupleHash"; @@ -90,7 +90,7 @@ impl Hash for TupleHashInternal { let n = self.output_len; let (buf, len) = right_encode((n as u64) * 8); self.cshake.do_update(&buf[..len]); - self.cshake.into_output().do_output(n) + self.cshake.into_squeezer().do_output(n) } fn do_final_out(mut self, output: &mut [u8]) -> usize { @@ -103,7 +103,7 @@ impl Hash for TupleHashInternal { // truncated read is this TupleHash cut short, not the TupleHash of a shorter length. let written = n.min(output.len()); output[written..].fill(0); - self.cshake.into_output().do_output_out(&mut output[..written]) + self.cshake.into_squeezer().do_output_out(&mut output[..written]) } /// # Errors @@ -163,11 +163,11 @@ impl TupleHashXOFInternal { } /// Hashes a whole tuple and returns the output stream. - pub fn output_for(mut self, tuple: &[&[u8]]) -> SHAKEOutput { + pub fn output_for(mut self, tuple: &[&[u8]]) -> SHAKESqueezer { for element in tuple { self.do_update(element); } - self.into_output() + self.into_squeezer() } } @@ -185,12 +185,12 @@ impl Hash for TupleHashXOFInternal { fn hash(mut self, data: &[u8]) -> Vec { let n = self.output_len(); self.do_update(data); - self.into_output().do_output(n) + self.into_squeezer().do_output(n) } fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { self.do_update(data); - self.into_output().do_output_out(output) + self.into_squeezer().do_output_out(output) } /// Appends **one tuple element**. @@ -200,11 +200,11 @@ impl Hash for TupleHashXOFInternal { fn do_final(self) -> Vec { let n = self.output_len(); - self.into_output().do_output(n) + self.into_squeezer().do_output(n) } fn do_final_out(self, output: &mut [u8]) -> usize { - self.into_output().do_output_out(output) + self.into_squeezer().do_output_out(output) } /// # Errors @@ -240,25 +240,25 @@ impl Hash for TupleHashXOFInternal { } impl XOF for TupleHashXOFInternal { - type Output = SHAKEOutput; + type Squeezer = SHAKESqueezer; - fn into_output(mut self) -> Self::Output { + fn into_squeezer(mut self) -> Self::Squeezer { // Sec 5.3.1 step 4: right_encode(0) rather than the length. let (buf, len) = right_encode(0); self.cshake.do_update(&buf[..len]); - self.cshake.into_output() + self.cshake.into_squeezer() } - fn into_output_partial_bits( + fn into_squeezer_partial_bits( self, _partial_byte: u8, num_bits: usize, - ) -> Result { + ) -> Result { if num_bits != 0 { return Err(HashError::InvalidLength( "TupleHashXOF cannot take a partial final byte: right_encode(0) must follow", )); } - Ok(self.into_output()) + Ok(self.into_squeezer()) } } diff --git a/crypto/sha3/tests/bc-test-data.rs b/crypto/sha3/tests/bc-test-data.rs index b3e312b2..071e1b46 100644 --- a/crypto/sha3/tests/bc-test-data.rs +++ b/crypto/sha3/tests/bc-test-data.rs @@ -25,7 +25,7 @@ //! `Outputlen = minoutbytes + (rightmost 16 bits of Output as big-endian integer) mod //! (maxoutbytes - minoutbytes + 1)` bytes; report `Output`/`Outputlen` per COUNT. -use bouncycastle_core::traits::{Hash, XOF, XOFOutput}; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle_hex as hex; use bouncycastle_sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256}; use std::fs; @@ -170,9 +170,10 @@ fn shake_bits(msg: &[u8], len_bits: usize, out_bits: usize) -> let (whole, partial) = (len_bits / 8, len_bits % 8); x.do_update(&msg[..whole]); let mut out_stream = if partial != 0 { - x.into_output_partial_bits(msg[whole].reverse_bits(), partial).expect("partial is in 1..=7") + x.into_squeezer_partial_bits(msg[whole].reverse_bits(), partial) + .expect("partial is in 1..=7") } else { - x.into_output() + x.into_squeezer() }; let (out_whole, out_partial) = (out_bits / 8, out_bits % 8); let mut out = out_stream.do_output(out_whole + usize::from(out_partial != 0)); @@ -291,7 +292,7 @@ fn run_shake_monte_file(orientation: &str, filename: &str) { let n = output.len().min(16); m[..n].copy_from_slice(&output[..n]); // Output = SHAKE(Msg, Outputlen) - output = X::default().hash_xof(&m, out_bytes); + output = X::default().xof(&m, out_bytes); // Rightmost_Output_bits = rightmost 16 bits of Output (big-endian integer) let l = output.len(); let rightmost = u16::from_be_bytes([output[l - 2], output[l - 1]]) as usize; diff --git a/crypto/sha3/tests/cshake_tests.rs b/crypto/sha3/tests/cshake_tests.rs index ecaa70c6..875e36dd 100644 --- a/crypto/sha3/tests/cshake_tests.rs +++ b/crypto/sha3/tests/cshake_tests.rs @@ -4,7 +4,7 @@ //! `../bc-test-data` (the same convention as the ML-KEM, ML-DSA and SHA-3 suites). If it is not //! present these tests print a warning and pass vacuously. -use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_hex as hex; use bouncycastle_sha3::{CSHAKE128, CSHAKE256, SHAKE128, SHAKE256}; @@ -84,12 +84,12 @@ fn nist_sp800_185_sample_values() { 128 => { let mut c = CSHAKE128::new(v.n.as_bytes(), v.s.as_bytes()); c.do_update(&v.msg); - c.into_output().do_output(want) + c.into_squeezer().do_output(want) } 256 => { let mut c = CSHAKE256::new(v.n.as_bytes(), v.s.as_bytes()); c.do_update(&v.msg); - c.into_output().do_output(want) + c.into_squeezer().do_output(want) } other => panic!("COUNT {i}: unexpected strength {other}"), }; @@ -110,13 +110,13 @@ fn empty_name_and_customization_is_plain_shake() { for msg in [b"".as_slice(), b"abc", &[0u8; 200], b"Hello, world!"] { for len in [1usize, 16, 32, 168, 200] { assert_eq!( - CSHAKE128::new(b"", b"").hash_xof(msg, len), - SHAKE128::new().hash_xof(msg, len), + CSHAKE128::new(b"", b"").xof(msg, len), + SHAKE128::new().xof(msg, len), "cSHAKE128 with no N or S must equal SHAKE128 / len {len}" ); assert_eq!( - CSHAKE256::new(b"", b"").hash_xof(msg, len), - SHAKE256::new().hash_xof(msg, len), + CSHAKE256::new(b"", b"").xof(msg, len), + SHAKE256::new().xof(msg, len), "cSHAKE256 with no N or S must equal SHAKE256 / len {len}" ); } @@ -128,10 +128,10 @@ fn empty_name_and_customization_is_plain_shake() { #[test] fn customization_separates_the_functions() { let msg = b"the same message"; - let plain = SHAKE128::new().hash_xof(msg, 32); - let email = CSHAKE128::new(b"", b"Email Signature").hash_xof(msg, 32); - let finger = CSHAKE128::new(b"", b"key fingerprint").hash_xof(msg, 32); - let named = CSHAKE128::new(b"KMAC", b"").hash_xof(msg, 32); + let plain = SHAKE128::new().xof(msg, 32); + let email = CSHAKE128::new(b"", b"Email Signature").xof(msg, 32); + let finger = CSHAKE128::new(b"", b"key fingerprint").xof(msg, 32); + let named = CSHAKE128::new(b"KMAC", b"").xof(msg, 32); assert_ne!(plain, email, "a customized cSHAKE must differ from SHAKE"); assert_ne!(email, finger, "different S must give unrelated output"); @@ -146,8 +146,8 @@ fn customization_separates_the_functions() { fn the_boundary_between_n_and_s_is_unambiguous() { let msg = b"x"; assert_ne!( - CSHAKE128::new(b"AB", b"").hash_xof(msg, 32), - CSHAKE128::new(b"A", b"B").hash_xof(msg, 32), + CSHAKE128::new(b"AB", b"").xof(msg, 32), + CSHAKE128::new(b"A", b"B").xof(msg, 32), "the split between N and S must be part of the computation" ); } @@ -156,13 +156,13 @@ fn the_boundary_between_n_and_s_is_unambiguous() { #[test] fn streaming_matches_one_shot() { let msg: Vec = (0..=255u8).collect(); - let one = CSHAKE128::new(b"", b"Email Signature").hash_xof(&msg, 64); + let one = CSHAKE128::new(b"", b"Email Signature").xof(&msg, 64); let mut c = CSHAKE128::new(b"", b"Email Signature"); for chunk in msg.chunks(7) { c.do_update(chunk); } - let mut out = c.into_output(); + let mut out = c.into_squeezer(); let head = out.do_output(20); let tail = out.do_output(44); assert_eq!([head, tail].concat(), one, "chunked in, split out, must equal the one-shot"); @@ -177,7 +177,7 @@ fn cshake_is_a_hash() { assert_eq!(digest.len(), 32, "cSHAKE128's nominal output length"); assert_eq!(CSHAKE128::new(b"", b"Email Signature").hash(b"abc"), digest); - let long = CSHAKE128::new(b"", b"Email Signature").hash_xof(b"abc", 64); + let long = CSHAKE128::new(b"", b"Email Signature").xof(b"abc", 64); assert_eq!(&long[..32], &digest[..], "do_final must be a prefix of the longer output"); let mut c = CSHAKE256::new(b"", b"Email Signature"); diff --git a/crypto/sha3/tests/kmac_tests.rs b/crypto/sha3/tests/kmac_tests.rs index 37e4020f..efee7ce2 100644 --- a/crypto/sha3/tests/kmac_tests.rs +++ b/crypto/sha3/tests/kmac_tests.rs @@ -110,12 +110,12 @@ fn nist_sp800_185_kmacxof_sample_values() { let key = key_material(&v.key); let got = match v.strength { - 128 => KMACXOF128::new(&key, v.s.as_bytes(), false) - .expect("a valid key") - .hash_xof(&v.msg, want), - 256 => KMACXOF256::new(&key, v.s.as_bytes(), false) - .expect("a valid key") - .hash_xof(&v.msg, want), + 128 => { + KMACXOF128::new(&key, v.s.as_bytes(), false).expect("a valid key").xof(&v.msg, want) + } + 256 => { + KMACXOF256::new(&key, v.s.as_bytes(), false).expect("a valid key").xof(&v.msg, want) + } other => panic!("COUNT {i}: unexpected strength {other}"), }; assert_eq!(got, v.output, "COUNT {i}: KMACXOF{} S={:?}", v.strength, v.s); @@ -126,7 +126,7 @@ fn nist_sp800_185_kmacxof_sample_values() { /// Sec 4.3.1 versus Sec 4.3: with identical key, message, customization *and* length, KMAC and /// KMACXOF are different functions, because one binds `right_encode(L)` and the other /// `right_encode(0)`. The published samples use the same inputs for both, so this is checkable -/// directly against them -- and it is the property that would break if `into_output` bound the +/// directly against them -- and it is the property that would break if `into_squeezer` bound the /// length by mistake. #[test] fn kmacxof_is_not_kmac_truncated() { @@ -239,9 +239,9 @@ fn algorithm_names() { #[test] fn kmacxof_output_is_one_stream() { let key = key_material(&[0x42u8; 32]); - let long = KMACXOF128::new(&key, b"", false).unwrap().hash_xof(b"abc", 64); + let long = KMACXOF128::new(&key, b"", false).unwrap().xof(b"abc", 64); - let short = KMACXOF128::new(&key, b"", false).unwrap().hash_xof(b"abc", 16); + let short = KMACXOF128::new(&key, b"", false).unwrap().xof(b"abc", 16); assert_eq!(&long[..16], &short[..], "KMACXOF at a shorter length must be a prefix"); let mut k = KMACXOF128::new(&key, b"", false).unwrap(); @@ -259,14 +259,14 @@ fn kmacxof_rejects_a_partial_final_byte() { let mut k = KMACXOF128::new(&key, b"", false).unwrap(); k.do_update(b"abc"); assert!(matches!( - k.into_output_partial_bits(0xF0, 4), + k.into_squeezer_partial_bits(0xF0, 4), Err(bouncycastle_core::errors::HashError::InvalidLength(_)) )); // ... but zero bits means the message ended on a byte boundary, which is fine. let mut k = KMACXOF128::new(&key, b"", false).unwrap(); k.do_update(b"abc"); - assert!(k.into_output_partial_bits(0, 0).is_ok()); + assert!(k.into_squeezer_partial_bits(0, 0).is_ok()); } #[test] @@ -411,7 +411,7 @@ fn key_type_is_checked() { /// The `Hash` view of the partial-byte entry points on KMACXOF: zero bits is the byte-aligned case /// and yields the same bytes as `do_final`; anything else is refused. The test above only covers -/// the `XOF` entry point, `into_output_partial_bits`. +/// the `XOF` entry point, `into_squeezer_partial_bits`. #[test] fn kmacxof_hash_view_partial_bits() { let key = key_material(&[0x42u8; 32]); diff --git a/crypto/sha3/tests/parallelhash_tests.rs b/crypto/sha3/tests/parallelhash_tests.rs index de0de3b4..50974408 100644 --- a/crypto/sha3/tests/parallelhash_tests.rs +++ b/crypto/sha3/tests/parallelhash_tests.rs @@ -96,8 +96,8 @@ fn nist_sp800_185_parallelhashxof_sample_values() { for (i, v) in vectors.iter().enumerate() { let want = v.output_len / 8; let got = match v.strength { - 128 => PARALLELHASHXOF128::new(v.block_size, v.s.as_bytes()).hash_xof(&v.msg, want), - 256 => PARALLELHASHXOF256::new(v.block_size, v.s.as_bytes()).hash_xof(&v.msg, want), + 128 => PARALLELHASHXOF128::new(v.block_size, v.s.as_bytes()).xof(&v.msg, want), + 256 => PARALLELHASHXOF256::new(v.block_size, v.s.as_bytes()).xof(&v.msg, want), other => panic!("COUNT {i}: unexpected strength {other}"), }; assert_eq!( @@ -188,8 +188,8 @@ fn length_binding_differs_between_the_two() { let long = PARALLELHASH128::new(4, b"", 32).hash(msg); assert_ne!(&long[..16], &short[..], "ParallelHash: a different length is a different function"); - let short = PARALLELHASHXOF128::new(4, b"").hash_xof(msg, 16); - let long = PARALLELHASHXOF128::new(4, b"").hash_xof(msg, 32); + let short = PARALLELHASHXOF128::new(4, b"").xof(msg, 16); + let long = PARALLELHASHXOF128::new(4, b"").xof(msg, 32); assert_eq!(&long[..16], &short[..], "ParallelHashXOF: one stream, so shorter is a prefix"); } @@ -202,7 +202,7 @@ fn partial_final_byte_is_refused() { let mut p = PARALLELHASHXOF128::new(8, b""); p.do_update(b"abc"); - assert!(matches!(p.into_output_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); + assert!(matches!(p.into_squeezer_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); } /// Sec 6.2 forbids a zero block size. @@ -305,11 +305,11 @@ fn check_xof_view(make: impl Fn() -> X, msg: &[u8], expected: &[u8], ctx Err(HashError::InvalidLength(_)) )); - assert_eq!(make().hash_xof(msg, n / 2), &expected[..n / 2], "{ctx}: hash_xof, shorter"); + assert_eq!(make().xof(msg, n / 2), &expected[..n / 2], "{ctx}: xof, shorter"); let mut out = vec![0u8; n]; - assert_eq!(make().hash_xof_out(msg, &mut out), n, "{ctx}: hash_xof_out returns the length"); - assert_eq!(out, expected, "{ctx}: hash_xof_out"); + assert_eq!(make().xof_out(msg, &mut out), n, "{ctx}: xof_out returns the length"); + assert_eq!(out, expected, "{ctx}: xof_out"); } #[test] diff --git a/crypto/sha3/tests/shake_tests.rs b/crypto/sha3/tests/shake_tests.rs index 226adbdb..1147467c 100644 --- a/crypto/sha3/tests/shake_tests.rs +++ b/crypto/sha3/tests/shake_tests.rs @@ -7,7 +7,7 @@ mod shake_tests { use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterial256, KeyMaterial512, KeyMaterialTrait, KeyType, }; - use bouncycastle_core::traits::{Hash, KDF, SecurityStrength, XOF, XOFOutput}; + use bouncycastle_core::traits::{Hash, KDF, SecurityStrength, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::DUMMY_SEED; use bouncycastle_core_test_framework::kdf::TestFrameworkKDF; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; @@ -19,9 +19,9 @@ mod shake_tests { /// packing: message bits 0001 in the low nibble, first bit in the LSB), i.e. 0x10 in the API's /// MSB-first order. #[test] - fn into_output_partial_bits_four_bits() { + fn into_squeezer_partial_bits_four_bits() { let shake = SHAKE128::new(); - let mut out = shake.into_output_partial_bits(0x10, 4).unwrap(); + let mut out = shake.into_squeezer_partial_bits(0x10, 4).unwrap(); assert_eq!( out.do_output(16), bouncycastle_hex::decode("d40238024b040a954d9c2c89daf480e5").unwrap(), @@ -29,16 +29,16 @@ mod shake_tests { ); } - /// into_output_partial_bits() must validate num_bits before shifting: 0 is allowed + /// into_squeezer_partial_bits() must validate num_bits before shifting: 0 is allowed /// (finalize with no partial byte), 8+ is rejected with InvalidLength rather than panicking. #[test] - fn into_output_partial_bits_validates_range() { + fn into_squeezer_partial_bits_validates_range() { for bad in [8usize, 9, 15, 16, 64, usize::MAX] { let mut shake = SHAKE128::new(); shake.do_update(b"abc"); assert!( matches!( - shake.into_output_partial_bits(0xFF, bad), + shake.into_squeezer_partial_bits(0xFF, bad), Err(HashError::InvalidLength(_)) ), "num_bits={bad}" @@ -46,15 +46,15 @@ mod shake_tests { } let mut a = SHAKE128::new(); a.do_update(b"abc"); - let mut a = a.into_output_partial_bits(0xFF, 0).unwrap(); - assert_eq!(a.do_output(32), SHAKE128::new().hash_xof(b"abc", 32)); + let mut a = a.into_squeezer_partial_bits(0xFF, 0).unwrap(); + assert_eq!(a.do_output(32), SHAKE128::new().xof(b"abc", 32)); // Upper boundary: 7 bits is the largest valid partial byte and must be accepted, and must // actually change the output relative to the byte-aligned message. let mut b = SHAKE128::new(); b.do_update(b"abc"); - let mut b = b.into_output_partial_bits(0xFE, 7).unwrap(); - assert_ne!(b.do_output(32), SHAKE128::new().hash_xof(b"abc", 32)); + let mut b = b.into_squeezer_partial_bits(0xFE, 7).unwrap(); + assert_ne!(b.do_output(32), SHAKE128::new().xof(b"abc", 32)); } /// The two `Hash` metadata methods, pinned to their actual values. @@ -271,11 +271,11 @@ mod shake_tests { // A helper that exercises the full round-trip for one SHAKE variant. // Each phase suspends as its own type: an absorbing state resumes as `X`, a squeezing one - // as `X::Output`, and each rejects the other's phase. + // as `X::Squeezer`, and each rejects the other's phase. fn round_trip(mut shake: X, input: &[u8]) where X: XOF + Suspendable + Clone, - X::Output: Suspendable + Clone, + X::Squeezer: Suspendable + Clone, { shake.do_update(input); @@ -285,13 +285,13 @@ mod shake_tests { // Test #1 // serialize the in-progress (absorbing) state, then read from the original and compare let absorbing_state = shake.clone().suspend(); - let mut out = shake.into_output(); + let mut out = shake.into_squeezer(); let expected = out.do_output(64); // rebuild from the serialized state and confirm it produces the same output let from_state = X::from_suspended(absorbing_state).expect("an absorbing state resumes as the XOF"); - assert_eq!(expected, from_state.into_output().do_output(64)); + assert_eq!(expected, from_state.into_squeezer().do_output(64)); // Test #2 // serialize the in-progress (squeezing) state, then read more from the original and compare @@ -299,7 +299,7 @@ mod shake_tests { let expected = out.do_output(64); // rebuild from the serialized state and confirm it produces the same output - let mut from_state = X::Output::from_suspended(squeezing_state) + let mut from_state = X::Squeezer::from_suspended(squeezing_state) .expect("a squeezing state resumes as the output"); assert_eq!(expected, from_state.do_output(64)); @@ -310,7 +310,7 @@ mod shake_tests { ); assert!( matches!( - X::Output::from_suspended(absorbing_state), + X::Squeezer::from_suspended(absorbing_state), Err(SuspendableError::InvalidData) ), "an absorbing state must not resume as an output" @@ -321,7 +321,7 @@ mod shake_tests { // + bits_in_queue(8) + squeezing(1) let mut busted = squeezing_state; busted[3 + 1 + 400] = 42; - match X::Output::from_suspended(busted) { + match X::Squeezer::from_suspended(busted) { Err(SuspendableError::InvalidData) => { /* good */ } _ => panic!("Expected an error for a corrupt squeezing byte"), } @@ -370,12 +370,12 @@ mod shake_tests { if partial_bits == 0 { shake.do_update(tc.msg.as_slice()); - let mut shake = shake.into_output(); + let mut shake = shake.into_squeezer(); output = shake.do_output(tc.output.len()); } else { shake.do_update(&tc.msg[..(tc.msg.len() - 1)]); let mut shake = shake - .into_output_partial_bits(tc.msg[tc.msg.len() - 1], partial_bits) + .into_squeezer_partial_bits(tc.msg[tc.msg.len() - 1], partial_bits) .expect("partial_bits is in 1..=7"); output = shake.do_output(tc.output.len()); } diff --git a/crypto/sha3/tests/tuplehash_tests.rs b/crypto/sha3/tests/tuplehash_tests.rs index f285258c..295fef44 100644 --- a/crypto/sha3/tests/tuplehash_tests.rs +++ b/crypto/sha3/tests/tuplehash_tests.rs @@ -3,7 +3,7 @@ //! Vectors come from the `bc-test-data` repo cloned alongside this one; see `cshake_tests.rs`. use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFOutput}; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_hex as hex; use bouncycastle_sha3::{TUPLEHASH128, TUPLEHASH256, TUPLEHASHXOF128, TUPLEHASHXOF256}; @@ -198,7 +198,7 @@ fn partial_final_byte_is_refused() { let mut t = TUPLEHASHXOF128::new(b""); t.do_update(b"abc"); - assert!(matches!(t.into_output_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); + assert!(matches!(t.into_squeezer_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); } #[test] @@ -311,17 +311,17 @@ fn check_xof_view(make: impl Fn() -> X, tuple: &[&[u8]], expected: &[u8] let mut x = make(); rest.iter().for_each(|e| x.do_update(e)); - assert_eq!(x.hash_xof(last, n), expected, "{ctx}: hash_xof"); + assert_eq!(x.xof(last, n), expected, "{ctx}: xof"); let mut x = make(); rest.iter().for_each(|e| x.do_update(e)); - assert_eq!(x.hash_xof(last, n / 2), &expected[..n / 2], "{ctx}: hash_xof, shorter"); + assert_eq!(x.xof(last, n / 2), &expected[..n / 2], "{ctx}: xof, shorter"); let mut x = make(); rest.iter().for_each(|e| x.do_update(e)); let mut out = vec![0u8; n]; - assert_eq!(x.hash_xof_out(last, &mut out), n, "{ctx}: hash_xof_out returns the length"); - assert_eq!(out, expected, "{ctx}: hash_xof_out"); + assert_eq!(x.xof_out(last, &mut out), n, "{ctx}: xof_out returns the length"); + assert_eq!(out, expected, "{ctx}: xof_out"); } #[test] diff --git a/mem_usage_benches/src/bench_sha3_mem_usage.rs b/mem_usage_benches/src/bench_sha3_mem_usage.rs index 4e9db510..1235b3b1 100644 --- a/mem_usage_benches/src/bench_sha3_mem_usage.rs +++ b/mem_usage_benches/src/bench_sha3_mem_usage.rs @@ -25,7 +25,7 @@ #![allow(dead_code)] #![allow(unused_imports)] -use bouncycastle::core::traits::{Hash, Suspendable, XOF, XOFOutput}; +use bouncycastle::core::traits::{Hash, Suspendable, XOF, XOFSqueezer}; use bouncycastle::sha3::{ SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN, }; @@ -87,7 +87,7 @@ fn bench_shake128_xof() { let mut x = SHAKE128::new(); x.do_update(&MSG); let mut out = [0u8; 512]; - let mut x = x.into_output(); + let mut x = x.into_squeezer(); x.do_output_out(&mut out); println!("{:x?}", out); } @@ -98,7 +98,7 @@ fn bench_shake256_xof() { let mut x = SHAKE256::new(); x.do_update(&MSG); let mut out = [0u8; 512]; - let mut x = x.into_output(); + let mut x = x.into_squeezer(); x.do_output_out(&mut out); println!("{:x?}", out); } From 0a2bb8f719340d40e99d131b0e670a9b5d1f9d6b Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 14 Sep 2026 14:23:11 +1000 Subject: [PATCH 21/22] core, core-test-framework, sha3: a final read of a XOF binds its output length, so XOFSqueezer gains do_final and do_final_out, KMACXOF, TupleHashXOF and ParallelHashXOF defer their right_encode(L) to the first read through a new LengthBoundSqueezer and compute the fixed-length function of SP 800-185 s. 4.3, 5.3 and 6.3 whenever do_final or a one-shot is that read, and the Hash view of every XOF, SHAKE and cSHAKE included, becomes a final read at output_len that zeroes the rest of the caller's buffer --- crypto/core-test-framework/src/xof.rs | 97 ++++++++++-- crypto/core/src/traits.rs | 63 +++++++- crypto/sha3/src/cshake.rs | 34 +++-- crypto/sha3/src/kmac.rs | 52 ++++--- crypto/sha3/src/length_bound_squeezer.rs | 106 +++++++++++++ crypto/sha3/src/lib.rs | 2 + crypto/sha3/src/parallelhash.rs | 59 +++++--- crypto/sha3/src/shake.rs | 39 +++-- crypto/sha3/src/tuplehash.rs | 48 +++--- crypto/sha3/tests/cshake_tests.rs | 29 ++++ crypto/sha3/tests/kmac_tests.rs | 135 +++++++++++++++-- crypto/sha3/tests/parallelhash_tests.rs | 182 ++++++++++++++++++++--- crypto/sha3/tests/shake_tests.rs | 44 ++++++ crypto/sha3/tests/tuplehash_tests.rs | 173 ++++++++++++++++++--- 14 files changed, 914 insertions(+), 149 deletions(-) create mode 100644 crypto/sha3/src/length_bound_squeezer.rs diff --git a/crypto/core-test-framework/src/xof.rs b/crypto/core-test-framework/src/xof.rs index d78af1e6..66348daa 100644 --- a/crypto/core-test-framework/src/xof.rs +++ b/crypto/core-test-framework/src/xof.rs @@ -8,12 +8,17 @@ pub struct TestFrameworkXOF { // Put any config options here /// Can be disabled for XOFs that don't support a partial final byte of input. pub enable_partial_byte_tests: bool, + /// Set for XOFs whose [`XOFSqueezer::do_final`] binds the length it is asked for when it is + /// the first read -- the SP 800-185 forms, which then compute their fixed-length counterpart + /// rather than the XOF stream. The suite cannot know those bytes, so it checks the split + /// instead and leaves the values to the implementation's own vector tests. + pub do_final_binds_output_length: bool, } impl TestFrameworkXOF { /// pub fn new() -> Self { - Self { enable_partial_byte_tests: true } + Self { enable_partial_byte_tests: true, do_final_binds_output_length: false } } /// Exercises the trait against a known input-output pair. @@ -76,17 +81,59 @@ impl TestFrameworkXOF { assert_eq!(n, expected_output.len()); assert_eq!(buf, expected_output, "do_output_out must zeroize before writing"); + /*** fn do_final(self, num_bytes: usize) -> Vec ***/ + // As the first read, do_final is either the end of this stream or -- for a XOF that binds + // the length it is asked for -- a different function altogether. Both are pinned here; the + // second's bytes belong to the implementation's own vector tests. + let mut xof = make(); + xof.do_update(input); + let first_read = xof.into_squeezer().do_final(expected_output.len()); + if self.do_final_binds_output_length { + assert_ne!( + first_read, expected_output, + "a length-binding do_final must not reproduce the XOF stream" + ); + } else { + assert_eq!(first_read, expected_output, "do_final must produce the expected bytes"); + } + + /*** fn do_final_out(self, output: &mut [u8]) -> usize ***/ + // Pre-filled so that the documented zeroization is observable. + let mut buf = vec![0xFFu8; expected_output.len()]; + let mut xof = make(); + xof.do_update(input); + let n = xof.into_squeezer().do_final_out(&mut buf); + assert_eq!(n, expected_output.len(), "do_final_out must report what it wrote"); + assert_eq!(buf, first_read, "do_final_out must agree with do_final"); + + // Once a read has happened there is nothing left to bind, so do_final continues the stream + // that read began rather than restarting it -- however the two behave as a first read. + let mut xof = make(); + xof.do_update(input); + let mut out = xof.into_squeezer(); + let first = out.do_output(split); + assert_eq!( + [first, out.do_final(expected_output.len() - split)].concat(), + expected_output, + "do_final after a read must continue that stream" + ); + /*** fn xof(self, data: &[u8], result_len: usize) -> Vec ***/ + // The one-shots name their length and never come back, so they read as do_final does: for + // a XOF that binds its output length they produce what do_final produced above, not the + // stream. + let one_shot: &[u8] = + if self.do_final_binds_output_length { &first_read } else { expected_output }; assert_eq!( make().xof(input, expected_output.len()), - expected_output, - "the one-shot must equal update-then-output" + one_shot, + "the one-shot must equal update-then-do_final" ); let mut output = vec![0xFFu8; expected_output.len()]; let n = make().xof_out(input, &mut output); assert_eq!(n, expected_output.len()); - assert_eq!(output, expected_output, "xof_out must agree with xof"); + assert_eq!(output, one_shot, "xof_out must agree with xof"); /*** Clone: a XOF mid-absorb can be forked ***/ // The clone continues from the same absorbed prefix and owns its own sponge. @@ -139,7 +186,6 @@ impl TestFrameworkXOF { "block_bitlen must be a whole number of bytes" ); - // do_final is do_output at the nominal length: the same stream, truncated. let mut a = make(); a.do_update(input); let via_hash = a.do_final(); @@ -147,19 +193,38 @@ impl TestFrameworkXOF { let mut b = make(); b.do_update(input); - assert_eq!( - via_hash, - b.into_squeezer().do_output(output_len), - "do_final must equal do_output(output_len)" - ); - - // ... and it is a prefix of the longer output, because a XOF cannot diversify by length. - if expected_output.len() >= output_len { + if self.do_final_binds_output_length { + // The Hash view is a final read at the nominal length, so it binds that length and is + // a different function from the stream -- and must agree with the squeezer's own final + // read at the same length. + assert_ne!( + via_hash, + b.into_squeezer().do_output(output_len), + "a length-binding Hash::do_final must not be the stream truncated" + ); + let mut c = make(); + c.do_update(input); + assert_eq!( + via_hash, + c.into_squeezer().do_final(output_len), + "Hash::do_final must be the squeezer's final read at output_len" + ); + } else { + // do_final is do_output at the nominal length: the same stream, truncated. assert_eq!( - &via_hash[..], - &expected_output[..output_len], - "do_final must be a prefix of the longer output" + via_hash, + b.into_squeezer().do_output(output_len), + "do_final must equal do_output(output_len)" ); + + // ... and a prefix of the longer output, because a XOF cannot diversify by length. + if expected_output.len() >= output_len { + assert_eq!( + &via_hash[..], + &expected_output[..output_len], + "do_final must be a prefix of the longer output" + ); + } } // do_final_out fills the caller's buffer, zeroizing it first. diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 94b7b411..adc702c5 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -1787,10 +1787,28 @@ where /// Output is one continuous stream: successive calls continue where the last left off, so reading /// 16 bytes twice gives the same 32 bytes as reading 32 once. /// -/// There is no `do_final` here, unlike [`Hash`] and [`MAC`]. On those it is load-bearing -- the -/// only way to get output, and it must consume the value because finalizing pads the state. A -/// squeeze has nothing to finalize, so such a method would only say "this read is my last", which -/// ownership already says: drop the value, or let it fall out of scope. +/// [`do_final`](Self::do_final) means something weaker here than on [`Hash`] and [`MAC`]. On those +/// it is load-bearing -- the only way to get output, and it must consume the value because +/// finalizing pads the state. A squeeze has nothing to finalize, so it produces exactly the bytes +/// [`do_output`](Self::do_output) would and differs only in taking ownership: it is how a caller +/// says "this read is my last", and it ends the stream at the point of the call rather than +/// leaving a `mut` binding alive for the rest of the scope. +/// +/// # Being the last read can be an input to the function +/// +/// For SHAKE and cSHAKE the bytes do not depend on how much of the stream is taken, so `do_final` +/// really is just `do_output` plus ownership, which is what the default does. That is not +/// universal. The SP 800-185 functions end their absorbed input with `right_encode(L)`, and their +/// XOF forms (s. 4.3.1, 5.3.1 and 6.3.1) differ from the fixed-length ones only in putting 0 there +/// -- so an implementation can leave `L` unchosen until it knows how the caller intends to read. +/// A `do_final` that is also the *first* read says both how many bytes are wanted and that there +/// will be no more, which is exactly `L`; such an implementation binds it and produces the +/// fixed-length function (KMAC, TupleHash, ParallelHash) rather than a prefix of the XOF stream. +/// +/// After a [`do_output`](Self::do_output) there is nothing left to choose -- `right_encode(0)` is +/// in the sponge and a length bound into a sponge cannot be revised -- so `do_final` then just +/// ends the stream that read began. Implementors that have no such choice to make should keep the +/// default. pub trait XOFSqueezer { /// Produces the next `num_bytes` bytes of the output stream. fn do_output(&mut self, num_bytes: usize) -> Vec; @@ -1798,6 +1816,30 @@ pub trait XOFSqueezer { /// As [`do_output`](Self::do_output), filling the caller's buffer, which is zeroized first. /// Returns the number of bytes written. fn do_output_out(&mut self, output: &mut [u8]) -> usize; + + /// Produces the last `num_bytes` bytes of the output stream and ends the object. + /// + /// Consumes self, so this must be the final call to this object. The default is a plain last + /// read -- the bytes [`do_output`](Self::do_output) would give, continuing from wherever + /// earlier reads left the stream. An implementation with an output length still to bind + /// overrides it to bind `num_bytes` when nothing has been read yet; see the trait docs. + fn do_final(mut self, num_bytes: usize) -> Vec + where + Self: Sized, + { + self.do_output(num_bytes) + } + + /// As [`do_final`](Self::do_final), filling the caller's buffer, which is zeroized first. + /// Returns the number of bytes written. + /// + /// Defaulted as [`do_final`](Self::do_final) is. + fn do_final_out(mut self, output: &mut [u8]) -> usize + where + Self: Sized, + { + self.do_output_out(output) + } } /// Extendable-Output Functions (XOFs): hashes whose output length is chosen by the caller. @@ -1848,25 +1890,30 @@ pub trait XOF: Hash { /// One-shot: absorbs `data` and produces `result_len` bytes. /// - /// The default absorbs and squeezes in the obvious way; override it only where the type can do + /// A one-shot names its length and never comes back, so this is + /// [`XOFSqueezer::do_final`]'s reading of the stream, not + /// [`do_output`](XOFSqueezer::do_output)'s: where an implementation binds the length it is + /// asked for, this binds `result_len`. For SHAKE and cSHAKE the two are the same bytes. + /// + /// The default absorbs and reads in the obvious way; override it only where the type can do /// better, as SHAKE does. fn xof(mut self, data: &[u8], result_len: usize) -> Vec where Self: Sized, { self.do_update(data); - self.into_squeezer().do_output(result_len) + self.into_squeezer().do_final(result_len) } /// One-shot: absorbs `data` and fills `output`, which is zeroized first. Returns the number of /// bytes written. /// - /// Defaulted as [`xof`](Self::xof) is. + /// A final read of `output.len()` bytes, and defaulted as [`xof`](Self::xof) is. fn xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize where Self: Sized, { self.do_update(data); - self.into_squeezer().do_output_out(output) + self.into_squeezer().do_final_out(output) } } diff --git a/crypto/sha3/src/cshake.rs b/crypto/sha3/src/cshake.rs index dfc21100..0f90ae06 100644 --- a/crypto/sha3/src/cshake.rs +++ b/crypto/sha3/src/cshake.rs @@ -142,29 +142,39 @@ impl Hash for CSHAKEInternal { self.shake.output_len() } - fn hash(self, data: &[u8]) -> Vec { - let n = self.output_len(); - let mut out = vec![0u8; n]; - self.hash_out(data, &mut out); - out + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() } fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { self.do_update(data); - self.into_squeezer().do_output_out(output) + self.do_final_out(output) } fn do_update(&mut self, data: &[u8]) { self.shake.do_update(data); } + /// A final read at the nominal length: [`Hash::output_len`] bytes, 32 for cSHAKE128 and 64 for + /// cSHAKE256, twice the security strength. + /// + /// Like SHAKE and unlike the SP 800-185 functions built on it, cSHAKE has no length to bind -- + /// `L` reaches it as "how much to read", not as absorbed input (Sec 3.3) -- so these are the + /// same bytes the squeezer produces. What the `Hash` view fixes is how many. fn do_final(self) -> Vec { let n = self.output_len(); - self.into_squeezer().do_output(n) + self.into_squeezer().do_final(n) } fn do_final_out(self, output: &mut [u8]) -> usize { - self.into_squeezer().do_output_out(output) + let n = self.output_len(); + // Per Hash::do_final_out: a short buffer is filled and the output truncated, a long one + // takes it in its first output_len bytes and zeros after. To fill a longer buffer, use the + // XOF spelling, which takes its length from the buffer. + let written = n.min(output.len()); + output[written..].fill(0); + self.into_squeezer().do_final_out(&mut output[..written]) } fn do_final_partial_bits( @@ -183,7 +193,13 @@ impl Hash for CSHAKEInternal { num_bits: usize, output: &mut [u8], ) -> Result { - Ok(self.into_squeezer_partial_bits(partial_byte, num_bits)?.do_output_out(output)) + let n = self.output_len(); + // Validated before anything is written, so a rejected call leaves `output` untouched. + let squeezer = self.into_squeezer_partial_bits(partial_byte, num_bits)?; + // The buffer rule of do_final_out applies here too: output_len bytes, then zeros. + let written = n.min(output.len()); + output[written..].fill(0); + Ok(squeezer.do_final_out(&mut output[..written])) } fn max_security_strength(&self) -> SecurityStrength { diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs index 3e8706a4..ca402acd 100644 --- a/crypto/sha3/src/kmac.rs +++ b/crypto/sha3/src/kmac.rs @@ -2,7 +2,7 @@ use crate::SHAKEParams; use crate::cshake::CSHAKEInternal; -use crate::shake::SHAKESqueezer; +use crate::length_bound_squeezer::LengthBoundSqueezer; use crate::xof_utils::right_encode; use bouncycastle_core::errors::{HashError, KeyMaterialError, MACError}; use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; @@ -184,9 +184,14 @@ impl MAC for KMACInternal { /// `Hash` share five method names (`do_update`, `do_final`, `output_len` and two more), one type /// implementing both would make every one of those calls ambiguous, so they are separate types. /// -/// Because the length is *not* bound here, output at one length really is a prefix of output at a -/// longer one -- the opposite of fixed-length KMAC -- so [`Hash::do_final`] is the first -/// [`Hash::output_len`] bytes of the same stream [`XOF::into_squeezer`] produces. +/// Read as a stream -- [`XOFSqueezer::do_output`] -- the length really is not bound, so output at +/// one length is a prefix of output at a longer one, the opposite of fixed-length KMAC. +/// +/// Read as a *final* read, it is bound, because a caller that names a length and will not be back +/// has said what `L` is: [`XOFSqueezer::do_final`] and [`XOF::xof`] absorb `right_encode(8n)` and +/// so produce `KMAC(K, X, 8n, S)` exactly (see [`LengthBoundSqueezer`]), and the [`Hash`] view -- +/// [`Hash::do_final`], [`Hash::hash`] and [`Hash::hash_out`] -- does the same at the nominal +/// [`Hash::output_len`], since a hash's output length is fixed by its type. #[derive(Clone)] pub struct KMACXOFInternal { cshake: CSHAKEInternal, @@ -216,12 +221,6 @@ impl KMACXOFInternal { let kmac = KMACInternal::::new_with_params(key, customization, 0, allow_weak_key)?; Ok(Self { cshake: kmac.cshake, strength: kmac.strength }) } - - /// Absorbs `right_encode(0)`, the Sec 4.3.1 length binding, ending the input phase. - fn bind_zero_length(&mut self) { - let (buf, len) = right_encode(0); - self.cshake.do_update(&buf[..len]); - } } impl Hash for KMACXOFInternal { @@ -229,34 +228,44 @@ impl Hash for KMACXOFInternal { self.cshake.block_bitlen() } - /// The nominal length, 32 or 64 bytes. Unlike [`KMACInternal`] this is not bound into the - /// computation -- it is only how many bytes [`Hash::do_final`] takes from the stream. + /// The nominal length, 32 or 64 bytes: twice the security strength of this KMAC, which is the + /// length at which the output carries that strength in full. Reading as a XOF does not bind + /// it; the [`Hash`] view does, because a hash has one output length and it is this one. fn output_len(&self) -> usize { self.cshake.output_len() } fn hash(mut self, data: &[u8]) -> Vec { - let n = self.output_len(); self.do_update(data); - self.into_squeezer().do_output(n) + self.do_final() } fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { self.do_update(data); - self.into_squeezer().do_output_out(output) + self.do_final_out(output) } fn do_update(&mut self, data: &[u8]) { self.cshake.do_update(data); } + /// A final read at the nominal length, so `L` is bound: this is `KMAC(K, X, 8n, S)` for + /// `n = ` [`Hash::output_len`] -- the fixed-length KMAC of Sec 4.3, not a prefix of the + /// KMACXOF stream. fn do_final(self) -> Vec { let n = self.output_len(); - self.into_squeezer().do_output(n) + self.into_squeezer().do_final(n) } fn do_final_out(self, output: &mut [u8]) -> usize { - self.into_squeezer().do_output_out(output) + let n = self.output_len(); + // Per Hash::do_final_out: a short buffer is filled and the output truncated, a long one + // takes it in its first output_len bytes and zeros after. `n` is what reaches + // right_encode either way, so a truncated read is this KMAC cut short rather than the + // KMAC of the buffer's length. + let written = n.min(output.len()); + output[written..].fill(0); + self.into_squeezer().do_final_out_with_length((n as u64) * 8, &mut output[..written]) } /// # Errors @@ -294,11 +303,12 @@ impl Hash for KMACXOFInternal { } impl XOF for KMACXOFInternal { - type Squeezer = SHAKESqueezer; + type Squeezer = LengthBoundSqueezer; - fn into_squeezer(mut self) -> Self::Squeezer { - self.bind_zero_length(); - self.cshake.into_squeezer() + /// The `right_encode(L)` of Sec 4.3.1 step 1 is not absorbed here: which `L` it carries depends + /// on how the first output is read, so [`LengthBoundSqueezer`] decides it. + fn into_squeezer(self) -> Self::Squeezer { + LengthBoundSqueezer::new(self.cshake) } fn into_squeezer_partial_bits( diff --git a/crypto/sha3/src/length_bound_squeezer.rs b/crypto/sha3/src/length_bound_squeezer.rs new file mode 100644 index 00000000..e7827cfc --- /dev/null +++ b/crypto/sha3/src/length_bound_squeezer.rs @@ -0,0 +1,106 @@ +//! The squeezing phase of the SP 800-185 functions that have an output length left to bind. + +use crate::SHAKEParams; +use crate::cshake::CSHAKEInternal; +use crate::shake::SHAKESqueezer; +use crate::xof_utils::right_encode; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; + +/// The squeezing phase of KMACXOF, TupleHashXOF and ParallelHashXOF, which still has a choice to +/// make. +/// +/// Every SP 800-185 function ends its absorbed input with `right_encode(L)`, and the two forms of +/// each function differ only in what goes in there: the fixed-length KMAC, TupleHash and +/// ParallelHash of s. 4.3, 5.3 and 6.3 encode the requested output length, and the XOF forms of +/// s. 4.3.1, 5.3.1 and 6.3.1 encode 0. Nothing else about them differs, so the choice can be left +/// until the caller says how it wants to read -- which is what this type does: +/// +/// * [`XOFSqueezer::do_output`] is the XOF reading. It is the caller saying "give me some bytes and +/// I may be back for more", which only `right_encode(0)` can answer, since a length bound into +/// the sponge cannot be revised once output has begun. +/// * [`XOFSqueezer::do_final`], as the **first** read, is the fixed-length reading. It is the +/// caller saying how many bytes it wants and that it will not be back, so `L` is that length in +/// bits and the result is the fixed-length function of s. 4.3, 5.3 or 6.3 -- the same bytes +/// `KMAC128(K, X, L, S)` produces, not a truncation of `KMACXOF128`. +/// +/// The first read commits: the encoding is in the sponge from then on, so a `do_final` that +/// follows a `do_output` cannot bind anything and simply continues the `right_encode(0)` stream +/// the earlier read already chose. +pub struct LengthBoundSqueezer { + phase: Phase, +} + +/// Which side of the first read this squeezer is on. +enum Phase { + /// Nothing read yet, so `right_encode(L)` is still the caller's to choose. + Unbound(CSHAKEInternal), + /// The encoding has been absorbed and the sponge is producing output. + Squeezing(SHAKESqueezer), + /// Never observed: [`LengthBoundSqueezer::read`] leaves this here only while the value moves + /// from one of the phases above to the other. + Binding, +} + +impl LengthBoundSqueezer { + /// Wraps a cSHAKE with everything but its `right_encode(L)` absorbed. + pub(crate) fn new(cshake: CSHAKEInternal) -> Self { + Self { phase: Phase::Unbound(cshake) } + } + + /// [`XOFSqueezer::do_final_out`] with `L` given rather than taken from the buffer. + /// + /// For the `Hash` view of these functions, whose length is fixed by the type: it binds the + /// nominal output length and then writes as much of it as the caller's buffer has room for, + /// which is what [`Hash::do_final_out`] promises. Going through + /// [`XOFSqueezer::do_final_out`] would bind the buffer's length instead, and a short buffer + /// would then compute a different function rather than truncating this one. + pub(crate) fn do_final_out_with_length(mut self, length_bits: u64, output: &mut [u8]) -> usize { + self.read(length_bits, output) + } + + /// Fills `output` from the stream, absorbing `right_encode(length_bits)` first if this is the + /// first read. `output` is zeroized before anything is written to it. + fn read(&mut self, length_bits: u64, output: &mut [u8]) -> usize { + self.phase = match core::mem::replace(&mut self.phase, Phase::Binding) { + Phase::Unbound(mut cshake) => { + let (buf, len) = right_encode(length_bits); + cshake.do_update(&buf[..len]); + Phase::Squeezing(cshake.into_squeezer()) + } + // An earlier read chose the encoding; this one continues that stream. + committed => committed, + }; + match &mut self.phase { + Phase::Squeezing(squeezer) => squeezer.do_output_out(output), + // The match above turns `Unbound` into `Squeezing` and puts `Binding` back as it found + // it, so neither can be live here. + _ => unreachable!("the first read always leaves the squeezing phase"), + } + } +} + +impl XOFSqueezer for LengthBoundSqueezer { + fn do_output(&mut self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.do_output_out(&mut out); + out + } + + /// Reading as a XOF, so `right_encode(0)` if this is the first read (s. 4.3.1, 5.3.1, 6.3.1). + fn do_output_out(&mut self, output: &mut [u8]) -> usize { + self.read(0, output) + } + + fn do_final(self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.do_final_out(&mut out); + out + } + + /// The last read, so if it is also the first, `L` is its length in bits and this is the + /// fixed-length function of s. 4.3, 5.3 or 6.3. After a [`XOFSqueezer::do_output`] the encoding + /// is already in the sponge and this just continues that stream. + fn do_final_out(mut self, output: &mut [u8]) -> usize { + self.read((output.len() as u64) * 8, output) + } +} diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 8d1b99c3..e0bfa7d1 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -204,6 +204,7 @@ use bouncycastle_core::traits::{Hash, KDF, MAC, Suspendable, XOF}; mod cshake; mod keccak; mod kmac; +mod length_bound_squeezer; mod parallelhash; mod sha3; mod shake; @@ -257,6 +258,7 @@ pub const PARALLELHASHXOF256_NAME: &str = "ParallelHashXOF256"; /*** pub types ***/ pub use cshake::CSHAKEInternal; pub use kmac::{KMACInternal, KMACXOFInternal}; +pub use length_bound_squeezer::LengthBoundSqueezer; pub use parallelhash::{ParallelHashInternal, ParallelHashXOFInternal}; pub use sha3::SHA3Internal; pub use tuplehash::{TupleHashInternal, TupleHashXOFInternal}; diff --git a/crypto/sha3/src/parallelhash.rs b/crypto/sha3/src/parallelhash.rs index 5d99f401..830f7594 100644 --- a/crypto/sha3/src/parallelhash.rs +++ b/crypto/sha3/src/parallelhash.rs @@ -2,7 +2,8 @@ use crate::SHAKEParams; use crate::cshake::{CSHAKEInternal, absorb_left_encode_into}; -use crate::shake::{SHAKEInternal, SHAKESqueezer}; +use crate::length_bound_squeezer::LengthBoundSqueezer; +use crate::shake::SHAKEInternal; use crate::xof_utils::right_encode; use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFSqueezer}; @@ -66,22 +67,32 @@ impl ParallelState { self.buffer.extend_from_slice(data); } - /// Flushes the short final block, then binds the block count and the length (steps 3 and 4). + /// Flushes the short final block and binds the block count: step 3, and the `right_encode(n)` + /// half of step 4. /// - /// `length_bits` is `right_encode`'s argument: the requested output length for the - /// fixed-length function, or 0 for the XOF (Sec 6.3.1). - fn finish(mut self, length_bits: u64) -> CSHAKEInternal { + /// The `right_encode(L)` that completes step 4 is left to the caller, because which `L` it + /// carries is not settled here: the fixed-length function knows it up front ([`Self::finish`]), + /// and the XOF leaves it to the first read ([`LengthBoundSqueezer`]). + fn finish_blocks(mut self) -> CSHAKEInternal { if !self.buffer.is_empty() { let block = core::mem::take(&mut self.buffer); self.absorb_block(&block); } - // Step 4: z = z || right_encode(n) || right_encode(L). - for value in [self.blocks, length_bits] { - let (buf, len) = right_encode(value); - self.cshake.do_update(&buf[..len]); - } + // Step 4: z = z || right_encode(n) ... + let (buf, len) = right_encode(self.blocks); + self.cshake.do_update(&buf[..len]); self.cshake } + + /// [`Self::finish_blocks`], then the `right_encode(L)` that completes step 4. + /// + /// `length_bits` is the requested output length of the fixed-length function of Sec 6.3. + fn finish(self, length_bits: u64) -> CSHAKEInternal { + let mut cshake = self.finish_blocks(); + let (buf, len) = right_encode(length_bits); + cshake.do_update(&buf[..len]); + cshake + } } /// Internal struct for ParallelHash. Use [`crate::PARALLELHASH128`] or [`crate::PARALLELHASH256`]. @@ -226,33 +237,41 @@ impl Hash for ParallelHashXOFInternal { self.state.cshake.block_bitlen() } - /// The nominal length, 32 or 64 bytes; not bound into the computation. + /// The nominal length, 32 or 64 bytes: twice the security strength, the length at which the + /// output carries that strength in full. Bound by the [`Hash`] view and not by the XOF one. fn output_len(&self) -> usize { self.state.cshake.output_len() } fn hash(mut self, data: &[u8]) -> Vec { - let n = self.output_len(); self.do_update(data); - self.into_squeezer().do_output(n) + self.do_final() } fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { self.do_update(data); - self.into_squeezer().do_output_out(output) + self.do_final_out(output) } fn do_update(&mut self, data: &[u8]) { self.state.do_update(data); } + /// A final read at the nominal length, so `L` is bound: this is the fixed-length ParallelHash + /// of Sec 6.3 at `n = ` [`Hash::output_len`], not a prefix of the ParallelHashXOF stream. fn do_final(self) -> Vec { let n = self.output_len(); - self.into_squeezer().do_output(n) + self.into_squeezer().do_final(n) } fn do_final_out(self, output: &mut [u8]) -> usize { - self.into_squeezer().do_output_out(output) + let n = self.output_len(); + // Per Hash::do_final_out, as for the fixed-length form: a short buffer truncates this + // ParallelHash rather than computing the ParallelHash of a shorter length, because `n` is + // what reaches right_encode, not the buffer's length. + let written = n.min(output.len()); + output[written..].fill(0); + self.into_squeezer().do_final_out_with_length((n as u64) * 8, &mut output[..written]) } /// # Errors @@ -288,11 +307,13 @@ impl Hash for ParallelHashXOFInternal { } impl XOF for ParallelHashXOFInternal { - type Squeezer = SHAKESqueezer; + type Squeezer = LengthBoundSqueezer; + /// The block count of Sec 6.3.1 step 4 is bound here; the `right_encode` that follows it is + /// not, because whether it carries 0 or the length of a final read is + /// [`LengthBoundSqueezer`]'s decision. fn into_squeezer(self) -> Self::Squeezer { - // Sec 6.3.1 step 4: right_encode(0) rather than the length. - self.state.finish(0).into_squeezer() + LengthBoundSqueezer::new(self.state.finish_blocks()) } fn into_squeezer_partial_bits( diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index 834cd9fd..91fc88c2 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -375,14 +375,14 @@ impl Hash for SHAKEInternal { (PARAMS::SIZE as usize) / 4 } - fn hash(self, data: &[u8]) -> Vec { - let result_len = self.output_len(); - self.hash_internal(data, result_len) + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() } - fn hash_out(self, data: &[u8], output: &mut [u8]) -> usize { - // hash_internal_out zeroizes `output` before writing. - self.hash_internal_out(data, output) + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.do_final_out(output) } /// Infallible, and this is a fact about the type rather than a promise. @@ -399,16 +399,26 @@ impl Hash for SHAKEInternal { self.keccak.absorb(data); } - /// Produces [`output_len`](Self::output_len) bytes and ends the object. + /// A final read at the nominal length: [`output_len`](Self::output_len) bytes, 32 for + /// SHAKE128 and 64 for SHAKE256, twice the security strength. + /// + /// FIPS 202 gives SHAKE no length to bind -- the output length is not an input to the function + /// -- so these are the same bytes the squeezer produces. What the `Hash` view fixes is *how + /// many*: a hash has one output length and it is this one. Ask for another through the XOF. fn do_final(self) -> Vec { let n = self.output_len(); - let mut out = vec![0u8; n]; - self.do_final_out(&mut out); - out + self.into_squeezer().do_final(n) } fn do_final_out(self, output: &mut [u8]) -> usize { - self.into_squeezer().do_output_out(output) + let n = self.output_len(); + // Per Hash::do_final_out: a short buffer is filled and the output truncated, a long one + // takes it in its first output_len bytes and zeros after. To fill a longer buffer, use the + // XOF spelling -- XOF::xof_out and XOFSqueezer::do_output_out take their length from the + // buffer, which is exactly the difference between a XOF and a hash. + let written = n.min(output.len()); + output[written..].fill(0); + self.into_squeezer().do_final_out(&mut output[..written]) } fn do_final_partial_bits( @@ -427,8 +437,13 @@ impl Hash for SHAKEInternal { num_bits: usize, output: &mut [u8], ) -> Result { + let n = self.output_len(); // Validated before anything is written, so a rejected call leaves `output` untouched. - Ok(self.into_squeezer_partial_bits(partial_byte, num_bits)?.do_output_out(output)) + let squeezer = self.into_squeezer_partial_bits(partial_byte, num_bits)?; + // The buffer rule of do_final_out applies here too: output_len bytes, then zeros. + let written = n.min(output.len()); + output[written..].fill(0); + Ok(squeezer.do_final_out(&mut output[..written])) } fn max_security_strength(&self) -> SecurityStrength { diff --git a/crypto/sha3/src/tuplehash.rs b/crypto/sha3/src/tuplehash.rs index 1eb3be28..858a8437 100644 --- a/crypto/sha3/src/tuplehash.rs +++ b/crypto/sha3/src/tuplehash.rs @@ -2,7 +2,7 @@ use crate::SHAKEParams; use crate::cshake::{CSHAKEInternal, absorb_encoded_string_into}; -use crate::shake::SHAKESqueezer; +use crate::length_bound_squeezer::LengthBoundSqueezer; use crate::xof_utils::right_encode; use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFSqueezer}; @@ -142,8 +142,15 @@ impl Hash for TupleHashInternal { /// /// The arbitrary-output-length TupleHash of Sec 5.3.1: `right_encode(0)` in place of the length. /// As with KMAC, it is a *different function* from the fixed-length one, not a longer view of it, -/// and it is a separate type for the same reason -- but here the length not being bound means -/// output at one length really is a prefix of output at a longer one. +/// and it is a separate type for the same reason -- but read as a stream +/// ([`XOFSqueezer::do_output`]) the length is not bound, so output at one length is a prefix of +/// output at a longer one. +/// +/// A *final* read binds it, because a caller that names a length and will not be back has said +/// what `L` is: [`XOFSqueezer::do_final`] and [`XOF::xof`] produce the fixed-length TupleHash of +/// Sec 5.3 (see [`LengthBoundSqueezer`]), and the [`Hash`] view -- [`Hash::do_final`], +/// [`Hash::hash`] and [`Hash::hash_out`] -- does the same at the nominal [`Hash::output_len`], +/// since a hash's output length is fixed by its type. /// /// [`Hash::do_update`] appends one tuple element, exactly as for [`TupleHashInternal`]. #[derive(Clone)] @@ -163,7 +170,7 @@ impl TupleHashXOFInternal { } /// Hashes a whole tuple and returns the output stream. - pub fn output_for(mut self, tuple: &[&[u8]]) -> SHAKESqueezer { + pub fn output_for(mut self, tuple: &[&[u8]]) -> LengthBoundSqueezer { for element in tuple { self.do_update(element); } @@ -176,21 +183,21 @@ impl Hash for TupleHashXOFInternal { self.cshake.block_bitlen() } - /// The nominal length, 32 or 64 bytes. Not bound into the computation -- see - /// [`TupleHashXOFInternal`]. + /// The nominal length, 32 or 64 bytes: twice the security strength, the length at which the + /// output carries that strength in full. Bound by the [`Hash`] view and not by the XOF one -- + /// see [`TupleHashXOFInternal`]. fn output_len(&self) -> usize { self.cshake.output_len() } fn hash(mut self, data: &[u8]) -> Vec { - let n = self.output_len(); self.do_update(data); - self.into_squeezer().do_output(n) + self.do_final() } fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { self.do_update(data); - self.into_squeezer().do_output_out(output) + self.do_final_out(output) } /// Appends **one tuple element**. @@ -198,13 +205,21 @@ impl Hash for TupleHashXOFInternal { absorb_encoded_string_into(&mut self.cshake, data); } + /// A final read at the nominal length, so `L` is bound: this is the fixed-length TupleHash of + /// Sec 5.3 at `n = ` [`Hash::output_len`], not a prefix of the TupleHashXOF stream. fn do_final(self) -> Vec { let n = self.output_len(); - self.into_squeezer().do_output(n) + self.into_squeezer().do_final(n) } fn do_final_out(self, output: &mut [u8]) -> usize { - self.into_squeezer().do_output_out(output) + let n = self.output_len(); + // Per Hash::do_final_out, as for the fixed-length form: a short buffer truncates this + // TupleHash rather than computing the TupleHash of a shorter length, because `n` is what + // reaches right_encode, not the buffer's length. + let written = n.min(output.len()); + output[written..].fill(0); + self.into_squeezer().do_final_out_with_length((n as u64) * 8, &mut output[..written]) } /// # Errors @@ -240,13 +255,12 @@ impl Hash for TupleHashXOFInternal { } impl XOF for TupleHashXOFInternal { - type Squeezer = SHAKESqueezer; + type Squeezer = LengthBoundSqueezer; - fn into_squeezer(mut self) -> Self::Squeezer { - // Sec 5.3.1 step 4: right_encode(0) rather than the length. - let (buf, len) = right_encode(0); - self.cshake.do_update(&buf[..len]); - self.cshake.into_squeezer() + /// The `right_encode` of Sec 5.3.1 step 4 is not absorbed here: whether it carries 0 or the + /// length of a final read is [`LengthBoundSqueezer`]'s decision. + fn into_squeezer(self) -> Self::Squeezer { + LengthBoundSqueezer::new(self.cshake) } fn into_squeezer_partial_bits( diff --git a/crypto/sha3/tests/cshake_tests.rs b/crypto/sha3/tests/cshake_tests.rs index 875e36dd..7a8288d5 100644 --- a/crypto/sha3/tests/cshake_tests.rs +++ b/crypto/sha3/tests/cshake_tests.rs @@ -185,6 +185,35 @@ fn cshake_is_a_hash() { assert_eq!(c.do_final().len(), 64, "cSHAKE256's nominal output length"); } +/// As for SHAKE: the `Hash` view writes [`Hash::output_len`] bytes and zeroizes the rest, while +/// the XOF spelling fills whatever buffer it is given. +#[test] +fn the_hash_view_writes_output_len_bytes_and_zeroes_the_rest() { + let make = || CSHAKE128::new(b"", b"Email Signature"); + + let mut hash_view = [0xFFu8; 100]; + assert_eq!(make().hash_out(b"abc", &mut hash_view), 32, "cSHAKE128's nominal length"); + assert_eq!(&hash_view[..32], &make().hash(b"abc")[..], "... written in full"); + assert_eq!(&hash_view[32..], &[0u8; 68][..], "everything past output_len is zeroized"); + + let mut buf = [0xFFu8; 100]; + let mut c = make(); + c.do_update(b"abc"); + assert_eq!(c.do_final_out(&mut buf), 32); + assert_eq!(buf, hash_view, "do_final_out must agree with hash_out"); + + let mut xof_view = [0xFFu8; 100]; + assert_eq!(make().xof_out(b"abc", &mut xof_view), 100, "the XOF fills the buffer"); + assert_eq!(&xof_view[..32], &hash_view[..32], "the same stream, read further"); + assert_ne!(&xof_view[32..], &[0u8; 68][..], "... rather than stopping at output_len"); + + // cSHAKE256's nominal length is 64, so its split lands elsewhere. + let mut hash_view = [0xFFu8; 100]; + let n = CSHAKE256::new(b"", b"Email Signature").hash_out(b"abc", &mut hash_view); + assert_eq!(n, 64, "cSHAKE256's nominal length"); + assert_eq!(&hash_view[64..], &[0u8; 36][..], "everything past output_len is zeroized"); +} + /// The algorithm names, so the factory and any registry agree with the specification's spelling. #[test] fn algorithm_names() { diff --git a/crypto/sha3/tests/kmac_tests.rs b/crypto/sha3/tests/kmac_tests.rs index efee7ce2..49cfb2aa 100644 --- a/crypto/sha3/tests/kmac_tests.rs +++ b/crypto/sha3/tests/kmac_tests.rs @@ -4,7 +4,7 @@ use bouncycastle_core::errors::{KeyMaterialError, MACError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{Algorithm, Hash, MAC, XOF}; +use bouncycastle_core::traits::{Algorithm, Hash, MAC, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_hex as hex; use bouncycastle_sha3::{KMAC128, KMAC256, KMACXOF128, KMACXOF256}; @@ -109,12 +109,18 @@ fn nist_sp800_185_kmacxof_sample_values() { let want = v.output_len / 8; let key = key_material(&v.key); + // Read with do_output, which is the XOF reading of the stream: the one-shots bind the + // length they are given, and are checked against the fixed-length samples elsewhere. let got = match v.strength { 128 => { - KMACXOF128::new(&key, v.s.as_bytes(), false).expect("a valid key").xof(&v.msg, want) + let mut k = KMACXOF128::new(&key, v.s.as_bytes(), false).expect("a valid key"); + k.do_update(&v.msg); + k.into_squeezer().do_output(want) } 256 => { - KMACXOF256::new(&key, v.s.as_bytes(), false).expect("a valid key").xof(&v.msg, want) + let mut k = KMACXOF256::new(&key, v.s.as_bytes(), false).expect("a valid key"); + k.do_update(&v.msg); + k.into_squeezer().do_output(want) } other => panic!("COUNT {i}: unexpected strength {other}"), }; @@ -233,22 +239,130 @@ fn algorithm_names() { assert_eq!(KMAC256::ALG_NAME, "KMAC256"); } -/// The counterpart to `output_length_changes_the_function`: because KMACXOF binds -/// `right_encode(0)` rather than the length, output at one length *is* a prefix of output at a -/// longer one, and `do_final` is simply the first `output_len` bytes of that same stream. +/// The counterpart to `output_length_changes_the_function`: read as a stream, KMACXOF binds +/// `right_encode(0)` rather than the length, so output at one length *is* a prefix of output at a +/// longer one. The `Hash` view is not part of that stream -- it is a final read at the nominal +/// length, so it binds `L` and computes fixed-length KMAC128 instead. #[test] fn kmacxof_output_is_one_stream() { let key = key_material(&[0x42u8; 32]); - let long = KMACXOF128::new(&key, b"", false).unwrap().xof(b"abc", 64); + let squeeze = |n| { + let mut k = KMACXOF128::new(&key, b"", false).unwrap(); + k.do_update(b"abc"); + k.into_squeezer().do_output(n) + }; + let long = squeeze(64); - let short = KMACXOF128::new(&key, b"", false).unwrap().xof(b"abc", 16); + let short = squeeze(16); assert_eq!(&long[..16], &short[..], "KMACXOF at a shorter length must be a prefix"); let mut k = KMACXOF128::new(&key, b"", false).unwrap(); k.do_update(b"abc"); let via_hash = k.do_final(); assert_eq!(via_hash.len(), 32, "the nominal output length"); - assert_eq!(&long[..32], &via_hash[..], "do_final must be a prefix of the stream"); + assert_ne!(&long[..32], &via_hash[..], "the Hash view binds L, so it leaves the stream"); + assert_eq!( + via_hash, + KMAC128::new(&key).unwrap().mac(b"abc"), + "... and lands on fixed-length KMAC128 at the nominal length" + ); +} + +/// `do_final` as the first read binds `right_encode(L)`, so it computes fixed-length KMAC. +/// +/// SP 800-185 s. 4.3 and s. 4.3.1 are the same function but for one field: step 1 absorbs +/// `bytepad(encode_string(K), 168) || X || right_encode(L)` for KMAC and `right_encode(0)` for +/// KMACXOF. Nothing else separates them, so the encoding need not be chosen until the caller says +/// how it wants to read -- and `do_final` as the first read says both how many bytes it wants and +/// that it will not be back, which is exactly `L`. +/// +/// So `KMACXOF128::into_squeezer().do_final(n)` must be `KMAC128(K, X, 8n, S)` to the byte, which +/// the paired sample files check directly: `KMAC.rsp` and `KMACXOF.rsp` publish the same key, +/// message, customization and length, and the fixed-length file is what `do_final` has to match. +#[test] +fn do_final_binds_the_length_when_nothing_has_been_read() { + let (Some(fixed), Some(xof)) = (read_vectors("KMAC.rsp"), read_vectors("KMACXOF.rsp")) else { + return; + }; + assert_eq!(fixed.len(), xof.len(), "the two sample files pair up"); + + for (i, (f, x)) in fixed.iter().zip(xof.iter()).enumerate() { + let key = key_material(&f.key); + let ctx = format!("COUNT {i}: KMACXOF{} S={:?}", f.strength, f.s); + let s = f.s.as_bytes(); + match f.strength { + 128 => check_do_final_binds_length( + || KMACXOF128::new(&key, s, false).expect("a valid key"), + |n| KMAC128::new_with_params(&key, s, n, false).expect("a valid key").mac(&f.msg), + &f.msg, + &f.output, + &x.output, + &ctx, + ), + 256 => check_do_final_binds_length( + || KMACXOF256::new(&key, s, false).expect("a valid key"), + |n| KMAC256::new_with_params(&key, s, n, false).expect("a valid key").mac(&f.msg), + &f.msg, + &f.output, + &x.output, + &ctx, + ), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + } + println!("KMACXOF do_final: {} sample values", fixed.len()); +} + +/// One paired sample through `do_final`. `fixed_expected` is the published fixed-length value, +/// `xof_expected` the published XOF value over the same inputs, and `fixed_of` computes the +/// fixed-length function at a length no vector covers. +fn check_do_final_binds_length( + make: impl Fn() -> X, + fixed_of: impl Fn(usize) -> Vec, + msg: &[u8], + fixed_expected: &[u8], + xof_expected: &[u8], + ctx: &str, +) { + let n = fixed_expected.len(); + assert_ne!(fixed_expected, xof_expected, "{ctx}: the two sample values must differ at all"); + + // The first read, with no do_output before it: right_encode(8n), so the fixed-length function. + let mut x = make(); + x.do_update(msg); + assert_eq!(x.into_squeezer().do_final(n), fixed_expected, "{ctx}: do_final binds the length"); + + // Pre-filled, so the documented zeroization is observable. + let mut buf = vec![0xFFu8; n]; + let mut x = make(); + x.do_update(msg); + assert_eq!(x.into_squeezer().do_final_out(&mut buf), n, "{ctx}: do_final_out returns the len"); + assert_eq!(buf, fixed_expected, "{ctx}: do_final_out binds the length"); + + // The `L` bound is the length actually asked for, not a fixed one. No sample value covers + // these lengths, so the comparison is against this library's own fixed-length function. + for shorter in [n / 2, n - 1] { + let mut x = make(); + x.do_update(msg); + assert_eq!(x.into_squeezer().do_final(shorter), fixed_of(shorter), "{ctx}: L = {shorter}"); + } + + // The one-shots name their length and never come back, so they bind it too. + assert_eq!(make().xof(msg, n), fixed_expected, "{ctx}: xof binds the length"); + + let mut buf = vec![0xFFu8; n]; + assert_eq!(make().xof_out(msg, &mut buf), n, "{ctx}: xof_out returns the length"); + assert_eq!(buf, fixed_expected, "{ctx}: xof_out binds the length"); + + // Once a read has happened right_encode(0) is in the sponge and cannot be revised, so do_final + // after a do_output is the XOF stream continuing, not the fixed-length function. + let split = n / 2; + let mut x = make(); + x.do_update(msg); + let mut squeezer = x.into_squeezer(); + let head = squeezer.do_output(split); + let tail = squeezer.do_final(n - split); + assert_eq!([head, tail].concat(), xof_expected, "{ctx}: do_final after a read stays the XOF"); } /// A partial final byte cannot be expressed: `right_encode(0)` has to follow the message, and the @@ -290,6 +404,9 @@ fn test_framework_xof() { // message -- so that part of the suite is switched off. let mut framework = TestFrameworkXOF::new(); framework.enable_partial_byte_tests = false; + // Sec 4.3.1: do_final as the first read binds right_encode(L), which is fixed-length KMAC + // rather than this stream. Checked against the paired sample files elsewhere in this file. + framework.do_final_binds_output_length = true; framework.test_xof( || KMACXOF128::new(&key, v.s.as_bytes(), false).expect("a valid key"), &v.msg, diff --git a/crypto/sha3/tests/parallelhash_tests.rs b/crypto/sha3/tests/parallelhash_tests.rs index 50974408..ede1c60b 100644 --- a/crypto/sha3/tests/parallelhash_tests.rs +++ b/crypto/sha3/tests/parallelhash_tests.rs @@ -3,7 +3,7 @@ //! Vectors come from the `bc-test-data` repo cloned alongside this one; see `cshake_tests.rs`. use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{Algorithm, Hash, XOF}; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_hex as hex; use bouncycastle_sha3::{PARALLELHASH128, PARALLELHASH256, PARALLELHASHXOF128, PARALLELHASHXOF256}; @@ -95,9 +95,19 @@ fn nist_sp800_185_parallelhashxof_sample_values() { for (i, v) in vectors.iter().enumerate() { let want = v.output_len / 8; + // Read with do_output, which is the XOF reading of the stream: the one-shots bind the + // length they are given, and are checked against the fixed-length samples elsewhere. let got = match v.strength { - 128 => PARALLELHASHXOF128::new(v.block_size, v.s.as_bytes()).xof(&v.msg, want), - 256 => PARALLELHASHXOF256::new(v.block_size, v.s.as_bytes()).xof(&v.msg, want), + 128 => { + let mut p = PARALLELHASHXOF128::new(v.block_size, v.s.as_bytes()); + p.do_update(&v.msg); + p.into_squeezer().do_output(want) + } + 256 => { + let mut p = PARALLELHASHXOF256::new(v.block_size, v.s.as_bytes()); + p.do_update(&v.msg); + p.into_squeezer().do_output(want) + } other => panic!("COUNT {i}: unexpected strength {other}"), }; assert_eq!( @@ -109,6 +119,101 @@ fn nist_sp800_185_parallelhashxof_sample_values() { println!("ParallelHashXOF: {} sample values", vectors.len()); } +/// `do_final` as the first read binds `right_encode(L)`, so it computes fixed-length ParallelHash. +/// +/// SP 800-185 s. 6.3 and s. 6.3.1 differ in one field: step 4 is `z = z || right_encode(n) || +/// right_encode(L)` for ParallelHash and `right_encode(0)` in that second slot for +/// ParallelHashXOF. The block count is settled when the input ends, but the length is not -- so it +/// waits for the first read, and `do_final` there says both how many bytes are wanted and that +/// there will be no more, which is exactly `L`. +/// +/// `ParallelHash.rsp` and `ParallelHashXOF.rsp` publish the same messages, block sizes, +/// customization and lengths, so the fixed-length file is what `do_final` has to match. +#[test] +fn do_final_binds_the_length_when_nothing_has_been_read() { + let (Some(fixed), Some(xof)) = + (read_vectors("ParallelHash.rsp"), read_vectors("ParallelHashXOF.rsp")) + else { + return; + }; + assert_eq!(fixed.len(), xof.len(), "the two sample files pair up"); + + for (i, (f, x)) in fixed.iter().zip(xof.iter()).enumerate() { + let ctx = + format!("COUNT {i}: ParallelHashXOF{} B={} S={:?}", f.strength, f.block_size, f.s); + let (b, s) = (f.block_size, f.s.as_bytes()); + match f.strength { + 128 => check_do_final_binds_length( + || PARALLELHASHXOF128::new(b, s), + |n| PARALLELHASH128::new(b, s, n).hash(&f.msg), + &f.msg, + &f.output, + &x.output, + &ctx, + ), + 256 => check_do_final_binds_length( + || PARALLELHASHXOF256::new(b, s), + |n| PARALLELHASH256::new(b, s, n).hash(&f.msg), + &f.msg, + &f.output, + &x.output, + &ctx, + ), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + } + println!("ParallelHashXOF do_final: {} sample values", fixed.len()); +} + +/// One paired sample through `do_final`. `fixed_expected` is the published fixed-length value, +/// `xof_expected` the published XOF value over the same message, and `fixed_of` computes the +/// fixed-length function at a length no vector covers. +fn check_do_final_binds_length( + make: impl Fn() -> X, + fixed_of: impl Fn(usize) -> Vec, + msg: &[u8], + fixed_expected: &[u8], + xof_expected: &[u8], + ctx: &str, +) { + let n = fixed_expected.len(); + assert_ne!(fixed_expected, xof_expected, "{ctx}: the two sample values must differ at all"); + let absorbed = || { + let mut x = make(); + x.do_update(msg); + x.into_squeezer() + }; + + // The first read, with no do_output before it: right_encode(8n), so the fixed-length function. + assert_eq!(absorbed().do_final(n), fixed_expected, "{ctx}: do_final binds the length"); + + // Pre-filled, so the documented zeroization is observable. + let mut buf = vec![0xFFu8; n]; + assert_eq!(absorbed().do_final_out(&mut buf), n, "{ctx}: do_final_out returns the length"); + assert_eq!(buf, fixed_expected, "{ctx}: do_final_out binds the length"); + + // The `L` bound is the length actually asked for, not a fixed one. No sample value covers + // these lengths, so the comparison is against this library's own fixed-length function. + for shorter in [n / 2, n - 1] { + assert_eq!(absorbed().do_final(shorter), fixed_of(shorter), "{ctx}: L = {shorter}"); + } + + // The one-shots name their length and never come back, so they bind it too. + assert_eq!(make().xof(msg, n), fixed_expected, "{ctx}: xof binds the length"); + + let mut buf = vec![0xFFu8; n]; + assert_eq!(make().xof_out(msg, &mut buf), n, "{ctx}: xof_out returns the length"); + assert_eq!(buf, fixed_expected, "{ctx}: xof_out binds the length"); + + // Once a read has happened right_encode(0) is in the sponge and cannot be revised, so do_final + // after a do_output is the XOF stream continuing, not the fixed-length function. + let split = n / 2; + let mut squeezer = absorbed(); + let head = squeezer.do_output(split); + let tail = squeezer.do_final(n - split); + assert_eq!([head, tail].concat(), xof_expected, "{ctx}: do_final after a read stays the XOF"); +} + /// The two are different functions on identical inputs. #[test] fn parallelhashxof_is_not_parallelhash_truncated() { @@ -188,8 +293,13 @@ fn length_binding_differs_between_the_two() { let long = PARALLELHASH128::new(4, b"", 32).hash(msg); assert_ne!(&long[..16], &short[..], "ParallelHash: a different length is a different function"); - let short = PARALLELHASHXOF128::new(4, b"").xof(msg, 16); - let long = PARALLELHASHXOF128::new(4, b"").xof(msg, 32); + let squeeze = |n| { + let mut p = PARALLELHASHXOF128::new(4, b""); + p.do_update(msg); + p.into_squeezer().do_output(n) + }; + let short = squeeze(16); + let long = squeeze(32); assert_eq!(&long[..16], &short[..], "ParallelHashXOF: one stream, so shorter is a prefix"); } @@ -265,38 +375,52 @@ fn check_fixed_view(make: impl Fn() -> H, msg: &[u8], expected: &[u8], assert_eq!(&out[n..], &[0u8; 7], "{ctx}: bytes past the output length are zeroized"); } -/// Every `Hash` and `XOF` entry point of the XOF form, against one sample value. The samples ask -/// for the nominal length, so `do_final` and `hash` must reproduce them exactly. -fn check_xof_view(make: impl Fn() -> X, msg: &[u8], expected: &[u8], ctx: &str) { +/// Every `Hash` and `XOF` entry point of the XOF form, against one paired sample value. +/// +/// The samples ask for the nominal length, and the `Hash` view is a final read at that length, so +/// it binds `L` and must reproduce the *fixed-length* sample; reading the stream with `do_output` +/// must reproduce the XOF one. +fn check_xof_view( + make: impl Fn() -> X, + msg: &[u8], + expected: &[u8], + fixed_expected: &[u8], + ctx: &str, +) { let n = expected.len(); assert_eq!(make().output_len(), n, "{ctx}: the samples ask for the nominal length"); + assert_eq!(fixed_expected.len(), n, "{ctx}: ... and the paired samples share it"); - assert_eq!(make().hash(msg), expected, "{ctx}: hash"); + assert_eq!(make().hash(msg), fixed_expected, "{ctx}: hash"); let mut out = vec![0u8; n]; assert_eq!(make().hash_out(msg, &mut out), n, "{ctx}: hash_out returns the length"); - assert_eq!(out, expected, "{ctx}: hash_out"); + assert_eq!(out, fixed_expected, "{ctx}: hash_out"); let mut x = make(); msg.chunks(5).for_each(|c| x.do_update(c)); - assert_eq!(x.do_final(), expected, "{ctx}: do_final"); + assert_eq!(x.do_final(), fixed_expected, "{ctx}: do_final"); let mut x = make(); x.do_update(msg); let mut out = vec![0u8; n]; assert_eq!(x.do_final_out(&mut out), n, "{ctx}: do_final_out returns the length"); - assert_eq!(out, expected, "{ctx}: do_final_out"); + assert_eq!(out, fixed_expected, "{ctx}: do_final_out"); // zero partial bits is the byte-aligned case and must be accepted; any other count refused let mut x = make(); x.do_update(msg); - assert_eq!(x.do_final_partial_bits(0, 0).unwrap(), expected, "{ctx}: do_final_partial_bits(0)"); + assert_eq!( + x.do_final_partial_bits(0, 0).unwrap(), + fixed_expected, + "{ctx}: do_final_partial_bits(0)" + ); let mut x = make(); x.do_update(msg); let mut out = vec![0u8; n]; assert_eq!(x.do_final_partial_bits_out(0, 0, &mut out).unwrap(), n, "{ctx}: ..._out length"); - assert_eq!(out, expected, "{ctx}: do_final_partial_bits_out(0)"); + assert_eq!(out, fixed_expected, "{ctx}: do_final_partial_bits_out(0)"); assert!(matches!(make().do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); let mut out = vec![0u8; n]; @@ -305,11 +429,17 @@ fn check_xof_view(make: impl Fn() -> X, msg: &[u8], expected: &[u8], ctx Err(HashError::InvalidLength(_)) )); - assert_eq!(make().xof(msg, n / 2), &expected[..n / 2], "{ctx}: xof, shorter"); + // The XOF reading of the stream is do_output; the one-shots bind the length they are given, + // so they belong to `do_final_binds_the_length_when_nothing_has_been_read` instead. + let mut x = make(); + x.do_update(msg); + assert_eq!(x.into_squeezer().do_output(n / 2), &expected[..n / 2], "{ctx}: do_output, shorter"); let mut out = vec![0u8; n]; - assert_eq!(make().xof_out(msg, &mut out), n, "{ctx}: xof_out returns the length"); - assert_eq!(out, expected, "{ctx}: xof_out"); + let mut x = make(); + x.do_update(msg); + assert_eq!(x.into_squeezer().do_output_out(&mut out), n, "{ctx}: do_output_out length"); + assert_eq!(out, expected, "{ctx}: do_output_out"); } #[test] @@ -329,13 +459,23 @@ fn hash_trait_view_agrees_with_the_sample_values() { #[test] fn xof_trait_view_agrees_with_the_sample_values() { - let Some(vectors) = read_vectors("ParallelHashXOF.rsp") else { return }; - for (i, v) in vectors.iter().enumerate() { + let (Some(fixed), Some(xof)) = + (read_vectors("ParallelHash.rsp"), read_vectors("ParallelHashXOF.rsp")) + else { + return; + }; + assert_eq!(fixed.len(), xof.len(), "the two sample files pair up"); + + for (i, (f, v)) in fixed.iter().zip(xof.iter()).enumerate() { let (b, s) = (v.block_size, v.s.as_bytes()); let ctx = format!("COUNT {i}: ParallelHashXOF{} B={b}", v.strength); match v.strength { - 128 => check_xof_view(|| PARALLELHASHXOF128::new(b, s), &v.msg, &v.output, &ctx), - 256 => check_xof_view(|| PARALLELHASHXOF256::new(b, s), &v.msg, &v.output, &ctx), + 128 => { + check_xof_view(|| PARALLELHASHXOF128::new(b, s), &v.msg, &v.output, &f.output, &ctx) + } + 256 => { + check_xof_view(|| PARALLELHASHXOF256::new(b, s), &v.msg, &v.output, &f.output, &ctx) + } other => panic!("COUNT {i}: unexpected strength {other}"), } } diff --git a/crypto/sha3/tests/shake_tests.rs b/crypto/sha3/tests/shake_tests.rs index 1147467c..214592c2 100644 --- a/crypto/sha3/tests/shake_tests.rs +++ b/crypto/sha3/tests/shake_tests.rs @@ -78,6 +78,50 @@ mod shake_tests { assert_eq!(SHAKE256::new().hash(b"abc").len(), 64); } + /// The `Hash` view writes [`Hash::output_len`] bytes and zeroizes the rest of the buffer; the + /// XOF spelling is what fills a buffer of the caller's choosing. + /// + /// FIPS 202 binds no length, so the two readings agree on the bytes they share -- the hash is + /// the first `output_len` bytes of the same stream -- and differ only in how much they write. + /// Before this, the `Hash` entry points took their length from the buffer, so a long one came + /// back full of XOF output and `output_len` meant nothing. + #[test] + fn the_hash_view_writes_output_len_bytes_and_zeroes_the_rest() { + let mut hash_view = [0xFFu8; 100]; + assert_eq!(SHAKE128::new().hash_out(b"abc", &mut hash_view), 32, "the nominal length"); + assert_eq!(&hash_view[..32], &SHAKE128::new().hash(b"abc")[..], "... written in full"); + assert_eq!(&hash_view[32..], &[0u8; 68][..], "everything past output_len is zeroized"); + + // do_final_out and the byte-aligned partial-bit spelling follow the same rule. + let mut buf = [0xFFu8; 100]; + let mut h = SHAKE128::new(); + h.do_update(b"abc"); + assert_eq!(h.do_final_out(&mut buf), 32); + assert_eq!(buf, hash_view, "do_final_out must agree with hash_out"); + + let mut buf = [0xFFu8; 100]; + let mut h = SHAKE128::new(); + h.do_update(b"abc"); + assert_eq!(h.do_final_partial_bits_out(0, 0, &mut buf).expect("0 is in range"), 32); + assert_eq!(buf, hash_view, "a zero-bit partial byte is the same call"); + + // A short buffer truncates, as it always did. + let mut short = [0xFFu8; 16]; + assert_eq!(SHAKE128::new().hash_out(b"abc", &mut short), 16); + assert_eq!(&short[..], &hash_view[..16], "a short buffer truncates the same output"); + + // The XOF spelling takes its length from the buffer and keeps reading past output_len. + let mut xof_view = [0xFFu8; 100]; + assert_eq!(SHAKE128::new().xof_out(b"abc", &mut xof_view), 100, "the XOF fills it"); + assert_eq!(&xof_view[..32], &hash_view[..32], "the same stream, read further"); + assert_ne!(&xof_view[32..], &[0u8; 68][..], "... rather than stopping at output_len"); + + // SHAKE256's nominal length is 64, so its split lands elsewhere. + let mut hash_view = [0xFFu8; 100]; + assert_eq!(SHAKE256::new().hash_out(b"abc", &mut hash_view), 64, "the nominal length"); + assert_eq!(&hash_view[64..], &[0u8; 36][..], "everything past output_len is zeroized"); + } + #[test] fn test_update_bytes() { for tc in read_test_vectors("SHAKETestVectors.txt") { diff --git a/crypto/sha3/tests/tuplehash_tests.rs b/crypto/sha3/tests/tuplehash_tests.rs index 295fef44..8395c13c 100644 --- a/crypto/sha3/tests/tuplehash_tests.rs +++ b/crypto/sha3/tests/tuplehash_tests.rs @@ -107,6 +107,8 @@ fn nist_sp800_185_tuplehashxof_sample_values() { for (i, v) in vectors.iter().enumerate() { let want = v.output_len / 8; let t = as_slices(&v.tuple); + // do_output is the XOF reading of the stream; do_final and the one-shots bind the length + // they are given, and are checked against the fixed-length samples elsewhere. let got = match v.strength { 128 => TUPLEHASHXOF128::new(v.s.as_bytes()).output_for(&t).do_output(want), 256 => TUPLEHASHXOF256::new(v.s.as_bytes()).output_for(&t).do_output(want), @@ -117,6 +119,117 @@ fn nist_sp800_185_tuplehashxof_sample_values() { println!("TupleHashXOF: {} sample values", vectors.len()); } +/// `do_final` as the first read binds `right_encode(L)`, so it computes fixed-length TupleHash. +/// +/// SP 800-185 s. 5.3 and s. 5.3.1 differ in one field: step 4 is `newX = z || right_encode(L)` for +/// TupleHash and `newX = z || right_encode(0)` for TupleHashXOF. The encoding therefore need not +/// be chosen until the caller says how it wants to read, and `do_final` as the first read says +/// both how many bytes it wants and that it will not be back -- which is exactly `L`. +/// +/// `TupleHash.rsp` and `TupleHashXOF.rsp` publish the same tuples, customization and lengths, so +/// the fixed-length file is what `do_final` has to match, byte for byte. +#[test] +fn do_final_binds_the_length_when_nothing_has_been_read() { + let (Some(fixed), Some(xof)) = + (read_vectors("TupleHash.rsp"), read_vectors("TupleHashXOF.rsp")) + else { + return; + }; + assert_eq!(fixed.len(), xof.len(), "the two sample files pair up"); + + for (i, (f, x)) in fixed.iter().zip(xof.iter()).enumerate() { + let t = as_slices(&f.tuple); + let ctx = format!("COUNT {i}: TupleHashXOF{} S={:?}", f.strength, f.s); + let s = f.s.as_bytes(); + match f.strength { + 128 => check_do_final_binds_length( + || TUPLEHASHXOF128::new(s), + |n| TUPLEHASH128::new(s, n).hash_tuple(&t), + &t, + &f.output, + &x.output, + &ctx, + ), + 256 => check_do_final_binds_length( + || TUPLEHASHXOF256::new(s), + |n| TUPLEHASH256::new(s, n).hash_tuple(&t), + &t, + &f.output, + &x.output, + &ctx, + ), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + + // `output_for` hands back the squeezer directly, so `do_final` on it is the first read by + // construction -- the shortest way to spell fixed-length TupleHash through the XOF type. + let n = f.output.len(); + let got = match f.strength { + 128 => TUPLEHASHXOF128::new(s).output_for(&t).do_final(n), + 256 => TUPLEHASHXOF256::new(s).output_for(&t).do_final(n), + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!(got, f.output, "{ctx}: output_for().do_final()"); + } + println!("TupleHashXOF do_final: {} sample values", fixed.len()); +} + +/// One paired sample through `do_final`. `fixed_expected` is the published fixed-length value, +/// `xof_expected` the published XOF value over the same tuple, and `fixed_of` computes the +/// fixed-length function at a length no vector covers. +fn check_do_final_binds_length( + make: impl Fn() -> X, + fixed_of: impl Fn(usize) -> Vec, + tuple: &[&[u8]], + fixed_expected: &[u8], + xof_expected: &[u8], + ctx: &str, +) { + let n = fixed_expected.len(); + assert_ne!(fixed_expected, xof_expected, "{ctx}: the two sample values must differ at all"); + let absorbed = || { + let mut x = make(); + tuple.iter().for_each(|element| x.do_update(element)); + x.into_squeezer() + }; + + // The first read, with no do_output before it: right_encode(8n), so the fixed-length function. + assert_eq!(absorbed().do_final(n), fixed_expected, "{ctx}: do_final binds the length"); + + // Pre-filled, so the documented zeroization is observable. + let mut buf = vec![0xFFu8; n]; + assert_eq!(absorbed().do_final_out(&mut buf), n, "{ctx}: do_final_out returns the length"); + assert_eq!(buf, fixed_expected, "{ctx}: do_final_out binds the length"); + + // The `L` bound is the length actually asked for, not a fixed one. No sample value covers + // these lengths, so the comparison is against this library's own fixed-length function. + for shorter in [n / 2, n - 1] { + assert_eq!(absorbed().do_final(shorter), fixed_of(shorter), "{ctx}: L = {shorter}"); + } + + // The one-shots name their length and never come back, so they bind it too. They take one + // tuple element, the last, after the rest have been fed in. + if let Some((last, rest)) = tuple.split_last() { + let mut x = make(); + rest.iter().for_each(|element| x.do_update(element)); + assert_eq!(x.xof(last, n), fixed_expected, "{ctx}: xof binds the length"); + + let mut buf = vec![0xFFu8; n]; + let mut x = make(); + rest.iter().for_each(|element| x.do_update(element)); + assert_eq!(x.xof_out(last, &mut buf), n, "{ctx}: xof_out returns the length"); + assert_eq!(buf, fixed_expected, "{ctx}: xof_out binds the length"); + } + + // Once a read has happened right_encode(0) is in the sponge and cannot be revised, so do_final + // after a do_output is the XOF stream continuing, not the fixed-length function. + let split = n / 2; + let mut squeezer = absorbed(); + let head = squeezer.do_output(split); + let tail = squeezer.do_final(n - split); + assert_eq!([head, tail].concat(), xof_expected, "{ctx}: do_final after a read stays the XOF"); +} + /// The two are different functions on identical inputs, as for KMAC. #[test] fn tuplehashxof_is_not_tuplehash_truncated() { @@ -263,32 +376,46 @@ fn check_fixed_view(make: impl Fn() -> H, tuple: &[&[u8]], expected: &[ assert_eq!(out, expected, "{ctx}: hash_out"); } -/// Every `Hash` and `XOF` entry point of the XOF form, against one sample value. The samples ask -/// for the nominal length, so `do_final` and `hash` must reproduce them exactly. -fn check_xof_view(make: impl Fn() -> X, tuple: &[&[u8]], expected: &[u8], ctx: &str) { +/// Every `Hash` and `XOF` entry point of the XOF form, against one paired sample value. +/// +/// The samples ask for the nominal length, and the `Hash` view is a final read at that length, so +/// it binds `L` and must reproduce the *fixed-length* sample; reading the stream with `do_output` +/// must reproduce the XOF one. +fn check_xof_view( + make: impl Fn() -> X, + tuple: &[&[u8]], + expected: &[u8], + fixed_expected: &[u8], + ctx: &str, +) { let n = expected.len(); assert_eq!(make().output_len(), n, "{ctx}: the samples ask for the nominal length"); + assert_eq!(fixed_expected.len(), n, "{ctx}: ... and the paired samples share it"); let mut x = make(); tuple.iter().for_each(|e| x.do_update(e)); - assert_eq!(x.do_final(), expected, "{ctx}: do_final"); + assert_eq!(x.do_final(), fixed_expected, "{ctx}: do_final"); let mut x = make(); tuple.iter().for_each(|e| x.do_update(e)); let mut out = vec![0u8; n]; assert_eq!(x.do_final_out(&mut out), n, "{ctx}: do_final_out returns the length"); - assert_eq!(out, expected, "{ctx}: do_final_out"); + assert_eq!(out, fixed_expected, "{ctx}: do_final_out"); // zero partial bits is the byte-aligned case and must be accepted; any other count refused let mut x = make(); tuple.iter().for_each(|e| x.do_update(e)); - assert_eq!(x.do_final_partial_bits(0, 0).unwrap(), expected, "{ctx}: do_final_partial_bits(0)"); + assert_eq!( + x.do_final_partial_bits(0, 0).unwrap(), + fixed_expected, + "{ctx}: do_final_partial_bits(0)" + ); let mut x = make(); tuple.iter().for_each(|e| x.do_update(e)); let mut out = vec![0u8; n]; assert_eq!(x.do_final_partial_bits_out(0, 0, &mut out).unwrap(), n, "{ctx}: ..._out length"); - assert_eq!(out, expected, "{ctx}: do_final_partial_bits_out(0)"); + assert_eq!(out, fixed_expected, "{ctx}: do_final_partial_bits_out(0)"); assert!(matches!(make().do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); let mut out = vec![0u8; n]; @@ -301,27 +428,32 @@ fn check_xof_view(make: impl Fn() -> X, tuple: &[&[u8]], expected: &[u8] let Some((last, rest)) = tuple.split_last() else { return }; let mut x = make(); rest.iter().for_each(|e| x.do_update(e)); - assert_eq!(x.hash(last), expected, "{ctx}: hash"); + assert_eq!(x.hash(last), fixed_expected, "{ctx}: hash"); let mut x = make(); rest.iter().for_each(|e| x.do_update(e)); let mut out = vec![0u8; n]; assert_eq!(x.hash_out(last, &mut out), n, "{ctx}: hash_out returns the length"); - assert_eq!(out, expected, "{ctx}: hash_out"); + assert_eq!(out, fixed_expected, "{ctx}: hash_out"); + // The XOF reading of the stream is do_output; the one-shots bind the length they are given, + // so they belong to `do_final_binds_the_length_when_nothing_has_been_read` instead. let mut x = make(); rest.iter().for_each(|e| x.do_update(e)); - assert_eq!(x.xof(last, n), expected, "{ctx}: xof"); + x.do_update(last); + assert_eq!(x.into_squeezer().do_output(n), expected, "{ctx}: do_output"); let mut x = make(); rest.iter().for_each(|e| x.do_update(e)); - assert_eq!(x.xof(last, n / 2), &expected[..n / 2], "{ctx}: xof, shorter"); + x.do_update(last); + assert_eq!(x.into_squeezer().do_output(n / 2), &expected[..n / 2], "{ctx}: do_output, shorter"); let mut x = make(); rest.iter().for_each(|e| x.do_update(e)); + x.do_update(last); let mut out = vec![0u8; n]; - assert_eq!(x.xof_out(last, &mut out), n, "{ctx}: xof_out returns the length"); - assert_eq!(out, expected, "{ctx}: xof_out"); + assert_eq!(x.into_squeezer().do_output_out(&mut out), n, "{ctx}: do_output_out length"); + assert_eq!(out, expected, "{ctx}: do_output_out"); } #[test] @@ -341,13 +473,20 @@ fn hash_trait_view_agrees_with_the_sample_values() { #[test] fn xof_trait_view_agrees_with_the_sample_values() { - let Some(vectors) = read_vectors("TupleHashXOF.rsp") else { return }; - for (i, v) in vectors.iter().enumerate() { + let (Some(fixed), Some(xof)) = + (read_vectors("TupleHash.rsp"), read_vectors("TupleHashXOF.rsp")) + else { + return; + }; + assert_eq!(fixed.len(), xof.len(), "the two sample files pair up"); + + for (i, (f, v)) in fixed.iter().zip(xof.iter()).enumerate() { let t = as_slices(&v.tuple); let ctx = format!("COUNT {i}: TupleHashXOF{}", v.strength); + let s = v.s.as_bytes(); match v.strength { - 128 => check_xof_view(|| TUPLEHASHXOF128::new(v.s.as_bytes()), &t, &v.output, &ctx), - 256 => check_xof_view(|| TUPLEHASHXOF256::new(v.s.as_bytes()), &t, &v.output, &ctx), + 128 => check_xof_view(|| TUPLEHASHXOF128::new(s), &t, &v.output, &f.output, &ctx), + 256 => check_xof_view(|| TUPLEHASHXOF256::new(s), &t, &v.output, &f.output, &ctx), other => panic!("COUNT {i}: unexpected strength {other}"), } } From 8a466833eefefb133314aecba9440d6be7f98d4d Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 14 Sep 2026 14:23:18 +1000 Subject: [PATCH 22/22] CLAUDE.md: record the cargo mutants mechanics this repo needs, since a bare run examines only the root package and finds nothing, the checked-in config's examine_globs silently overrides -f, crates whose mutants die in another crate's tests need --test-workspace, and without the /tmp/bc-test-data symlink the vector suites pass vacuously and their mutants all read as missed --- CLAUDE.md | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2a285ddc..247f7c8e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,9 +54,15 @@ Quality / mutation testing: ``` ./dev_scripts/quality_stats.sh ./crypto # lines-of-code, docstring & fallibility metrics; CI publishes this -cargo mutants # config in .cargo/mutants.toml (output: custom_mutants_output/) +cargo mutants -p bouncycastle-sha3 # config in .cargo/mutants.toml (output: custom_mutants_output/) ``` +`-p` is as non-optional here as `--workspace` is for build and test, and for the same reason: a bare +`cargo mutants` examines only the root `bouncycastle` package, whose single `src/lib.rs` yields no +mutants, so it prints "No mutants found under the active filters" and exits **0**. See +[the mutation-testing mechanics](#notes-on-testing) for scoping a run to one file, for crates whose +tests live elsewhere, and for the test-data symlink. + Stack-memory benches are separate binaries under `mem_usage_benches/src/`, each declared as a `[[bin]]` in that crate's `Cargo.toml`: @@ -158,38 +164,23 @@ Rules when working from the downloaded copy: - **Quote exactly, and locate precisely.** Comments and commit messages should name the document with its revision (e.g. "FIPS 203, Algorithm 13 (ML-KEM.Encaps_internal), step 2", "RFC 5869 Β§2.2"), and quote the spec verbatim where a quote is clearer than a paraphrase. Verify every section/algorithm/step number against the file you just downloaded β€” including numbers already present in the code, which may predate a spec revision. - **The specification is the source of truth for correct behaviour** β€” not the C/Java/Go implementation you have seen, not the BC Java or BC C# port, and not another crate. When an existing implementation appears to disagree with the spec, re-read the spec, and if the disagreement is real, follow the spec and note the discrepancy in the PR description rather than silently copying the other implementation. - **Optimizations are allowed, provided externally-visible behaviour is identical.** Restructuring loops, fusing steps, precomputing tables, constant-time rewrites, and in-place buffer reuse are all fine β€” the spec constrains observable outputs (and, for this library, timing behaviour on secret data), not the shape of the code. Any such deviation from the spec's literal steps gets a comment saying which spec steps it implements and why it is equivalent. -- **Test vectors come from the spec or its official companion files** (NIST CAVP / ACVP vectors, RFC test-vector appendices, the NIST "Examples with Intermediate Values" sample files). Never hand-write an "expected" value from recall. - -### Test vector data - -Vectors live in the **`bc-test-data`** repo, cloned alongside this one at `../bc-test-data`; suites read from it by relative path and print a warning and pass vacuously if it is absent (see `crypto/sha3/tests/cavp_tests.rs` for the pattern). Symlink it to `/tmp/bc-test-data` before running `cargo mutants`, whose build directories are elsewhere. - -- Commit the vectors there, not here, and not as PDFs β€” that repo holds `.rsp`, `.txt` and `.json`, and has no PDFs at all. Extract what a harness needs into the CAVP-style `.rsp` shape already used by `crypto/sha3/`. -- Every new directory gets a `README.md` giving provenance: upstream URL, licence or copyright status, retrieval date, and the SHA-256 of each source document so a refresh can be checked. `crypto/wycheproof/` and `crypto/sp800-185/` are the examples. -- **Validate an extraction against declared lengths, not just that it parses.** NIST sample-value PDFs split hex blocks across page boundaries, and the continuation line then begins with a form feed rather than spaces, so an "indented hex lines" pattern stops at the break and silently truncates. The result is still well-formed hex. Check each value against the length the file states (`Outputlen`, `Length of data is`, `Length of Key is`), and cross-check against BC Java's expected values where an equivalent test exists. +- **Test vectors come from the spec or its official companion files** (NIST CAVP / ACVP vectors, RFC test-vector appendices), downloaded the same way. Never hand-write an "expected" value from recall. ## Notes on testing What a crate must be tested against β€” including the mutation-testing expectation, the trait test framework, and the external vector suites β€” is specified in QUALITY_AND_STYLE.md and CONTRIBUTING.md. Repo-specific mechanics: -- `cargo mutants` is expected to be run on each crate; surviving mutants must be investigated but not all need to die (e.g. XOR/OR equivalences in crypto code are acceptable). Config lives in `.cargo/mutants.toml` (output dir `custom_mutants_output/`). +- `cargo mutants` is expected to be run on each crate; surviving mutants must be investigated but not all need to die (e.g. XOR/OR equivalences in crypto code are acceptable). Config lives in `.cargo/mutants.toml` (output dir `custom_mutants_output/`). Four things about running it here: + - **Always pass `-p `.** Without it only the root package is examined, which has no mutants, and the run "passes" vacuously β€” see [Common commands](#common-commands). + - **`-f`/`--file` does nothing while the checked-in config is in play**, because its `examine_globs` wins over the CLI filter: `cargo mutants -p bouncycastle-sha3 -f '**/kmac.rs'` still examines all ~874 mutants in the crate. To scope a run to the files you changed, copy `.cargo/mutants.toml` somewhere outside the repo, delete its `examine_globs` block, and pass `--config `; `-f` then filters as documented. (`--config /dev/null` also works but throws away `skip_calls`, `error_values`, `cap_lints` and the timeout multipliers with it.) + - **Add `--test-workspace true` when a crate's mutants are killed by another crate's tests.** The `core` traits are the case that matters: their default method bodies are exercised from `sha3` and `factory`, so a `-p bouncycastle-core` run alone reports them all as missed. + - **Symlink the test data into `/tmp`.** `cargo mutants` copies the tree to `/tmp/cargo-mutants-

-XXXX.tmp/`, so the `../../../bc-test-data/...` paths the vector suites use resolve to `/tmp/bc-test-data`. Without `ln -s /bc-test-data /tmp/bc-test-data` those tests print their "not found" warning, pass vacuously, and every mutant they would have killed is reported as missed. Use `--jobs 3` and an explicit `--timeout`; note that a mutant which makes a squeeze return no bytes hangs a fill loop for real, so some timeouts are kills rather than false alarms. - Integration tests in `tests/` are preferred over in-file `#[cfg(test)] mod tests` blocks β€” see "Unit tests vs integration tests" in QUALITY_AND_STYLE.md for the reasoning and the exceptions. A unit test is justified for high-risk code that has known-answer values and cannot be reached through the public API; when you write one, all of its helpers go inside that `mod tests`. - A property that can be asserted at compile time (`const _: () = assert!(...)`) stays a compile-time assertion even when a test also covers it: `cargo mutants` cannot see a const assertion fail, so pair the two rather than trading the guarantee for the coverage. -- Scoping a mutation run: **`--file` is silently ignored** by the installed cargo-mutants β€” it accepts the flag, filters nothing, and runs the whole package, so a run reported as covering one file may have covered the crate. Use **`-F `**, which matches the mutant names `--list` prints, and confirm the scope with `--list` first. `--test-workspace` needs an explicit value (`--test-workspace=true`), and is required whenever the mutated code is a `core` trait used by other crates. -- `--in-diff` finds nothing for a change that is mostly trait declarations, renamed call sites and documentation, because the executable code in impl bodies is unchanged. File-scoped runs are the useful gate for that shape of change; do not read "no mutants to filter" as "nothing to test". -- Behaviour-critical private functions can use in-file `#[cfg(test)] mod tests` blocks when they can't be exercised from outside the crate. - For traits in `core`, the canonical tests live in `core-test-framework` and are invoked from each implementor's integration tests β€” don't duplicate them per-implementation. - The per-width `impl Condition` blocks in `crypto/utils/src/ct.rs` (and their test modules) are deliberately duplicated rather than macro-generated: `cargo mutants` cannot see into `macro_rules!` bodies, so a macro would hide the mask identities from mutation testing. Do not fold them back into a macro. Any change to one width in a group (i64/i32, u64/u32) must be applied to every width in that group. -## Commit messages - -One-line subject only: no body, no "Squashed commits" list, and **no `Co-Authored-By` trailer**. This overrides the usual default of adding one. It applies on the release branches and on feature branches alike, so `git commit -m ""` is the whole of it β€” put in the subject what the body would have said. - -Subjects are `: `, and a change spanning several crates is normally split into one commit per crate, including that crate's factory and CLI wiring. Split only where each commit still builds: a trait change that every implementor must follow cannot be split that way and belongs in one commit. - -Do not strip `Co-Authored-By` from commits written in earlier sessions when rewording them during a rebase β€” that removes someone else's attribution. - ## CI The only workflow is `.github/workflows/publish_doc_benches_to_ghpages.yaml`: on every PR it builds rustdoc and runs `quality_stats.sh`; on `main` it additionally runs `cargo bench --all` and publishes docs, code stats, and benchmark results to GitHub Pages (`https://bcgit.github.io/bc-rust/`). There is no separate CI test/lint job β€” local `cargo test --workspace` is the gate, and nothing but a developer running it stands between a broken test and `main`. \ No newline at end of file