#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CredentialTimestamp(u64);
impl CredentialTimestamp {
#[must_use]
pub const fn from_seconds(seconds: u64) -> Self {
Self(seconds)
}
#[must_use]
pub const fn as_seconds(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CredentialLifetimeError {
ZeroExpiresIn,
ZeroRefreshWindow,
RefreshWindowTooLarge,
TimestampOverflow,
}
impl_static_error!(CredentialLifetimeError,
Self::ZeroExpiresIn => "credential expires_in must be nonzero",
Self::ZeroRefreshWindow => "credential refresh window must be nonzero",
Self::RefreshWindowTooLarge => "credential refresh window consumes the complete lifetime",
Self::TimestampOverflow => "credential expiry timestamp overflows",
);
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum CredentialLifetimeState {
ClockRollback,
Fresh,
RefreshRequired,
Expired,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CredentialLifetime {
observed_at: CredentialTimestamp,
refresh_at: CredentialTimestamp,
expires_at: CredentialTimestamp,
expires_in: u32,
refresh_before: u32,
}
impl CredentialLifetime {
pub fn from_expires_in(
observed_at: CredentialTimestamp,
expires_in: u32,
refresh_before: u32,
) -> Result<Self, CredentialLifetimeError> {
if expires_in == 0 {
return Err(CredentialLifetimeError::ZeroExpiresIn);
}
if refresh_before == 0 {
return Err(CredentialLifetimeError::ZeroRefreshWindow);
}
if refresh_before >= expires_in {
return Err(CredentialLifetimeError::RefreshWindowTooLarge);
}
let expires_at = observed_at
.0
.checked_add(u64::from(expires_in))
.ok_or(CredentialLifetimeError::TimestampOverflow)?;
let refresh_at = expires_at
.checked_sub(u64::from(refresh_before))
.ok_or(CredentialLifetimeError::TimestampOverflow)?;
Ok(Self {
observed_at,
refresh_at: CredentialTimestamp(refresh_at),
expires_at: CredentialTimestamp(expires_at),
expires_in,
refresh_before,
})
}
#[must_use]
pub const fn observed_at(self) -> CredentialTimestamp {
self.observed_at
}
#[must_use]
pub const fn refresh_at(self) -> CredentialTimestamp {
self.refresh_at
}
#[must_use]
pub const fn expires_at(self) -> CredentialTimestamp {
self.expires_at
}
#[must_use]
pub const fn expires_in(self) -> u32 {
self.expires_in
}
#[must_use]
pub const fn refresh_before(self) -> u32 {
self.refresh_before
}
#[must_use]
pub const fn state_at(self, now: CredentialTimestamp) -> CredentialLifetimeState {
if now.0 < self.observed_at.0 {
CredentialLifetimeState::ClockRollback
} else if now.0 >= self.expires_at.0 {
CredentialLifetimeState::Expired
} else if now.0 >= self.refresh_at.0 {
CredentialLifetimeState::RefreshRequired
} else {
CredentialLifetimeState::Fresh
}
}
}
#[cfg(test)]
mod tests {
use super::{
CredentialLifetime, CredentialLifetimeError, CredentialLifetimeState, CredentialTimestamp,
};
#[test]
fn expires_in_uses_explicit_caller_time_and_exclusive_expiry() {
let lifetime = CredentialLifetime::from_expires_in(
CredentialTimestamp::from_seconds(1_000),
3_599,
300,
);
assert!(lifetime.is_ok());
let Ok(lifetime) = lifetime else {
unreachable!("credential lifetime fixture construction failed");
};
assert_eq!(lifetime.refresh_at().as_seconds(), 4_299);
assert_eq!(lifetime.expires_at().as_seconds(), 4_599);
assert_eq!(
lifetime.state_at(CredentialTimestamp::from_seconds(999)),
CredentialLifetimeState::ClockRollback
);
assert_eq!(
lifetime.state_at(CredentialTimestamp::from_seconds(4_298)),
CredentialLifetimeState::Fresh
);
assert_eq!(
lifetime.state_at(CredentialTimestamp::from_seconds(4_299)),
CredentialLifetimeState::RefreshRequired
);
assert_eq!(
lifetime.state_at(CredentialTimestamp::from_seconds(4_599)),
CredentialLifetimeState::Expired
);
}
#[test]
fn invalid_or_overflowing_lifetimes_fail_closed() {
let now = CredentialTimestamp::from_seconds(10);
assert_eq!(
CredentialLifetime::from_expires_in(now, 0, 0),
Err(CredentialLifetimeError::ZeroExpiresIn)
);
assert_eq!(
CredentialLifetime::from_expires_in(now, 60, 0),
Err(CredentialLifetimeError::ZeroRefreshWindow)
);
assert_eq!(
CredentialLifetime::from_expires_in(now, 60, 60),
Err(CredentialLifetimeError::RefreshWindowTooLarge)
);
assert_eq!(
CredentialLifetime::from_expires_in(CredentialTimestamp::from_seconds(u64::MAX), 2, 1,),
Err(CredentialLifetimeError::TimestampOverflow)
);
}
}