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