Skip to main content

xml_sec/xmldsig/
parse.rs

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