use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::DocumentId;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimestampRequest {
pub version: u32,
pub message_imprint: MessageImprint,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
#[serde(default)]
pub cert_req: bool,
}
impl TimestampRequest {
#[must_use]
pub fn for_document(document_id: &DocumentId) -> Self {
Self {
version: 1,
message_imprint: MessageImprint {
hash_algorithm: document_id.algorithm().as_str().to_string(),
hashed_message: document_id.hex_digest().clone(),
},
nonce: None,
cert_req: true,
}
}
#[must_use]
pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
self.nonce = Some(nonce.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageImprint {
pub hash_algorithm: String,
pub hashed_message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimestampResponse {
pub status: TimestampStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token: Option<TimestampToken>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimestampStatus {
pub status: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status_string: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fail_info: Option<String>,
}
impl TimestampStatus {
#[must_use]
pub fn is_granted(&self) -> bool {
self.status == 0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimestampToken {
pub version: u32,
pub policy: String,
pub message_imprint: MessageImprint,
pub serial_number: String,
pub gen_time: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accuracy: Option<TimestampAccuracy>,
#[serde(default)]
pub ordering: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tsa: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_token: Option<String>,
}
impl TimestampToken {
#[must_use]
pub fn matches_document(&self, document_id: &DocumentId) -> bool {
self.message_imprint.hashed_message == document_id.hex_digest()
}
#[must_use]
pub fn timestamp(&self) -> DateTime<Utc> {
self.gen_time
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimestampAccuracy {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seconds: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub millis: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub micros: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{HashAlgorithm, Hasher};
#[test]
fn test_timestamp_request_creation() {
let doc_id = Hasher::hash(HashAlgorithm::Sha256, b"test document");
let request = TimestampRequest::for_document(&doc_id);
assert_eq!(request.version, 1);
assert!(request.cert_req);
assert_eq!(request.message_imprint.hashed_message, doc_id.hex_digest());
}
#[test]
fn test_timestamp_request_with_nonce() {
let doc_id = Hasher::hash(HashAlgorithm::Sha256, b"test");
let request = TimestampRequest::for_document(&doc_id).with_nonce("abc123");
assert_eq!(request.nonce, Some("abc123".to_string()));
}
#[test]
fn test_timestamp_status_granted() {
let status = TimestampStatus {
status: 0,
status_string: Some("Granted".to_string()),
fail_info: None,
};
assert!(status.is_granted());
}
#[test]
fn test_timestamp_status_rejected() {
let status = TimestampStatus {
status: 2,
status_string: Some("Rejection".to_string()),
fail_info: Some("Bad algorithm".to_string()),
};
assert!(!status.is_granted());
}
#[test]
fn test_timestamp_token_matches() {
let doc_id = Hasher::hash(HashAlgorithm::Sha256, b"document");
let token = TimestampToken {
version: 1,
policy: "1.2.3.4".to_string(),
message_imprint: MessageImprint {
hash_algorithm: "SHA-256".to_string(),
hashed_message: doc_id.hex_digest(),
},
serial_number: "12345".to_string(),
gen_time: Utc::now(),
accuracy: None,
ordering: false,
nonce: None,
tsa: Some("Example TSA".to_string()),
signature: None,
raw_token: None,
};
assert!(token.matches_document(&doc_id));
}
#[test]
fn test_timestamp_serialization() {
let doc_id = Hasher::hash(HashAlgorithm::Sha256, b"test");
let request = TimestampRequest::for_document(&doc_id);
let json = serde_json::to_string_pretty(&request).unwrap();
assert!(json.contains("\"version\": 1"));
assert!(json.contains("\"certReq\": true"));
let deserialized: TimestampRequest = serde_json::from_str(&json).unwrap();
assert_eq!(
deserialized.message_imprint.hashed_message,
request.message_imprint.hashed_message
);
}
}