mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-14 01:10:58 +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>
|
alepop <https://github.com/alepop>
|
||||||
Jan 'matejcik' Matějek <jan.matejek@satoshilabs.com>
|
Jan 'matejcik' Matějek <jan.matejek@satoshilabs.com>
|
||||||
Jan Pochyla <jan.pochyla@satoshilabs.com>
|
Jan Pochyla <jan.pochyla@satoshilabs.com>
|
||||||
|
Jiří Musil <https://github.com/grdddj>
|
||||||
Jochen Hoenicke <hoenicke@gmail.com>
|
Jochen Hoenicke <hoenicke@gmail.com>
|
||||||
Jonathan Cross <https://github.com/jonathancross>
|
Jonathan Cross <https://github.com/jonathancross>
|
||||||
Karel Bílek <karel.bilek@satoshilabs.com>
|
Karel Bílek <karel.bilek@satoshilabs.com>
|
||||||
|
@ -46,7 +46,7 @@ on one page here.
|
|||||||
eos EOS commands.
|
eos EOS commands.
|
||||||
ethereum Ethereum commands.
|
ethereum Ethereum commands.
|
||||||
fido FIDO2, U2F and WebAuthN management 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-features Retrieve device features and settings.
|
||||||
get-session Get a session ID for subsequent commands.
|
get-session Get a session ID for subsequent commands.
|
||||||
lisk Lisk commands.
|
lisk Lisk commands.
|
||||||
@ -101,6 +101,7 @@ Bitcoin and Bitcoin-like coins commands.
|
|||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
get-address Get address for specified path.
|
get-address Get address for specified path.
|
||||||
|
get-descriptor Get descriptor of given account.
|
||||||
get-public-node Get public node of given path.
|
get-public-node Get public node of given path.
|
||||||
sign-message Sign message using address of given path.
|
sign-message Sign message using address of given path.
|
||||||
sign-tx Sign transaction.
|
sign-tx Sign transaction.
|
||||||
@ -186,7 +187,6 @@ Miscellaneous debug features.
|
|||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
send-bytes Send raw bytes to Trezor.
|
send-bytes Send raw bytes to Trezor.
|
||||||
show-text Show text on Trezor display.
|
|
||||||
|
|
||||||
Device management commands - setup, recover seed, wipe, etc.
|
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.
|
counter Get or set the FIDO/U2F counter value.
|
||||||
credentials Manage FIDO2 resident credentials.
|
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.
|
Lisk commands.
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@ -376,15 +397,16 @@ Device settings.
|
|||||||
--help Show this message and exit.
|
--help Show this message and exit.
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
auto-lock-delay Set auto-lock delay (in seconds).
|
auto-lock-delay Set auto-lock delay (in seconds).
|
||||||
display-rotation Set display rotation.
|
display-rotation Set display rotation.
|
||||||
flags Set device flags.
|
experimental-features Enable or disable experimental message types.
|
||||||
homescreen Set new homescreen.
|
flags Set device flags.
|
||||||
label Set new device label.
|
homescreen Set new homescreen.
|
||||||
passphrase Enable, disable or configure passphrase protection.
|
label Set new device label.
|
||||||
pin Set, change or remove PIN.
|
passphrase Enable, disable or configure passphrase protection.
|
||||||
safety-checks Set safety check level.
|
pin Set, change or remove PIN.
|
||||||
wipe-code Set or remove the wipe code.
|
safety-checks Set safety check level.
|
||||||
|
wipe-code Set or remove the wipe code.
|
||||||
|
|
||||||
Stellar commands.
|
Stellar commands.
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
@ -14,13 +14,17 @@
|
|||||||
# You should have received a copy of the License along with this library.
|
# 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>.
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from typing import BinaryIO
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .. import exceptions, firmware
|
from .. import exceptions, firmware
|
||||||
from . import with_client
|
from ..client import TrezorClient
|
||||||
|
from . import TrezorConnection, with_client
|
||||||
|
|
||||||
ALLOWED_FIRMWARE_FORMATS = {
|
ALLOWED_FIRMWARE_FORMATS = {
|
||||||
1: (firmware.FirmwareFormat.TREZOR_ONE, firmware.FirmwareFormat.TREZOR_ONE_V2),
|
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)
|
vstr = "Firmware version {major}.{minor}.{patch} build {build}".format(**version)
|
||||||
click.echo(vstr)
|
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 version == firmware.FirmwareFormat.TREZOR_ONE:
|
||||||
if fw.embedded_onev2:
|
if fw.embedded_onev2:
|
||||||
click.echo("Trezor One firmware with embedded v2 image (1.8.0 or later)")
|
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.")
|
click.echo("Trezor T firmware image.")
|
||||||
vendor = fw.vendor_header.text
|
vendor = fw.vendor_header.text
|
||||||
vendor_version = "{major}.{minor}".format(**fw.vendor_header.version)
|
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)
|
_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:
|
try:
|
||||||
firmware.validate(version, fw, allow_unsigned=False)
|
firmware.validate(version, fw, allow_unsigned=False)
|
||||||
click.echo("Signatures are valid.")
|
click.echo("Signatures are valid.")
|
||||||
@ -68,24 +105,63 @@ def validate_firmware(version, fw, expected_fingerprint=None):
|
|||||||
click.echo("Firmware validation failed, aborting.")
|
click.echo("Firmware validation failed, aborting.")
|
||||||
sys.exit(4)
|
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()
|
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:
|
if version == firmware.FirmwareFormat.TREZOR_ONE and fw.embedded_onev2:
|
||||||
fingerprint_onev2 = firmware.digest(
|
fingerprint_onev2 = firmware.digest(
|
||||||
firmware.FirmwareFormat.TREZOR_ONE_V2, fw.embedded_onev2
|
firmware.FirmwareFormat.TREZOR_ONE_V2, fw.embedded_onev2
|
||||||
).hex()
|
).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:
|
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.")
|
click.echo("Fingerprints do not match, aborting.")
|
||||||
sys.exit(5)
|
sys.exit(5)
|
||||||
|
|
||||||
|
|
||||||
def find_best_firmware_version(
|
def check_device_match(
|
||||||
bootloader_version, requested_version=None, beta=False, bitcoin_only=False
|
version: str,
|
||||||
):
|
fw: firmware.ParsedFirmware,
|
||||||
url = "https://data.trezor.io/firmware/{}/releases.json"
|
bootloader_onev2: bool,
|
||||||
releases = requests.get(url.format(bootloader_version[0])).json()
|
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:
|
if not releases:
|
||||||
raise click.ClickException("Failed to get list of 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)
|
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):
|
def version_str(version):
|
||||||
return ".".join(map(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"]
|
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
|
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
|
confirm_different_version = False
|
||||||
while True:
|
while True:
|
||||||
|
# The want_version can be changed below, need to redefine it
|
||||||
want_version_str = version_str(want_version)
|
want_version_str = version_str(want_version)
|
||||||
try:
|
try:
|
||||||
release = next(r for r in releases if r["version"] == want_version)
|
release = next(r for r in releases if r["version"] == want_version)
|
||||||
except StopIteration:
|
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
|
# look for versions starting with the lowest
|
||||||
for release in reversed(releases):
|
for release in reversed(releases):
|
||||||
@ -129,31 +238,35 @@ def find_best_firmware_version(
|
|||||||
# stop at first that is higher than the requested
|
# stop at first that is higher than the requested
|
||||||
break
|
break
|
||||||
# if there was no break, the newest is used
|
# if there was no break, the newest is used
|
||||||
click.echo(
|
click.echo(f"Closest available version: {version_str(closest_version)}")
|
||||||
"Closest available version: {}".format(version_str(closest_version))
|
|
||||||
)
|
|
||||||
if not beta and want_version > highest_version:
|
if not beta and want_version > highest_version:
|
||||||
click.echo("Hint: specify --beta to look for a beta release.")
|
click.echo("Hint: specify --beta to look for a beta release.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if (
|
# It can be impossible to update from a very old version directly
|
||||||
"min_bootloader_version" in release
|
# to the newer one, in that case update to the minimal
|
||||||
and release["min_bootloader_version"] > bootloader_version
|
# compatible version first
|
||||||
):
|
# Choosing the version key to compare based on (not) being in BL mode
|
||||||
need_version_str = version_str(release["min_firmware_version"])
|
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(
|
click.echo(
|
||||||
"Version {} is required before upgrading to {}.".format(
|
f"Version {need_version_str} is required before upgrading to {want_version_str}."
|
||||||
need_version_str, want_version_str
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
want_version = release["min_firmware_version"]
|
want_version = need_version
|
||||||
confirm_different_version = True
|
confirm_different_version = True
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
if confirm_different_version:
|
if confirm_different_version:
|
||||||
installing_different = "Installing version {} instead.".format(want_version_str)
|
installing_different = f"Installing version {want_version_str} instead."
|
||||||
if requested_version is None:
|
if version is None:
|
||||||
click.echo(installing_different)
|
click.echo(installing_different)
|
||||||
else:
|
else:
|
||||||
ok = click.confirm(installing_different + " Continue?", default=True)
|
ok = click.confirm(installing_different + " Continue?", default=True)
|
||||||
@ -166,39 +279,212 @@ def find_best_firmware_version(
|
|||||||
else:
|
else:
|
||||||
url = release["url"]
|
url = release["url"]
|
||||||
fingerprint = release["fingerprint"]
|
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)
|
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
|
# fmt: off
|
||||||
@click.option("-f", "--filename", type=click.File("rb"))
|
@click.argument("filename", type=click.File("rb"))
|
||||||
@click.option("-u", "--url")
|
@click.option("-c", "--check-device", is_flag=True, help="Validate device compatibility")
|
||||||
@click.option("-v", "--version")
|
@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("-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("-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("--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("--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")
|
@click.option("--fingerprint", help="Expected firmware fingerprint in hex")
|
||||||
# fmt: on
|
# fmt: on
|
||||||
@with_client
|
@with_client
|
||||||
def firmware_update(
|
def update(
|
||||||
client,
|
client: TrezorClient,
|
||||||
filename,
|
filename: BinaryIO,
|
||||||
url,
|
url: str,
|
||||||
version,
|
version: str,
|
||||||
skip_check,
|
skip_check: bool,
|
||||||
fingerprint,
|
fingerprint: str,
|
||||||
raw,
|
raw: bool,
|
||||||
dry_run,
|
dry_run: bool,
|
||||||
beta,
|
beta: bool,
|
||||||
bitcoin_only,
|
bitcoin_only: bool,
|
||||||
):
|
) -> None:
|
||||||
"""Upload new firmware to device.
|
"""Upload new firmware to device.
|
||||||
|
|
||||||
Device must be in bootloader mode.
|
Device must be in bootloader mode.
|
||||||
@ -220,86 +506,36 @@ def firmware_update(
|
|||||||
click.echo("Please switch your device to bootloader mode.")
|
click.echo("Please switch your device to bootloader mode.")
|
||||||
sys.exit(1)
|
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:
|
if filename:
|
||||||
data = filename.read()
|
firmware_data = filename.read()
|
||||||
else:
|
else:
|
||||||
if not url:
|
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(
|
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:
|
if not fingerprint:
|
||||||
fingerprint = fp
|
fingerprint = fp
|
||||||
|
|
||||||
try:
|
firmware_data = download_firmware_data(url)
|
||||||
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
|
|
||||||
|
|
||||||
if not raw and not skip_check:
|
if not raw and not skip_check:
|
||||||
try:
|
validate_firmware(
|
||||||
version, fw = firmware.parse(data)
|
firmware_data=firmware_data,
|
||||||
except Exception as e:
|
fingerprint=fingerprint,
|
||||||
click.echo(e)
|
bootloader_onev2=_is_bootloader_onev2(client),
|
||||||
sys.exit(2)
|
trezor_major_version=client.features.major_version,
|
||||||
|
)
|
||||||
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)
|
|
||||||
|
|
||||||
if not raw:
|
if not raw:
|
||||||
# special handling for embedded-OneV2 format:
|
firmware_data = extract_embedded_fw(
|
||||||
# for bootloader < 1.8, keep the embedding
|
firmware_data=firmware_data,
|
||||||
# for bootloader 1.8.0 and up, strip the old OneV1 header
|
bootloader_onev2=_is_bootloader_onev2(client),
|
||||||
if bootloader_onev2 and data[:4] == b"TRZR" and data[256 : 256 + 4] == b"TRZF":
|
)
|
||||||
click.echo("Extracting embedded firmware image.")
|
|
||||||
data = data[256:]
|
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
click.echo("Dry run. Not uploading firmware to device.")
|
click.echo("Dry run. Not uploading firmware to device.")
|
||||||
else:
|
else:
|
||||||
try:
|
upload_firmware_into_device(
|
||||||
if f.major_version == 1 and f.firmware_present is not False:
|
client=client,
|
||||||
# Trezor One does not send ButtonRequest
|
firmware_data=firmware_data,
|
||||||
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)
|
|
||||||
|
@ -75,10 +75,12 @@ COMMAND_ALIASES = {
|
|||||||
"xrp": ripple.cli,
|
"xrp": ripple.cli,
|
||||||
"xlm": stellar.cli,
|
"xlm": stellar.cli,
|
||||||
"xtz": tezos.cli,
|
"xtz": tezos.cli,
|
||||||
# firmware-update aliases:
|
# firmware aliases:
|
||||||
"update-firmware": firmware.firmware_update,
|
"fw": firmware.cli,
|
||||||
"upgrade-firmware": firmware.firmware_update,
|
"update-firmware": firmware.update,
|
||||||
"firmware-upgrade": 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(stellar.cli)
|
||||||
cli.add_command(tezos.cli)
|
cli.add_command(tezos.cli)
|
||||||
|
|
||||||
cli.add_command(firmware.firmware_update)
|
cli.add_command(firmware.cli)
|
||||||
cli.add_command(debug.cli)
|
cli.add_command(debug.cli)
|
||||||
|
|
||||||
#
|
#
|
||||||
|
Loading…
Reference in New Issue
Block a user