Add signed private fleet controls for filtering and secondary radio

Enroll a private channel and publisher public key on repeaters and room servers. Local Companion fleet sends sign targeted or whole-fleet commands, with expiry, durable replay protection, and a restricted command allowlist. Preserve exact transmitted-reply barriers for secondary-radio changes, bound validation and replies, and qualify crypto, persistence, transport scope, role permissions, and compact-profile exclusions with regression tests.
This commit is contained in:
mikecarper committed 2026-10-08 16:45:19 -07:00
1 parent a14bbdc58f
commit d83fa7cf8e
27 files changed
+3028 -2

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
#pragma once
#ifndef MESH_ENABLE_FLEET_CONTROL
#define MESH_ENABLE_FLEET_CONTROL 1
#endif
#include <cassert>
#include <cstddef>
#include <cstdint>
// These narrower dispatch fixtures retain the complete production fleet branch
// but have no channel peripheral. The separate companion fleet suite compiles
// the real codec with real Ed25519 and exercises packet admission end to end.
#ifndef MAX_GROUP_CHANNELS
#define MAX_GROUP_CHANNELS 4
#endif
#ifndef PUB_KEY_SIZE
#define PUB_KEY_SIZE 32
#endif
#ifndef OUT_PATH_UNKNOWN
#define OUT_PATH_UNKNOWN 255
#endif
namespace mesh {
struct FleetCommand {
static constexpr uint16_t DataType = 0xFF01;
static constexpr size_t KeySize = 16, TargetSize = 16, MaxPayloadLength = 165;
static constexpr uint32_t MinEpoch = 1735689600UL, MaxLifetime = 600;
static bool parseTarget(const char*, uint8_t*) { return false; }
static bool commandAllowed(const char*) { return false; }
static bool privateKeyAllowed(const uint8_t*) { return false; }
template<class Signer>
static size_t encode(const Signer&, const uint8_t*, uint32_t, uint32_t,
const uint8_t*, const char*, uint8_t*, size_t) {
assert(false && "fleet encoding belongs in the dedicated real-codec suite");
return 0;
}
};
}
struct FleetFixtureChannel { uint8_t secret[32] = {}; };
struct ChannelDetails { FleetFixtureChannel channel; char name[32] = {}; };
struct FleetFixtureClock { uint32_t getCurrentTime() const { return 0; } };
struct CompanionFleetFixture {
bool getChannel(int, ChannelDetails&) { return false; }
FleetFixtureClock* getRTCClock() {
static FleetFixtureClock clock;
return &clock;
}
bool sendGroupData(FleetFixtureChannel&, uint8_t*, uint8_t, uint16_t,
const uint8_t*, int) {
assert(false && "fleet packet admission belongs in its dedicated suite");
return false;
}
};
+307
View File
@@ -0,0 +1,307 @@
#!/usr/bin/env python3
"""Execute the real Companion fleet-send CLI, preserving its client boundary.
Only channel storage and packet admission are mocked here. Protocol encoding,
target hashing and permission checks come from FleetCommand.cpp; OpenSSL
supplies real Ed25519 signing and verification through the Identity interface.
"""
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "examples/companion_radio/MyMesh.cpp"
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <helpers/FleetCommand.h>
#include <openssl/evp.h>
namespace mesh {
Identity::Identity() { memset(pub_key, 0, sizeof(pub_key)); }
LocalIdentity::LocalIdentity() : Identity() {
memset(prv_key, 0xAA, 32);
EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, prv_key, 32);
assert(key);
size_t length = sizeof(pub_key);
assert(EVP_PKEY_get_raw_public_key(key, pub_key, &length) == 1);
assert(length == sizeof(pub_key));
memcpy(prv_key + 32, pub_key, sizeof(pub_key));
EVP_PKEY_free(key);
}
void LocalIdentity::sign(uint8_t* signature, const uint8_t* data, int length) const {
EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, prv_key, 32);
EVP_MD_CTX* context = EVP_MD_CTX_new();
assert(key && context);
assert(EVP_DigestSignInit(context, nullptr, nullptr, nullptr, key) == 1);
size_t size = SIGNATURE_SIZE;
assert(EVP_DigestSign(context, signature, &size, data, length) == 1);
assert(size == SIGNATURE_SIZE);
EVP_MD_CTX_free(context); EVP_PKEY_free(key);
}
bool Identity::verify(const uint8_t* signature, const uint8_t* data, int length) const {
EVP_PKEY* key = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, pub_key, sizeof(pub_key));
EVP_MD_CTX* context = EVP_MD_CTX_new();
assert(key && context);
assert(EVP_DigestVerifyInit(context, nullptr, nullptr, nullptr, key) == 1);
const bool valid = EVP_DigestVerify(context, signature, SIGNATURE_SIZE, data, length) == 1;
EVP_MD_CTX_free(context); EVP_PKEY_free(key);
return valid;
}
}
#define MAX_GROUP_CHANNELS 4
#define OUT_PATH_UNKNOWN 255
struct GroupChannel {
uint8_t secret[32] = {};
uint8_t tx_radio = 0;
};
struct ChannelDetails { GroupChannel channel; char name[32] = {}; };
struct Clock { uint32_t now = mesh::FleetCommand::MinEpoch + 1000;
uint32_t getCurrentTime() const { return now; } };
class MyMesh {
public:
mesh::LocalIdentity self_id;
Clock clock;
ChannelDetails channels[MAX_GROUP_CHANNELS];
bool queue_ok = true;
unsigned queued = 0;
uint8_t sent_policy = 0;
uint16_t sent_type = 0;
uint8_t sent[mesh::FleetCommand::MaxPayloadLength] = {};
size_t sent_length = 0;
MyMesh() {
strcpy(channels[1].name, "private fleet");
memset(channels[1].channel.secret, 0x21, mesh::FleetCommand::KeySize);
channels[1].channel.tx_radio = 2;
}
bool getChannel(int index, ChannelDetails& out) {
if (index < 0 || index >= MAX_GROUP_CHANNELS) return false;
out = channels[index]; return true;
}
Clock* getRTCClock() { return &clock; }
bool sendGroupData(GroupChannel& channel, uint8_t* path, uint8_t path_length,
uint16_t type, const uint8_t* data, int length) {
assert(path == nullptr && path_length == OUT_PATH_UNKNOWN);
assert(length > 0 && size_t(length) <= sizeof(sent));
if (!queue_ok) return false;
sent_policy = channel.tx_radio; sent_type = type;
memcpy(sent, data, length); sent_length = length; ++queued;
return true;
}
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply);
};
@METHOD@
static void rejected(MyMesh& node, const char* text, uint32_t remote = 0) {
char reply[160] = {};
const unsigned before = node.queued;
assert(node.handleCommand(text, remote, reply));
assert(!strncmp(reply, "Error:", 6));
assert(before == node.queued);
}
static std::string fullKey(uint8_t value) {
char hex[65];
for (unsigned i = 0; i < 32; ++i) sprintf(hex + i * 2, "%02x", value);
return hex;
}
int main(int argc, char** argv) {
assert(argc == 2);
const std::string scenario = argv[1];
MyMesh node; char reply[160] = {};
if (scenario == "local_boundary") {
assert(!node.handleCommand(nullptr, 0, reply));
assert(!node.handleCommand("fleet send 1 all set radio2 off", 0, nullptr));
assert(!node.handleCommand("fleetish", 0, reply));
rejected(node, "fleet send 1 all set radio2 off", 1);
rejected(node, "fleet", 1);
rejected(node, "fleet send 1 all reboot");
rejected(node, "fleet send 1 all get prv.key");
rejected(node, "fleet send 1 all fleet send 1 all set radio2 off");
assert(node.handleCommand(" \tAB| fleet send 1 all set radio2 off", 0, reply));
assert(!strncmp(reply, "AB|OK - fleet command queued; seq=", 32));
assert(node.queued == 1 && node.sent_type == mesh::FleetCommand::DataType);
assert(node.sent_policy == 2); // The configured radio2 channel route survives.
} else if (scenario == "syntax") {
for (const char* command : {"fleet", "fleet send", "fleet sender 1 all set radio2 off",
"fleet send -1 all set radio2 off", "fleet send 4 all set radio2 off",
"fleet send 4294967296 all set radio2 off", "fleet send 1x all set radio2 off",
"fleet send 1 all", "fleet send 1 a set radio2 off",
"fleet send 1 111111111111 set radio2 off", "fleet send 1 all get password",
"fleet send 1 all set radio2 off\nreboot"}) rejected(node, command);
rejected(node, ("fleet send 1 " + std::string(65, '1') + " set radio2 off").c_str());
rejected(node, ("fleet send 1 all set flood.filter " + std::string(150, '1')).c_str());
assert(node.handleCommand("fleet\tsend\t1\tall\tset radio2 off", 0, reply));
assert(!strncmp(reply, "OK - fleet command queued", 25));
} else if (scenario == "private_channel") {
rejected(node, "fleet send 0 all set radio2 off");
memset(node.channels[1].channel.secret, 0, 32);
rejected(node, "fleet send 1 all set radio2 off");
const uint8_t public_key[16] = {0x8b,0x33,0x87,0xe9,0xc5,0xcd,0xea,0x6a,
0xc9,0xe5,0xed,0xba,0xa1,0x15,0xcd,0x72};
memcpy(node.channels[1].channel.secret, public_key, sizeof(public_key));
strcpy(node.channels[1].name, "renamed public");
rejected(node, "fleet send 1 all set radio2 off");
memset(node.channels[1].channel.secret, 0x21, 32);
rejected(node, "fleet send 1 all set radio2 off");
memset(node.channels[1].channel.secret + 16, 0, 16);
strcpy(node.channels[1].name, "Public"); // Names are not key authorization.
assert(node.handleCommand("fleet send 1 all set radio2 off", 0, reply));
assert(node.queued == 1);
} else if (scenario == "queue_sequence") {
node.queue_ok = false;
rejected(node, "fleet send 1 all set radio2 off");
node.queue_ok = true;
assert(node.handleCommand("fleet send 1 all set radio2 off", 0, reply));
assert(node.queued == 1);
rejected(node, "fleet send 1 all set radio2 off");
++node.clock.now;
assert(node.handleCommand("fleet send 1 all set radio2 off", 0, reply));
assert(node.queued == 2);
--node.clock.now;
rejected(node, "fleet send 1 all set radio2 off");
} else if (scenario == "clock") {
node.clock.now = 0;
rejected(node, "fleet send 1 all set radio2 off");
node.clock.now = mesh::FleetCommand::MinEpoch - 1;
rejected(node, "fleet send 1 all set radio2 off");
node.clock.now = UINT32_MAX;
rejected(node, "fleet send 1 all set radio2 off");
node.clock.now = mesh::FleetCommand::MinEpoch;
assert(node.handleCommand("fleet send 1 all set radio2 off", 0, reply));
assert(node.queued == 1);
} else if (scenario == "signed_target") {
const std::string command = "fleet send 1 " + fullKey(0x42)
+ " set tempradio2 910.5,500,5,5,rxtx,1";
assert(node.handleCommand(command.c_str(), 0, reply));
assert(node.queued == 1);
uint8_t recipient[32]; memset(recipient, 0x42, sizeof(recipient));
mesh::FleetCommand::Decoded decoded;
assert(mesh::FleetCommand::decode(node.self_id, node.channels[1].channel.secret,
node.sent, node.sent_length, node.clock.now, recipient, decoded));
assert(decoded.sequence == node.clock.now);
assert(decoded.expires == node.clock.now + mesh::FleetCommand::MaxLifetime);
assert(!strcmp(decoded.command, "set tempradio2 910.5,500,5,5,rxtx,1"));
recipient[31] ^= 1;
assert(!mesh::FleetCommand::decode(node.self_id, node.channels[1].channel.secret,
node.sent, node.sent_length, node.clock.now, recipient, decoded));
} else assert(false);
printf("Companion fleet scenario %s passed\n", argv[1]);
}
'''
class CompanionFleetControlTests(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.compiler = compiler
source = SOURCE.read_text()
function = body(source, "bool MyMesh::handleCommand(const char* command,")
# Retain the real NULL, whitespace and request-prefix handling too.
prefix = function[:function.index("#if COMPANION_FEATURE_READER")]
fleet_start = function.rindex("#if MESH_ENABLE_FLEET_CONTROL", 0,
function.index('if (!strncmp(command, "fleet", 5)'))
fleet_end = function.index(" if (_radio_profiles.handle", fleet_start)
# Keep the actual compile guard as well as its complete branch so
# compact profiles prove that none of the fleet dependencies survive.
cls.production = prefix + function[fleet_start:fleet_end] + "\n return false;\n}\n"
cls.work = tempfile.TemporaryDirectory(prefix="meshcore-fleet-companion-")
work = Path(cls.work.name)
cls.binary = work / "companion-fleet"
fixture = work / "fixture.cpp"
fixture.write_text(HARNESS.replace("@METHOD@", cls.production))
result = subprocess.run([
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-O2",
"-fstack-usage", "-isystem", str(ROOT / "test/mocks"),
"-I", str(ROOT / "src"), str(fixture),
str(ROOT / "src/helpers/FleetCommand.cpp"), "-lcrypto", "-o", str(cls.binary),
], capture_output=True, text=True, timeout=60)
if result.returncode:
cls.work.cleanup()
raise AssertionError(result.stdout + result.stderr)
@classmethod
def tearDownClass(cls):
cls.work.cleanup()
def scenario(self, name):
result = subprocess.run([str(self.binary), name], capture_output=True,
text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_local_client_boundary_and_channel_tx_route(self):
self.scenario("local_boundary")
def test_invalid_syntax_targets_and_forbidden_commands(self):
self.scenario("syntax")
def test_private_128_bit_key_required_even_if_channel_is_renamed(self):
self.scenario("private_channel")
def test_queue_failure_does_not_consume_sequence_and_success_does(self):
self.scenario("queue_sequence")
def test_invalid_clock_and_timestamp_overflow_never_queue(self):
self.scenario("clock")
def test_production_encode_targets_full_key_and_sets_bounded_expiry(self):
self.scenario("signed_target")
def test_disabled_profiles_require_no_sender_peripherals_or_crypto_link(self):
harness = r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <helpers/FleetCommand.h>
static_assert(MESH_ENABLE_FLEET_CONTROL == 0, "compact profile must disable fleet");
// No channels, clock, signing identity, codec definitions or sendGroupData.
class MyMesh {
public:
bool handleCommand(const char*, uint32_t, char*);
};
@METHOD@
int main() {
MyMesh node;
char reply[160] = {};
assert(!node.handleCommand("fleet send 1 all set radio2 off", 0, reply));
assert(!node.handleCommand("fleet send 1 all set radio2 off", 1, reply));
assert(!node.handleCommand("AB|fleet send 1 all set radio2 off", 0, reply));
}
'''
work = Path(self.work.name)
source = work / "disabled.cpp"
source.write_text(harness.replace("@METHOD@", self.production))
for profile, flags in (("stm32-default", ["-DSTM32_PLATFORM=1"]),
("explicit-disable", ["-DMESH_ENABLE_FLEET_CONTROL=0"])):
with self.subTest(profile=profile):
executable = work / profile
built = subprocess.run([
self.compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-isystem", str(ROOT / "test/mocks"),
"-I", str(ROOT / "src"), *flags, str(source), "-o", str(executable),
], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
result = subprocess.run([str(executable)], capture_output=True,
text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
if __name__ == "__main__":
unittest.main()
@@ -18,6 +18,7 @@ HARNESS = r'''
#include <helpers/RadioProfileCLI.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/radiolib/RXPowerSaving.h>
#include "companion_fleet_dispatch.h"
#include <sys/stat.h>
#include <limits>
#define constrain(value,low,high) ((value)<(low)?(low):((value)>(high)?(high):(value)))
@@ -57,7 +58,8 @@ struct Radio : mesh::Radio {
};
struct Sensors { double node_lat=0,node_lon=0; } sensors;
@COMMAND_FILTER@
struct MyMesh {
struct MyMesh : CompanionFleetFixture {
uint8_t self_id = 0; // Signing is unreachable in this radio-only fixture.
CompanionNodePrefs _prefs;
DataStore store;
DataStore* _store=&store;
@@ -293,6 +295,7 @@ public:
build = subprocess.run([compiler,'-std=c++17','-Wall','-Wextra',
'-Wno-unused-parameter','-Wno-sign-compare','-Wno-reorder',
'-DESP32_PLATFORM=1',*flags,'-I',str(work),
'-I',str(ROOT/'test/fixtures'),
'-I',str(ROOT/'test/fixtures/radio_profiles/mocks'),
'-I',str(ROOT/'test/mocks'),'-I',str(ROOT/'src'),
'-I',str(ROOT/'src/helpers'),'-I',str(ROOT),str(work/'test.cpp'),
+689
View File
@@ -0,0 +1,689 @@
#!/usr/bin/env python3
"""Execute production fleet receiver/storage/crypto boundaries with host mocks.
FleetChannel.cpp and FleetCommand.cpp are compiled unchanged. Only hardware,
packet admission, clock and filesystem I/O are mocked. Ed25519 signatures and
SHA-256 use real implementations; AES encryption/MAC are supplied by OpenSSL.
The real RP2040 ContactFileTransaction and nRF52 AtomicFileWriter validate and
publish persisted state using their actual platform-specific implementations.
"""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
UTILS = r'''
#pragma once
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <cstdio>
#include <cassert>
#include <SHA256.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#define PUB_KEY_SIZE 32
#define PRV_KEY_SIZE 64
#define SIGNATURE_SIZE 64
#define PATH_HASH_SIZE 1
#define CIPHER_MAC_SIZE 2
#define CIPHER_BLOCK_SIZE 16
inline unsigned hash_calls = 0, decrypt_calls = 0, verify_calls = 0, last_jitter_max = 0;
namespace mesh {
class RNG { public: uint32_t nextInt(uint32_t low, uint32_t high) {
assert(low == 500 && (high == 60500 || high == 15500 || high == 1500)); last_jitter_max=high; return 1000;
}};
class Utils { public:
static void sha256(uint8_t* out, size_t count, const uint8_t* data, int length) {
++hash_calls; SHA256 hash; hash.update(data, length); hash.finalize(out, count);
}
static void toHex(char* out, const uint8_t* data, size_t length) {
for (size_t i = 0; i < length; ++i) sprintf(out + 2*i, "%02x", data[i]);
out[2*length] = 0;
}
static bool fromHex(uint8_t* out, int length, const char* hex) {
if (strlen(hex) != size_t(2*length)) return false;
for (int i=0;i<length;++i) {
auto digit=[](char c) { return c>='0'&&c<='9'?c-'0':c>='a'&&c<='f'?c-'a'+10:
c>='A'&&c<='F'?c-'A'+10:-1; };
int a=digit(hex[2*i]), b=digit(hex[2*i+1]); if(a<0||b<0) return false;
out[i]=uint8_t(a*16+b);
} return true;
}
static int encryptThenMAC(const uint8_t* key, uint8_t* out, const uint8_t* data, int size) {
uint8_t padded[256] = {}; const int rounded = (size+15)/16*16;
assert(rounded <= int(sizeof(padded))); memcpy(padded,data,size);
EVP_CIPHER_CTX* ctx=EVP_CIPHER_CTX_new(); assert(ctx);
assert(EVP_EncryptInit_ex(ctx,EVP_aes_128_ecb(),nullptr,key,nullptr));
EVP_CIPHER_CTX_set_padding(ctx,0); int produced=0, tail=0;
assert(EVP_EncryptUpdate(ctx,out+CIPHER_MAC_SIZE,&produced,padded,rounded));
assert(EVP_EncryptFinal_ex(ctx,out+CIPHER_MAC_SIZE+produced,&tail));
EVP_CIPHER_CTX_free(ctx); assert(produced+tail==rounded);
uint8_t mac[32]; unsigned mac_len=0;
assert(HMAC(EVP_sha256(),key,32,out+CIPHER_MAC_SIZE,rounded,mac,&mac_len));
memcpy(out,mac,CIPHER_MAC_SIZE); return rounded+CIPHER_MAC_SIZE;
}
static int MACThenDecrypt(const uint8_t* key,uint8_t* out,const uint8_t* data,int size) {
++decrypt_calls; int cipher_size=size-CIPHER_MAC_SIZE;
if(cipher_size<=0 || cipher_size%16) return 0;
uint8_t mac[32]; unsigned mac_len=0;
assert(HMAC(EVP_sha256(),key,32,data+CIPHER_MAC_SIZE,cipher_size,mac,&mac_len));
if(memcmp(mac,data,CIPHER_MAC_SIZE)) return 0;
EVP_CIPHER_CTX* ctx=EVP_CIPHER_CTX_new(); assert(ctx);
assert(EVP_DecryptInit_ex(ctx,EVP_aes_128_ecb(),nullptr,key,nullptr));
EVP_CIPHER_CTX_set_padding(ctx,0); int produced=0,tail=0;
assert(EVP_DecryptUpdate(ctx,out,&produced,data+CIPHER_MAC_SIZE,cipher_size));
assert(EVP_DecryptFinal_ex(ctx,out+produced,&tail)); EVP_CIPHER_CTX_free(ctx);
return produced+tail;
}
};
}
'''
FILESYSTEM = r'''
#pragma once
#include <map>
#include <vector>
#include <string>
#include <memory>
#include <cstring>
#include <algorithm>
namespace fs { class FS; }
class File {
fs::FS* owner_=nullptr; std::string path_; size_t cursor_=0; bool live_=false;
public:
File() = default;
File(fs::FS* owner,const char* path,bool live):owner_(owner),path_(path),live_(live){}
explicit File(fs::FS& owner):owner_(&owner){}
bool open(const char* path,int flags);
explicit operator bool() const { return live_; }
bool isDirectory() const { return false; }
size_t size() const;
size_t read(uint8_t* data,size_t length);
size_t write(const uint8_t* data,size_t length);
void flush() {}
void close() {live_=false;}
};
namespace fs {
class FS { public:
std::map<std::string,std::vector<uint8_t>> files;
bool fail_open=false, fail_read=false, fail_write=false, fail_rename=false;
unsigned reads=0,writes=0,renames=0;
std::string fail_rename_from;
void _lockFS() {} void _unlockFS() {} FS* _getFS(){return this;}
bool exists(const char* path) {return files.count(path)!=0;}
bool remove(const char* path) {return files.erase(path)!=0;}
bool mkdir(const char*) {return true;}
bool rename(const char* old,const char* next) {
++renames; if(fail_rename || fail_rename_from==old || !exists(old)) return false;
files[next]=std::move(files[old]);files.erase(old);return true;
}
File open(const char* path,const char* mode="r") {
if(!path||fail_open) return {};
if(mode[0]=='w') {files[path].clear();return File(this,path,true);}
return File(this,path,exists(path));
}
};
}
inline bool File::open(const char* path,int flags){
if(!owner_)return false;*this=owner_->open(path,flags==2?"w":"r");return live_;
}
inline size_t File::size() const {return live_?owner_->files[path_].size():0;}
inline size_t File::read(uint8_t* data,size_t length) {
++owner_->reads; if(!live_||owner_->fail_read) return 0;
auto& bytes=owner_->files[path_];size_t n=std::min(length,bytes.size()-cursor_);
memcpy(data,bytes.data()+cursor_,n);cursor_+=n;return n;
}
inline size_t File::write(const uint8_t* data,size_t length) {
++owner_->writes; if(!live_||owner_->fail_write) return 0;
auto& bytes=owner_->files[path_];bytes.insert(bytes.end(),data,data+length);return length;
}
'''
MESH = r'''
#pragma once
#include <Identity.h>
#include <FS.h>
#include <vector>
#include <functional>
#define MAX_PACKET_PAYLOAD 184
#define PAYLOAD_TYPE_GRP_TXT 5
#define PAYLOAD_TYPE_GRP_DATA 6
#define FLOOD_RETRY_POLICY_DENY 1
inline uint32_t fake_ms = 1000;
inline uint32_t millis() {return fake_ms;}
namespace mesh {
struct Packet {
uint8_t type=PAYLOAD_TYPE_GRP_DATA,payload[MAX_PACKET_PAYLOAD]={},flood_retry_policy=0;
uint16_t payload_len=0,transport_codes[2]={};
uint32_t radio_generation=0;uint8_t radio_profile=0,path_hash_size=1;
bool scoped=false,radio_reply=false,radio_bound=false;
uint8_t getPayloadType() const{return type;}
uint8_t getPathHashSize() const{return path_hash_size;}
bool hasTransportCodes() const{return scoped;}
};
struct GroupChannel {uint8_t hash[PATH_HASH_SIZE]={},secret[32]={};};
struct Clock {uint32_t now=1735689600UL+1000;
uint32_t getCurrentTime() const{return now;}};
class Mesh;
class Dispatcher {public:
class ReceiveProfileScope {Mesh& mesh_;uint8_t old_;uint32_t generation_;
public:ReceiveProfileScope(Mesh&,uint8_t,uint32_t);~ReceiveProfileScope();};
virtual void onSendFail(Packet*)=0;
};
class PacketManager {public:
std::vector<Packet*> outbound;
int getOutboundTotal() const{return int(outbound.size());}
Packet* getOutboundByIdx(int i){return outbound.at(i);}
Packet* removeOutboundByIdx(int i){Packet* p=outbound.at(i);outbound.erase(outbound.begin()+i);return p;}
};
class Mesh:public Dispatcher {public:
LocalIdentity self_id;Clock clock;RNG rng;PacketManager manager;PacketManager* _mgr=&manager;
std::function<void(Packet*,const Packet*)> copying;
std::function<void(Packet*)> failure;
std::vector<Packet*> allocated;
Packet* in_flight=nullptr;bool queue_ok=true,allocation_ok=true,fanout=false;
unsigned queued=0,cancelled=0,released=0;
uint8_t scoped_profile=0,ack_path_size=0;uint32_t scoped_generation=0,ack_delay=0;
bool ack_scoped=false;uint16_t ack_codes[2]={};
~Mesh(){for(Packet* p:allocated)delete p;}
Clock* getRTCClock(){return &clock;}RNG* getRNG(){return &rng;}
Packet* createGroupDatagram(uint8_t type,const GroupChannel& channel,const uint8_t* data,size_t length){
assert(length<=168);if(!allocation_ok)return nullptr;Packet* p=new Packet;allocated.push_back(p);p->type=type;
p->payload[0]=channel.hash[0];p->payload_len=1+Utils::encryptThenMAC(channel.secret,p->payload+1,data,int(length));return p;
}
bool sendFlood(Packet* p,uint32_t delay,uint8_t hashes){
ack_delay=delay;ack_path_size=hashes;
if(!queue_ok)return false;manager.outbound.push_back(p);++queued;
if(fanout){Packet* second=new Packet(*p);allocated.push_back(second);manager.outbound.push_back(second);
if(copying)copying(second,p);}
return true;
}
bool sendFlood(Packet* p,const uint16_t* codes,uint32_t delay,uint8_t hashes){
ack_scoped=true;memcpy(ack_codes,codes,sizeof(ack_codes));return sendFlood(p,delay,hashes);
}
const Packet* getOutboundInFlight()const{return in_flight;}
void cancelOutboundRadioRetry(const Packet* p){assert(p==in_flight);++cancelled;}
void onSendFail(Packet* p)override{if(failure)failure(p);}
void releasePacket(Packet*){++released;}
};
inline Dispatcher::ReceiveProfileScope::ReceiveProfileScope(Mesh& m,uint8_t profile,uint32_t generation)
:mesh_(m),old_(m.scoped_profile),generation_(m.scoped_generation){m.scoped_profile=profile;m.scoped_generation=generation;}
inline Dispatcher::ReceiveProfileScope::~ReceiveProfileScope(){mesh_.scoped_profile=old_;mesh_.scoped_generation=generation_;}
}
'''
PROFILE = r'''
#pragma once
namespace mesh {
class RadioProfileCLI {public:
bool mutation=false,command=false;uint32_t generation=0;
unsigned finishes=0,commits=0,rollbacks=0;
void beginReplyCommand(){command=true;}void endReplyCommand(){command=false;}
bool hasReplyMutation()const{return mutation;}
uint32_t replyMutationGeneration()const{return generation;}
void stage(){assert(command);mutation=true;++generation;}
bool finishReplyMutation(bool delivered){assert(mutation);mutation=false;++finishes;
if(delivered)++commits;else ++rollbacks;return true;}
};
}
'''
HARNESS = r'''
#include <helpers/FleetChannel.h>
#include <helpers/PersistentStoreFormat.h>
#include <cassert>
#include <string>
#include <algorithm>
#include <cstdio>
@TRANSPORT_METHODS@
inline unsigned identity_counter=0;
namespace mesh {
Identity::Identity(){memset(pub_key,0,sizeof(pub_key));}
LocalIdentity::LocalIdentity():Identity(){
memset(prv_key,0xAA+identity_counter++,32);EVP_PKEY* key=EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519,nullptr,prv_key,32);
assert(key);size_t length=sizeof(pub_key);assert(EVP_PKEY_get_raw_public_key(key,pub_key,&length)==1);
memcpy(prv_key+32,pub_key,32);EVP_PKEY_free(key);
}
void LocalIdentity::sign(uint8_t* sig,const uint8_t* data,int length)const{
EVP_PKEY* key=EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519,nullptr,prv_key,32);
EVP_MD_CTX* ctx=EVP_MD_CTX_new();assert(key&&ctx);assert(EVP_DigestSignInit(ctx,nullptr,nullptr,nullptr,key)==1);
size_t size=64;assert(EVP_DigestSign(ctx,sig,&size,data,length)==1);assert(size==64);
EVP_MD_CTX_free(ctx);EVP_PKEY_free(key);
}
bool Identity::verify(const uint8_t* sig,const uint8_t* data,int length)const{
++verify_calls;EVP_PKEY* key=EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519,nullptr,pub_key,32);
EVP_MD_CTX* ctx=EVP_MD_CTX_new();assert(key&&ctx);assert(EVP_DigestVerifyInit(ctx,nullptr,nullptr,nullptr,key)==1);
bool valid=EVP_DigestVerify(ctx,sig,64,data,length)==1;EVP_MD_CTX_free(ctx);EVP_PKEY_free(key);return valid;
}
}
using namespace mesh;
static constexpr const char* Path="/fleet_channel";
static constexpr const char* Key="21212121212121212121212121212121";
static uint8_t channel_key[32]={0x21,0x21,0x21,0x21,0x21,0x21,0x21,0x21,
0x21,0x21,0x21,0x21,0x21,0x21,0x21,0x21};
static std::string config(FleetChannel& fleet,const std::string& command){
char reply[160]={};assert(fleet.handleConfig(command.c_str(),reply,sizeof(reply)));return reply;
}
static void enroll(FleetChannel& fleet,const LocalIdentity& publisher){
assert(config(fleet,std::string("set fleet.channel ")+Key).rfind("OK",0)==0);
char hex[65];Utils::toHex(hex,publisher.pub_key,32);
assert(config(fleet,std::string("set fleet.controller ")+hex).rfind("OK",0)==0);
}
static Packet raw(const uint8_t* bytes,size_t length){
Packet packet;Utils::sha256(packet.payload,1,channel_key,16);
packet.payload_len=1+Utils::encryptThenMAC(channel_key,packet.payload+1,bytes,int(length));
packet.radio_profile=1;packet.radio_generation=17;packet.path_hash_size=2;
return packet;
}
static Packet command(const LocalIdentity& publisher,uint32_t sequence,const char* text="set radio2 off",
const char* target="all"){
uint8_t payload[184]={},destination[16];assert(FleetCommand::parseTarget(target,destination));
size_t length=FleetCommand::encode(publisher,channel_key,sequence,sequence+300,destination,text,payload+3,sizeof(payload)-3);
assert(length);payload[0]=uint8_t(FleetCommand::DataType);payload[1]=uint8_t(FleetCommand::DataType>>8);payload[2]=uint8_t(length);
return raw(payload,3+length);
}
static Packet signedUnchecked(const LocalIdentity& publisher,uint32_t sequence,uint32_t expires,
const char* text){
uint8_t data[184]={},message[160]={};size_t text_size=strlen(text);
assert(text_size<=FleetCommand::MaxCommandLength);
uint8_t* payload=data+3;memcpy(payload,"FMC1",4);
storage::writeLE32(payload+4,sequence);storage::writeLE32(payload+8,expires);
payload[28]=uint8_t(text_size);memcpy(payload+29,text,text_size);
constexpr const char* domain="MeshCoreFleet1";const size_t domain_size=strlen(domain);
memcpy(message,domain,domain_size);memcpy(message+domain_size,channel_key,16);
memcpy(message+domain_size+16,payload,29+text_size);
publisher.sign(payload+29+text_size,message,int(domain_size+16+29+text_size));
data[0]=uint8_t(FleetCommand::DataType);data[1]=uint8_t(FleetCommand::DataType>>8);
data[2]=uint8_t(29+text_size+64);return raw(data,3+data[2]);
}
static void bind(FleetChannel& fleet,Mesh& mesh,RadioProfileCLI& profiles){
mesh.copying=[&](Packet* second,const Packet* first){fleet.copy(second,first);};
mesh.failure=[&](Packet* p){fleet.fail(p,profiles);};
}
static unsigned apply(FleetChannel& fleet,fs::FS& fs,Mesh& mesh,RadioProfileCLI& profiles,Packet& packet,
bool mutation=false,bool fail_handler=false,const TransportKey* scope=nullptr){
unsigned calls=0;fleet.receive(&packet,mesh,scope);
fleet.service(mesh,profiles,"test:node",[&](uint32_t seq,const char* text,char* reply){
++calls;assert(storage::readLE32(fs.files.at(Path).data()+56)==seq);
assert(mesh.scoped_profile==1&&mesh.scoped_generation==17);assert(profiles.command);
assert(!strcmp(text,"set radio2 off")||!strcmp(text,"get radio2.status"));
if(mutation)profiles.stage();strcpy(reply,fail_handler?"Err - rejected":"OK");
});
assert(mesh.scoped_profile==0&&mesh.scoped_generation==0);assert(!profiles.command);return calls;
}
static void reseal(std::vector<uint8_t>& image){
storage::writeLE32(image.data()+60,storage::updateCRC32(0xffffffff,image.data(),60));
}
int main(int argc,char** argv){
assert(argc==2);const std::string scenario=argv[1];fs::FS fs;Mesh mesh;RadioProfileCLI profiles;
FleetChannel fleet(&fs);LocalIdentity publisher;bind(fleet,mesh,profiles);
if(scenario=="enrollment"){
Packet packet=command(publisher,mesh.clock.now);assert(apply(fleet,fs,mesh,profiles,packet)==0);
assert(config(fleet,"get fleet.channel").find("off,controller=unset,store=ok")!=std::string::npos);
assert(config(fleet,"set fleet.channel 00000000000000000000000000000000").rfind("Err",0)==0);
assert(config(fleet,"set fleet.channel 8b3387e9c5cdea6ac9e5edbaa115cd72").rfind("Err",0)==0);
assert(config(fleet,"set fleet.controller 0000000000000000000000000000000000000000000000000000000000000000").rfind("Err",0)==0);
assert(config(fleet,std::string("set fleet.channel ")+Key).rfind("OK",0)==0);
assert(apply(fleet,fs,mesh,profiles,packet)==0);enroll(fleet,publisher);
const auto& image=fs.files.at(Path);assert(image.size()==64);
assert(!memcmp(image.data()+24,publisher.pub_key,32));
const std::vector<uint8_t> private_seed(32,0xAB);
assert(std::search(image.begin(),image.end(),private_seed.begin(),private_seed.end())==image.end());
assert(apply(fleet,fs,mesh,profiles,packet)==1);
assert(config(fleet,"set fleet.controller off").rfind("OK",0)==0);
++mesh.clock.now;packet=command(publisher,mesh.clock.now);assert(apply(fleet,fs,mesh,profiles,packet)==0);
}else if(scenario=="deferred_single_mailbox"){
enroll(fleet,publisher);Packet a=command(publisher,mesh.clock.now),b=command(publisher,mesh.clock.now+1);
unsigned decrypt_before=decrypt_calls,verify_before=verify_calls,writes_before=fs.writes;
fleet.receive(&a,mesh);fleet.receive(&b,mesh);
assert(decrypt_calls==decrypt_before&&verify_calls==verify_before&&fs.writes==writes_before);
assert(config(fleet,"get fleet.stats").find("busy=1 pending=1")!=std::string::npos);
assert(config(fleet,"set fleet.channel off").rfind("Err",0)==0);
unsigned calls=0;fleet.service(mesh,profiles,"node",[&](uint32_t seq,const char*,char* reply){
++calls;assert(seq==mesh.clock.now);strcpy(reply,"OK");});assert(calls==1);
assert(config(fleet,"get fleet.stats").find("accepted=1")!=std::string::npos);
}else if(scenario=="durable_replay"){
enroll(fleet,publisher);Packet packet=command(publisher,mesh.clock.now);
assert(apply(fleet,fs,mesh,profiles,packet,false,true)==1);
assert(apply(fleet,fs,mesh,profiles,packet)==0);FleetChannel rebooted(&fs);
assert(apply(rebooted,fs,mesh,profiles,packet)==0);
++mesh.clock.now;packet=command(publisher,mesh.clock.now);assert(apply(rebooted,fs,mesh,profiles,packet)==1);
}else if(scenario=="reserve_fault"){
enroll(fleet,publisher);const auto previous=fs.files.at(Path);fs.fail_write=true;
Packet packet=command(publisher,mesh.clock.now);assert(apply(fleet,fs,mesh,profiles,packet)==0);
assert(fs.files.at(Path)==previous);fs.fail_write=false;
assert(config(fleet,"get fleet.channel").find("store=error")!=std::string::npos);
assert(apply(fleet,fs,mesh,profiles,packet)==0);FleetChannel rebooted(&fs);
assert(apply(rebooted,fs,mesh,profiles,packet)==1);
}else if(scenario=="config_fault"){
enroll(fleet,publisher);const auto previous=fs.files.at(Path);fs.fail_write=true;
assert(config(fleet,"set fleet.channel off").rfind("Err",0)==0);
assert(fs.files.at(Path)==previous);fs.fail_write=false;
assert(config(fleet,"get fleet.channel").find("on,controller=set")!=std::string::npos);
FleetChannel rebooted(&fs);Packet packet=command(publisher,mesh.clock.now);
assert(apply(rebooted,fs,mesh,profiles,packet)==1);
}else if(scenario=="images"){
enroll(fleet,publisher);const auto original=fs.files.at(Path);
for(unsigned damage=0;damage<7;++damage){
fs.files[Path]=original;auto& image=fs.files[Path];
if(damage==0)image.resize(63);else if(damage==1)image.push_back(0);
else if(damage==2)image[0]='X';else if(damage==3)image[4]=2;
else if(damage==4)image[5]=1;else if(damage==5)image[60]^=1;
else memset(image.data()+8,0,16);
if(damage>=3&&damage!=5)reseal(image);
FleetChannel bad(&fs);assert(config(bad,"get fleet.channel").find("store=error")!=std::string::npos);
Packet packet=command(publisher,mesh.clock.now);assert(apply(bad,fs,mesh,profiles,packet)==0);
assert(config(bad,std::string("set fleet.channel ")+Key).rfind("Err",0)==0);
}
fs.files[Path]=original;fs.fail_read=true;FleetChannel unreadable(&fs);fs.fail_read=false;
assert(config(unreadable,"get fleet.channel").find("store=error")!=std::string::npos);
#if defined(RP2040_PLATFORM)
fs.files.erase(Path);fs.files[std::string(Path)+".bak"]=original;
FleetChannel recovered(&fs);assert(config(recovered,"get fleet.channel").find("store=ok")!=std::string::npos);
assert(fs.files[Path]==original);Packet packet=command(publisher,mesh.clock.now);
assert(apply(recovered,fs,mesh,profiles,packet)==1);
#endif
}else if(scenario=="ack_both_copies"){
enroll(fleet,publisher);mesh.fanout=true;Packet packet=command(publisher,mesh.clock.now);
TransportKey scope;memset(scope.key,0x12,sizeof(scope.key));
packet.scoped=true;packet.transport_codes[0]=scope.calcTransportCode(&packet);packet.transport_codes[1]=456;
assert(apply(fleet,fs,mesh,profiles,packet,true,false,&scope)==1);assert(fleet.waiting());
assert(mesh.manager.outbound.size()==2);assert(mesh.ack_scoped&&mesh.ack_codes[1]==0);
assert(mesh.ack_codes[0]==scope.calcTransportCode(mesh.manager.outbound[0]));
assert(mesh.ack_codes[0]!=packet.transport_codes[0]);
assert(mesh.ack_delay==1000&&mesh.ack_path_size==2&&last_jitter_max==15500);
assert(mesh.manager.outbound[0]->radio_reply&&!mesh.manager.outbound[0]->radio_bound);
assert(mesh.manager.outbound[0]->flood_retry_policy==FLOOD_RETRY_POLICY_DENY);
fleet.receive(&packet,mesh);assert(config(fleet,"get fleet.stats").find("busy=1")!=std::string::npos);
fleet.complete(mesh.manager.removeOutboundByIdx(0),profiles);assert(fleet.waiting());assert(profiles.commits==0);
fleet.fail(mesh.manager.removeOutboundByIdx(0),profiles);assert(!fleet.waiting());assert(profiles.commits==1&&profiles.rollbacks==0);
}else if(scenario=="ack_all_fail"){
enroll(fleet,publisher);mesh.fanout=true;Packet packet=command(publisher,mesh.clock.now);
assert(apply(fleet,fs,mesh,profiles,packet,true)==1);
fleet.fail(mesh.manager.removeOutboundByIdx(0),profiles);assert(fleet.waiting()&&profiles.rollbacks==0);
fleet.fail(mesh.manager.removeOutboundByIdx(0),profiles);assert(!fleet.waiting());assert(profiles.rollbacks==1);
}else if(scenario=="ack_queue_or_allocation"){
enroll(fleet,publisher);Packet packet=command(publisher,mesh.clock.now);mesh.queue_ok=false;
assert(apply(fleet,fs,mesh,profiles,packet,true)==1);assert(!fleet.waiting()&&profiles.rollbacks==1);
assert(apply(fleet,fs,mesh,profiles,packet,true)==0);mesh.queue_ok=true;mesh.allocation_ok=false;
++mesh.clock.now;packet=command(publisher,mesh.clock.now);
assert(apply(fleet,fs,mesh,profiles,packet,true)==1);assert(profiles.rollbacks==2);
}else if(scenario=="ack_timeout"){
enroll(fleet,publisher);mesh.fanout=true;Packet packet=command(publisher,mesh.clock.now);
assert(apply(fleet,fs,mesh,profiles,packet,true)==1);fake_ms+=300000;
fleet.service(mesh,profiles,"node",[](uint32_t,const char*,char*){assert(false);});
assert(mesh.manager.outbound.empty()&&mesh.released==2);assert(profiles.rollbacks==1&&!fleet.waiting());
++mesh.clock.now;packet=command(publisher,mesh.clock.now);assert(apply(fleet,fs,mesh,profiles,packet,true)==1);
mesh.in_flight=mesh.manager.removeOutboundByIdx(0);fake_ms+=300000;
fleet.service(mesh,profiles,"node",[](uint32_t,const char*,char*){assert(false);});
assert(mesh.cancelled==1&&fleet.waiting());fleet.fail(mesh.in_flight,profiles);mesh.in_flight=nullptr;
assert(!fleet.waiting()&&profiles.rollbacks==2);
}else if(scenario=="tamper_padding_target"){
enroll(fleet,publisher);Packet good=command(publisher,mesh.clock.now);
uint8_t data[184];int size=Utils::MACThenDecrypt(channel_key,data,good.payload+1,good.payload_len-1);
data[3+29]^=1;Packet forged=raw(data,size);assert(apply(fleet,fs,mesh,profiles,forged)==0);
size=Utils::MACThenDecrypt(channel_key,data,good.payload+1,good.payload_len-1);
data[size-1]=1;Packet padded=raw(data,size);assert(apply(fleet,fs,mesh,profiles,padded)==0);
Packet mac=good;mac.payload[1]^=1;assert(apply(fleet,fs,mesh,profiles,mac)==0);
char target[65];uint8_t other[32];memset(other,0x23,sizeof(other));Utils::toHex(target,other,sizeof(other));
Packet wrong=command(publisher,mesh.clock.now,"set radio2 off",target);assert(apply(fleet,fs,mesh,profiles,wrong)==0);
fake_ms+=1000;Utils::toHex(target,mesh.self_id.pub_key,32);
Packet mine=command(publisher,mesh.clock.now,"set radio2 off",target);assert(apply(fleet,fs,mesh,profiles,mine)==1);
assert(last_jitter_max==1500);
}else if(scenario=="verify_budget"){
enroll(fleet,publisher);Packet packet=command(publisher,mesh.clock.now);
packet.payload[1]^=1;unsigned before=decrypt_calls;
for(unsigned i=0;i<20;++i)assert(apply(fleet,fs,mesh,profiles,packet)==0);
assert(decrypt_calls-before==4);fake_ms+=1000;packet=command(publisher,mesh.clock.now);
assert(apply(fleet,fs,mesh,profiles,packet)==1);
}else if(scenario=="primary_denied"){
assert(!FleetCommand::commandAllowed("set radio 915,500,5,5"));
assert(!FleetCommand::commandAllowed("set tempradio 915,500,5,5,10"));
assert(!FleetCommand::commandAllowed("set password pass"));
assert(!FleetCommand::commandAllowed("reboot"));
assert(!FleetCommand::commandAllowed("set fleet.controller off"));
}else if(scenario=="signed_forbidden"){
enroll(fleet,publisher);
for(const char* text:{"set radio 915,500,5,5","set password pass","reboot",
"set fleet.channel off","get prv.key","set flood.max 5;reboot"}){
fake_ms+=1000;Packet packet=signedUnchecked(publisher,mesh.clock.now,mesh.clock.now+300,text);
assert(apply(fleet,fs,mesh,profiles,packet)==0);
}
assert(storage::readLE32(fs.files[Path].data()+56)==0);assert(mesh.queued==0);
}else if(scenario=="clock_limits"){
enroll(fleet,publisher);const uint32_t now=mesh.clock.now;
const std::vector<std::pair<uint32_t,uint32_t>> invalid={{now-301,now-1},{now+61,now+100},
{FleetCommand::MinEpoch-1,now+100},{now,now+FleetCommand::MaxLifetime+1},{now,now-1}};
for(const auto& times:invalid){fake_ms+=1000;
Packet packet=signedUnchecked(publisher,times.first,times.second,"set radio2 off");
assert(apply(fleet,fs,mesh,profiles,packet)==0);
}
mesh.clock.now=0;fake_ms+=1000;Packet packet=command(publisher,now);
assert(apply(fleet,fs,mesh,profiles,packet)==0);mesh.clock.now=now;
fake_ms+=1000;packet=signedUnchecked(publisher,now+60,now+100,"set radio2 off");
assert(apply(fleet,fs,mesh,profiles,packet)==1);
}else if(scenario=="transaction_faults"){
for(unsigned fault=0;fault<4;++fault){
fs::FS disk;Mesh transport;RadioProfileCLI radio;FleetChannel receiver(&disk);enroll(receiver,publisher);
auto previous=disk.files.at(Path);Packet packet=command(publisher,transport.clock.now);
if(fault==0)disk.fail_open=true;
else if(fault==1)disk.fail_read=true;
else if(fault==2)disk.fail_rename=true;
else disk.fail_rename_from=std::string(Path)+".tmp";
assert(apply(receiver,disk,transport,radio,packet)==0);assert(disk.files.at(Path)==previous);
assert(config(receiver,"get fleet.channel").find("store=error")!=std::string::npos);
}
}else if(scenario=="backup_replay"){
enroll(fleet,publisher);Packet packet=command(publisher,mesh.clock.now);
assert(apply(fleet,fs,mesh,profiles,packet)==1);const auto persisted=fs.files.at(Path);
#if defined(RP2040_PLATFORM)
fs.files.erase(Path);fs.files[std::string(Path)+".bak"]=persisted;
#else
// Atomic LittleFS replacement never has a live-name gap. Abandoned .tmp
// bytes are ignored; the committed live sequence remains authoritative.
fs.files[std::string(Path)+".tmp"]=persisted;
#endif
FleetChannel rebooted(&fs);assert(apply(rebooted,fs,mesh,profiles,packet)==0);
assert(storage::readLE32(fs.files.at(Path).data()+56)==mesh.clock.now);
}else if(scenario=="wrong_publisher"){
enroll(fleet,publisher);LocalIdentity stranger;
assert(memcmp(stranger.pub_key,publisher.pub_key,32));
Packet packet=command(stranger,mesh.clock.now);assert(apply(fleet,fs,mesh,profiles,packet)==0);
assert(verify_calls==1&&mesh.queued==0&&storage::readLE32(fs.files[Path].data()+56)==0);
}else if(scenario=="bounded_ack"){
enroll(fleet,publisher);Packet packet=command(publisher,mesh.clock.now);
fleet.receive(&packet,mesh);fleet.service(mesh,profiles,"sixteen:character:very long name",
[&](uint32_t,const char*,char* reply){memset(reply,'x',159);reply[159]=0;});
assert(mesh.queued==1);uint8_t data[184]={};Packet* ack=mesh.manager.outbound[0];
assert(ack->payload_len<=MAX_PACKET_PAYLOAD);
assert(Utils::MACThenDecrypt(channel_key,data,ack->payload+1,ack->payload_len-1)==176);
assert(!strncmp((char*)data+5,"sixteen;characte",15));
}else if(scenario=="unknown_scope"){
enroll(fleet,publisher);Packet packet=command(publisher,mesh.clock.now);packet.scoped=true;
packet.transport_codes[0]=123;assert(apply(fleet,fs,mesh,profiles,packet,true)==1);
assert(mesh.queued==0&&mesh.released==1&&profiles.rollbacks==1&&!fleet.waiting());
}else if(scenario=="rotated_scope"){
struct Region {bool wildcard=false;bool isWildcard()const{return wildcard;}};
struct Regions {TransportKey keys[2];int getTransportKeysFor(const Region&,TransportKey* out,int max){
assert(max==MAX_TKS_ENTRIES);memcpy(out,keys,sizeof(keys));return 2;}} regions;
memset(regions.keys[0].key,0x11,16);memset(regions.keys[1].key,0x22,16);
Region region;TransportKey captured{};Packet packet=command(publisher,mesh.clock.now);
packet.scoped=true;packet.transport_codes[0]=regions.keys[1].calcTransportCode(&packet);
assert(packet.transport_codes[0]!=regions.keys[0].calcTransportCode(&packet));
assert(captureFleetReplyScope(regions,&region,&packet,captured));
assert(!memcmp(captured.key,regions.keys[1].key,16));
enroll(fleet,publisher);assert(apply(fleet,fs,mesh,profiles,packet,true,false,&captured)==1);
assert(mesh.ack_codes[0]==regions.keys[1].calcTransportCode(mesh.manager.outbound[0]));
assert(mesh.ack_codes[0]!=packet.transport_codes[0]);
region.wildcard=true;assert(!captureFleetReplyScope(regions,&region,&packet,captured));
assert(!captureFleetReplyScope(regions,static_cast<Region*>(nullptr),&packet,captured));
region.wildcard=false;packet.transport_codes[0]^=1;
assert(!captureFleetReplyScope(regions,&region,&packet,captured));
}else if(scenario=="existing_mutation"){
enroll(fleet,publisher);Packet packet=command(publisher,mesh.clock.now);
profiles.mutation=true;assert(apply(fleet,fs,mesh,profiles,packet)==0);
assert(storage::readLE32(fs.files[Path].data()+56)==0);
profiles.mutation=false;assert(apply(fleet,fs,mesh,profiles,packet)==1);
}else assert(false);
printf("fleet runtime %s passed\n",argv[1]);
}
'''
class FleetChannelRuntimeTests(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-runtime-")
work = Path(cls.work.name)
(work / "helpers").mkdir()
for name, content in {"Utils.h": UTILS, "FS.h": FILESYSTEM, "Mesh.h": MESH,
"helpers/RadioProfileCLI.h": PROFILE,
"Packet.h": '#pragma once\n#include <Mesh.h>\n',
"Adafruit_LittleFS.h": '''
#pragma once
#include <FS.h>
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 <FS.h>\ninline fs::FS InternalFS;\n',
"Arduino.h": '#pragma once\n#include <Mesh.h>\n'}.items():
(work / name).write_text(content)
fixture = work / "fixture.cpp"
transport_source = (ROOT / "src/helpers/TransportKeyStore.cpp").read_text()
methods = "\n".join(body(transport_source, signature) for signature in [
"uint16_t TransportKey::calcTransportCode(", "bool TransportKey::isNull("])
fixture.write_text(HARNESS.replace("@TRANSPORT_METHODS@", methods))
sha_header = ROOT / "test/mocks/SHA256.h"
(work / "SHA256.h").write_text(f'''
#pragma once
#define SHA256 SHA256Base
#include "{sha_header}"
#undef SHA256
class SHA256 : public SHA256Base {{ public:
void finalize(void* out,size_t length) {{ SHA256Base::finalize(static_cast<uint8_t*>(out),length); }}
void finalizeHMAC(const uint8_t* key,size_t size,void* out,size_t length) {{
SHA256Base::finalizeHMAC(key,size,static_cast<uint8_t*>(out),length); }}
}};
''')
cls.binaries = {}
sanitizer_flags = (["-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-no-pie"]
if os.environ.get("MESHCORE_FLEET_SANITIZERS") == "1" else [])
for backend in ["RP2040", "NRF52"]:
binary = work / f"fleet-runtime-{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"))
result = subprocess.run([
compiler, *sanitizer_flags, "-std=c++17", f"-D{backend}_PLATFORM",
"-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
"-Wno-misleading-indentation", "-Wno-sign-compare", "-O2", "-g",
"-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 result.returncode:
cls.work.cleanup()
raise AssertionError(f"{backend}:\n" + result.stdout + result.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_private_channel_and_public_only_publisher_enrollment(self):
self.scenario("enrollment")
def test_receive_defers_crypto_writes_and_accepts_one_pending_command(self):
self.scenario("deferred_single_mailbox")
def test_sequence_reserved_before_handler_including_error_survives_reboot(self):
self.scenario("durable_replay")
def test_failed_sequence_save_grants_no_control_and_disables_receiver(self):
self.scenario("reserve_fault")
def test_failed_configuration_write_preserves_durable_enrollment(self):
self.scenario("config_fault")
def test_corrupt_short_long_unreadable_images_fail_closed_and_backup_recovers(self):
self.scenario("images")
def test_ack_waits_for_both_profile_copies_with_one_success_and_retains_rx_scope(self):
self.scenario("ack_both_copies")
def test_both_ack_transmissions_failing_roll_back_radio_mutation(self):
self.scenario("ack_all_fail")
def test_ack_allocation_queue_failure_rolls_back_but_keeps_reserved_sequence(self):
self.scenario("ack_queue_or_allocation")
def test_ack_timeout_cancels_queued_and_inflight_packets_before_rollback(self):
self.scenario("ack_timeout")
def test_real_signature_mac_padding_and_full_identity_target_checks(self):
self.scenario("tamper_padding_target")
def test_forged_packets_have_bounded_verification_cost(self):
self.scenario("verify_budget")
def test_fleet_permission_never_allows_primary_radio_or_administrator_changes(self):
self.scenario("primary_denied")
def test_publisher_signed_commands_still_cannot_escape_fleet_capability(self):
self.scenario("signed_forbidden")
def test_expiry_future_clock_bounds_and_invalid_rtc_prevent_dispatch(self):
self.scenario("clock_limits")
def test_open_read_rename_and_publish_faults_preserve_state_and_fail_closed(self):
self.scenario("transaction_faults")
def test_recovered_backup_preserves_durable_anti_replay_sequence(self):
self.scenario("backup_replay")
def test_another_valid_ed25519_identity_cannot_authorize_fleet_commands(self):
self.scenario("wrong_publisher")
def test_long_cli_reply_and_sender_name_fit_one_encrypted_group_packet(self):
self.scenario("bounded_ack")
def test_unknown_scoped_route_refuses_ack_and_rolls_back_radio_mutation(self):
self.scenario("unknown_scope")
def test_scope_selects_matching_rotated_region_key_and_authenticates_new_ack_payload(self):
self.scenario("rotated_scope")
def test_existing_remote_radio_mutation_blocks_fleet_without_reserving_sequence(self):
self.scenario("existing_mutation")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,273 @@
#include <gtest/gtest.h>
#include <Ed25519.h>
#include <helpers/FleetCommand.h>
#include <SHA256.h>
#include <array>
#include <string>
#include <vector>
namespace {
using Fleet = mesh::FleetCommand;
constexpr uint32_t Now = 1791500000UL;
constexpr uint8_t ChannelKey[16] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
};
class FixedRng : public mesh::RNG {
public:
void random(uint8_t* dest, size_t size) override {
for (size_t i = 0; i < size; ++i) dest[i] = uint8_t(i + 1);
}
};
class FleetCommandTest : public ::testing::Test {
protected:
FixedRng rng;
mesh::LocalIdentity publisher{&rng};
std::array<uint8_t, Fleet::TargetSize> all{};
Fleet::Decoded decoded{};
bool previous_verifier = g_mock_ed25519_verify_result;
void SetUp() override { g_mock_ed25519_verify_result = true; }
void TearDown() override { g_mock_ed25519_verify_result = previous_verifier; }
std::vector<uint8_t> encode(const char* command = "get radio2",
uint32_t sequence = Now,
uint32_t expires = Now + 120,
const uint8_t* target = nullptr) {
std::vector<uint8_t> bytes(Fleet::MaxPayloadLength);
const size_t size = Fleet::encode(publisher, ChannelKey, sequence, expires,
target ? target : all.data(), command, bytes.data(), bytes.size());
bytes.resize(size);
return bytes;
}
bool decode(const std::vector<uint8_t>& bytes, uint32_t now = Now) {
return Fleet::decode(publisher, ChannelKey, bytes.data(), bytes.size(),
now, publisher.pub_key, decoded);
}
};
TEST_F(FleetCommandTest, ExplicitRadioAndFilterFamiliesAreAllowed) {
for (const char* command : {
"get radio2", "get tempradio2", "get radio2.status", "get radio2.timing",
"get radio2.cross", "set radio2.cross auto", "set radio2.cross on",
"set radio2.cross off", "set radio2 off", "set tempradio2 off",
"set radio2 910.5,500,5,5,rxtx",
"set tempradio2 910.5,500,5,5,rx,10,auto",
"set radio2 910.5,500,5,5,rx,32",
"get flood.filter", "get flood.filter.1", "get flood.filter.blacklist.2",
"set flood.filter.2 grp_txt 4+", "set flood.filter.blacklist A1,B2",
"del flood.filter.blacklist.2", "del flood.filter all",
"get flood.rule.2", "set flood.rule.2 type=any rate=10/min stop",
"del flood.rule.2", "del flood.rule all",
"get flood.moderation.2", "set flood.moderation public '*' rate=2/min",
"del flood.moderation all", "get flood.channel.scope.3",
"set flood.channel.scope public scope=usa", "del flood.channel.scope.3",
"get flood.channel.scope.require.2",
"set flood.channel.scope.require.2 #fleet", "del flood.channel.scope.require all",
"get flood.channel.data", "set flood.channel.data on",
"get flood.channel.data.hops", "set flood.channel.data.hops 4",
"get flood.max", "set flood.max 8", "get flood.max.unscoped",
"set flood.max.unscoped 4", "get flood.max.advert", "set flood.max.advert 6"}) {
EXPECT_TRUE(Fleet::commandAllowed(command)) << command;
}
}
TEST_F(FleetCommandTest, NoGenericAdminNamespaceOrPrefixEscapes) {
for (const char* command : {
"get prv.key", "backup prv.key 1234567890ABCDEF", "get wifi.password",
"set wifi.password abc", "setperm abc 3", "reboot", "start ota",
"get fleet", "set fleet.channel abc", "region add usa", "get radio",
"set radio 910.5,500,5,5", "get radio2.reply", "get radio2.scan",
"set tx.reply both", "set radioat2 910.5,500,5,5,rxtx,+5",
"get flood.filter.secret", "get flood.filtering", "get flood.rulebook",
"get flood.rule.1.secret", "get flood.filter.blacklistx",
"get flood.channel.scope.required", "get flood.channel.data.secret",
"get flood.max.secret", "get flood.max.unscoped.extra", "get flood.rule.0",
"get flood.rule.01", "get flood.rule.256", "get flood.rule.9999999999",
"xy|set radio2 off", "GET radio2", "get radio2", " get radio2",
"get radio2 ", "get radio2 off", "del radio2", "set radio2",
"set radio2.cross true", "del flood.channel.data", "set flood.filter.2",
"del flood.rule.2 extra", "get flood.rule.1 all", "set flood.max",
"set radio2 910.5,500,5,5,rxtx,auto,99",
"set radio2 910.5,500,5,5,off", "set radio2 910.5,500,5,5,rxtx,",
"set tempradio2 910.5,500,5,5,rxtx",
"set tempradio2 910.5,500,5,5,rxtx,+10",
"set radio2 910.5,500,5,5,rxtx;reboot", "get radio2\nreboot",
"get radio2\r", "get\tradio2", "get radio2|reboot", "get radio2&reboot",
"set flood.rule.1 type=any `reboot`", "set flood.rule.1 type=any\\reboot"}) {
EXPECT_FALSE(Fleet::commandAllowed(command)) << command;
}
const std::string non_ascii = "set flood.rule.1 " + std::string(1, char(0x80));
EXPECT_FALSE(Fleet::commandAllowed(non_ascii.c_str()));
EXPECT_FALSE(Fleet::commandAllowed(nullptr));
EXPECT_FALSE(Fleet::commandAllowed(""));
}
TEST_F(FleetCommandTest, OnlyCompletePublicKeyTargetsOrExplicitAll) {
std::array<uint8_t, Fleet::TargetSize> target;
target.fill(0xAA);
EXPECT_TRUE(Fleet::parseTarget("all", target.data()));
EXPECT_EQ(all, target);
EXPECT_FALSE(Fleet::parseTarget("ALL", target.data()));
EXPECT_FALSE(Fleet::parseTarget("1234", target.data()));
EXPECT_FALSE(Fleet::parseTarget(std::string(64, '0').c_str(), target.data()));
EXPECT_FALSE(Fleet::parseTarget(std::string(64, 'z').c_str(), target.data()));
EXPECT_FALSE(Fleet::parseTarget(nullptr, target.data()));
EXPECT_FALSE(Fleet::parseTarget("all", nullptr));
const char* full = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
ASSERT_TRUE(Fleet::parseTarget(full, target.data()));
const std::array<uint8_t, 16> expected = {
0xae, 0x21, 0x6c, 0x2e, 0xf5, 0x24, 0x7a, 0x37,
0x82, 0xc1, 0x35, 0xef, 0xa2, 0x79, 0xa3, 0xe4
};
EXPECT_EQ(expected, target); // independently generated SHA-256 vector
}
TEST_F(FleetCommandTest, RejectsPublicZeroOrMissingPrivacyKeys) {
const uint8_t public_key[16] = {
0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72
};
const uint8_t empty[16] = {};
EXPECT_TRUE(Fleet::privateKeyAllowed(ChannelKey));
EXPECT_FALSE(Fleet::privateKeyAllowed(public_key));
EXPECT_FALSE(Fleet::privateKeyAllowed(empty));
EXPECT_FALSE(Fleet::privateKeyAllowed(nullptr));
std::array<uint8_t, Fleet::MaxPayloadLength> output;
for (const uint8_t* key : {public_key, empty, static_cast<const uint8_t*>(nullptr)}) {
EXPECT_EQ(0U, Fleet::encode(publisher, key, Now, Now + 10, all.data(),
"get radio2", output.data(), output.size()));
const auto bytes = encode();
EXPECT_FALSE(Fleet::decode(publisher, key, bytes.data(), bytes.size(), Now,
publisher.pub_key, decoded));
}
}
TEST_F(FleetCommandTest, ExactEnvelopeRoundTripsThroughVerifierBoundary) {
const auto bytes = encode();
ASSERT_EQ(Fleet::HeaderSize + 10 + Fleet::SignatureSize, bytes.size());
EXPECT_EQ(std::string("FMC1"), std::string(bytes.begin(), bytes.begin() + 4));
EXPECT_EQ(10, bytes[28]);
const unsigned calls = g_mock_ed25519_verify_calls;
ASSERT_TRUE(decode(bytes));
EXPECT_EQ(calls + 1, g_mock_ed25519_verify_calls);
EXPECT_EQ(Now, decoded.sequence);
EXPECT_EQ(Now + 120, decoded.expires);
EXPECT_STREQ("get radio2", decoded.command);
}
TEST_F(FleetCommandTest, FailedSignatureVerificationNeverReturnsCommand) {
const auto bytes = encode();
g_mock_ed25519_verify_result = false;
decoded.sequence = 123;
strcpy(decoded.command, "stale");
const unsigned calls = g_mock_ed25519_verify_calls;
EXPECT_FALSE(decode(bytes));
EXPECT_EQ(calls + 1, g_mock_ed25519_verify_calls);
EXPECT_EQ(0U, decoded.sequence);
EXPECT_EQ(0U, decoded.expires);
EXPECT_STREQ("", decoded.command);
}
TEST_F(FleetCommandTest, FullPayloadLimitFitsWithoutOverflow) {
const std::string prefix = "set flood.rule.1 ";
const std::string maximum = prefix + std::string(Fleet::MaxCommandLength - prefix.size(), 'a');
ASSERT_TRUE(Fleet::commandAllowed(maximum.c_str()));
const auto bytes = encode(maximum.c_str());
ASSERT_EQ(Fleet::MaxPayloadLength, bytes.size());
EXPECT_TRUE(decode(bytes));
EXPECT_STREQ(maximum.c_str(), decoded.command);
EXPECT_FALSE(Fleet::commandAllowed((maximum + "a").c_str()));
EXPECT_TRUE(encode((maximum + "a").c_str()).empty());
std::array<uint8_t, Fleet::MaxPayloadLength + 2> output;
output.fill(0xA5);
EXPECT_EQ(0U, Fleet::encode(publisher, ChannelKey, Now, Now + 10, all.data(),
maximum.c_str(), output.data() + 1, 164));
for (uint8_t value : output) EXPECT_EQ(0xA5, value);
}
TEST_F(FleetCommandTest, RejectsTruncationTrailingPaddingAndWrongMagic) {
const auto original = encode();
for (size_t size = 0; size < original.size(); ++size) {
const std::vector<uint8_t> shortened(original.begin(), original.begin() + size);
EXPECT_FALSE(decode(shortened)) << size;
}
auto bytes = original;
bytes.push_back(0);
EXPECT_FALSE(decode(bytes));
bytes = original;
bytes[0] ^= 1;
EXPECT_FALSE(decode(bytes));
bytes = original;
bytes[28] = 0;
EXPECT_FALSE(decode(bytes));
bytes[28] = 73;
EXPECT_FALSE(decode(bytes));
}
TEST_F(FleetCommandTest, RejectsEmbeddedNulAndForbiddenSignedCommandShape) {
auto bytes = encode();
bytes[Fleet::HeaderSize + 4] = 0;
EXPECT_FALSE(decode(bytes));
bytes = encode();
memcpy(bytes.data() + Fleet::HeaderSize, "get radio1", 10);
EXPECT_FALSE(decode(bytes));
bytes = encode();
bytes[Fleet::HeaderSize + 9] = '\n';
EXPECT_FALSE(decode(bytes));
}
TEST_F(FleetCommandTest, MatchesHashedCompleteSelfIdentityOrAll) {
std::array<uint8_t, Fleet::TargetSize> target;
SHA256 hash;
hash.update(publisher.pub_key, PUB_KEY_SIZE);
hash.finalize(target.data(), target.size());
ASSERT_TRUE(decode(encode("get radio2", Now, Now + 120, target.data())));
target[15] ^= 1;
EXPECT_FALSE(decode(encode("get radio2", Now, Now + 120, target.data())));
EXPECT_TRUE(decode(encode()));
}
TEST_F(FleetCommandTest, RequiresPlausibleClockAndBoundedExpiry) {
EXPECT_TRUE(encode("get radio2", Fleet::MinEpoch - 1, Fleet::MinEpoch + 10).empty());
EXPECT_TRUE(encode("get radio2", Now, Now - 1).empty());
EXPECT_TRUE(encode("get radio2", Now, Now + 601).empty());
EXPECT_FALSE(decode(encode(), Fleet::MinEpoch - 1));
EXPECT_TRUE(decode(encode(), Now + 120));
EXPECT_FALSE(decode(encode(), Now + 121));
EXPECT_TRUE(decode(encode("get radio2", Now + 60, Now + 120)));
EXPECT_FALSE(decode(encode("get radio2", Now + 61, Now + 120)));
EXPECT_TRUE(decode(encode("get radio2", Now, Now + 600)));
EXPECT_TRUE(decode(encode("get radio2", Now, Now)));
}
TEST_F(FleetCommandTest, RejectsMissingAndZeroIdentityInputs) {
const auto bytes = encode();
const uint8_t empty[32] = {};
const mesh::Identity absent(empty);
EXPECT_FALSE(Fleet::decode(absent, ChannelKey, bytes.data(), bytes.size(),
Now, publisher.pub_key, decoded));
EXPECT_FALSE(Fleet::decode(publisher, ChannelKey, bytes.data(), bytes.size(),
Now, empty, decoded));
EXPECT_FALSE(Fleet::decode(publisher, ChannelKey, bytes.data(), bytes.size(),
Now, nullptr, decoded));
EXPECT_FALSE(Fleet::decode(publisher, ChannelKey, nullptr, bytes.size(),
Now, publisher.pub_key, decoded));
}
TEST_F(FleetCommandTest, UnixRangeBoundaryDoesNotWrap) {
const uint32_t maximum = UINT32_MAX;
EXPECT_TRUE(decode(encode("get radio2", maximum, maximum), maximum - 60));
EXPECT_FALSE(decode(encode("get radio2", maximum, maximum), maximum - 61));
EXPECT_TRUE(encode("get radio2", maximum - 50, 1).empty());
}
} // namespace
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+249
View File
@@ -0,0 +1,249 @@
#!/usr/bin/env python3
"""Exercise the production fleet envelope with real Ed25519, never native mocks."""
import hashlib
from pathlib import Path
import struct
import subprocess
import tempfile
import unittest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
ROOT = Path(__file__).resolve().parents[1]
NOW = 1791500000
CHANNEL = bytes(range(1, 17))
DOMAIN = b"MeshCoreFleet1"
PUBLISHER = Ed25519PrivateKey.from_private_bytes(bytes(range(1, 33)))
PUBLIC = PUBLISHER.public_key().public_bytes(
serialization.Encoding.Raw, serialization.PublicFormat.Raw)
OTHER = Ed25519PrivateKey.from_private_bytes(bytes(range(33, 65)))
OTHER_PUBLIC = OTHER.public_key().public_bytes(
serialization.Encoding.Raw, serialization.PublicFormat.Raw)
FIXTURE = r'''
#include <helpers/FleetCommand.h>
#include <openssl/evp.h>
#include <array>
#include <cassert>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
// The production Identity interface is retained. OpenSSL implements its
// signatures here so these tests cannot inherit native's always-true mock.
namespace mesh {
Identity::Identity() { memset(pub_key, 0, sizeof(pub_key)); }
LocalIdentity::LocalIdentity(RNG* rng) {
memset(prv_key, 0, sizeof(prv_key)); rng->random(prv_key, 32);
EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, prv_key, 32);
assert(key); size_t length = sizeof(pub_key);
assert(EVP_PKEY_get_raw_public_key(key, pub_key, &length) == 1 && length == sizeof(pub_key));
EVP_PKEY_free(key);
}
void LocalIdentity::sign(uint8_t* signature, const uint8_t* message, int length) const {
EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, prv_key, 32);
EVP_MD_CTX* context = EVP_MD_CTX_new(); assert(key && context);
assert(EVP_DigestSignInit(context, nullptr, nullptr, nullptr, key) == 1);
size_t size = SIGNATURE_SIZE;
assert(EVP_DigestSign(context, signature, &size, message, size_t(length)) == 1);
assert(size == SIGNATURE_SIZE); EVP_MD_CTX_free(context); EVP_PKEY_free(key);
}
bool Identity::verify(const uint8_t* signature, const uint8_t* message, int length) const {
EVP_PKEY* key = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, pub_key, sizeof(pub_key));
EVP_MD_CTX* context = EVP_MD_CTX_new(); assert(key && context);
assert(EVP_DigestVerifyInit(context, nullptr, nullptr, nullptr, key) == 1);
const bool valid = EVP_DigestVerify(context, signature, SIGNATURE_SIZE, message, size_t(length)) == 1;
EVP_MD_CTX_free(context); EVP_PKEY_free(key); return valid;
}
} // namespace mesh
struct FixedRng : mesh::RNG {
void random(uint8_t* output, size_t size) override {
for (size_t i = 0; i < size; ++i) output[i] = uint8_t(i + 1);
}
};
std::vector<uint8_t> unhex(const char* input) {
const std::string text(input); assert(text.size() % 2 == 0);
std::vector<uint8_t> bytes(text.size() / 2);
for (size_t i = 0; i < bytes.size(); ++i)
bytes[i] = uint8_t(std::stoul(text.substr(i * 2, 2), nullptr, 16));
return bytes;
}
void hex(const uint8_t* bytes, size_t size) {
constexpr char digits[] = "0123456789abcdef";
for (size_t i = 0; i < size; ++i) std::cout << digits[bytes[i] >> 4] << digits[bytes[i] & 15];
std::cout << '\n';
}
int main(int argc, char** argv) {
using Fleet = mesh::FleetCommand;
if (argc == 7 && std::string(argv[1]) == "encode") {
FixedRng rng; mesh::LocalIdentity publisher(&rng);
const auto key = unhex(argv[2]); assert(key.size() == Fleet::KeySize);
std::array<uint8_t, Fleet::TargetSize> target;
if (!Fleet::parseTarget(argv[3], target.data())) { std::cout << "reject\n"; return 0; }
std::array<uint8_t, Fleet::MaxPayloadLength> output;
const size_t size = Fleet::encode(publisher, key.data(), uint32_t(std::stoul(argv[4])),
uint32_t(std::stoul(argv[5])), target.data(), argv[6], output.data(), output.size());
if (!size) std::cout << "reject\n"; else hex(output.data(), size);
return 0;
}
if (argc == 7 && std::string(argv[1]) == "decode") {
const auto public_key = unhex(argv[2]), key = unhex(argv[3]), self = unhex(argv[4]), bytes = unhex(argv[6]);
assert(public_key.size() == PUB_KEY_SIZE && self.size() == PUB_KEY_SIZE && key.size() == Fleet::KeySize);
mesh::Identity publisher(public_key.data()); Fleet::Decoded output;
if (!Fleet::decode(publisher, key.data(), bytes.data(), bytes.size(),
uint32_t(std::stoul(argv[5])), self.data(), output)) {
assert(output.sequence == 0 && output.expires == 0 && output.command[0] == 0);
std::cout << "reject\n";
} else std::cout << output.sequence << '\n' << output.expires << '\n' << output.command << '\n';
return 0;
}
return 2;
}
'''
def envelope(command="get radio2", sequence=NOW, expires=NOW + 120,
target=b"\0" * 16, channel=CHANNEL, publisher=PUBLISHER):
command = command.encode("ascii") if isinstance(command, str) else command
unsigned = b"FMC1" + struct.pack("<II", sequence, expires) + target + bytes([len(command)]) + command
return unsigned + publisher.sign(DOMAIN + channel + unsigned)
class FleetCommandCryptoTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.directory = tempfile.TemporaryDirectory(prefix="meshcore-fleet-crypto-")
cls.addClassCleanup(cls.directory.cleanup)
path = Path(cls.directory.name)
source = path / "fleet-crypto.cpp"
source.write_text(FIXTURE)
cls.binary = path / "fleet-crypto"
build = subprocess.run([
"g++", "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-Wno-sign-compare",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
"-I", str(ROOT / "src"), "-I", str(ROOT / "test/mocks"),
str(source), str(ROOT / "src/helpers/FleetCommand.cpp"),
"-lcrypto", "-o", str(cls.binary)], capture_output=True, text=True)
if build.returncode:
raise AssertionError(build.stdout + build.stderr)
def run_tool(self, *arguments):
result = subprocess.run([str(self.binary), *map(str, arguments)],
capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertNotIn("runtime error:", result.stderr)
self.assertNotIn("AddressSanitizer", result.stderr)
return result.stdout.strip()
def decode(self, payload, channel=CHANNEL, publisher=PUBLIC, self_key=PUBLIC, now=NOW):
return self.run_tool("decode", publisher.hex(), channel.hex(), self_key.hex(), now, payload.hex())
def test_cpp_and_python_signatures_match_exactly(self):
expected = envelope()
actual = self.run_tool("encode", CHANNEL.hex(), "all", NOW, NOW + 120, "get radio2")
self.assertEqual(actual, expected.hex())
PUBLISHER.public_key().verify(expected[-64:], DOMAIN + CHANNEL + expected[:-64])
self.assertEqual(self.decode(expected), f"{NOW}\n{NOW + 120}\nget radio2")
def test_python_signature_accepted_for_complete_hashed_target(self):
target = hashlib.sha256(PUBLIC).digest()[:16]
expected = envelope(target=target)
actual = self.run_tool("encode", CHANNEL.hex(), PUBLIC.hex(), NOW, NOW + 120, "get radio2")
self.assertEqual(actual, expected.hex())
self.assertNotEqual(self.decode(expected), "reject")
self.assertEqual(self.decode(expected, self_key=OTHER_PUBLIC), "reject")
def test_all_targets_reaches_another_receiver_without_other_private_key(self):
self.assertNotEqual(self.decode(envelope(), self_key=OTHER_PUBLIC), "reject")
def test_wrong_publisher_and_forged_publisher_signature_rejected(self):
self.assertEqual(self.decode(envelope(), publisher=OTHER_PUBLIC), "reject")
self.assertEqual(self.decode(envelope(publisher=OTHER)), "reject")
self.assertNotEqual(self.decode(envelope(publisher=OTHER), publisher=OTHER_PUBLIC), "reject")
def test_signature_is_bound_to_the_encryption_channel(self):
different = bytes(range(17, 33))
original = envelope()
self.assertEqual(self.decode(original, channel=different), "reject")
self.assertEqual(self.decode(envelope(channel=different)), "reject")
self.assertNotEqual(self.decode(envelope(channel=different), channel=different), "reject")
def test_every_signature_byte_is_verified(self):
original = envelope()
for offset in range(len(original) - 64, len(original)):
with self.subTest(signature_byte=offset):
altered = bytearray(original)
altered[offset] ^= 1
self.assertEqual(self.decode(altered), "reject")
def test_tampered_command_that_remains_allowed_is_rejected(self):
original = envelope(command="set flood.max 4")
# Equal-length valid commands exercise cryptographic rejection, rather
# than relying on the allowlist rejecting the changed text.
other = envelope(command="set flood.max 5")
forged = other[:-64] + original[-64:]
self.assertEqual(self.decode(forged), "reject")
def test_every_header_field_and_target_is_signed(self):
for changed in [
envelope(sequence=NOW + 1)[:-64],
envelope(expires=NOW + 121)[:-64],
envelope(target=hashlib.sha256(PUBLIC).digest()[:16])[:-64],
envelope(command="get tempradio2")[:-64]]:
with self.subTest(header=changed.hex()):
self.assertEqual(self.decode(changed + envelope()[-64:]), "reject")
def test_signed_forbidden_commands_and_multistatement_text_rejected(self):
for command in ["get prv.key", "setperm 0102 3", "start ota", "reboot",
"get radio2\nreboot", "xy|set radio2 off", "get radio2;reboot",
"get radio2\0", "get flood.filter.secret", "get radio2.scan"]:
with self.subTest(command=command):
self.assertEqual(self.decode(envelope(command)), "reject")
def test_exact_and_oversized_command_boundaries(self):
maximum = "set flood.rule.1 " + "a" * (72 - len("set flood.rule.1 "))
accepted = envelope(maximum)
self.assertEqual(len(accepted), 165)
self.assertNotEqual(self.decode(accepted), "reject")
self.assertEqual(self.decode(envelope(maximum + "a")), "reject")
def test_valid_signature_does_not_override_clock_policy(self):
for sequence, expires, now in [
(NOW, NOW - 1, NOW), (NOW, NOW + 601, NOW),
(NOW + 61, NOW + 120, NOW), (NOW, NOW + 120, NOW + 121),
(1735689599, 1735689600, 1735689600), (NOW, NOW + 120, 0)]:
with self.subTest(sequence=sequence, expires=expires, now=now):
self.assertEqual(self.decode(envelope(sequence=sequence, expires=expires), now=now), "reject")
self.assertNotEqual(self.decode(envelope(sequence=NOW + 60, expires=NOW + 120)), "reject")
self.assertNotEqual(self.decode(envelope(), now=NOW + 120), "reject")
def test_truncation_extra_padding_and_changed_length_rejected(self):
original = envelope()
for length in [0, 1, 28, 29, 92, len(original) - 1]:
with self.subTest(length=length):
self.assertEqual(self.decode(original[:length]), "reject")
self.assertEqual(self.decode(original + b"\0"), "reject")
altered = bytearray(original)
altered[28] = 0
self.assertEqual(self.decode(altered), "reject")
def test_default_public_key_cannot_form_fleet_privacy_channel(self):
public_channel = bytes.fromhex("8b3387e9c5cdea6ac9e5edbaa115cd72")
for channel in [b"\0" * 16, public_channel]:
with self.subTest(channel=channel.hex()):
self.assertEqual(self.decode(envelope(channel=channel), channel=channel), "reject")
self.assertEqual(self.run_tool("encode", channel.hex(), "all", NOW, NOW + 120, "get radio2"), "reject")
if __name__ == "__main__":
unittest.main()
+376
View File
@@ -0,0 +1,376 @@
#!/usr/bin/env python3
"""Execute production role hooks around the separately tested fleet receiver."""
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
#include <SHA256.h>
#define MESH_ENABLE_CLOCK_SYNC 0
#define MAX_TKS_ENTRIES 16
static std::vector<int> events;
namespace mesh {
struct Packet {
bool scoped = false;
uint16_t transport_codes[2] = {};
uint8_t payload[32] = {}, payload_len = 32, type = 5;
bool hasTransportCodes() const { return scoped; }
uint8_t getPayloadType() const { return type; }
};
struct Mesh {
virtual ~Mesh() = default;
virtual bool supportsFleetControl() const { return false; }
virtual void onSendComplete(Packet*) { events.push_back(1); }
virtual void onSendFail(Packet*) { events.push_back(3); }
virtual void onRadioProfileCopyQueued(Packet*, const Packet*, uint8_t) { events.push_back(5); }
};
}
struct TransportKey {
uint8_t key[16] = {};
uint16_t calcTransportCode(const mesh::Packet*) const;
};
struct Region { bool wildcard = false; bool isWildcard() const { return wildcard; } };
struct Regions {
std::vector<TransportKey> keys;
unsigned calls = 0;
int getTransportKeysFor(const Region&, TransportKey* out, int capacity) {
++calls;
const int count = std::min<int>(keys.size(), capacity);
for (int i = 0; i < count; ++i) out[i] = keys[i];
return count;
}
};
@SCOPE_HELPERS@
struct Profiles {};
struct Fleet {
unsigned received = 0, serviced = 0;
bool scoped = false;
TransportKey reply_scope;
uint32_t sequence = 1735689610;
char command[80] = "set radio2 off", reply[160] = {};
void receive(mesh::Packet*, mesh::Mesh&, const TransportKey* scope) {
++received; scoped = scope != nullptr;
if (scope) reply_scope = *scope;
}
void complete(mesh::Packet*, Profiles&) { events.push_back(2); }
void fail(mesh::Packet*, Profiles&) { events.push_back(4); }
void copy(mesh::Packet*, const mesh::Packet*) { events.push_back(6); }
template<typename Handler>
void service(mesh::Mesh&, Profiles&, const char* name, Handler handler) {
assert(!strcmp(name, "fleet node")); ++serviced;
handler(sequence, command, reply);
}
};
struct CLI {
Fleet fleet;
Profiles profiles;
bool enrolled = true;
Fleet* fleetChannel() { return enrolled ? &fleet : nullptr; }
Profiles& radioProfiles() { return profiles; }
};
struct ClockSync {
unsigned observed = 0;
void observeGroupPacket(mesh::Packet*) { ++observed; }
};
class Repeater : public mesh::Mesh {
public:
CLI _cli;
bool region_load_active = false;
Regions region_map;
Region region;
Region* recv_pkt_region = &region;
struct { const char* node_name = "fleet node"; } _prefs;
unsigned commands = 0;
uint32_t timestamp = 0;
@REPEATER_CAPABILITY@
void onGroupPacketRecv(mesh::Packet*);
void serviceFleet();
void handleCommand(uint32_t value, void* sender, char* command, char* reply) {
assert(sender == nullptr && !strcmp(command, "set radio2 off"));
timestamp = value; ++commands; strcpy(reply, "OK");
}
};
class Room : public mesh::Mesh {
public:
CLI _cli;
ClockSync _clock_sync;
bool region_load_active = false;
Regions region_map;
Region region;
Region* recv_pkt_region = &region;
struct { const char* node_name = "fleet node"; } _prefs;
unsigned commands = 0;
uint32_t timestamp = 0;
@ROOM_CAPABILITY@
void onGroupPacketRecv(mesh::Packet*);
void serviceFleet();
#if MESH_ENABLE_FLEET_CONTROL
void onSendComplete(mesh::Packet*) override;
void onSendFail(mesh::Packet*) override;
void onRadioProfileCopyQueued(mesh::Packet*, const mesh::Packet*, uint8_t) override;
#endif
void handleCommand(uint32_t value, char* command, char* reply) {
assert(!strcmp(command, "set radio2 off"));
timestamp = value; ++commands; strcpy(reply, "OK");
}
};
@METHODS@
template<typename Role>
void checkRole() {
Role role;
mesh::Packet packet;
TransportKey original, rotated;
memset(original.key, 0x11, sizeof(original.key));
memset(rotated.key, 0x22, sizeof(rotated.key));
role.region_map.keys = {original, rotated};
packet.scoped = true;
packet.transport_codes[0] = rotated.calcTransportCode(&packet);
assert(role.supportsFleetControl());
role.onGroupPacketRecv(&packet);
role.serviceFleet();
#if MESH_ENABLE_FLEET_CONTROL
assert(role._cli.fleet.received == 1 && role._cli.fleet.serviced == 1);
assert(role._cli.fleet.scoped);
assert(!memcmp(role._cli.fleet.reply_scope.key, rotated.key, sizeof(rotated.key)));
assert(role.commands == 1 && role.timestamp == role._cli.fleet.sequence);
role.region_load_active = true;
role.serviceFleet();
assert(role.commands == 1);
assert(strstr(role._cli.fleet.reply, "region load active"));
#else
assert(role._cli.fleet.received == 0 && role._cli.fleet.serviced == 0);
assert(role.commands == 0);
#endif
role._cli.enrolled = false;
const unsigned before = role._cli.fleet.serviced;
role.onGroupPacketRecv(&packet);
role.serviceFleet();
assert(role._cli.fleet.serviced == before);
}
static void checkScopeSelection() {
Regions regions;
Region region;
mesh::Packet packet;
TransportKey first, rotated, selected;
memset(first.key, 0x31, sizeof(first.key));
memset(rotated.key, 0xA4, sizeof(rotated.key));
regions.keys = {first, rotated};
assert(!mesh::captureFleetReplyScope(regions, &region, &packet, selected));
assert(regions.calls == 0);
packet.scoped = true;
packet.payload[0] = 0x6D;
packet.transport_codes[1] = rotated.calcTransportCode(&packet);
assert(mesh::captureFleetReplyScope(regions, &region, &packet, selected));
assert(!memcmp(selected.key, rotated.key, sizeof(rotated.key)));
// Codes belong to the received payload. Changing it invalidates the match.
packet.payload[0] ^= 1;
assert(!mesh::captureFleetReplyScope(regions, &region, &packet, selected));
packet.payload[0] ^= 1;
packet.transport_codes[0] = first.calcTransportCode(&packet);
packet.transport_codes[1] = 0;
assert(mesh::captureFleetReplyScope(regions, &region, &packet, selected));
assert(!memcmp(selected.key, first.key, sizeof(first.key)));
region.wildcard = true;
const unsigned calls = regions.calls;
assert(!mesh::captureFleetReplyScope(regions, &region, &packet, selected));
assert(!mesh::captureFleetReplyScope(regions, (const Region*)nullptr, &packet, selected));
assert(!mesh::captureFleetReplyScope(regions, &region, nullptr, selected));
assert(regions.calls == calls);
}
int main() {
checkScopeSelection();
checkRole<Repeater>(); checkRole<Room>();
Room room; mesh::Packet first, copy;
room.onGroupPacketRecv(&first);
assert(room._clock_sync.observed == 1); // Existing clock delivery survives.
room.onSendComplete(&first);
room.onSendFail(&first);
room.onRadioProfileCopyQueued(&copy, &first, 7);
#if MESH_ENABLE_FLEET_CONTROL
assert((events == std::vector<int>{1,2,3,4,5,6}));
#else
assert((events == std::vector<int>{1,3,5}));
#endif
puts("Fleet role receive, deferred dispatch, busy guard and callbacks passed");
}
'''
AUTHORIZATION = r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <new>
#include <initializer_list>
static unsigned configurations = 0, allocations = 0;
namespace mesh {
struct FleetChannel {
explicit FleetChannel(void*) { ++allocations; }
bool handleConfig(const char*, char* reply, size_t capacity) {
assert(capacity == 160); ++configurations; strcpy(reply, "OK"); return true;
}
};
}
struct Callbacks {
bool supported = true;
bool supportsFleetControl() const { return supported; }
};
class CommonCLI {
public:
Callbacks callbacks;
Callbacks* _callbacks = &callbacks;
void* _management_fs = nullptr;
mesh::FleetChannel* _fleet_channel = nullptr;
~CommonCLI() { delete _fleet_channel; }
bool handleFleetCommand(const char*, char*);
};
struct ClientInfo {
enum Role { Guest, Admin, RegionManager, FilterManager } role;
bool isAdmin() const { return role == Admin; }
bool isRegionMgr() const { return role == RegionManager; }
bool isFilterMgr() const { return role == FilterManager; }
};
@MANAGER_HELPERS@
@COMMON_FLEET@
static void dispatch(CommonCLI& cli, ClientInfo* sender, const char* command, char* reply) {
@MANAGER_GUARD@
if (!cli.handleFleetCommand(command, reply)) strcpy(reply, "unhandled");
}
int main() {
CommonCLI cli;
char reply[160] = {};
ClientInfo admin{ClientInfo::Admin};
for (const char* command : {"get fleet.channel", "get fleet.controller", "get fleet.stats",
"set fleet.channel off", "set fleet.controller 010203"}) {
for (auto role : {ClientInfo::Guest, ClientInfo::RegionManager, ClientInfo::FilterManager}) {
ClientInfo sender{role};
const unsigned before = configurations;
dispatch(cli, &sender, command, reply);
assert(!strcmp(reply, "Err - not permitted"));
assert(configurations == before);
assert(!isRegionMgrAllowed(command) && !isFilterMgrAllowed(command));
}
dispatch(cli, &admin, command, reply);
assert(!strcmp(reply, "OK"));
dispatch(cli, nullptr, command, reply);
assert(!strcmp(reply, "OK"));
}
assert(allocations == 1 && configurations == 10);
assert(isFilterMgrAllowed("set flood.filter.1 group-text 4"));
assert(isRegionMgrAllowed("set flood.channel.scope public #local"));
assert(!isFilterMgrAllowed("set radio2 off"));
assert(!isRegionMgrAllowed("get password"));
CommonCLI unsupported;
unsupported.callbacks.supported = false;
assert(unsupported.handleFleetCommand("set fleet.channel off", reply));
assert(strstr(reply, "infrastructure roles"));
assert(allocations == 1 && configurations == 10);
assert(!cli.handleFleetCommand("set fleeter.channel off", reply));
puts("Fleet enrollment remains restricted to local/admin infrastructure dispatch");
}
'''
class FleetRoleWiringTests(unittest.TestCase):
def test_enrollment_common_cli_stays_behind_live_manager_permissions(self):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler)
repeater = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
helpers = "\n".join(body(repeater, signature) for signature in (
"static bool commandFamilyMatches(", "static bool isCommonManagerReadOnlyAllowed(",
"static bool isRegionMgrAllowed(", "static bool isFilterMgrAllowed(",
))
handler = body(repeater, "void MyMesh::handleCommand(uint32_t sender_timestamp,")
guard_start = handler.rindex("if (sender && !sender->isAdmin())", 0,
handler.index("mesh::cli::handleACLGet("))
guard = body(handler[guard_start:], "if (sender && !sender->isAdmin())")
common = body((ROOT / "src/helpers/CommonCLI_Management.cpp").read_text(),
"bool CommonCLI::handleFleetCommand(")
harness = AUTHORIZATION.replace("@MANAGER_HELPERS@", helpers)
harness = harness.replace("@COMMON_FLEET@", common).replace("@MANAGER_GUARD@", guard)
with tempfile.TemporaryDirectory(prefix="meshcore-fleet-authorization-") as directory:
work = Path(directory)
source = work / "fixture.cpp"
source.write_text(harness)
for extended_filters in (0, 1):
with self.subTest(extended_filters=extended_filters):
executable = work / str(extended_filters)
built = subprocess.run([
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-DMESH_ENABLE_FLOOD_RULE_ENGINE=" + str(extended_filters),
str(source), "-o", str(executable),
], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
checked = subprocess.run([str(executable)], capture_output=True,
text=True, timeout=10)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
def test_roles_with_feature_enabled_and_disabled(self):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler)
methods = []
capabilities = {}
for role, folder in (("Repeater", "simple_repeater"), ("Room", "simple_room_server")):
source = (ROOT / "examples" / folder / "MyMesh.cpp").read_text()
header = (ROOT / "examples" / folder / "MyMesh.h").read_text()
capabilities[role] = body(header, "bool supportsFleetControl() const override")
methods.append(body(source, "void MyMesh::onGroupPacketRecv(").replace("MyMesh::", role + "::", 1))
loop = body(source, "void MyMesh::loop()")
fleet = body(loop, "if (_cli.fleetChannel())")
methods.append("void " + role + "::serviceFleet() {\n#if MESH_ENABLE_FLEET_CONTROL\n"
+ fleet + "\n#endif\n}\n")
if role == "Room":
for signature in ("void MyMesh::onSendComplete(", "void MyMesh::onSendFail(",
"void MyMesh::onRadioProfileCopyQueued("):
methods.append("#if MESH_ENABLE_FLEET_CONTROL\n"
+ body(source, signature).replace("MyMesh::", role + "::", 1)
+ "\n#endif\n")
harness = HARNESS.replace("@METHODS@", "\n".join(methods))
scope = body((ROOT / "src/helpers/FleetChannel.h").read_text(),
"template <typename Regions, typename Region>")
transport_code = body((ROOT / "src/helpers/TransportKeyStore.cpp").read_text(),
"uint16_t TransportKey::calcTransportCode(")
# The host SHA-256 shim has a byte-pointer output API; on-device Crypto
# accepts void*. Preserve the production algorithm with that cast only.
transport_code = transport_code.replace("&code, 2", "(uint8_t*)&code, 2")
harness = harness.replace("@SCOPE_HELPERS@", transport_code + "\nnamespace mesh {\n" + scope + "\n}\n")
harness = harness.replace("@REPEATER_CAPABILITY@", capabilities["Repeater"])
harness = harness.replace("@ROOM_CAPABILITY@", capabilities["Room"])
with tempfile.TemporaryDirectory(prefix="meshcore-fleet-role-") as directory:
work = Path(directory)
fixture = work / "fixture.cpp"
fixture.write_text(harness)
for enabled in (0, 1):
with self.subTest(fleet_control=enabled):
executable = work / str(enabled)
built = subprocess.run([
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-isystem", str(ROOT / "test/mocks"),
"-DMESH_ENABLE_FLEET_CONTROL=" + str(enabled),
str(fixture), "-o", str(executable),
], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
checked = subprocess.run([str(executable)], capture_output=True,
text=True, timeout=10)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
if __name__ == "__main__":
unittest.main()
+8 -1
View File
@@ -20,6 +20,7 @@ PREAMBLE = r'''
#include <helpers/CLICommandUtils.h>
#include <helpers/RadioProfileCommandUtils.h>
#include <helpers/BatteryChargeCLI.h>
#include "companion_fleet_dispatch.h"
#define ESP32 1
#define WIFI_SSID "test"
#define WITH_MQTT_BRIDGE 1
@@ -113,7 +114,7 @@ struct WebConfigServer {
};
const char* FIRMWARE_VERSION="test";
const char* FIRMWARE_BUILD_DATE="test";
struct MyMesh {
struct MyMesh : CompanionFleetFixture {
mesh::RadioProfileCLI _radio_profiles;
Identity self_id;
Store store; Store* _store=&store;
@@ -163,6 +164,11 @@ SCENARIOS = r'''
int main() {
MyMesh node;
char reply[160] = {};
for (uint32_t timestamp : {0u, 100u}) {
node.onCLICommandRecv(ContactInfo{}, nullptr, timestamp,
"fleet send 1 all set radio2 off", reply);
assert(strstr(reply, "requires local Companion access"));
}
assert(node.handleCommand("set charge.voltage 4.1",0,reply));
assert(board.charge_target==4100 && strstr(reply,"saved"));
for(uint32_t stamp:{0u,100u}) {
@@ -365,6 +371,7 @@ int main() {
binary = Path(temp) / "test"
result = subprocess.run([
"c++", "-std=c++17", "-x", "c++", "-", "-I"+str(ROOT / "src"),
"-I"+str(ROOT / "test/fixtures"),
"-DENABLE_PRIVATE_KEY_EXPORT="+str(export),
*(["-DWITH_WEBCONFIG=1"] if webconfig else []),
*SANITIZER_FLAGS, "-o", str(binary),