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