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