jacs 0.9.5

JACS JSON AI Communication Standard
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
//! Time utilities for JACS.
//!
//! This module provides centralized timestamp handling functions used throughout
//! the crate for consistent time formatting, parsing, and validation.

use crate::error::JacsError;
use chrono::{DateTime, Utc};

/// Maximum clock drift tolerance for signature timestamps (in seconds).
/// Signatures dated more than this many seconds in the future are rejected.
pub const MAX_FUTURE_TIMESTAMP_SECONDS: i64 = 300;

/// Maximum skew tolerance for `iat` (issued-at) signature claims in seconds.
/// Default: 0 (disabled) so that long-lived JACS documents remain verifiable.
/// Set `JACS_MAX_IAT_SKEW_SECONDS` to a positive value (e.g., 300) when JACS
/// is used for auth in HTTP servers, MCP transports, or any context where
/// replay-attack prevention requires temporal freshness guarantees.
pub const MAX_IAT_SKEW_SECONDS: i64 = 0;

/// Returns the effective `iat` skew window in seconds.
///
/// Set `JACS_MAX_IAT_SKEW_SECONDS` to override:
/// - Positive value: enforce that window
/// - `0` or negative: disable `iat` skew checks
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)
}

/// Default maximum signature age (in seconds).
/// Default: 0 (no expiration). JACS documents are designed to be idempotent and eternal.
/// Set `JACS_MAX_SIGNATURE_AGE_SECONDS` to a positive value to enable expiration
/// (e.g., 7776000 for 90 days).
pub const MAX_SIGNATURE_AGE_SECONDS: i64 = 0;

/// Returns the current UTC timestamp in RFC 3339 format.
///
/// This is the standard format used throughout JACS for timestamps.
///
/// # Example
///
/// ```rust
/// use jacs::time_utils::now_rfc3339;
///
/// let timestamp = now_rfc3339();
/// // Example: "2025-01-15T14:30:00.123456789+00:00"
/// ```
#[inline]
#[must_use]
pub fn now_rfc3339() -> String {
    Utc::now().to_rfc3339()
}

/// Returns the current UTC timestamp.
///
/// Use this when you need the `DateTime<Utc>` value directly for arithmetic
/// or other operations before formatting.
#[inline]
#[must_use]
pub fn now_utc() -> DateTime<Utc> {
    Utc::now()
}

/// Returns the current Unix timestamp in seconds.
///
/// Useful for timestamp comparisons where RFC 3339 parsing overhead is not needed.
#[inline]
#[must_use]
pub fn now_timestamp() -> i64 {
    Utc::now().timestamp()
}

/// Validates an issued-at (`iat`) Unix timestamp.
///
/// The timestamp must be within ±`MAX_IAT_SKEW_SECONDS` of current system time.
/// This is used to limit replay windows for signed envelopes.
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(())
}

/// Parses an RFC 3339 timestamp string into a `DateTime<Utc>`.
///
/// # Arguments
///
/// * `s` - The RFC 3339 formatted timestamp string
///
/// # Returns
///
/// The parsed `DateTime<Utc>` or a `JacsError` if parsing fails.
///
/// # Example
///
/// ```rust,ignore
/// use jacs::time_utils::parse_rfc3339;
///
/// let dt = parse_rfc3339("2025-01-15T14:30:00+00:00")?;
/// ```
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))
        })
}

/// Parses an RFC 3339 timestamp string and returns the Unix timestamp.
///
/// # Arguments
///
/// * `s` - The RFC 3339 formatted timestamp string
///
/// # Returns
///
/// The Unix timestamp (seconds since epoch) or a `JacsError` if parsing fails.
pub fn parse_rfc3339_to_timestamp(s: &str) -> Result<i64, JacsError> {
    parse_rfc3339(s).map(|dt| dt.timestamp())
}

