feat(core/embed): introduce wear leveling of mac-and-destroy slots
What changed, and why it matters
This commit changes how the Trezor hardware wallet uses a special secure-chip feature called 'mac-and-destroy' slots during PIN verification. Previously, the same small set of slots was used every time the PIN was set or checked. Over many PIN changes, repeatedly erasing and rewriting those same slots could physically wear out that part of the secure chip. The patch spreads the wear across 64 slots by rotating which slot is used based on how many times the PIN has been changed. This is a reliability and physical-security improvement, not a fix for an obvious remote attack. There is no claim in the commit that this resolves a known security bug or reported vulnerability.
Treat as a hardening/reliability improvement. Review that the counter update and read are atomic with respect to power-loss during PIN change, and verify that failure paths in tropic_pin_set do not leave the counter updated while slots remain on the old counter value. No urgent security patch is indicated by the commit alone.
Security signals we found
Wear-leveling of secure-element mac-and-destroy slots
Introduction of a PIN-change monotonic counter
Defensive hardening against physical degradation of secure storage
No direct memory-safety, cryptographic, or authentication bug visible in diff
Evidence from the diff
The patch introduces wear leveling for mac-and-destroy slots in the Tropic secure-element integration. It moves slot constants from the header into tropic.c, adds TROPIC_MAC_AND_DESTROY_SLOTS_COUNT (64) and TROPIC_MAC_AND_DESTROY_SHIFT (11, chosen coprime to 64), and replaces the static first-slot lookup with get_mac_and_destroy_slot(pin_index, change_pin_counter). A new monotonic counter (MCOUNTER_INDEX_4) tracks PIN changes; its value is decremented from a max of 0xfffffffe. The counter is read in tropic_pin_stretch, tropic_pin_reset_slots, and tropic_pin_set, and updated in tropic_pin_set. Timing helper functions are added for the new counter operations. The change is defensive: it reduces repeated erase/write cycles on the same physical slots, which can help preserve secure-element lifetime and the integrity of PIN-stretching operations.
Changed components
core/embed/sec/tropic/tropic.ccore/embed/sec/tropic/inc/sec/tropic.hTropic secure-element PIN-stretching and PIN-set flowsInspect captured patch +127 / −30
diff --git a/core/.changelog.d/6136.added b/core/.changelog.d/6136.added
new file mode 100644
index 000000000..5b512cdaf
--- /dev/null
+++ b/core/.changelog.d/6136.added
@@ -0,0 +1 @@
+[T3W1] Introduce wear leveling of mac-and-destroy slots in Tropic.
diff --git a/core/embed/sec/tropic/inc/sec/tropic.h b/core/embed/sec/tropic/inc/sec/tropic.h
index 21c8211d0..6454f4005 100644
--- a/core/embed/sec/tropic/inc/sec/tropic.h
+++ b/core/embed/sec/tropic/inc/sec/tropic.h
@@ -36,16 +36,6 @@
#define TROPIC_DEVICE_CERT_SLOT_COUNT 3
#define TROPIC_DEVICE_KEY_SLOT 0 // ECC_SLOT_0
-#ifdef USE_STORAGE
-// KEK masks used in PIN verification
-#define TROPIC_KEK_MASKS_PRIVILEGED_SLOT 128
-#define TROPIC_KEK_MASKS_UNPRIVILEGED_SLOT 256
-
-// Mac-and-destroy slots used in PIN verification
-#define TROPIC_FIRST_MAC_AND_DESTROY_SLOT_PRIVILEGED 0
-#define TROPIC_FIRST_MAC_AND_DESTROY_SLOT_UNPRIVILEGED 64
-#endif
-
// Pairing key used by prodtest to inject the privileged and unprivileged
// pairing keys.
#define TROPIC_FACTORY_PAIRING_KEY_SLOT 0 // PAIRING_KEY_SLOT_INDEX_0
diff --git a/core/embed/sec/tropic/tropic.c b/core/embed/sec/tropic/tropic.c
index bc484bf67..5ff8693d8 100644
--- a/core/embed/sec/tropic/tropic.c
+++ b/core/embed/sec/tropic/tropic.c
@@ -43,6 +43,29 @@
// Maximum time to wait for Tropic to boot. Chosen arbitrarily.
#define TROPIC_BOOT_TIMEOUT_MS 1000
+// KEK masks used in PIN verification
+#define TROPIC_KEK_MASKS_PRIVILEGED_SLOT 128
+#define TROPIC_KEK_MASKS_UNPRIVILEGED_SLOT 256
+
+// Mac-and-destroy slots used in PIN verification
+#define TROPIC_FIRST_MAC_AND_DESTROY_SLOT_PRIVILEGED 0
+#define TROPIC_FIRST_MAC_AND_DESTROY_SLOT_UNPRIVILEGED 64
+#define TROPIC_MAC_AND_DESTROY_SLOTS_COUNT 64
+
+// The value by which the index of the first mac-and-destroy slot is shifted
+// before every PIN change. The value is coprime to
+// TROPIC_MAC_AND_DESTROY_SLOTS_COUNT to ensure that
+// `get_mac_and_destroy_slot(i, change_pin_counter)` achieves the maximum
+// possible period in `change_pin_counter`.
+#define TROPIC_MAC_AND_DESTROY_SHIFT 11
+
+// The slot that stores the number of times the PIN has been changed.
+// Since the counter can only be decremented, its acutal value is
+// TROPIC_CHANGE_COUNTER_SLOT_MAX_VALUE minus the number of PIN changes. This
+// slot is used both for both privileged and the unprivileged sessions.
+#define TROPIC_CHANGE_COUNTER_SLOT MCOUNTER_INDEX_4
+#define TROPIC_CHANGE_COUNTER_SLOT_MAX_VALUE 0xfffffffe
+
typedef struct {
bool initialized;
bool session_started;
@@ -402,11 +425,16 @@ void tropic_random_buffer_time(uint32_t *time_ms) {
// Defined in tropic01.c
void tropic_set_ui_progress(tropic_ui_progress_t f);
-static mac_and_destroy_slot_t get_first_mac_and_destroy_slot(
- tropic_driver_t *drv) {
- return drv->pairing_key_index == TROPIC_UNPRIVILEGED_PAIRING_KEY_SLOT
- ? TROPIC_FIRST_MAC_AND_DESTROY_SLOT_UNPRIVILEGED
- : TROPIC_FIRST_MAC_AND_DESTROY_SLOT_PRIVILEGED;
+static mac_and_destroy_slot_t get_mac_and_destroy_slot(
+ uint16_t pin_index, uint32_t change_pin_counter) {
+ mac_and_destroy_slot_t first_slot_index =
+ g_tropic_driver.pairing_key_index == TROPIC_UNPRIVILEGED_PAIRING_KEY_SLOT
+ ? TROPIC_FIRST_MAC_AND_DESTROY_SLOT_UNPRIVILEGED
+ : TROPIC_FIRST_MAC_AND_DESTROY_SLOT_PRIVILEGED;
+
+ return first_slot_index +
+ (change_pin_counter * TROPIC_MAC_AND_DESTROY_SHIFT + pin_index) %
+ TROPIC_MAC_AND_DESTROY_SLOTS_COUNT;
}
static uint16_t get_kek_masks_slot(tropic_driver_t *drv) {
@@ -427,8 +455,66 @@ static void lt_r_mem_data_write_time(uint32_t *time_ms) {
*time_ms += 77;
}
+static void lt_mcounter_get_time(uint32_t *time_ms) { *time_ms += 51; }
+
+static void lt_mcounter_update_time(uint32_t *time_ms) { *time_ms += 51; }
+
static void lt_r_mem_data_erase_time(uint32_t *time_ms) { *time_ms += 55; }
+static bool get_change_pin_counter(uint32_t *change_pin_counter) {
+ tropic_driver_t *drv = &g_tropic_driver;
+
+ lt_ret_t ret = lt_mcounter_get(&drv->handle, TROPIC_CHANGE_COUNTER_SLOT,
+ change_pin_counter);
+ if (ret == LT_L3_COUNTER_INVALID) {
+ // The counter has not been initialized yet
+ *change_pin_counter = 0;
+ return true;
+ }
+
+ if (ret != LT_OK) {
+ return false;
+ }
+
+ *change_pin_counter =
+ TROPIC_CHANGE_COUNTER_SLOT_MAX_VALUE - *change_pin_counter;
+
+ return true;
+}
+
+static void get_change_pin_counter_time(uint32_t *time_ms) {
+ lt_mcounter_get_time(time_ms);
+}
+
+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);
+ 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);
+ if (ret != LT_OK) {
+ return false;
+ }
+
+ return true;
+ }
+
+ if (ret != LT_OK) {
+ return false;
+ }
+
+ return true;
+}
+
+static void update_change_pin_counter_time(uint32_t *time_ms) {
+ lt_mcounter_update_time(time_ms);
+ // Ignore the time of `lt_mcounter_init()` since we cannot easily determine
+ // whether it will be executed.
+}
+
bool tropic_pin_stretch(tropic_ui_progress_t ui_progress, uint16_t pin_index,
uint8_t stretched_pin[TROPIC_MAC_AND_DESTROY_SIZE]) {
if (pin_index >= PIN_MAX_TRIES) {
@@ -444,14 +530,18 @@ bool tropic_pin_stretch(tropic_ui_progress_t ui_progress, uint16_t pin_index,
goto cleanup;
}
- mac_and_destroy_slot_t first_slot_index = get_first_mac_and_destroy_slot(drv);
-
uint8_t digest[TROPIC_MAC_AND_DESTROY_SIZE] = {0};
hmac_sha256(stretched_pin, TROPIC_MAC_AND_DESTROY_SIZE, NULL, 0, digest);
- if (lt_mac_and_destroy(&drv->handle, first_slot_index + pin_index, digest,
- digest) != LT_OK) {
+ uint32_t change_pin_counter = 0;
+ if (!get_change_pin_counter(&change_pin_counter)) {
+ goto cleanup;
+ }
+
+ if (lt_mac_and_destroy(
+ &drv->handle, get_mac_and_destroy_slot(pin_index, change_pin_counter),
+ digest, digest) != LT_OK) {
goto cleanup;
}
@@ -467,6 +557,7 @@ cleanup:
}
void tropic_pin_stretch_time(uint32_t *time_ms) {
+ get_change_pin_counter_time(time_ms);
lt_mac_and_destroy_time(time_ms);
}
@@ -488,11 +579,15 @@ bool tropic_pin_reset_slots(
uint8_t output[TROPIC_MAC_AND_DESTROY_SIZE] = {0};
- mac_and_destroy_slot_t first_slot_index = get_first_mac_and_destroy_slot(drv);
+ uint32_t change_pin_counter = 0;
+ if (!get_change_pin_counter(&change_pin_counter)) {
+ goto cleanup;
+ }
for (int i = 0; i <= pin_index; i++) {
- if (lt_mac_and_destroy(&drv->handle, first_slot_index + i, reset_key,
- output) != LT_OK) {
+ if (lt_mac_and_destroy(&drv->handle,
+ get_mac_and_destroy_slot(i, change_pin_counter),
+ reset_key, output) != LT_OK) {
goto cleanup;
}
}
@@ -507,6 +602,7 @@ cleanup:
}
void tropic_pin_reset_slots_time(uint32_t *time_ms, uint16_t pin_index) {
+ get_change_pin_counter_time(time_ms);
for (int i = 0; i <= pin_index; i++) {
lt_mac_and_destroy_time(time_ms);
}
@@ -529,30 +625,38 @@ bool tropic_pin_set(
goto cleanup;
}
+ if (!update_change_pin_counter()) {
+ goto cleanup;
+ }
+ uint32_t change_pin_counter = 0;
+ if (!get_change_pin_counter(&change_pin_counter)) {
+ goto cleanup;
+ }
+
uint8_t output[TROPIC_MAC_AND_DESTROY_SIZE] = {0};
uint8_t digest[TROPIC_MAC_AND_DESTROY_SIZE] = {0};
- mac_and_destroy_slot_t first_slot_index = get_first_mac_and_destroy_slot(drv);
-
for (int i = 0; i < PIN_MAX_TRIES; i++) {
- if (lt_mac_and_destroy(&drv->handle, first_slot_index + i, reset_key,
- output) != LT_OK) {
+ mac_and_destroy_slot_t slot_index =
+ get_mac_and_destroy_slot(i, change_pin_counter);
+
+ if (lt_mac_and_destroy(&drv->handle, slot_index, reset_key, output) !=
+ LT_OK) {
goto cleanup;
}
hmac_sha256(stretched_pins[i], TROPIC_MAC_AND_DESTROY_SIZE, NULL, 0,
digest);
- if (lt_mac_and_destroy(&drv->handle, first_slot_index + i, digest,
- digest) != LT_OK) {
+ if (lt_mac_and_destroy(&drv->handle, slot_index, digest, digest) != LT_OK) {
goto cleanup;
}
hmac_sha256(stretched_pins[i], TROPIC_MAC_AND_DESTROY_SIZE, digest,
sizeof(digest), stretched_pins[i]);
- if (lt_mac_and_destroy(&drv->handle, first_slot_index + i, reset_key,
- output) != LT_OK) {
+ if (lt_mac_and_destroy(&drv->handle, slot_index, reset_key, output) !=
+ LT_OK) {
goto cleanup;
}
}
@@ -569,6 +673,8 @@ cleanup:
void tropic_pin_set_time(uint32_t *time_ms) {
rng_fill_buffer_strong_time(time_ms);
+ update_change_pin_counter_time(time_ms);
+ get_change_pin_counter_time(time_ms);
for (int i = 0; i < PIN_MAX_TRIES; i++) {
lt_mac_and_destroy_time(time_ms);
lt_mac_and_destroy_time(time_ms);
Why this scored 35/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.