feat(core,python): definitions payload version
What changed, and why it matters
This commit refactors how Trezor firmware recognizes external 'definitions' files (the data blobs that describe Ethereum networks and tokens). It splits the old 5-byte header 'trzd1' into a 4-byte magic 'trzd' plus a 1-byte version number. The change is framed by the authors as a necessary preparation for a future signature-quorum change, not as a fix for an active vulnerability. No currently reachable security flaw is present in the diff; the code still only accepts version '1', which keeps the existing 2-of-3 signature requirement.
No urgent action required. Treat as a normal feature/refactoring commit. Monitor the follow-up change that actually introduces a new format version / signature-quorum, because that is where real security assumptions will shift.
Security signals we found
Hardcoded signature threshold moved into versioned enum
New input validation for definition format version byte
No new version actually enabled; only V1 remains supported
Commit message describes future signature-quorum change, not a current vulnerability
Tests added for unsupported format versions and mangled magic
Evidence from the diff
The patch reinterprets the definitions payload header. constants.rs replaces the hardcoded THRESHOLD=2 with a DefsVersion enum whose only member V1 also returns threshold 2. obj.rs changes verify() to accept a format_version byte, validates it via DefsVersion::from_byte, and passes the matching threshold to cosi::verify. Python and Rust decoding paths now expect MAGIC=b’trzd’ followed by a single-byte SUPPORTED_FORMAT_VERSIONS=(b‘1’,). Tests and mocks are updated accordingly. The python/trezorlib verifier now maps version bytes to signature thresholds via a dictionary, but still only contains b‘1’:2. The commit message explicitly states this is preparation for a future backwards-incompatible definitions release with a bumped version.
Changed components
core/embed/rust/src/definitions/constants.rscore/embed/rust/src/definitions/obj.rscore/src/apps/common/definitions.pycore/src/apps/common/definitions_constants.pypython/src/trezorlib/definitions.pyexternal definitions decoding/verification pathInspect captured patch +124 / −49
### core/embed/rust/src/definitions/constants.rs
@@ -1,6 +1,25 @@
use crypto::ed25519;
-pub const THRESHOLD: u8 = 2;
+// Definition format versions, encoded on the wire as ASCII digit bytes.
+pub enum DefsVersion {
+ V1,
+}
+
+impl DefsVersion {
+ // CoSi signature threshold for this version.
+ pub const fn threshold(self) -> u8 {
+ match self {
+ DefsVersion::V1 => 2,
+ }
+ }
+
+ pub const fn from_byte(byte: u8) -> Option<Self> {
+ match byte {
+ b'1' => Some(DefsVersion::V1),
+ _ => None,
+ }
+ }
+}
#[cfg(feature = "dev_keys")]
pub const PUBLIC_KEYS_DEVEL: [ed25519::PublicKey; 3] = [
### core/embed/rust/src/definitions/obj.rs
@@ -1,54 +1,62 @@
use crypto::{cosi, ed25519};
use super::constants;
-use crate::error::Error;
+use crate::error::{value_error, Error};
use crate::micropython::buffer::get_buffer;
-use crate::micropython::macros::{obj_fn_3, obj_module};
+use crate::micropython::macros::{obj_fn_var, obj_module};
+use crate::micropython::map::Map;
use crate::micropython::module::Module;
use crate::micropython::obj::Obj;
use crate::micropython::qstr::Qstr;
use crate::micropython::util;
fn verify_with_keys(
+ threshold: u8,
digest: &[u8],
sig: &cosi::Signature,
public_keys: &[ed25519::PublicKey; 3],
) -> Result<(), Error> {
- Ok(cosi::verify(
- constants::THRESHOLD,
- digest,
- public_keys,
- sig,
- )?)
+ Ok(cosi::verify(threshold, digest, public_keys, sig)?)
}
-extern "C" fn verify(digest: Obj, sig: Obj, sigmask: Obj) -> Obj {
- let block = || {
+fn threshold_for_version(version: u8) -> Result<u8, Error> {
+ let version = constants::DefsVersion::from_byte(version)
+ .ok_or(value_error!(c"Unsupported definition format version"))?;
+ Ok(version.threshold())
+}
+
+extern "C" fn verify(n_args: usize, args: *const Obj) -> Obj {
+ let block = |args: &[Obj], _kwargs: &Map| {
+ if args.len() != 4 {
+ return Err(Error::TypeError);
+ }
// SAFETY: reference is discarded at the end of the block
- let digest = unsafe { get_buffer(digest)? };
- let signature = unsafe { get_buffer(sig)? };
+ let digest = unsafe { get_buffer(args[0])? };
+ let signature = unsafe { get_buffer(args[1])? };
+ let sigmask = u8::try_from(args[2])?;
+ let format_version = u8::try_from(args[3])?;
+ let threshold = threshold_for_version(format_version)?;
- let sig = cosi::Signature::new(
- u8::try_from(sigmask)?,
- signature.try_into().map_err(|_| Error::TypeError)?,
- );
+ let sig =
+ cosi::Signature::new(sigmask, signature.try_into().map_err(|_| Error::TypeError)?);
#[allow(unused_mut)]
- let mut result = verify_with_keys(digest, &sig, &constants::PUBLIC_KEYS_PRODUCTION);
+ let mut result =
+ verify_with_keys(threshold, digest, &sig, &constants::PUBLIC_KEYS_PRODUCTION);
#[cfg(feature = "dev_keys")]
if result.is_err() {
// allow development keys
- result = verify_with_keys(digest, &sig, &constants::PUBLIC_KEYS_DEVEL);
+ result = verify_with_keys(threshold, digest, &sig, &constants::PUBLIC_KEYS_DEVEL);
}
result.map(|()| Obj::const_none())
};
- unsafe { util::try_or_raise(block) }
+ unsafe { util::try_with_args_and_kwargs(n_args, args, &Map::EMPTY, block) }
}
#[no_mangle]
#[rustfmt::skip]
pub static mp_module_trezordefinitions: Module = obj_module! {
- /// def verify(digest: AnyBytes, sig: AnyBytes, sigmask: int) -> None:
+ /// def verify(digest: AnyBytes, sig: AnyBytes, sigmask: int, version: int) -> None:
/// """Verify the definitions signature."""
- Qstr::MP_QSTR_verify => obj_fn_3!(verify).as_obj(),
+ Qstr::MP_QSTR_verify => obj_fn_var!(4, 4, verify).as_obj(),
};
### core/mocks/generated/trezordefinitions.pyi
@@ -3,5 +3,5 @@ from buffer_types import *
# rust/src/definitions/obj.rs
-def verify(digest: AnyBytes, sig: AnyBytes, sigmask: int) -> None:
+def verify(digest: AnyBytes, sig: AnyBytes, sigmask: int, version: int) -> None:
"""Verify the definitions signature."""
### core/src/apps/common/definitions.py
@@ -46,15 +46,20 @@ def decode_definition(definition: AnyBytes, expected_type: type[DefType]) -> Def
expected_type_number = DefinitionType.ETHEREUM_DISPLAY_FORMAT
try:
- # first check format version
- if r.read_memoryview(len(consts.FORMAT_VERSION)) != consts.FORMAT_VERSION:
+ # first check magic
+ if r.read_memoryview(len(consts.MAGIC)) != consts.MAGIC:
raise DataError("Invalid definition")
- # second check the type of the data
+ # second check the format version
+ format_version = r.read_memoryview(1)
+ if format_version not in consts.SUPPORTED_FORMAT_VERSIONS:
+ raise DataError("Invalid definition")
+
+ # third check the type of the data
if r.get() != expected_type_number:
raise DataError("Definition type mismatch")
- # third check data version
+ # fourth check data version
if readers.read_uint32_le(r) < consts.MIN_DATA_VERSION:
raise DataError("Definition is outdated")
@@ -88,7 +93,7 @@ def decode_definition(definition: AnyBytes, expected_type: type[DefType]) -> Def
# verify signature
try:
- verify(hash, signature, sigmask)
+ verify(hash, signature, sigmask, format_version[0])
except ValueError:
raise DataError("Invalid definition signature")
### core/src/apps/common/definitions_constants.py
@@ -3,7 +3,11 @@
# do not edit manually!
MIN_DATA_VERSION = 1783520408
-FORMAT_VERSION = b"trzd1"
+MAGIC = b"trzd"
-# The public keys and signature threshold for definitions verification
+# Supported format versions of the definitions, encoded on the wire as
+# ASCII digit bytes ('1' = 0x31, '2' = 0x32, etc.).
+SUPPORTED_FORMAT_VERSIONS = (b"1",)
+
+# The public keys and signature thresholds for definitions verification
# live in Rust (core/embed/rust/src/definitions/constants.rs).
### core/src/apps/common/definitions_constants.py.mako
@@ -3,7 +3,11 @@
# do not edit manually!
MIN_DATA_VERSION = ${defs_timestamp}
-FORMAT_VERSION = b"trzd1"
+MAGIC = b"trzd"
-# The public keys and signature threshold for definitions verification
+# Supported format versions of the definitions, encoded on the wire as
+# ASCII digit bytes ('1' = 0x31, '2' = 0x32, etc.).
+SUPPORTED_FORMAT_VERSIONS = (b"1",)
+
+# The public keys and signature thresholds for definitions verification
# live in Rust (core/embed/rust/src/definitions/constants.rs).
### core/tests/ethereum_common.py
@@ -46,7 +46,8 @@ def make_solana_token(
def make_payload(
- prefix: bytes = b"trzd1",
+ magic: bytes = b"trzd",
+ format_version: bytes = b"1",
data_type: DefinitionType = DefinitionType.ETHEREUM_NETWORK,
timestamp: int = 0xFFFF_FFFF,
message: (
@@ -56,7 +57,8 @@ def make_payload(
| bytes
) = make_eth_network(),
) -> bytes:
- payload = prefix
+ payload = magic
+ payload += format_version
payload += data_type.to_bytes(1, "little")
payload += timestamp.to_bytes(4, "little")
if isinstance(message, bytes):
### core/tests/test_apps.common.definitions.py
@@ -53,7 +53,7 @@ def test_mangled_signature(self):
self.assertFailed(payload + proof + bad_signature)
def test_not_enough_signatures(self):
- payload = make_payload()
+ payload = make_payload(format_version=b"1")
proof, signature = sign_payload(payload, [], threshold=1)
self.assertFailed(payload + proof + signature)
@@ -86,11 +86,17 @@ def test_trimmed_proof(self):
bad_proof = proof[:-1]
self.assertFailed(payload + bad_proof + signature)
- def test_bad_prefix(self):
- payload = make_payload(prefix=b"trzd2")
+ def test_bad_magic(self):
+ payload = make_payload(magic=b"trze")
proof, signature = sign_payload(payload, [])
self.assertFailed(payload + proof + signature)
+ def test_unsupported_format_version(self):
+ for version in (b"0", b"3", b"\x01", b"\xff"):
+ payload = make_payload(format_version=version)
+ proof, signature = sign_payload(payload, [])
+ self.assertFailed(payload + proof + signature)
+
def test_bad_type(self):
payload = make_payload(
data_type=DefinitionType.ETHEREUM_TOKEN, message=make_eth_token()
### docs/common/external-definitions.md
@@ -108,15 +108,19 @@ and packaged in the following binary format.
All numbers are unsigned little endian.
-1. magic string `trzd1` (5 bytes)
-2. definition type according to `DefinitionType` enum (1 byte)
-3. data version of the definition (4 bytes)
-4. protobuf payload length (2 bytes)
-5. protobuf payload (N bytes)
+1. magic string `trzd` (4 bytes)
+2. format version, an ASCII digit byte: e.g. `1` (0x31), `2` (0x32), etc. (1 byte)
+3. definition type according to `DefinitionType` enum (1 byte)
+4. data version of the definition (4 bytes)
+5. protobuf payload length (2 bytes)
+6. protobuf payload (N bytes)
A Merkle tree is constructed from all binary definitions (see below) and its root is
signed by the CoSi algorithm.
+The format version is bumped on backward incompatible change.
+For versions 1 and 2, the data structure is identical.
+
The full format of the definition is as follows:
1. Data payload (see above)
### python/src/trezorlib/definitions.py
@@ -29,7 +29,7 @@
LOG = logging.getLogger(__name__)
-FORMAT_MAGIC = b"trzd1"
+MAGIC = b"trzd"
DEFS_BASE_URL = "https://data.trezor.io/firmware/definitions/"
DEFINITIONS_DEV_SIGS_REQUIRED = 1
@@ -38,7 +38,11 @@
for key in ("db995fe25169d141cab9bbba92baa01f9f2e1ece7df4cb2ac05190f37fcc1f9d",)
]
-DEFINITIONS_SIGS_REQUIRED = 2
+# Number of CoSi signatures required by definition format version.
+# Version 1 requires 2 signatures.
+DEFINITIONS_SIGS_REQUIRED = {
+ b"1": 2,
+}
DEFINITIONS_PUBLIC_KEYS = [
bytes.fromhex(key)
for key in (
@@ -54,12 +58,14 @@
class DefinitionPayload(Struct):
magic: bytes
+ version: bytes # ASCII digit byte of the format version, e.g. b"1"
data_type: DefinitionType
timestamp: int
data: bytes
SUBCON = c.Struct(
- "magic" / c.Const(FORMAT_MAGIC),
+ "magic" / c.Const(MAGIC),
+ "version" / c.Bytes(1),
"data_type" / EnumAdapter(c.Int8ul, DefinitionType),
"timestamp" / c.Int32ul,
"data" / c.Prefixed(c.Int16ul, c.GreedyBytes),
@@ -82,11 +88,24 @@ class Definition(Struct):
def verify(self, dev: bool = False) -> None:
payload = self.payload.build()
root = merkle_tree.evaluate_proof(payload, self.proof)
+ if dev:
+ sigs_required, public_keys = (
+ DEFINITIONS_DEV_SIGS_REQUIRED,
+ DEFINITIONS_DEV_PUBLIC_KEYS,
+ )
+ else:
+ try:
+ sigs_required = DEFINITIONS_SIGS_REQUIRED[self.payload.version]
+ except KeyError:
+ raise ValueError(
+ f"Unsupported definition format version {self.payload.version.decode()!r}"
+ ) from None
+ public_keys = DEFINITIONS_PUBLIC_KEYS
cosi.verify(
self.signature,
root,
- DEFINITIONS_DEV_SIGS_REQUIRED,
- DEFINITIONS_DEV_PUBLIC_KEYS,
+ sigs_required,
+ public_keys,
self.sigmask,
)
### tests/definitions.py
@@ -39,6 +39,8 @@ def make_eth_token(
def make_payload(
+ magic: bytes = b"trzd",
+ format_version: bytes = b"1",
data_type: messages.DefinitionType = messages.DefinitionType.ETHEREUM_NETWORK,
timestamp: int = 0xFFFF_FFFF,
message: (
@@ -57,7 +59,8 @@ def make_payload(
message_bytes = writer.getvalue()
payload = definitions.DefinitionPayload(
- magic=b"trzd1",
+ magic=magic,
+ version=format_version,
data_type=data_type,
timestamp=timestamp,
data=message_bytes,
### tests/device_tests/ethereum/test_definitions_bad.py
@@ -182,7 +182,8 @@ def test_trimmed_proof(session: Session) -> None:
def test_bad_prefix(session: Session) -> None:
for make, check in _cases(session):
payload = make()
- payload = b"trzd2" + payload[5:]
+ # mangle the magic, keep a valid version byte
+ payload = b"trze1" + payload[5:]
proof, signature = sign_payload(payload, [])
check(session, payload + proof + signature, "Invalid definition")
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.