use chrono::{DateTime, TimeDelta, Utc};
use trust_tasks_rs::FreshnessPolicy;
use trust_tasks_rs::freshness::{DEFAULT_EXPIRY_HORIZON, DEFAULT_SKEW};
use trust_tasks_rs::specs::trust_task_discovery::v0_4 as wire;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AcceptanceWindow {
pub max_age: TimeDelta,
pub clock_skew: TimeDelta,
pub expiry_horizon: Option<TimeDelta>,
}
pub const VTI_ACCEPTANCE_WINDOW: AcceptanceWindow = AcceptanceWindow {
max_age: super::ACCEPTANCE_WINDOW,
clock_skew: DEFAULT_SKEW,
expiry_horizon: Some(EXPIRY_HORIZON),
};
pub const EXPIRY_HORIZON: TimeDelta = DEFAULT_EXPIRY_HORIZON;
pub fn retain_until<P>(
policy: &FreshnessPolicy,
doc: &trust_tasks_rs::TrustTask<P>,
now: DateTime<Utc>,
) -> Option<DateTime<Utc>> {
let end = policy.record_expiry(doc, now)?;
Some(match doc.expires_at {
Some(expires_at) if end == expires_at => expires_at + policy.skew,
_ => end,
})
}
impl AcceptanceWindow {
pub fn freshness_policy(&self) -> FreshnessPolicy {
let policy = FreshnessPolicy::default()
.with_max_age(self.max_age)
.with_skew(self.clock_skew);
match self.expiry_horizon {
Some(horizon) => policy.with_expiry_horizon(horizon),
None => policy,
}
}
pub fn advertised(&self) -> Option<wire::AcceptanceWindow> {
let (max_age, clock_skew_seconds) = self.whole_seconds()?;
let expiry_horizon_seconds = self
.expiry_horizon
.and_then(|h| u64::try_from(h.num_seconds()).ok())
.and_then(std::num::NonZeroU64::new);
wire::AcceptanceWindow::builder()
.clock_skew_seconds(clock_skew_seconds)
.max_age_seconds(max_age)
.expiry_horizon_seconds(expiry_horizon_seconds)
.try_into()
.ok()
}
#[allow(deprecated)]
pub fn advertised_v0_3(
&self,
) -> Option<trust_tasks_rs::specs::trust_task_discovery::v0_3::AcceptanceWindow> {
let (max_age, clock_skew_seconds) = self.whole_seconds()?;
trust_tasks_rs::specs::trust_task_discovery::v0_3::AcceptanceWindow::builder()
.clock_skew_seconds(clock_skew_seconds)
.max_age_seconds(max_age)
.try_into()
.ok()
}
fn whole_seconds(&self) -> Option<(std::num::NonZeroU64, u64)> {
let max_age = u64::try_from(self.max_age.num_seconds()).ok()?;
let clock_skew_seconds = u64::try_from(self.clock_skew.num_seconds()).ok()?;
Some((std::num::NonZeroU64::new(max_age)?, clock_skew_seconds))
}
pub fn from_advertised(window: &wire::AcceptanceWindow) -> Self {
let secs = |s: u64| {
i64::try_from(s)
.ok()
.and_then(TimeDelta::try_seconds)
.unwrap_or(TimeDelta::MAX)
};
Self {
max_age: secs(window.max_age_seconds.get()),
clock_skew: secs(window.clock_skew_seconds),
expiry_horizon: window.expiry_horizon_seconds.map(|h| secs(h.get())),
}
}
pub fn past_max_age(&self, issued_at: DateTime<Utc>, now: DateTime<Utc>) -> bool {
issued_at
.checked_add_signed(self.max_age)
.is_some_and(|edge| now >= edge)
}
pub fn refuses(&self, issued_at: DateTime<Utc>, now: DateTime<Utc>) -> bool {
issued_at
.checked_add_signed(self.max_age)
.and_then(|t| t.checked_add_signed(self.clock_skew))
.is_some_and(|edge| now > edge)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vti_trn_047_the_vti_window_is_advertised_exactly() {
let wire = VTI_ACCEPTANCE_WINDOW.advertised().expect("advertisable");
assert_eq!(wire.max_age_seconds.get(), 600);
assert_eq!(wire.clock_skew_seconds, 60);
assert_eq!(
wire.expiry_horizon_seconds.map(|h| h.get()),
Some(30 * 24 * 60 * 60)
);
assert_eq!(
AcceptanceWindow::from_advertised(&wire),
VTI_ACCEPTANCE_WINDOW
);
}
#[test]
fn vti_trn_047_a_fractional_window_is_never_advertised_wider() {
let w = AcceptanceWindow {
max_age: TimeDelta::milliseconds(90_999),
clock_skew: TimeDelta::milliseconds(1_500),
expiry_horizon: Some(TimeDelta::milliseconds(3_600_999)),
};
let wire = w.advertised().expect("advertisable");
assert_eq!(wire.max_age_seconds.get(), 90);
assert_eq!(wire.clock_skew_seconds, 1);
assert_eq!(wire.expiry_horizon_seconds.map(|h| h.get()), Some(3_600));
let back = AcceptanceWindow::from_advertised(&wire);
assert!(back.max_age <= w.max_age && back.clock_skew <= w.clock_skew);
assert!(back.expiry_horizon <= w.expiry_horizon);
}
#[test]
fn vti_trn_047_a_sub_second_horizon_is_not_advertised() {
let w = AcceptanceWindow {
expiry_horizon: Some(TimeDelta::milliseconds(999)),
..VTI_ACCEPTANCE_WINDOW
};
assert_eq!(
w.advertised().expect("advertisable").expiry_horizon_seconds,
None
);
}
#[test]
fn vti_trn_047_the_v0_3_window_omits_the_horizon() {
let wire = VTI_ACCEPTANCE_WINDOW
.advertised_v0_3()
.expect("advertisable");
assert_eq!(wire.max_age_seconds.get(), 600);
assert_eq!(wire.clock_skew_seconds, 60);
}
#[test]
fn vti_ops_028_the_expiry_horizon_is_thirty_days() {
assert_eq!(EXPIRY_HORIZON, TimeDelta::days(30));
assert_eq!(
VTI_ACCEPTANCE_WINDOW.freshness_policy().expiry_horizon,
Some(EXPIRY_HORIZON)
);
}
#[test]
fn vti_ops_028_a_stated_expiry_replaces_the_maximum_age_up_to_the_horizon() {
let policy = VTI_ACCEPTANCE_WINDOW.freshness_policy();
let issued = chrono::SubsecRound::trunc_subsecs(Utc::now(), 0);
let doc = |expires: DateTime<Utc>| {
let mut d = trust_tasks_rs::TrustTask::new(
"urn:uuid:00000000-0000-4000-8000-000000000000".to_string(),
"https://trusttasks.org/spec/acl/list/0.1".parse().unwrap(),
serde_json::json!({}),
);
d.issued_at = Some(issued);
d.expires_at = Some(expires);
d
};
let day = doc(issued + TimeDelta::days(1));
assert!(
day.validate_freshness(issued + TimeDelta::hours(1), &policy)
.is_ok()
);
let year = doc(issued + TimeDelta::days(365));
assert!(year.validate_freshness(issued, &policy).is_ok());
assert!(
year.validate_freshness(issued + EXPIRY_HORIZON, &policy)
.is_ok()
);
assert!(
year.validate_freshness(
issued + EXPIRY_HORIZON + DEFAULT_SKEW + TimeDelta::seconds(1),
&policy
)
.is_err()
);
}
#[test]
fn vti_ops_029_the_record_outlasts_every_acceptance_end_by_the_skew() {
let policy = VTI_ACCEPTANCE_WINDOW.freshness_policy();
let issued = chrono::SubsecRound::trunc_subsecs(Utc::now(), 0);
let doc = |expires: Option<DateTime<Utc>>| {
let mut d = trust_tasks_rs::TrustTask::new(
"urn:uuid:00000000-0000-4000-8000-000000000000".to_string(),
"https://trusttasks.org/spec/acl/list/0.1".parse().unwrap(),
serde_json::json!({}),
);
d.issued_at = Some(issued);
d.expires_at = expires;
d
};
let skew = DEFAULT_SKEW;
assert_eq!(
retain_until(&policy, &doc(None), issued),
Some(issued + VTI_ACCEPTANCE_WINDOW.max_age + skew),
"no expiry: the maximum age"
);
let day = issued + TimeDelta::days(1);
assert_eq!(
retain_until(&policy, &doc(Some(day)), issued),
Some(day + skew),
"a stated expiry inside the horizon, past the maximum age"
);
let soon = issued + TimeDelta::minutes(2);
assert_eq!(
retain_until(&policy, &doc(Some(soon)), issued),
Some(soon + skew),
"a stated expiry inside the maximum age"
);
assert_eq!(
retain_until(&policy, &doc(Some(issued + TimeDelta::days(3650))), issued),
Some(issued + EXPIRY_HORIZON + skew),
"a stated expiry beyond the horizon"
);
}
#[test]
fn an_unstatable_window_is_not_advertised() {
let sub_second = AcceptanceWindow {
max_age: TimeDelta::milliseconds(500),
clock_skew: TimeDelta::zero(),
expiry_horizon: None,
};
assert!(sub_second.advertised().is_none());
assert!(sub_second.advertised_v0_3().is_none());
let negative_skew = AcceptanceWindow {
max_age: TimeDelta::minutes(1),
clock_skew: TimeDelta::seconds(-1),
expiry_horizon: None,
};
assert!(negative_skew.advertised().is_none());
}
#[test]
fn vti_trn_045_the_producer_predicates_agree_with_the_consumer_policy() {
let w = VTI_ACCEPTANCE_WINDOW;
let policy = w.freshness_policy();
assert_eq!(policy.max_age, Some(w.max_age));
assert_eq!(policy.skew, w.clock_skew);
let now = chrono::SubsecRound::trunc_subsecs(Utc::now(), 0);
let at_edge = now - w.max_age - w.clock_skew;
let past_edge = at_edge - TimeDelta::seconds(1);
let doc = |issued: DateTime<Utc>| {
let mut d = trust_tasks_rs::TrustTask::new(
"urn:uuid:00000000-0000-4000-8000-000000000000".to_string(),
"https://trusttasks.org/spec/acl/list/0.1".parse().unwrap(),
serde_json::json!({}),
);
d.issued_at = Some(issued);
d
};
assert!(doc(at_edge).validate_freshness(now, &policy).is_ok());
assert!(!w.refuses(at_edge, now));
assert!(doc(past_edge).validate_freshness(now, &policy).is_err());
assert!(w.refuses(past_edge, now));
assert!(w.past_max_age(now - w.max_age, now));
assert!(!w.past_max_age(now - w.max_age + TimeDelta::seconds(1), now));
}
}