Skip to main content

prism_crypto/
hash.rs

1//! `HashAxis` declaration and FIPS-180-4 / FIPS-202 / BLAKE3 impls.
2//!
3//! The axis declaration and its impls live in the same module per the
4//! Rust constraint that `#[macro_export]` macros emitted from a sub-
5//! module by proc-macro expansion are not reachable from sibling
6//! modules via either `use crate::<macro>` or bare-name resolution
7//! (Rust issue #52234). Consolidating per-axis impls into one file
8//! keeps the companion-macro call in scope at every invocation site.
9//!
10//! # ADR-055 substrate-Term verb body discipline
11//!
12//! Per [Wiki ADR-055](https://github.com/UOR-Foundation/UOR-Framework/wiki/09-Architecture-Decisions)
13//! every `AxisExtension` impl carries a substrate-Term verb body via
14//! the foundation-declared `SubstrateTermBody` supertrait. The
15//! `axis!` companion macro in foundation-sdk 0.4.11 emits a default
16//! empty `body_arena()` for every impl that doesn't supply an
17//! explicit `body = |input| { … };` clause — ADR-055 names this the
18//! **primitive-fast-path-equivalent realization**: the kernel-function
19//! dispatch path below is byte-output-equivalent to recursive
20//! fold-fusion through an empty body arena, so the hand-written
21//! kernel bodies satisfy the discipline as-shipped.
22//!
23//! Explicit substrate-Term canonical body composition for the hash
24//! family — compressing 32-/64-byte internal-state blocks via composed
25//! `Add` (mod 2^32 or 2^64), `Xor`, `And`, `Or`, `Bnot`, plus a `rotr`
26//! sub-verb composing `Or(Div(x, 2^k), Mul(x, 2^(width-k)))` per
27//! ADR-054 § Substrate-Term realization examples plus pad-and-finalize
28//! via `Concat` per ADR-056 — is **syntactically expressible** in
29//! foundation-sdk 0.4.11's verb-body grammar (every PrimitiveOp call
30//! form including `div`/`r#mod`/`pow`/`concat`/`le`/`lt`/`ge`/`gt`
31//! plus `hash` axis invocation is admitted in verb/axis bodies per
32//! ADR-056). The remaining work is **operational composition**: each
33//! canonical hash impl's 64- / 80- / 24-round compression unfolded as
34//! `fold_n` over the round-constant table is a published-roster
35//! follow-on; the hand-written kernel bodies below remain the
36//! operational form pending that composition.
37//!
38//! Byte-output equivalence with the canonical reference vectors
39//! (FIPS-180-4, FIPS-202, BLAKE3 spec) is verified by direct vectors
40//! in `tests/conformance.rs`. Per ADR-055's byte-output-equivalence-
41//! at-every-input clause the kernel-dispatch path and any future
42//! explicit substrate-Term `body` clause produce byte-identical
43//! outputs.
44
45#![allow(missing_docs)]
46
47use sha2::Digest as Sha2Digest;
48use sha3::Digest as Sha3Digest;
49use uor_foundation::enforcement::{Hasher, ShapeViolation};
50use uor_foundation::pipeline::AxisExtension;
51use uor_foundation_sdk::axis;
52
53axis! {
54    /// Wiki ADR-031 canonical hash-function family.
55    ///
56    /// Single kernel `hash(input: &[u8], out: &mut [u8])` emitting the
57    /// digest of `input` into the first `Self::MAX_OUTPUT_BYTES` of
58    /// `out`. Per ADR-030's signature constraint every axis-kernel
59    /// method takes `(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>`;
60    /// `HashAxis` exposes only one kernel because per-impl
61    /// (Sha256Hasher / Sha512Hasher / Sha3_256Hasher / Keccak256Hasher
62    /// / Blake3Hasher) the axis position in the model's `AxisTuple`
63    /// already commits to a single digest family.
64    pub trait HashAxis: AxisExtension {
65        const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/HashAxis";
66        const MAX_OUTPUT_BYTES: usize = 64;
67        /// Compute the digest of `input` into `out[..n]`, returning `n`.
68        ///
69        /// # Errors
70        ///
71        /// Returns `ShapeViolation` if `out` is too small to hold the digest.
72        fn hash(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>;
73    }
74}
75
76fn out_too_small_violation() -> ShapeViolation {
77    ShapeViolation {
78        shape_iri: "https://uor.foundation/axis/HashAxis",
79        constraint_iri: "https://uor.foundation/axis/HashAxis/outputBuffer",
80        property_iri: "https://uor.foundation/axis/outputBufferBytes",
81        expected_range: "https://uor.foundation/axis/DigestBytesFit",
82        min_count: 0,
83        max_count: 0,
84        kind: uor_foundation::ViolationKind::ValueCheck,
85    }
86}
87
88// =====================================================================
89// SHA-256 — FIPS-180-4 §6.2
90
91const SHA256_BYTES: usize = 32;
92
93/// FIPS-180-4 SHA-256 hasher. 32-byte digest.
94#[derive(Debug, Clone)]
95pub struct Sha256Hasher {
96    inner: sha2::Sha256,
97}
98
99impl Default for Sha256Hasher {
100    fn default() -> Self {
101        Self::initial()
102    }
103}
104
105impl HashAxis for Sha256Hasher {
106    const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/HashAxis/Sha256";
107    const MAX_OUTPUT_BYTES: usize = SHA256_BYTES;
108
109    fn hash(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
110        if out.len() < SHA256_BYTES {
111            return Err(out_too_small_violation());
112        }
113        let digest = sha2::Sha256::digest(input);
114        out[..SHA256_BYTES].copy_from_slice(&digest);
115        Ok(SHA256_BYTES)
116    }
117}
118
119axis_extension_impl_for_hash_axis!(Sha256Hasher);
120
121impl Hasher for Sha256Hasher {
122    const OUTPUT_BYTES: usize = SHA256_BYTES;
123
124    fn initial() -> Self {
125        Self {
126            inner: sha2::Sha256::new(),
127        }
128    }
129
130    fn fold_byte(mut self, b: u8) -> Self {
131        Sha2Digest::update(&mut self.inner, [b]);
132        self
133    }
134
135    fn fold_bytes(mut self, bytes: &[u8]) -> Self {
136        Sha2Digest::update(&mut self.inner, bytes);
137        self
138    }
139
140    fn finalize(self) -> [u8; 32] {
141        let result = Sha2Digest::finalize(self.inner);
142        let mut out = [0u8; 32];
143        out.copy_from_slice(&result);
144        out
145    }
146}
147
148// =====================================================================
149// SHA-512 — FIPS-180-4 §6.4
150
151const SHA512_BYTES: usize = 64;
152
153/// FIPS-180-4 SHA-512 hasher. 64-byte digest.
154#[derive(Debug, Clone)]
155pub struct Sha512Hasher {
156    inner: sha2::Sha512,
157}
158
159impl Default for Sha512Hasher {
160    fn default() -> Self {
161        <Self as Hasher<64>>::initial()
162    }
163}
164
165impl HashAxis for Sha512Hasher {
166    const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/HashAxis/Sha512";
167    const MAX_OUTPUT_BYTES: usize = SHA512_BYTES;
168
169    fn hash(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
170        if out.len() < SHA512_BYTES {
171            return Err(out_too_small_violation());
172        }
173        let digest = sha2::Sha512::digest(input);
174        out[..SHA512_BYTES].copy_from_slice(&digest);
175        Ok(SHA512_BYTES)
176    }
177}
178
179axis_extension_impl_for_hash_axis!(Sha512Hasher);
180
181impl Hasher<64> for Sha512Hasher {
182    const OUTPUT_BYTES: usize = SHA512_BYTES;
183
184    fn initial() -> Self {
185        Self {
186            inner: sha2::Sha512::new(),
187        }
188    }
189
190    fn fold_byte(mut self, b: u8) -> Self {
191        Sha2Digest::update(&mut self.inner, [b]);
192        self
193    }
194
195    fn fold_bytes(mut self, bytes: &[u8]) -> Self {
196        Sha2Digest::update(&mut self.inner, bytes);
197        self
198    }
199
200    fn finalize(self) -> [u8; 64] {
201        let result = Sha2Digest::finalize(self.inner);
202        let mut out = [0u8; 64];
203        out.copy_from_slice(&result);
204        out
205    }
206}
207
208// =====================================================================
209// SHA3-256 — FIPS-202
210
211const SHA3_256_BYTES: usize = 32;
212
213/// FIPS-202 SHA3-256 hasher. 32-byte digest.
214#[derive(Debug, Clone)]
215pub struct Sha3_256Hasher {
216    inner: sha3::Sha3_256,
217}
218
219impl Default for Sha3_256Hasher {
220    fn default() -> Self {
221        Self::initial()
222    }
223}
224
225impl HashAxis for Sha3_256Hasher {
226    const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/HashAxis/Sha3_256";
227    const MAX_OUTPUT_BYTES: usize = SHA3_256_BYTES;
228
229    fn hash(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
230        if out.len() < SHA3_256_BYTES {
231            return Err(out_too_small_violation());
232        }
233        let digest = sha3::Sha3_256::digest(input);
234        out[..SHA3_256_BYTES].copy_from_slice(&digest);
235        Ok(SHA3_256_BYTES)
236    }
237}
238
239axis_extension_impl_for_hash_axis!(Sha3_256Hasher);
240
241impl Hasher for Sha3_256Hasher {
242    const OUTPUT_BYTES: usize = SHA3_256_BYTES;
243
244    fn initial() -> Self {
245        Self {
246            inner: sha3::Sha3_256::new(),
247        }
248    }
249
250    fn fold_byte(mut self, b: u8) -> Self {
251        Sha3Digest::update(&mut self.inner, [b]);
252        self
253    }
254
255    fn fold_bytes(mut self, bytes: &[u8]) -> Self {
256        Sha3Digest::update(&mut self.inner, bytes);
257        self
258    }
259
260    fn finalize(self) -> [u8; 32] {
261        let result = Sha3Digest::finalize(self.inner);
262        let mut out = [0u8; 32];
263        out.copy_from_slice(&result);
264        out
265    }
266}
267
268// =====================================================================
269// Keccak-256 — pre-FIPS-202 sponge (Ethereum-adopted variant)
270
271const KECCAK256_BYTES: usize = 32;
272
273/// Keccak-256 hasher. 32-byte digest. The pre-FIPS-202 sponge (the
274/// variant adopted by Ethereum); distinguished from SHA3-256 by the
275/// 0x01 vs 0x06 domain-separation byte.
276#[derive(Debug, Clone)]
277pub struct Keccak256Hasher {
278    inner: sha3::Keccak256,
279}
280
281impl Default for Keccak256Hasher {
282    fn default() -> Self {
283        Self::initial()
284    }
285}
286
287impl HashAxis for Keccak256Hasher {
288    const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/HashAxis/Keccak256";
289    const MAX_OUTPUT_BYTES: usize = KECCAK256_BYTES;
290
291    fn hash(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
292        if out.len() < KECCAK256_BYTES {
293            return Err(out_too_small_violation());
294        }
295        let digest = sha3::Keccak256::digest(input);
296        out[..KECCAK256_BYTES].copy_from_slice(&digest);
297        Ok(KECCAK256_BYTES)
298    }
299}
300
301axis_extension_impl_for_hash_axis!(Keccak256Hasher);
302
303impl Hasher for Keccak256Hasher {
304    const OUTPUT_BYTES: usize = KECCAK256_BYTES;
305
306    fn initial() -> Self {
307        Self {
308            inner: sha3::Keccak256::new(),
309        }
310    }
311
312    fn fold_byte(mut self, b: u8) -> Self {
313        Sha3Digest::update(&mut self.inner, [b]);
314        self
315    }
316
317    fn fold_bytes(mut self, bytes: &[u8]) -> Self {
318        Sha3Digest::update(&mut self.inner, bytes);
319        self
320    }
321
322    fn finalize(self) -> [u8; 32] {
323        let result = Sha3Digest::finalize(self.inner);
324        let mut out = [0u8; 32];
325        out.copy_from_slice(&result);
326        out
327    }
328}
329
330// =====================================================================
331// BLAKE3
332
333const BLAKE3_BYTES: usize = 32;
334
335/// BLAKE3 hasher. 32-byte digest (the standard BLAKE3 output width;
336/// XOF mode is not exposed at the axis level).
337#[derive(Debug, Clone, Default)]
338pub struct Blake3Hasher {
339    inner: blake3::Hasher,
340}
341
342impl HashAxis for Blake3Hasher {
343    const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/HashAxis/Blake3";
344    const MAX_OUTPUT_BYTES: usize = BLAKE3_BYTES;
345
346    fn hash(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
347        if out.len() < BLAKE3_BYTES {
348            return Err(out_too_small_violation());
349        }
350        let digest = blake3::hash(input);
351        out[..BLAKE3_BYTES].copy_from_slice(digest.as_bytes());
352        Ok(BLAKE3_BYTES)
353    }
354}
355
356axis_extension_impl_for_hash_axis!(Blake3Hasher);
357
358impl Hasher for Blake3Hasher {
359    const OUTPUT_BYTES: usize = BLAKE3_BYTES;
360
361    fn initial() -> Self {
362        Self::default()
363    }
364
365    fn fold_byte(mut self, b: u8) -> Self {
366        self.inner.update(&[b]);
367        self
368    }
369
370    fn fold_bytes(mut self, bytes: &[u8]) -> Self {
371        self.inner.update(bytes);
372        self
373    }
374
375    fn finalize(self) -> [u8; 32] {
376        *self.inner.finalize().as_bytes()
377    }
378}