Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ rsa = { version = "0.10.0-rc.18", optional = true }
sha1 = { version = "0.11", features = ["oid"], optional = true }
sha2 = { version = "0.11", features = ["oid"], optional = true }
p256 = { version = "0.14", features = ["ecdsa"], optional = true }
p384 = { version = "0.14.0-rc.15", features = ["ecdsa"], optional = true }
p384 = { version = "0.14", features = ["ecdsa"], optional = true }
Comment thread
greptile-apps[bot] marked this conversation as resolved.
p521 = { version = "0.14.0-rc.15", features = ["ecdsa"], optional = true }
signature = { version = "3", optional = true }
subtle = { version = "2", optional = true }
getrandom = { version = "0.4", features = ["sys_rng"], optional = true }

# X.509 certificates
x509-parser = { version = "0.18", features = ["verify"], optional = true }
Expand All @@ -47,6 +48,7 @@ default = ["xmldsig", "c14n"]
xmldsig = [ # XML Digital Signatures (sign + verify)
"dep:der",
"dep:crypto-bigint",
"dep:getrandom",
"dep:p256",
"dep:p384",
"dep:p521",
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Pure Rust XML Security library. Drop-in replacement for libxmlsec1.
## Features

- **C14N** — XML Canonicalization (inclusive + exclusive, W3C compliant)
- **XMLDSig** — XML Digital Signatures (verify pipeline implemented; signing in progress, enveloped/enveloping/detached)
- **XMLDSig** — XML Digital Signatures (verify pipeline + template signing implemented; broader signing interop in progress)
- **XMLEnc** — XML Encryption (symmetric + asymmetric)
- **X.509** — Certificate-based key extraction and validation

Expand All @@ -39,13 +39,15 @@ Currently implemented (core paths):
- C14N 1.0, C14N 1.1, and Exclusive C14N
- XMLDSig parsing, same-document URI dereference, transform chains, and digest verification
- XMLDSig full verify pipeline (`SignedInfo` canonicalization + `SignatureValue` verification)
- XMLDSig template signing pipeline (`DigestValue` fill + `SignedInfo` canonicalization + `SignatureValue` fill)
- Built-in verification-key resolution from embedded X.509/DER/`KeyValue` sources and configured `KeyName`, X.509 subject, issuer/serial, SKI, or digest selectors
- RSA PKCS#1 v1.5 verification helpers for SHA-1 / SHA-256 / SHA-384 / SHA-512
- ECDSA verification helpers for P-256/SHA-256 and P-384/SHA-384
- RSA PKCS#1 v1.5 and ECDSA P-256/P-384 signing from PKCS#8 private keys
- Opt-in X.509 certificate-chain validation with explicit trust anchors, validity checks, CA constraints, and CRLs

Still in progress:
- XMLDSig signing pipeline
- XMLDSig signing KeyInfo writer, examples, and broader donor/CLI interop coverage
- XMLEnc encryption/decryption pipeline

Current toolchain target: latest stable Rust.
Expand Down
6 changes: 6 additions & 0 deletions src/xmldsig/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod digest;
pub mod keys;
pub mod mutation;
pub mod parse;
pub mod sign;
pub mod signature;
pub mod transforms;
pub mod types;
Expand All @@ -28,6 +29,11 @@ pub use parse::{
KeyInfo, KeyInfoSource, KeyValueInfo, ParseError, Reference, SignatureAlgorithm, SignedInfo,
X509DataInfo, find_signature_node, parse_key_info, parse_signed_info,
};
pub use sign::{
ComputedReferenceDigest, EcdsaP256SigningKey, EcdsaP384SigningKey, RsaSigningKey, SignContext,
SigningDigestError, SigningError, SigningKey, SigningKeyError, compute_reference_digest_values,
fill_reference_digest_values,
};
pub use signature::{
SignatureVerificationError, verify_ecdsa_signature_pem, verify_ecdsa_signature_spki,
verify_rsa_signature_pem, verify_rsa_signature_spki,
Expand Down
150 changes: 146 additions & 4 deletions src/xmldsig/mutation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,31 @@ where
fill_dsig_values(xml, "DigestValue", values)
}

/// Fill `<DigestValue>` elements for direct `<SignedInfo>/<Reference>` children.
pub fn fill_signed_info_digest_values<I, S>(
xml: &str,
values: I,
) -> Result<String, XmlMutationError>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let values: Vec<String> = values
.into_iter()
.map(|value| value.as_ref().to_owned())
.collect();
let expected = count_signed_info_digest_values(xml)?;
if expected != values.len() {
return Err(XmlMutationError::ValueCountMismatch {
element: "DigestValue",
expected,
actual: values.len(),
});
}

fill_dsig_values_matching(xml, "DigestValue", values, is_signed_info_reference_context)
}

/// Fill XMLDSig `<SignatureValue>` elements in document order.
pub fn fill_signature_values<I, S>(xml: &str, values: I) -> Result<String, XmlMutationError>
where
Expand All @@ -122,6 +147,25 @@ where
fill_dsig_values(xml, "SignatureValue", values)
}

/// Fill the direct `<Signature>/<SignatureValue>` child for a signing template.
pub fn fill_signature_value(xml: &str, value: &str) -> Result<String, XmlMutationError> {
let expected = count_direct_signature_values(xml)?;
if expected != 1 {
return Err(XmlMutationError::ValueCountMismatch {
element: "SignatureValue",
expected,
actual: 1,
});
}

fill_dsig_values_matching(
xml,
"SignatureValue",
vec![value.to_owned()],
is_direct_signature_context,
)
}

