What changed, and why it matters
This commit rewrites the handling of externally downloaded coin/network definitions from Python into Rust. It moves parsing, signature verification, and protobuf decoding into a new Rust module. The change is described by the vendor as a non-security refactor ('RIIR' = 'Rewrite It In Rust') with no changelog entry. The diff itself does not introduce obvious new vulnerabilities, but it is a partial refactor: the Python side now delegates most validation to Rust, and some safety comments rely on assumptions about MicroPython not mutating buffers. There is no vendor statement that this is a security fix or that it addresses a reported vulnerability.
Treat this as a code-quality refactor rather than an urgent security patch, but review the new Rust parser carefully: confirm the `align_to` safety justification, ensure `InputStream` bounds checks are exhaustive, verify that the `dev_keys` feature cannot be enabled in production builds, and check that the protobuf decoder's `enable_experimental: true` does not decode unexpected message types. Regression tests for malformed definition blobs should be run because the Python validation path is removed.
Security signals we found
Refactor of security-critical code: signature verification and protobuf decoding moved from Python to Rust
New Rust code parses untrusted external definition blobs and verifies CoSi signatures before decoding protobuf
Use of `unsafe { get_buffer(...) }` with a safety comment assuming no concurrent MicroPython mutation
Use of `unsafe { proof_bytes.align_to::<sha256::Digest>() }` to reinterpret proof bytes as digests
Development keys accepted when `dev_keys` feature is enabled, falling back from production keys
No changelog entry; commit title frames change as a refactor ('RIIR definitions')
Removal of Python-side format-version and magic checks; validation now primarily in Rust
Evidence from the diff
The commit refactors Trezor’s external definition-blob handling. A new Rust module blob.rs parses the signed definition format (magic, version, type, data version, payload, Merkle proof, CoSi signature) and verifies the CoSi signature against production (and optionally dev) keys. obj.rs exposes a single MicroPython-callable decode() function that parses/verifies and then decodes the payload into a specified protobuf message type. The Python implementation in apps/common/definitions.py is reduced to a thin wrapper that maps expected message types to DefinitionType numbers and calls the Rust decode(). Constants and generated minimum-data-version logic move from Python into Rust. The protobuf decode/obj modules are made pub(crate) so Rust can call them. Several files are removed or no longer included in the MicroPython build.
Changed components
core/embed/rust/src/definitions/blob.rscore/embed/rust/src/definitions/constants.rscore/embed/rust/src/definitions/generated.rscore/embed/rust/src/definitions/generated.rs.makocore/embed/rust/src/definitions/mod.rscore/embed/rust/src/definitions/obj.rscore/embed/rust/src/protobuf/mod.rscore/embed/upymod/build.rscore/embed/upymod/qstrdefsport.hcore/mocks/generated/trezordefinitions.pyicore/src/apps/common/definitions.pycore/src/apps/common/definitions_constants.pycore/src/apps/common/definitions_constants.py.makoInspect captured patch +175 / −141
### core/embed/rust/src/definitions/blob.rs
@@ -0,0 +1,101 @@
+use crypto::merkle::merkle_root;
+use crypto::{cosi, ed25519, sha256};
+
+use super::{constants, generated};
+use crate::error::{value_error, Error};
+use crate::io::InputStream;
+
+const INVALID_DEFINITION: Error = value_error!(c"Invalid definition");
+const INVALID_SIGNATURE: Error = value_error!(c"Invalid definition signature");
+
+fn read<'a>(reader: &mut InputStream<'a>, len: usize) -> Result<&'a [u8], Error> {
+ reader.read(len).map_err(|_| INVALID_DEFINITION)
+}
+
+fn read_byte(reader: &mut InputStream<'_>) -> Result<u8, Error> {
+ reader.read_byte().map_err(|_| INVALID_DEFINITION)
+}
+
+fn verify_with_keys(
+ threshold: u8,
+ digest: &[u8],
+ sig: &cosi::Signature,
+ public_keys: &[ed25519::PublicKey; 3],
+) -> Result<(), Error> {
+ cosi::verify(threshold, digest, public_keys, sig).map_err(|_| INVALID_SIGNATURE)
+}
+
+fn verify(threshold: u8, digest: &[u8], sig: &cosi::Signature) -> Result<(), Error> {
+ #[allow(unused_mut)]
+ 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(threshold, digest, sig, &constants::PUBLIC_KEYS_DEVEL);
+ }
+
+ result
+}
+
+/// Parse and verify a signed definition blob. Returns the protobuf payload.
+///
+/// Expects the definition format specified in
+/// `docs/common/external-definitions.md`.
+pub fn parse_and_verify(definition: &[u8], expected_type: u8) -> Result<&[u8], Error> {
+ let mut reader = InputStream::new(definition);
+
+ // magic
+ if read(&mut reader, constants::MAGIC.len())? != constants::MAGIC {
+ return Err(INVALID_DEFINITION);
+ }
+
+ // format version
+ let version =
+ constants::DefsVersion::from_byte(read_byte(&mut reader)?).ok_or(INVALID_DEFINITION)?;
+
+ // definition type
+ if read_byte(&mut reader)? != expected_type {
+ return Err(value_error!(c"Definition type mismatch"));
+ }
+
+ // data version
+ let data_version = reader.read_u32_le().map_err(|_| INVALID_DEFINITION)?;
+ if data_version < generated::MIN_DATA_VERSION {
+ return Err(value_error!(c"Definition is outdated"));
+ }
+
+ // payload
+ let payload_len: usize = reader.read_u16_le().map_err(|_| INVALID_DEFINITION)?.into();
+ let payload = read(&mut reader, payload_len)?;
+ let payload_end = reader.tell();
+
+ // Merkle proof
+ let proof_len: usize = read_byte(&mut reader)?.into();
+ let proof_bytes = read(&mut reader, proof_len * sha256::DIGEST_SIZE)?;
+ // SAFETY: sha256::Digest is a plain array of u8, so any bytes are valid.
+ let (_prefix, proof, _suffix) = unsafe { proof_bytes.align_to::<sha256::Digest>() };
+ if !_prefix.is_empty() || !_suffix.is_empty() {
+ return Err(INVALID_DEFINITION);
+ }
+
+ // CoSi signature
+ let sigmask = read_byte(&mut reader)?;
+ let signature = cosi::Signature::new(
+ sigmask,
+ unwrap!(read(&mut reader, ed25519::SIGNATURE_SIZE)?.try_into()),
+ );
+
+ // no trailing data
+ if reader.remaining() > 0 {
+ return Err(INVALID_DEFINITION);
+ }
+
+ // compute Merkle tree root hash using the payload with prefix as leaf data
+ // and verify the signature
+ let merkle_root = merkle_root(&definition[..payload_end], proof);
+
+ verify(version.threshold(), &merkle_root, &signature)?;
+
+ Ok(payload)
+}
### core/embed/rust/src/definitions/constants.rs
@@ -1,6 +1,10 @@
use crypto::ed25519;
+// Magic string at the beginning of every definition blob.
+pub const MAGIC: &[u8; 4] = b"trzd";
+
// Definition format versions, encoded on the wire as ASCII digit bytes.
+#[derive(Clone, Copy)]
pub enum DefsVersion {
V1,
V2,
### core/embed/rust/src/definitions/generated.rs
@@ -0,0 +1,6 @@
+// generated from generated.rs.mako
+// (by running `make templates` in `core`)
+// do not edit manually!
+
+/// Definitions older than this data version are rejected.
+pub const MIN_DATA_VERSION: u32 = 1783520408;
### core/embed/rust/src/definitions/generated.rs.mako
@@ -0,0 +1,6 @@
+// generated from generated.rs.mako
+// (by running `make templates` in `core`)
+// do not edit manually!
+
+/// Definitions older than this data version are rejected.
+pub const MIN_DATA_VERSION: u32 = ${defs_timestamp};
### core/embed/rust/src/definitions/mod.rs
@@ -1,3 +1,5 @@
+mod blob;
mod constants;
+mod generated;
#[cfg(feature = "micropython")]
mod obj;
### core/embed/rust/src/definitions/obj.rs
@@ -1,53 +1,40 @@
-use crypto::{cosi, ed25519};
-
-use super::constants;
+use super::blob;
use crate::error::{value_error, Error};
+use crate::io::InputStream;
use crate::micropython::buffer::get_buffer;
+use crate::micropython::gc::Gc;
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;
+use crate::protobuf::decode::Decoder;
+use crate::protobuf::obj::MsgDefObj;
-fn verify_with_keys(
- threshold: u8,
- digest: &[u8],
- sig: &cosi::Signature,
- public_keys: &[ed25519::PublicKey; 3],
-) -> Result<(), Error> {
- Ok(cosi::verify(threshold, digest, public_keys, sig)?)
-}
-
-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 {
+extern "C" fn decode(n_args: usize, args: *const Obj) -> Obj {
let block = |args: &[Obj], _kwargs: &Map| {
- if args.len() != 4 {
+ if args.len() != 3 {
return Err(Error::TypeError);
}
- // SAFETY: reference is discarded at the end of the block
- 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)?;
+ // SAFETY: We assume that for the lifetime of `definition`, no MicroPython
+ // code can run that would mutate the buffer, nor pass it to another Rust
+ // function.
+ let definition = unsafe { get_buffer(args[0])? };
+ let expected_type = u8::try_from(args[1])?;
+ let msg_def = Gc::<MsgDefObj>::try_from(args[2])?;
- let sig =
- cosi::Signature::new(sigmask, signature.try_into().map_err(|_| Error::TypeError)?);
- #[allow(unused_mut)]
- 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(threshold, digest, &sig, &constants::PUBLIC_KEYS_DEVEL);
- }
- result.map(|()| Obj::const_none())
+ // parse the definition blob and verify its CoSi signature
+ let payload = blob::parse_and_verify(definition, expected_type)?;
+
+ // decode the payload into the expected message type
+ let mut stream = InputStream::new(payload);
+ let decoder = Decoder {
+ enable_experimental: true,
+ };
+ decoder
+ .message_from_stream(&mut stream, msg_def.msg())
+ .map_err(|_| value_error!(c"Invalid definition"))
};
unsafe { util::try_with_args_and_kwargs(n_args, args, &Map::EMPTY, block) }
@@ -56,7 +43,17 @@ extern "C" fn verify(n_args: usize, args: *const Obj) -> Obj {
#[no_mangle]
#[rustfmt::skip]
pub static mp_module_trezordefinitions: Module = obj_module! {
- /// def verify(digest: AnyBytes, sig: AnyBytes, sigmask: int, version: int) -> None:
- /// """Verify the definitions signature."""
- Qstr::MP_QSTR_verify => obj_fn_var!(4, 4, verify).as_obj(),
+ /// from trezorproto import MessageType
+ ///
+ /// mock:global
+ /// T = TypeVar("T", bound=MessageType)
+ ///
+ /// def decode(
+ /// definition: AnyBytes,
+ /// expected_type: int,
+ /// msg_type: type[T],
+ /// ) -> T:
+ /// """Parse a signed definition blob, verify its signature and decode it
+ /// into the specified message type."""
+ Qstr::MP_QSTR_decode => obj_fn_var!(3, 3, decode).as_obj(),
};
### core/embed/rust/src/protobuf/mod.rs
@@ -1,6 +1,6 @@
-mod decode;
+pub(crate) mod decode;
mod defs;
mod encode;
mod error;
-mod obj;
+pub(crate) mod obj;
mod zigzag;
### core/embed/upymod/build.rs
@@ -1122,7 +1122,6 @@ impl<'a> MpyBuilder<'a> {
files.add(src, "apps/common/*.py")?;
files.remove(src, "apps/common/definitions.py");
- files.remove(src, "apps/common/definitions_constants.py");
if cfg!(not(feature = "sd_card")) {
files.remove(src, "apps/common/sdcard.py");
@@ -1195,7 +1194,6 @@ impl<'a> MpyBuilder<'a> {
if cfg!(feature = "universal_fw") {
files.add(src, "apps/common/definitions.py")?;
- files.add(src, "apps/common/definitions_constants.py")?;
files.add(src, "trezor/enums/DefinitionType.py")?;
files.add(src, "apps/cardano/*.py")?;
### core/embed/upymod/qstrdefsport.h
@@ -110,7 +110,6 @@ Q(apps.common.chunked)
Q(apps.common.coininfo)
Q(apps.common.coins)
Q(apps.common.definitions)
-Q(apps.common.definitions_constants)
Q(apps.common.keychain)
Q(apps.common.lock_manager)
Q(apps.common.passphrase)
@@ -216,7 +215,6 @@ Q(curve)
Q(curve_benchmark)
Q(decred)
Q(definitions)
-Q(definitions_constants)
Q(delizia)
Q(der)
Q(device)
### core/mocks/generated/trezordefinitions.pyi
@@ -1,7 +1,14 @@
from typing import *
from buffer_types import *
+from trezorproto import MessageType
+T = TypeVar("T", bound=MessageType)
# rust/src/definitions/obj.rs
-def verify(digest: AnyBytes, sig: AnyBytes, sigmask: int, version: int) -> None:
- """Verify the definitions signature."""
+def decode(
+ definition: AnyBytes,
+ expected_type: int,
+ msg_type: type[T],
+) -> T:
+ """Parse a signed definition blob, verify its signature and decode it
+ into the specified message type."""
### core/src/apps/common/definitions.py
@@ -23,21 +23,12 @@
def decode_definition(definition: AnyBytes, expected_type: type[DefType]) -> DefType:
- from trezor.crypto.hashlib import sha256
from trezor.enums import DefinitionType
- from trezor.protobuf import decode as protobuf_decode
- from trezor.utils import BufferReader
- from trezordefinitions import verify
-
- from apps.common import readers
-
- from . import definitions_constants as consts
-
- r = BufferReader(definition)
+ from trezordefinitions import decode
# determine the type number from the expected type
expected_type_number = DefinitionType.ETHEREUM_NETWORK
- # TODO: can't check equality of MsgDefObjs now, so we check the name
+ # NOTE: can't check equality of MsgDefObjs now, so we check the name
if expected_type.MESSAGE_NAME == EthereumTokenInfo.MESSAGE_NAME:
expected_type_number = DefinitionType.ETHEREUM_TOKEN
if expected_type.MESSAGE_NAME == SolanaTokenInfo.MESSAGE_NAME:
@@ -46,59 +37,9 @@ def decode_definition(definition: AnyBytes, expected_type: type[DefType]) -> Def
expected_type_number = DefinitionType.ETHEREUM_DISPLAY_FORMAT
try:
- # first check magic
- if r.read_memoryview(len(consts.MAGIC)) != consts.MAGIC:
- raise DataError("Invalid definition")
-
- # 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")
-
- # fourth check data version
- if readers.read_uint32_le(r) < consts.MIN_DATA_VERSION:
- raise DataError("Definition is outdated")
-
- # get payload
- payload_length = readers.read_uint16_le(r)
- payload = r.read_memoryview(payload_length)
-
- # at the end compute Merkle tree root hash using
- # provided leaf data (payload with prefix) and proof
- hasher = sha256(b"\x00")
- hasher.update(memoryview(definition)[: r.offset])
- hash = hasher.digest()
- proof_length = r.get()
- for _ in range(proof_length):
- proof_entry = r.read_memoryview(32)
- hash_a = min(hash, proof_entry) # type: ignore [not assignable to "bytes"]
- hash_b = max(hash, proof_entry) # type: ignore [not assignable to "bytes"]
- hasher = sha256(b"\x01")
- hasher.update(hash_a)
- hasher.update(hash_b)
- hash = hasher.digest()
-
- sigmask = r.get()
- signature = r.read_memoryview(64)
-
- if r.remaining_count():
- raise DataError("Invalid definition")
-
- except EOFError:
- raise DataError("Invalid definition")
-
- # verify signature
- try:
- verify(hash, signature, sigmask, format_version[0])
- except ValueError:
- raise DataError("Invalid definition signature")
-
- # decode it if it's OK
- try:
- return protobuf_decode(payload, expected_type, True)
- except (ValueError, EOFError):
- raise DataError("Invalid definition")
+ return decode(definition, expected_type_number, expected_type)
+ except ValueError as e:
+ if __debug__:
+ raise DataError(str(e))
+ else:
+ raise DataError("Invalid definitions")
### core/src/apps/common/definitions_constants.py
@@ -1,13 +0,0 @@
-# generated from definitions_constants.py.mako
-# (by running `make templates` in `core`)
-# do not edit manually!
-
-MIN_DATA_VERSION = 1783520408
-MAGIC = b"trzd"
-
-# Supported format versions of the definitions, encoded on the wire as
-# ASCII digit bytes ('1' = 0x31, '2' = 0x32, etc.).
-SUPPORTED_FORMAT_VERSIONS = (b"1", b"2")
-
-# 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
@@ -1,13 +0,0 @@
-# generated from definitions_constants.py.mako
-# (by running `make templates` in `core`)
-# do not edit manually!
-
-MIN_DATA_VERSION = ${defs_timestamp}
-MAGIC = b"trzd"
-
-# Supported format versions of the definitions, encoded on the wire as
-# ASCII digit bytes ('1' = 0x31, '2' = 0x32, etc.).
-SUPPORTED_FORMAT_VERSIONS = (b"1", b"2")
-
-# The public keys and signature thresholds for definitions verification
-# live in Rust (core/embed/rust/src/definitions/constants.rs).Why this scored 28/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.