1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-13 09:58:09 +00:00
trezor-firmware/python/src/trezorlib/_internal/firmware_headers.py

519 lines
16 KiB
Python
Raw Normal View History

# This file is part of the Trezor project.
#
# Copyright (C) 2012-2022 SatoshiLabs and contributors
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# 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>.
import typing as t
from copy import copy
from dataclasses import asdict
2020-01-03 12:16:33 +00:00
from enum import Enum
import click
import construct as c
from construct_classes import Struct
from typing_extensions import Protocol, Self, runtime_checkable
2020-01-03 12:16:33 +00:00
2020-09-01 09:04:20 +00:00
from .. import cosi, firmware
from ..firmware import models as fw_models
2020-01-03 12:16:33 +00:00
SYM_OK = click.style("\u2714", fg="green")
SYM_FAIL = click.style("\u274c", fg="red")
class Status(Enum):
VALID = click.style("VALID", fg="green", bold=True)
INVALID = click.style("INVALID", fg="red", bold=True)
MISSING = click.style("MISSING", fg="blue", bold=True)
DEVEL = click.style("DEVEL", fg="red", bold=True)
feat(python): add full type information WIP - typing the trezorctl apps typing functions trezorlib/cli addressing most of mypy issue for trezorlib apps and _internal folder fixing broken device tests by changing asserts in debuglink.py addressing most of mypy issues in trezorlib/cli folder adding types to some untyped functions, mypy section in setup.cfg typing what can be typed, some mypy fixes, resolving circular import issues importing type objects in "if TYPE_CHECKING:" branch fixing CI by removing assert in emulator, better ignore comments CI assert fix, style fixes, new config options fixup! CI assert fix, style fixes, new config options type fixes after rebasing on master fixing python3.6 and 3.7 unittests by importing Literal from typing_extensions couple mypy and style fixes fixes and improvements from code review silencing all but one mypy issues trial of typing the tools.expect function fixup! trial of typing the tools.expect function @expect and @session decorators correctly type-checked Optional args in CLI where relevant, not using general list/tuple/dict where possible python/Makefile commands, adding them into CI, ignoring last mypy issue documenting overload for expect decorator, two mypy fixes coming from that black style fix improved typing of decorators, pyright config file addressing or ignoring pyright errors, replacing mypy in CI by pyright fixing incomplete assert causing device tests to fail pyright issue that showed in CI but not locally, printing pyright version in CI fixup! pyright issue that showed in CI but not locally, printing pyright version in CI unifying type:ignore statements for pyright usage resolving PIL.Image issues, pyrightconfig not excluding anything replacing couple asserts with TypeGuard on safe_issubclass better error handling of usb1 import for webusb better error handling of hid import small typing details found out by strict pyright mode improvements from code review chore(python): changing List to Sequence for protobuf messages small code changes to reflect the protobuf change to Sequence importing TypedDict from typing_extensions to support 3.6 and 3.7 simplify _format_access_list function fixup! simplify _format_access_list function typing tools folder typing helper-scripts folder some click typing enforcing all functions to have typed arguments reverting the changed argument name in tools replacing TransportType with Transport making PinMatrixRequest.type protobuf attribute required reverting the protobuf change, making argument into get_pin Optional small fixes in asserts solving the session decorator type issues fixup! solving the session decorator type issues improvements from code review fixing new pyright errors introduced after version increase changing -> Iterable to -> Sequence in enumerate_devices, change in wait_for_devices style change in debuglink.py chore(python): adding type annotation to Sequences in messages.py better "self and cls" types on Transport fixup! better "self and cls" types on Transport fixing some easy things from strict pyright run
2021-11-03 22:12:53 +00:00
def is_ok(self) -> bool:
2020-01-03 12:16:33 +00:00
return self is Status.VALID or self is Status.DEVEL
VHASH_DEVEL = bytes.fromhex(
"c5b4d40cb76911392122c8d1c277937e49c69b2aaf818001ec5c7663fcce258f"
)
def _make_dev_keys(*key_bytes: bytes) -> t.Sequence[bytes]:
2020-01-03 12:16:33 +00:00
return [k * 32 for k in key_bytes]
def all_zero(data: bytes) -> bool:
return all(b == 0 for b in data)
def _check_signature_any(fw: "SignableImageProto", is_devel: bool = False) -> Status:
if not fw.signature_present():
2020-01-03 12:16:33 +00:00
return Status.MISSING
try:
fw.verify()
2020-01-03 12:16:33 +00:00
return Status.VALID if not is_devel else Status.DEVEL
except Exception:
pass
try:
fw.verify(dev_keys=True)
return Status.DEVEL
2020-01-03 12:16:33 +00:00
except Exception:
return Status.INVALID
# ====================== formatting functions ====================
class LiteralStr(str):
pass
def _format_container(
pb: t.Union[c.Container, Struct, dict],
2020-01-03 12:16:33 +00:00
indent: int = 0,
sep: str = " " * 4,
truncate_after: t.Optional[int] = 64,
truncate_to: t.Optional[int] = 32,
2020-01-03 12:16:33 +00:00
) -> str:
def mostly_printable(bytes: bytes) -> bool:
if not bytes:
return True
printable = sum(1 for byte in bytes if 0x20 <= byte <= 0x7E)
return printable / len(bytes) > 0.8
def pformat(value: t.Any, indent: int) -> str:
2020-01-03 12:16:33 +00:00
level = sep * indent
leadin = sep * (indent + 1)
if isinstance(value, LiteralStr):
return value
if isinstance(value, list):
# short list of simple values
feat(python): add full type information WIP - typing the trezorctl apps typing functions trezorlib/cli addressing most of mypy issue for trezorlib apps and _internal folder fixing broken device tests by changing asserts in debuglink.py addressing most of mypy issues in trezorlib/cli folder adding types to some untyped functions, mypy section in setup.cfg typing what can be typed, some mypy fixes, resolving circular import issues importing type objects in "if TYPE_CHECKING:" branch fixing CI by removing assert in emulator, better ignore comments CI assert fix, style fixes, new config options fixup! CI assert fix, style fixes, new config options type fixes after rebasing on master fixing python3.6 and 3.7 unittests by importing Literal from typing_extensions couple mypy and style fixes fixes and improvements from code review silencing all but one mypy issues trial of typing the tools.expect function fixup! trial of typing the tools.expect function @expect and @session decorators correctly type-checked Optional args in CLI where relevant, not using general list/tuple/dict where possible python/Makefile commands, adding them into CI, ignoring last mypy issue documenting overload for expect decorator, two mypy fixes coming from that black style fix improved typing of decorators, pyright config file addressing or ignoring pyright errors, replacing mypy in CI by pyright fixing incomplete assert causing device tests to fail pyright issue that showed in CI but not locally, printing pyright version in CI fixup! pyright issue that showed in CI but not locally, printing pyright version in CI unifying type:ignore statements for pyright usage resolving PIL.Image issues, pyrightconfig not excluding anything replacing couple asserts with TypeGuard on safe_issubclass better error handling of usb1 import for webusb better error handling of hid import small typing details found out by strict pyright mode improvements from code review chore(python): changing List to Sequence for protobuf messages small code changes to reflect the protobuf change to Sequence importing TypedDict from typing_extensions to support 3.6 and 3.7 simplify _format_access_list function fixup! simplify _format_access_list function typing tools folder typing helper-scripts folder some click typing enforcing all functions to have typed arguments reverting the changed argument name in tools replacing TransportType with Transport making PinMatrixRequest.type protobuf attribute required reverting the protobuf change, making argument into get_pin Optional small fixes in asserts solving the session decorator type issues fixup! solving the session decorator type issues improvements from code review fixing new pyright errors introduced after version increase changing -> Iterable to -> Sequence in enumerate_devices, change in wait_for_devices style change in debuglink.py chore(python): adding type annotation to Sequences in messages.py better "self and cls" types on Transport fixup! better "self and cls" types on Transport fixing some easy things from strict pyright run
2021-11-03 22:12:53 +00:00
if not value or isinstance(value[0], (int, bool, Enum)):
2020-01-03 12:16:33 +00:00
return repr(value)
# long list, one line per entry
lines = ["[", level + "]"]
lines[1:1] = [leadin + pformat(x, indent + 1) for x in value]
return "\n".join(lines)
if isinstance(value, Struct):
value = asdict(value)
2020-01-03 12:16:33 +00:00
if isinstance(value, dict):
lines = ["{"]
for key, val in value.items():
if key.startswith("_"):
continue
if val is None or val == []:
continue
lines.append(leadin + key + ": " + pformat(val, indent + 1))
lines.append(level + "}")
return "\n".join(lines)
if isinstance(value, (bytes, bytearray)):
length = len(value)
suffix = ""
if truncate_after and length > truncate_after:
suffix = "..."
value = value[: truncate_to or 0]
if mostly_printable(value):
output = repr(value)
else:
output = value.hex()
return f"{length} bytes {output}{suffix}"
2020-01-03 12:16:33 +00:00
if isinstance(value, Enum):
return str(value)
return repr(value)
return pformat(pb, indent)
def _format_version(version: t.Tuple[int, ...]) -> str:
return ".".join(str(i) for i in version)
def format_header(
header: firmware.core.FirmwareHeader,
code_hashes: t.Sequence[bytes],
digest: bytes,
sig_status: Status,
) -> str:
header_dict = asdict(header)
header_out = header_dict.copy()
for key, val in header_out.items():
if "version" in key:
header_out[key] = LiteralStr(_format_version(val))
hashes_out = []
for expected, actual in zip(header.hashes, code_hashes):
status = SYM_OK if expected == actual else SYM_FAIL
hashes_out.append(LiteralStr(f"{status} {expected.hex()}"))
if all(all_zero(h) for h in header.hashes):
hash_status = Status.MISSING
elif header.hashes != code_hashes:
hash_status = Status.INVALID
else:
hash_status = Status.VALID
header_out["hashes"] = hashes_out
all_ok = SYM_OK if hash_status.is_ok() and sig_status.is_ok() else SYM_FAIL
output = [
"Firmware Header " + _format_container(header_out),
f"Fingerprint: {click.style(digest.hex(), bold=True)}",
f"{all_ok} Signature is {sig_status.value}, hashes are {hash_status.value}",
]
return "\n".join(output)
2020-01-03 12:16:33 +00:00
# =========================== functionality implementations ===============
class SignableImageProto(Protocol):
NAME: t.ClassVar[str]
2020-01-03 12:16:33 +00:00
@classmethod
def parse(cls, data: bytes) -> Self:
"""Parse binary data into an image of this type."""
...
2020-01-03 12:16:33 +00:00
def digest(self) -> bytes:
"""Calculate digest that will be signed / verified."""
...
2020-01-03 12:16:33 +00:00
def verify(self, dev_keys: bool = False) -> None:
"""Verify signature of the image.
If dev_keys is True, verify using development keys. If selected, a production
image will fail verification.
"""
...
2020-01-03 12:16:33 +00:00
def build(self) -> bytes:
"""Reconstruct binary representation of the image."""
...
def format(self, verbose: bool = False) -> str:
"""Generate printable information about the image."""
...
def signature_present(self) -> bool:
"""Check if the image has a signature."""
...
def public_keys(self, dev_keys: bool = False) -> t.Sequence[bytes]:
"""Return public keys that should be used to sign the image.
This does _not_ return the keys with which the image is actually signed.
In particular, `image.public_keys()` will return the production
keys even if the image is signed with development keys.
If dev_keys is True, return development keys.
"""
...
@runtime_checkable
class CosiSignedImage(SignableImageProto, Protocol):
DEV_KEYS: t.ClassVar[t.Sequence[bytes]] = []
2020-01-03 12:16:33 +00:00
def insert_signature(self, signature: bytes, sigmask: int) -> None:
...
@runtime_checkable
class LegacySignedImage(SignableImageProto, Protocol):
def slots(self) -> t.Iterable[int]:
...
2020-01-03 12:16:33 +00:00
def insert_signature(self, slot: int, key_index: int, signature: bytes) -> None:
...
2020-01-03 12:16:33 +00:00
def public_keys(
self, dev_keys: bool = False, signature_version: int = 3
) -> t.Sequence[bytes]:
"""Return public keys that should be used to sign the image.
This does _not_ return the keys with which the image is actually signed.
In particular, `image.public_keys()` will return the production
keys even if the image is signed with development keys.
If dev_keys is True, return development keys.
Specifying signature_version allows to return keys for a different signature
scheme version. The default is the newest version 3.
"""
...
2020-01-03 12:16:33 +00:00
class CosiSignatureHeaderProto(Protocol):
signature: bytes
sigmask: int
2020-01-03 12:16:33 +00:00
class CosiSignedMixin:
def signature_present(self) -> bool:
header = self.get_header()
return not all_zero(header.signature) or header.sigmask != 0
def insert_signature(self, signature: bytes, sigmask: int) -> None:
self.get_header().signature = signature
self.get_header().sigmask = sigmask
def get_header(self) -> CosiSignatureHeaderProto:
raise NotImplementedError
class VendorHeader(firmware.VendorHeader, CosiSignedMixin):
NAME: t.ClassVar[str] = "vendorheader"
2020-01-03 12:16:33 +00:00
DEV_KEYS = _make_dev_keys(b"\x44", b"\x45")
SUBCON = c.Struct(*firmware.VendorHeader.SUBCON.subcons, c.Terminated)
2020-01-03 12:16:33 +00:00
def get_header(self) -> CosiSignatureHeaderProto:
return self
2020-01-03 12:16:33 +00:00
def _format(self, terse: bool) -> str:
if not terse:
output = [
"Vendor Header " + _format_container(self),
f"Pubkey bundle hash: {self.vhash().hex()}",
2020-01-03 12:16:33 +00:00
]
else:
output = [
"Vendor Header for {vendor} version {version} ({size} bytes)".format(
vendor=click.style(self.text, bold=True),
version=_format_version(self.version),
size=self.header_len,
2020-01-03 12:16:33 +00:00
),
]
if not terse:
output.append(f"Fingerprint: {click.style(self.digest().hex(), bold=True)}")
2020-01-03 12:16:33 +00:00
sig_status = _check_signature_any(self)
2020-01-03 12:16:33 +00:00
sym = SYM_OK if sig_status.is_ok() else SYM_FAIL
output.append(f"{sym} Signature is {sig_status.value}")
2020-01-03 12:16:33 +00:00
return "\n".join(output)
def format(self, verbose: bool = False) -> str:
return self._format(terse=False)
def public_keys(self, dev_keys: bool = False) -> t.Sequence[bytes]:
if not dev_keys:
return fw_models.TREZOR_T.bootloader_keys
else:
return fw_models.TREZOR_T_DEV.bootloader_keys
class VendorFirmware(firmware.VendorFirmware, CosiSignedMixin):
NAME: t.ClassVar[str] = "firmware"
DEV_KEYS = _make_dev_keys(b"\x47", b"\x48")
def get_header(self) -> CosiSignatureHeaderProto:
return self.firmware.header
2020-01-03 12:16:33 +00:00
def format(self, verbose: bool = False) -> str:
vh = copy(self.vendor_header)
vh.__class__ = VendorHeader
assert isinstance(vh, VendorHeader)
is_devel = self.vendor_header.vhash() == VHASH_DEVEL
return (
vh._format(terse=not verbose)
+ "\n"
+ format_header(
self.firmware.header,
self.firmware.code_hashes(),
self.digest(),
_check_signature_any(self, is_devel),
)
2020-01-03 12:16:33 +00:00
)
def public_keys(self, dev_keys: bool = False) -> t.Sequence[bytes]:
"""Return public keys that should be used to sign the image.
In vendor firmware, the public keys are stored in the vendor header.
There is no choice of development keys. If that is required, you need to create
an image with a development vendor header.
"""
return self.vendor_header.pubkeys
2020-01-03 12:16:33 +00:00
class BootloaderImage(firmware.FirmwareImage, CosiSignedMixin):
NAME: t.ClassVar[str] = "bootloader"
DEV_KEYS = _make_dev_keys(b"\x41", b"\x42")
2020-01-03 12:16:33 +00:00
def get_model(self) -> fw_models.Model:
if isinstance(self.header.hw_model, fw_models.Model):
return self.header.hw_model
return fw_models.Model.T
def get_model_keys(self, dev_keys: bool) -> fw_models.ModelKeys:
model = self.get_model()
if dev_keys:
return fw_models.MODEL_MAP_DEV[model]
else:
return fw_models.MODEL_MAP[model]
def get_header(self) -> CosiSignatureHeaderProto:
return self.header
2020-01-03 12:16:33 +00:00
def format(self, verbose: bool = False) -> str:
return format_header(
self.header,
self.code_hashes(),
self.digest(),
_check_signature_any(self),
)
2020-01-03 12:16:33 +00:00
def verify(self, dev_keys: bool = False) -> None:
self.validate_code_hashes()
public_keys = self.public_keys(dev_keys)
try:
cosi.verify(
self.header.signature,
self.digest(),
self.get_model_keys(dev_keys).boardloader_sigs_needed,
public_keys,
self.header.sigmask,
)
except Exception:
raise firmware.InvalidSignatureError("Invalid bootloader signature")
def public_keys(self, dev_keys: bool = False) -> t.Sequence[bytes]:
return self.get_model_keys(dev_keys).boardloader_keys
class LegacyFirmware(firmware.LegacyFirmware):
NAME: t.ClassVar[str] = "legacy_firmware_v1"
def signature_present(self) -> bool:
return any(i != 0 for i in self.key_indexes) or any(
not all_zero(sig) for sig in self.signatures
)
2020-01-03 12:16:33 +00:00
def insert_signature(self, slot: int, key_index: int, signature: bytes) -> None:
if not 0 <= slot < firmware.V1_SIGNATURE_SLOTS:
raise ValueError("Invalid slot number")
if not 0 < key_index <= len(fw_models.TREZOR_ONE_V1V2.firmware_keys):
raise ValueError("Invalid key index")
self.key_indexes[slot] = key_index
self.signatures[slot] = signature
2020-01-03 12:16:33 +00:00
def format(self, verbose: bool = False) -> str:
contents = asdict(self).copy()
del contents["embedded_v2"]
if self.embedded_v2:
em = copy(self.embedded_v2)
em.__class__ = LegacyV2Firmware
assert isinstance(em, LegacyV2Firmware)
embedded_content = "\nEmbedded V2 header: " + em.format(verbose=verbose)
2020-01-03 12:16:33 +00:00
else:
embedded_content = ""
2020-01-03 12:16:33 +00:00
return _format_container(contents) + embedded_content
2020-01-03 12:16:33 +00:00
def public_keys(
self, dev_keys: bool = False, signature_version: int = 2
) -> t.Sequence[bytes]:
if dev_keys:
return fw_models.TREZOR_ONE_V1V2_DEV.firmware_keys
else:
return fw_models.TREZOR_ONE_V1V2.firmware_keys
2020-01-03 12:16:33 +00:00
def slots(self) -> t.Iterable[int]:
return self.key_indexes
2020-01-03 12:16:33 +00:00
class LegacyV2Firmware(firmware.LegacyV2Firmware):
NAME: t.ClassVar[str] = "legacy_firmware_v2"
2020-01-03 12:16:33 +00:00
def signature_present(self) -> bool:
return any(i != 0 for i in self.header.v1_key_indexes) or any(
not all_zero(sig) for sig in self.header.v1_signatures
2020-01-03 12:16:33 +00:00
)
def insert_signature(self, slot: int, key_index: int, signature: bytes) -> None:
if not 0 <= slot < firmware.V1_SIGNATURE_SLOTS:
raise ValueError("Invalid slot number")
if not 0 < key_index <= len(firmware.V1_BOOTLOADER_KEYS):
raise ValueError("Invalid key index")
if not isinstance(self.header.v1_key_indexes, list):
self.header.v1_key_indexes = list(self.header.v1_key_indexes)
if not isinstance(self.header.v1_signatures, list):
self.header.v1_signatures = list(self.header.v1_signatures)
self.header.v1_key_indexes[slot] = key_index
self.header.v1_signatures[slot] = signature
2020-01-03 12:16:33 +00:00
def format(self, verbose: bool = False) -> str:
return format_header(
self.header,
self.code_hashes(),
self.digest(),
_check_signature_any(self),
2020-01-03 12:16:33 +00:00
)
def public_keys(
self, dev_keys: bool = False, signature_version: int = 3
) -> t.Sequence[bytes]:
keymap: t.Dict[t.Tuple[int, bool], fw_models.ModelKeys] = {
(3, False): fw_models.TREZOR_ONE_V3,
(3, True): fw_models.TREZOR_ONE_V3_DEV,
(2, False): fw_models.TREZOR_ONE_V1V2,
(2, True): fw_models.TREZOR_ONE_V1V2_DEV,
}
if not (signature_version, dev_keys) in keymap:
raise ValueError("Unsupported signature version")
return keymap[signature_version, dev_keys].firmware_keys
2020-01-03 12:16:33 +00:00
def slots(self) -> t.Iterable[int]:
return self.header.v1_key_indexes
2020-01-03 12:16:33 +00:00
def parse_image(image: bytes) -> SignableImageProto:
try:
return VendorFirmware.parse(image)
except c.ConstructError:
pass
2020-01-03 12:16:33 +00:00
try:
return VendorHeader.parse(image)
except c.ConstructError:
pass
try:
firmware_img = firmware.core.FirmwareImage.parse(image)
if firmware_img.header.magic == firmware.core.HeaderType.BOOTLOADER:
return BootloaderImage.parse(image)
if firmware_img.header.magic == firmware.core.HeaderType.FIRMWARE:
return LegacyV2Firmware.parse(image)
raise ValueError("Unrecognized firmware header magic")
except c.ConstructError:
pass
try:
return LegacyFirmware.parse(image)
except c.ConstructError:
pass
2020-01-03 12:16:33 +00:00
raise ValueError("Unrecognized firmware type")