use aws_lc_rs::digest::{self, SHA256};
use base64ct::{Base64, Encoding};
use crate::error::Error;
pub const SHA256_DIGEST_PREFIX: &str = "SHA-256=";
#[must_use]
pub fn sha256_digest_header(body: &[u8]) -> String {
let hash = digest::digest(&SHA256, body);
let encoded = Base64::encode_string(hash.as_ref());
format!("{SHA256_DIGEST_PREFIX}{encoded}")
}
pub fn verify_digest_header(header: &str, body: &[u8]) -> Result<(), Error> {
let encoded = header
.strip_prefix(SHA256_DIGEST_PREFIX)
.or_else(|| header.strip_prefix("sha-256="))
.or_else(|| header.strip_prefix("Sha-256="))
.ok_or_else(|| {
let algo = header
.split_once('=')
.map_or_else(|| header.to_owned(), |(a, _)| a.to_owned());
Error::UnsupportedDigestAlgorithm(algo)
})?;
let expected = digest::digest(&SHA256, body);
let decoded = Base64::decode_vec(encoded).map_err(|_| Error::DigestMismatch)?;
if !constant_time_eq(&decoded, expected.as_ref()) {
return Err(Error::DigestMismatch);
}
Ok(())
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
const EMPTY_SHA256_B64: &str = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=";
#[test]
fn sha256_digest_of_empty_body_matches_vector() {
let header = sha256_digest_header(b"");
assert_eq!(header, format!("{SHA256_DIGEST_PREFIX}{EMPTY_SHA256_B64}"));
}
#[test]
fn digest_roundtrips_through_verify() {
let body = b"Hello, Fediverse!";
let header = sha256_digest_header(body);
verify_digest_header(&header, body).expect("digest must verify");
}
#[test]
fn tampered_body_fails_verify() {
let header = sha256_digest_header(b"original body");
let err = verify_digest_header(&header, b"tampered body")
.expect_err("tampered body must not verify");
assert!(matches!(err, Error::DigestMismatch));
}
#[test]
fn unknown_algorithm_is_rejected() {
let err =
verify_digest_header("SHA-512=abcdef", b"").expect_err("SHA-512 must be rejected");
match err {
Error::UnsupportedDigestAlgorithm(algo) => assert_eq!(algo, "SHA-512"),
other => panic!("expected UnsupportedDigestAlgorithm, got {other:?}"),
}
}
#[test]
fn lowercase_algorithm_token_is_accepted() {
let body = b"interop tolerance";
let upper = sha256_digest_header(body);
let encoded = upper
.strip_prefix(SHA256_DIGEST_PREFIX)
.expect("has prefix");
let lower = format!("sha-256={encoded}");
verify_digest_header(&lower, body).expect("lowercase prefix must be accepted");
}
}