feat(kiss): add no-crypto KISS mode

Gate the KISS modem's identity/crypto hardware commands (GET_IDENTITY,
VERIFY_SIGNATURE, SIGN_DATA, ENCRYPT_DATA, DECRYPT_DATA, KEY_EXCHANGE,
HASH) and the IdentityStore/filesystem init behind KISS_NO_CRYPTO, for
MCUs too resource-constrained to carry KISS crypto commands or a
persistent identity/filesystem. Disabled commands fall through to the
existing HW_ERR_UNKNOWN_CMD path; radio config, stats, RSSI, TX/RX, and
GET_RANDOM are unaffected.

Adds a native_kiss_modem_no_crypto test env, wired into CI, covering
all seven gated opcodes plus PING/GET_RADIO/GET_RANDOM as still-working
controls.
This commit is contained in:
agessaman
2026-08-15 09:22:35 -07:00
parent a42b3f2797
commit fd2693eb99
6 changed files with 197 additions and 3 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
uses: ./.github/actions/setup-build-environment
- name: Run Unit Tests
run: pio test -e native -e native_kiss_modem -vv
run: pio test -e native -e native_kiss_modem -e native_kiss_modem_no_crypto -vv
- name: Upload Test Results
# Upload test results even if the test step failed.
+8
View File
@@ -303,12 +303,15 @@ void KissModem::processFrame() {
void KissModem::handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len) {
switch (sub_cmd) {
#if !defined(KISS_NO_CRYPTO)
case HW_CMD_GET_IDENTITY:
handleGetIdentity();
break;
#endif
case HW_CMD_GET_RANDOM:
handleGetRandom(data, len);
break;
#if !defined(KISS_NO_CRYPTO)
case HW_CMD_VERIFY_SIGNATURE:
handleVerifySignature(data, len);
break;
@@ -327,6 +330,7 @@ void KissModem::handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint
case HW_CMD_HASH:
handleHash(data, len);
break;
#endif
case HW_CMD_SET_RADIO:
handleSetRadio(data, len);
break;
@@ -462,9 +466,11 @@ void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet,
}
}
#if !defined(KISS_NO_CRYPTO)
void KissModem::handleGetIdentity() {
writeHardwareFrame(HW_RESP(HW_CMD_GET_IDENTITY), _identity.pub_key, PUB_KEY_SIZE);
}
#endif
void KissModem::handleGetRandom(const uint8_t* data, uint16_t len) {
if (len < 1) {
@@ -483,6 +489,7 @@ void KissModem::handleGetRandom(const uint8_t* data, uint16_t len) {
writeHardwareFrame(HW_RESP(HW_CMD_GET_RANDOM), buf, requested);
}
#if !defined(KISS_NO_CRYPTO)
void KissModem::handleVerifySignature(const uint8_t* data, uint16_t len) {
if (len < PUB_KEY_SIZE + SIGNATURE_SIZE + 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
@@ -570,6 +577,7 @@ void KissModem::handleHash(const uint8_t* data, uint16_t len) {
mesh::Utils::sha256(hash, 32, data, len);
writeHardwareFrame(HW_RESP(HW_CMD_HASH), hash, 32);
}
#endif
void KissModem::handleSetRadio(const uint8_t* data, uint16_t len) {
if (len < 10) {
+4
View File
@@ -166,14 +166,18 @@ class KissModem {
void handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
void processTx();
#if !defined(KISS_NO_CRYPTO)
void handleGetIdentity();
#endif
void handleGetRandom(const uint8_t* data, uint16_t len);
#if !defined(KISS_NO_CRYPTO)
void handleVerifySignature(const uint8_t* data, uint16_t len);
void handleSignData(const uint8_t* data, uint16_t len);
void handleEncryptData(const uint8_t* data, uint16_t len);
void handleDecryptData(const uint8_t* data, uint16_t len);
void handleKeyExchange(const uint8_t* data, uint16_t len);
void handleHash(const uint8_t* data, uint16_t len);
#endif
void handleSetRadio(const uint8_t* data, uint16_t len);
void handleSetTxPower(const uint8_t* data, uint16_t len);
void handleGetRadio();
+7 -1
View File
@@ -1,9 +1,10 @@
#include <Arduino.h>
#include <target.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/IdentityStore.h>
#include "KissModem.h"
#if !defined(KISS_NO_CRYPTO)
#include <helpers/IdentityStore.h>
#if defined(NRF52_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
@@ -13,6 +14,7 @@
#else
#include <InternalFileSystem.h>
#endif
#endif
#if defined(KISS_UART_RX) && defined(KISS_UART_TX)
#include <HardwareSerial.h>
@@ -33,6 +35,7 @@ void halt() {
while (1) ;
}
#if !defined(KISS_NO_CRYPTO)
void loadOrCreateIdentity() {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();
@@ -56,6 +59,7 @@ void loadOrCreateIdentity() {
store.save("_main", identity);
}
}
#endif
void onSetRadio(float freq, float bw, uint8_t sf, uint8_t cr) {
radio_driver.setParams(freq, bw, sf, cr);
@@ -85,7 +89,9 @@ void setup() {
radio_driver.begin();
rng.begin(radio_driver.getRngSeed());
#if !defined(KISS_NO_CRYPTO)
loadOrCreateIdentity();
#endif
sensors.begin();
+17 -1
View File
@@ -164,7 +164,7 @@ build_flags = -std=c++17
-I src
-I test/mocks
test_build_src = yes
test_ignore = test_kiss_modem
test_ignore = test_kiss_modem, test_kiss_modem_no_crypto
build_src_filter =
-<*>
+<../src/Utils.cpp>
@@ -187,3 +187,19 @@ build_src_filter =
+<../examples/kiss_modem/KissModem.cpp>
lib_deps =
google/googletest @ 1.17.0
[env:native_kiss_modem_no_crypto]
platform = native
test_framework = googletest
build_flags = -std=c++17
-D KISS_NO_CRYPTO=1
-I test/mocks
-I src
-I examples/kiss_modem
test_build_src = yes
test_filter = test_kiss_modem_no_crypto
build_src_filter =
-<*>
+<../examples/kiss_modem/KissModem.cpp>
lib_deps =
google/googletest @ 1.17.0
@@ -0,0 +1,160 @@
#include <gtest/gtest.h>
#include <queue>
#include <vector>
#include "KissModem.h"
class SimpleStream : public Stream {
public:
void pushRx(const std::vector<uint8_t>& bytes) {
for (uint8_t b : bytes) {
_rx.push(b);
}
}
const std::vector<uint8_t>& writesSnapshot() const { return _writes; }
int availableForWrite() override { return 4096; }
size_t write(const uint8_t* buffer, size_t size) override {
for (size_t i = 0; i < size; i++) {
_writes.push_back(buffer[i]);
}
return size;
}
size_t write(uint8_t b) override { return write(&b, 1); }
int available() override { return static_cast<int>(_rx.size()); }
int read() override {
if (_rx.empty()) return -1;
int b = _rx.front();
_rx.pop();
return b;
}
private:
std::queue<uint8_t> _rx;
std::vector<uint8_t> _writes;
};
class FakeRNG : public mesh::RNG {
public:
void random(uint8_t* dest, size_t sz) override {
for (size_t i = 0; i < sz; i++) {
dest[i] = (uint8_t)i;
}
}
};
class FakeRadio : public mesh::Radio {
public:
bool isReceiving() override { return false; }
uint32_t getEstAirtimeFor(uint16_t) override { return 10; }
bool startSendRaw(const uint8_t*, uint16_t) override { return true; }
bool isSendComplete() override { return true; }
void onSendFinished() override {}
int16_t getNoiseFloor() override { return -120; }
};
class FakeBoard : public mesh::MainBoard {
public:
uint16_t getBattMilliVolts() override { return 4200; }
float getMCUTemperature() override { return 24.0f; }
const char* getManufacturerName() override { return "test-board"; }
void reboot() override {}
};
class FakeSensors : public SensorManager {
public:
bool querySensors(uint8_t, CayenneLPP&) override { return false; }
};
static std::vector<uint8_t> hwFrame(uint8_t sub_cmd, const std::vector<uint8_t>& payload = {}) {
std::vector<uint8_t> frame = {KISS_FEND, KISS_CMD_SETHARDWARE, sub_cmd};
frame.insert(frame.end(), payload.begin(), payload.end());
frame.push_back(KISS_FEND);
return frame;
}
class KissModemNoCryptoFixture : public ::testing::Test {
protected:
SimpleStream serial;
mesh::LocalIdentity identity;
FakeRNG rng;
FakeRadio radio;
FakeBoard board;
FakeSensors sensors;
KissModem modem;
KissModemNoCryptoFixture()
: modem(serial, identity, rng, radio, board, sensors) {
modem.begin();
}
};
TEST_F(KissModemNoCryptoFixture, PingStillWorksWhenCryptoDisabled) {
serial.pushRx(hwFrame(HW_CMD_PING));
modem.loop();
const std::vector<uint8_t> expected = {
KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP(HW_CMD_PING), KISS_FEND};
EXPECT_EQ(serial.writesSnapshot(), expected);
}
TEST_F(KissModemNoCryptoFixture, GetRadioStillWorksWhenCryptoDisabled) {
serial.pushRx(hwFrame(HW_CMD_GET_RADIO));
modem.loop();
std::vector<uint8_t> expected = {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP(HW_CMD_GET_RADIO)};
expected.insert(expected.end(), 10, 0x00); // zero-initialized RadioConfig
expected.push_back(KISS_FEND);
EXPECT_EQ(serial.writesSnapshot(), expected);
}
TEST_F(KissModemNoCryptoFixture, GetRandomStillWorksWhenCryptoDisabled) {
serial.pushRx(hwFrame(HW_CMD_GET_RANDOM, {0x04}));
modem.loop();
const std::vector<uint8_t> expected = {
KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP(HW_CMD_GET_RANDOM), 0x00, 0x01, 0x02, 0x03, KISS_FEND};
EXPECT_EQ(serial.writesSnapshot(), expected);
}
class KissModemDisabledCommandTest : public ::testing::TestWithParam<uint8_t> {
protected:
SimpleStream serial;
mesh::LocalIdentity identity;
FakeRNG rng;
FakeRadio radio;
FakeBoard board;
FakeSensors sensors;
KissModem modem;
KissModemDisabledCommandTest()
: modem(serial, identity, rng, radio, board, sensors) {
modem.begin();
}
};
TEST_P(KissModemDisabledCommandTest, ReturnsUnknownCommandWhenCryptoDisabled) {
serial.pushRx(hwFrame(GetParam()));
modem.loop();
const std::vector<uint8_t> expected = {
KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_ERROR, HW_ERR_UNKNOWN_CMD, KISS_FEND};
EXPECT_EQ(serial.writesSnapshot(), expected);
}
INSTANTIATE_TEST_SUITE_P(
AllCryptoCommands, KissModemDisabledCommandTest,
::testing::Values(
HW_CMD_GET_IDENTITY, HW_CMD_VERIFY_SIGNATURE, HW_CMD_SIGN_DATA, HW_CMD_ENCRYPT_DATA,
HW_CMD_DECRYPT_DATA, HW_CMD_KEY_EXCHANGE, HW_CMD_HASH));
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}