use crate::error::JacsError;
use chrono::{DateTime, Utc};
pub const MAX_FUTURE_TIMESTAMP_SECONDS: i64 = 300;
pub const MAX_IAT_SKEW_SECONDS: i64 = 0;
pub fn max_iat_skew_seconds() -> i64 {
std::env::var("JACS_MAX_IAT_SKEW_SECONDS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(MAX_IAT_SKEW_SECONDS)
}
pub const MAX_SIGNATURE_AGE_SECONDS: i64 = 0;
#[inline]
#[must_use]
pub fn now_rfc3339() -> String {
Utc::now().to_rfc3339()
}
#[inline]
#[must_use]
pub fn now_utc() -> DateTime<Utc> {
Utc::now()
}
#[inline]
#[must_use]
pub fn now_timestamp() -> i64 {
Utc::now().timestamp()
}
pub fn validate_signature_iat(iat: i64) -> Result<(), JacsError> {
let max_skew_seconds = max_iat_skew_seconds();
if max_skew_seconds <= 0 {
return Ok(());
}
let now = now_timestamp();
let skew = (now - iat).abs();
if skew > max_skew_seconds {
return Err(JacsError::SignatureVerificationFailed {
reason: format!(
"Signature iat skew is {} seconds, exceeding maximum allowed {} seconds.",
skew, max_skew_seconds
),
});
}
Ok(())
}
pub fn parse_rfc3339(s: &str) -> Result<DateTime<Utc>, JacsError> {
DateTime::parse_from_rfc3339(s)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| {
JacsError::ValidationError(format!("Invalid RFC 3339 timestamp '{}': {}", s, e))
})
}
pub fn parse_rfc3339_to_timestamp(s: &str) -> Result<i64, JacsError> {
parse_rfc3339(s).map(|dt| dt.timestamp())
}
pub fn validate_timestamp_not_future(timestamp_str: &str) -> Result<(), JacsError> {
validate_timestamp_not_future_with_skew(timestamp_str, MAX_FUTURE_TIMESTAMP_SECONDS)
}
pub fn validate_timestamp_not_future_with_skew(
timestamp_str: &str,
max_skew_seconds: i64,
) -> Result<(), JacsError> {
let timestamp = parse_rfc3339(timestamp_str)?;
let now = Utc::now();
let future_limit = now + chrono::Duration::seconds(max_skew_seconds);
if timestamp > future_limit {
return Err(JacsError::ValidationError(format!(
"Timestamp '{}' is too far in the future (max {} seconds allowed). \
This may indicate clock skew or a forged timestamp.",
timestamp_str, max_skew_seconds
)));
}
Ok(())
}
pub fn validate_timestamp_not_expired(
timestamp_str: &str,
max_age_seconds: i64,
) -> Result<(), JacsError> {
if max_age_seconds <= 0 {
return Ok(());
}
let timestamp = parse_rfc3339(timestamp_str)?;
let now = Utc::now();
let expiry_limit = now - chrono::Duration::seconds(max_age_seconds);
if timestamp < expiry_limit {
return Err(JacsError::ValidationError(format!(
"Timestamp '{}' is too old (max age {} seconds). \
The document may need to be re-signed.",
timestamp_str, max_age_seconds
)));
}
Ok(())
}
pub fn max_signature_age() -> i64 {
std::env::var("JACS_MAX_SIGNATURE_AGE_SECONDS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(MAX_SIGNATURE_AGE_SECONDS)
}
pub fn validate_signature_timestamp(timestamp_str: &str) -> Result<(), JacsError> {
let signature_time =
parse_rfc3339(timestamp_str).map_err(|_| JacsError::SignatureVerificationFailed {
reason: format!("Invalid signature timestamp format '{}'", timestamp_str),
})?;
let now = Utc::now();
let future_limit = now + chrono::Duration::seconds(MAX_FUTURE_TIMESTAMP_SECONDS);
if signature_time > future_limit {
return Err(JacsError::SignatureVerificationFailed {
reason: format!(
"Signature timestamp {} is too far in the future (max {} seconds allowed). \
This may indicate clock skew or a forged signature.",
timestamp_str, MAX_FUTURE_TIMESTAMP_SECONDS
),
});
}
let age_limit = max_signature_age();
if age_limit > 0 {
let expiry_limit = now - chrono::Duration::seconds(age_limit);
if signature_time < expiry_limit {
return Err(JacsError::SignatureVerificationFailed {
reason: format!(
"Signature timestamp {} is too old (max age {} seconds / {} days). \
The agent document may need to be re-signed. \
Set JACS_MAX_SIGNATURE_AGE_SECONDS=0 to disable expiration.",
timestamp_str,
age_limit,
age_limit / 86400
),
});
}
}
Ok(())
}
#[inline]
#[must_use]
pub fn backup_timestamp_suffix() -> String {
Utc::now().format("backup-%Y-%m-%d-%H-%M").to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_now_rfc3339_format() {
let timestamp = now_rfc3339();
assert!(DateTime::parse_from_rfc3339(×tamp).is_ok());
}
#[test]
fn test_parse_rfc3339_valid() {
let result = parse_rfc3339("2025-01-15T14:30:00+00:00");
assert!(result.is_ok());
}
#[test]
fn test_parse_rfc3339_invalid() {
let result = parse_rfc3339("not a timestamp");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Invalid RFC 3339 timestamp"));
}
#[test]
fn test_validate_signature_iat_recent() {
let now = now_timestamp();
let result = validate_signature_iat(now - 10);
assert!(result.is_ok());
}
#[test]
fn test_validate_signature_iat_disabled_by_default() {
let now = now_timestamp();
let result = validate_signature_iat(now - 86400); assert!(
result.is_ok(),
"iat skew check should be disabled by default"
);
}
#[test]
fn test_validate_timestamp_not_future_current() {
let now = now_rfc3339();
let result = validate_timestamp_not_future(&now);
assert!(result.is_ok());
}
#[test]
fn test_validate_timestamp_not_future_past() {
let past = (Utc::now() - chrono::Duration::hours(1)).to_rfc3339();
let result = validate_timestamp_not_future(&past);
assert!(result.is_ok());
}
#[test]
fn test_validate_timestamp_not_future_slight_future() {
let slight_future = (Utc::now() + chrono::Duration::seconds(30)).to_rfc3339();
let result = validate_timestamp_not_future(&slight_future);
assert!(result.is_ok());
}
#[test]
fn test_validate_timestamp_not_future_far_future() {
let far_future = (Utc::now() + chrono::Duration::minutes(10)).to_rfc3339();
let result = validate_timestamp_not_future(&far_future);
assert!(result.is_err());
}
#[test]
fn test_validate_signature_timestamp_valid() {
let now = now_rfc3339();
let result = validate_signature_timestamp(&now);
assert!(result.is_ok());
}
#[test]
fn test_validate_signature_timestamp_far_future() {
let far_future = (Utc::now() + chrono::Duration::hours(1)).to_rfc3339();
let result = validate_signature_timestamp(&far_future);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("too far in the future"));
}
#[test]
fn test_validate_timestamp_not_expired() {
let recent = now_rfc3339();
let result = validate_timestamp_not_expired(&recent, 3600);
assert!(result.is_ok());
}
#[test]
fn test_validate_timestamp_expired() {
let old = (Utc::now() - chrono::Duration::hours(2)).to_rfc3339();
let result = validate_timestamp_not_expired(&old, 3600);
assert!(result.is_err());
}
#[test]
fn test_validate_timestamp_expiration_disabled() {
let old = (Utc::now() - chrono::Duration::days(365)).to_rfc3339();
let result = validate_timestamp_not_expired(&old, 0);
assert!(result.is_ok());
}
#[test]
fn test_backup_timestamp_suffix_format() {
let suffix = backup_timestamp_suffix();
assert!(suffix.starts_with("backup-"));
assert_eq!(suffix.len(), 23); }
#[test]
fn test_parse_rfc3339_to_timestamp() {
let result = parse_rfc3339_to_timestamp("2025-01-15T00:00:00+00:00");
assert!(result.is_ok());
let ts = result.unwrap();
assert!(ts > 0);
}
}