feat(core/embed): reset tropic and retry command when alarm mode is detected
What changed, and why it matters
This commit adds automatic retry logic for a secure chip (Tropic) inside some Trezor hardware wallets. When the chip reports it is in 'alarm mode' or certain communication errors occur, the device now resets the chip and retries the command up to five times. The change is framed as a reliability improvement, but it touches sensitive security operations like signing, PIN verification, and key storage. There is no direct evidence this fixes an exploitable vulnerability, but retrying destructive commands (like 'mac_and_destroy') could in theory have security side effects if not handled carefully.
Treat as a reliability/hardening change rather than a confirmed security fix. Reviewers should verify that retrying lt_mac_and_destroy cannot cause double-execution or state desynchronization, confirm that session restart re-establishes the same authenticated channel, and ensure retry loops cannot be induced by an attacker to bypass monotonic counters or PIN-attempt limits. No immediate user action is indicated.
Security signals we found
New retry wrapper around security-critical secure-element commands
Automatic reset and session restart on LT_L1_CHIP_ALARM_MODE
Retry applied to destructive/irreversible operation lt_mac_and_destroy
Retry applied to EdDSA signing and PIN-stretching flows
Cache invalidation added for change-PIN counter after update
No explicit security advisory, CVE, or bug bounty attribution in commit
Evidence from the diff
The patch introduces a TROPIC_RETRY_COMMAND macro that wraps low-level Tropic secure-element calls. On non-emulator builds, it captures the current session state and pairing key index, evaluates the command, and if the return code is LT_L1_CHIP_ALARM_MODE, LT_L1_SPI_ERROR, LT_L2_IN_CRC_ERR, or LT_L2_CRC_ERR, it deinitializes the chip, calls the new tropic01_reset(), re-initializes, waits for ready, and restarts the custom session if one was open, then retries the command up to TROPIC_MAX_RETRIES (5). The wrapper is applied to session start, EdDSA signing, monotonic counter get/update/init, mac_and_destroy, KEK mask write/read, and a helper that generates mac_and_destroy output. The changelog labels this as a feature (‘[T3W1] Reset Tropic and retry command when alarm mode is detected’).
Changed components
core/embed/sec/tropic/tropic.ccore/embed/sec/tropic/inc/sec/tropic.hTropic secure-element driver (T3W1 device)tropic_custom_session_starttropic_ecc_signtropic_pin_stretch / tropic_pin_set / tropic_pin_reset_slotstropic_pin_set_kek_masks / tropic_pin_unmask_kekmonotonic counter managementInspect captured patch +82 / −27
diff --git a/core/.changelog.d/6104.added b/core/.changelog.d/6104.added
new file mode 100644
index 000000000..56b5da3eb
--- /dev/null
+++ b/core/.changelog.d/6104.added
@@ -0,0 +1 @@
+[T3W1] Reset Tropic and retry command when alarm mode is detected.
diff --git a/core/embed/sec/tropic/inc/sec/tropic.h b/core/embed/sec/tropic/inc/sec/tropic.h
index 6454f4005..6450e96cb 100644
--- a/core/embed/sec/tropic/inc/sec/tropic.h
+++ b/core/embed/sec/tropic/inc/sec/tropic.h
@@ -57,6 +57,8 @@ bool tropic_init(uint16_t port);
bool tropic_init(void);
#endif
+void tropic01_reset(void);
+
void tropic_deinit(void);
#ifdef TREZOR_PRODTEST
diff --git a/core/embed/sec/tropic/tropic.c b/core/embed/sec/tropic/tropic.c
index cfeb0f118..b281b73a4 100644
--- a/core/embed/sec/tropic/tropic.c
+++ b/core/embed/sec/tropic/tropic.c
@@ -66,6 +66,51 @@
#define TROPIC_CHANGE_COUNTER_SLOT MCOUNTER_INDEX_4
#define TROPIC_CHANGE_COUNTER_SLOT_MAX_VALUE 0xfffffffe
+#ifdef TREZOR_EMULATOR
+#define TROPIC_RETRY_COMMAND(command) command
+#else
+#define TROPIC_MAX_RETRIES 5
+
+bool tropic_session_start(void);
+
+static bool is_retryable(lt_ret_t ret) {
+ return ret == LT_L1_CHIP_ALARM_MODE || ret == LT_L1_SPI_ERROR ||
+ ret == LT_L2_IN_CRC_ERR || ret == LT_L2_CRC_ERR;
+}
+
+// Statement expression, see
+// https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html
+#define TROPIC_RETRY_COMMAND(command) \
+ ({ \
+ bool TROPIC_RETRY_COMMAND_session_started = \
+ g_tropic_driver.session_started; \
+ pkey_index_t TROPIC_RETRY_COMMAND_pairing_key_index = \
+ g_tropic_driver.pairing_key_index; \
+ lt_ret_t TROPIC_RETRY_COMMAND_res = command; \
+ for (int TROPIC_RETRY_COMMAND_i = 0; \
+ TROPIC_RETRY_COMMAND_i < TROPIC_MAX_RETRIES - 1; \
+ TROPIC_RETRY_COMMAND_i++) { \
+ if (!is_retryable(TROPIC_RETRY_COMMAND_res)) { \
+ break; \
+ } \
+ if (TROPIC_RETRY_COMMAND_res == LT_L1_CHIP_ALARM_MODE) { \
+ tropic_deinit(); \
+ tropic01_reset(); \
+ tropic_init(); \
+ tropic_wait_for_ready(); \
+ if (TROPIC_RETRY_COMMAND_session_started) { \
+ if (tropic_custom_session_start( \
+ TROPIC_RETRY_COMMAND_pairing_key_index) != LT_OK) { \
+ continue; \
+ } \
+ } \
+ } \
+ TROPIC_RETRY_COMMAND_res = command; \
+ } \
+ TROPIC_RETRY_COMMAND_res; \
+ })
+#endif // TREZOR_EMULATOR
+
typedef struct {
bool initialized;
bool session_started;
@@ -225,8 +270,9 @@ lt_ret_t tropic_custom_session_start(pkey_index_t pairing_key_index) {
tropic_wait_for_ready();
- ret = lt_session_start(&drv->handle, tropic_public, pairing_key_index,
- trezor_private, trezor_public);
+ ret = TROPIC_RETRY_COMMAND(lt_session_start(&drv->handle, tropic_public,
+ pairing_key_index, trezor_private,
+ trezor_public));
drv->session_started = (ret == LT_OK);
drv->pairing_key_index = pairing_key_index;
@@ -352,8 +398,8 @@ bool tropic_ecc_sign(uint16_t key_slot_index, const uint8_t *dig,
return false;
}
- lt_ret_t res =
- lt_ecc_eddsa_sign(&drv->handle, key_slot_index, dig, dig_len, sig);
+ lt_ret_t res = TROPIC_RETRY_COMMAND(
+ lt_ecc_eddsa_sign(&drv->handle, key_slot_index, dig, dig_len, sig));
if (res != LT_OK) {
memzero(sig, ECDSA_RAW_SIGNATURE_SIZE);
return false;
@@ -488,8 +534,8 @@ static bool get_change_pin_counter(uint32_t *change_pin_counter) {
return true;
}
- lt_ret_t ret = lt_mcounter_get(&drv->handle, TROPIC_CHANGE_COUNTER_SLOT,
- change_pin_counter);
+ lt_ret_t ret = TROPIC_RETRY_COMMAND(lt_mcounter_get(
+ &drv->handle, TROPIC_CHANGE_COUNTER_SLOT, change_pin_counter));
if (ret == LT_OK) {
*change_pin_counter =
TROPIC_CHANGE_COUNTER_SLOT_MAX_VALUE - *change_pin_counter;
@@ -517,11 +563,15 @@ static bool update_change_pin_counter() {
tropic_driver_t *drv = &g_tropic_driver;
lt_ret_t ret = LT_FAIL;
- ret = lt_mcounter_update(&drv->handle, TROPIC_CHANGE_COUNTER_SLOT);
+ // The cache is invalidated because the counter may be updated more than once
+ g_is_change_pin_counter_cached = false;
+ ret = TROPIC_RETRY_COMMAND(
+ lt_mcounter_update(&drv->handle, TROPIC_CHANGE_COUNTER_SLOT));
if (ret == LT_L3_COUNTER_INVALID) {
// The counter has not been initialized yet
- ret = lt_mcounter_init(&drv->handle, TROPIC_CHANGE_COUNTER_SLOT,
- TROPIC_CHANGE_COUNTER_SLOT_MAX_VALUE - 1);
+ ret = TROPIC_RETRY_COMMAND(
+ lt_mcounter_init(&drv->handle, TROPIC_CHANGE_COUNTER_SLOT,
+ TROPIC_CHANGE_COUNTER_SLOT_MAX_VALUE - 1));
if (ret != LT_OK) {
return false;
}
@@ -534,10 +584,6 @@ static bool update_change_pin_counter() {
return false;
}
- if (g_is_change_pin_counter_cached) {
- g_change_pin_counter_cached++;
- }
-
return true;
}
@@ -571,9 +617,15 @@ bool tropic_pin_stretch(tropic_ui_progress_t ui_progress, uint16_t pin_index,
goto cleanup;
}
- if (lt_mac_and_destroy(
- &drv->handle, get_mac_and_destroy_slot(pin_index, change_pin_counter),
- digest, digest) != LT_OK) {
+ mac_and_destroy_slot_t slot_index =
+ get_mac_and_destroy_slot(pin_index, change_pin_counter);
+ // When `lt_mac_and_destroy()` returns an error and it is unclear whether the
+ // command was executed or not (for example, it returns LT_L1_CHIP_ALARM_MODE
+ // or LT_L1_SPI_ERROR), the best approach is to retry the command, hoping it
+ // has not already been been executed. If it has been executed, the PIN
+ // verification will fail.
+ if (TROPIC_RETRY_COMMAND(lt_mac_and_destroy(&drv->handle, slot_index, digest,
+ digest)) != LT_OK) {
goto cleanup;
}
@@ -617,9 +669,9 @@ bool tropic_pin_reset_slots(
}
for (int i = 0; i <= pin_index; i++) {
- if (lt_mac_and_destroy(&drv->handle,
- get_mac_and_destroy_slot(i, change_pin_counter),
- reset_key, output) != LT_OK) {
+ if (TROPIC_RETRY_COMMAND(lt_mac_and_destroy(
+ &drv->handle, get_mac_and_destroy_slot(i, change_pin_counter),
+ reset_key, output)) != LT_OK) {
goto cleanup;
}
}
@@ -696,16 +748,16 @@ bool tropic_pin_set(
hmac_sha256(stretched_pins[i], TROPIC_MAC_AND_DESTROY_SIZE, NULL, 0,
digest);
- if (generate_correct_mac_and_destroy_output(
- &drv->handle, slot_index, reset_key, digest, output) != LT_OK) {
+ if (TROPIC_RETRY_COMMAND(generate_correct_mac_and_destroy_output(
+ &drv->handle, slot_index, reset_key, digest, output)) != LT_OK) {
goto cleanup;
}
hmac_sha256(stretched_pins[i], TROPIC_MAC_AND_DESTROY_SIZE, output,
sizeof(output), stretched_pins[i]);
- if (lt_mac_and_destroy(&drv->handle, slot_index, reset_key, output) !=
- LT_OK) {
+ if (TROPIC_RETRY_COMMAND(lt_mac_and_destroy(&drv->handle, slot_index,
+ reset_key, output)) != LT_OK) {
goto cleanup;
}
}
@@ -756,8 +808,8 @@ bool tropic_pin_set_kek_masks(
uint16_t masked_kek_slot = get_kek_masks_slot(drv);
- if (lt_r_mem_data_erase_write(&drv->handle, masked_kek_slot, masks,
- sizeof(masks)) != LT_OK) {
+ if (TROPIC_RETRY_COMMAND(lt_r_mem_data_erase_write(
+ &drv->handle, masked_kek_slot, masks, sizeof(masks))) != LT_OK) {
goto cleanup;
}
@@ -795,8 +847,8 @@ bool tropic_pin_unmask_kek(
uint16_t masked_kek_slot = get_kek_masks_slot(drv);
- if (lt_r_mem_data_read(&drv->handle, masked_kek_slot, masks, &length) !=
- LT_OK) {
+ if (TROPIC_RETRY_COMMAND(lt_r_mem_data_read(&drv->handle, masked_kek_slot,
+ masks, &length)) != LT_OK) {
goto cleanup;
}
Why this scored 45/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.