fn fill_dsig_values<I, S>(
xml: &str,
local_name: &'static str,
Expand All @@ -144,11 +188,21 @@ where
});
}

fill_dsig_values_matching(xml, local_name, values, |_, _| true)
}

fn fill_dsig_values_matching(
xml: &str,
local_name: &'static str,
values: Vec<String>,
mut should_replace: impl FnMut(&[(bool, Vec<u8>)], &ResolveResult<'_>) -> bool,
) -> Result<String, XmlMutationError> {
let mut reader = NsReader::from_str(xml);
let mut writer = Writer::new(Vec::new());
let mut buf = Vec::new();
let mut value_index = 0usize;
let mut replacing_depth: Option<usize> = None;
let mut element_stack: Vec<(bool, Vec<u8>)> = Vec::new();

loop {
let (namespace, event) = reader.read_resolved_event_into(&mut buf)?;
Expand All @@ -158,6 +212,7 @@ where
Event::End(end) if *depth == 0 => {
writer.write_event(Event::End(end))?;
replacing_depth = None;
element_stack.pop();
}
Event::End(_) => *depth -= 1,
Event::Eof => break,
Expand All @@ -169,21 +224,39 @@ where

match event {
Event::Start(element)
if is_dsig_element(&namespace, element.local_name().as_ref(), local_name) =>
if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
&& should_replace(&element_stack, &namespace) =>
{
element_stack.push((
is_dsig_namespace(&namespace),
element.local_name().as_ref().to_vec(),
));
writer.write_event(Event::Start(element))?;
writer.write_event(Event::Text(BytesText::new(&values[value_index])))?;
value_index += 1;
replacing_depth = Some(0);
}
Event::Empty(element)
if is_dsig_element(&namespace, element.local_name().as_ref(), local_name) =>
if is_dsig_element(&namespace, element.local_name().as_ref(), local_name)
&& should_replace(&element_stack, &namespace) =>
{
writer.write_event(Event::Start(element.borrow()))?;
writer.write_event(Event::Text(BytesText::new(&values[value_index])))?;
value_index += 1;
writer.write_event(Event::End(element.to_end()))?;
}
Event::Start(element) => {
element_stack.push((
is_dsig_namespace(&namespace),
element.local_name().as_ref().to_vec(),
));
writer.write_event(Event::Start(element))?;
}
Event::Empty(element) => writer.write_event(Event::Empty(element))?,
Event::End(element) => {
element_stack.pop();
writer.write_event(Event::End(element))?;
}
Event::Eof => break,
event => writer.write_event(event)?,
}
Expand All @@ -193,7 +266,7 @@ where
if value_index != values.len() {
return Err(XmlMutationError::ValueCountMismatch {
element: local_name,
expected,
expected: values.len(),
actual: value_index,
});
}
Expand Down Expand Up @@ -225,9 +298,78 @@ fn count_dsig_elements(xml: &str, local_name: &str) -> Result<usize, XmlMutation
.count())
}

fn count_signed_info_digest_values(xml: &str) -> Result<usize, XmlMutationError> {
let document = roxmltree::Document::parse(xml)?;
Ok(document
.descendants()
.filter(|node| is_direct_signed_info_reference_digest(*node))
.count())
}

fn count_direct_signature_values(xml: &str) -> Result<usize, XmlMutationError> {
let document = roxmltree::Document::parse(xml)?;
Ok(document
.descendants()
.filter(|node| {
node.is_element()
&& node.tag_name().namespace() == Some(XMLDSIG_NS)
&& node.tag_name().name() == "SignatureValue"
&& node
.parent()
.is_some_and(|parent| is_dsig_node(parent, "Signature"))
})
.count())
}

fn is_direct_signed_info_reference_digest(node: roxmltree::Node<'_, '_>) -> bool {
node.is_element()
&& node.tag_name().namespace() == Some(XMLDSIG_NS)
&& node.tag_name().name() == "DigestValue"
&& node
.parent()
.is_some_and(|parent| is_dsig_node(parent, "Reference"))
&& node
.parent()
.and_then(|parent| parent.parent())
.is_some_and(|grandparent| is_dsig_node(grandparent, "SignedInfo"))
}

fn is_dsig_node(node: roxmltree::Node<'_, '_>, expected_local: &str) -> bool {
node.is_element()
&& node.tag_name().namespace() == Some(XMLDSIG_NS)
&& node.tag_name().name() == expected_local
}

fn is_signed_info_reference_context(
element_stack: &[(bool, Vec<u8>)],
namespace: &ResolveResult<'_>,
) -> bool {
is_dsig_namespace(namespace)
&& matches!(
element_stack,
[.., (true, signed_info), (true, reference)]
if signed_info.as_slice() == b"SignedInfo"
&& reference.as_slice() == b"Reference"
)
}

fn is_direct_signature_context(
element_stack: &[(bool, Vec<u8>)],
namespace: &ResolveResult<'_>,
) -> bool {
is_dsig_namespace(namespace)
&& matches!(
element_stack,
[.., (true, signature)] if signature.as_slice() == b"Signature"
)
}

fn is_dsig_element(namespace: &ResolveResult<'_>, local: &[u8], expected_local: &str) -> bool {
is_dsig_namespace(namespace) && local == expected_local.as_bytes()
}

fn is_dsig_namespace(namespace: &ResolveResult<'_>) -> bool {
matches!(namespace, ResolveResult::Bound(Namespace(ns)) if *ns == XMLDSIG_NS.as_bytes())
&& local == expected_local.as_bytes()
}

#[cfg(test)]
Expand Down
Loading