feat(core/prodtest): Check for device S/N in device certificates.
What changed, and why it matters
This commit adds a production-line safety check in Trezor's internal 'prodtest' firmware. Before writing a device-unique X.509 certificate to the device, the firmware now verifies that the certificate's subject serial number matches the device's own serial number stored in OTP. It also centralizes serial-number reading into a helper function. This is a hardening/validation improvement rather than a fix for an active user-facing vulnerability.
No immediate user action required. Treat as a manufacturing/provisioning hardening change. Reviewers should verify that get_device_sn correctly handles unterminated OTP data and that the certificate parser cannot be confused by multi-RDN subject DNs or unexpected string types.
Security signals we found
New certificate subject serialNumber validation against device OTP serial number
Refactoring of device serial number retrieval into a bounded helper with MAX_DEVICE_SN_SIZE limit
Addition of DER_PRINTABLE_STRING constant for X.509 parsing
Documentation updated to enforce ordering of provisioning commands
Evidence from the diff
The patch modifies the prodtest command handler (core/embed/projects/prodtest/cmd/common.c) to parse the serialNumber attribute (OID 2.5.4.5) from the subject DN of a device certificate and compare it against the device serial number read from OTP. A new helper get_device_sn() is added to unit_properties and used both in certificate validation and in the prodtest homescreen. The change is gated with an #if excluding T3B1/T3T1 models. DER printable string support is added to crypto/der.h. README instructions are updated to require otp-device-sn-write before certificate write commands.
Changed components
core/embed/projects/prodtest/cmd/common.ccore/embed/projects/prodtest/main.ccore/embed/util/unit_properties (stm32 and unix implementations)crypto/der.hcore/embed/projects/prodtest/README.mdInspect captured patch +122 / −39
diff --git a/core/embed/projects/prodtest/README.md b/core/embed/projects/prodtest/README.md
index db9c09b4..4f091fc7 100644
--- a/core/embed/projects/prodtest/README.md
+++ b/core/embed/projects/prodtest/README.md
@@ -700,7 +700,8 @@ OK 638c8a83ddc8fd84cddf5a0a4fa3d9615146cd341685dca942bab1132c2bc99b
```
### secrets-certdev-write
-Writes the X.509 device attestation certificate issued by the Trezor Company for the attestation key stored in MCU.
+Writes the X.509 device attestation certificate issued by the Trezor Company for the attestation key stored in the MCU.
+The `otp-device-sn-write` command must be executed before calling this command.
Example:
```
@@ -709,7 +710,7 @@ OK
```
### secrets-certdev-read
-Retrieves the X.509 device attestation certificate issued by the Trezor Company for the attestation key stored in MCU.
+Retrieves the X.509 device attestation certificate issued by the Trezor Company for the attestation key stored in the MCU.
Example:
```
@@ -746,6 +747,7 @@ OK <hexadecimal string>
### optiga-certdev-write
Writes the X.509 certificate issued by the Trezor Company for the device attestation key stored in Optiga.
+The `otp-device-sn-write` command must be executed before calling this command.
Example:
```
@@ -1008,7 +1010,7 @@ OK 00000300
### tropic-get-chip-id
-Reads the Tropic chip ID. The command returns `OK` followed by the chip ID.
+Reads the Tropic chip ID. The command returns `OK` followed by the 128-byte serialization of `lt_chip_id_t`.
Example:
```
@@ -1119,6 +1121,7 @@ OK <hexadecimal string>
### tropic-certdev-write
Writes the X.509 certificate issued by the Trezor Company for the device attestation key stored in Tropic.
+The `otp-device-sn-write` command must be executed before calling this command.
Example:
```
diff --git a/core/embed/projects/prodtest/cmd/common.c b/core/embed/projects/prodtest/cmd/common.c
index 4d0186f5..ed772708 100644
--- a/core/embed/projects/prodtest/cmd/common.c
+++ b/core/embed/projects/prodtest/cmd/common.c
@@ -19,6 +19,7 @@
#include <trezor_model.h>
#include <trezor_rtl.h>
+#include <util/unit_properties.h>
#include "common.h"
@@ -63,6 +64,13 @@ static const uint8_t OID_COMMON_NAME[] = {
0x55, 0x04, 0x03, // corresponds to commonName in X.509
};
+#if !(defined TREZOR_MODEL_T3B1 || defined TREZOR_MODEL_T3T1)
+static const uint8_t OID_SERIAL_NUMBER[] = {
+ 0x06, 0x03, // an OID of 3 bytes
+ 0x55, 0x04, 0x05, // corresponds to serialNumber in X.509
+};
+#endif
+
static const uint8_t SUBJECT_COMMON_NAME[] = {
#ifdef TREZOR_MODEL_T2B1
'T', '2', 'B', '1', ' ', 'T', 'r', 'e', 'z', 'o', 'r', ' ', 'S', 'a', 'f', 'e', ' ', '3',
@@ -184,44 +192,51 @@ static bool get_authority_key_digest(cli_t* cli, DER_ITEM* tbs_cert,
return true;
}
-static bool get_common_name(DER_ITEM* name, const uint8_t** common_name,
- size_t* common_name_size) {
+static bool get_name_attribute(DER_ITEM* name, const uint8_t* type,
+ size_t type_size, const uint8_t** value,
+ size_t* value_size) {
if (name->id != DER_SEQUENCE) {
return false;
}
- DER_ITEM distinguished_name = {0};
- if (!der_read_item(&name->buf, &distinguished_name) ||
- distinguished_name.id != DER_SET) {
- return false;
- }
+ DER_ITEM relative_distinguished_name = {0};
+ while (der_read_item(&name->buf, &relative_distinguished_name)) {
+ if (relative_distinguished_name.id != DER_SET) {
+ return false;
+ }
- DER_ITEM attribute = {0};
- if (!der_read_item(&distinguished_name.buf, &attribute) ||
- attribute.id != DER_SEQUENCE) {
- return false;
- }
+ DER_ITEM attribute = {0};
+ if (!der_read_item(&relative_distinguished_name.buf, &attribute) ||
+ attribute.id != DER_SEQUENCE) {
+ return false;
+ }
- DER_ITEM attribute_type = {0};
- if (!der_read_item(&attribute.buf, &attribute_type) ||
- attribute_type.buf.size != sizeof(OID_COMMON_NAME) ||
- memcmp(attribute_type.buf.data, OID_COMMON_NAME,
- sizeof(OID_COMMON_NAME)) != 0) {
- return false;
- }
+ DER_ITEM attribute_type = {0};
+ if (!der_read_item(&attribute.buf, &attribute_type)) {
+ return false;
+ }
- DER_ITEM attribute_value = {0};
- if (!der_read_item(&attribute.buf, &attribute_value) ||
- attribute_value.id != DER_UTF8_STRING) {
- return false;
- }
+ if (attribute_type.buf.size != type_size ||
+ memcmp(attribute_type.buf.data, type, type_size) != 0) {
+ continue;
+ }
- if (!buffer_ptr(&attribute_value.buf, common_name)) {
- return false;
+ DER_ITEM attribute_value = {0};
+ if (!der_read_item(&attribute.buf, &attribute_value) ||
+ (attribute_value.id != DER_UTF8_STRING &&
+ attribute_value.id != DER_PRINTABLE_STRING)) {
+ return false;
+ }
+
+ if (!buffer_ptr(&attribute_value.buf, value)) {
+ return false;
+ }
+ *value_size = buffer_remaining(&attribute_value.buf);
+ return true;
}
- *common_name_size = buffer_remaining(&attribute_value.buf);
- return true;
+ // Attribute not found.
+ return false;
}
static bool verify_signature(alg_id_t alg_id, const uint8_t* pub_key,
@@ -333,7 +348,9 @@ bool check_cert_chain(cli_t* cli, const uint8_t* chain, size_t chain_size,
// Check the common name of the subject of the device certificate.
const uint8_t* common_name = NULL;
size_t common_name_size = 0;
- if (!get_common_name(&subject, &common_name, &common_name_size) ||
+ if (!get_name_attribute(&subject, OID_COMMON_NAME,
+ sizeof(OID_COMMON_NAME), &common_name,
+ &common_name_size) ||
common_name_size != sizeof(SUBJECT_COMMON_NAME) ||
memcmp(common_name, SUBJECT_COMMON_NAME,
sizeof(SUBJECT_COMMON_NAME)) != 0) {
@@ -341,6 +358,33 @@ bool check_cert_chain(cli_t* cli, const uint8_t* chain, size_t chain_size,
"check_device_cert_chain, invalid common name.");
return false;
}
+
+#if !(defined TREZOR_MODEL_T3B1 || defined TREZOR_MODEL_T3T1)
+ // Check that the serial number of the subject, matches the device.
+ uint8_t device_sn[MAX_DEVICE_SN_SIZE] = {0};
+ size_t device_sn_size = 0;
+ if (!get_device_sn(device_sn, sizeof(device_sn), &device_sn_size) ||
+ device_sn_size == 0) {
+ cli_error(cli, CLI_ERROR,
+ "check_device_cert_chain, device_sn not set.");
+ }
+
+ const uint8_t* subject_sn = NULL;
+ size_t subject_sn_size = 0;
+ if (!get_name_attribute(&subject, OID_SERIAL_NUMBER,
+ sizeof(OID_SERIAL_NUMBER), &subject_sn,
+ &subject_sn_size)) {
+ cli_error(cli, CLI_ERROR,
+ "check_device_cert_chain, device_sn not set.");
+ }
+
+ if (subject_sn_size != device_sn_size ||
+ memcmp(subject_sn, device_sn, device_sn_size) != 0) {
+ cli_error(cli, CLI_ERROR,
+ "check_device_cert_chain, serial number mismatch.");
+ return false;
+ }
+#endif
}
// Read the Subject Public Key Info.
diff --git a/core/embed/projects/prodtest/main.c b/core/embed/projects/prodtest/main.c
index 5488b441..d03f2413 100644
--- a/core/embed/projects/prodtest/main.c
+++ b/core/embed/projects/prodtest/main.c
@@ -197,13 +197,11 @@ void prodtest_show_homescreen(void) {
memset(&g_layout, 0, sizeof(g_layout));
g_layout.set = true;
- static char device_sn[FLASH_OTP_BLOCK_SIZE] = {0};
-
- if (sectrue == flash_otp_read(FLASH_OTP_BLOCK_DEVICE_SN, 0,
- (uint8_t *)device_sn, sizeof(device_sn)) &&
- (device_sn[0] != 0xFF)) {
- screen_prodtest_welcome(&g_layout.layout, device_sn,
- strnlen(device_sn, sizeof(device_sn) - 1));
+ static char device_sn[MAX_DEVICE_SN_SIZE] = {0};
+ size_t device_sn_size = 0;
+ if (get_device_sn((uint8_t *)device_sn, sizeof(device_sn) - 1,
+ &device_sn_size)) {
+ screen_prodtest_welcome(&g_layout.layout, device_sn, device_sn_size);
} else {
screen_prodtest_welcome(&g_layout.layout, NULL, 0);
}
diff --git a/core/embed/util/unit_properties/inc/util/unit_properties.h b/core/embed/util/unit_properties/inc/util/unit_properties.h
index be813ff1..163302b9 100644
--- a/core/embed/util/unit_properties/inc/util/unit_properties.h
+++ b/core/embed/util/unit_properties/inc/util/unit_properties.h
@@ -21,6 +21,8 @@
#include <trezor_types.h>
+#define MAX_DEVICE_SN_SIZE 31
+
#ifdef SECURE_MODE
// Initializes module a detects the unit properties
@@ -64,3 +66,6 @@ void unit_properties_get(unit_properties_t* props);
// Gets a pointer to the static unit properties structure
const unit_properties_t* unit_properties(void);
+
+bool get_device_sn(uint8_t* device_sn, size_t max_device_sn_size,
+ size_t* device_sn_size);
diff --git a/core/embed/util/unit_properties/stm32/unit_properties.c b/core/embed/util/unit_properties/stm32/unit_properties.c
index d7716472..37712732 100644
--- a/core/embed/util/unit_properties/stm32/unit_properties.c
+++ b/core/embed/util/unit_properties/stm32/unit_properties.c
@@ -160,6 +160,26 @@ void unit_properties_get(unit_properties_t* props) {
*props = drv->cache;
}
+bool get_device_sn(uint8_t* device_sn, size_t max_device_sn_size,
+ size_t* device_sn_size) {
+ uint8_t block[FLASH_OTP_BLOCK_SIZE] = {0};
+ // The OTP block should contain a null-terminated string when set.
+ if (sectrue !=
+ flash_otp_read(FLASH_OTP_BLOCK_DEVICE_SN, 0, block, sizeof(block)) ||
+ block[0] == 0xFF) {
+ return false;
+ }
+
+ size_t len = strnlen((char*)block, sizeof(block));
+ if (len > max_device_sn_size) {
+ return false;
+ }
+
+ memcpy(device_sn, block, len);
+ *device_sn_size = len;
+ return true;
+}
+
#endif // SECURE_MODE
const unit_properties_t* unit_properties(void) {
diff --git a/core/embed/util/unit_properties/unix/unit_properties.c b/core/embed/util/unit_properties/unix/unit_properties.c
index c6585d2b..f161a269 100644
--- a/core/embed/util/unit_properties/unix/unit_properties.c
+++ b/core/embed/util/unit_properties/unix/unit_properties.c
@@ -78,3 +78,15 @@ 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) {
+ uint8_t sn[] = "12345678901234";
+ if (max_device_sn_size < sizeof(sn) - 1) {
+ return false;
+ }
+
+ memcpy(device_sn, sn, sizeof(sn) - 1);
+ *device_sn_size = sizeof(sn) - 1;
+ return true;
+}
diff --git a/crypto/der.h b/crypto/der.h
index 1888d8a7..2836a3c6 100644
--- a/crypto/der.h
+++ b/crypto/der.h
@@ -33,6 +33,7 @@
#define DER_SEQUENCE 0x30
#define DER_SET 0x31
#define DER_UTF8_STRING 0x0c
+#define DER_PRINTABLE_STRING 0x13
#define DER_INTEGER 0x02
#define DER_BIT_STRING 0x03
#define DER_OCTET_STRING 0x04
Why this scored 26/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.