1use aws_lc_rs::digest::{self, SHA256};
10use base64ct::{Base64, Encoding};
11
12use crate::error::Error;
13
14pub const SHA256_DIGEST_PREFIX: &str = "SHA-256=";
20
21#[must_use]
26pub fn sha256_digest_header(body: &[u8]) -> String {
27 let hash = digest::digest(&SHA256, body);
28 let encoded = Base64::encode_string(hash.as_ref());
29 format!("{SHA256_DIGEST_PREFIX}{encoded}")
30}
31
32pub fn verify_digest_header(header: &str, body: &[u8]) -> Result<(), Error> {
44 let encoded = header
45 .strip_prefix(SHA256_DIGEST_PREFIX)
46 .or_else(|| header.strip_prefix("sha-256="))
47 .or_else(|| header.strip_prefix("Sha-256="))
48 .ok_or_else(|| {
49 let algo = header
50 .split_once('=')
51 .map_or_else(|| header.to_owned(), |(a, _)| a.to_owned());
52 Error::UnsupportedDigestAlgorithm(algo)
53 })?;
54
55 let expected = digest::digest(&SHA256, body);
56
57 let decoded = Base64::decode_vec(encoded).map_err(|_| Error::DigestMismatch)?;
64 if !constant_time_eq(&decoded, expected.as_ref()) {
65 return Err(Error::DigestMismatch);
66 }
67 Ok(())
68}
69
70fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
76 if a.len() != b.len() {
77 return false;
78 }
79 let mut diff = 0u8;
80 for (x, y) in a.iter().zip(b.iter()) {
81 diff |= x ^ y;
82 }
83 diff == 0
84}
85
86#[cfg(test)]
87mod tests {
88 use pretty_assertions::assert_eq;
89
90 use super::*;
91
92 const EMPTY_SHA256_B64: &str = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=";
94
95 #[test]
96 fn sha256_digest_of_empty_body_matches_vector() {
97 let header = sha256_digest_header(b"");
98 assert_eq!(header, format!("{SHA256_DIGEST_PREFIX}{EMPTY_SHA256_B64}"));
99 }
100
101 #[test]
102 fn digest_roundtrips_through_verify() {
103 let body = b"Hello, Fediverse!";
104 let header = sha256_digest_header(body);
105 verify_digest_header(&header, body).expect("digest must verify");
106 }
107
108 #[test]
109 fn tampered_body_fails_verify() {
110 let header = sha256_digest_header(b"original body");
111 let err = verify_digest_header(&header, b"tampered body")
112 .expect_err("tampered body must not verify");
113 assert!(matches!(err, Error::DigestMismatch));
114 }
115
116 #[test]
117 fn unknown_algorithm_is_rejected() {
118 let err =
119 verify_digest_header("SHA-512=abcdef", b"").expect_err("SHA-512 must be rejected");
120 match err {
121 Error::UnsupportedDigestAlgorithm(algo) => assert_eq!(algo, "SHA-512"),
122 other => panic!("expected UnsupportedDigestAlgorithm, got {other:?}"),
123 }
124 }
125
126 #[test]
127 fn lowercase_algorithm_token_is_accepted() {
128 let body = b"interop tolerance";
130 let upper = sha256_digest_header(body);
131 let encoded = upper
132 .strip_prefix(SHA256_DIGEST_PREFIX)
133 .expect("has prefix");
134 let lower = format!("sha-256={encoded}");
135 verify_digest_header(&lower, body).expect("lowercase prefix must be accepted");
136 }
137}