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_sdk::axis;
51
52axis! {
53    /// Wiki ADR-031 canonical hash-function family.
54    ///
55    /// Single kernel `hash(input: &[u8], out: &mut [u8])` emitting the
56    /// digest of `input` into the first `Self::MAX_OUTPUT_BYTES` of
57    /// `out`. Per ADR-030's signature constraint every axis-kernel
58    /// method takes `(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>`;
59    /// `HashAxis` exposes only one kernel because per-impl
60    /// (Sha256Hasher / Sha512Hasher / Sha3_256Hasher / Keccak256Hasher
61    /// / Blake3Hasher) the axis position in the model's `AxisTuple`
62    /// already commits to a single digest family.
63    pub trait HashAxis: AxisExtension {
64        const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/HashAxis";
65        const MAX_OUTPUT_BYTES: usize = 64;
66        /// Compute the digest of `input` into `out[..n]`, returning `n`.
67        ///
68        /// # Errors
69        ///
70        /// Returns `ShapeViolation` if `out` is too small to hold the digest.
71        fn hash(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>;
72    }
73}
74
75fn out_too_small_violation() -> ShapeViolation {
76    ShapeViolation {
77        shape_iri: "https://uor.foundation/axis/HashAxis",
78        constraint_iri: "https://uor.foundation/axis/HashAxis/outputBuffer",
79        property_iri: "https://uor.foundation/axis/outputBufferBytes",
80        expected_range: "https://uor.foundation/axis/DigestBytesFit",
81        min_count: 0,
82        max_count: 0,
83        kind: uor_foundation::ViolationKind::ValueCheck,
84    }
85}
86
87// =====================================================================
88// SHA-256 — FIPS-180-4 §6.2
89
90const SHA256_BYTES: usize = 32;
91
92/// FIPS-180-4 SHA-256 hasher. 32-byte digest.
93#[derive(Debug, Clone)]
94pub struct Sha256Hasher {
95    inner: sha2::Sha256,
96}
97
98impl Default for Sha256Hasher {
99    fn default() -> Self {
100        Self::initial()
101    }
102}
103
104impl HashAxis for Sha256Hasher {
105    const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/HashAxis/Sha256";
106    const MAX_OUTPUT_BYTES: usize = SHA256_BYTES;
107
108    fn hash(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
109        if out.len() < SHA256_BYTES {
110            return Err(out_too_small_violation());
111        }
112        let digest = sha2::Sha256::digest(input);
113        out[..SHA256_BYTES].copy_from_slice(&digest);
114        Ok(SHA256_BYTES)
115    }
116}
117
118axis_extension_impl_for_hash_axis!(Sha256Hasher);
119
120impl Hasher for Sha256Hasher {
121    const OUTPUT_BYTES: usize = SHA256_BYTES;
122
123    fn initial() -> Self {
124        Self {
125            inner: sha2::Sha256::new(),
126        }
127    }
128
129    fn fold_byte(mut self, b: u8) -> Self {
130        Sha2Digest::update(&mut self.inner, [b]);
131        self
132    }
133
134    fn fold_bytes(mut self, bytes: &[u8]) -> Self {
135        Sha2Digest::update(&mut self.inner, bytes);
136        self
137    }
138
139    fn finalize(self) -> [u8; 32] {
140        let result = Sha2Digest::finalize(self.inner);
141        let mut out = [0u8; 32];
142        out.copy_from_slice(&result);
143        out
144    }
145}
146
147// =====================================================================
148// SHA-512 — FIPS-180-4 §6.4
149
150const SHA512_BYTES: usize = 64;
151
152/// FIPS-180-4 SHA-512 hasher. 64-byte digest.
153#[derive(Debug, Clone)]
154pub struct Sha512Hasher {
155    inner: sha2::Sha512,
156}
157
158impl Default for Sha512Hasher {
159    fn default() -> Self {
160        <Self as Hasher<64>>::initial()
161    }
162}
163
164impl HashAxis for Sha512Hasher {
165    const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/HashAxis/Sha512";
166    const MAX_OUTPUT_BYTES: usize = SHA512_BYTES;
167
168    fn hash(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
169        if out.len() < SHA512_BYTES {
170            return Err(out_too_small_violation());
171        }
172        let digest = sha2::Sha512::digest(input);
173        out[..SHA512_BYTES].copy_from_slice(&digest);
174        Ok(SHA512_BYTES)
175    }
176}
177
178axis_extension_impl_for_hash_axis!(Sha512Hasher);
179
180impl Hasher<64> for Sha512Hasher {
181    const OUTPUT_BYTES: usize = SHA512_BYTES;
182
183    fn initial() -> Self {
184        Self {
185            inner: sha2::Sha512::new(),
186        }
187    }
188
189    fn fold_byte(mut self, b: u8) -> Self {
190        Sha2Digest::update(&mut self.inner, [b]);
191        self
192    }
193
194    fn fold_bytes(mut self, bytes: &[u8]) -> Self {
195        Sha2Digest::update(&mut self.inner, bytes);
196        self
197    }
198
199    fn finalize(self) -> [u8; 64] {
200        let result = Sha2Digest::finalize(self.inner);
201        let mut out = [0u8; 64];
202        out.copy_from_slice(&result);
203        out
204    }
205}
206
207// =====================================================================
208// SHA3-256 — FIPS-202
209
210const SHA3_256_BYTES: usize = 32;
211
212/// FIPS-202 SHA3-256 hasher. 32-byte digest.
213#[derive(Debug, Clone)]
214pub struct Sha3_256Hasher {
215    inner: sha3::Sha3_256,
216}
217
218impl Default for Sha3_256Hasher {
219    fn default() -> Self {
220        Self::initial()
221    }
222}
223
224impl HashAxis for Sha3_256Hasher {
225    const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/HashAxis/Sha3_256";
226    const MAX_OUTPUT_BYTES: usize = SHA3_256_BYTES;
227
228    fn hash(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
229        if out.len() < SHA3_256_BYTES {
230            return Err(out_too_small_violation());
231        }
232        let digest = sha3::Sha3_256::digest(input);
233        out[..SHA3_256_BYTES].copy_from_slice(&digest);
234        Ok(SHA3_256_BYTES)
235    }
236}
237
238axis_extension_impl_for_hash_axis!(Sha3_256Hasher);
239
240impl Hasher for Sha3_256Hasher {
241    const OUTPUT_BYTES: usize = SHA3_256_BYTES;
242
243    fn initial() -> Self {
244        Self {
245            inner: sha3::Sha3_256::new(),
246        }
247    }
248
249    fn fold_byte(mut self, b: u8) -> Self {
250        Sha3Digest::update(&mut self.inner, [b]);
251        self
252    }
253
254    fn fold_bytes(mut self, bytes: &[u8]) -> Self {
255        Sha3Digest::update(&mut self.inner, bytes);
256        self
257    }
258
259    fn finalize(self) -> [u8; 32] {
260        let result = Sha3Digest::finalize(self.inner);
261        let mut out = [0u8; 32];
262        out.copy_from_slice(&result);
263        out
264    }
265}
266
267// =====================================================================
268// Keccak-256 — pre-FIPS-202 sponge (Ethereum-adopted variant)
269
270const KECCAK256_BYTES: usize = 32;
271
272/// Keccak-256 hasher. 32-byte digest. The pre-FIPS-202 sponge (the
273/// variant adopted by Ethereum); distinguished from SHA3-256 by the
274/// 0x01 vs 0x06 domain-separation byte.
275#[derive(Debug, Clone)]
276pub struct Keccak256Hasher {
277    inner: sha3::Keccak256,
278}
279
280impl Default for Keccak256Hasher {
281    fn default() -> Self {
282        Self::initial()
283    }
284}
285
286impl HashAxis for Keccak256Hasher {
287    const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/HashAxis/Keccak256";
288    const MAX_OUTPUT_BYTES: usize = KECCAK256_BYTES;
289
290    fn hash(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
291        if out.len() < KECCAK256_BYTES {
292            return Err(out_too_small_violation());
293        }
294        let digest = sha3::Keccak256::digest(input);
295        out[..KECCAK256_BYTES].copy_from_slice(&digest);
296        Ok(KECCAK256_BYTES)
297    }
298}
299
300axis_extension_impl_for_hash_axis!(Keccak256Hasher);
301
302impl Hasher for Keccak256Hasher {
303    const OUTPUT_BYTES: usize = KECCAK256_BYTES;
304
305    fn initial() -> Self {
306        Self {
307            inner: sha3::Keccak256::new(),
308        }
309    }
310
311    fn fold_byte(mut self, b: u8) -> Self {
312        Sha3Digest::update(&mut self.inner, [b]);
313        self
314    }
315
316    fn fold_bytes(mut self, bytes: &[u8]) -> Self {
317        Sha3Digest::update(&mut self.inner, bytes);
318        self
319    }
320
321    fn finalize(self) -> [u8; 32] {
322        let result = Sha3Digest::finalize(self.inner);
323        let mut out = [0u8; 32];
324        out.copy_from_slice(&result);
325        out
326    }
327}
328
329// =====================================================================
330// BLAKE3
331
332const BLAKE3_BYTES: usize = 32;
333
334/// BLAKE3 hasher. 32-byte digest (the standard BLAKE3 output width;
335/// XOF mode is not exposed at the axis level).
336#[derive(Debug, Clone, Default)]
337pub struct Blake3Hasher {
338    inner: blake3::Hasher,
339}
340
341impl HashAxis for Blake3Hasher {
342    const AXIS_ADDRESS: &'static str = "https://uor.foundation/axis/HashAxis/Blake3";
343    const MAX_OUTPUT_BYTES: usize = BLAKE3_BYTES;
344
345    fn hash(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
346        if out.len() < BLAKE3_BYTES {
347            return Err(out_too_small_violation());
348        }
349        let digest = blake3::hash(input);
350        out[..BLAKE3_BYTES].copy_from_slice(digest.as_bytes());
351        Ok(BLAKE3_BYTES)
352    }
353}
354
355axis_extension_impl_for_hash_axis!(Blake3Hasher);
356
357impl Hasher for Blake3Hasher {
358    const OUTPUT_BYTES: usize = BLAKE3_BYTES;
359
360    fn initial() -> Self {
361        Self::default()
362    }
363
364    fn fold_byte(mut self, b: u8) -> Self {
365        self.inner.update(&[b]);
366        self
367    }
368
369    fn fold_bytes(mut self, bytes: &[u8]) -> Self {
370        self.inner.update(bytes);
371        self
372    }
373
374    fn finalize(self) -> [u8; 32] {
375        *self.inner.finalize().as_bytes()
376    }
377}