feat(python): introduce interaction-less upgrade

pull/3363/head
cepetr 7 months ago committed by matejcik
parent ba83a7e644
commit 523e50db49

@ -0,0 +1 @@
Support interaction-less upgrade

@ -0,0 +1 @@
trezorctl: Automatically go to bootloader when upgrading firmware

@ -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)
if not dry_run and not client.features.bootloader_mode:
click.echo("Please switch your device to bootloader mode.")
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 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 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
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 dry_run:
click.echo("Dry run. Not uploading firmware to device.")
return
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)
if not raw and not skip_check:
validate_firmware(
firmware_data=firmware_data,
fingerprint=fingerprint,
bootloader_onev2=_is_bootloader_onev2(client),
model=client.model,
)
click.echo("Waiting for bootloader...")
while True:
time.sleep(0.5)
try:
obj.get_transport()
break
except Exception:
pass
if not raw:
firmware_data = extract_embedded_fw(
firmware_data=firmware_data,
bootloader_onev2=_is_bootloader_onev2(client),
)
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)

@ -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

@ -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):

Loading…
Cancel
Save