1use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
11use sha2::{Digest as _, Sha256, Sha512};
12
13fn digest(algorithm: &str, body: &[u8]) -> Option<Vec<u8>> {
16 match algorithm.to_ascii_lowercase().as_str() {
17 "sha-256" => Some(Sha256::digest(body).to_vec()),
18 "sha-512" => Some(Sha512::digest(body).to_vec()),
19 _ => None,
20 }
21}
22
23fn all_known_match<'a>(
26 digests: impl IntoIterator<Item = (&'a str, Option<Vec<u8>>)>,
27 body: &[u8],
28) -> bool {
29 let mut known = false;
30 for (algorithm, sent) in digests {
31 let Some(expected) = digest(algorithm, body) else {
32 continue;
33 };
34 if sent.as_deref() != Some(expected.as_slice()) {
35 return false;
36 }
37 known = true;
38 }
39 known
40}
41
42#[must_use]
45pub fn digest_matches(header: &str, body: &[u8]) -> bool {
46 all_known_match(
47 header.split(',').filter_map(|member| {
48 let (algorithm, value) = member.trim().split_once('=')?;
49 Some((algorithm.trim(), BASE64.decode(value.trim()).ok()))
50 }),
51 body,
52 )
53}
54
55#[must_use]
58pub fn content_digest_matches(header: &str, body: &[u8]) -> bool {
59 let Ok(dictionary) = sfv::Parser::new(header).parse::<sfv::Dictionary>() else {
60 return false;
61 };
62 all_known_match(
63 dictionary.iter().map(|(algorithm, member)| {
64 let sent = match member {
65 sfv::ListEntry::Item(item) => item.bare_item.as_byte_sequence().map(<[u8]>::to_vec),
66 sfv::ListEntry::InnerList(_) => None,
67 };
68 (algorithm.as_str(), sent)
69 }),
70 body,
71 )
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 const BODY: &[u8] = b"{\"hello\": \"world\"}";
79
80 fn sha256() -> String {
81 BASE64.encode(Sha256::digest(BODY))
82 }
83
84 fn sha512() -> String {
85 BASE64.encode(Sha512::digest(BODY))
86 }
87
88 #[test]
89 fn a_digest_header_may_list_several_algorithms_in_any_case() {
90 let (sha256, sha512) = (sha256(), sha512());
91 assert!(digest_matches(&format!("SHA-256={sha256}"), BODY));
92 assert!(digest_matches(&format!("sha-256={sha256}"), BODY));
93 assert!(digest_matches(&format!("SHA-512={sha512}"), BODY));
94 assert!(digest_matches(
95 &format!("SHA-256={sha256}, SHA-512={sha512}"),
96 BODY
97 ));
98 assert!(digest_matches(&format!("MD5=abc,SHA-256={sha256}"), BODY));
99 }
100
101 #[test]
102 fn every_known_digest_has_to_match_and_one_has_to_be_there() {
103 let (sha256, sha512) = (sha256(), sha512());
104 assert!(!digest_matches(&format!("SHA-256={sha512}"), BODY));
105 assert!(!digest_matches(
106 &format!("SHA-256={sha256},SHA-512={sha256}"),
107 BODY
108 ));
109 assert!(!digest_matches("MD5=abc", BODY));
110 assert!(!digest_matches("", BODY));
111 }
112
113 #[test]
114 fn a_content_digest_is_a_structured_field_dictionary() {
115 let (sha256, sha512) = (sha256(), sha512());
116 assert!(content_digest_matches(&format!("sha-256=:{sha256}:"), BODY));
117 assert!(content_digest_matches(
119 "sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:",
120 BODY
121 ));
122 assert!(content_digest_matches(
123 &format!("sha-256=:{sha256}:, sha-512=:{sha512}:"),
124 BODY
125 ));
126 assert!(!content_digest_matches(&format!("sha-256={sha256}"), BODY));
127 assert!(!content_digest_matches(
128 &format!("sha-256=:{sha256}:, sha-512=:{sha256}:"),
129 BODY
130 ));
131 assert!(!content_digest_matches("unixsum=:AAAA:", BODY));
132 }
133}