Skip to content

Commit 51bd0aa

Browse files
authored
ssh-key: allow u64::MAX as "no expiry" sentinel in UnixTime (#504)
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
1 parent 1218167 commit 51bd0aa

1 file changed

Lines changed: 42 additions & 8 deletions

File tree

ssh-key/src/certificate/unix_time.rs

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,19 @@ use {
1414
/// Maximum allowed value for a Unix timestamp.
1515
pub const MAX_SECS: u64 = i64::MAX as u64;
1616

17+
/// Sentinel value meaning "no expiry" per OpenSSH PROTOCOL.certkeys.
18+
/// When `valid_before` is set to this value, the certificate never expires.
19+
pub const FOREVER_SECS: u64 = u64::MAX;
20+
1721
/// Unix timestamps as used in OpenSSH certificates.
1822
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
1923
pub(super) struct UnixTime {
20-
/// Number of seconds since the Unix epoch
24+
/// Number of seconds since the Unix epoch.
25+
/// `u64::MAX` (FOREVER_SECS) is preserved as-is for round-trip encoding.
2126
secs: u64,
2227

23-
/// System time corresponding to this Unix timestamp
28+
/// System time corresponding to this Unix timestamp.
29+
/// For FOREVER_SECS, this is capped at MAX_SECS to keep a valid SystemTime.
2430
#[cfg(feature = "std")]
2531
time: SystemTime,
2632
}
@@ -29,10 +35,11 @@ impl UnixTime {
2935
/// Create a new Unix timestamp.
3036
///
3137
/// `secs` is the number of seconds since the Unix epoch and must be less
32-
/// than or equal to `i64::MAX`.
38+
/// than or equal to `i64::MAX`, or `u64::MAX` (the OpenSSH "no expiry"
39+
/// sentinel defined in PROTOCOL.certkeys).
3340
#[cfg(not(feature = "std"))]
3441
pub fn new(secs: u64) -> Result<Self> {
35-
if secs <= MAX_SECS {
42+
if secs == FOREVER_SECS || secs <= MAX_SECS {
3643
Ok(Self { secs })
3744
} else {
3845
Err(Error::Time)
@@ -43,14 +50,22 @@ impl UnixTime {
4350
///
4451
/// This version requires `std` and ensures there's a valid `SystemTime`
4552
/// representation with an infallible conversion (which also improves the
46-
/// `Debug` output)
53+
/// `Debug` output).
54+
///
55+
/// `u64::MAX` is the OpenSSH "no expiry" sentinel (PROTOCOL.certkeys) and
56+
/// is accepted; its `SystemTime` representation is capped at `MAX_SECS`.
4757
#[cfg(feature = "std")]
4858
pub fn new(secs: u64) -> Result<Self> {
49-
if secs > MAX_SECS {
59+
// u64::MAX is OpenSSH's sentinel for "certificate never expires".
60+
// Cap the SystemTime representation at MAX_SECS so it remains valid,
61+
// but preserve the original secs value for encoding round-trips.
62+
let time_secs = if secs == FOREVER_SECS { MAX_SECS } else { secs };
63+
64+
if time_secs > MAX_SECS {
5065
return Err(Error::Time);
5166
}
5267

53-
match UNIX_EPOCH.checked_add(Duration::from_secs(secs)) {
68+
match UNIX_EPOCH.checked_add(Duration::from_secs(time_secs)) {
5469
Some(time) => Ok(Self { secs, time }),
5570
None => Err(Error::Time),
5671
}
@@ -120,7 +135,7 @@ impl fmt::Debug for UnixTime {
120135

121136
#[cfg(test)]
122137
mod tests {
123-
use super::{MAX_SECS, UnixTime};
138+
use super::{FOREVER_SECS, MAX_SECS, UnixTime};
124139
use crate::Error;
125140

126141
#[test]
@@ -132,4 +147,23 @@ mod tests {
132147
fn new_over_max_secs_returns_error() {
133148
assert_eq!(UnixTime::new(MAX_SECS + 1), Err(Error::Time));
134149
}
150+
151+
#[test]
152+
fn new_with_forever_secs_is_ok() {
153+
// u64::MAX is the OpenSSH "no expiry" sentinel and must be accepted
154+
assert!(UnixTime::new(FOREVER_SECS).is_ok());
155+
}
156+
157+
#[test]
158+
fn forever_secs_preserves_raw_value() {
159+
let t = UnixTime::new(FOREVER_SECS).unwrap();
160+
assert_eq!(u64::from(t), FOREVER_SECS);
161+
}
162+
163+
#[test]
164+
fn forever_secs_greater_than_any_normal_timestamp() {
165+
let forever = UnixTime::new(FOREVER_SECS).unwrap();
166+
let now = UnixTime::new(MAX_SECS).unwrap();
167+
assert!(forever > now);
168+
}
135169
}

0 commit comments

Comments
 (0)