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