mirror of
https://github.com/ruvnet/RuView.git
synced 2026-09-01 13:07:25 +00:00
feat: add authenticated ESP32 BLE fusion path
This commit is contained in:
@@ -29,7 +29,7 @@ FEATURE_STATE_SRCS := $(MAIN_DIR)/rv_feature_state.c
|
||||
# before including the .c. The decide() body itself has no ESP-IDF deps.
|
||||
# Simpler: just recompile decide() here via a small shim.
|
||||
|
||||
TESTS := test_adaptive_controller test_rv_feature_state test_rv_mesh
|
||||
TESTS := test_adaptive_controller test_rv_feature_state test_rv_mesh test_ble_cs_protocol
|
||||
|
||||
all: $(TESTS)
|
||||
|
||||
@@ -46,12 +46,23 @@ test_rv_mesh: test_rv_mesh.c $(MAIN_DIR)/rv_mesh.c $(MAIN_DIR)/rv_mesh.h $(FEATU
|
||||
test_rv_mesh.c $(MAIN_DIR)/rv_mesh.c $(FEATURE_STATE_SRCS) \
|
||||
-o $@ $(LDLIBS)
|
||||
|
||||
test_ble_cs_protocol: test_ble_cs_protocol.c \
|
||||
$(MAIN_DIR)/ble_identity_protocol.c $(MAIN_DIR)/ble_identity_protocol.h \
|
||||
$(MAIN_DIR)/channel_sounding_protocol.c $(MAIN_DIR)/channel_sounding_protocol.h \
|
||||
$(MAIN_DIR)/radio_gateway_protocol.c $(MAIN_DIR)/radio_gateway_protocol.h
|
||||
$(CC) $(CFLAGS) test_ble_cs_protocol.c \
|
||||
$(MAIN_DIR)/ble_identity_protocol.c $(MAIN_DIR)/channel_sounding_protocol.c \
|
||||
$(MAIN_DIR)/radio_gateway_protocol.c \
|
||||
-o $@ $(LDLIBS)
|
||||
|
||||
check: all
|
||||
./test_adaptive_controller
|
||||
@echo ""
|
||||
./test_rv_feature_state
|
||||
@echo ""
|
||||
./test_rv_mesh
|
||||
@echo ""
|
||||
./test_ble_cs_protocol
|
||||
|
||||
clean:
|
||||
rm -f $(TESTS) *.o
|
||||
|
||||
201
firmware/esp32-csi-node/tests/host/test_ble_cs_protocol.c
Normal file
201
firmware/esp32-csi-node/tests/host/test_ble_cs_protocol.c
Normal file
@@ -0,0 +1,201 @@
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ble_identity_protocol.h"
|
||||
#include "channel_sounding_protocol.h"
|
||||
#include "radio_gateway_protocol.h"
|
||||
|
||||
static void put16(uint8_t *p, uint16_t value)
|
||||
{
|
||||
p[0] = (uint8_t)value;
|
||||
p[1] = (uint8_t)(value >> 8);
|
||||
}
|
||||
|
||||
static void put32(uint8_t *p, uint32_t value)
|
||||
{
|
||||
p[0] = (uint8_t)value;
|
||||
p[1] = (uint8_t)(value >> 8);
|
||||
p[2] = (uint8_t)(value >> 16);
|
||||
p[3] = (uint8_t)(value >> 24);
|
||||
}
|
||||
|
||||
static size_t build_ble_advert(uint8_t *advert, size_t cap)
|
||||
{
|
||||
const size_t total = RV_BLE_SERVICE_DATA_SIZE + 2u;
|
||||
assert(cap >= total);
|
||||
advert[0] = (uint8_t)(RV_BLE_SERVICE_DATA_SIZE + 1u);
|
||||
advert[1] = RV_BLE_AD_TYPE_SERVICE_DATA_UUID128;
|
||||
memcpy(&advert[2], RV_BLE_SERVICE_UUID_LE, 16u);
|
||||
uint8_t *body = &advert[18];
|
||||
body[0] = RV_BLE_TOKEN_VERSION;
|
||||
body[1] = 7u;
|
||||
put32(&body[2], 30000000u);
|
||||
put32(&body[6], 42u);
|
||||
for (size_t i = 0u; i < RV_BLE_EPHEMERAL_ID_SIZE; i++) body[10 + i] = (uint8_t)(0xa0u + i);
|
||||
for (size_t i = 0u; i < RV_BLE_AUTH_TAG_SIZE; i++) body[18 + i] = (uint8_t)(0x10u + i);
|
||||
return total;
|
||||
}
|
||||
|
||||
static void build_cs_frame(uint8_t frame[RV_CS_FRAME_SIZE])
|
||||
{
|
||||
memset(frame, 0, RV_CS_FRAME_SIZE);
|
||||
put32(&frame[0], RV_CS_MAGIC);
|
||||
frame[4] = RV_CS_VERSION;
|
||||
frame[5] = RV_CS_FLAG_CALIBRATED;
|
||||
frame[6] = 4u;
|
||||
frame[7] = 0u;
|
||||
put16(&frame[8], RV_CS_FRAME_SIZE);
|
||||
put16(&frame[10], 37u);
|
||||
put32(&frame[12], 9u);
|
||||
put32(&frame[16], 10000u);
|
||||
put32(&frame[20], 0x11223344u);
|
||||
put16(&frame[24], 800u);
|
||||
put16(&frame[26], 250u);
|
||||
put32(&frame[28], (uint32_t)-1200);
|
||||
put32(&frame[32], 50000u);
|
||||
put32(&frame[36], (uint32_t)-900);
|
||||
put32(&frame[40], 0x55667788u);
|
||||
put32(&frame[44], 0x12345678u);
|
||||
put16(&frame[48], 2u);
|
||||
put16(&frame[50], 8u);
|
||||
for (size_t i = 0u; i < RV_CS_AUTH_TAG_SIZE; i++) frame[52 + i] = (uint8_t)i;
|
||||
put32(&frame[68], rv_cs_crc32(frame, 68u));
|
||||
}
|
||||
|
||||
static void test_ble_parser_and_privacy_telemetry(void)
|
||||
{
|
||||
uint8_t advert[64];
|
||||
size_t advert_len = build_ble_advert(advert, sizeof(advert));
|
||||
rv_ble_token_t token;
|
||||
assert(rv_ble_parse_advertisement(advert, advert_len, &token));
|
||||
assert(token.key_id == 7u);
|
||||
assert(token.epoch_min == 30000000u);
|
||||
assert(token.ephemeral_id[0] == 0xa0u);
|
||||
assert(token.auth_tag[15] == 0x1fu);
|
||||
|
||||
uint8_t tampered[64];
|
||||
memcpy(tampered, advert, advert_len);
|
||||
tampered[0] = 63u; /* field extends beyond report */
|
||||
assert(!rv_ble_parse_advertisement(tampered, advert_len, &token));
|
||||
memcpy(tampered, advert, advert_len);
|
||||
tampered[18] = 2u; /* unsupported token version */
|
||||
assert(!rv_ble_parse_advertisement(tampered, advert_len, &token));
|
||||
|
||||
rv_ble_telemetry_t telemetry = {
|
||||
.node_id = 3u,
|
||||
.flags = RV_BLE_FLAG_AUTHENTICATED | RV_BLE_FLAG_TIME_VERIFIED,
|
||||
.key_id = 7u,
|
||||
.sequence = 11u,
|
||||
.observed_at_ms = 1200u,
|
||||
.ttl_ms = 3000u,
|
||||
.confidence_permille = 850u,
|
||||
.rssi_dbm = -61,
|
||||
.tx_power_dbm = 127,
|
||||
.token_epoch_min = 30000000u,
|
||||
};
|
||||
memcpy(telemetry.ephemeral_id, token.ephemeral_id, 8u);
|
||||
uint8_t packet[RV_BLE_TELEMETRY_SIZE];
|
||||
assert(rv_ble_serialize_telemetry(&telemetry, packet, sizeof(packet)));
|
||||
assert(RV_BLE_TELEMETRY_MAGIC != 0xC5110005u); /* compressed CSI */
|
||||
assert(packet[0] == 0xb1u && packet[1] == 0x00u
|
||||
&& packet[2] == 0x11u && packet[3] == 0xc5u);
|
||||
assert(packet[4] == RV_BLE_TELEMETRY_VERSION);
|
||||
assert(packet[5] == 3u);
|
||||
assert(packet[20] == (uint8_t)-61);
|
||||
/* The packet has no BLE address, raw advertising data, nonce or HMAC. */
|
||||
assert(memcmp(&packet[24], token.ephemeral_id, 8u) == 0);
|
||||
|
||||
telemetry.flags = 0u;
|
||||
assert(!rv_ble_serialize_telemetry(&telemetry, packet, sizeof(packet)));
|
||||
}
|
||||
|
||||
static void test_channel_sounding_validation(void)
|
||||
{
|
||||
uint8_t frame[RV_CS_FRAME_SIZE];
|
||||
build_cs_frame(frame);
|
||||
rv_cs_measurement_t measurement;
|
||||
assert(rv_cs_parse_frame(frame, sizeof(frame), 2000000u,
|
||||
600u, &measurement) == RV_CS_PARSE_OK);
|
||||
assert(measurement.source_id == 0x11223344u);
|
||||
assert(measurement.phase_millirad == -1200);
|
||||
assert(measurement.frequency_offset_hz == -900);
|
||||
assert(measurement.source_session_id == 0x55667788u);
|
||||
assert(measurement.procedure_id == 0x12345678u);
|
||||
assert(measurement.step_index == 2u);
|
||||
assert(measurement.step_count == 8u);
|
||||
|
||||
frame[28] ^= 1u;
|
||||
assert(rv_cs_parse_frame(frame, sizeof(frame), 2000000u,
|
||||
600u, &measurement) == RV_CS_PARSE_BAD_CRC);
|
||||
|
||||
build_cs_frame(frame);
|
||||
put32(&frame[16], 3000000u);
|
||||
put32(&frame[68], rv_cs_crc32(frame, 68u));
|
||||
assert(rv_cs_parse_frame(frame, sizeof(frame), 2000000u,
|
||||
600u, &measurement) == RV_CS_PARSE_STALE);
|
||||
|
||||
build_cs_frame(frame);
|
||||
uint8_t covered[RV_CS_MAC_INPUT_SIZE];
|
||||
rv_cs_mac_input(frame, covered);
|
||||
assert(memcmp(covered, "RuView/CS/v1", RV_CS_MAC_DOMAIN_SIZE) == 0);
|
||||
assert(memcmp(&covered[RV_CS_MAC_DOMAIN_SIZE], frame,
|
||||
RV_CS_SIGNED_PREFIX_SIZE) == 0);
|
||||
|
||||
assert(rv_cs_sequence_is_newer(10u, 9u));
|
||||
assert(!rv_cs_sequence_is_newer(9u, 9u));
|
||||
assert(rv_cs_sequence_is_newer(0u, UINT32_MAX));
|
||||
|
||||
build_cs_frame(frame);
|
||||
put16(&frame[50], 1u);
|
||||
put32(&frame[68], rv_cs_crc32(frame, 68u));
|
||||
assert(rv_cs_parse_frame(frame, sizeof(frame), 2000000u,
|
||||
600u, &measurement) == RV_CS_PARSE_BAD_STEP);
|
||||
}
|
||||
|
||||
static void test_gateway_envelope_contract(void)
|
||||
{
|
||||
uint8_t payload[RV_BLE_TELEMETRY_SIZE] = {0};
|
||||
uint8_t frame[RV_GATEWAY_MAX_FRAME_SIZE];
|
||||
size_t signed_len = 0u;
|
||||
size_t frame_len = 0u;
|
||||
rv_gateway_metadata_t metadata = {
|
||||
.payload_type = RV_GATEWAY_PAYLOAD_BLE_IDENTITY,
|
||||
.flags = RV_GATEWAY_FLAG_RX_MONOTONIC,
|
||||
.key_id = 3u,
|
||||
.node_id = 9u,
|
||||
.sequence = 11u,
|
||||
.boot_nonce = 0x0102030405060708ull,
|
||||
.received_at_boot_us = 9000u,
|
||||
.timing_uncertainty_us = 1000u,
|
||||
};
|
||||
assert(rv_gateway_build_unsigned(&metadata, payload, sizeof(payload),
|
||||
frame, sizeof(frame), &signed_len,
|
||||
&frame_len));
|
||||
assert(signed_len == RV_GATEWAY_HEADER_SIZE + sizeof(payload));
|
||||
assert(frame_len == signed_len + RV_GATEWAY_AUTH_TAG_SIZE);
|
||||
assert(memcmp(&frame[0], "RVAE", 4u) == 0);
|
||||
assert(frame[4] == RV_GATEWAY_VERSION);
|
||||
assert(frame[5] == RV_GATEWAY_PAYLOAD_BLE_IDENTITY);
|
||||
assert(frame[13] == 0u && frame[14] == 0u && frame[15] == 0u);
|
||||
|
||||
metadata.sequence = 0u;
|
||||
assert(!rv_gateway_build_unsigned(&metadata, payload, sizeof(payload),
|
||||
frame, sizeof(frame), &signed_len,
|
||||
&frame_len));
|
||||
uint8_t lhs[RV_GATEWAY_AUTH_TAG_SIZE] = {0};
|
||||
uint8_t rhs[RV_GATEWAY_AUTH_TAG_SIZE] = {0};
|
||||
assert(rv_gateway_auth_tag_equal(lhs, rhs));
|
||||
rhs[15] = 1u;
|
||||
assert(!rv_gateway_auth_tag_equal(lhs, rhs));
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_ble_parser_and_privacy_telemetry();
|
||||
test_channel_sounding_validation();
|
||||
test_gateway_envelope_contract();
|
||||
puts("BLE identity and external Channel Sounding protocol tests passed (SYNTHETIC)");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user