1use 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 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
81pub 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
103pub(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 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 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 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 pub fn resolve(&self, node: Node<'_, '_>) -> Result<Vec<u8>, XmlEncError> {
301 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 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 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 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 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 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 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 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 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 ®istrations,
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 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 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}