use sha2::{Digest, Sha256};
pub const SHA256_DIGEST_LENGTH: usize = 32;
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ContentDigest {
digest_bytes: [u8; SHA256_DIGEST_LENGTH],
}
impl ContentDigest {
pub fn new(content: &[u8]) -> Self {
let digest = Sha256::digest(content);
let mut digest_bytes = [0u8; SHA256_DIGEST_LENGTH];
digest_bytes.copy_from_slice(&digest);
Self { digest_bytes }
}
pub fn from_bytes(digest_bytes: [u8; SHA256_DIGEST_LENGTH]) -> Self {
Self { digest_bytes }
}
pub fn as_bytes(&self) -> &[u8] {
&self.digest_bytes
}
pub fn to_hex(&self) -> String {
self.digest_bytes
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
pub const fn algorithm() -> &'static str {
"sha-256"
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IntegrityProof {
digest: ContentDigest,
size: u64,
}
impl IntegrityProof {
pub fn verify(content: &[u8], expected: &ContentDigest) -> Result<Self, IntegrityError> {
let actual = ContentDigest::new(content);
if actual != *expected {
return Err(IntegrityError::new(expected.clone(), actual));
}
Ok(Self {
digest: actual,
size: content.len() as u64,
})
}
pub fn digest(&self) -> &ContentDigest {
&self.digest
}
pub fn size(&self) -> u64 {
self.size
}
pub fn matches(&self, expected: &ContentDigest, expected_size: u64) -> bool {
self.digest == *expected && self.size == expected_size
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IntegrityError {
expected: ContentDigest,
actual: ContentDigest,
}
impl IntegrityError {
pub fn new(expected: ContentDigest, actual: ContentDigest) -> Self {
Self { expected, actual }
}
pub fn expected(&self) -> &ContentDigest {
&self.expected
}
pub fn actual(&self) -> &ContentDigest {
&self.actual
}
}
pub fn verify_integrity(content: &[u8], expected: &ContentDigest) -> Result<(), IntegrityError> {
let actual = ContentDigest::new(content);
if actual == *expected {
Ok(())
} else {
Err(IntegrityError::new(expected.clone(), actual))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn integrity_proof_requires_matching_content() {
let content = b"trusted content";
let expected = ContentDigest::new(content);
let proof = IntegrityProof::verify(content, &expected).unwrap();
assert!(proof.matches(&expected, content.len() as u64));
assert_eq!(proof.digest(), &expected);
assert_eq!(proof.size(), content.len() as u64);
}
#[test]
fn integrity_proof_rejects_mismatched_content() {
let expected = ContentDigest::new(b"trusted content");
assert!(IntegrityProof::verify(b"tampered content", &expected).is_err());
}
#[test]
fn digest_uses_sha256() {
let digest = ContentDigest::new(b"abc");
assert_eq!(
digest.to_hex(),
"ba7816bf8f01cfea414140de5dae2223\
b00361a396177a9cb410ff61f20015ad"
.replace(char::is_whitespace, "")
);
assert_eq!(digest.as_bytes().len(), SHA256_DIGEST_LENGTH);
assert_eq!(ContentDigest::algorithm(), "sha-256");
}
#[test]
fn digest_has_fixed_length() {
let digest = ContentDigest::new(b"some content");
assert_eq!(digest.as_bytes().len(), 32);
}
#[test]
fn digest_is_deterministic() {
let first = ContentDigest::new(b"same content");
let second = ContentDigest::new(b"same content");
assert_eq!(first, second);
}
#[test]
fn different_content_produces_different_digest() {
let first = ContentDigest::new(b"content-a");
let second = ContentDigest::new(b"content-b");
assert_ne!(first, second);
}
#[test]
fn empty_content_has_real_sha256_digest() {
let digest = ContentDigest::new(b"");
assert_eq!(
digest.to_hex(),
"e3b0c44298fc1c149afbf4c8996fb924\
27ae41e4649b934ca495991b7852b855"
.replace(char::is_whitespace, "")
);
}
#[test]
fn from_bytes_preserves_digest() {
let bytes = [
0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae,
0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61,
0xf2, 0x00, 0x15, 0xad,
];
let digest = ContentDigest::from_bytes(bytes);
assert_eq!(
digest.to_hex(),
"ba7816bf8f01cfea414140de5dae2223\
b00361a396177a9cb410ff61f20015ad"
.replace(char::is_whitespace, "")
);
}
#[test]
fn matching_content_passes_integrity_verification() {
let content = b"exact content";
let digest = ContentDigest::new(content);
assert!(verify_integrity(content, &digest).is_ok());
}
#[test]
fn mismatching_content_fails_integrity_verification() {
let content = b"actual content";
let expected = ContentDigest::new(b"different content");
let result = verify_integrity(content, &expected);
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.expected(), &expected);
assert_eq!(error.actual(), &ContentDigest::new(content));
}
#[test]
fn integrity_error_preserves_expected_and_actual_digests() {
let expected = ContentDigest::new(b"expected");
let actual = ContentDigest::new(b"actual");
let error = IntegrityError::new(expected.clone(), actual.clone());
assert_eq!(error.expected(), &expected);
assert_eq!(error.actual(), &actual);
}
}