What changed, and why it matters
This commit adds the ability for the Trezor T3W1 hardware wallet to automatically update the firmware of its Tropic secure chip during normal use. The update is triggered after the user unlocks the device with their PIN, which is intended to prevent unauthorized changes. The change also adds recovery logic so that if an update is interrupted (for example by a power loss), it can be resumed on the next boot. While the feature is security-sensitive, the commit itself is a production enablement patch with safeguards, not an obvious vulnerability.
Treat this as a security-critical feature change requiring focused review. Verify that: (1) the Maintenance bit cannot be enabled before PIN verification in any boot path, (2) `tropic_ensure_fw_updated` cannot be invoked by unprivileged code to downgrade or reflash arbitrarily, (3) the version slot write is atomic with respect to power loss, and (4) the bundled firmware images are authenticated before flashing. Run the new `test_tropic_fw_update.py` upgrade scenarios and prodtest tests, and audit the SMC/Syscall verifiers for missing access-control checks.
Security signals we found
New privileged firmware update path added to production boot flow
Maintenance bit enablement gated by PIN unlock (`storage_is_unlocked`)
Update state machine distinguishes UNFINISHED/OUTDATED/UP_TO_DATE/ERROR
Recovery path for interrupted updates at boot (`tropic_check_and_restore_fw_update_in_progress`)
Silicon revision checks before flashing (`tropic_silicon_revision_matches`)
R-memory slot 8 now stores expected firmware versions for anti-rollback/downgrade checks
Prodtest `tropic-lock-check` reports NO when recorded firmware versions are missing or outdated
SMC/Syscall verifiers added but are thin wrappers without additional argument validation
Evidence from the diff
The patch enables Tropic firmware updates in production firmware for the T3W1 model. It introduces tropic_ensure_fw_updated() and tropic_check_and_restore_fw_update_in_progress() in core/embed/sec/tropic/tropic.c, exposes them through SMC/Syscall dispatchers, and calls them from core/src/boot.py after PIN unlock. The update flow checks the chip mode, enables the Maintenance bit only when allowed by I-config, flashes bundled RISC-V and SPECT firmware images, restores configuration, and records the new firmware version in R-memory slot 8. Prodtest tooling is updated to record and verify the same slot. Extensive upgrade and prodtest scenarios are added.
Changed components
core/embed/sec/tropic/tropic.ccore/embed/sec/tropic/inc/sec/tropic.hcore/src/boot.pycore/embed/sys/smcall/stm32/smcall_dispatch.ccore/embed/sys/smcall/stm32/smcall_numbers.hcore/embed/sys/smcall/stm32/smcall_stubs.ccore/embed/sys/smcall/stm32/smcall_verifiers.ccore/embed/sys/syscall/stm32/syscall_dispatch.ccore/embed/sys/syscall/stm32/syscall_numbers.hcore/embed/sys/syscall/stm32/syscall_stubs.ccore/embed/sys/syscall/stm32/syscall_verifiers.ccore/embed/upymod/modtrezorcrypto/modtrezorcrypto-tropic.hcore/embed/projects/prodtest/cmd/prodtest_tropic.ccore/embed/projects/firmware/src/unix/main_main.ccore/embed/projects/secmon/main.cInspect captured patch +1533 / −233
### .github/workflows/bot-tropic-emulator.yml
@@ -5,7 +5,7 @@ on:
types: [opened, closed, synchronize]
branches: [main]
paths:
- - tests/tropic_model/config.yml
+ - tests/tropic_model/firmware_config/current.yml
- vendor/ts-tvl
permissions:
@@ -17,13 +17,13 @@ concurrency:
env:
PULL_COMMENT: |
- ⚠️ Looks like you modified `tests/tropic_model/config.yml` or updated the `ts-tvl` submodule. Please make a numbered copy and make sure recent upgrade tests use it and pass.
+ ⚠️ Looks like you modified `tests/tropic_model/firmware_config/current.yml` or updated the `ts-tvl` submodule. Please check whether any firmware has been released since the previous change to `current.yml`. If so, copy the previous content of `current.yml` to `tests/tropic_model/firmware_config/X_Y_Z.yml`, where X.Y.Z is the latest release made before this change (the copy covers all releases since the previous change), and make sure upgrade tests pass.
<!-- -->
After the PR is merged, an issue will be created in [trezor-user-env](https://github.com/trezor/trezor-user-env) to notify maintainers of the change.
ISSUE_REPOSITORY: trezor-user-env
ISSUE_TITLE: Tropic emulator config/version changed in trezor-firmware
ISSUE_BODY: |
- A change to `tests/tropic_model/config.yml` or `vendor/ts-tvl` has been merged to main branch in trezor-firmware: ${{ github.event.pull_request.html_url }}
+ A change to `tests/tropic_model/firmware_config/current.yml` or `vendor/ts-tvl` has been merged to main branch in trezor-firmware: ${{ github.event.pull_request.html_url }}
<!-- -->
Please make sure nightly emulators use this new configuration. Feel free to contact the PR author or **#tech_firmware** for help.
ISSUE_BODY_PATH: ${{ github.workspace }}/tropic-emulator-issue.md
### .github/workflows/core.yml
@@ -290,7 +290,7 @@ jobs:
- run: nix-shell --run "uv run make -C core test_emu_sanity" # sanity check non-frozen emulator
- name: Start Tropic model
if: ${{ matrix.model == 'T3W1' }}
- run: nix-shell --run "uv run model_server tcp -c tests/tropic_model/config.yml > tests/trezor-tropic-model.log 2>&1 &"
+ run: nix-shell --run "uv run model_server tcp -c tests/tropic_model/firmware_config/current.yml > tests/trezor-tropic-model.log 2>&1 &"
- run: nix-shell --run "uv run make -C core test"
core_unit_rust_test:
### core/.changelog.d/7929.added
@@ -0,0 +1 @@
+[T3W1] Enable Tropic firmware update.
### core/embed/projects/firmware/src/unix/main_main.c
@@ -106,6 +106,8 @@ static void drivers_init(void) {
ensure_true(tropic_init(NULL) == LT_OK,
"Failed to initialize Tropic driver. Make sure the "
"`model_server` is running.");
+ ensure(tropic_check_and_restore_fw_update_in_progress(),
+ "Failed to restore FW update in progress");
ensure(tropic_ensure_configuration(), "Tropic configuration check failed");
#endif
### core/embed/projects/prodtest/.changelog.d/7929.changed
@@ -0,0 +1 @@
+Make `tropic-update-fw` record the FW versions in R-memory slot 8 and `tropic-lock-check` report `NO` while they are missing or outdated.
### core/embed/projects/prodtest/cmd/prodtest_tropic.c
@@ -37,8 +37,6 @@
#include "nist256p1.h"
#include "common.h"
-#include "fw_CPU.h"
-#include "fw_SPECT.h"
#include "libtropic.h"
#include "libtropic_l2.h"
@@ -72,9 +70,9 @@ static tropic_handshake_state_t g_tropic_handshake_state =
#define TROPIC_FIRST_UNPRIVILEGED_MCOUNTER 4
// R-memory range used by the writable-slot test. Starts right after the
-// certificate slots (0-5) and Tropic config distribution version slots (6, 7),
-// which must not be overwritten.
-#define TROPIC_RMEM_TEST_FIRST 8
+// certificate slots (0-5), the Tropic config distribution version slots (6, 7)
+// and the FW version slot (8), which must not be overwritten.
+#define TROPIC_RMEM_TEST_FIRST 9
#define TROPIC_RMEM_TEST_LAST TR01_R_MEM_DATA_SLOT_MAX
#define TROPIC_RMEM_TEST_COUNT \
(TROPIC_RMEM_TEST_LAST - TROPIC_RMEM_TEST_FIRST + 1)
@@ -329,6 +327,35 @@ tropic_locked_status get_tropic_locked_status(cli_t* cli,
return TROPIC_LOCKED_ERROR;
}
+ uint8_t read_fw_version[TROPIC_FW_VERSION_SIZE] = {0};
+ uint16_t read_fw_version_length = 0;
+ ret =
+ lt_r_mem_data_read(tropic_handle, TROPIC_FW_VERSION_SLOT, read_fw_version,
+ sizeof(read_fw_version), &read_fw_version_length);
+ if (ret == LT_L3_R_MEM_DATA_READ_SLOT_EMPTY) {
+ cli_trace(cli, "The FW version slot is empty.");
+ return TROPIC_LOCKED_FALSE;
+ } else if (ret != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_LOCK_CHECK_FW_READ,
+ "`lt_r_mem_data_read()` failed with error '%s'",
+ lt_ret_verbose(ret));
+ return TROPIC_LOCKED_ERROR;
+ } else if (read_fw_version_length != sizeof(read_fw_version)) {
+ cli_trace(cli, "The FW version slot has an unexpected length.");
+ return TROPIC_LOCKED_FALSE;
+ } else {
+ cli_trace(cli, "FW versions: RISC-V %d.%d.%d, SPECT %d.%d.%d",
+ read_fw_version[3], read_fw_version[2], read_fw_version[1],
+ read_fw_version[7], read_fw_version[6], read_fw_version[5]);
+ }
+
+ if (tropic_fw_slot_is_outdated(read_fw_version)) {
+ cli_trace(cli,
+ "The FW versions recorded are older than the bundled FW "
+ "versions.");
+ return TROPIC_LOCKED_FALSE;
+ }
+
return TROPIC_LOCKED_TRUE;
}
@@ -1466,6 +1493,8 @@ static void prodtest_tropic_keyfido_read(cli_t* cli) {
#endif // SECRET_KEY_MASKING
}
+static bool privileged_session_start(cli_t* cli);
+
static void prodtest_tropic_update_fw(cli_t* cli) {
if (cli_arg_count(cli) > 0) {
cli_error_arg_count(cli);
@@ -1490,40 +1519,19 @@ static void prodtest_tropic_update_fw(cli_t* cli) {
chip_id.silicon_rev[1], chip_id.silicon_rev[2],
chip_id.silicon_rev[3]);
-#ifdef LT_SILICON_REV_ABAB
- // CHIP_ID v0.0.0.1 has no silicon revision field (libtropic reports it as
- // "N/A") and was used only on ABAB silicon, so the version alone identifies
- // the chip. Newer ABAB chips spell the revision out.
- const bool rev_is_abab = strncmp((char*)chip_id.silicon_rev, "ABAB", 4) == 0;
- const bool chip_id_is_v0001 =
- chip_id.chip_id_ver[0] == 0 && chip_id.chip_id_ver[1] == 0 &&
- chip_id.chip_id_ver[2] == 0 && chip_id.chip_id_ver[3] == 1;
-
- if (!rev_is_abab && !chip_id_is_v0001) {
+ if (!tropic_silicon_revision_matches(&chip_id)) {
cli_error(cli, PRODTEST_ERR_TROPIC_UPDATE_WRONG_REVISION,
"Wrong tropic chip silicon revision");
return;
}
-#elif defined(LT_SILICON_REV_ACAB)
- if (strncmp((char*)chip_id.silicon_rev, "ACAB", 4) != 0) {
- cli_error(cli, PRODTEST_ERR_TROPIC_UPDATE_WRONG_REVISION,
- "Wrong tropic chip silicon revision");
- return;
- }
-#else
- cli_error(cli, PRODTEST_ERR_TROPIC_UPDATE_NO_REVISION,
- "Tropic chip silicon revision not set");
- return;
-#endif // LT_SILICON_REV_ABAB
cli_trace(cli, "Updating RISC-V and SPECT FW");
// We allow the libtropic's log to write to cli during the update, so that we
// can distinguish which step failed.
#ifdef USE_TROPIC_LOGGING
tropic_set_log_sink(cli);
#endif
- lt_ret_t ret = lt_do_mutable_fw_update(h, fw_CPU, sizeof(fw_CPU), fw_SPECT,
- sizeof(fw_SPECT));
+ lt_ret_t ret = tropic_flash_bundled_fw();
#ifdef USE_TROPIC_LOGGING
tropic_set_log_sink(NULL);
#endif
@@ -1563,6 +1571,18 @@ static void prodtest_tropic_update_fw(cli_t* cli) {
// Tropic rebooted during the update, so the driver's session data is stale.
tropic_session_forget();
+ if (!privileged_session_start(cli)) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_UPDATE_FW_SESSION,
+ "Failed to start privileged session");
+ goto cleanup;
+ }
+
+ // record the FW versions in R-memory
+ if (!tropic_write_fw_slot()) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_UPDATE_FW_SLOT,
+ "WARNING: could not record the FW versions in R-mem");
+ goto cleanup;
+ }
cli_ok(cli, "");
### core/embed/projects/prodtest/error_codes.json
@@ -1331,11 +1331,6 @@
"name": "PRODTEST_ERR_TROPIC_UPDATE_WRONG_REVISION",
"module": "tropic"
},
- {
- "code": 20076,
- "name": "PRODTEST_ERR_TROPIC_UPDATE_NO_REVISION",
- "module": "tropic"
- },
{
"code": 20078,
"name": "PRODTEST_ERR_TROPIC_UPDATE_FW",
@@ -1741,6 +1736,21 @@
"name": "PRODTEST_ERR_TROPIC_INIT",
"module": "tropic"
},
+ {
+ "code": 20160,
+ "name": "PRODTEST_ERR_TROPIC_LOCK_CHECK_FW_READ",
+ "module": "tropic"
+ },
+ {
+ "code": 20161,
+ "name": "PRODTEST_ERR_TROPIC_UPDATE_FW_SESSION",
+ "module": "tropic"
+ },
+ {
+ "code": 20162,
+ "name": "PRODTEST_ERR_TROPIC_UPDATE_FW_SLOT",
+ "module": "tropic"
+ },
{
"code": 21010,
"name": "PRODTEST_ERR_UNIT_TEST_FAILED",
### core/embed/projects/prodtest/prodtest_error_codes.h
@@ -337,7 +337,7 @@ typedef enum {
PRODTEST_ERR_TROPIC_KEYFIDO_READ_MASK = 20073,
PRODTEST_ERR_TROPIC_UPDATE_CHIP_ID = 20074,
PRODTEST_ERR_TROPIC_UPDATE_WRONG_REVISION = 20075,
- PRODTEST_ERR_TROPIC_UPDATE_NO_REVISION = 20076,
+ // PRODTEST_ERR_TROPIC_UPDATE_NO_REVISION = 20076, DEPRECATED
// PRODTEST_ERR_TROPIC_UPDATE_REBOOT_MAINTENANCE = 20077, DEPRECATED
PRODTEST_ERR_TROPIC_UPDATE_FW = 20078,
// PRODTEST_ERR_TROPIC_UPDATE_REBOOT_APP = 20079, DEPRECATED
@@ -421,6 +421,9 @@ typedef enum {
PRODTEST_ERR_TROPIC_TESTS_CLEANUP_ECC = 20157,
PRODTEST_ERR_TROPIC_TESTS_CLEANUP_COUNTER = 20158,
PRODTEST_ERR_TROPIC_INIT = 20159,
+ PRODTEST_ERR_TROPIC_LOCK_CHECK_FW_READ = 20160,
+ PRODTEST_ERR_TROPIC_UPDATE_FW_SESSION = 20161,
+ PRODTEST_ERR_TROPIC_UPDATE_FW_SLOT = 20162,
// === unit-test (21000–21999) ===
PRODTEST_ERR_UNIT_TEST_FAILED = 21010,
### core/embed/projects/secmon/main.c
@@ -131,6 +131,8 @@ static void drivers_init(void) {
ensure_true(tropic_init(NULL) == LT_OK, "Failed to initialize Tropic driver");
#if defined(USE_SECRET) && defined(LOCKABLE_BOOTLOADER)
if (secfalse != secret_bootloader_locked()) {
+ ensure(tropic_check_and_restore_fw_update_in_progress(),
+ "Failed to restore FW update in progress");
ensure(tropic_ensure_configuration(), "Tropic configuration check failed");
}
#else
### core/embed/sec/tropic/inc/sec/tropic.h
@@ -46,6 +46,11 @@
#define TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT 6
// Slot reserved for the backup distribution version of Tropic configuration.
#define TROPIC_CONFIG_BACKUP_DISTRIBUTION_VERSION_SLOT 7
+// Slot reserved for the Tropic FW versions holding a RISC-V version followed
+// by a SPECT version.
+#define TROPIC_FW_VERSION_SLOT 8
+// Number of bytes in slot 8
+#define TROPIC_FW_VERSION_SIZE 8
// Pairing key used by prodtest to inject the privileged and unprivileged
// pairing keys.
@@ -69,6 +74,8 @@
#include "libtropic.h"
+secbool tropic_check_and_restore_fw_update_in_progress(void);
+
lt_ret_t tropic_init(cli_t* cli);
void tropic_deinit(void);
@@ -88,6 +95,16 @@ typedef struct {
} tropic_expected_config_t;
#ifdef TREZOR_PRODTEST
+bool tropic_silicon_revision_matches(const lt_chip_id_t* chip_id);
+
+// Flashes the FW images bundled with this build.
+lt_ret_t tropic_flash_bundled_fw(void);
+
+bool tropic_write_fw_slot(void);
+
+bool tropic_fw_slot_is_outdated(
+ const uint8_t fw_version[TROPIC_FW_VERSION_SIZE]);
+
lt_handle_t* tropic_prodtest_init_and_get_handle(cli_t* cli);
lt_ret_t tropic_custom_session_start(cli_t* cli,
@@ -131,6 +148,17 @@ bool tropic_get_expected_tropic_config_from_distribution_version(
#endif // KERNEL_MODE
+typedef enum {
+ TROPIC_FW_UPDATE_UNFINISHED,
+ TROPIC_FW_UPDATE_OUTDATED,
+ TROPIC_FW_UPDATE_UP_TO_DATE,
+ TROPIC_FW_UPDATE_ERROR
+} tropic_fw_update_state_t;
+
+tropic_fw_update_state_t tropic_get_update_state(void);
+
+secbool tropic_ensure_fw_updated(void);
+
secbool tropic_ensure_configuration(void);
typedef secbool (*tropic_ui_progress_t)(void);
### core/embed/sec/tropic/tropic.c
@@ -33,6 +33,14 @@
#include <libtropic/cal/trezor_crypto/libtropic_trezor_crypto.h>
#include <lt_l3_process.h>
+#ifdef SECURE_MODE
+// These define non-static arrays, so this must remain the only translation
+// unit in any binary that includes them.
+// libtropic's `fw_CPU*` symbols hold the RISC-V FW.
+#include "fw_CPU.h"
+#include "fw_SPECT.h"
+#endif
+
#ifdef TREZOR_EMULATOR
#include <arpa/inet.h>
#include <libtropic/hal/posix/tcp/libtropic_port_posix_tcp.h>
@@ -109,6 +117,8 @@ static const uint8_t TROPIC_BATCHES_V1[][LT_MEMBER_SIZE(
// again.
#define TROPIC_RESTART_DELAY_MS 10
+#define TROPIC_FW_UPDATE_MAX_ATTEMPTS 3
+
// clang-format off
// Temporary address table for config objects, ordered to match lt_config_t.obj[].
// Using a local const table instead of `cfg_desc_table` from libtropic because including
@@ -191,9 +201,7 @@ static bool is_retryable(lt_ret_t ret) {
if (!is_retryable(TROPIC_RETRY_COMMAND_res)) { \
break; \
} \
- tropic_deinit(); \
- systick_delay_ms(TROPIC_RESTART_DELAY_MS); \
- if (tropic_init(NULL) != LT_OK) { \
+ if (!tropic_restart()) { \
break; \
} \
if (TROPIC_RETRY_COMMAND_session_started) { \
@@ -314,6 +322,19 @@ void tropic_session_forget(void) {
drv->session_started = false;
}
+static bool tropic_restart(void) {
+#ifdef TREZOR_EMULATOR
+ lt_ret_t ret = lt_reboot(&g_tropic_driver.handle, TR01_REBOOT);
+ tropic_session_forget();
+ return ret == LT_OK;
+
+#else
+ tropic_deinit();
+ systick_delay_ms(TROPIC_RESTART_DELAY_MS);
+ return tropic_init(NULL) == LT_OK;
+#endif
+}
+
// If `TREZOR_PRODTEST` is not defined, the `cli` argument is ignored.
lt_ret_t tropic_custom_session_start(cli_t *cli,
lt_pkey_index_t pairing_key_index) {
@@ -442,14 +463,6 @@ lt_ret_t lt_ecc_key_erase_retry(lt_handle_t *tropic_handle,
return TROPIC_RETRY_COMMAND(lt_ecc_key_erase(tropic_handle, ecc_slot));
}
-static lt_ret_t lt_r_mem_data_write_retry(lt_handle_t *tropic_handle,
- const uint16_t udata_slot,
- const uint8_t *data,
- const uint16_t size) {
- return TROPIC_RETRY_COMMAND(
- lt_r_mem_data_write(tropic_handle, udata_slot, data, size));
-}
-
lt_ret_t lt_r_mem_data_erase_retry(lt_handle_t *tropic_handle,
const uint16_t udata_slot) {
return TROPIC_RETRY_COMMAND(lt_r_mem_data_erase(tropic_handle, udata_slot));
@@ -885,10 +898,10 @@ static secbool set_expected_config(
uint8_t distribution_version_bytes[sizeof(uint32_t)] = {0};
write_be(distribution_version_bytes, expected_config->distribution_version);
- if (lt_r_mem_data_write_retry(&g_tropic_driver.handle,
- TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT,
- distribution_version_bytes,
- sizeof(distribution_version_bytes)) != LT_OK) {
+ if (lt_r_mem_data_erase_write_retry(
+ &g_tropic_driver.handle, TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT,
+ distribution_version_bytes,
+ sizeof(distribution_version_bytes)) != LT_OK) {
return secfalse;
}
@@ -910,9 +923,7 @@ static secbool set_expected_config(
}
// restart Tropic so the new config takes effect.
- tropic_deinit();
- systick_delay_ms(TROPIC_RESTART_DELAY_MS);
- if (tropic_init(NULL) != LT_OK) {
+ if (!tropic_restart()) {
return secfalse;
}
@@ -993,6 +1004,369 @@ secbool tropic_ensure_configuration(void) {
return sectrue;
}
+bool tropic_silicon_revision_matches(const lt_chip_id_t *chip_id) {
+#ifdef LT_SILICON_REV_ABAB
+ // CHIP_ID v0.0.0.1 has no silicon revision field (libtropic reports it as
+ // "N/A") and was used only on ABAB silicon, so the version alone identifies
+ // the chip. Newer ABAB chips spell the revision out.
+ const bool chip_id_is_v0001 =
+ chip_id->chip_id_ver[0] == 0 && chip_id->chip_id_ver[1] == 0 &&
+ chip_id->chip_id_ver[2] == 0 && chip_id->chip_id_ver[3] == 1;
+
+ return memcmp(chip_id->silicon_rev, "ABAB", 4) == 0 || chip_id_is_v0001;
+#elif defined(LT_SILICON_REV_ACAB)
+ return memcmp(chip_id->silicon_rev, "ACAB", 4) == 0;
+#else
+#error "Tropic silicon revision not set"
+#endif // LT_SILICON_REV_ABAB
+}
+
+lt_ret_t tropic_flash_bundled_fw(void) {
+#ifdef TREZOR_EMULATOR
+ // The model implements no FW-update L2 commands.
+ return LT_OK;
+#else
+ return lt_do_mutable_fw_update(&g_tropic_driver.handle, fw_CPU,
+ sizeof(fw_CPU), fw_SPECT, sizeof(fw_SPECT));
+#endif
+}
+
+// Version arrays are {reserved, patch, minor, major}; byte 0 is not compared.
+static bool fw_version_is_older(const uint8_t *have, const uint8_t *want) {
+ for (int i = 3; i >= 1; i--) {
+ if (have[i] != want[i]) {
+ return have[i] < want[i];
+ }
+ }
+ return false;
+}
+
+static bool tropic_read_fw_slot(uint8_t riscv[4], uint8_t spect[4],
+ bool *present) {
+ uint8_t buf[TROPIC_FW_VERSION_SIZE] = {0};
+ uint16_t length = 0;
+ lt_ret_t ret =
+ lt_r_mem_data_read_retry(&g_tropic_driver.handle, TROPIC_FW_VERSION_SLOT,
+ buf, sizeof(buf), &length);
+ if (ret == LT_L3_R_MEM_DATA_READ_SLOT_EMPTY) {
+ *present = false;
+ return true;
+ }
+ if (ret != LT_OK) {
+ return false;
+ }
+ if (length != sizeof(buf)) {
+ return false;
+ }
+
+ memcpy(riscv, buf, 4);
+ memcpy(spect, buf + 4, 4);
+ *present = true;
+ return true;
+}
+
+// Records that the bundled versions are written. Call only after a complete
+// update.
+bool tropic_write_fw_slot(void) {
+ if (!tropic_session_start()) {
+ return false;
+ }
+ uint8_t buf[TROPIC_FW_VERSION_SIZE] = {0};
+ memcpy(buf, fw_CPU_ver, 4);
+ memcpy(buf + 4, fw_SPECT_ver, 4);
+ if (lt_r_mem_data_erase_write_retry(&g_tropic_driver.handle,
+ TROPIC_FW_VERSION_SLOT, buf,
+ sizeof(buf)) != LT_OK) {
+ return false;
+ }
+
+ uint8_t riscv[4] = {0};
+ uint8_t spect[4] = {0};
+ bool present = false;
+ return tropic_read_fw_slot(riscv, spect, &present) && present &&
+ memcmp(riscv, fw_CPU_ver, 4) == 0 &&
+ memcmp(spect, fw_SPECT_ver, 4) == 0;
+}
+
+static bool tropic_erase_fw_slot(void) {
+ return lt_r_mem_data_erase_retry(&g_tropic_driver.handle,
+ TROPIC_FW_VERSION_SLOT) == LT_OK;
+}
+
+// Reset the configuration and restore the CFG version slots
+static bool tropic_cleanup_update_config(void) {
+ if (!tropic_session_start()) {
+ return false;
+ }
+
+ tropic_expected_config_t expected_config = {0};
+ if (!get_expected_tropic_config(&expected_config)) {
+ return false;
+ }
+
+ optional_u32_t backup_distribution_version = {0};
+ if (!tropic_get_distribution_version(
+ TROPIC_CONFIG_BACKUP_DISTRIBUTION_VERSION_SLOT,
+ &backup_distribution_version)) {
+ return false;
+ }
+ if (backup_distribution_version.has_value &&
+ backup_distribution_version.value >
+ expected_config.distribution_version) {
+ if (!tropic_get_expected_tropic_config_from_distribution_version(
+ backup_distribution_version.value, &expected_config)) {
+ return false;
+ }
+ }
+
+ if (set_expected_config(&expected_config, TROPIC_R_CONFIG_WRITE_ALWAYS) !=
+ sectrue) {
+ return false;
+ }
+
+ return true;
+}
+
+static bool tropic_finish_update(void) {
+ tropic_driver_t *drv = &g_tropic_driver;
+ if (!drv->initialized) {
+ return false;
+ }
+
+ lt_ret_t ret = LT_FAIL;
+
+ for (int i = 0; i < TROPIC_FW_UPDATE_MAX_ATTEMPTS; i++) {
+ ret = tropic_flash_bundled_fw();
+ if (ret == LT_OK) {
+ break;
+ }
+
+ // Maintenance forbidden, the FW is untouched.
+ if (ret == LT_L2_RESP_DISABLED) {
+ // Maintenance mode is disabled. We cannot update the FW -> RSOD
+ return false;
+ }
+
+ // We restart the chip and try again.
+ if (!tropic_restart()) {
+ return false;
+ }
+ }
+
+ // The update rebooted the chip. We clear stale session data.
+ tropic_session_forget();
+
+ if (ret != LT_OK) {
+ return false;
+ }
+
+ // Reset the configuration. This includes the Maintenance bit and the
+ // slots
+ if (!tropic_cleanup_update_config()) {
+ return false;
+ }
+
+ // We record the updated version in the R-memory.
+ if (!tropic_write_fw_slot()) {
+ return false;
+ }
+ return true;
+}
+
+// Check if the Maintenance bit is enabled. Enable it if possible.
+static bool tropic_prepare_update_config(void) {
+ lt_handle_t *handle = tropic_get_handle();
+ if (handle == NULL) {
+ return false;
+ }
+
+ if (!tropic_session_start()) {
+ return false;
+ }
+
+ // Read R-Config and check if Maintenance Mode is enabled.
+ // The whole R-Config is read in case we need to modify it in case the bit is
+ // OFF
+ lt_config_t r_config = {0};
+ if (lt_read_whole_R_config_retry(handle, &r_config) != LT_OK) {
+ return false;
+ }
+
+ // Check if Maintenance Mode is enabled in R-Config[CFG_START_UP]
+ if (!(r_config.obj[TR01_CFG_START_UP_IDX] &
+ BOOTLOADER_CO_CFG_START_UP_MAINTENANCE_ENA_MASK)) {
+ // The bit is disabled so we enable it
+
+ optional_u32_t distribution_version = {0};
+ if (!tropic_get_distribution_version(
+ TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT, &distribution_version)) {
+ return false;
+ }
+ if (distribution_version.has_value) {
+ if (!set_backup_distribution_version_to(distribution_version.value)) {
+ return false;
+ }
+ }
+ if (lt_r_mem_data_erase_retry(&g_tropic_driver.handle,
+ TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT) !=
+ LT_OK) {
+ return false;
+ }
+
+ // Flip the MAINTENANCE_ENA bit
+ r_config.obj[TR01_CFG_START_UP_IDX] |=
+ BOOTLOADER_CO_CFG_START_UP_MAINTENANCE_ENA_MASK;
+
+ // Modify the R-config
+ if (lt_erase_and_write_R_config_retry(handle, &r_config) != LT_OK) {
+ return false;
+ }
+
+ // Reboot tropic to apply R-Config changes
+ if (TROPIC_RETRY_COMMAND(lt_reboot(handle, TR01_REBOOT)) != LT_OK) {
+ return false;
+ }
+ // The reboot forgot the session data on the chip. We need to do it as well.
+ tropic_session_forget();
+ }
+ // the bit is ON
+ return true;
+}
+
+tropic_fw_update_state_t tropic_get_update_state(void) {
+ lt_handle_t *handle = &g_tropic_driver.handle;
+ lt_tr01_mode_t tr01_mode = LT_TR01_ALARM;
+ if (TROPIC_RETRY_COMMAND(lt_get_tr01_mode(handle, &tr01_mode)) != LT_OK) {
+ return TROPIC_FW_UPDATE_ERROR;
+ }
+ if (tr01_mode == LT_TR01_MAINTENANCE) {
+ return TROPIC_FW_UPDATE_UNFINISHED;
+ } else if (tr01_mode == LT_TR01_ALARM) {
+ return TROPIC_FW_UPDATE_ERROR;
+ }
+
+ if (!tropic_session_start()) {
+ return TROPIC_FW_UPDATE_ERROR;
+ }
+ uint8_t riscv_fw[4] = {0};
+ uint8_t spect_fw[4] = {0};
+ bool present = false;
+ if (!tropic_read_fw_slot(riscv_fw, spect_fw, &present)) {
+ return TROPIC_FW_UPDATE_ERROR;
+ }
+ if (!present) {
+ uint32_t r_config_cfg_startup = 0;
+ if (TROPIC_RETRY_COMMAND(lt_r_config_read(
+ handle, TR01_CFG_START_UP_ADDR, &r_config_cfg_startup)) != LT_OK) {
+ return TROPIC_FW_UPDATE_ERROR;
+ }
+ // maintenance on and no FW version in slot = update in progress
+ if ((r_config_cfg_startup &
+ BOOTLOADER_CO_CFG_START_UP_MAINTENANCE_ENA_MASK) != 0) {
+ return TROPIC_FW_UPDATE_UNFINISHED;
+ } else {
+ return TROPIC_FW_UPDATE_OUTDATED;
+ }
+ }
+
+ if (fw_version_is_older(riscv_fw, fw_CPU_ver) ||
+ fw_version_is_older(spect_fw, fw_SPECT_ver)) {
+ return TROPIC_FW_UPDATE_OUTDATED;
+ } else {
+ return TROPIC_FW_UPDATE_UP_TO_DATE;
+ }
+}
+
+static bool tropic_update_possible(void) {
+ lt_handle_t *handle = tropic_get_handle();
+ if (handle == NULL) {
+ return false;
+ }
+
+ lt_chip_id_t chip_id = {0};
+ if (TROPIC_RETRY_COMMAND(lt_get_info_chip_id(handle, &chip_id)) != LT_OK) {
+ return false;
+ }
+ if (!tropic_silicon_revision_matches(&chip_id)) {
+ return false;
+ }
+
+ lt_tr01_mode_t tr01_mode = LT_TR01_ALARM;
+ if (TROPIC_RETRY_COMMAND(lt_get_tr01_mode(handle, &tr01_mode)) != LT_OK) {
+ return false;
+ }
+ if (tr01_mode == LT_TR01_MAINTENANCE) {
+ return true;
+ }
+
+ if (!tropic_session_start()) {
+ return false;
+ }
+ uint32_t i_config_cfg_startup = 0;
+ if (TROPIC_RETRY_COMMAND(lt_i_config_read(handle, TR01_CFG_START_UP_ADDR,
+ &i_config_cfg_startup)) != LT_OK) {
+ return false;
+ }
+ return (i_config_cfg_startup &
+ BOOTLOADER_CO_CFG_START_UP_MAINTENANCE_ENA_MASK) != 0;
+}
+
+static bool tropic_update(void) {
+ if (!tropic_erase_fw_slot()) {
+ return false;
+ }
+ if (!tropic_prepare_update_config()) {
+ return false;
+ }
+ return tropic_finish_update();
+}
+
+secbool tropic_ensure_fw_updated(void) {
+#ifdef USE_STORAGE
+ // The Maintenance bit may only be enabled after PIN verification.
+ if (storage_is_unlocked() != sectrue) {
+ return secfalse;
+ }
+#endif
+ tropic_fw_update_state_t state = tropic_get_update_state();
+ if (state == TROPIC_FW_UPDATE_ERROR) {
+ return secfalse;
+ }
+ if (state == TROPIC_FW_UPDATE_UP_TO_DATE) {
+ return sectrue;
+ }
+ if (!tropic_update_possible()) {
+ return secfalse;
+ }
+
+ if (state == TROPIC_FW_UPDATE_OUTDATED) {
+ return tropic_update() ? sectrue : secfalse;
+ }
+
+ if (state == TROPIC_FW_UPDATE_UNFINISHED) {
+ return tropic_finish_update() ? sectrue : secfalse;
+ }
+
+ return secfalse;
+}
+
+// Runs at boot. It must never enable the Maintenance bit.
+secbool tropic_check_and_restore_fw_update_in_progress(void) {
+ tropic_fw_update_state_t state = tropic_get_update_state();
+ if (state == TROPIC_FW_UPDATE_ERROR) {
+ return secfalse;
+ }
+
+ if (state != TROPIC_FW_UPDATE_UNFINISHED) {
+ return sectrue;
+ }
+ if (!tropic_update_possible()) {
+ return secfalse;
+ }
+
+ return tropic_finish_update() ? sectrue : secfalse;
+}
+
#ifdef TREZOR_EMULATOR
static uint16_t get_tropic_model_port(void) {
char *port_str = getenv("TROPIC_MODEL_PORT");
@@ -1077,6 +1451,12 @@ lt_handle_t *tropic_prodtest_init_and_get_handle(cli_t *cli) {
return tropic_get_handle();
}
+
+bool tropic_fw_slot_is_outdated(
+ const uint8_t fw_version[TROPIC_FW_VERSION_SIZE]) {
+ return fw_version_is_older(fw_version, fw_CPU_ver) ||
+ fw_version_is_older(fw_version + 4, fw_SPECT_ver);
+}
#endif // TREZOR_PRODTEST
bool tropic_ping(const uint8_t *msg_out, uint8_t *msg_in, uint16_t msg_len) {
### core/embed/sys/smcall/stm32/smcall_dispatch.c
@@ -405,6 +405,14 @@ __attribute((no_stack_protector)) void smcall_handler(uint32_t *args,
uint16_t *size = (uint16_t *)args[2];
args[0] = tropic_data_read__verified(udata_slot, data, size);
} break;
+
+ case SMCALL_TROPIC_ENSURE_FW_UPDATED: {
+ args[0] = tropic_ensure_fw_updated__verified();
+ } break;
+
+ case SMCALL_TROPIC_GET_UPDATE_STATE: {
+ args[0] = tropic_get_update_state__verified();
+ } break;
#endif
#ifdef USE_BACKUP_RAM
### core/embed/sys/smcall/stm32/smcall_numbers.h
@@ -113,5 +113,7 @@ typedef enum {
SMCALL_TELEMETRY_GET,
SMCALL_MLDSA44_VERIFY,
+ SMCALL_TROPIC_ENSURE_FW_UPDATED,
+ SMCALL_TROPIC_GET_UPDATE_STATE,
} smcall_number_t;
### core/embed/sys/smcall/stm32/smcall_stubs.c
@@ -370,6 +370,8 @@ int firmware_hash_continue(uint8_t *hash, size_t hash_len) {
#ifdef USE_TROPIC
+#include <sec/tropic.h>
+
bool tropic_ping(const uint8_t *msg_in, uint8_t *msg_out, uint16_t msg_len) {
return (bool)smcall_invoke3((uint32_t)msg_in, (uint32_t)msg_out, msg_len,
SMCALL_TROPIC_PING);
@@ -391,6 +393,15 @@ bool tropic_data_read(uint16_t udata_slot, uint8_t *data, uint16_t *size) {
(uint32_t)size, SMCALL_TROPIC_DATA_READ);
}
+secbool tropic_ensure_fw_updated(void) {
+ return (secbool)smcall_invoke0(SMCALL_TROPIC_ENSURE_FW_UPDATED);
+}
+
+tropic_fw_update_state_t tropic_get_update_state(void) {
+ return (tropic_fw_update_state_t)smcall_invoke0(
+ SMCALL_TROPIC_GET_UPDATE_STATE);
+}
+
#endif
// =============================================================================
### core/embed/sys/smcall/stm32/smcall_verifiers.c
@@ -565,6 +565,14 @@ bool tropic_data_read__verified(uint16_t udata_slot, uint8_t *data,
apptask_access_violation();
return false;
}
+
+secbool tropic_ensure_fw_updated__verified(void) {
+ return tropic_ensure_fw_updated();
+}
+
+tropic_fw_update_state_t tropic_get_update_state__verified(void) {
+ return tropic_get_update_state();
+}
#endif
#ifdef USE_BACKUP_RAM
### core/embed/sys/smcall/stm32/smcall_verifiers.h
@@ -137,6 +137,8 @@ secbool firmware_get_vendor__verified(char *buff, size_t buff_size);
// ---------------------------------------------------------------------
#ifdef USE_TROPIC
+#include <sec/tropic.h>
+
bool tropic_ping__verified(const uint8_t *msg_out, uint8_t *msg_in,
uint16_t msg_len);
@@ -148,6 +150,10 @@ bool tropic_ecc_sign__verified(uint16_t key_slot_index, const uint8_t *dig,
bool tropic_data_read__verified(uint16_t udata_slot, uint8_t *data,
uint16_t *size);
+secbool tropic_ensure_fw_updated__verified(void);
+
+tropic_fw_update_state_t tropic_get_update_state__verified(void);
+
#endif
// ---------------------------------------------------------------------
### core/embed/sys/syscall/inc/sys/syscall_numbers.h
@@ -222,6 +222,8 @@ typedef enum {
SYSCALL_MCU_ATTESTATION_SIGN,
SYSCALL_TELEMETRY_GET,
+ SYSCALL_TROPIC_ENSURE_FW_UPDATED,
+ SYSCALL_TROPIC_GET_UPDATE_STATE,
// ------------------------------------------------------
// Following syscalls are executed in kernel thread mode
### core/embed/sys/syscall/stm32/syscall_dispatch.c
@@ -1045,6 +1045,14 @@ __attribute((no_stack_protector)) void syscall_handler(uint32_t *args,
uint16_t *size = (uint16_t *)args[2];
args[0] = tropic_data_read__verified(udata_slot, data, size);
} break;
+
+ case SYSCALL_TROPIC_ENSURE_FW_UPDATED: {
+ args[0] = tropic_ensure_fw_updated__verified();
+ } break;
+
+ case SYSCALL_TROPIC_GET_UPDATE_STATE: {
+ args[0] = tropic_get_update_state__verified();
+ } break;
#endif
#ifdef USE_APP_LOADING
### core/embed/sys/syscall/stm32/syscall_stubs.c
@@ -978,6 +978,8 @@ bool dma2d_rgba8888_blend_mono8(const gfx_bitblt_t *bb) {
#ifdef USE_TROPIC
+#include <sec/tropic.h>
+
bool tropic_ping(const uint8_t *msg_in, uint8_t *msg_out, uint16_t msg_len) {
return (bool)syscall_invoke3((uint32_t)msg_in, (uint32_t)msg_out, msg_len,
SYSCALL_TROPIC_PING);
@@ -999,6 +1001,15 @@ bool tropic_data_read(uint16_t udata_slot, uint8_t *data, uint16_t *size) {
(uint32_t)size, SYSCALL_TROPIC_DATA_READ);
}
+secbool tropic_ensure_fw_updated(void) {
+ return (secbool)syscall_invoke0(SYSCALL_TROPIC_ENSURE_FW_UPDATED);
+}
+
+tropic_fw_update_state_t tropic_get_update_state(void) {
+ return (tropic_fw_update_state_t)syscall_invoke0(
+ SYSCALL_TROPIC_GET_UPDATE_STATE);
+}
+
#endif
#ifdef USE_APP_LOADING
### core/embed/sys/syscall/stm32/syscall_verifiers.c
@@ -1544,6 +1544,14 @@ bool tropic_data_read__verified(uint16_t udata_slot, uint8_t *data,
apptask_access_violation();
return false;
}
+
+secbool tropic_ensure_fw_updated__verified(void) {
+ return tropic_ensure_fw_updated();
+}
+
+tropic_fw_update_state_t tropic_get_update_state__verified(void) {
+ return tropic_get_update_state();
+}
#endif
#ifdef USE_APP_LOADING
### core/embed/sys/syscall/stm32/syscall_verifiers.h
@@ -361,6 +361,8 @@ bool button_get_event__verified(button_event_t *event);
// ---------------------------------------------------------------------
#ifdef USE_TROPIC
+#include <sec/tropic.h>
+
bool tropic_ping__verified(const uint8_t *msg_out, uint8_t *msg_in,
uint16_t msg_len);
@@ -372,6 +374,10 @@ bool tropic_ecc_sign__verified(uint16_t key_slot_index, const uint8_t *dig,
bool tropic_data_read__verified(uint16_t udata_slot, uint8_t *data,
uint16_t *size);
+secbool tropic_ensure_fw_updated__verified(void);
+
+tropic_fw_update_state_t tropic_get_update_state__verified(void);
+
#endif
#ifdef USE_APP_LOADING
### core/embed/upymod/modtrezorcrypto/modtrezorcrypto-tropic.h
@@ -174,6 +174,34 @@ static MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_tropic_get_user_data_obj,
/// DEVICE_KEY_SLOT: int
/// FIDO_CERT_INDEX: int
/// FIDO_KEY_SLOT: int
+/// FW_UPDATE_UNFINISHED: int
+/// FW_UPDATE_OUTDATED: int
+/// FW_UPDATE_UP_TO_DATE: int
+/// FW_UPDATE_ERROR: int
+
+/// def ensure_fw_updated() -> bool:
+/// """
+/// Ensure that the firmware on the Tropic Square chip is up to date.
+/// """
+static mp_obj_t mod_trezorcrypto_tropic_ensure_fw_updated(void) {
+ ensure(tropic_ensure_fw_updated(),
+ "Failed to ensure Tropic FW is up to date");
+ return mp_const_true;
+}
+
+static MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorcrypto_tropic_ensure_fw_updated_obj,
+ mod_trezorcrypto_tropic_ensure_fw_updated);
+
+/// def get_update_state() -> int:
+/// """
+/// Return the FW update state of the Tropic Square chip.
+/// """
+static mp_obj_t mod_trezorcrypto_tropic_get_update_state(void) {
+ return mp_obj_new_int(tropic_get_update_state());
+}
+
+static MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorcrypto_tropic_get_update_state_obj,
+ mod_trezorcrypto_tropic_get_update_state);
static const mp_rom_map_elem_t mod_trezorcrypto_tropic_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_tropic)},
@@ -188,6 +216,17 @@ static const mp_rom_map_elem_t mod_trezorcrypto_tropic_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_sign), MP_ROM_PTR(&mod_trezorcrypto_tropic_sign_obj)},
{MP_ROM_QSTR(MP_QSTR_get_user_data),
MP_ROM_PTR(&mod_trezorcrypto_tropic_get_user_data_obj)},
+ {MP_ROM_QSTR(MP_QSTR_ensure_fw_updated),
+ MP_ROM_PTR(&mod_trezorcrypto_tropic_ensure_fw_updated_obj)},
+ {MP_ROM_QSTR(MP_QSTR_FW_UPDATE_UNFINISHED),
+ MP_ROM_INT(TROPIC_FW_UPDATE_UNFINISHED)},
+ {MP_ROM_QSTR(MP_QSTR_FW_UPDATE_OUTDATED),
+ MP_ROM_INT(TROPIC_FW_UPDATE_OUTDATED)},
+ {MP_ROM_QSTR(MP_QSTR_FW_UPDATE_UP_TO_DATE),
+ MP_ROM_INT(TROPIC_FW_UPDATE_UP_TO_DATE)},
+ {MP_ROM_QSTR(MP_QSTR_FW_UPDATE_ERROR), MP_ROM_INT(TROPIC_FW_UPDATE_ERROR)},
+ {MP_ROM_QSTR(MP_QSTR_get_update_state),
+ MP_ROM_PTR(&mod_trezorcrypto_tropic_get_update_state_obj)},
{MP_ROM_QSTR(MP_QSTR_TropicError), MP_ROM_PTR(&mp_type_TropicError)}};
static MP_DEFINE_CONST_DICT(mod_trezorcrypto_tropic_globals,
mod_trezorcrypto_tropic_globals_table);
### core/emu.py
@@ -26,7 +26,9 @@
HERE = Path(__file__).resolve().parent
MICROPYTHON = HERE / "build-xtask" / "artifacts" / "latest" / "firmware-emu"
-TROPIC_MODEL_CONFIG = HERE.parent / "tests" / "tropic_model" / "config.yml"
+TROPIC_MODEL_CONFIG = (
+ HERE.parent / "tests" / "tropic_model" / "firmware_config" / "current.yml"
+)
SRC_DIR = HERE / "src"
PROFILE_BASE = Path.home() / ".trezoremu"
### core/mocks/generated/trezorcrypto/tropic.pyi
@@ -42,3 +42,21 @@ DEVICE_CERT_INDEX: int
DEVICE_KEY_SLOT: int
FIDO_CERT_INDEX: int
FIDO_KEY_SLOT: int
+FW_UPDATE_UNFINISHED: int
+FW_UPDATE_OUTDATED: int
+FW_UPDATE_UP_TO_DATE: int
+FW_UPDATE_ERROR: int
+
+
+# upymod/modtrezorcrypto/modtrezorcrypto-tropic.h
+def ensure_fw_updated() -> bool:
+ """
+ Ensure that the firmware on the Tropic Square chip is up to date.
+ """
+
+
+# upymod/modtrezorcrypto/modtrezorcrypto-tropic.h
+def get_update_state() -> int:
+ """
+ Return the FW update state of the Tropic Square chip.
+ """
### core/src/boot.py
@@ -18,13 +18,17 @@
show_pin_timeout,
)
from trezor.ui.layouts.homescreen import run_lockscreen
+from trezor.ui.layouts.progress import progress
from apps.common.request_pin import verify_user_pin
from apps.common import lock_manager
if utils.USE_OPTIGA:
from trezor.crypto import optiga
+if utils.USE_TROPIC:
+ from trezor.crypto import tropic
+
if utils.USE_POWER_MANAGER:
from micropython import const
from trezor import workflow
@@ -105,13 +109,21 @@ async def bootscreen() -> None:
io.rgb_led.rgb_led_set_enabled(storage.device.get_rgb_led())
await pin_unlock_sequence()
storage.init_unlocked()
+ if utils.USE_TROPIC and utils.bootloader_locked():
+ if tropic.get_update_state() != tropic.FW_UPDATE_UP_TO_DATE:
+ progress().report(0)
+ tropic.ensure_fw_updated()
allow_all_loader_messages()
break
else:
# Even if PIN is not configured, storage needs to be unlocked, unless it has just been initialized.
if not config.is_unlocked():
await verify_user_pin()
storage.init_unlocked()
+ if utils.USE_TROPIC and utils.bootloader_locked():
+ if tropic.get_update_state() != tropic.FW_UPDATE_UP_TO_DATE:
+ progress().report(0)
+ tropic.ensure_fw_updated()
enforce_welcome_screen_duration()
rotation = storage.device.get_rotation()
if utils.USE_TOUCH_WAKEUP:
### core/tools/generate_tropic_model_config.py
@@ -2,6 +2,7 @@
import json
import os
+import struct
from pathlib import Path
import click
@@ -17,7 +18,7 @@
HERE = Path(__file__).parent
ROOT = HERE.parent.parent.resolve()
CONFIG_DIR = ROOT / "tests" / "tropic_model"
-DEST_PATH = CONFIG_DIR / "config.yml"
+DEST_PATH = CONFIG_DIR / "firmware_config" / "current.yml"
CFG_SPEC_PATH = (
ROOT / "core" / "embed" / "sec" / "tropic" / "config" / "tropic_configs.json"
)
@@ -26,9 +27,16 @@
CERT_CHAIN_DIR = CONFIG_DIR / "tropic_cert_chain"
CERT_STORE_PATH = CERT_CHAIN_DIR / "cert_store.der"
+TROPIC_FW_UPDATE_DIR = (
+ ROOT / "vendor" / "libtropic" / "TROPIC01_fw_update_files" / "boot_v_2_0_1"
+)
+# Mirrors `lt_mutable_fw_update_chunk_0_t` in libtropic's `libtropic_common.h`.
+FW_CHUNK_0_FORMAT = "<B64s32sHBBI"
+
HEADER = f"""# This file is auto-generated by `{Path(__file__).relative_to(ROOT)}`.
# Do not edit this file directly. Instead, run `make gen` or
-# `make tropic_config` in the root directory.\n
+# `make tropic_config` in the root directory.
+# Before changing this file, see README.md in this directory.\n
"""
@@ -37,12 +45,13 @@ def get_tropic_configuration(path: Path) -> dict:
all_config_versions = json.load(f)
config = {}
+ default_cfg = {"reversible_configurations": 1, "irreversible_configurations": 0}
for type_of_config in ["reversible_configurations", "irreversible_configurations"]:
short_name = (
"r_config" if type_of_config == "reversible_configurations" else "i_config"
)
for config_version in all_config_versions[type_of_config]:
- if config_version["version"] == 0: # 0 is the default version
+ if config_version["version"] == default_cfg[type_of_config]:
config[short_name] = config_version["config"]
break
@@ -81,6 +90,32 @@ def get_tropic_configuration(path: Path) -> dict:
return numbers
+def find_newest_fw_dir() -> Path:
+ fw_dirs = [d for d in TROPIC_FW_UPDATE_DIR.glob("fw_v_*_*_*") if d.is_dir()]
+ if not fw_dirs:
+ raise click.ClickException(f"No fw_v_X_Y_Z directory in {TROPIC_FW_UPDATE_DIR}")
+ return max(fw_dirs, key=lambda d: tuple(int(n) for n in d.name.split("_")[2:]))
+
+
+def find_fw_image(fw_dir: Path, pattern: str) -> Path:
+ images = list(fw_dir.glob(pattern))
+ if len(images) != 1:
+ raise click.ClickException(
+ f"Expected one {pattern} in {fw_dir}, found {len(images)}"
+ )
+ return images[0]
+
+
+def read_fw_version(image: Path) -> bytes:
+ """Return the {reserved, patch, minor, major} version from the chunk-0 header."""
+ *_, version = struct.unpack_from(FW_CHUNK_0_FORMAT, image.read_bytes())
+ return version.to_bytes(4, "little")
+
+
+def format_fw_version(version: bytes) -> str:
+ return f"{version[3]}.{version[2]}.{version[1]}"
+
+
@click.command()
@click.option("--check", is_flag=True)
def generate_config(check: bool) -> None:
@@ -89,8 +124,13 @@ def generate_config(check: bool) -> None:
)
cert_store = CERT_STORE_PATH.read_bytes()
- chip_id = bytes.fromhex("0" * 192 + "19071f0a04" + "0" * 54)
+ chip_id = bytes.fromhex("0" * 56 + "41434142" + "0" * 128 + "190a081010" + "0" * 54)
tropic_cfg = get_tropic_configuration(CFG_SPEC_PATH)
+ fw_dir = find_newest_fw_dir()
+ riscv_fw_image = find_fw_image(fw_dir, "fw_v*.bin")
+ spect_fw_image = find_fw_image(fw_dir, "spect_app-v*.bin")
+ riscv_fw_version = read_fw_version(riscv_fw_image)
+ spect_fw_version = read_fw_version(spect_fw_image)
config_dict = {
"s_t_priv": device_private_key.private_bytes(
@@ -102,8 +142,8 @@ def generate_config(check: bool) -> None:
"x509_certificate": cert_store,
"debug_random_value": b"\x00\xc0\xff\xee",
"chip_id": chip_id,
- "riscv_fw_version": "1.0.0", # Version of the RISCV Firmware (also know as Application FW) used in TS7 devices
- "spect_fw_version": "1.0.0",
+ "riscv_fw_version": format_fw_version(riscv_fw_version),
+ "spect_fw_version": format_fw_version(spect_fw_version),
"r_config": tropic_cfg["r_config"],
"i_config": tropic_cfg["i_config"],
"i_pairing_keys": {
@@ -133,9 +173,13 @@ def generate_config(check: bool) -> None:
# authenticity check) come from base_config; slot 6 is generated here.
# See https://github.com/trezor/trezor-firmware/blob/main/core/embed/sec/tropic/inc/sec/tropic.h#L31
TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT = 6
+ TROPIC_FW_VERSION_SLOT = 8
base_config["r_user_data"][TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT] = {
"value": (0).to_bytes(4, "big")
}
+ base_config["r_user_data"][TROPIC_FW_VERSION_SLOT] = {
+ "value": riscv_fw_version + spect_fw_version
+ }
overlap = config_dict.keys() & base_config.keys()
if overlap:
@@ -158,6 +202,8 @@ def generate_config(check: bool) -> None:
cfg_spec_stat = CFG_SPEC_PATH.stat()
cert_store_stat = CERT_STORE_PATH.stat()
device_key_stat = (CERT_CHAIN_DIR / "l3.key").stat()
+ riscv_fw_stat = riscv_fw_image.stat()
+ spect_fw_stat = spect_fw_image.stat()
DEST_PATH.write_text(config)
os.utime(
DEST_PATH,
@@ -167,12 +213,16 @@ def generate_config(check: bool) -> None:
cfg_spec_stat.st_atime_ns,
cert_store_stat.st_atime_ns,
device_key_stat.st_atime_ns,
+ riscv_fw_stat.st_atime_ns,
+ spect_fw_stat.st_atime_ns,
),
max(
base_config_stat.st_mtime_ns,
cfg_spec_stat.st_mtime_ns,
cert_store_stat.st_mtime_ns,
device_key_stat.st_mtime_ns,
+ riscv_fw_stat.st_mtime_ns,
+ spect_fw_stat.st_mtime_ns,
),
),
)
### tests/conftest.py
@@ -46,7 +46,7 @@
from . import ui_tests
from .emulators import (
- TROPIC_MODEL_CONFIGFILE,
+ TROPIC_MODEL_CURRENT_CONFIG,
EmulatorWrapper,
delete_profile,
get_logfile,
@@ -120,7 +120,7 @@ def tropic_model_port(request: pytest.FixtureRequest) -> t.Iterator[int | None]:
)
with TropicModel(
profile_dir=temp_dir,
- configfile=TROPIC_MODEL_CONFIGFILE,
+ configfile=TROPIC_MODEL_CURRENT_CONFIG,
port=port,
logfile=logfile,
) as tropic_model:
### tests/emulators.py
@@ -40,9 +40,8 @@
ENV = {"SDL_VIDEODRIVER": "dummy"}
-TROPIC_MODEL_CONFIGFILE = ROOT / "tests" / "tropic_model" / "config.yml"
-TROPIC_MODEL_CONFIGFILE_OLD = ROOT / "tests" / "tropic_model" / "config_old.yml"
-TROPIC_OLD_CONFIG_UNTIL_VERSION = (2, 12, 1) # inclusive
+TROPIC_MODEL_CONFIG_DIR = ROOT / "tests" / "tropic_model" / "firmware_config"
+TROPIC_MODEL_CURRENT_CONFIG = TROPIC_MODEL_CONFIG_DIR / "current.yml"
TROPIC_CAPABLE_MODELS = {"T3W1"}
### tests/prodtest_tests/__init__.py
@@ -33,7 +33,6 @@
# Prodtest command codes — core/embed/projects/prodtest/prodtest_error_codes.h
PRODTEST_ERR_BACKUP_RAM_KEY_NOT_FOUND = 1014
PRODTEST_ERR_OTP_EMPTY = 10012
-PRODTEST_ERR_TROPIC_UPDATE_WRONG_REVISION = 20075
PRODTEST_ERR_TROPIC_TEST_RNG_REPEAT = 20152
### tests/prodtest_tests/test_tropic.py
@@ -27,22 +27,24 @@
4. (optionally) assert on ``session.state()``.
The tests seed the Tropic model with the default device-test config
-(``tests/tropic_model/config.yml``), which represents an already-paired device.
+(``tests/tropic_model/firmware_config/current.yml``), which represents a provisioned device.
Commands that need a fresh/unpaired chip (``tropic-pair``) or device-specific
certificates are covered only where they succeed; some are exercised through
their error paths instead (e.g. the TRNG, which the model drives from a
-constant, and firmware update against a mismatched chip revision).
+constant).
"""
from __future__ import annotations
+from pathlib import Path
+
import pytest
+import yaml
from trezorlib._internal.prodtest_client import Cmd, ProdtestCommand
from . import (
PRODTEST_ERR_TROPIC_TEST_RNG_REPEAT,
- PRODTEST_ERR_TROPIC_UPDATE_WRONG_REVISION,
assert_command_fails,
assert_hexdata,
)
@@ -68,9 +70,6 @@
# Arbitrary non-default sensors config: 8 hex digits = big-endian uint32.
_SENSORS_CONFIG_VALUE = "0000000F"
-# Reversible-config keys that ``lock`` is expected to change.
-_LOCK_CHANGED_R_CONFIG_KEYS = {"cfg_start_up"}
-
# --- info / read-only commands ---------------------------------------------
@@ -111,18 +110,16 @@ def test_tropic_get_spect_fw_version(tropic_prodtest: TropicProdtest) -> None:
def test_tropic_lock_check(tropic_prodtest: TropicProdtest, is_emulator: bool) -> None:
"""``lock-check`` reports a yes/no answer.
- On the emulator this is always ``NO``: ``lock-check`` returns ``NO`` as soon
- as the MCU has no stored Tropic public key, which is the case for a freshly
- started emulator — the pairing process was never run against it. On real
- hardware either answer is valid depending on provisioning, so we only check
- the shape there.
+ On the emulator this is ``YES``: the default Tropic model config is a
+ locked device. On real hardware either answer is valid depending on
+ provisioning, so we only check the shape there.
"""
with tropic_prodtest() as session:
resp = session.client.command_ok(ProdtestCommand(Cmd.TROPIC_LOCK_CHECK))
assert resp.args in ("YES", "NO")
if is_emulator:
- assert resp.args == "NO"
+ assert resp.args == "YES"
@pytest.mark.requires_command(Cmd.TROPIC_READ_CONFIGS)
@@ -219,42 +216,34 @@ def test_tropic_test_counter(tropic_prodtest: TropicProdtest) -> None:
assert state.mcounters, "no monotonic counters were initialized"
-@pytest.mark.requires_command(Cmd.TROPIC_LOCK)
-def test_tropic_lock(tropic_prodtest: TropicProdtest) -> None:
- """``tropic-lock`` writes the expected config and distribution version.
+@pytest.mark.requires_command(Cmd.TROPIC_LOCK, Cmd.TROPIC_LOCK_CHECK)
+def test_tropic_lock(tropic_prodtest: TropicProdtest, tmp_path: Path) -> None:
+ """``tropic-lock`` turns a paired, unlocked chip into the provisioned one.
- ``lock`` is irreversible, but each Tropic test runs against its own
- throwaway model, so locking it is safe. It rewrites the reversible config to
- the expected "locked" values, writes the distribution version into its slot,
- erases the backup slot, and leaves the pairing keys untouched. We capture a
- fresh (unlocked) model as a baseline to show the reversible config actually
- changed.
+ The default config is the provisioned device, so it is the expected result.
+ The starting chip is that device before ``lock``: both configs erased and no
+ distribution version. Each test runs against its own throwaway model, so the
+ irreversible lock is safe.
"""
- baseline = TropicModelState.from_file(DEFAULT_TROPIC_MODEL_CONFIGFILE)
-
- with tropic_prodtest(
- tropic_model_configfile=DEFAULT_TROPIC_MODEL_CONFIGFILE
- ) as session:
+ config = yaml.safe_load(DEFAULT_TROPIC_MODEL_CONFIGFILE.read_text())
+ config["r_config"] = dict.fromkeys(config["r_config"], 0xFFFFFFFF)
+ config["i_config"] = dict.fromkeys(config["i_config"], 0xFFFFFFFF)
+ del config["r_user_data"][_DISTRIBUTION_VERSION_SLOT]
+ pre_lock_config = tmp_path / "tropic_model_config.yml"
+ pre_lock_config.write_text(yaml.safe_dump(config))
+
+ with tropic_prodtest(tropic_model_configfile=pre_lock_config) as session:
session.client.command_ok(ProdtestCommand(Cmd.TROPIC_LOCK))
+ resp = session.client.command_ok(ProdtestCommand(Cmd.TROPIC_LOCK_CHECK))
locked = session.state()
+ provisioned = TropicModelState.from_file(DEFAULT_TROPIC_MODEL_CONFIGFILE)
- # Locking rewrites the reversible config to the expected locked values.
- # Reporting the full diff on failure.
- r_config_diff = {
- key: (baseline.r_config.get(key), locked.r_config.get(key))
- for key in baseline.r_config.keys() | locked.r_config.keys()
- if baseline.r_config.get(key) != locked.r_config.get(key)
- }
- assert set(r_config_diff) == _LOCK_CHANGED_R_CONFIG_KEYS, (
- f"unexpected r_config changes: {r_config_diff}"
+ assert resp.args == "YES"
+ assert locked.r_config == provisioned.r_config
+ assert locked.i_config == provisioned.i_config
+ assert locked.slot_value(_DISTRIBUTION_VERSION_SLOT) == provisioned.slot_value(
+ _DISTRIBUTION_VERSION_SLOT
)
-
- assert locked.i_config == baseline.i_config
-
- # The distribution version is written (4-byte big-endian) and its backup
- # slot is left erased.
- version = locked.slot_value(_DISTRIBUTION_VERSION_SLOT)
- assert version is not None and len(version) == 4
assert locked.slot_is_erased(_BACKUP_DISTRIBUTION_VERSION_SLOT)
# Pairing keys survive the lock.
@@ -277,13 +266,9 @@ def test_tropic_test_rng_rejects_constant_rng(tropic_prodtest: TropicProdtest) -
@pytest.mark.requires_command(Cmd.TROPIC_UPDATE_FW)
-def test_tropic_update_fw_rejects_wrong_revision(
+def test_tropic_update_fw_accept_silicon_revision(
tropic_prodtest: TropicProdtest,
) -> None:
- """``update-fw`` refuses the model's mismatched chip silicon revision."""
+ """``update-fw`` accepts the model's chip silicon revision."""
with tropic_prodtest() as session:
- resp = assert_command_fails(
- session.client, ProdtestCommand(Cmd.TROPIC_UPDATE_FW)
- )
-
- assert resp.error_code == PRODTEST_ERR_TROPIC_UPDATE_WRONG_REVISION
+ session.client.command_ok(ProdtestCommand(Cmd.TROPIC_UPDATE_FW))
### tests/prodtest_tests/tropic_utils.py
@@ -34,13 +34,15 @@
from trezorlib._internal.prodtest_client import ProdtestClient
ROOT = Path(__file__).resolve().parent.parent.parent
-DEFAULT_TROPIC_MODEL_CONFIGFILE = ROOT / "tests" / "tropic_model" / "config.yml"
+DEFAULT_TROPIC_MODEL_CONFIGFILE = (
+ ROOT / "tests" / "tropic_model" / "firmware_config" / "current.yml"
+)
class TropicModelState:
"""Read-only view over a Tropic model config-output YAML file.
- The structure mirrors `tests/tropic_model/config.yml`:
+ The structure mirrors `tests/tropic_model/firmware_config/current.yml`:
- `i_config` / `r_config`: dicts of `cfg_*` config words (ints)
- `i_pairing_keys`: slot index -> {`state`, `value`}
### tests/tropic_model/firmware_config/2_12_1.yml
@@ -1,5 +1,16 @@
+# Do not edit this file. See `README.md` in this directory.
+
+chip_id: !!binary |
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEFDQUIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAA=
debug_random_value: !!binary |
AMD/7g==
+i_pairing_keys:
+ 2:
+ state: written
+ value: !!binary |
+ hC/jIagkdAg3N/8rm4iir0JELbDYqsxtxp6ZUzNEskY=
r_ecc_keys:
0:
a: !!binary |
### tests/tropic_model/firmware_config/2_12_5.yml
@@ -1,9 +1,7 @@
-# This file is auto-generated by `core/tools/generate_tropic_model_config.py`.
-# Do not edit this file directly. Instead, run `make gen` or
-# `make tropic_config` in the root directory.
+# Do not edit this file. See `README.md` in this directory.
chip_id: !!binary |
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEFDQUIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQcfCgQAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAA=
debug_random_value: !!binary |
### tests/tropic_model/firmware_config/README.md
@@ -0,0 +1,21 @@
+# Tropic model firmware configs
+
+This directory holds the Tropic model configurations used when running firmware
+(as opposed to prodtest) against the model, most notably by the upgrade tests.
+
+- `current.yml` — the configuration matching the current firmware on `main`.
+- `X_Y_Z.yml` — a snapshot of an older configuration, used by firmware releases
+ up to and including version X.Y.Z.
+
+For a given firmware version, the tests pick the oldest `X_Y_Z.yml` with
+X.Y.Z >= the firmware version, falling back to `current.yml`
+(see `_get_tropic_model_configfile` in `tests/upgrade_tests/conftest.py`).
+
+## When changing `current.yml`
+
+Check whether any firmware has been released since the previous change to
+`current.yml`. If so, copy the previous content of `current.yml` to
+`X_Y_Z.yml`, where X.Y.Z is the latest release made before your change (the
+copy covers all releases since the previous change), and make sure upgrade
+tests pass. A CI bot (`.github/workflows/bot-tropic-emulator.yml`) posts this
+reminder on pull requests touching `current.yml`.
### tests/tropic_model/firmware_config/current.yml
@@ -0,0 +1,178 @@
+# This file is auto-generated by `core/tools/generate_tropic_model_config.py`.
+# Do not edit this file directly. Instead, run `make gen` or
+# `make tropic_config` in the root directory.
+# Before changing this file, see README.md in this directory.
+
+chip_id: !!binary |
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEFDQUIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQoIEBAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAA=
+debug_random_value: !!binary |
+ AMD/7g==
+i_config:
+ cfg_debug: 4294967294
+ cfg_gpo: 4294967295
+ cfg_sensors: 4294967295
+ cfg_sleep_mode: 4294967295
+ cfg_start_up: 4294967289
+ cfg_uap_ecc_key_erase: 4278124286
+ cfg_uap_ecc_key_generate: 4278124284
+ cfg_uap_ecc_key_read: 4278124286
+ cfg_uap_ecc_key_store: 4278124284
+ cfg_uap_ecdsa_sign: 4278124284
+ cfg_uap_eddsa_sign: 4278124284
+ cfg_uap_i_config_read: 4294967038
+ cfg_uap_i_config_write: 4294967038
+ cfg_uap_mac_and_destroy: 4278123772
+ cfg_uap_mcounter_get: 4278124286
+ cfg_uap_mcounter_init: 4278124284
+ cfg_uap_mcounter_update: 4278124286
+ cfg_uap_pairing_key_invalidate: 4278124286
+ cfg_uap_pairing_key_read: 4278124286
+ cfg_uap_pairing_key_write: 4244438268
+ cfg_uap_ping: 4294967294
+ cfg_uap_r_config_read: 4294967038
+ cfg_uap_r_config_write_erase: 4294967292
+ cfg_uap_r_mem_data_erase: 4278123774
+ cfg_uap_r_mem_data_read: 4278123774
+ cfg_uap_r_mem_data_write: 4278123772
+ cfg_uap_random_value_get: 4294967294
+i_pairing_keys:
+ 0:
+ state: invalid
+ value: !!binary |
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+ 1:
+ state: written
+ value: !!binary |
+ zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=
+ 2:
+ state: written
+ value: !!binary |
+ hC/jIagkdAg3N/8rm4iir0JELbDYqsxtxp6ZUzNEskY=
+r_config:
+ cfg_debug: 0
+ cfg_gpo: 0
+ cfg_sensors: 0
+ cfg_sleep_mode: 1
+ cfg_start_up: 1
+ cfg_uap_ecc_key_erase: 101058054
+ cfg_uap_ecc_key_generate: 101058052
+ cfg_uap_ecc_key_read: 101058054
+ cfg_uap_ecc_key_store: 101058052
+ cfg_uap_ecdsa_sign: 101058052
+ cfg_uap_eddsa_sign: 101058052
+ cfg_uap_i_config_read: 1542
+ cfg_uap_i_config_write: 1542
+ cfg_uap_mac_and_destroy: 101057540
+ cfg_uap_mcounter_get: 101058054
+ cfg_uap_mcounter_init: 101058052
+ cfg_uap_mcounter_update: 101058054
+ cfg_uap_pairing_key_invalidate: 101058054
+ cfg_uap_pairing_key_read: 101058054
+ cfg_uap_pairing_key_write: 67372036
+ cfg_uap_ping: 6
+ cfg_uap_r_config_read: 1542
+ cfg_uap_r_config_write_erase: 4
+ cfg_uap_r_mem_data_erase: 101057542
+ cfg_uap_r_mem_data_read: 101057542
+ cfg_uap_r_mem_data_write: 101057540
+ cfg_uap_random_value_get: 6
+r_ecc_keys:
+ 0:
+ a: !!binary |
+ NO1FnxTZhbCto5o/wW32Vri5do/X7Y5p7MDuheV3oV4=
+ origin: 1
+ prefix: !!binary |
+ mPLEpBTIRX85mkGjhjMnqmcJXyeNMU/cLgtRRtfVXfs=
+ s: !!binary |
+ gt1Ziv3lkQN0BNo4ArnT8xQBjAbIc4KZVpcsC+DCSwc=
+r_user_data:
+ 3:
+ free: false
+ value: !!binary |
+ A1gwggGPMIIBQaADAgECAghqGYl8vU/fZTAFBgMrZXAwTzELMAkGA1UEBhMCQ1oxHjAcBgNVBAoM
+ FVRyZXpvciBDb21wYW55IHMuci5vLjEgMB4GA1UEAwwXVHJlem9yIE1hbnVmYWN0dXJpbmcgQ0Ew
+ IBcNMjYwNTI5MTI0MTM0WhgPMjA1NjA1MjkxMjQxMzRaMEcxGzAZBgNVBAMMElQzVzEgVHJlem9y
+ IFNhZmUgNzEXMBUGA1UEBRMOMTIzNDU2Nzg5MDEyMzQxDzANBgNVBC4TBlRyb3BpYzAqMAUGAytl
+ cAMhADTtRZ8U2YWwraOaP8Ft9la4uXaP1+2OaezA7oXld6Feo0EwPzAOBgNVHQ8BAf8EBAMCAIAw
+ DAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBQlrTb4rxo0D5Q4r67kLb0dWh8GfjAFBgMrZXADQQBY
+ UvSq/qeCetgLo2BVCVtTzNYQZ/h1/ywbr7nUQoVgtu4AmNylPAf0wU3/Nh1kIpmPOChftCxk3o3N
+ EC4FaxkBMIIBwTCCAXOgAwIBAgIIahmJfAChFPswBQYDK2VwMFQxCzAJBgNV
+ 4:
+ free: false
+ value: !!binary |
+ BAYTAkNaMR4wHAYDVQQKDBVUcmV6b3IgQ29tcGFueSBzLnIuby4xJTAjBgNVBAMMHFRyZXpvciBN
+ YW51ZmFjdHVyaW5nIFJvb3QgQ0EwIBcNMjYwNTI5MTI0MTM0WhgPMjA1NjA1MjkxMjQxMzRaME8x
+ CzAJBgNVBAYTAkNaMR4wHAYDVQQKDBVUcmV6b3IgQ29tcGFueSBzLnIuby4xIDAeBgNVBAMMF1Ry
+ ZXpvciBNYW51ZmFjdHVyaW5nIENBMCowBQYDK2VwAyEAIqu/Tw0FdXfZM6insNm/pYTxCaglF0l1
+ qEo7FJVm4eGjZjBkMA4GA1UdDwEB/wQEAwICBDASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW
+ BBQlrTb4rxo0D5Q4r67kLb0dWh8GfjAfBgNVHSMEGDAWgBTVNlLs4s+6GW64HN3F3UH2GuDYMTAF
+ BgMrZXADQQBwsfMsZ0SWMLi2f/nYVrraInCvvBGPR9oPbweuB1/nVqEDGM4aaa7DVnTW95mfq0PF
+ XHeEL692nN8SdN+goaYNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ 5:
+ free: false
+ value: !!binary |
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ 6:
+ value: !!binary |
+ AAAAAA==
+ 8:
+ value: !!binary |
+ AAABAgAAAwE=
+riscv_fw_version: 2.1.0
+s_t_priv: !!binary |
+ sEKAf+kv+HXfRScPbVWqKt9814O+VloX1Gbu1UW+42c=
+s_t_pub: !!binary |
+ MekK8VBFEO5O/XkTM0FIFYmiiVzF+7E+1XEcHpuBmHI=
+spect_fw_version: 1.3.0
+x509_certificate: !!binary |
+ AQQB5QJrApgCZjCCAeEwggFnoAMCAQICEALwAAUINBkECQwHAAADBaUwCgYIKoZIzj0EAwMwTDEL
+ MAkGA1UEBhMCQ1oxHTAbBgNVBAoMFFRyb3BpYyBTcXVhcmUgcy5yLm8uMR4wHAYDVQQDDBVUUk9Q
+ SUMwMS1YIFRFU1QgQ0EgdjEwHhcNMjYwNTE4MTQxNjA5WhcNNDYwNTE4MTQxNjA5WjAkMSIwIAYD
+ VQQDDBkiVFJPUElDMDEgZVNFIFRFU1QgQ0EgdjEiMCowBQYDK2VuAyEAMekK8VBFEO5O/XkTM0FI
+ FYmiiVzF+7E+1XEcHpuBmHKjgYEwfzAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIDCDAfBgNV
+ HSMEGDAWgBQZDolEsAhH4uilRBGRkPUfff7PZzA+BgNVHR8BAf8ENDAyMDCgLqAshipodHRwOi8v
+ cGtpLnRyb3BpY3NxdWFyZS5jb20vbDMvdDAxLVR2MS5jcmwwCgYIKoZIzj0EAwMDaAAwZQIxAKdL
+ S3FIxsmZxwo2YmdNK6xxvgaQhCuwiZgkrzeMEUHT3asAbwgv3BtlAV9koSaXgAIwdV2ulRPsb2Cn
+ zZt9cLJHNN8UYpqLazLw/Nu/L+yiGmT3unqZoN2AfBfqfR5Ntf0TMIICZzCCAe6gAwIBAgICJxEw
+ CgYIKoZIzj0EAwMwSjELMAkGA1UEBhMCQ1oxHTAbBgNVBAoMFFRyb3BpYyBTcXVhcmUgcy5yLm8u
+ MRwwGgYDVQQDDBNUUk9QSUMwMSBURVNUIENBIHYxMCAXDTI2MDUxODE0MTYwOVoYDzIwNjEwNTE4
+ MTQxNjA5WjBMMQswCQYDVQQGEwJDWjEdMBsGA1UECgwUVHJvcGljIFNxdWFyZSBzLnIuby4xHjAc
+ BgNVBAMMFVRST1BJQzAxLVggVEVTVCBDQSB2MTB2MBAGByqGSM49AgEGBSuBBAAiA2IABAY38Qt1
+ 5F3JW+764qvyC5RgSzRUfzbNDIVpbAzJFy9UmR5kw23tUIQTGevLQDmK2J/J9B/46LVAnTlQwQsd
+ rXpUl3NCv+i9BiXsxRCRyDJY6Ns8OCZ9a1USalEurSIBeqOBojCBnzAdBgNVHQ4EFgQUGQ6JRLAI
+ R+LopUQRkZD1H33+z2cwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwHwYDVR0j
+ BBgwFoAUo0YgBPKEgT2vcCLRbB9YwhDfUHowOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL3BraS50
+ cm9waWNzcXVhcmUuY29tL2wyL3QwMXYxLmNybDAKBggqhkjOPQQDAwNnADBkAjAJxfRR923ATeXG
+ 6DKOP0E/QiBXMT5jAirMDfj8CjHk8aOq9GAdqLwYyVCuYcb2XzQCMDeGx85ybI34C3DcEIbmAXm6
+ rgM6iJV3GlQTp41XicHD+qoaUreDKMfF7YZjkpXWqjCCApQwggH2oAMCAQICAgPpMAoGCCqGSM49
+ BAMEMFQxCzAJBgNVBAYTAkNaMR0wGwYDVQQKDBRUcm9waWMgU3F1YXJlIHMuci5vLjEmMCQGA1UE
+ AwwdVHJvcGljIFNxdWFyZSBURVNUIFJvb3QgQ0EgdjEwIBcNMjYwNTE4MTQxNjA5WhgPMjA2NjA1
+ MTgxNDE2MDlaMEoxCzAJBgNVBAYTAkNaMR0wGwYDVQQKDBRUcm9waWMgU3F1YXJlIHMuci5vLjEc
+ MBoGA1UEAwwTVFJPUElDMDEgVEVTVCBDQSB2MTB2MBAGByqGSM49AgEGBSuBBAAiA2IABFFv11sz
+ 121QOdvOI7Ye6hqEU6ULX5yDznmxJ1RoW7M45wp49yBimXQyrQuKoithQVVDHXHOoGIb5mrHKRq+
+ qOK79QXMBWiXdk0XzCzg8hWniVzmjKM4yzBsNQTEx6dX7aOBojCBnzAdBgNVHQ4EFgQUo0YgBPKE
+ gT2vcCLRbB9YwhDfUHowEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8EBAMCAQYwHwYDVR0j
+ BBgwFoAUOtJcdFSqqNHRCXlgCknmyX4bcUkwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL3BraS50
+ cm9waWNzcXVhcmUuY29tL2wxL3RzcnYxLmNybDAKBggqhkjOPQQDBAOBiwAwgYcCQgFJyvqNVY5l
+ 5Q2e4wilykUnJJNUiduP4Nwx32CWSohLmSvmW1cJsyCFG/0ladHE5QcWxse+ZGk8cVrI9SnVNat2
+ xwJBIRPc7phizoIgHU5sI6pFPuGrHekj/ZbF1VQgGakeBTumWSzSEKCkCd6uVQ6sBHmnOu9K2JEF
+ ED11HOzxMj6qmEIwggJiMIIBxKADAgECAgFlMAoGCCqGSM49BAMEMFQxCzAJBgNVBAYTAkNaMR0w
+ GwYDVQQKDBRUcm9waWMgU3F1YXJlIHMuci5vLjEmMCQGA1UEAwwdVHJvcGljIFNxdWFyZSBURVNU
+ IFJvb3QgQ0EgdjEwIBcNMjYwNTE4MTQxNjA5WhgPMjA3NjA1MTcxNDE2MDlaMFQxCzAJBgNVBAYT
+ AkNaMR0wGwYDVQQKDBRUcm9waWMgU3F1YXJlIHMuci5vLjEmMCQGA1UEAwwdVHJvcGljIFNxdWFy
+ ZSBURVNUIFJvb3QgQ0EgdjEwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAF2kR6+e8uAVCZbDLzX
+ aQTrYoh6tvi94vkJlbhEWMqAYCfioae9ZEh1wMK5kFCQ7UlO/1wjO3lT4NbfYTUmYFgW+wGkDOA6
+ siTLfpqDZV/Czta7dtYaVGXrV9MlaiUih9qggl35dy/297dTDOU/wwg29Bze46Ft2eHhPKB84A5X
+ JNi/NaNCMEAwHQYDVR0OBBYEFDrSXHRUqqjR0Ql5YApJ5sl+G3FJMA8GA1UdEwEB/wQFMAMBAf8w
+ DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMEA4GLADCBhwJCAJcZyplLgFyFcJQvuns22FEP/0LX
+ uwl3oK0Yr6UyMIEYEEJYS6oPw4qqVz5359+p+ueUulKXcTZ1mcYk45/OgQvFAkEQhKqUl8NhPkeX
+ HItERgqDxhJUTf995gZF/v1TOzJSix62Ykn7pd0p7gXYQVTWVDhLHSxUepPe7NxuD5UUJJtpUQ==
### tests/upgrade_tests/conftest.py
@@ -25,9 +25,7 @@
from trezorlib._internal.emulator import TropicModel
from ..emulators import (
- TROPIC_MODEL_CONFIGFILE,
- TROPIC_MODEL_CONFIGFILE_OLD,
- TROPIC_OLD_CONFIG_UNTIL_VERSION,
+ TROPIC_MODEL_CONFIG_DIR,
delete_profile,
get_logfile,
is_tropic_capable_model,
@@ -40,10 +38,16 @@ def _get_tropic_model_configfile(tag: str | None) -> Path:
if tag is not None and tag.startswith("v"):
tag_version = tag[1:].partition("-")[0]
if len(tag_version.split(".")) == 3:
- version_tuple = tuple(int(i) for i in tag_version.split("."))
- if version_tuple <= TROPIC_OLD_CONFIG_UNTIL_VERSION:
- return TROPIC_MODEL_CONFIGFILE_OLD
- return TROPIC_MODEL_CONFIGFILE
+ tag_version_tuple = tuple(int(i) for i in tag_version.split("."))
+ # Find the oldest versioned config whose version is >= the tag version.
+ candidates = []
+ for configfile in TROPIC_MODEL_CONFIG_DIR.glob("[0-9]*_[0-9]*_[0-9]*.yml"):
+ config_version_tuple = tuple(int(p) for p in configfile.stem.split("_"))
+ if tag_version_tuple <= config_version_tuple:
+ candidates.append((config_version_tuple, configfile))
+ if candidates:
+ return min(candidates)[1]
+ return TROPIC_MODEL_CONFIG_DIR / "current.yml"
# This fixture is very similar to `tropic_model` from the parent directory, but has a "function"
### tests/upgrade_tests/test_tropic_config_set.py
@@ -14,7 +14,6 @@
# You should have received a copy of the License along with this library.
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
-import json
import tempfile
from contextlib import nullcontext
from dataclasses import dataclass
@@ -24,24 +23,24 @@
import yaml
from tests.emulators import (
- ROOT,
- TROPIC_MODEL_CONFIGFILE,
+ TROPIC_MODEL_CURRENT_CONFIG,
EmulatorWrapper,
delete_profile,
get_logfile,
)
from trezorlib._internal.emulator import TropicModel
from . import model_only
-
-TROPIC_CONFIGS_JSON = (
- ROOT / "core" / "embed" / "sec" / "tropic" / "config" / "tropic_configs.json"
+from .tropic_common import (
+ TROPIC_CONFIG_BACKUP_DISTRIBUTION_VERSION_SLOT,
+ TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT,
+ expected_config,
+ set_chip_distribution,
+ set_version_slot,
+ slot_is_erased,
+ slot_value,
)
-TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT = 6
-TROPIC_CONFIG_BACKUP_DISTRIBUTION_VERSION_SLOT = 7
-CHIP_ID_BATCH_ID_OFFSET = 96
-BATCH_ID_V0 = bytes([0x19, 0x0A, 0x08, 0x10, 0x10])
-BATCH_ID_V1 = bytes([0x19, 0x07, 0x1F, 0x0A, 0x04])
+
INCOMPARABLE_I_CONFIG = "incomparable_i"
INCOMPARABLE_R_CONFIG = "incomparable_r"
@@ -209,121 +208,41 @@ class TropicBootScenario:
]
-def _config_to_numbers(config: dict, irreversible: bool) -> dict[str, int]:
- numbers = {}
- for category, category_config in config.items():
- number = 0xFFFFFFFF if irreversible else 0
- settings = category_config["setting"]
-
- if "uap" not in category:
- for details in settings.values():
- if irreversible:
- if not details["value"]:
- number &= ~(1 << details["bit"])
- elif details["value"]:
- number |= 1 << details["bit"]
- else:
- for i in range(4):
- for details in settings[f"pairing_key_{i}"].values():
- if irreversible:
- if not details["value"]:
- number &= ~(1 << details["bit"])
- elif details["value"]:
- number |= 1 << details["bit"]
-
- numbers[category] = number
- return numbers
-
-
-def _expected_config(config_type: str, version: int) -> dict[str, int]:
- configs = json.loads(TROPIC_CONFIGS_JSON.read_text())
- config_versions = configs[config_type]
- irreversible = config_type == "irreversible_configurations"
-
- for versioned_config in config_versions:
- if versioned_config["version"] == version:
- return _config_to_numbers(versioned_config["config"], irreversible)
-
- raise ValueError(f"Missing Tropic {config_type} version {version}")
-
-
def _initial_config(config_type: str, version_or_name: int | str) -> dict[str, int]:
if isinstance(version_or_name, int):
- return _expected_config(config_type, version_or_name)
+ return expected_config(config_type, version_or_name)
if version_or_name == INCOMPARABLE_I_CONFIG:
- config = _expected_config("irreversible_configurations", 0)
+ config = expected_config("irreversible_configurations", 0)
config["cfg_uap_mac_and_destroy"] = 4244438268
return config
if version_or_name == INCOMPARABLE_R_CONFIG:
- config = _expected_config("reversible_configurations", 0)
+ config = expected_config("reversible_configurations", 0)
config["cfg_start_up"] = 2
return config
raise ValueError(f"Unknown Tropic config scenario: {version_or_name}")
-def _set_chip_distribution(config: dict, distribution_version: int) -> None:
- batch_id = BATCH_ID_V1 if distribution_version == 1 else BATCH_ID_V0
- chip_id = bytearray(config["chip_id"])
- chip_id[CHIP_ID_BATCH_ID_OFFSET : CHIP_ID_BATCH_ID_OFFSET + len(batch_id)] = (
- batch_id
- )
- config["chip_id"] = bytes(chip_id)
-
-
-def _set_slot(config: dict, slot: int, value: int | None) -> None:
- r_user_data = config.setdefault("r_user_data", {})
- if value is None:
- r_user_data.pop(slot, None)
- return
-
- r_user_data[slot] = {"value": value.to_bytes(4, "big")}
-
-
def _build_tropic_model_config(scenario: TropicBootScenario) -> dict:
- config = yaml.safe_load(TROPIC_MODEL_CONFIGFILE.read_text())
- _set_chip_distribution(config, scenario.chip_distribution)
+ config = yaml.safe_load(TROPIC_MODEL_CURRENT_CONFIG.read_text())
+ set_chip_distribution(config, scenario.chip_distribution)
config["i_config"] = _initial_config(
"irreversible_configurations", scenario.initial_i_config
)
config["r_config"] = _initial_config(
"reversible_configurations", scenario.initial_r_config
)
- _set_slot(
+ set_version_slot(
config, TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT, scenario.distribution_slot
)
- _set_slot(
+ set_version_slot(
config, TROPIC_CONFIG_BACKUP_DISTRIBUTION_VERSION_SLOT, scenario.backup_slot
)
return config
-def _slot_config(config: dict, slot: int) -> dict | None:
- return (config.get("r_user_data") or {}).get(slot)
-
-
-def _slot_value(config: dict, slot: int) -> bytes | None:
- slot_config = _slot_config(config, slot)
- if not slot_config:
- return None
- return slot_config.get("value")
-
-
-def _slot_is_erased(config: dict, slot: int) -> bool:
- slot_config = _slot_config(config, slot)
- if not slot_config:
- return True
- if slot_config.get("free") is True:
- return True
-
- value = slot_config.get("value")
- if value in (None, b""):
- return True
- return all(byte == 0xFF for byte in value)
-
-
def _check_tropic_model_output(
output_path: Path,
expected_i_version: int,
@@ -337,18 +256,18 @@ def _check_tropic_model_output(
)
output = yaml.safe_load(output_path.read_text())
- assert output["i_config"] == _expected_config(
+ assert output["i_config"] == expected_config(
"irreversible_configurations", expected_i_version
)
- assert output["r_config"] == _expected_config(
+ assert output["r_config"] == expected_config(
"reversible_configurations", expected_r_version
)
- assert _slot_value(
+ assert slot_value(
output, TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT
) == expected_distribution_version.to_bytes(4, "big")
if expect_backup_erased:
- assert _slot_is_erased(output, TROPIC_CONFIG_BACKUP_DISTRIBUTION_VERSION_SLOT)
+ assert slot_is_erased(output, TROPIC_CONFIG_BACKUP_DISTRIBUTION_VERSION_SLOT)
@model_only("T3W1")
### tests/upgrade_tests/test_tropic_fw_update.py
@@ -0,0 +1,421 @@
+# This file is part of the Trezor project.
+#
+# Copyright (C) SatoshiLabs and contributors
+#
+# This library is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the License along with this library.
+# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
+
+import tempfile
+from contextlib import nullcontext
+from dataclasses import dataclass
+from pathlib import Path
+
+import pytest
+import yaml
+
+from tests.emulators import (
+ TROPIC_MODEL_CURRENT_CONFIG,
+ EmulatorWrapper,
+ delete_profile,
+ get_logfile,
+)
+from trezorlib._internal.emulator import TropicModel
+
+from . import model_only
+from .tropic_common import (
+ SILICON_REV_ABAB,
+ SILICON_REV_ACAB,
+ TROPIC_CONFIG_BACKUP_DISTRIBUTION_VERSION_SLOT,
+ TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT,
+ expected_config,
+ set_chip_distribution,
+ set_chip_revision,
+ set_slot,
+ set_version_slot,
+ slot_is_erased,
+ slot_value,
+)
+
+TROPIC_FW_VERSION_SLOT = 8
+
+MAINTENANCE_ENA_BIT = 3
+MAINTENANCE_FORBIDDEN_I_CONFIG = "maintenance_forbidden_i"
+
+
+def _bundled_fw_version_slot() -> bytes:
+ """The versions `tropic_write_fw_slot()` records after a successful update.
+
+ Taken from the generated model config, which `generate_tropic_model_config.py`
+ fills from the images bundled by `core/embed/sec/tropic/build.rs`. A firmware
+ bump therefore only has to be followed by `make tropic_config`.
+ """
+ config = yaml.safe_load(TROPIC_MODEL_CURRENT_CONFIG.read_text())
+ value = slot_value(config, TROPIC_FW_VERSION_SLOT)
+ assert value is not None, (
+ f"Missing FW version slot in {TROPIC_MODEL_CURRENT_CONFIG}"
+ )
+ return value
+
+
+def _shift_majors(slot: bytes, delta: int) -> bytes:
+ """Shift the major version of both halves of a FW version slot.
+
+ Both have to move: `fw_version_is_older()` is OR-ed over the two, so leaving
+ one half pinned would eventually make it older than the bundled image and
+ flip the scenario it belongs to.
+ """
+ version = bytearray(slot)
+ version[3] += delta # RISC-V major
+ version[7] += delta # SPECT major
+ return bytes(version)
+
+
+BUNDLED_FW_VERSION_SLOT = _bundled_fw_version_slot()
+# An older version makes `fw_version_is_older()` report that an update is due.
+OLDER_FW_VERSION_SLOT = _shift_majors(BUNDLED_FW_VERSION_SLOT, -1)
+# A newer version must be left alone -- no downgrades.
+NEWER_FW_VERSION_SLOT = _shift_majors(BUNDLED_FW_VERSION_SLOT, +1)
+
+TRUNCATED_FW_VERSION_SLOT = BUNDLED_FW_VERSION_SLOT[:4]
+OVERSIZED_FW_VERSION_SLOT = BUNDLED_FW_VERSION_SLOT + b"\x00"
+
+
+@dataclass(frozen=True)
+class TropicFwUpdateScenario:
+ id: str
+ chip_distribution: int
+ initial_i_config: int | str
+ initial_r_config: int
+ distribution_slot: int | None
+ backup_slot: int | None
+ fw_version_slot: bytes | None
+ expect_failure: bool
+ silicon_revision: bytes = SILICON_REV_ACAB
+ expected_i_version: int | None = None
+ expected_r_version: int | None = None
+ expected_distribution_version: int | None = None
+ expected_fw_version_slot: bytes | None = None
+
+
+TROPIC_FW_UPDATE_SCENARIOS = [
+ TropicFwUpdateScenario(
+ id="already-updated",
+ chip_distribution=0,
+ initial_i_config=0,
+ initial_r_config=1,
+ distribution_slot=0,
+ backup_slot=None,
+ fw_version_slot=BUNDLED_FW_VERSION_SLOT,
+ expect_failure=False,
+ expected_i_version=0,
+ expected_r_version=1,
+ expected_distribution_version=0,
+ expected_fw_version_slot=BUNDLED_FW_VERSION_SLOT,
+ ),
+ TropicFwUpdateScenario(
+ id="slot-empty",
+ chip_distribution=0,
+ initial_i_config=0,
+ initial_r_config=1,
+ distribution_slot=0,
+ backup_slot=None,
+ fw_version_slot=None,
+ expect_failure=False,
+ expected_i_version=0,
+ expected_r_version=1,
+ expected_distribution_version=0,
+ expected_fw_version_slot=BUNDLED_FW_VERSION_SLOT,
+ ),
+ TropicFwUpdateScenario(
+ id="older-version",
+ chip_distribution=0,
+ initial_i_config=0,
+ initial_r_config=1,
+ distribution_slot=0,
+ backup_slot=None,
+ fw_version_slot=OLDER_FW_VERSION_SLOT,
+ expect_failure=False,
+ expected_i_version=0,
+ expected_r_version=1,
+ expected_distribution_version=0,
+ expected_fw_version_slot=BUNDLED_FW_VERSION_SLOT,
+ ),
+ TropicFwUpdateScenario(
+ id="newer-version",
+ chip_distribution=0,
+ initial_i_config=0,
+ initial_r_config=1,
+ distribution_slot=0,
+ backup_slot=None,
+ fw_version_slot=NEWER_FW_VERSION_SLOT,
+ expect_failure=False,
+ expected_i_version=0,
+ expected_r_version=1,
+ expected_distribution_version=0,
+ expected_fw_version_slot=NEWER_FW_VERSION_SLOT,
+ ),
+ TropicFwUpdateScenario(
+ id="truncated-slot",
+ chip_distribution=0,
+ initial_i_config=0,
+ initial_r_config=1,
+ distribution_slot=0,
+ backup_slot=None,
+ fw_version_slot=TRUNCATED_FW_VERSION_SLOT,
+ expect_failure=True,
+ ),
+ TropicFwUpdateScenario(
+ id="oversized-slot",
+ chip_distribution=0,
+ initial_i_config=0,
+ initial_r_config=1,
+ distribution_slot=0,
+ backup_slot=None,
+ fw_version_slot=OVERSIZED_FW_VERSION_SLOT,
+ expect_failure=True,
+ ),
+ TropicFwUpdateScenario(
+ id="maintenance-already-on",
+ chip_distribution=0,
+ initial_i_config=0,
+ initial_r_config=0,
+ distribution_slot=0,
+ backup_slot=None,
+ fw_version_slot=None,
+ expect_failure=False,
+ expected_i_version=0,
+ expected_r_version=1,
+ expected_distribution_version=0,
+ expected_fw_version_slot=BUNDLED_FW_VERSION_SLOT,
+ ),
+ TropicFwUpdateScenario(
+ id="older-version-maintenance-on",
+ chip_distribution=0,
+ initial_i_config=0,
+ initial_r_config=0,
+ distribution_slot=0,
+ backup_slot=None,
+ fw_version_slot=OLDER_FW_VERSION_SLOT,
+ expect_failure=False,
+ expected_i_version=0,
+ expected_r_version=1,
+ expected_distribution_version=0,
+ expected_fw_version_slot=BUNDLED_FW_VERSION_SLOT,
+ ),
+ TropicFwUpdateScenario(
+ id="interrupted-update",
+ chip_distribution=0,
+ initial_i_config=0,
+ initial_r_config=0,
+ distribution_slot=None,
+ backup_slot=0,
+ fw_version_slot=None,
+ expect_failure=False,
+ expected_i_version=0,
+ expected_r_version=1,
+ expected_distribution_version=0,
+ expected_fw_version_slot=BUNDLED_FW_VERSION_SLOT,
+ ),
+ TropicFwUpdateScenario(
+ id="interrupted-update-v1",
+ chip_distribution=1,
+ initial_i_config=1,
+ initial_r_config=0,
+ distribution_slot=None,
+ backup_slot=1,
+ fw_version_slot=None,
+ expect_failure=False,
+ expected_i_version=1,
+ expected_r_version=1,
+ expected_distribution_version=1,
+ expected_fw_version_slot=BUNDLED_FW_VERSION_SLOT,
+ ),
+ TropicFwUpdateScenario(
+ id="distribution-ahead",
+ chip_distribution=0,
+ initial_i_config=1,
+ initial_r_config=1,
+ distribution_slot=1,
+ backup_slot=None,
+ fw_version_slot=OLDER_FW_VERSION_SLOT,
+ expect_failure=False,
+ expected_i_version=1,
+ expected_r_version=1,
+ expected_distribution_version=1,
+ expected_fw_version_slot=BUNDLED_FW_VERSION_SLOT,
+ ),
+ TropicFwUpdateScenario(
+ id="interrupted-update-distribution-ahead",
+ chip_distribution=0,
+ initial_i_config=1,
+ initial_r_config=0,
+ distribution_slot=None,
+ backup_slot=1,
+ fw_version_slot=None,
+ expect_failure=False,
+ expected_i_version=1,
+ expected_r_version=1,
+ expected_distribution_version=1,
+ expected_fw_version_slot=BUNDLED_FW_VERSION_SLOT,
+ ),
+ TropicFwUpdateScenario(
+ id="maintenance-forbidden",
+ chip_distribution=0,
+ initial_i_config=MAINTENANCE_FORBIDDEN_I_CONFIG,
+ initial_r_config=1,
+ distribution_slot=0,
+ backup_slot=None,
+ fw_version_slot=None,
+ expect_failure=True,
+ ),
+ TropicFwUpdateScenario(
+ id="wrong-silicon-revision",
+ chip_distribution=0,
+ initial_i_config=0,
+ initial_r_config=1,
+ distribution_slot=0,
+ backup_slot=None,
+ fw_version_slot=None,
+ expect_failure=True,
+ silicon_revision=SILICON_REV_ABAB,
+ ),
+ TropicFwUpdateScenario(
+ id="wrong-silicon-revision-maintenance-on",
+ chip_distribution=0,
+ initial_i_config=0,
+ initial_r_config=0,
+ distribution_slot=0,
+ backup_slot=None,
+ fw_version_slot=None,
+ expect_failure=True,
+ silicon_revision=SILICON_REV_ABAB,
+ ),
+]
+
+
+def _initial_i_config(version_or_name: int | str) -> dict[str, int]:
+ if isinstance(version_or_name, int):
+ return expected_config("irreversible_configurations", version_or_name)
+
+ if version_or_name == MAINTENANCE_FORBIDDEN_I_CONFIG:
+ config = expected_config("irreversible_configurations", 0)
+ config["cfg_start_up"] &= ~(1 << MAINTENANCE_ENA_BIT)
+ return config
+
+ raise ValueError(f"Unknown Tropic i_config scenario: {version_or_name}")
+
+
+def _build_tropic_model_config(scenario: TropicFwUpdateScenario) -> dict:
+ config = yaml.safe_load(TROPIC_MODEL_CURRENT_CONFIG.read_text())
+ set_chip_distribution(config, scenario.chip_distribution)
+ set_chip_revision(config, scenario.silicon_revision)
+ config["i_config"] = _initial_i_config(scenario.initial_i_config)
+ config["r_config"] = expected_config(
+ "reversible_configurations", scenario.initial_r_config
+ )
+ set_version_slot(
+ config, TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT, scenario.distribution_slot
+ )
+ set_version_slot(
+ config, TROPIC_CONFIG_BACKUP_DISTRIBUTION_VERSION_SLOT, scenario.backup_slot
+ )
+ set_slot(config, TROPIC_FW_VERSION_SLOT, scenario.fw_version_slot)
+ return config
+
+
+def _check_tropic_model_output(
+ output_path: Path, scenario: TropicFwUpdateScenario
+) -> None:
+ assert output_path.exists(), (
+ f"Tropic model output file was not generated: {output_path}. "
+ f"Profile contents: {sorted(path.name for path in output_path.parent.iterdir())}"
+ )
+ assert scenario.expected_i_version is not None
+ assert scenario.expected_r_version is not None
+ assert scenario.expected_distribution_version is not None
+ assert scenario.expected_fw_version_slot is not None
+
+ output = yaml.safe_load(output_path.read_text())
+ assert output["i_config"] == expected_config(
+ "irreversible_configurations", scenario.expected_i_version
+ )
+ assert output["r_config"] == expected_config(
+ "reversible_configurations", scenario.expected_r_version
+ )
+
+ assert slot_value(
+ output, TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT
+ ) == scenario.expected_distribution_version.to_bytes(4, "big")
+ assert slot_is_erased(output, TROPIC_CONFIG_BACKUP_DISTRIBUTION_VERSION_SLOT)
+ assert (
+ slot_value(output, TROPIC_FW_VERSION_SLOT) == scenario.expected_fw_version_slot
+ )
+
+
+def _check_tropic_model_unchanged(output_path: Path, initial_config: dict) -> None:
+ output = yaml.safe_load(output_path.read_text())
+ assert output["i_config"] == initial_config["i_config"]
+ assert output["r_config"] == initial_config["r_config"]
+
+ for slot in (
+ TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT,
+ TROPIC_CONFIG_BACKUP_DISTRIBUTION_VERSION_SLOT,
+ TROPIC_FW_VERSION_SLOT,
+ ):
+ initial_value = slot_value(initial_config, slot)
+ if initial_value is None:
+ assert slot_is_erased(output, slot)
+ else:
+ assert slot_value(output, slot) == initial_value
+
+
+@model_only("T3W1")
+@pytest.mark.parametrize("scenario", TROPIC_FW_UPDATE_SCENARIOS, ids=lambda s: s.id)
+def test_tropic_fw_update(scenario: TropicFwUpdateScenario) -> None:
+ with tempfile.TemporaryDirectory(
+ prefix="trezor-tropic-fw-update-", delete=delete_profile()
+ ) as temp_dir:
+ config_path = Path(temp_dir) / "tropic_model_config.yml"
+ output_path = Path(temp_dir) / "tropic_model_config_output.yml"
+ initial_config = _build_tropic_model_config(scenario)
+ config_path.write_text(yaml.safe_dump(initial_config, sort_keys=False))
+
+ with TropicModel(
+ profile_dir=temp_dir,
+ configfile=config_path,
+ configfile_output=output_path,
+ logfile=get_logfile("trezor-tropic-model.log", Path(temp_dir)),
+ ) as tropic_model:
+ tropic_model.start()
+
+ if scenario.expect_failure:
+ expectation = pytest.raises(RuntimeError, match="Emulator process died")
+ else:
+ expectation = nullcontext()
+
+ # The emulator only answers once `bootscreen()` has finished, so this
+ # covers the recovery at boot and the update after the unlock.
+ with (
+ expectation,
+ EmulatorWrapper(
+ model="core",
+ profile_dir=temp_dir,
+ tropic_model_port=tropic_model.port,
+ ),
+ ):
+ # wait for start, then exit immediately
+ pass
+
+ if scenario.expect_failure:
+ _check_tropic_model_unchanged(output_path, initial_config)
+ else:
+ _check_tropic_model_output(output_path, scenario)
### tests/upgrade_tests/tropic_common.py
@@ -0,0 +1,125 @@
+# This file is part of the Trezor project.
+#
+# Copyright (C) SatoshiLabs and contributors
+#
+# This library is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the License along with this library.
+# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
+
+import json
+
+from tests.emulators import ROOT
+
+TROPIC_CONFIGS_JSON = (
+ ROOT / "core" / "embed" / "sec" / "tropic" / "config" / "tropic_configs.json"
+)
+
+TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT = 6
+TROPIC_CONFIG_BACKUP_DISTRIBUTION_VERSION_SLOT = 7
+
+CHIP_ID_BATCH_ID_OFFSET = 96
+BATCH_ID_V0 = bytes([0x19, 0x0A, 0x08, 0x10, 0x10])
+BATCH_ID_V1 = bytes([0x19, 0x07, 0x1F, 0x0A, 0x04])
+
+CHIP_ID_SILICON_REV_OFFSET = 28
+SILICON_REV_ABAB = b"ABAB"
+SILICON_REV_ACAB = b"ACAB"
+
+
+def _config_to_numbers(config: dict, irreversible: bool) -> dict[str, int]:
+ numbers = {}
+ for category, category_config in config.items():
+ number = 0xFFFFFFFF if irreversible else 0
+ settings = category_config["setting"]
+
+ if "uap" not in category:
+ for details in settings.values():
+ if irreversible:
+ if not details["value"]:
+ number &= ~(1 << details["bit"])
+ elif details["value"]:
+ number |= 1 << details["bit"]
+ else:
+ for i in range(4):
+ for details in settings[f"pairing_key_{i}"].values():
+ if irreversible:
+ if not details["value"]:
+ number &= ~(1 << details["bit"])
+ elif details["value"]:
+ number |= 1 << details["bit"]
+
+ numbers[category] = number
+ return numbers
+
+
+def expected_config(config_type: str, version: int) -> dict[str, int]:
+ configs = json.loads(TROPIC_CONFIGS_JSON.read_text())
+ config_versions = configs[config_type]
+ irreversible = config_type == "irreversible_configurations"
+
+ for versioned_config in config_versions:
+ if versioned_config["version"] == version:
+ return _config_to_numbers(versioned_config["config"], irreversible)
+
+ raise ValueError(f"Missing Tropic {config_type} version {version}")
+
+
+def set_chip_id_field(config: dict, offset: int, value: bytes) -> None:
+ chip_id = bytearray(config["chip_id"])
+ chip_id[offset : offset + len(value)] = value
+ config["chip_id"] = bytes(chip_id)
+
+
+def set_chip_distribution(config: dict, distribution_version: int) -> None:
+ batch_id = BATCH_ID_V1 if distribution_version == 1 else BATCH_ID_V0
+ set_chip_id_field(config, CHIP_ID_BATCH_ID_OFFSET, batch_id)
+
+
+def set_chip_revision(config: dict, silicon_revision: bytes) -> None:
+ set_chip_id_field(config, CHIP_ID_SILICON_REV_OFFSET, silicon_revision)
+
+
+def set_slot(config: dict, slot: int, value: bytes | None) -> None:
+ r_user_data = config.setdefault("r_user_data", {})
+ if value is None:
+ r_user_data.pop(slot, None)
+ return
+
+ r_user_data[slot] = {"value": value}
+
+
+def set_version_slot(config: dict, slot: int, version: int | None) -> None:
+ """Write a 4-byte big-endian version into `slot`, or erase it."""
+ set_slot(config, slot, None if version is None else version.to_bytes(4, "big"))
+
+
+def _slot_config(config: dict, slot: int) -> dict | None:
+ return (config.get("r_user_data") or {}).get(slot)
+
+
+def slot_value(config: dict, slot: int) -> bytes | None:
+ slot_config = _slot_config(config, slot)
+ if not slot_config:
+ return None
+ return slot_config.get("value")
+
+
+def slot_is_erased(config: dict, slot: int) -> bool:
+ slot_config = _slot_config(config, slot)
+ if not slot_config:
+ return True
+ if slot_config.get("free") is True:
+ return True
+
+ value = slot_config.get("value")
+ if value in (None, b""):
+ return True
+ return all(byte == 0xFF for byte in value)Why this scored 57/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.