1use sha2::{Digest, Sha256};
2
3use crate::core::{Attribute, Document, ErrorKind, NodeId, NodeKind, QName, XmlError, XmlResult};
4
5use super::xmldsig::{
6 element_children, find_signature, required_child, required_child_text, XMLDSIG_NAMESPACE_URI,
7};
8use super::{
9 canonicalize_node, decode_standard_base64, digest_bytes, encode_standard_base64,
10 CanonicalizationConfig, DigestAlgorithm, XADES_NAMESPACE_URI,
11};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct TimestampRequest {
16 pub digest_algorithm: DigestAlgorithm,
17 pub message_imprint: Vec<u8>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct TimestampToken {
23 pub encoded: Vec<u8>,
24}
25
26impl TimestampToken {
27 pub fn new(encoded: impl Into<Vec<u8>>) -> Self {
28 Self {
29 encoded: encoded.into(),
30 }
31 }
32}
33
34pub trait TimestampAuthorityClient {
39 fn timestamp(&self, request: &TimestampRequest) -> XmlResult<TimestampToken>;
40
41 fn verify(&self, request: &TimestampRequest, token: &TimestampToken) -> XmlResult<bool> {
42 Ok(self.timestamp(request)?.encoded == token.encoded)
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct DeterministicTimestampAuthority {
53 secret: Vec<u8>,
54}
55
56impl DeterministicTimestampAuthority {
57 pub fn new(secret: impl Into<Vec<u8>>) -> Self {
58 Self {
59 secret: secret.into(),
60 }
61 }
62}
63
64impl TimestampAuthorityClient for DeterministicTimestampAuthority {
65 fn timestamp(&self, request: &TimestampRequest) -> XmlResult<TimestampToken> {
66 let mut hasher = Sha256::new();
67 hasher.update(b"xdoc-deterministic-timestamp");
68 hasher.update([0]);
69 hasher.update(request.digest_algorithm.uri().as_bytes());
70 hasher.update([0]);
71 hasher.update(&self.secret);
72 hasher.update([0]);
73 hasher.update(&request.message_imprint);
74 Ok(TimestampToken::new(hasher.finalize().to_vec()))
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct XadesTimestampConfig {
81 digest_algorithm: DigestAlgorithm,
82 canonicalization: CanonicalizationConfig,
83}
84
85impl XadesTimestampConfig {
86 pub fn new() -> Self {
87 Self::default()
88 }
89
90 pub fn with_digest_algorithm(mut self, digest_algorithm: DigestAlgorithm) -> Self {
91 self.digest_algorithm = digest_algorithm;
92 self
93 }
94
95 pub fn with_canonicalization(mut self, canonicalization: CanonicalizationConfig) -> Self {
96 self.canonicalization = canonicalization;
97 self
98 }
99
100 pub fn digest_algorithm(&self) -> DigestAlgorithm {
101 self.digest_algorithm
102 }
103
104 pub fn canonicalization(&self) -> &CanonicalizationConfig {
105 &self.canonicalization
106 }
107}
108
109impl Default for XadesTimestampConfig {
110 fn default() -> Self {
111 Self {
112 digest_algorithm: DigestAlgorithm::Sha256,
113 canonicalization: CanonicalizationConfig::default(),
114 }
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct TimestampValidationReport {
121 pub timestamp_present: bool,
122 pub structure_valid: bool,
123 pub token_valid: bool,
124 pub message_imprint: Vec<u8>,
125}
126
127pub fn add_signature_timestamp<C>(
129 document: &Document,
130 client: &C,
131 config: &XadesTimestampConfig,
132) -> XmlResult<Document>
133where
134 C: TimestampAuthorityClient + ?Sized,
135{
136 config.digest_algorithm.ensure_allowed_for_generation()?;
137
138 let mut timestamped = document.clone();
139 let signature = find_signature(×tamped)?;
140 let signature_value = required_child(×tamped, signature, "SignatureValue")?;
141 let message_imprint = signature_value_message_imprint(×tamped, signature_value, config)?;
142 let request = TimestampRequest {
143 digest_algorithm: config.digest_algorithm,
144 message_imprint,
145 };
146 let token = client.timestamp(&request)?;
147 let qualifying_properties = find_qualifying_properties(×tamped, signature)?;
148 let unsigned_signature_properties =
149 ensure_unsigned_signature_properties(&mut timestamped, qualifying_properties)?;
150
151 if optional_xades_child(
152 ×tamped,
153 unsigned_signature_properties,
154 "SignatureTimeStamp",
155 )?
156 .is_some()
157 {
158 return Err(XmlError::new(
159 ErrorKind::Signature,
160 "XAdES SignatureTimeStamp already exists",
161 ));
162 }
163
164 let signature_timestamp = timestamped.add_element(
165 unsigned_signature_properties,
166 QName::qualified("xades", "SignatureTimeStamp", XADES_NAMESPACE_URI)?,
167 )?;
168 let canonicalization = timestamped.add_element(
169 signature_timestamp,
170 QName::qualified("ds", "CanonicalizationMethod", XMLDSIG_NAMESPACE_URI)?,
171 )?;
172 timestamped.add_attribute(
173 canonicalization,
174 Attribute::new(
175 QName::new("Algorithm")?,
176 config.canonicalization.algorithm().uri(),
177 ),
178 )?;
179 let encoded = timestamped.add_element(
180 signature_timestamp,
181 QName::qualified("xades", "EncapsulatedTimeStamp", XADES_NAMESPACE_URI)?,
182 )?;
183 timestamped.add_text(encoded, encode_standard_base64(&token.encoded))?;
184
185 Ok(timestamped)
186}
187
188pub fn verify_signature_timestamp<C>(
190 document: &Document,
191 client: &C,
192 config: &XadesTimestampConfig,
193) -> XmlResult<TimestampValidationReport>
194where
195 C: TimestampAuthorityClient + ?Sized,
196{
197 config.digest_algorithm.ensure_allowed_for_generation()?;
198
199 let signature = find_signature(document)?;
200 let signature_value = required_child(document, signature, "SignatureValue")?;
201 let message_imprint = signature_value_message_imprint(document, signature_value, config)?;
202 let Some(signature_timestamp) = find_signature_timestamp(document, signature)? else {
203 return Ok(TimestampValidationReport {
204 timestamp_present: false,
205 structure_valid: false,
206 token_valid: false,
207 message_imprint,
208 });
209 };
210
211 let canonicalization_method =
212 required_child(document, signature_timestamp, "CanonicalizationMethod")?;
213 let canonicalization_algorithm =
214 optional_attribute(document, canonicalization_method, "Algorithm")?.ok_or_else(|| {
215 XmlError::new(
216 ErrorKind::Signature,
217 "XAdES SignatureTimeStamp CanonicalizationMethod requires Algorithm",
218 )
219 })?;
220 let structure_valid = canonicalization_algorithm == config.canonicalization.algorithm().uri();
221 let token_text = required_child_text(document, signature_timestamp, "EncapsulatedTimeStamp")?;
222 let token = TimestampToken::new(decode_standard_base64(&token_text)?);
223 let request = TimestampRequest {
224 digest_algorithm: config.digest_algorithm,
225 message_imprint: message_imprint.clone(),
226 };
227 let token_valid = structure_valid && client.verify(&request, &token)?;
228
229 Ok(TimestampValidationReport {
230 timestamp_present: true,
231 structure_valid,
232 token_valid,
233 message_imprint,
234 })
235}
236
237fn signature_value_message_imprint(
238 document: &Document,
239 signature_value: NodeId,
240 config: &XadesTimestampConfig,
241) -> XmlResult<Vec<u8>> {
242 let canonicalized = canonicalize_node(document, signature_value, &config.canonicalization)?;
243 digest_bytes(config.digest_algorithm, canonicalized)
244}
245
246fn find_signature_timestamp(document: &Document, signature: NodeId) -> XmlResult<Option<NodeId>> {
247 let qualifying_properties = find_qualifying_properties(document, signature)?;
248 let Some(unsigned_properties) =
249 optional_xades_child(document, qualifying_properties, "UnsignedProperties")?
250 else {
251 return Ok(None);
252 };
253 let Some(unsigned_signature_properties) =
254 optional_xades_child(document, unsigned_properties, "UnsignedSignatureProperties")?
255 else {
256 return Ok(None);
257 };
258 optional_xades_child(
259 document,
260 unsigned_signature_properties,
261 "SignatureTimeStamp",
262 )
263}
264
265fn ensure_unsigned_signature_properties(
266 document: &mut Document,
267 qualifying_properties: NodeId,
268) -> XmlResult<NodeId> {
269 let unsigned_properties =
270 match optional_xades_child(document, qualifying_properties, "UnsignedProperties")? {
271 Some(node) => node,
272 None => document.add_element(
273 qualifying_properties,
274 QName::qualified("xades", "UnsignedProperties", XADES_NAMESPACE_URI)?,
275 )?,
276 };
277
278 match optional_xades_child(document, unsigned_properties, "UnsignedSignatureProperties")? {
279 Some(node) => Ok(node),
280 None => document.add_element(
281 unsigned_properties,
282 QName::qualified("xades", "UnsignedSignatureProperties", XADES_NAMESPACE_URI)?,
283 ),
284 }
285}
286
287fn find_qualifying_properties(document: &Document, signature: NodeId) -> XmlResult<NodeId> {
288 let object = required_child(document, signature, "Object")?;
289 element_children(document, object)?
290 .into_iter()
291 .find(|child| is_xades_element(document, *child, "QualifyingProperties"))
292 .ok_or_else(|| {
293 XmlError::new(
294 ErrorKind::Signature,
295 "missing required XAdES QualifyingProperties",
296 )
297 })
298}
299
300fn optional_xades_child(
301 document: &Document,
302 parent: NodeId,
303 local: &str,
304) -> XmlResult<Option<NodeId>> {
305 Ok(element_children(document, parent)?
306 .into_iter()
307 .find(|child| is_xades_element(document, *child, local)))
308}
309
310fn optional_attribute(document: &Document, node: NodeId, local: &str) -> XmlResult<Option<String>> {
311 let NodeKind::Element(element) = document.node(node)?.kind() else {
312 return Ok(None);
313 };
314 Ok(element
315 .attributes()
316 .iter()
317 .find(|attribute| attribute.name().local() == local)
318 .map(|attribute| attribute.value().to_owned()))
319}
320
321fn is_xades_element(document: &Document, node: NodeId, local: &str) -> bool {
322 matches!(
323 document.node(node).map(|node| node.kind()),
324 Ok(NodeKind::Element(element))
325 if element.name().namespace_uri().map(|uri| uri.as_str()) == Some(XADES_NAMESPACE_URI)
326 && element.name().local() == local
327 )
328}
329
330#[cfg(test)]
331mod tests {
332 use crate::parser::parse_str;
333 use crate::signature::{
334 sign_xades_bes_enveloped, verify_xades_bes_enveloped, DeterministicSigningProvider,
335 XadesConfig,
336 };
337 use crate::writer::to_string_compact;
338
339 use super::*;
340
341 fn signing_provider() -> DeterministicSigningProvider {
342 DeterministicSigningProvider::new(b"test-cert".to_vec(), b"test-secret".to_vec())
343 }
344
345 fn timestamp_client() -> DeterministicTimestampAuthority {
346 DeterministicTimestampAuthority::new(b"timestamp-secret".to_vec())
347 }
348
349 fn unsigned_document() -> XmlResult<Document> {
350 parse_str(r#"<Root Id="doc-1"><Item>value</Item></Root>"#)
351 }
352
353 fn signed_document() -> XmlResult<Document> {
354 sign_xades_bes_enveloped(
355 &unsigned_document()?,
356 &signing_provider(),
357 &XadesConfig::new().with_signing_time("2026-06-11T12:00:00Z"),
358 )
359 }
360
361 #[test]
362 fn xades_timestamp_adds_unsigned_signature_timestamp() -> XmlResult<()> {
363 let timestamped = add_signature_timestamp(
364 &signed_document()?,
365 ×tamp_client(),
366 &XadesTimestampConfig::new(),
367 )?;
368 let xml = to_string_compact(×tamped)?;
369 let report = verify_signature_timestamp(
370 ×tamped,
371 ×tamp_client(),
372 &XadesTimestampConfig::new(),
373 )?;
374
375 assert!(report.timestamp_present);
376 assert!(report.structure_valid);
377 assert!(report.token_valid);
378 assert!(xml.contains("<xades:UnsignedProperties>"));
379 assert!(xml.contains("<xades:UnsignedSignatureProperties>"));
380 assert!(xml.contains("<xades:SignatureTimeStamp>"));
381 assert!(xml.contains("<xades:EncapsulatedTimeStamp>"));
382 assert!(
383 verify_xades_bes_enveloped(×tamped, &signing_provider(), &XadesConfig::new())?
384 .valid
385 );
386 Ok(())
387 }
388
389 #[test]
390 fn xades_timestamp_report_marks_missing_timestamp() -> XmlResult<()> {
391 let report = verify_signature_timestamp(
392 &signed_document()?,
393 ×tamp_client(),
394 &XadesTimestampConfig::new(),
395 )?;
396
397 assert!(!report.timestamp_present);
398 assert!(!report.structure_valid);
399 assert!(!report.token_valid);
400 assert!(!report.message_imprint.is_empty());
401 Ok(())
402 }
403
404 #[test]
405 fn xades_timestamp_rejects_duplicate_timestamp() -> XmlResult<()> {
406 let timestamped = add_signature_timestamp(
407 &signed_document()?,
408 ×tamp_client(),
409 &XadesTimestampConfig::new(),
410 )?;
411 let error = add_signature_timestamp(
412 ×tamped,
413 ×tamp_client(),
414 &XadesTimestampConfig::new(),
415 )
416 .expect_err("duplicate timestamp must fail");
417
418 assert_eq!(error.kind(), &ErrorKind::Signature);
419 assert!(error.message().contains("already exists"));
420 Ok(())
421 }
422
423 #[test]
424 fn xades_timestamp_detects_tampered_signature_value() -> XmlResult<()> {
425 let timestamped = add_signature_timestamp(
426 &signed_document()?,
427 ×tamp_client(),
428 &XadesTimestampConfig::new(),
429 )?;
430 let xml = to_string_compact(×tamped)?
431 .replace("<ds:SignatureValue>", "<ds:SignatureValue>tampered");
432 let tampered = parse_str(&xml)?;
433 let report = verify_signature_timestamp(
434 &tampered,
435 ×tamp_client(),
436 &XadesTimestampConfig::new(),
437 )?;
438
439 assert!(report.timestamp_present);
440 assert!(report.structure_valid);
441 assert!(!report.token_valid);
442 Ok(())
443 }
444}