feat(core/ble): enable obtaining bond list
What changed, and why it matters
This commit adds a new diagnostic command, 'ble-get-bonds', to Trezor's internal production-test firmware. It lets a factory technician read the list of Bluetooth devices previously paired with the device. The change is purely a feature addition for manufacturing/diagnostics; it does not, by itself, bypass pairing, steal funds, or change security settings. The main concern is that it exposes paired-device addresses (MAC addresses) through the production-test command-line interface, which could be a minor information-leak if the prodtest interface is accessible in the field.
Verify that the prodtest firmware is not shipped on consumer devices or that the 'ble-get-bonds' command is disabled/disallowed in production builds. If consumer firmware inherits this code path, add authorization checks and consider whether exposing bond lists is necessary. Review that CONFIG_BT_MAX_PAIRED matches BLE_MAX_BONDS to avoid truncation or mismatched expectations.
Security signals we found
New prodtest CLI command exposes Bluetooth bond/MAC information
No access-control check visible in the prodtest command handler
IPC message parsing uses length checks (len < 2 early return)
Output buffer is zeroed on failure paths
Refactoring of ble_get_mac changes public API signature
Evidence from the diff
The patch introduces a cross-core IPC command INTERNAL_CMD_GET_BOND_LIST / INTERNAL_EVENT_BOND_LIST that retrieves bonded Bluetooth addresses from the Nordic nRF BLE stack (bt_foreach_bond) and returns them to the STM32 host. It exposes this via the prodtest CLI as ‘ble-get-bonds’. The function ble_get_bond_list() zeroes the output buffer on several failure paths and requires count >= BLE_MAX_BONDS (8). No buffer overflow is evident: the STM32 side copies MIN(len-2, sizeof(drv->bonds)) bytes, and the nRF side sends a fixed-size buffer sized for CONFIG_BT_MAX_PAIRED entries. The commit also refactors ble_get_mac() to use a new bt_le_addr_t structure. There is no authentication or authorization check around the prodtest command, but prodtest is intended for factory use only.
Changed components
core/embed/io/ble/stm32/ble.ccore/embed/io/ble/stm32/ble_comm_defs.hcore/embed/io/ble/inc/io/ble.hcore/embed/io/ble/unix/ble.ccore/embed/projects/prodtest/cmd/prodtest_ble.cnordic/trezor/trezor-ble/src/ble/ble_management.cnordic/trezor/trezor-ble/src/ble/bonds.cnordic/trezor/trezor-ble/src/ble/ble_internal.hInspect captured patch +220 / −17
diff --git a/core/embed/io/ble/inc/io/ble.h b/core/embed/io/ble/inc/io/ble.h
index fb9588a60..e096aea73 100644
--- a/core/embed/io/ble/inc/io/ble.h
+++ b/core/embed/io/ble/inc/io/ble.h
@@ -31,6 +31,8 @@
#define BLE_ADV_NAME_LEN 20
#define BLE_PAIRING_CODE_LEN 6
+#define BLE_MAX_BONDS 8
+
typedef enum {
BLE_SWITCH_OFF = 0, // Turn off BLE advertising, disconnect
BLE_SWITCH_ON = 1, // Turn on BLE advertising
@@ -52,6 +54,20 @@ typedef enum {
BLE_MODE_DFU,
} ble_mode_t;
+/*
+ * Address types:
+ * BT_ADDR_LE_PUBLIC 0x00
+ * BT_ADDR_LE_RANDOM 0x01
+ * BT_ADDR_LE_PUBLIC_ID 0x02
+ * BT_ADDR_LE_RANDOM_ID 0x03
+ * BT_ADDR_LE_UNRESOLVED 0xFE
+ * BT_ADDR_LE_ANONYMOUS 0xFF
+ */
+typedef struct {
+ uint8_t type;
+ uint8_t addr[6];
+} bt_le_addr_t;
+
typedef struct {
uint8_t name[BLE_ADV_NAME_LEN];
bool static_mac;
@@ -173,6 +189,9 @@ bool ble_write(const uint8_t *data, uint16_t len);
// Check if read is possible
bool ble_can_read(void);
+// Get bond list
+uint8_t ble_get_bond_list(bt_le_addr_t *bonds, size_t count);
+
// Reads data from a connected BLE device
//
// max_len indicates the maximum number of bytes to read. Rest of the data
@@ -185,4 +204,4 @@ uint32_t ble_read(uint8_t *data, uint16_t max_len);
//
// When not using static address, the address is random and may not correspond
// to what is actually used for advertising
-bool ble_get_mac(uint8_t *mac, size_t max_len);
+bool ble_get_mac(bt_le_addr_t *addr);
diff --git a/core/embed/io/ble/stm32/ble.c b/core/embed/io/ble/stm32/ble.c
index bb5c34ec5..4a5b3d6da 100644
--- a/core/embed/io/ble/stm32/ble.c
+++ b/core/embed/io/ble/stm32/ble.c
@@ -75,8 +75,13 @@ typedef struct {
tsqueue_t tx_queue;
ble_adv_start_cmd_data_t adv_cmd;
- uint8_t mac[6];
+ bt_le_addr_t mac;
bool mac_ready;
+
+ uint8_t bond_count;
+ bt_le_addr_t bonds[BLE_MAX_BONDS];
+ bool bonds_ready;
+
systimer_t *timer;
uint16_t ping_cntr;
} ble_driver_t;
@@ -183,6 +188,13 @@ static bool ble_send_mac_request(ble_driver_t *drv) {
return nrf_send_msg(NRF_SERVICE_BLE_MANAGER, &cmd, sizeof(cmd), NULL, NULL);
}
+static bool ble_send_bond_list_request(ble_driver_t *drv) {
+ UNUSED(drv);
+ uint8_t cmd = INTERNAL_CMD_GET_BOND_LIST;
+
+ return nrf_send_msg(NRF_SERVICE_BLE_MANAGER, &cmd, sizeof(cmd), NULL, NULL);
+}
+
static void ble_process_rx_msg_status(const uint8_t *data, uint32_t len) {
ble_driver_t *drv = &g_ble_driver;
@@ -373,7 +385,23 @@ static void ble_process_rx_msg_mac(const uint8_t *data, uint32_t len) {
}
drv->mac_ready = true;
- memcpy(drv->mac, &data[1], sizeof(drv->mac));
+ drv->mac.type = 0;
+ memcpy(&drv->mac.addr, &data[1], sizeof(drv->mac.addr));
+}
+
+static void ble_process_rx_msg_bond_list(const uint8_t *data, uint32_t len) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!drv->initialized) {
+ return;
+ }
+
+ if (len < 2) {
+ return;
+ }
+
+ drv->bonds_ready = true;
+ drv->bond_count = data[1];
+ memcpy(&drv->bonds, &data[2], MIN(len - 2, sizeof(drv->bonds)));
}
static void ble_process_rx_msg(const uint8_t *data, uint32_t len) {
@@ -396,6 +424,10 @@ static void ble_process_rx_msg(const uint8_t *data, uint32_t len) {
break;
case INTERNAL_EVENT_PAIRING_COMPLETED:
ble_process_rx_msg_pairing_completed(data, len);
+ break;
+ case INTERNAL_EVENT_BOND_LIST:
+ ble_process_rx_msg_bond_list(data, len);
+ break;
default:
break;
}
@@ -968,15 +1000,11 @@ void ble_get_state(ble_state_t *state) {
irq_unlock(key);
}
-bool ble_get_mac(uint8_t *mac, size_t max_len) {
+bool ble_get_mac(bt_le_addr_t *mac) {
ble_driver_t *drv = &g_ble_driver;
- if (max_len < sizeof(drv->mac)) {
- return false;
- }
-
if (!drv->initialized) {
- memset(mac, 0, max_len);
+ memset(mac, 0, sizeof(*mac));
return false;
}
@@ -989,16 +1017,49 @@ bool ble_get_mac(uint8_t *mac, size_t max_len) {
uint32_t timeout = ticks_timeout(100);
while (!ticks_expired(timeout)) {
+ irq_key_t key = irq_lock();
if (drv->mac_ready) {
- memcpy(mac, drv->mac, sizeof(drv->mac));
+ memcpy(mac, &drv->mac, sizeof(drv->mac));
+ irq_unlock(key);
return true;
}
+ irq_unlock(key);
}
- memset(mac, 0, max_len);
+ memset(mac, 0, sizeof(*mac));
return false;
}
+uint8_t ble_get_bond_list(bt_le_addr_t *bonds, size_t count) {
+ ble_driver_t *drv = &g_ble_driver;
+
+ if (!drv->initialized || count < BLE_MAX_BONDS) {
+ memset(bonds, 0, count * sizeof(bt_le_addr_t));
+ return 0;
+ }
+
+ drv->bonds_ready = false;
+
+ if (!ble_send_bond_list_request(drv)) {
+ return 0;
+ }
+
+ uint32_t timeout = ticks_timeout(100);
+
+ while (!ticks_expired(timeout)) {
+ irq_key_t key = irq_lock();
+ if (drv->bonds_ready) {
+ memcpy(bonds, &drv->bonds, sizeof(drv->bonds));
+ irq_unlock(key);
+ return drv->bond_count;
+ }
+ irq_unlock(key);
+ }
+
+ memset(bonds, 0, count * sizeof(bt_le_addr_t));
+ return 0;
+}
+
void ble_get_advertising_name(char *name, size_t max_len) {
ble_driver_t *drv = &g_ble_driver;
diff --git a/core/embed/io/ble/stm32/ble_comm_defs.h b/core/embed/io/ble/stm32/ble_comm_defs.h
index 5c09c5489..e4ad11dbd 100644
--- a/core/embed/io/ble/stm32/ble_comm_defs.h
+++ b/core/embed/io/ble/stm32/ble_comm_defs.h
@@ -52,6 +52,7 @@ typedef enum {
INTERNAL_EVENT_PAIRING_CANCELLED = 0x05,
INTERNAL_EVENT_MAC = 0x06,
INTERNAL_EVENT_PAIRING_COMPLETED = 0x07,
+ INTERNAL_EVENT_BOND_LIST = 0x08,
} internal_event_t;
typedef enum {
@@ -66,6 +67,7 @@ typedef enum {
INTERNAL_CMD_UNPAIR = 0x08,
INTERNAL_CMD_GET_MAC = 0x09,
INTERNAL_CMD_SET_BUSY = 0x0A,
+ INTERNAL_CMD_GET_BOND_LIST = 0x0B,
} internal_cmd_t;
typedef struct {
diff --git a/core/embed/io/ble/unix/ble.c b/core/embed/io/ble/unix/ble.c
index 5e04c8930..b865ebb73 100644
--- a/core/embed/io/ble/unix/ble.c
+++ b/core/embed/io/ble/unix/ble.c
@@ -27,10 +27,12 @@ bool ble_can_read(void) { return false; }
uint32_t ble_read(uint8_t *data, uint16_t max_len) { return 0; }
-bool ble_get_mac(uint8_t *mac, size_t max_len) { return false; }
+bool ble_get_mac(bt_le_addr_t *addr) { return false; }
void ble_event_flush(void) {}
void ble_get_advertising_name(char *name, size_t max_len) {
memset(name, 0, max_len);
}
+
+uint8_t ble_get_bond_list(bt_le_addr_t *bonds, size_t count) { return 0; }
diff --git a/core/embed/projects/prodtest/README.md b/core/embed/projects/prodtest/README.md
index 7f5d519ce..665abcc31 100644
--- a/core/embed/projects/prodtest/README.md
+++ b/core/embed/projects/prodtest/README.md
@@ -200,6 +200,17 @@ ble-erase-bonds
OK
```
+### ble-get-bonds
+Retrieves all BLE bonds from the device.
+
+Example:
+```
+ble-get-bonds
+# Initializing the BLE...
+# Got 1 bonds.
+# Bond 1: 5c:dc:49:d1:8d:35
+OK
+```
### ble-radio-test
Runs radio test proxy-client. It requires special nRF radio test firmware, see https://docs.nordicsemi.com/bundle/sdk_nrf5_v17.0.2/page/nrf_radio_test_example.html for usage.
diff --git a/core/embed/projects/prodtest/cmd/prodtest_ble.c b/core/embed/projects/prodtest/cmd/prodtest_ble.c
index a203a2f56..acea924e4 100644
--- a/core/embed/projects/prodtest/cmd/prodtest_ble.c
+++ b/core/embed/projects/prodtest/cmd/prodtest_ble.c
@@ -168,15 +168,15 @@ static void prodtest_ble_info(cli_t* cli) {
return;
}
- uint8_t mac[6] = {0};
+ bt_le_addr_t mac = {0};
- if (!ble_get_mac(mac, 6)) {
+ if (!ble_get_mac(&mac)) {
cli_error(cli, CLI_ERROR, "Could not read MAC.");
return;
}
- cli_trace(cli, "MAC: %02x:%02x:%02x:%02x:%02x:%02x", mac[5], mac[4], mac[3],
- mac[2], mac[1], mac[0]);
+ cli_trace(cli, "MAC: %02x:%02x:%02x:%02x:%02x:%02x", mac.addr[5], mac.addr[4],
+ mac.addr[3], mac.addr[2], mac.addr[1], mac.addr[0]);
cli_ok(cli, "");
}
@@ -230,6 +230,31 @@ static void prodtest_ble_erase_bonds_cmd(cli_t* cli) {
cli_ok(cli, "");
}
+static void prodtest_ble_get_bonds(cli_t* cli) {
+ if (cli_arg_count(cli) > 0) {
+ cli_error_arg_count(cli);
+ return;
+ }
+
+ if (!ensure_ble_init(cli)) {
+ return;
+ }
+
+ bt_le_addr_t bonds[BLE_MAX_BONDS];
+
+ uint8_t cnt = ble_get_bond_list(bonds, BLE_MAX_BONDS);
+
+ cli_trace(cli, "Got %d bonds.", cnt);
+
+ for (uint8_t i = 0; i < cnt; i++) {
+ cli_trace(cli, "Bond %d: %02x:%02x:%02x:%02x:%02x:%02x", i + 1,
+ bonds[i].addr[5], bonds[i].addr[4], bonds[i].addr[3],
+ bonds[i].addr[2], bonds[i].addr[1], bonds[i].addr[0]);
+ }
+
+ cli_ok(cli, "");
+}
+
static void prodtest_ble_radio_test_cmd(cli_t* cli) {
if (cli_arg_count(cli) > 0) {
cli_error_arg_count(cli);
@@ -385,6 +410,14 @@ PRODTEST_CLI_CMD(
.args = ""
);
+PRODTEST_CLI_CMD(
+ .name = "ble-get-bonds",
+ .func = prodtest_ble_get_bonds,
+ .info = "Get list of current bonds",
+ .args = ""
+);
+
+
PRODTEST_CLI_CMD(
.name = "ble-radio-test",
.func = prodtest_ble_radio_test_cmd,
diff --git a/nordic/trezor/trezor-ble/src/ble/ble_internal.h b/nordic/trezor/trezor-ble/src/ble/ble_internal.h
index 34bb8bdfd..117d56645 100644
--- a/nordic/trezor/trezor-ble/src/ble/ble_internal.h
+++ b/nordic/trezor/trezor-ble/src/ble/ble_internal.h
@@ -83,6 +83,7 @@ typedef enum {
INTERNAL_EVENT_PAIRING_CANCELLED = 0x05,
INTERNAL_EVENT_MAC = 0x06,
INTERNAL_EVENT_PAIRING_COMPLETED = 0x07,
+ INTERNAL_EVENT_BOND_LIST = 0x08,
} internal_event_t;
typedef enum {
@@ -97,6 +98,7 @@ typedef enum {
INTERNAL_CMD_UNPAIR = 0x08,
INTERNAL_CMD_GET_MAC = 0x09,
INTERNAL_CMD_SET_BUSY = 0x0A,
+ INTERNAL_CMD_GET_BOND_LIST = 0x0B,
} internal_cmd_t;
typedef struct {
@@ -142,6 +144,10 @@ bool bonds_erase_all(void);
int bonds_get_count(void);
// Erase current bond
bool bonds_erase_current(void);
+// Erase bonds for a specific device
+bool bonds_erase_device(const bt_addr_le_t *addr);
+// Get all bonded devices
+size_t bonds_get_all(bt_addr_le_t *addr, size_t max_count);
// Advertising functions
// Initialization
diff --git a/nordic/trezor/trezor-ble/src/ble/ble_management.c b/nordic/trezor/trezor-ble/src/ble/ble_management.c
index c580a5ece..f1d83d115 100644
--- a/nordic/trezor/trezor-ble/src/ble/ble_management.c
+++ b/nordic/trezor/trezor-ble/src/ble/ble_management.c
@@ -123,6 +123,24 @@ static void management_send_mac(uint8_t *mac) {
trz_comm_send_msg(NRF_SERVICE_BLE_MANAGER, tx_data, sizeof(tx_data));
}
+static void management_send_bonds(void) {
+ bt_addr_le_t addr_list[CONFIG_BT_MAX_PAIRED] = {0};
+ size_t bond_count = bonds_get_all(addr_list, CONFIG_BT_MAX_PAIRED);
+
+ uint8_t tx_data[1 + (CONFIG_BT_MAX_PAIRED * (1 + BT_ADDR_SIZE))] = {0};
+
+ tx_data[0] = INTERNAL_EVENT_BOND_LIST;
+ tx_data[1] = bond_count;
+ for (size_t i = 0; i < bond_count; i++) {
+ tx_data[2 + i * (1 + BT_ADDR_SIZE)] = addr_list[i].type;
+ memcpy(&tx_data[2 + i * (1 + BT_ADDR_SIZE) + 1], addr_list[i].a.val,
+ BT_ADDR_SIZE);
+ }
+
+ trz_comm_send_msg(NRF_SERVICE_BLE_MANAGER, (uint8_t *)tx_data,
+ sizeof(tx_data));
+}
+
static void process_command(uint8_t *data, uint16_t len) {
uint8_t cmd = data[0];
bool success = true;
@@ -174,7 +192,10 @@ static void process_command(uint8_t *data, uint16_t len) {
} break;
case INTERNAL_CMD_SET_BUSY: {
ble_set_busy_flag(data[1]);
- }
+ } break;
+ case INTERNAL_CMD_GET_BOND_LIST: {
+ management_send_bonds();
+ } break;
default:
break;
}
diff --git a/nordic/trezor/trezor-ble/src/ble/bonds.c b/nordic/trezor/trezor-ble/src/ble/bonds.c
index 33fcaf7e2..5789350e0 100644
--- a/nordic/trezor/trezor-ble/src/ble/bonds.c
+++ b/nordic/trezor/trezor-ble/src/ble/bonds.c
@@ -27,6 +27,8 @@
#include "ble_internal.h"
+#include <string.h>
+
#define LOG_MODULE_NAME ble_bonds
LOG_MODULE_REGISTER(LOG_MODULE_NAME);
@@ -77,3 +79,49 @@ bool bonds_erase_current(void) {
return err == 0;
}
+
+typedef struct {
+ bt_addr_le_t *addr_list;
+ size_t max_count;
+ size_t filled;
+} bonds_ctx_t;
+
+static void get_bonds(const struct bt_bond_info *info, void *user_data) {
+ bonds_ctx_t *ctx = (bonds_ctx_t *)user_data;
+
+ if (ctx == NULL) {
+ return;
+ }
+
+ if ((ctx->filled < ctx->max_count) && (ctx->addr_list != NULL)) {
+ bt_addr_le_t *dst = &ctx->addr_list[ctx->filled];
+ // First byte: address type, next 6 bytes: MAC address
+ dst->type = info->addr.type;
+ memcpy(dst->a.val, info->addr.a.val, BT_ADDR_SIZE);
+ }
+
+ ctx->filled += 1;
+}
+
+size_t bonds_get_all(bt_addr_le_t *addr, size_t max_count) {
+ // If no storage provided, just return total number of bonds
+ if (addr == NULL || max_count == 0) {
+ int total = 0;
+ bt_foreach_bond(BT_ID_DEFAULT, count_bonds, &total);
+ return total;
+ }
+
+ bonds_ctx_t ctx = {
+ .addr_list = addr,
+ .max_count = max_count,
+ .filled = 0,
+ };
+
+ bt_foreach_bond(BT_ID_DEFAULT, get_bonds, &ctx);
+
+ // Return how many entries were actually written (capped by max_count)
+ if (ctx.filled > max_count) {
+ return max_count;
+ }
+ return ctx.filled;
+}
Why this scored 22/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.