diff --git a/digest/CHANGELOG.md b/digest/CHANGELOG.md index 9d0759017..53a5e2a97 100644 --- a/digest/CHANGELOG.md +++ b/digest/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.11.4 (UNRELEASED) +### Added +- Optional `impl:` section in the `buffer_ct_variable!` macro with support for `CustomizedInit` ([#2477]) + +[#2477]: https://github.com/RustCrypto/traits/pull/2477 + ## 0.11.3 (2026-04-03) ### Added - `dev::initialized_mac_test` function ([#2367]) diff --git a/digest/src/buffer_macros/variable.rs b/digest/src/buffer_macros/variable.rs index 6a3dea8a7..e28423523 100644 --- a/digest/src/buffer_macros/variable.rs +++ b/digest/src/buffer_macros/variable.rs @@ -1,5 +1,11 @@ /// Creates a buffered wrapper around block-level "core" type which implements variable output size traits /// with output size selected at compile time. +/// +/// Additional traits can be implemented for the generated type by listing them in the optional +/// trailing `impl: ...;` section, in the same way as it is done by the `buffer_fixed!` macro. +/// The section must be the last one, i.e. it goes after the `max_size:` section. Currently, the +/// only supported trait is `CustomizedInit`; it requires the "core" type to implement the +/// `VariableOutputCoreCustomized` trait. #[macro_export] macro_rules! buffer_ct_variable { ( @@ -208,4 +214,81 @@ macro_rules! buffer_ct_variable { } } }; + + // Same as the `exclude: SerializableState;` arm above, but additionally implements + // the traits listed in the `impl:` section. + ( + $(#[$attr:meta])* + $vis:vis struct $name:ident<$out_size:ident>($core_ty:ty); + exclude: SerializableState; + max_size: $max_size:ty; + impl: $($trait_name:ident)*; + ) => { + $crate::buffer_ct_variable!( + $(#[$attr])* + $vis struct $name<$out_size>($core_ty); + exclude: SerializableState; + max_size: $max_size; + ); + + $crate::buffer_ct_variable!( + impl_inner: $name<$out_size>($core_ty); + max_size: $max_size; + $($trait_name)*; + ); + }; + + // Same as the `max_size:`-only arm above, but additionally implements + // the traits listed in the `impl:` section. + ( + $(#[$attr:meta])* + $vis:vis struct $name:ident<$out_size:ident>($core_ty:ty); + max_size: $max_size:ty; + impl: $($trait_name:ident)*; + ) => { + $crate::buffer_ct_variable!( + $(#[$attr])* + $vis struct $name<$out_size>($core_ty); + max_size: $max_size; + ); + + $crate::buffer_ct_variable!( + impl_inner: $name<$out_size>($core_ty); + max_size: $max_size; + $($trait_name)*; + ); + }; + + // Terminates `impl_inner` sequences. + ( + impl_inner: $name:ident<$out_size:ident>($core_ty:ty); + max_size: $max_size:ty; + ; + ) => {}; + + // Implements `CustomizedInit` + ( + impl_inner: $name:ident<$out_size:ident>($core_ty:ty); + max_size: $max_size:ty; + CustomizedInit $($trait_name:ident)*; + ) => { + impl<$out_size> $crate::CustomizedInit for $name<$out_size> + where + $out_size: $crate::array::ArraySize + $crate::typenum::IsLessOrEqual<$max_size, Output = $crate::typenum::True>, + { + #[inline] + fn new_customized(customization: &[u8]) -> Self { + Self { + core: $crate::CustomizedInit::new_customized(customization), + buffer: Default::default(), + } + } + } + + $crate::buffer_ct_variable!( + impl_inner: $name<$out_size>($core_ty); + max_size: $max_size; + $($trait_name)*; + ); + }; } diff --git a/digest/tests/dummy_ct_variable.rs b/digest/tests/dummy_ct_variable.rs new file mode 100644 index 000000000..bcc89f294 --- /dev/null +++ b/digest/tests/dummy_ct_variable.rs @@ -0,0 +1,288 @@ +//! Tests against a pseudo-hash with output size selected at compile time. + +#![cfg(feature = "block-api")] + +mod block_api { + use core::fmt; + use digest::{ + HashMarker, InvalidOutputSize, Output, OutputSizeUser, + block_api::{ + AlgorithmName, Block, BlockSizeUser, Buffer, BufferKindUser, TruncSide, UpdateCore, + VariableOutputCore, VariableOutputCoreCustomized, + }, + common::hazmat::{DeserializeStateError, SerializableState, SerializedState}, + consts::U8, + }; + + /// Maximum output size of the test cores in bytes. + const MAX_OUTPUT_SIZE: usize = 8; + + /// Initial state derived from the requested output size. + fn seed(output_size: usize) -> u64 { + u64::try_from(output_size).unwrap_or_default() + } + + /// Core of primitive XOR hasher with variable output size for testing purposes + #[derive(Clone, Copy, Debug)] + pub struct VarHashCore { + state: u64, + } + + impl AlgorithmName for VarHashCore { + fn write_alg_name(f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.write_str("VarHash") + } + } + + impl BlockSizeUser for VarHashCore { + type BlockSize = U8; + } + + impl BufferKindUser for VarHashCore { + type BufferKind = block_buffer::Eager; + } + + impl OutputSizeUser for VarHashCore { + type OutputSize = U8; + } + + impl HashMarker for VarHashCore {} + + impl UpdateCore for VarHashCore { + fn update_blocks(&mut self, blocks: &[Block]) { + self.state = blocks + .iter() + .fold(self.state, |acc, block| acc ^ u64::from_le_bytes(block.0)); + } + } + + impl VariableOutputCore for VarHashCore { + const TRUNC_SIDE: TruncSide = TruncSide::Left; + + fn new(output_size: usize) -> Result { + (output_size <= MAX_OUTPUT_SIZE) + .then(|| Self { + state: seed(output_size), + }) + .ok_or(InvalidOutputSize) + } + + fn finalize_variable_core(&mut self, buffer: &mut Buffer, out: &mut Output) { + let block = buffer.pad_with_zeros(); + self.state ^= u64::from_le_bytes(block.0); + out.copy_from_slice(&self.state.to_le_bytes()); + } + } + + impl VariableOutputCoreCustomized for VarHashCore { + fn new_customized(customization: &[u8], output_size: usize) -> Self { + let state = customization + .iter() + .fold(seed(output_size), |acc, &byte| acc ^ u64::from(byte)); + Self { state } + } + } + + impl SerializableState for VarHashCore { + type SerializedStateSize = U8; + + fn serialize(&self) -> SerializedState { + self.state.to_le_bytes().into() + } + + fn deserialize( + serialized_state: &SerializedState, + ) -> Result { + Ok(Self { + state: u64::from_le_bytes(serialized_state.0), + }) + } + } + + /// Core of primitive XOR hasher with variable output size which deliberately does *not* + /// implement `VariableOutputCoreCustomized`, modelling the pre-existing + /// `buffer_ct_variable!` callers (groestl, kupyna). + /// + /// This is a fully independent type rather than a newtype around [`VarHashCore`] on purpose: + /// a delegating newtype would only type-check while both cores happen to share the same + /// `BlockSize`/`BufferKind`/`OutputSize`, so the regression guard could rot silently. + #[derive(Clone, Copy, Debug)] + pub struct PlainVarHashCore { + state: u64, + } + + impl AlgorithmName for PlainVarHashCore { + fn write_alg_name(f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.write_str("PlainVarHash") + } + } + + impl BlockSizeUser for PlainVarHashCore { + type BlockSize = U8; + } + + impl BufferKindUser for PlainVarHashCore { + type BufferKind = block_buffer::Eager; + } + + impl OutputSizeUser for PlainVarHashCore { + type OutputSize = U8; + } + + impl HashMarker for PlainVarHashCore {} + + impl UpdateCore for PlainVarHashCore { + fn update_blocks(&mut self, blocks: &[Block]) { + self.state = blocks + .iter() + .fold(self.state, |acc, block| acc ^ u64::from_le_bytes(block.0)); + } + } + + impl VariableOutputCore for PlainVarHashCore { + const TRUNC_SIDE: TruncSide = TruncSide::Left; + + fn new(output_size: usize) -> Result { + (output_size <= MAX_OUTPUT_SIZE) + .then(|| Self { + state: seed(output_size), + }) + .ok_or(InvalidOutputSize) + } + + fn finalize_variable_core(&mut self, buffer: &mut Buffer, out: &mut Output) { + let block = buffer.pad_with_zeros(); + self.state ^= u64::from_le_bytes(block.0); + out.copy_from_slice(&self.state.to_le_bytes()); + } + } + + impl SerializableState for PlainVarHashCore { + type SerializedStateSize = U8; + + fn serialize(&self) -> SerializedState { + self.state.to_le_bytes().into() + } + + fn deserialize( + serialized_state: &SerializedState, + ) -> Result { + Ok(Self { + state: u64::from_le_bytes(serialized_state.0), + }) + } + } +} + +use digest::{ + CustomizedInit, FixedOutput, TryCustomizedInit, Update, + common::hazmat::SerializableState, + consts::{U4, U8}, +}; + +// The four public call forms of `buffer_ct_variable!`: +// {`exclude:` present, absent} x {`impl:` present, absent}. + +digest::buffer_ct_variable!( + /// Primitive XOR hasher with output size selected at compile time + pub struct VarHash(block_api::VarHashCore); + max_size: U8; + impl: CustomizedInit; +); +digest::buffer_ct_variable!( + /// Primitive XOR hasher without `SerializableState` support + pub struct VarHashNoSer(block_api::VarHashCore); + exclude: SerializableState; + max_size: U8; + impl: CustomizedInit; +); +digest::buffer_ct_variable!( + /// Primitive XOR hasher over a core which does not support customization + pub struct PlainVarHash(block_api::PlainVarHashCore); + exclude: SerializableState; + max_size: U8; +); +digest::buffer_ct_variable!( + /// Primitive XOR hasher over a core which does not support customization, + /// with `SerializableState` support + pub struct PlainVarHashSer(block_api::PlainVarHashCore); + max_size: U8; +); + +/// check for `CustomizedInit` implementations +const _: () = { + const fn check_customized() {} + check_customized::>(); + check_customized::>(); + check_customized::>(); +}; + +#[test] +fn ct_variable_customized_init() { + // Empty customization string is equivalent to the default initialization + assert_eq!( + VarHash::::new_customized(&[]).finalize_fixed().0, + VarHash::::default().finalize_fixed().0, + ); + // Customization string reaches the core state: 8 ^ 1 ^ 2 == 0x0b + assert_eq!( + VarHash::::new_customized(&[0x01, 0x02]) + .finalize_fixed() + .0, + [0x0b, 0, 0, 0, 0, 0, 0, 0], + ); + // Updates are applied on top of the customized state: 0x0b ^ 0xff == 0xf4 + let mut hasher = VarHash::::new_customized(&[0x01, 0x02]); + hasher.update(&[0xff]); + assert_eq!(hasher.finalize_fixed().0, [0xf4, 0, 0, 0, 0, 0, 0, 0]); + // Output size reaches the core and the result is truncated: 4 ^ 1 ^ 2 == 0x07 + assert_eq!( + VarHash::::new_customized(&[0x01, 0x02]) + .finalize_fixed() + .0, + [0x07, 0, 0, 0], + ); + // The `exclude: SerializableState;` arm generates the same impl + assert_eq!( + VarHashNoSer::::new_customized(&[0x01, 0x02]) + .finalize_fixed() + .0, + [0x0b, 0, 0, 0, 0, 0, 0, 0], + ); +} + +#[test] +fn ct_variable_customized_init_composes_with_serializable_state() { + let mut hasher = VarHash::::new_customized(&[0x01, 0x02]); + hasher.update(&[0xff]); + let serialized = hasher.serialize(); + let restored = VarHash::::deserialize(&serialized).expect("state is valid"); + let restored_out = restored.finalize_fixed(); + assert_eq!(hasher.finalize_fixed(), restored_out); + + // The round trip above is symmetric, so on its own it is also satisfied by a + // `new_customized` which drops its argument. Pin that the serialized state + // really was customized by comparing against the uncustomized hasher. + let mut plain = VarHash::::default(); + plain.update(&[0xff]); + assert_ne!(restored_out, plain.finalize_fixed()); +} + +#[test] +fn ct_variable_without_customization() { + // Cores which do not implement `VariableOutputCoreCustomized` keep working + assert_eq!( + PlainVarHash::::default().finalize_fixed().0, + [0x08, 0, 0, 0, 0, 0, 0, 0], + ); + assert_eq!( + PlainVarHashSer::::default().finalize_fixed().0, + [0x04, 0, 0, 0], + ); + // The `max_size:`-only arm still emits `SerializableState` + let mut hasher = PlainVarHashSer::::default(); + hasher.update(&[0xff]); + let serialized = hasher.serialize(); + let restored = PlainVarHashSer::::deserialize(&serialized).expect("state is valid"); + assert_eq!(hasher.finalize_fixed(), restored.finalize_fixed()); +}