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 transform_parse_budget(&self) -> XPathSignatureParseBudget {
148 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 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 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 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 pub fn resolve(&self, node: Node<'_, '_>) -> Result<Vec<u8>, XmlEncError> {
307 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 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 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 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 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 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 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 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 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 ®istrations,
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 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 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}