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