Skip to main content

herolib_crypt/httpsig/
digest.rs

1//! RFC 9530 Content-Digest implementation.
2//!
3//! Provides SHA-256 digest computation and verification for HTTP message bodies.
4
5use crate::httpsig::error::HttpSigError;
6use base64::Engine;
7use sha2::{Digest, Sha256};
8
9/// The canonical SHA-256 digest of an empty string.
10///
11/// This is used for bodyless requests (GET, DELETE, etc.) to ensure
12/// all requests have a Content-Digest header.
13pub const EMPTY_BODY_DIGEST: &str = "sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:";
14
15/// Compute the RFC 9530 Content-Digest for a message body.
16///
17/// Returns a structured field value in the format: `sha-256=:base64:`
18///
19/// # Example
20///
21/// ```
22/// use herolib_crypt::httpsig::compute_content_digest;
23///
24/// let body = b"Hello, World!";
25/// let digest = compute_content_digest(body);
26/// assert!(digest.starts_with("sha-256=:"));
27/// assert!(digest.ends_with(":"));
28/// ```
29pub fn compute_content_digest(body: &[u8]) -> String {
30    if body.is_empty() {
31        return EMPTY_BODY_DIGEST.to_string();
32    }
33
34    let mut hasher = Sha256::new();
35    hasher.update(body);
36    let hash = hasher.finalize();
37
38    let b64 = base64::engine::general_purpose::STANDARD.encode(hash);
39    format!("sha-256=:{}:", b64)
40}
41
42/// Verify that a Content-Digest header matches the computed digest of a body.
43///
44/// # Errors
45///
46/// Returns `HttpSigError::DigestMismatch` if the digests don't match.
47/// Returns `HttpSigError::ParseError` if the digest format is invalid.
48///
49/// # Example
50///
51/// ```
52/// use herolib_crypt::httpsig::{compute_content_digest, verify_content_digest};
53///
54/// let body = b"Hello, World!";
55/// let digest = compute_content_digest(body);
56///
57/// assert!(verify_content_digest(body, &digest).is_ok());
58/// assert!(verify_content_digest(b"Wrong body", &digest).is_err());
59/// ```
60pub fn verify_content_digest(body: &[u8], digest_header: &str) -> Result<(), HttpSigError> {
61    let computed = compute_content_digest(body);
62
63    if digest_header != computed {
64        return Err(HttpSigError::DigestMismatch);
65    }
66
67    Ok(())
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn test_empty_body_digest() {
76        let digest = compute_content_digest(b"");
77        assert_eq!(digest, EMPTY_BODY_DIGEST);
78    }
79
80    #[test]
81    fn test_compute_digest() {
82        let body = b"Hello, World!";
83        let digest = compute_content_digest(body);
84
85        assert!(digest.starts_with("sha-256=:"));
86        assert!(digest.ends_with(":"));
87        assert!(digest.len() > 20);
88    }
89
90    #[test]
91    fn test_verify_digest_success() {
92        let body = b"Test body";
93        let digest = compute_content_digest(body);
94
95        assert!(verify_content_digest(body, &digest).is_ok());
96    }
97
98    #[test]
99    fn test_verify_digest_mismatch() {
100        let body = b"Test body";
101        let digest = compute_content_digest(body);
102
103        let result = verify_content_digest(b"Different body", &digest);
104        assert!(matches!(result, Err(HttpSigError::DigestMismatch)));
105    }
106
107    #[test]
108    fn test_digest_deterministic() {
109        let body = b"Deterministic test";
110        let digest1 = compute_content_digest(body);
111        let digest2 = compute_content_digest(body);
112
113        assert_eq!(digest1, digest2);
114    }
115}