Skip to main content

xml_sec/xmldsig/
digest.rs

1//! Digest computation for XMLDSig `<Reference>` processing.
2//!
3//! Implements [XMLDSig §6.1](https://www.w3.org/TR/xmldsig-core1/#sec-DigestMethod):
4//! compute message digests over transform output bytes using SHA-family algorithms.
5//!
6//! All digest computation uses `ring::digest` (no custom implementations).
7
8use ring::digest;
9
10/// Digest algorithms supported by XMLDSig.
11///
12/// SHA-1 is supported for verification only (legacy interop with older IdPs).
13/// SHA-256 is the recommended default for new signatures.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum DigestAlgorithm {
16    /// SHA-1 (160-bit). **Verify-only** — signing with SHA-1 is deprecated.
17    Sha1,
18    /// SHA-256 (256-bit). Default for SAML.
19    Sha256,
20    /// SHA-384 (384-bit).
21    Sha384,
22    /// SHA-512 (512-bit).
23    Sha512,
24}
25
26impl DigestAlgorithm {
27    /// Parse a digest algorithm from its XML namespace URI.
28    ///
29    /// Returns `None` for unrecognized URIs.
30    ///
31    /// # URIs
32    ///
33    /// | Algorithm | URI |
34    /// |-----------|-----|
35    /// | SHA-1 | `http://www.w3.org/2000/09/xmldsig#sha1` |
36    /// | SHA-256 | `http://www.w3.org/2001/04/xmlenc#sha256` |
37    /// | SHA-384 | `http://www.w3.org/2001/04/xmldsig-more#sha384` |
38    /// | SHA-512 | `http://www.w3.org/2001/04/xmlenc#sha512` |
39    pub fn from_uri(uri: &str) -> Option<Self> {
40        match uri {
41            "http://www.w3.org/2000/09/xmldsig#sha1" => Some(Self::Sha1),
42            "http://www.w3.org/2001/04/xmlenc#sha256" => Some(Self::Sha256),
43            "http://www.w3.org/2001/04/xmldsig-more#sha384" => Some(Self::Sha384),
44            "http://www.w3.org/2001/04/xmlenc#sha512" => Some(Self::Sha512),
45            _ => None,
46        }
47    }
48
49    /// Return the XML namespace URI for this digest algorithm.
50    pub fn uri(self) -> &'static str {
51        match self {
52            Self::Sha1 => "http://www.w3.org/2000/09/xmldsig#sha1",
53            Self::Sha256 => "http://www.w3.org/2001/04/xmlenc#sha256",
54            Self::Sha384 => "http://www.w3.org/2001/04/xmldsig-more#sha384",
55            Self::Sha512 => "http://www.w3.org/2001/04/xmlenc#sha512",
56        }
57    }
58
59    /// Whether this algorithm is allowed for signing (not just verification).
60    ///
61    /// SHA-1 is deprecated and restricted to verify-only for interop with
62    /// legacy IdPs.
63    pub fn signing_allowed(self) -> bool {
64        !matches!(self, Self::Sha1)
65    }
66
67    /// The expected output length in bytes.
68    pub fn output_len(self) -> usize {
69        match self {
70            Self::Sha1 => 20,
71            Self::Sha256 => 32,
72            Self::Sha384 => 48,
73            Self::Sha512 => 64,
74        }
75    }
76
77    /// Map to the corresponding `ring::digest` algorithm.
78    fn ring_algorithm(self) -> &'static digest::Algorithm {
79        match self {
80            Self::Sha1 => &digest::SHA1_FOR_LEGACY_USE_ONLY,
81            Self::Sha256 => &digest::SHA256,
82            Self::Sha384 => &digest::SHA384,
83            Self::Sha512 => &digest::SHA512,
84        }
85    }
86}
87
88/// Compute the digest of `data` using the specified algorithm.
89///
90/// Returns the raw digest bytes (not base64-encoded).
91pub fn compute_digest(algorithm: DigestAlgorithm, data: &[u8]) -> Vec<u8> {
92    let result = digest::digest(algorithm.ring_algorithm(), data);
93    result.as_ref().to_vec()
94}
95
96/// Constant-time comparison of two byte slices.
97///
98/// Returns `true` if and only if `a` and `b` have equal length and identical
99/// content. Execution time depends only on the length of the slices, not on
100/// where they differ — preventing timing side-channel attacks on digest
101/// comparison.
102///
103/// Uses `ring::constant_time::verify_slices_are_equal` internally.
104pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
105    #[expect(
106        deprecated,
107        reason = "legacy ring constant-time helper is still used here"
108    )]
109    ring::constant_time::verify_slices_are_equal(a, b).is_ok()
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    // ── from_uri / uri round-trip ────────────────────────────────────
117
118    #[test]
119    fn from_uri_sha1() {
120        let algo = DigestAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#sha1");
121        assert_eq!(algo, Some(DigestAlgorithm::Sha1));
122    }
123
124    #[test]
125    fn from_uri_sha256() {
126        let algo = DigestAlgorithm::from_uri("http://www.w3.org/2001/04/xmlenc#sha256");
127        assert_eq!(algo, Some(DigestAlgorithm::Sha256));
128    }
129
130    #[test]
131    fn from_uri_sha384() {
132        let algo = DigestAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#sha384");
133        assert_eq!(algo, Some(DigestAlgorithm::Sha384));
134    }
135
136    #[test]
137    fn from_uri_sha512() {
138        let algo = DigestAlgorithm::from_uri("http://www.w3.org/2001/04/xmlenc#sha512");
139        assert_eq!(algo, Some(DigestAlgorithm::Sha512));
140    }
141
142    #[test]
143    fn from_uri_unknown() {
144        assert_eq!(
145            DigestAlgorithm::from_uri("http://example.com/unknown"),
146            None
147        );
148    }
149
150    #[test]
151    fn uri_round_trip() {
152        for algo in [
153            DigestAlgorithm::Sha1,
154            DigestAlgorithm::Sha256,
155            DigestAlgorithm::Sha384,
156            DigestAlgorithm::Sha512,
157        ] {
158            assert_eq!(
159                DigestAlgorithm::from_uri(algo.uri()),
160                Some(algo),
161                "round-trip failed for {algo:?}"
162            );
163        }
164    }
165
166    // ── signing_allowed ──────────────────────────────────────────────
167
168    #[test]
169    fn sha1_verify_only() {
170        assert!(!DigestAlgorithm::Sha1.signing_allowed());
171    }
172
173    #[test]
174    fn sha256_signing_allowed() {
175        assert!(DigestAlgorithm::Sha256.signing_allowed());
176    }
177
178    #[test]
179    fn sha384_signing_allowed() {
180        assert!(DigestAlgorithm::Sha384.signing_allowed());
181    }
182
183    #[test]
184    fn sha512_signing_allowed() {
185        assert!(DigestAlgorithm::Sha512.signing_allowed());
186    }
187
188    // ── output_len ───────────────────────────────────────────────────
189
190    #[test]
191    fn output_lengths() {
192        assert_eq!(DigestAlgorithm::Sha1.output_len(), 20);
193        assert_eq!(DigestAlgorithm::Sha256.output_len(), 32);
194        assert_eq!(DigestAlgorithm::Sha384.output_len(), 48);
195        assert_eq!(DigestAlgorithm::Sha512.output_len(), 64);
196    }
197
198    // ── Known-answer tests (KAT) ────────────────────────────────────
199    // Reference values computed with `echo -n "..." | openssl dgst -sha*`
200
201    #[test]
202    fn sha1_empty() {
203        // SHA-1("") = da39a3ee5e6b4b0d3255bfef95601890afd80709
204        let digest = compute_digest(DigestAlgorithm::Sha1, b"");
205        assert_eq!(digest.len(), 20);
206        assert_eq!(hex(&digest), "da39a3ee5e6b4b0d3255bfef95601890afd80709");
207    }
208
209    #[test]
210    fn sha256_empty() {
211        // SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
212        let digest = compute_digest(DigestAlgorithm::Sha256, b"");
213        assert_eq!(digest.len(), 32);
214        assert_eq!(
215            hex(&digest),
216            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
217        );
218    }
219
220    #[test]
221    fn sha384_empty() {
222        // SHA-384("") = 38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b
223        let digest = compute_digest(DigestAlgorithm::Sha384, b"");
224        assert_eq!(digest.len(), 48);
225        assert_eq!(
226            hex(&digest),
227            "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b"
228        );
229    }
230
231    #[test]
232    fn sha512_empty() {
233        // SHA-512("") = cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e
234        let digest = compute_digest(DigestAlgorithm::Sha512, b"");
235        assert_eq!(digest.len(), 64);
236        assert_eq!(
237            hex(&digest),
238            "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
239        );
240    }
241
242    #[test]
243    fn sha256_hello_world() {
244        // SHA-256("hello world") = b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
245        let digest = compute_digest(DigestAlgorithm::Sha256, b"hello world");
246        assert_eq!(
247            hex(&digest),
248            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
249        );
250    }
251
252    #[test]
253    fn sha1_abc() {
254        // SHA-1("abc") = a9993e364706816aba3e25717850c26c9cd0d89d
255        let digest = compute_digest(DigestAlgorithm::Sha1, b"abc");
256        assert_eq!(hex(&digest), "a9993e364706816aba3e25717850c26c9cd0d89d");
257    }
258
259    // ── constant_time_eq ─────────────────────────────────────────────
260
261    #[test]
262    fn constant_time_eq_identical() {
263        let a = compute_digest(DigestAlgorithm::Sha256, b"test");
264        let b = compute_digest(DigestAlgorithm::Sha256, b"test");
265        assert!(constant_time_eq(&a, &b));
266    }
267
268    #[test]
269    fn constant_time_eq_different_content() {
270        let a = compute_digest(DigestAlgorithm::Sha256, b"test1");
271        let b = compute_digest(DigestAlgorithm::Sha256, b"test2");
272        assert!(!constant_time_eq(&a, &b));
273    }
274
275    #[test]
276    fn constant_time_eq_different_lengths() {
277        assert!(!constant_time_eq(&[1, 2, 3], &[1, 2]));
278    }
279
280    #[test]
281    fn constant_time_eq_empty() {
282        assert!(constant_time_eq(&[], &[]));
283    }
284
285    // ── Digest output matches expected length ────────────────────────
286
287    #[test]
288    fn digest_output_matches_declared_length() {
289        let data = b"test data for length verification";
290        for algo in [
291            DigestAlgorithm::Sha1,
292            DigestAlgorithm::Sha256,
293            DigestAlgorithm::Sha384,
294            DigestAlgorithm::Sha512,
295        ] {
296            let digest = compute_digest(algo, data);
297            assert_eq!(
298                digest.len(),
299                algo.output_len(),
300                "output length mismatch for {algo:?}"
301            );
302        }
303    }
304
305    /// Helper: format bytes as lowercase hex string.
306    fn hex(bytes: &[u8]) -> String {
307        bytes.iter().map(|b| format!("{b:02x}")).collect()
308    }
309}