Skip to main content

ojak_sig/
digest.rs

1//! Digests of a request's body: the `Digest` header (RFC 3230) draft-cavage
2//! signatures cover, and the `Content-Digest` field (RFC 9530) RFC 9421 ones
3//! do.
4//!
5//! Either may list several algorithms. Every one listed that is known here,
6//! SHA-256 or SHA-512, has to match the body, and at least one has to be
7//! there; the rest are ignored, as neither RFC lets a recipient check what it
8//! does not know.
9
10use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
11use sha2::{Digest as _, Sha256, Sha512};
12
13/// The body's digest under the algorithm named `algorithm`, in any case, or
14/// `None` for an algorithm not known here.
15fn 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
23/// Whether every known member of `digests`, each an algorithm and the digest
24/// it names, matches `body`, and there is one.
25fn 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/// Whether a `Digest` header holds the body's digest: `SHA-256=…`, possibly
43/// beside `SHA-512=…` or algorithms not known here, each base64.
44#[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/// Whether a `Content-Digest` field holds the body's digest: a
56/// structured-field dictionary of algorithms to byte sequences.
57#[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        // The digest RFC 9421's examples send, which is SHA-512.
118        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}