1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-22 07:28:10 +00:00

feat(python): introduce interaction-less upgrade

This commit is contained in:
cepetr 2023-10-20 14:58:47 +02:00 committed by matejcik
parent ba83a7e644
commit 523e50db49
5 changed files with 122 additions and 35 deletions

View File

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

View File

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

View File

@ -16,6 +16,7 @@
import os import os
import sys import sys
import time
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
@ -32,7 +33,7 @@ from urllib.parse import urlparse
import click import click
import requests import requests
from .. import exceptions, firmware, messages, models from .. import device, exceptions, firmware, messages, models
from ..firmware import models as fw_models from ..firmware import models as fw_models
from ..models import TrezorModel from ..models import TrezorModel
from . import ChoiceType, with_client from . import ChoiceType, with_client
@ -460,6 +461,43 @@ def upload_firmware_into_device(
sys.exit(3) 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") @click.group(name="firmware")
def cli() -> None: def cli() -> None:
"""Firmware commands.""" """Firmware commands."""
@ -581,9 +619,9 @@ def download(
@click.option("--raw", is_flag=True, help="Push raw firmware 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 @click.pass_obj
def update( def update(
client: "TrezorClient", obj: "TrezorConnection",
filename: Optional[BinaryIO], filename: Optional[BinaryIO],
url: Optional[str], url: Optional[str],
version: Optional[str], version: Optional[str],
@ -596,8 +634,6 @@ def update(
) -> None: ) -> None:
"""Upload new firmware to device. """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 specify a filename or URL from which the firmware can be downloaded.
You can also explicitly specify a firmware version that you want. You can also explicitly specify a firmware version that you want.
Otherwise, trezorctl will attempt to find latest available version Otherwise, trezorctl will attempt to find latest available version
@ -607,43 +643,66 @@ def update(
against downloaded firmware fingerprint. Otherwise fingerprint is checked against downloaded firmware fingerprint. Otherwise fingerprint is checked
against data.trezor.io information, if available. against data.trezor.io information, if available.
""" """
if sum(bool(x) for x in (filename, url, version)) > 1: with obj.client_context() as client:
click.echo("You can use only one of: filename, url, version.") if sum(bool(x) for x in (filename, url, version)) > 1:
sys.exit(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: if filename:
click.echo("Please switch your device to bootloader mode.") firmware_data = filename.read()
sys.exit(1) 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 = download_firmware_data(url)
firmware_data = filename.read()
else: if not raw and not skip_check:
if not url: validate_firmware(
url, fp = find_best_firmware_version( firmware_data=firmware_data,
client=client, version=version, beta=beta, bitcoin_only=bitcoin_only 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: if dry_run:
validate_firmware( click.echo("Dry run. Not uploading firmware to device.")
firmware_data=firmware_data, return
fingerprint=fingerprint,
bootloader_onev2=_is_bootloader_onev2(client),
model=client.model,
)
if not raw: if not client.features.bootloader_mode:
firmware_data = extract_embedded_fw( if _is_strict_update(client, firmware_data):
firmware_data=firmware_data, header_size = _get_firmware_header_size(firmware_data)
bootloader_onev2=_is_bootloader_onev2(client), 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) upload_firmware_into_device(client=client, firmware_data=firmware_data)

View File

@ -70,6 +70,14 @@ class VendorTrust(Struct):
2, 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): class VendorHeader(Struct):
header_len: int header_len: int

View File

@ -485,6 +485,11 @@ class WordRequestType(IntEnum):
Matrix6 = 2 Matrix6 = 2
class BootCommand(IntEnum):
STOP_AND_WAIT = 0
INSTALL_UPGRADE = 1
class DebugSwipeDirection(IntEnum): class DebugSwipeDirection(IntEnum):
UP = 0 UP = 0
DOWN = 1 DOWN = 1
@ -3746,6 +3751,19 @@ class CancelAuthorization(protobuf.MessageType):
class RebootToBootloader(protobuf.MessageType): class RebootToBootloader(protobuf.MessageType):
MESSAGE_WIRE_TYPE = 87 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): class GetNonce(protobuf.MessageType):