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