Skip to main content

amiss_wire/
digest.rs

1use core::fmt;
2
3use sha2::{Digest as _, Sha256};
4
5use crate::json::{Sink, Value, stream};
6
7/// The domain for a digest over exact raw bytes taken as evidence: a resolved
8/// target's blob, or one build lockfile as the release manifest records it.
9pub const RAW_EVIDENCE_DOMAIN: &str = "amiss/raw-evidence/v1";
10
11#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct Digest([u8; 32]);
13
14impl Digest {
15    #[must_use]
16    pub const fn as_bytes(&self) -> &[u8; 32] {
17        &self.0
18    }
19
20    /// Parses the `sha256:` wire form with exactly 64 lowercase hex digits.
21    #[must_use]
22    pub fn from_wire(raw: &str) -> Option<Self> {
23        let hex = raw.strip_prefix("sha256:")?;
24        if hex.len() != 64 {
25            return None;
26        }
27        let mut out = [0_u8; 32];
28        for (slot, pair) in out.iter_mut().zip(hex.as_bytes().chunks_exact(2)) {
29            let [high, low] = pair else { return None };
30            *slot = hex_value(*high)?.wrapping_shl(4) | hex_value(*low)?;
31        }
32        Some(Self(out))
33    }
34}
35
36fn hex_value(byte: u8) -> Option<u8> {
37    match byte {
38        b'0'..=b'9' => Some(byte.wrapping_sub(b'0')),
39        b'a'..=b'f' => Some(byte.wrapping_sub(b'a').wrapping_add(10)),
40        _ => None,
41    }
42}
43
44impl fmt::Display for Digest {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.write_str("sha256:")?;
47        for byte in self.0 {
48            write!(f, "{byte:02x}")?;
49        }
50        Ok(())
51    }
52}
53
54impl fmt::Debug for Digest {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        fmt::Display::fmt(self, f)
57    }
58}
59
60/// The plain SHA-256 of exact bytes, with no domain separation: the
61/// manifest's file and binary checksums are ordinary content digests, not
62/// domain-separated identities.
63#[must_use]
64pub fn sha256(bytes: &[u8]) -> Digest {
65    let mut hasher = Sha256::new();
66    hasher.update(bytes);
67    Digest(hasher.finalize().into())
68}
69
70#[must_use]
71pub fn hb(domain: &str, bytes: &[u8]) -> Digest {
72    let mut hasher = with_domain(domain);
73    hasher.update(bytes);
74    Digest(hasher.finalize().into())
75}
76
77#[must_use]
78pub fn hj(domain: &str, value: &Value) -> Digest {
79    let mut sink = HashSink(with_domain(domain));
80    stream(value, &mut sink);
81    Digest(sink.0.finalize().into())
82}
83
84struct HashSink(Sha256);
85
86impl Sink for HashSink {
87    fn write(&mut self, piece: &str) {
88        self.0.update(piece.as_bytes());
89    }
90}
91
92fn with_domain(domain: &str) -> Sha256 {
93    let mut hasher = Sha256::new();
94    hasher.update(domain.as_bytes());
95    hasher.update([0_u8]);
96    hasher
97}