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};
20use x509_parser::extensions::ParsedExtension;
21use x509_parser::prelude::FromDer;
22use x509_parser::public_key::PublicKey;
23
24use super::digest::{DigestAlgorithm, compute_digest, constant_time_eq};
25use super::transforms::{self, Transform};
26use super::whitespace::{
27    XmlBase64NormalizeLimitedError, is_xml_whitespace_only, normalize_xml_base64_text,
28    normalize_xml_base64_text_with_limit,
29};
30use crate::c14n::C14nAlgorithm;
31
32/// XMLDSig namespace URI.
33pub(crate) const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
34/// XMLDSig 1.1 namespace URI.
35pub(crate) const XMLDSIG11_NS: &str = "http://www.w3.org/2009/xmldsig11#";
36const MAX_DER_ENCODED_KEY_VALUE_LEN: usize = 8192;
37const MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN: usize = 65_536;
38const MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN: usize = MAX_DER_ENCODED_KEY_VALUE_LEN.div_ceil(3) * 4;
39const MAX_KEY_NAME_TEXT_LEN: usize = 4096;
40const MAX_RSA_MODULUS_LEN: usize = 1024;
41const MAX_RSA_EXPONENT_LEN: usize = 8;
42pub(crate) const EC_P256_OID: &str = "1.2.840.10045.3.1.7";
43pub(crate) const EC_P384_OID: &str = "1.3.132.0.34";
44const MAX_EC_PUBLIC_KEY_LEN: usize = 97;
45const MAX_X509_BASE64_TEXT_LEN: usize = 262_144;
46const MAX_X509_BASE64_NORMALIZED_LEN: usize = MAX_X509_BASE64_TEXT_LEN;
47const MAX_X509_DECODED_BINARY_LEN: usize = MAX_X509_BASE64_NORMALIZED_LEN.div_ceil(4) * 3;
48const MAX_X509_SUBJECT_NAME_TEXT_LEN: usize = 16_384;
49const MAX_X509_ISSUER_NAME_TEXT_LEN: usize = 16_384;
50const MAX_X509_SERIAL_NUMBER_TEXT_LEN: usize = 4096;
51const MAX_X509_DATA_ENTRY_COUNT: usize = 64;
52const MAX_X509_DATA_TOTAL_BINARY_LEN: usize = 1_048_576;
53const MAX_X509_CHAIN_DEPTH: usize = 9;
54pub(crate) const MAX_REFERENCES_PER_SIGNATURE: usize = 64;
55
56/// Signature algorithms supported for signing and verification.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58pub enum SignatureAlgorithm {
59    /// RSA with SHA-1. **Verify-only** — signing disabled.
60    RsaSha1,
61    /// RSA with SHA-256 (most common in SAML).
62    RsaSha256,
63    /// RSA with SHA-384.
64    RsaSha384,
65    /// RSA with SHA-512.
66    RsaSha512,
67    /// ECDSA P-256 with SHA-256.
68    EcdsaP256Sha256,
69    /// XMLDSig `ecdsa-sha384` URI.
70    ///
71    /// The variant name is historical.
72    ///
73    /// Verification currently accepts this XMLDSig URI for P-384 and for the
74    /// donor P-521 interop case.
75    EcdsaP384Sha384,
76}
77
78impl SignatureAlgorithm {
79    /// Parse from an XML algorithm URI.
80    #[must_use]
81    pub fn from_uri(uri: &str) -> Option<Self> {
82        match uri {
83            "http://www.w3.org/2000/09/xmldsig#rsa-sha1" => Some(Self::RsaSha1),
84            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => Some(Self::RsaSha256),
85            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" => Some(Self::RsaSha384),
86            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" => Some(Self::RsaSha512),
87            "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => Some(Self::EcdsaP256Sha256),
88            "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384" => Some(Self::EcdsaP384Sha384),
89            _ => None,
90        }
91    }
92
93    /// Return the XML namespace URI.
94    #[must_use]
95    pub fn uri(self) -> &'static str {
96        match self {
97            Self::RsaSha1 => "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
98            Self::RsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
99            Self::RsaSha384 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
100            Self::RsaSha512 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
101            Self::EcdsaP256Sha256 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
102            Self::EcdsaP384Sha384 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
103        }
104    }
105
106    /// Whether this algorithm is allowed for signing (not just verification).
107    #[must_use]
108    pub fn signing_allowed(self) -> bool {
109        !matches!(self, Self::RsaSha1)
110    }
111}
112
113/// Parsed `<SignedInfo>` element.
114#[derive(Debug)]
115pub struct SignedInfo {
116    /// Canonicalization method for SignedInfo itself.
117    pub c14n_method: C14nAlgorithm,
118    /// Signature algorithm.
119    pub signature_method: SignatureAlgorithm,
120    /// One or more `<Reference>` elements.
121    pub references: Vec<Reference>,
122}
123
124/// Parsed `<Reference>` element.
125#[derive(Debug)]
126pub struct Reference {
127    /// URI attribute (e.g., `""`, `"#_assert1"`).
128    pub uri: Option<String>,
129    /// Id attribute.
130    pub id: Option<String>,
131    /// Type attribute.
132    pub ref_type: Option<String>,
133    /// Transform chain.
134    pub transforms: Vec<Transform>,
135    /// Digest algorithm.
136    pub digest_method: DigestAlgorithm,
137    /// Raw digest value (base64-decoded).
138    pub digest_value: Vec<u8>,
139}
140
141/// Parsed `<KeyInfo>` element.
142#[derive(Debug, Default, Clone, PartialEq, Eq)]
143#[non_exhaustive]
144pub struct KeyInfo {
145    /// Sources discovered under `<KeyInfo>` in document order.
146    pub sources: Vec<KeyInfoSource>,
147}
148
149/// Top-level key material source parsed from `<KeyInfo>`.
150#[derive(Debug, Clone, PartialEq, Eq)]
151#[non_exhaustive]
152pub enum KeyInfoSource {
153    /// `<KeyName>` source.
154    KeyName(String),
155    /// `<KeyValue>` source.
156    KeyValue(KeyValueInfo),
157    /// `<X509Data>` source.
158    X509Data(X509DataInfo),
159    /// `dsig11:DEREncodedKeyValue` source (base64-decoded DER bytes).
160    DerEncodedKeyValue(Vec<u8>),
161}
162
163/// Parsed `<KeyValue>` dispatch result.
164#[derive(Debug, Clone, PartialEq, Eq)]
165#[non_exhaustive]
166pub enum KeyValueInfo {
167    /// `<RSAKeyValue>` with unsigned big-endian CryptoBinary parameters.
168    Rsa {
169        /// RSA modulus.
170        modulus: Vec<u8>,
171        /// RSA public exponent.
172        exponent: Vec<u8>,
173    },
174    /// `dsig11:ECKeyValue` with a supported named curve and SEC1 public point.
175    Ec {
176        /// Bare named-curve OID, without the XMLDSig `urn:oid:` prefix.
177        curve_oid: String,
178        /// Uncompressed SEC1 point (`0x04 || x || y`).
179        public_key: Vec<u8>,
180    },
181    /// `dsig11:ECKeyValue` with unusable curve or point data.
182    InvalidEcKeyValue,
183    /// Any other `<KeyValue>` child not yet supported by this phase.
184    Unsupported {
185        /// Namespace URI of the unsupported child, when present.
186        namespace: Option<String>,
187        /// Local name of the unsupported child element.
188        local_name: String,
189    },
190}
191
192/// Parsed `<X509Data>` children.
193#[derive(Debug, Default, Clone, PartialEq, Eq)]
194#[non_exhaustive]
195pub struct X509DataInfo {
196    /// DER-encoded certificates from `<X509Certificate>`.
197    ///
198    /// This vector has a 1:1 index correspondence with `parsed_certificates`.
199    pub certificates: Vec<Vec<u8>>,
200    /// Text values from `<X509SubjectName>`.
201    pub subject_names: Vec<String>,
202    /// `(IssuerName, SerialNumber)` tuples from `<X509IssuerSerial>`.
203    pub issuer_serials: Vec<(String, String)>,
204    /// Raw bytes from `<X509SKI>`.
205    pub skis: Vec<Vec<u8>>,
206    /// DER-encoded CRLs from `<X509CRL>`.
207    pub crls: Vec<Vec<u8>>,
208    /// `(Algorithm URI, digest bytes)` tuples from `dsig11:X509Digest`.
209    pub digests: Vec<(String, Vec<u8>)>,
210    /// Parsed metadata for each `<X509Certificate>` entry.
211    ///
212    /// This vector has a 1:1 index correspondence with `certificates`.
213    pub parsed_certificates: Vec<ParsedX509Certificate>,
214    /// Ordered certificate indexes, starting with the signing certificate.
215    pub certificate_chain: Vec<usize>,
216}
217
218/// Parsed X.509 certificate details extracted from DER.
219#[derive(Debug, Clone, PartialEq, Eq)]
220#[non_exhaustive]
221pub struct ParsedX509Certificate {
222    /// Subject distinguished name.
223    pub subject_dn: String,
224    /// Issuer distinguished name.
225    pub issuer_dn: String,
226    /// Certificate serial number bytes.
227    pub serial_number: Vec<u8>,
228    /// Uppercase hexadecimal certificate serial number without separators.
229    pub serial_number_hex: String,
230    /// Subject Key Identifier extension bytes (if present).
231    pub subject_key_identifier: Option<Vec<u8>>,
232    /// Parsed certificate public key material.
233    pub public_key: X509PublicKeyInfo,
234}
235
236/// Public key material extracted from certificate SubjectPublicKeyInfo.
237#[derive(Debug, Clone, PartialEq, Eq)]
238#[non_exhaustive]
239pub enum X509PublicKeyInfo {
240    /// RSA public key (`modulus`, `exponent`).
241    Rsa {
242        /// Unsigned big-endian RSA modulus (`n`), normalized without leading zeroes.
243        modulus: Vec<u8>,
244        /// Unsigned big-endian RSA public exponent (`e`), normalized without leading zeroes.
245        exponent: Vec<u8>,
246    },
247    /// EC public key (`curve_oid`, encoded point bytes).
248    Ec {
249        /// Named-curve OID from SubjectPublicKeyInfo parameters.
250        curve_oid: String,
251        /// Encoded EC point bytes from SubjectPublicKeyInfo.
252        public_key: Vec<u8>,
253    },
254    /// Public key algorithm is present but not parsed into a concrete key type.
255    Unsupported {
256        /// SubjectPublicKeyInfo algorithm OID.
257        algorithm_oid: String,
258    },
259}
260
261/// Errors during XMLDSig element parsing.
262#[derive(Debug, thiserror::Error)]
263#[non_exhaustive]
264pub enum ParseError {
265    /// Missing required element.
266    #[error("missing required element: <{element}>")]
267    MissingElement {
268        /// Name of the missing element.
269        element: &'static str,
270    },
271
272    /// Invalid structure (wrong child order, unexpected element, etc.).
273    #[error("invalid structure: {0}")]
274    InvalidStructure(String),
275
276    /// `<SignedInfo>` declared more references than one signature may process.
277    #[error("SignedInfo contains more than {max} Reference elements")]
278    TooManyReferences {
279        /// Maximum references accepted for one signature.
280        max: usize,
281    },
282
283    /// Unsupported algorithm URI.
284    #[error("unsupported algorithm: {uri}")]
285    UnsupportedAlgorithm {
286        /// The unrecognized algorithm URI.
287        uri: String,
288    },
289
290    /// Base64 decode error.
291    #[error("base64 decode error: {0}")]
292    Base64(String),
293
294    /// DigestValue length did not match the declared DigestMethod.
295    #[error(
296        "digest length mismatch for {algorithm}: expected {expected} bytes, got {actual} bytes"
297    )]
298    DigestLengthMismatch {
299        /// Digest algorithm URI/name used for diagnostics.
300        algorithm: &'static str,
301        /// Expected decoded digest length in bytes.
302        expected: usize,
303        /// Actual decoded digest length in bytes.
304        actual: usize,
305    },
306
307    /// Transform parsing error.
308    #[error("transform error: {0}")]
309    Transform(#[from] super::types::TransformError),
310}
311
312/// Find the first `<ds:Signature>` element in the document.
313#[must_use]
314pub fn find_signature_node<'a>(doc: &'a Document<'a>) -> Option<Node<'a, 'a>> {
315    doc.descendants().find(|n| {
316        n.is_element()
317            && n.tag_name().name() == "Signature"
318            && n.tag_name().namespace() == Some(XMLDSIG_NS)
319    })
320}
321
322/// Parse a `<ds:SignedInfo>` element.
323///
324/// Enforces strict child order per XMLDSig spec:
325/// `<CanonicalizationMethod>` → `<SignatureMethod>` → `<Reference>`+
326pub fn parse_signed_info(signed_info_node: Node) -> Result<SignedInfo, ParseError> {
327    parse_signed_info_with_xpath_budget(
328        signed_info_node,
329        &mut transforms::XPathSignatureParseBudget::default(),
330    )
331}
332
333pub(crate) fn parse_signed_info_with_xpath_budget(
334    signed_info_node: Node,
335    xpath_budget: &mut transforms::XPathSignatureParseBudget,
336) -> Result<SignedInfo, ParseError> {
337    verify_ds_element(signed_info_node, "SignedInfo")?;
338
339    let mut children = element_children(signed_info_node);
340
341    // 1. CanonicalizationMethod (required, first)
342    let c14n_node = children.next().ok_or(ParseError::MissingElement {
343        element: "CanonicalizationMethod",
344    })?;
345    verify_ds_element(c14n_node, "CanonicalizationMethod")?;
346    let c14n_uri = required_algorithm_attr(c14n_node, "CanonicalizationMethod")?;
347    let mut c14n_method =
348        C14nAlgorithm::from_uri(c14n_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
349            uri: c14n_uri.to_string(),
350        })?;
351    if let Some(prefix_list) = parse_inclusive_prefixes(c14n_node)? {
352        if c14n_method.mode() == crate::c14n::C14nMode::Exclusive1_0 {
353            c14n_method = c14n_method.with_prefix_list(&prefix_list);
354        } else {
355            return Err(ParseError::UnsupportedAlgorithm {
356                uri: c14n_uri.to_string(),
357            });
358        }
359    }
360
361    // 2. SignatureMethod (required, second)
362    let sig_method_node = children.next().ok_or(ParseError::MissingElement {
363        element: "SignatureMethod",
364    })?;
365    verify_ds_element(sig_method_node, "SignatureMethod")?;
366    let sig_uri = required_algorithm_attr(sig_method_node, "SignatureMethod")?;
367    let signature_method =
368        SignatureAlgorithm::from_uri(sig_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
369            uri: sig_uri.to_string(),
370        })?;
371
372    // 3. One or more Reference elements
373    let mut references = Vec::new();
374    for child in children {
375        verify_ds_element(child, "Reference")?;
376        if references.len() == MAX_REFERENCES_PER_SIGNATURE {
377            return Err(ParseError::TooManyReferences {
378                max: MAX_REFERENCES_PER_SIGNATURE,
379            });
380        }
381        references.push(parse_reference_with_xpath_budget(child, xpath_budget)?);
382    }
383    if references.is_empty() {
384        return Err(ParseError::MissingElement {
385            element: "Reference",
386        });
387    }
388
389    Ok(SignedInfo {
390        c14n_method,
391        signature_method,
392        references,
393    })
394}
395
396/// Parse a single `<ds:Reference>` element.
397///
398/// Structure: `<Transforms>?` → `<DigestMethod>` → `<DigestValue>`
399pub fn parse_reference(reference_node: Node) -> Result<Reference, ParseError> {
400    parse_reference_with_xpath_budget(
401        reference_node,
402        &mut transforms::XPathSignatureParseBudget::default(),
403    )
404}
405
406pub(crate) fn parse_reference_with_xpath_budget(
407    reference_node: Node,
408    xpath_budget: &mut transforms::XPathSignatureParseBudget,
409) -> Result<Reference, ParseError> {
410    verify_ds_element(reference_node, "Reference")?;
411    ensure_no_non_whitespace_text(reference_node, "Reference")?;
412    let uri = reference_node.attribute("URI").map(String::from);
413    let id = reference_node.attribute("Id").map(String::from);
414    let ref_type = reference_node.attribute("Type").map(String::from);
415
416    let mut children = element_children(reference_node);
417
418    // Optional <Transforms>
419    let mut transforms = Vec::new();
420    let mut next = children.next().ok_or(ParseError::MissingElement {
421        element: "DigestMethod",
422    })?;
423
424    if next.tag_name().name() == "Transforms" && next.tag_name().namespace() == Some(XMLDSIG_NS) {
425        transforms = transforms::parse_transforms_with_budget(next, xpath_budget)?;
426        next = children.next().ok_or(ParseError::MissingElement {
427            element: "DigestMethod",
428        })?;
429    }
430
431    // Required <DigestMethod>
432    verify_ds_element(next, "DigestMethod")?;
433    let digest_uri = required_algorithm_attr(next, "DigestMethod")?;
434    let digest_method =
435        DigestAlgorithm::from_uri(digest_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
436            uri: digest_uri.to_string(),
437        })?;
438
439    // Required <DigestValue>
440    let digest_value_node = children.next().ok_or(ParseError::MissingElement {
441        element: "DigestValue",
442    })?;
443    verify_ds_element(digest_value_node, "DigestValue")?;
444    let digest_value = decode_digest_value_children(digest_value_node, digest_method)?;
445
446    // No more children expected
447    if let Some(unexpected) = children.next() {
448        return Err(ParseError::InvalidStructure(format!(
449            "unexpected element <{}> after <DigestValue> in <Reference>",
450            unexpected.tag_name().name()
451        )));
452    }
453
454    Ok(Reference {
455        uri,
456        id,
457        ref_type,
458        transforms,
459        digest_method,
460        digest_value,
461    })
462}
463
464/// Parse `<ds:KeyInfo>` and dispatch supported child sources.
465///
466/// Supported source elements:
467/// - `<ds:KeyName>`
468/// - `<ds:KeyValue>` (dispatch by child QName; RSA and `dsig11:ECKeyValue` are parsed)
469/// - `<ds:X509Data>`
470/// - `<dsig11:DEREncodedKeyValue>`
471///
472/// Unknown top-level `<KeyInfo>` children are ignored (lax processing), while
473/// unknown XMLDSig-owned (`ds:*` / `dsig11:*`) children inside `<X509Data>` are
474/// rejected fail-closed.
475/// `<X509Data>` may still be empty or contain only non-XMLDSig extension children.
476pub fn parse_key_info(key_info_node: Node) -> Result<KeyInfo, ParseError> {
477    verify_ds_element(key_info_node, "KeyInfo")?;
478    ensure_no_non_whitespace_text(key_info_node, "KeyInfo")?;
479
480    let mut sources = Vec::new();
481    for child in element_children(key_info_node) {
482        match (child.tag_name().namespace(), child.tag_name().name()) {
483            (Some(XMLDSIG_NS), "KeyName") => {
484                ensure_no_element_children(child, "KeyName")?;
485                let key_name =
486                    collect_text_content_bounded(child, MAX_KEY_NAME_TEXT_LEN, "KeyName")?;
487                sources.push(KeyInfoSource::KeyName(key_name));
488            }
489            (Some(XMLDSIG_NS), "KeyValue") => {
490                let key_value = parse_key_value_dispatch(child)?;
491                sources.push(KeyInfoSource::KeyValue(key_value));
492            }
493            (Some(XMLDSIG_NS), "X509Data") => {
494                let x509 = parse_x509_data_dispatch(child)?;
495                sources.push(KeyInfoSource::X509Data(x509));
496            }
497            (Some(XMLDSIG11_NS), "DEREncodedKeyValue") => {
498                ensure_no_element_children(child, "DEREncodedKeyValue")?;
499                let der = decode_der_encoded_key_value_base64(child)?;
500                sources.push(KeyInfoSource::DerEncodedKeyValue(der));
501            }
502            _ => {}
503        }
504    }
505
506    Ok(KeyInfo { sources })
507}
508
509// ── Helpers ──────────────────────────────────────────────────────────────────
510
511/// Iterate only element children (skip text, comments, PIs).
512fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
513    node.children().filter(|n| n.is_element())
514}
515
516/// Verify that a node is a `<ds:{expected_name}>` element.
517fn verify_ds_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
518    if !node.is_element() {
519        return Err(ParseError::InvalidStructure(format!(
520            "expected element <{expected_name}>, got non-element node"
521        )));
522    }
523    let tag = node.tag_name();
524    if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG_NS) {
525        return Err(ParseError::InvalidStructure(format!(
526            "expected <ds:{expected_name}>, got <{}{}>",
527            tag.namespace()
528                .map(|ns| format!("{{{ns}}}"))
529                .unwrap_or_default(),
530            tag.name()
531        )));
532    }
533    Ok(())
534}
535
536/// Verify that a node is a `<dsig11:{expected_name}>` element.
537fn verify_dsig11_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
538    if !node.is_element() {
539        return Err(ParseError::InvalidStructure(format!(
540            "expected element <{expected_name}>, got non-element node"
541        )));
542    }
543    let tag = node.tag_name();
544    if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG11_NS) {
545        return Err(ParseError::InvalidStructure(format!(
546            "expected <dsig11:{expected_name}>, got <{}{}>",
547            tag.namespace()
548                .map(|ns| format!("{{{ns}}}"))
549                .unwrap_or_default(),
550            tag.name()
551        )));
552    }
553    Ok(())
554}
555
556/// Get the required `Algorithm` attribute from an element.
557fn required_algorithm_attr<'a>(
558    node: Node<'a, 'a>,
559    element_name: &'static str,
560) -> Result<&'a str, ParseError> {
561    node.attribute("Algorithm").ok_or_else(|| {
562        ParseError::InvalidStructure(format!("missing Algorithm attribute on <{element_name}>"))
563    })
564}
565
566/// Parse the `PrefixList` attribute from an `<ec:InclusiveNamespaces>` child of
567/// `<CanonicalizationMethod>`, if present.
568///
569/// This mirrors transform parsing for Exclusive C14N and keeps SignedInfo
570/// canonicalization parameters lossless.
571fn parse_inclusive_prefixes(node: Node) -> Result<Option<String>, ParseError> {
572    const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
573
574    for child in node.children() {
575        if child.is_element() {
576            let tag = child.tag_name();
577            if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
578            {
579                return child
580                    .attribute("PrefixList")
581                    .map(str::to_string)
582                    .ok_or_else(|| {
583                        ParseError::InvalidStructure(
584                            "missing PrefixList attribute on <InclusiveNamespaces>".into(),
585                        )
586                    })
587                    .map(Some);
588            }
589        }
590    }
591
592    Ok(None)
593}
594
595fn parse_key_value_dispatch(node: Node) -> Result<KeyValueInfo, ParseError> {
596    verify_ds_element(node, "KeyValue")?;
597    ensure_no_non_whitespace_text(node, "KeyValue")?;
598
599    let mut children = element_children(node);
600    let Some(first_child) = children.next() else {
601        return Err(ParseError::InvalidStructure(
602            "KeyValue must contain exactly one key-value child".into(),
603        ));
604    };
605    if children.next().is_some() {
606        return Err(ParseError::InvalidStructure(
607            "KeyValue must contain exactly one key-value child".into(),
608        ));
609    }
610
611    match (
612        first_child.tag_name().namespace(),
613        first_child.tag_name().name(),
614    ) {
615        (Some(XMLDSIG_NS), "RSAKeyValue") => parse_rsa_key_value(first_child),
616        (Some(XMLDSIG11_NS), "ECKeyValue") => parse_ec_key_value(first_child),
617        (namespace, child_name) => Ok(KeyValueInfo::Unsupported {
618            namespace: namespace.map(str::to_string),
619            local_name: child_name.to_string(),
620        }),
621    }
622}
623
624fn parse_ec_key_value(node: Node<'_, '_>) -> Result<KeyValueInfo, ParseError> {
625    verify_dsig11_element(node, "ECKeyValue")?;
626    ensure_no_non_whitespace_text(node, "ECKeyValue")?;
627
628    let mut children = element_children(node);
629    let Some(named_curve_node) = children.next() else {
630        return Ok(KeyValueInfo::InvalidEcKeyValue);
631    };
632    if named_curve_node.tag_name().namespace() == Some(XMLDSIG11_NS)
633        && named_curve_node.tag_name().name() == "ECParameters"
634    {
635        return Ok(KeyValueInfo::Unsupported {
636            namespace: Some(XMLDSIG11_NS.to_string()),
637            local_name: "ECKeyValue".into(),
638        });
639    }
640    if named_curve_node.tag_name().namespace() != Some(XMLDSIG11_NS)
641        || named_curve_node.tag_name().name() != "NamedCurve"
642    {
643        return Ok(KeyValueInfo::InvalidEcKeyValue);
644    }
645    ensure_no_element_children(named_curve_node, "NamedCurve")?;
646    ensure_no_non_whitespace_text(named_curve_node, "NamedCurve")?;
647    let Some((curve_oid, expected_public_key_len)) =
648        (match parse_ec_named_curve_oid(named_curve_node) {
649            Ok(curve) => curve,
650            Err(_) => return Ok(KeyValueInfo::InvalidEcKeyValue),
651        })
652    else {
653        return Ok(KeyValueInfo::Unsupported {
654            namespace: Some(XMLDSIG11_NS.to_string()),
655            local_name: "ECKeyValue".into(),
656        });
657    };
658
659    let Some(public_key_node) = children.next() else {
660        return Ok(KeyValueInfo::InvalidEcKeyValue);
661    };
662    if public_key_node.tag_name().namespace() != Some(XMLDSIG11_NS)
663        || public_key_node.tag_name().name() != "PublicKey"
664    {
665        return Ok(KeyValueInfo::InvalidEcKeyValue);
666    }
667    ensure_no_element_children(public_key_node, "PublicKey")?;
668    if children.next().is_some() {
669        return Ok(KeyValueInfo::InvalidEcKeyValue);
670    }
671
672    let public_key = match decode_crypto_binary(public_key_node, "PublicKey", MAX_EC_PUBLIC_KEY_LEN)
673    {
674        Ok(public_key) => public_key,
675        Err(_) => return Ok(KeyValueInfo::InvalidEcKeyValue),
676    };
677    if validate_ec_public_key_point(&public_key, expected_public_key_len).is_err() {
678        return Ok(KeyValueInfo::InvalidEcKeyValue);
679    }
680
681    Ok(KeyValueInfo::Ec {
682        curve_oid,
683        public_key,
684    })
685}
686
687fn parse_ec_named_curve_oid(node: Node<'_, '_>) -> Result<Option<(String, usize)>, ParseError> {
688    let uri = node.attribute("URI").ok_or_else(|| {
689        ParseError::InvalidStructure("ECKeyValue NamedCurve must include URI attribute".into())
690    })?;
691    let curve_oid = uri.strip_prefix("urn:oid:").unwrap_or(uri);
692    if curve_oid.is_empty() {
693        return Err(ParseError::InvalidStructure(
694            "ECKeyValue NamedCurve URI must not be empty".into(),
695        ));
696    }
697    let Some(public_key_len) = ec_public_key_len(curve_oid) else {
698        return Ok(None);
699    };
700    Ok(Some((curve_oid.to_string(), public_key_len)))
701}
702
703fn ec_public_key_len(curve_oid: &str) -> Option<usize> {
704    match curve_oid {
705        EC_P256_OID => Some(65),
706        EC_P384_OID => Some(97),
707        _ => None,
708    }
709}
710
711fn validate_ec_public_key_point(public_key: &[u8], expected_len: usize) -> Result<(), ParseError> {
712    if public_key.len() != expected_len {
713        return Err(ParseError::InvalidStructure(
714            "ECKeyValue PublicKey length does not match NamedCurve".into(),
715        ));
716    }
717    if public_key.first().copied() != Some(0x04) {
718        return Err(ParseError::InvalidStructure(
719            "ECKeyValue PublicKey must be an uncompressed SEC1 point".into(),
720        ));
721    }
722    Ok(())
723}
724
725fn parse_rsa_key_value(node: Node<'_, '_>) -> Result<KeyValueInfo, ParseError> {
726    verify_ds_element(node, "RSAKeyValue")?;
727    ensure_no_non_whitespace_text(node, "RSAKeyValue")?;
728
729    let mut children = element_children(node);
730    let modulus_node = children.next().ok_or_else(|| {
731        ParseError::InvalidStructure("RSAKeyValue requires Modulus and Exponent".into())
732    })?;
733    verify_ds_element(modulus_node, "Modulus")?;
734    ensure_no_element_children(modulus_node, "Modulus")?;
735
736    let exponent_node = children.next().ok_or_else(|| {
737        ParseError::InvalidStructure("RSAKeyValue requires Modulus and Exponent".into())
738    })?;
739    verify_ds_element(exponent_node, "Exponent")?;
740    ensure_no_element_children(exponent_node, "Exponent")?;
741    if children.next().is_some() {
742        return Err(ParseError::InvalidStructure(
743            "RSAKeyValue must contain exactly Modulus followed by Exponent".into(),
744        ));
745    }
746
747    Ok(KeyValueInfo::Rsa {
748        modulus: decode_crypto_binary(modulus_node, "Modulus", MAX_RSA_MODULUS_LEN)?,
749        exponent: decode_crypto_binary(exponent_node, "Exponent", MAX_RSA_EXPONENT_LEN)?,
750    })
751}
752
753fn decode_crypto_binary(
754    node: Node<'_, '_>,
755    element_name: &'static str,
756    max_decoded_len: usize,
757) -> Result<Vec<u8>, ParseError> {
758    use base64::Engine;
759    use base64::engine::general_purpose::STANDARD;
760
761    let max_base64_len = max_decoded_len.div_ceil(3) * 4;
762    let mut cleaned = String::with_capacity(max_base64_len);
763    for text in node.children().filter_map(|child| child.text()) {
764        normalize_xml_base64_text_with_limit(text, &mut cleaned, max_base64_len).map_err(
765            |err| match err {
766                XmlBase64NormalizeLimitedError::InvalidWhitespace(err) => {
767                    ParseError::Base64(format!(
768                        "invalid XML whitespace U+{:04X} in {element_name}",
769                        err.invalid_byte
770                    ))
771                }
772                XmlBase64NormalizeLimitedError::TooLong(_) => ParseError::InvalidStructure(
773                    format!("{element_name} exceeds maximum allowed base64 length"),
774                ),
775            },
776        )?;
777    }
778
779    let value = STANDARD
780        .decode(&cleaned)
781        .map_err(|err| ParseError::Base64(format!("{element_name}: {err}")))?;
782    if value.is_empty() {
783        return Err(ParseError::InvalidStructure(format!(
784            "{element_name} must not be empty"
785        )));
786    }
787    if value.len() > max_decoded_len {
788        return Err(ParseError::InvalidStructure(format!(
789            "{element_name} exceeds maximum allowed binary length"
790        )));
791    }
792    Ok(value)
793}
794
795fn parse_x509_data_dispatch(node: Node) -> Result<X509DataInfo, ParseError> {
796    verify_ds_element(node, "X509Data")?;
797    ensure_no_non_whitespace_text(node, "X509Data")?;
798
799    let mut info = X509DataInfo::default();
800    let mut total_binary_len = 0usize;
801    for child in element_children(node) {
802        match (child.tag_name().namespace(), child.tag_name().name()) {
803            (Some(XMLDSIG_NS), "X509Certificate") => {
804                ensure_no_element_children(child, "X509Certificate")?;
805                ensure_x509_data_entry_budget(&info)?;
806                let cert = decode_x509_base64(child, "X509Certificate")?;
807                add_x509_data_usage(&mut total_binary_len, cert.len())?;
808                let parsed_cert = parse_x509_certificate(cert.as_slice())?;
809                info.parsed_certificates.push(parsed_cert);
810                info.certificates.push(cert);
811            }
812            (Some(XMLDSIG_NS), "X509SubjectName") => {
813                ensure_no_element_children(child, "X509SubjectName")?;
814                ensure_x509_data_entry_budget(&info)?;
815                let subject_name = collect_text_content_bounded(
816                    child,
817                    MAX_X509_SUBJECT_NAME_TEXT_LEN,
818                    "X509SubjectName",
819                )?;
820                info.subject_names.push(subject_name);
821            }
822            (Some(XMLDSIG_NS), "X509IssuerSerial") => {
823                ensure_x509_data_entry_budget(&info)?;
824                let issuer_serial = parse_x509_issuer_serial(child)?;
825                info.issuer_serials.push(issuer_serial);
826            }
827            (Some(XMLDSIG_NS), "X509SKI") => {
828                ensure_no_element_children(child, "X509SKI")?;
829                ensure_x509_data_entry_budget(&info)?;
830                let ski = decode_x509_base64(child, "X509SKI")?;
831                add_x509_data_usage(&mut total_binary_len, ski.len())?;
832                info.skis.push(ski);
833            }
834            (Some(XMLDSIG_NS), "X509CRL") => {
835                ensure_no_element_children(child, "X509CRL")?;
836                ensure_x509_data_entry_budget(&info)?;
837                let crl = decode_x509_base64(child, "X509CRL")?;
838                add_x509_data_usage(&mut total_binary_len, crl.len())?;
839                info.crls.push(crl);
840            }
841            (Some(XMLDSIG11_NS), "X509Digest") => {
842                ensure_no_element_children(child, "X509Digest")?;
843                ensure_x509_data_entry_budget(&info)?;
844                let algorithm = required_algorithm_attr(child, "X509Digest")?;
845                let digest = decode_x509_base64(child, "X509Digest")?;
846                add_x509_data_usage(&mut total_binary_len, digest.len())?;
847                info.digests.push((algorithm.to_string(), digest));
848            }
849            (Some(XMLDSIG_NS), child_name) | (Some(XMLDSIG11_NS), child_name) => {
850                return Err(ParseError::InvalidStructure(format!(
851                    "X509Data contains unsupported XMLDSig child element <{child_name}>"
852                )));
853            }
854            _ => {}
855        }
856    }
857
858    info.certificate_chain = build_x509_certificate_chain(&info)?;
859    Ok(info)
860}
861
862fn build_x509_certificate_chain(info: &X509DataInfo) -> Result<Vec<usize>, ParseError> {
863    if info.parsed_certificates.is_empty() {
864        return Ok(Vec::new());
865    }
866
867    let signing_idx = select_x509_signing_certificate(info)?;
868    let mut chain = vec![signing_idx];
869
870    loop {
871        if chain.len() > MAX_X509_CHAIN_DEPTH {
872            return Err(ParseError::InvalidStructure(
873                "X509Data certificate chain exceeds maximum depth".into(),
874            ));
875        }
876
877        let current_idx = *chain
878            .last()
879            .expect("chain starts with signing certificate index");
880        let current = &info.parsed_certificates[current_idx];
881        if current.subject_dn == current.issuer_dn {
882            break;
883        }
884
885        let candidates = info
886            .parsed_certificates
887            .iter()
888            .enumerate()
889            .filter(|(idx, cert)| *idx != current_idx && cert.subject_dn == current.issuer_dn)
890            .map(|(idx, _)| idx)
891            .collect::<Vec<_>>();
892
893        match candidates.as_slice() {
894            [] => break,
895            [issuer_idx] => {
896                if chain.contains(issuer_idx) {
897                    return Err(ParseError::InvalidStructure(
898                        "X509Data certificate chain contains a cycle".into(),
899                    ));
900                }
901                if chain.len() == MAX_X509_CHAIN_DEPTH {
902                    return Err(ParseError::InvalidStructure(
903                        "X509Data certificate chain exceeds maximum depth".into(),
904                    ));
905                }
906                chain.push(*issuer_idx);
907            }
908            _ => {
909                return Err(ParseError::InvalidStructure(
910                    "X509Data certificate chain contains ambiguous issuer certificates".into(),
911                ));
912            }
913        }
914    }
915
916    Ok(chain)
917}
918
919fn select_x509_signing_certificate(info: &X509DataInfo) -> Result<usize, ParseError> {
920    let has_lookup_identifiers = x509_data_has_lookup_identifiers(info);
921    let mut candidates = Vec::new();
922    if has_lookup_identifiers {
923        for (idx, (parsed, der)) in info
924            .parsed_certificates
925            .iter()
926            .zip(&info.certificates)
927            .enumerate()
928        {
929            if x509_certificate_matches_any_selector(info, parsed, der)? {
930                candidates.push(idx);
931            }
932        }
933        if !x509_selector_categories_match_chain(info)? {
934            return Err(ParseError::InvalidStructure(
935                "X509Data lookup identifiers do not match the embedded certificate chain".into(),
936            ));
937        }
938    }
939
940    match candidates.as_slice() {
941        [idx] => return Ok(*idx),
942        [] if has_lookup_identifiers => {
943            return Err(ParseError::InvalidStructure(
944                "X509Data lookup identifiers do not match any embedded certificate".into(),
945            ));
946        }
947        [] => {}
948        _ => {}
949    }
950
951    let leaf_candidates = info
952        .parsed_certificates
953        .iter()
954        .enumerate()
955        .filter(|(_, cert)| {
956            cert.subject_dn != cert.issuer_dn
957                && !info
958                    .parsed_certificates
959                    .iter()
960                    .any(|other| other.issuer_dn == cert.subject_dn)
961        })
962        .map(|(idx, _)| idx)
963        .collect::<Vec<_>>();
964
965    let selected_leaves = leaf_candidates
966        .iter()
967        .filter(|idx| !has_lookup_identifiers || candidates.contains(idx))
968        .copied()
969        .collect::<Vec<_>>();
970
971    match selected_leaves.as_slice() {
972        [idx] => Ok(*idx),
973        [] if !has_lookup_identifiers => Ok(0),
974        [] => Err(ParseError::InvalidStructure(
975            "X509Data lookup identifiers match multiple certificates without a unique signing certificate"
976                .into(),
977        )),
978        _ => Err(ParseError::InvalidStructure(
979            if has_lookup_identifiers {
980                "X509Data lookup identifiers match multiple certificates"
981            } else {
982                "X509Data contains multiple possible signing certificates"
983            }
984            .into(),
985        )),
986    }
987}
988
989pub(crate) fn x509_data_has_lookup_identifiers(info: &X509DataInfo) -> bool {
990    !info.subject_names.is_empty()
991        || !info.issuer_serials.is_empty()
992        || !info.skis.is_empty()
993        || !info.digests.is_empty()
994}
995
996pub(crate) fn x509_certificate_matches_any_selector(
997    info: &X509DataInfo,
998    certificate: &ParsedX509Certificate,
999    certificate_der: &[u8],
1000) -> Result<bool, ParseError> {
1001    let subject_match = info
1002        .subject_names
1003        .iter()
1004        .any(|subject| subject.trim() == certificate.subject_dn);
1005    let mut issuer_serial_match = false;
1006    for (issuer, serial) in &info.issuer_serials {
1007        let serial_hex = x509_serial_decimal_to_hex(serial).ok_or_else(|| {
1008            ParseError::InvalidStructure(
1009                "X509Data lookup identifiers contain an invalid serial number".into(),
1010            )
1011        })?;
1012        issuer_serial_match |=
1013            issuer.trim() == certificate.issuer_dn && serial_hex == certificate.serial_number_hex;
1014    }
1015    let ski_match = certificate
1016        .subject_key_identifier
1017        .as_ref()
1018        .is_some_and(|certificate_ski| info.skis.iter().any(|ski| ski == certificate_ski));
1019    let mut digest_match = false;
1020    for (algorithm_uri, expected) in &info.digests {
1021        let algorithm = DigestAlgorithm::from_uri(algorithm_uri).ok_or_else(|| {
1022            ParseError::UnsupportedAlgorithm {
1023                uri: algorithm_uri.clone(),
1024            }
1025        })?;
1026        digest_match |= constant_time_eq(&compute_digest(algorithm, certificate_der), expected);
1027    }
1028    Ok(subject_match || issuer_serial_match || ski_match || digest_match)
1029}
1030
1031pub(crate) fn x509_selector_categories_match_chain(
1032    info: &X509DataInfo,
1033) -> Result<bool, ParseError> {
1034    let subject_match = info.subject_names.iter().all(|subject| {
1035        info.parsed_certificates
1036            .iter()
1037            .any(|certificate| subject.trim() == certificate.subject_dn)
1038    });
1039
1040    let mut issuer_serial_match = true;
1041    for (issuer, serial) in &info.issuer_serials {
1042        let serial_hex = x509_serial_decimal_to_hex(serial).ok_or_else(|| {
1043            ParseError::InvalidStructure(
1044                "X509Data lookup identifiers contain an invalid serial number".into(),
1045            )
1046        })?;
1047        issuer_serial_match &= info.parsed_certificates.iter().any(|certificate| {
1048            issuer.trim() == certificate.issuer_dn && serial_hex == certificate.serial_number_hex
1049        });
1050    }
1051
1052    let ski_match = info.skis.iter().all(|ski| {
1053        info.parsed_certificates.iter().any(|certificate| {
1054            certificate
1055                .subject_key_identifier
1056                .as_ref()
1057                .is_some_and(|certificate_ski| ski == certificate_ski)
1058        })
1059    });
1060
1061    let mut digest_match = true;
1062    for (algorithm_uri, expected) in &info.digests {
1063        let algorithm = DigestAlgorithm::from_uri(algorithm_uri).ok_or_else(|| {
1064            ParseError::UnsupportedAlgorithm {
1065                uri: algorithm_uri.clone(),
1066            }
1067        })?;
1068        digest_match &= info
1069            .certificates
1070            .iter()
1071            .any(|certificate| constant_time_eq(&compute_digest(algorithm, certificate), expected));
1072    }
1073
1074    Ok(subject_match && issuer_serial_match && ski_match && digest_match)
1075}
1076
1077fn ensure_x509_data_entry_budget(info: &X509DataInfo) -> Result<(), ParseError> {
1078    let total_entries = info.certificates.len()
1079        + info.subject_names.len()
1080        + info.issuer_serials.len()
1081        + info.skis.len()
1082        + info.crls.len()
1083        + info.digests.len();
1084    if total_entries >= MAX_X509_DATA_ENTRY_COUNT {
1085        return Err(ParseError::InvalidStructure(
1086            "X509Data contains too many entries".into(),
1087        ));
1088    }
1089    Ok(())
1090}
1091
1092fn add_x509_data_usage(total_binary_len: &mut usize, delta: usize) -> Result<(), ParseError> {
1093    *total_binary_len = total_binary_len.checked_add(delta).ok_or_else(|| {
1094        ParseError::InvalidStructure("X509Data exceeds maximum allowed total binary length".into())
1095    })?;
1096    if *total_binary_len > MAX_X509_DATA_TOTAL_BINARY_LEN {
1097        return Err(ParseError::InvalidStructure(
1098            "X509Data exceeds maximum allowed total binary length".into(),
1099        ));
1100    }
1101    Ok(())
1102}
1103
1104fn decode_x509_base64(
1105    node: Node<'_, '_>,
1106    element_name: &'static str,
1107) -> Result<Vec<u8>, ParseError> {
1108    use base64::Engine;
1109    use base64::engine::general_purpose::STANDARD;
1110
1111    let mut cleaned = String::new();
1112    let mut raw_text_len = 0usize;
1113    for text in node
1114        .children()
1115        .filter(|child| child.is_text())
1116        .filter_map(|child| child.text())
1117    {
1118        if raw_text_len.saturating_add(text.len()) > MAX_X509_BASE64_TEXT_LEN {
1119            return Err(ParseError::InvalidStructure(format!(
1120                "{element_name} exceeds maximum allowed text length"
1121            )));
1122        }
1123        raw_text_len = raw_text_len.saturating_add(text.len());
1124        normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
1125            ParseError::Base64(format!(
1126                "invalid XML whitespace U+{:04X} in {element_name}",
1127                err.invalid_byte
1128            ))
1129        })?;
1130        if cleaned.len() > MAX_X509_BASE64_NORMALIZED_LEN {
1131            return Err(ParseError::InvalidStructure(format!(
1132                "{element_name} exceeds maximum allowed base64 length"
1133            )));
1134        }
1135    }
1136
1137    let decoded = STANDARD
1138        .decode(&cleaned)
1139        .map_err(|e| ParseError::Base64(format!("{element_name}: {e}")))?;
1140    if decoded.is_empty() {
1141        return Err(ParseError::InvalidStructure(format!(
1142            "{element_name} must not be empty"
1143        )));
1144    }
1145    if decoded.len() > MAX_X509_DECODED_BINARY_LEN {
1146        return Err(ParseError::InvalidStructure(format!(
1147            "{element_name} exceeds maximum allowed binary length"
1148        )));
1149    }
1150    Ok(decoded)
1151}
1152
1153pub(crate) fn parse_x509_certificate(cert_der: &[u8]) -> Result<ParsedX509Certificate, ParseError> {
1154    let (rest, cert) =
1155        x509_parser::certificate::X509Certificate::from_der(cert_der).map_err(|err| {
1156            ParseError::InvalidStructure(format!("X509Certificate is not valid DER X.509: {err}"))
1157        })?;
1158    if !rest.is_empty() {
1159        return Err(ParseError::InvalidStructure(
1160            "X509Certificate contains trailing bytes after DER certificate".into(),
1161        ));
1162    }
1163
1164    let subject_dn = cert.subject().to_string();
1165    let issuer_dn = cert.issuer().to_string();
1166    let serial_number = cert.tbs_certificate.raw_serial().to_vec();
1167    let serial_number_hex = format_x509_serial_value_hex(&serial_number);
1168
1169    let subject_key_identifier = cert.extensions().iter().find_map(|ext| {
1170        if let ParsedExtension::SubjectKeyIdentifier(ski) = ext.parsed_extension() {
1171            Some(ski.0.to_vec())
1172        } else {
1173            None
1174        }
1175    });
1176
1177    let spki = cert.public_key();
1178    let public_key = match spki.parsed().map_err(|err| {
1179        ParseError::InvalidStructure(format!("X509Certificate public key parse error: {err}"))
1180    })? {
1181        PublicKey::RSA(rsa) => {
1182            let modulus = trim_leading_zeroes(rsa.modulus);
1183            let exponent = trim_leading_zeroes(rsa.exponent);
1184            if modulus.is_empty() || exponent.is_empty() {
1185                return Err(ParseError::InvalidStructure(
1186                    "X509Certificate RSA key contains empty modulus or exponent".into(),
1187                ));
1188            }
1189            X509PublicKeyInfo::Rsa { modulus, exponent }
1190        }
1191        PublicKey::EC(ec_point) => {
1192            let Some(params) = spki.algorithm.parameters.as_ref() else {
1193                return Err(ParseError::InvalidStructure(
1194                    "X509Certificate EC key is missing curve parameters".into(),
1195                ));
1196            };
1197
1198            match params.as_oid() {
1199                Ok(oid) => X509PublicKeyInfo::Ec {
1200                    curve_oid: oid.to_id_string(),
1201                    public_key: ec_point.data().to_vec(),
1202                },
1203                Err(_) => X509PublicKeyInfo::Unsupported {
1204                    algorithm_oid: spki.algorithm.algorithm.to_id_string(),
1205                },
1206            }
1207        }
1208        _ => X509PublicKeyInfo::Unsupported {
1209            algorithm_oid: spki.algorithm.algorithm.to_id_string(),
1210        },
1211    };
1212
1213    Ok(ParsedX509Certificate {
1214        subject_dn,
1215        issuer_dn,
1216        serial_number,
1217        serial_number_hex,
1218        subject_key_identifier,
1219        public_key,
1220    })
1221}
1222
1223fn format_x509_serial_hex(serial: &[u8]) -> String {
1224    serial
1225        .iter()
1226        .map(|byte| format!("{byte:02X}"))
1227        .collect::<String>()
1228}
1229
1230fn format_x509_serial_value_hex(serial: &[u8]) -> String {
1231    let first_non_zero = serial
1232        .iter()
1233        .position(|byte| *byte != 0)
1234        .unwrap_or(serial.len());
1235    let canonical = if first_non_zero == serial.len() {
1236        &[0]
1237    } else {
1238        &serial[first_non_zero..]
1239    };
1240    format_x509_serial_hex(canonical)
1241}
1242
1243fn x509_serial_decimal_to_hex(serial: &str) -> Option<String> {
1244    let serial = serial.trim();
1245    let serial = serial.strip_prefix('+').unwrap_or(serial);
1246    if serial.is_empty() || !serial.bytes().all(|byte| byte.is_ascii_digit()) {
1247        return None;
1248    }
1249
1250    let mut bytes = Vec::<u8>::new();
1251    for digit in serial.bytes().map(|byte| byte - b'0') {
1252        let mut carry = u16::from(digit);
1253        for byte in bytes.iter_mut().rev() {
1254            let value = u16::from(*byte) * 10 + carry;
1255            *byte = value as u8;
1256            carry = value >> 8;
1257        }
1258        while carry > 0 {
1259            bytes.insert(0, carry as u8);
1260            carry >>= 8;
1261        }
1262    }
1263
1264    Some(format_x509_serial_value_hex(&bytes))
1265}
1266
1267fn trim_leading_zeroes(bytes: &[u8]) -> Vec<u8> {
1268    let first_non_zero = bytes
1269        .iter()
1270        .position(|byte| *byte != 0)
1271        .unwrap_or(bytes.len());
1272    bytes[first_non_zero..].to_vec()
1273}
1274
1275fn parse_x509_issuer_serial(node: Node<'_, '_>) -> Result<(String, String), ParseError> {
1276    verify_ds_element(node, "X509IssuerSerial")?;
1277    ensure_no_non_whitespace_text(node, "X509IssuerSerial")?;
1278
1279    let children = element_children(node).collect::<Vec<_>>();
1280    if children.len() != 2 {
1281        return Err(ParseError::InvalidStructure(
1282            "X509IssuerSerial must contain exactly X509IssuerName then X509SerialNumber".into(),
1283        ));
1284    }
1285    if !matches!(
1286        (
1287            children[0].tag_name().namespace(),
1288            children[0].tag_name().name()
1289        ),
1290        (Some(XMLDSIG_NS), "X509IssuerName")
1291    ) {
1292        return Err(ParseError::InvalidStructure(
1293            "X509IssuerSerial must contain X509IssuerName as the first child element".into(),
1294        ));
1295    }
1296    if !matches!(
1297        (
1298            children[1].tag_name().namespace(),
1299            children[1].tag_name().name()
1300        ),
1301        (Some(XMLDSIG_NS), "X509SerialNumber")
1302    ) {
1303        return Err(ParseError::InvalidStructure(
1304            "X509IssuerSerial must contain X509SerialNumber as the second child element".into(),
1305        ));
1306    }
1307
1308    let issuer_node = children[0];
1309    ensure_no_element_children(issuer_node, "X509IssuerName")?;
1310    let issuer_name =
1311        collect_text_content_bounded(issuer_node, MAX_X509_ISSUER_NAME_TEXT_LEN, "X509IssuerName")?;
1312
1313    let serial_node = children[1];
1314    ensure_no_element_children(serial_node, "X509SerialNumber")?;
1315    let serial_number = collect_text_content_bounded(
1316        serial_node,
1317        MAX_X509_SERIAL_NUMBER_TEXT_LEN,
1318        "X509SerialNumber",
1319    )?;
1320    if issuer_name.trim().is_empty() || serial_number.trim().is_empty() {
1321        return Err(ParseError::InvalidStructure(
1322            "X509IssuerSerial requires non-empty X509IssuerName and X509SerialNumber".into(),
1323        ));
1324    }
1325
1326    Ok((issuer_name, serial_number))
1327}
1328
1329/// Base64-decode a digest value string, stripping whitespace.
1330///
1331/// XMLDSig allows whitespace within base64 content (line-wrapped encodings).
1332fn base64_decode_digest(b64: &str, digest_method: DigestAlgorithm) -> Result<Vec<u8>, ParseError> {
1333    use base64::Engine;
1334    use base64::engine::general_purpose::STANDARD;
1335
1336    let expected = digest_method.output_len();
1337    let max_base64_len = expected.div_ceil(3) * 4;
1338    let mut cleaned = String::with_capacity(b64.len().min(max_base64_len));
1339    normalize_xml_base64_text(b64, &mut cleaned).map_err(|err| {
1340        ParseError::Base64(format!(
1341            "invalid XML whitespace U+{:04X} in DigestValue",
1342            err.invalid_byte
1343        ))
1344    })?;
1345    if cleaned.len() > max_base64_len {
1346        return Err(ParseError::Base64(
1347            "DigestValue exceeds maximum allowed base64 length".into(),
1348        ));
1349    }
1350    let digest = STANDARD
1351        .decode(&cleaned)
1352        .map_err(|e| ParseError::Base64(e.to_string()))?;
1353    let actual = digest.len();
1354    if actual != expected {
1355        return Err(ParseError::DigestLengthMismatch {
1356            algorithm: digest_method.uri(),
1357            expected,
1358            actual,
1359        });
1360    }
1361    Ok(digest)
1362}
1363
1364fn decode_digest_value_children(
1365    digest_value_node: Node<'_, '_>,
1366    digest_method: DigestAlgorithm,
1367) -> Result<Vec<u8>, ParseError> {
1368    let max_base64_len = digest_method.output_len().div_ceil(3) * 4;
1369    let mut cleaned = String::with_capacity(max_base64_len);
1370
1371    for child in digest_value_node.children() {
1372        if child.is_element() {
1373            return Err(ParseError::InvalidStructure(
1374                "DigestValue must not contain element children".into(),
1375            ));
1376        }
1377        if let Some(text) = child.text() {
1378            normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
1379                ParseError::Base64(format!(
1380                    "invalid XML whitespace U+{:04X} in DigestValue",
1381                    err.invalid_byte
1382                ))
1383            })?;
1384            if cleaned.len() > max_base64_len {
1385                return Err(ParseError::Base64(
1386                    "DigestValue exceeds maximum allowed base64 length".into(),
1387                ));
1388            }
1389        }
1390    }
1391
1392    base64_decode_digest(&cleaned, digest_method)
1393}
1394
1395fn decode_der_encoded_key_value_base64(node: Node<'_, '_>) -> Result<Vec<u8>, ParseError> {
1396    use base64::Engine;
1397    use base64::engine::general_purpose::STANDARD;
1398
1399    let mut cleaned = String::new();
1400    let mut raw_text_len = 0usize;
1401    for text in node
1402        .children()
1403        .filter(|child| child.is_text())
1404        .filter_map(|child| child.text())
1405    {
1406        if raw_text_len.saturating_add(text.len()) > MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN {
1407            return Err(ParseError::InvalidStructure(
1408                "DEREncodedKeyValue exceeds maximum allowed text length".into(),
1409            ));
1410        }
1411        raw_text_len = raw_text_len.saturating_add(text.len());
1412        normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
1413            ParseError::Base64(format!(
1414                "invalid XML whitespace U+{:04X} in base64 text",
1415                err.invalid_byte
1416            ))
1417        })?;
1418        if cleaned.len() > MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN {
1419            return Err(ParseError::InvalidStructure(
1420                "DEREncodedKeyValue exceeds maximum allowed length".into(),
1421            ));
1422        }
1423    }
1424
1425    let der = STANDARD
1426        .decode(&cleaned)
1427        .map_err(|e| ParseError::Base64(e.to_string()))?;
1428    if der.is_empty() {
1429        return Err(ParseError::InvalidStructure(
1430            "DEREncodedKeyValue must not be empty".into(),
1431        ));
1432    }
1433    if der.len() > MAX_DER_ENCODED_KEY_VALUE_LEN {
1434        return Err(ParseError::InvalidStructure(
1435            "DEREncodedKeyValue exceeds maximum allowed length".into(),
1436        ));
1437    }
1438    Ok(der)
1439}
1440
1441fn collect_text_content_bounded(
1442    node: Node<'_, '_>,
1443    max_len: usize,
1444    element_name: &'static str,
1445) -> Result<String, ParseError> {
1446    let mut text = String::new();
1447    for chunk in node
1448        .children()
1449        .filter_map(|child| child.is_text().then(|| child.text()).flatten())
1450    {
1451        if text.len().saturating_add(chunk.len()) > max_len {
1452            return Err(ParseError::InvalidStructure(format!(
1453                "{element_name} exceeds maximum allowed text length"
1454            )));
1455        }
1456        text.push_str(chunk);
1457    }
1458    Ok(text)
1459}
1460
1461fn ensure_no_element_children(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
1462    if node.children().any(|child| child.is_element()) {
1463        return Err(ParseError::InvalidStructure(format!(
1464            "{element_name} must not contain child elements"
1465        )));
1466    }
1467    Ok(())
1468}
1469
1470fn ensure_no_non_whitespace_text(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
1471    for child in node.children().filter(|child| child.is_text()) {
1472        if let Some(text) = child.text()
1473            && !is_xml_whitespace_only(text)
1474        {
1475            return Err(ParseError::InvalidStructure(format!(
1476                "{element_name} must not contain non-whitespace mixed content"
1477            )));
1478        }
1479    }
1480    Ok(())
1481}
1482
1483#[cfg(test)]
1484#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
1485mod tests {
1486    use super::*;
1487    use base64::Engine;
1488
1489    fn fixture_rsa_cert_base64() -> String {
1490        fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem")
1491    }
1492
1493    fn fixture_cert_base64(path: &str) -> String {
1494        match path {
1495            "../../tests/fixtures/keys/rsa/rsa-2048-cert.pem" => {
1496                include_str!("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem")
1497            }
1498            "../../tests/fixtures/keys/rsa/rsa-4096-cert.pem" => {
1499                include_str!("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem")
1500            }
1501            "../../tests/fixtures/keys/ca2cert.pem" => {
1502                include_str!("../../tests/fixtures/keys/ca2cert.pem")
1503            }
1504            "../../tests/fixtures/keys/cacert.pem" => {
1505                include_str!("../../tests/fixtures/keys/cacert.pem")
1506            }
1507            _ => unreachable!("unknown certificate fixture"),
1508        }
1509        .lines()
1510        .skip_while(|line| *line != "-----BEGIN CERTIFICATE-----")
1511        .skip(1)
1512        .take_while(|line| *line != "-----END CERTIFICATE-----")
1513        .collect::<String>()
1514    }
1515
1516    // ── SignatureAlgorithm ───────────────────────────────────────────
1517
1518    #[test]
1519    fn signature_algorithm_from_uri_rsa_sha256() {
1520        assert_eq!(
1521            SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"),
1522            Some(SignatureAlgorithm::RsaSha256)
1523        );
1524    }
1525
1526    #[test]
1527    fn signature_algorithm_from_uri_rsa_sha1() {
1528        assert_eq!(
1529            SignatureAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
1530            Some(SignatureAlgorithm::RsaSha1)
1531        );
1532    }
1533
1534    #[test]
1535    fn signature_algorithm_from_uri_ecdsa_sha256() {
1536        assert_eq!(
1537            SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"),
1538            Some(SignatureAlgorithm::EcdsaP256Sha256)
1539        );
1540    }
1541
1542    #[test]
1543    fn signature_algorithm_from_uri_unknown() {
1544        assert_eq!(
1545            SignatureAlgorithm::from_uri("http://example.com/unknown"),
1546            None
1547        );
1548    }
1549
1550    #[test]
1551    fn signature_algorithm_uri_round_trip() {
1552        for algo in [
1553            SignatureAlgorithm::RsaSha1,
1554            SignatureAlgorithm::RsaSha256,
1555            SignatureAlgorithm::RsaSha384,
1556            SignatureAlgorithm::RsaSha512,
1557            SignatureAlgorithm::EcdsaP256Sha256,
1558            SignatureAlgorithm::EcdsaP384Sha384,
1559        ] {
1560            assert_eq!(
1561                SignatureAlgorithm::from_uri(algo.uri()),
1562                Some(algo),
1563                "round-trip failed for {algo:?}"
1564            );
1565        }
1566    }
1567
1568    #[test]
1569    fn rsa_sha1_verify_only() {
1570        assert!(!SignatureAlgorithm::RsaSha1.signing_allowed());
1571        assert!(SignatureAlgorithm::RsaSha256.signing_allowed());
1572        assert!(SignatureAlgorithm::EcdsaP256Sha256.signing_allowed());
1573    }
1574
1575    // ── find_signature_node ──────────────────────────────────────────
1576
1577    #[test]
1578    fn find_signature_in_saml() {
1579        let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
1580            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1581                <ds:SignedInfo/>
1582            </ds:Signature>
1583        </samlp:Response>"#;
1584        let doc = Document::parse(xml).unwrap();
1585        let sig = find_signature_node(&doc);
1586        assert!(sig.is_some());
1587        assert_eq!(sig.unwrap().tag_name().name(), "Signature");
1588    }
1589
1590    #[test]
1591    fn find_signature_missing() {
1592        let xml = "<root><child/></root>";
1593        let doc = Document::parse(xml).unwrap();
1594        assert!(find_signature_node(&doc).is_none());
1595    }
1596
1597    #[test]
1598    fn find_signature_ignores_wrong_namespace() {
1599        let xml = r#"<root><Signature xmlns="http://example.com/fake"/></root>"#;
1600        let doc = Document::parse(xml).unwrap();
1601        assert!(find_signature_node(&doc).is_none());
1602    }
1603
1604    // ── parse_key_info: dispatch parsing ──────────────────────────────
1605
1606    #[test]
1607    fn parse_key_info_dispatches_supported_children() {
1608        let cert_base64 = fixture_rsa_cert_base64();
1609        let expected_cert = base64::engine::general_purpose::STANDARD
1610            .decode(&cert_base64)
1611            .expect("fixture PEM must contain valid base64");
1612        let cert_digest = base64::engine::general_purpose::STANDARD
1613            .encode(compute_digest(DigestAlgorithm::Sha256, &expected_cert));
1614        let xml = format!(
1615            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
1616                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
1617            <KeyName>idp-signing-key</KeyName>
1618            <KeyValue>
1619                <RSAKeyValue>
1620                    <Modulus>AQAB</Modulus>
1621                    <Exponent>AQAB</Exponent>
1622                </RSAKeyValue>
1623            </KeyValue>
1624            <X509Data>
1625                <X509Certificate>{cert_base64}</X509Certificate>
1626                <X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
1627                <X509IssuerSerial>
1628                    <X509IssuerName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com</X509IssuerName>
1629                    <X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
1630                </X509IssuerSerial>
1631                <X509SKI>bcOXN/nsVl8GatRbcKrPbzIbw0Y=</X509SKI>
1632                <X509CRL>BAUGBw==</X509CRL>
1633                <dsig11:X509Digest Algorithm="http://www.w3.org/2001/04/xmlenc#sha256">{cert_digest}</dsig11:X509Digest>
1634            </X509Data>
1635            <dsig11:DEREncodedKeyValue>AQIDBA==</dsig11:DEREncodedKeyValue>
1636        </KeyInfo>"#
1637        );
1638        let doc = Document::parse(&xml).unwrap();
1639
1640        let key_info = parse_key_info(doc.root_element()).unwrap();
1641        assert_eq!(key_info.sources.len(), 4);
1642
1643        assert_eq!(
1644            key_info.sources[0],
1645            KeyInfoSource::KeyName("idp-signing-key".to_string())
1646        );
1647        assert_eq!(
1648            key_info.sources[1],
1649            KeyInfoSource::KeyValue(KeyValueInfo::Rsa {
1650                modulus: vec![1, 0, 1],
1651                exponent: vec![1, 0, 1],
1652            })
1653        );
1654        let x509_info = match &key_info.sources[2] {
1655            KeyInfoSource::X509Data(x509) => x509,
1656            other => panic!("expected X509Data source, got {other:?}"),
1657        };
1658        assert_eq!(x509_info.certificates, vec![expected_cert]);
1659        assert_eq!(
1660            x509_info.subject_names,
1661            vec![
1662                "C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048"
1663                    .to_string()
1664            ]
1665        );
1666        assert_eq!(
1667            x509_info.issuer_serials,
1668            vec![(
1669                "C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com".to_string(),
1670                "680572598617295163017172295025714171905498632019".to_string()
1671            )]
1672        );
1673        assert_eq!(
1674            x509_info.skis,
1675            vec![vec![
1676                109, 195, 151, 55, 249, 236, 86, 95, 6, 106, 212, 91, 112, 170, 207, 111, 50, 27,
1677                195, 70
1678            ]]
1679        );
1680        assert_eq!(x509_info.crls, vec![vec![4, 5, 6, 7]]);
1681        assert_eq!(
1682            x509_info.digests,
1683            vec![(
1684                "http://www.w3.org/2001/04/xmlenc#sha256".to_string(),
1685                compute_digest(DigestAlgorithm::Sha256, &x509_info.certificates[0])
1686            )]
1687        );
1688        assert_eq!(x509_info.parsed_certificates.len(), 1);
1689        assert_eq!(x509_info.certificate_chain, vec![0]);
1690        let parsed_cert = &x509_info.parsed_certificates[0];
1691        assert!(!parsed_cert.subject_dn.is_empty());
1692        assert!(!parsed_cert.issuer_dn.is_empty());
1693        assert_eq!(
1694            parsed_cert.serial_number_hex,
1695            "7735EE487F6862DAF1B3956D961CCB0FA6F34F53"
1696        );
1697        assert!(parsed_cert.subject_key_identifier.is_some());
1698        assert!(matches!(
1699            parsed_cert.public_key,
1700            X509PublicKeyInfo::Rsa { .. }
1701        ));
1702
1703        assert_eq!(
1704            key_info.sources[3],
1705            KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])
1706        );
1707    }
1708
1709    #[test]
1710    fn parse_rsa_key_value_preserves_wrapped_crypto_binary() {
1711        // CryptoBinary is unsigned big-endian data and XML whitespace is insignificant.
1712        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1713            <KeyValue><RSAKeyValue>
1714                <Modulus> AQID
1715BA== </Modulus>
1716                <Exponent> AQAB </Exponent>
1717            </RSAKeyValue></KeyValue>
1718        </KeyInfo>"#;
1719        let doc = Document::parse(xml).unwrap();
1720
1721        assert_eq!(
1722            parse_key_info(doc.root_element()).unwrap().sources,
1723            vec![KeyInfoSource::KeyValue(KeyValueInfo::Rsa {
1724                modulus: vec![1, 2, 3, 4],
1725                exponent: vec![1, 0, 1],
1726            })]
1727        );
1728    }
1729
1730    #[test]
1731    fn parse_rsa_key_value_rejects_reordered_parameters() {
1732        // XMLDSig defines Modulus followed by Exponent; accepting reordered input is ambiguous.
1733        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1734            <KeyValue><RSAKeyValue>
1735                <Exponent>AQAB</Exponent><Modulus>AQID</Modulus>
1736            </RSAKeyValue></KeyValue>
1737        </KeyInfo>"#;
1738        let doc = Document::parse(xml).unwrap();
1739
1740        assert!(matches!(
1741            parse_key_info(doc.root_element()),
1742            Err(ParseError::InvalidStructure(_))
1743        ));
1744    }
1745
1746    #[test]
1747    fn parse_rsa_key_value_rejects_missing_exponent() {
1748        // Both RSA public parameters are required to construct a usable key.
1749        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1750            <KeyValue><RSAKeyValue><Modulus>AQID</Modulus></RSAKeyValue></KeyValue>
1751        </KeyInfo>"#;
1752        let doc = Document::parse(xml).unwrap();
1753
1754        assert!(matches!(
1755            parse_key_info(doc.root_element()),
1756            Err(ParseError::InvalidStructure(_))
1757        ));
1758    }
1759
1760    #[test]
1761    fn parse_rsa_key_value_rejects_duplicate_exponent() {
1762        // RSAKeyValue has a closed two-child schema; duplicate parameters are invalid.
1763        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1764            <KeyValue><RSAKeyValue>
1765                <Modulus>AQID</Modulus><Exponent>AQAB</Exponent><Exponent>AQAB</Exponent>
1766            </RSAKeyValue></KeyValue>
1767        </KeyInfo>"#;
1768        let doc = Document::parse(xml).unwrap();
1769
1770        assert!(matches!(
1771            parse_key_info(doc.root_element()),
1772            Err(ParseError::InvalidStructure(_))
1773        ));
1774    }
1775
1776    #[test]
1777    fn parse_rsa_key_value_rejects_wrong_parameter_namespace() {
1778        // Local names from an extension namespace must not be treated as XMLDSig parameters.
1779        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:bad="urn:bad">
1780            <KeyValue><RSAKeyValue>
1781                <bad:Modulus>AQID</bad:Modulus><Exponent>AQAB</Exponent>
1782            </RSAKeyValue></KeyValue>
1783        </KeyInfo>"#;
1784        let doc = Document::parse(xml).unwrap();
1785
1786        assert!(matches!(
1787            parse_key_info(doc.root_element()),
1788            Err(ParseError::InvalidStructure(_))
1789        ));
1790    }
1791
1792    #[test]
1793    fn parse_rsa_key_value_rejects_nested_crypto_binary() {
1794        // CryptoBinary values are text-only and must not hide extension elements.
1795        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1796            <KeyValue><RSAKeyValue>
1797                <Modulus><chunk>AQID</chunk></Modulus><Exponent>AQAB</Exponent>
1798            </RSAKeyValue></KeyValue>
1799        </KeyInfo>"#;
1800        let doc = Document::parse(xml).unwrap();
1801
1802        assert!(matches!(
1803            parse_key_info(doc.root_element()),
1804            Err(ParseError::InvalidStructure(_))
1805        ));
1806    }
1807
1808    #[test]
1809    fn parse_rsa_key_value_rejects_malformed_base64() {
1810        // Malformed key parameters must be processing errors, not unresolved keys.
1811        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1812            <KeyValue><RSAKeyValue>
1813                <Modulus>%%%%</Modulus><Exponent>AQAB</Exponent>
1814            </RSAKeyValue></KeyValue>
1815        </KeyInfo>"#;
1816        let doc = Document::parse(xml).unwrap();
1817
1818        assert!(matches!(
1819            parse_key_info(doc.root_element()),
1820            Err(ParseError::Base64(_))
1821        ));
1822    }
1823
1824    #[test]
1825    fn parse_rsa_key_value_rejects_oversized_exponent_before_decode() {
1826        // Bound normalized text before allocation or integer construction.
1827        let exponent = "A".repeat(MAX_RSA_EXPONENT_LEN.div_ceil(3) * 4 + 1);
1828        let xml = format!(
1829            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1830                <KeyValue><RSAKeyValue>
1831                    <Modulus>AQID</Modulus><Exponent>{exponent}</Exponent>
1832                </RSAKeyValue></KeyValue>
1833            </KeyInfo>"#
1834        );
1835        let doc = Document::parse(&xml).unwrap();
1836
1837        assert!(matches!(
1838            parse_key_info(doc.root_element()),
1839            Err(ParseError::InvalidStructure(_))
1840        ));
1841    }
1842
1843    #[test]
1844    fn parse_key_info_ignores_unknown_children() {
1845        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1846            <Foo>bar</Foo>
1847            <KeyName>ok</KeyName>
1848        </KeyInfo>"#;
1849        let doc = Document::parse(xml).unwrap();
1850
1851        let key_info = parse_key_info(doc.root_element()).unwrap();
1852        assert_eq!(key_info.sources, vec![KeyInfoSource::KeyName("ok".into())]);
1853    }
1854
1855    #[test]
1856    fn parse_key_info_keyvalue_requires_single_child() {
1857        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1858            <KeyValue/>
1859        </KeyInfo>"#;
1860        let doc = Document::parse(xml).unwrap();
1861
1862        let err = parse_key_info(doc.root_element()).unwrap_err();
1863        assert!(matches!(err, ParseError::InvalidStructure(_)));
1864    }
1865
1866    #[test]
1867    fn parse_key_info_accepts_empty_x509data() {
1868        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1869            <X509Data/>
1870        </KeyInfo>"#;
1871        let doc = Document::parse(xml).unwrap();
1872
1873        let key_info = parse_key_info(doc.root_element()).unwrap();
1874        assert_eq!(
1875            key_info.sources,
1876            vec![KeyInfoSource::X509Data(X509DataInfo::default())]
1877        );
1878    }
1879
1880    #[test]
1881    fn parse_key_info_rejects_unknown_xmlsig_child_in_x509data() {
1882        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1883            <X509Data>
1884                <Foo/>
1885            </X509Data>
1886        </KeyInfo>"#;
1887        let doc = Document::parse(xml).unwrap();
1888
1889        let err = parse_key_info(doc.root_element()).unwrap_err();
1890        assert!(matches!(err, ParseError::InvalidStructure(_)));
1891    }
1892
1893    #[test]
1894    fn parse_key_info_rejects_unknown_xmlsig11_child_in_x509data() {
1895        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
1896                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
1897            <X509Data>
1898                <dsig11:Foo/>
1899            </X509Data>
1900        </KeyInfo>"#;
1901        let doc = Document::parse(xml).unwrap();
1902
1903        let err = parse_key_info(doc.root_element()).unwrap_err();
1904        assert!(matches!(err, ParseError::InvalidStructure(_)));
1905    }
1906
1907    #[test]
1908    fn parse_key_info_rejects_x509_issuer_serial_without_required_children() {
1909        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1910            <X509Data>
1911                <X509IssuerSerial>
1912                    <X509IssuerName>CN=CA</X509IssuerName>
1913                </X509IssuerSerial>
1914            </X509Data>
1915        </KeyInfo>"#;
1916        let doc = Document::parse(xml).unwrap();
1917
1918        let err = parse_key_info(doc.root_element()).unwrap_err();
1919        assert!(matches!(err, ParseError::InvalidStructure(_)));
1920    }
1921
1922    #[test]
1923    fn parse_key_info_rejects_x509_issuer_serial_with_duplicate_issuer_name() {
1924        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1925            <X509Data>
1926                <X509IssuerSerial>
1927                    <X509IssuerName>CN=CA-1</X509IssuerName>
1928                    <X509IssuerName>CN=CA-2</X509IssuerName>
1929                    <X509SerialNumber>42</X509SerialNumber>
1930                </X509IssuerSerial>
1931            </X509Data>
1932        </KeyInfo>"#;
1933        let doc = Document::parse(xml).unwrap();
1934
1935        let err = parse_key_info(doc.root_element()).unwrap_err();
1936        assert!(matches!(err, ParseError::InvalidStructure(_)));
1937    }
1938
1939    #[test]
1940    fn parse_key_info_rejects_x509_issuer_serial_with_duplicate_serial_number() {
1941        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1942            <X509Data>
1943                <X509IssuerSerial>
1944                    <X509IssuerName>CN=CA</X509IssuerName>
1945                    <X509SerialNumber>1</X509SerialNumber>
1946                    <X509SerialNumber>2</X509SerialNumber>
1947                </X509IssuerSerial>
1948            </X509Data>
1949        </KeyInfo>"#;
1950        let doc = Document::parse(xml).unwrap();
1951
1952        let err = parse_key_info(doc.root_element()).unwrap_err();
1953        assert!(matches!(err, ParseError::InvalidStructure(_)));
1954    }
1955
1956    #[test]
1957    fn parse_key_info_rejects_x509_issuer_serial_with_whitespace_only_values() {
1958        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1959            <X509Data>
1960                <X509IssuerSerial>
1961                    <X509IssuerName>   </X509IssuerName>
1962                    <X509SerialNumber>
1963                        
1964                    </X509SerialNumber>
1965                </X509IssuerSerial>
1966            </X509Data>
1967        </KeyInfo>"#;
1968        let doc = Document::parse(xml).unwrap();
1969
1970        let err = parse_key_info(doc.root_element()).unwrap_err();
1971        assert!(matches!(err, ParseError::InvalidStructure(_)));
1972    }
1973
1974    #[test]
1975    fn parse_key_info_rejects_x509_issuer_serial_with_wrong_child_order() {
1976        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1977            <X509Data>
1978                <X509IssuerSerial>
1979                    <X509SerialNumber>42</X509SerialNumber>
1980                    <X509IssuerName>CN=CA</X509IssuerName>
1981                </X509IssuerSerial>
1982            </X509Data>
1983        </KeyInfo>"#;
1984        let doc = Document::parse(xml).unwrap();
1985
1986        let err = parse_key_info(doc.root_element()).unwrap_err();
1987        assert!(matches!(err, ParseError::InvalidStructure(_)));
1988    }
1989
1990    #[test]
1991    fn parse_key_info_rejects_x509_issuer_serial_with_extra_child_element() {
1992        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
1993                              xmlns:foo="urn:example:foo">
1994            <X509Data>
1995                <X509IssuerSerial>
1996                    <X509IssuerName>CN=CA</X509IssuerName>
1997                    <X509SerialNumber>42</X509SerialNumber>
1998                    <foo:Extra/>
1999                </X509IssuerSerial>
2000            </X509Data>
2001        </KeyInfo>"#;
2002        let doc = Document::parse(xml).unwrap();
2003
2004        let err = parse_key_info(doc.root_element()).unwrap_err();
2005        assert!(matches!(err, ParseError::InvalidStructure(_)));
2006    }
2007
2008    #[test]
2009    fn parse_key_info_rejects_x509_digest_without_algorithm() {
2010        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2011                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2012            <X509Data>
2013                <dsig11:X509Digest>AQID</dsig11:X509Digest>
2014            </X509Data>
2015        </KeyInfo>"#;
2016        let doc = Document::parse(xml).unwrap();
2017
2018        let err = parse_key_info(doc.root_element()).unwrap_err();
2019        assert!(matches!(err, ParseError::InvalidStructure(_)));
2020    }
2021
2022    #[test]
2023    fn parse_key_info_rejects_invalid_x509_certificate_base64() {
2024        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2025            <X509Data>
2026                <X509Certificate>%%%invalid%%%</X509Certificate>
2027            </X509Data>
2028        </KeyInfo>"#;
2029        let doc = Document::parse(xml).unwrap();
2030
2031        let err = parse_key_info(doc.root_element()).unwrap_err();
2032        assert!(matches!(err, ParseError::Base64(_)));
2033    }
2034
2035    #[test]
2036    fn parse_key_info_rejects_x509_data_exceeding_entry_budget() {
2037        let subjects = (0..(MAX_X509_DATA_ENTRY_COUNT + 1))
2038            .map(|idx| format!("<X509SubjectName>CN={idx}</X509SubjectName>"))
2039            .collect::<Vec<_>>()
2040            .join("");
2041        let xml = format!(
2042            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{subjects}</X509Data></KeyInfo>"
2043        );
2044        let doc = Document::parse(&xml).unwrap();
2045
2046        let err = parse_key_info(doc.root_element()).unwrap_err();
2047        assert!(matches!(err, ParseError::InvalidStructure(_)));
2048    }
2049
2050    #[test]
2051    fn parse_key_info_rejects_x509_data_exceeding_total_binary_budget() {
2052        let payload = base64::engine::general_purpose::STANDARD.encode(vec![0u8; 190_000]);
2053        let entries = (0..6)
2054            .map(|_| format!("<X509SKI>{payload}</X509SKI>"))
2055            .collect::<Vec<_>>()
2056            .join("");
2057        let xml = format!(
2058            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{entries}</X509Data></KeyInfo>"
2059        );
2060        let doc = Document::parse(&xml).unwrap();
2061
2062        let err = parse_key_info(doc.root_element()).unwrap_err();
2063        assert!(matches!(err, ParseError::InvalidStructure(_)));
2064    }
2065
2066    #[test]
2067    fn parse_key_info_rejects_x509_certificate_with_invalid_der() {
2068        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2069            <X509Data>
2070                <X509Certificate>AQID</X509Certificate>
2071            </X509Data>
2072        </KeyInfo>"#;
2073        let doc = Document::parse(xml).unwrap();
2074
2075        let err = parse_key_info(doc.root_element()).unwrap_err();
2076        assert!(matches!(err, ParseError::InvalidStructure(_)));
2077    }
2078
2079    #[test]
2080    fn parse_key_info_rejects_x509_certificate_with_trailing_der_bytes() {
2081        let mut cert = base64::engine::general_purpose::STANDARD
2082            .decode(fixture_rsa_cert_base64())
2083            .unwrap();
2084        cert.extend_from_slice(&[0x00, 0x01]);
2085        let cert_base64 = base64::engine::general_purpose::STANDARD.encode(cert);
2086        let xml = format!(
2087            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2088                <X509Data>
2089                    <X509Certificate>{cert_base64}</X509Certificate>
2090                </X509Data>
2091            </KeyInfo>"#
2092        );
2093        let doc = Document::parse(&xml).unwrap();
2094
2095        let err = parse_key_info(doc.root_element()).unwrap_err();
2096        assert!(matches!(err, ParseError::InvalidStructure(_)));
2097    }
2098
2099    #[test]
2100    fn parse_key_info_marks_unsupported_spki_algorithm_as_unsupported() {
2101        let xml = include_str!(
2102            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt.xml"
2103        );
2104        let doc = Document::parse(xml).unwrap();
2105        let key_info_node = doc
2106            .descendants()
2107            .find(|node| {
2108                node.is_element()
2109                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
2110                    && node.tag_name().name() == "KeyInfo"
2111            })
2112            .expect("fixture must contain ds:KeyInfo");
2113
2114        let key_info = parse_key_info(key_info_node).expect("KeyInfo parse should succeed");
2115        let x509_info = match &key_info.sources[0] {
2116            KeyInfoSource::X509Data(x509) => x509,
2117            other => panic!("expected X509Data source, got {other:?}"),
2118        };
2119        assert_eq!(x509_info.certificates.len(), 1);
2120        assert_eq!(x509_info.parsed_certificates.len(), 1);
2121        assert_eq!(x509_info.certificate_chain, vec![0]);
2122        let parsed_cert = &x509_info.parsed_certificates[0];
2123        assert!(!parsed_cert.subject_dn.is_empty());
2124        assert!(!parsed_cert.issuer_dn.is_empty());
2125        assert!(parsed_cert.subject_key_identifier.is_some());
2126        assert!(matches!(
2127            parsed_cert.public_key,
2128            X509PublicKeyInfo::Unsupported { .. }
2129        ));
2130    }
2131
2132    #[test]
2133    fn parse_key_info_orders_x509_certificate_chain_from_signing_cert() {
2134        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
2135        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
2136        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
2137        let xml = format!(
2138            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2139                <X509Data>
2140                    <X509Certificate>{root}</X509Certificate>
2141                    <X509Certificate>{intermediate}</X509Certificate>
2142                    <X509Certificate>{leaf}</X509Certificate>
2143                </X509Data>
2144            </KeyInfo>"#
2145        );
2146        let doc = Document::parse(&xml).unwrap();
2147
2148        let key_info = parse_key_info(doc.root_element()).unwrap();
2149        let x509_info = match &key_info.sources[0] {
2150            KeyInfoSource::X509Data(x509) => x509,
2151            other => panic!("expected X509Data source, got {other:?}"),
2152        };
2153
2154        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
2155    }
2156
2157    #[test]
2158    fn parse_key_info_uses_issuer_serial_to_select_x509_signing_certificate() {
2159        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
2160        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
2161        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
2162        let xml = format!(
2163            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2164                <X509Data>
2165                    <X509IssuerSerial>
2166                        <X509IssuerName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com</X509IssuerName>
2167                        <X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
2168                    </X509IssuerSerial>
2169                    <X509Certificate>{root}</X509Certificate>
2170                    <X509Certificate>{intermediate}</X509Certificate>
2171                    <X509Certificate>{leaf}</X509Certificate>
2172                </X509Data>
2173            </KeyInfo>"#
2174        );
2175        let doc = Document::parse(&xml).unwrap();
2176
2177        let key_info = parse_key_info(doc.root_element()).unwrap();
2178        let x509_info = match &key_info.sources[0] {
2179            KeyInfoSource::X509Data(x509) => x509,
2180            other => panic!("expected X509Data source, got {other:?}"),
2181        };
2182
2183        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
2184    }
2185
2186    #[test]
2187    fn parse_key_info_allows_selectors_for_multiple_chain_members() {
2188        // X509Data may identify both the signing leaf and another certificate
2189        // in its chain; the unique leaf must remain the signing certificate.
2190        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
2191        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
2192        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
2193        let xml = format!(
2194            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2195                <X509Data>
2196                    <X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
2197                    <X509SKI>0X0XrEVCio75sBcl1TxymJ2IOiU=</X509SKI>
2198                    <X509Certificate>{root}</X509Certificate>
2199                    <X509Certificate>{intermediate}</X509Certificate>
2200                    <X509Certificate>{leaf}</X509Certificate>
2201                </X509Data>
2202            </KeyInfo>"#
2203        );
2204        let doc = Document::parse(&xml).unwrap();
2205
2206        let key_info = parse_key_info(doc.root_element()).unwrap();
2207        let x509_info = match &key_info.sources[0] {
2208            KeyInfoSource::X509Data(x509) => x509,
2209            other => panic!("expected X509Data source, got {other:?}"),
2210        };
2211
2212        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
2213    }
2214
2215    #[test]
2216    fn parse_key_info_uses_decimal_issuer_serial_to_select_x509_signing_certificate() {
2217        assert_eq!(
2218            x509_serial_decimal_to_hex("680572598617295163017172295025714171905498632019")
2219                .as_deref(),
2220            Some("7735EE487F6862DAF1B3956D961CCB0FA6F34F53")
2221        );
2222        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
2223        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
2224        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
2225        let other_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
2226        let xml = format!(
2227            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2228                <X509Data>
2229                    <X509IssuerSerial>
2230                        <X509IssuerName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com</X509IssuerName>
2231                        <X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
2232                    </X509IssuerSerial>
2233                    <X509Certificate>{root}</X509Certificate>
2234                    <X509Certificate>{intermediate}</X509Certificate>
2235                    <X509Certificate>{leaf}</X509Certificate>
2236                    <X509Certificate>{other_leaf}</X509Certificate>
2237                </X509Data>
2238            </KeyInfo>"#
2239        );
2240        let doc = Document::parse(&xml).unwrap();
2241
2242        let key_info = parse_key_info(doc.root_element()).unwrap();
2243        let x509_info = match &key_info.sources[0] {
2244            KeyInfoSource::X509Data(x509) => x509,
2245            other => panic!("expected X509Data source, got {other:?}"),
2246        };
2247
2248        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
2249    }
2250
2251    #[test]
2252    fn parse_key_info_rejects_ambiguous_x509_signing_certificate_candidates() {
2253        let first_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
2254        let second_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
2255        let xml = format!(
2256            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2257                <X509Data>
2258                    <X509Certificate>{first_leaf}</X509Certificate>
2259                    <X509Certificate>{second_leaf}</X509Certificate>
2260                </X509Data>
2261            </KeyInfo>"#
2262        );
2263        let doc = Document::parse(&xml).unwrap();
2264
2265        let err = parse_key_info(doc.root_element()).unwrap_err();
2266        assert!(matches!(err, ParseError::InvalidStructure(_)));
2267    }
2268
2269    #[test]
2270    fn parse_key_info_rejects_unmatched_x509_lookup_identifier() {
2271        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
2272        let xml = format!(
2273            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2274                <X509Data>
2275                    <X509SubjectName>CN=Not The Embedded Certificate</X509SubjectName>
2276                    <X509Certificate>{cert}</X509Certificate>
2277                </X509Data>
2278            </KeyInfo>"#
2279        );
2280        let doc = Document::parse(&xml).unwrap();
2281
2282        let err = parse_key_info(doc.root_element()).unwrap_err();
2283        assert!(
2284            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
2285        );
2286    }
2287
2288    #[test]
2289    fn parse_key_info_rejects_partially_matched_selector_category() {
2290        // Every selector value is an asserted lookup constraint; one matching
2291        // SubjectName must not mask another value absent from the chain.
2292        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
2293        let xml = format!(
2294            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2295                <X509Data>
2296                    <X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
2297                    <X509SubjectName>CN=Not In The Embedded Chain</X509SubjectName>
2298                    <X509Certificate>{cert}</X509Certificate>
2299                </X509Data>
2300            </KeyInfo>"#
2301        );
2302        let doc = Document::parse(&xml).unwrap();
2303
2304        let err = parse_key_info(doc.root_element()).unwrap_err();
2305        assert!(
2306            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
2307        );
2308    }
2309
2310    #[test]
2311    fn parse_key_info_rejects_malformed_issuer_serial_even_with_matching_subject() {
2312        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
2313        let xml = format!(
2314            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2315                <X509Data>
2316                    <X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
2317                    <X509IssuerSerial>
2318                        <X509IssuerName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com</X509IssuerName>
2319                        <X509SerialNumber>not-a-decimal-serial</X509SerialNumber>
2320                    </X509IssuerSerial>
2321                    <X509Certificate>{cert}</X509Certificate>
2322                </X509Data>
2323            </KeyInfo>"#
2324        );
2325        let doc = Document::parse(&xml).unwrap();
2326
2327        let err = parse_key_info(doc.root_element()).unwrap_err();
2328        assert!(
2329            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
2330        );
2331    }
2332
2333    #[test]
2334    fn parse_key_info_rejects_unmatched_ski_even_with_matching_subject() {
2335        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
2336        let xml = format!(
2337            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2338                <X509Data>
2339                    <X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
2340                    <X509SKI>AQIDBA==</X509SKI>
2341                    <X509Certificate>{cert}</X509Certificate>
2342                </X509Data>
2343            </KeyInfo>"#
2344        );
2345        let doc = Document::parse(&xml).unwrap();
2346
2347        let err = parse_key_info(doc.root_element()).unwrap_err();
2348        assert!(
2349            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
2350        );
2351    }
2352
2353    #[test]
2354    fn parse_key_info_rejects_lookup_hints_for_different_certificates() {
2355        let first_cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
2356        let second_cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
2357        let xml = format!(
2358            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2359                <X509Data>
2360                    <X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
2361                    <X509SKI>60zMLKCfzQ3qnXAzABzRNpdgQ8Q=</X509SKI>
2362                    <X509Certificate>{first_cert}</X509Certificate>
2363                    <X509Certificate>{second_cert}</X509Certificate>
2364                </X509Data>
2365            </KeyInfo>"#
2366        );
2367        let doc = Document::parse(&xml).unwrap();
2368
2369        let err = parse_key_info(doc.root_element()).unwrap_err();
2370        assert!(
2371            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers match multiple certificates"))
2372        );
2373    }
2374
2375    #[test]
2376    fn build_x509_certificate_chain_rejects_chain_exceeding_max_depth() {
2377        let parsed_certificates = (0..=MAX_X509_CHAIN_DEPTH)
2378            .map(|idx| ParsedX509Certificate {
2379                subject_dn: format!("CN=cert-{idx}"),
2380                issuer_dn: if idx == MAX_X509_CHAIN_DEPTH {
2381                    format!("CN=cert-{idx}")
2382                } else {
2383                    format!("CN=cert-{}", idx + 1)
2384                },
2385                serial_number: vec![u8::try_from(idx).unwrap()],
2386                serial_number_hex: format!("{idx:02X}"),
2387                subject_key_identifier: None,
2388                public_key: X509PublicKeyInfo::Unsupported {
2389                    algorithm_oid: "1.2.3.4".into(),
2390                },
2391            })
2392            .collect();
2393        let info = X509DataInfo {
2394            parsed_certificates,
2395            ..X509DataInfo::default()
2396        };
2397
2398        let err = build_x509_certificate_chain(&info).unwrap_err();
2399        assert!(
2400            matches!(err, ParseError::InvalidStructure(message) if message.contains("maximum depth"))
2401        );
2402    }
2403
2404    #[test]
2405    fn x509_serial_hex_strips_der_sign_extension_zeroes() {
2406        assert_eq!(format_x509_serial_value_hex(&[0x00, 0xFF]), "FF");
2407        assert_eq!(format_x509_serial_value_hex(&[0x00, 0x7F]), "7F");
2408        assert_eq!(format_x509_serial_value_hex(&[0x00, 0x00]), "00");
2409    }
2410
2411    #[test]
2412    fn parse_key_info_accepts_large_textual_x509_entries_within_entry_budget() {
2413        let issuer_name = "C".repeat(MAX_X509_ISSUER_NAME_TEXT_LEN);
2414        let serial_number = "7".repeat(MAX_X509_SERIAL_NUMBER_TEXT_LEN);
2415        let issuer_serials = (0..52)
2416            .map(|_| {
2417                format!(
2418                    "<X509IssuerSerial><X509IssuerName>{issuer_name}</X509IssuerName><X509SerialNumber>{serial_number}</X509SerialNumber></X509IssuerSerial>"
2419                )
2420            })
2421            .collect::<Vec<_>>()
2422            .join("");
2423        let xml = format!(
2424            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{issuer_serials}</X509Data></KeyInfo>"
2425        );
2426        let doc = Document::parse(&xml).unwrap();
2427
2428        let key_info = parse_key_info(doc.root_element()).unwrap();
2429        let parsed = match &key_info.sources[0] {
2430            KeyInfoSource::X509Data(x509) => x509,
2431            _ => panic!("expected X509Data source"),
2432        };
2433        assert_eq!(parsed.issuer_serials.len(), 52);
2434    }
2435
2436    #[test]
2437    fn parse_key_info_accepts_x509data_with_only_foreign_namespace_children() {
2438        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2439                              xmlns:foo="urn:example:foo">
2440            <X509Data>
2441                <foo:Bar/>
2442            </X509Data>
2443        </KeyInfo>"#;
2444        let doc = Document::parse(xml).unwrap();
2445
2446        let key_info = parse_key_info(doc.root_element()).unwrap();
2447        assert_eq!(
2448            key_info.sources,
2449            vec![KeyInfoSource::X509Data(X509DataInfo::default())]
2450        );
2451    }
2452
2453    #[test]
2454    fn parse_key_info_der_encoded_key_value_rejects_invalid_base64() {
2455        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2456                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2457            <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
2458        </KeyInfo>"#;
2459        let doc = Document::parse(xml).unwrap();
2460
2461        let err = parse_key_info(doc.root_element()).unwrap_err();
2462        assert!(matches!(err, ParseError::Base64(_)));
2463    }
2464
2465    #[test]
2466    fn parse_key_info_der_encoded_key_value_accepts_xml_whitespace() {
2467        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2468                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2469            <dsig11:DEREncodedKeyValue>
2470                AQID
2471                BA==
2472            </dsig11:DEREncodedKeyValue>
2473        </KeyInfo>"#;
2474        let doc = Document::parse(xml).unwrap();
2475
2476        let key_info = parse_key_info(doc.root_element()).unwrap();
2477        assert_eq!(
2478            key_info.sources,
2479            vec![KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])]
2480        );
2481    }
2482
2483    #[test]
2484    fn parse_key_info_dispatches_dsig11_ec_keyvalue() {
2485        let public_key = "BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=";
2486        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2487                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2488            <KeyValue>
2489                <dsig11:ECKeyValue>
2490                    <dsig11:NamedCurve URI="urn:oid:1.2.840.10045.3.1.7"/>
2491                    <dsig11:PublicKey>BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=</dsig11:PublicKey>
2492                </dsig11:ECKeyValue>
2493            </KeyValue>
2494        </KeyInfo>"#;
2495        let doc = Document::parse(xml).unwrap();
2496        let expected_public_key = base64::engine::general_purpose::STANDARD
2497            .decode(public_key)
2498            .expect("fixture EC point must be valid base64");
2499
2500        let key_info = parse_key_info(doc.root_element()).unwrap();
2501        assert_eq!(
2502            key_info.sources,
2503            vec![KeyInfoSource::KeyValue(KeyValueInfo::Ec {
2504                curve_oid: "1.2.840.10045.3.1.7".into(),
2505                public_key: expected_public_key,
2506            })]
2507        );
2508    }
2509
2510    #[test]
2511    fn parse_ec_key_value_accepts_bare_curve_oid() {
2512        use base64::Engine;
2513
2514        let encoded_public_key = "BO/yd/OZzDfjX4qivDY/vsUIuh6KWAxoxW5P4ukvwd+T6pVljWsX2UBJNNy5MdhTwB8e2YwB8kUbJwdsAS/XGi/fz8unFrs+lVlAgIs6s/xBYFbfUoRiAacD2SpVDe6XBA==";
2515        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2516                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2517            <KeyValue>
2518                <dsig11:ECKeyValue>
2519                    <dsig11:NamedCurve URI="1.3.132.0.34"/>
2520                    <dsig11:PublicKey>BO/yd/OZzDfjX4qivDY/vsUIuh6KWAxoxW5P4ukvwd+T6pVljWsX2UBJNNy5MdhTwB8e2YwB8kUbJwdsAS/XGi/fz8unFrs+lVlAgIs6s/xBYFbfUoRiAacD2SpVDe6XBA==</dsig11:PublicKey>
2521                </dsig11:ECKeyValue>
2522            </KeyValue>
2523        </KeyInfo>"#;
2524        let doc = Document::parse(xml).unwrap();
2525        let expected_public_key = base64::engine::general_purpose::STANDARD
2526            .decode(encoded_public_key)
2527            .unwrap();
2528
2529        let sources = parse_key_info(doc.root_element()).unwrap().sources;
2530
2531        assert!(matches!(
2532            &sources[0],
2533            KeyInfoSource::KeyValue(KeyValueInfo::Ec { curve_oid, public_key })
2534                if curve_oid == EC_P384_OID && public_key == &expected_public_key
2535        ));
2536    }
2537
2538    #[test]
2539    fn parse_ec_key_value_marks_ec_parameters_as_unsupported() {
2540        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2541                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2542            <KeyValue>
2543                <dsig11:ECKeyValue>
2544                    <dsig11:ECParameters/>
2545                    <dsig11:PublicKey>BA==</dsig11:PublicKey>
2546                </dsig11:ECKeyValue>
2547            </KeyValue>
2548        </KeyInfo>"#;
2549        let doc = Document::parse(xml).unwrap();
2550
2551        let key_info = parse_key_info(doc.root_element()).unwrap();
2552        assert_eq!(
2553            key_info.sources,
2554            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
2555                namespace: Some(XMLDSIG11_NS.to_string()),
2556                local_name: "ECKeyValue".into(),
2557            })]
2558        );
2559    }
2560
2561    #[test]
2562    fn parse_ec_key_value_marks_unsupported_curve_as_unsupported() {
2563        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2564                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2565            <KeyValue>
2566                <dsig11:ECKeyValue>
2567                    <dsig11:NamedCurve URI="urn:oid:1.3.132.0.35"/>
2568                    <dsig11:PublicKey>BA==</dsig11:PublicKey>
2569                </dsig11:ECKeyValue>
2570            </KeyValue>
2571        </KeyInfo>"#;
2572        let doc = Document::parse(xml).unwrap();
2573
2574        let key_info = parse_key_info(doc.root_element()).unwrap();
2575        assert_eq!(
2576            key_info.sources,
2577            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
2578                namespace: Some(XMLDSIG11_NS.to_string()),
2579                local_name: "ECKeyValue".into(),
2580            })]
2581        );
2582    }
2583
2584    #[test]
2585    fn parse_ec_key_value_marks_missing_named_curve_uri_invalid() {
2586        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2587                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2588            <KeyValue>
2589                <dsig11:ECKeyValue>
2590                    <dsig11:NamedCurve/>
2591                    <dsig11:PublicKey>BA==</dsig11:PublicKey>
2592                </dsig11:ECKeyValue>
2593            </KeyValue>
2594        </KeyInfo>"#;
2595        let doc = Document::parse(xml).unwrap();
2596
2597        let key_info = parse_key_info(doc.root_element()).unwrap();
2598        assert_eq!(
2599            key_info.sources,
2600            vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)]
2601        );
2602    }
2603
2604    #[test]
2605    fn parse_ec_key_value_marks_reordered_children_invalid() {
2606        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2607                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2608            <KeyValue>
2609                <dsig11:ECKeyValue>
2610                    <dsig11:PublicKey>BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=</dsig11:PublicKey>
2611                    <dsig11:NamedCurve URI="urn:oid:1.2.840.10045.3.1.7"/>
2612                </dsig11:ECKeyValue>
2613            </KeyValue>
2614        </KeyInfo>"#;
2615        let doc = Document::parse(xml).unwrap();
2616
2617        let key_info = parse_key_info(doc.root_element()).unwrap();
2618        assert_eq!(
2619            key_info.sources,
2620            vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)]
2621        );
2622    }
2623
2624    #[test]
2625    fn parse_ec_key_value_marks_non_uncompressed_point_invalid() {
2626        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2627                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2628            <KeyValue>
2629                <dsig11:ECKeyValue>
2630                    <dsig11:NamedCurve URI="urn:oid:1.2.840.10045.3.1.7"/>
2631                    <dsig11:PublicKey>Ap/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=</dsig11:PublicKey>
2632                </dsig11:ECKeyValue>
2633            </KeyValue>
2634        </KeyInfo>"#;
2635        let doc = Document::parse(xml).unwrap();
2636
2637        let key_info = parse_key_info(doc.root_element()).unwrap();
2638        assert_eq!(
2639            key_info.sources,
2640            vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)]
2641        );
2642    }
2643
2644    #[test]
2645    fn parse_key_info_marks_ds_namespace_ec_keyvalue_as_unsupported() {
2646        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2647            <KeyValue>
2648                <ECKeyValue/>
2649            </KeyValue>
2650        </KeyInfo>"#;
2651        let doc = Document::parse(xml).unwrap();
2652
2653        let key_info = parse_key_info(doc.root_element()).unwrap();
2654        assert_eq!(
2655            key_info.sources,
2656            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
2657                namespace: Some(XMLDSIG_NS.to_string()),
2658                local_name: "ECKeyValue".into(),
2659            })]
2660        );
2661    }
2662
2663    #[test]
2664    fn parse_key_info_keeps_unsupported_keyvalue_child_as_marker() {
2665        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2666            <KeyValue>
2667                <DSAKeyValue/>
2668            </KeyValue>
2669        </KeyInfo>"#;
2670        let doc = Document::parse(xml).unwrap();
2671
2672        let key_info = parse_key_info(doc.root_element()).unwrap();
2673        assert_eq!(
2674            key_info.sources,
2675            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
2676                namespace: Some(XMLDSIG_NS.to_string()),
2677                local_name: "DSAKeyValue".into(),
2678            })]
2679        );
2680    }
2681
2682    #[test]
2683    fn parse_key_info_rejects_keyname_with_child_elements() {
2684        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2685            <KeyName>ok<foo/></KeyName>
2686        </KeyInfo>"#;
2687        let doc = Document::parse(xml).unwrap();
2688
2689        let err = parse_key_info(doc.root_element()).unwrap_err();
2690        assert!(matches!(err, ParseError::InvalidStructure(_)));
2691    }
2692
2693    #[test]
2694    fn parse_key_info_preserves_keyname_text_without_trimming() {
2695        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2696            <KeyName>  signing key  </KeyName>
2697        </KeyInfo>"#;
2698        let doc = Document::parse(xml).unwrap();
2699
2700        let key_info = parse_key_info(doc.root_element()).unwrap();
2701        assert_eq!(
2702            key_info.sources,
2703            vec![KeyInfoSource::KeyName("  signing key  ".into())]
2704        );
2705    }
2706
2707    #[test]
2708    fn parse_key_info_rejects_oversized_keyname_text() {
2709        let oversized = "A".repeat(4097);
2710        let xml = format!(
2711            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><KeyName>{oversized}</KeyName></KeyInfo>"
2712        );
2713        let doc = Document::parse(&xml).unwrap();
2714
2715        let err = parse_key_info(doc.root_element()).unwrap_err();
2716        assert!(matches!(err, ParseError::InvalidStructure(_)));
2717    }
2718
2719    #[test]
2720    fn parse_key_info_rejects_non_whitespace_mixed_content() {
2721        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">oops<KeyName>k</KeyName></KeyInfo>"#;
2722        let doc = Document::parse(xml).unwrap();
2723
2724        let err = parse_key_info(doc.root_element()).unwrap_err();
2725        assert!(matches!(err, ParseError::InvalidStructure(_)));
2726    }
2727
2728    #[test]
2729    fn parse_key_info_rejects_nbsp_as_non_xml_whitespace_mixed_content() {
2730        let xml = "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\u{00A0}<KeyName>k</KeyName></KeyInfo>";
2731        let doc = Document::parse(xml).unwrap();
2732
2733        let err = parse_key_info(doc.root_element()).unwrap_err();
2734        assert!(matches!(err, ParseError::InvalidStructure(_)));
2735    }
2736
2737    #[test]
2738    fn parse_key_info_der_encoded_key_value_rejects_oversized_payload() {
2739        let oversized =
2740            base64::engine::general_purpose::STANDARD
2741                .encode(vec![0u8; MAX_DER_ENCODED_KEY_VALUE_LEN + 1]);
2742        let xml = format!(
2743            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:dsig11=\"http://www.w3.org/2009/xmldsig11#\"><dsig11:DEREncodedKeyValue>{oversized}</dsig11:DEREncodedKeyValue></KeyInfo>"
2744        );
2745        let doc = Document::parse(&xml).unwrap();
2746
2747        let err = parse_key_info(doc.root_element()).unwrap_err();
2748        assert!(matches!(err, ParseError::InvalidStructure(_)));
2749    }
2750
2751    #[test]
2752    fn parse_key_info_der_encoded_key_value_rejects_empty_payload() {
2753        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2754                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2755            <dsig11:DEREncodedKeyValue>
2756                
2757            </dsig11:DEREncodedKeyValue>
2758        </KeyInfo>"#;
2759        let doc = Document::parse(xml).unwrap();
2760
2761        let err = parse_key_info(doc.root_element()).unwrap_err();
2762        assert!(matches!(err, ParseError::InvalidStructure(_)));
2763    }
2764
2765    #[test]
2766    fn parse_key_info_der_encoded_key_value_non_xml_ascii_whitespace_is_not_parseable_xml() {
2767        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>";
2768        assert!(Document::parse(xml).is_err());
2769    }
2770
2771    // ── parse_signed_info: happy path ────────────────────────────────
2772
2773    #[test]
2774    fn parse_signed_info_rsa_sha256_with_reference() {
2775        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2776            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2777            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2778            <Reference URI="">
2779                <Transforms>
2780                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
2781                    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2782                </Transforms>
2783                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2784                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2785            </Reference>
2786        </SignedInfo>"#;
2787        let doc = Document::parse(xml).unwrap();
2788        let si = parse_signed_info(doc.root_element()).unwrap();
2789
2790        assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
2791        assert_eq!(si.references.len(), 1);
2792
2793        let r = &si.references[0];
2794        assert_eq!(r.uri.as_deref(), Some(""));
2795        assert_eq!(r.digest_method, DigestAlgorithm::Sha256);
2796        assert_eq!(r.digest_value, vec![0u8; 32]);
2797        assert_eq!(r.transforms.len(), 2);
2798    }
2799
2800    #[test]
2801    fn parse_signed_info_multiple_references() {
2802        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2803            <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
2804            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
2805            <Reference URI="#a">
2806                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2807                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2808            </Reference>
2809            <Reference URI="#b">
2810                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
2811                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2812            </Reference>
2813        </SignedInfo>"##;
2814        let doc = Document::parse(xml).unwrap();
2815        let si = parse_signed_info(doc.root_element()).unwrap();
2816
2817        assert_eq!(si.signature_method, SignatureAlgorithm::EcdsaP256Sha256);
2818        assert_eq!(si.references.len(), 2);
2819        assert_eq!(si.references[0].uri.as_deref(), Some("#a"));
2820        assert_eq!(si.references[0].digest_method, DigestAlgorithm::Sha256);
2821        assert_eq!(si.references[1].uri.as_deref(), Some("#b"));
2822        assert_eq!(si.references[1].digest_method, DigestAlgorithm::Sha1);
2823    }
2824
2825    #[test]
2826    fn parse_signed_info_rejects_too_many_references() {
2827        // Reference processing shares signature-wide resource budgets, so the
2828        // parser must bound cardinality before retaining attacker-controlled entries.
2829        let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
2830            .map(|index| {
2831                format!(
2832                    r##"<Reference URI="#item-{index}">
2833                        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2834                        <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2835                    </Reference>"##
2836                )
2837            })
2838            .collect::<String>();
2839        let xml = format!(
2840            r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2841                <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2842                <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2843                {references}
2844            </SignedInfo>"#
2845        );
2846        let document = Document::parse(&xml).expect("fixed oversized fixture must parse");
2847
2848        let error = parse_signed_info(document.root_element())
2849            .expect_err("the parser must reject the 65th Reference");
2850
2851        assert!(matches!(
2852            error,
2853            ParseError::TooManyReferences {
2854                max: MAX_REFERENCES_PER_SIGNATURE
2855            }
2856        ));
2857    }
2858
2859    #[test]
2860    fn parse_signed_info_bounds_xpath_expressions_across_references() {
2861        // Per-reference limits alone permit an attacker to retain and compile
2862        // thousands of XPath programs before signature verification begins.
2863        let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
2864            .repeat(64);
2865        let filter_transform = format!(
2866            r#"<Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</Transform>"#
2867        );
2868        let reference = |index, transforms: &str| {
2869            format!(
2870                r##"<Reference URI="#item-{index}">
2871                        <Transforms>{transforms}</Transforms>
2872                        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2873                        <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2874                    </Reference>"##
2875            )
2876        };
2877        let signed_info = |references: &str| {
2878            format!(
2879                r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2880                <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2881                <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2882                {references}
2883            </SignedInfo>"#
2884            )
2885        };
2886
2887        let max_reference = reference(0, &filter_transform.repeat(64));
2888        let boundary_xml = signed_info(&max_reference);
2889        let boundary_document =
2890            Document::parse(&boundary_xml).expect("fixed boundary fixture must parse");
2891        parse_signed_info(boundary_document.root_element())
2892            .expect("one maximum-shaped Reference must remain accepted");
2893
2894        let extra_transform = r#"<Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><XPath>true()</XPath></Transform>"#;
2895        let xml = signed_info(&format!("{max_reference}{}", reference(1, extra_transform)));
2896        let document = Document::parse(&xml).expect("fixed aggregate fixture must parse");
2897
2898        let error = parse_signed_info(document.root_element())
2899            .expect_err("signature-wide XPath expression count must be bounded");
2900
2901        assert!(
2902            error
2903                .to_string()
2904                .contains("signature-wide XPath expression budget")
2905        );
2906    }
2907
2908    #[test]
2909    fn parse_reference_without_transforms() {
2910        // Transforms element is optional
2911        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2912            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2913            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2914            <Reference URI="#obj">
2915                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2916                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2917            </Reference>
2918        </SignedInfo>"##;
2919        let doc = Document::parse(xml).unwrap();
2920        let si = parse_signed_info(doc.root_element()).unwrap();
2921
2922        assert!(si.references[0].transforms.is_empty());
2923    }
2924
2925    #[test]
2926    fn parse_reference_with_all_attributes() {
2927        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2928            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2929            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2930            <Reference URI="#data" Id="ref1" Type="http://www.w3.org/2000/09/xmldsig#Object">
2931                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2932                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2933            </Reference>
2934        </SignedInfo>"##;
2935        let doc = Document::parse(xml).unwrap();
2936        let si = parse_signed_info(doc.root_element()).unwrap();
2937        let r = &si.references[0];
2938
2939        assert_eq!(r.uri.as_deref(), Some("#data"));
2940        assert_eq!(r.id.as_deref(), Some("ref1"));
2941        assert_eq!(
2942            r.ref_type.as_deref(),
2943            Some("http://www.w3.org/2000/09/xmldsig#Object")
2944        );
2945    }
2946
2947    #[test]
2948    fn parse_reference_absent_uri() {
2949        // URI attribute is optional per spec
2950        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2951            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2952            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2953            <Reference>
2954                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2955                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2956            </Reference>
2957        </SignedInfo>"#;
2958        let doc = Document::parse(xml).unwrap();
2959        let si = parse_signed_info(doc.root_element()).unwrap();
2960        assert!(si.references[0].uri.is_none());
2961    }
2962
2963    #[test]
2964    fn parse_signed_info_preserves_inclusive_prefixes() {
2965        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2966                                 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
2967            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
2968                <ec:InclusiveNamespaces PrefixList="ds saml #default"/>
2969            </CanonicalizationMethod>
2970            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2971            <Reference URI="">
2972                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2973                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2974            </Reference>
2975        </SignedInfo>"#;
2976        let doc = Document::parse(xml).unwrap();
2977
2978        let si = parse_signed_info(doc.root_element()).unwrap();
2979        assert!(si.c14n_method.inclusive_prefixes().contains("ds"));
2980        assert!(si.c14n_method.inclusive_prefixes().contains("saml"));
2981        assert!(si.c14n_method.inclusive_prefixes().contains(""));
2982    }
2983
2984    // ── parse_signed_info: error cases ───────────────────────────────
2985
2986    #[test]
2987    fn missing_canonicalization_method() {
2988        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2989            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2990            <Reference URI="">
2991                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2992                <DigestValue>dGVzdA==</DigestValue>
2993            </Reference>
2994        </SignedInfo>"#;
2995        let doc = Document::parse(xml).unwrap();
2996        let result = parse_signed_info(doc.root_element());
2997        assert!(result.is_err());
2998        // SignatureMethod is first child but expected CanonicalizationMethod
2999        assert!(matches!(
3000            result.unwrap_err(),
3001            ParseError::InvalidStructure(_)
3002        ));
3003    }
3004
3005    #[test]
3006    fn missing_signature_method() {
3007        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3008            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3009            <Reference URI="">
3010                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3011                <DigestValue>dGVzdA==</DigestValue>
3012            </Reference>
3013        </SignedInfo>"#;
3014        let doc = Document::parse(xml).unwrap();
3015        let result = parse_signed_info(doc.root_element());
3016        assert!(result.is_err());
3017        // Reference is second child but expected SignatureMethod
3018        assert!(matches!(
3019            result.unwrap_err(),
3020            ParseError::InvalidStructure(_)
3021        ));
3022    }
3023
3024    #[test]
3025    fn no_references() {
3026        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3027            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3028            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3029        </SignedInfo>"#;
3030        let doc = Document::parse(xml).unwrap();
3031        let result = parse_signed_info(doc.root_element());
3032        assert!(matches!(
3033            result.unwrap_err(),
3034            ParseError::MissingElement {
3035                element: "Reference"
3036            }
3037        ));
3038    }
3039
3040    #[test]
3041    fn unsupported_c14n_algorithm() {
3042        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3043            <CanonicalizationMethod Algorithm="http://example.com/bogus-c14n"/>
3044            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3045            <Reference URI="">
3046                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3047                <DigestValue>dGVzdA==</DigestValue>
3048            </Reference>
3049        </SignedInfo>"#;
3050        let doc = Document::parse(xml).unwrap();
3051        let result = parse_signed_info(doc.root_element());
3052        assert!(matches!(
3053            result.unwrap_err(),
3054            ParseError::UnsupportedAlgorithm { .. }
3055        ));
3056    }
3057
3058    #[test]
3059    fn unsupported_signature_algorithm() {
3060        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3061            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3062            <SignatureMethod Algorithm="http://example.com/bogus-sign"/>
3063            <Reference URI="">
3064                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3065                <DigestValue>dGVzdA==</DigestValue>
3066            </Reference>
3067        </SignedInfo>"#;
3068        let doc = Document::parse(xml).unwrap();
3069        let result = parse_signed_info(doc.root_element());
3070        assert!(matches!(
3071            result.unwrap_err(),
3072            ParseError::UnsupportedAlgorithm { .. }
3073        ));
3074    }
3075
3076    #[test]
3077    fn unsupported_digest_algorithm() {
3078        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3079            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3080            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3081            <Reference URI="">
3082                <DigestMethod Algorithm="http://example.com/bogus-digest"/>
3083                <DigestValue>dGVzdA==</DigestValue>
3084            </Reference>
3085        </SignedInfo>"#;
3086        let doc = Document::parse(xml).unwrap();
3087        let result = parse_signed_info(doc.root_element());
3088        assert!(matches!(
3089            result.unwrap_err(),
3090            ParseError::UnsupportedAlgorithm { .. }
3091        ));
3092    }
3093
3094    #[test]
3095    fn missing_digest_method() {
3096        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3097            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3098            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3099            <Reference URI="">
3100                <DigestValue>dGVzdA==</DigestValue>
3101            </Reference>
3102        </SignedInfo>"#;
3103        let doc = Document::parse(xml).unwrap();
3104        let result = parse_signed_info(doc.root_element());
3105        // DigestValue is not DigestMethod
3106        assert!(result.is_err());
3107    }
3108
3109    #[test]
3110    fn missing_digest_value() {
3111        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3112            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3113            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3114            <Reference URI="">
3115                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3116            </Reference>
3117        </SignedInfo>"#;
3118        let doc = Document::parse(xml).unwrap();
3119        let result = parse_signed_info(doc.root_element());
3120        assert!(matches!(
3121            result.unwrap_err(),
3122            ParseError::MissingElement {
3123                element: "DigestValue"
3124            }
3125        ));
3126    }
3127
3128    #[test]
3129    fn invalid_base64_digest_value() {
3130        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3131            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3132            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3133            <Reference URI="">
3134                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3135                <DigestValue>!!!not-base64!!!</DigestValue>
3136            </Reference>
3137        </SignedInfo>"#;
3138        let doc = Document::parse(xml).unwrap();
3139        let result = parse_signed_info(doc.root_element());
3140        assert!(matches!(result.unwrap_err(), ParseError::Base64(_)));
3141    }
3142
3143    #[test]
3144    fn digest_value_length_must_match_digest_method() {
3145        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3146            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3147            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3148            <Reference URI="">
3149                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3150                <DigestValue>dGVzdA==</DigestValue>
3151            </Reference>
3152        </SignedInfo>"#;
3153        let doc = Document::parse(xml).unwrap();
3154
3155        let result = parse_signed_info(doc.root_element());
3156        assert!(matches!(
3157            result.unwrap_err(),
3158            ParseError::DigestLengthMismatch {
3159                algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
3160                expected: 32,
3161                actual: 4,
3162            }
3163        ));
3164    }
3165
3166    #[test]
3167    fn inclusive_prefixes_on_inclusive_c14n_is_rejected() {
3168        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3169                                 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
3170            <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">
3171                <ec:InclusiveNamespaces PrefixList="ds"/>
3172            </CanonicalizationMethod>
3173            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3174            <Reference URI="">
3175                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3176                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
3177            </Reference>
3178        </SignedInfo>"#;
3179        let doc = Document::parse(xml).unwrap();
3180
3181        let result = parse_signed_info(doc.root_element());
3182        assert!(matches!(
3183            result.unwrap_err(),
3184            ParseError::UnsupportedAlgorithm { .. }
3185        ));
3186    }
3187
3188    #[test]
3189    fn extra_element_after_digest_value() {
3190        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3191            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3192            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3193            <Reference URI="">
3194                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3195                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
3196                <Unexpected/>
3197            </Reference>
3198        </SignedInfo>"#;
3199        let doc = Document::parse(xml).unwrap();
3200        let result = parse_signed_info(doc.root_element());
3201        assert!(matches!(
3202            result.unwrap_err(),
3203            ParseError::InvalidStructure(_)
3204        ));
3205    }
3206
3207    #[test]
3208    fn digest_value_with_element_child_is_rejected() {
3209        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3210            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3211            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3212            <Reference URI="">
3213                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3214                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=<Junk/>AAAA</DigestValue>
3215            </Reference>
3216        </SignedInfo>"#;
3217        let doc = Document::parse(xml).unwrap();
3218
3219        let result = parse_signed_info(doc.root_element());
3220        assert!(matches!(
3221            result.unwrap_err(),
3222            ParseError::InvalidStructure(_)
3223        ));
3224    }
3225
3226    #[test]
3227    fn wrong_namespace_on_signed_info() {
3228        let xml = r#"<SignedInfo xmlns="http://example.com/fake">
3229            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3230        </SignedInfo>"#;
3231        let doc = Document::parse(xml).unwrap();
3232        let result = parse_signed_info(doc.root_element());
3233        assert!(matches!(
3234            result.unwrap_err(),
3235            ParseError::InvalidStructure(_)
3236        ));
3237    }
3238
3239    // ── Whitespace-wrapped base64 ────────────────────────────────────
3240
3241    #[test]
3242    fn base64_with_whitespace() {
3243        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3244            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3245            <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
3246            <Reference URI="">
3247                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
3248                <DigestValue>
3249                    AAAAAAAA
3250                    AAAAAAAAAAAAAAAAAAA=
3251                </DigestValue>
3252            </Reference>
3253        </SignedInfo>"#;
3254        let doc = Document::parse(xml).unwrap();
3255        let si = parse_signed_info(doc.root_element()).unwrap();
3256        assert_eq!(si.references[0].digest_value, vec![0u8; 20]);
3257    }
3258
3259    #[test]
3260    fn base64_decode_digest_accepts_xml_whitespace_chars() {
3261        let digest =
3262            base64_decode_digest("AAAA\tAAAA\rAAAA\nAAAA AAAAAAAAAAA=", DigestAlgorithm::Sha1)
3263                .expect("XML whitespace in DigestValue must be accepted");
3264        assert_eq!(digest, vec![0u8; 20]);
3265    }
3266
3267    #[test]
3268    fn base64_decode_digest_rejects_non_xml_ascii_whitespace() {
3269        let err = base64_decode_digest(
3270            "AAAA\u{000C}AAAAAAAAAAAAAAAAAAAAAAA=",
3271            DigestAlgorithm::Sha1,
3272        )
3273        .expect_err("form-feed/vertical-tab in DigestValue must be rejected");
3274        assert!(matches!(err, ParseError::Base64(_)));
3275    }
3276
3277    #[test]
3278    fn base64_decode_digest_rejects_oversized_base64_before_decode() {
3279        let err = base64_decode_digest("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", DigestAlgorithm::Sha1)
3280            .expect_err("oversized DigestValue base64 must fail before decode");
3281        match err {
3282            ParseError::Base64(message) => {
3283                assert!(
3284                    message.contains("DigestValue exceeds maximum allowed base64 length"),
3285                    "unexpected message: {message}"
3286                );
3287            }
3288            other => panic!("expected ParseError::Base64, got {other:?}"),
3289        }
3290    }
3291
3292    // ── Real-world SAML structure ────────────────────────────────────
3293
3294    #[test]
3295    fn saml_response_signed_info() {
3296        let xml = r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3297            <ds:SignedInfo>
3298                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3299                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
3300                <ds:Reference URI="#_resp1">
3301                    <ds:Transforms>
3302                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
3303                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3304                    </ds:Transforms>
3305                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
3306                    <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
3307                </ds:Reference>
3308            </ds:SignedInfo>
3309            <ds:SignatureValue>ZmFrZQ==</ds:SignatureValue>
3310        </ds:Signature>"##;
3311        let doc = Document::parse(xml).unwrap();
3312
3313        // Find SignedInfo within Signature
3314        let sig_node = doc.root_element();
3315        let signed_info_node = sig_node
3316            .children()
3317            .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
3318            .unwrap();
3319
3320        let si = parse_signed_info(signed_info_node).unwrap();
3321        assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
3322        assert_eq!(si.references.len(), 1);
3323        assert_eq!(si.references[0].uri.as_deref(), Some("#_resp1"));
3324        assert_eq!(si.references[0].transforms.len(), 2);
3325        assert_eq!(si.references[0].digest_value, vec![0u8; 32]);
3326    }
3327}