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 der::{
20    Decode,
21    asn1::{Ia5StringRef, ObjectIdentifier},
22};
23use roxmltree::{Document, Node};
24use x509_cert::ext::pkix::name::DirectoryString;
25use x509_cert::name::Name;
26use x509_parser::extensions::ParsedExtension;
27use x509_parser::prelude::FromDer;
28use x509_parser::public_key::PublicKey;
29use x509_parser::x509::X509Name;
30
31#[cfg(test)]
32use super::digest::compute_digest;
33use super::digest::{DigestAlgorithm, compute_digest_with_provider, constant_time_eq};
34use super::transforms::{self, Transform};
35use super::whitespace::{
36    XmlBase64NormalizeLimitedError, is_xml_whitespace_only, normalize_xml_base64_text,
37    normalize_xml_base64_text_with_limit,
38};
39use super::x509::certificate_signature_matches_with_provider;
40use crate::c14n::C14nAlgorithm;
41use crate::c14n::xml_base::{XmlBaseResolutionBudget, resolve_uri_from_node_with_budget};
42
43pub(crate) use crate::hard_limits::SIGNATURE_REFERENCE_CEILING as MAX_REFERENCES_PER_SIGNATURE;
44#[cfg(test)]
45use crate::hard_limits::X509_CHAIN_DEPTH_CEILING as MAX_X509_CHAIN_DEPTH;
46
47/// XMLDSig namespace URI.
48pub(crate) const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
49/// XMLDSig 1.1 namespace URI.
50pub(crate) const XMLDSIG11_NS: &str = "http://www.w3.org/2009/xmldsig11#";
51const MAX_DER_ENCODED_KEY_VALUE_LEN: usize = 8192;
52const MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN: usize = 65_536;
53const MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN: usize = MAX_DER_ENCODED_KEY_VALUE_LEN.div_ceil(3) * 4;
54const MAX_KEY_NAME_TEXT_LEN: usize = 4096;
55const MAX_KEY_INFO_CHILD_COUNT: usize = 64;
56const MAX_HMAC_OUTPUT_LENGTH_TEXT_LEN: usize = 32;
57const MAX_RETRIEVAL_XPATH_TEXT_LEN: usize = 256;
58const MAX_RSA_MODULUS_LEN: usize = 1024;
59const MAX_RSA_EXPONENT_LEN: usize = 8;
60pub(crate) const EC_P256_OID: &str = "1.2.840.10045.3.1.7";
61pub(crate) const EC_P384_OID: &str = "1.3.132.0.34";
62const MAX_EC_PUBLIC_KEY_LEN: usize = 97;
63const MAX_X509_BASE64_TEXT_LEN: usize = 262_144;
64const MAX_X509_BASE64_NORMALIZED_LEN: usize = MAX_X509_BASE64_TEXT_LEN;
65pub(crate) const MAX_X509_DECODED_BINARY_LEN: usize =
66    MAX_X509_BASE64_NORMALIZED_LEN.div_ceil(4) * 3;
67const MAX_X509_SUBJECT_NAME_TEXT_LEN: usize = 16_384;
68const MAX_X509_ISSUER_NAME_TEXT_LEN: usize = 16_384;
69const MAX_X509_SERIAL_NUMBER_RAW_TEXT_LEN: usize = 16_384;
70// RFC 5280 requires consumers to handle a 20-octet unsigned serial magnitude.
71// DER may add a leading sign-padding octet; XML Schema permits insignificant
72// leading decimal zeroes.
73const MAX_X509_SERIAL_NUMBER_VALUE_DIGITS: usize = 49;
74const MAX_X509_SERIAL_NUMBER_BYTES: usize = 20;
75const MAX_X509_DATA_ENTRY_COUNT: usize = 64;
76pub(crate) const MAX_X509_DATA_TOTAL_BINARY_LEN: usize = 1_048_576;
77
78/// Signature algorithms supported for signing and verification.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80#[non_exhaustive]
81pub enum SignatureAlgorithm {
82    /// DSA with SHA-1. Verify-only legacy XMLDSig algorithm.
83    DsaSha1,
84    /// HMAC with SHA-1. Verify-only legacy XMLDSig algorithm.
85    HmacSha1,
86    /// RSA with SHA-1. **Verify-only** — signing disabled.
87    RsaSha1,
88    /// RSA with SHA-256 (most common in SAML).
89    RsaSha256,
90    /// RSA with SHA-384.
91    RsaSha384,
92    /// RSA with SHA-512.
93    RsaSha512,
94    /// ECDSA with SHA-256; the key selects the elliptic curve.
95    EcdsaSha256,
96    /// ECDSA with SHA-384; the key selects the elliptic curve.
97    EcdsaSha384,
98}
99
100impl SignatureAlgorithm {
101    /// Parse from an XML algorithm URI.
102    #[must_use]
103    pub fn from_uri(uri: &str) -> Option<Self> {
104        match uri {
105            "http://www.w3.org/2000/09/xmldsig#dsa-sha1" => Some(Self::DsaSha1),
106            "http://www.w3.org/2000/09/xmldsig#hmac-sha1" => Some(Self::HmacSha1),
107            "http://www.w3.org/2000/09/xmldsig#rsa-sha1" => Some(Self::RsaSha1),
108            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => Some(Self::RsaSha256),
109            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" => Some(Self::RsaSha384),
110            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" => Some(Self::RsaSha512),
111            "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => Some(Self::EcdsaSha256),
112            "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384" => Some(Self::EcdsaSha384),
113            _ => None,
114        }
115    }
116
117    /// Return the XML namespace URI.
118    #[must_use]
119    pub fn uri(self) -> &'static str {
120        match self {
121            Self::DsaSha1 => "http://www.w3.org/2000/09/xmldsig#dsa-sha1",
122            Self::HmacSha1 => "http://www.w3.org/2000/09/xmldsig#hmac-sha1",
123            Self::RsaSha1 => "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
124            Self::RsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
125            Self::RsaSha384 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
126            Self::RsaSha512 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
127            Self::EcdsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
128            Self::EcdsaSha384 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
129        }
130    }
131
132    /// Whether this algorithm is allowed for signing (not just verification).
133    #[must_use]
134    pub fn signing_allowed(self) -> bool {
135        !matches!(self, Self::RsaSha1 | Self::DsaSha1 | Self::HmacSha1)
136    }
137}
138
139/// Parsed `<SignedInfo>` element.
140#[derive(Debug)]
141#[non_exhaustive]
142pub struct SignedInfo {
143    /// Canonicalization method for SignedInfo itself.
144    pub c14n_method: C14nAlgorithm,
145    /// Signature algorithm.
146    pub signature_method: SignatureAlgorithm,
147    /// Optional byte-aligned HMAC output length in bits.
148    pub hmac_output_length_bits: Option<usize>,
149    /// One or more `<Reference>` elements.
150    pub references: Vec<Reference>,
151}
152
153/// Parsed `<Reference>` element.
154#[derive(Debug)]
155pub struct Reference {
156    /// URI attribute (e.g., `""`, `"#_assert1"`).
157    pub uri: Option<String>,
158    /// Id attribute.
159    pub id: Option<String>,
160    /// Type attribute.
161    pub ref_type: Option<String>,
162    /// Transform chain.
163    pub transforms: Vec<Transform>,
164    /// Digest algorithm.
165    pub digest_method: DigestAlgorithm,
166    /// Raw digest value (base64-decoded).
167    pub digest_value: Vec<u8>,
168}
169
170/// Parsed `<KeyInfo>` element.
171#[derive(Debug, Default, Clone, PartialEq, Eq)]
172#[non_exhaustive]
173pub struct KeyInfo {
174    /// Sources discovered under `<KeyInfo>` in document order.
175    pub sources: Vec<KeyInfoSource>,
176}
177
178/// Top-level key material source parsed from `<KeyInfo>`.
179#[derive(Debug, Clone, PartialEq, Eq)]
180#[non_exhaustive]
181pub enum KeyInfoSource {
182    /// `<KeyName>` source.
183    KeyName(String),
184    /// `<KeyValue>` source.
185    KeyValue(KeyValueInfo),
186    /// `<X509Data>` source.
187    X509Data(X509DataInfo),
188    /// `dsig11:DEREncodedKeyValue` source (base64-decoded DER bytes).
189    DerEncodedKeyValue(Vec<u8>),
190    /// `<RetrievalMethod>` URI and optional type URI.
191    RetrievalMethod {
192        /// RFC 3986-resolved resource identity. Same-document fragments remain unchanged.
193        uri: String,
194        /// Declared resource type.
195        resource_type: Option<String>,
196        /// Supported transform shape declared by the retrieval method.
197        transforms: RetrievalMethodTransforms,
198    },
199}
200
201/// Transform forms accepted on `<RetrievalMethod>`.
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
203#[non_exhaustive]
204pub enum RetrievalMethodTransforms {
205    /// No transform chain is present.
206    None,
207    /// Filter a same-document node-set to one `ds:X509Data`-rooted subtree.
208    X509DataNodeSetFilter,
209    /// A transform chain attached to a RetrievalMethod type this implementation
210    /// does not materialize. Resolvers may ignore this advisory key source and
211    /// continue with later `<KeyInfo>` children.
212    Unsupported,
213}
214
215/// Parsed `<KeyValue>` dispatch result.
216#[derive(Debug, Clone, PartialEq, Eq)]
217#[non_exhaustive]
218pub enum KeyValueInfo {
219    /// `<DSAKeyValue>` public parameters.
220    Dsa {
221        /// Optional prime modulus P, present only together with Q.
222        p: Option<Vec<u8>>,
223        /// Optional prime divisor Q, present only together with P.
224        q: Option<Vec<u8>>,
225        /// Optional generator G.
226        g: Option<Vec<u8>>,
227        /// Public value Y.
228        y: Vec<u8>,
229    },
230    /// `<RSAKeyValue>` with unsigned big-endian CryptoBinary parameters.
231    Rsa {
232        /// RSA modulus.
233        modulus: Vec<u8>,
234        /// RSA public exponent.
235        exponent: Vec<u8>,
236    },
237    /// `dsig11:ECKeyValue` with a supported named curve and SEC1 public point.
238    Ec {
239        /// Bare named-curve OID, without the XMLDSig `urn:oid:` prefix.
240        curve_oid: String,
241        /// Uncompressed SEC1 point (`0x04 || x || y`).
242        public_key: Vec<u8>,
243    },
244    /// `dsig11:ECKeyValue` with unusable curve or point data.
245    InvalidEcKeyValue,
246    /// Any other `<KeyValue>` child not yet supported by this phase.
247    Unsupported {
248        /// Namespace URI of the unsupported child, when present.
249        namespace: Option<String>,
250        /// Local name of the unsupported child element.
251        local_name: String,
252    },
253}
254
255/// Parsed `<X509Data>` children.
256#[derive(Debug, Default, Clone, PartialEq, Eq)]
257#[non_exhaustive]
258pub struct X509DataInfo {
259    /// DER-encoded certificates from `<X509Certificate>`.
260    ///
261    /// This vector has a 1:1 index correspondence with `parsed_certificates`.
262    pub certificates: Vec<Vec<u8>>,
263    /// Text values from `<X509SubjectName>`.
264    pub subject_names: Vec<String>,
265    /// `(IssuerName, SerialNumber)` tuples from `<X509IssuerSerial>`.
266    pub issuer_serials: Vec<(String, String)>,
267    /// Raw bytes from `<X509SKI>`.
268    pub skis: Vec<Vec<u8>>,
269    /// DER-encoded CRLs from `<X509CRL>`.
270    pub crls: Vec<Vec<u8>>,
271    /// `(Algorithm URI, digest bytes)` tuples from `dsig11:X509Digest`.
272    pub digests: Vec<(String, Vec<u8>)>,
273    /// Parsed metadata for each `<X509Certificate>` entry.
274    ///
275    /// This vector has a 1:1 index correspondence with `certificates`.
276    pub parsed_certificates: Vec<ParsedX509Certificate>,
277    /// Ordered certificate indexes, starting with the signing certificate.
278    pub certificate_chain: Vec<usize>,
279}
280
281/// Parsed X.509 certificate details extracted from DER.
282#[derive(Debug, Clone, PartialEq, Eq)]
283#[non_exhaustive]
284pub struct ParsedX509Certificate {
285    /// Subject distinguished name.
286    pub subject_dn: String,
287    /// Issuer distinguished name.
288    pub issuer_dn: String,
289    /// Certificate serial number bytes.
290    pub serial_number: Vec<u8>,
291    /// Uppercase hexadecimal certificate serial number without separators.
292    pub serial_number_hex: String,
293    /// Subject Key Identifier extension bytes (if present).
294    pub subject_key_identifier: Option<Vec<u8>>,
295    /// Parsed certificate public key material.
296    pub public_key: X509PublicKeyInfo,
297}
298
299/// Public key material extracted from certificate SubjectPublicKeyInfo.
300#[derive(Debug, Clone, PartialEq, Eq)]
301#[non_exhaustive]
302pub enum X509PublicKeyInfo {
303    /// RSA public key (`modulus`, `exponent`).
304    Rsa {
305        /// Unsigned big-endian RSA modulus (`n`), normalized without leading zeroes.
306        modulus: Vec<u8>,
307        /// Unsigned big-endian RSA public exponent (`e`), normalized without leading zeroes.
308        exponent: Vec<u8>,
309    },
310    /// EC public key (`curve_oid`, encoded point bytes).
311    Ec {
312        /// Named-curve OID from SubjectPublicKeyInfo parameters.
313        curve_oid: String,
314        /// Encoded EC point bytes from SubjectPublicKeyInfo.
315        public_key: Vec<u8>,
316    },
317    /// Public key algorithm is present but not parsed into a concrete key type.
318    Unsupported {
319        /// SubjectPublicKeyInfo algorithm OID.
320        algorithm_oid: String,
321    },
322}
323
324/// Errors during XMLDSig element parsing.
325#[derive(Debug, thiserror::Error)]
326#[non_exhaustive]
327pub enum ParseError {
328    /// The selected cryptographic provider could not evaluate parsed key metadata.
329    #[error("cryptographic provider error: {0}")]
330    Provider(#[from] crate::provider::ProviderError),
331
332    /// Missing required element.
333    #[error("missing required element: <{element}>")]
334    MissingElement {
335        /// Name of the missing element.
336        element: &'static str,
337    },
338
339    /// Invalid structure (wrong child order, unexpected element, etc.).
340    #[error("invalid structure: {0}")]
341    InvalidStructure(String),
342
343    /// `<SignedInfo>` declared more references than one signature may process.
344    #[error("SignedInfo contains more than {max} Reference elements")]
345    TooManyReferences {
346        /// Maximum references accepted for one signature.
347        max: usize,
348    },
349
350    /// Unsupported algorithm URI.
351    #[error("unsupported algorithm: {uri}")]
352    UnsupportedAlgorithm {
353        /// The unrecognized algorithm URI.
354        uri: String,
355    },
356
357    /// Base64 decode error.
358    #[error("base64 decode error: {0}")]
359    Base64(String),
360
361    /// DigestValue length did not match the declared DigestMethod.
362    #[error(
363        "digest length mismatch for {algorithm}: expected {expected} bytes, got {actual} bytes"
364    )]
365    DigestLengthMismatch {
366        /// Digest algorithm URI/name used for diagnostics.
367        algorithm: &'static str,
368        /// Expected decoded digest length in bytes.
369        expected: usize,
370        /// Actual decoded digest length in bytes.
371        actual: usize,
372    },
373
374    /// Transform parsing error.
375    #[error("transform error: {0}")]
376    Transform(#[from] super::types::TransformError),
377}
378
379/// Find the first `<ds:Signature>` element in the document.
380#[must_use]
381pub fn find_signature_node<'a>(doc: &'a Document<'a>) -> Option<Node<'a, 'a>> {
382    doc.descendants().find(|n| {
383        n.is_element()
384            && n.tag_name().name() == "Signature"
385            && n.tag_name().namespace() == Some(XMLDSIG_NS)
386    })
387}
388
389/// Parse a `<ds:SignedInfo>` element.
390///
391/// Enforces strict child order per XMLDSig spec:
392/// `<CanonicalizationMethod>` → `<SignatureMethod>` → `<Reference>`+
393pub fn parse_signed_info(signed_info_node: Node) -> Result<SignedInfo, ParseError> {
394    parse_signed_info_with_xpath_budget(
395        signed_info_node,
396        &mut transforms::XPathSignatureParseBudget::default(),
397    )
398}
399
400pub(crate) fn parse_signed_info_with_xpath_budget(
401    signed_info_node: Node,
402    xpath_budget: &mut transforms::XPathSignatureParseBudget,
403) -> Result<SignedInfo, ParseError> {
404    verify_ds_element(signed_info_node, "SignedInfo")?;
405
406    let mut children = element_children(signed_info_node);
407
408    // 1. CanonicalizationMethod (required, first)
409    let c14n_node = children.next().ok_or(ParseError::MissingElement {
410        element: "CanonicalizationMethod",
411    })?;
412    verify_ds_element(c14n_node, "CanonicalizationMethod")?;
413    let c14n_uri = required_algorithm_attr(c14n_node, "CanonicalizationMethod")?;
414    let mut c14n_method =
415        C14nAlgorithm::from_uri(c14n_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
416            uri: c14n_uri.to_string(),
417        })?;
418    if let Some(prefix_list) = parse_inclusive_prefixes(c14n_node)? {
419        if c14n_method.mode() == crate::c14n::C14nMode::Exclusive1_0 {
420            c14n_method = c14n_method.with_prefix_list(&prefix_list);
421        } else {
422            return Err(ParseError::UnsupportedAlgorithm {
423                uri: c14n_uri.to_string(),
424            });
425        }
426    }
427
428    // 2. SignatureMethod (required, second)
429    let sig_method_node = children.next().ok_or(ParseError::MissingElement {
430        element: "SignatureMethod",
431    })?;
432    verify_ds_element(sig_method_node, "SignatureMethod")?;
433    let sig_uri = required_algorithm_attr(sig_method_node, "SignatureMethod")?;
434    let signature_method =
435        SignatureAlgorithm::from_uri(sig_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
436            uri: sig_uri.to_string(),
437        })?;
438    let hmac_output_length_bits = parse_hmac_output_length(sig_method_node, signature_method)?;
439
440    // 3. One or more Reference elements
441    let mut references = Vec::new();
442    for child in children {
443        verify_ds_element(child, "Reference")?;
444        if references.len() == crate::hard_limits::SIGNATURE_REFERENCE_CEILING {
445            return Err(ParseError::TooManyReferences {
446                max: crate::hard_limits::SIGNATURE_REFERENCE_CEILING,
447            });
448        }
449        references.push(parse_reference_with_xpath_budget(child, xpath_budget)?);
450    }
451    if references.is_empty() {
452        return Err(ParseError::MissingElement {
453            element: "Reference",
454        });
455    }
456
457    Ok(SignedInfo {
458        c14n_method,
459        signature_method,
460        hmac_output_length_bits,
461        references,
462    })
463}
464
465fn parse_hmac_output_length(
466    node: Node<'_, '_>,
467    algorithm: SignatureAlgorithm,
468) -> Result<Option<usize>, ParseError> {
469    ensure_no_non_whitespace_text(node, "SignatureMethod")?;
470    let mut children = element_children(node);
471    let Some(child) = children.next() else {
472        return Ok(None);
473    };
474    if algorithm != SignatureAlgorithm::HmacSha1
475        || child.tag_name().namespace() != Some(XMLDSIG_NS)
476        || child.tag_name().name() != "HMACOutputLength"
477        || children.next().is_some()
478    {
479        return Err(ParseError::InvalidStructure(
480            "SignatureMethod parameters do not match the selected algorithm".into(),
481        ));
482    }
483    ensure_no_element_children(child, "HMACOutputLength")?;
484    let text =
485        collect_text_content_bounded(child, MAX_HMAC_OUTPUT_LENGTH_TEXT_LEN, "HMACOutputLength")?;
486    let bits = text
487        .trim()
488        .parse::<usize>()
489        .map_err(|_| ParseError::InvalidStructure("invalid HMACOutputLength".into()))?;
490    // XMLDSig 1.1 section 6.3.1 requires HMAC truncation to end on a
491    // byte boundary because SignatureValue is encoded as complete octets:
492    // https://www.w3.org/TR/xmldsig-core1/#sec-HMAC
493    if !(80..=160).contains(&bits) || !bits.is_multiple_of(8) {
494        return Err(ParseError::InvalidStructure(
495            "HMACOutputLength must be a byte-aligned value from 80 through 160".into(),
496        ));
497    }
498    Ok(Some(bits))
499}
500
501/// Parse a single `<ds:Reference>` element.
502///
503/// Structure: `<Transforms>?` → `<DigestMethod>` → `<DigestValue>`
504pub fn parse_reference(reference_node: Node) -> Result<Reference, ParseError> {
505    parse_reference_with_xpath_budget(
506        reference_node,
507        &mut transforms::XPathSignatureParseBudget::default(),
508    )
509}
510
511pub(crate) fn parse_reference_with_xpath_budget(
512    reference_node: Node,
513    xpath_budget: &mut transforms::XPathSignatureParseBudget,
514) -> Result<Reference, ParseError> {
515    verify_ds_element(reference_node, "Reference")?;
516    ensure_no_non_whitespace_text(reference_node, "Reference")?;
517    let uri = reference_node.attribute("URI").map(String::from);
518    let id = reference_node.attribute("Id").map(String::from);
519    let ref_type = reference_node.attribute("Type").map(String::from);
520
521    let mut children = element_children(reference_node);
522
523    // Optional <Transforms>
524    let mut transforms = Vec::new();
525    let mut transform_error = None;
526    let (transforms_node, digest_method_node) =
527        reference_transforms_and_digest_method(&mut children)?;
528
529    if let Some(transforms_node) = transforms_node {
530        match transforms::parse_transforms_with_budget(transforms_node, xpath_budget) {
531            Ok(parsed) => transforms = parsed,
532            Err(error) => transform_error = Some(error),
533        }
534    }
535
536    // Required <DigestMethod>
537    let digest_uri = required_algorithm_attr(digest_method_node, "DigestMethod")?;
538    let digest_method =
539        DigestAlgorithm::from_uri(digest_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
540            uri: digest_uri.to_string(),
541        })?;
542
543    // Required <DigestValue>
544    let digest_value_node = children.next().ok_or(ParseError::MissingElement {
545        element: "DigestValue",
546    })?;
547    verify_ds_element(digest_value_node, "DigestValue")?;
548    let digest_value = decode_digest_value_children(digest_value_node, digest_method)?;
549
550    // No more children expected
551    if let Some(unexpected) = children.next() {
552        return Err(ParseError::InvalidStructure(format!(
553            "unexpected element <{}> after <DigestValue> in <Reference>",
554            unexpected.tag_name().name()
555        )));
556    }
557
558    // Validate the complete Reference before reporting an unsupported transform.
559    // This prevents malformed DigestMethod/DigestValue content from being
560    // downgraded to a non-fatal unsupported Manifest transform result.
561    if let Some(error) = transform_error {
562        return Err(ParseError::Transform(error));
563    }
564
565    Ok(Reference {
566        uri,
567        id,
568        ref_type,
569        transforms,
570        digest_method,
571        digest_value,
572    })
573}
574
575pub(crate) fn reference_digest_method(
576    reference_node: Node<'_, '_>,
577) -> Result<DigestAlgorithm, ParseError> {
578    verify_ds_element(reference_node, "Reference")?;
579    let mut children = element_children(reference_node);
580    let (_, digest_method_node) = reference_transforms_and_digest_method(&mut children)?;
581    let uri = required_algorithm_attr(digest_method_node, "DigestMethod")?;
582    DigestAlgorithm::from_uri(uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
583        uri: uri.to_owned(),
584    })
585}
586
587fn reference_transforms_and_digest_method<'a, 'input>(
588    children: &mut impl Iterator<Item = Node<'a, 'input>>,
589) -> Result<(Option<Node<'a, 'input>>, Node<'a, 'input>), ParseError> {
590    let first = children.next().ok_or(ParseError::MissingElement {
591        element: "DigestMethod",
592    })?;
593    let transforms_node = is_ds_element(first, "Transforms").then_some(first);
594    let digest_method_node = if transforms_node.is_some() {
595        children.next().ok_or(ParseError::MissingElement {
596            element: "DigestMethod",
597        })?
598    } else {
599        first
600    };
601    verify_ds_element(digest_method_node, "DigestMethod")?;
602    Ok((transforms_node, digest_method_node))
603}
604
605/// Parse `<ds:KeyInfo>` and dispatch supported child sources.
606///
607/// Supported source elements:
608/// - `<ds:KeyName>`
609/// - `<ds:KeyValue>` (dispatch by child QName; RSA and `dsig11:ECKeyValue` are parsed)
610/// - `<ds:X509Data>`
611/// - `<dsig11:DEREncodedKeyValue>`
612///
613/// Unknown top-level `<KeyInfo>` children are ignored (lax processing), while
614/// unknown XMLDSig-owned (`ds:*` / `dsig11:*`) children inside `<X509Data>` are
615/// rejected fail-closed.
616/// `<X509Data>` may still be empty or contain only non-XMLDSig extension children.
617pub fn parse_key_info(key_info_node: Node) -> Result<KeyInfo, ParseError> {
618    parse_key_info_with_provider(key_info_node, crate::provider::default_provider())
619}
620
621pub(crate) fn parse_key_info_with_provider(
622    key_info_node: Node,
623    provider: &dyn crate::provider::CryptoProvider,
624) -> Result<KeyInfo, ParseError> {
625    let xml_base_budget = XmlBaseResolutionBudget::default();
626    parse_key_info_with_provider_and_xml_base_budget(key_info_node, provider, &xml_base_budget)
627}
628
629pub(crate) fn parse_key_info_with_provider_and_xml_base_budget(
630    key_info_node: Node,
631    provider: &dyn crate::provider::CryptoProvider,
632    xml_base_budget: &XmlBaseResolutionBudget,
633) -> Result<KeyInfo, ParseError> {
634    verify_ds_element(key_info_node, "KeyInfo")?;
635    ensure_no_non_whitespace_text(key_info_node, "KeyInfo")?;
636
637    let mut sources = Vec::new();
638    let mut x509_total_binary_len = 0usize;
639    for (index, child) in element_children(key_info_node).enumerate() {
640        if index >= MAX_KEY_INFO_CHILD_COUNT {
641            return Err(ParseError::InvalidStructure(
642                "KeyInfo contains too many child elements".into(),
643            ));
644        }
645        match (child.tag_name().namespace(), child.tag_name().name()) {
646            (Some(XMLDSIG_NS), "KeyName") => {
647                ensure_no_element_children(child, "KeyName")?;
648                let key_name =
649                    collect_text_content_bounded(child, MAX_KEY_NAME_TEXT_LEN, "KeyName")?;
650                sources.push(KeyInfoSource::KeyName(key_name));
651            }
652            (Some(XMLDSIG_NS), "KeyValue") => {
653                let key_value = parse_key_value_dispatch(child)?;
654                sources.push(KeyInfoSource::KeyValue(key_value));
655            }
656            (Some(XMLDSIG_NS), "X509Data") => {
657                let x509 = parse_x509_data_dispatch_with_budget_and_provider(
658                    child,
659                    &mut x509_total_binary_len,
660                    provider,
661                )?;
662                sources.push(KeyInfoSource::X509Data(x509));
663            }
664            (Some(XMLDSIG_NS), "RetrievalMethod") => {
665                ensure_no_non_whitespace_text(child, "RetrievalMethod")?;
666                let lexical_uri = child.attribute("URI").ok_or_else(|| {
667                    ParseError::InvalidStructure("RetrievalMethod requires URI".into())
668                })?;
669                if lexical_uri.len() > MAX_KEY_NAME_TEXT_LEN {
670                    return Err(ParseError::InvalidStructure(
671                        "RetrievalMethod URI exceeds maximum length".into(),
672                    ));
673                }
674                let uri = if lexical_uri.is_empty() || lexical_uri.starts_with('#') {
675                    lexical_uri.to_owned()
676                } else {
677                    // RetrievalMethod is parsed independently from later key
678                    // materialization, so retain its resolved resource identity.
679                    resolve_uri_from_node_with_budget(child, lexical_uri, xml_base_budget)
680                        .map_err(|error| ParseError::InvalidStructure(error.to_string()))?
681                };
682                let resource_type = child.attribute("Type");
683                if resource_type.is_some_and(|value| value.len() > MAX_KEY_NAME_TEXT_LEN) {
684                    return Err(ParseError::InvalidStructure(
685                        "RetrievalMethod Type exceeds maximum length".into(),
686                    ));
687                }
688                let resource_type = resource_type.map(str::to_owned);
689                let transforms = if resource_type.as_deref()
690                    == Some("http://www.w3.org/2000/09/xmldsig#X509Data")
691                {
692                    parse_retrieval_method_transforms(child)?
693                } else if element_children(child).next().is_some() {
694                    RetrievalMethodTransforms::Unsupported
695                } else {
696                    RetrievalMethodTransforms::None
697                };
698                sources.push(KeyInfoSource::RetrievalMethod {
699                    uri,
700                    resource_type,
701                    transforms,
702                });
703            }
704            (Some(XMLDSIG11_NS), "DEREncodedKeyValue") => {
705                ensure_no_element_children(child, "DEREncodedKeyValue")?;
706                let der = decode_der_encoded_key_value_base64(child)?;
707                sources.push(KeyInfoSource::DerEncodedKeyValue(der));
708            }
709            _ => {}
710        }
711    }
712
713    Ok(KeyInfo { sources })
714}
715
716fn parse_retrieval_method_transforms(
717    node: Node<'_, '_>,
718) -> Result<RetrievalMethodTransforms, ParseError> {
719    let mut children = element_children(node);
720    let Some(transforms) = children.next() else {
721        return Ok(RetrievalMethodTransforms::None);
722    };
723    if children.next().is_some()
724        || transforms.tag_name().namespace() != Some(XMLDSIG_NS)
725        || transforms.tag_name().name() != "Transforms"
726    {
727        return Err(ParseError::InvalidStructure(
728            "RetrievalMethod accepts only one optional ds:Transforms child".into(),
729        ));
730    }
731    ensure_no_non_whitespace_text(transforms, "Transforms")?;
732    let mut transform_children = element_children(transforms);
733    let transform = transform_children.next().ok_or_else(|| {
734        ParseError::InvalidStructure("RetrievalMethod Transforms must not be empty".into())
735    })?;
736    if transform_children.next().is_some()
737        || transform.tag_name().namespace() != Some(XMLDSIG_NS)
738        || transform.tag_name().name() != "Transform"
739        || transform.attribute("Algorithm") != Some(transforms::XPATH_TRANSFORM_URI)
740    {
741        return Err(ParseError::InvalidStructure(
742            "unsupported RetrievalMethod transform chain".into(),
743        ));
744    }
745    ensure_no_non_whitespace_text(transform, "Transform")?;
746    let mut parameters = element_children(transform);
747    let xpath = parameters.next().ok_or_else(|| {
748        ParseError::InvalidStructure("RetrievalMethod XPath parameter is missing".into())
749    })?;
750    if parameters.next().is_some()
751        || xpath.tag_name().namespace() != Some(XMLDSIG_NS)
752        || xpath.tag_name().name() != "XPath"
753    {
754        return Err(ParseError::InvalidStructure(
755            "unsupported RetrievalMethod transform chain".into(),
756        ));
757    }
758    ensure_no_element_children(xpath, "XPath")?;
759    let expression =
760        collect_text_content_bounded(xpath, MAX_RETRIEVAL_XPATH_TEXT_LEN, "RetrievalMethod XPath")?;
761    let expression = expression.trim();
762    let selects_x509_data = expression
763        .strip_prefix("ancestor-or-self::")
764        .and_then(|step| step.split_once(':'))
765        .is_some_and(|(prefix, local)| {
766            local == "X509Data" && xpath.lookup_namespace_uri(Some(prefix)) == Some(XMLDSIG_NS)
767        });
768    if !selects_x509_data {
769        return Err(ParseError::InvalidStructure(
770            "unsupported RetrievalMethod XPath selection".into(),
771        ));
772    }
773    Ok(RetrievalMethodTransforms::X509DataNodeSetFilter)
774}
775
776// ── Helpers ──────────────────────────────────────────────────────────────────
777
778/// Iterate only element children (skip text, comments, PIs).
779fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
780    node.children().filter(|n| n.is_element())
781}
782
783/// Verify that a node is a `<ds:{expected_name}>` element.
784fn verify_ds_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
785    if !node.is_element() {
786        return Err(ParseError::InvalidStructure(format!(
787            "expected element <{expected_name}>, got non-element node"
788        )));
789    }
790    let tag = node.tag_name();
791    if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG_NS) {
792        return Err(ParseError::InvalidStructure(format!(
793            "expected <ds:{expected_name}>, got <{}{}>",
794            tag.namespace()
795                .map(|ns| format!("{{{ns}}}"))
796                .unwrap_or_default(),
797            tag.name()
798        )));
799    }
800    Ok(())
801}
802
803/// Verify that a node is a `<dsig11:{expected_name}>` element.
804fn verify_dsig11_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
805    if !node.is_element() {
806        return Err(ParseError::InvalidStructure(format!(
807            "expected element <{expected_name}>, got non-element node"
808        )));
809    }
810    let tag = node.tag_name();
811    if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG11_NS) {
812        return Err(ParseError::InvalidStructure(format!(
813            "expected <dsig11:{expected_name}>, got <{}{}>",
814            tag.namespace()
815                .map(|ns| format!("{{{ns}}}"))
816                .unwrap_or_default(),
817            tag.name()
818        )));
819    }
820    Ok(())
821}
822
823/// Get the required `Algorithm` attribute from an element.
824fn required_algorithm_attr<'a>(
825    node: Node<'a, 'a>,
826    element_name: &'static str,
827) -> Result<&'a str, ParseError> {
828    node.attribute("Algorithm").ok_or_else(|| {
829        ParseError::InvalidStructure(format!("missing Algorithm attribute on <{element_name}>"))
830    })
831}
832
833/// Parse the `PrefixList` attribute from an `<ec:InclusiveNamespaces>` child of
834/// `<CanonicalizationMethod>`, if present.
835///
836/// This mirrors transform parsing for Exclusive C14N and keeps SignedInfo
837/// canonicalization parameters lossless.
838fn parse_inclusive_prefixes(node: Node) -> Result<Option<String>, ParseError> {
839    const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
840
841    for child in node.children() {
842        if child.is_element() {
843            let tag = child.tag_name();
844            if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
845            {
846                return child
847                    .attribute("PrefixList")
848                    .map(str::to_string)
849                    .ok_or_else(|| {
850                        ParseError::InvalidStructure(
851                            "missing PrefixList attribute on <InclusiveNamespaces>".into(),
852                        )
853                    })
854                    .map(Some);
855            }
856        }
857    }
858
859    Ok(None)
860}
861
862fn parse_key_value_dispatch(node: Node) -> Result<KeyValueInfo, ParseError> {
863    verify_ds_element(node, "KeyValue")?;
864    ensure_no_non_whitespace_text(node, "KeyValue")?;
865
866    let mut children = element_children(node);
867    let Some(first_child) = children.next() else {
868        return Err(ParseError::InvalidStructure(
869            "KeyValue must contain exactly one key-value child".into(),
870        ));
871    };
872    if children.next().is_some() {
873        return Err(ParseError::InvalidStructure(
874            "KeyValue must contain exactly one key-value child".into(),
875        ));
876    }
877
878    match (
879        first_child.tag_name().namespace(),
880        first_child.tag_name().name(),
881    ) {
882        (Some(XMLDSIG_NS), "RSAKeyValue") => parse_rsa_key_value(first_child),
883        (Some(XMLDSIG_NS), "DSAKeyValue") => parse_dsa_key_value(first_child),
884        (Some(XMLDSIG11_NS), "ECKeyValue") => parse_ec_key_value(first_child),
885        (namespace, child_name) => Ok(KeyValueInfo::Unsupported {
886            namespace: namespace.map(str::to_string),
887            local_name: child_name.to_string(),
888        }),
889    }
890}
891
892fn parse_dsa_key_value(node: Node<'_, '_>) -> Result<KeyValueInfo, ParseError> {
893    verify_ds_element(node, "DSAKeyValue")?;
894    ensure_no_non_whitespace_text(node, "DSAKeyValue")?;
895    let children = element_children(node).collect::<Vec<_>>();
896    let mut index = 0;
897    let p = take_dsa_crypto_binary(&children, &mut index, "P")?;
898    let q = take_dsa_crypto_binary(&children, &mut index, "Q")?;
899    if p.is_some() != q.is_some() {
900        return Err(ParseError::InvalidStructure(
901            "DSAKeyValue P and Q must be present together".into(),
902        ));
903    }
904    let g = take_dsa_crypto_binary(&children, &mut index, "G")?;
905    let y = take_dsa_crypto_binary(&children, &mut index, "Y")?
906        .ok_or_else(|| ParseError::InvalidStructure("DSAKeyValue requires Y".into()))?;
907    let _j = take_dsa_crypto_binary(&children, &mut index, "J")?;
908    let seed = take_dsa_crypto_binary(&children, &mut index, "Seed")?;
909    let counter = take_dsa_crypto_binary(&children, &mut index, "PgenCounter")?;
910    if seed.is_some() != counter.is_some() {
911        return Err(ParseError::InvalidStructure(
912            "DSAKeyValue Seed and PgenCounter must be present together".into(),
913        ));
914    }
915    if index != children.len() {
916        return Err(ParseError::InvalidStructure(
917            "DSAKeyValue children do not match the XMLDSig schema order".into(),
918        ));
919    }
920    Ok(KeyValueInfo::Dsa { p, q, g, y })
921}
922
923fn take_dsa_crypto_binary(
924    children: &[Node<'_, '_>],
925    index: &mut usize,
926    name: &'static str,
927) -> Result<Option<Vec<u8>>, ParseError> {
928    let Some(&child) = children.get(*index) else {
929        return Ok(None);
930    };
931    if !is_ds_element(child, name) {
932        return Ok(None);
933    }
934    *index += 1;
935    ensure_no_element_children(child, name)?;
936    decode_crypto_binary(child, name, MAX_RSA_MODULUS_LEN).map(Some)
937}
938
939fn is_ds_element(node: Node<'_, '_>, name: &str) -> bool {
940    node.tag_name().namespace() == Some(XMLDSIG_NS) && node.tag_name().name() == name
941}
942
943fn parse_ec_key_value(node: Node<'_, '_>) -> Result<KeyValueInfo, ParseError> {
944    verify_dsig11_element(node, "ECKeyValue")?;
945    ensure_no_non_whitespace_text(node, "ECKeyValue")?;
946
947    let mut children = element_children(node);
948    let Some(named_curve_node) = children.next() else {
949        return Ok(KeyValueInfo::InvalidEcKeyValue);
950    };
951    if named_curve_node.tag_name().namespace() == Some(XMLDSIG11_NS)
952        && named_curve_node.tag_name().name() == "ECParameters"
953    {
954        return Ok(KeyValueInfo::Unsupported {
955            namespace: Some(XMLDSIG11_NS.to_string()),
956            local_name: "ECKeyValue".into(),
957        });
958    }
959    if named_curve_node.tag_name().namespace() != Some(XMLDSIG11_NS)
960        || named_curve_node.tag_name().name() != "NamedCurve"
961    {
962        return Ok(KeyValueInfo::InvalidEcKeyValue);
963    }
964    ensure_no_element_children(named_curve_node, "NamedCurve")?;
965    ensure_no_non_whitespace_text(named_curve_node, "NamedCurve")?;
966    let Some((curve_oid, expected_public_key_len)) =
967        (match parse_ec_named_curve_oid(named_curve_node) {
968            Ok(curve) => curve,
969            Err(_) => return Ok(KeyValueInfo::InvalidEcKeyValue),
970        })
971    else {
972        return Ok(KeyValueInfo::Unsupported {
973            namespace: Some(XMLDSIG11_NS.to_string()),
974            local_name: "ECKeyValue".into(),
975        });
976    };
977
978    let Some(public_key_node) = children.next() else {
979        return Ok(KeyValueInfo::InvalidEcKeyValue);
980    };
981    if public_key_node.tag_name().namespace() != Some(XMLDSIG11_NS)
982        || public_key_node.tag_name().name() != "PublicKey"
983    {
984        return Ok(KeyValueInfo::InvalidEcKeyValue);
985    }
986    ensure_no_element_children(public_key_node, "PublicKey")?;
987    if children.next().is_some() {
988        return Ok(KeyValueInfo::InvalidEcKeyValue);
989    }
990
991    let public_key = match decode_crypto_binary(public_key_node, "PublicKey", MAX_EC_PUBLIC_KEY_LEN)
992    {
993        Ok(public_key) => public_key,
994        Err(_) => return Ok(KeyValueInfo::InvalidEcKeyValue),
995    };
996    if validate_ec_public_key_point(&public_key, expected_public_key_len).is_err() {
997        return Ok(KeyValueInfo::InvalidEcKeyValue);
998    }
999
1000    Ok(KeyValueInfo::Ec {
1001        curve_oid,
1002        public_key,
1003    })
1004}
1005
1006fn parse_ec_named_curve_oid(node: Node<'_, '_>) -> Result<Option<(String, usize)>, ParseError> {
1007    let uri = node.attribute("URI").ok_or_else(|| {
1008        ParseError::InvalidStructure("ECKeyValue NamedCurve must include URI attribute".into())
1009    })?;
1010    let curve_oid = uri.strip_prefix("urn:oid:").unwrap_or(uri);
1011    if curve_oid.is_empty() {
1012        return Err(ParseError::InvalidStructure(
1013            "ECKeyValue NamedCurve URI must not be empty".into(),
1014        ));
1015    }
1016    let Some(public_key_len) = ec_public_key_len(curve_oid) else {
1017        return Ok(None);
1018    };
1019    Ok(Some((curve_oid.to_string(), public_key_len)))
1020}
1021
1022fn ec_public_key_len(curve_oid: &str) -> Option<usize> {
1023    match curve_oid {
1024        EC_P256_OID => Some(65),
1025        EC_P384_OID => Some(97),
1026        _ => None,
1027    }
1028}
1029
1030fn validate_ec_public_key_point(public_key: &[u8], expected_len: usize) -> Result<(), ParseError> {
1031    if public_key.len() != expected_len {
1032        return Err(ParseError::InvalidStructure(
1033            "ECKeyValue PublicKey length does not match NamedCurve".into(),
1034        ));
1035    }
1036    if public_key.first().copied() != Some(0x04) {
1037        return Err(ParseError::InvalidStructure(
1038            "ECKeyValue PublicKey must be an uncompressed SEC1 point".into(),
1039        ));
1040    }
1041    Ok(())
1042}
1043
1044fn parse_rsa_key_value(node: Node<'_, '_>) -> Result<KeyValueInfo, ParseError> {
1045    verify_ds_element(node, "RSAKeyValue")?;
1046    ensure_no_non_whitespace_text(node, "RSAKeyValue")?;
1047
1048    let mut children = element_children(node);
1049    let modulus_node = children.next().ok_or_else(|| {
1050        ParseError::InvalidStructure("RSAKeyValue requires Modulus and Exponent".into())
1051    })?;
1052    verify_ds_element(modulus_node, "Modulus")?;
1053    ensure_no_element_children(modulus_node, "Modulus")?;
1054
1055    let exponent_node = children.next().ok_or_else(|| {
1056        ParseError::InvalidStructure("RSAKeyValue requires Modulus and Exponent".into())
1057    })?;
1058    verify_ds_element(exponent_node, "Exponent")?;
1059    ensure_no_element_children(exponent_node, "Exponent")?;
1060    if children.next().is_some() {
1061        return Err(ParseError::InvalidStructure(
1062            "RSAKeyValue must contain exactly Modulus followed by Exponent".into(),
1063        ));
1064    }
1065
1066    Ok(KeyValueInfo::Rsa {
1067        modulus: decode_crypto_binary(modulus_node, "Modulus", MAX_RSA_MODULUS_LEN)?,
1068        exponent: decode_crypto_binary(exponent_node, "Exponent", MAX_RSA_EXPONENT_LEN)?,
1069    })
1070}
1071
1072fn decode_crypto_binary(
1073    node: Node<'_, '_>,
1074    element_name: &'static str,
1075    max_decoded_len: usize,
1076) -> Result<Vec<u8>, ParseError> {
1077    use base64::Engine;
1078    use base64::engine::general_purpose::STANDARD;
1079
1080    let max_base64_len = max_decoded_len.div_ceil(3) * 4;
1081    let mut cleaned = String::with_capacity(max_base64_len);
1082    for text in node
1083        .children()
1084        .filter(|child| child.is_text())
1085        .filter_map(|child| child.text())
1086    {
1087        normalize_xml_base64_text_with_limit(text, &mut cleaned, max_base64_len).map_err(
1088            |err| match err {
1089                XmlBase64NormalizeLimitedError::InvalidWhitespace(err) => {
1090                    ParseError::Base64(format!(
1091                        "invalid XML whitespace U+{:04X} in {element_name}",
1092                        err.invalid_byte
1093                    ))
1094                }
1095                XmlBase64NormalizeLimitedError::TooLong(_) => ParseError::InvalidStructure(
1096                    format!("{element_name} exceeds maximum allowed base64 length"),
1097                ),
1098            },
1099        )?;
1100    }
1101
1102    let value = STANDARD
1103        .decode(&cleaned)
1104        .map_err(|err| ParseError::Base64(format!("{element_name}: {err}")))?;
1105    if value.is_empty() {
1106        return Err(ParseError::InvalidStructure(format!(
1107            "{element_name} must not be empty"
1108        )));
1109    }
1110    if value.len() > max_decoded_len {
1111        return Err(ParseError::InvalidStructure(format!(
1112            "{element_name} exceeds maximum allowed binary length"
1113        )));
1114    }
1115    Ok(value)
1116}
1117
1118pub(crate) fn parse_x509_data_dispatch_with_budget_and_provider(
1119    node: Node,
1120    total_binary_len: &mut usize,
1121    provider: &dyn crate::provider::CryptoProvider,
1122) -> Result<X509DataInfo, ParseError> {
1123    verify_ds_element(node, "X509Data")?;
1124    ensure_no_non_whitespace_text(node, "X509Data")?;
1125
1126    let mut info = X509DataInfo::default();
1127    for child in element_children(node) {
1128        match (child.tag_name().namespace(), child.tag_name().name()) {
1129            (Some(XMLDSIG_NS), "X509Certificate") => {
1130                ensure_no_element_children(child, "X509Certificate")?;
1131                ensure_x509_data_entry_budget(&info)?;
1132                let cert = decode_x509_base64(child, "X509Certificate")?;
1133                add_x509_data_usage(total_binary_len, cert.len())?;
1134                let parsed_cert = parse_x509_certificate(cert.as_slice())?;
1135                info.parsed_certificates.push(parsed_cert);
1136                info.certificates.push(cert);
1137            }
1138            (Some(XMLDSIG_NS), "X509SubjectName") => {
1139                ensure_no_element_children(child, "X509SubjectName")?;
1140                ensure_x509_data_entry_budget(&info)?;
1141                let subject_name = collect_text_content_bounded(
1142                    child,
1143                    MAX_X509_SUBJECT_NAME_TEXT_LEN,
1144                    "X509SubjectName",
1145                )?;
1146                info.subject_names.push(subject_name);
1147            }
1148            (Some(XMLDSIG_NS), "X509IssuerSerial") => {
1149                ensure_x509_data_entry_budget(&info)?;
1150                let issuer_serial = parse_x509_issuer_serial(child)?;
1151                info.issuer_serials.push(issuer_serial);
1152            }
1153            (Some(XMLDSIG_NS), "X509SKI") => {
1154                ensure_no_element_children(child, "X509SKI")?;
1155                ensure_x509_data_entry_budget(&info)?;
1156                let ski = decode_x509_base64(child, "X509SKI")?;
1157                add_x509_data_usage(total_binary_len, ski.len())?;
1158                info.skis.push(ski);
1159            }
1160            (Some(XMLDSIG_NS), "X509CRL") => {
1161                ensure_no_element_children(child, "X509CRL")?;
1162                ensure_x509_data_entry_budget(&info)?;
1163                let crl = decode_x509_base64(child, "X509CRL")?;
1164                add_x509_data_usage(total_binary_len, crl.len())?;
1165                info.crls.push(crl);
1166            }
1167            (Some(XMLDSIG11_NS), "X509Digest") => {
1168                ensure_no_element_children(child, "X509Digest")?;
1169                ensure_x509_data_entry_budget(&info)?;
1170                let algorithm = required_algorithm_attr(child, "X509Digest")?;
1171                let digest = decode_x509_base64(child, "X509Digest")?;
1172                add_x509_data_usage(total_binary_len, digest.len())?;
1173                info.digests.push((algorithm.to_string(), digest));
1174            }
1175            (Some(XMLDSIG_NS), child_name) | (Some(XMLDSIG11_NS), child_name) => {
1176                return Err(ParseError::InvalidStructure(format!(
1177                    "X509Data contains unsupported XMLDSig child element <{child_name}>"
1178                )));
1179            }
1180            _ => {}
1181        }
1182    }
1183
1184    info.certificate_chain = build_x509_certificate_chain(&info, provider)?;
1185    Ok(info)
1186}
1187
1188fn build_x509_certificate_chain(
1189    info: &X509DataInfo,
1190    provider: &dyn crate::provider::CryptoProvider,
1191) -> Result<Vec<usize>, ParseError> {
1192    if info.parsed_certificates.is_empty() {
1193        return Ok(Vec::new());
1194    }
1195
1196    let signing_idx = select_x509_signing_certificate(info, provider)?;
1197    build_x509_certificate_chain_from(info, signing_idx, provider).map_err(ParseError::from)
1198}
1199
1200#[derive(Debug, Clone, PartialEq, Eq)]
1201pub(crate) enum X509ChainBuildError {
1202    InconsistentMetadata,
1203    DepthExceeded,
1204    Cycle,
1205    IssuerSignatureMismatch,
1206    AmbiguousIssuer,
1207    UnsupportedSignatureAlgorithm { oid: String },
1208    Provider(crate::provider::ProviderError),
1209}
1210
1211impl From<X509ChainBuildError> for ParseError {
1212    fn from(error: X509ChainBuildError) -> Self {
1213        let reason = match error {
1214            X509ChainBuildError::InconsistentMetadata => {
1215                "X509Data certificate metadata is inconsistent"
1216            }
1217            X509ChainBuildError::DepthExceeded => {
1218                "X509Data certificate chain exceeds maximum depth"
1219            }
1220            X509ChainBuildError::Cycle => "X509Data certificate chain contains a cycle",
1221            X509ChainBuildError::IssuerSignatureMismatch => {
1222                "X509Data issuer candidates do not verify the certificate signature"
1223            }
1224            X509ChainBuildError::AmbiguousIssuer => {
1225                "X509Data certificate chain contains ambiguous issuer certificates"
1226            }
1227            X509ChainBuildError::UnsupportedSignatureAlgorithm { oid } => {
1228                return Self::InvalidStructure(format!(
1229                    "X509Data certificate chain uses unsupported signature algorithm {oid}"
1230                ));
1231            }
1232            X509ChainBuildError::Provider(error) => return Self::Provider(error),
1233        };
1234        Self::InvalidStructure(reason.into())
1235    }
1236}
1237
1238/// Order an available certificate pool from a preselected signing certificate.
1239pub(crate) fn build_x509_certificate_chain_from(
1240    info: &X509DataInfo,
1241    signing_idx: usize,
1242    provider: &dyn crate::provider::CryptoProvider,
1243) -> Result<Vec<usize>, X509ChainBuildError> {
1244    if signing_idx >= info.parsed_certificates.len()
1245        || info.parsed_certificates.len() != info.certificates.len()
1246    {
1247        return Err(X509ChainBuildError::InconsistentMetadata);
1248    }
1249    let mut chain = vec![signing_idx];
1250
1251    loop {
1252        let current_idx = *chain
1253            .last()
1254            .expect("chain starts with signing certificate index");
1255        let current = &info.parsed_certificates[current_idx];
1256        if distinguished_names_equal(&current.subject_dn, &current.issuer_dn) {
1257            break;
1258        }
1259
1260        let candidates = info
1261            .parsed_certificates
1262            .iter()
1263            .enumerate()
1264            .filter(|(idx, cert)| {
1265                *idx != current_idx
1266                    && distinguished_names_equal(&cert.subject_dn, &current.issuer_dn)
1267            })
1268            .map(|(idx, _)| idx)
1269            .collect::<Vec<_>>();
1270
1271        let issuer_idx = match candidates.as_slice() {
1272            [] => break,
1273            [issuer_idx] => *issuer_idx,
1274            _ => {
1275                let mut verified = Vec::new();
1276                let mut unsupported_oid = None;
1277                for issuer_idx in candidates {
1278                    match certificate_signature_matches_with_provider(
1279                        &info.certificates[current_idx],
1280                        &info.certificates[issuer_idx],
1281                        provider,
1282                    ) {
1283                        Ok(true) => verified.push(issuer_idx),
1284                        Ok(false) => {}
1285                        Err(super::X509ChainError::Provider(
1286                            crate::provider::ProviderError::Unsupported {
1287                                operation: crate::provider::ProviderOperation::VerifyCertificate,
1288                                algorithm: Some(oid),
1289                            },
1290                        )) => {
1291                            unsupported_oid.get_or_insert(oid);
1292                        }
1293                        Err(super::X509ChainError::Provider(error)) => {
1294                            return Err(X509ChainBuildError::Provider(error));
1295                        }
1296                        Err(super::X509ChainError::UnsupportedSignatureAlgorithm { oid }) => {
1297                            unsupported_oid.get_or_insert(oid);
1298                        }
1299                        Err(_) => return Err(X509ChainBuildError::IssuerSignatureMismatch),
1300                    }
1301                }
1302                match verified.as_slice() {
1303                    [issuer_idx] => *issuer_idx,
1304                    [] => {
1305                        if let Some(oid) = unsupported_oid {
1306                            return Err(X509ChainBuildError::UnsupportedSignatureAlgorithm { oid });
1307                        }
1308                        return Err(X509ChainBuildError::IssuerSignatureMismatch);
1309                    }
1310                    _ => return Err(X509ChainBuildError::AmbiguousIssuer),
1311                }
1312            }
1313        };
1314        if chain.contains(&issuer_idx) {
1315            return Err(X509ChainBuildError::Cycle);
1316        }
1317        if chain.len() == crate::hard_limits::X509_CHAIN_DEPTH_CEILING {
1318            return Err(X509ChainBuildError::DepthExceeded);
1319        }
1320        chain.push(issuer_idx);
1321    }
1322
1323    Ok(chain)
1324}
1325
1326/// Enumerate signature-valid certificate paths that terminate at a certificate
1327/// in the trusted prefix. Trust and certificate policy are intentionally not
1328/// assigned here; callers must fully validate every returned candidate.
1329pub(crate) fn build_x509_certificate_paths_to_trusted_prefix(
1330    info: &X509DataInfo,
1331    signing_idx: usize,
1332    trusted_prefix_len: usize,
1333    max_depth: usize,
1334    max_candidate_paths: usize,
1335    provider: &dyn crate::provider::CryptoProvider,
1336) -> Result<Vec<Vec<usize>>, X509ChainBuildError> {
1337    if trusted_prefix_len > info.certificates.len() {
1338        return Err(X509ChainBuildError::InconsistentMetadata);
1339    }
1340    build_x509_certificate_paths(
1341        info,
1342        signing_idx,
1343        |index| index < trusted_prefix_len,
1344        false,
1345        max_depth,
1346        max_candidate_paths,
1347        provider,
1348    )
1349}
1350
1351/// Enumerate signature-valid paths that reach any candidate selector target.
1352/// A matching intermediate is retained as a candidate and traversal continues
1353/// so callers can test selector categories against every longer path as well.
1354pub(crate) fn build_x509_certificate_paths_to_selector_targets(
1355    info: &X509DataInfo,
1356    signing_idx: usize,
1357    targets: &[usize],
1358    max_depth: usize,
1359    max_candidate_paths: usize,
1360    provider: &dyn crate::provider::CryptoProvider,
1361) -> Result<Vec<Vec<usize>>, X509ChainBuildError> {
1362    if targets
1363        .iter()
1364        .any(|index| *index >= info.certificates.len())
1365    {
1366        return Err(X509ChainBuildError::InconsistentMetadata);
1367    }
1368    build_x509_certificate_paths(
1369        info,
1370        signing_idx,
1371        |index| targets.contains(&index),
1372        true,
1373        max_depth,
1374        max_candidate_paths,
1375        provider,
1376    )
1377}
1378
1379fn build_x509_certificate_paths(
1380    info: &X509DataInfo,
1381    signing_idx: usize,
1382    is_terminal: impl Fn(usize) -> bool,
1383    continue_after_terminal: bool,
1384    max_depth: usize,
1385    max_candidate_paths: usize,
1386    provider: &dyn crate::provider::CryptoProvider,
1387) -> Result<Vec<Vec<usize>>, X509ChainBuildError> {
1388    if signing_idx >= info.parsed_certificates.len()
1389        || info.parsed_certificates.len() != info.certificates.len()
1390    {
1391        return Err(X509ChainBuildError::InconsistentMetadata);
1392    }
1393    if max_candidate_paths == 0 {
1394        return Err(X509ChainBuildError::AmbiguousIssuer);
1395    }
1396
1397    let mut pending = vec![vec![signing_idx]];
1398    let mut completed = Vec::new();
1399    let mut generated_paths = 1usize;
1400    let mut depth_exceeded = false;
1401    let mut unsupported_oid = None;
1402    let mut issuer_cache = vec![None; info.parsed_certificates.len()];
1403    while let Some(path) = pending.pop() {
1404        let current_idx = *path
1405            .last()
1406            .expect("candidate path starts with signing certificate index");
1407        if is_terminal(current_idx) {
1408            completed.push(path.clone());
1409            if !continue_after_terminal {
1410                continue;
1411            }
1412        }
1413        if path.len() == max_depth {
1414            depth_exceeded = true;
1415            continue;
1416        }
1417
1418        let current = &info.parsed_certificates[current_idx];
1419        if issuer_cache[current_idx].is_none() {
1420            let mut verified = Vec::new();
1421            for (issuer_idx, issuer) in info.parsed_certificates.iter().enumerate() {
1422                if !distinguished_names_equal(&issuer.subject_dn, &current.issuer_dn) {
1423                    continue;
1424                }
1425                match certificate_signature_matches_with_provider(
1426                    &info.certificates[current_idx],
1427                    &info.certificates[issuer_idx],
1428                    provider,
1429                ) {
1430                    Ok(true) => verified.push(issuer_idx),
1431                    Ok(false) => {}
1432                    Err(super::X509ChainError::Provider(
1433                        crate::provider::ProviderError::Unsupported {
1434                            operation: crate::provider::ProviderOperation::VerifyCertificate,
1435                            algorithm: Some(oid),
1436                        },
1437                    )) => {
1438                        // Provider capability can depend on the issuer SPKI,
1439                        // so retain the diagnostic but try every same-DN key.
1440                        unsupported_oid.get_or_insert(oid);
1441                        continue;
1442                    }
1443                    Err(super::X509ChainError::Provider(error)) => {
1444                        return Err(X509ChainBuildError::Provider(error));
1445                    }
1446                    Err(super::X509ChainError::UnsupportedSignatureAlgorithm { oid }) => {
1447                        // The mapper rejected this child's AlgorithmIdentifier;
1448                        // no issuer candidate can alter it on the current path.
1449                        unsupported_oid.get_or_insert(oid);
1450                        break;
1451                    }
1452                    Err(_) => return Err(X509ChainBuildError::IssuerSignatureMismatch),
1453                }
1454            }
1455            issuer_cache[current_idx] = Some(verified);
1456        }
1457        let issuers = issuer_cache[current_idx]
1458            .as_ref()
1459            .expect("issuer cache entry was initialized");
1460        let issuers = issuers
1461            .iter()
1462            .copied()
1463            .filter(|issuer_idx| !path.contains(issuer_idx))
1464            .collect::<Vec<_>>();
1465        if generated_paths.saturating_add(issuers.len()) > max_candidate_paths {
1466            return Err(X509ChainBuildError::AmbiguousIssuer);
1467        }
1468        generated_paths += issuers.len();
1469        for issuer_idx in issuers {
1470            let mut candidate = path.clone();
1471            candidate.push(issuer_idx);
1472            pending.push(candidate);
1473        }
1474    }
1475
1476    if completed.is_empty() {
1477        if let Some(oid) = unsupported_oid {
1478            return Err(X509ChainBuildError::UnsupportedSignatureAlgorithm { oid });
1479        }
1480        if depth_exceeded {
1481            return Err(X509ChainBuildError::DepthExceeded);
1482        }
1483    }
1484    Ok(completed)
1485}
1486
1487fn select_x509_signing_certificate(
1488    info: &X509DataInfo,
1489    provider: &dyn crate::provider::CryptoProvider,
1490) -> Result<usize, ParseError> {
1491    let has_lookup_identifiers = x509_data_has_lookup_identifiers(info);
1492    let mut candidates = Vec::new();
1493    if has_lookup_identifiers {
1494        for (idx, (parsed, der)) in info
1495            .parsed_certificates
1496            .iter()
1497            .zip(&info.certificates)
1498            .enumerate()
1499        {
1500            if x509_certificate_matches_any_selector(info, parsed, der, provider)? {
1501                candidates.push(idx);
1502            }
1503        }
1504        if !x509_selector_categories_match_chain(info, provider)? {
1505            return Err(ParseError::InvalidStructure(
1506                "X509Data lookup identifiers do not match the embedded certificate chain".into(),
1507            ));
1508        }
1509    }
1510
1511    match candidates.as_slice() {
1512        [idx] => return Ok(*idx),
1513        [] if has_lookup_identifiers => {
1514            return Err(ParseError::InvalidStructure(
1515                "X509Data lookup identifiers do not match any embedded certificate".into(),
1516            ));
1517        }
1518        [] => {}
1519        _ => {}
1520    }
1521
1522    let leaf_candidates = info
1523        .parsed_certificates
1524        .iter()
1525        .enumerate()
1526        .filter(|(_, cert)| {
1527            !distinguished_names_equal(&cert.subject_dn, &cert.issuer_dn)
1528                && !info
1529                    .parsed_certificates
1530                    .iter()
1531                    .any(|other| distinguished_names_equal(&other.issuer_dn, &cert.subject_dn))
1532        })
1533        .map(|(idx, _)| idx)
1534        .collect::<Vec<_>>();
1535
1536    let selected_leaves = leaf_candidates
1537        .iter()
1538        .filter(|idx| !has_lookup_identifiers || candidates.contains(idx))
1539        .copied()
1540        .collect::<Vec<_>>();
1541
1542    match selected_leaves.as_slice() {
1543        [idx] => Ok(*idx),
1544        [] if !has_lookup_identifiers => Ok(0),
1545        [] => Err(ParseError::InvalidStructure(
1546            "X509Data lookup identifiers match multiple certificates without a unique signing certificate"
1547                .into(),
1548        )),
1549        _ => Err(ParseError::InvalidStructure(
1550            if has_lookup_identifiers {
1551                "X509Data lookup identifiers match multiple certificates"
1552            } else {
1553                "X509Data contains multiple possible signing certificates"
1554            }
1555            .into(),
1556        )),
1557    }
1558}
1559
1560pub(crate) fn x509_data_has_lookup_identifiers(info: &X509DataInfo) -> bool {
1561    !info.subject_names.is_empty()
1562        || !info.issuer_serials.is_empty()
1563        || !info.skis.is_empty()
1564        || !info.digests.is_empty()
1565}
1566
1567pub(crate) fn x509_certificate_matches_any_selector(
1568    info: &X509DataInfo,
1569    certificate: &ParsedX509Certificate,
1570    certificate_der: &[u8],
1571    provider: &dyn crate::provider::CryptoProvider,
1572) -> Result<bool, ParseError> {
1573    let subject_match = info
1574        .subject_names
1575        .iter()
1576        .any(|subject| distinguished_names_equal(subject, &certificate.subject_dn));
1577    let mut issuer_serial_match = false;
1578    for (issuer, serial) in &info.issuer_serials {
1579        let serial_hex = x509_serial_decimal_to_hex(serial).ok_or_else(|| {
1580            ParseError::InvalidStructure(
1581                "X509Data lookup identifiers contain an invalid serial number".into(),
1582            )
1583        })?;
1584        issuer_serial_match |= distinguished_names_equal(issuer, &certificate.issuer_dn)
1585            && serial_hex == certificate.serial_number_hex;
1586    }
1587    let ski_match = certificate
1588        .subject_key_identifier
1589        .as_ref()
1590        .is_some_and(|certificate_ski| info.skis.iter().any(|ski| ski == certificate_ski));
1591    let mut digest_match = false;
1592    for (algorithm_uri, expected) in &info.digests {
1593        let algorithm = DigestAlgorithm::from_uri(algorithm_uri).ok_or_else(|| {
1594            ParseError::UnsupportedAlgorithm {
1595                uri: algorithm_uri.clone(),
1596            }
1597        })?;
1598        digest_match |= constant_time_eq(
1599            &compute_digest_with_provider(provider, algorithm, certificate_der)?,
1600            expected,
1601        );
1602    }
1603    Ok(subject_match || issuer_serial_match || ski_match || digest_match)
1604}
1605
1606pub(crate) fn x509_selector_categories_match_chain(
1607    info: &X509DataInfo,
1608    provider: &dyn crate::provider::CryptoProvider,
1609) -> Result<bool, ParseError> {
1610    let subject_match = info.subject_names.iter().all(|subject| {
1611        info.parsed_certificates
1612            .iter()
1613            .any(|certificate| distinguished_names_equal(subject, &certificate.subject_dn))
1614    });
1615
1616    let mut issuer_serial_match = true;
1617    for (issuer, serial) in &info.issuer_serials {
1618        let serial_hex = x509_serial_decimal_to_hex(serial).ok_or_else(|| {
1619            ParseError::InvalidStructure(
1620                "X509Data lookup identifiers contain an invalid serial number".into(),
1621            )
1622        })?;
1623        issuer_serial_match &= info.parsed_certificates.iter().any(|certificate| {
1624            distinguished_names_equal(issuer, &certificate.issuer_dn)
1625                && serial_hex == certificate.serial_number_hex
1626        });
1627    }
1628
1629    let ski_match = info.skis.iter().all(|ski| {
1630        info.parsed_certificates.iter().any(|certificate| {
1631            certificate
1632                .subject_key_identifier
1633                .as_ref()
1634                .is_some_and(|certificate_ski| ski == certificate_ski)
1635        })
1636    });
1637
1638    let mut digest_match = true;
1639    for (algorithm_uri, expected) in &info.digests {
1640        let algorithm = DigestAlgorithm::from_uri(algorithm_uri).ok_or_else(|| {
1641            ParseError::UnsupportedAlgorithm {
1642                uri: algorithm_uri.clone(),
1643            }
1644        })?;
1645        let mut category_match = false;
1646        for certificate in &info.certificates {
1647            category_match |= constant_time_eq(
1648                &compute_digest_with_provider(provider, algorithm, certificate)?,
1649                expected,
1650            );
1651        }
1652        digest_match &= category_match;
1653    }
1654
1655    Ok(subject_match && issuer_serial_match && ski_match && digest_match)
1656}
1657
1658fn x509_attribute_values_equal(
1659    left: &x509_cert::attr::AttributeTypeAndValue,
1660    right: &x509_cert::attr::AttributeTypeAndValue,
1661) -> bool {
1662    if left.oid != right.oid {
1663        return false;
1664    }
1665    const EMAIL_ADDRESS: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.1");
1666    const DOMAIN_COMPONENT: ObjectIdentifier =
1667        ObjectIdentifier::new_unwrap("0.9.2342.19200300.100.1.25");
1668    if left.oid == EMAIL_ADDRESS {
1669        let (Ok(left), Ok(right)) = (
1670            Ia5StringRef::try_from(&left.value),
1671            Ia5StringRef::try_from(&right.value),
1672        ) else {
1673            return false;
1674        };
1675        let (Some((left_local, left_domain)), Some((right_local, right_domain))) = (
1676            left.as_str().rsplit_once('@'),
1677            right.as_str().rsplit_once('@'),
1678        ) else {
1679            return false;
1680        };
1681        return left_local == right_local && left_domain.eq_ignore_ascii_case(right_domain);
1682    }
1683    if left.oid == DOMAIN_COMPONENT {
1684        let (Ok(left), Ok(right)) = (
1685            Ia5StringRef::try_from(&left.value),
1686            Ia5StringRef::try_from(&right.value),
1687        ) else {
1688            return false;
1689        };
1690        return left.as_str().eq_ignore_ascii_case(right.as_str());
1691    }
1692    match (
1693        DirectoryString::try_from(&left.value),
1694        DirectoryString::try_from(&right.value),
1695    ) {
1696        (Ok(left), Ok(right)) => {
1697            // RFC 5280 section 7.1 requires caseIgnoreMatch with LDAP/X.520
1698            // string preparation for PrintableString and UTF8String names.
1699            let Ok(left) =
1700                x520_stringprep::x520_stringprep_to_case_ignore_string(left.value().as_ref())
1701            else {
1702                return false;
1703            };
1704            let Ok(right) =
1705                x520_stringprep::x520_stringprep_to_case_ignore_string(right.value().as_ref())
1706            else {
1707                return false;
1708            };
1709            left.trim_matches(' ') == right.trim_matches(' ')
1710        }
1711        _ => left.value == right.value,
1712    }
1713}
1714
1715fn x509_rdns_equal(
1716    left: &x509_cert::name::RelativeDistinguishedName,
1717    right: &x509_cert::name::RelativeDistinguishedName,
1718) -> bool {
1719    if left.len() != right.len() {
1720        return false;
1721    }
1722    // A DN is an ordered RDN sequence, but each individual RDN is a set.
1723    let right = right.iter().collect::<Vec<_>>();
1724    let mut matched = vec![false; right.len()];
1725    left.iter().all(|left_attribute| {
1726        right
1727            .iter()
1728            .enumerate()
1729            .find(|(index, right_attribute)| {
1730                !matched[*index] && x509_attribute_values_equal(left_attribute, right_attribute)
1731            })
1732            .is_some_and(|(index, _)| {
1733                matched[index] = true;
1734                true
1735            })
1736    })
1737}
1738
1739fn trailing_whitespace_is_escaped(value: &str) -> bool {
1740    let Some((&last, prefix)) = value.as_bytes().split_last() else {
1741        return false;
1742    };
1743    if !matches!(last, b' ' | b'\t' | b'\r' | b'\n') {
1744        return false;
1745    }
1746    prefix
1747        .iter()
1748        .rev()
1749        .take_while(|byte| **byte == b'\\')
1750        .count()
1751        % 2
1752        == 1
1753}
1754
1755fn parse_distinguished_name(value: &str) -> Option<Name> {
1756    let mut normalized = String::with_capacity(value.len());
1757    let mut chars = value
1758        .trim_start_matches([' ', '\t', '\r', '\n'])
1759        .chars()
1760        .peekable();
1761    let mut escaped = false;
1762
1763    while let Some(ch) = chars.next() {
1764        if escaped {
1765            normalized.push(ch);
1766            escaped = false;
1767            continue;
1768        }
1769        if ch == '\\' {
1770            normalized.push(ch);
1771            escaped = true;
1772            continue;
1773        }
1774        if matches!(ch, ',' | '+') {
1775            while normalized
1776                .chars()
1777                .next_back()
1778                .is_some_and(|last| matches!(last, ' ' | '\t' | '\r' | '\n'))
1779                && !trailing_whitespace_is_escaped(&normalized)
1780            {
1781                normalized.pop();
1782            }
1783            normalized.push(ch);
1784            while chars
1785                .next_if(|next| matches!(next, ' ' | '\t' | '\r' | '\n'))
1786                .is_some()
1787            {}
1788            continue;
1789        }
1790        normalized.push(ch);
1791    }
1792
1793    while normalized
1794        .chars()
1795        .next_back()
1796        .is_some_and(|last| matches!(last, ' ' | '\t' | '\r' | '\n'))
1797        && !trailing_whitespace_is_escaped(&normalized)
1798    {
1799        normalized.pop();
1800    }
1801    normalized.parse().ok()
1802}
1803
1804pub(crate) fn distinguished_names_equal(left: &str, right: &str) -> bool {
1805    parse_distinguished_name(left)
1806        .zip(parse_distinguished_name(right))
1807        .is_some_and(|(left, right)| {
1808            left.len() == right.len()
1809                && left
1810                    .iter_rdn()
1811                    .zip(right.iter_rdn())
1812                    .all(|(left, right)| x509_rdns_equal(left, right))
1813        })
1814}
1815
1816pub(crate) fn distinguished_name_within_subtree(name: &str, subtree: &str) -> bool {
1817    parse_distinguished_name(name)
1818        .zip(parse_distinguished_name(subtree))
1819        .is_some_and(|(name, subtree)| {
1820            subtree.len() <= name.len()
1821                && name
1822                    .iter_rdn()
1823                    .zip(subtree.iter_rdn())
1824                    .all(|(name, subtree)| x509_rdns_equal(name, subtree))
1825        })
1826}
1827
1828fn ensure_x509_data_entry_budget(info: &X509DataInfo) -> Result<(), ParseError> {
1829    let total_entries = info.certificates.len()
1830        + info.subject_names.len()
1831        + info.issuer_serials.len()
1832        + info.skis.len()
1833        + info.crls.len()
1834        + info.digests.len();
1835    if total_entries >= MAX_X509_DATA_ENTRY_COUNT {
1836        return Err(ParseError::InvalidStructure(
1837            "X509Data contains too many entries".into(),
1838        ));
1839    }
1840    Ok(())
1841}
1842
1843fn add_x509_data_usage(total_binary_len: &mut usize, delta: usize) -> Result<(), ParseError> {
1844    *total_binary_len = total_binary_len.checked_add(delta).ok_or_else(|| {
1845        ParseError::InvalidStructure("X509Data exceeds maximum allowed total binary length".into())
1846    })?;
1847    if *total_binary_len > MAX_X509_DATA_TOTAL_BINARY_LEN {
1848        return Err(ParseError::InvalidStructure(
1849            "X509Data exceeds maximum allowed total binary length".into(),
1850        ));
1851    }
1852    Ok(())
1853}
1854
1855fn decode_x509_base64(
1856    node: Node<'_, '_>,
1857    element_name: &'static str,
1858) -> Result<Vec<u8>, ParseError> {
1859    use base64::Engine;
1860    use base64::engine::general_purpose::STANDARD;
1861
1862    let mut cleaned = String::new();
1863    let mut raw_text_len = 0usize;
1864    for text in node
1865        .children()
1866        .filter(|child| child.is_text())
1867        .filter_map(|child| child.text())
1868    {
1869        if raw_text_len.saturating_add(text.len()) > MAX_X509_BASE64_TEXT_LEN {
1870            return Err(ParseError::InvalidStructure(format!(
1871                "{element_name} exceeds maximum allowed text length"
1872            )));
1873        }
1874        raw_text_len = raw_text_len.saturating_add(text.len());
1875        normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
1876            ParseError::Base64(format!(
1877                "invalid XML whitespace U+{:04X} in {element_name}",
1878                err.invalid_byte
1879            ))
1880        })?;
1881        if cleaned.len() > MAX_X509_BASE64_NORMALIZED_LEN {
1882            return Err(ParseError::InvalidStructure(format!(
1883                "{element_name} exceeds maximum allowed base64 length"
1884            )));
1885        }
1886    }
1887
1888    let decoded = STANDARD
1889        .decode(&cleaned)
1890        .map_err(|e| ParseError::Base64(format!("{element_name}: {e}")))?;
1891    if decoded.is_empty() {
1892        return Err(ParseError::InvalidStructure(format!(
1893            "{element_name} must not be empty"
1894        )));
1895    }
1896    if decoded.len() > MAX_X509_DECODED_BINARY_LEN {
1897        return Err(ParseError::InvalidStructure(format!(
1898            "{element_name} exceeds maximum allowed binary length"
1899        )));
1900    }
1901    Ok(decoded)
1902}
1903
1904pub(crate) fn parse_x509_certificate(cert_der: &[u8]) -> Result<ParsedX509Certificate, ParseError> {
1905    let (rest, cert) =
1906        x509_parser::certificate::X509Certificate::from_der(cert_der).map_err(|err| {
1907            ParseError::InvalidStructure(format!("X509Certificate is not valid DER X.509: {err}"))
1908        })?;
1909    if !rest.is_empty() {
1910        return Err(ParseError::InvalidStructure(
1911            "X509Certificate contains trailing bytes after DER certificate".into(),
1912        ));
1913    }
1914
1915    // x509-parser displays the DER RDN sequence in storage order, while
1916    // XMLDSig names follow RFC 4514 and serialize that sequence in reverse.
1917    // Normalize at the certificate boundary so matching remains ordered and
1918    // cannot confuse a DN with another hierarchy containing reversed RDNs.
1919    let subject_dn = x509_name_to_rfc4514(cert.subject())?;
1920    let issuer_dn = x509_name_to_rfc4514(cert.issuer())?;
1921    let serial_number = cert.tbs_certificate.raw_serial().to_vec();
1922    let serial_number_hex = format_x509_serial_value_hex(&serial_number);
1923
1924    let subject_key_identifier = cert.extensions().iter().find_map(|ext| {
1925        if let ParsedExtension::SubjectKeyIdentifier(ski) = ext.parsed_extension() {
1926            Some(ski.0.to_vec())
1927        } else {
1928            None
1929        }
1930    });
1931
1932    let spki = cert.public_key();
1933    let public_key = match spki.parsed().map_err(|err| {
1934        ParseError::InvalidStructure(format!("X509Certificate public key parse error: {err}"))
1935    })? {
1936        PublicKey::RSA(rsa) => {
1937            let modulus = trim_leading_zeroes(rsa.modulus);
1938            let exponent = trim_leading_zeroes(rsa.exponent);
1939            if modulus.is_empty() || exponent.is_empty() {
1940                return Err(ParseError::InvalidStructure(
1941                    "X509Certificate RSA key contains empty modulus or exponent".into(),
1942                ));
1943            }
1944            X509PublicKeyInfo::Rsa { modulus, exponent }
1945        }
1946        PublicKey::EC(ec_point) => {
1947            let Some(params) = spki.algorithm.parameters.as_ref() else {
1948                return Err(ParseError::InvalidStructure(
1949                    "X509Certificate EC key is missing curve parameters".into(),
1950                ));
1951            };
1952
1953            match params.as_oid() {
1954                Ok(oid) => X509PublicKeyInfo::Ec {
1955                    curve_oid: oid.to_id_string(),
1956                    public_key: ec_point.data().to_vec(),
1957                },
1958                Err(_) => X509PublicKeyInfo::Unsupported {
1959                    algorithm_oid: spki.algorithm.algorithm.to_id_string(),
1960                },
1961            }
1962        }
1963        _ => X509PublicKeyInfo::Unsupported {
1964            algorithm_oid: spki.algorithm.algorithm.to_id_string(),
1965        },
1966    };
1967
1968    Ok(ParsedX509Certificate {
1969        subject_dn,
1970        issuer_dn,
1971        serial_number,
1972        serial_number_hex,
1973        subject_key_identifier,
1974        public_key,
1975    })
1976}
1977
1978pub(crate) fn x509_name_to_rfc4514(name: &X509Name<'_>) -> Result<String, ParseError> {
1979    let name = Name::from_der(name.as_raw()).map_err(|error| {
1980        ParseError::InvalidStructure(format!(
1981            "X509Certificate distinguished name is invalid DER: {error}"
1982        ))
1983    })?;
1984    Ok(name.to_string())
1985}
1986
1987fn format_x509_serial_hex(serial: &[u8]) -> String {
1988    serial
1989        .iter()
1990        .map(|byte| format!("{byte:02X}"))
1991        .collect::<String>()
1992}
1993
1994fn format_x509_serial_value_hex(serial: &[u8]) -> String {
1995    let first_non_zero = serial
1996        .iter()
1997        .position(|byte| *byte != 0)
1998        .unwrap_or(serial.len());
1999    let canonical = if first_non_zero == serial.len() {
2000        &[0]
2001    } else {
2002        &serial[first_non_zero..]
2003    };
2004    format_x509_serial_hex(canonical)
2005}
2006
2007fn x509_serial_decimal_to_hex(serial: &str) -> Option<String> {
2008    let serial = serial.trim();
2009    let serial = serial.strip_prefix('+').unwrap_or(serial);
2010    let serial = serial.trim_start_matches('0');
2011    let serial = if serial.is_empty() { "0" } else { serial };
2012    if serial.len() > MAX_X509_SERIAL_NUMBER_VALUE_DIGITS
2013        || !serial.bytes().all(|byte| byte.is_ascii_digit())
2014    {
2015        return None;
2016    }
2017
2018    let mut bytes = [0_u8; MAX_X509_SERIAL_NUMBER_BYTES];
2019    for digit in serial.bytes().map(|byte| byte - b'0') {
2020        let mut carry = u16::from(digit);
2021        for byte in bytes.iter_mut().rev() {
2022            let value = u16::from(*byte) * 10 + carry;
2023            *byte = value as u8;
2024            carry = value >> 8;
2025        }
2026        if carry != 0 {
2027            return None;
2028        }
2029    }
2030
2031    if bytes.iter().all(|byte| *byte == 0) {
2032        return None;
2033    }
2034
2035    Some(format_x509_serial_value_hex(&bytes))
2036}
2037
2038fn trim_leading_zeroes(bytes: &[u8]) -> Vec<u8> {
2039    let first_non_zero = bytes
2040        .iter()
2041        .position(|byte| *byte != 0)
2042        .unwrap_or(bytes.len());
2043    bytes[first_non_zero..].to_vec()
2044}
2045
2046fn parse_x509_issuer_serial(node: Node<'_, '_>) -> Result<(String, String), ParseError> {
2047    verify_ds_element(node, "X509IssuerSerial")?;
2048    ensure_no_non_whitespace_text(node, "X509IssuerSerial")?;
2049
2050    let children = element_children(node).collect::<Vec<_>>();
2051    if children.len() != 2 {
2052        return Err(ParseError::InvalidStructure(
2053            "X509IssuerSerial must contain exactly X509IssuerName then X509SerialNumber".into(),
2054        ));
2055    }
2056    if !matches!(
2057        (
2058            children[0].tag_name().namespace(),
2059            children[0].tag_name().name()
2060        ),
2061        (Some(XMLDSIG_NS), "X509IssuerName")
2062    ) {
2063        return Err(ParseError::InvalidStructure(
2064            "X509IssuerSerial must contain X509IssuerName as the first child element".into(),
2065        ));
2066    }
2067    if !matches!(
2068        (
2069            children[1].tag_name().namespace(),
2070            children[1].tag_name().name()
2071        ),
2072        (Some(XMLDSIG_NS), "X509SerialNumber")
2073    ) {
2074        return Err(ParseError::InvalidStructure(
2075            "X509IssuerSerial must contain X509SerialNumber as the second child element".into(),
2076        ));
2077    }
2078
2079    let issuer_node = children[0];
2080    ensure_no_element_children(issuer_node, "X509IssuerName")?;
2081    let issuer_name =
2082        collect_text_content_bounded(issuer_node, MAX_X509_ISSUER_NAME_TEXT_LEN, "X509IssuerName")?;
2083
2084    let serial_node = children[1];
2085    ensure_no_element_children(serial_node, "X509SerialNumber")?;
2086    let serial_number = collect_x509_serial_number(serial_node)?;
2087    if issuer_name.trim().is_empty() {
2088        return Err(ParseError::InvalidStructure(
2089            "X509IssuerSerial requires non-empty X509IssuerName and X509SerialNumber".into(),
2090        ));
2091    }
2092
2093    Ok((issuer_name, serial_number))
2094}
2095
2096/// Base64-decode a digest value string, stripping whitespace.
2097///
2098/// XMLDSig allows whitespace within base64 content (line-wrapped encodings).
2099fn base64_decode_digest(b64: &str, digest_method: DigestAlgorithm) -> Result<Vec<u8>, ParseError> {
2100    use base64::Engine;
2101    use base64::engine::general_purpose::STANDARD;
2102
2103    let expected = digest_method.output_len();
2104    let max_base64_len = expected.div_ceil(3) * 4;
2105    let mut cleaned = String::with_capacity(b64.len().min(max_base64_len));
2106    normalize_xml_base64_text(b64, &mut cleaned).map_err(|err| {
2107        ParseError::Base64(format!(
2108            "invalid XML whitespace U+{:04X} in DigestValue",
2109            err.invalid_byte
2110        ))
2111    })?;
2112    if cleaned.len() > max_base64_len {
2113        return Err(ParseError::Base64(
2114            "DigestValue exceeds maximum allowed base64 length".into(),
2115        ));
2116    }
2117    let digest = STANDARD
2118        .decode(&cleaned)
2119        .map_err(|e| ParseError::Base64(e.to_string()))?;
2120    let actual = digest.len();
2121    if actual != expected {
2122        return Err(ParseError::DigestLengthMismatch {
2123            algorithm: digest_method.uri(),
2124            expected,
2125            actual,
2126        });
2127    }
2128    Ok(digest)
2129}
2130
2131fn decode_digest_value_children(
2132    digest_value_node: Node<'_, '_>,
2133    digest_method: DigestAlgorithm,
2134) -> Result<Vec<u8>, ParseError> {
2135    let max_base64_len = digest_method.output_len().div_ceil(3) * 4;
2136    let mut cleaned = String::with_capacity(max_base64_len);
2137
2138    for child in digest_value_node.children() {
2139        if child.is_element() {
2140            return Err(ParseError::InvalidStructure(
2141                "DigestValue must not contain element children".into(),
2142            ));
2143        }
2144        if let Some(text) = child.text() {
2145            normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
2146                ParseError::Base64(format!(
2147                    "invalid XML whitespace U+{:04X} in DigestValue",
2148                    err.invalid_byte
2149                ))
2150            })?;
2151            if cleaned.len() > max_base64_len {
2152                return Err(ParseError::Base64(
2153                    "DigestValue exceeds maximum allowed base64 length".into(),
2154                ));
2155            }
2156        }
2157    }
2158
2159    base64_decode_digest(&cleaned, digest_method)
2160}
2161
2162fn decode_der_encoded_key_value_base64(node: Node<'_, '_>) -> Result<Vec<u8>, ParseError> {
2163    use base64::Engine;
2164    use base64::engine::general_purpose::STANDARD;
2165
2166    let mut cleaned = String::new();
2167    let mut raw_text_len = 0usize;
2168    for text in node
2169        .children()
2170        .filter(|child| child.is_text())
2171        .filter_map(|child| child.text())
2172    {
2173        if raw_text_len.saturating_add(text.len()) > MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN {
2174            return Err(ParseError::InvalidStructure(
2175                "DEREncodedKeyValue exceeds maximum allowed text length".into(),
2176            ));
2177        }
2178        raw_text_len = raw_text_len.saturating_add(text.len());
2179        normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
2180            ParseError::Base64(format!(
2181                "invalid XML whitespace U+{:04X} in base64 text",
2182                err.invalid_byte
2183            ))
2184        })?;
2185        if cleaned.len() > MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN {
2186            return Err(ParseError::InvalidStructure(
2187                "DEREncodedKeyValue exceeds maximum allowed length".into(),
2188            ));
2189        }
2190    }
2191
2192    let der = STANDARD
2193        .decode(&cleaned)
2194        .map_err(|e| ParseError::Base64(e.to_string()))?;
2195    if der.is_empty() {
2196        return Err(ParseError::InvalidStructure(
2197            "DEREncodedKeyValue must not be empty".into(),
2198        ));
2199    }
2200    if der.len() > MAX_DER_ENCODED_KEY_VALUE_LEN {
2201        return Err(ParseError::InvalidStructure(
2202            "DEREncodedKeyValue exceeds maximum allowed length".into(),
2203        ));
2204    }
2205    Ok(der)
2206}
2207
2208fn collect_text_content_bounded(
2209    node: Node<'_, '_>,
2210    max_len: usize,
2211    element_name: &'static str,
2212) -> Result<String, ParseError> {
2213    let mut text = String::new();
2214    for chunk in node
2215        .children()
2216        .filter_map(|child| child.is_text().then(|| child.text()).flatten())
2217    {
2218        if text.len().saturating_add(chunk.len()) > max_len {
2219            return Err(ParseError::InvalidStructure(format!(
2220                "{element_name} exceeds maximum allowed text length"
2221            )));
2222        }
2223        text.push_str(chunk);
2224    }
2225    Ok(text)
2226}
2227
2228fn collect_x509_serial_number(node: Node<'_, '_>) -> Result<String, ParseError> {
2229    let mut serial = String::with_capacity(MAX_X509_SERIAL_NUMBER_VALUE_DIGITS);
2230    let mut raw_text_len = 0usize;
2231    let mut trailing_whitespace = false;
2232    let mut explicit_positive = false;
2233    let mut saw_digit = false;
2234
2235    for chunk in node
2236        .children()
2237        .filter_map(|child| child.is_text().then(|| child.text()).flatten())
2238    {
2239        raw_text_len = raw_text_len.saturating_add(chunk.len());
2240        if raw_text_len > MAX_X509_SERIAL_NUMBER_RAW_TEXT_LEN {
2241            return Err(ParseError::InvalidStructure(
2242                "X509SerialNumber exceeds maximum allowed text length".into(),
2243            ));
2244        }
2245        for byte in chunk.bytes() {
2246            if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
2247                trailing_whitespace |= explicit_positive || saw_digit;
2248                continue;
2249            }
2250            if byte == b'+' && !saw_digit && !explicit_positive && !trailing_whitespace {
2251                explicit_positive = true;
2252                continue;
2253            }
2254            if trailing_whitespace || !byte.is_ascii_digit() {
2255                return Err(ParseError::InvalidStructure(
2256                    "invalid X509SerialNumber decimal value".into(),
2257                ));
2258            }
2259            saw_digit = true;
2260            if byte == b'0' && serial.is_empty() {
2261                continue;
2262            }
2263            if serial.len() == MAX_X509_SERIAL_NUMBER_VALUE_DIGITS {
2264                return Err(ParseError::InvalidStructure(
2265                    "X509SerialNumber exceeds maximum allowed decimal value".into(),
2266                ));
2267            }
2268            serial.push(char::from(byte));
2269        }
2270    }
2271
2272    if !saw_digit {
2273        return Err(ParseError::InvalidStructure(
2274            "X509IssuerSerial requires non-empty X509IssuerName and X509SerialNumber".into(),
2275        ));
2276    }
2277    if serial.is_empty() {
2278        serial.push('0');
2279    }
2280    if x509_serial_decimal_to_hex(&serial).is_none() {
2281        return Err(ParseError::InvalidStructure(
2282            "invalid X509SerialNumber decimal value or RFC 5280 range".into(),
2283        ));
2284    }
2285
2286    Ok(serial)
2287}
2288
2289fn ensure_no_element_children(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
2290    if node.children().any(|child| child.is_element()) {
2291        return Err(ParseError::InvalidStructure(format!(
2292            "{element_name} must not contain child elements"
2293        )));
2294    }
2295    Ok(())
2296}
2297
2298fn ensure_no_non_whitespace_text(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
2299    for child in node.children().filter(|child| child.is_text()) {
2300        if let Some(text) = child.text()
2301            && !is_xml_whitespace_only(text)
2302        {
2303            return Err(ParseError::InvalidStructure(format!(
2304                "{element_name} must not contain non-whitespace mixed content"
2305            )));
2306        }
2307    }
2308    Ok(())
2309}
2310
2311#[cfg(test)]
2312#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
2313mod tests {
2314    use super::*;
2315    use base64::Engine;
2316
2317    fn fixture_rsa_cert_base64() -> String {
2318        fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem")
2319    }
2320
2321    fn fixture_cert_base64(path: &str) -> String {
2322        match path {
2323            "../../tests/fixtures/keys/rsa/rsa-2048-cert.pem" => {
2324                include_str!("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem")
2325            }
2326            "../../tests/fixtures/keys/rsa/rsa-4096-cert.pem" => {
2327                include_str!("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem")
2328            }
2329            "../../tests/fixtures/keys/ca2cert.pem" => {
2330                include_str!("../../tests/fixtures/keys/ca2cert.pem")
2331            }
2332            "../../tests/fixtures/keys/cacert.pem" => {
2333                include_str!("../../tests/fixtures/keys/cacert.pem")
2334            }
2335            _ => unreachable!("unknown certificate fixture"),
2336        }
2337        .lines()
2338        .skip_while(|line| *line != "-----BEGIN CERTIFICATE-----")
2339        .skip(1)
2340        .take_while(|line| *line != "-----END CERTIFICATE-----")
2341        .collect::<String>()
2342    }
2343
2344    // ── SignatureAlgorithm ───────────────────────────────────────────
2345
2346    #[test]
2347    fn signature_algorithm_from_uri_rsa_sha256() {
2348        assert_eq!(
2349            SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"),
2350            Some(SignatureAlgorithm::RsaSha256)
2351        );
2352    }
2353
2354    #[test]
2355    fn signature_algorithm_from_uri_rsa_sha1() {
2356        assert_eq!(
2357            SignatureAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
2358            Some(SignatureAlgorithm::RsaSha1)
2359        );
2360    }
2361
2362    #[test]
2363    fn signature_algorithm_from_uri_ecdsa_sha256() {
2364        assert_eq!(
2365            SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"),
2366            Some(SignatureAlgorithm::EcdsaSha256)
2367        );
2368    }
2369
2370    #[test]
2371    fn signature_algorithm_from_uri_unknown() {
2372        assert_eq!(
2373            SignatureAlgorithm::from_uri("http://example.com/unknown"),
2374            None
2375        );
2376    }
2377
2378    #[test]
2379    fn signature_algorithm_uri_round_trip() {
2380        for algo in [
2381            SignatureAlgorithm::DsaSha1,
2382            SignatureAlgorithm::HmacSha1,
2383            SignatureAlgorithm::RsaSha1,
2384            SignatureAlgorithm::RsaSha256,
2385            SignatureAlgorithm::RsaSha384,
2386            SignatureAlgorithm::RsaSha512,
2387            SignatureAlgorithm::EcdsaSha256,
2388            SignatureAlgorithm::EcdsaSha384,
2389        ] {
2390            assert_eq!(
2391                SignatureAlgorithm::from_uri(algo.uri()),
2392                Some(algo),
2393                "round-trip failed for {algo:?}"
2394            );
2395        }
2396    }
2397
2398    #[test]
2399    fn legacy_algorithms_are_verify_only() {
2400        assert!(!SignatureAlgorithm::DsaSha1.signing_allowed());
2401        assert!(!SignatureAlgorithm::HmacSha1.signing_allowed());
2402        assert!(!SignatureAlgorithm::RsaSha1.signing_allowed());
2403        assert!(SignatureAlgorithm::RsaSha256.signing_allowed());
2404        assert!(SignatureAlgorithm::EcdsaSha256.signing_allowed());
2405    }
2406
2407    // ── find_signature_node ──────────────────────────────────────────
2408
2409    #[test]
2410    fn find_signature_in_saml() {
2411        let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
2412            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2413                <ds:SignedInfo/>
2414            </ds:Signature>
2415        </samlp:Response>"#;
2416        let doc = Document::parse(xml).unwrap();
2417        let sig = find_signature_node(&doc);
2418        assert!(sig.is_some());
2419        assert_eq!(sig.unwrap().tag_name().name(), "Signature");
2420    }
2421
2422    #[test]
2423    fn find_signature_missing() {
2424        let xml = "<root><child/></root>";
2425        let doc = Document::parse(xml).unwrap();
2426        assert!(find_signature_node(&doc).is_none());
2427    }
2428
2429    #[test]
2430    fn find_signature_ignores_wrong_namespace() {
2431        let xml = r#"<root><Signature xmlns="http://example.com/fake"/></root>"#;
2432        let doc = Document::parse(xml).unwrap();
2433        assert!(find_signature_node(&doc).is_none());
2434    }
2435
2436    // ── parse_key_info: dispatch parsing ──────────────────────────────
2437
2438    #[test]
2439    fn parse_key_info_dispatches_supported_children() {
2440        let cert_base64 = fixture_rsa_cert_base64();
2441        let expected_cert = base64::engine::general_purpose::STANDARD
2442            .decode(&cert_base64)
2443            .expect("fixture PEM must contain valid base64");
2444        let cert_digest = base64::engine::general_purpose::STANDARD
2445            .encode(compute_digest(DigestAlgorithm::Sha256, &expected_cert));
2446        let xml = format!(
2447            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2448                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2449            <KeyName>idp-signing-key</KeyName>
2450            <KeyValue>
2451                <RSAKeyValue>
2452                    <Modulus>AQAB</Modulus>
2453                    <Exponent>AQAB</Exponent>
2454                </RSAKeyValue>
2455            </KeyValue>
2456            <X509Data>
2457                <X509Certificate>{cert_base64}</X509Certificate>
2458                <X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
2459                <X509IssuerSerial>
2460                    <X509IssuerName>Email=xmlsec@aleksey.com,CN=Aleksey Sanin,OU=Second level CA,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509IssuerName>
2461                    <X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
2462                </X509IssuerSerial>
2463                <X509SKI>bcOXN/nsVl8GatRbcKrPbzIbw0Y=</X509SKI>
2464                <X509CRL>BAUGBw==</X509CRL>
2465                <dsig11:X509Digest Algorithm="http://www.w3.org/2001/04/xmlenc#sha256">{cert_digest}</dsig11:X509Digest>
2466            </X509Data>
2467            <dsig11:DEREncodedKeyValue>AQIDBA==</dsig11:DEREncodedKeyValue>
2468        </KeyInfo>"#
2469        );
2470        let doc = Document::parse(&xml).unwrap();
2471
2472        let key_info = parse_key_info(doc.root_element()).unwrap();
2473        assert_eq!(key_info.sources.len(), 4);
2474
2475        assert_eq!(
2476            key_info.sources[0],
2477            KeyInfoSource::KeyName("idp-signing-key".to_string())
2478        );
2479        assert_eq!(
2480            key_info.sources[1],
2481            KeyInfoSource::KeyValue(KeyValueInfo::Rsa {
2482                modulus: vec![1, 0, 1],
2483                exponent: vec![1, 0, 1],
2484            })
2485        );
2486        let x509_info = match &key_info.sources[2] {
2487            KeyInfoSource::X509Data(x509) => x509,
2488            other => panic!("expected X509Data source, got {other:?}"),
2489        };
2490        assert_eq!(x509_info.certificates, vec![expected_cert]);
2491        assert_eq!(
2492            x509_info.subject_names,
2493            vec![
2494                "CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US"
2495                    .to_string()
2496            ]
2497        );
2498        assert_eq!(
2499            x509_info.issuer_serials,
2500            vec![(
2501                "Email=xmlsec@aleksey.com,CN=Aleksey Sanin,OU=Second level CA,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US".to_string(),
2502                "680572598617295163017172295025714171905498632019".to_string()
2503            )]
2504        );
2505        assert_eq!(
2506            x509_info.skis,
2507            vec![vec![
2508                109, 195, 151, 55, 249, 236, 86, 95, 6, 106, 212, 91, 112, 170, 207, 111, 50, 27,
2509                195, 70
2510            ]]
2511        );
2512        assert_eq!(x509_info.crls, vec![vec![4, 5, 6, 7]]);
2513        assert_eq!(
2514            x509_info.digests,
2515            vec![(
2516                "http://www.w3.org/2001/04/xmlenc#sha256".to_string(),
2517                compute_digest(DigestAlgorithm::Sha256, &x509_info.certificates[0])
2518            )]
2519        );
2520        assert_eq!(x509_info.parsed_certificates.len(), 1);
2521        assert_eq!(x509_info.certificate_chain, vec![0]);
2522        let parsed_cert = &x509_info.parsed_certificates[0];
2523        assert!(!parsed_cert.subject_dn.is_empty());
2524        assert!(!parsed_cert.issuer_dn.is_empty());
2525        assert_eq!(
2526            parsed_cert.serial_number_hex,
2527            "7735EE487F6862DAF1B3956D961CCB0FA6F34F53"
2528        );
2529        assert!(parsed_cert.subject_key_identifier.is_some());
2530        assert!(matches!(
2531            parsed_cert.public_key,
2532            X509PublicKeyInfo::Rsa { .. }
2533        ));
2534
2535        assert_eq!(
2536            key_info.sources[3],
2537            KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])
2538        );
2539    }
2540
2541    #[test]
2542    fn parse_rsa_key_value_preserves_wrapped_crypto_binary() {
2543        // CryptoBinary is unsigned big-endian data and XML whitespace is insignificant.
2544        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2545            <KeyValue><RSAKeyValue>
2546                <Modulus> AQID
2547BA== </Modulus>
2548                <Exponent> AQAB </Exponent>
2549            </RSAKeyValue></KeyValue>
2550        </KeyInfo>"##;
2551        let doc = Document::parse(xml).unwrap();
2552
2553        assert_eq!(
2554            parse_key_info(doc.root_element()).unwrap().sources,
2555            vec![KeyInfoSource::KeyValue(KeyValueInfo::Rsa {
2556                modulus: vec![1, 2, 3, 4],
2557                exponent: vec![1, 0, 1],
2558            })]
2559        );
2560    }
2561
2562    #[test]
2563    fn parse_rsa_key_value_rejects_reordered_parameters() {
2564        // XMLDSig defines Modulus followed by Exponent; accepting reordered input is ambiguous.
2565        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2566            <KeyValue><RSAKeyValue>
2567                <Exponent>AQAB</Exponent><Modulus>AQID</Modulus>
2568            </RSAKeyValue></KeyValue>
2569        </KeyInfo>"##;
2570        let doc = Document::parse(xml).unwrap();
2571
2572        assert!(matches!(
2573            parse_key_info(doc.root_element()),
2574            Err(ParseError::InvalidStructure(_))
2575        ));
2576    }
2577
2578    #[test]
2579    fn parse_rsa_key_value_rejects_missing_exponent() {
2580        // Both RSA public parameters are required to construct a usable key.
2581        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2582            <KeyValue><RSAKeyValue><Modulus>AQID</Modulus></RSAKeyValue></KeyValue>
2583        </KeyInfo>"##;
2584        let doc = Document::parse(xml).unwrap();
2585
2586        assert!(matches!(
2587            parse_key_info(doc.root_element()),
2588            Err(ParseError::InvalidStructure(_))
2589        ));
2590    }
2591
2592    #[test]
2593    fn parse_rsa_key_value_rejects_duplicate_exponent() {
2594        // RSAKeyValue has a closed two-child schema; duplicate parameters are invalid.
2595        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2596            <KeyValue><RSAKeyValue>
2597                <Modulus>AQID</Modulus><Exponent>AQAB</Exponent><Exponent>AQAB</Exponent>
2598            </RSAKeyValue></KeyValue>
2599        </KeyInfo>"##;
2600        let doc = Document::parse(xml).unwrap();
2601
2602        assert!(matches!(
2603            parse_key_info(doc.root_element()),
2604            Err(ParseError::InvalidStructure(_))
2605        ));
2606    }
2607
2608    #[test]
2609    fn parse_rsa_key_value_rejects_wrong_parameter_namespace() {
2610        // Local names from an extension namespace must not be treated as XMLDSig parameters.
2611        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:bad="urn:bad">
2612            <KeyValue><RSAKeyValue>
2613                <bad:Modulus>AQID</bad:Modulus><Exponent>AQAB</Exponent>
2614            </RSAKeyValue></KeyValue>
2615        </KeyInfo>"#;
2616        let doc = Document::parse(xml).unwrap();
2617
2618        assert!(matches!(
2619            parse_key_info(doc.root_element()),
2620            Err(ParseError::InvalidStructure(_))
2621        ));
2622    }
2623
2624    #[test]
2625    fn parse_rsa_key_value_rejects_nested_crypto_binary() {
2626        // CryptoBinary values are text-only and must not hide extension elements.
2627        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2628            <KeyValue><RSAKeyValue>
2629                <Modulus><chunk>AQID</chunk></Modulus><Exponent>AQAB</Exponent>
2630            </RSAKeyValue></KeyValue>
2631        </KeyInfo>"#;
2632        let doc = Document::parse(xml).unwrap();
2633
2634        assert!(matches!(
2635            parse_key_info(doc.root_element()),
2636            Err(ParseError::InvalidStructure(_))
2637        ));
2638    }
2639
2640    #[test]
2641    fn parse_rsa_key_value_rejects_malformed_base64() {
2642        // Malformed key parameters must be processing errors, not unresolved keys.
2643        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2644            <KeyValue><RSAKeyValue>
2645                <Modulus>%%%%</Modulus><Exponent>AQAB</Exponent>
2646            </RSAKeyValue></KeyValue>
2647        </KeyInfo>"#;
2648        let doc = Document::parse(xml).unwrap();
2649
2650        assert!(matches!(
2651            parse_key_info(doc.root_element()),
2652            Err(ParseError::Base64(_))
2653        ));
2654    }
2655
2656    #[test]
2657    fn parse_rsa_key_value_rejects_oversized_exponent_before_decode() {
2658        // Bound normalized text before allocation or integer construction.
2659        let exponent = "A".repeat(MAX_RSA_EXPONENT_LEN.div_ceil(3) * 4 + 1);
2660        let xml = format!(
2661            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2662                <KeyValue><RSAKeyValue>
2663                    <Modulus>AQID</Modulus><Exponent>{exponent}</Exponent>
2664                </RSAKeyValue></KeyValue>
2665            </KeyInfo>"#
2666        );
2667        let doc = Document::parse(&xml).unwrap();
2668
2669        assert!(matches!(
2670            parse_key_info(doc.root_element()),
2671            Err(ParseError::InvalidStructure(_))
2672        ));
2673    }
2674
2675    #[test]
2676    fn parse_key_info_ignores_unknown_children() {
2677        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2678            <Foo>bar</Foo>
2679            <KeyName>ok</KeyName>
2680        </KeyInfo>"#;
2681        let doc = Document::parse(xml).unwrap();
2682
2683        let key_info = parse_key_info(doc.root_element()).unwrap();
2684        assert_eq!(key_info.sources, vec![KeyInfoSource::KeyName("ok".into())]);
2685    }
2686
2687    #[test]
2688    fn parse_key_info_keyvalue_requires_single_child() {
2689        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2690            <KeyValue/>
2691        </KeyInfo>"#;
2692        let doc = Document::parse(xml).unwrap();
2693
2694        let err = parse_key_info(doc.root_element()).unwrap_err();
2695        assert!(matches!(err, ParseError::InvalidStructure(_)));
2696    }
2697
2698    #[test]
2699    fn parse_key_info_accepts_empty_x509data() {
2700        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2701            <X509Data/>
2702        </KeyInfo>"#;
2703        let doc = Document::parse(xml).unwrap();
2704
2705        let key_info = parse_key_info(doc.root_element()).unwrap();
2706        assert_eq!(
2707            key_info.sources,
2708            vec![KeyInfoSource::X509Data(X509DataInfo::default())]
2709        );
2710    }
2711
2712    #[test]
2713    fn parse_key_info_rejects_unknown_xmlsig_child_in_x509data() {
2714        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2715            <X509Data>
2716                <Foo/>
2717            </X509Data>
2718        </KeyInfo>"#;
2719        let doc = Document::parse(xml).unwrap();
2720
2721        let err = parse_key_info(doc.root_element()).unwrap_err();
2722        assert!(matches!(err, ParseError::InvalidStructure(_)));
2723    }
2724
2725    #[test]
2726    fn parse_key_info_rejects_unknown_xmlsig11_child_in_x509data() {
2727        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2728                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2729            <X509Data>
2730                <dsig11:Foo/>
2731            </X509Data>
2732        </KeyInfo>"#;
2733        let doc = Document::parse(xml).unwrap();
2734
2735        let err = parse_key_info(doc.root_element()).unwrap_err();
2736        assert!(matches!(err, ParseError::InvalidStructure(_)));
2737    }
2738
2739    #[test]
2740    fn parse_key_info_rejects_x509_issuer_serial_without_required_children() {
2741        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2742            <X509Data>
2743                <X509IssuerSerial>
2744                    <X509IssuerName>CN=CA</X509IssuerName>
2745                </X509IssuerSerial>
2746            </X509Data>
2747        </KeyInfo>"#;
2748        let doc = Document::parse(xml).unwrap();
2749
2750        let err = parse_key_info(doc.root_element()).unwrap_err();
2751        assert!(matches!(err, ParseError::InvalidStructure(_)));
2752    }
2753
2754    #[test]
2755    fn parse_key_info_rejects_x509_issuer_serial_with_duplicate_issuer_name() {
2756        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2757            <X509Data>
2758                <X509IssuerSerial>
2759                    <X509IssuerName>CN=CA-1</X509IssuerName>
2760                    <X509IssuerName>CN=CA-2</X509IssuerName>
2761                    <X509SerialNumber>42</X509SerialNumber>
2762                </X509IssuerSerial>
2763            </X509Data>
2764        </KeyInfo>"#;
2765        let doc = Document::parse(xml).unwrap();
2766
2767        let err = parse_key_info(doc.root_element()).unwrap_err();
2768        assert!(matches!(err, ParseError::InvalidStructure(_)));
2769    }
2770
2771    #[test]
2772    fn parse_key_info_rejects_x509_issuer_serial_with_duplicate_serial_number() {
2773        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2774            <X509Data>
2775                <X509IssuerSerial>
2776                    <X509IssuerName>CN=CA</X509IssuerName>
2777                    <X509SerialNumber>1</X509SerialNumber>
2778                    <X509SerialNumber>2</X509SerialNumber>
2779                </X509IssuerSerial>
2780            </X509Data>
2781        </KeyInfo>"#;
2782        let doc = Document::parse(xml).unwrap();
2783
2784        let err = parse_key_info(doc.root_element()).unwrap_err();
2785        assert!(matches!(err, ParseError::InvalidStructure(_)));
2786    }
2787
2788    #[test]
2789    fn parse_key_info_rejects_x509_issuer_serial_with_whitespace_only_values() {
2790        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2791            <X509Data>
2792                <X509IssuerSerial>
2793                    <X509IssuerName>   </X509IssuerName>
2794                    <X509SerialNumber>
2795                        
2796                    </X509SerialNumber>
2797                </X509IssuerSerial>
2798            </X509Data>
2799        </KeyInfo>"#;
2800        let doc = Document::parse(xml).unwrap();
2801
2802        let err = parse_key_info(doc.root_element()).unwrap_err();
2803        assert!(matches!(err, ParseError::InvalidStructure(_)));
2804    }
2805
2806    #[test]
2807    fn parse_key_info_rejects_x509_issuer_serial_with_wrong_child_order() {
2808        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2809            <X509Data>
2810                <X509IssuerSerial>
2811                    <X509SerialNumber>42</X509SerialNumber>
2812                    <X509IssuerName>CN=CA</X509IssuerName>
2813                </X509IssuerSerial>
2814            </X509Data>
2815        </KeyInfo>"#;
2816        let doc = Document::parse(xml).unwrap();
2817
2818        let err = parse_key_info(doc.root_element()).unwrap_err();
2819        assert!(matches!(err, ParseError::InvalidStructure(_)));
2820    }
2821
2822    #[test]
2823    fn parse_key_info_rejects_x509_issuer_serial_with_extra_child_element() {
2824        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2825                              xmlns:foo="urn:example:foo">
2826            <X509Data>
2827                <X509IssuerSerial>
2828                    <X509IssuerName>CN=CA</X509IssuerName>
2829                    <X509SerialNumber>42</X509SerialNumber>
2830                    <foo:Extra/>
2831                </X509IssuerSerial>
2832            </X509Data>
2833        </KeyInfo>"#;
2834        let doc = Document::parse(xml).unwrap();
2835
2836        let err = parse_key_info(doc.root_element()).unwrap_err();
2837        assert!(matches!(err, ParseError::InvalidStructure(_)));
2838    }
2839
2840    #[test]
2841    fn parse_key_info_rejects_x509_digest_without_algorithm() {
2842        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2843                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2844            <X509Data>
2845                <dsig11:X509Digest>AQID</dsig11:X509Digest>
2846            </X509Data>
2847        </KeyInfo>"#;
2848        let doc = Document::parse(xml).unwrap();
2849
2850        let err = parse_key_info(doc.root_element()).unwrap_err();
2851        assert!(matches!(err, ParseError::InvalidStructure(_)));
2852    }
2853
2854    #[test]
2855    fn parse_key_info_rejects_invalid_x509_certificate_base64() {
2856        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2857            <X509Data>
2858                <X509Certificate>%%%invalid%%%</X509Certificate>
2859            </X509Data>
2860        </KeyInfo>"#;
2861        let doc = Document::parse(xml).unwrap();
2862
2863        let err = parse_key_info(doc.root_element()).unwrap_err();
2864        assert!(matches!(err, ParseError::Base64(_)));
2865    }
2866
2867    #[test]
2868    fn parse_key_info_rejects_x509_data_exceeding_entry_budget() {
2869        let subjects = (0..(MAX_X509_DATA_ENTRY_COUNT + 1))
2870            .map(|idx| format!("<X509SubjectName>CN={idx}</X509SubjectName>"))
2871            .collect::<Vec<_>>()
2872            .join("");
2873        let xml = format!(
2874            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{subjects}</X509Data></KeyInfo>"
2875        );
2876        let doc = Document::parse(&xml).unwrap();
2877
2878        let err = parse_key_info(doc.root_element()).unwrap_err();
2879        assert!(matches!(err, ParseError::InvalidStructure(_)));
2880    }
2881
2882    #[test]
2883    fn parse_key_info_rejects_x509_data_exceeding_total_binary_budget() {
2884        let payload = base64::engine::general_purpose::STANDARD.encode(vec![0u8; 190_000]);
2885        let entries = (0..6)
2886            .map(|_| format!("<X509SKI>{payload}</X509SKI>"))
2887            .collect::<Vec<_>>()
2888            .join("");
2889        let xml = format!(
2890            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{entries}</X509Data></KeyInfo>"
2891        );
2892        let doc = Document::parse(&xml).unwrap();
2893
2894        let err = parse_key_info(doc.root_element()).unwrap_err();
2895        assert!(matches!(err, ParseError::InvalidStructure(_)));
2896    }
2897
2898    #[test]
2899    fn parse_key_info_rejects_x509_certificate_with_invalid_der() {
2900        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2901            <X509Data>
2902                <X509Certificate>AQID</X509Certificate>
2903            </X509Data>
2904        </KeyInfo>"#;
2905        let doc = Document::parse(xml).unwrap();
2906
2907        let err = parse_key_info(doc.root_element()).unwrap_err();
2908        assert!(matches!(err, ParseError::InvalidStructure(_)));
2909    }
2910
2911    #[test]
2912    fn parse_key_info_rejects_x509_certificate_with_trailing_der_bytes() {
2913        let mut cert = base64::engine::general_purpose::STANDARD
2914            .decode(fixture_rsa_cert_base64())
2915            .unwrap();
2916        cert.extend_from_slice(&[0x00, 0x01]);
2917        let cert_base64 = base64::engine::general_purpose::STANDARD.encode(cert);
2918        let xml = format!(
2919            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2920                <X509Data>
2921                    <X509Certificate>{cert_base64}</X509Certificate>
2922                </X509Data>
2923            </KeyInfo>"#
2924        );
2925        let doc = Document::parse(&xml).unwrap();
2926
2927        let err = parse_key_info(doc.root_element()).unwrap_err();
2928        assert!(matches!(err, ParseError::InvalidStructure(_)));
2929    }
2930
2931    #[test]
2932    fn parse_key_info_marks_unsupported_spki_algorithm_as_unsupported() {
2933        let xml = include_str!(
2934            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt.xml"
2935        );
2936        let doc = Document::parse(xml).unwrap();
2937        let key_info_node = doc
2938            .descendants()
2939            .find(|node| {
2940                node.is_element()
2941                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
2942                    && node.tag_name().name() == "KeyInfo"
2943            })
2944            .expect("fixture must contain ds:KeyInfo");
2945
2946        let key_info = parse_key_info(key_info_node).expect("KeyInfo parse should succeed");
2947        let x509_info = match &key_info.sources[0] {
2948            KeyInfoSource::X509Data(x509) => x509,
2949            other => panic!("expected X509Data source, got {other:?}"),
2950        };
2951        assert_eq!(x509_info.certificates.len(), 1);
2952        assert_eq!(x509_info.parsed_certificates.len(), 1);
2953        assert_eq!(x509_info.certificate_chain, vec![0]);
2954        let parsed_cert = &x509_info.parsed_certificates[0];
2955        assert!(!parsed_cert.subject_dn.is_empty());
2956        assert!(!parsed_cert.issuer_dn.is_empty());
2957        assert!(parsed_cert.subject_key_identifier.is_some());
2958        assert!(matches!(
2959            parsed_cert.public_key,
2960            X509PublicKeyInfo::Unsupported { .. }
2961        ));
2962    }
2963
2964    #[test]
2965    fn parse_key_info_orders_x509_certificate_chain_from_signing_cert() {
2966        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
2967        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
2968        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
2969        let xml = format!(
2970            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2971                <X509Data>
2972                    <X509Certificate>{root}</X509Certificate>
2973                    <X509Certificate>{intermediate}</X509Certificate>
2974                    <X509Certificate>{leaf}</X509Certificate>
2975                </X509Data>
2976            </KeyInfo>"#
2977        );
2978        let doc = Document::parse(&xml).unwrap();
2979
2980        let key_info = parse_key_info(doc.root_element()).unwrap();
2981        let x509_info = match &key_info.sources[0] {
2982            KeyInfoSource::X509Data(x509) => x509,
2983            other => panic!("expected X509Data source, got {other:?}"),
2984        };
2985
2986        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
2987    }
2988
2989    #[test]
2990    fn chain_builder_matches_x509_equivalent_distinguished_names() {
2991        // RFC 5280 name chaining uses X.501 matching rather than the lexical
2992        // RFC 4514 rendering. Case differences in DirectoryString values must
2993        // not disconnect an otherwise valid configured path.
2994        let certificates = [
2995            fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
2996            fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem"),
2997            fixture_cert_base64("../../tests/fixtures/keys/cacert.pem"),
2998        ]
2999        .map(|encoded| {
3000            base64::engine::general_purpose::STANDARD
3001                .decode(encoded)
3002                .unwrap()
3003        })
3004        .to_vec();
3005        let mut parsed_certificates = certificates
3006            .iter()
3007            .map(|certificate| parse_x509_certificate(certificate).unwrap())
3008            .collect::<Vec<_>>();
3009        parsed_certificates[0].issuer_dn = parsed_certificates[1].subject_dn.to_ascii_lowercase();
3010        parsed_certificates[1].issuer_dn = parsed_certificates[2].subject_dn.to_ascii_lowercase();
3011        let info = X509DataInfo {
3012            certificates,
3013            parsed_certificates,
3014            ..X509DataInfo::default()
3015        };
3016
3017        assert_eq!(
3018            select_x509_signing_certificate(&info, crate::provider::default_provider()).unwrap(),
3019            0
3020        );
3021        assert_eq!(
3022            build_x509_certificate_chain_from(&info, 0, crate::provider::default_provider())
3023                .unwrap(),
3024            vec![0, 1, 2]
3025        );
3026    }
3027
3028    #[test]
3029    fn parse_key_info_uses_issuer_serial_to_select_x509_signing_certificate() {
3030        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
3031        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
3032        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
3033        let xml = format!(
3034            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3035                <X509Data>
3036                    <X509IssuerSerial>
3037                        <X509IssuerName>Email=xmlsec@aleksey.com,CN=Aleksey Sanin,OU=Second level CA,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509IssuerName>
3038                        <X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
3039                    </X509IssuerSerial>
3040                    <X509Certificate>{root}</X509Certificate>
3041                    <X509Certificate>{intermediate}</X509Certificate>
3042                    <X509Certificate>{leaf}</X509Certificate>
3043                </X509Data>
3044            </KeyInfo>"#
3045        );
3046        let doc = Document::parse(&xml).unwrap();
3047
3048        let key_info = parse_key_info(doc.root_element()).unwrap();
3049        let x509_info = match &key_info.sources[0] {
3050            KeyInfoSource::X509Data(x509) => x509,
3051            other => panic!("expected X509Data source, got {other:?}"),
3052        };
3053
3054        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
3055    }
3056
3057    #[test]
3058    fn parse_key_info_allows_selectors_for_multiple_chain_members() {
3059        // X509Data may identify both the signing leaf and another certificate
3060        // in its chain; the unique leaf must remain the signing certificate.
3061        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
3062        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
3063        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
3064        let xml = format!(
3065            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3066                <X509Data>
3067                    <X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
3068                    <X509SKI>0X0XrEVCio75sBcl1TxymJ2IOiU=</X509SKI>
3069                    <X509Certificate>{root}</X509Certificate>
3070                    <X509Certificate>{intermediate}</X509Certificate>
3071                    <X509Certificate>{leaf}</X509Certificate>
3072                </X509Data>
3073            </KeyInfo>"#
3074        );
3075        let doc = Document::parse(&xml).unwrap();
3076
3077        let key_info = parse_key_info(doc.root_element()).unwrap();
3078        let x509_info = match &key_info.sources[0] {
3079            KeyInfoSource::X509Data(x509) => x509,
3080            other => panic!("expected X509Data source, got {other:?}"),
3081        };
3082
3083        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
3084    }
3085
3086    #[test]
3087    fn parse_key_info_uses_decimal_issuer_serial_to_select_x509_signing_certificate() {
3088        let serial = "680572598617295163017172295025714171905498632019";
3089        let padded_serial = format!("{}{}", "0".repeat(64), serial);
3090        assert_eq!(
3091            x509_serial_decimal_to_hex(&padded_serial).as_deref(),
3092            Some("7735EE487F6862DAF1B3956D961CCB0FA6F34F53")
3093        );
3094        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
3095        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
3096        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
3097        let other_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
3098        let xml = format!(
3099            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3100                <X509Data>
3101                    <X509IssuerSerial>
3102                        <X509IssuerName>Email=xmlsec@aleksey.com,CN=Aleksey Sanin,OU=Second level CA,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509IssuerName>
3103                        <X509SerialNumber>{padded_serial}</X509SerialNumber>
3104                    </X509IssuerSerial>
3105                    <X509Certificate>{root}</X509Certificate>
3106                    <X509Certificate>{intermediate}</X509Certificate>
3107                    <X509Certificate>{leaf}</X509Certificate>
3108                    <X509Certificate>{other_leaf}</X509Certificate>
3109                </X509Data>
3110            </KeyInfo>"#
3111        );
3112        let doc = Document::parse(&xml).unwrap();
3113
3114        let key_info = parse_key_info(doc.root_element()).unwrap();
3115        let x509_info = match &key_info.sources[0] {
3116            KeyInfoSource::X509Data(x509) => x509,
3117            other => panic!("expected X509Data source, got {other:?}"),
3118        };
3119
3120        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
3121    }
3122
3123    #[test]
3124    fn parse_key_info_rejects_ambiguous_x509_signing_certificate_candidates() {
3125        let first_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
3126        let second_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
3127        let xml = format!(
3128            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3129                <X509Data>
3130                    <X509Certificate>{first_leaf}</X509Certificate>
3131                    <X509Certificate>{second_leaf}</X509Certificate>
3132                </X509Data>
3133            </KeyInfo>"#
3134        );
3135        let doc = Document::parse(&xml).unwrap();
3136
3137        let err = parse_key_info(doc.root_element()).unwrap_err();
3138        assert!(matches!(err, ParseError::InvalidStructure(_)));
3139    }
3140
3141    #[test]
3142    fn parse_key_info_rejects_unmatched_x509_lookup_identifier() {
3143        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
3144        let xml = format!(
3145            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3146                <X509Data>
3147                    <X509SubjectName>CN=Not The Embedded Certificate</X509SubjectName>
3148                    <X509Certificate>{cert}</X509Certificate>
3149                </X509Data>
3150            </KeyInfo>"#
3151        );
3152        let doc = Document::parse(&xml).unwrap();
3153
3154        let err = parse_key_info(doc.root_element()).unwrap_err();
3155        assert!(
3156            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
3157        );
3158    }
3159
3160    #[test]
3161    fn parse_key_info_rejects_partially_matched_selector_category() {
3162        // Every selector value is an asserted lookup constraint; one matching
3163        // SubjectName must not mask another value absent from the chain.
3164        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
3165        let xml = format!(
3166            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3167                <X509Data>
3168                    <X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
3169                    <X509SubjectName>CN=Not In The Embedded Chain</X509SubjectName>
3170                    <X509Certificate>{cert}</X509Certificate>
3171                </X509Data>
3172            </KeyInfo>"#
3173        );
3174        let doc = Document::parse(&xml).unwrap();
3175
3176        let err = parse_key_info(doc.root_element()).unwrap_err();
3177        assert!(
3178            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
3179        );
3180    }
3181
3182    #[test]
3183    fn parse_key_info_rejects_malformed_issuer_serial_even_with_matching_subject() {
3184        // Lexically invalid serials must fail while parsing X509IssuerSerial,
3185        // before another selector or embedded certificate can mask them.
3186        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
3187        let xml = format!(
3188            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3189                <X509Data>
3190                    <X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
3191                    <X509IssuerSerial>
3192                        <X509IssuerName>Email=xmlsec@aleksey.com,CN=Aleksey Sanin,OU=Second level CA,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509IssuerName>
3193                        <X509SerialNumber>not-a-decimal-serial</X509SerialNumber>
3194                    </X509IssuerSerial>
3195                    <X509Certificate>{cert}</X509Certificate>
3196                </X509Data>
3197            </KeyInfo>"#
3198        );
3199        let doc = Document::parse(&xml).unwrap();
3200
3201        let err = parse_key_info(doc.root_element()).unwrap_err();
3202        assert!(
3203            matches!(err, ParseError::InvalidStructure(message) if message.contains("invalid X509SerialNumber"))
3204        );
3205    }
3206
3207    #[test]
3208    fn parse_key_info_rejects_unmatched_ski_even_with_matching_subject() {
3209        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
3210        let xml = format!(
3211            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3212                <X509Data>
3213                    <X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
3214                    <X509SKI>AQIDBA==</X509SKI>
3215                    <X509Certificate>{cert}</X509Certificate>
3216                </X509Data>
3217            </KeyInfo>"#
3218        );
3219        let doc = Document::parse(&xml).unwrap();
3220
3221        let err = parse_key_info(doc.root_element()).unwrap_err();
3222        assert!(
3223            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
3224        );
3225    }
3226
3227    #[test]
3228    fn parse_key_info_rejects_lookup_hints_for_different_certificates() {
3229        let first_cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
3230        let second_cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
3231        let xml = format!(
3232            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3233                <X509Data>
3234                    <X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
3235                    <X509SKI>60zMLKCfzQ3qnXAzABzRNpdgQ8Q=</X509SKI>
3236                    <X509Certificate>{first_cert}</X509Certificate>
3237                    <X509Certificate>{second_cert}</X509Certificate>
3238                </X509Data>
3239            </KeyInfo>"#
3240        );
3241        let doc = Document::parse(&xml).unwrap();
3242
3243        let err = parse_key_info(doc.root_element()).unwrap_err();
3244        assert!(
3245            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers match multiple certificates"))
3246        );
3247    }
3248
3249    #[test]
3250    fn build_x509_certificate_chain_rejects_chain_exceeding_max_depth() {
3251        let parsed_certificates: Vec<ParsedX509Certificate> = (0..=MAX_X509_CHAIN_DEPTH)
3252            .map(|idx| ParsedX509Certificate {
3253                subject_dn: format!("CN=cert-{idx}"),
3254                issuer_dn: if idx == MAX_X509_CHAIN_DEPTH {
3255                    format!("CN=cert-{idx}")
3256                } else {
3257                    format!("CN=cert-{}", idx + 1)
3258                },
3259                serial_number: vec![u8::try_from(idx).unwrap()],
3260                serial_number_hex: format!("{idx:02X}"),
3261                subject_key_identifier: None,
3262                public_key: X509PublicKeyInfo::Unsupported {
3263                    algorithm_oid: "1.2.3.4".into(),
3264                },
3265            })
3266            .collect();
3267        let certificates = vec![Vec::new(); parsed_certificates.len()];
3268        let info = X509DataInfo {
3269            certificates,
3270            parsed_certificates,
3271            ..X509DataInfo::default()
3272        };
3273
3274        let err =
3275            build_x509_certificate_chain(&info, crate::provider::default_provider()).unwrap_err();
3276        assert!(
3277            matches!(err, ParseError::InvalidStructure(message) if message.contains("maximum depth"))
3278        );
3279    }
3280
3281    #[test]
3282    fn x509_serial_hex_strips_der_sign_extension_zeroes() {
3283        assert_eq!(format_x509_serial_value_hex(&[0x00, 0xFF]), "FF");
3284        assert_eq!(format_x509_serial_value_hex(&[0x00, 0x7F]), "7F");
3285        assert_eq!(format_x509_serial_value_hex(&[0x00, 0x00]), "00");
3286    }
3287
3288    #[test]
3289    fn x509_serial_decimal_parser_enforces_rfc5280_positive_range() {
3290        // A 20-octet unsigned magnitude may need a 21st DER sign-padding
3291        // octet. The decimal selector denotes the value, not its DER encoding.
3292        let max_serial = "1461501637330902918203684832716283019655932542975";
3293        assert_eq!(
3294            x509_serial_decimal_to_hex(max_serial),
3295            Some("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF".into())
3296        );
3297        assert_eq!(
3298            x509_serial_decimal_to_hex("730750818665451459101842416358141509827966271488"),
3299            Some("8000000000000000000000000000000000000000".into())
3300        );
3301        assert_eq!(
3302            x509_serial_decimal_to_hex("0000000000000000000000000000000000000000000000001"),
3303            Some("01".into())
3304        );
3305        assert_eq!(
3306            x509_serial_decimal_to_hex("00000000000000000000000000000000000000000000000001"),
3307            Some("01".into())
3308        );
3309        assert_eq!(x509_serial_decimal_to_hex("+1"), Some("01".into()));
3310
3311        for invalid in [
3312            "",
3313            "0",
3314            "000",
3315            "+0",
3316            "++1",
3317            "-1",
3318            "1a",
3319            "1461501637330902918203684832716283019655932542976",
3320        ] {
3321            assert_eq!(
3322                x509_serial_decimal_to_hex(invalid),
3323                None,
3324                "invalid serial {invalid:?} must be rejected"
3325            );
3326        }
3327    }
3328
3329    #[test]
3330    fn parse_x509_serial_normalizes_boundary_whitespace_and_rejects_overflow() {
3331        // XML Schema collapses integer whitespace before validation; the
3332        // normalized value must still obey the RFC 5280 positive range.
3333        let max_serial = "1461501637330902918203684832716283019655932542975";
3334        let valid = format!(
3335            "<KeyInfo xmlns=\"{XMLDSIG_NS}\"><X509Data><X509IssuerSerial><X509IssuerName>CN=issuer</X509IssuerName><X509SerialNumber>\n {max_serial}\t</X509SerialNumber></X509IssuerSerial></X509Data></KeyInfo>"
3336        );
3337        let doc = Document::parse(&valid).unwrap();
3338        let parsed = parse_key_info(doc.root_element()).unwrap();
3339        let KeyInfoSource::X509Data(x509) = &parsed.sources[0] else {
3340            panic!("expected X509Data source");
3341        };
3342        assert_eq!(x509.issuer_serials[0].1, max_serial);
3343
3344        let explicit_positive = valid.replace(max_serial, "+42");
3345        let doc = Document::parse(&explicit_positive).unwrap();
3346        let parsed = parse_key_info(doc.root_element()).unwrap();
3347        let KeyInfoSource::X509Data(x509) = &parsed.sources[0] else {
3348            panic!("expected X509Data source");
3349        };
3350        assert_eq!(x509.issuer_serials[0].1, "42");
3351
3352        let overflow = valid.replace(
3353            max_serial,
3354            "1461501637330902918203684832716283019655932542976",
3355        );
3356        let doc = Document::parse(&overflow).unwrap();
3357        assert!(matches!(
3358            parse_key_info(doc.root_element()),
3359            Err(ParseError::InvalidStructure(message))
3360                if message.contains("invalid X509SerialNumber")
3361        ));
3362    }
3363
3364    #[test]
3365    fn issuer_selector_matches_a_sign_padded_twenty_octet_serial() {
3366        // Selector decimal text represents the unsigned magnitude; the DER
3367        // sign-padding octet must not make the same certificate unmatchable.
3368        let serial_hex = "8000000000000000000000000000000000000000";
3369        let info = X509DataInfo {
3370            issuer_serials: vec![(
3371                "CN=issuer".into(),
3372                "730750818665451459101842416358141509827966271488".into(),
3373            )],
3374            parsed_certificates: vec![ParsedX509Certificate {
3375                subject_dn: "CN=leaf".into(),
3376                issuer_dn: "CN=issuer".into(),
3377                serial_number: [vec![0, 0x80], vec![0; 19]].concat(),
3378                serial_number_hex: serial_hex.into(),
3379                subject_key_identifier: None,
3380                public_key: X509PublicKeyInfo::Unsupported {
3381                    algorithm_oid: "1.2.3.4".into(),
3382                },
3383            }],
3384            ..X509DataInfo::default()
3385        };
3386
3387        assert!(
3388            x509_selector_categories_match_chain(&info, crate::provider::default_provider())
3389                .unwrap()
3390        );
3391    }
3392
3393    #[test]
3394    fn distinguished_name_matching_preserves_rdn_order() {
3395        // RFC 4514 permits alternate encodings within an RDN, but reversing
3396        // the RDN sequence identifies a different hierarchical name.
3397        assert!(distinguished_names_equal(
3398            "CN=leaf, O=example",
3399            "CN=leaf,O=example"
3400        ));
3401        assert!(!distinguished_names_equal(
3402            "CN=leaf,O=example",
3403            "O=example,CN=leaf"
3404        ));
3405    }
3406
3407    #[test]
3408    fn distinguished_name_matching_applies_x520_string_preparation() {
3409        // RFC 5280 requires caseIgnoreMatch with insignificant-space handling
3410        // for DirectoryString values rather than exact ASN.1 value equality.
3411        assert!(distinguished_names_equal(
3412            "CN=  TEST   key  ,O=Example",
3413            "CN=test key,O=example"
3414        ));
3415        assert!(distinguished_names_equal(
3416            "CN=Straße,O=Example",
3417            "CN=STRASSE,O=EXAMPLE"
3418        ));
3419        assert!(distinguished_names_equal(
3420            "CN=test+OU=security,O=example",
3421            "OU=SECURITY+CN=TEST,O=EXAMPLE"
3422        ));
3423        assert!(!distinguished_names_equal(
3424            "1.2.3.4=#040141,O=example",
3425            "1.2.3.4=#040142,O=example"
3426        ));
3427    }
3428
3429    #[test]
3430    fn distinguished_name_matching_applies_ia5_matching_rules() {
3431        // RFC 5280 emailAddress matching preserves the local part while the
3432        // domain is case-insensitive; domainComponent is case-insensitive too.
3433        assert!(distinguished_names_equal(
3434            "EMAIL=ops@EXAMPLE.COM,DC=EXAMPLE,DC=COM",
3435            "EMAIL=ops@example.com,DC=example,DC=com"
3436        ));
3437        assert!(!distinguished_names_equal(
3438            "EMAIL=OPS@example.com,DC=example,DC=com",
3439            "EMAIL=ops@example.com,DC=example,DC=com"
3440        ));
3441    }
3442
3443    #[test]
3444    fn distinguished_name_matching_handles_rfc4514_escaped_values() {
3445        // Certificate values containing RFC 4514 separators and boundary spaces
3446        // must remain one attribute when matched against an XMLDSig selector.
3447        let value = " leading,plus+equals=slash\\trailing ";
3448        let mut params = rcgen::CertificateParams::new(Vec::new()).unwrap();
3449        params
3450            .distinguished_name
3451            .push(rcgen::DnType::CommonName, value);
3452        let key = rcgen::KeyPair::generate().unwrap();
3453        let certificate = params.self_signed(&key).unwrap();
3454        let parsed = parse_x509_certificate(certificate.der()).unwrap();
3455
3456        assert_eq!(
3457            parsed.subject_dn,
3458            r"CN=\ leading\,plus\+equals=slash\\trailing\ "
3459        );
3460        assert!(distinguished_names_equal(
3461            r"CN=\ leading\,plus\+equals=slash\\trailing\ ",
3462            &parsed.subject_dn
3463        ));
3464        assert!(distinguished_names_equal(
3465            "\n  CN=\\ leading\\,plus\\+equals=slash\\\\trailing\\ \n",
3466            &parsed.subject_dn
3467        ));
3468    }
3469
3470    #[test]
3471    fn distinguished_name_trailing_escape_covers_all_xml_whitespace() {
3472        // The normalizer strips all four XML whitespace characters, so escape
3473        // detection must preserve each one consistently at RDN boundaries.
3474        for whitespace in [' ', '\t', '\r', '\n'] {
3475            assert!(trailing_whitespace_is_escaped(&format!(
3476                "CN=value\\{whitespace}"
3477            )));
3478            assert!(!trailing_whitespace_is_escaped(&format!(
3479                "CN=value{whitespace}"
3480            )));
3481        }
3482    }
3483
3484    #[test]
3485    fn parse_key_info_accepts_large_textual_x509_entries_within_entry_budget() {
3486        let issuer_name = "C".repeat(MAX_X509_ISSUER_NAME_TEXT_LEN);
3487        let serial_number = "0".repeat(MAX_X509_SERIAL_NUMBER_VALUE_DIGITS - 1) + "1";
3488        let issuer_serials = (0..52)
3489            .map(|_| {
3490                format!(
3491                    "<X509IssuerSerial><X509IssuerName>{issuer_name}</X509IssuerName><X509SerialNumber>{serial_number}</X509SerialNumber></X509IssuerSerial>"
3492                )
3493            })
3494            .collect::<Vec<_>>()
3495            .join("");
3496        let xml = format!(
3497            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{issuer_serials}</X509Data></KeyInfo>"
3498        );
3499        let doc = Document::parse(&xml).unwrap();
3500
3501        let key_info = parse_key_info(doc.root_element()).unwrap();
3502        let parsed = match &key_info.sources[0] {
3503            KeyInfoSource::X509Data(x509) => x509,
3504            _ => panic!("expected X509Data source"),
3505        };
3506        assert_eq!(parsed.issuer_serials.len(), 52);
3507    }
3508
3509    #[test]
3510    fn parse_key_info_bounds_raw_x509_serial_text() {
3511        // Leading zeroes are lexically valid, but their raw XML representation
3512        // remains bounded independently from the canonical certificate value.
3513        let serial = "0".repeat(MAX_X509_SERIAL_NUMBER_RAW_TEXT_LEN + 1);
3514        let xml = format!(
3515            "<KeyInfo xmlns=\"{XMLDSIG_NS}\"><X509Data><X509IssuerSerial><X509IssuerName>CN=issuer</X509IssuerName><X509SerialNumber>{serial}</X509SerialNumber></X509IssuerSerial></X509Data></KeyInfo>"
3516        );
3517        let doc = Document::parse(&xml).unwrap();
3518
3519        let error = parse_key_info(doc.root_element()).unwrap_err();
3520
3521        assert!(matches!(
3522            error,
3523            ParseError::InvalidStructure(reason)
3524                if reason == "X509SerialNumber exceeds maximum allowed text length"
3525        ));
3526    }
3527
3528    #[test]
3529    fn parse_key_info_accepts_x509data_with_only_foreign_namespace_children() {
3530        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3531                              xmlns:foo="urn:example:foo">
3532            <X509Data>
3533                <foo:Bar/>
3534            </X509Data>
3535        </KeyInfo>"#;
3536        let doc = Document::parse(xml).unwrap();
3537
3538        let key_info = parse_key_info(doc.root_element()).unwrap();
3539        assert_eq!(
3540            key_info.sources,
3541            vec![KeyInfoSource::X509Data(X509DataInfo::default())]
3542        );
3543    }
3544
3545    #[test]
3546    fn parse_key_info_der_encoded_key_value_rejects_invalid_base64() {
3547        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3548                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3549            <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
3550        </KeyInfo>"#;
3551        let doc = Document::parse(xml).unwrap();
3552
3553        let err = parse_key_info(doc.root_element()).unwrap_err();
3554        assert!(matches!(err, ParseError::Base64(_)));
3555    }
3556
3557    #[test]
3558    fn parse_key_info_der_encoded_key_value_accepts_xml_whitespace() {
3559        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3560                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3561            <dsig11:DEREncodedKeyValue>
3562                AQID
3563                BA==
3564            </dsig11:DEREncodedKeyValue>
3565        </KeyInfo>"#;
3566        let doc = Document::parse(xml).unwrap();
3567
3568        let key_info = parse_key_info(doc.root_element()).unwrap();
3569        assert_eq!(
3570            key_info.sources,
3571            vec![KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])]
3572        );
3573    }
3574
3575    #[test]
3576    fn parse_key_info_dispatches_dsig11_ec_keyvalue() {
3577        let public_key = "BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=";
3578        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3579                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3580            <KeyValue>
3581                <dsig11:ECKeyValue>
3582                    <dsig11:NamedCurve URI="urn:oid:1.2.840.10045.3.1.7"/>
3583                    <dsig11:PublicKey>BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=</dsig11:PublicKey>
3584                </dsig11:ECKeyValue>
3585            </KeyValue>
3586        </KeyInfo>"#;
3587        let doc = Document::parse(xml).unwrap();
3588        let expected_public_key = base64::engine::general_purpose::STANDARD
3589            .decode(public_key)
3590            .expect("fixture EC point must be valid base64");
3591
3592        let key_info = parse_key_info(doc.root_element()).unwrap();
3593        assert_eq!(
3594            key_info.sources,
3595            vec![KeyInfoSource::KeyValue(KeyValueInfo::Ec {
3596                curve_oid: "1.2.840.10045.3.1.7".into(),
3597                public_key: expected_public_key,
3598            })]
3599        );
3600    }
3601
3602    #[test]
3603    fn parse_ec_key_value_accepts_bare_curve_oid() {
3604        use base64::Engine;
3605
3606        let encoded_public_key = "BO/yd/OZzDfjX4qivDY/vsUIuh6KWAxoxW5P4ukvwd+T6pVljWsX2UBJNNy5MdhTwB8e2YwB8kUbJwdsAS/XGi/fz8unFrs+lVlAgIs6s/xBYFbfUoRiAacD2SpVDe6XBA==";
3607        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3608                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3609            <KeyValue>
3610                <dsig11:ECKeyValue>
3611                    <dsig11:NamedCurve URI="1.3.132.0.34"/>
3612                    <dsig11:PublicKey>BO/yd/OZzDfjX4qivDY/vsUIuh6KWAxoxW5P4ukvwd+T6pVljWsX2UBJNNy5MdhTwB8e2YwB8kUbJwdsAS/XGi/fz8unFrs+lVlAgIs6s/xBYFbfUoRiAacD2SpVDe6XBA==</dsig11:PublicKey>
3613                </dsig11:ECKeyValue>
3614            </KeyValue>
3615        </KeyInfo>"#;
3616        let doc = Document::parse(xml).unwrap();
3617        let expected_public_key = base64::engine::general_purpose::STANDARD
3618            .decode(encoded_public_key)
3619            .unwrap();
3620
3621        let sources = parse_key_info(doc.root_element()).unwrap().sources;
3622
3623        assert!(matches!(
3624            &sources[0],
3625            KeyInfoSource::KeyValue(KeyValueInfo::Ec { curve_oid, public_key })
3626                if curve_oid == EC_P384_OID && public_key == &expected_public_key
3627        ));
3628    }
3629
3630    #[test]
3631    fn parse_ec_key_value_marks_ec_parameters_as_unsupported() {
3632        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3633                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3634            <KeyValue>
3635                <dsig11:ECKeyValue>
3636                    <dsig11:ECParameters/>
3637                    <dsig11:PublicKey>BA==</dsig11:PublicKey>
3638                </dsig11:ECKeyValue>
3639            </KeyValue>
3640        </KeyInfo>"#;
3641        let doc = Document::parse(xml).unwrap();
3642
3643        let key_info = parse_key_info(doc.root_element()).unwrap();
3644        assert_eq!(
3645            key_info.sources,
3646            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
3647                namespace: Some(XMLDSIG11_NS.to_string()),
3648                local_name: "ECKeyValue".into(),
3649            })]
3650        );
3651    }
3652
3653    #[test]
3654    fn parse_ec_key_value_marks_unsupported_curve_as_unsupported() {
3655        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3656                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3657            <KeyValue>
3658                <dsig11:ECKeyValue>
3659                    <dsig11:NamedCurve URI="urn:oid:1.3.132.0.35"/>
3660                    <dsig11:PublicKey>BA==</dsig11:PublicKey>
3661                </dsig11:ECKeyValue>
3662            </KeyValue>
3663        </KeyInfo>"#;
3664        let doc = Document::parse(xml).unwrap();
3665
3666        let key_info = parse_key_info(doc.root_element()).unwrap();
3667        assert_eq!(
3668            key_info.sources,
3669            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
3670                namespace: Some(XMLDSIG11_NS.to_string()),
3671                local_name: "ECKeyValue".into(),
3672            })]
3673        );
3674    }
3675
3676    #[test]
3677    fn parse_ec_key_value_marks_missing_named_curve_uri_invalid() {
3678        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3679                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3680            <KeyValue>
3681                <dsig11:ECKeyValue>
3682                    <dsig11:NamedCurve/>
3683                    <dsig11:PublicKey>BA==</dsig11:PublicKey>
3684                </dsig11:ECKeyValue>
3685            </KeyValue>
3686        </KeyInfo>"#;
3687        let doc = Document::parse(xml).unwrap();
3688
3689        let key_info = parse_key_info(doc.root_element()).unwrap();
3690        assert_eq!(
3691            key_info.sources,
3692            vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)]
3693        );
3694    }
3695
3696    #[test]
3697    fn parse_ec_key_value_marks_reordered_children_invalid() {
3698        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3699                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3700            <KeyValue>
3701                <dsig11:ECKeyValue>
3702                    <dsig11:PublicKey>BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=</dsig11:PublicKey>
3703                    <dsig11:NamedCurve URI="urn:oid:1.2.840.10045.3.1.7"/>
3704                </dsig11:ECKeyValue>
3705            </KeyValue>
3706        </KeyInfo>"#;
3707        let doc = Document::parse(xml).unwrap();
3708
3709        let key_info = parse_key_info(doc.root_element()).unwrap();
3710        assert_eq!(
3711            key_info.sources,
3712            vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)]
3713        );
3714    }
3715
3716    #[test]
3717    fn parse_ec_key_value_marks_non_uncompressed_point_invalid() {
3718        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3719                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3720            <KeyValue>
3721                <dsig11:ECKeyValue>
3722                    <dsig11:NamedCurve URI="urn:oid:1.2.840.10045.3.1.7"/>
3723                    <dsig11:PublicKey>Ap/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=</dsig11:PublicKey>
3724                </dsig11:ECKeyValue>
3725            </KeyValue>
3726        </KeyInfo>"#;
3727        let doc = Document::parse(xml).unwrap();
3728
3729        let key_info = parse_key_info(doc.root_element()).unwrap();
3730        assert_eq!(
3731            key_info.sources,
3732            vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)]
3733        );
3734    }
3735
3736    #[test]
3737    fn parse_key_info_marks_ds_namespace_ec_keyvalue_as_unsupported() {
3738        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3739            <KeyValue>
3740                <ECKeyValue/>
3741            </KeyValue>
3742        </KeyInfo>"#;
3743        let doc = Document::parse(xml).unwrap();
3744
3745        let key_info = parse_key_info(doc.root_element()).unwrap();
3746        assert_eq!(
3747            key_info.sources,
3748            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
3749                namespace: Some(XMLDSIG_NS.to_string()),
3750                local_name: "ECKeyValue".into(),
3751            })]
3752        );
3753    }
3754
3755    #[test]
3756    fn parse_key_info_keeps_unsupported_keyvalue_child_as_marker() {
3757        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3758            <KeyValue>
3759                <FutureKeyValue/>
3760            </KeyValue>
3761        </KeyInfo>"#;
3762        let doc = Document::parse(xml).unwrap();
3763
3764        let key_info = parse_key_info(doc.root_element()).unwrap();
3765        assert_eq!(
3766            key_info.sources,
3767            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
3768                namespace: Some(XMLDSIG_NS.to_string()),
3769                local_name: "FutureKeyValue".into(),
3770            })]
3771        );
3772    }
3773
3774    #[test]
3775    fn parse_key_info_accepts_supported_x509_retrieval_xpath() {
3776        // Merlin's same-document RetrievalMethod selects only X509Data nodes.
3777        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3778          <RetrievalMethod Type="http://www.w3.org/2000/09/xmldsig#X509Data" URI="#keys">
3779            <Transforms><Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
3780              <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">ancestor-or-self::dsig:X509Data</XPath>
3781            </Transform></Transforms>
3782          </RetrievalMethod>
3783        </KeyInfo>"##;
3784        let doc = Document::parse(xml).unwrap();
3785
3786        let key_info = parse_key_info(doc.root_element()).unwrap();
3787        assert!(matches!(
3788            key_info.sources.as_slice(),
3789            [KeyInfoSource::RetrievalMethod {
3790                uri,
3791                resource_type: Some(resource_type),
3792                transforms: RetrievalMethodTransforms::X509DataNodeSetFilter,
3793            }] if uri == "#keys"
3794                && resource_type == "http://www.w3.org/2000/09/xmldsig#X509Data"
3795        ));
3796    }
3797
3798    #[test]
3799    fn parse_key_info_rejects_oversized_retrieval_method_type() {
3800        // Type is advisory, but retaining it must not allocate unbounded
3801        // attacker-controlled KeyInfo metadata before resolution.
3802        let oversized_type = "x".repeat(MAX_KEY_NAME_TEXT_LEN + 1);
3803        let xml = format!(
3804            r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><RetrievalMethod URI="#key" Type="{oversized_type}"/></KeyInfo>"##
3805        );
3806        let document = Document::parse(&xml).unwrap();
3807
3808        assert!(matches!(
3809            parse_key_info(document.root_element()),
3810            Err(ParseError::InvalidStructure(reason))
3811                if reason == "RetrievalMethod Type exceeds maximum length"
3812        ));
3813    }
3814
3815    #[test]
3816    fn parse_key_info_bounds_retrieval_method_xml_base_chain() {
3817        // RetrievalMethod resolves its resource identity during parsing, so it
3818        // must use the same bounded XML Base algorithm as Reference lookup.
3819        let mut xml =
3820            format!(r#"<KeyInfo xmlns="{XMLDSIG_NS}"><RetrievalMethod URI="key.der"/></KeyInfo>"#);
3821        for _ in 0..65 {
3822            xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
3823        }
3824        let document = Document::parse(&xml).unwrap();
3825        let key_info = document
3826            .descendants()
3827            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
3828            .unwrap();
3829
3830        assert!(matches!(
3831            parse_key_info(key_info),
3832            Err(ParseError::InvalidStructure(reason))
3833                if reason.contains("XML Base resolution")
3834        ));
3835    }
3836
3837    #[test]
3838    fn parse_key_info_normalizes_external_retrieval_without_xml_base() {
3839        // RetrievalMethod stores the resolved identity used for caller-map
3840        // lookup, including RFC 3986 normalization when no base is declared.
3841        let xml = format!(
3842            r#"<KeyInfo xmlns="{XMLDSIG_NS}"><RetrievalMethod URI="https://example.test/a/../key.der"/></KeyInfo>"#
3843        );
3844        let document = Document::parse(&xml).unwrap();
3845        let key_info = parse_key_info(document.root_element()).unwrap();
3846
3847        assert!(matches!(
3848            key_info.sources.as_slice(),
3849            [KeyInfoSource::RetrievalMethod { uri, .. }]
3850                if uri == "https://example.test/key.der"
3851        ));
3852    }
3853
3854    #[test]
3855    fn parse_key_info_absolute_retrieval_bypasses_xml_base_chain() {
3856        // Absolute RetrievalMethod identities do not inherit xml:base, so an
3857        // otherwise excessive ancestor chain must not reject them.
3858        let mut xml = format!(
3859            r#"<KeyInfo xmlns="{XMLDSIG_NS}"><RetrievalMethod URI="https://example.test/a/../key.der"/></KeyInfo>"#
3860        );
3861        for _ in 0..65 {
3862            xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
3863        }
3864        let document = Document::parse(&xml).unwrap();
3865        let key_info_node = document
3866            .descendants()
3867            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
3868            .unwrap();
3869        let key_info = parse_key_info(key_info_node)
3870            .expect("absolute RetrievalMethod must not consume ancestor-base budget");
3871
3872        assert!(matches!(
3873            key_info.sources.as_slice(),
3874            [KeyInfoSource::RetrievalMethod { uri, .. }]
3875                if uri == "https://example.test/key.der"
3876        ));
3877    }
3878
3879    #[test]
3880    fn parse_key_info_accepts_namespace_equivalent_retrieval_xpath_prefix() {
3881        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3882          <RetrievalMethod Type="http://www.w3.org/2000/09/xmldsig#X509Data" URI="#keys">
3883            <Transforms><Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
3884              <XPath xmlns:ds="http://www.w3.org/2000/09/xmldsig#">ancestor-or-self::ds:X509Data</XPath>
3885            </Transform></Transforms>
3886          </RetrievalMethod>
3887        </KeyInfo>"##;
3888        let doc = Document::parse(xml).unwrap();
3889
3890        assert!(matches!(
3891            parse_key_info(doc.root_element())
3892                .unwrap()
3893                .sources
3894                .as_slice(),
3895            [KeyInfoSource::RetrievalMethod {
3896                transforms: RetrievalMethodTransforms::X509DataNodeSetFilter,
3897                ..
3898            }]
3899        ));
3900    }
3901
3902    #[test]
3903    fn parse_key_info_reads_complete_retrieval_xpath_text() {
3904        // XML comments split character data into multiple text nodes; all chunks
3905        // still belong to the XPath parameter's string-value.
3906        let valid = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3907          <RetrievalMethod Type="http://www.w3.org/2000/09/xmldsig#X509Data" URI="#keys">
3908            <Transforms><Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
3909              <XPath xmlns:ds="http://www.w3.org/2000/09/xmldsig#">ancestor-or-self::ds:X509<!-- split -->Data</XPath>
3910            </Transform></Transforms>
3911          </RetrievalMethod>
3912        </KeyInfo>"##;
3913        let document = Document::parse(valid).unwrap();
3914        assert!(parse_key_info(document.root_element()).is_ok());
3915
3916        let unsupported =
3917            valid.replace("X509<!-- split -->Data", "X509Data<!-- split -->[false()]");
3918        let document = Document::parse(&unsupported).unwrap();
3919        assert!(matches!(
3920            parse_key_info(document.root_element()),
3921            Err(ParseError::InvalidStructure(reason))
3922                if reason == "unsupported RetrievalMethod XPath selection"
3923        ));
3924    }
3925
3926    #[test]
3927    fn parse_dsa_key_value_accepts_schema_optional_parameters_and_rejects_half_pair() {
3928        let key_info = |parameters: &str| {
3929            format!(
3930                r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><KeyValue><DSAKeyValue>
3931                {parameters}
3932                </DSAKeyValue></KeyValue></KeyInfo>"#
3933            )
3934        };
3935        for parameters in [
3936            "<Y>AQ==</Y>",
3937            "<G>AQ==</G><Y>AQ==</Y>",
3938            "<P>AQ==</P><Q>AQ==</Q><Y>AQ==</Y>",
3939            "<P>AQ==</P><Q>AQ==</Q><G>AQ==</G><Y>AQ==</Y><J>AQ==</J>",
3940            "<Y>AQ==</Y><Seed>AQ==</Seed><PgenCounter>AQ==</PgenCounter>",
3941            "<Y>AQ==</Y><J>AQ==</J><Seed>AQ==</Seed><PgenCounter>AQ==</PgenCounter>",
3942        ] {
3943            let xml = key_info(parameters);
3944            let doc = Document::parse(&xml).unwrap();
3945            assert!(matches!(
3946                parse_key_info(doc.root_element())
3947                    .unwrap()
3948                    .sources
3949                    .as_slice(),
3950                [KeyInfoSource::KeyValue(KeyValueInfo::Dsa { .. })]
3951            ));
3952        }
3953
3954        for invalid_parameters in [
3955            "<P>AQ==</P><Y>AQ==</Y>",
3956            "<Q>AQ==</Q><Y>AQ==</Y>",
3957            "<Y>AQ==</Y><Seed>AQ==</Seed>",
3958            "<Y>AQ==</Y><PgenCounter>AQ==</PgenCounter>",
3959        ] {
3960            let xml = key_info(invalid_parameters);
3961            let doc = Document::parse(&xml).unwrap();
3962            assert!(matches!(
3963                parse_key_info(doc.root_element()),
3964                Err(ParseError::InvalidStructure(_))
3965            ));
3966        }
3967    }
3968
3969    #[test]
3970    fn parse_dsa_crypto_binary_ignores_comment_nodes() {
3971        // XML comments split simple content without contributing to its string value.
3972        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3973            <KeyValue><DSAKeyValue><Y>AQ<!-- split -->ID</Y></DSAKeyValue></KeyValue>
3974        </KeyInfo>"#;
3975        let doc = Document::parse(xml).unwrap();
3976
3977        assert!(matches!(
3978            parse_key_info(doc.root_element())
3979                .unwrap()
3980                .sources
3981                .as_slice(),
3982            [KeyInfoSource::KeyValue(KeyValueInfo::Dsa { y, .. })] if y == &[1, 2, 3]
3983        ));
3984    }
3985
3986    #[test]
3987    fn parse_rsa_crypto_binary_ignores_comment_nodes() {
3988        // The shared CryptoBinary decoder must apply XML simple-content semantics to every key type.
3989        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3990            <KeyValue><RSAKeyValue>
3991                <Modulus>AQ<!-- split -->ID</Modulus><Exponent>Aw==</Exponent>
3992            </RSAKeyValue></KeyValue>
3993        </KeyInfo>"#;
3994        let doc = Document::parse(xml).unwrap();
3995
3996        assert!(matches!(
3997            parse_key_info(doc.root_element())
3998                .unwrap()
3999                .sources
4000                .as_slice(),
4001            [KeyInfoSource::KeyValue(KeyValueInfo::Rsa { modulus, exponent })]
4002                if modulus == &[1, 2, 3] && exponent == &[3]
4003        ));
4004    }
4005
4006    #[test]
4007    fn parse_key_info_preserves_advisory_unsupported_retrieval_transform() {
4008        // Unsupported RetrievalMethod types are advisory key sources. Their
4009        // transform syntax must not hide a later source the resolver can use.
4010        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4011          <RetrievalMethod URI="#keys" Type="urn:vendor:key"><Transforms>
4012            <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#base64"/>
4013          </Transforms></RetrievalMethod>
4014          <KeyName>fallback</KeyName>
4015        </KeyInfo>"##;
4016        let doc = Document::parse(xml).unwrap();
4017
4018        let key_info = parse_key_info(doc.root_element())
4019            .expect("unsupported advisory retrieval must not reject all KeyInfo sources");
4020        assert!(matches!(
4021            key_info.sources.as_slice(),
4022            [
4023                KeyInfoSource::RetrievalMethod { resource_type: Some(resource_type), .. },
4024                KeyInfoSource::KeyName(name),
4025            ] if resource_type == "urn:vendor:key" && name == "fallback"
4026        ));
4027    }
4028
4029    #[test]
4030    fn parse_key_info_rejects_excessive_child_sources() {
4031        // KeyInfo extensions are lax, but their parse work remains bounded.
4032        let children = (0..=64)
4033            .map(|index| format!(r#"<extension xmlns="urn:test" index="{index}"/>"#))
4034            .collect::<String>();
4035        let xml =
4036            format!(r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">{children}</KeyInfo>"#);
4037        let document = Document::parse(&xml).unwrap();
4038
4039        assert!(matches!(
4040            parse_key_info(document.root_element()),
4041            Err(ParseError::InvalidStructure(reason))
4042                if reason == "KeyInfo contains too many child elements"
4043        ));
4044    }
4045
4046    #[test]
4047    fn parse_key_info_rejects_keyname_with_child_elements() {
4048        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4049            <KeyName>ok<foo/></KeyName>
4050        </KeyInfo>"#;
4051        let doc = Document::parse(xml).unwrap();
4052
4053        let err = parse_key_info(doc.root_element()).unwrap_err();
4054        assert!(matches!(err, ParseError::InvalidStructure(_)));
4055    }
4056
4057    #[test]
4058    fn parse_key_info_preserves_keyname_text_without_trimming() {
4059        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4060            <KeyName>  signing key  </KeyName>
4061        </KeyInfo>"#;
4062        let doc = Document::parse(xml).unwrap();
4063
4064        let key_info = parse_key_info(doc.root_element()).unwrap();
4065        assert_eq!(
4066            key_info.sources,
4067            vec![KeyInfoSource::KeyName("  signing key  ".into())]
4068        );
4069    }
4070
4071    #[test]
4072    fn parse_key_info_rejects_oversized_keyname_text() {
4073        let oversized = "A".repeat(4097);
4074        let xml = format!(
4075            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><KeyName>{oversized}</KeyName></KeyInfo>"
4076        );
4077        let doc = Document::parse(&xml).unwrap();
4078
4079        let err = parse_key_info(doc.root_element()).unwrap_err();
4080        assert!(matches!(err, ParseError::InvalidStructure(_)));
4081    }
4082
4083    #[test]
4084    fn parse_key_info_rejects_non_whitespace_mixed_content() {
4085        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">oops<KeyName>k</KeyName></KeyInfo>"#;
4086        let doc = Document::parse(xml).unwrap();
4087
4088        let err = parse_key_info(doc.root_element()).unwrap_err();
4089        assert!(matches!(err, ParseError::InvalidStructure(_)));
4090    }
4091
4092    #[test]
4093    fn parse_key_info_rejects_nbsp_as_non_xml_whitespace_mixed_content() {
4094        let xml = "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\u{00A0}<KeyName>k</KeyName></KeyInfo>";
4095        let doc = Document::parse(xml).unwrap();
4096
4097        let err = parse_key_info(doc.root_element()).unwrap_err();
4098        assert!(matches!(err, ParseError::InvalidStructure(_)));
4099    }
4100
4101    #[test]
4102    fn parse_key_info_der_encoded_key_value_rejects_oversized_payload() {
4103        let oversized =
4104            base64::engine::general_purpose::STANDARD
4105                .encode(vec![0u8; MAX_DER_ENCODED_KEY_VALUE_LEN + 1]);
4106        let xml = format!(
4107            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:dsig11=\"http://www.w3.org/2009/xmldsig11#\"><dsig11:DEREncodedKeyValue>{oversized}</dsig11:DEREncodedKeyValue></KeyInfo>"
4108        );
4109        let doc = Document::parse(&xml).unwrap();
4110
4111        let err = parse_key_info(doc.root_element()).unwrap_err();
4112        assert!(matches!(err, ParseError::InvalidStructure(_)));
4113    }
4114
4115    #[test]
4116    fn parse_key_info_der_encoded_key_value_rejects_empty_payload() {
4117        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
4118                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
4119            <dsig11:DEREncodedKeyValue>
4120                
4121            </dsig11:DEREncodedKeyValue>
4122        </KeyInfo>"#;
4123        let doc = Document::parse(xml).unwrap();
4124
4125        let err = parse_key_info(doc.root_element()).unwrap_err();
4126        assert!(matches!(err, ParseError::InvalidStructure(_)));
4127    }
4128
4129    #[test]
4130    fn parse_key_info_der_encoded_key_value_non_xml_ascii_whitespace_is_not_parseable_xml() {
4131        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>";
4132        assert!(Document::parse(xml).is_err());
4133    }
4134
4135    // ── parse_signed_info: happy path ────────────────────────────────
4136
4137    #[test]
4138    fn parse_hmac_output_length_reads_all_text_nodes() {
4139        // A comment may split valid simple content without changing its value.
4140        let xml = r#"<SignatureMethod xmlns="http://www.w3.org/2000/09/xmldsig#">
4141            <HMACOutputLength>8<!-- split -->0</HMACOutputLength>
4142        </SignatureMethod>"#;
4143        let document = Document::parse(xml).unwrap();
4144
4145        assert_eq!(
4146            parse_hmac_output_length(document.root_element(), SignatureAlgorithm::HmacSha1)
4147                .unwrap(),
4148            Some(80)
4149        );
4150    }
4151
4152    #[test]
4153    fn parse_hmac_output_length_rejects_hidden_suffix_text() {
4154        // Reading only the first text node would misinterpret 800 bits as 80.
4155        let xml = r#"<SignatureMethod xmlns="http://www.w3.org/2000/09/xmldsig#">
4156            <HMACOutputLength>80<!-- split -->0</HMACOutputLength>
4157        </SignatureMethod>"#;
4158        let document = Document::parse(xml).unwrap();
4159
4160        assert!(matches!(
4161            parse_hmac_output_length(document.root_element(), SignatureAlgorithm::HmacSha1),
4162            Err(ParseError::InvalidStructure(reason))
4163                if reason == "HMACOutputLength must be a byte-aligned value from 80 through 160"
4164        ));
4165    }
4166
4167    #[test]
4168    fn parse_hmac_output_length_rejects_non_octet_truncation() {
4169        // XMLDSig 1.1 section 6.3.1 requires a byte boundary even though the
4170        // HMACOutputLength schema represents the value as a bit count.
4171        let xml = r#"<SignatureMethod xmlns="http://www.w3.org/2000/09/xmldsig#">
4172            <HMACOutputLength>81</HMACOutputLength>
4173        </SignatureMethod>"#;
4174        let document = Document::parse(xml).unwrap();
4175
4176        assert!(matches!(
4177            parse_hmac_output_length(document.root_element(), SignatureAlgorithm::HmacSha1),
4178            Err(ParseError::InvalidStructure(reason))
4179                if reason == "HMACOutputLength must be a byte-aligned value from 80 through 160"
4180        ));
4181    }
4182
4183    #[test]
4184    fn parse_signed_info_rsa_sha256_with_reference() {
4185        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4186            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4187            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4188            <Reference URI="">
4189                <Transforms>
4190                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
4191                    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4192                </Transforms>
4193                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4194                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
4195            </Reference>
4196        </SignedInfo>"#;
4197        let doc = Document::parse(xml).unwrap();
4198        let si = parse_signed_info(doc.root_element()).unwrap();
4199
4200        assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
4201        assert_eq!(si.references.len(), 1);
4202
4203        let r = &si.references[0];
4204        assert_eq!(r.uri.as_deref(), Some(""));
4205        assert_eq!(r.digest_method, DigestAlgorithm::Sha256);
4206        assert_eq!(r.digest_value, vec![0u8; 32]);
4207        assert_eq!(r.transforms.len(), 2);
4208    }
4209
4210    #[test]
4211    fn parse_signed_info_multiple_references() {
4212        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4213            <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
4214            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
4215            <Reference URI="#a">
4216                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4217                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
4218            </Reference>
4219            <Reference URI="#b">
4220                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
4221                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
4222            </Reference>
4223        </SignedInfo>"##;
4224        let doc = Document::parse(xml).unwrap();
4225        let si = parse_signed_info(doc.root_element()).unwrap();
4226
4227        assert_eq!(si.signature_method, SignatureAlgorithm::EcdsaSha256);
4228        assert_eq!(si.references.len(), 2);
4229        assert_eq!(si.references[0].uri.as_deref(), Some("#a"));
4230        assert_eq!(si.references[0].digest_method, DigestAlgorithm::Sha256);
4231        assert_eq!(si.references[1].uri.as_deref(), Some("#b"));
4232        assert_eq!(si.references[1].digest_method, DigestAlgorithm::Sha1);
4233    }
4234
4235    #[test]
4236    fn parse_signed_info_rejects_too_many_references() {
4237        // Reference processing shares signature-wide resource budgets, so the
4238        // parser must bound cardinality before retaining attacker-controlled entries.
4239        let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
4240            .map(|index| {
4241                format!(
4242                    r##"<Reference URI="#item-{index}">
4243                        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4244                        <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
4245                    </Reference>"##
4246                )
4247            })
4248            .collect::<String>();
4249        let xml = format!(
4250            r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4251                <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4252                <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4253                {references}
4254            </SignedInfo>"#
4255        );
4256        let document = Document::parse(&xml).expect("fixed oversized fixture must parse");
4257
4258        let error = parse_signed_info(document.root_element())
4259            .expect_err("the parser must reject the 65th Reference");
4260
4261        assert!(matches!(
4262            error,
4263            ParseError::TooManyReferences {
4264                max: MAX_REFERENCES_PER_SIGNATURE
4265            }
4266        ));
4267    }
4268
4269    #[test]
4270    fn parse_signed_info_bounds_xpath_expressions_across_references() {
4271        // Per-reference limits alone permit an attacker to retain and compile
4272        // thousands of XPath programs before signature verification begins.
4273        let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
4274            .repeat(64);
4275        let filter_transform = format!(
4276            r#"<Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</Transform>"#
4277        );
4278        let reference = |index, transforms: &str| {
4279            format!(
4280                r##"<Reference URI="#item-{index}">
4281                        <Transforms>{transforms}</Transforms>
4282                        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4283                        <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
4284                    </Reference>"##
4285            )
4286        };
4287        let signed_info = |references: &str| {
4288            format!(
4289                r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4290                <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4291                <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4292                {references}
4293            </SignedInfo>"#
4294            )
4295        };
4296
4297        let max_reference = reference(0, &filter_transform.repeat(64));
4298        let boundary_xml = signed_info(&max_reference);
4299        let boundary_document =
4300            Document::parse(&boundary_xml).expect("fixed boundary fixture must parse");
4301        parse_signed_info(boundary_document.root_element())
4302            .expect("one maximum-shaped Reference must remain accepted");
4303
4304        let extra_transform = r#"<Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><XPath>true()</XPath></Transform>"#;
4305        let xml = signed_info(&format!("{max_reference}{}", reference(1, extra_transform)));
4306        let document = Document::parse(&xml).expect("fixed aggregate fixture must parse");
4307
4308        let error = parse_signed_info(document.root_element())
4309            .expect_err("signature-wide XPath expression count must be bounded");
4310
4311        assert!(
4312            error
4313                .to_string()
4314                .contains("signature-wide XPath expression budget")
4315        );
4316    }
4317
4318    #[test]
4319    fn parse_reference_without_transforms() {
4320        // Transforms element is optional
4321        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4322            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4323            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4324            <Reference URI="#obj">
4325                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4326                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
4327            </Reference>
4328        </SignedInfo>"##;
4329        let doc = Document::parse(xml).unwrap();
4330        let si = parse_signed_info(doc.root_element()).unwrap();
4331
4332        assert!(si.references[0].transforms.is_empty());
4333    }
4334
4335    #[test]
4336    fn parse_reference_with_all_attributes() {
4337        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4338            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4339            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4340            <Reference URI="#data" Id="ref1" Type="http://www.w3.org/2000/09/xmldsig#Object">
4341                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4342                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
4343            </Reference>
4344        </SignedInfo>"##;
4345        let doc = Document::parse(xml).unwrap();
4346        let si = parse_signed_info(doc.root_element()).unwrap();
4347        let r = &si.references[0];
4348
4349        assert_eq!(r.uri.as_deref(), Some("#data"));
4350        assert_eq!(r.id.as_deref(), Some("ref1"));
4351        assert_eq!(
4352            r.ref_type.as_deref(),
4353            Some("http://www.w3.org/2000/09/xmldsig#Object")
4354        );
4355    }
4356
4357    #[test]
4358    fn parse_reference_absent_uri() {
4359        // URI attribute is optional per spec
4360        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4361            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4362            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4363            <Reference>
4364                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4365                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
4366            </Reference>
4367        </SignedInfo>"#;
4368        let doc = Document::parse(xml).unwrap();
4369        let si = parse_signed_info(doc.root_element()).unwrap();
4370        assert!(si.references[0].uri.is_none());
4371    }
4372
4373    #[test]
4374    fn parse_signed_info_preserves_inclusive_prefixes() {
4375        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
4376                                 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
4377            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
4378                <ec:InclusiveNamespaces PrefixList="ds saml #default"/>
4379            </CanonicalizationMethod>
4380            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4381            <Reference URI="">
4382                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4383                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
4384            </Reference>
4385        </SignedInfo>"#;
4386        let doc = Document::parse(xml).unwrap();
4387
4388        let si = parse_signed_info(doc.root_element()).unwrap();
4389        assert!(si.c14n_method.inclusive_prefixes().contains("ds"));
4390        assert!(si.c14n_method.inclusive_prefixes().contains("saml"));
4391        assert!(si.c14n_method.inclusive_prefixes().contains(""));
4392    }
4393
4394    // ── parse_signed_info: error cases ───────────────────────────────
4395
4396    #[test]
4397    fn missing_canonicalization_method() {
4398        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4399            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4400            <Reference URI="">
4401                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4402                <DigestValue>dGVzdA==</DigestValue>
4403            </Reference>
4404        </SignedInfo>"#;
4405        let doc = Document::parse(xml).unwrap();
4406        let result = parse_signed_info(doc.root_element());
4407        assert!(result.is_err());
4408        // SignatureMethod is first child but expected CanonicalizationMethod
4409        assert!(matches!(
4410            result.unwrap_err(),
4411            ParseError::InvalidStructure(_)
4412        ));
4413    }
4414
4415    #[test]
4416    fn missing_signature_method() {
4417        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4418            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4419            <Reference URI="">
4420                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4421                <DigestValue>dGVzdA==</DigestValue>
4422            </Reference>
4423        </SignedInfo>"#;
4424        let doc = Document::parse(xml).unwrap();
4425        let result = parse_signed_info(doc.root_element());
4426        assert!(result.is_err());
4427        // Reference is second child but expected SignatureMethod
4428        assert!(matches!(
4429            result.unwrap_err(),
4430            ParseError::InvalidStructure(_)
4431        ));
4432    }
4433
4434    #[test]
4435    fn no_references() {
4436        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4437            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4438            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4439        </SignedInfo>"#;
4440        let doc = Document::parse(xml).unwrap();
4441        let result = parse_signed_info(doc.root_element());
4442        assert!(matches!(
4443            result.unwrap_err(),
4444            ParseError::MissingElement {
4445                element: "Reference"
4446            }
4447        ));
4448    }
4449
4450    #[test]
4451    fn unsupported_c14n_algorithm() {
4452        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4453            <CanonicalizationMethod Algorithm="http://example.com/bogus-c14n"/>
4454            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4455            <Reference URI="">
4456                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4457                <DigestValue>dGVzdA==</DigestValue>
4458            </Reference>
4459        </SignedInfo>"#;
4460        let doc = Document::parse(xml).unwrap();
4461        let result = parse_signed_info(doc.root_element());
4462        assert!(matches!(
4463            result.unwrap_err(),
4464            ParseError::UnsupportedAlgorithm { .. }
4465        ));
4466    }
4467
4468    #[test]
4469    fn unsupported_signature_algorithm() {
4470        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4471            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4472            <SignatureMethod Algorithm="http://example.com/bogus-sign"/>
4473            <Reference URI="">
4474                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4475                <DigestValue>dGVzdA==</DigestValue>
4476            </Reference>
4477        </SignedInfo>"#;
4478        let doc = Document::parse(xml).unwrap();
4479        let result = parse_signed_info(doc.root_element());
4480        assert!(matches!(
4481            result.unwrap_err(),
4482            ParseError::UnsupportedAlgorithm { .. }
4483        ));
4484    }
4485
4486    #[test]
4487    fn unsupported_digest_algorithm() {
4488        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4489            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4490            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4491            <Reference URI="">
4492                <DigestMethod Algorithm="http://example.com/bogus-digest"/>
4493                <DigestValue>dGVzdA==</DigestValue>
4494            </Reference>
4495        </SignedInfo>"#;
4496        let doc = Document::parse(xml).unwrap();
4497        let result = parse_signed_info(doc.root_element());
4498        assert!(matches!(
4499            result.unwrap_err(),
4500            ParseError::UnsupportedAlgorithm { .. }
4501        ));
4502    }
4503
4504    #[test]
4505    fn missing_digest_method() {
4506        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4507            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4508            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4509            <Reference URI="">
4510                <DigestValue>dGVzdA==</DigestValue>
4511            </Reference>
4512        </SignedInfo>"#;
4513        let doc = Document::parse(xml).unwrap();
4514        let result = parse_signed_info(doc.root_element());
4515        // DigestValue is not DigestMethod
4516        assert!(result.is_err());
4517    }
4518
4519    #[test]
4520    fn missing_digest_value() {
4521        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4522            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4523            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4524            <Reference URI="">
4525                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4526            </Reference>
4527        </SignedInfo>"#;
4528        let doc = Document::parse(xml).unwrap();
4529        let result = parse_signed_info(doc.root_element());
4530        assert!(matches!(
4531            result.unwrap_err(),
4532            ParseError::MissingElement {
4533                element: "DigestValue"
4534            }
4535        ));
4536    }
4537
4538    #[test]
4539    fn invalid_base64_digest_value() {
4540        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4541            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4542            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4543            <Reference URI="">
4544                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4545                <DigestValue>!!!not-base64!!!</DigestValue>
4546            </Reference>
4547        </SignedInfo>"#;
4548        let doc = Document::parse(xml).unwrap();
4549        let result = parse_signed_info(doc.root_element());
4550        assert!(matches!(result.unwrap_err(), ParseError::Base64(_)));
4551    }
4552
4553    #[test]
4554    fn digest_value_length_must_match_digest_method() {
4555        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4556            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4557            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4558            <Reference URI="">
4559                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4560                <DigestValue>dGVzdA==</DigestValue>
4561            </Reference>
4562        </SignedInfo>"#;
4563        let doc = Document::parse(xml).unwrap();
4564
4565        let result = parse_signed_info(doc.root_element());
4566        assert!(matches!(
4567            result.unwrap_err(),
4568            ParseError::DigestLengthMismatch {
4569                algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
4570                expected: 32,
4571                actual: 4,
4572            }
4573        ));
4574    }
4575
4576    #[test]
4577    fn inclusive_prefixes_on_inclusive_c14n_is_rejected() {
4578        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
4579                                 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
4580            <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">
4581                <ec:InclusiveNamespaces PrefixList="ds"/>
4582            </CanonicalizationMethod>
4583            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4584            <Reference URI="">
4585                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
4586                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
4587            </Reference>
4588        </SignedInfo>"#;
4589        let doc = Document::parse(xml).unwrap();
4590
4591        let result = parse_signed_info(doc.root_element());
4592        assert!(matches!(
4593            result.unwrap_err(),
4594            ParseError::UnsupportedAlgorithm { .. }
4595        ));
4596    }
4597
4598    #[test]
4599    fn extra_element_after_digest_value() {
4600        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4601            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4602            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4603            <Reference URI="">
4604                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4605                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
4606                <Unexpected/>
4607            </Reference>
4608        </SignedInfo>"#;
4609        let doc = Document::parse(xml).unwrap();
4610        let result = parse_signed_info(doc.root_element());
4611        assert!(matches!(
4612            result.unwrap_err(),
4613            ParseError::InvalidStructure(_)
4614        ));
4615    }
4616
4617    #[test]
4618    fn digest_value_with_element_child_is_rejected() {
4619        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4620            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4621            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4622            <Reference URI="">
4623                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
4624                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=<Junk/>AAAA</DigestValue>
4625            </Reference>
4626        </SignedInfo>"#;
4627        let doc = Document::parse(xml).unwrap();
4628
4629        let result = parse_signed_info(doc.root_element());
4630        assert!(matches!(
4631            result.unwrap_err(),
4632            ParseError::InvalidStructure(_)
4633        ));
4634    }
4635
4636    #[test]
4637    fn wrong_namespace_on_signed_info() {
4638        let xml = r#"<SignedInfo xmlns="http://example.com/fake">
4639            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4640        </SignedInfo>"#;
4641        let doc = Document::parse(xml).unwrap();
4642        let result = parse_signed_info(doc.root_element());
4643        assert!(matches!(
4644            result.unwrap_err(),
4645            ParseError::InvalidStructure(_)
4646        ));
4647    }
4648
4649    // ── Whitespace-wrapped base64 ────────────────────────────────────
4650
4651    #[test]
4652    fn base64_with_whitespace() {
4653        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4654            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4655            <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
4656            <Reference URI="">
4657                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
4658                <DigestValue>
4659                    AAAAAAAA
4660                    AAAAAAAAAAAAAAAAAAA=
4661                </DigestValue>
4662            </Reference>
4663        </SignedInfo>"#;
4664        let doc = Document::parse(xml).unwrap();
4665        let si = parse_signed_info(doc.root_element()).unwrap();
4666        assert_eq!(si.references[0].digest_value, vec![0u8; 20]);
4667    }
4668
4669    #[test]
4670    fn base64_decode_digest_accepts_xml_whitespace_chars() {
4671        let digest =
4672            base64_decode_digest("AAAA\tAAAA\rAAAA\nAAAA AAAAAAAAAAA=", DigestAlgorithm::Sha1)
4673                .expect("XML whitespace in DigestValue must be accepted");
4674        assert_eq!(digest, vec![0u8; 20]);
4675    }
4676
4677    #[test]
4678    fn base64_decode_digest_rejects_non_xml_ascii_whitespace() {
4679        let err = base64_decode_digest(
4680            "AAAA\u{000C}AAAAAAAAAAAAAAAAAAAAAAA=",
4681            DigestAlgorithm::Sha1,
4682        )
4683        .expect_err("form-feed/vertical-tab in DigestValue must be rejected");
4684        assert!(matches!(err, ParseError::Base64(_)));
4685    }
4686
4687    #[test]
4688    fn base64_decode_digest_rejects_oversized_base64_before_decode() {
4689        let err = base64_decode_digest("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", DigestAlgorithm::Sha1)
4690            .expect_err("oversized DigestValue base64 must fail before decode");
4691        match err {
4692            ParseError::Base64(message) => {
4693                assert!(
4694                    message.contains("DigestValue exceeds maximum allowed base64 length"),
4695                    "unexpected message: {message}"
4696                );
4697            }
4698            other => panic!("expected ParseError::Base64, got {other:?}"),
4699        }
4700    }
4701
4702    // ── Real-world SAML structure ────────────────────────────────────
4703
4704    #[test]
4705    fn saml_response_signed_info() {
4706        let xml = r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4707            <ds:SignedInfo>
4708                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4709                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
4710                <ds:Reference URI="#_resp1">
4711                    <ds:Transforms>
4712                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
4713                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4714                    </ds:Transforms>
4715                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
4716                    <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
4717                </ds:Reference>
4718            </ds:SignedInfo>
4719            <ds:SignatureValue>ZmFrZQ==</ds:SignatureValue>
4720        </ds:Signature>"##;
4721        let doc = Document::parse(xml).unwrap();
4722
4723        // Find SignedInfo within Signature
4724        let sig_node = doc.root_element();
4725        let signed_info_node = sig_node
4726            .children()
4727            .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
4728            .unwrap();
4729
4730        let si = parse_signed_info(signed_info_node).unwrap();
4731        assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
4732        assert_eq!(si.references.len(), 1);
4733        assert_eq!(si.references[0].uri.as_deref(), Some("#_resp1"));
4734        assert_eq!(si.references[0].transforms.len(), 2);
4735        assert_eq!(si.references[0].digest_value, vec![0u8; 32]);
4736    }
4737}