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 subtle::ConstantTimeEq;
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
78/// Compute the digest of `data` using the specified algorithm.
79///
80/// Returns the raw digest bytes (not base64-encoded).
81pub fn compute_digest(algorithm: DigestAlgorithm, data: &[u8]) -> Vec<u8> {
82    compute_digest_with_provider(crate::provider::default_provider(), algorithm, data)
83        .expect("default provider advertises every XMLDSig digest")
84}
85
86/// Compute a digest with an explicitly selected provider.
87pub fn compute_digest_with_provider(
88    provider: &dyn crate::provider::CryptoProvider,
89    algorithm: DigestAlgorithm,
90    data: &[u8],
91) -> Result<Vec<u8>, crate::provider::ProviderError> {
92    provider.require_capability(crate::provider::ProviderCapability::Digest(algorithm))?;
93    let digest = provider.digest(algorithm, data)?;
94    let expected = algorithm.output_len();
95    if digest.len() != expected {
96        return Err(crate::provider::ProviderError::InvalidOutputSize {
97            operation: crate::provider::ProviderOperation::Digest,
98            expected,
99            actual: digest.len(),
100        });
101    }
102    Ok(digest)
103}
104
105/// Constant-time comparison of two byte slices.
106///
107/// Returns `true` if and only if `a` and `b` have equal length and identical
108/// content. Execution time depends only on the length of the slices, not on
109/// where they differ — preventing timing side-channel attacks on digest
110/// comparison.
111///
112/// Uses `subtle` constant-time equality.
113pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
114    a.ct_eq(b).into()
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    struct RejectingDigestProvider {
122        output: Option<Vec<u8>>,
123    }
124
125    impl crate::provider::CryptoProvider for RejectingDigestProvider {
126        fn name(&self) -> &'static str {
127            "rejecting-digest"
128        }
129
130        fn supports(&self, capability: crate::provider::ProviderCapability<'_>) -> bool {
131            if self.output.is_none()
132                && matches!(capability, crate::provider::ProviderCapability::Digest(_))
133            {
134                return false;
135            }
136            crate::provider::default_provider().supports(capability)
137        }
138
139        fn fill_random(&self, output: &mut [u8]) -> Result<(), crate::provider::ProviderError> {
140            crate::provider::default_provider().fill_random(output)
141        }
142
143        fn derive_key(
144            &self,
145            parameters: &crate::provider::KdfParameters<'_>,
146            secret: &[u8],
147        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
148            crate::provider::default_provider().derive_key(parameters, secret)
149        }
150
151        fn digest(
152            &self,
153            _algorithm: DigestAlgorithm,
154            _data: &[u8],
155        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
156            if let Some(output) = &self.output {
157                return Ok(output.clone());
158            }
159            panic!("an unavailable digest capability must not be dispatched")
160        }
161
162        fn sign(
163            &self,
164            key: &dyn super::super::SigningKey,
165            algorithm: super::super::SignatureAlgorithm,
166            data: &[u8],
167        ) -> Result<Vec<u8>, super::super::SigningKeyError> {
168            crate::provider::default_provider().sign(key, algorithm, data)
169        }
170
171        fn verify(
172            &self,
173            key: &dyn super::super::VerifyingKey,
174            algorithm: super::super::SignatureAlgorithm,
175            data: &[u8],
176            signature: &[u8],
177        ) -> Result<bool, super::super::DsigError> {
178            crate::provider::default_provider().verify(key, algorithm, data, signature)
179        }
180
181        #[cfg(feature = "xmlenc")]
182        fn encrypt_data(
183            &self,
184            algorithm: crate::xmlenc::DataEncryptionAlgorithm,
185            key: &[u8],
186            plaintext: &[u8],
187        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
188            crate::provider::default_provider().encrypt_data(algorithm, key, plaintext)
189        }
190
191        #[cfg(feature = "xmlenc")]
192        fn decrypt_data(
193            &self,
194            algorithm: crate::xmlenc::DataEncryptionAlgorithm,
195            key: &[u8],
196            ciphertext: &[u8],
197        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
198            crate::provider::default_provider().decrypt_data(algorithm, key, ciphertext)
199        }
200
201        #[cfg(feature = "xmlenc")]
202        fn wrap_key(
203            &self,
204            algorithm: crate::xmlenc::KeyWrapAlgorithm,
205            kek: &[u8],
206            key: &[u8],
207        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
208            crate::provider::default_provider().wrap_key(algorithm, kek, key)
209        }
210
211        #[cfg(feature = "xmlenc")]
212        fn unwrap_key(
213            &self,
214            algorithm: crate::xmlenc::KeyWrapAlgorithm,
215            kek: &[u8],
216            wrapped: &[u8],
217        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
218            crate::provider::default_provider().unwrap_key(algorithm, kek, wrapped)
219        }
220
221        #[cfg(feature = "xmlenc")]
222        fn transport_key(
223            &self,
224            key: &dyn crate::provider::KeyTransportKey,
225            parameters: &crate::xmlenc::RsaOaepParameters,
226            plaintext: &[u8],
227        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
228            crate::provider::default_provider().transport_key(key, parameters, plaintext)
229        }
230
231        #[cfg(feature = "xmlenc")]
232        fn recover_key(
233            &self,
234            key: &dyn crate::provider::KeyRecoveryKey,
235            parameters: &crate::xmlenc::RsaOaepParameters,
236            ciphertext: &[u8],
237        ) -> Result<Vec<u8>, crate::provider::ProviderError> {
238            crate::provider::default_provider().recover_key(key, parameters, ciphertext)
239        }
240    }
241
242    #[test]
243    fn explicit_provider_digest_failures_are_returned() {
244        // A restricted provider is caller-controlled and must never turn an
245        // unsupported document-selected digest into a process panic.
246        assert!(matches!(
247            compute_digest_with_provider(
248                &RejectingDigestProvider { output: None },
249                DigestAlgorithm::Sha256,
250                b"x"
251            ),
252            Err(crate::provider::ProviderError::Unsupported { .. })
253        ));
254    }
255
256    #[test]
257    fn explicit_provider_digest_output_must_match_the_algorithm() {
258        // The facade, rather than an interchangeable provider, owns the XMLDSig
259        // algorithm contract and must reject bytes its own parser cannot accept.
260        for actual in [0, 31, 33] {
261            assert!(matches!(
262                compute_digest_with_provider(
263                    &RejectingDigestProvider {
264                        output: Some(vec![0_u8; actual]),
265                    },
266                    DigestAlgorithm::Sha256,
267                    b"x",
268                ),
269                Err(crate::provider::ProviderError::InvalidOutputSize {
270                    operation: crate::provider::ProviderOperation::Digest,
271                    expected: 32,
272                    actual: output_len,
273                }) if output_len == actual
274            ));
275        }
276
277        assert_eq!(
278            compute_digest_with_provider(
279                &RejectingDigestProvider {
280                    output: Some(vec![7_u8; 32]),
281                },
282                DigestAlgorithm::Sha256,
283                b"x",
284            ),
285            Ok(vec![7_u8; 32]),
286        );
287    }
288
289    // ── from_uri / uri round-trip ────────────────────────────────────
290
291    #[test]
292    fn from_uri_sha1() {
293        let algo = DigestAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#sha1");
294        assert_eq!(algo, Some(DigestAlgorithm::Sha1));
295    }
296
297    #[test]
298    fn from_uri_sha256() {
299        let algo = DigestAlgorithm::from_uri("http://www.w3.org/2001/04/xmlenc#sha256");
300        assert_eq!(algo, Some(DigestAlgorithm::Sha256));
301    }
302
303    #[test]
304    fn from_uri_sha384() {
305        let algo = DigestAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#sha384");
306        assert_eq!(algo, Some(DigestAlgorithm::Sha384));
307    }
308
309    #[test]
310    fn from_uri_sha512() {
311        let algo = DigestAlgorithm::from_uri("http://www.w3.org/2001/04/xmlenc#sha512");
312        assert_eq!(algo, Some(DigestAlgorithm::Sha512));
313    }
314
315    #[test]
316    fn from_uri_unknown() {
317        assert_eq!(
318            DigestAlgorithm::from_uri("http://example.com/unknown"),
319            None
320        );
321    }
322
323    #[test]
324    fn uri_round_trip() {
325        for algo in [
326            DigestAlgorithm::Sha1,
327            DigestAlgorithm::Sha256,
328            DigestAlgorithm::Sha384,
329            DigestAlgorithm::Sha512,
330        ] {
331            assert_eq!(
332                DigestAlgorithm::from_uri(algo.uri()),
333                Some(algo),
334                "round-trip failed for {algo:?}"
335            );
336        }
337    }
338
339    // ── signing_allowed ──────────────────────────────────────────────
340
341    #[test]
342    fn sha1_verify_only() {
343        assert!(!DigestAlgorithm::Sha1.signing_allowed());
344    }
345
346    #[test]
347    fn sha256_signing_allowed() {
348        assert!(DigestAlgorithm::Sha256.signing_allowed());
349    }
350
351    #[test]
352    fn sha384_signing_allowed() {
353        assert!(DigestAlgorithm::Sha384.signing_allowed());
354    }
355
356    #[test]
357    fn sha512_signing_allowed() {
358        assert!(DigestAlgorithm::Sha512.signing_allowed());
359    }
360
361    // ── output_len ───────────────────────────────────────────────────
362
363    #[test]
364    fn output_lengths() {
365        assert_eq!(DigestAlgorithm::Sha1.output_len(), 20);
366        assert_eq!(DigestAlgorithm::Sha256.output_len(), 32);
367        assert_eq!(DigestAlgorithm::Sha384.output_len(), 48);
368        assert_eq!(DigestAlgorithm::Sha512.output_len(), 64);
369    }
370
371    // ── Known-answer tests (KAT) ────────────────────────────────────
372    // Reference values computed with `echo -n "..." | openssl dgst -sha*`
373
374    #[test]
375    fn sha1_empty() {
376        // SHA-1("") = da39a3ee5e6b4b0d3255bfef95601890afd80709
377        let digest = compute_digest(DigestAlgorithm::Sha1, b"");
378        assert_eq!(digest.len(), 20);
379        assert_eq!(hex(&digest), "da39a3ee5e6b4b0d3255bfef95601890afd80709");
380    }
381
382    #[test]
383    fn sha256_empty() {
384        // SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
385        let digest = compute_digest(DigestAlgorithm::Sha256, b"");
386        assert_eq!(digest.len(), 32);
387        assert_eq!(
388            hex(&digest),
389            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
390        );
391    }
392
393    #[test]
394    fn sha384_empty() {
395        // SHA-384("") = 38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b
396        let digest = compute_digest(DigestAlgorithm::Sha384, b"");
397        assert_eq!(digest.len(), 48);
398        assert_eq!(
399            hex(&digest),
400            "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b"
401        );
402    }
403
404    #[test]
405    fn sha512_empty() {
406        // SHA-512("") = cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e
407        let digest = compute_digest(DigestAlgorithm::Sha512, b"");
408        assert_eq!(digest.len(), 64);
409        assert_eq!(
410            hex(&digest),
411            "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
412        );
413    }
414
415    #[test]
416    fn sha256_hello_world() {
417        // SHA-256("hello world") = b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
418        let digest = compute_digest(DigestAlgorithm::Sha256, b"hello world");
419        assert_eq!(
420            hex(&digest),
421            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
422        );
423    }
424
425    #[test]
426    fn sha1_abc() {
427        // SHA-1("abc") = a9993e364706816aba3e25717850c26c9cd0d89d
428        let digest = compute_digest(DigestAlgorithm::Sha1, b"abc");
429        assert_eq!(hex(&digest), "a9993e364706816aba3e25717850c26c9cd0d89d");
430    }
431
432    // ── constant_time_eq ─────────────────────────────────────────────
433
434    #[test]
435    fn constant_time_eq_identical() {
436        let a = compute_digest(DigestAlgorithm::Sha256, b"test");
437        let b = compute_digest(DigestAlgorithm::Sha256, b"test");
438        assert!(constant_time_eq(&a, &b));
439    }
440
441    #[test]
442    fn constant_time_eq_different_content() {
443        let a = compute_digest(DigestAlgorithm::Sha256, b"test1");
444        let b = compute_digest(DigestAlgorithm::Sha256, b"test2");
445        assert!(!constant_time_eq(&a, &b));
446    }
447
448    #[test]
449    fn constant_time_eq_different_lengths() {
450        assert!(!constant_time_eq(&[1, 2, 3], &[1, 2]));
451    }
452
453    #[test]
454    fn constant_time_eq_empty() {
455        assert!(constant_time_eq(&[], &[]));
456    }
457
458    // ── Digest output matches expected length ────────────────────────
459
460    #[test]
461    fn digest_output_matches_declared_length() {
462        let data = b"test data for length verification";
463        for algo in [
464            DigestAlgorithm::Sha1,
465            DigestAlgorithm::Sha256,
466            DigestAlgorithm::Sha384,
467            DigestAlgorithm::Sha512,
468        ] {
469            let digest = compute_digest(algo, data);
470            assert_eq!(
471                digest.len(),
472                algo.output_len(),
473                "output length mismatch for {algo:?}"
474            );
475        }
476    }
477
478    /// Helper: format bytes as lowercase hex string.
479    fn hex(bytes: &[u8]) -> String {
480        bytes.iter().map(|b| format!("{b:02x}")).collect()
481    }
482}