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