diff --git a/python/.changelog.d/2919.added.1 b/python/.changelog.d/2919.added.1 new file mode 100644 index 0000000000..a3bd005bab --- /dev/null +++ b/python/.changelog.d/2919.added.1 @@ -0,0 +1 @@ +Support interaction-less upgrade diff --git a/python/.changelog.d/2919.added.2 b/python/.changelog.d/2919.added.2 new file mode 100644 index 0000000000..3aa8b94247 --- /dev/null +++ b/python/.changelog.d/2919.added.2 @@ -0,0 +1 @@ +trezorctl: Automatically go to bootloader when upgrading firmware \ No newline at end of file diff --git a/python/src/trezorlib/cli/firmware.py b/python/src/trezorlib/cli/firmware.py index fcafe2b3bb..9e2b15a24d 100644 --- a/python/src/trezorlib/cli/firmware.py +++ b/python/src/trezorlib/cli/firmware.py @@ -16,6 +16,7 @@ import os import sys +import time from typing import ( TYPE_CHECKING, Any, @@ -32,7 +33,7 @@ from urllib.parse import urlparse import click import requests -from .. import exceptions, firmware, messages, models +from .. import device, exceptions, firmware, messages, models from ..firmware import models as fw_models from ..models import TrezorModel from . import ChoiceType, with_client @@ -460,6 +461,43 @@ def upload_firmware_into_device( sys.exit(3) +def _is_strict_update(client: "TrezorClient", firmware_data: bytes) -> bool: + """Check if the firmware is from the same vendor and the + firmware is newer than the currently installed firmware. + """ + try: + fw = firmware.parse(firmware_data) + except Exception as e: + click.echo(e) + sys.exit(2) + + if not isinstance(fw, firmware.VendorFirmware): + return False + + f = client.features + cur_version = (f.major_version, f.minor_version, f.patch_version, 0) + + return ( + fw.vendor_header.text == f.fw_vendor + and fw.firmware.header.version > cur_version + and fw.vendor_header.trust.is_full_trust() + ) + + +def _get_firmware_header_size(firmware_data: bytes) -> int: + """Returns size of vendor and image headers""" + try: + fw = firmware.parse(firmware_data) + except Exception as e: + click.echo(e) + sys.exit(2) + + if isinstance(fw, firmware.VendorFirmware): + return fw.firmware.header.header_len + fw.vendor_header.header_len + + return 0 + + @click.group(name="firmware") def cli() -> None: """Firmware commands.""" @@ -581,9 +619,9 @@ def download( @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 +@click.pass_obj def update( - client: "TrezorClient", + obj: "TrezorConnection", filename: Optional[BinaryIO], url: Optional[str], version: Optional[str], @@ -596,8 +634,6 @@ def update( ) -> None: """Upload new firmware to device. - Device must be in bootloader mode. - You can specify a filename or URL from which the firmware can be downloaded. You can also explicitly specify a firmware version that you want. Otherwise, trezorctl will attempt to find latest available version @@ -607,43 +643,66 @@ def update( against downloaded firmware fingerprint. Otherwise fingerprint is checked against data.trezor.io information, if available. """ - if sum(bool(x) for x in (filename, url, version)) > 1: - click.echo("You can use only one of: filename, url, version.") - sys.exit(1) + with obj.client_context() as client: + if sum(bool(x) for x in (filename, url, version)) > 1: + click.echo("You can use only one of: filename, url, version.") + sys.exit(1) - if not dry_run and not client.features.bootloader_mode: - click.echo("Please switch your device to bootloader mode.") - sys.exit(1) + if filename: + firmware_data = filename.read() + else: + if not url: + url, fp = find_best_firmware_version( + client=client, version=version, beta=beta, bitcoin_only=bitcoin_only + ) + if not fingerprint: + fingerprint = fp - if filename: - firmware_data = filename.read() - else: - if not url: - url, fp = find_best_firmware_version( - client=client, version=version, beta=beta, bitcoin_only=bitcoin_only + firmware_data = download_firmware_data(url) + + if not raw and not skip_check: + validate_firmware( + firmware_data=firmware_data, + fingerprint=fingerprint, + bootloader_onev2=_is_bootloader_onev2(client), + model=client.model, ) - if not fingerprint: - fingerprint = fp - firmware_data = download_firmware_data(url) + if not raw: + firmware_data = extract_embedded_fw( + firmware_data=firmware_data, + bootloader_onev2=_is_bootloader_onev2(client), + ) - if not raw and not skip_check: - validate_firmware( - firmware_data=firmware_data, - fingerprint=fingerprint, - bootloader_onev2=_is_bootloader_onev2(client), - model=client.model, - ) + if dry_run: + click.echo("Dry run. Not uploading firmware to device.") + return - if not raw: - firmware_data = extract_embedded_fw( - firmware_data=firmware_data, - bootloader_onev2=_is_bootloader_onev2(client), - ) + if not client.features.bootloader_mode: + if _is_strict_update(client, firmware_data): + header_size = _get_firmware_header_size(firmware_data) + device.reboot_to_bootloader( + client, + boot_command=messages.BootCommand.INSTALL_UPGRADE, + firmware_header=firmware_data[:header_size], + ) + else: + device.reboot_to_bootloader(client) + + click.echo("Waiting for bootloader...") + while True: + time.sleep(0.5) + try: + obj.get_transport() + break + except Exception: + pass + + with obj.client_context() as client: + if not client.features.bootloader_mode: + click.echo("Please switch your device to bootloader mode.") + sys.exit(1) - if dry_run: - click.echo("Dry run. Not uploading firmware to device.") - else: upload_firmware_into_device(client=client, firmware_data=firmware_data) diff --git a/python/src/trezorlib/firmware/vendor.py b/python/src/trezorlib/firmware/vendor.py index d4314d6d75..a83128f123 100644 --- a/python/src/trezorlib/firmware/vendor.py +++ b/python/src/trezorlib/firmware/vendor.py @@ -70,6 +70,14 @@ class VendorTrust(Struct): 2, ) + def is_full_trust(self) -> bool: + return ( + not self.show_vendor_string + and not self.require_user_click + and not self.red_background + and self.delay == 0 + ) + class VendorHeader(Struct): header_len: int diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 4076577562..9727bc4917 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -485,6 +485,11 @@ class WordRequestType(IntEnum): Matrix6 = 2 +class BootCommand(IntEnum): + STOP_AND_WAIT = 0 + INSTALL_UPGRADE = 1 + + class DebugSwipeDirection(IntEnum): UP = 0 DOWN = 1 @@ -3746,6 +3751,19 @@ class CancelAuthorization(protobuf.MessageType): class RebootToBootloader(protobuf.MessageType): MESSAGE_WIRE_TYPE = 87 + FIELDS = { + 1: protobuf.Field("boot_command", "BootCommand", repeated=False, required=False, default=BootCommand.STOP_AND_WAIT), + 2: protobuf.Field("firmware_header", "bytes", repeated=False, required=False, default=None), + } + + def __init__( + self, + *, + boot_command: Optional["BootCommand"] = BootCommand.STOP_AND_WAIT, + firmware_header: Optional["bytes"] = None, + ) -> None: + self.boot_command = boot_command + self.firmware_header = firmware_header class GetNonce(protobuf.MessageType):