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