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 super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_text};
24use crate::c14n::C14nAlgorithm;
25
26/// XMLDSig namespace URI.
27pub(crate) const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
28/// XMLDSig 1.1 namespace URI.
29pub(crate) const XMLDSIG11_NS: &str = "http://www.w3.org/2009/xmldsig11#";
30const MAX_DER_ENCODED_KEY_VALUE_LEN: usize = 8192;
31const MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN: usize = 65_536;
32const MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN: usize = MAX_DER_ENCODED_KEY_VALUE_LEN.div_ceil(3) * 4;
33const MAX_KEY_NAME_TEXT_LEN: usize = 4096;
34
35/// Signature algorithms supported for signing and verification.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum SignatureAlgorithm {
38    /// RSA with SHA-1. **Verify-only** — signing disabled.
39    RsaSha1,
40    /// RSA with SHA-256 (most common in SAML).
41    RsaSha256,
42    /// RSA with SHA-384.
43    RsaSha384,
44    /// RSA with SHA-512.
45    RsaSha512,
46    /// ECDSA P-256 with SHA-256.
47    EcdsaP256Sha256,
48    /// XMLDSig `ecdsa-sha384` URI.
49    ///
50    /// The variant name is historical.
51    ///
52    /// Verification currently accepts this XMLDSig URI for P-384 and for the
53    /// donor P-521 interop case.
54    EcdsaP384Sha384,
55}
56
57impl SignatureAlgorithm {
58    /// Parse from an XML algorithm URI.
59    #[must_use]
60    pub fn from_uri(uri: &str) -> Option<Self> {
61        match uri {
62            "http://www.w3.org/2000/09/xmldsig#rsa-sha1" => Some(Self::RsaSha1),
63            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => Some(Self::RsaSha256),
64            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" => Some(Self::RsaSha384),
65            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" => Some(Self::RsaSha512),
66            "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => Some(Self::EcdsaP256Sha256),
67            "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384" => Some(Self::EcdsaP384Sha384),
68            _ => None,
69        }
70    }
71
72    /// Return the XML namespace URI.
73    #[must_use]
74    pub fn uri(self) -> &'static str {
75        match self {
76            Self::RsaSha1 => "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
77            Self::RsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
78            Self::RsaSha384 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
79            Self::RsaSha512 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
80            Self::EcdsaP256Sha256 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
81            Self::EcdsaP384Sha384 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
82        }
83    }
84
85    /// Whether this algorithm is allowed for signing (not just verification).
86    #[must_use]
87    pub fn signing_allowed(self) -> bool {
88        !matches!(self, Self::RsaSha1)
89    }
90}
91
92/// Parsed `<SignedInfo>` element.
93#[derive(Debug)]
94pub struct SignedInfo {
95    /// Canonicalization method for SignedInfo itself.
96    pub c14n_method: C14nAlgorithm,
97    /// Signature algorithm.
98    pub signature_method: SignatureAlgorithm,
99    /// One or more `<Reference>` elements.
100    pub references: Vec<Reference>,
101}
102
103/// Parsed `<Reference>` element.
104#[derive(Debug)]
105pub struct Reference {
106    /// URI attribute (e.g., `""`, `"#_assert1"`).
107    pub uri: Option<String>,
108    /// Id attribute.
109    pub id: Option<String>,
110    /// Type attribute.
111    pub ref_type: Option<String>,
112    /// Transform chain.
113    pub transforms: Vec<Transform>,
114    /// Digest algorithm.
115    pub digest_method: DigestAlgorithm,
116    /// Raw digest value (base64-decoded).
117    pub digest_value: Vec<u8>,
118}
119
120/// Parsed `<KeyInfo>` element.
121#[derive(Debug, Default, Clone, PartialEq, Eq)]
122#[non_exhaustive]
123pub struct KeyInfo {
124    /// Sources discovered under `<KeyInfo>` in document order.
125    pub sources: Vec<KeyInfoSource>,
126}
127
128/// Top-level key material source parsed from `<KeyInfo>`.
129#[derive(Debug, Clone, PartialEq, Eq)]
130#[non_exhaustive]
131pub enum KeyInfoSource {
132    /// `<KeyName>` source.
133    KeyName(String),
134    /// `<KeyValue>` source.
135    KeyValue(KeyValueInfo),
136    /// `<X509Data>` source.
137    X509Data(X509DataInfo),
138    /// `dsig11:DEREncodedKeyValue` source (base64-decoded DER bytes).
139    DerEncodedKeyValue(Vec<u8>),
140}
141
142/// Parsed `<KeyValue>` dispatch result.
143#[derive(Debug, Clone, PartialEq, Eq)]
144#[non_exhaustive]
145pub enum KeyValueInfo {
146    /// `<RSAKeyValue>`.
147    RsaKeyValue,
148    /// `dsig11:ECKeyValue` (the XMLDSig 1.1 namespace form).
149    EcKeyValue,
150    /// Any other `<KeyValue>` child not yet supported by this phase.
151    Unsupported {
152        /// Namespace URI of the unsupported child, when present.
153        namespace: Option<String>,
154        /// Local name of the unsupported child element.
155        local_name: String,
156    },
157}
158
159/// Parsed `<X509Data>` children (dispatch-only in P2-001).
160#[derive(Debug, Default, Clone, PartialEq, Eq)]
161#[non_exhaustive]
162pub struct X509DataInfo {
163    /// Number of `<X509Certificate>` children.
164    pub certificate_count: usize,
165    /// Number of `<X509SubjectName>` children.
166    pub subject_name_count: usize,
167    /// Number of `<X509IssuerSerial>` children.
168    pub issuer_serial_count: usize,
169    /// Number of `<X509SKI>` children.
170    pub ski_count: usize,
171    /// Number of `<X509CRL>` children.
172    pub crl_count: usize,
173    /// Number of `<X509Digest>` children.
174    pub digest_count: usize,
175}
176
177/// Errors during XMLDSig element parsing.
178#[derive(Debug, thiserror::Error)]
179#[non_exhaustive]
180pub enum ParseError {
181    /// Missing required element.
182    #[error("missing required element: <{element}>")]
183    MissingElement {
184        /// Name of the missing element.
185        element: &'static str,
186    },
187
188    /// Invalid structure (wrong child order, unexpected element, etc.).
189    #[error("invalid structure: {0}")]
190    InvalidStructure(String),
191
192    /// Unsupported algorithm URI.
193    #[error("unsupported algorithm: {uri}")]
194    UnsupportedAlgorithm {
195        /// The unrecognized algorithm URI.
196        uri: String,
197    },
198
199    /// Base64 decode error.
200    #[error("base64 decode error: {0}")]
201    Base64(String),
202
203    /// DigestValue length did not match the declared DigestMethod.
204    #[error(
205        "digest length mismatch for {algorithm}: expected {expected} bytes, got {actual} bytes"
206    )]
207    DigestLengthMismatch {
208        /// Digest algorithm URI/name used for diagnostics.
209        algorithm: &'static str,
210        /// Expected decoded digest length in bytes.
211        expected: usize,
212        /// Actual decoded digest length in bytes.
213        actual: usize,
214    },
215
216    /// Transform parsing error.
217    #[error("transform error: {0}")]
218    Transform(#[from] super::types::TransformError),
219}
220
221/// Find the first `<ds:Signature>` element in the document.
222#[must_use]
223pub fn find_signature_node<'a>(doc: &'a Document<'a>) -> Option<Node<'a, 'a>> {
224    doc.descendants().find(|n| {
225        n.is_element()
226            && n.tag_name().name() == "Signature"
227            && n.tag_name().namespace() == Some(XMLDSIG_NS)
228    })
229}
230
231/// Parse a `<ds:SignedInfo>` element.
232///
233/// Enforces strict child order per XMLDSig spec:
234/// `<CanonicalizationMethod>` → `<SignatureMethod>` → `<Reference>`+
235pub fn parse_signed_info(signed_info_node: Node) -> Result<SignedInfo, ParseError> {
236    verify_ds_element(signed_info_node, "SignedInfo")?;
237
238    let mut children = element_children(signed_info_node);
239
240    // 1. CanonicalizationMethod (required, first)
241    let c14n_node = children.next().ok_or(ParseError::MissingElement {
242        element: "CanonicalizationMethod",
243    })?;
244    verify_ds_element(c14n_node, "CanonicalizationMethod")?;
245    let c14n_uri = required_algorithm_attr(c14n_node, "CanonicalizationMethod")?;
246    let mut c14n_method =
247        C14nAlgorithm::from_uri(c14n_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
248            uri: c14n_uri.to_string(),
249        })?;
250    if let Some(prefix_list) = parse_inclusive_prefixes(c14n_node)? {
251        if c14n_method.mode() == crate::c14n::C14nMode::Exclusive1_0 {
252            c14n_method = c14n_method.with_prefix_list(&prefix_list);
253        } else {
254            return Err(ParseError::UnsupportedAlgorithm {
255                uri: c14n_uri.to_string(),
256            });
257        }
258    }
259
260    // 2. SignatureMethod (required, second)
261    let sig_method_node = children.next().ok_or(ParseError::MissingElement {
262        element: "SignatureMethod",
263    })?;
264    verify_ds_element(sig_method_node, "SignatureMethod")?;
265    let sig_uri = required_algorithm_attr(sig_method_node, "SignatureMethod")?;
266    let signature_method =
267        SignatureAlgorithm::from_uri(sig_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
268            uri: sig_uri.to_string(),
269        })?;
270
271    // 3. One or more Reference elements
272    let mut references = Vec::new();
273    for child in children {
274        verify_ds_element(child, "Reference")?;
275        references.push(parse_reference(child)?);
276    }
277    if references.is_empty() {
278        return Err(ParseError::MissingElement {
279            element: "Reference",
280        });
281    }
282
283    Ok(SignedInfo {
284        c14n_method,
285        signature_method,
286        references,
287    })
288}
289
290/// Parse a single `<ds:Reference>` element.
291///
292/// Structure: `<Transforms>?` → `<DigestMethod>` → `<DigestValue>`
293pub(crate) fn parse_reference(reference_node: Node) -> Result<Reference, ParseError> {
294    let uri = reference_node.attribute("URI").map(String::from);
295    let id = reference_node.attribute("Id").map(String::from);
296    let ref_type = reference_node.attribute("Type").map(String::from);
297
298    let mut children = element_children(reference_node);
299
300    // Optional <Transforms>
301    let mut transforms = Vec::new();
302    let mut next = children.next().ok_or(ParseError::MissingElement {
303        element: "DigestMethod",
304    })?;
305
306    if next.tag_name().name() == "Transforms" && next.tag_name().namespace() == Some(XMLDSIG_NS) {
307        transforms = transforms::parse_transforms(next)?;
308        next = children.next().ok_or(ParseError::MissingElement {
309            element: "DigestMethod",
310        })?;
311    }
312
313    // Required <DigestMethod>
314    verify_ds_element(next, "DigestMethod")?;
315    let digest_uri = required_algorithm_attr(next, "DigestMethod")?;
316    let digest_method =
317        DigestAlgorithm::from_uri(digest_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
318            uri: digest_uri.to_string(),
319        })?;
320
321    // Required <DigestValue>
322    let digest_value_node = children.next().ok_or(ParseError::MissingElement {
323        element: "DigestValue",
324    })?;
325    verify_ds_element(digest_value_node, "DigestValue")?;
326    let digest_value = decode_digest_value_children(digest_value_node, digest_method)?;
327
328    // No more children expected
329    if let Some(unexpected) = children.next() {
330        return Err(ParseError::InvalidStructure(format!(
331            "unexpected element <{}> after <DigestValue> in <Reference>",
332            unexpected.tag_name().name()
333        )));
334    }
335
336    Ok(Reference {
337        uri,
338        id,
339        ref_type,
340        transforms,
341        digest_method,
342        digest_value,
343    })
344}
345
346/// Parse `<ds:KeyInfo>` and dispatch supported child sources.
347///
348/// Supported source elements:
349/// - `<ds:KeyName>`
350/// - `<ds:KeyValue>` (dispatch by child QName; only `dsig11:ECKeyValue` is treated as supported EC)
351/// - `<ds:X509Data>`
352/// - `<dsig11:DEREncodedKeyValue>`
353///
354/// Unknown top-level `<KeyInfo>` children are ignored (lax processing), while
355/// unknown XMLDSig-owned (`ds:*` / `dsig11:*`) children inside `<X509Data>` are
356/// rejected fail-closed.
357/// `<X509Data>` may still be empty or contain only non-XMLDSig extension children.
358pub fn parse_key_info(key_info_node: Node) -> Result<KeyInfo, ParseError> {
359    verify_ds_element(key_info_node, "KeyInfo")?;
360    ensure_no_non_whitespace_text(key_info_node, "KeyInfo")?;
361
362    let mut sources = Vec::new();
363    for child in element_children(key_info_node) {
364        match (child.tag_name().namespace(), child.tag_name().name()) {
365            (Some(XMLDSIG_NS), "KeyName") => {
366                ensure_no_element_children(child, "KeyName")?;
367                let key_name =
368                    collect_text_content_bounded(child, MAX_KEY_NAME_TEXT_LEN, "KeyName")?;
369                sources.push(KeyInfoSource::KeyName(key_name));
370            }
371            (Some(XMLDSIG_NS), "KeyValue") => {
372                let key_value = parse_key_value_dispatch(child)?;
373                sources.push(KeyInfoSource::KeyValue(key_value));
374            }
375            (Some(XMLDSIG_NS), "X509Data") => {
376                let x509 = parse_x509_data_dispatch(child)?;
377                sources.push(KeyInfoSource::X509Data(x509));
378            }
379            (Some(XMLDSIG11_NS), "DEREncodedKeyValue") => {
380                ensure_no_element_children(child, "DEREncodedKeyValue")?;
381                let der = decode_der_encoded_key_value_base64(child)?;
382                sources.push(KeyInfoSource::DerEncodedKeyValue(der));
383            }
384            _ => {}
385        }
386    }
387
388    Ok(KeyInfo { sources })
389}
390
391// ── Helpers ──────────────────────────────────────────────────────────────────
392
393/// Iterate only element children (skip text, comments, PIs).
394fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
395    node.children().filter(|n| n.is_element())
396}
397
398/// Verify that a node is a `<ds:{expected_name}>` element.
399fn verify_ds_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
400    if !node.is_element() {
401        return Err(ParseError::InvalidStructure(format!(
402            "expected element <{expected_name}>, got non-element node"
403        )));
404    }
405    let tag = node.tag_name();
406    if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG_NS) {
407        return Err(ParseError::InvalidStructure(format!(
408            "expected <ds:{expected_name}>, got <{}{}>",
409            tag.namespace()
410                .map(|ns| format!("{{{ns}}}"))
411                .unwrap_or_default(),
412            tag.name()
413        )));
414    }
415    Ok(())
416}
417
418/// Get the required `Algorithm` attribute from an element.
419fn required_algorithm_attr<'a>(
420    node: Node<'a, 'a>,
421    element_name: &'static str,
422) -> Result<&'a str, ParseError> {
423    node.attribute("Algorithm").ok_or_else(|| {
424        ParseError::InvalidStructure(format!("missing Algorithm attribute on <{element_name}>"))
425    })
426}
427
428/// Parse the `PrefixList` attribute from an `<ec:InclusiveNamespaces>` child of
429/// `<CanonicalizationMethod>`, if present.
430///
431/// This mirrors transform parsing for Exclusive C14N and keeps SignedInfo
432/// canonicalization parameters lossless.
433fn parse_inclusive_prefixes(node: Node) -> Result<Option<String>, ParseError> {
434    const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
435
436    for child in node.children() {
437        if child.is_element() {
438            let tag = child.tag_name();
439            if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
440            {
441                return child
442                    .attribute("PrefixList")
443                    .map(str::to_string)
444                    .ok_or_else(|| {
445                        ParseError::InvalidStructure(
446                            "missing PrefixList attribute on <InclusiveNamespaces>".into(),
447                        )
448                    })
449                    .map(Some);
450            }
451        }
452    }
453
454    Ok(None)
455}
456
457fn parse_key_value_dispatch(node: Node) -> Result<KeyValueInfo, ParseError> {
458    verify_ds_element(node, "KeyValue")?;
459    ensure_no_non_whitespace_text(node, "KeyValue")?;
460
461    let mut children = element_children(node);
462    let Some(first_child) = children.next() else {
463        return Err(ParseError::InvalidStructure(
464            "KeyValue must contain exactly one key-value child".into(),
465        ));
466    };
467    if children.next().is_some() {
468        return Err(ParseError::InvalidStructure(
469            "KeyValue must contain exactly one key-value child".into(),
470        ));
471    }
472
473    match (
474        first_child.tag_name().namespace(),
475        first_child.tag_name().name(),
476    ) {
477        (Some(XMLDSIG_NS), "RSAKeyValue") => Ok(KeyValueInfo::RsaKeyValue),
478        (Some(XMLDSIG11_NS), "ECKeyValue") => Ok(KeyValueInfo::EcKeyValue),
479        (namespace, child_name) => Ok(KeyValueInfo::Unsupported {
480            namespace: namespace.map(str::to_string),
481            local_name: child_name.to_string(),
482        }),
483    }
484}
485
486fn parse_x509_data_dispatch(node: Node) -> Result<X509DataInfo, ParseError> {
487    verify_ds_element(node, "X509Data")?;
488    ensure_no_non_whitespace_text(node, "X509Data")?;
489
490    let mut info = X509DataInfo::default();
491    for child in element_children(node) {
492        match (child.tag_name().namespace(), child.tag_name().name()) {
493            (Some(XMLDSIG_NS), "X509Certificate") => {
494                info.certificate_count += 1;
495            }
496            (Some(XMLDSIG_NS), "X509SubjectName") => {
497                info.subject_name_count += 1;
498            }
499            (Some(XMLDSIG_NS), "X509IssuerSerial") => {
500                info.issuer_serial_count += 1;
501            }
502            (Some(XMLDSIG_NS), "X509SKI") => {
503                info.ski_count += 1;
504            }
505            (Some(XMLDSIG_NS), "X509CRL") => {
506                info.crl_count += 1;
507            }
508            (Some(XMLDSIG11_NS), "X509Digest") => {
509                info.digest_count += 1;
510            }
511            (Some(XMLDSIG_NS), child_name) | (Some(XMLDSIG11_NS), child_name) => {
512                return Err(ParseError::InvalidStructure(format!(
513                    "X509Data contains unsupported XMLDSig child element <{child_name}>"
514                )));
515            }
516            _ => {}
517        }
518    }
519
520    Ok(info)
521}
522
523/// Base64-decode a digest value string, stripping whitespace.
524///
525/// XMLDSig allows whitespace within base64 content (line-wrapped encodings).
526fn base64_decode_digest(b64: &str, digest_method: DigestAlgorithm) -> Result<Vec<u8>, ParseError> {
527    use base64::Engine;
528    use base64::engine::general_purpose::STANDARD;
529
530    let expected = digest_method.output_len();
531    let max_base64_len = expected.div_ceil(3) * 4;
532    let mut cleaned = String::with_capacity(b64.len().min(max_base64_len));
533    normalize_xml_base64_text(b64, &mut cleaned).map_err(|err| {
534        ParseError::Base64(format!(
535            "invalid XML whitespace U+{:04X} in DigestValue",
536            err.invalid_byte
537        ))
538    })?;
539    if cleaned.len() > max_base64_len {
540        return Err(ParseError::Base64(
541            "DigestValue exceeds maximum allowed base64 length".into(),
542        ));
543    }
544    let digest = STANDARD
545        .decode(&cleaned)
546        .map_err(|e| ParseError::Base64(e.to_string()))?;
547    let actual = digest.len();
548    if actual != expected {
549        return Err(ParseError::DigestLengthMismatch {
550            algorithm: digest_method.uri(),
551            expected,
552            actual,
553        });
554    }
555    Ok(digest)
556}
557
558fn decode_digest_value_children(
559    digest_value_node: Node<'_, '_>,
560    digest_method: DigestAlgorithm,
561) -> Result<Vec<u8>, ParseError> {
562    let max_base64_len = digest_method.output_len().div_ceil(3) * 4;
563    let mut cleaned = String::with_capacity(max_base64_len);
564
565    for child in digest_value_node.children() {
566        if child.is_element() {
567            return Err(ParseError::InvalidStructure(
568                "DigestValue must not contain element children".into(),
569            ));
570        }
571        if let Some(text) = child.text() {
572            normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
573                ParseError::Base64(format!(
574                    "invalid XML whitespace U+{:04X} in DigestValue",
575                    err.invalid_byte
576                ))
577            })?;
578            if cleaned.len() > max_base64_len {
579                return Err(ParseError::Base64(
580                    "DigestValue exceeds maximum allowed base64 length".into(),
581                ));
582            }
583        }
584    }
585
586    base64_decode_digest(&cleaned, digest_method)
587}
588
589fn decode_der_encoded_key_value_base64(node: Node<'_, '_>) -> Result<Vec<u8>, ParseError> {
590    use base64::Engine;
591    use base64::engine::general_purpose::STANDARD;
592
593    let mut cleaned = String::new();
594    let mut raw_text_len = 0usize;
595    for text in node
596        .children()
597        .filter(|child| child.is_text())
598        .filter_map(|child| child.text())
599    {
600        if raw_text_len.saturating_add(text.len()) > MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN {
601            return Err(ParseError::InvalidStructure(
602                "DEREncodedKeyValue exceeds maximum allowed text length".into(),
603            ));
604        }
605        raw_text_len = raw_text_len.saturating_add(text.len());
606        normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
607            ParseError::Base64(format!(
608                "invalid XML whitespace U+{:04X} in base64 text",
609                err.invalid_byte
610            ))
611        })?;
612        if cleaned.len() > MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN {
613            return Err(ParseError::InvalidStructure(
614                "DEREncodedKeyValue exceeds maximum allowed length".into(),
615            ));
616        }
617    }
618
619    let der = STANDARD
620        .decode(&cleaned)
621        .map_err(|e| ParseError::Base64(e.to_string()))?;
622    if der.is_empty() {
623        return Err(ParseError::InvalidStructure(
624            "DEREncodedKeyValue must not be empty".into(),
625        ));
626    }
627    if der.len() > MAX_DER_ENCODED_KEY_VALUE_LEN {
628        return Err(ParseError::InvalidStructure(
629            "DEREncodedKeyValue exceeds maximum allowed length".into(),
630        ));
631    }
632    Ok(der)
633}
634
635fn collect_text_content_bounded(
636    node: Node<'_, '_>,
637    max_len: usize,
638    element_name: &'static str,
639) -> Result<String, ParseError> {
640    let mut text = String::new();
641    for chunk in node
642        .children()
643        .filter_map(|child| child.is_text().then(|| child.text()).flatten())
644    {
645        if text.len().saturating_add(chunk.len()) > max_len {
646            return Err(ParseError::InvalidStructure(format!(
647                "{element_name} exceeds maximum allowed text length"
648            )));
649        }
650        text.push_str(chunk);
651    }
652    Ok(text)
653}
654
655fn ensure_no_element_children(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
656    if node.children().any(|child| child.is_element()) {
657        return Err(ParseError::InvalidStructure(format!(
658            "{element_name} must not contain child elements"
659        )));
660    }
661    Ok(())
662}
663
664fn ensure_no_non_whitespace_text(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
665    for child in node.children().filter(|child| child.is_text()) {
666        if let Some(text) = child.text()
667            && !is_xml_whitespace_only(text)
668        {
669            return Err(ParseError::InvalidStructure(format!(
670                "{element_name} must not contain non-whitespace mixed content"
671            )));
672        }
673    }
674    Ok(())
675}
676
677#[cfg(test)]
678#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
679mod tests {
680    use super::*;
681    use base64::Engine;
682
683    // ── SignatureAlgorithm ───────────────────────────────────────────
684
685    #[test]
686    fn signature_algorithm_from_uri_rsa_sha256() {
687        assert_eq!(
688            SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"),
689            Some(SignatureAlgorithm::RsaSha256)
690        );
691    }
692
693    #[test]
694    fn signature_algorithm_from_uri_rsa_sha1() {
695        assert_eq!(
696            SignatureAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
697            Some(SignatureAlgorithm::RsaSha1)
698        );
699    }
700
701    #[test]
702    fn signature_algorithm_from_uri_ecdsa_sha256() {
703        assert_eq!(
704            SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"),
705            Some(SignatureAlgorithm::EcdsaP256Sha256)
706        );
707    }
708
709    #[test]
710    fn signature_algorithm_from_uri_unknown() {
711        assert_eq!(
712            SignatureAlgorithm::from_uri("http://example.com/unknown"),
713            None
714        );
715    }
716
717    #[test]
718    fn signature_algorithm_uri_round_trip() {
719        for algo in [
720            SignatureAlgorithm::RsaSha1,
721            SignatureAlgorithm::RsaSha256,
722            SignatureAlgorithm::RsaSha384,
723            SignatureAlgorithm::RsaSha512,
724            SignatureAlgorithm::EcdsaP256Sha256,
725            SignatureAlgorithm::EcdsaP384Sha384,
726        ] {
727            assert_eq!(
728                SignatureAlgorithm::from_uri(algo.uri()),
729                Some(algo),
730                "round-trip failed for {algo:?}"
731            );
732        }
733    }
734
735    #[test]
736    fn rsa_sha1_verify_only() {
737        assert!(!SignatureAlgorithm::RsaSha1.signing_allowed());
738        assert!(SignatureAlgorithm::RsaSha256.signing_allowed());
739        assert!(SignatureAlgorithm::EcdsaP256Sha256.signing_allowed());
740    }
741
742    // ── find_signature_node ──────────────────────────────────────────
743
744    #[test]
745    fn find_signature_in_saml() {
746        let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
747            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
748                <ds:SignedInfo/>
749            </ds:Signature>
750        </samlp:Response>"#;
751        let doc = Document::parse(xml).unwrap();
752        let sig = find_signature_node(&doc);
753        assert!(sig.is_some());
754        assert_eq!(sig.unwrap().tag_name().name(), "Signature");
755    }
756
757    #[test]
758    fn find_signature_missing() {
759        let xml = "<root><child/></root>";
760        let doc = Document::parse(xml).unwrap();
761        assert!(find_signature_node(&doc).is_none());
762    }
763
764    #[test]
765    fn find_signature_ignores_wrong_namespace() {
766        let xml = r#"<root><Signature xmlns="http://example.com/fake"/></root>"#;
767        let doc = Document::parse(xml).unwrap();
768        assert!(find_signature_node(&doc).is_none());
769    }
770
771    // ── parse_key_info: dispatch parsing ──────────────────────────────
772
773    #[test]
774    fn parse_key_info_dispatches_supported_children() {
775        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
776                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
777            <KeyName>idp-signing-key</KeyName>
778            <KeyValue>
779                <RSAKeyValue>
780                    <Modulus>AQAB</Modulus>
781                    <Exponent>AQAB</Exponent>
782                </RSAKeyValue>
783            </KeyValue>
784            <X509Data>
785                <X509Certificate>MIIB</X509Certificate>
786                <X509SubjectName>CN=Example</X509SubjectName>
787            </X509Data>
788            <dsig11:DEREncodedKeyValue>AQIDBA==</dsig11:DEREncodedKeyValue>
789        </KeyInfo>"#;
790        let doc = Document::parse(xml).unwrap();
791
792        let key_info = parse_key_info(doc.root_element()).unwrap();
793        assert_eq!(key_info.sources.len(), 4);
794
795        assert_eq!(
796            key_info.sources[0],
797            KeyInfoSource::KeyName("idp-signing-key".to_string())
798        );
799        assert_eq!(
800            key_info.sources[1],
801            KeyInfoSource::KeyValue(KeyValueInfo::RsaKeyValue)
802        );
803        assert_eq!(
804            key_info.sources[2],
805            KeyInfoSource::X509Data(X509DataInfo {
806                certificate_count: 1,
807                subject_name_count: 1,
808                issuer_serial_count: 0,
809                ski_count: 0,
810                crl_count: 0,
811                digest_count: 0,
812            })
813        );
814        assert_eq!(
815            key_info.sources[3],
816            KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])
817        );
818    }
819
820    #[test]
821    fn parse_key_info_ignores_unknown_children() {
822        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
823            <Foo>bar</Foo>
824            <KeyName>ok</KeyName>
825        </KeyInfo>"#;
826        let doc = Document::parse(xml).unwrap();
827
828        let key_info = parse_key_info(doc.root_element()).unwrap();
829        assert_eq!(key_info.sources, vec![KeyInfoSource::KeyName("ok".into())]);
830    }
831
832    #[test]
833    fn parse_key_info_keyvalue_requires_single_child() {
834        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
835            <KeyValue/>
836        </KeyInfo>"#;
837        let doc = Document::parse(xml).unwrap();
838
839        let err = parse_key_info(doc.root_element()).unwrap_err();
840        assert!(matches!(err, ParseError::InvalidStructure(_)));
841    }
842
843    #[test]
844    fn parse_key_info_accepts_empty_x509data() {
845        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
846            <X509Data/>
847        </KeyInfo>"#;
848        let doc = Document::parse(xml).unwrap();
849
850        let key_info = parse_key_info(doc.root_element()).unwrap();
851        assert_eq!(
852            key_info.sources,
853            vec![KeyInfoSource::X509Data(X509DataInfo::default())]
854        );
855    }
856
857    #[test]
858    fn parse_key_info_rejects_unknown_xmlsig_child_in_x509data() {
859        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
860            <X509Data>
861                <Foo/>
862            </X509Data>
863        </KeyInfo>"#;
864        let doc = Document::parse(xml).unwrap();
865
866        let err = parse_key_info(doc.root_element()).unwrap_err();
867        assert!(matches!(err, ParseError::InvalidStructure(_)));
868    }
869
870    #[test]
871    fn parse_key_info_rejects_unknown_xmlsig11_child_in_x509data() {
872        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
873                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
874            <X509Data>
875                <dsig11:Foo/>
876            </X509Data>
877        </KeyInfo>"#;
878        let doc = Document::parse(xml).unwrap();
879
880        let err = parse_key_info(doc.root_element()).unwrap_err();
881        assert!(matches!(err, ParseError::InvalidStructure(_)));
882    }
883
884    #[test]
885    fn parse_key_info_accepts_x509data_with_only_foreign_namespace_children() {
886        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
887                              xmlns:foo="urn:example:foo">
888            <X509Data>
889                <foo:Bar/>
890            </X509Data>
891        </KeyInfo>"#;
892        let doc = Document::parse(xml).unwrap();
893
894        let key_info = parse_key_info(doc.root_element()).unwrap();
895        assert_eq!(
896            key_info.sources,
897            vec![KeyInfoSource::X509Data(X509DataInfo::default())]
898        );
899    }
900
901    #[test]
902    fn parse_key_info_der_encoded_key_value_rejects_invalid_base64() {
903        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
904                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
905            <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
906        </KeyInfo>"#;
907        let doc = Document::parse(xml).unwrap();
908
909        let err = parse_key_info(doc.root_element()).unwrap_err();
910        assert!(matches!(err, ParseError::Base64(_)));
911    }
912
913    #[test]
914    fn parse_key_info_der_encoded_key_value_accepts_xml_whitespace() {
915        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
916                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
917            <dsig11:DEREncodedKeyValue>
918                AQID
919                BA==
920            </dsig11:DEREncodedKeyValue>
921        </KeyInfo>"#;
922        let doc = Document::parse(xml).unwrap();
923
924        let key_info = parse_key_info(doc.root_element()).unwrap();
925        assert_eq!(
926            key_info.sources,
927            vec![KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])]
928        );
929    }
930
931    #[test]
932    fn parse_key_info_dispatches_dsig11_ec_keyvalue() {
933        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
934                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
935            <KeyValue>
936                <dsig11:ECKeyValue/>
937            </KeyValue>
938        </KeyInfo>"#;
939        let doc = Document::parse(xml).unwrap();
940
941        let key_info = parse_key_info(doc.root_element()).unwrap();
942        assert_eq!(
943            key_info.sources,
944            vec![KeyInfoSource::KeyValue(KeyValueInfo::EcKeyValue)]
945        );
946    }
947
948    #[test]
949    fn parse_key_info_marks_ds_namespace_ec_keyvalue_as_unsupported() {
950        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
951            <KeyValue>
952                <ECKeyValue/>
953            </KeyValue>
954        </KeyInfo>"#;
955        let doc = Document::parse(xml).unwrap();
956
957        let key_info = parse_key_info(doc.root_element()).unwrap();
958        assert_eq!(
959            key_info.sources,
960            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
961                namespace: Some(XMLDSIG_NS.to_string()),
962                local_name: "ECKeyValue".into(),
963            })]
964        );
965    }
966
967    #[test]
968    fn parse_key_info_keeps_unsupported_keyvalue_child_as_marker() {
969        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
970            <KeyValue>
971                <DSAKeyValue/>
972            </KeyValue>
973        </KeyInfo>"#;
974        let doc = Document::parse(xml).unwrap();
975
976        let key_info = parse_key_info(doc.root_element()).unwrap();
977        assert_eq!(
978            key_info.sources,
979            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
980                namespace: Some(XMLDSIG_NS.to_string()),
981                local_name: "DSAKeyValue".into(),
982            })]
983        );
984    }
985
986    #[test]
987    fn parse_key_info_rejects_keyname_with_child_elements() {
988        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
989            <KeyName>ok<foo/></KeyName>
990        </KeyInfo>"#;
991        let doc = Document::parse(xml).unwrap();
992
993        let err = parse_key_info(doc.root_element()).unwrap_err();
994        assert!(matches!(err, ParseError::InvalidStructure(_)));
995    }
996
997    #[test]
998    fn parse_key_info_preserves_keyname_text_without_trimming() {
999        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1000            <KeyName>  signing key  </KeyName>
1001        </KeyInfo>"#;
1002        let doc = Document::parse(xml).unwrap();
1003
1004        let key_info = parse_key_info(doc.root_element()).unwrap();
1005        assert_eq!(
1006            key_info.sources,
1007            vec![KeyInfoSource::KeyName("  signing key  ".into())]
1008        );
1009    }
1010
1011    #[test]
1012    fn parse_key_info_rejects_oversized_keyname_text() {
1013        let oversized = "A".repeat(4097);
1014        let xml = format!(
1015            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><KeyName>{oversized}</KeyName></KeyInfo>"
1016        );
1017        let doc = Document::parse(&xml).unwrap();
1018
1019        let err = parse_key_info(doc.root_element()).unwrap_err();
1020        assert!(matches!(err, ParseError::InvalidStructure(_)));
1021    }
1022
1023    #[test]
1024    fn parse_key_info_rejects_non_whitespace_mixed_content() {
1025        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">oops<KeyName>k</KeyName></KeyInfo>"#;
1026        let doc = Document::parse(xml).unwrap();
1027
1028        let err = parse_key_info(doc.root_element()).unwrap_err();
1029        assert!(matches!(err, ParseError::InvalidStructure(_)));
1030    }
1031
1032    #[test]
1033    fn parse_key_info_rejects_nbsp_as_non_xml_whitespace_mixed_content() {
1034        let xml = "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\u{00A0}<KeyName>k</KeyName></KeyInfo>";
1035        let doc = Document::parse(xml).unwrap();
1036
1037        let err = parse_key_info(doc.root_element()).unwrap_err();
1038        assert!(matches!(err, ParseError::InvalidStructure(_)));
1039    }
1040
1041    #[test]
1042    fn parse_key_info_der_encoded_key_value_rejects_oversized_payload() {
1043        let oversized =
1044            base64::engine::general_purpose::STANDARD
1045                .encode(vec![0u8; MAX_DER_ENCODED_KEY_VALUE_LEN + 1]);
1046        let xml = format!(
1047            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:dsig11=\"http://www.w3.org/2009/xmldsig11#\"><dsig11:DEREncodedKeyValue>{oversized}</dsig11:DEREncodedKeyValue></KeyInfo>"
1048        );
1049        let doc = Document::parse(&xml).unwrap();
1050
1051        let err = parse_key_info(doc.root_element()).unwrap_err();
1052        assert!(matches!(err, ParseError::InvalidStructure(_)));
1053    }
1054
1055    #[test]
1056    fn parse_key_info_der_encoded_key_value_rejects_empty_payload() {
1057        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
1058                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
1059            <dsig11:DEREncodedKeyValue>
1060                
1061            </dsig11:DEREncodedKeyValue>
1062        </KeyInfo>"#;
1063        let doc = Document::parse(xml).unwrap();
1064
1065        let err = parse_key_info(doc.root_element()).unwrap_err();
1066        assert!(matches!(err, ParseError::InvalidStructure(_)));
1067    }
1068
1069    #[test]
1070    fn parse_key_info_der_encoded_key_value_non_xml_ascii_whitespace_is_not_parseable_xml() {
1071        let xml = "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:dsig11=\"http://www.w3.org/2009/xmldsig11#\"><dsig11:DEREncodedKeyValue>\u{000C}</dsig11:DEREncodedKeyValue></KeyInfo>";
1072        assert!(Document::parse(xml).is_err());
1073    }
1074
1075    // ── parse_signed_info: happy path ────────────────────────────────
1076
1077    #[test]
1078    fn parse_signed_info_rsa_sha256_with_reference() {
1079        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1080            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1081            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1082            <Reference URI="">
1083                <Transforms>
1084                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
1085                    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1086                </Transforms>
1087                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1088                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
1089            </Reference>
1090        </SignedInfo>"#;
1091        let doc = Document::parse(xml).unwrap();
1092        let si = parse_signed_info(doc.root_element()).unwrap();
1093
1094        assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
1095        assert_eq!(si.references.len(), 1);
1096
1097        let r = &si.references[0];
1098        assert_eq!(r.uri.as_deref(), Some(""));
1099        assert_eq!(r.digest_method, DigestAlgorithm::Sha256);
1100        assert_eq!(r.digest_value, vec![0u8; 32]);
1101        assert_eq!(r.transforms.len(), 2);
1102    }
1103
1104    #[test]
1105    fn parse_signed_info_multiple_references() {
1106        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1107            <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
1108            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
1109            <Reference URI="#a">
1110                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1111                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
1112            </Reference>
1113            <Reference URI="#b">
1114                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1115                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
1116            </Reference>
1117        </SignedInfo>"##;
1118        let doc = Document::parse(xml).unwrap();
1119        let si = parse_signed_info(doc.root_element()).unwrap();
1120
1121        assert_eq!(si.signature_method, SignatureAlgorithm::EcdsaP256Sha256);
1122        assert_eq!(si.references.len(), 2);
1123        assert_eq!(si.references[0].uri.as_deref(), Some("#a"));
1124        assert_eq!(si.references[0].digest_method, DigestAlgorithm::Sha256);
1125        assert_eq!(si.references[1].uri.as_deref(), Some("#b"));
1126        assert_eq!(si.references[1].digest_method, DigestAlgorithm::Sha1);
1127    }
1128
1129    #[test]
1130    fn parse_reference_without_transforms() {
1131        // Transforms element is optional
1132        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1133            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1134            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1135            <Reference URI="#obj">
1136                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1137                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
1138            </Reference>
1139        </SignedInfo>"##;
1140        let doc = Document::parse(xml).unwrap();
1141        let si = parse_signed_info(doc.root_element()).unwrap();
1142
1143        assert!(si.references[0].transforms.is_empty());
1144    }
1145
1146    #[test]
1147    fn parse_reference_with_all_attributes() {
1148        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1149            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1150            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1151            <Reference URI="#data" Id="ref1" Type="http://www.w3.org/2000/09/xmldsig#Object">
1152                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1153                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
1154            </Reference>
1155        </SignedInfo>"##;
1156        let doc = Document::parse(xml).unwrap();
1157        let si = parse_signed_info(doc.root_element()).unwrap();
1158        let r = &si.references[0];
1159
1160        assert_eq!(r.uri.as_deref(), Some("#data"));
1161        assert_eq!(r.id.as_deref(), Some("ref1"));
1162        assert_eq!(
1163            r.ref_type.as_deref(),
1164            Some("http://www.w3.org/2000/09/xmldsig#Object")
1165        );
1166    }
1167
1168    #[test]
1169    fn parse_reference_absent_uri() {
1170        // URI attribute is optional per spec
1171        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1172            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1173            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1174            <Reference>
1175                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1176                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
1177            </Reference>
1178        </SignedInfo>"#;
1179        let doc = Document::parse(xml).unwrap();
1180        let si = parse_signed_info(doc.root_element()).unwrap();
1181        assert!(si.references[0].uri.is_none());
1182    }
1183
1184    #[test]
1185    fn parse_signed_info_preserves_inclusive_prefixes() {
1186        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
1187                                 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
1188            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
1189                <ec:InclusiveNamespaces PrefixList="ds saml #default"/>
1190            </CanonicalizationMethod>
1191            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1192            <Reference URI="">
1193                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1194                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
1195            </Reference>
1196        </SignedInfo>"#;
1197        let doc = Document::parse(xml).unwrap();
1198
1199        let si = parse_signed_info(doc.root_element()).unwrap();
1200        assert!(si.c14n_method.inclusive_prefixes().contains("ds"));
1201        assert!(si.c14n_method.inclusive_prefixes().contains("saml"));
1202        assert!(si.c14n_method.inclusive_prefixes().contains(""));
1203    }
1204
1205    // ── parse_signed_info: error cases ───────────────────────────────
1206
1207    #[test]
1208    fn missing_canonicalization_method() {
1209        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1210            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1211            <Reference URI="">
1212                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1213                <DigestValue>dGVzdA==</DigestValue>
1214            </Reference>
1215        </SignedInfo>"#;
1216        let doc = Document::parse(xml).unwrap();
1217        let result = parse_signed_info(doc.root_element());
1218        assert!(result.is_err());
1219        // SignatureMethod is first child but expected CanonicalizationMethod
1220        assert!(matches!(
1221            result.unwrap_err(),
1222            ParseError::InvalidStructure(_)
1223        ));
1224    }
1225
1226    #[test]
1227    fn missing_signature_method() {
1228        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1229            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1230            <Reference URI="">
1231                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1232                <DigestValue>dGVzdA==</DigestValue>
1233            </Reference>
1234        </SignedInfo>"#;
1235        let doc = Document::parse(xml).unwrap();
1236        let result = parse_signed_info(doc.root_element());
1237        assert!(result.is_err());
1238        // Reference is second child but expected SignatureMethod
1239        assert!(matches!(
1240            result.unwrap_err(),
1241            ParseError::InvalidStructure(_)
1242        ));
1243    }
1244
1245    #[test]
1246    fn no_references() {
1247        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1248            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1249            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1250        </SignedInfo>"#;
1251        let doc = Document::parse(xml).unwrap();
1252        let result = parse_signed_info(doc.root_element());
1253        assert!(matches!(
1254            result.unwrap_err(),
1255            ParseError::MissingElement {
1256                element: "Reference"
1257            }
1258        ));
1259    }
1260
1261    #[test]
1262    fn unsupported_c14n_algorithm() {
1263        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1264            <CanonicalizationMethod Algorithm="http://example.com/bogus-c14n"/>
1265            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1266            <Reference URI="">
1267                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1268                <DigestValue>dGVzdA==</DigestValue>
1269            </Reference>
1270        </SignedInfo>"#;
1271        let doc = Document::parse(xml).unwrap();
1272        let result = parse_signed_info(doc.root_element());
1273        assert!(matches!(
1274            result.unwrap_err(),
1275            ParseError::UnsupportedAlgorithm { .. }
1276        ));
1277    }
1278
1279    #[test]
1280    fn unsupported_signature_algorithm() {
1281        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1282            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1283            <SignatureMethod Algorithm="http://example.com/bogus-sign"/>
1284            <Reference URI="">
1285                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1286                <DigestValue>dGVzdA==</DigestValue>
1287            </Reference>
1288        </SignedInfo>"#;
1289        let doc = Document::parse(xml).unwrap();
1290        let result = parse_signed_info(doc.root_element());
1291        assert!(matches!(
1292            result.unwrap_err(),
1293            ParseError::UnsupportedAlgorithm { .. }
1294        ));
1295    }
1296
1297    #[test]
1298    fn unsupported_digest_algorithm() {
1299        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1300            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1301            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1302            <Reference URI="">
1303                <DigestMethod Algorithm="http://example.com/bogus-digest"/>
1304                <DigestValue>dGVzdA==</DigestValue>
1305            </Reference>
1306        </SignedInfo>"#;
1307        let doc = Document::parse(xml).unwrap();
1308        let result = parse_signed_info(doc.root_element());
1309        assert!(matches!(
1310            result.unwrap_err(),
1311            ParseError::UnsupportedAlgorithm { .. }
1312        ));
1313    }
1314
1315    #[test]
1316    fn missing_digest_method() {
1317        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1318            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1319            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1320            <Reference URI="">
1321                <DigestValue>dGVzdA==</DigestValue>
1322            </Reference>
1323        </SignedInfo>"#;
1324        let doc = Document::parse(xml).unwrap();
1325        let result = parse_signed_info(doc.root_element());
1326        // DigestValue is not DigestMethod
1327        assert!(result.is_err());
1328    }
1329
1330    #[test]
1331    fn missing_digest_value() {
1332        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1333            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1334            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1335            <Reference URI="">
1336                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1337            </Reference>
1338        </SignedInfo>"#;
1339        let doc = Document::parse(xml).unwrap();
1340        let result = parse_signed_info(doc.root_element());
1341        assert!(matches!(
1342            result.unwrap_err(),
1343            ParseError::MissingElement {
1344                element: "DigestValue"
1345            }
1346        ));
1347    }
1348
1349    #[test]
1350    fn invalid_base64_digest_value() {
1351        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1352            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1353            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1354            <Reference URI="">
1355                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1356                <DigestValue>!!!not-base64!!!</DigestValue>
1357            </Reference>
1358        </SignedInfo>"#;
1359        let doc = Document::parse(xml).unwrap();
1360        let result = parse_signed_info(doc.root_element());
1361        assert!(matches!(result.unwrap_err(), ParseError::Base64(_)));
1362    }
1363
1364    #[test]
1365    fn digest_value_length_must_match_digest_method() {
1366        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1367            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1368            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1369            <Reference URI="">
1370                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1371                <DigestValue>dGVzdA==</DigestValue>
1372            </Reference>
1373        </SignedInfo>"#;
1374        let doc = Document::parse(xml).unwrap();
1375
1376        let result = parse_signed_info(doc.root_element());
1377        assert!(matches!(
1378            result.unwrap_err(),
1379            ParseError::DigestLengthMismatch {
1380                algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
1381                expected: 32,
1382                actual: 4,
1383            }
1384        ));
1385    }
1386
1387    #[test]
1388    fn inclusive_prefixes_on_inclusive_c14n_is_rejected() {
1389        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
1390                                 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
1391            <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">
1392                <ec:InclusiveNamespaces PrefixList="ds"/>
1393            </CanonicalizationMethod>
1394            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1395            <Reference URI="">
1396                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1397                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
1398            </Reference>
1399        </SignedInfo>"#;
1400        let doc = Document::parse(xml).unwrap();
1401
1402        let result = parse_signed_info(doc.root_element());
1403        assert!(matches!(
1404            result.unwrap_err(),
1405            ParseError::UnsupportedAlgorithm { .. }
1406        ));
1407    }
1408
1409    #[test]
1410    fn extra_element_after_digest_value() {
1411        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1412            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1413            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1414            <Reference URI="">
1415                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1416                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
1417                <Unexpected/>
1418            </Reference>
1419        </SignedInfo>"#;
1420        let doc = Document::parse(xml).unwrap();
1421        let result = parse_signed_info(doc.root_element());
1422        assert!(matches!(
1423            result.unwrap_err(),
1424            ParseError::InvalidStructure(_)
1425        ));
1426    }
1427
1428    #[test]
1429    fn digest_value_with_element_child_is_rejected() {
1430        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1431            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1432            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1433            <Reference URI="">
1434                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1435                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=<Junk/>AAAA</DigestValue>
1436            </Reference>
1437        </SignedInfo>"#;
1438        let doc = Document::parse(xml).unwrap();
1439
1440        let result = parse_signed_info(doc.root_element());
1441        assert!(matches!(
1442            result.unwrap_err(),
1443            ParseError::InvalidStructure(_)
1444        ));
1445    }
1446
1447    #[test]
1448    fn wrong_namespace_on_signed_info() {
1449        let xml = r#"<SignedInfo xmlns="http://example.com/fake">
1450            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1451        </SignedInfo>"#;
1452        let doc = Document::parse(xml).unwrap();
1453        let result = parse_signed_info(doc.root_element());
1454        assert!(matches!(
1455            result.unwrap_err(),
1456            ParseError::InvalidStructure(_)
1457        ));
1458    }
1459
1460    // ── Whitespace-wrapped base64 ────────────────────────────────────
1461
1462    #[test]
1463    fn base64_with_whitespace() {
1464        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1465            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1466            <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
1467            <Reference URI="">
1468                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
1469                <DigestValue>
1470                    AAAAAAAA
1471                    AAAAAAAAAAAAAAAAAAA=
1472                </DigestValue>
1473            </Reference>
1474        </SignedInfo>"#;
1475        let doc = Document::parse(xml).unwrap();
1476        let si = parse_signed_info(doc.root_element()).unwrap();
1477        assert_eq!(si.references[0].digest_value, vec![0u8; 20]);
1478    }
1479
1480    #[test]
1481    fn base64_decode_digest_accepts_xml_whitespace_chars() {
1482        let digest =
1483            base64_decode_digest("AAAA\tAAAA\rAAAA\nAAAA AAAAAAAAAAA=", DigestAlgorithm::Sha1)
1484                .expect("XML whitespace in DigestValue must be accepted");
1485        assert_eq!(digest, vec![0u8; 20]);
1486    }
1487
1488    #[test]
1489    fn base64_decode_digest_rejects_non_xml_ascii_whitespace() {
1490        let err = base64_decode_digest(
1491            "AAAA\u{000C}AAAAAAAAAAAAAAAAAAAAAAA=",
1492            DigestAlgorithm::Sha1,
1493        )
1494        .expect_err("form-feed/vertical-tab in DigestValue must be rejected");
1495        assert!(matches!(err, ParseError::Base64(_)));
1496    }
1497
1498    #[test]
1499    fn base64_decode_digest_rejects_oversized_base64_before_decode() {
1500        let err = base64_decode_digest("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", DigestAlgorithm::Sha1)
1501            .expect_err("oversized DigestValue base64 must fail before decode");
1502        match err {
1503            ParseError::Base64(message) => {
1504                assert!(
1505                    message.contains("DigestValue exceeds maximum allowed base64 length"),
1506                    "unexpected message: {message}"
1507                );
1508            }
1509            other => panic!("expected ParseError::Base64, got {other:?}"),
1510        }
1511    }
1512
1513    // ── Real-world SAML structure ────────────────────────────────────
1514
1515    #[test]
1516    fn saml_response_signed_info() {
1517        let xml = r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1518            <ds:SignedInfo>
1519                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1520                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
1521                <ds:Reference URI="#_resp1">
1522                    <ds:Transforms>
1523                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
1524                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
1525                    </ds:Transforms>
1526                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
1527                    <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
1528                </ds:Reference>
1529            </ds:SignedInfo>
1530            <ds:SignatureValue>ZmFrZQ==</ds:SignatureValue>
1531        </ds:Signature>"##;
1532        let doc = Document::parse(xml).unwrap();
1533
1534        // Find SignedInfo within Signature
1535        let sig_node = doc.root_element();
1536        let signed_info_node = sig_node
1537            .children()
1538            .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
1539            .unwrap();
1540
1541        let si = parse_signed_info(signed_info_node).unwrap();
1542        assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
1543        assert_eq!(si.references.len(), 1);
1544        assert_eq!(si.references[0].uri.as_deref(), Some("#_resp1"));
1545        assert_eq!(si.references[0].transforms.len(), 2);
1546        assert_eq!(si.references[0].digest_value, vec![0u8; 32]);
1547    }
1548}