use std::io::Write;
use quick_xml::Writer;
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
use crate::c14n::{C14nAlgorithm, C14nMode};
use crate::xml::is_xml_1_0_character;
use super::parse::MAX_REFERENCES_PER_SIGNATURE;
use super::transforms::{
MAX_TRANSFORMS_PER_REFERENCE, MAX_XPATH_FILTERS, XPathSignatureParseBudget,
validate_xpath_namespace_budget,
};
use super::xpath::compile_xpath;
use super::{
BASE64_TRANSFORM_URI, DigestAlgorithm, ENVELOPED_SIGNATURE_URI, SignatureAlgorithm, Transform,
XPATH_FILTER2_TRANSFORM_URI, XPATH_TRANSFORM_URI, XPathExpression,
};
const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
const EXCLUSIVE_C14N_NS: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
const XPATH_EXCLUDE_ALL_SIGNATURES: &str = "not(ancestor-or-self::dsig:Signature)";
#[derive(Debug, thiserror::Error)]
pub enum SignatureBuilderError {
#[error("invalid XML namespace prefix: {0}")]
InvalidNamespacePrefix(String),
#[error("XML namespace URI contains a character forbidden by XML 1.0: {0:?}")]
InvalidNamespaceUri(String),
#[error("XPath namespace binding conflicts with XMLDSig prefix: {0}")]
NamespacePrefixConflict(String),
#[error("invalid {element} Id: {value}")]
InvalidId {
element: &'static str,
value: String,
},
#[error("a signature template requires at least one Reference")]
MissingReference,
#[error("signature template contains {count} references; maximum is {max}")]
TooManyReferences {
count: usize,
max: usize,
},
#[error("transform chain contains {count} transforms; maximum is {max}")]
TooManyTransforms {
count: usize,
max: usize,
},
#[error("XPath Filter 2.0 requires between 1 and {max} expressions, got {count}")]
InvalidXPathFilterCount {
count: usize,
max: usize,
},
#[error("invalid XPath expression: {0}")]
InvalidXPath(String),
#[error("algorithm is not allowed for signing: {0}")]
SigningAlgorithmDisabled(&'static str),
#[error("XML serialization error: {0}")]
Serialization(#[from] std::io::Error),
#[error("XML writer emitted invalid UTF-8: {0}")]
InvalidUtf8(#[from] std::string::FromUtf8Error),
}
#[derive(Debug, Clone)]
pub struct ReferenceBuilder {
uri: Option<String>,
id: Option<String>,
ref_type: Option<String>,
transforms: Vec<Transform>,
digest_method: DigestAlgorithm,
}
impl ReferenceBuilder {
#[must_use]
pub fn new(digest_method: DigestAlgorithm) -> Self {
Self {
uri: None,
id: None,
ref_type: None,
transforms: Vec::new(),
digest_method,
}
}
#[must_use]
pub fn uri(mut self, uri: impl Into<String>) -> Self {
self.uri = Some(uri.into());
self
}
#[must_use]
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn ref_type(mut self, ref_type: impl Into<String>) -> Self {
self.ref_type = Some(ref_type.into());
self
}
#[must_use]
pub fn transform(mut self, transform: Transform) -> Self {
self.transforms.push(transform);
self
}
}
#[derive(Debug, Clone)]
pub struct SignatureBuilder {
c14n_method: C14nAlgorithm,
sign_method: SignatureAlgorithm,
ns_prefix: Option<String>,
signature_id: Option<String>,
references: Vec<ReferenceBuilder>,
include_key_info: bool,
}
impl SignatureBuilder {
#[must_use]
pub fn new(c14n_method: C14nAlgorithm, sign_method: SignatureAlgorithm) -> Self {
Self {
c14n_method,
sign_method,
ns_prefix: None,
signature_id: None,
references: Vec::new(),
include_key_info: false,
}
}
#[must_use]
pub fn ns_prefix(mut self, prefix: impl Into<String>) -> Self {
self.ns_prefix = Some(prefix.into());
self
}
#[must_use]
pub fn signature_id(mut self, id: impl Into<String>) -> Self {
self.signature_id = Some(id.into());
self
}
#[must_use]
pub fn add_reference(mut self, reference: ReferenceBuilder) -> Self {
self.references.push(reference);
self
}
#[must_use]
pub fn key_info(mut self, include: bool) -> Self {
self.include_key_info = include;
self
}
pub fn build_template(&self) -> Result<String, SignatureBuilderError> {
self.validate()?;
let prefix = self.ns_prefix.as_deref();
let mut writer = Writer::new(Vec::new());
let signature_name = qualified_name(prefix, "Signature");
let mut signature = BytesStart::new(&signature_name);
let namespace_attr = prefix.map_or_else(|| "xmlns".to_owned(), |p| format!("xmlns:{p}"));
signature.push_attribute((namespace_attr.as_str(), XMLDSIG_NS));
if let Some(id) = &self.signature_id {
signature.push_attribute(("Id", id.as_str()));
}
writer.write_event(Event::Start(signature))?;
write_start(&mut writer, prefix, "SignedInfo")?;
write_algorithm(
&mut writer,
prefix,
"CanonicalizationMethod",
self.c14n_method.uri(),
)?;
write_algorithm(
&mut writer,
prefix,
"SignatureMethod",
self.sign_method.uri(),
)?;
for reference in &self.references {
write_reference(&mut writer, prefix, reference)?;
}
write_end(&mut writer, prefix, "SignedInfo")?;
write_empty(&mut writer, prefix, "SignatureValue")?;
if self.include_key_info {
write_empty(&mut writer, prefix, "KeyInfo")?;
}
writer.write_event(Event::End(BytesEnd::new(signature_name)))?;
Ok(String::from_utf8(writer.into_inner())?)
}
fn validate(&self) -> Result<(), SignatureBuilderError> {
if let Some(prefix) = &self.ns_prefix
&& !is_namespace_prefix(prefix)
{
return Err(SignatureBuilderError::InvalidNamespacePrefix(
prefix.clone(),
));
}
if let Some(id) = &self.signature_id
&& !is_ncname(id)
{
return Err(SignatureBuilderError::InvalidId {
element: "Signature",
value: id.clone(),
});
}
if self.references.is_empty() {
return Err(SignatureBuilderError::MissingReference);
}
if self.references.len() > MAX_REFERENCES_PER_SIGNATURE {
return Err(SignatureBuilderError::TooManyReferences {
count: self.references.len(),
max: MAX_REFERENCES_PER_SIGNATURE,
});
}
let mut xpath_signature_budget = XPathSignatureParseBudget::default();
for reference in &self.references {
if reference.transforms.len() > MAX_TRANSFORMS_PER_REFERENCE {
return Err(SignatureBuilderError::TooManyTransforms {
count: reference.transforms.len(),
max: MAX_TRANSFORMS_PER_REFERENCE,
});
}
for transform in &reference.transforms {
match transform {
Transform::XPath(xpath) => {
validate_xpath_source(xpath.expression())?;
xpath_signature_budget.charge().map_err(|error| {
SignatureBuilderError::InvalidXPath(error.to_string())
})?;
}
Transform::XPathFilter2(filters) => {
if filters.is_empty() || filters.len() > MAX_XPATH_FILTERS {
return Err(SignatureBuilderError::InvalidXPathFilterCount {
count: filters.len(),
max: MAX_XPATH_FILTERS,
});
}
for filter in filters {
validate_xpath_source(filter.xpath().expression())?;
xpath_signature_budget.charge().map_err(|error| {
SignatureBuilderError::InvalidXPath(error.to_string())
})?;
}
}
_ => {}
}
}
validate_xpath_namespace_budget(
&reference.transforms,
self.ns_prefix.as_deref().map(|prefix| (prefix, XMLDSIG_NS)),
)
.map_err(|error| SignatureBuilderError::InvalidXPath(error.to_string()))?;
}
for (prefix, uri, shares_signature_namespace) in
self.references.iter().flat_map(|reference| {
reference
.transforms
.iter()
.flat_map(|transform| match transform {
Transform::XPath(xpath) => xpath
.namespaces()
.iter()
.map(|(prefix, uri)| (prefix, uri, true))
.collect::<Vec<_>>(),
Transform::XPathFilter2(filters) => filters
.iter()
.flat_map(|filter| {
filter
.xpath()
.namespaces()
.iter()
.map(|(prefix, uri)| (prefix, uri, false))
})
.collect(),
_ => Vec::new(),
})
})
{
if uri.is_empty() || uri == XMLNS_NS || !uri.chars().all(is_xml_1_0_character) {
return Err(SignatureBuilderError::InvalidNamespaceUri(uri.clone()));
}
if prefix == "xmlns"
|| (prefix == "xml") != (uri == XML_NS)
|| (prefix != "xml" && !is_namespace_prefix(prefix))
{
return Err(SignatureBuilderError::InvalidNamespacePrefix(
prefix.clone(),
));
}
if shares_signature_namespace
&& self.ns_prefix.as_ref() == Some(prefix)
&& uri != XMLDSIG_NS
{
return Err(SignatureBuilderError::NamespacePrefixConflict(
prefix.clone(),
));
}
}
if !self.sign_method.signing_allowed() {
return Err(SignatureBuilderError::SigningAlgorithmDisabled(
self.sign_method.uri(),
));
}
for reference in &self.references {
if let Some(id) = &reference.id
&& !is_ncname(id)
{
return Err(SignatureBuilderError::InvalidId {
element: "Reference",
value: id.clone(),
});
}
if !reference.digest_method.signing_allowed() {
return Err(SignatureBuilderError::SigningAlgorithmDisabled(
reference.digest_method.uri(),
));
}
}
Ok(())
}
}
fn validate_xpath_source(source: &str) -> Result<(), SignatureBuilderError> {
if let Some(character) = source
.chars()
.find(|character| !is_xml_1_0_character(*character))
{
return Err(SignatureBuilderError::InvalidXPath(format!(
"XPath expression contains a character forbidden by XML 1.0: {character:?}"
)));
}
compile_xpath(source).map_err(SignatureBuilderError::InvalidXPath)?;
Ok(())
}
fn write_reference<W: Write>(
writer: &mut Writer<W>,
prefix: Option<&str>,
reference: &ReferenceBuilder,
) -> Result<(), std::io::Error> {
let name = qualified_name(prefix, "Reference");
let mut element = BytesStart::new(&name);
if let Some(id) = &reference.id {
element.push_attribute(("Id", id.as_str()));
}
if let Some(ref_type) = &reference.ref_type {
element.push_attribute(("Type", ref_type.as_str()));
}
if let Some(uri) = &reference.uri {
element.push_attribute(("URI", uri.as_str()));
}
writer.write_event(Event::Start(element))?;
if !reference.transforms.is_empty() {
write_start(writer, prefix, "Transforms")?;
for transform in &reference.transforms {
write_transform(writer, prefix, transform)?;
}
write_end(writer, prefix, "Transforms")?;
}
write_algorithm(
writer,
prefix,
"DigestMethod",
reference.digest_method.uri(),
)?;
write_empty(writer, prefix, "DigestValue")?;
writer.write_event(Event::End(BytesEnd::new(name)))?;
Ok(())
}
fn write_transform<W: Write>(
writer: &mut Writer<W>,
prefix: Option<&str>,
transform: &Transform,
) -> Result<(), std::io::Error> {
match transform {
Transform::Enveloped => {
write_algorithm(writer, prefix, "Transform", ENVELOPED_SIGNATURE_URI)
}
Transform::XpathExcludeAllSignatures => {
let name = qualified_name(prefix, "Transform");
let mut element = BytesStart::new(&name);
element.push_attribute(("Algorithm", XPATH_TRANSFORM_URI));
writer.write_event(Event::Start(element))?;
let xpath_name = qualified_name(prefix, "XPath");
let mut xpath = BytesStart::new(&xpath_name);
xpath.push_attribute(("xmlns:dsig", XMLDSIG_NS));
writer.write_event(Event::Start(xpath))?;
writer.write_event(Event::Text(BytesText::new(XPATH_EXCLUDE_ALL_SIGNATURES)))?;
writer.write_event(Event::End(BytesEnd::new(xpath_name)))?;
writer.write_event(Event::End(BytesEnd::new(name)))?;
Ok(())
}
Transform::XPath(xpath) => {
let transform_name = qualified_name(prefix, "Transform");
let mut transform_element = BytesStart::new(&transform_name);
transform_element.push_attribute(("Algorithm", XPATH_TRANSFORM_URI));
writer.write_event(Event::Start(transform_element))?;
write_xpath_expression(writer, prefix, "XPath", None, xpath)?;
writer.write_event(Event::End(BytesEnd::new(transform_name)))?;
Ok(())
}
Transform::XPathFilter2(filters) => {
let transform_name = qualified_name(prefix, "Transform");
let mut transform_element = BytesStart::new(&transform_name);
transform_element.push_attribute(("Algorithm", XPATH_FILTER2_TRANSFORM_URI));
writer.write_event(Event::Start(transform_element))?;
for filter in filters {
write_xpath_expression(
writer,
None,
"XPath",
Some(filter.operation().as_str()),
filter.xpath(),
)?;
}
writer.write_event(Event::End(BytesEnd::new(transform_name)))?;
Ok(())
}
Transform::Base64Decode => {
write_algorithm(writer, prefix, "Transform", BASE64_TRANSFORM_URI)
}
Transform::C14n(algorithm) if algorithm.inclusive_prefixes().is_empty() => {
write_algorithm(writer, prefix, "Transform", algorithm.uri())
}
Transform::C14n(algorithm) => {
let name = qualified_name(prefix, "Transform");
let mut element = BytesStart::new(&name);
element.push_attribute(("Algorithm", algorithm.uri()));
writer.write_event(Event::Start(element))?;
if algorithm.mode() == C14nMode::Exclusive1_0 {
let mut prefixes: Vec<&str> = algorithm
.inclusive_prefixes()
.iter()
.map(String::as_str)
.collect();
prefixes.sort_unstable();
let prefix_list = prefixes
.into_iter()
.map(|p| if p.is_empty() { "#default" } else { p })
.collect::<Vec<_>>()
.join(" ");
let mut inclusive = BytesStart::new("ec:InclusiveNamespaces");
inclusive.push_attribute(("xmlns:ec", EXCLUSIVE_C14N_NS));
inclusive.push_attribute(("PrefixList", prefix_list.as_str()));
writer.write_event(Event::Empty(inclusive))?;
}
writer.write_event(Event::End(BytesEnd::new(name)))?;
Ok(())
}
}
}
fn write_xpath_expression<W: Write>(
writer: &mut Writer<W>,
prefix: Option<&str>,
local_name: &str,
filter: Option<&str>,
xpath: &XPathExpression,
) -> Result<(), std::io::Error> {
let name = qualified_name(prefix, local_name);
let mut element = BytesStart::new(&name);
let namespace_attributes = xpath
.namespaces()
.iter()
.filter(|(namespace_prefix, _)| namespace_prefix.as_str() != "xml")
.map(|(namespace_prefix, uri)| (format!("xmlns:{namespace_prefix}"), uri))
.collect::<Vec<_>>();
if prefix.is_none() && filter.is_some() {
element.push_attribute(("xmlns", XPATH_FILTER2_TRANSFORM_URI));
}
if let Some(filter) = filter {
element.push_attribute(("Filter", filter));
}
for (attribute, uri) in &namespace_attributes {
element.push_attribute((attribute.as_str(), uri.as_str()));
}
writer.write_event(Event::Start(element))?;
writer.write_event(Event::Text(BytesText::new(xpath.expression())))?;
writer.write_event(Event::End(BytesEnd::new(name)))?;
Ok(())
}
fn write_algorithm<W: Write>(
writer: &mut Writer<W>,
prefix: Option<&str>,
local_name: &str,
algorithm: &str,
) -> Result<(), std::io::Error> {
let name = qualified_name(prefix, local_name);
let mut element = BytesStart::new(name);
element.push_attribute(("Algorithm", algorithm));
writer.write_event(Event::Empty(element))?;
Ok(())
}
fn write_start<W: Write>(
writer: &mut Writer<W>,
prefix: Option<&str>,
local_name: &str,
) -> Result<(), std::io::Error> {
writer.write_event(Event::Start(BytesStart::new(qualified_name(
prefix, local_name,
))))?;
Ok(())
}
fn write_end<W: Write>(
writer: &mut Writer<W>,
prefix: Option<&str>,
local_name: &str,
) -> Result<(), std::io::Error> {
writer.write_event(Event::End(BytesEnd::new(qualified_name(
prefix, local_name,
))))?;
Ok(())
}
fn write_empty<W: Write>(
writer: &mut Writer<W>,
prefix: Option<&str>,
local_name: &str,
) -> Result<(), std::io::Error> {
writer.write_event(Event::Empty(BytesStart::new(qualified_name(
prefix, local_name,
))))?;
Ok(())
}
fn qualified_name(prefix: Option<&str>, local_name: &str) -> String {
prefix.map_or_else(
|| local_name.to_owned(),
|prefix| format!("{prefix}:{local_name}"),
)
}
fn is_ncname(value: &str) -> bool {
if value.is_empty() || value.contains(':') {
return false;
}
roxmltree::Document::parse(&format!("<{value}/>"))
.is_ok_and(|document| document.root_element().tag_name().name() == value)
}
fn is_namespace_prefix(value: &str) -> bool {
if matches!(value, "xml" | "xmlns") || !is_ncname(value) {
return false;
}
roxmltree::Document::parse(&format!(
"<{value}:n xmlns:{value}=\"urn:xml-sec:prefix-validation\"/>"
))
.is_ok()
}