#!/usr/bin/env python3 """Deliver real signed fleet requests to the production CommonCLI clock branches. Receiver, codec and anti-replay storage run unchanged. Shared fleet-runtime fixtures provide hardware I/O and real OpenSSL Ed25519/AES/MAC interfaces. The CommonCLI setter and manual-clock hook ordering are extracted from production; only the RTC peripheral and reply date formatting are adapted for the host. """ from pathlib import Path import os import shutil import subprocess import tempfile import unittest from cpp_source import body from test_fleet_channel_runtime import FILESYSTEM, HARNESS as RUNTIME, MESH, PROFILE, REGION_MAP, UTILS ROOT = Path(__file__).resolve().parents[1] HARNESS = r''' #include // Date formatting is a host peripheral adapter; it never mutates the RTC. class DateTime { std::tm value_{}; public: explicit DateTime(uint32_t seconds) { const std::time_t epoch = seconds; const std::tm* value = std::gmtime(&epoch); assert(value); value_ = *value; } int hour() const { return value_.tm_hour; } int minute() const { return value_.tm_min; } int day() const { return value_.tm_mday; } int month() const { return value_.tm_mon + 1; } int year() const { return value_.tm_year + 1900; } }; @ATOI@ struct ManualClockCallbacks { Clock& clock; unsigned hooks = 0; uint32_t observed = 0; void onManualClockSet() { assert(clock.sets == hooks + 1); // The production hook follows the setter. ++hooks; observed = clock.getCurrentTime(); } }; class CommonCLI { Clock& rtc_; public: ManualClockCallbacks callbacks; ManualClockCallbacks* _callbacks = &callbacks; explicit CommonCLI(Clock& rtc) : rtc_(rtc), callbacks{rtc} {} Clock* getRTCClock() { return &rtc_; } void clockCommand(uint32_t sender_timestamp, const char* command, char* reply) { @CLOCK_BRANCHES@ else assert(false && "fleet clock dispatch accepted an unexpected command"); } }; struct Fixture { fs::FS disk; Mesh mesh; RadioProfileCLI profiles; FleetChannel fleet{&disk}; LocalIdentity publisher; CommonCLI cli{mesh.clock}; unsigned dispatched = 0; char reply[160] = {}; Fixture() { enroll(fleet, publisher); bind(fleet, mesh, profiles); } unsigned offer(Packet& packet, FleetChannel* receiver = nullptr) { fake_ms += 1000; // Admission checks, rather than rate throttling, decide each case. const unsigned before = dispatched; FleetChannel& destination = receiver ? *receiver : fleet; destination.receive(&packet, mesh); destination.service(mesh, profiles, "clock node", [&](uint32_t sequence, const char* command_text, char* response) { ++dispatched; assert(storage::readLE32(disk.files.at(Path).data() + 56) == sequence); assert(profiles.command); cli.clockCommand(sequence, command_text, response); strcpy(reply, response); }); return dispatched - before; } void unchanged(Packet& packet) { const uint32_t now = mesh.clock.now; const unsigned setters = mesh.clock.sets, hooks = cli.callbacks.hooks; const auto saved = disk.files.at(Path); assert(offer(packet) == 0); assert(mesh.clock.now == now && mesh.clock.sets == setters); assert(cli.callbacks.hooks == hooks && disk.files.at(Path) == saved); } }; int main(int argc, char** argv) { assert(argc == 2); const std::string scenario = argv[1]; Fixture f; const uint32_t initial = f.mesh.clock.now; if (scenario == "clock_read") { Packet packet = command(f.publisher, initial, "clock"); assert(f.offer(packet) == 1); assert(strstr(f.reply, "UTC") && !strstr(f.reply, "clock set")); assert(f.mesh.clock.now == initial && f.mesh.clock.sets == 0); assert(f.cli.callbacks.hooks == 0); assert(storage::readLE32(f.disk.files.at(Path).data() + 56) == initial); } else if (scenario == "sync_forward") { const uint32_t sender_time = initial + 5; Packet packet = command(f.publisher, sender_time, "clock sync"); assert(f.offer(packet) == 1); assert(f.mesh.clock.now == sender_time + 1); assert(f.mesh.clock.sets == 1 && f.cli.callbacks.hooks == 1); assert(f.cli.callbacks.observed == sender_time + 1); assert(!strncmp(f.reply, "OK - clock set:", 15)); assert(storage::readLE32(f.disk.files.at(Path).data() + 56) == sender_time); } else if (scenario == "sync_uint32_boundary") { // Fleet envelopes require a later expiry and cannot carry this timestamp. // Exercise the production CommonCLI branch directly to cover other CLI // transports as well: adding one to UINT32_MAX must never reset the RTC. const auto saved = f.disk.files.at(Path); f.mesh.clock.now = UINT32_MAX - 1; f.cli.clockCommand(UINT32_MAX, "clock sync", f.reply); assert(f.mesh.clock.now == UINT32_MAX - 1); assert(f.mesh.clock.sets == 0 && f.cli.callbacks.hooks == 0); assert(strstr(f.reply, "ERR")); assert(f.disk.files.at(Path) == saved && f.dispatched == 0); // The adjacent representable result remains a valid forward update. f.mesh.clock.now = UINT32_MAX - 2; f.cli.clockCommand(UINT32_MAX - 1, "clock sync", f.reply); assert(f.mesh.clock.now == UINT32_MAX); assert(f.mesh.clock.sets == 1 && f.cli.callbacks.hooks == 1); assert(f.cli.callbacks.observed == UINT32_MAX); assert(!strncmp(f.reply, "OK - clock set:", 15)); } else if (scenario == "explicit_forward") { const uint32_t requested = initial + 20; const std::string text = "time " + std::to_string(requested); Packet packet = command(f.publisher, initial, text.c_str()); assert(f.offer(packet) == 1); assert(f.mesh.clock.now == requested); assert(f.mesh.clock.sets == 1 && f.cli.callbacks.hooks == 1); assert(f.cli.callbacks.observed == requested); assert(!strncmp(f.reply, "OK - clock set:", 15)); // The signed request sequence, rather than the requested clock value, // remains the durable anti-replay identity. assert(storage::readLE32(f.disk.files.at(Path).data() + 56) == initial); } else if (scenario == "backward_and_equal") { Packet sync = command(f.publisher, initial - 1, "clock sync"); assert(f.offer(sync) == 1); assert(strstr(f.reply, "clock cannot go backwards")); Packet equal = command(f.publisher, initial, "clock sync"); assert(f.offer(equal) == 1); assert(strstr(f.reply, "clock cannot go backwards")); const std::string backward = "time " + std::to_string(initial - 1); Packet earlier = command(f.publisher, initial + 1, backward.c_str()); assert(f.offer(earlier) == 1); assert(strstr(f.reply, "clock cannot go backwards")); const std::string same = "time " + std::to_string(initial); Packet unchanged = command(f.publisher, initial + 2, same.c_str()); assert(f.offer(unchanged) == 1); assert(strstr(f.reply, "clock cannot go backwards")); assert(f.mesh.clock.now == initial && f.mesh.clock.sets == 0); assert(f.cli.callbacks.hooks == 0); } else if (scenario == "signatures_and_target") { const std::string text = "time " + std::to_string(initial + 20); Packet valid = command(f.publisher, initial, text.c_str()); uint8_t plain[184] = {}; const int size = Utils::MACThenDecrypt(channel_key, plain, valid.payload + 1, valid.payload_len - 1); assert(size > 3 && plain[2] > FleetCommand::SignatureSize); plain[3 + plain[2] - 1] ^= 1; Packet invalid_signature = raw(plain, 3 + plain[2]); const unsigned before = verify_calls; f.unchanged(invalid_signature); assert(verify_calls == before + 1); // MAC was valid; Ed25519 denied control. LocalIdentity stranger; Packet wrong_publisher = command(stranger, initial, text.c_str()); f.unchanged(wrong_publisher); Packet wrong_target = command(f.publisher, initial, text.c_str(), publicHex(stranger).c_str()); f.unchanged(wrong_target); assert(f.offer(valid) == 1); // Rejected requests did not reserve the sequence. assert(f.mesh.clock.now == initial + 20 && f.cli.callbacks.hooks == 1); } else if (scenario == "durable_replay") { Packet packet = command(f.publisher, initial + 5, "clock sync"); assert(f.offer(packet) == 1); f.unchanged(packet); FleetChannel rebooted(&f.disk); const unsigned before = f.dispatched; const auto saved = f.disk.files.at(Path); assert(f.offer(packet, &rebooted) == 0); assert(f.dispatched == before && f.mesh.clock.now == initial + 6); assert(f.mesh.clock.sets == 1 && f.cli.callbacks.hooks == 1); assert(f.disk.files.at(Path) == saved); } else if (scenario == "invalid_rtc") { Packet packet = command(f.publisher, initial + 5, "clock sync"); for (uint32_t invalid : {0u, FleetCommand::MinEpoch - 1}) { f.mesh.clock.now = invalid; f.unchanged(packet); } assert(f.dispatched == 0 && f.mesh.clock.sets == 0); f.mesh.clock.now = initial; assert(f.offer(packet) == 1); assert(f.mesh.clock.now == initial + 6 && f.cli.callbacks.hooks == 1); } else if (scenario == "expired_and_future") { Packet expired = signedUnchecked(f.publisher, initial - 300, initial - 1, "clock sync"); f.unchanged(expired); Packet future = command(f.publisher, initial + FleetCommand::MaxClockLead + 1, "clock sync"); f.unchanged(future); assert(f.dispatched == 0 && f.mesh.clock.sets == 0 && f.cli.callbacks.hooks == 0); Packet current = command(f.publisher, initial + 1, "clock sync"); assert(f.offer(current) == 1); assert(f.mesh.clock.now == initial + 2 && f.cli.callbacks.hooks == 1); } else if (scenario == "signed_invalid_clock_forms") { for (const char* text : {"get clock", "set clock", "clock sync extra", "time NaN", "time -1", "time 4294967296", "time 1735689599"}) { Packet packet = signedUnchecked(f.publisher, initial, initial + 300, text); f.unchanged(packet); } assert(f.dispatched == 0 && f.mesh.clock.sets == 0 && f.cli.callbacks.hooks == 0); } else assert(false); printf("Fleet clock scenario %s passed\n", argv[1]); } ''' class FleetClockControlTests(unittest.TestCase): @classmethod def setUpClass(cls): compiler = shutil.which("g++") or shutil.which("clang++") if compiler is None: raise unittest.SkipTest("host C++ compiler required") cls.work = tempfile.TemporaryDirectory(prefix="meshcore-fleet-clock-") work = Path(cls.work.name) (work / "helpers").mkdir() clock_getter = "uint32_t getCurrentTime() const{return now;}" assert MESH.count(clock_getter) == 1 mesh = MESH.replace(clock_getter, clock_getter + "\n unsigned sets=0;" " void setCurrentTime(uint32_t value){++sets;now=value;}") for name, content in { "Utils.h": UTILS, "FS.h": FILESYSTEM, "Mesh.h": mesh, "helpers/RadioProfileCLI.h": PROFILE, "helpers/RegionMap.h": REGION_MAP, "Packet.h": '#pragma once\n#include \n', "Arduino.h": '#pragma once\n#include \n', "Adafruit_LittleFS.h": r''' #pragma once #include using Adafruit_LittleFS = fs::FS; namespace Adafruit_LittleFS_Namespace {} #define FILE_O_READ 1 #define FILE_O_WRITE 2 #define LFS_ERR_NOENT -2 struct lfs_info {}; inline int lfs_stat(fs::FS* fs,const char* path,lfs_info*) {return fs->exists(path)?0:LFS_ERR_NOENT;} ''', "InternalFileSystem.h": '#pragma once\n#include \ninline fs::FS InternalFS;\n', }.items(): (work / name).write_text(content) sha = ROOT / "test/mocks/SHA256.h" (work / "SHA256.h").write_text(f''' #pragma once #define SHA256 SHA256Base #include "{sha}" #undef SHA256 class SHA256 : public SHA256Base {{ public: void finalize(void* out,size_t length) {{ SHA256Base::finalize(static_cast(out),length); }} void finalizeHMAC(const uint8_t* key,size_t size,void* out,size_t length) {{ SHA256Base::finalizeHMAC(key,size,static_cast(out),length); }} }}; ''') common = (ROOT / "src/helpers/CommonCLI.cpp").read_text() # The ESP-only *next* command opens its #if before the clock-sync # branch's closing brace. Separate that following command's guard from # the unconditional clock body without altering any clock statements. sync_start = common.index('if (memcmp(command, "clock sync", 10) == 0)') next_guard = '#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)' guard_start = common.index(next_guard, sync_start) assert common[guard_start + len(next_guard):].startswith( '\n } else if (memcmp(command, "start webconfig", 15) == 0') clock_source = common[:guard_start] + common[guard_start + len(next_guard):] branches = "\nelse ".join(body(clock_source, signature) for signature in ( 'if (memcmp(command, "clock sync", 10) == 0)', 'if (memcmp(command, "clock", 5) == 0)', 'if (memcmp(command, "time ", 5) == 0)', )) clock_fixture = HARNESS.replace("@ATOI@", body(common, "static uint32_t _atoi(")) clock_fixture = clock_fixture.replace("@CLOCK_BRANCHES@", branches) transport = (ROOT / "src/helpers/TransportKeyStore.cpp").read_text() transport_methods = "\n".join(body(transport, signature) for signature in ( "uint16_t TransportKey::calcTransportCode(", "bool TransportKey::isNull(", )) shared = RUNTIME[:RUNTIME.index("static unsigned apply(")] fixture = work / "fixture.cpp" fixture.write_text(shared.replace("@TRANSPORT_METHODS@", transport_methods) + clock_fixture) sanitizer_flags = (["-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-no-pie"] if os.environ.get("MESHCORE_FLEET_SANITIZERS") == "1" else []) cls.binaries = {} for backend in ("RP2040", "NRF52"): binary = work / ("fleet-clock-" + backend.lower()) sources = [str(ROOT / "src/helpers/FleetChannel.cpp"), str(ROOT / "src/helpers/FleetCommand.cpp")] if backend == "NRF52": sources.append(str(ROOT / "src/helpers/AtomicFileWriter.cpp")) built = subprocess.run([ compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-unused-function", "-Wno-unused-parameter", "-Wno-misleading-indentation", "-Wno-sign-compare", "-O2", *sanitizer_flags, "-D" + backend + "_PLATFORM", "-I", str(work), "-isystem", str(ROOT / "test/mocks"), "-I", str(ROOT / "src"), str(fixture), *sources, "-lcrypto", "-o", str(binary), ], capture_output=True, text=True, timeout=60) if built.returncode: cls.work.cleanup() raise AssertionError(backend + ":\n" + built.stdout + built.stderr) cls.binaries[backend] = binary @classmethod def tearDownClass(cls): cls.work.cleanup() def scenario(self, name): for backend, binary in self.binaries.items(): with self.subTest(backend=backend): result = subprocess.run([str(binary), name], capture_output=True, text=True, timeout=10) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) def test_clock_getter_preserves_rtc_and_durably_consumes_request(self): self.scenario("clock_read") def test_clock_sync_uses_signed_sender_utc_plus_one_and_calls_manual_hook(self): self.scenario("sync_forward") def test_clock_sync_uint32_boundary_never_wraps_rtc_or_fires_manual_hook(self): self.scenario("sync_uint32_boundary") def test_explicit_forward_time_calls_production_setter_and_hook(self): self.scenario("explicit_forward") def test_backward_and_equal_clock_requests_never_set_rtc_or_call_hook(self): self.scenario("backward_and_equal") def test_invalid_signature_publisher_and_target_cannot_change_clock(self): self.scenario("signatures_and_target") def test_replayed_clock_change_is_denied_before_and_after_receiver_reboot(self): self.scenario("durable_replay") def test_invalid_or_unset_rtc_has_no_signed_clock_bootstrap_exception(self): self.scenario("invalid_rtc") def test_expired_and_excessively_future_clock_envelopes_never_dispatch(self): self.scenario("expired_and_future") def test_valid_signature_never_grants_malformed_clock_commands(self): self.scenario("signed_invalid_clock_forms") if __name__ == "__main__": unittest.main()