1use crate::xml_input_shared::DecodeStorage as _;
23use std::cell::Cell;
24use std::collections::{BTreeMap, HashSet};
25
26mod parameters;
27mod relationship;
28mod source;
29pub(crate) use parameters::ParseStorage;
30pub(super) use parameters::{Diagnostic, current_diagnostic};
31pub(super) use parameters::{Expression as ExpressionParameter, Filter as FilterParameter};
32use parameters::{Instruction, View as InstructionView};
33pub(crate) use relationship::validate_edition as validate_relationship_chain;
34pub use relationship::{RELATIONSHIP_TRANSFORM_URI, RelationshipSelector};
35
36use crate::xml::dom::storage::names::NameStorage;
37use crate::xml::dom::storage::{Native as NativeDom, Storage as DomStorage};
38use crate::xml::dom::{Document, Node, NodeId, NodeType};
39use base64::{Engine as _, engine::general_purpose::STANDARD};
40use sha2::{Digest as _, Sha256};
41
42use super::parse::XMLDSIG_NS;
43use super::types::projection::{Keys, NativeKeys, PaidKeys, Projection};
44use super::types::{
45 NodeSetMaterializationBudget, TransformData, TransformError, transform_resource_limit,
46};
47use super::whitespace::is_xml_whitespace_only;
48use super::xpath::{
49 XPathDocumentRelation, XPathWorkBudget, is_xpath_whitespace,
50 xpath_may_read_mutable_character_data,
51};
52use crate::c14n::scratch::{Bytes as _, List as _, Native, Scratch};
53use crate::c14n::xml_base::XmlBaseResolutionBudget;
54use crate::c14n::{self, C14nAlgorithm};
55use crate::document::{
56 DocumentParseSettings, XmlDocumentError, XmlParseWorkBudget,
57 parse_borrowed_with_settings_and_budget,
58};
59#[cfg(test)]
60use crate::hard_limits::XML_DOCUMENT_NODE_CEILING;
61
62pub const ENVELOPED_SIGNATURE_URI: &str = "http://www.w3.org/2000/09/xmldsig#enveloped-signature";
64pub const BASE64_TRANSFORM_URI: &str = "http://www.w3.org/2000/09/xmldsig#base64";
66pub const XPATH_TRANSFORM_URI: &str = "http://www.w3.org/TR/1999/REC-xpath-19991116";
68pub const XPATH_FILTER2_TRANSFORM_URI: &str = "http://www.w3.org/2002/06/xmldsig-filter2";
70pub const DEFAULT_IMPLICIT_C14N_URI: &str = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
73pub const MAX_TRANSFORMS_PER_REFERENCE: usize = crate::hard_limits::REFERENCE_TRANSFORM_CEILING;
79pub(super) const ENVELOPED_SIGNATURE_XPATH_PREFIX: &str = "dsig";
82pub(super) const ENVELOPED_SIGNATURE_XPATH_EXPR: &str = "not(ancestor-or-self::dsig:Signature)";
83pub(super) const MAX_XPATH_EXPRESSION_BYTES: usize =
84 crate::hard_limits::XPATH_EXPRESSION_BYTE_CEILING;
85pub(super) const MAX_XPATH_FILTERS: usize = crate::hard_limits::XPATH_FILTER_COUNT_CEILING;
86pub(super) const MAX_XPATH_EXPRESSIONS_PER_SIGNATURE: usize =
91 crate::hard_limits::XPATH_EXPRESSION_COUNT_CEILING;
92const MAX_XPATH_NAMESPACE_BINDINGS: usize = crate::hard_limits::XPATH_NAMESPACE_BINDING_CEILING;
93const MAX_XPATH_NAMESPACE_BYTES: usize = crate::hard_limits::XPATH_NAMESPACE_BYTE_CEILING;
94const MAX_BASE64_TRANSFORM_INPUT_BYTES: usize =
95 crate::hard_limits::BASE64_TRANSFORM_INPUT_BYTE_CEILING;
96const MAX_BASE64_TRANSFORM_OUTPUT_BYTES: usize =
97 crate::hard_limits::BASE64_TRANSFORM_OUTPUT_BYTE_CEILING;
98const MAX_C14N_OUTPUT_BYTES: usize = crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING;
99const MAX_NODE_SET_FILTER_WORK: usize = crate::hard_limits::NODE_SET_FILTER_WORK_CEILING;
101
102const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
104
105#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
107pub enum XPathHereSemantics {
108 #[default]
111 Specification,
112 XmlSecLegacy,
117}
118
119#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
121pub(crate) struct TransformOptions<'r> {
122 xpath_here_semantics: XPathHereSemantics,
123 allow_internal_dtd: bool,
124 opc_relationship_edition: crate::policy::OpcRelationshipEdition,
125 id_attributes: &'r [crate::xml::IdAttributeRegistration],
126}
127
128pub(crate) struct TransformExecutionBudget {
129 xpath: XPathWorkBudget,
130 base64: Base64WorkBudget,
131 c14n: C14nOutputBudget,
132 node_filter: NodeFilterWorkBudget,
133 node_set_materialization: NodeSetMaterializationBudget,
134 xml_base_resolution: XmlBaseResolutionBudget,
135 xml_parse_work: XmlParseWorkBudget,
136 xml_parse_settings: DocumentParseSettings,
137 state: TransformChainState,
138 opc_workspace: relationship::WorkspaceBudget,
139}
140
141impl Default for TransformExecutionBudget {
142 fn default() -> Self {
143 Self::from_resources(&crate::policy::ResourcePolicy::default())
144 }
145}
146
147pub(super) struct NodeFilterWorkBudget {
148 remaining: Cell<usize>,
149 maximum: usize,
150}
151
152impl Default for NodeFilterWorkBudget {
153 fn default() -> Self {
154 Self {
155 remaining: Cell::new(MAX_NODE_SET_FILTER_WORK),
156 maximum: MAX_NODE_SET_FILTER_WORK,
157 }
158 }
159}
160
161impl NodeFilterWorkBudget {
162 pub(super) fn charge(&self, entries: usize) -> Result<(), TransformError> {
163 let consumed = self.maximum.saturating_sub(self.remaining.get());
164 if !charge_byte_budget(&self.remaining, entries) {
165 return Err(transform_resource_limit(
166 crate::policy::resource_name::NODE_SET_FILTER_WORK,
167 self.maximum,
168 consumed.saturating_add(entries),
169 ));
170 }
171 Ok(())
172 }
173}
174
175struct Base64WorkBudget {
176 remaining_input_bytes: Cell<usize>,
177 remaining_output_bytes: Cell<usize>,
178 max_input_bytes: usize,
179 max_output_bytes: usize,
180}
181
182struct C14nOutputBudget {
183 remaining: Cell<usize>,
184 max_bytes: usize,
185}
186
187fn charge_byte_budget(remaining: &Cell<usize>, bytes: usize) -> bool {
188 let Some(next) = remaining.get().checked_sub(bytes) else {
189 remaining.set(0);
190 return false;
191 };
192 remaining.set(next);
193 true
194}
195
196impl Default for C14nOutputBudget {
197 fn default() -> Self {
198 Self {
199 remaining: Cell::new(MAX_C14N_OUTPUT_BYTES),
200 max_bytes: MAX_C14N_OUTPUT_BYTES,
201 }
202 }
203}
204
205impl C14nOutputBudget {
206 fn with_limit(max_bytes: usize) -> Self {
207 Self {
208 remaining: Cell::new(max_bytes),
209 max_bytes,
210 }
211 }
212
213 fn remaining(&self) -> usize {
214 self.remaining.get()
215 }
216
217 fn charge(&self, bytes: usize) -> Result<(), crate::policy::PolicyViolation> {
218 let consumed = self.max_bytes.saturating_sub(self.remaining.get());
219 if !charge_byte_budget(&self.remaining, bytes) {
220 return Err(crate::policy::PolicyViolation::ResourceLimit {
221 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
222 maximum: self.max_bytes,
223 actual: consumed.saturating_add(bytes),
224 });
225 }
226 Ok(())
227 }
228
229 fn exhaust(&self) {
230 self.remaining.set(0);
231 }
232}
233
234#[cfg(test)]
235mod c14n_budget_regression_tests {
236 use super::*;
237 use crate::c14n::C14nMode;
238 use crate::xml::dom::Document;
239 use crate::xmldsig::types::NodeSet;
240
241 #[test]
242 fn bounded_c14n_failure_exhausts_the_shared_budget() {
243 let document = Document::parse("<root><payload>more than eight bytes</payload></root>")
244 .expect("test XML must parse");
245 let budget = TransformExecutionBudget::with_c14n_limit(8);
246
247 let error = execute_transforms_with_options_and_budget(
248 document.root_element(),
249 TransformData::NodeSet(
250 NodeSet::entire_document_without_comments(&document)
251 .expect("test document must fit the node-set ceiling"),
252 ),
253 &[Transform::C14n(C14nAlgorithm::new(
254 C14nMode::Inclusive1_0,
255 false,
256 ))],
257 TransformOptions::default(),
258 &budget,
259 )
260 .expect_err("canonicalized output must exceed the shared budget");
261
262 assert!(matches!(
263 error,
264 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
265 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
266 maximum: 8,
267 ..
268 })
269 ));
270 assert_eq!(
271 budget.remaining_c14n_output(),
272 0,
273 "a failed bounded render must not leave the same allowance reusable"
274 );
275 }
276}
277
278impl Default for Base64WorkBudget {
279 fn default() -> Self {
280 Self {
281 remaining_input_bytes: Cell::new(MAX_BASE64_TRANSFORM_INPUT_BYTES),
282 remaining_output_bytes: Cell::new(MAX_BASE64_TRANSFORM_OUTPUT_BYTES),
283 max_input_bytes: MAX_BASE64_TRANSFORM_INPUT_BYTES,
284 max_output_bytes: MAX_BASE64_TRANSFORM_OUTPUT_BYTES,
285 }
286 }
287}
288
289impl Base64WorkBudget {
290 fn charge_input(&self, bytes: usize) -> Result<(), TransformError> {
291 let consumed = self
292 .max_input_bytes
293 .saturating_sub(self.remaining_input_bytes.get());
294 if !charge_byte_budget(&self.remaining_input_bytes, bytes) {
295 return Err(transform_resource_limit(
296 crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
297 self.max_input_bytes,
298 consumed.saturating_add(bytes),
299 ));
300 }
301 Ok(())
302 }
303
304 fn ensure_output_capacity(&self, bytes: usize) -> Result<(), TransformError> {
305 let consumed = self
306 .max_output_bytes
307 .saturating_sub(self.remaining_output_bytes.get());
308 if bytes > self.remaining_output_bytes.get() {
309 return Err(transform_resource_limit(
310 crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
311 self.max_output_bytes,
312 consumed.saturating_add(bytes),
313 ));
314 }
315 Ok(())
316 }
317
318 fn charge_output(&self, bytes: usize) -> Result<(), TransformError> {
319 self.ensure_output_capacity(bytes)?;
320 let charged = charge_byte_budget(&self.remaining_output_bytes, bytes);
321 debug_assert!(charged, "preflighted Base64 output charge must fit");
322 Ok(())
323 }
324}
325
326#[cfg(test)]
327impl TransformExecutionBudget {
328 pub(crate) fn with_xpath_limit(limit: usize) -> Self {
329 Self {
330 xpath: XPathWorkBudget::with_limit(limit),
331 base64: Base64WorkBudget::default(),
332 c14n: C14nOutputBudget::default(),
333 node_filter: NodeFilterWorkBudget::default(),
334 node_set_materialization: NodeSetMaterializationBudget::default(),
335 xml_base_resolution: XmlBaseResolutionBudget::default(),
336 xml_parse_work: XmlParseWorkBudget::from_resources(
337 &crate::policy::ResourcePolicy::default(),
338 ),
339 xml_parse_settings: DocumentParseSettings::default(),
340 state: TransformChainState::default(),
341 opc_workspace: relationship::WorkspaceBudget::default(),
342 }
343 }
344
345 fn with_node_filter_limit(limit: usize) -> Self {
346 Self {
347 xpath: XPathWorkBudget::default(),
348 base64: Base64WorkBudget::default(),
349 c14n: C14nOutputBudget::default(),
350 node_filter: NodeFilterWorkBudget {
351 remaining: Cell::new(limit),
352 maximum: limit,
353 },
354 node_set_materialization: NodeSetMaterializationBudget::default(),
355 xml_base_resolution: XmlBaseResolutionBudget::default(),
356 xml_parse_work: XmlParseWorkBudget::from_resources(
357 &crate::policy::ResourcePolicy::default(),
358 ),
359 xml_parse_settings: DocumentParseSettings::default(),
360 state: TransformChainState::default(),
361 opc_workspace: relationship::WorkspaceBudget::default(),
362 }
363 }
364
365 pub(crate) fn with_node_set_materialization_limit(limit: usize) -> Self {
366 Self {
367 xpath: XPathWorkBudget::default(),
368 base64: Base64WorkBudget::default(),
369 c14n: C14nOutputBudget::default(),
370 node_filter: NodeFilterWorkBudget::default(),
371 node_set_materialization: NodeSetMaterializationBudget::with_limit(limit),
372 opc_workspace: relationship::WorkspaceBudget::default(),
373 xml_base_resolution: XmlBaseResolutionBudget::default(),
374 xml_parse_work: XmlParseWorkBudget::from_resources(
375 &crate::policy::ResourcePolicy::default(),
376 ),
377 xml_parse_settings: DocumentParseSettings::default(),
378 state: TransformChainState::default(),
379 }
380 }
381
382 pub(crate) fn with_c14n_limit(max_bytes: usize) -> Self {
383 Self {
384 c14n: C14nOutputBudget::with_limit(max_bytes),
385 ..Self::default()
386 }
387 }
388}
389
390impl TransformExecutionBudget {
391 pub(crate) fn from_resources(resources: &crate::policy::ResourcePolicy) -> Self {
392 Self {
393 xpath: XPathWorkBudget::with_limits(resources),
394 base64: Base64WorkBudget {
395 remaining_input_bytes: Cell::new(resources.max_base64_transform_input_bytes),
396 remaining_output_bytes: Cell::new(resources.max_base64_transform_output_bytes),
397 max_input_bytes: resources.max_base64_transform_input_bytes,
398 max_output_bytes: resources.max_base64_transform_output_bytes,
399 },
400 c14n: C14nOutputBudget::with_limit(resources.effective_canonicalized_bytes()),
401 node_filter: NodeFilterWorkBudget {
402 remaining: Cell::new(resources.max_node_set_filter_work),
403 maximum: resources.max_node_set_filter_work,
404 },
405 node_set_materialization: NodeSetMaterializationBudget::with_limits(
406 resources.max_node_set_entries,
407 resources.max_node_set_owned_string_bytes,
408 resources.max_node_set_cumulative_owned_string_bytes,
409 ),
410 xml_base_resolution: XmlBaseResolutionBudget::with_limits(
411 resources.effective_xml_base_components(),
412 resources.effective_xml_base_resolution_bytes(),
413 ),
414 xml_parse_work: XmlParseWorkBudget::from_resources(resources),
415 xml_parse_settings: DocumentParseSettings::for_transform_output(resources),
416 state: TransformChainState::default(),
417 opc_workspace: relationship::WorkspaceBudget::new(resources.max_opc_workspace_bytes),
418 }
419 }
420
421 pub(crate) fn with_xml_backend(mut self, backend: crate::XmlBackend) -> Self {
422 self.xml_parse_settings = self.xml_parse_settings.with_backend(backend);
423 self
424 }
425
426 pub(crate) fn charge_c14n_output(&self, bytes: usize) -> Result<(), TransformError> {
427 self.c14n.charge(bytes).map_err(TransformError::from)
428 }
429
430 pub(crate) fn charge_c14n_output_policy(
431 &self,
432 bytes: usize,
433 ) -> Result<(), crate::policy::PolicyViolation> {
434 self.c14n.charge(bytes)
435 }
436
437 pub(crate) fn remaining_c14n_output(&self) -> usize {
438 self.c14n.remaining()
439 }
440
441 pub(crate) fn c14n_output_limit(&self) -> usize {
442 self.c14n.max_bytes
443 }
444
445 pub(crate) fn node_set_materialization(&self) -> &NodeSetMaterializationBudget {
446 &self.node_set_materialization
447 }
448
449 pub(crate) fn xml_base_resolution(&self) -> &XmlBaseResolutionBudget {
450 &self.xml_base_resolution
451 }
452
453 pub(crate) fn xml_parse_work(&self) -> &XmlParseWorkBudget {
454 &self.xml_parse_work
455 }
456
457 pub(crate) fn charge_xpath_work(&self, work: usize) -> Result<(), TransformError> {
458 self.xpath.charge(work)
459 }
460
461 pub(crate) fn validate_xpath_context_evaluations(
462 &self,
463 actual: usize,
464 ) -> Result<(), TransformError> {
465 self.xpath.validate_context_evaluations(actual)
466 }
467
468 pub(crate) fn charge_node_filter_work(&self, nodes: usize) -> Result<(), TransformError> {
469 self.node_filter.charge(nodes)
470 }
471}
472
473impl<'r> TransformOptions<'r> {
474 pub(crate) fn id_attributes(
475 mut self,
476 registrations: &'r [crate::xml::IdAttributeRegistration],
477 ) -> Self {
478 self.id_attributes = registrations;
479 self
480 }
481 pub(crate) fn opc_relationship_edition(
482 mut self,
483 edition: crate::policy::OpcRelationshipEdition,
484 ) -> Self {
485 self.opc_relationship_edition = edition;
486 self
487 }
488 #[must_use]
490 pub(crate) fn xpath_here_semantics(mut self, semantics: XPathHereSemantics) -> Self {
491 self.xpath_here_semantics = semantics;
492 self
493 }
494
495 #[must_use]
498 pub(crate) fn allow_internal_dtd(mut self, enabled: bool) -> Self {
499 self.allow_internal_dtd = enabled;
500 self
501 }
502
503 pub(crate) fn here_semantics(self) -> XPathHereSemantics {
504 self.xpath_here_semantics
505 }
506
507 pub(crate) fn internal_dtd_allowed(self) -> bool {
508 self.allow_internal_dtd
509 }
510}
511
512#[derive(Debug, Clone, Copy, PartialEq, Eq)]
513pub(crate) struct XPathHereNodes {
514 specification_xpath_element: NodeId,
515 xmlsec_legacy_transform_element: NodeId,
516 document: XPathDocumentIdentity,
517}
518
519#[derive(Debug, Clone, Copy, PartialEq, Eq)]
520pub(crate) struct XPathDocumentIdentity([u8; 32]);
521
522impl XPathDocumentIdentity {
523 fn from_document<D: DomStorage>(document: &Document<'_, D>) -> Self {
524 #[cfg(test)]
525 XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(|count| count.set(count.get() + 1));
526 Self(Sha256::digest(document.input_text().as_bytes()).into())
527 }
528}
529
530#[cfg(test)]
531thread_local! {
532 static XPATH_DOCUMENT_IDENTITY_COMPUTATIONS: Cell<usize> = const { Cell::new(0) };
533}
534
535#[derive(Default)]
536struct TransformChainState {
537 xpath_document_identity: Cell<Option<CachedXPathDocumentIdentity>>,
538}
539
540#[derive(Clone, Copy)]
541struct CachedXPathDocumentIdentity {
542 document: *const (),
543 identity: XPathDocumentIdentity,
544}
545
546impl TransformChainState {
547 fn begin_chain(&self) {
548 self.xpath_document_identity.set(None);
552 }
553
554 fn xpath_document_identity<D: DomStorage>(
555 &self,
556 document: &Document<'_, D>,
557 ) -> XPathDocumentIdentity {
558 let document_key = std::ptr::from_ref(document).cast::<()>();
559 if let Some(cached) = self.xpath_document_identity.get()
560 && cached.document == document_key
561 {
562 return cached.identity;
563 }
564 let identity = XPathDocumentIdentity::from_document(document);
565 self.xpath_document_identity
566 .set(Some(CachedXPathDocumentIdentity {
567 document: document_key,
568 identity,
569 }));
570 identity
571 }
572
573 fn document_reparsed(&self) {
574 self.xpath_document_identity.set(None);
575 }
576}
577
578struct TransformExecutionContext<'a, S: Scratch = Native> {
579 scratch: &'a S,
580 options: TransformOptions<'a>,
581 budget: &'a TransformExecutionBudget,
582 state: &'a TransformChainState,
583 xml_parse: &'a XmlParseWorkBudget,
584 source_has_signature: bool,
587}
588
589#[derive(Debug, Clone, PartialEq, Eq)]
591pub struct XPathExpression {
592 expression: String,
593 namespaces: BTreeMap<String, String>,
594 here_nodes: Option<XPathHereNodes>,
595}
596
597impl XPathExpression {
598 pub fn new(expression: impl Into<String>) -> Self {
600 Self {
601 expression: expression.into(),
602 namespaces: BTreeMap::new(),
603 here_nodes: None,
604 }
605 }
606
607 pub fn with_namespace(mut self, prefix: impl Into<String>, uri: impl Into<String>) -> Self {
609 self.namespaces.insert(prefix.into(), uri.into());
610 self
611 }
612
613 pub fn expression(&self) -> &str {
615 &self.expression
616 }
617
618 pub fn namespaces(&self) -> &BTreeMap<String, String> {
620 &self.namespaces
621 }
622
623 pub(crate) fn here_context_node(&self, semantics: XPathHereSemantics) -> Option<NodeId> {
624 self.here_nodes.map(|nodes| match semantics {
625 XPathHereSemantics::Specification => nodes.specification_xpath_element,
626 XPathHereSemantics::XmlSecLegacy => nodes.xmlsec_legacy_transform_element,
627 })
628 }
629
630 fn parsed_document_identity(&self) -> Option<XPathDocumentIdentity> {
631 self.here_nodes.map(|nodes| nodes.document)
632 }
633}
634
635#[derive(Debug, Clone, Copy, PartialEq, Eq)]
637pub enum XPathFilterOperation {
638 Intersect,
640 Subtract,
642 Union,
644}
645
646impl XPathFilterOperation {
647 pub(crate) fn as_str(self) -> &'static str {
648 match self {
649 Self::Intersect => "intersect",
650 Self::Subtract => "subtract",
651 Self::Union => "union",
652 }
653 }
654}
655
656#[derive(Debug, Clone, PartialEq, Eq)]
658pub struct XPathFilter {
659 operation: XPathFilterOperation,
660 xpath: XPathExpression,
661}
662
663impl XPathFilter {
664 pub fn new(operation: XPathFilterOperation, xpath: XPathExpression) -> Self {
666 Self { operation, xpath }
667 }
668
669 pub fn operation(&self) -> XPathFilterOperation {
671 self.operation
672 }
673
674 pub fn xpath(&self) -> &XPathExpression {
676 &self.xpath
677 }
678}
679
680#[derive(Debug, Clone, PartialEq, Eq)]
682pub enum Transform {
683 Enveloped,
688
689 XpathExcludeAllSignatures,
695
696 XPath(XPathExpression),
698
699 XPathFilter2(Vec<XPathFilter>),
701
702 C14n(C14nAlgorithm),
706
707 Base64Decode,
715
716 Relationship(Vec<RelationshipSelector>),
723}
724
725impl Transform {
726 pub(crate) fn algorithm_uri(&self) -> &'static str {
727 Instruction::algorithm_uri(self)
728 }
729}
730
731#[cfg(test)]
739pub(crate) fn apply_transform<'a>(
740 signature_node: Node<'a, 'a>,
741 transform: &Transform,
742 input: TransformData<'a>,
743) -> Result<TransformData<'a>, TransformError> {
744 let budget = TransformExecutionBudget::default();
745 let state = TransformChainState::default();
746 apply_transform_with_options_and_state(
747 signature_node,
748 transform,
749 input,
750 TransformOptions::default(),
751 &budget,
752 &state,
753 &budget.xml_parse_work,
754 )
755}
756
757#[cfg(test)]
758pub(super) fn apply_transform_with_options<'s, 'd>(
759 signature_node: Node<'s, 's>,
760 transform: &Transform,
761 input: TransformData<'d>,
762 options: TransformOptions<'_>,
763 budget: &TransformExecutionBudget,
764) -> Result<TransformData<'d>, TransformError> {
765 let state = TransformChainState::default();
766 apply_transform_with_options_and_state(
767 signature_node,
768 transform,
769 input,
770 options,
771 budget,
772 &state,
773 &budget.xml_parse_work,
774 )
775}
776
777#[cfg(test)]
778fn apply_transform_with_options_and_state<'s, 'd>(
779 signature_node: Node<'s, 's>,
780 transform: &Transform,
781 input: TransformData<'d>,
782 options: TransformOptions<'_>,
783 budget: &TransformExecutionBudget,
784 state: &TransformChainState,
785 xml_parse: &XmlParseWorkBudget,
786) -> Result<TransformData<'d>, TransformError> {
787 let context = TransformExecutionContext {
788 scratch: &Native,
789 options,
790 budget,
791 state,
792 xml_parse,
793 source_has_signature: true,
794 };
795 apply_transform_with_context(signature_node, transform, input.into(), &context)
796 .map(ExecutionData::into_native)
797}
798
799fn apply_transform_with_context<
800 's,
801 'd,
802 S: TransformScratch,
803 D: DomStorage,
804 G: DomStorage,
805 I: Instruction,
806>(
807 signature_node: Node<'s, 's, G>,
808 transform: &I,
809 input: ExecutionData<'d, S, D>,
810 context: &TransformExecutionContext<'_, S>,
811) -> Result<ExecutionData<'d, S, D>, TransformError> {
812 let options = context.options;
813 let budget = context.budget;
814 let state = context.state;
815 let xml_parse = context.xml_parse;
816 match transform.view() {
817 InstructionView::Relationship(selectors) => {
818 let collected;
819 let bytes = if let Some(bytes) = input.binary() {
820 bytes
821 } else {
822 let nodes = input.nodes().expect("nodes or bytes");
823 let algorithm =
824 C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).expect("constant URI");
825 let (bytes, _) = c14n::canonicalize_buffer_in(
826 nodes.document(),
827 nodes,
828 (&algorithm, None),
829 None,
830 budget.c14n.remaining(),
831 budget.xml_base_resolution(),
832 context.scratch,
833 )
834 .map_err(|error| map_c14n_limit_error(error, &budget.c14n))?;
835 budget.c14n.charge(bytes.len())?;
836 collected = bytes;
837 &collected
838 };
839 xml_parse.charge_policy(bytes.len())?;
840 let decoder = context.scratch.decoder();
844 let xml = crate::xml_input_shared::decode_xml_in(
845 bytes,
846 budget.xml_parse_settings.max_bytes,
847 |physical, declaration| {
848 if !physical.eq_ignore_ascii_case("UTF-8")
849 && !physical.eq_ignore_ascii_case("UTF-16LE")
850 && !physical.eq_ignore_ascii_case("UTF-16BE")
851 {
852 return Err(decoder.label_error(
853 crate::xml_input_shared::LabelError::Forbidden,
854 physical,
855 ));
856 }
857 if let Some(label) = declaration
858 && !label.eq_ignore_ascii_case("UTF-8")
859 && !label.eq_ignore_ascii_case("UTF-16")
860 {
861 return Err(decoder
862 .label_error(crate::xml_input_shared::LabelError::Forbidden, label));
863 }
864 Ok(())
865 },
866 &decoder,
867 )
868 .map_err(S::decode_error)?;
869 let settings = DocumentParseSettings {
870 allow_dtd: false,
871 ..budget.xml_parse_settings
872 };
873 let document = context
874 .scratch
875 .parse_xml(xml.as_ref(), settings, xml_parse)?;
876 state.document_reparsed();
877 relationship::normalize_in(
878 &document,
879 selectors,
880 options.opc_relationship_edition,
881 budget,
882 context.scratch,
883 )
884 .map(ExecutionData::Accounted)
885 }
886 InstructionView::Enveloped => {
887 let mut nodes = input.into_nodes()?;
888 if !std::ptr::eq(
897 std::ptr::from_ref(signature_node.document()).cast::<()>(),
898 std::ptr::from_ref(nodes.document()).cast::<()>(),
899 ) {
900 return Err(TransformError::CrossDocumentSignatureNode);
901 }
902 budget.node_filter.charge(nodes.len())?;
903 let signature = nodes
904 .document()
905 .get_node(signature_node.id())
906 .ok_or(TransformError::CrossDocumentSignatureNode)?;
907 nodes.exclude_subtree(signature)?;
908 Ok(ExecutionData::Nodes(nodes))
909 }
910 InstructionView::ExcludeSignatures => {
911 let mut nodes = input.into_nodes()?;
912 let doc = nodes.document();
913
914 budget.xpath.validate_context_evaluations(nodes.len())?;
919 let source_nodes = doc.descendants().len();
920 budget.xpath.charge(source_nodes)?;
921 context.scratch.charge(source_nodes)?;
922
923 for node in doc.descendants().filter(|node| {
924 node.is_element()
925 && node.tag_name().name() == "Signature"
926 && node.tag_name().namespace() == Some(XMLDSIG_NS)
927 }) {
928 budget.node_filter.charge(nodes.len())?;
929 nodes.exclude_subtree(node)?;
930 }
931
932 Ok(ExecutionData::Nodes(nodes))
933 }
934 InstructionView::XPath(xpath) => {
935 let nodes = input.into_nodes()?;
936 let document_relation = xpath_document_relation(
937 signature_node.document(),
938 nodes.document(),
939 std::iter::once(xpath),
940 state,
941 );
942 Ok(ExecutionData::Nodes(
943 super::xpath::apply_xpath_projection_with_budget(
944 nodes,
945 xpath,
946 super::xpath::XPathRequestContext::new(
947 options.here_semantics(),
948 options.id_attributes,
949 ),
950 document_relation,
951 &budget.xpath,
952 &budget.node_filter,
953 &budget.node_set_materialization,
954 )?,
955 ))
956 }
957 InstructionView::Filter2(filters) => {
958 let nodes = input.into_nodes()?;
959 let document_relation = xpath_document_relation(
960 signature_node.document(),
961 nodes.document(),
962 filters.iter().map(FilterParameter::xpath),
963 state,
964 );
965 Ok(ExecutionData::Nodes(
966 super::xpath::apply_xpath_projection2_with_budget(
967 nodes,
968 filters,
969 super::xpath::XPathRequestContext::new(
970 options.here_semantics(),
971 options.id_attributes,
972 ),
973 document_relation,
974 &budget.xpath,
975 &budget.node_filter,
976 &budget.node_set_materialization,
977 )?,
978 ))
979 }
980 InstructionView::C14n(algo, prefixes) => {
981 let nodes = input.into_nodes()?;
982 let (output, _) = c14n::canonicalize_buffer_in(
983 nodes.document(),
984 &nodes,
985 (algo, prefixes),
986 None,
987 budget.c14n.remaining(),
988 budget.xml_base_resolution(),
989 context.scratch,
990 )
991 .map_err(|error| map_c14n_limit_error(error, &budget.c14n))?;
992 budget.c14n.charge(output.len())?;
993 Ok(ExecutionData::Accounted(output))
994 }
995 InstructionView::Base64 => {
996 base64_projection(input.binary(), input.nodes(), budget, context.scratch)
997 .map(ExecutionData::Accounted)
998 }
999 }
1000}
1001
1002fn xpath_document_relation<'a, L: DomStorage, R: DomStorage, X: ExpressionParameter + 'a>(
1003 signature_document: &Document<'_, L>,
1004 input_document: &Document<'_, R>,
1005 expressions: impl IntoIterator<Item = &'a X>,
1006 state: &TransformChainState,
1007) -> XPathDocumentRelation {
1008 if matches!(
1009 XPathDocumentRelation::between(signature_document, input_document),
1010 XPathDocumentRelation::CrossDocument
1011 ) {
1012 return XPathDocumentRelation::CrossDocument;
1013 }
1014
1015 let mut parsed_identities = expressions
1016 .into_iter()
1017 .filter_map(ExpressionParameter::parsed_document_identity);
1018 let Some(first) = parsed_identities.next() else {
1019 return XPathDocumentRelation::SameDocument;
1020 };
1021 let input_identity = state.xpath_document_identity(input_document);
1022 if first == input_identity && parsed_identities.all(|identity| identity == input_identity) {
1023 XPathDocumentRelation::SameDocument
1024 } else {
1025 XPathDocumentRelation::CrossDocument
1026 }
1027}
1028
1029fn append_normalized_base64<S: Scratch>(
1035 encoded: &[u8],
1036 normalized: &mut S::Bytes,
1037 budget: &Base64WorkBudget,
1038 scratch: &S,
1039) -> Result<(), TransformError> {
1040 budget.charge_input(encoded.len())?;
1041 scratch.charge(encoded.len())?;
1042 let additional = encoded
1043 .iter()
1044 .filter(|byte| is_rfc2045_base64_byte(**byte))
1045 .count();
1046 normalized.reserve(additional)?;
1047 scratch.charge(encoded.len())?;
1048 normalized.append_filtered(encoded, is_rfc2045_base64_byte)?;
1049 Ok(())
1050}
1051
1052fn is_rfc2045_base64_byte(byte: u8) -> bool {
1053 byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/' | b'=')
1054}
1055
1056fn decode_base64_transform<S: Scratch>(
1057 normalized: &[u8],
1058 budget: &Base64WorkBudget,
1059 scratch: &S,
1060) -> Result<S::Bytes, TransformError> {
1061 scratch.charge(normalized.len())?;
1062 let padding = normalized
1063 .iter()
1064 .rev()
1065 .take_while(|byte| **byte == b'=')
1066 .count();
1067 let decoded_len = base64::decoded_len_estimate(normalized.len()).saturating_sub(padding);
1068 budget.ensure_output_capacity(decoded_len)?;
1069
1070 let mut decoded = scratch.bytes();
1071 decoded.zeros(decoded_len)?;
1072 scratch.charge(normalized.len())?;
1073 let written = STANDARD
1074 .decode_slice(normalized, &mut decoded)
1075 .map_err(TransformError::Base64)?;
1076 budget.charge_output(written)?;
1077 decoded.truncate(written);
1078 Ok(decoded)
1079}
1080
1081fn base64_projection<S: Scratch, K: super::types::projection::Keys, D: DomStorage>(
1082 binary: Option<&[u8]>,
1083 nodes: Option<&Projection<'_, K, D>>,
1084 budget: &TransformExecutionBudget,
1085 scratch: &S,
1086) -> Result<S::Bytes, TransformError> {
1087 let mut normalized = scratch.bytes();
1088 match (binary, nodes) {
1089 (Some(bytes), _) => {
1090 append_normalized_base64(bytes, &mut normalized, &budget.base64, scratch)?
1091 }
1092 (None, Some(nodes)) => {
1093 for node in nodes.document().descendants() {
1094 scratch.charge(1)?;
1095 if nodes.contains_tree(node)? && node.is_text() {
1096 append_normalized_base64(
1097 node.text().unwrap_or_default().as_bytes(),
1098 &mut normalized,
1099 &budget.base64,
1100 scratch,
1101 )?;
1102 }
1103 }
1104 }
1105 (None, None) => unreachable!("execution data has either nodes or bytes"),
1106 }
1107 decode_base64_transform(&normalized, &budget.base64, scratch)
1108}
1109
1110pub fn execute_transforms<'a>(
1119 signature_node: Node<'a, 'a>,
1120 initial_data: TransformData<'a>,
1121 transforms: &[Transform],
1122) -> Result<Vec<u8>, TransformError> {
1123 execute_transforms_with_policy(
1124 signature_node,
1125 initial_data,
1126 transforms,
1127 &crate::policy::VerificationPolicy::default(),
1128 )
1129}
1130
1131pub fn execute_transforms_with_policy<'a>(
1136 signature_node: Node<'a, 'a>,
1137 initial_data: TransformData<'a>,
1138 transforms: &[Transform],
1139 policy: &crate::policy::VerificationPolicy,
1140) -> Result<Vec<u8>, TransformError> {
1141 policy.validate()?;
1142 if transforms.len() > policy.resources.max_transforms_per_reference {
1143 return Err(transform_resource_limit(
1144 crate::policy::resource_name::REFERENCE_TRANSFORMS,
1145 policy.resources.max_transforms_per_reference,
1146 transforms.len(),
1147 ));
1148 }
1149 let parameters =
1150 relationship::WorkspaceBudget::parameters(policy.resources.max_opc_parameter_bytes);
1151 for transform in transforms {
1152 if let Transform::Relationship(selectors) = transform {
1153 relationship::validate_owned_selectors(selectors, ¶meters)?;
1154 }
1155 }
1156 validate_transform_policy(
1157 matches!(
1158 &initial_data,
1159 TransformData::Binary(_) | TransformData::BorrowedBinary(_)
1160 ),
1161 transforms,
1162 policy.transforms.allowed_algorithms.as_ref(),
1163 "transform execution",
1164 )?;
1165 let options = TransformOptions::default()
1166 .opc_relationship_edition(policy.transforms.opc_relationship_edition)
1167 .xpath_here_semantics(policy.transforms.xpath_here_semantics)
1168 .allow_internal_dtd(policy.xml.allow_internal_dtd);
1169 let budget = TransformExecutionBudget::from_resources(&policy.resources);
1170 execute_transforms_with_options_and_budget(
1171 signature_node,
1172 initial_data,
1173 transforms,
1174 options,
1175 &budget,
1176 )
1177}
1178
1179pub(crate) fn execute_transforms_with_options_and_budget<'a>(
1180 signature_node: Node<'a, 'a>,
1181 initial_data: TransformData<'a>,
1182 transforms: &[Transform],
1183 options: TransformOptions<'_>,
1184 budget: &TransformExecutionBudget,
1185) -> Result<Vec<u8>, TransformError> {
1186 execute_reference_transforms_with_budget(
1187 signature_node,
1188 Some(signature_node),
1189 initial_data,
1190 transforms,
1191 options,
1192 budget,
1193 budget.xml_parse_work(),
1194 )
1195}
1196
1197pub(crate) fn execute_reference_transforms_with_budget<'a>(
1200 source_node: Node<'a, 'a>,
1201 enveloped_signature: Option<Node<'a, 'a>>,
1202 initial_data: TransformData<'a>,
1203 transforms: &[Transform],
1204 options: TransformOptions<'_>,
1205 budget: &TransformExecutionBudget,
1206 xml_parse: &XmlParseWorkBudget,
1207) -> Result<Vec<u8>, TransformError> {
1208 ensure_transform_count(transforms.len())?;
1209 relationship::validate_edition(transforms, options.opc_relationship_edition)?;
1210 budget.state.begin_chain();
1211 let context = TransformExecutionContext {
1212 scratch: &Native,
1213 options,
1214 budget,
1215 state: &budget.state,
1216 xml_parse,
1217 source_has_signature: enveloped_signature.is_some(),
1218 };
1219 execute_transform_chain::<false, _, _, _, _, _, _>(
1220 source_node,
1221 enveloped_signature,
1222 initial_data.into(),
1223 transforms,
1224 None,
1225 None,
1226 &context,
1227 CollectTerminal,
1228 )
1229 .map(|output| output.output)
1230}
1231
1232pub(crate) struct ReplayExecution<'a, Q: structured_asn1::memory::Work> {
1235 pub(crate) options: TransformOptions<'a>,
1236 pub(crate) budget: &'a TransformExecutionBudget,
1237 pub(crate) work: &'a Q,
1238}
1239
1240pub(crate) fn replay_reference_transforms<
1241 'a,
1242 'w,
1243 E,
1244 F,
1245 Q: structured_asn1::memory::Work,
1246 I: Instruction,
1247>(
1248 source: Node<'a, 'a>,
1249 signature: Option<Node<'a, 'a>>,
1250 initial: super::uri::ReferenceInput<'a>,
1251 transforms: &[I],
1252 execution: ReplayExecution<'w, Q>,
1253 terminal: F,
1254) -> Result<(), E>
1255where
1256 E: From<TransformError>,
1257 F: TransformTerminal<crate::c14n::scratch::Accounted<'w, Q>, Output = (), Error = E>,
1258{
1259 let ReplayExecution {
1260 options,
1261 budget,
1262 work,
1263 } = execution;
1264 ensure_transform_count(transforms.len())?;
1265 relationship::validate_edition(transforms, options.opc_relationship_edition)?;
1266 budget.state.begin_chain();
1267 let scratch = crate::c14n::scratch::Accounted(work);
1268 let initial = match initial {
1269 super::uri::ReferenceInput::Xml {
1270 root,
1271 with_comments,
1272 } => ExecutionData::Nodes(Projection::subtree(
1273 root,
1274 with_comments,
1275 &budget.node_set_materialization,
1276 scratch.node_keys(),
1277 )?),
1278 super::uri::ReferenceInput::Binary(bytes) => ExecutionData::BorrowedBinary(bytes),
1279 };
1280 let context = TransformExecutionContext {
1281 scratch: &scratch,
1282 options,
1283 budget,
1284 state: &budget.state,
1285 xml_parse: budget.xml_parse_work(),
1286 source_has_signature: signature.is_some(),
1287 };
1288 execute_transform_chain::<true, _, _, _, _, _, _>(
1289 source, signature, initial, transforms, None, None, &context, terminal,
1290 )
1291 .map(|_| ())
1292}
1293
1294pub(crate) fn replay_output_limit(budget: &TransformExecutionBudget) -> usize {
1295 budget.c14n.remaining()
1296}
1297
1298pub(crate) fn charge_replay_output(
1299 budget: &TransformExecutionBudget,
1300 bytes: usize,
1301) -> Result<(), TransformError> {
1302 budget.c14n.charge(bytes).map_err(Into::into)
1303}
1304
1305pub(crate) fn replay_c14n_error(
1306 error: c14n::C14nError,
1307 budget: &TransformExecutionBudget,
1308) -> TransformError {
1309 map_c14n_limit_error(error, &budget.c14n)
1310}
1311
1312pub(crate) fn enveloped_signature_for_reference<'a, E, I: Instruction>(
1313 source: Node<'a, 'a>,
1314 transforms: &[I],
1315 mut admit: impl FnMut() -> Result<(), E>,
1316) -> Result<Option<Node<'a, 'a>>, E> {
1317 let mut needed = false;
1318 for transform in transforms {
1319 admit()?;
1320 if matches!(transform.view(), InstructionView::Enveloped) {
1321 needed = true;
1322 break;
1323 }
1324 }
1325 if !needed {
1326 return Ok(None);
1327 }
1328 for ancestor in source.ancestors() {
1329 admit()?;
1330 if ancestor.has_tag_name((XMLDSIG_NS, "Signature")) {
1331 return Ok(Some(ancestor));
1332 }
1333 }
1334 Ok(None)
1335}
1336
1337pub(crate) struct TransformDependencyOutput {
1339 pub(crate) dependencies: HashSet<usize>,
1340}
1341
1342struct TransformChainOutput<T> {
1343 output: T,
1344 dependencies: HashSet<usize>,
1345}
1346
1347fn retain_visible_dependencies<K: super::types::projection::Keys, D: DomStorage>(
1348 nodes: &Projection<'_, K, D>,
1349 tracking: &mut DependencyTracking,
1350) -> Result<(), TransformError> {
1351 let mut failure = None;
1352 tracking.active_nodes.retain(|tracked| {
1353 if failure.is_some() {
1354 return true;
1355 }
1356 let Some(node) = nodes.document().get_node(tracked.node_id) else {
1357 return false;
1358 };
1359 match nodes.contains_tree(node) {
1360 Ok(visible) => visible,
1361 Err(error) => {
1362 failure = Some(error);
1363 true
1364 }
1365 }
1366 });
1367 failure.map_or(Ok(()), |error| Err(error.into()))
1368}
1369
1370pub(crate) trait TransformScratch: Scratch {
1371 type NodeKeys: Keys;
1372 type Decoder: crate::xml_input_shared::DecodeStorage;
1373 type Dom: NameStorage;
1374 fn parse_xml<'input>(
1375 &self,
1376 xml: &'input str,
1377 settings: DocumentParseSettings,
1378 budget: &XmlParseWorkBudget,
1379 ) -> Result<Document<'input, Self::Dom>, TransformError>;
1380 fn node_keys(&self) -> Self::NodeKeys;
1381 fn decoder(&self) -> Self::Decoder;
1382 fn decode_error(
1383 error: <Self::Decoder as crate::xml_input_shared::DecodeStorage>::Error,
1384 ) -> TransformError;
1385}
1386
1387impl TransformScratch for Native {
1388 type NodeKeys = NativeKeys;
1389 type Decoder = crate::xml_input_shared::NativeStorage;
1390 type Dom = NativeDom;
1391 fn parse_xml<'input>(
1392 &self,
1393 xml: &'input str,
1394 settings: DocumentParseSettings,
1395 budget: &XmlParseWorkBudget,
1396 ) -> Result<Document<'input>, TransformError> {
1397 parse_borrowed_with_settings_and_budget(xml, settings, Some(budget))
1398 .map_err(|error| map_transform_xml_parse_error(error, settings))
1399 }
1400 fn decoder(&self) -> Self::Decoder {
1401 crate::xml_input_shared::NativeStorage
1402 }
1403 fn decode_error(error: crate::encoding::XmlEncodingError) -> TransformError {
1404 map_transform_xml_decode_error(error)
1405 }
1406 fn node_keys(&self) -> NativeKeys {
1407 NativeKeys
1408 }
1409}
1410
1411impl<'w, W: structured_asn1::memory::Work> TransformScratch
1412 for crate::c14n::scratch::Accounted<'w, W>
1413{
1414 type NodeKeys = PaidKeys<'w, W>;
1415 type Decoder = crate::encoding::AccountedDecoder<'w, W>;
1416 type Dom = crate::xml::dom::storage::Accounted<'w, W>;
1417 fn parse_xml<'input>(
1418 &self,
1419 xml: &'input str,
1420 settings: DocumentParseSettings,
1421 budget: &XmlParseWorkBudget,
1422 ) -> Result<Document<'input, Self::Dom>, TransformError> {
1423 crate::document::parse_borrowed_in(
1424 xml,
1425 settings,
1426 Some(budget),
1427 crate::xml::dom::storage::Accounted(self.0),
1428 )
1429 .map_err(|error| map_transform_xml_parse_error(error, settings))
1430 }
1431 fn decoder(&self) -> Self::Decoder {
1432 crate::encoding::AccountedDecoder(self.0)
1433 }
1434 fn decode_error(error: crate::encoding::DecodeError) -> TransformError {
1435 match error {
1436 crate::encoding::DecodeError::Encoding(error) => map_transform_xml_decode_error(error),
1437 crate::encoding::DecodeError::Label(error) => {
1438 TransformError::AccountedXmlEncoding(error)
1439 }
1440 crate::encoding::DecodeError::Execution(error) => error.into(),
1441 }
1442 }
1443 fn node_keys(&self) -> Self::NodeKeys {
1444 PaidKeys::new(self.0)
1445 }
1446}
1447
1448pub(crate) enum ExecutionData<'a, S: TransformScratch, D: DomStorage = NativeDom> {
1449 Nodes(Projection<'a, S::NodeKeys, D>),
1450 NativeBinary(Vec<u8>),
1451 BorrowedBinary(&'a [u8]),
1452 Accounted(S::Bytes),
1453}
1454
1455impl<'a> From<TransformData<'a>> for ExecutionData<'a, Native> {
1456 fn from(value: TransformData<'a>) -> Self {
1457 match value {
1458 TransformData::NodeSet(nodes) => Self::Nodes(nodes.projection),
1459 TransformData::Binary(bytes) => Self::NativeBinary(bytes),
1460 TransformData::BorrowedBinary(bytes) => Self::BorrowedBinary(bytes),
1461 }
1462 }
1463}
1464
1465impl<'a, S: TransformScratch, D: DomStorage> ExecutionData<'a, S, D> {
1466 pub(crate) fn nodes(&self) -> Option<&Projection<'a, S::NodeKeys, D>> {
1467 match self {
1468 Self::Nodes(nodes) => Some(nodes),
1469 _ => None,
1470 }
1471 }
1472 pub(crate) fn binary(&self) -> Option<&[u8]> {
1473 match self {
1474 Self::NativeBinary(bytes) => Some(bytes),
1475 Self::BorrowedBinary(bytes) => Some(bytes),
1476 Self::Accounted(bytes) => Some(bytes),
1477 Self::Nodes(_) => None,
1478 }
1479 }
1480 fn into_nodes(self) -> Result<Projection<'a, S::NodeKeys, D>, TransformError> {
1481 match self {
1482 Self::Nodes(nodes) => Ok(nodes),
1483 _ => Err(TransformError::TypeMismatch {
1484 expected: "NodeSet",
1485 got: "Binary",
1486 }),
1487 }
1488 }
1489}
1490
1491pub(crate) trait TransformTerminal<S: TransformScratch> {
1492 type Output;
1493 type Error: From<TransformError>;
1494 fn finish<D: DomStorage>(
1495 self,
1496 data: ExecutionData<'_, S, D>,
1497 explicit: Option<(&C14nAlgorithm, Option<&str>)>,
1498 budget: &TransformExecutionBudget,
1499 ) -> Result<Self::Output, Self::Error>;
1500}
1501
1502struct CollectTerminal;
1503impl TransformTerminal<Native> for CollectTerminal {
1504 type Output = Vec<u8>;
1505 type Error = TransformError;
1506 fn finish<D: DomStorage>(
1507 self,
1508 data: ExecutionData<'_, Native, D>,
1509 _: Option<(&C14nAlgorithm, Option<&str>)>,
1510 budget: &TransformExecutionBudget,
1511 ) -> Result<Vec<u8>, TransformError> {
1512 match data {
1513 ExecutionData::NativeBinary(bytes) | ExecutionData::Accounted(bytes) => Ok(bytes),
1514 ExecutionData::BorrowedBinary(bytes) => Ok(bytes.to_vec()),
1515 ExecutionData::Nodes(nodes) => {
1516 let algorithm =
1517 C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI).expect("constant C14N URI");
1518 let (output, _) = c14n::canonicalize_buffer_in(
1519 nodes.document(),
1520 &nodes,
1521 (&algorithm, None),
1522 None,
1523 budget.c14n.remaining(),
1524 budget.xml_base_resolution(),
1525 &Native,
1526 )
1527 .map_err(|error| map_c14n_limit_error(error, &budget.c14n))?;
1528 budget.c14n.charge(output.len())?;
1529 Ok(output)
1530 }
1531 }
1532 }
1533}
1534
1535#[cfg(test)]
1536impl<'a> ExecutionData<'a, Native> {
1537 fn into_native(self) -> TransformData<'a> {
1538 match self {
1539 Self::Nodes(projection) => TransformData::NodeSet(super::types::NodeSet { projection }),
1540 Self::NativeBinary(bytes) | Self::Accounted(bytes) => TransformData::Binary(bytes),
1541 Self::BorrowedBinary(bytes) => TransformData::BorrowedBinary(bytes),
1542 }
1543 }
1544}
1545
1546struct DependencyTracking {
1547 active_nodes: Vec<TrackedDependencyNode>,
1550 dormant_indexes: HashSet<usize>,
1555 opaque_dependencies: HashSet<usize>,
1558 canonical_positions: Option<Vec<CanonicalDependencyPosition>>,
1559}
1560
1561struct TrackedDependencyNode {
1562 index: usize,
1563 node_id: NodeId,
1564 node_type: NodeType,
1565}
1566
1567struct CanonicalDependencyPosition {
1568 index: usize,
1569 position: usize,
1570 node_type: NodeType,
1571}
1572
1573pub(crate) fn execute_transforms_with_dependency_nodes<'a>(
1579 signature_node: Node<'a, 'a>,
1580 initial_data: TransformData<'a>,
1581 transforms: &[Transform],
1582 options: TransformOptions<'_>,
1583 budget: &TransformExecutionBudget,
1584 tracked_nodes: Vec<(usize, NodeId)>,
1585) -> Result<TransformDependencyOutput, TransformError> {
1586 ensure_transform_count(transforms.len())?;
1587 relationship::validate_edition(transforms, options.opc_relationship_edition)?;
1588 budget.state.begin_chain();
1589 let mut active_nodes = Vec::with_capacity(tracked_nodes.len());
1590 let mut opaque_dependencies = HashSet::new();
1591 let mut dormant_indexes = HashSet::new();
1592 for (index, node_id) in tracked_nodes {
1593 if let Some(node) = signature_node.document().get_node(node_id) {
1594 let belongs_to_input = match &initial_data {
1595 TransformData::NodeSet(nodes) => nodes.contains(node),
1596 TransformData::Binary(_) | TransformData::BorrowedBinary(_) => false,
1597 };
1598 if belongs_to_input {
1599 active_nodes.push(TrackedDependencyNode {
1600 index,
1601 node_id,
1602 node_type: node.node_type(),
1603 });
1604 } else {
1605 dormant_indexes.insert(index);
1606 }
1607 } else {
1608 opaque_dependencies.insert(index);
1609 }
1610 }
1611 let context = TransformExecutionContext {
1612 scratch: &Native,
1613 options,
1614 budget,
1615 state: &budget.state,
1616 xml_parse: budget.xml_parse_work(),
1617 source_has_signature: true,
1618 };
1619 let output = execute_transform_chain::<false, _, _, _, _, _, _>(
1620 signature_node,
1621 Some(signature_node),
1622 initial_data.into(),
1623 transforms,
1624 None,
1625 Some(DependencyTracking {
1626 active_nodes,
1627 dormant_indexes,
1628 opaque_dependencies,
1629 canonical_positions: None,
1630 }),
1631 &context,
1632 CollectTerminal,
1633 )?;
1634 Ok(TransformDependencyOutput {
1635 dependencies: output.dependencies,
1636 })
1637}
1638
1639fn ensure_transform_count(count: usize) -> Result<(), TransformError> {
1640 if count > MAX_TRANSFORMS_PER_REFERENCE {
1641 return Err(transform_resource_limit(
1642 crate::policy::resource_name::REFERENCE_TRANSFORMS,
1643 MAX_TRANSFORMS_PER_REFERENCE,
1644 count,
1645 ));
1646 }
1647 Ok(())
1648}
1649
1650#[allow(clippy::too_many_arguments)]
1653fn execute_transform_chain<
1654 's,
1655 'e,
1656 'd,
1657 const STREAM_FINAL_C14N: bool,
1658 T,
1659 E,
1660 F,
1661 S: TransformScratch,
1662 D: DomStorage,
1663 I: Instruction,
1664>(
1665 source_signature: Node<'s, 's>,
1666 enveloped_signature: Option<Node<'e, 'e, D>>,
1667 data: ExecutionData<'d, S, D>,
1668 transforms: &[I],
1669 canonical_signature_position: Option<Option<usize>>,
1670 mut dependency_tracking: Option<DependencyTracking>,
1671 context: &TransformExecutionContext<'_, S>,
1672 finalize: F,
1673) -> Result<TransformChainOutput<T>, E>
1674where
1675 E: From<TransformError>,
1676 F: TransformTerminal<S, Output = T, Error = E>,
1677{
1678 let Some((transform, remaining)) = transforms.split_first() else {
1679 if let (Some(nodes), Some(tracking)) = (data.nodes(), &mut dependency_tracking) {
1680 retain_visible_dependencies(nodes, tracking)?;
1681 }
1682 let output = finalize.finish(data, None, context.budget)?;
1683 return Ok(TransformChainOutput {
1684 output,
1685 dependencies: dependency_indexes(dependency_tracking),
1686 });
1687 };
1688
1689 let binary = if transform_requires_node_set(transform) {
1690 data.binary()
1691 } else {
1692 None
1693 };
1694 if let Some(bytes) = binary {
1695 if canonical_signature_position.is_none() {
1700 context.budget.node_filter.charge(transforms.len())?;
1701 if transforms
1702 .iter()
1703 .any(|t| matches!(t.view(), InstructionView::Enveloped))
1704 {
1705 return Err(TransformError::CrossDocumentSignatureNode.into());
1706 }
1707 }
1708 context
1716 .xml_parse
1717 .charge_policy(bytes.len())
1718 .map_err(TransformError::from)?;
1719 let xml = crate::xml_input_shared::decode_xml_in(
1720 bytes,
1721 context.budget.xml_parse_settings.max_bytes,
1722 |_, _| Ok(()),
1723 &context.scratch.decoder(),
1724 )
1725 .map_err(S::decode_error)?;
1726 let settings = DocumentParseSettings {
1727 allow_dtd: context.options.internal_dtd_allowed(),
1728 ..context.budget.xml_parse_settings
1729 };
1730 let document = context
1731 .scratch
1732 .parse_xml(xml.as_ref(), settings, context.xml_parse)?;
1733 context.state.document_reparsed();
1734 let nodes = Projection::subtree(
1735 document.root(),
1736 true,
1737 &context.budget.node_set_materialization,
1738 context.scratch.node_keys(),
1739 )?;
1740 if let Some(tracking) = &mut dependency_tracking
1741 && let Some(positions) = tracking.canonical_positions.take()
1742 {
1743 let mut remapped = Vec::with_capacity(positions.len());
1744 for tracked in positions {
1745 if let Some(node) = document.descendants().find(|node| {
1746 node.node_type() == tracked.node_type && node.range().start == tracked.position
1747 }) {
1748 remapped.push(TrackedDependencyNode {
1749 index: tracked.index,
1750 node_id: node.id(),
1751 node_type: tracked.node_type,
1752 });
1753 } else {
1754 tracking.opaque_dependencies.insert(tracked.index);
1757 }
1758 }
1759 tracking.active_nodes = remapped;
1760 }
1761 return match canonical_signature_position {
1762 Some(Some(position)) => {
1763 let remapped = document
1764 .descendants()
1765 .find(|node| node.is_element() && node.range().start == position)
1766 .filter(|node| {
1767 enveloped_signature
1768 .is_some_and(|source| node.tag_name() == source.tag_name())
1769 })
1770 .ok_or(TransformError::CrossDocumentSignatureNode)?;
1771 execute_transform_chain::<STREAM_FINAL_C14N, _, _, _, _, _, _>(
1772 source_signature,
1773 Some(remapped),
1774 ExecutionData::Nodes(nodes),
1775 transforms,
1776 None,
1777 dependency_tracking,
1778 context,
1779 finalize,
1780 )
1781 }
1782 Some(None) => execute_transform_chain::<STREAM_FINAL_C14N, _, _, _, _, _, _>(
1783 source_signature,
1784 None,
1785 ExecutionData::Nodes(nodes),
1786 transforms,
1787 None,
1788 dependency_tracking,
1789 context,
1790 finalize,
1791 ),
1792 None => execute_transform_chain::<STREAM_FINAL_C14N, _, _, _, _, _, _>(
1793 source_signature,
1794 None,
1798 ExecutionData::Nodes(nodes),
1799 transforms,
1800 None,
1801 dependency_tracking,
1802 context,
1803 finalize,
1804 ),
1805 };
1806 }
1807
1808 if let InstructionView::C14n(algo, prefixes) = transform.view()
1809 && let Some(nodes) = data.nodes()
1810 {
1811 if let Some(tracking) = &mut dependency_tracking {
1812 retain_visible_dependencies(nodes, tracking)?;
1813 tracking.dormant_indexes.clear();
1814 }
1815 if STREAM_FINAL_C14N && remaining.is_empty() {
1816 let output = finalize.finish(data, Some((algo, prefixes)), context.budget)?;
1817 return Ok(TransformChainOutput {
1818 output,
1819 dependencies: dependency_indexes(dependency_tracking),
1820 });
1821 }
1822 let tracked_element = match enveloped_signature {
1823 Some(signature)
1824 if std::ptr::eq(signature.document(), nodes.document())
1825 && nodes
1826 .contains_tree(signature)
1827 .map_err(TransformError::from)? =>
1828 {
1829 Some(signature.id())
1830 }
1831 _ => None,
1832 };
1833 if S::ACCOUNTED || prefixes.is_some() {
1834 debug_assert!(dependency_tracking.is_none());
1837 let (output, position) = c14n::canonicalize_buffer_in(
1838 nodes.document(),
1839 nodes,
1840 (algo, prefixes),
1841 tracked_element,
1842 context.budget.c14n.remaining(),
1843 context.budget.xml_base_resolution(),
1844 context.scratch,
1845 )
1846 .map_err(|error| map_c14n_limit_error(error, &context.budget.c14n))?;
1847 context
1848 .budget
1849 .c14n
1850 .charge(output.len())
1851 .map_err(TransformError::from)?;
1852 drop(data);
1853 return execute_transform_chain::<STREAM_FINAL_C14N, _, _, _, _, _, _>(
1854 source_signature,
1855 enveloped_signature,
1856 ExecutionData::Accounted(output),
1857 remaining,
1858 Some(position),
1859 dependency_tracking,
1860 context,
1861 finalize,
1862 );
1863 }
1864 let mut output = Vec::new();
1865 let position = if let Some(tracking) = &mut dependency_tracking {
1866 let mut tracked_ids = tracking
1867 .active_nodes
1868 .iter()
1869 .map(|tracked| tracked.node_id)
1870 .collect::<Vec<_>>();
1871 if let Some(signature_id) = tracked_element
1872 && !tracked_ids.contains(&signature_id)
1873 {
1874 tracked_ids.push(signature_id);
1875 }
1876 let positions =
1877 c14n::canonicalize_with_visibility_and_positions_bounded_with_xml_base_budget(
1878 nodes.document(),
1879 Some(nodes),
1880 algo,
1881 &tracked_ids,
1882 context.budget.c14n.remaining(),
1883 context.budget.xml_base_resolution(),
1884 &mut output,
1885 )
1886 .map_err(|error| map_c14n_limit_error(error, &context.budget.c14n))?;
1887 let mut canonical_positions = Vec::with_capacity(tracking.active_nodes.len());
1888 for tracked in &tracking.active_nodes {
1889 if let Some((_, position)) = positions
1890 .iter()
1891 .find(|(tracked_id, _)| *tracked_id == tracked.node_id)
1892 {
1893 canonical_positions.push(CanonicalDependencyPosition {
1894 index: tracked.index,
1895 position: *position,
1896 node_type: tracked.node_type,
1897 });
1898 } else {
1899 tracking.opaque_dependencies.insert(tracked.index);
1900 }
1901 }
1902 tracking.canonical_positions = Some(canonical_positions);
1903 tracked_element.and_then(|signature_id| {
1904 positions
1905 .iter()
1906 .find(|(tracked_id, _)| *tracked_id == signature_id)
1907 .map(|(_, position)| *position)
1908 })
1909 } else {
1910 c14n::canonicalize_with_visibility_and_position_bounded_with_xml_base_budget(
1911 nodes.document(),
1912 Some(nodes),
1913 algo,
1914 tracked_element,
1915 context.budget.c14n.remaining(),
1916 context.budget.xml_base_resolution(),
1917 &mut output,
1918 )
1919 .map_err(|error| map_c14n_limit_error(error, &context.budget.c14n))?
1920 };
1921 context
1922 .budget
1923 .c14n
1924 .charge(output.len())
1925 .map_err(TransformError::from)?;
1926 return execute_transform_chain::<STREAM_FINAL_C14N, _, _, _, _, _, _>(
1927 source_signature,
1928 enveloped_signature,
1929 ExecutionData::NativeBinary(output),
1930 remaining,
1931 Some(position),
1932 dependency_tracking,
1933 context,
1934 finalize,
1935 );
1936 }
1937
1938 if S::ACCOUNTED && matches!(transform.view(), InstructionView::Base64) {
1939 let bytes =
1940 base64_projection(data.binary(), data.nodes(), context.budget, context.scratch)?;
1941 drop(data);
1944 if let Some(tracking) = &mut dependency_tracking {
1945 tracking
1946 .opaque_dependencies
1947 .extend(tracking.active_nodes.drain(..).map(|node| node.index));
1948 tracking.dormant_indexes.clear();
1949 tracking.canonical_positions = None;
1950 }
1951 return execute_transform_chain::<STREAM_FINAL_C14N, _, _, _, _, _, _>(
1952 source_signature,
1953 enveloped_signature,
1954 ExecutionData::Accounted(bytes),
1955 remaining,
1956 None,
1957 dependency_tracking,
1958 context,
1959 finalize,
1960 );
1961 }
1962
1963 if matches!(transform.view(), InstructionView::Enveloped) {
1964 let Some(signature) = enveloped_signature else {
1965 let data = if !context.source_has_signature {
1971 match data {
1972 ExecutionData::Nodes(nodes) => ExecutionData::Nodes(nodes.empty_like()),
1973 _ => unreachable!("node-set transforms adapt binary input before execution"),
1974 }
1975 } else {
1976 data
1977 };
1978 return execute_transform_chain::<STREAM_FINAL_C14N, _, _, _, _, _, _>(
1979 source_signature,
1980 None,
1981 data,
1982 remaining,
1983 None,
1984 dependency_tracking,
1985 context,
1986 finalize,
1987 );
1988 };
1989 let data = apply_transform_with_context(signature, transform, data, context)?;
1990 return execute_transform_chain::<STREAM_FINAL_C14N, _, _, _, _, _, _>(
1991 source_signature,
1992 Some(signature),
1993 data,
1994 remaining,
1995 None,
1996 dependency_tracking,
1997 context,
1998 finalize,
1999 );
2000 }
2001
2002 let data = apply_transform_with_context(source_signature, transform, data, context)?;
2003 if let Some(tracking) = &mut dependency_tracking {
2004 match &data {
2005 ExecutionData::Nodes(nodes) => {
2006 let preserve_excluded_as_opaque = match transform.view() {
2007 InstructionView::XPath(expression) => {
2008 xpath_may_read_mutable_character_data(expression.expression())
2009 }
2010 InstructionView::Filter2(filters) => filters.iter().any(|filter| {
2011 xpath_may_read_mutable_character_data(filter.xpath().expression())
2012 }),
2013 _ => false,
2014 };
2015 if preserve_excluded_as_opaque {
2016 tracking
2017 .opaque_dependencies
2018 .extend(tracking.dormant_indexes.drain());
2019 }
2020 let mut active_nodes = Vec::with_capacity(tracking.active_nodes.len());
2021 for tracked in tracking.active_nodes.drain(..) {
2022 let remains_visible = match nodes.document().get_node(tracked.node_id) {
2023 Some(node) => nodes.contains_tree(node).map_err(TransformError::from)?,
2024 None => false,
2025 };
2026 if remains_visible {
2027 active_nodes.push(tracked);
2028 } else if preserve_excluded_as_opaque {
2029 tracking.opaque_dependencies.insert(tracked.index);
2035 } else {
2036 tracking.dormant_indexes.insert(tracked.index);
2037 }
2038 }
2039 tracking.active_nodes = active_nodes;
2040 }
2041 ExecutionData::NativeBinary(_)
2042 | ExecutionData::BorrowedBinary(_)
2043 | ExecutionData::Accounted(_) => {
2044 tracking
2045 .opaque_dependencies
2046 .extend(tracking.active_nodes.drain(..).map(|tracked| tracked.index));
2047 tracking.dormant_indexes.clear();
2048 tracking.canonical_positions = None;
2049 }
2050 }
2051 }
2052 execute_transform_chain::<STREAM_FINAL_C14N, _, _, _, _, _, _>(
2053 source_signature,
2054 enveloped_signature,
2055 data,
2056 remaining,
2057 None,
2058 dependency_tracking,
2059 context,
2060 finalize,
2061 )
2062}
2063
2064fn map_transform_xml_parse_error(
2065 error: XmlDocumentError,
2066 settings: DocumentParseSettings,
2067) -> TransformError {
2068 if let XmlDocumentError::Parse(crate::xml::dom::ParseError::SharedExecution(error)) = error {
2069 return error.into();
2070 }
2071 match error.into_policy_violation(settings) {
2072 Ok(error) => TransformError::Policy(error),
2073 Err(error) => TransformError::XmlParse(error),
2074 }
2075}
2076
2077fn map_transform_xml_decode_error(error: crate::encoding::XmlEncodingError) -> TransformError {
2078 match error {
2079 crate::encoding::XmlEncodingError::DecodedLimit { maximum, actual } => {
2080 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
2081 resource: crate::policy::resource_name::XML_DOCUMENT,
2082 maximum,
2083 actual,
2084 })
2085 }
2086 error => TransformError::XmlParse(XmlDocumentError::Encoding(error)),
2087 }
2088}
2089
2090fn dependency_indexes(tracking: Option<DependencyTracking>) -> HashSet<usize> {
2091 let Some(tracking) = tracking else {
2092 return HashSet::new();
2093 };
2094 tracking
2095 .active_nodes
2096 .into_iter()
2097 .map(|tracked| tracked.index)
2098 .chain(tracking.opaque_dependencies)
2099 .collect()
2100}
2101
2102fn transform_requires_node_set(transform: &impl Instruction) -> bool {
2103 !matches!(
2104 transform.view(),
2105 InstructionView::Base64 | InstructionView::Relationship(_)
2106 )
2107}
2108
2109fn map_c14n_limit_error(error: c14n::C14nError, budget: &C14nOutputBudget) -> TransformError {
2110 if c14n::is_output_limit_error(&error) {
2111 budget.exhaust();
2114 }
2115 if let Some(violation) = map_c14n_resource_policy_violation(
2116 &error,
2117 crate::policy::resource_name::CANONICALIZED_BYTES,
2118 budget.max_bytes,
2119 ) {
2120 TransformError::Policy(violation)
2121 } else {
2122 TransformError::C14n(error)
2123 }
2124}
2125
2126pub(crate) fn transform_chain_produces_binary<I: Instruction>(
2127 initial_binary: bool,
2128 transforms: &[I],
2129) -> bool {
2130 transforms.last().map_or(initial_binary, |transform| {
2131 matches!(
2132 transform.view(),
2133 InstructionView::C14n(..) | InstructionView::Base64 | InstructionView::Relationship(_)
2134 )
2135 })
2136}
2137
2138pub(crate) fn map_c14n_resource_policy_violation(
2143 error: &c14n::C14nError,
2144 output_resource: &'static str,
2145 output_maximum: usize,
2146) -> Option<crate::policy::PolicyViolation> {
2147 match error {
2148 error if c14n::is_output_limit_error(error) => {
2149 Some(crate::policy::PolicyViolation::ResourceLimit {
2150 resource: output_resource,
2151 maximum: output_maximum,
2152 actual: output_maximum.saturating_add(1),
2155 })
2156 }
2157 c14n::C14nError::XmlBaseComponentsTooLarge { max, actual } => {
2158 Some(crate::policy::PolicyViolation::ResourceLimit {
2159 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
2160 maximum: *max,
2161 actual: *actual,
2162 })
2163 }
2164 c14n::C14nError::XmlBaseResolutionTooLarge { max_bytes, actual } => {
2165 Some(crate::policy::PolicyViolation::ResourceLimit {
2166 resource: crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
2167 maximum: *max_bytes,
2168 actual: *actual,
2169 })
2170 }
2171 _ => None,
2172 }
2173}
2174
2175pub(crate) fn validate_signing_transform_policy(
2176 initial_binary: bool,
2177 transforms: &[Transform],
2178 allowed: Option<&HashSet<String>>,
2179) -> Result<(), crate::policy::PolicyViolation> {
2180 validate_transform_policy(initial_binary, transforms, allowed, "signing transform")
2181}
2182
2183fn validate_transform_policy(
2184 initial_binary: bool,
2185 transforms: &[Transform],
2186 allowed: Option<&HashSet<String>>,
2187 operation: &'static str,
2188) -> Result<(), crate::policy::PolicyViolation> {
2189 let Some(allowed) = allowed else {
2190 return Ok(());
2191 };
2192 for transform in transforms {
2193 let algorithm = transform.algorithm_uri();
2194 if !allowed.contains(algorithm) {
2195 return Err(crate::policy::PolicyViolation::Algorithm {
2196 operation,
2197 algorithm: algorithm.to_owned(),
2198 });
2199 }
2200 }
2201 if !transform_chain_produces_binary(initial_binary, transforms)
2202 && !allowed.contains(DEFAULT_IMPLICIT_C14N_URI)
2203 {
2204 return Err(crate::policy::PolicyViolation::Algorithm {
2205 operation,
2206 algorithm: DEFAULT_IMPLICIT_C14N_URI.to_owned(),
2207 });
2208 }
2209 Ok(())
2210}
2211
2212pub fn parse_transforms(transforms_node: Node) -> Result<Vec<Transform>, TransformError> {
2221 parse_transforms_with_budget(transforms_node, &mut XPathSignatureParseBudget::default())
2222}
2223
2224pub(crate) fn parse_transforms_with_budget(
2225 transforms_node: Node,
2226 signature_budget: &mut XPathSignatureParseBudget,
2227) -> Result<Vec<Transform>, TransformError> {
2228 parse_reference_transforms_with_budget(transforms_node, XMLDSIG_NS, signature_budget)
2229}
2230
2231pub(crate) fn parse_reference_transforms_with_budget(
2232 transforms_node: Node,
2233 container_namespace: &str,
2234 signature_budget: &mut XPathSignatureParseBudget,
2235) -> Result<Vec<Transform>, TransformError> {
2236 parse_reference_transforms_in(
2237 transforms_node,
2238 container_namespace,
2239 signature_budget,
2240 &Native,
2241 )
2242}
2243
2244pub(crate) fn parse_reference_transforms_in<'a, S: ParseStorage<'a>>(
2245 transforms_node: Node<'a, 'a>,
2246 container_namespace: &str,
2247 signature_budget: &mut XPathSignatureParseBudget,
2248 scratch: &S,
2249) -> Result<S::Values<S::Instruction>, TransformError> {
2250 if !transforms_node.is_element() {
2252 return Err(scratch.unsupported(format_args!(
2253 "expected <Transforms> element but got non-element node"
2254 )));
2255 }
2256 let transforms_tag = transforms_node.tag_name();
2257 if transforms_tag.name() != "Transforms"
2258 || transforms_tag.namespace() != Some(container_namespace)
2259 {
2260 return Err(
2261 scratch.unsupported(format_args!("Transforms container has the wrong namespace"))
2262 );
2263 }
2264
2265 let mut chain = scratch.values();
2266 let mut xpath_state = XPathParseState::new(signature_budget);
2267
2268 scratch.charge(transforms_node.children().len())?;
2269 for child in transforms_node.children() {
2270 if !child.is_element() {
2271 if child.is_text()
2272 && let Some(text) = child.text()
2273 {
2274 scratch.charge(text.len())?;
2275 }
2276 if child.is_text() && !child.text().is_some_and(is_xml_whitespace_only) {
2281 return Err(
2282 scratch.unsupported(format_args!("Transforms must not contain character data"))
2283 );
2284 }
2285 continue;
2286 }
2287 ensure_transform_count(chain.len() + 1)?;
2288
2289 let tag = child.tag_name();
2291 if tag.name() != "Transform" || tag.namespace() != Some(XMLDSIG_NS) {
2292 return Err(scratch.unsupported(format_args!(
2293 "unexpected child element of <ds:Transforms>; only <ds:Transform> is allowed"
2294 )));
2295 }
2296 scratch.charge(child.attributes().len())?;
2297 let uri = child.attribute("Algorithm").ok_or_else(|| {
2298 scratch.unsupported(format_args!("missing Algorithm attribute on <Transform>"))
2299 })?;
2300
2301 if chain.len() == chain.capacity() {
2304 chain.reserve_exact(chain.len().max(1))?;
2305 }
2306 scratch.charge(uri.len())?;
2307
2308 let transform = if uri == ENVELOPED_SIGNATURE_URI {
2309 scratch.enveloped()
2310 } else if uri == BASE64_TRANSFORM_URI {
2311 validate_empty_transform(child, "Base64", scratch)?;
2312 scratch.base64()
2313 } else if uri == XPATH_TRANSFORM_URI {
2314 parse_xpath_transform_with_state(child, &mut xpath_state, scratch)?
2315 } else if uri == XPATH_FILTER2_TRANSFORM_URI {
2316 parse_xpath_filter2_transform(child, &mut xpath_state, scratch)?
2317 } else if uri == RELATIONSHIP_TRANSFORM_URI {
2318 scratch.relationships(relationship::parse_selectors_in(
2319 child,
2320 &xpath_state.signature_budget.opc_workspace,
2321 scratch,
2322 )?)
2323 } else if let Some(algo) = C14nAlgorithm::from_uri(uri) {
2324 let prefixes = if algo.mode() == c14n::C14nMode::Exclusive1_0 {
2326 scratch.charge(child.children().len())?;
2327 parse_inclusive_prefixes(child, scratch)?
2328 } else {
2329 None
2330 };
2331 scratch.canonicalization(algo, prefixes)
2332 } else {
2333 return Err(scratch.unsupported(format_args!("{uri}")));
2334 };
2335 chain.push(transform)?;
2336 }
2337
2338 scratch.charge(chain.len())?;
2339 relationship::validate_chain(&chain)?;
2340 Ok(chain)
2341}
2342
2343fn validate_empty_transform<'a, S: ParseStorage<'a>>(
2345 transform_node: Node<'a, 'a>,
2346 transform_name: &'static str,
2347 scratch: &S,
2348) -> Result<(), TransformError> {
2349 scratch.charge(transform_node.children().len())?;
2350 for child in transform_node.children() {
2351 if child.is_text()
2352 && let Some(text) = child.text()
2353 {
2354 scratch.charge(text.len())?;
2355 }
2356 if child.is_element()
2357 || (child.is_text()
2358 && child
2359 .text()
2360 .is_some_and(|text| !is_xml_whitespace_only(text)))
2361 {
2362 return Err(scratch.unsupported(format_args!(
2363 "{transform_name} transform must not contain parameters"
2364 )));
2365 }
2366 }
2367 Ok(())
2368}
2369
2370#[cfg(test)]
2371pub(super) fn parse_xpath_transform(transform_node: Node) -> Result<Transform, TransformError> {
2372 parse_xpath_transform_with_state(
2373 transform_node,
2374 &mut XPathParseState::new(&mut XPathSignatureParseBudget::default()),
2375 &Native,
2376 )
2377}
2378
2379fn parse_xpath_transform_with_state<'a, S: ParseStorage<'a>>(
2380 transform_node: Node<'a, 'a>,
2381 xpath_state: &mut XPathParseState,
2382 scratch: &S,
2383) -> Result<S::Instruction, TransformError> {
2384 let mut xpath_node = None;
2385
2386 scratch.charge(transform_node.children().len())?;
2387 for child in transform_node.children() {
2388 if child.is_text()
2389 && let Some(text) = child.text()
2390 {
2391 scratch.charge(text.len())?;
2392 }
2393 if child.is_text() && child.text().is_some_and(is_xml_whitespace_only) {
2394 continue;
2395 }
2396 if child.is_comment() || child.is_pi() {
2397 continue;
2398 }
2399 if !child.is_element() {
2400 return Err(scratch.xpath_error(format_args!(
2401 "XPath transform contains non-whitespace parameter content"
2402 )));
2403 }
2404 let tag = child.tag_name();
2405 if tag.name() == "XPath" && tag.namespace() == Some(XMLDSIG_NS) {
2406 if xpath_node.is_some() {
2407 return Err(scratch.xpath_error(format_args!(
2408 "XPath transform must contain exactly one XMLDSig <XPath> child element"
2409 )));
2410 }
2411 xpath_node = Some(child);
2412 } else {
2413 return Err(scratch.xpath_error(format_args!(
2414 "XPath transform allows only a single XMLDSig <XPath> child element"
2415 )));
2416 }
2417 }
2418
2419 let xpath_node = xpath_node.ok_or_else(|| {
2420 scratch.xpath_error(format_args!(
2421 "XPath transform requires a single XMLDSig <XPath> child element"
2422 ))
2423 })?;
2424 if xpath_node.attributes().len() != 0 {
2425 return Err(scratch.xpath_error(format_args!("XMLDSig <XPath> does not allow attributes")));
2426 }
2427 let xpath = parse_xpath_expression(xpath_node, transform_node.id(), xpath_state, scratch)?;
2428
2429 scratch.charge(xpath.expression().len())?;
2430 let mut signature_prefix = false;
2431 if xpath.expression() == ENVELOPED_SIGNATURE_XPATH_EXPR {
2432 for (prefix, uri) in xpath.namespaces() {
2433 scratch.charge(prefix.len() + uri.len())?;
2434 if prefix == "dsig" {
2435 signature_prefix = uri == XMLDSIG_NS;
2436 break;
2437 }
2438 }
2439 }
2440 if signature_prefix {
2441 Ok(scratch.exclude_signatures())
2442 } else {
2443 Ok(scratch.xpath(xpath))
2444 }
2445}
2446
2447fn parse_xpath_filter2_transform<'a, S: ParseStorage<'a>>(
2448 transform_node: Node<'a, 'a>,
2449 xpath_state: &mut XPathParseState,
2450 scratch: &S,
2451) -> Result<S::Instruction, TransformError> {
2452 let mut filters = scratch.values();
2453 scratch.charge(transform_node.children().len())?;
2454 for child in transform_node.children() {
2455 if child.is_text()
2456 && let Some(text) = child.text()
2457 {
2458 scratch.charge(text.len())?;
2459 }
2460 if child.is_text() && child.text().is_some_and(is_xml_whitespace_only) {
2461 continue;
2462 }
2463 if child.is_comment() || child.is_pi() {
2464 continue;
2465 }
2466 if !child.is_element()
2467 || child.tag_name().name() != "XPath"
2468 || child.tag_name().namespace() != Some(XPATH_FILTER2_TRANSFORM_URI)
2469 {
2470 return Err(scratch.xpath_error(format_args!(
2471 "XPath Filter 2.0 allows only filter-namespace <XPath> children"
2472 )));
2473 }
2474 if filters.len() == xpath_state.signature_budget.max_filters {
2475 return Err(transform_resource_limit(
2476 crate::policy::resource_name::XPATH_FILTERS,
2477 xpath_state.signature_budget.max_filters,
2478 filters.len().saturating_add(1),
2479 ));
2480 }
2481 let invalid_attributes = || {
2482 scratch.xpath_error(format_args!(
2483 "XPath Filter 2.0 <XPath> requires only the unqualified Filter attribute"
2484 ))
2485 };
2486 if child.attributes().len() != 1 {
2487 return Err(invalid_attributes());
2488 }
2489 scratch.charge(1)?;
2490 let filter = child.attribute("Filter").ok_or_else(invalid_attributes)?;
2491 let operation = match filter {
2492 "intersect" => XPathFilterOperation::Intersect,
2493 "subtract" => XPathFilterOperation::Subtract,
2494 "union" => XPathFilterOperation::Union,
2495 value => {
2496 return Err(scratch.xpath_error(format_args!(
2497 "unsupported XPath Filter 2.0 operation: {value}"
2498 )));
2499 }
2500 };
2501 if filters.len() == filters.capacity() {
2502 filters.reserve_exact(filters.len().max(1))?;
2503 }
2504 filters.push(scratch.filter(
2505 operation,
2506 parse_xpath_expression(child, transform_node.id(), xpath_state, scratch)?,
2507 ))?;
2508 }
2509 if filters.is_empty() {
2510 return Err(scratch.xpath_error(format_args!(
2511 "XPath Filter 2.0 requires at least one expression"
2512 )));
2513 }
2514 Ok(scratch.filters(filters))
2515}
2516
2517fn parse_xpath_expression<'a, S: ParseStorage<'a>>(
2518 xpath_node: Node<'a, 'a>,
2519 transform_node: NodeId,
2520 xpath_state: &mut XPathParseState,
2521 scratch: &S,
2522) -> Result<S::Expression, TransformError> {
2523 let source_text = source::read(
2524 xpath_node,
2525 xpath_state.signature_budget.max_expression_bytes,
2526 scratch,
2527 )?;
2528 let source = source_text.view()?;
2529 if source.is_empty() {
2530 return Err(scratch.xpath_error(format_args!("XPath expression must not be empty")));
2531 }
2532 xpath_state.signature_budget.charge()?;
2533 if source.len() > xpath_state.signature_budget.max_expression_bytes {
2534 return Err(transform_resource_limit(
2535 crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
2536 xpath_state.signature_budget.max_expression_bytes,
2537 source.len(),
2538 ));
2539 }
2540 scratch.charge(source.len())?;
2541 let complexity = crate::xmldsig::xpath::xpath_expression_complexity(source);
2542 if complexity > xpath_state.signature_budget.max_expression_complexity {
2543 return Err(transform_resource_limit(
2544 crate::policy::resource_name::XPATH_EXPRESSION_COMPLEXITY,
2545 xpath_state.signature_budget.max_expression_complexity,
2546 complexity,
2547 ));
2548 }
2549 scratch.validate_xpath(
2550 source,
2551 xpath_state.signature_budget.max_expression_bytes,
2552 xpath_state.signature_budget.max_expression_complexity,
2553 )?;
2554
2555 let mut namespaces = XPathNamespaceBudget::with_limits(
2556 xpath_state.signature_budget.max_namespace_bindings,
2557 xpath_state.signature_budget.max_namespace_bytes,
2558 );
2559 for namespace in xpath_node.namespaces() {
2560 scratch.charge(1)?;
2561 if let Some(prefix) = namespace.name() {
2562 scratch.charge(prefix.len() + namespace.uri().len())?;
2563 namespaces.charge(prefix, namespace.uri())?;
2564 }
2565 }
2566 if xpath_state.document_identity.is_none() {
2567 scratch.charge(xpath_node.document().input_text().len())?;
2568 }
2569 Ok(scratch.expression(
2570 source_text,
2571 xpath_node,
2572 XPathHereNodes {
2573 specification_xpath_element: xpath_node.id(),
2576 xmlsec_legacy_transform_element: transform_node,
2577 document: xpath_state.document_identity(xpath_node.document()),
2581 },
2582 ))
2583}
2584
2585struct XPathParseState<'a> {
2586 document_identity: Option<XPathDocumentIdentity>,
2587 signature_budget: &'a mut XPathSignatureParseBudget,
2588}
2589
2590impl<'a> XPathParseState<'a> {
2591 fn new(signature_budget: &'a mut XPathSignatureParseBudget) -> Self {
2592 Self {
2593 document_identity: None,
2594 signature_budget,
2595 }
2596 }
2597
2598 fn document_identity(&mut self, document: &Document<'_>) -> XPathDocumentIdentity {
2599 *self
2600 .document_identity
2601 .get_or_insert_with(|| XPathDocumentIdentity::from_document(document))
2602 }
2603}
2604
2605pub(crate) struct XPathSignatureParseBudget {
2608 opc_workspace: relationship::WorkspaceBudget,
2609 expressions: usize,
2610 max_expressions: usize,
2611 max_expression_bytes: usize,
2612 max_expression_complexity: usize,
2613 max_namespace_bindings: usize,
2614 max_namespace_bytes: usize,
2615 max_filters: usize,
2616}
2617
2618impl Default for XPathSignatureParseBudget {
2619 fn default() -> Self {
2620 Self {
2621 opc_workspace: relationship::WorkspaceBudget::parameters(
2622 crate::hard_limits::OPC_PARAMETER_BYTE_CEILING,
2623 ),
2624 expressions: 0,
2625 max_expressions: MAX_XPATH_EXPRESSIONS_PER_SIGNATURE,
2626 max_expression_bytes: MAX_XPATH_EXPRESSION_BYTES,
2627 max_expression_complexity: crate::hard_limits::XPATH_EXPRESSION_COMPLEXITY_CEILING,
2628 max_namespace_bindings: MAX_XPATH_NAMESPACE_BINDINGS,
2629 max_namespace_bytes: MAX_XPATH_NAMESPACE_BYTES,
2630 max_filters: MAX_XPATH_FILTERS,
2631 }
2632 }
2633}
2634
2635impl XPathSignatureParseBudget {
2636 pub(crate) fn validate_relationship_selectors(
2637 &self,
2638 selectors: &[RelationshipSelector],
2639 ) -> Result<(), TransformError> {
2640 relationship::validate_owned_selectors(selectors, &self.opc_workspace)
2641 }
2642
2643 pub(crate) fn from_resources(resources: &crate::policy::ResourcePolicy) -> Self {
2644 Self {
2645 opc_workspace: relationship::WorkspaceBudget::parameters(
2646 resources.max_opc_parameter_bytes,
2647 ),
2648 expressions: 0,
2649 max_expressions: resources.max_xpath_expressions,
2650 max_expression_bytes: resources.max_xpath_expression_bytes,
2651 max_expression_complexity: resources.max_xpath_expression_complexity,
2652 max_namespace_bindings: resources.max_xpath_namespace_bindings,
2653 max_namespace_bytes: resources.max_xpath_namespace_bytes,
2654 max_filters: resources.max_xpath_filters,
2655 }
2656 }
2657
2658 pub(crate) fn charge(&mut self) -> Result<(), TransformError> {
2659 self.expressions = self
2660 .expressions
2661 .checked_add(1)
2662 .ok_or_else(|| self.error())?;
2663 if self.expressions > self.max_expressions {
2664 return Err(self.error());
2665 }
2666 Ok(())
2667 }
2668
2669 pub(crate) fn validate_expression(&mut self, source: &str) -> Result<(), TransformError> {
2670 if source.is_empty() {
2671 return Err(TransformError::XPath(
2672 "XPath expression must not be empty".into(),
2673 ));
2674 }
2675 self.charge()?;
2676 if source.len() > self.max_expression_bytes {
2677 return Err(transform_resource_limit(
2678 crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
2679 self.max_expression_bytes,
2680 source.len(),
2681 ));
2682 }
2683 let complexity = crate::xmldsig::xpath::xpath_expression_complexity(source);
2684 if complexity > self.max_expression_complexity {
2685 return Err(transform_resource_limit(
2686 crate::policy::resource_name::XPATH_EXPRESSION_COMPLEXITY,
2687 self.max_expression_complexity,
2688 complexity,
2689 ));
2690 }
2691 crate::xmldsig::xpath::compile_xpath_with_policy_limits(
2692 source,
2693 self.max_expression_bytes,
2694 self.max_expression_complexity,
2695 )
2696 .map(|_| ())
2697 .map_err(TransformError::XPath)
2698 }
2699
2700 pub(crate) fn validate_namespaces(
2701 &self,
2702 namespaces: &BTreeMap<String, String>,
2703 ) -> Result<(), TransformError> {
2704 let mut budget = XPathNamespaceBudget::with_limits(
2705 self.max_namespace_bindings,
2706 self.max_namespace_bytes,
2707 );
2708 for (prefix, uri) in namespaces {
2709 budget.charge(prefix, uri)?;
2710 }
2711 Ok(())
2712 }
2713
2714 fn error(&self) -> TransformError {
2715 transform_resource_limit(
2716 crate::policy::resource_name::XPATH_EXPRESSIONS,
2717 self.max_expressions,
2718 self.expressions.max(self.max_expressions.saturating_add(1)),
2719 )
2720 }
2721}
2722
2723struct XPathNamespaceBudget {
2724 bindings: usize,
2725 bytes: usize,
2726 max_bindings: usize,
2727 max_bytes: usize,
2728}
2729
2730impl Default for XPathNamespaceBudget {
2731 fn default() -> Self {
2732 Self::with_limits(MAX_XPATH_NAMESPACE_BINDINGS, MAX_XPATH_NAMESPACE_BYTES)
2733 }
2734}
2735
2736impl XPathNamespaceBudget {
2737 fn with_limits(max_bindings: usize, max_bytes: usize) -> Self {
2738 Self {
2739 bindings: 0,
2740 bytes: 0,
2741 max_bindings,
2742 max_bytes,
2743 }
2744 }
2745
2746 fn charge(&mut self, prefix: &str, uri: &str) -> Result<(), TransformError> {
2747 let bindings = self.bindings.saturating_add(1);
2748 let bytes = self
2749 .bytes
2750 .checked_add(prefix.len())
2751 .and_then(|bytes| bytes.checked_add(uri.len()))
2752 .unwrap_or(usize::MAX);
2753 if bindings > self.max_bindings {
2754 return Err(transform_resource_limit(
2755 crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
2756 self.max_bindings,
2757 bindings,
2758 ));
2759 }
2760 if bytes > self.max_bytes {
2761 return Err(transform_resource_limit(
2762 crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
2763 self.max_bytes,
2764 bytes,
2765 ));
2766 }
2767 self.bindings = bindings;
2768 self.bytes = bytes;
2769 Ok(())
2770 }
2771}
2772
2773pub(crate) fn collect_xpath_namespaces_with_resources(
2774 xpath_node: Node<'_, '_>,
2775 resources: &crate::policy::ResourcePolicy,
2776) -> Result<BTreeMap<String, String>, TransformError> {
2777 collect_xpath_namespaces_with_limits(
2778 xpath_node,
2779 resources.max_xpath_namespace_bindings,
2780 resources.max_xpath_namespace_bytes,
2781 )
2782}
2783
2784fn collect_xpath_namespaces_with_limits(
2785 xpath_node: Node<'_, '_>,
2786 max_bindings: usize,
2787 max_bytes: usize,
2788) -> Result<BTreeMap<String, String>, TransformError> {
2789 let mut budget = XPathNamespaceBudget::with_limits(max_bindings, max_bytes);
2790 for namespace in xpath_node.namespaces() {
2791 if let Some(prefix) = namespace.name() {
2792 budget.charge(prefix, namespace.uri())?;
2793 }
2794 }
2795 Ok(xpath_node
2796 .namespaces()
2797 .filter_map(|namespace| {
2798 namespace
2799 .name()
2800 .map(|prefix| (prefix.to_owned(), namespace.uri().to_owned()))
2801 })
2802 .collect())
2803}
2804
2805pub(crate) fn validate_xpath_namespace_budget_with_resources(
2806 transforms: &[Transform],
2807 inherited_namespace: Option<(&str, &str)>,
2808 resources: &crate::policy::ResourcePolicy,
2809) -> Result<(), TransformError> {
2810 validate_xpath_namespace_budget_with_limits(
2811 transforms,
2812 inherited_namespace,
2813 resources.max_xpath_namespace_bindings,
2814 resources.max_xpath_namespace_bytes,
2815 )
2816}
2817
2818fn validate_xpath_namespace_budget_with_limits(
2819 transforms: &[Transform],
2820 inherited_namespace: Option<(&str, &str)>,
2821 max_bindings: usize,
2822 max_bytes: usize,
2823) -> Result<(), TransformError> {
2824 let validate_expression = |xpath: &XPathExpression| {
2825 let mut budget = XPathNamespaceBudget::with_limits(max_bindings, max_bytes);
2826 for (prefix, uri) in xpath.namespaces() {
2827 budget.charge(prefix, uri)?;
2828 }
2829 if let Some((prefix, uri)) = inherited_namespace
2830 && !xpath.namespaces().contains_key(prefix)
2831 {
2832 budget.charge(prefix, uri)?;
2833 }
2834 Ok::<(), TransformError>(())
2835 };
2836 for transform in transforms {
2837 match transform {
2838 Transform::XpathExcludeAllSignatures => {
2839 let xpath = XPathExpression::new(ENVELOPED_SIGNATURE_XPATH_EXPR)
2840 .with_namespace(ENVELOPED_SIGNATURE_XPATH_PREFIX, XMLDSIG_NS);
2841 validate_expression(&xpath)?;
2842 }
2843 Transform::XPath(xpath) => validate_expression(xpath)?,
2844 Transform::XPathFilter2(filters) => {
2845 for filter in filters {
2846 validate_expression(filter.xpath())?;
2847 }
2848 }
2849 _ => {}
2850 }
2851 }
2852 Ok(())
2853}
2854
2855fn parse_inclusive_prefixes<'a, S: ParseStorage<'a>>(
2873 transform_node: Node<'a, 'a>,
2874 scratch: &S,
2875) -> Result<Option<&'a str>, TransformError> {
2876 for child in transform_node.children() {
2877 if child.is_element() {
2878 let tag = child.tag_name();
2879 if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
2880 {
2881 scratch.charge(child.attributes().len())?;
2882 let prefix_list = child.attribute("PrefixList").ok_or_else(|| {
2883 scratch.unsupported(format_args!(
2884 "missing PrefixList attribute on <InclusiveNamespaces>"
2885 ))
2886 })?;
2887 return Ok(Some(prefix_list));
2888 }
2889 }
2890 }
2891 Ok(None)
2892}
2893
2894#[cfg(test)]
2895#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
2896mod tests {
2897 #[test]
2898 fn scoped_empty_transform_pays_for_parameter_text_validation() {
2899 use structured_asn1::memory::{Memory, MemoryAccount};
2902 let cost = |padding: usize| {
2903 let xml = format!(
2904 "<Transforms xmlns='{XMLDSIG_NS}'><Transform Algorithm='{BASE64_TRANSFORM_URI}'>{}</Transform></Transforms>",
2905 " ".repeat(padding)
2906 );
2907 let document = Document::parse(&xml).expect("fixed whitespace parameter XML");
2908 let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 20)));
2909 let chain = parse_reference_transforms_in(
2910 document.root_element(),
2911 XMLDSIG_NS,
2912 &mut XPathSignatureParseBudget::default(),
2913 &crate::c14n::scratch::Accounted(&work),
2914 )
2915 .expect("whitespace does not add a Base64 parameter");
2916 assert!(matches!(&*chain, [parameters::OriginalInstruction::Base64]));
2917 drop(chain);
2918 assert_eq!(work.used(), 0);
2919 work.work_used()
2920 };
2921 assert!(cost(1024) >= cost(1) + 1023);
2922 }
2923
2924 #[test]
2925 fn scoped_transform_chain_admits_capacity_before_parameter_parsing() {
2926 use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
2929 let document = Document::parse(concat!(
2930 "<Transforms xmlns='http://www.w3.org/2000/09/xmldsig#'>",
2931 "<Transform Algorithm='http://www.w3.org/TR/1999/REC-xpath-19991116'>",
2932 "<XPath>invalid(</XPath></Transform></Transforms>"
2933 ))
2934 .expect("fixed parameter XML");
2935 let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1 << 20)));
2936 let result = parse_reference_transforms_in(
2937 document.root_element(),
2938 XMLDSIG_NS,
2939 &mut XPathSignatureParseBudget::default(),
2940 &crate::c14n::scratch::Accounted(&work),
2941 );
2942 assert!(matches!(
2943 result,
2944 Err(TransformError::C14n(
2945 crate::c14n::C14nError::SharedExecution(MemoryError::LimitExceeded)
2946 ))
2947 ));
2948 assert_eq!(work.used(), 0);
2949 }
2950
2951 #[test]
2952 fn scoped_transform_chain_retains_its_actual_capacity() {
2953 use structured_asn1::memory::{Memory, MemoryAccount};
2956 let document = Document::parse(concat!(
2957 "<Transforms xmlns='http://www.w3.org/2000/09/xmldsig#'>",
2958 "<Transform Algorithm='http://www.w3.org/2000/09/xmldsig#enveloped-signature'/>",
2959 "<Transform Algorithm='http://www.w3.org/2000/09/xmldsig#base64'/>",
2960 "</Transforms>"
2961 ))
2962 .expect("fixed parameter XML");
2963 let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 20)));
2964 let chain = parse_reference_transforms_in(
2965 document.root_element(),
2966 XMLDSIG_NS,
2967 &mut XPathSignatureParseBudget::default(),
2968 &crate::c14n::scratch::Accounted(&work),
2969 )
2970 .expect("admitted chain");
2971 let native = parse_transforms(document.root_element()).expect("native chain");
2972 assert!(
2973 chain
2974 .iter()
2975 .map(Instruction::algorithm_uri)
2976 .eq(native.iter().map(Transform::algorithm_uri))
2977 );
2978 assert!(work.used() >= 2 * std::mem::size_of_val(&chain[0]));
2979 drop(chain);
2980 assert_eq!(work.used(), 0);
2981 }
2982
2983 #[test]
2984 fn optimized_xpath_scan_admits_current_work_even_without_a_signature() {
2985 use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
2988 let document = Document::parse("<root><a/><b/></root>").expect("fixed XML");
2989 let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 20)));
2990 let budget = TransformExecutionBudget::default();
2991 let nodes = Projection::accounted_subtree(
2992 document.root(),
2993 true,
2994 &budget.node_set_materialization,
2995 &work,
2996 )
2997 .expect("initial projection fits");
2998 let remaining = document.descendants().len() - 1;
2999 work.charge_work((work.work_remaining() - remaining) as u64)
3000 .expect("leave one fewer scan unit");
3001 let scratch = crate::c14n::scratch::Accounted(&work);
3002 let context = TransformExecutionContext {
3003 scratch: &scratch,
3004 options: TransformOptions::default(),
3005 budget: &budget,
3006 state: &budget.state,
3007 xml_parse: budget.xml_parse_work(),
3008 source_has_signature: false,
3009 };
3010 let result = apply_transform_with_context(
3011 document.root_element(),
3012 &Transform::XpathExcludeAllSignatures,
3013 ExecutionData::Nodes(nodes),
3014 &context,
3015 );
3016 assert!(matches!(
3017 result,
3018 Err(TransformError::C14n(
3019 crate::c14n::C14nError::SharedExecution(MemoryError::WorkExceeded)
3020 ))
3021 ));
3022 assert_eq!(work.used(), 0);
3023 }
3024
3025 #[test]
3026 fn binary_xml_adapter_preserves_typed_encoding_cause() {
3027 let error = map_transform_xml_decode_error(
3030 crate::encoding::XmlEncodingError::InvalidBytes("UTF-8"),
3031 );
3032 assert!(matches!(
3033 error,
3034 TransformError::XmlParse(XmlDocumentError::Encoding(
3035 crate::encoding::XmlEncodingError::InvalidBytes("UTF-8")
3036 ))
3037 ));
3038 }
3039
3040 #[test]
3041 fn terminal_callback_receives_final_nodes_before_c14n_materialization() {
3042 use crate::c14n::C14nMode;
3043 #[derive(Debug)]
3047 enum TerminalError {
3048 Processing(TransformError),
3049 Refused(&'static str),
3050 }
3051 impl From<TransformError> for TerminalError {
3052 fn from(error: TransformError) -> Self {
3053 Self::Processing(error)
3054 }
3055 }
3056 struct RefusingTerminal;
3057 impl TransformTerminal<Native> for RefusingTerminal {
3058 type Output = ();
3059 type Error = TerminalError;
3060 fn finish<D: DomStorage>(
3061 self,
3062 data: ExecutionData<'_, Native, D>,
3063 explicit: Option<(&C14nAlgorithm, Option<&str>)>,
3064 _: &TransformExecutionBudget,
3065 ) -> Result<(), TerminalError> {
3066 assert!(data.nodes().is_some());
3067 assert_eq!(explicit.unwrap().0.mode(), C14nMode::Exclusive1_0);
3068 Err(TerminalError::Refused("original terminal cause"))
3069 }
3070 }
3071 let document = Document::parse("<root/>").unwrap();
3072 let budget = TransformExecutionBudget::default();
3073 let context = TransformExecutionContext {
3074 scratch: &Native,
3075 options: TransformOptions::default(),
3076 budget: &budget,
3077 state: &budget.state,
3078 xml_parse: budget.xml_parse_work(),
3079 source_has_signature: true,
3080 };
3081 let algorithm = C14nAlgorithm::new(C14nMode::Exclusive1_0, false);
3082 let result: Result<TransformChainOutput<()>, TerminalError> =
3083 execute_transform_chain::<true, _, _, _, _, _, _>(
3084 document.root_element(),
3085 Some(document.root_element()),
3086 TransformData::NodeSet(NodeSet::entire_document_with_comments(&document).unwrap())
3087 .into(),
3088 &[Transform::C14n(algorithm)],
3089 None,
3090 None,
3091 &context,
3092 RefusingTerminal,
3093 );
3094 match result {
3095 Err(TerminalError::Refused(cause)) => assert_eq!(cause, "original terminal cause"),
3096 Err(TerminalError::Processing(error)) => panic!("unexpected processing error: {error}"),
3097 Ok(_) => panic!("terminal refusal was ignored"),
3098 }
3099 }
3100
3101 use super::*;
3102 use crate::xml::dom::Document;
3103 use crate::xmldsig::NodeSet;
3104
3105 fn assert_resource_limit(error: &TransformError, expected_resource: &'static str) {
3106 assert!(
3107 matches!(
3108 error,
3109 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3110 resource,
3111 maximum,
3112 actual,
3113 }) if *resource == expected_resource && actual > maximum
3114 ),
3115 "unexpected error: {error:?}"
3116 );
3117 }
3118
3119 #[test]
3122 fn enveloped_excludes_signature_subtree() {
3123 let xml = r#"<root>
3125 <data>hello</data>
3126 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
3127 <SignedInfo><Reference URI=""/></SignedInfo>
3128 <SignatureValue>abc</SignatureValue>
3129 </Signature>
3130 </root>"#;
3131 let doc = Document::parse(xml).unwrap();
3132
3133 let sig_node = doc
3135 .descendants()
3136 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
3137 .unwrap();
3138
3139 let node_set = NodeSet::entire_document_without_comments(&doc).unwrap();
3141 let data = TransformData::NodeSet(node_set);
3142
3143 let result = apply_transform(sig_node, &Transform::Enveloped, data).unwrap();
3145 let node_set = result.into_node_set().unwrap();
3146
3147 assert!(node_set.contains(doc.root_element()));
3149 let data_elem = doc
3150 .descendants()
3151 .find(|n| n.is_element() && n.tag_name().name() == "data")
3152 .unwrap();
3153 assert!(node_set.contains(data_elem));
3154
3155 assert!(
3157 !node_set.contains(sig_node),
3158 "Signature element should be excluded"
3159 );
3160 let signed_info = doc
3161 .descendants()
3162 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
3163 .unwrap();
3164 assert!(
3165 !node_set.contains(signed_info),
3166 "SignedInfo (child of Signature) should be excluded"
3167 );
3168 }
3169
3170 #[test]
3171 fn enveloped_requires_node_set_input() {
3172 let xml = "<root/>";
3173 let doc = Document::parse(xml).unwrap();
3174 let data = TransformData::Binary(vec![1, 2, 3]);
3176 let result = apply_transform(doc.root_element(), &Transform::Enveloped, data);
3177 assert!(result.is_err());
3178 match result.unwrap_err() {
3179 TransformError::TypeMismatch { expected, got } => {
3180 assert_eq!(expected, "NodeSet");
3181 assert_eq!(got, "Binary");
3182 }
3183 other => panic!("expected TypeMismatch, got: {other:?}"),
3184 }
3185 }
3186
3187 #[test]
3188 fn enveloped_rejects_cross_document_signature_node() {
3189 let xml = r#"<Root><Signature Id="sig"/></Root>"#;
3192 let doc1 = Document::parse(xml).unwrap();
3193 let doc2 = Document::parse(xml).unwrap();
3194
3195 let node_set = NodeSet::entire_document_without_comments(&doc1).unwrap();
3197 let input = TransformData::NodeSet(node_set);
3198 let sig_from_doc2 = doc2
3199 .descendants()
3200 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
3201 .unwrap();
3202
3203 let result = apply_transform(sig_from_doc2, &Transform::Enveloped, input);
3204 assert!(matches!(
3205 result,
3206 Err(TransformError::CrossDocumentSignatureNode)
3207 ));
3208 }
3209
3210 #[test]
3213 fn c14n_transform_produces_bytes() {
3214 let xml = r#"<root b="2" a="1"><child/></root>"#;
3215 let doc = Document::parse(xml).unwrap();
3216
3217 let node_set = NodeSet::entire_document_without_comments(&doc).unwrap();
3218 let data = TransformData::NodeSet(node_set);
3219
3220 let algo =
3221 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
3222 let result = apply_transform(doc.root_element(), &Transform::C14n(algo), data).unwrap();
3223
3224 let bytes = result.into_binary().unwrap();
3225 let output = String::from_utf8(bytes).unwrap();
3226 assert_eq!(output, r#"<root a="1" b="2"><child></child></root>"#);
3228 }
3229
3230 #[test]
3231 fn c14n_transform_requires_node_set() {
3232 let xml = "<root/>";
3233 let doc = Document::parse(xml).unwrap();
3234
3235 let algo =
3236 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
3237 let data = TransformData::Binary(vec![1, 2, 3]);
3238 let result = apply_transform(doc.root_element(), &Transform::C14n(algo), data);
3239
3240 assert!(result.is_err());
3241 assert!(matches!(
3242 result.unwrap_err(),
3243 TransformError::TypeMismatch { .. }
3244 ));
3245 }
3246
3247 #[test]
3248 fn c14n_1_1_uses_the_compiled_xml_base_policy() {
3249 let document = Document::parse(
3252 r#"<root xml:base="one/"><parent xml:base="two/"><leaf/></parent></root>"#,
3253 )
3254 .unwrap();
3255 let leaf = document
3256 .descendants()
3257 .find(|node| node.has_tag_name("leaf"))
3258 .unwrap();
3259 let resources = crate::policy::ResourcePolicy {
3260 max_xml_base_components: 1,
3261 ..crate::policy::ResourcePolicy::default()
3262 };
3263 let budget = TransformExecutionBudget::from_resources(&resources);
3264 let algorithm = C14nAlgorithm::new(crate::c14n::C14nMode::Inclusive1_1, false);
3265
3266 let error = execute_transforms_with_options_and_budget(
3267 document.root_element(),
3268 TransformData::NodeSet(NodeSet::subtree(leaf).unwrap()),
3269 &[Transform::C14n(algorithm)],
3270 TransformOptions::default(),
3271 &budget,
3272 )
3273 .expect_err("C14N must use the operation's XML Base component limit");
3274
3275 assert!(matches!(
3276 error,
3277 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3278 resource: crate::policy::resource_name::XML_BASE_COMPONENTS,
3279 maximum: 1,
3280 actual: 2
3281 })
3282 ));
3283 }
3284
3285 #[test]
3288 fn base64_transform_decodes_binary_with_xml_whitespace() {
3289 let doc = Document::parse("<root/>").unwrap();
3291 let input = TransformData::Binary(b" SGV\tsbG8=\r\n".to_vec());
3292
3293 let result = apply_transform(doc.root_element(), &Transform::Base64Decode, input).unwrap();
3294
3295 assert_eq!(result.into_binary().unwrap(), b"Hello");
3296 }
3297
3298 #[test]
3299 fn base64_transform_concatenates_only_selected_text_nodes_in_document_order() {
3300 let xml = r#"<root><Data ID="payload">SGV<!-- split --><Part>sb</Part><?pi ignored?>G8=</Data></root>"#;
3303 let doc = Document::parse(xml).unwrap();
3304 let data = doc
3305 .descendants()
3306 .find(|node| node.attribute("ID") == Some("payload"))
3307 .unwrap();
3308 let input = TransformData::NodeSet(NodeSet::subtree(data).unwrap());
3309
3310 let result = apply_transform(data, &Transform::Base64Decode, input).unwrap();
3311
3312 assert_eq!(result.into_binary().unwrap(), b"Hello");
3313 }
3314
3315 #[test]
3316 fn base64_transform_omits_text_excluded_from_the_node_set() {
3317 let xml = "<root>SGV<Excluded>QUJD</Excluded>sbG8=</root>";
3320 let doc = Document::parse(xml).unwrap();
3321 let excluded = doc
3322 .descendants()
3323 .find(|node| node.has_tag_name("Excluded"))
3324 .unwrap();
3325 let mut nodes = NodeSet::subtree(doc.root_element()).unwrap();
3326 nodes.exclude_subtree(excluded);
3327
3328 let result = apply_transform(
3329 doc.root_element(),
3330 &Transform::Base64Decode,
3331 TransformData::NodeSet(nodes),
3332 )
3333 .unwrap();
3334
3335 assert_eq!(result.into_binary().unwrap(), b"Hello");
3336 }
3337
3338 #[test]
3339 fn base64_transform_ignores_rfc2045_non_alphabet_bytes() {
3340 let doc = Document::parse("<root/>").unwrap();
3341 let input = TransformData::Binary(b"SGVs!\xFFbG8=".to_vec());
3342
3343 let result = apply_transform(doc.root_element(), &Transform::Base64Decode, input).unwrap();
3344
3345 assert_eq!(result.into_binary().unwrap(), b"Hello");
3346 }
3347
3348 #[test]
3349 fn base64_refusal_retains_typed_decoder_cause() {
3350 let doc = Document::parse("<root/>").unwrap();
3353 let error = apply_transform(
3354 doc.root_element(),
3355 &Transform::Base64Decode,
3356 TransformData::Binary(b"SGVsbG8===".to_vec()),
3357 )
3358 .unwrap_err();
3359 assert!(
3360 std::error::Error::source(&error)
3361 .and_then(|cause| cause.downcast_ref::<base64::DecodeSliceError>())
3362 .is_some()
3363 );
3364 }
3365
3366 #[test]
3367 fn base64_transform_rejects_invalid_padding() {
3368 let doc = Document::parse("<root/>").unwrap();
3369 let result = apply_transform(
3370 doc.root_element(),
3371 &Transform::Base64Decode,
3372 TransformData::Binary(b"SGVsbG8===".to_vec()),
3373 );
3374
3375 assert!(matches!(result, Err(TransformError::Base64(_))));
3376 }
3377
3378 #[test]
3379 fn base64_transform_accepts_empty_input() {
3380 let doc = Document::parse("<root/>").unwrap();
3381 let result = apply_transform(
3382 doc.root_element(),
3383 &Transform::Base64Decode,
3384 TransformData::Binary(Vec::new()),
3385 )
3386 .unwrap();
3387
3388 assert!(result.into_binary().unwrap().is_empty());
3389 }
3390
3391 #[test]
3392 fn base64_transform_rejects_oversized_raw_binary_before_normalization() {
3393 let doc = Document::parse("<root/>").unwrap();
3396 let input = TransformData::Binary(vec![b' '; MAX_BASE64_TRANSFORM_INPUT_BYTES + 1]);
3397
3398 let result = apply_transform(doc.root_element(), &Transform::Base64Decode, input);
3399
3400 assert!(matches!(
3401 result,
3402 Err(TransformError::Policy(
3403 crate::policy::PolicyViolation::ResourceLimit {
3404 resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
3405 maximum: MAX_BASE64_TRANSFORM_INPUT_BYTES,
3406 ..
3407 }
3408 ))
3409 ));
3410 }
3411
3412 #[test]
3413 fn base64_transform_rejects_node_set_that_decodes_past_output_budget() {
3414 let encoded_len = MAX_BASE64_TRANSFORM_OUTPUT_BYTES.div_ceil(3) * 4 + 4;
3417 let xml = format!("<root>{}</root>", "A".repeat(encoded_len));
3418 let doc = Document::parse(&xml).unwrap();
3419 let input = TransformData::NodeSet(NodeSet::subtree(doc.root_element()).unwrap());
3420
3421 let result = apply_transform(doc.root_element(), &Transform::Base64Decode, input);
3422
3423 assert!(matches!(
3424 result,
3425 Err(TransformError::Policy(
3426 crate::policy::PolicyViolation::ResourceLimit {
3427 resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
3428 maximum: MAX_BASE64_TRANSFORM_OUTPUT_BYTES,
3429 ..
3430 }
3431 ))
3432 ));
3433 }
3434
3435 #[test]
3436 fn base64_transform_handles_highly_fragmented_node_set_input() {
3437 let expected = vec![0x42_u8; 3 * 1_024];
3441 let encoded = STANDARD.encode(&expected);
3442 let mut xml = String::from("<root>");
3443 for byte in encoded.bytes() {
3444 xml.push(char::from(byte));
3445 xml.push_str("<!-- split -->");
3446 }
3447 xml.push_str("</root>");
3448 let doc = Document::parse(&xml).unwrap();
3449 let input = TransformData::NodeSet(NodeSet::subtree(doc.root_element()).unwrap());
3450
3451 let result = apply_transform(doc.root_element(), &Transform::Base64Decode, input).unwrap();
3452
3453 assert_eq!(result.into_binary().unwrap(), expected);
3454 }
3455
3456 #[test]
3457 fn pipeline_rejects_cumulative_base64_input_past_budget() {
3458 let doc = Document::parse("<root/>").unwrap();
3461 let inner = vec![b'A'; MAX_BASE64_TRANSFORM_OUTPUT_BYTES];
3462 let outer = STANDARD.encode(&inner);
3463 let transforms = [Transform::Base64Decode, Transform::Base64Decode];
3464
3465 let result = execute_transforms(
3466 doc.root_element(),
3467 TransformData::Binary(outer.into_bytes()),
3468 &transforms,
3469 );
3470
3471 assert!(matches!(
3472 result,
3473 Err(TransformError::Policy(
3474 crate::policy::PolicyViolation::ResourceLimit {
3475 resource: crate::policy::resource_name::BASE64_TRANSFORM_INPUT_BYTES,
3476 maximum: MAX_BASE64_TRANSFORM_INPUT_BYTES,
3477 ..
3478 }
3479 ))
3480 ));
3481 }
3482
3483 #[test]
3484 fn operation_rejects_cumulative_base64_output_past_budget() {
3485 let doc = Document::parse("<root/>").unwrap();
3488 let resources = crate::policy::ResourcePolicy {
3489 max_base64_transform_input_bytes: 8,
3490 max_base64_transform_output_bytes: 1,
3491 ..crate::policy::ResourcePolicy::default()
3492 };
3493 let budget = TransformExecutionBudget::from_resources(&resources);
3494
3495 let first = apply_transform_with_options(
3496 doc.root_element(),
3497 &Transform::Base64Decode,
3498 TransformData::Binary(b"YQ==".to_vec()),
3499 TransformOptions::default(),
3500 &budget,
3501 )
3502 .expect("the first one-byte output must fit");
3503 assert_eq!(first.into_binary().unwrap(), b"a");
3504
3505 let error = apply_transform_with_options(
3506 doc.root_element(),
3507 &Transform::Base64Decode,
3508 TransformData::Binary(b"Yg==".to_vec()),
3509 TransformOptions::default(),
3510 &budget,
3511 )
3512 .expect_err("the second output must exceed the cumulative allowance");
3513
3514 assert!(matches!(
3515 error,
3516 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3517 resource: crate::policy::resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
3518 maximum: 1,
3519 actual: 2,
3520 })
3521 ));
3522 }
3523
3524 #[test]
3525 fn pipeline_rejects_unbounded_programmatic_transform_chain() {
3526 let doc = Document::parse("<root/>").unwrap();
3529 let transforms = vec![Transform::Base64Decode; 65];
3530
3531 let result = execute_transforms(
3532 doc.root_element(),
3533 TransformData::Binary(Vec::new()),
3534 &transforms,
3535 );
3536
3537 assert!(matches!(
3538 result,
3539 Err(TransformError::Policy(
3540 crate::policy::PolicyViolation::ResourceLimit {
3541 resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
3542 maximum: MAX_TRANSFORMS_PER_REFERENCE,
3543 ..
3544 }
3545 ))
3546 ));
3547 }
3548
3549 #[test]
3552 fn byte_budgets_remain_exhausted_after_overflow() {
3553 let c14n = C14nOutputBudget::default();
3556 assert!(c14n.charge(MAX_C14N_OUTPUT_BYTES + 1).is_err());
3557 assert!(c14n.charge(1).is_err());
3558
3559 let base64 = Base64WorkBudget::default();
3560 assert!(
3561 base64
3562 .charge_input(MAX_BASE64_TRANSFORM_INPUT_BYTES + 1)
3563 .is_err()
3564 );
3565 assert!(base64.charge_input(1).is_err());
3566 }
3567
3568 #[test]
3569 fn pipeline_rejects_cumulative_c14n_output() {
3570 let xml = format!("<root>{}</root>", "x".repeat(4_096));
3573 let document = Document::parse(&xml).unwrap();
3574 let algorithm =
3575 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
3576 let transforms = vec![
3577 Transform::C14n(algorithm.clone()),
3578 Transform::C14n(algorithm),
3579 ];
3580 let one_output = execute_transforms(
3581 document.root_element(),
3582 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap()),
3583 &transforms[..1],
3584 )
3585 .expect("one canonicalization must succeed")
3586 .len();
3587 let limit = one_output * 2 - 1;
3588
3589 let result = execute_transforms_with_options_and_budget(
3590 document.root_element(),
3591 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap()),
3592 &transforms,
3593 TransformOptions::default(),
3594 &TransformExecutionBudget::with_c14n_limit(limit),
3595 );
3596
3597 assert!(matches!(
3598 result,
3599 Err(TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3600 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
3601 maximum,
3602 ..
3603 })) if maximum == limit
3604 ));
3605 }
3606
3607 #[test]
3608 fn binary_to_node_set_adapter_uses_shared_materialization_budget() {
3609 let signature_document = Document::parse("<Signature/>").unwrap();
3613 let budget = TransformExecutionBudget::with_node_set_materialization_limit(1);
3614 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
3615
3616 let error = execute_transforms_with_options_and_budget(
3617 signature_document.root_element(),
3618 TransformData::Binary(b"<root xmlns:n=\"urn:namespace\"/>".to_vec()),
3619 &transforms,
3620 TransformOptions::default(),
3621 &budget,
3622 )
3623 .expect_err("the binary adapter must charge cloned namespace strings");
3624
3625 assert!(matches!(
3626 error,
3627 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3628 resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
3629 ..
3630 })
3631 ));
3632 }
3633
3634 #[test]
3635 fn binary_to_node_set_adapter_preserves_decoded_document_limit() {
3636 let signature_document = Document::parse("<Signature/>").unwrap();
3639 let xml = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><root>\xe9\xe9</root>";
3640 let resources = crate::policy::ResourcePolicy {
3641 max_xml_document_bytes: xml.len(),
3642 ..crate::policy::ResourcePolicy::default()
3643 };
3644 let budget = TransformExecutionBudget::from_resources(&resources);
3645 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
3646
3647 let error = execute_transforms_with_options_and_budget(
3648 signature_document.root_element(),
3649 TransformData::Binary(xml.to_vec()),
3650 &transforms,
3651 TransformOptions::default(),
3652 &budget,
3653 )
3654 .expect_err("decoded XML expansion must retain resource-limit classification");
3655
3656 assert!(matches!(
3657 error,
3658 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3659 resource: crate::policy::resource_name::XML_DOCUMENT,
3660 maximum,
3661 actual,
3662 }) if maximum == xml.len() && actual > maximum
3663 ));
3664 }
3665
3666 #[test]
3667 fn binary_adapter_gates_decode_even_when_input_is_malformed() {
3668 let signature_document = Document::parse("<Signature/>").unwrap();
3670 let resources = crate::policy::ResourcePolicy {
3671 max_xml_parse_work_bytes: 0,
3672 ..crate::policy::ResourcePolicy::default()
3673 };
3674 let budget = TransformExecutionBudget::from_resources(&resources);
3675 let error = execute_transforms_with_options_and_budget(
3676 signature_document.root_element(),
3677 TransformData::Binary(vec![0xff, 0xfe, 0x00, 0xd8]),
3678 &[Transform::XPath(XPathExpression::new("true()"))],
3679 TransformOptions::default(),
3680 &budget,
3681 )
3682 .expect_err("zero work must reject before malformed decoding");
3683 assert!(matches!(
3684 error,
3685 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3686 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3687 maximum: 0,
3688 ..
3689 })
3690 ));
3691 }
3692
3693 #[test]
3694 fn recursive_binary_adapters_share_xml_parse_work() {
3695 let signature_document = Document::parse("<Signature/>").unwrap();
3699 let xml = b"<root/>";
3700 let parser_passes = crate::document::selected_parser_passes();
3701 let adapter_work = xml.len() * (1 + parser_passes);
3703 let resources = crate::policy::ResourcePolicy {
3704 max_xml_parse_work_bytes: adapter_work,
3705 ..crate::policy::ResourcePolicy::default()
3706 };
3707 let budget = TransformExecutionBudget::from_resources(&resources);
3708 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
3709
3710 execute_transforms_with_options_and_budget(
3711 signature_document.root_element(),
3712 TransformData::Binary(xml.to_vec()),
3713 &transforms,
3714 TransformOptions::default(),
3715 &budget,
3716 )
3717 .expect("the first adapter parse must consume the exact allowance");
3718 let error = execute_transforms_with_options_and_budget(
3719 signature_document.root_element(),
3720 TransformData::Binary(xml.to_vec()),
3721 &transforms,
3722 TransformOptions::default(),
3723 &budget,
3724 )
3725 .expect_err("the second adapter parse must inherit exhausted work");
3726
3727 assert!(matches!(
3728 error,
3729 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3730 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3731 maximum,
3732 actual,
3733 }) if maximum == adapter_work
3734 && actual == adapter_work + xml.len()
3735 ));
3736 }
3737
3738 #[test]
3739 fn binary_to_node_set_adapter_bounds_external_xml_nodes_during_parse() {
3740 let signature_document = Document::parse("<Signature/>").unwrap();
3743 let xml = format!(
3744 "<root>{}</root>",
3745 "<n/>".repeat(XML_DOCUMENT_NODE_CEILING as usize + 1),
3746 );
3747 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
3748
3749 execute_transforms(
3750 signature_document.root_element(),
3751 TransformData::Binary(b"<root><n/></root>".to_vec()),
3752 &transforms,
3753 )
3754 .expect("external XML below the node ceiling must parse and transform");
3755
3756 let error = execute_transforms(
3757 signature_document.root_element(),
3758 TransformData::Binary(xml.into_bytes()),
3759 &transforms,
3760 )
3761 .expect_err("external XML exceeding the node ceiling must fail during parse");
3762
3763 assert!(matches!(
3764 error,
3765 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3766 resource: crate::policy::resource_name::XML_NODES,
3767 ..
3768 })
3769 ));
3770 }
3771
3772 #[test]
3773 fn binary_to_node_set_adapter_enforces_operation_depth() {
3774 let signature_document = Document::parse("<Signature/>").unwrap();
3777 let resources = crate::policy::ResourcePolicy {
3778 max_xml_depth: 2,
3779 ..crate::policy::ResourcePolicy::default()
3780 };
3781 let budget = TransformExecutionBudget::from_resources(&resources);
3782 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
3783
3784 let error = execute_transforms_with_options_and_budget(
3785 signature_document.root_element(),
3786 TransformData::Binary(b"<root><child><leaf/></child></root>".to_vec()),
3787 &transforms,
3788 TransformOptions::default(),
3789 &budget,
3790 )
3791 .expect_err("over-depth transform XML must be rejected before XPath");
3792
3793 assert!(matches!(
3794 error,
3795 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3796 resource: crate::policy::resource_name::XML_DEPTH,
3797 maximum: 2,
3798 actual: 3,
3799 })
3800 ));
3801 }
3802
3803 #[test]
3804 fn binary_to_node_set_adapter_enforces_operation_namespace_limit() {
3805 let signature_document = Document::parse("<Signature/>").unwrap();
3808 let resources = crate::policy::ResourcePolicy {
3809 max_xml_namespace_bindings: 1,
3810 ..crate::policy::ResourcePolicy::default()
3811 };
3812 let budget = TransformExecutionBudget::from_resources(&resources);
3813 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
3814
3815 let error = execute_transforms_with_options_and_budget(
3816 signature_document.root_element(),
3817 TransformData::Binary(b"<root xmlns:a=\"urn:a\" xmlns:b=\"urn:b\"/>".to_vec()),
3818 &transforms,
3819 TransformOptions::default(),
3820 &budget,
3821 )
3822 .expect_err("detached XML must inherit the operation namespace-binding limit");
3823
3824 assert!(matches!(
3825 error,
3826 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3827 resource: crate::policy::resource_name::XML_NAMESPACE_BINDINGS,
3828 maximum: 1,
3829 actual: 2,
3830 })
3831 ));
3832 }
3833
3834 #[test]
3835 fn xpath_projection_uses_shared_materialization_budget() {
3836 let document = Document::parse("<root attribute=\"value\"/>").unwrap();
3840 let budget = TransformExecutionBudget::with_node_set_materialization_limit(1);
3841 let transforms = [Transform::XPath(XPathExpression::new("true()"))];
3842
3843 let error = execute_transforms_with_options_and_budget(
3844 document.root_element(),
3845 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap()),
3846 &transforms,
3847 TransformOptions::default(),
3848 &budget,
3849 )
3850 .expect_err("XPath projection must charge cloned attribute names");
3851
3852 assert!(matches!(
3853 error,
3854 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3855 resource: crate::policy::resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
3856 ..
3857 })
3858 ));
3859 }
3860
3861 #[test]
3862 fn explicit_and_implicit_c14n_stop_at_the_execution_ceiling() {
3863 let xml = format!("<root>{}</root>", "x".repeat(4_096));
3866 let document = Document::parse(&xml).unwrap();
3867 let nodes = || {
3868 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap())
3869 };
3870 let algorithm =
3871 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
3872
3873 for transforms in [&[][..], &[Transform::C14n(algorithm)][..]] {
3874 let error = execute_transforms_with_options_and_budget(
3875 document.root_element(),
3876 nodes(),
3877 transforms,
3878 TransformOptions::default(),
3879 &TransformExecutionBudget::with_c14n_limit(64),
3880 )
3881 .expect_err("canonicalization must stop at the execution ceiling");
3882
3883 assert!(matches!(
3884 error,
3885 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3886 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
3887 maximum: 64,
3888 ..
3889 })
3890 ));
3891 }
3892 }
3893
3894 #[test]
3895 fn execution_budget_bounds_c14n_output_across_references() {
3896 let xml = format!("<root>{}</root>", "x".repeat(4_096));
3900 let document = Document::parse(&xml).unwrap();
3901 let algorithm =
3902 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
3903 let transforms = [Transform::C14n(algorithm)];
3904 let input = || {
3905 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap())
3906 };
3907 let one_output = execute_transforms(document.root_element(), input(), &transforms)
3908 .expect("one canonicalization must succeed")
3909 .len();
3910 let limit = one_output * 2 - 1;
3911 let execution_budget = TransformExecutionBudget::with_c14n_limit(limit);
3912
3913 execute_transforms_with_options_and_budget(
3914 document.root_element(),
3915 input(),
3916 &transforms,
3917 TransformOptions::default(),
3918 &execution_budget,
3919 )
3920 .expect("the first Reference must fit the cumulative C14N output budget");
3921 let result = execute_transforms_with_options_and_budget(
3922 document.root_element(),
3923 input(),
3924 &transforms,
3925 TransformOptions::default(),
3926 &execution_budget,
3927 );
3928
3929 assert!(matches!(
3930 result,
3931 Err(TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3932 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
3933 maximum,
3934 ..
3935 })) if maximum == limit
3936 ));
3937 }
3938
3939 #[test]
3940 fn execution_budget_bounds_repeated_node_set_exclusions() {
3941 let document =
3944 Document::parse("<root><payload/><Signature><Object/></Signature></root>").unwrap();
3945 let signature = document
3946 .descendants()
3947 .find(|node| node.has_tag_name("Signature"))
3948 .unwrap();
3949 let input = || NodeSet::entire_document_with_comments(&document).unwrap();
3950 let entries_per_exclusion = input().len();
3951 let budget = TransformExecutionBudget::with_node_filter_limit(
3952 entries_per_exclusion.saturating_mul(2).saturating_sub(1),
3953 );
3954
3955 execute_transforms_with_options_and_budget(
3956 signature,
3957 TransformData::NodeSet(input()),
3958 &[Transform::Enveloped],
3959 TransformOptions::default(),
3960 &budget,
3961 )
3962 .expect("the first reference exclusion must fit the shared budget");
3963 let result = execute_transforms_with_options_and_budget(
3964 signature,
3965 TransformData::NodeSet(input()),
3966 &[Transform::Enveloped],
3967 TransformOptions::default(),
3968 &budget,
3969 );
3970
3971 assert!(
3972 matches!(
3973 result,
3974 Err(TransformError::Policy(
3975 crate::policy::PolicyViolation::ResourceLimit {
3976 resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
3977 ..
3978 }
3979 ))
3980 ),
3981 "the second reference exclusion must exhaust the shared budget"
3982 );
3983 }
3984
3985 #[test]
3986 fn xpath_node_set_operations_consume_filter_work_budget() {
3987 let document = Document::parse("<root><keep/><drop/></root>").unwrap();
3991 let transforms = [
3992 Transform::XPath(XPathExpression::new("true()")),
3993 Transform::XPathFilter2(vec![XPathFilter::new(
3994 XPathFilterOperation::Intersect,
3995 XPathExpression::new("//*"),
3996 )]),
3997 ];
3998
3999 for transform in transforms {
4000 let resources = crate::policy::ResourcePolicy {
4001 max_node_set_filter_work: 0,
4002 ..crate::policy::ResourcePolicy::default()
4003 };
4004 let budget = TransformExecutionBudget::from_resources(&resources);
4005 let input = NodeSet::entire_document_without_comments(&document)
4006 .map(TransformData::NodeSet)
4007 .unwrap();
4008 let error = execute_transforms_with_options_and_budget(
4009 document.root_element(),
4010 input,
4011 &[transform],
4012 TransformOptions::default(),
4013 &budget,
4014 )
4015 .expect_err("zero filter-work policy must deny XPath node-set operations");
4016
4017 assert!(matches!(
4018 error,
4019 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
4020 resource: crate::policy::resource_name::NODE_SET_FILTER_WORK,
4021 maximum: 0,
4022 ..
4023 })
4024 ));
4025 }
4026 }
4027
4028 #[test]
4029 fn optimized_exclusion_consumes_xpath_execution_budgets() {
4030 let document = Document::parse(
4033 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><value/><ds:Signature/></root>"#,
4034 )
4035 .unwrap();
4036
4037 for resource in [
4038 crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS,
4039 crate::policy::resource_name::XPATH_EVALUATION_WORK,
4040 ] {
4041 let resources = if resource == crate::policy::resource_name::XPATH_CONTEXT_EVALUATIONS {
4042 crate::policy::ResourcePolicy {
4043 max_xpath_context_evaluations: 0,
4044 ..crate::policy::ResourcePolicy::default()
4045 }
4046 } else {
4047 crate::policy::ResourcePolicy {
4048 max_xpath_evaluation_work: 0,
4049 ..crate::policy::ResourcePolicy::default()
4050 }
4051 };
4052 let budget = TransformExecutionBudget::from_resources(&resources);
4053 let input = NodeSet::entire_document_without_comments(&document)
4054 .map(TransformData::NodeSet)
4055 .unwrap();
4056 let error = execute_transforms_with_options_and_budget(
4057 document.root_element(),
4058 input,
4059 &[Transform::XpathExcludeAllSignatures],
4060 TransformOptions::default(),
4061 &budget,
4062 )
4063 .expect_err("optimized XPath must obey execution budgets");
4064
4065 assert!(matches!(
4066 error,
4067 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
4068 resource: actual,
4069 maximum: 0,
4070 ..
4071 }) if actual == resource
4072 ));
4073 }
4074 }
4075
4076 #[test]
4077 fn optimized_exclusion_charges_document_scan_and_each_filter_pass() {
4078 let document = Document::parse(
4083 r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><payload><value/></payload><padding/><ds:Signature/><ds:Signature/></root>"#,
4084 )
4085 .unwrap();
4086 let payload = document
4087 .descendants()
4088 .find(|node| node.has_tag_name("payload"))
4089 .unwrap();
4090 let input = || NodeSet::subtree(payload).unwrap();
4091 let fragment_entries = input().len();
4092
4093 for resource in [
4094 crate::policy::resource_name::XPATH_EVALUATION_WORK,
4095 crate::policy::resource_name::NODE_SET_FILTER_WORK,
4096 ] {
4097 let resources = if resource == crate::policy::resource_name::XPATH_EVALUATION_WORK {
4098 crate::policy::ResourcePolicy {
4099 max_xpath_evaluation_work: fragment_entries,
4100 ..crate::policy::ResourcePolicy::default()
4101 }
4102 } else {
4103 crate::policy::ResourcePolicy {
4104 max_node_set_filter_work: fragment_entries,
4105 ..crate::policy::ResourcePolicy::default()
4106 }
4107 };
4108 let budget = TransformExecutionBudget::from_resources(&resources);
4109 let error = execute_transforms_with_options_and_budget(
4110 document.root_element(),
4111 TransformData::NodeSet(input()),
4112 &[Transform::XpathExcludeAllSignatures],
4113 TransformOptions::default(),
4114 &budget,
4115 )
4116 .expect_err("document-sized optimized XPath work must exceed the fragment budget");
4117
4118 assert_resource_limit(&error, resource);
4119 }
4120 }
4121
4122 #[test]
4123 fn filter2_charges_document_sized_set_operations() {
4124 let document =
4128 Document::parse("<root><payload><value/></payload><outside/><outside/></root>")
4129 .unwrap();
4130 let payload = document
4131 .descendants()
4132 .find(|node| node.has_tag_name("payload"))
4133 .unwrap();
4134 let input = NodeSet::subtree(payload).unwrap();
4135 let resources = crate::policy::ResourcePolicy {
4136 max_node_set_filter_work: input.len(),
4137 ..crate::policy::ResourcePolicy::default()
4138 };
4139 let budget = TransformExecutionBudget::from_resources(&resources);
4140 let transform = Transform::XPathFilter2(vec![XPathFilter::new(
4141 XPathFilterOperation::Intersect,
4142 XPathExpression::new("//*"),
4143 )]);
4144
4145 let error = execute_transforms_with_options_and_budget(
4146 document.root_element(),
4147 TransformData::NodeSet(input),
4148 &[transform],
4149 TransformOptions::default(),
4150 &budget,
4151 )
4152 .expect_err("Filter 2 must charge document-sized set operations");
4153
4154 assert_resource_limit(&error, crate::policy::resource_name::NODE_SET_FILTER_WORK);
4155 }
4156
4157 #[test]
4158 fn execution_budget_bounds_implicit_c14n_across_references() {
4159 let xml = format!("<root>{}</root>", "x".repeat(4_096));
4163 let document = Document::parse(&xml).unwrap();
4164 let input = || {
4165 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap())
4166 };
4167 let one_output = execute_transforms(document.root_element(), input(), &[])
4168 .expect("one implicit canonicalization must succeed")
4169 .len();
4170 let limit = one_output * 3 - 1;
4171 let execution_budget = TransformExecutionBudget::with_c14n_limit(limit);
4172
4173 for _ in 0..2 {
4174 execute_transforms_with_options_and_budget(
4175 document.root_element(),
4176 input(),
4177 &[],
4178 TransformOptions::default(),
4179 &execution_budget,
4180 )
4181 .expect("two implicit C14N outputs must fit the shared budget");
4182 }
4183 let result = execute_transforms_with_options_and_budget(
4184 document.root_element(),
4185 input(),
4186 &[],
4187 TransformOptions::default(),
4188 &execution_budget,
4189 );
4190
4191 assert!(matches!(
4192 result,
4193 Err(TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
4194 resource: crate::policy::resource_name::CANONICALIZED_BYTES,
4195 maximum,
4196 ..
4197 })) if maximum == limit
4198 ));
4199 }
4200
4201 #[test]
4202 fn pipeline_enveloped_then_c14n() {
4203 let xml = r#"<root xmlns:ns="http://example.com" b="2" a="1">
4205 <data>hello</data>
4206 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
4207 <SignedInfo/>
4208 <SignatureValue>abc</SignatureValue>
4209 </Signature>
4210 </root>"#;
4211 let doc = Document::parse(xml).unwrap();
4212
4213 let sig_node = doc
4214 .descendants()
4215 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
4216 .unwrap();
4217
4218 let initial =
4219 TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc).unwrap());
4220 let transforms = vec![
4221 Transform::Enveloped,
4222 Transform::C14n(
4223 C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#").unwrap(),
4224 ),
4225 ];
4226
4227 let result = execute_transforms(sig_node, initial, &transforms).unwrap();
4228
4229 let output = String::from_utf8(result).unwrap();
4230 assert!(!output.contains("Signature"));
4232 assert!(!output.contains("SignedInfo"));
4233 assert!(!output.contains("SignatureValue"));
4234 assert!(output.contains("<data>hello</data>"));
4235 }
4236
4237 #[test]
4238 fn pipeline_c14n_then_enveloped_remaps_the_exact_signature() {
4239 let xml = r#"<root>
4243 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#" Id="other"/>
4244 <data>hello</data>
4245 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#" Id="owner"/>
4246 </root>"#;
4247 let document = Document::parse(xml).unwrap();
4248 let signature = document
4249 .descendants()
4250 .find(|node| node.attribute("Id") == Some("owner"))
4251 .unwrap();
4252 let initial =
4253 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap());
4254 let transforms = vec![
4255 Transform::C14n(
4256 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap(),
4257 ),
4258 Transform::Enveloped,
4259 ];
4260
4261 let output = execute_transforms(signature, initial, &transforms).unwrap();
4262 let output = String::from_utf8(output).unwrap();
4263
4264 assert!(output.contains("Id=\"other\""));
4265 assert!(!output.contains("Id=\"owner\""));
4266 assert!(output.contains("<data>hello</data>"));
4267 }
4268
4269 #[test]
4270 fn pipeline_remaps_signature_after_xpath_removes_an_earlier_sibling() {
4271 let xml = r#"<root>
4274 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#" Id="other"/>
4275 <discard/>
4276 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#" Id="owner"/>
4277 </root>"#;
4278 let document = Document::parse(xml).unwrap();
4279 let signature = document
4280 .descendants()
4281 .find(|node| node.attribute("Id") == Some("owner"))
4282 .unwrap();
4283 let initial =
4284 TransformData::NodeSet(NodeSet::entire_document_without_comments(&document).unwrap());
4285 let transforms = vec![
4286 Transform::XPath(XPathExpression::new("not(self::discard)")),
4287 Transform::C14n(
4288 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap(),
4289 ),
4290 Transform::Enveloped,
4291 ];
4292
4293 let output = execute_transforms(signature, initial, &transforms).unwrap();
4294 let output = String::from_utf8(output).unwrap();
4295
4296 assert!(output.contains("Id=\"other\""));
4297 assert!(!output.contains("Id=\"owner\""));
4298 assert!(!output.contains("discard"));
4299 }
4300
4301 #[test]
4302 fn dependency_tracking_retains_structurally_excluded_nodes_for_later_xpath() {
4303 let document = Document::parse(
4306 r#"<root><Signature><Object><Manifest><DigestValue>pending</DigestValue></Manifest></Object></Signature></root>"#,
4307 )
4308 .unwrap();
4309 let signature = document
4310 .descendants()
4311 .find(|node| node.tag_name().name() == "Signature")
4312 .unwrap();
4313 let manifest = document
4314 .descendants()
4315 .find(|node| node.tag_name().name() == "Manifest")
4316 .unwrap();
4317 let digest_text = document
4318 .descendants()
4319 .find(|node| node.is_text() && node.text() == Some("pending"))
4320 .unwrap();
4321 let transforms = [
4322 Transform::XPath(XPathExpression::new("not(ancestor-or-self::DigestValue)")),
4323 Transform::XPath(XPathExpression::new(
4324 "string-length(string(//DigestValue)) >= 0",
4325 )),
4326 ];
4327
4328 let output = execute_transforms_with_dependency_nodes(
4329 signature,
4330 TransformData::NodeSet(NodeSet::subtree(manifest).unwrap()),
4331 &transforms,
4332 TransformOptions::default(),
4333 &TransformExecutionBudget::default(),
4334 vec![(7, digest_text.id())],
4335 )
4336 .unwrap();
4337
4338 assert_eq!(output.dependencies, HashSet::from([7]));
4339 }
4340
4341 #[test]
4342 fn dependency_tracking_discards_dormant_nodes_at_binary_boundary() {
4343 let document = Document::parse(
4346 r#"<root><DigestValue>external</DigestValue><Signature><payload>value</payload></Signature></root>"#,
4347 )
4348 .unwrap();
4349 let signature = document
4350 .descendants()
4351 .find(|node| node.tag_name().name() == "Signature")
4352 .unwrap();
4353 let digest_text = document
4354 .descendants()
4355 .find(|node| node.is_text() && node.text() == Some("external"))
4356 .unwrap();
4357 let transforms = [
4358 Transform::C14n(
4359 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap(),
4360 ),
4361 Transform::XPath(XPathExpression::new(
4362 "string-length(string(//DigestValue)) >= 0",
4363 )),
4364 ];
4365
4366 let output = execute_transforms_with_dependency_nodes(
4367 signature,
4368 TransformData::NodeSet(NodeSet::subtree(signature).unwrap()),
4369 &transforms,
4370 TransformOptions::default(),
4371 &TransformExecutionBudget::default(),
4372 vec![(11, digest_text.id())],
4373 )
4374 .unwrap();
4375
4376 assert!(output.dependencies.is_empty());
4377 }
4378
4379 #[test]
4380 fn pipeline_enveloped_rejects_foreign_base64_document() {
4381 let source = Document::parse(
4386 r#"<root><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"/></root>"#,
4387 )
4388 .unwrap();
4389 let signature = source
4390 .descendants()
4391 .find(|node| node.tag_name().name() == "Signature")
4392 .unwrap();
4393 let encoded = base64::engine::general_purpose::STANDARD.encode(b"<payload>ok</payload>");
4394 let transforms = vec![
4395 Transform::Base64Decode,
4396 Transform::XPath(XPathExpression::new("true()")),
4397 Transform::Enveloped,
4398 ];
4399
4400 let result = execute_transforms(
4401 signature,
4402 TransformData::Binary(encoded.into()),
4403 &transforms,
4404 );
4405
4406 assert!(matches!(
4407 result,
4408 Err(TransformError::CrossDocumentSignatureNode)
4409 ));
4410 }
4411
4412 #[test]
4413 fn pipeline_no_transforms_applies_default_c14n() {
4414 let xml = r#"<root b="2" a="1"><child/></root>"#;
4416 let doc = Document::parse(xml).unwrap();
4417
4418 let initial =
4419 TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc).unwrap());
4420 let result = execute_transforms(doc.root_element(), initial, &[]).unwrap();
4421
4422 let output = String::from_utf8(result).unwrap();
4423 assert_eq!(output, r#"<root a="1" b="2"><child></child></root>"#);
4424 }
4425
4426 #[test]
4427 fn pipeline_binary_passthrough() {
4428 let xml = "<root/>";
4431 let doc = Document::parse(xml).unwrap();
4432
4433 let initial = TransformData::Binary(b"raw bytes".to_vec());
4434 let result = execute_transforms(doc.root_element(), initial, &[]).unwrap();
4435
4436 assert_eq!(result, b"raw bytes");
4437 }
4438
4439 #[test]
4442 fn enveloped_only_excludes_own_signature() {
4443 let xml = r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4446 <data>hello</data>
4447 <ds:Signature Id="sig-other">
4448 <ds:SignedInfo><ds:Reference URI=""/></ds:SignedInfo>
4449 </ds:Signature>
4450 <ds:Signature Id="sig-target">
4451 <ds:SignedInfo><ds:Reference URI=""/></ds:SignedInfo>
4452 </ds:Signature>
4453 </root>"#;
4454 let doc = Document::parse(xml).unwrap();
4455
4456 let sig_node = doc
4458 .descendants()
4459 .find(|n| n.is_element() && n.attribute("Id") == Some("sig-target"))
4460 .unwrap();
4461
4462 let node_set = NodeSet::entire_document_without_comments(&doc).unwrap();
4463 let data = TransformData::NodeSet(node_set);
4464
4465 let result = apply_transform(sig_node, &Transform::Enveloped, data).unwrap();
4466 let node_set = result.into_node_set().unwrap();
4467
4468 let sig_other = doc
4470 .descendants()
4471 .find(|n| n.is_element() && n.attribute("Id") == Some("sig-other"))
4472 .unwrap();
4473 assert!(
4474 node_set.contains(sig_other),
4475 "other Signature elements should NOT be excluded"
4476 );
4477
4478 assert!(
4480 !node_set.contains(sig_node),
4481 "the specific Signature being verified should be excluded"
4482 );
4483 }
4484
4485 #[test]
4488 fn parse_transforms_enveloped_and_exc_c14n() {
4489 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4490 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
4491 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
4492 </Transforms>"#;
4493 let doc = Document::parse(xml).unwrap();
4494 let transforms_node = doc.root_element();
4495
4496 let chain = parse_transforms(transforms_node).unwrap();
4497 assert_eq!(chain.len(), 2);
4498 assert!(matches!(chain[0], Transform::Enveloped));
4499 assert!(matches!(chain[1], Transform::C14n(_)));
4500 }
4501
4502 #[test]
4503 fn parse_transforms_rejects_unbounded_chain() {
4504 let entries = format!(r#"<Transform Algorithm="{BASE64_TRANSFORM_URI}"/>"#).repeat(65);
4507 let xml = format!(r#"<Transforms xmlns="{XMLDSIG_NS}">{entries}</Transforms>"#);
4508 let doc = Document::parse(&xml).unwrap();
4509
4510 assert!(matches!(
4511 parse_transforms(doc.root_element()),
4512 Err(TransformError::Policy(
4513 crate::policy::PolicyViolation::ResourceLimit {
4514 resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
4515 maximum: MAX_TRANSFORMS_PER_REFERENCE,
4516 ..
4517 }
4518 ))
4519 ));
4520 }
4521
4522 #[test]
4523 fn parse_transforms_accepts_parameterless_base64() {
4524 let xml = format!(
4525 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{BASE64_TRANSFORM_URI}">
4526 </Transform></Transforms>"#
4527 );
4528 let doc = Document::parse(&xml).unwrap();
4529
4530 let chain = parse_transforms(doc.root_element()).unwrap();
4531
4532 assert_eq!(chain.len(), 1);
4533 assert!(matches!(chain[0], Transform::Base64Decode));
4534 }
4535
4536 #[test]
4537 fn parse_transforms_rejects_base64_parameters() {
4538 for parameter in ["<Parameter/>", "unexpected", "\u{00A0}"] {
4539 let xml = format!(
4540 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{BASE64_TRANSFORM_URI}">{parameter}</Transform></Transforms>"#
4541 );
4542 let doc = Document::parse(&xml).unwrap();
4543
4544 let result = parse_transforms(doc.root_element());
4545
4546 assert!(matches!(
4547 result,
4548 Err(TransformError::UnsupportedTransform(_))
4549 ));
4550 }
4551 }
4552
4553 #[test]
4554 fn parse_transforms_rejects_non_xpath_boundary_whitespace() {
4555 let xml = format!(
4558 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{XPATH_TRANSFORM_URI}"><XPath> true()</XPath></Transform></Transforms>"#
4559 );
4560 let doc = Document::parse(&xml).unwrap();
4561
4562 let result = parse_transforms(doc.root_element());
4563
4564 assert!(matches!(result, Err(TransformError::XPath(_))));
4565 }
4566
4567 #[test]
4568 fn parse_transforms_with_inclusive_prefixes() {
4569 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#"
4570 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
4571 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
4572 <ec:InclusiveNamespaces PrefixList="ds saml #default"/>
4573 </Transform>
4574 </Transforms>"#;
4575 let doc = Document::parse(xml).unwrap();
4576 let transforms_node = doc.root_element();
4577
4578 let chain = parse_transforms(transforms_node).unwrap();
4579 assert_eq!(chain.len(), 1);
4580 match &chain[0] {
4581 Transform::C14n(algo) => {
4582 assert!(algo.inclusive_prefixes().contains("ds"));
4583 assert!(algo.inclusive_prefixes().contains("saml"));
4584 assert!(algo.inclusive_prefixes().contains("")); }
4586 other => panic!("expected C14n, got: {other:?}"),
4587 }
4588 }
4589
4590 #[test]
4591 fn parse_transforms_ignores_wrong_ns_inclusive_namespaces() {
4592 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4595 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
4596 <InclusiveNamespaces xmlns="http://example.com/fake"
4597 PrefixList="attacker-controlled"/>
4598 </Transform>
4599 </Transforms>"#;
4600 let doc = Document::parse(xml).unwrap();
4601
4602 let chain = parse_transforms(doc.root_element()).unwrap();
4603 assert_eq!(chain.len(), 1);
4604 match &chain[0] {
4605 Transform::C14n(algo) => {
4606 assert!(
4608 algo.inclusive_prefixes().is_empty(),
4609 "should ignore InclusiveNamespaces in wrong namespace"
4610 );
4611 }
4612 other => panic!("expected C14n, got: {other:?}"),
4613 }
4614 }
4615
4616 #[test]
4617 fn parse_transforms_missing_prefix_list_is_error() {
4618 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#"
4621 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
4622 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
4623 <ec:InclusiveNamespaces/>
4624 </Transform>
4625 </Transforms>"#;
4626 let doc = Document::parse(xml).unwrap();
4627
4628 let result = parse_transforms(doc.root_element());
4629 assert!(result.is_err());
4630 assert!(matches!(
4631 result.unwrap_err(),
4632 TransformError::UnsupportedTransform(_)
4633 ));
4634 }
4635
4636 #[test]
4637 fn parse_transforms_unsupported_algorithm() {
4638 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4639 <Transform Algorithm="http://example.com/unknown"/>
4640 </Transforms>"#;
4641 let doc = Document::parse(xml).unwrap();
4642
4643 let result = parse_transforms(doc.root_element());
4644 assert!(result.is_err());
4645 assert!(matches!(
4646 result.unwrap_err(),
4647 TransformError::UnsupportedTransform(_)
4648 ));
4649 }
4650
4651 #[test]
4652 fn parse_transforms_missing_algorithm() {
4653 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4654 <Transform/>
4655 </Transforms>"#;
4656 let doc = Document::parse(xml).unwrap();
4657
4658 let result = parse_transforms(doc.root_element());
4659 assert!(result.is_err());
4660 assert!(matches!(
4661 result.unwrap_err(),
4662 TransformError::UnsupportedTransform(_)
4663 ));
4664 }
4665
4666 #[test]
4667 fn parse_transforms_empty() {
4668 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#"/>"#;
4669 let doc = Document::parse(xml).unwrap();
4670
4671 let chain = parse_transforms(doc.root_element()).unwrap();
4672 assert!(chain.is_empty());
4673 }
4674
4675 #[test]
4676 fn parse_transforms_accepts_enveloped_compat_xpath() {
4677 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4678 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
4679 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
4680 not(ancestor-or-self::dsig:Signature)
4681 </XPath>
4682 </Transform>
4683 </Transforms>"#;
4684 let doc = Document::parse(xml).unwrap();
4685
4686 let chain = parse_transforms(doc.root_element()).unwrap();
4687 assert_eq!(chain.len(), 1);
4688 assert!(matches!(chain[0], Transform::XpathExcludeAllSignatures));
4689 }
4690
4691 #[test]
4692 fn parse_transforms_accepts_general_xpath_expressions() {
4693 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4696 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
4697 <XPath>self::node()</XPath>
4698 </Transform>
4699 </Transforms>"#;
4700 let doc = Document::parse(xml).unwrap();
4701
4702 let result = parse_transforms(doc.root_element()).unwrap();
4703 assert!(matches!(result.as_slice(), [Transform::XPath(_)]));
4704 }
4705
4706 #[test]
4707 fn parse_xpath_transform_ignores_comments_and_processing_instructions() {
4708 let xml = format!(
4711 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{XPATH_TRANSFORM_URI}"><!-- before --><?probe value?><XPath>true()</XPath><!-- after --><?done?></Transform></Transforms>"#
4712 );
4713 let doc = Document::parse(&xml).unwrap();
4714
4715 let transforms = parse_transforms(doc.root_element()).unwrap();
4716
4717 assert!(matches!(transforms.as_slice(), [Transform::XPath(_)]));
4718 }
4719
4720 #[test]
4721 fn parse_filter2_transform_ignores_comments_and_processing_instructions() {
4722 let xml = format!(
4725 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>"#
4726 );
4727 let doc = Document::parse(&xml).unwrap();
4728
4729 let transforms = parse_transforms(doc.root_element()).unwrap();
4730
4731 assert!(matches!(
4732 transforms.as_slice(),
4733 [Transform::XPathFilter2(filters)] if filters.len() == 1
4734 ));
4735 }
4736
4737 #[test]
4738 fn parse_transforms_bounds_raw_xpath_parameter_text() {
4739 let padding = " ".repeat(MAX_XPATH_EXPRESSION_BYTES);
4742 let xml = format!(
4743 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{XPATH_TRANSFORM_URI}"><XPath>{padding}true()</XPath></Transform></Transforms>"#
4744 );
4745 let doc = Document::parse(&xml).unwrap();
4746
4747 let error = parse_transforms(doc.root_element())
4748 .expect_err("raw XPath parameter text must obey the expression bound");
4749
4750 assert!(matches!(
4751 error,
4752 TransformError::Policy(crate::policy::PolicyViolation::ResourceLimit {
4753 resource: crate::policy::resource_name::XPATH_EXPRESSION_BYTES,
4754 maximum: MAX_XPATH_EXPRESSION_BYTES,
4755 ..
4756 })
4757 ));
4758 }
4759
4760 #[test]
4761 fn xpath_namespace_limits_apply_to_each_expression() {
4762 let xml = format!(
4765 r#"<Transforms xmlns="{XMLDSIG_NS}">
4766 <Transform Algorithm="{XPATH_TRANSFORM_URI}">
4767 <XPath xmlns:a="urn:a">a:item</XPath>
4768 </Transform>
4769 <Transform Algorithm="{XPATH_TRANSFORM_URI}">
4770 <XPath xmlns:b="urn:b">b:item</XPath>
4771 </Transform>
4772 </Transforms>"#
4773 );
4774 let doc = Document::parse(&xml).unwrap();
4775 let resources = crate::policy::ResourcePolicy {
4776 max_xpath_namespace_bindings: 1,
4777 ..crate::policy::ResourcePolicy::default()
4778 };
4779 let mut budget = XPathSignatureParseBudget::from_resources(&resources);
4780
4781 let transforms = parse_transforms_with_budget(doc.root_element(), &mut budget)
4782 .expect("each XPath independently satisfies the one-binding ceiling");
4783
4784 assert_eq!(transforms.len(), 2);
4785 }
4786
4787 #[test]
4788 fn parse_transforms_applies_namespace_storage_limit_per_expression() {
4789 let declarations = (0..32)
4792 .map(|index| {
4793 format!(
4794 "xmlns:n{index}=\"urn:namespace:{index}:{}\"",
4795 "x".repeat(64)
4796 )
4797 })
4798 .collect::<Vec<_>>()
4799 .join(" ");
4800 let filters = (0..MAX_XPATH_FILTERS)
4801 .map(|_| {
4802 format!(
4803 r#"<XPath xmlns="{XPATH_FILTER2_TRANSFORM_URI}" Filter="intersect">true()</XPath>"#
4804 )
4805 })
4806 .collect::<String>();
4807 let xml = format!(
4808 r#"<Transforms xmlns="{XMLDSIG_NS}" {declarations}><Transform Algorithm="{XPATH_FILTER2_TRANSFORM_URI}">{filters}</Transform></Transforms>"#
4809 );
4810 let doc = Document::parse(&xml).unwrap();
4811
4812 let transforms = parse_transforms(doc.root_element())
4813 .expect("each expression remains below its namespace storage ceiling");
4814
4815 assert!(matches!(
4816 transforms.as_slice(),
4817 [Transform::XPathFilter2(filters)] if filters.len() == MAX_XPATH_FILTERS
4818 ));
4819 }
4820
4821 #[test]
4822 fn parse_transforms_rejects_xpath_in_wrong_namespace() {
4823 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4824 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
4825 <foo:XPath xmlns:foo="http://example.com/ns">
4826 not(ancestor-or-self::dsig:Signature)
4827 </foo:XPath>
4828 </Transform>
4829 </Transforms>"#;
4830 let doc = Document::parse(xml).unwrap();
4831
4832 let result = parse_transforms(doc.root_element());
4833 assert!(result.is_err());
4834 assert!(matches!(result.unwrap_err(), TransformError::XPath(_)));
4835 }
4836
4837 #[test]
4838 fn parse_transforms_preserves_nonstandard_prefix_bindings() {
4839 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4843 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
4844 <XPath xmlns:dsig="http://example.com/not-xmldsig">
4845 not(ancestor-or-self::dsig:Signature)
4846 </XPath>
4847 </Transform>
4848 </Transforms>"#;
4849 let doc = Document::parse(xml).unwrap();
4850
4851 let result = parse_transforms(doc.root_element()).unwrap();
4852 let [Transform::XPath(xpath)] = result.as_slice() else {
4853 panic!("expected general XPath transform");
4854 };
4855 assert_eq!(
4856 xpath.namespaces().get("dsig").map(String::as_str),
4857 Some("http://example.com/not-xmldsig")
4858 );
4859 }
4860
4861 #[test]
4862 fn parse_transforms_rejects_xpath_with_internal_whitespace_mutation() {
4863 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4864 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
4865 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
4866 not(ancestor-or-self::dsig:Signa ture)
4867 </XPath>
4868 </Transform>
4869 </Transforms>"#;
4870 let doc = Document::parse(xml).unwrap();
4871
4872 let result = parse_transforms(doc.root_element());
4873 assert!(matches!(result.unwrap_err(), TransformError::XPath(_)));
4874 }
4875
4876 #[test]
4877 fn parse_transforms_rejects_multiple_xpath_children() {
4878 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4879 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
4880 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
4881 not(ancestor-or-self::dsig:Signature)
4882 </XPath>
4883 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
4884 not(ancestor-or-self::dsig:Signature)
4885 </XPath>
4886 </Transform>
4887 </Transforms>"#;
4888 let doc = Document::parse(xml).unwrap();
4889
4890 let result = parse_transforms(doc.root_element());
4891 assert!(result.is_err());
4892 assert!(matches!(result.unwrap_err(), TransformError::XPath(_)));
4893 }
4894
4895 #[test]
4896 fn parse_transforms_rejects_non_xpath_element_children() {
4897 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
4898 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
4899 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
4900 not(ancestor-or-self::dsig:Signature)
4901 </XPath>
4902 <Extra/>
4903 </Transform>
4904 </Transforms>"#;
4905 let doc = Document::parse(xml).unwrap();
4906
4907 let result = parse_transforms(doc.root_element());
4908 assert!(result.is_err());
4909 assert!(matches!(result.unwrap_err(), TransformError::XPath(_)));
4910 }
4911
4912 #[test]
4913 fn parse_transforms_rejects_malformed_xpath_filter2_parameters() {
4914 for parameter in [
4918 r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2">//Data</XPath>"#,
4919 r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="exclude">//Data</XPath>"#,
4920 r#"<XPath xmlns="urn:wrong" Filter="intersect">//Data</XPath>"#,
4921 r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect" Extra="value">//Data</XPath>"#,
4922 ] {
4923 let xml = format!(
4924 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{XPATH_FILTER2_TRANSFORM_URI}">{parameter}</Transform></Transforms>"#
4925 );
4926 let doc = Document::parse(&xml).unwrap();
4927
4928 let result = parse_transforms(doc.root_element());
4929
4930 assert!(matches!(result, Err(TransformError::XPath(_))));
4931 }
4932 }
4933
4934 #[test]
4935 fn parse_transforms_rejects_empty_xpath_filter2_sequence() {
4936 let xml = format!(
4939 r#"<Transforms xmlns="{XMLDSIG_NS}"><Transform Algorithm="{XPATH_FILTER2_TRANSFORM_URI}"/></Transforms>"#
4940 );
4941 let doc = Document::parse(&xml).unwrap();
4942
4943 let result = parse_transforms(doc.root_element());
4944
4945 assert!(matches!(result, Err(TransformError::XPath(_))));
4946 }
4947
4948 #[test]
4949 fn parse_transform_chain_hashes_xpath_document_once() {
4950 let filters = format!(
4953 r#"<XPath xmlns="{XPATH_FILTER2_TRANSFORM_URI}" Filter="intersect">true()</XPath>"#
4954 )
4955 .repeat(MAX_XPATH_FILTERS);
4956 let transform = format!(
4957 r#"<Transform Algorithm="{XPATH_FILTER2_TRANSFORM_URI}">{filters}</Transform>"#
4958 );
4959 let xml =
4960 format!(r#"<Transforms xmlns="{XMLDSIG_NS}">{transform}{transform}</Transforms>"#);
4961 let document = Document::parse(&xml).unwrap();
4962
4963 XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(|count| count.set(0));
4964 let transforms = parse_transforms(document.root_element()).unwrap();
4965 let computations = XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(Cell::get);
4966
4967 assert_eq!(transforms.len(), 2);
4968 assert!(transforms.iter().all(
4969 |transform| matches!(transform, Transform::XPathFilter2(filters) if filters.len() == MAX_XPATH_FILTERS)
4970 ));
4971 assert_eq!(
4972 computations, 1,
4973 "one parsed transform chain must hash its source document once"
4974 );
4975 }
4976
4977 #[test]
4978 fn xpath_compat_excludes_other_signature_subtrees_too() {
4979 let xml = r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
4980 <payload>keep-me</payload>
4981 <ds:Signature Id="sig-1">
4982 <ds:SignedInfo/>
4983 <ds:SignatureValue>one</ds:SignatureValue>
4984 </ds:Signature>
4985 <ds:Signature Id="sig-2">
4986 <ds:SignedInfo/>
4987 <ds:SignatureValue>two</ds:SignatureValue>
4988 </ds:Signature>
4989 </root>"#;
4990 let doc = Document::parse(xml).unwrap();
4991 let signature_nodes: Vec<_> = doc
4992 .descendants()
4993 .filter(|node| {
4994 node.is_element()
4995 && node.tag_name().name() == "Signature"
4996 && node.tag_name().namespace() == Some(XMLDSIG_NS)
4997 })
4998 .collect();
4999 let sig_node = signature_nodes[0];
5000
5001 let enveloped = execute_transforms(
5002 sig_node,
5003 TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc).unwrap()),
5004 &[
5005 Transform::Enveloped,
5006 Transform::C14n(C14nAlgorithm::new(
5007 crate::c14n::C14nMode::Inclusive1_0,
5008 false,
5009 )),
5010 ],
5011 )
5012 .unwrap();
5013 let xpath_compat = execute_transforms(
5014 sig_node,
5015 TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc).unwrap()),
5016 &[
5017 Transform::XpathExcludeAllSignatures,
5018 Transform::C14n(C14nAlgorithm::new(
5019 crate::c14n::C14nMode::Inclusive1_0,
5020 false,
5021 )),
5022 ],
5023 )
5024 .unwrap();
5025
5026 let enveloped = String::from_utf8(enveloped).unwrap();
5027 let xpath_compat = String::from_utf8(xpath_compat).unwrap();
5028
5029 assert!(enveloped.contains("sig-2"));
5030 assert!(!xpath_compat.contains("sig-1"));
5031 assert!(!xpath_compat.contains("sig-2"));
5032 assert!(xpath_compat.contains("keep-me"));
5033 }
5034
5035 #[test]
5036 fn parse_transforms_inclusive_c14n_variants() {
5037 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
5038 <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
5039 <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/>
5040 <Transform Algorithm="http://www.w3.org/2006/12/xml-c14n11"/>
5041 </Transforms>"#;
5042 let doc = Document::parse(xml).unwrap();
5043
5044 let chain = parse_transforms(doc.root_element()).unwrap();
5045 assert_eq!(chain.len(), 3);
5046 for t in &chain {
5048 assert!(matches!(t, Transform::C14n(_)));
5049 }
5050 }
5051
5052 #[test]
5053 fn parsed_xpath_rejects_node_id_collision_from_another_document() {
5054 let source = Document::parse(
5058 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>"#,
5059 )
5060 .unwrap();
5061 let transforms_node = source
5062 .descendants()
5063 .find(|node| node.has_tag_name((XMLDSIG_NS, "Transforms")))
5064 .unwrap();
5065 let transforms = parse_transforms(transforms_node).unwrap();
5066
5067 let target = Document::parse(
5068 "<root><container><parameter><unrelated/></parameter></container></root>",
5069 )
5070 .unwrap();
5071 let error = execute_transforms(
5072 target.root_element(),
5073 TransformData::NodeSet(NodeSet::entire_document_without_comments(&target).unwrap()),
5074 &transforms,
5075 )
5076 .expect_err("parsed here() provenance must reject another XML document");
5077
5078 assert!(
5079 matches!(error, TransformError::XPath(ref message) if message.contains("same XML document"))
5080 );
5081 }
5082
5083 #[test]
5084 fn transform_chain_computes_document_identity_once() {
5085 let document = Document::parse(
5088 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>"#,
5089 )
5090 .unwrap();
5091 let transforms_node = document
5092 .descendants()
5093 .find(|node| node.has_tag_name((XMLDSIG_NS, "Transforms")))
5094 .unwrap();
5095 let transforms = parse_transforms(transforms_node).unwrap();
5096 let signature = document
5097 .descendants()
5098 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
5099 .unwrap();
5100 let initial = NodeSet::entire_document_without_comments(&document)
5101 .map(TransformData::NodeSet)
5102 .unwrap();
5103
5104 XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(|count| count.set(0));
5105 execute_transforms(signature, initial, &transforms).unwrap();
5106 let computations = XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(Cell::get);
5107
5108 assert_eq!(
5109 computations, 1,
5110 "one live document must be hashed once per chain"
5111 );
5112 }
5113
5114 #[test]
5115 fn transform_cache_identity_does_not_cross_chain_boundaries() {
5116 let document = Document::parse(
5119 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>"#,
5120 )
5121 .unwrap();
5122 let transforms_node = document
5123 .descendants()
5124 .find(|node| node.has_tag_name((XMLDSIG_NS, "Transforms")))
5125 .unwrap();
5126 let transforms = parse_transforms(transforms_node).unwrap();
5127 let signature = document
5128 .descendants()
5129 .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
5130 .unwrap();
5131 let budget = TransformExecutionBudget::default();
5132
5133 XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(|count| count.set(0));
5134 for _ in 0..2 {
5135 let initial = NodeSet::entire_document_without_comments(&document)
5136 .map(TransformData::NodeSet)
5137 .unwrap();
5138 execute_transforms_with_options_and_budget(
5139 signature,
5140 initial,
5141 &transforms,
5142 TransformOptions::default(),
5143 &budget,
5144 )
5145 .unwrap();
5146 }
5147 let computations = XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(Cell::get);
5148
5149 assert_eq!(
5150 computations, 2,
5151 "each transform chain must establish a fresh document identity"
5152 );
5153 }
5154
5155 #[test]
5156 fn transform_chain_state_keys_identity_by_document() {
5157 let first_document = Document::parse("<first/>").unwrap();
5160 let second_document = Document::parse("<second/>").unwrap();
5161 let state = TransformChainState::default();
5162
5163 XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(|count| count.set(0));
5164 let first_identity = state.xpath_document_identity(&first_document);
5165 let second_identity = state.xpath_document_identity(&second_document);
5166 let computations = XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(Cell::get);
5167
5168 assert_ne!(first_identity, second_identity);
5169 assert_eq!(
5170 computations, 2,
5171 "each distinct live document must receive its own cached identity"
5172 );
5173 }
5174
5175 #[test]
5176 fn template_xpath_skips_document_identity_hash() {
5177 let document = Document::parse("<root><value/></root>").unwrap();
5180 let transforms = [
5181 Transform::XPath(XPathExpression::new("true()")),
5182 Transform::XPath(XPathExpression::new("true()")),
5183 ];
5184 let initial = NodeSet::entire_document_without_comments(&document)
5185 .map(TransformData::NodeSet)
5186 .unwrap();
5187
5188 XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(|count| count.set(0));
5189 execute_transforms(document.root_element(), initial, &transforms).unwrap();
5190 let computations = XPATH_DOCUMENT_IDENTITY_COMPUTATIONS.with(Cell::get);
5191
5192 assert_eq!(
5193 computations, 0,
5194 "XPath without parsed here() provenance must not hash XML"
5195 );
5196 }
5197
5198 #[test]
5201 fn saml_enveloped_signature_full_pipeline() {
5202 let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
5204 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
5205 ID="_resp1">
5206 <saml:Assertion ID="_assert1">
5207 <saml:Subject>user@example.com</saml:Subject>
5208 </saml:Assertion>
5209 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5210 <ds:SignedInfo>
5211 <ds:Reference URI="">
5212 <ds:Transforms>
5213 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
5214 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5215 </ds:Transforms>
5216 </ds:Reference>
5217 </ds:SignedInfo>
5218 <ds:SignatureValue>fakesig==</ds:SignatureValue>
5219 </ds:Signature>
5220 </samlp:Response>"#;
5221 let doc = Document::parse(xml).unwrap();
5222
5223 let sig_node = doc
5225 .descendants()
5226 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
5227 .unwrap();
5228
5229 let reference = doc
5231 .descendants()
5232 .find(|n| n.is_element() && n.tag_name().name() == "Reference")
5233 .unwrap();
5234 let transforms_elem = reference
5235 .children()
5236 .find(|n| n.is_element() && n.tag_name().name() == "Transforms")
5237 .unwrap();
5238 let transforms = parse_transforms(transforms_elem).unwrap();
5239 assert_eq!(transforms.len(), 2);
5240
5241 let initial =
5243 TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc).unwrap());
5244 let result = execute_transforms(sig_node, initial, &transforms).unwrap();
5245
5246 let output = String::from_utf8(result).unwrap();
5247
5248 assert!(!output.contains("Signature"), "Signature should be removed");
5250 assert!(
5251 !output.contains("SignedInfo"),
5252 "SignedInfo should be removed"
5253 );
5254 assert!(
5255 !output.contains("SignatureValue"),
5256 "SignatureValue should be removed"
5257 );
5258 assert!(
5259 !output.contains("fakesig"),
5260 "signature value should be removed"
5261 );
5262
5263 assert!(output.contains("samlp:Response"));
5265 assert!(output.contains("saml:Assertion"));
5266 assert!(output.contains("user@example.com"));
5267 }
5268}