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
10pub 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#[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
160pub 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
216pub 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}