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 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 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 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 pub fn resolve(&self, node: Node<'_, '_>) -> Result<Vec<u8>, XmlEncError> {
300 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 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 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 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 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 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 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 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 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 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}