What changed, and why it matters
This commit only changes internal test scripts used to download Trezor firmware emulators for testing. It does not modify the actual device firmware, wallet logic, cryptography, or any code that end users rely on. There is no security issue visible in the change.
No security action required; this is a test-infrastructure maintenance change. Reviewers may optionally verify the new download URL and manifest parsing behave correctly during CI.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch replaces a recursive wget-based shell script with a Python script that downloads emulator binaries per device model from a new URL layout (https://data.trezor.io/dev/firmware/releases/emulators-new). It adds availability checks, executable-bit handling, and uses a local releases.json manifest. No cryptographic, parsing, or privileged operations are introduced, and the download URL is fixed to a Trezor-controlled domain over HTTPS.
Changed components
tests/download_emulators.shtests/download_emulators.pyInspect captured patch +159 / −16
diff --git a/tests/download_emulators.py b/tests/download_emulators.py
new file mode 100755
index 000000000..43b237471
--- /dev/null
+++ b/tests/download_emulators.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3
+import json
+import stat
+from http import HTTPStatus
+from pathlib import Path
+from typing import TypeAlias
+
+import click
+import requests
+from emulators import ALL_MODELS, gen_from_model
+
+EmulatorDict: TypeAlias = dict[str, list[str]]
+
+
+OLDEST_AVAILABLE = {
+ "legacy": (1, 6, 2),
+ "core": (2, 0, 8),
+}
+
+EMULATORS_URL_PREFIX = "https://data.trezor.io/dev/firmware/releases/emulators-new"
+
+TESTS_DIR = Path(__file__).resolve().parent
+SAVE_DIR = TESTS_DIR / "emulators"
+RELEASES_JSON = TESTS_DIR.parent / "common" / "releases.json"
+
+
+class MissingArtifactError(Exception):
+ model: str
+ version: str
+
+ def __init__(self, model: str, version: str) -> None:
+ self.model = model
+ self.version = version
+ super().__init__()
+
+
+class KnownMissingArtifactError(MissingArtifactError):
+ pass
+
+
+class Emulator:
+ version: str
+ model: str
+ url: str
+ save_path: Path | None = None
+
+ def __init__(self, version: str, model: str) -> None:
+ self.version = version
+ self.model = model
+ self.url = self._get_download_url()
+
+ def download(
+ self, save_path: Path | None = None, skip_if_exists: bool = True
+ ) -> None:
+ if save_path is None:
+ save_path = self._get_default_save_path()
+
+ self.save_path = save_path
+
+ if save_path.exists() and skip_if_exists:
+ # Skipping
+ return
+
+ # Make sure that all parent directories exist.
+ # If not, create them.
+ save_path.parent.mkdir(parents=True, exist_ok=True)
+
+ click.echo(f"Downloading from {self.url} to {save_path}")
+ with requests.get(self.url, stream=True, timeout=30) as r:
+ r.raise_for_status()
+ with open(save_path, "wb") as f:
+ for chunk in r.iter_content(chunk_size=8192):
+ f.write(chunk)
+
+ def set_as_executable(self) -> None:
+ if self.save_path is None:
+ raise click.ClickException("Run `download` first")
+
+ path = Path(self.save_path)
+ path.chmod(path.stat().st_mode | stat.S_IXUSR)
+
+ def check_download_availability(self) -> None:
+
+ version_tuple = tuple(int(part) for part in self.version.split("."))
+ if version_tuple < OLDEST_AVAILABLE[gen_from_model(self.model)]:
+ # Is old known-to-be-unavailable version
+ raise KnownMissingArtifactError(self.model, self.version)
+
+ status_code = requests.head(self.url, timeout=10).status_code
+ if status_code != HTTPStatus.OK:
+ # Not available for download
+ raise MissingArtifactError(self.model, self.version)
+
+ def _get_filename(self) -> str:
+ return f"trezor-emu-{gen_from_model(self.model)}-{self.model}-v{self.version}"
+
+ def _get_default_save_path(self) -> Path:
+ return SAVE_DIR / self.model / self._get_filename()
+
+ def _get_download_url(self) -> str:
+ return f"{EMULATORS_URL_PREFIX}/{self.model}/{self._get_filename()}"
+
+
+def get_all_releases() -> EmulatorDict:
+ with RELEASES_JSON.open(encoding="utf-8") as f:
+ releases: dict[str, EmulatorDict] = json.load(f)
+ return releases["firmware"]
+
+
+def get_emulators_for_model(model: str, firmwares: EmulatorDict) -> list[Emulator]:
+ emulators: list[Emulator] = []
+ for version, models in firmwares.items():
+ if model in models:
+ try:
+ emu = Emulator(version, model)
+ emu.check_download_availability()
+ emulators.append(emu)
+ except KnownMissingArtifactError:
+ # Old artifacts that are known to be unavailable
+ pass
+ except MissingArtifactError as e:
+ click.echo(
+ f"Artifact for model {e.model}, version: {e.version} is unavailable!"
+ )
+ return emulators
+
+
+def download_emulators_for_model(model: str) -> None:
+ if model not in ALL_MODELS:
+ raise ValueError(f"Unknown model: {model}")
+
+ all_releases = get_all_releases()
+ emus = get_emulators_for_model(model, all_releases)
+
+ for emu in emus:
+ emu.download()
+ emu.set_as_executable()
+
+
+@click.command()
+@click.argument("model", type=click.Choice(ALL_MODELS, case_sensitive=True))
+def main(model: str) -> None:
+ """
+ Download all available emulators for a given Trezor model.
+ """
+ download_emulators_for_model(model)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/download_emulators.sh b/tests/download_emulators.sh
index 11744e734..1db5b2d88 100755
--- a/tests/download_emulators.sh
+++ b/tests/download_emulators.sh
@@ -1,24 +1,17 @@
#!/usr/bin/env bash
set -e
+if [ $# -lt 1 ]; then
+ echo "Usage: $0 <model>"
+ exit 1
+fi
-SITE="https://data.trezor.io/dev/firmware/releases/emulators/"
-cd "$(dirname "$0")"
+MODEL="$1"
-# download all emulators without index files, without directories and only if not present
-wget -e robots=off \
- --no-verbose \
- --no-clobber \
- --no-parent \
- --no-directories \
- --no-host-directories \
- --recursive \
- --reject "index.html*" \
- --reject "-arm" \
- -P emulators/ \
- $SITE
+cd "$(dirname "$0")"
-chmod u+x emulators/trezor-emu-*
+# download emulators for the given model if not already present
+uv run python download_emulators.py "$MODEL"
cd ..
# are we in Nix(OS)?
-command -v nix-shell >/dev/null && nix-shell --run 'NIX_BINTOOLS=$NIX_BINTOOLS_FOR_TARGET autoPatchelf tests/emulators'
+command -v nix-shell >/dev/null && nix-shell --run 'NIX_BINTOOLS=$NIX_BINTOOLS_FOR_TARGET autoPatchelf tests/emulators/$MODEL'
Why this scored 15/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.