Skip to main content

cdx_core/provenance/
timestamp.rs

1//! RFC 3161 timestamp token support.
2//!
3//! This module provides types for timestamp anchoring, allowing
4//! documents to be cryptographically anchored to a specific point in time.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::DocumentId;
10
11/// A timestamp request to be sent to a Time Stamp Authority (TSA).
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct TimestampRequest {
15    /// Version of the request format.
16    pub version: u32,
17
18    /// Hash of the data to be timestamped.
19    pub message_imprint: MessageImprint,
20
21    /// Optional nonce for replay protection.
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub nonce: Option<String>,
24
25    /// Whether to include the TSA certificate in the response.
26    #[serde(default)]
27    pub cert_req: bool,
28}
29
30impl TimestampRequest {
31    /// Create a new timestamp request for a document.
32    #[must_use]
33    pub fn for_document(document_id: &DocumentId) -> Self {
34        Self {
35            version: 1,
36            message_imprint: MessageImprint {
37                hash_algorithm: document_id.algorithm().as_str().to_string(),
38                hashed_message: document_id.hex_digest().clone(),
39            },
40            nonce: None,
41            cert_req: true,
42        }
43    }
44
45    /// Set a nonce for replay protection.
46    #[must_use]
47    pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
48        self.nonce = Some(nonce.into());
49        self
50    }
51}
52
53/// Hash of the message being timestamped.
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct MessageImprint {
57    /// Hash algorithm OID or name.
58    pub hash_algorithm: String,
59
60    /// Hex-encoded hash value.
61    pub hashed_message: String,
62}
63
64/// Response from a Time Stamp Authority.
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub struct TimestampResponse {
68    /// Status of the request.
69    pub status: TimestampStatus,
70
71    /// The timestamp token (if successful).
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub token: Option<TimestampToken>,
74}
75
76/// Status of a timestamp request.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct TimestampStatus {
80    /// Status code (0 = granted).
81    pub status: u32,
82
83    /// Status string.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub status_string: Option<String>,
86
87    /// Failure info if request failed.
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub fail_info: Option<String>,
90}
91
92impl TimestampStatus {
93    /// Check if the request was granted.
94    #[must_use]
95    pub fn is_granted(&self) -> bool {
96        self.status == 0
97    }
98}
99
100/// A timestamp token from a TSA.
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct TimestampToken {
104    /// Version of the token format.
105    pub version: u32,
106
107    /// Policy OID under which the token was issued.
108    pub policy: String,
109
110    /// The message imprint that was timestamped.
111    pub message_imprint: MessageImprint,
112
113    /// Serial number of this token.
114    pub serial_number: String,
115
116    /// Time when the timestamp was generated.
117    pub gen_time: DateTime<Utc>,
118
119    /// Accuracy of the timestamp (optional).
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub accuracy: Option<TimestampAccuracy>,
122
123    /// Ordering flag.
124    #[serde(default)]
125    pub ordering: bool,
126
127    /// Nonce (if provided in request).
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub nonce: Option<String>,
130
131    /// TSA name.
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub tsa: Option<String>,
134
135    /// Base64-encoded signature over the token.
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub signature: Option<String>,
138
139    /// Base64-encoded DER token (for verification).
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub raw_token: Option<String>,
142}
143
144impl TimestampToken {
145    /// Verify that this token matches a document.
146    ///
147    /// Note: This only checks the message imprint, not the cryptographic signature.
148    /// Full verification requires the TSA's certificate chain.
149    #[must_use]
150    pub fn matches_document(&self, document_id: &DocumentId) -> bool {
151        self.message_imprint.hashed_message == document_id.hex_digest()
152    }
153
154    /// Get the timestamp time.
155    #[must_use]
156    pub fn timestamp(&self) -> DateTime<Utc> {
157        self.gen_time
158    }
159}
160
161/// Accuracy of a timestamp.
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct TimestampAccuracy {
165    /// Seconds component.
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub seconds: Option<u32>,
168
169    /// Milliseconds component.
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub millis: Option<u32>,
172
173    /// Microseconds component.
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub micros: Option<u32>,
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::{HashAlgorithm, Hasher};
182
183    #[test]
184    fn test_timestamp_request_creation() {
185        let doc_id = Hasher::hash(HashAlgorithm::Sha256, b"test document");
186        let request = TimestampRequest::for_document(&doc_id);
187
188        assert_eq!(request.version, 1);
189        assert!(request.cert_req);
190        assert_eq!(request.message_imprint.hashed_message, doc_id.hex_digest());
191    }
192
193    #[test]
194    fn test_timestamp_request_with_nonce() {
195        let doc_id = Hasher::hash(HashAlgorithm::Sha256, b"test");
196        let request = TimestampRequest::for_document(&doc_id).with_nonce("abc123");
197
198        assert_eq!(request.nonce, Some("abc123".to_string()));
199    }
200
201    #[test]
202    fn test_timestamp_status_granted() {
203        let status = TimestampStatus {
204            status: 0,
205            status_string: Some("Granted".to_string()),
206            fail_info: None,
207        };
208
209        assert!(status.is_granted());
210    }
211
212    #[test]
213    fn test_timestamp_status_rejected() {
214        let status = TimestampStatus {
215            status: 2,
216            status_string: Some("Rejection".to_string()),
217            fail_info: Some("Bad algorithm".to_string()),
218        };
219
220        assert!(!status.is_granted());
221    }
222
223    #[test]
224    fn test_timestamp_token_matches() {
225        let doc_id = Hasher::hash(HashAlgorithm::Sha256, b"document");
226
227        let token = TimestampToken {
228            version: 1,
229            policy: "1.2.3.4".to_string(),
230            message_imprint: MessageImprint {
231                hash_algorithm: "SHA-256".to_string(),
232                hashed_message: doc_id.hex_digest(),
233            },
234            serial_number: "12345".to_string(),
235            gen_time: Utc::now(),
236            accuracy: None,
237            ordering: false,
238            nonce: None,
239            tsa: Some("Example TSA".to_string()),
240            signature: None,
241            raw_token: None,
242        };
243
244        assert!(token.matches_document(&doc_id));
245    }
246
247    #[test]
248    fn test_timestamp_serialization() {
249        let doc_id = Hasher::hash(HashAlgorithm::Sha256, b"test");
250        let request = TimestampRequest::for_document(&doc_id);
251
252        let json = serde_json::to_string_pretty(&request).unwrap();
253        assert!(json.contains("\"version\": 1"));
254        assert!(json.contains("\"certReq\": true"));
255
256        let deserialized: TimestampRequest = serde_json::from_str(&json).unwrap();
257        assert_eq!(
258            deserialized.message_imprint.hashed_message,
259            request.message_imprint.hashed_message
260        );
261    }
262}