#!/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 #include #include #include #include #include #include #include #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='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 #include #include #include #include #include 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> 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 #include #include #include #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 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 copying; std::function failure; std::vector 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 #include #include #include #include #include @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 targetsCommand(const LocalIdentity& publisher,uint32_t sequence,const std::string& list, const char* text="get radio2"){ FleetCommand::Targets targets;assert(FleetCommand::parseTargets(list.c_str(),list.size(),targets)); uint8_t payload[184]={}; const size_t length=FleetCommand::encode(publisher,channel_key,sequence,sequence+300,targets,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 shortNewAll(const LocalIdentity& publisher,uint32_t sequence){ constexpr const char* text="get radio2"; uint8_t data[184]={},message[160]={};uint8_t* payload=data+3; memcpy(payload,"FMC2",4);storage::writeLE32(payload+4,sequence); storage::writeLE32(payload+8,sequence+300);payload[12]=0; const size_t text_size=strlen(text);payload[13]=uint8_t(text_size);memcpy(payload+14,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,14+text_size); publisher.sign(payload+14+text_size,message,int(domain_size+16+14+text_size)); data[0]=uint8_t(FleetCommand::DataType);data[1]=uint8_t(FleetCommand::DataType>>8); data[2]=uint8_t(14+text_size+64);return raw(data,3+data[2]); } static std::string publicHex(const Identity& identity){ char text[65];Utils::toHex(text,identity.pub_key,32);return text; } 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")||!strcmp(text,"get radio2")); 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& 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 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> 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,®ion,&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,®ion,&packet,captured)); assert(!captureFleetReplyScope(regions,static_cast(nullptr),&packet,captured)); region.wildcard=false;packet.transport_codes[0]^=1; assert(!captureFleetReplyScope(regions,®ion,&packet,captured)); }else if(scenario=="fmc2_short_all"){ enroll(fleet,publisher);Packet packet=shortNewAll(publisher,mesh.clock.now); uint8_t plaintext[184]={};assert(Utils::MACThenDecrypt(channel_key,plaintext,packet.payload+1,packet.payload_len-1)>0); assert(plaintext[2]0); assert(plaintext[2]==FleetCommand::HeaderSize+FleetCommand::SignatureSize); assert(!memcmp(plaintext+3,"FMC2",4)); assert(apply(fleet,fs,mesh,profiles,packet)==1);assert(last_jitter_max==60500); FleetChannel rebooted(&fs);assert(apply(rebooted,fs,mesh,profiles,packet)==0); }else if(scenario=="fmc2_overlap_matches_once"){ enroll(fleet,publisher);const std::string mine=publicHex(mesh.self_id); Packet packet=targetsCommand(publisher,mesh.clock.now,mine.substr(0,8)+","+mine); assert(apply(fleet,fs,mesh,profiles,packet)==1);assert(mesh.queued==1&&last_jitter_max==60500); assert(config(fleet,"get fleet.stats").find("accepted=1")!=std::string::npos); }else if(scenario=="fmc2_nonmatching_no_reserve"){ enroll(fleet,publisher);const auto previous=fs.files.at(Path);LocalIdentity stranger; char different_prefix[9];uint8_t changed[4];memcpy(changed,mesh.self_id.pub_key,4);changed[0]^=1; Utils::toHex(different_prefix,changed,4); Packet packet=targetsCommand(publisher,mesh.clock.now,std::string(different_prefix)+","+publicHex(stranger)); assert(apply(fleet,fs,mesh,profiles,packet)==0);assert(mesh.queued==0&&fs.files.at(Path)==previous); packet=targetsCommand(publisher,mesh.clock.now,publicHex(stranger)+","+publicHex(mesh.self_id)); assert(apply(fleet,fs,mesh,profiles,packet)==1);assert(last_jitter_max==60500); }else if(scenario=="fmc2_target_tamper"){ enroll(fleet,publisher);const auto previous=fs.files.at(Path); Packet good=targetsCommand(publisher,mesh.clock.now,publicHex(mesh.self_id).substr(0,12)); uint8_t plaintext[184]={};const int length=Utils::MACThenDecrypt(channel_key,plaintext,good.payload+1,good.payload_len-1); assert(length>0);plaintext[3+13]^=1;Packet changed=raw(plaintext,length); assert(apply(fleet,fs,mesh,profiles,changed)==0);assert(fs.files.at(Path)==previous); assert(apply(fleet,fs,mesh,profiles,good)==1); }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 \n', "Adafruit_LittleFS.h": ''' #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', "Arduino.h": '#pragma once\n#include \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(out),length); }} void finalizeHMAC(const uint8_t* key,size_t size,void* out,size_t length) {{ SHA256Base::finalizeHMAC(key,size,static_cast(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_new_short_broadcast_envelope_dispatches_and_preserves_replay_protection(self): self.scenario("fmc2_short_all") def test_new_public_key_prefix_envelope_dispatches_and_staggers_possible_multiple_acks(self): self.scenario("fmc2_short_prefix") def test_overlapping_prefix_and_full_key_matches_execute_once(self): self.scenario("fmc2_overlap_matches_once") def test_nonmatching_target_list_never_reserves_sequence_and_matching_list_can_reuse_it(self): self.scenario("fmc2_nonmatching_no_reserve") def test_modified_new_target_header_never_dispatches_without_valid_signature(self): self.scenario("fmc2_target_tamper") def test_existing_remote_radio_mutation_blocks_fleet_without_reserving_sequence(self): self.scenario("existing_mutation") if __name__ == "__main__": unittest.main()