feat(core): introduce explicit manufacturing mode exit
What changed, and why it matters
This commit adds a new factory/production tool command that permanently exits 'manufacturing mode' by writing and locking a special one-time-programmable memory block. It also reorganizes how each Trezor model defines its OTP memory layout and unit properties. The change is a feature addition for the production-test firmware, not a fix for a known exploitable bug in user-facing firmware. It does not appear to introduce an obvious vulnerability, but it changes security-relevant manufacturing state logic.
Review the manufacturing-lock write command for authorization checks and ensure it is only reachable in factory/production firmware. Verify that the `locked` property logic correctly handles partially-written T3W1 units and does not accidentally lock out legitimate manufacturing operations or enable unintended mode transitions. Confirm that `unit_properties_deinit()` properly zeroizes sensitive cached data.
Security signals we found
Adds permanent OTP lock command in production-test firmware
Changes how 'manufacturing mode' vs 'normal mode' is detected across models
Introduces model-specific OTP layout definitions
Adds T3W1-specific manufacturing-lock compatibility logic
Touches security-relevant unit_properties driver and flash OTP code
Evidence from the diff
The patch introduces per-model OTP layout headers and unit-properties content headers, replacing a shared global definition. It adds prodtest CLI commands manufacturing-lock-read and manufacturing-lock-write that use FLASH_OTP_BLOCK_MANUFACTURING_LOCK (where defined) to mark a device as having left manufacturing mode. The unit_properties driver now derives the locked flag from either the device-variant OTP block lock or a dedicated manufacturing-lock OTP block, with a T3W1-specific compatibility path. A unit_properties_deinit() function is added to clear cached properties after OTP writes.
Changed components
core/embed/projects/prodtest/cmd/prodtest_manufacturing_lock.ccore/embed/sec/unit_properties/stm32/unit_properties.ccore/embed/sec/unit_properties/unix/unit_properties.ccore/embed/models/*/otp_layout.hcore/embed/models/*/unit_properties_content.hcore/embed/models/otp_layout.hcore/embed/models/unit_properties_content.hcore/embed/models/trezor_model.hcore/site_scons/models/*/__init__.pyInspect captured patch +418 / −27
diff --git a/core/SConscript.prodtest b/core/SConscript.prodtest
index 315cfd7d..bf0c9af1 100644
--- a/core/SConscript.prodtest
+++ b/core/SConscript.prodtest
@@ -200,6 +200,7 @@ SOURCE_PRODTEST = [
'embed/projects/prodtest/cmd/prodtest_button.c',
'embed/projects/prodtest/cmd/prodtest_crc.c',
'embed/projects/prodtest/cmd/prodtest_display.c',
+ 'embed/projects/prodtest/cmd/prodtest_manufacturing_lock.c',
'embed/projects/prodtest/cmd/prodtest_prodtest.c',
'embed/projects/prodtest/cmd/prodtest_backup_ram.c',
'embed/projects/prodtest/cmd/prodtest_get_cpuid.c',
diff --git a/core/embed/models/D001/otp_layout.h b/core/embed/models/D001/otp_layout.h
new file mode 100644
index 00000000..97263c62
--- /dev/null
+++ b/core/embed/models/D001/otp_layout.h
@@ -0,0 +1,12 @@
+#pragma once
+
+// OTP blocks allocation
+#define FLASH_OTP_BLOCK_BATCH 0
+#define FLASH_OTP_BLOCK_BOOTLOADER_VERSION 1
+#define FLASH_OTP_BLOCK_VENDOR_HEADER_LOCK 2
+#define FLASH_OTP_BLOCK_RANDOMNESS 3
+#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
+#define FLASH_OTP_BLOCK_MASTER_KEY 8
diff --git a/core/embed/models/D001/test_bindgen_macros.txt b/core/embed/models/D001/test_bindgen_macros.txt
index 74198c75..f5dc620f 100644
--- a/core/embed/models/D001/test_bindgen_macros.txt
+++ b/core/embed/models/D001/test_bindgen_macros.txt
@@ -6,3 +6,5 @@
-DDISPLAY_RESY=320,
-DTREZOR_BOARD="D001/boards/stm32f429i-disc1.h",
-DMODEL_HEADER="D001/model_D001.h",
+-DOTP_LAYOUT_HEADER="D001/otp_layout.h",
+-DUNIT_PROPERTIES_CONTENT_HEADER="D001/unit_properties_content.h",
diff --git a/core/embed/models/D001/unit_properties_content.h b/core/embed/models/D001/unit_properties_content.h
new file mode 100644
index 00000000..3eb18d68
--- /dev/null
+++ b/core/embed/models/D001/unit_properties_content.h
@@ -0,0 +1,8 @@
+#pragma once
+
+// OTP device variant block layout
+// byte 0: version (always 0x01)
+#define UNIT_PROPERTIES_BYTE_COLOR 1
+#define UNIT_PROPERTIES_BYTE_BTCONLY 2
+#define UNIT_PROPERTIES_BYTE_PACKAGING 3
+#define UNIT_PROPERTIES_BYTE_BATTERY_TYPE 4
diff --git a/core/embed/models/D002/otp_layout.h b/core/embed/models/D002/otp_layout.h
new file mode 100644
index 00000000..139c63de
--- /dev/null
+++ b/core/embed/models/D002/otp_layout.h
@@ -0,0 +1,13 @@
+#pragma once
+
+// OTP blocks allocation
+#define FLASH_OTP_BLOCK_BATCH 0
+#define FLASH_OTP_BLOCK_BOOTLOADER_VERSION 1
+#define FLASH_OTP_BLOCK_VENDOR_HEADER_LOCK 2
+#define FLASH_OTP_BLOCK_RANDOMNESS 3
+#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
+#define FLASH_OTP_BLOCK_MASTER_KEY 8
+#define FLASH_OTP_BLOCK_MANUFACTURING_LOCK 9
diff --git a/core/embed/models/D002/test_bindgen_macros.txt b/core/embed/models/D002/test_bindgen_macros.txt
index 02323e5d..9a5256e0 100644
--- a/core/embed/models/D002/test_bindgen_macros.txt
+++ b/core/embed/models/D002/test_bindgen_macros.txt
@@ -6,3 +6,5 @@
-DDISPLAY_RESY=520,
-DTREZOR_BOARD="D002/boards/stm32u5g9j-dk.h",
-DMODEL_HEADER="D002/model_D002.h",
+-DOTP_LAYOUT_HEADER="D002/otp_layout.h",
+-DUNIT_PROPERTIES_CONTENT_HEADER="D002/unit_properties_content.h",
diff --git a/core/embed/models/D002/unit_properties_content.h b/core/embed/models/D002/unit_properties_content.h
new file mode 100644
index 00000000..3eb18d68
--- /dev/null
+++ b/core/embed/models/D002/unit_properties_content.h
@@ -0,0 +1,8 @@
+#pragma once
+
+// OTP device variant block layout
+// byte 0: version (always 0x01)
+#define UNIT_PROPERTIES_BYTE_COLOR 1
+#define UNIT_PROPERTIES_BYTE_BTCONLY 2
+#define UNIT_PROPERTIES_BYTE_PACKAGING 3
+#define UNIT_PROPERTIES_BYTE_BATTERY_TYPE 4
diff --git a/core/embed/models/T2B1/otp_layout.h b/core/embed/models/T2B1/otp_layout.h
new file mode 100644
index 00000000..97263c62
--- /dev/null
+++ b/core/embed/models/T2B1/otp_layout.h
@@ -0,0 +1,12 @@
+#pragma once
+
+// OTP blocks allocation
+#define FLASH_OTP_BLOCK_BATCH 0
+#define FLASH_OTP_BLOCK_BOOTLOADER_VERSION 1
+#define FLASH_OTP_BLOCK_VENDOR_HEADER_LOCK 2
+#define FLASH_OTP_BLOCK_RANDOMNESS 3
+#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
+#define FLASH_OTP_BLOCK_MASTER_KEY 8
diff --git a/core/embed/models/T2B1/test_bindgen_macros.txt b/core/embed/models/T2B1/test_bindgen_macros.txt
index fbe397d0..857d46d8 100644
--- a/core/embed/models/T2B1/test_bindgen_macros.txt
+++ b/core/embed/models/T2B1/test_bindgen_macros.txt
@@ -6,3 +6,5 @@
-DDISPLAY_RESY=64,
-DTREZOR_BOARD="T2B1/boards/t2b1-unix.h",
-DMODEL_HEADER="T2B1/model_T2B1.h",
+-DOTP_LAYOUT_HEADER="T2B1/otp_layout.h",
+-DUNIT_PROPERTIES_CONTENT_HEADER="T2B1/unit_properties_content.h",
diff --git a/core/embed/models/T2B1/unit_properties_content.h b/core/embed/models/T2B1/unit_properties_content.h
new file mode 100644
index 00000000..3eb18d68
--- /dev/null
+++ b/core/embed/models/T2B1/unit_properties_content.h
@@ -0,0 +1,8 @@
+#pragma once
+
+// OTP device variant block layout
+// byte 0: version (always 0x01)
+#define UNIT_PROPERTIES_BYTE_COLOR 1
+#define UNIT_PROPERTIES_BYTE_BTCONLY 2
+#define UNIT_PROPERTIES_BYTE_PACKAGING 3
+#define UNIT_PROPERTIES_BYTE_BATTERY_TYPE 4
diff --git a/core/embed/models/T2T1/otp_layout.h b/core/embed/models/T2T1/otp_layout.h
new file mode 100644
index 00000000..97263c62
--- /dev/null
+++ b/core/embed/models/T2T1/otp_layout.h
@@ -0,0 +1,12 @@
+#pragma once
+
+// OTP blocks allocation
+#define FLASH_OTP_BLOCK_BATCH 0
+#define FLASH_OTP_BLOCK_BOOTLOADER_VERSION 1
+#define FLASH_OTP_BLOCK_VENDOR_HEADER_LOCK 2
+#define FLASH_OTP_BLOCK_RANDOMNESS 3
+#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
+#define FLASH_OTP_BLOCK_MASTER_KEY 8
diff --git a/core/embed/models/T2T1/test_bindgen_macros.txt b/core/embed/models/T2T1/test_bindgen_macros.txt
index 2a5c21d2..9776c3b5 100644
--- a/core/embed/models/T2T1/test_bindgen_macros.txt
+++ b/core/embed/models/T2T1/test_bindgen_macros.txt
@@ -6,3 +6,5 @@
-DDISPLAY_RESY=240,
-DTREZOR_BOARD="T2T1/boards/t2t1-unix.h",
-DMODEL_HEADER="T2T1/model_T2T1.h",
+-DOTP_LAYOUT_HEADER="T2T1/otp_layout.h",
+-DUNIT_PROPERTIES_CONTENT_HEADER="T2T1/unit_properties_content.h",
diff --git a/core/embed/models/T2T1/unit_properties_content.h b/core/embed/models/T2T1/unit_properties_content.h
new file mode 100644
index 00000000..879bf16e
--- /dev/null
+++ b/core/embed/models/T2T1/unit_properties_content.h
@@ -0,0 +1,13 @@
+#pragma once
+
+// OTP device variant block layout
+// byte 0: version (always 0x01)
+#define UNIT_PROPERTIES_BYTE_COLOR 1
+#define UNIT_PROPERTIES_BYTE_BTCONLY 2
+#define UNIT_PROPERTIES_BYTE_PACKAGING 3
+#define UNIT_PROPERTIES_BYTE_BATTERY_TYPE 4
+
+// SD hotswap configuration
+// T2T1: SD hotswap is disabled for units produced in 2018 or earlier
+#define UNIT_PROPERTIES_SD_HOTSWAP_ENABLED true
+#define UNIT_PROPERTIES_SD_HOTSWAP_EARLY_PRODUCTION_YEAR 18
diff --git a/core/embed/models/T3B1/otp_layout.h b/core/embed/models/T3B1/otp_layout.h
new file mode 100644
index 00000000..97263c62
--- /dev/null
+++ b/core/embed/models/T3B1/otp_layout.h
@@ -0,0 +1,12 @@
+#pragma once
+
+// OTP blocks allocation
+#define FLASH_OTP_BLOCK_BATCH 0
+#define FLASH_OTP_BLOCK_BOOTLOADER_VERSION 1
+#define FLASH_OTP_BLOCK_VENDOR_HEADER_LOCK 2
+#define FLASH_OTP_BLOCK_RANDOMNESS 3
+#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
+#define FLASH_OTP_BLOCK_MASTER_KEY 8
diff --git a/core/embed/models/T3B1/test_bindgen_macros.txt b/core/embed/models/T3B1/test_bindgen_macros.txt
index 2f9e64eb..41d7e0b6 100644
--- a/core/embed/models/T3B1/test_bindgen_macros.txt
+++ b/core/embed/models/T3B1/test_bindgen_macros.txt
@@ -6,3 +6,5 @@
-DDISPLAY_RESY=64,
-DTREZOR_BOARD="T3B1/boards/t3b1-unix.h",
-DMODEL_HEADER="T3B1/model_T3B1.h",
+-DOTP_LAYOUT_HEADER="T3B1/otp_layout.h",
+-DUNIT_PROPERTIES_CONTENT_HEADER="T3B1/unit_properties_content.h",
diff --git a/core/embed/models/T3B1/unit_properties_content.h b/core/embed/models/T3B1/unit_properties_content.h
new file mode 100644
index 00000000..3eb18d68
--- /dev/null
+++ b/core/embed/models/T3B1/unit_properties_content.h
@@ -0,0 +1,8 @@
+#pragma once
+
+// OTP device variant block layout
+// byte 0: version (always 0x01)
+#define UNIT_PROPERTIES_BYTE_COLOR 1
+#define UNIT_PROPERTIES_BYTE_BTCONLY 2
+#define UNIT_PROPERTIES_BYTE_PACKAGING 3
+#define UNIT_PROPERTIES_BYTE_BATTERY_TYPE 4
diff --git a/core/embed/models/T3T1/otp_layout.h b/core/embed/models/T3T1/otp_layout.h
new file mode 100644
index 00000000..97263c62
--- /dev/null
+++ b/core/embed/models/T3T1/otp_layout.h
@@ -0,0 +1,12 @@
+#pragma once
+
+// OTP blocks allocation
+#define FLASH_OTP_BLOCK_BATCH 0
+#define FLASH_OTP_BLOCK_BOOTLOADER_VERSION 1
+#define FLASH_OTP_BLOCK_VENDOR_HEADER_LOCK 2
+#define FLASH_OTP_BLOCK_RANDOMNESS 3
+#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
+#define FLASH_OTP_BLOCK_MASTER_KEY 8
diff --git a/core/embed/models/T3T1/test_bindgen_macros.txt b/core/embed/models/T3T1/test_bindgen_macros.txt
index d57c7163..ff532cdb 100644
--- a/core/embed/models/T3T1/test_bindgen_macros.txt
+++ b/core/embed/models/T3T1/test_bindgen_macros.txt
@@ -6,3 +6,5 @@
-DDISPLAY_RESY=240,
-DTREZOR_BOARD="T3T1/boards/t3t1-unix.h",
-DMODEL_HEADER="T3T1/model_T3T1.h",
+-DOTP_LAYOUT_HEADER="T3T1/otp_layout.h",
+-DUNIT_PROPERTIES_CONTENT_HEADER="T3T1/unit_properties_content.h",
diff --git a/core/embed/models/T3T1/unit_properties_content.h b/core/embed/models/T3T1/unit_properties_content.h
new file mode 100644
index 00000000..64f0e72c
--- /dev/null
+++ b/core/embed/models/T3T1/unit_properties_content.h
@@ -0,0 +1,11 @@
+#pragma once
+
+// OTP device variant block layout
+// byte 0: version (always 0x01)
+#define UNIT_PROPERTIES_BYTE_COLOR 1
+#define UNIT_PROPERTIES_BYTE_BTCONLY 2
+#define UNIT_PROPERTIES_BYTE_PACKAGING 3
+#define UNIT_PROPERTIES_BYTE_BATTERY_TYPE 4
+
+// SD hotswap configuration
+#define UNIT_PROPERTIES_SD_HOTSWAP_ENABLED true
diff --git a/core/embed/models/T3W1/otp_layout.h b/core/embed/models/T3W1/otp_layout.h
new file mode 100644
index 00000000..3ad41bcf
--- /dev/null
+++ b/core/embed/models/T3W1/otp_layout.h
@@ -0,0 +1,12 @@
+#pragma once
+
+// OTP blocks allocation
+#define FLASH_OTP_BLOCK_BATCH 0
+#define FLASH_OTP_BLOCK_BOOTLOADER_VERSION 1
+#define FLASH_OTP_BLOCK_VENDOR_HEADER_LOCK 2
+#define FLASH_OTP_BLOCK_RANDOMNESS 3
+#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
+#define FLASH_OTP_BLOCK_MANUFACTURING_LOCK 8
diff --git a/core/embed/models/T3W1/test_bindgen_macros.txt b/core/embed/models/T3W1/test_bindgen_macros.txt
index 8e4c6e80..a38de5e7 100644
--- a/core/embed/models/T3W1/test_bindgen_macros.txt
+++ b/core/embed/models/T3W1/test_bindgen_macros.txt
@@ -6,3 +6,5 @@
-DDISPLAY_RESY=520,
-DTREZOR_BOARD="T3W1/boards/t3w1-unix.h",
-DMODEL_HEADER="T3W1/model_T3W1.h",
+-DOTP_LAYOUT_HEADER="T3W1/otp_layout.h",
+-DUNIT_PROPERTIES_CONTENT_HEADER="T3W1/unit_properties_content.h",
diff --git a/core/embed/models/T3W1/unit_properties_content.h b/core/embed/models/T3W1/unit_properties_content.h
new file mode 100644
index 00000000..32d461ab
--- /dev/null
+++ b/core/embed/models/T3W1/unit_properties_content.h
@@ -0,0 +1,9 @@
+#pragma once
+
+// OTP device variant block layout
+// byte 0: version (always 0x01)
+#define UNIT_PROPERTIES_BYTE_COLOR 1
+#define UNIT_PROPERTIES_BYTE_BTCONLY 2
+#define UNIT_PROPERTIES_BYTE_PACKAGING 3
+#define UNIT_PROPERTIES_BYTE_BATTERY_TYPE 4
+#define UNIT_PROPERTIES_MANUFACTURING_LOCK_PRESENT 5
diff --git a/core/embed/models/otp_layout.h b/core/embed/models/otp_layout.h
index 178abb45..9e46ba4c 100644
--- a/core/embed/models/otp_layout.h
+++ b/core/embed/models/otp_layout.h
@@ -19,16 +19,4 @@
#pragma once
-// OTP blocks allocation
-#define FLASH_OTP_BLOCK_BATCH 0
-#define FLASH_OTP_BLOCK_BOOTLOADER_VERSION 1
-#define FLASH_OTP_BLOCK_VENDOR_HEADER_LOCK 2
-#define FLASH_OTP_BLOCK_RANDOMNESS 3
-#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
-
-#ifndef SECRET_PRIVILEGED_MASTER_KEY_SLOT
-#define FLASH_OTP_BLOCK_MASTER_KEY 8
-#endif
+#include OTP_LAYOUT_HEADER
diff --git a/core/embed/models/trezor_model.h b/core/embed/models/trezor_model.h
index 21c420fa..40e1959a 100644
--- a/core/embed/models/trezor_model.h
+++ b/core/embed/models/trezor_model.h
@@ -2,3 +2,4 @@
#include MODEL_HEADER
#include "otp_layout.h"
+#include "unit_properties_content.h"
diff --git a/core/embed/models/unit_properties_content.h b/core/embed/models/unit_properties_content.h
new file mode 100644
index 00000000..d361df36
--- /dev/null
+++ b/core/embed/models/unit_properties_content.h
@@ -0,0 +1,22 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include UNIT_PROPERTIES_CONTENT_HEADER
diff --git a/core/embed/projects/prodtest/.changelog.d/6613.added b/core/embed/projects/prodtest/.changelog.d/6613.added
new file mode 100644
index 00000000..fba76b44
--- /dev/null
+++ b/core/embed/projects/prodtest/.changelog.d/6613.added
@@ -0,0 +1 @@
+[T3W1] Added explicit exit from manufacturing mode.
diff --git a/core/embed/projects/prodtest/README.md b/core/embed/projects/prodtest/README.md
index ea686e07..c7da4811 100644
--- a/core/embed/projects/prodtest/README.md
+++ b/core/embed/projects/prodtest/README.md
@@ -691,6 +691,31 @@ otp-device-sn-write 123456ABCD --dry-run
# Locking OTP block...
```
+### manufacturing-lock-write
+Writes the manufacturing lock into OTP memory, transitioning the device from manufacturing mode to normal mode. Once written, this lock is permanent and cannot be reverted.
+
+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.
+
+Example:
+```
+manufacturing-lock-write --dry-run
+#
+# !!! It's a dry run, OTP will be left unchanged.
+# !!! Use '--execute' switch to write to OTP memory.
+#
+# Writing manufacturing lock into OTP memory...
+OK
+```
+
+### manufacturing-lock-read
+Reads the current manufacturing lock status from OTP memory. Returns `locked` if the device has exited manufacturing mode, or `unlocked` if it is still in manufacturing mode.
+
+Example:
+```
+manufacturing-lock-read
+OK locked
+```
+
### otp-variant-write
Writes up to 31 decimal values, each representing device variant options, to device's OTP memory. Each value must range from 0 to 255.
diff --git a/core/embed/projects/prodtest/cmd/prodtest_manufacturing_lock.c b/core/embed/projects/prodtest/cmd/prodtest_manufacturing_lock.c
new file mode 100644
index 00000000..deb762e5
--- /dev/null
+++ b/core/embed/projects/prodtest/cmd/prodtest_manufacturing_lock.c
@@ -0,0 +1,127 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_bsp.h>
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include <rtl/cli.h>
+#include <sec/unit_properties.h>
+#include <sys/flash_otp.h>
+
+static void prodtest_manufacturing_lock_read(cli_t* cli) {
+ if (cli_arg_count(cli) > 0) {
+ cli_error_arg_count(cli);
+ return;
+ }
+
+ unit_properties_t props = {0};
+ unit_properties_get(&props);
+
+ if (props.locked) {
+ cli_ok(cli, "locked");
+ } else {
+ cli_ok(cli, "unlocked");
+ }
+}
+
+#ifdef FLASH_OTP_BLOCK_MANUFACTURING_LOCK
+
+static void prodtest_manufacturing_lock_write(cli_t* cli) {
+#if PRODUCTION
+ bool dry_run = false;
+#else
+ bool dry_run = true;
+#endif
+
+ int arg_idx = 0;
+ while (arg_idx < cli_arg_count(cli)) {
+ const char* arg = cli_nth_arg(cli, arg_idx++);
+ if (strcmp(arg, "--execute") == 0) {
+ dry_run = false;
+ } else if (strcmp(arg, "--dry-run") == 0) {
+ dry_run = true;
+ } else {
+ cli_error_arg(cli, "Unknown argument: %s", arg);
+ return;
+ }
+ }
+
+ if (sectrue == flash_otp_is_locked(FLASH_OTP_BLOCK_MANUFACTURING_LOCK)) {
+ cli_error(cli, CLI_ERROR_LOCKED, "Manufacturing lock is already set.");
+ return;
+ }
+
+ if (dry_run) {
+ cli_trace(cli, "");
+ cli_trace(cli, "!!! It's a dry run, OTP will be left unchanged.");
+ cli_trace(cli, "!!! Use '--execute' switch to write to OTP memory.");
+ cli_trace(cli, "");
+ }
+
+ // Write a non-0xFF value to mark the block as used
+ uint8_t block[FLASH_OTP_BLOCK_SIZE];
+ memset(block, 0x00, sizeof(block));
+ block[0] = 0x01; // Manufacturing lock marker
+
+ cli_trace(cli, "Writing manufacturing lock into OTP memory...");
+
+ if (!dry_run) {
+ if (sectrue != flash_otp_write(FLASH_OTP_BLOCK_MANUFACTURING_LOCK, 0, block,
+ sizeof(block))) {
+ cli_error(cli, CLI_ERROR, "Failed to write OTP block.");
+ return;
+ }
+
+ cli_trace(cli, "Locking OTP block...");
+
+ if (sectrue != flash_otp_lock(FLASH_OTP_BLOCK_MANUFACTURING_LOCK)) {
+ cli_error(cli, CLI_ERROR, "Failed to lock the OTP block.");
+ return;
+ }
+ }
+
+ // reset cached properties
+ unit_properties_deinit();
+ unit_properties_init();
+
+ cli_ok(cli, "");
+}
+
+#endif
+
+// clang-format off
+
+PRODTEST_CLI_CMD(
+ .name = "manufacturing-lock-read",
+ .func = prodtest_manufacturing_lock_read,
+ .info = "Read the manufacturing lock status from OTP memory",
+ .args = ""
+);
+
+#ifdef FLASH_OTP_BLOCK_MANUFACTURING_LOCK
+
+PRODTEST_CLI_CMD(
+ .name = "manufacturing-lock-write",
+ .func = prodtest_manufacturing_lock_write,
+ .info = "Write the manufacturing lock into OTP memory, exiting manufacturing mode",
+ .args = "[--execute | --dry-run]"
+);
+
+#endif // FLASH_OTP_BLOCK_MANUFACTURING_LOCK
diff --git a/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c b/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c
index 32a3f7fd..8b692b0f 100644
--- a/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c
+++ b/core/embed/projects/prodtest/cmd/prodtest_otp_variant.c
@@ -23,6 +23,7 @@
#include <rtl/cli.h>
#include <rtl/printf.h>
+#include <sec/unit_properties.h>
#include <sys/flash_otp.h>
#include <stdlib.h>
@@ -235,6 +236,10 @@ static void prodtest_otp_variant_write(cli_t* cli) {
}
}
+ // reset cached properties
+ unit_properties_deinit();
+ unit_properties_init();
+
// Respond with an OK message
cli_ok(cli, "");
}
diff --git a/core/embed/projects/prodtest/cmd/prodtest_telemetry.c b/core/embed/projects/prodtest/cmd/prodtest_telemetry.c
index ebddacaa..e955c5bb 100644
--- a/core/embed/projects/prodtest/cmd/prodtest_telemetry.c
+++ b/core/embed/projects/prodtest/cmd/prodtest_telemetry.c
@@ -53,7 +53,9 @@ static void prodtest_telemetry_reset(cli_t* cli) {
}
#if PRODUCTION
- if (unit_properties()->locked) {
+ unit_properties_t props = {0};
+ unit_properties_get(&props);
+ if (props.locked) {
cli_error(cli, CLI_ERROR, "Device is not in manufacturing mode.");
return;
}
diff --git a/core/embed/sec/unit_properties/inc/sec/unit_properties.h b/core/embed/sec/unit_properties/inc/sec/unit_properties.h
index 2ca44559..a064c9a6 100644
--- a/core/embed/sec/unit_properties/inc/sec/unit_properties.h
+++ b/core/embed/sec/unit_properties/inc/sec/unit_properties.h
@@ -33,6 +33,11 @@
*/
bool unit_properties_init(void);
+/**
+ * @brief Deinitializes module
+ */
+void unit_properties_deinit(void);
+
#endif // SECURE_MODE
typedef struct {
diff --git a/core/embed/sec/unit_properties/stm32/unit_properties.c b/core/embed/sec/unit_properties/stm32/unit_properties.c
index dbc717df..0068b27d 100644
--- a/core/embed/sec/unit_properties/stm32/unit_properties.c
+++ b/core/embed/sec/unit_properties/stm32/unit_properties.c
@@ -90,9 +90,6 @@ static bool get_production_date(int* year, int* month, int* day) {
static bool detect_properties(unit_properties_t* props) {
uint8_t otp_data[FLASH_OTP_BLOCK_SIZE];
- 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;
@@ -109,6 +106,10 @@ static bool detect_properties(unit_properties_t* props) {
}
}
+#ifdef UNIT_PROPERTIES_MANUFACTURING_LOCK_PRESENT
+ bool manufacturing_lock_present = false;
+#endif
+
switch (otp_data[0]) {
case 0xFF:
// OTP block was not written yet, keep the defaults
@@ -117,14 +118,18 @@ static bool detect_properties(unit_properties_t* props) {
case 0x01:
// 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 = otp_data[UNIT_PROPERTIES_BYTE_COLOR];
props->color_is_valid = true;
- props->btconly = otp_data[2] == 1;
+ props->btconly = otp_data[UNIT_PROPERTIES_BYTE_BTCONLY] == 1;
props->btconly_is_valid = true;
- props->packaging = otp_data[3];
+ props->packaging = otp_data[UNIT_PROPERTIES_BYTE_PACKAGING];
props->packaging_is_valid = true;
- props->battery_type = otp_data[4];
+ props->battery_type = otp_data[UNIT_PROPERTIES_BYTE_BATTERY_TYPE];
props->battery_type_is_valid = true;
+#ifdef UNIT_PROPERTIES_MANUFACTURING_LOCK_PRESENT
+ manufacturing_lock_present =
+ otp_data[UNIT_PROPERTIES_MANUFACTURING_LOCK_PRESENT] == 1;
+#endif
break;
default:
@@ -132,20 +137,41 @@ static bool detect_properties(unit_properties_t* props) {
break;
}
+#ifdef FLASH_OTP_BLOCK_MANUFACTURING_LOCK
+#ifdef UNIT_PROPERTIES_MANUFACTURING_LOCK_PRESENT
+ // workaround for T3W1, where manufacturing lock was introduced during
+ // production
+ if (manufacturing_lock_present) {
+ props->locked =
+ sectrue == flash_otp_is_locked(FLASH_OTP_BLOCK_MANUFACTURING_LOCK);
+ } else {
+ props->locked =
+ sectrue == flash_otp_is_locked(FLASH_OTP_BLOCK_DEVICE_VARIANT);
+ }
+#else
+ props->locked =
+ sectrue == flash_otp_is_locked(FLASH_OTP_BLOCK_MANUFACTURING_LOCK);
+#endif
+#else
+ props->locked =
+ sectrue == flash_otp_is_locked(FLASH_OTP_BLOCK_DEVICE_VARIANT);
+#endif
+
int production_year = 0, production_month = 0, production_day = 0;
get_production_date(&production_year, &production_month, &production_day);
props->production_date.year = 2000 + production_year;
props->production_date.month = production_month;
props->production_date.day = production_day;
- props->sd_hotswap_enabled = true;
-#ifdef TREZOR_MODEL_T2T1
- // Early produced TTs have a HW bug that prevents hotswapping of the SD card,
- // lets check the build data and decide based on that.
-
- if (production_year <= 18) {
+#ifdef UNIT_PROPERTIES_SD_HOTSWAP_ENABLED
+ props->sd_hotswap_enabled = UNIT_PROPERTIES_SD_HOTSWAP_ENABLED;
+#ifdef UNIT_PROPERTIES_SD_HOTSWAP_EARLY_PRODUCTION_YEAR
+ // Early produced units have a HW bug that prevents hotswapping of the SD
+ // card, lets check the build data and decide based on that.
+ if (production_year <= UNIT_PROPERTIES_SD_HOTSWAP_EARLY_PRODUCTION_YEAR) {
props->sd_hotswap_enabled = false;
}
+#endif
#endif
return true;
@@ -169,6 +195,11 @@ bool unit_properties_init(void) {
return true;
}
+void unit_properties_deinit(void) {
+ unit_properties_driver_t* drv = &g_unit_properties_driver;
+ memset(drv, 0, sizeof(unit_properties_driver_t));
+}
+
void unit_properties_get(unit_properties_t* props) {
unit_properties_driver_t* drv = &g_unit_properties_driver;
diff --git a/core/embed/sec/unit_properties/unix/unit_properties.c b/core/embed/sec/unit_properties/unix/unit_properties.c
index 637f8833..3c4d2cd8 100644
--- a/core/embed/sec/unit_properties/unix/unit_properties.c
+++ b/core/embed/sec/unit_properties/unix/unit_properties.c
@@ -59,6 +59,11 @@ bool unit_properties_init(void) {
return true;
}
+void unit_properties_deinit(void) {
+ unit_properties_driver_t* drv = &g_unit_properties_driver;
+ memset(drv, 0, sizeof(unit_properties_driver_t));
+}
+
void unit_properties_get(unit_properties_t* props) {
unit_properties_driver_t* drv = &g_unit_properties_driver;
diff --git a/core/site_scons/models/D001/__init__.py b/core/site_scons/models/D001/__init__.py
index 2b4a38da..bea20b5d 100644
--- a/core/site_scons/models/D001/__init__.py
+++ b/core/site_scons/models/D001/__init__.py
@@ -16,6 +16,8 @@ def configure_board(
defines += (("MODEL_HEADER", '"D001/model_D001.h"'),)
defines += (("VERSIONS_HEADER", '"D001/versions.h"'),)
+ defines += (("OTP_LAYOUT_HEADER", '"D001/otp_layout.h"'),)
+ defines += (("UNIT_PROPERTIES_CONTENT_HEADER", '"D001/unit_properties_content.h"'),)
return configure(env, features_wanted, defines, sources, paths)
diff --git a/core/site_scons/models/D002/__init__.py b/core/site_scons/models/D002/__init__.py
index 10160244..a788b2e3 100644
--- a/core/site_scons/models/D002/__init__.py
+++ b/core/site_scons/models/D002/__init__.py
@@ -15,6 +15,8 @@ def configure_board(
):
defines += (("MODEL_HEADER", '"D002/model_D002.h"'),)
defines += (("VERSIONS_HEADER", '"D002/versions.h"'),)
+ defines += (("OTP_LAYOUT_HEADER", '"D002/otp_layout.h"'),)
+ defines += (("UNIT_PROPERTIES_CONTENT_HEADER", '"D002/unit_properties_content.h"'),)
return configure(env, features_wanted, defines, sources, paths)
diff --git a/core/site_scons/models/T2B1/__init__.py b/core/site_scons/models/T2B1/__init__.py
index 163c38ec..b1050ed3 100644
--- a/core/site_scons/models/T2B1/__init__.py
+++ b/core/site_scons/models/T2B1/__init__.py
@@ -16,6 +16,8 @@ def configure_board(
):
defines += (("MODEL_HEADER", '"T2B1/model_T2B1.h"'),)
defines += (("VERSIONS_HEADER", '"T2B1/versions.h"'),)
+ defines += (("OTP_LAYOUT_HEADER", '"T2B1/otp_layout.h"'),)
+ defines += (("UNIT_PROPERTIES_CONTENT_HEADER", '"T2B1/unit_properties_content.h"'),)
# Set default revision if None
revision = revision or "10"
diff --git a/core/site_scons/models/T2T1/__init__.py b/core/site_scons/models/T2T1/__init__.py
index 166c9aca..5fdfa80c 100644
--- a/core/site_scons/models/T2T1/__init__.py
+++ b/core/site_scons/models/T2T1/__init__.py
@@ -16,6 +16,8 @@ def configure_board(
):
defines += (("MODEL_HEADER", '"T2T1/model_T2T1.h"'),)
defines += (("VERSIONS_HEADER", '"T2T1/versions.h"'),)
+ defines += (("OTP_LAYOUT_HEADER", '"T2T1/otp_layout.h"'),)
+ defines += (("UNIT_PROPERTIES_CONTENT_HEADER", '"T2T1/unit_properties_content.h"'),)
if revision == "emulator":
return emul(env, features_wanted, defines, sources, paths)
diff --git a/core/site_scons/models/T3B1/__init__.py b/core/site_scons/models/T3B1/__init__.py
index f721e43f..fc13c3e2 100644
--- a/core/site_scons/models/T3B1/__init__.py
+++ b/core/site_scons/models/T3B1/__init__.py
@@ -16,6 +16,8 @@ def configure_board(
):
defines += (("MODEL_HEADER", '"T3B1/model_T3B1.h"'),)
defines += (("VERSIONS_HEADER", '"T3B1/versions.h"'),)
+ defines += (("OTP_LAYOUT_HEADER", '"T3B1/otp_layout.h"'),)
+ defines += (("UNIT_PROPERTIES_CONTENT_HEADER", '"T3B1/unit_properties_content.h"'),)
# Set default revision if None
revision = revision or "B"
diff --git a/core/site_scons/models/T3T1/__init__.py b/core/site_scons/models/T3T1/__init__.py
index a4f5e6c0..4df5a7ff 100644
--- a/core/site_scons/models/T3T1/__init__.py
+++ b/core/site_scons/models/T3T1/__init__.py
@@ -16,6 +16,8 @@ def configure_board(
):
defines += (("MODEL_HEADER", '"T3T1/model_T3T1.h"'),)
defines += (("VERSIONS_HEADER", '"T3T1/versions.h"'),)
+ defines += (("OTP_LAYOUT_HEADER", '"T3T1/otp_layout.h"'),)
+ defines += (("UNIT_PROPERTIES_CONTENT_HEADER", '"T3T1/unit_properties_content.h"'),)
# Set default revision if None
revision = revision or "E"
diff --git a/core/site_scons/models/T3W1/__init__.py b/core/site_scons/models/T3W1/__init__.py
index 0122e845..a57c8791 100644
--- a/core/site_scons/models/T3W1/__init__.py
+++ b/core/site_scons/models/T3W1/__init__.py
@@ -16,6 +16,8 @@ def configure_board(
):
defines += (("MODEL_HEADER", '"T3W1/model_T3W1.h"'),)
defines += (("VERSIONS_HEADER", '"T3W1/versions.h"'),)
+ defines += (("OTP_LAYOUT_HEADER", '"T3W1/otp_layout.h"'),)
+ defines += (("UNIT_PROPERTIES_CONTENT_HEADER", '"T3W1/unit_properties_content.h"'),)
# Set default revision if None
revision = revision or "C"
Why this scored 30/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.