use std::collections::BTreeSet;
use std::fs;
use std::path::Path;
use crate::core::{Document, ErrorKind, NodeId, NodeKind, QName, XmlError, XmlResult};
use super::xmldsig::{element_children, find_signature, required_child};
use super::{decode_standard_base64, encode_standard_base64, XADES_NAMESPACE_URI};
pub trait XadesValidationDataProvider {
fn certificate_values(&self) -> XmlResult<Vec<Vec<u8>>>;
fn ocsp_values(&self) -> XmlResult<Vec<Vec<u8>>> {
Ok(Vec::new())
}
fn crl_values(&self) -> XmlResult<Vec<Vec<u8>>> {
Ok(Vec::new())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StaticValidationDataProvider {
certificates: Vec<Vec<u8>>,
ocsp: Vec<Vec<u8>>,
crls: Vec<Vec<u8>>,
}
impl StaticValidationDataProvider {
pub fn new() -> Self {
Self::default()
}
pub fn with_certificate(mut self, certificate: impl Into<Vec<u8>>) -> Self {
self.certificates.push(certificate.into());
self
}
pub fn with_ocsp(mut self, ocsp: impl Into<Vec<u8>>) -> Self {
self.ocsp.push(ocsp.into());
self
}
pub fn with_crl(mut self, crl: impl Into<Vec<u8>>) -> Self {
self.crls.push(crl.into());
self
}
pub fn with_ocsp_file(self, path: impl AsRef<Path>) -> XmlResult<Self> {
Ok(self.with_ocsp(read_validation_file(path, "cannot read OCSP file")?))
}
pub fn with_ocsp_files<I, P>(mut self, paths: I) -> XmlResult<Self>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
for path in paths {
self = self.with_ocsp_file(path)?;
}
Ok(self)
}
pub fn with_crl_file(self, path: impl AsRef<Path>) -> XmlResult<Self> {
Ok(self.with_crl(read_validation_file(path, "cannot read CRL file")?))
}
pub fn with_crl_files<I, P>(mut self, paths: I) -> XmlResult<Self>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
for path in paths {
self = self.with_crl_file(path)?;
}
Ok(self)
}
}
impl XadesValidationDataProvider for StaticValidationDataProvider {
fn certificate_values(&self) -> XmlResult<Vec<Vec<u8>>> {
Ok(self.certificates.clone())
}
fn ocsp_values(&self) -> XmlResult<Vec<Vec<u8>>> {
Ok(self.ocsp.clone())
}
fn crl_values(&self) -> XmlResult<Vec<Vec<u8>>> {
Ok(self.crls.clone())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XadesValidationDataConfig {
require_certificate_values: bool,
require_revocation_values: bool,
}
impl XadesValidationDataConfig {
pub fn new() -> Self {
Self::default()
}
pub fn require_certificate_values(mut self, required: bool) -> Self {
self.require_certificate_values = required;
self
}
pub fn require_revocation_values(mut self, required: bool) -> Self {
self.require_revocation_values = required;
self
}
pub fn certificate_values_required(&self) -> bool {
self.require_certificate_values
}
pub fn revocation_values_required(&self) -> bool {
self.require_revocation_values
}
}
impl Default for XadesValidationDataConfig {
fn default() -> Self {
Self {
require_certificate_values: true,
require_revocation_values: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum XadesValidationDataKind {
CertificateValues,
RevocationValues,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XadesValidationDataReport {
pub certificate_values_present: bool,
pub revocation_values_present: bool,
pub certificate_count: usize,
pub ocsp_count: usize,
pub crl_count: usize,
pub missing: Vec<XadesValidationDataKind>,
}
impl XadesValidationDataReport {
pub fn required_material_present(&self) -> bool {
self.missing.is_empty()
}
}
pub fn add_xades_validation_data<P>(
document: &Document,
provider: &P,
config: &XadesValidationDataConfig,
) -> XmlResult<Document>
where
P: XadesValidationDataProvider + ?Sized,
{
let mut enriched = document.clone();
let signature = find_signature(&enriched)?;
let qualifying_properties = find_qualifying_properties(&enriched, signature)?;
let unsigned_signature_properties =
ensure_unsigned_signature_properties(&mut enriched, qualifying_properties)?;
if optional_xades_child(
&enriched,
unsigned_signature_properties,
"CertificateValues",
)?
.is_some()
|| optional_xades_child(&enriched, unsigned_signature_properties, "RevocationValues")?
.is_some()
{
return Err(XmlError::new(
ErrorKind::Signature,
"XAdES validation data already exists",
));
}
let certificates = unique_values(provider.certificate_values()?);
let ocsp = unique_values(provider.ocsp_values()?);
let crls = unique_values(provider.crl_values()?);
if config.require_certificate_values && certificates.is_empty() {
return Err(XmlError::new(
ErrorKind::Signature,
"required XAdES CertificateValues material is missing",
));
}
if config.require_revocation_values && ocsp.is_empty() && crls.is_empty() {
return Err(XmlError::new(
ErrorKind::Signature,
"required XAdES RevocationValues material is missing",
));
}
if !certificates.is_empty() {
add_certificate_values(&mut enriched, unsigned_signature_properties, &certificates)?;
}
if !ocsp.is_empty() || !crls.is_empty() {
add_revocation_values(&mut enriched, unsigned_signature_properties, &ocsp, &crls)?;
}
Ok(enriched)
}
pub fn verify_xades_validation_data(
document: &Document,
config: &XadesValidationDataConfig,
) -> XmlResult<XadesValidationDataReport> {
let signature = find_signature(document)?;
let qualifying_properties = find_qualifying_properties(document, signature)?;
let Some(unsigned_properties) =
optional_xades_child(document, qualifying_properties, "UnsignedProperties")?
else {
return Ok(missing_report(config));
};
let Some(unsigned_signature_properties) =
optional_xades_child(document, unsigned_properties, "UnsignedSignatureProperties")?
else {
return Ok(missing_report(config));
};
let certificate_values =
optional_xades_child(document, unsigned_signature_properties, "CertificateValues")?;
let revocation_values =
optional_xades_child(document, unsigned_signature_properties, "RevocationValues")?;
let certificate_count = match certificate_values {
Some(node) => count_xades_values(document, node, "EncapsulatedX509Certificate")?,
None => 0,
};
let (ocsp_count, crl_count) = match revocation_values {
Some(node) => (
count_xades_values(document, node, "EncapsulatedOCSPValue")?,
count_xades_values(document, node, "EncapsulatedCRLValue")?,
),
None => (0, 0),
};
let mut missing = Vec::new();
if config.require_certificate_values && certificate_count == 0 {
missing.push(XadesValidationDataKind::CertificateValues);
}
if config.require_revocation_values && ocsp_count == 0 && crl_count == 0 {
missing.push(XadesValidationDataKind::RevocationValues);
}
Ok(XadesValidationDataReport {
certificate_values_present: certificate_values.is_some(),
revocation_values_present: revocation_values.is_some(),
certificate_count,
ocsp_count,
crl_count,
missing,
})
}
fn missing_report(config: &XadesValidationDataConfig) -> XadesValidationDataReport {
let mut missing = Vec::new();
if config.require_certificate_values {
missing.push(XadesValidationDataKind::CertificateValues);
}
if config.require_revocation_values {
missing.push(XadesValidationDataKind::RevocationValues);
}
XadesValidationDataReport {
certificate_values_present: false,
revocation_values_present: false,
certificate_count: 0,
ocsp_count: 0,
crl_count: 0,
missing,
}
}
fn unique_values(values: Vec<Vec<u8>>) -> Vec<Vec<u8>> {
let mut seen = BTreeSet::new();
values
.into_iter()
.filter(|value| seen.insert(value.clone()))
.collect()
}
fn read_validation_file(path: impl AsRef<Path>, context: &str) -> XmlResult<Vec<u8>> {
let path = path.as_ref();
fs::read(path).map_err(|error| {
XmlError::new(
ErrorKind::Signature,
format!("{context} `{}`: {error}", path.display()),
)
})
}
fn add_certificate_values(
document: &mut Document,
parent: NodeId,
certificates: &[Vec<u8>],
) -> XmlResult<()> {
let certificate_values = document.add_element(
parent,
QName::qualified("xades", "CertificateValues", XADES_NAMESPACE_URI)?,
)?;
for certificate in certificates {
add_text_element(
document,
certificate_values,
"EncapsulatedX509Certificate",
encode_standard_base64(certificate),
)?;
}
Ok(())
}
fn add_revocation_values(
document: &mut Document,
parent: NodeId,
ocsp: &[Vec<u8>],
crls: &[Vec<u8>],
) -> XmlResult<()> {
let revocation_values = document.add_element(
parent,
QName::qualified("xades", "RevocationValues", XADES_NAMESPACE_URI)?,
)?;
if !ocsp.is_empty() {
let ocsp_values = document.add_element(
revocation_values,
QName::qualified("xades", "OCSPValues", XADES_NAMESPACE_URI)?,
)?;
for value in ocsp {
add_text_element(
document,
ocsp_values,
"EncapsulatedOCSPValue",
encode_standard_base64(value),
)?;
}
}
if !crls.is_empty() {
let crl_values = document.add_element(
revocation_values,
QName::qualified("xades", "CRLValues", XADES_NAMESPACE_URI)?,
)?;
for value in crls {
add_text_element(
document,
crl_values,
"EncapsulatedCRLValue",
encode_standard_base64(value),
)?;
}
}
Ok(())
}
fn add_text_element(
document: &mut Document,
parent: NodeId,
local: &str,
value: impl Into<String>,
) -> XmlResult<NodeId> {
let node = document.add_element(
parent,
QName::qualified("xades", local, XADES_NAMESPACE_URI)?,
)?;
document.add_text(node, value)?;
Ok(node)
}
fn count_xades_values(document: &Document, parent: NodeId, local: &str) -> XmlResult<usize> {
let mut count = 0;
count_xades_values_recursive(document, parent, local, &mut count)?;
Ok(count)
}
fn count_xades_values_recursive(
document: &Document,
parent: NodeId,
local: &str,
count: &mut usize,
) -> XmlResult<()> {
for child in element_children(document, parent)? {
if is_xades_element(document, child, local) {
let text = text_content(document, child)?;
decode_standard_base64(&text)?;
*count += 1;
}
count_xades_values_recursive(document, child, local, count)?;
}
Ok(())
}
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);
}
}
if text.is_empty() {
return Err(XmlError::new(
ErrorKind::Signature,
"XAdES validation data value must contain text",
));
}
Ok(text)
}
fn find_qualifying_properties(document: &Document, signature: NodeId) -> XmlResult<NodeId> {
let object = required_child(document, signature, "Object")?;
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",
)
})
}
fn ensure_unsigned_signature_properties(
document: &mut Document,
qualifying_properties: NodeId,
) -> XmlResult<NodeId> {
let unsigned_properties =
match optional_xades_child(document, qualifying_properties, "UnsignedProperties")? {
Some(node) => node,
None => document.add_element(
qualifying_properties,
QName::qualified("xades", "UnsignedProperties", XADES_NAMESPACE_URI)?,
)?,
};
match optional_xades_child(document, unsigned_properties, "UnsignedSignatureProperties")? {
Some(node) => Ok(node),
None => document.add_element(
unsigned_properties,
QName::qualified("xades", "UnsignedSignatureProperties", XADES_NAMESPACE_URI)?,
),
}
}
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
)
}
#[cfg(test)]
mod tests {
use std::fs;
use crate::parser::parse_str;
use crate::signature::{sign_xades_bes_enveloped, DeterministicSigningProvider, XadesConfig};
use crate::writer::to_string_compact;
use super::*;
fn provider() -> DeterministicSigningProvider {
DeterministicSigningProvider::new(b"test-cert".to_vec(), b"test-secret".to_vec())
}
fn signed_document() -> XmlResult<Document> {
let document = parse_str(r#"<Root Id="doc-1"><Item>value</Item></Root>"#)?;
sign_xades_bes_enveloped(
&document,
&provider(),
&XadesConfig::new().with_signing_time("2026-06-11T12:00:00Z"),
)
}
fn temp_path(name: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"xdoc-validation-data-{name}-{}",
std::process::id()
))
}
#[test]
fn xades_validation_data_adds_certificate_and_revocation_values() -> XmlResult<()> {
let validation_provider = StaticValidationDataProvider::new()
.with_certificate(b"chain-cert")
.with_ocsp(b"ocsp-response")
.with_crl(b"crl-response");
let enriched = add_xades_validation_data(
&signed_document()?,
&validation_provider,
&XadesValidationDataConfig::new(),
)?;
let report = verify_xades_validation_data(&enriched, &XadesValidationDataConfig::new())?;
let xml = to_string_compact(&enriched)?;
assert!(report.required_material_present());
assert_eq!(report.certificate_count, 1);
assert_eq!(report.ocsp_count, 1);
assert_eq!(report.crl_count, 1);
assert!(xml.contains("<xades:CertificateValues>"));
assert!(xml.contains("<xades:RevocationValues>"));
assert!(xml.contains("<xades:EncapsulatedX509Certificate>"));
assert!(xml.contains("<xades:EncapsulatedOCSPValue>"));
assert!(xml.contains("<xades:EncapsulatedCRLValue>"));
Ok(())
}
#[test]
fn xades_validation_data_reports_missing_material() -> XmlResult<()> {
let report =
verify_xades_validation_data(&signed_document()?, &XadesValidationDataConfig::new())?;
assert!(!report.required_material_present());
assert_eq!(
report.missing,
vec![
XadesValidationDataKind::CertificateValues,
XadesValidationDataKind::RevocationValues
]
);
Ok(())
}
#[test]
fn xades_validation_data_rejects_duplicate_insertion() -> XmlResult<()> {
let validation_provider = StaticValidationDataProvider::new()
.with_certificate(b"chain-cert")
.with_ocsp(b"ocsp-response");
let enriched = add_xades_validation_data(
&signed_document()?,
&validation_provider,
&XadesValidationDataConfig::new(),
)?;
let error = add_xades_validation_data(
&enriched,
&validation_provider,
&XadesValidationDataConfig::new(),
)
.expect_err("validation data must not be duplicated");
assert_eq!(error.kind(), &ErrorKind::Signature);
Ok(())
}
#[test]
fn xades_validation_data_deduplicates_provider_values() -> XmlResult<()> {
let validation_provider = StaticValidationDataProvider::new()
.with_certificate(b"chain-cert")
.with_certificate(b"chain-cert")
.with_ocsp(b"ocsp-response")
.with_ocsp(b"ocsp-response");
let enriched = add_xades_validation_data(
&signed_document()?,
&validation_provider,
&XadesValidationDataConfig::new(),
)?;
let report = verify_xades_validation_data(&enriched, &XadesValidationDataConfig::new())?;
assert_eq!(report.certificate_count, 1);
assert_eq!(report.ocsp_count, 1);
assert_eq!(report.crl_count, 0);
Ok(())
}
#[test]
fn static_validation_data_provider_can_read_ocsp_and_crl_files() -> XmlResult<()> {
let ocsp_path = temp_path("ocsp.der");
let crl_path = temp_path("crl.der");
fs::write(&ocsp_path, b"ocsp-file").expect("write ocsp fixture");
fs::write(&crl_path, b"crl-file").expect("write crl fixture");
let provider = StaticValidationDataProvider::new()
.with_ocsp_files([&ocsp_path])?
.with_crl_files([&crl_path])?;
assert_eq!(provider.ocsp_values()?, vec![b"ocsp-file".to_vec()]);
assert_eq!(provider.crl_values()?, vec![b"crl-file".to_vec()]);
let _ = fs::remove_file(ocsp_path);
let _ = fs::remove_file(crl_path);
Ok(())
}
}