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