1use roxmltree::Node;
21
22use super::parse::XMLDSIG_NS;
23use super::types::{TransformData, TransformError};
24use crate::c14n::{self, C14nAlgorithm};
25
26pub const ENVELOPED_SIGNATURE_URI: &str = "http://www.w3.org/2000/09/xmldsig#enveloped-signature";
28pub const XPATH_TRANSFORM_URI: &str = "http://www.w3.org/TR/1999/REC-xpath-19991116";
30pub const DEFAULT_IMPLICIT_C14N_URI: &str = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
33const ENVELOPED_SIGNATURE_XPATH_EXPR: &str = "not(ancestor-or-self::dsig:Signature)";
36
37const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
39
40#[derive(Debug, Clone)]
42pub enum Transform {
43 Enveloped,
48
49 XpathExcludeAllSignatures,
55
56 C14n(C14nAlgorithm),
60}
61
62pub(crate) fn apply_transform<'a>(
70 signature_node: Node<'a, 'a>,
71 transform: &Transform,
72 input: TransformData<'a>,
73) -> Result<TransformData<'a>, TransformError> {
74 match transform {
75 Transform::Enveloped => {
76 let mut nodes = input.into_node_set()?;
77 if !std::ptr::eq(signature_node.document(), nodes.document()) {
86 return Err(TransformError::CrossDocumentSignatureNode);
87 }
88 nodes.exclude_subtree(signature_node);
89 Ok(TransformData::NodeSet(nodes))
90 }
91 Transform::XpathExcludeAllSignatures => {
92 let mut nodes = input.into_node_set()?;
93 let doc = nodes.document();
94
95 for node in doc.descendants().filter(|node| {
96 node.is_element()
97 && node.tag_name().name() == "Signature"
98 && node.tag_name().namespace() == Some(XMLDSIG_NS)
99 }) {
100 nodes.exclude_subtree(node);
101 }
102
103 Ok(TransformData::NodeSet(nodes))
104 }
105 Transform::C14n(algo) => {
106 let nodes = input.into_node_set()?;
107 let mut output = Vec::new();
108 let predicate = |node: Node| nodes.contains(node);
109 c14n::canonicalize(nodes.document(), Some(&predicate), algo, &mut output)
110 .map_err(|e| TransformError::C14n(e.to_string()))?;
111 Ok(TransformData::Binary(output))
112 }
113 }
114}
115
116pub fn execute_transforms<'a>(
125 signature_node: Node<'a, 'a>,
126 initial_data: TransformData<'a>,
127 transforms: &[Transform],
128) -> Result<Vec<u8>, TransformError> {
129 let mut data = initial_data;
130
131 for transform in transforms {
132 data = apply_transform(signature_node, transform, data)?;
133 }
134
135 match data {
138 TransformData::Binary(bytes) => Ok(bytes),
139 TransformData::NodeSet(nodes) => {
140 #[expect(clippy::expect_used, reason = "hardcoded URI is a known constant")]
141 let algo = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI)
142 .expect("default C14N algorithm URI must be supported by C14nAlgorithm::from_uri");
143 let mut output = Vec::new();
144 let predicate = |node: Node| nodes.contains(node);
145 c14n::canonicalize(nodes.document(), Some(&predicate), &algo, &mut output)
146 .map_err(|e| TransformError::C14n(e.to_string()))?;
147 Ok(output)
148 }
149 }
150}
151
152pub fn parse_transforms(transforms_node: Node) -> Result<Vec<Transform>, TransformError> {
161 if !transforms_node.is_element() {
163 return Err(TransformError::UnsupportedTransform(
164 "expected <Transforms> element but got non-element node".into(),
165 ));
166 }
167 let transforms_tag = transforms_node.tag_name();
168 if transforms_tag.name() != "Transforms" || transforms_tag.namespace() != Some(XMLDSIG_NS) {
169 return Err(TransformError::UnsupportedTransform(
170 "expected <ds:Transforms> element in XMLDSig namespace".into(),
171 ));
172 }
173
174 let mut chain = Vec::new();
175
176 for child in transforms_node.children() {
177 if !child.is_element() {
178 continue;
179 }
180
181 let tag = child.tag_name();
183 if tag.name() != "Transform" || tag.namespace() != Some(XMLDSIG_NS) {
184 return Err(TransformError::UnsupportedTransform(
185 "unexpected child element of <ds:Transforms>; only <ds:Transform> is allowed"
186 .into(),
187 ));
188 }
189 let uri = child.attribute("Algorithm").ok_or_else(|| {
190 TransformError::UnsupportedTransform(
191 "missing Algorithm attribute on <Transform>".into(),
192 )
193 })?;
194
195 let transform = if uri == ENVELOPED_SIGNATURE_URI {
196 Transform::Enveloped
197 } else if uri == XPATH_TRANSFORM_URI {
198 parse_xpath_compat_transform(child)?
199 } else if let Some(mut algo) = C14nAlgorithm::from_uri(uri) {
200 if algo.mode() == c14n::C14nMode::Exclusive1_0
202 && let Some(prefix_list) = parse_inclusive_prefixes(child)?
203 {
204 algo = algo.with_prefix_list(&prefix_list);
205 }
206 Transform::C14n(algo)
207 } else {
208 return Err(TransformError::UnsupportedTransform(uri.to_string()));
209 };
210 chain.push(transform);
211 }
212
213 Ok(chain)
214}
215
216fn parse_xpath_compat_transform(transform_node: Node) -> Result<Transform, TransformError> {
222 let mut xpath_node = None;
223
224 for child in transform_node.children().filter(|node| node.is_element()) {
225 let tag = child.tag_name();
226 if tag.name() == "XPath" && tag.namespace() == Some(XMLDSIG_NS) {
227 if xpath_node.is_some() {
228 return Err(TransformError::UnsupportedTransform(
229 "XPath transform must contain exactly one XMLDSig <XPath> child element".into(),
230 ));
231 }
232 xpath_node = Some(child);
233 } else {
234 return Err(TransformError::UnsupportedTransform(
235 "XPath transform allows only a single XMLDSig <XPath> child element".into(),
236 ));
237 }
238 }
239
240 let xpath_node = xpath_node.ok_or_else(|| {
241 TransformError::UnsupportedTransform(
242 "XPath transform requires a single XMLDSig <XPath> child element".into(),
243 )
244 })?;
245
246 let expr = xpath_node
247 .text()
248 .map(|text| text.trim().to_string())
249 .unwrap_or_default();
250
251 if expr == ENVELOPED_SIGNATURE_XPATH_EXPR {
252 let dsig_ns = xpath_node.lookup_namespace_uri(Some("dsig"));
253 if dsig_ns == Some(XMLDSIG_NS) {
254 Ok(Transform::XpathExcludeAllSignatures)
255 } else {
256 Err(TransformError::UnsupportedTransform(
257 "XPath compatibility form requires the `dsig` prefix to be bound to the XMLDSig namespace"
258 .into(),
259 ))
260 }
261 } else {
262 Err(TransformError::UnsupportedTransform(
263 "unsupported XPath expression in compatibility transform; only `not(ancestor-or-self::dsig:Signature)` is supported"
264 .into(),
265 ))
266 }
267}
268
269fn parse_inclusive_prefixes(transform_node: Node) -> Result<Option<String>, TransformError> {
287 for child in transform_node.children() {
288 if child.is_element() {
289 let tag = child.tag_name();
290 if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
291 {
292 let prefix_list = child.attribute("PrefixList").ok_or_else(|| {
293 TransformError::UnsupportedTransform(
294 "missing PrefixList attribute on <InclusiveNamespaces>".into(),
295 )
296 })?;
297 return Ok(Some(prefix_list.to_string()));
298 }
299 }
300 }
301 Ok(None)
302}
303
304#[cfg(test)]
305#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
306mod tests {
307 use super::*;
308 use crate::xmldsig::NodeSet;
309 use roxmltree::Document;
310
311 #[test]
314 fn enveloped_excludes_signature_subtree() {
315 let xml = r#"<root>
317 <data>hello</data>
318 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
319 <SignedInfo><Reference URI=""/></SignedInfo>
320 <SignatureValue>abc</SignatureValue>
321 </Signature>
322 </root>"#;
323 let doc = Document::parse(xml).unwrap();
324
325 let sig_node = doc
327 .descendants()
328 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
329 .unwrap();
330
331 let node_set = NodeSet::entire_document_without_comments(&doc);
333 let data = TransformData::NodeSet(node_set);
334
335 let result = apply_transform(sig_node, &Transform::Enveloped, data).unwrap();
337 let node_set = result.into_node_set().unwrap();
338
339 assert!(node_set.contains(doc.root_element()));
341 let data_elem = doc
342 .descendants()
343 .find(|n| n.is_element() && n.tag_name().name() == "data")
344 .unwrap();
345 assert!(node_set.contains(data_elem));
346
347 assert!(
349 !node_set.contains(sig_node),
350 "Signature element should be excluded"
351 );
352 let signed_info = doc
353 .descendants()
354 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
355 .unwrap();
356 assert!(
357 !node_set.contains(signed_info),
358 "SignedInfo (child of Signature) should be excluded"
359 );
360 }
361
362 #[test]
363 fn enveloped_requires_node_set_input() {
364 let xml = "<root/>";
365 let doc = Document::parse(xml).unwrap();
366 let data = TransformData::Binary(vec![1, 2, 3]);
368 let result = apply_transform(doc.root_element(), &Transform::Enveloped, data);
369 assert!(result.is_err());
370 match result.unwrap_err() {
371 TransformError::TypeMismatch { expected, got } => {
372 assert_eq!(expected, "NodeSet");
373 assert_eq!(got, "Binary");
374 }
375 other => panic!("expected TypeMismatch, got: {other:?}"),
376 }
377 }
378
379 #[test]
380 fn enveloped_rejects_cross_document_signature_node() {
381 let xml = r#"<Root><Signature Id="sig"/></Root>"#;
384 let doc1 = Document::parse(xml).unwrap();
385 let doc2 = Document::parse(xml).unwrap();
386
387 let node_set = NodeSet::entire_document_without_comments(&doc1);
389 let input = TransformData::NodeSet(node_set);
390 let sig_from_doc2 = doc2
391 .descendants()
392 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
393 .unwrap();
394
395 let result = apply_transform(sig_from_doc2, &Transform::Enveloped, input);
396 assert!(matches!(
397 result,
398 Err(TransformError::CrossDocumentSignatureNode)
399 ));
400 }
401
402 #[test]
405 fn c14n_transform_produces_bytes() {
406 let xml = r#"<root b="2" a="1"><child/></root>"#;
407 let doc = Document::parse(xml).unwrap();
408
409 let node_set = NodeSet::entire_document_without_comments(&doc);
410 let data = TransformData::NodeSet(node_set);
411
412 let algo =
413 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
414 let result = apply_transform(doc.root_element(), &Transform::C14n(algo), data).unwrap();
415
416 let bytes = result.into_binary().unwrap();
417 let output = String::from_utf8(bytes).unwrap();
418 assert_eq!(output, r#"<root a="1" b="2"><child></child></root>"#);
420 }
421
422 #[test]
423 fn c14n_transform_requires_node_set() {
424 let xml = "<root/>";
425 let doc = Document::parse(xml).unwrap();
426
427 let algo =
428 C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
429 let data = TransformData::Binary(vec![1, 2, 3]);
430 let result = apply_transform(doc.root_element(), &Transform::C14n(algo), data);
431
432 assert!(result.is_err());
433 assert!(matches!(
434 result.unwrap_err(),
435 TransformError::TypeMismatch { .. }
436 ));
437 }
438
439 #[test]
442 fn pipeline_enveloped_then_c14n() {
443 let xml = r#"<root xmlns:ns="http://example.com" b="2" a="1">
445 <data>hello</data>
446 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
447 <SignedInfo/>
448 <SignatureValue>abc</SignatureValue>
449 </Signature>
450 </root>"#;
451 let doc = Document::parse(xml).unwrap();
452
453 let sig_node = doc
454 .descendants()
455 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
456 .unwrap();
457
458 let initial = TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc));
459 let transforms = vec![
460 Transform::Enveloped,
461 Transform::C14n(
462 C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#").unwrap(),
463 ),
464 ];
465
466 let result = execute_transforms(sig_node, initial, &transforms).unwrap();
467
468 let output = String::from_utf8(result).unwrap();
469 assert!(!output.contains("Signature"));
471 assert!(!output.contains("SignedInfo"));
472 assert!(!output.contains("SignatureValue"));
473 assert!(output.contains("<data>hello</data>"));
474 }
475
476 #[test]
477 fn pipeline_no_transforms_applies_default_c14n() {
478 let xml = r#"<root b="2" a="1"><child/></root>"#;
480 let doc = Document::parse(xml).unwrap();
481
482 let initial = TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc));
483 let result = execute_transforms(doc.root_element(), initial, &[]).unwrap();
484
485 let output = String::from_utf8(result).unwrap();
486 assert_eq!(output, r#"<root a="1" b="2"><child></child></root>"#);
487 }
488
489 #[test]
490 fn pipeline_binary_passthrough() {
491 let xml = "<root/>";
494 let doc = Document::parse(xml).unwrap();
495
496 let initial = TransformData::Binary(b"raw bytes".to_vec());
497 let result = execute_transforms(doc.root_element(), initial, &[]).unwrap();
498
499 assert_eq!(result, b"raw bytes");
500 }
501
502 #[test]
505 fn enveloped_only_excludes_own_signature() {
506 let xml = r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
509 <data>hello</data>
510 <ds:Signature Id="sig-other">
511 <ds:SignedInfo><ds:Reference URI=""/></ds:SignedInfo>
512 </ds:Signature>
513 <ds:Signature Id="sig-target">
514 <ds:SignedInfo><ds:Reference URI=""/></ds:SignedInfo>
515 </ds:Signature>
516 </root>"#;
517 let doc = Document::parse(xml).unwrap();
518
519 let sig_node = doc
521 .descendants()
522 .find(|n| n.is_element() && n.attribute("Id") == Some("sig-target"))
523 .unwrap();
524
525 let node_set = NodeSet::entire_document_without_comments(&doc);
526 let data = TransformData::NodeSet(node_set);
527
528 let result = apply_transform(sig_node, &Transform::Enveloped, data).unwrap();
529 let node_set = result.into_node_set().unwrap();
530
531 let sig_other = doc
533 .descendants()
534 .find(|n| n.is_element() && n.attribute("Id") == Some("sig-other"))
535 .unwrap();
536 assert!(
537 node_set.contains(sig_other),
538 "other Signature elements should NOT be excluded"
539 );
540
541 assert!(
543 !node_set.contains(sig_node),
544 "the specific Signature being verified should be excluded"
545 );
546 }
547
548 #[test]
551 fn parse_transforms_enveloped_and_exc_c14n() {
552 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
553 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
554 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
555 </Transforms>"#;
556 let doc = Document::parse(xml).unwrap();
557 let transforms_node = doc.root_element();
558
559 let chain = parse_transforms(transforms_node).unwrap();
560 assert_eq!(chain.len(), 2);
561 assert!(matches!(chain[0], Transform::Enveloped));
562 assert!(matches!(chain[1], Transform::C14n(_)));
563 }
564
565 #[test]
566 fn parse_transforms_with_inclusive_prefixes() {
567 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#"
568 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
569 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
570 <ec:InclusiveNamespaces PrefixList="ds saml #default"/>
571 </Transform>
572 </Transforms>"#;
573 let doc = Document::parse(xml).unwrap();
574 let transforms_node = doc.root_element();
575
576 let chain = parse_transforms(transforms_node).unwrap();
577 assert_eq!(chain.len(), 1);
578 match &chain[0] {
579 Transform::C14n(algo) => {
580 assert!(algo.inclusive_prefixes().contains("ds"));
581 assert!(algo.inclusive_prefixes().contains("saml"));
582 assert!(algo.inclusive_prefixes().contains("")); }
584 other => panic!("expected C14n, got: {other:?}"),
585 }
586 }
587
588 #[test]
589 fn parse_transforms_ignores_wrong_ns_inclusive_namespaces() {
590 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
593 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
594 <InclusiveNamespaces xmlns="http://example.com/fake"
595 PrefixList="attacker-controlled"/>
596 </Transform>
597 </Transforms>"#;
598 let doc = Document::parse(xml).unwrap();
599
600 let chain = parse_transforms(doc.root_element()).unwrap();
601 assert_eq!(chain.len(), 1);
602 match &chain[0] {
603 Transform::C14n(algo) => {
604 assert!(
606 algo.inclusive_prefixes().is_empty(),
607 "should ignore InclusiveNamespaces in wrong namespace"
608 );
609 }
610 other => panic!("expected C14n, got: {other:?}"),
611 }
612 }
613
614 #[test]
615 fn parse_transforms_missing_prefix_list_is_error() {
616 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#"
619 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
620 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
621 <ec:InclusiveNamespaces/>
622 </Transform>
623 </Transforms>"#;
624 let doc = Document::parse(xml).unwrap();
625
626 let result = parse_transforms(doc.root_element());
627 assert!(result.is_err());
628 assert!(matches!(
629 result.unwrap_err(),
630 TransformError::UnsupportedTransform(_)
631 ));
632 }
633
634 #[test]
635 fn parse_transforms_unsupported_algorithm() {
636 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
637 <Transform Algorithm="http://example.com/unknown"/>
638 </Transforms>"#;
639 let doc = Document::parse(xml).unwrap();
640
641 let result = parse_transforms(doc.root_element());
642 assert!(result.is_err());
643 assert!(matches!(
644 result.unwrap_err(),
645 TransformError::UnsupportedTransform(_)
646 ));
647 }
648
649 #[test]
650 fn parse_transforms_missing_algorithm() {
651 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
652 <Transform/>
653 </Transforms>"#;
654 let doc = Document::parse(xml).unwrap();
655
656 let result = parse_transforms(doc.root_element());
657 assert!(result.is_err());
658 assert!(matches!(
659 result.unwrap_err(),
660 TransformError::UnsupportedTransform(_)
661 ));
662 }
663
664 #[test]
665 fn parse_transforms_empty() {
666 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#"/>"#;
667 let doc = Document::parse(xml).unwrap();
668
669 let chain = parse_transforms(doc.root_element()).unwrap();
670 assert!(chain.is_empty());
671 }
672
673 #[test]
674 fn parse_transforms_accepts_enveloped_compat_xpath() {
675 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
676 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
677 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
678 not(ancestor-or-self::dsig:Signature)
679 </XPath>
680 </Transform>
681 </Transforms>"#;
682 let doc = Document::parse(xml).unwrap();
683
684 let chain = parse_transforms(doc.root_element()).unwrap();
685 assert_eq!(chain.len(), 1);
686 assert!(matches!(chain[0], Transform::XpathExcludeAllSignatures));
687 }
688
689 #[test]
690 fn parse_transforms_rejects_other_xpath_expressions() {
691 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
692 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
693 <XPath>self::node()</XPath>
694 </Transform>
695 </Transforms>"#;
696 let doc = Document::parse(xml).unwrap();
697
698 let result = parse_transforms(doc.root_element());
699 assert!(result.is_err());
700 assert!(matches!(
701 result.unwrap_err(),
702 TransformError::UnsupportedTransform(_)
703 ));
704 }
705
706 #[test]
707 fn parse_transforms_rejects_xpath_in_wrong_namespace() {
708 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
709 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
710 <foo:XPath xmlns:foo="http://example.com/ns">
711 not(ancestor-or-self::dsig:Signature)
712 </foo:XPath>
713 </Transform>
714 </Transforms>"#;
715 let doc = Document::parse(xml).unwrap();
716
717 let result = parse_transforms(doc.root_element());
718 assert!(result.is_err());
719 assert!(matches!(
720 result.unwrap_err(),
721 TransformError::UnsupportedTransform(_)
722 ));
723 }
724
725 #[test]
726 fn parse_transforms_rejects_xpath_with_wrong_dsig_prefix_binding() {
727 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
728 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
729 <XPath xmlns:dsig="http://example.com/not-xmldsig">
730 not(ancestor-or-self::dsig:Signature)
731 </XPath>
732 </Transform>
733 </Transforms>"#;
734 let doc = Document::parse(xml).unwrap();
735
736 let result = parse_transforms(doc.root_element());
737 assert!(result.is_err());
738 assert!(matches!(
739 result.unwrap_err(),
740 TransformError::UnsupportedTransform(_)
741 ));
742 }
743
744 #[test]
745 fn parse_transforms_rejects_xpath_with_internal_whitespace_mutation() {
746 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
747 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
748 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
749 not(ancestor-or-self::dsig:Signa ture)
750 </XPath>
751 </Transform>
752 </Transforms>"#;
753 let doc = Document::parse(xml).unwrap();
754
755 let result = parse_transforms(doc.root_element());
756 assert!(matches!(
757 result.unwrap_err(),
758 TransformError::UnsupportedTransform(_)
759 ));
760 }
761
762 #[test]
763 fn parse_transforms_rejects_multiple_xpath_children() {
764 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
765 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
766 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
767 not(ancestor-or-self::dsig:Signature)
768 </XPath>
769 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
770 not(ancestor-or-self::dsig:Signature)
771 </XPath>
772 </Transform>
773 </Transforms>"#;
774 let doc = Document::parse(xml).unwrap();
775
776 let result = parse_transforms(doc.root_element());
777 assert!(result.is_err());
778 assert!(matches!(
779 result.unwrap_err(),
780 TransformError::UnsupportedTransform(_)
781 ));
782 }
783
784 #[test]
785 fn parse_transforms_rejects_non_xpath_element_children() {
786 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
787 <Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
788 <XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
789 not(ancestor-or-self::dsig:Signature)
790 </XPath>
791 <Extra/>
792 </Transform>
793 </Transforms>"#;
794 let doc = Document::parse(xml).unwrap();
795
796 let result = parse_transforms(doc.root_element());
797 assert!(result.is_err());
798 assert!(matches!(
799 result.unwrap_err(),
800 TransformError::UnsupportedTransform(_)
801 ));
802 }
803
804 #[test]
805 fn xpath_compat_excludes_other_signature_subtrees_too() {
806 let xml = r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
807 <payload>keep-me</payload>
808 <ds:Signature Id="sig-1">
809 <ds:SignedInfo/>
810 <ds:SignatureValue>one</ds:SignatureValue>
811 </ds:Signature>
812 <ds:Signature Id="sig-2">
813 <ds:SignedInfo/>
814 <ds:SignatureValue>two</ds:SignatureValue>
815 </ds:Signature>
816 </root>"#;
817 let doc = Document::parse(xml).unwrap();
818 let signature_nodes: Vec<_> = doc
819 .descendants()
820 .filter(|node| {
821 node.is_element()
822 && node.tag_name().name() == "Signature"
823 && node.tag_name().namespace() == Some(XMLDSIG_NS)
824 })
825 .collect();
826 let sig_node = signature_nodes[0];
827
828 let enveloped = execute_transforms(
829 sig_node,
830 TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc)),
831 &[
832 Transform::Enveloped,
833 Transform::C14n(C14nAlgorithm::new(
834 crate::c14n::C14nMode::Inclusive1_0,
835 false,
836 )),
837 ],
838 )
839 .unwrap();
840 let xpath_compat = execute_transforms(
841 sig_node,
842 TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc)),
843 &[
844 Transform::XpathExcludeAllSignatures,
845 Transform::C14n(C14nAlgorithm::new(
846 crate::c14n::C14nMode::Inclusive1_0,
847 false,
848 )),
849 ],
850 )
851 .unwrap();
852
853 let enveloped = String::from_utf8(enveloped).unwrap();
854 let xpath_compat = String::from_utf8(xpath_compat).unwrap();
855
856 assert!(enveloped.contains("sig-2"));
857 assert!(!xpath_compat.contains("sig-1"));
858 assert!(!xpath_compat.contains("sig-2"));
859 assert!(xpath_compat.contains("keep-me"));
860 }
861
862 #[test]
863 fn parse_transforms_inclusive_c14n_variants() {
864 let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
865 <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
866 <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/>
867 <Transform Algorithm="http://www.w3.org/2006/12/xml-c14n11"/>
868 </Transforms>"#;
869 let doc = Document::parse(xml).unwrap();
870
871 let chain = parse_transforms(doc.root_element()).unwrap();
872 assert_eq!(chain.len(), 3);
873 for t in &chain {
875 assert!(matches!(t, Transform::C14n(_)));
876 }
877 }
878
879 #[test]
882 fn saml_enveloped_signature_full_pipeline() {
883 let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
885 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
886 ID="_resp1">
887 <saml:Assertion ID="_assert1">
888 <saml:Subject>user@example.com</saml:Subject>
889 </saml:Assertion>
890 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
891 <ds:SignedInfo>
892 <ds:Reference URI="">
893 <ds:Transforms>
894 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
895 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
896 </ds:Transforms>
897 </ds:Reference>
898 </ds:SignedInfo>
899 <ds:SignatureValue>fakesig==</ds:SignatureValue>
900 </ds:Signature>
901 </samlp:Response>"#;
902 let doc = Document::parse(xml).unwrap();
903
904 let sig_node = doc
906 .descendants()
907 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
908 .unwrap();
909
910 let reference = doc
912 .descendants()
913 .find(|n| n.is_element() && n.tag_name().name() == "Reference")
914 .unwrap();
915 let transforms_elem = reference
916 .children()
917 .find(|n| n.is_element() && n.tag_name().name() == "Transforms")
918 .unwrap();
919 let transforms = parse_transforms(transforms_elem).unwrap();
920 assert_eq!(transforms.len(), 2);
921
922 let initial = TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc));
924 let result = execute_transforms(sig_node, initial, &transforms).unwrap();
925
926 let output = String::from_utf8(result).unwrap();
927
928 assert!(!output.contains("Signature"), "Signature should be removed");
930 assert!(
931 !output.contains("SignedInfo"),
932 "SignedInfo should be removed"
933 );
934 assert!(
935 !output.contains("SignatureValue"),
936 "SignatureValue should be removed"
937 );
938 assert!(
939 !output.contains("fakesig"),
940 "signature value should be removed"
941 );
942
943 assert!(output.contains("samlp:Response"));
945 assert!(output.contains("saml:Assertion"));
946 assert!(output.contains("user@example.com"));
947 }
948}