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