tap_node/validation/
timestamp_validator.rs

1//! Timestamp validation for TAP messages
2
3use super::{MessageValidator, ValidationResult};
4use async_trait::async_trait;
5use chrono::{DateTime, Duration, Utc};
6use tap_msg::didcomm::PlainMessage;
7
8/// Validator that checks message timestamps
9///
10/// This validator ensures that:
11/// - Messages are not too far in the future (prevents clock drift issues)
12/// - Messages have not expired
13/// - Timestamps are valid and parseable
14pub struct TimestampValidator {
15    max_future_drift_secs: i64,
16}
17
18impl TimestampValidator {
19    /// Create a new timestamp validator
20    ///
21    /// # Arguments
22    /// * `max_future_drift_secs` - Maximum allowed seconds a message can be from the future
23    pub fn new(max_future_drift_secs: i64) -> Self {
24        Self {
25            max_future_drift_secs,
26        }
27    }
28
29    /// Convert a Unix timestamp to DateTime
30    fn timestamp_to_datetime(timestamp: u64) -> DateTime<Utc> {
31        // Check if timestamp would overflow when converting to i64
32        if timestamp > i64::MAX as u64 {
33            // Return a date far in the future (year 3000+)
34            DateTime::parse_from_rfc3339("3000-01-01T00:00:00Z")
35                .unwrap()
36                .with_timezone(&Utc)
37        } else {
38            DateTime::from_timestamp(timestamp as i64, 0).unwrap_or_else(Utc::now)
39        }
40    }
41}
42
43#[async_trait]
44impl MessageValidator for TimestampValidator {
45    async fn validate(&self, message: &PlainMessage) -> ValidationResult {
46        let now = Utc::now();
47
48        // Check created_time
49        if let Some(created_time) = message.created_time {
50            let created_dt = Self::timestamp_to_datetime(created_time);
51
52            // Check if message is too far in the future
53            let max_future = now + Duration::seconds(self.max_future_drift_secs);
54            if created_dt > max_future {
55                return ValidationResult::Reject(format!(
56                    "Message created_time is too far in the future: {} (max allowed: {})",
57                    created_dt, max_future
58                ));
59            }
60        }
61
62        // Check expires_time if present
63        if let Some(expires_time) = message.expires_time {
64            let expires_dt = Self::timestamp_to_datetime(expires_time);
65
66            // Check if message has expired
67            if now > expires_dt {
68                return ValidationResult::Reject(format!(
69                    "Message has expired at: {} (current time: {})",
70                    expires_dt, now
71                ));
72            }
73        }
74
75        ValidationResult::Accept
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use chrono::Duration;
83
84    #[tokio::test]
85    async fn test_valid_timestamp() {
86        let validator = TimestampValidator::new(60);
87        let message = PlainMessage::new(
88            "test_msg_1".to_string(),
89            "test_type".to_string(),
90            serde_json::json!({}),
91            "did:example:sender".to_string(),
92        )
93        .with_recipient("did:example:receiver");
94
95        // created_time is set automatically in PlainMessage::new, which uses current time
96
97        match validator.validate(&message).await {
98            ValidationResult::Accept => {} // Expected
99            ValidationResult::Reject(reason) => panic!("Expected accept, got reject: {}", reason),
100        }
101    }
102
103    #[tokio::test]
104    async fn test_future_timestamp_within_drift() {
105        let validator = TimestampValidator::new(60);
106        let mut message = PlainMessage::new(
107            "test_msg_2".to_string(),
108            "test_type".to_string(),
109            serde_json::json!({}),
110            "did:example:sender".to_string(),
111        )
112        .with_recipient("did:example:receiver");
113
114        // Set created_time to 30 seconds in the future (within allowed drift)
115        let future_time = Utc::now() + Duration::seconds(30);
116        message.created_time = Some(future_time.timestamp() as u64);
117
118        match validator.validate(&message).await {
119            ValidationResult::Accept => {} // Expected
120            ValidationResult::Reject(reason) => panic!("Expected accept, got reject: {}", reason),
121        }
122    }
123
124    #[tokio::test]
125    async fn test_future_timestamp_exceeds_drift() {
126        let validator = TimestampValidator::new(60);
127        let mut message = PlainMessage::new(
128            "test_msg_2".to_string(),
129            "test_type".to_string(),
130            serde_json::json!({}),
131            "did:example:sender".to_string(),
132        )
133        .with_recipient("did:example:receiver");
134
135        // Set created_time to 2 minutes in the future (exceeds allowed drift)
136        let future_time = Utc::now() + Duration::seconds(120);
137        message.created_time = Some(future_time.timestamp() as u64);
138
139        match validator.validate(&message).await {
140            ValidationResult::Accept => panic!("Expected reject, got accept"),
141            ValidationResult::Reject(reason) => {
142                assert!(reason.contains("too far in the future"));
143            }
144        }
145    }
146
147    #[tokio::test]
148    async fn test_expired_message() {
149        let validator = TimestampValidator::new(60);
150        let mut message = PlainMessage::new(
151            "test_msg_2".to_string(),
152            "test_type".to_string(),
153            serde_json::json!({}),
154            "did:example:sender".to_string(),
155        )
156        .with_recipient("did:example:receiver");
157
158        // Set expires_time to 1 minute ago
159        let expired_time = Utc::now() - Duration::seconds(60);
160        message.expires_time = Some(expired_time.timestamp() as u64);
161
162        match validator.validate(&message).await {
163            ValidationResult::Accept => panic!("Expected reject, got accept"),
164            ValidationResult::Reject(reason) => {
165                assert!(reason.contains("has expired"));
166            }
167        }
168    }
169
170    #[tokio::test]
171    async fn test_very_large_timestamp() {
172        let validator = TimestampValidator::new(60);
173        let mut message = PlainMessage::new(
174            "test_msg_2".to_string(),
175            "test_type".to_string(),
176            serde_json::json!({}),
177            "did:example:sender".to_string(),
178        )
179        .with_recipient("did:example:receiver");
180
181        // Set created_time to a very large number that would fail datetime conversion
182        message.created_time = Some(u64::MAX);
183
184        // With u64::MAX, this should be far in the future and fail validation
185        match validator.validate(&message).await {
186            ValidationResult::Accept => panic!("Expected reject, got accept"),
187            ValidationResult::Reject(reason) => {
188                assert!(reason.contains("too far in the future"));
189            }
190        }
191    }
192}