Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/.changelog.d/6136.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[T3W1] Introduce wear leveling of mac-and-destroy slots in Tropic.
10 changes: 0 additions & 10 deletions core/embed/sec/tropic/inc/sec/tropic.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
165 changes: 145 additions & 20 deletions core/embed/sec/tropic/tropic.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 a 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;
Expand Down Expand Up @@ -370,11 +393,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) {
Expand All @@ -395,8 +423,85 @@ 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 uint32_t change_pin_counter_cached = 0;
static bool is_change_pin_counter_cached = false;

static bool get_change_pin_counter(uint32_t *change_pin_counter) {
tropic_driver_t *drv = &g_tropic_driver;

if (is_change_pin_counter_cached) {
*change_pin_counter = change_pin_counter_cached;
return true;
}

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;
change_pin_counter_cached = *change_pin_counter;
is_change_pin_counter_cached = true;
return true;
}

if (ret != LT_OK) {
return false;
}

*change_pin_counter =
TROPIC_CHANGE_COUNTER_SLOT_MAX_VALUE - *change_pin_counter;
change_pin_counter_cached = *change_pin_counter;
is_change_pin_counter_cached = true;

return true;
}

static void get_change_pin_counter_time(uint32_t *time_ms) {
if (!is_change_pin_counter_cached) {
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;
}
change_pin_counter_cached = 1;
is_change_pin_counter_cached = true;
return true;
}

if (ret != LT_OK) {
return false;
}

if (is_change_pin_counter_cached) {
change_pin_counter_cached++;
}

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) {
Expand All @@ -412,14 +517,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;
}

Expand All @@ -435,6 +544,7 @@ bool tropic_pin_stretch(tropic_ui_progress_t ui_progress, uint16_t pin_index,
}

void tropic_pin_stretch_time(uint32_t *time_ms) {
get_change_pin_counter_time(time_ms);
lt_mac_and_destroy_time(time_ms);
}

Expand All @@ -456,11 +566,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;
}
}
Expand All @@ -475,6 +589,7 @@ bool tropic_pin_reset_slots(
}

void tropic_pin_reset_slots_time(uint32_t *time_ms, uint16_t pin_index) {
get_change_pin_counter_time(time_ms);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling get_change_pin_counter_time() will give an overestimate when the counter is not cached, because it will have already been cached in tropic_pin_stretch(), so in tropic_pin_reset_slots() it will be a no-op.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 55a745a.

for (int i = 0; i <= pin_index; i++) {
lt_mac_and_destroy_time(time_ms);
}
Expand All @@ -497,30 +612,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;
}
}
Expand All @@ -537,6 +660,8 @@ bool tropic_pin_set(

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);
Copy link
Contributor

@andrewkozlik andrewkozlik Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling get_change_pin_counter_time() will give an incorrect result, because if the counter is not cached, then time will be added to the estimate, but in reality update_change_pin_counter() will have cached it, so it will be a no-op.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 55a745a.

for (int i = 0; i < PIN_MAX_TRIES; i++) {
lt_mac_and_destroy_time(time_ms);
lt_mac_and_destroy_time(time_ms);
Expand Down