Skip to main content

xml_sec/xmlenc/
cipher_reference.rs

1//! Source-anchored XMLEnc CipherReference processing.
2
3use std::collections::HashMap;
4
5use crate::xml::dom::Node;
6use crate::xmldsig::transforms::{
7    TransformExecutionBudget, TransformOptions, XPathSignatureParseBudget,
8    execute_reference_transforms_with_budget, parse_reference_transforms_with_budget,
9};
10use crate::xmldsig::uri::ExternalResourceContext;
11
12use super::types::{XMLENC_NS, XmlEncError};
13
14pub(super) fn parse_reference<'doc, 'input>(
15    node: Node<'doc, 'input>,
16    resources: &crate::policy::ResourcePolicy,
17    budget: &mut XPathSignatureParseBudget,
18) -> Result<(&'doc str, Vec<crate::xmldsig::transforms::Transform>), XmlEncError> {
19    if !node.has_tag_name((XMLENC_NS, "CipherReference")) {
20        return Err(XmlEncError::InvalidStructure(
21            "expected xenc:CipherReference".into(),
22        ));
23    }
24    let uri = node
25        .attribute("URI")
26        .ok_or(XmlEncError::MissingRequired("CipherReference URI"))?;
27    if uri.len() > resources.max_encryption_metadata_bytes {
28        return Err(crate::policy::PolicyViolation::ResourceLimit {
29            resource: crate::policy::resource_name::ENCRYPTION_METADATA_BYTES,
30            maximum: resources.max_encryption_metadata_bytes,
31            actual: uri.len(),
32        }
33        .into());
34    }
35    let mut container = None;
36    for child in node.children() {
37        if child.is_comment() || child.is_pi() {
38            continue;
39        }
40        if child.is_text()
41            && child.text().is_some_and(|text| {
42                text.bytes()
43                    .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
44            })
45        {
46            continue;
47        }
48        if !child.has_tag_name((XMLENC_NS, "Transforms")) || container.is_some() {
49            return Err(XmlEncError::InvalidStructure(
50                "CipherReference permits only one xenc:Transforms child".into(),
51            ));
52        }
53        container = Some(child);
54    }
55    // XMLEnc 1.1 section 3.3.1 changes the container namespace but preserves
56    // ds:Transform children and requires at least one when the container exists.
57    // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-CipherReference
58    let transforms = match container {
59        Some(container) => {
60            let count = container.children().filter(Node::is_element).count();
61            if count > resources.max_transforms_per_reference {
62                return Err(crate::policy::PolicyViolation::ResourceLimit {
63                    resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
64                    maximum: resources.max_transforms_per_reference,
65                    actual: count,
66                }
67                .into());
68            }
69            if count == 0 {
70                return Err(XmlEncError::InvalidStructure(
71                    "xenc:Transforms must contain a ds:Transform".into(),
72                ));
73            }
74            parse_reference_transforms_with_budget(container, XMLENC_NS, budget)?
75        }
76        None => Vec::new(),
77    };
78    Ok((uri, transforms))
79}
80
81/// One operation's source-anchored ciphertext reference processor.
82///
83/// Resources are supplied by the caller; this processor performs no I/O. All
84/// resolutions through this context share external-byte and transform budgets.
85pub struct CipherReferenceContext<'a> {
86    policy: &'a crate::policy::DecryptionPolicy,
87    external: ExternalResourceContext<'a>,
88    transforms: TransformExecutionBudget,
89    parse: std::cell::RefCell<XPathSignatureParseBudget>,
90    id_attributes: &'a [crate::IdAttributeRegistration],
91    backend: crate::XmlBackend,
92    operation: Option<&'a dyn ReferenceOperationGate>,
93}
94
95pub(super) trait ReferenceOperationGate {
96    fn run_resource(
97        &self,
98        identity: &crate::operation::OperationResourceIdentity,
99        action: &mut dyn FnMut() -> Result<Vec<u8>, XmlEncError>,
100    ) -> Result<Vec<u8>, XmlEncError>;
101}
102
103/// One lazily indexed source document, borrowed for this operation only.
104pub(super) struct BoundCipherReferenceContext<'context, 'doc> {
105    context: &'context CipherReferenceContext<'context>,
106    document: &'doc crate::XmlDomDocument<'doc>,
107    document_base: Option<&'doc str>,
108    resolver: std::cell::OnceCell<crate::xmldsig::uri::UriReferenceResolver<'doc>>,
109}
110
111impl<'context: 'doc, 'doc> BoundCipherReferenceContext<'context, 'doc> {
112    pub(super) fn resolver(&self) -> &crate::xmldsig::uri::UriReferenceResolver<'doc> {
113        self.resolver.get_or_init(|| {
114            self.context.external.bind(
115                self.document,
116                self.context.id_attributes,
117                self.context.policy.transforms.same_document_id_semantics,
118            )
119        })
120    }
121
122    pub(super) fn resolve_parsed(
123        &self,
124        node: Node<'_, '_>,
125        uri: &str,
126        transforms: &[crate::xmldsig::transforms::Transform],
127        xml_parse: &crate::document::XmlParseWorkBudget,
128    ) -> Result<Vec<u8>, XmlEncError> {
129        self.context.validate_reference_policy(uri, transforms)?;
130        if !core::ptr::eq(node.document(), self.document) {
131            return Err(XmlEncError::OperationPlan(
132                "foreign CipherReference origin".into(),
133            ));
134        }
135        self.context.resolve_with_resolver(
136            node,
137            uri,
138            transforms,
139            xml_parse,
140            self.resolver(),
141            self.document_base,
142        )
143    }
144}
145
146impl<'a> CipherReferenceContext<'a> {
147    pub(super) fn preflight_cipher_data(
148        &self,
149        cipher: &super::CipherData,
150    ) -> Result<(), XmlEncError> {
151        if self.operation.is_some()
152            && let super::CipherData::Reference { uri, transforms } = cipher
153        {
154            self.validate_reference_policy(uri, transforms)?;
155        }
156        Ok(())
157    }
158
159    pub(super) fn validate_method(
160        &self,
161        method: &super::EncryptionMethod,
162        content: bool,
163        provider: &dyn crate::provider::CryptoProvider,
164    ) -> Result<(), XmlEncError> {
165        // Standalone parsing preserves descriptors even for algorithms the
166        // selected provider cannot execute. Runtime parsing preflights work.
167        if self.operation.is_none() {
168            return Ok(());
169        }
170        if content {
171            method.validate_structure()?;
172            let algorithm = super::DataEncryptionAlgorithm::from_uri(&method.algorithm)?;
173            crate::policy::check_content_algorithm(
174                self.policy.data_algorithms.as_ref(),
175                algorithm,
176                "decryption",
177            )?;
178            provider.require_capability(crate::provider::ProviderCapability::Decrypt(algorithm))?;
179            Ok(())
180        } else {
181            super::decrypt::validate_key_encryption_method_policy(method, self.policy)
182        }
183    }
184
185    pub(super) fn with_operation(mut self, operation: &'a dyn ReferenceOperationGate) -> Self {
186        self.operation = Some(operation);
187        self
188    }
189    pub(super) fn backend(&self) -> crate::XmlBackend {
190        self.backend
191    }
192    pub(super) fn resolve_key_reference(
193        &self,
194        node: Node<'_, '_>,
195        uri: &str,
196        allowed_uris: crate::xmldsig::UriTypeSet,
197        transforms: &[crate::xmldsig::transforms::Transform],
198        document_base: Option<&str>,
199        xml_parse: &crate::document::XmlParseWorkBudget,
200    ) -> Result<(Vec<u8>, Option<String>), XmlEncError> {
201        if !allowed_uris.allows(uri) {
202            return Err(crate::policy::PolicyViolation::Algorithm {
203                operation: "encryption key retrieval URI",
204                algorithm: uri.into(),
205            }
206            .into());
207        }
208        let external = !uri.is_empty() && !uri.starts_with('#');
209        crate::xmldsig::transforms::validate_signing_transform_policy(
210            external,
211            transforms,
212            self.policy.transforms.allowed_algorithms.as_ref(),
213        )?;
214        let resolved = if external {
215            Some(
216                crate::c14n::xml_base::resolve_uri_from_node_with_document_base_with_budget(
217                    node,
218                    uri,
219                    document_base,
220                    self.transforms.xml_base_resolution(),
221                )
222                .map_err(|error| {
223                    XmlEncError::InvalidStructure(format!("key retrieval XML Base: {error}"))
224                })?,
225            )
226        } else {
227            document_base.map(str::to_owned)
228        };
229        let resolver = self.external.bind(
230            node.document(),
231            self.id_attributes,
232            self.policy.transforms.same_document_id_semantics,
233        );
234        // XMLDSig 1.1 section 4.5.3 applies transforms in the RetrievalMethod's
235        // original context, before interpreting the resulting key XML.
236        // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-RetrievalMethod
237        let bytes = self.execute_resolved_reference(
238            node,
239            if external {
240                resolved.as_deref().expect("external URI resolved")
241            } else {
242                uri
243            },
244            transforms,
245            xml_parse,
246            &resolver,
247        )?;
248        Ok((bytes, resolved))
249    }
250
251    pub(super) fn bind_document<'doc>(
252        &'doc self,
253        document: &'doc crate::XmlDomDocument<'doc>,
254    ) -> BoundCipherReferenceContext<'doc, 'doc> {
255        BoundCipherReferenceContext {
256            context: self,
257            document,
258            document_base: None,
259            resolver: std::cell::OnceCell::new(),
260        }
261    }
262
263    pub(super) fn bind_document_with_base<'doc>(
264        &'doc self,
265        document: &'doc crate::XmlDomDocument<'doc>,
266        document_base: Option<&'doc str>,
267    ) -> BoundCipherReferenceContext<'doc, 'doc> {
268        let mut bound = self.bind_document(document);
269        bound.document_base = document_base;
270        bound
271    }
272    /// Bind caller-owned resources and the operation's immutable policy.
273    pub fn new(
274        policy: &'a crate::policy::DecryptionPolicy,
275        resources: Option<&'a HashMap<String, Vec<u8>>>,
276        backend: crate::XmlBackend,
277        id_attributes: &'a [crate::IdAttributeRegistration],
278    ) -> Result<Self, XmlEncError> {
279        policy.validate()?;
280        Ok(Self {
281            policy,
282            external: ExternalResourceContext::new(
283                resources,
284                policy.resources.max_external_resource_bytes,
285                policy.resources.max_external_resource_total_bytes,
286            ),
287            transforms: TransformExecutionBudget::from_resources(&policy.resources)
288                .with_xml_backend(backend),
289            parse: std::cell::RefCell::new(XPathSignatureParseBudget::from_resources(
290                &policy.resources,
291            )),
292            id_attributes,
293            backend,
294            operation: None,
295        })
296    }
297
298    /// Resolve an original `xenc:CipherReference` without serializing its
299    /// subtree or discarding inherited namespaces, XML Base, or XPath `here()`.
300    pub fn resolve(&self, node: Node<'_, '_>) -> Result<Vec<u8>, XmlEncError> {
301        // A public Node does not attest which parser policy produced it. Apply
302        // the operation policy to its source before processing the original
303        // node; XPath provenance must not be replaced by the validation parse.
304        let settings = crate::document::DocumentParseSettings::from_policy(
305            &self.policy.xml,
306            &self.policy.resources,
307        )
308        .with_backend(self.backend);
309        crate::document::parse_borrowed_with_settings_and_budget(
310            node.document().input_text(),
311            settings,
312            Some(self.transforms.xml_parse_work()),
313        )
314        .map_err(|error| super::map_document_error(error, settings))?;
315        self.resolve_validated(node, self.transforms.xml_parse_work())
316    }
317
318    pub(super) fn resolve_validated(
319        &self,
320        node: Node<'_, '_>,
321        xml_parse: &crate::document::XmlParseWorkBudget,
322    ) -> Result<Vec<u8>, XmlEncError> {
323        let (uri, transforms) =
324            parse_reference(node, &self.policy.resources, &mut self.parse.borrow_mut())?;
325        self.resolve_parsed(node, uri, &transforms, xml_parse)
326    }
327
328    pub(super) fn resolve_parsed(
329        &self,
330        node: Node<'_, '_>,
331        uri: &str,
332        transforms: &[crate::xmldsig::transforms::Transform],
333        xml_parse: &crate::document::XmlParseWorkBudget,
334    ) -> Result<Vec<u8>, XmlEncError> {
335        self.bind_document(node.document())
336            .resolve_parsed(node, uri, transforms, xml_parse)
337    }
338
339    pub(super) fn validate_reference_policy(
340        &self,
341        uri: &str,
342        transforms: &[crate::xmldsig::transforms::Transform],
343    ) -> Result<(), XmlEncError> {
344        crate::xmldsig::transforms::validate_relationship_chain(
345            transforms,
346            self.policy.transforms.opc_relationship_edition,
347        )?;
348        if !self.policy.uris.references.allows(uri) {
349            return Err(crate::policy::PolicyViolation::Algorithm {
350                operation: "cipher reference URI",
351                algorithm: uri.to_owned(),
352            }
353            .into());
354        }
355        let initial_binary = !uri.is_empty() && !uri.starts_with('#');
356        crate::xmldsig::transforms::validate_signing_transform_policy(
357            initial_binary,
358            transforms,
359            self.policy.transforms.allowed_algorithms.as_ref(),
360        )?;
361        Ok(())
362    }
363
364    fn resolve_with_resolver(
365        &self,
366        node: Node<'_, '_>,
367        uri: &str,
368        transforms: &[crate::xmldsig::transforms::Transform],
369        xml_parse: &crate::document::XmlParseWorkBudget,
370        resolver: &crate::xmldsig::uri::UriReferenceResolver<'_>,
371        document_base: Option<&str>,
372    ) -> Result<Vec<u8>, XmlEncError> {
373        if uri.is_empty() || uri.starts_with('#') {
374            self.execute_resolved_reference(node, uri, transforms, xml_parse, resolver)
375        } else {
376            let resolved =
377                crate::c14n::xml_base::resolve_uri_from_node_with_document_base_with_budget(
378                    node,
379                    uri,
380                    document_base,
381                    self.transforms.xml_base_resolution(),
382                )
383                .map_err(|error| {
384                    XmlEncError::InvalidStructure(format!("cipher reference XML Base: {error}"))
385                })?;
386            self.execute_resolved_reference(node, &resolved, transforms, xml_parse, resolver)
387        }
388    }
389
390    fn execute_resolved_reference(
391        &self,
392        node: Node<'_, '_>,
393        uri: &str,
394        transforms: &[crate::xmldsig::transforms::Transform],
395        xml_parse: &crate::document::XmlParseWorkBudget,
396        resolver: &crate::xmldsig::uri::UriReferenceResolver<'_>,
397    ) -> Result<Vec<u8>, XmlEncError> {
398        use crate::operation::OperationResourceIdentity;
399        // Reserve selected external bytes before hashing or copying them. The
400        // immutable caller map keeps this identity stable through execution.
401        let external = if !uri.is_empty() && !uri.starts_with('#') {
402            Some(
403                resolver
404                    .external_resource(uri)?
405                    .ok_or_else(|| crate::xmldsig::TransformError::UnsupportedUri(uri.into()))?,
406            )
407        } else {
408            None
409        };
410        let mut action = || {
411            let input = match external {
412                Some(bytes) => crate::xmldsig::TransformData::Binary(bytes.to_vec()),
413                None => resolver
414                    .dereference_with_budget(uri, self.transforms.node_set_materialization())?,
415            };
416            execute_reference_transforms_with_budget(
417                node,
418                // XMLDSig 1.1 section 6.6.4 identifies the Signature which
419                // contains this transform, not a presumed XMLEnc-only root.
420                // An encrypted Object can itself be inside a Signature.
421                // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-EnvelopedSignature
422                node.ancestors().find(|ancestor| {
423                    ancestor.has_tag_name((super::types::XMLDSIG_NS, "Signature"))
424                }),
425                input,
426                transforms,
427                TransformOptions::default()
428                    .id_attributes(resolver.id_registrations())
429                    .xpath_here_semantics(self.policy.transforms.xpath_here_semantics)
430                    .opc_relationship_edition(self.policy.transforms.opc_relationship_edition)
431                    .allow_internal_dtd(self.policy.xml.allow_internal_dtd),
432                &self.transforms,
433                xml_parse,
434            )
435            .map_err(XmlEncError::from)
436        };
437        match self.operation {
438            Some(operation) => {
439                let identity = match external {
440                    Some(bytes) => OperationResourceIdentity::external(uri, bytes),
441                    None => OperationResourceIdentity::BorrowedDocumentNode {
442                        document: node.document() as *const _ as usize,
443                        node: node.id(),
444                    },
445                };
446                operation.run_resource(&identity, &mut action)
447            }
448            None => action(),
449        }
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456    use crate::xml::dom::Document;
457    use crate::xmldsig::UriTypeSet;
458
459    const DS: &str = "http://www.w3.org/2000/09/xmldsig#";
460    const BASE64: &str = "http://www.w3.org/2000/09/xmldsig#base64";
461
462    #[test]
463    fn bound_references_share_one_index_and_reject_foreign_origins() {
464        // Repeated ciphertext references must not rebuild a document-sized
465        // index. Equal lexical XML in another document is not this origin.
466        let xml = format!("<root>{}</root>", reference("urn:cipher", ""));
467        let document = Document::parse(&xml).expect("source XML");
468        let foreign = Document::parse(&xml).expect("independent XML");
469        let resources = HashMap::from([("urn:cipher".into(), b"ciphertext".to_vec())]);
470        let policy = crate::policy::DecryptionPolicy {
471            uris: crate::policy::UriPolicy {
472                references: UriTypeSet::ALL,
473                ..Default::default()
474            },
475            ..Default::default()
476        };
477        let context = CipherReferenceContext::new(
478            &policy,
479            Some(&resources),
480            crate::XmlBackend::default(),
481            &[],
482        )
483        .expect("reference context");
484        let bound = context.bind_document(&document);
485        assert!(bound.resolver.get().is_none());
486        let node = document
487            .descendants()
488            .find(|node| node.has_tag_name((XMLENC_NS, "CipherReference")))
489            .expect("source reference");
490        assert_eq!(
491            bound
492                .resolve_parsed(node, "urn:cipher", &[], context.transforms.xml_parse_work())
493                .expect("first resolution"),
494            b"ciphertext"
495        );
496        let index = bound.resolver.get().expect("initialized index") as *const _;
497        assert_eq!(
498            bound
499                .resolve_parsed(node, "urn:cipher", &[], context.transforms.xml_parse_work())
500                .expect("second resolution"),
501            b"ciphertext"
502        );
503        assert_eq!(
504            bound.resolver.get().expect("retained index") as *const _,
505            index
506        );
507        let foreign_node = foreign
508            .descendants()
509            .find(|node| node.has_tag_name((XMLENC_NS, "CipherReference")))
510            .expect("foreign reference");
511        assert!(matches!(
512            bound.resolve_parsed(
513                foreign_node,
514                "urn:cipher",
515                &[],
516                context.transforms.xml_parse_work()
517            ),
518            Err(XmlEncError::OperationPlan(_))
519        ));
520    }
521
522    fn reference(uri: &str, children: &str) -> String {
523        format!(
524            "<CipherReference xmlns='{XMLENC_NS}' xmlns:ds='{DS}' URI='{uri}'>{children}</CipherReference>"
525        )
526    }
527
528    #[test]
529    fn external_ciphertext_is_raw_not_implicitly_base64_decoded() {
530        // CipherReference without transforms already yields ciphertext octets;
531        // an inline CipherValue's base64 conversion must not be inherited.
532        let xml = reference("urn:cipher", "");
533        let document = Document::parse(&xml).expect("reference XML");
534        let resources = HashMap::from([("urn:cipher".into(), b"YWJj".to_vec())]);
535        let mut policy = crate::policy::DecryptionPolicy::default();
536        policy.uris.references = UriTypeSet::ALL;
537        let context = CipherReferenceContext::new(
538            &policy,
539            Some(&resources),
540            crate::XmlBackend::default(),
541            &[],
542        )
543        .expect("context");
544        assert_eq!(
545            context
546                .resolve(document.root_element())
547                .expect("ciphertext"),
548            b"YWJj"
549        );
550    }
551
552    #[test]
553    fn encryption_transform_container_uses_signature_transform_children() {
554        // Section 3.3.1 intentionally changes only the container namespace.
555        let xml = reference(
556            "urn:cipher",
557            &format!("<Transforms><ds:Transform Algorithm='{BASE64}'/></Transforms>"),
558        );
559        let document = Document::parse(&xml).expect("reference XML");
560        let resources = HashMap::from([("urn:cipher".into(), b"YWJj".to_vec())]);
561        let mut policy = crate::policy::DecryptionPolicy::default();
562        policy.uris.references = UriTypeSet::ALL;
563        let context = CipherReferenceContext::new(
564            &policy,
565            Some(&resources),
566            crate::XmlBackend::default(),
567            &[],
568        )
569        .expect("context");
570        assert_eq!(
571            context
572                .resolve(document.root_element())
573                .expect("ciphertext"),
574            b"abc"
575        );
576    }
577
578    #[test]
579    fn malformed_reference_does_not_spend_external_budget() {
580        // Syntax rejection precedes resource resolution, leaving the sole
581        // permitted read available for the subsequent valid reference.
582        let resources = HashMap::from([("urn:cipher".into(), b"abc".to_vec())]);
583        let mut policy = crate::policy::DecryptionPolicy::default();
584        policy.uris.references = UriTypeSet::ALL;
585        policy.resources.max_external_resource_total_bytes = 3;
586        let context = CipherReferenceContext::new(
587            &policy,
588            Some(&resources),
589            crate::XmlBackend::default(),
590            &[],
591        )
592        .expect("context");
593        for children in [
594            "<ds:Transforms/>",
595            "<Transforms/>",
596            "<Transforms/><Transforms/>",
597            "non-whitespace",
598            "<foreign/>",
599        ] {
600            let xml = reference("urn:cipher", children);
601            let document = Document::parse(&xml).expect("reference XML");
602            assert!(
603                context.resolve(document.root_element()).is_err(),
604                "{children}"
605            );
606        }
607        let xml = reference("urn:cipher", "");
608        let document = Document::parse(&xml).expect("reference XML");
609        assert_eq!(
610            context
611                .resolve(document.root_element())
612                .expect("remaining read"),
613            b"abc"
614        );
615        assert!(context.resolve(document.root_element()).is_err());
616    }
617
618    #[test]
619    fn inherited_xml_base_resolves_caller_owned_resource() {
620        // Moving the CipherReference into a synthetic fragment would lose the
621        // ancestor base and select a different resource identity.
622        let xml = format!(
623            "<root xml:base='https://example.invalid/a/'><CipherReference xmlns='{XMLENC_NS}' URI='../cipher'/></root>"
624        );
625        let document = Document::parse(&xml).expect("reference XML");
626        let node = document
627            .descendants()
628            .find(|node| node.has_tag_name((XMLENC_NS, "CipherReference")))
629            .expect("reference");
630        let resources = HashMap::from([("https://example.invalid/cipher".into(), vec![1, 2, 3])]);
631        let mut policy = crate::policy::DecryptionPolicy::default();
632        policy.uris.references = UriTypeSet::ALL;
633        let context = CipherReferenceContext::new(
634            &policy,
635            Some(&resources),
636            crate::XmlBackend::default(),
637            &[],
638        )
639        .expect("context");
640        assert_eq!(context.resolve(node).expect("ciphertext"), [1, 2, 3]);
641    }
642
643    #[test]
644    fn cipher_reference_xpath_inherits_custom_id_registration() {
645        // URI lookup and XPath id() must select the same caller-registered ID.
646        let xml = format!(
647            "<root><data Token='cipher'>YWJj</data>{}</root>",
648            reference(
649                "#cipher",
650                &format!(
651                    "<Transforms><ds:Transform Algorithm='http://www.w3.org/TR/1999/REC-xpath-19991116'><ds:XPath>count(id('cipher')) = 1 and count(ancestor-or-self::node() | id('cipher')) = count(ancestor-or-self::node())</ds:XPath></ds:Transform><ds:Transform Algorithm='{BASE64}'/></Transforms>"
652                )
653            )
654        );
655        let document = Document::parse(&xml).expect("custom ID reference XML must parse");
656        let node = document
657            .descendants()
658            .find(|node| node.has_tag_name((XMLENC_NS, "CipherReference")))
659            .expect("fixture contains a CipherReference");
660        let policy = crate::policy::DecryptionPolicy::default();
661        let registrations = [crate::IdAttributeRegistration::global("Token")];
662        let context = CipherReferenceContext::new(
663            &policy,
664            None,
665            crate::XmlBackend::default(),
666            &registrations,
667        )
668        .expect("default decryption context must be valid");
669        assert_eq!(
670            context.resolve(node).expect("custom ID must reach XPath"),
671            b"abc"
672        );
673    }
674
675    #[test]
676    fn same_document_base64_preserves_original_node_context() {
677        // Same-document dereference must retain the actual text nodes, not
678        // canonicalize and then decode XML markup as if it were base64.
679        let xml = format!(
680            "<root><data xml:id='cipher'>YWJj</data>{}</root>",
681            reference(
682                "#cipher",
683                &format!("<Transforms><ds:Transform Algorithm='{BASE64}'/></Transforms>")
684            )
685        );
686        let document = Document::parse(&xml).expect("reference XML");
687        let node = document
688            .descendants()
689            .find(|node| node.has_tag_name((XMLENC_NS, "CipherReference")))
690            .expect("reference");
691        let policy = crate::policy::DecryptionPolicy::default();
692        let context = CipherReferenceContext::new(&policy, None, crate::XmlBackend::default(), &[])
693            .expect("context");
694        assert_eq!(context.resolve(node).expect("ciphertext"), b"abc");
695    }
696
697    #[test]
698    fn transforms_reject_character_data_before_resource_resolution() {
699        // TransformsType is element-only. Treating arbitrary character data as
700        // trivia would accept a malformed reference and still read its source.
701        let xml = reference(
702            "urn:cipher",
703            &format!("<Transforms>invalid<ds:Transform Algorithm='{BASE64}'/></Transforms>"),
704        );
705        let document = Document::parse(&xml).expect("reference XML");
706        let resources = HashMap::from([("urn:cipher".into(), b"YWJj".to_vec())]);
707        let mut policy = crate::policy::DecryptionPolicy::default();
708        policy.uris.references = UriTypeSet::ALL;
709        let context = CipherReferenceContext::new(
710            &policy,
711            Some(&resources),
712            crate::XmlBackend::default(),
713            &[],
714        )
715        .expect("context");
716        assert!(context.resolve(document.root_element()).is_err());
717    }
718}