Skip to main content

commonware_cryptography/
sha512.rs

1//! SHA-512 implementation of the [Hasher] trait.
2//!
3//! This implementation uses the `sha2` crate to generate SHA-512 digests. On x86-64 CPUs with
4//! AVX-512F, [Hasher::hash_many] hashes up to eight messages at once, one per SIMD lane. On
5//! aarch64 CPUs with the SHA-512 instructions, it hashes messages in pairs, interleaving their
6//! rounds. It splits the messages across its strategy only between whole batches of the kernel.
7//!
8//! # Example
9//! ```rust
10//! use commonware_cryptography::{Hasher, Sha512};
11//! use commonware_parallel::Sequential;
12//!
13//! // Hash data in a single shot
14//! let digest = Sha512::hash(&[b"hello,", b"world!"]);
15//! println!("digest: {:?}", digest);
16//!
17//! // Or stream data incrementally
18//! let mut hasher = Sha512::default();
19//! hasher.update(b"hello,");
20//! hasher.update(b"world!");
21//! let (_hasher, digest) = hasher.finalize();
22//! println!("digest: {:?}", digest);
23//!
24//! // Hash independent messages, which may differ in length, in one call
25//! let messages: [&[u8]; 3] = [b"a", b"bc", b"def"];
26//! let digests = Sha512::hash_many(&messages, &Sequential);
27//! assert_eq!(digests[1], Sha512::hash(&[b"bc"]));
28//! ```
29
30use crate::Hasher;
31#[cfg(not(feature = "std"))]
32use alloc::vec::Vec;
33use bytes::BufMut;
34use commonware_codec::{Buf, Error as CodecError, FixedArray, FixedSize, Read, ReadExt, Write};
35use commonware_formatting::Hex;
36use commonware_math::algebra::Random;
37use commonware_parallel::Strategy;
38use commonware_utils::{Array, Span, sequence::FixedBytes};
39use core::{
40    cmp::Ordering,
41    fmt::{Debug, Display},
42    ops::Deref,
43};
44use rand_core::CryptoRng;
45use sha2::Digest as _;
46use zeroize::Zeroize;
47
48#[cfg(target_arch = "aarch64")]
49mod aarch64;
50#[cfg(target_arch = "x86_64")]
51mod avx512;
52#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
53mod padding;
54
55/// The underlying SHA-512 implementation.
56pub type CoreSha512 = sha2::Sha512;
57
58/// Length of a SHA-512 digest in bytes.
59const DIGEST_LENGTH: usize = 64;
60
61#[cfg(target_arch = "x86_64")]
62cpufeatures::new!(has_avx512f, "avx512f");
63
64// Rust and cpufeatures group the Armv8.2 SHA-512 instructions with SHA-3's under `sha3`.
65#[cfg(target_arch = "aarch64")]
66cpufeatures::new!(has_sha3, "sha3");
67
68/// The number of messages the kernel for this CPU hashes at once.
69fn lanes() -> usize {
70    #[cfg(target_arch = "x86_64")]
71    if has_avx512f::get() {
72        return avx512::LANES;
73    }
74    #[cfg(target_arch = "aarch64")]
75    if has_sha3::get() {
76        return aarch64::LANES;
77    }
78    1
79}
80
81/// SHA-512 hasher.
82#[derive(Debug, Default)]
83pub struct Sha512 {
84    hasher: CoreSha512,
85}
86
87impl Hasher for Sha512 {
88    type Digest = Digest;
89
90    fn hash(parts: &[&[u8]]) -> Self::Digest {
91        let mut hasher = CoreSha512::new();
92        for part in parts {
93            hasher.update(part);
94        }
95        Digest(hasher.finalize().into())
96    }
97
98    fn hash_pair(
99        left: &[&[u8]],
100        right: &[&[u8]],
101        _strategy: &impl Strategy,
102    ) -> (Self::Digest, Self::Digest) {
103        (Self::hash(left), Self::hash(right))
104    }
105
106    fn hash_many<M: AsRef<[u8]> + Sync>(
107        messages: &[M],
108        strategy: &impl Strategy,
109    ) -> Vec<Self::Digest> {
110        // Shares hold whole kernel batches, so only the last call can start with unfilled lanes.
111        crate::hash_batches(messages, lanes(), strategy, hash_batch)
112    }
113
114    fn update(&mut self, message: &[u8]) -> &mut Self {
115        self.hasher.update(message);
116        self
117    }
118
119    fn finalize(mut self) -> (Self, Self::Digest) {
120        let finalized = self.hasher.finalize_reset();
121        (self, Digest(finalized.into()))
122    }
123}
124
125/// Hash independent messages in order on the calling thread, a kernel batch at a time where the
126/// CPU has a multi-message kernel.
127fn hash_batch<M: AsRef<[u8]>>(messages: &[M]) -> Vec<Digest> {
128    // With AVX-512F, hash up to eight messages per kernel call, one per lane.
129    #[cfg(target_arch = "x86_64")]
130    if has_avx512f::get() {
131        let mut digests = Vec::with_capacity(messages.len());
132        for chunk in messages.chunks(avx512::LANES) {
133            let mut lanes: [&[u8]; avx512::LANES] = [&[]; avx512::LANES];
134            for (lane, message) in lanes.iter_mut().zip(chunk) {
135                *lane = message.as_ref();
136            }
137
138            // A short final chunk passes only its own messages, and the zero digests of the
139            // unused lanes are dropped.
140            // SAFETY: AVX-512F support was just detected.
141            let chunk_digests = unsafe { avx512::hash(&lanes[..chunk.len()]) };
142            digests.extend_from_slice(&chunk_digests[..chunk.len()]);
143        }
144        return digests;
145    }
146
147    // With the SHA-512 instructions, hash the messages in interleaved pairs.
148    #[cfg(target_arch = "aarch64")]
149    if has_sha3::get() {
150        let mut digests = Vec::with_capacity(messages.len());
151        let (pairs, remainder) = messages.as_chunks::<{ aarch64::LANES }>();
152        for pair in pairs {
153            let lanes = [pair[0].as_ref(), pair[1].as_ref()];
154
155            // SAFETY: Support for the SHA-512 instructions was just detected.
156            digests.extend_from_slice(&unsafe { aarch64::hash(&lanes) });
157        }
158
159        // An odd last message has no partner to interleave with, so it is hashed alone.
160        for message in remainder {
161            digests.push(Sha512::hash(&[message.as_ref()]));
162        }
163        return digests;
164    }
165
166    // Without a multi-message kernel, hash each message alone.
167    messages
168        .iter()
169        .map(|message| Sha512::hash(&[message.as_ref()]))
170        .collect()
171}
172
173/// Digest of a SHA-512 hashing operation.
174#[derive(Clone, Copy, Eq, PartialEq, Hash, FixedArray)]
175#[fixed_array(infallible)]
176#[repr(transparent)]
177pub struct Digest(pub [u8; DIGEST_LENGTH]);
178
179impl Ord for Digest {
180    #[inline]
181    fn cmp(&self, other: &Self) -> Ordering {
182        FixedBytes::new(self.0).cmp(&FixedBytes::new(other.0))
183    }
184}
185
186impl PartialOrd for Digest {
187    #[inline]
188    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
189        Some(self.cmp(other))
190    }
191}
192
193#[cfg(feature = "arbitrary")]
194impl<'a> arbitrary::Arbitrary<'a> for Digest {
195    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
196        let len = u.int_in_range(0..=256)?;
197        let data = u.bytes(len)?;
198        Ok(Sha512::hash(&[data]))
199    }
200}
201
202impl Write for Digest {
203    fn write(&self, buf: &mut impl BufMut) {
204        self.0.write(buf);
205    }
206}
207
208impl Read for Digest {
209    type Cfg = ();
210
211    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
212        let array = <[u8; DIGEST_LENGTH]>::read(buf)?;
213        Ok(Self(array))
214    }
215}
216
217impl FixedSize for Digest {
218    const SIZE: usize = DIGEST_LENGTH;
219}
220
221impl Span for Digest {}
222
223impl Array for Digest {}
224
225impl AsRef<[u8]> for Digest {
226    fn as_ref(&self) -> &[u8] {
227        &self.0
228    }
229}
230
231impl Deref for Digest {
232    type Target = [u8];
233    fn deref(&self) -> &[u8] {
234        &self.0
235    }
236}
237
238impl Debug for Digest {
239    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
240        write!(f, "{}", Hex(&self.0))
241    }
242}
243
244impl Display for Digest {
245    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
246        write!(f, "{}", Hex(&self.0))
247    }
248}
249
250impl crate::Digest for Digest {
251    const EMPTY: Self = Self([0u8; DIGEST_LENGTH]);
252}
253
254impl Random for Digest {
255    fn random(mut rng: impl CryptoRng) -> Self {
256        let mut array = [0u8; DIGEST_LENGTH];
257        rng.fill_bytes(&mut array);
258        Self(array)
259    }
260}
261
262impl Zeroize for Digest {
263    fn zeroize(&mut self) {
264        self.0.zeroize();
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use commonware_codec::{Copying, DecodeExt, Encode};
272    use commonware_formatting::hex;
273    use commonware_parallel::{Sequential, mocks::explore};
274
275    const EMPTY_DIGEST: [u8; DIGEST_LENGTH] = hex!(
276        "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce"
277        "47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
278    );
279    const ABC_DIGEST: [u8; DIGEST_LENGTH] = hex!(
280        "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a"
281        "2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"
282    );
283
284    /// The `sha2` digest of `message`.
285    fn expected(message: &[u8]) -> Digest {
286        Digest(CoreSha512::digest(message).into())
287    }
288
289    /// A `len`-byte message whose bytes depend on `seed`.
290    fn message(len: usize, seed: usize) -> Vec<u8> {
291        (0..len)
292            .map(|i| (i.wrapping_mul(31) ^ seed.wrapping_mul(0x9e37)) as u8)
293            .collect()
294    }
295
296    /// Checks [Sha512::hash_many] on `strategy` against `sha2` one message at a time.
297    fn check_hash_many<M: AsRef<[u8]> + Sync>(messages: &[M], strategy: &impl Strategy) {
298        let digests: Vec<Digest> = messages
299            .iter()
300            .map(|message| expected(message.as_ref()))
301            .collect();
302        assert_eq!(Sha512::hash_many(messages, strategy), digests);
303    }
304
305    #[test]
306    fn test_known_answers_and_reset() {
307        // Every one-shot entry point gives the known digests of the empty message and `abc`,
308        // including when a message is split into parts around an empty one.
309        assert_eq!(Sha512::hash(&[]).as_ref(), EMPTY_DIGEST);
310        assert_eq!(Sha512::hash(&[b"abc"]).as_ref(), ABC_DIGEST);
311        assert_eq!(Sha512::hash(&[b"a", b"", b"bc"]).as_ref(), ABC_DIGEST);
312        assert_eq!(
313            Sha512::hash_pair(&[], &[b"a", b"bc"], &Sequential),
314            (Digest(EMPTY_DIGEST), Digest(ABC_DIGEST))
315        );
316        assert_eq!(
317            Sha512::hash_many(&[b"".as_slice(), b"abc"], &Sequential),
318            [Digest(EMPTY_DIGEST), Digest(ABC_DIGEST)]
319        );
320
321        // Stream each message a byte at a time and keep the hasher that `finalize` returns. Every
322        // digest after the first matches only if finalizing reset the hasher.
323        let mut hasher = Sha512::default();
324        for message in [b"abc".as_slice(), b"", b"abc", b""] {
325            for byte in message {
326                hasher.update(core::slice::from_ref(byte));
327            }
328            let (reset, digest) = hasher.finalize();
329            let expected = if message.is_empty() {
330                EMPTY_DIGEST
331            } else {
332                ABC_DIGEST
333            };
334            assert_eq!(digest.as_ref(), expected);
335            hasher = reset;
336        }
337    }
338
339    /// Every message count up to 20 (two full eight-lane AVX-512 groups and a partial one) at
340    /// every length in `0..=300`.
341    #[test]
342    fn test_hash_many_equal_lengths() {
343        explore(|strategy| {
344            for len in 0..=300 {
345                let messages: Vec<Vec<u8>> = (0..20).map(|lane| message(len, lane)).collect();
346                for count in 0..=messages.len() {
347                    check_hash_many(&messages[..count], strategy);
348                }
349            }
350        });
351    }
352
353    /// Every message count up to 20 with lengths that differ within each lane group, where one
354    /// message of each length in `0..=300` (and two longer lengths) moves across lanes, so
355    /// messages both run out early and outlast the rest of their group.
356    #[test]
357    fn test_hash_many_mixed_lengths() {
358        explore(|strategy| {
359            for len in (0..=300).chain([1000, 4113]) {
360                for count in 1..=20 {
361                    let messages: Vec<Vec<u8>> = (0..count)
362                        .map(|lane| {
363                            // Message `len % count` has length `len`, and the others take
364                            // lengths spread over `0..=300`.
365                            let other = (len * 7 + lane * 37) % 301;
366                            message(if lane == len % count { len } else { other }, lane)
367                        })
368                        .collect();
369                    check_hash_many(&messages, strategy);
370                }
371            }
372        });
373    }
374
375    /// Overlapping and aliased messages borrowed from one buffer.
376    #[test]
377    fn test_hash_many_overlapping_messages() {
378        // Message `lane` starts at byte `lane` and spans `100 + 12 * lane` bytes, so the
379        // messages overlap and differ in length.
380        let backing = message(400, 0);
381        let messages: Vec<&[u8]> = (0..20)
382            .map(|lane| &backing[lane..lane * 13 + 100])
383            .collect();
384        explore(|strategy| {
385            check_hash_many(&messages, strategy);
386
387            // Nine aliases of one buffer fill a group of eight lanes and spill into the next. For
388            // the paired kernel, they leave an odd message out.
389            check_hash_many(&[&backing[..]; 9], strategy);
390        });
391    }
392
393    /// The AVX-512 kernel runs on every CPU that has AVX-512F, so the tests above cover it
394    /// there.
395    #[cfg(target_arch = "x86_64")]
396    #[test]
397    fn test_avx512_detection() {
398        assert_eq!(
399            has_avx512f::get(),
400            std::arch::is_x86_feature_detected!("avx512f")
401        );
402    }
403
404    /// The paired kernel runs on every CPU that has the SHA-512 instructions, so the tests above
405    /// cover it there.
406    #[cfg(target_arch = "aarch64")]
407    #[test]
408    fn test_sha3_detection() {
409        assert_eq!(
410            has_sha3::get(),
411            std::arch::is_aarch64_feature_detected!("sha3")
412        );
413    }
414
415    #[test]
416    fn test_codec_and_zeroize() {
417        // A digest round-trips as its raw bytes, and a truncated encoding fails to decode.
418        let mut digest = Sha512::hash(&[b"abc"]);
419        let encoded = digest.encode();
420        assert_eq!(Digest::SIZE, DIGEST_LENGTH);
421        assert_eq!(encoded.as_ref(), ABC_DIGEST);
422        assert_eq!(Digest::decode(encoded).unwrap(), digest);
423        assert!(Digest::decode(Copying(&ABC_DIGEST[..DIGEST_LENGTH - 1])).is_err());
424
425        // Zeroizing leaves the all-zero `EMPTY` digest.
426        digest.zeroize();
427        assert_eq!(digest, <Digest as crate::Digest>::EMPTY);
428    }
429
430    #[test]
431    fn test_digest_ord() {
432        let a = Digest([0; DIGEST_LENGTH]);
433        let mut b = a;
434        b.0[DIGEST_LENGTH - 1] = 1;
435
436        // The first byte decides even when the remaining bytes order the other way.
437        let mut c = a;
438        c.0[0] = 0x7f;
439        c.0[1..].fill(0xff);
440        let mut d = a;
441        d.0[0] = 0x80;
442        for (a, b, expected) in [
443            (a, a, Ordering::Equal),
444            (a, b, Ordering::Less),
445            (b, a, Ordering::Greater),
446            (c, d, Ordering::Less),
447            (d, c, Ordering::Greater),
448        ] {
449            assert_eq!(a.cmp(&b), expected);
450            assert_eq!(a.partial_cmp(&b), Some(expected));
451        }
452    }
453
454    #[cfg(feature = "arbitrary")]
455    mod conformance {
456        use super::*;
457        use commonware_codec::conformance::CodecConformance;
458
459        commonware_conformance::conformance_tests! {
460            CodecConformance<Digest>,
461        }
462    }
463}