use crate::core::{
Attribute, Document, ErrorKind, NamespaceDeclaration, NodeId, NodeKind, QName, XmlError,
XmlResult,
};
use crate::query::{NamespaceContext, Query, QueryValue};
use super::{
canonicalization::canonicalize_node_excluding, canonicalize_node, decode_standard_base64,
digest_bytes, encode_standard_base64, find_element_by_id, CanonicalizationAlgorithm,
CanonicalizationConfig, DigestAlgorithm, IdAttributePolicy, SignatureAlgorithm,
SigningProvider, XMLDSIG_ENVELOPED_SIGNATURE_URI,
};
pub const XMLDSIG_NAMESPACE_URI: &str = "http://www.w3.org/2000/09/xmldsig#";
pub(crate) const DEFAULT_KEY_INFO_ID: &str = "xdoc-key-info-1";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XmlDsigConfig {
id_policy: IdAttributePolicy,
canonicalization: CanonicalizationAlgorithm,
digest_algorithm: DigestAlgorithm,
signature_algorithm: SignatureAlgorithm,
document_id: String,
signature_id: String,
key_info_id: Option<String>,
references: Vec<XmlDsigReferenceConfig>,
signature_placement: SignaturePlacement,
}
impl XmlDsigConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_document_id(mut self, id: impl Into<String>) -> Self {
self.document_id = id.into();
self
}
pub fn with_signature_id(mut self, id: impl Into<String>) -> Self {
self.signature_id = id.into();
self
}
pub fn with_canonicalization(mut self, algorithm: CanonicalizationAlgorithm) -> Self {
self.canonicalization = algorithm;
self
}
pub fn with_key_info_id(mut self, id: impl Into<String>) -> Self {
self.key_info_id = Some(id.into());
self
}
pub fn with_references(mut self, references: Vec<XmlDsigReferenceConfig>) -> Self {
self.references = references;
self
}
pub fn with_signature_placement(mut self, placement: SignaturePlacement) -> Self {
self.signature_placement = placement;
self
}
pub fn id_policy(&self) -> &IdAttributePolicy {
&self.id_policy
}
pub fn canonicalization(&self) -> CanonicalizationAlgorithm {
self.canonicalization
}
pub fn digest_algorithm(&self) -> DigestAlgorithm {
self.digest_algorithm
}
pub fn signature_algorithm(&self) -> SignatureAlgorithm {
self.signature_algorithm
}
pub fn document_id(&self) -> &str {
&self.document_id
}
pub fn signature_id(&self) -> &str {
&self.signature_id
}
pub fn key_info_id(&self) -> Option<&str> {
self.key_info_id.as_deref()
}
pub fn references(&self) -> &[XmlDsigReferenceConfig] {
&self.references
}
pub fn signature_placement(&self) -> &SignaturePlacement {
&self.signature_placement
}
}
impl Default for XmlDsigConfig {
fn default() -> Self {
Self {
id_policy: IdAttributePolicy::Standard,
canonicalization: CanonicalizationAlgorithm::CanonicalXml11,
digest_algorithm: DigestAlgorithm::Sha256,
signature_algorithm: SignatureAlgorithm::RsaSha256,
document_id: "xdoc-doc-1".to_owned(),
signature_id: "xdoc-sig-1".to_owned(),
key_info_id: None,
references: vec![XmlDsigReferenceConfig::document_id()],
signature_placement: SignaturePlacement::Root,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SignaturePlacement {
Root,
ParentNode(NodeId),
Query {
source: String,
namespaces: NamespaceContext,
},
}
impl SignaturePlacement {
pub fn root() -> Self {
Self::Root
}
pub fn parent_node(node: NodeId) -> Self {
Self::ParentNode(node)
}
pub fn query(source: impl Into<String>) -> Self {
Self::Query {
source: source.into(),
namespaces: NamespaceContext::new(),
}
}
pub fn query_with_context(source: impl Into<String>, namespaces: NamespaceContext) -> Self {
Self::Query {
source: source.into(),
namespaces,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XmlDsigReferenceConfig {
target: XmlDsigReferenceTarget,
transforms: Option<Vec<Transform>>,
digest_algorithm: Option<DigestAlgorithm>,
}
impl XmlDsigReferenceConfig {
pub fn document_id() -> Self {
Self::new(XmlDsigReferenceTarget::DocumentId)
}
pub fn whole_document() -> Self {
Self::new(XmlDsigReferenceTarget::WholeDocument)
}
pub fn key_info() -> Self {
Self::new(XmlDsigReferenceTarget::KeyInfo)
}
pub fn new(target: XmlDsigReferenceTarget) -> Self {
Self {
target,
transforms: None,
digest_algorithm: None,
}
}
pub fn with_transforms(mut self, transforms: Vec<Transform>) -> Self {
self.transforms = Some(transforms);
self
}
pub fn with_digest_algorithm(mut self, digest_algorithm: DigestAlgorithm) -> Self {
self.digest_algorithm = Some(digest_algorithm);
self
}
pub fn target(&self) -> XmlDsigReferenceTarget {
self.target
}
pub fn transforms(&self) -> Option<&[Transform]> {
self.transforms.as_deref()
}
pub fn digest_algorithm(&self) -> Option<DigestAlgorithm> {
self.digest_algorithm
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum XmlDsigReferenceTarget {
DocumentId,
WholeDocument,
KeyInfo,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedInfo {
pub canonicalization_algorithm: CanonicalizationAlgorithm,
pub signature_algorithm: SignatureAlgorithm,
pub references: Vec<Reference>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reference {
pub uri: String,
pub type_uri: Option<String>,
pub transforms: Vec<Transform>,
pub digest_algorithm: DigestAlgorithm,
pub digest_value: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Transform {
EnvelopedSignature,
Canonicalization(CanonicalizationAlgorithm),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignatureValue(pub Vec<u8>);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyInfo {
pub certificate_der: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerificationReport {
pub valid: bool,
pub reference_results: Vec<ReferenceValidationResult>,
pub signature_value_valid: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReferenceValidationResult {
pub uri: String,
pub valid: bool,
}
pub fn sign_enveloped(
document: &Document,
provider: &impl SigningProvider,
config: &XmlDsigConfig,
) -> XmlResult<Document> {
config.digest_algorithm.ensure_allowed_for_generation()?;
config.signature_algorithm.ensure_allowed_for_generation()?;
validate_reference_configs(config)?;
let mut signed = document.clone();
let root = signed.root().ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
"cannot sign a document without a root element",
)
})?;
let document_id = if config
.references
.iter()
.any(|reference| reference.target == XmlDsigReferenceTarget::DocumentId)
{
Some(ensure_root_id(&mut signed, root, config)?)
} else {
None
};
let signature_parent = resolve_signature_parent(&signed, config)?;
let certificate = provider.certificate_der()?;
let key_info_id = key_info_reference_id(config);
let canonicalization = CanonicalizationConfig::new(config.canonicalization);
let signature = signed.add_element(
signature_parent,
QName::qualified("ds", "Signature", XMLDSIG_NAMESPACE_URI)?,
)?;
signed.add_namespace_declaration(
signature,
NamespaceDeclaration::prefixed("ds", XMLDSIG_NAMESPACE_URI)?,
)?;
signed.add_attribute(
signature,
Attribute::new(QName::new("Id")?, config.signature_id.clone()),
)?;
let signed_info = add_signed_info_element(&mut signed, signature)?;
let signature_value_node = signed.add_element(
signature,
QName::qualified("ds", "SignatureValue", XMLDSIG_NAMESPACE_URI)?,
)?;
let key_info = add_key_info(&mut signed, signature, &certificate, key_info_id)?;
let references = build_references(
&signed,
root,
signature,
document_id.as_deref(),
Some(key_info),
config,
)?;
populate_signed_info(&mut signed, signed_info, config, &references)?;
let signed_info_bytes = canonicalize_node(&signed, signed_info, &canonicalization)?;
let signature_value = provider.sign(config.signature_algorithm, &signed_info_bytes)?;
signed.add_text(
signature_value_node,
encode_standard_base64(&signature_value),
)?;
super::ensure_unique_ids(&signed, &config.id_policy)?;
Ok(signed)
}
pub fn verify_enveloped(
document: &Document,
provider: &impl SigningProvider,
config: &XmlDsigConfig,
) -> XmlResult<VerificationReport> {
let signature = find_signature(document)?;
let signed_info = required_child(document, signature, "SignedInfo")?;
let signature_value = required_child_text(document, signature, "SignatureValue")?;
let signature_value = decode_standard_base64(&signature_value)?;
let parsed = parse_signed_info(document, signed_info)?;
let mut reference_results = Vec::new();
for reference in &parsed.references {
let valid = verify_reference(document, signature, reference, config)?;
reference_results.push(ReferenceValidationResult {
uri: reference.uri.clone(),
valid,
});
}
let canonicalization = CanonicalizationConfig::new(parsed.canonicalization_algorithm);
let signed_info_bytes = canonicalize_node(document, signed_info, &canonicalization)?;
let signature_value_valid = provider.verify(
parsed.signature_algorithm,
&signed_info_bytes,
&signature_value,
)?;
let references_valid = reference_results.iter().all(|result| result.valid);
Ok(VerificationReport {
valid: references_valid && signature_value_valid,
reference_results,
signature_value_valid,
})
}
pub(crate) fn resolve_signature_parent(
document: &Document,
config: &XmlDsigConfig,
) -> XmlResult<NodeId> {
let parent = match &config.signature_placement {
SignaturePlacement::Root => document.root().ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
"cannot place signature in a document without a root element",
)
})?,
SignaturePlacement::ParentNode(node) => *node,
SignaturePlacement::Query { source, namespaces } => {
let query = Query::parse(source)?;
let result = query.evaluate_with_context(document, namespaces)?;
let values = result.values();
match values {
[QueryValue::Node(node)] => *node,
[] => {
return Err(XmlError::new(
ErrorKind::Signature,
format!("signature placement query `{source}` did not match any node"),
));
}
[_] => {
return Err(XmlError::new(
ErrorKind::Signature,
format!("signature placement query `{source}` must select an element node"),
));
}
_ => {
return Err(XmlError::new(
ErrorKind::Signature,
format!("signature placement query `{source}` is ambiguous"),
));
}
}
}
};
ensure_signature_parent(document, parent)?;
Ok(parent)
}
fn ensure_signature_parent(document: &Document, parent: NodeId) -> XmlResult<()> {
match document.node(parent)?.kind() {
NodeKind::Element(_) => Ok(()),
_ => Err(XmlError::new(
ErrorKind::Signature,
"signature placement target must be an element node",
)),
}
}
pub(crate) fn validate_reference_configs(config: &XmlDsigConfig) -> XmlResult<()> {
if config.references.is_empty() {
return Err(XmlError::new(
ErrorKind::Signature,
"XMLDSig config must contain at least one reference",
));
}
for reference in &config.references {
let digest_algorithm = reference
.digest_algorithm
.unwrap_or(config.digest_algorithm);
digest_algorithm.ensure_allowed_for_generation()?;
let transforms = reference_transforms(reference, config);
ensure_reference_transforms(reference.target, &transforms)?;
}
Ok(())
}
pub(crate) fn key_info_reference_id(config: &XmlDsigConfig) -> Option<&str> {
config
.references
.iter()
.any(|reference| reference.target == XmlDsigReferenceTarget::KeyInfo)
.then(|| config.key_info_id.as_deref().unwrap_or(DEFAULT_KEY_INFO_ID))
}
pub(crate) fn build_references(
document: &Document,
root: NodeId,
signature: NodeId,
document_id: Option<&str>,
key_info: Option<NodeId>,
config: &XmlDsigConfig,
) -> XmlResult<Vec<Reference>> {
let mut references = Vec::with_capacity(config.references.len());
for reference in &config.references {
references.push(build_reference(
document,
root,
signature,
document_id,
key_info,
config,
reference,
)?);
}
Ok(references)
}
fn build_reference(
document: &Document,
root: NodeId,
signature: NodeId,
document_id: Option<&str>,
key_info: Option<NodeId>,
config: &XmlDsigConfig,
reference: &XmlDsigReferenceConfig,
) -> XmlResult<Reference> {
let transforms = reference_transforms(reference, config);
let digest_algorithm = reference
.digest_algorithm
.unwrap_or(config.digest_algorithm);
let (uri, canonicalized) = match reference.target {
XmlDsigReferenceTarget::DocumentId => {
let document_id = document_id.ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
"document ID reference requires a document Id",
)
})?;
(
format!("#{document_id}"),
canonicalize_reference_node(document, root, Some(signature), &transforms)?,
)
}
XmlDsigReferenceTarget::WholeDocument => (
String::new(),
canonicalize_reference_node(document, root, Some(signature), &transforms)?,
),
XmlDsigReferenceTarget::KeyInfo => {
let key_info = key_info.ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
"KeyInfo reference requires a KeyInfo element",
)
})?;
let key_info_id = key_info_reference_id(config).ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
"KeyInfo reference requires a KeyInfo Id",
)
})?;
(
format!("#{key_info_id}"),
canonicalize_reference_node(document, key_info, None, &transforms)?,
)
}
};
Ok(Reference {
uri,
type_uri: None,
transforms,
digest_algorithm,
digest_value: digest_bytes(digest_algorithm, canonicalized)?,
})
}
fn reference_transforms(
reference: &XmlDsigReferenceConfig,
config: &XmlDsigConfig,
) -> Vec<Transform> {
reference
.transforms
.clone()
.unwrap_or_else(|| default_transforms_for_target(reference.target, config.canonicalization))
}
fn default_transforms_for_target(
target: XmlDsigReferenceTarget,
canonicalization: CanonicalizationAlgorithm,
) -> Vec<Transform> {
match target {
XmlDsigReferenceTarget::DocumentId | XmlDsigReferenceTarget::WholeDocument => vec![
Transform::EnvelopedSignature,
Transform::Canonicalization(canonicalization),
],
XmlDsigReferenceTarget::KeyInfo => vec![Transform::Canonicalization(canonicalization)],
}
}
fn ensure_reference_transforms(
target: XmlDsigReferenceTarget,
transforms: &[Transform],
) -> XmlResult<()> {
if canonicalization_from_transforms(transforms).is_none() {
return Err(XmlError::new(
ErrorKind::Signature,
"XMLDSig reference must include a canonicalization transform",
));
}
if matches!(
target,
XmlDsigReferenceTarget::DocumentId | XmlDsigReferenceTarget::WholeDocument
) && !transforms
.iter()
.any(|transform| matches!(transform, Transform::EnvelopedSignature))
{
return Err(XmlError::new(
ErrorKind::Signature,
"document XMLDSig reference must include enveloped-signature transform",
));
}
Ok(())
}
fn canonicalization_from_transforms(transforms: &[Transform]) -> Option<CanonicalizationAlgorithm> {
transforms.iter().find_map(|transform| match transform {
Transform::Canonicalization(algorithm) => Some(*algorithm),
Transform::EnvelopedSignature => None,
})
}
fn canonicalize_reference_node(
document: &Document,
target: NodeId,
signature: Option<NodeId>,
transforms: &[Transform],
) -> XmlResult<Vec<u8>> {
let canonicalization = canonicalization_from_transforms(transforms).ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
"reference is missing a canonicalization transform",
)
})?;
let c14n_config = CanonicalizationConfig::new(canonicalization);
if let Some(signature) = signature.filter(|_| {
transforms
.iter()
.any(|transform| matches!(transform, Transform::EnvelopedSignature))
}) {
canonicalize_node_excluding(document, target, &[signature], &c14n_config)
} else {
canonicalize_node(document, target, &c14n_config)
}
}
pub(crate) fn add_key_info(
document: &mut Document,
signature: NodeId,
certificate: &[u8],
key_info_id: Option<&str>,
) -> XmlResult<NodeId> {
let key_info = document.add_element(
signature,
QName::qualified("ds", "KeyInfo", XMLDSIG_NAMESPACE_URI)?,
)?;
populate_key_info(document, key_info, certificate, key_info_id)?;
Ok(key_info)
}
fn populate_key_info(
document: &mut Document,
key_info: NodeId,
certificate: &[u8],
key_info_id: Option<&str>,
) -> XmlResult<()> {
if let Some(key_info_id) = key_info_id {
document.add_namespace_declaration(
key_info,
NamespaceDeclaration::prefixed("ds", XMLDSIG_NAMESPACE_URI)?,
)?;
document.add_attribute(key_info, Attribute::new(QName::new("Id")?, key_info_id))?;
}
let x509_data = document.add_element(
key_info,
QName::qualified("ds", "X509Data", XMLDSIG_NAMESPACE_URI)?,
)?;
let x509_cert = document.add_element(
x509_data,
QName::qualified("ds", "X509Certificate", XMLDSIG_NAMESPACE_URI)?,
)?;
document.add_text(x509_cert, encode_standard_base64(certificate))?;
Ok(())
}
pub(crate) fn ensure_root_id(
document: &mut Document,
root: NodeId,
config: &XmlDsigConfig,
) -> XmlResult<String> {
if let Some(id) = element_id(document, root, &config.id_policy)? {
return Ok(id);
}
document.add_attribute(
root,
Attribute::new(QName::new("Id")?, config.document_id.clone()),
)?;
super::ensure_unique_ids(document, &config.id_policy)?;
Ok(config.document_id.clone())
}
fn element_id(
document: &Document,
node: NodeId,
policy: &IdAttributePolicy,
) -> XmlResult<Option<String>> {
super::ensure_unique_ids(document, policy)?;
let NodeKind::Element(element) = document.node(node)?.kind() else {
return Ok(None);
};
for attribute in element.attributes() {
if !super::ids::is_id_attribute_name(policy, attribute.name()) {
continue;
}
let found = find_element_by_id(document, attribute.value(), policy);
if matches!(found, Ok(found_node) if found_node == node) {
return Ok(Some(attribute.value().to_owned()));
}
}
Ok(None)
}
pub(crate) fn add_signed_info_element(
document: &mut Document,
signature: NodeId,
) -> XmlResult<NodeId> {
let signed_info = document.add_element(
signature,
QName::qualified("ds", "SignedInfo", XMLDSIG_NAMESPACE_URI)?,
)?;
document.add_namespace_declaration(
signed_info,
NamespaceDeclaration::prefixed("ds", XMLDSIG_NAMESPACE_URI)?,
)?;
Ok(signed_info)
}
pub(crate) fn populate_signed_info(
document: &mut Document,
signed_info: NodeId,
config: &XmlDsigConfig,
references: &[Reference],
) -> XmlResult<()> {
let c14n_method = document.add_element(
signed_info,
QName::qualified("ds", "CanonicalizationMethod", XMLDSIG_NAMESPACE_URI)?,
)?;
document.add_attribute(
c14n_method,
Attribute::new(QName::new("Algorithm")?, config.canonicalization.uri()),
)?;
let signature_method = document.add_element(
signed_info,
QName::qualified("ds", "SignatureMethod", XMLDSIG_NAMESPACE_URI)?,
)?;
document.add_attribute(
signature_method,
Attribute::new(QName::new("Algorithm")?, config.signature_algorithm.uri()),
)?;
for reference in references {
add_reference(document, signed_info, reference)?;
}
Ok(())
}
fn add_reference(
document: &mut Document,
signed_info: NodeId,
reference: &Reference,
) -> XmlResult<()> {
let reference_node = document.add_element(
signed_info,
QName::qualified("ds", "Reference", XMLDSIG_NAMESPACE_URI)?,
)?;
document.add_attribute(
reference_node,
Attribute::new(QName::new("URI")?, reference.uri.clone()),
)?;
if let Some(type_uri) = &reference.type_uri {
document.add_attribute(
reference_node,
Attribute::new(QName::new("Type")?, type_uri),
)?;
}
let transforms = document.add_element(
reference_node,
QName::qualified("ds", "Transforms", XMLDSIG_NAMESPACE_URI)?,
)?;
for transform in &reference.transforms {
let transform_node = document.add_element(
transforms,
QName::qualified("ds", "Transform", XMLDSIG_NAMESPACE_URI)?,
)?;
document.add_attribute(
transform_node,
Attribute::new(QName::new("Algorithm")?, transform.uri()),
)?;
}
let digest_method = document.add_element(
reference_node,
QName::qualified("ds", "DigestMethod", XMLDSIG_NAMESPACE_URI)?,
)?;
document.add_attribute(
digest_method,
Attribute::new(QName::new("Algorithm")?, reference.digest_algorithm.uri()),
)?;
let digest_value = document.add_element(
reference_node,
QName::qualified("ds", "DigestValue", XMLDSIG_NAMESPACE_URI)?,
)?;
document.add_text(
digest_value,
encode_standard_base64(&reference.digest_value),
)?;
Ok(())
}
fn verify_reference(
document: &Document,
signature: NodeId,
reference: &Reference,
config: &XmlDsigConfig,
) -> XmlResult<bool> {
let target = reference_target(document, &reference.uri, config)?;
let canonicalized =
canonicalize_reference_node(document, target, Some(signature), &reference.transforms)?;
let actual = digest_bytes(reference.digest_algorithm, canonicalized)?;
Ok(actual == reference.digest_value)
}
fn reference_target(document: &Document, uri: &str, config: &XmlDsigConfig) -> XmlResult<NodeId> {
if uri.is_empty() {
return document.root().ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
"cannot verify whole-document reference without a root element",
)
});
}
let id = uri.strip_prefix('#').ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
format!("unsupported reference URI `{uri}`"),
)
})?;
find_element_by_id(document, id, &config.id_policy)
}
pub(crate) fn parse_signed_info(document: &Document, signed_info: NodeId) -> XmlResult<SignedInfo> {
let canonicalization_method = required_child(document, signed_info, "CanonicalizationMethod")?;
let signature_method = required_child(document, signed_info, "SignatureMethod")?;
let canonicalization_algorithm = CanonicalizationAlgorithm::from_uri(&required_attribute(
document,
canonicalization_method,
"Algorithm",
)?)?;
let signature_algorithm = SignatureAlgorithm::from_uri(&required_attribute(
document,
signature_method,
"Algorithm",
)?)?;
signature_algorithm.ensure_allowed_for_generation()?;
let mut references = Vec::new();
for child in element_children(document, signed_info)? {
if element_local_name(document, child)? == "Reference" {
references.push(parse_reference(document, child)?);
}
}
if references.is_empty() {
return Err(XmlError::new(
ErrorKind::Signature,
"SignedInfo must contain at least one Reference",
));
}
Ok(SignedInfo {
canonicalization_algorithm,
signature_algorithm,
references,
})
}
fn parse_reference(document: &Document, reference: NodeId) -> XmlResult<Reference> {
let uri = required_attribute(document, reference, "URI")?;
let type_uri = optional_attribute(document, reference, "Type")?;
let transforms_node = required_child(document, reference, "Transforms")?;
let mut transforms = Vec::new();
for transform in element_children(document, transforms_node)? {
if element_local_name(document, transform)? != "Transform" {
continue;
}
transforms.push(Transform::from_uri(&required_attribute(
document,
transform,
"Algorithm",
)?)?);
}
let digest_method = required_child(document, reference, "DigestMethod")?;
let digest_algorithm =
DigestAlgorithm::from_uri(&required_attribute(document, digest_method, "Algorithm")?)?;
digest_algorithm.ensure_allowed_for_generation()?;
let digest_value =
decode_standard_base64(&required_child_text(document, reference, "DigestValue")?)?;
Ok(Reference {
uri,
type_uri,
transforms,
digest_algorithm,
digest_value,
})
}
pub(crate) fn find_signature(document: &Document) -> XmlResult<NodeId> {
let root = document.root().ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
"cannot verify a document without a root element",
)
})?;
let mut signatures = Vec::new();
collect_elements_by_local_name(document, root, "Signature", &mut signatures)?;
match signatures.as_slice() {
[signature] => Ok(*signature),
[] => Err(XmlError::new(
ErrorKind::Signature,
"document does not contain a ds:Signature element",
)),
_ => Err(XmlError::new(
ErrorKind::Signature,
"document contains multiple Signature elements; verification target is ambiguous",
)),
}
}
fn collect_elements_by_local_name(
document: &Document,
node: NodeId,
local: &str,
matches: &mut Vec<NodeId>,
) -> XmlResult<()> {
let NodeKind::Element(element) = document.node(node)?.kind() else {
return Ok(());
};
if element.name().namespace_uri().map(|uri| uri.as_str()) == Some(XMLDSIG_NAMESPACE_URI)
&& element.name().local() == local
{
matches.push(node);
}
for child in element.children() {
collect_elements_by_local_name(document, *child, local, matches)?;
}
Ok(())
}
pub(crate) fn required_child(
document: &Document,
parent: NodeId,
local: &str,
) -> XmlResult<NodeId> {
element_children(document, parent)?
.into_iter()
.find(|child| element_local_name(document, *child).as_deref() == Ok(local))
.ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
format!("missing required XMLDSig child `{local}`"),
)
})
}
pub(crate) fn required_child_text(
document: &Document,
parent: NodeId,
local: &str,
) -> XmlResult<String> {
let child = required_child(document, parent, local)?;
let mut text = String::new();
for node in element_children_or_text(document, child)? {
if let NodeKind::Text(value) = document.node(node)?.kind() {
text.push_str(value);
}
}
if text.is_empty() {
return Err(XmlError::new(
ErrorKind::Signature,
format!("XMLDSig child `{local}` must contain text"),
));
}
Ok(text)
}
pub(crate) fn required_attribute(
document: &Document,
node: NodeId,
local: &str,
) -> XmlResult<String> {
optional_attribute(document, node, local)?.ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
format!("missing required XMLDSig attribute `{local}`"),
)
})
}
pub(crate) fn optional_attribute(
document: &Document,
node: NodeId,
local: &str,
) -> XmlResult<Option<String>> {
let NodeKind::Element(element) = document.node(node)?.kind() else {
return Ok(None);
};
Ok(element
.attributes()
.iter()
.find(|attribute| attribute.name().local() == local)
.map(|attribute| attribute.value().to_owned()))
}
pub(crate) fn element_children(document: &Document, parent: NodeId) -> XmlResult<Vec<NodeId>> {
let NodeKind::Element(element) = document.node(parent)?.kind() else {
return Ok(Vec::new());
};
Ok(element
.children()
.iter()
.copied()
.filter(|child| {
matches!(
document.node(*child).map(|node| node.kind()),
Ok(NodeKind::Element(_))
)
})
.collect())
}
fn element_children_or_text(document: &Document, parent: NodeId) -> XmlResult<Vec<NodeId>> {
let NodeKind::Element(element) = document.node(parent)?.kind() else {
return Ok(Vec::new());
};
Ok(element.children().to_vec())
}
pub(crate) fn element_local_name(document: &Document, node: NodeId) -> XmlResult<String> {
match document.node(node)?.kind() {
NodeKind::Element(element) => Ok(element.name().local().to_owned()),
_ => Err(XmlError::new(
ErrorKind::Signature,
"expected XMLDSig element node",
)),
}
}
impl Transform {
fn uri(&self) -> &'static str {
match self {
Self::EnvelopedSignature => XMLDSIG_ENVELOPED_SIGNATURE_URI,
Self::Canonicalization(algorithm) => algorithm.uri(),
}
}
fn from_uri(uri: &str) -> XmlResult<Self> {
if uri == XMLDSIG_ENVELOPED_SIGNATURE_URI {
return Ok(Self::EnvelopedSignature);
}
Ok(Self::Canonicalization(CanonicalizationAlgorithm::from_uri(
uri,
)?))
}
}
#[cfg(test)]
mod tests {
use crate::core::Attribute;
use crate::parser::parse_str;
use crate::signature::DeterministicSigningProvider;
use crate::writer::to_string_compact;
use super::*;
fn provider() -> DeterministicSigningProvider {
DeterministicSigningProvider::new(b"test-cert".to_vec(), b"test-secret".to_vec())
}
fn unsigned_document() -> XmlResult<Document> {
parse_str(r#"<Root Id="doc-1"><Item>value</Item></Root>"#)
}
fn extension_document() -> XmlResult<Document> {
parse_str(r#"<Root Id="doc-1"><Extension><Payload>value</Payload></Extension></Root>"#)
}
#[test]
fn xmldsig_signs_and_verifies_enveloped_document() -> XmlResult<()> {
let signed = sign_enveloped(&unsigned_document()?, &provider(), &XmlDsigConfig::new())?;
let report = verify_enveloped(&signed, &provider(), &XmlDsigConfig::new())?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert!(report.signature_value_valid);
assert_eq!(report.reference_results.len(), 1);
assert!(xml.contains("<ds:Signature"));
assert!(xml.contains("<ds:SignedInfo"));
assert!(xml.contains("<ds:KeyInfo>"));
Ok(())
}
#[test]
fn xmldsig_adds_root_id_when_missing() -> XmlResult<()> {
let document = parse_str("<Root><Item>value</Item></Root>")?;
let signed = sign_enveloped(&document, &provider(), &XmlDsigConfig::new())?;
let root = signed.root().expect("root");
let NodeKind::Element(element) = signed.node(root)?.kind() else {
panic!("root is element");
};
assert!(
element
.attributes()
.iter()
.any(|attribute| attribute.name().local() == "Id"
&& attribute.value() == "xdoc-doc-1")
);
assert!(verify_enveloped(&signed, &provider(), &XmlDsigConfig::new())?.valid);
Ok(())
}
#[test]
fn signature_placement_can_use_explicit_parent_node() -> XmlResult<()> {
let document = extension_document()?;
let extension = Query::parse("/Root/Extension")?
.evaluate(&document)?
.nodes()
.pop()
.expect("extension node");
let config = XmlDsigConfig::new()
.with_signature_placement(SignaturePlacement::parent_node(extension));
let signed = sign_enveloped(&document, &provider(), &config)?;
let report = verify_enveloped(&signed, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert!(xml.contains("<Extension><Payload>value</Payload><ds:Signature"));
Ok(())
}
#[test]
fn signature_placement_can_use_query() -> XmlResult<()> {
let config = XmlDsigConfig::new()
.with_signature_placement(SignaturePlacement::query("/Root/Extension"));
let signed = sign_enveloped(&extension_document()?, &provider(), &config)?;
let report = verify_enveloped(&signed, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert!(xml.contains("<Extension><Payload>value</Payload><ds:Signature"));
Ok(())
}
#[test]
fn signature_placement_rejects_ambiguous_query() {
let document = parse_str(r#"<Root Id="doc-1"><Extension/><Extension/></Root>"#)
.expect("valid document");
let config =
XmlDsigConfig::new().with_signature_placement(SignaturePlacement::query("//Extension"));
let error = sign_enveloped(&document, &provider(), &config)
.expect_err("ambiguous placement must fail");
assert_eq!(error.kind(), &ErrorKind::Signature);
assert!(error.message().contains("ambiguous"));
}
#[test]
fn xmldsig_can_select_c14n10_by_config() -> XmlResult<()> {
let config =
XmlDsigConfig::new().with_canonicalization(CanonicalizationAlgorithm::CanonicalXml10);
let signed = sign_enveloped(&unsigned_document()?, &provider(), &config)?;
let report = verify_enveloped(&signed, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert!(xml.contains("ds:CanonicalizationMethod"));
assert!(xml.contains("ds:Transform"));
assert_eq!(
xml.matches(r#"Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315""#)
.count(),
2
);
Ok(())
}
#[test]
fn xmldsig_references_can_target_whole_document_uri() -> XmlResult<()> {
let document = parse_str("<Root><Item>value</Item></Root>")?;
let config =
XmlDsigConfig::new().with_references(vec![XmlDsigReferenceConfig::whole_document()]);
let signed = sign_enveloped(&document, &provider(), &config)?;
let report = verify_enveloped(&signed, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert_eq!(report.reference_results.len(), 1);
assert_eq!(report.reference_results[0].uri, "");
assert!(!xml.contains("xdoc-doc-1"));
assert!(xml.contains(r#"<ds:Reference URI="">"#));
assert!(xml.contains(XMLDSIG_ENVELOPED_SIGNATURE_URI));
Ok(())
}
#[test]
fn key_info_reference_is_signed_and_detects_tampering() -> XmlResult<()> {
let config = XmlDsigConfig::new()
.with_key_info_id("key-info-1")
.with_references(vec![
XmlDsigReferenceConfig::document_id(),
XmlDsigReferenceConfig::key_info(),
]);
let signed = sign_enveloped(&unsigned_document()?, &provider(), &config)?;
let report = verify_enveloped(&signed, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert_eq!(report.reference_results.len(), 2);
assert!(report
.reference_results
.iter()
.any(|result| result.uri == "#key-info-1" && result.valid));
assert!(xml.contains(r##"<ds:Reference URI="#key-info-1">"##));
assert!(xml.contains(
r##"<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="key-info-1">"##
));
let tampered = parse_str(&xml.replace(
&encode_standard_base64(b"test-cert"),
&encode_standard_base64(b"other-cert"),
))?;
let report = verify_enveloped(&tampered, &provider(), &config)?;
assert!(!report.valid);
assert!(report
.reference_results
.iter()
.any(|result| result.uri == "#doc-1" && result.valid));
assert!(report
.reference_results
.iter()
.any(|result| result.uri == "#key-info-1" && !result.valid));
assert!(report.signature_value_valid);
Ok(())
}
#[test]
fn key_info_reference_rejects_duplicate_id() {
let config = XmlDsigConfig::new()
.with_key_info_id("doc-1")
.with_references(vec![
XmlDsigReferenceConfig::document_id(),
XmlDsigReferenceConfig::key_info(),
]);
let error = sign_enveloped(
&unsigned_document().expect("valid document"),
&provider(),
&config,
)
.expect_err("duplicate KeyInfo Id must fail");
assert_eq!(error.kind(), &ErrorKind::Signature);
}
#[test]
fn xmldsig_references_reject_document_without_enveloped_transform() {
let config =
XmlDsigConfig::new().with_references(vec![XmlDsigReferenceConfig::whole_document()
.with_transforms(vec![Transform::Canonicalization(
CanonicalizationAlgorithm::CanonicalXml11,
)])]);
let error = sign_enveloped(
&parse_str("<Root/>").expect("valid document"),
&provider(),
&config,
)
.expect_err("document references require enveloped transform");
assert_eq!(error.kind(), &ErrorKind::Signature);
assert!(error.message().contains("enveloped-signature"));
}
#[test]
fn xmldsig_verify_fails_when_signed_content_changes() -> XmlResult<()> {
let mut signed = sign_enveloped(&unsigned_document()?, &provider(), &XmlDsigConfig::new())?;
let root = signed.root().expect("root");
signed.add_text(root, "tampered")?;
let report = verify_enveloped(&signed, &provider(), &XmlDsigConfig::new())?;
assert!(!report.valid);
assert!(!report.reference_results[0].valid);
Ok(())
}
#[test]
fn xmldsig_verify_fails_when_signature_value_changes() -> XmlResult<()> {
let signed = sign_enveloped(&unsigned_document()?, &provider(), &XmlDsigConfig::new())?;
let signature = find_signature(&signed)?;
let original = required_child_text(&signed, signature, "SignatureValue")?;
let mut tampered = decode_standard_base64(&original)?;
tampered[0] ^= 0xff;
let xml = to_string_compact(&signed)?.replace(&original, &encode_standard_base64(tampered));
let signed = parse_str(&xml)?;
let report = verify_enveloped(&signed, &provider(), &XmlDsigConfig::new())?;
assert!(!report.valid);
assert!(!report.signature_value_valid);
Ok(())
}
#[test]
fn xmldsig_verify_rejects_unsupported_transform() -> XmlResult<()> {
let signed = sign_enveloped(&unsigned_document()?, &provider(), &XmlDsigConfig::new())?;
let xml = to_string_compact(&signed)?
.replace(XMLDSIG_ENVELOPED_SIGNATURE_URI, "urn:unsupported-transform");
let document = parse_str(&xml)?;
let error = verify_enveloped(&document, &provider(), &XmlDsigConfig::new())
.expect_err("unsupported transform must fail");
assert_eq!(error.kind(), &ErrorKind::Signature);
assert!(error
.message()
.contains("unsupported canonicalization algorithm"));
Ok(())
}
#[test]
fn xmldsig_verify_rejects_ambiguous_references() -> XmlResult<()> {
let mut document = unsigned_document()?;
let root = document.root().expect("root");
let duplicate = document.add_element(root, QName::new("Duplicate")?)?;
document.add_attribute(duplicate, Attribute::new(QName::new("Id")?, "doc-1"))?;
let error = sign_enveloped(&document, &provider(), &XmlDsigConfig::new())
.expect_err("duplicate IDs must fail");
assert_eq!(error.kind(), &ErrorKind::Signature);
Ok(())
}
}