use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use crate::dsig::algorithms::{C14nAlgorithm, DigestAlgorithm};
use crate::dsig::c14n::canonicalize;
use crate::error::Error;
use crate::xml::parse::{Document, Element, ElementId, Node};
pub(crate) const DS_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
pub(crate) const EC_NS: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
pub(crate) const ENVELOPED_SIGNATURE_URI: &str =
"http://www.w3.org/2000/09/xmldsig#enveloped-signature";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AllowedTransform {
EnvelopedSignature,
ExclusiveCanonical,
ExclusiveCanonicalWithComments,
InclusiveCanonical,
InclusiveCanonicalWithComments,
}
impl AllowedTransform {
pub(crate) fn from_uri(uri: &str) -> Result<Self, Error> {
match uri {
ENVELOPED_SIGNATURE_URI => Ok(Self::EnvelopedSignature),
"http://www.w3.org/2001/10/xml-exc-c14n#" => Ok(Self::ExclusiveCanonical),
"http://www.w3.org/2001/10/xml-exc-c14n#WithComments" => {
Ok(Self::ExclusiveCanonicalWithComments)
}
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315" => Ok(Self::InclusiveCanonical),
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" => {
Ok(Self::InclusiveCanonicalWithComments)
}
_ => Err(Error::DisallowedTransform {
transform: uri.to_owned(),
}),
}
}
pub(crate) fn as_c14n_algorithm(self) -> Option<C14nAlgorithm> {
match self {
Self::EnvelopedSignature => None,
Self::ExclusiveCanonical => Some(C14nAlgorithm::ExclusiveCanonical),
Self::ExclusiveCanonicalWithComments => {
Some(C14nAlgorithm::ExclusiveCanonicalWithComments)
}
Self::InclusiveCanonical => Some(C14nAlgorithm::InclusiveCanonical),
Self::InclusiveCanonicalWithComments => {
Some(C14nAlgorithm::InclusiveCanonicalWithComments)
}
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct ParsedReference {
pub target: ElementId,
pub transforms: Vec<AllowedTransform>,
pub digest_algorithm: DigestAlgorithm,
pub digest_value: Vec<u8>,
pub inclusive_namespace_prefixes: Vec<String>,
}
pub(crate) fn parse_reference(
document: &Document,
reference: &Element,
) -> Result<ParsedReference, Error> {
let uri_attr = reference.attribute(None, "URI").unwrap_or("");
let target = resolve_uri(document, uri_attr)?;
let mut transforms: Vec<AllowedTransform> = Vec::new();
let mut inclusive_namespace_prefixes: Vec<String> = Vec::new();
if let Some(transforms_elem) = reference.child_element(Some(DS_NS), "Transforms") {
for transform_elem in transforms_elem.all_child_elements(Some(DS_NS), "Transform") {
let alg_uri = transform_elem.attribute(None, "Algorithm").ok_or_else(|| {
Error::DisallowedTransform {
transform: String::new(),
}
})?;
let kind = AllowedTransform::from_uri(alg_uri)?;
transforms.push(kind);
if let Some(incl) = transform_elem.child_element(Some(EC_NS), "InclusiveNamespaces")
&& let Some(list) = incl.attribute(None, "PrefixList")
{
for token in list.split_whitespace() {
inclusive_namespace_prefixes.push(token.to_owned());
}
}
}
}
let c14n_indices: Vec<usize> = transforms
.iter()
.enumerate()
.filter(|(_, t)| t.as_c14n_algorithm().is_some())
.map(|(i, _)| i)
.collect();
if c14n_indices.len() > 1 {
return Err(Error::DisallowedTransform {
transform: "multiple c14n transforms in Reference".to_owned(),
});
}
if let Some(&idx) = c14n_indices.first()
&& Some(idx) != transforms.len().checked_sub(1)
{
return Err(Error::DisallowedTransform {
transform: "c14n transform must be the final transform".to_owned(),
});
}
let digest_method = reference.child_element(Some(DS_NS), "DigestMethod").ok_or(
Error::SignatureVerification {
reason: "Reference missing DigestMethod",
},
)?;
let digest_alg_uri =
digest_method
.attribute(None, "Algorithm")
.ok_or(Error::SignatureVerification {
reason: "DigestMethod missing Algorithm",
})?;
let digest_algorithm = DigestAlgorithm::from_uri(digest_alg_uri)?;
let digest_value_elem = reference.child_element(Some(DS_NS), "DigestValue").ok_or(
Error::SignatureVerification {
reason: "Reference missing DigestValue",
},
)?;
let digest_text = digest_value_elem.text_content();
let digest_value = decode_base64_lenient(&digest_text)?;
Ok(ParsedReference {
target,
transforms,
digest_algorithm,
digest_value,
inclusive_namespace_prefixes,
})
}
fn resolve_uri(document: &Document, uri: &str) -> Result<ElementId, Error> {
if uri.is_empty() {
return Ok(document.root().id());
}
if let Some(rest) = uri.strip_prefix('#') {
if rest.is_empty() || rest.starts_with("xpointer") || rest.contains('(') {
return Err(Error::ReferenceResolution);
}
return document
.element_by_id_attr(rest)
.ok_or(Error::ReferenceResolution);
}
Err(Error::ReferenceResolution)
}
pub(crate) fn decode_base64_lenient(input: &str) -> Result<Vec<u8>, Error> {
let mut cleaned: Vec<u8> = Vec::with_capacity(input.len());
for &b in input.as_bytes() {
if !matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c) {
cleaned.push(b);
}
}
BASE64_STANDARD
.decode(&cleaned)
.map_err(|_e| Error::Base64Decode)
}
pub(crate) fn compute_reference_digest(
document: &Document,
parsed: &ParsedReference,
enclosing_signature: ElementId,
) -> Result<Vec<u8>, Error> {
let target = document
.element(parsed.target)
.ok_or(Error::ReferenceResolution)?;
let chain = ancestor_chain(document, parsed.target).ok_or(Error::ReferenceResolution)?;
let strip_signature = parsed
.transforms
.contains(&AllowedTransform::EnvelopedSignature);
let c14n_alg = parsed
.transforms
.iter()
.find_map(|t| t.as_c14n_algorithm())
.unwrap_or(C14nAlgorithm::ExclusiveCanonical);
let prefix_refs: Vec<&str> = parsed
.inclusive_namespace_prefixes
.iter()
.map(String::as_str)
.collect();
let canonical_bytes = if strip_signature {
let pruned = clone_excluding_id(target, enclosing_signature);
canonicalize(document, &pruned, &chain, c14n_alg, &prefix_refs)?
} else {
canonicalize(document, target, &chain, c14n_alg, &prefix_refs)?
};
Ok(parsed.digest_algorithm.digest(&canonical_bytes))
}
pub(crate) fn ancestor_chain(document: &Document, target: ElementId) -> Option<Vec<&Element>> {
let path = document.paths.get(target.0 as usize)?;
let mut chain: Vec<&Element> = Vec::with_capacity(path.len());
let mut current: &Element = &document.root;
if path.is_empty() {
return Some(Vec::new());
}
chain.push(current);
let last = path.len().checked_sub(1)?;
let parents = path.get(..last)?;
for &idx in parents {
let child_node = current.children().nth(idx as usize)?;
match child_node {
Node::Element(child) => {
current = child;
chain.push(current);
}
_ => return None,
}
}
Some(chain)
}
fn clone_excluding_id(element: &Element, excluded: ElementId) -> Element {
let mut cloned_children: Vec<Node> = Vec::with_capacity(element.children.len());
for child in &element.children {
match child {
Node::Element(child_elem) => {
if child_elem.id() == excluded {
continue;
}
cloned_children.push(Node::Element(clone_excluding_id(child_elem, excluded)));
}
Node::Text(t) => cloned_children.push(Node::Text(t.clone())),
Node::Comment(c) => cloned_children.push(Node::Comment(c.clone())),
}
}
Element {
qname: element.qname.clone(),
source_prefix: element.source_prefix.clone(),
namespaces_declared_here: element.namespaces_declared_here.clone(),
attributes: element.attributes.clone(),
children: cloned_children,
id: element.id, }
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(xml: &str) -> Document {
Document::parse(xml.as_bytes()).expect("parse")
}
#[test]
fn from_uri_accepts_whitelisted_transforms() {
for uri in [
ENVELOPED_SIGNATURE_URI,
"http://www.w3.org/2001/10/xml-exc-c14n#",
"http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments",
] {
AllowedTransform::from_uri(uri).expect("whitelisted");
}
}
#[test]
fn from_uri_rejects_xslt() {
let err =
AllowedTransform::from_uri("http://www.w3.org/TR/1999/REC-xslt-19991116").unwrap_err();
match err {
Error::DisallowedTransform { transform } => {
assert!(transform.contains("xslt"), "got: {transform}");
}
other => panic!("expected DisallowedTransform, got {other:?}"),
}
}
#[test]
fn from_uri_rejects_xpath() {
let err =
AllowedTransform::from_uri("http://www.w3.org/TR/1999/REC-xpath-19991116").unwrap_err();
assert!(matches!(err, Error::DisallowedTransform { .. }));
}
#[test]
fn from_uri_rejects_base64() {
let err =
AllowedTransform::from_uri("http://www.w3.org/2000/09/xmldsig#base64").unwrap_err();
assert!(matches!(err, Error::DisallowedTransform { .. }));
}
#[test]
fn resolve_uri_empty_returns_root() {
let doc = parse(r#"<Root xmlns="urn:p" ID="r"/>"#);
let id = resolve_uri(&doc, "").unwrap();
assert_eq!(id, doc.root().id());
}
#[test]
fn resolve_uri_hash_resolves_via_id_index() {
let doc = parse(r#"<Root xmlns="urn:p" ID="r"><Child ID="abc"/></Root>"#);
let id = resolve_uri(&doc, "#abc").unwrap();
assert_eq!(id, doc.element_by_id_attr("abc").unwrap());
}
#[test]
fn resolve_uri_unknown_id_errors() {
let doc = parse(r#"<Root ID="r"/>"#);
let err = resolve_uri(&doc, "#nope").unwrap_err();
assert!(matches!(err, Error::ReferenceResolution));
}
#[test]
fn resolve_uri_external_rejected() {
let doc = parse(r#"<Root ID="r"/>"#);
let err = resolve_uri(&doc, "https://attacker.example/x.xml").unwrap_err();
assert!(matches!(err, Error::ReferenceResolution));
}
#[test]
fn resolve_uri_xpointer_rejected() {
let doc = parse(r#"<Root ID="r"/>"#);
let err = resolve_uri(&doc, "#xpointer(/Root)").unwrap_err();
assert!(matches!(err, Error::ReferenceResolution));
}
#[test]
fn ancestor_chain_root_has_empty() {
let doc = parse(r"<Root><A/></Root>");
let chain = ancestor_chain(&doc, doc.root().id()).unwrap();
assert!(chain.is_empty());
}
#[test]
fn ancestor_chain_deep_walks_to_parent() {
let doc = parse(r"<Root><A><B><C/></B></A></Root>");
let c_id = {
let a = doc.root().child_element(None, "A").unwrap();
let b = a.child_element(None, "B").unwrap();
b.child_element(None, "C").unwrap().id()
};
let chain = ancestor_chain(&doc, c_id).unwrap();
let names: Vec<&str> = chain.iter().map(|e| e.qname().local()).collect();
assert_eq!(names, vec!["Root", "A", "B"]);
}
#[test]
fn clone_excluding_id_drops_only_the_named_signature() {
let xml = r#"<Root xmlns="urn:p" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><A/><ds:Signature/><B><ds:Signature/></B></Root>"#;
let doc = parse(xml);
let outer_sig_id = doc
.root()
.child_element(Some(DS_NS), "Signature")
.unwrap()
.id();
let cloned = clone_excluding_id(doc.root(), outer_sig_id);
let kinds: Vec<&str> = cloned
.children()
.filter_map(|n| match n {
Node::Element(e) => Some(e.qname().local()),
_ => None,
})
.collect();
assert_eq!(kinds, vec!["A", "B"]);
let b = cloned.child_element(Some("urn:p"), "B").unwrap();
assert!(
b.child_element(Some(DS_NS), "Signature").is_some(),
"nested ds:Signature with a different ElementId must be preserved"
);
}
fn synth_reference(uri: &str, digest_b64: &str) -> String {
format!(
r#"<Root xmlns="urn:p" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ID="X">
<ds:Reference URI="{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:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>{digest_b64}</ds:DigestValue>
</ds:Reference>
</Root>"#
)
}
#[test]
fn parse_reference_resolves_uri_and_transforms() {
let xml = synth_reference("#X", "AAAA");
let doc = parse(&xml);
let reference = doc
.root()
.child_element(Some(DS_NS), "Reference")
.expect("ds:Reference");
let parsed = parse_reference(&doc, reference).expect("parse_reference");
assert_eq!(parsed.target, doc.element_by_id_attr("X").unwrap());
assert_eq!(parsed.digest_algorithm, DigestAlgorithm::Sha256);
assert_eq!(
parsed.transforms,
vec![
AllowedTransform::EnvelopedSignature,
AllowedTransform::ExclusiveCanonical,
]
);
assert_eq!(parsed.digest_value, vec![0u8, 0, 0]);
}
#[test]
fn parse_reference_empty_uri_targets_root() {
let xml = synth_reference("", "AAAA");
let doc = parse(&xml);
let reference = doc.root().child_element(Some(DS_NS), "Reference").unwrap();
let parsed = parse_reference(&doc, reference).unwrap();
assert_eq!(parsed.target, doc.root().id());
}
#[test]
fn parse_reference_rejects_xslt_transform() {
let xml = r##"<Root xmlns="urn:p" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ID="X">
<ds:Reference URI="#X">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xslt-19991116"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>AAAA</ds:DigestValue>
</ds:Reference>
</Root>"##;
let doc = parse(xml);
let reference = doc.root().child_element(Some(DS_NS), "Reference").unwrap();
let err = parse_reference(&doc, reference).unwrap_err();
assert!(matches!(err, Error::DisallowedTransform { .. }));
}
#[test]
fn parse_reference_rejects_c14n_not_last() {
let xml = r##"<Root xmlns="urn:p" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ID="X">
<ds:Reference URI="#X">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>AAAA</ds:DigestValue>
</ds:Reference>
</Root>"##;
let doc = parse(xml);
let reference = doc.root().child_element(Some(DS_NS), "Reference").unwrap();
let err = parse_reference(&doc, reference).unwrap_err();
assert!(matches!(err, Error::DisallowedTransform { .. }));
}
#[test]
fn parse_reference_parses_inclusive_namespaces_prefix_list() {
let xml = r##"<Root xmlns="urn:p" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ID="X">
<ds:Reference URI="#X">
<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#">
<ec:InclusiveNamespaces xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList="ds saml #default"/>
</ds:Transform>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>AAAA</ds:DigestValue>
</ds:Reference>
</Root>"##;
let doc = parse(xml);
let reference = doc.root().child_element(Some(DS_NS), "Reference").unwrap();
let parsed = parse_reference(&doc, reference).unwrap();
assert_eq!(
parsed.inclusive_namespace_prefixes,
vec!["ds".to_owned(), "saml".to_owned(), "#default".to_owned()]
);
}
#[test]
fn compute_digest_round_trip_simple_element() {
let xml = r#"<Root xmlns="urn:p" ID="X"><Inner>hello</Inner></Root>"#;
let doc = parse(xml);
let target = doc.root();
let chain = ancestor_chain(&doc, target.id()).unwrap();
let bytes =
canonicalize(&doc, target, &chain, C14nAlgorithm::ExclusiveCanonical, &[]).unwrap();
let expected_digest = DigestAlgorithm::Sha256.digest(&bytes);
let parsed = ParsedReference {
target: target.id(),
transforms: vec![AllowedTransform::ExclusiveCanonical],
digest_algorithm: DigestAlgorithm::Sha256,
digest_value: expected_digest.clone(),
inclusive_namespace_prefixes: Vec::new(),
};
let got = compute_reference_digest(&doc, &parsed, doc.root().id()).unwrap();
assert_eq!(got, expected_digest);
}
#[test]
fn compute_digest_with_enveloped_signature_strips_ds_signature() {
let with_sig = r#"<Root xmlns="urn:p" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ID="X"><Inner>hello</Inner><ds:Signature><ds:SignedInfo/></ds:Signature></Root>"#;
let without_sig = r#"<Root xmlns="urn:p" ID="X"><Inner>hello</Inner></Root>"#;
let doc_a = parse(with_sig);
let doc_b = parse(without_sig);
let parsed_a = ParsedReference {
target: doc_a.root().id(),
transforms: vec![
AllowedTransform::EnvelopedSignature,
AllowedTransform::ExclusiveCanonical,
],
digest_algorithm: DigestAlgorithm::Sha256,
digest_value: Vec::new(),
inclusive_namespace_prefixes: Vec::new(),
};
let outer_sig_id = doc_a
.root()
.child_element(Some(DS_NS), "Signature")
.unwrap()
.id();
let digest_a = compute_reference_digest(&doc_a, &parsed_a, outer_sig_id).unwrap();
let chain_b = ancestor_chain(&doc_b, doc_b.root().id()).unwrap();
let bytes_b = canonicalize(
&doc_b,
doc_b.root(),
&chain_b,
C14nAlgorithm::ExclusiveCanonical,
&[],
)
.unwrap();
let digest_b = DigestAlgorithm::Sha256.digest(&bytes_b);
assert_eq!(
digest_a, digest_b,
"enveloped-signature must produce the same digest as the un-signed equivalent",
);
let pruned = clone_excluding_id(doc_a.root(), outer_sig_id);
let chain_a = ancestor_chain(&doc_a, doc_a.root().id()).unwrap();
let canonical_pruned = canonicalize(
&doc_a,
&pruned,
&chain_a,
C14nAlgorithm::ExclusiveCanonical,
&[],
)
.unwrap();
assert!(
!canonical_pruned
.windows(b"ds:Signature".len())
.any(|w| w == b"ds:Signature"),
"ds:Signature must be absent from canonical bytes after enveloped-signature transform",
);
}
#[test]
fn parse_reference_missing_digest_method_errors() {
let xml = r##"<Root xmlns="urn:p" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ID="X">
<ds:Reference URI="#X">
<ds:DigestValue>AAAA</ds:DigestValue>
</ds:Reference>
</Root>"##;
let doc = parse(xml);
let reference = doc.root().child_element(Some(DS_NS), "Reference").unwrap();
let err = parse_reference(&doc, reference).unwrap_err();
assert!(matches!(err, Error::SignatureVerification { .. }));
}
#[test]
fn parse_reference_bad_base64_digest_errors() {
let xml = synth_reference("#X", "not!base64!");
let doc = parse(&xml);
let reference = doc.root().child_element(Some(DS_NS), "Reference").unwrap();
let err = parse_reference(&doc, reference).unwrap_err();
assert!(matches!(err, Error::Base64Decode));
}
}