|
|
|
@ -14,13 +14,15 @@
|
|
|
|
|
# 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 struct
|
|
|
|
|
import typing as t
|
|
|
|
|
from copy import copy
|
|
|
|
|
from dataclasses import asdict
|
|
|
|
|
from enum import Enum
|
|
|
|
|
from hashlib import blake2s
|
|
|
|
|
from typing import Any, List, Optional
|
|
|
|
|
|
|
|
|
|
import click
|
|
|
|
|
import construct as c
|
|
|
|
|
from construct_classes import Struct
|
|
|
|
|
from typing_extensions import Protocol, Self, runtime_checkable
|
|
|
|
|
|
|
|
|
|
from .. import cosi, firmware
|
|
|
|
|
|
|
|
|
@ -43,48 +45,25 @@ VHASH_DEVEL = bytes.fromhex(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
AnyFirmware = c.Struct(
|
|
|
|
|
"vendor_header" / c.Optional(firmware.VendorHeader),
|
|
|
|
|
"image" / c.Optional(firmware.FirmwareImage),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ImageType(Enum):
|
|
|
|
|
VENDOR_HEADER = 0
|
|
|
|
|
BOOTLOADER = 1
|
|
|
|
|
FIRMWARE = 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_dev_keys(*key_bytes: bytes) -> List[bytes]:
|
|
|
|
|
def _make_dev_keys(*key_bytes: bytes) -> t.Sequence[bytes]:
|
|
|
|
|
return [k * 32 for k in key_bytes]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def compute_vhash(vendor_header: c.Container) -> bytes:
|
|
|
|
|
m = vendor_header.sig_m
|
|
|
|
|
n = vendor_header.sig_n
|
|
|
|
|
pubkeys = vendor_header.pubkeys
|
|
|
|
|
h = blake2s()
|
|
|
|
|
h.update(struct.pack("<BB", m, n))
|
|
|
|
|
for i in range(8):
|
|
|
|
|
if i < n:
|
|
|
|
|
h.update(pubkeys[i])
|
|
|
|
|
else:
|
|
|
|
|
h.update(b"\x00" * 32)
|
|
|
|
|
return h.digest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def all_zero(data: bytes) -> bool:
|
|
|
|
|
return all(b == 0 for b in data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _check_signature_any(
|
|
|
|
|
header: c.Container, m: int, pubkeys: List[bytes], is_devel: bool
|
|
|
|
|
) -> Status:
|
|
|
|
|
if all_zero(header.signature) and header.sigmask == 0:
|
|
|
|
|
def _check_signature_any(fw: "SignableImageProto", is_devel: bool) -> Status:
|
|
|
|
|
if not fw.signature_present():
|
|
|
|
|
return Status.MISSING
|
|
|
|
|
try:
|
|
|
|
|
digest = firmware.header_digest(header)
|
|
|
|
|
cosi.verify(header.signature, digest, m, pubkeys, header.sigmask)
|
|
|
|
|
fw.verify()
|
|
|
|
|
return Status.VALID if not is_devel else Status.DEVEL
|
|
|
|
|
except Exception:
|
|
|
|
|
return Status.INVALID
|
|
|
|
@ -98,11 +77,11 @@ class LiteralStr(str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _format_container(
|
|
|
|
|
pb: c.Container,
|
|
|
|
|
pb: t.Union[c.Container, Struct, dict],
|
|
|
|
|
indent: int = 0,
|
|
|
|
|
sep: str = " " * 4,
|
|
|
|
|
truncate_after: Optional[int] = 64,
|
|
|
|
|
truncate_to: Optional[int] = 32,
|
|
|
|
|
truncate_after: t.Optional[int] = 64,
|
|
|
|
|
truncate_to: t.Optional[int] = 32,
|
|
|
|
|
) -> str:
|
|
|
|
|
def mostly_printable(bytes: bytes) -> bool:
|
|
|
|
|
if not bytes:
|
|
|
|
@ -110,7 +89,7 @@ def _format_container(
|
|
|
|
|
printable = sum(1 for byte in bytes if 0x20 <= byte <= 0x7E)
|
|
|
|
|
return printable / len(bytes) > 0.8
|
|
|
|
|
|
|
|
|
|
def pformat(value: Any, indent: int) -> str:
|
|
|
|
|
def pformat(value: t.Any, indent: int) -> str:
|
|
|
|
|
level = sep * indent
|
|
|
|
|
leadin = sep * (indent + 1)
|
|
|
|
|
|
|
|
|
@ -127,6 +106,9 @@ def _format_container(
|
|
|
|
|
lines[1:1] = [leadin + pformat(x, indent + 1) for x in value]
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
if isinstance(value, Struct):
|
|
|
|
|
value = asdict(value)
|
|
|
|
|
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
|
lines = ["{"]
|
|
|
|
|
for key, val in value.items():
|
|
|
|
@ -158,88 +140,140 @@ def _format_container(
|
|
|
|
|
return pformat(pb, indent)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _format_version(version: c.Container) -> str:
|
|
|
|
|
version_str = ".".join(
|
|
|
|
|
str(version[k]) for k in ("major", "minor", "patch") if k in version
|
|
|
|
|
)
|
|
|
|
|
if "build" in version:
|
|
|
|
|
version_str += f" build {version.build}"
|
|
|
|
|
return version_str
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =========================== functionality implementations ===============
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SignableImage:
|
|
|
|
|
NAME = "Unrecognized image"
|
|
|
|
|
BIP32_INDEX: Optional[int] = None
|
|
|
|
|
DEV_KEYS: List[bytes] = []
|
|
|
|
|
DEV_KEY_SIGMASK = 0b11
|
|
|
|
|
class SignableImageProto(Protocol):
|
|
|
|
|
NAME: t.ClassVar[str]
|
|
|
|
|
|
|
|
|
|
def __init__(self, fw: c.Container) -> None:
|
|
|
|
|
self.fw = fw
|
|
|
|
|
self.header: Any
|
|
|
|
|
self.public_keys: List[bytes]
|
|
|
|
|
self.sigs_required = firmware.V2_SIGS_REQUIRED
|
|
|
|
|
@classmethod
|
|
|
|
|
def parse(cls, data: bytes) -> Self:
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
def digest(self) -> bytes:
|
|
|
|
|
return firmware.header_digest(self.header)
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
def check_signature(self) -> Status:
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
def verify(self) -> None:
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
def rehash(self) -> None:
|
|
|
|
|
pass
|
|
|
|
|
def build(self) -> bytes:
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
def format(self, verbose: bool = False) -> str:
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
def signature_present(self) -> bool:
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
def public_keys(self) -> t.Sequence[bytes]:
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@runtime_checkable
|
|
|
|
|
class CosiSignedImage(SignableImageProto, Protocol):
|
|
|
|
|
DEV_KEYS: t.ClassVar[t.Sequence[bytes]] = []
|
|
|
|
|
|
|
|
|
|
def insert_signature(self, signature: bytes, sigmask: int) -> None:
|
|
|
|
|
self.header.signature = signature
|
|
|
|
|
self.header.sigmask = sigmask
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@runtime_checkable
|
|
|
|
|
class LegacySignedImage(SignableImageProto, Protocol):
|
|
|
|
|
def slots(self) -> t.Iterable[int]:
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
def dump(self) -> bytes:
|
|
|
|
|
return AnyFirmware.build(self.fw)
|
|
|
|
|
def insert_signature(self, slot: int, key_index: int, signature: bytes) -> None:
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
def format(self, verbose: bool) -> str:
|
|
|
|
|
return _format_container(self.fw)
|
|
|
|
|
|
|
|
|
|
class CosiSignatureHeaderProto(Protocol):
|
|
|
|
|
signature: bytes
|
|
|
|
|
sigmask: int
|
|
|
|
|
|
|
|
|
|
class VendorHeader(SignableImage):
|
|
|
|
|
|
|
|
|
|
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 = "vendorheader"
|
|
|
|
|
BIP32_INDEX = 1
|
|
|
|
|
DEV_KEYS = _make_dev_keys(b"\x44", b"\x45")
|
|
|
|
|
|
|
|
|
|
def __init__(self, fw: c.Container) -> None:
|
|
|
|
|
super().__init__(fw)
|
|
|
|
|
self.header = fw.vendor_header
|
|
|
|
|
self.public_keys = firmware.V2_BOOTLOADER_KEYS
|
|
|
|
|
SUBCON = c.Struct(*firmware.VendorHeader.SUBCON.subcons, c.Terminated)
|
|
|
|
|
|
|
|
|
|
def check_signature(self) -> Status:
|
|
|
|
|
return _check_signature_any(
|
|
|
|
|
self.header, self.sigs_required, self.public_keys, False
|
|
|
|
|
)
|
|
|
|
|
def get_header(self) -> CosiSignatureHeaderProto:
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def _format(self, terse: bool) -> str:
|
|
|
|
|
vh = self.fw.vendor_header
|
|
|
|
|
if not terse:
|
|
|
|
|
vhash = compute_vhash(vh)
|
|
|
|
|
output = [
|
|
|
|
|
"Vendor Header " + _format_container(vh),
|
|
|
|
|
f"Pubkey bundle hash: {vhash.hex()}",
|
|
|
|
|
"Vendor Header " + _format_container(self),
|
|
|
|
|
f"Pubkey bundle hash: {self.vhash().hex()}",
|
|
|
|
|
]
|
|
|
|
|
else:
|
|
|
|
|
output = [
|
|
|
|
|
"Vendor Header for {vendor} version {version} ({size} bytes)".format(
|
|
|
|
|
vendor=click.style(vh.text, bold=True),
|
|
|
|
|
version=_format_version(vh.version),
|
|
|
|
|
size=vh.header_len,
|
|
|
|
|
vendor=click.style(self.text, bold=True),
|
|
|
|
|
version=_format_version(self.version),
|
|
|
|
|
size=self.header_len,
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
fingerprint = firmware.header_digest(vh)
|
|
|
|
|
|
|
|
|
|
if not terse:
|
|
|
|
|
output.append(f"Fingerprint: {click.style(fingerprint.hex(), bold=True)}")
|
|
|
|
|
output.append(f"Fingerprint: {click.style(self.digest().hex(), bold=True)}")
|
|
|
|
|
|
|
|
|
|
sig_status = self.check_signature()
|
|
|
|
|
sig_status = _check_signature_any(self, is_devel=False)
|
|
|
|
|
sym = SYM_OK if sig_status.is_ok() else SYM_FAIL
|
|
|
|
|
output.append(f"{sym} Signature is {sig_status.value}")
|
|
|
|
|
|
|
|
|
@ -248,138 +282,168 @@ class VendorHeader(SignableImage):
|
|
|
|
|
def format(self, verbose: bool = False) -> str:
|
|
|
|
|
return self._format(terse=False)
|
|
|
|
|
|
|
|
|
|
def public_keys(self) -> t.Sequence[bytes]:
|
|
|
|
|
return firmware.V2_BOOTLOADER_KEYS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VendorFirmware(firmware.VendorFirmware, CosiSignedMixin):
|
|
|
|
|
NAME = "firmware"
|
|
|
|
|
DEV_KEYS = _make_dev_keys(b"\x47", b"\x48")
|
|
|
|
|
|
|
|
|
|
def get_header(self) -> CosiSignatureHeaderProto:
|
|
|
|
|
return self.firmware.header
|
|
|
|
|
|
|
|
|
|
class BinImage(SignableImage):
|
|
|
|
|
def __init__(self, fw: c.Container) -> None:
|
|
|
|
|
super().__init__(fw)
|
|
|
|
|
self.header = self.fw.image.header
|
|
|
|
|
self.code_hashes = firmware.calculate_code_hashes(
|
|
|
|
|
self.fw.image.code, self.fw.image._code_offset
|
|
|
|
|
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),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
self.digest_header = self.header.copy()
|
|
|
|
|
self.digest_header.hashes = self.code_hashes
|
|
|
|
|
|
|
|
|
|
def insert_signature(self, signature: bytes, sigmask: int) -> None:
|
|
|
|
|
super().insert_signature(signature, sigmask)
|
|
|
|
|
self.digest_header.signature = signature
|
|
|
|
|
self.digest_header.sigmask = sigmask
|
|
|
|
|
def public_keys(self) -> t.Sequence[bytes]:
|
|
|
|
|
return self.vendor_header.pubkeys
|
|
|
|
|
|
|
|
|
|
def digest(self) -> bytes:
|
|
|
|
|
return firmware.header_digest(self.digest_header)
|
|
|
|
|
|
|
|
|
|
def rehash(self) -> None:
|
|
|
|
|
self.header.hashes = self.code_hashes
|
|
|
|
|
class BootloaderImage(firmware.FirmwareImage, CosiSignedMixin):
|
|
|
|
|
NAME = "bootloader"
|
|
|
|
|
DEV_KEYS = _make_dev_keys(b"\x41", b"\x42")
|
|
|
|
|
|
|
|
|
|
def format(self, verbose: bool = False) -> str:
|
|
|
|
|
header_out = self.header.copy()
|
|
|
|
|
def get_header(self) -> CosiSignatureHeaderProto:
|
|
|
|
|
return self.header
|
|
|
|
|
|
|
|
|
|
if not verbose:
|
|
|
|
|
for key in self.header:
|
|
|
|
|
if key.startswith("v1"):
|
|
|
|
|
del header_out[key]
|
|
|
|
|
if "version" in key:
|
|
|
|
|
header_out[key] = LiteralStr(_format_version(self.header[key]))
|
|
|
|
|
def format(self, verbose: bool = False) -> str:
|
|
|
|
|
return format_header(
|
|
|
|
|
self.header,
|
|
|
|
|
self.code_hashes(),
|
|
|
|
|
self.digest(),
|
|
|
|
|
_check_signature_any(self, False),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
all_ok = SYM_OK
|
|
|
|
|
hash_status = Status.VALID
|
|
|
|
|
sig_status = Status.VALID
|
|
|
|
|
def verify(self) -> None:
|
|
|
|
|
self.validate_code_hashes()
|
|
|
|
|
try:
|
|
|
|
|
cosi.verify(
|
|
|
|
|
self.header.signature,
|
|
|
|
|
self.digest(),
|
|
|
|
|
firmware.V2_SIGS_REQUIRED,
|
|
|
|
|
firmware.V2_BOARDLOADER_KEYS,
|
|
|
|
|
self.header.sigmask,
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
raise firmware.InvalidSignatureError("Invalid bootloader signature")
|
|
|
|
|
|
|
|
|
|
def public_keys(self) -> t.Sequence[bytes]:
|
|
|
|
|
return firmware.V2_BOARDLOADER_KEYS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LegacyFirmware(firmware.LegacyFirmware):
|
|
|
|
|
NAME = "legacy_firmware_v1"
|
|
|
|
|
BIP32_INDEX = None
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
hashes_out = []
|
|
|
|
|
for expected, actual in zip(self.header.hashes, self.code_hashes):
|
|
|
|
|
status = SYM_OK if expected == actual else SYM_FAIL
|
|
|
|
|
hashes_out.append(LiteralStr(f"{status} {expected.hex()}"))
|
|
|
|
|
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")
|
|
|
|
|
self.key_indexes[slot] = key_index
|
|
|
|
|
self.signatures[slot] = signature
|
|
|
|
|
|
|
|
|
|
if all(all_zero(h) for h in self.header.hashes):
|
|
|
|
|
hash_status = Status.MISSING
|
|
|
|
|
elif self.header.hashes != self.code_hashes:
|
|
|
|
|
hash_status = Status.INVALID
|
|
|
|
|
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)
|
|
|
|
|
else:
|
|
|
|
|
hash_status = Status.VALID
|
|
|
|
|
|
|
|
|
|
header_out["hashes"] = hashes_out
|
|
|
|
|
embedded_content = ""
|
|
|
|
|
|
|
|
|
|
sig_status = self.check_signature()
|
|
|
|
|
all_ok = SYM_OK if hash_status.is_ok() and sig_status.is_ok() else SYM_FAIL
|
|
|
|
|
return _format_container(contents) + embedded_content
|
|
|
|
|
|
|
|
|
|
output = [
|
|
|
|
|
"Firmware Header " + _format_container(header_out),
|
|
|
|
|
f"Fingerprint: {click.style(self.digest().hex(), bold=True)}",
|
|
|
|
|
f"{all_ok} Signature is {sig_status.value}, hashes are {hash_status.value}",
|
|
|
|
|
]
|
|
|
|
|
def public_keys(self) -> t.Sequence[bytes]:
|
|
|
|
|
return firmware.V1_BOOTLOADER_KEYS
|
|
|
|
|
|
|
|
|
|
return "\n".join(output)
|
|
|
|
|
def slots(self) -> t.Iterable[int]:
|
|
|
|
|
return self.key_indexes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FirmwareImage(BinImage):
|
|
|
|
|
NAME = "firmware"
|
|
|
|
|
BIP32_INDEX = 2
|
|
|
|
|
DEV_KEYS = _make_dev_keys(b"\x47", b"\x48")
|
|
|
|
|
class LegacyV2Firmware(firmware.LegacyV2Firmware):
|
|
|
|
|
NAME = "legacy_firmware_v2"
|
|
|
|
|
BIP32_INDEX = 5
|
|
|
|
|
|
|
|
|
|
def __init__(self, fw: c.Container) -> None:
|
|
|
|
|
super().__init__(fw)
|
|
|
|
|
self.public_keys = fw.vendor_header.pubkeys
|
|
|
|
|
self.sigs_required = fw.vendor_header.sig_m
|
|
|
|
|
|
|
|
|
|
def check_signature(self) -> Status:
|
|
|
|
|
vhash = compute_vhash(self.fw.vendor_header)
|
|
|
|
|
return _check_signature_any(
|
|
|
|
|
self.digest_header,
|
|
|
|
|
self.sigs_required,
|
|
|
|
|
self.public_keys,
|
|
|
|
|
vhash == VHASH_DEVEL,
|
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
def format(self, verbose: bool = False) -> str:
|
|
|
|
|
return (
|
|
|
|
|
VendorHeader(self.fw)._format(terse=not verbose)
|
|
|
|
|
+ "\n"
|
|
|
|
|
+ super().format(verbose)
|
|
|
|
|
return format_header(
|
|
|
|
|
self.header,
|
|
|
|
|
self.code_hashes(),
|
|
|
|
|
self.digest(),
|
|
|
|
|
_check_signature_any(self, False),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def public_keys(self) -> t.Sequence[bytes]:
|
|
|
|
|
return firmware.V1_BOOTLOADER_KEYS
|
|
|
|
|
|
|
|
|
|
class BootloaderImage(BinImage):
|
|
|
|
|
NAME = "bootloader"
|
|
|
|
|
BIP32_INDEX = 0
|
|
|
|
|
DEV_KEYS = _make_dev_keys(b"\x41", b"\x42")
|
|
|
|
|
def slots(self) -> t.Iterable[int]:
|
|
|
|
|
return self.header.v1_key_indexes
|
|
|
|
|
|
|
|
|
|
def __init__(self, fw: c.Container) -> None:
|
|
|
|
|
super().__init__(fw)
|
|
|
|
|
self._identify_dev_keys()
|
|
|
|
|
|
|
|
|
|
def insert_signature(self, signature: bytes, sigmask: int) -> None:
|
|
|
|
|
super().insert_signature(signature, sigmask)
|
|
|
|
|
self._identify_dev_keys()
|
|
|
|
|
|
|
|
|
|
def _identify_dev_keys(self) -> None:
|
|
|
|
|
# try checking signature with dev keys first
|
|
|
|
|
self.public_keys = firmware.V2_BOARDLOADER_DEV_KEYS
|
|
|
|
|
if not self.check_signature().is_ok():
|
|
|
|
|
# validation with dev keys failed, use production keys
|
|
|
|
|
self.public_keys = firmware.V2_BOARDLOADER_KEYS
|
|
|
|
|
|
|
|
|
|
def check_signature(self) -> Status:
|
|
|
|
|
return _check_signature_any(
|
|
|
|
|
self.header,
|
|
|
|
|
self.sigs_required,
|
|
|
|
|
self.public_keys,
|
|
|
|
|
self.public_keys == firmware.V2_BOARDLOADER_DEV_KEYS,
|
|
|
|
|
)
|
|
|
|
|
def parse_image(image: bytes) -> SignableImageProto:
|
|
|
|
|
try:
|
|
|
|
|
return VendorFirmware.parse(image)
|
|
|
|
|
except c.ConstructError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
def parse_image(image: bytes) -> SignableImage:
|
|
|
|
|
fw = AnyFirmware.parse(image)
|
|
|
|
|
if fw.vendor_header and not fw.image:
|
|
|
|
|
return VendorHeader(fw)
|
|
|
|
|
if (
|
|
|
|
|
not fw.vendor_header
|
|
|
|
|
and fw.image
|
|
|
|
|
and fw.image.header.magic == firmware.HeaderType.BOOTLOADER
|
|
|
|
|
):
|
|
|
|
|
return BootloaderImage(fw)
|
|
|
|
|
if (
|
|
|
|
|
fw.vendor_header
|
|
|
|
|
and fw.image
|
|
|
|
|
and fw.image.header.magic == firmware.HeaderType.FIRMWARE
|
|
|
|
|
):
|
|
|
|
|
return FirmwareImage(fw)
|
|
|
|
|
raise ValueError("Unrecognized image type")
|
|
|
|
|
raise ValueError("Unrecognized firmware type")
|
|
|
|
|