feat(core): defs MIN_DATA_VERSION based on version
What changed, and why it matters
This change updates how Trezor devices decide whether downloaded Ethereum token/chain definition files are too old to trust. Previously there was one global cutoff date; now each definition format version has its own cutoff. This is a maintenance and correctness improvement, not a fix for an active security bug. It reduces the chance that a future format version could accidentally accept stale or incompatible definition data.
No urgent action required. Treat as routine maintenance. Reviewers should verify that the v1 and v2 timestamps correspond to correctly signed definitions releases and that the legacy firmware's continued use of the v1 timestamp is intentional.
Security signals we found
Defense-in-depth: per-version minimum timestamps prevent cross-version acceptance of stale definitions
Build-time metadata now fetched per format version with fallback to legacy all-in-one metadata
No new cryptographic checks or parsing of untrusted data introduced
No evidence of memory safety issues, bypass, or active vulnerability patched
Evidence from the diff
The commit introduces per-version minimum data timestamps for externally signed Ethereum definitions. It replaces a single MIN_DATA_VERSION constant with MIN_DATA_VERSION_V1 and MIN_DATA_VERSION_V2, updates the Rust DefsVersion enum to expose a min_data_version() method, and changes the build tooling (cointool.py) to fetch and record timestamps per format version from trezor/definitions. The legacy firmware continues to use only the v1 timestamp. The parsing code now rejects outdated definitions based on the version-specific threshold rather than a shared threshold.
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.makolegacy/firmware/ethereum_definitions_constants.h.makocommon/tools/cointool.pycommon/defs/ethereum/released-definitions-timestamp.jsoncommon/defs/ethereum/released-definitions-timestamp.txtInspect captured patch +104 / −24
### common/defs/ethereum/released-definitions-timestamp.json
@@ -0,0 +1 @@
+{"1": "2026-07-08T14:20:08+00:00", "2": "2026-08-31T12:07:39+00:00"}
### common/defs/ethereum/released-definitions-timestamp.txt
@@ -1 +0,0 @@
-2026-07-08T14:20:08+00:00
### common/tools/cointool.py
@@ -21,8 +21,14 @@
from coin_info import Coin, CoinBuckets, Coins, CoinsInfo, FidoApps, SupportInfo
DEFINITIONS_TIMESTAMP_PATH = (
- coin_info.DEFS_DIR / "ethereum" / "released-definitions-timestamp.txt"
+ coin_info.DEFS_DIR / "ethereum" / "released-definitions-timestamp.json"
)
+# Definitions format versions, kept in sync with ACTIVE_VERSIONS in
+# trezor/definitions. When a version is retired there, prune it here too,
+# otherwise MIN_DATA_VERSION for that version would freeze at its stale
+# timestamp.
+DEFINITIONS_FORMAT_VERSIONS: tuple[int, ...] = (1, 2)
+DEFINITIONS_METADATA_URL_TEMPLATE = "https://raw.githubusercontent.com/trezor/definitions/signed/definitions-latest-metadata-v{version}.json"
DEFINITIONS_LATEST_URL = "https://raw.githubusercontent.com/trezor/definitions/signed/definitions-latest.json"
HERE = Path(__file__).parent.resolve()
@@ -158,19 +164,30 @@ def supported_on(device: str, coins: Coins) -> Iterator[Coin]:
DEBUG_PREFIXES = ("debug",)
+def load_definitions_timestamps() -> dict[int, int]:
+ """Read per-version definitions timestamps, in unix seconds, keyed by version.
+
+ The timestamp file is a JSON object mapping format version (as string) to
+ an ISO 8601 datetime, written by the `new-definitions` command.
+ """
+ datetimes = json.loads(DEFINITIONS_TIMESTAMP_PATH.read_text())
+ return {
+ int(version): int(datetime.datetime.fromisoformat(dt).timestamp())
+ for version, dt in datetimes.items()
+ }
+
+
def render_file(
src: Path, dst: Path, coins: CoinsInfo, support_info: SupportInfo, models: list[str]
) -> None:
"""Renders `src` template into `dst`."""
template = mako.template.Template(filename=str(src.resolve()))
- eth_defs_date = datetime.datetime.fromisoformat(
- DEFINITIONS_TIMESTAMP_PATH.read_text().strip()
- )
+ defs_timestamps = load_definitions_timestamps()
this_file = Path(src)
result = template.render(
support_info=support_info,
supported_on=make_support_filter(support_info),
- defs_timestamp=int(eth_defs_date.timestamp()),
+ defs_timestamps=defs_timestamps,
THIS_FILE=this_file,
ROOT=ROOT,
ALTCOIN_PREFIXES=ALTCOIN_PREFIXES,
@@ -945,23 +962,73 @@ def do_render(src: Path, dst: Path) -> None:
do_render(file, file.parent / file.stem)
+def _fetch_json(url: str) -> Any:
+ assert requests is not None
+ response = requests.get(url)
+ response.raise_for_status()
+ return response.json()
+
+
+def _fetch_definitions_metadata() -> dict[int, Any]:
+ """Fetch per-version definitions metadata from the `signed` branch.
+
+ Returns a dict of metadata objects keyed by definitions format version.
+ """
+ assert requests is not None
+ metadata: dict[int, Any] = {}
+ missing: list[int] = []
+ for version in DEFINITIONS_FORMAT_VERSIONS:
+ url = DEFINITIONS_METADATA_URL_TEMPLATE.format(version=version)
+ response = requests.get(url)
+ if response.status_code == 404:
+ missing.append(version)
+ continue
+ response.raise_for_status()
+ metadata[version] = response.json()
+
+ if not missing:
+ return metadata
+
+ if metadata:
+ raise click.ClickException(
+ "Some per-version metadata files are missing from the `signed` branch "
+ f"(versions: {', '.join(map(str, missing))}). "
+ "This is an inconsistent state -- check the trezor/definitions repo."
+ )
+
+ # MIGRATION FALLBACK: the `signed` branch does not carry per-version
+ # metadata yet (definitions-latest-metadata-v*.json were introduced in
+ # trezor/definitions PR #113). The upcoming release is the last one
+ # reading the old all-in-one definitions-latest.json; once it is cut and
+ # the `signed` branch is re-signed, remove this fallback.
+ print_log(
+ logging.WARNING,
+ "Per-version definitions metadata not found on the `signed` branch, "
+ f"falling back to legacy {DEFINITIONS_LATEST_URL}",
+ )
+ legacy_metadata = _fetch_json(DEFINITIONS_LATEST_URL)["metadata"]
+ return {version: legacy_metadata for version in DEFINITIONS_FORMAT_VERSIONS}
+
+
# fmt: off
@cli.command()
@click.option("-v", "--verbose", is_flag=True, help="Print timestamp and merkle root")
# fmt: on
def new_definitions(verbose: bool) -> None:
"""Update timestamp of external coin definitions."""
- assert requests is not None
- eth_defs = requests.get(DEFINITIONS_LATEST_URL).json()
- eth_defs_date = eth_defs["metadata"]["datetime"]
- if verbose:
- click.echo(
- f"Latest definitions from {eth_defs_date}: {eth_defs['metadata']['merkle_root']}"
- )
- eth_defs_date = datetime.datetime.fromisoformat(eth_defs_date)
- DEFINITIONS_TIMESTAMP_PATH.write_text(
- eth_defs_date.isoformat(timespec="seconds") + "\n"
- )
+ metadata = _fetch_definitions_metadata()
+ datetimes = {}
+ for version in DEFINITIONS_FORMAT_VERSIONS:
+ eth_defs_date = metadata[version]["datetime"]
+ if verbose:
+ click.echo(
+ f"Latest definitions v{version} from {eth_defs_date}: "
+ f"{metadata[version]['merkle_root']}"
+ )
+ # normalize to whole seconds
+ parsed_date = datetime.datetime.fromisoformat(eth_defs_date)
+ datetimes[str(version)] = parsed_date.isoformat(timespec="seconds")
+ DEFINITIONS_TIMESTAMP_PATH.write_text(json.dumps(datetimes) + "\n")
if __name__ == "__main__":
### core/embed/rust/src/definitions/blob.rs
@@ -1,8 +1,8 @@
use crypto::merkle::merkle_root;
use crypto::{cosi, ed25519, sha256};
+use super::constants;
use super::error::Error;
-use super::{constants, generated};
use crate::io::InputStream;
fn verify_with_keys(
@@ -49,7 +49,7 @@ pub fn parse_and_verify(definition: &[u8], expected_type: u8) -> Result<&[u8], E
// data version
let data_version = reader.read_u32_le()?;
- if data_version < generated::MIN_DATA_VERSION {
+ if data_version < version.min_data_version() {
return Err(Error::Outdated);
}
### core/embed/rust/src/definitions/constants.rs
@@ -1,6 +1,7 @@
use crypto::ed25519;
use super::error::Error;
+use super::generated;
// Magic string at the beginning of every definition blob.
pub const MAGIC: &[u8; 4] = b"trzd";
@@ -21,6 +22,14 @@ impl DefsVersion {
}
}
+ // Minimum accepted data version (definitions timestamp) for this version.
+ pub const fn min_data_version(self) -> u32 {
+ match self {
+ DefsVersion::V1 => generated::MIN_DATA_VERSION_V1,
+ DefsVersion::V2 => generated::MIN_DATA_VERSION_V2,
+ }
+ }
+
pub const fn try_from_byte(byte: u8) -> Result<Self, Error> {
match byte {
b'1' => Ok(DefsVersion::V1),
### core/embed/rust/src/definitions/generated.rs
@@ -2,5 +2,7 @@
// (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;
+/// v1 definitions older than this data version are rejected.
+pub const MIN_DATA_VERSION_V1: u32 = 1783520408;
+/// v2 definitions older than this data version are rejected.
+pub const MIN_DATA_VERSION_V2: u32 = 1788178059;
### core/embed/rust/src/definitions/generated.rs.mako
@@ -2,5 +2,7 @@
// (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};
+/// v1 definitions older than this data version are rejected.
+pub const MIN_DATA_VERSION_V1: u32 = ${defs_timestamps[1]};
+/// v2 definitions older than this data version are rejected.
+pub const MIN_DATA_VERSION_V2: u32 = ${defs_timestamps[2]};
### legacy/firmware/ethereum_definitions_constants.h.mako
@@ -9,7 +9,7 @@
#include "crypto.h"
#include "pb.h"
-#define MIN_DATA_VERSION ${defs_timestamp}
+#define MIN_DATA_VERSION ${defs_timestamps[1]}
#define FORMAT_VERSION_LENGTH 5
#define FORMAT_VERSION (const pb_byte_t *)"trzd2"
Why this scored 19/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.