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