Files
MeshCore/test/test_mqtt_advert_verify.py
T
mikecarper b5e493996c Merge observer firmware with persistent opt-in advert signature verification
Add Waveshare LR1121 observer recipes. Adapt observer advert verification to existing transactional CLI, append-only binary preferences and Companion NVS, preserving default file lengths and WiFi credentials. Reject oversized unsigned advert tails consistently with Mesh::onRecvPacket. Keep current BLE recovery and GPS claim accounting.
2026-10-15 13:45:27 -07:00

133 lines
5.3 KiB
Python

"""Exercise the production MQTT advert gate and transactional CLI on the host."""
from pathlib import Path
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
def compile_run(source):
with tempfile.TemporaryDirectory(prefix="mqtt-advert-verify-") as directory:
cpp, binary = Path(directory) / "test.cpp", Path(directory) / "test"
cpp.write_text(source, encoding="utf-8")
result = subprocess.run([
"g++", "-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie", "-I", str(ROOT / "src"),
str(cpp), "-o", str(binary),
], text=True, capture_output=True)
if result.returncode:
raise AssertionError(result.stdout + result.stderr)
subprocess.run([str(binary)], check=True)
class MqttAdvertVerifyTests(unittest.TestCase):
def test_actual_queue_gate_rejects_bad_and_unsigned_tails_only_when_enabled(self):
source = (ROOT / "src/helpers/bridges/MQTTBridge.cpp").read_text()
method = extract_braced(source, "void MQTTBridge::onPacketReceived(")
compile_run(r'''
#include <cassert>
#include <initializer_list>
#include <cstdint>
#include <cstring>
#include <helpers/AdvertSignature.h>
constexpr unsigned PAYLOAD_TYPE_ADVERT=4, PAYLOAD_TYPE_TEXT=2;
static unsigned verifies=0;
static bool valid_signature=true;
namespace mesh {
struct Packet {
uint8_t payload[184] = {};
int payload_len=100;
unsigned type=PAYLOAD_TYPE_ADVERT;
unsigned getPayloadType() const { return type; }
};
struct Identity {
const uint8_t* pub;
explicit Identity(const uint8_t* p) : pub(p) {}
bool verify(const uint8_t* sig, const uint8_t* message, int size) {
++verifies;
assert(sig == pub+36 && size>=36 && size<=68);
assert(!memcmp(pub,message,36));
if(size>36) assert(!memcmp(pub+100,message+36,size-36));
return valid_signature;
}
};
}
struct MQTTBridge {
struct Obs { bool mqtt_packets_enabled=true, mqtt_rx_enabled=true, mqtt_advert_verify=false; } obs;
Obs* _obs=&obs;
bool _initialized=true, type_allowed=true, scope_allowed=true;
struct Stop { bool value=false; bool load(int)const{return value;} } _stop_requested;
unsigned _filtered_packets=0, _bad_sig_adverts=0, queued=0;
bool shouldQueuePacketType(unsigned, bool& filtered) { filtered=!type_allowed; return type_allowed; }
bool allowsPacket(mesh::Packet*) { return scope_allowed; }
void queuePacket(mesh::Packet*,bool tx){assert(!tx);++queued;}
void onPacketReceived(mesh::Packet*);
};
namespace std { constexpr int memory_order_acquire=0; }
''' + method + r'''
int main() {
MQTTBridge bridge; mesh::Packet packet;
for(unsigned i=0;i<sizeof(packet.payload);++i) packet.payload[i]=uint8_t(i);
// Default off preserves the observer's existing publish-as-heard behavior.
valid_signature=false; bridge.onPacketReceived(&packet);
assert(bridge.queued==1 && verifies==0 && bridge._bad_sig_adverts==0);
bridge.obs.mqtt_advert_verify=true;
bridge.onPacketReceived(&packet);
assert(bridge.queued==1 && verifies==1 && bridge._bad_sig_adverts==1);
valid_signature=true; packet.payload_len=132;
bridge.onPacketReceived(&packet);
assert(bridge.queued==2 && verifies==2);
// Valid signed prefixes cannot bless an appended unsigned byte.
for(int size : {99,133,184}) {
packet.payload_len=size; bridge.onPacketReceived(&packet);
assert(bridge.queued==2 && verifies==2);
}
assert(bridge._bad_sig_adverts==4);
packet.type=PAYLOAD_TYPE_TEXT; bridge.onPacketReceived(&packet);
assert(bridge.queued==3 && verifies==2);
packet.type=PAYLOAD_TYPE_ADVERT; packet.payload_len=100;
bridge.scope_allowed=false; bridge.onPacketReceived(&packet);
assert(bridge.queued==3 && verifies==2 && bridge._filtered_packets==1);
bridge.scope_allowed=true; bridge.type_allowed=false; bridge.onPacketReceived(&packet);
assert(bridge.queued==3 && verifies==2 && bridge._filtered_packets==2);
}
''')
def test_actual_observer_setter_rejects_typos_and_rolls_back_failed_saves(self):
source = (ROOT / "src/helpers/CommonCLI_Observer.cpp").read_text()
branch = extract_braced(source, 'if (memcmp(config, "mqtt.advert.verify ", 19) == 0)')
compile_run(r'''
#include <cassert>
#include <cstdint>
#include <cstring>
struct CLI {
struct { uint8_t mqtt_advert_verify=0; } _mqtt_prefs;
bool save_ok=true; unsigned saves=0;
bool saveObserverPrefs(){++saves;return save_ok;}
bool set(const char* config,char* reply) {
''' + branch + r'''
else return false;
return true;
}
};
int main() {
CLI cli; char reply[160]={};
assert(cli.set("mqtt.advert.verify on",reply));
assert(cli.saves==1 && cli._mqtt_prefs.mqtt_advert_verify==1 && !strcmp(reply,"OK"));
assert(cli.set("mqtt.advert.verify onx",reply));
assert(cli.saves==1 && cli._mqtt_prefs.mqtt_advert_verify==1 && !strncmp(reply,"Error:",6));
cli.save_ok=false; assert(cli.set("mqtt.advert.verify off",reply));
assert(cli.saves==2 && cli._mqtt_prefs.mqtt_advert_verify==1 && !strncmp(reply,"Error:",6));
cli.save_ok=true; assert(cli.set("mqtt.advert.verify off",reply));
assert(cli.saves==3 && cli._mqtt_prefs.mqtt_advert_verify==0 && !strcmp(reply,"OK"));
}
''')
if __name__ == "__main__":
unittest.main()