From d3f43a98d9eab293df0bf235fb74e9e52c284b39 Mon Sep 17 00:00:00 2001 From: Georg von Zengen Date: Thu, 7 May 2026 23:17:18 +0200 Subject: [PATCH] ssh-key: accept u64::MAX as OpenSSH "no expiry" sentinel in UnixTime OpenSSH PROTOCOL.certkeys specifies that valid_before=0xffffffffffffffff (u64::MAX) means the certificate never expires. Previously UnixTime::new rejected this value because it exceeds MAX_SECS (i64::MAX), causing Certificate parsing to fail for any cert generated without an explicit validity window (e.g. ssh-keygen -s ca -h key.pub without -V). Add FOREVER_SECS=u64::MAX constant. In new(), cap its SystemTime representation at MAX_SECS to keep a valid SystemTime while preserving the raw secs value for round-trip encoding correctness. Fixes: #503 --- ssh-key/src/certificate/unix_time.rs | 50 +++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/ssh-key/src/certificate/unix_time.rs b/ssh-key/src/certificate/unix_time.rs index 0d31d2bf..165a42d7 100644 --- a/ssh-key/src/certificate/unix_time.rs +++ b/ssh-key/src/certificate/unix_time.rs @@ -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, } @@ -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 { - if secs <= MAX_SECS { + if secs == FOREVER_SECS || secs <= MAX_SECS { Ok(Self { secs }) } else { Err(Error::Time) @@ -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 { - 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), } @@ -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] @@ -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); + } }