Files
MeshCore/test/test_companion_channel_alias.py
T

657 lines
31 KiB
Python

"""Execute Companion channel aliases with independent SHA-256 test vectors."""
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
HASH_NAMES = (
"#emergency", "#r2-emergency", "#mt-emergency",
"#custom", "#r2-custom", "#mt-custom",
)
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <initializer_list>
#include <helpers/CompanionChannelAlias.h>
struct DigestVector { const char* text; uint8_t digest[32]; };
static const DigestVector vectors[] = { @VECTORS@ };
struct KeyDigestVector { uint8_t key[32]; size_t length; uint8_t digest[32]; };
static const KeyDigestVector key_vectors[] = { @KEY_VECTORS@ };
static void sha256(uint8_t* out, size_t out_length, const uint8_t* in, int in_length) {
assert(out && in && in_length >= 0 && out_length <= 32);
for (const auto& vector : vectors) {
if (strlen(vector.text) == static_cast<size_t>(in_length)
&& !memcmp(vector.text, in, in_length)) {
memcpy(out, vector.digest, out_length);
return;
}
}
for (const auto& vector : key_vectors) {
if (vector.length == static_cast<size_t>(in_length) && !memcmp(vector.key, in, in_length)) {
memcpy(out, vector.digest, out_length);
return;
}
}
assert(!"unexpected input to independent SHA-256 oracle");
}
static void hashtagKey(const char* name, uint8_t out[32]) {
memset(out, 0, 32);
sha256(out, 16, reinterpret_cast<const uint8_t*>(name), strlen(name));
}
static void assertAlias(const char* name, CompanionChannelKind kind,
bool hashtag, const char* tail) {
const auto parsed = parseCompanionChannelAlias(name);
assert(parsed.kind == kind);
assert(parsed.hashtag == hashtag);
assert(parsed.tail && !strcmp(parsed.tail, tail));
}
static void grammar() {
assertAlias("", CompanionChannelKind::MeshCore, false, "");
assert(parseCompanionChannelAlias(nullptr).kind == CompanionChannelKind::Invalid);
assertAlias("Public", CompanionChannelKind::MeshCore, false, "Public");
assertAlias("#emergency", CompanionChannelKind::MeshCore, true, "emergency");
assertAlias("r2-Public", CompanionChannelKind::Radio2, false, "Public");
assertAlias("#r2-emergency", CompanionChannelKind::Radio2, true, "emergency");
assertAlias("mt-LongFast", CompanionChannelKind::Meshtext, false, "LongFast");
assertAlias("#mt-emergency", CompanionChannelKind::Meshtext, true, "emergency");
// Routing markers must be complete, leading and case-sensitive.
assertAlias("r2", CompanionChannelKind::MeshCore, false, "r2");
assertAlias("mt", CompanionChannelKind::MeshCore, false, "mt");
assertAlias("R2-Public", CompanionChannelKind::MeshCore, false, "R2-Public");
assertAlias("#MT-emergency", CompanionChannelKind::MeshCore, true, "MT-emergency");
assertAlias("room-r2-Public", CompanionChannelKind::MeshCore, false, "room-r2-Public");
for (const auto* name : {"r2-", "#r2-", "mt-", "#mt-",
"r2-#room", "mt-#room", "#r2-#room", "#mt-#room",
"r2-r2-room", "r2-mt-room", "mt-r2-room", "mt-mt-room",
"#r2-r2-room", "#r2-mt-room", "#mt-r2-room", "#mt-mt-room"}) {
assert(parseCompanionChannelAlias(name).kind == CompanionChannelKind::Invalid);
}
puts("PASS grammar");
}
static void canonical() {
struct Case { const char* display; const char* canonical; };
const Case cases[] = {{"Public", "Public"}, {"#emergency", "#emergency"},
{"r2-Public", "Public"}, {"#r2-emergency", "#emergency"},
{"mt-LongFast", "LongFast"}, {"#mt-emergency", "#emergency"}};
for (const auto& item : cases) {
char output[40];
assert(companionChannelCanonicalName(item.display, output, sizeof(output)));
assert(!strcmp(output, item.canonical));
// The exact terminating-NUL boundary must be accepted, never truncated.
assert(companionChannelCanonicalName(item.display, output, strlen(item.canonical) + 1));
assert(!strcmp(output, item.canonical));
assert(!companionChannelCanonicalName(item.display, output, strlen(item.canonical)));
}
char output[40];
assert(!companionChannelCanonicalName("#mt-", output, sizeof(output)));
assert(!companionChannelCanonicalName("#r2-mt-room", output, sizeof(output)));
assert(!companionChannelCanonicalName("#r2-emergency", nullptr, 40));
assert(!companionChannelCanonicalName(nullptr, output, sizeof(output)));
assert(!companionChannelCanonicalName("#r2-emergency", output, 0));
puts("PASS canonical");
}
static void keys() {
uint8_t canonical[32], alias[32], key[32], before[32];
hashtagKey("#emergency", canonical);
// Fixed published emergency vector also checks the Python-generated oracle.
const uint8_t emergency[16] = {0xe1,0xad,0x57,0x8d,0x25,0x10,0x8e,0x34,
0x48,0x08,0xf3,0x0d,0xfd,0xaa,0xf9,0x26};
assert(!memcmp(canonical, emergency, sizeof(emergency)));
for (const auto* name : {"#r2-emergency", "#mt-emergency"}) {
hashtagKey(name, alias);
memcpy(key, alias, 32);
resolveCompanionChannelKey(name, key, sha256);
assert(!memcmp(key, canonical, 32));
// A second resolution of a wire copy must not derive a new key.
resolveCompanionChannelKey(name, key, sha256);
assert(!memcmp(key, canonical, 32));
assert(memcmp(alias, canonical, 16));
}
hashtagKey("#custom", canonical);
for (const auto* name : {"#r2-custom", "#mt-custom"}) {
hashtagKey(name, key);
resolveCompanionChannelKey(name, key, sha256);
assert(!memcmp(key, canonical, 32));
}
// Unprefixed hashtag keys and non-hashtag channel keys retain exact bytes.
hashtagKey("#emergency", key); memcpy(before, key, 32);
resolveCompanionChannelKey("#emergency", key, sha256);
assert(!memcmp(key, before, 32));
for (const auto* name : {"Public", "r2-Public", "mt-LongFast",
"#r2-emergency", "#mt-emergency", "#r2-mt-room"}) {
for (size_t i = 0; i < 16; ++i) key[i] = static_cast<uint8_t>(i + 1);
memset(key + 16, 0, 16); memcpy(before, key, 32);
resolveCompanionChannelKey(name, key, sha256);
assert(!memcmp(key, before, 32));
// A 256-bit custom PSK whose first half happens to equal the alias hash
// must remain a 256-bit key, rather than being replaced by a public key.
hashtagKey("#r2-emergency", key);
for (size_t i = 16; i < 32; ++i) key[i] = static_cast<uint8_t>(i + 1);
memcpy(before, key, 32);
resolveCompanionChannelKey(name, key, sha256);
assert(!memcmp(key, before, 32));
}
puts("PASS keys");
}
static void receive() {
for (const auto* name : {"Public", "#emergency"}) {
assert(companionChannelReceivePriority(name, 0) == 0);
assert(companionChannelReceivePriority(name, 1) == 1);
assert(companionChannelReceivePriority(name, 0, false) == 0);
assert(companionChannelReceivePriority(name, 1, false) == 1);
}
for (const auto* name : {"r2-Public", "#r2-emergency"}) {
assert(companionChannelReceivePriority(name, 0) == -1);
assert(companionChannelReceivePriority(name, 1) == 0);
assert(companionChannelReceivePriority(name, 0, false) == 0);
assert(companionChannelReceivePriority(name, 1, false) == 1);
}
for (const auto* name : {"mt-LongFast", "#mt-emergency"}) {
assert(companionChannelReceivePriority(name, 0) == -1);
assert(companionChannelReceivePriority(name, 1) == -1);
// Without a configured radio2, either prefix behaves as ordinary MeshCore.
assert(companionChannelReceivePriority(name, 0, false) == 0);
assert(companionChannelReceivePriority(name, 1, false) == 1);
}
for (const auto* name : {"#mt-", "#r2-mt-emergency"}) {
for (uint8_t origin : {0, 1}) {
assert(companionChannelReceivePriority(name, origin, true) == -1);
assert(companionChannelReceivePriority(name, origin, false) == -1);
}
}
puts("PASS receive");
}
int main(int argc, char** argv) {
assert(argc == 2);
if (!strcmp(argv[1], "grammar")) grammar();
else if (!strcmp(argv[1], "canonical")) canonical();
else if (!strcmp(argv[1], "keys")) keys();
else if (!strcmp(argv[1], "receive")) receive();
else assert(!"unknown test case");
}
'''
RUNTIME_HARNESS = r'''
#include <RadioProfiles.h>
#define MAX_GROUP_CHANNELS 5
#define MAX_PACKET_PAYLOAD 255
namespace mesh {
struct GroupChannel { uint8_t secret[32] = {}, hash[1] = {}, tx_radio = RADIO_TX_AUTO; };
struct Packet { uint8_t radio_origin = 0, tx_radio = RADIO_TX_AUTO;
uint8_t payload[MAX_PACKET_PAYLOAD] = {}; int payload_len = 0; };
struct Utils {
static void sha256(uint8_t* out, size_t length, const uint8_t* in, int in_length) {
::sha256(out, length, in, in_length);
}
static bool isZeroes(const uint8_t* bytes, size_t length) {
for (size_t i = 0; i < length; ++i) if (bytes[i]) return false;
return true;
}
// The native test exercises the caller's authenticated-key gating, while
// substituting only the cipher primitive with an explicit key-match oracle.
static int MACThenDecrypt(const uint8_t* key, uint8_t*, const uint8_t* data, int length) {
return length == 16 && !memcmp(key, data, 16) ? 1 : 0;
}
};
}
struct ChannelDetails { mesh::GroupChannel channel; char name[32] = {}; };
struct Radio { mesh::RadioProfiles config; bool present = true;
mesh::RadioProfiles* profiles() { return present ? &config : nullptr; } };
class BaseChatMesh {
public:
ChannelDetails stored[MAX_GROUP_CHANNELS]; unsigned delivered = 0;
bool getChannel(int index, ChannelDetails& channel) {
if (index < 0 || index >= MAX_GROUP_CHANNELS) return false;
channel = stored[index]; return true;
}
bool setChannel(int index, const ChannelDetails& channel) {
if (index < 0 || index >= MAX_GROUP_CHANNELS) return false;
stored[index] = channel;
mesh::Utils::sha256(stored[index].channel.hash, sizeof(stored[index].channel.hash),
channel.channel.secret,
mesh::Utils::isZeroes(channel.channel.secret + 16, 16) ? 16 : 32);
return true;
}
void onGroupDataRecv(mesh::Packet*, uint8_t, const mesh::GroupChannel&, uint8_t*, size_t) {
++delivered;
}
};
class MyMesh : public BaseChatMesh {
public:
Radio radio; Radio* _radio = &radio;
bool isAliasRadio2Defined() const;
bool resolveChannelAlias(ChannelDetails&) const;
bool getChannel(int, ChannelDetails&);
bool setChannel(int, const ChannelDetails&);
int searchChannelsForPacket(const mesh::Packet*, const uint8_t*, mesh::GroupChannel[], int);
int findChannelIdxForPacket(const mesh::GroupChannel&, const mesh::Packet*);
bool allowGroupPacketOnRadio(mesh::Packet*);
void onGroupDataRecv(mesh::Packet*, uint8_t, const mesh::GroupChannel&, uint8_t*, size_t);
@SAVE_GETTER@
};
@METHODS@
static ChannelDetails channel(const char* name) {
ChannelDetails value;
strcpy(value.name, name);
hashtagKey(name, value.channel.secret);
return value;
}
int main() {
MyMesh node;
node.radio.config.secondary.mode = mesh::RadioProfileMode::RxTx;
auto ordinary = channel("#emergency");
auto secondary = channel("#r2-emergency");
auto mt = channel("#mt-emergency");
ordinary.channel.tx_radio = mesh::RADIO_TX_BOTH;
secondary.channel.tx_radio = mesh::RADIO_TX_PRIMARY;
mt.channel.tx_radio = mesh::RADIO_TX_BOTH;
assert(node.setChannel(0, ordinary));
assert(node.setChannel(1, secondary));
assert(node.setChannel(2, mt));
ChannelDetails wire, raw;
assert(node.getChannel(1, wire));
assert(wire.channel.tx_radio == mesh::RADIO_TX_SECONDARY);
assert(!memcmp(wire.channel.secret, ordinary.channel.secret, 32));
uint8_t expected_hash;
sha256(&expected_hash, 1, ordinary.channel.secret, 16);
assert(wire.channel.hash[0] == expected_hash);
assert(node.getChannelForSave(1, raw));
assert(!strcmp(raw.name, secondary.name));
assert(!memcmp(raw.channel.secret, secondary.channel.secret, 32));
assert(raw.channel.tx_radio == mesh::RADIO_TX_PRIMARY);
// Applying the raw GET/persistence representation again must be idempotent.
assert(node.setChannel(1, raw) && node.getChannel(1, wire));
assert(!memcmp(wire.channel.secret, ordinary.channel.secret, 32));
assert(node.getChannel(2, wire));
assert(wire.channel.tx_radio == mesh::RADIO_TX_OFF);
assert(!memcmp(wire.channel.secret, ordinary.channel.secret, 32));
assert(node.getChannelForSave(2, raw));
assert(!memcmp(raw.channel.secret, mt.channel.secret, 32));
assert(raw.channel.tx_radio == mesh::RADIO_TX_BOTH);
// Both Rx and RxTx define the secondary profile. The real RadioProfiles
// transmit permission remains distinct from the channel's routing policy.
for (auto mode : {mesh::RadioProfileMode::Rx, mesh::RadioProfileMode::RxTx}) {
node.radio.config.secondary.mode = mode;
assert(node.isAliasRadio2Defined());
assert(node.getChannel(0, wire) && wire.channel.tx_radio == mesh::RADIO_TX_BOTH);
assert(node.getChannel(1, wire) && wire.channel.tx_radio == mesh::RADIO_TX_SECONDARY);
assert(node.getChannel(2, wire) && wire.channel.tx_radio == mesh::RADIO_TX_OFF);
assert(node.radio.config.canTransmit(1) == (mode == mesh::RadioProfileMode::RxTx));
}
mesh::Packet packet;
packet.payload[0] = wire.channel.hash[0]; packet.payload_len = 17;
memcpy(packet.payload + 1, ordinary.channel.secret, 16);
mesh::GroupChannel matches[4];
assert(node.allowGroupPacketOnRadio(&packet));
assert(packet.tx_radio == mesh::RADIO_TX_AUTO); // the ordinary primary slot wins
assert(node.searchChannelsForPacket(&packet, packet.payload, matches, 4) == 1);
assert(node.findChannelIdxForPacket(matches[0], &packet) == 0);
packet.radio_origin = 1;
assert(node.allowGroupPacketOnRadio(&packet));
assert(packet.tx_radio == mesh::RADIO_TX_SECONDARY); // bound before local delivery
assert(node.searchChannelsForPacket(&packet, packet.payload, matches, 4) == 1);
assert(node.findChannelIdxForPacket(matches[0], &packet) == 1);
uint8_t data = 0;
node.onGroupDataRecv(&packet, 0, matches[0], &data, 1);
assert(node.delivered == 1 && packet.tx_radio == mesh::RADIO_TX_SECONDARY);
ChannelDetails empty;
assert(node.setChannel(0, empty));
packet.tx_radio = mesh::RADIO_TX_AUTO;
assert(node.allowGroupPacketOnRadio(&packet));
assert(packet.tx_radio == mesh::RADIO_TX_SECONDARY); // alias-only secondary admission
packet.radio_origin = 0;
packet.tx_radio = mesh::RADIO_TX_AUTO;
assert(!node.allowGroupPacketOnRadio(&packet)); // authenticated alias on wrong radio
assert(packet.tx_radio == mesh::RADIO_TX_AUTO);
packet.payload[1] ^= 1;
assert(node.allowGroupPacketOnRadio(&packet)); // hash match alone is insufficient
packet.payload[1] ^= 1;
assert(node.setChannel(1, empty));
for (uint8_t origin : {0, 1}) {
packet.radio_origin = origin;
assert(node.searchChannelsForPacket(&packet, packet.payload, matches, 4) == 0);
assert(node.findChannelIdxForPacket(wire.channel, &packet) == -1);
assert(!node.allowGroupPacketOnRadio(&packet)); // configured MT cannot enter MeshCore
uint8_t blocked_data = 0;
const auto before_delivery = node.delivered;
node.onGroupDataRecv(&packet, 0, wire.channel, &blocked_data, 1);
assert(node.delivered == before_delivery);
}
// Both disabled secondary profile and an absent profile ignore routing markers.
for (bool absent : {false, true}) {
node.radio.config.secondary.mode = mesh::RadioProfileMode::Off; node.radio.present = !absent;
assert(!node.isAliasRadio2Defined());
assert(node.getChannel(2, wire) && wire.channel.tx_radio == mesh::RADIO_TX_BOTH);
assert(!memcmp(wire.channel.secret, ordinary.channel.secret, 32));
for (uint8_t origin : {0, 1}) {
packet.radio_origin = origin;
assert(node.allowGroupPacketOnRadio(&packet));
assert(node.searchChannelsForPacket(&packet, packet.payload, matches, 4) == 1);
assert(node.findChannelIdxForPacket(matches[0], &packet) == 2);
}
assert(node.setChannel(1, secondary) && node.getChannel(1, wire));
assert(wire.channel.tx_radio == mesh::RADIO_TX_PRIMARY);
assert(!memcmp(wire.channel.secret, ordinary.channel.secret, 32));
assert(node.setChannel(1, empty));
}
// Custom private keys survive both wire resolution and storage access.
ChannelDetails custom = secondary;
for (size_t i = 0; i < 16; ++i) custom.channel.secret[i] = static_cast<uint8_t>(i + 1);
assert(node.setChannel(3, custom) && node.getChannel(3, wire));
assert(!memcmp(wire.channel.secret, custom.channel.secret, 32));
assert(node.getChannelForSave(3, raw));
assert(!memcmp(raw.channel.secret, custom.channel.secret, 32));
custom = secondary;
for (size_t i = 16; i < 32; ++i) custom.channel.secret[i] = static_cast<uint8_t>(i + 1);
assert(node.setChannel(3, custom) && node.getChannel(3, wire));
assert(!memcmp(wire.channel.secret, custom.channel.secret, 32));
assert(node.getChannelForSave(3, raw));
assert(!memcmp(raw.channel.secret, custom.channel.secret, 32));
strcpy(custom.name, "#r2-mt-room");
assert(!node.setChannel(3, custom));
assert(!node.getChannel(-1, wire) && !node.setChannel(MAX_GROUP_CHANNELS, custom));
puts("PASS runtime raw/wire keys, receive aliases and radio2 fallback");
}
'''
COMMAND_STUBS = r'''
struct StrHelper {
static void strncpy(char* out, const char* in, size_t length) {
assert(length > 0);
::strncpy(out, in, length - 1); out[length - 1] = 0;
}
};
struct Serial {
uint8_t frame[128] = {}; int length = 0;
void writeFrame(const uint8_t* data, int size) {
assert(size >= 0 && size <= static_cast<int>(sizeof(frame)));
memcpy(frame, data, size); length = size;
}
};
'''
COMMAND_MEMBERS = r'''
Serial serial; Serial* _serial = &serial;
bool save_result = true, ok = false; int error = -1; unsigned save_calls = 0;
bool saveChannels() { ++save_calls; return save_result; }
void writeErrFrame(uint8_t code) { error = code; }
void writeOKFrame() { ok = true; }
void command(uint8_t*, int);
'''
COMMAND_HARNESS = r'''
void MyMesh::command(uint8_t* cmd_frame, int len) {
uint8_t out_frame[128] = {};
error = -1; ok = false; serial.length = 0;
@COMMAND_BRANCHES@
}
static void applySet(MyMesh& node, uint8_t index, const ChannelDetails& value, int key_bytes = 16) {
uint8_t frame[66] = {CMD_SET_CHANNEL, index};
memcpy(frame + 2, value.name, 32);
memcpy(frame + 34, value.channel.secret, key_bytes);
node.command(frame, 34 + key_bytes);
}
static void expectRaw(MyMesh& node, uint8_t index, const ChannelDetails& expected) {
ChannelDetails raw;
assert(node.getChannelForSave(index, raw));
assert(!strcmp(raw.name, expected.name));
assert(!memcmp(raw.channel.secret, expected.channel.secret, 32));
assert(raw.channel.tx_radio == expected.channel.tx_radio);
}
static void expectGet(MyMesh& node, uint8_t index, const ChannelDetails& expected) {
uint8_t command[2] = {CMD_GET_CHANNEL, index};
node.command(command, sizeof(command));
assert(node.error == -1 && !node.ok && node.serial.length == 50);
assert(node.serial.frame[0] == RESP_CODE_CHANNEL_INFO && node.serial.frame[1] == index);
assert(!strcmp(reinterpret_cast<const char*>(node.serial.frame + 2), expected.name));
assert(!memcmp(node.serial.frame + 34, expected.channel.secret, 16));
}
int main() {
MyMesh node;
node.radio.config.secondary.mode = mesh::RadioProfileMode::RxTx;
auto original = channel("#r2-emergency");
original.channel.tx_radio = mesh::RADIO_TX_BOTH;
assert(node.setChannel(0, original));
expectGet(node, 0, original);
ChannelDetails wire;
assert(node.getChannel(0, wire));
assert(memcmp(node.serial.frame + 34, wire.channel.secret, 16)); // GET is not the wire key
// Switching the display alias without changing the canonical key preserves
// the user's raw manual policy, not the alias's effective secondary policy.
auto ordinary = channel("#emergency");
applySet(node, 0, ordinary);
assert(node.ok && node.error == -1 && node.save_calls == 1);
ordinary.channel.tx_radio = mesh::RADIO_TX_BOTH;
expectRaw(node, 0, ordinary);
applySet(node, 0, original);
assert(node.ok && node.error == -1 && node.save_calls == 2);
expectRaw(node, 0, original);
expectGet(node, 0, original);
// Failed persistence restores the exact app representation. In particular,
// it must not replace the stored prefixed hashtag key with its wire key.
node.save_result = false;
const auto replacement = channel("#r2-custom");
applySet(node, 0, replacement);
assert(!node.ok && node.error == ERR_CODE_FILE_IO_ERROR && node.save_calls == 3);
expectRaw(node, 0, original);
expectGet(node, 0, original);
node.save_result = true;
// A genuinely different private PSK resets manual routing to AUTO.
auto private_channel = original;
for (size_t i = 0; i < 16; ++i) private_channel.channel.secret[i] = static_cast<uint8_t>(i + 1);
private_channel.channel.tx_radio = mesh::RADIO_TX_AUTO;
applySet(node, 0, private_channel);
assert(node.ok && node.error == -1 && node.save_calls == 4);
expectRaw(node, 0, private_channel);
assert(node.getChannel(0, wire));
assert(!memcmp(wire.channel.secret, private_channel.channel.secret, 32));
// Reapplying an unchanged private key retains an explicitly saved policy.
private_channel.channel.tx_radio = mesh::RADIO_TX_PRIMARY;
assert(node.setChannel(0, private_channel));
applySet(node, 0, private_channel);
assert(node.ok && node.error == -1 && node.save_calls == 5);
expectRaw(node, 0, private_channel);
auto invalid = private_channel;
strcpy(invalid.name, "#r2-mt-emergency");
applySet(node, 0, invalid);
assert(!node.ok && node.error == ERR_CODE_ILLEGAL_ARG && node.save_calls == 5);
expectRaw(node, 0, private_channel);
const auto mt = channel("#mt-emergency");
applySet(node, 0, mt);
assert(!node.ok && node.error == ERR_CODE_UNSUPPORTED_CMD && node.save_calls == 5);
expectRaw(node, 0, private_channel);
// Disabled or absent radio2 permits the MT display marker only as an
// ordinary MeshCore channel; it does not activate Meshtext transport.
node.radio.config.secondary.mode = mesh::RadioProfileMode::Off;
applySet(node, 0, mt);
assert(node.ok && node.error == -1 && node.save_calls == 6);
expectRaw(node, 0, mt);
expectGet(node, 0, mt);
assert(node.getChannel(0, wire));
assert(wire.channel.tx_radio == mesh::RADIO_TX_AUTO);
assert(!memcmp(wire.channel.secret, ordinary.channel.secret, 32));
node.radio.present = false;
applySet(node, 0, mt);
assert(node.ok && node.error == -1 && node.save_calls == 7);
expectRaw(node, 0, mt);
// Binary framing remains 128-bit only; larger keys are not silently cut.
applySet(node, 0, mt, 32);
assert(!node.ok && node.error == ERR_CODE_UNSUPPORTED_CMD && node.save_calls == 7);
expectRaw(node, 0, mt);
applySet(node, MAX_GROUP_CHANNELS, mt);
assert(!node.ok && node.error == ERR_CODE_NOT_FOUND && node.save_calls == 7);
uint8_t bad_get[] = {CMD_GET_CHANNEL, MAX_GROUP_CHANNELS};
node.command(bad_get, sizeof(bad_get));
assert(node.error == ERR_CODE_NOT_FOUND && node.serial.length == 0);
puts("PASS command raw GET, canonical-key SET, rollback and unsupported transport");
}
'''
def oracle_vectors():
vectors = ",\n".join(
"{" + json.dumps(name) + ",{" + ",".join(map(str, hashlib.sha256(name.encode()).digest())) + "}}"
for name in HASH_NAMES
)
keys = [hashlib.sha256(name.encode()).digest()[:16] for name in HASH_NAMES]
keys += [bytes(16), bytes(range(1, 17)),
hashlib.sha256(b"#r2-emergency").digest()[:16] + bytes(range(17, 33))]
key_vectors = ",\n".join(
"{{" + ",".join(map(str, key)) + "}," + str(len(key)) + ",{" +
",".join(map(str, hashlib.sha256(key).digest())) + "}}" for key in keys
)
return vectors, key_vectors
class CompanionChannelAliasTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if not compiler:
raise unittest.SkipTest("a host C++ compiler is required")
cls.temporary = tempfile.TemporaryDirectory(prefix="meshcore-channel-alias-")
cls.addClassCleanup(cls.temporary.cleanup)
work = Path(cls.temporary.name)
cls.binary = work / ("alias.exe" if sys.platform == "win32" else "alias")
vectors, key_vectors = oracle_vectors()
source = work / "alias.cpp"
source.write_text(HARNESS.replace("@VECTORS@", vectors).replace("@KEY_VECTORS@", key_vectors), encoding="utf-8")
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
built = subprocess.run(
[compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror", *sanitizers,
"-I" + str(ROOT / "src"), str(source), "-o", str(cls.binary)],
capture_output=True, text=True, timeout=60,
)
if built.returncode:
raise AssertionError(built.stdout + built.stderr)
def run_case(self, case):
checked = subprocess.run([str(self.binary), case], capture_output=True, text=True, timeout=30)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn("PASS " + case, checked.stdout)
def test_prefix_grammar(self):
self.run_case("grammar")
def test_canonical_names_and_output_bounds(self):
self.run_case("canonical")
def test_independent_sha256_vectors_and_custom_keys(self):
self.run_case("keys")
def test_receive_priority_and_unconfigured_radio2_fallback(self):
self.run_case("receive")
def test_core_admission_precedes_group_seen_and_direct_forward(self):
core = (ROOT / "src/Mesh.cpp").read_text(encoding="utf-8")
received = body(core, "DispatcherAction Mesh::onRecvPacket(Packet* pkt)")
gate = re.search(
r"if\s*\(\(pkt->getPayloadType\(\)\s*==\s*PAYLOAD_TYPE_GRP_TXT\s*"
r"\|\|\s*pkt->getPayloadType\(\)\s*==\s*PAYLOAD_TYPE_GRP_DATA\)\s*"
r"&&\s*!allowGroupPacketOnRadio\(pkt\)\)\s*return ACTION_RELEASE;", received,
)
self.assertIsNotNone(gate, "both encrypted group types must pass admission")
# TRACE has an unrelated earlier direct block; check the general
# direct-path branch, whose relaying runs before local group delivery.
direct = received.index("if (pkt->isRouteDirect() && pkt->getPathHashCount() > 0)")
group = received.index("case PAYLOAD_TYPE_GRP_DATA:")
group_seen = received.index("_tables->markSeen(pkt);", group)
candidates = received.index("searchChannelsForPacket(pkt, &channel_hash, channels, 4)", group)
delivery = received.index("onGroupDataRecv(pkt,", candidates)
self.assertLess(gate.end(), direct)
self.assertLess(gate.end(), group_seen)
self.assertLess(group_seen, candidates)
self.assertLess(candidates, delivery)
header = (ROOT / "src/Mesh.h").read_text(encoding="utf-8")
self.assertRegex(header, r"virtual bool allowGroupPacketOnRadio\(Packet\* packet\)\s*\{ return true; \}")
def test_production_channel_access_and_receive_resolution(self):
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text(encoding="utf-8")
header = (ROOT / "examples/companion_radio/MyMesh.h").read_text(encoding="utf-8")
signatures = (
"bool MyMesh::isAliasRadio2Defined(", "bool MyMesh::resolveChannelAlias(",
"bool MyMesh::getChannel(", "bool MyMesh::setChannel(",
"int MyMesh::searchChannelsForPacket(", "int MyMesh::findChannelIdxForPacket(",
"bool MyMesh::allowGroupPacketOnRadio(", "void MyMesh::onGroupDataRecv(",
)
methods = "\n".join(body(companion, signature) for signature in signatures)
save_getter = body(header, "bool getChannelForSave(uint8_t").replace("override", "")
shared = HARNESS[:HARNESS.index("static void assertAlias(")]
vectors, key_vectors = oracle_vectors()
source_text = (shared + RUNTIME_HARNESS).replace("@VECTORS@", vectors).replace(
"@KEY_VECTORS@", key_vectors).replace("@METHODS@", methods).replace("@SAVE_GETTER@", save_getter)
self.run_native_program(source_text, "runtime")
def test_production_binary_get_set_raw_keys_and_rollback(self):
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text(encoding="utf-8")
header = (ROOT / "examples/companion_radio/MyMesh.h").read_text(encoding="utf-8")
signatures = ("bool MyMesh::isAliasRadio2Defined(", "bool MyMesh::resolveChannelAlias(",
"bool MyMesh::getChannel(", "bool MyMesh::setChannel(")
methods = "\n".join(body(companion, signature) for signature in signatures)
save_getter = body(header, "bool getChannelForSave(uint8_t").replace("override", "")
command_signatures = (
"if (cmd_frame[0] == CMD_GET_CHANNEL && len >= 2)",
"if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 32)",
"if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 16)",
)
branches = " else ".join(body(companion, signature) for signature in command_signatures)
macros = ("CMD_GET_CHANNEL", "CMD_SET_CHANNEL", "RESP_CODE_CHANNEL_INFO",
"ERR_CODE_UNSUPPORTED_CMD", "ERR_CODE_NOT_FOUND", "ERR_CODE_FILE_IO_ERROR",
"ERR_CODE_ILLEGAL_ARG")
constants = "\n".join(re.search(r"^#define\s+" + name + r"\s+[^\r\n]+", companion,
flags=re.MULTILINE).group(0) for name in macros)
shared = HARNESS[:HARNESS.index("static void assertAlias(")]
fixture = RUNTIME_HARNESS[:RUNTIME_HARNESS.index("\nint main()")]
fixture = fixture.replace("class MyMesh :", COMMAND_STUBS + "\nclass MyMesh :")
fixture = fixture.replace("@SAVE_GETTER@", save_getter + "\n" + COMMAND_MEMBERS)
vectors, key_vectors = oracle_vectors()
source_text = (shared + constants + "\n" + fixture + COMMAND_HARNESS).replace(
"@VECTORS@", vectors).replace("@KEY_VECTORS@", key_vectors).replace(
"@METHODS@", methods).replace("@COMMAND_BRANCHES@", branches)
self.run_native_program(source_text, "command")
def run_native_program(self, source_text, expected):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
with tempfile.TemporaryDirectory(prefix="meshcore-channel-runtime-") as directory:
work = Path(directory)
source = work / "runtime.cpp"
source.write_text(source_text, encoding="utf-8")
binary = work / ("runtime.exe" if sys.platform == "win32" else "runtime")
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
built = subprocess.run(
[compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror", *sanitizers,
"-I" + str(ROOT / "src"), str(source), "-o", str(binary)],
capture_output=True, text=True, timeout=60,
)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn("PASS " + expected, checked.stdout)
if __name__ == "__main__":
unittest.main()