feat(core): allow rework of unit variant
What changed, and why it matters
This commit adds a one-time 'rework' feature to the Trezor production-test tool, allowing a factory worker to overwrite a device's variant (color, Bitcoin-only flag, packaging) if it was originally written incorrectly. The change is intended for manufacturing repair, not normal user operation. It does not appear to be a security patch; it is a production-process feature.
No immediate action required. Treat as a manufacturing-feature commit. If reviewing for supply-chain assurance, verify that prodtest firmware is only used in controlled factory environments and that the --rework option cannot be invoked by end-user firmware or unauthorized tooling.
Security signals we found
New OTP block for one-time variant correction
Rework write requires original variant block to be locked and new value to differ
Rework block itself is locked after writing
Runtime code prefers locked rework block over original variant block
Feature is limited to prodtest firmware CLI, not end-user firmware
Evidence from the diff
The patch introduces a new OTP block FLASH_OTP_BLOCK_DEVICE_VARIANT_REWORK and a –rework flag for the prodtest otp-variant-write command. When –rework is used, the variant is written to the new OTP block instead of the original block, but only if the original block is already locked and the new value differs. The new block is then locked. Runtime unit-property detection now prefers the rework block when it is locked and its first byte is not 0xFF. The change is gated by the production-test CLI and is not exposed to end-user firmware.
Changed components
core/embed/projects/prodtest/cmd/prodtest_otp_variant.ccore/embed/util/unit_properties/stm32/unit_properties.ccore/embed/models/otp_layout.hcore/embed/util/unit_properties/unix/unit_properties.c (renamed function only)Inspect captured patch +69 / −10
diff --git a/core/embed/models/otp_layout.h b/core/embed/models/otp_layout.h
index cd299c6bc..0557f244a 100644
--- a/core/embed/models/otp_layout.h
+++ b/core/embed/models/otp_layout.h
@@ -27,3 +27,4 @@
#define FLASH_OTP_BLOCK_DEVICE_VARIANT 4
#define FLASH_OTP_BLOCK_FIRMWARE_VERSION 5
#define FLASH_OTP_BLOCK_DEVICE_SN 6
+#define FLASH_OTP_BLOCK_DEVICE_VARIANT_REWORK 7
diff --git a/core/embed/projects/prodtest/.changelog.d/5809.added b/core/embed/projects/prodtest/.changelog.d/5809.added
new file mode 100644
index 000000000..bc8e70dd5
--- /dev/null
+++ b/core/embed/projects/prodtest/.changelog.d/5809.added
@@ -0,0 +1 @@
+Allow rework of unit variant.
diff --git a/core/embed/projects/prodtest/README.md b/core/embed/projects/prodtest/README.md
index a8be461be..6a5691307 100644
--- a/core/embed/projects/prodtest/README.md
+++ b/core/embed/projects/prodtest/README.md
@@ -638,6 +638,7 @@ Currently, three values are required during production:
`otp-variant-write <unit_color> <unit_btconly> <unit_packaging>`.
In non-production firmware, you must include `--execute` as the last parameter to write the data to the OTP memory. Conversely, in production firmware, you can use `--dry-run` as the last parameter to simulate the command without actually writing to the OTP memory.
+You can also use `--rework` to fix an incorrectly written value. This is only usable once.
Example (to write 3 bytes into OTP memory):
```
diff --git a/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c b/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c
index c52149be5..249e6f525 100644
--- a/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c
+++ b/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c
@@ -40,6 +40,7 @@ static void prodtest_otp_variant_read(cli_t* cli) {
}
uint8_t block[FLASH_OTP_BLOCK_SIZE] = {0};
+ uint8_t block_rework[FLASH_OTP_BLOCK_SIZE] = {0};
cli_trace(cli, "Reading device OTP memory...");
@@ -49,6 +50,17 @@ static void prodtest_otp_variant_read(cli_t* cli) {
return;
}
+ if (sectrue != flash_otp_read(FLASH_OTP_BLOCK_DEVICE_VARIANT_REWORK, 0,
+ block_rework, sizeof(block_rework))) {
+ cli_error(cli, CLI_ERROR, "Failed to read OTP memory.");
+ return;
+ }
+
+ if (block_rework[0] != 0xFF) {
+ cli_trace(cli, "Rework block present, using it instead of the original.");
+ memcpy(block, block_rework, sizeof(block));
+ }
+
char block_hex[FLASH_OTP_BLOCK_SIZE * 2 + 1];
if (!cstr_encode_hex(block_hex, sizeof(block_hex), block, sizeof(block))) {
@@ -83,6 +95,7 @@ static void prodtest_otp_variant_write(cli_t* cli) {
#else
bool dry_run = true;
#endif
+ bool rework = false;
int arg_idx = 0;
int val_count = 0;
@@ -95,6 +108,9 @@ static void prodtest_otp_variant_write(cli_t* cli) {
if (strcmp(arg, "--execute") == 0) {
dry_run = false;
+ } else if (strcmp(arg, "--rework") == 0) {
+ dry_run = false;
+ rework = true;
} else if (strcmp(arg, "--dry-run") == 0) {
dry_run = true;
} else if (!cstr_parse_uint32(arg, 0, &val) || val > 255) {
@@ -156,7 +172,36 @@ static void prodtest_otp_variant_write(cli_t* cli) {
}
#endif
- if (sectrue == flash_otp_is_locked(FLASH_OTP_BLOCK_DEVICE_VARIANT)) {
+ uint8_t block_num = FLASH_OTP_BLOCK_DEVICE_VARIANT;
+
+ if (rework) {
+ block_num = FLASH_OTP_BLOCK_DEVICE_VARIANT_REWORK;
+
+ if (sectrue == flash_otp_is_locked(block_num)) {
+ cli_error(cli, CLI_ERROR_LOCKED,
+ "OTP rework block is locked and cannot be written again.");
+ return;
+ }
+
+ if (sectrue != flash_otp_is_locked(FLASH_OTP_BLOCK_DEVICE_VARIANT)) {
+ cli_error(cli, CLI_ERROR_LOCKED,
+ "Variant first block in not locked, rework not allowed.");
+ return;
+ }
+
+ uint8_t block_read[FLASH_OTP_BLOCK_SIZE] = {0};
+ if (sectrue != flash_otp_read(FLASH_OTP_BLOCK_DEVICE_VARIANT, 0, block_read,
+ sizeof(block_read))) {
+ cli_error(cli, CLI_ERROR, "Failed to read OTP memory.");
+ return;
+ }
+ if (memcmp(block_read, block, sizeof(block_read)) == 0) {
+ cli_error(cli, CLI_ERROR, "Rework not needed, already up to date.");
+ return;
+ }
+ }
+
+ if (sectrue == flash_otp_is_locked(block_num)) {
cli_error(cli, CLI_ERROR_LOCKED,
"OTP block is locked and cannot be written again.");
return;
@@ -172,8 +217,7 @@ static void prodtest_otp_variant_write(cli_t* cli) {
cli_trace(cli, "Bytes written: %s", block_hex);
if (!dry_run) {
- if (sectrue != flash_otp_write(FLASH_OTP_BLOCK_DEVICE_VARIANT, 0, block,
- sizeof(block))) {
+ if (sectrue != flash_otp_write(block_num, 0, block, sizeof(block))) {
cli_error(cli, CLI_ERROR, "Failed to write OTP block.");
return;
}
@@ -182,7 +226,7 @@ static void prodtest_otp_variant_write(cli_t* cli) {
cli_trace(cli, "Locking OTP block...");
if (!dry_run) {
- if (sectrue != flash_otp_lock(FLASH_OTP_BLOCK_DEVICE_VARIANT)) {
+ if (sectrue != flash_otp_lock(block_num)) {
cli_error(cli, CLI_ERROR, "Failed to lock the OTP block.");
return;
}
@@ -205,5 +249,5 @@ PRODTEST_CLI_CMD(
.name = "otp-variant-write",
.func = prodtest_otp_variant_write,
.info = "Write the device variant info into OTP memory",
- .args = "<values...> [--execute | --dry-run]"
+ .args = "<values...> [--execute | --dry-run | --rework]"
);
diff --git a/core/embed/util/unit_properties/stm32/unit_properties.c b/core/embed/util/unit_properties/stm32/unit_properties.c
index 978f50ec1..c98546ab7 100644
--- a/core/embed/util/unit_properties/stm32/unit_properties.c
+++ b/core/embed/util/unit_properties/stm32/unit_properties.c
@@ -92,20 +92,32 @@ static bool get_production_date(int* year) {
static bool detect_properties(unit_properties_t* props) {
uint8_t otp_data[FLASH_OTP_BLOCK_SIZE];
- props->locked = flash_otp_is_locked(FLASH_OTP_BLOCK_DEVICE_VARIANT);
+ props->locked =
+ sectrue == flash_otp_is_locked(FLASH_OTP_BLOCK_DEVICE_VARIANT);
if (sectrue != flash_otp_read(FLASH_OTP_BLOCK_DEVICE_VARIANT, 0, otp_data,
FLASH_OTP_BLOCK_SIZE)) {
return false;
}
+ if (sectrue == flash_otp_is_locked(FLASH_OTP_BLOCK_DEVICE_VARIANT_REWORK)) {
+ uint8_t otp_rework_data[FLASH_OTP_BLOCK_SIZE];
+ if (sectrue != flash_otp_read(FLASH_OTP_BLOCK_DEVICE_VARIANT_REWORK, 0,
+ otp_rework_data, FLASH_OTP_BLOCK_SIZE)) {
+ return false;
+ }
+ if (otp_rework_data[0] != 0xFF) {
+ memcpy(otp_data, otp_rework_data, sizeof(otp_rework_data));
+ }
+ }
+
switch (otp_data[0]) {
case 0xFF:
- // OTP block were not written yet, keep the defaults
+ // OTP block was not written yet, keep the defaults
break;
case 0x01:
- // The field were gradually added to the OTP block over time.
+ // The fields were gradually added to the OTP block over time.
// Unused trailing bytes were always set to 0x00.
props->color = otp_data[1];
props->color_is_valid = true;
diff --git a/core/embed/util/unit_properties/unix/unit_properties.c b/core/embed/util/unit_properties/unix/unit_properties.c
index f161a2699..6224c08f1 100644
--- a/core/embed/util/unit_properties/unix/unit_properties.c
+++ b/core/embed/util/unit_properties/unix/unit_properties.c
@@ -79,8 +79,8 @@ const unit_properties_t* unit_properties(void) {
return &cache;
}
-bool get_device_sn(uint8_t* device_sn, size_t max_device_sn_size,
- size_t* device_sn_size) {
+bool unit_properties_get_sn(uint8_t* device_sn, size_t max_device_sn_size,
+ size_t* device_sn_size) {
uint8_t sn[] = "12345678901234";
if (max_device_sn_size < sizeof(sn) - 1) {
return false;
Why this scored 23/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.