feat(core): introduce secmon downgrade protection
What changed, and why it matters
This commit adds downgrade protection for the Secure Monitor (secmon), a small privileged security component that runs alongside the main firmware on Trezor hardware wallets. Previously, only the bootloader and firmware had downgrade protection via monotonic counters stored in secure flash. This change reserves a third monotonic counter, records the secmon version in the image header, and makes the bootloader reject older or rolled-back secmon images during firmware updates and boot. It is a defensive hardening feature, not a fix for an active vulnerability, and the initial secmon monotonic version starts at 0 so existing images remain compatible.
No immediate user action is required; this is a hardening improvement. Trezor should ensure the new monotonic counter region is provisioned correctly during manufacturing and that the secmon signing pipeline populates the monotonic byte. Reviewers should verify that check_secmon_min_version cannot be bypassed before ensure_secmon_min_version is called, and that the initial value SECMON_MONOTONIC_VERSION 0 does not create a rollback window once the first non-zero secmon version is shipped.
Security signals we found
Adds anti-rollback protection for a previously unprotected security-critical component (Secure Monitor)
Introduces a new monotonic counter slot in secure flash for secmon versioning
Bootloader now rejects secmon images with monotonic version lower than the stored minimum
Firmware update workflow returns a new error code (UPLOAD_ERR_INVALID_SECMON_VERSION) for secmon downgrades
Reorders boot-time checks so version enforcement precedes content validation and persistent counter updates
Changelog fragment explicitly labels the change as 'Secure Monitor downgrade protection'
Evidence from the diff
The patch extends the existing monoctr subsystem with MONOCTR_SECMON_VERSION backed by a new secure-flash region (SECRET_MONOTONIC_COUNTER_2_OFFSET/LEN) on the T3W1 model. The secmon binary header gains a monotonic version byte, parsed by both the C image header struct and the Python secmon tooling. The bootloader now calls check_secmon_min_version() before accepting a secmon image in firmware-update workflow and before jumping to firmware, and ensure_secmon_min_version() persists the new minimum after successful validation. The firmware downgrade check is also reordered so that version checks happen before content validation, and the persistent update is done after all checks pass.
Changed components
Trezor Core bootloader (fw_check.c, main.c, version_check.c/h, workflow/wf_firmware_update.c)Secure Monitor image header and build tooling (secmon/header.S, prodtest/secmon_header.S, secmon/version.h, python/src/trezorlib/firmware/secmon.py)Monotonic counter driver (core/embed/sec/monoctr/inc/sec/monoctr.h, core/embed/sec/monoctr/stm32u5/monoctr.c)T3W1 model layout/versions (core/embed/models/T3W1/secret_layout.h, core/embed/models/T3W1/versions.h)Shared image header definition (core/embed/util/image/inc/util/image.h)Inspect captured patch +72 / −6
diff --git a/core/embed/models/T3W1/secret_layout.h b/core/embed/models/T3W1/secret_layout.h
index 0fedd8ab..4f7e65c4 100644
--- a/core/embed/models/T3W1/secret_layout.h
+++ b/core/embed/models/T3W1/secret_layout.h
@@ -47,6 +47,9 @@
#define SECRET_MCU_DEVICE_CERT_OFFSET 0x870
#define SECRET_MCU_DEVICE_CERT_SIZE 0x1000
+#define SECRET_MONOTONIC_COUNTER_2_OFFSET 0x1870
+#define SECRET_MONOTONIC_COUNTER_2_LEN 0x400
+
#define SECRET_LOCK_SLOT_OFFSET 0x1FF0
#define SECRET_LOCK_SLOT_LEN 0x10
diff --git a/core/embed/models/T3W1/versions.h b/core/embed/models/T3W1/versions.h
index 42408b3d..76f69fe4 100644
--- a/core/embed/models/T3W1/versions.h
+++ b/core/embed/models/T3W1/versions.h
@@ -1,3 +1,4 @@
#define BOOTLOADER_MONOTONIC_VERSION 1
#define FIRMWARE_MONOTONIC_VERSION 1
+#define SECMON_MONOTONIC_VERSION 0
diff --git a/core/embed/projects/bootloader/.changelog.d/6244.added b/core/embed/projects/bootloader/.changelog.d/6244.added
new file mode 100644
index 00000000..83eb7d8d
--- /dev/null
+++ b/core/embed/projects/bootloader/.changelog.d/6244.added
@@ -0,0 +1 @@
+Introduced Secure Monitor downgrade protection.
diff --git a/core/embed/projects/bootloader/fw_check.c b/core/embed/projects/bootloader/fw_check.c
index 589f8ae2..e550da64 100644
--- a/core/embed/projects/bootloader/fw_check.c
+++ b/core/embed/projects/bootloader/fw_check.c
@@ -112,6 +112,7 @@ void fw_check(fw_info_t *fw_info) {
volatile secbool secmon_model_valid = secfalse;
volatile secbool secmon_header_sig_valid = secfalse;
volatile secbool secmon_contents_valid = secfalse;
+ volatile secbool secmon_version_ok = secfalse;
if (sectrue == fw_info->header_present) {
secmon_header_present =
@@ -129,8 +130,12 @@ void fw_check(fw_info_t *fw_info) {
}
if (sectrue == secmon_header_sig_valid) {
+ secmon_version_ok = check_secmon_min_version(secmon_hdr->monotonic);
+ }
+
+ if (sectrue == secmon_version_ok) {
secmon_contents_valid = secbool_and(
- secmon_header_sig_valid,
+ secmon_version_ok,
check_secmon_contents(secmon_hdr, secmon_start - FIRMWARE_START,
&FIRMWARE_AREA));
secmon_valid = secmon_contents_valid;
diff --git a/core/embed/projects/bootloader/main.c b/core/embed/projects/bootloader/main.c
index 35890881..90f97827 100644
--- a/core/embed/projects/bootloader/main.c
+++ b/core/embed/projects/bootloader/main.c
@@ -382,7 +382,6 @@ void real_jump_to_firmware(void) {
ensure(check_firmware_min_version(hdr->monotonic),
"Firmware downgrade protection");
- ensure_firmware_min_version(hdr->monotonic);
ensure(check_image_contents(hdr, IMAGE_HEADER_SIZE + vhdr.hdrlen,
&FIRMWARE_AREA),
@@ -406,11 +405,20 @@ void real_jump_to_firmware(void) {
ensure(check_secmon_header_sig(secmon_hdr), "Invalid secmon signature");
+ ensure(check_secmon_min_version(secmon_hdr->monotonic),
+ "Secmon downgrade protection");
+
ensure(check_secmon_contents(secmon_hdr, secmon_start - FIRMWARE_START,
&FIRMWARE_AREA),
"Secmon is corrupted");
#endif
+ // ensure minimal versions are properly stored for both firmware and secmon
+ ensure_firmware_min_version(hdr->monotonic);
+#ifdef USE_SECMON_VERIFICATION
+ ensure_secmon_min_version(secmon_hdr->monotonic);
+#endif
+
secbool provisioning_access =
((vhdr.vtrust & (VTRUST_ALLOW_PROVISIONING | VTRUST_SECRET_MASK)) ==
(VTRUST_SECRET_ALLOW | VTRUST_ALLOW_PROVISIONING)) *
diff --git a/core/embed/projects/bootloader/version_check.c b/core/embed/projects/bootloader/version_check.c
index 8b7298b4..af5d91ef 100644
--- a/core/embed/projects/bootloader/version_check.c
+++ b/core/embed/projects/bootloader/version_check.c
@@ -44,3 +44,17 @@ void ensure_firmware_min_version(uint8_t version) {
ensure(monoctr_read(MONOCTR_FIRMWARE_VERSION, &val), NULL);
ensure(sectrue * (val == version), "Firmware downgrade protection");
}
+
+secbool check_secmon_min_version(uint8_t check_version) {
+ uint8_t min_version = 0;
+ ensure(monoctr_read(MONOCTR_SECMON_VERSION, &min_version), "monoctr read");
+
+ return (check_version >= min_version) * sectrue;
+}
+
+void ensure_secmon_min_version(uint8_t version) {
+ monoctr_write(MONOCTR_SECMON_VERSION, version);
+ uint8_t val = 0;
+ ensure(monoctr_read(MONOCTR_SECMON_VERSION, &val), NULL);
+ ensure(sectrue * (val == version), "Secmon downgrade protection");
+}
diff --git a/core/embed/projects/bootloader/version_check.h b/core/embed/projects/bootloader/version_check.h
index 4572d3c8..187f9c3e 100644
--- a/core/embed/projects/bootloader/version_check.h
+++ b/core/embed/projects/bootloader/version_check.h
@@ -37,3 +37,12 @@ secbool check_firmware_min_version(uint8_t check_version);
// Ensures firmware version is stored in monotonic counter
// If the version cannot be written, the function will shutdown the device
void ensure_firmware_min_version(uint8_t version);
+
+// This functions checks if the secmon version is at least the minimum
+// required version, returns sectrue if check_version is higher or equal to the
+// stored version
+secbool check_secmon_min_version(uint8_t check_version);
+
+// Ensures secmon version is stored in monotonic counter
+// If the version cannot be written, the function will shutdown the device
+void ensure_secmon_min_version(uint8_t version);
diff --git a/core/embed/projects/bootloader/workflow/wf_firmware_update.c b/core/embed/projects/bootloader/workflow/wf_firmware_update.c
index eb1ed359..c868f27b 100644
--- a/core/embed/projects/bootloader/workflow/wf_firmware_update.c
+++ b/core/embed/projects/bootloader/workflow/wf_firmware_update.c
@@ -70,6 +70,7 @@ typedef enum {
UPLOAD_ERR_INVALID_SECMON_HEADER_SIG = -19,
UPLOAD_ERR_INVALID_SECMON_MODEL = -20,
UPLOAD_ERR_INVALID_SECMON_HASH = -21,
+ UPLOAD_ERR_INVALID_SECMON_VERSION = -23,
UPLOAD_ERR_SECMON_TOO_BIG = -22,
} upload_status_t;
@@ -289,6 +290,12 @@ static upload_status_t process_msg_FirmwareUpload(protob_io_t *iface,
return UPLOAD_ERR_INVALID_SECMON_HEADER_SIG;
}
+ if (sectrue != check_secmon_min_version(secmon_hdr->monotonic)) {
+ send_msg_failure(iface, FailureType_Failure_ProcessError,
+ "Secmon downgrade protection");
+ return UPLOAD_ERR_INVALID_SECMON_VERSION;
+ }
+
ctx->secmon_code_size = secmon_hdr->codelen;
memcpy(ctx->expected_secmon_hash, secmon_hdr->hash,
diff --git a/core/embed/projects/prodtest/secmon_header.S b/core/embed/projects/prodtest/secmon_header.S
index cc02fc26..8d114eb0 100644
--- a/core/embed/projects/prodtest/secmon_header.S
+++ b/core/embed/projects/prodtest/secmon_header.S
@@ -19,7 +19,8 @@ g_header:
.byte VERSION_BUILD // vbuild
.word HW_MODEL // type of the designated hardware
.byte HW_REVISION // revision of the designated hardware
- . = . + 3 // reserved
+ .byte SECMON_MONOTONIC_VERSION // monotonic version of the binary
+ . = . + 2 // reserved
. = . + 32 // hash of entire secmon
. = . + 391 // reserved
.byte 0 // sigmask
diff --git a/core/embed/projects/secmon/header.S b/core/embed/projects/secmon/header.S
index 9b2c920d..ee9a5a6c 100644
--- a/core/embed/projects/secmon/header.S
+++ b/core/embed/projects/secmon/header.S
@@ -19,7 +19,8 @@ g_header:
.byte VERSION_BUILD // vbuild
.word HW_MODEL // type of the designated hardware
.byte HW_REVISION // revision of the designated hardware
- . = . + 3 // reserved
+ .byte SECMON_MONOTONIC_VERSION // monotonic version of the binary
+ . = . + 2 // reserved
. = . + 32 // hash of entire secmon
. = . + 391 // reserved
.byte 0 // sigmask
diff --git a/core/embed/projects/secmon/version.h b/core/embed/projects/secmon/version.h
index 322e3ff1..e0b41f4f 100644
--- a/core/embed/projects/secmon/version.h
+++ b/core/embed/projects/secmon/version.h
@@ -1,3 +1,7 @@
+#pragma once
+
+#include "model_version.h"
+
#define VERSION_MAJOR 1
#define VERSION_MINOR 0
#define VERSION_PATCH 7
diff --git a/core/embed/sec/monoctr/inc/sec/monoctr.h b/core/embed/sec/monoctr/inc/sec/monoctr.h
index 9d5d8d87..53b70dec 100644
--- a/core/embed/sec/monoctr/inc/sec/monoctr.h
+++ b/core/embed/sec/monoctr/inc/sec/monoctr.h
@@ -30,6 +30,7 @@
typedef enum {
MONOCTR_BOOTLOADER_VERSION = 0,
MONOCTR_FIRMWARE_VERSION = 1,
+ MONOCTR_SECMON_VERSION = 2,
} monoctr_type_t;
// Write a new value to the monotonic counter
diff --git a/core/embed/sec/monoctr/stm32u5/monoctr.c b/core/embed/sec/monoctr/stm32u5/monoctr.c
index 1f78d189..9edad6a3 100644
--- a/core/embed/sec/monoctr/stm32u5/monoctr.c
+++ b/core/embed/sec/monoctr/stm32u5/monoctr.c
@@ -32,6 +32,10 @@ static int32_t get_offset(monoctr_type_t type) {
return SECRET_MONOTONIC_COUNTER_0_OFFSET;
case MONOCTR_FIRMWARE_VERSION:
return SECRET_MONOTONIC_COUNTER_1_OFFSET;
+#ifdef SECRET_MONOTONIC_COUNTER_2_OFFSET
+ case MONOCTR_SECMON_VERSION:
+ return SECRET_MONOTONIC_COUNTER_2_OFFSET;
+#endif
default:
return -1;
}
@@ -43,6 +47,10 @@ static size_t get_length(monoctr_type_t type) {
return SECRET_MONOTONIC_COUNTER_0_LEN;
case MONOCTR_FIRMWARE_VERSION:
return SECRET_MONOTONIC_COUNTER_1_LEN;
+#ifdef SECRET_MONOTONIC_COUNTER_2_LEN
+ case MONOCTR_SECMON_VERSION:
+ return SECRET_MONOTONIC_COUNTER_2_LEN;
+#endif
default:
return 0;
}
diff --git a/core/embed/util/image/inc/util/image.h b/core/embed/util/image/inc/util/image.h
index 7e242f31..3b8e85c0 100644
--- a/core/embed/util/image/inc/util/image.h
+++ b/core/embed/util/image/inc/util/image.h
@@ -147,7 +147,8 @@ typedef struct {
uint32_t version;
uint32_t hw_model;
uint8_t hw_revision;
- uint8_t reserved_0[3];
+ uint8_t monotonic;
+ uint8_t reserved_0[2];
uint8_t hash[32];
uint8_t reserved_1[391];
uint8_t sigmask;
diff --git a/python/src/trezorlib/firmware/secmon.py b/python/src/trezorlib/firmware/secmon.py
index bff4491a..651f798d 100644
--- a/python/src/trezorlib/firmware/secmon.py
+++ b/python/src/trezorlib/firmware/secmon.py
@@ -37,6 +37,7 @@ class SecmonHeader(Struct):
version: tuple[int, int, int, int]
hw_model: Model | bytes
hw_revision: int
+ monotonic: int
hash: bytes
sigmask: int
@@ -51,7 +52,8 @@ class SecmonHeader(Struct):
"version" / TupleAdapter(c.Int8ul, c.Int8ul, c.Int8ul, c.Int8ul),
"hw_model" / EnumAdapter(c.Bytes(4), Model),
"hw_revision" / c.Int8ul,
- "_reserved" / c.Padding(3),
+ "monotonic" / c.Int8ul,
+ "_reserved" / c.Padding(2),
"hash" / c.Bytes(32),
"_reserved" / c.Padding(391),
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.