Skip to main content

xdoc/signature/
validation_data.rs

1use std::collections::BTreeSet;
2use std::fs;
3use std::path::Path;
4
5use crate::core::{Document, ErrorKind, NodeId, NodeKind, QName, XmlError, XmlResult};
6
7use super::xmldsig::{element_children, find_signature, required_child};
8use super::{decode_standard_base64, encode_standard_base64, XADES_NAMESPACE_URI};
9
10/// Supplies validation material for XAdES long-term profiles.
11///
12/// Implementations may load certificates, OCSP responses, or CRLs from an
13/// external source, but the engine does not perform network access by default.
14pub trait XadesValidationDataProvider {
15    fn certificate_values(&self) -> XmlResult<Vec<Vec<u8>>>;
16
17    fn ocsp_values(&self) -> XmlResult<Vec<Vec<u8>>> {
18        Ok(Vec::new())
19    }
20
21    fn crl_values(&self) -> XmlResult<Vec<Vec<u8>>> {
22        Ok(Vec::new())
23    }
24}
25
26#[derive(Debug, Clone, Default, PartialEq, Eq)]
27pub struct StaticValidationDataProvider {
28    certificates: Vec<Vec<u8>>,
29    ocsp: Vec<Vec<u8>>,
30    crls: Vec<Vec<u8>>,
31}
32
33impl StaticValidationDataProvider {
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    pub fn with_certificate(mut self, certificate: impl Into<Vec<u8>>) -> Self {
39        self.certificates.push(certificate.into());
40        self
41    }
42
43    pub fn with_ocsp(mut self, ocsp: impl Into<Vec<u8>>) -> Self {
44        self.ocsp.push(ocsp.into());
45        self
46    }
47
48    pub fn with_crl(mut self, crl: impl Into<Vec<u8>>) -> Self {
49        self.crls.push(crl.into());
50        self
51    }
52
53    pub fn with_ocsp_file(self, path: impl AsRef<Path>) -> XmlResult<Self> {
54        Ok(self.with_ocsp(read_validation_file(path, "cannot read OCSP file")?))
55    }
56
57    pub fn with_ocsp_files<I, P>(mut self, paths: I) -> XmlResult<Self>
58    where
59        I: IntoIterator<Item = P>,
60        P: AsRef<Path>,
61    {
62        for path in paths {
63            self = self.with_ocsp_file(path)?;
64        }
65        Ok(self)
66    }
67
68    pub fn with_crl_file(self, path: impl AsRef<Path>) -> XmlResult<Self> {
69        Ok(self.with_crl(read_validation_file(path, "cannot read CRL file")?))
70    }
71
72    pub fn with_crl_files<I, P>(mut self, paths: I) -> XmlResult<Self>
73    where
74        I: IntoIterator<Item = P>,
75        P: AsRef<Path>,
76    {
77        for path in paths {
78            self = self.with_crl_file(path)?;
79        }
80        Ok(self)
81    }
82}
83
84impl XadesValidationDataProvider for StaticValidationDataProvider {
85    fn certificate_values(&self) -> XmlResult<Vec<Vec<u8>>> {
86        Ok(self.certificates.clone())
87    }
88
89    fn ocsp_values(&self) -> XmlResult<Vec<Vec<u8>>> {
90        Ok(self.ocsp.clone())
91    }
92
93    fn crl_values(&self) -> XmlResult<Vec<Vec<u8>>> {
94        Ok(self.crls.clone())
95    }
96}
97
98/// Configuration for XAdES validation data unsigned properties.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct XadesValidationDataConfig {
101    require_certificate_values: bool,
102    require_revocation_values: bool,
103}
104
105impl XadesValidationDataConfig {
106    pub fn new() -> Self {
107        Self::default()
108    }
109
110    pub fn require_certificate_values(mut self, required: bool) -> Self {
111        self.require_certificate_values = required;
112        self
113    }
114
115    pub fn require_revocation_values(mut self, required: bool) -> Self {
116        self.require_revocation_values = required;
117        self
118    }
119
120    pub fn certificate_values_required(&self) -> bool {
121        self.require_certificate_values
122    }
123
124    pub fn revocation_values_required(&self) -> bool {
125        self.require_revocation_values
126    }
127}
128
129impl Default for XadesValidationDataConfig {
130    fn default() -> Self {
131        Self {
132            require_certificate_values: true,
133            require_revocation_values: true,
134        }
135    }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum XadesValidationDataKind {
140    CertificateValues,
141    RevocationValues,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct XadesValidationDataReport {
146    pub certificate_values_present: bool,
147    pub revocation_values_present: bool,
148    pub certificate_count: usize,
149    pub ocsp_count: usize,
150    pub crl_count: usize,
151    pub missing: Vec<XadesValidationDataKind>,
152}
153
154impl XadesValidationDataReport {
155    pub fn required_material_present(&self) -> bool {
156        self.missing.is_empty()
157    }
158}
159
160/// Adds XAdES validation data as unsigned signature properties.
161pub fn add_xades_validation_data<P>(
162    document: &Document,
163    provider: &P,
164    config: &XadesValidationDataConfig,
165) -> XmlResult<Document>
166where
167    P: XadesValidationDataProvider + ?Sized,
168{
169    let mut enriched = document.clone();
170    let signature = find_signature(&enriched)?;
171    let qualifying_properties = find_qualifying_properties(&enriched, signature)?;
172    let unsigned_signature_properties =
173        ensure_unsigned_signature_properties(&mut enriched, qualifying_properties)?;
174
175    if optional_xades_child(
176        &enriched,
177        unsigned_signature_properties,
178        "CertificateValues",
179    )?
180    .is_some()
181        || optional_xades_child(&enriched, unsigned_signature_properties, "RevocationValues")?
182            .is_some()
183    {
184        return Err(XmlError::new(
185            ErrorKind::Signature,
186            "XAdES validation data already exists",
187        ));
188    }
189
190    let certificates = unique_values(provider.certificate_values()?);
191    let ocsp = unique_values(provider.ocsp_values()?);
192    let crls = unique_values(provider.crl_values()?);
193    if config.require_certificate_values && certificates.is_empty() {
194        return Err(XmlError::new(
195            ErrorKind::Signature,
196            "required XAdES CertificateValues material is missing",
197        ));
198    }
199    if config.require_revocation_values && ocsp.is_empty() && crls.is_empty() {
200        return Err(XmlError::new(
201            ErrorKind::Signature,
202            "required XAdES RevocationValues material is missing",
203        ));
204    }
205
206    if !certificates.is_empty() {
207        add_certificate_values(&mut enriched, unsigned_signature_properties, &certificates)?;
208    }
209    if !ocsp.is_empty() || !crls.is_empty() {
210        add_revocation_values(&mut enriched, unsigned_signature_properties, &ocsp, &crls)?;
211    }
212
213    Ok(enriched)
214}
215
216/// Reports whether XAdES validation data unsigned properties are present.
217pub fn verify_xades_validation_data(
218    document: &Document,
219    config: &XadesValidationDataConfig,
220) -> XmlResult<XadesValidationDataReport> {
221    let signature = find_signature(document)?;
222    let qualifying_properties = find_qualifying_properties(document, signature)?;
223    let Some(unsigned_properties) =
224        optional_xades_child(document, qualifying_properties, "UnsignedProperties")?
225    else {
226        return Ok(missing_report(config));
227    };
228    let Some(unsigned_signature_properties) =
229        optional_xades_child(document, unsigned_properties, "UnsignedSignatureProperties")?
230    else {
231        return Ok(missing_report(config));
232    };
233
234    let certificate_values =
235        optional_xades_child(document, unsigned_signature_properties, "CertificateValues")?;
236    let revocation_values =
237        optional_xades_child(document, unsigned_signature_properties, "RevocationValues")?;
238    let certificate_count = match certificate_values {
239        Some(node) => count_xades_values(document, node, "EncapsulatedX509Certificate")?,
240        None => 0,
241    };
242    let (ocsp_count, crl_count) = match revocation_values {
243        Some(node) => (
244            count_xades_values(document, node, "EncapsulatedOCSPValue")?,
245            count_xades_values(document, node, "EncapsulatedCRLValue")?,
246        ),
247        None => (0, 0),
248    };
249
250    let mut missing = Vec::new();
251    if config.require_certificate_values && certificate_count == 0 {
252        missing.push(XadesValidationDataKind::CertificateValues);
253    }
254    if config.require_revocation_values && ocsp_count == 0 && crl_count == 0 {
255        missing.push(XadesValidationDataKind::RevocationValues);
256    }
257
258    Ok(XadesValidationDataReport {
259        certificate_values_present: certificate_values.is_some(),
260        revocation_values_present: revocation_values.is_some(),
261        certificate_count,
262        ocsp_count,
263        crl_count,
264        missing,
265    })
266}
267
268fn missing_report(config: &XadesValidationDataConfig) -> XadesValidationDataReport {
269    let mut missing = Vec::new();
270    if config.require_certificate_values {
271        missing.push(XadesValidationDataKind::CertificateValues);
272    }
273    if config.require_revocation_values {
274        missing.push(XadesValidationDataKind::RevocationValues);
275    }
276    XadesValidationDataReport {
277        certificate_values_present: false,
278        revocation_values_present: false,
279        certificate_count: 0,
280        ocsp_count: 0,
281        crl_count: 0,
282        missing,
283    }
284}
285
286fn unique_values(values: Vec<Vec<u8>>) -> Vec<Vec<u8>> {
287    let mut seen = BTreeSet::new();
288    values
289        .into_iter()
290        .filter(|value| seen.insert(value.clone()))
291        .collect()
292}
293
294fn read_validation_file(path: impl AsRef<Path>, context: &str) -> XmlResult<Vec<u8>> {
295    let path = path.as_ref();
296    fs::read(path).map_err(|error| {
297        XmlError::new(
298            ErrorKind::Signature,
299            format!("{context} `{}`: {error}", path.display()),
300        )
301    })
302}
303
304fn add_certificate_values(
305    document: &mut Document,
306    parent: NodeId,
307    certificates: &[Vec<u8>],
308) -> XmlResult<()> {
309    let certificate_values = document.add_element(
310        parent,
311        QName::qualified("xades", "CertificateValues", XADES_NAMESPACE_URI)?,
312    )?;
313    for certificate in certificates {
314        add_text_element(
315            document,
316            certificate_values,
317            "EncapsulatedX509Certificate",
318            encode_standard_base64(certificate),
319        )?;
320    }
321    Ok(())
322}
323
324fn add_revocation_values(
325    document: &mut Document,
326    parent: NodeId,
327    ocsp: &[Vec<u8>],
328    crls: &[Vec<u8>],
329) -> XmlResult<()> {
330    let revocation_values = document.add_element(
331        parent,
332        QName::qualified("xades", "RevocationValues", XADES_NAMESPACE_URI)?,
333    )?;
334    if !ocsp.is_empty() {
335        let ocsp_values = document.add_element(
336            revocation_values,
337            QName::qualified("xades", "OCSPValues", XADES_NAMESPACE_URI)?,
338        )?;
339        for value in ocsp {
340            add_text_element(
341                document,
342                ocsp_values,
343                "EncapsulatedOCSPValue",
344                encode_standard_base64(value),
345            )?;
346        }
347    }
348    if !crls.is_empty() {
349        let crl_values = document.add_element(
350            revocation_values,
351            QName::qualified("xades", "CRLValues", XADES_NAMESPACE_URI)?,
352        )?;
353        for value in crls {
354            add_text_element(
355                document,
356                crl_values,
357                "EncapsulatedCRLValue",
358                encode_standard_base64(value),
359            )?;
360        }
361    }
362    Ok(())
363}
364
365fn add_text_element(
366    document: &mut Document,
367    parent: NodeId,
368    local: &str,
369    value: impl Into<String>,
370) -> XmlResult<NodeId> {
371    let node = document.add_element(
372        parent,
373        QName::qualified("xades", local, XADES_NAMESPACE_URI)?,
374    )?;
375    document.add_text(node, value)?;
376    Ok(node)
377}
378
379fn count_xades_values(document: &Document, parent: NodeId, local: &str) -> XmlResult<usize> {
380    let mut count = 0;
381    count_xades_values_recursive(document, parent, local, &mut count)?;
382    Ok(count)
383}
384
385fn count_xades_values_recursive(
386    document: &Document,
387    parent: NodeId,
388    local: &str,
389    count: &mut usize,
390) -> XmlResult<()> {
391    for child in element_children(document, parent)? {
392        if is_xades_element(document, child, local) {
393            let text = text_content(document, child)?;
394            decode_standard_base64(&text)?;
395            *count += 1;
396        }
397        count_xades_values_recursive(document, child, local, count)?;
398    }
399    Ok(())
400}
401
402fn text_content(document: &Document, parent: NodeId) -> XmlResult<String> {
403    let mut text = String::new();
404    for child in document.children(parent)? {
405        if let NodeKind::Text(value) = document.node(*child)?.kind() {
406            text.push_str(value);
407        }
408    }
409    if text.is_empty() {
410        return Err(XmlError::new(
411            ErrorKind::Signature,
412            "XAdES validation data value must contain text",
413        ));
414    }
415    Ok(text)
416}
417
418fn find_qualifying_properties(document: &Document, signature: NodeId) -> XmlResult<NodeId> {
419    let object = required_child(document, signature, "Object")?;
420    element_children(document, object)?
421        .into_iter()
422        .find(|child| is_xades_element(document, *child, "QualifyingProperties"))
423        .ok_or_else(|| {
424            XmlError::new(
425                ErrorKind::Signature,
426                "missing required XAdES QualifyingProperties",
427            )
428        })
429}
430
431fn ensure_unsigned_signature_properties(
432    document: &mut Document,
433    qualifying_properties: NodeId,
434) -> XmlResult<NodeId> {
435    let unsigned_properties =
436        match optional_xades_child(document, qualifying_properties, "UnsignedProperties")? {
437            Some(node) => node,
438            None => document.add_element(
439                qualifying_properties,
440                QName::qualified("xades", "UnsignedProperties", XADES_NAMESPACE_URI)?,
441            )?,
442        };
443
444    match optional_xades_child(document, unsigned_properties, "UnsignedSignatureProperties")? {
445        Some(node) => Ok(node),
446        None => document.add_element(
447            unsigned_properties,
448            QName::qualified("xades", "UnsignedSignatureProperties", XADES_NAMESPACE_URI)?,
449        ),
450    }
451}
452
453fn optional_xades_child(
454    document: &Document,
455    parent: NodeId,
456    local: &str,
457) -> XmlResult<Option<NodeId>> {
458    Ok(element_children(document, parent)?
459        .into_iter()
460        .find(|child| is_xades_element(document, *child, local)))
461}
462
463fn is_xades_element(document: &Document, node: NodeId, local: &str) -> bool {
464    matches!(
465        document.node(node).map(|node| node.kind()),
466        Ok(NodeKind::Element(element))
467            if element.name().namespace_uri().map(|uri| uri.as_str()) == Some(XADES_NAMESPACE_URI)
468                && element.name().local() == local
469    )
470}
471
472#[cfg(test)]
473mod tests {
474    use std::fs;
475
476    use crate::parser::parse_str;
477    use crate::signature::{sign_xades_bes_enveloped, DeterministicSigningProvider, XadesConfig};
478    use crate::writer::to_string_compact;
479
480    use super::*;
481
482    fn provider() -> DeterministicSigningProvider {
483        DeterministicSigningProvider::new(b"test-cert".to_vec(), b"test-secret".to_vec())
484    }
485
486    fn signed_document() -> XmlResult<Document> {
487        let document = parse_str(r#"<Root Id="doc-1"><Item>value</Item></Root>"#)?;
488        sign_xades_bes_enveloped(
489            &document,
490            &provider(),
491            &XadesConfig::new().with_signing_time("2026-06-11T12:00:00Z"),
492        )
493    }
494
495    fn temp_path(name: &str) -> std::path::PathBuf {
496        std::env::temp_dir().join(format!(
497            "xdoc-validation-data-{name}-{}",
498            std::process::id()
499        ))
500    }
501
502    #[test]
503    fn xades_validation_data_adds_certificate_and_revocation_values() -> XmlResult<()> {
504        let validation_provider = StaticValidationDataProvider::new()
505            .with_certificate(b"chain-cert")
506            .with_ocsp(b"ocsp-response")
507            .with_crl(b"crl-response");
508        let enriched = add_xades_validation_data(
509            &signed_document()?,
510            &validation_provider,
511            &XadesValidationDataConfig::new(),
512        )?;
513        let report = verify_xades_validation_data(&enriched, &XadesValidationDataConfig::new())?;
514        let xml = to_string_compact(&enriched)?;
515
516        assert!(report.required_material_present());
517        assert_eq!(report.certificate_count, 1);
518        assert_eq!(report.ocsp_count, 1);
519        assert_eq!(report.crl_count, 1);
520        assert!(xml.contains("<xades:CertificateValues>"));
521        assert!(xml.contains("<xades:RevocationValues>"));
522        assert!(xml.contains("<xades:EncapsulatedX509Certificate>"));
523        assert!(xml.contains("<xades:EncapsulatedOCSPValue>"));
524        assert!(xml.contains("<xades:EncapsulatedCRLValue>"));
525        Ok(())
526    }
527
528    #[test]
529    fn xades_validation_data_reports_missing_material() -> XmlResult<()> {
530        let report =
531            verify_xades_validation_data(&signed_document()?, &XadesValidationDataConfig::new())?;
532
533        assert!(!report.required_material_present());
534        assert_eq!(
535            report.missing,
536            vec![
537                XadesValidationDataKind::CertificateValues,
538                XadesValidationDataKind::RevocationValues
539            ]
540        );
541        Ok(())
542    }
543
544    #[test]
545    fn xades_validation_data_rejects_duplicate_insertion() -> XmlResult<()> {
546        let validation_provider = StaticValidationDataProvider::new()
547            .with_certificate(b"chain-cert")
548            .with_ocsp(b"ocsp-response");
549        let enriched = add_xades_validation_data(
550            &signed_document()?,
551            &validation_provider,
552            &XadesValidationDataConfig::new(),
553        )?;
554        let error = add_xades_validation_data(
555            &enriched,
556            &validation_provider,
557            &XadesValidationDataConfig::new(),
558        )
559        .expect_err("validation data must not be duplicated");
560
561        assert_eq!(error.kind(), &ErrorKind::Signature);
562        Ok(())
563    }
564
565    #[test]
566    fn xades_validation_data_deduplicates_provider_values() -> XmlResult<()> {
567        let validation_provider = StaticValidationDataProvider::new()
568            .with_certificate(b"chain-cert")
569            .with_certificate(b"chain-cert")
570            .with_ocsp(b"ocsp-response")
571            .with_ocsp(b"ocsp-response");
572        let enriched = add_xades_validation_data(
573            &signed_document()?,
574            &validation_provider,
575            &XadesValidationDataConfig::new(),
576        )?;
577        let report = verify_xades_validation_data(&enriched, &XadesValidationDataConfig::new())?;
578
579        assert_eq!(report.certificate_count, 1);
580        assert_eq!(report.ocsp_count, 1);
581        assert_eq!(report.crl_count, 0);
582        Ok(())
583    }
584
585    #[test]
586    fn static_validation_data_provider_can_read_ocsp_and_crl_files() -> XmlResult<()> {
587        let ocsp_path = temp_path("ocsp.der");
588        let crl_path = temp_path("crl.der");
589        fs::write(&ocsp_path, b"ocsp-file").expect("write ocsp fixture");
590        fs::write(&crl_path, b"crl-file").expect("write crl fixture");
591
592        let provider = StaticValidationDataProvider::new()
593            .with_ocsp_files([&ocsp_path])?
594            .with_crl_files([&crl_path])?;
595
596        assert_eq!(provider.ocsp_values()?, vec![b"ocsp-file".to_vec()]);
597        assert_eq!(provider.crl_values()?, vec![b"crl-file".to_vec()]);
598
599        let _ = fs::remove_file(ocsp_path);
600        let _ = fs::remove_file(crl_path);
601        Ok(())
602    }
603}