From 3832ef93e7ac20dfc95106340f62a1526deeaa08 Mon Sep 17 00:00:00 2001 From: farukest Date: Fri, 5 Dec 2025 10:26:02 +0300 Subject: [PATCH 1/4] feat: add SHA-256 and SHA-512 wrapper implementations Implements Hasher, HasherExt, and ElementHasher traits for SHA2 hash functions. Includes NIST test vectors and property-based tests. Closes #689 --- CHANGELOG.md | 1 + miden-crypto/src/hash/mod.rs | 3 + miden-crypto/src/hash/sha2/mod.rs | 448 ++++++++++++++++++++++++++++ miden-crypto/src/hash/sha2/tests.rs | 212 +++++++++++++ 4 files changed, 664 insertions(+) create mode 100644 miden-crypto/src/hash/sha2/mod.rs create mode 100644 miden-crypto/src/hash/sha2/tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b3aa6ee06..8721e9b0ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.20.0 (TBD) +- Added SHA-256 and SHA-512 hash function wrappers ([#692](https://github.com/0xMiden/crypto/pull/692)). - [BREAKING] Rename `MmrProof` to `MmrPath`, and introduce a new `MmrProof` with the leaf value included ([#656](https://github.com/0xMiden/crypto/pull/656)). - Added `+ Sync` bound to `StorageError` and `LargeSmtError` ([#680](https://github.com/0xMiden/crypto/pull/680)). - [BREAKING] Refactored `SmtProof` verification API to return `Result<(), SmtProofError>` ([#682](https://github.com/0xMiden/crypto/pull/682)). diff --git a/miden-crypto/src/hash/mod.rs b/miden-crypto/src/hash/mod.rs index 27b29aa4a7..84b4757c68 100644 --- a/miden-crypto/src/hash/mod.rs +++ b/miden-crypto/src/hash/mod.rs @@ -8,6 +8,9 @@ pub mod blake; /// Keccak hash function. pub mod keccak; +/// SHA-2 hash functions (SHA-256 and SHA-512). +pub mod sha2; + /// Poseidon2 hash function. pub mod poseidon2 { pub use super::algebraic_sponge::poseidon2::Poseidon2; diff --git a/miden-crypto/src/hash/sha2/mod.rs b/miden-crypto/src/hash/sha2/mod.rs new file mode 100644 index 0000000000..92619833b0 --- /dev/null +++ b/miden-crypto/src/hash/sha2/mod.rs @@ -0,0 +1,448 @@ +//! SHA2 hash function wrappers (SHA-256 and SHA-512). + +use alloc::string::String; +use core::{ + mem::size_of, + ops::Deref, + slice::{self, from_raw_parts}, +}; + +use sha2::Digest as Sha2Digest; + +use super::{Digest, ElementHasher, Felt, FieldElement, Hasher, HasherExt}; +use crate::utils::{ + ByteReader, ByteWriter, Deserializable, DeserializationError, HexParseError, Serializable, + bytes_to_hex_string, hex_to_bytes, +}; + +#[cfg(test)] +mod tests; + +// CONSTANTS +// ================================================================================================ + +const DIGEST256_BYTES: usize = 32; +const DIGEST512_BYTES: usize = 64; + +// SHA256 DIGEST +// ================================================================================================ + +/// SHA-256 digest (32 bytes). +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(into = "String", try_from = "&str"))] +pub struct Sha256Digest([u8; DIGEST256_BYTES]); + +impl Sha256Digest { + pub fn digests_as_bytes(digests: &[Sha256Digest]) -> &[u8] { + let p = digests.as_ptr(); + let len = digests.len() * DIGEST256_BYTES; + unsafe { slice::from_raw_parts(p as *const u8, len) } + } +} + +impl Default for Sha256Digest { + fn default() -> Self { + Self([0; DIGEST256_BYTES]) + } +} + +impl Deref for Sha256Digest { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for [u8; DIGEST256_BYTES] { + fn from(value: Sha256Digest) -> Self { + value.0 + } +} + +impl From<[u8; DIGEST256_BYTES]> for Sha256Digest { + fn from(value: [u8; DIGEST256_BYTES]) -> Self { + Self(value) + } +} + +impl From for String { + fn from(value: Sha256Digest) -> Self { + bytes_to_hex_string(value.as_bytes()) + } +} + +impl TryFrom<&str> for Sha256Digest { + type Error = HexParseError; + + fn try_from(value: &str) -> Result { + hex_to_bytes(value).map(|v| v.into()) + } +} + +impl Serializable for Sha256Digest { + fn write_into(&self, target: &mut W) { + target.write_bytes(&self.0); + } +} + +impl Deserializable for Sha256Digest { + fn read_from(source: &mut R) -> Result { + source.read_array().map(Self) + } +} + +impl Digest for Sha256Digest { + fn as_bytes(&self) -> [u8; 32] { + self.0 + } +} + +// SHA256 HASHER +// ================================================================================================ + +/// SHA-256 hash function. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Sha256; + +impl HasherExt for Sha256 { + fn hash_iter<'a>(slices: impl Iterator) -> Self::Digest { + let mut hasher = sha2::Sha256::new(); + for slice in slices { + hasher.update(slice); + } + Sha256Digest(hasher.finalize().into()) + } +} + +impl Hasher for Sha256 { + /// SHA-256 collision resistance is 128-bits for 32-bytes output. + const COLLISION_RESISTANCE: u32 = 128; + + type Digest = Sha256Digest; + + fn hash(bytes: &[u8]) -> Self::Digest { + let mut hasher = sha2::Sha256::new(); + hasher.update(bytes); + + Sha256Digest(hasher.finalize().into()) + } + + fn merge(values: &[Self::Digest; 2]) -> Self::Digest { + Self::hash(prepare_merge(values)) + } + + fn merge_many(values: &[Self::Digest]) -> Self::Digest { + let data = Sha256Digest::digests_as_bytes(values); + let mut hasher = sha2::Sha256::new(); + hasher.update(data); + + Sha256Digest(hasher.finalize().into()) + } + + fn merge_with_int(seed: Self::Digest, value: u64) -> Self::Digest { + let mut hasher = sha2::Sha256::new(); + hasher.update(seed.0); + hasher.update(value.to_le_bytes()); + + Sha256Digest(hasher.finalize().into()) + } +} + +impl ElementHasher for Sha256 { + type BaseField = Felt; + + fn hash_elements(elements: &[E]) -> Self::Digest + where + E: FieldElement, + { + Sha256Digest(hash_elements_256(elements)) + } +} + +impl Sha256 { + /// Returns a hash of the provided sequence of bytes. + #[inline(always)] + pub fn hash(bytes: &[u8]) -> Sha256Digest { + ::hash(bytes) + } + + /// Returns a hash of two digests. This method is intended for use in construction of + /// Merkle trees and verification of Merkle paths. + #[inline(always)] + pub fn merge(values: &[Sha256Digest; 2]) -> Sha256Digest { + ::merge(values) + } + + /// Returns a hash of the provided field elements. + #[inline(always)] + pub fn hash_elements(elements: &[E]) -> Sha256Digest + where + E: FieldElement, + { + ::hash_elements(elements) + } + + /// Hashes an iterator of byte slices. + #[inline(always)] + pub fn hash_iter<'a>(slices: impl Iterator) -> Sha256Digest { + ::hash_iter(slices) + } +} + +// SHA512 DIGEST +// ================================================================================================ + +/// SHA-512 digest (64 bytes). +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(into = "String", try_from = "&str"))] +pub struct Sha512Digest([u8; DIGEST512_BYTES]); + +impl Sha512Digest { + pub fn digests_as_bytes(digests: &[Sha512Digest]) -> &[u8] { + let p = digests.as_ptr(); + let len = digests.len() * DIGEST512_BYTES; + unsafe { slice::from_raw_parts(p as *const u8, len) } + } +} + +impl Default for Sha512Digest { + fn default() -> Self { + Self([0; DIGEST512_BYTES]) + } +} + +impl Deref for Sha512Digest { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for [u8; DIGEST512_BYTES] { + fn from(value: Sha512Digest) -> Self { + value.0 + } +} + +impl From<[u8; DIGEST512_BYTES]> for Sha512Digest { + fn from(value: [u8; DIGEST512_BYTES]) -> Self { + Self(value) + } +} + +impl From for String { + fn from(value: Sha512Digest) -> Self { + bytes_to_hex_string(value.0) + } +} + +impl TryFrom<&str> for Sha512Digest { + type Error = HexParseError; + + fn try_from(value: &str) -> Result { + hex_to_bytes(value).map(|v| v.into()) + } +} + +impl Serializable for Sha512Digest { + fn write_into(&self, target: &mut W) { + target.write_bytes(&self.0); + } +} + +impl Deserializable for Sha512Digest { + fn read_from(source: &mut R) -> Result { + source.read_array().map(Self) + } +} + +impl Digest for Sha512Digest { + fn as_bytes(&self) -> [u8; 32] { + // Return first 32 bytes of the 64-byte digest + let mut result = [0u8; 32]; + result.copy_from_slice(&self.0[..32]); + result + } +} + +// SHA512 HASHER +// ================================================================================================ + +/// SHA-512 hash function. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Sha512; + +impl HasherExt for Sha512 { + fn hash_iter<'a>(slices: impl Iterator) -> Self::Digest { + let mut hasher = sha2::Sha512::new(); + for slice in slices { + hasher.update(slice); + } + Sha512Digest(hasher.finalize().into()) + } +} + +impl Hasher for Sha512 { + /// SHA-512 collision resistance is 256-bits for 64-bytes output. + const COLLISION_RESISTANCE: u32 = 256; + + type Digest = Sha512Digest; + + fn hash(bytes: &[u8]) -> Self::Digest { + let mut hasher = sha2::Sha512::new(); + hasher.update(bytes); + + Sha512Digest(hasher.finalize().into()) + } + + fn merge(values: &[Self::Digest; 2]) -> Self::Digest { + Self::hash(prepare_merge(values)) + } + + fn merge_many(values: &[Self::Digest]) -> Self::Digest { + let data = Sha512Digest::digests_as_bytes(values); + let mut hasher = sha2::Sha512::new(); + hasher.update(data); + + Sha512Digest(hasher.finalize().into()) + } + + fn merge_with_int(seed: Self::Digest, value: u64) -> Self::Digest { + let mut hasher = sha2::Sha512::new(); + hasher.update(seed.0); + hasher.update(value.to_le_bytes()); + + Sha512Digest(hasher.finalize().into()) + } +} + +impl ElementHasher for Sha512 { + type BaseField = Felt; + + fn hash_elements(elements: &[E]) -> Self::Digest + where + E: FieldElement, + { + Sha512Digest(hash_elements_512(elements)) + } +} + +impl Sha512 { + /// Returns a hash of the provided sequence of bytes. + #[inline(always)] + pub fn hash(bytes: &[u8]) -> Sha512Digest { + ::hash(bytes) + } + + /// Returns a hash of two digests. This method is intended for use in construction of + /// Merkle trees and verification of Merkle paths. + #[inline(always)] + pub fn merge(values: &[Sha512Digest; 2]) -> Sha512Digest { + ::merge(values) + } + + /// Returns a hash of the provided field elements. + #[inline(always)] + pub fn hash_elements(elements: &[E]) -> Sha512Digest + where + E: FieldElement, + { + ::hash_elements(elements) + } + + /// Hashes an iterator of byte slices. + #[inline(always)] + pub fn hash_iter<'a>(slices: impl Iterator) -> Sha512Digest { + ::hash_iter(slices) + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Hash the elements into bytes for SHA-256. +fn hash_elements_256(elements: &[E]) -> [u8; DIGEST256_BYTES] +where + E: FieldElement, +{ + // don't leak assumptions from felt and check its actual implementation. + // this is a compile-time branch so it is for free + let digest = if Felt::IS_CANONICAL { + let mut hasher = sha2::Sha256::new(); + hasher.update(E::elements_as_bytes(elements)); + hasher.finalize() + } else { + let mut hasher = sha2::Sha256::new(); + // SHA-256 has a block size of 64 bytes, so we can absorb 64 bytes per block. + // We move the elements into the hasher via the buffer to give the CPU a chance + // to process multiple element-to-byte conversions in parallel. + let mut buf = [0_u8; 64]; + let mut chunk_iter = E::slice_as_base_elements(elements).chunks_exact(8); + for chunk in chunk_iter.by_ref() { + for i in 0..8 { + buf[i * 8..(i + 1) * 8].copy_from_slice(&chunk[i].as_int().to_le_bytes()); + } + hasher.update(buf); + } + + for element in chunk_iter.remainder() { + hasher.update(element.as_int().to_le_bytes()); + } + + hasher.finalize() + }; + digest.into() +} + +/// Hash the elements into bytes for SHA-512. +fn hash_elements_512(elements: &[E]) -> [u8; DIGEST512_BYTES] +where + E: FieldElement, +{ + // don't leak assumptions from felt and check its actual implementation. + // this is a compile-time branch so it is for free + let digest = if Felt::IS_CANONICAL { + let mut hasher = sha2::Sha512::new(); + hasher.update(E::elements_as_bytes(elements)); + hasher.finalize() + } else { + let mut hasher = sha2::Sha512::new(); + // SHA-512 has a block size of 128 bytes, so we can absorb 128 bytes per block. + // We move the elements into the hasher via the buffer to give the CPU a chance + // to process multiple element-to-byte conversions in parallel. + let mut buf = [0_u8; 128]; + let mut chunk_iter = E::slice_as_base_elements(elements).chunks_exact(16); + for chunk in chunk_iter.by_ref() { + for i in 0..16 { + buf[i * 8..(i + 1) * 8].copy_from_slice(&chunk[i].as_int().to_le_bytes()); + } + hasher.update(buf); + } + + for element in chunk_iter.remainder() { + hasher.update(element.as_int().to_le_bytes()); + } + + hasher.finalize() + }; + digest.into() +} + +/// Cast the slice into contiguous bytes. +fn prepare_merge(args: &[D; N]) -> &[u8] +where + D: Deref, +{ + // compile-time assertion + assert!(N > 0, "N shouldn't represent an empty slice!"); + let values = args.as_ptr() as *const u8; + let len = size_of::() * N; + // safety: the values are tested to be contiguous + let bytes = unsafe { from_raw_parts(values, len) }; + debug_assert_eq!(args[0].deref(), &bytes[..len / N]); + bytes +} diff --git a/miden-crypto/src/hash/sha2/tests.rs b/miden-crypto/src/hash/sha2/tests.rs new file mode 100644 index 0000000000..1e2f2b7e71 --- /dev/null +++ b/miden-crypto/src/hash/sha2/tests.rs @@ -0,0 +1,212 @@ +use alloc::vec::Vec; + +use proptest::prelude::*; +use rand_utils::rand_vector; + +use super::*; + +// SHA-256 TESTS +// ================================================================================================ + +#[test] +fn sha256_hash_elements() { + // test multiple of 8 + let elements = rand_vector::(16); + let expected = compute_expected_sha256_element_hash(&elements); + let actual: [u8; DIGEST256_BYTES] = hash_elements_256(&elements); + assert_eq!(&expected, &actual); + + // test not multiple of 8 + let elements = rand_vector::(17); + let expected = compute_expected_sha256_element_hash(&elements); + let actual: [u8; DIGEST256_BYTES] = hash_elements_256(&elements); + assert_eq!(&expected, &actual); +} + +proptest! { + #[test] + fn sha256_wont_panic_with_arbitrary_input(ref vec in any::>()) { + Sha256::hash(vec); + } + + #[test] + fn sha256_hash_iter_matches_hash(ref slices in any::>>()) { + // Concatenate all slices to create the expected result + let mut concatenated = Vec::new(); + for slice in slices.iter() { + concatenated.extend_from_slice(slice); + } + let expected = Sha256::hash(&concatenated); + + // Test with iterator + let actual = Sha256::hash_iter(slices.iter().map(|v| v.as_slice())); + assert_eq!(expected, actual); + + // Test with empty slices list + let empty_actual = Sha256::hash_iter(core::iter::empty()); + let empty_expected = Sha256::hash(b""); + assert_eq!(empty_expected, empty_actual); + + // Test with single slice + if let Some(single_slice) = slices.first() { + let single_actual = Sha256::hash_iter(core::iter::once(single_slice.as_slice())); + let single_expected = Sha256::hash(single_slice); + assert_eq!(single_expected, single_actual); + } + } +} + +#[test] +fn test_sha256_nist_test_vectors() { + for (i, vector) in SHA256_TEST_VECTORS.iter().enumerate() { + let result = Sha256::hash(vector.input); + let expected = hex::decode(vector.expected).unwrap(); + assert_eq!( + result.to_vec(), + expected, + "SHA-256 test vector {} failed: {}", + i, + vector.description + ); + } +} + +// SHA-512 TESTS +// ================================================================================================ + +#[test] +fn sha512_hash_elements() { + // test multiple of 16 + let elements = rand_vector::(32); + let expected = compute_expected_sha512_element_hash(&elements); + let actual: [u8; DIGEST512_BYTES] = hash_elements_512(&elements); + assert_eq!(&expected, &actual); + + // test not multiple of 16 + let elements = rand_vector::(17); + let expected = compute_expected_sha512_element_hash(&elements); + let actual: [u8; DIGEST512_BYTES] = hash_elements_512(&elements); + assert_eq!(&expected, &actual); +} + +proptest! { + #[test] + fn sha512_wont_panic_with_arbitrary_input(ref vec in any::>()) { + Sha512::hash(vec); + } + + #[test] + fn sha512_hash_iter_matches_hash(ref slices in any::>>()) { + // Concatenate all slices to create the expected result + let mut concatenated = Vec::new(); + for slice in slices.iter() { + concatenated.extend_from_slice(slice); + } + let expected = Sha512::hash(&concatenated); + + // Test with iterator + let actual = Sha512::hash_iter(slices.iter().map(|v| v.as_slice())); + assert_eq!(expected, actual); + + // Test with empty slices list + let empty_actual = Sha512::hash_iter(core::iter::empty()); + let empty_expected = Sha512::hash(b""); + assert_eq!(empty_expected, empty_actual); + + // Test with single slice + if let Some(single_slice) = slices.first() { + let single_actual = Sha512::hash_iter(core::iter::once(single_slice.as_slice())); + let single_expected = Sha512::hash(single_slice); + assert_eq!(single_expected, single_actual); + } + } +} + +#[test] +fn test_sha512_nist_test_vectors() { + for (i, vector) in SHA512_TEST_VECTORS.iter().enumerate() { + let result = Sha512::hash(vector.input); + let expected = hex::decode(vector.expected).unwrap(); + assert_eq!( + result.to_vec(), + expected, + "SHA-512 test vector {} failed: {}", + i, + vector.description + ); + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +fn compute_expected_sha256_element_hash(elements: &[Felt]) -> [u8; DIGEST256_BYTES] { + let mut bytes = Vec::new(); + for element in elements.iter() { + bytes.extend_from_slice(&element.as_int().to_le_bytes()); + } + let mut hasher = sha2::Sha256::new(); + hasher.update(&bytes); + + hasher.finalize().into() +} + +fn compute_expected_sha512_element_hash(elements: &[Felt]) -> [u8; DIGEST512_BYTES] { + let mut bytes = Vec::new(); + for element in elements.iter() { + bytes.extend_from_slice(&element.as_int().to_le_bytes()); + } + let mut hasher = sha2::Sha512::new(); + hasher.update(&bytes); + + hasher.finalize().into() +} + +struct TestVector { + input: &'static [u8], + expected: &'static str, + description: &'static str, +} + +// TEST VECTORS +// ================================================================================================ + +// NIST test vectors for SHA-256 +// https://csrc.nist.gov/projects/cryptographic-standards-and-guidelines/example-values +const SHA256_TEST_VECTORS: &[TestVector] = &[ + TestVector { + input: b"", + expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + description: "Empty input", + }, + TestVector { + input: b"abc", + expected: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + description: "String 'abc'", + }, + TestVector { + input: b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + expected: "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1", + description: "448 bits message", + }, +]; + +// NIST test vectors for SHA-512 +// https://csrc.nist.gov/projects/cryptographic-standards-and-guidelines/example-values +const SHA512_TEST_VECTORS: &[TestVector] = &[ + TestVector { + input: b"", + expected: "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + description: "Empty input", + }, + TestVector { + input: b"abc", + expected: "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f", + description: "String 'abc'", + }, + TestVector { + input: b"abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", + expected: "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909", + description: "896 bits message", + }, +]; From abf947bc3b397ea49bd07b3c8a97cda45a1914d2 Mon Sep 17 00:00:00 2001 From: farukest Date: Sun, 7 Dec 2025 17:42:24 +0300 Subject: [PATCH 2/4] Update NIST test vector links to specific PDF documents --- miden-crypto/src/hash/sha2/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miden-crypto/src/hash/sha2/tests.rs b/miden-crypto/src/hash/sha2/tests.rs index 1e2f2b7e71..9119585543 100644 --- a/miden-crypto/src/hash/sha2/tests.rs +++ b/miden-crypto/src/hash/sha2/tests.rs @@ -172,7 +172,7 @@ struct TestVector { // ================================================================================================ // NIST test vectors for SHA-256 -// https://csrc.nist.gov/projects/cryptographic-standards-and-guidelines/example-values +// https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/SHA256.pdf const SHA256_TEST_VECTORS: &[TestVector] = &[ TestVector { input: b"", @@ -192,7 +192,7 @@ const SHA256_TEST_VECTORS: &[TestVector] = &[ ]; // NIST test vectors for SHA-512 -// https://csrc.nist.gov/projects/cryptographic-standards-and-guidelines/example-values +// https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/SHA512.pdf const SHA512_TEST_VECTORS: &[TestVector] = &[ TestVector { input: b"", From 97a1ab3a7df34255c37aa8ff31a51c925a1a51e5 Mon Sep 17 00:00:00 2001 From: farukest Date: Tue, 9 Dec 2025 12:40:25 +0300 Subject: [PATCH 3/4] Add safety improvements for unsafe code and SHA-512 truncation docs - Add #[repr(transparent)] to Sha256Digest and Sha512Digest for safe pointer casting in digests_as_bytes() - Document that Sha512Digest::as_bytes() returns truncated SHA-512, NOT SHA-512/256 (different IVs per FIPS 180-4) - Add memory layout tests to verify struct size and alignment assumptions - Add digests_as_bytes correctness tests for both digest types - Add PropTest for merge_many to verify unsafe code produces correct results - Add test demonstrating SHA-512 truncation vs SHA-512/256 difference --- miden-crypto/src/hash/sha2/mod.rs | 17 +++- miden-crypto/src/hash/sha2/tests.rs | 143 ++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) diff --git a/miden-crypto/src/hash/sha2/mod.rs b/miden-crypto/src/hash/sha2/mod.rs index 92619833b0..ae81b1ed48 100644 --- a/miden-crypto/src/hash/sha2/mod.rs +++ b/miden-crypto/src/hash/sha2/mod.rs @@ -1,4 +1,11 @@ //! SHA2 hash function wrappers (SHA-256 and SHA-512). +//! +//! # Note on SHA-512 Digest trait implementation +//! +//! The [Sha512Digest::as_bytes] method returns only the first 32 bytes of the full 64-byte +//! SHA-512 digest. This is truncated SHA-512, NOT SHA-512/256 (which uses different +//! initialization vectors as per FIPS 180-4). The full 64-byte digest is always available +//! via the [Deref] implementation. use alloc::string::String; use core::{ @@ -31,6 +38,7 @@ const DIGEST512_BYTES: usize = 64; #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(into = "String", try_from = "&str"))] +#[repr(transparent)] pub struct Sha256Digest([u8; DIGEST256_BYTES]); impl Sha256Digest { @@ -198,6 +206,7 @@ impl Sha256 { #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(into = "String", try_from = "&str"))] +#[repr(transparent)] pub struct Sha512Digest([u8; DIGEST512_BYTES]); impl Sha512Digest { @@ -261,8 +270,14 @@ impl Deserializable for Sha512Digest { } impl Digest for Sha512Digest { + /// Returns the first 32 bytes of the 64-byte SHA-512 digest. + /// + /// # Note + /// + /// This returns truncated SHA-512, NOT SHA-512/256. SHA-512/256 uses different + /// initialization vectors (IVs) as specified in FIPS 180-4 and produces different + /// output. For the full 64-byte digest, use the [Deref] implementation. fn as_bytes(&self) -> [u8; 32] { - // Return first 32 bytes of the 64-byte digest let mut result = [0u8; 32]; result.copy_from_slice(&self.0[..32]); result diff --git a/miden-crypto/src/hash/sha2/tests.rs b/miden-crypto/src/hash/sha2/tests.rs index 9119585543..a1c3983027 100644 --- a/miden-crypto/src/hash/sha2/tests.rs +++ b/miden-crypto/src/hash/sha2/tests.rs @@ -210,3 +210,146 @@ const SHA512_TEST_VECTORS: &[TestVector] = &[ description: "896 bits message", }, ]; + +// MEMORY LAYOUT TESTS +// ================================================================================================ + +#[test] +fn test_memory_layout_assumptions() { + // Verify struct size equals inner array size (required for safe pointer casting) + assert_eq!( + core::mem::size_of::(), + core::mem::size_of::<[u8; 32]>() + ); + + // Verify alignment + assert_eq!( + core::mem::align_of::(), + core::mem::align_of::<[u8; 32]>() + ); + + // Same for Sha512Digest + assert_eq!( + core::mem::size_of::(), + core::mem::size_of::<[u8; 64]>() + ); + assert_eq!( + core::mem::align_of::(), + core::mem::align_of::<[u8; 64]>() + ); +} + +#[test] +fn test_sha256_digests_as_bytes_correctness() { + let digests = vec![ + Sha256Digest([1u8; 32]), + Sha256Digest([2u8; 32]), + Sha256Digest([3u8; 32]), + ]; + + let bytes = Sha256Digest::digests_as_bytes(&digests); + + // Verify length + assert_eq!(bytes.len(), 96); + + // Verify contiguous layout + assert_eq!(&bytes[0..32], &[1u8; 32]); + assert_eq!(&bytes[32..64], &[2u8; 32]); + assert_eq!(&bytes[64..96], &[3u8; 32]); +} + +#[test] +fn test_sha512_digests_as_bytes_correctness() { + let digests = vec![ + Sha512Digest([1u8; 64]), + Sha512Digest([2u8; 64]), + Sha512Digest([3u8; 64]), + ]; + + let bytes = Sha512Digest::digests_as_bytes(&digests); + + // Verify length + assert_eq!(bytes.len(), 192); + + // Verify contiguous layout + assert_eq!(&bytes[0..64], &[1u8; 64]); + assert_eq!(&bytes[64..128], &[2u8; 64]); + assert_eq!(&bytes[128..192], &[3u8; 64]); +} + +// SHA-512 TRUNCATION TEST +// ================================================================================================ + +/// This test demonstrates that Sha512Digest::as_bytes() returns truncated SHA-512, +/// NOT SHA-512/256. SHA-512/256 uses different initialization vectors and produces +/// completely different output. +#[test] +fn test_sha512_truncation_not_sha512_256() { + // Hash "abc" with SHA-512 + let sha512_digest = Sha512::hash(b"abc"); + + // Get the first 32 bytes (truncated SHA-512) + let truncated: [u8; 32] = sha512_digest.as_bytes(); + + // SHA-512("abc") = ddaf35a193617aba... + // First 32 bytes should be: ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a + let expected_truncated = hex::decode( + "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a" + ).unwrap(); + + assert_eq!(truncated.to_vec(), expected_truncated, + "Truncated SHA-512 should be the first 32 bytes of full SHA-512 digest"); + + // SHA-512/256("abc") would be: 53048e2681941ef99b2e29b76b4c7dabe4c2d0c634fc6d46e0e2f13107e7af23 + // This is completely different from truncated SHA-512 + let sha512_256_expected = hex::decode( + "53048e2681941ef99b2e29b76b4c7dabe4c2d0c634fc6d46e0e2f13107e7af23" + ).unwrap(); + + assert_ne!(truncated.to_vec(), sha512_256_expected, + "Truncated SHA-512 should NOT equal SHA-512/256 (different IVs per FIPS 180-4)"); +} + +// MERGE_MANY CORRECTNESS TESTS +// ================================================================================================ + +proptest! { + #[test] + fn sha256_merge_many_matches_concatenated_hash( + digests in prop::collection::vec(any::<[u8; 32]>(), 1..10) + ) { + let sha_digests: Vec = + digests.iter().map(|&d| Sha256Digest(d)).collect(); + + // Method 1: Using merge_many (uses unsafe digests_as_bytes) + let result1 = Sha256::merge_many(&sha_digests); + + // Method 2: Safe concatenation for comparison + let mut concat = Vec::new(); + for d in &sha_digests { + concat.extend_from_slice(&d.0); + } + let result2 = Sha256::hash(&concat); + + // Should produce identical results + assert_eq!(result1, result2); + } + + #[test] + fn sha512_merge_many_matches_concatenated_hash( + digests in prop::collection::vec(any::<[u8; 64]>(), 1..10) + ) { + let sha_digests: Vec = + digests.iter().map(|&d| Sha512Digest(d)).collect(); + + let result1 = Sha512::merge_many(&sha_digests); + + let mut concat = Vec::new(); + for d in &sha_digests { + concat.extend_from_slice(&d.0); + } + let result2 = Sha512::hash(&concat); + + assert_eq!(result1, result2); + } +} From 47fd1392045858934c6900292558f76fe42513f7 Mon Sep 17 00:00:00 2001 From: farukest Date: Wed, 10 Dec 2025 03:46:45 +0300 Subject: [PATCH 4/4] Remove Digest/Hasher trait implementations for Sha512 SHA-512 produces 64-byte output which is incompatible with Winterfell's Digest trait (requires 32-byte as_bytes()). Standalone methods are kept. See https://github.com/facebook/winterfell/issues/406 --- miden-crypto/src/hash/sha2/mod.rs | 107 ++++++++-------------------- miden-crypto/src/hash/sha2/tests.rs | 65 ++--------------- 2 files changed, 37 insertions(+), 135 deletions(-) diff --git a/miden-crypto/src/hash/sha2/mod.rs b/miden-crypto/src/hash/sha2/mod.rs index ae81b1ed48..a4c7609cd1 100644 --- a/miden-crypto/src/hash/sha2/mod.rs +++ b/miden-crypto/src/hash/sha2/mod.rs @@ -1,11 +1,14 @@ //! SHA2 hash function wrappers (SHA-256 and SHA-512). //! -//! # Note on SHA-512 Digest trait implementation +//! # Note on SHA-512 and the Digest trait //! -//! The [Sha512Digest::as_bytes] method returns only the first 32 bytes of the full 64-byte -//! SHA-512 digest. This is truncated SHA-512, NOT SHA-512/256 (which uses different -//! initialization vectors as per FIPS 180-4). The full 64-byte digest is always available -//! via the [Deref] implementation. +//! `Sha512Digest` does not implement the `Digest` trait because Winterfell's `Digest` trait +//! requires a fixed 32-byte output via `as_bytes() -> [u8; 32]`, which is incompatible with +//! SHA-512's native 64-byte output. Truncating to 32 bytes would create confusion with +//! SHA-512/256 (which uses different initialization vectors per FIPS 180-4). +//! +//! See for a proposal to make the +//! `Digest` trait generic over output size. use alloc::string::String; use core::{ @@ -269,96 +272,44 @@ impl Deserializable for Sha512Digest { } } -impl Digest for Sha512Digest { - /// Returns the first 32 bytes of the 64-byte SHA-512 digest. - /// - /// # Note - /// - /// This returns truncated SHA-512, NOT SHA-512/256. SHA-512/256 uses different - /// initialization vectors (IVs) as specified in FIPS 180-4 and produces different - /// output. For the full 64-byte digest, use the [Deref] implementation. - fn as_bytes(&self) -> [u8; 32] { - let mut result = [0u8; 32]; - result.copy_from_slice(&self.0[..32]); - result - } -} +// NOTE: Sha512 intentionally does not implement the Hasher, HasherExt, ElementHasher, +// or Digest traits. See the module-level documentation for details. // SHA512 HASHER // ================================================================================================ /// SHA-512 hash function. +/// +/// Unlike [Sha256], this struct does not implement the [Hasher], [HasherExt], or [ElementHasher] +/// traits because those traits require [Digest], which mandates a 32-byte output. SHA-512 +/// produces a 64-byte digest, and truncating it would create confusion with SHA-512/256. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct Sha512; -impl HasherExt for Sha512 { - fn hash_iter<'a>(slices: impl Iterator) -> Self::Digest { - let mut hasher = sha2::Sha512::new(); - for slice in slices { - hasher.update(slice); - } - Sha512Digest(hasher.finalize().into()) - } -} - -impl Hasher for Sha512 { - /// SHA-512 collision resistance is 256-bits for 64-bytes output. - const COLLISION_RESISTANCE: u32 = 256; - - type Digest = Sha512Digest; - - fn hash(bytes: &[u8]) -> Self::Digest { +impl Sha512 { + /// Returns a hash of the provided sequence of bytes. + #[inline(always)] + pub fn hash(bytes: &[u8]) -> Sha512Digest { let mut hasher = sha2::Sha512::new(); hasher.update(bytes); - Sha512Digest(hasher.finalize().into()) } - fn merge(values: &[Self::Digest; 2]) -> Self::Digest { + /// Returns a hash of two digests. This method is intended for use in construction of + /// Merkle trees and verification of Merkle paths. + #[inline(always)] + pub fn merge(values: &[Sha512Digest; 2]) -> Sha512Digest { Self::hash(prepare_merge(values)) } - fn merge_many(values: &[Self::Digest]) -> Self::Digest { + /// Returns a hash of the provided digests. + #[inline(always)] + pub fn merge_many(values: &[Sha512Digest]) -> Sha512Digest { let data = Sha512Digest::digests_as_bytes(values); let mut hasher = sha2::Sha512::new(); hasher.update(data); - - Sha512Digest(hasher.finalize().into()) - } - - fn merge_with_int(seed: Self::Digest, value: u64) -> Self::Digest { - let mut hasher = sha2::Sha512::new(); - hasher.update(seed.0); - hasher.update(value.to_le_bytes()); - Sha512Digest(hasher.finalize().into()) } -} - -impl ElementHasher for Sha512 { - type BaseField = Felt; - - fn hash_elements(elements: &[E]) -> Self::Digest - where - E: FieldElement, - { - Sha512Digest(hash_elements_512(elements)) - } -} - -impl Sha512 { - /// Returns a hash of the provided sequence of bytes. - #[inline(always)] - pub fn hash(bytes: &[u8]) -> Sha512Digest { - ::hash(bytes) - } - - /// Returns a hash of two digests. This method is intended for use in construction of - /// Merkle trees and verification of Merkle paths. - #[inline(always)] - pub fn merge(values: &[Sha512Digest; 2]) -> Sha512Digest { - ::merge(values) - } /// Returns a hash of the provided field elements. #[inline(always)] @@ -366,13 +317,17 @@ impl Sha512 { where E: FieldElement, { - ::hash_elements(elements) + Sha512Digest(hash_elements_512(elements)) } /// Hashes an iterator of byte slices. #[inline(always)] pub fn hash_iter<'a>(slices: impl Iterator) -> Sha512Digest { - ::hash_iter(slices) + let mut hasher = sha2::Sha512::new(); + for slice in slices { + hasher.update(slice); + } + Sha512Digest(hasher.finalize().into()) } } diff --git a/miden-crypto/src/hash/sha2/tests.rs b/miden-crypto/src/hash/sha2/tests.rs index a1c3983027..68ab28dfe4 100644 --- a/miden-crypto/src/hash/sha2/tests.rs +++ b/miden-crypto/src/hash/sha2/tests.rs @@ -217,35 +217,19 @@ const SHA512_TEST_VECTORS: &[TestVector] = &[ #[test] fn test_memory_layout_assumptions() { // Verify struct size equals inner array size (required for safe pointer casting) - assert_eq!( - core::mem::size_of::(), - core::mem::size_of::<[u8; 32]>() - ); + assert_eq!(core::mem::size_of::(), core::mem::size_of::<[u8; 32]>()); // Verify alignment - assert_eq!( - core::mem::align_of::(), - core::mem::align_of::<[u8; 32]>() - ); + assert_eq!(core::mem::align_of::(), core::mem::align_of::<[u8; 32]>()); // Same for Sha512Digest - assert_eq!( - core::mem::size_of::(), - core::mem::size_of::<[u8; 64]>() - ); - assert_eq!( - core::mem::align_of::(), - core::mem::align_of::<[u8; 64]>() - ); + assert_eq!(core::mem::size_of::(), core::mem::size_of::<[u8; 64]>()); + assert_eq!(core::mem::align_of::(), core::mem::align_of::<[u8; 64]>()); } #[test] fn test_sha256_digests_as_bytes_correctness() { - let digests = vec![ - Sha256Digest([1u8; 32]), - Sha256Digest([2u8; 32]), - Sha256Digest([3u8; 32]), - ]; + let digests = vec![Sha256Digest([1u8; 32]), Sha256Digest([2u8; 32]), Sha256Digest([3u8; 32])]; let bytes = Sha256Digest::digests_as_bytes(&digests); @@ -260,11 +244,7 @@ fn test_sha256_digests_as_bytes_correctness() { #[test] fn test_sha512_digests_as_bytes_correctness() { - let digests = vec![ - Sha512Digest([1u8; 64]), - Sha512Digest([2u8; 64]), - Sha512Digest([3u8; 64]), - ]; + let digests = vec![Sha512Digest([1u8; 64]), Sha512Digest([2u8; 64]), Sha512Digest([3u8; 64])]; let bytes = Sha512Digest::digests_as_bytes(&digests); @@ -277,39 +257,6 @@ fn test_sha512_digests_as_bytes_correctness() { assert_eq!(&bytes[128..192], &[3u8; 64]); } -// SHA-512 TRUNCATION TEST -// ================================================================================================ - -/// This test demonstrates that Sha512Digest::as_bytes() returns truncated SHA-512, -/// NOT SHA-512/256. SHA-512/256 uses different initialization vectors and produces -/// completely different output. -#[test] -fn test_sha512_truncation_not_sha512_256() { - // Hash "abc" with SHA-512 - let sha512_digest = Sha512::hash(b"abc"); - - // Get the first 32 bytes (truncated SHA-512) - let truncated: [u8; 32] = sha512_digest.as_bytes(); - - // SHA-512("abc") = ddaf35a193617aba... - // First 32 bytes should be: ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a - let expected_truncated = hex::decode( - "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a" - ).unwrap(); - - assert_eq!(truncated.to_vec(), expected_truncated, - "Truncated SHA-512 should be the first 32 bytes of full SHA-512 digest"); - - // SHA-512/256("abc") would be: 53048e2681941ef99b2e29b76b4c7dabe4c2d0c634fc6d46e0e2f13107e7af23 - // This is completely different from truncated SHA-512 - let sha512_256_expected = hex::decode( - "53048e2681941ef99b2e29b76b4c7dabe4c2d0c634fc6d46e0e2f13107e7af23" - ).unwrap(); - - assert_ne!(truncated.to_vec(), sha512_256_expected, - "Truncated SHA-512 should NOT equal SHA-512/256 (different IVs per FIPS 180-4)"); -} - // MERGE_MANY CORRECTNESS TESTS // ================================================================================================