use crate::error::WSError;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub const BUILD_TIMESTAMP: u64 = {
match option_env!("WSC_BUILD_TIMESTAMP") {
Some(s) => {
let bytes = s.as_bytes();
let mut result: u64 = 0;
let mut i = 0;
while i < bytes.len() {
let digit = bytes[i] as u64 - b'0' as u64;
result = result * 10 + digit;
i += 1;
}
result
}
None => 1704067200,
}
};
pub trait TimeSource: Send + Sync {
fn now(&self) -> Result<SystemTime, WSError>;
fn minimum_time(&self) -> SystemTime;
fn is_reliable(&self) -> bool;
fn now_unix(&self) -> Result<u64, WSError> {
let time = self.now()?;
Ok(time
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs())
}
fn minimum_unix(&self) -> u64 {
self.minimum_time()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemTimeSource;
impl TimeSource for SystemTimeSource {
fn now(&self) -> Result<SystemTime, WSError> {
Ok(SystemTime::now())
}
fn minimum_time(&self) -> SystemTime {
UNIX_EPOCH + Duration::from_secs(BUILD_TIMESTAMP)
}
fn is_reliable(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BuildTimeSource;
impl TimeSource for BuildTimeSource {
fn now(&self) -> Result<SystemTime, WSError> {
Err(WSError::TimeError(
"BuildTimeSource does not provide current time - use Rekor integrated_time for verification".to_string()
))
}
fn minimum_time(&self) -> SystemTime {
UNIX_EPOCH + Duration::from_secs(BUILD_TIMESTAMP)
}
fn is_reliable(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Copy)]
pub struct FixedTimeSource {
timestamp: SystemTime,
}
impl FixedTimeSource {
pub fn from_unix_secs(secs: u64) -> Result<Self, WSError> {
if secs < BUILD_TIMESTAMP {
return Err(WSError::TimeError(format!(
"Timestamp {} is before build time {}",
secs, BUILD_TIMESTAMP
)));
}
Ok(Self {
timestamp: UNIX_EPOCH + Duration::from_secs(secs),
})
}
pub fn from_system_time(time: SystemTime) -> Result<Self, WSError> {
let secs = time
.duration_since(UNIX_EPOCH)
.map_err(|e| WSError::TimeError(format!("Time before Unix epoch: {}", e)))?
.as_secs();
Self::from_unix_secs(secs)
}
pub fn timestamp(&self) -> SystemTime {
self.timestamp
}
}
impl TimeSource for FixedTimeSource {
fn now(&self) -> Result<SystemTime, WSError> {
Ok(self.timestamp)
}
fn minimum_time(&self) -> SystemTime {
UNIX_EPOCH + Duration::from_secs(BUILD_TIMESTAMP)
}
fn is_reliable(&self) -> bool {
true
}
}
pub struct TimeValidationConfig {
pub time_source: Option<Box<dyn TimeSource>>,
pub max_signature_age: Option<Duration>,
pub clock_skew_tolerance: Duration,
}
impl std::fmt::Debug for TimeValidationConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TimeValidationConfig")
.field("time_source", &self.time_source.as_ref().map(|_| "<TimeSource>"))
.field("max_signature_age", &self.max_signature_age)
.field("clock_skew_tolerance", &self.clock_skew_tolerance)
.finish()
}
}
impl Default for TimeValidationConfig {
fn default() -> Self {
Self {
time_source: None,
max_signature_age: None,
clock_skew_tolerance: Duration::from_secs(300), }
}
}
impl TimeValidationConfig {
pub fn no_freshness() -> Self {
Self::default()
}
pub fn with_system_time() -> Self {
Self {
time_source: Some(Box::new(SystemTimeSource)),
..Default::default()
}
}
pub fn with_time_source(source: impl TimeSource + 'static) -> Self {
Self {
time_source: Some(Box::new(source)),
..Default::default()
}
}
pub fn max_age(mut self, age: Duration) -> Self {
self.max_signature_age = Some(age);
self
}
pub fn clock_skew(mut self, tolerance: Duration) -> Self {
self.clock_skew_tolerance = tolerance;
self
}
}
impl Clone for TimeValidationConfig {
fn clone(&self) -> Self {
Self {
time_source: None,
max_signature_age: self.max_signature_age,
clock_skew_tolerance: self.clock_skew_tolerance,
}
}
}
pub fn validate_timestamp(timestamp_secs: u64, config: &TimeValidationConfig) -> Result<bool, WSError> {
if timestamp_secs < BUILD_TIMESTAMP {
log::warn!(
"Timestamp {} is before build time {}",
timestamp_secs,
BUILD_TIMESTAMP
);
return Ok(false);
}
if let Some(ref time_source) = config.time_source {
if time_source.is_reliable() {
let now = time_source.now_unix()?;
let skew = config.clock_skew_tolerance.as_secs();
if timestamp_secs > now + skew {
log::warn!(
"Timestamp {} is {} seconds in the future (tolerance: {})",
timestamp_secs,
timestamp_secs - now,
skew
);
return Ok(false);
}
if let Some(max_age) = config.max_signature_age {
let max_age_secs = max_age.as_secs();
if now > timestamp_secs && now - timestamp_secs > max_age_secs {
log::warn!(
"Signature is {} seconds old (max age: {})",
now - timestamp_secs,
max_age_secs
);
return Ok(false);
}
}
}
}
Ok(true)
}
pub fn parse_timestamp(timestamp: &str) -> Result<u64, WSError> {
if let Ok(secs) = timestamp.parse::<u64>() {
return Ok(secs);
}
let timestamp = timestamp.trim();
if timestamp.len() >= 20 && timestamp.ends_with('Z') {
let parts: Vec<&str> = timestamp[..19].split(|c| c == '-' || c == 'T' || c == ':').collect();
if parts.len() == 6 {
let year: i32 = parts[0].parse().map_err(|_| WSError::TimeError("Invalid year".into()))?;
let month: u32 = parts[1].parse().map_err(|_| WSError::TimeError("Invalid month".into()))?;
let day: u32 = parts[2].parse().map_err(|_| WSError::TimeError("Invalid day".into()))?;
let hour: u32 = parts[3].parse().map_err(|_| WSError::TimeError("Invalid hour".into()))?;
let minute: u32 = parts[4].parse().map_err(|_| WSError::TimeError("Invalid minute".into()))?;
let second: u32 = parts[5].parse().map_err(|_| WSError::TimeError("Invalid second".into()))?;
let days = days_since_epoch(year, month, day)?;
let secs = (days as u64) * 86400 + (hour as u64) * 3600 + (minute as u64) * 60 + (second as u64);
return Ok(secs);
}
}
Err(WSError::TimeError(format!(
"Cannot parse timestamp: '{}'",
timestamp
)))
}
fn days_since_epoch(year: i32, month: u32, day: u32) -> Result<i64, WSError> {
if year < 1970 {
return Err(WSError::TimeError("Year before 1970".into()));
}
if !(1..=12).contains(&month) {
return Err(WSError::TimeError("Invalid month".into()));
}
if !(1..=31).contains(&day) {
return Err(WSError::TimeError("Invalid day".into()));
}
const DAYS_IN_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let is_leap = |y: i32| y % 4 == 0 && (y % 100 != 0 || y % 400 == 0);
let mut days: i64 = 0;
for y in 1970..year {
days += if is_leap(y) { 366 } else { 365 };
}
for m in 1..month {
let d = DAYS_IN_MONTH[(m - 1) as usize];
days += d as i64;
if m == 2 && is_leap(year) {
days += 1;
}
}
days += (day - 1) as i64;
Ok(days)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_timestamp_is_reasonable() {
assert!(BUILD_TIMESTAMP >= 1704067200);
assert!(BUILD_TIMESTAMP < 4102444800);
}
#[test]
fn test_system_time_source() {
let source = SystemTimeSource;
let now = source.now().unwrap();
assert!(now > source.minimum_time());
assert!(source.is_reliable());
}
#[test]
fn test_build_time_source() {
let source = BuildTimeSource;
assert!(source.now().is_err());
assert!(!source.is_reliable());
assert!(source.minimum_unix() >= 1704067200);
}
#[test]
fn test_fixed_time_source() {
let future_time = BUILD_TIMESTAMP + 86400; let source = FixedTimeSource::from_unix_secs(future_time).unwrap();
assert!(source.is_reliable());
assert_eq!(source.now_unix().unwrap(), future_time);
}
#[test]
fn test_fixed_time_source_rejects_old_time() {
let result = FixedTimeSource::from_unix_secs(1000000000);
assert!(result.is_err());
}
#[test]
fn test_parse_timestamp_unix() {
assert_eq!(parse_timestamp("1704067200").unwrap(), 1704067200);
}
#[test]
fn test_parse_timestamp_iso8601() {
assert_eq!(parse_timestamp("2024-01-01T00:00:00Z").unwrap(), 1704067200);
let expected = 1704067200 + 14 * 86400 + 12 * 3600 + 30 * 60 + 45;
assert_eq!(parse_timestamp("2024-01-15T12:30:45Z").unwrap(), expected);
}
#[test]
fn test_parse_timestamp_with_millis() {
assert_eq!(
parse_timestamp("2024-01-01T00:00:00.123Z").unwrap(),
1704067200
);
}
#[test]
fn test_validate_timestamp_basic() {
let config = TimeValidationConfig::no_freshness();
let future = BUILD_TIMESTAMP + 86400 * 365; assert!(validate_timestamp(future, &config).unwrap());
assert!(!validate_timestamp(1000000000, &config).unwrap());
}
#[test]
fn test_validate_timestamp_with_max_age() {
let config = TimeValidationConfig::with_system_time()
.max_age(Duration::from_secs(3600));
let now = SystemTimeSource.now_unix().unwrap();
let old_time = if now > BUILD_TIMESTAMP + 7200 {
now - 7200 } else {
BUILD_TIMESTAMP };
let old_result = validate_timestamp(old_time, &config).unwrap();
if now - old_time > 3600 {
assert!(!old_result, "Timestamps older than max_age should fail");
}
let recent = std::cmp::max(now - 1800, BUILD_TIMESTAMP);
if now - recent <= 3600 && recent >= BUILD_TIMESTAMP {
assert!(validate_timestamp(recent, &config).unwrap());
}
}
#[test]
fn test_validate_timestamp_future_with_skew() {
let config = TimeValidationConfig::with_system_time()
.clock_skew(Duration::from_secs(300));
let now = SystemTimeSource.now_unix().unwrap();
assert!(validate_timestamp(now + 60, &config).unwrap());
assert!(!validate_timestamp(now + 600, &config).unwrap()); }
#[test]
fn test_time_validation_config_clone() {
let config = TimeValidationConfig::with_system_time()
.max_age(Duration::from_secs(3600))
.clock_skew(Duration::from_secs(120));
let cloned = config.clone();
assert_eq!(cloned.max_signature_age, Some(Duration::from_secs(3600)));
assert_eq!(cloned.clock_skew_tolerance, Duration::from_secs(120));
assert!(cloned.time_source.is_none());
}
#[test]
fn test_days_since_epoch() {
assert_eq!(days_since_epoch(1970, 1, 1).unwrap(), 0);
assert_eq!(days_since_epoch(1970, 1, 2).unwrap(), 1);
assert_eq!(days_since_epoch(2024, 1, 1).unwrap(), 19723);
}
}