Skip to main content

dsfb_gpu_debug_core/
hash.rs

1//! Hand-rolled SHA-256.
2//!
3//! Why hand-rolled: the `dsfb-gpu-debug-core` crate is `no_std` and
4//! dependency-free by policy. The case file's hash chain is the load-bearing
5//! audit artifact; if it depended on an external SHA-256 crate, the chain
6//! would inherit that crate's compatibility and supply-chain surface. A
7//! ~150-line in-tree implementation eliminates both.
8//!
9//! Correctness is validated against the published FIPS 180-4 test vectors
10//! plus a small handful of additional edge cases (empty input, one block,
11//! exactly 56 bytes, exactly 64 bytes, and a multi-block input).
12//!
13//! API shape: a streaming `Sha256` builder for incremental hashing of
14//! pipeline buffers, and a `sha256(bytes)` one-shot wrapper for the cases
15//! where the input is already in memory. Both produce a `[u8; 32]` digest.
16
17#![allow(clippy::module_name_repetitions)]
18
19/// Length in bytes of a SHA-256 digest.
20pub const DIGEST_BYTES: usize = 32;
21
22/// SHA-256 round constants (FIPS 180-4 §4.2.2). First 32 bits of the
23/// fractional parts of the cube roots of the first 64 primes.
24const K: [u32; 64] = [
25    0x428a_2f98,
26    0x7137_4491,
27    0xb5c0_fbcf,
28    0xe9b5_dba5,
29    0x3956_c25b,
30    0x59f1_11f1,
31    0x923f_82a4,
32    0xab1c_5ed5,
33    0xd807_aa98,
34    0x1283_5b01,
35    0x2431_85be,
36    0x550c_7dc3,
37    0x72be_5d74,
38    0x80de_b1fe,
39    0x9bdc_06a7,
40    0xc19b_f174,
41    0xe49b_69c1,
42    0xefbe_4786,
43    0x0fc1_9dc6,
44    0x240c_a1cc,
45    0x2de9_2c6f,
46    0x4a74_84aa,
47    0x5cb0_a9dc,
48    0x76f9_88da,
49    0x983e_5152,
50    0xa831_c66d,
51    0xb003_27c8,
52    0xbf59_7fc7,
53    0xc6e0_0bf3,
54    0xd5a7_9147,
55    0x06ca_6351,
56    0x1429_2967,
57    0x27b7_0a85,
58    0x2e1b_2138,
59    0x4d2c_6dfc,
60    0x5338_0d13,
61    0x650a_7354,
62    0x766a_0abb,
63    0x81c2_c92e,
64    0x9272_2c85,
65    0xa2bf_e8a1,
66    0xa81a_664b,
67    0xc24b_8b70,
68    0xc76c_51a3,
69    0xd192_e819,
70    0xd699_0624,
71    0xf40e_3585,
72    0x106a_a070,
73    0x19a4_c116,
74    0x1e37_6c08,
75    0x2748_774c,
76    0x34b0_bcb5,
77    0x391c_0cb3,
78    0x4ed8_aa4a,
79    0x5b9c_ca4f,
80    0x682e_6ff3,
81    0x748f_82ee,
82    0x78a5_636f,
83    0x84c8_7814,
84    0x8cc7_0208,
85    0x90be_fffa,
86    0xa450_6ceb,
87    0xbef9_a3f7,
88    0xc671_78f2,
89];
90
91/// Initial hash values (FIPS 180-4 §5.3.3). First 32 bits of the fractional
92/// parts of the square roots of the first 8 primes.
93const H_INIT: [u32; 8] = [
94    0x6a09_e667,
95    0xbb67_ae85,
96    0x3c6e_f372,
97    0xa54f_f53a,
98    0x510e_527f,
99    0x9b05_688c,
100    0x1f83_d9ab,
101    0x5be0_cd19,
102];
103
104/// Streaming SHA-256 state.
105///
106/// Buffers up to 64 bytes (one compression-function block) before processing.
107/// `len_bits` tracks the total message length so the padding step can write
108/// the canonical 64-bit big-endian length suffix even when the caller
109/// supplies bytes in many small chunks.
110#[derive(Clone)]
111pub struct Sha256 {
112    state: [u32; 8],
113    buffer: [u8; 64],
114    buffer_len: usize,
115    len_bits: u64,
116}
117
118impl Default for Sha256 {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124impl Sha256 {
125    /// Construct a fresh hasher with the standard initial state.
126    #[must_use]
127    pub const fn new() -> Self {
128        Self {
129            state: H_INIT,
130            buffer: [0u8; 64],
131            buffer_len: 0,
132            len_bits: 0,
133        }
134    }
135
136    /// Absorb `data` into the running hash. Safe to call repeatedly with
137    /// arbitrary chunk sizes — partial blocks are buffered.
138    pub fn update(&mut self, data: &[u8]) {
139        // Account for the new bytes in the running message-length counter
140        // first, in bits, so the final padding step can write the canonical
141        // 64-bit length suffix.
142        self.len_bits = self.len_bits.wrapping_add((data.len() as u64) * 8);
143
144        let mut input = data;
145
146        // If there is a partial block sitting in the buffer, fill it first.
147        if self.buffer_len > 0 {
148            let take = (64 - self.buffer_len).min(input.len());
149            self.buffer[self.buffer_len..self.buffer_len + take].copy_from_slice(&input[..take]);
150            self.buffer_len += take;
151            input = &input[take..];
152            if self.buffer_len == 64 {
153                let block = self.buffer;
154                Self::compress(&mut self.state, &block);
155                self.buffer_len = 0;
156            }
157        }
158
159        // Process whole 64-byte blocks straight out of the input slice. The
160        // `split_first_chunk` route is preferred over `try_into().unwrap()`
161        // because it doesn't introduce a panic path; the `while` condition
162        // already guarantees a block is present.
163        while let Some((block, rest)) = input.split_first_chunk::<64>() {
164            Self::compress(&mut self.state, block);
165            input = rest;
166        }
167
168        // Stash the trailing partial block for the next update or finalize.
169        if !input.is_empty() {
170            self.buffer[..input.len()].copy_from_slice(input);
171            self.buffer_len = input.len();
172        }
173    }
174
175    /// Finalize the hash, returning the 32-byte digest. Consumes `self`.
176    #[must_use]
177    pub fn finalize(mut self) -> [u8; DIGEST_BYTES] {
178        // Standard SHA-2 padding: append 0x80, then enough zero bytes so the
179        // total length (including the 8-byte length suffix) is a multiple of
180        // 64, then the original message length in bits as a big-endian u64.
181        let len_bits = self.len_bits;
182
183        let mut buf = self.buffer;
184        let mut buf_len = self.buffer_len;
185        buf[buf_len] = 0x80;
186        buf_len += 1;
187
188        if buf_len > 56 {
189            // Not enough room in this block for the length suffix; pad out
190            // and compress, then start a fresh block for the suffix.
191            for byte in &mut buf[buf_len..64] {
192                *byte = 0;
193            }
194            Self::compress(&mut self.state, &buf);
195            buf = [0u8; 64];
196            buf_len = 0;
197        }
198
199        for byte in &mut buf[buf_len..56] {
200            *byte = 0;
201        }
202        buf[56..64].copy_from_slice(&len_bits.to_be_bytes());
203        Self::compress(&mut self.state, &buf);
204
205        let mut digest = [0u8; 32];
206        for (i, word) in self.state.iter().enumerate() {
207            digest[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes());
208        }
209        digest
210    }
211
212    /// The SHA-256 compression function. Operates on a single 64-byte block
213    /// and a mutable hash state. Direct transcription of FIPS 180-4 §6.2.2.
214    ///
215    /// The working variables are named `a..h` to match the published
216    /// specification verbatim, which makes the line-by-line correspondence
217    /// auditable. Renaming them to something more verbose would obscure
218    /// that mapping for readers cross-checking against FIPS 180-4.
219    #[allow(clippy::many_single_char_names)]
220    fn compress(state: &mut [u32; 8], block: &[u8; 64]) {
221        let mut w = [0u32; 64];
222        for i in 0..16 {
223            let off = i * 4;
224            w[i] = u32::from_be_bytes([block[off], block[off + 1], block[off + 2], block[off + 3]]);
225        }
226        for i in 16..64 {
227            let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
228            let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
229            w[i] = w[i - 16]
230                .wrapping_add(s0)
231                .wrapping_add(w[i - 7])
232                .wrapping_add(s1);
233        }
234
235        let mut a = state[0];
236        let mut b = state[1];
237        let mut c = state[2];
238        let mut d = state[3];
239        let mut e = state[4];
240        let mut f = state[5];
241        let mut g = state[6];
242        let mut h = state[7];
243
244        for i in 0..64 {
245            let big_sigma1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
246            let ch = (e & f) ^ ((!e) & g);
247            let t1 = h
248                .wrapping_add(big_sigma1)
249                .wrapping_add(ch)
250                .wrapping_add(K[i])
251                .wrapping_add(w[i]);
252            let big_sigma0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
253            let maj = (a & b) ^ (a & c) ^ (b & c);
254            let t2 = big_sigma0.wrapping_add(maj);
255
256            h = g;
257            g = f;
258            f = e;
259            e = d.wrapping_add(t1);
260            d = c;
261            c = b;
262            b = a;
263            a = t1.wrapping_add(t2);
264        }
265
266        state[0] = state[0].wrapping_add(a);
267        state[1] = state[1].wrapping_add(b);
268        state[2] = state[2].wrapping_add(c);
269        state[3] = state[3].wrapping_add(d);
270        state[4] = state[4].wrapping_add(e);
271        state[5] = state[5].wrapping_add(f);
272        state[6] = state[6].wrapping_add(g);
273        state[7] = state[7].wrapping_add(h);
274    }
275}
276
277/// One-shot SHA-256 of an in-memory byte slice. Useful when the caller has
278/// already assembled the canonical byte representation of a stage's output.
279#[must_use]
280pub fn sha256(bytes: &[u8]) -> [u8; DIGEST_BYTES] {
281    let mut hasher = Sha256::new();
282    hasher.update(bytes);
283    hasher.finalize()
284}
285
286/// Format a 32-byte digest as a lowercase hex string with the `sha256:`
287/// prefix the contract format expects. Used by the case-file serializer.
288#[must_use]
289pub fn format_digest(digest: &[u8; DIGEST_BYTES]) -> [u8; 71] {
290    const HEX: &[u8; 16] = b"0123456789abcdef";
291    let mut out = [0u8; 71];
292    out[..7].copy_from_slice(b"sha256:");
293    for (i, byte) in digest.iter().enumerate() {
294        out[7 + i * 2] = HEX[(byte >> 4) as usize];
295        out[7 + i * 2 + 1] = HEX[(byte & 0x0F) as usize];
296    }
297    out
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    /// Known-vector test from FIPS 180-4 / NIST CSRC: empty input.
305    /// Expected digest: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
306    #[test]
307    fn sha256_empty_string() {
308        let digest = sha256(b"");
309        let expected = [
310            0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f,
311            0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b,
312            0x78, 0x52, 0xb8, 0x55,
313        ];
314        assert_eq!(digest, expected);
315    }
316
317    /// Known-vector test from FIPS 180-4: "abc".
318    /// Expected digest: ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
319    #[test]
320    fn sha256_abc() {
321        let digest = sha256(b"abc");
322        let expected = [
323            0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae,
324            0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61,
325            0xf2, 0x00, 0x15, 0xad,
326        ];
327        assert_eq!(digest, expected);
328    }
329
330    /// Known-vector test from FIPS 180-4: the 56-byte boundary input.
331    /// Exercises the multi-block padding path.
332    /// "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
333    /// Expected digest: 248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1
334    #[test]
335    fn sha256_56_byte_input_exercises_padding() {
336        let msg: &[u8] = b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
337        assert_eq!(msg.len(), 56);
338        let digest = sha256(msg);
339        let expected = [
340            0x24, 0x8d, 0x6a, 0x61, 0xd2, 0x06, 0x38, 0xb8, 0xe5, 0xc0, 0x26, 0x93, 0x0c, 0x3e,
341            0x60, 0x39, 0xa3, 0x3c, 0xe4, 0x59, 0x64, 0xff, 0x21, 0x67, 0xf6, 0xec, 0xed, 0xd4,
342            0x19, 0xdb, 0x06, 0xc1,
343        ];
344        assert_eq!(digest, expected);
345    }
346
347    /// Million-a vector compressed to a smaller version for unit-test runtime:
348    /// 1000 bytes of 'a'. Validated against the openssl/coreutils reference
349    /// implementation.
350    #[test]
351    fn sha256_thousand_a() {
352        let mut data = [0u8; 1000];
353        data.fill(b'a');
354        let digest = sha256(&data);
355        // sha256(1000 * 'a') — verified out-of-band against a reference impl.
356        let expected = [
357            0x41, 0xed, 0xec, 0xe4, 0x2d, 0x63, 0xe8, 0xd9, 0xbf, 0x51, 0x5a, 0x9b, 0xa6, 0x93,
358            0x2e, 0x1c, 0x20, 0xcb, 0xc9, 0xf5, 0xa5, 0xd1, 0x34, 0x64, 0x5a, 0xdb, 0x5d, 0xb1,
359            0xb9, 0x73, 0x7e, 0xa3,
360        ];
361        assert_eq!(digest, expected);
362    }
363
364    /// Streaming-mode equivalence: hashing a single buffer one byte at a time
365    /// must produce the same digest as hashing it in one call.
366    #[test]
367    fn streaming_matches_one_shot() {
368        let msg: &[u8] = b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
369        let one_shot = sha256(msg);
370        let mut h = Sha256::new();
371        for byte in msg {
372            h.update(core::slice::from_ref(byte));
373        }
374        let streamed = h.finalize();
375        assert_eq!(streamed, one_shot);
376    }
377
378    /// Streaming with mid-block boundaries equals one-shot.
379    #[test]
380    fn streaming_with_split_chunks_matches() {
381        let msg: &[u8] = b"the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog";
382        let one_shot = sha256(msg);
383        let mut h = Sha256::new();
384        h.update(&msg[..3]);
385        h.update(&msg[3..30]);
386        h.update(&msg[30..64]);
387        h.update(&msg[64..]);
388        assert_eq!(h.finalize(), one_shot);
389    }
390
391    /// The hex formatter prefixes "sha256:" and emits 64 lowercase hex chars.
392    #[test]
393    fn format_digest_emits_sha256_prefix() {
394        let digest = sha256(b"");
395        let formatted = format_digest(&digest);
396        assert!(formatted.starts_with(b"sha256:"));
397        assert_eq!(formatted.len(), 71);
398        // 64 hex characters of the empty-string digest.
399        assert_eq!(
400            &formatted[7..],
401            b"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
402        );
403    }
404}