use std::time::{SystemTime, UNIX_EPOCH};
use crate::core::{
Attribute, Document, ErrorKind, NamespaceDeclaration, NodeId, NodeKind, QName, XmlError,
XmlResult,
};
use super::xmldsig::{
add_key_info, add_signed_info_element, build_references, element_children, element_local_name,
ensure_root_id, find_signature, key_info_reference_id, optional_attribute, parse_signed_info,
populate_signed_info, required_attribute, required_child, required_child_text,
resolve_signature_parent, validate_reference_configs,
};
use super::{
canonicalize_node, decode_standard_base64, digest_bytes, encode_standard_base64,
verify_enveloped, CanonicalizationConfig, CertificateDetails, DigestAlgorithm, Reference,
SignaturePolicy, SignaturePolicyId, SignaturePolicyQualifier, SignerRole,
SigningCertificateMode, SigningProvider, Transform, VerificationReport, XadesProfile,
XmlDsigConfig, XmlDsigReferenceTarget, XMLDSIG_NAMESPACE_URI,
XMLDSIG_SIGNED_PROPERTIES_TYPE_URI,
};
pub const XADES_NAMESPACE_URI: &str = "http://uri.etsi.org/01903/v1.3.2#";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XadesConfig {
profile: XadesProfile,
xmldsig: XmlDsigConfig,
signing_time: XadesSigningTime,
signed_properties_id: String,
signing_certificate_mode: SigningCertificateMode,
include_certificate_chain: bool,
signer_role: SignerRole,
}
impl XadesConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_profile(mut self, profile: XadesProfile) -> Self {
self.profile = profile;
self
}
pub fn with_xmldsig_config(mut self, xmldsig: XmlDsigConfig) -> Self {
self.xmldsig = xmldsig;
self
}
pub fn with_signing_time(mut self, signing_time: impl Into<String>) -> Self {
self.signing_time = XadesSigningTime::FixedXml(signing_time.into());
self
}
pub fn with_signing_time_unix_timestamp(mut self, unix_timestamp: i64) -> Self {
self.signing_time = XadesSigningTime::FixedUnixTimestamp(unix_timestamp);
self
}
pub fn with_current_signing_time(mut self) -> Self {
self.signing_time = XadesSigningTime::CurrentSystemTime;
self
}
pub fn with_signed_properties_id(mut self, id: impl Into<String>) -> Self {
self.signed_properties_id = id.into();
self
}
pub fn with_signing_certificate_mode(mut self, mode: SigningCertificateMode) -> Self {
self.signing_certificate_mode = mode;
self
}
pub fn with_certificate_chain(mut self, include_certificate_chain: bool) -> Self {
self.include_certificate_chain = include_certificate_chain;
self
}
pub fn with_signer_role(mut self, signer_role: SignerRole) -> Self {
self.signer_role = signer_role;
self
}
pub fn with_claimed_role(mut self, role: impl Into<String>) -> Self {
self.signer_role = self.signer_role.with_claimed_role(role);
self
}
pub fn profile(&self) -> &XadesProfile {
&self.profile
}
pub fn xmldsig_config(&self) -> &XmlDsigConfig {
&self.xmldsig
}
pub fn signing_time(&self) -> Option<&str> {
match &self.signing_time {
XadesSigningTime::FixedXml(value) => Some(value),
XadesSigningTime::CurrentSystemTime | XadesSigningTime::FixedUnixTimestamp(_) => None,
}
}
pub fn signing_time_source(&self) -> &XadesSigningTime {
&self.signing_time
}
pub fn signed_properties_id(&self) -> &str {
&self.signed_properties_id
}
pub fn signing_certificate_mode(&self) -> SigningCertificateMode {
self.signing_certificate_mode
}
pub fn include_certificate_chain(&self) -> bool {
self.include_certificate_chain
}
pub fn signer_role(&self) -> &SignerRole {
&self.signer_role
}
}
impl Default for XadesConfig {
fn default() -> Self {
Self {
profile: XadesProfile::Bes,
xmldsig: XmlDsigConfig::new(),
signing_time: XadesSigningTime::CurrentSystemTime,
signed_properties_id: "xdoc-signed-props-1".to_owned(),
signing_certificate_mode: SigningCertificateMode::SigningCertificateV2,
include_certificate_chain: false,
signer_role: SignerRole::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum XadesSigningTime {
CurrentSystemTime,
FixedXml(String),
FixedUnixTimestamp(i64),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XadesVerificationReport {
pub valid: bool,
pub profile: XadesProfile,
pub xmldsig: VerificationReport,
pub signed_properties_reference_valid: bool,
pub signing_certificate_valid: bool,
pub signature_policy_valid: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct SigningCertificateReference {
details: CertificateDetails,
digest_value: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ParsedSigningCertificateReference {
digest_algorithm: DigestAlgorithm,
digest_value: Vec<u8>,
issuer_name: Option<String>,
serial_number: Option<String>,
}
pub fn sign_xades_bes_enveloped(
document: &Document,
provider: &impl SigningProvider,
config: &XadesConfig,
) -> XmlResult<Document> {
require_bes_profile(config)?;
sign_xades_enveloped(document, provider, config)
}
pub fn sign_xades_epes_enveloped(
document: &Document,
provider: &impl SigningProvider,
config: &XadesConfig,
) -> XmlResult<Document> {
require_epes_profile(config)?;
sign_xades_enveloped(document, provider, config)
}
pub fn sign_xades_baseline_b_enveloped(
document: &Document,
provider: &impl SigningProvider,
config: &XadesConfig,
) -> XmlResult<Document> {
require_baseline_b_profile(config)?;
sign_xades_enveloped(document, provider, config)
}
fn sign_xades_enveloped(
document: &Document,
provider: &impl SigningProvider,
config: &XadesConfig,
) -> XmlResult<Document> {
config
.xmldsig
.digest_algorithm()
.ensure_allowed_for_generation()?;
config
.xmldsig
.signature_algorithm()
.ensure_allowed_for_generation()?;
validate_reference_configs(&config.xmldsig)?;
if let Some(policy) = expected_policy(config) {
policy.digest_algorithm.ensure_allowed_for_generation()?;
}
let signing_certificates = signing_certificate_references(provider, config)?;
let certificate = signing_certificates[0].details.der().to_vec();
let signing_time = resolve_signing_time(provider, config)?;
let canonicalization = CanonicalizationConfig::new(config.xmldsig.canonicalization());
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
.xmldsig
.references()
.iter()
.any(|reference| reference.target() == XmlDsigReferenceTarget::DocumentId)
{
Some(ensure_root_id(&mut signed, root, &config.xmldsig)?)
} else {
None
};
let signature_parent = resolve_signature_parent(&signed, &config.xmldsig)?;
let key_info_id = key_info_reference_id(&config.xmldsig);
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_namespace_declaration(
signature,
NamespaceDeclaration::prefixed("xades", XADES_NAMESPACE_URI)?,
)?;
signed.add_attribute(
signature,
Attribute::new(QName::new("Id")?, config.xmldsig.signature_id()),
)?;
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 signed_properties = add_xades_object(
&mut signed,
signature,
config,
&signing_certificates,
&signing_time,
)?;
let mut references = build_references(
&signed,
root,
signature,
document_id.as_deref(),
Some(key_info),
&config.xmldsig,
)?;
let signed_properties_digest = digest_bytes(
config.xmldsig.digest_algorithm(),
canonicalize_node(&signed, signed_properties, &canonicalization)?,
)?;
references.push(Reference {
uri: format!("#{}", config.signed_properties_id),
type_uri: Some(XMLDSIG_SIGNED_PROPERTIES_TYPE_URI.to_owned()),
transforms: vec![Transform::Canonicalization(
config.xmldsig.canonicalization(),
)],
digest_algorithm: config.xmldsig.digest_algorithm(),
digest_value: signed_properties_digest,
});
populate_signed_info(&mut signed, signed_info, &config.xmldsig, &references)?;
let signed_info_bytes = canonicalize_node(&signed, signed_info, &canonicalization)?;
let signature_value =
provider.sign(config.xmldsig.signature_algorithm(), &signed_info_bytes)?;
signed.add_text(
signature_value_node,
encode_standard_base64(signature_value),
)?;
Ok(signed)
}
fn resolve_signing_time(
provider: &impl SigningProvider,
config: &XadesConfig,
) -> XmlResult<String> {
match config.signing_time_source() {
XadesSigningTime::CurrentSystemTime => {
let unix_timestamp = current_unix_timestamp()?;
provider.ensure_certificate_valid_at(unix_timestamp)?;
format_unix_timestamp_utc(unix_timestamp)
}
XadesSigningTime::FixedUnixTimestamp(unix_timestamp) => {
provider.ensure_certificate_valid_at(*unix_timestamp)?;
format_unix_timestamp_utc(*unix_timestamp)
}
XadesSigningTime::FixedXml(value) => Ok(value.clone()),
}
}
fn current_unix_timestamp() -> XmlResult<i64> {
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|error| {
XmlError::new(
ErrorKind::Signature,
format!("system clock is before UNIX epoch: {error}"),
)
})?;
i64::try_from(duration.as_secs()).map_err(|error| {
XmlError::new(
ErrorKind::Signature,
format!("current signing time does not fit into i64: {error}"),
)
})
}
fn format_unix_timestamp_utc(unix_timestamp: i64) -> XmlResult<String> {
if unix_timestamp < 0 {
return Err(XmlError::new(
ErrorKind::Signature,
"XAdES SigningTime cannot be before UNIX epoch",
));
}
let days = unix_timestamp / 86_400;
let seconds_of_day = unix_timestamp % 86_400;
let (year, month, day) = civil_from_days(days);
let hour = seconds_of_day / 3_600;
let minute = (seconds_of_day % 3_600) / 60;
let second = seconds_of_day % 60;
Ok(format!(
"{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
))
}
fn civil_from_days(days_since_unix_epoch: i64) -> (i64, i64, i64) {
let days = days_since_unix_epoch + 719_468;
let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
let day_of_era = days - era * 146_097;
let year_of_era =
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let month_parameter = (5 * day_of_year + 2) / 153;
let day = day_of_year - (153 * month_parameter + 2) / 5 + 1;
let month = month_parameter + if month_parameter < 10 { 3 } else { -9 };
let year = year + if month <= 2 { 1 } else { 0 };
(year, month, day)
}
pub fn verify_xades_bes_enveloped(
document: &Document,
provider: &impl SigningProvider,
config: &XadesConfig,
) -> XmlResult<XadesVerificationReport> {
require_bes_profile(config)?;
verify_xades_enveloped(document, provider, config)
}
pub fn verify_xades_epes_enveloped(
document: &Document,
provider: &impl SigningProvider,
config: &XadesConfig,
) -> XmlResult<XadesVerificationReport> {
require_epes_profile(config)?;
verify_xades_enveloped(document, provider, config)
}
pub fn verify_xades_baseline_b_enveloped(
document: &Document,
provider: &impl SigningProvider,
config: &XadesConfig,
) -> XmlResult<XadesVerificationReport> {
require_baseline_b_profile(config)?;
let mut report = verify_xades_enveloped(document, provider, config)?;
let signature = find_signature(document)?;
report.valid = report.valid && verify_baseline_b_constraints(document, signature)?;
Ok(report)
}
fn verify_xades_enveloped(
document: &Document,
provider: &impl SigningProvider,
config: &XadesConfig,
) -> XmlResult<XadesVerificationReport> {
let xmldsig = verify_enveloped(document, provider, &config.xmldsig)?;
let signature = find_signature(document)?;
let signed_info = required_child(document, signature, "SignedInfo")?;
let parsed = parse_signed_info(document, signed_info)?;
let signed_properties = find_signed_properties(document, signature)?;
let signed_properties_id = required_attribute(document, signed_properties, "Id")?;
let signed_properties_uri = format!("#{signed_properties_id}");
let signed_properties_reference_valid = parsed.references.iter().any(|reference| {
reference.uri == signed_properties_uri
&& reference.type_uri.as_deref() == Some(XMLDSIG_SIGNED_PROPERTIES_TYPE_URI)
&& xmldsig
.reference_results
.iter()
.any(|result| result.uri == reference.uri && result.valid)
});
required_signing_time(document, signed_properties)?;
let signing_certificate_valid = verify_signing_certificate(document, signature, provider)?;
let signature_policy_valid = match expected_policy(config) {
Some(policy) => Some(verify_signature_policy(
document,
signed_properties,
policy,
)?),
None => None,
};
let policy_valid = signature_policy_valid.unwrap_or(true);
let valid = xmldsig.valid
&& signed_properties_reference_valid
&& signing_certificate_valid
&& policy_valid;
Ok(XadesVerificationReport {
valid,
profile: config.profile.clone(),
xmldsig,
signed_properties_reference_valid,
signing_certificate_valid,
signature_policy_valid,
})
}
fn require_bes_profile(config: &XadesConfig) -> XmlResult<()> {
match config.profile {
XadesProfile::Bes => Ok(()),
XadesProfile::Epes(_) | XadesProfile::BaselineB { .. } => Err(XmlError::new(
ErrorKind::Signature,
"XAdES-BES operation requires XadesProfile::Bes",
)),
}
}
fn require_epes_profile(config: &XadesConfig) -> XmlResult<()> {
match config.profile {
XadesProfile::Epes(_) => Ok(()),
XadesProfile::Bes | XadesProfile::BaselineB { .. } => Err(XmlError::new(
ErrorKind::Signature,
"XAdES-EPES operation requires XadesProfile::Epes",
)),
}
}
fn require_baseline_b_profile(config: &XadesConfig) -> XmlResult<()> {
match config.profile {
XadesProfile::BaselineB { .. } => Ok(()),
XadesProfile::Bes | XadesProfile::Epes(_) => Err(XmlError::new(
ErrorKind::Signature,
"XAdES Baseline-B operation requires XadesProfile::BaselineB",
)),
}
}
fn expected_policy(config: &XadesConfig) -> Option<&SignaturePolicy> {
match &config.profile {
XadesProfile::Bes => None,
XadesProfile::Epes(policy) => Some(policy),
XadesProfile::BaselineB { policy } => policy.as_ref(),
}
}
fn effective_signing_certificate_mode(config: &XadesConfig) -> SigningCertificateMode {
match config.profile {
XadesProfile::BaselineB { .. } => SigningCertificateMode::SigningCertificateV2,
XadesProfile::Bes | XadesProfile::Epes(_) => config.signing_certificate_mode,
}
}
fn signing_certificate_references(
provider: &impl SigningProvider,
config: &XadesConfig,
) -> XmlResult<Vec<SigningCertificateReference>> {
let signing_certificate = provider.certificate_details()?;
let certificates = if config.include_certificate_chain {
let chain = provider.certificate_chain_details()?;
if chain.is_empty() {
vec![signing_certificate.clone()]
} else {
chain
}
} else {
vec![signing_certificate.clone()]
};
let first = certificates.first().ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
"certificate chain must include the signing certificate",
)
})?;
if first.der() != signing_certificate.der() {
return Err(XmlError::new(
ErrorKind::Signature,
"certificate chain must start with the signing certificate",
));
}
certificates
.into_iter()
.map(|details| {
Ok(SigningCertificateReference {
digest_value: digest_bytes(config.xmldsig.digest_algorithm(), details.der())?,
details,
})
})
.collect()
}
fn add_xades_object(
document: &mut Document,
signature: NodeId,
config: &XadesConfig,
certificates: &[SigningCertificateReference],
signing_time: &str,
) -> XmlResult<NodeId> {
let object = document.add_element(
signature,
QName::qualified("ds", "Object", XMLDSIG_NAMESPACE_URI)?,
)?;
let qualifying_properties = document.add_element(
object,
QName::qualified("xades", "QualifyingProperties", XADES_NAMESPACE_URI)?,
)?;
document.add_namespace_declaration(
qualifying_properties,
NamespaceDeclaration::prefixed("xades", XADES_NAMESPACE_URI)?,
)?;
document.add_attribute(
qualifying_properties,
Attribute::new(
QName::new("Target")?,
format!("#{}", config.xmldsig.signature_id()),
),
)?;
let signed_properties = document.add_element(
qualifying_properties,
QName::qualified("xades", "SignedProperties", XADES_NAMESPACE_URI)?,
)?;
add_signed_properties_root_data(document, signed_properties, config)?;
add_signed_properties_content(
document,
signed_properties,
config,
certificates,
signing_time,
)?;
Ok(signed_properties)
}
fn add_signed_properties_root_data(
document: &mut Document,
signed_properties: NodeId,
config: &XadesConfig,
) -> XmlResult<()> {
document.add_namespace_declaration(
signed_properties,
NamespaceDeclaration::prefixed("ds", XMLDSIG_NAMESPACE_URI)?,
)?;
document.add_namespace_declaration(
signed_properties,
NamespaceDeclaration::prefixed("xades", XADES_NAMESPACE_URI)?,
)?;
document.add_attribute(
signed_properties,
Attribute::new(QName::new("Id")?, config.signed_properties_id()),
)?;
Ok(())
}
fn add_signed_properties_content(
document: &mut Document,
signed_properties: NodeId,
config: &XadesConfig,
certificates: &[SigningCertificateReference],
signing_time: &str,
) -> XmlResult<()> {
let signed_signature_properties = document.add_element(
signed_properties,
QName::qualified("xades", "SignedSignatureProperties", XADES_NAMESPACE_URI)?,
)?;
add_text_element(
document,
signed_signature_properties,
"xades",
"SigningTime",
XADES_NAMESPACE_URI,
signing_time,
)?;
match effective_signing_certificate_mode(config) {
SigningCertificateMode::ClassicSigningCertificate => add_signing_certificate(
document,
signed_signature_properties,
"SigningCertificate",
config.xmldsig.digest_algorithm(),
certificates,
),
SigningCertificateMode::SigningCertificateV2 => add_signing_certificate(
document,
signed_signature_properties,
"SigningCertificateV2",
config.xmldsig.digest_algorithm(),
certificates,
),
}?;
if let Some(policy) = expected_policy(config) {
add_signature_policy_identifier(document, signed_signature_properties, policy)?;
}
if !config.signer_role.is_empty() {
add_signer_role(document, signed_signature_properties, &config.signer_role)?;
}
Ok(())
}
fn add_signature_policy_identifier(
document: &mut Document,
parent: NodeId,
policy: &SignaturePolicy,
) -> XmlResult<()> {
let identifier = document.add_element(
parent,
QName::qualified("xades", "SignaturePolicyIdentifier", XADES_NAMESPACE_URI)?,
)?;
let policy_id = document.add_element(
identifier,
QName::qualified("xades", "SignaturePolicyId", XADES_NAMESPACE_URI)?,
)?;
let sig_policy_id = document.add_element(
policy_id,
QName::qualified("xades", "SigPolicyId", XADES_NAMESPACE_URI)?,
)?;
let identifier = add_text_element(
document,
sig_policy_id,
"xades",
"Identifier",
XADES_NAMESPACE_URI,
policy.id.value(),
)?;
if matches!(policy.id, SignaturePolicyId::Oid(_)) {
document.add_attribute(identifier, Attribute::new(QName::new("Qualifier")?, "OID"))?;
}
if let Some(description) = &policy.description {
add_text_element(
document,
sig_policy_id,
"xades",
"Description",
XADES_NAMESPACE_URI,
description,
)?;
}
let sig_policy_hash = document.add_element(
policy_id,
QName::qualified("xades", "SigPolicyHash", XADES_NAMESPACE_URI)?,
)?;
let digest_method = document.add_element(
sig_policy_hash,
QName::qualified("ds", "DigestMethod", XMLDSIG_NAMESPACE_URI)?,
)?;
document.add_attribute(
digest_method,
Attribute::new(QName::new("Algorithm")?, policy.digest_algorithm.uri()),
)?;
add_text_element(
document,
sig_policy_hash,
"ds",
"DigestValue",
XMLDSIG_NAMESPACE_URI,
encode_standard_base64(&policy.digest_value),
)?;
if !policy.qualifiers.is_empty() {
let qualifiers = document.add_element(
policy_id,
QName::qualified("xades", "SigPolicyQualifiers", XADES_NAMESPACE_URI)?,
)?;
for qualifier in &policy.qualifiers {
add_signature_policy_qualifier(document, qualifiers, qualifier)?;
}
}
Ok(())
}
fn add_signature_policy_qualifier(
document: &mut Document,
parent: NodeId,
qualifier: &SignaturePolicyQualifier,
) -> XmlResult<()> {
let wrapper = document.add_element(
parent,
QName::qualified("xades", "SigPolicyQualifier", XADES_NAMESPACE_URI)?,
)?;
match qualifier {
SignaturePolicyQualifier::SpUri(uri) => {
add_text_element(
document,
wrapper,
"xades",
"SPURI",
XADES_NAMESPACE_URI,
uri,
)?;
}
SignaturePolicyQualifier::SpUserNotice {
organization,
notice_numbers,
explicit_text,
} => {
let notice = document.add_element(
wrapper,
QName::qualified("xades", "SPUserNotice", XADES_NAMESPACE_URI)?,
)?;
if organization.is_some() || !notice_numbers.is_empty() {
let notice_ref = document.add_element(
notice,
QName::qualified("xades", "NoticeRef", XADES_NAMESPACE_URI)?,
)?;
if let Some(organization) = organization {
add_text_element(
document,
notice_ref,
"xades",
"Organization",
XADES_NAMESPACE_URI,
organization,
)?;
}
if !notice_numbers.is_empty() {
let numbers = document.add_element(
notice_ref,
QName::qualified("xades", "NoticeNumbers", XADES_NAMESPACE_URI)?,
)?;
for number in notice_numbers {
add_text_element(
document,
numbers,
"xades",
"int",
XADES_NAMESPACE_URI,
number.to_string(),
)?;
}
}
}
if let Some(explicit_text) = explicit_text {
add_text_element(
document,
notice,
"xades",
"ExplicitText",
XADES_NAMESPACE_URI,
explicit_text,
)?;
}
}
}
Ok(())
}
fn add_signer_role(
document: &mut Document,
parent: NodeId,
signer_role: &SignerRole,
) -> XmlResult<()> {
let role = document.add_element(
parent,
QName::qualified("xades", "SignerRole", XADES_NAMESPACE_URI)?,
)?;
if !signer_role.claimed_roles().is_empty() {
let claimed_roles = document.add_element(
role,
QName::qualified("xades", "ClaimedRoles", XADES_NAMESPACE_URI)?,
)?;
for claimed_role in signer_role.claimed_roles() {
add_text_element(
document,
claimed_roles,
"xades",
"ClaimedRole",
XADES_NAMESPACE_URI,
claimed_role,
)?;
}
}
Ok(())
}
fn add_signing_certificate(
document: &mut Document,
parent: NodeId,
local_name: &str,
digest_algorithm: DigestAlgorithm,
certificates: &[SigningCertificateReference],
) -> XmlResult<()> {
let signing_certificate = document.add_element(
parent,
QName::qualified("xades", local_name, XADES_NAMESPACE_URI)?,
)?;
for certificate in certificates {
add_signing_certificate_reference(
document,
signing_certificate,
digest_algorithm,
certificate,
)?;
}
Ok(())
}
fn add_signing_certificate_reference(
document: &mut Document,
signing_certificate: NodeId,
digest_algorithm: DigestAlgorithm,
certificate: &SigningCertificateReference,
) -> XmlResult<()> {
let cert = document.add_element(
signing_certificate,
QName::qualified("xades", "Cert", XADES_NAMESPACE_URI)?,
)?;
let cert_digest = document.add_element(
cert,
QName::qualified("xades", "CertDigest", XADES_NAMESPACE_URI)?,
)?;
let digest_method = document.add_element(
cert_digest,
QName::qualified("ds", "DigestMethod", XMLDSIG_NAMESPACE_URI)?,
)?;
document.add_attribute(
digest_method,
Attribute::new(QName::new("Algorithm")?, digest_algorithm.uri()),
)?;
add_text_element(
document,
cert_digest,
"ds",
"DigestValue",
XMLDSIG_NAMESPACE_URI,
encode_standard_base64(&certificate.digest_value),
)?;
if let Some((issuer_name, serial_number)) = certificate.details.issuer_serial() {
let issuer_serial = document.add_element(
cert,
QName::qualified("xades", "IssuerSerial", XADES_NAMESPACE_URI)?,
)?;
add_text_element(
document,
issuer_serial,
"ds",
"X509IssuerName",
XMLDSIG_NAMESPACE_URI,
issuer_name,
)?;
add_text_element(
document,
issuer_serial,
"ds",
"X509SerialNumber",
XMLDSIG_NAMESPACE_URI,
serial_number,
)?;
}
Ok(())
}
fn add_text_element(
document: &mut Document,
parent: NodeId,
prefix: &str,
local: &str,
namespace_uri: &str,
value: impl Into<String>,
) -> XmlResult<NodeId> {
let node = document.add_element(parent, QName::qualified(prefix, local, namespace_uri)?)?;
document.add_text(node, value)?;
Ok(node)
}
fn find_signed_properties(document: &Document, signature: NodeId) -> XmlResult<NodeId> {
let object = required_child(document, signature, "Object")?;
let qualifying_properties = element_children(document, object)?
.into_iter()
.find(|child| is_xades_element(document, *child, "QualifyingProperties"))
.ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
"missing required XAdES QualifyingProperties",
)
})?;
element_children(document, qualifying_properties)?
.into_iter()
.find(|child| is_xades_element(document, *child, "SignedProperties"))
.ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
"missing required XAdES SignedProperties",
)
})
}
fn required_signing_time(document: &Document, signed_properties: NodeId) -> XmlResult<String> {
let signed_signature_properties =
required_xades_child(document, signed_properties, "SignedSignatureProperties")?;
let signing_time = required_xades_child(document, signed_signature_properties, "SigningTime")?;
let value = text_content(document, signing_time)?;
if value.is_empty() {
return Err(XmlError::new(
ErrorKind::Signature,
"XAdES SigningTime must contain text",
));
}
Ok(value)
}
fn verify_signing_certificate(
document: &Document,
signature: NodeId,
provider: &impl SigningProvider,
) -> XmlResult<bool> {
let key_info = required_child(document, signature, "KeyInfo")?;
let x509_data = required_child(document, key_info, "X509Data")?;
let certificate_text = required_child_text(document, x509_data, "X509Certificate")?;
let key_info_certificate = decode_standard_base64(&certificate_text)?;
let provider_certificate = provider.certificate_der()?;
if key_info_certificate != provider_certificate {
return Ok(false);
}
let signed_properties = find_signed_properties(document, signature)?;
let signed_signature_properties =
required_xades_child(document, signed_properties, "SignedSignatureProperties")?;
let signing_certificate = optional_xades_child(
document,
signed_signature_properties,
"SigningCertificateV2",
)?
.or(optional_xades_child(
document,
signed_signature_properties,
"SigningCertificate",
)?)
.ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
"missing required XAdES SigningCertificate",
)
})?;
let parsed_certificates = parse_signing_certificate_references(document, signing_certificate)?;
if parsed_certificates.is_empty() {
return Ok(false);
}
let provider_signing_certificate = provider.certificate_details()?;
let mut provider_certificates = provider.certificate_chain_details()?;
if provider_certificates.is_empty() {
provider_certificates.push(provider_signing_certificate.clone());
}
if provider_certificates[0].der() != provider_signing_certificate.der() {
return Ok(false);
}
if parsed_certificates.len() > provider_certificates.len() {
return Ok(false);
}
for (parsed, expected) in parsed_certificates.iter().zip(provider_certificates.iter()) {
let actual_digest = digest_bytes(parsed.digest_algorithm, expected.der())?;
if actual_digest != parsed.digest_value {
return Ok(false);
}
if !issuer_serial_matches(parsed, expected) {
return Ok(false);
}
}
Ok(true)
}
fn parse_signing_certificate_references(
document: &Document,
signing_certificate: NodeId,
) -> XmlResult<Vec<ParsedSigningCertificateReference>> {
let mut parsed = Vec::new();
for cert in element_children(document, signing_certificate)? {
if !is_xades_element(document, cert, "Cert") {
continue;
}
parsed.push(parse_signing_certificate_reference(document, cert)?);
}
Ok(parsed)
}
fn parse_signing_certificate_reference(
document: &Document,
cert: NodeId,
) -> XmlResult<ParsedSigningCertificateReference> {
let cert_digest = required_xades_child(document, cert, "CertDigest")?;
let digest_method = required_child(document, cert_digest, "DigestMethod")?;
let digest_algorithm =
DigestAlgorithm::from_uri(&required_attribute(document, digest_method, "Algorithm")?)?;
let digest_value =
decode_standard_base64(&required_child_text(document, cert_digest, "DigestValue")?)?;
let (issuer_name, serial_number) = parse_issuer_serial(document, cert)?;
Ok(ParsedSigningCertificateReference {
digest_algorithm,
digest_value,
issuer_name,
serial_number,
})
}
fn parse_issuer_serial(
document: &Document,
cert: NodeId,
) -> XmlResult<(Option<String>, Option<String>)> {
let Some(issuer_serial) = optional_xades_child(document, cert, "IssuerSerial")? else {
return Ok((None, None));
};
let issuer_name = required_child_text(document, issuer_serial, "X509IssuerName")?;
let serial_number = required_child_text(document, issuer_serial, "X509SerialNumber")?;
Ok((Some(issuer_name), Some(serial_number)))
}
fn issuer_serial_matches(
parsed: &ParsedSigningCertificateReference,
expected: &CertificateDetails,
) -> bool {
match (
parsed.issuer_name.as_deref(),
parsed.serial_number.as_deref(),
expected.issuer_serial(),
) {
(None, None, None) => true,
(Some(parsed_issuer), Some(parsed_serial), Some((expected_issuer, expected_serial))) => {
parsed_issuer == expected_issuer && parsed_serial == expected_serial
}
_ => false,
}
}
fn verify_baseline_b_constraints(document: &Document, signature: NodeId) -> XmlResult<bool> {
let key_info = required_child(document, signature, "KeyInfo")?;
required_child(document, key_info, "X509Data")?;
let signed_properties = find_signed_properties(document, signature)?;
let signed_signature_properties =
required_xades_child(document, signed_properties, "SignedSignatureProperties")?;
let has_modern_certificate = optional_xades_child(
document,
signed_signature_properties,
"SigningCertificateV2",
)?
.is_some();
let has_classic_certificate =
optional_xades_child(document, signed_signature_properties, "SigningCertificate")?
.is_some();
Ok(has_modern_certificate && !has_classic_certificate)
}
fn verify_signature_policy(
document: &Document,
signed_properties: NodeId,
expected: &SignaturePolicy,
) -> XmlResult<bool> {
let signed_signature_properties =
required_xades_child(document, signed_properties, "SignedSignatureProperties")?;
let identifier = required_xades_child(
document,
signed_signature_properties,
"SignaturePolicyIdentifier",
)?;
let policy_id = required_xades_child(document, identifier, "SignaturePolicyId")?;
let sig_policy_id = required_xades_child(document, policy_id, "SigPolicyId")?;
let identifier_node = required_xades_child(document, sig_policy_id, "Identifier")?;
let actual_id_value = text_content(document, identifier_node)?;
if actual_id_value != expected.id.value() {
return Ok(false);
}
let actual_is_oid = matches!(expected.id, SignaturePolicyId::Oid(_));
let actual_qualifier = optional_attribute(document, identifier_node, "Qualifier")?;
if actual_is_oid != matches!(actual_qualifier.as_deref(), Some("OID")) {
return Ok(false);
}
let actual_description = optional_xades_child(document, sig_policy_id, "Description")?
.map(|node| text_content(document, node))
.transpose()?;
if actual_description != expected.description {
return Ok(false);
}
let sig_policy_hash = required_xades_child(document, policy_id, "SigPolicyHash")?;
let digest_method = required_child(document, sig_policy_hash, "DigestMethod")?;
let actual_algorithm =
DigestAlgorithm::from_uri(&required_attribute(document, digest_method, "Algorithm")?)?;
if actual_algorithm != expected.digest_algorithm {
return Ok(false);
}
let actual_digest = decode_standard_base64(&required_child_text(
document,
sig_policy_hash,
"DigestValue",
)?)?;
if actual_digest != expected.digest_value {
return Ok(false);
}
let actual_qualifiers = parse_signature_policy_qualifiers(document, policy_id)?;
Ok(actual_qualifiers == expected.qualifiers)
}
fn parse_signature_policy_qualifiers(
document: &Document,
policy_id: NodeId,
) -> XmlResult<Vec<SignaturePolicyQualifier>> {
let Some(qualifiers) = optional_xades_child(document, policy_id, "SigPolicyQualifiers")? else {
return Ok(Vec::new());
};
let mut parsed = Vec::new();
for qualifier in element_children(document, qualifiers)? {
if !is_xades_element(document, qualifier, "SigPolicyQualifier") {
continue;
}
parsed.push(parse_signature_policy_qualifier(document, qualifier)?);
}
Ok(parsed)
}
fn parse_signature_policy_qualifier(
document: &Document,
qualifier: NodeId,
) -> XmlResult<SignaturePolicyQualifier> {
let children = element_children(document, qualifier)?;
let [child] = children.as_slice() else {
return Err(XmlError::new(
ErrorKind::Signature,
"XAdES SigPolicyQualifier must contain exactly one supported child",
));
};
match element_local_name(document, *child)?.as_str() {
"SPURI" if is_xades_element(document, *child, "SPURI") => Ok(
SignaturePolicyQualifier::SpUri(text_content(document, *child)?),
),
"SPUserNotice" if is_xades_element(document, *child, "SPUserNotice") => {
parse_sp_user_notice(document, *child)
}
local => Err(XmlError::new(
ErrorKind::Signature,
format!("unsupported XAdES signature policy qualifier `{local}`"),
)),
}
}
fn parse_sp_user_notice(
document: &Document,
notice: NodeId,
) -> XmlResult<SignaturePolicyQualifier> {
let mut organization = None;
let mut notice_numbers = Vec::new();
let mut explicit_text = None;
if let Some(notice_ref) = optional_xades_child(document, notice, "NoticeRef")? {
if let Some(node) = optional_xades_child(document, notice_ref, "Organization")? {
organization = Some(text_content(document, node)?);
}
if let Some(numbers) = optional_xades_child(document, notice_ref, "NoticeNumbers")? {
for number in element_children(document, numbers)? {
if !is_xades_element(document, number, "int") {
continue;
}
let value = text_content(document, number)?;
let value = value.parse::<i64>().map_err(|_| {
XmlError::new(
ErrorKind::Signature,
format!("invalid SPUserNotice number `{value}`"),
)
})?;
notice_numbers.push(value);
}
}
}
if let Some(node) = optional_xades_child(document, notice, "ExplicitText")? {
explicit_text = Some(text_content(document, node)?);
}
Ok(SignaturePolicyQualifier::SpUserNotice {
organization,
notice_numbers,
explicit_text,
})
}
fn required_xades_child(document: &Document, parent: NodeId, local: &str) -> XmlResult<NodeId> {
optional_xades_child(document, parent, local)?.ok_or_else(|| {
XmlError::new(
ErrorKind::Signature,
format!("missing required XAdES child `{local}`"),
)
})
}
fn optional_xades_child(
document: &Document,
parent: NodeId,
local: &str,
) -> XmlResult<Option<NodeId>> {
Ok(element_children(document, parent)?
.into_iter()
.find(|child| is_xades_element(document, *child, local)))
}
fn is_xades_element(document: &Document, node: NodeId, local: &str) -> bool {
matches!(
document.node(node).map(|node| node.kind()),
Ok(NodeKind::Element(element))
if element.name().namespace_uri().map(|uri| uri.as_str()) == Some(XADES_NAMESPACE_URI)
&& element.name().local() == local
)
}
fn text_content(document: &Document, parent: NodeId) -> XmlResult<String> {
let mut text = String::new();
for child in document.children(parent)? {
if let NodeKind::Text(value) = document.node(*child)?.kind() {
text.push_str(value);
}
}
Ok(text)
}
#[cfg(test)]
mod tests {
use crate::parser::parse_str;
use crate::signature::xmldsig::element_local_name;
use crate::signature::{
CanonicalizationAlgorithm, CertificateDetails, DeterministicSigningProvider,
SignaturePlacement, XmlDsigReferenceConfig,
};
use crate::writer::to_string_compact;
use super::*;
fn provider() -> DeterministicSigningProvider {
DeterministicSigningProvider::new(b"test-cert".to_vec(), b"test-secret".to_vec())
}
fn provider_with_certificate_details() -> DeterministicSigningProvider {
provider().with_certificate_issuer_serial("CN=Signer,O=Example", "123456789")
}
fn provider_with_certificate_chain() -> DeterministicSigningProvider {
provider()
.with_certificate_issuer_serial("CN=Signer,O=Example", "123456789")
.with_certificate_chain_details(vec![
CertificateDetails::new(b"test-cert".to_vec())
.with_issuer_serial("CN=Signer,O=Example", "123456789"),
CertificateDetails::new(b"issuer-cert".to_vec())
.with_issuer_serial("CN=Issuer,O=Example", "987654321"),
])
}
fn config() -> XadesConfig {
XadesConfig::new().with_signing_time("2026-06-11T12:00:00Z")
}
fn signature_policy() -> XmlResult<SignaturePolicy> {
Ok(SignaturePolicy::new(
SignaturePolicyId::Uri("urn:example:policy:v1".to_owned()),
DigestAlgorithm::Sha256,
digest_bytes(DigestAlgorithm::Sha256, b"example policy bytes")?,
)
.with_qualifier(SignaturePolicyQualifier::SpUri(
"https://example.test/policy.pdf".to_owned(),
)))
}
fn extension_policy() -> XmlResult<SignaturePolicy> {
Ok(SignaturePolicy::new(
SignaturePolicyId::Uri("urn:example:policy:extensions".to_owned()),
DigestAlgorithm::Sha256,
digest_bytes(DigestAlgorithm::Sha256, b"extension policy bytes")?,
)
.with_description("Example policy description")
.with_qualifier(SignaturePolicyQualifier::SpUserNotice {
organization: Some("Example Org".to_owned()),
notice_numbers: vec![7, 11],
explicit_text: Some("Explicit notice text".to_owned()),
}))
}
fn epes_config() -> XmlResult<XadesConfig> {
Ok(config().with_profile(XadesProfile::Epes(signature_policy()?)))
}
fn baseline_b_config() -> XadesConfig {
config().with_profile(XadesProfile::BaselineB { policy: None })
}
fn baseline_b_policy_config() -> XmlResult<XadesConfig> {
Ok(config().with_profile(XadesProfile::BaselineB {
policy: Some(signature_policy()?),
}))
}
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 signing_time_unix_timestamp_formats_as_utc_xml_value() -> XmlResult<()> {
assert_eq!(format_unix_timestamp_utc(0)?, "1970-01-01T00:00:00Z");
assert_eq!(
format_unix_timestamp_utc(1_800_000_000)?,
"2027-01-15T08:00:00Z"
);
Ok(())
}
#[test]
fn xades_bes_signs_and_verifies_enveloped_document() -> XmlResult<()> {
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &config())?;
let report = verify_xades_bes_enveloped(&signed, &provider(), &config())?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert_eq!(report.profile, XadesProfile::Bes);
assert!(report.signed_properties_reference_valid);
assert!(report.signing_certificate_valid);
assert_eq!(report.signature_policy_valid, None);
assert!(xml.contains("<ds:Signature"));
assert!(xml.contains("<ds:Object><xades:QualifyingProperties"));
assert!(xml.contains("<xades:SignedProperties"));
assert!(xml.contains("Type=\"http://uri.etsi.org/01903#SignedProperties\""));
assert!(xml.contains("<xades:SigningTime>2026-06-11T12:00:00Z</xades:SigningTime>"));
assert!(xml.contains("<xades:SigningCertificateV2>"));
Ok(())
}
#[test]
fn xades_bes_matches_golden_fixture() -> XmlResult<()> {
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &config())?;
let xml = to_string_compact(&signed)?;
let expected = include_str!("../../tests/golden/signature/xades_bes.xml")
.strip_suffix('\n')
.unwrap_or(include_str!("../../tests/golden/signature/xades_bes.xml"));
assert_eq!(xml, expected);
Ok(())
}
#[test]
fn xades_bes_fails_when_signed_document_changes() -> XmlResult<()> {
let mut signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &config())?;
let root = signed.root().expect("root");
signed.add_text(root, "tampered")?;
let report = verify_xades_bes_enveloped(&signed, &provider(), &config())?;
assert!(!report.valid);
assert!(!report.xmldsig.reference_results[0].valid);
Ok(())
}
#[test]
fn signed_properties_fail_when_modified() -> XmlResult<()> {
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &config())?;
let xml =
to_string_compact(&signed)?.replace("2026-06-11T12:00:00Z", "2026-06-11T12:00:01Z");
let tampered = parse_str(&xml)?;
let report = verify_xades_bes_enveloped(&tampered, &provider(), &config())?;
assert!(!report.valid);
assert!(!report.signed_properties_reference_valid);
Ok(())
}
#[test]
fn signing_certificate_fails_when_key_info_certificate_changes() -> XmlResult<()> {
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &config())?;
let original_cert = encode_standard_base64(b"test-cert");
let changed_cert = encode_standard_base64(b"other-cert");
let tampered =
parse_str(&to_string_compact(&signed)?.replace(&original_cert, &changed_cert))?;
let report = verify_xades_bes_enveloped(&tampered, &provider(), &config())?;
assert!(!report.valid);
assert!(!report.signing_certificate_valid);
Ok(())
}
#[test]
fn signing_certificate_digest_is_validated() -> XmlResult<()> {
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &config())?;
let signature = find_signature(&signed)?;
assert!(verify_signing_certificate(&signed, signature, &provider())?);
Ok(())
}
#[test]
fn signing_certificate_details_are_emitted_and_validated_for_v2() -> XmlResult<()> {
let provider = provider_with_certificate_details();
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider, &config())?;
let report = verify_xades_bes_enveloped(&signed, &provider, &config())?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert!(xml.contains("<xades:SigningCertificateV2>"));
assert!(xml.contains("<xades:IssuerSerial>"));
assert!(xml.contains("<ds:X509IssuerName>CN=Signer,O=Example</ds:X509IssuerName>"));
assert!(xml.contains("<ds:X509SerialNumber>123456789</ds:X509SerialNumber>"));
Ok(())
}
#[test]
fn signing_certificate_details_are_emitted_and_validated_for_classic() -> XmlResult<()> {
let provider = provider_with_certificate_details();
let config = config()
.with_signing_certificate_mode(SigningCertificateMode::ClassicSigningCertificate);
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider, &config)?;
let report = verify_xades_bes_enveloped(&signed, &provider, &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert!(xml.contains("<xades:SigningCertificate>"));
assert!(xml.contains("<ds:X509IssuerName>CN=Signer,O=Example</ds:X509IssuerName>"));
assert!(xml.contains("<ds:X509SerialNumber>123456789</ds:X509SerialNumber>"));
Ok(())
}
#[test]
fn signing_certificate_details_reject_wrong_issuer_serial() -> XmlResult<()> {
let provider = provider_with_certificate_details();
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider, &config())?;
let tampered = parse_str(
&to_string_compact(&signed)?
.replace("<ds:X509SerialNumber>123456789", "<ds:X509SerialNumber>111"),
)?;
let report = verify_xades_bes_enveloped(&tampered, &provider, &config())?;
assert!(!report.valid);
assert!(!report.signing_certificate_valid);
Ok(())
}
#[test]
fn xades_bes_can_emit_classic_signing_certificate() -> XmlResult<()> {
let config = config()
.with_signing_certificate_mode(SigningCertificateMode::ClassicSigningCertificate);
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
assert!(xml.contains("<xades:SigningCertificate>"));
assert!(verify_xades_bes_enveloped(&signed, &provider(), &config)?.valid);
Ok(())
}
#[test]
fn xades_bes_can_select_c14n10_from_xmldsig_config() -> XmlResult<()> {
let config = config().with_xmldsig_config(
XmlDsigConfig::new().with_canonicalization(CanonicalizationAlgorithm::CanonicalXml10),
);
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &config)?;
let report = verify_xades_bes_enveloped(&signed, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert_eq!(
xml.matches(r#"Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315""#)
.count(),
3
);
Ok(())
}
#[test]
fn xades_bes_can_sign_whole_document_and_key_info_references() -> XmlResult<()> {
let config = config().with_xmldsig_config(
XmlDsigConfig::new()
.with_key_info_id("key-info-1")
.with_references(vec![
XmlDsigReferenceConfig::whole_document(),
XmlDsigReferenceConfig::key_info(),
]),
);
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &config)?;
let report = verify_xades_bes_enveloped(&signed, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert_eq!(report.xmldsig.reference_results.len(), 3);
assert!(xml.contains(r#"<ds:Reference URI="">"#));
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">"#
));
assert!(xml.contains(r#"Type="http://uri.etsi.org/01903#SignedProperties""#));
Ok(())
}
#[test]
fn xades_bes_can_place_signature_by_query() -> XmlResult<()> {
let config = config().with_xmldsig_config(
XmlDsigConfig::new()
.with_signature_placement(SignaturePlacement::query("/Root/Extension")),
);
let signed = sign_xades_bes_enveloped(&extension_document()?, &provider(), &config)?;
let report = verify_xades_bes_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 xades_profile_name_is_stable() {
assert_eq!(XadesProfile::Bes.name(), "XAdES-BES");
assert_eq!(
XadesProfile::Epes(SignaturePolicy::new(
SignaturePolicyId::Uri("urn:p".to_owned()),
DigestAlgorithm::Sha256,
[]
))
.name(),
"XAdES-EPES"
);
assert_eq!(XadesProfile::BaselineB { policy: None }.name(), "XAdES-B-B");
}
#[test]
fn xades_uses_xml_element_names_for_required_children() -> XmlResult<()> {
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &config())?;
let signature = find_signature(&signed)?;
let object = required_child(&signed, signature, "Object")?;
assert_eq!(element_local_name(&signed, object)?, "Object");
Ok(())
}
#[test]
fn certificate_chain_can_be_included_by_config() -> XmlResult<()> {
let provider = provider_with_certificate_chain();
let config = config().with_certificate_chain(true);
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider, &config)?;
let report = verify_xades_bes_enveloped(&signed, &provider, &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert_eq!(xml.matches("<xades:Cert>").count(), 2);
assert!(xml.contains("<ds:X509IssuerName>CN=Issuer,O=Example</ds:X509IssuerName>"));
assert!(xml.contains("<ds:X509SerialNumber>987654321</ds:X509SerialNumber>"));
Ok(())
}
#[test]
fn certificate_chain_rejects_tampered_issuer_serial() -> XmlResult<()> {
let provider = provider_with_certificate_chain();
let config = config().with_certificate_chain(true);
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider, &config)?;
let tampered = parse_str(
&to_string_compact(&signed)?
.replace("<ds:X509SerialNumber>987654321", "<ds:X509SerialNumber>222"),
)?;
let report = verify_xades_bes_enveloped(&tampered, &provider, &config)?;
assert!(!report.valid);
assert!(!report.signing_certificate_valid);
Ok(())
}
#[test]
fn xades_epes_signs_and_verifies_enveloped_document() -> XmlResult<()> {
let config = epes_config()?;
let signed = sign_xades_epes_enveloped(&unsigned_document()?, &provider(), &config)?;
let report = verify_xades_epes_enveloped(&signed, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert_eq!(report.profile, *config.profile());
assert!(report.signed_properties_reference_valid);
assert!(report.signing_certificate_valid);
assert_eq!(report.signature_policy_valid, Some(true));
assert!(xml.contains("<xades:SignaturePolicyIdentifier>"));
assert!(xml.contains("<xades:SignaturePolicyId>"));
assert!(xml.contains("<xades:SigPolicyHash>"));
assert!(xml.contains("<xades:SPURI>https://example.test/policy.pdf</xades:SPURI>"));
Ok(())
}
#[test]
fn xades_epes_matches_golden_fixture() -> XmlResult<()> {
let config = epes_config()?;
let signed = sign_xades_epes_enveloped(&unsigned_document()?, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
let expected = include_str!("../../tests/golden/signature/xades_epes.xml")
.strip_suffix('\n')
.unwrap_or(include_str!("../../tests/golden/signature/xades_epes.xml"));
assert_eq!(xml, expected);
Ok(())
}
#[test]
fn xades_epes_rejects_bes_when_epes_is_required() -> XmlResult<()> {
let bes = sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &config())?;
let error = verify_xades_epes_enveloped(&bes, &provider(), &epes_config()?)
.expect_err("BES must not satisfy EPES verification");
assert_eq!(error.kind(), &ErrorKind::Signature);
assert!(error.message().contains("SignaturePolicyIdentifier"));
Ok(())
}
#[test]
fn xades_epes_fails_when_policy_identifier_changes() -> XmlResult<()> {
let config = epes_config()?;
let signed = sign_xades_epes_enveloped(&unsigned_document()?, &provider(), &config)?;
let tampered = parse_str(
&to_string_compact(&signed)?.replace("urn:example:policy:v1", "urn:example:policy:v2"),
)?;
let report = verify_xades_epes_enveloped(&tampered, &provider(), &config)?;
assert!(!report.valid);
assert!(!report.signed_properties_reference_valid);
assert_eq!(report.signature_policy_valid, Some(false));
Ok(())
}
#[test]
fn xades_epes_fails_when_policy_hash_changes() -> XmlResult<()> {
let config = epes_config()?;
let signed = sign_xades_epes_enveloped(&unsigned_document()?, &provider(), &config)?;
let original_hash = encode_standard_base64(digest_bytes(
DigestAlgorithm::Sha256,
b"example policy bytes",
)?);
let changed_hash = encode_standard_base64(digest_bytes(
DigestAlgorithm::Sha256,
b"other policy bytes",
)?);
let tampered =
parse_str(&to_string_compact(&signed)?.replace(&original_hash, &changed_hash))?;
let report = verify_xades_epes_enveloped(&tampered, &provider(), &config)?;
assert!(!report.valid);
assert!(!report.signed_properties_reference_valid);
assert_eq!(report.signature_policy_valid, Some(false));
Ok(())
}
#[test]
fn xades_signed_properties_extensions_emit_policy_description_and_user_notice() -> XmlResult<()>
{
let config = config().with_profile(XadesProfile::Epes(extension_policy()?));
let signed = sign_xades_epes_enveloped(&unsigned_document()?, &provider(), &config)?;
let report = verify_xades_epes_enveloped(&signed, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert_eq!(report.signature_policy_valid, Some(true));
assert!(xml.contains("<xades:Description>Example policy description</xades:Description>"));
assert!(xml.contains("<xades:SPUserNotice>"));
assert!(xml.contains("<xades:Organization>Example Org</xades:Organization>"));
assert!(xml.contains("<xades:int>7</xades:int>"));
assert!(xml.contains("<xades:int>11</xades:int>"));
assert!(xml.contains("<xades:ExplicitText>Explicit notice text</xades:ExplicitText>"));
Ok(())
}
#[test]
fn xades_signed_properties_extensions_reject_tampered_policy_description() -> XmlResult<()> {
let config = config().with_profile(XadesProfile::Epes(extension_policy()?));
let signed = sign_xades_epes_enveloped(&unsigned_document()?, &provider(), &config)?;
let tampered = parse_str(
&to_string_compact(&signed)?
.replace("Example policy description", "Changed policy description"),
)?;
let report = verify_xades_epes_enveloped(&tampered, &provider(), &config)?;
assert!(!report.valid);
assert!(!report.signed_properties_reference_valid);
assert_eq!(report.signature_policy_valid, Some(false));
Ok(())
}
#[test]
fn signer_role_claimed_roles_are_signed() -> XmlResult<()> {
let config =
config().with_signer_role(SignerRole::claimed("author").with_claimed_role("reviewer"));
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &config)?;
let report = verify_xades_bes_enveloped(&signed, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert_eq!(config.signer_role().claimed_roles(), ["author", "reviewer"]);
assert!(xml.contains("<xades:SignerRole>"));
assert!(xml.contains("<xades:ClaimedRole>author</xades:ClaimedRole>"));
assert!(xml.contains("<xades:ClaimedRole>reviewer</xades:ClaimedRole>"));
Ok(())
}
#[test]
fn signer_role_rejects_tampered_claimed_role() -> XmlResult<()> {
let config = config().with_claimed_role("author");
let signed = sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &config)?;
let tampered = parse_str(&to_string_compact(&signed)?.replace(
"<xades:ClaimedRole>author</xades:ClaimedRole>",
"<xades:ClaimedRole>changed</xades:ClaimedRole>",
))?;
let report = verify_xades_bes_enveloped(&tampered, &provider(), &config)?;
assert!(!report.valid);
assert!(!report.signed_properties_reference_valid);
Ok(())
}
#[test]
fn xades_signer_and_verifier_support_epes_profile() -> XmlResult<()> {
let config = epes_config()?;
let signer = crate::signature::XadesSigner::new(provider()).with_config(config.clone());
let verifier = crate::signature::XadesVerifier::new(provider()).with_config(config);
let signed = signer.sign_document(&unsigned_document()?)?;
let report = verifier.verify_document(&signed)?;
assert!(report.valid);
assert_eq!(report.signature_policy_valid, Some(true));
Ok(())
}
#[test]
fn xades_baseline_b_signs_and_verifies_enveloped_document() -> XmlResult<()> {
let config = baseline_b_config()
.with_signing_certificate_mode(SigningCertificateMode::ClassicSigningCertificate);
let signed = sign_xades_baseline_b_enveloped(&unsigned_document()?, &provider(), &config)?;
let report = verify_xades_baseline_b_enveloped(&signed, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert_eq!(report.profile, XadesProfile::BaselineB { policy: None });
assert_eq!(report.signature_policy_valid, None);
assert!(xml.contains("<ds:KeyInfo><ds:X509Data>"));
assert!(xml.contains("<xades:SigningCertificateV2>"));
assert!(!xml.contains("<xades:SigningCertificate>"));
assert!(!xml.contains("<xades:SignaturePolicyIdentifier>"));
Ok(())
}
#[test]
fn xades_baseline_b_matches_golden_fixture() -> XmlResult<()> {
let config = baseline_b_config();
let signed = sign_xades_baseline_b_enveloped(&unsigned_document()?, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
let expected = include_str!("../../tests/golden/signature/xades_baseline_b.xml")
.strip_suffix('\n')
.unwrap_or(include_str!(
"../../tests/golden/signature/xades_baseline_b.xml"
));
assert_eq!(xml, expected);
Ok(())
}
#[test]
fn xades_baseline_b_allows_optional_policy() -> XmlResult<()> {
let config = baseline_b_policy_config()?;
let signed = sign_xades_baseline_b_enveloped(&unsigned_document()?, &provider(), &config)?;
let report = verify_xades_baseline_b_enveloped(&signed, &provider(), &config)?;
let xml = to_string_compact(&signed)?;
assert!(report.valid);
assert_eq!(report.signature_policy_valid, Some(true));
assert!(xml.contains("<xades:SignaturePolicyIdentifier>"));
Ok(())
}
#[test]
fn xades_baseline_b_rejects_classic_signing_certificate() -> XmlResult<()> {
let classic_bes_config = config()
.with_signing_certificate_mode(SigningCertificateMode::ClassicSigningCertificate);
let signed =
sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &classic_bes_config)?;
let report = verify_xades_baseline_b_enveloped(&signed, &provider(), &baseline_b_config())?;
assert!(!report.valid);
assert!(report.signing_certificate_valid);
Ok(())
}
#[test]
fn xades_signer_and_verifier_support_baseline_b_profile() -> XmlResult<()> {
let config = baseline_b_config();
let signer = crate::signature::XadesSigner::new(provider()).with_config(config.clone());
let verifier = crate::signature::XadesVerifier::new(provider()).with_config(config);
let signed = signer.sign_document(&unsigned_document()?)?;
let report = verifier.verify_document(&signed)?;
assert!(report.valid);
assert_eq!(report.signature_policy_valid, None);
Ok(())
}
}