fix(C3): persist a monotonic time floor to defeat clock-rewind replay - #10
Open
d33mobile wants to merge 2 commits into
Open
fix(C3): persist a monotonic time floor to defeat clock-rewind replay#10d33mobile wants to merge 2 commits into
d33mobile wants to merge 2 commits into
Conversation
…eplay)
network/ntp.c's rollback_check() returned true before the first sync
("no floor before first sync"), and synced/last_sync_unix are RAM-only
statics wiped by every power cycle. An attacker who captured a valid
TOTP code could power-cycle the lock and spoof the first NTP response
with an old timestamp, rewinding the RTC and replaying the code
(ISSUES.md C3).
Persist a coarse "maximum unix time ever observed" watermark to
littlefs (hourly granularity to bound flash wear) via a new
storage_time_floor_get/set accessor pair (mirrors the wifi persistence
pattern). Load it in ntp_init() at boot and enforce new_time >=
persisted_floor in rollback_check() on EVERY sync, including the first -
the !synced early-return no longer bypasses the floor. apply_time()
advances the watermark whenever an accepted time crosses into a new
hourly bucket.
Host tests: harness_storage gains a time-floor roundtrip + pre-mount
refusal (asan/valgrind/coverage green). fuzz_ntp gains storage stubs and
a C3 first-sync-floor scenario; while wiring it up, repaired three
pre-existing bugs that had kept that harness from ever linking/running
(missing wifi_is_connected stub, NULL addr passed into ip_addr_cmp, and
make_ntp_packet leaving stratum=0 -> kiss-o-death reject). It now runs
clean (~5M execs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d33mobile
force-pushed
the
fix/c3-time-floor
branch
from
August 4, 2026 17:57
1072311 to
d9689aa
Compare
…ompiles Rebasing onto current master pulled in a latent build break: cmd_status declares `keys` inside the `if (commands_is_admin())` block but scrubs it via `secure_wipe(keys, sizeof(keys))` at function scope, so `keys` is undeclared there. asan_commands links serial/commands.c (not commands_system.c) so it never compiled this TU, but `make -C test coverage` compiles the whole first-party surface and fails here (`keys undeclared`) -- as does the real firmware build. Restore `keys` to function scope, matching the scrub's intent of always clearing the key DB from BSS (incl. the non-admin path where it stays zero-initialised). Same fix as PR hakierspejs#15 (M1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Finding (ISSUES.md — C3, Critical)
Since in-window TOTP replay protection is an accepted risk, this persisted floor is the only control stopping a code captured hours/days earlier from being replayed, so it is load-bearing.
Fix
storage_time_floor_get()/storage_time_floor_set()accessors persist a coarse "maximum unix time ever observed" value to a/timefloorlittlefs file (mirrors the existingstorage_wifi_*pattern). Granularity is hourly (TIME_FLOOR_GRANULARITY_S, rounded down) to bound flash wear to ≤24 writes/day.ntp_init()loads the watermark intopersisted_floor(storage is already mounted at that point —main.crunsstorage_init()beforentp_init(); in recovery mode the floor stays 0 and only the existing sane band applies).rollback_check()now rejectsnew_time < persisted_floorbefore the!syncedearly-return, so a spoofed first-sync response can no longer rewind the clock. The in-session monotonic floor is unchanged and still applies once synced.apply_time()persists a new coarse bucket whenever an accepted (and already floor-checked) time crosses into it, so it only ever moves forward.Host-test status
make -C test asan/valgrind/coverageand./ci --action=check(clang-format + shellcheck) all green.harness_storage.cgains a time-floor set/get roundtrip + pre-mount refusal assertions, exercising the new storage accessors under ASan/UBSan + valgrind.fuzz_ntp.cgains storage stubs and a C3 first-sync-floor scenario asserting that a below-watermark first sync is rejected (RTC untouched) while an above-watermark first sync is accepted and advances the coarse watermark. While wiring this up I repaired three pre-existing bugs that had keptfuzz_ntpfrom ever linking/running (missingwifi_is_connectedstub,NULLaddr passed into theip_addr_cmpderef, andmake_ntp_packetleavingstratum=0→ kiss-o-death reject). The harness now builds and runs clean (~5M execs, no crashes), so the new assertions actually execute.Firmware/hardware behaviour (actual flash persistence across a real power cycle) is verify-on-hw — no ARM toolchain /
PICO_SDK_PATHin this environment, so the RP2040 paths were validated by code review + the host tests above.Refs ISSUES.md C3. Focused on C3 only.
🤖 Generated with Claude Code