Skip to main content

commonware_cryptography/sha256/
mod.rs

1//! SHA-256 implementation of the `Hasher` trait.
2//!
3//! This implementation uses the `sha2` crate to generate SHA-256 digests.
4//!
5//! # Example
6//! ```rust
7//! use commonware_cryptography::{Hasher, Sha256};
8//!
9//! // Hash data in a single shot (fastest path)
10//! let digest = Sha256::hash(&[b"hello,", b"world!"]);
11//! println!("digest: {:?}", digest);
12//!
13//! // Or stream data incrementally
14//! let mut hasher = Sha256::default();
15//! hasher.update(b"hello,");
16//! hasher.update(b"world!");
17//! let (_hasher, digest) = hasher.finalize();
18//! println!("digest: {:?}", digest);
19//! ```
20
21use crate::Hasher;
22#[cfg(not(feature = "std"))]
23use alloc::vec;
24use bytes::{Buf, BufMut};
25use commonware_codec::{
26    DecodeExt, Error as CodecError, FixedArray, FixedSize, Read, ReadExt, Write,
27};
28use commonware_formatting::Hex;
29use commonware_math::algebra::Random;
30use commonware_utils::{Array, Span};
31use core::{
32    fmt::{Debug, Display},
33    ops::Deref,
34};
35use rand_core::CryptoRng;
36use sha2::{Digest as _, Sha256 as ISha256, block_api::compress256};
37use zeroize::Zeroize;
38
39#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
40mod simd;
41
42/// Re-export `sha2::Sha256` as `CoreSha256` for external use if needed.
43pub type CoreSha256 = ISha256;
44
45const DIGEST_LENGTH: usize = 32;
46
47/// The SHA-256 block size in bytes.
48const BLOCK_LENGTH: usize = 64;
49
50/// Maximum message length, in bytes, that the fixed-size fast path can handle.
51///
52/// SHA-256 padding appends a single `0x80` byte and an 8-byte length suffix.
53/// Within two blocks (128 bytes), at most `128 - 9 = 119` bytes of message can
54/// be hashed without spilling into a third block, which is the range we
55/// specialize for.
56const MAX_FIXED: usize = 2 * BLOCK_LENGTH - 9;
57
58/// The SHA-256 initial hash values (FIPS 180-4, ยง5.3.3).
59const IV: [u32; 8] = [
60    0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
61];
62
63/// Serialize the SHA-256 state words into a big-endian digest.
64#[inline]
65fn digest_from_state(state: [u32; 8]) -> [u8; DIGEST_LENGTH] {
66    let mut out = [0u8; DIGEST_LENGTH];
67    for (chunk, word) in out.as_chunks_mut::<4>().0.iter_mut().zip(state) {
68        *chunk = word.to_be_bytes();
69    }
70    out
71}
72
73/// Pad and compress `scratch[..len]` (where `len <= MAX_FIXED`) directly from
74/// the IV, assuming `scratch[len..]` is already zeroed (i.e. fresh scratch).
75///
76/// This avoids the streaming hasher's buffering and the redundant zero-fill of
77/// the padding region, which is the bulk of the one-shot speedup.
78#[inline]
79fn finalize_fixed_fresh(scratch: &mut [u8; 2 * BLOCK_LENGTH], len: usize) -> [u8; DIGEST_LENGTH] {
80    assert!(len <= MAX_FIXED);
81    let bit_len = ((len as u64) * 8).to_be_bytes();
82    scratch[len] = 0x80;
83    let mut state = IV;
84    if len < BLOCK_LENGTH - 8 {
85        // Message + padding fit in a single block.
86        scratch[BLOCK_LENGTH - 8..BLOCK_LENGTH].copy_from_slice(&bit_len);
87        let (blocks, _) = scratch[..BLOCK_LENGTH].as_chunks::<BLOCK_LENGTH>();
88        compress256(&mut state, blocks);
89    } else {
90        // Padding spills into a second block.
91        scratch[2 * BLOCK_LENGTH - 8..].copy_from_slice(&bit_len);
92        let (blocks, _) = scratch.as_chunks::<BLOCK_LENGTH>();
93        compress256(&mut state, blocks);
94    }
95    digest_from_state(state)
96}
97
98/// Specialize the hot merkle shapes: constant offsets let the compiler inline
99/// the copies and drop the runtime-length bookkeeping. The general case is
100/// outlined into [`hash_general`] so this stays small enough to inline.
101#[inline(always)]
102fn hash_specialized(parts: &[&[u8]]) -> Digest {
103    match parts {
104        [p, l, r] if p.len() == 8 && l.len() == 32 && r.len() == 32 => {
105            let mut scratch = [0u8; 2 * BLOCK_LENGTH];
106            scratch[..8].copy_from_slice(p);
107            scratch[8..40].copy_from_slice(l);
108            scratch[40..72].copy_from_slice(r);
109            Digest(finalize_fixed_fresh(&mut scratch, 72))
110        }
111        [a, b] if a.len() == 32 && b.len() == 32 => {
112            let mut scratch = [0u8; 2 * BLOCK_LENGTH];
113            scratch[..32].copy_from_slice(a);
114            scratch[32..64].copy_from_slice(b);
115            Digest(finalize_fixed_fresh(&mut scratch, 64))
116        }
117        [p, d] if p.len() == 8 && d.len() == 32 => {
118            let mut scratch = [0u8; 2 * BLOCK_LENGTH];
119            scratch[..8].copy_from_slice(p);
120            scratch[8..40].copy_from_slice(d);
121            Digest(finalize_fixed_fresh(&mut scratch, 40))
122        }
123        [p, d] if p.len() == 4 && d.len() == 32 => {
124            let mut scratch = [0u8; 2 * BLOCK_LENGTH];
125            scratch[..4].copy_from_slice(p);
126            scratch[4..36].copy_from_slice(d);
127            Digest(finalize_fixed_fresh(&mut scratch, 36))
128        }
129        _ => hash_general(parts),
130    }
131}
132
133/// General-purpose assembly + streaming fallback for shapes that miss the
134/// specialized arms (e.g. single-part messages and variable-length leaves).
135/// Outlined so it never bloats callers.
136#[inline(never)]
137fn hash_general(parts: &[&[u8]]) -> Digest {
138    let mut scratch = [0u8; 2 * BLOCK_LENGTH];
139    let mut len = 0usize;
140    let mut parts = parts.iter();
141    loop {
142        match parts.next() {
143            Some(part) if len + part.len() <= MAX_FIXED => {
144                scratch[len..len + part.len()].copy_from_slice(part);
145                len += part.len();
146            }
147            Some(part) => {
148                let mut hasher = ISha256::new();
149                hasher.update(&scratch[..len]);
150                hasher.update(part);
151                for part in parts {
152                    hasher.update(part);
153                }
154                let array: [u8; DIGEST_LENGTH] = hasher.finalize().into();
155                return Digest(array);
156            }
157            None => break,
158        }
159    }
160    Digest(finalize_fixed_fresh(&mut scratch, len))
161}
162
163/// SHA-256 hasher.
164#[derive(Debug, Default)]
165pub struct Sha256 {
166    hasher: ISha256,
167}
168
169impl Sha256 {
170    /// Convenience function for testing that creates an easily recognizable digest by repeating a
171    /// single byte.
172    pub fn fill(b: u8) -> <Self as Hasher>::Digest {
173        <Self as Hasher>::Digest::decode(vec![b; DIGEST_LENGTH].as_ref()).unwrap()
174    }
175}
176
177impl Hasher for Sha256 {
178    type Digest = Digest;
179
180    #[inline]
181    fn hash(parts: &[&[u8]]) -> Self::Digest {
182        hash_specialized(parts)
183    }
184
185    #[inline]
186    fn hash_pair(left: &[&[u8]], right: &[&[u8]]) -> (Self::Digest, Self::Digest) {
187        #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
188        if let Some(pair) = simd::hash_pair(left, right) {
189            return pair;
190        }
191        (Self::hash(left), Self::hash(right))
192    }
193
194    #[inline]
195    fn update(&mut self, message: &[u8]) -> &mut Self {
196        self.hasher.update(message);
197        self
198    }
199
200    #[inline]
201    fn finalize(mut self) -> (Self, Self::Digest) {
202        let finalized = self.hasher.finalize_reset();
203        let array: [u8; DIGEST_LENGTH] = finalized.into();
204        (self, Digest(array))
205    }
206}
207
208/// Digest of a SHA-256 hashing operation.
209#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, FixedArray)]
210#[fixed_array(infallible)]
211#[repr(transparent)]
212pub struct Digest(pub [u8; DIGEST_LENGTH]);
213
214#[cfg(feature = "arbitrary")]
215impl<'a> arbitrary::Arbitrary<'a> for Digest {
216    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
217        // Generate random bytes and compute their Sha256 hash
218        let len = u.int_in_range(0..=256)?;
219        let data = u.bytes(len)?;
220        Ok(Sha256::hash(&[data]))
221    }
222}
223
224impl Write for Digest {
225    fn write(&self, buf: &mut impl BufMut) {
226        self.0.write(buf);
227    }
228}
229
230impl Read for Digest {
231    type Cfg = ();
232
233    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
234        let array = <[u8; DIGEST_LENGTH]>::read(buf)?;
235        Ok(Self(array))
236    }
237}
238
239impl FixedSize for Digest {
240    const SIZE: usize = DIGEST_LENGTH;
241}
242
243impl Span for Digest {}
244
245impl Array for Digest {}
246
247impl AsRef<[u8]> for Digest {
248    fn as_ref(&self) -> &[u8] {
249        &self.0
250    }
251}
252
253impl Deref for Digest {
254    type Target = [u8];
255    fn deref(&self) -> &[u8] {
256        &self.0
257    }
258}
259
260impl Debug for Digest {
261    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
262        write!(f, "{}", Hex(&self.0))
263    }
264}
265
266impl Display for Digest {
267    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
268        write!(f, "{}", Hex(&self.0))
269    }
270}
271
272impl crate::Digest for Digest {
273    const EMPTY: Self = Self([0u8; DIGEST_LENGTH]);
274}
275
276impl Random for Digest {
277    fn random(mut rng: impl CryptoRng) -> Self {
278        let mut array = [0u8; DIGEST_LENGTH];
279        rng.fill_bytes(&mut array);
280        Self(array)
281    }
282}
283
284impl Zeroize for Digest {
285    fn zeroize(&mut self) {
286        self.0.zeroize();
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use commonware_codec::{DecodeExt, Encode};
294
295    const HELLO_DIGEST: [u8; DIGEST_LENGTH] = commonware_formatting::hex!(
296        "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
297    );
298
299    /// Anchor the streaming and one-shot paths to a known SHA-256 digest,
300    /// which the differential fuzz tests (comparing paths against each
301    /// other) cannot do.
302    #[test]
303    fn test_sha256() {
304        let msg = b"hello world";
305
306        // Generate hash via streaming
307        let mut hasher = Sha256::default();
308        hasher.update(msg);
309        let (_, digest) = hasher.finalize();
310        assert!(Digest::decode(digest.as_ref()).is_ok());
311        assert_eq!(digest.as_ref(), HELLO_DIGEST);
312
313        // Test one-shot hasher
314        let hash = Sha256::hash(&[msg]);
315        assert_eq!(hash.as_ref(), HELLO_DIGEST);
316
317        // Test multi-part one-shot hasher
318        let hash = Sha256::hash(&[b"hello", b" world"]);
319        assert_eq!(hash.as_ref(), HELLO_DIGEST);
320    }
321
322    /// Exhaustively sweep every total length across the block-padding and
323    /// `MAX_FIXED` boundaries, checking the one-shot path against the
324    /// streaming implementation. Fuzzing only hits specific off-by-one
325    /// lengths probabilistically, while this sweep guarantees them all.
326    #[test]
327    fn test_sha256_hash_parts_boundaries() {
328        for total in 0..=300usize {
329            let data: Vec<u8> = (0..total).map(|i| i as u8).collect();
330            // Split into a few parts of varying sizes.
331            let mid = total / 3;
332            let parts: [&[u8]; 3] = [&data[..mid], &data[mid..2 * mid], &data[2 * mid..]];
333
334            let oneshot = Sha256::hash(&parts);
335
336            let mut hasher = Sha256::default();
337            for part in &parts {
338                hasher.update(part);
339            }
340            let (_, streamed) = hasher.finalize();
341
342            assert_eq!(oneshot, streamed, "mismatch for total={total}");
343        }
344    }
345
346    /// Pin every specialized fast-path arm against the streaming implementation.
347    #[test]
348    fn test_sha256_hash_specialized_arms() {
349        let data: Vec<u8> = (0u8..72).collect();
350        let shapes: [&[&[u8]]; 4] = [
351            &[&data[..8], &data[8..40], &data[40..72]],
352            &[&data[..32], &data[32..64]],
353            &[&data[..8], &data[8..40]],
354            &[&data[..4], &data[4..36]],
355        ];
356        for parts in shapes {
357            let oneshot = Sha256::hash(parts);
358
359            let mut hasher = Sha256::default();
360            for part in parts {
361                hasher.update(part);
362            }
363            let (_, streamed) = hasher.finalize();
364
365            assert_eq!(oneshot, streamed, "mismatch for shape {parts:?}");
366        }
367    }
368
369    #[test]
370    fn test_sha256_len() {
371        assert_eq!(Digest::SIZE, DIGEST_LENGTH);
372    }
373
374    /// Deterministically exercise the pair (assembly) kernel with the MMR
375    /// node shape (position || left || right) that motivates it, regardless
376    /// of what the fuzz generators happen to sample.
377    #[test]
378    fn test_hash_pair_mmr_node_shape_matches_streaming() {
379        fn node(position: u64, fill: u8) -> Vec<Vec<u8>> {
380            vec![
381                position.to_be_bytes().to_vec(),
382                vec![fill; 32],
383                vec![fill + 1; 32],
384            ]
385        }
386        crate::fuzz::Plan::<Sha256>::new(node(42, 0x11), node(43, 0x33)).run();
387    }
388
389    /// Deterministically exercise the pair (assembly) kernel with the BMT
390    /// node shape (left || right, no position) that motivates it, regardless
391    /// of what the fuzz generators happen to sample.
392    #[test]
393    fn test_hash_pair_bmt_node_shape_matches_streaming() {
394        fn node(fill: u8) -> Vec<Vec<u8>> {
395            vec![vec![fill; 32], vec![fill + 1; 32]]
396        }
397        crate::fuzz::Plan::<Sha256>::new(node(0x11), node(0x33)).run();
398    }
399
400    #[test]
401    fn test_codec() {
402        let msg = b"hello world";
403        let mut hasher = Sha256::default();
404        hasher.update(msg);
405        let (_, digest) = hasher.finalize();
406
407        let encoded = digest.encode();
408        assert_eq!(encoded.len(), DIGEST_LENGTH);
409        assert_eq!(encoded, digest.as_ref());
410
411        let decoded = Digest::decode(encoded).unwrap();
412        assert_eq!(digest, decoded);
413    }
414
415    #[cfg(feature = "arbitrary")]
416    mod conformance {
417        use super::*;
418        use commonware_codec::conformance::CodecConformance;
419
420        commonware_conformance::conformance_tests! {
421            CodecConformance<Digest>,
422        }
423    }
424}