Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

update-bundle/src/digest.rs

//! Content digests. Every bundle member carries **both** a SHA-256 and a BLAKE3
//! digest; verification recomputes both over the exact stored bytes.
//!
//! Self-contained (`sha2` + `blake3`) — the bundle's digests are a closed domain
//! (they are compared only against other digests stored in the same bundle), so
//! this crate does not couple to the wider workspace hash primitive.

use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

/// Lowercase hex encoding of a byte slice.
pub fn hex_lower(bytes: &[u8]) -> String {
    const LUT: &[u8; 16] = b"0123456789abcdef";
    let mut out = String::with_capacity(bytes.len() * 2);
    for &b in bytes {
        out.push(LUT[(b >> 4) as usize] as char);
        out.push(LUT[(b & 0x0f) as usize] as char);
    }
    out
}

/// Decode a lowercase/uppercase hex string to bytes. Returns `None` on any
/// non-hex character or odd length.
pub fn hex_decode(s: &str) -> Option<Vec<u8>> {
    let s = s.as_bytes();
    if !s.len().is_multiple_of(2) {
        return None;
    }
    fn nib(b: u8) -> Option<u8> {
        match b {
            b'0'..=b'9' => Some(b - b'0'),
            b'a'..=b'f' => Some(b - b'a' + 10),
            b'A'..=b'F' => Some(b - b'A' + 10),
            _ => None,
        }
    }
    let mut out = Vec::with_capacity(s.len() / 2);
    for pair in s.chunks(2) {
        out.push((nib(pair[0])? << 4) | nib(pair[1])?);
    }
    Some(out)
}

/// SHA-256 of `bytes` as 64 lowercase hex characters.
pub fn sha256_hex(bytes: &[u8]) -> String {
    hex_lower(&Sha256::digest(bytes))
}

/// BLAKE3 of `bytes` as 64 lowercase hex characters.
pub fn blake3_hex(bytes: &[u8]) -> String {
    blake3::hash(bytes).to_hex().to_string()
}

/// The dual digest + size recorded for every payload and evidence member.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Digests {
    pub size_bytes: u64,
    pub sha256: String,
    pub blake3: String,
}

impl Digests {
    /// Compute the dual digest of `bytes`.
    pub fn of(bytes: &[u8]) -> Self {
        Digests {
            size_bytes: bytes.len() as u64,
            sha256: sha256_hex(bytes),
            blake3: blake3_hex(bytes),
        }
    }

    /// [`Digests::of`] for every member at once, in input order (stinky 30). Each
    /// member's SHA-256 and its BLAKE3 are SEPARATE units on one gatling fan-out,
    /// so a single large payload costs the slower of the two hashes rather than
    /// their sum, and many members hash on every core. Bit-identical to calling
    /// [`Digests::of`] on each.
    pub fn of_many(members: &[&[u8]]) -> Vec<Digests> {
        let hex = gatling::gatling_forkjoin::gatling_for_each(members.len() * 2, 0, |u| {
            let bytes = members[u / 2];
            if u % 2 == 0 {
                sha256_hex(bytes)
            } else {
                blake3_hex(bytes)
            }
        });
        let mut it = hex.into_iter();
        members
            .iter()
            .map(|m| Digests {
                size_bytes: m.len() as u64,
                sha256: it.next().unwrap_or_default(),
                blake3: it.next().unwrap_or_default(),
            })
            .collect()
    }

    /// True when both digests and the size match `bytes` exactly.
    pub fn matches(&self, bytes: &[u8]) -> bool {
        self.size_bytes == bytes.len() as u64
            && self.sha256 == sha256_hex(bytes)
            && self.blake3 == blake3_hex(bytes)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn known_vectors() {
        // SHA-256("") and BLAKE3("") well-known digests.
        assert_eq!(
            sha256_hex(b""),
            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
        );
        assert_eq!(
            blake3_hex(b""),
            "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
        );
    }

    #[test]
    fn digests_roundtrip_and_detect_change() {
        let d = Digests::of(b"firmware-bytes");
        assert!(d.matches(b"firmware-bytes"));
        assert!(!d.matches(b"firmware-byteS"));
        assert!(!d.matches(b"firmware-bytes-extra"));
    }
}