/// Validates that a timestamp is not too far in the future.
///
/// This function checks that the given timestamp is not more than
/// `MAX_FUTURE_TIMESTAMP_SECONDS` in the future, allowing for reasonable
/// clock drift between systems.
///
/// # Arguments
///
/// * `timestamp_str` - The RFC 3339 formatted timestamp string
///
/// # Returns
///
/// `Ok(())` if the timestamp is valid, or a `JacsError` describing the issue.
pub fn validate_timestamp_not_future(timestamp_str: &str) -> Result<(), JacsError> {
    validate_timestamp_not_future_with_skew(timestamp_str, MAX_FUTURE_TIMESTAMP_SECONDS)
}

/// Validates that a timestamp is not too far in the future with custom skew tolerance.
///
/// # Arguments
///
/// * `timestamp_str` - The RFC 3339 formatted timestamp string
/// * `max_skew_seconds` - Maximum allowed clock skew in seconds
///
/// # Returns
///
/// `Ok(())` if the timestamp is valid, or a `JacsError` describing the issue.
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(())
}

/// Validates that a timestamp is not too old.
///
/// # Arguments
///
/// * `timestamp_str` - The RFC 3339 formatted timestamp string
/// * `max_age_seconds` - Maximum allowed age in seconds
///
/// # Returns
///
/// `Ok(())` if the timestamp is valid, or a `JacsError` describing the issue.
pub fn validate_timestamp_not_expired(
    timestamp_str: &str,
    max_age_seconds: i64,
) -> Result<(), JacsError> {
    if max_age_seconds <= 0 {
        // Expiration checking disabled
        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(())
}

/// Returns the effective maximum signature age in seconds.
///
/// Checks `JACS_MAX_SIGNATURE_AGE_SECONDS` environment variable first,
/// falls back to the compiled-in default (0 = no expiration).
/// Set to a positive value to enable expiration (e.g., 7776000 for 90 days).
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)
}

/// Validates a signature timestamp.
///
/// This combines both future and expiration checks.
///
/// # Arguments
///
/// * `timestamp_str` - RFC 3339 formatted timestamp string
///
/// # Returns
///
/// `Ok(())` if the timestamp is valid, or a `JacsError` describing the issue.
///
/// # Validation Rules
///
/// 1. The timestamp must be a valid RFC 3339 / ISO 8601 format
/// 2. The timestamp must not be more than `MAX_FUTURE_TIMESTAMP_SECONDS` in the future
///    (allows for small clock drift between systems)
/// 3. If signature age limit > 0 (default: disabled), the timestamp must not be older than that.
///    Set `JACS_MAX_SIGNATURE_AGE_SECONDS` to a positive value to enable (e.g., 7776000 for 90 days).
pub fn validate_signature_timestamp(timestamp_str: &str) -> Result<(), JacsError> {
    // Parse the timestamp (validates format)
    let signature_time =
        parse_rfc3339(timestamp_str).map_err(|_| JacsError::SignatureVerificationFailed {
            reason: format!("Invalid signature timestamp format '{}'", timestamp_str),
        })?;

    let now = Utc::now();

    // Check for future timestamps (with clock drift tolerance)
    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
            ),
        });
    }

    // Check for expired signatures (if expiration is enabled)
    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(())
}

/// Generates a backup filename suffix based on current timestamp.
///
/// # Returns
///
/// A string like "backup-2025-01-15-14-30" suitable for backup filenames.
#[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();
        // Should be parseable as RFC 3339
        assert!(DateTime::parse_from_rfc3339(&timestamp).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() {
        // With MAX_IAT_SKEW_SECONDS=0, iat skew checks are disabled.
        // Documents are eternal — only API callers enforce freshness.
        let now = now_timestamp();
        let result = validate_signature_iat(now - 86400); // 1 day old
        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() {
        // Within tolerance
        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() {
        // Beyond tolerance
        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();
        // With max_age_seconds = 0, expiration is disabled
        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-"));
        // Should have format like "backup-2025-01-15-14-30"
        assert_eq!(suffix.len(), 23); // "backup-YYYY-MM-DD-HH-MM"
    }

    #[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);
    }
}