feat(core): implement explicit secret sector locking mechanism
What changed, and why it matters
This commit adds a new 'lock' feature for a special secret-storage area inside Trezor hardware wallets. It lets factory-test software explicitly lock the secret sector after writing keys, and it makes the secret-write function report failures instead of silently crashing. The change is defensive: it reduces the chance that secrets can be accidentally or maliciously modified after production. There is no direct evidence in the commit that this fixes a known active attack.
Treat as a hardening/improvement commit rather than an urgent vulnerability fix. Review whether SECRET_LOCK_SLOT_OFFSET is defined for all shipped products, verify that lock status cannot be spoofed or rolled back, and ensure prodtest workflows always call secrets-lock before otp_variant_write. Consider adding a changelog entry for traceability.
Security signals we found
New explicit lock primitive for secret flash sector
secret_write() changed from void to secbool to propagate failures
prodtest now refuses OTP variant write if secrets are not locked
Fallback lock detection heuristic based on key writability/presence retained for older configurations
No changelog entry despite security-relevant behavior change
Evidence from the diff
The patch introduces an explicit secret-sector locking mechanism for prodtest/firmware. Key changes: (1) secret_write() now returns secbool and propagates flash-write failures rather than calling ensure() inside the loop; (2) new prodtest CLI command ‘secrets-lock’ and secret_lock()/secret_is_locked() APIs are added, gated by SECRET_LOCK_SLOT_OFFSET; (3) on STM32U5, locking is implemented by writing a zero-filled slot and checking it later; (4) otp_variant_write now refuses to proceed unless secrets are locked when the lock slot is defined; (5) secret_prepare_fw logic is refactored to use the explicit lock status when available, falling back to key-presence/writability heuristics otherwise. The change improves error handling and enforces a production-time lock step.
Changed components
core/embed/sec/secret/stm32u5/secret.ccore/embed/sec/secret/stm32f4/secret.ccore/embed/sec/secret/unix/secret.ccore/embed/sec/secret/inc/sec/secret.hcore/embed/projects/prodtest/cmd/prodtest_secrets.ccore/embed/projects/prodtest/cmd/prodtest_otp_variant.ccore/embed/sec/monoctr/stm32u5/monoctr.ccore/embed/rtl/inc/rtl/secbool.hInspect captured patch +173 / −21
diff --git a/core/embed/projects/prodtest/README.md b/core/embed/projects/prodtest/README.md
index ce95959a8..8a7211016 100644
--- a/core/embed/projects/prodtest/README.md
+++ b/core/embed/projects/prodtest/README.md
@@ -656,6 +656,16 @@ secrets-init
OK
```
+### secrets-lock
+Locks the secret sector.
+
+Example:
+```
+secrets-lock
+Lock successful
+OK
+```
+
### optiga-pair
Writes the pairing secret to the Optiga chip to pair it with the MCU. The command `secrets-init` must be executed before calling this command.
diff --git a/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c b/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c
index f491bb7af..065c149d2 100644
--- a/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c
+++ b/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c
@@ -22,11 +22,12 @@
#include <trezor_rtl.h>
#include <rtl/cli.h>
+#include <rtl/mini_printf.h>
+#include <sec/secret.h>
#include <util/flash_otp.h>
#include <stdlib.h>
#include "prodtest_optiga.h"
-#include "rtl/mini_printf.h"
static void prodtest_otp_variant_read(cli_t* cli) {
if (cli_arg_count(cli) > 0) {
@@ -116,6 +117,13 @@ static void prodtest_otp_variant_write(cli_t* cli) {
cli_trace(cli, "");
}
+#ifdef SECRET_LOCK_SLOT_OFFSET
+ if (sectrue != secret_is_locked()) {
+ cli_error(cli, CLI_ERROR, "Secrets not locked");
+ return;
+ }
+#endif
+
#ifdef USE_OPTIGA
optiga_locked_status optiga_status = get_optiga_locked_status(cli);
diff --git a/core/embed/projects/prodtest/cmd/prodtest_secrets.c b/core/embed/projects/prodtest/cmd/prodtest_secrets.c
index cd8c329ca..0dc4fe1ac 100644
--- a/core/embed/projects/prodtest/cmd/prodtest_secrets.c
+++ b/core/embed/projects/prodtest/cmd/prodtest_secrets.c
@@ -128,6 +128,29 @@ static void prodtest_secrets_init(cli_t* cli) {
cli_ok(cli, "");
}
+#ifdef SECRET_LOCK_SLOT_OFFSET
+static void prodtest_secrets_lock(cli_t* cli) {
+ if (cli_arg_count(cli) > 0) {
+ cli_error_arg_count(cli);
+ return;
+ }
+
+ if (sectrue == secret_is_locked()) {
+ cli_trace(cli, "Already locked");
+ cli_ok(cli, "");
+ return;
+ }
+
+ if (sectrue != secret_lock()) {
+ cli_error(cli, CLI_ERROR, "Failed to lock secret sector");
+ return;
+ }
+
+ cli_trace(cli, "Lock successful");
+ cli_ok(cli, "");
+}
+#endif
+
// clang-format off
PRODTEST_CLI_CMD(
@@ -136,3 +159,12 @@ PRODTEST_CLI_CMD(
.info = "Generate and write secrets to flash",
.args = ""
);
+
+#ifdef SECRET_LOCK_SLOT_OFFSET
+PRODTEST_CLI_CMD(
+ .name = "secrets-lock",
+ .func = prodtest_secrets_lock,
+ .info = "Locks the secret sector",
+ .args = ""
+);
+#endif
diff --git a/core/embed/rtl/inc/rtl/secbool.h b/core/embed/rtl/inc/rtl/secbool.h
index db4e1a8c7..1134255fe 100644
--- a/core/embed/rtl/inc/rtl/secbool.h
+++ b/core/embed/rtl/inc/rtl/secbool.h
@@ -39,6 +39,13 @@ static inline secbool secbool_and(secbool a, secbool b) {
return secfalse;
}
+static inline secbool secbool_not(secbool a) {
+ if (sectrue == a) {
+ return secfalse;
+ }
+ return sectrue;
+}
+
#ifndef __wur
#define __wur __attribute__((warn_unused_result))
#endif
diff --git a/core/embed/sec/monoctr/stm32u5/monoctr.c b/core/embed/sec/monoctr/stm32u5/monoctr.c
index af87f1eca..1f78d1896 100644
--- a/core/embed/sec/monoctr/stm32u5/monoctr.c
+++ b/core/embed/sec/monoctr/stm32u5/monoctr.c
@@ -75,7 +75,9 @@ secbool monoctr_write(monoctr_type_t type, uint8_t value) {
for (int i = 0; i < value; i++) {
uint32_t data[4] = {0};
- secret_write((uint8_t *)data, offset + i * 16, 16);
+ if (sectrue != secret_write((uint8_t *)data, offset + i * 16, 16)) {
+ return secfalse;
+ }
}
return sectrue;
diff --git a/core/embed/sec/secret/inc/sec/secret.h b/core/embed/sec/secret/inc/sec/secret.h
index 101413899..4823fd769 100644
--- a/core/embed/sec/secret/inc/sec/secret.h
+++ b/core/embed/sec/secret/inc/sec/secret.h
@@ -29,8 +29,10 @@
* @param data Pointer to the data to write.
* @param offset Offset in the storage to begin writing.
* @param len Number of bytes to write.
+ *
+ * @return secbool sectrue on successful write, secfalse otherwise.
*/
-void secret_write(const uint8_t* data, uint32_t offset, uint32_t len);
+secbool secret_write(const uint8_t* data, uint32_t offset, uint32_t len);
/**
* @brief Reads data from the secret storage.
@@ -114,6 +116,21 @@ void secret_init(void);
*/
void secret_safety_erase(void);
+/**
+ * Locks the secret sector, making it unavailable for further writing from
+ * prodtest.
+ *
+ * @return sectrue if the lock was successful
+ */
+secbool secret_lock(void);
+
+/**
+ * Checks if the secret storage is locked.
+ *
+ * @return sectrue if locked.
+ */
+secbool secret_is_locked(void);
+
#ifdef LOCKABLE_BOOTLOADER
/**
diff --git a/core/embed/sec/secret/stm32f4/secret.c b/core/embed/sec/secret/stm32f4/secret.c
index 7f9b539b4..891a4ed36 100644
--- a/core/embed/sec/secret/stm32f4/secret.c
+++ b/core/embed/sec/secret/stm32f4/secret.c
@@ -84,18 +84,24 @@ void secret_unlock_bootloader(void) { secret_erase(); }
void secret_write_header(void) {
uint8_t header[SECRET_HEADER_LEN] = {0};
memcpy(header, SECRET_HEADER_MAGIC, SECRET_HEADER_MAGIC_LEN);
- secret_write(header, SECRET_HEADER_OFFSET, SECRET_HEADER_LEN);
+ ensure(secret_write(header, SECRET_HEADER_OFFSET, SECRET_HEADER_LEN),
+ "secret write header failed");
}
-void secret_write(const uint8_t* data, uint32_t offset, uint32_t len) {
+secbool secret_write(const uint8_t* data, uint32_t offset, uint32_t len) {
mpu_mode_t mpu_mode = mpu_reconfig(MPU_MODE_SECRET);
ensure(flash_unlock_write(), "secret write");
for (int i = 0; i < len; i++) {
- ensure(flash_area_write_byte(&SECRET_AREA, offset + i, data[i]),
- "secret write");
+ if (sectrue != flash_area_write_byte(&SECRET_AREA, offset + i, data[i])) {
+ ensure(flash_lock_write(), "secret write");
+ mpu_restore(mpu_mode);
+ return secfalse;
+ }
}
ensure(flash_lock_write(), "secret write");
mpu_restore(mpu_mode);
+
+ return sectrue;
}
secbool secret_read(uint8_t* data, uint32_t offset, uint32_t len) {
@@ -152,8 +158,7 @@ secbool secret_key_set(uint8_t slot, const uint8_t* key, size_t len) {
secret_erase();
secret_write_header();
- secret_write(key, offset, len);
- return sectrue;
+ return secret_write(key, offset, len);
}
secbool secret_key_get(uint8_t slot, uint8_t* dest, size_t len) {
diff --git a/core/embed/sec/secret/stm32u5/secret.c b/core/embed/sec/secret/stm32u5/secret.c
index 89691c2ec..bd4468d38 100644
--- a/core/embed/sec/secret/stm32u5/secret.c
+++ b/core/embed/sec/secret/stm32u5/secret.c
@@ -127,7 +127,8 @@ static void secret_erase(void) {
static void secret_write_header(void) {
uint8_t header[SECRET_HEADER_LEN] = {0};
memcpy(header, SECRET_HEADER_MAGIC, SECRET_HEADER_MAGIC_LEN);
- secret_write(header, SECRET_HEADER_OFFSET, SECRET_HEADER_LEN);
+ ensure(secret_write(header, SECRET_HEADER_OFFSET, SECRET_HEADER_LEN),
+ "secret write header failed");
}
static secbool secret_ensure_initialized(void) {
@@ -139,16 +140,20 @@ static secbool secret_ensure_initialized(void) {
return sectrue;
}
-void secret_write(const uint8_t *data, uint32_t offset, uint32_t len) {
+secbool secret_write(const uint8_t *data, uint32_t offset, uint32_t len) {
mpu_mode_t mpu_mode = mpu_reconfig(MPU_MODE_SECRET);
ensure(flash_unlock_write(), "secret write");
for (int i = 0; i < len / 16; i++) {
- ensure(flash_area_write_quadword(&SECRET_AREA, offset + (i * 16),
- (uint32_t *)&data[(i * 16)]),
- "secret write");
+ if (sectrue != flash_area_write_quadword(&SECRET_AREA, offset + (i * 16),
+ (uint32_t *)&data[(i * 16)])) {
+ ensure(flash_lock_write(), "secret write");
+ mpu_restore(mpu_mode);
+ return secfalse;
+ }
}
ensure(flash_lock_write(), "secret write");
mpu_restore(mpu_mode);
+ return sectrue;
}
secbool secret_read(uint8_t *data, uint32_t offset, uint32_t len) {
@@ -327,7 +332,10 @@ secbool secret_key_set(uint8_t slot, const uint8_t *key, size_t len) {
secure_aes_ecb_encrypt_hw(key, len, secret_enc, SECURE_AES_KEY_DHUK_SP)) {
return secfalse;
}
- secret_write(secret_enc, offset, len);
+ if (sectrue != secret_write(secret_enc, offset, len)) {
+ memzero(secret_enc, sizeof(secret_enc));
+ return secfalse;
+ }
memzero(secret_enc, sizeof(secret_enc));
secret_key_cache(slot);
return sectrue;
@@ -385,7 +393,7 @@ static void secret_key_erase(uint8_t slot) {
uint32_t offset = secret_get_slot_offset(slot);
uint32_t slot_len = secret_get_slot_len(slot);
- secret_write(value, offset, slot_len);
+ ensure(secret_write(value, offset, slot_len), "secret erase failed");
}
// Provision the secret BHK from the secret storage to the BHK register
@@ -499,7 +507,7 @@ static secbool secret_keys_present_any(void) {
#endif
// return sectrue if at least one key slot is writable
-static secbool secret_keys_writable(void) {
+__attribute__((unused)) static secbool secret_keys_writable(void) {
secbool result = secfalse;
for (uint8_t i = 0; i < SECRET_NUM_KEY_SLOTS; i++) {
@@ -542,6 +550,45 @@ void secret_unlock_bootloader(void) {
#endif
+#ifdef SECRET_LOCK_SLOT_OFFSET
+
+secbool secret_lock(void) {
+ uint8_t lock_data[SECRET_LOCK_SLOT_LEN] = {0};
+ return secret_write(lock_data, SECRET_LOCK_SLOT_OFFSET, sizeof(lock_data));
+}
+
+secbool secret_is_locked(void) {
+ uint8_t *header_data =
+ (uint8_t *)flash_area_get_address(&SECRET_AREA, 0, SECRET_HEADER_LEN);
+
+ mpu_mode_t mpu_mode = mpu_reconfig(MPU_MODE_SECRET);
+ uint16_t zero_count = 0;
+ for (int i = 0; i < SECRET_HEADER_LEN; i++) {
+ // 0 is returned when the secret sector is inaccessible
+ if (header_data[i] == 0) {
+ zero_count++;
+ }
+ }
+ mpu_restore(mpu_mode);
+
+ if (zero_count == SECRET_HEADER_LEN) {
+ return sectrue;
+ }
+
+ uint8_t lock_data[SECRET_LOCK_SLOT_LEN] = {0};
+ secret_read(lock_data, SECRET_LOCK_SLOT_OFFSET, SECRET_LOCK_SLOT_LEN);
+
+ for (int i = 0; i < SECRET_LOCK_SLOT_LEN; i++) {
+ // 0xFF being the default value of the flash memory (before any write)
+ if (lock_data[i] != 0xFF) {
+ return sectrue;
+ }
+ }
+
+ return secfalse;
+}
+#endif
+
void secret_prepare_fw(secbool allow_run_with_secret,
secbool allow_provisioning_access) {
/**
@@ -567,11 +614,20 @@ void secret_prepare_fw(secbool allow_run_with_secret,
secret_bhk_lock();
secret_keys_uncache();
secbool secret_present = secret_keys_present();
+
+#ifdef SECRET_LOCK_SLOT_OFFSET
+ secbool secret_locked = secret_is_locked();
+#else
+ // Without the lock record, we determine the lock status by the presence of
+ // keys. When none of the keys is writable, or all keys are present, it means
+ // the sector is locked.
secbool secret_writable = secret_keys_writable();
- if (sectrue == allow_provisioning_access && sectrue == secret_writable &&
- secfalse == secret_present) {
- // Secret keys are not present and they are writable.
- // This means the U5 chip is unprovisioned.
+ secbool secret_locked =
+ secbool_or(secbool_not(secret_writable), secret_present);
+#endif
+
+ if (sectrue == allow_provisioning_access && secfalse == secret_locked) {
+ // U5 chip is unprovisioned.
// Allow trusted firmware (prodtest presumably) to access the secret sector,
// early return here.
secret_keys_cache();
diff --git a/core/embed/sec/secret/unix/secret.c b/core/embed/sec/secret/unix/secret.c
index 1df7135c4..c902d6967 100644
--- a/core/embed/sec/secret/unix/secret.c
+++ b/core/embed/sec/secret/unix/secret.c
@@ -44,6 +44,10 @@ static uint8_t secret_key_slot1[SECRET_KEY_SLOT_1_LEN] = {0};
static uint8_t secret_key_slot2[SECRET_KEY_SLOT_2_LEN] = {0};
#endif
+#ifdef SECRET_LOCK_SLOT_OFFSET
+static secbool secret_sector_locked = secfalse;
+#endif
+
size_t secret_get_slot_len(uint8_t slot) {
switch (slot) {
#ifdef SECRET_KEY_SLOT_0_LEN
@@ -175,4 +179,15 @@ void secret_prepare_fw(secbool allow_run_with_secret,
void secret_init(void) {}
+#ifdef SECRET_LOCK_SLOT_OFFSET
+
+secbool secret_is_locked(void) { return secret_sector_locked; }
+
+secbool secret_lock(void) {
+ secret_sector_locked = sectrue;
+ return sectrue;
+}
+
+#endif
+
#endif // KERNEL_MODE
Why this scored 43/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.