use roxmltree::Node;
use super::parse::XMLDSIG_NS;
use super::types::{TransformData, TransformError};
use crate::c14n::{self, C14nAlgorithm};
pub const ENVELOPED_SIGNATURE_URI: &str = "http://www.w3.org/2000/09/xmldsig#enveloped-signature";
pub const XPATH_TRANSFORM_URI: &str = "http://www.w3.org/TR/1999/REC-xpath-19991116";
pub const DEFAULT_IMPLICIT_C14N_URI: &str = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
const ENVELOPED_SIGNATURE_XPATH_EXPR: &str = "not(ancestor-or-self::dsig:Signature)";
const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
#[derive(Debug, Clone)]
pub enum Transform {
Enveloped,
XpathExcludeAllSignatures,
C14n(C14nAlgorithm),
}
pub(crate) fn apply_transform<'a>(
signature_node: Node<'a, 'a>,
transform: &Transform,
input: TransformData<'a>,
) -> Result<TransformData<'a>, TransformError> {
match transform {
Transform::Enveloped => {
let mut nodes = input.into_node_set()?;
if !std::ptr::eq(signature_node.document(), nodes.document()) {
return Err(TransformError::CrossDocumentSignatureNode);
}
nodes.exclude_subtree(signature_node);
Ok(TransformData::NodeSet(nodes))
}
Transform::XpathExcludeAllSignatures => {
let mut nodes = input.into_node_set()?;
let doc = nodes.document();
for node in doc.descendants().filter(|node| {
node.is_element()
&& node.tag_name().name() == "Signature"
&& node.tag_name().namespace() == Some(XMLDSIG_NS)
}) {
nodes.exclude_subtree(node);
}
Ok(TransformData::NodeSet(nodes))
}
Transform::C14n(algo) => {
let nodes = input.into_node_set()?;
let mut output = Vec::new();
let predicate = |node: Node| nodes.contains(node);
c14n::canonicalize(nodes.document(), Some(&predicate), algo, &mut output)?;
Ok(TransformData::Binary(output))
}
}
}
pub fn execute_transforms<'a>(
signature_node: Node<'a, 'a>,
initial_data: TransformData<'a>,
transforms: &[Transform],
) -> Result<Vec<u8>, TransformError> {
let mut data = initial_data;
for transform in transforms {
data = apply_transform(signature_node, transform, data)?;
}
match data {
TransformData::Binary(bytes) => Ok(bytes),
TransformData::NodeSet(nodes) => {
#[expect(clippy::expect_used, reason = "hardcoded URI is a known constant")]
let algo = C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI)
.expect("default C14N algorithm URI must be supported by C14nAlgorithm::from_uri");
let mut output = Vec::new();
let predicate = |node: Node| nodes.contains(node);
c14n::canonicalize(nodes.document(), Some(&predicate), &algo, &mut output)?;
Ok(output)
}
}
}
pub fn parse_transforms(transforms_node: Node) -> Result<Vec<Transform>, TransformError> {
if !transforms_node.is_element() {
return Err(TransformError::UnsupportedTransform(
"expected <Transforms> element but got non-element node".into(),
));
}
let transforms_tag = transforms_node.tag_name();
if transforms_tag.name() != "Transforms" || transforms_tag.namespace() != Some(XMLDSIG_NS) {
return Err(TransformError::UnsupportedTransform(
"expected <ds:Transforms> element in XMLDSig namespace".into(),
));
}
let mut chain = Vec::new();
for child in transforms_node.children() {
if !child.is_element() {
continue;
}
let tag = child.tag_name();
if tag.name() != "Transform" || tag.namespace() != Some(XMLDSIG_NS) {
return Err(TransformError::UnsupportedTransform(
"unexpected child element of <ds:Transforms>; only <ds:Transform> is allowed"
.into(),
));
}
let uri = child.attribute("Algorithm").ok_or_else(|| {
TransformError::UnsupportedTransform(
"missing Algorithm attribute on <Transform>".into(),
)
})?;
let transform = if uri == ENVELOPED_SIGNATURE_URI {
Transform::Enveloped
} else if uri == XPATH_TRANSFORM_URI {
parse_xpath_compat_transform(child)?
} else if let Some(mut algo) = C14nAlgorithm::from_uri(uri) {
if algo.mode() == c14n::C14nMode::Exclusive1_0
&& let Some(prefix_list) = parse_inclusive_prefixes(child)?
{
algo = algo.with_prefix_list(&prefix_list);
}
Transform::C14n(algo)
} else {
return Err(TransformError::UnsupportedTransform(uri.to_string()));
};
chain.push(transform);
}
Ok(chain)
}
fn parse_xpath_compat_transform(transform_node: Node) -> Result<Transform, TransformError> {
let mut xpath_node = None;
for child in transform_node.children().filter(|node| node.is_element()) {
let tag = child.tag_name();
if tag.name() == "XPath" && tag.namespace() == Some(XMLDSIG_NS) {
if xpath_node.is_some() {
return Err(TransformError::UnsupportedTransform(
"XPath transform must contain exactly one XMLDSig <XPath> child element".into(),
));
}
xpath_node = Some(child);
} else {
return Err(TransformError::UnsupportedTransform(
"XPath transform allows only a single XMLDSig <XPath> child element".into(),
));
}
}
let xpath_node = xpath_node.ok_or_else(|| {
TransformError::UnsupportedTransform(
"XPath transform requires a single XMLDSig <XPath> child element".into(),
)
})?;
let expr = xpath_node
.text()
.map(|text| text.trim().to_string())
.unwrap_or_default();
if expr == ENVELOPED_SIGNATURE_XPATH_EXPR {
let dsig_ns = xpath_node.lookup_namespace_uri(Some("dsig"));
if dsig_ns == Some(XMLDSIG_NS) {
Ok(Transform::XpathExcludeAllSignatures)
} else {
Err(TransformError::UnsupportedTransform(
"XPath compatibility form requires the `dsig` prefix to be bound to the XMLDSig namespace"
.into(),
))
}
} else {
Err(TransformError::UnsupportedTransform(
"unsupported XPath expression in compatibility transform; only `not(ancestor-or-self::dsig:Signature)` is supported"
.into(),
))
}
}
fn parse_inclusive_prefixes(transform_node: Node) -> Result<Option<String>, TransformError> {
for child in transform_node.children() {
if child.is_element() {
let tag = child.tag_name();
if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
{
let prefix_list = child.attribute("PrefixList").ok_or_else(|| {
TransformError::UnsupportedTransform(
"missing PrefixList attribute on <InclusiveNamespaces>".into(),
)
})?;
return Ok(Some(prefix_list.to_string()));
}
}
}
Ok(None)
}
#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
mod tests {
use super::*;
use crate::xmldsig::NodeSet;
use roxmltree::Document;
#[test]
fn enveloped_excludes_signature_subtree() {
let xml = r#"<root>
<data>hello</data>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo><Reference URI=""/></SignedInfo>
<SignatureValue>abc</SignatureValue>
</Signature>
</root>"#;
let doc = Document::parse(xml).unwrap();
let sig_node = doc
.descendants()
.find(|n| n.is_element() && n.tag_name().name() == "Signature")
.unwrap();
let node_set = NodeSet::entire_document_without_comments(&doc);
let data = TransformData::NodeSet(node_set);
let result = apply_transform(sig_node, &Transform::Enveloped, data).unwrap();
let node_set = result.into_node_set().unwrap();
assert!(node_set.contains(doc.root_element()));
let data_elem = doc
.descendants()
.find(|n| n.is_element() && n.tag_name().name() == "data")
.unwrap();
assert!(node_set.contains(data_elem));
assert!(
!node_set.contains(sig_node),
"Signature element should be excluded"
);
let signed_info = doc
.descendants()
.find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
.unwrap();
assert!(
!node_set.contains(signed_info),
"SignedInfo (child of Signature) should be excluded"
);
}
#[test]
fn enveloped_requires_node_set_input() {
let xml = "<root/>";
let doc = Document::parse(xml).unwrap();
let data = TransformData::Binary(vec![1, 2, 3]);
let result = apply_transform(doc.root_element(), &Transform::Enveloped, data);
assert!(result.is_err());
match result.unwrap_err() {
TransformError::TypeMismatch { expected, got } => {
assert_eq!(expected, "NodeSet");
assert_eq!(got, "Binary");
}
other => panic!("expected TypeMismatch, got: {other:?}"),
}
}
#[test]
fn enveloped_rejects_cross_document_signature_node() {
let xml = r#"<Root><Signature Id="sig"/></Root>"#;
let doc1 = Document::parse(xml).unwrap();
let doc2 = Document::parse(xml).unwrap();
let node_set = NodeSet::entire_document_without_comments(&doc1);
let input = TransformData::NodeSet(node_set);
let sig_from_doc2 = doc2
.descendants()
.find(|n| n.is_element() && n.tag_name().name() == "Signature")
.unwrap();
let result = apply_transform(sig_from_doc2, &Transform::Enveloped, input);
assert!(matches!(
result,
Err(TransformError::CrossDocumentSignatureNode)
));
}
#[test]
fn c14n_transform_produces_bytes() {
let xml = r#"<root b="2" a="1"><child/></root>"#;
let doc = Document::parse(xml).unwrap();
let node_set = NodeSet::entire_document_without_comments(&doc);
let data = TransformData::NodeSet(node_set);
let algo =
C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
let result = apply_transform(doc.root_element(), &Transform::C14n(algo), data).unwrap();
let bytes = result.into_binary().unwrap();
let output = String::from_utf8(bytes).unwrap();
assert_eq!(output, r#"<root a="1" b="2"><child></child></root>"#);
}
#[test]
fn c14n_transform_requires_node_set() {
let xml = "<root/>";
let doc = Document::parse(xml).unwrap();
let algo =
C14nAlgorithm::from_uri("http://www.w3.org/TR/2001/REC-xml-c14n-20010315").unwrap();
let data = TransformData::Binary(vec![1, 2, 3]);
let result = apply_transform(doc.root_element(), &Transform::C14n(algo), data);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TransformError::TypeMismatch { .. }
));
}
#[test]
fn pipeline_enveloped_then_c14n() {
let xml = r#"<root xmlns:ns="http://example.com" b="2" a="1">
<data>hello</data>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo/>
<SignatureValue>abc</SignatureValue>
</Signature>
</root>"#;
let doc = Document::parse(xml).unwrap();
let sig_node = doc
.descendants()
.find(|n| n.is_element() && n.tag_name().name() == "Signature")
.unwrap();
let initial = TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc));
let transforms = vec![
Transform::Enveloped,
Transform::C14n(
C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#").unwrap(),
),
];
let result = execute_transforms(sig_node, initial, &transforms).unwrap();
let output = String::from_utf8(result).unwrap();
assert!(!output.contains("Signature"));
assert!(!output.contains("SignedInfo"));
assert!(!output.contains("SignatureValue"));
assert!(output.contains("<data>hello</data>"));
}
#[test]
fn pipeline_no_transforms_applies_default_c14n() {
let xml = r#"<root b="2" a="1"><child/></root>"#;
let doc = Document::parse(xml).unwrap();
let initial = TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc));
let result = execute_transforms(doc.root_element(), initial, &[]).unwrap();
let output = String::from_utf8(result).unwrap();
assert_eq!(output, r#"<root a="1" b="2"><child></child></root>"#);
}
#[test]
fn pipeline_binary_passthrough() {
let xml = "<root/>";
let doc = Document::parse(xml).unwrap();
let initial = TransformData::Binary(b"raw bytes".to_vec());
let result = execute_transforms(doc.root_element(), initial, &[]).unwrap();
assert_eq!(result, b"raw bytes");
}
#[test]
fn enveloped_only_excludes_own_signature() {
let xml = r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<data>hello</data>
<ds:Signature Id="sig-other">
<ds:SignedInfo><ds:Reference URI=""/></ds:SignedInfo>
</ds:Signature>
<ds:Signature Id="sig-target">
<ds:SignedInfo><ds:Reference URI=""/></ds:SignedInfo>
</ds:Signature>
</root>"#;
let doc = Document::parse(xml).unwrap();
let sig_node = doc
.descendants()
.find(|n| n.is_element() && n.attribute("Id") == Some("sig-target"))
.unwrap();
let node_set = NodeSet::entire_document_without_comments(&doc);
let data = TransformData::NodeSet(node_set);
let result = apply_transform(sig_node, &Transform::Enveloped, data).unwrap();
let node_set = result.into_node_set().unwrap();
let sig_other = doc
.descendants()
.find(|n| n.is_element() && n.attribute("Id") == Some("sig-other"))
.unwrap();
assert!(
node_set.contains(sig_other),
"other Signature elements should NOT be excluded"
);
assert!(
!node_set.contains(sig_node),
"the specific Signature being verified should be excluded"
);
}
#[test]
fn parse_transforms_enveloped_and_exc_c14n() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let transforms_node = doc.root_element();
let chain = parse_transforms(transforms_node).unwrap();
assert_eq!(chain.len(), 2);
assert!(matches!(chain[0], Transform::Enveloped));
assert!(matches!(chain[1], Transform::C14n(_)));
}
#[test]
fn parse_transforms_with_inclusive_prefixes() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
<ec:InclusiveNamespaces PrefixList="ds saml #default"/>
</Transform>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let transforms_node = doc.root_element();
let chain = parse_transforms(transforms_node).unwrap();
assert_eq!(chain.len(), 1);
match &chain[0] {
Transform::C14n(algo) => {
assert!(algo.inclusive_prefixes().contains("ds"));
assert!(algo.inclusive_prefixes().contains("saml"));
assert!(algo.inclusive_prefixes().contains("")); }
other => panic!("expected C14n, got: {other:?}"),
}
}
#[test]
fn parse_transforms_ignores_wrong_ns_inclusive_namespaces() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
<InclusiveNamespaces xmlns="http://example.com/fake"
PrefixList="attacker-controlled"/>
</Transform>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let chain = parse_transforms(doc.root_element()).unwrap();
assert_eq!(chain.len(), 1);
match &chain[0] {
Transform::C14n(algo) => {
assert!(
algo.inclusive_prefixes().is_empty(),
"should ignore InclusiveNamespaces in wrong namespace"
);
}
other => panic!("expected C14n, got: {other:?}"),
}
}
#[test]
fn parse_transforms_missing_prefix_list_is_error() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
<ec:InclusiveNamespaces/>
</Transform>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_transforms(doc.root_element());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TransformError::UnsupportedTransform(_)
));
}
#[test]
fn parse_transforms_unsupported_algorithm() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform Algorithm="http://example.com/unknown"/>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_transforms(doc.root_element());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TransformError::UnsupportedTransform(_)
));
}
#[test]
fn parse_transforms_missing_algorithm() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform/>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_transforms(doc.root_element());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TransformError::UnsupportedTransform(_)
));
}
#[test]
fn parse_transforms_empty() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#"/>"#;
let doc = Document::parse(xml).unwrap();
let chain = parse_transforms(doc.root_element()).unwrap();
assert!(chain.is_empty());
}
#[test]
fn parse_transforms_accepts_enveloped_compat_xpath() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
not(ancestor-or-self::dsig:Signature)
</XPath>
</Transform>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let chain = parse_transforms(doc.root_element()).unwrap();
assert_eq!(chain.len(), 1);
assert!(matches!(chain[0], Transform::XpathExcludeAllSignatures));
}
#[test]
fn parse_transforms_rejects_other_xpath_expressions() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath>self::node()</XPath>
</Transform>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_transforms(doc.root_element());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TransformError::UnsupportedTransform(_)
));
}
#[test]
fn parse_transforms_rejects_xpath_in_wrong_namespace() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<foo:XPath xmlns:foo="http://example.com/ns">
not(ancestor-or-self::dsig:Signature)
</foo:XPath>
</Transform>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_transforms(doc.root_element());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TransformError::UnsupportedTransform(_)
));
}
#[test]
fn parse_transforms_rejects_xpath_with_wrong_dsig_prefix_binding() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath xmlns:dsig="http://example.com/not-xmldsig">
not(ancestor-or-self::dsig:Signature)
</XPath>
</Transform>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_transforms(doc.root_element());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TransformError::UnsupportedTransform(_)
));
}
#[test]
fn parse_transforms_rejects_xpath_with_internal_whitespace_mutation() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
not(ancestor-or-self::dsig:Signa ture)
</XPath>
</Transform>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_transforms(doc.root_element());
assert!(matches!(
result.unwrap_err(),
TransformError::UnsupportedTransform(_)
));
}
#[test]
fn parse_transforms_rejects_multiple_xpath_children() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
not(ancestor-or-self::dsig:Signature)
</XPath>
<XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
not(ancestor-or-self::dsig:Signature)
</XPath>
</Transform>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_transforms(doc.root_element());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TransformError::UnsupportedTransform(_)
));
}
#[test]
fn parse_transforms_rejects_non_xpath_element_children() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
not(ancestor-or-self::dsig:Signature)
</XPath>
<Extra/>
</Transform>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_transforms(doc.root_element());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TransformError::UnsupportedTransform(_)
));
}
#[test]
fn xpath_compat_excludes_other_signature_subtrees_too() {
let xml = r#"<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<payload>keep-me</payload>
<ds:Signature Id="sig-1">
<ds:SignedInfo/>
<ds:SignatureValue>one</ds:SignatureValue>
</ds:Signature>
<ds:Signature Id="sig-2">
<ds:SignedInfo/>
<ds:SignatureValue>two</ds:SignatureValue>
</ds:Signature>
</root>"#;
let doc = Document::parse(xml).unwrap();
let signature_nodes: Vec<_> = doc
.descendants()
.filter(|node| {
node.is_element()
&& node.tag_name().name() == "Signature"
&& node.tag_name().namespace() == Some(XMLDSIG_NS)
})
.collect();
let sig_node = signature_nodes[0];
let enveloped = execute_transforms(
sig_node,
TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc)),
&[
Transform::Enveloped,
Transform::C14n(C14nAlgorithm::new(
crate::c14n::C14nMode::Inclusive1_0,
false,
)),
],
)
.unwrap();
let xpath_compat = execute_transforms(
sig_node,
TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc)),
&[
Transform::XpathExcludeAllSignatures,
Transform::C14n(C14nAlgorithm::new(
crate::c14n::C14nMode::Inclusive1_0,
false,
)),
],
)
.unwrap();
let enveloped = String::from_utf8(enveloped).unwrap();
let xpath_compat = String::from_utf8(xpath_compat).unwrap();
assert!(enveloped.contains("sig-2"));
assert!(!xpath_compat.contains("sig-1"));
assert!(!xpath_compat.contains("sig-2"));
assert!(xpath_compat.contains("keep-me"));
}
#[test]
fn parse_transforms_inclusive_c14n_variants() {
let xml = r#"<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/>
<Transform Algorithm="http://www.w3.org/2006/12/xml-c14n11"/>
</Transforms>"#;
let doc = Document::parse(xml).unwrap();
let chain = parse_transforms(doc.root_element()).unwrap();
assert_eq!(chain.len(), 3);
for t in &chain {
assert!(matches!(t, Transform::C14n(_)));
}
}
#[test]
fn saml_enveloped_signature_full_pipeline() {
let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_resp1">
<saml:Assertion ID="_assert1">
<saml:Subject>user@example.com</saml:Subject>
</saml:Assertion>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:Reference URI="">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>fakesig==</ds:SignatureValue>
</ds:Signature>
</samlp:Response>"#;
let doc = Document::parse(xml).unwrap();
let sig_node = doc
.descendants()
.find(|n| n.is_element() && n.tag_name().name() == "Signature")
.unwrap();
let reference = doc
.descendants()
.find(|n| n.is_element() && n.tag_name().name() == "Reference")
.unwrap();
let transforms_elem = reference
.children()
.find(|n| n.is_element() && n.tag_name().name() == "Transforms")
.unwrap();
let transforms = parse_transforms(transforms_elem).unwrap();
assert_eq!(transforms.len(), 2);
let initial = TransformData::NodeSet(NodeSet::entire_document_without_comments(&doc));
let result = execute_transforms(sig_node, initial, &transforms).unwrap();
let output = String::from_utf8(result).unwrap();
assert!(!output.contains("Signature"), "Signature should be removed");
assert!(
!output.contains("SignedInfo"),
"SignedInfo should be removed"
);
assert!(
!output.contains("SignatureValue"),
"SignatureValue should be removed"
);
assert!(
!output.contains("fakesig"),
"signature value should be removed"
);
assert!(output.contains("samlp:Response"));
assert!(output.contains("saml:Assertion"));
assert!(output.contains("user@example.com"));
}
}