mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-27 00:28:10 +00:00
Merge branch 'matejcik/headertool'
This commit is contained in:
commit
06003d0e01
1
Pipfile
1
Pipfile
@ -9,6 +9,7 @@ trezor = {editable = true,path = "./python"}
|
||||
scons = "*"
|
||||
protobuf = "==3.6.1"
|
||||
pyblake2 = "*"
|
||||
Pyro4 = "*"
|
||||
|
||||
## test tools
|
||||
pytest = "*"
|
||||
|
30
Pipfile.lock
generated
30
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "e8d9a82935300b8716e549422ded189b2ce4408bc31b8d5c91bbe9979bf15a0a"
|
||||
"sha256": "1ff8a28e128037758ba995c7530207c6caf381d0b062cda5b3a611b5ddd936bd"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@ -407,10 +407,10 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47",
|
||||
"sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"
|
||||
"sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb",
|
||||
"sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8"
|
||||
],
|
||||
"version": "==19.2"
|
||||
"version": "==20.0"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
@ -533,6 +533,14 @@
|
||||
],
|
||||
"version": "==2.4.6"
|
||||
},
|
||||
"pyro4": {
|
||||
"hashes": [
|
||||
"sha256:2bfe12a22f396474b0e57c898c7e2c561a8f850bf2055d8cf0f7119f0c7a523f",
|
||||
"sha256:7c4712257ba5bed8bc4ed037bdad5b4683a483a5fd634a2ac3effa5ba787f511"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.77"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa",
|
||||
@ -600,6 +608,13 @@
|
||||
"index": "pypi",
|
||||
"version": "==3.1.2"
|
||||
},
|
||||
"serpent": {
|
||||
"hashes": [
|
||||
"sha256:c8e18d7c787612abb811474a2cce310a1e5e737eac9d858a073d4f3c44a4f3b6",
|
||||
"sha256:f306336ca09aa38e526f3b03cab58eb7e45af09981267233167bcf3bfd6436ab"
|
||||
],
|
||||
"version": "==1.28"
|
||||
},
|
||||
"shamir-mnemonic": {
|
||||
"hashes": [
|
||||
"sha256:1cc08d276e05b13cd32bd3b0c5d1cb6c30254e0086e0f6857ec106d4cceff121",
|
||||
@ -700,7 +715,8 @@
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"
|
||||
"sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603",
|
||||
"sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"
|
||||
],
|
||||
"version": "==0.1.8"
|
||||
},
|
||||
@ -715,10 +731,10 @@
|
||||
"develop": {
|
||||
"scan-build": {
|
||||
"hashes": [
|
||||
"sha256:29f8a99f61fa5bedd4be4eff00d1dd50d9990ec9853230b9fc826c0c694146fa"
|
||||
"sha256:296bb899dc4c126b985fca0c15c9ec20cf758b7c7ff830db64c4ca971d886797"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.17"
|
||||
"version": "==2.0.18"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -214,8 +214,8 @@ gdb_firmware: $(FIRMWARE_BUILD_DIR)/firmware.elf ## start remote gdb session to
|
||||
## misc commands:
|
||||
|
||||
binctl: ## print info about binary files
|
||||
./tools/binctl $(BOOTLOADER_BUILD_DIR)/bootloader.bin
|
||||
./tools/binctl $(FIRMWARE_BUILD_DIR)/firmware.bin
|
||||
./tools/headertool.py $(BOOTLOADER_BUILD_DIR)/bootloader.bin
|
||||
./tools/headertool.py $(FIRMWARE_BUILD_DIR)/firmware.bin
|
||||
|
||||
bloaty: ## run bloaty size profiler
|
||||
bloaty -d symbols -n 0 -s file $(FIRMWARE_BUILD_DIR)/firmware.elf | less
|
||||
|
@ -160,8 +160,7 @@ env.Replace(
|
||||
ASPPFLAGS='$CFLAGS $CCFLAGS', )
|
||||
|
||||
env.Replace(
|
||||
BINCTL='tools/binctl',
|
||||
KEYCTL='tools/keyctl',
|
||||
HEADERTOOL='tools/headertool.py',
|
||||
)
|
||||
|
||||
#
|
||||
@ -186,6 +185,5 @@ program_bin = env.Command(
|
||||
source=program_elf,
|
||||
action=[
|
||||
'$OBJCOPY -O binary -j .header -j .flash -j .data $SOURCE $TARGET',
|
||||
'$BINCTL $TARGET -h',
|
||||
'$BINCTL $TARGET -s 1:2 `$KEYCTL sign bootloader $TARGET 4141414141414141414141414141414141414141414141414141414141414141 4242424242424242424242424242424242424242424242424242424242424242`' if ARGUMENTS.get('PRODUCTION', '0') == '0' else '',
|
||||
'$HEADERTOOL $TARGET ' + ('-D' if ARGUMENTS.get('PRODUCTION', '0') == '0' else ''),
|
||||
], )
|
||||
|
@ -397,8 +397,7 @@ env.Replace(
|
||||
ASPPFLAGS='$CFLAGS $CCFLAGS', )
|
||||
|
||||
env.Replace(
|
||||
BINCTL='tools/binctl',
|
||||
KEYCTL='tools/keyctl',
|
||||
HEADERTOOL='tools/headertool.py',
|
||||
PYTHON='python',
|
||||
MAKEQSTRDATA='$PYTHON vendor/micropython/py/makeqstrdata.py',
|
||||
MAKEVERSIONHDR='$PYTHON vendor/micropython/py/makeversionhdr.py',
|
||||
@ -613,8 +612,7 @@ if env.get('TREZOR_MODEL') == 'T':
|
||||
'$OBJCOPY -O binary -j .vendorheader -j .header -j .flash -j .data --pad-to 0x08100000 $SOURCE ${TARGET}.p1',
|
||||
'$OBJCOPY -O binary -j .flash2 $SOURCE ${TARGET}.p2',
|
||||
'$CAT ${TARGET}.p1 ${TARGET}.p2 > $TARGET',
|
||||
'$BINCTL $TARGET -h',
|
||||
'$BINCTL $TARGET -s 1:2 `$KEYCTL sign firmware $TARGET 4747474747474747474747474747474747474747474747474747474747474747 4848484848484848484848484848484848484848484848484848484848484848`' if ARGUMENTS.get('PRODUCTION', '0') == '0' else '',
|
||||
'$HEADERTOOL $TARGET ' + ('-D' if ARGUMENTS.get('PRODUCTION', '0') == '0' else ''),
|
||||
'$DD if=$TARGET of=${TARGET}.p1 skip=0 bs=128k count=6',
|
||||
]
|
||||
else:
|
||||
|
@ -136,8 +136,7 @@ env.Replace(
|
||||
ASPPFLAGS='$CFLAGS $CCFLAGS', )
|
||||
|
||||
env.Replace(
|
||||
BINCTL='tools/binctl',
|
||||
KEYCTL='tools/keyctl',
|
||||
HEADERTOOL='tools/headertool.py',
|
||||
)
|
||||
|
||||
#
|
||||
@ -172,6 +171,5 @@ program_bin = env.Command(
|
||||
source=program_elf,
|
||||
action=[
|
||||
'$OBJCOPY -O binary -j .vendorheader -j .header -j .flash -j .data $SOURCE $TARGET',
|
||||
'$BINCTL $TARGET -h',
|
||||
'$BINCTL $TARGET -s 1:2 `$KEYCTL sign firmware $TARGET 4747474747474747474747474747474747474747474747474747474747474747 4848484848484848484848484848484848484848484848484848484848484848`' if ARGUMENTS.get('PRODUCTION', '0') == '0' else '',
|
||||
'$HEADERTOOL $TARGET ' + ('-D' if ARGUMENTS.get('PRODUCTION', '0') == '0' else ''),
|
||||
], )
|
||||
|
@ -130,8 +130,7 @@ env.Replace(
|
||||
ASPPFLAGS='$CFLAGS $CCFLAGS', )
|
||||
|
||||
env.Replace(
|
||||
BINCTL='tools/binctl',
|
||||
KEYCTL='tools/keyctl',
|
||||
HEADERTOOL='tools/headertool.py',
|
||||
)
|
||||
|
||||
#
|
||||
@ -166,6 +165,5 @@ program_bin = env.Command(
|
||||
source=program_elf,
|
||||
action=[
|
||||
'$OBJCOPY -O binary -j .vendorheader -j .header -j .flash -j .data $SOURCE $TARGET',
|
||||
'$BINCTL $TARGET -h',
|
||||
'$BINCTL $TARGET -s 1:2 `$KEYCTL sign firmware $TARGET 4747474747474747474747474747474747474747474747474747474747474747 4848484848484848484848484848484848484848484848484848484848484848`' if ARGUMENTS.get('PRODUCTION', '0') == '0' else '',
|
||||
'$HEADERTOOL $TARGET ' + ('-D' if ARGUMENTS.get('PRODUCTION', '0') == '0' else ''),
|
||||
], )
|
||||
|
@ -1,13 +1,12 @@
|
||||
BINCTL=../../tools/binctl
|
||||
KEYCTL=../../tools/keyctl
|
||||
BUILDVH=../../tools/build_vendorheader
|
||||
BINCTL=../../tools/headertool.py
|
||||
|
||||
# construct the default unsafe vendor header
|
||||
$BUILDVH e28a8970753332bd72fef413e6b0b2ef1b4aadda7aa2c141f233712a6876b351:d4eec1869fb1b8a4e817516ad5a931557cb56805c3eb16e8f3a803d647df7869:772c8a442b7db06e166cfbc1ccbcbcde6f3eba76a4e98ef3ffc519502237d6ef 2 0.0 xxx...x "UNSAFE, DO NOT USE!" vendor_unsafe.toif vendorheader_unsafe_unsigned.bin
|
||||
# construct all vendor headers
|
||||
for fn in *.json; do
|
||||
name=$(echo $fn | sed 's/vendor_\(.*\)\.json/\1/')
|
||||
$BUILDVH vendor_${name}.json vendor_${name}.toif vendorheader_${name}_unsigned.bin
|
||||
done
|
||||
|
||||
# sign the default unsafe vendor header using development keys
|
||||
# sign dev vendor header
|
||||
cp -a vendorheader_unsafe_unsigned.bin vendorheader_unsafe_signed_dev.bin
|
||||
$BINCTL vendorheader_unsafe_signed_dev.bin -s 1:2 `$KEYCTL sign vendorheader vendorheader_unsafe_signed_dev.bin 4444444444444444444444444444444444444444444444444444444444444444 4545454545454545454545454545454545454545454545454545454545454545`
|
||||
|
||||
# construct SatoshiLabs vendor header
|
||||
$BUILDVH 47fbdc84d8abef44fe6abde8f87b6ead821b7082ec63b9f7cc33dc53bf6c708d:9af22a52ab47a93091403612b3d6731a2dfef8a33383048ed7556a20e8b03c81:2218c25f8ba70c82eba8ed6a321df209c0a7643d014f33bf9317846f62923830 2 0.0 ....... SatoshiLabs vendor_satoshilabs.toif vendorheader_satoshilabs_unsigned.bin
|
||||
$BINCTL -D vendorheader_unsafe_signed_dev.bin
|
||||
|
20
core/embed/vendorheader/vendor_satoshilabs.json
Normal file
20
core/embed/vendorheader/vendor_satoshilabs.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"text": "SatoshiLabs",
|
||||
"expiry": 0,
|
||||
"version": {
|
||||
"major": 0,
|
||||
"minor": 0
|
||||
},
|
||||
"sig_m": 2,
|
||||
"trust": {
|
||||
"show_vendor_string": false,
|
||||
"require_user_click": false,
|
||||
"red_background": false,
|
||||
"delay": 0
|
||||
},
|
||||
"pubkeys": [
|
||||
"47fbdc84d8abef44fe6abde8f87b6ead821b7082ec63b9f7cc33dc53bf6c708d",
|
||||
"9af22a52ab47a93091403612b3d6731a2dfef8a33383048ed7556a20e8b03c81",
|
||||
"2218c25f8ba70c82eba8ed6a321df209c0a7643d014f33bf9317846f62923830"
|
||||
]
|
||||
}
|
20
core/embed/vendorheader/vendor_unsafe.json
Normal file
20
core/embed/vendorheader/vendor_unsafe.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"text": "UNSAFE, DO NOT USE!",
|
||||
"expiry": 0,
|
||||
"version": {
|
||||
"major": 0,
|
||||
"minor": 0
|
||||
},
|
||||
"sig_m": 2,
|
||||
"trust": {
|
||||
"show_vendor_string": true,
|
||||
"require_user_click": true,
|
||||
"red_background": true,
|
||||
"delay": 1
|
||||
},
|
||||
"pubkeys": [
|
||||
"e28a8970753332bd72fef413e6b0b2ef1b4aadda7aa2c141f233712a6876b351",
|
||||
"d4eec1869fb1b8a4e817516ad5a931557cb56805c3eb16e8f3a803d647df7869",
|
||||
"772c8a442b7db06e166cfbc1ccbcbcde6f3eba76a4e98ef3ffc519502237d6ef"
|
||||
]
|
||||
}
|
@ -1,359 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
import pyblake2
|
||||
|
||||
from trezorlib import cosi
|
||||
|
||||
|
||||
def format_sigmask(sigmask):
|
||||
bits = [str(b + 1) if sigmask & (1 << b) else "." for b in range(8)]
|
||||
return "0x%02x = [%s]" % (sigmask, " ".join(bits))
|
||||
|
||||
|
||||
def format_vtrust(vtrust):
|
||||
bits = [str(b) if vtrust & (1 << b) == 0 else "." for b in range(16)]
|
||||
# see docs/bootloader.md for vtrust constants
|
||||
desc = ""
|
||||
wait = (vtrust & 0x000F) ^ 0x000F
|
||||
if wait > 0:
|
||||
desc = "WAIT_%d" % wait
|
||||
if vtrust & 0x0010 == 0:
|
||||
desc += " RED"
|
||||
if vtrust & 0x0020 == 0:
|
||||
desc += " CLICK"
|
||||
if vtrust & 0x0040 == 0:
|
||||
desc += " STRING"
|
||||
return "%d = [%s] = [%s]" % (vtrust, " ".join(bits), desc)
|
||||
|
||||
|
||||
# bootloader/firmware headers specification: https://github.com/trezor/trezor-core/blob/master/docs/bootloader.md
|
||||
|
||||
IMAGE_HEADER_SIZE = 1024
|
||||
IMAGE_SIG_SIZE = 65
|
||||
IMAGE_CHUNK_SIZE = 128 * 1024
|
||||
BOOTLOADER_SECTORS_COUNT = 1
|
||||
FIRMWARE_SECTORS_COUNT = 6 + 7
|
||||
|
||||
|
||||
class BinImage(object):
|
||||
def __init__(self, data, magic, max_size):
|
||||
header = struct.unpack("<4sIIIBBBBBBBB8s512s415sB64s", data[:IMAGE_HEADER_SIZE])
|
||||
self.magic, self.hdrlen, self.expiry, self.codelen, self.vmajor, self.vminor, self.vpatch, self.vbuild, self.fix_vmajor, self.fix_vminor, self.fix_vpatch, self.fix_vbuild, self.reserved1, self.hashes, self.reserved2, self.sigmask, self.sig = (
|
||||
header
|
||||
)
|
||||
assert self.magic == magic
|
||||
assert self.hdrlen == IMAGE_HEADER_SIZE
|
||||
total_len = self.hdrlen + self.codelen
|
||||
assert total_len % 512 == 0
|
||||
assert total_len >= 4 * 1024
|
||||
assert total_len <= max_size
|
||||
assert self.reserved1 == 8 * b"\x00"
|
||||
assert self.reserved2 == 415 * b"\x00"
|
||||
self.code = data[self.hdrlen :]
|
||||
assert len(self.code) == self.codelen
|
||||
|
||||
def print(self):
|
||||
if self.magic == b"TRZF":
|
||||
print("Trezor Firmware Image")
|
||||
total_len = self.vhdrlen + self.hdrlen + self.codelen
|
||||
elif self.magic == b"TRZB":
|
||||
print("Trezor Bootloader Image")
|
||||
total_len = self.hdrlen + self.codelen
|
||||
else:
|
||||
print("Trezor Unknown Image")
|
||||
print(" * magic :", self.magic.decode())
|
||||
print(" * hdrlen :", self.hdrlen)
|
||||
print(" * expiry :", self.expiry)
|
||||
print(" * codelen :", self.codelen)
|
||||
print(
|
||||
" * version : %d.%d.%d.%d"
|
||||
% (self.vmajor, self.vminor, self.vpatch, self.vbuild)
|
||||
)
|
||||
print(
|
||||
" * fixver : %d.%d.%d.%d"
|
||||
% (self.fix_vmajor, self.fix_vminor, self.fix_vpatch, self.fix_vbuild)
|
||||
)
|
||||
print(" * hashes: %s" % ("OK" if self.check_hashes() else "INCORRECT"))
|
||||
for i in range(16):
|
||||
print(
|
||||
" - %02d : %s"
|
||||
% (i, binascii.hexlify(self.hashes[i * 32 : i * 32 + 32]).decode())
|
||||
)
|
||||
print(" * sigmask :", format_sigmask(self.sigmask))
|
||||
print(" * sig :", binascii.hexlify(self.sig).decode())
|
||||
print(" * total : %d bytes" % total_len)
|
||||
print(" * fngprnt :", self.fingerprint())
|
||||
print()
|
||||
|
||||
def compute_hashes(self):
|
||||
if self.magic == b"TRZF":
|
||||
hdrlen = self.vhdrlen + self.hdrlen
|
||||
else:
|
||||
hdrlen = self.hdrlen
|
||||
hashes = b""
|
||||
for i in range(16):
|
||||
if i == 0:
|
||||
d = self.code[: IMAGE_CHUNK_SIZE - hdrlen]
|
||||
else:
|
||||
s = IMAGE_CHUNK_SIZE - hdrlen + (i - 1) * IMAGE_CHUNK_SIZE
|
||||
d = self.code[s : s + IMAGE_CHUNK_SIZE]
|
||||
if len(d) > 0:
|
||||
h = pyblake2.blake2s(d).digest()
|
||||
else:
|
||||
h = 32 * b"\x00"
|
||||
hashes += h
|
||||
return hashes
|
||||
|
||||
def check_hashes(self):
|
||||
return self.hashes == self.compute_hashes()
|
||||
|
||||
def update_hashes(self):
|
||||
self.hashes = self.compute_hashes()
|
||||
|
||||
def serialize_header(self, sig=True):
|
||||
header = struct.pack(
|
||||
"<4sIIIBBBBBBBB8s512s415s",
|
||||
self.magic,
|
||||
self.hdrlen,
|
||||
self.expiry,
|
||||
self.codelen,
|
||||
self.vmajor,
|
||||
self.vminor,
|
||||
self.vpatch,
|
||||
self.vbuild,
|
||||
self.fix_vmajor,
|
||||
self.fix_vminor,
|
||||
self.fix_vpatch,
|
||||
self.fix_vbuild,
|
||||
self.reserved1,
|
||||
self.hashes,
|
||||
self.reserved2,
|
||||
)
|
||||
if sig:
|
||||
header += struct.pack("<B64s", self.sigmask, self.sig)
|
||||
else:
|
||||
header += IMAGE_SIG_SIZE * b"\x00"
|
||||
assert len(header) == self.hdrlen
|
||||
return header
|
||||
|
||||
def fingerprint(self):
|
||||
return pyblake2.blake2s(self.serialize_header(sig=False)).hexdigest()
|
||||
|
||||
def sign(self, sigmask, signature):
|
||||
header = self.serialize_header(sig=False)
|
||||
data = header + self.code
|
||||
assert len(data) == self.hdrlen + self.codelen
|
||||
self.sigmask = sigmask
|
||||
self.sig = signature
|
||||
|
||||
def write(self, filename):
|
||||
with open(filename, "wb") as f:
|
||||
f.write(self.serialize_header())
|
||||
f.write(self.code)
|
||||
|
||||
|
||||
class FirmwareImage(BinImage):
|
||||
def __init__(self, data, vhdrlen):
|
||||
super(FirmwareImage, self).__init__(
|
||||
data[vhdrlen:],
|
||||
magic=b"TRZF",
|
||||
max_size=FIRMWARE_SECTORS_COUNT * IMAGE_CHUNK_SIZE,
|
||||
)
|
||||
self.vhdrlen = vhdrlen
|
||||
self.vheader = data[:vhdrlen]
|
||||
|
||||
def write(self, filename):
|
||||
with open(filename, "wb") as f:
|
||||
f.write(self.vheader)
|
||||
f.write(self.serialize_header())
|
||||
f.write(self.code)
|
||||
|
||||
|
||||
class BootloaderImage(BinImage):
|
||||
def __init__(self, data):
|
||||
super(BootloaderImage, self).__init__(
|
||||
data, magic=b"TRZB", max_size=BOOTLOADER_SECTORS_COUNT * IMAGE_CHUNK_SIZE
|
||||
)
|
||||
|
||||
|
||||
class VendorHeader(object):
|
||||
def __init__(self, data):
|
||||
header = struct.unpack("<4sIIBBBBH", data[:18])
|
||||
self.magic, self.hdrlen, self.expiry, self.vmajor, self.vminor, self.vsig_m, self.vsig_n, self.vtrust = (
|
||||
header
|
||||
)
|
||||
assert self.magic == b"TRZV"
|
||||
data = data[: self.hdrlen] # strip remaining data (firmware)
|
||||
assert self.vsig_m > 0 and self.vsig_m <= self.vsig_n
|
||||
assert self.vsig_n > 0 and self.vsig_n <= 8
|
||||
p = 32
|
||||
self.vpub = []
|
||||
for _ in range(self.vsig_n):
|
||||
self.vpub.append(data[p : p + 32])
|
||||
p += 32
|
||||
self.vstr_len = data[p]
|
||||
p += 1
|
||||
self.vstr = data[p : p + self.vstr_len]
|
||||
p += self.vstr_len
|
||||
vstr_pad = -p & 3
|
||||
p += vstr_pad
|
||||
self.vimg_len = len(data) - IMAGE_SIG_SIZE - p
|
||||
self.vimg = data[p : p + self.vimg_len]
|
||||
p += self.vimg_len
|
||||
self.sigmask = data[p]
|
||||
p += 1
|
||||
self.sig = data[p : p + 64]
|
||||
assert (
|
||||
len(data)
|
||||
== 4
|
||||
+ 4
|
||||
+ 4
|
||||
+ 1
|
||||
+ 1
|
||||
+ 1
|
||||
+ 1
|
||||
+ 1
|
||||
+ 15
|
||||
+ 32 * len(self.vpub)
|
||||
+ 1
|
||||
+ self.vstr_len
|
||||
+ vstr_pad
|
||||
+ self.vimg_len
|
||||
+ IMAGE_SIG_SIZE
|
||||
)
|
||||
assert len(data) % 512 == 0
|
||||
|
||||
def print(self):
|
||||
print("Trezor Vendor Header")
|
||||
print(" * magic :", self.magic.decode())
|
||||
print(" * hdrlen :", self.hdrlen)
|
||||
print(" * expiry :", self.expiry)
|
||||
print(" * version : %d.%d" % (self.vmajor, self.vminor))
|
||||
print(" * scheme : %d out of %d" % (self.vsig_m, self.vsig_n))
|
||||
print(" * trust :", format_vtrust(self.vtrust))
|
||||
for i in range(self.vsig_n):
|
||||
print(" * vpub #%d :" % (i + 1), binascii.hexlify(self.vpub[i]).decode())
|
||||
print(" * vstr :", self.vstr.decode())
|
||||
print(" * vhash :", binascii.hexlify(self.vhash()).decode())
|
||||
print(" * vimg : (%d bytes)" % len(self.vimg))
|
||||
print(" * sigmask :", format_sigmask(self.sigmask))
|
||||
print(" * sig :", binascii.hexlify(self.sig).decode())
|
||||
print(" * fngprnt :", self.fingerprint())
|
||||
print()
|
||||
|
||||
def serialize_header(self, sig=True):
|
||||
header = struct.pack(
|
||||
"<4sIIBBBBH",
|
||||
self.magic,
|
||||
self.hdrlen,
|
||||
self.expiry,
|
||||
self.vmajor,
|
||||
self.vminor,
|
||||
self.vsig_m,
|
||||
self.vsig_n,
|
||||
self.vtrust,
|
||||
)
|
||||
header += 14 * b"\x00"
|
||||
for i in range(self.vsig_n):
|
||||
header += self.vpub[i]
|
||||
header += struct.pack("<B", self.vstr_len) + self.vstr
|
||||
header += (-len(header) & 3) * b"\x00" # vstr_pad
|
||||
header += self.vimg
|
||||
if sig:
|
||||
header += struct.pack("<B64s", self.sigmask, self.sig)
|
||||
else:
|
||||
header += IMAGE_SIG_SIZE * b"\x00"
|
||||
assert len(header) == self.hdrlen
|
||||
return header
|
||||
|
||||
def fingerprint(self):
|
||||
return pyblake2.blake2s(self.serialize_header(sig=False)).hexdigest()
|
||||
|
||||
def vhash(self):
|
||||
h = pyblake2.blake2s()
|
||||
h.update(struct.pack("<BB", self.vsig_m, self.vsig_n))
|
||||
for i in range(8):
|
||||
if i < self.vsig_n:
|
||||
h.update(self.vpub[i])
|
||||
else:
|
||||
h.update(b"\x00" * 32)
|
||||
return h.digest()
|
||||
|
||||
def sign(self, sigmask, signature):
|
||||
header = self.serialize_header(sig=False)
|
||||
assert len(header) == self.hdrlen
|
||||
self.sigmask = sigmask
|
||||
self.sig = signature
|
||||
|
||||
def write(self, filename):
|
||||
with open(filename, "wb") as f:
|
||||
f.write(self.serialize_header())
|
||||
|
||||
|
||||
def binopen(filename):
|
||||
data = open(filename, "rb").read()
|
||||
magic = data[:4]
|
||||
if magic == b"TRZB":
|
||||
return BootloaderImage(data)
|
||||
if magic == b"TRZV":
|
||||
vheader = VendorHeader(data)
|
||||
if len(data) == vheader.hdrlen:
|
||||
return vheader
|
||||
vheader.print()
|
||||
subdata = data[vheader.hdrlen :]
|
||||
if subdata[:4] == b"TRZF":
|
||||
firmware = FirmwareImage(data, vheader.hdrlen)
|
||||
# check signatures against signing keys in the vendor header
|
||||
if firmware.sigmask > 0:
|
||||
pk = [vheader.vpub[i] for i in range(8) if firmware.sigmask & (1 << i)]
|
||||
global_pk = cosi.combine_keys(pk)
|
||||
hdr = (
|
||||
subdata[: IMAGE_HEADER_SIZE - IMAGE_SIG_SIZE]
|
||||
+ IMAGE_SIG_SIZE * b"\x00"
|
||||
)
|
||||
digest = pyblake2.blake2s(hdr).digest()
|
||||
try:
|
||||
cosi.verify(firmware.sig, digest, global_pk)
|
||||
print("Firmware signature OK")
|
||||
except ValueError:
|
||||
print("Firmware signature INCORRECT")
|
||||
else:
|
||||
print("No firmware signature")
|
||||
return firmware
|
||||
if magic == b"TRZF":
|
||||
return FirmwareImage(data, 0)
|
||||
raise Exception("Unknown file format")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: binctl file.bin [-s sigmask signature] [-h]")
|
||||
return 1
|
||||
fn = sys.argv[1]
|
||||
sign = len(sys.argv) > 2 and sys.argv[2] == "-s"
|
||||
rehash = len(sys.argv) == 3 and sys.argv[2] == "-h"
|
||||
b = binopen(fn)
|
||||
if sign:
|
||||
sigmask = 0
|
||||
if ":" in sys.argv[3]:
|
||||
for idx in sys.argv[3].split(":"):
|
||||
sigmask |= 1 << (int(idx) - 1)
|
||||
else:
|
||||
sigmask = 1 << (int(sys.argv[3]) - 1)
|
||||
signature = binascii.unhexlify(sys.argv[4])
|
||||
b.sign(sigmask, signature)
|
||||
b.write(fn)
|
||||
if rehash:
|
||||
b.update_hashes()
|
||||
b.write(fn)
|
||||
b.print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,65 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import struct
|
||||
import binascii
|
||||
import json
|
||||
|
||||
import click
|
||||
|
||||
from trezorlib import firmware
|
||||
|
||||
|
||||
# encode vendor name, add length byte and padding to multiple of 4
|
||||
def encode_vendor(vname):
|
||||
vbin = vname.encode()
|
||||
vbin = struct.pack("<B", len(vbin)) + vbin
|
||||
vbin += b"\0" * (-len(vbin) & 3)
|
||||
return vbin
|
||||
@click.command()
|
||||
@click.argument("specfile", type=click.File("r"))
|
||||
@click.argument("image", type=click.File("rb"))
|
||||
@click.argument("outfile", type=click.File("wb"))
|
||||
def build_vendorheader(specfile, image, outfile):
|
||||
spec = json.load(specfile)
|
||||
spec["pubkeys"] = [bytes.fromhex(k) for k in spec["pubkeys"]]
|
||||
spec["image"] = firmware.Toif.parse(image.read())
|
||||
spec["sigmask"] = 0
|
||||
spec["signature"] = b"\x00" * 64
|
||||
outfile.write(firmware.VendorHeader.build(spec))
|
||||
|
||||
|
||||
def encode_pubkey(pubkey):
|
||||
if len(pubkey) != 64:
|
||||
raise Exception("Wrong public key length")
|
||||
return binascii.unhexlify(pubkey)
|
||||
|
||||
|
||||
def decode_vtrust(vtrust):
|
||||
t = 0xFFFF
|
||||
for i, b in enumerate(reversed(vtrust)):
|
||||
if b != ".":
|
||||
t &= ~(1 << i)
|
||||
return t
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 7:
|
||||
print(
|
||||
'Usage build_vendorheader "pubkey1hex:pubkey2hex:..." m version vendortrust vendorname vendorimage.toif vendorheader.bin'
|
||||
)
|
||||
return 1
|
||||
|
||||
keys = [encode_pubkey(x) for x in sys.argv[1].split(":")]
|
||||
m = int(sys.argv[2])
|
||||
(vmajor, vminor) = [int(x) for x in sys.argv[3].split(".")]
|
||||
vtrust = decode_vtrust(sys.argv[4])
|
||||
vname = sys.argv[5]
|
||||
ifn = sys.argv[6]
|
||||
ofn = sys.argv[7]
|
||||
if not ifn.endswith(".toif"):
|
||||
print("Must provide TOIF file")
|
||||
return 2
|
||||
|
||||
expiry = 0
|
||||
vheader = b"TRZV" + struct.pack(
|
||||
"<IIBBBBH", 0, expiry, vmajor, vminor, m, len(keys), vtrust
|
||||
)
|
||||
vheader += 14 * b"\0"
|
||||
for k in keys:
|
||||
vheader += k
|
||||
vheader += encode_vendor(vname) + open(ifn, "rb").read()
|
||||
padding = 65 + (-len(vheader) - 65) & 511
|
||||
vheader += b"\0" * padding
|
||||
|
||||
# put in length
|
||||
vheader = vheader[0:4] + struct.pack("<I", len(vheader)) + vheader[8:]
|
||||
|
||||
with open(ofn, "wb") as f:
|
||||
f.write(vheader)
|
||||
|
||||
|
||||
main()
|
||||
if __name__ == "__main__":
|
||||
build_vendorheader()
|
||||
|
279
core/tools/headertool.py
Executable file
279
core/tools/headertool.py
Executable file
@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env python3
|
||||
import click
|
||||
|
||||
from trezorlib import cosi, firmware
|
||||
from trezorlib._internal import firmware_headers
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
try:
|
||||
import Pyro4
|
||||
Pyro4.config.SERIALIZER = "marshal"
|
||||
except ImportError:
|
||||
Pyro4 = None
|
||||
|
||||
PORT = 5001
|
||||
|
||||
# =========================== signing =========================
|
||||
|
||||
|
||||
def sign_with_privkeys(digest: bytes, privkeys: List[bytes]) -> bytes:
|
||||
"""Locally produce a CoSi signature."""
|
||||
pubkeys = [cosi.pubkey_from_privkey(sk) for sk in privkeys]
|
||||
nonces = [cosi.get_nonce(sk, digest, i) for i, sk in enumerate(privkeys)]
|
||||
|
||||
global_pk = cosi.combine_keys(pubkeys)
|
||||
global_R = cosi.combine_keys(R for r, R in nonces)
|
||||
|
||||
sigs = [
|
||||
cosi.sign_with_privkey(digest, sk, global_pk, r, global_R)
|
||||
for sk, (r, R) in zip(privkeys, nonces)
|
||||
]
|
||||
|
||||
signature = cosi.combine_sig(global_R, sigs)
|
||||
try:
|
||||
cosi.verify_combined(signature, digest, global_pk)
|
||||
except Exception as e:
|
||||
raise click.ClickException(f"Failed to produce valid signature.") from e
|
||||
|
||||
return signature
|
||||
|
||||
|
||||
def parse_privkey_args(privkey_data: List[str]) -> Tuple[int, List[bytes]]:
|
||||
privkeys = []
|
||||
sigmask = 0
|
||||
for key in privkey_data:
|
||||
try:
|
||||
idx, key_hex = key.split(":", maxsplit=1)
|
||||
privkeys.append(bytes.fromhex(key_hex))
|
||||
sigmask |= 1 << (int(idx) - 1)
|
||||
except ValueError:
|
||||
click.echo(f"Could not parse key: {key}")
|
||||
click.echo("Keys must be in the format: <key index>:<hex-encoded key>")
|
||||
raise click.ClickException("Unrecognized key format.")
|
||||
return sigmask, privkeys
|
||||
|
||||
|
||||
def process_remote_signers(fw, addrs: List[str]) -> Tuple[int, List[bytes]]:
|
||||
if len(addrs) < fw.sigs_required:
|
||||
raise click.ClickException(
|
||||
f"Not enough signers (need at least {fw.sigs_required})"
|
||||
)
|
||||
|
||||
digest = fw.digest()
|
||||
name = fw.NAME
|
||||
|
||||
def mkproxy(addr):
|
||||
return Pyro4.Proxy(f"PYRO:keyctl@{addr}:{PORT}")
|
||||
|
||||
sigmask = 0
|
||||
pks, Rs = [], []
|
||||
for addr in addrs:
|
||||
click.echo(f"Connecting to {addr}...")
|
||||
with mkproxy(addr) as proxy:
|
||||
pk, R = proxy.get_commit(name, digest)
|
||||
if pk not in fw.public_keys:
|
||||
raise click.ClickException(
|
||||
f"Signer at {addr} commits with unknown public key {pk.hex()}"
|
||||
)
|
||||
idx = fw.public_keys.index(pk)
|
||||
click.echo(f"Signer at {addr} commits with public key #{idx+1}: {pk.hex()}")
|
||||
sigmask |= 1 << idx
|
||||
pks.append(pk)
|
||||
Rs.append(R)
|
||||
|
||||
# compute global commit
|
||||
global_pk = cosi.combine_keys(pks)
|
||||
global_R = cosi.combine_keys(Rs)
|
||||
|
||||
# collect signatures
|
||||
sigs = []
|
||||
for addr in addrs:
|
||||
click.echo(f"Waiting for {addr} to sign... ", nl=False)
|
||||
with mkproxy(addr) as proxy:
|
||||
sig = proxy.get_signature(name, digest, global_R, global_pk)
|
||||
sigs.append(sig)
|
||||
click.echo("OK")
|
||||
|
||||
for addr in addrs:
|
||||
with mkproxy(addr) as proxy:
|
||||
proxy.finish()
|
||||
|
||||
# compute global signature
|
||||
return sigmask, cosi.combine_sig(global_R, sigs)
|
||||
|
||||
|
||||
# ===================== CLI actions =========================
|
||||
|
||||
|
||||
def do_replace_vendorheader(fw, vh_file) -> None:
|
||||
if not isinstance(fw, firmware_headers.FirmwareImage):
|
||||
raise click.ClickException("Invalid image type (must be firmware).")
|
||||
|
||||
vh = firmware.VendorHeader.parse(vh_file.read())
|
||||
if vh.header_len != fw.fw.vendor_header.header_len:
|
||||
raise click.ClickException("New vendor header must have the same size.")
|
||||
|
||||
fw.fw.vendor_header = vh
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("-n", "--dry-run", is_flag=True, help="Do not save changes.")
|
||||
@click.option("-h", "--rehash", is_flag=True, help="Force recalculate hashes.")
|
||||
@click.option("-v", "--verbose", is_flag=True, help="Show verbose info about headers.")
|
||||
@click.option(
|
||||
"-S",
|
||||
"--sign-private",
|
||||
"privkey_data",
|
||||
metavar="INDEX:PRIVKEY_HEX",
|
||||
multiple=True,
|
||||
help="Private key to use for signing. Can be repeated.",
|
||||
)
|
||||
@click.option(
|
||||
"-D", "--sign-dev-keys", is_flag=True, help="Sign with development header keys."
|
||||
)
|
||||
@click.option(
|
||||
"-s",
|
||||
"--signature",
|
||||
"insert_signature",
|
||||
nargs=2,
|
||||
metavar="INDEX:INDEX:INDEX... SIGNATURE_HEX",
|
||||
help="Insert external signature.",
|
||||
)
|
||||
@click.option("-V", "--replace-vendor-header", type=click.File("rb"))
|
||||
@click.option(
|
||||
"-d",
|
||||
"--digest",
|
||||
"print_digest",
|
||||
is_flag=True,
|
||||
help="Only output header digest for signing and exit.",
|
||||
)
|
||||
@click.option(
|
||||
"-r",
|
||||
"--remote",
|
||||
metavar="IPADDR",
|
||||
multiple=True,
|
||||
help="IP address of remote signer. Can be repeated.",
|
||||
)
|
||||
@click.argument("firmware_file", type=click.File("rb+"))
|
||||
def cli(
|
||||
firmware_file,
|
||||
verbose,
|
||||
rehash,
|
||||
dry_run,
|
||||
privkey_data,
|
||||
sign_dev_keys,
|
||||
insert_signature,
|
||||
replace_vendor_header,
|
||||
print_digest,
|
||||
remote,
|
||||
):
|
||||
"""Manage trezor-core firmware headers.
|
||||
|
||||
This tool supports three types of files: raw vendor headers (TRZV), bootloader
|
||||
images (TRZB), and firmware images which are prefixed with a vendor header
|
||||
(TRZV+TRZF).
|
||||
|
||||
Run with no options on a file to dump information about that file.
|
||||
|
||||
Run with -d to print the header digest and exit. This works correctly regardless of
|
||||
whether code hashes have been filled.
|
||||
|
||||
Run with -h to recalculate and fill in code hashes.
|
||||
|
||||
To insert an external signature:
|
||||
|
||||
./headertool.py firmware.bin -s 1:2:3 ABCDEF<...signature in hex format>
|
||||
|
||||
The string "1:2:3" is a list of 1-based indexes of keys used to generate the signature.
|
||||
|
||||
To sign with local private keys:
|
||||
|
||||
\b
|
||||
./headertool.py firmware.bin -S 1:ABCDEF<...hex private key> -S 2:1234<..hex private key>
|
||||
|
||||
Each instance of -S is in the form "index:privkey", where index is the same as
|
||||
above. Instead of specifying the keys manually, use -D to substitue known
|
||||
development keys.
|
||||
|
||||
Signature validity is not checked in either of the two cases.
|
||||
|
||||
To sign with remote participants:
|
||||
|
||||
./headertool.py firmware.bin -r 10.24.13.11 -r 10.24.13.190 ...
|
||||
|
||||
Each participant must be running keyctl-proxy configured on the same file. Signers'
|
||||
public keys must be in the list of known signers and are matched to indexes
|
||||
automatically.
|
||||
"""
|
||||
firmware_data = firmware_file.read()
|
||||
|
||||
try:
|
||||
fw = firmware_headers.parse_image(firmware_data)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
magic = firmware_data[:4]
|
||||
raise click.ClickException(
|
||||
f"Could not parse file (magic bytes: {magic!r})"
|
||||
) from e
|
||||
|
||||
digest = fw.digest()
|
||||
if print_digest:
|
||||
click.echo(digest.hex())
|
||||
return
|
||||
|
||||
if replace_vendor_header:
|
||||
do_replace_vendorheader(fw, replace_vendor_header)
|
||||
|
||||
if rehash:
|
||||
fw.rehash()
|
||||
|
||||
if sign_dev_keys:
|
||||
privkeys = fw.DEV_KEYS
|
||||
sigmask = fw.DEV_KEY_SIGMASK
|
||||
else:
|
||||
sigmask, privkeys = parse_privkey_args(privkey_data)
|
||||
|
||||
signature = None
|
||||
|
||||
if privkeys:
|
||||
click.echo("Signing with local private keys...", err=True)
|
||||
signature = sign_with_privkeys(digest, privkeys)
|
||||
|
||||
if insert_signature:
|
||||
click.echo("Inserting external signature...", err=True)
|
||||
sigmask_str, signature = insert_signature
|
||||
signature = bytes.fromhex(signature)
|
||||
sigmask = 0
|
||||
for bit in sigmask_str.split(":"):
|
||||
sigmask |= 1 << (int(bit) - 1)
|
||||
|
||||
if remote:
|
||||
if Pyro4 is None:
|
||||
raise click.ClickException("Please install Pyro4 for remote signing.")
|
||||
click.echo(fw.format())
|
||||
click.echo(f"Signing with {len(remote)} remote participants.")
|
||||
sigmask, signature = process_remote_signers(fw, remote)
|
||||
|
||||
if signature:
|
||||
fw.rehash()
|
||||
fw.insert_signature(signature, sigmask)
|
||||
|
||||
click.echo(fw.format(verbose))
|
||||
|
||||
updated_data = fw.dump()
|
||||
if updated_data == firmware_data:
|
||||
click.echo("No changes made", err=True)
|
||||
elif dry_run:
|
||||
click.echo("Not saving changes", err=True)
|
||||
else:
|
||||
firmware_file.seek(0)
|
||||
firmware_file.truncate(0)
|
||||
firmware_file.write(updated_data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import binascii
|
||||
import struct
|
||||
import click
|
||||
import pyblake2
|
||||
from trezorlib import cosi
|
||||
|
||||
indexmap = {"bootloader": 0, "vendorheader": 1, "firmware": 2}
|
||||
|
||||
|
||||
def header_digest(index, filename):
|
||||
data = open(filename, "rb").read()
|
||||
z = bytes(65 * [0x00])
|
||||
if index == "bootloader":
|
||||
header = data[:0x03BF] + z
|
||||
elif index == "vendorheader":
|
||||
header = data[:-65] + z
|
||||
elif index == "firmware":
|
||||
vhdrlen = struct.unpack("<I", data[4:8])[0]
|
||||
header = data[vhdrlen : vhdrlen + 0x03BF] + z
|
||||
else:
|
||||
raise ValueError('Unknown index "%s"' % index)
|
||||
return pyblake2.blake2s(header).digest()
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli():
|
||||
pass
|
||||
|
||||
|
||||
@cli.command(help="")
|
||||
@click.argument("index", type=click.Choice(indexmap.keys()))
|
||||
@click.argument("filename")
|
||||
@click.argument("seckeys", nargs=-1)
|
||||
def sign(index, filename, seckeys):
|
||||
# compute header digest
|
||||
digest = header_digest(index, filename)
|
||||
# collect commits
|
||||
pks, nonces, Rs = [], [], []
|
||||
for ctr, seckey in enumerate(seckeys):
|
||||
sk = binascii.unhexlify(seckey)
|
||||
pk = cosi.pubkey_from_privkey(sk)
|
||||
r, R = cosi.get_nonce(sk, digest, ctr)
|
||||
pks.append(pk)
|
||||
nonces.append(r)
|
||||
Rs.append(R)
|
||||
# compute global commit
|
||||
global_pk = cosi.combine_keys(pks)
|
||||
global_R = cosi.combine_keys(Rs)
|
||||
# collect signatures
|
||||
sigs = []
|
||||
for seckey, nonce in zip(seckeys, nonces):
|
||||
sk = binascii.unhexlify(seckey)
|
||||
sig = cosi.sign_with_privkey(digest, sk, global_pk, nonce, global_R)
|
||||
sigs.append(sig)
|
||||
# compute global signature
|
||||
sig = cosi.combine_sig(global_R, sigs)
|
||||
cosi.verify(sig, digest, global_pk)
|
||||
print(binascii.hexlify(sig).decode())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import binascii
|
||||
import struct
|
||||
import click
|
||||
import pyblake2
|
||||
import Pyro4
|
||||
import serpent
|
||||
from trezorlib import cosi
|
||||
|
||||
PORT = 5001
|
||||
indexmap = {"bootloader": 0, "vendorheader": 1, "firmware": 2}
|
||||
|
||||
|
||||
def header_digest(index, filename):
|
||||
data = open(filename, "rb").read()
|
||||
z = bytes(65 * [0x00])
|
||||
if index == "bootloader":
|
||||
header = data[:0x03BF] + z
|
||||
elif index == "vendorheader":
|
||||
header = data[:-65] + z
|
||||
elif index == "firmware":
|
||||
vhdrlen = struct.unpack("<I", data[4:8])[0]
|
||||
header = data[vhdrlen : vhdrlen + 0x03BF] + z
|
||||
else:
|
||||
raise ValueError('Unknown index "%s"' % index)
|
||||
return pyblake2.blake2s(header).digest()
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli():
|
||||
pass
|
||||
|
||||
|
||||
@cli.command(help="")
|
||||
@click.argument("index", type=click.Choice(indexmap.keys()))
|
||||
@click.argument("filename")
|
||||
@click.argument("participants", nargs=-1)
|
||||
def sign(index, filename, participants):
|
||||
# compute header digest
|
||||
digest = header_digest(index, filename)
|
||||
# create participant proxies
|
||||
if len(participants) < 1:
|
||||
raise ValueError("Not enough participants")
|
||||
print("connecting to %d participants:" % len(participants))
|
||||
proxy = []
|
||||
for p in participants:
|
||||
uri = "PYRO:keyctl@%s:%d" % (p, PORT)
|
||||
proxy.append(Pyro4.Proxy(uri))
|
||||
# collect commits
|
||||
pks, Rs = [], []
|
||||
for i, p in enumerate(proxy):
|
||||
pk, R = p.get_commit(index, digest)
|
||||
pk, R = serpent.tobytes(pk), serpent.tobytes(R)
|
||||
pks.append(pk)
|
||||
Rs.append(R)
|
||||
print("collected commit #%d from %s" % (i, p._pyroUri.host))
|
||||
# compute global commit
|
||||
global_pk = cosi.combine_keys(pks)
|
||||
global_R = cosi.combine_keys(Rs)
|
||||
# collect signatures
|
||||
sigs = []
|
||||
for i, p in enumerate(proxy):
|
||||
sig = p.get_signature(index, digest, global_R, global_pk)
|
||||
sig = serpent.tobytes(sig)
|
||||
sigs.append(sig)
|
||||
print("collected signature #%d from %s" % (i, p._pyroUri.host))
|
||||
# compute global signature
|
||||
sig = cosi.combine_sig(global_R, sigs)
|
||||
cosi.verify(sig, digest, global_pk)
|
||||
print("global signature:")
|
||||
print(binascii.hexlify(sig).decode())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
@ -1,86 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
import binascii
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import click
|
||||
import Pyro4
|
||||
import serpent
|
||||
from trezorlib import cosi, tools
|
||||
from trezorlib import cosi
|
||||
from trezorlib.client import get_default_client
|
||||
from trezorlib.tools import parse_path
|
||||
from trezorlib._internal.firmware_headers import (
|
||||
parse_image,
|
||||
VendorHeader,
|
||||
BootloaderImage,
|
||||
FirmwareImage,
|
||||
)
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
Pyro4.config.SERIALIZER = "marshal"
|
||||
|
||||
PORT = 5001
|
||||
indexmap = {"bootloader": 0, "vendorheader": 1, "firmware": 2}
|
||||
indexmap = {
|
||||
"bootloader": BootloaderImage,
|
||||
"vendorheader": VendorHeader,
|
||||
"firmware": FirmwareImage,
|
||||
}
|
||||
|
||||
PATH = "10018h/{}h"
|
||||
|
||||
TREZOR = None
|
||||
|
||||
|
||||
def get_trezor():
|
||||
from trezorlib.client import TrezorClient
|
||||
from trezorlib.transport import get_transport
|
||||
from trezorlib.ui import ClickUI
|
||||
def make_commit(fw_or_type, digest, public_keys):
|
||||
path = PATH.format(fw_or_type.BIP32_INDEX)
|
||||
address_n = parse_path(path)
|
||||
|
||||
return TrezorClient(get_transport(), ui=ClickUI())
|
||||
# device information - show only first time
|
||||
click.echo(
|
||||
f"\nUsing device {click.style(TREZOR.features.label, bold=True)} "
|
||||
f"at path {TREZOR.transport.get_path()}"
|
||||
)
|
||||
|
||||
while True:
|
||||
# signing information - repeat every time
|
||||
click.echo(f"Commiting to {click.style(fw_or_type.NAME, bold=True)} hash:")
|
||||
for partid in range(4):
|
||||
digest_part = digest[partid * 8 : (partid + 1) * 8]
|
||||
color = "red" if partid % 2 else "cyan"
|
||||
digest_str = click.style(digest_part.hex().upper(), fg=color)
|
||||
click.echo("\t" + digest_str)
|
||||
click.echo(f"Using path: {click.style(path, bold=True)}")
|
||||
|
||||
def get_path(index):
|
||||
return "10018'/%d'" % indexmap[index]
|
||||
try:
|
||||
commit = cosi.commit(TREZOR, address_n, digest)
|
||||
if public_keys is not None and commit.pubkey not in public_keys:
|
||||
click.echo(f"\n\nPublic key {commit.pubkey.hex()} is unknown.")
|
||||
if click.confirm("Retry with a different passphrase?", default=True):
|
||||
TREZOR.init_device()
|
||||
continue
|
||||
|
||||
return commit.pubkey, commit.commitment
|
||||
except Exception as e:
|
||||
click.echo(e)
|
||||
traceback.print_exc()
|
||||
click.echo("Trying again ...\n\n")
|
||||
|
||||
|
||||
@Pyro4.expose
|
||||
class KeyctlProxy(object):
|
||||
def get_commit(self, index, digest):
|
||||
digest = serpent.tobytes(digest)
|
||||
path = get_path(index)
|
||||
commit = None
|
||||
while commit is None:
|
||||
try:
|
||||
t = get_trezor()
|
||||
print(
|
||||
"\n\n\nCommiting to hash %s with path %s:"
|
||||
% (binascii.hexlify(digest).decode(), path)
|
||||
)
|
||||
commit = cosi.commit(t, tools.parse_path(path), digest)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
traceback.print_exc()
|
||||
print("Trying again ...")
|
||||
pk = commit.pubkey
|
||||
R = commit.commitment
|
||||
print("Commitment sent!")
|
||||
return (pk, R)
|
||||
class KeyctlProxy:
|
||||
def __init__(
|
||||
self, daemon, image_type, digest: bytes, commit: Tuple[bytes, bytes]
|
||||
) -> None:
|
||||
self.daemon = daemon
|
||||
self.name = image_type.NAME
|
||||
self.address_n = parse_path(PATH.format(image_type.BIP32_INDEX))
|
||||
self.digest = digest
|
||||
self.commit = commit
|
||||
self.signature = None
|
||||
self.global_params = None
|
||||
|
||||
def get_signature(self, index, digest, global_R, global_pk):
|
||||
digest, global_R, global_pk = (
|
||||
serpent.tobytes(digest),
|
||||
serpent.tobytes(global_R),
|
||||
serpent.tobytes(global_pk),
|
||||
)
|
||||
path = get_path(index)
|
||||
signature = None
|
||||
while signature is None:
|
||||
def _check_name_digest(self, name, digest):
|
||||
if name != self.name or digest != self.digest:
|
||||
click.echo(f"ERROR! Remote wants to sign {name} with digest {digest.hex()}")
|
||||
click.echo(f"Expected: {self.name} with digest {self.digest.hex()}")
|
||||
raise ValueError("Unexpected index/digest")
|
||||
|
||||
def get_commit(self, name, digest):
|
||||
self._check_name_digest(name, digest)
|
||||
click.echo("Sending commitment!")
|
||||
return self.commit
|
||||
|
||||
def _make_signature(self, global_R, global_pk):
|
||||
while True:
|
||||
try:
|
||||
t = get_trezor()
|
||||
print(
|
||||
"\n\n\nSigning hash %s with path %s:"
|
||||
% (binascii.hexlify(digest).decode(), path)
|
||||
)
|
||||
click.echo("\n\n\nSigning...")
|
||||
signature = cosi.sign(
|
||||
t, tools.parse_path(path), digest, global_R, global_pk
|
||||
TREZOR, self.address_n, self.digest, global_R, global_pk
|
||||
)
|
||||
return signature.signature
|
||||
except Exception as e:
|
||||
print(e)
|
||||
click.echo(e)
|
||||
traceback.print_exc()
|
||||
print("Trying again ...")
|
||||
sig = signature.signature
|
||||
print("Signature sent!")
|
||||
return sig
|
||||
click.echo("Trying again ...")
|
||||
|
||||
|
||||
def get_signature(self, name, digest, global_R, global_pk):
|
||||
self._check_name_digest(name, digest)
|
||||
global_params = global_R, global_pk
|
||||
if global_params != self.global_params:
|
||||
self.signature = self._make_signature(global_R, global_pk)
|
||||
self.global_params = global_params
|
||||
click.echo("Sending signature!")
|
||||
return self.signature
|
||||
|
||||
@Pyro4.oneway
|
||||
def finish(self):
|
||||
click.echo("Done! \\(^o^)/")
|
||||
self.daemon.shutdown()
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"-l", "--listen", "ipaddr", default="0.0.0.0", help="Bind to particular ip address"
|
||||
)
|
||||
@click.option("-t", "--header-type", "fw_or_type", type=click.Choice(indexmap.keys()))
|
||||
@click.option("-d", "--digest")
|
||||
@click.argument("fw_file", type=click.File("rb"), required=False)
|
||||
def cli(ipaddr, fw_file, fw_or_type, digest):
|
||||
"""Participate in signing of firmware.
|
||||
|
||||
Specify either fw_file to auto-detect type and digest, or use -t and -d to specify
|
||||
the type and digest manually.
|
||||
"""
|
||||
global TREZOR
|
||||
|
||||
public_keys = None
|
||||
if fw_file:
|
||||
if fw_or_type or digest:
|
||||
raise click.ClickException("Do not specify fw_file together with -t/-d")
|
||||
|
||||
fw_or_type = parse_image(fw_file.read())
|
||||
digest = fw_or_type.digest()
|
||||
public_keys = fw_or_type.public_keys
|
||||
|
||||
click.echo(fw_or_type.format())
|
||||
|
||||
if not fw_file and (not fw_or_type or not digest):
|
||||
raise click.ClickException("Please specify either fw_file or -t and -h")
|
||||
|
||||
try:
|
||||
TREZOR = get_default_client()
|
||||
TREZOR.ui.always_prompt = True
|
||||
except Exception as e:
|
||||
raise click.ClickException("Please connect a Trezor and retry.") from e
|
||||
|
||||
pubkey, R = make_commit(fw_or_type, digest, public_keys)
|
||||
|
||||
daemon = Pyro4.Daemon(host=ipaddr, port=PORT)
|
||||
proxy = KeyctlProxy(daemon, fw_or_type, digest, (pubkey, R))
|
||||
uri = daemon.register(proxy, "keyctl")
|
||||
click.echo(f"keyctl-proxy running at URI: {uri}")
|
||||
click.echo("Press Ctrl+C to abort.")
|
||||
daemon.requestLoop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
ipaddr = sys.argv[1]
|
||||
else:
|
||||
print("Usage: keyctl-proxy ipaddress")
|
||||
sys.exit(1)
|
||||
daemon = Pyro4.Daemon(host=ipaddr, port=PORT)
|
||||
proxy = KeyctlProxy()
|
||||
uri = daemon.register(proxy, "keyctl")
|
||||
print('keyctl-proxy running at URI: "%s"' % uri)
|
||||
daemon.requestLoop()
|
||||
cli()
|
||||
|
373
python/src/trezorlib/_internal/firmware_headers.py
Normal file
373
python/src/trezorlib/_internal/firmware_headers.py
Normal file
@ -0,0 +1,373 @@
|
||||
import struct
|
||||
from enum import Enum
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import click
|
||||
import construct as c
|
||||
import pyblake2
|
||||
|
||||
from trezorlib import cosi, firmware
|
||||
|
||||
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)
|
||||
|
||||
def is_ok(self):
|
||||
return self is Status.VALID or self is Status.DEVEL
|
||||
|
||||
|
||||
VHASH_DEVEL = bytes.fromhex(
|
||||
"c5b4d40cb76911392122c8d1c277937e49c69b2aaf818001ec5c7663fcce258f"
|
||||
)
|
||||
|
||||
|
||||
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]:
|
||||
return [k * 32 for k in key_bytes]
|
||||
|
||||
|
||||
def compute_vhash(vendor_header):
|
||||
m = vendor_header.sig_m
|
||||
n = vendor_header.sig_n
|
||||
pubkeys = vendor_header.pubkeys
|
||||
h = pyblake2.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
|
||||
) -> Optional[bool]:
|
||||
if all_zero(header.signature) and header.sigmask == 0:
|
||||
return Status.MISSING
|
||||
try:
|
||||
digest = firmware.header_digest(header)
|
||||
cosi.verify(header.signature, digest, m, pubkeys, header.sigmask)
|
||||
return Status.VALID if not is_devel else Status.DEVEL
|
||||
except Exception:
|
||||
return Status.INVALID
|
||||
|
||||
|
||||
# ====================== formatting functions ====================
|
||||
|
||||
|
||||
class LiteralStr(str):
|
||||
pass
|
||||
|
||||
|
||||
def _format_container(
|
||||
pb: c.Container,
|
||||
indent: int = 0,
|
||||
sep: str = " " * 4,
|
||||
truncate_after: Optional[int] = 64,
|
||||
truncate_to: Optional[int] = 32,
|
||||
) -> 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: Any, indent: int) -> str:
|
||||
level = sep * indent
|
||||
leadin = sep * (indent + 1)
|
||||
|
||||
if isinstance(value, LiteralStr):
|
||||
return value
|
||||
|
||||
if isinstance(value, list):
|
||||
# short list of simple values
|
||||
if not value or isinstance(value, (int, bool, Enum)):
|
||||
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, 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 "{} bytes {}{}".format(length, output, suffix)
|
||||
|
||||
if isinstance(value, Enum):
|
||||
return str(value)
|
||||
|
||||
return repr(value)
|
||||
|
||||
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 += " build {}".format(version.build)
|
||||
return version_str
|
||||
|
||||
|
||||
# =========================== functionality implementations ===============
|
||||
|
||||
|
||||
class SignableImage:
|
||||
NAME = "Unrecognized image"
|
||||
BIP32_INDEX = None
|
||||
DEV_KEYS = []
|
||||
DEV_KEY_SIGMASK = 0b11
|
||||
|
||||
def __init__(self, fw: c.Container) -> None:
|
||||
self.fw = fw
|
||||
self.header = None
|
||||
self.public_keys = None
|
||||
self.sigs_required = firmware.V2_SIGS_REQUIRED
|
||||
|
||||
def digest(self) -> bytes:
|
||||
return firmware.header_digest(self.header)
|
||||
|
||||
def check_signature(self) -> Status:
|
||||
raise NotImplementedError
|
||||
|
||||
def rehash(self) -> None:
|
||||
pass
|
||||
|
||||
def insert_signature(self, signature: bytes, sigmask: int) -> None:
|
||||
self.header.signature = signature
|
||||
self.header.sigmask = sigmask
|
||||
|
||||
def dump(self) -> bytes:
|
||||
return AnyFirmware.build(self.fw)
|
||||
|
||||
def format(self, verbose: bool) -> str:
|
||||
return _format_container(self.fw)
|
||||
|
||||
|
||||
class VendorHeader(SignableImage):
|
||||
NAME = "vendorheader"
|
||||
BIP32_INDEX = 1
|
||||
DEV_KEYS = _make_dev_keys(b"\x44", b"\x45")
|
||||
|
||||
def __init__(self, fw):
|
||||
super().__init__(fw)
|
||||
self.header = fw.vendor_header
|
||||
self.public_keys = firmware.V2_BOOTLOADER_KEYS
|
||||
|
||||
def check_signature(self) -> Status:
|
||||
return _check_signature_any(
|
||||
self.header, self.sigs_required, self.public_keys, False
|
||||
)
|
||||
|
||||
def _format(self, terse: bool) -> str:
|
||||
vh = self.fw.vendor_header
|
||||
if not terse:
|
||||
vhash = compute_vhash(vh)
|
||||
output = [
|
||||
"Vendor Header " + _format_container(vh),
|
||||
"Pubkey bundle hash: {}".format(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,
|
||||
),
|
||||
]
|
||||
|
||||
fingerprint = firmware.header_digest(vh)
|
||||
|
||||
if not terse:
|
||||
output.append(
|
||||
"Fingerprint: {}".format(click.style(fingerprint.hex(), bold=True))
|
||||
)
|
||||
|
||||
sig_status = self.check_signature()
|
||||
sym = SYM_OK if sig_status.is_ok() else SYM_FAIL
|
||||
output.append("{} Signature is {}".format(sym, sig_status.value))
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
def format(self, verbose: bool = False) -> str:
|
||||
return self._format(terse=False)
|
||||
|
||||
|
||||
class BinImage(SignableImage):
|
||||
def __init__(self, fw):
|
||||
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
|
||||
)
|
||||
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 digest(self) -> bytes:
|
||||
return firmware.header_digest(self.digest_header)
|
||||
|
||||
def rehash(self):
|
||||
self.header.hashes = self.code_hashes
|
||||
|
||||
def format(self, verbose: bool = False) -> str:
|
||||
header_out = self.header.copy()
|
||||
|
||||
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]))
|
||||
|
||||
all_ok = SYM_OK
|
||||
hash_status = Status.VALID
|
||||
sig_status = Status.VALID
|
||||
|
||||
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("{} {}".format(status, expected.hex())))
|
||||
|
||||
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
|
||||
else:
|
||||
hash_status = Status.VALID
|
||||
|
||||
header_out["hashes"] = hashes_out
|
||||
|
||||
sig_status = self.check_signature()
|
||||
all_ok = SYM_OK if hash_status.is_ok() and sig_status.is_ok() else SYM_FAIL
|
||||
|
||||
output = [
|
||||
"Firmware Header " + _format_container(header_out),
|
||||
"Fingerprint: {}".format(click.style(self.digest().hex(), bold=True)),
|
||||
"{} Signature is {}, hashes are {}".format(
|
||||
all_ok, sig_status.value, hash_status.value
|
||||
),
|
||||
]
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
class FirmwareImage(BinImage):
|
||||
NAME = "firmware"
|
||||
BIP32_INDEX = 2
|
||||
DEV_KEYS = _make_dev_keys(b"\x47", b"\x48")
|
||||
|
||||
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 format(self, verbose: bool = False) -> str:
|
||||
return (
|
||||
VendorHeader(self.fw)._format(terse=not verbose)
|
||||
+ "\n"
|
||||
+ super().format(verbose)
|
||||
)
|
||||
|
||||
|
||||
class BootloaderImage(BinImage):
|
||||
NAME = "bootloader"
|
||||
BIP32_INDEX = 0
|
||||
DEV_KEYS = _make_dev_keys(b"\x41", b"\x42")
|
||||
|
||||
def __init__(self, fw):
|
||||
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):
|
||||
# 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):
|
||||
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")
|
@ -36,18 +36,18 @@ def validate_firmware(version, fw, expected_fingerprint=None):
|
||||
if version == firmware.FirmwareFormat.TREZOR_ONE:
|
||||
if fw.embedded_onev2:
|
||||
click.echo("Trezor One firmware with embedded v2 image (1.8.0 or later)")
|
||||
_print_version(fw.embedded_onev2.firmware_header.version)
|
||||
_print_version(fw.embedded_onev2.header.version)
|
||||
else:
|
||||
click.echo("Trezor One firmware image.")
|
||||
elif version == firmware.FirmwareFormat.TREZOR_ONE_V2:
|
||||
click.echo("Trezor One v2 firmware (1.8.0 or later)")
|
||||
_print_version(fw.firmware_header.version)
|
||||
_print_version(fw.header.version)
|
||||
elif version == firmware.FirmwareFormat.TREZOR_T:
|
||||
click.echo("Trezor T firmware image.")
|
||||
vendor = fw.vendor_header.vendor_string
|
||||
vendor = fw.vendor_header.text
|
||||
vendor_version = "{major}.{minor}".format(**fw.vendor_header.version)
|
||||
click.echo("Vendor header from {}, version {}".format(vendor, vendor_version))
|
||||
_print_version(fw.firmware_header.version)
|
||||
_print_version(fw.image.header.version)
|
||||
|
||||
try:
|
||||
firmware.validate(version, fw, allow_unsigned=False)
|
||||
@ -198,17 +198,18 @@ def firmware_update(
|
||||
click.echo("Please switch your device to bootloader mode.")
|
||||
sys.exit(1)
|
||||
|
||||
# bootloader for T1 does not export 'model', so we rely on major_version
|
||||
f = client.features
|
||||
bootloader_onev2 = f.major_version == 1 and f.minor_version >= 8
|
||||
bootloader_version = (f.major_version, f.minor_version, f.patch_version)
|
||||
bootloader_onev2 = f.major_version == 1 and bootloader_version >= (1, 8, 0)
|
||||
|
||||
if filename:
|
||||
data = open(filename, "rb").read()
|
||||
else:
|
||||
if not url:
|
||||
bootloader_version = [f.major_version, f.minor_version, f.patch_version]
|
||||
version_list = [int(x) for x in version.split(".")] if version else None
|
||||
url, fp = find_best_firmware_version(
|
||||
bootloader_version, version_list, beta, bitcoin_only
|
||||
list(bootloader_version), version_list, beta, bitcoin_only
|
||||
)
|
||||
if not fingerprint:
|
||||
fingerprint = fp
|
||||
|
@ -15,6 +15,7 @@
|
||||
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from types import SimpleNamespace
|
||||
@ -73,13 +74,16 @@ def get_default_client(path=None, ui=None, **kwargs):
|
||||
|
||||
Returns a TrezorClient instance with minimum fuss.
|
||||
|
||||
If no path is specified, finds first connected Trezor. Otherwise performs
|
||||
a prefix-search for the specified device. If no UI is supplied, instantiates
|
||||
the default CLI UI.
|
||||
If path is specified, does a prefix-search for the specified device. Otherwise, uses
|
||||
the value of TREZOR_PATH env variable, or finds first connected Trezor.
|
||||
If no UI is supplied, instantiates the default CLI UI.
|
||||
"""
|
||||
from .transport import get_transport
|
||||
from .ui import ClickUI
|
||||
|
||||
if path is None:
|
||||
path = os.getenv("TREZOR_PATH")
|
||||
|
||||
transport = get_transport(path, prefix_search=True)
|
||||
if ui is None:
|
||||
ui = ClickUI()
|
||||
|
@ -67,31 +67,45 @@ def get_nonce(
|
||||
return r, Ed25519PublicPoint(_ed25519.encodepoint(R))
|
||||
|
||||
|
||||
def verify(
|
||||
def verify_combined(
|
||||
signature: Ed25519Signature, digest: bytes, pub_key: Ed25519PublicPoint
|
||||
) -> None:
|
||||
"""Verify Ed25519 signature. Raise exception if the signature is invalid."""
|
||||
"""Verify Ed25519 signature. Raise exception if the signature is invalid.
|
||||
|
||||
A CoSi combined signature is equivalent to a plain Ed25519 signature with a public
|
||||
key that is a combination of the cosigners' public keys. This function takes the
|
||||
combined public key and performs simple Ed25519 verification.
|
||||
"""
|
||||
# XXX this *might* change to bool function
|
||||
_ed25519.checkvalid(signature, digest, pub_key)
|
||||
|
||||
|
||||
def verify_m_of_n(
|
||||
def verify(
|
||||
signature: Ed25519Signature,
|
||||
digest: bytes,
|
||||
m: int,
|
||||
n: int,
|
||||
mask: int,
|
||||
sigs_required: int,
|
||||
keys: List[Ed25519PublicPoint],
|
||||
mask: int,
|
||||
) -> None:
|
||||
if m < 1:
|
||||
raise ValueError("At least 1 signer must be specified")
|
||||
selected_keys = [keys[i] for i in range(n) if mask & (1 << i)]
|
||||
if len(selected_keys) < m:
|
||||
raise ValueError(
|
||||
"Not enough signers ({} required, {} found)".format(m, len(selected_keys))
|
||||
)
|
||||
"""Verify a CoSi multi-signature. Raise exception if the signature is invalid.
|
||||
|
||||
This function verifies a M-of-N signature scheme. The arguments are:
|
||||
- the minimum number M of signatures required
|
||||
- public keys of all N possible cosigners
|
||||
- a bitmask specifying which of the N cosigners have produced the signature.
|
||||
|
||||
The verification checks that the mask specifies at least M cosigners, then combines
|
||||
the selected public keys and verifies the signature against the combined key.
|
||||
"""
|
||||
if sigs_required < 1:
|
||||
raise ValueError("At least one signer must be specified.")
|
||||
if mask.bit_length() > len(keys):
|
||||
raise ValueError("Sigmask specifies more public keys than provided.")
|
||||
selected_keys = [key for i, key in enumerate(keys) if mask & (1 << i)]
|
||||
if len(selected_keys) < sigs_required:
|
||||
raise _ed25519.SignatureMismatch("Insufficient number of signatures.")
|
||||
global_pk = combine_keys(selected_keys)
|
||||
return verify(signature, digest, global_pk)
|
||||
return verify_combined(signature, digest, global_pk)
|
||||
|
||||
|
||||
def pubkey_from_privkey(privkey: Ed25519PrivateKey) -> Ed25519PublicPoint:
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import hashlib
|
||||
from enum import Enum
|
||||
from typing import Callable, List, NewType, Tuple
|
||||
from typing import Callable, List, Tuple
|
||||
|
||||
import construct as c
|
||||
import ecdsa
|
||||
@ -30,21 +30,45 @@ except ImportError:
|
||||
|
||||
|
||||
V1_SIGNATURE_SLOTS = 3
|
||||
V1_BOOTLOADER_KEYS = {
|
||||
1: "04d571b7f148c5e4232c3814f777d8faeaf1a84216c78d569b71041ffc768a5b2d810fc3bb134dd026b57e65005275aedef43e155f48fc11a32ec790a93312bd58",
|
||||
2: "0463279c0c0866e50c05c799d32bd6bab0188b6de06536d1109d2ed9ce76cb335c490e55aee10cc901215132e853097d5432eda06b792073bd7740c94ce4516cb1",
|
||||
3: "0443aedbb6f7e71c563f8ed2ef64ec9981482519e7ef4f4aa98b27854e8c49126d4956d300ab45fdc34cd26bc8710de0a31dbdf6de7435fd0b492be70ac75fde58",
|
||||
4: "04877c39fd7c62237e038235e9c075dab261630f78eeb8edb92487159fffedfdf6046c6f8b881fa407c4a4ce6c28de0b19c1f4e29f1fcbc5a58ffd1432a3e0938a",
|
||||
5: "047384c51ae81add0a523adbb186c91b906ffb64c2c765802bf26dbd13bdf12c319e80c2213a136c8ee03d7874fd22b70d68e7dee469decfbbb510ee9a460cda45",
|
||||
}
|
||||
V1_BOOTLOADER_KEYS = [
|
||||
bytes.fromhex(key)
|
||||
for key in (
|
||||
"04d571b7f148c5e4232c3814f777d8faeaf1a84216c78d569b71041ffc768a5b2d810fc3bb134dd026b57e65005275aedef43e155f48fc11a32ec790a93312bd58",
|
||||
"0463279c0c0866e50c05c799d32bd6bab0188b6de06536d1109d2ed9ce76cb335c490e55aee10cc901215132e853097d5432eda06b792073bd7740c94ce4516cb1",
|
||||
"0443aedbb6f7e71c563f8ed2ef64ec9981482519e7ef4f4aa98b27854e8c49126d4956d300ab45fdc34cd26bc8710de0a31dbdf6de7435fd0b492be70ac75fde58",
|
||||
"04877c39fd7c62237e038235e9c075dab261630f78eeb8edb92487159fffedfdf6046c6f8b881fa407c4a4ce6c28de0b19c1f4e29f1fcbc5a58ffd1432a3e0938a",
|
||||
"047384c51ae81add0a523adbb186c91b906ffb64c2c765802bf26dbd13bdf12c319e80c2213a136c8ee03d7874fd22b70d68e7dee469decfbbb510ee9a460cda45",
|
||||
)
|
||||
]
|
||||
|
||||
V2_BOARDLOADER_KEYS = [
|
||||
bytes.fromhex(key)
|
||||
for key in (
|
||||
"0eb9856be9ba7e972c7f34eac1ed9b6fd0efd172ec00faf0c589759da4ddfba0",
|
||||
"ac8ab40b32c98655798fd5da5e192be27a22306ea05c6d277cdff4a3f4125cd8",
|
||||
"ce0fcd12543ef5936cf2804982136707863d17295faced72af171d6e6513ff06",
|
||||
)
|
||||
]
|
||||
|
||||
V2_BOARDLOADER_DEV_KEYS = [
|
||||
bytes.fromhex(key)
|
||||
for key in (
|
||||
"db995fe25169d141cab9bbba92baa01f9f2e1ece7df4cb2ac05190f37fcc1f9d",
|
||||
"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12",
|
||||
"22fc297792f0b6ffc0bfcfdb7edb0c0aa14e025a365ec0e342e86e3829cb74b6",
|
||||
)
|
||||
]
|
||||
|
||||
V2_BOOTLOADER_KEYS = [
|
||||
bytes.fromhex("c2c87a49c5a3460977fbb2ec9dfe60f06bd694db8244bd4981fe3b7a26307f3f"),
|
||||
bytes.fromhex("80d036b08739b846f4cb77593078deb25dc9487aedcf52e30b4fb7cd7024178a"),
|
||||
bytes.fromhex("b8307a71f552c60a4cbb317ff48b82cdbf6b6bb5f04c920fec7badf017883751"),
|
||||
bytes.fromhex(key)
|
||||
for key in (
|
||||
"c2c87a49c5a3460977fbb2ec9dfe60f06bd694db8244bd4981fe3b7a26307f3f",
|
||||
"80d036b08739b846f4cb77593078deb25dc9487aedcf52e30b4fb7cd7024178a",
|
||||
"b8307a71f552c60a4cbb317ff48b82cdbf6b6bb5f04c920fec7badf017883751",
|
||||
)
|
||||
]
|
||||
V2_BOOTLOADER_M = 2
|
||||
V2_BOOTLOADER_N = 3
|
||||
|
||||
V2_SIGS_REQUIRED = 2
|
||||
|
||||
ONEV2_CHUNK_SIZE = 1024 * 64
|
||||
V2_CHUNK_SIZE = 1024 * 128
|
||||
@ -80,6 +104,11 @@ class ToifMode(Enum):
|
||||
grayscale = b"g"
|
||||
|
||||
|
||||
class HeaderType(Enum):
|
||||
FIRMWARE = b"TRZF"
|
||||
BOOTLOADER = b"TRZB"
|
||||
|
||||
|
||||
class EnumAdapter(c.Adapter):
|
||||
def __init__(self, subcon, enum):
|
||||
self.enum = enum
|
||||
@ -106,7 +135,7 @@ Toif = c.Struct(
|
||||
|
||||
|
||||
VendorTrust = c.Transformed(c.BitStruct(
|
||||
"reserved" / c.Default(c.BitsInteger(9), 0),
|
||||
"_reserved" / c.Default(c.BitsInteger(9), 0),
|
||||
"show_vendor_string" / c.Flag,
|
||||
"require_user_click" / c.Flag,
|
||||
"red_background" / c.Flag,
|
||||
@ -123,13 +152,13 @@ VendorHeader = c.Struct(
|
||||
"major" / c.Int8ul,
|
||||
"minor" / c.Int8ul,
|
||||
),
|
||||
"vendor_sigs_required" / c.Int8ul,
|
||||
"vendor_sigs_n" / c.Rebuild(c.Int8ul, c.len_(c.this.pubkeys)),
|
||||
"vendor_trust" / VendorTrust,
|
||||
"reserved" / c.Padding(14),
|
||||
"pubkeys" / c.Bytes(32)[c.this.vendor_sigs_n],
|
||||
"vendor_string" / c.Aligned(4, c.PascalString(c.Int8ul, "utf-8")),
|
||||
"vendor_image" / Toif,
|
||||
"sig_m" / c.Int8ul,
|
||||
"sig_n" / c.Rebuild(c.Int8ul, c.len_(c.this.pubkeys)),
|
||||
"trust" / VendorTrust,
|
||||
"_reserved" / c.Padding(14),
|
||||
"pubkeys" / c.Bytes(32)[c.this.sig_n],
|
||||
"text" / c.Aligned(4, c.PascalString(c.Int8ul, "utf-8")),
|
||||
"image" / Toif,
|
||||
"_data_end_offset" / c.Tell,
|
||||
|
||||
c.Padding(-(c.this._data_end_offset + 65) % 512),
|
||||
@ -154,7 +183,7 @@ VersionLong = c.Struct(
|
||||
|
||||
FirmwareHeader = c.Struct(
|
||||
"_start_offset" / c.Tell,
|
||||
"magic" / c.Const(b"TRZF"),
|
||||
"magic" / EnumAdapter(c.Bytes(4), HeaderType),
|
||||
"header_len" / c.Int32ul,
|
||||
"expiry" / c.Int32ul,
|
||||
"code_length" / c.Rebuild(
|
||||
@ -165,13 +194,13 @@ FirmwareHeader = c.Struct(
|
||||
),
|
||||
"version" / VersionLong,
|
||||
"fix_version" / VersionLong,
|
||||
"reserved" / c.Padding(8),
|
||||
"_reserved" / c.Padding(8),
|
||||
"hashes" / c.Bytes(32)[16],
|
||||
|
||||
"v1_signatures" / c.Bytes(64)[V1_SIGNATURE_SLOTS],
|
||||
"v1_key_indexes" / c.Int8ul[V1_SIGNATURE_SLOTS], # pylint: disable=E1136
|
||||
|
||||
"reserved" / c.Padding(220),
|
||||
"_reserved" / c.Padding(220),
|
||||
"sigmask" / c.Byte,
|
||||
"signature" / c.Bytes(64),
|
||||
|
||||
@ -187,24 +216,37 @@ FirmwareHeader = c.Struct(
|
||||
)
|
||||
|
||||
|
||||
Firmware = c.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."""
|
||||
FirmwareImage = c.Struct(
|
||||
"header" / FirmwareHeader,
|
||||
"_code_offset" / c.Tell,
|
||||
"code" / c.Bytes(c.this.header.code_length),
|
||||
c.Terminated,
|
||||
)
|
||||
|
||||
|
||||
"""Firmware image prefixed by a vendor header.
|
||||
|
||||
This is the expected format of firmware binaries for Trezor T."""
|
||||
VendorFirmware = c.Struct(
|
||||
"vendor_header" / VendorHeader,
|
||||
"firmware_header" / FirmwareHeader,
|
||||
"_code_offset" / c.Tell,
|
||||
"code" / c.Bytes(c.this.firmware_header.code_length),
|
||||
"image" / FirmwareImage,
|
||||
c.Terminated,
|
||||
)
|
||||
|
||||
|
||||
FirmwareOneV2 = c.Struct(
|
||||
"firmware_header" / FirmwareHeader,
|
||||
"_code_offset" / c.Tell,
|
||||
"code" / c.Bytes(c.this.firmware_header.code_length),
|
||||
c.Terminated,
|
||||
)
|
||||
"""Legacy firmware image.
|
||||
Consists of a custom header and code block.
|
||||
This is the expected format of firmware binaries for Trezor One pre-1.8.0.
|
||||
|
||||
|
||||
FirmwareOne = c.Struct(
|
||||
The code block can optionally be interpreted as a new-style firmware image. That is the
|
||||
expected format of firmware binary for Trezor One version 1.8.0, which can be installed
|
||||
by both the older and the newer bootloader."""
|
||||
LegacyFirmware = c.Struct(
|
||||
"magic" / c.Const(b"TRZR"),
|
||||
"code_length" / c.Rebuild(c.Int32ul, c.len_(c.this.code)),
|
||||
"key_indexes" / c.Int8ul[V1_SIGNATURE_SLOTS], # pylint: disable=E1136
|
||||
@ -212,12 +254,12 @@ FirmwareOne = c.Struct(
|
||||
c.Padding(7),
|
||||
"restore_storage" / c.Flag,
|
||||
),
|
||||
"reserved" / c.Padding(52),
|
||||
"_reserved" / c.Padding(52),
|
||||
"signatures" / c.Bytes(64)[V1_SIGNATURE_SLOTS],
|
||||
"code" / c.Bytes(c.this.code_length),
|
||||
c.Terminated,
|
||||
|
||||
"embedded_onev2" / c.RestreamData(c.this.code, c.Optional(FirmwareOneV2)),
|
||||
"embedded_onev2" / c.RestreamData(c.this.code, c.Optional(FirmwareImage)),
|
||||
)
|
||||
|
||||
# fmt: on
|
||||
@ -229,20 +271,19 @@ class FirmwareFormat(Enum):
|
||||
TREZOR_ONE_V2 = 3
|
||||
|
||||
|
||||
FirmwareType = NewType("FirmwareType", c.Container)
|
||||
ParsedFirmware = Tuple[FirmwareFormat, FirmwareType]
|
||||
ParsedFirmware = Tuple[FirmwareFormat, c.Container]
|
||||
|
||||
|
||||
def parse(data: bytes) -> ParsedFirmware:
|
||||
if data[:4] == b"TRZR":
|
||||
version = FirmwareFormat.TREZOR_ONE
|
||||
cls = FirmwareOne
|
||||
cls = LegacyFirmware
|
||||
elif data[:4] == b"TRZV":
|
||||
version = FirmwareFormat.TREZOR_T
|
||||
cls = Firmware
|
||||
cls = VendorFirmware
|
||||
elif data[:4] == b"TRZF":
|
||||
version = FirmwareFormat.TREZOR_ONE_V2
|
||||
cls = FirmwareOneV2
|
||||
cls = FirmwareImage
|
||||
else:
|
||||
raise ValueError("Unrecognized firmware image type")
|
||||
|
||||
@ -250,10 +291,10 @@ def parse(data: bytes) -> ParsedFirmware:
|
||||
fw = cls.parse(data)
|
||||
except Exception as e:
|
||||
raise FirmwareIntegrityError("Invalid firmware image") from e
|
||||
return version, FirmwareType(fw)
|
||||
return version, fw
|
||||
|
||||
|
||||
def digest_onev1(fw: FirmwareType) -> bytes:
|
||||
def digest_onev1(fw: c.Container) -> bytes:
|
||||
return hashlib.sha256(fw.code).digest()
|
||||
|
||||
|
||||
@ -272,14 +313,14 @@ def check_sig_v1(
|
||||
)
|
||||
|
||||
for i in range(len(key_indexes)):
|
||||
key_idx = key_indexes[i]
|
||||
key_idx = key_indexes[i] - 1
|
||||
signature = signatures[i]
|
||||
|
||||
if key_idx not in V1_BOOTLOADER_KEYS:
|
||||
if key_idx >= len(V1_BOOTLOADER_KEYS):
|
||||
# unknown pubkey
|
||||
raise InvalidSignatureError("Unknown key in slot {}".format(i))
|
||||
|
||||
pubkey = bytes.fromhex(V1_BOOTLOADER_KEYS[key_idx])[1:]
|
||||
pubkey = V1_BOOTLOADER_KEYS[key_idx][1:]
|
||||
verify = ecdsa.VerifyingKey.from_string(pubkey, curve=ecdsa.curves.SECP256k1)
|
||||
try:
|
||||
verify.verify_digest(signature, digest)
|
||||
@ -287,72 +328,88 @@ def check_sig_v1(
|
||||
raise InvalidSignatureError("Invalid signature in slot {}".format(i)) from e
|
||||
|
||||
|
||||
def _header_digest(
|
||||
header: c.Container, header_type: c.Construct, hash_function: Callable = blake2s
|
||||
) -> bytes:
|
||||
def header_digest(header: c.Container, hash_function: Callable = blake2s) -> bytes:
|
||||
stripped_header = header.copy()
|
||||
stripped_header.sigmask = 0
|
||||
stripped_header.signature = b"\0" * 64
|
||||
stripped_header.v1_key_indexes = [0, 0, 0]
|
||||
stripped_header.v1_signatures = [b"\0" * 64] * 3
|
||||
if header.magic == b"TRZV":
|
||||
header_type = VendorHeader
|
||||
else:
|
||||
header_type = FirmwareHeader
|
||||
header_bytes = header_type.build(stripped_header)
|
||||
return hash_function(header_bytes).digest()
|
||||
|
||||
|
||||
def digest_v2(fw: FirmwareType) -> bytes:
|
||||
return _header_digest(fw.firmware_header, FirmwareHeader, blake2s)
|
||||
def digest_v2(fw: c.Container) -> bytes:
|
||||
return header_digest(fw.image.header, blake2s)
|
||||
|
||||
|
||||
def digest_onev2(fw: FirmwareType) -> bytes:
|
||||
return _header_digest(fw.firmware_header, FirmwareHeader, hashlib.sha256)
|
||||
def digest_onev2(fw: c.Container) -> bytes:
|
||||
return header_digest(fw.header, hashlib.sha256)
|
||||
|
||||
|
||||
def validate_code_hashes(
|
||||
fw: FirmwareType,
|
||||
def calculate_code_hashes(
|
||||
code: bytes,
|
||||
code_offset: int,
|
||||
hash_function: Callable = blake2s,
|
||||
chunk_size: int = V2_CHUNK_SIZE,
|
||||
padding_byte: bytes = None,
|
||||
) -> None:
|
||||
for i, expected_hash in enumerate(fw.firmware_header.hashes):
|
||||
if i == 0:
|
||||
# Because first chunk is sent along with headers, there is less code in it.
|
||||
chunk = fw.code[: chunk_size - fw._code_offset]
|
||||
else:
|
||||
# Subsequent chunks are shifted by the "missing header" size.
|
||||
ptr = i * chunk_size - fw._code_offset
|
||||
chunk = fw.code[ptr : ptr + chunk_size]
|
||||
|
||||
# padding for last chunk
|
||||
if padding_byte is not None and i > 1 and chunk and len(chunk) < chunk_size:
|
||||
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) * chunk_size - code_offset for i in range(16)]
|
||||
start = 0
|
||||
for end in ends:
|
||||
chunk = code[start:end]
|
||||
# padding for last non-empty chunk
|
||||
if padding_byte is not None and start < len(code) and end > len(code):
|
||||
chunk += padding_byte[0:1] * (chunk_size - len(chunk))
|
||||
|
||||
if not chunk and expected_hash == b"\0" * 32:
|
||||
continue
|
||||
chunk_hash = hash_function(chunk).digest()
|
||||
if chunk_hash != expected_hash:
|
||||
if not chunk:
|
||||
hashes.append(b"\0" * 32)
|
||||
else:
|
||||
hashes.append(hash_function(chunk).digest())
|
||||
|
||||
start = end
|
||||
|
||||
return hashes
|
||||
|
||||
|
||||
def validate_code_hashes(fw: c.Container, version: FirmwareFormat) -> None:
|
||||
if version == FirmwareFormat.TREZOR_ONE_V2:
|
||||
image = fw
|
||||
hash_function = hashlib.sha256
|
||||
chunk_size = ONEV2_CHUNK_SIZE
|
||||
padding_byte = b"\xff"
|
||||
else:
|
||||
image = fw.image
|
||||
hash_function = blake2s
|
||||
chunk_size = V2_CHUNK_SIZE
|
||||
padding_byte = None
|
||||
|
||||
expected_hashes = calculate_code_hashes(
|
||||
image.code, image._code_offset, hash_function, chunk_size, padding_byte
|
||||
)
|
||||
if expected_hashes != image.header.hashes:
|
||||
raise FirmwareIntegrityError("Invalid firmware data.")
|
||||
|
||||
|
||||
def validate_onev2(fw: FirmwareType, allow_unsigned: bool = False) -> None:
|
||||
def validate_onev2(fw: c.Container, allow_unsigned: bool = False) -> None:
|
||||
try:
|
||||
check_sig_v1(
|
||||
digest_onev2(fw),
|
||||
fw.firmware_header.v1_key_indexes,
|
||||
fw.firmware_header.v1_signatures,
|
||||
digest_onev2(fw), fw.header.v1_key_indexes, fw.header.v1_signatures,
|
||||
)
|
||||
except Unsigned:
|
||||
if not allow_unsigned:
|
||||
raise
|
||||
|
||||
validate_code_hashes(
|
||||
fw,
|
||||
hash_function=hashlib.sha256,
|
||||
chunk_size=ONEV2_CHUNK_SIZE,
|
||||
padding_byte=b"\xFF",
|
||||
)
|
||||
validate_code_hashes(fw, FirmwareFormat.TREZOR_ONE_V2)
|
||||
|
||||
|
||||
def validate_onev1(fw: FirmwareType, allow_unsigned: bool = False) -> None:
|
||||
def validate_onev1(fw: c.Container, allow_unsigned: bool = False) -> None:
|
||||
try:
|
||||
check_sig_v1(digest_onev1(fw), fw.key_indexes, fw.signatures)
|
||||
except Unsigned:
|
||||
@ -362,21 +419,20 @@ def validate_onev1(fw: FirmwareType, allow_unsigned: bool = False) -> None:
|
||||
validate_onev2(fw.embedded_onev2, allow_unsigned)
|
||||
|
||||
|
||||
def validate_v2(fw: FirmwareType, skip_vendor_header: bool = False) -> None:
|
||||
vendor_fingerprint = _header_digest(fw.vendor_header, VendorHeader)
|
||||
def validate_v2(fw: c.Container, skip_vendor_header: bool = False) -> None:
|
||||
vendor_fingerprint = header_digest(fw.vendor_header)
|
||||
fingerprint = digest_v2(fw)
|
||||
|
||||
if not skip_vendor_header:
|
||||
try:
|
||||
# if you want to validate a custom vendor header, you can modify
|
||||
# the global variables to match your keys and m-of-n scheme
|
||||
cosi.verify_m_of_n(
|
||||
cosi.verify(
|
||||
fw.vendor_header.signature,
|
||||
vendor_fingerprint,
|
||||
V2_BOOTLOADER_M,
|
||||
V2_BOOTLOADER_N,
|
||||
fw.vendor_header.sigmask,
|
||||
V2_SIGS_REQUIRED,
|
||||
V2_BOOTLOADER_KEYS,
|
||||
fw.vendor_header.sigmask,
|
||||
)
|
||||
except Exception:
|
||||
raise InvalidSignatureError("Invalid vendor header signature.")
|
||||
@ -387,24 +443,23 @@ def validate_v2(fw: FirmwareType, skip_vendor_header: bool = False) -> None:
|
||||
# raise ValueError("Vendor header expired.")
|
||||
|
||||
try:
|
||||
cosi.verify_m_of_n(
|
||||
fw.firmware_header.signature,
|
||||
cosi.verify(
|
||||
fw.image.header.signature,
|
||||
fingerprint,
|
||||
fw.vendor_header.vendor_sigs_required,
|
||||
fw.vendor_header.vendor_sigs_n,
|
||||
fw.firmware_header.sigmask,
|
||||
fw.vendor_header.sig_m,
|
||||
fw.vendor_header.pubkeys,
|
||||
fw.image.header.sigmask,
|
||||
)
|
||||
except Exception:
|
||||
raise InvalidSignatureError("Invalid firmware signature.")
|
||||
|
||||
# XXX expiry is not used now
|
||||
# if time.gmtime(fw.firmware_header.expiry) < now:
|
||||
# if time.gmtime(fw.image.header.expiry) < now:
|
||||
# raise ValueError("Firmware header expired.")
|
||||
validate_code_hashes(fw)
|
||||
validate_code_hashes(fw, FirmwareFormat.TREZOR_T)
|
||||
|
||||
|
||||
def digest(version: FirmwareFormat, fw: FirmwareType) -> bytes:
|
||||
def digest(version: FirmwareFormat, fw: c.Container) -> bytes:
|
||||
if version == FirmwareFormat.TREZOR_ONE:
|
||||
return digest_onev1(fw)
|
||||
elif version == FirmwareFormat.TREZOR_ONE_V2:
|
||||
@ -416,7 +471,7 @@ def digest(version: FirmwareFormat, fw: FirmwareType) -> bytes:
|
||||
|
||||
|
||||
def validate(
|
||||
version: FirmwareFormat, fw: FirmwareType, allow_unsigned: bool = False
|
||||
version: FirmwareFormat, fw: c.Container, allow_unsigned: bool = False
|
||||
) -> None:
|
||||
if version == FirmwareFormat.TREZOR_ONE:
|
||||
return validate_onev1(fw, allow_unsigned)
|
||||
|
@ -104,13 +104,13 @@ def test_single_eddsa_vector(privkey, pubkey, message, signature):
|
||||
my_pubkey = cosi.pubkey_from_privkey(privkey)
|
||||
assert my_pubkey == pubkey
|
||||
try:
|
||||
cosi.verify(signature, message, pubkey)
|
||||
cosi.verify_combined(signature, message, pubkey)
|
||||
except ValueError:
|
||||
pytest.fail("Signature does not verify.")
|
||||
|
||||
fake_signature = signature[:37] + b"\xf0" + signature[38:]
|
||||
with pytest.raises(_ed25519.SignatureMismatch):
|
||||
cosi.verify(fake_signature, message, pubkey)
|
||||
cosi.verify_combined(fake_signature, message, pubkey)
|
||||
|
||||
|
||||
def test_combine_keys():
|
||||
@ -148,7 +148,7 @@ def test_cosi_combination(keyset):
|
||||
global_sig = cosi.combine_sig(global_commit, signatures)
|
||||
|
||||
try:
|
||||
cosi.verify(global_sig, message, global_pk)
|
||||
cosi.verify_combined(global_sig, message, global_pk)
|
||||
except Exception:
|
||||
pytest.fail("Failed to validate global signature")
|
||||
|
||||
@ -175,25 +175,27 @@ def test_m_of_n():
|
||||
|
||||
try:
|
||||
# this is what we are actually doing
|
||||
cosi.verify_m_of_n(global_sig, message, 3, 4, sigmask, pubkeys)
|
||||
cosi.verify(global_sig, message, 3, pubkeys, sigmask)
|
||||
# we can require less signers too
|
||||
cosi.verify_m_of_n(global_sig, message, 1, 4, sigmask, pubkeys)
|
||||
cosi.verify(global_sig, message, 1, pubkeys, sigmask)
|
||||
except Exception:
|
||||
pytest.fail("Failed to validate by sigmask")
|
||||
|
||||
# and now for various ways that should fail
|
||||
with pytest.raises(ValueError) as e:
|
||||
cosi.verify_m_of_n(global_sig, message, 4, 4, sigmask, pubkeys)
|
||||
assert "Not enough signers" in e.value.args[0]
|
||||
cosi.verify(global_sig, message, 3, pubkeys[:2], sigmask)
|
||||
assert "more public keys than provided" in e.value.args[0]
|
||||
|
||||
with pytest.raises(_ed25519.SignatureMismatch):
|
||||
# when N < number of possible signers, the topmost signers will be ignored
|
||||
cosi.verify_m_of_n(global_sig, message, 2, 3, sigmask, pubkeys)
|
||||
with pytest.raises(ValueError) as e:
|
||||
cosi.verify(global_sig, message, 0, pubkeys, 0)
|
||||
assert "At least one signer" in e.value.args[0]
|
||||
|
||||
with pytest.raises(_ed25519.SignatureMismatch):
|
||||
with pytest.raises(_ed25519.SignatureMismatch) as e:
|
||||
# at least 5 signatures required
|
||||
cosi.verify(global_sig, message, 5, pubkeys, sigmask)
|
||||
assert "Insufficient number of signatures" in e.value.args[0]
|
||||
|
||||
with pytest.raises(_ed25519.SignatureMismatch) as e:
|
||||
# wrong sigmask
|
||||
cosi.verify_m_of_n(global_sig, message, 1, 4, 5, pubkeys)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
# can't use "0 of N" scheme
|
||||
cosi.verify_m_of_n(global_sig, message, 0, 4, sigmask, pubkeys)
|
||||
cosi.verify(global_sig, message, 3, pubkeys, 7)
|
||||
assert "signature does not pass verification" in e.value.args[0]
|
||||
|
@ -73,7 +73,7 @@ class TestCosi:
|
||||
global_R, [sig0.signature, sig1.signature, sig2.signature]
|
||||
)
|
||||
|
||||
cosi.verify(sig, digest, global_pk)
|
||||
cosi.verify_combined(sig, digest, global_pk)
|
||||
|
||||
def test_cosi_compat(self, client):
|
||||
digest = sha256(b"this is not a pipe").digest()
|
||||
@ -94,4 +94,4 @@ class TestCosi:
|
||||
)
|
||||
sig = cosi.combine_sig(global_R, [remote_sig.signature, local_sig])
|
||||
|
||||
cosi.verify(sig, digest, global_pk)
|
||||
cosi.verify_combined(sig, digest, global_pk)
|
||||
|
Loading…
Reference in New Issue
Block a user