feat(core/prodtest): Require Tropic to be locked before otp-variant-write.
What changed, and why it matters
This commit tightens a factory/production testing tool in Trezor firmware so that it refuses to permanently write a device variant code into one-time programmable memory unless the Tropic secure chip is already locked. Previously, the variant could be written before the chip was locked, which could let a misconfigured or tampered device pass through production with an incorrect or attacker-chosen variant setting. The change also improves the lock-check command so it can return an explicit error state instead of silently answering 'NO'.
Treat as a hardening/improvement commit in production tooling. Review whether the stale comment referencing get_optiga_locked_status() should be corrected. Verify that the new TROPIC_LOCKED_ERROR paths in prodtest_otp_variant_write() halt execution as intended and do not leave the CLI in an inconsistent state.
Security signals we found
Enforces a security precondition (Tropic locked) before an irreversible OTP write
Refactors lock-check logic into a reusable API with explicit error state
Prevents production tooling from writing device variant before secure element is paired/locked
Comment typo suggests the change may have been copied from an Optiga-based implementation
Evidence from the diff
The patch modifies the prodtest (production test) CLI commands for Tropic secure-element handling. It extracts the existing lock-detection logic from prodtest_tropic_lock_check() into a new reusable helper get_tropic_locked_status() that returns TROPIC_LOCKED_TRUE, TROPIC_LOCKED_FALSE, or TROPIC_LOCKED_ERROR. prodtest_otp_variant_write() now calls this helper when USE_TROPIC is defined and aborts the OTP variant write if the Tropic chip is not locked. Several error paths that previously reported ‘NO’ now report TROPIC_LOCKED_FALSE, while true failure paths report TROPIC_LOCKED_ERROR. The comment in the OTP variant file still incorrectly refers to get_optiga_locked_status() instead of get_tropic_locked_status().
Changed components
core/embed/projects/prodtest/cmd/prodtest_otp_variant.ccore/embed/projects/prodtest/cmd/prodtest_tropic.ccore/embed/projects/prodtest/cmd/prodtest_tropic.hInspect captured patch +52 / −5
diff --git a/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c b/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c
index 7ff14c40c..c52149be5 100644
--- a/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c
+++ b/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c
@@ -142,6 +142,20 @@ static void prodtest_otp_variant_write(cli_t* cli) {
}
#endif
+#ifdef USE_TROPIC
+ tropic_locked_status tropic_status = get_tropic_locked_status(cli);
+
+ if (tropic_status == TROPIC_LOCKED_FALSE) {
+ cli_error(cli, CLI_ERROR, "Tropic not locked");
+ return;
+ }
+
+ if (tropic_status != TROPIC_LOCKED_TRUE) {
+ // Error reported by get_optiga_locked_status().
+ return;
+ }
+#endif
+
if (sectrue == flash_otp_is_locked(FLASH_OTP_BLOCK_DEVICE_VARIANT)) {
cli_error(cli, CLI_ERROR_LOCKED,
"OTP block is locked and cannot be written again.");
diff --git a/core/embed/projects/prodtest/cmd/prodtest_tropic.c b/core/embed/projects/prodtest/cmd/prodtest_tropic.c
index 7155a5db6..52ead8aa1 100644
--- a/core/embed/projects/prodtest/cmd/prodtest_tropic.c
+++ b/core/embed/projects/prodtest/cmd/prodtest_tropic.c
@@ -18,6 +18,7 @@
*/
#ifdef USE_TROPIC
+#include "prodtest_tropic.h"
#include <trezor_model.h>
#include <trezor_rtl.h>
@@ -636,6 +637,22 @@ static void prodtest_tropic_lock_check(cli_t* cli) {
return;
}
+ tropic_locked_status status = get_tropic_locked_status(cli);
+ switch (status) {
+ case TROPIC_LOCKED_TRUE:
+ cli_ok(cli, "YES");
+ break;
+ case TROPIC_LOCKED_FALSE:
+ cli_ok(cli, "NO");
+ break;
+ default:
+ // Error reported by get_tropic_locked_status.
+ break;
+ }
+}
+
+tropic_locked_status get_tropic_locked_status(cli_t* cli) {
+ tropic_locked_status locked_status = TROPIC_LOCKED_ERROR;
tropic_handshake_state = TROPIC_HANDSHAKE_STATE_0;
lt_handle_t* tropic_handle = tropic_get_handle();
@@ -644,7 +661,7 @@ static void prodtest_tropic_lock_check(cli_t* cli) {
curve25519_key tropic_public = {0};
if (secret_key_tropic_public(tropic_public) != sectrue) {
// The Tropic pairing process was not initiated.
- cli_ok(cli, "NO");
+ locked_status = TROPIC_LOCKED_FALSE;
goto cleanup;
}
@@ -652,6 +669,7 @@ static void prodtest_tropic_lock_check(cli_t* cli) {
if (secret_key_tropic_pairing_privileged(privileged_private) != sectrue) {
cli_error(cli, CLI_ERROR,
"`secret_key_tropic_pairing_privileged()` failed.");
+ locked_status = TROPIC_LOCKED_ERROR;
goto cleanup;
}
curve25519_key privileged_public = {0};
@@ -662,7 +680,7 @@ static void prodtest_tropic_lock_check(cli_t* cli) {
privileged_public);
if (ret != LT_OK) {
// The Tropic pairing process was initiated but probably failed midway.
- cli_ok(cli, "NO");
+ locked_status = TROPIC_LOCKED_FALSE;
goto cleanup;
}
@@ -672,12 +690,13 @@ static void prodtest_tropic_lock_check(cli_t* cli) {
if (ret != LT_OK) {
cli_error(cli, CLI_ERROR, "`lt_read_whole_R_config()` failed with error %d",
ret);
+ locked_status = TROPIC_LOCKED_ERROR;
goto cleanup;
}
if (memcmp(&reversible_configuration, (uint8_t*)&configuration_read,
sizeof(reversible_configuration)) != 0) {
- cli_ok(cli, "NO");
+ locked_status = TROPIC_LOCKED_FALSE;
goto cleanup;
}
@@ -685,19 +704,21 @@ static void prodtest_tropic_lock_check(cli_t* cli) {
if (ret != LT_OK) {
cli_error(cli, CLI_ERROR, "`lt_read_whole_I_config()` failed with error %d",
ret);
+ locked_status = TROPIC_LOCKED_ERROR;
goto cleanup;
}
if (memcmp(&irreversible_configuration, (uint8_t*)&configuration_read,
sizeof(irreversible_configuration)) != 0) {
- cli_ok(cli, "NO");
+ locked_status = TROPIC_LOCKED_FALSE;
goto cleanup;
}
- cli_ok(cli, "YES");
+ locked_status = TROPIC_LOCKED_TRUE;
cleanup:
memzero(privileged_private, sizeof(privileged_private));
+ return locked_status;
}
static lt_ret_t pairing_key_write(lt_handle_t* handle, pkey_index_t slot,
diff --git a/core/embed/projects/prodtest/cmd/prodtest_tropic.h b/core/embed/projects/prodtest/cmd/prodtest_tropic.h
index eaf1cb78f..f465da5fd 100644
--- a/core/embed/projects/prodtest/cmd/prodtest_tropic.h
+++ b/core/embed/projects/prodtest/cmd/prodtest_tropic.h
@@ -19,4 +19,16 @@
#pragma once
+#include <rtl/cli.h>
+
+#include <libtropic.h>
+
+typedef enum {
+ TROPIC_LOCKED_TRUE,
+ TROPIC_LOCKED_FALSE,
+ TROPIC_LOCKED_ERROR,
+} tropic_locked_status;
+
+tropic_locked_status get_tropic_locked_status(cli_t* cli);
+
bool prodtest_tropic_factory_session_start(lt_handle_t* tropic_handle);
Why this scored 42/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.