diff options
| author | Luke Hoersten <[email protected]> | 2026-07-28 17:18:43 -0500 |
|---|---|---|
| committer | Luke Hoersten <[email protected]> | 2026-07-28 17:18:43 -0500 |
| commit | 28a660e8bacac84523601f67547b2671058d9b20 (patch) | |
| tree | 10dd1c7f4c9cde9e9a06d028cea33e145e95a542 /src/time.rs | |
| parent | 07d4713f554b2ae2ccf4871f6be0590c129342b0 (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.rs | 69 |
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] |
