diff --git a/tools/cgathui_common/tests/test_ble_join.c b/tools/cgathui_common/tests/test_ble_join.c new file mode 100644 index 00000000..23b41272 --- /dev/null +++ b/tools/cgathui_common/tests/test_ble_join.c @@ -0,0 +1,67 @@ +/* Проверка границы доверия: настоящий CCM, подмена, replay, reflection, новый challenge и отмена. */ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include "../ble_join.c" +#include +#include + +struct test_radio { struct ll_entry* packet; }; +int ble_channel_tx_ready(struct ble_transport* radio, ble_link_id id, enum ble_channel channel) { + (void)id; assert(channel == BLE_CHANNEL_INVITE); return !((struct test_radio*)radio)->packet; +} +int ble_channel_tx_put(struct ble_transport* radio, ble_link_id id, enum ble_channel channel, struct ll_entry* packet) { + assert(ble_channel_tx_ready(radio, id, channel)); ((struct test_radio*)radio)->packet = packet; return BLE_OK; +} +static void wake(void* arg) { (void)arg; } +static int connect_peer(void* arg, uint64_t peer) { (void)arg; (void)peer; return -1; } +static void free_packet(struct ll_entry* packet) { queue_dgram_free(packet); queue_entry_free(packet); } +static void deliver(struct test_radio* radio, struct ble_join* receiver) { + struct ll_entry* packet = radio->packet; assert(packet); radio->packet = NULL; + assert(ble_join_receive(receiver, 1, packet->dgram, packet->len)); free_packet(packet); +} +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); + size_t baseline = u_get_allocated_count(); + struct UTUN_INSTANCE inst[2] = {0}; struct test_radio radio[2] = {0}; struct ble_join* b[2]; + for (int i = 0; i < 2; i++) { + assert(sc_generate_keypair(&inst[i].my_keys) == SC_OK); + inst[i].node_id = sc_derive_node_id_from_pubkey(inst[i].my_keys.public_key); + b[i] = ble_join_create(&inst[i], (struct ble_transport*)&radio[i], connect_peer, wake, NULL); assert(b[i]); + } + for (int i = 0; i < 2; i++) ble_join_connection(b[i], 1, inst[1-i].node_id, BLE_READY); + uint64_t handle = 0; assert(!inst[0].join_carrier->open(b[0], inst[1].my_keys.public_key, &handle) && handle); + ble_join_run(b[0]); deliver(&radio[0], b[1]); ble_join_run(b[1]); deliver(&radio[1], b[0]); ble_join_run(b[0]); + assert(b[0]->links[0].authenticated && !b[1]->links[0].authenticated); + uint8_t wire[CHAT_JOIN_WIRE_MAX] = {ETCP_ID_CHAT_SYNC}; wire[9] = CS_MSG_JOIN_INFO_REQ; + assert(!inst[0].join_carrier->send(b[0], handle, wire, sizeof(wire))); ble_join_run(b[0]); + struct ll_entry* packet = radio[0].packet; assert(packet && packet->len > BLE_FRAME_MAX); + /* Изменённый ciphertext, ключ заголовка и отражение не устанавливают сессию. */ + packet->dgram[packet->len - 1] ^= 1; assert(ble_join_receive(b[1], 1, packet->dgram, packet->len)); + assert(!b[1]->links[0].authenticated && !b[1]->links[0].rx_sequence); packet->dgram[packet->len - 1] ^= 1; + packet->dgram[2] ^= 1; assert(ble_join_receive(b[1], 1, packet->dgram, packet->len)); + assert(!b[1]->links[0].rx_sequence); packet->dgram[2] ^= 1; + assert(ble_join_receive(b[0], 1, packet->dgram, packet->len)); assert(!b[0]->links[0].rx_sequence); + assert(ble_join_receive(b[1], 1, packet->dgram, packet->len)); + assert(b[1]->links[0].authenticated && b[1]->links[0].rx_sequence == 1); + assert(ble_join_receive(b[1], 1, packet->dgram, packet->len)); assert(b[1]->links[0].rx_sequence == 1); + /* Старый ciphertext не проходит после нового challenge, даже с тем же статическим ключом. */ + reset(b[0], &b[0]->links[0], 0); reset(b[1], &b[1]->links[0], 0); + uint64_t fresh = 0; assert(!carrier_open(b[0], inst[1].my_keys.public_key, &fresh) && fresh != handle); + radio[0].packet = NULL; + ble_join_run(b[0]); deliver(&radio[0], b[1]); ble_join_run(b[1]); deliver(&radio[1], b[0]); ble_join_run(b[0]); + assert(ble_join_receive(b[1], 1, packet->dgram, packet->len)); assert(!b[1]->links[0].rx_sequence); free_packet(packet); + assert(carrier_send(b[0], handle, wire, 18) < 0); /* Старый handle не переиспользуется. */ + wire[9] = CS_MSG_DB_SYNC_REQ; assert(carrier_send(b[0], fresh, wire, 18) < 0); wire[9] = CS_MSG_JOIN_INFO_REQ; + for (int i = 0; i < JOIN_QUEUE; i++) assert(!carrier_send(b[0], fresh, wire, 18)); + assert(carrier_send(b[0], fresh, wire, 18) < 0 && b[0]->links[0].count == JOIN_QUEUE); + reset(b[0], &b[0]->links[0], 0); reset(b[1], &b[1]->links[0], 0); + assert(!carrier_open(b[0], inst[1].my_keys.public_key, &fresh)); + ble_join_run(b[0]); deliver(&radio[0], b[1]); ble_join_run(b[1]); deliver(&radio[1], b[0]); ble_join_run(b[0]); + carrier_close(b[0], fresh, 0); ble_join_run(b[0]); deliver(&radio[0], b[1]); ble_join_run(b[0]); + assert(!ble_join_holds(b[0], 1) && !ble_join_holds(b[1], 1)); + for (int i = 0; i < 2; i++) { ble_join_destroy(b[i]); assert(!inst[i].join_carrier && !radio[i].packet); } + assert(u_get_allocated_count() == baseline); + puts("PASS BLE JOIN trust boundary: CCM tamper, key substitution, reflection, replay, fresh session, whitelist, queue bound, cancellation"); + return 0; +} diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index a8953a8b..273d997f 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -136,6 +136,10 @@ add_executable(test_ble_etcp ../cgathui_common/tests/test_ble_etcp.c) target_link_libraries(test_ble_etcp PRIVATE chat_ble_etcp) set_target_properties(test_ble_etcp PROPERTIES AUTOMOC OFF) add_test(NAME test_ble_etcp COMMAND test_ble_etcp) +add_executable(test_ble_join ../cgathui_common/tests/test_ble_join.c) +target_link_libraries(test_ble_join PRIVATE utun) +set_target_properties(test_ble_join PROPERTIES AUTOMOC OFF) +add_test(NAME test_ble_join COMMAND test_ble_join) add_executable(ble_etcp_probe ../cgathui_common/tests/ble_etcp_probe.c) target_link_libraries(ble_etcp_probe PRIVATE chat_ble_etcp vibechat_ble) set_target_properties(ble_etcp_probe PROPERTIES AUTOMOC OFF)