use super::{MessageValidator, ValidationResult};
use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use tap_msg::didcomm::PlainMessage;
pub struct TimestampValidator {
max_future_drift_secs: i64,
}
impl TimestampValidator {
pub fn new(max_future_drift_secs: i64) -> Self {
Self {
max_future_drift_secs,
}
}
fn timestamp_to_datetime(timestamp: u64) -> DateTime<Utc> {
let timestamp_seconds = if timestamp < 10_000_000_000 {
timestamp
} else {
timestamp / 1000
};
if timestamp_seconds > i64::MAX as u64 {
DateTime::parse_from_rfc3339("3000-01-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc)
} else {
DateTime::from_timestamp(timestamp_seconds as i64, 0).unwrap_or_else(Utc::now)
}
}
}
#[async_trait]
impl MessageValidator for TimestampValidator {
async fn validate(&self, message: &PlainMessage) -> ValidationResult {
let now = Utc::now();
if let Some(created_time) = message.created_time {
let created_dt = Self::timestamp_to_datetime(created_time);
let max_future = now + Duration::seconds(self.max_future_drift_secs);
if created_dt > max_future {
return ValidationResult::Reject(format!(
"Message created_time is too far in the future: {} (max allowed: {})",
created_dt, max_future
));
}
}
if let Some(expires_time) = message.expires_time {
let expires_dt = Self::timestamp_to_datetime(expires_time);
if now > expires_dt {
return ValidationResult::Reject(format!(
"Message has expired at: {} (current time: {})",
expires_dt, now
));
}
}
ValidationResult::Accept
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
#[tokio::test]
async fn test_valid_timestamp() {
let validator = TimestampValidator::new(60);
let message = PlainMessage::new(
"test_msg_1".to_string(),
"test_type".to_string(),
serde_json::json!({}),
"did:example:sender".to_string(),
)
.with_recipient("did:example:receiver");
match validator.validate(&message).await {
ValidationResult::Accept => {} ValidationResult::Reject(reason) => panic!("Expected accept, got reject: {}", reason),
}
}
#[tokio::test]
async fn test_future_timestamp_within_drift() {
let validator = TimestampValidator::new(60);
let mut message = PlainMessage::new(
"test_msg_2".to_string(),
"test_type".to_string(),
serde_json::json!({}),
"did:example:sender".to_string(),
)
.with_recipient("did:example:receiver");
let future_time = Utc::now() + Duration::seconds(30);
message.created_time = Some(future_time.timestamp() as u64);
match validator.validate(&message).await {
ValidationResult::Accept => {} ValidationResult::Reject(reason) => panic!("Expected accept, got reject: {}", reason),
}
}
#[tokio::test]
async fn test_future_timestamp_exceeds_drift() {
let validator = TimestampValidator::new(60);
let mut message = PlainMessage::new(
"test_msg_2".to_string(),
"test_type".to_string(),
serde_json::json!({}),
"did:example:sender".to_string(),
)
.with_recipient("did:example:receiver");
let future_time = Utc::now() + Duration::seconds(120);
message.created_time = Some(future_time.timestamp() as u64);
match validator.validate(&message).await {
ValidationResult::Accept => panic!("Expected reject, got accept"),
ValidationResult::Reject(reason) => {
assert!(reason.contains("too far in the future"));
}
}
}
#[tokio::test]
async fn test_expired_message() {
let validator = TimestampValidator::new(60);
let mut message = PlainMessage::new(
"test_msg_2".to_string(),
"test_type".to_string(),
serde_json::json!({}),
"did:example:sender".to_string(),
)
.with_recipient("did:example:receiver");
let expired_time = Utc::now() - Duration::seconds(60);
message.expires_time = Some(expired_time.timestamp() as u64);
match validator.validate(&message).await {
ValidationResult::Accept => panic!("Expected reject, got accept"),
ValidationResult::Reject(reason) => {
assert!(reason.contains("has expired"));
}
}
}
#[tokio::test]
async fn test_very_large_timestamp() {
let validator = TimestampValidator::new(60);
let mut message = PlainMessage::new(
"test_msg_2".to_string(),
"test_type".to_string(),
serde_json::json!({}),
"did:example:sender".to_string(),
)
.with_recipient("did:example:receiver");
message.created_time = Some(32503680000000);
match validator.validate(&message).await {
ValidationResult::Accept => panic!("Expected reject, got accept"),
ValidationResult::Reject(reason) => {
assert!(reason.contains("too far in the future"));
}
}
}
#[tokio::test]
async fn test_timestamp_milliseconds() {
let validator = TimestampValidator::new(60);
let mut message = PlainMessage::new(
"test_msg_3".to_string(),
"test_type".to_string(),
serde_json::json!({}),
"did:example:sender".to_string(),
)
.with_recipient("did:example:receiver");
message.created_time = Some(Utc::now().timestamp_millis() as u64);
match validator.validate(&message).await {
ValidationResult::Accept => {} ValidationResult::Reject(reason) => panic!("Expected accept, got reject: {}", reason),
}
}
#[tokio::test]
async fn test_future_timestamp_milliseconds_exceeds_drift() {
let validator = TimestampValidator::new(60);
let mut message = PlainMessage::new(
"test_msg_4".to_string(),
"test_type".to_string(),
serde_json::json!({}),
"did:example:sender".to_string(),
)
.with_recipient("did:example:receiver");
let future_time = Utc::now() + Duration::seconds(120);
message.created_time = Some(future_time.timestamp_millis() as u64);
match validator.validate(&message).await {
ValidationResult::Accept => panic!("Expected reject, got accept"),
ValidationResult::Reject(reason) => {
assert!(reason.contains("too far in the future"));
}
}
}
}