use sha2::{Digest, Sha256};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DigestMismatch {
pub expected: String,
pub actual: String,
}
impl std::fmt::Display for DigestMismatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"artifact integrity check failed: expected sha256:{}, got sha256:{}",
self.expected, self.actual
)
}
}
impl std::error::Error for DigestMismatch {}
pub fn verify_sha256_digest(bytes: &[u8], expected: &str) -> Result<(), DigestMismatch> {
let expected_normalized = expected
.trim()
.strip_prefix("sha256:")
.unwrap_or(expected.trim())
.to_ascii_lowercase();
let actual = hex_encode(&Sha256::digest(bytes));
if actual == expected_normalized {
Ok(())
} else {
Err(DigestMismatch {
expected: expected_normalized,
actual,
})
}
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
const HELLO_WORLD_NEWLINE: &[u8] = b"hello world\n";
const HELLO_WORLD_SHA256: &str =
"a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447";
#[test]
fn matching_digest_passes() {
assert!(verify_sha256_digest(HELLO_WORLD_NEWLINE, HELLO_WORLD_SHA256).is_ok());
}
#[test]
fn matching_digest_is_case_insensitive() {
assert!(
verify_sha256_digest(HELLO_WORLD_NEWLINE, &HELLO_WORLD_SHA256.to_ascii_uppercase())
.is_ok()
);
}
#[test]
fn accepts_the_sha256_prefixed_shape() {
let prefixed = format!("sha256:{HELLO_WORLD_SHA256}");
assert!(verify_sha256_digest(HELLO_WORLD_NEWLINE, &prefixed).is_ok());
}
#[test]
fn counterexample_mismatched_digest_fails_closed() {
let tampered = b"hello world, tampered\n";
let err = verify_sha256_digest(tampered, HELLO_WORLD_SHA256).unwrap_err();
assert_eq!(err.expected, HELLO_WORLD_SHA256);
assert_ne!(err.actual, HELLO_WORLD_SHA256);
}
#[test]
fn malformed_expected_digest_fails_closed_not_skipped() {
let err = verify_sha256_digest(HELLO_WORLD_NEWLINE, "not-a-real-digest").unwrap_err();
assert_eq!(err.expected, "not-a-real-digest");
}
#[test]
fn empty_bytes_have_the_well_known_empty_sha256() {
const EMPTY_SHA256: &str =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
assert!(verify_sha256_digest(b"", EMPTY_SHA256).is_ok());
}
}