xdoc-rs 0.1.1

Declarative XML engine for Rust
Documentation
use crate::core::{Document, ErrorKind, XmlError, XmlResult};

use super::{
    add_signature_timestamp, add_xades_archive_timestamp, add_xades_validation_data,
    sign_xades_baseline_b_enveloped, sign_xades_bes_enveloped, sign_xades_epes_enveloped,
    verify_signature_timestamp, verify_xades_archive_timestamps, verify_xades_baseline_b_enveloped,
    verify_xades_bes_enveloped, verify_xades_epes_enveloped, verify_xades_validation_data,
    SignatureValidationProfile, SignatureValidationReport, SigningProvider,
    TimestampAuthorityClient, TimestampValidationReport, XadesArchiveConfig, XadesArchiveReport,
    XadesConfig, XadesProfile, XadesTimestampConfig, XadesValidationDataConfig,
    XadesValidationDataProvider, XadesValidationDataReport, XadesVerificationReport,
};

/// Optional steps for a high-level XAdES signing flow.
///
/// The signer remains domain-agnostic: callers provide all policy, timestamp,
/// validation-data, placement, and strictness requirements explicitly.
pub struct XadesSigningOptions<'a> {
    timestamp_authority: Option<&'a dyn TimestampAuthorityClient>,
    timestamp_config: Option<XadesTimestampConfig>,
    validation_data_provider: Option<&'a dyn XadesValidationDataProvider>,
    validation_data_config: Option<XadesValidationDataConfig>,
    archive_timestamp_authority: Option<&'a dyn TimestampAuthorityClient>,
    archive_config: Option<XadesArchiveConfig>,
    initial_validation_profile: Option<SignatureValidationProfile>,
    final_validation_profile: Option<SignatureValidationProfile>,
}

impl<'a> XadesSigningOptions<'a> {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_timestamp<C>(mut self, client: &'a C, config: XadesTimestampConfig) -> Self
    where
        C: TimestampAuthorityClient + 'a,
    {
        self.timestamp_authority = Some(client);
        self.timestamp_config = Some(config);
        self
    }

    pub fn with_validation_data<P>(
        mut self,
        provider: &'a P,
        config: XadesValidationDataConfig,
    ) -> Self
    where
        P: XadesValidationDataProvider + 'a,
    {
        self.validation_data_provider = Some(provider);
        self.validation_data_config = Some(config);
        self
    }

    pub fn with_archive_timestamp<C>(mut self, client: &'a C, config: XadesArchiveConfig) -> Self
    where
        C: TimestampAuthorityClient + 'a,
    {
        self.archive_timestamp_authority = Some(client);
        self.archive_config = Some(config);
        self
    }

    pub fn with_initial_validation_profile(mut self, profile: SignatureValidationProfile) -> Self {
        self.initial_validation_profile = Some(profile);
        self
    }

    pub fn with_final_validation_profile(mut self, profile: SignatureValidationProfile) -> Self {
        self.final_validation_profile = Some(profile);
        self
    }
}

impl Default for XadesSigningOptions<'_> {
    fn default() -> Self {
        Self {
            timestamp_authority: None,
            timestamp_config: None,
            validation_data_provider: None,
            validation_data_config: None,
            archive_timestamp_authority: None,
            archive_config: None,
            initial_validation_profile: None,
            final_validation_profile: None,
        }
    }
}

/// Reports generated by a high-level XAdES signing flow.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XadesSigningReport {
    pub initial_xades: XadesVerificationReport,
    pub initial_validation_profile: Option<SignatureValidationReport>,
    pub timestamp: Option<TimestampValidationReport>,
    pub validation_data: Option<XadesValidationDataReport>,
    pub archive: Option<XadesArchiveReport>,
    pub final_xades: XadesVerificationReport,
    pub final_validation_profile: Option<SignatureValidationReport>,
}

/// Document plus structured reports returned by a high-level XAdES signing flow.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XadesSignedDocument {
    document: Document,
    report: XadesSigningReport,
}

impl XadesSignedDocument {
    pub fn new(document: Document, report: XadesSigningReport) -> Self {
        Self { document, report }
    }

    pub fn document(&self) -> &Document {
        &self.document
    }

    pub fn report(&self) -> &XadesSigningReport {
        &self.report
    }

    pub fn into_document(self) -> Document {
        self.document
    }

    pub fn into_parts(self) -> (Document, XadesSigningReport) {
        (self.document, self.report)
    }
}

/// High-level XAdES signer for complete XML documents.
#[derive(Debug, Clone)]
pub struct XadesSigner<P> {
    provider: P,
    config: XadesConfig,
}

