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