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