use crate::error::{Error, Result};
use crate::json::MAX_SAFE_INTEGER;
pub trait Clock: Send + Sync {
fn now(&self) -> Result<i64>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FixedClock(pub i64);
impl Clock for FixedClock {
fn now(&self) -> Result<i64> {
in_range(self.0)
}
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemClock;
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
impl Clock for SystemClock {
fn now(&self) -> Result<i64> {
let since_epoch = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| Error::InvalidClaim("clock"))?;
let secs =
i64::try_from(since_epoch.as_secs()).map_err(|_| Error::InvalidClaim("clock"))?;
in_range(secs)
}
}
pub(crate) fn now_checked(clock: &dyn Clock) -> Result<i64> {
in_range(clock.now()?)
}
fn in_range(secs: i64) -> Result<i64> {
if secs.unsigned_abs() <= MAX_SAFE_INTEGER.unsigned_abs() {
Ok(secs)
} else {
Err(Error::InvalidClaim("clock"))
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Raw(i64);
impl Clock for Raw {
fn now(&self) -> Result<i64> {
Ok(self.0)
}
}
struct Failing;
impl Clock for Failing {
fn now(&self) -> Result<i64> {
Err(Error::InvalidClaim("clock"))
}
}
#[test]
fn fixed_clock_returns_its_instant() {
for t in [0, 1_300_819_380, -1, MAX_SAFE_INTEGER, -MAX_SAFE_INTEGER] {
assert_eq!(FixedClock(t).now(), Ok(t));
}
}
#[test]
fn fixed_clock_outside_the_safe_range_is_an_error() {
for t in [
MAX_SAFE_INTEGER + 1,
-MAX_SAFE_INTEGER - 1,
i64::MAX,
i64::MIN,
] {
assert_eq!(FixedClock(t).now(), Err(Error::InvalidClaim("clock")));
}
}
#[test]
fn now_checked_bounds_any_clock() {
assert_eq!(now_checked(&Raw(42)), Ok(42));
assert_eq!(now_checked(&Raw(MAX_SAFE_INTEGER)), Ok(MAX_SAFE_INTEGER));
assert_eq!(
now_checked(&Raw(MAX_SAFE_INTEGER + 1)),
Err(Error::InvalidClaim("clock"))
);
assert_eq!(
now_checked(&Raw(i64::MIN)),
Err(Error::InvalidClaim("clock"))
);
assert_eq!(now_checked(&Failing), Err(Error::InvalidClaim("clock")));
}
#[test]
fn clock_is_object_safe_send_and_sync() {
fn shared<T: Send + Sync + ?Sized>(_: &T) {}
let clock: &dyn Clock = &FixedClock(7);
shared(clock);
assert_eq!(clock.now(), Ok(7));
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
#[test]
fn system_clock_is_a_clock() {
fn is_clock<T: Clock + Default + Copy + core::fmt::Debug>() {}
is_clock::<SystemClock>();
assert_eq!(format!("{SystemClock:?}"), "SystemClock");
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
#[test]
fn system_clock_now_is_after_a_fixed_past_instant() {
let now = SystemClock.now();
assert!(
matches!(now, Ok(secs) if secs > 1_700_000_000 && secs <= MAX_SAFE_INTEGER),
"{now:?}"
);
}
const OTHER_SOURCES: &[(&str, &str)] = &[
("claims.rs", include_str!("claims.rs")),
("keyset.rs", include_str!("keyset.rs")),
("mod.rs", include_str!("mod.rs")),
("nested.rs", include_str!("nested.rs")),
("numeric_date.rs", include_str!("numeric_date.rs")),
("policy.rs", include_str!("policy.rs")),
("sign.rs", include_str!("sign.rs")),
("typ.rs", include_str!("typ.rs")),
("validate.rs", include_str!("validate.rs")),
("verify.rs", include_str!("verify.rs")),
];
const TIME_SOURCES: &[&str] = &["SystemTime", "UNIX_EPOCH", "Instant"];
#[test]
fn no_other_jwt_file_reads_a_platform_clock() {
for (name, source) in OTHER_SOURCES {
for needle in TIME_SOURCES {
assert!(
!source.contains(needle),
"{name} names {needle}: only clock.rs may read the time (ADR 0028 §5)"
);
}
}
}
#[test]
fn the_scan_covers_every_jwt_source_file() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src/jwt");
let mut on_disk: Vec<String> = std::fs::read_dir(dir)
.unwrap()
.map(|entry| entry.unwrap().file_name().into_string().unwrap())
.filter(|name| name != "clock.rs")
.collect();
on_disk.sort();
let mut scanned: Vec<String> = OTHER_SOURCES
.iter()
.map(|(name, _)| (*name).to_owned())
.collect();
scanned.sort();
assert_eq!(scanned, on_disk);
}
}