Skip to main content

xml_sec/xmldsig/
verify.rs

1//! Reference processing and digest verification for XMLDSig.
2//!
3//! Implements [XMLDSig §4.3.3](https://www.w3.org/TR/xmldsig-core1/#sec-CoreValidation):
4//! for each `<Reference>` in `<SignedInfo>`, dereference the URI, apply transforms,
5//! compute the digest, and compare with the stored `<DigestValue>`.
6//!
7//! This module wires together:
8//! - [`UriReferenceResolver`] for URI dereference
9//! - [`execute_transforms`] for the transform pipeline
10//! - [`compute_digest`] + [`constant_time_eq`] for digest computation and comparison
11
12use roxmltree::Node;
13
14use super::digest::{DigestAlgorithm, compute_digest, constant_time_eq};
15use super::parse::Reference;
16use super::transforms::execute_transforms;
17use super::uri::UriReferenceResolver;
18
19/// Per-reference verification result.
20#[derive(Debug)]
21pub struct ReferenceResult {
22    /// URI from the `<Reference>` element (for diagnostics).
23    pub uri: Option<String>,
24    /// Digest algorithm used.
25    pub digest_algorithm: DigestAlgorithm,
26    /// Whether the computed digest matched the stored `<DigestValue>`.
27    pub valid: bool,
28    /// Pre-digest bytes (populated when `store_pre_digest` is enabled).
29    pub pre_digest_data: Option<Vec<u8>>,
30}
31
32/// Result of processing all `<Reference>` elements in `<SignedInfo>`.
33#[derive(Debug)]
34pub struct ReferencesResult {
35    /// Per-reference results (one per `<Reference>` in order).
36    /// On fail-fast, only references up to and including the failed one are present.
37    pub results: Vec<ReferenceResult>,
38    /// Index of the first failed reference, if any.
39    pub first_failure: Option<usize>,
40}
41
42impl ReferencesResult {
43    /// Whether all references passed digest verification.
44    #[must_use]
45    pub fn all_valid(&self) -> bool {
46        self.first_failure.is_none()
47    }
48}
49
50/// Process a single `<Reference>`: dereference URI → apply transforms → compute
51/// digest → compare with stored `<DigestValue>`.
52///
53/// # Arguments
54///
55/// - `reference`: The parsed `<Reference>` element.
56/// - `resolver`: URI resolver for the document.
57/// - `signature_node`: The `<Signature>` element (for enveloped-signature transform).
58/// - `store_pre_digest`: If true, store the pre-digest bytes in the result.
59///
60/// # Errors
61///
62/// Returns `Err` for processing failures (URI dereference, transform errors).
63/// Digest mismatch is NOT an error — it produces `Ok(ReferenceResult { valid: false })`.
64pub fn process_reference(
65    reference: &Reference,
66    resolver: &UriReferenceResolver<'_>,
67    signature_node: Node<'_, '_>,
68    store_pre_digest: bool,
69) -> Result<ReferenceResult, ReferenceProcessingError> {
70    // 1. Dereference URI. Omitted URI is distinct from URI="" in XMLDSig and
71    // must be rejected until caller-provided external object resolution exists.
72    let uri = reference
73        .uri
74        .as_deref()
75        .ok_or(ReferenceProcessingError::MissingUri)?;
76    let initial_data = resolver
77        .dereference(uri)
78        .map_err(ReferenceProcessingError::UriDereference)?;
79
80    // 2. Apply transform chain
81    let pre_digest_bytes = execute_transforms(signature_node, initial_data, &reference.transforms)
82        .map_err(ReferenceProcessingError::Transform)?;
83
84    // 3. Compute digest
85    let computed_digest = compute_digest(reference.digest_method, &pre_digest_bytes);
86
87    // 4. Compare with stored DigestValue (constant-time)
88    let valid = constant_time_eq(&computed_digest, &reference.digest_value);
89
90    Ok(ReferenceResult {
91        uri: reference.uri.clone(),
92        digest_algorithm: reference.digest_method,
93        valid,
94        pre_digest_data: if store_pre_digest {
95            Some(pre_digest_bytes)
96        } else {
97            None
98        },
99    })
100}
101
102/// Process all `<Reference>` elements in a `<SignedInfo>`, with fail-fast
103/// on the first digest mismatch.
104///
105/// Per XMLDSig spec: if any reference fails, the entire signature is invalid.
106/// Processing stops at the first failure for efficiency.
107///
108/// # Errors
109///
110/// Returns `Err` only for processing failures (malformed XML, unsupported
111/// transform, etc.). Digest mismatches are reported via
112/// `ReferencesResult::first_failure`.
113pub fn process_all_references(
114    references: &[Reference],
115    resolver: &UriReferenceResolver<'_>,
116    signature_node: Node<'_, '_>,
117    store_pre_digest: bool,
118) -> Result<ReferencesResult, ReferenceProcessingError> {
119    let mut results = Vec::with_capacity(references.len());
120
121    for (i, reference) in references.iter().enumerate() {
122        let result = process_reference(reference, resolver, signature_node, store_pre_digest)?;
123        let failed = !result.valid;
124        results.push(result);
125
126        if failed {
127            return Ok(ReferencesResult {
128                results,
129                first_failure: Some(i),
130            });
131        }
132    }
133
134    Ok(ReferencesResult {
135        results,
136        first_failure: None,
137    })
138}
139
140/// Errors during reference processing.
141///
142/// Distinct from digest mismatch (which is a validation result, not a processing error).
143#[derive(Debug, thiserror::Error)]
144#[non_exhaustive]
145pub enum ReferenceProcessingError {
146    /// `<Reference>` omitted the `URI` attribute, which we do not resolve implicitly.
147    #[error("reference URI is required; omitted URI references are not supported")]
148    MissingUri,
149
150    /// URI dereference failed.
151    #[error("URI dereference failed: {0}")]
152    UriDereference(super::types::TransformError),
153
154    /// Transform execution failed.
155    #[error("transform failed: {0}")]
156    Transform(super::types::TransformError),
157}
158
159#[cfg(test)]
160#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
161mod tests {
162    use super::*;
163    use crate::xmldsig::digest::DigestAlgorithm;
164    use crate::xmldsig::parse::{Reference, parse_signed_info};
165    use crate::xmldsig::transforms::Transform;
166    use crate::xmldsig::uri::UriReferenceResolver;
167    use roxmltree::Document;
168
169    // ── Helpers ──────────────────────────────────────────────────────
170
171    /// Build a Reference with given URI, transforms, digest method, and expected digest.
172    fn make_reference(
173        uri: &str,
174        transforms: Vec<Transform>,
175        digest_method: DigestAlgorithm,
176        digest_value: Vec<u8>,
177    ) -> Reference {
178        Reference {
179            uri: Some(uri.to_string()),
180            id: None,
181            ref_type: None,
182            transforms,
183            digest_method,
184            digest_value,
185        }
186    }
187
188    // ── process_reference: happy path ────────────────────────────────
189
190    #[test]
191    fn reference_with_correct_digest_passes() {
192        // Create a simple document, compute its canonical form digest,
193        // then verify that process_reference returns valid=true.
194        let xml = r##"<root>
195            <data>hello world</data>
196            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
197                <ds:SignedInfo/>
198            </ds:Signature>
199        </root>"##;
200        let doc = Document::parse(xml).unwrap();
201        let resolver = UriReferenceResolver::new(&doc);
202        let sig_node = doc
203            .descendants()
204            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
205            .unwrap();
206
207        // First, compute the expected digest by running the pipeline
208        let initial_data = resolver.dereference("").unwrap();
209        let transforms = vec![
210            Transform::Enveloped,
211            Transform::C14n(
212                crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
213                    .unwrap(),
214            ),
215        ];
216        let pre_digest_bytes =
217            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
218        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
219
220        // Now build a Reference with the correct digest and verify
221        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
222
223        let result = process_reference(&reference, &resolver, sig_node, false).unwrap();
224        assert!(result.valid, "digest should match");
225        assert!(result.pre_digest_data.is_none());
226    }
227
228    #[test]
229    fn reference_with_wrong_digest_fails() {
230        let xml = r##"<root>
231            <data>hello</data>
232            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
233                <ds:SignedInfo/>
234            </ds:Signature>
235        </root>"##;
236        let doc = Document::parse(xml).unwrap();
237        let resolver = UriReferenceResolver::new(&doc);
238        let sig_node = doc
239            .descendants()
240            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
241            .unwrap();
242
243        let transforms = vec![Transform::Enveloped];
244        // Wrong digest value — all zeros
245        let wrong_digest = vec![0u8; 32];
246        let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
247
248        let result = process_reference(&reference, &resolver, sig_node, false).unwrap();
249        assert!(!result.valid, "wrong digest should fail");
250    }
251
252    #[test]
253    fn reference_stores_pre_digest_data() {
254        let xml = "<root><child>text</child></root>";
255        let doc = Document::parse(xml).unwrap();
256        let resolver = UriReferenceResolver::new(&doc);
257
258        // No transforms, no enveloped — just canonicalize entire document
259        let initial_data = resolver.dereference("").unwrap();
260        let pre_digest =
261            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
262        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
263
264        let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
265        let result = process_reference(&reference, &resolver, doc.root_element(), true).unwrap();
266
267        assert!(result.valid);
268        assert!(result.pre_digest_data.is_some());
269        assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
270    }
271
272    // ── process_reference: URI dereference ───────────────────────────
273
274    #[test]
275    fn reference_with_id_uri() {
276        let xml = r##"<root>
277            <item ID="target">specific content</item>
278            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
279                <ds:SignedInfo/>
280            </ds:Signature>
281        </root>"##;
282        let doc = Document::parse(xml).unwrap();
283        let resolver = UriReferenceResolver::new(&doc);
284        let sig_node = doc
285            .descendants()
286            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
287            .unwrap();
288
289        // Compute expected digest for the #target subtree
290        let initial_data = resolver.dereference("#target").unwrap();
291        let transforms = vec![Transform::C14n(
292            crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
293                .unwrap(),
294        )];
295        let pre_digest =
296            crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
297        let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
298
299        let reference = make_reference(
300            "#target",
301            transforms,
302            DigestAlgorithm::Sha256,
303            expected_digest,
304        );
305        let result = process_reference(&reference, &resolver, sig_node, false).unwrap();
306        assert!(result.valid);
307    }
308
309    #[test]
310    fn reference_with_nonexistent_id_fails() {
311        let xml = "<root><child/></root>";
312        let doc = Document::parse(xml).unwrap();
313        let resolver = UriReferenceResolver::new(&doc);
314
315        let reference =
316            make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
317        let result = process_reference(&reference, &resolver, doc.root_element(), false);
318        assert!(result.is_err());
319    }
320
321    #[test]
322    fn reference_with_absent_uri_fails_closed() {
323        let xml = "<root><child>text</child></root>";
324        let doc = Document::parse(xml).unwrap();
325        let resolver = UriReferenceResolver::new(&doc);
326
327        let reference = Reference {
328            uri: None, // absent URI
329            id: None,
330            ref_type: None,
331            transforms: vec![],
332            digest_method: DigestAlgorithm::Sha256,
333            digest_value: vec![0; 32],
334        };
335
336        let result = process_reference(&reference, &resolver, doc.root_element(), false);
337        assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
338    }
339
340    // ── process_all_references: fail-fast ────────────────────────────
341
342    #[test]
343    fn all_references_pass() {
344        let xml = "<root><child>text</child></root>";
345        let doc = Document::parse(xml).unwrap();
346        let resolver = UriReferenceResolver::new(&doc);
347
348        // Compute correct digest
349        let initial_data = resolver.dereference("").unwrap();
350        let pre_digest =
351            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
352        let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
353
354        let refs = vec![
355            make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
356            make_reference("", vec![], DigestAlgorithm::Sha256, digest),
357        ];
358
359        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
360        assert!(result.all_valid());
361        assert_eq!(result.results.len(), 2);
362        assert!(result.first_failure.is_none());
363    }
364
365    #[test]
366    fn fail_fast_on_first_mismatch() {
367        let xml = "<root><child>text</child></root>";
368        let doc = Document::parse(xml).unwrap();
369        let resolver = UriReferenceResolver::new(&doc);
370
371        let wrong_digest = vec![0u8; 32];
372        let refs = vec![
373            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
374            // Second reference should NOT be processed
375            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
376        ];
377
378        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
379        assert!(!result.all_valid());
380        assert_eq!(result.first_failure, Some(0));
381        // Only first reference should be in results (fail-fast)
382        assert_eq!(result.results.len(), 1);
383        assert!(!result.results[0].valid);
384    }
385
386    #[test]
387    fn fail_fast_second_reference() {
388        let xml = "<root><child>text</child></root>";
389        let doc = Document::parse(xml).unwrap();
390        let resolver = UriReferenceResolver::new(&doc);
391
392        // Compute correct digest for first ref
393        let initial_data = resolver.dereference("").unwrap();
394        let pre_digest =
395            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
396        let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
397        let wrong_digest = vec![0u8; 32];
398
399        let refs = vec![
400            make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
401            make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
402        ];
403
404        let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
405        assert!(!result.all_valid());
406        assert_eq!(result.first_failure, Some(1));
407        // Both references should be in results
408        assert_eq!(result.results.len(), 2);
409        assert!(result.results[0].valid);
410        assert!(!result.results[1].valid);
411    }
412
413    #[test]
414    fn empty_references_list() {
415        let xml = "<root/>";
416        let doc = Document::parse(xml).unwrap();
417        let resolver = UriReferenceResolver::new(&doc);
418
419        let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
420        assert!(result.all_valid());
421        assert!(result.results.is_empty());
422    }
423
424    // ── Digest algorithms ────────────────────────────────────────────
425
426    #[test]
427    fn reference_sha1_digest() {
428        let xml = "<root>content</root>";
429        let doc = Document::parse(xml).unwrap();
430        let resolver = UriReferenceResolver::new(&doc);
431
432        let initial_data = resolver.dereference("").unwrap();
433        let pre_digest =
434            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
435        let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
436
437        let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
438        let result = process_reference(&reference, &resolver, doc.root_element(), false).unwrap();
439        assert!(result.valid);
440        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
441    }
442
443    #[test]
444    fn reference_sha512_digest() {
445        let xml = "<root>content</root>";
446        let doc = Document::parse(xml).unwrap();
447        let resolver = UriReferenceResolver::new(&doc);
448
449        let initial_data = resolver.dereference("").unwrap();
450        let pre_digest =
451            crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
452        let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
453
454        let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
455        let result = process_reference(&reference, &resolver, doc.root_element(), false).unwrap();
456        assert!(result.valid);
457        assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
458    }
459
460    // ── SAML-like end-to-end ─────────────────────────────────────────
461
462    #[test]
463    fn saml_enveloped_reference_processing() {
464        // Realistic SAML Response with enveloped signature
465        let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
466                                     xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
467                                     ID="_resp1">
468            <saml:Assertion ID="_assert1">
469                <saml:Subject>user@example.com</saml:Subject>
470            </saml:Assertion>
471            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
472                <ds:SignedInfo>
473                    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
474                    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
475                    <ds:Reference URI="">
476                        <ds:Transforms>
477                            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
478                            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
479                        </ds:Transforms>
480                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
481                        <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
482                    </ds:Reference>
483                </ds:SignedInfo>
484                <ds:SignatureValue>fakesig==</ds:SignatureValue>
485            </ds:Signature>
486        </samlp:Response>"##;
487        let doc = Document::parse(xml).unwrap();
488        let resolver = UriReferenceResolver::new(&doc);
489        let sig_node = doc
490            .descendants()
491            .find(|n| n.is_element() && n.tag_name().name() == "Signature")
492            .unwrap();
493
494        // Parse SignedInfo to get the Reference
495        let signed_info_node = sig_node
496            .children()
497            .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
498            .unwrap();
499        let signed_info = parse_signed_info(signed_info_node).unwrap();
500        let reference = &signed_info.references[0];
501
502        // Compute the correct digest by running the actual pipeline
503        let initial_data = resolver.dereference("").unwrap();
504        let pre_digest =
505            crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
506                .unwrap();
507        let correct_digest = compute_digest(reference.digest_method, &pre_digest);
508
509        // Build a reference with the correct digest
510        let corrected_ref = make_reference(
511            "",
512            reference.transforms.clone(),
513            reference.digest_method,
514            correct_digest,
515        );
516
517        // Verify: should pass
518        let result = process_reference(&corrected_ref, &resolver, sig_node, true).unwrap();
519        assert!(result.valid, "SAML reference should verify");
520        assert!(result.pre_digest_data.is_some());
521
522        // Verify the pre-digest data contains the canonicalized document without Signature
523        let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
524        assert!(
525            pre_digest_str.contains("samlp:Response"),
526            "pre-digest should contain Response"
527        );
528        assert!(
529            !pre_digest_str.contains("SignatureValue"),
530            "pre-digest should NOT contain Signature"
531        );
532    }
533}