mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-26 16:18:22 +00:00
feat(python/trezorctl): improving and refactoring firmware CLI commands
This commit is contained in:
parent
e67b3ab214
commit
6bbc9a78f3
1
python/.changelog.d/1258.added
Normal file
1
python/.changelog.d/1258.added
Normal file
@ -0,0 +1 @@
|
||||
CLI: two new firmware commands - "trezorctl firmware download" and "trezorctl firmware verify"
|
1
python/.changelog.d/1258.changed
Normal file
1
python/.changelog.d/1258.changed
Normal file
@ -0,0 +1 @@
|
||||
CLI: "trezorctl firmware-update" command changed to "trezorctl firmware update"
|
@ -7,6 +7,7 @@ list of credits:
|
||||
alepop <https://github.com/alepop>
|
||||
Jan 'matejcik' Matějek <jan.matejek@satoshilabs.com>
|
||||
Jan Pochyla <jan.pochyla@satoshilabs.com>
|
||||
Jiří Musil <https://github.com/grdddj>
|
||||
Jochen Hoenicke <hoenicke@gmail.com>
|
||||
Jonathan Cross <https://github.com/jonathancross>
|
||||
Karel Bílek <karel.bilek@satoshilabs.com>
|
||||
|
@ -46,7 +46,7 @@ on one page here.
|
||||
eos EOS commands.
|
||||
ethereum Ethereum commands.
|
||||
fido FIDO2, U2F and WebAuthN management commands.
|
||||
firmware-update Upload new firmware to device.
|
||||
firmware Firmware commands.
|
||||
get-features Retrieve device features and settings.
|
||||
get-session Get a session ID for subsequent commands.
|
||||
lisk Lisk commands.
|
||||
@ -101,6 +101,7 @@ Bitcoin and Bitcoin-like coins commands.
|
||||
|
||||
Commands:
|
||||
get-address Get address for specified path.
|
||||
get-descriptor Get descriptor of given account.
|
||||
get-public-node Get public node of given path.
|
||||
sign-message Sign message using address of given path.
|
||||
sign-tx Sign transaction.
|
||||
@ -186,7 +187,6 @@ Miscellaneous debug features.
|
||||
|
||||
Commands:
|
||||
send-bytes Send raw bytes to Trezor.
|
||||
show-text Show text on Trezor display.
|
||||
|
||||
Device management commands - setup, recover seed, wipe, etc.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -276,6 +276,27 @@ FIDO2, U2F and WebAuthN management commands.
|
||||
counter Get or set the FIDO/U2F counter value.
|
||||
credentials Manage FIDO2 resident credentials.
|
||||
|
||||
Firmware commands.
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
trezorctl firmware --help
|
||||
|
||||
.. code::
|
||||
|
||||
Usage: trezorctl firmware [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Firmware commands.
|
||||
|
||||
Options:
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
download Download and save the firmware image.
|
||||
update Upload new firmware to device.
|
||||
verify Verify the integrity of the firmware data stored in a file.
|
||||
|
||||
Lisk commands.
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@ -376,15 +397,16 @@ Device settings.
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
auto-lock-delay Set auto-lock delay (in seconds).
|
||||
display-rotation Set display rotation.
|
||||
flags Set device flags.
|
||||
homescreen Set new homescreen.
|
||||
label Set new device label.
|
||||
passphrase Enable, disable or configure passphrase protection.
|
||||
pin Set, change or remove PIN.
|
||||
safety-checks Set safety check level.
|
||||
wipe-code Set or remove the wipe code.
|
||||
auto-lock-delay Set auto-lock delay (in seconds).
|
||||
display-rotation Set display rotation.
|
||||
experimental-features Enable or disable experimental message types.
|
||||
flags Set device flags.
|
||||
homescreen Set new homescreen.
|
||||
label Set new device label.
|
||||
passphrase Enable, disable or configure passphrase protection.
|
||||
pin Set, change or remove PIN.
|
||||
safety-checks Set safety check level.
|
||||
wipe-code Set or remove the wipe code.
|
||||
|
||||
Stellar commands.
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
@ -14,13 +14,17 @@
|
||||
# You should have received a copy of the License along with this library.
|
||||
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import BinaryIO
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import click
|
||||
import requests
|
||||
|
||||
from .. import exceptions, firmware
|
||||
from . import with_client
|
||||
from ..client import TrezorClient
|
||||
from . import TrezorConnection, with_client
|
||||
|
||||
ALLOWED_FIRMWARE_FORMATS = {
|
||||
1: (firmware.FirmwareFormat.TREZOR_ONE, firmware.FirmwareFormat.TREZOR_ONE_V2),
|
||||
@ -28,12 +32,34 @@ ALLOWED_FIRMWARE_FORMATS = {
|
||||
}
|
||||
|
||||
|
||||
def _print_version(version):
|
||||
def _print_version(version: dict) -> None:
|
||||
vstr = "Firmware version {major}.{minor}.{patch} build {build}".format(**version)
|
||||
click.echo(vstr)
|
||||
|
||||
|
||||
def validate_firmware(version, fw, expected_fingerprint=None):
|
||||
def _is_bootloader_onev2(client: TrezorClient) -> bool:
|
||||
"""Check if bootloader is capable of installing the Trezor One v2 firmware directly.
|
||||
|
||||
This is the case from bootloader version 1.8.0, and also holds for firmware version
|
||||
1.8.0 because that installs the appropriate bootloader.
|
||||
"""
|
||||
f = client.features
|
||||
version = (f.major_version, f.minor_version, f.patch_version)
|
||||
bootloader_onev2 = f.major_version == 1 and version >= (1, 8, 0)
|
||||
return bootloader_onev2
|
||||
|
||||
|
||||
def _get_file_name_from_url(url: str) -> str:
|
||||
"""Parse the name of the file being downloaded from the specific url."""
|
||||
full_path = urlparse(url).path
|
||||
return os.path.basename(full_path)
|
||||
|
||||
|
||||
def print_firmware_version(
|
||||
version: str,
|
||||
fw: firmware.ParsedFirmware,
|
||||
) -> None:
|
||||
"""Print out the firmware version and details."""
|
||||
if version == firmware.FirmwareFormat.TREZOR_ONE:
|
||||
if fw.embedded_onev2:
|
||||
click.echo("Trezor One firmware with embedded v2 image (1.8.0 or later)")
|
||||
@ -47,9 +73,20 @@ def validate_firmware(version, fw, expected_fingerprint=None):
|
||||
click.echo("Trezor T firmware image.")
|
||||
vendor = fw.vendor_header.text
|
||||
vendor_version = "{major}.{minor}".format(**fw.vendor_header.version)
|
||||
click.echo("Vendor header from {}, version {}".format(vendor, vendor_version))
|
||||
click.echo(f"Vendor header from {vendor}, version {vendor_version}")
|
||||
_print_version(fw.image.header.version)
|
||||
|
||||
|
||||
def validate_signatures(
|
||||
version: str,
|
||||
fw: firmware.ParsedFirmware,
|
||||
) -> None:
|
||||
"""Check the signatures on the firmware.
|
||||
|
||||
Prints the validity status.
|
||||
In case of Trezor One v1 prompts the user (as the signature is missing).
|
||||
Exits if the validation fails.
|
||||
"""
|
||||
try:
|
||||
firmware.validate(version, fw, allow_unsigned=False)
|
||||
click.echo("Signatures are valid.")
|
||||
@ -68,24 +105,63 @@ def validate_firmware(version, fw, expected_fingerprint=None):
|
||||
click.echo("Firmware validation failed, aborting.")
|
||||
sys.exit(4)
|
||||
|
||||
|
||||
def validate_fingerprint(
|
||||
version: str, fw: firmware.ParsedFirmware, expected_fingerprint: str = None
|
||||
) -> None:
|
||||
"""Determine and validate the firmware fingerprint.
|
||||
|
||||
Prints the fingerprint.
|
||||
Exits if the validation fails.
|
||||
"""
|
||||
fingerprint = firmware.digest(version, fw).hex()
|
||||
click.echo("Firmware fingerprint: {}".format(fingerprint))
|
||||
click.echo(f"Firmware fingerprint: {fingerprint}")
|
||||
if version == firmware.FirmwareFormat.TREZOR_ONE and fw.embedded_onev2:
|
||||
fingerprint_onev2 = firmware.digest(
|
||||
firmware.FirmwareFormat.TREZOR_ONE_V2, fw.embedded_onev2
|
||||
).hex()
|
||||
click.echo("Embedded v2 image fingerprint: {}".format(fingerprint_onev2))
|
||||
click.echo(f"Embedded v2 image fingerprint: {fingerprint_onev2}")
|
||||
if expected_fingerprint and fingerprint != expected_fingerprint:
|
||||
click.echo("Expected fingerprint: {}".format(expected_fingerprint))
|
||||
click.echo(f"Expected fingerprint: {expected_fingerprint}")
|
||||
click.echo("Fingerprints do not match, aborting.")
|
||||
sys.exit(5)
|
||||
|
||||
|
||||
def find_best_firmware_version(
|
||||
bootloader_version, requested_version=None, beta=False, bitcoin_only=False
|
||||
):
|
||||
url = "https://data.trezor.io/firmware/{}/releases.json"
|
||||
releases = requests.get(url.format(bootloader_version[0])).json()
|
||||
def check_device_match(
|
||||
version: str,
|
||||
fw: firmware.ParsedFirmware,
|
||||
bootloader_onev2: bool,
|
||||
trezor_major_version: int,
|
||||
) -> None:
|
||||
"""Validate if the device and firmware are compatible.
|
||||
|
||||
Prints error message and exits if the validation fails.
|
||||
"""
|
||||
if (
|
||||
bootloader_onev2
|
||||
and version == firmware.FirmwareFormat.TREZOR_ONE
|
||||
and not fw.embedded_onev2
|
||||
):
|
||||
click.echo("Firmware is too old for your device. Aborting.")
|
||||
sys.exit(3)
|
||||
elif not bootloader_onev2 and version == firmware.FirmwareFormat.TREZOR_ONE_V2:
|
||||
click.echo("You need to upgrade to bootloader 1.8.0 first.")
|
||||
sys.exit(3)
|
||||
|
||||
if trezor_major_version not in ALLOWED_FIRMWARE_FORMATS:
|
||||
click.echo("trezorctl doesn't know your device version. Aborting.")
|
||||
sys.exit(3)
|
||||
elif version not in ALLOWED_FIRMWARE_FORMATS[trezor_major_version]:
|
||||
click.echo("Firmware does not match your device, aborting.")
|
||||
sys.exit(3)
|
||||
|
||||
|
||||
def get_all_firmware_releases(
|
||||
bitcoin_only: bool, beta: bool, major_version: int
|
||||
) -> list:
|
||||
"""Get sorted list of all releases suitable for inputted parameters"""
|
||||
url = f"https://data.trezor.io/firmware/{major_version}/releases.json"
|
||||
releases = requests.get(url).json()
|
||||
if not releases:
|
||||
raise click.ClickException("Failed to get list of releases")
|
||||
|
||||
@ -104,23 +180,56 @@ def find_best_firmware_version(
|
||||
|
||||
releases.sort(key=lambda r: r["version"], reverse=True)
|
||||
|
||||
return releases
|
||||
|
||||
|
||||
def find_best_firmware_version(
|
||||
client: TrezorClient,
|
||||
version: str = None,
|
||||
beta: bool = False,
|
||||
bitcoin_only: bool = False,
|
||||
) -> tuple:
|
||||
"""Get the url from which to download the firmware and its expected fingerprint.
|
||||
|
||||
When the version (X.Y.Z) is specified, checks for that specific release.
|
||||
Otherwise takes the latest one.
|
||||
|
||||
If the specified version is not found, prints the closest available version
|
||||
(higher than the specified one, if existing).
|
||||
"""
|
||||
|
||||
def version_str(version):
|
||||
return ".".join(map(str, version))
|
||||
|
||||
want_version = requested_version
|
||||
f = client.features
|
||||
|
||||
releases = get_all_firmware_releases(bitcoin_only, beta, f.major_version)
|
||||
highest_version = releases[0]["version"]
|
||||
|
||||
if want_version is None:
|
||||
if version:
|
||||
want_version = [int(x) for x in version.split(".")]
|
||||
if len(want_version) != 3:
|
||||
click.echo("Please use the 'X.Y.Z' version format.")
|
||||
if want_version[0] != f.major_version:
|
||||
model = f.model or "1"
|
||||
click.echo(
|
||||
f"Warning: Trezor {model} firmware version should be "
|
||||
f"{f.major_version}.X.Y (requested: {version})"
|
||||
)
|
||||
else:
|
||||
want_version = highest_version
|
||||
click.echo("Best available version: {}".format(version_str(want_version)))
|
||||
click.echo(f"Best available version: {version_str(want_version)}")
|
||||
|
||||
# Identifying the release we will install
|
||||
# It may happen that the different version will need to be installed first
|
||||
confirm_different_version = False
|
||||
while True:
|
||||
# The want_version can be changed below, need to redefine it
|
||||
want_version_str = version_str(want_version)
|
||||
try:
|
||||
release = next(r for r in releases if r["version"] == want_version)
|
||||
except StopIteration:
|
||||
click.echo("Version {} not found for your device.".format(want_version_str))
|
||||
click.echo(f"Version {want_version_str} not found for your device.")
|
||||
|
||||
# look for versions starting with the lowest
|
||||
for release in reversed(releases):
|
||||
@ -129,31 +238,35 @@ def find_best_firmware_version(
|
||||
# stop at first that is higher than the requested
|
||||
break
|
||||
# if there was no break, the newest is used
|
||||
click.echo(
|
||||
"Closest available version: {}".format(version_str(closest_version))
|
||||
)
|
||||
click.echo(f"Closest available version: {version_str(closest_version)}")
|
||||
if not beta and want_version > highest_version:
|
||||
click.echo("Hint: specify --beta to look for a beta release.")
|
||||
sys.exit(1)
|
||||
|
||||
if (
|
||||
"min_bootloader_version" in release
|
||||
and release["min_bootloader_version"] > bootloader_version
|
||||
):
|
||||
need_version_str = version_str(release["min_firmware_version"])
|
||||
# It can be impossible to update from a very old version directly
|
||||
# to the newer one, in that case update to the minimal
|
||||
# compatible version first
|
||||
# Choosing the version key to compare based on (not) being in BL mode
|
||||
client_version = [f.major_version, f.minor_version, f.patch_version]
|
||||
if f.bootloader_mode:
|
||||
key_to_compare = "min_bootloader_version"
|
||||
else:
|
||||
key_to_compare = "min_firmware_version"
|
||||
|
||||
if key_to_compare in release and release[key_to_compare] > client_version:
|
||||
need_version = release["min_firmware_version"]
|
||||
need_version_str = version_str(need_version)
|
||||
click.echo(
|
||||
"Version {} is required before upgrading to {}.".format(
|
||||
need_version_str, want_version_str
|
||||
)
|
||||
f"Version {need_version_str} is required before upgrading to {want_version_str}."
|
||||
)
|
||||
want_version = release["min_firmware_version"]
|
||||
want_version = need_version
|
||||
confirm_different_version = True
|
||||
else:
|
||||
break
|
||||
|
||||
if confirm_different_version:
|
||||
installing_different = "Installing version {} instead.".format(want_version_str)
|
||||
if requested_version is None:
|
||||
installing_different = f"Installing version {want_version_str} instead."
|
||||
if version is None:
|
||||
click.echo(installing_different)
|
||||
else:
|
||||
ok = click.confirm(installing_different + " Continue?", default=True)
|
||||
@ -166,39 +279,212 @@ def find_best_firmware_version(
|
||||
else:
|
||||
url = release["url"]
|
||||
fingerprint = release["fingerprint"]
|
||||
if not url.startswith("data/"):
|
||||
click.echo("Unsupported URL found: {}".format(url))
|
||||
|
||||
url_prefix = "data/"
|
||||
if not url.startswith(url_prefix):
|
||||
click.echo(f"Unsupported URL found: {url}")
|
||||
sys.exit(1)
|
||||
url = "https://data.trezor.io/" + url[5:]
|
||||
final_url = "https://data.trezor.io/" + url[len(url_prefix) :]
|
||||
|
||||
return url, fingerprint
|
||||
return final_url, fingerprint
|
||||
|
||||
|
||||
@click.command()
|
||||
def download_firmware_data(url: str) -> bytes:
|
||||
try:
|
||||
click.echo(f"Downloading from {url}")
|
||||
r = requests.get(url)
|
||||
r.raise_for_status()
|
||||
return r.content
|
||||
except requests.exceptions.HTTPError as err:
|
||||
click.echo(f"Error downloading file: {err}")
|
||||
sys.exit(3)
|
||||
|
||||
|
||||
def validate_firmware(
|
||||
firmware_data: bytes,
|
||||
fingerprint: str = None,
|
||||
bootloader_onev2: bool = None,
|
||||
trezor_major_version: int = None,
|
||||
) -> None:
|
||||
"""Validate the firmware through multiple tests.
|
||||
|
||||
- parsing it properly
|
||||
- containing valid signatures and fingerprint (when chosen)
|
||||
- being compatible with the device (when chosen)
|
||||
"""
|
||||
try:
|
||||
version, fw = firmware.parse(firmware_data)
|
||||
except Exception as e:
|
||||
click.echo(e)
|
||||
sys.exit(2)
|
||||
|
||||
print_firmware_version(version, fw)
|
||||
validate_signatures(version, fw)
|
||||
validate_fingerprint(version, fw, fingerprint)
|
||||
|
||||
if bootloader_onev2 is not None and trezor_major_version is not None:
|
||||
check_device_match(
|
||||
version=version,
|
||||
fw=fw,
|
||||
bootloader_onev2=bootloader_onev2,
|
||||
trezor_major_version=trezor_major_version,
|
||||
)
|
||||
click.echo("Firmware is appropriate for your device.")
|
||||
|
||||
|
||||
def extract_embedded_fw(
|
||||
firmware_data: bytes,
|
||||
bootloader_onev2: bool,
|
||||
) -> bytes:
|
||||
"""Modify the firmware data for sending into Trezor, if necessary."""
|
||||
# special handling for embedded-OneV2 format:
|
||||
# for bootloader < 1.8, keep the embedding
|
||||
# for bootloader 1.8.0 and up, strip the old OneV1 header
|
||||
if (
|
||||
bootloader_onev2
|
||||
and firmware_data[:4] == b"TRZR"
|
||||
and firmware_data[256 : 256 + 4] == b"TRZF"
|
||||
):
|
||||
click.echo("Extracting embedded firmware image.")
|
||||
return firmware_data[256:]
|
||||
|
||||
return firmware_data
|
||||
|
||||
|
||||
def upload_firmware_into_device(
|
||||
client: TrezorClient,
|
||||
firmware_data: bytes,
|
||||
) -> None:
|
||||
"""Perform the final act of loading the firmware into Trezor."""
|
||||
f = client.features
|
||||
try:
|
||||
if f.major_version == 1 and f.firmware_present is not False:
|
||||
# Trezor One does not send ButtonRequest
|
||||
click.echo("Please confirm the action on your Trezor device")
|
||||
firmware.update(client, firmware_data)
|
||||
except exceptions.Cancelled:
|
||||
click.echo("Update aborted on device.")
|
||||
except exceptions.TrezorException as e:
|
||||
click.echo(f"Update failed: {e}")
|
||||
sys.exit(3)
|
||||
|
||||
|
||||
@click.group(name="firmware")
|
||||
def cli():
|
||||
"""Firmware commands."""
|
||||
|
||||
|
||||
@cli.command()
|
||||
# fmt: off
|
||||
@click.option("-f", "--filename", type=click.File("rb"))
|
||||
@click.option("-u", "--url")
|
||||
@click.option("-v", "--version")
|
||||
@click.argument("filename", type=click.File("rb"))
|
||||
@click.option("-c", "--check-device", is_flag=True, help="Validate device compatibility")
|
||||
@click.option("--fingerprint", help="Expected firmware fingerprint in hex")
|
||||
@click.pass_obj
|
||||
# fmt: on
|
||||
def verify(
|
||||
obj: TrezorConnection,
|
||||
filename: BinaryIO,
|
||||
check_device: bool,
|
||||
fingerprint: str,
|
||||
) -> None:
|
||||
"""Verify the integrity of the firmware data stored in a file.
|
||||
|
||||
By default the device is not checked and does not need to be connected.
|
||||
Its validation must be specified.
|
||||
|
||||
In case of validation failure exits with the appropriate exit code.
|
||||
"""
|
||||
# Deciding if to take the device into account
|
||||
if check_device:
|
||||
with obj.client_context() as client:
|
||||
bootloader_onev2 = _is_bootloader_onev2(client)
|
||||
trezor_major_version = client.features.major_version
|
||||
else:
|
||||
bootloader_onev2 = None
|
||||
trezor_major_version = None
|
||||
|
||||
firmware_data = filename.read()
|
||||
validate_firmware(
|
||||
firmware_data=firmware_data,
|
||||
fingerprint=fingerprint,
|
||||
bootloader_onev2=bootloader_onev2,
|
||||
trezor_major_version=trezor_major_version,
|
||||
)
|
||||
|
||||
|
||||
@cli.command()
|
||||
# fmt: off
|
||||
@click.option("-o", "--output", type=click.File("wb"), help="Output file to save firmware data to")
|
||||
@click.option("-v", "--version", help="Which version to download")
|
||||
@click.option("-s", "--skip-check", is_flag=True, help="Do not validate firmware integrity")
|
||||
@click.option("--beta", is_flag=True, help="Use firmware from BETA channel")
|
||||
@click.option("--bitcoin-only", is_flag=True, help="Use bitcoin-only firmware (if possible)")
|
||||
@click.option("--fingerprint", help="Expected firmware fingerprint in hex")
|
||||
# fmt: on
|
||||
@with_client
|
||||
def download(
|
||||
client: TrezorClient,
|
||||
output: BinaryIO,
|
||||
version: str,
|
||||
skip_check: bool,
|
||||
fingerprint: str,
|
||||
beta: bool,
|
||||
bitcoin_only: bool,
|
||||
) -> None:
|
||||
"""Download and save the firmware image.
|
||||
|
||||
Validation is done by default, can be omitted by "-s" or "--skip-check".
|
||||
When fingerprint or output file are not set, take them from SL servers.
|
||||
"""
|
||||
url, fp = find_best_firmware_version(
|
||||
client=client, version=version, beta=beta, bitcoin_only=bitcoin_only
|
||||
)
|
||||
|
||||
firmware_data = download_firmware_data(url)
|
||||
|
||||
if not fingerprint:
|
||||
fingerprint = fp
|
||||
|
||||
if not skip_check:
|
||||
validate_firmware(
|
||||
firmware_data=firmware_data,
|
||||
fingerprint=fingerprint,
|
||||
bootloader_onev2=_is_bootloader_onev2(client),
|
||||
trezor_major_version=client.features.major_version,
|
||||
)
|
||||
|
||||
if not output:
|
||||
output = open(_get_file_name_from_url(url), "wb")
|
||||
output.write(firmware_data)
|
||||
output.close()
|
||||
click.echo(f"Firmware saved under {output.name}.")
|
||||
|
||||
|
||||
@cli.command()
|
||||
# fmt: off
|
||||
@click.option("-f", "--filename", type=click.File("rb"), help="File containing firmware data")
|
||||
@click.option("-u", "--url", help="Where to get the firmware from - full link")
|
||||
@click.option("-v", "--version", help="Which version to download")
|
||||
@click.option("-s", "--skip-check", is_flag=True, help="Do not validate firmware integrity")
|
||||
@click.option("-n", "--dry-run", is_flag=True, help="Perform all steps but do not actually upload the firmware")
|
||||
@click.option("--beta", is_flag=True, help="Use firmware from BETA channel")
|
||||
@click.option("--bitcoin-only", is_flag=True, help="Use bitcoin-only firmware (if possible)")
|
||||
@click.option("--raw", is_flag=True, help="Push raw data to Trezor")
|
||||
@click.option("--raw", is_flag=True, help="Push raw firmware data to Trezor")
|
||||
@click.option("--fingerprint", help="Expected firmware fingerprint in hex")
|
||||
# fmt: on
|
||||
@with_client
|
||||
def firmware_update(
|
||||
client,
|
||||
filename,
|
||||
url,
|
||||
version,
|
||||
skip_check,
|
||||
fingerprint,
|
||||
raw,
|
||||
dry_run,
|
||||
beta,
|
||||
bitcoin_only,
|
||||
):
|
||||
def update(
|
||||
client: TrezorClient,
|
||||
filename: BinaryIO,
|
||||
url: str,
|
||||
version: str,
|
||||
skip_check: bool,
|
||||
fingerprint: str,
|
||||
raw: bool,
|
||||
dry_run: bool,
|
||||
beta: bool,
|
||||
bitcoin_only: bool,
|
||||
) -> None:
|
||||
"""Upload new firmware to device.
|
||||
|
||||
Device must be in bootloader mode.
|
||||
@ -220,86 +506,36 @@ def firmware_update(
|
||||
click.echo("Please switch your device to bootloader mode.")
|
||||
sys.exit(1)
|
||||
|
||||
# bootloader for T1 does not export 'model', so we rely on major_version
|
||||
f = client.features
|
||||
bootloader_version = (f.major_version, f.minor_version, f.patch_version)
|
||||
bootloader_onev2 = f.major_version == 1 and bootloader_version >= (1, 8, 0)
|
||||
model = client.features.model or "1"
|
||||
|
||||
if filename:
|
||||
data = filename.read()
|
||||
firmware_data = filename.read()
|
||||
else:
|
||||
if not url:
|
||||
if version:
|
||||
version_list = [int(x) for x in version.split(".")]
|
||||
if version_list[0] != bootloader_version[0]:
|
||||
click.echo(
|
||||
"Warning: Trezor {} firmware version should be {}.X.Y (requested: {})".format(
|
||||
model, bootloader_version[0], version
|
||||
)
|
||||
)
|
||||
else:
|
||||
version_list = None
|
||||
url, fp = find_best_firmware_version(
|
||||
list(bootloader_version), version_list, beta, bitcoin_only
|
||||
client=client, version=version, beta=beta, bitcoin_only=bitcoin_only
|
||||
)
|
||||
if not fingerprint:
|
||||
fingerprint = fp
|
||||
|
||||
try:
|
||||
click.echo("Downloading from {}".format(url))
|
||||
r = requests.get(url)
|
||||
r.raise_for_status()
|
||||
except requests.exceptions.HTTPError as err:
|
||||
click.echo("Error downloading file: {}".format(err))
|
||||
sys.exit(3)
|
||||
|
||||
data = r.content
|
||||
firmware_data = download_firmware_data(url)
|
||||
|
||||
if not raw and not skip_check:
|
||||
try:
|
||||
version, fw = firmware.parse(data)
|
||||
except Exception as e:
|
||||
click.echo(e)
|
||||
sys.exit(2)
|
||||
|
||||
validate_firmware(version, fw, fingerprint)
|
||||
if (
|
||||
bootloader_onev2
|
||||
and version == firmware.FirmwareFormat.TREZOR_ONE
|
||||
and not fw.embedded_onev2
|
||||
):
|
||||
click.echo("Firmware is too old for your device. Aborting.")
|
||||
sys.exit(3)
|
||||
elif not bootloader_onev2 and version == firmware.FirmwareFormat.TREZOR_ONE_V2:
|
||||
click.echo("You need to upgrade to bootloader 1.8.0 first.")
|
||||
sys.exit(3)
|
||||
|
||||
if f.major_version not in ALLOWED_FIRMWARE_FORMATS:
|
||||
click.echo("trezorctl doesn't know your device version. Aborting.")
|
||||
sys.exit(3)
|
||||
elif version not in ALLOWED_FIRMWARE_FORMATS[f.major_version]:
|
||||
click.echo("Firmware does not match your device, aborting.")
|
||||
sys.exit(3)
|
||||
validate_firmware(
|
||||
firmware_data=firmware_data,
|
||||
fingerprint=fingerprint,
|
||||
bootloader_onev2=_is_bootloader_onev2(client),
|
||||
trezor_major_version=client.features.major_version,
|
||||
)
|
||||
|
||||
if not raw:
|
||||
# special handling for embedded-OneV2 format:
|
||||
# for bootloader < 1.8, keep the embedding
|
||||
# for bootloader 1.8.0 and up, strip the old OneV1 header
|
||||
if bootloader_onev2 and data[:4] == b"TRZR" and data[256 : 256 + 4] == b"TRZF":
|
||||
click.echo("Extracting embedded firmware image.")
|
||||
data = data[256:]
|
||||
firmware_data = extract_embedded_fw(
|
||||
firmware_data=firmware_data,
|
||||
bootloader_onev2=_is_bootloader_onev2(client),
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
click.echo("Dry run. Not uploading firmware to device.")
|
||||
else:
|
||||
try:
|
||||
if f.major_version == 1 and f.firmware_present is not False:
|
||||
# Trezor One does not send ButtonRequest
|
||||
click.echo("Please confirm the action on your Trezor device")
|
||||
return firmware.update(client, data)
|
||||
except exceptions.Cancelled:
|
||||
click.echo("Update aborted on device.")
|
||||
except exceptions.TrezorException as e:
|
||||
click.echo("Update failed: {}".format(e))
|
||||
sys.exit(3)
|
||||
upload_firmware_into_device(
|
||||
client=client,
|
||||
firmware_data=firmware_data,
|
||||
)
|
||||
|
@ -75,10 +75,12 @@ COMMAND_ALIASES = {
|
||||
"xrp": ripple.cli,
|
||||
"xlm": stellar.cli,
|
||||
"xtz": tezos.cli,
|
||||
# firmware-update aliases:
|
||||
"update-firmware": firmware.firmware_update,
|
||||
"upgrade-firmware": firmware.firmware_update,
|
||||
"firmware-upgrade": firmware.firmware_update,
|
||||
# firmware aliases:
|
||||
"fw": firmware.cli,
|
||||
"update-firmware": firmware.update,
|
||||
"upgrade-firmware": firmware.update,
|
||||
"firmware-upgrade": firmware.update,
|
||||
"firmware-update": firmware.update,
|
||||
}
|
||||
|
||||
|
||||
@ -339,7 +341,7 @@ cli.add_command(settings.cli)
|
||||
cli.add_command(stellar.cli)
|
||||
cli.add_command(tezos.cli)
|
||||
|
||||
cli.add_command(firmware.firmware_update)
|
||||
cli.add_command(firmware.cli)
|
||||
cli.add_command(debug.cli)
|
||||
|
||||
#
|
||||
|
Loading…
Reference in New Issue
Block a user