refactor(core/embed): make counter limit uint32_t
What changed, and why it matters
This commit changes how Trezor devices set a lifetime PIN-attempt counter in the secure Optiga chip. Previously the counter limit was stored as an 8-bit number (max 255), which was too small for the intended 600,000 stretching operations. The patch widens the limit field to 32 bits and correctly writes the full 600,000 value into the chip. Without this change, the counter could wrap or be capped at 255, potentially allowing more PIN attempts than designed or prematurely bricking the device. The change is a hardening fix, not an obvious remote exploit.
Treat this as a security hardening fix and ensure it is included in release notes. Verify that the new 32-bit encoding matches the Optiga data object format expected by the secure element, and regression-test counter exhaustion behavior. Consider whether devices already initialized with the truncated 192 limit need remediation or migration.
Security signals we found
Integer width mismatch between API and intended security limit
Truncation of a security-critical counter limit from 600000 to 192
Refactor of secure-element counter initialization code
No changelog entry despite security-relevant behavior change
Evidence from the diff
The refactor updates optiga_reset_counter() to accept uint32_t instead of uint8_t and writes the 4-byte big-endian limit into the last four bytes of an 8-byte data object. It also replaces a static uint8_t array initialization (PIN_TOTAL_CTR_INIT) with a direct call using PIN_TOTAL_CTR_LIMIT (600000). The old code only placed the limit in byte 7 of the value_array, which would have truncated 600000 (0x927C0) to 0xC0 (192). This truncation would have set the lifetime counter far below the intended security policy, likely allowing more operations than intended before the secure element refused further PIN stretching.
Changed components
core/embed/sec/optiga/optiga.ccore/embed/sec/optiga/optiga_commands.ccore/embed/sec/optiga/inc/sec/optiga_commands.hOptiga secure-element counter initializationInspect captured patch +14 / −11
diff --git a/core/embed/sec/optiga/inc/sec/optiga_commands.h b/core/embed/sec/optiga/inc/sec/optiga_commands.h
index c562bd175..0cbd6e055 100644
--- a/core/embed/sec/optiga/inc/sec/optiga_commands.h
+++ b/core/embed/sec/optiga/inc/sec/optiga_commands.h
@@ -240,7 +240,7 @@ optiga_result optiga_derive_key(optiga_key_derivation deriv, uint16_t oid,
optiga_result optiga_set_trust_anchor(void);
optiga_result optiga_set_priv_key(uint16_t oid, const uint8_t priv_key[32]);
optiga_result optiga_clear_all_auto_states(void);
-optiga_result optiga_reset_counter(uint16_t oid, uint8_t limit);
+optiga_result optiga_reset_counter(uint16_t oid, uint32_t limit);
#if !PRODUCTION
void optiga_command_set_log_hex(optiga_log_hex_t f);
diff --git a/core/embed/sec/optiga/optiga.c b/core/embed/sec/optiga/optiga.c
index 8c643882c..65143c6a9 100644
--- a/core/embed/sec/optiga/optiga.c
+++ b/core/embed/sec/optiga/optiga.c
@@ -71,6 +71,12 @@
// The throttling delay when the security event counter is at its maximum.
#define OPTIGA_T_MAX_MS 5000
+// Initial value of the counter which limits the total number of PIN stretching
+// operations. The limit is 600000 stretching operations, which equates to
+// 300000 / PIN_STRETCH_ITERATIONS unlock operations over the lifetime of the
+// device.
+#define PIN_TOTAL_CTR_LIMIT 600000
+
// Stretched PINs
// The first stretched PIN is OPTIGA_OID_DATA + 4 to preserve compatiblity with
// Trezors without Tropics.
@@ -85,12 +91,6 @@ _Static_assert(sizeof(OID_STRETCHED_PINS) / sizeof(OID_STRETCHED_PINS[0]) >=
STRETCHED_PIN_COUNT,
"STRETCHED_PIN_COUNT too large");
-// Initial value of the counter which limits the total number of PIN stretching
-// operations. The limit is 600000 stretching operations, which equates to
-// 300000 / PIN_STRETCH_ITERATIONS unlock operations over the lifetime of the
-// device.
-static const uint8_t PIN_TOTAL_CTR_INIT[] = {0, 0, 0, 0, 0, 0x09, 0x27, 0xC0};
-
static const optiga_metadata_item TYPE_AUTOREF =
OPTIGA_META_VALUE(OPTIGA_DATA_TYPE_AUTOREF);
static const optiga_metadata_item TYPE_PRESSEC =
@@ -499,8 +499,7 @@ static bool optiga_pin_init_metadata() {
metadata.change = OPTIGA_META_ACCESS_ALWAYS;
if (write_metadata(OID_PIN_TOTAL_CTR, &metadata)) {
optiga_result res =
- optiga_set_data_object(OID_PIN_TOTAL_CTR, false, PIN_TOTAL_CTR_INIT,
- sizeof(PIN_TOTAL_CTR_INIT));
+ optiga_reset_counter(OID_PIN_TOTAL_CTR, PIN_TOTAL_CTR_LIMIT);
if (res != OPTIGA_SUCCESS) {
return false;
}
diff --git a/core/embed/sec/optiga/optiga_commands.c b/core/embed/sec/optiga/optiga_commands.c
index e44521149..400528926 100644
--- a/core/embed/sec/optiga/optiga_commands.c
+++ b/core/embed/sec/optiga/optiga_commands.c
@@ -999,8 +999,12 @@ optiga_result optiga_clear_all_auto_states(void) {
return OPTIGA_SUCCESS;
}
-optiga_result optiga_reset_counter(uint16_t oid, uint8_t limit) {
- uint8_t value_array[8] = {0, 0, 0, 0, 0, 0, 0, limit};
+optiga_result optiga_reset_counter(uint16_t oid, uint32_t limit) {
+ uint8_t value_array[8] = {0};
+ for (int i = 7; i >= 4; i--) {
+ value_array[i] = limit & 0xff;
+ limit >>= 8;
+ }
return optiga_set_data_object(oid, false, value_array, sizeof(value_array));
}
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.