Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 42 additions & 8 deletions ssh-key/src/certificate/unix_time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ use {
/// Maximum allowed value for a Unix timestamp.
pub const MAX_SECS: u64 = i64::MAX as u64;

/// Sentinel value meaning "no expiry" per OpenSSH PROTOCOL.certkeys.
/// When `valid_before` is set to this value, the certificate never expires.
pub const FOREVER_SECS: u64 = u64::MAX;

/// Unix timestamps as used in OpenSSH certificates.
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
pub(super) struct UnixTime {
/// Number of seconds since the Unix epoch
/// Number of seconds since the Unix epoch.
/// `u64::MAX` (FOREVER_SECS) is preserved as-is for round-trip encoding.
secs: u64,

/// System time corresponding to this Unix timestamp
/// System time corresponding to this Unix timestamp.
/// For FOREVER_SECS, this is capped at MAX_SECS to keep a valid SystemTime.
#[cfg(feature = "std")]
time: SystemTime,
}
Expand All @@ -29,10 +35,11 @@ impl UnixTime {
/// Create a new Unix timestamp.
///
/// `secs` is the number of seconds since the Unix epoch and must be less
/// than or equal to `i64::MAX`.
/// than or equal to `i64::MAX`, or `u64::MAX` (the OpenSSH "no expiry"
/// sentinel defined in PROTOCOL.certkeys).
#[cfg(not(feature = "std"))]
pub fn new(secs: u64) -> Result<Self> {
if secs <= MAX_SECS {
if secs == FOREVER_SECS || secs <= MAX_SECS {
Ok(Self { secs })
} else {
Err(Error::Time)
Expand All @@ -43,14 +50,22 @@ impl UnixTime {
///
/// This version requires `std` and ensures there's a valid `SystemTime`
/// representation with an infallible conversion (which also improves the
/// `Debug` output)
/// `Debug` output).
///
/// `u64::MAX` is the OpenSSH "no expiry" sentinel (PROTOCOL.certkeys) and
/// is accepted; its `SystemTime` representation is capped at `MAX_SECS`.
#[cfg(feature = "std")]
pub fn new(secs: u64) -> Result<Self> {
if secs > MAX_SECS {
// u64::MAX is OpenSSH's sentinel for "certificate never expires".
// Cap the SystemTime representation at MAX_SECS so it remains valid,
// but preserve the original secs value for encoding round-trips.
let time_secs = if secs == FOREVER_SECS { MAX_SECS } else { secs };

if time_secs > MAX_SECS {
return Err(Error::Time);
}

match UNIX_EPOCH.checked_add(Duration::from_secs(secs)) {
match UNIX_EPOCH.checked_add(Duration::from_secs(time_secs)) {
Some(time) => Ok(Self { secs, time }),
None => Err(Error::Time),
}
Expand Down Expand Up @@ -120,7 +135,7 @@ impl fmt::Debug for UnixTime {

#[cfg(test)]
mod tests {
use super::{MAX_SECS, UnixTime};
use super::{FOREVER_SECS, MAX_SECS, UnixTime};
use crate::Error;

#[test]
Expand All @@ -132,4 +147,23 @@ mod tests {
fn new_over_max_secs_returns_error() {
assert_eq!(UnixTime::new(MAX_SECS + 1), Err(Error::Time));
}

#[test]
fn new_with_forever_secs_is_ok() {
// u64::MAX is the OpenSSH "no expiry" sentinel and must be accepted
assert!(UnixTime::new(FOREVER_SECS).is_ok());
}

#[test]
fn forever_secs_preserves_raw_value() {
let t = UnixTime::new(FOREVER_SECS).unwrap();
assert_eq!(u64::from(t), FOREVER_SECS);
}

#[test]
fn forever_secs_greater_than_any_normal_timestamp() {
let forever = UnixTime::new(FOREVER_SECS).unwrap();
let now = UnixTime::new(MAX_SECS).unwrap();
assert!(forever > now);
}
}
Loading