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//! use commonware_parallel::Sequential;
9//!
10//! // Hash data in a single shot (fastest path)
11//! let digest = Sha256::hash(&[b"hello,", b"world!"]);
12//! println!("digest: {:?}", digest);
13//!
14//! // Or stream data incrementally
15//! let mut hasher = Sha256::default();
16//! hasher.update(b"hello,");
17//! hasher.update(b"world!");
18//! let (_hasher, digest) = hasher.finalize();
19//! println!("digest: {:?}", digest);
20//!
21//! // Hash independent messages with SIMD acceleration when available.
22//! // Batching is most effective for messages of the same length.
23//! let messages: [[u8; 32]; 16] = core::array::from_fn(|lane| [lane as u8; 32]);
24//! let digests = Sha256::hash_many(&messages, &Sequential);
25//! assert_eq!(digests[3], Sha256::hash(&[messages[3].as_slice()]));
26//! ```
27
28use crate::Hasher;
29#[cfg(not(feature = "std"))]
30use alloc::vec;
31#[cfg(all(not(feature = "std"), target_arch = "x86_64"))]
32use alloc::vec::Vec;
33use bytes::BufMut;
34use commonware_codec::{
35    Buf, DecodeExt, Error as CodecError, FixedArray, FixedSize, Read, ReadExt, Write,
36};
37use commonware_formatting::Hex;
38use commonware_math::algebra::Random;
39use commonware_parallel::Strategy;
40use commonware_utils::{Array, Span, sequence::FixedBytes};
41use core::{
42    cmp::Ordering,
43    fmt::{Debug, Display},
44    ops::Deref,
45};
46use rand_core::CryptoRng;
47use sha2::{Digest as _, Sha256 as ISha256, block_api::compress256};
48use zeroize::Zeroize;
49
50#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
51mod simd;
52
53/// Re-export `sha2::Sha256` as `CoreSha256` for external use if needed.
54pub type CoreSha256 = ISha256;
55
56const DIGEST_LENGTH: usize = 32;
57
58/// The SHA-256 block size in bytes.
59const BLOCK_LENGTH: usize = 64;
60
61/// Maximum message length, in bytes, that the fixed-size fast path can handle.
62///
63/// SHA-256 padding appends a single `0x80` byte and an 8-byte length suffix.
64/// Within two blocks (128 bytes), at most `128 - 9 = 119` bytes of message can
65/// be hashed without spilling into a third block, which is the range we
66/// specialize for.
67const MAX_FIXED: usize = 2 * BLOCK_LENGTH - 9;
68
69/// The SHA-256 initial hash values (FIPS 180-4, section 5.3.3).
70const IV: [u32; 8] = [
71    0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
72];
73
74/// Serialize the SHA-256 state words into a big-endian digest.
75#[inline]
76fn digest_from_state(state: [u32; 8]) -> [u8; DIGEST_LENGTH] {
77    let mut out = [0u8; DIGEST_LENGTH];
78    for (chunk, word) in out.as_chunks_mut::<4>().0.iter_mut().zip(state) {
79        *chunk = word.to_be_bytes();
80    }
81    out
82}
83
84/// Pad and compress `scratch[..len]` (where `len <= MAX_FIXED`) directly from
85/// the IV, assuming `scratch[len..]` is already zeroed (i.e. fresh scratch).
86///
87/// This avoids the streaming hasher's buffering and the redundant zero-fill of
88/// the padding region, which is the bulk of the one-shot speedup.
89#[inline]
90fn finalize_fixed_fresh(scratch: &mut [u8; 2 * BLOCK_LENGTH], len: usize) -> [u8; DIGEST_LENGTH] {
91    assert!(len <= MAX_FIXED);
92    let bit_len = ((len as u64) * 8).to_be_bytes();
93    scratch[len] = 0x80;
94    let mut state = IV;
95    if len < BLOCK_LENGTH - 8 {
96        // Message + padding fit in a single block.
97        scratch[BLOCK_LENGTH - 8..BLOCK_LENGTH].copy_from_slice(&bit_len);
98        let (blocks, _) = scratch[..BLOCK_LENGTH].as_chunks::<BLOCK_LENGTH>();
99        compress256(&mut state, blocks);
100    } else {
101        // Padding spills into a second block.
102        scratch[2 * BLOCK_LENGTH - 8..].copy_from_slice(&bit_len);
103        let (blocks, _) = scratch.as_chunks::<BLOCK_LENGTH>();
104        compress256(&mut state, blocks);
105    }
106    digest_from_state(state)
107}
108
109/// Specialize the hot merkle shapes: constant offsets let the compiler inline
110/// the copies and drop the runtime-length bookkeeping. The general case is
111/// outlined into [`hash_general`] so this stays small enough to inline.
112#[inline(always)]
113fn hash_specialized(parts: &[&[u8]]) -> Digest {
114    match parts {
115        [p, l, r] if p.len() == 8 && l.len() == 32 && r.len() == 32 => {
116            let mut scratch = [0u8; 2 * BLOCK_LENGTH];
117            scratch[..8].copy_from_slice(p);
118            scratch[8..40].copy_from_slice(l);
119            scratch[40..72].copy_from_slice(r);
120            Digest(finalize_fixed_fresh(&mut scratch, 72))
121        }
122        [a, b] if a.len() == 32 && b.len() == 32 => {
123            let mut scratch = [0u8; 2 * BLOCK_LENGTH];
124            scratch[..32].copy_from_slice(a);
125            scratch[32..64].copy_from_slice(b);
126            Digest(finalize_fixed_fresh(&mut scratch, 64))
127        }
128        [p, d] if p.len() == 8 && d.len() == 32 => {
129            let mut scratch = [0u8; 2 * BLOCK_LENGTH];
130            scratch[..8].copy_from_slice(p);
131            scratch[8..40].copy_from_slice(d);
132            Digest(finalize_fixed_fresh(&mut scratch, 40))
133        }
134        [p, d] if p.len() == 4 && d.len() == 32 => {
135            let mut scratch = [0u8; 2 * BLOCK_LENGTH];
136            scratch[..4].copy_from_slice(p);
137            scratch[4..36].copy_from_slice(d);
138            Digest(finalize_fixed_fresh(&mut scratch, 36))
139        }
140        _ => hash_general(parts),
141    }
142}
143
144/// General-purpose assembly + streaming fallback for shapes that miss the
145/// specialized arms (e.g. single-part messages and variable-length leaves).
146/// Outlined so it never bloats callers.
147#[inline(never)]
148fn hash_general(parts: &[&[u8]]) -> Digest {
149    let mut scratch = [0u8; 2 * BLOCK_LENGTH];
150    let mut len = 0usize;
151    let mut parts = parts.iter();
152    loop {
153        match parts.next() {
154            Some(part) if len + part.len() <= MAX_FIXED => {
155                scratch[len..len + part.len()].copy_from_slice(part);
156                len += part.len();
157            }
158            Some(part) => {
159                let mut hasher = ISha256::new();
160                hasher.update(&scratch[..len]);
161                hasher.update(part);
162                for part in parts {
163                    hasher.update(part);
164                }
165                let array: [u8; DIGEST_LENGTH] = hasher.finalize().into();
166                return Digest(array);
167            }
168            None => break,
169        }
170    }
171    Digest(finalize_fixed_fresh(&mut scratch, len))
172}
173
174/// SHA-256 hasher.
175#[derive(Debug, Default)]
176pub struct Sha256 {
177    hasher: ISha256,
178}
179
180impl Sha256 {
181    /// Convenience function for testing that creates an easily recognizable digest by repeating a
182    /// single byte.
183    pub fn fill(b: u8) -> <Self as Hasher>::Digest {
184        <Self as Hasher>::Digest::decode(vec![b; DIGEST_LENGTH]).unwrap()
185    }
186}
187
188impl Hasher for Sha256 {
189    type Digest = Digest;
190
191    #[inline]
192    fn hash(parts: &[&[u8]]) -> Self::Digest {
193        hash_specialized(parts)
194    }
195
196    #[inline]
197    fn hash_pair(
198        left: &[&[u8]],
199        right: &[&[u8]],
200        _strategy: &impl Strategy,
201    ) -> (Self::Digest, Self::Digest) {
202        #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
203        if let Some(pair) = simd::hash_pair(left, right) {
204            return pair;
205        }
206        (Self::hash(left), Self::hash(right))
207    }
208
209    #[cfg(target_arch = "x86_64")]
210    fn hash_many<M: AsRef<[u8]> + Sync>(
211        messages: &[M],
212        strategy: &impl Strategy,
213    ) -> Vec<Self::Digest> {
214        // Where the x16 kernel exists but SHA-NI does not, the kernel costs the same however few
215        // lanes are active, so shares hold whole sixteen-message batches: when the messages share
216        // a length, only the last kernel call can run with idle lanes. Without the kernel, every
217        // message hashes alone, and with SHA-NI, a share too small for the kernel hashes on SHA-NI
218        // instead, so the strategy may split the messages one at a time across more workers.
219        let lanes = if simd::minimum_x16_batch_len().is_some() && !simd::supports_sha_ni() {
220            simd::X16_LANES
221        } else {
222            1
223        };
224        crate::hash_batches(messages, lanes, strategy, hash_batch)
225    }
226
227    #[inline]
228    fn update(&mut self, message: &[u8]) -> &mut Self {
229        self.hasher.update(message);
230        self
231    }
232
233    #[inline]
234    fn finalize(mut self) -> (Self, Self::Digest) {
235        let finalized = self.hasher.finalize_reset();
236        let array: [u8; DIGEST_LENGTH] = finalized.into();
237        (self, Digest(array))
238    }
239}
240
241/// Hash independent messages in order on the calling thread. Where the AVX-512 kernel is
242/// available, it takes adjacent equal-length messages up to sixteen at a time, and batches below
243/// its minimum size hash one message at a time.
244#[cfg(target_arch = "x86_64")]
245fn hash_batch<M: AsRef<[u8]>>(messages: &[M]) -> Vec<Digest> {
246    let Some(minimum) = simd::minimum_x16_batch_len() else {
247        return messages
248            .iter()
249            .map(|message| hash_specialized(&[message.as_ref()]))
250            .collect();
251    };
252
253    // Adjacent equal-length runs satisfy the kernel's length requirement and
254    // keep the resulting digests in input order.
255    let mut digests = Vec::with_capacity(messages.len());
256    for run in messages.chunk_by(|left, right| left.as_ref().len() == right.as_ref().len()) {
257        for batch in run.chunks(simd::X16_LANES) {
258            if batch.len() >= minimum {
259                // Spare lanes borrow the first input; only active lanes contribute output.
260                let mut inputs = [batch[0].as_ref(); simd::X16_LANES];
261                for (input, message) in inputs[1..].iter_mut().zip(&batch[1..]) {
262                    *input = message.as_ref();
263                }
264                if let Some(batch_digests) = simd::hash_x16(inputs) {
265                    digests.extend_from_slice(&batch_digests[..batch.len()]);
266                    continue;
267                }
268            }
269            digests.extend(
270                batch
271                    .iter()
272                    .map(|message| hash_specialized(&[message.as_ref()])),
273            );
274        }
275    }
276    digests
277}
278
279/// Digest of a SHA-256 hashing operation.
280#[derive(Clone, Copy, Eq, PartialEq, Hash, FixedArray)]
281#[fixed_array(infallible)]
282#[repr(transparent)]
283pub struct Digest(pub [u8; DIGEST_LENGTH]);
284
285impl Ord for Digest {
286    #[inline]
287    fn cmp(&self, other: &Self) -> Ordering {
288        FixedBytes::new(self.0).cmp(&FixedBytes::new(other.0))
289    }
290}
291
292impl PartialOrd for Digest {
293    #[inline]
294    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
295        Some(self.cmp(other))
296    }
297}
298
299#[cfg(feature = "arbitrary")]
300impl<'a> arbitrary::Arbitrary<'a> for Digest {
301    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
302        // Generate random bytes and compute their Sha256 hash
303        let len = u.int_in_range(0..=256)?;
304        let data = u.bytes(len)?;
305        Ok(Sha256::hash(&[data]))
306    }
307}
308
309impl Write for Digest {
310    fn write(&self, buf: &mut impl BufMut) {
311        self.0.write(buf);
312    }
313}
314
315impl Read for Digest {
316    type Cfg = ();
317
318    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
319        let array = <[u8; DIGEST_LENGTH]>::read(buf)?;
320        Ok(Self(array))
321    }
322}
323
324impl FixedSize for Digest {
325    const SIZE: usize = DIGEST_LENGTH;
326}
327
328impl Span for Digest {}
329
330impl Array for Digest {}
331
332impl AsRef<[u8]> for Digest {
333    fn as_ref(&self) -> &[u8] {
334        &self.0
335    }
336}
337
338impl Deref for Digest {
339    type Target = [u8];
340    fn deref(&self) -> &[u8] {
341        &self.0
342    }
343}
344
345impl Debug for Digest {
346    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
347        write!(f, "{}", Hex(&self.0))
348    }
349}
350
351impl Display for Digest {
352    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
353        write!(f, "{}", Hex(&self.0))
354    }
355}
356
357impl crate::Digest for Digest {
358    const EMPTY: Self = Self([0u8; DIGEST_LENGTH]);
359}
360
361impl Random for Digest {
362    fn random(mut rng: impl CryptoRng) -> Self {
363        let mut array = [0u8; DIGEST_LENGTH];
364        rng.fill_bytes(&mut array);
365        Self(array)
366    }
367}
368
369impl Zeroize for Digest {
370    fn zeroize(&mut self) {
371        self.0.zeroize();
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use commonware_codec::{Copying, DecodeExt, Encode};
379    use commonware_parallel::{Sequential, mocks::explore};
380
381    const HELLO_DIGEST: [u8; DIGEST_LENGTH] = commonware_formatting::hex!(
382        "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
383    );
384
385    #[test]
386    fn test_digest_ord() {
387        let a = Digest([0; DIGEST_LENGTH]);
388        let mut b = a;
389        b.0[DIGEST_LENGTH - 1] = 1;
390
391        // The first byte decides even when the remaining bytes order the other way.
392        let mut c = a;
393        c.0[0] = 0x7f;
394        c.0[1..].fill(0xff);
395        let mut d = a;
396        d.0[0] = 0x80;
397        for (a, b, expected) in [
398            (a, a, Ordering::Equal),
399            (a, b, Ordering::Less),
400            (b, a, Ordering::Greater),
401            (c, d, Ordering::Less),
402            (d, c, Ordering::Greater),
403        ] {
404            assert_eq!(a.cmp(&b), expected);
405            assert_eq!(a.partial_cmp(&b), Some(expected));
406        }
407    }
408
409    /// Anchor the streaming and one-shot paths to a known SHA-256 digest,
410    /// which the differential fuzz tests (comparing paths against each
411    /// other) cannot do.
412    #[test]
413    fn test_sha256() {
414        let msg = b"hello world";
415
416        // Generate hash via streaming
417        let mut hasher = Sha256::default();
418        hasher.update(msg);
419        let (_, digest) = hasher.finalize();
420        assert!(Digest::decode(Copying(&digest)).is_ok());
421        assert_eq!(digest.as_ref(), HELLO_DIGEST);
422
423        // Test one-shot hasher
424        let hash = Sha256::hash(&[msg]);
425        assert_eq!(hash.as_ref(), HELLO_DIGEST);
426
427        // Test multi-part one-shot hasher
428        let hash = Sha256::hash(&[b"hello", b" world"]);
429        assert_eq!(hash.as_ref(), HELLO_DIGEST);
430    }
431
432    /// Exhaustively sweep every total length across the block-padding and
433    /// `MAX_FIXED` boundaries, checking the one-shot path against the
434    /// streaming implementation. Fuzzing only hits specific off-by-one
435    /// lengths probabilistically, while this sweep guarantees them all.
436    #[test]
437    fn test_sha256_hash_parts_boundaries() {
438        for total in 0..=300usize {
439            let data: Vec<u8> = (0..total).map(|i| i as u8).collect();
440            // Split into a few parts of varying sizes.
441            let mid = total / 3;
442            let parts: [&[u8]; 3] = [&data[..mid], &data[mid..2 * mid], &data[2 * mid..]];
443
444            let oneshot = Sha256::hash(&parts);
445
446            let mut hasher = Sha256::default();
447            for part in &parts {
448                hasher.update(part);
449            }
450            let (_, streamed) = hasher.finalize();
451
452            assert_eq!(oneshot, streamed, "mismatch for total={total}");
453        }
454    }
455
456    /// Pin every specialized fast-path arm against the streaming implementation.
457    #[test]
458    fn test_sha256_hash_specialized_arms() {
459        let data: Vec<u8> = (0u8..72).collect();
460        let shapes: [&[&[u8]]; 4] = [
461            &[&data[..8], &data[8..40], &data[40..72]],
462            &[&data[..32], &data[32..64]],
463            &[&data[..8], &data[8..40]],
464            &[&data[..4], &data[4..36]],
465        ];
466        for parts in shapes {
467            let oneshot = Sha256::hash(parts);
468
469            let mut hasher = Sha256::default();
470            for part in parts {
471                hasher.update(part);
472            }
473            let (_, streamed) = hasher.finalize();
474
475            assert_eq!(oneshot, streamed, "mismatch for shape {parts:?}");
476        }
477    }
478
479    #[test]
480    fn test_sha256_len() {
481        assert_eq!(Digest::SIZE, DIGEST_LENGTH);
482    }
483
484    /// Deterministically exercise the pair (assembly) kernel with the MMR
485    /// node shape (position || left || right) that motivates it, regardless
486    /// of what the fuzz generators happen to sample.
487    #[test]
488    fn test_hash_pair_mmr_node_shape_matches_streaming() {
489        fn node(position: u64, fill: u8) -> Vec<Vec<u8>> {
490            vec![
491                position.to_be_bytes().to_vec(),
492                vec![fill; 32],
493                vec![fill + 1; 32],
494            ]
495        }
496        crate::fuzz::Plan::<Sha256>::new(node(42, 0x11), node(43, 0x33)).run();
497    }
498
499    /// Deterministically exercise the pair (assembly) kernel with the BMT
500    /// node shape (left || right, no position) that motivates it, regardless
501    /// of what the fuzz generators happen to sample.
502    #[test]
503    fn test_hash_pair_bmt_node_shape_matches_streaming() {
504        fn assert_pair(left: [&[u8]; 2], right: [&[u8]; 2]) {
505            let expected = |parts: &[&[u8]]| {
506                let mut hasher = Sha256::default();
507                for part in parts {
508                    hasher.update(part);
509                }
510                hasher.finalize().1
511            };
512            let (left_digest, right_digest) = Sha256::hash_pair(&left, &right, &Sequential);
513            let expected_left = expected(&left);
514            let expected_right = expected(&right);
515            assert_eq!(Sha256::hash(&left), expected_left);
516            assert_eq!(Sha256::hash(&right), expected_right);
517            assert_eq!(left_digest, expected_left);
518            assert_eq!(right_digest, expected_right);
519        }
520
521        let zero = [0u8; 32];
522        let ff = [0xff; 32];
523        let ascending: [u8; 32] = core::array::from_fn(|i| i as u8);
524        let descending: [u8; 32] = core::array::from_fn(|i| 0xff - i as u8);
525        assert_pair([&zero, &zero], [&zero, &zero]);
526        assert_pair([&ff, &ff], [&ff, &ff]);
527        assert_pair([&ascending, &descending], [&ff, &zero]);
528
529        let backing: Vec<u8> = (0..96).map(|i| i as u8).collect();
530        assert_pair(
531            [&backing[1..33], &backing[17..49]],
532            [&backing[2..34], &backing[18..50]],
533        );
534    }
535
536    /// Checks [Sha256::hash_many] on `strategy` against hashing each message alone.
537    fn check_hash_many(messages: &[&[u8]], strategy: &impl Strategy) {
538        let expected = messages
539            .iter()
540            .map(|&message| Sha256::hash(&[message]))
541            .collect::<Vec<_>>();
542        assert_eq!(Sha256::hash_many(messages, strategy), expected);
543    }
544
545    #[test]
546    fn test_hash_many_boundaries_match_individual_hashes() {
547        explore(|strategy| {
548            for len in (0..=129).chain([255, 256, 1024, 12_634, 50_534]) {
549                let messages: [Vec<u8>; 33] = core::array::from_fn(|lane| {
550                    (0..len)
551                        .map(|i| (i as u8).wrapping_add(lane as u8))
552                        .collect()
553                });
554                let refs = messages.each_ref().map(Vec::as_slice);
555                for count in [0, 1, 2, 6, 7, 15, 16, 17, 31, 32, 33] {
556                    check_hash_many(&refs[..count], strategy);
557                }
558            }
559        });
560
561        let messages: [Vec<u8>; 16] = core::array::from_fn(|lane| vec![lane as u8; 64]);
562        let mut refs = messages.each_ref().map(Vec::as_slice);
563        refs[9] = &messages[9][..63];
564        explore(|strategy| check_hash_many(&refs, strategy));
565        #[cfg(target_arch = "x86_64")]
566        assert!(simd::hash_x16(refs).is_none());
567    }
568
569    #[test]
570    fn test_hash_many_aliased_unaligned_inputs_match_individual_hashes() {
571        let backing: Vec<u8> = (0..160).map(|i| i as u8).collect();
572        let messages: [&[u8]; 16] = core::array::from_fn(|lane| &backing[lane..lane + 129]);
573        explore(|strategy| {
574            for count in 1..=messages.len() {
575                check_hash_many(&messages[..count], strategy);
576            }
577            check_hash_many(&[&backing[1..130]; 16], strategy);
578        });
579    }
580
581    #[test]
582    fn test_codec() {
583        let msg = b"hello world";
584        let mut hasher = Sha256::default();
585        hasher.update(msg);
586        let (_, digest) = hasher.finalize();
587
588        let encoded = digest.encode();
589        assert_eq!(encoded.len(), DIGEST_LENGTH);
590        assert_eq!(encoded, digest.as_ref());
591
592        let decoded = Digest::decode(encoded).unwrap();
593        assert_eq!(digest, decoded);
594    }
595
596    #[cfg(feature = "arbitrary")]
597    mod conformance {
598        use super::*;
599        use commonware_codec::conformance::CodecConformance;
600
601        commonware_conformance::conformance_tests! {
602            CodecConformance<Digest>,
603        }
604    }
605}