use roxmltree::{Document, Node};
use x509_parser::extensions::ParsedExtension;
use x509_parser::prelude::FromDer;
use x509_parser::public_key::PublicKey;
use super::digest::DigestAlgorithm;
use super::transforms::{self, Transform};
use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_text};
use crate::c14n::C14nAlgorithm;
pub(crate) const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
pub(crate) const XMLDSIG11_NS: &str = "http://www.w3.org/2009/xmldsig11#";
const MAX_DER_ENCODED_KEY_VALUE_LEN: usize = 8192;
const MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN: usize = 65_536;
const MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN: usize = MAX_DER_ENCODED_KEY_VALUE_LEN.div_ceil(3) * 4;
const MAX_KEY_NAME_TEXT_LEN: usize = 4096;
const MAX_X509_BASE64_TEXT_LEN: usize = 262_144;
const MAX_X509_BASE64_NORMALIZED_LEN: usize = MAX_X509_BASE64_TEXT_LEN;
const MAX_X509_DECODED_BINARY_LEN: usize = MAX_X509_BASE64_NORMALIZED_LEN.div_ceil(4) * 3;
const MAX_X509_SUBJECT_NAME_TEXT_LEN: usize = 16_384;
const MAX_X509_ISSUER_NAME_TEXT_LEN: usize = 16_384;
const MAX_X509_SERIAL_NUMBER_TEXT_LEN: usize = 4096;
const MAX_X509_DATA_ENTRY_COUNT: usize = 64;
const MAX_X509_DATA_TOTAL_BINARY_LEN: usize = 1_048_576;
const MAX_X509_CHAIN_DEPTH: usize = 9;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SignatureAlgorithm {
RsaSha1,
RsaSha256,
RsaSha384,
RsaSha512,
EcdsaP256Sha256,
EcdsaP384Sha384,
}
impl SignatureAlgorithm {
#[must_use]
pub fn from_uri(uri: &str) -> Option<Self> {
match uri {
"http://www.w3.org/2000/09/xmldsig#rsa-sha1" => Some(Self::RsaSha1),
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => Some(Self::RsaSha256),
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" => Some(Self::RsaSha384),
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" => Some(Self::RsaSha512),
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => Some(Self::EcdsaP256Sha256),
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384" => Some(Self::EcdsaP384Sha384),
_ => None,
}
}
#[must_use]
pub fn uri(self) -> &'static str {
match self {
Self::RsaSha1 => "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
Self::RsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
Self::RsaSha384 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
Self::RsaSha512 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
Self::EcdsaP256Sha256 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
Self::EcdsaP384Sha384 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
}
}
#[must_use]
pub fn signing_allowed(self) -> bool {
!matches!(self, Self::RsaSha1)
}
}
#[derive(Debug)]
pub struct SignedInfo {
pub c14n_method: C14nAlgorithm,
pub signature_method: SignatureAlgorithm,
pub references: Vec<Reference>,
}
#[derive(Debug)]
pub struct Reference {
pub uri: Option<String>,
pub id: Option<String>,
pub ref_type: Option<String>,
pub transforms: Vec<Transform>,
pub digest_method: DigestAlgorithm,
pub digest_value: Vec<u8>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct KeyInfo {
pub sources: Vec<KeyInfoSource>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum KeyInfoSource {
KeyName(String),
KeyValue(KeyValueInfo),
X509Data(X509DataInfo),
DerEncodedKeyValue(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum KeyValueInfo {
RsaKeyValue,
EcKeyValue,
Unsupported {
namespace: Option<String>,
local_name: String,
},
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct X509DataInfo {
pub certificates: Vec<Vec<u8>>,
pub subject_names: Vec<String>,
pub issuer_serials: Vec<(String, String)>,
pub skis: Vec<Vec<u8>>,
pub crls: Vec<Vec<u8>>,
pub digests: Vec<(String, Vec<u8>)>,
pub parsed_certificates: Vec<ParsedX509Certificate>,
pub certificate_chain: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ParsedX509Certificate {
pub subject_dn: String,
pub issuer_dn: String,
pub serial_number: Vec<u8>,
pub serial_number_hex: String,
pub subject_key_identifier: Option<Vec<u8>>,
pub public_key: X509PublicKeyInfo,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum X509PublicKeyInfo {
Rsa {
modulus: Vec<u8>,
exponent: Vec<u8>,
},
Ec {
curve_oid: String,
public_key: Vec<u8>,
},
Unsupported {
algorithm_oid: String,
},
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ParseError {
#[error("missing required element: <{element}>")]
MissingElement {
element: &'static str,
},
#[error("invalid structure: {0}")]
InvalidStructure(String),
#[error("unsupported algorithm: {uri}")]
UnsupportedAlgorithm {
uri: String,
},
#[error("base64 decode error: {0}")]
Base64(String),
#[error(
"digest length mismatch for {algorithm}: expected {expected} bytes, got {actual} bytes"
)]
DigestLengthMismatch {
algorithm: &'static str,
expected: usize,
actual: usize,
},
#[error("transform error: {0}")]
Transform(#[from] super::types::TransformError),
}
#[must_use]
pub fn find_signature_node<'a>(doc: &'a Document<'a>) -> Option<Node<'a, 'a>> {
doc.descendants().find(|n| {
n.is_element()
&& n.tag_name().name() == "Signature"
&& n.tag_name().namespace() == Some(XMLDSIG_NS)
})
}
pub fn parse_signed_info(signed_info_node: Node) -> Result<SignedInfo, ParseError> {
verify_ds_element(signed_info_node, "SignedInfo")?;
let mut children = element_children(signed_info_node);
let c14n_node = children.next().ok_or(ParseError::MissingElement {
element: "CanonicalizationMethod",
})?;
verify_ds_element(c14n_node, "CanonicalizationMethod")?;
let c14n_uri = required_algorithm_attr(c14n_node, "CanonicalizationMethod")?;
let mut c14n_method =
C14nAlgorithm::from_uri(c14n_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
uri: c14n_uri.to_string(),
})?;
if let Some(prefix_list) = parse_inclusive_prefixes(c14n_node)? {
if c14n_method.mode() == crate::c14n::C14nMode::Exclusive1_0 {
c14n_method = c14n_method.with_prefix_list(&prefix_list);
} else {
return Err(ParseError::UnsupportedAlgorithm {
uri: c14n_uri.to_string(),
});
}
}
let sig_method_node = children.next().ok_or(ParseError::MissingElement {
element: "SignatureMethod",
})?;
verify_ds_element(sig_method_node, "SignatureMethod")?;
let sig_uri = required_algorithm_attr(sig_method_node, "SignatureMethod")?;
let signature_method =
SignatureAlgorithm::from_uri(sig_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
uri: sig_uri.to_string(),
})?;
let mut references = Vec::new();
for child in children {
verify_ds_element(child, "Reference")?;
references.push(parse_reference(child)?);
}
if references.is_empty() {
return Err(ParseError::MissingElement {
element: "Reference",
});
}
Ok(SignedInfo {
c14n_method,
signature_method,
references,
})
}
pub(crate) fn parse_reference(reference_node: Node) -> Result<Reference, ParseError> {
let uri = reference_node.attribute("URI").map(String::from);
let id = reference_node.attribute("Id").map(String::from);
let ref_type = reference_node.attribute("Type").map(String::from);
let mut children = element_children(reference_node);
let mut transforms = Vec::new();
let mut next = children.next().ok_or(ParseError::MissingElement {
element: "DigestMethod",
})?;
if next.tag_name().name() == "Transforms" && next.tag_name().namespace() == Some(XMLDSIG_NS) {
transforms = transforms::parse_transforms(next)?;
next = children.next().ok_or(ParseError::MissingElement {
element: "DigestMethod",
})?;
}
verify_ds_element(next, "DigestMethod")?;
let digest_uri = required_algorithm_attr(next, "DigestMethod")?;
let digest_method =
DigestAlgorithm::from_uri(digest_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
uri: digest_uri.to_string(),
})?;
let digest_value_node = children.next().ok_or(ParseError::MissingElement {
element: "DigestValue",
})?;
verify_ds_element(digest_value_node, "DigestValue")?;
let digest_value = decode_digest_value_children(digest_value_node, digest_method)?;
if let Some(unexpected) = children.next() {
return Err(ParseError::InvalidStructure(format!(
"unexpected element <{}> after <DigestValue> in <Reference>",
unexpected.tag_name().name()
)));
}
Ok(Reference {
uri,
id,
ref_type,
transforms,
digest_method,
digest_value,
})
}
pub fn parse_key_info(key_info_node: Node) -> Result<KeyInfo, ParseError> {
verify_ds_element(key_info_node, "KeyInfo")?;
ensure_no_non_whitespace_text(key_info_node, "KeyInfo")?;
let mut sources = Vec::new();
for child in element_children(key_info_node) {
match (child.tag_name().namespace(), child.tag_name().name()) {
(Some(XMLDSIG_NS), "KeyName") => {
ensure_no_element_children(child, "KeyName")?;
let key_name =
collect_text_content_bounded(child, MAX_KEY_NAME_TEXT_LEN, "KeyName")?;
sources.push(KeyInfoSource::KeyName(key_name));
}
(Some(XMLDSIG_NS), "KeyValue") => {
let key_value = parse_key_value_dispatch(child)?;
sources.push(KeyInfoSource::KeyValue(key_value));
}
(Some(XMLDSIG_NS), "X509Data") => {
let x509 = parse_x509_data_dispatch(child)?;
sources.push(KeyInfoSource::X509Data(x509));
}
(Some(XMLDSIG11_NS), "DEREncodedKeyValue") => {
ensure_no_element_children(child, "DEREncodedKeyValue")?;
let der = decode_der_encoded_key_value_base64(child)?;
sources.push(KeyInfoSource::DerEncodedKeyValue(der));
}
_ => {}
}
}
Ok(KeyInfo { sources })
}
fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
node.children().filter(|n| n.is_element())
}
fn verify_ds_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
if !node.is_element() {
return Err(ParseError::InvalidStructure(format!(
"expected element <{expected_name}>, got non-element node"
)));
}
let tag = node.tag_name();
if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG_NS) {
return Err(ParseError::InvalidStructure(format!(
"expected <ds:{expected_name}>, got <{}{}>",
tag.namespace()
.map(|ns| format!("{{{ns}}}"))
.unwrap_or_default(),
tag.name()
)));
}
Ok(())
}
fn required_algorithm_attr<'a>(
node: Node<'a, 'a>,
element_name: &'static str,
) -> Result<&'a str, ParseError> {
node.attribute("Algorithm").ok_or_else(|| {
ParseError::InvalidStructure(format!("missing Algorithm attribute on <{element_name}>"))
})
}
fn parse_inclusive_prefixes(node: Node) -> Result<Option<String>, ParseError> {
const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
for child in node.children() {
if child.is_element() {
let tag = child.tag_name();
if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
{
return child
.attribute("PrefixList")
.map(str::to_string)
.ok_or_else(|| {
ParseError::InvalidStructure(
"missing PrefixList attribute on <InclusiveNamespaces>".into(),
)
})
.map(Some);
}
}
}
Ok(None)
}
fn parse_key_value_dispatch(node: Node) -> Result<KeyValueInfo, ParseError> {
verify_ds_element(node, "KeyValue")?;
ensure_no_non_whitespace_text(node, "KeyValue")?;
let mut children = element_children(node);
let Some(first_child) = children.next() else {
return Err(ParseError::InvalidStructure(
"KeyValue must contain exactly one key-value child".into(),
));
};
if children.next().is_some() {
return Err(ParseError::InvalidStructure(
"KeyValue must contain exactly one key-value child".into(),
));
}
match (
first_child.tag_name().namespace(),
first_child.tag_name().name(),
) {
(Some(XMLDSIG_NS), "RSAKeyValue") => Ok(KeyValueInfo::RsaKeyValue),
(Some(XMLDSIG11_NS), "ECKeyValue") => Ok(KeyValueInfo::EcKeyValue),
(namespace, child_name) => Ok(KeyValueInfo::Unsupported {
namespace: namespace.map(str::to_string),
local_name: child_name.to_string(),
}),
}
}
fn parse_x509_data_dispatch(node: Node) -> Result<X509DataInfo, ParseError> {
verify_ds_element(node, "X509Data")?;
ensure_no_non_whitespace_text(node, "X509Data")?;
let mut info = X509DataInfo::default();
let mut total_binary_len = 0usize;
for child in element_children(node) {
match (child.tag_name().namespace(), child.tag_name().name()) {
(Some(XMLDSIG_NS), "X509Certificate") => {
ensure_no_element_children(child, "X509Certificate")?;
ensure_x509_data_entry_budget(&info)?;
let cert = decode_x509_base64(child, "X509Certificate")?;
add_x509_data_usage(&mut total_binary_len, cert.len())?;
let parsed_cert = parse_x509_certificate(cert.as_slice())?;
info.parsed_certificates.push(parsed_cert);
info.certificates.push(cert);
}
(Some(XMLDSIG_NS), "X509SubjectName") => {
ensure_no_element_children(child, "X509SubjectName")?;
ensure_x509_data_entry_budget(&info)?;
let subject_name = collect_text_content_bounded(
child,
MAX_X509_SUBJECT_NAME_TEXT_LEN,
"X509SubjectName",
)?;
info.subject_names.push(subject_name);
}
(Some(XMLDSIG_NS), "X509IssuerSerial") => {
ensure_x509_data_entry_budget(&info)?;
let issuer_serial = parse_x509_issuer_serial(child)?;
info.issuer_serials.push(issuer_serial);
}
(Some(XMLDSIG_NS), "X509SKI") => {
ensure_no_element_children(child, "X509SKI")?;
ensure_x509_data_entry_budget(&info)?;
let ski = decode_x509_base64(child, "X509SKI")?;
add_x509_data_usage(&mut total_binary_len, ski.len())?;
info.skis.push(ski);
}
(Some(XMLDSIG_NS), "X509CRL") => {
ensure_no_element_children(child, "X509CRL")?;
ensure_x509_data_entry_budget(&info)?;
let crl = decode_x509_base64(child, "X509CRL")?;
add_x509_data_usage(&mut total_binary_len, crl.len())?;
info.crls.push(crl);
}
(Some(XMLDSIG11_NS), "X509Digest") => {
ensure_no_element_children(child, "X509Digest")?;
ensure_x509_data_entry_budget(&info)?;
let algorithm = required_algorithm_attr(child, "X509Digest")?;
let digest = decode_x509_base64(child, "X509Digest")?;
add_x509_data_usage(&mut total_binary_len, digest.len())?;
info.digests.push((algorithm.to_string(), digest));
}
(Some(XMLDSIG_NS), child_name) | (Some(XMLDSIG11_NS), child_name) => {
return Err(ParseError::InvalidStructure(format!(
"X509Data contains unsupported XMLDSig child element <{child_name}>"
)));
}
_ => {}
}
}
info.certificate_chain = build_x509_certificate_chain(&info)?;
Ok(info)
}
fn build_x509_certificate_chain(info: &X509DataInfo) -> Result<Vec<usize>, ParseError> {
if info.parsed_certificates.is_empty() {
return Ok(Vec::new());
}
let signing_idx = select_x509_signing_certificate(info)?;
let mut chain = vec![signing_idx];
loop {
if chain.len() > MAX_X509_CHAIN_DEPTH {
return Err(ParseError::InvalidStructure(
"X509Data certificate chain exceeds maximum depth".into(),
));
}
let current_idx = *chain
.last()
.expect("chain starts with signing certificate index");
let current = &info.parsed_certificates[current_idx];
if current.subject_dn == current.issuer_dn {
break;
}
let candidates = info
.parsed_certificates
.iter()
.enumerate()
.filter(|(idx, cert)| *idx != current_idx && cert.subject_dn == current.issuer_dn)
.map(|(idx, _)| idx)
.collect::<Vec<_>>();
match candidates.as_slice() {
[] => break,
[issuer_idx] => {
if chain.contains(issuer_idx) {
return Err(ParseError::InvalidStructure(
"X509Data certificate chain contains a cycle".into(),
));
}
if chain.len() == MAX_X509_CHAIN_DEPTH {
return Err(ParseError::InvalidStructure(
"X509Data certificate chain exceeds maximum depth".into(),
));
}
chain.push(*issuer_idx);
}
_ => {
return Err(ParseError::InvalidStructure(
"X509Data certificate chain contains ambiguous issuer certificates".into(),
));
}
}
}
Ok(chain)
}
fn select_x509_signing_certificate(info: &X509DataInfo) -> Result<usize, ParseError> {
let mut candidates = Vec::new();
let has_lookup_identifiers =
!info.subject_names.is_empty() || !info.issuer_serials.is_empty() || !info.skis.is_empty();
for subject_name in &info.subject_names {
let subject_name = subject_name.trim();
let matches = info
.parsed_certificates
.iter()
.enumerate()
.filter(|(_, cert)| subject_name == cert.subject_dn)
.map(|(idx, _)| idx)
.collect::<Vec<_>>();
if matches.is_empty() {
return Err(ParseError::InvalidStructure(
"X509Data lookup identifiers do not match any embedded certificate".into(),
));
}
candidates.extend(matches);
}
for (issuer_name, serial) in &info.issuer_serials {
let serial_hex = x509_serial_decimal_to_hex(serial).ok_or_else(|| {
ParseError::InvalidStructure(
"X509Data lookup identifiers do not match any embedded certificate".into(),
)
})?;
let issuer_name = issuer_name.trim();
let matches = info
.parsed_certificates
.iter()
.enumerate()
.filter(|(_, cert)| {
issuer_name == cert.issuer_dn && serial_hex == cert.serial_number_hex
})
.map(|(idx, _)| idx)
.collect::<Vec<_>>();
if matches.is_empty() {
return Err(ParseError::InvalidStructure(
"X509Data lookup identifiers do not match any embedded certificate".into(),
));
}
candidates.extend(matches);
}
for ski in &info.skis {
let matches = info
.parsed_certificates
.iter()
.enumerate()
.filter(|(_, cert)| {
cert.subject_key_identifier
.as_ref()
.is_some_and(|subject_key_identifier| subject_key_identifier == ski)
})
.map(|(idx, _)| idx)
.collect::<Vec<_>>();
if matches.is_empty() {
return Err(ParseError::InvalidStructure(
"X509Data lookup identifiers do not match any embedded certificate".into(),
));
}
candidates.extend(matches);
}
if has_lookup_identifiers {
candidates.sort_unstable();
candidates.dedup();
}
match candidates.as_slice() {
[idx] => return Ok(*idx),
[] if has_lookup_identifiers => {
return Err(ParseError::InvalidStructure(
"X509Data lookup identifiers do not match any embedded certificate".into(),
));
}
[] => {}
_ => {
return Err(ParseError::InvalidStructure(
"X509Data lookup identifiers match multiple certificates".into(),
));
}
}
let leaf_candidates = info
.parsed_certificates
.iter()
.enumerate()
.filter(|(_, cert)| {
cert.subject_dn != cert.issuer_dn
&& !info
.parsed_certificates
.iter()
.any(|other| other.issuer_dn == cert.subject_dn)
})
.map(|(idx, _)| idx)
.collect::<Vec<_>>();
match leaf_candidates.as_slice() {
[idx] => Ok(*idx),
[] => Ok(0),
_ => Err(ParseError::InvalidStructure(
"X509Data contains multiple possible signing certificates".into(),
)),
}
}
fn ensure_x509_data_entry_budget(info: &X509DataInfo) -> Result<(), ParseError> {
let total_entries = info.certificates.len()
+ info.subject_names.len()
+ info.issuer_serials.len()
+ info.skis.len()
+ info.crls.len()
+ info.digests.len();
if total_entries >= MAX_X509_DATA_ENTRY_COUNT {
return Err(ParseError::InvalidStructure(
"X509Data contains too many entries".into(),
));
}
Ok(())
}
fn add_x509_data_usage(total_binary_len: &mut usize, delta: usize) -> Result<(), ParseError> {
*total_binary_len = total_binary_len.checked_add(delta).ok_or_else(|| {
ParseError::InvalidStructure("X509Data exceeds maximum allowed total binary length".into())
})?;
if *total_binary_len > MAX_X509_DATA_TOTAL_BINARY_LEN {
return Err(ParseError::InvalidStructure(
"X509Data exceeds maximum allowed total binary length".into(),
));
}
Ok(())
}
fn decode_x509_base64(
node: Node<'_, '_>,
element_name: &'static str,
) -> Result<Vec<u8>, ParseError> {
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
let mut cleaned = String::new();
let mut raw_text_len = 0usize;
for text in node
.children()
.filter(|child| child.is_text())
.filter_map(|child| child.text())
{
if raw_text_len.saturating_add(text.len()) > MAX_X509_BASE64_TEXT_LEN {
return Err(ParseError::InvalidStructure(format!(
"{element_name} exceeds maximum allowed text length"
)));
}
raw_text_len = raw_text_len.saturating_add(text.len());
normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
ParseError::Base64(format!(
"invalid XML whitespace U+{:04X} in {element_name}",
err.invalid_byte
))
})?;
if cleaned.len() > MAX_X509_BASE64_NORMALIZED_LEN {
return Err(ParseError::InvalidStructure(format!(
"{element_name} exceeds maximum allowed base64 length"
)));
}
}
let decoded = STANDARD
.decode(&cleaned)
.map_err(|e| ParseError::Base64(format!("{element_name}: {e}")))?;
if decoded.is_empty() {
return Err(ParseError::InvalidStructure(format!(
"{element_name} must not be empty"
)));
}
if decoded.len() > MAX_X509_DECODED_BINARY_LEN {
return Err(ParseError::InvalidStructure(format!(
"{element_name} exceeds maximum allowed binary length"
)));
}
Ok(decoded)
}
fn parse_x509_certificate(cert_der: &[u8]) -> Result<ParsedX509Certificate, ParseError> {
let (rest, cert) =
x509_parser::certificate::X509Certificate::from_der(cert_der).map_err(|err| {
ParseError::InvalidStructure(format!("X509Certificate is not valid DER X.509: {err}"))
})?;
if !rest.is_empty() {
return Err(ParseError::InvalidStructure(
"X509Certificate contains trailing bytes after DER certificate".into(),
));
}
let subject_dn = cert.subject().to_string();
let issuer_dn = cert.issuer().to_string();
let serial_number = cert.tbs_certificate.raw_serial().to_vec();
let serial_number_hex = format_x509_serial_value_hex(&serial_number);
let subject_key_identifier = cert.extensions().iter().find_map(|ext| {
if let ParsedExtension::SubjectKeyIdentifier(ski) = ext.parsed_extension() {
Some(ski.0.to_vec())
} else {
None
}
});
let spki = cert.public_key();
let public_key = match spki.parsed().map_err(|err| {
ParseError::InvalidStructure(format!("X509Certificate public key parse error: {err}"))
})? {
PublicKey::RSA(rsa) => {
let modulus = trim_leading_zeroes(rsa.modulus);
let exponent = trim_leading_zeroes(rsa.exponent);
if modulus.is_empty() || exponent.is_empty() {
return Err(ParseError::InvalidStructure(
"X509Certificate RSA key contains empty modulus or exponent".into(),
));
}
X509PublicKeyInfo::Rsa { modulus, exponent }
}
PublicKey::EC(ec_point) => {
let Some(params) = spki.algorithm.parameters.as_ref() else {
return Err(ParseError::InvalidStructure(
"X509Certificate EC key is missing curve parameters".into(),
));
};
match params.as_oid() {
Ok(oid) => X509PublicKeyInfo::Ec {
curve_oid: oid.to_id_string(),
public_key: ec_point.data().to_vec(),
},
Err(_) => X509PublicKeyInfo::Unsupported {
algorithm_oid: spki.algorithm.algorithm.to_id_string(),
},
}
}
_ => X509PublicKeyInfo::Unsupported {
algorithm_oid: spki.algorithm.algorithm.to_id_string(),
},
};
Ok(ParsedX509Certificate {
subject_dn,
issuer_dn,
serial_number,
serial_number_hex,
subject_key_identifier,
public_key,
})
}
fn format_x509_serial_hex(serial: &[u8]) -> String {
serial
.iter()
.map(|byte| format!("{byte:02X}"))
.collect::<String>()
}
fn format_x509_serial_value_hex(serial: &[u8]) -> String {
let first_non_zero = serial
.iter()
.position(|byte| *byte != 0)
.unwrap_or(serial.len());
let canonical = if first_non_zero == serial.len() {
&[0]
} else {
&serial[first_non_zero..]
};
format_x509_serial_hex(canonical)
}
fn x509_serial_decimal_to_hex(serial: &str) -> Option<String> {
let serial = serial.trim();
let serial = serial.strip_prefix('+').unwrap_or(serial);
if serial.is_empty() || !serial.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let mut bytes = Vec::<u8>::new();
for digit in serial.bytes().map(|byte| byte - b'0') {
let mut carry = u16::from(digit);
for byte in bytes.iter_mut().rev() {
let value = u16::from(*byte) * 10 + carry;
*byte = value as u8;
carry = value >> 8;
}
while carry > 0 {
bytes.insert(0, carry as u8);
carry >>= 8;
}
}
Some(format_x509_serial_value_hex(&bytes))
}
fn trim_leading_zeroes(bytes: &[u8]) -> Vec<u8> {
let first_non_zero = bytes
.iter()
.position(|byte| *byte != 0)
.unwrap_or(bytes.len());
bytes[first_non_zero..].to_vec()
}
fn parse_x509_issuer_serial(node: Node<'_, '_>) -> Result<(String, String), ParseError> {
verify_ds_element(node, "X509IssuerSerial")?;
ensure_no_non_whitespace_text(node, "X509IssuerSerial")?;
let children = element_children(node).collect::<Vec<_>>();
if children.len() != 2 {
return Err(ParseError::InvalidStructure(
"X509IssuerSerial must contain exactly X509IssuerName then X509SerialNumber".into(),
));
}
if !matches!(
(
children[0].tag_name().namespace(),
children[0].tag_name().name()
),
(Some(XMLDSIG_NS), "X509IssuerName")
) {
return Err(ParseError::InvalidStructure(
"X509IssuerSerial must contain X509IssuerName as the first child element".into(),
));
}
if !matches!(
(
children[1].tag_name().namespace(),
children[1].tag_name().name()
),
(Some(XMLDSIG_NS), "X509SerialNumber")
) {
return Err(ParseError::InvalidStructure(
"X509IssuerSerial must contain X509SerialNumber as the second child element".into(),
));
}
let issuer_node = children[0];
ensure_no_element_children(issuer_node, "X509IssuerName")?;
let issuer_name =
collect_text_content_bounded(issuer_node, MAX_X509_ISSUER_NAME_TEXT_LEN, "X509IssuerName")?;
let serial_node = children[1];
ensure_no_element_children(serial_node, "X509SerialNumber")?;
let serial_number = collect_text_content_bounded(
serial_node,
MAX_X509_SERIAL_NUMBER_TEXT_LEN,
"X509SerialNumber",
)?;
if issuer_name.trim().is_empty() || serial_number.trim().is_empty() {
return Err(ParseError::InvalidStructure(
"X509IssuerSerial requires non-empty X509IssuerName and X509SerialNumber".into(),
));
}
Ok((issuer_name, serial_number))
}
fn base64_decode_digest(b64: &str, digest_method: DigestAlgorithm) -> Result<Vec<u8>, ParseError> {
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
let expected = digest_method.output_len();
let max_base64_len = expected.div_ceil(3) * 4;
let mut cleaned = String::with_capacity(b64.len().min(max_base64_len));
normalize_xml_base64_text(b64, &mut cleaned).map_err(|err| {
ParseError::Base64(format!(
"invalid XML whitespace U+{:04X} in DigestValue",
err.invalid_byte
))
})?;
if cleaned.len() > max_base64_len {
return Err(ParseError::Base64(
"DigestValue exceeds maximum allowed base64 length".into(),
));
}
let digest = STANDARD
.decode(&cleaned)
.map_err(|e| ParseError::Base64(e.to_string()))?;
let actual = digest.len();
if actual != expected {
return Err(ParseError::DigestLengthMismatch {
algorithm: digest_method.uri(),
expected,
actual,
});
}
Ok(digest)
}
fn decode_digest_value_children(
digest_value_node: Node<'_, '_>,
digest_method: DigestAlgorithm,
) -> Result<Vec<u8>, ParseError> {
let max_base64_len = digest_method.output_len().div_ceil(3) * 4;
let mut cleaned = String::with_capacity(max_base64_len);
for child in digest_value_node.children() {
if child.is_element() {
return Err(ParseError::InvalidStructure(
"DigestValue must not contain element children".into(),
));
}
if let Some(text) = child.text() {
normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
ParseError::Base64(format!(
"invalid XML whitespace U+{:04X} in DigestValue",
err.invalid_byte
))
})?;
if cleaned.len() > max_base64_len {
return Err(ParseError::Base64(
"DigestValue exceeds maximum allowed base64 length".into(),
));
}
}
}
base64_decode_digest(&cleaned, digest_method)
}
fn decode_der_encoded_key_value_base64(node: Node<'_, '_>) -> Result<Vec<u8>, ParseError> {
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
let mut cleaned = String::new();
let mut raw_text_len = 0usize;
for text in node
.children()
.filter(|child| child.is_text())
.filter_map(|child| child.text())
{
if raw_text_len.saturating_add(text.len()) > MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN {
return Err(ParseError::InvalidStructure(
"DEREncodedKeyValue exceeds maximum allowed text length".into(),
));
}
raw_text_len = raw_text_len.saturating_add(text.len());
normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
ParseError::Base64(format!(
"invalid XML whitespace U+{:04X} in base64 text",
err.invalid_byte
))
})?;
if cleaned.len() > MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN {
return Err(ParseError::InvalidStructure(
"DEREncodedKeyValue exceeds maximum allowed length".into(),
));
}
}
let der = STANDARD
.decode(&cleaned)
.map_err(|e| ParseError::Base64(e.to_string()))?;
if der.is_empty() {
return Err(ParseError::InvalidStructure(
"DEREncodedKeyValue must not be empty".into(),
));
}
if der.len() > MAX_DER_ENCODED_KEY_VALUE_LEN {
return Err(ParseError::InvalidStructure(
"DEREncodedKeyValue exceeds maximum allowed length".into(),
));
}
Ok(der)
}
fn collect_text_content_bounded(
node: Node<'_, '_>,
max_len: usize,
element_name: &'static str,
) -> Result<String, ParseError> {
let mut text = String::new();
for chunk in node
.children()
.filter_map(|child| child.is_text().then(|| child.text()).flatten())
{
if text.len().saturating_add(chunk.len()) > max_len {
return Err(ParseError::InvalidStructure(format!(
"{element_name} exceeds maximum allowed text length"
)));
}
text.push_str(chunk);
}
Ok(text)
}
fn ensure_no_element_children(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
if node.children().any(|child| child.is_element()) {
return Err(ParseError::InvalidStructure(format!(
"{element_name} must not contain child elements"
)));
}
Ok(())
}
fn ensure_no_non_whitespace_text(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
for child in node.children().filter(|child| child.is_text()) {
if let Some(text) = child.text()
&& !is_xml_whitespace_only(text)
{
return Err(ParseError::InvalidStructure(format!(
"{element_name} must not contain non-whitespace mixed content"
)));
}
}
Ok(())
}
#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
mod tests {
use super::*;
use base64::Engine;
fn fixture_rsa_cert_base64() -> String {
fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem")
}
fn fixture_cert_base64(path: &str) -> String {
match path {
"../../tests/fixtures/keys/rsa/rsa-2048-cert.pem" => {
include_str!("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem")
}
"../../tests/fixtures/keys/rsa/rsa-4096-cert.pem" => {
include_str!("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem")
}
"../../tests/fixtures/keys/ca2cert.pem" => {
include_str!("../../tests/fixtures/keys/ca2cert.pem")
}
"../../tests/fixtures/keys/cacert.pem" => {
include_str!("../../tests/fixtures/keys/cacert.pem")
}
_ => unreachable!("unknown certificate fixture"),
}
.lines()
.skip_while(|line| *line != "-----BEGIN CERTIFICATE-----")
.skip(1)
.take_while(|line| *line != "-----END CERTIFICATE-----")
.collect::<String>()
}
#[test]
fn signature_algorithm_from_uri_rsa_sha256() {
assert_eq!(
SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"),
Some(SignatureAlgorithm::RsaSha256)
);
}
#[test]
fn signature_algorithm_from_uri_rsa_sha1() {
assert_eq!(
SignatureAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
Some(SignatureAlgorithm::RsaSha1)
);
}
#[test]
fn signature_algorithm_from_uri_ecdsa_sha256() {
assert_eq!(
SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"),
Some(SignatureAlgorithm::EcdsaP256Sha256)
);
}
#[test]
fn signature_algorithm_from_uri_unknown() {
assert_eq!(
SignatureAlgorithm::from_uri("http://example.com/unknown"),
None
);
}
#[test]
fn signature_algorithm_uri_round_trip() {
for algo in [
SignatureAlgorithm::RsaSha1,
SignatureAlgorithm::RsaSha256,
SignatureAlgorithm::RsaSha384,
SignatureAlgorithm::RsaSha512,
SignatureAlgorithm::EcdsaP256Sha256,
SignatureAlgorithm::EcdsaP384Sha384,
] {
assert_eq!(
SignatureAlgorithm::from_uri(algo.uri()),
Some(algo),
"round-trip failed for {algo:?}"
);
}
}
#[test]
fn rsa_sha1_verify_only() {
assert!(!SignatureAlgorithm::RsaSha1.signing_allowed());
assert!(SignatureAlgorithm::RsaSha256.signing_allowed());
assert!(SignatureAlgorithm::EcdsaP256Sha256.signing_allowed());
}
#[test]
fn find_signature_in_saml() {
let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo/>
</ds:Signature>
</samlp:Response>"#;
let doc = Document::parse(xml).unwrap();
let sig = find_signature_node(&doc);
assert!(sig.is_some());
assert_eq!(sig.unwrap().tag_name().name(), "Signature");
}
#[test]
fn find_signature_missing() {
let xml = "<root><child/></root>";
let doc = Document::parse(xml).unwrap();
assert!(find_signature_node(&doc).is_none());
}
#[test]
fn find_signature_ignores_wrong_namespace() {
let xml = r#"<root><Signature xmlns="http://example.com/fake"/></root>"#;
let doc = Document::parse(xml).unwrap();
assert!(find_signature_node(&doc).is_none());
}
#[test]
fn parse_key_info_dispatches_supported_children() {
let cert_base64 = fixture_rsa_cert_base64();
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<KeyName>idp-signing-key</KeyName>
<KeyValue>
<RSAKeyValue>
<Modulus>AQAB</Modulus>
<Exponent>AQAB</Exponent>
</RSAKeyValue>
</KeyValue>
<X509Data>
<X509Certificate>{cert_base64}</X509Certificate>
<X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
<X509IssuerSerial>
<X509IssuerName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com</X509IssuerName>
<X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
</X509IssuerSerial>
<X509SKI>bcOXN/nsVl8GatRbcKrPbzIbw0Y=</X509SKI>
<X509CRL>BAUGBw==</X509CRL>
<dsig11:X509Digest Algorithm="http://www.w3.org/2001/04/xmlenc#sha256">CAkK</dsig11:X509Digest>
</X509Data>
<dsig11:DEREncodedKeyValue>AQIDBA==</dsig11:DEREncodedKeyValue>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(key_info.sources.len(), 4);
assert_eq!(
key_info.sources[0],
KeyInfoSource::KeyName("idp-signing-key".to_string())
);
assert_eq!(
key_info.sources[1],
KeyInfoSource::KeyValue(KeyValueInfo::RsaKeyValue)
);
let x509_info = match &key_info.sources[2] {
KeyInfoSource::X509Data(x509) => x509,
other => panic!("expected X509Data source, got {other:?}"),
};
let expected_cert = base64::engine::general_purpose::STANDARD
.decode(&cert_base64)
.expect("fixture PEM must contain valid base64");
assert_eq!(x509_info.certificates, vec![expected_cert]);
assert_eq!(
x509_info.subject_names,
vec![
"C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048"
.to_string()
]
);
assert_eq!(
x509_info.issuer_serials,
vec![(
"C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com".to_string(),
"680572598617295163017172295025714171905498632019".to_string()
)]
);
assert_eq!(
x509_info.skis,
vec![vec![
109, 195, 151, 55, 249, 236, 86, 95, 6, 106, 212, 91, 112, 170, 207, 111, 50, 27,
195, 70
]]
);
assert_eq!(x509_info.crls, vec![vec![4, 5, 6, 7]]);
assert_eq!(
x509_info.digests,
vec![(
"http://www.w3.org/2001/04/xmlenc#sha256".to_string(),
vec![8, 9, 10]
)]
);
assert_eq!(x509_info.parsed_certificates.len(), 1);
assert_eq!(x509_info.certificate_chain, vec![0]);
let parsed_cert = &x509_info.parsed_certificates[0];
assert!(!parsed_cert.subject_dn.is_empty());
assert!(!parsed_cert.issuer_dn.is_empty());
assert_eq!(
parsed_cert.serial_number_hex,
"7735EE487F6862DAF1B3956D961CCB0FA6F34F53"
);
assert!(parsed_cert.subject_key_identifier.is_some());
assert!(matches!(
parsed_cert.public_key,
X509PublicKeyInfo::Rsa { .. }
));
assert_eq!(
key_info.sources[3],
KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])
);
}
#[test]
fn parse_key_info_ignores_unknown_children() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<Foo>bar</Foo>
<KeyName>ok</KeyName>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(key_info.sources, vec![KeyInfoSource::KeyName("ok".into())]);
}
#[test]
fn parse_key_info_keyvalue_requires_single_child() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue/>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_accepts_empty_x509data() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data/>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::X509Data(X509DataInfo::default())]
);
}
#[test]
fn parse_key_info_rejects_unknown_xmlsig_child_in_x509data() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<Foo/>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_unknown_xmlsig11_child_in_x509data() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<X509Data>
<dsig11:Foo/>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_issuer_serial_without_required_children() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>CN=CA</X509IssuerName>
</X509IssuerSerial>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_issuer_serial_with_duplicate_issuer_name() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>CN=CA-1</X509IssuerName>
<X509IssuerName>CN=CA-2</X509IssuerName>
<X509SerialNumber>42</X509SerialNumber>
</X509IssuerSerial>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_issuer_serial_with_duplicate_serial_number() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>CN=CA</X509IssuerName>
<X509SerialNumber>1</X509SerialNumber>
<X509SerialNumber>2</X509SerialNumber>
</X509IssuerSerial>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_issuer_serial_with_whitespace_only_values() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509IssuerName> </X509IssuerName>
<X509SerialNumber>
</X509SerialNumber>
</X509IssuerSerial>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_issuer_serial_with_wrong_child_order() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509SerialNumber>42</X509SerialNumber>
<X509IssuerName>CN=CA</X509IssuerName>
</X509IssuerSerial>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_issuer_serial_with_extra_child_element() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:foo="urn:example:foo">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>CN=CA</X509IssuerName>
<X509SerialNumber>42</X509SerialNumber>
<foo:Extra/>
</X509IssuerSerial>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_digest_without_algorithm() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<X509Data>
<dsig11:X509Digest>AQID</dsig11:X509Digest>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_invalid_x509_certificate_base64() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>%%%invalid%%%</X509Certificate>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::Base64(_)));
}
#[test]
fn parse_key_info_rejects_x509_data_exceeding_entry_budget() {
let subjects = (0..(MAX_X509_DATA_ENTRY_COUNT + 1))
.map(|idx| format!("<X509SubjectName>CN={idx}</X509SubjectName>"))
.collect::<Vec<_>>()
.join("");
let xml = format!(
"<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{subjects}</X509Data></KeyInfo>"
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_data_exceeding_total_binary_budget() {
let payload = base64::engine::general_purpose::STANDARD.encode(vec![0u8; 190_000]);
let entries = (0..6)
.map(|_| format!("<X509SKI>{payload}</X509SKI>"))
.collect::<Vec<_>>()
.join("");
let xml = format!(
"<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{entries}</X509Data></KeyInfo>"
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_certificate_with_invalid_der() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>AQID</X509Certificate>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_certificate_with_trailing_der_bytes() {
let mut cert = base64::engine::general_purpose::STANDARD
.decode(fixture_rsa_cert_base64())
.unwrap();
cert.extend_from_slice(&[0x00, 0x01]);
let cert_base64 = base64::engine::general_purpose::STANDARD.encode(cert);
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>{cert_base64}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_marks_unsupported_spki_algorithm_as_unsupported() {
let xml = include_str!(
"../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt.xml"
);
let doc = Document::parse(xml).unwrap();
let key_info_node = doc
.descendants()
.find(|node| {
node.is_element()
&& node.tag_name().namespace() == Some(XMLDSIG_NS)
&& node.tag_name().name() == "KeyInfo"
})
.expect("fixture must contain ds:KeyInfo");
let key_info = parse_key_info(key_info_node).expect("KeyInfo parse should succeed");
let x509_info = match &key_info.sources[0] {
KeyInfoSource::X509Data(x509) => x509,
other => panic!("expected X509Data source, got {other:?}"),
};
assert_eq!(x509_info.certificates.len(), 1);
assert_eq!(x509_info.parsed_certificates.len(), 1);
assert_eq!(x509_info.certificate_chain, vec![0]);
let parsed_cert = &x509_info.parsed_certificates[0];
assert!(!parsed_cert.subject_dn.is_empty());
assert!(!parsed_cert.issuer_dn.is_empty());
assert!(parsed_cert.subject_key_identifier.is_some());
assert!(matches!(
parsed_cert.public_key,
X509PublicKeyInfo::Unsupported { .. }
));
}
#[test]
fn parse_key_info_orders_x509_certificate_chain_from_signing_cert() {
let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>{root}</X509Certificate>
<X509Certificate>{intermediate}</X509Certificate>
<X509Certificate>{leaf}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
let x509_info = match &key_info.sources[0] {
KeyInfoSource::X509Data(x509) => x509,
other => panic!("expected X509Data source, got {other:?}"),
};
assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
}
#[test]
fn parse_key_info_uses_issuer_serial_to_select_x509_signing_certificate() {
let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com</X509IssuerName>
<X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
</X509IssuerSerial>
<X509Certificate>{root}</X509Certificate>
<X509Certificate>{intermediate}</X509Certificate>
<X509Certificate>{leaf}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
let x509_info = match &key_info.sources[0] {
KeyInfoSource::X509Data(x509) => x509,
other => panic!("expected X509Data source, got {other:?}"),
};
assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
}
#[test]
fn parse_key_info_uses_decimal_issuer_serial_to_select_x509_signing_certificate() {
assert_eq!(
x509_serial_decimal_to_hex("680572598617295163017172295025714171905498632019")
.as_deref(),
Some("7735EE487F6862DAF1B3956D961CCB0FA6F34F53")
);
let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let other_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com</X509IssuerName>
<X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
</X509IssuerSerial>
<X509Certificate>{root}</X509Certificate>
<X509Certificate>{intermediate}</X509Certificate>
<X509Certificate>{leaf}</X509Certificate>
<X509Certificate>{other_leaf}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
let x509_info = match &key_info.sources[0] {
KeyInfoSource::X509Data(x509) => x509,
other => panic!("expected X509Data source, got {other:?}"),
};
assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
}
#[test]
fn parse_key_info_rejects_ambiguous_x509_signing_certificate_candidates() {
let first_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let second_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>{first_leaf}</X509Certificate>
<X509Certificate>{second_leaf}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_unmatched_x509_lookup_identifier() {
let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509SubjectName>CN=Not The Embedded Certificate</X509SubjectName>
<X509Certificate>{cert}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(
matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
);
}
#[test]
fn parse_key_info_rejects_malformed_issuer_serial_even_with_matching_subject() {
let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
<X509IssuerSerial>
<X509IssuerName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com</X509IssuerName>
<X509SerialNumber>not-a-decimal-serial</X509SerialNumber>
</X509IssuerSerial>
<X509Certificate>{cert}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(
matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
);
}
#[test]
fn parse_key_info_rejects_unmatched_ski_even_with_matching_subject() {
let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
<X509SKI>AQIDBA==</X509SKI>
<X509Certificate>{cert}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(
matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
);
}
#[test]
fn parse_key_info_rejects_lookup_hints_for_different_certificates() {
let first_cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let second_cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
<X509SKI>60zMLKCfzQ3qnXAzABzRNpdgQ8Q=</X509SKI>
<X509Certificate>{first_cert}</X509Certificate>
<X509Certificate>{second_cert}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(
matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers match multiple certificates"))
);
}
#[test]
fn build_x509_certificate_chain_rejects_chain_exceeding_max_depth() {
let parsed_certificates = (0..=MAX_X509_CHAIN_DEPTH)
.map(|idx| ParsedX509Certificate {
subject_dn: format!("CN=cert-{idx}"),
issuer_dn: if idx == MAX_X509_CHAIN_DEPTH {
format!("CN=cert-{idx}")
} else {
format!("CN=cert-{}", idx + 1)
},
serial_number: vec![u8::try_from(idx).unwrap()],
serial_number_hex: format!("{idx:02X}"),
subject_key_identifier: None,
public_key: X509PublicKeyInfo::Unsupported {
algorithm_oid: "1.2.3.4".into(),
},
})
.collect();
let info = X509DataInfo {
parsed_certificates,
..X509DataInfo::default()
};
let err = build_x509_certificate_chain(&info).unwrap_err();
assert!(
matches!(err, ParseError::InvalidStructure(message) if message.contains("maximum depth"))
);
}
#[test]
fn x509_serial_hex_strips_der_sign_extension_zeroes() {
assert_eq!(format_x509_serial_value_hex(&[0x00, 0xFF]), "FF");
assert_eq!(format_x509_serial_value_hex(&[0x00, 0x7F]), "7F");
assert_eq!(format_x509_serial_value_hex(&[0x00, 0x00]), "00");
}
#[test]
fn parse_key_info_accepts_large_textual_x509_entries_within_entry_budget() {
let issuer_name = "C".repeat(MAX_X509_ISSUER_NAME_TEXT_LEN);
let serial_number = "7".repeat(MAX_X509_SERIAL_NUMBER_TEXT_LEN);
let issuer_serials = (0..52)
.map(|_| {
format!(
"<X509IssuerSerial><X509IssuerName>{issuer_name}</X509IssuerName><X509SerialNumber>{serial_number}</X509SerialNumber></X509IssuerSerial>"
)
})
.collect::<Vec<_>>()
.join("");
let xml = format!(
"<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{issuer_serials}</X509Data></KeyInfo>"
);
let doc = Document::parse(&xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
let parsed = match &key_info.sources[0] {
KeyInfoSource::X509Data(x509) => x509,
_ => panic!("expected X509Data source"),
};
assert_eq!(parsed.issuer_serials.len(), 52);
}
#[test]
fn parse_key_info_accepts_x509data_with_only_foreign_namespace_children() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:foo="urn:example:foo">
<X509Data>
<foo:Bar/>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::X509Data(X509DataInfo::default())]
);
}
#[test]
fn parse_key_info_der_encoded_key_value_rejects_invalid_base64() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::Base64(_)));
}
#[test]
fn parse_key_info_der_encoded_key_value_accepts_xml_whitespace() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<dsig11:DEREncodedKeyValue>
AQID
BA==
</dsig11:DEREncodedKeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])]
);
}
#[test]
fn parse_key_info_dispatches_dsig11_ec_keyvalue() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<KeyValue>
<dsig11:ECKeyValue/>
</KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::KeyValue(KeyValueInfo::EcKeyValue)]
);
}
#[test]
fn parse_key_info_marks_ds_namespace_ec_keyvalue_as_unsupported() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue>
<ECKeyValue/>
</KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
namespace: Some(XMLDSIG_NS.to_string()),
local_name: "ECKeyValue".into(),
})]
);
}
#[test]
fn parse_key_info_keeps_unsupported_keyvalue_child_as_marker() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue>
<DSAKeyValue/>
</KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
namespace: Some(XMLDSIG_NS.to_string()),
local_name: "DSAKeyValue".into(),
})]
);
}
#[test]
fn parse_key_info_rejects_keyname_with_child_elements() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyName>ok<foo/></KeyName>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_preserves_keyname_text_without_trimming() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyName> signing key </KeyName>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::KeyName(" signing key ".into())]
);
}
#[test]
fn parse_key_info_rejects_oversized_keyname_text() {
let oversized = "A".repeat(4097);
let xml = format!(
"<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><KeyName>{oversized}</KeyName></KeyInfo>"
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_non_whitespace_mixed_content() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">oops<KeyName>k</KeyName></KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_nbsp_as_non_xml_whitespace_mixed_content() {
let xml = "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\u{00A0}<KeyName>k</KeyName></KeyInfo>";
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_der_encoded_key_value_rejects_oversized_payload() {
let oversized =
base64::engine::general_purpose::STANDARD
.encode(vec![0u8; MAX_DER_ENCODED_KEY_VALUE_LEN + 1]);
let xml = format!(
"<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:dsig11=\"http://www.w3.org/2009/xmldsig11#\"><dsig11:DEREncodedKeyValue>{oversized}</dsig11:DEREncodedKeyValue></KeyInfo>"
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_der_encoded_key_value_rejects_empty_payload() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<dsig11:DEREncodedKeyValue>
</dsig11:DEREncodedKeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_der_encoded_key_value_non_xml_ascii_whitespace_is_not_parseable_xml() {
let xml = "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:dsig11=\"http://www.w3.org/2009/xmldsig11#\"><dsig11:DEREncodedKeyValue>\u{000C}</dsig11:DEREncodedKeyValue></KeyInfo>";
assert!(Document::parse(xml).is_err());
}
#[test]
fn parse_signed_info_rsa_sha256_with_reference() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
assert_eq!(si.references.len(), 1);
let r = &si.references[0];
assert_eq!(r.uri.as_deref(), Some(""));
assert_eq!(r.digest_method, DigestAlgorithm::Sha256);
assert_eq!(r.digest_value, vec![0u8; 32]);
assert_eq!(r.transforms.len(), 2);
}
#[test]
fn parse_signed_info_multiple_references() {
let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
<Reference URI="#a">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
<Reference URI="#b">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"##;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
assert_eq!(si.signature_method, SignatureAlgorithm::EcdsaP256Sha256);
assert_eq!(si.references.len(), 2);
assert_eq!(si.references[0].uri.as_deref(), Some("#a"));
assert_eq!(si.references[0].digest_method, DigestAlgorithm::Sha256);
assert_eq!(si.references[1].uri.as_deref(), Some("#b"));
assert_eq!(si.references[1].digest_method, DigestAlgorithm::Sha1);
}
#[test]
fn parse_reference_without_transforms() {
let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="#obj">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"##;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
assert!(si.references[0].transforms.is_empty());
}
#[test]
fn parse_reference_with_all_attributes() {
let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="#data" Id="ref1" Type="http://www.w3.org/2000/09/xmldsig#Object">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"##;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
let r = &si.references[0];
assert_eq!(r.uri.as_deref(), Some("#data"));
assert_eq!(r.id.as_deref(), Some("ref1"));
assert_eq!(
r.ref_type.as_deref(),
Some("http://www.w3.org/2000/09/xmldsig#Object")
);
}
#[test]
fn parse_reference_absent_uri() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
assert!(si.references[0].uri.is_none());
}
#[test]
fn parse_signed_info_preserves_inclusive_prefixes() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
<ec:InclusiveNamespaces PrefixList="ds saml #default"/>
</CanonicalizationMethod>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
assert!(si.c14n_method.inclusive_prefixes().contains("ds"));
assert!(si.c14n_method.inclusive_prefixes().contains("saml"));
assert!(si.c14n_method.inclusive_prefixes().contains(""));
}
#[test]
fn missing_canonicalization_method() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidStructure(_)
));
}
#[test]
fn missing_signature_method() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidStructure(_)
));
}
#[test]
fn no_references() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingElement {
element: "Reference"
}
));
}
#[test]
fn unsupported_c14n_algorithm() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://example.com/bogus-c14n"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::UnsupportedAlgorithm { .. }
));
}
#[test]
fn unsupported_signature_algorithm() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://example.com/bogus-sign"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::UnsupportedAlgorithm { .. }
));
}
#[test]
fn unsupported_digest_algorithm() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://example.com/bogus-digest"/>
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::UnsupportedAlgorithm { .. }
));
}
#[test]
fn missing_digest_method() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(result.is_err());
}
#[test]
fn missing_digest_value() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingElement {
element: "DigestValue"
}
));
}
#[test]
fn invalid_base64_digest_value() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>!!!not-base64!!!</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(result.unwrap_err(), ParseError::Base64(_)));
}
#[test]
fn digest_value_length_must_match_digest_method() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::DigestLengthMismatch {
algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
expected: 32,
actual: 4,
}
));
}
#[test]
fn inclusive_prefixes_on_inclusive_c14n_is_rejected() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">
<ec:InclusiveNamespaces PrefixList="ds"/>
</CanonicalizationMethod>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::UnsupportedAlgorithm { .. }
));
}
#[test]
fn extra_element_after_digest_value() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
<Unexpected/>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidStructure(_)
));
}
#[test]
fn digest_value_with_element_child_is_rejected() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=<Junk/>AAAA</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidStructure(_)
));
}
#[test]
fn wrong_namespace_on_signed_info() {
let xml = r#"<SignedInfo xmlns="http://example.com/fake">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidStructure(_)
));
}
#[test]
fn base64_with_whitespace() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>
AAAAAAAA
AAAAAAAAAAAAAAAAAAA=
</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
assert_eq!(si.references[0].digest_value, vec![0u8; 20]);
}
#[test]
fn base64_decode_digest_accepts_xml_whitespace_chars() {
let digest =
base64_decode_digest("AAAA\tAAAA\rAAAA\nAAAA AAAAAAAAAAA=", DigestAlgorithm::Sha1)
.expect("XML whitespace in DigestValue must be accepted");
assert_eq!(digest, vec![0u8; 20]);
}
#[test]
fn base64_decode_digest_rejects_non_xml_ascii_whitespace() {
let err = base64_decode_digest(
"AAAA\u{000C}AAAAAAAAAAAAAAAAAAAAAAA=",
DigestAlgorithm::Sha1,
)
.expect_err("form-feed/vertical-tab in DigestValue must be rejected");
assert!(matches!(err, ParseError::Base64(_)));
}
#[test]
fn base64_decode_digest_rejects_oversized_base64_before_decode() {
let err = base64_decode_digest("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", DigestAlgorithm::Sha1)
.expect_err("oversized DigestValue base64 must fail before decode");
match err {
ParseError::Base64(message) => {
assert!(
message.contains("DigestValue exceeds maximum allowed base64 length"),
"unexpected message: {message}"
);
}
other => panic!("expected ParseError::Base64, got {other:?}"),
}
}
#[test]
fn saml_response_signed_info() {
let xml = r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#_resp1">
<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>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>ZmFrZQ==</ds:SignatureValue>
</ds:Signature>"##;
let doc = Document::parse(xml).unwrap();
let sig_node = doc.root_element();
let signed_info_node = sig_node
.children()
.find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
.unwrap();
let si = parse_signed_info(signed_info_node).unwrap();
assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
assert_eq!(si.references.len(), 1);
assert_eq!(si.references[0].uri.as_deref(), Some("#_resp1"));
assert_eq!(si.references[0].transforms.len(), 2);
assert_eq!(si.references[0].digest_value, vec![0u8; 32]);
}
}