tsafe-attest 1.1.0

Attestation pipeline for tsafe — secret scanner + env-injection contract + run-evidence harness (algol-merged)
Documentation
//! Hashing helpers — BLAKE3 canonical, SHA-256 deprecation-API.
//!
//! Per ec ADR-0003 (hash convergence), tsafe content-hashes are BLAKE3.
//! Phase 3 swaps the scanner's `ScanFinding.hash` field from SHA-256 to
//! BLAKE3; the wire-format change is a breaking change for any consumer
//! pinning a fingerprint as a content-address. SHA-256 is retained as a
//! deprecation API so callers needing the old format can migrate
//! incrementally.
//!
//! # Format
//!
//! - BLAKE3 outputs `blake3:<64 hex chars>` (lowercase hex, 32-byte digest)
//! - SHA-256 (deprecated) outputs `sha256:<64 hex chars>` (lowercase hex,
//!   32-byte digest) — kept for backwards-compatibility ONLY; new code
//!   should call [`blake3_hash`].

use sha2::{Digest, Sha256};

/// BLAKE3 content hash with the `blake3:` prefix.
///
/// This is the canonical content-hash family per ec ADR-0003. Use it for
/// all new scanner-fingerprint output.
pub fn blake3_hash(value: impl AsRef<[u8]>) -> String {
    let digest = blake3::hash(value.as_ref());
    format!("blake3:{}", digest.to_hex())
}

/// SHA-256 content hash with the `sha256:` prefix.
///
/// **Deprecated.** Retained to keep [`crate::run_evidence`]-shaped Phase 1
/// artifacts (RunEvidence, etc.) on their current SHA-256 wire format and
/// to support consumers mid-migration. New scanner fingerprints use
/// [`blake3_hash`] per ec ADR-0003.
#[deprecated(
    since = "1.1.0",
    note = "Use `blake3_hash` per ec ADR-0003 (hash convergence). SHA-256 \
            output is retained for Phase 1 RunEvidence shape compatibility \
            only; scanner fingerprints are BLAKE3 in Phase 3+."
)]
pub fn sha256_hash(value: impl AsRef<[u8]>) -> String {
    let digest = Sha256::digest(value.as_ref());
    let mut out = String::with_capacity(7 + 64);
    out.push_str("sha256:");
    for byte in digest.iter() {
        out.push_str(&format!("{byte:02x}"));
    }
    out
}

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

    #[test]
    fn blake3_is_deterministic_and_distinct() {
        assert_eq!(blake3_hash("same"), blake3_hash("same"));
        assert_ne!(blake3_hash("same"), blake3_hash("different"));
    }

    #[test]
    fn blake3_has_prefix_and_64_hex_chars() {
        let h = blake3_hash("payload");
        let hex = h.strip_prefix("blake3:").expect("blake3: prefix present");
        assert_eq!(hex.len(), 64);
        assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
    }

    #[test]
    #[allow(deprecated)]
    fn sha256_still_available_for_compat() {
        let h = super::sha256_hash("payload");
        let hex = h.strip_prefix("sha256:").expect("sha256: prefix present");
        assert_eq!(hex.len(), 64);
        assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
    }

    #[test]
    fn blake3_and_sha256_produce_different_outputs_for_same_input() {
        #[allow(deprecated)]
        let s = super::sha256_hash("same-input");
        let b = blake3_hash("same-input");
        // They must differ — both algorithm choice and prefix change.
        assert_ne!(s, b);
        assert!(s.starts_with("sha256:"));
        assert!(b.starts_with("blake3:"));
    }
}