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};
6use base64::{Engine as _, engine::general_purpose::STANDARD};
7
8use crate::document::{
9    DocumentParseSettings, XmlParseWorkBudget, parse_borrowed_with_settings_and_budget,
10};
11
12use super::map_document_error;
13use super::types::{
14    CipherData, EncryptedData, EncryptedDataType, EncryptedKey, EncryptionMethod,
15    MAX_CIPHER_VALUE_BASE64_LEN, ReferenceList, XMLDSIG_NS, XMLENC_NS, XMLENC11_NS, XmlEncError,
16};
17
18#[derive(Clone, Copy)]
19pub(super) struct ParsingPolicy<'a> {
20    xml: &'a crate::policy::XmlInputPolicy,
21    resources: &'a crate::policy::ResourcePolicy,
22}
23
24impl<'a> From<&'a crate::policy::EncryptionPolicy> for ParsingPolicy<'a> {
25    fn from(policy: &'a crate::policy::EncryptionPolicy) -> Self {
26        Self {
27            xml: &policy.xml,
28            resources: &policy.resources,
29        }
30    }
31}
32
33impl<'a> From<&'a crate::policy::DecryptionPolicy> for ParsingPolicy<'a> {
34    fn from(policy: &'a crate::policy::DecryptionPolicy) -> Self {
35        Self {
36            xml: &policy.xml,
37            resources: &policy.resources,
38        }
39    }
40}
41
42struct ParsedKeyInfo {
43    key_name: Option<String>,
44    encrypted_keys: Vec<EncryptedKey>,
45}
46
47/// Parse one `xenc:EncryptedData` document fragment.
48pub fn parse_encrypted_data(xml: &str) -> Result<EncryptedData, XmlEncError> {
49    parse_encrypted_data_with_policy(xml, &crate::policy::DecryptionPolicy::default())
50}
51
52pub(super) fn parse_encrypted_data_with_policy(
53    xml: &str,
54    policy: &crate::policy::DecryptionPolicy,
55) -> Result<EncryptedData, XmlEncError> {
56    parse_encrypted_data_with_policy_and_backend(xml, policy, crate::XmlBackend::default())
57}
58
59pub(super) fn parse_encrypted_data_with_policy_and_backend(
60    xml: &str,
61    policy: &crate::policy::DecryptionPolicy,
62    backend: crate::XmlBackend,
63) -> Result<EncryptedData, XmlEncError> {
64    let parse_budget = XmlParseWorkBudget::from_resources(&policy.resources);
65    parse_encrypted_data_with_policy_backend_and_budget(xml, policy, backend, &parse_budget)
66}
67
68pub(super) fn parse_encrypted_data_with_policy_backend_and_budget(
69    xml: &str,
70    policy: &crate::policy::DecryptionPolicy,
71    backend: crate::XmlBackend,
72    parse_budget: &XmlParseWorkBudget,
73) -> Result<EncryptedData, XmlEncError> {
74    policy.validate()?;
75    policy.resources.validate_xml_document_len(xml.len())?;
76    let settings =
77        DocumentParseSettings::from_policy(&policy.xml, &policy.resources).with_backend(backend);
78    let document = parse_borrowed_with_settings_and_budget(xml, settings, Some(parse_budget))
79        .map_err(|error| map_document_error(error, settings))?;
80    parse_encrypted_data_node(document.root_element(), policy.into(), false)
81}
82
83/// Parse a selected `xenc:EncryptedData` node under an immutable policy snapshot.
84///
85/// This is the node-oriented counterpart to [`parse_encrypted_data`]. It lets
86/// callers that already parsed a containing document validate the complete
87/// encrypted-data structure without serializing the selected subtree and losing
88/// namespace declarations inherited from its ancestors. The containing source
89/// document is reparsed because [`Node`] does not expose its parser provenance.
90pub fn parse_encrypted_data_node_with_policy(
91    node: Node<'_, '_>,
92    policy: &crate::policy::DecryptionPolicy,
93) -> Result<EncryptedData, XmlEncError> {
94    parse_encrypted_data_node_with_policy_and_backend(node, policy, crate::XmlBackend::default())
95}
96
97/// Parse a selected `xenc:EncryptedData` node with an explicit parser backend.
98pub fn parse_encrypted_data_node_with_policy_and_backend(
99    node: Node<'_, '_>,
100    policy: &crate::policy::DecryptionPolicy,
101    backend: crate::XmlBackend,
102) -> Result<EncryptedData, XmlEncError> {
103    let parse_budget = XmlParseWorkBudget::from_resources(&policy.resources);
104    parse_encrypted_data_node_with_policy_and_budget(node, policy, &parse_budget, backend)
105}
106
107pub(super) fn parse_encrypted_data_node_with_policy_and_budget(
108    node: Node<'_, '_>,
109    policy: &crate::policy::DecryptionPolicy,
110    parse_budget: &XmlParseWorkBudget,
111    backend: crate::XmlBackend,
112) -> Result<EncryptedData, XmlEncError> {
113    policy.validate()?;
114    let policy = ParsingPolicy::from(policy);
115    validate_node_document_policy(node, policy, parse_budget, backend)?;
116    parse_encrypted_data_node(node, policy, false)
117}
118
119/// Parse an `xenc:EncryptedData` template under an immutable policy snapshot.
120///
121/// This applies the complete encrypted-data grammar and metadata limits while
122/// permitting empty `CipherValue` placeholders that encryption will replace.
123/// Non-empty placeholders must still be well-formed base64. The containing
124/// source document is reparsed under this policy before template inspection.
125pub fn parse_encrypted_data_template_node_with_policy(
126    node: Node<'_, '_>,
127    policy: &crate::policy::EncryptionPolicy,
128) -> Result<EncryptedData, XmlEncError> {
129    parse_encrypted_data_template_node_with_policy_and_backend(
130        node,
131        policy,
132        crate::XmlBackend::default(),
133    )
134}
135
136/// Parse an `xenc:EncryptedData` template node with an explicit parser backend.
137pub fn parse_encrypted_data_template_node_with_policy_and_backend(
138    node: Node<'_, '_>,
139    policy: &crate::policy::EncryptionPolicy,
140    backend: crate::XmlBackend,
141) -> Result<EncryptedData, XmlEncError> {
142    let parse_budget = XmlParseWorkBudget::from_resources(&policy.resources);
143    policy.validate()?;
144    let policy = ParsingPolicy::from(policy);
145    validate_node_document_policy(node, policy, &parse_budget, backend)?;
146    parse_encrypted_data_node(node, policy, true)
147}
148
149fn validate_node_document_policy(
150    node: Node<'_, '_>,
151    policy: ParsingPolicy<'_>,
152    parse_budget: &XmlParseWorkBudget,
153    backend: crate::XmlBackend,
154) -> Result<(), XmlEncError> {
155    parse_policy_document(node.document().input_text(), policy, parse_budget, backend)?;
156    Ok(())
157}
158
159fn parse_policy_document<'a>(
160    xml: &'a str,
161    policy: ParsingPolicy<'_>,
162    parse_budget: &XmlParseWorkBudget,
163    backend: crate::XmlBackend,
164) -> Result<Document<'a>, XmlEncError> {
165    let settings =
166        DocumentParseSettings::from_policy(policy.xml, policy.resources).with_backend(backend);
167    parse_borrowed_with_settings_and_budget(xml, settings, Some(parse_budget))
168        .map_err(|error| map_document_error(error, settings))
169}
170
171fn parse_encrypted_data_node(
172    node: Node<'_, '_>,
173    policy: ParsingPolicy<'_>,
174    allow_empty_cipher_values: bool,
175) -> Result<EncryptedData, XmlEncError> {
176    require_element(node, XMLENC_NS, "EncryptedData")?;
177    validate_encrypted_type_attributes(node, policy)?;
178    let mut children = element_children(node);
179    let encryption_method = parse_encryption_method_with_limit(
180        next_required(&mut children, "EncryptionMethod")?,
181        policy.resources.max_encryption_metadata_bytes,
182    )?;
183    if children
184        .peek()
185        .is_some_and(|child| child.has_tag_name((XMLENC_NS, "EncryptionMethod")))
186    {
187        return Err(XmlEncError::InvalidStructure(
188            "EncryptedData contains more than one direct EncryptionMethod".into(),
189        ));
190    }
191
192    let key_info = match children.peek() {
193        Some(child) if child.has_tag_name((XMLDSIG_NS, "KeyInfo")) => parse_key_info(
194            next_required(&mut children, "KeyInfo")?,
195            policy,
196            allow_empty_cipher_values,
197        )?,
198        _ => ParsedKeyInfo {
199            key_name: None,
200            encrypted_keys: Vec::new(),
201        },
202    };
203    if children
204        .peek()
205        .is_some_and(|child| child.has_tag_name((XMLDSIG_NS, "KeyInfo")))
206    {
207        return Err(XmlEncError::InvalidStructure(
208            "EncryptedData contains more than one direct KeyInfo".into(),
209        ));
210    }
211
212    let cipher_data = parse_cipher_data(
213        next_required(&mut children, "CipherData")?,
214        allow_empty_cipher_values,
215    )?;
216    consume_encryption_properties(&mut children);
217    if children.next().is_some() {
218        return Err(XmlEncError::InvalidStructure(
219            "EncryptedData has unexpected child after CipherData".into(),
220        ));
221    }
222
223    let encrypted = EncryptedData {
224        id: bounded_attribute(node, "Id", policy)?,
225        encrypted_type: parse_encrypted_data_type(node.attribute("Type")),
226        key_name: key_info.key_name,
227        encryption_method,
228        encrypted_keys: key_info.encrypted_keys,
229        cipher_data,
230    };
231    validate_encrypted_data_metadata(&encrypted, policy)?;
232    Ok(encrypted)
233}
234
235fn parse_key_info(
236    node: Node<'_, '_>,
237    policy: ParsingPolicy<'_>,
238    allow_empty_cipher_values: bool,
239) -> Result<ParsedKeyInfo, XmlEncError> {
240    require_element(node, XMLDSIG_NS, "KeyInfo")?;
241    let mut key_name = None;
242    let mut encrypted_keys = Vec::new();
243    let mut unsupported_agreement = None;
244    for child in node.children().filter(Node::is_element) {
245        if child.has_tag_name((XMLDSIG_NS, "KeyName")) {
246            if key_name.is_some() {
247                return Err(XmlEncError::InvalidStructure(
248                    "KeyInfo contains more than one direct KeyName".into(),
249                ));
250            }
251            key_name = Some(parse_key_name(child, policy)?);
252        } else if child.has_tag_name((XMLENC_NS, "EncryptedKey")) {
253            if encrypted_keys.len() == policy.resources.max_encryption_recipients {
254                return Err(crate::policy::PolicyViolation::ResourceLimit {
255                    resource: crate::policy::resource_name::ENCRYPTION_RECIPIENTS,
256                    maximum: policy.resources.max_encryption_recipients,
257                    actual: encrypted_keys.len() + 1,
258                }
259                .into());
260            }
261            encrypted_keys.push(parse_encrypted_key(
262                child,
263                policy,
264                allow_empty_cipher_values,
265            )?);
266        } else if child.has_tag_name((XMLENC_NS, "AgreementMethod")) {
267            // Agreement methods require a separate key-derivation trust boundary.
268            // Keep the URI as a fallback error while allowing another advertised
269            // key candidate to be selected by the caller's resolver.
270            let algorithm = child
271                .attribute("Algorithm")
272                .ok_or(XmlEncError::MissingRequired(
273                    "AgreementMethod Algorithm attribute",
274                ))?;
275            validate_metadata_len(
276                algorithm.len(),
277                policy.resources.max_encryption_metadata_bytes,
278            )?;
279            unsupported_agreement.get_or_insert_with(|| algorithm.to_owned());
280        }
281    }
282    if key_name.is_none()
283        && encrypted_keys.is_empty()
284        && let Some(algorithm) = unsupported_agreement
285    {
286        return Err(XmlEncError::UnsupportedAlgorithm(algorithm));
287    }
288    Ok(ParsedKeyInfo {
289        key_name,
290        encrypted_keys,
291    })
292}
293
294fn parse_encrypted_key(
295    node: Node<'_, '_>,
296    policy: ParsingPolicy<'_>,
297    allow_empty_cipher_values: bool,
298) -> Result<EncryptedKey, XmlEncError> {
299    require_element(node, XMLENC_NS, "EncryptedKey")?;
300    validate_encrypted_type_attributes(node, policy)?;
301    let mut children = element_children(node);
302    let encryption_method = parse_encryption_method_with_limit(
303        next_required(&mut children, "EncryptionMethod")?,
304        policy.resources.max_encryption_metadata_bytes,
305    )?;
306    if children
307        .peek()
308        .is_some_and(|child| child.has_tag_name((XMLENC_NS, "EncryptionMethod")))
309    {
310        return Err(XmlEncError::InvalidStructure(
311            "EncryptedKey contains more than one direct EncryptionMethod".into(),
312        ));
313    }
314    let key_name = if children
315        .peek()
316        .is_some_and(|child| child.has_tag_name((XMLDSIG_NS, "KeyInfo")))
317    {
318        parse_key_name_hint(next_required(&mut children, "KeyInfo")?, policy)?
319    } else {
320        None
321    };
322    if children
323        .peek()
324        .is_some_and(|child| child.has_tag_name((XMLDSIG_NS, "KeyInfo")))
325    {
326        return Err(XmlEncError::InvalidStructure(
327            "EncryptedKey contains more than one direct KeyInfo".into(),
328        ));
329    }
330    let cipher_data = parse_cipher_data(
331        next_required(&mut children, "CipherData")?,
332        allow_empty_cipher_values,
333    )?;
334    consume_encryption_properties(&mut children);
335    let reference_list = if children
336        .peek()
337        .is_some_and(|child| child.has_tag_name((XMLENC_NS, "ReferenceList")))
338    {
339        Some(parse_reference_list(
340            next_required(&mut children, "ReferenceList")?,
341            policy,
342        )?)
343    } else {
344        None
345    };
346    let carried_key_name = if children
347        .peek()
348        .is_some_and(|child| child.has_tag_name((XMLENC_NS, "CarriedKeyName")))
349    {
350        Some(parse_carried_key_name(
351            next_required(&mut children, "CarriedKeyName")?,
352            policy,
353        )?)
354    } else {
355        None
356    };
357    if children.next().is_some() {
358        return Err(XmlEncError::InvalidStructure(
359            "EncryptedKey has unexpected child after CipherData".into(),
360        ));
361    }
362    Ok(EncryptedKey {
363        id: bounded_attribute(node, "Id", policy)?,
364        recipient: bounded_attribute(node, "Recipient", policy)?,
365        key_name,
366        encryption_method,
367        cipher_data,
368        reference_list,
369        carried_key_name,
370    })
371}
372
373fn parse_carried_key_name(
374    node: Node<'_, '_>,
375    policy: ParsingPolicy<'_>,
376) -> Result<String, XmlEncError> {
377    require_element(node, XMLENC_NS, "CarriedKeyName")?;
378    let value = bounded_simple_text(node, "CarriedKeyName", policy)?;
379    if value.is_empty() {
380        return Err(XmlEncError::InvalidStructure(
381            "CarriedKeyName is empty".into(),
382        ));
383    }
384    Ok(value)
385}
386
387fn parse_key_name_hint(
388    node: Node<'_, '_>,
389    policy: ParsingPolicy<'_>,
390) -> Result<Option<String>, XmlEncError> {
391    require_element(node, XMLDSIG_NS, "KeyInfo")?;
392    let mut key_names = node
393        .children()
394        .filter(|child| child.has_tag_name((XMLDSIG_NS, "KeyName")));
395    let Some(key_name) = key_names.next() else {
396        return Ok(None);
397    };
398    if key_names.next().is_some() {
399        return Err(XmlEncError::InvalidStructure(
400            "EncryptedKey KeyInfo contains more than one direct KeyName".into(),
401        ));
402    }
403    parse_key_name(key_name, policy).map(Some)
404}
405
406fn parse_key_name(node: Node<'_, '_>, policy: ParsingPolicy<'_>) -> Result<String, XmlEncError> {
407    let value = bounded_simple_text(node, "KeyName", policy)?;
408    if value.is_empty() {
409        return Err(XmlEncError::InvalidStructure("KeyName is empty".into()));
410    }
411    Ok(value)
412}
413
414fn parse_reference_list(
415    node: Node<'_, '_>,
416    policy: ParsingPolicy<'_>,
417) -> Result<ReferenceList, XmlEncError> {
418    require_element(node, XMLENC_NS, "ReferenceList")?;
419    let mut data_references = Vec::new();
420    let mut key_references = Vec::new();
421    for child in node.children().filter(Node::is_element) {
422        let uri = child
423            .attribute("URI")
424            .filter(|uri| !uri.is_empty())
425            .ok_or(XmlEncError::MissingRequired("Reference URI attribute"))?;
426        validate_metadata_len(uri.len(), policy.resources.max_encryption_metadata_bytes)?;
427        let uri = uri.to_owned();
428        match (child.tag_name().namespace(), child.tag_name().name()) {
429            (Some(XMLENC_NS), "DataReference") => data_references.push(uri),
430            (Some(XMLENC_NS), "KeyReference") => key_references.push(uri),
431            _ => {
432                return Err(XmlEncError::InvalidStructure(format!(
433                    "unsupported ReferenceList child {}",
434                    child.tag_name().name()
435                )));
436            }
437        }
438    }
439    if data_references.is_empty() && key_references.is_empty() {
440        return Err(XmlEncError::InvalidStructure(
441            "ReferenceList must contain at least one reference".into(),
442        ));
443    }
444    Ok(ReferenceList {
445        data_references,
446        key_references,
447    })
448}
449
450fn consume_encryption_properties<'a, I>(children: &mut std::iter::Peekable<I>)
451where
452    I: Iterator<Item = Node<'a, 'a>>,
453{
454    if children
455        .peek()
456        .is_some_and(|child| child.has_tag_name((XMLENC_NS, "EncryptionProperties")))
457    {
458        let _ = children.next();
459    }
460}
461
462#[cfg(test)]
463fn parse_encryption_method(node: Node<'_, '_>) -> Result<EncryptionMethod, XmlEncError> {
464    parse_encryption_method_with_limit(node, crate::hard_limits::ENCRYPTION_METADATA_BYTE_CEILING)
465}
466
467fn parse_encryption_method_with_limit(
468    node: Node<'_, '_>,
469    metadata_limit: usize,
470) -> Result<EncryptionMethod, XmlEncError> {
471    require_element(node, XMLENC_NS, "EncryptionMethod")?;
472    let algorithm = node
473        .attribute("Algorithm")
474        .ok_or(XmlEncError::MissingRequired(
475            "EncryptionMethod Algorithm attribute",
476        ))?;
477    validate_metadata_len(algorithm.len(), metadata_limit)?;
478    let algorithm = algorithm.to_owned();
479
480    let mut oaep_digest = None;
481    let mut mgf_algorithm = None;
482    let mut oaep_params = None;
483    let mut key_size_bits = None;
484    for child in node.children().filter(Node::is_element) {
485        match (child.tag_name().namespace(), child.tag_name().name()) {
486            (Some(XMLENC_NS), "KeySize")
487                if key_size_bits.is_none()
488                    && oaep_params.is_none()
489                    && oaep_digest.is_none()
490                    && mgf_algorithm.is_none() =>
491            {
492                key_size_bits = Some(parse_key_size(child, metadata_limit)?);
493            }
494            (Some(XMLENC_NS), "OAEPparams") if oaep_params.is_none() => {
495                oaep_params = Some(decode_bounded_base64_text(child, metadata_limit)?);
496            }
497            (Some(XMLDSIG_NS), "DigestMethod") if oaep_digest.is_none() => {
498                let digest = child
499                    .attribute("Algorithm")
500                    .ok_or(XmlEncError::MissingRequired(
501                        "DigestMethod Algorithm attribute",
502                    ))?;
503                validate_metadata_len(digest.len(), metadata_limit)?;
504                oaep_digest = Some(digest.to_owned());
505            }
506            (Some(XMLENC11_NS), "MGF") if mgf_algorithm.is_none() => {
507                let mgf = child
508                    .attribute("Algorithm")
509                    .ok_or(XmlEncError::MissingRequired("MGF Algorithm attribute"))?;
510                validate_metadata_len(mgf.len(), metadata_limit)?;
511                mgf_algorithm = Some(mgf.to_owned());
512            }
513            _ => {
514                return Err(XmlEncError::InvalidStructure(format!(
515                    "unsupported EncryptionMethod child {}",
516                    child.tag_name().name()
517                )));
518            }
519        }
520    }
521
522    let method = EncryptionMethod {
523        algorithm,
524        key_size_bits,
525        oaep_digest,
526        mgf_algorithm,
527        oaep_params,
528    };
529    method.validate_structure()?;
530    Ok(method)
531}
532
533fn parse_key_size(node: Node<'_, '_>, metadata_limit: usize) -> Result<usize, XmlEncError> {
534    let value = bounded_simple_text_with_limit(node, "KeySize", metadata_limit)?;
535    let value = value.trim();
536    let bits = value
537        .parse::<usize>()
538        .map_err(|_| XmlEncError::InvalidStructure("KeySize must be a positive integer".into()))?;
539    if bits == 0 {
540        return Err(XmlEncError::InvalidStructure(
541            "KeySize must be a positive integer".into(),
542        ));
543    }
544    Ok(bits)
545}
546
547fn parse_cipher_data(node: Node<'_, '_>, allow_empty: bool) -> Result<CipherData, XmlEncError> {
548    require_element(node, XMLENC_NS, "CipherData")?;
549    let mut children = element_children(node);
550    let value = next_required(&mut children, "CipherValue")?;
551    require_element(value, XMLENC_NS, "CipherValue")?;
552    if children.next().is_some() {
553        return Err(XmlEncError::InvalidStructure(
554            "CipherData must contain exactly one CipherValue".into(),
555        ));
556    }
557    Ok(CipherData {
558        value: normalize_base64_with_empty(&simple_text(value, "CipherValue")?, allow_empty)?,
559    })
560}
561
562fn simple_text(node: Node<'_, '_>, element_name: &str) -> Result<String, XmlEncError> {
563    if node.children().any(|child| child.is_element()) {
564        return Err(XmlEncError::InvalidStructure(format!(
565            "{element_name} must not contain element children"
566        )));
567    }
568    Ok(node
569        .children()
570        .filter(Node::is_text)
571        .filter_map(|child| child.text())
572        .collect())
573}
574
575fn bounded_simple_text(
576    node: Node<'_, '_>,
577    field: &'static str,
578    policy: ParsingPolicy<'_>,
579) -> Result<String, XmlEncError> {
580    bounded_simple_text_with_limit(node, field, policy.resources.max_encryption_metadata_bytes)
581}
582
583fn bounded_simple_text_with_limit(
584    node: Node<'_, '_>,
585    field: &'static str,
586    maximum: usize,
587) -> Result<String, XmlEncError> {
588    if node.children().any(|child| child.is_element()) {
589        return Err(XmlEncError::InvalidStructure(format!(
590            "{field} must not contain element children"
591        )));
592    }
593    let mut value = String::new();
594    for text in node
595        .children()
596        .filter(Node::is_text)
597        .filter_map(|child| child.text())
598    {
599        let actual = value.len().saturating_add(text.len());
600        validate_metadata_len(actual, maximum)?;
601        value.push_str(text);
602    }
603    Ok(value)
604}
605
606fn bounded_attribute(
607    node: Node<'_, '_>,
608    attribute: &str,
609    policy: ParsingPolicy<'_>,
610) -> Result<Option<String>, XmlEncError> {
611    let Some(value) = node.attribute(attribute) else {
612        return Ok(None);
613    };
614    validate_metadata_len(value.len(), policy.resources.max_encryption_metadata_bytes)?;
615    Ok(Some(value.to_owned()))
616}
617
618fn validate_metadata_len(actual: usize, maximum: usize) -> Result<(), XmlEncError> {
619    if actual <= maximum {
620        Ok(())
621    } else {
622        Err(crate::policy::PolicyViolation::ResourceLimit {
623            resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
624            maximum,
625            actual,
626        }
627        .into())
628    }
629}
630
631pub(super) fn validate_encrypted_data_metadata<'a>(
632    encrypted: &EncryptedData,
633    policy: impl Into<ParsingPolicy<'a>>,
634) -> Result<(), XmlEncError> {
635    let policy = policy.into();
636    let maximum = policy.resources.max_encryption_metadata_bytes;
637    let validate = |value: Option<&str>| validate_metadata_len(value.map_or(0, str::len), maximum);
638    validate(encrypted.id.as_deref())?;
639    if let Some(encrypted_type) = encrypted.encrypted_type.as_ref() {
640        let value = match encrypted_type {
641            EncryptedDataType::Element => "http://www.w3.org/2001/04/xmlenc#Element",
642            EncryptedDataType::Content => "http://www.w3.org/2001/04/xmlenc#Content",
643            EncryptedDataType::Other(value) => value,
644        };
645        validate(Some(value))?;
646    }
647    validate(encrypted.key_name.as_deref())?;
648    validate_encryption_method_metadata(&encrypted.encryption_method, maximum)?;
649    for key in &encrypted.encrypted_keys {
650        validate(key.id.as_deref())?;
651        validate(key.recipient.as_deref())?;
652        validate(key.key_name.as_deref())?;
653        validate(key.carried_key_name.as_deref())?;
654        validate_encryption_method_metadata(&key.encryption_method, maximum)?;
655        if let Some(references) = key.reference_list.as_ref() {
656            for uri in references
657                .data_references
658                .iter()
659                .chain(&references.key_references)
660            {
661                validate(Some(uri))?;
662            }
663        }
664    }
665    Ok(())
666}
667
668fn validate_encryption_method_metadata(
669    method: &EncryptionMethod,
670    maximum: usize,
671) -> Result<(), XmlEncError> {
672    validate_metadata_len(method.algorithm.len(), maximum)?;
673    if let Some(value) = method.oaep_digest.as_deref() {
674        validate_metadata_len(value.len(), maximum)?;
675    }
676    if let Some(value) = method.mgf_algorithm.as_deref() {
677        validate_metadata_len(value.len(), maximum)?;
678    }
679    if let Some(value) = method.oaep_params.as_deref() {
680        validate_metadata_len(value.len(), maximum)?;
681    }
682    Ok(())
683}
684
685fn validate_encrypted_type_attributes(
686    node: Node<'_, '_>,
687    policy: ParsingPolicy<'_>,
688) -> Result<(), XmlEncError> {
689    // Both EncryptedData and EncryptedKey derive these attributes from the XML
690    // Encryption EncryptedType schema. Template mutation preserves attributes
691    // that are not represented in the cryptographic model, so bound them here.
692    bounded_attribute(node, "Type", policy)?;
693    bounded_attribute(node, "MimeType", policy)?;
694    bounded_attribute(node, "Encoding", policy)?;
695    Ok(())
696}
697
698fn parse_encrypted_data_type(value: Option<&str>) -> Option<EncryptedDataType> {
699    value.map(|value| match value {
700        "http://www.w3.org/2001/04/xmlenc#Element" => EncryptedDataType::Element,
701        "http://www.w3.org/2001/04/xmlenc#Content" => EncryptedDataType::Content,
702        other => EncryptedDataType::Other(other.to_owned()),
703    })
704}
705
706fn decode_bounded_base64_text(node: Node<'_, '_>, maximum: usize) -> Result<Vec<u8>, XmlEncError> {
707    if node.children().any(|child| child.is_element()) {
708        return Err(XmlEncError::InvalidStructure(
709            "OAEPparams must not contain element children".into(),
710        ));
711    }
712    let encoded_limit = maximum.div_ceil(3).saturating_mul(4);
713    let mut normalized = String::with_capacity(encoded_limit);
714    for character in node
715        .children()
716        .filter(Node::is_text)
717        .filter_map(|child| child.text())
718        .flat_map(str::chars)
719    {
720        if !character.is_ascii() {
721            return Err(XmlEncError::Base64(
722                "OAEPparams contains non-ASCII data".into(),
723            ));
724        }
725        if !character.is_ascii_whitespace() {
726            if normalized.len() == encoded_limit {
727                return Err(crate::policy::PolicyViolation::ResourceLimit {
728                    resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
729                    maximum,
730                    actual: maximum.saturating_add(1),
731                }
732                .into());
733            }
734            normalized.push(character);
735        }
736    }
737    let decoded = STANDARD
738        .decode(normalized)
739        .map_err(|error| XmlEncError::Base64(error.to_string()))?;
740    validate_metadata_len(decoded.len(), maximum)?;
741    Ok(decoded)
742}
743
744fn normalize_base64_with_empty(value: &str, allow_empty: bool) -> Result<String, XmlEncError> {
745    let mut normalized = String::with_capacity(value.len().min(MAX_CIPHER_VALUE_BASE64_LEN));
746    for character in value.chars() {
747        if !character.is_ascii() {
748            return Err(XmlEncError::Base64(
749                "CipherValue contains non-ASCII data".into(),
750            ));
751        }
752        if !character.is_ascii_whitespace() {
753            if normalized.len() == MAX_CIPHER_VALUE_BASE64_LEN {
754                return Err(XmlEncError::Base64(format!(
755                    "CipherValue exceeds {MAX_CIPHER_VALUE_BASE64_LEN}-byte limit"
756                )));
757            }
758            normalized.push(character);
759        }
760    }
761    if normalized.is_empty() && !allow_empty {
762        return Err(XmlEncError::Base64("CipherValue is empty".into()));
763    }
764    if !normalized.is_empty() {
765        STANDARD
766            .decode(&normalized)
767            .map_err(|error| XmlEncError::Base64(error.to_string()))?;
768    }
769    Ok(normalized)
770}
771
772#[cfg(test)]
773fn normalize_base64(value: &str) -> Result<String, XmlEncError> {
774    normalize_base64_with_empty(value, false)
775}
776
777fn require_element(node: Node<'_, '_>, namespace: &str, name: &str) -> Result<(), XmlEncError> {
778    if node.has_tag_name((namespace, name)) {
779        Ok(())
780    } else {
781        Err(XmlEncError::InvalidStructure(format!(
782            "expected {{{namespace}}}{name}"
783        )))
784    }
785}
786
787fn element_children<'a>(
788    node: Node<'a, 'a>,
789) -> std::iter::Peekable<impl Iterator<Item = Node<'a, 'a>>> {
790    node.children().filter(Node::is_element).peekable()
791}
792
793fn next_required<'a, I>(
794    children: &mut std::iter::Peekable<I>,
795    expected: &'static str,
796) -> Result<Node<'a, 'a>, XmlEncError>
797where
798    I: Iterator<Item = Node<'a, 'a>>,
799{
800    children
801        .next()
802        .ok_or(XmlEncError::MissingRequired(expected))
803}
804
805#[cfg(test)]
806mod tests {
807    use super::*;
808
809    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>";
810
811    #[test]
812    fn parses_supported_encrypted_data_and_normalizes_cipher_value() {
813        // XML base64 permits line wrapping, but the retained value must be canonical.
814        let parsed = parse_encrypted_data(DATA).expect("valid XMLEnc data must parse");
815        assert_eq!(parsed.cipher_data.value, "YWJjZA==");
816        assert_eq!(parsed.encrypted_type, Some(EncryptedDataType::Element));
817    }
818
819    #[cfg(feature = "xml-backend-differential")]
820    #[test]
821    fn node_revalidation_uses_the_operation_backend() {
822        // A node selected from an operation-scoped document must not silently
823        // switch to the build default when its containing XML is revalidated.
824        let backend = crate::XmlBackend::Roxmltree;
825        let document = Document::parse_with_backend(DATA, backend).expect("test XML must parse");
826        let expected_work = DATA.len() * 3;
827        let resources = crate::policy::ResourcePolicy {
828            max_xml_parse_work_bytes: expected_work,
829            ..crate::policy::ResourcePolicy::default()
830        };
831        let policy = crate::policy::DecryptionPolicy {
832            resources: resources.clone(),
833            ..crate::policy::DecryptionPolicy::default()
834        };
835        let budget = XmlParseWorkBudget::from_resources(&resources);
836
837        parse_encrypted_data_node_with_policy_and_budget(
838            document.root_element(),
839            &policy,
840            &budget,
841            backend,
842        )
843        .expect("node revalidation must retain the selected backend");
844        assert_eq!(budget.consumed(), expected_work);
845
846        parse_encrypted_data_node_with_policy_and_backend(
847            document.root_element(),
848            &policy,
849            backend,
850        )
851        .expect("public decryption parser must retain the selected backend");
852        let encryption_policy = crate::policy::EncryptionPolicy {
853            resources,
854            ..crate::policy::EncryptionPolicy::default()
855        };
856        parse_encrypted_data_template_node_with_policy_and_backend(
857            document.root_element(),
858            &encryption_policy,
859            backend,
860        )
861        .expect("public template parser must retain the selected backend");
862    }
863
864    #[test]
865    fn node_parsers_enforce_the_containing_document_byte_limit() {
866        // A caller-selected node retains its complete source document. Passing a
867        // small subtree must not bypass the operation's document-byte ceiling.
868        let containing = format!("<root>{}<payload/></root>", DATA);
869        let document = Document::parse(&containing).expect("containing document must parse");
870        let encrypted_data = document
871            .descendants()
872            .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData")))
873            .expect("selected EncryptedData");
874        let resources = crate::policy::ResourcePolicy {
875            max_xml_document_bytes: DATA.len(),
876            ..crate::policy::ResourcePolicy::default()
877        };
878        let decryption = crate::policy::DecryptionPolicy {
879            resources: resources.clone(),
880            ..crate::policy::DecryptionPolicy::default()
881        };
882        let encryption = crate::policy::EncryptionPolicy {
883            resources,
884            ..crate::policy::EncryptionPolicy::default()
885        };
886
887        for result in [
888            parse_encrypted_data_node_with_policy(encrypted_data, &decryption),
889            parse_encrypted_data_template_node_with_policy(encrypted_data, &encryption),
890        ] {
891            assert!(matches!(
892                result,
893                Err(XmlEncError::Policy(
894                    crate::policy::PolicyViolation::ResourceLimit {
895                        resource: crate::policy::resource_name::XML_DOCUMENT,
896                        ..
897                    }
898                ))
899            ));
900        }
901    }
902
903    #[test]
904    fn node_parsers_enforce_the_containing_document_node_limit() {
905        // A selected EncryptedData subtree must not hide sibling nodes from the
906        // immutable resource policy supplied for the containing document.
907        let containing = format!("<root>{}<payload/></root>", DATA);
908        let document = Document::parse(&containing).expect("containing document must parse");
909        let encrypted_data = document
910            .descendants()
911            .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData")))
912            .expect("selected EncryptedData");
913        let actual_nodes = document.root().descendants().count();
914        let resources = crate::policy::ResourcePolicy {
915            max_xml_nodes: actual_nodes - 1,
916            ..crate::policy::ResourcePolicy::default()
917        };
918        let decryption = crate::policy::DecryptionPolicy {
919            resources: resources.clone(),
920            ..crate::policy::DecryptionPolicy::default()
921        };
922        let encryption = crate::policy::EncryptionPolicy {
923            resources,
924            ..crate::policy::EncryptionPolicy::default()
925        };
926
927        for result in [
928            parse_encrypted_data_node_with_policy(encrypted_data, &decryption),
929            parse_encrypted_data_template_node_with_policy(encrypted_data, &encryption),
930        ] {
931            assert!(matches!(
932                result,
933                Err(XmlEncError::Policy(
934                    crate::policy::PolicyViolation::ResourceLimit {
935                        resource: "XML nodes",
936                        maximum,
937                        actual,
938                    }
939                )) if maximum == actual_nodes - 1 && actual == actual_nodes
940            ));
941        }
942
943        let exact_resources = crate::policy::ResourcePolicy {
944            max_xml_nodes: actual_nodes,
945            ..crate::policy::ResourcePolicy::default()
946        };
947        let exact_policy = crate::policy::DecryptionPolicy {
948            resources: exact_resources,
949            ..crate::policy::DecryptionPolicy::default()
950        };
951        parse_encrypted_data_node_with_policy(encrypted_data, &exact_policy)
952            .expect("a document exactly at the node ceiling must parse");
953    }
954
955    #[test]
956    fn policy_parsers_enforce_the_complete_document_depth() {
957        // Both borrowed-node entry points revalidate the containing document;
958        // selecting a shallow EncryptedData subtree must not hide deep ancestors.
959        let containing = format!("<outer><inner>{DATA}</inner></outer>");
960        let document = Document::parse(&containing).expect("containing document must parse");
961        let encrypted_data = document
962            .descendants()
963            .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData")))
964            .expect("selected EncryptedData");
965        let actual_depth = encrypted_data
966            .document()
967            .descendants()
968            .filter(|node| node.is_element())
969            .map(|node| {
970                node.ancestors()
971                    .filter(|ancestor| ancestor.is_element())
972                    .count()
973            })
974            .max()
975            .expect("fixture has elements");
976        let resources = crate::policy::ResourcePolicy {
977            max_xml_depth: actual_depth - 1,
978            ..crate::policy::ResourcePolicy::default()
979        };
980        let decryption = crate::policy::DecryptionPolicy {
981            resources: resources.clone(),
982            ..crate::policy::DecryptionPolicy::default()
983        };
984        let encryption = crate::policy::EncryptionPolicy {
985            resources,
986            ..crate::policy::EncryptionPolicy::default()
987        };
988
989        for result in [
990            parse_encrypted_data_node_with_policy(encrypted_data, &decryption),
991            parse_encrypted_data_template_node_with_policy(encrypted_data, &encryption),
992        ] {
993            assert!(matches!(
994                result,
995                Err(XmlEncError::Policy(
996                    crate::policy::PolicyViolation::ResourceLimit {
997                        resource: crate::policy::resource_name::XML_DEPTH,
998                        maximum,
999                        actual,
1000                    }
1001                )) if maximum == actual_depth - 1 && actual == actual_depth
1002            ));
1003        }
1004    }
1005
1006    #[test]
1007    fn node_parsers_revalidate_the_containing_documents_dtd_policy() {
1008        // Node parse provenance is not available through roxmltree. Both public
1009        // entry points must therefore validate the source document themselves.
1010        let containing = format!(
1011            r#"<!DOCTYPE root [<!ENTITY marker "allowed">]><root>{DATA}<payload>&marker;</payload></root>"#
1012        );
1013        let document = Document::parse_with_options(
1014            &containing,
1015            ParsingOptions {
1016                allow_dtd: true,
1017                ..ParsingOptions::default()
1018            },
1019        )
1020        .expect("the caller can parse a document under a more permissive policy");
1021        let encrypted_data = document
1022            .descendants()
1023            .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData")))
1024            .expect("selected EncryptedData");
1025
1026        for result in [
1027            parse_encrypted_data_node_with_policy(
1028                encrypted_data,
1029                &crate::policy::DecryptionPolicy::default(),
1030            ),
1031            parse_encrypted_data_template_node_with_policy(
1032                encrypted_data,
1033                &crate::policy::EncryptionPolicy::default(),
1034            ),
1035        ] {
1036            assert!(matches!(result, Err(XmlEncError::XmlParse(_))));
1037        }
1038
1039        let mut decryption_allowed = crate::policy::DecryptionPolicy::default();
1040        decryption_allowed.xml.allow_internal_dtd = true;
1041        parse_encrypted_data_node_with_policy(encrypted_data, &decryption_allowed)
1042            .expect("explicitly permitted internal DTD must remain accepted");
1043        let mut encryption_allowed = crate::policy::EncryptionPolicy::default();
1044        encryption_allowed.xml.allow_internal_dtd = true;
1045        parse_encrypted_data_template_node_with_policy(encrypted_data, &encryption_allowed)
1046            .expect("template parsing must share the same explicit DTD policy");
1047    }
1048
1049    #[test]
1050    fn template_parser_rejects_nonempty_invalid_cipher_values() {
1051        // Empty placeholders are intentional template slots, but every nonempty
1052        // direct or recipient value must already satisfy the base64Binary syntax.
1053        let invalid_direct = DATA.replace(" YWJj\nZA== ", "!!!!");
1054        let invalid_recipient = format!(
1055            "<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>"
1056        );
1057        for xml in [&invalid_direct, &invalid_recipient] {
1058            let document = Document::parse(xml).expect("template must be well-formed XML");
1059            assert!(matches!(
1060                parse_encrypted_data_template_node_with_policy(
1061                    document.root_element(),
1062                    &crate::policy::EncryptionPolicy::default(),
1063                ),
1064                Err(XmlEncError::Base64(_))
1065            ));
1066        }
1067
1068        let empty = DATA.replace(" YWJj\nZA== ", "");
1069        let document = Document::parse(&empty).expect("empty template must be XML");
1070        parse_encrypted_data_template_node_with_policy(
1071            document.root_element(),
1072            &crate::policy::EncryptionPolicy::default(),
1073        )
1074        .expect("an explicit empty template placeholder remains valid");
1075    }
1076
1077    #[test]
1078    fn rejects_cipher_reference_and_trailing_children() {
1079        // External CipherReference retrieval would cross a caller-controlled trust boundary.
1080        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>";
1081        assert!(
1082            parse_encrypted_data(xml).is_err(),
1083            "CipherReference must fail closed"
1084        );
1085    }
1086
1087    #[test]
1088    fn joins_comment_split_cipher_text_and_rejects_element_children() {
1089        // Comments may split XML character data, but elements would change the
1090        // CipherValue schema and must not be silently ignored.
1091        let split = DATA.replace("YWJj\nZA==", "YW<!-- split -->Jj\nZA==");
1092        let parsed = parse_encrypted_data(&split).expect("comment-split base64 must parse");
1093        assert_eq!(parsed.cipher_data.value, "YWJjZA==");
1094
1095        let nested = DATA.replace("YWJj\nZA==", "YW<xenc:Unexpected/>JjZA==");
1096        assert!(matches!(
1097            parse_encrypted_data(&nested),
1098            Err(XmlEncError::InvalidStructure(_))
1099        ));
1100    }
1101
1102    #[test]
1103    fn rejects_wrong_namespaces_and_retains_recipient_keys() {
1104        // Local names alone are insufficient: accepting lookalike namespaces would
1105        // let an attacker change the data model interpreted by the decryptor.
1106        let wrong_namespace = DATA.replace(XMLENC_NS, "urn:not-xmlenc");
1107        assert!(matches!(
1108            parse_encrypted_data(&wrong_namespace),
1109            Err(XmlEncError::InvalidStructure(_))
1110        ));
1111
1112        let encrypted_key = |recipient: &str| {
1113            format!(
1114                "<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>"
1115            )
1116        };
1117        let recipients = format!(
1118            "<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>",
1119            encrypted_key("alice"),
1120            encrypted_key("bob")
1121        );
1122        let parsed = parse_encrypted_data(&recipients).expect("recipient keys must parse");
1123        assert_eq!(
1124            parsed
1125                .encrypted_keys
1126                .iter()
1127                .filter_map(|key| key.recipient.as_deref())
1128                .collect::<Vec<_>>(),
1129            ["alice", "bob"]
1130        );
1131    }
1132
1133    /// Verifies that a lone unsupported agreement reports its algorithm URI.
1134    #[test]
1135    fn rejects_unsupported_key_agreement_explicitly() {
1136        // AgreementMethod is outside the supported secure profile. Reporting its
1137        // URI avoids misclassifying a present but unsupported key as missing.
1138        let xml = format!(
1139            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>"#
1140        );
1141        assert!(matches!(
1142            parse_encrypted_data(&xml),
1143            Err(XmlEncError::UnsupportedAlgorithm(uri))
1144                if uri == "http://www.w3.org/2001/04/xmlenc#dh"
1145        ));
1146
1147        let missing_algorithm =
1148            xml.replace(" Algorithm=\"http://www.w3.org/2001/04/xmlenc#dh\"", "");
1149        assert!(matches!(
1150            parse_encrypted_data(&missing_algorithm),
1151            Err(XmlEncError::MissingRequired(
1152                "AgreementMethod Algorithm attribute"
1153            ))
1154        ));
1155    }
1156
1157    /// Verifies that unsupported agreement metadata does not hide usable keys.
1158    #[test]
1159    fn retains_supported_key_candidates_alongside_unsupported_agreement() {
1160        // Multi-recipient KeyInfo may advertise an unsupported agreement method
1161        // before a key candidate that the configured resolver can actually use.
1162        let xml = format!(
1163            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>"#
1164        );
1165
1166        let parsed = parse_encrypted_data(&xml)
1167            .expect("a supported key candidate must take precedence over agreement fallback");
1168        assert_eq!(parsed.key_name.as_deref(), Some("content-key"));
1169        assert_eq!(parsed.encrypted_keys.len(), 1);
1170        assert_eq!(parsed.encrypted_keys[0].recipient.as_deref(), Some("alice"));
1171    }
1172
1173    #[test]
1174    fn rejects_missing_algorithm_and_duplicate_oaep_parameters() {
1175        // Algorithm selection and OAEP parameter cardinality are security-sensitive,
1176        // so malformed declarations must not fall back to implicit behavior.
1177        let missing_algorithm = DATA.replace(
1178            " Algorithm=\"http://www.w3.org/2009/xmlenc11#aes128-gcm\"",
1179            "",
1180        );
1181        assert!(matches!(
1182            parse_encrypted_data(&missing_algorithm),
1183            Err(XmlEncError::MissingRequired(_))
1184        ));
1185
1186        let duplicate_oaep = format!(
1187            "<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>"
1188        );
1189        assert!(matches!(
1190            parse_encrypted_data(&duplicate_oaep),
1191            Err(XmlEncError::InvalidStructure(_))
1192        ));
1193
1194        let oaep_on_aes = DATA.replace(
1195            "/><xenc:CipherData>",
1196            "><xenc:OAEPparams>YQ==</xenc:OAEPparams></xenc:EncryptionMethod><xenc:CipherData>",
1197        );
1198        assert!(matches!(
1199            parse_encrypted_data(&oaep_on_aes),
1200            Err(XmlEncError::InvalidStructure(_))
1201        ));
1202    }
1203
1204    #[test]
1205    fn accepts_empty_oaep_params_as_an_explicit_empty_label() {
1206        // base64Binary permits an empty lexical value. Preserve presence separately
1207        // from absence because RSA-OAEP treats both as the same empty label bytes.
1208        for params in ["", " \n\t "] {
1209            let xml = format!(
1210                "<xenc:EncryptionMethod xmlns:xenc=\"{XMLENC_NS}\" Algorithm=\"http://www.w3.org/2009/xmlenc11#rsa-oaep\"><xenc:OAEPparams>{params}</xenc:OAEPparams></xenc:EncryptionMethod>"
1211            );
1212            let document = Document::parse(&xml).expect("test method must be XML");
1213            let parsed = parse_encryption_method(document.root_element())
1214                .expect("empty OAEPparams must decode as an empty label");
1215            assert_eq!(parsed.oaep_params, Some(Vec::new()));
1216        }
1217
1218        assert!(matches!(
1219            normalize_base64(" \n\t "),
1220            Err(XmlEncError::Base64(_))
1221        ));
1222    }
1223
1224    #[test]
1225    fn bounds_oaep_parameters_before_base64_allocation() {
1226        // OAEP labels are retained as decoded metadata. The parser must cap the
1227        // normalized lexical form before either String or decoded Vec can grow.
1228        let xml = format!(
1229            "<xenc:EncryptionMethod xmlns:xenc=\"{XMLENC_NS}\" Algorithm=\"http://www.w3.org/2009/xmlenc11#rsa-oaep\"><xenc:OAEPparams>{}</xenc:OAEPparams></xenc:EncryptionMethod>",
1230            STANDARD.encode([0_u8; 65])
1231        );
1232        let document = Document::parse(&xml).expect("test method must be XML");
1233
1234        assert!(matches!(
1235            parse_encryption_method_with_limit(document.root_element(), 64),
1236            Err(XmlEncError::Policy(
1237                crate::policy::PolicyViolation::ResourceLimit {
1238                    resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
1239                    maximum: 64,
1240                    actual: 65,
1241                }
1242            ))
1243        ));
1244    }
1245
1246    #[test]
1247    fn validates_explicit_key_size_for_supported_aes_methods() {
1248        // KeySize is valid for every EncryptionMethod, but fixed-size AES URIs
1249        // must reject a declaration that disagrees with the algorithm.
1250        for (algorithm, bits) in [
1251            ("http://www.w3.org/2001/04/xmlenc#aes128-cbc", 128),
1252            ("http://www.w3.org/2001/04/xmlenc#aes256-cbc", 256),
1253            ("http://www.w3.org/2009/xmlenc11#aes128-gcm", 128),
1254            ("http://www.w3.org/2009/xmlenc11#aes256-gcm", 256),
1255            ("http://www.w3.org/2001/04/xmlenc#kw-aes128", 128),
1256            ("http://www.w3.org/2001/04/xmlenc#kw-aes256", 256),
1257        ] {
1258            let xml = format!(
1259                "<xenc:EncryptionMethod xmlns:xenc=\"{XMLENC_NS}\" Algorithm=\"{algorithm}\"><xenc:KeySize>{bits}</xenc:KeySize></xenc:EncryptionMethod>"
1260            );
1261            let document = Document::parse(&xml).expect("test method must be XML");
1262            let parsed = parse_encryption_method(document.root_element())
1263                .expect("matching AES KeySize must parse");
1264            assert_eq!(parsed.key_size_bits, Some(bits));
1265
1266            let inconsistent = xml.replace(&format!(">{bits}<"), ">192<");
1267            let document = Document::parse(&inconsistent).expect("test method must be XML");
1268            assert!(matches!(
1269                parse_encryption_method(document.root_element()),
1270                Err(XmlEncError::InvalidStructure(_))
1271            ));
1272        }
1273
1274        for key_size in ["128.0", "", "128</xenc:KeySize><xenc:KeySize>128"] {
1275            let xml = format!(
1276                "<xenc:EncryptionMethod xmlns:xenc=\"{XMLENC_NS}\" Algorithm=\"http://www.w3.org/2009/xmlenc11#aes128-gcm\"><xenc:KeySize>{key_size}</xenc:KeySize></xenc:EncryptionMethod>"
1277            );
1278            let document = Document::parse(&xml).expect("test method must be XML");
1279            assert!(matches!(
1280                parse_encryption_method(document.root_element()),
1281                Err(XmlEncError::InvalidStructure(_))
1282            ));
1283        }
1284    }
1285
1286    #[test]
1287    fn key_size_text_is_bounded_before_integer_parsing() {
1288        // Leading zeroes keep the numeric value valid while making the lexical
1289        // form arbitrarily large; enforce the metadata budget before parsing.
1290        let key_size = format!("{}128", "0".repeat(65));
1291        let xml = format!(
1292            "<xenc:EncryptionMethod xmlns:xenc=\"{XMLENC_NS}\" Algorithm=\"http://www.w3.org/2009/xmlenc11#aes128-gcm\"><xenc:KeySize>{key_size}</xenc:KeySize></xenc:EncryptionMethod>"
1293        );
1294        let document = Document::parse(&xml).expect("test method must be XML");
1295
1296        assert!(matches!(
1297            parse_encryption_method_with_limit(document.root_element(), 64),
1298            Err(XmlEncError::Policy(
1299                crate::policy::PolicyViolation::ResourceLimit {
1300                    resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
1301                    maximum: 64,
1302                    actual: 68,
1303                }
1304            ))
1305        ));
1306    }
1307
1308    #[test]
1309    fn retains_key_names_and_encrypted_key_reference_list() {
1310        // Key selection and reference metadata must survive parsing even though
1311        // sibling-key dereferencing remains the caller's responsibility.
1312        let xml = format!(
1313            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>"##
1314        );
1315        let parsed = parse_encrypted_data(&xml).expect("complete key metadata must parse");
1316        assert_eq!(parsed.key_name.as_deref(), Some("content-key"));
1317        let encrypted_key = parsed
1318            .encrypted_keys
1319            .first()
1320            .expect("embedded key must be retained");
1321        assert_eq!(encrypted_key.key_name.as_deref(), Some("wrapping-key"));
1322        let references = encrypted_key
1323            .reference_list
1324            .as_ref()
1325            .expect("reference list must be retained");
1326        assert_eq!(references.data_references, ["#data-1"]);
1327        assert_eq!(references.key_references, ["#key-2"]);
1328    }
1329
1330    #[test]
1331    fn preserves_key_identifier_whitespace() {
1332        // Key identifiers use exact string matching. Leading and trailing XML
1333        // character data must not be normalized into a different key identity.
1334        let xml = format!(
1335            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>"#
1336        );
1337        let parsed = parse_encrypted_data(&xml).expect("key metadata must parse");
1338        assert_eq!(parsed.key_name.as_deref(), Some(" content-key "));
1339        let encrypted_key = parsed
1340            .encrypted_keys
1341            .first()
1342            .expect("embedded key must be retained");
1343        assert_eq!(encrypted_key.key_name.as_deref(), Some(" wrapping-key "));
1344        assert_eq!(
1345            encrypted_key.carried_key_name.as_deref(),
1346            Some(" transported-key ")
1347        );
1348    }
1349
1350    #[test]
1351    fn accepts_one_carried_key_name_and_rejects_duplicates() {
1352        // CarriedKeyName is optional transported-key metadata after ReferenceList;
1353        // accepting more than one would violate EncryptedKey's content model.
1354        let xml = format!(
1355            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>"##
1356        );
1357        let parsed = parse_encrypted_data(&xml).expect("one CarriedKeyName must parse");
1358        assert_eq!(
1359            parsed
1360                .encrypted_keys
1361                .first()
1362                .expect("embedded key must be retained")
1363                .carried_key_name
1364                .as_deref(),
1365            Some("transported-key")
1366        );
1367
1368        let duplicate = xml.replace(
1369            "</xenc:EncryptedKey>",
1370            "<xenc:CarriedKeyName>duplicate</xenc:CarriedKeyName></xenc:EncryptedKey>",
1371        );
1372        assert!(matches!(
1373            parse_encrypted_data(&duplicate),
1374            Err(XmlEncError::InvalidStructure(_))
1375        ));
1376    }
1377
1378    #[test]
1379    fn accepts_encrypted_key_key_info_without_key_name() {
1380        // Certificates are valid EncryptedKey KeyInfo content; absence of a
1381        // direct KeyName must not reject RSA-backed interoperability vectors.
1382        let xml = format!(
1383            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>"#
1384        );
1385        let parsed = parse_encrypted_data(&xml).expect("certificate-only KeyInfo must parse");
1386        assert_eq!(
1387            parsed
1388                .encrypted_keys
1389                .first()
1390                .expect("embedded key must be retained")
1391                .key_name
1392                .as_deref(),
1393            None
1394        );
1395    }
1396
1397    #[test]
1398    fn rejects_malformed_encrypted_key_reference_lists() {
1399        // ReferenceList entries are security-sensitive associations: empty lists,
1400        // absent URIs, and foreign children must fail rather than be ignored.
1401        let template = format!(
1402            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>"#
1403        );
1404        for malformed in [
1405            "<xenc:ReferenceList/>",
1406            "<xenc:ReferenceList><xenc:DataReference/></xenc:ReferenceList>",
1407            "<xenc:ReferenceList><xenc:Unexpected URI=\"#data\"/></xenc:ReferenceList>",
1408        ] {
1409            let xml = template.replace("{reference_list}", malformed);
1410            assert!(
1411                parse_encrypted_data(&xml).is_err(),
1412                "malformed list must fail: {malformed}"
1413            );
1414        }
1415    }
1416
1417    #[test]
1418    fn bounds_normalized_cipher_value_before_decode() {
1419        // The bound applies after XML whitespace removal and before base64 allocates
1420        // its decoded output, preventing oversized transient allocations.
1421        let oversized = "A".repeat(MAX_CIPHER_VALUE_BASE64_LEN + 1);
1422        assert!(matches!(
1423            normalize_base64(&oversized),
1424            Err(XmlEncError::Base64(_))
1425        ));
1426    }
1427
1428    #[test]
1429    fn policy_bounds_copied_encryption_metadata() {
1430        // Every retained metadata field must be rejected before it can bypass
1431        // the configured per-field ceiling through the XML parser entry point.
1432        let policy = crate::policy::DecryptionPolicy {
1433            resources: crate::policy::ResourcePolicy {
1434                max_encryption_metadata_bytes: 64,
1435                ..crate::policy::ResourcePolicy::default()
1436            },
1437            ..crate::policy::DecryptionPolicy::default()
1438        };
1439        let oversized = "x".repeat(65);
1440        for xml in [
1441            DATA.replace("<xenc:EncryptedData ", &format!("<xenc:EncryptedData Id=\"{oversized}\" ")),
1442            DATA.replace(
1443                "<xenc:EncryptedData ",
1444                &format!("<xenc:EncryptedData MimeType=\"{oversized}\" "),
1445            ),
1446            DATA.replace(
1447                "<xenc:EncryptedData ",
1448                &format!("<xenc:EncryptedData Encoding=\"{oversized}\" "),
1449            ),
1450            DATA.replace(
1451                "<xenc:CipherData>",
1452                &format!("<ds:KeyInfo xmlns:ds=\"{XMLDSIG_NS}\"><ds:KeyName>{oversized}</ds:KeyName></ds:KeyInfo><xenc:CipherData>"),
1453            ),
1454        ] {
1455            assert!(matches!(
1456                parse_encrypted_data_with_policy(&xml, &policy),
1457                Err(XmlEncError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1458                    maximum: 64,
1459                    actual: 65,
1460                    ..
1461                }))
1462            ));
1463        }
1464    }
1465
1466    #[test]
1467    fn policy_bounds_common_encrypted_type_metadata_on_nested_keys() {
1468        // EncryptedKey inherits Type, MimeType, and Encoding from EncryptedType.
1469        // Even though the key model does not retain them, both parse entry points
1470        // must reject oversized values before a template can preserve them.
1471        let resources = crate::policy::ResourcePolicy {
1472            max_encryption_metadata_bytes: 64,
1473            ..crate::policy::ResourcePolicy::default()
1474        };
1475        let decryption = crate::policy::DecryptionPolicy {
1476            resources: resources.clone(),
1477            ..crate::policy::DecryptionPolicy::default()
1478        };
1479        let encryption = crate::policy::EncryptionPolicy {
1480            resources,
1481            ..crate::policy::EncryptionPolicy::default()
1482        };
1483        let oversized = "x".repeat(65);
1484
1485        for attribute in ["Type", "MimeType", "Encoding"] {
1486            let xml = format!(
1487                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>"#
1488            );
1489            assert!(matches!(
1490                parse_encrypted_data_with_policy(&xml, &decryption),
1491                Err(XmlEncError::Policy(
1492                    crate::policy::PolicyViolation::ResourceLimit {
1493                        resource: "encryption metadata bytes",
1494                        maximum: 64,
1495                        actual: 65,
1496                    }
1497                ))
1498            ));
1499
1500            let document = Document::parse(&xml).expect("test template must be XML");
1501            assert!(matches!(
1502                parse_encrypted_data_template_node_with_policy(
1503                    document.root_element(),
1504                    &encryption,
1505                ),
1506                Err(XmlEncError::Policy(
1507                    crate::policy::PolicyViolation::ResourceLimit {
1508                        resource: "encryption metadata bytes",
1509                        maximum: 64,
1510                        actual: 65,
1511                    }
1512                ))
1513            ));
1514        }
1515    }
1516
1517    #[test]
1518    fn rejects_non_ascii_base64_before_it_can_cross_the_byte_bound() {
1519        // Base64 is ASCII-only. Rejecting Unicode before insertion also prevents a
1520        // multi-byte scalar from jumping from below the byte limit to above it.
1521        assert!(matches!(
1522            normalize_base64("YWJjéA=="),
1523            Err(XmlEncError::Base64(_))
1524        ));
1525
1526        let mut boundary = "A".repeat(MAX_CIPHER_VALUE_BASE64_LEN - 1);
1527        boundary.push('é');
1528        assert!(matches!(
1529            normalize_base64(&boundary),
1530            Err(XmlEncError::Base64(_))
1531        ));
1532    }
1533}