You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
188 lines
5.4 KiB
188 lines
5.4 KiB
import hashlib
|
|
import typing as t
|
|
from copy import copy
|
|
from enum import Enum
|
|
|
|
import construct as c
|
|
from construct_classes import Struct, subcon
|
|
|
|
from .. import cosi
|
|
from ..tools import EnumAdapter, TupleAdapter
|
|
from . import consts, util
|
|
from .vendor import VendorHeader
|
|
|
|
__all__ = [
|
|
"HeaderType",
|
|
"FirmwareHeader",
|
|
"FirmwareImage",
|
|
"VendorFirmware",
|
|
]
|
|
|
|
|
|
class HeaderType(Enum):
|
|
FIRMWARE = b"TRZF"
|
|
BOOTLOADER = b"TRZB"
|
|
|
|
|
|
class FirmwareHeader(Struct):
|
|
magic: HeaderType
|
|
header_len: int
|
|
expiry: int
|
|
code_length: int
|
|
version: t.Tuple[int, int, int, int]
|
|
fix_version: t.Tuple[int, int, int, int]
|
|
hashes: t.Sequence[bytes]
|
|
|
|
v1_signatures: t.Sequence[bytes]
|
|
v1_key_indexes: t.Sequence[int]
|
|
|
|
sigmask: int
|
|
signature: bytes
|
|
|
|
# fmt: off
|
|
SUBCON = c.Struct(
|
|
"_start_offset" / c.Tell,
|
|
"magic" / EnumAdapter(c.Bytes(4), HeaderType),
|
|
"header_len" / c.Int32ul,
|
|
"expiry" / c.Int32ul,
|
|
"code_length" / c.Rebuild(
|
|
c.Int32ul,
|
|
lambda this:
|
|
len(this._.code) if "code" in this._
|
|
else (this.code_length or 0)
|
|
),
|
|
"version" / TupleAdapter(c.Int8ul, c.Int8ul, c.Int8ul, c.Int8ul),
|
|
"fix_version" / TupleAdapter(c.Int8ul, c.Int8ul, c.Int8ul, c.Int8ul),
|
|
"_reserved" / c.Padding(8),
|
|
"hashes" / c.Bytes(32)[16],
|
|
|
|
"v1_signatures" / c.Bytes(64)[consts.V1_SIGNATURE_SLOTS],
|
|
"v1_key_indexes" / c.Int8ul[consts.V1_SIGNATURE_SLOTS], # pylint: disable=E1136
|
|
|
|
"_reserved" / c.Padding(220),
|
|
"sigmask" / c.Byte,
|
|
"signature" / c.Bytes(64),
|
|
|
|
"_end_offset" / c.Tell,
|
|
|
|
"_rebuild_header_len" / c.If(
|
|
c.this.version[0] > 1,
|
|
c.Pointer(
|
|
c.this._start_offset + 4,
|
|
c.Rebuild(c.Int32ul, c.this._end_offset - c.this._start_offset)
|
|
),
|
|
),
|
|
)
|
|
# fmt: on
|
|
|
|
|
|
class FirmwareImage(Struct):
|
|
"""Raw firmware image.
|
|
|
|
Consists of firmware header and code block.
|
|
This is the expected format of firmware binaries for Trezor One, or bootloader images
|
|
for Trezor T."""
|
|
|
|
header: FirmwareHeader = subcon(FirmwareHeader)
|
|
_code_offset: int
|
|
code: bytes
|
|
|
|
SUBCON = c.Struct(
|
|
"header" / FirmwareHeader.SUBCON,
|
|
"_code_offset" / c.Tell,
|
|
"code" / c.Bytes(c.this.header.code_length),
|
|
c.Terminated,
|
|
)
|
|
|
|
HASH_PARAMS = util.FirmwareHashParameters(
|
|
hash_function=hashlib.blake2s,
|
|
chunk_size=consts.V2_CHUNK_SIZE,
|
|
padding_byte=None,
|
|
)
|
|
|
|
def code_hashes(self) -> t.List[bytes]:
|
|
"""Calculate hashes of chunks of `code`.
|
|
|
|
Assume that the first `code_offset` bytes of `code` are taken up by the header.
|
|
"""
|
|
hashes = []
|
|
# End offset for each chunk. Normally this would be (i+1)*chunk_size for i-th chunk,
|
|
# but the first chunk is shorter by code_offset, so all end offsets are shifted.
|
|
ends = [
|
|
(i + 1) * self.HASH_PARAMS.chunk_size - self._code_offset for i in range(16)
|
|
]
|
|
start = 0
|
|
for end in ends:
|
|
chunk = self.code[start:end]
|
|
# padding for last non-empty chunk
|
|
if (
|
|
self.HASH_PARAMS.padding_byte is not None
|
|
and start < len(self.code)
|
|
and end > len(self.code)
|
|
):
|
|
chunk += self.HASH_PARAMS.padding_byte[0:1] * (end - start - len(chunk))
|
|
|
|
if not chunk:
|
|
hashes.append(b"\0" * 32)
|
|
else:
|
|
hashes.append(self.HASH_PARAMS.hash_function(chunk).digest())
|
|
|
|
start = end
|
|
|
|
return hashes
|
|
|
|
def validate_code_hashes(self) -> None:
|
|
if self.code_hashes() != self.header.hashes:
|
|
raise util.FirmwareIntegrityError("Invalid firmware data.")
|
|
|
|
def digest(self) -> bytes:
|
|
header = copy(self.header)
|
|
header.hashes = self.code_hashes()
|
|
header.signature = b"\x00" * 64
|
|
header.sigmask = 0
|
|
header.v1_key_indexes = [0] * consts.V1_SIGNATURE_SLOTS
|
|
header.v1_signatures = [b"\x00" * 64] * consts.V1_SIGNATURE_SLOTS
|
|
return self.HASH_PARAMS.hash_function(header.build()).digest()
|
|
|
|
|
|
class VendorFirmware(Struct):
|
|
"""Firmware image prefixed by a vendor header.
|
|
|
|
This is the expected format of firmware binaries for Trezor T."""
|
|
|
|
vendor_header: VendorHeader = subcon(VendorHeader)
|
|
firmware: FirmwareImage = subcon(FirmwareImage)
|
|
|
|
SUBCON = c.Struct(
|
|
"vendor_header" / VendorHeader.SUBCON,
|
|
"firmware" / FirmwareImage.SUBCON,
|
|
c.Terminated,
|
|
)
|
|
|
|
def digest(self) -> bytes:
|
|
return self.firmware.digest()
|
|
|
|
def verify(self, _public_keys: t.Sequence[bytes] = ()) -> None:
|
|
if _public_keys:
|
|
raise ValueError("Cannot supply custom keys for vendor firmware.")
|
|
|
|
self.firmware.validate_code_hashes()
|
|
|
|
self.vendor_header.verify()
|
|
digest = self.digest()
|
|
try:
|
|
cosi.verify(
|
|
self.firmware.header.signature,
|
|
digest,
|
|
self.vendor_header.sig_m,
|
|
self.vendor_header.pubkeys,
|
|
self.firmware.header.sigmask,
|
|
)
|
|
except Exception:
|
|
raise util.InvalidSignatureError("Invalid firmware signature.")
|
|
|
|
# XXX expiry is not used now
|
|
# now = time.gmtime()
|
|
# if time.gmtime(fw.vendor_header.expiry) < now:
|
|
# raise ValueError("Vendor header expired.")
|