src.nth.io/

summaryrefslogtreecommitdiff
path: root/src/host.rs
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-07-28 17:38:06 -0500
committerLuke Hoersten <[email protected]>2026-07-28 17:38:06 -0500
commit29943e536f2be420a9d4042cd26d7b7eddf71438 (patch)
tree3083eafc75877243777a8257dd4a46e88ee8a3cf /src/host.rs
parent28a660e8bacac84523601f67547b2671058d9b20 (diff)
Address remaining audit findings: input hardening and cleanup
Security: - SNTP replies must echo a random nonce in their originate timestamp, and unsynchronized (LI=3) or invalid-stratum servers are rejected, so an off-path spoofer cannot defeat the clock-trust gate - The storage directory is created 0700 but an existing one is tightened only when it is provably ours (holds our artifacts or is empty); a misconfigured path like /tmp under root is left alone with a warning - Device-supplied vendor/product names are stripped of control characters at ingestion so a device cannot inject terminal escapes Cleanup: - ensure_fabric splits into decision logic plus bootstrap_fabric - read_device_names merges the two name readers; a From impl replaces the hand-mapped IdentityReport; run_sync uses map/transpose with an extracted manual_instant; pick_interface runs once; print_json escapes its fallback; device_name dedups the vendor/product join; identity_status ignores leftover .tmp blobs; dead parameters and repeated random-id code removed 36 unit tests, clippy clean.
Diffstat (limited to 'src/host.rs')
-rw-r--r--src/host.rs34
1 files changed, 32 insertions, 2 deletions
diff --git a/src/host.rs b/src/host.rs
index feb5a34..d3e871e 100644
--- a/src/host.rs
+++ b/src/host.rs
@@ -42,6 +42,14 @@ pub fn clock_is_ntp_synchronized() -> bool {
/// clock relative to the server (positive = local clock ahead). Uses the
/// request/response midpoint, so the error is bounded by half the round
/// trip, which is milliseconds against a threshold of a second.
+///
+/// The request carries a random transmit timestamp that the reply must echo
+/// in its originate field (RFC 4330 ยง5); this binds the answer to our
+/// request so an off-path packet or a stale reply is rejected. It cannot
+/// stop an on-path attacker, but the gate only decides whether to trust the
+/// *host's own* clock (the device is never set from the NTP value), so the
+/// worst an attacker gains is suppressing sync or blessing an already-wrong
+/// host clock.
fn sntp_offset(server: &str) -> Option<f64> {
let socket = UdpSocket::bind(("0.0.0.0", 0)).ok()?;
socket.set_read_timeout(Some(Duration::from_secs(2))).ok()?;
@@ -49,14 +57,29 @@ fn sntp_offset(server: &str) -> Option<f64> {
let mut request = [0u8; 48];
request[0] = 0b00_100_011; // LI 0, version 4, mode 3 (client)
+ // Random transmit timestamp (bytes 40..48) used as an anti-spoof nonce.
+ let mut nonce = [0u8; 8];
+ getrandom(&mut nonce);
+ request[40..48].copy_from_slice(&nonce);
let sent_at = unix_now();
socket.send(&request).ok()?;
let mut response = [0u8; 48];
let len = socket.recv(&mut response).ok()?;
let received_at = unix_now();
- if len < 48 || response[0] & 0x07 != 4 {
- // Not a server-mode reply.
+ if len < 48 {
+ return None;
+ }
+ let leap = response[0] >> 6;
+ let mode = response[0] & 0x07;
+ let stratum = response[1];
+ // Reject non-server replies, unsynchronized servers (LI=3), and
+ // kiss-of-death / invalid stratum (0, or 16+ = unsynchronized).
+ if mode != 4 || leap == 3 || !(1..=15).contains(&stratum) {
+ return None;
+ }
+ // The reply's originate timestamp (bytes 24..32) must echo our nonce.
+ if response[24..32] != nonce {
return None;
}
@@ -71,6 +94,13 @@ fn sntp_offset(server: &str) -> Option<f64> {
Some((sent_at + received_at) / 2.0 - server_time)
}
+/// Fills `buf` with random bytes via rs-matter's crypto RNG (already a
+/// dependency); a nonce only needs unpredictability, not the full clock.
+fn getrandom(buf: &mut [u8]) {
+ use rand::RngCore as _;
+ rand::thread_rng().fill_bytes(buf);
+}
+
fn unix_now() -> f64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)