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