1use std::cell::Cell;
23use std::collections::{BTreeMap, HashSet};
24
25mod relationship;
26pub(crate) use relationship::validate_edition as validate_relationship_chain;
27pub use relationship::{RELATIONSHIP_TRANSFORM_URI, RelationshipSelector};
28
29use crate::xml::dom::{Document, Node, NodeId, NodeType};
30use base64::{Engine as _, engine::general_purpose::STANDARD};
31use sha2::{Digest as _, Sha256};
32
33use super::parse::XMLDSIG_NS;
34use super::types::{
35 NodeSetMaterializationBudget, TransformData, TransformError, transform_resource_limit,
36};
37use super::whitespace::is_xml_whitespace_only;
38use super::xpath::{
39 XPathDocumentRelation, XPathWorkBudget, apply_xpath_filter_with_semantics_and_budget,
40 apply_xpath_filter2_with_semantics_and_budget, is_xpath_whitespace,
41 xpath_may_read_mutable_character_data,
42};
43use crate::c14n::xml_base::XmlBaseResolutionBudget;
44use crate::c14n::{self, C14nAlgorithm};
45use crate::document::{
46 DocumentParseSettings, XmlDocumentError, XmlParseWorkBudget,
47 parse_borrowed_with_settings_and_budget,
48};
49#[cfg(test)]
50use crate::hard_limits::XML_DOCUMENT_NODE_CEILING;
51
52pub const ENVELOPED_SIGNATURE_URI: &str = "http://www.w3.org/2000/09/xmldsig#enveloped-signature";
54pub const BASE64_TRANSFORM_URI: &str = "http://www.w3.org/2000/09/xmldsig#base64";
56pub const XPATH_TRANSFORM_URI: &str = "http://www.w3.org/TR/1999/REC-xpath-19991116";
58pub const XPATH_FILTER2_TRANSFORM_URI: &str = "http://www.w3.org/2002/06/xmldsig-filter2";
60pub const DEFAULT_IMPLICIT_C14N_URI: &str = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
63pub const MAX_TRANSFORMS_PER_REFERENCE: usize = crate::hard_limits::REFERENCE_TRANSFORM_CEILING;
69pub(super) const ENVELOPED_SIGNATURE_XPATH_PREFIX: &str = "dsig";
72pub(super) const ENVELOPED_SIGNATURE_XPATH_EXPR: &str = "not(ancestor-or-self::dsig:Signature)";
73pub(super) const MAX_XPATH_EXPRESSION_BYTES: usize =
74 crate::hard_limits::XPATH_EXPRESSION_BYTE_CEILING;
75pub(super) const MAX_XPATH_FILTERS: usize = crate::hard_limits::XPATH_FILTER_COUNT_CEILING;
76pub(super) const MAX_XPATH_EXPRESSIONS_PER_SIGNATURE: usize =
81 crate::hard_limits::XPATH_EXPRESSION_COUNT_CEILING;
82const MAX_XPATH_NAMESPACE_BINDINGS: usize = crate::hard_limits::XPATH_NAMESPACE_BINDING_CEILING;
83const MAX_XPATH_NAMESPACE_BYTES: usize = crate::hard_limits::XPATH_NAMESPACE_BYTE_CEILING;
84const MAX_BASE64_TRANSFORM_INPUT_BYTES: usize =
85 crate::hard_limits::BASE64_TRANSFORM_INPUT_BYTE_CEILING;
86const MAX_BASE64_TRANSFORM_OUTPUT_BYTES: usize =
87 crate::hard_limits::BASE64_TRANSFORM_OUTPUT_BYTE_CEILING;
88const MAX_C14N_OUTPUT_BYTES: usize = crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING;
89const MAX_NODE_SET_FILTER_WORK: usize = crate::hard_limits::NODE_SET_FILTER_WORK_CEILING;
91
92const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
94
95#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
97pub enum XPathHereSemantics {
98 #[default]
101 Specification,
102 XmlSecLegacy,
107}
108
109#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
111pub(crate) struct TransformOptions<'r> {
112 xpath_here_semantics: XPathHereSemantics,
113 allow_internal_dtd: bool,
114 opc_relationship_edition: crate::policy::OpcRelationshipEdition,
115 id_attributes: &'r [crate::xml::IdAttributeRegistration],
116}
117
118pub(crate) struct TransformExecutionBudget {
119 xpath: XPathWorkBudget,
120 base64: Base64WorkBudget,
121 c14n: C14nOutputBudget,
122 node_filter: NodeFilterWorkBudget,
123 node_set_materialization: NodeSetMaterializationBudget,
124 xml_base_resolution: XmlBaseResolutionBudget,
125 xml_parse_work: XmlParseWorkBudget,
126 xml_parse_settings: DocumentParseSettings,
127 state: TransformChainState,
128 opc_workspace: relationship::WorkspaceBudget,
129}
130
131impl Default for TransformExecutionBudget {
132 fn default() -> Self {
133 Self::from_resources(&crate::policy::ResourcePolicy::default())
134 }
135}
136
137pub(super) struct NodeFilterWorkBudget {
138 remaining: Cell<usize>,
139 maximum: usize,
140}
141
142impl Default for NodeFilterWorkBudget {
143 fn default() -> Self {
144 Self {
145 remaining: Cell::new(MAX_NODE_SET_FILTER_WORK),
146 maximum: MAX_NODE_SET_FILTER_WORK,
147 }
148 }
149}
150
151impl NodeFilterWorkBudget {
152 pub(super) fn charge(&self, entries: usize) -> Result<(), TransformError> {
153 let consumed = self.maximum.saturating_sub(self.remaining.get());
154 if !charge_byte_budget(&self.remaining, entries) {
155 return Err(transform_resource_limit(
156 crate::policy::resource_name::NODE_SET_FILTER_WORK,
157 self.maximum,
158 consumed.saturating_add(entries),
159 ));
160 }
161 Ok(())
162 }
163}
164
165struct Base64WorkBudget {
166 remaining_input_bytes: Cell<usize>,
167 remaining_output_bytes: Cell<usize>,
168 max_input_bytes: usize,
169 max_output_bytes: usize,
170}
171
172struct C14nOutputBudget {
173 remaining: Cell<usize>,
174 max_bytes: usize,
175}
176
177fn charge_byte_budget(remaining: &Cell<usize>, bytes: usize) -> bool {
178 let Some(next) = remaining.get().checked_sub(bytes) else {
179 remaining.set(0);
180 return false;
181 };
182 remaining.set(next);
183 true
184}
185
186impl Default for C14nOutputBudget {
187 fn default() -> Self {
188 Self {
189 remaining: Cell::new(MAX_C14N_OUTPUT_BYTES),
190 max_bytes: MAX_C14N_OUTPUT_BYTES,
191 }
192 }
193}
194
195impl C14nOutputBudget {
196 fn with_limit(max_bytes: usize) -> Self {
197 Self {
198 remaining: Cell::new(max_bytes),
199 max_bytes,
200 }
201 }
202
203 fn remaining(&self) -> usize {
204 self.remaining.get()
205 }
206
207 fn charge(&self, bytes: usize) -> Result<(), crate::policy::PolicyViolation> {
208 let consumed = self.max_bytes.saturating_sub(self.remaining.get());
209 if !charge_byte_budget(&self.remaining, bytes) {
210 return Err(crate::policy::PolicyViolation::ResourceLimit {
211 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
212 maximum: self.max_bytes,
213 actual: consumed.saturating_add(bytes),
214 });
215 }
216 Ok(())
217 }
218
219 fn exhaust(&self) {
220 self.remaining.set(0);
221 }
222}
223
224#[cfg(test)]
225mod c14n_budget_regression_tests {
226 use super::*;
227 use crate::c14n::C14nMode;
228 use crate::xml::dom::Document;
229 use crate::xmldsig::types::NodeSet;
230
231 #[test]
232 fn bounded_c14n_failure_exhausts_the_shared_budget() {
233 let document = Document::parse("<root><payload>more than eight bytes</payload></root>")
234 .expect("test XML must parse");
235 let budget = TransformExecutionBudget::with_c14n_limit(8);
236
237 let error = execute_transforms_with_options_and_budget(
238 document.root_element(),
239 TransformData::NodeSet(
240 NodeSet::entire_document_without_comments(&document)
241 .expect("test document must fit the node-set ceiling"),
242 ),
243 &[Transform::C14n(C14nAlgorithm::new(
244 C14nMode::Inclusive1_0,
245 false,
246 ))],
247 TransformOptions::default(),
248 &budget,
249 )
250 .expect_err("canonicalized output must exceed the shared budget");
251
252 assert!(matches!(
253 error,
254 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
255 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
256 maximum: 8,
257 ..
258 })
259 ));
260 assert_eq!(
261 budget.remaining_c14n_output(),
262 0,
263 "a failed bounded render must not leave the same allowance reusable"
264 );
265 }
266}
267
268impl Default for Base64WorkBudget {
269 fn default() -> Self {
270 Self {
271 remaining_input_bytes: Cell::new(MAX_BASE64_TRANSFORM_INPUT_BYTES),
272 remaining_output_bytes: Cell::new(MAX_BASE64_TRANSFORM_OUTPUT_BYTES),
273 max_input_bytes: MAX_BASE64_TRANSFORM_INPUT_BYTES,
274 max_output_bytes: MAX_BASE64_TRANSFORM_OUTPUT_BYTES,
275 }
276 }
277}
278
279impl Base64WorkBudget {
280 fn charge_input(&self, bytes: usize) -> Result<(), TransformError> {
281 let consumed = self
282 .max_input_bytes
283 .saturating_sub(self.remaining_input_bytes.get());
284 if !charge_byte_budget(&self.remaining_input_bytes, bytes) {
285 return Err(transform_resource_limit(
286 crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
287 self.max_input_bytes,
288 consumed.saturating_add(bytes),
289 ));
290 }
291 Ok(())
292 }
293
294 fn ensure_output_capacity(&self, bytes: usize) -> Result<(), TransformError> {
295 let consumed = self
296 .max_output_bytes
297 .saturating_sub(self.remaining_output_bytes.get());
298 if bytes > self.remaining_output_bytes.get() {
299 return Err(transform_resource_limit(
300 crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
301 self.max_output_bytes,
302 consumed.saturating_add(bytes),
303 ));
304 }
305 Ok(())
306 }
307
308 fn charge_output(&self, bytes: usize) -> Result<(), TransformError> {
309 self.ensure_output_capacity(bytes)?;
310 let charged = charge_byte_budget(&self.remaining_output_bytes, bytes);
311 debug_assert!(charged, "preflighted Base64 output charge must fit");
312 Ok(())
313 }
314}
315
316#[cfg(test)]
317impl TransformExecutionBudget {
318 pub(crate) fn with_xpath_limit(limit: usize) -> Self {
319 Self {
320 xpath: XPathWorkBudget::with_limit(limit),
321 base64: Base64WorkBudget::default(),
322 c14n: C14nOutputBudget::default(),
323 node_filter: NodeFilterWorkBudget::default(),
324 node_set_materialization: NodeSetMaterializationBudget::default(),
325 xml_base_resolution: XmlBaseResolutionBudget::default(),
326 xml_parse_work: XmlParseWorkBudget::from_resources(
327 &crate::policy::ResourcePolicy::default(),
328 ),
329 xml_parse_settings: DocumentParseSettings::default(),
330 state: TransformChainState::default(),
331 opc_workspace: relationship::WorkspaceBudget::default(),
332 }
333 }
334
335 fn with_node_filter_limit(limit: usize) -> Self {
336 Self {
337 xpath: XPathWorkBudget::default(),
338 base64: Base64WorkBudget::default(),
339 c14n: C14nOutputBudget::default(),
340 node_filter: NodeFilterWorkBudget {
341 remaining: Cell::new(limit),
342 maximum: limit,
343 },
344 node_set_materialization: NodeSetMaterializationBudget::default(),
345 xml_base_resolution: XmlBaseResolutionBudget::default(),
346 xml_parse_work: XmlParseWorkBudget::from_resources(
347 &crate::policy::ResourcePolicy::default(),
348 ),
349 xml_parse_settings: DocumentParseSettings::default(),
350 state: TransformChainState::default(),
351 opc_workspace: relationship::WorkspaceBudget::default(),
352 }
353 }
354
355 pub(crate) fn with_node_set_materialization_limit(limit: usize) -> Self {
356 Self {
357 xpath: XPathWorkBudget::default(),
358 base64: Base64WorkBudget::default(),
359 c14n: C14nOutputBudget::default(),
360 node_filter: NodeFilterWorkBudget::default(),
361 node_set_materialization: NodeSetMaterializationBudget::with_limit(limit),
362 opc_workspace: relationship::WorkspaceBudget::default(),
363 xml_base_resolution: XmlBaseResolutionBudget::default(),
364 xml_parse_work: XmlParseWorkBudget::from_resources(
365 &crate::policy::ResourcePolicy::default(),
366 ),
367 xml_parse_settings: DocumentParseSettings::default(),
368 state: TransformChainState::default(),
369 }
370 }
371
372 pub(crate) fn with_c14n_limit(max_bytes: usize) -> Self {
373 Self {
374 c14n: C14nOutputBudget::with_limit(max_bytes),
375 ..Self::default()
376 }
377 }
378}
379
380impl TransformExecutionBudget {
381 pub(crate) fn from_resources(resources: &crate::policy::ResourcePolicy) -> Self {
382 Self {
383 xpath: XPathWorkBudget::with_limits(resources),
384 base64: Base64WorkBudget {
385 remaining_input_bytes: Cell::new(resources.max_base64_transform_input_bytes),
386 remaining_output_bytes: Cell::new(resources.max_base64_transform_output_bytes),
387 max_input_bytes: resources.max_base64_transform_input_bytes,
388 max_output_bytes: resources.max_base64_transform_output_bytes,
389 },
390 c14n: C14nOutputBudget::with_limit(resources.effective_canonicalized_bytes()),
391 node_filter: NodeFilterWorkBudget {
392 remaining: Cell::new(resources.max_node_set_filter_work),
393 maximum: resources.max_node_set_filter_work,
394 },
395 node_set_materialization: NodeSetMaterializationBudget::with_limits(
396 resources.max_node_set_entries,
397 resources.max_node_set_owned_string_bytes,
398 resources.max_node_set_cumulative_owned_string_bytes,
399 ),
400 xml_base_resolution: XmlBaseResolutionBudget::with_limits(
401 resources.effective_xml_base_components(),
402 resources.effective_xml_base_resolution_bytes(),
403 ),
404 xml_parse_work: XmlParseWorkBudget::from_resources(resources),
405 xml_parse_settings: DocumentParseSettings::for_transform_output(resources),
406 state: TransformChainState::default(),
407 opc_workspace: relationship::WorkspaceBudget::new(resources.max_opc_workspace_bytes),
408 }
409 }
410
411 pub(crate) fn with_xml_backend(mut self, backend: crate::XmlBackend) -> Self {
412 self.xml_parse_settings = self.xml_parse_settings.with_backend(backend);
413 self
414 }
415
416 pub(crate) fn charge_c14n_output(&self, bytes: usize) -> Result<(), TransformError> {
417 self.c14n.charge(bytes).map_err(TransformError::from)
418 }
419
420 pub(crate) fn charge_c14n_output_policy(
421 &self,
422 bytes: usize,
423 ) -> Result<(), crate::policy::PolicyViolation> {
424 self.c14n.charge(bytes)
425 }
426
427 pub(crate) fn remaining_c14n_output(&self) -> usize {
428 self.c14n.remaining()
429 }
430
431 pub(crate) fn c14n_output_limit(&self) -> usize {
432 self.c14n.max_bytes
433 }
434
435 pub(crate) fn node_set_materialization(&self) -> &NodeSetMaterializationBudget {
436 &self.node_set_materialization
437 }
438
439 pub(crate) fn xml_base_resolution(&self) -> &XmlBaseResolutionBudget {
440 &self.xml_base_resolution
441 }
442
443 pub(crate) fn xml_parse_work(&self) -> &XmlParseWorkBudget {
444 &self.xml_parse_work
445 }
446
447 pub(crate) fn charge_xpath_work(&self, work: usize) -> Result<(), TransformError> {
448 self.xpath.charge(work)
449 }
450
451 pub(crate) fn validate_xpath_context_evaluations(
452 &self,
453 actual: usize,
454 ) -> Result<(), TransformError> {
455 self.xpath.validate_context_evaluations(actual)
456 }
457
458 pub(crate) fn charge_node_filter_work(&self, nodes: usize) -> Result<(), TransformError> {
459 self.node_filter.charge(nodes)
460 }
461}
462
463impl<'r> TransformOptions<'r> {
464 pub(crate) fn id_attributes(
465 mut self,
466 registrations: &'r [crate::xml::IdAttributeRegistration],
467 ) -> Self {
468 self.id_attributes = registrations;
469 self
470 }
471 pub(crate) fn opc_relationship_edition(
472 mut self,
473 edition: crate::policy::OpcRelationshipEdition,
474 ) -> Self {
475 self.opc_relationship_edition = edition;
476 self
477 }
478 #[must_use]
480 pub(crate) fn xpath_here_semantics(mut self, semantics: XPathHereSemantics) -> Self {
481 self.xpath_here_semantics = semantics;
482 self
483 }
484
485 #[must_use]
488 pub(crate) fn allow_internal_dtd(mut self, enabled: bool) -> Self {
489 self.allow_internal_dtd = enabled;
490 self
491 }
492
493 pub(crate) fn here_semantics(self) -> XPathHereSemantics {
494 self.xpath_here_semantics
495 }
496
497 pub(crate) fn internal_dtd_allowed(self) -> bool {
498 self.allow_internal_dtd
499 }
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, Eq)]
503struct XPathHereNodes {
504 specification_xpath_element: NodeId,
505 xmlsec_legacy_transform_element: NodeId,
506 document: XPathDocumentIdentity,
507}
508
509#[derive(Debug, Clone, Copy, PartialEq, Eq)]
510struct XPathDocumentIdentity([u8; 32]);
511
512impl XPathDocumentIdentity {
513 fn from_document(document: &Document<'_>) -> Self {
514 #[cfg(test)]
515 XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(|count| count.set(count.get() + 1));
516 Self(Sha256::digest(document.input_text().as_bytes()).into())
517 }
518}
519
520#[cfg(test)]
521thread_local! {
522 static XPATH_DOCUMENT_IDENTITY_COMPUTATIONS: Cell<usize> = const { Cell::new(0) };
523}
524
525#[derive(Default)]
526struct TransformChainState {
527 xpath_document_identity: Cell<Option<CachedXPathDocumentIdentity>>,
528}
529
530#[derive(Clone, Copy)]
531struct CachedXPathDocumentIdentity {
532 document: *const (),
533 identity: XPathDocumentIdentity,
534}
535
536impl TransformChainState {
537 fn begin_chain(&self) {
538 self.xpath_document_identity.set(None);
542 }
543
544 fn xpath_document_identity(&self, document: &Document<'_>) -> XPathDocumentIdentity {
545 let document_key = std::ptr::from_ref(document).cast::<()>();
546 if let Some(cached) = self.xpath_document_identity.get()
547 && cached.document == document_key
548 {
549 return cached.identity;
550 }
551 let identity = XPathDocumentIdentity::from_document(document);
552 self.xpath_document_identity
553 .set(Some(CachedXPathDocumentIdentity {
554 document: document_key,
555 identity,
556 }));
557 identity
558 }
559
560 fn document_reparsed(&self) {
561 self.xpath_document_identity.set(None);
562 }
563}
564
565struct TransformExecutionContext<'a> {
566 options: TransformOptions<'a>,
567 budget: &'a TransformExecutionBudget,
568 state: &'a TransformChainState,
569 xml_parse: &'a XmlParseWorkBudget,
570}
571
572#[derive(Debug, Clone, PartialEq, Eq)]
574pub struct XPathExpression {
575 expression: String,
576 namespaces: BTreeMap<String, String>,
577 here_nodes: Option<XPathHereNodes>,
578}
579
580impl XPathExpression {
581 pub fn new(expression: impl Into<String>) -> Self {
583 Self {
584 expression: expression.into(),
585 namespaces: BTreeMap::new(),
586 here_nodes: None,
587 }
588 }
589
590 pub fn with_namespace(mut self, prefix: impl Into<String>, uri: impl Into<String>) -> Self {
592 self.namespaces.insert(prefix.into(), uri.into());
593 self
594 }
595
596 pub fn expression(&self) -> &str {
598 &self.expression
599 }
600
601 pub fn namespaces(&self) -> &BTreeMap<String, String> {
603 &self.namespaces
604 }
605
606 pub(crate) fn here_context_node(&self, semantics: XPathHereSemantics) -> Option<NodeId> {
607 self.here_nodes.map(|nodes| match semantics {
608 XPathHereSemantics::Specification => nodes.specification_xpath_element,
609 XPathHereSemantics::XmlSecLegacy => nodes.xmlsec_legacy_transform_element,
610 })
611 }
612
613 fn parsed_document_identity(&self) -> Option<XPathDocumentIdentity> {
614 self.here_nodes.map(|nodes| nodes.document)
615 }
616}
617
618#[derive(Debug, Clone, Copy, PartialEq, Eq)]
620pub enum XPathFilterOperation {
621 Intersect,
623 Subtract,
625 Union,
627}
628
629impl XPathFilterOperation {
630 pub(crate) fn as_str(self) -> &'static str {
631 match self {
632 Self::Intersect => "intersect",
633 Self::Subtract => "subtract",
634 Self::Union => "union",
635 }
636 }
637}
638
639#[derive(Debug, Clone, PartialEq, Eq)]
641pub struct XPathFilter {
642 operation: XPathFilterOperation,
643 xpath: XPathExpression,
644}
645
646impl XPathFilter {
647 pub fn new(operation: XPathFilterOperation, xpath: XPathExpression) -> Self {
649 Self { operation, xpath }
650 }
651
652 pub fn operation(&self) -> XPathFilterOperation {
654 self.operation
655 }
656
657 pub fn xpath(&self) -> &XPathExpression {
659 &self.xpath
660 }
661}
662
663#[derive(Debug, Clone, PartialEq, Eq)]
665pub enum Transform {
666 Enveloped,
671
672 XpathExcludeAllSignatures,
678
679 XPath(XPathExpression),
681
682 XPathFilter2(Vec<XPathFilter>),
684
685 C14n(C14nAlgorithm),
689
690 Base64Decode,
698
699 Relationship(Vec<RelationshipSelector>),
706}
707
708impl Transform {
709 pub(crate) fn algorithm_uri(&self) -> &'static str {
710 match self {
711 Self::Enveloped => ENVELOPED_SIGNATURE_URI,
712 Self::XpathExcludeAllSignatures | Self::XPath(_) => XPATH_TRANSFORM_URI,
713 Self::XPathFilter2(_) => XPATH_FILTER2_TRANSFORM_URI,
714 Self::C14n(algorithm) => algorithm.uri(),
715 Self::Base64Decode => BASE64_TRANSFORM_URI,
716 Self::Relationship(_) => RELATIONSHIP_TRANSFORM_URI,
717 }
718 }
719}
720
721#[cfg(test)]
729pub(crate) fn apply_transform<'a>(
730 signature_node: Node<'a, 'a>,
731 transform: &Transform,
732 input: TransformData<'a>,
733) -> Result<TransformData<'a>, TransformError> {
734 let budget = TransformExecutionBudget::default();
735 let state = TransformChainState::default();
736 apply_transform_with_options_and_state(
737 signature_node,
738 transform,
739 input,
740 TransformOptions::default(),
741 &budget,
742 &state,
743 &budget.xml_parse_work,
744 )
745}
746
747#[cfg(test)]
748pub(super) fn apply_transform_with_options<'s, 'd>(
749 signature_node: Node<'s, 's>,
750 transform: &Transform,
751 input: TransformData<'d>,
752 options: TransformOptions<'_>,
753 budget: &TransformExecutionBudget,
754) -> Result<TransformData<'d>, TransformError> {
755 let state = TransformChainState::default();
756 apply_transform_with_options_and_state(
757 signature_node,
758 transform,
759 input,
760 options,
761 budget,
762 &state,
763 &budget.xml_parse_work,
764 )
765}
766
767fn apply_transform_with_options_and_state<'s, 'd>(
768 signature_node: Node<'s, 's>,
769 transform: &Transform,
770 input: TransformData<'d>,
771 options: TransformOptions<'_>,
772 budget: &TransformExecutionBudget,
773 state: &TransformChainState,
774 xml_parse: &XmlParseWorkBudget,
775) -> Result<TransformData<'d>, TransformError> {
776 match transform {
777 Transform::Relationship(selectors) => {
778 let bytes = finalize_transform_data(input, budget)?;
779 xml_parse.charge_policy(bytes.len())?;
780 let xml = crate::xml_input_shared::decode_xml_bounded_with_encoding_guard(
784 &bytes,
785 budget.xml_parse_settings.max_bytes,
786 |physical, declaration| {
787 if !physical.eq_ignore_ascii_case("UTF-8")
788 && !physical.eq_ignore_ascii_case("UTF-16LE")
789 && !physical.eq_ignore_ascii_case("UTF-16BE")
790 {
791 return Err(crate::encoding::XmlEncodingError::ForbiddenEncoding(
792 physical.into(),
793 ));
794 }
795 if let Some(label) = declaration
796 && !label.eq_ignore_ascii_case("UTF-8")
797 && !label.eq_ignore_ascii_case("UTF-16")
798 {
799 return Err(crate::encoding::XmlEncodingError::ForbiddenEncoding(
800 label.into(),
801 ));
802 }
803 Ok(())
804 },
805 )
806 .map_err(map_transform_xml_decode_error)?;
807 let settings = DocumentParseSettings {
808 allow_dtd: false,
809 ..budget.xml_parse_settings
810 };
811 let document = parse_borrowed_with_settings_and_budget(&xml, settings, Some(xml_parse))
812 .map_err(|error| map_transform_xml_parse_error(error, settings))?;
813 state.document_reparsed();
814 relationship::normalize(
815 &document,
816 selectors,
817 options.opc_relationship_edition,
818 budget,
819 )
820 .map(TransformData::Binary)
821 }
822 Transform::Enveloped => {
823 let mut nodes = input.into_node_set()?;
824 if !std::ptr::eq(signature_node.document(), nodes.document()) {
833 return Err(TransformError::CrossDocumentSignatureNode);
834 }
835 budget.node_filter.charge(nodes.len())?;
836 nodes.exclude_subtree(signature_node);
837 Ok(TransformData::NodeSet(nodes))
838 }
839 Transform::XpathExcludeAllSignatures => {
840 let mut nodes = input.into_node_set()?;
841 let doc = nodes.document();
842
843 budget.xpath.validate_context_evaluations(nodes.len())?;
848 budget.xpath.charge(doc.descendants().count())?;
849
850 for node in doc.descendants().filter(|node| {
851 node.is_element()
852 && node.tag_name().name() == "Signature"
853 && node.tag_name().namespace() == Some(XMLDSIG_NS)
854 }) {
855 budget.node_filter.charge(nodes.len())?;
856 nodes.exclude_subtree(node);
857 }
858
859 Ok(TransformData::NodeSet(nodes))
860 }
861 Transform::XPath(xpath) => {
862 let nodes = input.into_node_set()?;
863 let document_relation = xpath_document_relation(
864 signature_node.document(),
865 nodes.document(),
866 std::iter::once(xpath),
867 state,
868 );
869 Ok(TransformData::NodeSet(
870 apply_xpath_filter_with_semantics_and_budget(
871 nodes,
872 xpath,
873 super::xpath::XPathRequestContext::new(
874 options.here_semantics(),
875 options.id_attributes,
876 ),
877 document_relation,
878 &budget.xpath,
879 &budget.node_filter,
880 &budget.node_set_materialization,
881 )?,
882 ))
883 }
884 Transform::XPathFilter2(filters) => {
885 let nodes = input.into_node_set()?;
886 let document_relation = xpath_document_relation(
887 signature_node.document(),
888 nodes.document(),
889 filters.iter().map(XPathFilter::xpath),
890 state,
891 );
892 Ok(TransformData::NodeSet(
893 apply_xpath_filter2_with_semantics_and_budget(
894 nodes,
895 filters,
896 super::xpath::XPathRequestContext::new(
897 options.here_semantics(),
898 options.id_attributes,
899 ),
900 document_relation,
901 &budget.xpath,
902 &budget.node_filter,
903 &budget.node_set_materialization,
904 )?,
905 ))
906 }
907 Transform::C14n(algo) => {
908 let nodes = input.into_node_set()?;
909 let mut output = Vec::new();
910 c14n::canonicalize_with_visibility_and_position_bounded_with_xml_base_budget(
911 nodes.document(),
912 Some(&nodes),
913 algo,
914 None,
915 budget.c14n.remaining(),
916 budget.xml_base_resolution(),
917 &mut output,
918 )
919 .map_err(|error| map_c14n_limit_error(error, &budget.c14n))?;
920 budget.c14n.charge(output.len())?;
921 Ok(TransformData::Binary(output))
922 }
923 Transform::Base64Decode => {
924 let mut normalized = Vec::new();
925 match input {
926 TransformData::Binary(bytes) => {
927 append_normalized_base64(&bytes, &mut normalized, &budget.base64)?;
928 }
929 TransformData::NodeSet(nodes) => {
930 for node in nodes.document().descendants() {
931 if nodes.contains(node) && node.is_text() {
932 append_normalized_base64(
933 node.text().unwrap_or_default().as_bytes(),
934 &mut normalized,
935 &budget.base64,
936 )?;
937 }
938 }
939 }
940 }
941 Ok(TransformData::Binary(decode_base64_transform(
942 &normalized,
943 &budget.base64,
944 )?))
945 }
946 }
947}
948
949fn xpath_document_relation<'a>(
950 signature_document: &Document<'_>,
951 input_document: &Document<'_>,
952 expressions: impl IntoIterator<Item = &'a XPathExpression>,
953 state: &TransformChainState,
954) -> XPathDocumentRelation {
955 if matches!(
956 XPathDocumentRelation::between(signature_document, input_document),
957 XPathDocumentRelation::CrossDocument
958 ) {
959 return XPathDocumentRelation::CrossDocument;
960 }
961
962 let mut parsed_identities = expressions
963 .into_iter()
964 .filter_map(XPathExpression::parsed_document_identity);
965 let Some(first) = parsed_identities.next() else {
966 return XPathDocumentRelation::SameDocument;
967 };
968 let input_identity = state.xpath_document_identity(input_document);
969 if first == input_identity && parsed_identities.all(|identity| identity == input_identity) {
970 XPathDocumentRelation::SameDocument
971 } else {
972 XPathDocumentRelation::CrossDocument
973 }
974}
975
976fn append_normalized_base64(
982 encoded: &[u8],
983 normalized: &mut Vec<u8>,
984 budget: &Base64WorkBudget,
985) -> Result<(), TransformError> {
986 budget.charge_input(encoded.len())?;
987
988 let additional = encoded
989 .iter()
990 .filter(|byte| is_rfc2045_base64_byte(**byte))
991 .count();
992 normalized.reserve(additional);
993 normalized.extend(
994 encoded
995 .iter()
996 .copied()
997 .filter(|byte| is_rfc2045_base64_byte(*byte)),
998 );
999 Ok(())
1000}
1001
1002fn is_rfc2045_base64_byte(byte: u8) -> bool {
1003 byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/' | b'=')
1004}
1005
1006fn decode_base64_transform(
1007 normalized: &[u8],
1008 budget: &Base64WorkBudget,
1009) -> Result<Vec<u8>, TransformError> {
1010 let padding = normalized
1011 .iter()
1012 .rev()
1013 .take_while(|byte| **byte == b'=')
1014 .count();
1015 let decoded_len = base64::decoded_len_estimate(normalized.len()).saturating_sub(padding);
1016 budget.ensure_output_capacity(decoded_len)?;
1017
1018 let mut decoded = vec![0_u8; decoded_len];
1019 let written = STANDARD
1020 .decode_slice(normalized, &mut decoded)
1021 .map_err(|error| TransformError::Base64(error.to_string()))?;
1022 budget.charge_output(written)?;
1023 decoded.truncate(written);
1024 Ok(decoded)
1025}
1026
1027pub fn execute_transforms<'a>(
1036 signature_node: Node<'a, 'a>,
1037 initial_data: TransformData<'a>,
1038 transforms: &[Transform],
1039) -> Result<Vec<u8>, TransformError> {
1040 execute_transforms_with_policy(
1041 signature_node,
1042 initial_data,
1043 transforms,
1044 &crate::policy::VerificationPolicy::default(),
1045 )
1046}
1047
1048pub fn execute_transforms_with_policy<'a>(
1053 signature_node: Node<'a, 'a>,
1054 initial_data: TransformData<'a>,
1055 transforms: &[Transform],
1056 policy: &crate::policy::VerificationPolicy,
1057) -> Result<Vec<u8>, TransformError> {
1058 policy.validate()?;
1059 if transforms.len() > policy.resources.max_transforms_per_reference {
1060 return Err(transform_resource_limit(
1061 crate::policy::resource_name::REFERENCE_TRANSFORMS,
1062 policy.resources.max_transforms_per_reference,
1063 transforms.len(),
1064 ));
1065 }
1066 let parameters =
1067 relationship::WorkspaceBudget::parameters(policy.resources.max_opc_parameter_bytes);
1068 for transform in transforms {
1069 if let Transform::Relationship(selectors) = transform {
1070 relationship::validate_owned_selectors(selectors, ¶meters)?;
1071 }
1072 }
1073 validate_transform_policy(
1074 matches!(&initial_data, TransformData::Binary(_)),
1075 transforms,
1076 policy.transforms.allowed_algorithms.as_ref(),
1077 "transform execution",
1078 )?;
1079 let options = TransformOptions::default()
1080 .opc_relationship_edition(policy.transforms.opc_relationship_edition)
1081 .xpath_here_semantics(policy.transforms.xpath_here_semantics)
1082 .allow_internal_dtd(policy.xml.allow_internal_dtd);
1083 let budget = TransformExecutionBudget::from_resources(&policy.resources);
1084 execute_transforms_with_options_and_budget(
1085 signature_node,
1086 initial_data,
1087 transforms,
1088 options,
1089 &budget,
1090 )
1091}
1092
1093pub(crate) fn execute_transforms_with_options_and_budget<'a>(
1094 signature_node: Node<'a, 'a>,
1095 initial_data: TransformData<'a>,
1096 transforms: &[Transform],
1097 options: TransformOptions<'_>,
1098 budget: &TransformExecutionBudget,
1099) -> Result<Vec<u8>, TransformError> {
1100 execute_reference_transforms_with_budget(
1101 signature_node,
1102 Some(signature_node),
1103 initial_data,
1104 transforms,
1105 options,
1106 budget,
1107 budget.xml_parse_work(),
1108 )
1109}
1110
1111pub(crate) fn execute_reference_transforms_with_budget<'a>(
1114 source_node: Node<'a, 'a>,
1115 enveloped_signature: Option<Node<'a, 'a>>,
1116 initial_data: TransformData<'a>,
1117 transforms: &[Transform],
1118 options: TransformOptions<'_>,
1119 budget: &TransformExecutionBudget,
1120 xml_parse: &XmlParseWorkBudget,
1121) -> Result<Vec<u8>, TransformError> {
1122 ensure_transform_count(transforms.len())?;
1123 relationship::validate_edition(transforms, options.opc_relationship_edition)?;
1124 budget.state.begin_chain();
1125 let context = TransformExecutionContext {
1126 options,
1127 budget,
1128 state: &budget.state,
1129 xml_parse,
1130 };
1131 execute_transform_chain(
1132 source_node,
1133 enveloped_signature,
1134 initial_data,
1135 transforms,
1136 None,
1137 None,
1138 &context,
1139 )
1140 .map(|output| output.bytes)
1141}
1142
1143pub(crate) struct TransformDependencyOutput {
1145 pub(crate) dependencies: HashSet<usize>,
1146}
1147
1148struct TransformChainOutput {
1149 bytes: Vec<u8>,
1150 dependencies: HashSet<usize>,
1151}
1152
1153struct DependencyTracking {
1154 active_nodes: Vec<TrackedDependencyNode>,
1157 dormant_indexes: HashSet<usize>,
1162 opaque_dependencies: HashSet<usize>,
1165 canonical_positions: Option<Vec<CanonicalDependencyPosition>>,
1166}
1167
1168struct TrackedDependencyNode {
1169 index: usize,
1170 node_id: NodeId,
1171 node_type: NodeType,
1172}
1173
1174struct CanonicalDependencyPosition {
1175 index: usize,
1176 position: usize,
1177 node_type: NodeType,
1178}
1179
1180pub(crate) fn execute_transforms_with_dependency_nodes<'a>(
1186 signature_node: Node<'a, 'a>,
1187 initial_data: TransformData<'a>,
1188 transforms: &[Transform],
1189 options: TransformOptions<'_>,
1190 budget: &TransformExecutionBudget,
1191 tracked_nodes: Vec<(usize, NodeId)>,
1192) -> Result<TransformDependencyOutput, TransformError> {
1193 ensure_transform_count(transforms.len())?;
1194 relationship::validate_edition(transforms, options.opc_relationship_edition)?;
1195 budget.state.begin_chain();
1196 let mut active_nodes = Vec::with_capacity(tracked_nodes.len());
1197 let mut opaque_dependencies = HashSet::new();
1198 let mut dormant_indexes = HashSet::new();
1199 for (index, node_id) in tracked_nodes {
1200 if let Some(node) = signature_node.document().get_node(node_id) {
1201 let belongs_to_input = match &initial_data {
1202 TransformData::NodeSet(nodes) => nodes.contains(node),
1203 TransformData::Binary(_) => false,
1204 };
1205 if belongs_to_input {
1206 active_nodes.push(TrackedDependencyNode {
1207 index,
1208 node_id,
1209 node_type: node.node_type(),
1210 });
1211 } else {
1212 dormant_indexes.insert(index);
1213 }
1214 } else {
1215 opaque_dependencies.insert(index);
1216 }
1217 }
1218 let context = TransformExecutionContext {
1219 options,
1220 budget,
1221 state: &budget.state,
1222 xml_parse: budget.xml_parse_work(),
1223 };
1224 let output = execute_transform_chain(
1225 signature_node,
1226 Some(signature_node),
1227 initial_data,
1228 transforms,
1229 None,
1230 Some(DependencyTracking {
1231 active_nodes,
1232 dormant_indexes,
1233 opaque_dependencies,
1234 canonical_positions: None,
1235 }),
1236 &context,
1237 )?;
1238 Ok(TransformDependencyOutput {
1239 dependencies: output.dependencies,
1240 })
1241}
1242
1243fn ensure_transform_count(count: usize) -> Result<(), TransformError> {
1244 if count > MAX_TRANSFORMS_PER_REFERENCE {
1245 return Err(transform_resource_limit(
1246 crate::policy::resource_name::REFERENCE_TRANSFORMS,
1247 MAX_TRANSFORMS_PER_REFERENCE,
1248 count,
1249 ));
1250 }
1251 Ok(())
1252}
1253
1254fn execute_transform_chain<'s, 'e, 'd>(
1255 source_signature: Node<'s, 's>,
1256 enveloped_signature: Option<Node<'e, 'e>>,
1257 data: TransformData<'d>,
1258 transforms: &[Transform],
1259 canonical_signature_position: Option<Option<usize>>,
1260 mut dependency_tracking: Option<DependencyTracking>,
1261 context: &TransformExecutionContext<'_>,
1262) -> Result<TransformChainOutput, TransformError> {
1263 let Some((transform, remaining)) = transforms.split_first() else {
1264 if let (TransformData::NodeSet(nodes), Some(tracking)) = (&data, &mut dependency_tracking) {
1265 tracking.active_nodes.retain(|tracked| {
1266 nodes
1267 .document()
1268 .get_node(tracked.node_id)
1269 .is_some_and(|node| nodes.contains(node))
1270 });
1271 }
1272 let bytes = finalize_transform_data(data, context.budget)?;
1273 return Ok(TransformChainOutput {
1274 bytes,
1275 dependencies: dependency_indexes(dependency_tracking),
1276 });
1277 };
1278
1279 if transform_requires_node_set(transform)
1280 && let TransformData::Binary(bytes) = data
1281 {
1282 context.xml_parse.charge_policy(bytes.len())?;
1290 let xml =
1291 crate::encoding::decode_xml_octets(&bytes, context.budget.xml_parse_settings.max_bytes)
1292 .map_err(map_transform_xml_decode_error)?;
1293 let settings = DocumentParseSettings {
1294 allow_dtd: context.options.internal_dtd_allowed(),
1295 ..context.budget.xml_parse_settings
1296 };
1297 let document =
1298 parse_borrowed_with_settings_and_budget(&xml, settings, Some(context.xml_parse))
1299 .map_err(|error| map_transform_xml_parse_error(error, settings))?;
1300 context.state.document_reparsed();
1301 let nodes = super::types::NodeSet::entire_document_with_comments_with_budget(
1302 &document,
1303 &context.budget.node_set_materialization,
1304 )?;
1305 if let Some(tracking) = &mut dependency_tracking
1306 && let Some(positions) = tracking.canonical_positions.take()
1307 {
1308 let mut remapped = Vec::with_capacity(positions.len());
1309 for tracked in positions {
1310 if let Some(node) = document.descendants().find(|node| {
1311 node.node_type() == tracked.node_type && node.range().start == tracked.position
1312 }) {
1313 remapped.push(TrackedDependencyNode {
1314 index: tracked.index,
1315 node_id: node.id(),
1316 node_type: tracked.node_type,
1317 });
1318 } else {
1319 tracking.opaque_dependencies.insert(tracked.index);
1322 }
1323 }
1324 tracking.active_nodes = remapped;
1325 }
1326 return match canonical_signature_position {
1327 Some(Some(position)) => {
1328 let remapped = document
1329 .descendants()
1330 .find(|node| node.is_element() && node.range().start == position)
1331 .filter(|node| {
1332 enveloped_signature
1333 .is_some_and(|source| node.tag_name() == source.tag_name())
1334 })
1335 .ok_or(TransformError::CrossDocumentSignatureNode)?;
1336 execute_transform_chain(
1337 source_signature,
1338 Some(remapped),
1339 TransformData::NodeSet(nodes),
1340 transforms,
1341 None,
1342 dependency_tracking,
1343 context,
1344 )
1345 }
1346 Some(None) => execute_transform_chain(
1347 source_signature,
1348 None,
1349 TransformData::NodeSet(nodes),
1350 transforms,
1351 None,
1352 dependency_tracking,
1353 context,
1354 ),
1355 None => execute_transform_chain(
1356 source_signature,
1357 None,
1361 TransformData::NodeSet(nodes),
1362 transforms,
1363 None,
1364 dependency_tracking,
1365 context,
1366 ),
1367 };
1368 }
1369
1370 if let Transform::C14n(algo) = transform
1371 && let TransformData::NodeSet(nodes) = &data
1372 {
1373 let tracked_element = enveloped_signature
1374 .filter(|signature| std::ptr::eq(signature.document(), nodes.document()))
1375 .filter(|signature| nodes.contains(*signature))
1376 .map(|signature| signature.id());
1377 let mut output = Vec::new();
1378 if let Some(tracking) = &mut dependency_tracking {
1379 tracking.active_nodes.retain(|tracked| {
1380 nodes
1381 .document()
1382 .get_node(tracked.node_id)
1383 .is_some_and(|node| nodes.contains(node))
1384 });
1385 tracking.dormant_indexes.clear();
1386 }
1387 let position = if let Some(tracking) = &mut dependency_tracking {
1388 let mut tracked_ids = tracking
1389 .active_nodes
1390 .iter()
1391 .map(|tracked| tracked.node_id)
1392 .collect::<Vec<_>>();
1393 if let Some(signature_id) = tracked_element
1394 && !tracked_ids.contains(&signature_id)
1395 {
1396 tracked_ids.push(signature_id);
1397 }
1398 let positions =
1399 c14n::canonicalize_with_visibility_and_positions_bounded_with_xml_base_budget(
1400 nodes.document(),
1401 Some(nodes),
1402 algo,
1403 &tracked_ids,
1404 context.budget.c14n.remaining(),
1405 context.budget.xml_base_resolution(),
1406 &mut output,
1407 )
1408 .map_err(|error| map_c14n_limit_error(error, &context.budget.c14n))?;
1409 let mut canonical_positions = Vec::with_capacity(tracking.active_nodes.len());
1410 for tracked in &tracking.active_nodes {
1411 if let Some((_, position)) = positions
1412 .iter()
1413 .find(|(tracked_id, _)| *tracked_id == tracked.node_id)
1414 {
1415 canonical_positions.push(CanonicalDependencyPosition {
1416 index: tracked.index,
1417 position: *position,
1418 node_type: tracked.node_type,
1419 });
1420 } else {
1421 tracking.opaque_dependencies.insert(tracked.index);
1422 }
1423 }
1424 tracking.canonical_positions = Some(canonical_positions);
1425 tracked_element.and_then(|signature_id| {
1426 positions
1427 .iter()
1428 .find(|(tracked_id, _)| *tracked_id == signature_id)
1429 .map(|(_, position)| *position)
1430 })
1431 } else {
1432 c14n::canonicalize_with_visibility_and_position_bounded_with_xml_base_budget(
1433 nodes.document(),
1434 Some(nodes),
1435 algo,
1436 tracked_element,
1437 context.budget.c14n.remaining(),
1438 context.budget.xml_base_resolution(),
1439 &mut output,
1440 )
1441 .map_err(|error| map_c14n_limit_error(error, &context.budget.c14n))?
1442 };
1443 context.budget.c14n.charge(output.len())?;
1444 return execute_transform_chain(
1445 source_signature,
1446 enveloped_signature,
1447 TransformData::Binary(output),
1448 remaining,
1449 Some(position),
1450 dependency_tracking,
1451 context,
1452 );
1453 }
1454
1455 if matches!(transform, Transform::Enveloped) {
1456 let Some(signature) = enveloped_signature else {
1457 return execute_transform_chain(
1458 source_signature,
1459 None,
1460 data,
1461 remaining,
1462 None,
1463 dependency_tracking,
1464 context,
1465 );
1466 };
1467 let data = apply_transform_with_options_and_state(
1468 signature,
1469 transform,
1470 data,
1471 context.options,
1472 context.budget,
1473 context.state,
1474 context.xml_parse,
1475 )?;
1476 return execute_transform_chain(
1477 source_signature,
1478 Some(signature),
1479 data,
1480 remaining,
1481 None,
1482 dependency_tracking,
1483 context,
1484 );
1485 }
1486
1487 let data = apply_transform_with_options_and_state(
1488 source_signature,
1489 transform,
1490 data,
1491 context.options,
1492 context.budget,
1493 context.state,
1494 context.xml_parse,
1495 )?;
1496 if let Some(tracking) = &mut dependency_tracking {
1497 match &data {
1498 TransformData::NodeSet(nodes) => {
1499 let preserve_excluded_as_opaque = match transform {
1500 Transform::XPath(expression) => {
1501 xpath_may_read_mutable_character_data(expression.expression())
1502 }
1503 Transform::XPathFilter2(filters) => filters.iter().any(|filter| {
1504 xpath_may_read_mutable_character_data(filter.xpath().expression())
1505 }),
1506 _ => false,
1507 };
1508 if preserve_excluded_as_opaque {
1509 tracking
1510 .opaque_dependencies
1511 .extend(tracking.dormant_indexes.drain());
1512 }
1513 let mut active_nodes = Vec::with_capacity(tracking.active_nodes.len());
1514 for tracked in tracking.active_nodes.drain(..) {
1515 let remains_visible = nodes
1516 .document()
1517 .get_node(tracked.node_id)
1518 .is_some_and(|node| nodes.contains(node));
1519 if remains_visible {
1520 active_nodes.push(tracked);
1521 } else if preserve_excluded_as_opaque {
1522 tracking.opaque_dependencies.insert(tracked.index);
1528 } else {
1529 tracking.dormant_indexes.insert(tracked.index);
1530 }
1531 }
1532 tracking.active_nodes = active_nodes;
1533 }
1534 TransformData::Binary(_) => {
1535 tracking
1536 .opaque_dependencies
1537 .extend(tracking.active_nodes.drain(..).map(|tracked| tracked.index));
1538 tracking.dormant_indexes.clear();
1539 tracking.canonical_positions = None;
1540 }
1541 }
1542 }
1543 execute_transform_chain(
1544 source_signature,
1545 enveloped_signature,
1546 data,
1547 remaining,
1548 None,
1549 dependency_tracking,
1550 context,
1551 )
1552}
1553
1554fn map_transform_xml_parse_error(
1555 error: XmlDocumentError,
1556 settings: DocumentParseSettings,
1557) -> TransformError {
1558 match error.into_policy_violation(settings) {
1559 Ok(error) => TransformError::Policy(error),
1560 Err(error) => TransformError::XmlParse(error.to_string()),
1561 }
1562}
1563
1564fn map_transform_xml_decode_error(error: crate::encoding::XmlEncodingError) -> TransformError {
1565 match error {
1566 crate::encoding::XmlEncodingError::DecodedLimit { maximum, actual } => {
1567 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
1568 resource: crate::policy::resource_name::XML_DOCUMENT,
1569 maximum,
1570 actual,
1571 })
1572 }
1573 error => TransformError::XmlParse(error.to_string()),
1574 }
1575}
1576
1577fn dependency_indexes(tracking: Option<DependencyTracking>) -> HashSet<usize> {
1578 let Some(tracking) = tracking else {
1579 return HashSet::new();
1580 };
1581 tracking
1582 .active_nodes
1583 .into_iter()
1584 .map(|tracked| tracked.index)
1585 .chain(tracking.opaque_dependencies)
1586 .collect()
1587}
1588
1589fn transform_requires_node_set(transform: &Transform) -> bool {
1590 !matches!(
1591 transform,
1592 Transform::Base64Decode | Transform::Relationship(_)
1593 )
1594}
1595
1596fn finalize_transform_data(
1597 data: TransformData<'_>,
1598 budget: &TransformExecutionBudget,
1599) -> Result<Vec<u8>, TransformError> {
1600 match data {
1603 TransformData::Binary(bytes) => Ok(bytes),
1604 TransformData::NodeSet(nodes) => {
1605 #[expect(clippy::expect_used, reason = "hardcoded URI is a known constant")]
1606 let algo = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI)
1607 .expect("default C14N algorithm URI must be supported by C14nAlgorithm::from_uri");
1608 let mut output = Vec::new();
1609 c14n::canonicalize_with_visibility_and_position_bounded_with_xml_base_budget(
1610 nodes.document(),
1611 Some(&nodes),
1612 &algo,
1613 None,
1614 budget.c14n.remaining(),
1615 budget.xml_base_resolution(),
1616 &mut output,
1617 )
1618 .map_err(|error| map_c14n_limit_error(error, &budget.c14n))?;
1619 budget.c14n.charge(output.len())?;
1620 Ok(output)
1621 }
1622 }
1623}
1624
1625fn map_c14n_limit_error(error: c14n::C14nError, budget: &C14nOutputBudget) -> TransformError {
1626 if c14n::is_output_limit_error(&error) {
1627 budget.exhaust();
1630 }
1631 if let Some(violation) = map_c14n_resource_policy_violation(
1632 &error,
1633 crate::policy::resource_name::CANONICALIZED_BYTES,
1634 budget.max_bytes,
1635 ) {
1636 TransformError::Policy(violation)
1637 } else {
1638 TransformError::C14n(error)
1639 }
1640}
1641
1642pub(crate) fn transform_chain_produces_binary(
1643 initial_binary: bool,
1644 transforms: &[Transform],
1645) -> bool {
1646 transforms.last().map_or(initial_binary, |transform| {
1647 matches!(
1648 transform,
1649 Transform::C14n(_) | Transform::Base64Decode | Transform::Relationship(_)
1650 )
1651 })
1652}
1653
1654pub(crate) fn map_c14n_resource_policy_violation(
1659 error: &c14n::C14nError,
1660 output_resource: &'static str,
1661 output_maximum: usize,
1662) -> Option<crate::policy::PolicyViolation> {
1663 match error {
1664 error if c14n::is_output_limit_error(error) => {
1665 Some(crate::policy::PolicyViolation::ResourceLimit {
1666 resource: output_resource,
1667 maximum: output_maximum,
1668 actual: output_maximum.saturating_add(1),
1671 })
1672 }
1673 c14n::C14nError::XmlBaseComponentsTooLarge { max, actual } => {
1674 Some(crate::policy::PolicyViolation::ResourceLimit {
1675 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
1676 maximum: *max,
1677 actual: *actual,
1678 })
1679 }
1680 c14n::C14nError::XmlBaseResolutionTooLarge { max_bytes, actual } => {
1681 Some(crate::policy::PolicyViolation::ResourceLimit {
1682 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
1683 maximum: *max_bytes,
1684 actual: *actual,
1685 })
1686 }
1687 _ => None,
1688 }
1689}
1690
1691pub(crate) fn validate_signing_transform_policy(
1692 initial_binary: bool,
1693 transforms: &[Transform],
1694 allowed: Option<&HashSet<String>>,
1695) -> Result<(), crate::policy::PolicyViolation> {
1696 validate_transform_policy(initial_binary, transforms, allowed, "signing transform")
1697}
1698
1699fn validate_transform_policy(
1700 initial_binary: bool,
1701 transforms: &[Transform],
1702 allowed: Option<&HashSet<String>>,
1703 operation: &'static str,
1704) -> Result<(), crate::policy::PolicyViolation> {
1705 let Some(allowed) = allowed else {
1706 return Ok(());
1707 };
1708 for transform in transforms {
1709 let algorithm = transform.algorithm_uri();
1710 if !allowed.contains(algorithm) {
1711 return Err(crate::policy::PolicyViolation::Algorithm {
1712 operation,
1713 algorithm: algorithm.to_owned(),
1714 });
1715 }
1716 }
1717 if !transform_chain_produces_binary(initial_binary, transforms)
1718 && !allowed.contains(DEFAULT_IMPLICIT_C14N_URI)
1719 {
1720 return Err(crate::policy::PolicyViolation::Algorithm {
1721 operation,
1722 algorithm: DEFAULT_IMPLICIT_C14N_URI.to_owned(),
1723 });
1724 }
1725 Ok(())
1726}
1727
1728pub fn parse_transforms(transforms_node: Node) -> Result<Vec<Transform>, TransformError> {
1737 parse_transforms_with_budget(transforms_node, &mut XPathSignatureParseBudget::default())
1738}
1739
1740pub(crate) fn parse_transforms_with_budget(
1741 transforms_node: Node,
1742 signature_budget: &mut XPathSignatureParseBudget,
1743) -> Result<Vec<Transform>, TransformError> {
1744 parse_reference_transforms_with_budget(transforms_node, XMLDSIG_NS, signature_budget)
1745}
1746
1747pub(crate) fn parse_reference_transforms_with_budget(
1748 transforms_node: Node,
1749 container_namespace: &str,
1750 signature_budget: &mut XPathSignatureParseBudget,
1751) -> Result<Vec<Transform>, TransformError> {
1752 if !transforms_node.is_element() {
1754 return Err(TransformError::UnsupportedTransform(
1755 "expected <Transforms> element but got non-element node".into(),
1756 ));
1757 }
1758 let transforms_tag = transforms_node.tag_name();
1759 if transforms_tag.name() != "Transforms"
1760 || transforms_tag.namespace() != Some(container_namespace)
1761 {
1762 return Err(TransformError::UnsupportedTransform(
1763 "Transforms container has the wrong namespace".into(),
1764 ));
1765 }
1766
1767 let mut chain = Vec::new();
1768 let mut xpath_state = XPathParseState::new(signature_budget);
1769
1770 for child in transforms_node.children() {
1771 if !child.is_element() {
1772 if child.is_text() && !child.text().is_some_and(is_xml_whitespace_only) {
1777 return Err(TransformError::UnsupportedTransform(
1778 "Transforms must not contain character data".into(),
1779 ));
1780 }
1781 continue;
1782 }
1783 ensure_transform_count(chain.len() + 1)?;
1784
1785 let tag = child.tag_name();
1787 if tag.name() != "Transform" || tag.namespace() != Some(XMLDSIG_NS) {
1788 return Err(TransformError::UnsupportedTransform(
1789 "unexpected child element of <ds:Transforms>; only <ds:Transform> is allowed"
1790 .into(),
1791 ));
1792 }
1793 let uri = child.attribute("Algorithm").ok_or_else(|| {
1794 TransformError::UnsupportedTransform(
1795 "missing Algorithm attribute on <Transform>".into(),
1796 )
1797 })?;
1798
1799 let transform = if uri == ENVELOPED_SIGNATURE_URI {
1800 Transform::Enveloped
1801 } else if uri == BASE64_TRANSFORM_URI {
1802 validate_empty_transform(child, "Base64")?;
1803 Transform::Base64Decode
1804 } else if uri == XPATH_TRANSFORM_URI {
1805 parse_xpath_transform_with_state(child, &mut xpath_state)?
1806 } else if uri == XPATH_FILTER2_TRANSFORM_URI {
1807 parse_xpath_filter2_transform(child, &mut xpath_state)?
1808 } else if uri == RELATIONSHIP_TRANSFORM_URI {
1809 Transform::Relationship(relationship::parse_selectors(
1810 child,
1811 &xpath_state.signature_budget.opc_workspace,
1812 )?)
1813 } else if let Some(mut algo) = C14nAlgorithm::from_uri(uri) {
1814 if algo.mode() == c14n::C14nMode::Exclusive1_0
1816 && let Some(prefix_list) = parse_inclusive_prefixes(child)?
1817 {
1818 algo = algo.with_prefix_list(&prefix_list);
1819 }
1820 Transform::C14n(algo)
1821 } else {
1822 return Err(TransformError::UnsupportedTransform(uri.to_string()));
1823 };
1824 chain.push(transform);
1825 }
1826
1827 relationship::validate_chain(&chain)?;
1828 Ok(chain)
1829}
1830
1831fn validate_empty_transform(
1833 transform_node: Node,
1834 transform_name: &'static str,
1835) -> Result<(), TransformError> {
1836 for child in transform_node.children() {
1837 if child.is_element()
1838 || (child.is_text()
1839 && child
1840 .text()
1841 .is_some_and(|text| !is_xml_whitespace_only(text)))
1842 {
1843 return Err(TransformError::UnsupportedTransform(format!(
1844 "{transform_name} transform must not contain parameters"
1845 )));
1846 }
1847 }
1848 Ok(())
1849}
1850
1851#[cfg(test)]
1852pub(super) fn parse_xpath_transform(transform_node: Node) -> Result<Transform, TransformError> {
1853 parse_xpath_transform_with_state(
1854 transform_node,
1855 &mut XPathParseState::new(&mut XPathSignatureParseBudget::default()),
1856 )
1857}
1858
1859fn parse_xpath_transform_with_state(
1860 transform_node: Node,
1861 xpath_state: &mut XPathParseState,
1862) -> Result<Transform, TransformError> {
1863 let mut xpath_node = None;
1864
1865 for child in transform_node.children() {
1866 if child.is_text() && child.text().is_some_and(is_xml_whitespace_only) {
1867 continue;
1868 }
1869 if child.is_comment() || child.is_pi() {
1870 continue;
1871 }
1872 if !child.is_element() {
1873 return Err(TransformError::XPath(
1874 "XPath transform contains non-whitespace parameter content".into(),
1875 ));
1876 }
1877 let tag = child.tag_name();
1878 if tag.name() == "XPath" && tag.namespace() == Some(XMLDSIG_NS) {
1879 if xpath_node.is_some() {
1880 return Err(TransformError::XPath(
1881 "XPath transform must contain exactly one XMLDSig <XPath> child element".into(),
1882 ));
1883 }
1884 xpath_node = Some(child);
1885 } else {
1886 return Err(TransformError::XPath(
1887 "XPath transform allows only a single XMLDSig <XPath> child element".into(),
1888 ));
1889 }
1890 }
1891
1892 let xpath_node = xpath_node.ok_or_else(|| {
1893 TransformError::XPath(
1894 "XPath transform requires a single XMLDSig <XPath> child element".into(),
1895 )
1896 })?;
1897 if xpath_node.attributes().len() != 0 {
1898 return Err(TransformError::XPath(
1899 "XMLDSig <XPath> does not allow attributes".into(),
1900 ));
1901 }
1902 let xpath = parse_xpath_expression(xpath_node, transform_node.id(), xpath_state)?;
1903
1904 if xpath.expression() == ENVELOPED_SIGNATURE_XPATH_EXPR
1905 && xpath.namespaces().get("dsig").map(String::as_str) == Some(XMLDSIG_NS)
1906 {
1907 Ok(Transform::XpathExcludeAllSignatures)
1908 } else {
1909 Ok(Transform::XPath(xpath))
1910 }
1911}
1912
1913fn parse_xpath_filter2_transform(
1914 transform_node: Node,
1915 xpath_state: &mut XPathParseState,
1916) -> Result<Transform, TransformError> {
1917 let mut filters = Vec::new();
1918 for child in transform_node.children() {
1919 if child.is_text() && child.text().is_some_and(is_xml_whitespace_only) {
1920 continue;
1921 }
1922 if child.is_comment() || child.is_pi() {
1923 continue;
1924 }
1925 if !child.is_element()
1926 || child.tag_name().name() != "XPath"
1927 || child.tag_name().namespace() != Some(XPATH_FILTER2_TRANSFORM_URI)
1928 {
1929 return Err(TransformError::XPath(
1930 "XPath Filter 2.0 allows only filter-namespace <XPath> children".into(),
1931 ));
1932 }
1933 if filters.len() == xpath_state.signature_budget.max_filters {
1934 return Err(transform_resource_limit(
1935 crate::policy::resource_name::XPATH_FILTERS,
1936 xpath_state.signature_budget.max_filters,
1937 filters.len().saturating_add(1),
1938 ));
1939 }
1940 if child.attributes().len() != 1 || child.attribute("Filter").is_none() {
1941 return Err(TransformError::XPath(
1942 "XPath Filter 2.0 <XPath> requires only the unqualified Filter attribute".into(),
1943 ));
1944 }
1945 let operation = match child.attribute("Filter") {
1946 Some("intersect") => XPathFilterOperation::Intersect,
1947 Some("subtract") => XPathFilterOperation::Subtract,
1948 Some("union") => XPathFilterOperation::Union,
1949 Some(value) => {
1950 return Err(TransformError::XPath(format!(
1951 "unsupported XPath Filter 2.0 operation: {value}"
1952 )));
1953 }
1954 None => unreachable!("Filter presence was checked above"),
1955 };
1956 filters.push(XPathFilter::new(
1957 operation,
1958 parse_xpath_expression(child, transform_node.id(), xpath_state)?,
1959 ));
1960 }
1961 if filters.is_empty() {
1962 return Err(TransformError::XPath(
1963 "XPath Filter 2.0 requires at least one expression".into(),
1964 ));
1965 }
1966 Ok(Transform::XPathFilter2(filters))
1967}
1968
1969fn parse_xpath_expression(
1970 xpath_node: Node,
1971 transform_node: NodeId,
1972 xpath_state: &mut XPathParseState,
1973) -> Result<XPathExpression, TransformError> {
1974 let mut source = String::new();
1975 for child in xpath_node.children() {
1976 if child.is_text() {
1977 let text = child.text().unwrap_or_default();
1978 let attempted = source.len().saturating_add(text.len());
1979 if attempted > xpath_state.signature_budget.max_expression_bytes {
1980 return Err(transform_resource_limit(
1981 crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
1982 xpath_state.signature_budget.max_expression_bytes,
1983 attempted,
1984 ));
1985 }
1986 source.push_str(text);
1987 } else if child.is_element() {
1988 return Err(TransformError::XPath(
1989 "XPath expressions must contain text only".into(),
1990 ));
1991 }
1992 }
1993 let source = source.trim_matches(is_xpath_whitespace);
1994 if source.is_empty() {
1995 return Err(TransformError::XPath(
1996 "XPath expression must not be empty".into(),
1997 ));
1998 }
1999 xpath_state.signature_budget.charge()?;
2000 if source.len() > xpath_state.signature_budget.max_expression_bytes {
2001 return Err(transform_resource_limit(
2002 crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
2003 xpath_state.signature_budget.max_expression_bytes,
2004 source.len(),
2005 ));
2006 }
2007 let complexity = crate::xmldsig::xpath::xpath_expression_complexity(source);
2008 if complexity > xpath_state.signature_budget.max_expression_complexity {
2009 return Err(transform_resource_limit(
2010 crate::policy::resource_name::XPATH_EXPRESSION_COMPLEXITY,
2011 xpath_state.signature_budget.max_expression_complexity,
2012 complexity,
2013 ));
2014 }
2015 crate::xmldsig::xpath::compile_xpath_with_policy_limits(
2016 source,
2017 xpath_state.signature_budget.max_expression_bytes,
2018 xpath_state.signature_budget.max_expression_complexity,
2019 )
2020 .map_err(TransformError::XPath)?;
2021
2022 let namespaces = collect_xpath_namespaces_with_limits(
2023 xpath_node,
2024 xpath_state.signature_budget.max_namespace_bindings,
2025 xpath_state.signature_budget.max_namespace_bytes,
2026 )?;
2027 let xpath = XPathExpression {
2028 expression: source.to_owned(),
2029 namespaces,
2030 here_nodes: Some(XPathHereNodes {
2031 specification_xpath_element: xpath_node.id(),
2034 xmlsec_legacy_transform_element: transform_node,
2035 document: xpath_state.document_identity(xpath_node.document()),
2039 }),
2040 };
2041 Ok(xpath)
2042}
2043
2044struct XPathParseState<'a> {
2045 document_identity: Option<XPathDocumentIdentity>,
2046 signature_budget: &'a mut XPathSignatureParseBudget,
2047}
2048
2049impl<'a> XPathParseState<'a> {
2050 fn new(signature_budget: &'a mut XPathSignatureParseBudget) -> Self {
2051 Self {
2052 document_identity: None,
2053 signature_budget,
2054 }
2055 }
2056
2057 fn document_identity(&mut self, document: &Document<'_>) -> XPathDocumentIdentity {
2058 *self
2059 .document_identity
2060 .get_or_insert_with(|| XPathDocumentIdentity::from_document(document))
2061 }
2062}
2063
2064pub(crate) struct XPathSignatureParseBudget {
2067 opc_workspace: relationship::WorkspaceBudget,
2068 expressions: usize,
2069 max_expressions: usize,
2070 max_expression_bytes: usize,
2071 max_expression_complexity: usize,
2072 max_namespace_bindings: usize,
2073 max_namespace_bytes: usize,
2074 max_filters: usize,
2075}
2076
2077impl Default for XPathSignatureParseBudget {
2078 fn default() -> Self {
2079 Self {
2080 opc_workspace: relationship::WorkspaceBudget::parameters(
2081 crate::hard_limits::OPC_PARAMETER_BYTE_CEILING,
2082 ),
2083 expressions: 0,
2084 max_expressions: MAX_XPATH_EXPRESSIONS_PER_SIGNATURE,
2085 max_expression_bytes: MAX_XPATH_EXPRESSION_BYTES,
2086 max_expression_complexity: crate::hard_limits::XPATH_EXPRESSION_COMPLEXITY_CEILING,
2087 max_namespace_bindings: MAX_XPATH_NAMESPACE_BINDINGS,
2088 max_namespace_bytes: MAX_XPATH_NAMESPACE_BYTES,
2089 max_filters: MAX_XPATH_FILTERS,
2090 }
2091 }
2092}
2093
2094impl XPathSignatureParseBudget {
2095 pub(crate) fn validate_relationship_selectors(
2096 &self,
2097 selectors: &[RelationshipSelector],
2098 ) -> Result<(), TransformError> {
2099 relationship::validate_owned_selectors(selectors, &self.opc_workspace)
2100 }
2101
2102 pub(crate) fn from_resources(resources: &crate::policy::ResourcePolicy) -> Self {
2103 Self {
2104 opc_workspace: relationship::WorkspaceBudget::parameters(
2105 resources.max_opc_parameter_bytes,
2106 ),
2107 expressions: 0,
2108 max_expressions: resources.max_xpath_expressions,
2109 max_expression_bytes: resources.max_xpath_expression_bytes,
2110 max_expression_complexity: resources.max_xpath_expression_complexity,
2111 max_namespace_bindings: resources.max_xpath_namespace_bindings,
2112 max_namespace_bytes: resources.max_xpath_namespace_bytes,
2113 max_filters: resources.max_xpath_filters,
2114 }
2115 }
2116
2117 pub(crate) fn charge(&mut self) -> Result<(), TransformError> {
2118 self.expressions = self
2119 .expressions
2120 .checked_add(1)
2121 .ok_or_else(|| self.error())?;
2122 if self.expressions > self.max_expressions {
2123 return Err(self.error());
2124 }
2125 Ok(())
2126 }
2127
2128 pub(crate) fn validate_expression(&mut self, source: &str) -> Result<(), TransformError> {
2129 if source.is_empty() {
2130 return Err(TransformError::XPath(
2131 "XPath expression must not be empty".into(),
2132 ));
2133 }
2134 self.charge()?;
2135 if source.len() > self.max_expression_bytes {
2136 return Err(transform_resource_limit(
2137 crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
2138 self.max_expression_bytes,
2139 source.len(),
2140 ));
2141 }
2142 let complexity = crate::xmldsig::xpath::xpath_expression_complexity(source);
2143 if complexity > self.max_expression_complexity {
2144 return Err(transform_resource_limit(
2145 crate::policy::resource_name::XPATH_EXPRESSION_COMPLEXITY,
2146 self.max_expression_complexity,
2147 complexity,
2148 ));
2149 }
2150 crate::xmldsig::xpath::compile_xpath_with_policy_limits(
2151 source,
2152 self.max_expression_bytes,
2153 self.max_expression_complexity,
2154 )
2155 .map(|_| ())
2156 .map_err(TransformError::XPath)
2157 }
2158
2159 pub(crate) fn validate_namespaces(
2160 &self,
2161 namespaces: &BTreeMap<String, String>,
2162 ) -> Result<(), TransformError> {
2163 let mut budget = XPathNamespaceBudget::with_limits(
2164 self.max_namespace_bindings,
2165 self.max_namespace_bytes,
2166 );
2167 for (prefix, uri) in namespaces {
2168 budget.charge(prefix, uri)?;
2169 }
2170 Ok(())
2171 }
2172
2173 fn error(&self) -> TransformError {
2174 transform_resource_limit(
2175 crate::policy::resource_name::XPATH_EXPRESSIONS,
2176 self.max_expressions,
2177 self.expressions.max(self.max_expressions.saturating_add(1)),
2178 )
2179 }
2180}
2181
2182struct XPathNamespaceBudget {
2183 bindings: usize,
2184 bytes: usize,
2185 max_bindings: usize,
2186 max_bytes: usize,
2187}
2188
2189impl Default for XPathNamespaceBudget {
2190 fn default() -> Self {
2191 Self::with_limits(MAX_XPATH_NAMESPACE_BINDINGS, MAX_XPATH_NAMESPACE_BYTES)
2192 }
2193}
2194
2195impl XPathNamespaceBudget {
2196 fn with_limits(max_bindings: usize, max_bytes: usize) -> Self {
2197 Self {
2198 bindings: 0,
2199 bytes: 0,
2200 max_bindings,
2201 max_bytes,
2202 }
2203 }
2204
2205 fn charge(&mut self, prefix: &str, uri: &str) -> Result<(), TransformError> {
2206 let bindings = self.bindings.saturating_add(1);
2207 let bytes = self
2208 .bytes
2209 .checked_add(prefix.len())
2210 .and_then(|bytes| bytes.checked_add(uri.len()))
2211 .unwrap_or(usize::MAX);
2212 if bindings > self.max_bindings {
2213 return Err(transform_resource_limit(
2214 crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
2215 self.max_bindings,
2216 bindings,
2217 ));
2218 }
2219 if bytes > self.max_bytes {
2220 return Err(transform_resource_limit(
2221 crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
2222 self.max_bytes,
2223 bytes,
2224 ));
2225 }
2226 self.bindings = bindings;
2227 self.bytes = bytes;
2228 Ok(())
2229 }
2230}
2231
2232pub(crate) fn collect_xpath_namespaces_with_resources(
2233 xpath_node: Node<'_, '_>,
2234 resources: &crate::policy::ResourcePolicy,
2235) -> Result<BTreeMap<String, String>, TransformError> {
2236 collect_xpath_namespaces_with_limits(
2237 xpath_node,
2238 resources.max_xpath_namespace_bindings,
2239 resources.max_xpath_namespace_bytes,
2240 )
2241}
2242
2243fn collect_xpath_namespaces_with_limits(
2244 xpath_node: Node<'_, '_>,
2245 max_bindings: usize,
2246 max_bytes: usize,
2247) -> Result<BTreeMap<String, String>, TransformError> {
2248 let mut budget = XPathNamespaceBudget::with_limits(max_bindings, max_bytes);
2249 for namespace in xpath_node.namespaces() {
2250 if let Some(prefix) = namespace.name() {
2251 budget.charge(prefix, namespace.uri())?;
2252 }
2253 }
2254 Ok(xpath_node
2255 .namespaces()
2256 .filter_map(|namespace| {
2257 namespace
2258 .name()
2259 .map(|prefix| (prefix.to_owned(), namespace.uri().to_owned()))
2260 })
2261 .collect())
2262}
2263
2264pub(crate) fn validate_xpath_namespace_budget_with_resources(
2265 transforms: &[Transform],
2266 inherited_namespace: Option<(&str, &str)>,
2267 resources: &crate::policy::ResourcePolicy,
2268) -> Result<(), TransformError> {
2269 validate_xpath_namespace_budget_with_limits(
2270 transforms,
2271 inherited_namespace,
2272 resources.max_xpath_namespace_bindings,
2273 resources.max_xpath_namespace_bytes,
2274 )
2275}
2276
2277fn validate_xpath_namespace_budget_with_limits(
2278 transforms: &[Transform],
2279 inherited_namespace: Option<(&str, &str)>,
2280 max_bindings: usize,
2281 max_bytes: usize,
2282) -> Result<(), TransformError> {
2283 let validate_expression = |xpath: &XPathExpression| {
2284 let mut budget = XPathNamespaceBudget::with_limits(max_bindings, max_bytes);
2285 for (prefix, uri) in xpath.namespaces() {
2286 budget.charge(prefix, uri)?;
2287 }
2288 if let Some((prefix, uri)) = inherited_namespace
2289 && !xpath.namespaces().contains_key(prefix)
2290 {
2291 budget.charge(prefix, uri)?;
2292 }
2293 Ok::<(), TransformError>(())
2294 };
2295 for transform in transforms {
2296 match transform {
2297 Transform::XpathExcludeAllSignatures => {
2298 let xpath = XPathExpression::new(ENVELOPED_SIGNATURE_XPATH_EXPR)
2299 .with_namespace(ENVELOPED_SIGNATURE_XPATH_PREFIX, XMLDSIG_NS);
2300 validate_expression(&xpath)?;
2301 }
2302 Transform::XPath(xpath) => validate_expression(xpath)?,
2303 Transform::XPathFilter2(filters) => {
2304 for filter in filters {
2305 validate_expression(filter.xpath())?;
2306 }
2307 }
2308 _ => {}
2309 }
2310 }
2311 Ok(())
2312}
2313
2314fn parse_inclusive_prefixes(transform_node: Node) -> Result<Option<String>, TransformError> {
2332 for child in transform_node.children() {
2333 if child.is_element() {
2334 let tag = child.tag_name();
2335 if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
2336 {
2337 let prefix_list = child.attribute("PrefixList").ok_or_else(|| {
2338 TransformError::UnsupportedTransform(
2339 "missing PrefixList attribute on <InclusiveNamespaces>".into(),
2340 )
2341 })?;
2342 return Ok(Some(prefix_list.to_string()));
2343 }
2344 }
2345 }
2346 Ok(None)
2347}
2348
2349#[cfg(test)]
2350#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
2351mod tests {
2352 use super::*;
2353 use crate::xml::dom::Document;
2354 use crate::xmldsig::NodeSet;
2355
2356 fn assert_resource_limit(error: &TransformError, expected_resource: &'static str) {
2357 assert!(
2358 matches!(
2359 error,
2360 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2361 resource,
2362 maximum,
2363 actual,
2364 }) if *resource == expected_resource && actual > maximum
2365 ),
2366 "unexpected error: {error:?}"
2367 );
2368 }
2369
2370 #[test]
2373 fn enveloped_excludes_signature_subtree() {
2374 let xml = r#"<root>
2376 <data>hello</data>
2377 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
2378 <SignedInfo><Reference URI=""/></SignedInfo>
2379 <SignatureValue>abc</SignatureValue>
2380 </Signature>
2381 </root>"#;
2382 let doc = Document::parse(xml).unwrap();
2383
2384 let sig_node = doc
2386 .descendants()
2387 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2388 .unwrap();
2389
2390 let node_set = NodeSet::entire_document_without_comments(&doc).unwrap();
2392 let data = TransformData::NodeSet(node_set);
2393
2394 let result = apply_transform(sig_node, &Transform::Enveloped, data).unwrap();
2396 let node_set = result.into_node_set().unwrap();
2397
2398 assert!(node_set.contains(doc.root_element()));
2400 let data_elem = doc
2401 .descendants()
2402 .find(|n| n.is_element() && n.tag_name().name() == "data")
2403 .unwrap();
2404 assert!(node_set.contains(data_elem));
2405
2406 assert!(
2408 !node_set.contains(sig_node),
2409 "Signature element should be excluded"
2410 );
2411 let signed_info = doc
2412 .descendants()
2413 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
2414 .unwrap();
2415 assert!(
2416 !node_set.contains(signed_info),
2417 "SignedInfo (child of Signature) should be excluded"
2418 );
2419 }
2420
2421 #[test]
2422 fn enveloped_requires_node_set_input() {
2423 let xml = "<root/>";
2424 let doc = Document::parse(xml).unwrap();
2425 let data = TransformData::Binary(vec![1, 2, 3]);
2427 let result = apply_transform(doc.root_element(), &Transform::Enveloped, data);
2428 assert!(result.is_err());
2429 match result.unwrap_err() {
2430 TransformError::TypeMismatch { expected, got } => {
2431 assert_eq!(expected, "NodeSet");
2432 assert_eq!(got, "Binary");
2433 }
2434 other => panic!("expected TypeMismatch, got: {other:?}"),
2435 }
2436 }
2437
2438 #[test]
2439 fn enveloped_rejects_cross_document_signature_node() {
2440 let xml = r#"<Root><Signature Id="sig"/></Root>"#;
2443 let doc1 = Document::parse(xml).unwrap();
2444 let doc2 = Document::parse(xml).unwrap();
2445
2446 let node_set = NodeSet::entire_document_without_comments(&doc1).unwrap();
2448 let input = TransformData::NodeSet(node_set);
2449 let sig_from_doc2 = doc2
2450 .descendants()
2451 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
2452 .unwrap();
2453
2454 let result = apply_transform(sig_from_doc2, &Transform::Enveloped, input);
2455 assert!(matches!(
2456 result,
2457 Err(TransformError::CrossDocumentSignatureNode)
2458 ));
2459 }
2460
2461 #[test]
2464 fn c14n_transform_produces_bytes() {
2465 let xml = r#"<root b="2" a="1"><child/></root>"#;
2466 let doc = Document::parse(xml).unwrap();
2467
2468 let node_set = NodeSet::entire_document_without_comments(&doc).unwrap();
2469 let data = TransformData::NodeSet(node_set);
2470
2471 let algo =
2472 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
2473 let result = apply_transform(doc.root_element(), &Transform::C14n(algo), data).unwrap();
2474
2475 let bytes = result.into_binary().unwrap();
2476 let output = String::from_utf8(bytes).unwrap();
2477 assert_eq!(output, r#"<root a="1" b="2"><child></child></root>"#);
2479 }
2480
2481 #[test]
2482 fn c14n_transform_requires_node_set() {
2483 let xml = "<root/>";
2484 let doc = Document::parse(xml).unwrap();
2485
2486 let algo =
2487 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
2488 let data = TransformData::Binary(vec![1, 2, 3]);
2489 let result = apply_transform(doc.root_element(), &Transform::C14n(algo), data);
2490
2491 assert!(result.is_err());
2492 assert!(matches!(
2493 result.unwrap_err(),
2494 TransformError::TypeMismatch { .. }
2495 ));
2496 }
2497
2498 #[test]
2499 fn c14n_1_1_uses_the_compiled_xml_base_policy() {
2500 let document = Document::parse(
2503 r#"<root xml:base="one/"><parent xml:base="two/"><leaf/></parent></root>"#,
2504 )
2505 .unwrap();
2506 let leaf = document
2507 .descendants()
2508 .find(|node| node.has_tag_name("leaf"))
2509 .unwrap();
2510 let resources = crate::policy::ResourcePolicy {
2511 max_xml_base_components: 1,
2512 ..crate::policy::ResourcePolicy::default()
2513 };
2514 let budget = TransformExecutionBudget::from_resources(&resources);
2515 let algorithm = C14nAlgorithm::new(crate::c14n::C14nMode::Inclusive1_1, false);
2516
2517 let error = execute_transforms_with_options_and_budget(
2518 document.root_element(),
2519 TransformData::NodeSet(NodeSet::subtree(leaf).unwrap()),
2520 &[Transform::C14n(algorithm)],
2521 TransformOptions::default(),
2522 &budget,
2523 )
2524 .expect_err("C14N must use the operation's XML Base component limit");
2525
2526 assert!(matches!(
2527 error,
2528 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2529 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
2530 maximum: 1,
2531 actual: 2
2532 })
2533 ));
2534 }
2535
2536 #[test]
2539 fn base64_transform_decodes_binary_with_xml_whitespace() {
2540 let doc = Document::parse("<root/>").unwrap();
2542 let input = TransformData::Binary(b" SGV\tsbG8=\r\n".to_vec());
2543
2544 let result = apply_transform(doc.root_element(), &Transform::Base64Decode, input).unwrap();
2545
2546 assert_eq!(result.into_binary().unwrap(), b"Hello");
2547 }
2548
2549 #[test]
2550 fn base64_transform_concatenates_only_selected_text_nodes_in_document_order() {
2551 let xml = r#"<root><Data ID="payload">SGV<!-- split --><Part>sb</Part><?pi ignored?>G8=</Data></root>"#;
2554 let doc = Document::parse(xml).unwrap();
2555 let data = doc
2556 .descendants()
2557 .find(|node| node.attribute("ID") == Some("payload"))
2558 .unwrap();
2559 let input = TransformData::NodeSet(NodeSet::subtree(data).unwrap());
2560
2561 let result = apply_transform(data, &Transform::Base64Decode, input).unwrap();
2562
2563 assert_eq!(result.into_binary().unwrap(), b"Hello");
2564 }
2565
2566 #[test]
2567 fn base64_transform_omits_text_excluded_from_the_node_set() {
2568 let xml = "<root>SGV<Excluded>QUJD</Excluded>sbG8=</root>";
2571 let doc = Document::parse(xml).unwrap();
2572 let excluded = doc
2573 .descendants()
2574 .find(|node| node.has_tag_name("Excluded"))
2575 .unwrap();
2576 let mut nodes = NodeSet::subtree(doc.root_element()).unwrap();
2577 nodes.exclude_subtree(excluded);
2578
2579 let result = apply_transform(
2580 doc.root_element(),
2581 &Transform::Base64Decode,
2582 TransformData::NodeSet(nodes),
2583 )
2584 .unwrap();
2585
2586 assert_eq!(result.into_binary().unwrap(), b"Hello");
2587 }
2588
2589 #[test]
2590 fn base64_transform_ignores_rfc2045_non_alphabet_bytes() {
2591 let doc = Document::parse("<root/>").unwrap();
2592 let input = TransformData::Binary(b"SGVs!\xFFbG8=".to_vec());
2593
2594 let result = apply_transform(doc.root_element(), &Transform::Base64Decode, input).unwrap();
2595
2596 assert_eq!(result.into_binary().unwrap(), b"Hello");
2597 }
2598
2599 #[test]
2600 fn base64_transform_rejects_invalid_padding() {
2601 let doc = Document::parse("<root/>").unwrap();
2602 let result = apply_transform(
2603 doc.root_element(),
2604 &Transform::Base64Decode,
2605 TransformData::Binary(b"SGVsbG8===".to_vec()),
2606 );
2607
2608 assert!(matches!(result, Err(TransformError::Base64(_))));
2609 }
2610
2611 #[test]
2612 fn base64_transform_accepts_empty_input() {
2613 let doc = Document::parse("<root/>").unwrap();
2614 let result = apply_transform(
2615 doc.root_element(),
2616 &Transform::Base64Decode,
2617 TransformData::Binary(Vec::new()),
2618 )
2619 .unwrap();
2620
2621 assert!(result.into_binary().unwrap().is_empty());
2622 }
2623
2624 #[test]
2625 fn base64_transform_rejects_oversized_raw_binary_before_normalization() {
2626 let doc = Document::parse("<root/>").unwrap();
2629 let input = TransformData::Binary(vec![b' '; MAX_BASE64_TRANSFORM_INPUT_BYTES + 1]);
2630
2631 let result = apply_transform(doc.root_element(), &Transform::Base64Decode, input);
2632
2633 assert!(matches!(
2634 result,
2635 Err(TransformError::Policy(
2636 crate::policy::PolicyViolation::ResourceLimit {
2637 resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
2638 maximum: MAX_BASE64_TRANSFORM_INPUT_BYTES,
2639 ..
2640 }
2641 ))
2642 ));
2643 }
2644
2645 #[test]
2646 fn base64_transform_rejects_node_set_that_decodes_past_output_budget() {
2647 let encoded_len = MAX_BASE64_TRANSFORM_OUTPUT_BYTES.div_ceil(3) * 4 + 4;
2650 let xml = format!("<root>{}</root>", "A".repeat(encoded_len));
2651 let doc = Document::parse(&xml).unwrap();
2652 let input = TransformData::NodeSet(NodeSet::subtree(doc.root_element()).unwrap());
2653
2654 let result = apply_transform(doc.root_element(), &Transform::Base64Decode, input);
2655
2656 assert!(matches!(
2657 result,
2658 Err(TransformError::Policy(
2659 crate::policy::PolicyViolation::ResourceLimit {
2660 resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
2661 maximum: MAX_BASE64_TRANSFORM_OUTPUT_BYTES,
2662 ..
2663 }
2664 ))
2665 ));
2666 }
2667
2668 #[test]
2669 fn base64_transform_handles_highly_fragmented_node_set_input() {
2670 let expected = vec![0x42_u8; 3 * 1_024];
2674 let encoded = STANDARD.encode(&expected);
2675 let mut xml = String::from("<root>");
2676 for byte in encoded.bytes() {
2677 xml.push(char::from(byte));
2678 xml.push_str("<!-- split -->");
2679 }
2680 xml.push_str("</root>");
2681 let doc = Document::parse(&xml).unwrap();
2682 let input = TransformData::NodeSet(NodeSet::subtree(doc.root_element()).unwrap());
2683
2684 let result = apply_transform(doc.root_element(), &Transform::Base64Decode, input).unwrap();
2685
2686 assert_eq!(result.into_binary().unwrap(), expected);
2687 }
2688
2689 #[test]
2690 fn pipeline_rejects_cumulative_base64_input_past_budget() {
2691 let doc = Document::parse("<root/>").unwrap();
2694 let inner = vec![b'A'; MAX_BASE64_TRANSFORM_OUTPUT_BYTES];
2695 let outer = STANDARD.encode(&inner);
2696 let transforms = [Transform::Base64Decode, Transform::Base64Decode];
2697
2698 let result = execute_transforms(
2699 doc.root_element(),
2700 TransformData::Binary(outer.into_bytes()),
2701 &transforms,
2702 );
2703
2704 assert!(matches!(
2705 result,
2706 Err(TransformError::Policy(
2707 crate::policy::PolicyViolation::ResourceLimit {
2708 resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
2709 maximum: MAX_BASE64_TRANSFORM_INPUT_BYTES,
2710 ..
2711 }
2712 ))
2713 ));
2714 }
2715
2716 #[test]
2717 fn operation_rejects_cumulative_base64_output_past_budget() {
2718 let doc = Document::parse("<root/>").unwrap();
2721 let resources = crate::policy::ResourcePolicy {
2722 max_base64_transform_input_bytes: 8,
2723 max_base64_transform_output_bytes: 1,
2724 ..crate::policy::ResourcePolicy::default()
2725 };
2726 let budget = TransformExecutionBudget::from_resources(&resources);
2727
2728 let first = apply_transform_with_options(
2729 doc.root_element(),
2730 &Transform::Base64Decode,
2731 TransformData::Binary(b"YQ==".to_vec()),
2732 TransformOptions::default(),
2733 &budget,
2734 )
2735 .expect("the first one-byte output must fit");
2736 assert_eq!(first.into_binary().unwrap(), b"a");
2737
2738 let error = apply_transform_with_options(
2739 doc.root_element(),
2740 &Transform::Base64Decode,
2741 TransformData::Binary(b"Yg==".to_vec()),
2742 TransformOptions::default(),
2743 &budget,
2744 )
2745 .expect_err("the second output must exceed the cumulative allowance");
2746
2747 assert!(matches!(
2748 error,
2749 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2750 resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
2751 maximum: 1,
2752 actual: 2,
2753 })
2754 ));
2755 }
2756
2757 #[test]
2758 fn pipeline_rejects_unbounded_programmatic_transform_chain() {
2759 let doc = Document::parse("<root/>").unwrap();
2762 let transforms = vec![Transform::Base64Decode; 65];
2763
2764 let result = execute_transforms(
2765 doc.root_element(),
2766 TransformData::Binary(Vec::new()),
2767 &transforms,
2768 );
2769
2770 assert!(matches!(
2771 result,
2772 Err(TransformError::Policy(
2773 crate::policy::PolicyViolation::ResourceLimit {
2774 resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
2775 maximum: MAX_TRANSFORMS_PER_REFERENCE,
2776 ..
2777 }
2778 ))
2779 ));
2780 }
2781
2782 #[test]
2785 fn byte_budgets_remain_exhausted_after_overflow() {
2786 let c14n = C14nOutputBudget::default();
2789 assert!(c14n.charge(MAX_C14N_OUTPUT_BYTES + 1).is_err());
2790 assert!(c14n.charge(1).is_err());
2791
2792 let base64 = Base64WorkBudget::default();
2793 assert!(
2794 base64
2795 .charge_input(MAX_BASE64_TRANSFORM_INPUT_BYTES + 1)
2796 .is_err()
2797 );
2798 assert!(base64.charge_input(1).is_err());
2799 }
2800
2801 #[test]
2802 fn pipeline_rejects_cumulative_c14n_output() {
2803 let xml = format!("<root>{}</root>", "x".repeat(4_096));
2806 let document = Document::parse(&xml).unwrap();
2807 let algorithm =
2808 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
2809 let transforms = vec![
2810 Transform::C14n(algorithm.clone()),
2811 Transform::C14n(algorithm),
2812 ];
2813 let one_output = execute_transforms(
2814 document.root_element(),
2815 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap()),
2816 &transforms[..1],
2817 )
2818 .expect("one canonicalization must succeed")
2819 .len();
2820 let limit = one_output * 2 - 1;
2821
2822 let result = execute_transforms_with_options_and_budget(
2823 document.root_element(),
2824 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap()),
2825 &transforms,
2826 TransformOptions::default(),
2827 &TransformExecutionBudget::with_c14n_limit(limit),
2828 );
2829
2830 assert!(matches!(
2831 result,
2832 Err(TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2833 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
2834 maximum,
2835 ..
2836 })) if maximum == limit
2837 ));
2838 }
2839
2840 #[test]
2841 fn binary_to_node_set_adapter_uses_shared_materialization_budget() {
2842 let signature_document = Document::parse("<Signature/>").unwrap();
2846 let budget = TransformExecutionBudget::with_node_set_materialization_limit(1);
2847 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
2848
2849 let error = execute_transforms_with_options_and_budget(
2850 signature_document.root_element(),
2851 TransformData::Binary(b"<root xmlns:n=\"urn:namespace\"/>".to_vec()),
2852 &transforms,
2853 TransformOptions::default(),
2854 &budget,
2855 )
2856 .expect_err("the binary adapter must charge cloned namespace strings");
2857
2858 assert!(matches!(
2859 error,
2860 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2861 resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
2862 ..
2863 })
2864 ));
2865 }
2866
2867 #[test]
2868 fn binary_to_node_set_adapter_preserves_decoded_document_limit() {
2869 let signature_document = Document::parse("<Signature/>").unwrap();
2872 let xml = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><root>\xe9\xe9</root>";
2873 let resources = crate::policy::ResourcePolicy {
2874 max_xml_document_bytes: xml.len(),
2875 ..crate::policy::ResourcePolicy::default()
2876 };
2877 let budget = TransformExecutionBudget::from_resources(&resources);
2878 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
2879
2880 let error = execute_transforms_with_options_and_budget(
2881 signature_document.root_element(),
2882 TransformData::Binary(xml.to_vec()),
2883 &transforms,
2884 TransformOptions::default(),
2885 &budget,
2886 )
2887 .expect_err("decoded XML expansion must retain resource-limit classification");
2888
2889 assert!(matches!(
2890 error,
2891 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2892 resource: crate::policy::resource_name::XML_DOCUMENT,
2893 maximum,
2894 actual,
2895 }) if maximum == xml.len() && actual > maximum
2896 ));
2897 }
2898
2899 #[test]
2900 fn binary_adapter_gates_decode_even_when_input_is_malformed() {
2901 let signature_document = Document::parse("<Signature/>").unwrap();
2903 let resources = crate::policy::ResourcePolicy {
2904 max_xml_parse_work_bytes: 0,
2905 ..crate::policy::ResourcePolicy::default()
2906 };
2907 let budget = TransformExecutionBudget::from_resources(&resources);
2908 let error = execute_transforms_with_options_and_budget(
2909 signature_document.root_element(),
2910 TransformData::Binary(vec![0xff, 0xfe, 0x00, 0xd8]),
2911 &[Transform::XPath(XPathExpression::new("true()"))],
2912 TransformOptions::default(),
2913 &budget,
2914 )
2915 .expect_err("zero work must reject before malformed decoding");
2916 assert!(matches!(
2917 error,
2918 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2919 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
2920 maximum: 0,
2921 ..
2922 })
2923 ));
2924 }
2925
2926 #[test]
2927 fn recursive_binary_adapters_share_xml_parse_work() {
2928 let signature_document = Document::parse("<Signature/>").unwrap();
2932 let xml = b"<root/>";
2933 let parser_passes = crate::document::selected_parser_passes();
2934 let adapter_work = xml.len() * (1 + parser_passes);
2936 let resources = crate::policy::ResourcePolicy {
2937 max_xml_parse_work_bytes: adapter_work,
2938 ..crate::policy::ResourcePolicy::default()
2939 };
2940 let budget = TransformExecutionBudget::from_resources(&resources);
2941 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
2942
2943 execute_transforms_with_options_and_budget(
2944 signature_document.root_element(),
2945 TransformData::Binary(xml.to_vec()),
2946 &transforms,
2947 TransformOptions::default(),
2948 &budget,
2949 )
2950 .expect("the first adapter parse must consume the exact allowance");
2951 let error = execute_transforms_with_options_and_budget(
2952 signature_document.root_element(),
2953 TransformData::Binary(xml.to_vec()),
2954 &transforms,
2955 TransformOptions::default(),
2956 &budget,
2957 )
2958 .expect_err("the second adapter parse must inherit exhausted work");
2959
2960 assert!(matches!(
2961 error,
2962 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2963 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
2964 maximum,
2965 actual,
2966 }) if maximum == adapter_work
2967 && actual == adapter_work + xml.len()
2968 ));
2969 }
2970
2971 #[test]
2972 fn binary_to_node_set_adapter_bounds_external_xml_nodes_during_parse() {
2973 let signature_document = Document::parse("<Signature/>").unwrap();
2976 let xml = format!(
2977 "<root>{}</root>",
2978 "<n/>".repeat(XML_DOCUMENT_NODE_CEILING as usize + 1),
2979 );
2980 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
2981
2982 execute_transforms(
2983 signature_document.root_element(),
2984 TransformData::Binary(b"<root><n/></root>".to_vec()),
2985 &transforms,
2986 )
2987 .expect("external XML below the node ceiling must parse and transform");
2988
2989 let error = execute_transforms(
2990 signature_document.root_element(),
2991 TransformData::Binary(xml.into_bytes()),
2992 &transforms,
2993 )
2994 .expect_err("external XML exceeding the node ceiling must fail during parse");
2995
2996 assert!(matches!(
2997 error,
2998 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2999 resource: crate::policy::resource_name::XML_NODES,
3000 ..
3001 })
3002 ));
3003 }
3004
3005 #[test]
3006 fn binary_to_node_set_adapter_enforces_operation_depth() {
3007 let signature_document = Document::parse("<Signature/>").unwrap();
3010 let resources = crate::policy::ResourcePolicy {
3011 max_xml_depth: 2,
3012 ..crate::policy::ResourcePolicy::default()
3013 };
3014 let budget = TransformExecutionBudget::from_resources(&resources);
3015 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
3016
3017 let error = execute_transforms_with_options_and_budget(
3018 signature_document.root_element(),
3019 TransformData::Binary(b"<root><child><leaf/></child></root>".to_vec()),
3020 &transforms,
3021 TransformOptions::default(),
3022 &budget,
3023 )
3024 .expect_err("over-depth transform XML must be rejected before XPath");
3025
3026 assert!(matches!(
3027 error,
3028 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3029 resource: crate::policy::resource_name::XML_DEPTH,
3030 maximum: 2,
3031 actual: 3,
3032 })
3033 ));
3034 }
3035
3036 #[test]
3037 fn binary_to_node_set_adapter_enforces_operation_namespace_limit() {
3038 let signature_document = Document::parse("<Signature/>").unwrap();
3041 let resources = crate::policy::ResourcePolicy {
3042 max_xml_namespace_bindings: 1,
3043 ..crate::policy::ResourcePolicy::default()
3044 };
3045 let budget = TransformExecutionBudget::from_resources(&resources);
3046 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
3047
3048 let error = execute_transforms_with_options_and_budget(
3049 signature_document.root_element(),
3050 TransformData::Binary(b"<root xmlns:a=\"urn:a\" xmlns:b=\"urn:b\"/>".to_vec()),
3051 &transforms,
3052 TransformOptions::default(),
3053 &budget,
3054 )
3055 .expect_err("detached XML must inherit the operation namespace-binding limit");
3056
3057 assert!(matches!(
3058 error,
3059 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3060 resource: crate::policy::resource_name::XML_NAMESPACE_BINDINGS,
3061 maximum: 1,
3062 actual: 2,
3063 })
3064 ));
3065 }
3066
3067 #[test]
3068 fn xpath_projection_uses_shared_materialization_budget() {
3069 let document = Document::parse("<root attribute=\"value\"/>").unwrap();
3073 let budget = TransformExecutionBudget::with_node_set_materialization_limit(1);
3074 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
3075
3076 let error = execute_transforms_with_options_and_budget(
3077 document.root_element(),
3078 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap()),
3079 &transforms,
3080 TransformOptions::default(),
3081 &budget,
3082 )
3083 .expect_err("XPath projection must charge cloned attribute names");
3084
3085 assert!(matches!(
3086 error,
3087 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3088 resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
3089 ..
3090 })
3091 ));
3092 }
3093
3094 #[test]
3095 fn explicit_and_implicit_c14n_stop_at_the_execution_ceiling() {
3096 let xml = format!("<root>{}</root>", "x".repeat(4_096));
3099 let document = Document::parse(&xml).unwrap();
3100 let nodes = || {
3101 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap())
3102 };
3103 let algorithm =
3104 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
3105
3106 for transforms in [&[][..], &[Transform::C14n(algorithm)][..]] {
3107 let error = execute_transforms_with_options_and_budget(
3108 document.root_element(),
3109 nodes(),
3110 transforms,
3111 TransformOptions::default(),
3112 &TransformExecutionBudget::with_c14n_limit(64),
3113 )
3114 .expect_err("canonicalization must stop at the execution ceiling");
3115
3116 assert!(matches!(
3117 error,
3118 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3119 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
3120 maximum: 64,
3121 ..
3122 })
3123 ));
3124 }
3125 }
3126
3127 #[test]
3128 fn execution_budget_bounds_c14n_output_across_references() {
3129 let xml = format!("<root>{}</root>", "x".repeat(4_096));
3133 let document = Document::parse(&xml).unwrap();
3134 let algorithm =
3135 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
3136 let transforms = [Transform::C14n(algorithm)];
3137 let input = || {
3138 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap())
3139 };
3140 let one_output = execute_transforms(document.root_element(), input(), &transforms)
3141 .expect("one canonicalization must succeed")
3142 .len();
3143 let limit = one_output * 2 - 1;
3144 let execution_budget = TransformExecutionBudget::with_c14n_limit(limit);
3145
3146 execute_transforms_with_options_and_budget(
3147 document.root_element(),
3148 input(),
3149 &transforms,
3150 TransformOptions::default(),
3151 &execution_budget,
3152 )
3153 .expect("the first Reference must fit the cumulative C14N output budget");
3154 let result = execute_transforms_with_options_and_budget(
3155 document.root_element(),
3156 input(),
3157 &transforms,
3158 TransformOptions::default(),
3159 &execution_budget,
3160 );
3161
3162 assert!(matches!(
3163 result,
3164 Err(TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3165 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
3166 maximum,
3167 ..
3168 })) if maximum == limit
3169 ));
3170 }
3171
3172 #[test]
3173 fn execution_budget_bounds_repeated_node_set_exclusions() {
3174 let document =
3177 Document::parse("<root><payload/><Signature><Object/></Signature></root>").unwrap();
3178 let signature = document
3179 .descendants()
3180 .find(|node| node.has_tag_name("Signature"))
3181 .unwrap();
3182 let input = || NodeSet::entire_document_with_comments(&document).unwrap();
3183 let entries_per_exclusion = input().len();
3184 let budget = TransformExecutionBudget::with_node_filter_limit(
3185 entries_per_exclusion.saturating_mul(2).saturating_sub(1),
3186 );
3187
3188 execute_transforms_with_options_and_budget(
3189 signature,
3190 TransformData::NodeSet(input()),
3191 &[Transform::Enveloped],
3192 TransformOptions::default(),
3193 &budget,
3194 )
3195 .expect("the first reference exclusion must fit the shared budget");
3196 let result = execute_transforms_with_options_and_budget(
3197 signature,
3198 TransformData::NodeSet(input()),
3199 &[Transform::Enveloped],
3200 TransformOptions::default(),
3201 &budget,
3202 );
3203
3204 assert!(
3205 matches!(
3206 result,
3207 Err(TransformError::Policy(
3208 crate::policy::PolicyViolation::ResourceLimit {
3209 resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
3210 ..
3211 }
3212 ))
3213 ),
3214 "the second reference exclusion must exhaust the shared budget"
3215 );
3216 }
3217
3218 #[test]
3219 fn xpath_node_set_operations_consume_filter_work_budget() {
3220 let document = Document::parse("<root><keep/><drop/></root>").unwrap();
3224 let transforms = [
3225 Transform::XPath(XPathExpression::new("true()")),
3226 Transform::XPathFilter2(vec![XPathFilter::new(
3227 XPathFilterOperation::Intersect,
3228 XPathExpression::new("//*"),
3229 )]),
3230 ];
3231
3232 for transform in transforms {
3233 let resources = crate::policy::ResourcePolicy {
3234 max_node_set_filter_work: 0,
3235 ..crate::policy::ResourcePolicy::default()
3236 };
3237 let budget = TransformExecutionBudget::from_resources(&resources);
3238 let input = NodeSet::entire_document_without_comments(&document)
3239 .map(TransformData::NodeSet)
3240 .unwrap();
3241 let error = execute_transforms_with_options_and_budget(
3242 document.root_element(),
3243 input,
3244 &[transform],
3245 TransformOptions::default(),
3246 &budget,
3247 )
3248 .expect_err("zero filter-work policy must deny XPath node-set operations");
3249
3250 assert!(matches!(
3251 error,
3252 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3253 resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
3254 maximum: 0,
3255 ..
3256 })
3257 ));
3258 }
3259 }
3260
3261 #[test]
3262 fn optimized_exclusion_consumes_xpath_execution_budgets() {
3263 let document = Document::parse(
3266 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><value/><ds:Signature/></root>"#,
3267 )
3268 .unwrap();
3269
3270 for resource in [
3271 crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS,
3272 crate::policy::resource_name::XPATH_EVALUATION_WORK,
3273 ] {
3274 let resources = if resource == crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS {
3275 crate::policy::ResourcePolicy {
3276 max_xpath_context_evaluations: 0,
3277 ..crate::policy::ResourcePolicy::default()
3278 }
3279 } else {
3280 crate::policy::ResourcePolicy {
3281 max_xpath_evaluation_work: 0,
3282 ..crate::policy::ResourcePolicy::default()
3283 }
3284 };
3285 let budget = TransformExecutionBudget::from_resources(&resources);
3286 let input = NodeSet::entire_document_without_comments(&document)
3287 .map(TransformData::NodeSet)
3288 .unwrap();
3289 let error = execute_transforms_with_options_and_budget(
3290 document.root_element(),
3291 input,
3292 &[Transform::XpathExcludeAllSignatures],
3293 TransformOptions::default(),
3294 &budget,
3295 )
3296 .expect_err("optimized XPath must obey execution budgets");
3297
3298 assert!(matches!(
3299 error,
3300 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3301 resource: actual,
3302 maximum: 0,
3303 ..
3304 }) if actual == resource
3305 ));
3306 }
3307 }
3308
3309 #[test]
3310 fn optimized_exclusion_charges_document_scan_and_each_filter_pass() {
3311 let document = Document::parse(
3316 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><payload><value/></payload><padding/><ds:Signature/><ds:Signature/></root>"#,
3317 )
3318 .unwrap();
3319 let payload = document
3320 .descendants()
3321 .find(|node| node.has_tag_name("payload"))
3322 .unwrap();
3323 let input = || NodeSet::subtree(payload).unwrap();
3324 let fragment_entries = input().len();
3325
3326 for resource in [
3327 crate::policy::resource_name::XPATH_EVALUATION_WORK,
3328 crate::policy::resource_name::NODE_SET_FILTER_WORK,
3329 ] {
3330 let resources = if resource == crate::policy::resource_name::XPATH_EVALUATION_WORK {
3331 crate::policy::ResourcePolicy {
3332 max_xpath_evaluation_work: fragment_entries,
3333 ..crate::policy::ResourcePolicy::default()
3334 }
3335 } else {
3336 crate::policy::ResourcePolicy {
3337 max_node_set_filter_work: fragment_entries,
3338 ..crate::policy::ResourcePolicy::default()
3339 }
3340 };
3341 let budget = TransformExecutionBudget::from_resources(&resources);
3342 let error = execute_transforms_with_options_and_budget(
3343 document.root_element(),
3344 TransformData::NodeSet(input()),
3345 &[Transform::XpathExcludeAllSignatures],
3346 TransformOptions::default(),
3347 &budget,
3348 )
3349 .expect_err("document-sized optimized XPath work must exceed the fragment budget");
3350
3351 assert_resource_limit(&error, resource);
3352 }
3353 }
3354
3355 #[test]
3356 fn filter2_charges_document_sized_set_operations() {
3357 let document =
3361 Document::parse("<root><payload><value/></payload><outside/><outside/></root>")
3362 .unwrap();
3363 let payload = document
3364 .descendants()
3365 .find(|node| node.has_tag_name("payload"))
3366 .unwrap();
3367 let input = NodeSet::subtree(payload).unwrap();
3368 let resources = crate::policy::ResourcePolicy {
3369 max_node_set_filter_work: input.len(),
3370 ..crate::policy::ResourcePolicy::default()
3371 };
3372 let budget = TransformExecutionBudget::from_resources(&resources);
3373 let transform = Transform::XPathFilter2(vec![XPathFilter::new(
3374 XPathFilterOperation::Intersect,
3375 XPathExpression::new("//*"),
3376 )]);
3377
3378 let error = execute_transforms_with_options_and_budget(
3379 document.root_element(),
3380 TransformData::NodeSet(input),
3381 &[transform],
3382 TransformOptions::default(),
3383 &budget,
3384 )
3385 .expect_err("Filter 2 must charge document-sized set operations");
3386
3387 assert_resource_limit(&error, crate::policy::resource_name::NODE_SET_FILTER_WORK);
3388 }
3389
3390 #[test]
3391 fn execution_budget_bounds_implicit_c14n_across_references() {
3392 let xml = format!("<root>{}</root>", "x".repeat(4_096));
3396 let document = Document::parse(&xml).unwrap();
3397 let input = || {
3398 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap())
3399 };
3400 let one_output = execute_transforms(document.root_element(), input(), &[])
3401 .expect("one implicit canonicalization must succeed")
3402 .len();
3403 let limit = one_output * 3 - 1;
3404 let execution_budget = TransformExecutionBudget::with_c14n_limit(limit);
3405
3406 for _ in 0..2 {
3407 execute_transforms_with_options_and_budget(
3408 document.root_element(),
3409 input(),
3410 &[],
3411 TransformOptions::default(),
3412 &execution_budget,
3413 )
3414 .expect("two implicit C14N outputs must fit the shared budget");
3415 }
3416 let result = execute_transforms_with_options_and_budget(
3417 document.root_element(),
3418 input(),
3419 &[],
3420 TransformOptions::default(),
3421 &execution_budget,
3422 );
3423
3424 assert!(matches!(
3425 result,
3426 Err(TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3427 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
3428 maximum,
3429 ..
3430 })) if maximum == limit
3431 ));
3432 }
3433
3434 #[test]
3435 fn pipeline_enveloped_then_c14n() {
3436 let xml = r#"<root xmlns:ns="http://example.com" b="2" a="1">
3438 <data>hello</data>
3439 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
3440 <SignedInfo/>
3441 <SignatureValue>abc</SignatureValue>
3442 </Signature>
3443 </root>"#;
3444 let doc = Document::parse(xml).unwrap();
3445
3446 let sig_node = doc
3447 .descendants()
3448 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
3449 .unwrap();
3450
3451 let initial =
3452 TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc).unwrap());
3453 let transforms = vec![
3454 Transform::Enveloped,
3455 Transform::C14n(
3456 C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#").unwrap(),
3457 ),
3458 ];
3459
3460 let result = execute_transforms(sig_node, initial, &transforms).unwrap();
3461
3462 let output = String::from_utf8(result).unwrap();
3463 assert!(!output.contains("Signature"));
3465 assert!(!output.contains("SignedInfo"));
3466 assert!(!output.contains("SignatureValue"));
3467 assert!(output.contains("<data>hello</data>"));
3468 }
3469
3470 #[test]
3471 fn pipeline_c14n_then_enveloped_remaps_the_exact_signature() {
3472 let xml = r#"<root>
3476 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#" Id="other"/>
3477 <data>hello</data>
3478 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#" Id="owner"/>
3479 </root>"#;
3480 let document = Document::parse(xml).unwrap();
3481 let signature = document
3482 .descendants()
3483 .find(|node| node.attribute("Id") == Some("owner"))
3484 .unwrap();
3485 let initial =
3486 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap());
3487 let transforms = vec![
3488 Transform::C14n(
3489 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap(),
3490 ),
3491 Transform::Enveloped,
3492 ];
3493
3494 let output = execute_transforms(signature, initial, &transforms).unwrap();
3495 let output = String::from_utf8(output).unwrap();
3496
3497 assert!(output.contains("Id=\"other\""));
3498 assert!(!output.contains("Id=\"owner\""));
3499 assert!(output.contains("<data>hello</data>"));
3500 }
3501
3502 #[test]
3503 fn pipeline_remaps_signature_after_xpath_removes_an_earlier_sibling() {
3504 let xml = r#"<root>
3507 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#" Id="other"/>
3508 <discard/>
3509 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#" Id="owner"/>
3510 </root>"#;
3511 let document = Document::parse(xml).unwrap();
3512 let signature = document
3513 .descendants()
3514 .find(|node| node.attribute("Id") == Some("owner"))
3515 .unwrap();
3516 let initial =
3517 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap());
3518 let transforms = vec![
3519 Transform::XPath(XPathExpression::new("not(self::discard)")),
3520 Transform::C14n(
3521 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap(),
3522 ),
3523 Transform::Enveloped,
3524 ];
3525
3526 let output = execute_transforms(signature, initial, &transforms).unwrap();
3527 let output = String::from_utf8(output).unwrap();
3528
3529 assert!(output.contains("Id=\"other\""));
3530 assert!(!output.contains("Id=\"owner\""));
3531 assert!(!output.contains("discard"));
3532 }
3533
3534 #[test]
3535 fn dependency_tracking_retains_structurally_excluded_nodes_for_later_xpath() {
3536 let document = Document::parse(
3539 r#"<root><Signature><Object><Manifest><DigestValue>pending</DigestValue></Manifest></Object></Signature></root>"#,
3540 )
3541 .unwrap();
3542 let signature = document
3543 .descendants()
3544 .find(|node| node.tag_name().name() == "Signature")
3545 .unwrap();
3546 let manifest = document
3547 .descendants()
3548 .find(|node| node.tag_name().name() == "Manifest")
3549 .unwrap();
3550 let digest_text = document
3551 .descendants()
3552 .find(|node| node.is_text() && node.text() == Some("pending"))
3553 .unwrap();
3554 let transforms = [
3555 Transform::XPath(XPathExpression::new("not(ancestor-or-self::DigestValue)")),
3556 Transform::XPath(XPathExpression::new(
3557 "string-length(string(//DigestValue)) >= 0",
3558 )),
3559 ];
3560
3561 let output = execute_transforms_with_dependency_nodes(
3562 signature,
3563 TransformData::NodeSet(NodeSet::subtree(manifest).unwrap()),
3564 &transforms,
3565 TransformOptions::default(),
3566 &TransformExecutionBudget::default(),
3567 vec![(7, digest_text.id())],
3568 )
3569 .unwrap();
3570
3571 assert_eq!(output.dependencies, HashSet::from([7]));
3572 }
3573
3574 #[test]
3575 fn dependency_tracking_discards_dormant_nodes_at_binary_boundary() {
3576 let document = Document::parse(
3579 r#"<root><DigestValue>external</DigestValue><Signature><payload>value</payload></Signature></root>"#,
3580 )
3581 .unwrap();
3582 let signature = document
3583 .descendants()
3584 .find(|node| node.tag_name().name() == "Signature")
3585 .unwrap();
3586 let digest_text = document
3587 .descendants()
3588 .find(|node| node.is_text() && node.text() == Some("external"))
3589 .unwrap();
3590 let transforms = [
3591 Transform::C14n(
3592 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap(),
3593 ),
3594 Transform::XPath(XPathExpression::new(
3595 "string-length(string(//DigestValue)) >= 0",
3596 )),
3597 ];
3598
3599 let output = execute_transforms_with_dependency_nodes(
3600 signature,
3601 TransformData::NodeSet(NodeSet::subtree(signature).unwrap()),
3602 &transforms,
3603 TransformOptions::default(),
3604 &TransformExecutionBudget::default(),
3605 vec![(11, digest_text.id())],
3606 )
3607 .unwrap();
3608
3609 assert!(output.dependencies.is_empty());
3610 }
3611
3612 #[test]
3613 fn pipeline_enveloped_ignores_signature_absent_after_base64_adaptation() {
3614 let source = Document::parse(
3618 r#"<root><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"/></root>"#,
3619 )
3620 .unwrap();
3621 let signature = source
3622 .descendants()
3623 .find(|node| node.tag_name().name() == "Signature")
3624 .unwrap();
3625 let encoded = base64::engine::general_purpose::STANDARD.encode(b"<payload>ok</payload>");
3626 let transforms = vec![
3627 Transform::Base64Decode,
3628 Transform::XPath(XPathExpression::new("true()")),
3629 Transform::Enveloped,
3630 ];
3631
3632 let output = execute_transforms(
3633 signature,
3634 TransformData::Binary(encoded.into()),
3635 &transforms,
3636 )
3637 .unwrap();
3638
3639 assert_eq!(output, b"<payload>ok</payload>");
3640 }
3641
3642 #[test]
3643 fn pipeline_no_transforms_applies_default_c14n() {
3644 let xml = r#"<root b="2" a="1"><child/></root>"#;
3646 let doc = Document::parse(xml).unwrap();
3647
3648 let initial =
3649 TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc).unwrap());
3650 let result = execute_transforms(doc.root_element(), initial, &[]).unwrap();
3651
3652 let output = String::from_utf8(result).unwrap();
3653 assert_eq!(output, r#"<root a="1" b="2"><child></child></root>"#);
3654 }
3655
3656 #[test]
3657 fn pipeline_binary_passthrough() {
3658 let xml = "<root/>";
3661 let doc = Document::parse(xml).unwrap();
3662
3663 let initial = TransformData::Binary(b"raw bytes".to_vec());
3664 let result = execute_transforms(doc.root_element(), initial, &[]).unwrap();
3665
3666 assert_eq!(result, b"raw bytes");
3667 }
3668
3669 #[test]
3672 fn enveloped_only_excludes_own_signature() {
3673 let xml = r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3676 <data>hello</data>
3677 <ds:Signature Id="sig-other">
3678 <ds:SignedInfo><ds:Reference URI=""/></ds:SignedInfo>
3679 </ds:Signature>
3680 <ds:Signature Id="sig-target">
3681 <ds:SignedInfo><ds:Reference URI=""/></ds:SignedInfo>
3682 </ds:Signature>
3683 </root>"#;
3684 let doc = Document::parse(xml).unwrap();
3685
3686 let sig_node = doc
3688 .descendants()
3689 .find(|n| n.is_element() && n.attribute("Id") == Some("sig-target"))
3690 .unwrap();
3691
3692 let node_set = NodeSet::entire_document_without_comments(&doc).unwrap();
3693 let data = TransformData::NodeSet(node_set);
3694
3695 let result = apply_transform(sig_node, &Transform::Enveloped, data).unwrap();
3696 let node_set = result.into_node_set().unwrap();
3697
3698 let sig_other = doc
3700 .descendants()
3701 .find(|n| n.is_element() && n.attribute("Id") == Some("sig-other"))
3702 .unwrap();
3703 assert!(
3704 node_set.contains(sig_other),
3705 "other Signature elements should NOT be excluded"
3706 );
3707
3708 assert!(
3710 !node_set.contains(sig_node),
3711 "the specific Signature being verified should be excluded"
3712 );
3713 }
3714
3715 #[test]
3718 fn parse_transforms_enveloped_and_exc_c14n() {
3719 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
3720 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
3721 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3722 </Transforms>"#;
3723 let doc = Document::parse(xml).unwrap();
3724 let transforms_node = doc.root_element();
3725
3726 let chain = parse_transforms(transforms_node).unwrap();
3727 assert_eq!(chain.len(), 2);
3728 assert!(matches!(chain[0], Transform::Enveloped));
3729 assert!(matches!(chain[1], Transform::C14n(_)));
3730 }
3731
3732 #[test]
3733 fn parse_transforms_rejects_unbounded_chain() {
3734 let entries = format!(r#"<Transform Algorithm="{BASE64_TRANSFORM_URI}"/>"#).repeat(65);
3737 let xml = format!(r#"<Transforms xmlns="{XMLDSIG_NS}">{entries}</Transforms>"#);
3738 let doc = Document::parse(&xml).unwrap();
3739
3740 assert!(matches!(
3741 parse_transforms(doc.root_element()),
3742 Err(TransformError::Policy(
3743 crate::policy::PolicyViolation::ResourceLimit {
3744 resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
3745 maximum: MAX_TRANSFORMS_PER_REFERENCE,
3746 ..
3747 }
3748 ))
3749 ));
3750 }
3751
3752 #[test]
3753 fn parse_transforms_accepts_parameterless_base64() {
3754 let xml = format!(
3755 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{BASE64_TRANSFORM_URI}">
3756 </Transform></Transforms>"#
3757 );
3758 let doc = Document::parse(&xml).unwrap();
3759
3760 let chain = parse_transforms(doc.root_element()).unwrap();
3761
3762 assert_eq!(chain.len(), 1);
3763 assert!(matches!(chain[0], Transform::Base64Decode));
3764 }
3765
3766 #[test]
3767 fn parse_transforms_rejects_base64_parameters() {
3768 for parameter in ["<Parameter/>", "unexpected", "\u{00A0}"] {
3769 let xml = format!(
3770 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{BASE64_TRANSFORM_URI}">{parameter}</Transform></Transforms>"#
3771 );
3772 let doc = Document::parse(&xml).unwrap();
3773
3774 let result = parse_transforms(doc.root_element());
3775
3776 assert!(matches!(
3777 result,
3778 Err(TransformError::UnsupportedTransform(_))
3779 ));
3780 }
3781 }
3782
3783 #[test]
3784 fn parse_transforms_rejects_non_xpath_boundary_whitespace() {
3785 let xml = format!(
3788 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{XPATH_TRANSFORM_URI}"><XPath> true()</XPath></Transform></Transforms>"#
3789 );
3790 let doc = Document::parse(&xml).unwrap();
3791
3792 let result = parse_transforms(doc.root_element());
3793
3794 assert!(matches!(result, Err(TransformError::XPath(_))));
3795 }
3796
3797 #[test]
3798 fn parse_transforms_with_inclusive_prefixes() {
3799 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#"
3800 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
3801 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
3802 <ec:InclusiveNamespaces PrefixList="ds saml #default"/>
3803 </Transform>
3804 </Transforms>"#;
3805 let doc = Document::parse(xml).unwrap();
3806 let transforms_node = doc.root_element();
3807
3808 let chain = parse_transforms(transforms_node).unwrap();
3809 assert_eq!(chain.len(), 1);
3810 match &chain[0] {
3811 Transform::C14n(algo) => {
3812 assert!(algo.inclusive_prefixes().contains("ds"));
3813 assert!(algo.inclusive_prefixes().contains("saml"));
3814 assert!(algo.inclusive_prefixes().contains("")); }
3816 other => panic!("expected C14n, got: {other:?}"),
3817 }
3818 }
3819
3820 #[test]
3821 fn parse_transforms_ignores_wrong_ns_inclusive_namespaces() {
3822 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
3825 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
3826 <InclusiveNamespaces xmlns="http://example.com/fake"
3827 PrefixList="attacker-controlled"/>
3828 </Transform>
3829 </Transforms>"#;
3830 let doc = Document::parse(xml).unwrap();
3831
3832 let chain = parse_transforms(doc.root_element()).unwrap();
3833 assert_eq!(chain.len(), 1);
3834 match &chain[0] {
3835 Transform::C14n(algo) => {
3836 assert!(
3838 algo.inclusive_prefixes().is_empty(),
3839 "should ignore InclusiveNamespaces in wrong namespace"
3840 );
3841 }
3842 other => panic!("expected C14n, got: {other:?}"),
3843 }
3844 }
3845
3846 #[test]
3847 fn parse_transforms_missing_prefix_list_is_error() {
3848 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#"
3851 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
3852 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
3853 <ec:InclusiveNamespaces/>
3854 </Transform>
3855 </Transforms>"#;
3856 let doc = Document::parse(xml).unwrap();
3857
3858 let result = parse_transforms(doc.root_element());
3859 assert!(result.is_err());
3860 assert!(matches!(
3861 result.unwrap_err(),
3862 TransformError::UnsupportedTransform(_)
3863 ));
3864 }
3865
3866 #[test]
3867 fn parse_transforms_unsupported_algorithm() {
3868 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
3869 <Transform Algorithm="http://example.com/unknown"/>
3870 </Transforms>"#;
3871 let doc = Document::parse(xml).unwrap();
3872
3873 let result = parse_transforms(doc.root_element());
3874 assert!(result.is_err());
3875 assert!(matches!(
3876 result.unwrap_err(),
3877 TransformError::UnsupportedTransform(_)
3878 ));
3879 }
3880
3881 #[test]
3882 fn parse_transforms_missing_algorithm() {
3883 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
3884 <Transform/>
3885 </Transforms>"#;
3886 let doc = Document::parse(xml).unwrap();
3887
3888 let result = parse_transforms(doc.root_element());
3889 assert!(result.is_err());
3890 assert!(matches!(
3891 result.unwrap_err(),
3892 TransformError::UnsupportedTransform(_)
3893 ));
3894 }
3895
3896 #[test]
3897 fn parse_transforms_empty() {
3898 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#"/>"#;
3899 let doc = Document::parse(xml).unwrap();
3900
3901 let chain = parse_transforms(doc.root_element()).unwrap();
3902 assert!(chain.is_empty());
3903 }
3904
3905 #[test]
3906 fn parse_transforms_accepts_enveloped_compat_xpath() {
3907 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
3908 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
3909 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
3910 not(ancestor-or-self::dsig:Signature)
3911 </XPath>
3912 </Transform>
3913 </Transforms>"#;
3914 let doc = Document::parse(xml).unwrap();
3915
3916 let chain = parse_transforms(doc.root_element()).unwrap();
3917 assert_eq!(chain.len(), 1);
3918 assert!(matches!(chain[0], Transform::XpathExcludeAllSignatures));
3919 }
3920
3921 #[test]
3922 fn parse_transforms_accepts_general_xpath_expressions() {
3923 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
3926 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
3927 <XPath>self::node()</XPath>
3928 </Transform>
3929 </Transforms>"#;
3930 let doc = Document::parse(xml).unwrap();
3931
3932 let result = parse_transforms(doc.root_element()).unwrap();
3933 assert!(matches!(result.as_slice(), [Transform::XPath(_)]));
3934 }
3935
3936 #[test]
3937 fn parse_xpath_transform_ignores_comments_and_processing_instructions() {
3938 let xml = format!(
3941 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{XPATH_TRANSFORM_URI}"><!-- before --><?probe value?><XPath>true()</XPath><!-- after --><?done?></Transform></Transforms>"#
3942 );
3943 let doc = Document::parse(&xml).unwrap();
3944
3945 let transforms = parse_transforms(doc.root_element()).unwrap();
3946
3947 assert!(matches!(transforms.as_slice(), [Transform::XPath(_)]));
3948 }
3949
3950 #[test]
3951 fn parse_filter2_transform_ignores_comments_and_processing_instructions() {
3952 let xml = format!(
3955 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{XPATH_FILTER2_TRANSFORM_URI}"><!-- before --><?probe value?><XPath xmlns="{XPATH_FILTER2_TRANSFORM_URI}" Filter="intersect">/root</XPath><!-- after --><?done?></Transform></Transforms>"#
3956 );
3957 let doc = Document::parse(&xml).unwrap();
3958
3959 let transforms = parse_transforms(doc.root_element()).unwrap();
3960
3961 assert!(matches!(
3962 transforms.as_slice(),
3963 [Transform::XPathFilter2(filters)] if filters.len() == 1
3964 ));
3965 }
3966
3967 #[test]
3968 fn parse_transforms_bounds_raw_xpath_parameter_text() {
3969 let padding = " ".repeat(MAX_XPATH_EXPRESSION_BYTES);
3972 let xml = format!(
3973 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{XPATH_TRANSFORM_URI}"><XPath>{padding}true()</XPath></Transform></Transforms>"#
3974 );
3975 let doc = Document::parse(&xml).unwrap();
3976
3977 let error = parse_transforms(doc.root_element())
3978 .expect_err("raw XPath parameter text must obey the expression bound");
3979
3980 assert!(matches!(
3981 error,
3982 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3983 resource: crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
3984 maximum: MAX_XPATH_EXPRESSION_BYTES,
3985 ..
3986 })
3987 ));
3988 }
3989
3990 #[test]
3991 fn xpath_namespace_limits_apply_to_each_expression() {
3992 let xml = format!(
3995 r#"<Transforms xmlns="{XMLDSIG_NS}">
3996 <Transform Algorithm="{XPATH_TRANSFORM_URI}">
3997 <XPath xmlns:a="urn:a">a:item</XPath>
3998 </Transform>
3999 <Transform Algorithm="{XPATH_TRANSFORM_URI}">
4000 <XPath xmlns:b="urn:b">b:item</XPath>
4001 </Transform>
4002 </Transforms>"#
4003 );
4004 let doc = Document::parse(&xml).unwrap();
4005 let resources = crate::policy::ResourcePolicy {
4006 max_xpath_namespace_bindings: 1,
4007 ..crate::policy::ResourcePolicy::default()
4008 };
4009 let mut budget = XPathSignatureParseBudget::from_resources(&resources);
4010
4011 let transforms = parse_transforms_with_budget(doc.root_element(), &mut budget)
4012 .expect("each XPath independently satisfies the one-binding ceiling");
4013
4014 assert_eq!(transforms.len(), 2);
4015 }
4016
4017 #[test]
4018 fn parse_transforms_applies_namespace_storage_limit_per_expression() {
4019 let declarations = (0..32)
4022 .map(|index| {
4023 format!(
4024 "xmlns:n{index}=\"urn:namespace:{index}:{}\"",
4025 "x".repeat(64)
4026 )
4027 })
4028 .collect::<Vec<_>>()
4029 .join(" ");
4030 let filters = (0..MAX_XPATH_FILTERS)
4031 .map(|_| {
4032 format!(
4033 r#"<XPath xmlns="{XPATH_FILTER2_TRANSFORM_URI}" Filter="intersect">true()</XPath>"#
4034 )
4035 })
4036 .collect::<String>();
4037 let xml = format!(
4038 r#"<Transforms xmlns="{XMLDSIG_NS}" {declarations}><Transform Algorithm="{XPATH_FILTER2_TRANSFORM_URI}">{filters}</Transform></Transforms>"#
4039 );
4040 let doc = Document::parse(&xml).unwrap();
4041
4042 let transforms = parse_transforms(doc.root_element())
4043 .expect("each expression remains below its namespace storage ceiling");
4044
4045 assert!(matches!(
4046 transforms.as_slice(),
4047 [Transform::XPathFilter2(filters)] if filters.len() == MAX_XPATH_FILTERS
4048 ));
4049 }
4050
4051 #[test]
4052 fn parse_transforms_rejects_xpath_in_wrong_namespace() {
4053 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4054 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
4055 <foo:XPath xmlns:foo="http://example.com/ns">
4056 not(ancestor-or-self::dsig:Signature)
4057 </foo:XPath>
4058 </Transform>
4059 </Transforms>"#;
4060 let doc = Document::parse(xml).unwrap();
4061
4062 let result = parse_transforms(doc.root_element());
4063 assert!(result.is_err());
4064 assert!(matches!(result.unwrap_err(), TransformError::XPath(_)));
4065 }
4066
4067 #[test]
4068 fn parse_transforms_preserves_nonstandard_prefix_bindings() {
4069 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4073 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
4074 <XPath xmlns:dsig="http://example.com/not-xmldsig">
4075 not(ancestor-or-self::dsig:Signature)
4076 </XPath>
4077 </Transform>
4078 </Transforms>"#;
4079 let doc = Document::parse(xml).unwrap();
4080
4081 let result = parse_transforms(doc.root_element()).unwrap();
4082 let [Transform::XPath(xpath)] = result.as_slice() else {
4083 panic!("expected general XPath transform");
4084 };
4085 assert_eq!(
4086 xpath.namespaces().get("dsig").map(String::as_str),
4087 Some("http://example.com/not-xmldsig")
4088 );
4089 }
4090
4091 #[test]
4092 fn parse_transforms_rejects_xpath_with_internal_whitespace_mutation() {
4093 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4094 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
4095 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
4096 not(ancestor-or-self::dsig:Signa ture)
4097 </XPath>
4098 </Transform>
4099 </Transforms>"#;
4100 let doc = Document::parse(xml).unwrap();
4101
4102 let result = parse_transforms(doc.root_element());
4103 assert!(matches!(result.unwrap_err(), TransformError::XPath(_)));
4104 }
4105
4106 #[test]
4107 fn parse_transforms_rejects_multiple_xpath_children() {
4108 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4109 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
4110 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
4111 not(ancestor-or-self::dsig:Signature)
4112 </XPath>
4113 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
4114 not(ancestor-or-self::dsig:Signature)
4115 </XPath>
4116 </Transform>
4117 </Transforms>"#;
4118 let doc = Document::parse(xml).unwrap();
4119
4120 let result = parse_transforms(doc.root_element());
4121 assert!(result.is_err());
4122 assert!(matches!(result.unwrap_err(), TransformError::XPath(_)));
4123 }
4124
4125 #[test]
4126 fn parse_transforms_rejects_non_xpath_element_children() {
4127 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4128 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
4129 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
4130 not(ancestor-or-self::dsig:Signature)
4131 </XPath>
4132 <Extra/>
4133 </Transform>
4134 </Transforms>"#;
4135 let doc = Document::parse(xml).unwrap();
4136
4137 let result = parse_transforms(doc.root_element());
4138 assert!(result.is_err());
4139 assert!(matches!(result.unwrap_err(), TransformError::XPath(_)));
4140 }
4141
4142 #[test]
4143 fn parse_transforms_rejects_malformed_xpath_filter2_parameters() {
4144 for parameter in [
4148 r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2">//Data</XPath>"#,
4149 r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="exclude">//Data</XPath>"#,
4150 r#"<XPath xmlns="urn:wrong" Filter="intersect">//Data</XPath>"#,
4151 r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect" Extra="value">//Data</XPath>"#,
4152 ] {
4153 let xml = format!(
4154 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{XPATH_FILTER2_TRANSFORM_URI}">{parameter}</Transform></Transforms>"#
4155 );
4156 let doc = Document::parse(&xml).unwrap();
4157
4158 let result = parse_transforms(doc.root_element());
4159
4160 assert!(matches!(result, Err(TransformError::XPath(_))));
4161 }
4162 }
4163
4164 #[test]
4165 fn parse_transforms_rejects_empty_xpath_filter2_sequence() {
4166 let xml = format!(
4169 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{XPATH_FILTER2_TRANSFORM_URI}"/></Transforms>"#
4170 );
4171 let doc = Document::parse(&xml).unwrap();
4172
4173 let result = parse_transforms(doc.root_element());
4174
4175 assert!(matches!(result, Err(TransformError::XPath(_))));
4176 }
4177
4178 #[test]
4179 fn parse_transform_chain_hashes_xpath_document_once() {
4180 let filters = format!(
4183 r#"<XPath xmlns="{XPATH_FILTER2_TRANSFORM_URI}" Filter="intersect">true()</XPath>"#
4184 )
4185 .repeat(MAX_XPATH_FILTERS);
4186 let transform = format!(
4187 r#"<Transform Algorithm="{XPATH_FILTER2_TRANSFORM_URI}">{filters}</Transform>"#
4188 );
4189 let xml =
4190 format!(r#"<Transforms xmlns="{XMLDSIG_NS}">{transform}{transform}</Transforms>"#);
4191 let document = Document::parse(&xml).unwrap();
4192
4193 XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(|count| count.set(0));
4194 let transforms = parse_transforms(document.root_element()).unwrap();
4195 let computations = XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(Cell::get);
4196
4197 assert_eq!(transforms.len(), 2);
4198 assert!(transforms.iter().all(
4199 |transform| matches!(transform, Transform::XPathFilter2(filters) if filters.len() == MAX_XPATH_FILTERS)
4200 ));
4201 assert_eq!(
4202 computations, 1,
4203 "one parsed transform chain must hash its source document once"
4204 );
4205 }
4206
4207 #[test]
4208 fn xpath_compat_excludes_other_signature_subtrees_too() {
4209 let xml = r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4210 <payload>keep-me</payload>
4211 <ds:Signature Id="sig-1">
4212 <ds:SignedInfo/>
4213 <ds:SignatureValue>one</ds:SignatureValue>
4214 </ds:Signature>
4215 <ds:Signature Id="sig-2">
4216 <ds:SignedInfo/>
4217 <ds:SignatureValue>two</ds:SignatureValue>
4218 </ds:Signature>
4219 </root>"#;
4220 let doc = Document::parse(xml).unwrap();
4221 let signature_nodes: Vec<_> = doc
4222 .descendants()
4223 .filter(|node| {
4224 node.is_element()
4225 && node.tag_name().name() == "Signature"
4226 && node.tag_name().namespace() == Some(XMLDSIG_NS)
4227 })
4228 .collect();
4229 let sig_node = signature_nodes[0];
4230
4231 let enveloped = execute_transforms(
4232 sig_node,
4233 TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc).unwrap()),
4234 &[
4235 Transform::Enveloped,
4236 Transform::C14n(C14nAlgorithm::new(
4237 crate::c14n::C14nMode::Inclusive1_0,
4238 false,
4239 )),
4240 ],
4241 )
4242 .unwrap();
4243 let xpath_compat = execute_transforms(
4244 sig_node,
4245 TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc).unwrap()),
4246 &[
4247 Transform::XpathExcludeAllSignatures,
4248 Transform::C14n(C14nAlgorithm::new(
4249 crate::c14n::C14nMode::Inclusive1_0,
4250 false,
4251 )),
4252 ],
4253 )
4254 .unwrap();
4255
4256 let enveloped = String::from_utf8(enveloped).unwrap();
4257 let xpath_compat = String::from_utf8(xpath_compat).unwrap();
4258
4259 assert!(enveloped.contains("sig-2"));
4260 assert!(!xpath_compat.contains("sig-1"));
4261 assert!(!xpath_compat.contains("sig-2"));
4262 assert!(xpath_compat.contains("keep-me"));
4263 }
4264
4265 #[test]
4266 fn parse_transforms_inclusive_c14n_variants() {
4267 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4268 <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
4269 <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/>
4270 <Transform Algorithm="http://www.w3.org/2006/12/xml-c14n11"/>
4271 </Transforms>"#;
4272 let doc = Document::parse(xml).unwrap();
4273
4274 let chain = parse_transforms(doc.root_element()).unwrap();
4275 assert_eq!(chain.len(), 3);
4276 for t in &chain {
4278 assert!(matches!(t, Transform::C14n(_)));
4279 }
4280 }
4281
4282 #[test]
4283 fn parsed_xpath_rejects_node_id_collision_from_another_document() {
4284 let source = Document::parse(
4288 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>count(. | here()) = 1</ds:XPath></ds:Transform></ds:Transforms></root>"#,
4289 )
4290 .unwrap();
4291 let transforms_node = source
4292 .descendants()
4293 .find(|node| node.has_tag_name((XMLDSIG_NS, "Transforms")))
4294 .unwrap();
4295 let transforms = parse_transforms(transforms_node).unwrap();
4296
4297 let target = Document::parse(
4298 "<root><container><parameter><unrelated/></parameter></container></root>",
4299 )
4300 .unwrap();
4301 let error = execute_transforms(
4302 target.root_element(),
4303 TransformData::NodeSet(NodeSet::entire_document_without_comments(&target).unwrap()),
4304 &transforms,
4305 )
4306 .expect_err("parsed here() provenance must reject another XML document");
4307
4308 assert!(
4309 matches!(error, TransformError::XPath(ref message) if message.contains("same XML document"))
4310 );
4311 }
4312
4313 #[test]
4314 fn transform_chain_computes_document_identity_once() {
4315 let document = Document::parse(
4318 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:Signature><ds:SignedInfo><ds:Reference URI=""><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>count(. | here()) = 1 or true()</ds:XPath></ds:Transform><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>count(. | here()) = 1 or true()</ds:XPath></ds:Transform></ds:Transforms></ds:Reference></ds:SignedInfo></ds:Signature></root>"#,
4319 )
4320 .unwrap();
4321 let transforms_node = document
4322 .descendants()
4323 .find(|node| node.has_tag_name((XMLDSIG_NS, "Transforms")))
4324 .unwrap();
4325 let transforms = parse_transforms(transforms_node).unwrap();
4326 let signature = document
4327 .descendants()
4328 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
4329 .unwrap();
4330 let initial = NodeSet::entire_document_without_comments(&document)
4331 .map(TransformData::NodeSet)
4332 .unwrap();
4333
4334 XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(|count| count.set(0));
4335 execute_transforms(signature, initial, &transforms).unwrap();
4336 let computations = XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(Cell::get);
4337
4338 assert_eq!(
4339 computations, 1,
4340 "one live document must be hashed once per chain"
4341 );
4342 }
4343
4344 #[test]
4345 fn transform_cache_identity_does_not_cross_chain_boundaries() {
4346 let document = Document::parse(
4349 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:Signature><ds:SignedInfo><ds:Reference URI=""><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>true()</ds:XPath></ds:Transform></ds:Transforms></ds:Reference></ds:SignedInfo></ds:Signature></root>"#,
4350 )
4351 .unwrap();
4352 let transforms_node = document
4353 .descendants()
4354 .find(|node| node.has_tag_name((XMLDSIG_NS, "Transforms")))
4355 .unwrap();
4356 let transforms = parse_transforms(transforms_node).unwrap();
4357 let signature = document
4358 .descendants()
4359 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
4360 .unwrap();
4361 let budget = TransformExecutionBudget::default();
4362
4363 XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(|count| count.set(0));
4364 for _ in 0..2 {
4365 let initial = NodeSet::entire_document_without_comments(&document)
4366 .map(TransformData::NodeSet)
4367 .unwrap();
4368 execute_transforms_with_options_and_budget(
4369 signature,
4370 initial,
4371 &transforms,
4372 TransformOptions::default(),
4373 &budget,
4374 )
4375 .unwrap();
4376 }
4377 let computations = XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(Cell::get);
4378
4379 assert_eq!(
4380 computations, 2,
4381 "each transform chain must establish a fresh document identity"
4382 );
4383 }
4384
4385 #[test]
4386 fn transform_chain_state_keys_identity_by_document() {
4387 let first_document = Document::parse("<first/>").unwrap();
4390 let second_document = Document::parse("<second/>").unwrap();
4391 let state = TransformChainState::default();
4392
4393 XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(|count| count.set(0));
4394 let first_identity = state.xpath_document_identity(&first_document);
4395 let second_identity = state.xpath_document_identity(&second_document);
4396 let computations = XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(Cell::get);
4397
4398 assert_ne!(first_identity, second_identity);
4399 assert_eq!(
4400 computations, 2,
4401 "each distinct live document must receive its own cached identity"
4402 );
4403 }
4404
4405 #[test]
4406 fn template_xpath_skips_document_identity_hash() {
4407 let document = Document::parse("<root><value/></root>").unwrap();
4410 let transforms = [
4411 Transform::XPath(XPathExpression::new("true()")),
4412 Transform::XPath(XPathExpression::new("true()")),
4413 ];
4414 let initial = NodeSet::entire_document_without_comments(&document)
4415 .map(TransformData::NodeSet)
4416 .unwrap();
4417
4418 XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(|count| count.set(0));
4419 execute_transforms(document.root_element(), initial, &transforms).unwrap();
4420 let computations = XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(Cell::get);
4421
4422 assert_eq!(
4423 computations, 0,
4424 "XPath without parsed here() provenance must not hash XML"
4425 );
4426 }
4427
4428 #[test]
4431 fn saml_enveloped_signature_full_pipeline() {
4432 let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
4434 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
4435 ID="_resp1">
4436 <saml:Assertion ID="_assert1">
4437 <saml:Subject>user@example.com</saml:Subject>
4438 </saml:Assertion>
4439 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4440 <ds:SignedInfo>
4441 <ds:Reference URI="">
4442 <ds:Transforms>
4443 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
4444 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4445 </ds:Transforms>
4446 </ds:Reference>
4447 </ds:SignedInfo>
4448 <ds:SignatureValue>fakesig==</ds:SignatureValue>
4449 </ds:Signature>
4450 </samlp:Response>"#;
4451 let doc = Document::parse(xml).unwrap();
4452
4453 let sig_node = doc
4455 .descendants()
4456 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
4457 .unwrap();
4458
4459 let reference = doc
4461 .descendants()
4462 .find(|n| n.is_element() && n.tag_name().name() == "Reference")
4463 .unwrap();
4464 let transforms_elem = reference
4465 .children()
4466 .find(|n| n.is_element() && n.tag_name().name() == "Transforms")
4467 .unwrap();
4468 let transforms = parse_transforms(transforms_elem).unwrap();
4469 assert_eq!(transforms.len(), 2);
4470
4471 let initial =
4473 TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc).unwrap());
4474 let result = execute_transforms(sig_node, initial, &transforms).unwrap();
4475
4476 let output = String::from_utf8(result).unwrap();
4477
4478 assert!(!output.contains("Signature"), "Signature should be removed");
4480 assert!(
4481 !output.contains("SignedInfo"),
4482 "SignedInfo should be removed"
4483 );
4484 assert!(
4485 !output.contains("SignatureValue"),
4486 "SignatureValue should be removed"
4487 );
4488 assert!(
4489 !output.contains("fakesig"),
4490 "signature value should be removed"
4491 );
4492
4493 assert!(output.contains("samlp:Response"));
4495 assert!(output.contains("saml:Assertion"));
4496 assert!(output.contains("user@example.com"));
4497 }
4498}