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