Skip to main content

xml_sec/xmlenc/
parse.rs

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