Skip to main content

xml_sec/xmlenc/
parse.rs

1//! Strict parsing for the subset of XMLEnc needed by the decryption API.
2
3#[cfg(test)]
4use crate::xml::dom::ParsingOptions;
5use crate::xml::dom::{Document, Node};
6#[cfg(test)]
7use base64::{Engine as _, engine::general_purpose::STANDARD};
8
9use crate::document::{
10    DocumentParseSettings, XmlParseWorkBudget, parse_borrowed_with_settings_and_budget,
11};
12
13use super::map_document_error;
14use super::types::{
15    CipherData, EncryptedData, EncryptedDataType, EncryptedKey, EncryptionMethod,
16    MAX_CIPHER_VALUE_BASE64_LEN, ReferenceList, XMLDSIG_NS, XMLENC_NS, XMLENC11_NS, XmlEncError,
17};
18
19#[derive(Clone, Copy)]
20pub(super) struct ParsingPolicy<'a> {
21    key_establishment: &'a crate::policy::KeyEstablishmentPolicy,
22    xml: &'a crate::policy::XmlInputPolicy,
23    resources: &'a crate::policy::ResourcePolicy,
24    uris: Option<&'a crate::policy::UriPolicy>,
25    transforms: Option<&'a crate::policy::TransformPolicy>,
26}
27
28impl<'a> From<&'a crate::policy::EncryptionPolicy> for ParsingPolicy<'a> {
29    fn from(policy: &'a crate::policy::EncryptionPolicy) -> Self {
30        Self {
31            key_establishment: &policy.key_establishment,
32            xml: &policy.xml,
33            resources: &policy.resources,
34            uris: None,
35            transforms: None,
36        }
37    }
38}
39
40impl<'a> From<&'a crate::policy::DecryptionPolicy> for ParsingPolicy<'a> {
41    fn from(policy: &'a crate::policy::DecryptionPolicy) -> Self {
42        Self {
43            key_establishment: &policy.key_establishment,
44            xml: &policy.xml,
45            resources: &policy.resources,
46            uris: Some(&policy.uris),
47            transforms: Some(&policy.transforms),
48        }
49    }
50}
51
52struct ParsedKeyInfo {
53    encapsulation_methods: Vec<crate::key_establishment::EncapsulationMechanism>,
54    source_nodes: Vec<crate::NodeId>,
55    key_name: Option<String>,
56    encrypted_keys: Vec<EncryptedKey>,
57    derived_keys: Vec<super::DerivedKey>,
58    agreement_methods: Vec<super::AgreementMethod>,
59}
60
61struct SharedKeySourceParseBudget<'a> {
62    retained_cipher_bytes: usize,
63    transforms: crate::xmldsig::transforms::XPathSignatureParseBudget,
64    key_info: crate::xmldsig::parse::KeyInfoParsingSession<'a>,
65    reference_ancestry: Vec<(Option<String>, [u8; 32])>,
66}
67
68impl<'a> SharedKeySourceParseBudget<'a> {
69    fn new(
70        resources: &'a crate::policy::ResourcePolicy,
71        references: Option<&super::CipherReferenceContext<'_>>,
72    ) -> Result<Self, XmlEncError> {
73        Ok(Self {
74            retained_cipher_bytes: 0,
75            transforms: match references {
76                Some(context) => context.transform_parse_budget(),
77                None => {
78                    crate::xmldsig::transforms::XPathSignatureParseBudget::from_resources(resources)
79                }
80            },
81            key_info: crate::xmldsig::parse::KeyInfoParsingSession::new(resources)
82                .map_err(super::agreement::map_key_info_error)?,
83            reference_ancestry: Vec::new(),
84        })
85    }
86}
87
88struct KeySourceParseBudget<'a, 'doc, 'shared> {
89    policy: ParsingPolicy<'a>,
90    provider: &'a dyn crate::provider::CryptoProvider,
91    shared: &'shared mut SharedKeySourceParseBudget<'a>,
92    references: Option<&'a super::CipherReferenceContext<'a>>,
93    xml_parse: Option<&'a XmlParseWorkBudget>,
94    document_base: Option<String>,
95    resolver: Option<crate::xmldsig::uri::UriReferenceResolver<'doc>>,
96    registrations: &'a [crate::IdAttributeRegistration],
97    ancestry: Vec<crate::NodeId>,
98    origins: Vec<Option<crate::NodeId>>,
99    encapsulation_values: Vec<Node<'doc, 'doc>>,
100    encrypted_key_depth: usize,
101    detached: Option<Vec<crate::NodeId>>,
102}
103
104impl<'a, 'doc, 'shared> KeySourceParseBudget<'a, 'doc, 'shared> {
105    fn resolver(
106        &mut self,
107        source: Node<'doc, 'doc>,
108    ) -> &crate::xmldsig::uri::UriReferenceResolver<'doc> {
109        self.resolver.get_or_insert_with(|| {
110            crate::xmldsig::uri::UriReferenceResolver::with_id_registrations(
111                source.document(),
112                self.registrations,
113            )
114            .with_same_document_id_semantics(
115                self.policy.transforms.map_or(Default::default(), |policy| {
116                    policy.same_document_id_semantics
117                }),
118            )
119        })
120    }
121
122    fn referenced_node(
123        &mut self,
124        source: Node<'doc, 'doc>,
125        uri: &str,
126    ) -> Result<Node<'doc, 'doc>, XmlEncError> {
127        self.resolver(source)
128            .node_for_same_document_reference(uri)?
129            .ok_or_else(|| {
130                XmlEncError::InvalidStructure(
131                    "encryption key reference target is absent or ambiguous".into(),
132                )
133            })
134    }
135    fn new(
136        policy: ParsingPolicy<'a>,
137        registrations: &'a [crate::IdAttributeRegistration],
138        provider: &'a dyn crate::provider::CryptoProvider,
139        shared: &'shared mut SharedKeySourceParseBudget<'a>,
140    ) -> Self {
141        Self {
142            policy,
143            provider,
144            shared,
145            references: None,
146            xml_parse: None,
147            document_base: None,
148            resolver: None,
149            registrations,
150            ancestry: Vec::new(),
151            origins: Vec::new(),
152            encapsulation_values: Vec::new(),
153            encrypted_key_depth: 0,
154            detached: None,
155        }
156    }
157
158    fn charge(&mut self) -> Result<(), XmlEncError> {
159        self.shared
160            .key_info
161            .charge_encryption_source()
162            .map_err(super::agreement::map_key_info_error)
163    }
164
165    fn charge_recipient(
166        &mut self,
167        resources: &crate::policy::ResourcePolicy,
168        existing: usize,
169    ) -> Result<(), XmlEncError> {
170        if existing >= resources.max_encryption_recipients {
171            return Err(crate::policy::PolicyViolation::ResourceLimit {
172                resource: crate::policy::resource_name::ENCRYPTION_RECIPIENTS,
173                maximum: resources.max_encryption_recipients,
174                actual: existing + 1,
175            }
176            .into());
177        }
178        self.charge()
179    }
180}
181
182/// Parse one `xenc:EncryptedData` document fragment.
183pub fn parse_encrypted_data(xml: &str) -> Result<EncryptedData, XmlEncError> {
184    parse_encrypted_data_with_policy(xml, &crate::policy::DecryptionPolicy::default())
185}
186
187pub(super) fn parse_encrypted_data_with_policy(
188    xml: &str,
189    policy: &crate::policy::DecryptionPolicy,
190) -> Result<EncryptedData, XmlEncError> {
191    parse_encrypted_data_with_policy_and_backend(xml, policy, crate::XmlBackend::default())
192}
193
194pub(super) fn parse_encrypted_data_with_policy_and_backend(
195    xml: &str,
196    policy: &crate::policy::DecryptionPolicy,
197    backend: crate::XmlBackend,
198) -> Result<EncryptedData, XmlEncError> {
199    let parse_budget = XmlParseWorkBudget::from_resources(&policy.resources);
200    parse_encrypted_data_with_policy_backend_and_budget(xml, policy, backend, &parse_budget)
201}
202
203pub(super) fn parse_encrypted_data_with_policy_backend_and_budget(
204    xml: &str,
205    policy: &crate::policy::DecryptionPolicy,
206    backend: crate::XmlBackend,
207    parse_budget: &XmlParseWorkBudget,
208) -> Result<EncryptedData, XmlEncError> {
209    policy.validate()?;
210    policy.resources.validate_xml_document_len(xml.len())?;
211    let settings =
212        DocumentParseSettings::from_policy(&policy.xml, &policy.resources).with_backend(backend);
213    let document = parse_borrowed_with_settings_and_budget(xml, settings, Some(parse_budget))
214        .map_err(|error| map_document_error(error, settings))?;
215    let references = super::CipherReferenceContext::new(policy, None, backend, &[])?;
216    parse_encrypted_data_node_with_origins(
217        document.root_element(),
218        policy.into(),
219        false,
220        &[],
221        crate::provider::default_provider(),
222        Some(&references),
223        Some(parse_budget),
224    )
225    .map(|(data, _)| data)
226}
227
228/// Parse a selected `xenc:EncryptedData` node under an immutable policy snapshot.
229///
230/// This is the node-oriented counterpart to [`parse_encrypted_data`]. It lets
231/// callers that already parsed a containing document validate the complete
232/// encrypted-data structure without serializing the selected subtree and losing
233/// namespace declarations inherited from its ancestors. The containing source
234/// document is reparsed because [`Node`] does not expose its parser provenance.
235pub fn parse_encrypted_data_node_with_policy(
236    node: Node<'_, '_>,
237    policy: &crate::policy::DecryptionPolicy,
238) -> Result<EncryptedData, XmlEncError> {
239    parse_encrypted_data_node_with_policy_and_backend(node, policy, crate::XmlBackend::default())
240}
241
242/// Parse a selected `xenc:EncryptedData` node with an explicit parser backend.
243pub fn parse_encrypted_data_node_with_policy_and_backend(
244    node: Node<'_, '_>,
245    policy: &crate::policy::DecryptionPolicy,
246    backend: crate::XmlBackend,
247) -> Result<EncryptedData, XmlEncError> {
248    parse_encrypted_data_node_with_context(
249        node,
250        policy,
251        backend,
252        crate::provider::default_provider(),
253        &[],
254    )
255}
256
257/// Inspect a selected node using the same provider and ID registrations as decryption.
258/// Same-document KeyInfo references are resolved before returning recipient metadata.
259pub fn parse_encrypted_data_node_with_context(
260    node: Node<'_, '_>,
261    policy: &crate::policy::DecryptionPolicy,
262    backend: crate::XmlBackend,
263    provider: &dyn crate::provider::CryptoProvider,
264    id_attributes: &[crate::IdAttributeRegistration],
265) -> Result<EncryptedData, XmlEncError> {
266    let parse_budget = XmlParseWorkBudget::from_resources(&policy.resources);
267    parse_encrypted_data_node_with_context_and_budget(
268        node,
269        policy,
270        &parse_budget,
271        backend,
272        provider,
273        id_attributes,
274    )
275}
276
277fn parse_encrypted_data_node_with_context_and_budget(
278    node: Node<'_, '_>,
279    policy: &crate::policy::DecryptionPolicy,
280    parse_budget: &XmlParseWorkBudget,
281    backend: crate::XmlBackend,
282    provider: &dyn crate::provider::CryptoProvider,
283    id_attributes: &[crate::IdAttributeRegistration],
284) -> Result<EncryptedData, XmlEncError> {
285    policy.validate()?;
286    let references = super::CipherReferenceContext::new(policy, None, backend, id_attributes)?;
287    let policy = ParsingPolicy::from(policy);
288    validate_node_document_policy(node, policy, parse_budget, backend)?;
289    parse_encrypted_data_node_with_origins(
290        node,
291        policy,
292        false,
293        id_attributes,
294        provider,
295        Some(&references),
296        Some(parse_budget),
297    )
298    .map(|(data, _)| data)
299}
300
301/// Immutable descriptor graph with an association-checked recipient location.
302/// No key material is copied to record selection; the private location is bound
303/// to this graph and cannot become stale through caller mutation.
304pub struct EncryptedDataInspection {
305    data: EncryptedData,
306    recipient: Option<super::decrypt::EncapsulationRecipient>,
307}
308
309impl EncryptedDataInspection {
310    /// Validated, fully resolved encrypted-data metadata.
311    pub fn data(&self) -> &EncryptedData {
312        &self.data
313    }
314
315    /// Whether decryption must retain the original document's reference and ID
316    /// semantics rather than use the standalone typed-descriptor API.
317    pub fn requires_document_context(&self) -> bool {
318        fn keys_require_context(keys: &[EncryptedKey]) -> bool {
319            keys.iter().any(|key| {
320                key.reference_list.is_some()
321                    || matches!(key.cipher_data, CipherData::Reference { .. })
322                    || key
323                        .sources
324                        .derived_keys
325                        .iter()
326                        .any(|key| key.reference_list.is_some())
327                    || keys_require_context(&key.sources.encrypted_keys)
328            })
329        }
330        // Parsed graphs have the non-configurable KeyInfo depth ceiling. This
331        // borrowed traversal allocates nothing and cannot recurse past it.
332        matches!(self.data.cipher_data, CipherData::Reference { .. })
333            || self
334                .data
335                .derived_keys
336                .iter()
337                .any(|key| key.reference_list.is_some())
338            || keys_require_context(&self.data.encrypted_keys)
339    }
340
341    /// The sole applicable KEM recipient, after encrypted-key association checks.
342    pub fn recipient_encapsulation(
343        &self,
344    ) -> Option<&crate::key_establishment::EncapsulationMechanism> {
345        self.recipient
346            .as_ref()
347            .map(|recipient| recipient.mechanism(&self.data))
348    }
349}
350
351/// Inspect recipient selection using the same source identities and association
352/// predicates as decryption, before loading any caller-provided private key.
353pub fn inspect_encrypted_data_node_with_context(
354    node: Node<'_, '_>,
355    policy: &crate::policy::DecryptionPolicy,
356    backend: crate::XmlBackend,
357    provider: &dyn crate::provider::CryptoProvider,
358    id_attributes: &[crate::IdAttributeRegistration],
359) -> Result<EncryptedDataInspection, XmlEncError> {
360    let parse_budget = XmlParseWorkBudget::from_resources(&policy.resources);
361    policy.validate()?;
362    let references = super::CipherReferenceContext::new(policy, None, backend, id_attributes)?;
363    validate_node_document_policy(node, policy.into(), &parse_budget, backend)?;
364    let (data, origins) = parse_encrypted_data_node_with_origins(
365        node,
366        policy.into(),
367        false,
368        id_attributes,
369        provider,
370        Some(&references),
371        Some(&parse_budget),
372    )?;
373    let bound = references.bind_document(node.document());
374    let recipient =
375        super::decrypt::select_encapsulation_recipient(&data, &bound, node.id(), &origins, policy)?;
376    Ok(EncryptedDataInspection { data, recipient })
377}
378
379/// Parse an `xenc:EncryptedData` template under an immutable policy snapshot.
380///
381/// This applies the complete encrypted-data grammar and metadata limits while
382/// permitting empty `CipherValue` placeholders that encryption will replace.
383/// Non-empty placeholders must still be well-formed base64. The containing
384/// source document is reparsed under this policy before template inspection.
385pub fn parse_encrypted_data_template_node_with_policy(
386    node: Node<'_, '_>,
387    policy: &crate::policy::EncryptionPolicy,
388) -> Result<EncryptedData, XmlEncError> {
389    parse_encrypted_data_template_node_with_policy_and_backend(
390        node,
391        policy,
392        crate::XmlBackend::default(),
393    )
394}
395
396/// Parse an `xenc:EncryptedData` template node with an explicit parser backend.
397pub fn parse_encrypted_data_template_node_with_policy_and_backend(
398    node: Node<'_, '_>,
399    policy: &crate::policy::EncryptionPolicy,
400    backend: crate::XmlBackend,
401) -> Result<EncryptedData, XmlEncError> {
402    inspect_encrypted_data_template_node_with_context(
403        node,
404        policy,
405        backend,
406        crate::provider::default_provider(),
407        &[],
408    )
409    .map(|template| template.data)
410}
411
412/// Validated template metadata and original same-document mutation targets.
413/// The borrowed nodes remain bound to the inspected source DOM; they are never
414/// inferred from a generated document or from a lexical descendant scan.
415pub struct EncryptedDataTemplate<'doc> {
416    data: EncryptedData,
417    encapsulation_values: Vec<Node<'doc, 'doc>>,
418}
419
420impl<'doc> EncryptedDataTemplate<'doc> {
421    /// Fully resolved establishment descriptors, bound to the original targets.
422    pub fn data(&self) -> &EncryptedData {
423        &self.data
424    }
425
426    /// Consume inspection when only the descriptor graph is needed.
427    pub fn into_data(self) -> EncryptedData {
428        self.data
429    }
430
431    /// Original CipherValue placeholders in resolved mechanism order.
432    pub fn encapsulation_cipher_values(&self) -> &[Node<'doc, 'doc>] {
433        &self.encapsulation_values
434    }
435}
436
437/// Inspect a template with the operation's engine and registered XML IDs.
438pub fn inspect_encrypted_data_template_node_with_context<'doc>(
439    node: Node<'doc, 'doc>,
440    policy: &crate::policy::EncryptionPolicy,
441    backend: crate::XmlBackend,
442    provider: &dyn crate::provider::CryptoProvider,
443    id_attributes: &[crate::IdAttributeRegistration],
444) -> Result<EncryptedDataTemplate<'doc>, XmlEncError> {
445    let parse_budget = XmlParseWorkBudget::from_resources(&policy.resources);
446    policy.validate()?;
447    let policy = ParsingPolicy::from(policy);
448    validate_node_document_policy(node, policy, &parse_budget, backend)?;
449    let (data, _, encapsulation_values) = parse_encrypted_data_node_traced(
450        node,
451        policy,
452        true,
453        id_attributes,
454        provider,
455        None,
456        Some(&parse_budget),
457    )?;
458    Ok(EncryptedDataTemplate {
459        data,
460        encapsulation_values,
461    })
462}
463
464fn validate_node_document_policy(
465    node: Node<'_, '_>,
466    policy: ParsingPolicy<'_>,
467    parse_budget: &XmlParseWorkBudget,
468    backend: crate::XmlBackend,
469) -> Result<(), XmlEncError> {
470    parse_policy_document(node.document().input_text(), policy, parse_budget, backend)?;
471    Ok(())
472}
473
474fn parse_policy_document<'a>(
475    xml: &'a str,
476    policy: ParsingPolicy<'_>,
477    parse_budget: &XmlParseWorkBudget,
478    backend: crate::XmlBackend,
479) -> Result<Document<'a>, XmlEncError> {
480    let settings =
481        DocumentParseSettings::from_policy(policy.xml, policy.resources).with_backend(backend);
482    parse_borrowed_with_settings_and_budget(xml, settings, Some(parse_budget))
483        .map_err(|error| map_document_error(error, settings))
484}
485
486pub(super) fn parse_encrypted_data_node_with_origins(
487    node: Node<'_, '_>,
488    policy: ParsingPolicy<'_>,
489    allow_empty_cipher_values: bool,
490    registrations: &[crate::IdAttributeRegistration],
491    provider: &dyn crate::provider::CryptoProvider,
492    references: Option<&super::CipherReferenceContext<'_>>,
493    xml_parse: Option<&XmlParseWorkBudget>,
494) -> Result<(EncryptedData, Vec<Option<crate::NodeId>>), XmlEncError> {
495    parse_encrypted_data_node_traced(
496        node,
497        policy,
498        allow_empty_cipher_values,
499        registrations,
500        provider,
501        references,
502        xml_parse,
503    )
504    .map(|(data, origins, _)| (data, origins))
505}
506
507type TracedEncryptedData<'doc> = (
508    EncryptedData,
509    Vec<Option<crate::NodeId>>,
510    Vec<Node<'doc, 'doc>>,
511);
512
513fn parse_encrypted_data_node_traced<'doc>(
514    node: Node<'doc, 'doc>,
515    policy: ParsingPolicy<'_>,
516    allow_empty_cipher_values: bool,
517    registrations: &[crate::IdAttributeRegistration],
518    provider: &dyn crate::provider::CryptoProvider,
519    references: Option<&super::CipherReferenceContext<'_>>,
520    xml_parse: Option<&XmlParseWorkBudget>,
521) -> Result<TracedEncryptedData<'doc>, XmlEncError> {
522    require_element(node, XMLENC_NS, "EncryptedData")?;
523    let mut shared = SharedKeySourceParseBudget::new(policy.resources, references)?;
524    let mut budget = KeySourceParseBudget::new(policy, registrations, provider, &mut shared);
525    budget.references = references;
526    budget.xml_parse = xml_parse;
527    validate_encrypted_type_attributes(node, policy)?;
528    let mut children = element_children(node);
529    let encryption_method = parse_encryption_method_with_limit(
530        next_required(&mut children, "EncryptionMethod")?,
531        policy.resources.max_encryption_metadata_bytes,
532    )?;
533    if children
534        .peek()
535        .is_some_and(|child| child.has_tag_name((XMLENC_NS, "EncryptionMethod")))
536    {
537        return Err(XmlEncError::InvalidStructure(
538            "EncryptedData contains more than one direct EncryptionMethod".into(),
539        ));
540    }
541
542    let key_info_node = match children.peek() {
543        Some(child) if child.has_tag_name((XMLDSIG_NS, "KeyInfo")) => {
544            Some(next_required(&mut children, "KeyInfo")?)
545        }
546        _ => None,
547    };
548    if children
549        .peek()
550        .is_some_and(|child| child.has_tag_name((XMLDSIG_NS, "KeyInfo")))
551    {
552        return Err(XmlEncError::InvalidStructure(
553            "EncryptedData contains more than one direct KeyInfo".into(),
554        ));
555    }
556
557    let cipher_data = parse_cipher_data(
558        next_required(&mut children, "CipherData")?,
559        allow_empty_cipher_values
560            || encryption_method.algorithm == super::DataEncryptionAlgorithm::ChaCha20.uri(),
561        policy,
562        &mut budget.shared.transforms,
563        &mut budget.shared.retained_cipher_bytes,
564    )?;
565    consume_encryption_properties(&mut children);
566    if children.next().is_some() {
567        return Err(XmlEncError::InvalidStructure(
568            "EncryptedData has unexpected child after CipherData".into(),
569        ));
570    }
571
572    // Reject the containing value and policy before indirect KeyInfo processing
573    // can resolve resources or execute transforms.
574    if let Some(references) = references {
575        references.validate_method(&encryption_method, true, provider)?;
576        references.preflight_cipher_data(&cipher_data)?;
577    }
578    let mut key_info = match key_info_node {
579        Some(key_info) => {
580            parse_key_info(key_info, policy, allow_empty_cipher_values, &mut budget, 0)?
581        }
582        None => ParsedKeyInfo {
583            encapsulation_methods: Vec::new(),
584            source_nodes: Vec::new(),
585            key_name: None,
586            encrypted_keys: Vec::new(),
587            derived_keys: Vec::new(),
588            agreement_methods: Vec::new(),
589        },
590    };
591    append_detached_keys(
592        node,
593        &mut key_info,
594        policy,
595        allow_empty_cipher_values,
596        &mut budget,
597        0,
598    )?;
599
600    let encrypted = EncryptedData {
601        encapsulation_methods: key_info.encapsulation_methods,
602        id: bounded_attribute(node, "Id", policy)?,
603        encrypted_type: parse_encrypted_data_type(node.attribute("Type")),
604        key_name: key_info.key_name,
605        encryption_method,
606        encrypted_keys: key_info.encrypted_keys,
607        derived_keys: key_info.derived_keys,
608        agreement_methods: key_info.agreement_methods,
609        cipher_data,
610    };
611    validate_encrypted_data_metadata_inner(&encrypted, policy, allow_empty_cipher_values)?;
612    Ok((encrypted, budget.origins, budget.encapsulation_values))
613}
614
615fn parse_key_info<'doc>(
616    node: Node<'doc, 'doc>,
617    policy: ParsingPolicy<'_>,
618    allow_empty_cipher_values: bool,
619    budget: &mut KeySourceParseBudget<'_, 'doc, '_>,
620    depth: usize,
621) -> Result<ParsedKeyInfo, XmlEncError> {
622    require_element(node, XMLDSIG_NS, "KeyInfo")?;
623    policy.resources.validate_key_info_reference_depth(depth)?;
624    let recipients = node
625        .children()
626        .filter(|child| child.has_tag_name((XMLENC_NS, "EncryptedKey")))
627        .count();
628    if recipients > policy.resources.max_encryption_recipients {
629        return Err(crate::policy::PolicyViolation::ResourceLimit {
630            resource: crate::policy::resource_name::ENCRYPTION_RECIPIENTS,
631            maximum: policy.resources.max_encryption_recipients,
632            actual: recipients,
633        }
634        .into());
635    }
636    if budget.ancestry.contains(&node.id()) {
637        return Err(XmlEncError::InvalidStructure(
638            "cyclic encryption KeyInfo reference".into(),
639        ));
640    }
641    budget.ancestry.push(node.id());
642    let mut key_name = None;
643    let mut source_nodes = Vec::new();
644    let mut encrypted_keys = Vec::new();
645    let mut derived_keys = Vec::new();
646    let mut agreement_methods = Vec::new();
647    let mut encapsulation_methods = Vec::new();
648    for child in node.children().filter(Node::is_element) {
649        if child.has_tag_name((
650            crate::key_establishment::ENCAPSULATION_NS,
651            "EncapsulationMechanism",
652        )) {
653            budget.charge()?;
654            let mechanism =
655                crate::key_establishment::parse_encapsulation(child, allow_empty_cipher_values)?;
656            policy
657                .key_establishment
658                .check_encapsulation(mechanism.algorithm)?;
659            let bytes = mechanism.ciphertext().len().div_ceil(3) * 4;
660            let maximum = policy.resources.max_xml_document_bytes;
661            let used = budget.shared.retained_cipher_bytes;
662            if used > maximum || bytes > maximum - used {
663                return Err(crate::policy::PolicyViolation::ResourceLimit {
664                    resource: crate::policy::resource_name::AGGREGATE_ENCRYPTION_CIPHER_VALUE_BYTES,
665                    maximum,
666                    actual: used.saturating_add(bytes),
667                }
668                .into());
669            }
670            budget.shared.retained_cipher_bytes += bytes;
671            let info = parse_encapsulation_key_info(mechanism.key_info, budget, depth + 1)?;
672            if allow_empty_cipher_values && budget.encrypted_key_depth == 0 {
673                budget.encapsulation_values.push(mechanism.cipher_value);
674            }
675            encapsulation_methods.push(crate::key_establishment::EncapsulationMechanism {
676                algorithm: mechanism.algorithm,
677                key_info: info,
678                ciphertext: mechanism.ciphertext().to_vec(),
679            });
680            source_nodes.push(child.id());
681        } else if child.has_tag_name((XMLDSIG_NS, "KeyName")) {
682            if key_name.is_some() {
683                return Err(XmlEncError::InvalidStructure(
684                    "KeyInfo contains more than one direct KeyName".into(),
685                ));
686            }
687            key_name = Some(parse_key_name(child, policy)?);
688        } else if child.has_tag_name((XMLENC_NS, "EncryptedKey")) {
689            budget.charge_recipient(policy.resources, encrypted_keys.len())?;
690            source_nodes.push(child.id());
691            encrypted_keys.push(parse_encrypted_key(
692                child,
693                policy,
694                allow_empty_cipher_values,
695                budget,
696                depth + 1,
697            )?);
698        } else if child.has_tag_name((XMLENC11_NS, "DerivedKey")) {
699            budget.charge()?;
700            source_nodes.push(child.id());
701            derived_keys.push(super::derived_key::parse(child, policy.resources)?);
702        } else if child.has_tag_name((XMLENC_NS, "AgreementMethod")) {
703            budget.charge()?;
704            // Role parsing preflights against this same session before binary
705            // metadata inspection, then charges embedded keys exactly once.
706            agreement_methods.push(super::agreement::parse(
707                child,
708                policy.resources,
709                &mut budget.shared.key_info,
710                budget.provider,
711            )?);
712        } else if child.has_tag_name((XMLDSIG_NS, "RetrievalMethod")) {
713            // XMLDSig 1.1 section 4.5.3 defines element-only content here.
714            // KeyInfo's mixed content must not leak into this child grammar.
715            // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-RetrievalMethod
716            if child.children().any(|node| {
717                node.is_text()
718                    && node.text().is_some_and(|text| {
719                        !text
720                            .bytes()
721                            .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
722                    })
723            }) {
724                return Err(XmlEncError::InvalidStructure(
725                    "RetrievalMethod contains non-whitespace text".into(),
726                ));
727            }
728            // XMLEnc 1.1 §3.5.3 defines both EncryptedKey and DerivedKey
729            // retrieval. An explicit Type must agree with the actual target.
730            // Resolve against one ID index and retain the original key node.
731            // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-Extensions-to-KeyInfo
732            let uri = child
733                .attribute("URI")
734                .ok_or(XmlEncError::MissingRequired("RetrievalMethod URI"))?;
735            validate_metadata_len(uri.len(), policy.resources.max_encryption_metadata_bytes)?;
736            let allowed = policy
737                .uris
738                .map_or(crate::xmldsig::UriTypeSet::SAME_DOCUMENT, |uris| {
739                    uris.retrieval_methods
740                });
741            if !allowed.allows(uri) {
742                return Err(crate::policy::PolicyViolation::Algorithm {
743                    operation: "encryption key retrieval URI",
744                    algorithm: uri.to_owned(),
745                }
746                .into());
747            }
748            let mut transform_children = element_children(child);
749            let transforms = match transform_children.next() {
750                Some(container) if container.has_tag_name((XMLDSIG_NS, "Transforms")) => {
751                    if transform_children.next().is_some()
752                        || !container.children().any(|node| node.is_element())
753                    {
754                        return Err(XmlEncError::InvalidStructure(
755                            "RetrievalMethod requires one nonempty ds:Transforms container".into(),
756                        ));
757                    }
758                    crate::xmldsig::transforms::parse_reference_transforms_with_budget(
759                        container,
760                        XMLDSIG_NS,
761                        &mut budget.shared.transforms,
762                    )?
763                }
764                Some(_) => {
765                    return Err(XmlEncError::InvalidStructure(
766                        "unexpected RetrievalMethod child".into(),
767                    ));
768                }
769                None => Vec::new(),
770            };
771            let declared_type = child.attribute("Type");
772            let declared_type = declared_type
773                .map(|kind| {
774                    validate_metadata_len(
775                        kind.len(),
776                        policy.resources.max_encryption_metadata_bytes,
777                    )?;
778                    RetrievedKeyType::parse(kind)
779                })
780                .transpose()?;
781            if !uri.is_empty() && !uri.starts_with('#') || !transforms.is_empty() {
782                budget.charge()?;
783                match parse_processed_key_reference(
784                    child,
785                    uri,
786                    &transforms,
787                    declared_type,
788                    allow_empty_cipher_values,
789                    budget,
790                    depth + 1,
791                )? {
792                    RetrievedKey::Encrypted(key) => {
793                        if encrypted_keys.len() >= policy.resources.max_encryption_recipients {
794                            return Err(crate::policy::PolicyViolation::ResourceLimit {
795                                resource: crate::policy::resource_name::ENCRYPTION_RECIPIENTS,
796                                maximum: policy.resources.max_encryption_recipients,
797                                actual: encrypted_keys.len() + 1,
798                            }
799                            .into());
800                        }
801                        encrypted_keys.push(key);
802                    }
803                    RetrievedKey::Derived(key) => derived_keys.push(key),
804                }
805                continue;
806            }
807            let target = budget.referenced_node(child, uri)?;
808            if let Some(kind) = declared_type {
809                kind.validate_target(target)?;
810            }
811            if target.has_tag_name((XMLENC11_NS, "DerivedKey")) {
812                budget.charge()?;
813                source_nodes.push(target.id());
814                derived_keys.push(super::derived_key::parse(target, policy.resources)?);
815                continue;
816            }
817            budget.charge_recipient(policy.resources, encrypted_keys.len())?;
818            source_nodes.push(target.id());
819            encrypted_keys.push(parse_encrypted_key(
820                target,
821                policy,
822                allow_empty_cipher_values,
823                budget,
824                depth + 1,
825            )?);
826        } else if child.has_tag_name((crate::xmldsig::parse::XMLDSIG11_NS, "KeyInfoReference")) {
827            // XMLDSig 1.1 §4.5.10 requires a KeyInfo target, without
828            // RetrievalMethod transforms. The same recursion budget and
829            // ancestry track indirect and embedded encryption sources.
830            // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-KeyInfoReference
831            let uri = child
832                .attribute("URI")
833                .ok_or(XmlEncError::MissingRequired("KeyInfoReference URI"))?;
834            validate_metadata_len(uri.len(), policy.resources.max_encryption_metadata_bytes)?;
835            if child.children().any(|node| {
836                node.is_element()
837                    || node.is_text()
838                        && node.text().is_some_and(|text| {
839                            !text
840                                .bytes()
841                                .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
842                        })
843            }) {
844                return Err(XmlEncError::InvalidStructure(
845                    "KeyInfoReference must be empty".into(),
846                ));
847            }
848            let allowed = policy
849                .uris
850                .map_or(crate::xmldsig::UriTypeSet::SAME_DOCUMENT, |uris| {
851                    uris.key_info_references
852                });
853            if !allowed.allows(uri) {
854                return Err(crate::policy::PolicyViolation::Algorithm {
855                    operation: "encryption KeyInfo reference URI",
856                    algorithm: uri.to_owned(),
857                }
858                .into());
859            }
860            budget.charge()?;
861            let target = budget.referenced_node(child, uri)?;
862            let info =
863                parse_key_info(target, policy, allow_empty_cipher_values, budget, depth + 1)?;
864            if let Some(name) = info.key_name {
865                if key_name.is_some() {
866                    return Err(XmlEncError::InvalidStructure(
867                        "indirect KeyInfo contains conflicting KeyName".into(),
868                    ));
869                }
870                key_name = Some(name);
871            }
872            if encrypted_keys.len() + info.encrypted_keys.len()
873                > policy.resources.max_encryption_recipients
874            {
875                return Err(crate::policy::PolicyViolation::ResourceLimit {
876                    resource: crate::policy::resource_name::ENCRYPTION_RECIPIENTS,
877                    maximum: policy.resources.max_encryption_recipients,
878                    actual: encrypted_keys.len() + info.encrypted_keys.len(),
879                }
880                .into());
881            }
882            encrypted_keys.extend(info.encrypted_keys);
883            source_nodes.extend(info.source_nodes);
884            derived_keys.extend(info.derived_keys);
885            agreement_methods.extend(info.agreement_methods);
886            encapsulation_methods.extend(info.encapsulation_methods);
887        }
888    }
889    budget.ancestry.pop();
890    Ok(ParsedKeyInfo {
891        encapsulation_methods,
892        source_nodes,
893        key_name,
894        encrypted_keys,
895        derived_keys,
896        agreement_methods,
897    })
898}
899
900fn parse_encapsulation_key_info<'doc>(
901    node: Node<'doc, 'doc>,
902    budget: &mut KeySourceParseBudget<'_, 'doc, '_>,
903    depth: usize,
904) -> Result<crate::xmldsig::parse::KeyInfo, XmlEncError> {
905    use crate::xmldsig::parse::KeyInfoSource;
906    budget
907        .policy
908        .resources
909        .validate_key_info_reference_depth(depth)?;
910    if budget.ancestry.contains(&node.id()) {
911        return Err(XmlEncError::InvalidStructure(
912            "cyclic encryption KeyInfo reference".into(),
913        ));
914    }
915    budget.ancestry.push(node.id());
916    let mut info = budget
917        .shared
918        .key_info
919        .parse_with_provider_and_document_base(
920            node,
921            budget.provider,
922            budget.document_base.as_deref(),
923        )
924        .map_err(super::agreement::map_key_info_error)?;
925    if info
926        .sources
927        .iter()
928        .any(|source| matches!(source, KeyInfoSource::KeyInfoReference { .. }))
929    {
930        let mut sources = Vec::new();
931        for source in std::mem::take(&mut info.sources) {
932            if let KeyInfoSource::KeyInfoReference { uri } = source {
933                // XMLDSig 1.1 §4.5.10 also applies inside the mechanism's KeyInfo.
934                // Resolve before recipient selection, sharing ancestry and work.
935                // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-KeyInfoReference
936                let allowed = budget
937                    .policy
938                    .uris
939                    .map_or(crate::xmldsig::UriTypeSet::SAME_DOCUMENT, |uris| {
940                        uris.key_info_references
941                    });
942                if !allowed.allows(&uri) {
943                    return Err(crate::policy::PolicyViolation::Uri {
944                        operation: "encryption KeyInfoReference",
945                        reason: "URI class is disabled",
946                    }
947                    .into());
948                }
949                budget.charge()?;
950                budget
951                    .policy
952                    .resources
953                    .validate_key_info_reference_depth(depth + 1)?;
954                let referenced = if uri.is_empty() || uri.starts_with('#') {
955                    let target = budget.referenced_node(node, &uri)?;
956                    parse_encapsulation_key_info(target, budget, depth + 1)?
957                } else {
958                    let resource = uri
959                        .split_once('#')
960                        .map_or(uri.as_str(), |(resource, _)| resource);
961                    let fragment = &uri[resource.len()..];
962                    with_processed_key_reference(
963                        node,
964                        resource,
965                        allowed,
966                        &[],
967                        budget,
968                        |root, child| {
969                            let target = if fragment.is_empty() || fragment == "#" {
970                                root
971                            } else {
972                                child.referenced_node(root, fragment)?
973                            };
974                            // §4.5.10 requires KeyInfo, not arbitrary retrieved key data.
975                            require_element(target, XMLDSIG_NS, "KeyInfo")?;
976                            parse_encapsulation_key_info(target, child, depth + 1)
977                        },
978                    )?
979                };
980                sources.extend(referenced.sources);
981            } else {
982                sources.push(source);
983            }
984        }
985        info.sources = sources;
986    }
987    budget.ancestry.pop();
988    Ok(info)
989}
990
991enum RetrievedKey {
992    Encrypted(EncryptedKey),
993    Derived(super::DerivedKey),
994}
995
996#[derive(Clone, Copy)]
997enum RetrievedKeyType {
998    Encrypted,
999    Derived,
1000}
1001
1002impl RetrievedKeyType {
1003    fn parse(uri: &str) -> Result<Self, XmlEncError> {
1004        // XMLEnc 1.1 §3.5.2 specifies the xmlenc11 identifier, while §3.5.3
1005        // prints xmlenc. Accept the latter as an interoperability alias; both
1006        // still require an xenc11:DerivedKey target, including after transforms.
1007        // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-DerivedKey
1008        // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-RetrievalMethod
1009        match uri {
1010            "http://www.w3.org/2001/04/xmlenc#EncryptedKey" => Ok(Self::Encrypted),
1011            "http://www.w3.org/2009/xmlenc11#DerivedKey"
1012            | "http://www.w3.org/2001/04/xmlenc#DerivedKey" => Ok(Self::Derived),
1013            _ => Err(XmlEncError::InvalidStructure(
1014                "unsupported encryption RetrievalMethod Type".into(),
1015            )),
1016        }
1017    }
1018
1019    fn validate_target(self, node: Node<'_, '_>) -> Result<(), XmlEncError> {
1020        let (namespace, tag) = match self {
1021            Self::Encrypted => (XMLENC_NS, "EncryptedKey"),
1022            Self::Derived => (XMLENC11_NS, "DerivedKey"),
1023        };
1024        if node.has_tag_name((namespace, tag)) {
1025            Ok(())
1026        } else {
1027            Err(XmlEncError::InvalidStructure(format!(
1028                "RetrievalMethod Type disagrees with {tag} target"
1029            )))
1030        }
1031    }
1032}
1033
1034fn parse_processed_key_reference(
1035    source: Node<'_, '_>,
1036    uri: &str,
1037    transforms: &[crate::xmldsig::transforms::Transform],
1038    declared_type: Option<RetrievedKeyType>,
1039    allow_empty: bool,
1040    budget: &mut KeySourceParseBudget<'_, '_, '_>,
1041    depth: usize,
1042) -> Result<RetrievedKey, XmlEncError> {
1043    budget
1044        .policy
1045        .resources
1046        .validate_key_info_reference_depth(depth)?;
1047    let allowed = budget
1048        .policy
1049        .uris
1050        .map_or(crate::xmldsig::UriTypeSet::SAME_DOCUMENT, |uris| {
1051            uris.retrieval_methods
1052        });
1053    with_processed_key_reference(
1054        source,
1055        uri,
1056        allowed,
1057        transforms,
1058        budget,
1059        |target, child_budget| {
1060            if let Some(kind) = declared_type {
1061                kind.validate_target(target)?;
1062            }
1063            if target.has_tag_name((XMLENC11_NS, "DerivedKey")) {
1064                return Ok(RetrievedKey::Derived(super::derived_key::parse(
1065                    target,
1066                    child_budget.policy.resources,
1067                )?));
1068            }
1069            let mut key = parse_encrypted_key(
1070                target,
1071                child_budget.policy,
1072                allow_empty,
1073                child_budget,
1074                depth,
1075            )?;
1076            let context = child_budget
1077                .references
1078                .expect("processed reference context");
1079            let parse = child_budget
1080                .xml_parse
1081                .expect("processed reference parse budget");
1082            let bound = context
1083                .bind_document_with_base(target.document(), child_budget.document_base.as_deref());
1084            let mut origins = child_budget.origins.iter();
1085            super::decrypt::resolve_nested_cipher_references(
1086                core::slice::from_mut(&mut key),
1087                target.document(),
1088                &mut origins,
1089                &bound,
1090                parse,
1091            )?;
1092            if origins.next().is_some() {
1093                return Err(XmlEncError::OperationPlan(
1094                    "unused processed key origin".into(),
1095                ));
1096            }
1097            Ok(RetrievedKey::Encrypted(key))
1098        },
1099    )
1100}
1101
1102fn with_processed_key_reference<T>(
1103    source: Node<'_, '_>,
1104    uri: &str,
1105    allowed_uris: crate::xmldsig::UriTypeSet,
1106    transforms: &[crate::xmldsig::transforms::Transform],
1107    budget: &mut KeySourceParseBudget<'_, '_, '_>,
1108    consume: impl for<'doc> FnOnce(
1109        Node<'doc, 'doc>,
1110        &mut KeySourceParseBudget<'_, 'doc, '_>,
1111    ) -> Result<T, XmlEncError>,
1112) -> Result<T, XmlEncError> {
1113    use sha2::Digest as _;
1114    let policy = budget.policy;
1115    let context = budget.references.ok_or_else(|| {
1116        XmlEncError::InvalidStructure(
1117            "processed key retrieval requires an operation resource context".into(),
1118        )
1119    })?;
1120    let parse = budget
1121        .xml_parse
1122        .ok_or_else(|| XmlEncError::OperationPlan("missing key resource parse budget".into()))?;
1123    let (bytes, base) = context.resolve_key_reference(
1124        source,
1125        uri,
1126        allowed_uris,
1127        transforms,
1128        budget.document_base.as_deref(),
1129        parse,
1130    )?;
1131    policy.resources.validate_xml_document_len(bytes.len())?;
1132    // Equality is over the selected, bounded result, never a phase-local node
1133    // handle or a scan/hash of every unused caller resource.
1134    let fingerprint: [u8; 32] = sha2::Sha256::digest(&bytes).into();
1135    if budget
1136        .shared
1137        .reference_ancestry
1138        .iter()
1139        .any(|(identity, hash)| identity == &base && hash == &fingerprint)
1140    {
1141        return Err(XmlEncError::InvalidStructure(
1142            "cyclic processed encryption key reference".into(),
1143        ));
1144    }
1145    budget
1146        .shared
1147        .reference_ancestry
1148        .push((base.clone(), fingerprint));
1149    let settings = DocumentParseSettings::from_policy(policy.xml, policy.resources)
1150        .with_backend(context.backend());
1151    let xml = crate::document::decode_xml_with_budget(
1152        &bytes,
1153        policy.resources.max_xml_document_bytes,
1154        Some(parse),
1155    )
1156    .map_err(|error| map_document_error(error, settings))?;
1157    let document = parse_policy_document(&xml, policy, parse, context.backend())?;
1158    let mut child_budget = KeySourceParseBudget::new(
1159        budget.policy,
1160        budget.registrations,
1161        budget.provider,
1162        &mut *budget.shared,
1163    );
1164    child_budget.references = Some(context);
1165    child_budget.xml_parse = Some(parse);
1166    child_budget.document_base = base;
1167    let result = consume(document.root_element(), &mut child_budget)?;
1168    budget
1169        .origins
1170        .extend(child_budget.origins.iter().map(|_| None));
1171    budget.shared.reference_ancestry.pop();
1172    Ok(result)
1173}
1174
1175fn append_detached_keys<'doc>(
1176    node: Node<'doc, 'doc>,
1177    info: &mut ParsedKeyInfo,
1178    policy: ParsingPolicy<'_>,
1179    allow_empty_cipher_values: bool,
1180    budget: &mut KeySourceParseBudget<'_, 'doc, '_>,
1181    depth: usize,
1182) -> Result<(), XmlEncError> {
1183    // XMLEnc §§3.5.1, 3.5.2 and 3.6 bind detached transports/derivations to either
1184    // encrypted-object class. Inventory node IDs once, not the document once
1185    // per recursive key. Original nodes retain namespaces and provenance.
1186    // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-ReferenceList
1187    if budget.detached.is_none() {
1188        let mut candidates = Vec::new();
1189        let maximum = policy.resources.effective_xml_nodes() as usize;
1190        for (index, candidate) in node.document().descendants().enumerate() {
1191            // Inventory is bounded document work, not candidate execution.
1192            // Gate the scan before retaining any additional node identities.
1193            if index >= maximum {
1194                return Err(crate::policy::PolicyViolation::ResourceLimit {
1195                    resource: crate::policy::resource_name::XML_NODES,
1196                    maximum,
1197                    actual: index + 1,
1198                }
1199                .into());
1200            }
1201            if candidate.has_tag_name((XMLENC_NS, "EncryptedKey"))
1202                || candidate.has_tag_name((XMLENC11_NS, "DerivedKey"))
1203            {
1204                candidates.push(candidate.id());
1205            }
1206        }
1207        budget.detached = Some(candidates);
1208    }
1209    let count = budget
1210        .detached
1211        .as_ref()
1212        .expect("inventory initialized")
1213        .len();
1214    for index in 0..count {
1215        let id = budget.detached.as_ref().expect("inventory initialized")[index];
1216        let candidate = node
1217            .document()
1218            .get_node(id)
1219            .expect("inventory belongs to this document");
1220        let derived = candidate.has_tag_name((XMLENC11_NS, "DerivedKey"));
1221        let named = info.key_name.as_deref().is_some_and(|expected| {
1222            candidate
1223                .children()
1224                .find(|child| {
1225                    child.has_tag_name(if derived {
1226                        (XMLENC11_NS, "DerivedKeyName")
1227                    } else {
1228                        (XMLENC_NS, "CarriedKeyName")
1229                    })
1230                })
1231                .is_some_and(|label| {
1232                    !label.children().any(|child| child.is_element())
1233                        && label
1234                            .children()
1235                            .filter_map(|child| child.text())
1236                            .flat_map(str::bytes)
1237                            .eq(expected.bytes())
1238                })
1239        });
1240        let mut referenced = false;
1241        for list in candidate
1242            .children()
1243            .filter(|child| child.has_tag_name((XMLENC_NS, "ReferenceList")))
1244        {
1245            let data_target = node.has_tag_name((XMLENC_NS, "EncryptedData"));
1246            // Association is a borrowed, allocation-free prefilter, not full
1247            // validation of every key in the document. Errors in unrelated
1248            // metadata cannot invalidate the selected encrypted object.
1249            for child in list.children() {
1250                if !child.has_tag_name((
1251                    XMLENC_NS,
1252                    if data_target {
1253                        "DataReference"
1254                    } else {
1255                        "KeyReference"
1256                    },
1257                )) {
1258                    continue;
1259                }
1260                let Some(uri) = child.attribute("URI") else {
1261                    continue;
1262                };
1263                if !uri.is_empty() && !uri.starts_with('#') {
1264                    continue;
1265                }
1266                if uri.len() > policy.resources.max_encryption_metadata_bytes {
1267                    continue;
1268                }
1269                if budget
1270                    .resolver(node)
1271                    .same_document_reference_targets(uri, node.id())
1272                {
1273                    referenced = true;
1274                    break;
1275                }
1276            }
1277            if referenced {
1278                break;
1279            }
1280        }
1281        if !named && !referenced {
1282            continue;
1283        }
1284        // Once associated, validate the entire list, including malformed
1285        // siblings and URI grammar, before parsing or resolving key sources.
1286        for list in candidate
1287            .children()
1288            .filter(|child| child.has_tag_name((XMLENC_NS, "ReferenceList")))
1289        {
1290            visit_reference_list(list, policy.resources, |_, uri| {
1291                if uri.is_empty() || uri.starts_with('#') {
1292                    budget
1293                        .resolver(candidate)
1294                        .node_for_same_document_reference(uri)?;
1295                }
1296                Ok(())
1297            })?;
1298        }
1299        if budget.ancestry.contains(&id) {
1300            return Err(XmlEncError::InvalidStructure(
1301                "cyclic detached EncryptedKey association".into(),
1302            ));
1303        }
1304        if info.source_nodes.contains(&id) {
1305            continue;
1306        }
1307        if derived {
1308            budget.charge()?;
1309            info.source_nodes.push(id);
1310            info.derived_keys
1311                .push(super::derived_key::parse(candidate, policy.resources)?);
1312            continue;
1313        }
1314        budget.charge_recipient(policy.resources, info.encrypted_keys.len())?;
1315        info.source_nodes.push(id);
1316        info.encrypted_keys.push(parse_encrypted_key(
1317            candidate,
1318            policy,
1319            allow_empty_cipher_values,
1320            budget,
1321            depth + 1,
1322        )?);
1323    }
1324    Ok(())
1325}
1326
1327fn parse_encrypted_key<'doc>(
1328    node: Node<'doc, 'doc>,
1329    policy: ParsingPolicy<'_>,
1330    allow_empty_cipher_values: bool,
1331    budget: &mut KeySourceParseBudget<'_, 'doc, '_>,
1332    depth: usize,
1333) -> Result<EncryptedKey, XmlEncError> {
1334    require_element(node, XMLENC_NS, "EncryptedKey")?;
1335    // XMLEnc §3.5 allows EncryptedKey inside another key's KeyInfo. The
1336    // product indirection ceiling bounds both parsing and execution stacks.
1337    // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-Extensions-to-KeyInfo
1338    policy.resources.validate_key_info_reference_depth(depth)?;
1339    if budget.ancestry.contains(&node.id()) {
1340        return Err(XmlEncError::InvalidStructure(
1341            "cyclic EncryptedKey reference".into(),
1342        ));
1343    }
1344    budget.ancestry.push(node.id());
1345    budget.origins.push(Some(node.id()));
1346    budget.encrypted_key_depth += 1;
1347    validate_encrypted_type_attributes(node, policy)?;
1348    let mut children = element_children(node);
1349    let encryption_method = parse_encryption_method_with_limit(
1350        next_required(&mut children, "EncryptionMethod")?,
1351        policy.resources.max_encryption_metadata_bytes,
1352    )?;
1353    if children
1354        .peek()
1355        .is_some_and(|child| child.has_tag_name((XMLENC_NS, "EncryptionMethod")))
1356    {
1357        return Err(XmlEncError::InvalidStructure(
1358            "EncryptedKey contains more than one direct EncryptionMethod".into(),
1359        ));
1360    }
1361    let key_info_node = if children
1362        .peek()
1363        .is_some_and(|child| child.has_tag_name((XMLDSIG_NS, "KeyInfo")))
1364    {
1365        Some(next_required(&mut children, "KeyInfo")?)
1366    } else {
1367        None
1368    };
1369    if children
1370        .peek()
1371        .is_some_and(|child| child.has_tag_name((XMLDSIG_NS, "KeyInfo")))
1372    {
1373        return Err(XmlEncError::InvalidStructure(
1374            "EncryptedKey contains more than one direct KeyInfo".into(),
1375        ));
1376    }
1377    let cipher_data = parse_cipher_data(
1378        next_required(&mut children, "CipherData")?,
1379        allow_empty_cipher_values,
1380        policy,
1381        &mut budget.shared.transforms,
1382        &mut budget.shared.retained_cipher_bytes,
1383    )?;
1384    consume_encryption_properties(&mut children);
1385    let reference_list = if children
1386        .peek()
1387        .is_some_and(|child| child.has_tag_name((XMLENC_NS, "ReferenceList")))
1388    {
1389        Some(parse_reference_list(
1390            next_required(&mut children, "ReferenceList")?,
1391            policy,
1392        )?)
1393    } else {
1394        None
1395    };
1396    let carried_key_name = if children
1397        .peek()
1398        .is_some_and(|child| child.has_tag_name((XMLENC_NS, "CarriedKeyName")))
1399    {
1400        Some(parse_carried_key_name(
1401            next_required(&mut children, "CarriedKeyName")?,
1402            policy,
1403        )?)
1404    } else {
1405        None
1406    };
1407    if children.next().is_some() {
1408        return Err(XmlEncError::InvalidStructure(
1409            "EncryptedKey has unexpected child after CipherData".into(),
1410        ));
1411    }
1412    if let Some(references) = budget.references {
1413        // Unsupported unused recipients remain skippable. Preflight a key
1414        // method here only when its key source would initiate indirect work.
1415        if key_info_node.is_some_and(|info| {
1416            info.descendants().any(|child| {
1417                child.has_tag_name((XMLDSIG_NS, "RetrievalMethod"))
1418                    || child.has_tag_name((crate::xmldsig::parse::XMLDSIG11_NS, "KeyInfoReference"))
1419            })
1420        }) {
1421            references.validate_method(&encryption_method, false, budget.provider)?;
1422        }
1423        references.preflight_cipher_data(&cipher_data)?;
1424    }
1425    let mut key_info = match key_info_node {
1426        Some(key_info) => Some(parse_key_info(
1427            key_info,
1428            policy,
1429            allow_empty_cipher_values,
1430            budget,
1431            depth,
1432        )?),
1433        None => None,
1434    };
1435    let info = key_info.get_or_insert_with(|| ParsedKeyInfo {
1436        encapsulation_methods: Vec::new(),
1437        source_nodes: Vec::new(),
1438        key_name: None,
1439        encrypted_keys: Vec::new(),
1440        derived_keys: Vec::new(),
1441        agreement_methods: Vec::new(),
1442    });
1443    append_detached_keys(node, info, policy, allow_empty_cipher_values, budget, depth)?;
1444    budget.ancestry.pop();
1445    budget.encrypted_key_depth -= 1;
1446    Ok(EncryptedKey {
1447        sources: key_info
1448            .as_mut()
1449            .map(|info| super::EncryptionKeySources {
1450                encapsulation_methods: core::mem::take(&mut info.encapsulation_methods),
1451                encrypted_keys: core::mem::take(&mut info.encrypted_keys),
1452                derived_keys: core::mem::take(&mut info.derived_keys),
1453                agreement_methods: core::mem::take(&mut info.agreement_methods),
1454            })
1455            .unwrap_or_default(),
1456        id: bounded_attribute(node, "Id", policy)?,
1457        recipient: bounded_attribute(node, "Recipient", policy)?,
1458        key_name: key_info.and_then(|info| info.key_name),
1459        encryption_method,
1460        cipher_data,
1461        reference_list,
1462        carried_key_name,
1463    })
1464}
1465
1466fn parse_carried_key_name(
1467    node: Node<'_, '_>,
1468    policy: ParsingPolicy<'_>,
1469) -> Result<String, XmlEncError> {
1470    require_element(node, XMLENC_NS, "CarriedKeyName")?;
1471    let value = bounded_simple_text(node, "CarriedKeyName", policy)?;
1472    if value.is_empty() {
1473        return Err(XmlEncError::InvalidStructure(
1474            "CarriedKeyName is empty".into(),
1475        ));
1476    }
1477    Ok(value)
1478}
1479
1480fn parse_key_name(node: Node<'_, '_>, policy: ParsingPolicy<'_>) -> Result<String, XmlEncError> {
1481    let value = bounded_simple_text(node, "KeyName", policy)?;
1482    if value.is_empty() {
1483        return Err(XmlEncError::InvalidStructure("KeyName is empty".into()));
1484    }
1485    Ok(value)
1486}
1487
1488fn parse_reference_list(
1489    node: Node<'_, '_>,
1490    policy: ParsingPolicy<'_>,
1491) -> Result<ReferenceList, XmlEncError> {
1492    parse_reference_list_with_resources(node, policy.resources)
1493}
1494
1495pub(super) fn parse_reference_list_with_resources(
1496    node: Node<'_, '_>,
1497    resources: &crate::policy::ResourcePolicy,
1498) -> Result<ReferenceList, XmlEncError> {
1499    require_element(node, XMLENC_NS, "ReferenceList")?;
1500    let mut data_references = Vec::new();
1501    let mut key_references = Vec::new();
1502    visit_reference_list(node, resources, |data_reference, uri| {
1503        if data_reference {
1504            data_references.push(uri.to_owned());
1505        } else {
1506            key_references.push(uri.to_owned());
1507        }
1508        Ok(())
1509    })?;
1510    Ok(ReferenceList {
1511        data_references,
1512        key_references,
1513    })
1514}
1515
1516fn visit_reference_list(
1517    node: Node<'_, '_>,
1518    resources: &crate::policy::ResourcePolicy,
1519    mut visit: impl FnMut(bool, &str) -> Result<(), XmlEncError>,
1520) -> Result<(), XmlEncError> {
1521    require_element(node, XMLENC_NS, "ReferenceList")?;
1522    let mut count = 0;
1523    // XMLEnc 1.1 §3.6: an element-only choice of DataReference/KeyReference;
1524    // URI is required, but its anyURI type does not require a nonempty value.
1525    // Validate the choice before allocating its URI or consulting a resolver.
1526    // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-ReferenceList
1527    for child in node.children() {
1528        if child.is_text()
1529            && child.text().is_some_and(|text| {
1530                !text
1531                    .bytes()
1532                    .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
1533            })
1534        {
1535            return Err(XmlEncError::InvalidStructure(
1536                "ReferenceList contains character data".into(),
1537            ));
1538        }
1539        if !child.is_element() {
1540            continue;
1541        }
1542        let data_reference = match (child.tag_name().namespace(), child.tag_name().name()) {
1543            (Some(XMLENC_NS), "DataReference") => true,
1544            (Some(XMLENC_NS), "KeyReference") => false,
1545            _ => {
1546                return Err(XmlEncError::InvalidStructure(format!(
1547                    "unsupported ReferenceList child {}",
1548                    child.tag_name().name(),
1549                )));
1550            }
1551        };
1552        count += 1;
1553        if count > resources.max_references {
1554            return Err(crate::policy::PolicyViolation::ResourceLimit {
1555                resource: crate::policy::resource_name::ENCRYPTION_REFERENCES,
1556                maximum: resources.max_references,
1557                actual: count,
1558            }
1559            .into());
1560        }
1561        let uri = child
1562            .attribute("URI")
1563            .ok_or(XmlEncError::MissingRequired("Reference URI attribute"))?;
1564        validate_metadata_len(uri.len(), resources.max_encryption_metadata_bytes)?;
1565        visit(data_reference, uri)?;
1566    }
1567    if count == 0 {
1568        return Err(XmlEncError::InvalidStructure(
1569            "ReferenceList must contain at least one reference".into(),
1570        ));
1571    }
1572    Ok(())
1573}
1574
1575fn consume_encryption_properties<'a, I>(children: &mut std::iter::Peekable<I>)
1576where
1577    I: Iterator<Item = Node<'a, 'a>>,
1578{
1579    if children
1580        .peek()
1581        .is_some_and(|child| child.has_tag_name((XMLENC_NS, "EncryptionProperties")))
1582    {
1583        let _ = children.next();
1584    }
1585}
1586
1587#[cfg(test)]
1588fn parse_encryption_method(node: Node<'_, '_>) -> Result<EncryptionMethod, XmlEncError> {
1589    parse_encryption_method_with_limit(node, crate::hard_limits::ENCRYPTION_METADATA_BYTE_CEILING)
1590}
1591
1592fn parse_encryption_method_with_limit(
1593    node: Node<'_, '_>,
1594    metadata_limit: usize,
1595) -> Result<EncryptionMethod, XmlEncError> {
1596    require_element(node, XMLENC_NS, "EncryptionMethod")?;
1597    let algorithm = node
1598        .attribute("Algorithm")
1599        .ok_or(XmlEncError::MissingRequired(
1600            "EncryptionMethod Algorithm attribute",
1601        ))?;
1602    validate_metadata_len(algorithm.len(), metadata_limit)?;
1603    let algorithm = algorithm.to_owned();
1604
1605    let mut oaep_digest = None;
1606    let mut mgf_algorithm = None;
1607    let mut oaep_params = None;
1608    let mut key_size_bits = None;
1609    let mut chacha = None;
1610    for child in node.children().filter(Node::is_element) {
1611        match (child.tag_name().namespace(), child.tag_name().name()) {
1612            (Some(XMLENC_NS), "KeySize")
1613                if key_size_bits.is_none()
1614                    && oaep_params.is_none()
1615                    && oaep_digest.is_none()
1616                    && mgf_algorithm.is_none()
1617                    && chacha.is_none() =>
1618            {
1619                key_size_bits = Some(parse_key_size(child, metadata_limit)?);
1620            }
1621            (Some(XMLENC_NS), "OAEPparams") if oaep_params.is_none() => {
1622                oaep_params = Some(decode_bounded_base64_text(
1623                    child,
1624                    "OAEPparams",
1625                    metadata_limit,
1626                )?);
1627            }
1628            (Some(XMLDSIG_NS), "DigestMethod") if oaep_digest.is_none() => {
1629                let digest = child
1630                    .attribute("Algorithm")
1631                    .ok_or(XmlEncError::MissingRequired(
1632                        "DigestMethod Algorithm attribute",
1633                    ))?;
1634                validate_metadata_len(digest.len(), metadata_limit)?;
1635                oaep_digest = Some(digest.to_owned());
1636            }
1637            (Some(XMLENC11_NS), "MGF") if mgf_algorithm.is_none() => {
1638                let mgf = child
1639                    .attribute("Algorithm")
1640                    .ok_or(XmlEncError::MissingRequired("MGF Algorithm attribute"))?;
1641                validate_metadata_len(mgf.len(), metadata_limit)?;
1642                mgf_algorithm = Some(mgf.to_owned());
1643            }
1644            (
1645                None | Some("http://www.w3.org/2021/04/xmldsig-more#"),
1646                name @ ("Nonce" | "Counter" | "AAD"),
1647            ) => {
1648                // RFC 9231 §§2.6.7-2.6.8 uses unqualified parameters;
1649                // libxmlsec1 1.3.13 uses the namespaced draft form. Both share
1650                // validation, including duplicate detection across forms.
1651                // https://www.rfc-editor.org/rfc/rfc9231.html#section-2.6.7
1652                let parameters = chacha.get_or_insert_with(super::ChaChaParameters::default);
1653                match name {
1654                    "Nonce" if parameters.nonce.is_none() => {
1655                        parameters.nonce = Some(parse_chacha_hex(child, "Nonce", metadata_limit)?);
1656                    }
1657                    "Counter" if parameters.counter.is_none() => {
1658                        parameters.counter =
1659                            Some(parse_chacha_hex(child, "Counter", metadata_limit)?);
1660                    }
1661                    "AAD" if parameters.aad.is_none() => {
1662                        parameters.aad = Some(bounded_simple_text_with_limit(
1663                            child,
1664                            "AAD",
1665                            metadata_limit,
1666                        )?);
1667                    }
1668                    _ => {
1669                        return Err(XmlEncError::InvalidStructure(format!(
1670                            "duplicate ChaCha {name}"
1671                        )));
1672                    }
1673                }
1674            }
1675            _ => {
1676                return Err(XmlEncError::InvalidStructure(format!(
1677                    "unsupported EncryptionMethod child {}",
1678                    child.tag_name().name()
1679                )));
1680            }
1681        }
1682    }
1683
1684    if chacha.is_none() && algorithm == super::DataEncryptionAlgorithm::ChaCha20Poly1305.uri() {
1685        chacha = Some(super::ChaChaParameters::default());
1686    }
1687    let method = EncryptionMethod {
1688        algorithm,
1689        key_size_bits,
1690        oaep_digest,
1691        mgf_algorithm,
1692        oaep_params,
1693        chacha,
1694    };
1695    method.validate_structure()?;
1696    Ok(method)
1697}
1698
1699fn parse_chacha_hex<const N: usize>(
1700    node: Node<'_, '_>,
1701    field: &'static str,
1702    metadata_limit: usize,
1703) -> Result<[u8; N], XmlEncError> {
1704    let value = bounded_simple_text_with_limit(node, field, metadata_limit)?;
1705    let value = value.trim_matches([' ', '\t', '\r', '\n']);
1706    if value.len() != N * 2 {
1707        return Err(XmlEncError::InvalidStructure(format!(
1708            "ChaCha {field} requires {} hexadecimal digits",
1709            N * 2
1710        )));
1711    }
1712    let mut result = [0; N];
1713    for (byte, pair) in result.iter_mut().zip(value.as_bytes().as_chunks::<2>().0) {
1714        let digit = |value: u8| match value {
1715            b'0'..=b'9' => Some(value - b'0'),
1716            b'a'..=b'f' => Some(value - b'a' + 10),
1717            b'A'..=b'F' => Some(value - b'A' + 10),
1718            _ => None,
1719        };
1720        *byte = digit(pair[0])
1721            .zip(digit(pair[1]))
1722            .map(|(high, low)| high * 16 + low)
1723            .ok_or_else(|| {
1724                XmlEncError::InvalidStructure(format!("invalid ChaCha {field} hexBinary"))
1725            })?;
1726    }
1727    Ok(result)
1728}
1729
1730fn parse_key_size(node: Node<'_, '_>, metadata_limit: usize) -> Result<usize, XmlEncError> {
1731    let value = bounded_simple_text_with_limit(node, "KeySize", metadata_limit)?;
1732    let value = value.trim();
1733    let bits = value
1734        .parse::<usize>()
1735        .map_err(|_| XmlEncError::InvalidStructure("KeySize must be a positive integer".into()))?;
1736    if bits == 0 {
1737        return Err(XmlEncError::InvalidStructure(
1738            "KeySize must be a positive integer".into(),
1739        ));
1740    }
1741    Ok(bits)
1742}
1743
1744fn parse_cipher_data(
1745    node: Node<'_, '_>,
1746    allow_empty: bool,
1747    policy: ParsingPolicy<'_>,
1748    transform_budget: &mut crate::xmldsig::transforms::XPathSignatureParseBudget,
1749    retained_cipher_bytes: &mut usize,
1750) -> Result<CipherData, XmlEncError> {
1751    require_element(node, XMLENC_NS, "CipherData")?;
1752    let mut children = element_children(node);
1753    let value = next_required(&mut children, "CipherValue")?;
1754    if children.next().is_some() {
1755        return Err(XmlEncError::InvalidStructure(
1756            "CipherData must contain exactly one CipherValue or CipherReference".into(),
1757        ));
1758    }
1759    // XMLEnc 1.1 section 3.3 defines a choice, not two optional siblings.
1760    // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-CipherData
1761    if value.has_tag_name((XMLENC_NS, "CipherReference")) {
1762        let (uri, transforms) =
1763            super::cipher_reference::parse_reference(value, policy.resources, transform_budget)?;
1764        return Ok(CipherData::Reference {
1765            uri: uri.to_owned(),
1766            transforms,
1767        });
1768    }
1769    require_element(value, XMLENC_NS, "CipherValue")?;
1770    // XSD 1.0 section 3.2.16 permits only XML whitespace in base64Binary.
1771    // Borrow text across comments/CDATA; validate before retaining one buffer.
1772    // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#base64Binary
1773    let payload = crate::xmldsig::whitespace::XmlBase64Payload::bounded(
1774        value,
1775        usize::MAX,
1776        MAX_CIPHER_VALUE_BASE64_LEN,
1777    )
1778    .map_err(|reason| {
1779        if reason == "unexpected nested element" {
1780            XmlEncError::InvalidStructure("CipherValue must not contain element children".into())
1781        } else {
1782            XmlEncError::Base64(format!("CipherValue: {reason}"))
1783        }
1784    })?;
1785    if payload.normalized_len == 0 && !allow_empty {
1786        return Err(XmlEncError::Base64("CipherValue is empty".into()));
1787    }
1788    let maximum = policy.resources.max_xml_document_bytes;
1789    if payload.normalized_len > maximum - *retained_cipher_bytes {
1790        return Err(crate::policy::PolicyViolation::ResourceLimit {
1791            resource: crate::policy::resource_name::AGGREGATE_ENCRYPTION_CIPHER_VALUE_BYTES,
1792            maximum,
1793            actual: retained_cipher_bytes.saturating_add(payload.normalized_len),
1794        }
1795        .into());
1796    }
1797    *retained_cipher_bytes += payload.normalized_len;
1798    Ok(CipherData::Value {
1799        value: payload
1800            .normalized()
1801            .map_err(|reason| XmlEncError::Base64(format!("CipherValue: {reason}")))?,
1802    })
1803}
1804
1805fn bounded_simple_text(
1806    node: Node<'_, '_>,
1807    field: &'static str,
1808    policy: ParsingPolicy<'_>,
1809) -> Result<String, XmlEncError> {
1810    bounded_simple_text_with_limit(node, field, policy.resources.max_encryption_metadata_bytes)
1811}
1812
1813pub(super) fn bounded_simple_text_with_limit(
1814    node: Node<'_, '_>,
1815    field: &'static str,
1816    maximum: usize,
1817) -> Result<String, XmlEncError> {
1818    if node.children().any(|child| child.is_element()) {
1819        return Err(XmlEncError::InvalidStructure(format!(
1820            "{field} must not contain element children"
1821        )));
1822    }
1823    let mut value = String::new();
1824    for text in node
1825        .children()
1826        .filter(Node::is_text)
1827        .filter_map(|child| child.text())
1828    {
1829        let actual = value.len().saturating_add(text.len());
1830        validate_metadata_len(actual, maximum)?;
1831        value.push_str(text);
1832    }
1833    Ok(value)
1834}
1835
1836fn bounded_attribute(
1837    node: Node<'_, '_>,
1838    attribute: &str,
1839    policy: ParsingPolicy<'_>,
1840) -> Result<Option<String>, XmlEncError> {
1841    let Some(value) = node.attribute(attribute) else {
1842        return Ok(None);
1843    };
1844    validate_metadata_len(value.len(), policy.resources.max_encryption_metadata_bytes)?;
1845    Ok(Some(value.to_owned()))
1846}
1847
1848pub(super) fn validate_metadata_len(actual: usize, maximum: usize) -> Result<(), XmlEncError> {
1849    if actual <= maximum {
1850        Ok(())
1851    } else {
1852        Err(crate::policy::PolicyViolation::ResourceLimit {
1853            resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
1854            maximum,
1855            actual,
1856        }
1857        .into())
1858    }
1859}
1860
1861pub(super) fn validate_encrypted_data_metadata<'a>(
1862    encrypted: &EncryptedData,
1863    policy: impl Into<ParsingPolicy<'a>>,
1864) -> Result<(), XmlEncError> {
1865    let policy = policy.into();
1866    validate_encrypted_data_metadata_inner(encrypted, policy, false)
1867}
1868
1869fn validate_encrypted_data_metadata_inner(
1870    encrypted: &EncryptedData,
1871    policy: ParsingPolicy<'_>,
1872    template: bool,
1873) -> Result<(), XmlEncError> {
1874    let maximum = policy.resources.max_encryption_metadata_bytes;
1875    let validate = |value: Option<&str>| validate_metadata_len(value.map_or(0, str::len), maximum);
1876    validate(encrypted.id.as_deref())?;
1877    if let Some(encrypted_type) = encrypted.encrypted_type.as_ref() {
1878        let value = match encrypted_type {
1879            EncryptedDataType::Element => "http://www.w3.org/2001/04/xmlenc#Element",
1880            EncryptedDataType::Content => "http://www.w3.org/2001/04/xmlenc#Content",
1881            EncryptedDataType::Other(value) => value,
1882        };
1883        validate(Some(value))?;
1884    }
1885    validate(encrypted.key_name.as_deref())?;
1886    validate_encryption_method_metadata(&encrypted.encryption_method, maximum)?;
1887    if !encrypted.encapsulation_methods.is_empty() {
1888        policy
1889            .key_establishment
1890            .check_kem_content(super::DataEncryptionAlgorithm::from_uri(
1891                &encrypted.encryption_method.algorithm,
1892            )?)?;
1893    }
1894    validate_key_sources(
1895        &encrypted.encrypted_keys,
1896        &encrypted.derived_keys,
1897        &encrypted.agreement_methods,
1898        &encrypted.encapsulation_methods,
1899        KeySourceValidation { policy, template },
1900        0,
1901        &mut 0,
1902    )
1903}
1904
1905#[derive(Clone, Copy)]
1906struct KeySourceValidation<'a> {
1907    policy: ParsingPolicy<'a>,
1908    template: bool,
1909}
1910
1911fn validate_key_sources(
1912    keys: &[EncryptedKey],
1913    derived: &[super::DerivedKey],
1914    agreements: &[super::AgreementMethod],
1915    encapsulations: &[crate::key_establishment::EncapsulationMechanism],
1916    validation: KeySourceValidation<'_>,
1917    depth: usize,
1918    count: &mut usize,
1919) -> Result<(), XmlEncError> {
1920    let KeySourceValidation { policy, template } = validation;
1921    policy.resources.validate_key_info_reference_depth(depth)?;
1922    let actual = count
1923        .saturating_add(keys.len())
1924        .saturating_add(derived.len())
1925        .saturating_add(agreements.len());
1926    let actual = actual.saturating_add(encapsulations.len());
1927    policy.resources.validate_key_candidates(actual)?;
1928    *count = actual;
1929    let maximum = policy.resources.max_encryption_metadata_bytes;
1930    let validate = |value: Option<&str>| validate_metadata_len(value.map_or(0, str::len), maximum);
1931    for descriptor in encapsulations {
1932        policy
1933            .key_establishment
1934            .check_encapsulation(descriptor.algorithm)?;
1935        if descriptor.ciphertext.len() != descriptor.algorithm.ciphertext_len()
1936            && !(template && descriptor.ciphertext.is_empty())
1937        {
1938            // RFC 9935 Appendix B fixes ciphertext width per parameter set.
1939            // This shared metadata preflight also gates caller-constructed
1940            // descriptors before resolver dispatch, not only XML parsing.
1941            // https://www.rfc-editor.org/rfc/rfc9935.html#appendix-B
1942            return Err(crate::key_establishment::KeyEstablishmentError::Structure(
1943                "ciphertext size does not match Algorithm",
1944            )
1945            .into());
1946        }
1947        super::agreement::validate_role_metadata(&descriptor.key_info, maximum)?;
1948        *count = count.saturating_add(descriptor.key_info.sources.len());
1949        policy.resources.validate_key_candidates(*count)?;
1950    }
1951    for descriptor in agreements {
1952        validate(Some(descriptor.algorithm.uri()))?;
1953        validate(descriptor.legacy_digest.as_deref())?;
1954        validate_metadata_len(descriptor.nonce.len(), maximum)?;
1955        for role in descriptor.originator.iter().chain(&descriptor.recipient) {
1956            super::agreement::validate_role_metadata(role, maximum)?;
1957            *count = count.saturating_add(role.embedded_candidate_count());
1958            policy.resources.validate_key_candidates(*count)?;
1959        }
1960        if let Some(method) = &descriptor.method {
1961            method.validate_metadata(maximum)?;
1962        }
1963    }
1964    for descriptor in derived {
1965        validate(descriptor.id.as_deref())?;
1966        validate(descriptor.recipient.as_deref())?;
1967        validate(descriptor.key_type.as_deref())?;
1968        validate(descriptor.derived_key_name.as_deref())?;
1969        validate(descriptor.master_key_name.as_deref())?;
1970        if let Some(method) = &descriptor.method {
1971            method.validate_metadata(maximum)?;
1972        }
1973        if let Some(list) = &descriptor.reference_list {
1974            validate_reference_list(list, policy.resources)?;
1975        }
1976    }
1977    for key in keys {
1978        validate(key.id.as_deref())?;
1979        validate(key.recipient.as_deref())?;
1980        validate(key.key_name.as_deref())?;
1981        validate(key.carried_key_name.as_deref())?;
1982        validate_encryption_method_metadata(&key.encryption_method, maximum)?;
1983        if let Some(references) = key.reference_list.as_ref() {
1984            validate_reference_list(references, policy.resources)?;
1985        }
1986        validate_key_sources(
1987            &key.sources.encrypted_keys,
1988            &key.sources.derived_keys,
1989            &key.sources.agreement_methods,
1990            &key.sources.encapsulation_methods,
1991            validation,
1992            depth + 1,
1993            count,
1994        )?;
1995    }
1996    Ok(())
1997}
1998
1999fn validate_reference_list(
2000    references: &ReferenceList,
2001    resources: &crate::policy::ResourcePolicy,
2002) -> Result<(), XmlEncError> {
2003    let count = references
2004        .data_references
2005        .len()
2006        .saturating_add(references.key_references.len());
2007    if count > resources.max_references {
2008        return Err(crate::policy::PolicyViolation::ResourceLimit {
2009            resource: crate::policy::resource_name::ENCRYPTION_REFERENCES,
2010            maximum: resources.max_references,
2011            actual: count,
2012        }
2013        .into());
2014    }
2015    if count == 0 {
2016        return Err(XmlEncError::InvalidStructure(
2017            "ReferenceList must contain at least one reference".into(),
2018        ));
2019    }
2020    for uri in references
2021        .data_references
2022        .iter()
2023        .chain(&references.key_references)
2024    {
2025        validate_metadata_len(uri.len(), resources.max_encryption_metadata_bytes)?;
2026    }
2027    Ok(())
2028}
2029
2030fn validate_encryption_method_metadata(
2031    method: &EncryptionMethod,
2032    maximum: usize,
2033) -> Result<(), XmlEncError> {
2034    validate_metadata_len(method.algorithm.len(), maximum)?;
2035    if let Some(value) = method.oaep_digest.as_deref() {
2036        validate_metadata_len(value.len(), maximum)?;
2037    }
2038    if let Some(value) = method.mgf_algorithm.as_deref() {
2039        validate_metadata_len(value.len(), maximum)?;
2040    }
2041    if let Some(value) = method.oaep_params.as_deref() {
2042        validate_metadata_len(value.len(), maximum)?;
2043    }
2044    if let Some(parameters) = &method.chacha {
2045        if parameters.nonce.is_some() {
2046            validate_metadata_len(12, maximum)?;
2047        }
2048        if parameters.counter.is_some() {
2049            validate_metadata_len(4, maximum)?;
2050        }
2051        if let Some(aad) = parameters.aad.as_deref() {
2052            validate_metadata_len(aad.len(), maximum)?;
2053        }
2054    }
2055    Ok(())
2056}
2057
2058fn validate_encrypted_type_attributes(
2059    node: Node<'_, '_>,
2060    policy: ParsingPolicy<'_>,
2061) -> Result<(), XmlEncError> {
2062    // Both EncryptedData and EncryptedKey derive these attributes from the XML
2063    // Encryption EncryptedType schema. Template mutation preserves attributes
2064    // that are not represented in the cryptographic model, so bound them here.
2065    for attribute in ["Id", "Recipient", "Type", "MimeType", "Encoding"] {
2066        if let Some(value) = node.attribute(attribute) {
2067            validate_metadata_len(value.len(), policy.resources.max_encryption_metadata_bytes)?;
2068        }
2069    }
2070    Ok(())
2071}
2072
2073fn parse_encrypted_data_type(value: Option<&str>) -> Option<EncryptedDataType> {
2074    value.map(|value| match value {
2075        "http://www.w3.org/2001/04/xmlenc#Element" => EncryptedDataType::Element,
2076        "http://www.w3.org/2001/04/xmlenc#Content" => EncryptedDataType::Content,
2077        other => EncryptedDataType::Other(other.to_owned()),
2078    })
2079}
2080
2081pub(super) fn decode_bounded_base64_text(
2082    node: Node<'_, '_>,
2083    field: &'static str,
2084    maximum: usize,
2085) -> Result<Vec<u8>, XmlEncError> {
2086    if node.children().any(|child| child.is_element()) {
2087        return Err(XmlEncError::InvalidStructure(format!(
2088            "{field} must not contain element children"
2089        )));
2090    }
2091    let encoded_limit = maximum.div_ceil(3).saturating_mul(4);
2092    // XSD Datatypes 1.0 §3.2.16 permits only XML whitespace in base64Binary.
2093    // Borrow fragmented text; reject size before allocating the exact output.
2094    // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#base64Binary
2095    let payload =
2096        crate::xmldsig::whitespace::XmlBase64Payload::bounded(node, usize::MAX, encoded_limit)
2097            .map_err(|reason| {
2098                if reason == "maximum allowed base64 length" {
2099                    XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2100                        resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
2101                        maximum,
2102                        actual: maximum.saturating_add(1),
2103                    })
2104                } else {
2105                    XmlEncError::Base64(format!("{field}: {reason}"))
2106                }
2107            })?;
2108    validate_metadata_len(payload.decoded_len, maximum)?;
2109    payload
2110        .decode()
2111        .map_err(|reason| XmlEncError::Base64(format!("{field}: {reason}")))
2112}
2113
2114#[cfg(test)]
2115fn normalize_base64(value: &str) -> Result<String, XmlEncError> {
2116    // Exercise the production node parser, not a second normalization algorithm.
2117    let xml =
2118        format!("<CipherData xmlns='{XMLENC_NS}'><CipherValue>{value}</CipherValue></CipherData>");
2119    let document = Document::parse(&xml).expect("base64 test text forms XML");
2120    let policy = crate::policy::DecryptionPolicy::default();
2121    let mut budget =
2122        crate::xmldsig::transforms::XPathSignatureParseBudget::from_resources(&policy.resources);
2123    let CipherData::Value { value } = parse_cipher_data(
2124        document.root_element(),
2125        false,
2126        (&policy).into(),
2127        &mut budget,
2128        &mut 0,
2129    )?
2130    else {
2131        unreachable!("inline fixture")
2132    };
2133    Ok(value)
2134}
2135
2136pub(super) fn require_element(
2137    node: Node<'_, '_>,
2138    namespace: &str,
2139    name: &str,
2140) -> Result<(), XmlEncError> {
2141    if node.has_tag_name((namespace, name)) {
2142        Ok(())
2143    } else {
2144        Err(XmlEncError::InvalidStructure(format!(
2145            "expected {{{namespace}}}{name}"
2146        )))
2147    }
2148}
2149
2150fn element_children<'a>(
2151    node: Node<'a, 'a>,
2152) -> std::iter::Peekable<impl Iterator<Item = Node<'a, 'a>>> {
2153    node.children().filter(Node::is_element).peekable()
2154}
2155
2156fn next_required<'a, I>(
2157    children: &mut std::iter::Peekable<I>,
2158    expected: &'static str,
2159) -> Result<Node<'a, 'a>, XmlEncError>
2160where
2161    I: Iterator<Item = Node<'a, 'a>>,
2162{
2163    children
2164        .next()
2165        .ok_or(XmlEncError::MissingRequired(expected))
2166}
2167
2168#[cfg(test)]
2169mod tests {
2170    use super::*;
2171
2172    #[test]
2173    fn chacha_accepts_rfc9231_parameter_elements() {
2174        // RFC 9231 §§2.6.7-2.6.8 examples use unqualified parameter children.
2175        for (algorithm, tail) in [
2176            ("chacha20", "<Counter>01000000</Counter>"),
2177            ("chacha20poly1305", "<AAD>payload</AAD>"),
2178        ] {
2179            let xml = format!(
2180                "<x:EncryptionMethod xmlns:x='{XMLENC_NS}' Algorithm='http://www.w3.org/2021/04/xmldsig-more#{algorithm}'><Nonce>000102030405060708090a0b</Nonce>{tail}</x:EncryptionMethod>"
2181            );
2182            let document = Document::parse(&xml).expect("RFC parameter XML");
2183            let method = parse_encryption_method_with_limit(document.root_element(), 128)
2184                .expect("RFC parameter grammar must be accepted");
2185            assert_eq!(
2186                method
2187                    .chacha
2188                    .as_ref()
2189                    .expect("parsed ChaCha parameters")
2190                    .nonce,
2191                Some([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
2192            );
2193        }
2194    }
2195
2196    #[test]
2197    fn chacha_namespace_aliases_do_not_bypass_duplicate_validation() {
2198        // Aliases share one parameter state; foreign namespaces are not aliases.
2199        for child in [
2200            "<c:Nonce>000102030405060708090a0b</c:Nonce>",
2201            "<foreign:AAD xmlns:foreign='urn:foreign'>payload</foreign:AAD>",
2202        ] {
2203            let xml = format!(
2204                "<x:EncryptionMethod xmlns:x='{XMLENC_NS}' xmlns:c='http://www.w3.org/2021/04/xmldsig-more#' Algorithm='http://www.w3.org/2021/04/xmldsig-more#chacha20poly1305'><Nonce>000102030405060708090a0b</Nonce>{child}</x:EncryptionMethod>"
2205            );
2206            let document = Document::parse(&xml).expect("well-formed namespace regression XML");
2207            assert!(matches!(
2208                parse_encryption_method_with_limit(document.root_element(), 128),
2209                Err(XmlEncError::InvalidStructure(_))
2210            ));
2211        }
2212    }
2213
2214    const DATA: &str = "<xenc:EncryptedData xmlns:xenc=\"http://www.w3.org/2001/04/xmlenc#\" Type=\"http://www.w3.org/2001/04/xmlenc#Element\"><xenc:EncryptionMethod Algorithm=\"http://www.w3.org/2009/xmlenc11#aes128-gcm\"/><xenc:CipherData><xenc:CipherValue> YWJj\nZA== </xenc:CipherValue></xenc:CipherData></xenc:EncryptedData>";
2215
2216    #[test]
2217    fn chacha_parameter_syntax_rejects_malformed_and_cross_profile_values() {
2218        // Parsing enforces the experimental profile before key resolution:
2219        // duplicate fields, malformed hex, nested text and cross-profile
2220        // Counter/AAD cannot reach cryptographic dispatch.
2221        let nonce = "<c:Nonce>000102030405060708090a0b</c:Nonce>";
2222        let counter = "<c:Counter>01000000</c:Counter>";
2223        for (algorithm, children) in [
2224            ("chacha20", format!("{nonce}{counter}{nonce}")),
2225            ("chacha20", format!("{nonce}{counter}{counter}")),
2226            ("chacha20", format!("{nonce}{counter}<c:AAD/>")),
2227            (
2228                "chacha20",
2229                format!("{nonce}<c:Counter>0100000z</c:Counter>"),
2230            ),
2231            ("chacha20", format!("{nonce}<c:Counter>010000</c:Counter>")),
2232            (
2233                "chacha20",
2234                format!("<c:Nonce>000102030405060708090a</c:Nonce>{counter}"),
2235            ),
2236            (
2237                "chacha20",
2238                format!("<c:Nonce><c:Nonce/></c:Nonce>{counter}"),
2239            ),
2240            ("chacha20poly1305", format!("{nonce}{counter}")),
2241            ("chacha20poly1305", format!("{nonce}<c:AAD/><c:AAD/>")),
2242            (
2243                "chacha20poly1305",
2244                format!("{nonce}<c:AAD><c:AAD/></c:AAD>"),
2245            ),
2246        ] {
2247            let xml = format!(
2248                "<x:EncryptionMethod xmlns:x='{XMLENC_NS}' xmlns:c='http://www.w3.org/2021/04/xmldsig-more#' Algorithm='http://www.w3.org/2021/04/xmldsig-more#{algorithm}'>{children}</x:EncryptionMethod>"
2249            );
2250            let document = Document::parse(&xml).expect("well-formed malformed-parameter case");
2251            assert!(
2252                matches!(
2253                    parse_encryption_method_with_limit(document.root_element(), 1024),
2254                    Err(XmlEncError::InvalidStructure(_))
2255                ),
2256                "{algorithm}: {children}"
2257            );
2258        }
2259    }
2260
2261    #[test]
2262    fn chacha_metadata_budget_and_xml_text_are_preserved() {
2263        // AAD is XML text, including whitespace and decoded entities. Bound
2264        // it as metadata rather than silently trimming or base64-decoding it.
2265        let aad = "  &lt;payload&gt;\n";
2266        let xml = format!(
2267            "<x:EncryptionMethod xmlns:x='{XMLENC_NS}' xmlns:c='http://www.w3.org/2021/04/xmldsig-more#' Algorithm='http://www.w3.org/2021/04/xmldsig-more#chacha20poly1305'><c:Nonce> 000102030405060708090a0b </c:Nonce><c:AAD>{aad}</c:AAD></x:EncryptionMethod>"
2268        );
2269        let document = Document::parse(&xml).expect("AAD XML");
2270        let parsed =
2271            parse_encryption_method_with_limit(document.root_element(), 128).expect("bounded AAD");
2272        let parameters = parsed.chacha.expect("ChaCha metadata");
2273        assert_eq!(parameters.aad.as_deref(), Some("  <payload>\n"));
2274        assert_eq!(
2275            parameters.nonce,
2276            Some([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
2277        );
2278        let oversized = xml.replace(aad, &"a".repeat(129));
2279        let document = Document::parse(&oversized).expect("oversized AAD XML");
2280        assert!(matches!(
2281            parse_encryption_method_with_limit(document.root_element(), 128),
2282            Err(XmlEncError::Policy(
2283                crate::policy::PolicyViolation::ResourceLimit {
2284                    maximum: 128,
2285                    actual: 129,
2286                    ..
2287                }
2288            ))
2289        ));
2290    }
2291
2292    #[test]
2293    fn parses_supported_encrypted_data_and_normalizes_cipher_value() {
2294        // XML base64 permits line wrapping, but the retained value must be canonical.
2295        let parsed = parse_encrypted_data(DATA).expect("valid XMLEnc data must parse");
2296        assert_eq!(parsed.cipher_data.inline_value(), Some("YWJjZA=="));
2297        assert_eq!(parsed.encrypted_type, Some(EncryptedDataType::Element));
2298    }
2299
2300    #[cfg(feature = "xml-backend-differential")]
2301    #[test]
2302    fn node_revalidation_uses_the_operation_backend() {
2303        // A node selected from an operation-scoped document must not silently
2304        // switch to the build default when its containing XML is revalidated.
2305        let backend = crate::XmlBackend::Roxmltree;
2306        let document = Document::parse_with_backend(DATA, backend).expect("test XML must parse");
2307        let expected_work = DATA.len() * 3;
2308        let resources = crate::policy::ResourcePolicy {
2309            max_xml_parse_work_bytes: expected_work,
2310            ..crate::policy::ResourcePolicy::default()
2311        };
2312        let policy = crate::policy::DecryptionPolicy {
2313            resources: resources.clone(),
2314            ..crate::policy::DecryptionPolicy::default()
2315        };
2316        let budget = XmlParseWorkBudget::from_resources(&resources);
2317
2318        parse_encrypted_data_node_with_context_and_budget(
2319            document.root_element(),
2320            &policy,
2321            &budget,
2322            backend,
2323            crate::provider::default_provider(),
2324            &[],
2325        )
2326        .expect("node revalidation must retain the selected backend");
2327        assert_eq!(budget.consumed(), expected_work);
2328
2329        parse_encrypted_data_node_with_policy_and_backend(
2330            document.root_element(),
2331            &policy,
2332            backend,
2333        )
2334        .expect("public decryption parser must retain the selected backend");
2335        let encryption_policy = crate::policy::EncryptionPolicy {
2336            resources,
2337            ..crate::policy::EncryptionPolicy::default()
2338        };
2339        parse_encrypted_data_template_node_with_policy_and_backend(
2340            document.root_element(),
2341            &encryption_policy,
2342            backend,
2343        )
2344        .expect("public template parser must retain the selected backend");
2345    }
2346
2347    #[test]
2348    fn node_parsers_enforce_the_containing_document_byte_limit() {
2349        // A caller-selected node retains its complete source document. Passing a
2350        // small subtree must not bypass the operation's document-byte ceiling.
2351        let containing = format!("<root>{}<payload/></root>", DATA);
2352        let document = Document::parse(&containing).expect("containing document must parse");
2353        let encrypted_data = document
2354            .descendants()
2355            .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData")))
2356            .expect("selected EncryptedData");
2357        let resources = crate::policy::ResourcePolicy {
2358            max_xml_document_bytes: DATA.len(),
2359            ..crate::policy::ResourcePolicy::default()
2360        };
2361        let decryption = crate::policy::DecryptionPolicy {
2362            resources: resources.clone(),
2363            ..crate::policy::DecryptionPolicy::default()
2364        };
2365        let encryption = crate::policy::EncryptionPolicy {
2366            resources,
2367            ..crate::policy::EncryptionPolicy::default()
2368        };
2369
2370        for result in [
2371            parse_encrypted_data_node_with_policy(encrypted_data, &decryption),
2372            parse_encrypted_data_template_node_with_policy(encrypted_data, &encryption),
2373        ] {
2374            assert!(matches!(
2375                result,
2376                Err(XmlEncError::Policy(
2377                    crate::policy::PolicyViolation::ResourceLimit {
2378                        resource: crate::policy::resource_name::XML_DOCUMENT,
2379                        ..
2380                    }
2381                ))
2382            ));
2383        }
2384    }
2385
2386    #[test]
2387    fn node_parsers_enforce_the_containing_document_node_limit() {
2388        // A selected EncryptedData subtree must not hide sibling nodes from the
2389        // immutable resource policy supplied for the containing document.
2390        let containing = format!("<root>{}<payload/></root>", DATA);
2391        let document = Document::parse(&containing).expect("containing document must parse");
2392        let encrypted_data = document
2393            .descendants()
2394            .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData")))
2395            .expect("selected EncryptedData");
2396        let actual_nodes = document.root().descendants().count();
2397        let resources = crate::policy::ResourcePolicy {
2398            max_xml_nodes: actual_nodes - 1,
2399            ..crate::policy::ResourcePolicy::default()
2400        };
2401        let decryption = crate::policy::DecryptionPolicy {
2402            resources: resources.clone(),
2403            ..crate::policy::DecryptionPolicy::default()
2404        };
2405        let encryption = crate::policy::EncryptionPolicy {
2406            resources,
2407            ..crate::policy::EncryptionPolicy::default()
2408        };
2409
2410        for result in [
2411            parse_encrypted_data_node_with_policy(encrypted_data, &decryption),
2412            parse_encrypted_data_template_node_with_policy(encrypted_data, &encryption),
2413        ] {
2414            assert!(matches!(
2415                result,
2416                Err(XmlEncError::Policy(
2417                    crate::policy::PolicyViolation::ResourceLimit {
2418                        resource: "XML nodes",
2419                        maximum,
2420                        actual,
2421                    }
2422                )) if maximum == actual_nodes - 1 && actual == actual_nodes
2423            ));
2424        }
2425
2426        let exact_resources = crate::policy::ResourcePolicy {
2427            max_xml_nodes: actual_nodes,
2428            ..crate::policy::ResourcePolicy::default()
2429        };
2430        let exact_policy = crate::policy::DecryptionPolicy {
2431            resources: exact_resources,
2432            ..crate::policy::DecryptionPolicy::default()
2433        };
2434        parse_encrypted_data_node_with_policy(encrypted_data, &exact_policy)
2435            .expect("a document exactly at the node ceiling must parse");
2436    }
2437
2438    #[test]
2439    fn policy_parsers_enforce_the_complete_document_depth() {
2440        // Both borrowed-node entry points revalidate the containing document;
2441        // selecting a shallow EncryptedData subtree must not hide deep ancestors.
2442        let containing = format!("<outer><inner>{DATA}</inner></outer>");
2443        let document = Document::parse(&containing).expect("containing document must parse");
2444        let encrypted_data = document
2445            .descendants()
2446            .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData")))
2447            .expect("selected EncryptedData");
2448        let actual_depth = encrypted_data
2449            .document()
2450            .descendants()
2451            .filter(|node| node.is_element())
2452            .map(|node| {
2453                node.ancestors()
2454                    .filter(|ancestor| ancestor.is_element())
2455                    .count()
2456            })
2457            .max()
2458            .expect("fixture has elements");
2459        let resources = crate::policy::ResourcePolicy {
2460            max_xml_depth: actual_depth - 1,
2461            ..crate::policy::ResourcePolicy::default()
2462        };
2463        let decryption = crate::policy::DecryptionPolicy {
2464            resources: resources.clone(),
2465            ..crate::policy::DecryptionPolicy::default()
2466        };
2467        let encryption = crate::policy::EncryptionPolicy {
2468            resources,
2469            ..crate::policy::EncryptionPolicy::default()
2470        };
2471
2472        for result in [
2473            parse_encrypted_data_node_with_policy(encrypted_data, &decryption),
2474            parse_encrypted_data_template_node_with_policy(encrypted_data, &encryption),
2475        ] {
2476            assert!(matches!(
2477                result,
2478                Err(XmlEncError::Policy(
2479                    crate::policy::PolicyViolation::ResourceLimit {
2480                        resource: crate::policy::resource_name::XML_DEPTH,
2481                        maximum,
2482                        actual,
2483                    }
2484                )) if maximum == actual_depth - 1 && actual == actual_depth
2485            ));
2486        }
2487    }
2488
2489    #[test]
2490    fn node_parsers_revalidate_the_containing_documents_dtd_policy() {
2491        // Node parse provenance is not available through roxmltree. Both public
2492        // entry points must therefore validate the source document themselves.
2493        let containing = format!(
2494            r#"<!DOCTYPE root [<!ENTITY marker "allowed">]><root>{DATA}<payload>&marker;</payload></root>"#
2495        );
2496        let document = Document::parse_with_options(
2497            &containing,
2498            ParsingOptions {
2499                allow_dtd: true,
2500                ..ParsingOptions::default()
2501            },
2502        )
2503        .expect("the caller can parse a document under a more permissive policy");
2504        let encrypted_data = document
2505            .descendants()
2506            .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData")))
2507            .expect("selected EncryptedData");
2508
2509        for result in [
2510            parse_encrypted_data_node_with_policy(
2511                encrypted_data,
2512                &crate::policy::DecryptionPolicy::default(),
2513            ),
2514            parse_encrypted_data_template_node_with_policy(
2515                encrypted_data,
2516                &crate::policy::EncryptionPolicy::default(),
2517            ),
2518        ] {
2519            assert!(matches!(result, Err(XmlEncError::XmlParse(_))));
2520        }
2521
2522        let mut decryption_allowed = crate::policy::DecryptionPolicy::default();
2523        decryption_allowed.xml.allow_internal_dtd = true;
2524        parse_encrypted_data_node_with_policy(encrypted_data, &decryption_allowed)
2525            .expect("explicitly permitted internal DTD must remain accepted");
2526        let mut encryption_allowed = crate::policy::EncryptionPolicy::default();
2527        encryption_allowed.xml.allow_internal_dtd = true;
2528        parse_encrypted_data_template_node_with_policy(encrypted_data, &encryption_allowed)
2529            .expect("template parsing must share the same explicit DTD policy");
2530    }
2531
2532    #[test]
2533    fn template_parser_rejects_nonempty_invalid_cipher_values() {
2534        // Empty placeholders are intentional template slots, but every nonempty
2535        // direct or recipient value must already satisfy the base64Binary syntax.
2536        let invalid_direct = DATA.replace(" YWJj\nZA== ", "!!!!");
2537        let invalid_recipient = format!(
2538            "<xenc:EncryptedData xmlns:xenc=\"{XMLENC_NS}\" xmlns:ds=\"{XMLDSIG_NS}\"><xenc:EncryptionMethod Algorithm=\"http://www.w3.org/2009/xmlenc11#aes128-gcm\"/><ds:KeyInfo><xenc:EncryptedKey><xenc:EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"/><xenc:CipherData><xenc:CipherValue>!!!!</xenc:CipherValue></xenc:CipherData></xenc:EncryptedKey></ds:KeyInfo><xenc:CipherData><xenc:CipherValue/></xenc:CipherData></xenc:EncryptedData>"
2539        );
2540        for xml in [&invalid_direct, &invalid_recipient] {
2541            let document = Document::parse(xml).expect("template must be well-formed XML");
2542            assert!(matches!(
2543                parse_encrypted_data_template_node_with_policy(
2544                    document.root_element(),
2545                    &crate::policy::EncryptionPolicy::default(),
2546                ),
2547                Err(XmlEncError::Base64(_))
2548            ));
2549        }
2550
2551        let empty = DATA.replace(" YWJj\nZA== ", "");
2552        let document = Document::parse(&empty).expect("empty template must be XML");
2553        parse_encrypted_data_template_node_with_policy(
2554            document.root_element(),
2555            &crate::policy::EncryptionPolicy::default(),
2556        )
2557        .expect("an explicit empty template placeholder remains valid");
2558    }
2559
2560    #[test]
2561    fn parses_cipher_reference_without_performing_retrieval() {
2562        // Parsing the source descriptor grants no URI permission or I/O.
2563        let xml = "<xenc:EncryptedData xmlns:xenc=\"http://www.w3.org/2001/04/xmlenc#\"><xenc:EncryptionMethod Algorithm=\"http://www.w3.org/2009/xmlenc11#aes128-gcm\"/><xenc:CipherData><xenc:CipherReference URI=\"https://attacker.invalid/key\"/></xenc:CipherData></xenc:EncryptedData>";
2564        assert!(
2565            matches!(parse_encrypted_data(xml).expect("reference descriptor").cipher_data,
2566            CipherData::Reference { ref uri, .. } if uri == "https://attacker.invalid/key")
2567        );
2568    }
2569
2570    #[test]
2571    fn joins_comment_split_cipher_text_and_rejects_element_children() {
2572        // Comments may split XML character data, but elements would change the
2573        // CipherValue schema and must not be silently ignored.
2574        let split = DATA.replace("YWJj\nZA==", "YW<!-- split -->Jj\nZA==");
2575        let parsed = parse_encrypted_data(&split).expect("comment-split base64 must parse");
2576        assert_eq!(parsed.cipher_data.inline_value(), Some("YWJjZA=="));
2577
2578        let nested = DATA.replace("YWJj\nZA==", "YW<xenc:Unexpected/>JjZA==");
2579        assert!(matches!(
2580            parse_encrypted_data(&nested),
2581            Err(XmlEncError::InvalidStructure(_))
2582        ));
2583    }
2584
2585    #[test]
2586    fn rejects_wrong_namespaces_and_retains_recipient_keys() {
2587        // Local names alone are insufficient: accepting lookalike namespaces would
2588        // let an attacker change the data model interpreted by the decryptor.
2589        let wrong_namespace = DATA.replace(XMLENC_NS, "urn:not-xmlenc");
2590        assert!(matches!(
2591            parse_encrypted_data(&wrong_namespace),
2592            Err(XmlEncError::InvalidStructure(_))
2593        ));
2594
2595        let encrypted_key = |recipient: &str| {
2596            format!(
2597                "<xenc:EncryptedKey Recipient=\"{recipient}\"><xenc:EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#kw-aes128\"/><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</xenc:CipherValue></xenc:CipherData></xenc:EncryptedKey>"
2598            )
2599        };
2600        let recipients = format!(
2601            "<xenc:EncryptedData xmlns:xenc=\"{XMLENC_NS}\" xmlns:ds=\"{XMLDSIG_NS}\"><xenc:EncryptionMethod Algorithm=\"http://www.w3.org/2009/xmlenc11#aes128-gcm\"/><ds:KeyInfo>{}{}</ds:KeyInfo><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData>",
2602            encrypted_key("alice"),
2603            encrypted_key("bob")
2604        );
2605        let parsed = parse_encrypted_data(&recipients).expect("recipient keys must parse");
2606        assert_eq!(
2607            parsed
2608                .encrypted_keys
2609                .iter()
2610                .filter_map(|key| key.recipient.as_deref())
2611                .collect::<Vec<_>>(),
2612            ["alice", "bob"]
2613        );
2614    }
2615
2616    /// Known agreement metadata parses independently of execution permission;
2617    /// unknown mechanisms retain their exact URI and missing algorithms fail.
2618    #[test]
2619    fn rejects_unsupported_key_agreement_explicitly() {
2620        let xml = format!(
2621            r#"<xenc:EncryptedData xmlns:xenc="{XMLENC_NS}" xmlns:ds="{XMLDSIG_NS}"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/><ds:KeyInfo><xenc:AgreementMethod Algorithm="http://www.w3.org/2001/04/xmlenc#dh"/></ds:KeyInfo><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData>"#
2622        );
2623        let parsed = parse_encrypted_data(&xml).expect("legacy DH descriptor parses");
2624        assert_eq!(
2625            parsed.agreement_methods[0].algorithm,
2626            crate::policy::KeyAgreementAlgorithm::LegacyDh
2627        );
2628        let unknown = xml.replace(
2629            "http://www.w3.org/2001/04/xmlenc#dh",
2630            "urn:unknown:agreement",
2631        );
2632        assert!(matches!(
2633            parse_encrypted_data(&unknown),
2634            Err(XmlEncError::UnsupportedAlgorithm(uri))
2635                if uri == "urn:unknown:agreement"
2636        ));
2637
2638        let missing_algorithm =
2639            xml.replace(" Algorithm=\"http://www.w3.org/2001/04/xmlenc#dh\"", "");
2640        assert!(matches!(
2641            parse_encrypted_data(&missing_algorithm),
2642            Err(XmlEncError::MissingRequired("AgreementMethod Algorithm"))
2643        ));
2644    }
2645
2646    /// Multiple supported mechanisms remain independent ordered candidates.
2647    #[test]
2648    fn retains_supported_key_candidates_alongside_unsupported_agreement() {
2649        // Parsing legacy DH does not discard an independent wrapped recipient.
2650        let xml = format!(
2651            r#"<xenc:EncryptedData xmlns:xenc="{XMLENC_NS}" xmlns:ds="{XMLDSIG_NS}"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/><ds:KeyInfo><xenc:AgreementMethod Algorithm="http://www.w3.org/2001/04/xmlenc#dh"/><ds:KeyName>content-key</ds:KeyName><xenc:EncryptedKey Recipient="alice"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#kw-aes128"/><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</xenc:CipherValue></xenc:CipherData></xenc:EncryptedKey></ds:KeyInfo><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData>"#
2652        );
2653
2654        let parsed = parse_encrypted_data(&xml)
2655            .expect("a supported key candidate must take precedence over agreement fallback");
2656        assert_eq!(parsed.key_name.as_deref(), Some("content-key"));
2657        assert_eq!(parsed.encrypted_keys.len(), 1);
2658        assert_eq!(parsed.agreement_methods.len(), 1);
2659        assert_eq!(parsed.encrypted_keys[0].recipient.as_deref(), Some("alice"));
2660    }
2661
2662    #[test]
2663    fn rejects_missing_algorithm_and_duplicate_oaep_parameters() {
2664        // Algorithm selection and OAEP parameter cardinality are security-sensitive,
2665        // so malformed declarations must not fall back to implicit behavior.
2666        let missing_algorithm = DATA.replace(
2667            " Algorithm=\"http://www.w3.org/2009/xmlenc11#aes128-gcm\"",
2668            "",
2669        );
2670        assert!(matches!(
2671            parse_encrypted_data(&missing_algorithm),
2672            Err(XmlEncError::MissingRequired(_))
2673        ));
2674
2675        let duplicate_oaep = format!(
2676            "<xenc:EncryptedData xmlns:xenc=\"{XMLENC_NS}\"><xenc:EncryptionMethod Algorithm=\"http://www.w3.org/2009/xmlenc11#aes128-gcm\"><xenc:OAEPparams>YQ==</xenc:OAEPparams><xenc:OAEPparams>Yg==</xenc:OAEPparams></xenc:EncryptionMethod><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData>"
2677        );
2678        assert!(matches!(
2679            parse_encrypted_data(&duplicate_oaep),
2680            Err(XmlEncError::InvalidStructure(_))
2681        ));
2682
2683        let oaep_on_aes = DATA.replace(
2684            "/><xenc:CipherData>",
2685            "><xenc:OAEPparams>YQ==</xenc:OAEPparams></xenc:EncryptionMethod><xenc:CipherData>",
2686        );
2687        assert!(matches!(
2688            parse_encrypted_data(&oaep_on_aes),
2689            Err(XmlEncError::InvalidStructure(_))
2690        ));
2691    }
2692
2693    #[test]
2694    fn accepts_empty_oaep_params_as_an_explicit_empty_label() {
2695        // base64Binary permits an empty lexical value. Preserve presence separately
2696        // from absence because RSA-OAEP treats both as the same empty label bytes.
2697        for params in ["", " \n\t "] {
2698            let xml = format!(
2699                "<xenc:EncryptionMethod xmlns:xenc=\"{XMLENC_NS}\" Algorithm=\"http://www.w3.org/2009/xmlenc11#rsa-oaep\"><xenc:OAEPparams>{params}</xenc:OAEPparams></xenc:EncryptionMethod>"
2700            );
2701            let document = Document::parse(&xml).expect("test method must be XML");
2702            let parsed = parse_encryption_method(document.root_element())
2703                .expect("empty OAEPparams must decode as an empty label");
2704            assert_eq!(parsed.oaep_params, Some(Vec::new()));
2705        }
2706
2707        assert!(matches!(
2708            normalize_base64(" \n\t "),
2709            Err(XmlEncError::Base64(_))
2710        ));
2711    }
2712
2713    #[test]
2714    fn bounds_oaep_parameters_before_base64_allocation() {
2715        // OAEP labels are retained as decoded metadata. The parser must cap the
2716        // normalized lexical form before either String or decoded Vec can grow.
2717        let xml = format!(
2718            "<xenc:EncryptionMethod xmlns:xenc=\"{XMLENC_NS}\" Algorithm=\"http://www.w3.org/2009/xmlenc11#rsa-oaep\"><xenc:OAEPparams>{}</xenc:OAEPparams></xenc:EncryptionMethod>",
2719            STANDARD.encode([0_u8; 65])
2720        );
2721        let document = Document::parse(&xml).expect("test method must be XML");
2722
2723        assert!(matches!(
2724            parse_encryption_method_with_limit(document.root_element(), 64),
2725            Err(XmlEncError::Policy(
2726                crate::policy::PolicyViolation::ResourceLimit {
2727                    resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
2728                    maximum: 64,
2729                    actual: 65,
2730                }
2731            ))
2732        ));
2733    }
2734
2735    #[test]
2736    fn validates_explicit_key_size_for_supported_aes_methods() {
2737        // KeySize is valid for every EncryptionMethod, but fixed-size AES URIs
2738        // must reject a declaration that disagrees with the algorithm.
2739        for (algorithm, bits) in [
2740            ("http://www.w3.org/2001/04/xmlenc#aes128-cbc", 128),
2741            ("http://www.w3.org/2001/04/xmlenc#aes256-cbc", 256),
2742            ("http://www.w3.org/2009/xmlenc11#aes128-gcm", 128),
2743            ("http://www.w3.org/2009/xmlenc11#aes256-gcm", 256),
2744            ("http://www.w3.org/2001/04/xmlenc#kw-aes128", 128),
2745            ("http://www.w3.org/2001/04/xmlenc#kw-aes256", 256),
2746        ] {
2747            let xml = format!(
2748                "<xenc:EncryptionMethod xmlns:xenc=\"{XMLENC_NS}\" Algorithm=\"{algorithm}\"><xenc:KeySize>{bits}</xenc:KeySize></xenc:EncryptionMethod>"
2749            );
2750            let document = Document::parse(&xml).expect("test method must be XML");
2751            let parsed = parse_encryption_method(document.root_element())
2752                .expect("matching AES KeySize must parse");
2753            assert_eq!(parsed.key_size_bits, Some(bits));
2754
2755            let inconsistent = xml.replace(&format!(">{bits}<"), ">192<");
2756            let document = Document::parse(&inconsistent).expect("test method must be XML");
2757            assert!(matches!(
2758                parse_encryption_method(document.root_element()),
2759                Err(XmlEncError::InvalidStructure(_))
2760            ));
2761        }
2762
2763        for key_size in ["128.0", "", "128</xenc:KeySize><xenc:KeySize>128"] {
2764            let xml = format!(
2765                "<xenc:EncryptionMethod xmlns:xenc=\"{XMLENC_NS}\" Algorithm=\"http://www.w3.org/2009/xmlenc11#aes128-gcm\"><xenc:KeySize>{key_size}</xenc:KeySize></xenc:EncryptionMethod>"
2766            );
2767            let document = Document::parse(&xml).expect("test method must be XML");
2768            assert!(matches!(
2769                parse_encryption_method(document.root_element()),
2770                Err(XmlEncError::InvalidStructure(_))
2771            ));
2772        }
2773    }
2774
2775    #[test]
2776    fn key_size_text_is_bounded_before_integer_parsing() {
2777        // Leading zeroes keep the numeric value valid while making the lexical
2778        // form arbitrarily large; enforce the metadata budget before parsing.
2779        let key_size = format!("{}128", "0".repeat(65));
2780        let xml = format!(
2781            "<xenc:EncryptionMethod xmlns:xenc=\"{XMLENC_NS}\" Algorithm=\"http://www.w3.org/2009/xmlenc11#aes128-gcm\"><xenc:KeySize>{key_size}</xenc:KeySize></xenc:EncryptionMethod>"
2782        );
2783        let document = Document::parse(&xml).expect("test method must be XML");
2784
2785        assert!(matches!(
2786            parse_encryption_method_with_limit(document.root_element(), 64),
2787            Err(XmlEncError::Policy(
2788                crate::policy::PolicyViolation::ResourceLimit {
2789                    resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
2790                    maximum: 64,
2791                    actual: 68,
2792                }
2793            ))
2794        ));
2795    }
2796
2797    #[test]
2798    fn retains_key_names_and_encrypted_key_reference_list() {
2799        // Key selection and reference metadata must survive parsing even though
2800        // sibling-key dereferencing remains the caller's responsibility.
2801        let xml = format!(
2802            r##"<xenc:EncryptedData xmlns:xenc="{XMLENC_NS}" xmlns:ds="{XMLDSIG_NS}" Id="data-1"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes128-gcm"/><ds:KeyInfo><ds:KeyName>content-key</ds:KeyName><xenc:EncryptedKey Id="key-1" Recipient="alice"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#kw-aes128"/><ds:KeyInfo><ds:X509Data/><ds:KeyName>wrapping-key</ds:KeyName></ds:KeyInfo><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</xenc:CipherValue></xenc:CipherData><xenc:ReferenceList><xenc:DataReference URI="#data-1"/><xenc:KeyReference URI="#key-2"/></xenc:ReferenceList></xenc:EncryptedKey></ds:KeyInfo><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData>"##
2803        );
2804        let parsed = parse_encrypted_data(&xml).expect("complete key metadata must parse");
2805        assert_eq!(parsed.key_name.as_deref(), Some("content-key"));
2806        let encrypted_key = parsed
2807            .encrypted_keys
2808            .first()
2809            .expect("embedded key must be retained");
2810        assert_eq!(encrypted_key.key_name.as_deref(), Some("wrapping-key"));
2811        let references = encrypted_key
2812            .reference_list
2813            .as_ref()
2814            .expect("reference list must be retained");
2815        assert_eq!(references.data_references, ["#data-1"]);
2816        assert_eq!(references.key_references, ["#key-2"]);
2817    }
2818
2819    #[test]
2820    fn preserves_key_identifier_whitespace() {
2821        // Key identifiers use exact string matching. Leading and trailing XML
2822        // character data must not be normalized into a different key identity.
2823        let xml = format!(
2824            r#"<xenc:EncryptedData xmlns:xenc="{XMLENC_NS}" xmlns:ds="{XMLDSIG_NS}"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes128-gcm"/><ds:KeyInfo><ds:KeyName> content-key </ds:KeyName><xenc:EncryptedKey><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#kw-aes128"/><ds:KeyInfo><ds:KeyName> wrapping-key </ds:KeyName></ds:KeyInfo><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</xenc:CipherValue></xenc:CipherData><xenc:CarriedKeyName> transported-key </xenc:CarriedKeyName></xenc:EncryptedKey></ds:KeyInfo><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData>"#
2825        );
2826        let parsed = parse_encrypted_data(&xml).expect("key metadata must parse");
2827        assert_eq!(parsed.key_name.as_deref(), Some(" content-key "));
2828        let encrypted_key = parsed
2829            .encrypted_keys
2830            .first()
2831            .expect("embedded key must be retained");
2832        assert_eq!(encrypted_key.key_name.as_deref(), Some(" wrapping-key "));
2833        assert_eq!(
2834            encrypted_key.carried_key_name.as_deref(),
2835            Some(" transported-key ")
2836        );
2837    }
2838
2839    #[test]
2840    fn accepts_one_carried_key_name_and_rejects_duplicates() {
2841        // CarriedKeyName is optional transported-key metadata after ReferenceList;
2842        // accepting more than one would violate EncryptedKey's content model.
2843        let xml = format!(
2844            r##"<xenc:EncryptedData xmlns:xenc="{XMLENC_NS}" xmlns:ds="{XMLDSIG_NS}"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes128-gcm"/><ds:KeyInfo><xenc:EncryptedKey><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#kw-aes128"/><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</xenc:CipherValue></xenc:CipherData><xenc:ReferenceList><xenc:DataReference URI="#data-1"/></xenc:ReferenceList><xenc:CarriedKeyName>transported-key</xenc:CarriedKeyName></xenc:EncryptedKey></ds:KeyInfo><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData>"##
2845        );
2846        let parsed = parse_encrypted_data(&xml).expect("one CarriedKeyName must parse");
2847        assert_eq!(
2848            parsed
2849                .encrypted_keys
2850                .first()
2851                .expect("embedded key must be retained")
2852                .carried_key_name
2853                .as_deref(),
2854            Some("transported-key")
2855        );
2856
2857        let duplicate = xml.replace(
2858            "</xenc:EncryptedKey>",
2859            "<xenc:CarriedKeyName>duplicate</xenc:CarriedKeyName></xenc:EncryptedKey>",
2860        );
2861        assert!(matches!(
2862            parse_encrypted_data(&duplicate),
2863            Err(XmlEncError::InvalidStructure(_))
2864        ));
2865    }
2866
2867    #[test]
2868    fn accepts_encrypted_key_key_info_without_key_name() {
2869        // Certificates are valid EncryptedKey KeyInfo content; absence of a
2870        // direct KeyName must not reject RSA-backed interoperability vectors.
2871        let xml = format!(
2872            r#"<xenc:EncryptedData xmlns:xenc="{XMLENC_NS}" xmlns:ds="{XMLDSIG_NS}"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes128-gcm"/><ds:KeyInfo><xenc:EncryptedKey><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/><ds:KeyInfo><ds:X509Data><ds:X509Certificate>YQ==</ds:X509Certificate></ds:X509Data></ds:KeyInfo><xenc:CipherData><xenc:CipherValue>YQ==</xenc:CipherValue></xenc:CipherData></xenc:EncryptedKey></ds:KeyInfo><xenc:CipherData><xenc:CipherValue>YQ==</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData>"#
2873        );
2874        let parsed = parse_encrypted_data(&xml).expect("certificate-only KeyInfo must parse");
2875        assert_eq!(
2876            parsed
2877                .encrypted_keys
2878                .first()
2879                .expect("embedded key must be retained")
2880                .key_name
2881                .as_deref(),
2882            None
2883        );
2884    }
2885
2886    #[test]
2887    fn rejects_malformed_encrypted_key_reference_lists() {
2888        // ReferenceList entries are security-sensitive associations: empty lists,
2889        // absent URIs, and foreign children must fail rather than be ignored.
2890        let template = format!(
2891            r#"<xenc:EncryptedData xmlns:xenc="{XMLENC_NS}" xmlns:ds="{XMLDSIG_NS}"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes128-gcm"/><ds:KeyInfo><xenc:EncryptedKey><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#kw-aes128"/><xenc:CipherData><xenc:CipherValue>YQ==</xenc:CipherValue></xenc:CipherData>{{reference_list}}</xenc:EncryptedKey></ds:KeyInfo><xenc:CipherData><xenc:CipherValue>YQ==</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData>"#
2892        );
2893        for malformed in [
2894            "<xenc:ReferenceList/>",
2895            "<xenc:ReferenceList><xenc:DataReference/></xenc:ReferenceList>",
2896            "<xenc:ReferenceList><xenc:Unexpected URI=\"#data\"/></xenc:ReferenceList>",
2897        ] {
2898            let xml = template.replace("{reference_list}", malformed);
2899            assert!(
2900                parse_encrypted_data(&xml).is_err(),
2901                "malformed list must fail: {malformed}"
2902            );
2903        }
2904    }
2905
2906    #[test]
2907    fn bounds_normalized_cipher_value_before_decode() {
2908        // The public document ceiling rejects an over-ceiling inline value
2909        // before its text can be retained or decoded by CipherData parsing.
2910        let oversized = "A".repeat(MAX_CIPHER_VALUE_BASE64_LEN + 1);
2911        let xml = DATA.replace("YWJj\nZA==", &oversized);
2912        assert!(matches!(
2913            parse_encrypted_data(&xml),
2914            Err(XmlEncError::Policy(
2915                crate::policy::PolicyViolation::ResourceLimit {
2916                    resource: crate::policy::resource_name::XML_DOCUMENT,
2917                    ..
2918                }
2919            ))
2920        ));
2921    }
2922
2923    #[test]
2924    fn policy_bounds_copied_encryption_metadata() {
2925        // Every retained metadata field must be rejected before it can bypass
2926        // the configured per-field ceiling through the XML parser entry point.
2927        let policy = crate::policy::DecryptionPolicy {
2928            resources: crate::policy::ResourcePolicy {
2929                max_encryption_metadata_bytes: 64,
2930                ..crate::policy::ResourcePolicy::default()
2931            },
2932            ..crate::policy::DecryptionPolicy::default()
2933        };
2934        let oversized = "x".repeat(65);
2935        for xml in [
2936            DATA.replace("<xenc:EncryptedData ", &format!("<xenc:EncryptedData Id=\"{oversized}\" ")),
2937            DATA.replace(
2938                "<xenc:EncryptedData ",
2939                &format!("<xenc:EncryptedData MimeType=\"{oversized}\" "),
2940            ),
2941            DATA.replace(
2942                "<xenc:EncryptedData ",
2943                &format!("<xenc:EncryptedData Encoding=\"{oversized}\" "),
2944            ),
2945            DATA.replace(
2946                "<xenc:CipherData>",
2947                &format!("<ds:KeyInfo xmlns:ds=\"{XMLDSIG_NS}\"><ds:KeyName>{oversized}</ds:KeyName></ds:KeyInfo><xenc:CipherData>"),
2948            ),
2949        ] {
2950            assert!(matches!(
2951                parse_encrypted_data_with_policy(&xml, &policy),
2952                Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2953                    maximum: 64,
2954                    actual: 65,
2955                    ..
2956                }))
2957            ));
2958        }
2959    }
2960
2961    #[test]
2962    fn policy_bounds_common_encrypted_type_metadata_on_nested_keys() {
2963        // EncryptedKey inherits Type, MimeType, and Encoding from EncryptedType.
2964        // Even though the key model does not retain them, both parse entry points
2965        // must reject oversized values before a template can preserve them.
2966        let resources = crate::policy::ResourcePolicy {
2967            max_encryption_metadata_bytes: 64,
2968            ..crate::policy::ResourcePolicy::default()
2969        };
2970        let decryption = crate::policy::DecryptionPolicy {
2971            resources: resources.clone(),
2972            ..crate::policy::DecryptionPolicy::default()
2973        };
2974        let encryption = crate::policy::EncryptionPolicy {
2975            resources,
2976            ..crate::policy::EncryptionPolicy::default()
2977        };
2978        let oversized = "x".repeat(65);
2979
2980        for attribute in ["Type", "MimeType", "Encoding"] {
2981            let xml = format!(
2982                r#"<xenc:EncryptedData xmlns:xenc="{XMLENC_NS}" xmlns:ds="{XMLDSIG_NS}"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes128-gcm"/><ds:KeyInfo><xenc:EncryptedKey {attribute}="{oversized}"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#kw-aes128"/><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</xenc:CipherValue></xenc:CipherData></xenc:EncryptedKey></ds:KeyInfo><xenc:CipherData><xenc:CipherValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData>"#
2983            );
2984            assert!(matches!(
2985                parse_encrypted_data_with_policy(&xml, &decryption),
2986                Err(XmlEncError::Policy(
2987                    crate::policy::PolicyViolation::ResourceLimit {
2988                        resource: "encryption metadata bytes",
2989                        maximum: 64,
2990                        actual: 65,
2991                    }
2992                ))
2993            ));
2994
2995            let document = Document::parse(&xml).expect("test template must be XML");
2996            assert!(matches!(
2997                parse_encrypted_data_template_node_with_policy(
2998                    document.root_element(),
2999                    &encryption,
3000                ),
3001                Err(XmlEncError::Policy(
3002                    crate::policy::PolicyViolation::ResourceLimit {
3003                        resource: "encryption metadata bytes",
3004                        maximum: 64,
3005                        actual: 65,
3006                    }
3007                ))
3008            ));
3009        }
3010    }
3011
3012    #[test]
3013    fn rejects_non_ascii_base64_before_it_can_cross_the_byte_bound() {
3014        // Base64 is ASCII-only. Rejecting Unicode before insertion also prevents a
3015        // multi-byte scalar from jumping from below the byte limit to above it.
3016        assert!(matches!(
3017            normalize_base64("YWJjéA=="),
3018            Err(XmlEncError::Base64(_))
3019        ));
3020
3021        // At an admitted document size the production lexical preflight
3022        // rejects Unicode, including immediately after a full quartet.
3023        assert!(matches!(
3024            normalize_base64("AAAAé"),
3025            Err(XmlEncError::Base64(_))
3026        ));
3027    }
3028}