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