Skip to main content

xml_sec/xmldsig/
parse.rs

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