src.nth.io/

summaryrefslogtreecommitdiff
path: root/src/time.rs
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-07-28 17:18:43 -0500
committerLuke Hoersten <[email protected]>2026-07-28 17:18:43 -0500
commit28a660e8bacac84523601f67547b2671058d9b20 (patch)
tree10dd1c7f4c9cde9e9a06d028cea33e145e95a542 /src/time.rs
parent07d4713f554b2ae2ccf4871f6be0590c129342b0 (diff)
Harden against untrusted input and reduce controller boilerplate
Security: - Epoch conversions no longer panic on out-of-range values: a device may report any u64 as its clock, so to_timestamp is fallible (rendered labeled-raw), from_timestamp clamps a pre-epoch host clock to zero (a Pi with no RTC reads 1970 early after boot), and delta arithmetic saturates. Fixes crashes in inspect and status - Identity Debug redacts the root CA private key so no stray {:?} leaks it - fsync the temp file before rename, so a power loss cannot surface an empty identity.json - Document in the README that device attestation is not verified, the NTP servers contacted, and the at-rest key Cleanup: - A MatterCtx extension trait replaces the repeated map_err/anyhow closure at every rs-matter call site with .ctx("what") - SyncOutcome::failed/skipped constructors collapse two large literals - read_our_fabric_entry shares the fabric-filtered read across the label, decommission, and inspect paths 36 unit tests, clippy clean.
Diffstat (limited to 'src/time.rs')
-rw-r--r--src/time.rs69
1 files changed, 53 insertions, 16 deletions
diff --git a/src/time.rs b/src/time.rs
index bedc1fa..6c38cc6 100644
--- a/src/time.rs
+++ b/src/time.rs
@@ -23,28 +23,40 @@ impl MatterMicros {
Self::from_timestamp(Timestamp::now())
}
- /// Panics on pre-2000 timestamps, which cannot be represented on the
- /// wire and cannot arise from a running host clock we already required
- /// to be NTP-synchronized.
+ /// A host clock before the Matter epoch (a Pi with no RTC reads 1970
+ /// early after boot) clamps to 0 rather than panicking; the NTP gate,
+ /// not this conversion, is what keeps a bad clock off a device.
pub fn from_timestamp(at: Timestamp) -> Self {
let micros = at.as_microsecond() - MATTER_EPOCH_UNIX_MICROS;
- Self(u64::try_from(micros).expect("timestamp precedes the Matter epoch"))
+ Self(u64::try_from(micros).unwrap_or(0))
}
- pub fn to_timestamp(self) -> Timestamp {
- Timestamp::from_microsecond(self.0 as i64 + MATTER_EPOCH_UNIX_MICROS)
- .expect("Matter timestamp out of jiff range")
+ /// `None` when the value is outside jiff's representable range: a device
+ /// is free to report any u64 as its clock, and a diagnostic read must
+ /// not panic on garbage.
+ pub fn to_timestamp(self) -> Option<Timestamp> {
+ i64::try_from(self.0)
+ .ok()
+ .and_then(|micros| micros.checked_add(MATTER_EPOCH_UNIX_MICROS))
+ .and_then(|micros| Timestamp::from_microsecond(micros).ok())
}
- /// Signed delta `self - other` in microseconds.
+ /// Signed delta `self - other` in microseconds, saturating so an
+ /// adversarial device value cannot overflow. i128 has ample headroom
+ /// for two u64 operands; the clamp only bites on nonsense inputs.
pub fn delta_micros(self, other: Self) -> i64 {
- self.0 as i64 - other.0 as i64
+ (self.0 as i128 - other.0 as i128).clamp(i64::MIN as i128, i64::MAX as i128) as i64
}
}
impl fmt::Display for MatterMicros {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.to_timestamp().fmt(f)
+ match self.to_timestamp() {
+ Some(timestamp) => timestamp.fmt(f),
+ // Out of representable range: show the raw value labeled, so a
+ // device reporting garbage is legible rather than a crash.
+ None => write!(f, "{}us-since-matter-epoch(out-of-range)", self.0),
+ }
}
}
@@ -76,10 +88,10 @@ impl ClockAssessment {
return Self::Unset;
};
let delta_micros = device.delta_micros(host);
- let shift_error = delta_micros - MATTER_EPOCH_UNIX_MICROS;
+ let shift_error = delta_micros.saturating_sub(MATTER_EPOCH_UNIX_MICROS);
Self::Offset {
delta_micros,
- epoch_shifted: shift_error.abs() <= EPOCH_SHIFT_DETECTION_WINDOW_MICROS,
+ epoch_shifted: shift_error.unsigned_abs() <= EPOCH_SHIFT_DETECTION_WINDOW_MICROS as u64,
}
}
@@ -92,7 +104,7 @@ impl ClockAssessment {
delta_micros,
epoch_shifted,
} => Some(if epoch_shifted {
- delta_micros - MATTER_EPOCH_UNIX_MICROS
+ delta_micros.saturating_sub(MATTER_EPOCH_UNIX_MICROS)
} else {
delta_micros
}),
@@ -207,17 +219,42 @@ mod tests {
MatterMicros::from_timestamp(ts("2000-01-01T00:00:00Z")).0,
0
);
- assert_eq!(MatterMicros(0).to_timestamp(), ts("2000-01-01T00:00:00Z"));
+ assert_eq!(
+ MatterMicros(0).to_timestamp(),
+ Some(ts("2000-01-01T00:00:00Z"))
+ );
assert_eq!(
MatterMicros(1_000_000).to_timestamp(),
- ts("2000-01-01T00:00:01Z")
+ Some(ts("2000-01-01T00:00:01Z"))
);
}
#[test]
fn conversion_round_trips_current_dates() {
let now = ts("2026-07-26T20:00:00.123456Z");
- assert_eq!(MatterMicros::from_timestamp(now).to_timestamp(), now);
+ assert_eq!(MatterMicros::from_timestamp(now).to_timestamp(), Some(now));
+ }
+
+ #[test]
+ fn garbage_device_values_do_not_panic() {
+ // A device may report any u64 as its clock; conversion and delta
+ // arithmetic must degrade, not crash.
+ let garbage = MatterMicros(u64::MAX);
+ assert_eq!(garbage.to_timestamp(), None);
+ assert!(garbage.to_string().contains("out-of-range"));
+ let host = MatterMicros::from_timestamp(ts("2026-07-26T20:00:00Z"));
+ let _ = garbage.delta_micros(host); // saturates, no overflow
+ let assessment = ClockAssessment::compare(Some(garbage), host);
+ let _ = assessment.effective_delta_micros();
+ let _ = assessment.to_string();
+ }
+
+ #[test]
+ fn pre_2000_host_clock_clamps_instead_of_panicking() {
+ assert_eq!(
+ MatterMicros::from_timestamp(ts("1970-01-01T00:00:00Z")).0,
+ 0
+ );
}
#[test]