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