From 391243473c216bf57051a3f6d823b38be6475ed7 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 16 Sep 2026 14:37:58 +1000 Subject: [PATCH 01/28] core: SecurityStrength::from_bits and from_bytes become const fn, so a parameter set can derive MAX_SECURITY_STRENGTH from a const generic instead of naming a variant by hand; no behaviour change, and the following commit is the first caller that needs it Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 --- crypto/core/src/traits.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 8285227c..7ad51967 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -982,7 +982,7 @@ impl TryFrom for SecurityStrength { impl SecurityStrength { /// Rounds down to the closest supported security strength. /// For example, 120-bits is rounded down to 112-bit. - pub fn from_bits(bits: usize) -> Self { + pub const fn from_bits(bits: usize) -> Self { if bits < 112 { Self::None } else if bits < 128 { @@ -998,7 +998,7 @@ impl SecurityStrength { /// Rounds down to the closest supported security strength. /// For example, 15 bytes (120-bits) is rounded down to 112-bit. - pub fn from_bytes(bytes: usize) -> Self { + pub const fn from_bytes(bytes: usize) -> Self { Self::from_bits(bytes * 8) } From 785dbef98263a82d033e7bd03eaea79df57fd6bf Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 16 Sep 2026 14:38:09 +1000 Subject: [PATCH 02/28] sha2: SHA512t becomes usable for every t FIPS 180-4 s. 5.3.6 defines a hash for, not just the two approved truncations, so the IV Generation Function regains the one- and two-digit decimal branches that b11f8f6 dropped as unreachable -- writing a fixed three digits gives the "0256" spelling the section forbids, and hence the wrong IV, for every t below 100; t is checked against the section's own rule (positive, below 512, not 384) plus this crate's multiple-of-8 requirement, which BC Java's SHA512tDigest also imposes, and the unapproved truncations carry SHA512tParams::FIPS_APPROVED = false which SHA512Internal::new asserts in an inline const, so reaching one takes new_allow_unapproved_t() the way an encrypting mode takes ENCRYPTION_APPROVED; ALG_NAME, OUTPUT_LEN and MAX_SECURITY_STRENGTH are now derived from t and pinned to their old values for t = 224 and t = 256; new sha512t_tests.rs cross-checks eight truncations spanning all three digit branches against BC Java, and of the file's 259 mutants 180 are caught, 5 die on timeout and the 5 missed are the XOR/OR equivalences already documented at their sites Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 --- crypto/sha2/src/lib.rs | 198 +++++++++++++++---- crypto/sha2/src/sha512.rs | 146 ++++++++++++-- crypto/sha2/tests/sha512t_tests.rs | 298 +++++++++++++++++++++++++++++ 3 files changed, 588 insertions(+), 54 deletions(-) create mode 100644 crypto/sha2/tests/sha512t_tests.rs diff --git a/crypto/sha2/src/lib.rs b/crypto/sha2/src/lib.rs index a54a5faa..3c1200a8 100644 --- a/crypto/sha2/src/lib.rs +++ b/crypto/sha2/src/lib.rs @@ -98,14 +98,26 @@ //! | Object | Size (bytes) | //! |----------------------------------------------------------|--------------| //! | `SHA224`, `SHA256` | 112 | -//! | `SHA384`, `SHA512`, `SHA512_224`, `SHA512_256` | 208 | +//! | `SHA384`, `SHA512`, `SHA512t` (incl. `SHA512_224`, `SHA512_256`) | 208 | //! | Suspended `SHA224`/`SHA256` state | 108 | -//! | Suspended `SHA384`/`SHA512`/`SHA512_224`/`SHA512_256` state | 204 | +//! | Suspended `SHA384`/`SHA512`/`SHA512t` state | 204 | +//! +//! `T` does not affect either size: the truncation happens on the way out of `do_final`, so every +//! member of the SHA-512 family carries the same 512-bit chaining value and 1024-bit buffer. //! //! # Security Considerations //! //! * SHA-224/256/384/512 offer 112/128/192/256 bits of collision resistance respectively; -//! SHA-512/224 and SHA-512/256 offer 112 and 128 bits (SP 800-107r1, Table 1 (§4.2)). +//! SHA-512/224 and SHA-512/256 offer 112 and 128 bits (SP 800-107r1, Table 1 (§4.2)). More +//! generally SHA-512/t offers t/2 bits, which is what [`SHA512t`]'s `MAX_SECURITY_STRENGTH` +//! reports, rounded down to a modelled level. +//! * **Only two SHA-512/t truncations are approved.** [`SHA512t`] is generic over `T`, but FIPS +//! 180-4 s. 5.3.6 approves only t = 224 and t = 256. Any other `T` is a well-defined hash that +//! is nonetheless unapproved, and has to be constructed through +//! [`SHA512Internal::new_allow_unapproved_t`](sha512::SHA512Internal::new_allow_unapproved_t) +//! rather than `new()`; see [`SHA512t`] for the reasoning and the compile-time gate. Small `t` +//! is also simply weak -- SHA-512/8 has a one-byte digest -- and carries a +//! `SecurityStrength::None`. //! * SHA-2 is a Merkle–Damgård construction and is therefore subject to length-extension: //! `H(k || m)` is not a secure MAC. Use HMAC (`bouncycastle-hmac`) for keyed hashing. //! * SHA-224, SHA-384, SHA-512/224 and SHA-512/256 are truncations of SHA-256 or SHA-512 with @@ -162,9 +174,68 @@ pub type SHA256 = SHA256Internal; pub type SHA384 = SHA512Internal; /// Public type for SHA512. pub type SHA512 = SHA512Internal; -/// Public type for the SHA-512/t truncating family (FIPS 180-4 s. 5.3.6): SHA-512 with a t-specific initial -/// hash value, truncated to `T` bits. Only the NIST-approved truncations `T = 224` and `T = 256` -/// can be instantiated, enforced by the sealing trait `SHA512InitValue`; see [`SHA512_224`] and [`SHA512_256`]. +/// Public type for the SHA-512/t truncating family (FIPS 180-4 s. 5.3.6): SHA-512 with a +/// t-specific initial hash value, truncated to `T` bits. +/// +/// `T` may be any truncation the standard defines a hash for -- "any positive integer without a +/// leading zero such that t < 512, and t is not 384" -- narrowed here to multiples of 8, since the +/// digest has to be a whole number of bytes. Anything else is a compile error naming the rule it +/// broke. The initial hash value is produced at compile time by the s. 5.3.6 IV Generation +/// Function, so a new `T` costs nothing at runtime and needs no table. +/// +/// ``` +/// use bouncycastle_core::traits::Hash; +/// use bouncycastle_sha2::{SHA512_256, SHA512t}; +/// +/// // An approved truncation: the ordinary constructor. +/// let digest = SHA512_256::new().hash(b"abc"); +/// assert_eq!(digest.len(), 32); +/// +/// // SHA512t<256> *is* SHA512_256. +/// assert_eq!(SHA512t::<256>::new().hash(b"abc"), digest); +/// ``` +/// +/// # Only `T = 224` and `T = 256` are approved +/// +/// FIPS 180-4 s. 5.3.6 approves exactly two truncations, SHA-512/224 and SHA-512/256 ("Other +/// SHA-512/t hash algorithms with different t values may be specified in [SP 800-107] in the +/// future as the need arises"). Every other `T` is a well-defined SHA-512/t but not an approved +/// hash algorithm, so it must not be used where an approved one is required. +/// +/// That distinction is enforced rather than merely documented, in the same shape as +/// `ElectronicCodeBook::ENCRYPTION_APPROVED` in the cipher traits: the unapproved truncations carry +/// [`SHA512tParams::FIPS_APPROVED`]` == false`, and +/// [`SHA512Internal::new`](sha512::SHA512Internal::new) checks it in an inline `const`. Building +/// one the ordinary way -- including through `Default`, and so through any generic code that +/// requires it -- is therefore a compile error at the call site, and +/// [`SHA512Internal::new_allow_unapproved_t`](sha512::SHA512Internal::new_allow_unapproved_t) is +/// the way to say you meant it: +/// +/// ``` +/// use bouncycastle_core::traits::{Algorithm, Hash}; +/// use bouncycastle_sha2::{SHA512t, SHA512tParams}; +/// +/// assert!(!SHA512tParams::<96>::FIPS_APPROVED); +/// let digest = SHA512t::<96>::new_allow_unapproved_t().hash(b""); +/// assert_eq!(digest.len(), 12); +/// assert_eq!( as Algorithm>::ALG_NAME, "SHA512/96"); +/// ``` +/// +/// ```compile_fail +/// use bouncycastle_sha2::SHA512t; +/// // SHA-512/96 is not an approved hash algorithm, so `new()` does not build. +/// let _ = SHA512t::<96>::new(); +/// ``` +/// +/// ```compile_fail +/// use bouncycastle_sha2::SHA512t; +/// // FIPS 180-4 s. 5.3.6: "t is not 384" -- SHA384 is its own algorithm with its own IV. +/// let _ = SHA512t::<384>::new_allow_unapproved_t(); +/// ``` +/// +/// See [`SHA512_224`] and [`SHA512_256`] for the approved pair, which are aliases of this type and +/// are additionally the only truncations with an assigned [`AlgorithmOID`] and a `HashFactory` +/// entry. pub type SHA512t = SHA512Internal>; /// Public type for SHA512/224 (FIPS 180-4 s. 6.6). pub type SHA512_224 = SHA512t<224>; @@ -189,6 +260,16 @@ trait SHA256InitValue: HashAlgParams { trait SHA512InitValue: HashAlgParams { /// The initial hash value H(0), FIPS 180-4 s. 5.3.4 / 5.3.5 / 5.3.6. const H0: [u64; 8]; + + /// Whether this parameter set is an approved hash algorithm. + /// + /// `true` for SHA-384, SHA-512 and the two approved truncations SHA-512/224 and SHA-512/256; + /// `false` for every other SHA-512/t, which FIPS 180-4 s. 5.3.6 defines but does not approve. + /// [`SHA512Internal::new`] checks this in an inline `const`, so constructing an unapproved + /// truncation the ordinary way is a compile error at the call site and + /// [`SHA512Internal::new_allow_unapproved_t`] is the deliberate way in -- the same shape as + /// `ElectronicCodeBook::ENCRYPTION_APPROVED` in the cipher traits. + const FIPS_APPROVED: bool = true; } /// The public hash types expose the same parameters as their `*Params` marker, so the constants @@ -297,53 +378,104 @@ impl SHA512InitValue for SHA512Params { /*** SHA-512/t ***/ /// The parameters for SHA-512/t (FIPS 180-4 s. 5.3.6), for a truncation of `T` bits. /// -/// The parameter traits are implemented only for the NIST-approved truncations `T = 224` and -/// `T = 256` ("Other SHA-512/t hash algorithms with different t values may be specified in -/// [SP 800-107] in the future as the need arises"), so any other `T` is a compile-time error. +/// Implemented for every `T` the section defines a hash for, with two restrictions checked when +/// the parameter set is instantiated, so a bad `T` is a compile error rather than a runtime one: +/// +/// * FIPS 180-4 s. 5.3.6's own rule, "t is any positive integer without a leading zero such that +/// t < 512, and t is not 384"; +/// * this crate's additional requirement that `T` be a multiple of 8, since the digest has to be a +/// whole number of bytes. See [`sha512::sha512t_h0`] for why. +/// +/// Only `T = 224` and `T = 256` are *approved* ("Other SHA-512/t hash algorithms with different t +/// values may be specified in [SP 800-107] in the future as the need arises"); the rest are +/// defined but unapproved, and are gated behind +/// [`SHA512Internal::new_allow_unapproved_t`](sha512::SHA512Internal::new_allow_unapproved_t). #[derive(Clone)] pub struct SHA512tParams; -/*** SHA512/224 ***/ -impl Algorithm for SHA512tParams<224> { - const ALG_NAME: &'static str = SHA512_224_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_112bit; +impl SHA512tParams { + /// Whether SHA-512/`T` is an approved hash algorithm: FIPS 180-4 s. 5.3.6 approves only + /// t = 224 and t = 256. + /// + /// This is what [`SHA512Internal::new`](sha512::SHA512Internal::new) gates on, so it is also + /// the answer to "does this truncation need + /// [`new_allow_unapproved_t`](sha512::SHA512Internal::new_allow_unapproved_t)?". Public so a + /// caller can make the same check -- `const { assert!(SHA512tParams::::FIPS_APPROVED) }` in + /// generic code -- without reaching into the sealed parameter trait. + pub const FIPS_APPROVED: bool = sha512::t_is_fips_approved(T); + + /// `"SHA512/t"` with `T` in decimal, NUL-padded; see [`Self::ALG_NAME_STR`]. + const ALG_NAME_BYTES: [u8; sha512::ALG_NAME_BUF_LEN] = sha512::alg_name_bytes(T); + + /// The algorithm name, e.g. `"SHA512/224"`. Built at compile time from `T` because a const + /// generic cannot be formatted into a `&'static str` directly. + const ALG_NAME_STR: &'static str = { + let bytes: &'static [u8; sha512::ALG_NAME_BUF_LEN] = &Self::ALG_NAME_BYTES; + let (name, _padding) = bytes.split_at(sha512::alg_name_len(T)); + match core::str::from_utf8(name) { + Ok(name) => name, + // unreachable: alg_name_bytes writes only ASCII. + Err(_) => panic!("SHA-512/t algorithm name is not UTF-8"), + } + }; +} + +impl Algorithm for SHA512tParams { + const ALG_NAME: &'static str = Self::ALG_NAME_STR; + /// SP 800-107 Rev 1 Table 1: a t-bit digest offers t/2 bits of collision resistance, rounded + /// down to a modelled level. This reproduces the values the two approved truncations carry: + /// 112-bit for SHA-512/224 and 128-bit for SHA-512/256. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::from_bits(T / 2); } -impl HashAlgParams for SHA512tParams<224> { - const OUTPUT_LEN: usize = 28; // FIPS 180-4 s. 6.6 exception 2: truncated to the left-most 224 bits +impl HashAlgParams for SHA512tParams { + /// FIPS 180-4 s. 6.6 / s. 6.7 exception 2: truncated to the left-most `T` bits. `T` is a + /// multiple of 8 (checked by [`sha512::check_t`]), so this is exact. + const OUTPUT_LEN: usize = T / 8; const BLOCK_LEN: usize = 128; // FIPS 180-4 Figure 1: block size 1024 bits } +impl SHA512InitValue for SHA512tParams { + /// FIPS 180-4 s. 5.3.6: H(0) from the IV Generation Function. For t = 224 and t = 256 this is + /// the value listed in s. 5.3.6.1 / s. 5.3.6.2, pinned against those words by + /// `tests/sha512t_h0_tests.rs`. + const H0: [u64; 8] = sha512t_h0(T); + // Not recursive: inherent associated consts win name resolution, so this is the public + // `SHA512tParams::::FIPS_APPROVED` above, forwarded so the two cannot disagree. + const FIPS_APPROVED: bool = Self::FIPS_APPROVED; +} + +// The two approved truncations get everything else from the generic impls above; only their +// object identifiers, which exist for no other t, are specific to them. + +/*** SHA512/224 ***/ /// Assigned by NIST in the Computer Security Objects Register: id-sha512-224 { hashAlgs 5 } impl AlgorithmOID for SHA512_224 { const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 2, 5]; const OID_DER: &'static [u8] = &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x05]; } -impl SHA512InitValue for SHA512tParams<224> { - // FIPS 180-4 s. 6.6 exception 1: H(0) as specified in s. 5.3.6.1 (pinned against the words - // listed there by tests/sha512t_h0_tests.rs). - const H0: [u64; 8] = sha512t_h0(224); -} /*** SHA512/256 ***/ -impl Algorithm for SHA512tParams<256> { - const ALG_NAME: &'static str = SHA512_256_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} -impl HashAlgParams for SHA512tParams<256> { - const OUTPUT_LEN: usize = 32; // FIPS 180-4 s. 6.7 exception 2: truncated to the left-most 256 bits - const BLOCK_LEN: usize = 128; // FIPS 180-4 Figure 1: block size 1024 bits -} /// Assigned by NIST in the Computer Security Objects Register: id-sha512-256 { hashAlgs 6 } impl AlgorithmOID for SHA512_256 { const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 2, 6]; const OID_DER: &'static [u8] = &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x06]; } -impl SHA512InitValue for SHA512tParams<256> { - // FIPS 180-4 s. 6.7 exception 1: H(0) as specified in s. 5.3.6.2 (pinned against the words - // listed there by tests/sha512t_h0_tests.rs). - const H0: [u64; 8] = sha512t_h0(256); -} + +// The generic name and output length must keep reproducing exactly what the two approved +// truncations had when they were spelled out by hand, and the two approved truncations must stay +// the only approved ones. `cargo mutants` cannot see a const assertion fail, so these are paired +// with the runtime coverage in tests/sha512t_tests.rs rather than replacing it. +const _: () = assert!(matches!(SHA512tParams::<224>::ALG_NAME_STR.as_bytes(), b"SHA512/224")); +const _: () = assert!(matches!(SHA512tParams::<256>::ALG_NAME_STR.as_bytes(), b"SHA512/256")); +const _: () = assert!(matches!(SHA512_224_NAME.as_bytes(), b"SHA512/224")); +const _: () = assert!(matches!(SHA512_256_NAME.as_bytes(), b"SHA512/256")); +const _: () = assert!(SHA512tParams::<224>::OUTPUT_LEN == 28); +const _: () = assert!(SHA512tParams::<256>::OUTPUT_LEN == 32); +const _: () = assert!(SHA512tParams::<224>::FIPS_APPROVED); +const _: () = assert!(SHA512tParams::<256>::FIPS_APPROVED); +const _: () = assert!(!SHA512tParams::<8>::FIPS_APPROVED); +const _: () = assert!(!SHA512tParams::<504>::FIPS_APPROVED); pub use sha256::SUSPENDED_SHA256_STATE_LEN; pub use sha512::SUSPENDED_SHA512_STATE_LEN; diff --git a/crypto/sha2/src/sha512.rs b/crypto/sha2/src/sha512.rs index 8ca2cf8a..f2811304 100644 --- a/crypto/sha2/src/sha512.rs +++ b/crypto/sha2/src/sha512.rs @@ -42,6 +42,72 @@ pub(crate) const SHA512_H0: [u64; 8] = [ 0x510E527FADE682D1, 0x9B05688C2B3E6C1F, 0x1F83D9ABFB41BD6B, 0x5BE0CD19137E2179, ]; +/// The truncations FIPS 180-4 s. 5.3.6 actually approves: "SHA-512/224 (t = 224) and SHA-512/256 +/// (t = 256) are approved hash algorithms. Other SHA-512/t hash algorithms with different t values +/// may be specified in [SP 800-107] in the future as the need arises." +pub(crate) const fn t_is_fips_approved(t: usize) -> bool { + t == 224 || t == 256 +} + +/// Rejects, at compile time, every `t` for which SHA-512/t is not defined or not representable +/// here. See [`sha512t_h0`] for where each rule comes from; the multiple-of-8 rule is this crate's, +/// the rest are FIPS 180-4 s. 5.3.6's. +pub(crate) const fn check_t(t: usize) { + // FIPS 180-4 s. 5.3.6: "t is any positive integer ... such that t < 512". + assert!(t > 0, "FIPS 180-4 s. 5.3.6: t must be a positive integer"); + assert!(t < 512, "FIPS 180-4 s. 5.3.6: t must be less than 512"); + // FIPS 180-4 s. 5.3.6: "and t is not 384". SHA-384 is its own algorithm (s. 5.3.4 / s. 6.5) + // with an IV that is not the one this function would generate. + assert!(t != 384, "FIPS 180-4 s. 5.3.6: t must not be 384 -- use SHA384 instead"); + // This crate's restriction, not the standard's: the digest must be a whole number of bytes. + assert!(t.is_multiple_of(8), "SHA-512/t here requires t to be a multiple of 8"); +} + +/// The number of decimal digits in `t`, i.e. the length of the "t" part of the ASCII string +/// "SHA-512/t" that FIPS 180-4 s. 5.3.6 hashes. `t < 512`, so one, two or three. +pub(crate) const fn t_digits(t: usize) -> usize { + if t >= 100 { + 3 + } else if t >= 10 { + 2 + } else { + 1 + } +} + +/// This crate's algorithm name for SHA-512/t, `"SHA512/t"` with `t` in decimal -- `"SHA512/224"`, +/// `"SHA512/256"`, `"SHA512/8"` -- returned NUL-padded to the longest form, with +/// [`alg_name_len`] giving the significant prefix. Two pieces because +/// [`Algorithm::ALG_NAME`](bouncycastle_core::traits::Algorithm::ALG_NAME) is a `&'static str` and +/// a const generic cannot size the buffer to the digit count. +/// +/// Note this is *not* the s. 5.3.6 spelling: the string the IV Generation Function hashes is +/// "SHA-512/t", with the hyphen, and is built separately in [`sha512t_h0`]. This one follows the +/// crate's existing names, [`SHA512_224_NAME`](crate::SHA512_224_NAME) and +/// [`SHA512_256_NAME`](crate::SHA512_256_NAME), which it has to keep reproducing exactly. +pub(crate) const fn alg_name_bytes(t: usize) -> [u8; ALG_NAME_BUF_LEN] { + let mut buf = [b'S', b'H', b'A', b'5', b'1', b'2', b'/', 0, 0, 0]; + let mut i = 7; + if t >= 100 { + buf[i] = b'0' + (t / 100) as u8; + i += 1; + } + if t >= 10 { + buf[i] = b'0' + ((t / 10) % 10) as u8; + i += 1; + } + buf[i] = b'0' + (t % 10) as u8; + buf +} + +/// Size of the [`alg_name_bytes`] buffer: `"SHA512/"` plus the most digits `t` can have. +pub(crate) const ALG_NAME_BUF_LEN: usize = 7 + 3; + +/// The significant length of [`alg_name_bytes`]'s output for `t`. +pub(crate) const fn alg_name_len(t: usize) -> usize { + 7 + t_digits(t) +} + /// FIPS 180-4 s. 5.3.6 "SHA-512/t IV Generation Function": computes the initial hash value H(0) /// for SHA-512/t. /// @@ -61,20 +127,18 @@ pub(crate) const SHA512_H0: [u64; 8] = [ /// and t is not 384", and "SHA-512/t" is the ASCII string with t written in decimal (so for t = 256 /// the message is the 11 bytes `53 48 41 2D 35 31 32 2F 32 35 36`). /// -/// Deliberate deviation from s. 5.3.6: only a three-digit t is accepted. The crate instantiates -/// only the two truncations FIPS 180-4 approves, t = 224 (s. 5.3.6.1) and t = 256 (s. 5.3.6.2), -/// and both are three digits, so the one- and two-digit cases of the decimal formatting would be -/// branches no caller and no test can reach. +/// Deliberate deviation from s. 5.3.6: `t` must additionally be a multiple of 8. The section +/// allows "any positive integer" below 512, including values that are not a whole number of bytes, +/// but [`Hash`](bouncycastle_core::traits::Hash) is byte-oriented -- `OUTPUT_LEN` is a byte count +/// and `do_final_out` writes whole bytes -- so a t of, say, 100 bits has no representable digest +/// here. BC Java's `SHA512tDigest` imposes the same restriction ("bitLength needs to be a multiple +/// of 8"), so the two libraries accept exactly the same set of truncations. /// -/// This is a `const fn` so that the IV is computed at compile time. +/// This is a `const fn` so that the IV is computed at compile time, which is also what makes the +/// rules above compile errors rather than panics: an unusable `t` fails the build at the point the +/// parameter set is instantiated. pub(crate) const fn sha512t_h0(t: usize) -> [u64; 8] { - // FIPS 180-4 s. 5.3.6 asks only for "any positive integer without a leading zero such that - // t < 512, and t is not 384"; the t >= 100 is ours, from the three-digit formatting below, so a - // new t under 100 fails the build rather than being written with a leading zero s. 5.3.6 forbids. - assert!( - t >= 100 && t < 512 && t != 384, - "sha512t_h0 formats t as three digits: need 100 <= t < 512 and t != 384" - ); + check_t(t); // FIPS 180-4 s. 5.3.6: H(0)'' = H(0)', the SHA-512 initial hash value (s. 5.3.5), with each word XOR a5a5a5a5a5a5a5a5. let mut h = SHA512_H0; @@ -84,8 +148,9 @@ pub(crate) const fn sha512t_h0(t: usize) -> [u64; 8] { i += 1; } - // FIPS 180-4 s. 5.3.6: the message is the ASCII string "SHA-512/t" (11 bytes, so one block). - // It is built directly in its padded form (s. 5.1.2) inside a single 1024-bit block (s. 5.2.2). + // FIPS 180-4 s. 5.3.6: the message is the ASCII string "SHA-512/t" (at most 11 bytes, so one + // block). It is built directly in its padded form (s. 5.1.2) inside a single 1024-bit block + // (s. 5.2.2). let mut block = [0u8; 128]; let prefix = b"SHA-512/"; let mut len = 0; @@ -93,13 +158,20 @@ pub(crate) const fn sha512t_h0(t: usize) -> [u64; 8] { block[len] = prefix[len]; len += 1; } - // FIPS 180-4 s. 5.3.6: t written in decimal "without a leading zero"; three digits, since - // 100 <= t < 512 (the assertion above), so "SHA-512/t" is the 11 characters of the s. 5.3.6 - // example for t = 256. - block[len] = b'0' + (t / 100) as u8; - block[len + 1] = b'0' + ((t / 10) % 10) as u8; - block[len + 2] = b'0' + (t % 10) as u8; - len += 3; + // FIPS 180-4 s. 5.3.6: t written in decimal "without a leading zero" ("t is 256, but not + // 0256"). t < 512, so one, two or three digits, and the leading digit is emitted only when it + // is significant -- writing a fixed three digits would produce the "0256" spelling the section + // forbids, and hence the wrong IV, for every t below 100. + if t >= 100 { + block[len] = b'0' + (t / 100) as u8; + len += 1; + } + if t >= 10 { + block[len] = b'0' + ((t / 10) % 10) as u8; + len += 1; + } + block[len] = b'0' + (t % 10) as u8; + len += 1; // FIPS 180-4 s. 5.1.2: append the bit "1", then k zero bits (the rest of the block is already zero). block[len] = 0x80; @@ -266,7 +338,39 @@ pub struct SHA512Internal { impl SHA512Internal { /// Creates a new SHA512 instance, ready for use. + /// + /// Restricted to parameter sets that are approved hash algorithms. Every member of the family + /// but SHA-512/t is one; for SHA-512/t only t = 224 and t = 256 are (FIPS 180-4 s. 5.3.6), so + /// any other truncation is a compile error here and has to be asked for by name through + /// [`new_allow_unapproved_t`](Self::new_allow_unapproved_t). pub fn new() -> Self { + const { + assert!( + PARAMS::FIPS_APPROVED, + "this SHA-512/t truncation is not FIPS 180-4 approved (only t = 224 and t = 256 are); \ + use SHA512Internal::new_allow_unapproved_t() if that is deliberate" + ) + }; + Self::construct() + } + + /// As [`new`](Self::new), but accepts the SHA-512/t truncations FIPS 180-4 s. 5.3.6 does not + /// approve. + /// + /// The IV Generation Function is defined for every `t` this crate accepts, and the resulting + /// hash is a perfectly well-formed SHA-512/t -- it is simply not one NIST has approved, so it + /// must not be used where an approved algorithm is required. Reaching for this constructor is + /// how that choice is made explicit; [`new`](Self::new) will not build for such a `t`, and + /// neither will anything that goes through `Default`, which keeps an unapproved truncation + /// from reaching generic code by accident. + /// + /// The `t` validity rules themselves are not relaxed: `t` must still be a positive multiple of + /// 8 below 512 and not 384, checked when the parameter set is instantiated. + pub fn new_allow_unapproved_t() -> Self { + Self::construct() + } + + fn construct() -> Self { Self { _params: core::marker::PhantomData, state: Sha512State::::new(), diff --git a/crypto/sha2/tests/sha512t_tests.rs b/crypto/sha2/tests/sha512t_tests.rs new file mode 100644 index 00000000..12d9f55a --- /dev/null +++ b/crypto/sha2/tests/sha512t_tests.rs @@ -0,0 +1,298 @@ +//! SHA-512/t (FIPS 180-4 s. 5.3.6) across the whole range of `t`, not just the two approved +//! truncations. +//! +//! `SHA512t` is instantiable for every `t` the standard defines a hash for -- any positive +//! multiple of 8 below 512 other than 384 -- so the IV Generation Function's decimal formatting of +//! `t` now has three reachable branches ("SHA-512/8", "SHA-512/96", "SHA-512/224") where it +//! previously only ever saw three-digit values. These tests cover all three. +//! +//! # Where the expected values come from +//! +//! FIPS 180-4 publishes H(0) for t = 224 and t = 256 only (s. 5.3.6.1 / s. 5.3.6.2, pinned by +//! `sha512t_h0_tests.rs`) and no digests at all for the unapproved truncations. The known-answer +//! values below were therefore generated with BC Java's `org.bouncycastle.crypto.digests +//! .SHA512tDigest`, an independent implementation of the same section, over the FIPS 180-4 +//! Appendix C sample messages. The two approved truncations are in the table as well, so a change +//! that broke the cross-check would have to break it consistently with the NIST-published values +//! for t = 224 and t = 256 to go unnoticed. +//! +//! A wrong H(0) for a given t changes every digest for that t, so these digests pin the IV +//! Generation Function -- including which decimal branch it took -- as well as the truncation. + +use bouncycastle_core::traits::{Algorithm, Hash, HashAlgParams, SecurityStrength}; +use bouncycastle_sha2::{SHA512_224, SHA512_256, SHA512t, SHA512tParams}; + +/// FIPS 180-4 Appendix C.1 / C.2 sample message. +const ABC: &[u8] = b"abc"; +/// FIPS 180-4 Appendix C.3 sample message (two-block). +const TWO_BLOCK: &[u8] = b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; + +fn from_hex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have an even length"); + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() +} + +/// Drives one `SHA512t` through the whole [`Hash`] surface and checks every route agrees with +/// `expected_hex`. +/// +/// `construct` is passed in because the ordinary constructor is gated: an unapproved truncation +/// has to come from `new_allow_unapproved_t()`, so the two cases cannot share one call. +fn check( + construct: impl Fn() -> H, + input: &[u8], + expected_hex: &str, +) { + let expected = from_hex(expected_hex); + assert_eq!( + expected.len(), + H::OUTPUT_LEN, + "{}: the expected value is {} bytes but OUTPUT_LEN is {}", + H::ALG_NAME, + expected.len(), + H::OUTPUT_LEN + ); + + /*** fn hash(self, data: &[u8]) -> Vec ***/ + assert_eq!(construct().hash(input), expected, "{}: hash()", H::ALG_NAME); + + /*** fn hash_out(self, data: &[u8], output: &mut [u8]) -> usize ***/ + let mut out = vec![0u8; H::OUTPUT_LEN]; + assert_eq!(construct().hash_out(input, &mut out), H::OUTPUT_LEN, "{}: hash_out()", H::ALG_NAME); + assert_eq!(out, expected, "{}: hash_out()", H::ALG_NAME); + + /*** streaming in one do_update, then do_final() ***/ + let mut h = construct(); + h.do_update(input); + assert_eq!(h.do_final(), expected, "{}: do_update + do_final", H::ALG_NAME); + + /*** streaming in one do_update, then do_final_out() ***/ + let mut h = construct(); + h.do_update(input); + let mut out = vec![0u8; H::OUTPUT_LEN]; + assert_eq!(h.do_final_out(&mut out), H::OUTPUT_LEN, "{}: do_final_out", H::ALG_NAME); + assert_eq!(out, expected, "{}: do_final_out", H::ALG_NAME); + + /*** chunked absorb must equal one-shot, at chunk sizes either side of the 128-byte block ***/ + for chunk_len in [1usize, 7, 64, 127, 128, 129] { + let mut h = construct(); + for chunk in input.chunks(chunk_len) { + h.do_update(chunk); + } + assert_eq!( + h.do_final(), + expected, + "{}: absorbing in {chunk_len}-byte chunks must equal the one-shot", + H::ALG_NAME + ); + } +} + +/// BC Java `SHA512tDigest` cross-check, for the truncations FIPS 180-4 s. 5.3.6 does not approve. +/// +/// The `t` values span all three decimal branches of the IV Generation Function's "SHA-512/t" +/// string: one digit (8), two digits (16, 24, 88, 96) and three (104, 264, 504). +macro_rules! unapproved_kat { + ($name:ident, $t:literal, $empty:literal, $abc:literal, $two_block:literal) => { + #[test] + fn $name() { + let make = || SHA512t::<$t>::new_allow_unapproved_t(); + check(make, b"", $empty); + check(make, ABC, $abc); + check(make, TWO_BLOCK, $two_block); + } + }; +} + +unapproved_kat!(sha512_t8, 8, "79", "c5", "8d"); +unapproved_kat!(sha512_t16, 16, "b44e", "1768", "e8d7"); +unapproved_kat!(sha512_t24, 24, "2f8a89", "1e17ce", "765639"); +unapproved_kat!( + sha512_t88, 88, "f0a49fbe063fd7fba2bf3b", "8194668ea596265aef4ef5", "c040324022ed56c0badf79" +); +unapproved_kat!( + sha512_t96, + 96, + "44ab9c7c3eb2da370d2c0ed7", + "67246fd8d90dca7009449ad5", + "c75100023425182c76253d0a" +); +unapproved_kat!( + sha512_t104, + 104, + "47f922a2d2508feb288af79a30", + "456045a75a5d7e0ea4af09dfce", + "64fc045733525b8c29376fc6be" +); +unapproved_kat!( + sha512_t264, + 264, + "78180c9a54d1c1f5bd3b941cfec4ee2cded5663ed7bf535ecd964518515174db49", + "888cfb35a25f524f8d17a1bb97134a9a6850b0ff269f1eb26ae038c22cd47f4c58", + "873b4bd852e7e441c406e49b1caa88f76bfc4b95d373f783350398db4b4a3e5909" +); +unapproved_kat!( + sha512_t504, + 504, + "6c46fed4cb277417c5f2d88b19a88a9a010e9e81a24d4a38d818c84a1aa3b88dd115f9550869eb097001fe0e8315b1d6f04124215f095e0be7ca94f99cdc6a", + "8c43e4bf1cad93067af1ad632ba38bba0b5673bf0129f01a469224c2d981b8ecaa301facf8e392f97efc5997885a1c90cefba70d81892f40267df4fd6fef9a", + "9f4bd94b6620e1ec80a9d4cfa315ee73f6228ee7f8fc8f58f232cc117c58633936b2df04f2cef341aae4f92f68c53223c1a631f5b6eb597c6933e0fc5f3f1a" +); + +/// The two approved truncations must keep producing exactly what they did before `SHA512t` became +/// generic, through the ordinary (ungated) constructor. These are the published SHA-512/224 and +/// SHA-512/256 values, and they agree with the same BC Java run that produced the table above. +#[test] +fn approved_truncations_are_unchanged() { + check(SHA512_224::new, b"", "6ed0dd02806fa89e25de060c19d3ac86cabb87d6a0ddd05c333b84f4"); + check(SHA512_224::new, ABC, "4634270f707b6a54daae7530460842e20e37ed265ceee9a43e8924aa"); + check(SHA512_224::new, TWO_BLOCK, "e5302d6d54bb242275d1e7622d68df6eb02dedd13f564c13dbda2174"); + + check(SHA512_256::new, b"", "c672b8d1ef56ed28ab87c3622c5114069bdd3ad7b8f9737498d0c01ecef0967a"); + check(SHA512_256::new, ABC, "53048e2681941ef99b2e29b76b4c7dabe4c2d0c634fc6d46e0e2f13107e7af23"); + check( + SHA512_256::new, + TWO_BLOCK, + "bde8e1f9f19bb9fd3406c90ec6bc47bd36d8ada9f11880dbc8a22a7078b6a461", + ); +} + +/// `SHA512t<224>` / `SHA512t<256>` and the named aliases are the same type, so the alias cannot +/// drift away from the generic parameter set. +#[test] +fn the_named_aliases_are_the_generic_type() { + fn same_type(_: &T, _: &T) {} + same_type(&SHA512_224::new(), &SHA512t::<224>::new()); + same_type(&SHA512_256::new(), &SHA512t::<256>::new()); +} + +/// A message spanning many blocks, to catch a `t` whose IV is right but whose multi-block path is +/// not. FIPS 180-4 Appendix C uses one million 'a' for exactly this. +#[test] +fn one_million_a() { + let million = vec![b'a'; 1_000_000]; + check(SHA512t::<8>::new_allow_unapproved_t, &million, "32"); + check(SHA512t::<96>::new_allow_unapproved_t, &million, "0e1f626963a870088bab77da"); + check(SHA512_224::new, &million, "37ab331d76f0d36de422bd0edeb22a28accd487b7a8453ae965dd287"); + check( + SHA512_256::new, + &million, + "9a59a052930187a97038cae692f30708aa6491923ef5194394dc68d56c74fb21", + ); + check( + SHA512t::<504>::new_allow_unapproved_t, + &million, + "f94e0eb099411d073274d87a908531ce7faa8591b28f56d86694e056ab0477f03af082453f5f44ec75c67ac58843fedd44429b0aa3322277b32b04e8a0586c", + ); +} + +/// The algorithm name is built from `T` at compile time; check every digit count, and that the two +/// approved truncations still spell themselves the way the crate's name constants do. +#[test] +fn alg_name_spells_t_in_decimal() { + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/8"); + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/16"); + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/96"); + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/104"); + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/224"); + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/256"); + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/504"); + + assert_eq!( as Algorithm>::ALG_NAME, bouncycastle_sha2::SHA512_224_NAME); + assert_eq!( as Algorithm>::ALG_NAME, bouncycastle_sha2::SHA512_256_NAME); + + // No leading zero and no trailing NUL from the fixed-size buffer the name is built in. + for name in [ + as Algorithm>::ALG_NAME, + as Algorithm>::ALG_NAME, + as Algorithm>::ALG_NAME, + ] { + let digits = name.strip_prefix("SHA512/").expect("name starts with SHA512/"); + assert!(digits.bytes().all(|b| b.is_ascii_digit()), "{name}: digits only"); + assert!(!digits.starts_with('0'), "{name}: FIPS 180-4 s. 5.3.6 forbids a leading zero"); + } +} + +/// `OUTPUT_LEN` is `T / 8`, exactly, for every accepted `T`. +#[test] +fn output_len_is_t_over_eight() { + assert_eq!( as HashAlgParams>::OUTPUT_LEN, 1); + assert_eq!( as HashAlgParams>::OUTPUT_LEN, 12); + assert_eq!( as HashAlgParams>::OUTPUT_LEN, 28); + assert_eq!( as HashAlgParams>::OUTPUT_LEN, 32); + assert_eq!( as HashAlgParams>::OUTPUT_LEN, 63); + + // BLOCK_LEN does not vary with t: FIPS 180-4 Figure 1, block size 1024 bits. + assert_eq!( as HashAlgParams>::BLOCK_LEN, 128); + assert_eq!( as HashAlgParams>::BLOCK_LEN, 128); + assert_eq!(SHA512t::<8>::new_allow_unapproved_t().block_bitlen(), 1024); +} + +/// Collision resistance is t/2 bits, rounded down to a modelled level, and the two approved +/// truncations keep the strengths they were given by hand. +#[test] +fn security_strength_is_half_of_t() { + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::None); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::None); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::_192bit); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::_192bit); + + // and the instance method agrees with the associated const + assert_eq!( + SHA512_224::new().max_security_strength(), + as Algorithm>::MAX_SECURITY_STRENGTH + ); + assert_eq!( + SHA512t::<504>::new_allow_unapproved_t().max_security_strength(), + as Algorithm>::MAX_SECURITY_STRENGTH + ); +} + +/// Only t = 224 and t = 256 are approved (FIPS 180-4 s. 5.3.6). `FIPS_APPROVED` is what +/// `SHA512Internal::new` gates on, so it decides which truncations need +/// `new_allow_unapproved_t()`; the compile-time half of this is the `const _` assertions in +/// `lib.rs`, which `cargo mutants` cannot see fail. +#[test] +fn only_224_and_256_are_approved() { + // Looped rather than asserted one by one so the value reaches the assertion through a binding: + // `assert!(SHA512tParams::<224>::FIPS_APPROVED)` is a constant, which clippy's + // `assertions_on_constants` would have us fold into a `const` block -- and a const assertion is + // exactly what `cargo mutants` cannot see fail. The const-block half already exists in lib.rs; + // this is the half that has to stay observable at runtime. + for approved in [SHA512tParams::<224>::FIPS_APPROVED, SHA512tParams::<256>::FIPS_APPROVED] { + assert!(approved, "t = 224 and t = 256 are FIPS 180-4 approved"); + } + + for approved in [ + SHA512tParams::<8>::FIPS_APPROVED, + SHA512tParams::<16>::FIPS_APPROVED, + SHA512tParams::<96>::FIPS_APPROVED, + SHA512tParams::<104>::FIPS_APPROVED, + SHA512tParams::<216>::FIPS_APPROVED, + SHA512tParams::<232>::FIPS_APPROVED, + SHA512tParams::<248>::FIPS_APPROVED, + SHA512tParams::<264>::FIPS_APPROVED, + SHA512tParams::<504>::FIPS_APPROVED, + ] { + assert!(!approved, "only t = 224 and t = 256 are FIPS 180-4 approved"); + } +} + +/// A shorter output buffer truncates and a longer one is zero-filled past the digest, for a +/// generic `t` as much as for the approved ones. +#[test] +fn output_buffer_shorter_and_longer_than_the_digest() { + let full = from_hex("44ab9c7c3eb2da370d2c0ed7"); // SHA512/96("") + + let mut short = [0u8; 5]; + assert_eq!(SHA512t::<96>::new_allow_unapproved_t().hash_out(b"", &mut short), 5); + assert_eq!(short, full[..5]); + + let mut long = [0xAAu8; 20]; + assert_eq!(SHA512t::<96>::new_allow_unapproved_t().hash_out(b"", &mut long), 12); + assert_eq!(&long[..12], &full[..]); + assert_eq!(&long[12..], &[0u8; 8], "past the digest the buffer is zero-filled"); +} From 4053f215208609baa05a6a237f57443a35743e1a Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 15:06:06 +1000 Subject: [PATCH 03/28] core, sha3: XOF extends Hash, so SHAKE128 and SHAKE256 are hashes; squeezing becomes its own type --- cli/src/sha3_cmd.rs | 7 +- crypto/core-test-framework/src/xof.rs | 386 ++++++++---------- crypto/core/src/traits.rs | 139 +++---- crypto/factory/src/xof_factory.rs | 162 ++++++-- crypto/mldsa-lowmemory/src/aux_functions.rs | 36 +- crypto/mldsa-lowmemory/src/hash_mldsa.rs | 34 +- crypto/mldsa-lowmemory/src/mldsa.rs | 42 +- crypto/mldsa-lowmemory/src/mldsa_keys.rs | 17 +- crypto/mldsa-lowmemory/tests/bc_test_data.rs | 19 +- crypto/mldsa-lowmemory/tests/mldsa_tests.rs | 9 +- crypto/mldsa/src/aux_functions.rs | 37 +- crypto/mldsa/src/hash_mldsa.rs | 34 +- crypto/mldsa/src/matrix.rs | 4 +- crypto/mldsa/src/mldsa.rs | 76 ++-- crypto/mldsa/tests/bc_test_data.rs | 16 +- crypto/mldsa/tests/mldsa_tests.rs | 9 +- crypto/mlkem-lowmemory/src/aux_functions.rs | 25 +- crypto/mlkem-lowmemory/src/mlkem.rs | 8 +- crypto/mlkem-lowmemory/tests/mlkem_tests.rs | 12 +- crypto/mlkem/src/aux_functions.rs | 25 +- crypto/mlkem/src/mlkem.rs | 8 +- crypto/mlkem/tests/mlkem_tests.rs | 12 +- crypto/sha3/src/lib.rs | 30 +- crypto/sha3/src/shake.rs | 325 ++++++++++----- crypto/sha3/tests/bc-test-data.rs | 24 +- crypto/sha3/tests/shake_tests.rs | 227 +++------- mem_usage_benches/src/bench_sha3_mem_usage.rs | 12 +- 27 files changed, 902 insertions(+), 833 deletions(-) diff --git a/cli/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index b6107e0c..b620e9c1 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -1,4 +1,4 @@ -use bouncycastle::core::traits::{Hash, XOF}; +use bouncycastle::core::traits::{Hash, XOF, XofOutput}; use std::io; use std::io::{Read, Write}; @@ -49,11 +49,12 @@ fn do_shake(mut shake: impl XOF, output_len: usize, output_hex: bool) { // read from stdin let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); while bytes_read != 0 { - shake.absorb(&buf[..bytes_read]).expect("absorb before squeeze is infallible"); + shake.do_update(&buf[..bytes_read]); bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); } - let out = shake.squeeze(output_len); + let mut shake = shake.into_output(); + let out = shake.do_output(output_len); if output_hex { for b in out.iter() { print!("{b:02x}"); diff --git a/crypto/core-test-framework/src/xof.rs b/crypto/core-test-framework/src/xof.rs index fbbe7006..46edb466 100644 --- a/crypto/core-test-framework/src/xof.rs +++ b/crypto/core-test-framework/src/xof.rs @@ -1,12 +1,12 @@ //! Generic behaviour tests for anything that implements [`XOF`]. use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{XOF, XofOutput}; /// Instance of the test framework. pub struct TestFrameworkXOF { // Put any config options here - /// Can be disabled for XOFs that don't implement [`XOF::absorb_last_partial_byte`]. + /// Can be disabled for XOFs that don't support a partial final byte of input. pub enable_partial_byte_tests: bool, } @@ -16,239 +16,199 @@ impl TestFrameworkXOF { Self { enable_partial_byte_tests: true } } - /// Test the absorb-after-squeeze members of trait XOF against the given input-output pair. - /// This is not exhaustive; it covers the rules laid out in the "State and Absorb-after-Squeeze" - /// section of the [`XOF`] docs: an XOF is an absorb phase followed by a squeeze phase, once - /// squeezing has begun any further absorb returns [`HashError::InvalidState`], and a rejected - /// absorb leaves the object usable for further squeezing. - /// `expected_output` is the result of squeezing `expected_output.len()` bytes after absorbing - /// `input`. + /// Exercises the trait against a known input-output pair. + /// + /// `expected_output` is the result of reading `expected_output.len()` bytes after absorbing + /// `input`. There is deliberately no absorb-after-squeeze test: [`XOF::into_output`] consumes + /// the XOF, so absorbing afterwards is not expressible and there is no runtime rule left to + /// check. That guarantee is asserted instead by `compile_fail` doctests on the implementors. pub fn test_xof(&self, input: &[u8], expected_output: &[u8]) { - /*** fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> ***/ - // Absorbing is fine, repeatedly, right up until the first squeeze. + /*** fn do_update(&mut self, data: &[u8]) ***/ + // Feeding the input in pieces must equal feeding it in one go. let mut xof = X::default(); for chunk in input.chunks(16) { - xof.absorb(chunk).expect("absorb() before any squeeze must succeed"); + xof.do_update(chunk); } + assert_eq!( + xof.into_output().do_output(expected_output.len()), + expected_output, + "chunked input must equal a single update" + ); - // "once the XOF has begun squeezing, attempting to absorb more will return - // HashError::InvalidState" - // squeeze() begins squeezing ... + /*** fn do_output(&mut self, num_bytes: usize) -> Vec ***/ let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(expected_output.len()); - assert!( - matches!(xof.absorb(b"more input"), Err(HashError::InvalidState(_))), - "absorb() after squeeze() must return InvalidState" + xof.do_update(input); + assert_eq!( + xof.into_output().do_output(expected_output.len()), + expected_output, + "do_output must produce the expected bytes" ); - // ... and so does squeeze_out() + /*** fn do_output_out(&mut self, output: &mut [u8]) -> usize ***/ + // Pre-filled so that the documented zeroization is observable. + let mut output = vec![0xFFu8; expected_output.len()]; let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let mut output = vec![0u8; expected_output.len()]; - xof.squeeze_out(&mut output); - assert!( - matches!(xof.absorb(b"more input"), Err(HashError::InvalidState(_))), - "absorb() after squeeze_out() must return InvalidState" - ); + xof.do_update(input); + let n = xof.into_output().do_output_out(&mut output); + assert_eq!(n, expected_output.len(), "do_output_out must report what it wrote"); + assert_eq!(output, expected_output, "do_output_out must agree with do_output"); - /*** fn squeeze(&mut self, num_bytes: usize) -> Vec ***/ - /*** fn squeeze_out(&mut self, output: &mut [u8]) -> usize ***/ - // "... and leave the object usable for further squeezing" - // So squeezing the output in two halves around a rejected absorb must give exactly the same - // stream as one clean squeeze: a rejected absorb must not consume, pad, or otherwise - // disturb the sponge. + // One output stream: reading it in two goes equals reading it in one. let split = expected_output.len() / 2; - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let first_half = xof.squeeze(split); - assert!(xof.absorb(b"more input").is_err()); - let mut second_half = vec![0u8; expected_output.len() - split]; - xof.squeeze_out(&mut second_half); - + xof.do_update(input); + let mut out = xof.into_output(); + let first = out.do_output(split); + let mut second = vec![0u8; expected_output.len() - split]; + out.do_output_out(&mut second); assert_eq!( - first_half.as_slice(), - &expected_output[..split], - "Incorrect output for input / the output stream must be unchanged by a rejected absorb" + [first, second].concat(), + expected_output, + "successive reads must continue one stream" ); + + /*** fn hash_xof(self, data: &[u8], result_len: usize) -> Vec ***/ assert_eq!( - second_half.as_slice(), - &expected_output[split..], - "Incorrect output for input / the output stream must continue as if the rejected absorb never happened" + X::default().hash_xof(input, expected_output.len()), + expected_output, + "the one-shot must equal update-then-output" ); + let mut output = vec![0xFFu8; expected_output.len()]; + let n = X::default().hash_xof_out(input, &mut output); + assert_eq!(n, expected_output.len()); + assert_eq!(output, expected_output, "hash_xof_out must agree with hash_xof"); + + /*** the Hash half: a XOF is a hash ***/ + self.test_xof_as_hash::(input, expected_output); + if self.enable_partial_byte_tests { - /*** fn absorb_last_partial_byte(&mut self, partial_byte: u8, num_bits: usize) -> Result<(), HashError> ***/ - // The same phase rule applies to absorb_last_partial_byte() once squeezing has begun. + self.test_xof_partial_bits::(input, expected_output); + } + } + + /// The inherited [`Hash`] surface. `XOF: Hash`, so SHAKE can be used wherever a hash is wanted; + /// these checks pin that the inherited methods agree with the XOF ones. + fn test_xof_as_hash(&self, input: &[u8], expected_output: &[u8]) { + let xof = X::default(); + let output_len = xof.output_len(); + assert!(output_len > 0, "output_len must be positive"); + assert!(xof.block_bitlen() > 0, "block_bitlen must be positive"); + assert!( + xof.block_bitlen().is_multiple_of(8), + "block_bitlen must be a whole number of bytes" + ); + + // do_final is do_output at the nominal length: the same stream, truncated. + let mut a = X::default(); + a.do_update(input); + let via_hash = a.do_final(); + assert_eq!(via_hash.len(), output_len, "do_final must produce output_len bytes"); + + let mut b = X::default(); + b.do_update(input); + assert_eq!( + via_hash, + b.into_output().do_output(output_len), + "do_final must equal do_output(output_len)" + ); + + // ... and it is a prefix of the longer output, because a XOF cannot diversify by length. + if expected_output.len() >= output_len { + assert_eq!( + &via_hash[..], + &expected_output[..output_len], + "do_final must be a prefix of the longer output" + ); + } + + // do_final_out fills the caller's buffer, zeroizing it first. + let mut buf = vec![0xFFu8; output_len]; + let mut c = X::default(); + c.do_update(input); + let n = c.do_final_out(&mut buf); + assert_eq!(n, output_len); + assert_eq!(buf, via_hash, "do_final_out must agree with do_final"); + + // The one-shot Hash entry points. + assert_eq!(X::default().hash(input), via_hash, "hash must equal update-then-do_final"); + let mut buf = vec![0xFFu8; output_len]; + assert_eq!(X::default().hash_out(input, &mut buf), output_len); + assert_eq!(buf, via_hash, "hash_out must agree with hash"); + } + + /// A partial final byte of input, in both the XOF and the Hash spelling. + fn test_xof_partial_bits(&self, input: &[u8], expected_output: &[u8]) { + // num_bits = 0 means the message ended on a byte boundary, so it must match plain input. + let mut xof = X::default(); + xof.do_update(input); + assert_eq!( + xof.into_output_partial_bits(0, 0) + .expect("0 is in range") + .do_output(expected_output.len()), + expected_output, + "num_bits = 0 must equal a byte-aligned message" + ); + + // A real partial byte must change the output, and both spellings must agree. + for num_bits in 1..=7usize { + let mut a = X::default(); + a.do_update(input); + let with_bits = a + .into_output_partial_bits(0xFE, num_bits) + .expect("num_bits is in 1..=7") + .do_output(expected_output.len()); + assert_ne!( + with_bits, expected_output, + "a partial byte must change the output / num_bits: {num_bits}" + ); + + let mut b = X::default(); + b.do_update(input); + let via_hash = b.do_final_partial_bits(0xFE, num_bits).expect("num_bits is in 1..=7"); + assert_eq!( + via_hash, + with_bits[..via_hash.len()], + "do_final_partial_bits must be the same stream / num_bits: {num_bits}" + ); + + let mut buf = vec![0xFFu8; via_hash.len()]; + let mut c = X::default(); + c.do_update(input); + let n = c + .do_final_partial_bits_out(0xFE, num_bits, &mut buf) + .expect("num_bits is in 1..=7"); + assert_eq!(n, via_hash.len()); + assert_eq!(buf, via_hash, "the _out form must agree / num_bits: {num_bits}"); + } + + // "num_bits must be in 0..=7; larger values return HashError::InvalidLength." + for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] { let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(expected_output.len()); + xof.do_update(input); assert!( - matches!(xof.absorb_last_partial_byte(0x01, 3), Err(HashError::InvalidState(_))), - "absorb_last_partial_byte() after squeeze() must return InvalidState" + matches!( + xof.into_output_partial_bits(0xFF, num_bits), + Err(HashError::InvalidLength(_)) + ), + "into_output_partial_bits must reject num_bits = {num_bits}" ); - // "Unlike XOF::absorb, this switches the XOF from Absorbing mode into Squeezing mode - // because absorbing more input after absorbing a partial byte is undefined - // behaviour." - // So it leaves the object in the same state a squeeze does, for every valid num_bits, - // with no squeeze having happened at all. - for num_bits in 0..=7 { - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - xof.absorb_last_partial_byte(0xFF, num_bits) - .expect("absorb_last_partial_byte() must succeed for num_bits in 0..=7"); - let expected_partial_output = xof.squeeze(expected_output.len()); - - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - xof.absorb_last_partial_byte(0xFF, num_bits) - .expect("absorb_last_partial_byte() must succeed for num_bits in 0..=7"); - - assert!( - matches!(xof.absorb(b"more input"), Err(HashError::InvalidState(_))), - "absorb() after absorb_last_partial_byte() must return InvalidState / num_bits: {num_bits}" - ); - assert!( - matches!( - xof.absorb_last_partial_byte(0xFF, num_bits), - Err(HashError::InvalidState(_)) - ), - "a second absorb_last_partial_byte() must return InvalidState / num_bits: {num_bits}" - ); - - // ... and, again, the rejections must leave the object usable for further squeezing. - assert_eq!( - xof.squeeze(expected_output.len()), - expected_partial_output, - "the output stream must be unchanged by a rejected absorb / num_bits: {num_bits}" - ); - } - - // Helper: the output stream of `input` finished with the top `num_bits` bits of - // `partial_byte`. - let partial_absorb_output = |partial_byte: u8, num_bits: usize| -> Vec { - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - xof.absorb_last_partial_byte(partial_byte, num_bits) - .expect("absorb_last_partial_byte() must succeed for num_bits in 0..=7"); - xof.squeeze(expected_output.len()) - }; - - // "0 is a valid value and means the message ends on a byte boundary (equivalent to - // XOF::absorb)." - // So the message is still just `input`, whatever the discarded bits of partial_byte are. - for partial_byte in [0x00u8, 0x01, 0x80, 0xA5, 0xFF] { - assert_eq!( - partial_absorb_output(partial_byte, 0), - expected_output, - "num_bits = 0 must leave the message byte-aligned / partial_byte: {partial_byte:#04X}" - ); - } - - // "the num_bits message bits are the most significant bits of partial_byte ... and the - // low 8 - num_bits bits (the BIT STRING's "unused bits") are ignored". - // So the unused low bits are not part of the message and must not change the output. - for num_bits in 0..=7 { - // the used bits are the top num_bits; built in u16 so that num_bits == 0 cannot overflow - let mask = (0xFF00u16 >> num_bits) as u8; - for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] { - assert_eq!( - partial_absorb_output(partial_byte, num_bits), - partial_absorb_output(partial_byte & mask, num_bits), - "the low 8 - num_bits = {} bits must be ignored / partial_byte: {partial_byte:#04X}", - 8 - num_bits - ); - } - } - - // "num_bits must be in 0..=7; larger values return HashError::InvalidLength." - // Checked on an absorbing object, so that it is the range check rejecting the call and - // not the phase check above. - for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] { - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - assert!( - matches!( - xof.absorb_last_partial_byte(0xFF, num_bits), - Err(HashError::InvalidLength(_)) - ), - "absorb_last_partial_byte() must reject num_bits = {num_bits} with InvalidLength" - ); - } - - /*** fn squeeze_partial_byte_final(self, num_bits: usize) -> Result ***/ - /*** fn squeeze_partial_byte_final_out(self, num_bits: usize, output: &mut u8) -> Result<(), HashError> ***/ - // "in the most significant num_bits bits of the returned u8, first output bit first, with - // the low 8 - num_bits "unused" bits zero." - // They are the first bits of the next byte of the output stream, which `expected_output` - // gives us: after squeezing `split` bytes, the next byte is expected_output[split]. In - // that byte the first output bit is the LSB (FIPS 202 B.1 / the byte-oriented stream), so - // the expected partial byte is the bit-reversal of it, masked to the top num_bits bits. - let split = expected_output.len() / 2; - for num_bits in 0..=7 { - // the used bits are the top num_bits; built in u16 so that num_bits == 0 cannot overflow - let mask = (0xFF00u16 >> num_bits) as u8; - - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(split); - let partial_byte = xof - .squeeze_partial_byte_final(num_bits) - .expect("squeeze_partial_byte_final() must succeed for num_bits in 0..=7"); - - assert_eq!( - partial_byte, - expected_output[split].reverse_bits() & mask, - "the squeezed bits must be the first bits of the next output byte, MSB-first / num_bits: {num_bits}" - ); - assert_eq!( - partial_byte & !mask, - 0x00, - "the unused low bits of the result must be zero / num_bits: {num_bits}" - ); - - // "The same as XOF::squeeze_partial_byte_final, but writes into the provided output - // byte. The output byte is zeroized before the result is written." - // Pre-filled with 0xFF so that the zeroization is observable. - let mut output_byte = 0xFFu8; - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(split); - xof.squeeze_partial_byte_final_out(num_bits, &mut output_byte) - .expect("squeeze_partial_byte_final_out() must succeed for num_bits in 0..=7"); - assert_eq!( - output_byte, partial_byte, - "squeeze_partial_byte_final_out() must agree with squeeze_partial_byte_final() / num_bits: {num_bits}" - ); - } - - // "num_bits must be in 0..=7; larger values return HashError::InvalidLength." - for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] { - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(split); - assert!( - matches!( - xof.squeeze_partial_byte_final(num_bits), - Err(HashError::InvalidLength(_)) - ), - "squeeze_partial_byte_final() must reject num_bits = {num_bits} with InvalidLength" - ); - - let mut output_byte = 0u8; - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(split); - assert!( - matches!( - xof.squeeze_partial_byte_final_out(num_bits, &mut output_byte), - Err(HashError::InvalidLength(_)) - ), - "squeeze_partial_byte_final_out() must reject num_bits = {num_bits} with InvalidLength" - ); - } + let mut xof = X::default(); + xof.do_update(input); + assert!( + matches!( + xof.do_final_partial_bits(0xFF, num_bits), + Err(HashError::InvalidLength(_)) + ), + "do_final_partial_bits must reject num_bits = {num_bits}" + ); } } } + +impl Default for TestFrameworkXOF { + fn default() -> Self { + Self::new() + } +} diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 7ad51967..f8f4f19c 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -1743,91 +1743,78 @@ where } } -/// Extensible Output Functions (XOFs) are similar to hash functions, except that they can produce output of arbitrary length. -/// The naming used for the functions of this trait are borrowed from the SHA3-style sponge constructions that split XOF operation -/// into two phases: an absorb phase in which an arbitrary amount of input is provided to the XOF, -/// and then a squeeze phase in which an arbitrary amount of output is extracted. -/// Once squeezing begins, no more input can be absorbed. +/// The squeezing phase of an [`XOF`]: a value that produces output and can no longer take input. /// -/// XOFs are _similar to_ hash functions, but are not hash functions for one technical but important reason: -/// since the amount of output to produce is not provided to the XOF in advance, it cannot be used to -/// diversify the XOF output streams. -/// In other words, the overlapping parts of their outputs will be the same! -/// For example, consider two XOFs that absorb the same input data, one that is squeezed to produce 32 bytes, -/// and the other to produce 1 kb; both outputs will be identical in their first 32 bytes. -/// This could lead to loss of security in a number of ways, for example distinguishing attacks where -/// it is sufficient for the attacker to know that two values came from the same input, even if the -/// attacker cannot learn what that input was. This is attack is often sufficient, for example, -/// to break anonymity-preserving technology. -/// Applications that require the arbitrary-length output of an XOF, but also care about these -/// distinguishing attacks should consider adding a cryptographic salt to diversify the inputs. +/// This is the type [`XOF::into_output`] hands back. Absorbing and squeezing are separate types +/// rather than separate states of one type, so "no more input once output has begun" is a fact the +/// compiler enforces rather than a rule the documentation asks callers to follow. BC Java draws the +/// same line at run time, throwing `IllegalStateException` from `KeccakDigest.absorb`. /// -/// # State and Absorb-after-Squeeze -/// This trait makes the design choice that an XOF consists of an absorb phase followed by a squeeze phase. -/// This means that once the XOF has begun squeezing, attempting to absorb more will return -/// [`HashError::InvalidState`] and leave the object usable for further squeezing. +/// Output is one continuous stream: successive calls continue where the last left off, so reading +/// 16 bytes twice gives the same 32 bytes as reading 32 once. +pub trait XofOutput { + /// Produces the next `num_bytes` bytes of the output stream. + /// + /// BC Java's `Xof.doOutput(out, outOff, outLen)`. + fn do_output(&mut self, num_bytes: usize) -> Vec; + + /// As [`do_output`](Self::do_output), filling the caller's buffer, which is zeroized first. + /// Returns the number of bytes written. + fn do_output_out(&mut self, output: &mut [u8]) -> usize; +} + +/// Extendable-Output Functions (XOFs): hashes whose output length is chosen by the caller. /// -/// Without this restriction, the [`XOF::absorb_last_partial_byte`] API cannot function correctly. +/// `XOF: Hash`, so SHAKE128 and SHAKE256 *are* hashes and can be used wherever one is wanted. This +/// is the relationship BC Java draws with `Xof extends ExtendedDigest extends Digest`. As a hash, a +/// XOF has a nominal output length -- [`Hash::output_len`], which for SHAKE is +/// `fixedOutputLength / 4`, matching `SHAKEDigest.getDigestSize()` -- and [`Hash::do_final`] +/// produces exactly that many bytes. This trait adds the ability to ask for a different number. /// -/// If Absorb-after-Squeeze becomes necessary to support in the future, then these design choices can be revisited. -pub trait XOF: Default { - /// A static one-shot API that digests the input data and produces `result_len` bytes of output. - fn hash_xof(self, data: &[u8], result_len: usize) -> Vec; - - /// A static one-shot API that digests the input data and produces `result_len` bytes of output. - /// Fills the provided output slice. - /// The entire output buffer is zeroized before the output is written. - fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize; +/// # Absorb, then squeeze +/// +/// A sponge takes input, then produces output, and cannot go back. Here that is expressed in the +/// types: [`into_output`](Self::into_output) consumes the XOF and returns an [`XofOutput`], so +/// after output has begun there is no value left on which to call [`Hash::do_update`]. Nothing +/// returns an "absorbed after squeezing" error because nothing can reach that state. +/// +/// # A XOF is not a hash, cryptographically +/// +/// It satisfies the trait, but the output length is not an input to the computation, so it cannot +/// diversify the output. Two XOFs given the same input, one read for 32 bytes and one for 1 KiB, +/// agree on their first 32 bytes. An attacker who only needs to know that two values came from the +/// same input -- enough to break an anonymity property -- learns it from the overlap. Where that +/// matters, salt the input. +pub trait XOF: Hash { + /// The squeezing state this XOF turns into. + type Output: XofOutput; - /// Absorb some amount of input. - fn absorb(&mut self, data: &[u8]) -> Result<(), HashError>; + /// Ends the input phase and begins producing output. + /// + /// BC Java's `Xof.doOutput` in effect, but the phase change is in the type: what comes back + /// takes no more input. + fn into_output(self) -> Self::Output; - /// The same as [`XOF::absorb`], but allows for supplying a partial byte as the last input. - /// The partial byte is taken as it arrives in the final octet of an ASN.1 BIT STRING - /// (X.690 s. 8.6.2.1): the `num_bits` message bits are the most significant bits of - /// `partial_byte`, leading bit first, and the low `8 - num_bits` bits (the BIT STRING's "unused - /// bits") are ignored. This is the same convention as [`Hash::do_final_partial_bits`]; see there - /// for the relationship to the FIPS 202 Appendix B.1 bit order and to the NIST test vector files. - /// 0 is a valid value and means the message ends on a byte boundary (equivalent to [`XOF::absorb`]). - /// `num_bits` must be in `0..=7`; larger values return [`HashError::InvalidLength`]. + /// As [`into_output`](Self::into_output), with a final partial **byte** of input. /// - /// Unlike [`XOF::absorb`], this switches the XOF from Absorbing mode into Squeezing mode because - /// absorbing more input after absorbing a partial byte is undefined behaviour. - fn absorb_last_partial_byte( - &mut self, + /// The partial byte arrives as the final octet of an ASN.1 BIT STRING (X.690 s. 8.6.2.1): the + /// `num_bits` message bits are the most significant bits of `partial_byte`, leading bit first, + /// and the low `8 - num_bits` "unused" bits are ignored. Same convention as + /// [`Hash::do_final_partial_bits`]. `num_bits` of 0 means the message ended on a byte boundary + /// and is equivalent to [`into_output`](Self::into_output). + /// + /// # Errors + /// [`HashError::InvalidLength`] if `num_bits` is not in `0..=7`. + fn into_output_partial_bits( + self, partial_byte: u8, num_bits: usize, - ) -> Result<(), HashError>; - - /// Can be called multiple times. - fn squeeze(&mut self, num_bytes: usize) -> Vec; - - /// Can be called multiple times. - /// Fills the provided output slice. - /// The entire output buffer is zeroized before the output is written. - fn squeeze_out(&mut self, output: &mut [u8]) -> usize; - - /// Squeezes a partial byte (`num_bits` in `0..=7`) from the XOF. - /// The bits are returned as they would be placed in the final octet of an ASN.1 BIT STRING - /// (X.690 s. 8.6.2.1): in the most significant `num_bits` bits of the returned u8, first output - /// bit first, with the low `8 - num_bits` "unused" bits zero. This matches the input convention of - /// [`XOF::absorb_last_partial_byte`]. (FIPS 202 Appendix B.1 orders the bits of an output byte - /// LSB-first; the implementation converts.) - /// 0 is a valid value and requests no bits, so the result is `0x00`. - /// `num_bits` must be in `0..=7`; larger values return [`HashError::InvalidLength`]. - /// This is a final call and consumes self. - fn squeeze_partial_byte_final(self, num_bits: usize) -> Result; + ) -> Result; - /// The same as [`XOF::squeeze_partial_byte_final`], but writes into the provided output byte. - /// The output byte is zeroized before the result is written. - fn squeeze_partial_byte_final_out( - self, - num_bits: usize, - output: &mut u8, - ) -> Result<(), HashError>; + /// One-shot: absorbs `data` and produces `result_len` bytes. + fn hash_xof(self, data: &[u8], result_len: usize) -> Vec; - /// Returns the maximum security strength that this KDF is capable of supporting, based on the underlying primitives. - // todo: we should do a refactor to make [Algorithm] be a `security_strength()` function instead of constant, - // then have `RNG: Algorithm`, then delete this function. - fn max_security_strength(&self) -> SecurityStrength; + /// One-shot: absorbs `data` and fills `output`, which is zeroized first. Returns the number of + /// bytes written. + fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize; } diff --git a/crypto/factory/src/xof_factory.rs b/crypto/factory/src/xof_factory.rs index c3d97473..cb36e2ca 100644 --- a/crypto/factory/src/xof_factory.rs +++ b/crypto/factory/src/xof_factory.rs @@ -5,7 +5,7 @@ //! //! Example usage: //! ``` -//! use bouncycastle_core::traits::XOF; +//! use bouncycastle_core::traits::{Hash, XOF, XofOutput}; //! use bouncycastle_factory::AlgorithmFactory; //! use bouncycastle_factory::xof_factory::XOFFactory; //! use bouncycastle_sha3 as sha3; @@ -13,9 +13,11 @@ //! let data: &[u8] = b"Hello, world!"; //! //! let mut h = XOFFactory::new(sha3::SHAKE128_NAME).unwrap(); -//! h.absorb(data); -//! let output: Vec = h.squeeze(16); +//! h.do_update(data); +//! let output: Vec = h.into_output().do_output(16); //! ``` +//! `XOFFactory` implements [`Hash`] too, so it can be used wherever a hash is wanted; `do_final` +//! then produces the nominal 32 or 64 bytes. //! Equivalently, it may be invoked by passing a string instead of using the constant: //! //! ``` @@ -35,7 +37,7 @@ use crate::{AlgorithmFactory, FactoryError}; use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::{KDF, SecurityStrength, XOF}; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XofOutput}; use bouncycastle_sha3 as sha3; use bouncycastle_sha3::{SHAKE128_NAME, SHAKE256_NAME}; @@ -82,81 +84,161 @@ impl AlgorithmFactory for XOFFactory { } } } -impl XOF for XOFFactory { - fn hash_xof(self, data: &[u8], result_len: usize) -> Vec { +/// `Hash` requires it, and the factory does not know which algorithm it holds until it is +/// constructed, so the constants are placeholders -- the same stance `HashFactory` takes. The +/// per-value answers come from [`Hash::output_len`] and [`Hash::max_security_strength`], which +/// dispatch on the variant. +impl Algorithm for XOFFactory { + const ALG_NAME: &'static str = "TODO"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; +} + +/// The squeezing phase of whichever XOF the factory selected. +/// +/// [`XOF::into_output`] consumes the factory value, so this enum is what remains; like +/// [`XOFFactory`] itself it dispatches on the variant. +pub enum XOFFactoryOutput { + /// SHAKE128 output. + SHAKE128(::Output), + /// SHAKE256 output. + SHAKE256(::Output), +} + +impl XofOutput for XOFFactoryOutput { + fn do_output(&mut self, num_bytes: usize) -> Vec { match self { - Self::SHAKE128(h) => h.hash_xof(data, result_len), - Self::SHAKE256(h) => h.hash_xof(data, result_len), + Self::SHAKE128(o) => o.do_output(num_bytes), + Self::SHAKE256(o) => o.do_output(num_bytes), } } - fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { - output.fill(0); + fn do_output_out(&mut self, output: &mut [u8]) -> usize { + match self { + Self::SHAKE128(o) => o.do_output_out(output), + Self::SHAKE256(o) => o.do_output_out(output), + } + } +} +impl Hash for XOFFactory { + fn block_bitlen(&self) -> usize { match self { - Self::SHAKE128(h) => h.hash_xof_out(data, output), - Self::SHAKE256(h) => h.hash_xof_out(data, output), + Self::SHAKE128(h) => h.block_bitlen(), + Self::SHAKE256(h) => h.block_bitlen(), } } - fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> { + fn output_len(&self) -> usize { match self { - Self::SHAKE128(h) => h.absorb(data), - Self::SHAKE256(h) => h.absorb(data), + Self::SHAKE128(h) => h.output_len(), + Self::SHAKE256(h) => h.output_len(), } } - fn absorb_last_partial_byte( - &mut self, - partial_byte: u8, - num_partial_bits: usize, - ) -> Result<(), HashError> { + fn hash(self, data: &[u8]) -> Vec { match self { - Self::SHAKE128(h) => h.absorb_last_partial_byte(partial_byte, num_partial_bits), - Self::SHAKE256(h) => h.absorb_last_partial_byte(partial_byte, num_partial_bits), + Self::SHAKE128(h) => h.hash(data), + Self::SHAKE256(h) => h.hash(data), } } - fn squeeze(&mut self, num_bytes: usize) -> Vec { + fn hash_out(self, data: &[u8], output: &mut [u8]) -> usize { match self { - Self::SHAKE128(h) => h.squeeze(num_bytes), - Self::SHAKE256(h) => h.squeeze(num_bytes), + Self::SHAKE128(h) => h.hash_out(data, output), + Self::SHAKE256(h) => h.hash_out(data, output), } } - fn squeeze_out(&mut self, output: &mut [u8]) -> usize { - output.fill(0); + fn do_update(&mut self, data: &[u8]) { + match self { + Self::SHAKE128(h) => h.do_update(data), + Self::SHAKE256(h) => h.do_update(data), + } + } + fn do_final(self) -> Vec { match self { - Self::SHAKE128(h) => h.squeeze_out(output), - Self::SHAKE256(h) => h.squeeze_out(output), + Self::SHAKE128(h) => h.do_final(), + Self::SHAKE256(h) => h.do_final(), } } - fn squeeze_partial_byte_final(self, num_bits: usize) -> Result { + fn do_final_out(self, output: &mut [u8]) -> usize { match self { - Self::SHAKE128(h) => h.squeeze_partial_byte_final(num_bits), - Self::SHAKE256(h) => h.squeeze_partial_byte_final(num_bits), + Self::SHAKE128(h) => h.do_final_out(output), + Self::SHAKE256(h) => h.do_final_out(output), } } - fn squeeze_partial_byte_final_out( + fn do_final_partial_bits( self, + partial_byte: u8, num_bits: usize, - output: &mut u8, - ) -> Result<(), HashError> { - *output = 0; + ) -> Result, HashError> { + match self { + Self::SHAKE128(h) => h.do_final_partial_bits(partial_byte, num_bits), + Self::SHAKE256(h) => h.do_final_partial_bits(partial_byte, num_bits), + } + } + fn do_final_partial_bits_out( + self, + partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { match self { - Self::SHAKE128(h) => h.squeeze_partial_byte_final_out(num_bits, output), - Self::SHAKE256(h) => h.squeeze_partial_byte_final_out(num_bits, output), + Self::SHAKE128(h) => h.do_final_partial_bits_out(partial_byte, num_bits, output), + Self::SHAKE256(h) => h.do_final_partial_bits_out(partial_byte, num_bits, output), } } fn max_security_strength(&self) -> SecurityStrength { match self { - Self::SHAKE128(h) => KDF::max_security_strength(h), - Self::SHAKE256(h) => XOF::max_security_strength(h), + Self::SHAKE128(h) => Hash::max_security_strength(h), + Self::SHAKE256(h) => Hash::max_security_strength(h), + } + } +} + +impl XOF for XOFFactory { + type Output = XOFFactoryOutput; + + fn into_output(self) -> Self::Output { + match self { + Self::SHAKE128(h) => XOFFactoryOutput::SHAKE128(h.into_output()), + Self::SHAKE256(h) => XOFFactoryOutput::SHAKE256(h.into_output()), + } + } + + fn into_output_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result { + Ok(match self { + Self::SHAKE128(h) => { + XOFFactoryOutput::SHAKE128(h.into_output_partial_bits(partial_byte, num_bits)?) + } + Self::SHAKE256(h) => { + XOFFactoryOutput::SHAKE256(h.into_output_partial_bits(partial_byte, num_bits)?) + } + }) + } + + fn hash_xof(self, data: &[u8], result_len: usize) -> Vec { + match self { + Self::SHAKE128(h) => h.hash_xof(data, result_len), + Self::SHAKE256(h) => h.hash_xof(data, result_len), + } + } + + fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { + output.fill(0); + + match self { + Self::SHAKE128(h) => h.hash_xof_out(data, output), + Self::SHAKE256(h) => h.hash_xof_out(data, output), } } } diff --git a/crypto/mldsa-lowmemory/src/aux_functions.rs b/crypto/mldsa-lowmemory/src/aux_functions.rs index 5eaf55f3..488045b5 100644 --- a/crypto/mldsa-lowmemory/src/aux_functions.rs +++ b/crypto/mldsa-lowmemory/src/aux_functions.rs @@ -7,7 +7,7 @@ use crate::params::{ MLDSAParams, }; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{Hash, XOF, XofOutput}; use bouncycastle_utils::secret::ZeroizablePrimitive; /// Algorithm 14 CoeffFromThreeBytes(𝑏0, 𝑏1, 𝑏2) @@ -433,9 +433,10 @@ pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { // 3: ctx ← H.Absorb(ctx, 𝜌) // 4: (ctx, 𝑠) ← H.Squeeze(ctx, 8) let mut h = H::new(); - h.absorb(rho.as_ref()).expect("absorb before squeeze is infallible"); + h.do_update(rho.as_ref()); let mut s = [0u8; 8]; - h.squeeze_out(&mut s); + let mut h = h.into_output(); + h.do_output_out(&mut s); // 5: ℎ ← BytesToBits(𝑠) // ▷ ℎ is a bit string of length 64 @@ -453,13 +454,13 @@ pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { // 7: (ctx, 𝑗) ← H.Squeeze(ctx, 1) // Note: At first, it might seem to be faster to pre-squeeze a buffer outside the loop. // However, after experimentation and testing, the difference is not noticeable. - h.squeeze_out(&mut j); + h.do_output_out(&mut j); // 8: while 𝑗 > 𝑖 do while j[0] as usize > i { // ▷ rejection sampling in {0, … , 𝑖} // 9: (ctx, 𝑗) ← H.Squeeze(ctx, 1) - h.squeeze_out(&mut j); + h.do_output_out(&mut j); } // 11: 𝑐𝑖 ← 𝑐𝑗 @@ -496,8 +497,8 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { let mut w_hat = Polynomial::new(); let mut j: usize = 0; let mut g = G::new(); - g.absorb(rho).expect("absorb before squeeze is infallible"); - g.absorb(nonce).expect("absorb before squeeze is infallible"); + g.do_update(rho); + g.do_update(nonce); // SHAKE is fairly inefficient if only 3 bytes are squeezed at a time, so the implementation does a block instead. // size is not a limitation, so long as it's a multiple of 3. @@ -505,12 +506,13 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // It's probably around the average rejection rate, and 288 is a multiple of both 3 (required for this alg) // and 8 (efficient for SHAKE). let mut s = [0u8; 288]; - g.squeeze_out(&mut s); + let mut g = g.into_output(); + g.do_output_out(&mut s); let mut idx: usize = 0; while j < N { if idx == s.len() { - g.squeeze_out(&mut s); + g.do_output_out(&mut s); idx = 0; } w_hat[j] = match coeff_from_three_bytes(&s[idx..idx + 3].try_into().unwrap()) { @@ -541,8 +543,8 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) let mut a = Polynomial::new(); let mut j: usize = 0; let mut h = H::new(); - h.absorb(rho).expect("absorb before squeeze is infallible"); - h.absorb(nonce).expect("absorb before squeeze is infallible"); + h.do_update(rho); + h.do_update(nonce); // SHAKE is fairly inefficient if only 3 bytes are squeezed at a time, so the implementation does a block instead. // size is not a limitation as long as it is a multiple of 3. @@ -550,7 +552,8 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) // which is possibly also related with the average rejection rate. // Also, 312 is a multiple of 8 (efficient for SHAKE) let mut z_arr = [0u8; 312]; - h.squeeze_out(&mut z_arr); + let mut h = h.into_output(); + h.do_output_out(&mut z_arr); let mut idx: usize = 0; while j < N { @@ -568,7 +571,7 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) idx += 1; if idx == z_arr.len() { - h.squeeze_out(&mut z_arr); + h.do_output_out(&mut z_arr); idx = 0; } } @@ -588,10 +591,11 @@ pub(crate) fn expand_mask_poly(rho: &[u8; 64], nonce: u16) -> Po // The 32𝑐 bytes squeezed on line 4 are exactly `P::POLY_Z_PACKED_LEN`, so the buffer for them // is `P::PolyZPacked`; see the docs on `MLDSAParams::POLY_Z_PACKED_LEN`. let mut h = H::new(); - h.absorb(rho).expect("absorb before squeeze is infallible"); - h.absorb(&nonce.to_le_bytes()).expect("absorb before squeeze is infallible"); + h.do_update(rho); + h.do_update(&nonce.to_le_bytes()); let mut v = ::ZEROED; - h.squeeze_out(v.as_mut()); + let mut h = h.into_output(); + h.do_output_out(v.as_mut()); bit_unpack_gamma1::

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

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

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

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

().as_ref()); } let mut sig_val_c_tilde = ::ZEROED; - hash.squeeze_out(sig_val_c_tilde.as_mut()); + let mut hash = hash.into_output(); + hash.do_output_out(sig_val_c_tilde.as_mut()); sig_val_c_tilde }; // 16: 𝑐 ∈ 𝑅𝑞 ← SampleInBall(c_tilde) @@ -1013,7 +1015,7 @@ impl< // 12: 𝑐_tilde_p ← H(𝜇||w1Encode(𝐰1'), 𝜆/4) // ▷ hash it; this should match 𝑐_tilde let mut hash = H::new(); - hash.absorb(mu).expect("absorb before squeeze is infallible"); + hash.do_update(mu); for row in 0..P::k { let mut wp_approx = match { @@ -1034,12 +1036,12 @@ impl< // 10: 𝐰1′ ← UseHint(𝐡, 𝐰'_approx) // ▷ reconstruction of signer’s commitment wp_approx.use_hint::

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

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

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

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

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

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

(&mut hash); - hash.squeeze_out(c_tilde_p.as_mut()); + let mut hash = hash.into_output(); + hash.do_output_out(c_tilde_p.as_mut()); c_tilde_p }; @@ -1242,17 +1247,18 @@ impl< // ▷ expand seed let (rho, rho_prime, K) = { let mut h = H::default(); - h.absorb(seed.ref_to_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::l as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); + h.do_update(seed.ref_to_bytes()); + h.do_update(&(P::k as u8).to_le_bytes()); + h.do_update(&(P::l as u8).to_le_bytes()); let mut rho = [0u8; 32]; - let bytes_written = h.squeeze_out(&mut rho); + let mut h = h.into_output(); + let bytes_written = h.do_output_out(&mut rho); debug_assert_eq!(bytes_written, 32); let mut rho_prime = [0u8; 64]; - let bytes_written = h.squeeze_out(&mut rho_prime); + let bytes_written = h.do_output_out(&mut rho_prime); debug_assert_eq!(bytes_written, 64); let mut K: [u8; 32] = [0u8; 32]; - let bytes_written = h.squeeze_out(&mut K); + let bytes_written = h.do_output_out(&mut K); debug_assert_eq!(bytes_written, 32); (rho, rho_prime, K) @@ -1261,11 +1267,12 @@ impl< // Alg 7; 7: 𝜌″ ← H(𝐾||𝑟𝑛𝑑||𝜇, 64) let rho_p_p = { let mut h = H::new(); - h.absorb(&K).expect("absorb before squeeze is infallible"); - h.absorb(&rnd).expect("absorb before squeeze is infallible"); - h.absorb(mu).expect("absorb before squeeze is infallible"); + h.do_update(&K); + h.do_update(&rnd); + h.do_update(mu); let mut rho_p_p = [0u8; 64]; - h.squeeze_out(&mut rho_p_p); + let mut h = h.into_output(); + h.do_output_out(&mut rho_p_p); rho_p_p }; @@ -1333,9 +1340,10 @@ impl< // 15: 𝑐_tilde ← H(𝜇||w1Encode(𝐰1), 𝜆/4) // ▷ commitment hash let mut hash = H::new(); - hash.absorb(mu).expect("absorb before squeeze is infallible"); + hash.do_update(mu); w1.w1_encode_and_hash::

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

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

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

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

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

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

-XXXX.tmp/`, so the `../../../bc-test-data/...` paths the vector suites use resolve to `/tmp/bc-test-data`. Without `ln -s /bc-test-data /tmp/bc-test-data` those tests print their "not found" warning, pass vacuously, and every mutant they would have killed is reported as missed. Use `--jobs 3` and an explicit `--timeout`; note that a mutant which makes a squeeze return no bytes hangs a fill loop for real, so some timeouts are kills rather than false alarms. - Integration tests in `tests/` are preferred over in-file `#[cfg(test)] mod tests` blocks — see "Unit tests vs integration tests" in QUALITY_AND_STYLE.md for the reasoning and the exceptions. A unit test is justified for high-risk code that has known-answer values and cannot be reached through the public API; when you write one, all of its helpers go inside that `mod tests`. - A property that can be asserted at compile time (`const _: () = assert!(...)`) stays a compile-time assertion even when a test also covers it: `cargo mutants` cannot see a const assertion fail, so pair the two rather than trading the guarantee for the coverage. -- Scoping a mutation run: **`--file` is silently ignored** by the installed cargo-mutants — it accepts the flag, filters nothing, and runs the whole package, so a run reported as covering one file may have covered the crate. Use **`-F `**, which matches the mutant names `--list` prints, and confirm the scope with `--list` first. `--test-workspace` needs an explicit value (`--test-workspace=true`), and is required whenever the mutated code is a `core` trait used by other crates. -- `--in-diff` finds nothing for a change that is mostly trait declarations, renamed call sites and documentation, because the executable code in impl bodies is unchanged. File-scoped runs are the useful gate for that shape of change; do not read "no mutants to filter" as "nothing to test". -- Behaviour-critical private functions can use in-file `#[cfg(test)] mod tests` blocks when they can't be exercised from outside the crate. - For traits in `core`, the canonical tests live in `core-test-framework` and are invoked from each implementor's integration tests — don't duplicate them per-implementation. - The per-width `impl Condition` blocks in `crypto/utils/src/ct.rs` (and their test modules) are deliberately duplicated rather than macro-generated: `cargo mutants` cannot see into `macro_rules!` bodies, so a macro would hide the mask identities from mutation testing. Do not fold them back into a macro. Any change to one width in a group (i64/i32, u64/u32) must be applied to every width in that group. -## Commit messages - -One-line subject only: no body, no "Squashed commits" list, and **no `Co-Authored-By` trailer**. This overrides the usual default of adding one. It applies on the release branches and on feature branches alike, so `git commit -m ""` is the whole of it — put in the subject what the body would have said. - -Subjects are `: `, and a change spanning several crates is normally split into one commit per crate, including that crate's factory and CLI wiring. Split only where each commit still builds: a trait change that every implementor must follow cannot be split that way and belongs in one commit. - -Do not strip `Co-Authored-By` from commits written in earlier sessions when rewording them during a rebase — that removes someone else's attribution. - ## CI The only workflow is `.github/workflows/publish_doc_benches_to_ghpages.yaml`: on every PR it builds rustdoc and runs `quality_stats.sh`; on `main` it additionally runs `cargo bench --all` and publishes docs, code stats, and benchmark results to GitHub Pages (`https://bcgit.github.io/bc-rust/`). There is no separate CI test/lint job — local `cargo test --workspace` is the gate, and nothing but a developer running it stands between a broken test and `main`. \ No newline at end of file From b5fcf99252c22b3222bc51bacdb3e7e751516fed Mon Sep 17 00:00:00 2001 From: officialfrancismendoza Date: Wed, 9 Sep 2026 23:58:37 +0700 Subject: [PATCH 25/28] core, core-test-framework: AEADCipherEncryptor/AEADCipherDecryptor gain update_out_len and a FINAL_LEN final buffer so a buffering cipher or an inline ciphertext||tag layout can be expressed; TaggedEncryptor/TaggedDecryptor adapt any FINAL_LEN=0 pair to the SimpleCipherEncryptor/SimpleCipherDecryptor ciphertext||tag shape; the block, simple-cipher and AEAD strength sweeps assert they are not vacuous, and the AEAD streaming suite gains a genuinely-buffering toy plus undersized-buffer and std-one-shot coverage --- .../src/symmetric_ciphers.rs | 610 +++++++++++++++++- crypto/core/src/lib.rs | 1 + crypto/core/src/tagged_aead.rs | 529 +++++++++++++++ crypto/core/src/traits.rs | 388 ++++++++++- 4 files changed, 1514 insertions(+), 14 deletions(-) create mode 100644 crypto/core/src/tagged_aead.rs diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index b3878ac7..3809fc55 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -6,8 +6,9 @@ use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; use bouncycastle_core::traits::{ - AEADCipher, BlockCipherDecryptor, BlockCipherEncryptor, SecurityStrength, - SimpleCipherDecryptor, SimpleCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor, + AEADCipher, AEADCipherDecryptor, AEADCipherEncryptor, BlockCipherDecryptor, + BlockCipherEncryptor, SecurityStrength, SimpleCipherDecryptor, SimpleCipherEncryptor, + StreamCipherDecryptor, StreamCipherEncryptor, }; /// Instance of the test framework. @@ -408,6 +409,7 @@ impl TestFrameworkBlockCipher { SecurityStrength::_192bit, SecurityStrength::_256bit, ]; + let mut strengths_tested = 0; for ss in security_strengths.iter() { // `set_security_strength` enforces its key-length guard even inside a // do_hazardous_operations() closure -- a KEY_LEN-byte key cannot be tagged at a @@ -418,9 +420,10 @@ impl TestFrameworkBlockCipher { if ss > &SecurityStrength::from_bytes(KEY_LEN) { continue; } - - // Tag the key at an arbitrary strength for the purpose of this test. + // Inside a do_hazardous_operations() closure set_security_strength() raises the + // strength without complaining; any error here is a framework bug, hence unwrap(). do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); + strengths_tested += 1; match E::do_encrypt_init(&key) { Ok(_) => { @@ -438,6 +441,7 @@ impl TestFrameworkBlockCipher { _ => panic!("Unexpected error"), }; } + assert!(strengths_tested > 0, "strength sweep must not be vacuous"); } } @@ -595,15 +599,21 @@ impl TestFrameworkAEADCipher { // Modifying the ciphertext MUST cause an AEAD failure: unlike an unauthenticated cipher, // a conformant AEAD must never return plaintext for a ciphertext that fails its tag check. ct[17] ^= 0xFF; + pt[..ct_bytes_written].fill(0xAA); match C::aead_decrypt_out(&key, &nonce, aad, &ct[..ct_bytes_written], &tag, &mut pt) { Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } Err(SymmetricCipherError::DecryptionFailed) => { /* also acceptable */ } _ => panic!("Modified ciphertext must fail the AEAD tag check"), }; + assert!( + pt[..ct_bytes_written].iter().all(|&b| b == 0), + "AEAD must not leave plaintext in the output buffer after a failed tag check" + ); // restore the ciphertext so the AAD- and tag-tamper checks below each test one variable ct[17] ^= 0xFF; // messing with the aad causes the aead_decrypt to fail + pt[..ct_bytes_written].fill(0xAA); match C::aead_decrypt_out( &key, &nonce, @@ -615,8 +625,13 @@ impl TestFrameworkAEADCipher { Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } _ => panic!("Expected TagCheckFailed error"), }; + assert!( + pt[..ct_bytes_written].iter().all(|&b| b == 0), + "AEAD must not leave plaintext in the output buffer after a failed tag check" + ); // messing with the tag causes the aead_decrypt to fail + pt[..ct_bytes_written].fill(0xAA); match C::aead_decrypt_out( &key, &nonce, @@ -628,6 +643,10 @@ impl TestFrameworkAEADCipher { Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } _ => panic!("Expected TagCheckFailed error"), }; + assert!( + pt[..ct_bytes_written].iter().all(|&b| b == 0), + "AEAD must not leave plaintext in the output buffer after a failed tag check" + ); // multiple invocations give different nonces let (nonce1, _ct_bytes_written, _tag) = @@ -658,6 +677,7 @@ impl TestFrameworkAEADCipher { SecurityStrength::_192bit, SecurityStrength::_256bit, ]; + let mut strengths_tested = 0; for ss in security_strengths.iter() { // `set_security_strength` enforces its key-length guard even inside a // do_hazardous_operations() closure -- a KEY_LEN-byte key cannot be tagged at a @@ -671,6 +691,7 @@ impl TestFrameworkAEADCipher { // Tag the key at an arbitrary strength for the purpose of this test. do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); + strengths_tested += 1; // The key-strength requirement must be enforced both by the AEAD one-shot and by the // plain one (encrypt_out), so exercise both. @@ -692,6 +713,587 @@ impl TestFrameworkAEADCipher { check_strength(C::aead_encrypt_out(&key, aad, msg, &mut ct).map(|_| ())); check_strength(C::encrypt_out(&key, msg, &mut ct).map(|_| ())); } + assert!(strengths_tested > 0, "strength sweep must not be vacuous"); + } + + /// Exercises the [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] streaming contract for a + /// paired implementor. The counterpart of [`TestFrameworkBlockCipher::test`] for an + /// authenticated cipher. + /// + /// Checks, in order: + /// * the one-shot round trip for every message length from 0 to a few times `TAG_LEN`, and + /// that the tag is not the all-zero array; + /// * streaming in every chunking, of both the AAD and the data, agrees with `update_out_len` + /// on every call and gives the one-shot's ciphertext and tag byte for byte, and decrypts in + /// every chunking; + /// * an empty AAD is a no-op -- it gives what absorbing no AAD at all gives -- and a message + /// with no data still authenticates its AAD; + /// * `do_update_aad` with non-empty AAD after the first `do_update_out` is refused with a + /// [`SymmetricCipherError::StateError`], and the refusal leaves the value usable; + /// * a tampered ciphertext, tag, AAD or nonce all fail the tag check, and the one-shot + /// `decrypt` leaves no plaintext behind when they do; + /// * two encryptions under the same key draw different nonces; + /// * a key of the wrong [`KeyType`] is rejected, and the security-strength policy matches + /// [`Algorithm::MAX_SECURITY_STRENGTH`]. + /// + /// This only ever drives `E`/`D` with `FINAL_LEN` bytes-or-fewer actually flushed at + /// finalization; it does not by itself prove that a *genuinely buffering* implementor's + /// `update_out_len` is honoured mid-stream (nothing here ever expects `do_update_out` to + /// return less than it was given). [`Self::test_buffering_toy`] pins that separately, against + /// a toy built to hold data back, since `E`/`D` here are supplied by the caller and might not + /// exercise it. + /// + /// [`Algorithm::MAX_SECURITY_STRENGTH`]: bouncycastle_core::traits::Algorithm::MAX_SECURITY_STRENGTH + pub fn test_encryptor_decryptor< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, + E: AEADCipherEncryptor, + D: AEADCipherDecryptor, + >( + &self, + ) { + let key = KeyMaterial::::from_bytes_as_type( + &DUMMY_SEED[..KEY_LEN], + KeyType::SymmetricCipherKey, + ) + .unwrap(); + let aad: &[u8] = b"some associated data"; + + // one-shot round trip, every length up to a few times the tag length + let max_len = 3 * TAG_LEN.max(1) + 5; + for len in 0..=max_len { + let msg = &DUMMY_SEED[..len]; + let mut ct = vec![0u8; E::encrypt_out_len(len)]; + let (nonce, ct_len, tag) = E::encrypt_out(&key, aad, msg, &mut ct).unwrap(); + ct.truncate(ct_len); + assert_ne!(tag, [0u8; TAG_LEN], "len {len}: the tag must not be all zeros"); + // Only assert the ciphertext differs from the plaintext once there is enough of it for + // an accidental match to be negligible rather than a 1-in-256 flake. + if len >= 8 { + assert_ne!(&ct[..], msg, "len {len}: the ciphertext must not be the plaintext"); + } + let mut pt = vec![0u8; D::decrypt_out_max_len(ct.len())]; + let pt_len = D::decrypt_out(&key, &nonce, aad, &ct, &tag, &mut pt).unwrap(); + pt.truncate(pt_len); + assert_eq!(&pt[..], msg, "one-shot round trip, len {len}"); + + // the std one-shots agree with the _out ones for the same nonce + let (nonce2, ct2, tag2) = E::encrypt(&key, aad, msg).unwrap(); + assert_eq!(ct2.len(), ct_len, "encrypt must return exactly the bytes written"); + let pt2 = D::decrypt(&key, &nonce2, aad, &ct2, &tag2).unwrap(); + assert_eq!(pt2, msg, "std round trip, len {len}"); + let pt3 = D::decrypt(&key, &nonce, aad, &ct, &tag).unwrap(); + assert_eq!(pt3, msg, "decrypt must agree with decrypt_out"); + + // too-short output buffers on the one-shots are refused with the required length, + // before any work is done + let need = E::encrypt_out_len(len); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match E::encrypt_out(&key, aad, msg, &mut short) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, n)) => { + assert_eq!(n, need) + } + other => panic!("encrypt_out into a short buffer: {other:?}"), + } + let mut short = vec![0u8; need - 1]; + match E::encrypt_out_rng( + &key, + &mut FixedSeedRNG::::new([0xA5u8; NONCE_LEN]), + aad, + msg, + &mut short, + ) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, n)) => { + assert_eq!(n, need) + } + other => panic!("encrypt_out_rng into a short buffer: {other:?}"), + } + } + let need = D::decrypt_out_max_len(ct.len()); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match D::decrypt_out(&key, &nonce, aad, &ct, &tag, &mut short) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, n)) => { + assert_eq!(n, need) + } + other => panic!("decrypt_out into a short buffer: {other:?}"), + } + } + } + + // streaming in every chunking agrees with the one-shot, for both the AAD and the data. + // The pinned RNG is what makes the nonce -- and so the ciphertext -- comparable. + let msg = &DUMMY_SEED[..max_len.max(17)]; + let pinned = [0xA5u8; NONCE_LEN]; + let mut ct_ref = vec![0u8; E::encrypt_out_len(msg.len())]; + let (nonce_ref, ct_ref_len, tag_ref) = E::encrypt_out_rng( + &key, + &mut FixedSeedRNG::::new(pinned), + aad, + msg, + &mut ct_ref, + ) + .unwrap(); + ct_ref.truncate(ct_ref_len); + + for chunk in [1usize, 2, 3, 7, TAG_LEN.max(1), TAG_LEN + 1, msg.len()] { + let (mut enc, nonce) = + E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(pinned)).unwrap(); + assert_eq!(nonce, nonce_ref, "the same RNG stream must give the same nonce"); + for piece in aad.chunks(chunk) { + enc.do_update_aad(piece).unwrap(); + } + let mut ct = Vec::new(); + for piece in msg.chunks(chunk) { + let expect = enc.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "chunk {chunk}: update_out_len must be exact (encrypt)"); + ct.extend_from_slice(&buf[..n]); + } + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_encrypt_final(&mut final_buf).unwrap(); + ct.extend_from_slice(&final_buf[..final_len]); + assert_eq!(ct, ct_ref, "chunk {chunk}: streaming must give the one-shot ciphertext"); + assert_eq!(tag, tag_ref, "chunk {chunk}: streaming must give the one-shot tag"); + + // ...and the decryptor agrees in every chunking too + let mut dec = D::do_decrypt_init(&key, &nonce).unwrap(); + for piece in aad.chunks(chunk) { + dec.do_update_aad(piece).unwrap(); + } + let mut pt = Vec::new(); + for piece in ct.chunks(chunk) { + let expect = dec.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "chunk {chunk}: update_out_len must be exact (decrypt)"); + pt.extend_from_slice(&buf[..n]); + } + let mut final_buf = [0u8; FINAL_LEN]; + let final_len = dec.do_decrypt_final(&tag, &mut final_buf).unwrap(); + pt.extend_from_slice(&final_buf[..final_len]); + assert_eq!(pt, msg, "chunk {chunk}: streaming round trip"); + } + + // too-short output buffers on the streaming `do_update_out` are refused with the required + // length, before any work is done -- on both sides, not just the one-shots above. + if !msg.is_empty() { + let (mut enc, _) = E::do_encrypt_init(&key).unwrap(); + let need = enc.update_out_len(msg.len()); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match enc.do_update_out(msg, &mut short) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, n)) => { + assert_eq!(n, need) + } + other => panic!("encrypt do_update_out into a short buffer: {other:?}"), + } + } + + let (mut dec, _) = { + let (mut enc, nonce) = E::do_encrypt_init(&key).unwrap(); + let mut ct = vec![0u8; enc.update_out_len(msg.len())]; + enc.do_update_out(msg, &mut ct).unwrap(); + (D::do_decrypt_init(&key, &nonce).unwrap(), ct) + }; + let need = dec.update_out_len(msg.len()); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match dec.do_update_out(msg, &mut short) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, n)) => { + assert_eq!(n, need) + } + other => panic!("decrypt do_update_out into a short buffer: {other:?}"), + } + } + } + + // an empty AAD is a no-op: it must give exactly what absorbing no AAD at all gives + let mut with_empty = vec![0u8; E::encrypt_out_len(msg.len())]; + let (nonce_empty, len_empty, tag_empty) = E::encrypt_out_rng( + &key, + &mut FixedSeedRNG::::new(pinned), + b"", + msg, + &mut with_empty, + ) + .unwrap(); + with_empty.truncate(len_empty); + let mut without = vec![0u8; E::encrypt_out_len(msg.len())]; + let (nonce_none, len_none, tag_none) = E::encrypt_out_rng( + &key, + &mut FixedSeedRNG::::new(pinned), + &[], + msg, + &mut without, + ) + .unwrap(); + without.truncate(len_none); + assert_eq!(nonce_empty, nonce_none); + assert_eq!(tag_empty, tag_none, "an empty AAD must be a no-op"); + assert_eq!(with_empty, without, "an empty AAD must be a no-op"); + + // a message with no data at all still authenticates its AAD + let (nonce, _ct_len, tag) = E::encrypt_out(&key, aad, &[], &mut []).unwrap(); + D::decrypt_out(&key, &nonce, aad, &[], &tag, &mut []).unwrap(); + match D::decrypt_out(&key, &nonce, b"different associated data", &[], &tag, &mut []) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("an empty message must still authenticate its AAD, got {other:?}"), + }; + + // the AAD phase is over once data has been fed in -- on both sides + let (mut enc, nonce) = E::do_encrypt_init(&key).unwrap(); + let mut ct = vec![0u8; enc.update_out_len(msg.len())]; + enc.do_update_out(msg, &mut ct).unwrap(); + match enc.do_update_aad(aad) { + Err(SymmetricCipherError::StateError(_)) => { /* good */ } + other => panic!("AAD after data must be refused, got {other:?}"), + }; + // an empty AAD stays a no-op even here, and the refused call must not have disturbed the + // state: the value is still good for the rest of the flow. + enc.do_update_aad(b"").unwrap(); + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_encrypt_final(&mut final_buf).unwrap(); + ct.extend_from_slice(&final_buf[..final_len]); + + let mut dec = D::do_decrypt_init(&key, &nonce).unwrap(); + let mut pt = vec![0u8; dec.update_out_len(ct.len())]; + dec.do_update_out(&ct, &mut pt).unwrap(); + match dec.do_update_aad(aad) { + Err(SymmetricCipherError::StateError(_)) => { /* good */ } + other => panic!("AAD after data must be refused, got {other:?}"), + }; + dec.do_update_aad(b"").unwrap(); + let mut final_buf = [0u8; FINAL_LEN]; + let final_len = dec.do_decrypt_final(&tag, &mut final_buf).unwrap(); + pt.extend_from_slice(&final_buf[..final_len]); + assert_eq!(&pt[..], msg, "a refused do_update_aad must not disturb the state"); + + // tampering: every one of these must fail the tag check, and the one-shot must leave no + // plaintext behind when it does + let mut ct = vec![0u8; E::encrypt_out_len(msg.len())]; + let (nonce, ct_len, tag) = E::encrypt_out(&key, aad, msg, &mut ct).unwrap(); + ct.truncate(ct_len); + + let mut tampered = ct.clone(); + tampered[3] ^= 0xFF; + let mut buf = vec![0u8; D::decrypt_out_max_len(tampered.len())]; + match D::decrypt_out(&key, &nonce, aad, &tampered, &tag, &mut buf) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified ciphertext must fail the tag check, got {other:?}"), + }; + assert!( + buf.iter().all(|&b| b == 0), + "the one-shot decrypt must zeroize the buffer when the tag check fails" + ); + + let mut wrong_tag = tag; + wrong_tag[0] ^= 0xFF; + let mut buf = vec![0u8; D::decrypt_out_max_len(ct.len())]; + match D::decrypt_out(&key, &nonce, aad, &ct, &wrong_tag, &mut buf) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified tag must fail the tag check, got {other:?}"), + }; + + let mut buf = vec![0u8; D::decrypt_out_max_len(ct.len())]; + match D::decrypt_out(&key, &nonce, b"not the right associated data", &ct, &tag, &mut buf) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified AAD must fail the tag check, got {other:?}"), + }; + + if NONCE_LEN > 0 { + let mut wrong_nonce = nonce; + wrong_nonce[0] ^= 0xFF; + let mut buf = vec![0u8; D::decrypt_out_max_len(ct.len())]; + match D::decrypt_out(&key, &wrong_nonce, aad, &ct, &tag, &mut buf) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified nonce must fail the tag check, got {other:?}"), + }; + + // two encryptions under the same key must not reuse a nonce + let (_enc1, nonce1) = E::do_encrypt_init(&key).unwrap(); + let (_enc2, nonce2) = E::do_encrypt_init(&key).unwrap(); + assert_ne!(nonce1, nonce2); + } + + // error case: KeyMaterial of wrong type + let mac_key = + KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], KeyType::MACKey) + .unwrap(); + match E::do_encrypt_init(&mac_key) { + Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } + _ => panic!("Unexpected error"), + }; + match D::do_decrypt_init(&mac_key, &nonce) { + Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } + _ => panic!("Unexpected error"), + }; + + // error case: security strengths too weak and too strong + let mut key = KeyMaterial::::from_bytes_as_type( + &DUMMY_SEED[..KEY_LEN], + KeyType::SymmetricCipherKey, + ) + .unwrap(); + let security_strengths = [ + SecurityStrength::None, + SecurityStrength::_112bit, + SecurityStrength::_128bit, + SecurityStrength::_192bit, + SecurityStrength::_256bit, + ]; + let mut strengths_tested = 0; + for ss in security_strengths.iter() { + // See the note in `test_plain_one_shots`: a KEY_LEN-byte key cannot be tagged above + // `from_bytes(KEY_LEN)` even inside `do_hazardous_operations`, so skip the strengths + // this key cannot carry. + if ss > &SecurityStrength::from_bytes(KEY_LEN) { + continue; + } + + // Tag the key at an arbitrary strength for the purpose of this test. + do_hazardous_operations(&mut key, |key| key.set_security_strength(*ss)).unwrap(); + strengths_tested += 1; + + // Both directions must enforce the same policy. + let check_strength = |result: Result<(), SymmetricCipherError>| match result { + Ok(_) => { + if ss >= &E::MAX_SECURITY_STRENGTH { /* good */ + } else { + panic!("Should have been a strong enough key"); + } + } + Err(SymmetricCipherError::KeyMaterialError(_)) => { + if ss < &E::MAX_SECURITY_STRENGTH { /* good */ + } else { + panic!("Should not have accepted a key weaker than algorithm"); + } + } + _ => panic!("Unexpected error"), + }; + check_strength(E::do_encrypt_init(&key).map(|_| ())); + check_strength(D::do_decrypt_init(&key, &nonce).map(|_| ())); + } + assert!(strengths_tested > 0, "strength sweep must not be vacuous"); + } + + /// Pins that a *genuinely buffering* [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] pair's + /// `update_out_len` is honoured through every chunking, against a toy built to hold back up to + /// three bytes at a time before releasing them -- the property + /// [`Self::test_encryptor_decryptor`] cannot pin on its own, since a caller-supplied `E`/`D` + /// might never buffer (Ascon-AEAD128 never does). Modelled on the toy permutations + /// `crypto/modes/tests/common/mod.rs` uses for the equivalent block-cipher property. + /// + /// The toy's "ciphertext" is the plaintext with a per-byte counter XORed in, released three + /// bytes behind what it has consumed (so `update_out_len(n)` is `0` for the first two bytes of + /// any run and `n` thereafter, once three bytes are already buffered); its "tag" is a length + /// check. Not remotely a real AEAD -- it exists solely to make holding data back observable. + pub fn test_buffering_toy(&self) { + use bouncycastle_core::errors::SymmetricCipherError; + use bouncycastle_core::key_material::{KeyMaterial, KeyType}; + use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, SecurityStrength, + }; + + const HOLD_BACK: usize = 3; + const KEY_LEN: usize = 4; + const NONCE_LEN: usize = 4; + const TAG_LEN: usize = 1; + + struct Buffered { + pos: u8, + held: [u8; HOLD_BACK], + held_len: usize, + len_seen: usize, + } + + impl Buffered { + fn new() -> Self { + Self { pos: 0, held: [0u8; HOLD_BACK], held_len: 0, len_seen: 0 } + } + + /// Feeds `input` in, holding back the last `HOLD_BACK` bytes and releasing (XORed + /// with a running counter) everything older than that into `output`. + fn update_out(&mut self, input: &[u8], output: &mut [u8]) -> usize { + self.len_seen += input.len(); + let total = self.held_len + input.len(); + let releasable = total.saturating_sub(HOLD_BACK); + let from_held = self.held_len.min(releasable); + let from_new = releasable - from_held; + for (i, b) in self.held[..from_held].iter().enumerate() { + output[i] = *b ^ self.pos; + self.pos = self.pos.wrapping_add(1); + } + for (i, b) in input[..from_new].iter().enumerate() { + output[from_held + i] = *b ^ self.pos; + self.pos = self.pos.wrapping_add(1); + } + // The amount kept is `total - releasable`, which is `HOLD_BACK` once `total` + // reaches it but only `total` itself before that -- so the tail of `new_held` + // actually in use is `new_len`, not always the full array up to `HOLD_BACK`. + let new_len = total - releasable; + let mut new_held = [0u8; HOLD_BACK]; + let kept_from_held = self.held_len - from_held; + new_held[..kept_from_held].copy_from_slice(&self.held[from_held..self.held_len]); + new_held[kept_from_held..new_len].copy_from_slice(&input[from_new..]); + self.held = new_held; + self.held_len = new_len; + releasable + } + + fn finish(self, output: &mut [u8]) -> usize { + for (i, b) in self.held[..self.held_len].iter().enumerate() { + output[i] = *b ^ self.pos; + } + self.held_len + } + } + + struct Enc(Buffered); + struct Dec(Buffered); + + impl Algorithm for Enc { + const ALG_NAME: &'static str = "buffering-toy"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; + } + impl Algorithm for Dec { + const ALG_NAME: &'static str = "buffering-toy"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; + } + + impl AEADCipherEncryptor for Enc { + fn do_encrypt_init( + _key: &KeyMaterial, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + Ok((Self(Buffered::new()), [0u8; NONCE_LEN])) + } + fn do_encrypt_init_rng( + key: &KeyMaterial, + _rng: &mut dyn RNG, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + Self::do_encrypt_init(key) + } + fn do_update_aad(&mut self, _aad: &[u8]) -> Result<(), SymmetricCipherError> { + Ok(()) + } + fn update_out_len(&self, input_len: usize) -> usize { + (self.0.held_len + input_len).saturating_sub(HOLD_BACK) + } + fn do_update_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + Ok(self.0.update_out(plaintext, ciphertext)) + } + fn do_encrypt_final( + self, + output: &mut [u8; HOLD_BACK], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + let len_seen = self.0.len_seen; + let n = self.0.finish(output); + Ok((n, [(len_seen % 256) as u8; TAG_LEN])) + } + } + + impl AEADCipherDecryptor for Dec { + fn do_decrypt_init( + _key: &KeyMaterial, + _nonce: &[u8; NONCE_LEN], + ) -> Result { + Ok(Self(Buffered::new())) + } + fn do_update_aad(&mut self, _aad: &[u8]) -> Result<(), SymmetricCipherError> { + Ok(()) + } + fn update_out_len(&self, input_len: usize) -> usize { + (self.0.held_len + input_len).saturating_sub(HOLD_BACK) + } + fn do_update_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + Ok(self.0.update_out(ciphertext, plaintext)) + } + fn do_decrypt_final( + self, + tag: &[u8; TAG_LEN], + output: &mut [u8; HOLD_BACK], + ) -> Result { + let len_seen = self.0.len_seen; + let n = self.0.finish(output); + if *tag != [(len_seen % 256) as u8; TAG_LEN] { + return Err(SymmetricCipherError::AEADTagCheckFailed); + } + Ok(n) + } + } + + let key = KeyMaterial::::from_bytes_as_type( + &DUMMY_SEED[..KEY_LEN], + KeyType::SymmetricCipherKey, + ) + .unwrap(); + + for len in 0..=(3 * HOLD_BACK + 5) { + let msg = &DUMMY_SEED[..len]; + let mut ct = vec![0u8; len + HOLD_BACK]; + let (nonce, ct_len, tag) = Enc::encrypt_out(&key, b"", msg, &mut ct).unwrap(); + ct.truncate(ct_len); + assert_eq!(ct_len, len, "the toy never expands the data, only the finalizer flushes"); + + for chunk in [1usize, 2, 3, HOLD_BACK, HOLD_BACK + 1, len.max(1)] { + let (mut enc, _) = Enc::do_encrypt_init(&key).unwrap(); + let mut chunked = Vec::new(); + for piece in msg.chunks(chunk) { + let expect = enc.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "len {len} chunk {chunk}: update_out_len must be exact"); + chunked.extend_from_slice(&buf[..n]); + } + let mut final_buf = [0u8; HOLD_BACK]; + let (final_len, chunked_tag) = enc.do_encrypt_final(&mut final_buf).unwrap(); + chunked.extend_from_slice(&final_buf[..final_len]); + assert_eq!(chunked, ct, "len {len} chunk {chunk}: chunking must not be visible"); + assert_eq!( + chunked_tag, tag, + "len {len} chunk {chunk}: tag must not depend on chunking" + ); + + let mut dec = Dec::do_decrypt_init(&key, &nonce).unwrap(); + let mut pt = Vec::new(); + for piece in ct.chunks(chunk) { + let expect = dec.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "len {len} chunk {chunk}: update_out_len must be exact"); + pt.extend_from_slice(&buf[..n]); + } + let mut final_buf = [0u8; HOLD_BACK]; + let final_len = dec.do_decrypt_final(&tag, &mut final_buf).unwrap(); + pt.extend_from_slice(&final_buf[..final_len]); + assert_eq!(pt, msg, "len {len} chunk {chunk}: round trip"); + } + + // For any length past the hold-back window, at least one prefix of the input must be + // held back rather than released immediately -- the property this whole test exists + // to pin. (For `len < HOLD_BACK` nothing is ever releasable until `do_encrypt_final`, + // which is also correct but does not exercise `do_update_out` returning less than it + // was given.) + if len > HOLD_BACK { + let (mut enc, _) = Enc::do_encrypt_init(&key).unwrap(); + let first = &msg[..1]; + let mut buf = vec![0u8; enc.update_out_len(first.len())]; + let n = enc.do_update_out(first, &mut buf).unwrap(); + assert_eq!(n, 0, "len {len}: the first byte alone must be held back, not released"); + } + } } } diff --git a/crypto/core/src/lib.rs b/crypto/core/src/lib.rs index a75792dc..53460b5c 100644 --- a/crypto/core/src/lib.rs +++ b/crypto/core/src/lib.rs @@ -9,4 +9,5 @@ pub mod errors; pub mod key_material; pub mod suspendable_state; +pub mod tagged_aead; pub mod traits; diff --git a/crypto/core/src/tagged_aead.rs b/crypto/core/src/tagged_aead.rs new file mode 100644 index 00000000..9874e172 --- /dev/null +++ b/crypto/core/src/tagged_aead.rs @@ -0,0 +1,529 @@ +//! Adapts an [`AEADCipherEncryptor`] / +//! [`AEADCipherDecryptor`] pair to the separate-output +//! [`SimpleCipherEncryptor`] / +//! [`SimpleCipherDecryptor`] shape by inlining the tag as +//! the last `TAG_LEN` bytes of the ciphertext stream -- the `ciphertext || tag` layout most wire +//! formats and files use, as opposed to the AEAD pair's own detached-tag shape. +//! +//! This is deliberately the *inverse* direction from every other adapter in this crate: instead +//! of adding capability (an AEAD's AAD, its generated nonce), it *drops* the AAD phase, because +//! [`SimpleCipherEncryptor`] has nowhere to carry one. An +//! AEAD wrapped here can still be driven with AAD through the inherent +//! [`TaggedEncryptor::do_update_aad`] / [`TaggedDecryptor::do_update_aad`], which forward to the +//! wrapped value's own method (see their docs for why this can't be part of the +//! `SimpleCipherEncryptor`/`SimpleCipherDecryptor` impl itself); a caller who does not need AAD +//! can ignore that entirely and use [`SimpleCipherEncryptor`]'s +//! full one-shot and streaming API unchanged. +//! +//! # Restricted to non-buffering ciphers +//! +//! Both adapters require the wrapped `FINAL_LEN` to be `0` -- nothing held back at +//! finalization -- which covers Ascon-AEAD128 and any other AEAD that releases every ciphertext +//! byte as soon as it produces it. A cipher that also buffers a partial final block would need +//! this adapter's own `FINAL_LEN` to be `INNER_FINAL_LEN + TAG_LEN`, a value derived from two +//! other const generics; Rust's stable const generics cannot express that as a trait argument +//! (it needs the still-incomplete `generic_const_exprs`), so supporting it is left to a future, +//! more general adapter. + +use crate::errors::SymmetricCipherError; +use crate::key_material::KeyMaterial; +use crate::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, SecurityStrength, + SimpleCipherDecryptor, SimpleCipherEncryptor, +}; + +/// Adapts an [`AEADCipherEncryptor`] with `FINAL_LEN = 0` to +/// [`SimpleCipherEncryptor`], appending the tag as the final segment +/// so the output stream is `ciphertext || tag`. See the module docs for the AAD caveat and the +/// `FINAL_LEN = 0` restriction. +pub struct TaggedEncryptor(E); + +impl TaggedEncryptor { + /// Absorbs `aad` on the wrapped encryptor; see + /// [`AEADCipherEncryptor::do_update_aad`] + /// for the rules (repeatable before the first `do_update_out`, an empty slice always a no-op). + /// Not part of the [`SimpleCipherEncryptor`] impl below, which has no AAD concept at all. + pub fn do_update_aad( + &mut self, + aad: &[u8], + ) -> Result<(), SymmetricCipherError> + where + E: AEADCipherEncryptor, + { + self.0.do_update_aad(aad) + } +} + +// Bounded on `Algorithm` alone, not the full `AEADCipherEncryptor` +// used below: those three consts appear only in a `where` clause, which Rust's coherence check +// does not accept as constraining an impl's generic parameters (E0207), and `Algorithm`'s own +// consts do not need them. +impl Algorithm for TaggedEncryptor { + const ALG_NAME: &'static str = E::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = E::MAX_SECURITY_STRENGTH; +} + +impl + SimpleCipherEncryptor for TaggedEncryptor +where + E: AEADCipherEncryptor, +{ + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + let (inner, nonce) = E::do_encrypt_init(key)?; + Ok((Self(inner), nonce)) + } + + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + let (inner, nonce) = E::do_encrypt_init_rng(key, rng)?; + Ok((Self(inner), nonce)) + } + + /// Identical to the wrapped encryptor's: this adapter never itself buffers, since the tag has + /// nowhere to go until `do_final`. + fn update_out_len(&self, input_len: usize) -> usize { + self.0.update_out_len(input_len) + } + + fn do_update_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + self.0.do_update_out(plaintext, ciphertext) + } + + /// Finishes the inner encryptor (with an empty flush buffer, since `FINAL_LEN = 0` on the + /// bound above) and returns its tag as this trait's own `FINAL_LEN`-byte final segment. + fn do_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + let mut nothing = [0u8; 0]; + let (flushed, tag) = self.0.do_encrypt_final(&mut nothing)?; + debug_assert_eq!(flushed, 0, "FINAL_LEN = 0 on the AEADCipherEncryptor bound"); + Ok((tag, TAG_LEN)) + } + + /// The plaintext length plus the tag: the inline layout this adapter produces. + fn encrypt_out_len(plaintext_len: usize) -> usize { + plaintext_len + TAG_LEN + } +} + +/// Adapts an [`AEADCipherDecryptor`] with `FINAL_LEN = 0` to +/// [`SimpleCipherDecryptor`], reading the tag as the last `TAG_LEN` +/// bytes of the ciphertext stream. `FINAL_LEN` here is `TAG_LEN` only to match +/// [`TaggedEncryptor`]'s own `FINAL_LEN` -- the pair contract [`SimpleCipherEncryptor`] / +/// [`SimpleCipherDecryptor`] share -- not because anything is actually flushed; see this type's +/// `do_final` impl. See the module docs for the AAD caveat and the wrapped AEAD's own +/// `FINAL_LEN = 0` restriction. +/// +/// # Holding back the tag +/// +/// The wire format gives no advance notice of where the ciphertext ends and the tag begins -- +/// that boundary is only known once the whole stream has been seen -- so this type holds back the +/// last `TAG_LEN` bytes it has been given at all times, in `tail`, releasing everything older than +/// that through the wrapped decryptor as soon as it is known not to be part of the tag. This is +/// the same technique `cli/src/ascon_cmd.rs`'s `aead128_decrypt_stream` used by hand before this +/// adapter existed. +pub struct TaggedDecryptor { + inner: D, + tail: [u8; TAG_LEN], + tail_len: usize, +} + +impl TaggedDecryptor { + /// Absorbs `aad` on the wrapped decryptor; see + /// [`AEADCipherDecryptor::do_update_aad`] + /// for the rules. Not part of the [`SimpleCipherDecryptor`] impl below, which has no AAD + /// concept at all. + pub fn do_update_aad( + &mut self, + aad: &[u8], + ) -> Result<(), SymmetricCipherError> + where + D: AEADCipherDecryptor, + { + self.inner.do_update_aad(aad) + } +} + +// See the equivalent impl on `TaggedEncryptor` for why this bounds on `Algorithm` alone. +impl Algorithm for TaggedDecryptor { + const ALG_NAME: &'static str = D::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = D::MAX_SECURITY_STRENGTH; +} + +impl + SimpleCipherDecryptor for TaggedDecryptor +where + D: AEADCipherDecryptor, +{ + fn do_decrypt_init( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ) -> Result { + Ok(Self { inner: D::do_decrypt_init(key, nonce)?, tail: [0u8; TAG_LEN], tail_len: 0 }) + } + + /// Only the bytes no longer eligible to be the tag: `tail_len + input_len - TAG_LEN`, floored + /// at `0` while the stream is still shorter than the tag itself. + fn update_out_len(&self, input_len: usize) -> usize { + (self.tail_len + input_len).saturating_sub(TAG_LEN) + } + + fn do_update_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + let releasable = self.update_out_len(ciphertext.len()); + if plaintext.len() < releasable { + return Err(SymmetricCipherError::IncorrectOutputBufferLength("plaintext", releasable)); + } + + let total = self.tail_len + ciphertext.len(); + if total <= TAG_LEN { + // Everything seen so far might still be the tag; buffer it and release nothing. + self.tail[self.tail_len..total].copy_from_slice(ciphertext); + self.tail_len = total; + return Ok(0); + } + + // Release the old tail (in full, or as much of it as `releasable` allows) followed by + // however much of the new input is also releasable; two streaming calls into the wrapped + // decryptor, equivalent to one over their concatenation. + let from_tail = self.tail_len.min(releasable); + let from_new = releasable - from_tail; + if from_tail > 0 { + self.inner.do_update_out(&self.tail[..from_tail], &mut plaintext[..from_tail])?; + } + if from_new > 0 { + self.inner + .do_update_out(&ciphertext[..from_new], &mut plaintext[from_tail..releasable])?; + } + + // The new tail is whatever was not just released -- the suffix of the old tail, then the + // suffix of the new ciphertext -- which together are exactly TAG_LEN bytes, since + // `total - releasable == TAG_LEN` by construction of `releasable` above. + let mut new_tail = [0u8; TAG_LEN]; + let old_tail_kept = self.tail_len - from_tail; + new_tail[..old_tail_kept].copy_from_slice(&self.tail[from_tail..self.tail_len]); + new_tail[old_tail_kept..].copy_from_slice(&ciphertext[from_new..]); + self.tail = new_tail; + self.tail_len = TAG_LEN; + + Ok(releasable) + } + + /// Nothing is held back for release -- every plaintext byte was already emitted by + /// `do_update_out` -- so this is purely the tag check, against whatever ended up in `tail`. + /// The returned array is `FINAL_LEN = TAG_LEN` bytes only to match + /// [`TaggedEncryptor`]'s `FINAL_LEN` (the pair contract both traits share); the `0` data-byte + /// count says none of it is meaningful, exactly the case [`SimpleCipherDecryptor::do_final`]'s + /// own docs anticipate ("an authenticated cipher may release nothing at all once it has + /// checked the tag"). + /// + /// # Errors + /// [`SymmetricCipherError::DecryptionFailed`] if fewer than `TAG_LEN` bytes were ever seen (the + /// input was shorter than the tag). Otherwise, whatever + /// [`AEADCipherDecryptor::do_decrypt_final`] + /// returns, most notably [`SymmetricCipherError::AEADTagCheckFailed`]. + fn do_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + if self.tail_len < TAG_LEN { + return Err(SymmetricCipherError::DecryptionFailed); + } + let mut nothing = [0u8; 0]; + self.inner.do_decrypt_final(&self.tail, &mut nothing)?; + Ok(([0u8; TAG_LEN], 0)) + } + + /// The ciphertext length minus the tag, floored at `0` for an input shorter than the tag + /// (which `do_final` rejects rather than `do_update_out`, so the buffer must still be sized). + fn decrypt_out_max_len(ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(TAG_LEN) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::key_material::{KeyMaterialTrait, KeyType, do_hazardous_operations}; + use crate::traits::RNG; + use bouncycastle_utils::secret::Secret; + + const KEY_LEN: usize = 4; + const NONCE_LEN: usize = 4; + const TAG_LEN: usize = 3; + + /// A toy AEAD: "ciphertext" is the plaintext XORed byte-by-byte with the key (cycled), and the + /// "tag" is a running XOR of every AAD/plaintext byte seen, repeated to `TAG_LEN` bytes. Not + /// remotely secure -- it exists only to drive `TaggedEncryptor`/`TaggedDecryptor` through + /// [`crate::traits::SimpleCipherEncryptor`]/[`SimpleCipherDecryptor`]'s chunked-equivalence + /// contract at exact byte-boundary edge cases around `TAG_LEN`, which is what this module's + /// hand-written tail bookkeeping needs pinned directly (see CLAUDE.md on testing + /// behaviour-critical private logic in-file). + #[derive(Clone)] + struct Toy { + key: Secret<[u8; KEY_LEN]>, + pos: usize, + acc: u8, + } + + impl Toy { + fn new(key: &KeyMaterial) -> Result { + let mut k = Secret::<[u8; KEY_LEN]>::new(); + k.copy_from_slice(key.ref_to_bytes()); + Ok(Self { key: k, pos: 0, acc: 0 }) + } + + /// Transforms `data` in place, accumulating `acc` over the *plaintext* byte on both + /// sides: encrypting, `data` starts as plaintext, so `acc` is updated before the XOR; + /// decrypting, `data` starts as ciphertext, so the XOR (which recovers the plaintext byte + /// into the same slot) must happen first. + fn transform(&mut self, data: &mut [u8], encrypting: bool) { + for b in data.iter_mut() { + if encrypting { + self.acc ^= *b; + } + *b ^= self.key[self.pos % KEY_LEN]; + if !encrypting { + self.acc ^= *b; + } + self.pos += 1; + } + } + } + + struct ToyEnc(Toy); + struct ToyDec(Toy); + + impl Algorithm for ToyEnc { + const ALG_NAME: &'static str = "toy-aead"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; + } + impl Algorithm for ToyDec { + const ALG_NAME: &'static str = "toy-aead"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; + } + + impl AEADCipherEncryptor for ToyEnc { + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + Ok((Self(Toy::new(key)?), [0u8; NONCE_LEN])) + } + fn do_encrypt_init_rng( + key: &KeyMaterial, + _rng: &mut dyn RNG, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + Self::do_encrypt_init(key) + } + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + for &b in aad { + self.0.acc ^= b; + } + Ok(()) + } + fn update_out_len(&self, input_len: usize) -> usize { + input_len + } + fn do_update_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + if ciphertext.len() < plaintext.len() { + return Err(SymmetricCipherError::IncorrectOutputBufferLength( + "ciphertext", + plaintext.len(), + )); + } + let out = &mut ciphertext[..plaintext.len()]; + out.copy_from_slice(plaintext); + self.0.transform(out, true); + Ok(plaintext.len()) + } + fn do_encrypt_final( + self, + _output: &mut [u8; 0], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + Ok((0, [self.0.acc; TAG_LEN])) + } + } + + impl AEADCipherDecryptor for ToyDec { + fn do_decrypt_init( + key: &KeyMaterial, + _nonce: &[u8; NONCE_LEN], + ) -> Result { + Ok(Self(Toy::new(key)?)) + } + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + for &b in aad { + self.0.acc ^= b; + } + Ok(()) + } + fn update_out_len(&self, input_len: usize) -> usize { + input_len + } + fn do_update_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + if plaintext.len() < ciphertext.len() { + return Err(SymmetricCipherError::IncorrectOutputBufferLength( + "plaintext", + ciphertext.len(), + )); + } + let out = &mut plaintext[..ciphertext.len()]; + out.copy_from_slice(ciphertext); + self.0.transform(out, false); + Ok(ciphertext.len()) + } + fn do_decrypt_final( + self, + tag: &[u8; TAG_LEN], + _output: &mut [u8; 0], + ) -> Result { + if [self.0.acc; TAG_LEN] != *tag { + return Err(SymmetricCipherError::AEADTagCheckFailed); + } + Ok(0) + } + } + + fn key() -> KeyMaterial { + let mut km = + KeyMaterial::::from_bytes_as_type(&[1, 2, 3, 4], KeyType::SymmetricCipherKey) + .unwrap(); + do_hazardous_operations(&mut km, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::None) + }) + .unwrap(); + km + } + + /// The one-shot round trip through the adapters, at every message length crossing a few + /// multiples of `TAG_LEN`, and every chunking of `do_update_out` on both sides -- this is what + /// pins the tail bookkeeping's off-by-one edges directly, complementing the framework's own + /// generic `test_encryptor_decryptor` coverage (which this same adapter pair is expected to + /// pass against `SimpleCipherEncryptor`/`SimpleCipherDecryptor`'s contract elsewhere). + #[test] + fn tagged_round_trip_at_every_length_and_chunking() { + let km = key(); + for len in 0..=(4 * TAG_LEN + 5) { + let msg: Vec = + (0..len).map(|i| (i as u8).wrapping_mul(31).wrapping_add(7)).collect(); + + let (mut enc, nonce) = as SimpleCipherEncryptor< + KEY_LEN, + NONCE_LEN, + TAG_LEN, + >>::do_encrypt_init(&km) + .unwrap(); + enc.do_update_aad::(b"aad").unwrap(); + let mut ct = vec![0u8; msg.len() + TAG_LEN]; + for chunk in [1usize, 2, 3, TAG_LEN.max(1), len.max(1)] { + let mut enc = { + let (mut e, _) = as SimpleCipherEncryptor< + KEY_LEN, + NONCE_LEN, + TAG_LEN, + >>::do_encrypt_init(&km) + .unwrap(); + e.do_update_aad::(b"aad").unwrap(); + e + }; + let mut written = 0; + for piece in msg.chunks(chunk) { + written += enc.do_update_out(piece, &mut ct[written..]).unwrap(); + } + let mut last = [0u8; TAG_LEN]; + let last_len = as SimpleCipherEncryptor< + KEY_LEN, + NONCE_LEN, + TAG_LEN, + >>::do_final_out(enc, &mut last) + .unwrap(); + ct[written..written + last_len].copy_from_slice(&last[..last_len]); + written += last_len; + ct.truncate(written); + + let mut dec = as SimpleCipherDecryptor< + KEY_LEN, + NONCE_LEN, + TAG_LEN, + >>::do_decrypt_init(&km, &nonce) + .unwrap(); + dec.do_update_aad::(b"aad").unwrap(); + let mut pt = vec![0u8; ct.len()]; + let mut written = 0; + for piece in ct.chunks(chunk) { + written += dec.do_update_out(piece, &mut pt[written..]).unwrap(); + } + let (_, data_len) = dec.do_final().unwrap(); + pt.truncate(written + data_len); + assert_eq!(pt, msg, "len {len}, chunk {chunk}"); + + ct.resize(msg.len() + TAG_LEN, 0); + } + } + } + + /// A tampered inline stream must fail at `do_final`, and a stream shorter than the tag must be + /// rejected as `DecryptionFailed` rather than panicking on the short slice. + #[test] + fn tampering_and_short_input_are_rejected() { + let km = key(); + let (mut enc, nonce) = as SimpleCipherEncryptor< + KEY_LEN, + NONCE_LEN, + TAG_LEN, + >>::do_encrypt_init(&km) + .unwrap(); + let mut ct = vec![0u8; 10 + TAG_LEN]; + let written = enc.do_update_out(&[7u8; 10], &mut ct).unwrap(); + let mut last = [0u8; TAG_LEN]; + let last_len = as SimpleCipherEncryptor< + KEY_LEN, + NONCE_LEN, + TAG_LEN, + >>::do_final_out(enc, &mut last) + .unwrap(); + ct[written..written + last_len].copy_from_slice(&last[..last_len]); + + let mut tampered = ct.clone(); + tampered[0] ^= 0xFF; + let mut dec = as SimpleCipherDecryptor< + KEY_LEN, + NONCE_LEN, + TAG_LEN, + >>::do_decrypt_init(&km, &nonce) + .unwrap(); + let mut pt = vec![0u8; tampered.len()]; + let mut written = 0; + written += dec.do_update_out(&tampered, &mut pt[written..]).unwrap(); + let _ = written; + assert!(matches!(dec.do_final(), Err(SymmetricCipherError::AEADTagCheckFailed))); + + for short_len in 0..TAG_LEN { + let dec = as SimpleCipherDecryptor< + KEY_LEN, + NONCE_LEN, + TAG_LEN, + >>::do_decrypt_init(&km, &nonce) + .unwrap(); + let mut dec = dec; + let mut pt = vec![0u8; short_len]; + dec.do_update_out(&ct[..short_len], &mut pt).unwrap(); + assert!(matches!(dec.do_final(), Err(SymmetricCipherError::DecryptionFailed))); + } + } +} diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index adc702c5..4fc5219b 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -55,8 +55,11 @@ pub trait AEADCipher`, so it needs the `std` feature. /// /// # Errors - /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. The caller learns - /// only that decryption failed. + /// [`SymmetricCipherError::DecryptionFailed`] if the ciphertext does not authenticate. This + /// view has no AAD and no separate tag to name, so it reports every authentication failure + /// this way rather than as [`SymmetricCipherError::AEADTagCheckFailed`], which is reserved for + /// [`aead_decrypt`](Self::aead_decrypt) / [`aead_decrypt_out`](Self::aead_decrypt_out); either + /// way, the caller learns only that decryption failed, not why. fn decrypt( key: &KeyMaterial, init_data: [u8; NONCE_LEN], @@ -100,10 +103,14 @@ pub trait AEADCipher Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError>; - /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) or a stream cipher ([`StreamCipherEncryptor`] / [`StreamCipherDecryptor`]), and so will already - /// have a streaming API. - /// This allows you to finish either style of streaming API flow with AEAD specific do_final() - /// that computes and returns the authentication tag. + /// Finishes a streaming encryption flow with an AEAD-specific `do_final()` that computes and + /// returns the authentication tag. + /// + /// An AEAD's own streaming API is [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], which has + /// this step (as [`AEADCipherEncryptor::do_encrypt_final`]) and an AAD phase of its own; this + /// method is for an implementor that streams through one of the unauthenticated cipher traits + /// -- [`BlockCipherEncryptor`] / [`BlockCipherDecryptor`] or [`StreamCipherEncryptor`] / + /// [`StreamCipherDecryptor`] -- and needs somewhere to put the tag. fn do_aead_encrypt_final(self) -> Result<[u8; TAG_LEN], SymmetricCipherError>; #[cfg(feature = "std")] /// A one-shot API to decrypt some ciphertext with the given key. @@ -129,13 +136,374 @@ pub trait AEADCipher Result; - /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) or a stream cipher ([`StreamCipherEncryptor`] / [`StreamCipherDecryptor`]), and so will already - /// have a streaming API. - /// This allows you to finish either style of streaming API flow with AEAD specific do_final() - /// that computes and returns the authentication tag. + /// Finishes a streaming decryption flow by checking `tag`; the mirror of + /// [`do_aead_encrypt_final`](Self::do_aead_encrypt_final), and see it for when this is the + /// right finalizer rather than [`AEADCipherDecryptor::do_decrypt_final`]. fn do_aead_decrypt_final(self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError>; } +/// The decryption half of an AEAD cipher's streaming API; see [`AEADCipherEncryptor`], whose notes +/// on the AAD phase, buffering, and the `Result` all apply here too. +/// +/// # The plaintext is not authenticated until `do_decrypt_final` returns `Ok` +/// +/// This is the one thing a streaming AEAD API cannot hide from its caller. +/// [`do_update_out`](Self::do_update_out) releases plaintext as soon as it can, long before there +/// is a tag to check it against, so a caller that *uses* those bytes before +/// [`do_decrypt_final`](Self::do_decrypt_final) has returned `Ok` is acting on unauthenticated +/// plaintext -- bytes an attacker may have chosen. Preventing exactly that is what the tag is for. +/// A streaming caller must therefore treat everything `do_update_out` produces as untrusted until +/// the final call succeeds, and scrub it if it does not. +/// +/// The one-shot [`decrypt`](Self::decrypt) has no such caveat: it owns the whole message, so it +/// zeroizes the buffer itself before returning the error. +pub trait AEADCipherDecryptor< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +>: Algorithm + Sized +{ + /// Begins a streaming decryption flow from the nonce returned by + /// [`AEADCipherEncryptor::do_encrypt_init`]. + /// + /// # Errors + /// Rejects a key whose [`KeyType`] is not [`KeyType::SymmetricCipherKey`], and one whose + /// security strength is below [`Algorithm::MAX_SECURITY_STRENGTH`], both as a + /// [`SymmetricCipherError::KeyMaterialError`]. + fn do_decrypt_init( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ) -> Result; + + /// Absorbs additional authenticated data; see [`AEADCipherEncryptor::do_update_aad`] for the + /// rules, which are the same on both sides. The concatenation of what a decryptor absorbs must + /// be byte-for-byte the concatenation the encryptor absorbed, or the tag check fails. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] if called with a non-empty `aad` after + /// [`do_update_out`](Self::do_update_out). + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError>; + + /// The exact number of bytes the next [`do_update_out`](Self::do_update_out) will write if + /// given `input_len` more bytes of ciphertext. Depends on what is already buffered; identically + /// `0` for a cipher that never holds anything back, such as Ascon-AEAD128. + fn update_out_len(&self, input_len: usize) -> usize; + + /// Streaming: consumes `ciphertext`, writing every plaintext byte that can be released so far + /// into `plaintext` and buffering the rest. Returns the number of bytes written, which is + /// exactly [`update_out_len`](Self::update_out_len) of `ciphertext.len()`. + /// + /// The bytes this writes are *not* yet authenticated; see the trait docs. A decryptor may have + /// to hold back the tail of what it has seen -- a block-oriented cipher's partial final block, + /// or the bytes that might turn out to be an inline tag -- so a sequence of calls releases data + /// later than the corresponding encryptor produced it, but the concatenation of everything + /// released, in any chunking, plus the data part of + /// [`do_decrypt_final`](Self::do_decrypt_final), is the plaintext. + /// + /// # Errors + /// [`SymmetricCipherError::IncorrectOutputBufferLength`] if `plaintext` is shorter than + /// [`update_out_len`](Self::update_out_len), carrying the required length. Nothing is + /// consumed in that case. + fn do_update_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result; + + /// Finishes the decryption, consuming the decryptor: flushes whatever ciphertext was held back + /// into `output`, computes the tag over the AAD and ciphertext it has seen, and compares it + /// against `tag`. Returns how many leading bytes of `output` are plaintext; the remainder is + /// not data and must not be used. `Ok` is the only thing that makes those bytes -- or anything + /// already released by [`do_update_out`](Self::do_update_out) -- trustworthy. + /// + /// # Errors + /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. Implementors must + /// compare in constant time, and the caller learns only that the check failed. + fn do_decrypt_final( + self, + tag: &[u8; TAG_LEN], + output: &mut [u8; FINAL_LEN], + ) -> Result; + + /// An upper bound on the plaintext recovered from `ciphertext_len` bytes of ciphertext, i.e. + /// the buffer [`decrypt_out`](Self::decrypt_out) requires. The default returns `ciphertext_len` + /// itself, which is exact for every conformant AEAD: unlike a padding scheme, an AEAD never + /// expands or shrinks the data it is given, only adds the separate `tag`. + fn decrypt_out_max_len(ciphertext_len: usize) -> usize { + ciphertext_len + } + + /// One-shot: decrypts `ciphertext` into `plaintext`, which needs + /// [`decrypt_out_max_len`](Self::decrypt_out_max_len) bytes, under `nonce` and `aad`, and + /// checks `tag`. Returns the number of plaintext bytes written. + /// + /// Unlike the streaming methods this releases nothing unauthenticated: on failure `plaintext` + /// is zeroized before the error is returned, so a caller who ignores the `Result` is left with + /// zeros rather than attacker-chosen plaintext. + /// + /// # Errors + /// [`SymmetricCipherError::IncorrectOutputBufferLength`] if `plaintext` is too short, checked + /// before any work is done; otherwise whatever the streaming methods return, including + /// [`do_decrypt_final`](Self::do_decrypt_final)'s. + fn decrypt_out( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + plaintext: &mut [u8], + ) -> Result { + let needed = Self::decrypt_out_max_len(ciphertext.len()); + if plaintext.len() < needed { + return Err(SymmetricCipherError::IncorrectOutputBufferLength("plaintext", needed)); + } + let mut dec = Self::do_decrypt_init(key, nonce)?; + dec.do_update_aad(aad)?; + let written = dec.do_update_out(ciphertext, plaintext)?; + let mut final_buf = [0u8; FINAL_LEN]; + match dec.do_decrypt_final(tag, &mut final_buf) { + Ok(final_len) => { + plaintext[written..written + final_len].copy_from_slice(&final_buf[..final_len]); + Ok(written + final_len) + } + Err(e) => { + // As in the trait docs: what `do_update_out` already released is unauthenticated, + // and this one-shot owns the whole message, so it does not leave that in the + // caller's hands. A plain `fill` rather than a volatile write because `core` is + // `#![forbid(unsafe_code)]`; the store is to the caller's own buffer, which the + // caller may read after this returns, so it is not a dead store the optimizer is + // entitled to drop. + plaintext[..written].fill(0); + Err(e) + } + } + } + + #[cfg(feature = "std")] + /// One-shot, allocating: as [`decrypt_out`](Self::decrypt_out), returning the plaintext as a + /// `Vec` of exactly the recovered length. Only available with the `std` feature. + fn decrypt( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + ) -> Result, SymmetricCipherError> { + let mut plaintext = vec![0u8; Self::decrypt_out_max_len(ciphertext.len())]; + let written = Self::decrypt_out(key, nonce, aad, ciphertext, tag, &mut plaintext)?; + plaintext.truncate(written); + Ok(plaintext) + } +} + +/// The encryption half of an AEAD cipher's streaming API. This is the AEAD counterpart of +/// [`SimpleCipherEncryptor`] -- the same separate-output, init-data-generating, possibly-buffering +/// shape -- with the two differences that authentication forces. +/// +/// The first is an extra phase. An AEAD authenticates data it does not encrypt -- additional +/// authenticated data (AAD), typically a header that has to travel in the clear but must still be +/// protected against tampering -- and every AEAD construction absorbs that AAD *before* the +/// plaintext. So [`do_update_aad`](Self::do_update_aad) may be called any number of times after +/// the constructor and before the first [`do_update_out`](Self::do_update_out), and returns +/// [`SymmetricCipherError::StateError`] thereafter. (An empty `aad` slice is a no-op and is +/// accepted at any point, so a generic caller may pass one unconditionally.) That is a runtime +/// error for the same reason [`XOF`] rejects absorb-after-squeeze at runtime: the phase order is a +/// property of a value's history, and encoding it in the type would cost every implementor an +/// extra type and an explicit transition. +/// +/// The second is a finalization step that also produces a tag: [`do_encrypt_final`](Self::do_encrypt_final) +/// consumes the encryptor, flushes whatever ciphertext it was holding back into `output`, and +/// returns the tag, which the recipient needs for [`AEADCipherDecryptor::do_decrypt_final`]. Where +/// the tag travels -- appended to the ciphertext, carried in a separate field -- is the caller's +/// choice, not this trait's; contrast [`AEADCipher`], whose one-shots pick a layout for you, and +/// see `bouncycastle_core::tagged_aead` for an adapter that appends it. +/// +/// Encryption and decryption are separate traits, as with [`BlockCipherEncryptor`] / +/// [`BlockCipherDecryptor`], so that the direction is encoded in the type. For an AEAD that also +/// buys away a class of runtime check: a single type serving both directions has to remember which +/// one it is and refuse the other's methods, whereas a paired-type implementation cannot be asked +/// the question. +/// +/// # The nonce is generated, not supplied +/// +/// The constructor draws the nonce itself and returns it for transmission alongside the ciphertext; +/// there is no API here for the caller to choose one, for the same reason as in +/// [`BlockCipherEncryptor`], but with sharper consequences. Reusing a nonce under one key does not +/// merely leak equality of plaintexts as it does for an unauthenticated mode -- for most AEAD +/// constructions it forfeits confidentiality of the affected messages and can expose the material +/// the tag is computed from, costing authenticity for every other message under that key. A caller +/// who genuinely needs a deterministic, caller-chosen nonce (to follow a protocol's construction, +/// or to run a spec's test vectors) should see the documentation of the underlying implementation, +/// which is where that hazard belongs. +/// +/// # A cipher may buffer +/// +/// [`do_update_out`](Self::do_update_out) takes separate input and output buffers, because an AEAD +/// is not guaranteed to release a ciphertext byte the moment it sees the matching plaintext byte. +/// Ascon-AEAD128 does -- each rate-block byte is transformed independently of the others in that +/// block -- but a block-oriented AEAD holds back a partial final block, and any AEAD adapted to an +/// inline `ciphertext || tag` layout must hold back at least `TAG_LEN` bytes until it knows they +/// are not the tag (see `bouncycastle_core::tagged_aead`). [`update_out_len`](Self::update_out_len) +/// answers exactly how many bytes the next call releases, so a caller never has to guess a buffer +/// size or find plaintext left over at the end of one it guessed too large; the concatenation of +/// everything released, in any chunking, plus the data part of +/// [`do_encrypt_final`](Self::do_encrypt_final), is the ciphertext. +/// +/// # Any length, as a slice +/// +/// [`do_update_out`](Self::do_update_out)'s input is a `&[u8]` rather than a `&[u8; LEN]` because +/// every length is valid, including zero, so there is no invariant for a const parameter to carry +/// and nothing for a compile-time check to check -- the same reasoning as +/// [`StreamCipherEncryptor`], and the reason there is no `BLOCK_LEN` here. +/// +/// # Why the data methods still return `Result` +/// +/// Nothing about the buffer can go wrong, and a constructed value is always ready to use, so +/// [`do_update_out`](Self::do_update_out) has nothing to report for most ciphers. The `Result` is +/// for the per-(key, nonce) data limit an AEAD generally has -- past it the construction's security +/// argument no longer holds -- which a streaming API cannot check any earlier than the call that +/// would cross it, and for [`IncorrectOutputBufferLength`](SymmetricCipherError::IncorrectOutputBufferLength) +/// if the caller under-sized `ciphertext`. +pub trait AEADCipherEncryptor< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +>: Algorithm + Sized +{ + /// Begins a streaming encryption flow, returning the encryptor and the generated nonce, which + /// the recipient needs for [`AEADCipherDecryptor::do_decrypt_init`]. Sources randomness from + /// the library's default OS-backed RNG. + /// + /// # Errors + /// Rejects a key whose [`KeyType`] is not [`KeyType::SymmetricCipherKey`], and one whose + /// security strength is below [`Algorithm::MAX_SECURITY_STRENGTH`], both as a + /// [`SymmetricCipherError::KeyMaterialError`]; a failure to draw the nonce comes back as a + /// [`SymmetricCipherError::RNGError`]. + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError>; + + /// As [`do_encrypt_init`](Self::do_encrypt_init), but sources randomness from the provided RNG. + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError>; + + /// Absorbs `aad`: data that is authenticated by the tag but not encrypted. May be called + /// repeatedly before the first [`do_update_out`](Self::do_update_out); a sequence of calls is + /// equivalent to one call over the concatenation. An empty `aad` is a no-op. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] if called with a non-empty `aad` after + /// [`do_update_out`](Self::do_update_out) -- see the trait docs for why the AAD comes first. + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError>; + + /// The exact number of bytes the next [`do_update_out`](Self::do_update_out) will write if + /// given `input_len` more bytes of plaintext. Depends on what is already buffered; identically + /// `0` for a cipher that never holds anything back, such as Ascon-AEAD128. + fn update_out_len(&self, input_len: usize) -> usize; + + /// Streaming: consumes `plaintext`, writing every ciphertext byte that can be produced so far + /// into `ciphertext` and buffering the rest. Returns the number of bytes written, which is + /// exactly [`update_out_len`](Self::update_out_len) of `plaintext.len()`. A sequence of calls + /// is equivalent to one call over the concatenation, whatever the chunking. + /// + /// # Errors + /// [`SymmetricCipherError::IncorrectOutputBufferLength`] if `ciphertext` is shorter than + /// [`update_out_len`](Self::update_out_len), carrying the required length. Nothing is + /// consumed in that case. + fn do_update_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result; + + /// Finishes the encryption, consuming the encryptor: flushes whatever plaintext was held back, + /// encrypted, into `output`, and returns how many leading bytes of it are ciphertext together + /// with the tag over the AAD and plaintext it has seen. The tag must be transmitted with the + /// ciphertext; the recipient passes it to [`AEADCipherDecryptor::do_decrypt_final`]. + fn do_encrypt_final( + self, + output: &mut [u8; FINAL_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError>; + + /// The exact ciphertext length for a `plaintext_len`-byte plaintext, i.e. the buffer + /// [`encrypt_out`](Self::encrypt_out) requires and the number of bytes it writes (the tag is + /// returned separately, not counted here). The default returns `plaintext_len` itself, which + /// holds for every conformant AEAD: unlike a padding scheme, an AEAD never expands or shrinks + /// the data it is given. + fn encrypt_out_len(plaintext_len: usize) -> usize { + plaintext_len + } + + /// One-shot: encrypts `plaintext` into `ciphertext`, which needs + /// [`encrypt_out_len`](Self::encrypt_out_len) bytes, authenticating `aad` along with it under a + /// fresh nonce. Returns the generated nonce, the number of bytes written, and the tag. + /// + /// Provided as `do_encrypt_init`, one `do_update_aad`, one `do_update_out` and + /// `do_encrypt_final`. + /// + /// # Errors + /// [`SymmetricCipherError::IncorrectOutputBufferLength`] if `ciphertext` is too short, checked + /// before any work is done; otherwise whatever the streaming methods return. + fn encrypt_out( + key: &KeyMaterial, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { + let needed = Self::encrypt_out_len(plaintext.len()); + if ciphertext.len() < needed { + return Err(SymmetricCipherError::IncorrectOutputBufferLength("ciphertext", needed)); + } + let (mut enc, nonce) = Self::do_encrypt_init(key)?; + enc.do_update_aad(aad)?; + let written = enc.do_update_out(plaintext, ciphertext)?; + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_encrypt_final(&mut final_buf)?; + // `encrypt_out_len` bounds `written + final_len`, so this fits in `ciphertext[..needed]`. + ciphertext[written..written + final_len].copy_from_slice(&final_buf[..final_len]); + Ok((nonce, written + final_len, tag)) + } + + /// As [`encrypt_out`](Self::encrypt_out), but sources randomness from the provided RNG. + fn encrypt_out_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { + let needed = Self::encrypt_out_len(plaintext.len()); + if ciphertext.len() < needed { + return Err(SymmetricCipherError::IncorrectOutputBufferLength("ciphertext", needed)); + } + let (mut enc, nonce) = Self::do_encrypt_init_rng(key, rng)?; + enc.do_update_aad(aad)?; + let written = enc.do_update_out(plaintext, ciphertext)?; + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_encrypt_final(&mut final_buf)?; + ciphertext[written..written + final_len].copy_from_slice(&final_buf[..final_len]); + Ok((nonce, written + final_len, tag)) + } + + #[cfg(feature = "std")] + /// One-shot, allocating: as [`encrypt_out`](Self::encrypt_out), returning the ciphertext as a + /// `Vec`. Only available with the `std` feature. + fn encrypt( + key: &KeyMaterial, + aad: &[u8], + plaintext: &[u8], + ) -> Result<([u8; NONCE_LEN], Vec, [u8; TAG_LEN]), SymmetricCipherError> { + let mut ciphertext = vec![0u8; Self::encrypt_out_len(plaintext.len())]; + let (nonce, written, tag) = Self::encrypt_out(key, aad, plaintext, &mut ciphertext)?; + ciphertext.truncate(written); + Ok((nonce, ciphertext, tag)) + } +} + /// Metadata about a cryptographic algorithm. pub trait Algorithm { /// String name for the algorithm, used consistently across the library. From 120b2fe2201ec2f02a6e1f716dab1958436bfc03 Mon Sep 17 00:00:00 2001 From: officialfrancismendoza Date: Wed, 9 Sep 2026 23:59:18 +0700 Subject: [PATCH 26/28] ascon, cli: add bouncycastle-ascon (SP 800-232 Ascon-AEAD128/Hash256/XOF128/CXOF128) implementing AEADCipherEncryptor/AEADCipherDecryptor via AsconAead128Encryptor/AsconAead128Decryptor, with HashFactory/XOFFactory registration and CLI wiring including a TaggedDecryptor-based decrypt stream --- Cargo.toml | 2 + alpha_0.1.3_release_notes.md | 558 ++++++++++++- cli/src/ascon_cmd.rs | 194 +++++ cli/src/helpers.rs | 34 +- cli/src/main.rs | 83 ++ cli/src/sha3_cmd.rs | 36 +- cli/tests/ascon_cli_tests.rs | 308 ++++++++ crypto/ascon/Cargo.toml | 25 + crypto/ascon/benches/ascon_benches.rs | 93 +++ crypto/ascon/src/ascon_aead128.rs | 865 +++++++++++++++++++++ crypto/ascon/src/ascon_cxof128.rs | 218 ++++++ crypto/ascon/src/ascon_hash256.rs | 185 +++++ crypto/ascon/src/ascon_xof128.rs | 172 ++++ crypto/ascon/src/lib.rs | 137 ++++ crypto/ascon/src/permutation.rs | 138 ++++ crypto/ascon/src/sponge.rs | 189 +++++ crypto/ascon/tests/aead128_tests.rs | 768 ++++++++++++++++++ crypto/ascon/tests/bc_test_data.rs | 242 ++++++ crypto/ascon/tests/cxof128_tests.rs | 221 ++++++ crypto/ascon/tests/hash256_tests.rs | 152 ++++ crypto/ascon/tests/xof128_tests.rs | 183 +++++ crypto/factory/Cargo.toml | 1 + crypto/factory/src/hash_factory.rs | 17 + crypto/factory/src/xof_factory.rs | 45 +- crypto/factory/tests/hash_factory_tests.rs | 24 + crypto/factory/tests/xof_factory_tests.rs | 115 ++- src/lib.rs | 1 + 27 files changed, 4950 insertions(+), 56 deletions(-) create mode 100644 cli/src/ascon_cmd.rs create mode 100644 cli/tests/ascon_cli_tests.rs create mode 100644 crypto/ascon/Cargo.toml create mode 100644 crypto/ascon/benches/ascon_benches.rs create mode 100644 crypto/ascon/src/ascon_aead128.rs create mode 100644 crypto/ascon/src/ascon_cxof128.rs create mode 100644 crypto/ascon/src/ascon_hash256.rs create mode 100644 crypto/ascon/src/ascon_xof128.rs create mode 100644 crypto/ascon/src/lib.rs create mode 100644 crypto/ascon/src/permutation.rs create mode 100644 crypto/ascon/src/sponge.rs create mode 100644 crypto/ascon/tests/aead128_tests.rs create mode 100644 crypto/ascon/tests/bc_test_data.rs create mode 100644 crypto/ascon/tests/cxof128_tests.rs create mode 100644 crypto/ascon/tests/hash256_tests.rs create mode 100644 crypto/ascon/tests/xof128_tests.rs diff --git a/Cargo.toml b/Cargo.toml index 63f0d999..7aa567d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ version = "0.1.3" # *** Internal Dependencies *** bouncycastle = { path = "./" } bouncycastle-aes = { path = "./crypto/aes" } +bouncycastle-ascon = { path = "./crypto/ascon" } bouncycastle-base64 = { path = "./crypto/base64" } bouncycastle-modes = { path = "./crypto/modes" } bouncycastle-core = { path = "crypto/core" } @@ -46,6 +47,7 @@ edition.workspace = true [dependencies] bouncycastle-aes.workspace = true +bouncycastle-ascon.workspace = true bouncycastle-base64.workspace = true bouncycastle-core.workspace = true bouncycastle-factory.workspace = true diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index d5185528..17b6e1fa 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -2,9 +2,561 @@ ## Major features -* New algorithms added to crypto/ : - * SM3 -- the SM3 hash (GB/T 32905-2016 / ISO/IEC 10118-3:2018), ported from bc-java. - * AES -- AES-128/192/256, along with its modes AES_ECB, AES_CBC, AES_GCM. +* New algorithms added to crypto/ (PR #89): + * sm3 -- the SM3 hash (GB/T 32905-2016 / ISO/IEC 10118-3:2018), ported from bc-java. Implements `Hash`, + `Suspendable` and `AlgorithmOID`, supports bit-oriented (partial final byte) messages per GB/T 32905-2016 s. 5.2 + with the partial byte in ASN.1 BIT STRING order like SHA-2/SHA-3, and is registered in `HashFactory` + (`"SM3"`) with a `bc-rust sm3` CLI subcommand. + * HMAC-SM3, in the hmac crate, registered in `MACFactory` (`"HMAC-SM3"`) with a `bc-rust hmac-sm3` CLI subcommand. + * Test vectors are the GB/T 32905-2016 Appendix A examples plus the bc-java `SM3DigestTest` / `HMac` vectors, with + additional digests cross-checked against OpenSSL and bc-java. + +New crate `bouncycastle-aes` (`bouncycastle::aes`): AES-128/192/256 as a raw keyed block +permutation (NIST FIPS 197), re-exported from the umbrella crate. + +* **Constant-time and table-free.** The S-box is evaluated as a Boolean circuit -- the 113-gate Boyar-Peralta + straight-line program, 32 AND / 77 XOR / 4 XNOR -- over eight `u32` bit-planes, so there is no secret-indexed + memory access and no secret-dependent branch anywhere, including in the key schedule. A table-driven "light" + AES that removes the tables only from the cipher still leaks through `SUBWORD()` in the expansion. +* **Low memory.** No lookup tables at all (0 bytes, against 512 bytes for BC Java's `AESLightEngine` and 2-8 KiB + for T-table engines) and no heap allocation. The only persistent state is the key schedule, stored bit-sliced + in a compressed form that is exactly the FIPS 197 Sec 5.2 size: `AES_128` 176 B, `AES_192` 208 B, `AES_256` 240 B. +* **Both directions from one value.** Decryption follows FIPS 197 Algorithm 3 (the straight inverse cipher) rather + than the equivalent inverse cipher of Sec 5.3.5, so it uses the unmodified key schedule -- one stored schedule + encrypts and decrypts, with no second copy and no transformation at construction time. +* **Two-block entry points.** The bit-sliced state holds two blocks, so `encrypt_2blocks` / `decrypt_2blocks` are + the natural unit of work and roughly double single-block throughput. `encrypt_block` / `decrypt_block` are + provided but do twice the necessary work; modes whose blocks are independent (CTR, and CBC/CFB decryption) + should prefer the pair form. +* Verified against FIPS 197 Appendix A.1/A.2/A.3 (every schedule word), FIPS 197 Appendix B, an exhaustive check + of all 256 S-box and inverse S-box inputs against Tables 4 and 6, SP 800-38A Appendix F.1 (ECB, all three key + lengths, both directions), and 2138 NIST ACVP `ACVP-AES-ECB` cases from `bc-test-data` (skipped with a warning + if that repository is not checked out). +* Deliberately ships no CLI subcommand, no factory entry and no `core` cipher-trait impls: a raw permutation can + only offer ECB, and those are mode-of-operation concerns. `Algorithm` is implemented (name and security + strength); per-mode OIDs and the `BlockCipherEncryptor` / `BlockCipherDecryptor` impls belong to the mode crates. +* Ships the type aliases `AES_CBC_128` / `AES_CBC_192` / `AES_CBC_256`, `AES_CFB_128` / + `AES_CFB_192` / `AES_CFB_256`, `AES_CFB8_128` / `AES_CFB8_192` / `AES_CFB8_256`, + `AES_CTR_128` / `AES_CTR_192` / `AES_CTR_256` (12-byte nonce, 4-byte counter) and + `AES_ECB_128` / `AES_ECB_192` / `AES_ECB_256`, which fill in the + const parameters of `bouncycastle-modes`' `Cbc`, `Cfb`, `Cfb8`, `Ctr` and `Ecb`. The three stream + modes leave the direction as the only type parameter; the two **block** modes, CBC and ECB, take + a padding scheme as well -- `AES_CBC_128` -- because neither is defined on data + that is not a whole number of blocks, so the scheme is a choice the caller has to make and one + both ends must agree on. Naming it in the type makes a mismatched pair a compile error instead of + a decryption that returns plausible rubbish. `PaddedMode` is the crate-internal projection that lets a single + alias carry both parameters, `PaddedEncryptor` and `PaddedDecryptor` being distinct types. They are aliases only -- no new engine + code, and each one's doctest round-trips and shows that a misaligned length fails to compile. + +New crate `bouncycastle-modes` (`bouncycastle::modes`): cipher modes of operation +(NIST SP 800-38A), providing **CBC** (Sec 6.2), **CFB128** and **CFB8** (Sec 6.3, `s = b` and +`s = 8`), **CTR** (Sec 6.5) and **ECB** (Sec 6.1) -- four of the recommendation's five modes, with +only OFB outstanding. Re-exported from the umbrella crate. + +* `Cbc`, `Cfb`, `Cfb8` and `Ecb`, each ``, and `Ctr`, which takes a + nonce length as a fifth parameter, over any + `ElectronicCodeBook`, so the crate depends on no concrete cipher. The direction is a type parameter: + the encryptor trait is implemented only for `<_, Encrypting, _, _>` and the decryptor trait + only for `<_, Decrypting, _, _>`, making a wrong-direction call a compile error rather than a + runtime check. +* **Block modes and stream modes.** `Cbc` and `Ecb` are block ciphers + (`BlockCipherEncryptor` / `BlockCipherDecryptor`): whole blocks in, whole blocks out, with + arbitrary-length data going through `bouncycastle-padding`. `Cfb`, `Cfb8` and `Ctr` are stream + ciphers (`StreamCipherEncryptor` / `StreamCipherDecryptor`): any length in, the same length out, + no padding layer and no finalization step. That split follows SP 800-38A Sec 5.2, which requires a + multiple of the *block* size only for ECB and CBC, a multiple of the *segment* size `s` for CFB, + and nothing at all for CTR ("the plaintext need not be a multiple of the block size"). +* **The IV is generated, never accepted.** SP 800-38A Sec 5.3 requires the CBC *and CFB* IV to be + *unpredictable*, not merely unique, so `do_encrypt_init` draws one from the library's default + OS-backed DRBG (Appendix C's second recommended method) and returns it; there is no API for + supplying your own. Known-answer tests drive `do_encrypt_init_rng` with a fixed-output test RNG. + This matters more for CFB than for CBC: CFB XORs a keystream, so a repeated key-and-IV pair leaks + `P1 XOR P1'` outright rather than merely whether the blocks were equal. +* **Parallel decryption.** Sec 6.2 notes CBC decryption's inverse cipher calls can run in + parallel, so `do_decrypt_blocks` walks the ciphertext in fours through + `ElectronicCodeBook::decrypt_4blocks`, then pairs through `decrypt_2blocks`, then a one-block + remainder. A toy permutation that rotates its four results proves the four path is taken, and + only for full fours. Measured against an + otherwise identical permutation that does not override the pair methods, this is **1.83x** the + decryption throughput (67.9 vs 37.1 MiB/s, AES-128, 16 KiB, N=8). CBC encryption is serial by + construction and does not use it. +* Strictly block-aligned, as Sec 5.2 requires of CBC. Arbitrary-length data goes through + `bouncycastle-padding`'s `PaddedEncryptor` / `PaddedDecryptor`, which wrap either mode; no padding + logic lives in this crate. `crypto/modes/tests/cfb_tests.rs` round-trips every length from 0 to + `3 * BLOCK_LEN + 1` through PKCS7 to pin that the two crates compose. +* Verified against all six SP 800-38A Appendix F.2 vectors (CBC-AES128/192/256, Encrypt and + Decrypt), each checked in one call, one block at a time, in a `3 + 1` grouping that exercises the + pair remainder, and through the `_out` variant. Appendix D error propagation is tested + exhaustively for the IV (every one of the 128 bit positions flips exactly its own bit of P1) and + for a ciphertext bit error (affects exactly two blocks). +* Also verified against the **2150 NIST ACVP `ACVP-AES-CBC` AFT cases** from `bc-test-data` (all + three key lengths, both directions, 60 of them spanning 2-10 blocks). Each case is run twice -- + block by block, and in pairs with a one-block remainder -- so the `decrypt_2blocks` path is + exercised against real vectors, not only against the toy permutation. Unlike the ECB response + file, the CBC one carries only the answer against a `tcId`, so the request and response files are + joined; the 6 MCT groups are skipped and the count reported. These vectors were already in + `bc-test-data` and previously unused. +CFB128 (`Cfb`), SP 800-38A Sec 6.3 with `s = b`: + +* **A stream cipher.** Sec 6.3 parameterises CFB by a segment size `s` with `1 <= s <= b`, and + `Cfb` implements `s = b` -- CFB128 for AES. With `s = b` the spec's + `LSB_{b-s}(I_{j-1}) | C#_{j-1}` collapses to `Ij = C_{j-1}` and `MSB_s(Oj)` to `Oj`, which the + module docs derive step by step. CFB never puts the data through the cipher, only the input + block, so `Cfb` implements `StreamCipherEncryptor` / `StreamCipherDecryptor`: a `&mut [u8]` of + any length, in place, chunked however the caller likes, with no padding layer. +* **The short final segment.** Sec 5.2 defines CFB only on a multiple of `s`, and Appendix A puts + padding outside the recommendation's scope. Rather than reject a message that is not a whole + number of blocks, `Cfb` takes the `s = 8r` step of the Sec 6.3 equations for the last segment + alone -- `C#_n = P#_n XOR MSB_{8r}(On)` -- discarding the rest of `On` exactly as Sec 6.3 + discards `b - s` bits of every output block when `s < b`. No input block is formed after the last + segment, so the feedback rule that distinguishes `s < b` from `s = b` is never reached and the + result is unambiguous. This is what streaming CFB128 implementations do in practice, and the + ciphertexts interoperate: checked byte for byte against OpenSSL's `EVP_aes_128_cfb128` on a + 37-byte message, in both directions. +* **One buffer, three roles.** Within a segment the single stored block holds the ciphertext + produced so far and the unused tail of `Oj` at once -- each ciphertext byte is written over the + keystream byte that produced it, and is exactly what the next input block wants in that position + -- so the same 16 bytes are the input block, then the output block, then the next input block, + with no copy and no second buffer. That costs one `usize` over `Cbc` (200/232/264 B for + AES-128/192/256) to record how much of the current segment has been used. +* **Decryption uses the forward cipher function.** Sec 6.3 applies `CIPH_K` in both directions, so + `Cfb<_, Decrypting, _, _>` never calls `decrypt_block` or `decrypt_2blocks`. This is pinned by a + test permutation whose inverse methods panic, run over both the pair and single-block paths -- so + the claim is enforced rather than merely documented. +* **Parallel decryption**, via `encrypt_4blocks` / `encrypt_2blocks` (fours, then pairs, then a single block, like CBC): Sec 6.3 notes CFB decryption's forward cipher + calls "can be performed in parallel if the input blocks are first constructed (in series) from the + IV and the ciphertext", and with `s = b` those input blocks simply *are* the IV followed by the + ciphertext. Re-measured after the stream-cipher rewrite: against an otherwise identical + permutation that does not override the pair methods, this is **1.96x** the decryption throughput + (106.8 vs 54.6 MiB/s, AES-128, 16 KiB, N=8). In the same run CFB decryption was **1.26x** CBC + decryption (106.8 vs 84.9 MiB/s), because the bit-sliced engine's forward direction is cheaper + than its inverse and CFB only ever needs the forward one. CFB encryption is serial by + construction and does not use the pair path -- verified, not assumed: the swapped-pair test + permutation produces identical ciphertext under `Cfb` encrypt. +* **The byte path is close to free on encryption and modest on decryption.** Calls that are not a + whole number of blocks end mid-segment and the next call finishes that segment byte by byte. At + 125-byte calls (7 blocks and 13 bytes) encryption measured 51.1 MiB/s against 51.4 for + block-aligned calls, and decryption 90.6 against 106.8 -- the decrypt side pays because a partial + segment at each end of a call breaks the four-block batch. +* Verified against all six SP 800-38A **Appendix F.3.13-F.3.18** vectors (CFB128-AES128/192/256, + Encrypt and Decrypt) in the same four groupings as CBC. F.3 additionally tabulates the *output + blocks* -- the keystream -- so those are checked against the raw permutation too + (`Oj == CIPH_K(I_j)` and `Cj == Pj XOR Oj` for all four segments of all three key lengths), which + pins the mode's internals and not just its final output. As a transcription cross-check, CFB128 + is required to agree with **Appendix F.4.1 (OFB)** on the first block -- both compute + `C1 = P1 XOR CIPH_K(IV)` -- and to disagree from the second. +* Also verified against the **2138 NIST ACVP `ACVP-AES-CFB128` AFT cases** from `bc-test-data` (all + three key lengths, both directions, 54 of them spanning 2-10 blocks), each run in four groupings: + block by block, in pairs with a remainder, as one call over the whole payload, and in 5-byte + calls that never line up with a block, so the byte path is exercised against real vectors with a + segment left open across calls. The 6 MCT groups are skipped and the count reported. These + vectors were already in `bc-test-data` and previously unused. +* Appendix D error propagation is tested in the direction that distinguishes CFB from CBC. Table D.2 + gives CFB "SBE in the decryption of Cj": every one of the 128 bit positions of `C2` is flipped and + required to flip *exactly* that bit of `P2` (the block the attacker aimed at, unlike CBC where it + lands in `P3`), to randomise `P3`, and to leave `P1` and `P4` untouched. The IV case is checked + with real AES, where a corrupted IV must *randomise* `P1` rather than flip a bit in place, and + must not affect any later block -- with `s = b`, Appendix D's "first `i/s` (rounding up)" + segments is one segment for every bit position. +* Mutation-tested: `cargo mutants -p bouncycastle-modes` reports **0 surviving mutants** across + the whole crate (220 mutants, 108 caught, 112 unviable, 0 missed, 0 timed out) -- 45 caught in + `ctr.rs`, 28 in `cfb.rs`, 16 in `cbc.rs`, 14 in `cfb8.rs`, 2 each in `ecb.rs` and `iv.rs` -- + including every `^`-to-`|`/`&` substitution and every keystream-stubbing mutant in the three + keystream modes. One mutant needed the tests to reach past runtime behaviour: stubbing out CTR's + compile-time counter-width guard cannot fail any runtime test, so the `compile_fail` doctests on + `Ctr` are what kill it. +* Still not implemented, and listed in the crate docs: **CFB1** (`s = 1`), whose segment is a + single bit rather than a whole number of bytes and so does not fit a byte-oriented API at all, + and **OFB** and **CTR**. + +CFB8 (`Cfb8`), SP 800-38A Sec 6.3 with `s = 8`: + +* **A different mode, not a variant.** `Cfb8` is its own type, because CFB8 and CFB128 are not + interoperable: they agree on the first byte of ciphertext -- `P1 XOR MSB_8(CIPH_K(IV))` in both -- + and diverge from the second, since `s = b` replaces the whole input block with the ciphertext + block while `s = 8` shifts one byte into a register. Both the type docs and the CLI help say so, + and a test asserts exactly that agree-then-diverge pattern rather than merely that the outputs + differ. +* **The shift register is the spec's own alternative description.** `I_{j+1} = LSB_{b-8}(Ij) | Cj` + is implemented as `rotate_left(1)` followed by writing the ciphertext byte into the last + position, which is Sec 6.3's "the bits of the first input block circularly shift s positions to + the left, and then the ciphertext segment replaces the s least significant bits of the result", + in that order. `MSB_8(Oj)` is the first byte of the output block; the other `b - 8` are + discarded, as Sec 6.3 requires. +* **A stream cipher with a one-byte segment**, so every byte string is a valid message: no + alignment rule, no padding, no partial-segment state. Same size as `Cbc` (192/224/256 B for + AES-128/192/256). +* **One forward cipher per byte.** Discarding 15 of every 16 output bytes is what the mode costs: + encryption measured **3.41 MiB/s** against CFB128's 51.4 on the same data and cipher, a factor of + 15. That is inherent to `s = 8`, and the crate docs, the type docs and the CLI help all say to + prefer `Cfb` unless a byte-granular self-synchronising stream is required or a format demands + CFB8. +* **Decryption still batches.** Sec 6.3's parallel decryption applies: the successive register + states depend only on the IV and the ciphertext, so they are built in series -- byte shuffling, + no cipher calls -- and the forward ciphers then run four at a time through `encrypt_4blocks`, + then in pairs. Measured **1.94x** the throughput of the same decryption in 1-byte calls, which + never batch (6.61 vs 3.40 MiB/s). Encryption cannot batch and does not. +* **Decryption never calls the inverse cipher**, as in CFB128, pinned by the same test permutation + whose inverse methods panic, run over the four-block, pair and single-byte paths. +* Verified against all six SP 800-38A **Appendix F.3.7-F.3.12** vectors (CFB8-AES128/192/256, + Encrypt and Decrypt), each in seven groupings from one byte per call up to the whole message. + F.3.7's tabulated **input and output blocks** -- all 18 of each -- are checked three ways: that + each input block is the previous one shifted with the ciphertext byte appended, that each output + block is `CIPH_K` of it through the raw permutation, and that `Cj == Pj XOR MSB_8(Oj)`. That pins + the register construction against the spec's own table rather than only the final ciphertext. +* Also verified against the **2138 NIST ACVP `ACVP-AES-CFB8` AFT cases** from `bc-test-data` (all + three key lengths, both directions, 60 of them 16 to 160 bytes), each run in four groupings -- + whole message, byte by byte, 8-byte calls and 3-byte calls that never line up with the batch. + The 6 MCT groups are skipped and the count reported. These vectors were already in + `bc-test-data` and previously unused. +* Appendix D error propagation is checked in the form that distinguishes CFB8 from CFB128. Table + D.2 gives "SBE in the decryption of Cj" plus "RBE in ... Cj+1,...,Cj+b/s", and `b/s` is **16** + here rather than 1: with real AES, flipping a ciphertext bit flips exactly that bit of that + plaintext byte, randomises the following 16 bytes, and then decryption **resynchronises + exactly** -- byte `j + 17` onwards is required to be byte-identical to the original plaintext. + That self-synchronisation is the property CFB8 is chosen for, and the equality assertion on the + tail is what pins it. +* Interoperability checked byte for byte against OpenSSL's `EVP_aes_128_cfb8` on a 37-byte message, + in both directions. + +CTR (`Ctr`), SP 800-38A Sec 6.5: + +* **The nonce is the init data, and its length picks the counter width.** Sec 6.5 needs a sequence + of counter blocks that are distinct across every message under a key, and Appendix B.2's second + approach builds each one as a message nonce followed by a counter: "if N is the message nonce for + a given message, then the jth counter block is given by `Tj = N | [j]m`". `Ctr` takes that + literally, splitting the block by the length of its init data: the init data *is* the nonce, and + the remaining `BLOCK_LEN - INIT_DATA_LEN` bytes are the counter. The counter is capped at **4 + bytes** and must be at least 1, both checked at compile time, so on AES the nonce is 12, 13, 14 or + 15 bytes and a wrong one is a compile error rather than a runtime `Err`. +* **The counter starts at zero**, i.e. `Tj = N | [j - 1]m`, one below B.2's `[j]m`. Appendix B + presents B.2 as one of "Two examples of approaches" and closes by allowing "other methods and + approaches for achieving the uniqueness property", so both indexings satisfy the only normative + requirement, that the blocks be distinct. Zero is what makes a nonce-with-zero-counter vector line + up with an implementation handed the whole block as an IV -- which is how the ACVP vectors are + written, and how OpenSSL is driven. +* **Running out of counter is an error, and nothing is consumed.** A `CTR_LEN`-byte counter gives + `2^(8 * CTR_LEN)` blocks -- 64 GiB for a 4-byte counter, 4 KiB for a 1-byte one -- and Appendix + B.1 bounds a message at exactly that ("provided that `n <= 2^m`"). Past it the counter would + repeat, which for a keystream mode is keystream reuse *within one message*. `Ctr` therefore checks + the whole call up front and returns `SymmetricCipherError::StateError` without touching the data, + so a message is never half-encrypted before the mode notices. This is the first and only use in + the crate of the `Result` the data methods have always returned; CBC, CFB, CFB8 and ECB never fail + them. The counter is held as a `u64` rather than as the counter bytes precisely so that exhaustion + is representable: the counter field itself wraps. +* **Both directions are parallel**, the only mode here of which that is true. Sec 6.5: "In both CTR + encryption and CTR decryption, the forward cipher functions can be performed in parallel." + Counter blocks depend on nothing but the nonce and the index, so encryption batches through + `encrypt_4blocks` / `encrypt_2blocks` exactly as decryption does, and encryption and decryption are + the same operation. Only the forward cipher function is ever used, as in the CFB modes. +* The keystream block is the one buffer in this crate wrapped in `Secret`: a call may end part-way + through a block and the remainder is kept for the next one, and unlike a chaining value that + remainder is live key material for the bytes still to come. 224/256/288 B for AES-128/192/256 with + a 12-byte nonce. +* Verified against **1853 of the 2138 NIST ACVP `ACVP-AES-CTR` AFT cases** (all three key lengths, + both directions), each in four groupings. The other 285 begin at a non-zero counter and so cannot + be expressed through a nonce-plus-zero-counter API; they are skipped with the count reported. +* **Every ACVP case is a single block**, so none of them exercises the counter increment at all -- + a mode whose counter never advanced, or advanced little-endian, passes the entire set. (Checked, + not assumed: a deliberately little-endian counter was run against the ACVP suite while these tests + were written, and passed.) Two things close that gap. `ctr_vector_tests.rs` adds five-block + vectors for all three key lengths generated with **OpenSSL 3.0.13**, whose last block is partial + so they also pin Sec 6.5's `MSB_u(On)`; and `ctr_tests.rs` checks the counter blocks against the + raw permutation **at all four counter widths**, across the 255-to-256 carry where the width allows + it. That width sweep matters because the counter occupies a width-dependent slice, and getting it + wrong is invisible to a round-trip test: both directions would build the same wrong block and + still recover the plaintext. +* Cross-checked against **BC Java's `SICBlockCipher`**, which is the closest comparison available: + unlike OpenSSL, whose `-aes-*-ctr` takes the whole block as its IV and so has no notion of a + nonce, `SICBlockCipher` is built the same way -- a short IV goes in the leading bytes, the rest is + zero-filled so the counter starts at 0, it increments big-endian with carry, and it throws + `IllegalStateException("Counter in CTR/SIC mode out of range.")` once the carry would reach the + IV. Same construction, same start, same overflow rule; the only difference is that BC Java caps + the counter at `min(8, blockSize / 2)` bytes where this type stops at 4, so ours is a subset and + the two agree exactly on nonces of 12 to 15 bytes. Agreement is byte for byte on the 69-byte + vectors and on a 5000-byte message across the 255-to-256 carry at all three key lengths, and the + counter limit falls on the same byte at both the 1-byte (4 KiB) and 2-byte (1 MiB) widths. + `ctr_bc_java_tests.rs` pins what neither the ACVP nor the OpenSSL suite can reach: the keystream + at **1, 2 and 3-byte counters**, including both ends of the 1-byte counter's range and the + 2-byte counter's carry from block 255 to 256. +* SP 800-38A **Appendix F.5** is not transcribed: its vectors start the counter at `0xfcfdfeff` + rather than zero, so they cannot be expressed through this API. What F.5 does corroborate is the + split -- across its four blocks the counter moves only within the last four bytes, leaving the + leading twelve fixed -- and a test pins that reading. +* The counter limit is tested at two widths: a 1-byte counter (256 blocks, 4 KiB) and a 2-byte one + (65536 blocks, 1 MiB), in both directions, including that a refused call leaves the data and the + counter untouched so the bytes that do fit are unaffected by the attempt. + +`cli`: twelve new subcommands -- `aes{128,192,256}-cbc`, `-cfb`, `-cfb8` and `-ctr` -- each taking +`encrypt` or `decrypt` and streaming stdin to stdout in 1 KiB chunks. + +* The mode-independent plumbing lives once, in two halves that share their key loading and their + `encrypt` / `decrypt` spelling. `cli/src/block_mode_cmd.rs` holds the block half -- stdin framing + with block-alignment enforcement, hex/binary output -- generic over `BlockCipherEncryptor` / + `BlockCipherDecryptor`; `cli/src/stream_mode_cmd.rs` holds the stream half, generic over + `StreamCipherEncryptor` / `StreamCipherDecryptor`, which buffers nothing to a boundary and + rejects no length. `aes_cbc_cmd.rs`, `aes_ecb_cmd.rs`, `aes_cfb_cmd.rs` and `aes_cfb8_cmd.rs` are + thin dispatchers, so the commands cannot drift apart on the parts that affect correctness. +* Key from `--key` (hex) or `--key-file` (binary or hex), with the usual note that secrets on the + command line end up in shell history. The key length must match the variant exactly. +* **The IV travels in the ciphertext**: since there is no API for supplying one, `encrypt` writes + the generated IV as the first 16 bytes of its output and `decrypt` reads it back from the first + 16 bytes of its input, so `encrypt | decrypt` composes with no `--iv` flag anywhere. The IV need + not be secret (SP 800-38A Sec 5.3), so this is sound. +* Input to the `-cbc` and `-ecb` commands must be a whole number of 16-byte blocks; unaligned input + is rejected with a message saying the commands apply no padding rather than being silently + padded. The `-cfb` and `-cfb8` commands take **any length** and pad nothing, because they are + stream ciphers; their output is exactly as long as their input. +* The `-cfb` commands are **CFB128** and the `-cfb8` commands are **CFB8**, and every subcommand's + help names its segment size and says the two are not interoperable, because they would otherwise + silently produce incompatible output. +* The `-ctr` commands write a **12-byte nonce**, not the 16-byte IV every other mode writes, so + their output is 12 bytes longer than their input rather than 16. The per-command help says so, and + `cli/tests/aes_ctr_cli_tests.rs` (21 tests) pins it along with the OpenSSL vectors end to end, + CTR's total malleability (a flipped ciphertext bit flips exactly one plaintext bit and disturbs + nothing else), and that a CFB command cannot read a CTR ciphertext. +* Reads need not respect block boundaries: bytes accumulate in a 1 KiB buffer that goes through the flat + `do_*_out::<1024>` when full, and the whole-block remainder at end of input goes one block at a time; verified by + round-tripping 64 KiB through `dd bs=3`. +* Verified against SP 800-38A F.2 (CBC), F.3.13/F.3.15/F.3.17 (CFB128) and F.3.7/F.3.9/F.3.11 + (CFB8): prepending the spec's IV to the spec's ciphertext and running `decrypt` reproduces the + spec's plaintext for all three key lengths in every mode. The `encrypt` direction was + cross-checked against OpenSSL under the IV the CLI generated -- for CBC, and for both CFB modes + on a 37-byte (deliberately unaligned) message, where our ciphertext and `openssl enc + -aes-128-cfb` / `-aes-128-cfb8` agree byte for byte and each tool decrypts the other's output. +* `cli/tests/aes_cbc_cli_tests.rs` (16 tests) drives the built binary as a subprocess via + `CARGO_BIN_EXE_bc-rust`, so all of the above is asserted by `cargo test` rather than by hand: + the F.2 vectors, round trips across the chunk boundary, a fresh IV per invocation, hex/binary + agreement, `--key-file` in both hex and binary, and every error path with its message. +* `cli/tests/aes_cfb_cli_tests.rs` (21 tests) mirrors that suite -- the shared plumbing is generic + over the mode, so a wiring mistake in the CFB dispatcher would not show up in the CBC tests -- and + adds four CFB-specific checks: the F.3 vectors, the Appendix D single-bit malleability observed + end to end through the pipe, a guard that a CFB ciphertext does not decrypt as CBC or vice + versa (neither mode is authenticated, so the mismatch is otherwise silent), and that every length + from 0 to 33 bytes round-trips with the ciphertext exactly as long as the plaintext. +* `cli/tests/aes_cfb8_cli_tests.rs` (19 tests) does the same for CFB8, including the F.3.7/9/11 + vectors, every length from 0 to 33 bytes, and the Appendix D window: a flipped ciphertext bit + flips the same bit of the same plaintext byte, corrupts the next 16 bytes, and then the output is + required to be byte-identical to the original again. + +ECB (`Ecb`), SP 800-38A Sec 6.1: + +* **The raw permutation with the mode API, for interoperability only.** `Ecb` implements + `BlockCipherEncryptor` / `BlockCipherDecryptor` with `INIT_DATA_LEN = 0`: `do_encrypt_init` returns an empty array and + draws nothing from the RNG, `do_decrypt_init` takes one. Same direction typing, streaming and one-shot methods, + compile-time length checks and padding-layer composition as `Cbc` / `Cfb`, so a key-wrapping scheme, a legacy protocol + or a test-vector harness that needs ECB can use it through the same interface. The crate docs, the type docs and the + CLI help all say the same thing about it: **not a confidentiality mode for data** (Sec 6.1: "any given plaintext block + always gets encrypted to the same ciphertext block"). One block smaller than `Cbc` / `Cfb`, since nothing chains + (176 / 208 / 240 B for AES-128/192/256). +* **Both directions batch.** Sec 6.1 allows forward and inverse cipher calls "to be computed in parallel", so encryption + as well as decryption walks the blocks through `ElectronicCodeBook::{en,de}crypt_4blocks`, then the pair methods, then + a single block. The swapped-pair and rotated-four test permutations prove both paths are taken in both directions. +* `aes128-ecb` / `aes192-ecb` / `aes256-ecb` CLI subcommands over the shared block-mode plumbing, which is now generic + over `INIT_DATA_LEN`: nothing is prepended on `encrypt` or consumed on `decrypt`, so output is exactly as long as + input. The per-command help carries the warning. +* Verified against all six SP 800-38A **Appendix F.1** vectors (ECB-AES128/192/256, Encrypt and Decrypt) in five + groupings each -- and, since there is no IV, `encrypt` is checked against the published ciphertext too, through the + streaming API and the one-shot. Each tabulated ciphertext block is also checked to be `CIPH_K` of its plaintext block + through the raw permutation. The **NIST ACVP `ACVP-AES-ECB`** set (2138 AFT cases) already used by `aes` + is run again through the mode API, both directions, in three groupings including one that reaches the four-block + path. Structural tests pin the Sec 6.1 equations against a reference over the toy permutation, determinism and the + codebook property, Appendix D error propagation (a corrupted block randomises itself and nothing else, checked over + all 128 bit positions with real AES), the empty init data, and composition with `bouncycastle-padding`. + +`core`: new `ElectronicCodeBook` trait (`crypto/core/src/traits.rs`), the raw +keyed permutation -- `CIPH_K` / `CIPH^-1_K` of SP 800-38A Sec 5.1 -- that a mode is built on. +`new`, `encrypt_block`, `decrypt_block`, plus provided `encrypt_2blocks` / `decrypt_2blocks` that +default to two single-block calls and `encrypt_4blocks` / `decrypt_4blocks` that default to two pair +calls, all of which bit-sliced implementations override (AES the pair form, SM4 both). The block methods +are infallible; only `new` can fail, and only on the key. `bouncycastle-aes` implements +it for all three key lengths (the data-encryption traits are still deliberately not implemented +there). + +`core`: new `SimpleCipherEncryptor` and +`SimpleCipherDecryptor` traits, the arbitrary-length data API a +caller uses, as opposed to the block-aligned `BlockCipher*` traits a mode implements. Their shape is +taken from `PaddedEncryptor` / `PaddedDecryptor`, which now implement them: streaming +`do_{en,de}crypt_init[_rng]`, exact `update_out_len`, `do_update_out`, and a consuming `do_final` that +returns the `FINAL_LEN` trailing buffer (the padded block; a tag for an AEAD) paired with how many of its +bytes are output -- always `FINAL_LEN` except for a padding scheme that adds nothing to aligned data -- +and, for the decryptor, how many of them are data. `do_final_out`, the `_out` one-shots +(`encrypt_out[_rng]`, `decrypt_out`, with `encrypt_out_len` exact and `decrypt_out_max_len` an upper +bound, checked before any work is done) and the `std` `Vec` one-shots are provided over the streaming +methods, so an implementor writes six methods. + +The older one-shot-only `SymmetricCipher` trait is **deleted**, and its four methods -- `encrypt`, +`encrypt_out`, `decrypt`, `decrypt_out` -- move onto `AEADCipher`, which was its only remaining +user. Every other kind of cipher now reaches an arbitrary-length one-shot some other way: a block +mode through `SimpleCipherEncryptor` / `SimpleCipherDecryptor` and the padding adapters, a +stream mode through those same traits directly. `AEADCipher` therefore drops the supertrait and +declares the four itself, against `NONCE_LEN`, with the documentation saying what they mean for an +AEAD: no additional authenticated data, and a ciphertext layout that is the implementation's +business because the tag has to go somewhere. `TestFrameworkSimpleCipher::test`, which was that +trait's suite, moves to `TestFrameworkAEADCipher::test_plain_one_shots` and is called from +`TestFrameworkAEADCipher::test`, so an AEAD implementor keeps the coverage without asking for it. + +That move also closed the last of a latent bug recorded in `core-test-framework/summary.md`: two +security-strength loops unwrapped `set_security_strength` at all five strengths, which a key shorter +than 32 bytes cannot carry, so they would have panicked for the first AEAD implementor — ASCON-128 +and AES-128-GCM among them. Relocating one of them into a method the AEAD suite calls would have +made that worse, so both now carry the same key-length guard the block and stream suites already +had. Every strength loop in the file is guarded. + +Stream ciphers also reach the arbitrary-length API: `StreamCipherEncryptor` and +`StreamCipherDecryptor` get blanket impls of `SimpleCipherEncryptor` / `SimpleCipherDecryptor` +with `FINAL_LEN = 0`, written in terms of the in-place `do_encrypt` / `do_decrypt`. An implementor +still writes only the in-place methods, but a caller can use `encrypt_out`, `do_update_out` and the +`std` one-shots, and can hold a stream mode through the same trait as a padded block mode -- which +is what makes "any of the five modes behind one trait" true rather than aspirational. For a stream +cipher the length predictions are exact rather than upper bounds, and `do_final` has nothing to +produce. The one cost is that both traits then spell `do_encrypt_init` identically, so code with +both in scope must qualify the call; `crypto/modes/tests/simple_cipher_api_tests.rs` is written +that way deliberately, to show it is workable. That file also runs all three stream modes through +`TestFrameworkSimpleCipher::test_encryptor_decryptor`, the same conformance suite the padded +adapters run, and checks the separate-output API against the in-place one byte for byte. + +Mutation-tested with `--test-workspace`, which is what these blanket impls need: run against core's +own tests alone they look untested, because core has no implementors of its own traits. Scoped to +the change, 45 mutants, 22 caught, 19 unviable, 4 missed -- all four the same equivalent mutant, +`[]` against `[0; 0]` and `[1; 0]` for a zero-length array, which no test can distinguish because +they are the same value; both sites carry a comment saying so. The one genuinely uncovered mutant +the run found, the decryptor's output-buffer length comparison, is now covered. + +`StreamCipher` is **replaced** by the split pair `StreamCipherEncryptor` / `StreamCipherDecryptor`, +shaped like `BlockCipherEncryptor` / `BlockCipherDecryptor` and for the same reasons: the direction +is encoded in the type, and a policy can permit decryption of an algorithm while forbidding new +encryptions. The old trait carried both directions and a `BLOCK_LEN` const parameter on every data +method, which a stream cipher has no use for; the new pair takes a `&mut [u8]` of any length, works +in place, generates its own init data in the constructor (never accepting one), and provides its +one-shots over a single implementor hook per direction. `Cfb` and `Cfb8` are its first implementors. + +Testing: + +* `core-test-framework` gains `TestFrameworkSimpleCipher::test_encryptor_decryptor`, which pins the + paired contract: one-shot round trips at every length up to a few final chunks, the `std` one-shots + against the `_out` ones, streaming in eight chunkings with `update_out_len` exact on every call, + `do_final_out` against `do_final`, a driven RNG reproducing its init data and determining the + ciphertext, corruption detection, short output buffers refused with the required length, and the + key-type and security-strength policy. The padded adapters run it. +* `core-test-framework` gains `TestFrameworkElectronicCodeBook`, which pins the trait contract: + both directions are inverses either way round, the permutation is injective, and the pair + methods are indistinguishable from two single-block calls **including their order** -- the check + that makes an override safe. +* Fixed a latent bug in `TestFrameworkBlockCipher`: it unwrapped `set_security_strength` at all + five strengths, which a key shorter than 32 bytes cannot carry, so the framework panicked for + any 16- or 24-byte key. It now skips the strengths the key length cannot hold. The bug was + invisible until now because nothing in the workspace implemented the block cipher traits. The + identical loop in `TestFrameworkSimpleCipher` and `TestFrameworkAEADCipher` got the same fix in + the same PR, and each also gained a `strengths_tested > 0` assertion so the sweep cannot silently + become vacuous again. `bouncycastle-ascon`'s `AsconAead128Encryptor`/`AsconAead128Decryptor` + (16-byte key) are now the first implementors to actually exercise the AEAD suite's guard. +* `TestFrameworkStreamCipher::test` was a `todo!()` and is now implemented for the + `StreamCipherEncryptor` / `StreamCipherDecryptor` pair, carrying the same key-length guard as the + block suite from the start. It pins the paired contract: one-shot round trips, streaming in nine + chunkings checked against the one-shot and against every other chunking (including empty calls, + so a call may end mid-segment), the RNG-taking constructors reproducing their init data and + determining the ciphertext, distinct init data across runs, the wrong key type rejected in both + directions, and the security-strength policy. `Cfb` and `Cfb8` both run it. + +* Block cipher padding (PR #97): + * padding -- new crate (`bouncycastle-padding`, no_std, re-exported as `bouncycastle::padding`) providing `PKCS7`, + the padding scheme of RFC 5652 s. 6.3, for any block length 1..=255 (enforced at compile time). `unpad` examines + every byte with `Condition` mask arithmetic and has a single public decision point, so it does not leak a + padding oracle through timing or error detail. + * `PaddedEncryptor` / `PaddedDecryptor` adapt a block-aligned `BlockCipherEncryptor` / + `BlockCipherDecryptor` to arbitrary-length data: streaming `do_update_out` / `do_final(self)` plus one-shot + `encrypt_out` / `decrypt_out`, with exact output-length helpers. The buffered partial plaintext block is held in + a `Secret`, and the decryptor withholds one complete block until `do_final`, since only the last block carries + padding. + * `core` gains the `Padding` trait (in-place `pad(block, data_len)`, constant-time + `unpad(block) -> data_len`, and `ALWAYS_PADS`, whether the scheme appends a block to already-aligned data) and + `PaddingError { DataLengthTooLong, InvalidPadding, PaddingNotPermitted }`, wrapped as a new variant of + `SymmetricCipherError`. + * `NoPadding`: the absence of padding as a `Padding` scheme, for data that must already be a whole number of + blocks. `pad` never writes a byte and returns `PaddingNotPermitted` whenever called; `unpad` reports the whole + block as data; `ALWAYS_PADS` is false. Through `PaddedEncryptor` / `PaddedDecryptor` this *enforces* alignment + with the arbitrary-length API shape: an aligned message passes through with its length unchanged and no final + block, an unaligned one fails at `do_final` / `encrypt_out`, and an empty ciphertext decrypts to the empty + message. The test framework's `TestFrameworkSimpleCipher` gained `required_alignment`, which makes it assert + that every unaligned length is refused. + * Tests are derived from the RFC 5652 padding rule; the adapters are driven with a toy XOR-CBC cipher implementing + the new block cipher traits, covering every data length, ten chunkings in both directions, tampering, malformed + lengths, and buffer sizing. Criterion bench included. + +`core`: new `AEADCipherEncryptor` and +`AEADCipherDecryptor` traits (#119/#120), the streaming API +for an authenticated cipher, shaped like `SimpleCipherEncryptor` / `SimpleCipherDecryptor` (separate +input/output buffers, exact `update_out_len`, generated nonce) with the two things authentication +adds: an AAD phase (`do_update_aad`, repeatable before the first `do_update_out`, refused with +`StateError` once data has started) and a finalizer that also produces the tag +(`do_encrypt_final`/`do_decrypt_final`, flushing up to `FINAL_LEN` held-back bytes alongside it). +`FINAL_LEN` is `0` for a cipher like Ascon-AEAD128 that never buffers; a block-oriented AEAD or one +whose wire format inlines the tag would need it non-zero. The one-shots (`encrypt_out[_rng]`, +`decrypt_out`, and the `std` `Vec` forms) are provided over the streaming methods, so an implementor +writes seven. `bouncycastle-ascon`'s `AsconAead128Encryptor` / `AsconAead128Decryptor` are the first +implementors. + +Mutation-tested with `cargo mutants -p bouncycastle-core -F 'AEADCipher(Encryptor|Decryptor)' +--test-package bouncycastle-ascon` (`core` has no implementor of its own to test against): 68 +mutants, 49 caught, 10 unviable, 9 missed -- all nine equivalent given `FINAL_LEN = 0`, the only +value Ascon-AEAD128 exercises. Six are `written + final_len` vs `written - final_len` in +`encrypt_out`/`encrypt_out_rng`/`decrypt_out`'s final-buffer splice, indistinguishable because +`final_len` is always `0` there; the other three are the one-shots' own buffer-length guard +(`plaintext.len() < needed` / `ciphertext.len() < needed`) against `>`, indistinguishable because +`needed` at `FINAL_LEN = 0` is exactly the bound Ascon's own `do_update_out` already enforces one +call deeper, so the outer guard's direction is never the only thing standing between a short buffer +and an error. A future `FINAL_LEN > 0` implementor (a block-oriented AEAD) would give both classes +of mutant something to bite on. + +Where the tag goes is deliberately not fixed by the pair (contrast `AEADCipher`, whose one-shots +pick a layout): `core::tagged_aead::TaggedEncryptor` / `TaggedDecryptor` adapt any +`FINAL_LEN = 0` implementor to `SimpleCipherEncryptor` / `SimpleCipherDecryptor`, producing and +consuming the inline `ciphertext || tag` layout most wire formats and files use, with the AAD phase +still reachable through an inherent `do_update_aad` the `SimpleCipher*` traits have no slot for. +`TaggedDecryptor` holds back exactly the last `TAG_LEN` bytes it has seen at any point, releasing +everything older through the wrapped decryptor as soon as it is known not to be the tag -- the same +technique `bc-rust`'s `ascon-aead128 --decrypt` used by hand before this adapter existed, now +provided once. (A fully general adapter over a implementor whose own `FINAL_LEN` is non-zero needs +this adapter's `FINAL_LEN` to be `INNER_FINAL_LEN + TAG_LEN`, a value derived from two other const +generics that stable const generics cannot express as a trait argument; left to a future adapter.) + +New crate `bouncycastle-ascon` (`bouncycastle::ascon`): Ascon-AEAD128 / Ascon-Hash256 / Ascon-XOF128 +/ Ascon-CXOF128 (NIST SP 800-232), the lightweight cryptography suite selected from the NIST +Lightweight Cryptography competition. + +* `AsconAead128` is the streaming primitive (rate 128 bits, capacity 192 bits, `Ascon-p[12]` at + init/finalization and `Ascon-p[8]` on AAD/data blocks), with a caller-supplied nonce for KAT and + protocol use. Every plaintext/ciphertext byte is transformed and emitted the moment it is seen -- + no held-back buffering across calls -- because within a rate block each byte is independent of + the others in it; this is what lets its finalizers have nothing left to flush. + `AsconAead128Encryptor` / `AsconAead128Decryptor` are thin newtypes over it implementing the new + `AEADCipherEncryptor` / `AEADCipherDecryptor` pair with an internally-generated nonce; `AsconAead128` + itself keeps implementing the one-shot-only `AEADCipher` (both directions on one type, chosen by a + runtime flag), which the newtype split cannot replace since that trait needs both directions + available on a single implementor. +* `AsconHash256` (`Hash`) and `AsconXof128` (`XOF`) are sponge constructions over the same + permutation; `AsconCXof128` (`XOF`) adds the customization string of SP 800-232 Algorithm 7 (up to + 256 bytes). All four are byte-oriented: `do_final_partial_bits`/the equivalent XOF methods always + return an error rather than accept a partial final byte, unlike SHA-2/SHA-3. Registered in + `HashFactory` (`"Ascon-Hash256"`) and `XOFFactory` (`"Ascon-XOF128"`), with `ascon-hash256`, + `ascon-xof128`, `ascon-cxof128` and `ascon-aead128` CLI subcommands; the last streams both + directions in 1 KiB chunks, decrypting through `TaggedDecryptor` rather than a hand-rolled tail + buffer. +* **Decryption releases plaintext before the tag is checked**, streaming or through the CLI: bytes + are necessarily written to the caller's buffer (or stdout) before the last `TAG_LEN` bytes -- the + tag -- can be read and compared. A non-zero exit from the CLI, or an `Err` from the streaming + finalizer, means the input was tampered with and any output already produced must be discarded; + do not treat it as authentic before that point. The one-shot APIs (`AsconAead128::decrypt`, both + `AEADCipher` and `AEADCipherDecryptor` views) do not have this caveat: they own the whole message + and zeroize the output buffer before returning an error. +* Verified against 4228 NIST LWC KAT vectors from `bc-test-data` (1089 each for AEAD128 and + CXOF128, 1025 each for Hash256 and XOF128), plus embedded always-on vectors for when that + repository is not checked out. Mutation-tested with `cargo mutants -p bouncycastle-ascon`: 665 + mutants, 558 caught, 103 unviable, 4 missed -- all four the same equivalent survivors as the + crate's introduction (PR #21): the `Sponge::absorb`/`squeeze` boundary pair and the disjoint-bit + `set_state_byte` OR-vs-XOR pair, neither touched by the `AEADCipherEncryptor`/`AEADCipherDecryptor` + work. ## Minor features / bug fixes diff --git a/cli/src/ascon_cmd.rs b/cli/src/ascon_cmd.rs new file mode 100644 index 00000000..49ca5297 --- /dev/null +++ b/cli/src/ascon_cmd.rs @@ -0,0 +1,194 @@ +use std::io::{self, Read}; +use std::process::exit; + +use bouncycastle::ascon::ascon_aead128::{AsconAead128, AsconAead128Decryptor}; +use bouncycastle::ascon::ascon_cxof128::AsconCXof128; +use bouncycastle::ascon::ascon_hash256::AsconHash256; +use bouncycastle::ascon::ascon_xof128::AsconXof128; +use bouncycastle::core::errors::SymmetricCipherError; +use bouncycastle::core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle::core::tagged_aead::TaggedDecryptor; +use bouncycastle::core::traits::{SecurityStrength, SimpleCipherDecryptor}; +use bouncycastle::hex; + +use crate::helpers; + +/// Load a hex string or a binary/hex file into bytes; exits with an error if neither is supplied. +fn load_bytes(value: &Option, value_file: &Option, label: &str) -> Vec { + if let Some(file) = value_file { + helpers::read_from_file(file) + } else if let Some(v) = value { + hex::decode(v).unwrap_or_else(|_| { + eprintln!("Error: {label} is not valid hex."); + exit(-1) + }) + } else { + eprintln!("Error: {label} must be supplied."); + exit(-1) + } +} + +fn require_16(bytes: Vec, label: &str) -> [u8; 16] { + bytes.try_into().unwrap_or_else(|_: Vec| { + eprintln!("Error: {label} must be exactly 16 bytes."); + exit(-1) + }) +} + +/// Build a `KeyMaterial<16>` for the AEAD key, warning (and forcing usable metadata) only if the +/// key turns out to be low-entropy (e.g. all-zero), the same way `helpers::parse_seed` does. +fn load_key_material(key_bytes: &[u8; 16]) -> KeyMaterial<16> { + let mut key = + KeyMaterial::<16>::from_bytes_as_type(key_bytes, KeyType::SymmetricCipherKey).unwrap(); + if key.key_type() == KeyType::Zeroized || key.security_strength() < SecurityStrength::_128bit { + eprintln!( + "Warning: low entropy key provided. We'll still process it, but it may be insecure." + ); + do_hazardous_operations(&mut key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::_128bit) + }) + .unwrap(); + } + key +} + +/// Ascon-Hash256 of stdin. Streaming update; 256-bit digest. +pub(crate) fn hash256_cmd(output_hex: bool) { + helpers::stream_hash(AsconHash256::new(), output_hex); +} + +/// Ascon-XOF128 of stdin, producing `output_len` bytes. Streaming absorb. +pub(crate) fn xof128_cmd(output_len: usize, output_hex: bool) { + helpers::stream_xof(AsconXof128::new(), output_len, output_hex); +} + +/// Ascon-CXOF128 of stdin with a hex customization string, producing `output_len` bytes. +pub(crate) fn cxof128_cmd(customization: &Option, output_len: usize, output_hex: bool) { + let z = match customization { + Some(v) => hex::decode(v).unwrap_or_else(|_| { + eprintln!("Error: customization is not valid hex."); + exit(-1) + }), + None => Vec::new(), + }; + let x = AsconCXof128::with_customization(&z).unwrap_or_else(|_| { + eprintln!("Error: customization string exceeds 256 bytes."); + exit(-1) + }); + helpers::stream_xof(x, output_len, output_hex); +} + +/// Ascon-AEAD128 of stdin. Encrypts (stdin = plaintext, output = ciphertext||tag) or, with +/// `decrypt`, decrypts (stdin = ciphertext||tag, output = plaintext). Decryption exits with a +/// non-zero status if the authentication tag does not verify. +/// +/// Both directions stream stdin in fixed-size chunks (no full-buffer slurp). Encryption emits +/// ciphertext eagerly, before the tag is known; note that in the decryption direction, plaintext +/// is likewise emitted before the tag has been checked, so it should not be treated as +/// authentic until this command exits with status 0 (see the crate's "Security Considerations"). +pub(crate) fn aead128_cmd( + key: &Option, + key_file: &Option, + nonce: &Option, + nonce_file: &Option, + ad: &Option, + decrypt: bool, + output_hex: bool, +) { + let key = load_key_material(&require_16(load_bytes(key, key_file, "key"), "key")); + let nonce = require_16(load_bytes(nonce, nonce_file, "nonce"), "nonce"); + let ad_bytes = match ad { + Some(v) => hex::decode(v).unwrap_or_else(|_| { + eprintln!("Error: associated data is not valid hex."); + exit(-1) + }), + None => Vec::new(), + }; + let ad_opt = if ad_bytes.is_empty() { None } else { Some(ad_bytes.as_slice()) }; + + if decrypt { + aead128_decrypt_stream(&key, &nonce, ad_opt, output_hex); + } else { + aead128_encrypt_stream(&key, &nonce, ad_opt, output_hex); + } +} + +fn aead128_encrypt_stream( + key: &KeyMaterial<16>, + nonce: &[u8; 16], + ad_opt: Option<&[u8]>, + output_hex: bool, +) { + let mut cipher = AsconAead128::new(key, nonce, ad_opt, true).unwrap(); + let mut buf = [0u8; 1024]; + loop { + let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + if n == 0 { + break; + } + cipher.do_encrypt_update(&mut buf[..n]); + helpers::write_bytes_or_hex(&buf[..n], output_hex); + } + let tag = cipher.do_encrypt_final(); + helpers::write_bytes_or_hex(&tag, output_hex); + if output_hex { + println!(); + } +} + +/// Decrypts a stream whose final 16 bytes are the tag, which is only known once EOF is reached. +/// The tag-candidate hold-back this needs is [`TaggedDecryptor`]'s job, not this function's: it +/// adapts [`AsconAead128Decryptor`] to the `ciphertext || tag` layout, releasing everything but +/// the last 16 bytes it has seen as soon as it is known not to be the tag. +fn aead128_decrypt_stream( + key: &KeyMaterial<16>, + nonce: &[u8; 16], + ad_opt: Option<&[u8]>, + output_hex: bool, +) { + const CHUNK: usize = 1024; + + let mut cipher = as SimpleCipherDecryptor< + 16, + 16, + 16, + >>::do_decrypt_init(key, nonce) + .unwrap(); + if let Some(ad) = ad_opt { + cipher.do_update_aad::<16, 16>(ad).unwrap(); + } + + let mut buf = [0u8; CHUNK]; + loop { + let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + if n == 0 { + break; + } + let expect = cipher.update_out_len(n); + let mut out = vec![0u8; expect]; + // infallible: `out` is sized exactly to `update_out_len`, the only length + // `IncorrectOutputBufferLength` could complain about. + let written = cipher.do_update_out(&buf[..n], &mut out).unwrap(); + helpers::write_bytes_or_hex(&out[..written], output_hex); + } + + match cipher.do_final() { + Ok((last, last_len)) => { + helpers::write_bytes_or_hex(&last[..last_len], output_hex); + if output_hex { + println!(); + } + } + Err(SymmetricCipherError::DecryptionFailed) => { + eprintln!("Error: ciphertext is shorter than the 16-byte tag."); + exit(-1); + } + Err(_) => { + eprintln!("Error: Ascon-AEAD128 authentication failed."); + exit(-1); + } + } +} diff --git a/cli/src/helpers.rs b/cli/src/helpers.rs index 207f0ee0..2873e1e6 100644 --- a/cli/src/helpers.rs +++ b/cli/src/helpers.rs @@ -1,7 +1,7 @@ use bouncycastle::core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; -use bouncycastle::core::traits::SecurityStrength; +use bouncycastle::core::traits::{Hash, SecurityStrength, XOF}; use bouncycastle::hex; use std::fs::File; use std::io; @@ -116,3 +116,35 @@ pub(crate) fn parse_seed(bytes: &[u8]) -> Result, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Ascon-AEAD128 authenticated encryption/decryption of the content provided on stdin. + /// Encrypts by default (stdin = plaintext, output = ciphertext||tag); with --decrypt the + /// reverse. Decryption fails with a non-zero exit status if the tag does not verify. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + /// Security note: decryption streams its output, so plaintext bytes are written to stdout + /// before the authentication tag (the last 16 bytes of input) can be checked. Do not treat + /// the output as authentic until this command exits with status 0; a non-zero exit means the + /// input was tampered with and any plaintext already written must be discarded. + AsconAEAD128 { + /// The 128-bit key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 128-bit key in hex or binary. + #[arg(long)] + key_file: Option, + + /// The 128-bit nonce in hex. Must be unique per encryption under a given key. + #[arg(long)] + nonce: Option, + + /// A file containing the 128-bit nonce in hex or binary. + #[arg(long)] + nonce_file: Option, + + /// Associated data in hex (authenticated but not encrypted). + #[arg(long)] + ad: Option, + + /// Decrypt instead of encrypt. + #[arg(short, long)] + decrypt: bool, + + #[arg(short)] + /// Output in hex format. x: bool, }, @@ -1248,6 +1320,17 @@ fn main() { } Some(Subcommands::CSHAKE256 { length, customization, function_name, x }) => { sha3_cmd::cshake_cmd(256, *length, function_name, customization, *x); + Some(Subcommands::AsconHash256 { x }) => { + ascon_cmd::hash256_cmd(*x); + } + Some(Subcommands::AsconXOF128 { length, x }) => { + ascon_cmd::xof128_cmd(*length, *x); + } + Some(Subcommands::AsconCXOF128 { length, customization, x }) => { + ascon_cmd::cxof128_cmd(customization, *length, *x); + } + Some(Subcommands::AsconAEAD128 { key, key_file, nonce, nonce_file, ad, decrypt, x }) => { + ascon_cmd::aead128_cmd(key, key_file, nonce, nonce_file, ad, *decrypt, *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 7c0ae4c6..d7a8d7fc 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -9,42 +9,22 @@ use bouncycastle::sha3::{ }; use std::process::exit; +use crate::helpers::{stream_hash, stream_xof}; + pub(crate) fn sha3_cmd(bit_len: usize, output_hex: bool) { match bit_len { - 224 => do_sha3(SHA3_224::new(), output_hex), - 256 => do_sha3(SHA3_256::new(), output_hex), - 384 => do_sha3(SHA3_384::new(), output_hex), - 512 => do_sha3(SHA3_512::new(), output_hex), + 224 => stream_hash(SHA3_224::new(), output_hex), + 256 => stream_hash(SHA3_256::new(), output_hex), + 384 => stream_hash(SHA3_384::new(), output_hex), + 512 => stream_hash(SHA3_512::new(), output_hex), _ => panic!("Unsupported algorithm: SHA3-{}", bit_len), } } -fn do_sha3(mut sha3: impl Hash, output_hex: bool) { - let mut buf: [u8; 1024] = [0u8; 1024]; - - // read from stdin - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - while bytes_read != 0 { - sha3.do_update(&buf[..bytes_read]); - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - } - - let out = sha3.do_final(); - - if output_hex { - for b in out.iter() { - print!("{b:02x}"); - } - } else { - io::stdout().write(&out).unwrap(); - } - println!(); -} - pub(crate) fn shake_cmd(bit_len: usize, output_len: usize, output_hex: bool) { match bit_len { - 128 => do_shake(SHAKE128::new(), output_len, output_hex), - 256 => do_shake(SHAKE256::new(), output_len, output_hex), + 128 => stream_xof(SHAKE128::new(), output_len, output_hex), + 256 => stream_xof(SHAKE256::new(), output_len, output_hex), _ => panic!("Unsupported algorithm: SHAKE-{}", bit_len), } } diff --git a/cli/tests/ascon_cli_tests.rs b/cli/tests/ascon_cli_tests.rs new file mode 100644 index 00000000..3cf3c6de --- /dev/null +++ b/cli/tests/ascon_cli_tests.rs @@ -0,0 +1,308 @@ +//! Tests for the `ascon-hash256` / `ascon-xof128` / `ascon-cxof128` / `ascon-aead128` +//! subcommands. +//! +//! These drive the built `bc-rust` binary as a subprocess, because the behaviour worth testing is +//! the command-line contract itself -- KAT-level correctness through the pipe, the `ciphertext || +//! tag` layout, `--key-file`/`--nonce-file` loading, AAD, and exit codes -- none of which is +//! reachable from the library API, which `crypto/ascon/tests/*.rs` already covers directly. +//! +//! The KAT values below are taken from the embedded vectors already pinned in +//! `crypto/ascon/tests/{hash256,xof128,cxof128,aead128}_tests.rs` (themselves NIST LWC vectors), +//! not retyped from memory. +//! +//! `CARGO_BIN_EXE_bc-rust` is set by cargo for integration tests and points at the binary for the +//! current profile, so there is nothing to build or locate by hand. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; +use std::thread; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// The NIST LWC AEAD KAT convention uses key == nonce for the embedded vectors (see +/// `crypto/ascon/tests/aead128_tests.rs`'s `aead128_embedded_kat`). +const KEY_HEX: &str = "000102030405060708090a0b0c0d0e0f"; + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +/// +/// See `aes_ctr_cli_tests.rs::run` for why stdin is written from a separate thread (a pipe with a +/// bounded buffer deadlocks otherwise) and why a `BrokenPipe` write error is swallowed (an +/// error-path command may exit before draining stdin). +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + let mut stdin = child.stdin.take().expect("stdin piped"); + let payload = stdin_bytes.to_vec(); + let writer = thread::spawn(move || { + match stdin.write_all(&payload) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + // `stdin` drops here, closing the pipe so the child sees EOF and can exit. + }); + + let output = child.wait_with_output().expect("failed to wait for bc-rust"); + writer.join().expect("the stdin writer thread panicked"); + output +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG or on `/dev/urandom`. +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} + +fn hex_stdout(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run_ok(args, stdin_bytes); + String::from_utf8(out).expect("hex output is text").trim_end().to_string() +} + +// ---- ascon-hash256 ------------------------------------------------------------------------ + +/// LWC_HASH_KAT_256.txt Count 1: the digest of the empty message. +#[test] +fn ascon_hash256_matches_the_embedded_kat_for_the_empty_message() { + let out = hex_stdout(&["ascon-hash256", "-x"], &[]); + assert_eq!(out, "0b3be5850f2f6b98caf29f8fdea89b64a1fa70aa249b8f839bd53baa304d92b2"); +} + +/// A non-empty message, matching LWC_HASH_KAT_256.txt Count 9. +#[test] +fn ascon_hash256_matches_the_embedded_kat_for_a_multi_byte_message() { + let out = hex_stdout(&["ascon-hash256", "-x"], &unhex("0001020304050607")); + assert_eq!(out, "b88e497ae8e6fb641b87ef622eb8f2fca0ed95383f7ffebe167acf1099ba764f"); +} + +// ---- ascon-xof128 -------------------------------------------------------------------------- + +/// LWC_XOF_KAT_128_512.txt Count 1: 64 bytes squeezed after absorbing the empty message. +#[test] +fn ascon_xof128_matches_the_embedded_kat_for_the_empty_message() { + let out = hex_stdout(&["ascon-xof128", "64", "-x"], &[]); + assert_eq!( + out, + "473d5e6164f58b39dfd84aacdb8ae42ec2d91fed33388ee0d960d9b3993295c\ + 6ad77855a5d3b13fe6ad9e6098988373af7d0956d05a8f1665d2c67d1a3ad10ff" + ); +} + +/// The output length is the caller's choice, and shorter output is a prefix of longer output +/// (every XOF's defining property) -- pinned here through the CLI specifically, since the CLI is +/// what turns the length into a positional argument. +#[test] +fn ascon_xof128_output_length_is_a_prefix_of_a_longer_squeeze() { + let full = hex_stdout(&["ascon-xof128", "64", "-x"], &[]); + let short = hex_stdout(&["ascon-xof128", "16", "-x"], &[]); + assert_eq!(short.len(), 32, "16 bytes is 32 hex characters"); + assert!(full.starts_with(&short)); +} + +// ---- ascon-cxof128 ------------------------------------------------------------------------- + +/// LWC_CXOF_KAT_128_512.txt Count 4: message `00`, customization `10`. +#[test] +fn ascon_cxof128_matches_the_embedded_kat() { + let out = hex_stdout(&["ascon-cxof128", "64", "--customization", "10", "-x"], &unhex("00")); + assert_eq!( + out, + "63fa8ba86382f2d544580f51322d080424b42c556eb74503cd73cf052bb993\ + bd6f5210984c71c9c445f43ccc5b158226e509bd339cd634414377f79411aa8d5c" + ); +} + +/// No `--customization` at all must give the same output as an empty one: `AsconCXof128::new()` +/// versus `with_customization(&[])`, both reachable only through the library elsewhere -- here we +/// pin that the CLI's `Option` plumbing treats "absent" and "empty" identically. +#[test] +fn ascon_cxof128_with_no_customization_matches_an_empty_one() { + let without = hex_stdout(&["ascon-cxof128", "64", "-x"], &[]); + let with_empty = hex_stdout(&["ascon-cxof128", "64", "--customization", "", "-x"], &[]); + assert_eq!(without, with_empty); + // LWC_CXOF_KAT_128_512.txt Count 1: message and customization both empty. + assert_eq!( + without, + "4f50159ef70bb3dad8807e034eaebd44c4fa2cbbc8cf1f05511ab66cdcc5299\ + 05ca12083fc186ad899b270b1473dc5f7ec88d1052082dcdfe69fb75d269e7b74" + ); +} + +// ---- ascon-aead128 ------------------------------------------------------------------------- + +/// LWC_AEAD_KAT_128_128.txt Count 1: the tag over an empty message with no AAD (key == nonce). +#[test] +fn ascon_aead128_matches_the_embedded_kat_for_an_empty_message() { + let out = hex_stdout(&["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX, "-x"], &[]); + assert_eq!(out, "4427d64b8e1e1451fc445960f0839bb0"); +} + +/// Encrypt then `--decrypt` round-trips a multi-KB payload, byte for byte, and the ciphertext is +/// exactly the plaintext plus the 16-byte tag. +#[test] +fn ascon_aead128_encrypt_then_decrypt_round_trips() { + let plaintext = pseudo_random(4096, 0xC0FFEE); + let ciphertext = run_ok(&["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX], &plaintext); + assert_eq!(ciphertext.len(), plaintext.len() + 16, "ciphertext is plaintext plus the tag"); + + let recovered = + run_ok(&["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX, "--decrypt"], &ciphertext); + assert_eq!(recovered, plaintext); +} + +/// Associated data is authenticated on both sides of a round trip. +#[test] +fn ascon_aead128_associated_data_round_trips() { + let plaintext = pseudo_random(256, 7); + let ciphertext = run_ok( + &["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX, "--ad", "deadbeef"], + &plaintext, + ); + let recovered = run_ok( + &["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX, "--ad", "deadbeef", "--decrypt"], + &ciphertext, + ); + assert_eq!(recovered, plaintext); +} + +/// Decrypting with the wrong associated data must fail the tag check, the same as tampering with +/// the ciphertext itself. +#[test] +fn ascon_aead128_wrong_associated_data_is_rejected() { + let plaintext = pseudo_random(64, 11); + let ciphertext = run_ok( + &["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX, "--ad", "deadbeef"], + &plaintext, + ); + let stderr = run_err( + &["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX, "--ad", "cafebabe", "--decrypt"], + &ciphertext, + ); + assert!(stderr.contains("authentication failed"), "stderr: {stderr}"); +} + +/// A single flipped ciphertext byte must fail the tag check on decrypt, with a non-zero exit and +/// an explanatory stderr message -- the security-relevant contract the streaming decrypt path +/// (`ascon_cmd.rs::aead128_decrypt_stream`) exists to uphold. +#[test] +fn ascon_aead128_a_flipped_ciphertext_byte_is_rejected() { + let plaintext = pseudo_random(64, 1); + let mut ciphertext = + run_ok(&["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX], &plaintext); + ciphertext[0] ^= 0x01; + + let stderr = + run_err(&["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX, "--decrypt"], &ciphertext); + assert!(stderr.contains("authentication failed"), "stderr: {stderr}"); +} + +/// A flipped tag byte (the last byte of the stream) must be rejected the same way. +#[test] +fn ascon_aead128_a_flipped_tag_byte_is_rejected() { + let plaintext = pseudo_random(64, 2); + let mut ciphertext = + run_ok(&["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX], &plaintext); + let last = ciphertext.len() - 1; + ciphertext[last] ^= 0x01; + + let stderr = + run_err(&["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX, "--decrypt"], &ciphertext); + assert!(stderr.contains("authentication failed"), "stderr: {stderr}"); +} + +/// Decrypt input shorter than the 16-byte tag is rejected before any tag check is attempted, +/// including the empty-input case. +#[test] +fn ascon_aead128_decrypt_input_shorter_than_the_tag_is_rejected() { + for len in [0usize, 1, 15] { + let stderr = run_err( + &["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX, "--decrypt"], + &pseudo_random(len, len as u32 + 1), + ); + assert!( + stderr.contains("shorter than the 16-byte tag"), + "len {len}: stderr should explain the missing tag: {stderr}" + ); + } +} + +/// `--key-file`/`--nonce-file` accept binary content, not just hex, the same as the AES commands' +/// `--key-file` (see `key_file_accepts_hex_and_binary` in `aes_ctr_cli_tests.rs`). +#[test] +fn ascon_aead128_key_file_and_nonce_file_accept_binary_content() { + let dir = std::env::temp_dir().join(format!("ascon_cli_test_{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + let key_path = dir.join("key.bin"); + let nonce_path = dir.join("nonce.bin"); + std::fs::write(&key_path, unhex(KEY_HEX)).expect("write key file"); + std::fs::write(&nonce_path, unhex(KEY_HEX)).expect("write nonce file"); + + let out = hex_stdout( + &[ + "ascon-aead128", + "--key-file", + key_path.to_str().unwrap(), + "--nonce-file", + nonce_path.to_str().unwrap(), + "-x", + ], + &[], + ); + assert_eq!(out, "4427d64b8e1e1451fc445960f0839bb0"); + + let _ = std::fs::remove_dir_all(&dir); +} + +/// The subcommands are listed in top-level help. +#[test] +fn the_subcommands_are_listed_in_help() { + let out = run_ok(&["--help"], &[]); + let text = String::from_utf8_lossy(&out); + for name in ["ascon-hash256", "ascon-xof128", "ascon-cxof128", "ascon-aead128"] { + assert!(text.contains(name), "--help should list {name}"); + } +} diff --git a/crypto/ascon/Cargo.toml b/crypto/ascon/Cargo.toml new file mode 100644 index 00000000..25a58829 --- /dev/null +++ b/crypto/ascon/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "bouncycastle-ascon" +version.workspace = true +edition.workspace = true + +[features] +# `std` gates the ergonomic, allocating (`Vec`-returning) one-shot cipher APIs, mirroring the +# `std` feature of `bouncycastle-core`. On by default; a future `--no-default-features` build is +# what will let the crate move toward `#![no_std]`. +default = ["std"] +std = ["bouncycastle-core/std"] + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-rng.workspace = true +bouncycastle-utils.workspace = true + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +criterion.workspace = true + +[[bench]] +name = "ascon_benches" +harness = false diff --git a/crypto/ascon/benches/ascon_benches.rs b/crypto/ascon/benches/ascon_benches.rs new file mode 100644 index 00000000..eebe3f17 --- /dev/null +++ b/crypto/ascon/benches/ascon_benches.rs @@ -0,0 +1,93 @@ +use bouncycastle_rng as rng; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ascon::ascon_aead128::AsconAead128; +use bouncycastle_ascon::ascon_cxof128::AsconCXof128; +use bouncycastle_ascon::ascon_hash256::AsconHash256; +use bouncycastle_ascon::ascon_xof128::AsconXof128; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{Hash, RNG, XOF}; + +const DATA_LEN: usize = 16 * 1024; + +fn random_data(len: usize) -> Vec { + let mut data = vec![0u8; len]; + rng::DefaultRNG::default().next_bytes_out(&mut data).unwrap(); + data +} + +fn bench_aead128_encrypt(c: &mut Criterion) { + let key = + KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); + let nonce = [0x24u8; 16]; + let data = random_data(DATA_LEN); + let mut out = vec![0u8; DATA_LEN + 16]; + + let mut group = c.benchmark_group("ascon::AsconAead128"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + group.bench_function(format!("{DATA_LEN} bytes -- ::encrypt()"), |b| { + b.iter(|| { + AsconAead128::encrypt(&key, &nonce, None, black_box(&data), &mut out).unwrap(); + black_box(&out); + }) + }); + group.finish(); +} + +fn bench_hash256(c: &mut Criterion) { + let data = random_data(DATA_LEN); + let mut digest = [0u8; 32]; + + let mut group = c.benchmark_group("ascon::AsconHash256"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + group.bench_function(format!("{DATA_LEN} bytes -- ::hash_out()"), |b| { + b.iter(|| { + AsconHash256::new().hash_out(black_box(&data), &mut digest); + black_box(&digest); + }) + }); + group.finish(); +} + +fn bench_xof128(c: &mut Criterion) { + let data = random_data(DATA_LEN); + let mut out = [0u8; 64]; + + let mut group = c.benchmark_group("ascon::AsconXof128"); + group.throughput(Throughput::Bytes((DATA_LEN + out.len()) as u64)); + group.bench_function( + format!("input: {DATA_LEN} bytes, output: 64 bytes -- ::hash_xof_out()"), + |b| { + b.iter(|| { + AsconXof128::new().hash_xof_out(black_box(&data), &mut out); + black_box(&out); + }) + }, + ); + group.finish(); +} + +fn bench_cxof128(c: &mut Criterion) { + let data = random_data(DATA_LEN); + let customization = b"bench-customization"; + let mut out = [0u8; 64]; + + let mut group = c.benchmark_group("ascon::AsconCXof128"); + group.throughput(Throughput::Bytes((DATA_LEN + out.len()) as u64)); + group.bench_function( + format!("input: {DATA_LEN} bytes, output: 64 bytes -- ::hash_xof_out()"), + |b| { + b.iter(|| { + AsconCXof128::with_customization(customization) + .unwrap() + .hash_xof_out(black_box(&data), &mut out); + black_box(&out); + }) + }, + ); + group.finish(); +} + +criterion_group!(benches, bench_aead128_encrypt, bench_hash256, bench_xof128, bench_cxof128); +criterion_main!(benches); diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs new file mode 100644 index 00000000..ee34d2cd --- /dev/null +++ b/crypto/ascon/src/ascon_aead128.rs @@ -0,0 +1,865 @@ +//! Ascon-AEAD128 authenticated encryption, as specified in NIST SP 800-232 §4. +//! +//! Rate = 128 bits, capacity = 192 bits, 128-bit key/nonce/tag. Initialization and finalization use +//! `Ascon-p[12]`; associated-data and plaintext/ciphertext blocks use `Ascon-p[8]`. +//! +//! Every byte of plaintext/ciphertext is transformed and emitted as soon as it is seen (no +//! held-back buffering across `do_encrypt_update`/`do_decrypt_update` calls); this is what lets the +//! finalizers be plain `self -> tag` / `self -> Result<(), _>` calls with nothing left to flush. +//! Ascon-AEAD128 permits this because within a 128-bit rate block each plaintext/ciphertext byte +//! is transformed independently of the others in that block; the permutation only runs once a +//! full 16-byte block has been absorbed, or at finalization. +//! +//! [`AsconAead128Encryptor`] / [`AsconAead128Decryptor`] adapt this type's direction-agnostic +//! streaming API (a single [`AsconAead128`] value serves either direction, chosen by a runtime +//! flag to [`AsconAead128::new`]) to [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], whose +//! direction is fixed by the type: each newtype wraps an [`AsconAead128`] already constructed for +//! its own direction and only ever calls that direction's inherent methods, so the wrong-direction +//! panics inside [`AsconAead128::do_encrypt_update`] and friends are unreachable through them. See +//! their docs for why a thin newtype pair rather than encoding the direction into `AsconAead128` +//! itself: that would need a second, incompatible implementation of the single-type [`AEADCipher`] +//! this module also provides, which needs both directions available on the one type. + +use core::fmt::{self, Debug, Display, Formatter}; + +use bouncycastle_core::errors::{KeyMaterialError, SuspendableError, SymmetricCipherError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_core::traits::{ + AEADCipher, AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, SecurityStrength, + SuspendableKeyed, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_utils::ct::ct_eq_bytes; +use bouncycastle_utils::secret::Secret; + +use crate::permutation::{AsconState, load_u64_le, p8, p12, store_u64_le}; + +/// Length in bytes of the Ascon-AEAD128 key. +pub const KEY_LEN: usize = 16; +/// Length in bytes of the Ascon-AEAD128 nonce. +pub const NONCE_LEN: usize = 16; +/// Length in bytes of the Ascon-AEAD128 authentication tag. +pub const TAG_LEN: usize = 16; +const RATE: usize = 16; + +/// Ascon-AEAD128 initial value (SP 800-232 Table 14). +const ASCON_IV: u64 = 0x00001000808C0001; + +/// State machine for enforcing the call order and remembering the direction (encrypt/decrypt). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum StateMachine { + EncInit, + EncAad, + EncData, + DecInit, + DecAad, + DecData, +} + +impl StateMachine { + // Stable u8 encoding used when suspending/resuming the AEAD state machine. + fn to_u8(self) -> u8 { + match self { + StateMachine::EncInit => 0, + StateMachine::EncAad => 1, + StateMachine::EncData => 2, + StateMachine::DecInit => 4, + StateMachine::DecAad => 5, + StateMachine::DecData => 6, + } + } + + fn from_u8(v: u8) -> Option { + Some(match v { + 0 => StateMachine::EncInit, + 1 => StateMachine::EncAad, + 2 => StateMachine::EncData, + 4 => StateMachine::DecInit, + 5 => StateMachine::DecAad, + 6 => StateMachine::DecData, + _ => return None, + }) + } + + fn is_encrypt(self) -> bool { + matches!(self, StateMachine::EncInit | StateMachine::EncAad | StateMachine::EncData) + } + + fn is_init(self) -> bool { + matches!(self, StateMachine::EncInit | StateMachine::DecInit) + } +} + +/// An implementation of the Ascon-AEAD128 algorithm (NIST SP 800-232). +/// +/// A single instance performs one operation (encryption or decryption) under one (key, nonce) pair. +/// See [`AsconAead128::new`] for the streaming workflow and [`AsconAead128::encrypt`] / +/// [`AsconAead128::decrypt`] for the one-shot APIs. +#[derive(Clone)] +pub struct AsconAead128 { + // 128-bit secret key (two 64-bit words). It is re-added to the state at finalization, so it must + // be retained; wrapped in `Secret` for volatile-write zeroization on drop. + key: Secret<[u64; 2]>, + // 320-bit internal state (five 64-bit words). Carries keystream/plaintext-derived material, so + // it is likewise wrapped in `Secret`. + state: Secret, + // Byte position (0..RATE) within the current rate block. + pos: usize, + // State machine for enforcing the call order and remembering the direction. + state_machine: StateMachine, +} + +impl AsconAead128 { + /// Validate a [`KeyMaterial`] for use with Ascon-AEAD128 and return its key words. + /// The key must be tagged as a [`KeyType::SymmetricCipherKey`] and carry at least the + /// algorithm's 128-bit security strength (SP 800-232 R1/R2). + fn checked_key(key: &KeyMaterial) -> Result<[u64; 2], SymmetricCipherError> { + if key.key_type() != KeyType::SymmetricCipherKey { + return Err(KeyMaterialError::InvalidKeyType( + "Ascon-AEAD128 requires a SymmetricCipherKey", + ) + .into()); + } + if key.security_strength() < SecurityStrength::_128bit { + return Err(KeyMaterialError::SecurityStrength( + "Ascon-AEAD128 requires a key with at least 128-bit security strength", + ) + .into()); + } + let bytes = key.ref_to_bytes(); + if bytes.len() != KEY_LEN { + return Err(KeyMaterialError::InvalidLength.into()); + } + Ok([load_u64_le(bytes, 0), load_u64_le(bytes, 8)]) + } + + /// Draw a fresh, unique 128-bit nonce from the library's default OS-seeded DRBG. + /// + /// The one-shot APIs of main's cipher framework generate the init data / nonce internally, so + /// Ascon's per-encryption nonce-uniqueness requirement (SP 800-232 R3) is satisfied by sourcing + /// each nonce from a CSPRNG. Callers who need deterministic, caller-supplied nonces should use + /// the inherent streaming API ([`AsconAead128::new`]). + fn fresh_nonce() -> Result<[u8; NONCE_LEN], SymmetricCipherError> { + let mut rng = HashDRBG_SHA512::new_from_os(); + let mut nonce = [0u8; NONCE_LEN]; + rng.next_bytes_out(&mut nonce)?; + Ok(nonce) + } + + /// Create a new streaming instance. + /// * `key` is validated as a [`KeyType::SymmetricCipherKey`] with at least 128-bit strength. + /// * `nonce` is the 128-bit nonce. It **must** be unique per encryption under a given key. + /// * `ad` is optional associated data (authenticated, not encrypted); processed immediately. + /// * `for_encryption` is true for encryption, false for decryption. + pub fn new( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ad: Option<&[u8]>, + for_encryption: bool, + ) -> Result { + let key_words = Self::checked_key(key)?; + let mut key_secret: Secret<[u64; 2]> = Secret::new(); + *key_secret = key_words; + + let mut state: Secret = Secret::new(); + // Initialization (SP 800-232 §4.1.1 step 1 / Eq. 15-17): S = IV||K||N, then Ascon-p[12], + // then XOR K into the last 128 bits. + state[0] = ASCON_IV; + state[1] = key_words[0]; + state[2] = key_words[1]; + state[3] = load_u64_le(nonce, 0); + state[4] = load_u64_le(nonce, 8); + p12(&mut state); + state[3] ^= key_words[0]; + state[4] ^= key_words[1]; + + let mut aead = AsconAead128 { + key: key_secret, + state, + pos: 0, + state_machine: if for_encryption { + StateMachine::EncInit + } else { + StateMachine::DecInit + }, + }; + if let Some(ad_bytes) = ad { + // infallible: a freshly constructed instance has processed no data yet, so + // `check_aad` cannot return `StateError`. + aead.do_update_aad(ad_bytes).unwrap(); + } + Ok(aead) + } + + /// One-shot authenticated encryption with a caller-supplied nonce (SP 800-232 Algorithm 3). + /// Writes ciphertext followed by the 128-bit tag into `out`, which must be at least + /// `plaintext.len() + 16` bytes. Returns the number of bytes written. + pub fn encrypt( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ad: Option<&[u8]>, + plaintext: &[u8], + out: &mut [u8], + ) -> Result { + let needed = plaintext.len() + TAG_LEN; + if out.len() < needed { + return Err(SymmetricCipherError::IncorrectOutputBufferLength( + "Ascon-AEAD128 output buffer too small (need plaintext length + 16)", + needed, + )); + } + let mut cipher = Self::new(key, nonce, ad, true)?; + out[..plaintext.len()].copy_from_slice(plaintext); + cipher.do_encrypt_update(&mut out[..plaintext.len()]); + let tag = cipher.do_encrypt_final(); + out[plaintext.len()..needed].copy_from_slice(&tag); + Ok(needed) + } + + /// One-shot authenticated decryption with a caller-supplied nonce (SP 800-232 Algorithm 4). + /// `ciphertext` is the ciphertext followed by the 128-bit tag. Writes the recovered plaintext + /// into `out`, which must be at least `ciphertext.len() - 16` bytes. Returns the number of + /// bytes written, or [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify -- + /// in which case `out` is zeroized before returning. + pub fn decrypt( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ad: Option<&[u8]>, + ciphertext: &[u8], + out: &mut [u8], + ) -> Result { + if ciphertext.len() < TAG_LEN { + return Err(SymmetricCipherError::GenericError( + "Ascon-AEAD128 ciphertext shorter than tag", + )); + } + let pt_len = ciphertext.len() - TAG_LEN; + if out.len() < pt_len { + return Err(SymmetricCipherError::IncorrectOutputBufferLength( + "Ascon-AEAD128 output buffer too small", + pt_len, + )); + } + let mut cipher = Self::new(key, nonce, ad, false)?; + out[..pt_len].copy_from_slice(&ciphertext[..pt_len]); + cipher.do_decrypt_update(&mut out[..pt_len]); + // infallible: ciphertext.len() - pt_len == TAG_LEN by construction above. + let tag: &[u8; TAG_LEN] = ciphertext[pt_len..].try_into().unwrap(); + match cipher.do_decrypt_final(tag) { + Ok(()) => Ok(pt_len), + Err(e) => { + out[..pt_len].fill(0); + Err(e) + } + } + } + + /// Read the value of state byte `pos` (0 = LSB of word 0, ..., 15 = MSB of word 1). + fn state_byte(&self, pos: usize) -> u8 { + let word = if pos < 8 { self.state[0] } else { self.state[1] }; + (word >> ((pos % 8) * 8)) as u8 + } + + /// XOR `b` into state byte `pos`. + fn xor_state_byte(&mut self, pos: usize, b: u8) { + let shifted = (b as u64) << ((pos % 8) * 8); + if pos < 8 { self.state[0] ^= shifted } else { self.state[1] ^= shifted } + } + + /// Overwrite state byte `pos` with `b`. + fn set_state_byte(&mut self, pos: usize, b: u8) { + let shift = (pos % 8) * 8; + let mask = !(0xFFu64 << shift); + let shifted = (b as u64) << shift; + if pos < 8 { + self.state[0] = (self.state[0] & mask) | shifted; + } else { + self.state[1] = (self.state[1] & mask) | shifted; + } + } + + /// Advance to the next byte position, running `Ascon-p[8]` and wrapping back to 0 once a full + /// rate block (16 bytes) has been absorbed. + fn advance(&mut self) { + self.pos += 1; + if self.pos == RATE { + p8(&mut self.state); + self.pos = 0; + } + } + + fn absorb_aad_byte(&mut self, b: u8) { + self.xor_state_byte(self.pos, b); + self.advance(); + } + + fn encrypt_byte(&mut self, p: u8) -> u8 { + self.xor_state_byte(self.pos, p); + let c = self.state_byte(self.pos); + self.advance(); + c + } + + fn decrypt_byte(&mut self, c: u8) -> u8 { + let prev = self.state_byte(self.pos); + self.set_state_byte(self.pos, c); + self.advance(); + prev ^ c + } + + fn check_aad(&mut self) -> Result<(), SymmetricCipherError> { + match self.state_machine { + StateMachine::EncInit => self.state_machine = StateMachine::EncAad, + StateMachine::DecInit => self.state_machine = StateMachine::DecAad, + StateMachine::EncAad | StateMachine::DecAad => {} + StateMachine::EncData | StateMachine::DecData => { + return Err(SymmetricCipherError::StateError( + "Ascon-AEAD128: associated data must be processed before plaintext/ciphertext", + )); + } + } + Ok(()) + } + + // Ends the associated-data phase (SP 800-232 §4.1.1/§4.1.2 step 2): pads and absorbs the + // final (possibly empty) AAD block only if any AAD was actually supplied, then applies the + // domain-separation bit unconditionally. + fn finish_aad(&mut self) { + if matches!(self.state_machine, StateMachine::EncAad | StateMachine::DecAad) { + self.xor_state_byte(self.pos, 0x01); + p8(&mut self.state); + self.pos = 0; + } + // Domain separation (Eq. 22/40: S ^= (0^319 || 1)). + self.state[4] ^= 0x8000000000000000; + self.state_machine = match self.state_machine { + StateMachine::EncInit | StateMachine::EncAad => StateMachine::EncData, + StateMachine::DecInit | StateMachine::DecAad => StateMachine::DecData, + StateMachine::EncData | StateMachine::DecData => unreachable!(), + }; + } + + fn check_data(&mut self) { + if !matches!(self.state_machine, StateMachine::EncData | StateMachine::DecData) { + self.finish_aad(); + } + } + + // Finalization (SP 800-232 §4.1.1 step 4 / §4.1.2 step 4, Eq. 30-32 / 49-51): re-add the key, + // permute with Ascon-p[12], and add the key again; the tag is the resulting last 128 bits. + fn finish_data(&mut self) -> [u8; TAG_LEN] { + self.state[2] ^= self.key[0]; + self.state[3] ^= self.key[1]; + p12(&mut self.state); + self.state[3] ^= self.key[0]; + self.state[4] ^= self.key[1]; + + let mut tag = [0u8; TAG_LEN]; + store_u64_le(&mut tag, 0, self.state[3]); + store_u64_le(&mut tag, 8, self.state[4]); + tag + } + + /// Process associated data (AAD) bytes. May be called multiple times, but only before any + /// plaintext/ciphertext is processed; an empty `input` is always a no-op, even after data. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] if `input` is non-empty and plaintext/ciphertext has + /// already been processed. + pub fn do_update_aad(&mut self, input: &[u8]) -> Result<(), SymmetricCipherError> { + if input.is_empty() { + return Ok(()); + } + self.check_aad()?; + + let mut input = input; + while !input.is_empty() { + if self.pos == 0 && input.len() >= RATE { + self.state[0] ^= load_u64_le(input, 0); + self.state[1] ^= load_u64_le(input, 8); + p8(&mut self.state); + input = &input[RATE..]; + } else { + self.absorb_aad_byte(input[0]); + input = &input[1..]; + } + } + Ok(()) + } + + /// Encrypt `data` in place (SP 800-232 §4.1.1 step 3). Every byte is transformed and emitted + /// immediately; nothing is buffered across calls. + pub fn do_encrypt_update(&mut self, data: &mut [u8]) { + if !self.state_machine.is_encrypt() { + panic!("Ascon-AEAD128: do_encrypt_update called on a decryptor"); + } + self.check_data(); + + let mut data = data; + while !data.is_empty() { + if self.pos == 0 && data.len() >= RATE { + let c0 = self.state[0] ^ load_u64_le(data, 0); + let c1 = self.state[1] ^ load_u64_le(data, 8); + store_u64_le(data, 0, c0); + store_u64_le(data, 8, c1); + self.state[0] = c0; + self.state[1] = c1; + p8(&mut self.state); + data = &mut data[RATE..]; + } else { + data[0] = self.encrypt_byte(data[0]); + data = &mut data[1..]; + } + } + } + + /// Finish encryption; returns the 128-bit tag (SP 800-232 §4.1.1 steps 3-4). Pads the final + /// (possibly empty) plaintext block; no further bytes are emitted here since every + /// plaintext/ciphertext byte was already written by `do_encrypt_update`. + pub fn do_encrypt_final(mut self) -> [u8; TAG_LEN] { + if !self.state_machine.is_encrypt() { + panic!("Ascon-AEAD128: do_encrypt_final called on a decryptor"); + } + self.check_data(); + // Padding of the final (possibly empty) plaintext block (Eq. 27). + self.xor_state_byte(self.pos, 0x01); + self.finish_data() + } + + /// Decrypt `data` in place (SP 800-232 §4.1.2 step 3). Every byte is transformed and emitted + /// immediately; the plaintext is **not** authenticated until [`AsconAead128::do_decrypt_final`] + /// returns `Ok`. + pub fn do_decrypt_update(&mut self, data: &mut [u8]) { + if self.state_machine.is_encrypt() { + panic!("Ascon-AEAD128: do_decrypt_update called on an encryptor"); + } + self.check_data(); + + let mut data = data; + while !data.is_empty() { + if self.pos == 0 && data.len() >= RATE { + let t0 = load_u64_le(data, 0); + let t1 = load_u64_le(data, 8); + store_u64_le(data, 0, self.state[0] ^ t0); + store_u64_le(data, 8, self.state[1] ^ t1); + self.state[0] = t0; + self.state[1] = t1; + p8(&mut self.state); + data = &mut data[RATE..]; + } else { + data[0] = self.decrypt_byte(data[0]); + data = &mut data[1..]; + } + } + } + + /// Finish decryption, checking `tag` in constant time (SP 800-232 §4.1.2 steps 3-4). + pub fn do_decrypt_final(mut self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { + if self.state_machine.is_encrypt() { + panic!("Ascon-AEAD128: do_decrypt_final called on an encryptor"); + } + self.check_data(); + // Padding of the final (possibly empty) ciphertext block (Eq. 47). + self.xor_state_byte(self.pos, 0x01); + let computed = self.finish_data(); + + if !ct_eq_bytes(&computed, tag) { + return Err(SymmetricCipherError::AEADTagCheckFailed); + } + Ok(()) + } +} + +impl Algorithm for AsconAead128 { + const ALG_NAME: &'static str = "Ascon-AEAD128"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +// Ascon-AEAD128 as an `AEADCipher`. `encrypt`/`encrypt_out`/`decrypt`/`decrypt_out` are the +// "basic" (non-AEAD) view: the init data is the 128-bit nonce, and the ciphertext produced by +// these APIs is `Ascon ciphertext || 16-byte tag` (empty AAD). `aead_*` are the full AEAD view +// with associated data and a separate tag. +impl AEADCipher for AsconAead128 { + #[cfg(feature = "std")] + fn encrypt( + key: &KeyMaterial, + plaintext: &[u8], + ) -> Result<([u8; NONCE_LEN], Vec), SymmetricCipherError> { + let mut ciphertext = vec![0u8; plaintext.len() + TAG_LEN]; + let (nonce, written) = Self::encrypt_out(key, plaintext, &mut ciphertext)?; + ciphertext.truncate(written); + Ok((nonce, ciphertext)) + } + + fn encrypt_out( + key: &KeyMaterial, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { + let _ = Self::checked_key(key)?; + let nonce = Self::fresh_nonce()?; + // No associated data for the plain, non-AEAD view; the tag is appended to `ciphertext`. + // `encrypt` itself checks that `ciphertext` is long enough. + let written = Self::encrypt(key, &nonce, None, plaintext, ciphertext)?; + Ok((nonce, written)) + } + + #[cfg(feature = "std")] + fn decrypt( + key: &KeyMaterial, + init_data: [u8; NONCE_LEN], + ciphertext: &[u8], + ) -> Result, SymmetricCipherError> { + if ciphertext.len() < TAG_LEN { + return Err(SymmetricCipherError::GenericError( + "Ascon-AEAD128 ciphertext shorter than tag", + )); + } + let mut plaintext = vec![0u8; ciphertext.len() - TAG_LEN]; + let written = Self::decrypt_out(key, init_data, ciphertext, &mut plaintext)?; + plaintext.truncate(written); + Ok(plaintext) + } + + fn decrypt_out( + key: &KeyMaterial, + init_data: [u8; NONCE_LEN], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + let _ = Self::checked_key(key)?; + if ciphertext.len() < TAG_LEN { + return Err(SymmetricCipherError::GenericError( + "Ascon-AEAD128 ciphertext shorter than tag", + )); + } + let pt_len = ciphertext.len() - TAG_LEN; + if plaintext.len() < pt_len { + return Err(SymmetricCipherError::IncorrectOutputBufferLength( + "Ascon-AEAD128 plaintext buffer too small", + pt_len, + )); + } + // `ciphertext` is `Ascon ciphertext || 16-byte tag`; `decrypt` splits it internally. + // This plain, non-AEAD view has no AAD and so nothing that distinguishes an + // authentication failure from any other decryption failure; report both as + // `DecryptionFailed`, matching the trait's documented "the caller learns only that + // decryption failed". `AEADTagCheckFailed` is reserved for the AEAD view + // (`aead_decrypt`/`aead_decrypt_out`), which is honest about there being a separate tag. + Self::decrypt(key, &init_data, None, ciphertext, plaintext).map_err(|e| match e { + SymmetricCipherError::AEADTagCheckFailed => SymmetricCipherError::DecryptionFailed, + other => other, + }) + } + + #[cfg(feature = "std")] + fn aead_encrypt( + key: &KeyMaterial, + aad: &[u8], + plaintext: &[u8], + ) -> Result<([u8; NONCE_LEN], Vec, [u8; TAG_LEN]), SymmetricCipherError> { + let mut ciphertext = vec![0u8; plaintext.len()]; + let (nonce, written, tag) = Self::aead_encrypt_out(key, aad, plaintext, &mut ciphertext)?; + ciphertext.truncate(written); + Ok((nonce, ciphertext, tag)) + } + + fn aead_encrypt_out( + key: &KeyMaterial, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { + let _ = Self::checked_key(key)?; + if ciphertext.len() < plaintext.len() { + return Err(SymmetricCipherError::IncorrectOutputBufferLength( + "Ascon-AEAD128 ciphertext buffer too small", + plaintext.len(), + )); + } + let nonce = Self::fresh_nonce()?; + let aad_opt = if aad.is_empty() { None } else { Some(aad) }; + let mut cipher = Self::new(key, &nonce, aad_opt, true)?; + ciphertext[..plaintext.len()].copy_from_slice(plaintext); + cipher.do_encrypt_update(&mut ciphertext[..plaintext.len()]); + let tag = cipher.do_encrypt_final(); + Ok((nonce, plaintext.len(), tag)) + } + + fn do_aead_encrypt_final(self) -> Result<[u8; TAG_LEN], SymmetricCipherError> { + Ok(self.do_encrypt_final()) + } + + #[cfg(feature = "std")] + fn aead_decrypt( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + ) -> Result, SymmetricCipherError> { + let mut plaintext = vec![0u8; ciphertext.len()]; + let written = Self::aead_decrypt_out(key, nonce, aad, ciphertext, tag, &mut plaintext)?; + plaintext.truncate(written); + Ok(plaintext) + } + + fn aead_decrypt_out( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + plaintext: &mut [u8], + ) -> Result { + let _ = Self::checked_key(key)?; + if plaintext.len() < ciphertext.len() { + return Err(SymmetricCipherError::IncorrectOutputBufferLength( + "Ascon-AEAD128 plaintext buffer too small", + ciphertext.len(), + )); + } + let aad_opt = if aad.is_empty() { None } else { Some(aad) }; + let mut cipher = Self::new(key, nonce, aad_opt, false)?; + plaintext[..ciphertext.len()].copy_from_slice(ciphertext); + cipher.do_decrypt_update(&mut plaintext[..ciphertext.len()]); + match cipher.do_decrypt_final(tag) { + Ok(()) => Ok(ciphertext.len()), + Err(e) => { + // A failed tag check must not leave plaintext in the caller's buffer. + plaintext[..ciphertext.len()].fill(0); + Err(e) + } + } + } + + fn do_aead_decrypt_final(self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { + self.do_decrypt_final(tag) + } +} + +/// Adapts [`AsconAead128`]'s encrypting direction to [`AEADCipherEncryptor`]; see the module docs +/// for why this is a thin wrapper rather than a change to `AsconAead128` itself. +pub struct AsconAead128Encryptor(AsconAead128); + +impl Algorithm for AsconAead128Encryptor { + const ALG_NAME: &'static str = AsconAead128::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = AsconAead128::MAX_SECURITY_STRENGTH; +} + +impl AEADCipherEncryptor for AsconAead128Encryptor { + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + let nonce = AsconAead128::fresh_nonce()?; + Ok((Self(AsconAead128::new(key, &nonce, None, true)?), nonce)) + } + + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + let mut nonce = [0u8; NONCE_LEN]; + rng.next_bytes_out(&mut nonce)?; + Ok((Self(AsconAead128::new(key, &nonce, None, true)?), nonce)) + } + + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.0.do_update_aad(aad) + } + + /// Ascon-AEAD128 never buffers: every byte given is a byte returned. + fn update_out_len(&self, input_len: usize) -> usize { + input_len + } + + fn do_update_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + if ciphertext.len() < plaintext.len() { + return Err(SymmetricCipherError::IncorrectOutputBufferLength( + "ciphertext", + plaintext.len(), + )); + } + let out = &mut ciphertext[..plaintext.len()]; + out.copy_from_slice(plaintext); + self.0.do_encrypt_update(out); + Ok(plaintext.len()) + } + + /// `output` is always `[u8; 0]`: nothing is ever held back to flush. + fn do_encrypt_final( + self, + _output: &mut [u8; 0], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + Ok((0, self.0.do_encrypt_final())) + } +} + +/// Adapts [`AsconAead128`]'s decrypting direction to [`AEADCipherDecryptor`]; see the module docs +/// for why this is a thin wrapper rather than a change to `AsconAead128` itself. +pub struct AsconAead128Decryptor(AsconAead128); + +impl Algorithm for AsconAead128Decryptor { + const ALG_NAME: &'static str = AsconAead128::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = AsconAead128::MAX_SECURITY_STRENGTH; +} + +impl AEADCipherDecryptor for AsconAead128Decryptor { + fn do_decrypt_init( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ) -> Result { + Ok(Self(AsconAead128::new(key, nonce, None, false)?)) + } + + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.0.do_update_aad(aad) + } + + /// Ascon-AEAD128 never buffers: every byte given is a byte returned. + fn update_out_len(&self, input_len: usize) -> usize { + input_len + } + + fn do_update_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + if plaintext.len() < ciphertext.len() { + return Err(SymmetricCipherError::IncorrectOutputBufferLength( + "plaintext", + ciphertext.len(), + )); + } + let out = &mut plaintext[..ciphertext.len()]; + out.copy_from_slice(ciphertext); + self.0.do_decrypt_update(out); + Ok(ciphertext.len()) + } + + /// `output` is always `[u8; 0]`: nothing is ever held back to flush. + fn do_decrypt_final( + self, + tag: &[u8; TAG_LEN], + _output: &mut [u8; 0], + ) -> Result { + self.0.do_decrypt_final(tag)?; + Ok(0) + } +} + +impl Debug for AsconAead128 { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "AsconAead128 (key/state masked)") + } +} + +impl Display for AsconAead128 { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "AsconAead128 (key/state masked)") + } +} + +/// Length in bytes of the serialized state of [`AsconAead128`]. +/// Layout: 3-byte library version || 1-byte state tag || 40-byte permutation state (5 × u64 LE) +/// || 1-byte byte position within the current rate block || 1-byte call-state/direction. +/// The secret key is **not** serialized; it is re-supplied to [`SuspendableKeyed::from_suspended`]. +pub const SUSPENDED_ASCON_AEAD128_STATE_LEN: usize = 46; + +const AEAD128_STATE_TAG: u8 = 0x04; + +impl SuspendableKeyed for AsconAead128 { + // The 128-bit key must be re-supplied when resuming; it is never part of the serialized state, + // and is re-validated exactly as `new()` validates it. + type Key = KeyMaterial; + + fn suspend(self) -> [u8; SUSPENDED_ASCON_AEAD128_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_ASCON_AEAD128_STATE_LEN]; + // infallible: add_lib_ver returns a slice of exactly SUSPENDED_ASCON_AEAD128_STATE_LEN - 3 = 43 bytes. + let out: &mut [u8; SUSPENDED_ASCON_AEAD128_STATE_LEN - 3] = + add_lib_ver(&mut out_to_return).try_into().unwrap(); + + out[0] = AEAD128_STATE_TAG; + for i in 0..5 { + out[1 + i * 8..1 + i * 8 + 8].copy_from_slice(&self.state[i].to_le_bytes()); + } + debug_assert!(self.pos < RATE); + out[41] = self.pos as u8; + out[42] = self.state_machine.to_u8(); + + out_to_return + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_AEAD128_STATE_LEN], + key: &Self::Key, + ) -> Result { + // infallible: check_lib_ver returns a slice of exactly SUSPENDED_ASCON_AEAD128_STATE_LEN - 3 = 43 bytes. + let input: &[u8; SUSPENDED_ASCON_AEAD128_STATE_LEN - 3] = + check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + + if input[0] != AEAD128_STATE_TAG { + return Err(SuspendableError::InvalidData); + } + let mut s = Secret::::new(); + for i in 0..5 { + // infallible: each slice is exactly 8 bytes (1+i*8..1+i*8+8) by construction. + s[i] = u64::from_le_bytes(input[1 + i * 8..1 + i * 8 + 8].try_into().unwrap()); + } + let pos = input[41] as usize; + if pos >= RATE { + return Err(SuspendableError::InvalidData); + } + let state_machine = + StateMachine::from_u8(input[42]).ok_or(SuspendableError::InvalidData)?; + // A nonzero byte position implies at least one AAD/data byte has already been absorbed + // into the current rate block, which is only possible once the *Aad or *Data phase has + // begun -- never while still in *Init. + if pos != 0 && state_machine.is_init() { + return Err(SuspendableError::InvalidData); + } + + let key_words = Self::checked_key(key).map_err(|_| SuspendableError::InvalidData)?; + let mut key_secret = Secret::<[u64; 2]>::new(); + *key_secret = key_words; + + Ok(AsconAead128 { key: key_secret, state: s, pos, state_machine }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // StateMachine is private, so its to_u8/from_u8 round trip -- exercised end-to-end via + // suspend/resume in tests/aead128_tests.rs for the states reachable there -- is pinned + // directly here for every discriminant, including ones a successful resume never needs to + // decode into (EncInit/EncAad/DecInit/DecAad never survive to be the *end* state of a + // still-running cipher in the integration tests, since further processing always advances + // them to *Data). + #[test] + fn state_machine_u8_round_trip() { + let all = [ + StateMachine::EncInit, + StateMachine::EncAad, + StateMachine::EncData, + StateMachine::DecInit, + StateMachine::DecAad, + StateMachine::DecData, + ]; + for s in all { + assert_eq!(StateMachine::from_u8(s.to_u8()), Some(s), "round trip failed for {s:?}"); + } + // Unassigned discriminants (3 and 7 are deliberately skipped by to_u8's encoding) must + // be rejected, not silently mapped to a variant. + for v in [3u8, 7, 200] { + assert_eq!(StateMachine::from_u8(v), None, "discriminant {v} must be rejected"); + } + } +} diff --git a/crypto/ascon/src/ascon_cxof128.rs b/crypto/ascon/src/ascon_cxof128.rs new file mode 100644 index 00000000..4a0b055f --- /dev/null +++ b/crypto/ascon/src/ascon_cxof128.rs @@ -0,0 +1,218 @@ +//! Ascon-CXOF128 customized extendable-output function (NIST SP 800-232 §5.3). +//! +//! A variant of Ascon-XOF128 that first absorbs a user-supplied customization string `Z` +//! (length-prefixed per SP 800-232 Alg. 7) to provide domain separation. Same sponge parameters as +//! Ascon-XOF128 (rate = 64 bits, capacity = 256 bits, `Ascon-p[12]`). + +use bouncycastle_core::errors::{HashError, SuspendableError}; +use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_core::traits::{Algorithm, SecurityStrength, Suspendable, XOF}; +use bouncycastle_utils::secret::Secret; + +use crate::sponge::{RATE, Sponge}; + +/// Maximum customization-string length in bytes (2048 bits, per SP 800-232 §5.3). +const MAX_CUSTOMIZATION_BYTES: usize = 256; + +/// Ascon-CXOF128 customized extendable-output function (NIST SP 800-232 §5.3). +#[derive(Clone)] +pub struct AsconCXof128 { + sponge: Sponge, +} + +impl AsconCXof128 { + /// Create a new Ascon-CXOF128 instance with no customization string. + pub fn new() -> Self { + // Precomputed state after initializing and then absorbing an empty customization string + // (SP 800-232 Algorithm 7 with |Z| = 0): starting from the Table 12 CXOF128 initialization + // state, XOR the length word Z_0 = int64(0) into S[0..63], Ascon-p[12], then XOR the + // pad-only last customization block (Eq. 77: pad(empty, 64) = 0x01 || 0^63) into S[0..63] + // and Ascon-p[12] again. Recomputed from those raw Table 12 words and pinned by + // `permutation::tests::cxof128_empty_customization_state_matches_algorithm_7`. + let mut sponge = Sponge::from_state([ + 0x500CCCC894E3C9E8, 0x5BED06F28F71248D, 0x3B03A0F930AFD512, 0x112EF093AA5C698B, + 0x00C8356340A347F0, + ]); + sponge.reset_buffer(); + Self { sponge } + } + + /// Create a new Ascon-CXOF128 instance with the given customization string `z`. + /// + /// Returns [`HashError::InvalidInput`] if `z` is longer than 256 bytes (2048 bits, the bound + /// required by SP 800-232 §5.3). + pub fn with_customization(z: &[u8]) -> Result { + if z.len() > MAX_CUSTOMIZATION_BYTES { + return Err(HashError::InvalidInput( + "Ascon-CXOF128 customization string exceeds 256 bytes", + )); + } + if z.is_empty() { + return Ok(Self::new()); + } + + // Precomputed state after the initialization permutation (SP 800-232 Table 12). + let mut sponge = Sponge::from_state([ + 0x675527C2A0E8DE03, 0x43D12D7DC0377BBC, 0xE9901DEC426E81B5, 0x2AB14907720780B6, + 0x8F3F1D02D432BC46, + ]); + + // Z0 = int64(|Z|) in bits, then absorb the parsed/padded customization blocks + // (SP 800-232 §5.3 Eq. 75-78 / Algorithm 7, "Customization" loop). + let bit_length = (z.len() as u64) << 3; + sponge.xor_word0(bit_length); + sponge.permute(); + sponge.absorb(z); + sponge.pad_and_absorb(); + sponge.permute(); + + // Customization is complete; reset the buffer to begin the message-absorb phase. + sponge.reset_buffer(); + Ok(Self { sponge }) + } + + // Squeeze `output.len()` bytes of output. May be called multiple times; the first call ends the + // absorb phase by padding and absorbing the final block. Returns the number of bytes written. + fn squeeze_into(&mut self, output: &mut [u8]) -> usize { + let written = output.len(); + if !self.sponge.squeezing() { + self.sponge.pad_and_absorb(); + } + self.sponge.squeeze(output); + written + } +} + +impl Default for AsconCXof128 { + fn default() -> Self { + Self::new() + } +} + +impl Algorithm for AsconCXof128 { + const ALG_NAME: &'static str = "Ascon-CXOF128"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl XOF for AsconCXof128 { + fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { + self.sponge.absorb(data); + let mut out = vec![0u8; result_len]; + self.squeeze_into(&mut out); + out + } + + fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.sponge.absorb(data); + self.squeeze_into(output) + } + + fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> { + if self.sponge.squeezing() { + return Err(HashError::InvalidState( + "Ascon-CXOF128 cannot absorb after squeezing has begun", + )); + } + self.sponge.absorb(data); + Ok(()) + } + + fn absorb_last_partial_byte( + &mut self, + _partial_byte: u8, + _num_partial_bits: usize, + ) -> Result<(), HashError> { + Err(HashError::InvalidInput("Ascon-CXOF128 does not support partial byte input")) + } + + fn squeeze(&mut self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.squeeze_into(&mut out); + out + } + + fn squeeze_out(&mut self, output: &mut [u8]) -> usize { + self.squeeze_into(output) + } + + fn squeeze_partial_byte_final(self, _num_bits: usize) -> Result { + Err(HashError::InvalidInput("Ascon-CXOF128 does not support partial byte output")) + } + + fn squeeze_partial_byte_final_out( + self, + _num_bits: usize, + _output: &mut u8, + ) -> Result<(), HashError> { + Err(HashError::InvalidInput("Ascon-CXOF128 does not support partial byte output")) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::_128bit + } +} + +/// Length in bytes of the serialized state of [`AsconCXof128`]. +/// Layout: 3-byte library version || 1-byte state tag || 40-byte sponge state (5 × u64 LE) +/// || 8-byte rate buffer || 1-byte buffer position || 1-byte squeezing flag. +/// +/// Note: the customization string is absorbed at construction time and is not part of the +/// suspended state; resuming continues the message-absorb / squeeze phase already in progress. +pub const SUSPENDED_ASCON_CXOF128_STATE_LEN: usize = 54; + +// Distinguishes an Ascon-CXOF128 serialized state from the other (same-shaped) Ascon sponge states. +const CXOF128_STATE_TAG: u8 = 0x03; + +impl Suspendable for AsconCXof128 { + fn suspend(self) -> [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_ASCON_CXOF128_STATE_LEN]; + // infallible: add_lib_ver returns a slice of exactly SUSPENDED_ASCON_CXOF128_STATE_LEN - 3 = 51 bytes. + let out: &mut [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN - 3] = + add_lib_ver(&mut out_to_return).try_into().unwrap(); + + out[0] = CXOF128_STATE_TAG; + let state = self.sponge.state_words(); + for i in 0..5 { + out[1 + i * 8..1 + i * 8 + 8].copy_from_slice(&state[i].to_le_bytes()); + } + out[41..49].copy_from_slice(&self.sponge.buf_bytes()); + debug_assert!(self.sponge.buf_pos() <= RATE); + out[49] = self.sponge.buf_pos() as u8; + out[50] = self.sponge.squeezing() as u8; + + out_to_return + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN], + ) -> Result { + // infallible: check_lib_ver returns a slice of exactly SUSPENDED_ASCON_CXOF128_STATE_LEN - 3 = 51 bytes. + let input: &[u8; SUSPENDED_ASCON_CXOF128_STATE_LEN - 3] = + check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + + if input[0] != CXOF128_STATE_TAG { + return Err(SuspendableError::InvalidData); + } + let mut s = Secret::<[u64; 5]>::new(); + for i in 0..5 { + // infallible: each slice is exactly 8 bytes (1+i*8..1+i*8+8) by construction. + s[i] = u64::from_le_bytes(input[1 + i * 8..1 + i * 8 + 8].try_into().unwrap()); + } + let mut buf = Secret::<[u8; RATE]>::new(); + buf.copy_from_slice(&input[41..49]); + let buf_pos = input[49] as usize; + let squeezing = match input[50] { + 0 => false, + 1 => true, + _ => return Err(SuspendableError::InvalidData), + }; + // While absorbing, buf_pos must be < RATE (a full buffer is drained immediately); once + // squeezing, buf_pos may equal RATE (meaning "no leftover squeezed byte buffered"). + let valid_pos = if squeezing { buf_pos <= RATE } else { buf_pos < RATE }; + if !valid_pos { + return Err(SuspendableError::InvalidData); + } + + Ok(AsconCXof128 { sponge: Sponge::from_parts(s, buf, buf_pos, squeezing) }) + } +} diff --git a/crypto/ascon/src/ascon_hash256.rs b/crypto/ascon/src/ascon_hash256.rs new file mode 100644 index 00000000..9d2b87d5 --- /dev/null +++ b/crypto/ascon/src/ascon_hash256.rs @@ -0,0 +1,185 @@ +//! Ascon-Hash256 cryptographic hash (NIST SP 800-232 §5.1), producing a 256-bit digest. +//! +//! Sponge mode over `Ascon-p[12]` with rate = 64 bits, capacity = 256 bits. + +use bouncycastle_core::errors::{HashError, SuspendableError}; +use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_core::traits::{Algorithm, Hash, HashAlgParams, SecurityStrength, Suspendable}; +use bouncycastle_utils::secret::Secret; + +use crate::sponge::{RATE, Sponge}; + +const DIGEST_BYTES: usize = 32; + +/// Ascon-Hash256 hash function (NIST SP 800-232 §5.1), producing a 256-bit digest. +#[derive(Clone)] +pub struct AsconHash256 { + sponge: Sponge, +} + +impl AsconHash256 { + /// Creates a new AsconHash256 instance. + pub fn new() -> Self { + // Precomputed state after the initialization permutation (SP 800-232 Table 12). + Self { + sponge: Sponge::from_state([ + 0x9B1E_5494_E934_D681, 0x4BC3_A01E_3337_51D2, 0xAE65_396C_6B34_B81A, + 0x3C7F_D4A4_D56A_4DB3, 0x1A5C_4649_06C5_976D, + ]), + } + } + + /// One-shot hash of `data`, returning the 32-byte digest. + pub fn digest(data: &[u8]) -> [u8; DIGEST_BYTES] { + let mut hasher = Self::new(); + hasher.sponge.absorb(data); + let mut out = [0u8; DIGEST_BYTES]; + hasher.squeeze_into(&mut out); + out + } + + // Pad, absorb the final block, and squeeze the four 64-bit digest blocks (SP 800-232 + // Algorithm 5). The 32-byte digest is exactly RATE * 4 bytes, so a single generic + // `Sponge::squeeze()` call over the whole output produces all four blocks with no leftover. + fn squeeze_into(&mut self, output: &mut [u8; DIGEST_BYTES]) { + self.sponge.pad_and_absorb(); + self.sponge.squeeze(output); + } +} + +impl Default for AsconHash256 { + fn default() -> Self { + Self::new() + } +} + +impl Algorithm for AsconHash256 { + const ALG_NAME: &'static str = "Ascon-Hash256"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl HashAlgParams for AsconHash256 { + const OUTPUT_LEN: usize = DIGEST_BYTES; + const BLOCK_LEN: usize = RATE; +} + +impl Hash for AsconHash256 { + fn block_bitlen(&self) -> usize { + RATE * 8 + } + + fn output_len(&self) -> usize { + DIGEST_BYTES + } + + fn hash(mut self, data: &[u8]) -> Vec { + self.sponge.absorb(data); + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + out.to_vec() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.sponge.absorb(data); + output.fill(0); + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + let n = core::cmp::min(output.len(), DIGEST_BYTES); + output[..n].copy_from_slice(&out[..n]); + n + } + + fn do_update(&mut self, data: &[u8]) { + self.sponge.absorb(data); + } + + fn do_final(mut self) -> Vec { + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + out.to_vec() + } + + fn do_final_out(mut self, output: &mut [u8]) -> usize { + output.fill(0); + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + let n = core::cmp::min(output.len(), DIGEST_BYTES); + output[..n].copy_from_slice(&out[..n]); + n + } + + fn do_final_partial_bits( + self, + _partial_byte: u8, + _num_partial_bits: usize, + ) -> Result, HashError> { + Err(HashError::InvalidInput("Ascon-Hash256 does not support partial byte input")) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + _num_partial_bits: usize, + _output: &mut [u8], + ) -> Result { + Err(HashError::InvalidInput("Ascon-Hash256 does not support partial byte input")) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::_128bit + } +} + +/// Length in bytes of the serialized state of [`AsconHash256`]. +/// Layout: 3-byte library version || 1-byte state tag || 40-byte sponge state (5 × u64 LE) +/// || 8-byte rate buffer || 1-byte buffer position. +pub const SUSPENDED_ASCON_HASH256_STATE_LEN: usize = 53; + +// Distinguishes an Ascon-Hash256 serialized state from the other (same-shaped) Ascon sponge states. +const HASH256_STATE_TAG: u8 = 0x01; + +impl Suspendable for AsconHash256 { + fn suspend(self) -> [u8; SUSPENDED_ASCON_HASH256_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_ASCON_HASH256_STATE_LEN]; + // infallible: add_lib_ver returns a slice of exactly SUSPENDED_ASCON_HASH256_STATE_LEN - 3 = 50 bytes. + let out: &mut [u8; SUSPENDED_ASCON_HASH256_STATE_LEN - 3] = + add_lib_ver(&mut out_to_return).try_into().unwrap(); + + out[0] = HASH256_STATE_TAG; + let state = self.sponge.state_words(); + for i in 0..5 { + out[1 + i * 8..1 + i * 8 + 8].copy_from_slice(&state[i].to_le_bytes()); + } + out[41..49].copy_from_slice(&self.sponge.buf_bytes()); + // buf_pos is always < RATE (8) before squeezing has begun, so it fits in one byte. + debug_assert!(self.sponge.buf_pos() < RATE); + out[49] = self.sponge.buf_pos() as u8; + + out_to_return + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_HASH256_STATE_LEN], + ) -> Result { + // infallible: check_lib_ver returns a slice of exactly SUSPENDED_ASCON_HASH256_STATE_LEN - 3 = 50 bytes. + let input: &[u8; SUSPENDED_ASCON_HASH256_STATE_LEN - 3] = + check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + + if input[0] != HASH256_STATE_TAG { + return Err(SuspendableError::InvalidData); + } + let mut s = Secret::<[u64; 5]>::new(); + for i in 0..5 { + // infallible: each slice is exactly 8 bytes (1+i*8..1+i*8+8) by construction. + s[i] = u64::from_le_bytes(input[1 + i * 8..1 + i * 8 + 8].try_into().unwrap()); + } + let mut buf = Secret::<[u8; RATE]>::new(); + buf.copy_from_slice(&input[41..49]); + let buf_pos = input[49] as usize; + if buf_pos >= RATE { + return Err(SuspendableError::InvalidData); + } + + Ok(AsconHash256 { sponge: Sponge::from_parts(s, buf, buf_pos, false) }) + } +} diff --git a/crypto/ascon/src/ascon_xof128.rs b/crypto/ascon/src/ascon_xof128.rs new file mode 100644 index 00000000..0b6e8a8f --- /dev/null +++ b/crypto/ascon/src/ascon_xof128.rs @@ -0,0 +1,172 @@ +//! Ascon-XOF128 extendable-output function (NIST SP 800-232 §5.2). +//! +//! Sponge mode over `Ascon-p[12]` with rate = 64 bits, capacity = 256 bits. Supports the streaming +//! absorb/squeeze API of SP 800-232 §5.4 (squeeze may be called repeatedly). + +use bouncycastle_core::errors::{HashError, SuspendableError}; +use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_core::traits::{Algorithm, SecurityStrength, Suspendable, XOF}; +use bouncycastle_utils::secret::Secret; + +use crate::sponge::{RATE, Sponge}; + +/// Ascon-XOF128 as specified in NIST SP 800-232. +#[derive(Clone)] +pub struct AsconXof128 { + sponge: Sponge, +} + +impl AsconXof128 { + /// Creates a new Ascon-XOF128 instance. + pub fn new() -> Self { + // Precomputed state after the initialization permutation (SP 800-232 Table 12). + Self { + sponge: Sponge::from_state([ + 0xDA82CE768D9447EB, 0xCC7CE6C75F1EF969, 0xE7508FD780085631, 0x0EE0EA53416B58CC, + 0xE0547524DB6F0BDE, + ]), + } + } + + // Squeeze `output.len()` bytes of output. May be called multiple times; the first call ends the + // absorb phase by padding and absorbing the final block. Returns the number of bytes written. + fn squeeze_into(&mut self, output: &mut [u8]) -> usize { + let written = output.len(); + if !self.sponge.squeezing() { + self.sponge.pad_and_absorb(); + } + self.sponge.squeeze(output); + written + } +} + +impl Default for AsconXof128 { + fn default() -> Self { + Self::new() + } +} + +impl Algorithm for AsconXof128 { + const ALG_NAME: &'static str = "Ascon-XOF128"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl XOF for AsconXof128 { + fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { + self.sponge.absorb(data); + let mut out = vec![0u8; result_len]; + self.squeeze_into(&mut out); + out + } + + fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.sponge.absorb(data); + self.squeeze_into(output) + } + + fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> { + if self.sponge.squeezing() { + return Err(HashError::InvalidState( + "Ascon-XOF128 cannot absorb after squeezing has begun", + )); + } + self.sponge.absorb(data); + Ok(()) + } + + fn absorb_last_partial_byte( + &mut self, + _partial_byte: u8, + _num_partial_bits: usize, + ) -> Result<(), HashError> { + Err(HashError::InvalidInput("Ascon-XOF128 does not support partial byte input")) + } + + fn squeeze(&mut self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.squeeze_into(&mut out); + out + } + + fn squeeze_out(&mut self, output: &mut [u8]) -> usize { + self.squeeze_into(output) + } + + fn squeeze_partial_byte_final(self, _num_bits: usize) -> Result { + Err(HashError::InvalidInput("Ascon-XOF128 does not support partial byte output")) + } + + fn squeeze_partial_byte_final_out( + self, + _num_bits: usize, + _output: &mut u8, + ) -> Result<(), HashError> { + Err(HashError::InvalidInput("Ascon-XOF128 does not support partial byte output")) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::_128bit + } +} + +/// Length in bytes of the serialized state of [`AsconXof128`]. +/// Layout: 3-byte library version || 1-byte state tag || 40-byte sponge state (5 × u64 LE) +/// || 8-byte rate buffer || 1-byte buffer position || 1-byte squeezing flag. +pub const SUSPENDED_ASCON_XOF128_STATE_LEN: usize = 54; + +// Distinguishes an Ascon-XOF128 serialized state from the other (same-shaped) Ascon sponge states. +const XOF128_STATE_TAG: u8 = 0x02; + +impl Suspendable for AsconXof128 { + fn suspend(self) -> [u8; SUSPENDED_ASCON_XOF128_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_ASCON_XOF128_STATE_LEN]; + // infallible: add_lib_ver returns a slice of exactly SUSPENDED_ASCON_XOF128_STATE_LEN - 3 = 51 bytes. + let out: &mut [u8; SUSPENDED_ASCON_XOF128_STATE_LEN - 3] = + add_lib_ver(&mut out_to_return).try_into().unwrap(); + + out[0] = XOF128_STATE_TAG; + let state = self.sponge.state_words(); + for i in 0..5 { + out[1 + i * 8..1 + i * 8 + 8].copy_from_slice(&state[i].to_le_bytes()); + } + out[41..49].copy_from_slice(&self.sponge.buf_bytes()); + debug_assert!(self.sponge.buf_pos() <= RATE); + out[49] = self.sponge.buf_pos() as u8; + out[50] = self.sponge.squeezing() as u8; + + out_to_return + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_XOF128_STATE_LEN], + ) -> Result { + // infallible: check_lib_ver returns a slice of exactly SUSPENDED_ASCON_XOF128_STATE_LEN - 3 = 51 bytes. + let input: &[u8; SUSPENDED_ASCON_XOF128_STATE_LEN - 3] = + check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + + if input[0] != XOF128_STATE_TAG { + return Err(SuspendableError::InvalidData); + } + let mut s = Secret::<[u64; 5]>::new(); + for i in 0..5 { + // infallible: each slice is exactly 8 bytes (1+i*8..1+i*8+8) by construction. + s[i] = u64::from_le_bytes(input[1 + i * 8..1 + i * 8 + 8].try_into().unwrap()); + } + let mut buf = Secret::<[u8; RATE]>::new(); + buf.copy_from_slice(&input[41..49]); + let buf_pos = input[49] as usize; + let squeezing = match input[50] { + 0 => false, + 1 => true, + _ => return Err(SuspendableError::InvalidData), + }; + // While absorbing, buf_pos must be < RATE (a full buffer is drained immediately); once + // squeezing, buf_pos may equal RATE (meaning "no leftover squeezed byte buffered"). + let valid_pos = if squeezing { buf_pos <= RATE } else { buf_pos < RATE }; + if !valid_pos { + return Err(SuspendableError::InvalidData); + } + + Ok(AsconXof128 { sponge: Sponge::from_parts(s, buf, buf_pos, squeezing) }) + } +} diff --git a/crypto/ascon/src/lib.rs b/crypto/ascon/src/lib.rs new file mode 100644 index 00000000..661aa6e9 --- /dev/null +++ b/crypto/ascon/src/lib.rs @@ -0,0 +1,137 @@ +//! Ascon-based lightweight cryptography (NIST SP 800-232). +//! +//! This crate implements the four Ascon functions standardized in NIST SP 800-232 (August 2025): +//! +//! - [`ascon_aead128::AsconAead128`] — Ascon-AEAD128 authenticated encryption (128-bit +//! key/nonce/tag, 128-bit single-key security). +//! - [`ascon_hash256::AsconHash256`] — Ascon-Hash256 hash function (256-bit digest, 128-bit +//! security). +//! - [`ascon_xof128::AsconXof128`] — Ascon-XOF128 extendable-output function. +//! - [`ascon_cxof128::AsconCXof128`] — Ascon-CXOF128 customized extendable-output function. +//! +//! # Usage Examples +//! +//! Hashing (one-shot and streaming): +//! ``` +//! use bouncycastle_ascon::ascon_hash256::AsconHash256; +//! use bouncycastle_core::traits::Hash; +//! +//! // One-shot: +//! let digest = AsconHash256::digest(b"hello world"); +//! assert_eq!(digest.len(), 32); +//! +//! // Streaming: +//! let mut h = AsconHash256::new(); +//! h.do_update(b"hello "); +//! h.do_update(b"world"); +//! let mut out = [0u8; 32]; +//! h.do_final_out(&mut out); +//! assert_eq!(out, digest); +//! ``` +//! +//! Authenticated encryption (one-shot): +//! ``` +//! use bouncycastle_ascon::ascon_aead128::AsconAead128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let nonce = [1u8; 16]; // MUST be unique per encryption under a given key +//! let ad = b"associated data"; +//! let plaintext = b"secret message"; +//! +//! let mut ct = vec![0u8; plaintext.len() + 16]; // ciphertext || 16-byte tag +//! let n = AsconAead128::encrypt(&key, &nonce, Some(ad), plaintext, &mut ct).unwrap(); +//! ct.truncate(n); +//! +//! let mut pt = vec![0u8; ct.len() - 16]; +//! let m = AsconAead128::decrypt(&key, &nonce, Some(ad), &ct, &mut pt).unwrap(); +//! pt.truncate(m); +//! assert_eq!(&pt, plaintext); +//! ``` +//! +//! Authenticated encryption (streaming, in place): +//! ``` +//! use bouncycastle_ascon::ascon_aead128::AsconAead128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let nonce = [1u8; 16]; +//! +//! let mut buf = *b"secret message!!"; // transformed in place +//! let mut enc = AsconAead128::new(&key, &nonce, Some(b"associated data"), true).unwrap(); +//! enc.do_encrypt_update(&mut buf); // now ciphertext +//! let tag = enc.do_encrypt_final(); +//! +//! let mut dec = AsconAead128::new(&key, &nonce, Some(b"associated data"), false).unwrap(); +//! dec.do_decrypt_update(&mut buf); // now plaintext again, but not yet authenticated +//! dec.do_decrypt_final(&tag).unwrap(); // now authenticated +//! assert_eq!(&buf, b"secret message!!"); +//! ``` +//! +//! Extendable output: +//! ``` +//! use bouncycastle_ascon::ascon_xof128::AsconXof128; +//! use bouncycastle_core::traits::XOF; +//! +//! let out = AsconXof128::new().hash_xof(b"input", 64); +//! assert_eq!(out.len(), 64); +//! ``` +//! +//! # Memory Usage +//! +//! Ascon is a lightweight, permutation-based design intended for constrained devices. The internal +//! permutation state is 320 bits (40 bytes), held as five `u64` words, shared by all four +//! functions. There are no heap allocations in the streaming/`*_out` APIs, and stack usage is +//! small and constant; consequently this crate has no dedicated `mem_usage_benches` harness. +//! +//! | Type | In-memory size (bytes) | Suspended state size (bytes) | +//! |------|-------------------------|-------------------------------| +//! | [`ascon_aead128::AsconAead128`] | 72 | [`ascon_aead128::SUSPENDED_ASCON_AEAD128_STATE_LEN`] (46) | +//! | [`ascon_hash256::AsconHash256`] | 64 | [`ascon_hash256::SUSPENDED_ASCON_HASH256_STATE_LEN`] (53) | +//! | [`ascon_xof128::AsconXof128`] | 64 | [`ascon_xof128::SUSPENDED_ASCON_XOF128_STATE_LEN`] (54) | +//! | [`ascon_cxof128::AsconCXof128`] | 64 | [`ascon_cxof128::SUSPENDED_ASCON_CXOF128_STATE_LEN`] (54) | +//! +//! "In-memory size" is `core::mem::size_of` on a 64-bit target. +//! +//! # Security Considerations +//! +//! - **Nonce uniqueness (SP 800-232 R3):** a (key, nonce) pair must never be reused for two +//! different Ascon-AEAD128 encryptions. Nonce reuse breaks confidentiality. +//! - **Tag length:** this crate always produces and verifies the full 128-bit tag. Truncated tags +//! (SP 800-232 §4.2.1) are not exposed. +//! - **No partial-byte input:** Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 are byte-oriented; +//! their `do_final_partial_bits`/`do_final_partial_bits_out` (and the equivalent XOF methods) +//! always return `HashError::InvalidInput`, including when reached through `HashFactory`. A +//! caller that needs a partial-byte final block should reach for SHA-3, which supports one. +//! - **Decryption tag check failure:** a ciphertext decryption whose finalization returns +//! `Err(SymmetricCipherError::AEADTagCheckFailed)` must be treated as tampered, and the entire +//! plaintext rejected. The one-shot APIs ([`ascon_aead128::AsconAead128::decrypt`] and the +//! `AEADCipher` trait impl) zeroize their output buffer before returning that +//! error. The streaming API ([`ascon_aead128::AsconAead128::do_decrypt_update`] / +//! [`ascon_aead128::AsconAead128::do_decrypt_final`]) does not: plaintext bytes are necessarily +//! written to the caller's buffer *before* the tag can be checked, so an application streaming a +//! large plaintext must have a way to cancel the operation or transaction if finalization returns +//! an error. + +// `bouncycastle-core` still uses `Vec` internally (see the TODO at the top of +// crypto/core/src/lib.rs), which blocks this crate from being `#![no_std]` as long as it depends +// on core's `std`-gated APIs. +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +mod permutation; +mod sponge; + +pub mod ascon_aead128; +pub mod ascon_cxof128; +pub mod ascon_hash256; +pub mod ascon_xof128; + +/// Algorithm name for Ascon-AEAD128. +pub const ASCON_AEAD128_NAME: &str = "Ascon-AEAD128"; +/// Algorithm name for Ascon-Hash256. +pub const ASCON_HASH256_NAME: &str = "Ascon-Hash256"; +/// Algorithm name for Ascon-XOF128. +pub const ASCON_XOF128_NAME: &str = "Ascon-XOF128"; +/// Algorithm name for Ascon-CXOF128. +pub const ASCON_CXOF128_NAME: &str = "Ascon-CXOF128"; diff --git a/crypto/ascon/src/permutation.rs b/crypto/ascon/src/permutation.rs new file mode 100644 index 00000000..a373bb78 --- /dev/null +++ b/crypto/ascon/src/permutation.rs @@ -0,0 +1,138 @@ +//! The Ascon-p permutation family (NIST SP 800-232 §3), shared by all four functions in this +//! crate: Ascon-AEAD128 uses both `Ascon-p[12]` and `Ascon-p[8]`; Ascon-Hash256, Ascon-XOF128, and +//! Ascon-CXOF128 use only `Ascon-p[12]`. +//! +//! These also carry the little-endian load/store helpers, replacing the external `arrayref` +//! crate so that this crate carries no third-party runtime dependencies (per the project's +//! QUALITY_AND_STYLE rules). All callers pass slices that are at least 8 bytes long at the given +//! offset, so `copy_from_slice` is infallible by construction and no fallible conversion is +//! involved. + +/// Load the 8 bytes at `src[off..off + 8]` as a little-endian `u64`. +#[inline(always)] +pub(crate) fn load_u64_le(src: &[u8], off: usize) -> u64 { + let mut b = [0u8; 8]; + b.copy_from_slice(&src[off..off + 8]); + u64::from_le_bytes(b) +} + +/// Store `val` as little-endian into `dst[off..off + 8]`. +#[inline(always)] +pub(crate) fn store_u64_le(dst: &mut [u8], off: usize, val: u64) { + dst[off..off + 8].copy_from_slice(&val.to_le_bytes()); +} + +/// The 320-bit Ascon state (SP 800-232 §3.1 Eq. 2): five 64-bit words S0..S4. +pub(crate) type AsconState = [u64; 5]; + +// The constants const_0..const_15 used to derive the round constants of Ascon-p[r] +// (SP 800-232 Table 5). The round constant for round i (0 <= i <= r-1) of Ascon-p[r] is +// c_i = const_{16-r+i} (SP 800-232 §3.2 Eq. 3). +const ROUND_CONSTS: [u64; 16] = [ + 0x3c, 0x2d, 0x1e, 0x0f, 0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87, 0x78, 0x69, 0x5a, 0x4b, +]; + +/// One round p = p_L ∘ p_S ∘ p_C (SP 800-232 §3.2–3.4 Eq. 1): the constant-addition layer p_C +/// (§3.2 Eq. 4), the substitution layer p_S (§3.3 Eqs. 6–7), and the linear diffusion layer p_L +/// (§3.4 Eqs. 8–12) are fused here in their bitsliced form. +#[inline(always)] +pub(crate) fn round(s: &mut AsconState, c: u64) { + let sx = s[2] ^ c; + let t0 = s[0] ^ s[1] ^ sx ^ s[3] ^ (s[1] & (s[0] ^ sx ^ s[4])); + let t1 = s[0] ^ sx ^ s[3] ^ s[4] ^ ((s[1] ^ sx) & (s[1] ^ s[3])); + let t2 = s[1] ^ sx ^ s[4] ^ (s[3] & s[4]); + let t3 = s[0] ^ s[1] ^ sx ^ ((!s[0]) & (s[3] ^ s[4])); + let t4 = s[1] ^ s[3] ^ s[4] ^ ((s[0] ^ s[4]) & s[1]); + s[0] = t0 ^ t0.rotate_right(19) ^ t0.rotate_right(28); + s[1] = t1 ^ t1.rotate_right(39) ^ t1.rotate_right(61); + s[2] = !(t2 ^ t2.rotate_right(1) ^ t2.rotate_right(6)); + s[3] = t3 ^ t3.rotate_right(10) ^ t3.rotate_right(17); + s[4] = t4 ^ t4.rotate_right(7) ^ t4.rotate_right(41); +} + +/// Ascon-p[12] (SP 800-232 §3.2 Eq. 3: c_i = const_{4+i} for i = 0..11, i.e. round constants +/// const_4..const_15 of Table 5). +#[inline(always)] +pub(crate) fn p12(s: &mut AsconState) { + for &c in &ROUND_CONSTS[4..16] { + round(s, c); + } +} + +/// Ascon-p[8] (SP 800-232 §3.2 Eq. 3: c_i = const_{8+i} for i = 0..7, i.e. round constants +/// const_8..const_15 of Table 5). +#[inline(always)] +pub(crate) fn p8(s: &mut AsconState) { + for &c in &ROUND_CONSTS[8..16] { + round(s, c); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // SP 800-232 Table 14: initial values (before the initialization permutation). + const HASH256_IV: u64 = 0x0000080100cc0002; + const XOF128_IV: u64 = 0x0000080000cc0003; + const CXOF128_IV: u64 = 0x0000080000cc0004; + + // Pins the permutation independently of the KAT sweeps: SP 800-232 Table 12 gives the state + // at the end of each function's initialization phase, i.e. Ascon-p[12](IV || 0^256). + #[test] + fn p12_matches_table_12_precomputed_states() { + let mut s: AsconState = [HASH256_IV, 0, 0, 0, 0]; + p12(&mut s); + assert_eq!( + s, + [ + 0x9b1e5494e934d681, 0x4bc3a01e333751d2, 0xae65396c6b34b81a, 0x3c7fd4a4d56a4db3, + 0x1a5c464906c5976d, + ] + ); + + let mut s: AsconState = [XOF128_IV, 0, 0, 0, 0]; + p12(&mut s); + assert_eq!( + s, + [ + 0xda82ce768d9447eb, 0xcc7ce6c75f1ef969, 0xe7508fd780085631, 0x0ee0ea53416b58cc, + 0xe0547524db6f0bde, + ] + ); + + let mut s: AsconState = [CXOF128_IV, 0, 0, 0, 0]; + p12(&mut s); + assert_eq!( + s, + [ + 0x675527c2a0e8de03, 0x43d12d7dc0377bbc, 0xe9901dec426e81b5, 0x2ab14907720780b6, + 0x8f3f1d02d432bc46, + ] + ); + } + + // Pins `AsconCXof128::new()`'s precomputed empty-customization state (see + // `ascon_cxof128.rs`) by recomputing it from the Table 12 CXOF128 state above, following + // SP 800-232 Algorithm 7 with |Z| = 0: XOR the length word Z_0 = int64(0) into S[0..63], + // Ascon-p[12], then XOR the pad-only last customization block (Eq. 77: pad(empty, 64) = + // 0x01 || 0^63, i.e. byte 0x01 loaded little-endian into S[0..63]) and Ascon-p[12] again. + #[test] + fn cxof128_empty_customization_state_matches_algorithm_7() { + let mut s: AsconState = [ + 0x675527c2a0e8de03, 0x43d12d7dc0377bbc, 0xe9901dec426e81b5, 0x2ab14907720780b6, + 0x8f3f1d02d432bc46, + ]; + s[0] ^= 0u64; // Z_0 = int64(|Z|) = int64(0) = 0 (a no-op XOR, spelled out for clarity) + p12(&mut s); + s[0] ^= 0x01u64; // pad(empty, 64) = 0x01 || 0^63, loaded little-endian + p12(&mut s); + assert_eq!( + s, + [ + 0x500cccc894e3c9e8, 0x5bed06f28f71248d, 0x3b03a0f930afd512, 0x112ef093aa5c698b, + 0x00c8356340a347f0, + ] + ); + } +} diff --git a/crypto/ascon/src/sponge.rs b/crypto/ascon/src/sponge.rs new file mode 100644 index 00000000..c1618b6d --- /dev/null +++ b/crypto/ascon/src/sponge.rs @@ -0,0 +1,189 @@ +//! The absorb/pad/squeeze sponge shared by Ascon-Hash256, Ascon-XOF128, and Ascon-CXOF128 +//! (NIST SP 800-232 §5): a 64-bit rate over `Ascon-p[12]`. Each of those three types holds one +//! [`Sponge`] and differs only in its initial state and (for Ascon-CXOF128) an extra +//! customization-string absorption performed before message absorption begins. + +use bouncycastle_utils::secret::Secret; + +use crate::permutation::{AsconState, load_u64_le, p12, store_u64_le}; + +/// Rate in bytes for the Hash256/XOF128/CXOF128 sponge (64 bits, per SP 800-232 §5). +pub(crate) const RATE: usize = 8; + +pub(crate) struct Sponge { + // 320-bit sponge state (five 64-bit words S0..S4). Wrapped in `Secret` so the working state + // -- which absorbs the message -- is scrubbed with volatile writes when dropped. + s: Secret, + // Rate buffer: partial input block while absorbing, or leftover squeezed bytes afterwards. + buf: Secret<[u8; RATE]>, + buf_pos: usize, + squeezing: bool, +} + +impl Sponge { + /// Construct a sponge already in the given state (typically a function's precomputed + /// post-initialization state, SP 800-232 Table 12), ready to absorb. + pub(crate) fn from_state(state: AsconState) -> Self { + let mut s: Secret = Secret::new(); + *s = state; + Self { s, buf: Secret::new(), buf_pos: 0, squeezing: false } + } + + /// Reconstruct a sponge from raw parts (used by `Suspendable::from_suspended`). + pub(crate) fn from_parts( + s: Secret, + buf: Secret<[u8; RATE]>, + buf_pos: usize, + squeezing: bool, + ) -> Self { + Self { s, buf, buf_pos, squeezing } + } + + pub(crate) fn state_words(&self) -> [u64; 5] { + *self.s + } + + pub(crate) fn buf_bytes(&self) -> [u8; RATE] { + *self.buf + } + + pub(crate) fn buf_pos(&self) -> usize { + self.buf_pos + } + + pub(crate) fn squeezing(&self) -> bool { + self.squeezing + } + + /// XOR `v` into the first state word. Used by Ascon-CXOF128 to absorb the customization + /// string's bit length (SP 800-232 §5.3 Eq. 75) before the length-prefixed customization + /// blocks are absorbed via [`Sponge::absorb`]. + pub(crate) fn xor_word0(&mut self, v: u64) { + self.s[0] ^= v; + } + + /// Apply `Ascon-p[12]` to the state directly. Used by Ascon-CXOF128 between customization + /// blocks (SP 800-232 Algorithm 7). + pub(crate) fn permute(&mut self) { + p12(&mut self.s); + } + + /// Reset the rate buffer to begin a fresh absorb phase. Used by Ascon-CXOF128 once the + /// customization string has been fully absorbed, before message absorption begins. + pub(crate) fn reset_buffer(&mut self) { + self.buf.fill(0); + self.buf_pos = 0; + } + + /// Absorb input data. Panics if called after squeezing has begun. + pub(crate) fn absorb(&mut self, input: &[u8]) { + if self.squeezing { + panic!("attempt to absorb while squeezing"); + } + + let available = RATE - self.buf_pos; + if input.len() < available { + self.buf[self.buf_pos..self.buf_pos + input.len()].copy_from_slice(input); + self.buf_pos += input.len(); + return; + } + + let mut input = input; + + if self.buf_pos > 0 { + self.buf[self.buf_pos..].copy_from_slice(&input[..available]); + self.s[0] ^= u64::from_le_bytes(*self.buf); + p12(&mut self.s); + input = &input[available..]; + } + + while input.len() >= RATE { + self.s[0] ^= load_u64_le(input, 0); + p12(&mut self.s); + input = &input[RATE..]; + } + + self.buf[..input.len()].copy_from_slice(input); + self.buf_pos = input.len(); + } + + // Pad the final absorbed block (SP 800-232 Appendix A.2, Algorithm 2) by XORing in the + // buffered bytes (masked to `buf_pos` bytes -- any stale bytes beyond that in `buf` are + // masked off) followed by the padding bit at byte position `buf_pos`. Deliberately does not + // permute: the permutation is folded into the first block of `squeeze()` below, since Ascon- + // Hash256's fixed 4-block output and Ascon-XOF128/CXOF128's streaming output both begin + // their squeeze phase with a permute-then-read (SP 800-232 Algorithms 5-7). + pub(crate) fn pad_and_absorb(&mut self) { + let final_bits = (self.buf_pos << 3) as u32; + let x = u64::from_le_bytes(*self.buf); + let mask = + if final_bits == 0 { 0u64 } else { 0x00FF_FFFF_FFFF_FFFF_u64 >> (56 - final_bits) }; + self.s[0] ^= x & mask; + self.s[0] ^= 0x01u64 << final_bits; + } + + /// Squeeze `output.len()` bytes. May be called multiple times; the first call must follow + /// [`Sponge::pad_and_absorb`] and ends the absorb phase. + pub(crate) fn squeeze(&mut self, output: &mut [u8]) { + let mut output = output; + + if !self.squeezing { + self.squeezing = true; + self.buf_pos = RATE; + } else if self.buf_pos < RATE { + let available = RATE - self.buf_pos; + if output.len() <= available { + let end_pos = self.buf_pos + output.len(); + output.copy_from_slice(&self.buf[self.buf_pos..end_pos]); + self.buf_pos = end_pos; + return; + } + + output[..available].copy_from_slice(&self.buf[self.buf_pos..]); + output = &mut output[available..]; + self.buf_pos = RATE; + } + + while output.len() >= RATE { + p12(&mut self.s); + store_u64_le(output, 0, self.s[0]); + output = &mut output[RATE..]; + } + + if !output.is_empty() { + p12(&mut self.s); + *self.buf = self.s[0].to_le_bytes(); + output.copy_from_slice(&self.buf[..output.len()]); + self.buf_pos = output.len(); + } + } +} + +impl Clone for Sponge { + fn clone(&self) -> Self { + Self { + s: self.s.clone(), + buf: self.buf.clone(), + buf_pos: self.buf_pos, + squeezing: self.squeezing, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // `xor_word0` cannot be exercised as an XOR (as opposed to e.g. an OR) via any published KAT: + // its only caller (Ascon-CXOF128's customization-length absorption) combines a bit_length + // value -- always a multiple of 8 -- with a state word whose low 3 bits happen to be the + // only ones set for every customization length actually covered by NIST's KAT file (max 32 + // bytes). Pin the arithmetic directly instead. + #[test] + fn xor_word0_is_xor_not_or() { + let mut sponge = Sponge::from_state([0b0000_0101, 0, 0, 0, 0]); + sponge.xor_word0(0b0000_0110); + // 0b101 ^ 0b110 = 0b011. An OR would give 0b111. + assert_eq!(sponge.state_words()[0], 0b0000_0011); + } +} diff --git a/crypto/ascon/tests/aead128_tests.rs b/crypto/ascon/tests/aead128_tests.rs new file mode 100644 index 00000000..d9b06635 --- /dev/null +++ b/crypto/ascon/tests/aead128_tests.rs @@ -0,0 +1,768 @@ +//! Ascon-AEAD128 tests (NIST SP 800-232). +//! +//! - A small embedded set of NIST LWC known-answer vectors (always-on correctness, no external +//! repo required). The full sweep lives in `bc_test_data.rs`. +//! - Behavioral / contract tests (round-trips, streaming chunk-boundary equivalence, authentication +//! failures, determinism), driven through the inherent explicit-nonce API. +//! - The shared `AEADCipher` conformance framework (`core-test-framework`), which exercises the +//! generic `AEADCipher` trait surface with internally-generated nonces. + +use bouncycastle_ascon::ascon_aead128::{ + AsconAead128, AsconAead128Decryptor, AsconAead128Encryptor, +}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::traits::SecurityStrength; +use bouncycastle_core_test_framework::symmetric_ciphers::{ + TestFrameworkAEADCipher, TestFrameworkSimpleCipher, +}; +use bouncycastle_hex as hex; + +// All embedded vectors use this fixed key/nonce (the NIST LWC KAT convention). +const KEY: [u8; 16] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, +]; +const NONCE: [u8; 16] = [ + 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, +]; + +const PT_SIZES: [usize; 10] = [0, 1, 15, 16, 17, 31, 32, 33, 64, 100]; +const CHUNK_SIZES: [usize; 6] = [1, 3, 7, 13, 16, 17]; + +/// Embedded NIST LWC Ascon-AEAD128 vectors `(plaintext, associated_data, ciphertext||tag)` in hex. +/// Key = Nonce = 000102…0F. Spans empty input, AD-only (incl. a full 32-byte AD block), partial PT +/// with AD, and a multi-block plaintext. (Counts 1, 2, 5, 33, 68, 69, 153, 1057 of +/// LWC_AEAD_KAT_128_128.txt.) +const AEAD_KAT: &[(&str, &str, &str)] = &[ + ("", "", "4427D64B8E1E1451FC445960F0839BB0"), + ("", "00", "103AB79D913A0321287715A979BB8585"), + ("", "00010203", "C6FF3CF70575B144B955820D9BC7685E"), + ( + "", + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "22133A313FBF0B38029A45870AADC542", + ), + ("0001", "00", "25FB41D2732019820A0F8BAB4248B35E7B0B"), + ("0001", "0001", "49E57017A30E8073D1FA284AC8346110F89F"), + ( + "00010203", + "000102030405060708090A0B0C0D0E0F10111213", + "C305EB0E9A9A7833C5F6FB36BD82F1C78C322678", + ), + ( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "", + "E770D289D2A44AEE7CD0A48ECE5274E381BAD7E163DCC4970F7873610DEBBEB1A28657F6E82FE53D08B09EFF9330BD2B", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn ad_opt(ad: &[u8]) -> Option<&[u8]> { + if ad.is_empty() { None } else { Some(ad) } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +/// Build a `KeyMaterial<16>` suitable for `AsconAead128`. The NIST LWC KAT vectors include an +/// all-zero key (Count=1), which `KeyMaterial::from_bytes_as_type` would otherwise tag +/// `KeyType::Zeroized` / `SecurityStrength::None`; force the type/strength the way a caller who +/// knows the provenance of the key would (see `cli/src/helpers.rs::parse_seed`). +fn key_material(key: &[u8; 16]) -> KeyMaterial<16> { + let mut km = KeyMaterial::<16>::from_bytes_as_type(key, KeyType::SymmetricCipherKey).unwrap(); + do_hazardous_operations(&mut km, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::_128bit) + }) + .unwrap(); + km +} + +fn enc_oneshot(key: &[u8; 16], nonce: &[u8; 16], ad: &[u8], pt: &[u8]) -> Vec { + let km = key_material(key); + let mut out = vec![0u8; pt.len() + 16]; + let n = AsconAead128::encrypt(&km, nonce, ad_opt(ad), pt, &mut out).unwrap(); + out.truncate(n); + out +} + +fn dec_oneshot( + key: &[u8; 16], + nonce: &[u8; 16], + ad: &[u8], + ct: &[u8], +) -> Result, SymmetricCipherError> { + let km = key_material(key); + let mut out = vec![0u8; ct.len()]; + let n = AsconAead128::decrypt(&km, nonce, ad_opt(ad), ct, &mut out)?; + out.truncate(n); + Ok(out) +} + +fn enc_chunked(key: &[u8; 16], nonce: &[u8; 16], ad: &[u8], pt: &[u8], chunk: usize) -> Vec { + let km = key_material(key); + let mut cipher = AsconAead128::new(&km, nonce, ad_opt(ad), true).unwrap(); + let mut out = vec![0u8; pt.len() + 16]; + out[..pt.len()].copy_from_slice(pt); + + let chunk = chunk.max(1); + let mut off = 0; + while off < pt.len() { + let end = (off + chunk).min(pt.len()); + cipher.do_encrypt_update(&mut out[off..end]); + off = end; + } + let tag = cipher.do_encrypt_final(); + out[pt.len()..].copy_from_slice(&tag); + out +} + +fn dec_chunked( + key: &[u8; 16], + nonce: &[u8; 16], + ad: &[u8], + ct: &[u8], + chunk: usize, +) -> Result, SymmetricCipherError> { + let km = key_material(key); + let mut cipher = AsconAead128::new(&km, nonce, ad_opt(ad), false).unwrap(); + let pt_len = ct.len() - 16; + let mut out = vec![0u8; pt_len]; + out.copy_from_slice(&ct[..pt_len]); + + let chunk = chunk.max(1); + let mut off = 0; + while off < pt_len { + let end = (off + chunk).min(pt_len); + cipher.do_decrypt_update(&mut out[off..end]); + off = end; + } + // infallible: ct.len() - pt_len == 16 by construction above. + let tag: [u8; 16] = ct[pt_len..].try_into().unwrap(); + cipher.do_decrypt_final(&tag)?; + Ok(out) +} + +/* -------------------------------------------------------------------------- */ +/* Embedded known-answer vectors */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead128_embedded_kat() { + // The NIST LWC AEAD KAT convention uses Key == Nonce == 000102…0F (i.e. KEY for both). + let kat_nonce = KEY; + for (pt_hex, ad_hex, ct_hex) in AEAD_KAT { + let pt = dh(pt_hex); + let ad = dh(ad_hex); + let expected_ct = dh(ct_hex); + + let got_ct = enc_oneshot(&KEY, &kat_nonce, &ad, &pt); + assert_eq!(got_ct, expected_ct, "encrypt mismatch for PT={pt_hex} AD={ad_hex}"); + + let got_pt = + dec_oneshot(&KEY, &kat_nonce, &ad, &expected_ct).expect("decrypt should succeed"); + assert_eq!(got_pt, pt, "decrypt mismatch for CT={ct_hex}"); + } +} + +/* -------------------------------------------------------------------------- */ +/* Round-trips and AAD handling */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead_round_trip_sizes_and_ad() { + for &pt_len in PT_SIZES.iter() { + let pt = pattern(pt_len); + for ad in [Vec::new(), b"associated-data".to_vec(), pattern(40)] { + let ct = enc_oneshot(&KEY, &NONCE, &ad, &pt); + assert_eq!(ct.len(), pt_len + 16, "ciphertext = plaintext || 16-byte tag"); + let recovered = dec_oneshot(&KEY, &NONCE, &ad, &ct).expect("decrypt should succeed"); + assert_eq!(recovered, pt, "round-trip mismatch (pt_len={pt_len}, ad_len={})", ad.len()); + } + } +} + +#[test] +fn aead_aad_only_round_trip() { + // Empty plaintext, non-empty AD: ciphertext is just the 16-byte tag. + let ad = b"only-associated-data"; + let ct = enc_oneshot(&KEY, &NONCE, ad, b""); + assert_eq!(ct.len(), 16); + let recovered = dec_oneshot(&KEY, &NONCE, ad, &ct).expect("decrypt should succeed"); + assert!(recovered.is_empty()); +} + +/* -------------------------------------------------------------------------- */ +/* Streaming chunk-boundary equivalence */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead_streaming_matches_one_shot() { + for &pt_len in PT_SIZES.iter() { + let pt = pattern(pt_len); + let ad = pattern(20); + let ct_ref = enc_oneshot(&KEY, &NONCE, &ad, &pt); + + for &chunk in CHUNK_SIZES.iter() { + let ct = enc_chunked(&KEY, &NONCE, &ad, &pt, chunk); + assert_eq!(ct, ct_ref, "chunked encrypt mismatch (pt_len={pt_len}, chunk={chunk})"); + + let pt_back = dec_chunked(&KEY, &NONCE, &ad, &ct_ref, chunk) + .expect("chunked decrypt should pass"); + assert_eq!(pt_back, pt, "chunked decrypt mismatch (pt_len={pt_len}, chunk={chunk})"); + } + } +} + +#[test] +fn aead_chunked_aad_matches_one_shot() { + let pt = pattern(30); + let ad = pattern(40); + let ct_ref = enc_oneshot(&KEY, &NONCE, &ad, &pt); + let km = key_material(&KEY); + + for &chunk in CHUNK_SIZES.iter() { + let mut e = AsconAead128::new(&km, &NONCE, None, true).unwrap(); + for piece in ad.chunks(chunk) { + e.do_update_aad(piece).unwrap(); + } + let mut out = vec![0u8; pt.len() + 16]; + out[..pt.len()].copy_from_slice(&pt); + e.do_encrypt_update(&mut out[..pt.len()]); + let tag = e.do_encrypt_final(); + out[pt.len()..].copy_from_slice(&tag); + assert_eq!(out, ct_ref, "chunked AAD mismatch (chunk={chunk})"); + } +} + +/* -------------------------------------------------------------------------- */ +/* Trait-driven streaming sweep (this is what would have caught F1/F2) */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead_trait_streaming_sweep() { + use bouncycastle_core::traits::AEADCipher; + + let km = key_material(&KEY); + for pt_len in 0..=40 { + let pt = pattern(pt_len); + for ad_len in [0, 1, 15, 16, 17, 33] { + let ad = pattern(ad_len); + let ad_opt_ = ad_opt(&ad); + let ct_ref = enc_oneshot(&KEY, &NONCE, &ad, &pt); + let (ct_ref_body, tag_ref) = ct_ref.split_at(pt_len); + + for &chunk in [1, 2, 7, 15, 16, 17, 31, 32, 1024].iter() { + let mut e = AsconAead128::new(&km, &NONCE, ad_opt_, true).unwrap(); + let mut out = pt.clone(); + let chunk = chunk.max(1); + let mut off = 0; + while off < out.len() { + let end = (off + chunk).min(out.len()); + e.do_encrypt_update(&mut out[off..end]); + off = end; + } + let tag = e.do_aead_encrypt_final().unwrap(); + assert_eq!(out, ct_ref_body, "pt_len={pt_len} ad_len={ad_len} chunk={chunk}"); + assert_eq!(tag, tag_ref, "pt_len={pt_len} ad_len={ad_len} chunk={chunk}"); + + let mut d = AsconAead128::new(&km, &NONCE, ad_opt_, false).unwrap(); + let mut back = ct_ref_body.to_vec(); + let mut off = 0; + while off < back.len() { + let end = (off + chunk).min(back.len()); + d.do_decrypt_update(&mut back[off..end]); + off = end; + } + let tag_arr: [u8; 16] = tag_ref.try_into().unwrap(); + d.do_aead_decrypt_final(&tag_arr).unwrap(); + assert_eq!(back, pt, "pt_len={pt_len} ad_len={ad_len} chunk={chunk}"); + } + } + } +} + +#[test] +fn do_aead_decrypt_final_rejects_wrong_tag() { + use bouncycastle_core::traits::AEADCipher; + + let km = key_material(&KEY); + let pt = pattern(20); + let mut d = AsconAead128::new(&km, &NONCE, None, false).unwrap(); + let mut buf = pt.clone(); + d.do_decrypt_update(&mut buf); + let wrong_tag = [0xFFu8; 16]; + assert!(matches!( + d.do_aead_decrypt_final(&wrong_tag), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); +} + +/* -------------------------------------------------------------------------- */ +/* std-only Vec-returning trait wrappers */ +/* -------------------------------------------------------------------------- */ + +// `TestFrameworkAEADCipher` only exercises the `_out` (buffer-based) +// entry points, so the `#[cfg(feature = "std")]` `Vec`-returning wrappers (`encrypt`, `decrypt`, +// `aead_encrypt`, `aead_decrypt`) are otherwise never called by any test. +#[test] +fn aead128_std_vec_wrappers_round_trip() { + use bouncycastle_core::traits::AEADCipher; + + let km = key_material(&KEY); + let msg = pattern(40); + + let (nonce, ct) = >::encrypt(&km, &msg).unwrap(); + assert_eq!(ct.len(), msg.len() + 16); + let pt = >::decrypt(&km, nonce, &ct).unwrap(); + assert_eq!(pt, msg); + + let (nonce, ct, tag) = + >::aead_encrypt(&km, b"aad", &msg).unwrap(); + assert_eq!(ct.len(), msg.len()); + let pt = >::aead_decrypt(&km, &nonce, b"aad", &ct, &tag) + .unwrap(); + assert_eq!(pt, msg); + + // Tampering must still be rejected through these entry points too. + assert!( + >::aead_decrypt( + &km, &nonce, b"wrong-aad", &ct, &tag + ) + .is_err() + ); +} + +// None of the length checks in the `AEADCipher` `_out` entry points are ever +// triggered by `TestFrameworkAEADCipher` (which always pass a +// generously-sized fixed buffer), nor by the inherent one-shot `encrypt`/`decrypt` tests above +// (which always size their own buffer correctly). Exercise every one directly. +#[test] +fn aead128_undersized_buffers_are_rejected() { + use bouncycastle_core::traits::AEADCipher; + + let km = key_material(&KEY); + let msg = pattern(40); + + // AEADCipher::encrypt_out: ciphertext buffer shorter than plaintext.len() + 16. + let mut too_small = vec![0u8; msg.len() + 15]; + match >::encrypt_out(&km, &msg, &mut too_small) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, needed)) => { + assert_eq!(needed, msg.len() + 16); + } + other => panic!("expected IncorrectOutputBufferLength, got {other:?}"), + } + + // AEADCipher::decrypt / decrypt_out: ciphertext shorter than the 16-byte tag. + let short = [0u8; 8]; + match >::decrypt(&km, NONCE, &short) { + Err(SymmetricCipherError::GenericError(_)) => {} + other => panic!("expected GenericError, got {other:?}"), + } + let mut pt_buf = [0u8; 8]; + match >::decrypt_out(&km, NONCE, &short, &mut pt_buf) { + Err(SymmetricCipherError::GenericError(_)) => {} + other => panic!("expected GenericError, got {other:?}"), + } + + // AEADCipher::decrypt_out: valid-length ciphertext, but undersized plaintext buffer. + let ct = enc_oneshot(&KEY, &NONCE, &[], &msg); + let mut too_small_pt = vec![0u8; msg.len() - 1]; + match >::decrypt_out(&km, NONCE, &ct, &mut too_small_pt) + { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, needed)) => { + assert_eq!(needed, msg.len()); + } + other => panic!("expected IncorrectOutputBufferLength, got {other:?}"), + } + + // decrypt / decrypt_out: ciphertext of exactly 16 bytes (an empty plaintext plus the tag) is + // the boundary case and must NOT be rejected as "too short". + let empty_ct = enc_oneshot(&KEY, &NONCE, &[], &[]); + assert_eq!(empty_ct.len(), 16); + assert_eq!( + >::decrypt(&km, NONCE, &empty_ct).unwrap(), + Vec::::new() + ); + let mut empty_pt_buf = [0u8; 0]; + assert_eq!( + >::decrypt_out( + &km, NONCE, &empty_ct, &mut empty_pt_buf + ) + .unwrap(), + 0 + ); + + // decrypt_out: a plaintext buffer *larger* than needed must succeed, not be rejected. + let mut oversized_pt = vec![0xAAu8; msg.len() + 5]; + let n = + >::decrypt_out(&km, NONCE, &ct, &mut oversized_pt) + .unwrap(); + assert_eq!(n, msg.len()); + assert_eq!(&oversized_pt[..n], &msg[..]); + + // AEADCipher::aead_encrypt_out: ciphertext buffer shorter than the plaintext. + let mut too_small = vec![0u8; msg.len() - 1]; + match >::aead_encrypt_out( + &km, b"aad", &msg, &mut too_small, + ) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, needed)) => { + assert_eq!(needed, msg.len()); + } + other => panic!("expected IncorrectOutputBufferLength, got {other:?}"), + } + + // AEADCipher::aead_decrypt_out: plaintext buffer shorter than the ciphertext. + let (nonce, ct, tag) = + >::aead_encrypt(&km, b"aad", &msg).unwrap(); + let mut too_small_pt = vec![0u8; ct.len() - 1]; + match >::aead_decrypt_out( + &km, &nonce, b"aad", &ct, &tag, &mut too_small_pt, + ) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, needed)) => { + assert_eq!(needed, ct.len()); + } + other => panic!("expected IncorrectOutputBufferLength, got {other:?}"), + } +} + +// The plain (non-AEAD) view's `decrypt`/`decrypt_out` report an authentication failure as +// `DecryptionFailed`, not `AEADTagCheckFailed` (see the comment on `AsconAead128`'s +// `AEADCipher::decrypt_out` impl): this view has no separate tag to name, and the trait's own doc +// comment says every implementor reports it this way. A mutant deleting that remapping would +// otherwise survive, since nothing else in this file calls the plain view on a tampered +// ciphertext. +#[test] +fn aead128_plain_view_reports_tamper_as_decryption_failed() { + use bouncycastle_core::traits::AEADCipher; + + let km = key_material(&KEY); + let msg = pattern(40); + let ct = enc_oneshot(&KEY, &NONCE, &[], &msg); + + let mut tampered = ct.clone(); + tampered[0] ^= 0x01; + + match >::decrypt(&km, NONCE, &tampered) { + Err(SymmetricCipherError::DecryptionFailed) => {} + other => panic!("expected DecryptionFailed, got {other:?}"), + } + + let mut pt_buf = vec![0u8; msg.len()]; + match >::decrypt_out(&km, NONCE, &tampered, &mut pt_buf) + { + Err(SymmetricCipherError::DecryptionFailed) => {} + other => panic!("expected DecryptionFailed, got {other:?}"), + } +} + +/* -------------------------------------------------------------------------- */ +/* Authentication failures */ +/* -------------------------------------------------------------------------- */ + +fn assert_auth_failed(result: Result, SymmetricCipherError>, ctx: &str) { + match result { + Err(SymmetricCipherError::AEADTagCheckFailed) => {} + other => panic!("{ctx}: expected AEADTagCheckFailed, got {other:?}"), + } +} + +#[test] +fn aead_rejects_tampering() { + let pt = pattern(50); + let ad = b"the-aad"; + let ct = enc_oneshot(&KEY, &NONCE, ad, &pt); + + // Wrong key. + let mut bad_key = KEY; + bad_key[0] ^= 0x01; + assert_auth_failed(dec_oneshot(&bad_key, &NONCE, ad, &ct), "wrong key"); + + // Wrong nonce. + let mut bad_nonce = NONCE; + bad_nonce[3] ^= 0x80; + assert_auth_failed(dec_oneshot(&KEY, &bad_nonce, ad, &ct), "wrong nonce"); + + // Modified associated data. + assert_auth_failed(dec_oneshot(&KEY, &NONCE, b"the-AAD", &ct), "modified ad"); + + // Flipped tag byte (last byte). + let mut tag_flip = ct.clone(); + let last = tag_flip.len() - 1; + tag_flip[last] ^= 0x01; + assert_auth_failed(dec_oneshot(&KEY, &NONCE, ad, &tag_flip), "flipped tag"); + + // Flipped ciphertext body byte. + let mut body_flip = ct.clone(); + body_flip[0] ^= 0x01; + assert_auth_failed(dec_oneshot(&KEY, &NONCE, ad, &body_flip), "flipped body"); +} + +#[test] +fn aead_tamper_leaves_no_plaintext_in_output_buffer() { + let pt = pattern(20); + let ad = b"ctx"; + let ct = enc_oneshot(&KEY, &NONCE, ad, &pt); + let mut tampered = ct.clone(); + tampered[0] ^= 0x01; + + let km = key_material(&KEY); + let mut out = vec![0xAAu8; pt.len()]; + let n = AsconAead128::decrypt(&km, &NONCE, ad_opt(ad), &tampered, &mut out); + assert!(matches!(n, Err(SymmetricCipherError::AEADTagCheckFailed))); + assert!(out.iter().all(|&b| b == 0), "output buffer must be zeroized on tag failure"); +} + +#[test] +fn aead_short_ciphertext_is_error() { + let short = [0u8; 8]; // shorter than the 16-byte tag + let km = key_material(&KEY); + let mut out = [0u8; 16]; + match AsconAead128::decrypt(&km, &NONCE, None, &short, &mut out) { + Err(SymmetricCipherError::GenericError(_)) => {} + other => panic!("expected GenericError for short ciphertext, got {other:?}"), + } +} + +/* -------------------------------------------------------------------------- */ +/* Determinism / nonce sensitivity / Debug mask */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead_is_deterministic_and_nonce_sensitive() { + let pt = pattern(40); + let ad = b"ctx"; + let a = enc_oneshot(&KEY, &NONCE, ad, &pt); + let b = enc_oneshot(&KEY, &NONCE, ad, &pt); + assert_eq!(a, b, "same (key,nonce,ad,pt) must yield identical (ct,tag)"); + + let mut other_nonce = NONCE; + other_nonce[0] ^= 0x01; + let c = enc_oneshot(&KEY, &other_nonce, ad, &pt); + assert_ne!(a, c, "changing the nonce must change the ciphertext (SP 800-232 R3)"); +} + +#[test] +fn aead_debug_display_are_masked() { + let km = key_material(&KEY); + let e = AsconAead128::new(&km, &NONCE, None, true).unwrap(); + assert!(format!("{e:?}").contains("masked")); + assert!(format!("{e}").contains("masked")); +} + +/* -------------------------------------------------------------------------- */ +/* Direction-misuse guards */ +/* -------------------------------------------------------------------------- */ + +#[test] +#[should_panic(expected = "decryptor")] +fn do_encrypt_update_on_decryptor_panics() { + let km = key_material(&KEY); + let mut d = AsconAead128::new(&km, &NONCE, None, false).unwrap(); + let mut buf = [0u8; 4]; + d.do_encrypt_update(&mut buf); +} + +#[test] +#[should_panic(expected = "encryptor")] +fn do_decrypt_update_on_encryptor_panics() { + let km = key_material(&KEY); + let mut e = AsconAead128::new(&km, &NONCE, None, true).unwrap(); + let mut buf = [0u8; 4]; + e.do_decrypt_update(&mut buf); +} + +/* -------------------------------------------------------------------------- */ +/* AEADCipher trait conformance (shared core-test-framework) */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead128_trait_framework() { + // Exercises the generic AEADCipher<16,16,16> surface: internally + // generated (random, distinct) nonces, key-type / key-strength enforcement, and the AEAD + // tamper-detection contract (modified ciphertext / AAD / tag must fail the tag check, and + // must never leave plaintext in the output buffer). + TestFrameworkAEADCipher::new().test::<16, 16, 16, AsconAead128>(); +} + +/// Exercises [`AEADCipherEncryptor`]/[`AEADCipherDecryptor`], the streaming pair +/// [`AsconAead128Encryptor`]/[`AsconAead128Decryptor`] adapt [`AsconAead128`] to: `update_out_len` +/// correctness, chunking-independence of both AAD and data, the AAD-after-data `StateError`, and +/// tamper detection, all against the generic conformance suite rather than hand-written here. +/// +/// [`AEADCipherEncryptor`]: bouncycastle_core::traits::AEADCipherEncryptor +/// [`AEADCipherDecryptor`]: bouncycastle_core::traits::AEADCipherDecryptor +#[test] +fn aead128_encryptor_decryptor_trait_framework() { + TestFrameworkAEADCipher::new() + .test_encryptor_decryptor::<16, 16, 16, 0, AsconAead128Encryptor, AsconAead128Decryptor>(); +} + +/// The inline-tag adapter ([`TaggedEncryptor`]/[`TaggedDecryptor`]) over the same +/// [`AsconAead128Encryptor`]/[`AsconAead128Decryptor`] pair must pass the unrelated +/// [`SimpleCipherEncryptor`]/[`SimpleCipherDecryptor`] conformance suite -- proof that adapting an +/// AEAD to the `ciphertext || tag` layout costs nothing beyond appending the tag. +/// +/// [`TaggedEncryptor`]: bouncycastle_core::tagged_aead::TaggedEncryptor +/// [`TaggedDecryptor`]: bouncycastle_core::tagged_aead::TaggedDecryptor +/// [`SimpleCipherEncryptor`]: bouncycastle_core::traits::SimpleCipherEncryptor +/// [`SimpleCipherDecryptor`]: bouncycastle_core::traits::SimpleCipherDecryptor +#[test] +fn aead128_tagged_adapter_passes_simple_cipher_framework() { + use bouncycastle_core::tagged_aead::{TaggedDecryptor, TaggedEncryptor}; + + TestFrameworkSimpleCipher::new().test_encryptor_decryptor::< + 16, + 16, + 16, + TaggedEncryptor, + TaggedDecryptor, + >(); +} + +/// The two tag layouts must agree byte for byte: `direct_ciphertext || direct_tag`, produced by +/// streaming [`AsconAead128Encryptor`] directly, must equal what streaming through +/// [`TaggedEncryptor`] gives for the same key, nonce (driven by the same RNG stream), AAD and +/// message -- and the reverse must decrypt either back to the original plaintext. +/// +/// [`TaggedEncryptor`]: bouncycastle_core::tagged_aead::TaggedEncryptor +#[test] +fn aead128_tagged_and_direct_layouts_agree() { + use bouncycastle_core::tagged_aead::{TaggedDecryptor, TaggedEncryptor}; + use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SimpleCipherDecryptor, SimpleCipherEncryptor, + }; + use bouncycastle_core_test_framework::FixedSeedRNG; + + let km = key_material(&KEY); + let aad = b"tagged-adapter-aad"; + for pt_len in [0usize, 1, 15, 16, 17, 40] { + let pt = pattern(pt_len); + let pinned = [0x11u8; 16]; + + let (mut direct_enc, direct_nonce) = + AsconAead128Encryptor::do_encrypt_init_rng(&km, &mut FixedSeedRNG::<16>::new(pinned)) + .unwrap(); + direct_enc.do_update_aad(aad).unwrap(); + let mut direct_ct = vec![0u8; pt.len()]; + direct_enc.do_update_out(&pt, &mut direct_ct).unwrap(); + let mut nothing = [0u8; 0]; + let (_flushed, direct_tag) = direct_enc.do_encrypt_final(&mut nothing).unwrap(); + let mut direct_inline = direct_ct.clone(); + direct_inline.extend_from_slice(&direct_tag); + + let (mut tagged_enc, tagged_nonce) = + as SimpleCipherEncryptor<16, 16, 16>>::do_encrypt_init_rng( + &km, + &mut FixedSeedRNG::<16>::new(pinned), + ) + .unwrap(); + tagged_enc.do_update_aad::<16, 16, 16>(aad).unwrap(); + let mut tagged_out = vec![0u8; pt.len() + 16]; + let written = tagged_enc.do_update_out(&pt, &mut tagged_out).unwrap(); + let mut last = [0u8; 16]; + let last_len = as SimpleCipherEncryptor< + 16, + 16, + 16, + >>::do_final_out(tagged_enc, &mut last) + .unwrap(); + tagged_out[written..written + last_len].copy_from_slice(&last[..last_len]); + tagged_out.truncate(written + last_len); + + assert_eq!(direct_nonce, tagged_nonce, "pt_len {pt_len}: same RNG stream, same nonce"); + assert_eq!(direct_inline, tagged_out, "pt_len {pt_len}: inline layout must agree"); + + // ...and both decrypt back to the original plaintext, each through its own view. + let mut direct_dec = AsconAead128Decryptor::do_decrypt_init(&km, &direct_nonce).unwrap(); + direct_dec.do_update_aad(aad).unwrap(); + let mut direct_pt = vec![0u8; direct_ct.len()]; + direct_dec.do_update_out(&direct_ct, &mut direct_pt).unwrap(); + let tag_arr: [u8; 16] = direct_tag; + direct_dec.do_decrypt_final(&tag_arr, &mut nothing).unwrap(); + assert_eq!(direct_pt, pt, "pt_len {pt_len}: direct decrypt round trip"); + + let mut tagged_dec = as SimpleCipherDecryptor< + 16, + 16, + 16, + >>::do_decrypt_init(&km, &tagged_nonce) + .unwrap(); + tagged_dec.do_update_aad::<16, 16>(aad).unwrap(); + let mut tagged_pt = vec![0u8; tagged_out.len()]; + let written = tagged_dec.do_update_out(&tagged_out, &mut tagged_pt).unwrap(); + let (_, final_data_len) = tagged_dec.do_final().unwrap(); + tagged_pt.truncate(written + final_data_len); + assert_eq!(tagged_pt, pt, "pt_len {pt_len}: tagged decrypt round trip"); + } +} + +#[test] +fn aead128_suspendable_keyed_state() { + use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core::traits::SuspendableKeyed; + use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableKeyedState; + + let pt = pattern(40); + let ad = b"suspend-ad"; + let ct_ref = enc_oneshot(&KEY, &NONCE, ad, &pt); + let km = key_material(&KEY); + + // Encrypt part of the plaintext, suspend, resume with the re-supplied key, finish, and confirm + // the output matches a one-shot encryption. The key is never part of the serialized state. + let mut e = AsconAead128::new(&km, &NONCE, Some(ad), true).unwrap(); + let mut out = vec![0u8; pt.len() + 16]; + out[..pt.len()].copy_from_slice(&pt); + e.do_encrypt_update(&mut out[..18]); + + TestFrameworkSuspendableKeyedState::new().test(&e, &km); + + let serialized = e.clone().suspend(); + let mut resumed = AsconAead128::from_suspended(serialized, &km).unwrap(); + resumed.do_encrypt_update(&mut out[18..pt.len()]); + let tag = resumed.do_encrypt_final(); + out[pt.len()..].copy_from_slice(&tag); + assert_eq!(out, ct_ref, "resumed AEAD ciphertext must match one-shot encryption"); + + // A corrupted state tag must be rejected (the tag is the byte after the 3-byte version prefix). + let mut busted = serialized; + busted[3] ^= 0xFF; + assert!(matches!( + AsconAead128::from_suspended(busted, &km), + Err(SuspendableError::InvalidData) + )); + + // An unknown call-state discriminant must be rejected. + let last = serialized.len() - 1; + let pos_offset = serialized.len() - 2; + let mut bad_state = serialized; + bad_state[last] = 200; + assert!(matches!( + AsconAead128::from_suspended(bad_state, &km), + Err(SuspendableError::InvalidData) + )); + + // A nonzero byte position while still in an *Init state must be rejected. + let mut inconsistent = serialized; + inconsistent[pos_offset] = 3; // pos = 3 + inconsistent[last] = 0; // EncInit + assert!(matches!( + AsconAead128::from_suspended(inconsistent, &km), + Err(SuspendableError::InvalidData) + )); + + // pos >= RATE (16) must be rejected. + let mut bad_pos = serialized; + bad_pos[pos_offset] = 16; + assert!(matches!( + AsconAead128::from_suspended(bad_pos, &km), + Err(SuspendableError::InvalidData) + )); +} diff --git a/crypto/ascon/tests/bc_test_data.rs b/crypto/ascon/tests/bc_test_data.rs new file mode 100644 index 00000000..01525a94 --- /dev/null +++ b/crypto/ascon/tests/bc_test_data.rs @@ -0,0 +1,242 @@ +//! 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 (or "../../../bc-test-data" relative +//! to this crate). When the repo is absent these tests print a warning and are skipped. +//! +//! The NIST SP 800-232 ASCON known-answer test (KAT) vectors live under +//! `bc-test-data/crypto/ascon//`. These full sweeps (1025–1089 cases each) complement the +//! small embedded vector sets in the per-primitive test files. + +#[cfg(test)] +mod bc_test_data { + use bouncycastle_ascon::ascon_aead128::AsconAead128; + use bouncycastle_ascon::ascon_cxof128::AsconCXof128; + use bouncycastle_ascon::ascon_hash256::AsconHash256; + use bouncycastle_ascon::ascon_xof128::AsconXof128; + use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, + }; + use bouncycastle_core::traits::{SecurityStrength, XOF}; + use bouncycastle_hex as hex; + use std::collections::BTreeMap; + use std::fs; + use std::path::Path; + use std::sync::Once; + + const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/crypto/ascon"; + const TEST_DATA_PATH: &str = "../bc-test-data/crypto/ascon"; + + static TEST_DATA_CHECK: Once = Once::new(); + + fn get_test_data(filename: &str) -> Result { + let found: u8; + if Path::new(TEST_DATA_PATH_RELATIVE).exists() { + found = 1; + } else if Path::new(TEST_DATA_PATH).exists() { + found = 2; + } else { + found = 3; + }; + + // just print once + TEST_DATA_CHECK.call_once(|| match found { + 1 => println!("bc-test-data found at: {:?}", TEST_DATA_PATH_RELATIVE), + 2 => println!("bc-test-data found at: {:?}", TEST_DATA_PATH), + _ => println!("WARNING: bc-test-data directory not found; tests will be skipped"), + }); + + let contents = if Path::new(TEST_DATA_PATH_RELATIVE).exists() { + fs::read_to_string(TEST_DATA_PATH_RELATIVE.to_string() + "/" + filename).unwrap() + } else if Path::new(TEST_DATA_PATH).exists() { + fs::read_to_string(TEST_DATA_PATH.to_string() + "/" + filename).unwrap() + } else { + return Err(()); + }; + + Ok(contents) + } + + fn decode_hex(value: &str) -> Vec { + let clean = value.trim(); + if clean.is_empty() { Vec::new() } else { hex::decode(clean).expect("valid hex") } + } + + /// Parse a NIST LWC KAT file: blank-line-delimited `Tag = Value` cases. + fn parse_kat(contents: &str) -> Vec> { + let mut cases = Vec::new(); + let mut current = BTreeMap::new(); + + for raw in contents.lines() { + let line = raw.trim(); + if line.is_empty() { + if !current.is_empty() { + cases.push(std::mem::take(&mut current)); + } + continue; + } + if line.starts_with('#') { + continue; + } + if let Some((key, value)) = line.split_once('=') { + let key = key.trim().to_string(); + let value = value.trim().to_string(); + if key == "Count" && !current.is_empty() { + cases.push(std::mem::take(&mut current)); + } + current.insert(key, value); + } + } + if !current.is_empty() { + cases.push(current); + } + cases + } + + fn field<'a>(case: &'a BTreeMap, names: &[&str]) -> &'a str { + for name in names { + if let Some(v) = case.get(*name) { + return v.as_str(); + } + } + panic!("missing field {names:?}; case had {:?}", case.keys().collect::>()); + } + + fn to_16(bytes: &[u8], what: &str) -> [u8; 16] { + bytes.try_into().unwrap_or_else(|_| panic!("{what} must be 16 bytes, got {}", bytes.len())) + } + + /// Build a `KeyMaterial<16>` for a KAT key. The NIST LWC vectors include an all-zero key + /// (Count=1), which `KeyMaterial::from_bytes_as_type` would otherwise tag + /// `KeyType::Zeroized` / `SecurityStrength::None`; force the type/strength the way a caller + /// who knows the provenance of the key would (see `cli/src/helpers.rs::parse_seed`). + fn key_material(key: &[u8; 16]) -> KeyMaterial<16> { + let mut km = + KeyMaterial::<16>::from_bytes_as_type(key, KeyType::SymmetricCipherKey).unwrap(); + do_hazardous_operations(&mut km, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::_128bit) + }) + .unwrap(); + km + } + + #[test] + fn ascon_aead128_kat() { + let contents = match get_test_data("asconaead128/LWC_AEAD_KAT_128_128.txt") { + Ok(c) => c, + Err(()) => return, + }; + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no AEAD cases parsed"); + + for case in &cases { + let key = key_material(&to_16(&decode_hex(field(case, &["Key", "K"])), "key")); + let nonce = to_16(&decode_hex(field(case, &["Nonce", "N"])), "nonce"); + let ad = decode_hex(field(case, &["AD", "A"])); + let pt = decode_hex(field(case, &["PT", "P"])); + let expected_ct = decode_hex(field(case, &["CT", "C"])); + let ad_opt = if ad.is_empty() { None } else { Some(ad.as_slice()) }; + + // One-shot encrypt. + let mut ct = vec![0u8; pt.len() + 16]; + let n = AsconAead128::encrypt(&key, &nonce, ad_opt, &pt, &mut ct).unwrap(); + ct.truncate(n); + assert_eq!(ct, expected_ct, "encrypt mismatch (Count {})", field(case, &["Count"])); + + // One-shot decrypt round-trip. + let mut pt_out = vec![0u8; expected_ct.len()]; + let m = AsconAead128::decrypt(&key, &nonce, ad_opt, &expected_ct, &mut pt_out) + .expect("decrypt should authenticate"); + pt_out.truncate(m); + assert_eq!(pt_out, pt, "decrypt mismatch (Count {})", field(case, &["Count"])); + + // Byte-at-a-time streaming encrypt/decrypt, through the inherent API. + let mut enc = AsconAead128::new(&key, &nonce, ad_opt, true).unwrap(); + let mut stream_ct = pt.clone(); + for byte in stream_ct.iter_mut() { + enc.do_encrypt_update(core::slice::from_mut(byte)); + } + let tag = enc.do_encrypt_final(); + stream_ct.extend_from_slice(&tag); + assert_eq!( + stream_ct, + expected_ct, + "streaming encrypt mismatch (Count {})", + field(case, &["Count"]) + ); + + let mut dec = AsconAead128::new(&key, &nonce, ad_opt, false).unwrap(); + let mut stream_pt = expected_ct[..pt.len()].to_vec(); + for byte in stream_pt.iter_mut() { + dec.do_decrypt_update(core::slice::from_mut(byte)); + } + dec.do_decrypt_final(&tag).expect("streaming decrypt should authenticate"); + assert_eq!( + stream_pt, + pt, + "streaming decrypt mismatch (Count {})", + field(case, &["Count"]) + ); + } + println!("Ascon-AEAD128: {} KAT cases passed", cases.len()); + } + + #[test] + fn ascon_hash256_kat() { + let contents = match get_test_data("asconhash256/LWC_HASH_KAT_256.txt") { + Ok(c) => c, + Err(()) => return, + }; + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no Hash256 cases parsed"); + + for case in &cases { + let msg = decode_hex(field(case, &["Msg"])); + let expected = decode_hex(field(case, &["MD"])); + assert_eq!( + AsconHash256::digest(&msg).as_slice(), + expected.as_slice(), + "Hash256 mismatch (Count {})", + field(case, &["Count"]) + ); + } + println!("Ascon-Hash256: {} KAT cases passed", cases.len()); + } + + #[test] + fn ascon_xof128_kat() { + let contents = match get_test_data("asconxof128/LWC_XOF_KAT_128_512.txt") { + Ok(c) => c, + Err(()) => return, + }; + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no XOF128 cases parsed"); + + for case in &cases { + let msg = decode_hex(field(case, &["Msg"])); + let expected = decode_hex(field(case, &["MD", "Output"])); + let got = AsconXof128::new().hash_xof(&msg, expected.len()); + assert_eq!(got, expected, "XOF128 mismatch (Count {})", field(case, &["Count"])); + } + println!("Ascon-XOF128: {} KAT cases passed", cases.len()); + } + + #[test] + fn ascon_cxof128_kat() { + let contents = match get_test_data("asconcxof128/LWC_CXOF_KAT_128_512.txt") { + Ok(c) => c, + Err(()) => return, + }; + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no CXOF128 cases parsed"); + + for case in &cases { + let msg = decode_hex(field(case, &["Msg"])); + let z = decode_hex(field(case, &["Z", "Customization"])); + let expected = decode_hex(field(case, &["MD", "Output"])); + let got = AsconCXof128::with_customization(&z).unwrap().hash_xof(&msg, expected.len()); + assert_eq!(got, expected, "CXOF128 mismatch (Count {})", field(case, &["Count"])); + } + println!("Ascon-CXOF128: {} KAT cases passed", cases.len()); + } +} diff --git a/crypto/ascon/tests/cxof128_tests.rs b/crypto/ascon/tests/cxof128_tests.rs new file mode 100644 index 00000000..5478ba58 --- /dev/null +++ b/crypto/ascon/tests/cxof128_tests.rs @@ -0,0 +1,221 @@ +//! Ascon-CXOF128 tests (NIST SP 800-232 §5.3). +//! +//! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus +//! domain-separation, streaming/byte-at-a-time equivalence, trait-API, and misuse-guard tests. + +use bouncycastle_ascon::ascon_cxof128::AsconCXof128; +use bouncycastle_ascon::ascon_xof128::AsconXof128; +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::XOF; +use bouncycastle_core_test_framework::xof::TestFrameworkXOF; +use bouncycastle_hex as hex; + +/// Embedded NIST LWC Ascon-CXOF128 vectors `(message, customization Z, 512-bit output)` in hex, +/// spanning empty/non-empty customization and message. (Counts 1, 2, 3, 35, 36 of +/// LWC_CXOF_KAT_128_512.txt; each output is 64 bytes.) +const CXOF_KAT: &[(&str, &str, &str)] = &[ + ( + "", + "", + "4F50159EF70BB3DAD8807E034EAEBD44C4FA2CBBC8CF1F05511AB66CDCC529905CA12083FC186AD899B270B1473DC5F7EC88D1052082DCDFE69FB75D269E7B74", + ), + ( + "", + "10", + "0C93A483E7D574D49FE52CCE03EE646117977D57A8AA57704AB4DAF44B501430FF6AC11A5D1FD6F2154B5C65728268270C8BB578508487B8965718ADA6272FD6", + ), + ( + "", + "1011", + "D1106C7622E79FE955BD9D79E03B918E770FE0E0CDDDE28BEB924B02C5FC936B33ACCA299C89ECA5D71886CBBFA4D54A21C55FDE2B679F5E2488063A1719DC32", + ), + ( + "00", + "10", + "63FA8BA86382F2D544580F51322D080424B42C556EB74503CD73CF052BB993BD6F5210984C71C9C445F43CCC5B158226E509BD339CD634414377F79411AA8D5C", + ), + ( + "00", + "1011", + "DF7909DD1F371E54ABBABB50DDEE195720D7EF1BB2CF2271C36A76C19908178BA3255E5A3D31D994C1D217A67AE4D13681AC1ABC4FAA2ECDD1681520BC7D7347", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +#[test] +fn cxof128_embedded_kat() { + for (msg_hex, z_hex, md_hex) in CXOF_KAT { + let msg = dh(msg_hex); + let z = dh(z_hex); + let expected = dh(md_hex); + let got = AsconCXof128::with_customization(&z).unwrap().hash_xof(&msg, expected.len()); + assert_eq!(got, expected, "msg={msg_hex} z={z_hex}"); + + // `AsconCXof128::default()` uses an empty customization string, so the generic XOF + // framework (which constructs via `Default`) only applies to the empty-Z vectors; the + // non-empty-Z vectors are covered by `cxof128_prefix_property_and_streaming` below. + if z.is_empty() { + // AsconCXof128 has no absorb_last_partial_byte / squeeze_partial_byte_final support, so + // that part of the framework is disabled; everything else (hash_xof, streaming, prefix + // property, chunked absorb, absorb-after-squeeze) is exercised here. + TestFrameworkXOF { enable_partial_byte_tests: false } + .test_xof::(&msg, &expected); + } + } +} + +#[test] +fn cxof128_domain_separation() { + let msg = pattern(48); + + let out_z1 = AsconCXof128::with_customization(b"context-1").unwrap().hash_xof(&msg, 64); + let out_z2 = AsconCXof128::with_customization(b"context-2").unwrap().hash_xof(&msg, 64); + assert_ne!(out_z1, out_z2, "different customization strings must give different output"); + + // Empty-customization CXOF128 must differ from XOF128 (different IV). + let cxof_empty = AsconCXof128::new().hash_xof(&msg, 64); + let xof = AsconXof128::new().hash_xof(&msg, 64); + assert_ne!(cxof_empty, xof, "CXOF128 (empty Z) must differ from XOF128"); +} + +#[test] +fn cxof128_prefix_property_and_streaming() { + let z = b"cust"; + let msg = pattern(70); + let full = AsconCXof128::with_customization(z).unwrap().hash_xof(&msg, 100); + + // Squeezing in several calls yields the same stream (prefix property). + let mut x = AsconCXof128::with_customization(z).unwrap(); + x.absorb(&msg).unwrap(); + let mut piecewise = Vec::new(); + for n in [30usize, 40, 30] { + let mut part = vec![0u8; n]; + x.squeeze_out(&mut part); + piecewise.extend_from_slice(&part); + } + assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); + + // Absorbing in chunks equals one-shot absorb. + for chunk in [1usize, 8, 9, 64] { + let mut xc = AsconCXof128::with_customization(z).unwrap(); + for piece in msg.chunks(chunk) { + xc.absorb(piece).unwrap(); + } + let mut got = vec![0u8; 100]; + xc.squeeze_out(&mut got); + assert_eq!(got, full, "chunked absorb mismatch (chunk={chunk})"); + } +} + +#[test] +fn cxof128_byte_at_a_time_matches_one_shot() { + let msg = pattern(40); // > 8 bytes so byte-at-a-time absorb triggers full-block absorption + let cref = AsconCXof128::with_customization(b"zz").unwrap().hash_xof(&msg, 48); + let mut c = AsconCXof128::with_customization(b"zz").unwrap(); + for &b in &msg { + c.absorb(&[b]).unwrap(); + } + let mut o = [0u8; 48]; + c.squeeze_out(&mut o); + assert_eq!(o.to_vec(), cref, "CXOF128 byte-at-a-time absorb mismatch"); +} + +#[test] +fn cxof128_unsupported_partial_ops_return_err() { + let mut c = AsconCXof128::new(); + assert!(c.absorb_last_partial_byte(0, 3).is_err()); + assert!(AsconCXof128::new().squeeze_partial_byte_final(3).is_err()); + let mut b = 0u8; + assert!(AsconCXof128::new().squeeze_partial_byte_final_out(3, &mut b).is_err()); +} + +#[test] +fn cxof128_absorb_after_squeeze_errors() { + let mut x = AsconCXof128::with_customization(b"z").unwrap(); + x.absorb(b"data").unwrap(); + let mut out = [0u8; 8]; + x.squeeze_out(&mut out); + // Absorbing after squeezing has begun is reported as an error rather than a panic. + assert!(matches!(x.absorb(b"more"), Err(HashError::InvalidState(_)))); +} + +#[test] +fn cxof128_suspendable_state() { + use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core::traits::Suspendable; + use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableState; + + let z = b"customization"; + let data: Vec = (0..30u8).collect(); + + // Reference: uninterrupted absorb + squeeze under the same customization string. + let mut r = AsconCXof128::with_customization(z).unwrap(); + r.absorb(&data).unwrap(); + let mut expected = [0u8; 40]; + r.squeeze_out(&mut expected); + + // Suspend mid-absorb, resume, finish, and confirm the squeezed output matches. (The + // customization string was already absorbed at construction and is not part of the state.) + let mut x = AsconCXof128::with_customization(z).unwrap(); + x.absorb(&data[..5]).unwrap(); + TestFrameworkSuspendableState::new().test(&x); + + let serialized = x.clone().suspend(); + let mut resumed = AsconCXof128::from_suspended(serialized).unwrap(); + resumed.absorb(&data[5..]).unwrap(); + let mut out = [0u8; 40]; + resumed.squeeze_out(&mut out); + assert_eq!(out, expected, "resumed CXOF output must match uninterrupted output"); + + // A corrupted state tag must be rejected. + let mut busted = serialized; + busted[3] ^= 0xFF; + assert!(matches!(AsconCXof128::from_suspended(busted), Err(SuspendableError::InvalidData))); + + // Cross-type guard: an Ascon-XOF128 state (same serialized length) must be rejected by + // Ascon-CXOF128 via the state tag. + let mut xof = AsconXof128::new(); + xof.absorb(&data).unwrap(); + let xof_state = xof.suspend(); + assert!(matches!(AsconCXof128::from_suspended(xof_state), Err(SuspendableError::InvalidData))); + + // An inconsistent buf_pos/squeezing combination must be rejected: buf_pos == RATE (8) is only + // valid once squeezing has begun. + let mut bad = serialized; + let len = bad.len(); + bad[len - 2] = 8; // buf_pos = RATE + bad[len - 1] = 0; // squeezing = false + assert!(matches!(AsconCXof128::from_suspended(bad), Err(SuspendableError::InvalidData))); + + // Suspend mid-squeeze (not just mid-absorb) and confirm resuming continues the same stream. + let mut sq = AsconCXof128::with_customization(z).unwrap(); + sq.absorb(&data).unwrap(); + let mut head = [0u8; 5]; + sq.squeeze_out(&mut head); + let squeezing_state = sq.clone().suspend(); + let mut resumed_sq = AsconCXof128::from_suspended(squeezing_state).unwrap(); + let mut tail = [0u8; 35]; + resumed_sq.squeeze_out(&mut tail); + let mut combined = Vec::new(); + combined.extend_from_slice(&head); + combined.extend_from_slice(&tail); + assert_eq!(combined, expected, "resuming mid-squeeze must continue the same output stream"); +} + +#[test] +fn cxof128_customization_length_bound() { + // SP 800-232 §5.3: the customization string shall be at most 2048 bits (256 bytes). + let ok = vec![0u8; 256]; + assert!(AsconCXof128::with_customization(&ok).is_ok()); + + let too_long = vec![0u8; 257]; + assert!(matches!(AsconCXof128::with_customization(&too_long), Err(HashError::InvalidInput(_)))); +} diff --git a/crypto/ascon/tests/hash256_tests.rs b/crypto/ascon/tests/hash256_tests.rs new file mode 100644 index 00000000..8e6ee545 --- /dev/null +++ b/crypto/ascon/tests/hash256_tests.rs @@ -0,0 +1,152 @@ +//! Ascon-Hash256 tests (NIST SP 800-232 §5.1). +//! +//! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus +//! streaming-equivalence, one-shot/trait-API, metadata, and unsupported-partial-op tests. + +use bouncycastle_ascon::ascon_hash256::AsconHash256; +use bouncycastle_core::traits::{Hash, HashAlgParams}; +use bouncycastle_core_test_framework::hash::TestFrameworkHash; +use bouncycastle_hex as hex; + +/// Embedded NIST LWC Ascon-Hash256 vectors `(message, digest)` in hex, spanning empty, sub-block, +/// exact-block, and multi-block messages. (Counts 1, 2, 9, 17, 33 of LWC_HASH_KAT_256.txt.) +const HASH_KAT: &[(&str, &str)] = &[ + ("", "0B3BE5850F2F6B98CAF29F8FDEA89B64A1FA70AA249B8F839BD53BAA304D92B2"), + ("00", "0728621035AF3ED2BCA03BF6FDE900F9456F5330E4B5EE23E7F6A1E70291BC80"), + ("0001020304050607", "B88E497AE8E6FB641B87EF622EB8F2FCA0ED95383F7FFEBE167ACF1099BA764F"), + ( + "000102030405060708090A0B0C0D0E0F", + "3158C1940A2FBADBD68AB661777859B94A689E4EFC375911467ADDD641835C38", + ), + ( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "BD9D3D60A66B53868EAB2A5C74539A518A1F60F01EB176C60E43DEE81680B33E", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +#[test] +fn hash256_embedded_kat() { + for (msg_hex, md_hex) in HASH_KAT { + let msg = dh(msg_hex); + let expected = dh(md_hex); + assert_eq!(AsconHash256::digest(&msg).as_slice(), expected.as_slice(), "msg={msg_hex}"); + + // AsconHash256 has no do_final_partial_bits support, so that part of the framework + // is disabled; everything else (hash/hash_out/do_update+do_final(_out), truncation, + // oversized-buffer zero-fill) is exercised here. + TestFrameworkHash { enable_partial_byte_tests: false } + .test_hash::(&msg, &expected); + } +} + +#[test] +fn hash256_streaming_matches_one_shot() { + let msg = pattern(100); + let expected = AsconHash256::digest(&msg); + + // One-shot APIs agree. + assert_eq!(AsconHash256::new().hash(&msg), expected.to_vec()); + let mut buf = [0u8; 32]; + let mut h = AsconHash256::new(); + h.do_update(&msg); + h.do_final_out(&mut buf); + assert_eq!(buf, expected); + + // Chunked do_update agrees for a range of chunk sizes. + for chunk in [1usize, 7, 8, 9, 16, 33] { + let mut hasher = AsconHash256::new(); + for piece in msg.chunks(chunk) { + hasher.do_update(piece); + } + let mut got = [0u8; 32]; + hasher.do_final_out(&mut got); + assert_eq!(got, expected, "chunked hash mismatch (chunk={chunk})"); + } + + // Byte-at-a-time do_update() agrees. + let mut hasher = AsconHash256::new(); + for &b in &msg { + hasher.do_update(&[b]); + } + let mut got = [0u8; 32]; + hasher.do_final_out(&mut got); + assert_eq!(got, expected, "byte-at-a-time hash mismatch"); +} + +#[test] +fn hash256_metadata_accessors() { + assert_eq!(AsconHash256::OUTPUT_LEN, 32); + let h = AsconHash256::new(); + assert_eq!(h.output_len(), 32); + assert_eq!(h.block_bitlen(), 64); +} + +#[test] +fn hash256_do_final_out_truncates_to_buffer() { + let msg = pattern(50); + let expected = AsconHash256::digest(&msg); + + let mut h = AsconHash256::new(); + h.do_update(&msg); + let mut o = [0u8; 16]; + assert_eq!(h.do_final_out(&mut o), 16); + assert_eq!(o, expected[..16]); +} + +#[test] +fn hash256_hash_out_zeroizes_past_output_len() { + let msg = pattern(50); + let expected = AsconHash256::digest(&msg); + + let mut o = [0xEEu8; 64]; + assert_eq!(AsconHash256::new().hash_out(&msg, &mut o), 32); + assert_eq!(&o[..32], &expected[..]); + assert_eq!(&o[32..], &[0u8; 32]); +} + +#[test] +fn hash256_unsupported_partial_ops_return_err() { + assert!(AsconHash256::new().do_final_partial_bits(0, 3).is_err()); + let mut o = [0u8; 32]; + assert!(AsconHash256::new().do_final_partial_bits_out(0, 3, &mut o).is_err()); +} + +#[test] +fn hash256_suspendable_state() { + use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core::traits::Suspendable; + use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableState; + + let data: Vec = (0..37u8).collect(); + let expected = AsconHash256::digest(&data).to_vec(); + + // Suspend mid-absorb, resume, finish, and confirm the digest matches an uninterrupted run. + let mut h = AsconHash256::new(); + h.do_update(&data[..7]); + TestFrameworkSuspendableState::new().test(&h); + + let serialized = h.clone().suspend(); + let mut resumed = AsconHash256::from_suspended(serialized).unwrap(); + resumed.do_update(&data[7..]); + assert_eq!(resumed.do_final(), expected, "resumed digest must match uninterrupted digest"); + + // A corrupted state tag must be rejected (the tag is the byte after the 3-byte version prefix). + let mut busted = serialized; + busted[3] ^= 0xFF; + assert!(matches!(AsconHash256::from_suspended(busted), Err(SuspendableError::InvalidData))); + + // An out-of-range buffer position must be rejected (buf_pos is the final byte). + let mut bad_pos = serialized; + let last = bad_pos.len() - 1; + bad_pos[last] = 99; // >= RATE (8) + assert!(matches!(AsconHash256::from_suspended(bad_pos), Err(SuspendableError::InvalidData))); +} diff --git a/crypto/ascon/tests/xof128_tests.rs b/crypto/ascon/tests/xof128_tests.rs new file mode 100644 index 00000000..22ed9c0a --- /dev/null +++ b/crypto/ascon/tests/xof128_tests.rs @@ -0,0 +1,183 @@ +//! Ascon-XOF128 tests (NIST SP 800-232 §5.2). +//! +//! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus the +//! prefix property, streaming/byte-at-a-time equivalence, trait-API, and misuse-guard tests. + +use bouncycastle_ascon::ascon_xof128::AsconXof128; +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::XOF; +use bouncycastle_core_test_framework::xof::TestFrameworkXOF; +use bouncycastle_hex as hex; + +/// Embedded NIST LWC Ascon-XOF128 vectors `(message, 512-bit output)` in hex, spanning empty, +/// sub-block, exact-block, and multi-block messages. (Counts 1, 2, 9, 17, 33 of +/// LWC_XOF_KAT_128_512.txt; each output is 64 bytes.) +const XOF_KAT: &[(&str, &str)] = &[ + ( + "", + "473D5E6164F58B39DFD84AACDB8AE42EC2D91FED33388EE0D960D9B3993295C6AD77855A5D3B13FE6AD9E6098988373AF7D0956D05A8F1665D2C67D1A3AD10FF", + ), + ( + "00", + "51430E0438ECDF642B393630D977625F5F337656BA58AB1E960784AC32A16E0D446405551F5469384F8EA283CF12E64FA72C426BFEBAEA3AA1529E2C4AB23A2F", + ), + ( + "0001020304050607", + "8D1886F5D3EC4AF8D15B44BC62B74DA6EA91BC28FB82F9C34079B5ED6E38B6C951803D7DFB3C5E512A0EF5E4060062A6FD067F9C73EF9BEE527411BDA67FC896", + ), + ( + "000102030405060708090A0B0C0D0E0F", + "10BFEDC5F6442D3E1D8C324878CE1DDF73B01CAFC365589283AC4CBB98E48DE3CEDA8A41BB0983D539E4D90F6458C5C781724FAD641ED3CDB4779931097440B3", + ), + ( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "2E5F3403F4171471CC7934B51982CECE8D6628435DB70E89880F3BE4E0B7B05232DFE63C44A836D771337C9C5A2688D1B71ECABE0D5C2006FEF36EF3186138AD", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +#[test] +fn xof128_embedded_kat() { + for (msg_hex, md_hex) in XOF_KAT { + let msg = dh(msg_hex); + let expected = dh(md_hex); + let got = AsconXof128::new().hash_xof(&msg, expected.len()); + assert_eq!(got, expected, "msg={msg_hex}"); + // AsconXof128 has no absorb_last_partial_byte / squeeze_partial_byte_final support, so that + // part of the framework is disabled; everything else (hash_xof, streaming, prefix property, + // chunked absorb, absorb-after-squeeze) is exercised here. + TestFrameworkXOF { enable_partial_byte_tests: false } + .test_xof::(&msg, &expected); + } +} + +#[test] +fn xof128_prefix_property_and_streaming() { + let msg = pattern(70); + let full = AsconXof128::new().hash_xof(&msg, 100); + + // Squeezing in several calls yields the same stream (prefix property). + let mut x = AsconXof128::new(); + x.absorb(&msg).unwrap(); + let mut piecewise = Vec::new(); + for n in [30usize, 40, 30] { + let mut part = vec![0u8; n]; + x.squeeze_out(&mut part); + piecewise.extend_from_slice(&part); + } + assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); + + // Absorbing in chunks equals one-shot absorb. + for chunk in [1usize, 8, 9, 64] { + let mut xc = AsconXof128::new(); + for piece in msg.chunks(chunk) { + xc.absorb(piece).unwrap(); + } + let mut got = vec![0u8; 100]; + xc.squeeze_out(&mut got); + assert_eq!(got, full, "chunked absorb mismatch (chunk={chunk})"); + } +} + +#[test] +fn xof128_byte_at_a_time_matches_one_shot() { + let msg = pattern(40); // > 8 bytes so byte-at-a-time absorb triggers full-block absorption + let xref = AsconXof128::new().hash_xof(&msg, 48); + let mut x = AsconXof128::new(); + for &b in &msg { + x.absorb(&[b]).unwrap(); + } + let mut o = [0u8; 48]; + x.squeeze_out(&mut o); + assert_eq!(o.to_vec(), xref, "XOF128 byte-at-a-time absorb mismatch"); +} + +#[test] +fn xof128_unsupported_partial_ops_return_err() { + let mut x = AsconXof128::new(); + assert!(x.absorb_last_partial_byte(0, 3).is_err()); + assert!(AsconXof128::new().squeeze_partial_byte_final(3).is_err()); + let mut b = 0u8; + assert!(AsconXof128::new().squeeze_partial_byte_final_out(3, &mut b).is_err()); +} + +#[test] +fn xof128_absorb_after_squeeze_errors() { + let mut x = AsconXof128::new(); + x.absorb(b"data").unwrap(); + let mut out = [0u8; 8]; + x.squeeze_out(&mut out); + // Absorbing after squeezing has begun is a usage error; the trait API reports it as an error + // rather than panicking. + assert!(matches!(x.absorb(b"more"), Err(HashError::InvalidState(_)))); +} + +#[test] +fn xof128_suspendable_state() { + use bouncycastle_ascon::ascon_cxof128::AsconCXof128; + use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core::traits::Suspendable; + use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableState; + + let data: Vec = (0..30u8).collect(); + + // Reference: uninterrupted absorb + squeeze. + let mut r = AsconXof128::new(); + r.absorb(&data).unwrap(); + let mut expected = [0u8; 40]; + r.squeeze_out(&mut expected); + + // Suspend mid-absorb, resume, finish, and confirm the squeezed output matches. + let mut x = AsconXof128::new(); + x.absorb(&data[..5]).unwrap(); + TestFrameworkSuspendableState::new().test(&x); + + let serialized = x.clone().suspend(); + let mut resumed = AsconXof128::from_suspended(serialized).unwrap(); + resumed.absorb(&data[5..]).unwrap(); + let mut out = [0u8; 40]; + resumed.squeeze_out(&mut out); + assert_eq!(out, expected, "resumed XOF output must match uninterrupted output"); + + // A corrupted state tag must be rejected. + let mut busted = serialized; + busted[3] ^= 0xFF; + assert!(matches!(AsconXof128::from_suspended(busted), Err(SuspendableError::InvalidData))); + + // Cross-type guard: an Ascon-CXOF128 state (same serialized length) must be rejected by + // Ascon-XOF128 via the state tag. + let mut c = AsconCXof128::with_customization(b"z").unwrap(); + c.absorb(&data).unwrap(); + let c_state = c.suspend(); + assert!(matches!(AsconXof128::from_suspended(c_state), Err(SuspendableError::InvalidData))); + + // An inconsistent buf_pos/squeezing combination must be rejected: buf_pos == RATE (8) is only + // valid once squeezing has begun. + let mut bad = serialized; + let len = bad.len(); + bad[len - 2] = 8; // buf_pos = RATE + bad[len - 1] = 0; // squeezing = false + assert!(matches!(AsconXof128::from_suspended(bad), Err(SuspendableError::InvalidData))); + + // Suspend mid-squeeze (not just mid-absorb) and confirm resuming continues the same stream. + let mut sq = AsconXof128::new(); + sq.absorb(&data).unwrap(); + let mut head = [0u8; 5]; + sq.squeeze_out(&mut head); + let squeezing_state = sq.clone().suspend(); + let mut resumed_sq = AsconXof128::from_suspended(squeezing_state).unwrap(); + let mut tail = [0u8; 35]; + resumed_sq.squeeze_out(&mut tail); + let mut combined = Vec::new(); + combined.extend_from_slice(&head); + combined.extend_from_slice(&tail); + assert_eq!(combined, expected, "resuming mid-squeeze must continue the same output stream"); +} diff --git a/crypto/factory/Cargo.toml b/crypto/factory/Cargo.toml index 22836c5f..c9765796 100644 --- a/crypto/factory/Cargo.toml +++ b/crypto/factory/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true [dependencies] +bouncycastle-ascon.workspace = true bouncycastle-core.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true diff --git a/crypto/factory/src/hash_factory.rs b/crypto/factory/src/hash_factory.rs index 3e6646ee..a300d14d 100644 --- a/crypto/factory/src/hash_factory.rs +++ b/crypto/factory/src/hash_factory.rs @@ -28,6 +28,8 @@ use crate::{AlgorithmFactory, FactoryError}; use crate::{DEFAULT, DEFAULT_128_BIT, DEFAULT_256_BIT}; +use bouncycastle_ascon as ascon; +use bouncycastle_ascon::ASCON_HASH256_NAME; use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength}; use bouncycastle_sha2 as sha2; @@ -66,6 +68,8 @@ pub enum HashFactory { SHA3_512(sha3::SHA3_512), /// SM3(sm3::SM3), + /// + AsconHash256(ascon::ascon_hash256::AsconHash256), } impl Default for HashFactory { @@ -98,6 +102,7 @@ impl AlgorithmFactory for HashFactory { SHA3_384_NAME => Ok(Self::SHA3_384(sha3::SHA3_384::new())), SHA3_512_NAME => Ok(Self::SHA3_512(sha3::SHA3_512::new())), SM3_NAME => Ok(Self::SM3(sm3::SM3::new())), + ASCON_HASH256_NAME => Ok(Self::AsconHash256(ascon::ascon_hash256::AsconHash256::new())), _ => Err(FactoryError::UnsupportedAlgorithm(format!( "The algorithm: \"{}\" is not a known Hash", alg_name @@ -129,6 +134,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.block_bitlen(), Self::SHA3_512(h) => h.block_bitlen(), Self::SM3(h) => h.block_bitlen(), + Self::AsconHash256(h) => h.block_bitlen(), } } @@ -145,6 +151,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.output_len(), Self::SHA3_512(h) => h.output_len(), Self::SM3(h) => h.output_len(), + Self::AsconHash256(h) => h.output_len(), } } @@ -161,6 +168,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.hash(data), Self::SHA3_512(h) => h.hash(data), Self::SM3(h) => h.hash(data), + Self::AsconHash256(h) => h.hash(data), } } @@ -179,6 +187,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.hash_out(data, output), Self::SHA3_512(h) => h.hash_out(data, output), Self::SM3(h) => h.hash_out(data, output), + Self::AsconHash256(h) => h.hash_out(data, output), } } @@ -195,6 +204,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.do_update(data), Self::SHA3_512(h) => h.do_update(data), Self::SM3(h) => h.do_update(data), + Self::AsconHash256(h) => h.do_update(data), } } @@ -211,6 +221,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.do_final(), Self::SHA3_512(h) => h.do_final(), Self::SM3(h) => h.do_final(), + Self::AsconHash256(h) => h.do_final(), } } @@ -229,6 +240,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.do_final_out(output), Self::SHA3_512(h) => h.do_final_out(output), Self::SM3(h) => h.do_final_out(output), + Self::AsconHash256(h) => h.do_final_out(output), } } @@ -249,6 +261,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), Self::SHA3_512(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), Self::SM3(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), + Self::AsconHash256(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), } } @@ -282,6 +295,9 @@ impl Hash for HashFactory { h.do_final_partial_bits_out(partial_byte, num_partial_bits, output) } Self::SM3(h) => h.do_final_partial_bits_out(partial_byte, num_partial_bits, output), + Self::AsconHash256(h) => { + h.do_final_partial_bits_out(partial_byte, num_partial_bits, output) + } } } @@ -298,6 +314,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.max_security_strength(), Self::SHA3_512(h) => h.max_security_strength(), Self::SM3(h) => h.max_security_strength(), + Self::AsconHash256(h) => h.max_security_strength(), } } } diff --git a/crypto/factory/src/xof_factory.rs b/crypto/factory/src/xof_factory.rs index 27cc5a5e..027a64b2 100644 --- a/crypto/factory/src/xof_factory.rs +++ b/crypto/factory/src/xof_factory.rs @@ -36,12 +36,15 @@ //! ``` use crate::{AlgorithmFactory, FactoryError}; +use bouncycastle_ascon::ASCON_XOF128_NAME; +use bouncycastle_ascon::ascon_xof128::AsconXof128; use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, XOF, XOFSqueezer}; use bouncycastle_sha3 as sha3; use bouncycastle_sha3::{SHAKE128_NAME, SHAKE256_NAME}; /*** Defaults ***/ + /// pub const DEFAULT_XOF_NAME: &str = SHAKE128_NAME; /// @@ -57,6 +60,8 @@ pub enum XOFFactory { SHAKE128(sha3::SHAKE128), /// SHAKE256(sha3::SHAKE256), + /// + AsconXof128(AsconXof128), } impl Default for XOFFactory { @@ -78,6 +83,7 @@ impl AlgorithmFactory for XOFFactory { match alg_name { SHAKE128_NAME => Ok(Self::SHAKE128(sha3::SHAKE128::new())), SHAKE256_NAME => Ok(Self::SHAKE256(sha3::SHAKE256::new())), + ASCON_XOF128_NAME => Ok(Self::AsconXof128(AsconXof128::new())), _ => Err(FactoryError::UnsupportedAlgorithm(format!( "The algorithm: \"{}\" is not a known XOF", alg_name @@ -85,6 +91,7 @@ impl AlgorithmFactory for XOFFactory { } } } + /// `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 @@ -101,8 +108,12 @@ impl Algorithm for XOFFactory { pub enum XOFFactorySqueezer { /// SHAKE128 output. SHAKE128(::Squeezer), + /// SHAKE256 output. SHAKE256(::Squeezer), + + /// Ascon-XOF128 output. + AsconXof128(::Squeezer), } impl XOFSqueezer for XOFFactorySqueezer { @@ -110,6 +121,7 @@ impl XOFSqueezer for XOFFactorySqueezer { match self { Self::SHAKE128(o) => o.do_output(num_bytes), Self::SHAKE256(o) => o.do_output(num_bytes), + Self::AsconXof128(o) => o.do_output(num_bytes), } } @@ -117,6 +129,7 @@ impl XOFSqueezer for XOFFactorySqueezer { match self { Self::SHAKE128(o) => o.do_output_out(output), Self::SHAKE256(o) => o.do_output_out(output), + Self::AsconXof128(o) => o.do_output_out(output), } } } @@ -126,6 +139,7 @@ impl Hash for XOFFactory { match self { Self::SHAKE128(h) => h.block_bitlen(), Self::SHAKE256(h) => h.block_bitlen(), + Self::AsconXof128(h) => h.block_bitlen(), } } @@ -133,6 +147,7 @@ impl Hash for XOFFactory { match self { Self::SHAKE128(h) => h.output_len(), Self::SHAKE256(h) => h.output_len(), + Self::AsconXof128(h) => h.output_len(), } } @@ -140,6 +155,7 @@ impl Hash for XOFFactory { match self { Self::SHAKE128(h) => h.hash(data), Self::SHAKE256(h) => h.hash(data), + Self::AsconXof128(h) => h.hash(data), } } @@ -147,6 +163,7 @@ impl Hash for XOFFactory { match self { Self::SHAKE128(h) => h.hash_out(data, output), Self::SHAKE256(h) => h.hash_out(data, output), + Self::AsconXof128(h) => h.hash_out(data, output), } } @@ -154,6 +171,7 @@ impl Hash for XOFFactory { match self { Self::SHAKE128(h) => h.do_update(data), Self::SHAKE256(h) => h.do_update(data), + Self::AsconXof128(h) => h.do_update(data), } } @@ -161,6 +179,7 @@ impl Hash for XOFFactory { match self { Self::SHAKE128(h) => h.do_final(), Self::SHAKE256(h) => h.do_final(), + Self::AsconXof128(h) => h.do_final(), } } @@ -168,6 +187,7 @@ impl Hash for XOFFactory { match self { Self::SHAKE128(h) => h.do_final_out(output), Self::SHAKE256(h) => h.do_final_out(output), + Self::AsconXof128(h) => h.do_final_out(output), } } @@ -179,6 +199,7 @@ impl Hash for XOFFactory { 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), + Self::AsconXof128(h) => h.do_final_partial_bits(partial_byte, num_bits), } } @@ -191,6 +212,9 @@ impl Hash for XOFFactory { match self { 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), + Self::AsconXof128(h) => { + h.do_final_partial_bits_out(partial_byte, num_bits, output) + } } } @@ -198,6 +222,7 @@ impl Hash for XOFFactory { match self { Self::SHAKE128(h) => Hash::max_security_strength(h), Self::SHAKE256(h) => Hash::max_security_strength(h), + Self::AsconXof128(h) => Hash::max_security_strength(h), } } } @@ -209,6 +234,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => XOFFactorySqueezer::SHAKE128(h.into_squeezer()), Self::SHAKE256(h) => XOFFactorySqueezer::SHAKE256(h.into_squeezer()), + Self::AsconXof128(h) => XOFFactorySqueezer::AsconXof128(h.into_squeezer()), } } @@ -218,12 +244,15 @@ impl XOF for XOFFactory { num_bits: usize, ) -> Result { Ok(match self { - Self::SHAKE128(h) => { - XOFFactorySqueezer::SHAKE128(h.into_squeezer_partial_bits(partial_byte, num_bits)?) - } - Self::SHAKE256(h) => { - XOFFactorySqueezer::SHAKE256(h.into_squeezer_partial_bits(partial_byte, num_bits)?) - } + Self::SHAKE128(h) => XOFFactorySqueezer::SHAKE128( + h.into_squeezer_partial_bits(partial_byte, num_bits)?, + ), + Self::SHAKE256(h) => XOFFactorySqueezer::SHAKE256( + h.into_squeezer_partial_bits(partial_byte, num_bits)?, + ), + Self::AsconXof128(h) => XOFFactorySqueezer::AsconXof128( + h.into_squeezer_partial_bits(partial_byte, num_bits)?, + ), }) } @@ -231,6 +260,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.xof(data, result_len), Self::SHAKE256(h) => h.xof(data, result_len), + Self::AsconXof128(h) => h.xof(data, result_len), } } @@ -240,6 +270,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.xof_out(data, output), Self::SHAKE256(h) => h.xof_out(data, output), + Self::AsconXof128(h) => h.xof_out(data, output), } } -} +} \ No newline at end of file diff --git a/crypto/factory/tests/hash_factory_tests.rs b/crypto/factory/tests/hash_factory_tests.rs index 22f5a3b4..5d70757f 100644 --- a/crypto/factory/tests/hash_factory_tests.rs +++ b/crypto/factory/tests/hash_factory_tests.rs @@ -164,6 +164,30 @@ mod hash_factory_tests { assert_eq!(XOFFactory::new("SHAKE256").unwrap().xof(&DUMMY_SEED[..512], 32), b"\xa1\xd7\x18\x85\xb0\xa8\x41\xf0\x3d\x1d\xc7\xf2\x73\x8a\x15\xcc\x98\x40\x71\xa1\x7f\xfe\xd5\xec\xac\xb9\xf5\x87\x20\xa4\x73\xbe"); } + #[test] + fn ascon_hash_tests() { + use bouncycastle_ascon::ASCON_HASH256_NAME; + use bouncycastle_ascon::ascon_hash256::AsconHash256; + use bouncycastle_factory::FactoryError; + + let direct = AsconHash256::new().hash(&DUMMY_SEED[..512]); + + // Construct by literal name and by the crate's name constant; both must match the + // direct implementation. + let by_name = HashFactory::new("Ascon-Hash256").unwrap(); + assert_eq!(by_name.output_len(), 32); + assert_eq!(by_name.hash(&DUMMY_SEED[..512]), direct); + + let by_const = HashFactory::new(ASCON_HASH256_NAME).unwrap(); + assert_eq!(by_const.hash(&DUMMY_SEED[..512]), direct); + + // Unknown algorithm names are still rejected. + assert!(matches!( + HashFactory::new("Ascon-Hash999"), + Err(FactoryError::UnsupportedAlgorithm(_)) + )); + } + #[test] fn test_defaults() { // All the ways to get "default" diff --git a/crypto/factory/tests/xof_factory_tests.rs b/crypto/factory/tests/xof_factory_tests.rs index bea0ca87..a3d3d1b7 100644 --- a/crypto/factory/tests/xof_factory_tests.rs +++ b/crypto/factory/tests/xof_factory_tests.rs @@ -1,7 +1,9 @@ -//! `XOFFactory` is a pass-through to the SHAKE types in `bouncycastle-sha3`, so the oracle for +//! `XOFFactory` is a pass-through to the concrete XOF implementations, 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_ascon::ASCON_XOF128_NAME; +use bouncycastle_ascon::ascon_xof128::AsconXof128; use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; @@ -51,18 +53,29 @@ fn check_against(make: impl Fn() -> XOFFactory, ctx: &str) { let mut f = make(); f.do_update(MSG); - assert_eq!(f.do_final_partial_bits(0x05, 3).unwrap(), expected_bits, "{ctx}: partial bits"); + 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!( + 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(_))), + matches!( + f.do_final_partial_bits(0xFF, 8), + Err(HashError::InvalidLength(_)) + ), "{ctx}: eight partial bits is not a partial byte" ); @@ -70,47 +83,104 @@ fn check_against(make: impl Fn() -> XOFFactory, ctx: &str) { let mut s = S::default(); s.do_update(MSG); let long = s.into_squeezer().do_output(3 * n); - assert_eq!(&long[..n], &expected[..], "the direct type's hash is a prefix of its stream"); + 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_squeezer(); assert_eq!(fo.do_output(n), &long[..n], "{ctx}: do_output"); + let mut buf = vec![0u8; 2 * n]; - assert_eq!(fo.do_output_out(&mut buf), 2 * n, "{ctx}: do_output_out returns the length"); + 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_squeezer_partial_bits(0x05, 3).unwrap().do_output(n); + let want = s + .into_squeezer_partial_bits(0x05, 3) + .unwrap() + .do_output(n); + let mut f = make(); f.do_update(MSG); assert_eq!( - f.into_squeezer_partial_bits(0x05, 3).unwrap().do_output(n), + f.into_squeezer_partial_bits(0x05, 3) + .unwrap() + .do_output(n), want, "{ctx}: into_squeezer_partial_bits" ); + let mut f = make(); f.do_update(MSG); - assert!(matches!(f.into_squeezer_partial_bits(0xFF, 8), Err(HashError::InvalidLength(_)))); + assert!(matches!( + f.into_squeezer_partial_bits(0xFF, 8), + Err(HashError::InvalidLength(_)) + )); // the one-shots assert_eq!(make().xof(MSG, 3 * n), long, "{ctx}: xof"); + let mut out = vec![0xFFu8; 3 * n]; - assert_eq!(make().xof_out(MSG, &mut out), 3 * n, "{ctx}: xof_out returns the length"); + assert_eq!( + make().xof_out(MSG, &mut out), + 3 * n, + "{ctx}: xof_out returns the length" + ); assert_eq!(out, long, "{ctx}: 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"); + 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"); + check_against::( + || XOFFactory::new(SHAKE256_NAME).unwrap(), + "SHAKE256 by constant", + ); + check_against::( + || XOFFactory::new("SHAKE256").unwrap(), + "SHAKE256 by string", + ); +} + +/// Verify that the Ascon-XOF128 factory registration resolves to the same implementation +/// as constructing Ascon-XOF128 directly. +#[test] +fn ascon_xof128_by_name_matches_the_direct_type() { + let direct = AsconXof128::new().xof(MSG, 64); + + // Construct using the crate constant. + assert_eq!( + XOFFactory::new(ASCON_XOF128_NAME).unwrap().xof(MSG, 64), + direct, + "Ascon-XOF128 by constant" + ); + + // Construct using the literal algorithm name. + assert_eq!( + XOFFactory::new("Ascon-XOF128").unwrap().xof(MSG, 64), + direct, + "Ascon-XOF128 by string" + ); } /// The configured defaults: SHAKE128 for the general and 128-bit defaults, SHAKE256 for 256-bit. @@ -123,9 +193,18 @@ fn defaults() { #[test] fn unknown_names_are_refused() { - for name in ["SHAKE512", "shake128", "", "cSHAKE128"] { + for name in [ + "SHAKE512", + "shake128", + "", + "cSHAKE128", + "Ascon-XOF999", + ] { assert!( - matches!(XOFFactory::new(name), Err(FactoryError::UnsupportedAlgorithm(_))), + matches!( + XOFFactory::new(name), + Err(FactoryError::UnsupportedAlgorithm(_)) + ), "{name:?} must not construct a XOF" ); } @@ -135,14 +214,16 @@ fn unknown_names_are_refused() { #[test] fn test_framework_xof() { let framework = TestFrameworkXOF::new(); + framework.test_xof( || XOFFactory::new(SHAKE128_NAME).unwrap(), MSG, &SHAKE128::new().xof(MSG, 100), ); + framework.test_xof( || XOFFactory::new(SHAKE256_NAME).unwrap(), MSG, &SHAKE256::new().xof(MSG, 100), ); -} +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 16a27ad1..4cd3b075 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub use bouncycastle_aes as aes; +pub use bouncycastle_ascon as ascon; pub use bouncycastle_base64 as base64; pub use bouncycastle_core as core; pub use bouncycastle_factory as factory; From 2c479f49fcf04a453a1c39ec37bb38bbceb2098a Mon Sep 17 00:00:00 2001 From: officialfrancismendoza Date: Thu, 17 Sep 2026 20:35:28 +0700 Subject: [PATCH 27/28] Rebased #120 onto #118. Ported ASCON XOF/CXOF to new Hash/XOF/XOFSqueezer API and updated factory/CLI/tests/benches to compile against the new API (#119) --- cli/src/helpers.rs | 18 +- cli/src/main.rs | 4 + crypto/ascon/benches/ascon_benches.rs | 17 +- crypto/ascon/src/ascon_cxof128.rs | 331 ++++++++++++++++------ crypto/ascon/src/ascon_xof128.rs | 329 +++++++++++++++------ crypto/ascon/tests/bc_test_data.rs | 41 ++- crypto/ascon/tests/cxof128_tests.rs | 218 ++++++++++---- crypto/ascon/tests/xof128_tests.rs | 192 +++++++++---- crypto/factory/src/xof_factory.rs | 18 +- crypto/factory/tests/xof_factory_tests.rs | 84 ++---- 10 files changed, 877 insertions(+), 375 deletions(-) diff --git a/cli/src/helpers.rs b/cli/src/helpers.rs index 2873e1e6..fa476b04 100644 --- a/cli/src/helpers.rs +++ b/cli/src/helpers.rs @@ -1,7 +1,7 @@ use bouncycastle::core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; -use bouncycastle::core::traits::{Hash, SecurityStrength, XOF}; +use bouncycastle::core::traits::{Hash, SecurityStrength, XOF, XOFSqueezer}; use bouncycastle::hex; use std::fs::File; use std::io; @@ -58,6 +58,7 @@ pub(crate) fn read_from_file_or_stdin(filename: &Option) -> Vec { pub(crate) fn write_bytes_or_hex(bytes: &[u8], output_hex: bool) { // first flush stdout to ensure any buffered data is written io::stdout().flush().unwrap(); + if output_hex { for b in bytes.iter() { print!("{b:02x}"); @@ -69,6 +70,7 @@ pub(crate) fn write_bytes_or_hex(bytes: &[u8], output_hex: bool) { pub(crate) fn write_bytes_or_hex_to_file(bytes: &[u8], filename: &str, output_hex: bool) { let mut file = File::create(filename).expect("Failed to create file"); + if output_hex { for b in bytes.iter() { file.write_all(format!("{b:02x}").as_bytes()).unwrap(); @@ -89,13 +91,15 @@ pub(crate) fn parse_seed(bytes: &[u8]) -> Result { - // it's not hex, so take the fist SEED_LEN bytes of the raw binary + // it's not hex, so take the first SEED_LEN bytes of the raw binary if bytes.len() < SEED_LEN || bytes.len() > SEED_LEN + 1 { return Err(()); } + bytes[..SEED_LEN].try_into().unwrap() } }; @@ -108,12 +112,14 @@ pub(crate) fn parse_seed(bytes: &[u8]) -> Result { sha3_cmd::cshake_cmd(256, *length, function_name, customization, *x); + } Some(Subcommands::AsconHash256 { x }) => { ascon_cmd::hash256_cmd(*x); } diff --git a/crypto/ascon/benches/ascon_benches.rs b/crypto/ascon/benches/ascon_benches.rs index eebe3f17..2238302c 100644 --- a/crypto/ascon/benches/ascon_benches.rs +++ b/crypto/ascon/benches/ascon_benches.rs @@ -26,12 +26,14 @@ fn bench_aead128_encrypt(c: &mut Criterion) { let mut group = c.benchmark_group("ascon::AsconAead128"); group.throughput(Throughput::Bytes(DATA_LEN as u64)); + group.bench_function(format!("{DATA_LEN} bytes -- ::encrypt()"), |b| { b.iter(|| { AsconAead128::encrypt(&key, &nonce, None, black_box(&data), &mut out).unwrap(); black_box(&out); }) }); + group.finish(); } @@ -41,12 +43,14 @@ fn bench_hash256(c: &mut Criterion) { let mut group = c.benchmark_group("ascon::AsconHash256"); group.throughput(Throughput::Bytes(DATA_LEN as u64)); + group.bench_function(format!("{DATA_LEN} bytes -- ::hash_out()"), |b| { b.iter(|| { AsconHash256::new().hash_out(black_box(&data), &mut digest); black_box(&digest); }) }); + group.finish(); } @@ -56,15 +60,17 @@ fn bench_xof128(c: &mut Criterion) { let mut group = c.benchmark_group("ascon::AsconXof128"); group.throughput(Throughput::Bytes((DATA_LEN + out.len()) as u64)); + group.bench_function( - format!("input: {DATA_LEN} bytes, output: 64 bytes -- ::hash_xof_out()"), + format!("input: {DATA_LEN} bytes, output: 64 bytes -- ::xof_out()"), |b| { b.iter(|| { - AsconXof128::new().hash_xof_out(black_box(&data), &mut out); + AsconXof128::new().xof_out(black_box(&data), &mut out); black_box(&out); }) }, ); + group.finish(); } @@ -75,17 +81,20 @@ fn bench_cxof128(c: &mut Criterion) { let mut group = c.benchmark_group("ascon::AsconCXof128"); group.throughput(Throughput::Bytes((DATA_LEN + out.len()) as u64)); + group.bench_function( - format!("input: {DATA_LEN} bytes, output: 64 bytes -- ::hash_xof_out()"), + format!("input: {DATA_LEN} bytes, output: 64 bytes -- ::xof_out()"), |b| { b.iter(|| { AsconCXof128::with_customization(customization) .unwrap() - .hash_xof_out(black_box(&data), &mut out); + .xof_out(black_box(&data), &mut out); + black_box(&out); }) }, ); + group.finish(); } diff --git a/crypto/ascon/src/ascon_cxof128.rs b/crypto/ascon/src/ascon_cxof128.rs index 4a0b055f..ae6d18db 100644 --- a/crypto/ascon/src/ascon_cxof128.rs +++ b/crypto/ascon/src/ascon_cxof128.rs @@ -3,10 +3,14 @@ //! A variant of Ascon-XOF128 that first absorbs a user-supplied customization string `Z` //! (length-prefixed per SP 800-232 Alg. 7) to provide domain separation. Same sponge parameters as //! Ascon-XOF128 (rate = 64 bits, capacity = 256 bits, `Ascon-p[12]`). +//! +//! Input absorption and output squeezing are represented by separate Rust types: +//! [`AsconCXof128`] accepts input, while [`AsconCXof128Squeezer`] produces the +//! extendable output stream. use bouncycastle_core::errors::{HashError, SuspendableError}; use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; -use bouncycastle_core::traits::{Algorithm, SecurityStrength, Suspendable, XOF}; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, Suspendable, XOF, XOFSqueezer}; use bouncycastle_utils::secret::Secret; use crate::sponge::{RATE, Sponge}; @@ -14,6 +18,12 @@ use crate::sponge::{RATE, Sponge}; /// Maximum customization-string length in bytes (2048 bits, per SP 800-232 §5.3). const MAX_CUSTOMIZATION_BYTES: usize = 256; +/// Nominal hash-view output length for Ascon-CXOF128. +/// +/// XOFs do not have an inherent output length. The [`Hash`] view therefore uses +/// twice the 128-bit security strength, matching the convention used for SHAKE128. +const NOMINAL_OUTPUT_LEN: usize = 32; + /// Ascon-CXOF128 customized extendable-output function (NIST SP 800-232 §5.3). #[derive(Clone)] pub struct AsconCXof128 { @@ -34,6 +44,7 @@ impl AsconCXof128 { 0x00C8356340A347F0, ]); sponge.reset_buffer(); + Self { sponge } } @@ -47,6 +58,7 @@ impl AsconCXof128 { "Ascon-CXOF128 customization string exceeds 256 bytes", )); } + if z.is_empty() { return Ok(Self::new()); } @@ -68,18 +80,23 @@ impl AsconCXof128 { // Customization is complete; reset the buffer to begin the message-absorb phase. sponge.reset_buffer(); + Ok(Self { sponge }) } - // Squeeze `output.len()` bytes of output. May be called multiple times; the first call ends the - // absorb phase by padding and absorbing the final block. Returns the number of bytes written. + /// Produces `output.len()` bytes from the XOF stream. + /// + /// The first call ends the message-absorb phase by padding and absorbing the + /// final message block. Subsequent calls continue the same output stream. fn squeeze_into(&mut self, output: &mut [u8]) -> usize { - let written = output.len(); + output.fill(0); + if !self.sponge.squeezing() { self.sponge.pad_and_absorb(); } + self.sponge.squeeze(output); - written + output.len() } } @@ -94,57 +111,114 @@ impl Algorithm for AsconCXof128 { const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; } -impl XOF for AsconCXof128 { - fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { - self.sponge.absorb(data); - let mut out = vec![0u8; result_len]; - self.squeeze_into(&mut out); +/// The output-producing half of [`AsconCXof128`]. +/// +/// Calling [`XOF::into_squeezer`] consumes the absorbing `AsconCXof128`, so once +/// output begins there is no longer an object on which [`Hash::do_update`] can +/// be called. +#[derive(Clone)] +pub struct AsconCXof128Squeezer { + xof: AsconCXof128, +} + +impl XOFSqueezer for AsconCXof128Squeezer { + 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(mut self, data: &[u8], output: &mut [u8]) -> usize { - self.sponge.absorb(data); - self.squeeze_into(output) + fn do_output_out(&mut self, output: &mut [u8]) -> usize { + self.xof.squeeze_into(output) } +} - fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> { - if self.sponge.squeezing() { - return Err(HashError::InvalidState( - "Ascon-CXOF128 cannot absorb after squeezing has begun", - )); - } - self.sponge.absorb(data); - Ok(()) +impl Hash for AsconCXof128 { + /// Ascon-CXOF128 absorbs at a rate of 64 bits. + fn block_bitlen(&self) -> usize { + RATE * 8 } - fn absorb_last_partial_byte( - &mut self, - _partial_byte: u8, - _num_partial_bits: usize, - ) -> Result<(), HashError> { - Err(HashError::InvalidInput("Ascon-CXOF128 does not support partial byte input")) + /// Nominal digest size used when Ascon-CXOF128 is viewed through [`Hash`]. + fn output_len(&self) -> usize { + NOMINAL_OUTPUT_LEN } - fn squeeze(&mut self, num_bytes: usize) -> Vec { - let mut out = vec![0u8; num_bytes]; - self.squeeze_into(&mut out); - out + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.do_final_out(output) } - fn squeeze_out(&mut self, output: &mut [u8]) -> usize { - self.squeeze_into(output) + fn do_update(&mut self, data: &[u8]) { + // A caller-visible AsconCXof128 is always in the absorbing phase: + // into_squeezer() consumes it before output can begin. + debug_assert!( + !self.sponge.squeezing(), + "a reachable AsconCXof128 must not already be squeezing" + ); + + self.sponge.absorb(data); } - fn squeeze_partial_byte_final(self, _num_bits: usize) -> Result { - Err(HashError::InvalidInput("Ascon-CXOF128 does not support partial byte output")) + fn do_final(self) -> Vec { + let output_len = self.output_len(); + self.into_squeezer().do_final(output_len) } - fn squeeze_partial_byte_final_out( + fn do_final_out(self, output: &mut [u8]) -> usize { + let output_len = self.output_len(); + let written = output_len.min(output.len()); + + // Hash::do_final_out requires bytes beyond output_len to be zero. + output[written..].fill(0); + + self.into_squeezer().do_final_out(&mut output[..written]) + } + + fn do_final_partial_bits( self, - _num_bits: usize, - _output: &mut u8, - ) -> Result<(), HashError> { - Err(HashError::InvalidInput("Ascon-CXOF128 does not support partial byte output")) + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-CXOF128 does not support partial byte input", + )); + } + + // A zero-bit partial byte means the message is byte-aligned. + let _ = partial_byte; + Ok(self.do_final()) + } + + fn do_final_partial_bits_out( + self, + partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-CXOF128 does not support partial byte input", + )); + } + + // A zero-bit partial byte means the message is byte-aligned. + let _ = partial_byte; + Ok(self.do_final_out(output)) } fn max_security_strength(&self) -> SecurityStrength { @@ -152,67 +226,156 @@ impl XOF for AsconCXof128 { } } -/// Length in bytes of the serialized state of [`AsconCXof128`]. -/// Layout: 3-byte library version || 1-byte state tag || 40-byte sponge state (5 × u64 LE) -/// || 8-byte rate buffer || 1-byte buffer position || 1-byte squeezing flag. +impl XOF for AsconCXof128 { + type Squeezer = AsconCXof128Squeezer; + + fn into_squeezer(self) -> Self::Squeezer { + AsconCXof128Squeezer { xof: self } + } + + fn into_squeezer_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result { + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-CXOF128 does not support partial byte input", + )); + } + + // Per the XOF trait contract, zero partial bits is exactly the + // byte-aligned into_squeezer() operation. + let _ = partial_byte; + Ok(self.into_squeezer()) + } +} + +/// Length in bytes of the serialized Ascon-CXOF128 state. /// -/// Note: the customization string is absorbed at construction time and is not part of the -/// suspended state; resuming continues the message-absorb / squeeze phase already in progress. +/// Layout: +/// +/// - 3-byte library version +/// - 1-byte state tag +/// - 40-byte sponge state (`5 × u64`, little endian) +/// - 8-byte rate buffer +/// - 1-byte buffer position +/// - 1-byte squeezing flag +/// +/// The customization string is already absorbed during construction, so it +/// does not need to be stored separately in the suspended representation. pub const SUSPENDED_ASCON_CXOF128_STATE_LEN: usize = 54; -// Distinguishes an Ascon-CXOF128 serialized state from the other (same-shaped) Ascon sponge states. +/// Distinguishes an Ascon-CXOF128 serialized state from other Ascon sponge states. const CXOF128_STATE_TAG: u8 = 0x03; +/// Deserialize the common sponge representation used by both the absorbing +/// [`AsconCXof128`] and squeezing [`AsconCXof128Squeezer`] forms. +fn deserialize_sponge( + serialized_state: [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN], +) -> Result { + // Infallible: check_lib_ver returns exactly 51 bytes after removing + // the three-byte library-version prefix. + let input: &[u8; SUSPENDED_ASCON_CXOF128_STATE_LEN - 3] = + check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + + if input[0] != CXOF128_STATE_TAG { + return Err(SuspendableError::InvalidData); + } + + let mut state = Secret::<[u64; 5]>::new(); + + for i in 0..5 { + // Each selected slice is exactly eight bytes. + state[i] = u64::from_le_bytes(input[1 + i * 8..1 + i * 8 + 8].try_into().unwrap()); + } + + let mut buf = Secret::<[u8; RATE]>::new(); + buf.copy_from_slice(&input[41..49]); + + let buf_pos = input[49] as usize; + + let squeezing = match input[50] { + 0 => false, + 1 => true, + _ => return Err(SuspendableError::InvalidData), + }; + + // While absorbing, a full rate buffer is drained immediately, so the + // position must be strictly less than RATE. During squeezing, RATE is + // allowed to represent "no buffered squeezed byte remains". + let valid_pos = if squeezing { buf_pos <= RATE } else { buf_pos < RATE }; + + if !valid_pos { + return Err(SuspendableError::InvalidData); + } + + Ok(Sponge::from_parts(state, buf, buf_pos, squeezing)) +} + +/// Serialize the common sponge representation. +fn serialize_sponge(sponge: &Sponge) -> [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_ASCON_CXOF128_STATE_LEN]; + + // Infallible: add_lib_ver returns exactly 51 bytes. + let out: &mut [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN - 3] = + add_lib_ver(&mut out_to_return).try_into().unwrap(); + + out[0] = CXOF128_STATE_TAG; + + let state = sponge.state_words(); + for i in 0..5 { + out[1 + i * 8..1 + i * 8 + 8].copy_from_slice(&state[i].to_le_bytes()); + } + + out[41..49].copy_from_slice(&sponge.buf_bytes()); + + debug_assert!(sponge.buf_pos() <= RATE); + out[49] = sponge.buf_pos() as u8; + out[50] = sponge.squeezing() as u8; + + out_to_return +} + impl Suspendable for AsconCXof128 { fn suspend(self) -> [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN] { - let mut out_to_return = [0u8; SUSPENDED_ASCON_CXOF128_STATE_LEN]; - // infallible: add_lib_ver returns a slice of exactly SUSPENDED_ASCON_CXOF128_STATE_LEN - 3 = 51 bytes. - let out: &mut [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN - 3] = - add_lib_ver(&mut out_to_return).try_into().unwrap(); - - out[0] = CXOF128_STATE_TAG; - let state = self.sponge.state_words(); - for i in 0..5 { - out[1 + i * 8..1 + i * 8 + 8].copy_from_slice(&state[i].to_le_bytes()); - } - out[41..49].copy_from_slice(&self.sponge.buf_bytes()); - debug_assert!(self.sponge.buf_pos() <= RATE); - out[49] = self.sponge.buf_pos() as u8; - out[50] = self.sponge.squeezing() as u8; - - out_to_return + serialize_sponge(&self.sponge) } fn from_suspended( serialized_state: [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN], ) -> Result { - // infallible: check_lib_ver returns a slice of exactly SUSPENDED_ASCON_CXOF128_STATE_LEN - 3 = 51 bytes. - let input: &[u8; SUSPENDED_ASCON_CXOF128_STATE_LEN - 3] = - check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + let sponge = deserialize_sponge(serialized_state)?; - if input[0] != CXOF128_STATE_TAG { + // The absorbing type must never contain a state that has already + // transitioned into squeezing. Such states belong to the squeezer. + if sponge.squeezing() { return Err(SuspendableError::InvalidData); } - let mut s = Secret::<[u64; 5]>::new(); - for i in 0..5 { - // infallible: each slice is exactly 8 bytes (1+i*8..1+i*8+8) by construction. - s[i] = u64::from_le_bytes(input[1 + i * 8..1 + i * 8 + 8].try_into().unwrap()); - } - let mut buf = Secret::<[u8; RATE]>::new(); - buf.copy_from_slice(&input[41..49]); - let buf_pos = input[49] as usize; - let squeezing = match input[50] { - 0 => false, - 1 => true, - _ => return Err(SuspendableError::InvalidData), - }; - // While absorbing, buf_pos must be < RATE (a full buffer is drained immediately); once - // squeezing, buf_pos may equal RATE (meaning "no leftover squeezed byte buffered"). - let valid_pos = if squeezing { buf_pos <= RATE } else { buf_pos < RATE }; - if !valid_pos { + + Ok(Self { sponge }) + } +} + +impl Suspendable for AsconCXof128Squeezer { + fn suspend(self) -> [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN] { + serialize_sponge(&self.xof.sponge) + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN], + ) -> Result { + let sponge = deserialize_sponge(serialized_state)?; + + // The squeezer is only valid after the phase transition has happened. + if !sponge.squeezing() { return Err(SuspendableError::InvalidData); } - Ok(AsconCXof128 { sponge: Sponge::from_parts(s, buf, buf_pos, squeezing) }) + Ok(Self { xof: AsconCXof128 { sponge } }) } } diff --git a/crypto/ascon/src/ascon_xof128.rs b/crypto/ascon/src/ascon_xof128.rs index 0b6e8a8f..2e087df9 100644 --- a/crypto/ascon/src/ascon_xof128.rs +++ b/crypto/ascon/src/ascon_xof128.rs @@ -1,15 +1,23 @@ //! Ascon-XOF128 extendable-output function (NIST SP 800-232 §5.2). //! -//! Sponge mode over `Ascon-p[12]` with rate = 64 bits, capacity = 256 bits. Supports the streaming -//! absorb/squeeze API of SP 800-232 §5.4 (squeeze may be called repeatedly). +//! Sponge mode over `Ascon-p[12]` with rate = 64 bits and capacity = 256 bits. +//! Input absorption and output squeezing are represented by separate Rust types: +//! [`AsconXof128`] accepts input, while [`AsconXof128Squeezer`] produces the +//! extendable output stream. use bouncycastle_core::errors::{HashError, SuspendableError}; use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; -use bouncycastle_core::traits::{Algorithm, SecurityStrength, Suspendable, XOF}; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, Suspendable, XOF, XOFSqueezer}; use bouncycastle_utils::secret::Secret; use crate::sponge::{RATE, Sponge}; +/// Nominal hash-view output length for Ascon-XOF128. +/// +/// XOFs do not have an inherent output length. The [`Hash`] view therefore uses +/// twice the 128-bit security strength, matching the convention used for SHAKE128. +const NOMINAL_OUTPUT_LEN: usize = 32; + /// Ascon-XOF128 as specified in NIST SP 800-232. #[derive(Clone)] pub struct AsconXof128 { @@ -19,7 +27,8 @@ pub struct AsconXof128 { impl AsconXof128 { /// Creates a new Ascon-XOF128 instance. pub fn new() -> Self { - // Precomputed state after the initialization permutation (SP 800-232 Table 12). + // Precomputed state after the initialization permutation + // (SP 800-232 Table 12). Self { sponge: Sponge::from_state([ 0xDA82CE768D9447EB, 0xCC7CE6C75F1EF969, 0xE7508FD780085631, 0x0EE0EA53416B58CC, @@ -28,15 +37,19 @@ impl AsconXof128 { } } - // Squeeze `output.len()` bytes of output. May be called multiple times; the first call ends the - // absorb phase by padding and absorbing the final block. Returns the number of bytes written. + /// Produces `output.len()` bytes from the XOF stream. + /// + /// The first call ends the absorb phase by padding and absorbing the final + /// message block. Subsequent calls continue the same output stream. fn squeeze_into(&mut self, output: &mut [u8]) -> usize { - let written = output.len(); + output.fill(0); + if !self.sponge.squeezing() { self.sponge.pad_and_absorb(); } + self.sponge.squeeze(output); - written + output.len() } } @@ -51,57 +64,114 @@ impl Algorithm for AsconXof128 { const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; } -impl XOF for AsconXof128 { - fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { - self.sponge.absorb(data); - let mut out = vec![0u8; result_len]; - self.squeeze_into(&mut out); +/// The output-producing half of [`AsconXof128`]. +/// +/// Calling [`XOF::into_squeezer`] consumes the absorbing `AsconXof128`, so once +/// output begins there is no longer an object on which [`Hash::do_update`] can +/// be called. +#[derive(Clone)] +pub struct AsconXof128Squeezer { + xof: AsconXof128, +} + +impl XOFSqueezer for AsconXof128Squeezer { + 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(mut self, data: &[u8], output: &mut [u8]) -> usize { - self.sponge.absorb(data); - self.squeeze_into(output) + fn do_output_out(&mut self, output: &mut [u8]) -> usize { + self.xof.squeeze_into(output) + } +} + +impl Hash for AsconXof128 { + /// Ascon-XOF128 absorbs at a rate of 64 bits. + fn block_bitlen(&self) -> usize { + RATE * 8 } - fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> { - if self.sponge.squeezing() { - return Err(HashError::InvalidState( - "Ascon-XOF128 cannot absorb after squeezing has begun", - )); - } - self.sponge.absorb(data); - Ok(()) + /// Nominal digest size used when Ascon-XOF128 is viewed through [`Hash`]. + fn output_len(&self) -> usize { + NOMINAL_OUTPUT_LEN } - fn absorb_last_partial_byte( - &mut self, - _partial_byte: u8, - _num_partial_bits: usize, - ) -> Result<(), HashError> { - Err(HashError::InvalidInput("Ascon-XOF128 does not support partial byte input")) + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() } - fn squeeze(&mut self, num_bytes: usize) -> Vec { - let mut out = vec![0u8; num_bytes]; - self.squeeze_into(&mut out); - out + 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]) { + // A caller-visible AsconXof128 is always in the absorbing phase: + // into_squeezer() consumes it before output can begin. + debug_assert!( + !self.sponge.squeezing(), + "a reachable AsconXof128 must not already be squeezing" + ); + + self.sponge.absorb(data); } - fn squeeze_out(&mut self, output: &mut [u8]) -> usize { - self.squeeze_into(output) + fn do_final(self) -> Vec { + let output_len = self.output_len(); + self.into_squeezer().do_final(output_len) } - fn squeeze_partial_byte_final(self, _num_bits: usize) -> Result { - Err(HashError::InvalidInput("Ascon-XOF128 does not support partial byte output")) + fn do_final_out(self, output: &mut [u8]) -> usize { + let output_len = self.output_len(); + let written = output_len.min(output.len()); + + // Hash::do_final_out requires bytes beyond output_len to be zero. + output[written..].fill(0); + + self.into_squeezer().do_final_out(&mut output[..written]) } - fn squeeze_partial_byte_final_out( + fn do_final_partial_bits( self, - _num_bits: usize, - _output: &mut u8, - ) -> Result<(), HashError> { - Err(HashError::InvalidInput("Ascon-XOF128 does not support partial byte output")) + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-XOF128 does not support partial byte input", + )); + } + + // A zero-bit partial byte means the message is byte-aligned. + let _ = partial_byte; + Ok(self.do_final()) + } + + fn do_final_partial_bits_out( + self, + partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-XOF128 does not support partial byte input", + )); + } + + // A zero-bit partial byte means the message is byte-aligned. + let _ = partial_byte; + Ok(self.do_final_out(output)) } fn max_security_strength(&self) -> SecurityStrength { @@ -109,64 +179,153 @@ impl XOF for AsconXof128 { } } -/// Length in bytes of the serialized state of [`AsconXof128`]. -/// Layout: 3-byte library version || 1-byte state tag || 40-byte sponge state (5 × u64 LE) -/// || 8-byte rate buffer || 1-byte buffer position || 1-byte squeezing flag. +impl XOF for AsconXof128 { + type Squeezer = AsconXof128Squeezer; + + fn into_squeezer(self) -> Self::Squeezer { + AsconXof128Squeezer { xof: self } + } + + fn into_squeezer_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result { + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-XOF128 does not support partial byte input", + )); + } + + // Per the XOF trait contract, zero partial bits is exactly the + // byte-aligned into_squeezer() operation. + let _ = partial_byte; + Ok(self.into_squeezer()) + } +} + +/// Length in bytes of the serialized Ascon-XOF128 state. +/// +/// Layout: +/// +/// - 3-byte library version +/// - 1-byte state tag +/// - 40-byte sponge state (`5 × u64`, little endian) +/// - 8-byte rate buffer +/// - 1-byte buffer position +/// - 1-byte squeezing flag pub const SUSPENDED_ASCON_XOF128_STATE_LEN: usize = 54; -// Distinguishes an Ascon-XOF128 serialized state from the other (same-shaped) Ascon sponge states. +/// Distinguishes an Ascon-XOF128 serialized state from other Ascon sponge states. const XOF128_STATE_TAG: u8 = 0x02; +/// Deserialize the common sponge representation used by both the absorbing +/// [`AsconXof128`] and squeezing [`AsconXof128Squeezer`] forms. +fn deserialize_sponge( + serialized_state: [u8; SUSPENDED_ASCON_XOF128_STATE_LEN], +) -> Result { + // Infallible: check_lib_ver returns exactly 51 bytes after removing + // the three-byte library-version prefix. + let input: &[u8; SUSPENDED_ASCON_XOF128_STATE_LEN - 3] = + check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + + if input[0] != XOF128_STATE_TAG { + return Err(SuspendableError::InvalidData); + } + + let mut state = Secret::<[u64; 5]>::new(); + + for i in 0..5 { + // Each selected slice is exactly eight bytes. + state[i] = u64::from_le_bytes(input[1 + i * 8..1 + i * 8 + 8].try_into().unwrap()); + } + + let mut buf = Secret::<[u8; RATE]>::new(); + buf.copy_from_slice(&input[41..49]); + + let buf_pos = input[49] as usize; + + let squeezing = match input[50] { + 0 => false, + 1 => true, + _ => return Err(SuspendableError::InvalidData), + }; + + // While absorbing, a full rate buffer is drained immediately, so the + // position must be strictly less than RATE. During squeezing, RATE is + // allowed to represent "no buffered squeezed byte remains". + let valid_pos = if squeezing { buf_pos <= RATE } else { buf_pos < RATE }; + + if !valid_pos { + return Err(SuspendableError::InvalidData); + } + + Ok(Sponge::from_parts(state, buf, buf_pos, squeezing)) +} + +/// Serialize the common sponge representation. +fn serialize_sponge(sponge: &Sponge) -> [u8; SUSPENDED_ASCON_XOF128_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_ASCON_XOF128_STATE_LEN]; + + // Infallible: add_lib_ver returns exactly 51 bytes. + let out: &mut [u8; SUSPENDED_ASCON_XOF128_STATE_LEN - 3] = + add_lib_ver(&mut out_to_return).try_into().unwrap(); + + out[0] = XOF128_STATE_TAG; + + let state = sponge.state_words(); + for i in 0..5 { + out[1 + i * 8..1 + i * 8 + 8].copy_from_slice(&state[i].to_le_bytes()); + } + + out[41..49].copy_from_slice(&sponge.buf_bytes()); + + debug_assert!(sponge.buf_pos() <= RATE); + out[49] = sponge.buf_pos() as u8; + out[50] = sponge.squeezing() as u8; + + out_to_return +} + impl Suspendable for AsconXof128 { fn suspend(self) -> [u8; SUSPENDED_ASCON_XOF128_STATE_LEN] { - let mut out_to_return = [0u8; SUSPENDED_ASCON_XOF128_STATE_LEN]; - // infallible: add_lib_ver returns a slice of exactly SUSPENDED_ASCON_XOF128_STATE_LEN - 3 = 51 bytes. - let out: &mut [u8; SUSPENDED_ASCON_XOF128_STATE_LEN - 3] = - add_lib_ver(&mut out_to_return).try_into().unwrap(); - - out[0] = XOF128_STATE_TAG; - let state = self.sponge.state_words(); - for i in 0..5 { - out[1 + i * 8..1 + i * 8 + 8].copy_from_slice(&state[i].to_le_bytes()); - } - out[41..49].copy_from_slice(&self.sponge.buf_bytes()); - debug_assert!(self.sponge.buf_pos() <= RATE); - out[49] = self.sponge.buf_pos() as u8; - out[50] = self.sponge.squeezing() as u8; - - out_to_return + serialize_sponge(&self.sponge) } fn from_suspended( serialized_state: [u8; SUSPENDED_ASCON_XOF128_STATE_LEN], ) -> Result { - // infallible: check_lib_ver returns a slice of exactly SUSPENDED_ASCON_XOF128_STATE_LEN - 3 = 51 bytes. - let input: &[u8; SUSPENDED_ASCON_XOF128_STATE_LEN - 3] = - check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + let sponge = deserialize_sponge(serialized_state)?; - if input[0] != XOF128_STATE_TAG { + // The absorbing type must never contain a state that has already + // transitioned into squeezing. Such states belong to the squeezer. + if sponge.squeezing() { return Err(SuspendableError::InvalidData); } - let mut s = Secret::<[u64; 5]>::new(); - for i in 0..5 { - // infallible: each slice is exactly 8 bytes (1+i*8..1+i*8+8) by construction. - s[i] = u64::from_le_bytes(input[1 + i * 8..1 + i * 8 + 8].try_into().unwrap()); - } - let mut buf = Secret::<[u8; RATE]>::new(); - buf.copy_from_slice(&input[41..49]); - let buf_pos = input[49] as usize; - let squeezing = match input[50] { - 0 => false, - 1 => true, - _ => return Err(SuspendableError::InvalidData), - }; - // While absorbing, buf_pos must be < RATE (a full buffer is drained immediately); once - // squeezing, buf_pos may equal RATE (meaning "no leftover squeezed byte buffered"). - let valid_pos = if squeezing { buf_pos <= RATE } else { buf_pos < RATE }; - if !valid_pos { + + Ok(Self { sponge }) + } +} + +impl Suspendable for AsconXof128Squeezer { + fn suspend(self) -> [u8; SUSPENDED_ASCON_XOF128_STATE_LEN] { + serialize_sponge(&self.xof.sponge) + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_XOF128_STATE_LEN], + ) -> Result { + let sponge = deserialize_sponge(serialized_state)?; + + // The squeezer is only valid after the phase transition has happened. + if !sponge.squeezing() { return Err(SuspendableError::InvalidData); } - Ok(AsconXof128 { sponge: Sponge::from_parts(s, buf, buf_pos, squeezing) }) + Ok(Self { xof: AsconXof128 { sponge } }) } } diff --git a/crypto/ascon/tests/bc_test_data.rs b/crypto/ascon/tests/bc_test_data.rs index 01525a94..44305e7a 100644 --- a/crypto/ascon/tests/bc_test_data.rs +++ b/crypto/ascon/tests/bc_test_data.rs @@ -30,13 +30,14 @@ mod bc_test_data { fn get_test_data(filename: &str) -> Result { let found: u8; + if Path::new(TEST_DATA_PATH_RELATIVE).exists() { found = 1; } else if Path::new(TEST_DATA_PATH).exists() { found = 2; } else { found = 3; - }; + } // just print once TEST_DATA_CHECK.call_once(|| match found { @@ -58,6 +59,7 @@ mod bc_test_data { fn decode_hex(value: &str) -> Vec { let clean = value.trim(); + if clean.is_empty() { Vec::new() } else { hex::decode(clean).expect("valid hex") } } @@ -68,27 +70,34 @@ mod bc_test_data { for raw in contents.lines() { let line = raw.trim(); + if line.is_empty() { if !current.is_empty() { cases.push(std::mem::take(&mut current)); } continue; } + if line.starts_with('#') { continue; } + if let Some((key, value)) = line.split_once('=') { let key = key.trim().to_string(); let value = value.trim().to_string(); + if key == "Count" && !current.is_empty() { cases.push(std::mem::take(&mut current)); } + current.insert(key, value); } } + if !current.is_empty() { cases.push(current); } + cases } @@ -98,6 +107,7 @@ mod bc_test_data { return v.as_str(); } } + panic!("missing field {names:?}; case had {:?}", case.keys().collect::>()); } @@ -112,11 +122,13 @@ mod bc_test_data { fn key_material(key: &[u8; 16]) -> KeyMaterial<16> { let mut km = KeyMaterial::<16>::from_bytes_as_type(key, KeyType::SymmetricCipherKey).unwrap(); + do_hazardous_operations(&mut km, |k| { k.set_key_type(KeyType::SymmetricCipherKey)?; k.set_security_strength(SecurityStrength::_128bit) }) .unwrap(); + km } @@ -126,6 +138,7 @@ mod bc_test_data { Ok(c) => c, Err(()) => return, }; + let cases = parse_kat(&contents); assert!(!cases.is_empty(), "no AEAD cases parsed"); @@ -135,29 +148,36 @@ mod bc_test_data { let ad = decode_hex(field(case, &["AD", "A"])); let pt = decode_hex(field(case, &["PT", "P"])); let expected_ct = decode_hex(field(case, &["CT", "C"])); + let ad_opt = if ad.is_empty() { None } else { Some(ad.as_slice()) }; // One-shot encrypt. let mut ct = vec![0u8; pt.len() + 16]; let n = AsconAead128::encrypt(&key, &nonce, ad_opt, &pt, &mut ct).unwrap(); ct.truncate(n); + assert_eq!(ct, expected_ct, "encrypt mismatch (Count {})", field(case, &["Count"])); // One-shot decrypt round-trip. let mut pt_out = vec![0u8; expected_ct.len()]; let m = AsconAead128::decrypt(&key, &nonce, ad_opt, &expected_ct, &mut pt_out) .expect("decrypt should authenticate"); + pt_out.truncate(m); + assert_eq!(pt_out, pt, "decrypt mismatch (Count {})", field(case, &["Count"])); // Byte-at-a-time streaming encrypt/decrypt, through the inherent API. let mut enc = AsconAead128::new(&key, &nonce, ad_opt, true).unwrap(); let mut stream_ct = pt.clone(); + for byte in stream_ct.iter_mut() { enc.do_encrypt_update(core::slice::from_mut(byte)); } + let tag = enc.do_encrypt_final(); stream_ct.extend_from_slice(&tag); + assert_eq!( stream_ct, expected_ct, @@ -167,10 +187,13 @@ mod bc_test_data { let mut dec = AsconAead128::new(&key, &nonce, ad_opt, false).unwrap(); let mut stream_pt = expected_ct[..pt.len()].to_vec(); + for byte in stream_pt.iter_mut() { dec.do_decrypt_update(core::slice::from_mut(byte)); } + dec.do_decrypt_final(&tag).expect("streaming decrypt should authenticate"); + assert_eq!( stream_pt, pt, @@ -178,6 +201,7 @@ mod bc_test_data { field(case, &["Count"]) ); } + println!("Ascon-AEAD128: {} KAT cases passed", cases.len()); } @@ -187,12 +211,14 @@ mod bc_test_data { Ok(c) => c, Err(()) => return, }; + let cases = parse_kat(&contents); assert!(!cases.is_empty(), "no Hash256 cases parsed"); for case in &cases { let msg = decode_hex(field(case, &["Msg"])); let expected = decode_hex(field(case, &["MD"])); + assert_eq!( AsconHash256::digest(&msg).as_slice(), expected.as_slice(), @@ -200,6 +226,7 @@ mod bc_test_data { field(case, &["Count"]) ); } + println!("Ascon-Hash256: {} KAT cases passed", cases.len()); } @@ -209,15 +236,19 @@ mod bc_test_data { Ok(c) => c, Err(()) => return, }; + let cases = parse_kat(&contents); assert!(!cases.is_empty(), "no XOF128 cases parsed"); for case in &cases { let msg = decode_hex(field(case, &["Msg"])); let expected = decode_hex(field(case, &["MD", "Output"])); - let got = AsconXof128::new().hash_xof(&msg, expected.len()); + + let got = AsconXof128::new().xof(&msg, expected.len()); + assert_eq!(got, expected, "XOF128 mismatch (Count {})", field(case, &["Count"])); } + println!("Ascon-XOF128: {} KAT cases passed", cases.len()); } @@ -227,6 +258,7 @@ mod bc_test_data { Ok(c) => c, Err(()) => return, }; + let cases = parse_kat(&contents); assert!(!cases.is_empty(), "no CXOF128 cases parsed"); @@ -234,9 +266,12 @@ mod bc_test_data { let msg = decode_hex(field(case, &["Msg"])); let z = decode_hex(field(case, &["Z", "Customization"])); let expected = decode_hex(field(case, &["MD", "Output"])); - let got = AsconCXof128::with_customization(&z).unwrap().hash_xof(&msg, expected.len()); + + let got = AsconCXof128::with_customization(&z).unwrap().xof(&msg, expected.len()); + assert_eq!(got, expected, "CXOF128 mismatch (Count {})", field(case, &["Count"])); } + println!("Ascon-CXOF128: {} KAT cases passed", cases.len()); } } diff --git a/crypto/ascon/tests/cxof128_tests.rs b/crypto/ascon/tests/cxof128_tests.rs index 5478ba58..bf3ee43b 100644 --- a/crypto/ascon/tests/cxof128_tests.rs +++ b/crypto/ascon/tests/cxof128_tests.rs @@ -1,12 +1,13 @@ //! Ascon-CXOF128 tests (NIST SP 800-232 §5.3). //! //! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus -//! domain-separation, streaming/byte-at-a-time equivalence, trait-API, and misuse-guard tests. +//! domain-separation, streaming/byte-at-a-time equivalence, trait-API, partial-input rejection, +//! and suspend/resume tests. -use bouncycastle_ascon::ascon_cxof128::AsconCXof128; +use bouncycastle_ascon::ascon_cxof128::{AsconCXof128, AsconCXof128Squeezer}; use bouncycastle_ascon::ascon_xof128::AsconXof128; use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{Hash, Suspendable, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_hex as hex; @@ -43,6 +44,7 @@ const CXOF_KAT: &[(&str, &str, &str)] = &[ fn dh(s: &str) -> Vec { let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } } @@ -56,18 +58,22 @@ fn cxof128_embedded_kat() { let msg = dh(msg_hex); let z = dh(z_hex); let expected = dh(md_hex); - let got = AsconCXof128::with_customization(&z).unwrap().hash_xof(&msg, expected.len()); + + let got = AsconCXof128::with_customization(&z).unwrap().xof(&msg, expected.len()); + assert_eq!(got, expected, "msg={msg_hex} z={z_hex}"); - // `AsconCXof128::default()` uses an empty customization string, so the generic XOF - // framework (which constructs via `Default`) only applies to the empty-Z vectors; the - // non-empty-Z vectors are covered by `cxof128_prefix_property_and_streaming` below. + // AsconCXof128::default() uses an empty customization string, so the generic XOF + // framework, which constructs a fresh value itself, only applies directly to empty-Z + // vectors. Non-empty customization is exercised explicitly by the other tests below. if z.is_empty() { - // AsconCXof128 has no absorb_last_partial_byte / squeeze_partial_byte_final support, so - // that part of the framework is disabled; everything else (hash_xof, streaming, prefix - // property, chunked absorb, absorb-after-squeeze) is exercised here. - TestFrameworkXOF { enable_partial_byte_tests: false } - .test_xof::(&msg, &expected); + let mut framework = TestFrameworkXOF::new(); + + // SP 800-232 Ascon-CXOF128 operates on byte strings in this implementation, so + // non-byte-aligned final input is deliberately unsupported. + framework.enable_partial_byte_tests = false; + + framework.test_xof(AsconCXof128::new, &msg, &expected); } } } @@ -76,13 +82,17 @@ fn cxof128_embedded_kat() { fn cxof128_domain_separation() { let msg = pattern(48); - let out_z1 = AsconCXof128::with_customization(b"context-1").unwrap().hash_xof(&msg, 64); - let out_z2 = AsconCXof128::with_customization(b"context-2").unwrap().hash_xof(&msg, 64); + let out_z1 = AsconCXof128::with_customization(b"context-1").unwrap().xof(&msg, 64); + + let out_z2 = AsconCXof128::with_customization(b"context-2").unwrap().xof(&msg, 64); + assert_ne!(out_z1, out_z2, "different customization strings must give different output"); - // Empty-customization CXOF128 must differ from XOF128 (different IV). - let cxof_empty = AsconCXof128::new().hash_xof(&msg, 64); - let xof = AsconXof128::new().hash_xof(&msg, 64); + // Empty-customization CXOF128 must differ from XOF128 because the two functions use + // different initialization/domain separation. + let cxof_empty = AsconCXof128::new().xof(&msg, 64); + let xof = AsconXof128::new().xof(&msg, 64); + assert_ne!(cxof_empty, xof, "CXOF128 (empty Z) must differ from XOF128"); } @@ -90,123 +100,203 @@ fn cxof128_domain_separation() { fn cxof128_prefix_property_and_streaming() { let z = b"cust"; let msg = pattern(70); - let full = AsconCXof128::with_customization(z).unwrap().hash_xof(&msg, 100); - // Squeezing in several calls yields the same stream (prefix property). + let full = AsconCXof128::with_customization(z).unwrap().xof(&msg, 100); + + // Reading from one squeezer in several calls must produce exactly the same continuous + // stream as requesting the whole output in one shot. let mut x = AsconCXof128::with_customization(z).unwrap(); - x.absorb(&msg).unwrap(); + x.do_update(&msg); + let mut squeezer = x.into_squeezer(); + let mut piecewise = Vec::new(); + for n in [30usize, 40, 30] { let mut part = vec![0u8; n]; - x.squeeze_out(&mut part); + let written = squeezer.do_output_out(&mut part); + + assert_eq!(written, n); piecewise.extend_from_slice(&part); } + assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); - // Absorbing in chunks equals one-shot absorb. + // Absorbing the message in chunks must equal absorbing it in one call. for chunk in [1usize, 8, 9, 64] { let mut xc = AsconCXof128::with_customization(z).unwrap(); + for piece in msg.chunks(chunk) { - xc.absorb(piece).unwrap(); + xc.do_update(piece); } + let mut got = vec![0u8; 100]; - xc.squeeze_out(&mut got); + let written = xc.into_squeezer().do_output_out(&mut got); + + assert_eq!(written, got.len()); assert_eq!(got, full, "chunked absorb mismatch (chunk={chunk})"); } } #[test] fn cxof128_byte_at_a_time_matches_one_shot() { - let msg = pattern(40); // > 8 bytes so byte-at-a-time absorb triggers full-block absorption - let cref = AsconCXof128::with_customization(b"zz").unwrap().hash_xof(&msg, 48); + let msg = pattern(40); + + let reference = AsconCXof128::with_customization(b"zz").unwrap().xof(&msg, 48); + let mut c = AsconCXof128::with_customization(b"zz").unwrap(); + for &b in &msg { - c.absorb(&[b]).unwrap(); + c.do_update(&[b]); } - let mut o = [0u8; 48]; - c.squeeze_out(&mut o); - assert_eq!(o.to_vec(), cref, "CXOF128 byte-at-a-time absorb mismatch"); + + let mut out = [0u8; 48]; + let written = c.into_squeezer().do_output_out(&mut out); + + assert_eq!(written, out.len()); + assert_eq!(out.to_vec(), reference, "CXOF128 byte-at-a-time absorb mismatch"); } #[test] -fn cxof128_unsupported_partial_ops_return_err() { - let mut c = AsconCXof128::new(); - assert!(c.absorb_last_partial_byte(0, 3).is_err()); - assert!(AsconCXof128::new().squeeze_partial_byte_final(3).is_err()); - let mut b = 0u8; - assert!(AsconCXof128::new().squeeze_partial_byte_final_out(3, &mut b).is_err()); +fn cxof128_unsupported_partial_input_returns_err() { + // num_bits == 0 means there is no partial byte and must behave exactly like ordinary + // finalization / into_squeezer. + assert!(AsconCXof128::new().into_squeezer_partial_bits(0xFF, 0).is_ok()); + + assert!(AsconCXof128::new().do_final_partial_bits(0x80, 0).is_ok()); + + // Real partial-byte input is deliberately unsupported by Ascon-CXOF128. + assert!(matches!( + AsconCXof128::new().into_squeezer_partial_bits(0xA0, 3), + Err(HashError::InvalidInput(_)) + )); + + assert!(matches!( + AsconCXof128::new().do_final_partial_bits(0xA0, 3), + Err(HashError::InvalidInput(_)) + )); + + let mut out = [0u8; 32]; + + assert!(matches!( + AsconCXof128::new().do_final_partial_bits_out(0xA0, 3, &mut out), + Err(HashError::InvalidInput(_)) + )); + + // More than seven bits is not a partial byte at all. + assert!(matches!( + AsconCXof128::new().into_squeezer_partial_bits(0xFF, 8), + Err(HashError::InvalidLength(_)) + )); } #[test] -fn cxof128_absorb_after_squeeze_errors() { +fn cxof128_absorb_then_squeeze_type_transition() { let mut x = AsconCXof128::with_customization(b"z").unwrap(); - x.absorb(b"data").unwrap(); - let mut out = [0u8; 8]; - x.squeeze_out(&mut out); - // Absorbing after squeezing has begun is reported as an error rather than a panic. - assert!(matches!(x.absorb(b"more"), Err(HashError::InvalidState(_)))); + x.do_update(b"data"); + + let mut squeezer = x.into_squeezer(); + + let first = squeezer.do_output(8); + let second = squeezer.do_output(8); + + let whole = AsconCXof128::with_customization(b"z").unwrap().xof(b"data", 16); + + assert_eq!( + [first, second].concat(), + whole, + "successive reads must continue the same XOF stream" + ); + + // There is deliberately no "absorb after squeeze" runtime test anymore. + // `into_squeezer()` consumes the AsconCXof128, and the returned squeezer does not implement + // Hash::do_update, so that invalid state is prevented by the type system. } #[test] fn cxof128_suspendable_state() { use bouncycastle_core::errors::SuspendableError; - use bouncycastle_core::traits::Suspendable; use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableState; let z = b"customization"; let data: Vec = (0..30u8).collect(); // Reference: uninterrupted absorb + squeeze under the same customization string. - let mut r = AsconCXof128::with_customization(z).unwrap(); - r.absorb(&data).unwrap(); + let mut reference = AsconCXof128::with_customization(z).unwrap(); + reference.do_update(&data); + let mut expected = [0u8; 40]; - r.squeeze_out(&mut expected); + reference.into_squeezer().do_output_out(&mut expected); - // Suspend mid-absorb, resume, finish, and confirm the squeezed output matches. (The - // customization string was already absorbed at construction and is not part of the state.) + // Suspend in the absorbing phase, resume, finish the remaining input, and confirm that + // the output matches the uninterrupted computation. The customization string has already + // been folded into the sponge state at construction time. let mut x = AsconCXof128::with_customization(z).unwrap(); - x.absorb(&data[..5]).unwrap(); + x.do_update(&data[..5]); + TestFrameworkSuspendableState::new().test(&x); let serialized = x.clone().suspend(); + let mut resumed = AsconCXof128::from_suspended(serialized).unwrap(); - resumed.absorb(&data[5..]).unwrap(); + resumed.do_update(&data[5..]); + let mut out = [0u8; 40]; - resumed.squeeze_out(&mut out); + resumed.into_squeezer().do_output_out(&mut out); + assert_eq!(out, expected, "resumed CXOF output must match uninterrupted output"); // A corrupted state tag must be rejected. let mut busted = serialized; busted[3] ^= 0xFF; + assert!(matches!(AsconCXof128::from_suspended(busted), Err(SuspendableError::InvalidData))); - // Cross-type guard: an Ascon-XOF128 state (same serialized length) must be rejected by - // Ascon-CXOF128 via the state tag. + // Cross-type guard: an Ascon-XOF128 state has the same serialized length but a different + // state tag, so Ascon-CXOF128 must reject it. let mut xof = AsconXof128::new(); - xof.absorb(&data).unwrap(); + xof.do_update(&data); + let xof_state = xof.suspend(); + assert!(matches!(AsconCXof128::from_suspended(xof_state), Err(SuspendableError::InvalidData))); - // An inconsistent buf_pos/squeezing combination must be rejected: buf_pos == RATE (8) is only - // valid once squeezing has begun. + // An inconsistent buf_pos/squeezing combination must be rejected: buf_pos == RATE (8) + // is only valid after squeezing has begun. let mut bad = serialized; let len = bad.len(); - bad[len - 2] = 8; // buf_pos = RATE - bad[len - 1] = 0; // squeezing = false + + bad[len - 2] = 8; + bad[len - 1] = 0; + assert!(matches!(AsconCXof128::from_suspended(bad), Err(SuspendableError::InvalidData))); - // Suspend mid-squeeze (not just mid-absorb) and confirm resuming continues the same stream. + // Suspend after squeezing has actually begun and confirm that restoring the squeezer + // continues the same stream. let mut sq = AsconCXof128::with_customization(z).unwrap(); - sq.absorb(&data).unwrap(); + sq.do_update(&data); + + let mut sq = sq.into_squeezer(); + let mut head = [0u8; 5]; - sq.squeeze_out(&mut head); + sq.do_output_out(&mut head); + let squeezing_state = sq.clone().suspend(); - let mut resumed_sq = AsconCXof128::from_suspended(squeezing_state).unwrap(); + + // A squeezing state belongs to AsconCXof128Squeezer, not the absorbing AsconCXof128 type. + assert!(matches!( + AsconCXof128::from_suspended(squeezing_state), + Err(SuspendableError::InvalidData) + )); + + let mut resumed_sq = AsconCXof128Squeezer::from_suspended(squeezing_state).unwrap(); + let mut tail = [0u8; 35]; - resumed_sq.squeeze_out(&mut tail); + resumed_sq.do_output_out(&mut tail); + let mut combined = Vec::new(); combined.extend_from_slice(&head); combined.extend_from_slice(&tail); + assert_eq!(combined, expected, "resuming mid-squeeze must continue the same output stream"); } @@ -214,8 +304,10 @@ fn cxof128_suspendable_state() { fn cxof128_customization_length_bound() { // SP 800-232 §5.3: the customization string shall be at most 2048 bits (256 bytes). let ok = vec![0u8; 256]; + assert!(AsconCXof128::with_customization(&ok).is_ok()); let too_long = vec![0u8; 257]; + assert!(matches!(AsconCXof128::with_customization(&too_long), Err(HashError::InvalidInput(_)))); } diff --git a/crypto/ascon/tests/xof128_tests.rs b/crypto/ascon/tests/xof128_tests.rs index 22ed9c0a..acc2e768 100644 --- a/crypto/ascon/tests/xof128_tests.rs +++ b/crypto/ascon/tests/xof128_tests.rs @@ -1,11 +1,12 @@ //! Ascon-XOF128 tests (NIST SP 800-232 §5.2). //! //! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus the -//! prefix property, streaming/byte-at-a-time equivalence, trait-API, and misuse-guard tests. +//! prefix property, streaming/byte-at-a-time equivalence, trait-API, partial-input rejection, +//! and suspend/resume tests. -use bouncycastle_ascon::ascon_xof128::AsconXof128; +use bouncycastle_ascon::ascon_xof128::{AsconXof128, AsconXof128Squeezer}; use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{Hash, Suspendable, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_hex as hex; @@ -37,6 +38,7 @@ const XOF_KAT: &[(&str, &str)] = &[ fn dh(s: &str) -> Vec { let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } } @@ -49,135 +51,217 @@ fn xof128_embedded_kat() { for (msg_hex, md_hex) in XOF_KAT { let msg = dh(msg_hex); let expected = dh(md_hex); - let got = AsconXof128::new().hash_xof(&msg, expected.len()); + + let got = AsconXof128::new().xof(&msg, expected.len()); + assert_eq!(got, expected, "msg={msg_hex}"); - // AsconXof128 has no absorb_last_partial_byte / squeeze_partial_byte_final support, so that - // part of the framework is disabled; everything else (hash_xof, streaming, prefix property, - // chunked absorb, absorb-after-squeeze) is exercised here. - TestFrameworkXOF { enable_partial_byte_tests: false } - .test_xof::(&msg, &expected); + + let mut framework = TestFrameworkXOF::new(); + + // This implementation intentionally supports only byte-aligned Ascon-XOF128 input. + framework.enable_partial_byte_tests = false; + + framework.test_xof(AsconXof128::new, &msg, &expected); } } #[test] fn xof128_prefix_property_and_streaming() { let msg = pattern(70); - let full = AsconXof128::new().hash_xof(&msg, 100); - // Squeezing in several calls yields the same stream (prefix property). + let full = AsconXof128::new().xof(&msg, 100); + + // Squeezing in several calls yields the same continuous stream. let mut x = AsconXof128::new(); - x.absorb(&msg).unwrap(); + x.do_update(&msg); + + let mut squeezer = x.into_squeezer(); let mut piecewise = Vec::new(); + for n in [30usize, 40, 30] { let mut part = vec![0u8; n]; - x.squeeze_out(&mut part); + let written = squeezer.do_output_out(&mut part); + + assert_eq!(written, n); piecewise.extend_from_slice(&part); } + assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); - // Absorbing in chunks equals one-shot absorb. + // Absorbing in chunks equals one-shot input. for chunk in [1usize, 8, 9, 64] { let mut xc = AsconXof128::new(); + for piece in msg.chunks(chunk) { - xc.absorb(piece).unwrap(); + xc.do_update(piece); } + let mut got = vec![0u8; 100]; - xc.squeeze_out(&mut got); + let written = xc.into_squeezer().do_output_out(&mut got); + + assert_eq!(written, got.len()); + assert_eq!(got, full, "chunked absorb mismatch (chunk={chunk})"); } } #[test] fn xof128_byte_at_a_time_matches_one_shot() { - let msg = pattern(40); // > 8 bytes so byte-at-a-time absorb triggers full-block absorption - let xref = AsconXof128::new().hash_xof(&msg, 48); + let msg = pattern(40); + + let reference = AsconXof128::new().xof(&msg, 48); + let mut x = AsconXof128::new(); + for &b in &msg { - x.absorb(&[b]).unwrap(); + x.do_update(&[b]); } - let mut o = [0u8; 48]; - x.squeeze_out(&mut o); - assert_eq!(o.to_vec(), xref, "XOF128 byte-at-a-time absorb mismatch"); + + let mut out = [0u8; 48]; + let written = x.into_squeezer().do_output_out(&mut out); + + assert_eq!(written, out.len()); + + assert_eq!(out.to_vec(), reference, "XOF128 byte-at-a-time absorb mismatch"); } #[test] -fn xof128_unsupported_partial_ops_return_err() { - let mut x = AsconXof128::new(); - assert!(x.absorb_last_partial_byte(0, 3).is_err()); - assert!(AsconXof128::new().squeeze_partial_byte_final(3).is_err()); - let mut b = 0u8; - assert!(AsconXof128::new().squeeze_partial_byte_final_out(3, &mut b).is_err()); +fn xof128_unsupported_partial_input_returns_err() { + // num_bits == 0 is byte-aligned input and must behave like ordinary finalization. + assert!(AsconXof128::new().into_squeezer_partial_bits(0xFF, 0).is_ok()); + + assert!(AsconXof128::new().do_final_partial_bits(0x80, 0).is_ok()); + + // Genuine partial-byte input is intentionally unsupported. + assert!(matches!( + AsconXof128::new().into_squeezer_partial_bits(0xA0, 3), + Err(HashError::InvalidInput(_)) + )); + + assert!(matches!( + AsconXof128::new().do_final_partial_bits(0xA0, 3), + Err(HashError::InvalidInput(_)) + )); + + let mut out = [0u8; 32]; + + assert!(matches!( + AsconXof128::new().do_final_partial_bits_out(0xA0, 3, &mut out), + Err(HashError::InvalidInput(_)) + )); + + // Eight bits is not a partial byte. + assert!(matches!( + AsconXof128::new().into_squeezer_partial_bits(0xFF, 8), + Err(HashError::InvalidLength(_)) + )); } #[test] -fn xof128_absorb_after_squeeze_errors() { +fn xof128_absorb_then_squeeze_type_transition() { let mut x = AsconXof128::new(); - x.absorb(b"data").unwrap(); - let mut out = [0u8; 8]; - x.squeeze_out(&mut out); - // Absorbing after squeezing has begun is a usage error; the trait API reports it as an error - // rather than panicking. - assert!(matches!(x.absorb(b"more"), Err(HashError::InvalidState(_)))); + x.do_update(b"data"); + + let mut squeezer = x.into_squeezer(); + + let first = squeezer.do_output(8); + let second = squeezer.do_output(8); + + let whole = AsconXof128::new().xof(b"data", 16); + + assert_eq!( + [first, second].concat(), + whole, + "successive reads must continue the same XOF stream" + ); + + // There is deliberately no runtime "absorb after squeeze" test anymore. + // into_squeezer() consumes AsconXof128, and the resulting squeezer does not implement + // Hash::do_update, so that invalid state cannot be expressed. } #[test] fn xof128_suspendable_state() { use bouncycastle_ascon::ascon_cxof128::AsconCXof128; use bouncycastle_core::errors::SuspendableError; - use bouncycastle_core::traits::Suspendable; use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableState; let data: Vec = (0..30u8).collect(); // Reference: uninterrupted absorb + squeeze. - let mut r = AsconXof128::new(); - r.absorb(&data).unwrap(); + let mut reference = AsconXof128::new(); + reference.do_update(&data); + let mut expected = [0u8; 40]; - r.squeeze_out(&mut expected); + reference.into_squeezer().do_output_out(&mut expected); // Suspend mid-absorb, resume, finish, and confirm the squeezed output matches. let mut x = AsconXof128::new(); - x.absorb(&data[..5]).unwrap(); + x.do_update(&data[..5]); + TestFrameworkSuspendableState::new().test(&x); let serialized = x.clone().suspend(); + let mut resumed = AsconXof128::from_suspended(serialized).unwrap(); - resumed.absorb(&data[5..]).unwrap(); + resumed.do_update(&data[5..]); + let mut out = [0u8; 40]; - resumed.squeeze_out(&mut out); + resumed.into_squeezer().do_output_out(&mut out); + assert_eq!(out, expected, "resumed XOF output must match uninterrupted output"); // A corrupted state tag must be rejected. let mut busted = serialized; busted[3] ^= 0xFF; + assert!(matches!(AsconXof128::from_suspended(busted), Err(SuspendableError::InvalidData))); - // Cross-type guard: an Ascon-CXOF128 state (same serialized length) must be rejected by - // Ascon-XOF128 via the state tag. + // Cross-type guard: an Ascon-CXOF128 state has the same serialized length but a different + // state tag, so Ascon-XOF128 must reject it. let mut c = AsconCXof128::with_customization(b"z").unwrap(); - c.absorb(&data).unwrap(); + c.do_update(&data); + let c_state = c.suspend(); + assert!(matches!(AsconXof128::from_suspended(c_state), Err(SuspendableError::InvalidData))); - // An inconsistent buf_pos/squeezing combination must be rejected: buf_pos == RATE (8) is only - // valid once squeezing has begun. + // An inconsistent buf_pos/squeezing combination must be rejected: buf_pos == RATE (8) + // is only valid once squeezing has begun. let mut bad = serialized; let len = bad.len(); - bad[len - 2] = 8; // buf_pos = RATE - bad[len - 1] = 0; // squeezing = false + + bad[len - 2] = 8; + bad[len - 1] = 0; + assert!(matches!(AsconXof128::from_suspended(bad), Err(SuspendableError::InvalidData))); - // Suspend mid-squeeze (not just mid-absorb) and confirm resuming continues the same stream. + // Suspend after squeezing has begun and confirm that restoring the squeezer continues the + // same stream. let mut sq = AsconXof128::new(); - sq.absorb(&data).unwrap(); + sq.do_update(&data); + + let mut sq = sq.into_squeezer(); + let mut head = [0u8; 5]; - sq.squeeze_out(&mut head); + sq.do_output_out(&mut head); + let squeezing_state = sq.clone().suspend(); - let mut resumed_sq = AsconXof128::from_suspended(squeezing_state).unwrap(); + + // A squeezing state must not be accepted as the absorbing AsconXof128 type. + assert!(matches!( + AsconXof128::from_suspended(squeezing_state), + Err(SuspendableError::InvalidData) + )); + + let mut resumed_sq = AsconXof128Squeezer::from_suspended(squeezing_state).unwrap(); + let mut tail = [0u8; 35]; - resumed_sq.squeeze_out(&mut tail); + resumed_sq.do_output_out(&mut tail); + let mut combined = Vec::new(); combined.extend_from_slice(&head); combined.extend_from_slice(&tail); + assert_eq!(combined, expected, "resuming mid-squeeze must continue the same output stream"); } diff --git a/crypto/factory/src/xof_factory.rs b/crypto/factory/src/xof_factory.rs index 027a64b2..e8eb8495 100644 --- a/crypto/factory/src/xof_factory.rs +++ b/crypto/factory/src/xof_factory.rs @@ -212,9 +212,7 @@ impl Hash for XOFFactory { match self { 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), - Self::AsconXof128(h) => { - h.do_final_partial_bits_out(partial_byte, num_bits, output) - } + Self::AsconXof128(h) => h.do_final_partial_bits_out(partial_byte, num_bits, output), } } @@ -244,12 +242,12 @@ impl XOF for XOFFactory { num_bits: usize, ) -> Result { Ok(match self { - Self::SHAKE128(h) => XOFFactorySqueezer::SHAKE128( - h.into_squeezer_partial_bits(partial_byte, num_bits)?, - ), - Self::SHAKE256(h) => XOFFactorySqueezer::SHAKE256( - h.into_squeezer_partial_bits(partial_byte, num_bits)?, - ), + Self::SHAKE128(h) => { + XOFFactorySqueezer::SHAKE128(h.into_squeezer_partial_bits(partial_byte, num_bits)?) + } + Self::SHAKE256(h) => { + XOFFactorySqueezer::SHAKE256(h.into_squeezer_partial_bits(partial_byte, num_bits)?) + } Self::AsconXof128(h) => XOFFactorySqueezer::AsconXof128( h.into_squeezer_partial_bits(partial_byte, num_bits)?, ), @@ -273,4 +271,4 @@ impl XOF for XOFFactory { Self::AsconXof128(h) => h.xof_out(data, output), } } -} \ No newline at end of file +} diff --git a/crypto/factory/tests/xof_factory_tests.rs b/crypto/factory/tests/xof_factory_tests.rs index a3d3d1b7..2dce009e 100644 --- a/crypto/factory/tests/xof_factory_tests.rs +++ b/crypto/factory/tests/xof_factory_tests.rs @@ -53,29 +53,18 @@ fn check_against(make: impl Fn() -> XOFFactory, ctx: &str) { let mut f = make(); f.do_update(MSG); - assert_eq!( - f.do_final_partial_bits(0x05, 3).unwrap(), - expected_bits, - "{ctx}: partial bits" - ); + 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!(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(_)) - ), + matches!(f.do_final_partial_bits(0xFF, 8), Err(HashError::InvalidLength(_))), "{ctx}: eight partial bits is not a partial byte" ); @@ -83,11 +72,7 @@ fn check_against(make: impl Fn() -> XOFFactory, ctx: &str) { let mut s = S::default(); s.do_update(MSG); let long = s.into_squeezer().do_output(3 * n); - assert_eq!( - &long[..n], - &expected[..], - "the direct type's hash is a prefix of its stream" - ); + assert_eq!(&long[..n], &expected[..], "the direct type's hash is a prefix of its stream"); let mut f = make(); f.do_update(MSG); @@ -95,71 +80,43 @@ fn check_against(make: impl Fn() -> XOFFactory, ctx: &str) { 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!(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_squeezer_partial_bits(0x05, 3) - .unwrap() - .do_output(n); + let want = s.into_squeezer_partial_bits(0x05, 3).unwrap().do_output(n); let mut f = make(); f.do_update(MSG); assert_eq!( - f.into_squeezer_partial_bits(0x05, 3) - .unwrap() - .do_output(n), + f.into_squeezer_partial_bits(0x05, 3).unwrap().do_output(n), want, "{ctx}: into_squeezer_partial_bits" ); let mut f = make(); f.do_update(MSG); - assert!(matches!( - f.into_squeezer_partial_bits(0xFF, 8), - Err(HashError::InvalidLength(_)) - )); + assert!(matches!(f.into_squeezer_partial_bits(0xFF, 8), Err(HashError::InvalidLength(_)))); // the one-shots assert_eq!(make().xof(MSG, 3 * n), long, "{ctx}: xof"); let mut out = vec![0xFFu8; 3 * n]; - assert_eq!( - make().xof_out(MSG, &mut out), - 3 * n, - "{ctx}: xof_out returns the length" - ); + assert_eq!(make().xof_out(MSG, &mut out), 3 * n, "{ctx}: xof_out returns the length"); assert_eq!(out, long, "{ctx}: 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", - ); + 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", - ); + check_against::(|| XOFFactory::new(SHAKE256_NAME).unwrap(), "SHAKE256 by constant"); + check_against::(|| XOFFactory::new("SHAKE256").unwrap(), "SHAKE256 by string"); } /// Verify that the Ascon-XOF128 factory registration resolves to the same implementation @@ -193,18 +150,9 @@ fn defaults() { #[test] fn unknown_names_are_refused() { - for name in [ - "SHAKE512", - "shake128", - "", - "cSHAKE128", - "Ascon-XOF999", - ] { + for name in ["SHAKE512", "shake128", "", "cSHAKE128", "Ascon-XOF999"] { assert!( - matches!( - XOFFactory::new(name), - Err(FactoryError::UnsupportedAlgorithm(_)) - ), + matches!(XOFFactory::new(name), Err(FactoryError::UnsupportedAlgorithm(_))), "{name:?} must not construct a XOF" ); } @@ -226,4 +174,4 @@ fn test_framework_xof() { MSG, &SHAKE256::new().xof(MSG, 100), ); -} \ No newline at end of file +} From 465e684b0ba7ac2ada9851cda7b2b157abbbf0a7 Mon Sep 17 00:00:00 2001 From: officialfrancismendoza Date: Thu, 17 Sep 2026 20:43:39 +0700 Subject: [PATCH 28/28] Minor doc fix to lib.rs given new XOF api (#119) --- crypto/ascon/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crypto/ascon/src/lib.rs b/crypto/ascon/src/lib.rs index 661aa6e9..b8be0622 100644 --- a/crypto/ascon/src/lib.rs +++ b/crypto/ascon/src/lib.rs @@ -15,6 +15,7 @@ //! ``` //! use bouncycastle_ascon::ascon_hash256::AsconHash256; //! use bouncycastle_core::traits::Hash; +//! use bouncycastle_core::traits::XOF; //! //! // One-shot: //! let digest = AsconHash256::digest(b"hello world"); @@ -73,7 +74,7 @@ //! use bouncycastle_ascon::ascon_xof128::AsconXof128; //! use bouncycastle_core::traits::XOF; //! -//! let out = AsconXof128::new().hash_xof(b"input", 64); +//! let out = AsconXof128::new().xof(b"input", 64); //! assert_eq!(out.len(), 64); //! ``` //!