feat(core): check nRF FW authenticity in firmware
What changed, and why it matters
This commit adds a new security feature to Trezor hardware wallets that checks whether the Bluetooth chip (nRF) is running authentic, manufacturer-approved firmware before the main wallet software starts up. In production builds, if the Bluetooth chip fails this authenticity check, the device now shuts down with an error instead of continuing to boot. The change also separates the authentication code from general Bluetooth support so it can be enabled independently.
Review the nrf_authenticate() implementation for timing, replay, and downgrade resistance; verify that the 5-second timeout cannot be abused to bypass authentication; ensure production signing/provisioning workflows correctly inject the nRF pairing secret; and confirm that error_shutdown path does not leak sensitive state.
Security signals we found
New boot-time authentication enforcement for coprocessor firmware
Fail-closed behavior: error_shutdown on authentication failure
Addition of pre-check timeout loop to ensure nRF is responsive before challenge
Feature flag separation (USE_NRF_AUTH) to allow auth without full BLE stack
SMCALL/secret key APIs now gated under USE_NRF_AUTH
Evidence from the diff
The patch introduces USE_NRF_AUTH as a distinct compile-time feature from USE_NRF/USE_BLE, wires nrf_authenticate() into the firmware main() boot path under #if PRODUCTION, and adds a 5-second pre-authentication wait loop for nRF communication readiness. It updates SCons feature lists, secret-key/SMCALL gating, and T3W1 model configurations to support nrf_auth independently. The change is defensive: it enforces nRF firmware authenticity at boot and fails closed (error_shutdown) on authentication failure.
Changed components
core/embed/projects/firmware/main.ccore/embed/io/nrf/stm32u5/nrf.ccore/embed/io/nrf/stm32u5/nrf_test.ccore/embed/sec/secret/stm32u5/secret_keys.ccore/embed/sec/secret/inc/sec/secret_keys.hcore/embed/sys/smcall/stm32/smcall_dispatch.ccore/embed/sys/smcall/stm32/smcall_verifiers.ccore/embed/sys/smcall/stm32/smcall_verifiers.hcore/site_scons/models/T3W1/trezor_t3w1_revA.pycore/site_scons/models/T3W1/trezor_t3w1_revB.pycore/site_scons/models/T3W1/trezor_t3w1_revC.pycore/SConscript.prodtestcore/SConscript.secmonInspect captured patch +38 / −9
diff --git a/core/SConscript.prodtest b/core/SConscript.prodtest
index 31c91531..55587429 100644
--- a/core/SConscript.prodtest
+++ b/core/SConscript.prodtest
@@ -31,6 +31,7 @@ FEATURES_WANTED = [
"input",
"kernel_mode",
"nfc",
+ "nrf_auth",
"optiga",
"power_manager",
"rgb_led",
diff --git a/core/SConscript.secmon b/core/SConscript.secmon
index 5f642ff4..50c067cf 100644
--- a/core/SConscript.secmon
+++ b/core/SConscript.secmon
@@ -42,6 +42,7 @@ FEATURES_WANTED = [
"storage",
"suspend",
"tropic",
+ "nrf_auth"
]
if DISABLE_OPTIGA:
diff --git a/core/embed/io/nrf/stm32u5/nrf.c b/core/embed/io/nrf/stm32u5/nrf.c
index bcb368d6..dec39608 100644
--- a/core/embed/io/nrf/stm32u5/nrf.c
+++ b/core/embed/io/nrf/stm32u5/nrf.c
@@ -551,6 +551,15 @@ bool nrf_authenticate(void) {
return false;
}
+ uint32_t timeout = ticks_timeout(5000);
+
+ // check that nRF communication is running prior to auth check
+ while (!ticks_expired(timeout)) {
+ if (nrf_get_info(&drv->info)) {
+ break;
+ }
+ }
+
drv->info_valid = false;
uint32_t challenge[8] = {0};
@@ -572,7 +581,7 @@ bool nrf_authenticate(void) {
return false;
}
- uint32_t timeout = ticks_timeout(100);
+ timeout = ticks_timeout(100);
while (!ticks_expired(timeout)) {
if (drv->auth_data_valid) {
diff --git a/core/embed/io/nrf/stm32u5/nrf_test.c b/core/embed/io/nrf/stm32u5/nrf_test.c
index 767d93a9..5a16d2e5 100644
--- a/core/embed/io/nrf/stm32u5/nrf_test.c
+++ b/core/embed/io/nrf/stm32u5/nrf_test.c
@@ -226,6 +226,7 @@ cleanup:
}
#ifdef SECURE_MODE
+#ifdef USE_NRF_AUTH
bool nrf_test_pair(void) {
nrf_register_listener(NRF_SERVICE_PRODTEST, nrf_test_cb);
@@ -261,5 +262,6 @@ bool nrf_test_pair(void) {
return false;
}
#endif
+#endif
#endif
diff --git a/core/embed/projects/firmware/main.c b/core/embed/projects/firmware/main.c
index 4b88c360..32765ff7 100644
--- a/core/embed/projects/firmware/main.c
+++ b/core/embed/projects/firmware/main.c
@@ -109,6 +109,14 @@ int main_func(uint32_t cmd, void *arg) {
}
#endif
+#ifdef USE_NRF
+#if PRODUCTION
+ if (!nrf_authenticate()) {
+ error_shutdown("Bluetooth authentication failed");
+ }
+#endif
+#endif
+
screen_boot_stage_2(fading);
notify_send(NOTIFY_BOOT);
diff --git a/core/embed/sec/secret/inc/sec/secret_keys.h b/core/embed/sec/secret/inc/sec/secret_keys.h
index cf13ad0d..1eacf2c7 100644
--- a/core/embed/sec/secret/inc/sec/secret_keys.h
+++ b/core/embed/sec/secret/inc/sec/secret_keys.h
@@ -56,7 +56,7 @@ secbool secret_key_tropic_masking(uint8_t dest[ECDSA_PRIVATE_KEY_SIZE]);
#endif // USE_TROPIC
-#ifdef USE_NRF
+#ifdef USE_NRF_AUTH
#define NRF_PAIRING_SECRET_SIZE 32
secbool secret_key_nrf_pairing(uint8_t dest[NRF_PAIRING_SECRET_SIZE]);
@@ -71,7 +71,7 @@ secbool secret_key_storage_salt(uint16_t fw_type,
#endif // SECURE_MODE
#ifdef KERNEL_MODE
-#ifdef USE_NRF
+#ifdef USE_NRF_AUTH
secbool secret_validate_nrf_pairing(const uint8_t *message, size_t msg_len,
const uint8_t *mac, size_t mac_len);
diff --git a/core/embed/sec/secret/stm32u5/secret_keys.c b/core/embed/sec/secret/stm32u5/secret_keys.c
index 41b448aa..bd2073d3 100644
--- a/core/embed/sec/secret/stm32u5/secret_keys.c
+++ b/core/embed/sec/secret/stm32u5/secret_keys.c
@@ -158,7 +158,7 @@ secbool secret_key_tropic_masking(uint8_t dest[ECDSA_PRIVATE_KEY_SIZE]) {
#endif // USE_TROPIC
-#ifdef USE_NRF
+#ifdef USE_NRF_AUTH
static secbool secequal(const void *ptr1, const void *ptr2, size_t n) {
const uint8_t *p1 = ptr1;
diff --git a/core/embed/sys/smcall/stm32/smcall_dispatch.c b/core/embed/sys/smcall/stm32/smcall_dispatch.c
index ae89b85e..53890e5d 100644
--- a/core/embed/sys/smcall/stm32/smcall_dispatch.c
+++ b/core/embed/sys/smcall/stm32/smcall_dispatch.c
@@ -132,7 +132,7 @@ __attribute((no_stack_protector)) void smcall_handler(uint32_t *args,
} break;
#endif
-#ifdef USE_NRF
+#ifdef USE_NRF_AUTH
case SMCALL_SECRET_VALIDATE_NRF_PAIRING: {
const uint8_t *message = (const uint8_t *)args[0];
size_t message_len = args[1];
@@ -141,8 +141,7 @@ __attribute((no_stack_protector)) void smcall_handler(uint32_t *args,
args[0] = secret_validate_nrf_pairing__verified(message, message_len, mac,
mac_len);
} break;
-
-#endif // USE_NRF
+#endif // USE_NRF_AUTH
case SMCALL_WAIT_RANDOM: {
wait_random();
diff --git a/core/embed/sys/smcall/stm32/smcall_verifiers.c b/core/embed/sys/smcall/stm32/smcall_verifiers.c
index ecd4f6b6..e18bd367 100644
--- a/core/embed/sys/smcall/stm32/smcall_verifiers.c
+++ b/core/embed/sys/smcall/stm32/smcall_verifiers.c
@@ -477,7 +477,7 @@ access_violation:
#endif // USE_BACKUP_RAM
-#ifdef USE_NRF
+#ifdef USE_NRF_AUTH
secbool secret_validate_nrf_pairing__verified(const uint8_t *message,
size_t msg_len,
const uint8_t *mac,
diff --git a/core/embed/sys/smcall/stm32/smcall_verifiers.h b/core/embed/sys/smcall/stm32/smcall_verifiers.h
index 34e9bf6c..b5f11243 100644
--- a/core/embed/sys/smcall/stm32/smcall_verifiers.h
+++ b/core/embed/sys/smcall/stm32/smcall_verifiers.h
@@ -134,7 +134,10 @@ bool backup_ram_write__verified(uint16_t key, backup_ram_item_type_t type,
#endif // USE_BACKUP_RAM
-#ifdef USE_NRF
+#ifdef USE_NRF_AUTH
+
+#include <sec/secret_keys.h>
+
secbool secret_validate_nrf_pairing__verified(const uint8_t *message,
size_t msg_len,
const uint8_t *mac,
diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revA.py b/core/site_scons/models/T3W1/trezor_t3w1_revA.py
index 329c2b38..09d0b97d 100644
--- a/core/site_scons/models/T3W1/trezor_t3w1_revA.py
+++ b/core/site_scons/models/T3W1/trezor_t3w1_revA.py
@@ -137,6 +137,8 @@ def configure(
"vendor/stm32u5xx_hal_driver/Src/stm32u5xx_hal_uart.c",
"vendor/stm32u5xx_hal_driver/Src/stm32u5xx_hal_uart_ex.c",
]
+ if "nrf_auth" in features_wanted or "ble" in features_wanted:
+ defines += [("USE_NRF_AUTH", "1")]
if "ble" in features_wanted and "smp" in features_wanted:
sources += ["embed/io/nrf/stm32u5/nrf_uart.c"]
diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revB.py b/core/site_scons/models/T3W1/trezor_t3w1_revB.py
index 14c119e4..1feb097c 100644
--- a/core/site_scons/models/T3W1/trezor_t3w1_revB.py
+++ b/core/site_scons/models/T3W1/trezor_t3w1_revB.py
@@ -138,6 +138,8 @@ def configure(
"vendor/stm32u5xx_hal_driver/Src/stm32u5xx_hal_uart.c",
"vendor/stm32u5xx_hal_driver/Src/stm32u5xx_hal_uart_ex.c",
]
+ if "nrf_auth" in features_wanted or "ble" in features_wanted:
+ defines += [("USE_NRF_AUTH", "1")]
if "ble" in features_wanted and "smp" in features_wanted:
sources += ["embed/io/nrf/stm32u5/nrf_uart.c"]
diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revC.py b/core/site_scons/models/T3W1/trezor_t3w1_revC.py
index 1fbf4c88..6b3d55bd 100644
--- a/core/site_scons/models/T3W1/trezor_t3w1_revC.py
+++ b/core/site_scons/models/T3W1/trezor_t3w1_revC.py
@@ -137,6 +137,8 @@ def configure(
"vendor/stm32u5xx_hal_driver/Src/stm32u5xx_hal_uart.c",
"vendor/stm32u5xx_hal_driver/Src/stm32u5xx_hal_uart_ex.c",
]
+ if "nrf_auth" in features_wanted or "ble" in features_wanted:
+ defines += [("USE_NRF_AUTH", "1")]
if "ble" in features_wanted and "smp" in features_wanted:
sources += ["embed/io/nrf/stm32u5/nrf_uart.c"]
Why this scored 58/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.