1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::DocumentId;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct TimestampRequest {
15 pub version: u32,
17
18 pub message_imprint: MessageImprint,
20
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub nonce: Option<String>,
24
25 #[serde(default)]
27 pub cert_req: bool,
28}
29
30impl TimestampRequest {
31 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct MessageImprint {
57 pub hash_algorithm: String,
59
60 pub hashed_message: String,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub struct TimestampResponse {
68 pub status: TimestampStatus,
70
71 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub token: Option<TimestampToken>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct TimestampStatus {
80 pub status: u32,
82
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub status_string: Option<String>,
86
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub fail_info: Option<String>,
90}
91
92impl TimestampStatus {
93 #[must_use]
95 pub fn is_granted(&self) -> bool {
96 self.status == 0
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct TimestampToken {
104 pub version: u32,
106
107 pub policy: String,
109
110 pub message_imprint: MessageImprint,
112
113 pub serial_number: String,
115
116 pub gen_time: DateTime<Utc>,
118
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub accuracy: Option<TimestampAccuracy>,
122
123 #[serde(default)]
125 pub ordering: bool,
126
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub nonce: Option<String>,
130
131 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub tsa: Option<String>,
134
135 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub signature: Option<String>,
138
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub raw_token: Option<String>,
142}
143
144impl TimestampToken {
145 #[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 #[must_use]
156 pub fn timestamp(&self) -> DateTime<Utc> {
157 self.gen_time
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct TimestampAccuracy {
165 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub seconds: Option<u32>,
168
169 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub millis: Option<u32>,
172
173 #[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}