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