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