From b1a7731779c8d3db47be7351b53c8014b3a0b012 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 15:06:06 +1000 Subject: [PATCH 01/16] 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/cavp_tests.rs | 24 +- crypto/sha3/tests/shake_tests.rs | 227 +++-------- mem_usage_benches/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 8285227c..56c2d02d 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 4e26061b..494a31d8 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -53,7 +53,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: //!``` @@ -65,27 +65,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/cavp_tests.rs b/crypto/sha3/tests/cavp_tests.rs index 334bb6f9..147d7c91 100644 --- a/crypto/sha3/tests/cavp_tests.rs +++ b/crypto/sha3/tests/cavp_tests.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/bench_sha3_mem_usage.rs b/mem_usage_benches/bench_sha3_mem_usage.rs index e4155b61..9ed8d4d1 100644 --- a/mem_usage_benches/bench_sha3_mem_usage.rs +++ b/mem_usage_benches/bench_sha3_mem_usage.rs @@ -21,7 +21,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, }; @@ -81,9 +81,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); } @@ -91,9 +92,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 61a798a8e37ca127474c0d539886a9a9b1bdefa6 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 15:29:01 +1000 Subject: [PATCH 02/16] 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 11b645a1a611a760e16a195b93212cd35b66d217 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 17:19:46 +1000 Subject: [PATCH 03/16] 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 56c2d02d..ac60a9eb 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 52d88a7ae5b333312b35d11d79d87aecab4918b7 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 18:01:37 +1000 Subject: [PATCH 04/16] 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 494a31d8..4f136dd8 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -191,9 +191,11 @@ use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::traits::{Hash, KDF, Suspendable, XOF}; // end of doc-only imports +mod cshake; mod keccak; mod sha3; mod shake; +mod xof_utils; /*** String constants ***/ /// Algorithm name string for SHA3-224, as used by the factories and CLI. @@ -208,10 +210,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; @@ -338,6 +356,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)] @@ -349,6 +372,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 { @@ -366,6 +390,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 dfbd3f9f61f81a6e246e399971e1614dff739787 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 18:13:38 +1000 Subject: [PATCH 05/16] docs: record the cargo mutants scoping flags, the bc-test-data conventions and the commit message style in CLAUDE.md --- CLAUDE.md | 20 +++++++++++++++++++- crypto/sha3/tests/cshake_tests.rs | 17 +++++++++++------ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 47afcb05..79e867db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,15 +108,33 @@ 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 - `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/`). +- 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` is the gate. \ 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 68933c2f1e8104e3c02667ad29962a0d8c66715a Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 18:22:09 +1000 Subject: [PATCH 06/16] 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 581a70cb..516e7e05 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::hmac::{HMAC_SHA256, HMAC_SHA512, HMAC_SHA512_224, HMAC_SHA512_256, HMAC_SM3}; +use bouncycastle::sha3::{KMAC128, KMAC256}; #[allow(non_camel_case_types)] pub(crate) enum HMACVariant { @@ -18,14 +19,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() { @@ -41,6 +36,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 { @@ -67,6 +73,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 d5d415ab..83103c55 100644 --- a/crypto/factory/src/mac_factory.rs +++ b/crypto/factory/src/mac_factory.rs @@ -85,6 +85,7 @@ use bouncycastle_hmac::{ }; use bouncycastle_sha2 as sha2; use bouncycastle_sha3 as sha3; +use bouncycastle_sha3::{KMAC128, KMAC128_NAME, KMAC256, KMAC256_NAME}; use bouncycastle_sm3 as sm3; /*** Defaults ***/ @@ -102,6 +103,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(hmac::HMAC), /// @@ -145,6 +153,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(hmac::HMAC::::new(key)?)), HMAC_SHA256_NAME => Ok(Self::HMAC_SHA256(hmac::HMAC::::new(key)?)), HMAC_SHA384_NAME => Ok(Self::HMAC_SHA384(hmac::HMAC::::new(key)?)), @@ -181,6 +191,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(), @@ -197,6 +209,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), @@ -215,6 +229,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), @@ -231,6 +247,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), @@ -247,6 +265,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), @@ -263,6 +283,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(), @@ -281,6 +303,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), @@ -297,6 +321,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), @@ -313,6 +339,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 4f136dd8..b511ff8d 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -193,6 +193,7 @@ use bouncycastle_core::traits::{Hash, KDF, Suspendable, XOF}; mod cshake; mod keccak; +mod kmac; mod sha3; mod shake; mod xof_utils; @@ -214,9 +215,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. @@ -229,6 +235,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; @@ -361,6 +378,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)] @@ -373,6 +392,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 { @@ -391,6 +411,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 386b43d48291fefe04479cdf57e93dc5b4fe18f0 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 18:50:55 +1000 Subject: [PATCH 07/16] 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 ac60a9eb..8bcdf6fb 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 3c25e451463eed145a3566084bb6638b9fd16777 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 18:57:06 +1000 Subject: [PATCH 08/16] 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 b511ff8d..0ed25ab6 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -219,10 +219,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. @@ -240,12 +244,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; @@ -380,6 +394,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)] @@ -393,6 +409,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 { @@ -412,6 +429,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 e5b707bb6804f4a9a84806ef2425c4f94e3d89c5 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 19:01:01 +1000 Subject: [PATCH 09/16] 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 d2a103c34d76f33b67715b29e8e83530a630e90c Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 19:11:21 +1000 Subject: [PATCH 10/16] 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 0ed25ab6..5c2bcd73 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -196,6 +196,7 @@ mod keccak; mod kmac; mod sha3; mod shake; +mod tuplehash; mod xof_utils; /*** String constants ***/ @@ -223,11 +224,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. /// @@ -260,6 +270,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; @@ -396,6 +419,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)] @@ -410,6 +437,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 { @@ -430,6 +459,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 59c360fd7ad9d360325c44e9d69451633973958e Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 19:18:27 +1000 Subject: [PATCH 11/16] 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 5c2bcd73..d591c987 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -194,6 +194,7 @@ use bouncycastle_core::traits::{Hash, KDF, Suspendable, XOF}; mod cshake; mod keccak; mod kmac; +mod parallelhash; mod sha3; mod shake; mod tuplehash; @@ -232,10 +233,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}; @@ -283,6 +293,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; @@ -423,6 +445,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)] @@ -439,6 +465,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 { @@ -461,6 +489,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 4eb5eed1da4273dc844aeddbebe67d0847174ada Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 19:29:46 +1000 Subject: [PATCH 12/16] 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 7df50c44c98d0fa5f4bea3001454a358fc3c3f96 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 08:45:56 +1000 Subject: [PATCH 13/16] 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 5c45ae84fda2db6302f5332ffce9a5ff8303519b Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 08:45:56 +1000 Subject: [PATCH 14/16] 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 cb06141a9efdb901b3ef3d221c4092552a7aa87d Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 9 Sep 2026 15:45:58 +1000 Subject: [PATCH 15/16] 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 | 4 +-- 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, 89 insertions(+), 5 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 8bcdf6fb..168ef933 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 60f2d341..0a11273b 100644 --- a/crypto/sha2/src/lib.rs +++ b/crypto/sha2/src/lib.rs @@ -198,7 +198,7 @@ trait SHA2Params: HashAlgParams {} /// The SHA-256 family (SHA-224, SHA-256) shares one compression function and differs only in the /// initial hash value and the output truncation, so each member supplies its H(0) here. /// Private for the same reason as [`SHA2Params`]. -trait Sha256Family: SHA2Params { +trait Sha256Family: SHA2Params + Clone { /// The initial hash value H(0), FIPS 180-4 s. 5.3.2 / 5.3.3. const H0: [u32; 8]; } @@ -206,7 +206,7 @@ trait Sha256Family: SHA2Params { /// 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. /// Private for the same reason as [`SHA2Params`]. -trait Sha512Family: SHA2Params { +trait Sha512Family: SHA2Params + 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 d591c987..638c4267 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -325,7 +325,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 @@ -428,7 +428,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 4c7736e64a00f90842ee053e8e1739e64ec2c08e Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 10 Sep 2026 22:23:20 +1000 Subject: [PATCH 16/16] 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/cavp_tests.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/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 168ef933..a55fcd80 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 638c4267..3b48677a 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -67,8 +67,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 @@ -76,7 +76,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/cavp_tests.rs b/crypto/sha3/tests/cavp_tests.rs index 147d7c91..b3e312b2 100644 --- a/crypto/sha3/tests/cavp_tests.rs +++ b/crypto/sha3/tests/cavp_tests.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/bench_sha3_mem_usage.rs b/mem_usage_benches/bench_sha3_mem_usage.rs index 9ed8d4d1..f71eaebd 100644 --- a/mem_usage_benches/bench_sha3_mem_usage.rs +++ b/mem_usage_benches/bench_sha3_mem_usage.rs @@ -21,7 +21,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, };