From 7cadaa5dbb6478bfe2e9d74cc2c19283f875604f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 00:05:13 +0000 Subject: [PATCH] fix(attestation): hash evidence counts as u64 on every platform The evidence hash fed files_read to SHA-256 as usize::to_le_bytes, so a 32-bit build hashed 4 bytes where a 64-bit build hashed 8, and the same scan produced different evidence hashes. files_read is now widened to u64 like bytes_read and wall_clock_ms. On 64-bit platforms the bytes, and so every existing hash, are unchanged. The computation moves to a pub evidence_hash function with a known-answer test whose expected value comes from coreutils sha256sum. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PFaqv6iBXrt3W5Akd1Fq5k --- CHANGELOG.adoc | 8 ++++++ src/attestation/evidence.rs | 50 +++++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 50f9828..245c2fa 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -2,6 +2,14 @@ === [Unreleased] +==== Changed — evidence hash is platform-independent + +* The attestation evidence hash now feeds `+files_read+` to SHA-256 as an 8-byte +little-endian `+u64+`, like the other counts, instead of a platform-sized +`+usize+`. Hashes are unchanged on 64-bit platforms; 32-bit builds now agree +with them. The computation lives in `+attestation::evidence::evidence_hash+`, +with a known-answer test whose value comes from coreutils `+sha256sum+`. + ==== Fixed — KYAML keys holding a line separator * KYAML output now escapes U+2028 (LS) and U+2029 (PS) as `+\uXXXX+`, as it diff --git a/src/attestation/evidence.rs b/src/attestation/evidence.rs index d90f2fe..c55dab9 100644 --- a/src/attestation/evidence.rs +++ b/src/attestation/evidence.rs @@ -104,7 +104,8 @@ pub struct ExecutionEvidence { /// Unique programming languages detected. pub languages_detected: Vec, - /// `SHA-256(session_nonce || files_read || bytes_read || rolling_content_hash || wall_clock_ms)`. + /// `SHA-256(session_nonce || files_read || bytes_read || rolling_content_hash || wall_clock_ms)`, + /// with each count as 8 little-endian bytes; see [`evidence_hash`]. pub evidence_hash: String, } @@ -171,16 +172,13 @@ impl EvidenceAccumulator { let mut languages: Vec = self.languages_detected.into_iter().collect(); languages.sort(); - // Evidence hash: binding commitment over the key metrics - let evidence_hash = { - let mut hasher = Sha256::new(); - hasher.update(self.session_nonce.as_bytes()); - hasher.update(self.files_read.to_le_bytes()); - hasher.update(self.bytes_read.to_le_bytes()); - hasher.update(rolling_content_hash.as_bytes()); - hasher.update(wall_clock_ms.to_le_bytes()); - hex::encode(hasher.finalize()) - }; + let evidence_hash = evidence_hash( + &self.session_nonce, + self.files_read as u64, + self.bytes_read, + &rolling_content_hash, + wall_clock_ms, + ); ExecutionEvidence { session_nonce: self.session_nonce, @@ -198,6 +196,26 @@ impl EvidenceAccumulator { } } +/// The evidence hash: a binding commitment over the key metrics. +/// +/// Counts are hashed as `u64` little-endian, so the value does not depend on +/// the platform's `usize` width and other tools can recompute it. +pub fn evidence_hash( + session_nonce: &str, + files_read: u64, + bytes_read: u64, + rolling_content_hash: &str, + wall_clock_ms: u64, +) -> String { + let mut hasher = Sha256::new(); + hasher.update(session_nonce.as_bytes()); + hasher.update(files_read.to_le_bytes()); + hasher.update(bytes_read.to_le_bytes()); + hasher.update(rolling_content_hash.as_bytes()); + hasher.update(wall_clock_ms.to_le_bytes()); + hex::encode(hasher.finalize()) +} + /// Best-effort CPU time in milliseconds (Linux-only via /proc/self/stat). fn get_cpu_time_ms() -> Option { #[cfg(target_os = "linux")] @@ -259,6 +277,16 @@ fn get_peak_rss() -> Option { mod tests { use super::*; + #[test] + fn evidence_hash_known_answer() { + // Expected value from coreutils, not this crate: + // printf 'nonce\002\0\0\0\0\0\0\0\350\003\0\0\0\0\0\0rolling\007\0\0\0\0\0\0\0' | sha256sum + assert_eq!( + evidence_hash("nonce", 2, 1000, "rolling", 7), + "314d68809916de80906ebcf90bc79abbb9a4f4020a5266bc9cd4377adc4585de" + ); + } + #[test] fn test_accumulator_basic_flow() { let mut acc = EvidenceAccumulator::new("a".repeat(64).as_str());