Skip to main content

xml_sec/xmldsig/
parse.rs

1//! Parsing of XMLDSig `<Signature>` and `<SignedInfo>` elements.
2//!
3//! Implements strict child order enforcement per
4//! [XMLDSig §4.1](https://www.w3.org/TR/xmldsig-core1/#sec-Signature):
5//!
6//! ```text
7//! <Signature>
8//!   <SignedInfo>
9//!     <CanonicalizationMethod Algorithm="..."/>
10//!     <SignatureMethod Algorithm="..."/>
11//!     <Reference URI="..." Id="..." Type="...">+
12//!   </SignedInfo>
13//!   <SignatureValue>...</SignatureValue>
14//!   <KeyInfo>?
15//!   <Object>*
16//! </Signature>
17//! ```
18
19use roxmltree::{Document, Node};
20
21use super::digest::DigestAlgorithm;
22use super::transforms::{self, Transform};
23use crate::c14n::C14nAlgorithm;
24
25/// XMLDSig namespace URI.
26pub(crate) const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
27
28/// Signature algorithms supported for signing and verification.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum SignatureAlgorithm {
31    /// RSA with SHA-1. **Verify-only** — signing disabled.
32    RsaSha1,
33    /// RSA with SHA-256 (most common in SAML).
34    RsaSha256,
35    /// RSA with SHA-384.
36    RsaSha384,
37    /// RSA with SHA-512.
38    RsaSha512,
39    /// ECDSA P-256 with SHA-256.
40    EcdsaP256Sha256,
41    /// XMLDSig `ecdsa-sha384` URI.
42    ///
43    /// The variant name is historical.
44    ///
45    /// Verification currently accepts this XMLDSig URI for P-384 and for the
46    /// donor P-521 interop case.
47    EcdsaP384Sha384,
48}
49
50impl SignatureAlgorithm {
51    /// Parse from an XML algorithm URI.
52    #[must_use]
53    pub fn from_uri(uri: &str) -> Option<Self> {
54        match uri {
55            "http://www.w3.org/2000/09/xmldsig#rsa-sha1" => Some(Self::RsaSha1),
56            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => Some(Self::RsaSha256),
57            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" => Some(Self::RsaSha384),
58            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" => Some(Self::RsaSha512),
59            "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => Some(Self::EcdsaP256Sha256),
60            "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384" => Some(Self::EcdsaP384Sha384),
61            _ => None,
62        }
63    }
64
65    /// Return the XML namespace URI.
66    #[must_use]
67    pub fn uri(self) -> &'static str {
68        match self {
69            Self::RsaSha1 => "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
70            Self::RsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
71            Self::RsaSha384 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
72            Self::RsaSha512 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
73            Self::EcdsaP256Sha256 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
74            Self::EcdsaP384Sha384 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
75        }
76    }
77
78    /// Whether this algorithm is allowed for signing (not just verification).
79    #[must_use]
80    pub fn signing_allowed(self) -> bool {
81        !matches!(self, Self::RsaSha1)
82    }
83}
84
85/// Parsed `<SignedInfo>` element.
86#[derive(Debug)]
87pub struct SignedInfo {
88    /// Canonicalization method for SignedInfo itself.
89    pub c14n_method: C14nAlgorithm,
90    /// Signature algorithm.
91    pub signature_method: SignatureAlgorithm,
92    /// One or more `<Reference>` elements.
93    pub references: Vec<Reference>,
94}
95
96/// Parsed `<Reference>` element.
97#[derive(Debug)]
98pub struct Reference {
99    /// URI attribute (e.g., `""`, `"#_assert1"`).
100    pub uri: Option<String>,
101    /// Id attribute.
102    pub id: Option<String>,
103    /// Type attribute.
104    pub ref_type: Option<String>,
105    /// Transform chain.
106    pub transforms: Vec<Transform>,
107    /// Digest algorithm.
108    pub digest_method: DigestAlgorithm,
109    /// Raw digest value (base64-decoded).
110    pub digest_value: Vec<u8>,
111}
112
113/// Errors during XMLDSig element parsing.
114#[derive(Debug, thiserror::Error)]
115#[non_exhaustive]
116pub enum ParseError {
117    /// Missing required element.
118    #[error("missing required element: <{element}>")]
119    MissingElement {
120        /// Name of the missing element.
121        element: &'static str,
122    },
123
124    /// Invalid structure (wrong child order, unexpected element, etc.).
125    #[error("invalid structure: {0}")]
126    InvalidStructure(String),
127
128    /// Unsupported algorithm URI.
129    #[error("unsupported algorithm: {uri}")]
130    UnsupportedAlgorithm {
131        /// The unrecognized algorithm URI.
132        uri: String,
133    },
134
135    /// Base64 decode error.
136    #[error("base64 decode error: {0}")]
137    Base64(String),
138
139    /// DigestValue length did not match the declared DigestMethod.
140    #[error(
141        "digest length mismatch for {algorithm}: expected {expected} bytes, got {actual} bytes"
142    )]
143    DigestLengthMismatch {
144        /// Digest algorithm URI/name used for diagnostics.
145        algorithm: &'static str,
146        /// Expected decoded digest length in bytes.
147        expected: usize,
148        /// Actual decoded digest length in bytes.
149        actual: usize,
150    },
151
152    /// Transform parsing error.
153    #[error("transform error: {0}")]
154    Transform(#[from] super::types::TransformError),
155}
156
157/// Find the first `<ds:Signature>` element in the document.
158#[must_use]
159pub fn find_signature_node<'a>(doc: &'a Document<'a>) -> Option<Node<'a, 'a>> {
160    doc.descendants().find(|n| {
161        n.is_element()
162            && n.tag_name().name() == "Signature"
163            && n.tag_name().namespace() == Some(XMLDSIG_NS)
164    })
165}
166
167/// Parse a `<ds:SignedInfo>` element.
168///
169/// Enforces strict child order per XMLDSig spec:
170/// `<CanonicalizationMethod>` → `<SignatureMethod>` → `<Reference>`+
171pub fn parse_signed_info(signed_info_node: Node) -> Result<SignedInfo, ParseError> {
172    verify_ds_element(signed_info_node, "SignedInfo")?;
173
174    let mut children = element_children(signed_info_node);
175
176    // 1. CanonicalizationMethod (required, first)
177    let c14n_node = children.next().ok_or(ParseError::MissingElement {
178        element: "CanonicalizationMethod",
179    })?;
180    verify_ds_element(c14n_node, "CanonicalizationMethod")?;
181    let c14n_uri = required_algorithm_attr(c14n_node, "CanonicalizationMethod")?;
182    let mut c14n_method =
183        C14nAlgorithm::from_uri(c14n_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
184            uri: c14n_uri.to_string(),
185        })?;
186    if let Some(prefix_list) = parse_inclusive_prefixes(c14n_node)? {
187        if c14n_method.mode() == crate::c14n::C14nMode::Exclusive1_0 {
188            c14n_method = c14n_method.with_prefix_list(&prefix_list);
189        } else {
190            return Err(ParseError::UnsupportedAlgorithm {
191                uri: c14n_uri.to_string(),
192            });
193        }
194    }
195
196    // 2. SignatureMethod (required, second)
197    let sig_method_node = children.next().ok_or(ParseError::MissingElement {
198        element: "SignatureMethod",
199    })?;
200    verify_ds_element(sig_method_node, "SignatureMethod")?;
201    let sig_uri = required_algorithm_attr(sig_method_node, "SignatureMethod")?;
202    let signature_method =
203        SignatureAlgorithm::from_uri(sig_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
204            uri: sig_uri.to_string(),
205        })?;
206
207    // 3. One or more Reference elements
208    let mut references = Vec::new();
209    for child in children {
210        verify_ds_element(child, "Reference")?;
211        references.push(parse_reference(child)?);
212    }
213    if references.is_empty() {
214        return Err(ParseError::MissingElement {
215            element: "Reference",
216        });
217    }
218
219    Ok(SignedInfo {
220        c14n_method,
221        signature_method,
222        references,
223    })
224}
225
226/// Parse a single `<ds:Reference>` element.
227///
228/// Structure: `<Transforms>?` → `<DigestMethod>` → `<DigestValue>`
229pub(crate) fn parse_reference(reference_node: Node) -> Result<Reference, ParseError> {
230    let uri = reference_node.attribute("URI").map(String::from);
231    let id = reference_node.attribute("Id").map(String::from);
232    let ref_type = reference_node.attribute("Type").map(String::from);
233
234    let mut children = element_children(reference_node);
235
236    // Optional <Transforms>
237    let mut transforms = Vec::new();
238    let mut next = children.next().ok_or(ParseError::MissingElement {
239        element: "DigestMethod",
240    })?;
241
242    if next.tag_name().name() == "Transforms" && next.tag_name().namespace() == Some(XMLDSIG_NS) {
243        transforms = transforms::parse_transforms(next)?;
244        next = children.next().ok_or(ParseError::MissingElement {
245            element: "DigestMethod",
246        })?;
247    }
248
249    // Required <DigestMethod>
250    verify_ds_element(next, "DigestMethod")?;
251    let digest_uri = required_algorithm_attr(next, "DigestMethod")?;
252    let digest_method =
253        DigestAlgorithm::from_uri(digest_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
254            uri: digest_uri.to_string(),
255        })?;
256
257    // Required <DigestValue>
258    let digest_value_node = children.next().ok_or(ParseError::MissingElement {
259        element: "DigestValue",
260    })?;
261    verify_ds_element(digest_value_node, "DigestValue")?;
262    let mut digest_b64 = String::new();
263    for child in digest_value_node.children() {
264        if child.is_element() {
265            return Err(ParseError::InvalidStructure(
266                "DigestValue must not contain element children".into(),
267            ));
268        }
269        if child.is_text()
270            && let Some(text) = child.text()
271        {
272            digest_b64.push_str(text);
273        }
274    }
275    let digest_value = base64_decode_digest(&digest_b64, digest_method)?;
276
277    // No more children expected
278    if let Some(unexpected) = children.next() {
279        return Err(ParseError::InvalidStructure(format!(
280            "unexpected element <{}> after <DigestValue> in <Reference>",
281            unexpected.tag_name().name()
282        )));
283    }
284
285    Ok(Reference {
286        uri,
287        id,
288        ref_type,
289        transforms,
290        digest_method,
291        digest_value,
292    })
293}
294
295// ── Helpers ──────────────────────────────────────────────────────────────────
296
297/// Iterate only element children (skip text, comments, PIs).
298fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
299    node.children().filter(|n| n.is_element())
300}
301
302/// Verify that a node is a `<ds:{expected_name}>` element.
303fn verify_ds_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
304    if !node.is_element() {
305        return Err(ParseError::InvalidStructure(format!(
306            "expected element <{expected_name}>, got non-element node"
307        )));
308    }
309    let tag = node.tag_name();
310    if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG_NS) {
311        return Err(ParseError::InvalidStructure(format!(
312            "expected <ds:{expected_name}>, got <{}{}>",
313            tag.namespace()
314                .map(|ns| format!("{{{ns}}}"))
315                .unwrap_or_default(),
316            tag.name()
317        )));
318    }
319    Ok(())
320}
321
322/// Get the required `Algorithm` attribute from an element.
323fn required_algorithm_attr<'a>(
324    node: Node<'a, 'a>,
325    element_name: &'static str,
326) -> Result<&'a str, ParseError> {
327    node.attribute("Algorithm").ok_or_else(|| {
328        ParseError::InvalidStructure(format!("missing Algorithm attribute on <{element_name}>"))
329    })
330}
331
332/// Parse the `PrefixList` attribute from an `<ec:InclusiveNamespaces>` child of
333/// `<CanonicalizationMethod>`, if present.
334///
335/// This mirrors transform parsing for Exclusive C14N and keeps SignedInfo
336/// canonicalization parameters lossless.
337fn parse_inclusive_prefixes(node: Node) -> Result<Option<String>, ParseError> {
338    const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
339
340    for child in node.children() {
341        if child.is_element() {
342            let tag = child.tag_name();
343            if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
344            {
345                return child
346                    .attribute("PrefixList")
347                    .map(str::to_string)
348                    .ok_or_else(|| {
349                        ParseError::InvalidStructure(
350                            "missing PrefixList attribute on <InclusiveNamespaces>".into(),
351                        )
352                    })
353                    .map(Some);
354            }
355        }
356    }
357
358    Ok(None)
359}
360
361/// Base64-decode a digest value string, stripping whitespace.
362///
363/// XMLDSig allows whitespace within base64 content (line-wrapped encodings).
364fn base64_decode_digest(b64: &str, digest_method: DigestAlgorithm) -> Result<Vec<u8>, ParseError> {
365    use base64::Engine;
366    use base64::engine::general_purpose::STANDARD;
367
368    let mut cleaned = String::with_capacity(b64.len());
369    for ch in b64.chars() {
370        if matches!(ch, ' ' | '\t' | '\r' | '\n') {
371            continue;
372        }
373        if ch.is_ascii_whitespace() {
374            return Err(ParseError::Base64(format!(
375                "invalid XML whitespace U+{:04X} in DigestValue",
376                u32::from(ch)
377            )));
378        }
379        cleaned.push(ch);
380    }
381    let digest = STANDARD
382        .decode(&cleaned)
383        .map_err(|e| ParseError::Base64(e.to_string()))?;
384    let expected = digest_method.output_len();
385    let actual = digest.len();
386    if actual != expected {
387        return Err(ParseError::DigestLengthMismatch {
388            algorithm: digest_method.uri(),
389            expected,
390            actual,
391        });
392    }
393    Ok(digest)
394}
395
396#[cfg(test)]
397#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
398mod tests {
399    use super::*;
400
401    // ── SignatureAlgorithm ───────────────────────────────────────────
402
403    #[test]
404    fn signature_algorithm_from_uri_rsa_sha256() {
405        assert_eq!(
406            SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"),
407            Some(SignatureAlgorithm::RsaSha256)
408        );
409    }
410
411    #[test]
412    fn signature_algorithm_from_uri_rsa_sha1() {
413        assert_eq!(
414            SignatureAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
415            Some(SignatureAlgorithm::RsaSha1)
416        );
417    }
418
419    #[test]
420    fn signature_algorithm_from_uri_ecdsa_sha256() {
421        assert_eq!(
422            SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"),
423            Some(SignatureAlgorithm::EcdsaP256Sha256)
424        );
425    }
426
427    #[test]
428    fn signature_algorithm_from_uri_unknown() {
429        assert_eq!(
430            SignatureAlgorithm::from_uri("http://example.com/unknown"),
431            None
432        );
433    }
434
435    #[test]
436    fn signature_algorithm_uri_round_trip() {
437        for algo in [
438            SignatureAlgorithm::RsaSha1,
439            SignatureAlgorithm::RsaSha256,
440            SignatureAlgorithm::RsaSha384,
441            SignatureAlgorithm::RsaSha512,
442            SignatureAlgorithm::EcdsaP256Sha256,
443            SignatureAlgorithm::EcdsaP384Sha384,
444        ] {
445            assert_eq!(
446                SignatureAlgorithm::from_uri(algo.uri()),
447                Some(algo),
448                "round-trip failed for {algo:?}"
449            );
450        }
451    }
452
453    #[test]
454    fn rsa_sha1_verify_only() {
455        assert!(!SignatureAlgorithm::RsaSha1.signing_allowed());
456        assert!(SignatureAlgorithm::RsaSha256.signing_allowed());
457        assert!(SignatureAlgorithm::EcdsaP256Sha256.signing_allowed());
458    }
459
460    // ── find_signature_node ──────────────────────────────────────────
461
462    #[test]
463    fn find_signature_in_saml() {
464        let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
465            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
466                <ds:SignedInfo/>
467            </ds:Signature>
468        </samlp:Response>"#;
469        let doc = Document::parse(xml).unwrap();
470        let sig = find_signature_node(&doc);
471        assert!(sig.is_some());
472        assert_eq!(sig.unwrap().tag_name().name(), "Signature");
473    }
474
475    #[test]
476    fn find_signature_missing() {
477        let xml = "<root><child/></root>";
478        let doc = Document::parse(xml).unwrap();
479        assert!(find_signature_node(&doc).is_none());
480    }
481
482    #[test]
483    fn find_signature_ignores_wrong_namespace() {
484        let xml = r#"<root><Signature xmlns="http://example.com/fake"/></root>"#;
485        let doc = Document::parse(xml).unwrap();
486        assert!(find_signature_node(&doc).is_none());
487    }
488
489    // ── parse_signed_info: happy path ────────────────────────────────
490
491    #[test]
492    fn parse_signed_info_rsa_sha256_with_reference() {
493        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
494            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
495            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
496            <Reference URI="">
497                <Transforms>
498                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
499                    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
500                </Transforms>
501                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
502                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
503            </Reference>
504        </SignedInfo>"#;
505        let doc = Document::parse(xml).unwrap();
506        let si = parse_signed_info(doc.root_element()).unwrap();
507
508        assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
509        assert_eq!(si.references.len(), 1);
510
511        let r = &si.references[0];
512        assert_eq!(r.uri.as_deref(), Some(""));
513        assert_eq!(r.digest_method, DigestAlgorithm::Sha256);
514        assert_eq!(r.digest_value, vec![0u8; 32]);
515        assert_eq!(r.transforms.len(), 2);
516    }
517
518    #[test]
519    fn parse_signed_info_multiple_references() {
520        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
521            <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
522            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
523            <Reference URI="#a">
524                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
525                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
526            </Reference>
527            <Reference URI="#b">
528                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
529                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
530            </Reference>
531        </SignedInfo>"##;
532        let doc = Document::parse(xml).unwrap();
533        let si = parse_signed_info(doc.root_element()).unwrap();
534
535        assert_eq!(si.signature_method, SignatureAlgorithm::EcdsaP256Sha256);
536        assert_eq!(si.references.len(), 2);
537        assert_eq!(si.references[0].uri.as_deref(), Some("#a"));
538        assert_eq!(si.references[0].digest_method, DigestAlgorithm::Sha256);
539        assert_eq!(si.references[1].uri.as_deref(), Some("#b"));
540        assert_eq!(si.references[1].digest_method, DigestAlgorithm::Sha1);
541    }
542
543    #[test]
544    fn parse_reference_without_transforms() {
545        // Transforms element is optional
546        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
547            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
548            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
549            <Reference URI="#obj">
550                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
551                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
552            </Reference>
553        </SignedInfo>"##;
554        let doc = Document::parse(xml).unwrap();
555        let si = parse_signed_info(doc.root_element()).unwrap();
556
557        assert!(si.references[0].transforms.is_empty());
558    }
559
560    #[test]
561    fn parse_reference_with_all_attributes() {
562        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
563            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
564            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
565            <Reference URI="#data" Id="ref1" Type="http://www.w3.org/2000/09/xmldsig#Object">
566                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
567                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
568            </Reference>
569        </SignedInfo>"##;
570        let doc = Document::parse(xml).unwrap();
571        let si = parse_signed_info(doc.root_element()).unwrap();
572        let r = &si.references[0];
573
574        assert_eq!(r.uri.as_deref(), Some("#data"));
575        assert_eq!(r.id.as_deref(), Some("ref1"));
576        assert_eq!(
577            r.ref_type.as_deref(),
578            Some("http://www.w3.org/2000/09/xmldsig#Object")
579        );
580    }
581
582    #[test]
583    fn parse_reference_absent_uri() {
584        // URI attribute is optional per spec
585        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
586            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
587            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
588            <Reference>
589                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
590                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
591            </Reference>
592        </SignedInfo>"#;
593        let doc = Document::parse(xml).unwrap();
594        let si = parse_signed_info(doc.root_element()).unwrap();
595        assert!(si.references[0].uri.is_none());
596    }
597
598    #[test]
599    fn parse_signed_info_preserves_inclusive_prefixes() {
600        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
601                                 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
602            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
603                <ec:InclusiveNamespaces PrefixList="ds saml #default"/>
604            </CanonicalizationMethod>
605            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
606            <Reference URI="">
607                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
608                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
609            </Reference>
610        </SignedInfo>"#;
611        let doc = Document::parse(xml).unwrap();
612
613        let si = parse_signed_info(doc.root_element()).unwrap();
614        assert!(si.c14n_method.inclusive_prefixes().contains("ds"));
615        assert!(si.c14n_method.inclusive_prefixes().contains("saml"));
616        assert!(si.c14n_method.inclusive_prefixes().contains(""));
617    }
618
619    // ── parse_signed_info: error cases ───────────────────────────────
620
621    #[test]
622    fn missing_canonicalization_method() {
623        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
624            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
625            <Reference URI="">
626                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
627                <DigestValue>dGVzdA==</DigestValue>
628            </Reference>
629        </SignedInfo>"#;
630        let doc = Document::parse(xml).unwrap();
631        let result = parse_signed_info(doc.root_element());
632        assert!(result.is_err());
633        // SignatureMethod is first child but expected CanonicalizationMethod
634        assert!(matches!(
635            result.unwrap_err(),
636            ParseError::InvalidStructure(_)
637        ));
638    }
639
640    #[test]
641    fn missing_signature_method() {
642        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
643            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
644            <Reference URI="">
645                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
646                <DigestValue>dGVzdA==</DigestValue>
647            </Reference>
648        </SignedInfo>"#;
649        let doc = Document::parse(xml).unwrap();
650        let result = parse_signed_info(doc.root_element());
651        assert!(result.is_err());
652        // Reference is second child but expected SignatureMethod
653        assert!(matches!(
654            result.unwrap_err(),
655            ParseError::InvalidStructure(_)
656        ));
657    }
658
659    #[test]
660    fn no_references() {
661        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
662            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
663            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
664        </SignedInfo>"#;
665        let doc = Document::parse(xml).unwrap();
666        let result = parse_signed_info(doc.root_element());
667        assert!(matches!(
668            result.unwrap_err(),
669            ParseError::MissingElement {
670                element: "Reference"
671            }
672        ));
673    }
674
675    #[test]
676    fn unsupported_c14n_algorithm() {
677        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
678            <CanonicalizationMethod Algorithm="http://example.com/bogus-c14n"/>
679            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
680            <Reference URI="">
681                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
682                <DigestValue>dGVzdA==</DigestValue>
683            </Reference>
684        </SignedInfo>"#;
685        let doc = Document::parse(xml).unwrap();
686        let result = parse_signed_info(doc.root_element());
687        assert!(matches!(
688            result.unwrap_err(),
689            ParseError::UnsupportedAlgorithm { .. }
690        ));
691    }
692
693    #[test]
694    fn unsupported_signature_algorithm() {
695        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
696            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
697            <SignatureMethod Algorithm="http://example.com/bogus-sign"/>
698            <Reference URI="">
699                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
700                <DigestValue>dGVzdA==</DigestValue>
701            </Reference>
702        </SignedInfo>"#;
703        let doc = Document::parse(xml).unwrap();
704        let result = parse_signed_info(doc.root_element());
705        assert!(matches!(
706            result.unwrap_err(),
707            ParseError::UnsupportedAlgorithm { .. }
708        ));
709    }
710
711    #[test]
712    fn unsupported_digest_algorithm() {
713        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
714            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
715            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
716            <Reference URI="">
717                <DigestMethod Algorithm="http://example.com/bogus-digest"/>
718                <DigestValue>dGVzdA==</DigestValue>
719            </Reference>
720        </SignedInfo>"#;
721        let doc = Document::parse(xml).unwrap();
722        let result = parse_signed_info(doc.root_element());
723        assert!(matches!(
724            result.unwrap_err(),
725            ParseError::UnsupportedAlgorithm { .. }
726        ));
727    }
728
729    #[test]
730    fn missing_digest_method() {
731        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
732            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
733            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
734            <Reference URI="">
735                <DigestValue>dGVzdA==</DigestValue>
736            </Reference>
737        </SignedInfo>"#;
738        let doc = Document::parse(xml).unwrap();
739        let result = parse_signed_info(doc.root_element());
740        // DigestValue is not DigestMethod
741        assert!(result.is_err());
742    }
743
744    #[test]
745    fn missing_digest_value() {
746        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
747            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
748            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
749            <Reference URI="">
750                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
751            </Reference>
752        </SignedInfo>"#;
753        let doc = Document::parse(xml).unwrap();
754        let result = parse_signed_info(doc.root_element());
755        assert!(matches!(
756            result.unwrap_err(),
757            ParseError::MissingElement {
758                element: "DigestValue"
759            }
760        ));
761    }
762
763    #[test]
764    fn invalid_base64_digest_value() {
765        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
766            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
767            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
768            <Reference URI="">
769                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
770                <DigestValue>!!!not-base64!!!</DigestValue>
771            </Reference>
772        </SignedInfo>"#;
773        let doc = Document::parse(xml).unwrap();
774        let result = parse_signed_info(doc.root_element());
775        assert!(matches!(result.unwrap_err(), ParseError::Base64(_)));
776    }
777
778    #[test]
779    fn digest_value_length_must_match_digest_method() {
780        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
781            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
782            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
783            <Reference URI="">
784                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
785                <DigestValue>dGVzdA==</DigestValue>
786            </Reference>
787        </SignedInfo>"#;
788        let doc = Document::parse(xml).unwrap();
789
790        let result = parse_signed_info(doc.root_element());
791        assert!(matches!(
792            result.unwrap_err(),
793            ParseError::DigestLengthMismatch {
794                algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
795                expected: 32,
796                actual: 4,
797            }
798        ));
799    }
800
801    #[test]
802    fn inclusive_prefixes_on_inclusive_c14n_is_rejected() {
803        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
804                                 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
805            <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">
806                <ec:InclusiveNamespaces PrefixList="ds"/>
807            </CanonicalizationMethod>
808            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
809            <Reference URI="">
810                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
811                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
812            </Reference>
813        </SignedInfo>"#;
814        let doc = Document::parse(xml).unwrap();
815
816        let result = parse_signed_info(doc.root_element());
817        assert!(matches!(
818            result.unwrap_err(),
819            ParseError::UnsupportedAlgorithm { .. }
820        ));
821    }
822
823    #[test]
824    fn extra_element_after_digest_value() {
825        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
826            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
827            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
828            <Reference URI="">
829                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
830                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
831                <Unexpected/>
832            </Reference>
833        </SignedInfo>"#;
834        let doc = Document::parse(xml).unwrap();
835        let result = parse_signed_info(doc.root_element());
836        assert!(matches!(
837            result.unwrap_err(),
838            ParseError::InvalidStructure(_)
839        ));
840    }
841
842    #[test]
843    fn digest_value_with_element_child_is_rejected() {
844        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
845            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
846            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
847            <Reference URI="">
848                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
849                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=<Junk/>AAAA</DigestValue>
850            </Reference>
851        </SignedInfo>"#;
852        let doc = Document::parse(xml).unwrap();
853
854        let result = parse_signed_info(doc.root_element());
855        assert!(matches!(
856            result.unwrap_err(),
857            ParseError::InvalidStructure(_)
858        ));
859    }
860
861    #[test]
862    fn wrong_namespace_on_signed_info() {
863        let xml = r#"<SignedInfo xmlns="http://example.com/fake">
864            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
865        </SignedInfo>"#;
866        let doc = Document::parse(xml).unwrap();
867        let result = parse_signed_info(doc.root_element());
868        assert!(matches!(
869            result.unwrap_err(),
870            ParseError::InvalidStructure(_)
871        ));
872    }
873
874    // ── Whitespace-wrapped base64 ────────────────────────────────────
875
876    #[test]
877    fn base64_with_whitespace() {
878        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
879            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
880            <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
881            <Reference URI="">
882                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
883                <DigestValue>
884                    AAAAAAAA
885                    AAAAAAAAAAAAAAAAAAA=
886                </DigestValue>
887            </Reference>
888        </SignedInfo>"#;
889        let doc = Document::parse(xml).unwrap();
890        let si = parse_signed_info(doc.root_element()).unwrap();
891        assert_eq!(si.references[0].digest_value, vec![0u8; 20]);
892    }
893
894    #[test]
895    fn base64_decode_digest_accepts_xml_whitespace_chars() {
896        let digest =
897            base64_decode_digest("AAAA\tAAAA\rAAAA\nAAAA AAAAAAAAAAA=", DigestAlgorithm::Sha1)
898                .expect("XML whitespace in DigestValue must be accepted");
899        assert_eq!(digest, vec![0u8; 20]);
900    }
901
902    #[test]
903    fn base64_decode_digest_rejects_non_xml_ascii_whitespace() {
904        let err = base64_decode_digest(
905            "AAAA\u{000C}AAAAAAAAAAAAAAAAAAAAAAA=",
906            DigestAlgorithm::Sha1,
907        )
908        .expect_err("form-feed/vertical-tab in DigestValue must be rejected");
909        assert!(matches!(err, ParseError::Base64(_)));
910    }
911
912    // ── Real-world SAML structure ────────────────────────────────────
913
914    #[test]
915    fn saml_response_signed_info() {
916        let xml = r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
917            <ds:SignedInfo>
918                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
919                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
920                <ds:Reference URI="#_resp1">
921                    <ds:Transforms>
922                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
923                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
924                    </ds:Transforms>
925                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
926                    <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
927                </ds:Reference>
928            </ds:SignedInfo>
929            <ds:SignatureValue>ZmFrZQ==</ds:SignatureValue>
930        </ds:Signature>"##;
931        let doc = Document::parse(xml).unwrap();
932
933        // Find SignedInfo within Signature
934        let sig_node = doc.root_element();
935        let signed_info_node = sig_node
936            .children()
937            .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
938            .unwrap();
939
940        let si = parse_signed_info(signed_info_node).unwrap();
941        assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
942        assert_eq!(si.references.len(), 1);
943        assert_eq!(si.references[0].uri.as_deref(), Some("#_resp1"));
944        assert_eq!(si.references[0].transforms.len(), 2);
945        assert_eq!(si.references[0].digest_value, vec![0u8; 32]);
946    }
947}