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