feat(storage,core/embed): use tropic for pin protection
What changed, and why it matters
This commit adds support for using a second hardware security chip called Tropic to protect the device PIN, alongside the existing Optiga chip. It is a feature addition that strengthens PIN protection by adding another physical barrier against brute-force and chip-level attacks. There is no direct evidence in the commit that it fixes an existing security flaw; it appears to be a defensive hardening change.
Treat as a defensive hardening feature rather than an urgent vulnerability fix. Review the new Tropic PIN flow for correct error handling, ensure the reset key is properly cleared on all failure paths, and verify that the reduced PIN_MAX_TRIES is consistently enforced across UI and firmware when both chips are enabled. Conduct hardware-in-the-loop testing of the Tropic path before release.
Security signals we found
Adds hardware-backed PIN protection using Tropic secure element
Reduces PIN_MAX_TRIES from 16 to 10 when both Optiga and Tropic are enabled
Uses mac-and-destroy slots and protected memory for PIN-derived secrets
Stores a reset key encrypted in flash to reset Tropic slots after successful unlock
Adds static assertions to enforce size compatibility between SHA256 digest and Tropic slot sizes
Evidence from the diff
The patch extends Trezor’s storage subsystem to optionally use the Tropic secure element for PIN stretching, verification, and key encryption key (KEK) unmasking. New functions in tropic.c implement HMAC-SHA256 based PIN stretching using Tropic’s mac-and-destroy slots, KEK mask storage in protected memory, and reset-key handling after successful unlock. storage.c is updated to call these routines when USE_TROPIC is defined, parallel to existing USE_OPTIGA logic. PIN_MAX_TRIES is reduced from 16 to 10 when both Optiga and Tropic are enabled, because each attempt consumes a stretched PIN slot on Optiga. The change is gated behind build flags and does not alter behavior when Tropic is not present.
Changed components
core/embed/sec/tropic/tropic.ccore/embed/sec/tropic/inc/sec/tropic.hstorage/storage.cstorage/storage.hInspect captured patch +389 / −3
diff --git a/core/.changelog.d/5845.added b/core/.changelog.d/5845.added
new file mode 100644
index 00000000..b48b226d
--- /dev/null
+++ b/core/.changelog.d/5845.added
@@ -0,0 +1 @@
+Support using both Tropic and Optiga to protect PIN.
diff --git a/core/embed/sec/tropic/inc/sec/tropic.h b/core/embed/sec/tropic/inc/sec/tropic.h
index caf40793..26e5876c 100644
--- a/core/embed/sec/tropic/inc/sec/tropic.h
+++ b/core/embed/sec/tropic/inc/sec/tropic.h
@@ -19,6 +19,9 @@
#pragma once
+#ifdef USE_STORAGE
+#include <sec/storage.h>
+#endif
#include <trezor_types.h>
#include "ed25519-donna/ed25519.h"
@@ -33,6 +36,16 @@
#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
@@ -44,6 +57,8 @@
// Pairing key used by official firmware.
#define TROPIC_PRIVILEGED_PAIRING_KEY_SLOT 2 // PAIRING_KEY_SLOT_INDEX_2
+#define TROPIC_MAC_AND_DESTROY_SIZE 32
+
#ifdef KERNEL_MODE
bool tropic_init(void);
@@ -57,6 +72,8 @@ lt_handle_t* tropic_get_handle(void);
#endif
+typedef secbool (*tropic_ui_progress_t)(void);
+
void tropic_get_factory_privkey(curve25519_key privkey);
bool tropic_ping(const uint8_t* msg_out, uint8_t* msg_in, uint16_t msg_len);
@@ -75,3 +92,29 @@ bool tropic_data_multi_read(uint16_t first_slot, uint16_t slot_count,
size_t* data_length);
bool tropic_random_buffer(void* buffer, size_t length);
+
+#ifdef USE_STORAGE
+bool tropic_pin_stretch(tropic_ui_progress_t ui_progress, uint16_t pin_index,
+ uint8_t stretched_pin[TROPIC_MAC_AND_DESTROY_SIZE]);
+
+bool tropic_pin_reset_slots(
+ tropic_ui_progress_t ui_progress, uint16_t pin_index,
+ const uint8_t reset_key[TROPIC_MAC_AND_DESTROY_SIZE]);
+
+bool tropic_pin_set(
+ tropic_ui_progress_t ui_progress,
+ uint8_t stretched_pins[PIN_MAX_TRIES][TROPIC_MAC_AND_DESTROY_SIZE],
+ uint8_t reset_key[TROPIC_MAC_AND_DESTROY_SIZE]);
+
+bool tropic_pin_set_kek_masks(
+ tropic_ui_progress_t ui_progress,
+ const uint8_t kek[TROPIC_MAC_AND_DESTROY_SIZE],
+ const uint8_t stretched_pins[PIN_MAX_TRIES][TROPIC_MAC_AND_DESTROY_SIZE]);
+
+bool tropic_pin_unmask_kek(
+ tropic_ui_progress_t ui_progress, uint16_t pin_index,
+ const uint8_t stretched_pin[TROPIC_MAC_AND_DESTROY_SIZE],
+ uint8_t kek[TROPIC_MAC_AND_DESTROY_SIZE]);
+
+uint32_t tropic_estimate_time_ms(storage_pin_op_t op, uint16_t pin_index);
+#endif
diff --git a/core/embed/sec/tropic/tropic.c b/core/embed/sec/tropic/tropic.c
index 9dc59ba7..90324134 100644
--- a/core/embed/sec/tropic/tropic.c
+++ b/core/embed/sec/tropic/tropic.c
@@ -20,10 +20,13 @@
#include <trezor_rtl.h>
#include <trezor_types.h>
+#include <sec/rng.h>
#include <sec/secret_keys.h>
#include <sec/tropic.h>
#include <sys/systick.h>
+#include "hmac.h"
+
#include <libtropic.h>
#ifdef TREZOR_EMULATOR
@@ -287,6 +290,258 @@ bool tropic_random_buffer(void *buffer, size_t length) {
return true;
}
+#ifdef USE_STORAGE
+
+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 uint16_t get_kek_masks_slot(tropic_driver_t *drv) {
+ return drv->pairing_key_index == TROPIC_UNPRIVILEGED_PAIRING_KEY_SLOT
+ ? TROPIC_KEK_MASKS_UNPRIVILEGED_SLOT
+ : TROPIC_KEK_MASKS_PRIVILEGED_SLOT;
+}
+
+bool tropic_pin_stretch(tropic_ui_progress_t ui_progress, uint16_t pin_index,
+ uint8_t stretched_pin[TROPIC_MAC_AND_DESTROY_SIZE]) {
+ // Time: 50 ms
+
+ if (pin_index >= PIN_MAX_TRIES) {
+ return false;
+ }
+
+ tropic_driver_t *drv = &g_tropic_driver;
+
+ if (!drv->initialized) {
+ return false;
+ }
+
+ 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);
+
+ ui_progress();
+
+ lt_ret_t res = lt_mac_and_destroy(&drv->handle, first_slot_index + pin_index,
+ digest, digest);
+
+ ui_progress();
+
+ hmac_sha256(stretched_pin, TROPIC_MAC_AND_DESTROY_SIZE, digest,
+ sizeof(digest), stretched_pin);
+
+ memzero(digest, sizeof(digest));
+ return res == LT_OK;
+}
+
+bool tropic_pin_reset_slots(
+ tropic_ui_progress_t ui_progress, uint16_t pin_index,
+ const uint8_t reset_key[TROPIC_MAC_AND_DESTROY_SIZE]) {
+ // Time: (pin_index + 1) * 50 ms
+
+ if (pin_index >= PIN_MAX_TRIES) {
+ return false;
+ }
+
+ tropic_driver_t *drv = &g_tropic_driver;
+
+ if (!drv->initialized) {
+ return false;
+ }
+
+ lt_ret_t res = LT_FAIL;
+ uint8_t output[TROPIC_MAC_AND_DESTROY_SIZE] = {0};
+
+ mac_and_destroy_slot_t first_slot_index = get_first_mac_and_destroy_slot(drv);
+
+ ui_progress();
+
+ for (int i = 0; i <= pin_index; i++) {
+ res = lt_mac_and_destroy(&drv->handle, first_slot_index + i, reset_key,
+ output);
+ if (res != LT_OK) {
+ goto cleanup;
+ }
+
+ ui_progress();
+ }
+
+cleanup:
+ memzero(output, sizeof(output));
+
+ return res == LT_OK;
+}
+
+bool tropic_pin_set(
+ tropic_ui_progress_t ui_progress,
+ uint8_t stretched_pins[PIN_MAX_TRIES][TROPIC_MAC_AND_DESTROY_SIZE],
+ uint8_t reset_key[TROPIC_MAC_AND_DESTROY_SIZE]) {
+ // Time: 65 ms + PIN_MAX_TRIES * 155 ms
+
+ tropic_driver_t *drv = &g_tropic_driver;
+
+ if (!drv->initialized) {
+ return false;
+ }
+
+ if (!rng_fill_buffer_strong(reset_key, TROPIC_MAC_AND_DESTROY_SIZE)) {
+ return false;
+ }
+
+ lt_ret_t res = LT_FAIL;
+ 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);
+
+ ui_progress();
+
+ for (int i = 0; i < PIN_MAX_TRIES; i++) {
+ res = lt_mac_and_destroy(&drv->handle, first_slot_index + i, reset_key,
+ output);
+ if (res != LT_OK) {
+ goto cleanup;
+ }
+
+ hmac_sha256(stretched_pins[i], TROPIC_MAC_AND_DESTROY_SIZE, NULL, 0,
+ digest);
+
+ ui_progress();
+
+ res =
+ lt_mac_and_destroy(&drv->handle, first_slot_index + i, digest, digest);
+ if (res != LT_OK) {
+ goto cleanup;
+ }
+
+ ui_progress();
+
+ hmac_sha256(stretched_pins[i], TROPIC_MAC_AND_DESTROY_SIZE, digest,
+ sizeof(digest), stretched_pins[i]);
+
+ res = lt_mac_and_destroy(&drv->handle, first_slot_index + i, reset_key,
+ output);
+ if (res != LT_OK) {
+ goto cleanup;
+ }
+
+ ui_progress();
+ }
+
+cleanup:
+ memzero(output, sizeof(output));
+ memzero(digest, sizeof(digest));
+
+ return res == LT_OK;
+}
+
+bool tropic_pin_set_kek_masks(
+ tropic_ui_progress_t ui_progress,
+ const uint8_t kek[TROPIC_MAC_AND_DESTROY_SIZE],
+ const uint8_t stretched_pins[PIN_MAX_TRIES][TROPIC_MAC_AND_DESTROY_SIZE]) {
+ // Time: 130 ms
+
+ tropic_driver_t *drv = &g_tropic_driver;
+ lt_ret_t ret = LT_FAIL;
+
+ uint8_t masks[PIN_MAX_TRIES * TROPIC_MAC_AND_DESTROY_SIZE] = {0};
+ for (int i = 0; i < PIN_MAX_TRIES; i++) {
+ for (int j = 0; j < TROPIC_MAC_AND_DESTROY_SIZE; j++) {
+ masks[i * TROPIC_MAC_AND_DESTROY_SIZE + j] =
+ kek[j] ^ stretched_pins[i][j];
+ }
+ }
+
+ ui_progress();
+
+ uint16_t masked_kek_slot = get_kek_masks_slot(drv);
+
+ ret = lt_r_mem_data_erase(&drv->handle, masked_kek_slot);
+ if (ret != LT_OK) {
+ goto cleanup;
+ }
+
+ ui_progress();
+
+ ret =
+ lt_r_mem_data_write(&drv->handle, masked_kek_slot, masks, sizeof(masks));
+ if (ret != LT_OK) {
+ goto cleanup;
+ }
+
+ ui_progress();
+
+cleanup:
+ memzero(masks, sizeof(masks));
+
+ return ret == LT_OK;
+}
+
+bool tropic_pin_unmask_kek(
+ tropic_ui_progress_t ui_progress, uint16_t pin_index,
+ const uint8_t stretched_pin[TROPIC_MAC_AND_DESTROY_SIZE],
+ uint8_t kek[TROPIC_MAC_AND_DESTROY_SIZE]) {
+ // Time: 100 ms
+
+ tropic_driver_t *drv = &g_tropic_driver;
+
+ uint8_t masks[R_MEM_DATA_SIZE_MAX] = {0};
+ _Static_assert(
+ R_MEM_DATA_SIZE_MAX >= PIN_MAX_TRIES * TROPIC_MAC_AND_DESTROY_SIZE,
+ "R_MEM_DATA_SIZE_MAX too small");
+ uint16_t length = 0;
+
+ uint16_t masked_kek_slot = get_kek_masks_slot(drv);
+
+ ui_progress();
+
+ if (lt_r_mem_data_read(&drv->handle, masked_kek_slot, masks, &length) !=
+ LT_OK) {
+ return false;
+ }
+
+ if (length != PIN_MAX_TRIES * TROPIC_MAC_AND_DESTROY_SIZE) {
+ return false;
+ }
+
+ ui_progress();
+
+ for (int i = 0; i < TROPIC_MAC_AND_DESTROY_SIZE; i++) {
+ kek[i] =
+ masks[pin_index * TROPIC_MAC_AND_DESTROY_SIZE + i] ^ stretched_pin[i];
+ }
+ return true;
+}
+
+uint32_t tropic_estimate_time_ms(storage_pin_op_t op, uint16_t pin_index) {
+ const int set_time = 65 + PIN_MAX_TRIES * 155;
+ const int set_kek_masks_time = 130;
+ const int stretch_time = 50;
+ const int unmask_kek_time = 100;
+ const int reset_slots_time = (pin_index + 1) * 50;
+
+ const int pin_verify_time = stretch_time + unmask_kek_time + reset_slots_time;
+ const int pin_set_time = set_time + set_kek_masks_time;
+
+ switch (op) {
+ case STORAGE_PIN_OP_SET:
+ return pin_set_time;
+ case STORAGE_PIN_OP_VERIFY:
+ return pin_verify_time;
+ case STORAGE_PIN_OP_CHANGE:
+ return pin_set_time + pin_verify_time;
+ default:
+ return 0;
+ }
+}
+
+#endif // USE_STORAGE
+
#endif // SECURE_MODE
bool tropic_data_multi_size(uint16_t first_slot, size_t *data_length) {
diff --git a/storage/storage.c b/storage/storage.c
index 163d9bba..c2f02c80 100644
--- a/storage/storage.c
+++ b/storage/storage.c
@@ -42,6 +42,10 @@
#include "optiga.h"
#endif
+#if USE_TROPIC
+#include <sec/tropic.h>
+#endif
+
#ifdef USE_STORAGE_HWKEY
#include "secure_aes.h"
#endif
@@ -76,6 +80,11 @@
// NOTE: This should always equal the value in VERSION_KEY.
#define UNAUTH_VERSION_KEY ((APP_STORAGE << 8) | 0x08)
+#if USE_TROPIC
+// Key that is used to reset the M&D slots in Tropic after successfull unlock.
+#define TROPIC_MAC_AND_DESTROY_RESET_KEY ((APP_STORAGE << 8) | 0x09)
+#endif
+
#if USE_OPTIGA && STRETCHED_PIN_COUNT > 1
// Key that is used to reset the HMAC counter in Optiga after successfull
// unlock.
@@ -464,7 +473,7 @@ static secbool is_not_wipe_code(const uint8_t *pin, size_t pin_len) {
static uint32_t ui_estimate_time_ms(storage_pin_op_t op) {
uint32_t time_ms = 0;
-#if USE_OPTIGA
+#if USE_OPTIGA || USE_TROPIC
uint32_t pin_index = 0;
#if STRETCHED_PIN_COUNT > 1
if (sectrue != pin_get_fails(&pin_index)) {
@@ -475,6 +484,9 @@ static uint32_t ui_estimate_time_ms(storage_pin_op_t op) {
#if USE_OPTIGA
time_ms += optiga_estimate_time_ms(op, pin_index);
#endif
+#if USE_TROPIC
+ time_ms += tropic_estimate_time_ms(op, pin_index);
+#endif
uint32_t pbkdf2_ms = time_estimate_pbkdf2_ms(PIN_ITER_COUNT);
switch (op) {
@@ -694,11 +706,26 @@ static secbool __wur derive_kek_set(const uint8_t *pin, size_t pin_len,
if (!optiga_pin_stretch_cmac_ecdh(ui_progress, stretched_pins[0])) {
goto cleanup;
}
+#endif
#if STRETCHED_PIN_COUNT > 1
for (int i = 1; i < STRETCHED_PIN_COUNT; i++) {
memcpy(stretched_pins[i], stretched_pins[0], SHA256_DIGEST_LENGTH);
}
#endif
+#if USE_TROPIC
+ _Static_assert(SHA256_DIGEST_LENGTH == TROPIC_MAC_AND_DESTROY_SIZE);
+ uint8_t tropic_mac_and_destroy_reset_key[TROPIC_MAC_AND_DESTROY_SIZE] = {0};
+ if (!tropic_pin_set(ui_progress, stretched_pins,
+ tropic_mac_and_destroy_reset_key)) {
+ goto cleanup;
+ }
+ if (storage_set_encrypted(
+ TROPIC_MAC_AND_DESTROY_RESET_KEY, tropic_mac_and_destroy_reset_key,
+ sizeof(tropic_mac_and_destroy_reset_key)) != sectrue) {
+ goto cleanup;
+ }
+#endif
+#if USE_OPTIGA
_Static_assert(SHA256_DIGEST_LENGTH == OPTIGA_PIN_SECRET_SIZE);
uint8_t optiga_hmac_reset_key[SHA256_DIGEST_LENGTH] = {0};
if (!optiga_pin_set(ui_progress, stretched_pins, optiga_hmac_reset_key)) {
@@ -711,10 +738,26 @@ static secbool __wur derive_kek_set(const uint8_t *pin, size_t pin_len,
}
#endif
#endif
+#if USE_TROPIC
+ if (!rng_fill_buffer_strong(kek, SHA256_DIGEST_LENGTH)) {
+ goto cleanup;
+ }
+ if (tropic_pin_set_kek_masks(ui_progress, kek, stretched_pins) != true) {
+ goto cleanup;
+ }
+#else
+ _Static_assert(STRETCHED_PIN_COUNT == 1, "KEK masks not defined");
memcpy(kek, stretched_pins[0], SHA256_DIGEST_LENGTH);
+#endif
ret = sectrue;
-#if USE_OPTIGA
+#if USE_TROPIC || USE_OPTIGA
cleanup:
+#endif
+#if USE_TROPIC
+ memzero(tropic_mac_and_destroy_reset_key,
+ sizeof(tropic_mac_and_destroy_reset_key));
+#endif
+#if USE_OPTIGA
memzero(optiga_hmac_reset_key, sizeof(optiga_hmac_reset_key));
#endif
memzero(stretched_pins, sizeof(stretched_pins));
@@ -762,7 +805,7 @@ static secbool __wur derive_kek_unlock(
secbool privileged_bhk) {
mcu_pin_stretch(pin, pin_len, storage_salt, ext_salt, stretched_pin,
privileged_bhk);
-#if USE_OPTIGA
+#if USE_OPTIGA || USE_TROPIC
uint32_t pin_index = 0;
#if STRETCHED_PIN_COUNT > 1
uint32_t pin_fails = 0;
@@ -773,6 +816,12 @@ static secbool __wur derive_kek_unlock(
#if USE_OPTIGA
ensure(optiga_pin_stretch_cmac_ecdh(ui_progress, stretched_pin) * sectrue,
"optiga_pin_stretch_cmac_ecdh failed");
+#endif
+#if USE_TROPIC
+ ensure(tropic_pin_stretch(ui_progress, pin_index, stretched_pin) * sectrue,
+ "tropic_pin_stretch failed");
+#endif
+#if USE_OPTIGA
optiga_pin_result optiga_ret =
optiga_pin_verify(ui_progress, pin_index, stretched_pin);
if (optiga_ret != OPTIGA_PIN_SUCCESS) {
@@ -787,6 +836,12 @@ static secbool __wur derive_kek_unlock(
"optiga_pin_verify failed");
return secfalse;
}
+#endif
+#if USE_TROPIC
+ ensure(tropic_pin_unmask_kek(ui_progress, pin_index, stretched_pin,
+ stretched_pin) *
+ sectrue,
+ "tropic_pin_unmask_kek failed");
#endif
return sectrue;
}
@@ -1185,6 +1240,27 @@ static secbool unlock(const uint8_t *pin, size_t pin_len,
}
#endif
+#if USE_TROPIC
+ uint8_t tropic_mac_and_destroy_reset_key[TROPIC_MAC_AND_DESTROY_SIZE] = {0};
+ uint16_t tropic_mac_and_destroy_reset_key_len = 0;
+ if (storage_get_encrypted(TROPIC_MAC_AND_DESTROY_RESET_KEY,
+ &tropic_mac_and_destroy_reset_key,
+ sizeof(tropic_mac_and_destroy_reset_key),
+ &tropic_mac_and_destroy_reset_key_len) != sectrue ||
+ tropic_mac_and_destroy_reset_key_len !=
+ sizeof(tropic_mac_and_destroy_reset_key)) {
+ return secfalse;
+ }
+ if (!tropic_pin_reset_slots(ui_progress, ctr,
+ tropic_mac_and_destroy_reset_key)) {
+ memzero(tropic_mac_and_destroy_reset_key,
+ sizeof(tropic_mac_and_destroy_reset_key));
+ return secfalse;
+ }
+ memzero(tropic_mac_and_destroy_reset_key,
+ sizeof(tropic_mac_and_destroy_reset_key));
+#endif
+
// Finally set the counter to 0 to indicate success.
return pin_fails_reset();
}
diff --git a/storage/storage.h b/storage/storage.h
index 919b599a..6890bf81 100644
--- a/storage/storage.h
+++ b/storage/storage.h
@@ -44,10 +44,21 @@ extern const uint8_t *PIN_EMPTY;
// Maximum number of failed unlock attempts.
// NOTE: The PIN counter logic relies on this constant being less than or equal
// to 16.
+#if USE_TROPIC && USE_OPTIGA
+// If both Optiga and Tropic are used, every PIN attempt requires a stretched
+// PIN slot on Optiga. This restricts the total number of PIN
+// attempts.
+#define PIN_MAX_TRIES 10
+#else
#define PIN_MAX_TRIES 16
+#endif
// The number of slots configured as stretched PINs.
+#if USE_TROPIC
+#define STRETCHED_PIN_COUNT PIN_MAX_TRIES
+#else
#define STRETCHED_PIN_COUNT 1
+#endif
// The length of the random salt in bytes.
#if USE_OPTIGA
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.