Skip to main content

xdoc/signature/
timestamp.rs

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/// Request sent to a timestamp authority abstraction.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct TimestampRequest {
16    pub digest_algorithm: DigestAlgorithm,
17    pub message_imprint: Vec<u8>,
18}
19
20/// Opaque timestamp token bytes.
21#[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
34/// Supplies timestamp tokens for XAdES unsigned properties.
35///
36/// Implementations may call a real TSA, HSM, KMS, or local service, but the
37/// engine does not include any network client by default.
38pub 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/// Deterministic timestamp authority for tests and fixtures.
47///
48/// This is not an RFC 3161 implementation. It creates stable opaque bytes so
49/// the XAdES-T structure and message-imprint flow can be tested without
50/// enabling network access.
51#[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/// Configuration for adding and validating XAdES signature timestamps.
79#[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/// Result of validating a XAdES SignatureTimeStamp.
119#[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
127/// Adds `xades:SignatureTimeStamp` as an unsigned property to an existing XAdES signature.
128pub 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(&timestamped)?;
140    let signature_value = required_child(&timestamped, signature, "SignatureValue")?;
141    let message_imprint = signature_value_message_imprint(&timestamped, 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(&timestamped, signature)?;
148    let unsigned_signature_properties =
149        ensure_unsigned_signature_properties(&mut timestamped, qualifying_properties)?;
150
151    if optional_xades_child(
152        &timestamped,
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
188/// Validates the presence, structure, and opaque token of a XAdES SignatureTimeStamp.
189pub 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            &timestamp_client(),
366            &XadesTimestampConfig::new(),
367        )?;
368        let xml = to_string_compact(&timestamped)?;
369        let report = verify_signature_timestamp(
370            &timestamped,
371            &timestamp_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(&timestamped, &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            &timestamp_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            &timestamp_client(),
409            &XadesTimestampConfig::new(),
410        )?;
411        let error = add_signature_timestamp(
412            &timestamped,
413            &timestamp_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            &timestamp_client(),
428            &XadesTimestampConfig::new(),
429        )?;
430        let xml = to_string_compact(&timestamped)?
431            .replace("<ds:SignatureValue>", "<ds:SignatureValue>tampered");
432        let tampered = parse_str(&xml)?;
433        let report = verify_signature_timestamp(
434            &tampered,
435            &timestamp_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}