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:
parent
ba83a7e644
commit
523e50db49
1
python/.changelog.d/2919.added.1
Normal file
1
python/.changelog.d/2919.added.1
Normal file
@ -0,0 +1 @@
|
|||||||
|
Support interaction-less upgrade
|
1
python/.changelog.d/2919.added.2
Normal file
1
python/.changelog.d/2919.added.2
Normal file
@ -0,0 +1 @@
|
|||||||
|
trezorctl: Automatically go to bootloader when upgrading firmware
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user