impl<P> XadesSigner<P>
where
    P: SigningProvider,
{
    pub fn new(provider: P) -> Self {
        Self {
            provider,
            config: XadesConfig::new(),
        }
    }

    pub fn with_config(mut self, config: XadesConfig) -> Self {
        self.config = config;
        self
    }

    pub fn sign_document(&self, document: &Document) -> XmlResult<Document> {
        match self.config.profile() {
            XadesProfile::Bes => sign_xades_bes_enveloped(document, &self.provider, &self.config),
            XadesProfile::Epes(_) => {
                sign_xades_epes_enveloped(document, &self.provider, &self.config)
            }
            XadesProfile::BaselineB { .. } => {
                sign_xades_baseline_b_enveloped(document, &self.provider, &self.config)
            }
        }
    }

    pub fn sign_document_with_options(
        &self,
        document: &Document,
        options: &XadesSigningOptions<'_>,
    ) -> XmlResult<XadesSignedDocument> {
        let mut signed = self.sign_document(document)?;
        let initial_xades = self.verify_xades(&signed)?;
        let initial_validation_profile = options
            .initial_validation_profile
            .as_ref()
            .map(|profile| profile.validate(&signed, &self.provider))
            .transpose()?;

        let timestamp = if let Some(config) = &options.timestamp_config {
            let client = options.timestamp_authority.ok_or_else(|| {
                XmlError::new(
                    ErrorKind::Signature,
                    "XAdES timestamp config requires a timestamp authority",
                )
            })?;
            signed = add_signature_timestamp(&signed, client, config)?;
            Some(verify_signature_timestamp(&signed, client, config)?)
        } else {
            None
        };

        let validation_data = if let Some(config) = &options.validation_data_config {
            let provider = options.validation_data_provider.ok_or_else(|| {
                XmlError::new(
                    ErrorKind::Signature,
                    "XAdES validation data config requires a validation data provider",
                )
            })?;
            signed = add_xades_validation_data(&signed, provider, config)?;
            Some(verify_xades_validation_data(&signed, config)?)
        } else {
            None
        };

        let archive = if let Some(config) = &options.archive_config {
            let client = options.archive_timestamp_authority.ok_or_else(|| {
                XmlError::new(
                    ErrorKind::Signature,
                    "XAdES archive config requires a timestamp authority",
                )
            })?;
            signed = add_xades_archive_timestamp(&signed, client, config)?;
            Some(verify_xades_archive_timestamps(&signed, client, config)?)
        } else {
            None
        };

        let final_xades = self.verify_xades(&signed)?;
        let final_validation_profile = options
            .final_validation_profile
            .as_ref()
            .map(|profile| profile.validate(&signed, &self.provider))
            .transpose()?;

        Ok(XadesSignedDocument::new(
            signed,
            XadesSigningReport {
                initial_xades,
                initial_validation_profile,
                timestamp,
                validation_data,
                archive,
                final_xades,
                final_validation_profile,
            },
        ))
    }

    pub fn config(&self) -> &XadesConfig {
        &self.config
    }

    pub fn provider(&self) -> &P {
        &self.provider
    }

    fn verify_xades(&self, document: &Document) -> XmlResult<XadesVerificationReport> {
        match self.config.profile() {
            XadesProfile::Bes => verify_xades_bes_enveloped(document, &self.provider, &self.config),
            XadesProfile::Epes(_) => {
                verify_xades_epes_enveloped(document, &self.provider, &self.config)
            }
            XadesProfile::BaselineB { .. } => {
                verify_xades_baseline_b_enveloped(document, &self.provider, &self.config)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::parser::parse_str;
    use crate::signature::{
        DeterministicSigningProvider, DeterministicTimestampAuthority, SignatureValidationLevel,
        SignatureValidationProfile, StaticValidationDataProvider, XadesArchiveConfig,
        XadesTimestampConfig, XadesValidationDataConfig,
    };
    use crate::writer::to_string_compact;

    use super::*;

    fn document() -> XmlResult<Document> {
        parse_str(r#"<Root Id="doc-1"><Item>value</Item></Root>"#)
    }

    fn provider() -> DeterministicSigningProvider {
        DeterministicSigningProvider::new(b"cert".to_vec(), b"secret".to_vec())
    }

    #[test]
    fn signer_options_add_long_term_unsigned_properties() -> XmlResult<()> {
        let timestamp_authority = DeterministicTimestampAuthority::new(b"tsa");
        let validation_data = StaticValidationDataProvider::new()
            .with_certificate(b"cert".to_vec())
            .with_ocsp(b"ocsp".to_vec());
        let final_profile = SignatureValidationProfile::new()
            .with_level(SignatureValidationLevel::XadesLta)
            .with_xades_config(XadesConfig::new());
        let options = XadesSigningOptions::new()
            .with_timestamp(&timestamp_authority, XadesTimestampConfig::new())
            .with_validation_data(&validation_data, XadesValidationDataConfig::new())
            .with_archive_timestamp(&timestamp_authority, XadesArchiveConfig::new())
            .with_final_validation_profile(final_profile);

        let signed =
            XadesSigner::new(provider()).sign_document_with_options(&document()?, &options)?;
        let xml = to_string_compact(signed.document())?;
        let report = signed.report();

        assert!(report.initial_xades.valid);
        assert!(report.final_xades.valid);
        assert!(report
            .timestamp
            .as_ref()
            .is_some_and(|report| report.token_valid));
        assert!(report
            .validation_data
            .as_ref()
            .is_some_and(|report| report.required_material_present()));
        assert!(report
            .archive
            .as_ref()
            .is_some_and(|report| report.preservation_ready));
        assert!(report
            .final_validation_profile
            .as_ref()
            .is_some_and(|report| report.valid));
        assert!(xml.contains("<xades:SignatureTimeStamp>"));
        assert!(xml.contains("<xades:CertificateValues>"));
        assert!(xml.contains("<xades:RevocationValues>"));
        assert!(xml.contains("<xades:ArchiveTimeStamp>"));

        Ok(())
    }

    #[test]
    fn signer_options_can_validate_initial_signature_before_unsigned_steps() -> XmlResult<()> {
        let initial_profile = SignatureValidationProfile::new()
            .with_level(SignatureValidationLevel::XadesBes)
            .with_xades_config(XadesConfig::new());
        let options = XadesSigningOptions::new().with_initial_validation_profile(initial_profile);

        let signed =
            XadesSigner::new(provider()).sign_document_with_options(&document()?, &options)?;
        let report = signed.report();

        assert!(report.initial_xades.valid);
        assert!(report.final_xades.valid);
        assert!(report.timestamp.is_none());
        assert!(report.validation_data.is_none());
        assert!(report.archive.is_none());
        assert!(report
            .initial_validation_profile
            .as_ref()
            .is_some_and(|report| report.valid));

        Ok(())
    }
}