Skip to main content

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        // Detect if timestamp is in seconds or milliseconds
32        // Timestamps in seconds since 1970 are much smaller than timestamps in milliseconds
33        // A reasonable cutoff is 10^10 (around year 2286 in seconds, or year 1970 + 4 months in milliseconds)
34        let timestamp_seconds = if timestamp < 10_000_000_000 {
35            // Timestamp is likely in seconds
36            timestamp
37        } else {
38            // Timestamp is likely in milliseconds, convert to seconds
39            timestamp / 1000
40        };
41
42        // Check if timestamp would overflow when converting to i64
43        if timestamp_seconds > i64::MAX as u64 {
44            // Return a date far in the future (year 3000+)
45            DateTime::parse_from_rfc3339("3000-01-01T00:00:00Z")
46                .unwrap()
47                .with_timezone(&Utc)
48        } else {
49            DateTime::from_timestamp(timestamp_seconds as i64, 0).unwrap_or_else(Utc::now)
50        }
51    }
52}
53
54#[async_trait]
55impl MessageValidator for TimestampValidator {
56    async fn validate(&self, message: &PlainMessage) -> ValidationResult {
57        let now = Utc::now();
58
59        // Check created_time
60        if let Some(created_time) = message.created_time {
61            let created_dt = Self::timestamp_to_datetime(created_time);
62
63            // Check if message is too far in the future
64            let max_future = now + Duration::seconds(self.max_future_drift_secs);
65            if created_dt > max_future {
66                return ValidationResult::Reject(format!(
67                    "Message created_time is too far in the future: {} (max allowed: {})",
68                    created_dt, max_future
69                ));
70            }
71        }
72
73        // Check expires_time if present
74        if let Some(expires_time) = message.expires_time {
75            let expires_dt = Self::timestamp_to_datetime(expires_time);
76
77            // Check if message has expired
78            if now > expires_dt {
79                return ValidationResult::Reject(format!(
80                    "Message has expired at: {} (current time: {})",
81                    expires_dt, now
82                ));
83            }
84        }
85
86        ValidationResult::Accept
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use chrono::Duration;
94
95    #[tokio::test]
96    async fn test_valid_timestamp() {
97        let validator = TimestampValidator::new(60);
98        let message = PlainMessage::new(
99            "test_msg_1".to_string(),
100            "test_type".to_string(),
101            serde_json::json!({}),
102            "did:example:sender".to_string(),
103        )
104        .with_recipient("did:example:receiver");
105
106        // created_time is set automatically in PlainMessage::new, which uses current time
107
108        match validator.validate(&message).await {
109            ValidationResult::Accept => {} // Expected
110            ValidationResult::Reject(reason) => panic!("Expected accept, got reject: {}", reason),
111        }
112    }
113
114    #[tokio::test]
115    async fn test_future_timestamp_within_drift() {
116        let validator = TimestampValidator::new(60);
117        let mut message = PlainMessage::new(
118            "test_msg_2".to_string(),
119            "test_type".to_string(),
120            serde_json::json!({}),
121            "did:example:sender".to_string(),
122        )
123        .with_recipient("did:example:receiver");
124
125        // Set created_time to 30 seconds in the future (within allowed drift)
126        let future_time = Utc::now() + Duration::seconds(30);
127        message.created_time = Some(future_time.timestamp() as u64);
128
129        match validator.validate(&message).await {
130            ValidationResult::Accept => {} // Expected
131            ValidationResult::Reject(reason) => panic!("Expected accept, got reject: {}", reason),
132        }
133    }
134
135    #[tokio::test]
136    async fn test_future_timestamp_exceeds_drift() {
137        let validator = TimestampValidator::new(60);
138        let mut message = PlainMessage::new(
139            "test_msg_2".to_string(),
140            "test_type".to_string(),
141            serde_json::json!({}),
142            "did:example:sender".to_string(),
143        )
144        .with_recipient("did:example:receiver");
145
146        // Set created_time to 2 minutes in the future (exceeds allowed drift)
147        let future_time = Utc::now() + Duration::seconds(120);
148        message.created_time = Some(future_time.timestamp() as u64);
149
150        match validator.validate(&message).await {
151            ValidationResult::Accept => panic!("Expected reject, got accept"),
152            ValidationResult::Reject(reason) => {
153                assert!(reason.contains("too far in the future"));
154            }
155        }
156    }
157
158    #[tokio::test]
159    async fn test_expired_message() {
160        let validator = TimestampValidator::new(60);
161        let mut message = PlainMessage::new(
162            "test_msg_2".to_string(),
163            "test_type".to_string(),
164            serde_json::json!({}),
165            "did:example:sender".to_string(),
166        )
167        .with_recipient("did:example:receiver");
168
169        // Set expires_time to 1 minute ago
170        let expired_time = Utc::now() - Duration::seconds(60);
171        message.expires_time = Some(expired_time.timestamp() as u64);
172
173        match validator.validate(&message).await {
174            ValidationResult::Accept => panic!("Expected reject, got accept"),
175            ValidationResult::Reject(reason) => {
176                assert!(reason.contains("has expired"));
177            }
178        }
179    }
180
181    #[tokio::test]
182    async fn test_very_large_timestamp() {
183        let validator = TimestampValidator::new(60);
184        let mut message = PlainMessage::new(
185            "test_msg_2".to_string(),
186            "test_type".to_string(),
187            serde_json::json!({}),
188            "did:example:sender".to_string(),
189        )
190        .with_recipient("did:example:receiver");
191
192        // Set created_time to year 3000 in milliseconds (well beyond reasonable future)
193        // 3000-01-01 = approximately 32503680000 seconds = 32503680000000 milliseconds
194        message.created_time = Some(32503680000000);
195
196        // This should be far in the future and fail validation
197        match validator.validate(&message).await {
198            ValidationResult::Accept => panic!("Expected reject, got accept"),
199            ValidationResult::Reject(reason) => {
200                assert!(reason.contains("too far in the future"));
201            }
202        }
203    }
204
205    #[tokio::test]
206    async fn test_timestamp_milliseconds() {
207        let validator = TimestampValidator::new(60);
208        let mut message = PlainMessage::new(
209            "test_msg_3".to_string(),
210            "test_type".to_string(),
211            serde_json::json!({}),
212            "did:example:sender".to_string(),
213        )
214        .with_recipient("did:example:receiver");
215
216        // Set created_time to current time in milliseconds
217        message.created_time = Some(Utc::now().timestamp_millis() as u64);
218
219        match validator.validate(&message).await {
220            ValidationResult::Accept => {} // Expected
221            ValidationResult::Reject(reason) => panic!("Expected accept, got reject: {}", reason),
222        }
223    }
224
225    #[tokio::test]
226    async fn test_future_timestamp_milliseconds_exceeds_drift() {
227        let validator = TimestampValidator::new(60);
228        let mut message = PlainMessage::new(
229            "test_msg_4".to_string(),
230            "test_type".to_string(),
231            serde_json::json!({}),
232            "did:example:sender".to_string(),
233        )
234        .with_recipient("did:example:receiver");
235
236        // Set created_time to 2 minutes in the future in milliseconds (exceeds allowed drift)
237        let future_time = Utc::now() + Duration::seconds(120);
238        message.created_time = Some(future_time.timestamp_millis() as u64);
239
240        match validator.validate(&message).await {
241            ValidationResult::Accept => panic!("Expected reject, got accept"),
242            ValidationResult::Reject(reason) => {
243                assert!(reason.contains("too far in the future"));
244            }
245        }
246    }
247}