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    /// Start a session using the operation's resource policy.
1370    pub fn new(resources: &'a crate::policy::ResourcePolicy) -> Result<Self, ParseError> {
1371        resources.validate()?;
1372        Ok(Self {
1373            resources,
1374            xml_base: XmlBaseResolutionBudget::with_limits(
1375                resources.effective_xml_base_components(),
1376                resources.effective_xml_base_resolution_bytes(),
1377            ),
1378            usage: KeyInfoParseUsage::default(),
1379        })
1380    }
1381
1382    /// Parse another element without resetting this operation's allowance.
1383    pub fn parse(&mut self, node: Node) -> Result<KeyInfo, ParseError> {
1384        self.parse_with_provider(node, crate::provider::default_provider())
1385    }
1386
1387    /// Parse with the operation's explicit crypto engine and existing allowance.
1388    pub fn parse_with_provider(
1389        &mut self,
1390        node: Node,
1391        provider: &dyn crate::provider::CryptoProvider,
1392    ) -> Result<KeyInfo, ParseError> {
1393        parse_key_info_in_session(
1394            node,
1395            provider,
1396            &self.xml_base,
1397            self.resources,
1398            None,
1399            None,
1400            &mut self.usage,
1401        )
1402    }
1403
1404    /// Parse the KeyInfoType carried by an XML Encryption agreement role.
1405    /// Keep the original node for XML Base and share this session's allowance.
1406    #[cfg(feature = "xmlenc")]
1407    pub(crate) fn parse_agreement_role(
1408        &mut self,
1409        node: Node,
1410        provider: &dyn crate::provider::CryptoProvider,
1411    ) -> Result<KeyInfo, ParseError> {
1412        let namespace = "http://www.w3.org/2001/04/xmlenc#";
1413        if !node.has_tag_name((namespace, "OriginatorKeyInfo"))
1414            && !node.has_tag_name((namespace, "RecipientKeyInfo"))
1415        {
1416            return Err(ParseError::InvalidStructure(
1417                "invalid agreement key role".into(),
1418            ));
1419        }
1420        ensure_no_non_whitespace_text(node, "agreement KeyInfo")?;
1421        parse_key_info_contents_in_session(
1422            node,
1423            provider,
1424            &self.xml_base,
1425            self.resources,
1426            None,
1427            None,
1428            &mut self.usage,
1429        )
1430    }
1431}
1432
1433pub(crate) fn parse_key_info_with_provider(
1434    key_info_node: Node,
1435    provider: &dyn crate::provider::CryptoProvider,
1436) -> Result<KeyInfo, ParseError> {
1437    let xml_base_budget = XmlBaseResolutionBudget::default();
1438    parse_key_info_with_policy_budgets(
1439        key_info_node,
1440        provider,
1441        &xml_base_budget,
1442        &crate::policy::ResourcePolicy::default(),
1443        None,
1444    )
1445}
1446
1447pub(crate) fn parse_key_info_with_policy_budgets(
1448    key_info_node: Node,
1449    provider: &dyn crate::provider::CryptoProvider,
1450    xml_base_budget: &XmlBaseResolutionBudget,
1451    resources: &crate::policy::ResourcePolicy,
1452    allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
1453) -> Result<KeyInfo, ParseError> {
1454    parse_key_info_with_policy_budgets_and_document_base(
1455        key_info_node,
1456        provider,
1457        xml_base_budget,
1458        resources,
1459        None,
1460        allowed,
1461    )
1462}
1463
1464pub(crate) fn parse_key_info_with_policy_budgets_and_document_base(
1465    key_info_node: Node,
1466    provider: &dyn crate::provider::CryptoProvider,
1467    xml_base_budget: &XmlBaseResolutionBudget,
1468    resources: &crate::policy::ResourcePolicy,
1469    document_base: Option<&str>,
1470    allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
1471) -> Result<KeyInfo, ParseError> {
1472    parse_key_info_in_session(
1473        key_info_node,
1474        provider,
1475        xml_base_budget,
1476        resources,
1477        document_base,
1478        allowed,
1479        &mut KeyInfoParseUsage::default(),
1480    )
1481}
1482
1483fn parse_key_info_in_session(
1484    key_info_node: Node,
1485    provider: &dyn crate::provider::CryptoProvider,
1486    xml_base_budget: &XmlBaseResolutionBudget,
1487    resources: &crate::policy::ResourcePolicy,
1488    document_base: Option<&str>,
1489    allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
1490    usage: &mut KeyInfoParseUsage,
1491) -> Result<KeyInfo, ParseError> {
1492    validate_key_info_container(key_info_node)?;
1493
1494    parse_key_info_contents_in_session(
1495        key_info_node,
1496        provider,
1497        xml_base_budget,
1498        resources,
1499        document_base,
1500        allowed,
1501        usage,
1502    )
1503}
1504
1505fn parse_key_info_contents_in_session(
1506    key_info_node: Node,
1507    provider: &dyn crate::provider::CryptoProvider,
1508    xml_base_budget: &XmlBaseResolutionBudget,
1509    resources: &crate::policy::ResourcePolicy,
1510    document_base: Option<&str>,
1511    allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
1512    usage: &mut KeyInfoParseUsage,
1513) -> Result<KeyInfo, ParseError> {
1514    let mut sources = Vec::new();
1515    // KeyInfo is parsed before source selection, so preflight the cardinality
1516    // of every embedded key before decoding or algorithm-specific parsing.
1517    // This does not consume the resolver's inspected-candidate work budget.
1518    for (index, child) in element_children(key_info_node).enumerate() {
1519        if index >= MAX_KEY_INFO_CHILD_COUNT {
1520            return Err(ParseError::InvalidStructure(
1521                "KeyInfo contains too many child elements".into(),
1522            ));
1523        }
1524        match (child.tag_name().namespace(), child.tag_name().name()) {
1525            (Some(XMLDSIG_NS), "KeyName") => {
1526                ensure_no_element_children(child, "KeyName")?;
1527                let key_name =
1528                    collect_text_content_bounded(child, MAX_KEY_NAME_TEXT_LEN, "KeyName")?;
1529                sources.push(KeyInfoSource::KeyName(key_name));
1530            }
1531            (Some(XMLDSIG_NS), "KeyValue") => {
1532                charge_embedded_key_candidate(&mut usage.embedded_candidates, resources)?;
1533                let key_value = parse_key_value_dispatch(child)?;
1534                sources.push(KeyInfoSource::KeyValue(key_value));
1535            }
1536            (Some(XMLDSIG_NS), "X509Data") => {
1537                let x509 = parse_x509_data_dispatch_with_budget_and_provider(
1538                    child,
1539                    &mut usage.x509_binary_bytes,
1540                    &mut usage.embedded_candidates,
1541                    provider,
1542                    resources,
1543                    allowed,
1544                )?;
1545                sources.push(KeyInfoSource::X509Data(x509));
1546            }
1547            (Some(XMLDSIG_NS), "RetrievalMethod") => {
1548                ensure_no_non_whitespace_text(child, "RetrievalMethod")?;
1549                let lexical_uri = child.attribute("URI").ok_or_else(|| {
1550                    ParseError::InvalidStructure("RetrievalMethod requires URI".into())
1551                })?;
1552                if lexical_uri.len() > MAX_KEY_NAME_TEXT_LEN {
1553                    return Err(ParseError::InvalidStructure(
1554                        "RetrievalMethod URI exceeds maximum length".into(),
1555                    ));
1556                }
1557                let uri = if lexical_uri.is_empty() || lexical_uri.starts_with('#') {
1558                    lexical_uri.to_owned()
1559                } else {
1560                    // RetrievalMethod is parsed independently from later key
1561                    // materialization, so retain its resolved resource identity.
1562                    resolve_uri_from_node_with_document_base_with_budget(
1563                        child,
1564                        lexical_uri,
1565                        document_base,
1566                        xml_base_budget,
1567                    )
1568                    .map_err(|error| ParseError::InvalidStructure(error.to_string()))?
1569                };
1570                let resource_type = child.attribute("Type");
1571                if resource_type.is_some_and(|value| value.len() > MAX_KEY_NAME_TEXT_LEN) {
1572                    return Err(ParseError::InvalidStructure(
1573                        "RetrievalMethod Type exceeds maximum length".into(),
1574                    ));
1575                }
1576                let resource_type = resource_type.map(str::to_owned);
1577                let transforms = if resource_type.as_deref()
1578                    == Some("http://www.w3.org/2000/09/xmldsig#X509Data")
1579                {
1580                    parse_retrieval_method_transforms(child, resources)?
1581                } else if element_children(child).next().is_some() {
1582                    RetrievalMethodTransforms::Unsupported
1583                } else {
1584                    RetrievalMethodTransforms::None
1585                };
1586                sources.push(KeyInfoSource::RetrievalMethod {
1587                    uri,
1588                    resource_type,
1589                    transforms,
1590                });
1591            }
1592            (Some(XMLDSIG11_NS), "DEREncodedKeyValue") => {
1593                charge_embedded_key_candidate(&mut usage.embedded_candidates, resources)?;
1594                ensure_no_element_children(child, "DEREncodedKeyValue")?;
1595                let der = decode_der_encoded_key_value_base64(child)?;
1596                sources.push(KeyInfoSource::DerEncodedKeyValue(der));
1597            }
1598            (Some(XMLDSIG11_NS), "KeyInfoReference") => {
1599                ensure_no_element_children(child, "KeyInfoReference")?;
1600                ensure_no_non_whitespace_text(child, "KeyInfoReference")?;
1601                let lexical_uri = child.attribute("URI").ok_or_else(|| {
1602                    ParseError::InvalidStructure("KeyInfoReference requires URI".into())
1603                })?;
1604                if lexical_uri.len() > MAX_KEY_NAME_TEXT_LEN {
1605                    return Err(ParseError::InvalidStructure(
1606                        "KeyInfoReference URI exceeds maximum length".into(),
1607                    ));
1608                }
1609                let uri = if lexical_uri.is_empty() || lexical_uri.starts_with('#') {
1610                    lexical_uri.to_owned()
1611                } else {
1612                    resolve_uri_from_node_with_document_base_with_budget(
1613                        child,
1614                        lexical_uri,
1615                        document_base,
1616                        xml_base_budget,
1617                    )
1618                    .map_err(|error| ParseError::InvalidStructure(error.to_string()))?
1619                };
1620                sources.push(KeyInfoSource::KeyInfoReference { uri });
1621            }
1622            _ => {}
1623        }
1624    }
1625
1626    Ok(KeyInfo { sources })
1627}
1628
1629fn charge_embedded_key_candidate(
1630    embedded_key_candidates: &mut usize,
1631    resources: &crate::policy::ResourcePolicy,
1632) -> Result<(), ParseError> {
1633    *embedded_key_candidates = embedded_key_candidates.saturating_add(1);
1634    resources.validate_key_candidates(*embedded_key_candidates)?;
1635    Ok(())
1636}
1637
1638fn parse_retrieval_method_transforms(
1639    node: Node<'_, '_>,
1640    resources: &crate::policy::ResourcePolicy,
1641) -> Result<RetrievalMethodTransforms, ParseError> {
1642    let mut children = element_children(node);
1643    let Some(transforms) = children.next() else {
1644        return Ok(RetrievalMethodTransforms::None);
1645    };
1646    if children.next().is_some()
1647        || transforms.tag_name().namespace() != Some(XMLDSIG_NS)
1648        || transforms.tag_name().name() != "Transforms"
1649    {
1650        return Err(ParseError::InvalidStructure(
1651            "RetrievalMethod accepts only one optional ds:Transforms child".into(),
1652        ));
1653    }
1654    ensure_no_non_whitespace_text(transforms, "Transforms")?;
1655    let mut transform_children = element_children(transforms);
1656    let transform = transform_children.next().ok_or_else(|| {
1657        ParseError::InvalidStructure("RetrievalMethod Transforms must not be empty".into())
1658    })?;
1659    if transform_children.next().is_some()
1660        || transform.tag_name().namespace() != Some(XMLDSIG_NS)
1661        || transform.tag_name().name() != "Transform"
1662        || transform.attribute("Algorithm") != Some(transforms::XPATH_TRANSFORM_URI)
1663    {
1664        return Err(ParseError::InvalidStructure(
1665            "unsupported RetrievalMethod transform chain".into(),
1666        ));
1667    }
1668    ensure_no_non_whitespace_text(transform, "Transform")?;
1669    let mut parameters = element_children(transform);
1670    let xpath = parameters.next().ok_or_else(|| {
1671        ParseError::InvalidStructure("RetrievalMethod XPath parameter is missing".into())
1672    })?;
1673    if parameters.next().is_some()
1674        || xpath.tag_name().namespace() != Some(XMLDSIG_NS)
1675        || xpath.tag_name().name() != "XPath"
1676    {
1677        return Err(ParseError::InvalidStructure(
1678            "unsupported RetrievalMethod transform chain".into(),
1679        ));
1680    }
1681    ensure_no_element_children(xpath, "XPath")?;
1682    let expression =
1683        collect_text_content_bounded(xpath, MAX_RETRIEVAL_XPATH_TEXT_LEN, "RetrievalMethod XPath")?;
1684    let normalized_expression = expression.trim();
1685    let selects_x509_data = normalized_expression
1686        .strip_prefix("ancestor-or-self::")
1687        .and_then(|step| step.split_once(':'))
1688        .is_some_and(|(prefix, local)| {
1689            local == "X509Data" && xpath.lookup_namespace_uri(Some(prefix)) == Some(XMLDSIG_NS)
1690        });
1691    if !selects_x509_data {
1692        return Err(ParseError::InvalidStructure(
1693            "unsupported RetrievalMethod XPath selection".into(),
1694        ));
1695    }
1696    let namespaces = transforms::collect_xpath_namespaces_with_resources(xpath, resources)?;
1697    Ok(RetrievalMethodTransforms::X509DataNodeSetFilter {
1698        expression,
1699        namespaces,
1700    })
1701}
1702
1703// ── Helpers ──────────────────────────────────────────────────────────────────
1704
1705/// Iterate only element children (skip text, comments, PIs).
1706fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
1707    node.children().filter(|n| n.is_element())
1708}
1709
1710/// Verify that a node is a `<ds:{expected_name}>` element.
1711fn verify_ds_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
1712    verify_namespaced_element(node, expected_name, XMLDSIG_NS, "ds")
1713}
1714
1715/// Verify that a node is a `<dsig11:{expected_name}>` element.
1716fn verify_dsig11_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
1717    verify_namespaced_element(node, expected_name, XMLDSIG11_NS, "dsig11")
1718}
1719
1720fn verify_namespaced_element(
1721    node: Node,
1722    expected_name: &'static str,
1723    expected_namespace: &'static str,
1724    diagnostic_prefix: &'static str,
1725) -> Result<(), ParseError> {
1726    if !node.is_element() {
1727        return Err(ParseError::InvalidStructure(format!(
1728            "expected element <{expected_name}>, got non-element node"
1729        )));
1730    }
1731    let tag = node.tag_name();
1732    if tag.name() != expected_name || tag.namespace() != Some(expected_namespace) {
1733        return Err(ParseError::InvalidStructure(format!(
1734            "expected <{diagnostic_prefix}:{expected_name}>, got <{}{}>",
1735            tag.namespace()
1736                .map(|ns| format!("{{{ns}}}"))
1737                .unwrap_or_default(),
1738            tag.name()
1739        )));
1740    }
1741    Ok(())
1742}
1743
1744/// Get the required `Algorithm` attribute from an element.
1745fn required_algorithm_attr<'a>(
1746    node: Node<'a, 'a>,
1747    element_name: &'static str,
1748) -> Result<&'a str, ParseError> {
1749    node.attribute("Algorithm").ok_or_else(|| {
1750        ParseError::InvalidStructure(format!("missing Algorithm attribute on <{element_name}>"))
1751    })
1752}
1753
1754/// Parse the `PrefixList` attribute from an `<ec:InclusiveNamespaces>` child of
1755/// `<CanonicalizationMethod>`, if present.
1756///
1757/// This mirrors transform parsing for Exclusive C14N and keeps SignedInfo
1758/// canonicalization parameters lossless.
1759fn parse_inclusive_prefixes(node: Node) -> Result<Option<String>, ParseError> {
1760    const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
1761
1762    for child in node.children() {
1763        if child.is_element() {
1764            let tag = child.tag_name();
1765            if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
1766            {
1767                return child
1768                    .attribute("PrefixList")
1769                    .map(str::to_string)
1770                    .ok_or_else(|| {
1771                        ParseError::InvalidStructure(
1772                            "missing PrefixList attribute on <InclusiveNamespaces>".into(),
1773                        )
1774                    })
1775                    .map(Some);
1776            }
1777        }
1778    }
1779
1780    Ok(None)
1781}
1782
1783pub(crate) fn parse_key_value_dispatch(node: Node) -> Result<KeyValueInfo, ParseError> {
1784    verify_ds_element(node, "KeyValue")?;
1785    ensure_no_non_whitespace_text(node, "KeyValue")?;
1786
1787    let mut children = element_children(node);
1788    let Some(first_child) = children.next() else {
1789        return Err(ParseError::InvalidStructure(
1790            "KeyValue must contain exactly one key-value child".into(),
1791        ));
1792    };
1793    if children.next().is_some() {
1794        return Err(ParseError::InvalidStructure(
1795            "KeyValue must contain exactly one key-value child".into(),
1796        ));
1797    }
1798
1799    match (
1800        first_child.tag_name().namespace(),
1801        first_child.tag_name().name(),
1802    ) {
1803        (Some(XMLDSIG_NS), "RSAKeyValue") => parse_rsa_key_value(first_child),
1804        (Some(XMLDSIG_NS), "DSAKeyValue") => parse_dsa_key_value(first_child),
1805        (Some(XMLDSIG11_NS), "ECKeyValue") => parse_ec_key_value(first_child),
1806        (Some("http://www.w3.org/2001/04/xmlenc#"), "DHKeyValue") => {
1807            parse_dh_key_value(first_child)
1808        }
1809        (namespace, child_name) => Ok(KeyValueInfo::Unsupported {
1810            namespace: namespace.map(str::to_string),
1811            local_name: child_name.to_string(),
1812        }),
1813    }
1814}
1815
1816pub(crate) fn validate_key_info_container(node: Node) -> Result<(), ParseError> {
1817    verify_ds_element(node, "KeyInfo")?;
1818    ensure_no_non_whitespace_text(node, "KeyInfo")
1819}
1820
1821fn parse_dsa_key_value(node: Node<'_, '_>) -> Result<KeyValueInfo, ParseError> {
1822    verify_ds_element(node, "DSAKeyValue")?;
1823    ensure_no_non_whitespace_text(node, "DSAKeyValue")?;
1824    let children = element_children(node).collect::<Vec<_>>();
1825    let mut index = 0;
1826    let p = take_dsa_crypto_binary(&children, &mut index, "P")?;
1827    let q = take_dsa_crypto_binary(&children, &mut index, "Q")?;
1828    if p.is_some() != q.is_some() {
1829        return Err(ParseError::InvalidStructure(
1830            "DSAKeyValue P and Q must be present together".into(),
1831        ));
1832    }
1833    let g = take_dsa_crypto_binary(&children, &mut index, "G")?;
1834    let y = take_dsa_crypto_binary(&children, &mut index, "Y")?
1835        .ok_or_else(|| ParseError::InvalidStructure("DSAKeyValue requires Y".into()))?;
1836    let _j = take_dsa_crypto_binary(&children, &mut index, "J")?;
1837    let seed = take_dsa_crypto_binary(&children, &mut index, "Seed")?;
1838    let counter = take_dsa_crypto_binary(&children, &mut index, "PgenCounter")?;
1839    if seed.is_some() != counter.is_some() {
1840        return Err(ParseError::InvalidStructure(
1841            "DSAKeyValue Seed and PgenCounter must be present together".into(),
1842        ));
1843    }
1844    if index != children.len() {
1845        return Err(ParseError::InvalidStructure(
1846            "DSAKeyValue children do not match the XMLDSig schema order".into(),
1847        ));
1848    }
1849    Ok(KeyValueInfo::Dsa { p, q, g, y })
1850}
1851
1852fn parse_dh_key_value(node: Node<'_, '_>) -> Result<KeyValueInfo, ParseError> {
1853    const NS: &str = "http://www.w3.org/2001/04/xmlenc#";
1854    ensure_no_non_whitespace_text(node, "DHKeyValue")?;
1855    let mut children = element_children(node).peekable();
1856    let mut take = |name: &'static str| -> Result<Option<Vec<u8>>, ParseError> {
1857        if !children
1858            .peek()
1859            .is_some_and(|child| child.has_tag_name((NS, name)))
1860        {
1861            return Ok(None);
1862        }
1863        let child = children.next().expect("peeked child");
1864        ensure_no_element_children(child, name)?;
1865        decode_crypto_binary(
1866            child,
1867            name,
1868            crate::hard_limits::DH_MODULUS_BIT_CEILING.div_ceil(8),
1869        )
1870        .map(Some)
1871    };
1872    // XMLEnc 1.1 §5.6.1 has optional sequences, not independently optional
1873    // fields; absent parameters must come from trusted application state.
1874    // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-DHKeyValue
1875    let p = take("P")?;
1876    let q = take("Q")?;
1877    let generator = take("Generator")?;
1878    if p.is_some() != q.is_some() || p.is_some() != generator.is_some() {
1879        return Err(ParseError::InvalidStructure(
1880            "DHKeyValue requires P, Q and Generator together".into(),
1881        ));
1882    }
1883    let public = take("Public")?
1884        .ok_or_else(|| ParseError::InvalidStructure("DHKeyValue requires Public".into()))?;
1885    let seed = take("seed")?;
1886    let pgen_counter = take("pgenCounter")?;
1887    if seed.is_some() != pgen_counter.is_some() || children.next().is_some() {
1888        return Err(ParseError::InvalidStructure(
1889            "DHKeyValue children do not match the XMLEnc schema order".into(),
1890        ));
1891    }
1892    Ok(KeyValueInfo::Dh {
1893        p,
1894        q,
1895        generator,
1896        public,
1897        seed,
1898        pgen_counter,
1899    })
1900}
1901
1902fn take_dsa_crypto_binary(
1903    children: &[Node<'_, '_>],
1904    index: &mut usize,
1905    name: &'static str,
1906) -> Result<Option<Vec<u8>>, ParseError> {
1907    let Some(&child) = children.get(*index) else {
1908        return Ok(None);
1909    };
1910    if !is_ds_element(child, name) {
1911        return Ok(None);
1912    }
1913    *index += 1;
1914    ensure_no_element_children(child, name)?;
1915    decode_crypto_binary(child, name, MAX_RSA_MODULUS_LEN).map(Some)
1916}
1917
1918fn is_ds_element(node: Node<'_, '_>, name: &str) -> bool {
1919    node.tag_name().namespace() == Some(XMLDSIG_NS) && node.tag_name().name() == name
1920}
1921
1922fn parse_ec_key_value(node: Node<'_, '_>) -> Result<KeyValueInfo, ParseError> {
1923    verify_dsig11_element(node, "ECKeyValue")?;
1924    ensure_no_non_whitespace_text(node, "ECKeyValue")?;
1925
1926    let mut children = element_children(node);
1927    let Some(named_curve_node) = children.next() else {
1928        return Ok(KeyValueInfo::InvalidEcKeyValue);
1929    };
1930    if named_curve_node.tag_name().namespace() == Some(XMLDSIG11_NS)
1931        && named_curve_node.tag_name().name() == "ECParameters"
1932    {
1933        return Ok(KeyValueInfo::Unsupported {
1934            namespace: Some(XMLDSIG11_NS.to_string()),
1935            local_name: "ECKeyValue".into(),
1936        });
1937    }
1938    if named_curve_node.tag_name().namespace() != Some(XMLDSIG11_NS)
1939        || named_curve_node.tag_name().name() != "NamedCurve"
1940    {
1941        return Ok(KeyValueInfo::InvalidEcKeyValue);
1942    }
1943    ensure_no_element_children(named_curve_node, "NamedCurve")?;
1944    ensure_no_non_whitespace_text(named_curve_node, "NamedCurve")?;
1945    let Some((curve_oid, expected_public_key_len)) =
1946        (match parse_ec_named_curve_oid(named_curve_node) {
1947            Ok(curve) => curve,
1948            Err(_) => return Ok(KeyValueInfo::InvalidEcKeyValue),
1949        })
1950    else {
1951        return Ok(KeyValueInfo::Unsupported {
1952            namespace: Some(XMLDSIG11_NS.to_string()),
1953            local_name: "ECKeyValue".into(),
1954        });
1955    };
1956
1957    let Some(public_key_node) = children.next() else {
1958        return Ok(KeyValueInfo::InvalidEcKeyValue);
1959    };
1960    if public_key_node.tag_name().namespace() != Some(XMLDSIG11_NS)
1961        || public_key_node.tag_name().name() != "PublicKey"
1962    {
1963        return Ok(KeyValueInfo::InvalidEcKeyValue);
1964    }
1965    ensure_no_element_children(public_key_node, "PublicKey")?;
1966    if children.next().is_some() {
1967        return Ok(KeyValueInfo::InvalidEcKeyValue);
1968    }
1969
1970    let public_key = match decode_crypto_binary(public_key_node, "PublicKey", MAX_EC_PUBLIC_KEY_LEN)
1971    {
1972        Ok(public_key) => public_key,
1973        Err(_) => return Ok(KeyValueInfo::InvalidEcKeyValue),
1974    };
1975    if validate_ec_public_key_point(&public_key, expected_public_key_len).is_err() {
1976        return Ok(KeyValueInfo::InvalidEcKeyValue);
1977    }
1978
1979    Ok(KeyValueInfo::Ec {
1980        curve_oid,
1981        public_key,
1982    })
1983}
1984
1985fn parse_ec_named_curve_oid(node: Node<'_, '_>) -> Result<Option<(String, usize)>, ParseError> {
1986    let uri = node.attribute("URI").ok_or_else(|| {
1987        ParseError::InvalidStructure("ECKeyValue NamedCurve must include URI attribute".into())
1988    })?;
1989    let curve_oid = uri.strip_prefix("urn:oid:").unwrap_or(uri);
1990    if curve_oid.is_empty() {
1991        return Err(ParseError::InvalidStructure(
1992            "ECKeyValue NamedCurve URI must not be empty".into(),
1993        ));
1994    }
1995    let Some(public_key_len) = ec_public_key_len(curve_oid) else {
1996        return Ok(None);
1997    };
1998    Ok(Some((curve_oid.to_string(), public_key_len)))
1999}
2000
2001fn ec_public_key_len(curve_oid: &str) -> Option<usize> {
2002    match curve_oid {
2003        EC_P256_OID => Some(65),
2004        EC_P384_OID => Some(97),
2005        EC_P521_OID => Some(133),
2006        _ => None,
2007    }
2008}
2009
2010fn validate_ec_public_key_point(public_key: &[u8], expected_len: usize) -> Result<(), ParseError> {
2011    if public_key.len() != expected_len {
2012        return Err(ParseError::InvalidStructure(
2013            "ECKeyValue PublicKey length does not match NamedCurve".into(),
2014        ));
2015    }
2016    if public_key.first().copied() != Some(0x04) {
2017        return Err(ParseError::InvalidStructure(
2018            "ECKeyValue PublicKey must be an uncompressed SEC1 point".into(),
2019        ));
2020    }
2021    Ok(())
2022}
2023
2024fn parse_rsa_key_value(node: Node<'_, '_>) -> Result<KeyValueInfo, ParseError> {
2025    verify_ds_element(node, "RSAKeyValue")?;
2026    ensure_no_non_whitespace_text(node, "RSAKeyValue")?;
2027
2028    let mut children = element_children(node);
2029    let modulus_node = children.next().ok_or_else(|| {
2030        ParseError::InvalidStructure("RSAKeyValue requires Modulus and Exponent".into())
2031    })?;
2032    verify_ds_element(modulus_node, "Modulus")?;
2033    ensure_no_element_children(modulus_node, "Modulus")?;
2034
2035    let exponent_node = children.next().ok_or_else(|| {
2036        ParseError::InvalidStructure("RSAKeyValue requires Modulus and Exponent".into())
2037    })?;
2038    verify_ds_element(exponent_node, "Exponent")?;
2039    ensure_no_element_children(exponent_node, "Exponent")?;
2040    if children.next().is_some() {
2041        return Err(ParseError::InvalidStructure(
2042            "RSAKeyValue must contain exactly Modulus followed by Exponent".into(),
2043        ));
2044    }
2045
2046    Ok(KeyValueInfo::Rsa {
2047        modulus: decode_crypto_binary(modulus_node, "Modulus", MAX_RSA_MODULUS_LEN)?,
2048        exponent: decode_crypto_binary(exponent_node, "Exponent", MAX_RSA_EXPONENT_LEN)?,
2049    })
2050}
2051
2052fn decode_crypto_binary(
2053    node: Node<'_, '_>,
2054    element_name: &'static str,
2055    max_decoded_len: usize,
2056) -> Result<Vec<u8>, ParseError> {
2057    let max_base64_len = max_decoded_len.div_ceil(3) * 4;
2058    let payload = super::whitespace::XmlBase64Payload::bounded(node, usize::MAX, max_base64_len)
2059        .map_err(|reason| xml_base64_payload_error(element_name, reason))?;
2060    if payload.normalized_len > max_base64_len {
2061        return Err(ParseError::InvalidStructure(format!(
2062            "{element_name} exceeds maximum allowed base64 length"
2063        )));
2064    }
2065    if payload.decoded_len == 0 {
2066        return Err(ParseError::InvalidStructure(format!(
2067            "{element_name} must not be empty"
2068        )));
2069    }
2070    if payload.decoded_len > max_decoded_len {
2071        return Err(ParseError::InvalidStructure(format!(
2072            "{element_name} exceeds maximum allowed binary length"
2073        )));
2074    }
2075    payload
2076        .decode()
2077        .map_err(|reason| ParseError::Base64(format!("{element_name}: {reason}")))
2078}
2079
2080pub(crate) fn parse_x509_data_dispatch_with_budget_and_provider(
2081    node: Node,
2082    total_binary_len: &mut usize,
2083    embedded_key_candidates: &mut usize,
2084    provider: &dyn crate::provider::CryptoProvider,
2085    resources: &crate::policy::ResourcePolicy,
2086    allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
2087) -> Result<X509DataInfo, ParseError> {
2088    verify_ds_element(node, "X509Data")?;
2089    ensure_no_non_whitespace_text(node, "X509Data")?;
2090
2091    let mut info = X509DataInfo::default();
2092    for child in element_children(node) {
2093        match (child.tag_name().namespace(), child.tag_name().name()) {
2094            (Some(XMLDSIG_NS), "X509Certificate") => {
2095                charge_embedded_key_candidate(embedded_key_candidates, resources)?;
2096                ensure_no_element_children(child, "X509Certificate")?;
2097                ensure_x509_data_entry_budget(&info)?;
2098                let cert =
2099                    decode_x509_base64(child, "X509Certificate", total_binary_len, resources)?;
2100                let parsed_cert = parse_x509_certificate(cert.as_slice())?;
2101                info.parsed_certificates.push(parsed_cert);
2102                info.certificates.push(cert);
2103            }
2104            (Some(XMLDSIG_NS), "X509SubjectName") => {
2105                ensure_no_element_children(child, "X509SubjectName")?;
2106                ensure_x509_data_entry_budget(&info)?;
2107                let subject_name = collect_text_content_bounded(
2108                    child,
2109                    MAX_X509_SUBJECT_NAME_TEXT_LEN,
2110                    "X509SubjectName",
2111                )?;
2112                info.subject_names.push(subject_name);
2113            }
2114            (Some(XMLDSIG_NS), "X509IssuerSerial") => {
2115                ensure_x509_data_entry_budget(&info)?;
2116                let issuer_serial = parse_x509_issuer_serial(child)?;
2117                info.issuer_serials.push(issuer_serial);
2118            }
2119            (Some(XMLDSIG_NS), "X509SKI") => {
2120                ensure_no_element_children(child, "X509SKI")?;
2121                ensure_x509_data_entry_budget(&info)?;
2122                let ski = decode_x509_base64(child, "X509SKI", total_binary_len, resources)?;
2123                info.skis.push(ski);
2124            }
2125            (Some(XMLDSIG_NS), "X509CRL") => {
2126                ensure_no_element_children(child, "X509CRL")?;
2127                ensure_x509_data_entry_budget(&info)?;
2128                let crl = decode_x509_base64(child, "X509CRL", total_binary_len, resources)?;
2129                info.crls.push(crl);
2130            }
2131            (Some(XMLDSIG11_NS), "X509Digest") => {
2132                ensure_no_element_children(child, "X509Digest")?;
2133                ensure_x509_data_entry_budget(&info)?;
2134                let algorithm = required_algorithm_attr(child, "X509Digest")?;
2135                let digest = decode_x509_base64(child, "X509Digest", total_binary_len, resources)?;
2136                info.digests.push((algorithm.to_string(), digest));
2137            }
2138            (Some(XMLDSIG_NS), child_name) | (Some(XMLDSIG11_NS), child_name) => {
2139                return Err(ParseError::InvalidStructure(format!(
2140                    "X509Data contains unsupported XMLDSig child element <{child_name}>"
2141                )));
2142            }
2143            _ => {}
2144        }
2145    }
2146
2147    info.certificate_chain = build_x509_certificate_chain(&info, provider, allowed)?;
2148    Ok(info)
2149}
2150
2151/// Check untrusted selector requests before parsing hashes embedded certificates.
2152/// This borrows the operation's existing policy, without a parallel policy knob.
2153pub(crate) fn validate_x509_digest_policy(
2154    node: Node,
2155    check: &impl Fn(DigestAlgorithm) -> Result<(), crate::policy::PolicyViolation>,
2156) -> Result<(), ParseError> {
2157    fn check_data(
2158        data: Node,
2159        check: &impl Fn(DigestAlgorithm) -> Result<(), crate::policy::PolicyViolation>,
2160    ) -> Result<(), ParseError> {
2161        for child in element_children(data) {
2162            if child.has_tag_name((XMLDSIG11_NS, "X509Digest")) {
2163                let uri = required_algorithm_attr(child, "X509Digest")?;
2164                // Unknown URIs retain the parser/resolver's unsupported-
2165                // algorithm error, not a policy denial; no primitive can run
2166                // for them. Gate every recognized selector before hashing.
2167                if let Some(algorithm) = DigestAlgorithm::from_uri(uri) {
2168                    check(algorithm)?;
2169                }
2170            }
2171        }
2172        Ok(())
2173    }
2174    if node.has_tag_name((XMLDSIG_NS, "X509Data")) {
2175        return check_data(node, check);
2176    }
2177    for child in element_children(node) {
2178        if child.has_tag_name((XMLDSIG_NS, "X509Data")) {
2179            check_data(child, check)?;
2180        }
2181    }
2182    Ok(())
2183}
2184
2185fn build_x509_certificate_chain(
2186    info: &X509DataInfo,
2187    provider: &dyn crate::provider::CryptoProvider,
2188    allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
2189) -> Result<Vec<usize>, ParseError> {
2190    if info.parsed_certificates.is_empty() {
2191        return Ok(Vec::new());
2192    }
2193
2194    let signing_idx = select_x509_signing_certificate(info, provider)?;
2195    build_x509_certificate_chain_from(info, signing_idx, provider, allowed)
2196        .map_err(ParseError::from)
2197}
2198
2199#[derive(Debug, Clone, PartialEq, Eq)]
2200pub(crate) enum X509ChainBuildError {
2201    Policy(crate::policy::PolicyViolation),
2202    InconsistentMetadata,
2203    DepthExceeded,
2204    Cycle,
2205    IssuerSignatureMismatch,
2206    AmbiguousIssuer,
2207    UnsupportedSignatureAlgorithm { oid: String },
2208    Provider(crate::provider::ProviderError),
2209}
2210
2211impl From<X509ChainBuildError> for ParseError {
2212    fn from(error: X509ChainBuildError) -> Self {
2213        let reason = match error {
2214            X509ChainBuildError::InconsistentMetadata => {
2215                "X509Data certificate metadata is inconsistent"
2216            }
2217            X509ChainBuildError::DepthExceeded => {
2218                "X509Data certificate chain exceeds maximum depth"
2219            }
2220            X509ChainBuildError::Cycle => "X509Data certificate chain contains a cycle",
2221            X509ChainBuildError::IssuerSignatureMismatch => {
2222                "X509Data issuer candidates do not verify the certificate signature"
2223            }
2224            X509ChainBuildError::AmbiguousIssuer => {
2225                "X509Data certificate chain contains ambiguous issuer certificates"
2226            }
2227            X509ChainBuildError::UnsupportedSignatureAlgorithm { oid } => {
2228                return Self::InvalidStructure(format!(
2229                    "X509Data certificate chain uses unsupported signature algorithm {oid}"
2230                ));
2231            }
2232            X509ChainBuildError::Provider(error) => return Self::Provider(error),
2233            X509ChainBuildError::Policy(error) => return Self::Policy(error),
2234        };
2235        Self::InvalidStructure(reason.into())
2236    }
2237}
2238
2239/// Order an available certificate pool from a preselected signing certificate.
2240pub(crate) fn build_x509_certificate_chain_from(
2241    info: &X509DataInfo,
2242    signing_idx: usize,
2243    provider: &dyn crate::provider::CryptoProvider,
2244    allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
2245) -> Result<Vec<usize>, X509ChainBuildError> {
2246    if signing_idx >= info.parsed_certificates.len()
2247        || info.parsed_certificates.len() != info.certificates.len()
2248    {
2249        return Err(X509ChainBuildError::InconsistentMetadata);
2250    }
2251    let mut chain = vec![signing_idx];
2252
2253    loop {
2254        let current_idx = *chain
2255            .last()
2256            .expect("chain starts with signing certificate index");
2257        let current = &info.parsed_certificates[current_idx];
2258        if distinguished_names_equal(&current.subject_dn, &current.issuer_dn) {
2259            break;
2260        }
2261
2262        let candidates = info
2263            .parsed_certificates
2264            .iter()
2265            .enumerate()
2266            .filter(|(idx, cert)| {
2267                *idx != current_idx
2268                    && distinguished_names_equal(&cert.subject_dn, &current.issuer_dn)
2269            })
2270            .map(|(idx, _)| idx)
2271            .collect::<Vec<_>>();
2272
2273        let issuer_idx = match candidates.as_slice() {
2274            [] => break,
2275            [issuer_idx] => *issuer_idx,
2276            _ => {
2277                let mut verified = Vec::new();
2278                let mut unsupported_oid = None;
2279                for issuer_idx in candidates {
2280                    match certificate_signature_matches_with_provider(
2281                        &info.certificates[current_idx],
2282                        &info.certificates[issuer_idx],
2283                        provider,
2284                        allowed,
2285                    ) {
2286                        Ok(true) => verified.push(issuer_idx),
2287                        Ok(false) => {}
2288                        Err(super::X509ChainError::Provider(
2289                            crate::provider::ProviderError::Unsupported {
2290                                operation: crate::provider::ProviderOperation::VerifyCertificate,
2291                                algorithm: Some(oid),
2292                            },
2293                        )) => {
2294                            unsupported_oid.get_or_insert(oid);
2295                        }
2296                        Err(super::X509ChainError::Provider(error)) => {
2297                            return Err(X509ChainBuildError::Provider(error));
2298                        }
2299                        Err(super::X509ChainError::UnsupportedSignatureAlgorithm { oid }) => {
2300                            unsupported_oid.get_or_insert(oid);
2301                        }
2302                        Err(super::X509ChainError::Policy(error)) => {
2303                            return Err(X509ChainBuildError::Policy(error));
2304                        }
2305                        Err(_) => return Err(X509ChainBuildError::IssuerSignatureMismatch),
2306                    }
2307                }
2308                match verified.as_slice() {
2309                    [issuer_idx] => *issuer_idx,
2310                    [] => {
2311                        if let Some(oid) = unsupported_oid {
2312                            return Err(X509ChainBuildError::UnsupportedSignatureAlgorithm { oid });
2313                        }
2314                        return Err(X509ChainBuildError::IssuerSignatureMismatch);
2315                    }
2316                    _ => return Err(X509ChainBuildError::AmbiguousIssuer),
2317                }
2318            }
2319        };
2320        if chain.contains(&issuer_idx) {
2321            return Err(X509ChainBuildError::Cycle);
2322        }
2323        if chain.len() == crate::hard_limits::X509_CHAIN_DEPTH_CEILING {
2324            return Err(X509ChainBuildError::DepthExceeded);
2325        }
2326        chain.push(issuer_idx);
2327    }
2328
2329    Ok(chain)
2330}
2331
2332/// Enumerate signature-valid certificate paths that terminate at a certificate
2333/// in the trusted prefix. Trust and certificate policy are intentionally not
2334/// assigned here; callers must fully validate every returned candidate.
2335pub(crate) fn build_x509_certificate_paths_to_trusted_prefix(
2336    info: &X509DataInfo,
2337    signing_idx: usize,
2338    trusted_prefix_len: usize,
2339    max_depth: usize,
2340    max_candidate_paths: usize,
2341    provider: &dyn crate::provider::CryptoProvider,
2342    allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
2343) -> Result<Vec<Vec<usize>>, X509ChainBuildError> {
2344    if trusted_prefix_len > info.certificates.len() {
2345        return Err(X509ChainBuildError::InconsistentMetadata);
2346    }
2347    build_x509_certificate_paths(
2348        info,
2349        signing_idx,
2350        |index| index < trusted_prefix_len,
2351        false,
2352        X509PathBounds {
2353            max_depth,
2354            max_candidate_paths,
2355        },
2356        provider,
2357        allowed,
2358    )
2359}
2360
2361/// Enumerate signature-valid paths that reach any candidate selector target.
2362/// A matching intermediate is retained as a candidate and traversal continues
2363/// so callers can test selector categories against every longer path as well.
2364pub(crate) fn build_x509_certificate_paths_to_selector_targets(
2365    info: &X509DataInfo,
2366    signing_idx: usize,
2367    targets: &[usize],
2368    max_depth: usize,
2369    max_candidate_paths: usize,
2370    provider: &dyn crate::provider::CryptoProvider,
2371    allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
2372) -> Result<Vec<Vec<usize>>, X509ChainBuildError> {
2373    if targets
2374        .iter()
2375        .any(|index| *index >= info.certificates.len())
2376    {
2377        return Err(X509ChainBuildError::InconsistentMetadata);
2378    }
2379    build_x509_certificate_paths(
2380        info,
2381        signing_idx,
2382        |index| targets.contains(&index),
2383        true,
2384        X509PathBounds {
2385            max_depth,
2386            max_candidate_paths,
2387        },
2388        provider,
2389        allowed,
2390    )
2391}
2392
2393struct X509PathBounds {
2394    max_depth: usize,
2395    max_candidate_paths: usize,
2396}
2397
2398fn build_x509_certificate_paths(
2399    info: &X509DataInfo,
2400    signing_idx: usize,
2401    is_terminal: impl Fn(usize) -> bool,
2402    continue_after_terminal: bool,
2403    bounds: X509PathBounds,
2404    provider: &dyn crate::provider::CryptoProvider,
2405    allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
2406) -> Result<Vec<Vec<usize>>, X509ChainBuildError> {
2407    let X509PathBounds {
2408        max_depth,
2409        max_candidate_paths,
2410    } = bounds;
2411    if signing_idx >= info.parsed_certificates.len()
2412        || info.parsed_certificates.len() != info.certificates.len()
2413    {
2414        return Err(X509ChainBuildError::InconsistentMetadata);
2415    }
2416    if max_candidate_paths == 0 {
2417        return Err(X509ChainBuildError::AmbiguousIssuer);
2418    }
2419
2420    let mut pending = vec![vec![signing_idx]];
2421    let mut completed = Vec::new();
2422    let mut generated_paths = 1usize;
2423    let mut depth_exceeded = false;
2424    let mut unsupported_oid = None;
2425    let mut policy_rejection = None;
2426    let mut issuer_cache = vec![None; info.parsed_certificates.len()];
2427    while let Some(path) = pending.pop() {
2428        let current_idx = *path
2429            .last()
2430            .expect("candidate path starts with signing certificate index");
2431        if is_terminal(current_idx) {
2432            completed.push(path.clone());
2433            if !continue_after_terminal {
2434                continue;
2435            }
2436        }
2437        if path.len() == max_depth {
2438            depth_exceeded = true;
2439            continue;
2440        }
2441
2442        let current = &info.parsed_certificates[current_idx];
2443        if issuer_cache[current_idx].is_none() {
2444            let mut verified = Vec::new();
2445            for (issuer_idx, issuer) in info.parsed_certificates.iter().enumerate() {
2446                if !distinguished_names_equal(&issuer.subject_dn, &current.issuer_dn) {
2447                    continue;
2448                }
2449                match certificate_signature_matches_with_provider(
2450                    &info.certificates[current_idx],
2451                    &info.certificates[issuer_idx],
2452                    provider,
2453                    allowed,
2454                ) {
2455                    Ok(true) => verified.push(issuer_idx),
2456                    Ok(false) => {}
2457                    Err(super::X509ChainError::Provider(
2458                        crate::provider::ProviderError::Unsupported {
2459                            operation: crate::provider::ProviderOperation::VerifyCertificate,
2460                            algorithm: Some(oid),
2461                        },
2462                    )) => {
2463                        // Provider capability can depend on the issuer SPKI,
2464                        // so retain the diagnostic but try every same-DN key.
2465                        unsupported_oid.get_or_insert(oid);
2466                        continue;
2467                    }
2468                    Err(super::X509ChainError::Provider(error)) => {
2469                        return Err(X509ChainBuildError::Provider(error));
2470                    }
2471                    Err(super::X509ChainError::UnsupportedSignatureAlgorithm { oid }) => {
2472                        // The mapper rejected this child's AlgorithmIdentifier;
2473                        // no issuer candidate can alter it on the current path.
2474                        unsupported_oid.get_or_insert(oid);
2475                        break;
2476                    }
2477                    Err(super::X509ChainError::Policy(error)) => {
2478                        // Certificate permission belongs to this child's
2479                        // parent edge, not to other cross-certified paths.
2480                        // RFC 5280 section 6.1 validates a prospective path;
2481                        // discovery among alternatives is our product policy.
2482                        // https://www.rfc-editor.org/rfc/rfc5280.html#section-6.1
2483                        policy_rejection.get_or_insert(error);
2484                        break;
2485                    }
2486                    Err(_) => return Err(X509ChainBuildError::IssuerSignatureMismatch),
2487                }
2488            }
2489            issuer_cache[current_idx] = Some(verified);
2490        }
2491        let issuers = issuer_cache[current_idx]
2492            .as_ref()
2493            .expect("issuer cache entry was initialized");
2494        let issuers = issuers
2495            .iter()
2496            .copied()
2497            .filter(|issuer_idx| !path.contains(issuer_idx))
2498            .collect::<Vec<_>>();
2499        if generated_paths.saturating_add(issuers.len()) > max_candidate_paths {
2500            return Err(X509ChainBuildError::AmbiguousIssuer);
2501        }
2502        generated_paths += issuers.len();
2503        for issuer_idx in issuers {
2504            let mut candidate = path.clone();
2505            candidate.push(issuer_idx);
2506            pending.push(candidate);
2507        }
2508    }
2509
2510    if completed.is_empty() {
2511        if let Some(error) = policy_rejection {
2512            return Err(X509ChainBuildError::Policy(error));
2513        }
2514        if let Some(oid) = unsupported_oid {
2515            return Err(X509ChainBuildError::UnsupportedSignatureAlgorithm { oid });
2516        }
2517        if depth_exceeded {
2518            return Err(X509ChainBuildError::DepthExceeded);
2519        }
2520    }
2521    Ok(completed)
2522}
2523
2524fn select_x509_signing_certificate(
2525    info: &X509DataInfo,
2526    provider: &dyn crate::provider::CryptoProvider,
2527) -> Result<usize, ParseError> {
2528    let has_lookup_identifiers = x509_data_has_lookup_identifiers(info);
2529    let mut candidates = Vec::new();
2530    if has_lookup_identifiers {
2531        for (idx, (parsed, der)) in info
2532            .parsed_certificates
2533            .iter()
2534            .zip(&info.certificates)
2535            .enumerate()
2536        {
2537            if x509_certificate_matches_any_selector(info, parsed, der, provider)? {
2538                candidates.push(idx);
2539            }
2540        }
2541        if !x509_selector_categories_match_chain(info, provider)? {
2542            return Err(ParseError::InvalidStructure(
2543                "X509Data lookup identifiers do not match the embedded certificate chain".into(),
2544            ));
2545        }
2546    }
2547
2548    match candidates.as_slice() {
2549        [idx] => return Ok(*idx),
2550        [] if has_lookup_identifiers => {
2551            return Err(ParseError::InvalidStructure(
2552                "X509Data lookup identifiers do not match any embedded certificate".into(),
2553            ));
2554        }
2555        [] => {}
2556        _ => {}
2557    }
2558
2559    let leaf_candidates = info
2560        .parsed_certificates
2561        .iter()
2562        .enumerate()
2563        .filter(|(_, cert)| {
2564            !distinguished_names_equal(&cert.subject_dn, &cert.issuer_dn)
2565                && !info
2566                    .parsed_certificates
2567                    .iter()
2568                    .any(|other| distinguished_names_equal(&other.issuer_dn, &cert.subject_dn))
2569        })
2570        .map(|(idx, _)| idx)
2571        .collect::<Vec<_>>();
2572
2573    let selected_leaves = leaf_candidates
2574        .iter()
2575        .filter(|idx| !has_lookup_identifiers || candidates.contains(idx))
2576        .copied()
2577        .collect::<Vec<_>>();
2578
2579    match selected_leaves.as_slice() {
2580        [idx] => Ok(*idx),
2581        [] if !has_lookup_identifiers => Ok(0),
2582        [] => Err(ParseError::InvalidStructure(
2583            "X509Data lookup identifiers match multiple certificates without a unique signing certificate"
2584                .into(),
2585        )),
2586        _ => Err(ParseError::InvalidStructure(
2587            if has_lookup_identifiers {
2588                "X509Data lookup identifiers match multiple certificates"
2589            } else {
2590                "X509Data contains multiple possible signing certificates"
2591            }
2592            .into(),
2593        )),
2594    }
2595}
2596
2597pub(crate) fn x509_data_has_lookup_identifiers(info: &X509DataInfo) -> bool {
2598    !info.subject_names.is_empty()
2599        || !info.issuer_serials.is_empty()
2600        || !info.skis.is_empty()
2601        || !info.digests.is_empty()
2602}
2603
2604pub(crate) fn x509_certificate_matches_any_selector(
2605    info: &X509DataInfo,
2606    certificate: &ParsedX509Certificate,
2607    certificate_der: &[u8],
2608    provider: &dyn crate::provider::CryptoProvider,
2609) -> Result<bool, ParseError> {
2610    let subject_match = info
2611        .subject_names
2612        .iter()
2613        .any(|subject| distinguished_names_equal(subject, &certificate.subject_dn));
2614    let mut issuer_serial_match = false;
2615    for (issuer, serial) in &info.issuer_serials {
2616        let serial_hex = x509_serial_decimal_to_hex(serial).ok_or_else(|| {
2617            ParseError::InvalidStructure(
2618                "X509Data lookup identifiers contain an invalid serial number".into(),
2619            )
2620        })?;
2621        issuer_serial_match |= distinguished_names_equal(issuer, &certificate.issuer_dn)
2622            && serial_hex == certificate.serial_number_hex;
2623    }
2624    let ski_match = certificate
2625        .subject_key_identifier
2626        .as_ref()
2627        .is_some_and(|certificate_ski| info.skis.iter().any(|ski| ski == certificate_ski));
2628    let mut digest_match = false;
2629    for (algorithm_uri, expected) in &info.digests {
2630        let algorithm = DigestAlgorithm::from_uri(algorithm_uri).ok_or_else(|| {
2631            ParseError::UnsupportedAlgorithm {
2632                uri: algorithm_uri.clone(),
2633            }
2634        })?;
2635        digest_match |= constant_time_eq(
2636            &compute_digest_with_provider(provider, algorithm, certificate_der)?,
2637            expected,
2638        );
2639    }
2640    Ok(subject_match || issuer_serial_match || ski_match || digest_match)
2641}
2642
2643/// Match every selector asserted by `X509Data` against one configured certificate.
2644///
2645/// This supports XMLDSig lookup-only `X509Data`, where the document identifies a
2646/// certificate without embedding it. All selector values are constraints and must
2647/// match the same candidate certificate.
2648pub fn x509_certificate_matches_selectors(
2649    info: &X509DataInfo,
2650    certificate_der: &[u8],
2651    provider: &dyn crate::provider::CryptoProvider,
2652) -> Result<bool, ParseError> {
2653    let mut candidate = info.clone();
2654    candidate.certificates = vec![certificate_der.to_vec()];
2655    candidate.parsed_certificates = vec![parse_x509_certificate(certificate_der)?];
2656    candidate.certificate_chain = vec![0];
2657    x509_selector_categories_match_chain(&candidate, provider)
2658}
2659
2660pub(crate) fn x509_selector_categories_match_chain(
2661    info: &X509DataInfo,
2662    provider: &dyn crate::provider::CryptoProvider,
2663) -> Result<bool, ParseError> {
2664    let subject_match = info.subject_names.iter().all(|subject| {
2665        info.parsed_certificates
2666            .iter()
2667            .any(|certificate| distinguished_names_equal(subject, &certificate.subject_dn))
2668    });
2669
2670    let mut issuer_serial_match = true;
2671    for (issuer, serial) in &info.issuer_serials {
2672        let serial_hex = x509_serial_decimal_to_hex(serial).ok_or_else(|| {
2673            ParseError::InvalidStructure(
2674                "X509Data lookup identifiers contain an invalid serial number".into(),
2675            )
2676        })?;
2677        issuer_serial_match &= info.parsed_certificates.iter().any(|certificate| {
2678            distinguished_names_equal(issuer, &certificate.issuer_dn)
2679                && serial_hex == certificate.serial_number_hex
2680        });
2681    }
2682
2683    let ski_match = info.skis.iter().all(|ski| {
2684        info.parsed_certificates.iter().any(|certificate| {
2685            certificate
2686                .subject_key_identifier
2687                .as_ref()
2688                .is_some_and(|certificate_ski| ski == certificate_ski)
2689        })
2690    });
2691
2692    let mut digest_match = true;
2693    for (algorithm_uri, expected) in &info.digests {
2694        let algorithm = DigestAlgorithm::from_uri(algorithm_uri).ok_or_else(|| {
2695            ParseError::UnsupportedAlgorithm {
2696                uri: algorithm_uri.clone(),
2697            }
2698        })?;
2699        let mut category_match = false;
2700        for certificate in &info.certificates {
2701            category_match |= constant_time_eq(
2702                &compute_digest_with_provider(provider, algorithm, certificate)?,
2703                expected,
2704            );
2705        }
2706        digest_match &= category_match;
2707    }
2708
2709    Ok(subject_match && issuer_serial_match && ski_match && digest_match)
2710}
2711
2712fn x509_attribute_values_equal(
2713    left: &x509_cert::attr::AttributeTypeAndValue,
2714    right: &x509_cert::attr::AttributeTypeAndValue,
2715) -> bool {
2716    if left.oid != right.oid {
2717        return false;
2718    }
2719    const EMAIL_ADDRESS: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.1");
2720    const DOMAIN_COMPONENT: ObjectIdentifier =
2721        ObjectIdentifier::new_unwrap("0.9.2342.19200300.100.1.25");
2722    if left.oid == EMAIL_ADDRESS {
2723        let (Ok(left), Ok(right)) = (
2724            Ia5StringRef::try_from(&left.value),
2725            Ia5StringRef::try_from(&right.value),
2726        ) else {
2727            return false;
2728        };
2729        let (Some((left_local, left_domain)), Some((right_local, right_domain))) = (
2730            left.as_str().rsplit_once('@'),
2731            right.as_str().rsplit_once('@'),
2732        ) else {
2733            return false;
2734        };
2735        return left_local == right_local && left_domain.eq_ignore_ascii_case(right_domain);
2736    }
2737    if left.oid == DOMAIN_COMPONENT {
2738        let (Ok(left), Ok(right)) = (
2739            Ia5StringRef::try_from(&left.value),
2740            Ia5StringRef::try_from(&right.value),
2741        ) else {
2742            return false;
2743        };
2744        return left.as_str().eq_ignore_ascii_case(right.as_str());
2745    }
2746    match (
2747        DirectoryString::try_from(&left.value),
2748        DirectoryString::try_from(&right.value),
2749    ) {
2750        (Ok(left), Ok(right)) => {
2751            // RFC 5280 section 7.1 requires caseIgnoreMatch with LDAP/X.520
2752            // string preparation for PrintableString and UTF8String names.
2753            let Ok(left) =
2754                x520_stringprep::x520_stringprep_to_case_ignore_string(left.value().as_ref())
2755            else {
2756                return false;
2757            };
2758            let Ok(right) =
2759                x520_stringprep::x520_stringprep_to_case_ignore_string(right.value().as_ref())
2760            else {
2761                return false;
2762            };
2763            left.trim_matches(' ') == right.trim_matches(' ')
2764        }
2765        _ => left.value == right.value,
2766    }
2767}
2768
2769fn x509_rdns_equal(
2770    left: &x509_cert::name::RelativeDistinguishedName,
2771    right: &x509_cert::name::RelativeDistinguishedName,
2772) -> bool {
2773    if left.len() != right.len() {
2774        return false;
2775    }
2776    // A DN is an ordered RDN sequence, but each individual RDN is a set.
2777    let right = right.iter().collect::<Vec<_>>();
2778    let mut matched = vec![false; right.len()];
2779    left.iter().all(|left_attribute| {
2780        right
2781            .iter()
2782            .enumerate()
2783            .find(|(index, right_attribute)| {
2784                !matched[*index] && x509_attribute_values_equal(left_attribute, right_attribute)
2785            })
2786            .is_some_and(|(index, _)| {
2787                matched[index] = true;
2788                true
2789            })
2790    })
2791}
2792
2793fn trailing_whitespace_is_escaped(value: &str) -> bool {
2794    let Some((&last, prefix)) = value.as_bytes().split_last() else {
2795        return false;
2796    };
2797    if !matches!(last, b' ' | b'\t' | b'\r' | b'\n') {
2798        return false;
2799    }
2800    prefix
2801        .iter()
2802        .rev()
2803        .take_while(|byte| **byte == b'\\')
2804        .count()
2805        % 2
2806        == 1
2807}
2808
2809fn parse_distinguished_name(value: &str) -> Option<Name> {
2810    let mut normalized = String::with_capacity(value.len());
2811    let mut chars = value
2812        .trim_start_matches([' ', '\t', '\r', '\n'])
2813        .chars()
2814        .peekable();
2815    let mut escaped = false;
2816
2817    while let Some(ch) = chars.next() {
2818        if escaped {
2819            normalized.push(ch);
2820            escaped = false;
2821            continue;
2822        }
2823        if ch == '\\' {
2824            normalized.push(ch);
2825            escaped = true;
2826            continue;
2827        }
2828        if matches!(ch, ',' | '+') {
2829            while normalized
2830                .chars()
2831                .next_back()
2832                .is_some_and(|last| matches!(last, ' ' | '\t' | '\r' | '\n'))
2833                && !trailing_whitespace_is_escaped(&normalized)
2834            {
2835                normalized.pop();
2836            }
2837            normalized.push(ch);
2838            while chars
2839                .next_if(|next| matches!(next, ' ' | '\t' | '\r' | '\n'))
2840                .is_some()
2841            {}
2842            continue;
2843        }
2844        normalized.push(ch);
2845    }
2846
2847    while normalized
2848        .chars()
2849        .next_back()
2850        .is_some_and(|last| matches!(last, ' ' | '\t' | '\r' | '\n'))
2851        && !trailing_whitespace_is_escaped(&normalized)
2852    {
2853        normalized.pop();
2854    }
2855    normalized.parse().ok()
2856}
2857
2858pub(crate) fn distinguished_names_equal(left: &str, right: &str) -> bool {
2859    parse_distinguished_name(left)
2860        .zip(parse_distinguished_name(right))
2861        .is_some_and(|(left, right)| {
2862            left.len() == right.len()
2863                && left
2864                    .iter_rdn()
2865                    .zip(right.iter_rdn())
2866                    .all(|(left, right)| x509_rdns_equal(left, right))
2867        })
2868}
2869
2870pub(crate) fn distinguished_name_within_subtree(name: &str, subtree: &str) -> bool {
2871    parse_distinguished_name(name)
2872        .zip(parse_distinguished_name(subtree))
2873        .is_some_and(|(name, subtree)| {
2874            subtree.len() <= name.len()
2875                && name
2876                    .iter_rdn()
2877                    .zip(subtree.iter_rdn())
2878                    .all(|(name, subtree)| x509_rdns_equal(name, subtree))
2879        })
2880}
2881
2882fn ensure_x509_data_entry_budget(info: &X509DataInfo) -> Result<(), ParseError> {
2883    let total_entries = info.certificates.len()
2884        + info.subject_names.len()
2885        + info.issuer_serials.len()
2886        + info.skis.len()
2887        + info.crls.len()
2888        + info.digests.len();
2889    if total_entries >= MAX_X509_DATA_ENTRY_COUNT {
2890        return Err(ParseError::InvalidStructure(
2891            "X509Data contains too many entries".into(),
2892        ));
2893    }
2894    Ok(())
2895}
2896
2897fn add_x509_data_usage(total_binary_len: &mut usize, delta: usize) -> Result<(), ParseError> {
2898    *total_binary_len = total_binary_len.checked_add(delta).ok_or_else(|| {
2899        ParseError::InvalidStructure("X509Data exceeds maximum allowed total binary length".into())
2900    })?;
2901    if *total_binary_len > MAX_X509_DATA_TOTAL_BINARY_LEN {
2902        return Err(ParseError::InvalidStructure(
2903            "X509Data exceeds maximum allowed total binary length".into(),
2904        ));
2905    }
2906    Ok(())
2907}
2908
2909fn decode_x509_base64(
2910    node: Node<'_, '_>,
2911    element_name: &'static str,
2912    total_binary_len: &mut usize,
2913    resources: &crate::policy::ResourcePolicy,
2914) -> Result<Vec<u8>, ParseError> {
2915    let payload = super::whitespace::XmlBase64Payload::bounded(
2916        node,
2917        MAX_X509_BASE64_TEXT_LEN,
2918        MAX_X509_BASE64_NORMALIZED_LEN,
2919    )
2920    .map_err(|reason| xml_base64_payload_error(element_name, reason))?;
2921    if payload.text_len > MAX_X509_BASE64_TEXT_LEN {
2922        return Err(ParseError::InvalidStructure(format!(
2923            "{element_name} exceeds maximum allowed text length"
2924        )));
2925    }
2926    if payload.normalized_len > MAX_X509_BASE64_NORMALIZED_LEN {
2927        return Err(ParseError::InvalidStructure(format!(
2928            "{element_name} exceeds maximum allowed base64 length"
2929        )));
2930    }
2931    if payload.decoded_len == 0 {
2932        return Err(ParseError::InvalidStructure(format!(
2933            "{element_name} must not be empty"
2934        )));
2935    }
2936    if payload.decoded_len > MAX_X509_DECODED_BINARY_LEN {
2937        return Err(ParseError::InvalidStructure(format!(
2938            "{element_name} exceeds maximum allowed binary length"
2939        )));
2940    }
2941    if payload.decoded_len > resources.max_external_resource_bytes {
2942        return Err(crate::policy::PolicyViolation::ResourceLimitExceeded {
2943            resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
2944            maximum: resources.max_external_resource_bytes,
2945        }
2946        .into());
2947    }
2948    if *total_binary_len > resources.max_external_resource_total_bytes
2949        || payload.decoded_len > resources.max_external_resource_total_bytes - *total_binary_len
2950    {
2951        return Err(crate::policy::PolicyViolation::ResourceLimitExceeded {
2952            resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
2953            maximum: resources.max_external_resource_total_bytes,
2954        }
2955        .into());
2956    }
2957    // Reserve before allocation/DER parsing. Failed candidates consume the
2958    // session budget too; retrying malformed data must not reset spent work.
2959    add_x509_data_usage(total_binary_len, payload.decoded_len)?;
2960    payload
2961        .decode()
2962        .map_err(|reason| ParseError::Base64(format!("{element_name}: {reason}")))
2963}
2964
2965fn xml_base64_payload_error(element: &str, reason: &'static str) -> ParseError {
2966    match reason {
2967        "maximum allowed text length" | "maximum allowed base64 length" => {
2968            ParseError::InvalidStructure(format!("{element} exceeds {reason}"))
2969        }
2970        _ => ParseError::Base64(format!("{element}: {reason}")),
2971    }
2972}
2973
2974pub(crate) fn parse_x509_certificate(cert_der: &[u8]) -> Result<ParsedX509Certificate, ParseError> {
2975    let (rest, cert) =
2976        x509_parser::certificate::X509Certificate::from_der(cert_der).map_err(|err| {
2977            ParseError::InvalidStructure(format!("X509Certificate is not valid DER X.509: {err}"))
2978        })?;
2979    if !rest.is_empty() {
2980        return Err(ParseError::InvalidStructure(
2981            "X509Certificate contains trailing bytes after DER certificate".into(),
2982        ));
2983    }
2984
2985    // x509-parser displays the DER RDN sequence in storage order, while
2986    // XMLDSig names follow RFC 4514 and serialize that sequence in reverse.
2987    // Normalize at the certificate boundary so matching remains ordered and
2988    // cannot confuse a DN with another hierarchy containing reversed RDNs.
2989    let subject_dn = x509_name_to_rfc4514(cert.subject())?;
2990    let issuer_dn = x509_name_to_rfc4514(cert.issuer())?;
2991    let serial_number = cert.tbs_certificate.raw_serial().to_vec();
2992    let serial_number_hex = format_x509_serial_value_hex(&serial_number);
2993
2994    let subject_key_identifier = cert.extensions().iter().find_map(|ext| {
2995        if let ParsedExtension::SubjectKeyIdentifier(ski) = ext.parsed_extension() {
2996            Some(ski.0.to_vec())
2997        } else {
2998            None
2999        }
3000    });
3001
3002    let spki = cert.public_key();
3003    let public_key = match spki.parsed().map_err(|err| {
3004        ParseError::InvalidStructure(format!("X509Certificate public key parse error: {err}"))
3005    })? {
3006        PublicKey::RSA(rsa) => {
3007            let modulus = trim_leading_zeroes(rsa.modulus);
3008            let exponent = trim_leading_zeroes(rsa.exponent);
3009            if modulus.is_empty() || exponent.is_empty() {
3010                return Err(ParseError::InvalidStructure(
3011                    "X509Certificate RSA key contains empty modulus or exponent".into(),
3012                ));
3013            }
3014            X509PublicKeyInfo::Rsa { modulus, exponent }
3015        }
3016        PublicKey::EC(ec_point) => {
3017            let Some(params) = spki.algorithm.parameters.as_ref() else {
3018                return Err(ParseError::InvalidStructure(
3019                    "X509Certificate EC key is missing curve parameters".into(),
3020                ));
3021            };
3022
3023            match params.as_oid() {
3024                Ok(oid) => X509PublicKeyInfo::Ec {
3025                    curve_oid: oid.to_id_string(),
3026                    public_key: ec_point.data().to_vec(),
3027                },
3028                Err(_) => X509PublicKeyInfo::Unsupported {
3029                    algorithm_oid: spki.algorithm.algorithm.to_id_string(),
3030                },
3031            }
3032        }
3033        _ => X509PublicKeyInfo::Unsupported {
3034            algorithm_oid: spki.algorithm.algorithm.to_id_string(),
3035        },
3036    };
3037
3038    Ok(ParsedX509Certificate {
3039        subject_dn,
3040        issuer_dn,
3041        serial_number,
3042        serial_number_hex,
3043        subject_key_identifier,
3044        public_key,
3045    })
3046}
3047
3048pub(crate) fn x509_name_to_rfc4514(name: &X509Name<'_>) -> Result<String, ParseError> {
3049    let name = Name::from_der(name.as_raw()).map_err(|error| {
3050        ParseError::InvalidStructure(format!(
3051            "X509Certificate distinguished name is invalid DER: {error}"
3052        ))
3053    })?;
3054    Ok(name.to_string())
3055}
3056
3057fn format_x509_serial_hex(serial: &[u8]) -> String {
3058    serial
3059        .iter()
3060        .map(|byte| format!("{byte:02X}"))
3061        .collect::<String>()
3062}
3063
3064fn format_x509_serial_value_hex(serial: &[u8]) -> String {
3065    let first_non_zero = serial
3066        .iter()
3067        .position(|byte| *byte != 0)
3068        .unwrap_or(serial.len());
3069    let canonical = if first_non_zero == serial.len() {
3070        &[0]
3071    } else {
3072        &serial[first_non_zero..]
3073    };
3074    format_x509_serial_hex(canonical)
3075}
3076
3077fn x509_serial_decimal_to_hex(serial: &str) -> Option<String> {
3078    let serial = serial.trim();
3079    let serial = serial.strip_prefix('+').unwrap_or(serial);
3080    let serial = serial.trim_start_matches('0');
3081    let serial = if serial.is_empty() { "0" } else { serial };
3082    if serial.len() > MAX_X509_SERIAL_NUMBER_VALUE_DIGITS
3083        || !serial.bytes().all(|byte| byte.is_ascii_digit())
3084    {
3085        return None;
3086    }
3087
3088    let mut bytes = [0_u8; MAX_X509_SERIAL_NUMBER_BYTES];
3089    for digit in serial.bytes().map(|byte| byte - b'0') {
3090        let mut carry = u16::from(digit);
3091        for byte in bytes.iter_mut().rev() {
3092            let value = u16::from(*byte) * 10 + carry;
3093            *byte = value as u8;
3094            carry = value >> 8;
3095        }
3096        if carry != 0 {
3097            return None;
3098        }
3099    }
3100
3101    if bytes.iter().all(|byte| *byte == 0) {
3102        return None;
3103    }
3104
3105    Some(format_x509_serial_value_hex(&bytes))
3106}
3107
3108fn trim_leading_zeroes(bytes: &[u8]) -> Vec<u8> {
3109    let first_non_zero = bytes
3110        .iter()
3111        .position(|byte| *byte != 0)
3112        .unwrap_or(bytes.len());
3113    bytes[first_non_zero..].to_vec()
3114}
3115
3116fn parse_x509_issuer_serial(node: Node<'_, '_>) -> Result<(String, String), ParseError> {
3117    verify_ds_element(node, "X509IssuerSerial")?;
3118    ensure_no_non_whitespace_text(node, "X509IssuerSerial")?;
3119
3120    let children = element_children(node).collect::<Vec<_>>();
3121    if children.len() != 2 {
3122        return Err(ParseError::InvalidStructure(
3123            "X509IssuerSerial must contain exactly X509IssuerName then X509SerialNumber".into(),
3124        ));
3125    }
3126    if !matches!(
3127        (
3128            children[0].tag_name().namespace(),
3129            children[0].tag_name().name()
3130        ),
3131        (Some(XMLDSIG_NS), "X509IssuerName")
3132    ) {
3133        return Err(ParseError::InvalidStructure(
3134            "X509IssuerSerial must contain X509IssuerName as the first child element".into(),
3135        ));
3136    }
3137    if !matches!(
3138        (
3139            children[1].tag_name().namespace(),
3140            children[1].tag_name().name()
3141        ),
3142        (Some(XMLDSIG_NS), "X509SerialNumber")
3143    ) {
3144        return Err(ParseError::InvalidStructure(
3145            "X509IssuerSerial must contain X509SerialNumber as the second child element".into(),
3146        ));
3147    }
3148
3149    let issuer_node = children[0];
3150    ensure_no_element_children(issuer_node, "X509IssuerName")?;
3151    let issuer_name =
3152        collect_text_content_bounded(issuer_node, MAX_X509_ISSUER_NAME_TEXT_LEN, "X509IssuerName")?;
3153
3154    let serial_node = children[1];
3155    ensure_no_element_children(serial_node, "X509SerialNumber")?;
3156    let serial_number = collect_x509_serial_number(serial_node)?;
3157    if issuer_name.trim().is_empty() {
3158        return Err(ParseError::InvalidStructure(
3159            "X509IssuerSerial requires non-empty X509IssuerName and X509SerialNumber".into(),
3160        ));
3161    }
3162
3163    Ok((issuer_name, serial_number))
3164}
3165
3166/// Base64-decode a digest value string, stripping whitespace.
3167///
3168/// XMLDSig allows whitespace within base64 content (line-wrapped encodings).
3169fn base64_decode_digest(b64: &str, digest_method: DigestAlgorithm) -> Result<Vec<u8>, ParseError> {
3170    use base64::Engine;
3171    use base64::engine::general_purpose::STANDARD;
3172
3173    let expected = digest_method.output_len();
3174    let max_base64_len = expected.div_ceil(3) * 4;
3175    let mut cleaned = String::with_capacity(b64.len().min(max_base64_len));
3176    normalize_xml_base64_text(b64, &mut cleaned).map_err(|err| {
3177        ParseError::Base64(format!(
3178            "invalid XML whitespace U+{:04X} in DigestValue",
3179            err.invalid_byte
3180        ))
3181    })?;
3182    if cleaned.len() > max_base64_len {
3183        return Err(ParseError::Base64(
3184            "DigestValue exceeds maximum allowed base64 length".into(),
3185        ));
3186    }
3187    let digest = STANDARD
3188        .decode(&cleaned)
3189        .map_err(|e| ParseError::Base64(e.to_string()))?;
3190    let actual = digest.len();
3191    if actual != expected {
3192        return Err(ParseError::DigestLengthMismatch {
3193            algorithm: digest_method.uri(),
3194            expected,
3195            actual,
3196        });
3197    }
3198    Ok(digest)
3199}
3200
3201fn decode_digest_value_children(
3202    digest_value_node: Node<'_, '_>,
3203    digest_method: DigestAlgorithm,
3204) -> Result<Vec<u8>, ParseError> {
3205    let max_base64_len = digest_method.output_len().div_ceil(3) * 4;
3206    let mut cleaned = String::with_capacity(max_base64_len);
3207
3208    for child in digest_value_node.children() {
3209        if child.is_element() {
3210            return Err(ParseError::InvalidStructure(
3211                "DigestValue must not contain element children".into(),
3212            ));
3213        }
3214        if let Some(text) = child.text() {
3215            normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
3216                ParseError::Base64(format!(
3217                    "invalid XML whitespace U+{:04X} in DigestValue",
3218                    err.invalid_byte
3219                ))
3220            })?;
3221            if cleaned.len() > max_base64_len {
3222                return Err(ParseError::Base64(
3223                    "DigestValue exceeds maximum allowed base64 length".into(),
3224                ));
3225            }
3226        }
3227    }
3228
3229    base64_decode_digest(&cleaned, digest_method)
3230}
3231
3232fn decode_der_encoded_key_value_base64(node: Node<'_, '_>) -> Result<Vec<u8>, ParseError> {
3233    use base64::Engine;
3234    use base64::engine::general_purpose::STANDARD;
3235
3236    let mut cleaned = String::new();
3237    let mut raw_text_len = 0usize;
3238    for text in node
3239        .children()
3240        .filter(|child| child.is_text())
3241        .filter_map(|child| child.text())
3242    {
3243        if raw_text_len.saturating_add(text.len()) > MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN {
3244            return Err(ParseError::InvalidStructure(
3245                "DEREncodedKeyValue exceeds maximum allowed text length".into(),
3246            ));
3247        }
3248        raw_text_len = raw_text_len.saturating_add(text.len());
3249        normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
3250            ParseError::Base64(format!(
3251                "invalid XML whitespace U+{:04X} in base64 text",
3252                err.invalid_byte
3253            ))
3254        })?;
3255        if cleaned.len() > MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN {
3256            return Err(ParseError::InvalidStructure(
3257                "DEREncodedKeyValue exceeds maximum allowed length".into(),
3258            ));
3259        }
3260    }
3261
3262    let der = STANDARD
3263        .decode(&cleaned)
3264        .map_err(|e| ParseError::Base64(e.to_string()))?;
3265    if der.is_empty() {
3266        return Err(ParseError::InvalidStructure(
3267            "DEREncodedKeyValue must not be empty".into(),
3268        ));
3269    }
3270    if der.len() > MAX_DER_ENCODED_KEY_VALUE_LEN {
3271        return Err(ParseError::InvalidStructure(
3272            "DEREncodedKeyValue exceeds maximum allowed length".into(),
3273        ));
3274    }
3275    Ok(der)
3276}
3277
3278fn collect_text_content_bounded(
3279    node: Node<'_, '_>,
3280    max_len: usize,
3281    element_name: &'static str,
3282) -> Result<String, ParseError> {
3283    let mut text = String::new();
3284    for chunk in node
3285        .children()
3286        .filter_map(|child| child.is_text().then(|| child.text()).flatten())
3287    {
3288        if text.len().saturating_add(chunk.len()) > max_len {
3289            return Err(ParseError::InvalidStructure(format!(
3290                "{element_name} exceeds maximum allowed text length"
3291            )));
3292        }
3293        text.push_str(chunk);
3294    }
3295    Ok(text)
3296}
3297
3298fn collect_x509_serial_number(node: Node<'_, '_>) -> Result<String, ParseError> {
3299    let mut serial = String::with_capacity(MAX_X509_SERIAL_NUMBER_VALUE_DIGITS);
3300    let mut raw_text_len = 0usize;
3301    let mut trailing_whitespace = false;
3302    let mut explicit_positive = false;
3303    let mut saw_digit = false;
3304
3305    for chunk in node
3306        .children()
3307        .filter_map(|child| child.is_text().then(|| child.text()).flatten())
3308    {
3309        raw_text_len = raw_text_len.saturating_add(chunk.len());
3310        if raw_text_len > MAX_X509_SERIAL_NUMBER_RAW_TEXT_LEN {
3311            return Err(ParseError::InvalidStructure(
3312                "X509SerialNumber exceeds maximum allowed text length".into(),
3313            ));
3314        }
3315        for byte in chunk.bytes() {
3316            if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
3317                trailing_whitespace |= explicit_positive || saw_digit;
3318                continue;
3319            }
3320            if byte == b'+' && !saw_digit && !explicit_positive && !trailing_whitespace {
3321                explicit_positive = true;
3322                continue;
3323            }
3324            if trailing_whitespace || !byte.is_ascii_digit() {
3325                return Err(ParseError::InvalidStructure(
3326                    "invalid X509SerialNumber decimal value".into(),
3327                ));
3328            }
3329            saw_digit = true;
3330            if byte == b'0' && serial.is_empty() {
3331                continue;
3332            }
3333            if serial.len() == MAX_X509_SERIAL_NUMBER_VALUE_DIGITS {
3334                return Err(ParseError::InvalidStructure(
3335                    "X509SerialNumber exceeds maximum allowed decimal value".into(),
3336                ));
3337            }
3338            serial.push(char::from(byte));
3339        }
3340    }
3341
3342    if !saw_digit {
3343        return Err(ParseError::InvalidStructure(
3344            "X509IssuerSerial requires non-empty X509IssuerName and X509SerialNumber".into(),
3345        ));
3346    }
3347    if serial.is_empty() {
3348        serial.push('0');
3349    }
3350    if x509_serial_decimal_to_hex(&serial).is_none() {
3351        return Err(ParseError::InvalidStructure(
3352            "invalid X509SerialNumber decimal value or RFC 5280 range".into(),
3353        ));
3354    }
3355
3356    Ok(serial)
3357}
3358
3359fn ensure_no_element_children(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
3360    if node.children().any(|child| child.is_element()) {
3361        return Err(ParseError::InvalidStructure(format!(
3362            "{element_name} must not contain child elements"
3363        )));
3364    }
3365    Ok(())
3366}
3367
3368fn ensure_no_non_whitespace_text(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
3369    for child in node.children().filter(|child| child.is_text()) {
3370        if let Some(text) = child.text()
3371            && !is_xml_whitespace_only(text)
3372        {
3373            return Err(ParseError::InvalidStructure(format!(
3374                "{element_name} must not contain non-whitespace mixed content"
3375            )));
3376        }
3377    }
3378    Ok(())
3379}
3380
3381#[cfg(test)]
3382#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
3383mod tests {
3384    use super::*;
3385    use crate::xmldsig::TransformError;
3386    use base64::Engine;
3387
3388    fn fixture_rsa_cert_base64() -> String {
3389        fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem")
3390    }
3391
3392    fn fixture_cert_base64(path: &str) -> String {
3393        match path {
3394            "../../tests/fixtures/keys/rsa/rsa-2048-cert.pem" => {
3395                include_str!("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem")
3396            }
3397            "../../tests/fixtures/keys/rsa/rsa-4096-cert.pem" => {
3398                include_str!("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem")
3399            }
3400            "../../tests/fixtures/keys/ca2cert.pem" => {
3401                include_str!("../../tests/fixtures/keys/ca2cert.pem")
3402            }
3403            "../../tests/fixtures/keys/cacert.pem" => {
3404                include_str!("../../tests/fixtures/keys/cacert.pem")
3405            }
3406            _ => unreachable!("unknown certificate fixture"),
3407        }
3408        .lines()
3409        .skip_while(|line| *line != "-----BEGIN CERTIFICATE-----")
3410        .skip(1)
3411        .take_while(|line| *line != "-----END CERTIFICATE-----")
3412        .collect::<String>()
3413    }
3414
3415    // ── SignatureAlgorithm ───────────────────────────────────────────
3416
3417    #[test]
3418    fn signature_algorithm_from_uri_rsa_sha256() {
3419        assert_eq!(
3420            SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"),
3421            Some(SignatureAlgorithm::RsaSha256)
3422        );
3423    }
3424
3425    #[test]
3426    fn signature_algorithm_from_uri_rsa_sha1() {
3427        assert_eq!(
3428            SignatureAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
3429            Some(SignatureAlgorithm::RsaSha1)
3430        );
3431    }
3432
3433    #[test]
3434    fn signature_algorithm_from_uri_ecdsa_sha256() {
3435        assert_eq!(
3436            SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"),
3437            Some(SignatureAlgorithm::EcdsaSha256)
3438        );
3439    }
3440
3441    #[test]
3442    fn signature_algorithm_from_uri_unknown() {
3443        assert_eq!(
3444            SignatureAlgorithm::from_uri("http://example.com/unknown"),
3445            None
3446        );
3447    }
3448
3449    #[test]
3450    fn signature_algorithm_uri_round_trip() {
3451        for algo in SignatureAlgorithm::ALL {
3452            assert_eq!(
3453                SignatureAlgorithm::from_uri(algo.uri()),
3454                Some(algo),
3455                "round-trip failed for {algo:?}"
3456            );
3457        }
3458    }
3459
3460    #[test]
3461    fn legacy_algorithms_are_verify_only() {
3462        assert!(!SignatureAlgorithm::DsaSha1.signing_allowed());
3463        assert!(!SignatureAlgorithm::HmacSha1.signing_allowed());
3464        assert!(!SignatureAlgorithm::RsaSha1.signing_allowed());
3465        assert!(SignatureAlgorithm::RsaSha256.signing_allowed());
3466        assert!(SignatureAlgorithm::EcdsaSha256.signing_allowed());
3467    }
3468
3469    // ── find_signature_node ──────────────────────────────────────────
3470
3471    #[test]
3472    fn find_signature_in_saml() {
3473        let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
3474            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3475                <ds:SignedInfo/>
3476            </ds:Signature>
3477        </samlp:Response>"#;
3478        let doc = Document::parse(xml).unwrap();
3479        let sig = find_signature_node(&doc);
3480        assert!(sig.is_some());
3481        assert_eq!(sig.unwrap().tag_name().name(), "Signature");
3482    }
3483
3484    #[test]
3485    fn find_signature_missing() {
3486        let xml = "<root><child/></root>";
3487        let doc = Document::parse(xml).unwrap();
3488        assert!(find_signature_node(&doc).is_none());
3489    }
3490
3491    #[test]
3492    fn find_signature_ignores_wrong_namespace() {
3493        let xml = r#"<root><Signature xmlns="http://example.com/fake"/></root>"#;
3494        let doc = Document::parse(xml).unwrap();
3495        assert!(find_signature_node(&doc).is_none());
3496    }
3497
3498    // ── parse_key_info: dispatch parsing ──────────────────────────────
3499
3500    #[test]
3501    fn key_info_session_applies_x509_byte_limits_before_decode() {
3502        // SKI is binary X.509 metadata too; repeated parses share one allowance.
3503        let document = Document::parse("<KeyInfo xmlns='http://www.w3.org/2000/09/xmldsig#'><X509Data><X509SKI>AQID</X509SKI></X509Data></KeyInfo>").unwrap();
3504        for individual in [true, false] {
3505            let resources = crate::policy::ResourcePolicy {
3506                max_external_resource_bytes: if individual { 2 } else { 3 },
3507                max_external_resource_total_bytes: if individual { 100 } else { 5 },
3508                ..Default::default()
3509            };
3510            let mut session = KeyInfoParsingSession::new(&resources).unwrap();
3511            if !individual {
3512                session.parse(document.root_element()).unwrap();
3513            }
3514            assert!(matches!(
3515                session.parse(document.root_element()),
3516                Err(ParseError::Policy(_))
3517            ));
3518        }
3519    }
3520
3521    #[test]
3522    fn all_x509_binary_fields_preflight_active_byte_policy() {
3523        // Even invalid certificate DER must be rejected by the smaller active
3524        // byte limit before certificate parsing, just like SKI/CRL/digest data.
3525        for element in ["X509Certificate", "X509SKI", "X509CRL", "X509Digest"] {
3526            let namespace = if element == "X509Digest" {
3527                XMLDSIG11_NS
3528            } else {
3529                XMLDSIG_NS
3530            };
3531            let xml = format!(
3532                "<KeyInfo xmlns='{XMLDSIG_NS}'><X509Data><v:{element} xmlns:v='{namespace}' Algorithm='test'>AQID</v:{element}></X509Data></KeyInfo>"
3533            );
3534            let document = Document::parse(&xml).unwrap();
3535            let resources = crate::policy::ResourcePolicy {
3536                max_external_resource_bytes: 2,
3537                ..Default::default()
3538            };
3539            let mut session = KeyInfoParsingSession::new(&resources).unwrap();
3540            assert!(
3541                matches!(
3542                    session.parse(document.root_element()),
3543                    Err(ParseError::Policy(_))
3544                ),
3545                "{element}"
3546            );
3547        }
3548    }
3549
3550    #[test]
3551    fn failed_x509_der_parse_keeps_session_byte_charge() {
3552        // A failed certificate attempt has already decoded three bytes. A
3553        // subsequent valid SKI cannot reuse those bytes' aggregate allowance.
3554        let invalid = Document::parse("<KeyInfo xmlns='http://www.w3.org/2000/09/xmldsig#'><X509Data><X509Certificate>AQID</X509Certificate></X509Data></KeyInfo>").unwrap();
3555        let valid = Document::parse("<KeyInfo xmlns='http://www.w3.org/2000/09/xmldsig#'><X509Data><X509SKI>AQID</X509SKI></X509Data></KeyInfo>").unwrap();
3556        let resources = crate::policy::ResourcePolicy {
3557            max_external_resource_bytes: 3,
3558            max_external_resource_total_bytes: 5,
3559            ..Default::default()
3560        };
3561        let mut session = KeyInfoParsingSession::new(&resources).unwrap();
3562        assert!(matches!(
3563            session.parse(invalid.root_element()),
3564            Err(ParseError::InvalidStructure(_))
3565        ));
3566        assert!(matches!(
3567            session.parse(valid.root_element()),
3568            Err(ParseError::Policy(_))
3569        ));
3570        assert!(
3571            KeyInfoParsingSession::new(&resources)
3572                .unwrap()
3573                .parse(valid.root_element())
3574                .is_ok()
3575        );
3576    }
3577
3578    #[test]
3579    fn key_info_session_retains_failed_work_and_applies_active_policy() {
3580        // A failed KeyValue has already consumed inspection work; a second
3581        // parse cannot recover that allowance or use the standalone default.
3582        let resources = crate::policy::ResourcePolicy {
3583            max_key_candidates: 1,
3584            ..Default::default()
3585        };
3586        let xml = "<KeyInfo xmlns='http://www.w3.org/2000/09/xmldsig#'><KeyValue/></KeyInfo>";
3587        let document = Document::parse(xml).unwrap();
3588        let mut session = KeyInfoParsingSession::new(&resources).unwrap();
3589        assert!(matches!(
3590            session.parse(document.root_element()),
3591            Err(ParseError::InvalidStructure(_))
3592        ));
3593        assert!(matches!(
3594            session.parse(document.root_element()),
3595            Err(ParseError::Policy(_))
3596        ));
3597        let zero = crate::policy::ResourcePolicy {
3598            max_key_candidates: 0,
3599            ..resources
3600        };
3601        assert!(matches!(
3602            KeyInfoParsingSession::new(&zero)
3603                .unwrap()
3604                .parse(document.root_element()),
3605            Err(ParseError::Policy(_))
3606        ));
3607    }
3608
3609    #[test]
3610    fn key_info_candidate_budget_precedes_key_value_parsing() {
3611        // A denied embedded candidate must fail before malformed key material
3612        // reaches the algorithm-specific parser.
3613        let document = Document::parse(
3614            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3615                <KeyValue><RSAKeyValue><Modulus>AQAB</Modulus></RSAKeyValue></KeyValue>
3616            </KeyInfo>"#,
3617        )
3618        .expect("fixed KeyInfo fixture must parse as XML");
3619        let resources = crate::policy::ResourcePolicy {
3620            max_key_candidates: 0,
3621            ..crate::policy::ResourcePolicy::default()
3622        };
3623
3624        let error = parse_key_info_with_policy_budgets(
3625            document.root_element(),
3626            crate::provider::default_provider(),
3627            &XmlBaseResolutionBudget::default(),
3628            &resources,
3629            None,
3630        )
3631        .expect_err("candidate policy must reject KeyValue before RSA parsing");
3632
3633        assert!(matches!(
3634            error,
3635            ParseError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3636                resource: crate::policy::resource_name::KEY_CANDIDATES,
3637                maximum: 0,
3638                actual: 1,
3639            })
3640        ));
3641    }
3642
3643    #[test]
3644    fn key_info_candidate_budget_precedes_der_key_decoding() {
3645        // Candidate accounting must reject DEREncodedKeyValue before retaining
3646        // or base64-decoding its attacker-controlled text.
3647        let document = Document::parse(
3648            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3649                       xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3650                <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
3651            </KeyInfo>"#,
3652        )
3653        .expect("fixed KeyInfo fixture must parse as XML");
3654        let resources = crate::policy::ResourcePolicy {
3655            max_key_candidates: 0,
3656            ..crate::policy::ResourcePolicy::default()
3657        };
3658
3659        let error = parse_key_info_with_policy_budgets(
3660            document.root_element(),
3661            crate::provider::default_provider(),
3662            &XmlBaseResolutionBudget::default(),
3663            &resources,
3664            None,
3665        )
3666        .expect_err("candidate policy must reject DEREncodedKeyValue before decoding");
3667
3668        assert!(matches!(
3669            error,
3670            ParseError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3671                resource: crate::policy::resource_name::KEY_CANDIDATES,
3672                maximum: 0,
3673                actual: 1,
3674            })
3675        ));
3676    }
3677
3678    #[test]
3679    fn key_info_embedded_candidate_count_matches_materialized_key_kinds() {
3680        // Only materialized cryptographic candidates belong to the parser
3681        // preflight; names and unresolved RetrievalMethods become work later.
3682        let key_info = KeyInfo {
3683            sources: vec![
3684                KeyInfoSource::KeyName("configured-key".into()),
3685                KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
3686                    namespace: Some(XMLDSIG_NS.into()),
3687                    local_name: "FutureKeyValue".into(),
3688                }),
3689                KeyInfoSource::DerEncodedKeyValue(vec![1]),
3690                KeyInfoSource::X509Data(X509DataInfo {
3691                    certificates: vec![vec![2], vec![3]],
3692                    ..X509DataInfo::default()
3693                }),
3694                KeyInfoSource::RetrievalMethod {
3695                    uri: "urn:certificate".into(),
3696                    resource_type: None,
3697                    transforms: RetrievalMethodTransforms::None,
3698                },
3699            ],
3700        };
3701
3702        assert_eq!(key_info.embedded_candidate_count(), 4);
3703    }
3704
3705    #[test]
3706    fn parse_key_info_dispatches_supported_children() {
3707        let cert_base64 = fixture_rsa_cert_base64();
3708        let expected_cert = base64::engine::general_purpose::STANDARD
3709            .decode(&cert_base64)
3710            .expect("fixture PEM must contain valid base64");
3711        let cert_digest = base64::engine::general_purpose::STANDARD
3712            .encode(compute_digest(DigestAlgorithm::Sha256, &expected_cert));
3713        let xml = format!(
3714            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3715                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3716            <KeyName>idp-signing-key</KeyName>
3717            <KeyValue>
3718                <RSAKeyValue>
3719                    <Modulus>AQAB</Modulus>
3720                    <Exponent>AQAB</Exponent>
3721                </RSAKeyValue>
3722            </KeyValue>
3723            <X509Data>
3724                <X509Certificate>{cert_base64}</X509Certificate>
3725                <X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
3726                <X509IssuerSerial>
3727                    <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>
3728                    <X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
3729                </X509IssuerSerial>
3730                <X509SKI>bcOXN/nsVl8GatRbcKrPbzIbw0Y=</X509SKI>
3731                <X509CRL>BAUGBw==</X509CRL>
3732                <dsig11:X509Digest Algorithm="http://www.w3.org/2001/04/xmlenc#sha256">{cert_digest}</dsig11:X509Digest>
3733            </X509Data>
3734            <dsig11:DEREncodedKeyValue>AQIDBA==</dsig11:DEREncodedKeyValue>
3735        </KeyInfo>"#
3736        );
3737        let doc = Document::parse(&xml).unwrap();
3738
3739        let key_info = parse_key_info(doc.root_element()).unwrap();
3740        assert_eq!(key_info.sources.len(), 4);
3741
3742        assert_eq!(
3743            key_info.sources[0],
3744            KeyInfoSource::KeyName("idp-signing-key".to_string())
3745        );
3746        assert_eq!(
3747            key_info.sources[1],
3748            KeyInfoSource::KeyValue(KeyValueInfo::Rsa {
3749                modulus: vec![1, 0, 1],
3750                exponent: vec![1, 0, 1],
3751            })
3752        );
3753        let x509_info = match &key_info.sources[2] {
3754            KeyInfoSource::X509Data(x509) => x509,
3755            other => panic!("expected X509Data source, got {other:?}"),
3756        };
3757        assert_eq!(x509_info.certificates, vec![expected_cert]);
3758        assert_eq!(
3759            x509_info.subject_names,
3760            vec![
3761                "CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US"
3762                    .to_string()
3763            ]
3764        );
3765        assert_eq!(
3766            x509_info.issuer_serials,
3767            vec![(
3768                "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(),
3769                "680572598617295163017172295025714171905498632019".to_string()
3770            )]
3771        );
3772        assert_eq!(
3773            x509_info.skis,
3774            vec![vec![
3775                109, 195, 151, 55, 249, 236, 86, 95, 6, 106, 212, 91, 112, 170, 207, 111, 50, 27,
3776                195, 70
3777            ]]
3778        );
3779        assert_eq!(x509_info.crls, vec![vec![4, 5, 6, 7]]);
3780        assert_eq!(
3781            x509_info.digests,
3782            vec![(
3783                "http://www.w3.org/2001/04/xmlenc#sha256".to_string(),
3784                compute_digest(DigestAlgorithm::Sha256, &x509_info.certificates[0])
3785            )]
3786        );
3787        assert_eq!(x509_info.parsed_certificates.len(), 1);
3788        assert_eq!(x509_info.certificate_chain, vec![0]);
3789        let parsed_cert = &x509_info.parsed_certificates[0];
3790        assert!(!parsed_cert.subject_dn.is_empty());
3791        assert!(!parsed_cert.issuer_dn.is_empty());
3792        assert_eq!(
3793            parsed_cert.serial_number_hex,
3794            "7735EE487F6862DAF1B3956D961CCB0FA6F34F53"
3795        );
3796        assert!(parsed_cert.subject_key_identifier.is_some());
3797        assert!(matches!(
3798            parsed_cert.public_key,
3799            X509PublicKeyInfo::Rsa { .. }
3800        ));
3801
3802        assert_eq!(
3803            key_info.sources[3],
3804            KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])
3805        );
3806    }
3807
3808    #[test]
3809    fn parse_rsa_key_value_preserves_wrapped_crypto_binary() {
3810        // CryptoBinary is unsigned big-endian data and XML whitespace is insignificant.
3811        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3812            <KeyValue><RSAKeyValue>
3813                <Modulus> AQID
3814BA== </Modulus>
3815                <Exponent> AQAB </Exponent>
3816            </RSAKeyValue></KeyValue>
3817        </KeyInfo>"##;
3818        let doc = Document::parse(xml).unwrap();
3819
3820        assert_eq!(
3821            parse_key_info(doc.root_element()).unwrap().sources,
3822            vec![KeyInfoSource::KeyValue(KeyValueInfo::Rsa {
3823                modulus: vec![1, 2, 3, 4],
3824                exponent: vec![1, 0, 1],
3825            })]
3826        );
3827    }
3828
3829    #[test]
3830    fn parse_rsa_key_value_rejects_reordered_parameters() {
3831        // XMLDSig defines Modulus followed by Exponent; accepting reordered input is ambiguous.
3832        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3833            <KeyValue><RSAKeyValue>
3834                <Exponent>AQAB</Exponent><Modulus>AQID</Modulus>
3835            </RSAKeyValue></KeyValue>
3836        </KeyInfo>"##;
3837        let doc = Document::parse(xml).unwrap();
3838
3839        assert!(matches!(
3840            parse_key_info(doc.root_element()),
3841            Err(ParseError::InvalidStructure(_))
3842        ));
3843    }
3844
3845    #[test]
3846    fn parse_rsa_key_value_rejects_missing_exponent() {
3847        // Both RSA public parameters are required to construct a usable key.
3848        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3849            <KeyValue><RSAKeyValue><Modulus>AQID</Modulus></RSAKeyValue></KeyValue>
3850        </KeyInfo>"##;
3851        let doc = Document::parse(xml).unwrap();
3852
3853        assert!(matches!(
3854            parse_key_info(doc.root_element()),
3855            Err(ParseError::InvalidStructure(_))
3856        ));
3857    }
3858
3859    #[test]
3860    fn parse_rsa_key_value_rejects_duplicate_exponent() {
3861        // RSAKeyValue has a closed two-child schema; duplicate parameters are invalid.
3862        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3863            <KeyValue><RSAKeyValue>
3864                <Modulus>AQID</Modulus><Exponent>AQAB</Exponent><Exponent>AQAB</Exponent>
3865            </RSAKeyValue></KeyValue>
3866        </KeyInfo>"##;
3867        let doc = Document::parse(xml).unwrap();
3868
3869        assert!(matches!(
3870            parse_key_info(doc.root_element()),
3871            Err(ParseError::InvalidStructure(_))
3872        ));
3873    }
3874
3875    #[test]
3876    fn parse_rsa_key_value_rejects_wrong_parameter_namespace() {
3877        // Local names from an extension namespace must not be treated as XMLDSig parameters.
3878        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:bad="urn:bad">
3879            <KeyValue><RSAKeyValue>
3880                <bad:Modulus>AQID</bad:Modulus><Exponent>AQAB</Exponent>
3881            </RSAKeyValue></KeyValue>
3882        </KeyInfo>"#;
3883        let doc = Document::parse(xml).unwrap();
3884
3885        assert!(matches!(
3886            parse_key_info(doc.root_element()),
3887            Err(ParseError::InvalidStructure(_))
3888        ));
3889    }
3890
3891    #[test]
3892    fn parse_rsa_key_value_rejects_nested_crypto_binary() {
3893        // CryptoBinary values are text-only and must not hide extension elements.
3894        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3895            <KeyValue><RSAKeyValue>
3896                <Modulus><chunk>AQID</chunk></Modulus><Exponent>AQAB</Exponent>
3897            </RSAKeyValue></KeyValue>
3898        </KeyInfo>"#;
3899        let doc = Document::parse(xml).unwrap();
3900
3901        assert!(matches!(
3902            parse_key_info(doc.root_element()),
3903            Err(ParseError::InvalidStructure(_))
3904        ));
3905    }
3906
3907    #[test]
3908    fn parse_rsa_key_value_rejects_malformed_base64() {
3909        // Malformed key parameters must be processing errors, not unresolved keys.
3910        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3911            <KeyValue><RSAKeyValue>
3912                <Modulus>%%%%</Modulus><Exponent>AQAB</Exponent>
3913            </RSAKeyValue></KeyValue>
3914        </KeyInfo>"#;
3915        let doc = Document::parse(xml).unwrap();
3916
3917        assert!(matches!(
3918            parse_key_info(doc.root_element()),
3919            Err(ParseError::Base64(_))
3920        ));
3921    }
3922
3923    #[test]
3924    fn parse_rsa_key_value_rejects_oversized_exponent_before_decode() {
3925        // Bound normalized text before allocation or integer construction.
3926        let exponent = "A".repeat(MAX_RSA_EXPONENT_LEN.div_ceil(3) * 4 + 1);
3927        let xml = format!(
3928            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3929                <KeyValue><RSAKeyValue>
3930                    <Modulus>AQID</Modulus><Exponent>{exponent}</Exponent>
3931                </RSAKeyValue></KeyValue>
3932            </KeyInfo>"#
3933        );
3934        let doc = Document::parse(&xml).unwrap();
3935
3936        assert!(matches!(
3937            parse_key_info(doc.root_element()),
3938            Err(ParseError::InvalidStructure(_))
3939        ));
3940    }
3941
3942    #[test]
3943    fn parse_key_info_ignores_unknown_children() {
3944        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3945            <Foo>bar</Foo>
3946            <KeyName>ok</KeyName>
3947        </KeyInfo>"#;
3948        let doc = Document::parse(xml).unwrap();
3949
3950        let key_info = parse_key_info(doc.root_element()).unwrap();
3951        assert_eq!(key_info.sources, vec![KeyInfoSource::KeyName("ok".into())]);
3952    }
3953
3954    #[test]
3955    fn parse_key_info_keyvalue_requires_single_child() {
3956        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3957            <KeyValue/>
3958        </KeyInfo>"#;
3959        let doc = Document::parse(xml).unwrap();
3960
3961        let err = parse_key_info(doc.root_element()).unwrap_err();
3962        assert!(matches!(err, ParseError::InvalidStructure(_)));
3963    }
3964
3965    #[test]
3966    fn parse_key_info_accepts_empty_x509data() {
3967        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3968            <X509Data/>
3969        </KeyInfo>"#;
3970        let doc = Document::parse(xml).unwrap();
3971
3972        let key_info = parse_key_info(doc.root_element()).unwrap();
3973        assert_eq!(
3974            key_info.sources,
3975            vec![KeyInfoSource::X509Data(X509DataInfo::default())]
3976        );
3977    }
3978
3979    #[test]
3980    fn parse_key_info_rejects_unknown_xmlsig_child_in_x509data() {
3981        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
3982            <X509Data>
3983                <Foo/>
3984            </X509Data>
3985        </KeyInfo>"#;
3986        let doc = Document::parse(xml).unwrap();
3987
3988        let err = parse_key_info(doc.root_element()).unwrap_err();
3989        assert!(matches!(err, ParseError::InvalidStructure(_)));
3990    }
3991
3992    #[test]
3993    fn parse_key_info_rejects_unknown_xmlsig11_child_in_x509data() {
3994        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
3995                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
3996            <X509Data>
3997                <dsig11:Foo/>
3998            </X509Data>
3999        </KeyInfo>"#;
4000        let doc = Document::parse(xml).unwrap();
4001
4002        let err = parse_key_info(doc.root_element()).unwrap_err();
4003        assert!(matches!(err, ParseError::InvalidStructure(_)));
4004    }
4005
4006    #[test]
4007    fn parse_key_info_rejects_x509_issuer_serial_without_required_children() {
4008        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4009            <X509Data>
4010                <X509IssuerSerial>
4011                    <X509IssuerName>CN=CA</X509IssuerName>
4012                </X509IssuerSerial>
4013            </X509Data>
4014        </KeyInfo>"#;
4015        let doc = Document::parse(xml).unwrap();
4016
4017        let err = parse_key_info(doc.root_element()).unwrap_err();
4018        assert!(matches!(err, ParseError::InvalidStructure(_)));
4019    }
4020
4021    #[test]
4022    fn parse_key_info_rejects_x509_issuer_serial_with_duplicate_issuer_name() {
4023        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4024            <X509Data>
4025                <X509IssuerSerial>
4026                    <X509IssuerName>CN=CA-1</X509IssuerName>
4027                    <X509IssuerName>CN=CA-2</X509IssuerName>
4028                    <X509SerialNumber>42</X509SerialNumber>
4029                </X509IssuerSerial>
4030            </X509Data>
4031        </KeyInfo>"#;
4032        let doc = Document::parse(xml).unwrap();
4033
4034        let err = parse_key_info(doc.root_element()).unwrap_err();
4035        assert!(matches!(err, ParseError::InvalidStructure(_)));
4036    }
4037
4038    #[test]
4039    fn parse_key_info_rejects_x509_issuer_serial_with_duplicate_serial_number() {
4040        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4041            <X509Data>
4042                <X509IssuerSerial>
4043                    <X509IssuerName>CN=CA</X509IssuerName>
4044                    <X509SerialNumber>1</X509SerialNumber>
4045                    <X509SerialNumber>2</X509SerialNumber>
4046                </X509IssuerSerial>
4047            </X509Data>
4048        </KeyInfo>"#;
4049        let doc = Document::parse(xml).unwrap();
4050
4051        let err = parse_key_info(doc.root_element()).unwrap_err();
4052        assert!(matches!(err, ParseError::InvalidStructure(_)));
4053    }
4054
4055    #[test]
4056    fn parse_key_info_rejects_x509_issuer_serial_with_whitespace_only_values() {
4057        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4058            <X509Data>
4059                <X509IssuerSerial>
4060                    <X509IssuerName>   </X509IssuerName>
4061                    <X509SerialNumber>
4062                        
4063                    </X509SerialNumber>
4064                </X509IssuerSerial>
4065            </X509Data>
4066        </KeyInfo>"#;
4067        let doc = Document::parse(xml).unwrap();
4068
4069        let err = parse_key_info(doc.root_element()).unwrap_err();
4070        assert!(matches!(err, ParseError::InvalidStructure(_)));
4071    }
4072
4073    #[test]
4074    fn parse_key_info_rejects_x509_issuer_serial_with_wrong_child_order() {
4075        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4076            <X509Data>
4077                <X509IssuerSerial>
4078                    <X509SerialNumber>42</X509SerialNumber>
4079                    <X509IssuerName>CN=CA</X509IssuerName>
4080                </X509IssuerSerial>
4081            </X509Data>
4082        </KeyInfo>"#;
4083        let doc = Document::parse(xml).unwrap();
4084
4085        let err = parse_key_info(doc.root_element()).unwrap_err();
4086        assert!(matches!(err, ParseError::InvalidStructure(_)));
4087    }
4088
4089    #[test]
4090    fn parse_key_info_rejects_x509_issuer_serial_with_extra_child_element() {
4091        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
4092                              xmlns:foo="urn:example:foo">
4093            <X509Data>
4094                <X509IssuerSerial>
4095                    <X509IssuerName>CN=CA</X509IssuerName>
4096                    <X509SerialNumber>42</X509SerialNumber>
4097                    <foo:Extra/>
4098                </X509IssuerSerial>
4099            </X509Data>
4100        </KeyInfo>"#;
4101        let doc = Document::parse(xml).unwrap();
4102
4103        let err = parse_key_info(doc.root_element()).unwrap_err();
4104        assert!(matches!(err, ParseError::InvalidStructure(_)));
4105    }
4106
4107    #[test]
4108    fn parse_key_info_rejects_x509_digest_without_algorithm() {
4109        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
4110                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
4111            <X509Data>
4112                <dsig11:X509Digest>AQID</dsig11:X509Digest>
4113            </X509Data>
4114        </KeyInfo>"#;
4115        let doc = Document::parse(xml).unwrap();
4116
4117        let err = parse_key_info(doc.root_element()).unwrap_err();
4118        assert!(matches!(err, ParseError::InvalidStructure(_)));
4119    }
4120
4121    #[test]
4122    fn parse_key_info_rejects_invalid_x509_certificate_base64() {
4123        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4124            <X509Data>
4125                <X509Certificate>%%%invalid%%%</X509Certificate>
4126            </X509Data>
4127        </KeyInfo>"#;
4128        let doc = Document::parse(xml).unwrap();
4129
4130        let err = parse_key_info(doc.root_element()).unwrap_err();
4131        assert!(matches!(err, ParseError::Base64(_)));
4132    }
4133
4134    #[test]
4135    fn parse_key_info_rejects_x509_data_exceeding_entry_budget() {
4136        let subjects = (0..(MAX_X509_DATA_ENTRY_COUNT + 1))
4137            .map(|idx| format!("<X509SubjectName>CN={idx}</X509SubjectName>"))
4138            .collect::<Vec<_>>()
4139            .join("");
4140        let xml = format!(
4141            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{subjects}</X509Data></KeyInfo>"
4142        );
4143        let doc = Document::parse(&xml).unwrap();
4144
4145        let err = parse_key_info(doc.root_element()).unwrap_err();
4146        assert!(matches!(err, ParseError::InvalidStructure(_)));
4147    }
4148
4149    #[test]
4150    fn parse_key_info_rejects_x509_data_exceeding_total_binary_budget() {
4151        let payload = base64::engine::general_purpose::STANDARD.encode(vec![0u8; 190_000]);
4152        let entries = (0..6)
4153            .map(|_| format!("<X509SKI>{payload}</X509SKI>"))
4154            .collect::<Vec<_>>()
4155            .join("");
4156        let xml = format!(
4157            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{entries}</X509Data></KeyInfo>"
4158        );
4159        let doc = Document::parse(&xml).unwrap();
4160
4161        let err = parse_key_info(doc.root_element()).unwrap_err();
4162        assert!(matches!(err, ParseError::InvalidStructure(_)));
4163    }
4164
4165    #[test]
4166    fn parse_key_info_rejects_x509_certificate_with_invalid_der() {
4167        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4168            <X509Data>
4169                <X509Certificate>AQID</X509Certificate>
4170            </X509Data>
4171        </KeyInfo>"#;
4172        let doc = Document::parse(xml).unwrap();
4173
4174        let err = parse_key_info(doc.root_element()).unwrap_err();
4175        assert!(matches!(err, ParseError::InvalidStructure(_)));
4176    }
4177
4178    #[test]
4179    fn parse_key_info_rejects_x509_certificate_with_trailing_der_bytes() {
4180        let mut cert = base64::engine::general_purpose::STANDARD
4181            .decode(fixture_rsa_cert_base64())
4182            .unwrap();
4183        cert.extend_from_slice(&[0x00, 0x01]);
4184        let cert_base64 = base64::engine::general_purpose::STANDARD.encode(cert);
4185        let xml = format!(
4186            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4187                <X509Data>
4188                    <X509Certificate>{cert_base64}</X509Certificate>
4189                </X509Data>
4190            </KeyInfo>"#
4191        );
4192        let doc = Document::parse(&xml).unwrap();
4193
4194        let err = parse_key_info(doc.root_element()).unwrap_err();
4195        assert!(matches!(err, ParseError::InvalidStructure(_)));
4196    }
4197
4198    #[test]
4199    fn parse_key_info_marks_unsupported_spki_algorithm_as_unsupported() {
4200        let xml = include_str!(
4201            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt.xml"
4202        );
4203        let doc = Document::parse(xml).unwrap();
4204        let key_info_node = doc
4205            .descendants()
4206            .find(|node| {
4207                node.is_element()
4208                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
4209                    && node.tag_name().name() == "KeyInfo"
4210            })
4211            .expect("fixture must contain ds:KeyInfo");
4212
4213        let key_info = parse_key_info(key_info_node).expect("KeyInfo parse should succeed");
4214        let x509_info = match &key_info.sources[0] {
4215            KeyInfoSource::X509Data(x509) => x509,
4216            other => panic!("expected X509Data source, got {other:?}"),
4217        };
4218        assert_eq!(x509_info.certificates.len(), 1);
4219        assert_eq!(x509_info.parsed_certificates.len(), 1);
4220        assert_eq!(x509_info.certificate_chain, vec![0]);
4221        let parsed_cert = &x509_info.parsed_certificates[0];
4222        assert!(!parsed_cert.subject_dn.is_empty());
4223        assert!(!parsed_cert.issuer_dn.is_empty());
4224        assert!(parsed_cert.subject_key_identifier.is_some());
4225        assert!(matches!(
4226            parsed_cert.public_key,
4227            X509PublicKeyInfo::Unsupported { .. }
4228        ));
4229    }
4230
4231    #[test]
4232    fn parse_key_info_orders_x509_certificate_chain_from_signing_cert() {
4233        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
4234        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
4235        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
4236        let xml = format!(
4237            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4238                <X509Data>
4239                    <X509Certificate>{root}</X509Certificate>
4240                    <X509Certificate>{intermediate}</X509Certificate>
4241                    <X509Certificate>{leaf}</X509Certificate>
4242                </X509Data>
4243            </KeyInfo>"#
4244        );
4245        let doc = Document::parse(&xml).unwrap();
4246
4247        let key_info = parse_key_info(doc.root_element()).unwrap();
4248        let x509_info = match &key_info.sources[0] {
4249            KeyInfoSource::X509Data(x509) => x509,
4250            other => panic!("expected X509Data source, got {other:?}"),
4251        };
4252
4253        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
4254    }
4255
4256    #[test]
4257    fn chain_builder_matches_x509_equivalent_distinguished_names() {
4258        // RFC 5280 name chaining uses X.501 matching rather than the lexical
4259        // RFC 4514 rendering. Case differences in DirectoryString values must
4260        // not disconnect an otherwise valid configured path.
4261        let certificates = [
4262            fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
4263            fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem"),
4264            fixture_cert_base64("../../tests/fixtures/keys/cacert.pem"),
4265        ]
4266        .map(|encoded| {
4267            base64::engine::general_purpose::STANDARD
4268                .decode(encoded)
4269                .unwrap()
4270        })
4271        .to_vec();
4272        let mut parsed_certificates = certificates
4273            .iter()
4274            .map(|certificate| parse_x509_certificate(certificate).unwrap())
4275            .collect::<Vec<_>>();
4276        parsed_certificates[0].issuer_dn = parsed_certificates[1].subject_dn.to_ascii_lowercase();
4277        parsed_certificates[1].issuer_dn = parsed_certificates[2].subject_dn.to_ascii_lowercase();
4278        let info = X509DataInfo {
4279            certificates,
4280            parsed_certificates,
4281            ..X509DataInfo::default()
4282        };
4283
4284        assert_eq!(
4285            select_x509_signing_certificate(&info, crate::provider::default_provider()).unwrap(),
4286            0
4287        );
4288        assert_eq!(
4289            build_x509_certificate_chain_from(&info, 0, crate::provider::default_provider(), None)
4290                .unwrap(),
4291            vec![0, 1, 2]
4292        );
4293    }
4294
4295    #[test]
4296    fn parse_key_info_uses_issuer_serial_to_select_x509_signing_certificate() {
4297        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
4298        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
4299        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
4300        let xml = format!(
4301            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4302                <X509Data>
4303                    <X509IssuerSerial>
4304                        <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>
4305                        <X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
4306                    </X509IssuerSerial>
4307                    <X509Certificate>{root}</X509Certificate>
4308                    <X509Certificate>{intermediate}</X509Certificate>
4309                    <X509Certificate>{leaf}</X509Certificate>
4310                </X509Data>
4311            </KeyInfo>"#
4312        );
4313        let doc = Document::parse(&xml).unwrap();
4314
4315        let key_info = parse_key_info(doc.root_element()).unwrap();
4316        let x509_info = match &key_info.sources[0] {
4317            KeyInfoSource::X509Data(x509) => x509,
4318            other => panic!("expected X509Data source, got {other:?}"),
4319        };
4320
4321        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
4322    }
4323
4324    #[test]
4325    fn parse_key_info_allows_selectors_for_multiple_chain_members() {
4326        // X509Data may identify both the signing leaf and another certificate
4327        // in its chain; the unique leaf must remain the signing certificate.
4328        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
4329        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
4330        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
4331        let xml = format!(
4332            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4333                <X509Data>
4334                    <X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
4335                    <X509SKI>0X0XrEVCio75sBcl1TxymJ2IOiU=</X509SKI>
4336                    <X509Certificate>{root}</X509Certificate>
4337                    <X509Certificate>{intermediate}</X509Certificate>
4338                    <X509Certificate>{leaf}</X509Certificate>
4339                </X509Data>
4340            </KeyInfo>"#
4341        );
4342        let doc = Document::parse(&xml).unwrap();
4343
4344        let key_info = parse_key_info(doc.root_element()).unwrap();
4345        let x509_info = match &key_info.sources[0] {
4346            KeyInfoSource::X509Data(x509) => x509,
4347            other => panic!("expected X509Data source, got {other:?}"),
4348        };
4349
4350        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
4351    }
4352
4353    #[test]
4354    fn parse_key_info_uses_decimal_issuer_serial_to_select_x509_signing_certificate() {
4355        let serial = "680572598617295163017172295025714171905498632019";
4356        let padded_serial = format!("{}{}", "0".repeat(64), serial);
4357        assert_eq!(
4358            x509_serial_decimal_to_hex(&padded_serial).as_deref(),
4359            Some("7735EE487F6862DAF1B3956D961CCB0FA6F34F53")
4360        );
4361        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
4362        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
4363        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
4364        let other_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
4365        let xml = format!(
4366            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4367                <X509Data>
4368                    <X509IssuerSerial>
4369                        <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>
4370                        <X509SerialNumber>{padded_serial}</X509SerialNumber>
4371                    </X509IssuerSerial>
4372                    <X509Certificate>{root}</X509Certificate>
4373                    <X509Certificate>{intermediate}</X509Certificate>
4374                    <X509Certificate>{leaf}</X509Certificate>
4375                    <X509Certificate>{other_leaf}</X509Certificate>
4376                </X509Data>
4377            </KeyInfo>"#
4378        );
4379        let doc = Document::parse(&xml).unwrap();
4380
4381        let key_info = parse_key_info(doc.root_element()).unwrap();
4382        let x509_info = match &key_info.sources[0] {
4383            KeyInfoSource::X509Data(x509) => x509,
4384            other => panic!("expected X509Data source, got {other:?}"),
4385        };
4386
4387        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
4388    }
4389
4390    #[test]
4391    fn parse_key_info_rejects_ambiguous_x509_signing_certificate_candidates() {
4392        let first_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
4393        let second_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
4394        let xml = format!(
4395            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4396                <X509Data>
4397                    <X509Certificate>{first_leaf}</X509Certificate>
4398                    <X509Certificate>{second_leaf}</X509Certificate>
4399                </X509Data>
4400            </KeyInfo>"#
4401        );
4402        let doc = Document::parse(&xml).unwrap();
4403
4404        let err = parse_key_info(doc.root_element()).unwrap_err();
4405        assert!(matches!(err, ParseError::InvalidStructure(_)));
4406    }
4407
4408    #[test]
4409    fn parse_key_info_rejects_unmatched_x509_lookup_identifier() {
4410        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
4411        let xml = format!(
4412            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4413                <X509Data>
4414                    <X509SubjectName>CN=Not The Embedded Certificate</X509SubjectName>
4415                    <X509Certificate>{cert}</X509Certificate>
4416                </X509Data>
4417            </KeyInfo>"#
4418        );
4419        let doc = Document::parse(&xml).unwrap();
4420
4421        let err = parse_key_info(doc.root_element()).unwrap_err();
4422        assert!(
4423            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
4424        );
4425    }
4426
4427    #[test]
4428    fn parse_key_info_rejects_partially_matched_selector_category() {
4429        // Every selector value is an asserted lookup constraint; one matching
4430        // SubjectName must not mask another value absent from the chain.
4431        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
4432        let xml = format!(
4433            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4434                <X509Data>
4435                    <X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
4436                    <X509SubjectName>CN=Not In The Embedded Chain</X509SubjectName>
4437                    <X509Certificate>{cert}</X509Certificate>
4438                </X509Data>
4439            </KeyInfo>"#
4440        );
4441        let doc = Document::parse(&xml).unwrap();
4442
4443        let err = parse_key_info(doc.root_element()).unwrap_err();
4444        assert!(
4445            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
4446        );
4447    }
4448
4449    #[test]
4450    fn parse_key_info_rejects_malformed_issuer_serial_even_with_matching_subject() {
4451        // Lexically invalid serials must fail while parsing X509IssuerSerial,
4452        // before another selector or embedded certificate can mask them.
4453        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
4454        let xml = format!(
4455            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4456                <X509Data>
4457                    <X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
4458                    <X509IssuerSerial>
4459                        <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>
4460                        <X509SerialNumber>not-a-decimal-serial</X509SerialNumber>
4461                    </X509IssuerSerial>
4462                    <X509Certificate>{cert}</X509Certificate>
4463                </X509Data>
4464            </KeyInfo>"#
4465        );
4466        let doc = Document::parse(&xml).unwrap();
4467
4468        let err = parse_key_info(doc.root_element()).unwrap_err();
4469        assert!(
4470            matches!(err, ParseError::InvalidStructure(message) if message.contains("invalid X509SerialNumber"))
4471        );
4472    }
4473
4474    #[test]
4475    fn parse_key_info_rejects_unmatched_ski_even_with_matching_subject() {
4476        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
4477        let xml = format!(
4478            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4479                <X509Data>
4480                    <X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
4481                    <X509SKI>AQIDBA==</X509SKI>
4482                    <X509Certificate>{cert}</X509Certificate>
4483                </X509Data>
4484            </KeyInfo>"#
4485        );
4486        let doc = Document::parse(&xml).unwrap();
4487
4488        let err = parse_key_info(doc.root_element()).unwrap_err();
4489        assert!(
4490            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
4491        );
4492    }
4493
4494    #[test]
4495    fn parse_key_info_rejects_lookup_hints_for_different_certificates() {
4496        let first_cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
4497        let second_cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
4498        let xml = format!(
4499            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
4500                <X509Data>
4501                    <X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
4502                    <X509SKI>60zMLKCfzQ3qnXAzABzRNpdgQ8Q=</X509SKI>
4503                    <X509Certificate>{first_cert}</X509Certificate>
4504                    <X509Certificate>{second_cert}</X509Certificate>
4505                </X509Data>
4506            </KeyInfo>"#
4507        );
4508        let doc = Document::parse(&xml).unwrap();
4509
4510        let err = parse_key_info(doc.root_element()).unwrap_err();
4511        assert!(
4512            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers match multiple certificates"))
4513        );
4514    }
4515
4516    #[test]
4517    fn configured_certificate_matching_requires_every_x509_selector_category() {
4518        // A configured certificate is the lookup candidate for selector-only
4519        // X509Data. Every asserted category must match that same certificate.
4520        let certificate = base64::engine::general_purpose::STANDARD
4521            .decode(fixture_rsa_cert_base64())
4522            .unwrap();
4523        let parsed = parse_x509_certificate(&certificate).unwrap();
4524        let digest = compute_digest_with_provider(
4525            crate::provider::default_provider(),
4526            DigestAlgorithm::Sha256,
4527            &certificate,
4528        )
4529        .unwrap();
4530        let matching = X509DataInfo {
4531            subject_names: vec![parsed.subject_dn.clone()],
4532            issuer_serials: vec![(
4533                parsed.issuer_dn.clone(),
4534                "680572598617295163017172295025714171905498632019".into(),
4535            )],
4536            skis: vec![parsed.subject_key_identifier.clone().unwrap()],
4537            digests: vec![(DigestAlgorithm::Sha256.uri().into(), digest)],
4538            ..X509DataInfo::default()
4539        };
4540
4541        assert!(
4542            x509_certificate_matches_selectors(
4543                &matching,
4544                &certificate,
4545                crate::provider::default_provider()
4546            )
4547            .unwrap()
4548        );
4549        for mismatching in [
4550            X509DataInfo {
4551                subject_names: vec!["CN=other".into()],
4552                ..matching.clone()
4553            },
4554            X509DataInfo {
4555                issuer_serials: vec![(parsed.issuer_dn.clone(), "1".into())],
4556                ..matching.clone()
4557            },
4558            X509DataInfo {
4559                skis: vec![vec![0]],
4560                ..matching.clone()
4561            },
4562            X509DataInfo {
4563                digests: vec![(DigestAlgorithm::Sha256.uri().into(), vec![0; 32])],
4564                ..matching.clone()
4565            },
4566        ] {
4567            assert!(
4568                !x509_certificate_matches_selectors(
4569                    &mismatching,
4570                    &certificate,
4571                    crate::provider::default_provider()
4572                )
4573                .unwrap()
4574            );
4575        }
4576    }
4577
4578    #[test]
4579    fn build_x509_certificate_chain_rejects_chain_exceeding_max_depth() {
4580        let parsed_certificates: Vec<ParsedX509Certificate> = (0..=MAX_X509_CHAIN_DEPTH)
4581            .map(|idx| ParsedX509Certificate {
4582                subject_dn: format!("CN=cert-{idx}"),
4583                issuer_dn: if idx == MAX_X509_CHAIN_DEPTH {
4584                    format!("CN=cert-{idx}")
4585                } else {
4586                    format!("CN=cert-{}", idx + 1)
4587                },
4588                serial_number: vec![u8::try_from(idx).unwrap()],
4589                serial_number_hex: format!("{idx:02X}"),
4590                subject_key_identifier: None,
4591                public_key: X509PublicKeyInfo::Unsupported {
4592                    algorithm_oid: "1.2.3.4".into(),
4593                },
4594            })
4595            .collect();
4596        let certificates = vec![Vec::new(); parsed_certificates.len()];
4597        let info = X509DataInfo {
4598            certificates,
4599            parsed_certificates,
4600            ..X509DataInfo::default()
4601        };
4602
4603        let err = build_x509_certificate_chain(&info, crate::provider::default_provider(), None)
4604            .unwrap_err();
4605        assert!(
4606            matches!(err, ParseError::InvalidStructure(message) if message.contains("maximum depth"))
4607        );
4608    }
4609
4610    #[test]
4611    fn x509_serial_hex_strips_der_sign_extension_zeroes() {
4612        assert_eq!(format_x509_serial_value_hex(&[0x00, 0xFF]), "FF");
4613        assert_eq!(format_x509_serial_value_hex(&[0x00, 0x7F]), "7F");
4614        assert_eq!(format_x509_serial_value_hex(&[0x00, 0x00]), "00");
4615    }
4616
4617    #[test]
4618    fn x509_serial_decimal_parser_enforces_rfc5280_positive_range() {
4619        // A 20-octet unsigned magnitude may need a 21st DER sign-padding
4620        // octet. The decimal selector denotes the value, not its DER encoding.
4621        let max_serial = "1461501637330902918203684832716283019655932542975";
4622        assert_eq!(
4623            x509_serial_decimal_to_hex(max_serial),
4624            Some("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF".into())
4625        );
4626        assert_eq!(
4627            x509_serial_decimal_to_hex("730750818665451459101842416358141509827966271488"),
4628            Some("8000000000000000000000000000000000000000".into())
4629        );
4630        assert_eq!(
4631            x509_serial_decimal_to_hex("0000000000000000000000000000000000000000000000001"),
4632            Some("01".into())
4633        );
4634        assert_eq!(
4635            x509_serial_decimal_to_hex("00000000000000000000000000000000000000000000000001"),
4636            Some("01".into())
4637        );
4638        assert_eq!(x509_serial_decimal_to_hex("+1"), Some("01".into()));
4639
4640        for invalid in [
4641            "",
4642            "0",
4643            "000",
4644            "+0",
4645            "++1",
4646            "-1",
4647            "1a",
4648            "1461501637330902918203684832716283019655932542976",
4649        ] {
4650            assert_eq!(
4651                x509_serial_decimal_to_hex(invalid),
4652                None,
4653                "invalid serial {invalid:?} must be rejected"
4654            );
4655        }
4656    }
4657
4658    #[test]
4659    fn parse_x509_serial_normalizes_boundary_whitespace_and_rejects_overflow() {
4660        // XML Schema collapses integer whitespace before validation; the
4661        // normalized value must still obey the RFC 5280 positive range.
4662        let max_serial = "1461501637330902918203684832716283019655932542975";
4663        let valid = format!(
4664            "<KeyInfo xmlns=\"{XMLDSIG_NS}\"><X509Data><X509IssuerSerial><X509IssuerName>CN=issuer</X509IssuerName><X509SerialNumber>\n {max_serial}\t</X509SerialNumber></X509IssuerSerial></X509Data></KeyInfo>"
4665        );
4666        let doc = Document::parse(&valid).unwrap();
4667        let parsed = parse_key_info(doc.root_element()).unwrap();
4668        let KeyInfoSource::X509Data(x509) = &parsed.sources[0] else {
4669            panic!("expected X509Data source");
4670        };
4671        assert_eq!(x509.issuer_serials[0].1, max_serial);
4672
4673        let explicit_positive = valid.replace(max_serial, "+42");
4674        let doc = Document::parse(&explicit_positive).unwrap();
4675        let parsed = parse_key_info(doc.root_element()).unwrap();
4676        let KeyInfoSource::X509Data(x509) = &parsed.sources[0] else {
4677            panic!("expected X509Data source");
4678        };
4679        assert_eq!(x509.issuer_serials[0].1, "42");
4680
4681        let overflow = valid.replace(
4682            max_serial,
4683            "1461501637330902918203684832716283019655932542976",
4684        );
4685        let doc = Document::parse(&overflow).unwrap();
4686        assert!(matches!(
4687            parse_key_info(doc.root_element()),
4688            Err(ParseError::InvalidStructure(message))
4689                if message.contains("invalid X509SerialNumber")
4690        ));
4691    }
4692
4693    #[test]
4694    fn issuer_selector_matches_a_sign_padded_twenty_octet_serial() {
4695        // Selector decimal text represents the unsigned magnitude; the DER
4696        // sign-padding octet must not make the same certificate unmatchable.
4697        let serial_hex = "8000000000000000000000000000000000000000";
4698        let info = X509DataInfo {
4699            issuer_serials: vec![(
4700                "CN=issuer".into(),
4701                "730750818665451459101842416358141509827966271488".into(),
4702            )],
4703            parsed_certificates: vec![ParsedX509Certificate {
4704                subject_dn: "CN=leaf".into(),
4705                issuer_dn: "CN=issuer".into(),
4706                serial_number: [vec![0, 0x80], vec![0; 19]].concat(),
4707                serial_number_hex: serial_hex.into(),
4708                subject_key_identifier: None,
4709                public_key: X509PublicKeyInfo::Unsupported {
4710                    algorithm_oid: "1.2.3.4".into(),
4711                },
4712            }],
4713            ..X509DataInfo::default()
4714        };
4715
4716        assert!(
4717            x509_selector_categories_match_chain(&info, crate::provider::default_provider())
4718                .unwrap()
4719        );
4720    }
4721
4722    #[test]
4723    fn distinguished_name_matching_preserves_rdn_order() {
4724        // RFC 4514 permits alternate encodings within an RDN, but reversing
4725        // the RDN sequence identifies a different hierarchical name.
4726        assert!(distinguished_names_equal(
4727            "CN=leaf, O=example",
4728            "CN=leaf,O=example"
4729        ));
4730        assert!(!distinguished_names_equal(
4731            "CN=leaf,O=example",
4732            "O=example,CN=leaf"
4733        ));
4734    }
4735
4736    #[test]
4737    fn distinguished_name_matching_applies_x520_string_preparation() {
4738        // RFC 5280 requires caseIgnoreMatch with insignificant-space handling
4739        // for DirectoryString values rather than exact ASN.1 value equality.
4740        assert!(distinguished_names_equal(
4741            "CN=  TEST   key  ,O=Example",
4742            "CN=test key,O=example"
4743        ));
4744        assert!(distinguished_names_equal(
4745            "CN=Straße,O=Example",
4746            "CN=STRASSE,O=EXAMPLE"
4747        ));
4748        assert!(distinguished_names_equal(
4749            "CN=test+OU=security,O=example",
4750            "OU=SECURITY+CN=TEST,O=EXAMPLE"
4751        ));
4752        assert!(!distinguished_names_equal(
4753            "1.2.3.4=#040141,O=example",
4754            "1.2.3.4=#040142,O=example"
4755        ));
4756    }
4757
4758    #[test]
4759    fn distinguished_name_matching_applies_ia5_matching_rules() {
4760        // RFC 5280 emailAddress matching preserves the local part while the
4761        // domain is case-insensitive; domainComponent is case-insensitive too.
4762        assert!(distinguished_names_equal(
4763            "EMAIL=ops@EXAMPLE.COM,DC=EXAMPLE,DC=COM",
4764            "EMAIL=ops@example.com,DC=example,DC=com"
4765        ));
4766        assert!(!distinguished_names_equal(
4767            "EMAIL=OPS@example.com,DC=example,DC=com",
4768            "EMAIL=ops@example.com,DC=example,DC=com"
4769        ));
4770    }
4771
4772    #[test]
4773    fn distinguished_name_matching_handles_rfc4514_escaped_values() {
4774        // Certificate values containing RFC 4514 separators and boundary spaces
4775        // must remain one attribute when matched against an XMLDSig selector.
4776        let value = " leading,plus+equals=slash\\trailing ";
4777        let mut params = rcgen::CertificateParams::new(Vec::new()).unwrap();
4778        params
4779            .distinguished_name
4780            .push(rcgen::DnType::CommonName, value);
4781        let key = rcgen::KeyPair::generate().unwrap();
4782        let certificate = params.self_signed(&key).unwrap();
4783        let parsed = parse_x509_certificate(certificate.der()).unwrap();
4784
4785        assert_eq!(
4786            parsed.subject_dn,
4787            r"CN=\ leading\,plus\+equals=slash\\trailing\ "
4788        );
4789        assert!(distinguished_names_equal(
4790            r"CN=\ leading\,plus\+equals=slash\\trailing\ ",
4791            &parsed.subject_dn
4792        ));
4793        assert!(distinguished_names_equal(
4794            "\n  CN=\\ leading\\,plus\\+equals=slash\\\\trailing\\ \n",
4795            &parsed.subject_dn
4796        ));
4797    }
4798
4799    #[test]
4800    fn distinguished_name_trailing_escape_covers_all_xml_whitespace() {
4801        // The normalizer strips all four XML whitespace characters, so escape
4802        // detection must preserve each one consistently at RDN boundaries.
4803        for whitespace in [' ', '\t', '\r', '\n'] {
4804            assert!(trailing_whitespace_is_escaped(&format!(
4805                "CN=value\\{whitespace}"
4806            )));
4807            assert!(!trailing_whitespace_is_escaped(&format!(
4808                "CN=value{whitespace}"
4809            )));
4810        }
4811    }
4812
4813    #[test]
4814    fn parse_key_info_accepts_large_textual_x509_entries_within_entry_budget() {
4815        let issuer_name = "C".repeat(MAX_X509_ISSUER_NAME_TEXT_LEN);
4816        let serial_number = "0".repeat(MAX_X509_SERIAL_NUMBER_VALUE_DIGITS - 1) + "1";
4817        let issuer_serials = (0..52)
4818            .map(|_| {
4819                format!(
4820                    "<X509IssuerSerial><X509IssuerName>{issuer_name}</X509IssuerName><X509SerialNumber>{serial_number}</X509SerialNumber></X509IssuerSerial>"
4821                )
4822            })
4823            .collect::<Vec<_>>()
4824            .join("");
4825        let xml = format!(
4826            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{issuer_serials}</X509Data></KeyInfo>"
4827        );
4828        let doc = Document::parse(&xml).unwrap();
4829
4830        let key_info = parse_key_info(doc.root_element()).unwrap();
4831        let parsed = match &key_info.sources[0] {
4832            KeyInfoSource::X509Data(x509) => x509,
4833            _ => panic!("expected X509Data source"),
4834        };
4835        assert_eq!(parsed.issuer_serials.len(), 52);
4836    }
4837
4838    #[test]
4839    fn parse_key_info_bounds_raw_x509_serial_text() {
4840        // Leading zeroes are lexically valid, but their raw XML representation
4841        // remains bounded independently from the canonical certificate value.
4842        let serial = "0".repeat(MAX_X509_SERIAL_NUMBER_RAW_TEXT_LEN + 1);
4843        let xml = format!(
4844            "<KeyInfo xmlns=\"{XMLDSIG_NS}\"><X509Data><X509IssuerSerial><X509IssuerName>CN=issuer</X509IssuerName><X509SerialNumber>{serial}</X509SerialNumber></X509IssuerSerial></X509Data></KeyInfo>"
4845        );
4846        let doc = Document::parse(&xml).unwrap();
4847
4848        let error = parse_key_info(doc.root_element()).unwrap_err();
4849
4850        assert!(matches!(
4851            error,
4852            ParseError::InvalidStructure(reason)
4853                if reason == "X509SerialNumber exceeds maximum allowed text length"
4854        ));
4855    }
4856
4857    #[test]
4858    fn parse_key_info_accepts_x509data_with_only_foreign_namespace_children() {
4859        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
4860                              xmlns:foo="urn:example:foo">
4861            <X509Data>
4862                <foo:Bar/>
4863            </X509Data>
4864        </KeyInfo>"#;
4865        let doc = Document::parse(xml).unwrap();
4866
4867        let key_info = parse_key_info(doc.root_element()).unwrap();
4868        assert_eq!(
4869            key_info.sources,
4870            vec![KeyInfoSource::X509Data(X509DataInfo::default())]
4871        );
4872    }
4873
4874    #[test]
4875    fn parse_key_info_der_encoded_key_value_rejects_invalid_base64() {
4876        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
4877                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
4878            <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
4879        </KeyInfo>"#;
4880        let doc = Document::parse(xml).unwrap();
4881
4882        let err = parse_key_info(doc.root_element()).unwrap_err();
4883        assert!(matches!(err, ParseError::Base64(_)));
4884    }
4885
4886    #[test]
4887    fn parse_key_info_der_encoded_key_value_accepts_xml_whitespace() {
4888        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
4889                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
4890            <dsig11:DEREncodedKeyValue>
4891                AQID
4892                BA==
4893            </dsig11:DEREncodedKeyValue>
4894        </KeyInfo>"#;
4895        let doc = Document::parse(xml).unwrap();
4896
4897        let key_info = parse_key_info(doc.root_element()).unwrap();
4898        assert_eq!(
4899            key_info.sources,
4900            vec![KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])]
4901        );
4902    }
4903
4904    #[test]
4905    fn parse_key_info_dispatches_dsig11_ec_keyvalue() {
4906        let public_key = "BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=";
4907        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
4908                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
4909            <KeyValue>
4910                <dsig11:ECKeyValue>
4911                    <dsig11:NamedCurve URI="urn:oid:1.2.840.10045.3.1.7"/>
4912                    <dsig11:PublicKey>BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=</dsig11:PublicKey>
4913                </dsig11:ECKeyValue>
4914            </KeyValue>
4915        </KeyInfo>"#;
4916        let doc = Document::parse(xml).unwrap();
4917        let expected_public_key = base64::engine::general_purpose::STANDARD
4918            .decode(public_key)
4919            .expect("fixture EC point must be valid base64");
4920
4921        let key_info = parse_key_info(doc.root_element()).unwrap();
4922        assert_eq!(
4923            key_info.sources,
4924            vec![KeyInfoSource::KeyValue(KeyValueInfo::Ec {
4925                curve_oid: "1.2.840.10045.3.1.7".into(),
4926                public_key: expected_public_key,
4927            })]
4928        );
4929    }
4930
4931    #[test]
4932    fn parse_ec_key_value_accepts_bare_curve_oid() {
4933        use base64::Engine;
4934
4935        let encoded_public_key = "BO/yd/OZzDfjX4qivDY/vsUIuh6KWAxoxW5P4ukvwd+T6pVljWsX2UBJNNy5MdhTwB8e2YwB8kUbJwdsAS/XGi/fz8unFrs+lVlAgIs6s/xBYFbfUoRiAacD2SpVDe6XBA==";
4936        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
4937                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
4938            <KeyValue>
4939                <dsig11:ECKeyValue>
4940                    <dsig11:NamedCurve URI="1.3.132.0.34"/>
4941                    <dsig11:PublicKey>BO/yd/OZzDfjX4qivDY/vsUIuh6KWAxoxW5P4ukvwd+T6pVljWsX2UBJNNy5MdhTwB8e2YwB8kUbJwdsAS/XGi/fz8unFrs+lVlAgIs6s/xBYFbfUoRiAacD2SpVDe6XBA==</dsig11:PublicKey>
4942                </dsig11:ECKeyValue>
4943            </KeyValue>
4944        </KeyInfo>"#;
4945        let doc = Document::parse(xml).unwrap();
4946        let expected_public_key = base64::engine::general_purpose::STANDARD
4947            .decode(encoded_public_key)
4948            .unwrap();
4949
4950        let sources = parse_key_info(doc.root_element()).unwrap().sources;
4951
4952        assert!(matches!(
4953            &sources[0],
4954            KeyInfoSource::KeyValue(KeyValueInfo::Ec { curve_oid, public_key })
4955                if curve_oid == EC_P384_OID && public_key == &expected_public_key
4956        ));
4957    }
4958
4959    #[test]
4960    fn parse_ec_key_value_marks_ec_parameters_as_unsupported() {
4961        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
4962                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
4963            <KeyValue>
4964                <dsig11:ECKeyValue>
4965                    <dsig11:ECParameters/>
4966                    <dsig11:PublicKey>BA==</dsig11:PublicKey>
4967                </dsig11:ECKeyValue>
4968            </KeyValue>
4969        </KeyInfo>"#;
4970        let doc = Document::parse(xml).unwrap();
4971
4972        let key_info = parse_key_info(doc.root_element()).unwrap();
4973        assert_eq!(
4974            key_info.sources,
4975            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
4976                namespace: Some(XMLDSIG11_NS.to_string()),
4977                local_name: "ECKeyValue".into(),
4978            })]
4979        );
4980    }
4981
4982    #[test]
4983    fn parse_ec_key_value_marks_unsupported_curve_as_unsupported() {
4984        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
4985                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
4986            <KeyValue>
4987                <dsig11:ECKeyValue>
4988                    <dsig11:NamedCurve URI="urn:oid:1.3.132.0.36"/>
4989                    <dsig11:PublicKey>BA==</dsig11:PublicKey>
4990                </dsig11:ECKeyValue>
4991            </KeyValue>
4992        </KeyInfo>"#;
4993        let doc = Document::parse(xml).unwrap();
4994
4995        let key_info = parse_key_info(doc.root_element()).unwrap();
4996        assert_eq!(
4997            key_info.sources,
4998            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
4999                namespace: Some(XMLDSIG11_NS.to_string()),
5000                local_name: "ECKeyValue".into(),
5001            })]
5002        );
5003    }
5004
5005    #[test]
5006    fn parse_ec_key_value_marks_missing_named_curve_uri_invalid() {
5007        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
5008                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
5009            <KeyValue>
5010                <dsig11:ECKeyValue>
5011                    <dsig11:NamedCurve/>
5012                    <dsig11:PublicKey>BA==</dsig11:PublicKey>
5013                </dsig11:ECKeyValue>
5014            </KeyValue>
5015        </KeyInfo>"#;
5016        let doc = Document::parse(xml).unwrap();
5017
5018        let key_info = parse_key_info(doc.root_element()).unwrap();
5019        assert_eq!(
5020            key_info.sources,
5021            vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)]
5022        );
5023    }
5024
5025    #[test]
5026    fn parse_ec_key_value_marks_reordered_children_invalid() {
5027        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
5028                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
5029            <KeyValue>
5030                <dsig11:ECKeyValue>
5031                    <dsig11:PublicKey>BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=</dsig11:PublicKey>
5032                    <dsig11:NamedCurve URI="urn:oid:1.2.840.10045.3.1.7"/>
5033                </dsig11:ECKeyValue>
5034            </KeyValue>
5035        </KeyInfo>"#;
5036        let doc = Document::parse(xml).unwrap();
5037
5038        let key_info = parse_key_info(doc.root_element()).unwrap();
5039        assert_eq!(
5040            key_info.sources,
5041            vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)]
5042        );
5043    }
5044
5045    #[test]
5046    fn parse_ec_key_value_marks_non_uncompressed_point_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 URI="urn:oid:1.2.840.10045.3.1.7"/>
5052                    <dsig11:PublicKey>Ap/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=</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_key_info_marks_ds_namespace_ec_keyvalue_as_unsupported() {
5067        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5068            <KeyValue>
5069                <ECKeyValue/>
5070            </KeyValue>
5071        </KeyInfo>"#;
5072        let doc = Document::parse(xml).unwrap();
5073
5074        let key_info = parse_key_info(doc.root_element()).unwrap();
5075        assert_eq!(
5076            key_info.sources,
5077            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
5078                namespace: Some(XMLDSIG_NS.to_string()),
5079                local_name: "ECKeyValue".into(),
5080            })]
5081        );
5082    }
5083
5084    #[test]
5085    fn parse_key_info_keeps_unsupported_keyvalue_child_as_marker() {
5086        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5087            <KeyValue>
5088                <FutureKeyValue/>
5089            </KeyValue>
5090        </KeyInfo>"#;
5091        let doc = Document::parse(xml).unwrap();
5092
5093        let key_info = parse_key_info(doc.root_element()).unwrap();
5094        assert_eq!(
5095            key_info.sources,
5096            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
5097                namespace: Some(XMLDSIG_NS.to_string()),
5098                local_name: "FutureKeyValue".into(),
5099            })]
5100        );
5101    }
5102
5103    #[test]
5104    fn parse_key_info_accepts_supported_x509_retrieval_xpath() {
5105        // Merlin's same-document RetrievalMethod selects only X509Data nodes.
5106        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5107          <RetrievalMethod Type="http://www.w3.org/2000/09/xmldsig#X509Data" URI="#keys">
5108            <Transforms><Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
5109              <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">ancestor-or-self::dsig:X509Data</XPath>
5110            </Transform></Transforms>
5111          </RetrievalMethod>
5112        </KeyInfo>"##;
5113        let doc = Document::parse(xml).unwrap();
5114
5115        let key_info = parse_key_info(doc.root_element()).unwrap();
5116        assert!(matches!(
5117            key_info.sources.as_slice(),
5118            [KeyInfoSource::RetrievalMethod {
5119                uri,
5120                resource_type: Some(resource_type),
5121                transforms: RetrievalMethodTransforms::X509DataNodeSetFilter { .. },
5122            }] if uri == "#keys"
5123                && resource_type == "http://www.w3.org/2000/09/xmldsig#X509Data"
5124        ));
5125    }
5126
5127    #[test]
5128    fn retrieval_xpath_namespace_binding_policy_precedes_materialization() {
5129        // RetrievalMethod must reject inherited namespace bindings before
5130        // cloning their prefix and URI into the retained transform model.
5131        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5132          <RetrievalMethod Type="http://www.w3.org/2000/09/xmldsig#X509Data" URI="#keys">
5133            <Transforms><Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
5134              <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">ancestor-or-self::dsig:X509Data</XPath>
5135            </Transform></Transforms>
5136          </RetrievalMethod>
5137        </KeyInfo>"##;
5138        let document = Document::parse(xml).expect("fixed KeyInfo fixture must parse");
5139        let resources = crate::policy::ResourcePolicy {
5140            max_xpath_namespace_bindings: 0,
5141            ..crate::policy::ResourcePolicy::default()
5142        };
5143
5144        let error = parse_key_info_with_policy_budgets(
5145            document.root_element(),
5146            crate::provider::default_provider(),
5147            &XmlBaseResolutionBudget::default(),
5148            &resources,
5149            None,
5150        )
5151        .expect_err("zero namespace bindings must reject RetrievalMethod XPath");
5152
5153        assert!(matches!(
5154            error,
5155            ParseError::Transform(TransformError::Policy(
5156                crate::policy::PolicyViolation::ResourceLimit {
5157                    resource: crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
5158                    maximum: 0,
5159                    actual: 1,
5160                }
5161            ))
5162        ));
5163    }
5164
5165    #[test]
5166    fn retrieval_xpath_namespace_byte_policy_precedes_materialization() {
5167        // The aggregate namespace byte limit applies to borrowed prefix and URI
5168        // text before either attacker-controlled string is allocated.
5169        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5170          <RetrievalMethod Type="http://www.w3.org/2000/09/xmldsig#X509Data" URI="#keys">
5171            <Transforms><Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
5172              <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">ancestor-or-self::dsig:X509Data</XPath>
5173            </Transform></Transforms>
5174          </RetrievalMethod>
5175        </KeyInfo>"##;
5176        let document = Document::parse(xml).expect("fixed KeyInfo fixture must parse");
5177        let resources = crate::policy::ResourcePolicy {
5178            max_xpath_namespace_bytes: 0,
5179            ..crate::policy::ResourcePolicy::default()
5180        };
5181
5182        let error = parse_key_info_with_policy_budgets(
5183            document.root_element(),
5184            crate::provider::default_provider(),
5185            &XmlBaseResolutionBudget::default(),
5186            &resources,
5187            None,
5188        )
5189        .expect_err("zero namespace bytes must reject RetrievalMethod XPath");
5190
5191        assert!(matches!(
5192            error,
5193            ParseError::Transform(TransformError::Policy(
5194                crate::policy::PolicyViolation::ResourceLimit {
5195                    resource: crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
5196                    maximum: 0,
5197                    actual,
5198                }
5199            )) if actual == "dsig".len() + XMLDSIG_NS.len()
5200        ));
5201    }
5202
5203    #[test]
5204    fn parse_key_info_rejects_oversized_retrieval_method_type() {
5205        // Type is advisory, but retaining it must not allocate unbounded
5206        // attacker-controlled KeyInfo metadata before resolution.
5207        let oversized_type = "x".repeat(MAX_KEY_NAME_TEXT_LEN + 1);
5208        let xml = format!(
5209            r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><RetrievalMethod URI="#key" Type="{oversized_type}"/></KeyInfo>"##
5210        );
5211        let document = Document::parse(&xml).unwrap();
5212
5213        assert!(matches!(
5214            parse_key_info(document.root_element()),
5215            Err(ParseError::InvalidStructure(reason))
5216                if reason == "RetrievalMethod Type exceeds maximum length"
5217        ));
5218    }
5219
5220    #[test]
5221    fn parse_key_info_bounds_retrieval_method_xml_base_chain() {
5222        // RetrievalMethod resolves its resource identity during parsing, so it
5223        // must use the same bounded XML Base algorithm as Reference lookup.
5224        let mut xml =
5225            format!(r#"<KeyInfo xmlns="{XMLDSIG_NS}"><RetrievalMethod URI="key.der"/></KeyInfo>"#);
5226        for _ in 0..65 {
5227            xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
5228        }
5229        let document = Document::parse(&xml).unwrap();
5230        let key_info = document
5231            .descendants()
5232            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5233            .unwrap();
5234
5235        assert!(matches!(
5236            parse_key_info(key_info),
5237            Err(ParseError::InvalidStructure(reason))
5238                if reason.contains("XML Base resolution")
5239        ));
5240    }
5241
5242    #[test]
5243    fn parse_key_info_normalizes_external_retrieval_without_xml_base() {
5244        // RetrievalMethod stores the resolved identity used for caller-map
5245        // lookup, including RFC 3986 normalization when no base is declared.
5246        let xml = format!(
5247            r#"<KeyInfo xmlns="{XMLDSIG_NS}"><RetrievalMethod URI="https://example.test/a/../key.der"/></KeyInfo>"#
5248        );
5249        let document = Document::parse(&xml).unwrap();
5250        let key_info = parse_key_info(document.root_element()).unwrap();
5251
5252        assert!(matches!(
5253            key_info.sources.as_slice(),
5254            [KeyInfoSource::RetrievalMethod { uri, .. }]
5255                if uri == "https://example.test/key.der"
5256        ));
5257    }
5258
5259    #[test]
5260    fn parse_key_info_absolute_retrieval_bypasses_xml_base_chain() {
5261        // Absolute RetrievalMethod identities do not inherit xml:base, so an
5262        // otherwise excessive ancestor chain must not reject them.
5263        let mut xml = format!(
5264            r#"<KeyInfo xmlns="{XMLDSIG_NS}"><RetrievalMethod URI="https://example.test/a/../key.der"/></KeyInfo>"#
5265        );
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_node = document
5271            .descendants()
5272            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
5273            .unwrap();
5274        let key_info = parse_key_info(key_info_node)
5275            .expect("absolute RetrievalMethod must not consume ancestor-base budget");
5276
5277        assert!(matches!(
5278            key_info.sources.as_slice(),
5279            [KeyInfoSource::RetrievalMethod { uri, .. }]
5280                if uri == "https://example.test/key.der"
5281        ));
5282    }
5283
5284    #[test]
5285    fn parse_key_info_accepts_namespace_equivalent_retrieval_xpath_prefix() {
5286        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5287          <RetrievalMethod Type="http://www.w3.org/2000/09/xmldsig#X509Data" URI="#keys">
5288            <Transforms><Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
5289              <XPath xmlns:ds="http://www.w3.org/2000/09/xmldsig#">ancestor-or-self::ds:X509Data</XPath>
5290            </Transform></Transforms>
5291          </RetrievalMethod>
5292        </KeyInfo>"##;
5293        let doc = Document::parse(xml).unwrap();
5294
5295        assert!(matches!(
5296            parse_key_info(doc.root_element())
5297                .unwrap()
5298                .sources
5299                .as_slice(),
5300            [KeyInfoSource::RetrievalMethod {
5301                transforms: RetrievalMethodTransforms::X509DataNodeSetFilter { .. },
5302                ..
5303            }]
5304        ));
5305    }
5306
5307    #[test]
5308    fn parse_key_info_reads_complete_retrieval_xpath_text() {
5309        // XML comments split character data into multiple text nodes; all chunks
5310        // still belong to the XPath parameter's string-value.
5311        let valid = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5312          <RetrievalMethod Type="http://www.w3.org/2000/09/xmldsig#X509Data" URI="#keys">
5313            <Transforms><Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
5314              <XPath xmlns:ds="http://www.w3.org/2000/09/xmldsig#">ancestor-or-self::ds:X509<!-- split -->Data</XPath>
5315            </Transform></Transforms>
5316          </RetrievalMethod>
5317        </KeyInfo>"##;
5318        let document = Document::parse(valid).unwrap();
5319        assert!(parse_key_info(document.root_element()).is_ok());
5320
5321        let unsupported =
5322            valid.replace("X509<!-- split -->Data", "X509Data<!-- split -->[false()]");
5323        let document = Document::parse(&unsupported).unwrap();
5324        assert!(matches!(
5325            parse_key_info(document.root_element()),
5326            Err(ParseError::InvalidStructure(reason))
5327                if reason == "unsupported RetrievalMethod XPath selection"
5328        ));
5329    }
5330
5331    #[test]
5332    fn parse_dsa_key_value_accepts_schema_optional_parameters_and_rejects_half_pair() {
5333        let key_info = |parameters: &str| {
5334            format!(
5335                r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><KeyValue><DSAKeyValue>
5336                {parameters}
5337                </DSAKeyValue></KeyValue></KeyInfo>"#
5338            )
5339        };
5340        for parameters in [
5341            "<Y>AQ==</Y>",
5342            "<G>AQ==</G><Y>AQ==</Y>",
5343            "<P>AQ==</P><Q>AQ==</Q><Y>AQ==</Y>",
5344            "<P>AQ==</P><Q>AQ==</Q><G>AQ==</G><Y>AQ==</Y><J>AQ==</J>",
5345            "<Y>AQ==</Y><Seed>AQ==</Seed><PgenCounter>AQ==</PgenCounter>",
5346            "<Y>AQ==</Y><J>AQ==</J><Seed>AQ==</Seed><PgenCounter>AQ==</PgenCounter>",
5347        ] {
5348            let xml = key_info(parameters);
5349            let doc = Document::parse(&xml).unwrap();
5350            assert!(matches!(
5351                parse_key_info(doc.root_element())
5352                    .unwrap()
5353                    .sources
5354                    .as_slice(),
5355                [KeyInfoSource::KeyValue(KeyValueInfo::Dsa { .. })]
5356            ));
5357        }
5358
5359        for invalid_parameters in [
5360            "<P>AQ==</P><Y>AQ==</Y>",
5361            "<Q>AQ==</Q><Y>AQ==</Y>",
5362            "<Y>AQ==</Y><Seed>AQ==</Seed>",
5363            "<Y>AQ==</Y><PgenCounter>AQ==</PgenCounter>",
5364        ] {
5365            let xml = key_info(invalid_parameters);
5366            let doc = Document::parse(&xml).unwrap();
5367            assert!(matches!(
5368                parse_key_info(doc.root_element()),
5369                Err(ParseError::InvalidStructure(_))
5370            ));
5371        }
5372    }
5373
5374    #[test]
5375    fn parse_dsa_crypto_binary_ignores_comment_nodes() {
5376        // XML comments split simple content without contributing to its string value.
5377        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5378            <KeyValue><DSAKeyValue><Y>AQ<!-- split -->ID</Y></DSAKeyValue></KeyValue>
5379        </KeyInfo>"#;
5380        let doc = Document::parse(xml).unwrap();
5381
5382        assert!(matches!(
5383            parse_key_info(doc.root_element())
5384                .unwrap()
5385                .sources
5386                .as_slice(),
5387            [KeyInfoSource::KeyValue(KeyValueInfo::Dsa { y, .. })] if y == &[1, 2, 3]
5388        ));
5389    }
5390
5391    #[test]
5392    fn parse_rsa_crypto_binary_ignores_comment_nodes() {
5393        // The shared CryptoBinary decoder must apply XML simple-content semantics to every key type.
5394        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5395            <KeyValue><RSAKeyValue>
5396                <Modulus>AQ<!-- split -->ID</Modulus><Exponent>Aw==</Exponent>
5397            </RSAKeyValue></KeyValue>
5398        </KeyInfo>"#;
5399        let doc = Document::parse(xml).unwrap();
5400
5401        assert!(matches!(
5402            parse_key_info(doc.root_element())
5403                .unwrap()
5404                .sources
5405                .as_slice(),
5406            [KeyInfoSource::KeyValue(KeyValueInfo::Rsa { modulus, exponent })]
5407                if modulus == &[1, 2, 3] && exponent == &[3]
5408        ));
5409    }
5410
5411    #[test]
5412    fn parse_key_info_preserves_advisory_unsupported_retrieval_transform() {
5413        // Unsupported RetrievalMethod types are advisory key sources. Their
5414        // transform syntax must not hide a later source the resolver can use.
5415        let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5416          <RetrievalMethod URI="#keys" Type="urn:vendor:key"><Transforms>
5417            <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#base64"/>
5418          </Transforms></RetrievalMethod>
5419          <KeyName>fallback</KeyName>
5420        </KeyInfo>"##;
5421        let doc = Document::parse(xml).unwrap();
5422
5423        let key_info = parse_key_info(doc.root_element())
5424            .expect("unsupported advisory retrieval must not reject all KeyInfo sources");
5425        assert!(matches!(
5426            key_info.sources.as_slice(),
5427            [
5428                KeyInfoSource::RetrievalMethod { resource_type: Some(resource_type), .. },
5429                KeyInfoSource::KeyName(name),
5430            ] if resource_type == "urn:vendor:key" && name == "fallback"
5431        ));
5432    }
5433
5434    #[test]
5435    fn parse_key_info_preserves_empty_same_document_reference_uri() {
5436        // KeyInfoReference inherits Reference URI semantics: the attribute is
5437        // mandatory, but an empty value is a valid same-document URI class.
5438        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
5439            xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
5440            <dsig11:KeyInfoReference URI=""/>
5441        </KeyInfo>"#;
5442        let document = Document::parse(xml).unwrap();
5443
5444        assert!(matches!(
5445            parse_key_info(document.root_element())
5446                .unwrap()
5447                .sources
5448                .as_slice(),
5449            [KeyInfoSource::KeyInfoReference { uri }] if uri.is_empty()
5450        ));
5451    }
5452
5453    #[test]
5454    fn parse_key_info_rejects_excessive_child_sources() {
5455        // KeyInfo extensions are lax, but their parse work remains bounded.
5456        let children = (0..=64)
5457            .map(|index| format!(r#"<extension xmlns="urn:test" index="{index}"/>"#))
5458            .collect::<String>();
5459        let xml =
5460            format!(r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">{children}</KeyInfo>"#);
5461        let document = Document::parse(&xml).unwrap();
5462
5463        assert!(matches!(
5464            parse_key_info(document.root_element()),
5465            Err(ParseError::InvalidStructure(reason))
5466                if reason == "KeyInfo contains too many child elements"
5467        ));
5468    }
5469
5470    #[test]
5471    fn parse_key_info_rejects_keyname_with_child_elements() {
5472        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5473            <KeyName>ok<foo/></KeyName>
5474        </KeyInfo>"#;
5475        let doc = Document::parse(xml).unwrap();
5476
5477        let err = parse_key_info(doc.root_element()).unwrap_err();
5478        assert!(matches!(err, ParseError::InvalidStructure(_)));
5479    }
5480
5481    #[test]
5482    fn parse_key_info_preserves_keyname_text_without_trimming() {
5483        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5484            <KeyName>  signing key  </KeyName>
5485        </KeyInfo>"#;
5486        let doc = Document::parse(xml).unwrap();
5487
5488        let key_info = parse_key_info(doc.root_element()).unwrap();
5489        assert_eq!(
5490            key_info.sources,
5491            vec![KeyInfoSource::KeyName("  signing key  ".into())]
5492        );
5493    }
5494
5495    #[test]
5496    fn parse_key_info_rejects_oversized_keyname_text() {
5497        let oversized = "A".repeat(4097);
5498        let xml = format!(
5499            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><KeyName>{oversized}</KeyName></KeyInfo>"
5500        );
5501        let doc = Document::parse(&xml).unwrap();
5502
5503        let err = parse_key_info(doc.root_element()).unwrap_err();
5504        assert!(matches!(err, ParseError::InvalidStructure(_)));
5505    }
5506
5507    #[test]
5508    fn parse_key_info_rejects_non_whitespace_mixed_content() {
5509        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">oops<KeyName>k</KeyName></KeyInfo>"#;
5510        let doc = Document::parse(xml).unwrap();
5511
5512        let err = parse_key_info(doc.root_element()).unwrap_err();
5513        assert!(matches!(err, ParseError::InvalidStructure(_)));
5514    }
5515
5516    #[test]
5517    fn parse_key_info_rejects_nbsp_as_non_xml_whitespace_mixed_content() {
5518        let xml = "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\u{00A0}<KeyName>k</KeyName></KeyInfo>";
5519        let doc = Document::parse(xml).unwrap();
5520
5521        let err = parse_key_info(doc.root_element()).unwrap_err();
5522        assert!(matches!(err, ParseError::InvalidStructure(_)));
5523    }
5524
5525    #[test]
5526    fn parse_key_info_der_encoded_key_value_rejects_oversized_payload() {
5527        let oversized =
5528            base64::engine::general_purpose::STANDARD
5529                .encode(vec![0u8; MAX_DER_ENCODED_KEY_VALUE_LEN + 1]);
5530        let xml = format!(
5531            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:dsig11=\"http://www.w3.org/2009/xmldsig11#\"><dsig11:DEREncodedKeyValue>{oversized}</dsig11:DEREncodedKeyValue></KeyInfo>"
5532        );
5533        let doc = Document::parse(&xml).unwrap();
5534
5535        let err = parse_key_info(doc.root_element()).unwrap_err();
5536        assert!(matches!(err, ParseError::InvalidStructure(_)));
5537    }
5538
5539    #[test]
5540    fn parse_key_info_der_encoded_key_value_rejects_empty_payload() {
5541        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
5542                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
5543            <dsig11:DEREncodedKeyValue>
5544                
5545            </dsig11:DEREncodedKeyValue>
5546        </KeyInfo>"#;
5547        let doc = Document::parse(xml).unwrap();
5548
5549        let err = parse_key_info(doc.root_element()).unwrap_err();
5550        assert!(matches!(err, ParseError::InvalidStructure(_)));
5551    }
5552
5553    #[test]
5554    fn parse_key_info_der_encoded_key_value_non_xml_ascii_whitespace_is_not_parseable_xml() {
5555        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>";
5556        assert!(Document::parse(xml).is_err());
5557    }
5558
5559    // ── parse_signed_info: happy path ────────────────────────────────
5560
5561    #[test]
5562    fn parse_hmac_output_length_reads_all_text_nodes() {
5563        // A comment may split valid simple content without changing its value.
5564        let xml = r#"<SignatureMethod xmlns="http://www.w3.org/2000/09/xmldsig#">
5565            <HMACOutputLength>8<!-- split -->0</HMACOutputLength>
5566        </SignatureMethod>"#;
5567        let document = Document::parse(xml).unwrap();
5568
5569        assert_eq!(
5570            parse_hmac_output_length(document.root_element(), SignatureAlgorithm::HmacSha1)
5571                .unwrap(),
5572            Some(80)
5573        );
5574    }
5575
5576    #[test]
5577    fn hmac_sha256_accepts_byte_aligned_output_length() {
5578        let document = Document::parse(
5579            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>"#,
5580        )
5581        .unwrap();
5582        let algorithm =
5583            SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#hmac-sha256")
5584                .expect("HMAC-SHA256 must be recognized");
5585
5586        assert_eq!(
5587            parse_hmac_output_length(document.root_element(), algorithm).unwrap(),
5588            Some(128)
5589        );
5590    }
5591
5592    #[test]
5593    fn parse_hmac_output_length_rejects_hidden_suffix_text() {
5594        // Reading only the first text node would misinterpret 800 bits as 80.
5595        let xml = r#"<SignatureMethod xmlns="http://www.w3.org/2000/09/xmldsig#">
5596            <HMACOutputLength>80<!-- split -->0</HMACOutputLength>
5597        </SignatureMethod>"#;
5598        let document = Document::parse(xml).unwrap();
5599
5600        assert!(matches!(
5601            parse_hmac_output_length(document.root_element(), SignatureAlgorithm::HmacSha1),
5602            Err(ParseError::InvalidStructure(reason))
5603                if reason == "HMACOutputLength must be a positive byte-aligned value no greater than 160"
5604        ));
5605    }
5606
5607    #[test]
5608    fn hmac_output_length_rejects_non_xml_whitespace() {
5609        // xs:integer whitespace normalization must not turn Unicode separators
5610        // into legal surrounding XML whitespace (XSD 1.0 section 3.3.13).
5611        for separator in ['\u{a0}', '\u{2003}', '\u{85}'] {
5612            let text = format!("{separator}128{separator}");
5613            assert!(ByteAlignedHmacOutputLength::parse(&text, 256).is_err());
5614        }
5615        assert_eq!(
5616            ByteAlignedHmacOutputLength::parse(" \t\r\n+00128 \t", 256)
5617                .unwrap()
5618                .bits(),
5619            128
5620        );
5621    }
5622
5623    #[test]
5624    fn xmldsig_1_1_rejects_non_octet_hmac_output_length() {
5625        // XMLDSig 1.1 section 6.3.1 requires a byte boundary even though the
5626        // HMACOutputLength schema represents the value as a bit count.
5627        let xml = r#"<SignatureMethod xmlns="http://www.w3.org/2000/09/xmldsig#">
5628            <HMACOutputLength>129</HMACOutputLength>
5629        </SignatureMethod>"#;
5630        let document = Document::parse(xml).unwrap();
5631
5632        assert!(matches!(
5633            parse_hmac_output_length(document.root_element(), SignatureAlgorithm::HmacSha256),
5634            Err(ParseError::InvalidStructure(reason))
5635                if reason == "HMACOutputLength must be a positive byte-aligned value no greater than 256"
5636        ));
5637    }
5638
5639    #[test]
5640    fn parse_signed_info_rsa_sha256_with_reference() {
5641        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5642            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5643            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5644            <Reference URI="">
5645                <Transforms>
5646                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
5647                    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5648                </Transforms>
5649                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5650                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
5651            </Reference>
5652        </SignedInfo>"#;
5653        let doc = Document::parse(xml).unwrap();
5654        let si = parse_signed_info(doc.root_element()).unwrap();
5655
5656        assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
5657        assert_eq!(si.references.len(), 1);
5658
5659        let r = &si.references[0];
5660        assert_eq!(r.uri.as_deref(), Some(""));
5661        assert_eq!(r.digest_method, DigestAlgorithm::Sha256);
5662        assert_eq!(r.digest_value, vec![0u8; 32]);
5663        assert_eq!(r.transforms.len(), 2);
5664    }
5665
5666    #[test]
5667    fn parse_signed_info_multiple_references() {
5668        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5669            <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
5670            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
5671            <Reference URI="#a">
5672                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5673                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
5674            </Reference>
5675            <Reference URI="#b">
5676                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
5677                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
5678            </Reference>
5679        </SignedInfo>"##;
5680        let doc = Document::parse(xml).unwrap();
5681        let si = parse_signed_info(doc.root_element()).unwrap();
5682
5683        assert_eq!(si.signature_method, SignatureAlgorithm::EcdsaSha256);
5684        assert_eq!(si.references.len(), 2);
5685        assert_eq!(si.references[0].uri.as_deref(), Some("#a"));
5686        assert_eq!(si.references[0].digest_method, DigestAlgorithm::Sha256);
5687        assert_eq!(si.references[1].uri.as_deref(), Some("#b"));
5688        assert_eq!(si.references[1].digest_method, DigestAlgorithm::Sha1);
5689    }
5690
5691    #[test]
5692    fn parse_signed_info_rejects_too_many_references() {
5693        // Reference processing shares signature-wide resource budgets, so the
5694        // parser must bound cardinality before retaining attacker-controlled entries.
5695        let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
5696            .map(|index| {
5697                format!(
5698                    r##"<Reference URI="#item-{index}">
5699                        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5700                        <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
5701                    </Reference>"##
5702                )
5703            })
5704            .collect::<String>();
5705        let xml = format!(
5706            r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5707                <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5708                <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5709                {references}
5710            </SignedInfo>"#
5711        );
5712        let document = Document::parse(&xml).expect("fixed oversized fixture must parse");
5713
5714        let error = parse_signed_info(document.root_element())
5715            .expect_err("the parser must reject the 65th Reference");
5716
5717        assert!(matches!(
5718            error,
5719            ParseError::Policy(crate::policy::PolicyViolation::ResourceLimit {
5720                resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
5721                maximum: MAX_REFERENCES_PER_SIGNATURE,
5722                actual: 65,
5723            })
5724        ));
5725    }
5726
5727    #[test]
5728    fn parse_signed_info_bounds_xpath_expressions_across_references() {
5729        // Per-reference limits alone permit an attacker to retain and compile
5730        // thousands of XPath programs before signature verification begins.
5731        let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
5732            .repeat(64);
5733        let filter_transform = format!(
5734            r#"<Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</Transform>"#
5735        );
5736        let reference = |index, transforms: &str| {
5737            format!(
5738                r##"<Reference URI="#item-{index}">
5739                        <Transforms>{transforms}</Transforms>
5740                        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5741                        <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
5742                    </Reference>"##
5743            )
5744        };
5745        let signed_info = |references: &str| {
5746            format!(
5747                r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5748                <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5749                <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5750                {references}
5751            </SignedInfo>"#
5752            )
5753        };
5754
5755        let max_reference = reference(0, &filter_transform.repeat(64));
5756        let boundary_xml = signed_info(&max_reference);
5757        let boundary_document =
5758            Document::parse(&boundary_xml).expect("fixed boundary fixture must parse");
5759        parse_signed_info(boundary_document.root_element())
5760            .expect("one maximum-shaped Reference must remain accepted");
5761
5762        let extra_transform = r#"<Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><XPath>true()</XPath></Transform>"#;
5763        let xml = signed_info(&format!("{max_reference}{}", reference(1, extra_transform)));
5764        let document = Document::parse(&xml).expect("fixed aggregate fixture must parse");
5765
5766        let error = parse_signed_info(document.root_element())
5767            .expect_err("signature-wide XPath expression count must be bounded");
5768
5769        assert!(matches!(
5770            error,
5771            ParseError::Transform(TransformError::Policy(
5772                crate::policy::PolicyViolation::ResourceLimit {
5773                    resource: "XPath expressions",
5774                    ..
5775                }
5776            ))
5777        ));
5778    }
5779
5780    #[test]
5781    fn parse_reference_without_transforms() {
5782        // Transforms element is optional
5783        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5784            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5785            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5786            <Reference URI="#obj">
5787                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5788                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
5789            </Reference>
5790        </SignedInfo>"##;
5791        let doc = Document::parse(xml).unwrap();
5792        let si = parse_signed_info(doc.root_element()).unwrap();
5793
5794        assert!(si.references[0].transforms.is_empty());
5795    }
5796
5797    #[test]
5798    fn parse_reference_with_all_attributes() {
5799        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5800            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5801            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5802            <Reference URI="#data" Id="ref1" Type="http://www.w3.org/2000/09/xmldsig#Object">
5803                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5804                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
5805            </Reference>
5806        </SignedInfo>"##;
5807        let doc = Document::parse(xml).unwrap();
5808        let si = parse_signed_info(doc.root_element()).unwrap();
5809        let r = &si.references[0];
5810
5811        assert_eq!(r.uri.as_deref(), Some("#data"));
5812        assert_eq!(r.id.as_deref(), Some("ref1"));
5813        assert_eq!(
5814            r.ref_type.as_deref(),
5815            Some("http://www.w3.org/2000/09/xmldsig#Object")
5816        );
5817    }
5818
5819    #[test]
5820    fn parse_reference_absent_uri() {
5821        // URI attribute is optional per spec
5822        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5823            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5824            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5825            <Reference>
5826                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5827                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
5828            </Reference>
5829        </SignedInfo>"#;
5830        let doc = Document::parse(xml).unwrap();
5831        let si = parse_signed_info(doc.root_element()).unwrap();
5832        assert!(si.references[0].uri.is_none());
5833    }
5834
5835    #[test]
5836    fn parse_signed_info_preserves_inclusive_prefixes() {
5837        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
5838                                 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
5839            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
5840                <ec:InclusiveNamespaces PrefixList="ds saml #default"/>
5841            </CanonicalizationMethod>
5842            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5843            <Reference URI="">
5844                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5845                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
5846            </Reference>
5847        </SignedInfo>"#;
5848        let doc = Document::parse(xml).unwrap();
5849
5850        let si = parse_signed_info(doc.root_element()).unwrap();
5851        assert!(si.c14n_method.inclusive_prefixes().contains("ds"));
5852        assert!(si.c14n_method.inclusive_prefixes().contains("saml"));
5853        assert!(si.c14n_method.inclusive_prefixes().contains(""));
5854    }
5855
5856    // ── parse_signed_info: error cases ───────────────────────────────
5857
5858    #[test]
5859    fn missing_canonicalization_method() {
5860        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5861            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5862            <Reference URI="">
5863                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5864                <DigestValue>dGVzdA==</DigestValue>
5865            </Reference>
5866        </SignedInfo>"#;
5867        let doc = Document::parse(xml).unwrap();
5868        let result = parse_signed_info(doc.root_element());
5869        assert!(result.is_err());
5870        // SignatureMethod is first child but expected CanonicalizationMethod
5871        assert!(matches!(
5872            result.unwrap_err(),
5873            ParseError::InvalidStructure(_)
5874        ));
5875    }
5876
5877    #[test]
5878    fn missing_signature_method() {
5879        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5880            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5881            <Reference URI="">
5882                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5883                <DigestValue>dGVzdA==</DigestValue>
5884            </Reference>
5885        </SignedInfo>"#;
5886        let doc = Document::parse(xml).unwrap();
5887        let result = parse_signed_info(doc.root_element());
5888        assert!(result.is_err());
5889        // Reference is second child but expected SignatureMethod
5890        assert!(matches!(
5891            result.unwrap_err(),
5892            ParseError::InvalidStructure(_)
5893        ));
5894    }
5895
5896    #[test]
5897    fn no_references() {
5898        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5899            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5900            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5901        </SignedInfo>"#;
5902        let doc = Document::parse(xml).unwrap();
5903        let result = parse_signed_info(doc.root_element());
5904        assert!(matches!(
5905            result.unwrap_err(),
5906            ParseError::MissingElement {
5907                element: "Reference"
5908            }
5909        ));
5910    }
5911
5912    #[test]
5913    fn unsupported_c14n_algorithm() {
5914        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5915            <CanonicalizationMethod Algorithm="http://example.com/bogus-c14n"/>
5916            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5917            <Reference URI="">
5918                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5919                <DigestValue>dGVzdA==</DigestValue>
5920            </Reference>
5921        </SignedInfo>"#;
5922        let doc = Document::parse(xml).unwrap();
5923        let result = parse_signed_info(doc.root_element());
5924        assert!(matches!(
5925            result.unwrap_err(),
5926            ParseError::UnsupportedAlgorithm { .. }
5927        ));
5928    }
5929
5930    #[test]
5931    fn unsupported_signature_algorithm() {
5932        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5933            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5934            <SignatureMethod Algorithm="http://example.com/bogus-sign"/>
5935            <Reference URI="">
5936                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5937                <DigestValue>dGVzdA==</DigestValue>
5938            </Reference>
5939        </SignedInfo>"#;
5940        let doc = Document::parse(xml).unwrap();
5941        let result = parse_signed_info(doc.root_element());
5942        assert!(matches!(
5943            result.unwrap_err(),
5944            ParseError::UnsupportedAlgorithm { .. }
5945        ));
5946    }
5947
5948    #[test]
5949    fn unsupported_digest_algorithm() {
5950        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5951            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5952            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5953            <Reference URI="">
5954                <DigestMethod Algorithm="http://example.com/bogus-digest"/>
5955                <DigestValue>dGVzdA==</DigestValue>
5956            </Reference>
5957        </SignedInfo>"#;
5958        let doc = Document::parse(xml).unwrap();
5959        let result = parse_signed_info(doc.root_element());
5960        assert!(matches!(
5961            result.unwrap_err(),
5962            ParseError::UnsupportedAlgorithm { .. }
5963        ));
5964    }
5965
5966    #[test]
5967    fn missing_digest_method() {
5968        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5969            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5970            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5971            <Reference URI="">
5972                <DigestValue>dGVzdA==</DigestValue>
5973            </Reference>
5974        </SignedInfo>"#;
5975        let doc = Document::parse(xml).unwrap();
5976        let result = parse_signed_info(doc.root_element());
5977        // DigestValue is not DigestMethod
5978        assert!(result.is_err());
5979    }
5980
5981    #[test]
5982    fn missing_digest_value() {
5983        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5984            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5985            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
5986            <Reference URI="">
5987                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5988            </Reference>
5989        </SignedInfo>"#;
5990        let doc = Document::parse(xml).unwrap();
5991        let result = parse_signed_info(doc.root_element());
5992        assert!(matches!(
5993            result.unwrap_err(),
5994            ParseError::MissingElement {
5995                element: "DigestValue"
5996            }
5997        ));
5998    }
5999
6000    #[test]
6001    fn invalid_base64_digest_value() {
6002        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
6003            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6004            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6005            <Reference URI="">
6006                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6007                <DigestValue>!!!not-base64!!!</DigestValue>
6008            </Reference>
6009        </SignedInfo>"#;
6010        let doc = Document::parse(xml).unwrap();
6011        let result = parse_signed_info(doc.root_element());
6012        assert!(matches!(result.unwrap_err(), ParseError::Base64(_)));
6013    }
6014
6015    #[test]
6016    fn digest_value_length_must_match_digest_method() {
6017        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
6018            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6019            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6020            <Reference URI="">
6021                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6022                <DigestValue>dGVzdA==</DigestValue>
6023            </Reference>
6024        </SignedInfo>"#;
6025        let doc = Document::parse(xml).unwrap();
6026
6027        let result = parse_signed_info(doc.root_element());
6028        assert!(matches!(
6029            result.unwrap_err(),
6030            ParseError::DigestLengthMismatch {
6031                algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
6032                expected: 32,
6033                actual: 4,
6034            }
6035        ));
6036    }
6037
6038    #[test]
6039    fn inclusive_prefixes_on_inclusive_c14n_is_rejected() {
6040        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
6041                                 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
6042            <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">
6043                <ec:InclusiveNamespaces PrefixList="ds"/>
6044            </CanonicalizationMethod>
6045            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6046            <Reference URI="">
6047                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6048                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
6049            </Reference>
6050        </SignedInfo>"#;
6051        let doc = Document::parse(xml).unwrap();
6052
6053        let result = parse_signed_info(doc.root_element());
6054        assert!(matches!(
6055            result.unwrap_err(),
6056            ParseError::UnsupportedAlgorithm { .. }
6057        ));
6058    }
6059
6060    #[test]
6061    fn extra_element_after_digest_value() {
6062        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
6063            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6064            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6065            <Reference URI="">
6066                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6067                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
6068                <Unexpected/>
6069            </Reference>
6070        </SignedInfo>"#;
6071        let doc = Document::parse(xml).unwrap();
6072        let result = parse_signed_info(doc.root_element());
6073        assert!(matches!(
6074            result.unwrap_err(),
6075            ParseError::InvalidStructure(_)
6076        ));
6077    }
6078
6079    #[test]
6080    fn digest_value_with_element_child_is_rejected() {
6081        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
6082            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6083            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6084            <Reference URI="">
6085                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6086                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=<Junk/>AAAA</DigestValue>
6087            </Reference>
6088        </SignedInfo>"#;
6089        let doc = Document::parse(xml).unwrap();
6090
6091        let result = parse_signed_info(doc.root_element());
6092        assert!(matches!(
6093            result.unwrap_err(),
6094            ParseError::InvalidStructure(_)
6095        ));
6096    }
6097
6098    #[test]
6099    fn wrong_namespace_on_signed_info() {
6100        let xml = r#"<SignedInfo xmlns="http://example.com/fake">
6101            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6102        </SignedInfo>"#;
6103        let doc = Document::parse(xml).unwrap();
6104        let result = parse_signed_info(doc.root_element());
6105        assert!(matches!(
6106            result.unwrap_err(),
6107            ParseError::InvalidStructure(_)
6108        ));
6109    }
6110
6111    // ── Whitespace-wrapped base64 ────────────────────────────────────
6112
6113    #[test]
6114    fn base64_with_whitespace() {
6115        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
6116            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6117            <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
6118            <Reference URI="">
6119                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6120                <DigestValue>
6121                    AAAAAAAA
6122                    AAAAAAAAAAAAAAAAAAA=
6123                </DigestValue>
6124            </Reference>
6125        </SignedInfo>"#;
6126        let doc = Document::parse(xml).unwrap();
6127        let si = parse_signed_info(doc.root_element()).unwrap();
6128        assert_eq!(si.references[0].digest_value, vec![0u8; 20]);
6129    }
6130
6131    #[test]
6132    fn digest_value_ignores_processing_instruction_data() {
6133        // PI payload is not XML character data and therefore cannot alter the
6134        // base64 octets represented by DigestValue.
6135        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
6136            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6137            <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
6138            <Reference URI="">
6139                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
6140                <DigestValue>AAAAAAAA<?ignored not-base64?>AAAAAAAAAAAAAAAAAAA=</DigestValue>
6141            </Reference>
6142        </SignedInfo>"#;
6143        let document = Document::parse(xml).expect("fixture must parse");
6144        let signed_info =
6145            parse_signed_info(document.root_element()).expect("PI must not alter digest data");
6146
6147        assert_eq!(signed_info.references[0].digest_value, vec![0_u8; 20]);
6148    }
6149
6150    #[test]
6151    fn base64_decode_digest_accepts_xml_whitespace_chars() {
6152        let digest =
6153            base64_decode_digest("AAAA\tAAAA\rAAAA\nAAAA AAAAAAAAAAA=", DigestAlgorithm::Sha1)
6154                .expect("XML whitespace in DigestValue must be accepted");
6155        assert_eq!(digest, vec![0u8; 20]);
6156    }
6157
6158    #[test]
6159    fn base64_decode_digest_rejects_non_xml_ascii_whitespace() {
6160        let err = base64_decode_digest(
6161            "AAAA\u{000C}AAAAAAAAAAAAAAAAAAAAAAA=",
6162            DigestAlgorithm::Sha1,
6163        )
6164        .expect_err("form-feed/vertical-tab in DigestValue must be rejected");
6165        assert!(matches!(err, ParseError::Base64(_)));
6166    }
6167
6168    #[test]
6169    fn base64_decode_digest_rejects_oversized_base64_before_decode() {
6170        let err = base64_decode_digest("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", DigestAlgorithm::Sha1)
6171            .expect_err("oversized DigestValue base64 must fail before decode");
6172        match err {
6173            ParseError::Base64(message) => {
6174                assert!(
6175                    message.contains("DigestValue exceeds maximum allowed base64 length"),
6176                    "unexpected message: {message}"
6177                );
6178            }
6179            other => panic!("expected ParseError::Base64, got {other:?}"),
6180        }
6181    }
6182
6183    // ── Real-world SAML structure ────────────────────────────────────
6184
6185    #[test]
6186    fn saml_response_signed_info() {
6187        let xml = r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6188            <ds:SignedInfo>
6189                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6190                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
6191                <ds:Reference URI="#_resp1">
6192                    <ds:Transforms>
6193                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
6194                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
6195                    </ds:Transforms>
6196                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
6197                    <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
6198                </ds:Reference>
6199            </ds:SignedInfo>
6200            <ds:SignatureValue>ZmFrZQ==</ds:SignatureValue>
6201        </ds:Signature>"##;
6202        let doc = Document::parse(xml).unwrap();
6203
6204        // Find SignedInfo within Signature
6205        let sig_node = doc.root_element();
6206        let signed_info_node = sig_node
6207            .children()
6208            .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
6209            .unwrap();
6210
6211        let si = parse_signed_info(signed_info_node).unwrap();
6212        assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
6213        assert_eq!(si.references.len(), 1);
6214        assert_eq!(si.references[0].uri.as_deref(), Some("#_resp1"));
6215        assert_eq!(si.references[0].transforms.len(), 2);
6216        assert_eq!(si.references[0].digest_value, vec![0u8; 32]);
6217    }
6218}