From fb2c57d3c9b50ed6fa522dd4fbc2386b531fce86 Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 18 Dec 2019 13:12:07 +0100 Subject: [PATCH 01/26] python/firmware: improve handling of bootloader keys --- python/src/trezorlib/firmware.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/python/src/trezorlib/firmware.py b/python/src/trezorlib/firmware.py index 36ed6be399..c8abe5c59a 100644 --- a/python/src/trezorlib/firmware.py +++ b/python/src/trezorlib/firmware.py @@ -30,18 +30,24 @@ 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_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 @@ -272,14 +278,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: # 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) From 7e6b39cd8ef1cdad87acd67e414c5eecb9766353 Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 18 Dec 2019 13:13:32 +0100 Subject: [PATCH 02/26] python/firmware: mark reserved fields as private --- python/src/trezorlib/firmware.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/src/trezorlib/firmware.py b/python/src/trezorlib/firmware.py index c8abe5c59a..7e00e59a28 100644 --- a/python/src/trezorlib/firmware.py +++ b/python/src/trezorlib/firmware.py @@ -112,7 +112,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, @@ -132,7 +132,7 @@ VendorHeader = c.Struct( "vendor_sigs_required" / c.Int8ul, "vendor_sigs_n" / c.Rebuild(c.Int8ul, c.len_(c.this.pubkeys)), "vendor_trust" / VendorTrust, - "reserved" / c.Padding(14), + "_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, @@ -171,13 +171,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), @@ -218,7 +218,7 @@ 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, From 6cd976fdee4e9b5774e447160797b09e6ae7e5f5 Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 18 Dec 2019 13:14:44 +0100 Subject: [PATCH 03/26] python/firmware: support bootloader headers --- python/src/trezorlib/firmware.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/python/src/trezorlib/firmware.py b/python/src/trezorlib/firmware.py index 7e00e59a28..b25e778947 100644 --- a/python/src/trezorlib/firmware.py +++ b/python/src/trezorlib/firmware.py @@ -86,6 +86,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 @@ -160,7 +165,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( From ab82382b1e7bdbc5c3db427e64f23d362733e037 Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 18 Dec 2019 13:15:16 +0100 Subject: [PATCH 04/26] python/firmware: make header digest function public --- python/src/trezorlib/firmware.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/src/trezorlib/firmware.py b/python/src/trezorlib/firmware.py index b25e778947..c72631b704 100644 --- a/python/src/trezorlib/firmware.py +++ b/python/src/trezorlib/firmware.py @@ -298,7 +298,7 @@ def check_sig_v1( raise InvalidSignatureError("Invalid signature in slot {}".format(i)) from e -def _header_digest( +def header_digest( header: c.Container, header_type: c.Construct, hash_function: Callable = blake2s ) -> bytes: stripped_header = header.copy() @@ -311,11 +311,11 @@ def _header_digest( def digest_v2(fw: FirmwareType) -> bytes: - return _header_digest(fw.firmware_header, FirmwareHeader, blake2s) + return header_digest(fw.image.header, FirmwareHeader, blake2s) def digest_onev2(fw: FirmwareType) -> bytes: - return _header_digest(fw.firmware_header, FirmwareHeader, hashlib.sha256) + return header_digest(fw.header, FirmwareHeader, hashlib.sha256) def validate_code_hashes( @@ -374,7 +374,7 @@ def validate_onev1(fw: FirmwareType, allow_unsigned: bool = False) -> None: def validate_v2(fw: FirmwareType, skip_vendor_header: bool = False) -> None: - vendor_fingerprint = _header_digest(fw.vendor_header, VendorHeader) + vendor_fingerprint = header_digest(fw.vendor_header, VendorHeader) fingerprint = digest_v2(fw) if not skip_vendor_header: From 941087179f0983b54967a7b77bd8779b9a091b53 Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 18 Dec 2019 15:44:51 +0100 Subject: [PATCH 05/26] python/firmware: clarify firmware image types --- python/src/trezorlib/cli/firmware.py | 13 +-- python/src/trezorlib/firmware.py | 131 ++++++++++++++++----------- 2 files changed, 86 insertions(+), 58 deletions(-) diff --git a/python/src/trezorlib/cli/firmware.py b/python/src/trezorlib/cli/firmware.py index 28046763e1..9ed8b71260 100644 --- a/python/src/trezorlib/cli/firmware.py +++ b/python/src/trezorlib/cli/firmware.py @@ -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_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 diff --git a/python/src/trezorlib/firmware.py b/python/src/trezorlib/firmware.py index c72631b704..74ac0b23cf 100644 --- a/python/src/trezorlib/firmware.py +++ b/python/src/trezorlib/firmware.py @@ -198,24 +198,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 @@ -228,7 +241,7 @@ FirmwareOne = c.Struct( "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 @@ -240,20 +253,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") @@ -261,10 +273,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() @@ -286,7 +298,7 @@ def check_sig_v1( 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)) @@ -310,60 +322,75 @@ def header_digest( return hash_function(header_bytes).digest() -def digest_v2(fw: FirmwareType) -> bytes: +def digest_v2(fw: c.Container) -> bytes: return header_digest(fw.image.header, FirmwareHeader, blake2s) -def digest_onev2(fw: FirmwareType) -> bytes: +def digest_onev2(fw: c.Container) -> bytes: return header_digest(fw.header, FirmwareHeader, 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): + hashes = [] + for i in range(16): 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] + chunk = code[: chunk_size - 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] + ptr = i * chunk_size - code_offset + chunk = 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: 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: - raise FirmwareIntegrityError("Invalid firmware data.") + if not chunk: + hashes.append(b"\0" * 32) + else: + hashes.append(hash_function(chunk).digest()) + + return hashes -def validate_onev2(fw: FirmwareType, allow_unsigned: bool = False) -> None: +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: 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: @@ -373,7 +400,7 @@ 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: +def validate_v2(fw: c.Container, skip_vendor_header: bool = False) -> None: vendor_fingerprint = header_digest(fw.vendor_header, VendorHeader) fingerprint = digest_v2(fw) @@ -399,23 +426,23 @@ def validate_v2(fw: FirmwareType, skip_vendor_header: bool = False) -> None: try: cosi.verify_m_of_n( - fw.firmware_header.signature, + fw.image.header.signature, fingerprint, fw.vendor_header.vendor_sigs_required, fw.vendor_header.vendor_sigs_n, - fw.firmware_header.sigmask, + fw.image.header.sigmask, fw.vendor_header.pubkeys, ) 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: @@ -427,7 +454,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) From 15bd35824b78d49fd803e579cbc34c63bb14f4fe Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 20 Dec 2019 13:45:41 +0100 Subject: [PATCH 06/26] python/cosi: improve API cosi.verify was renamed to verify_combined, because it is pretty much ed25519.verify, and the new name implies what it does in terms of the CoSi scheme: verify a signature with already-combined public keys. cosi.verify_m_of_n signature was simplified by not requiring the `n` parameter, which is not important for verification. The updated function was renamed to cosi.verify, because this is the standard CoSi verification operation: given signature, digest, required number of signatures, sigmask, and a list of public keys, verify that enough signatures are indicated and that they sign the digest. --- core/tools/binctl | 10 +++++--- core/tools/keyctl | 2 +- core/tools/keyctl-coordinator | 2 +- python/src/trezorlib/cosi.py | 42 +++++++++++++++++++++----------- python/src/trezorlib/firmware.py | 16 ++++++------ python/tests/test_cosi.py | 34 ++++++++++++++------------ tests/device_tests/test_cosi.py | 4 +-- 7 files changed, 64 insertions(+), 46 deletions(-) diff --git a/core/tools/binctl b/core/tools/binctl index c86c8bf567..9685fa3dd4 100755 --- a/core/tools/binctl +++ b/core/tools/binctl @@ -311,15 +311,19 @@ def binopen(filename): 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) + cosi.verify( + firmware.sig, + digest, + vheader.vsig_m, + vheader.vpub, + firmware.sigmask, + ) print("Firmware signature OK") except ValueError: print("Firmware signature INCORRECT") diff --git a/core/tools/keyctl b/core/tools/keyctl index dfcd907366..d1c9aaf0c7 100755 --- a/core/tools/keyctl +++ b/core/tools/keyctl @@ -55,7 +55,7 @@ def sign(index, filename, seckeys): sigs.append(sig) # compute global signature sig = cosi.combine_sig(global_R, sigs) - cosi.verify(sig, digest, global_pk) + cosi.verify_combined(sig, digest, global_pk) print(binascii.hexlify(sig).decode()) diff --git a/core/tools/keyctl-coordinator b/core/tools/keyctl-coordinator index 75499a96c4..3ed7ac24ad 100755 --- a/core/tools/keyctl-coordinator +++ b/core/tools/keyctl-coordinator @@ -66,7 +66,7 @@ def sign(index, filename, participants): 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) + cosi.verify_combined(sig, digest, global_pk) print("global signature:") print(binascii.hexlify(sig).decode()) diff --git a/python/src/trezorlib/cosi.py b/python/src/trezorlib/cosi.py index 057693c9a1..51e59acb33 100644 --- a/python/src/trezorlib/cosi.py +++ b/python/src/trezorlib/cosi.py @@ -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: diff --git a/python/src/trezorlib/firmware.py b/python/src/trezorlib/firmware.py index 74ac0b23cf..461e9d2861 100644 --- a/python/src/trezorlib/firmware.py +++ b/python/src/trezorlib/firmware.py @@ -49,8 +49,8 @@ V2_BOOTLOADER_KEYS = [ "b8307a71f552c60a4cbb317ff48b82cdbf6b6bb5f04c920fec7badf017883751", ) ] -V2_BOOTLOADER_M = 2 -V2_BOOTLOADER_N = 3 + +V2_SIGS_REQUIRED = 2 ONEV2_CHUNK_SIZE = 1024 * 64 V2_CHUNK_SIZE = 1024 * 128 @@ -408,13 +408,12 @@ def validate_v2(fw: c.Container, skip_vendor_header: bool = False) -> None: 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.") @@ -425,13 +424,12 @@ def validate_v2(fw: c.Container, skip_vendor_header: bool = False) -> None: # raise ValueError("Vendor header expired.") try: - cosi.verify_m_of_n( + cosi.verify( fw.image.header.signature, fingerprint, fw.vendor_header.vendor_sigs_required, - fw.vendor_header.vendor_sigs_n, - fw.image.header.sigmask, fw.vendor_header.pubkeys, + fw.image.header.sigmask, ) except Exception: raise InvalidSignatureError("Invalid firmware signature.") diff --git a/python/tests/test_cosi.py b/python/tests/test_cosi.py index 88281f5767..e58b6b1bf6 100644 --- a/python/tests/test_cosi.py +++ b/python/tests/test_cosi.py @@ -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] diff --git a/tests/device_tests/test_cosi.py b/tests/device_tests/test_cosi.py index aadb33e9dd..1f562bb589 100644 --- a/tests/device_tests/test_cosi.py +++ b/tests/device_tests/test_cosi.py @@ -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) From 18d4bd30a3f0388108a02f39240f110ea6de14c5 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 20 Dec 2019 13:48:59 +0100 Subject: [PATCH 07/26] python/firmware: add production and development boardloader keys --- python/src/trezorlib/firmware.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/python/src/trezorlib/firmware.py b/python/src/trezorlib/firmware.py index 461e9d2861..5bb68c2823 100644 --- a/python/src/trezorlib/firmware.py +++ b/python/src/trezorlib/firmware.py @@ -41,6 +41,24 @@ V1_BOOTLOADER_KEYS = [ ) ] +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(key) for key in ( From 40477b836e5693b451f2c9a947b477dd1f9a90bf Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 20 Dec 2019 13:49:35 +0100 Subject: [PATCH 08/26] python/firmware: make header_digest function more intelligent --- python/src/trezorlib/firmware.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/python/src/trezorlib/firmware.py b/python/src/trezorlib/firmware.py index 5bb68c2823..48c8388f3b 100644 --- a/python/src/trezorlib/firmware.py +++ b/python/src/trezorlib/firmware.py @@ -328,24 +328,26 @@ 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: c.Container) -> bytes: - return header_digest(fw.image.header, FirmwareHeader, blake2s) + return header_digest(fw.image.header, blake2s) def digest_onev2(fw: c.Container) -> bytes: - return header_digest(fw.header, FirmwareHeader, hashlib.sha256) + return header_digest(fw.header, hashlib.sha256) def calculate_code_hashes( @@ -419,7 +421,7 @@ def validate_onev1(fw: c.Container, allow_unsigned: bool = False) -> None: def validate_v2(fw: c.Container, skip_vendor_header: bool = False) -> None: - vendor_fingerprint = header_digest(fw.vendor_header, VendorHeader) + vendor_fingerprint = header_digest(fw.vendor_header) fingerprint = digest_v2(fw) if not skip_vendor_header: From 3fc32312547de2d70efb444aacaa2d64f5a6c819 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 20 Dec 2019 13:50:24 +0100 Subject: [PATCH 09/26] python/firmware: simplify calculate_code_hashes --- python/src/trezorlib/firmware.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/python/src/trezorlib/firmware.py b/python/src/trezorlib/firmware.py index 48c8388f3b..2d917d43cd 100644 --- a/python/src/trezorlib/firmware.py +++ b/python/src/trezorlib/firmware.py @@ -358,17 +358,14 @@ def calculate_code_hashes( padding_byte: bytes = None, ) -> None: hashes = [] - for i in range(16): - if i == 0: - # Because first chunk is sent along with headers, there is less code in it. - chunk = code[: chunk_size - code_offset] - else: - # Subsequent chunks are shifted by the "missing header" size. - ptr = i * chunk_size - code_offset - chunk = 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: + # 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: @@ -376,6 +373,8 @@ def calculate_code_hashes( else: hashes.append(hash_function(chunk).digest()) + start = end + return hashes From b26a430b859217fcbf1be3f3d4ad835652f13c3a Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 20 Dec 2019 13:50:44 +0100 Subject: [PATCH 10/26] python/firmware: shorten names for vendor header fields --- python/src/trezorlib/cli/firmware.py | 2 +- python/src/trezorlib/firmware.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/python/src/trezorlib/cli/firmware.py b/python/src/trezorlib/cli/firmware.py index 9ed8b71260..39124a7ddd 100644 --- a/python/src/trezorlib/cli/firmware.py +++ b/python/src/trezorlib/cli/firmware.py @@ -44,7 +44,7 @@ def validate_firmware(version, fw, expected_fingerprint=None): _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.image.header.version) diff --git a/python/src/trezorlib/firmware.py b/python/src/trezorlib/firmware.py index 2d917d43cd..efb185a4cd 100644 --- a/python/src/trezorlib/firmware.py +++ b/python/src/trezorlib/firmware.py @@ -152,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, + "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.vendor_sigs_n], - "vendor_string" / c.Aligned(4, c.PascalString(c.Int8ul, "utf-8")), - "vendor_image" / Toif, + "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), @@ -446,7 +446,7 @@ def validate_v2(fw: c.Container, skip_vendor_header: bool = False) -> None: cosi.verify( fw.image.header.signature, fingerprint, - fw.vendor_header.vendor_sigs_required, + fw.vendor_header.sig_m, fw.vendor_header.pubkeys, fw.image.header.sigmask, ) From fe4ef336aab7e63100150ffa05ceea81a5a1aa3c Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 20 Dec 2019 15:00:32 +0100 Subject: [PATCH 11/26] core/keyctl: get rid of serpent conversions --- core/tools/keyctl-coordinator | 5 ++--- core/tools/keyctl-proxy | 9 ++------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/core/tools/keyctl-coordinator b/core/tools/keyctl-coordinator index 3ed7ac24ad..f666ed6c2a 100755 --- a/core/tools/keyctl-coordinator +++ b/core/tools/keyctl-coordinator @@ -4,9 +4,10 @@ import struct import click import pyblake2 import Pyro4 -import serpent from trezorlib import cosi +Pyro4.config.SERIALIZER = "marshal" + PORT = 5001 indexmap = {"bootloader": 0, "vendorheader": 1, "firmware": 2} @@ -50,7 +51,6 @@ def sign(index, filename, participants): 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)) @@ -61,7 +61,6 @@ def sign(index, filename, participants): 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 diff --git a/core/tools/keyctl-proxy b/core/tools/keyctl-proxy index 9633afeb1a..1246477168 100755 --- a/core/tools/keyctl-proxy +++ b/core/tools/keyctl-proxy @@ -4,9 +4,10 @@ import sys import traceback import Pyro4 -import serpent from trezorlib import cosi, tools +Pyro4.config.SERIALIZER = "marshal" + PORT = 5001 indexmap = {"bootloader": 0, "vendorheader": 1, "firmware": 2} @@ -26,7 +27,6 @@ def get_path(index): @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: @@ -47,11 +47,6 @@ class KeyctlProxy(object): return (pk, R) 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: From cc29b22f9188c2ead35e7cb80041bedfd2908915 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 3 Jan 2020 13:16:33 +0100 Subject: [PATCH 12/26] core/tools: introduce headertool --- core/tools/headertool.py | 162 ++++++++ .../trezorlib/_internal/firmware_headers.py | 369 ++++++++++++++++++ 2 files changed, 531 insertions(+) create mode 100755 core/tools/headertool.py create mode 100644 python/src/trezorlib/_internal/firmware_headers.py diff --git a/core/tools/headertool.py b/core/tools/headertool.py new file mode 100755 index 0000000000..11598ff7b0 --- /dev/null +++ b/core/tools/headertool.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +import click + +from trezorlib import cosi, firmware +from trezorlib._internal import firmware_headers + +from typing import List, Tuple + +# =========================== 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: :") + raise click.ClickException("Unrecognized key format.") + return sigmask, privkeys + + +# ===================== 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", + multiple=True, + help="Private key to use for signing.", +) +@click.option( + "-D", "--sign-dev-keys", is_flag=True, help="Sign with development header keys." +) +@click.option( + "-s", "--signature", "insert_signature", nargs=2, 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 fingerprint for signing.", +) +@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, +): + 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 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() diff --git a/python/src/trezorlib/_internal/firmware_headers.py b/python/src/trezorlib/_internal/firmware_headers.py new file mode 100644 index 0000000000..cf6e6daabb --- /dev/null +++ b/python/src/trezorlib/_internal/firmware_headers.py @@ -0,0 +1,369 @@ +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(" 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 += f" build {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: + raise NotImplementedError + + 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), + f"Pubkey bundle hash: {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(f"Fingerprint: {click.style(fingerprint.hex(), bold=True)}") + + sig_status = self.check_signature() + sym = SYM_OK if sig_status.is_ok() else SYM_FAIL + output.append(f"{sym} Signature is {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(f"{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), + f"Fingerprint: {click.style(self.digest().hex(), bold=True)}", + f"{all_ok} Signature is {sig_status.value}, hashes are {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") From 5b48505b88d207cc88425ba9392d5cd7157f3140 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 3 Jan 2020 14:18:36 +0100 Subject: [PATCH 13/26] core/tools: fold keyctl-coordinator into headertool --- core/tools/headertool.py | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/core/tools/headertool.py b/core/tools/headertool.py index 11598ff7b0..aafda9b3d0 100755 --- a/core/tools/headertool.py +++ b/core/tools/headertool.py @@ -1,11 +1,16 @@ #!/usr/bin/env python3 import click +import Pyro4 from trezorlib import cosi, firmware from trezorlib._internal import firmware_headers from typing import List, Tuple +Pyro4.config.SERIALIZER = "marshal" + +PORT = 5001 + # =========================== signing ========================= @@ -46,6 +51,49 @@ def parse_privkey_args(privkey_data: List[str]) -> Tuple[int, List[bytes]]: 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 + + sigmask = 0 + proxies = [] + pks, Rs = [], [] + for addr in addrs: + click.echo(f"Connecting to {addr}...") + proxy = Pyro4.Proxy(f"PYRO:keyctl@{addr}:{PORT}") + proxies.append((addr, 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, proxy in proxies: + click.echo(f"Waiting for {addr} to sign... ", nl=False) + sig = proxy.get_signature(name, digest, global_R, global_pk) + sigs.append(sig) + click.echo("OK") + + # compute global signature + return sigmask, cosi.combine_sig(global_R, sigs) + + # ===================== CLI actions ========================= @@ -85,6 +133,7 @@ def do_replace_vendorheader(fw, vh_file) -> None: is_flag=True, help="Only output fingerprint for signing.", ) +@click.option("-r", "--remote", multiple=True, help="IP address of remote signer.") @click.argument("firmware_file", type=click.File("rb+")) def cli( firmware_file, @@ -96,6 +145,7 @@ def cli( insert_signature, replace_vendor_header, print_digest, + remote, ): firmware_data = firmware_file.read() @@ -141,6 +191,11 @@ def cli( for bit in sigmask_str.split(":"): sigmask |= 1 << (int(bit) - 1) + if remote: + 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) From c03ac3f8dd5561b41beec28f63d5be7778c34c5e Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 3 Jan 2020 14:19:02 +0100 Subject: [PATCH 14/26] core/tools: update keyctl-proxy to work with headertool --- core/tools/keyctl-proxy | 162 ++++++++++++++++++++++++++-------------- 1 file changed, 105 insertions(+), 57 deletions(-) diff --git a/core/tools/keyctl-proxy b/core/tools/keyctl-proxy index 1246477168..05cf02f36e 100755 --- a/core/tools/keyctl-proxy +++ b/core/tools/keyctl-proxy @@ -1,81 +1,129 @@ #!/usr/bin/env python3 -import binascii import sys import traceback +import click import Pyro4 -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" -def get_trezor(): - from trezorlib.client import TrezorClient - from trezorlib.transport import get_transport - from trezorlib.ui import ClickUI +def make_commit(index, digest): + path = PATH.format(index) + address_n = parse_path(path) + first_pass = True + while True: + try: + t = get_default_client() + if first_pass: + t.clear_session() + first_pass = False - return TrezorClient(get_transport(), ui=ClickUI()) - - -def get_path(index): - return "10018'/%d'" % indexmap[index] + click.echo(f"\n\n\nCommiting to hash {digest.hex()} with path {path}") + commit = cosi.commit(t, address_n, digest) + return commit.pubkey, commit.commitment + except Exception as e: + print(e) + traceback.print_exc() + print("Trying again ...") @Pyro4.expose -class KeyctlProxy(object): - def get_commit(self, index, 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, image_type, digest: bytes, commit: Tuple[bytes, bytes]) -> None: + self.name = image_type.NAME + self.address_n = parse_path(PATH.format(image_type.BIP32_INDEX)) + self.digest = digest + self.commit = commit - def get_signature(self, index, digest, global_R, 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: + print(f"ERROR! Remote wants to sign {name} with digest {digest.hex()}") + print(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) + print("Sending commitment!") + return self.commit + + def get_signature(self, name, digest, global_R, global_pk): + self._check_name_digest(name, digest) + while True: try: - t = get_trezor() - print( - "\n\n\nSigning hash %s with path %s:" - % (binascii.hexlify(digest).decode(), path) - ) - signature = cosi.sign( - t, tools.parse_path(path), digest, global_R, global_pk - ) + t = get_default_client() + print("\n\n\nSigning...") + signature = cosi.sign(t, self.address_n, digest, global_R, global_pk) + print("Sending signature!") + return signature.signature except Exception as e: print(e) traceback.print_exc() print("Trying again ...") - sig = signature.signature - print("Signature sent!") - return sig + + +@click.command() +@click.option( + "-l", "--listen", "ipaddr", default="0.0.0.0", help="Bind to particular ip address" +) +@click.option("-t", "--header-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, header_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. + """ + public_keys = None + if fw_file: + if header_type or digest: + raise click.ClickException("Do not specify fw_file together with -t/-d") + + fw = parse_image(fw_file.read()) + digest = fw.digest() + public_keys = fw.public_keys + + click.echo(fw.format()) + + if not fw_file and (not header_type or not digest): + raise click.ClickException("Please specify either fw_file or -t and -h") + + while True: + pubkey, R = make_commit(header_type.BIP32_INDEX, digest) + if public_keys is not None and pubkey not in public_keys: + click.echo(f"\n\nPublic key {pubkey.hex()} is unknown.") + if click.confirm("Retry with a different passphrase?"): + continue + break + + daemon = Pyro4.Daemon(host=ipaddr, port=PORT) + proxy = KeyctlProxy(header_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() From 9341f0d5848f3672afcf05d0b79572686dbbba74 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 3 Jan 2020 16:43:44 +0100 Subject: [PATCH 15/26] core: improve building of vendor headers --- core/embed/vendorheader/generate.sh | 17 ++-- .../vendorheader/vendor_satoshilabs.json | 20 +++++ core/embed/vendorheader/vendor_unsafe.json | 20 +++++ core/tools/build_vendorheader | 78 +++++-------------- .../trezorlib/_internal/firmware_headers.py | 2 +- 5 files changed, 67 insertions(+), 70 deletions(-) create mode 100644 core/embed/vendorheader/vendor_satoshilabs.json create mode 100644 core/embed/vendorheader/vendor_unsafe.json diff --git a/core/embed/vendorheader/generate.sh b/core/embed/vendorheader/generate.sh index 0f78b25d73..c566911c5d 100755 --- a/core/embed/vendorheader/generate.sh +++ b/core/embed/vendorheader/generate.sh @@ -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 diff --git a/core/embed/vendorheader/vendor_satoshilabs.json b/core/embed/vendorheader/vendor_satoshilabs.json new file mode 100644 index 0000000000..7c4c6f4567 --- /dev/null +++ b/core/embed/vendorheader/vendor_satoshilabs.json @@ -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" + ] +} diff --git a/core/embed/vendorheader/vendor_unsafe.json b/core/embed/vendorheader/vendor_unsafe.json new file mode 100644 index 0000000000..e7af4710e5 --- /dev/null +++ b/core/embed/vendorheader/vendor_unsafe.json @@ -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" + ] +} diff --git a/core/tools/build_vendorheader b/core/tools/build_vendorheader index 0658ff78e8..be085c121e 100755 --- a/core/tools/build_vendorheader +++ b/core/tools/build_vendorheader @@ -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(" None: - raise NotImplementedError + pass def insert_signature(self, signature: bytes, sigmask: int) -> None: self.header.signature = signature From 1b04d1caa7321d3e80aa4492e91b36390cf00847 Mon Sep 17 00:00:00 2001 From: matejcik Date: Fri, 3 Jan 2020 16:53:55 +0100 Subject: [PATCH 16/26] core/tools: drop tools obsoleted by headertool --- core/Makefile | 4 +- core/SConscript.bootloader | 6 +- core/SConscript.firmware | 6 +- core/SConscript.prodtest | 6 +- core/SConscript.reflash | 6 +- core/tools/binctl | 363 ------------------------------- core/tools/keyctl | 63 ------ core/tools/keyctl-coordinator | 74 ------- python/src/trezorlib/firmware.py | 2 +- 9 files changed, 11 insertions(+), 519 deletions(-) delete mode 100755 core/tools/binctl delete mode 100755 core/tools/keyctl delete mode 100755 core/tools/keyctl-coordinator diff --git a/core/Makefile b/core/Makefile index 6a7a98cf4d..7c1ca0e8cd 100644 --- a/core/Makefile +++ b/core/Makefile @@ -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 diff --git a/core/SConscript.bootloader b/core/SConscript.bootloader index 092c351a20..1310a1df63 100644 --- a/core/SConscript.bootloader +++ b/core/SConscript.bootloader @@ -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 ''), ], ) diff --git a/core/SConscript.firmware b/core/SConscript.firmware index e83c765d5b..be45c9b210 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -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: diff --git a/core/SConscript.prodtest b/core/SConscript.prodtest index 2307c42a25..77695ac4af 100644 --- a/core/SConscript.prodtest +++ b/core/SConscript.prodtest @@ -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 ''), ], ) diff --git a/core/SConscript.reflash b/core/SConscript.reflash index 51c2c0e9b2..0369b9a8d2 100644 --- a/core/SConscript.reflash +++ b/core/SConscript.reflash @@ -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 ''), ], ) diff --git a/core/tools/binctl b/core/tools/binctl deleted file mode 100755 index 9685fa3dd4..0000000000 --- a/core/tools/binctl +++ /dev/null @@ -1,363 +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(" 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(" 0: - hdr = ( - subdata[: IMAGE_HEADER_SIZE - IMAGE_SIG_SIZE] - + IMAGE_SIG_SIZE * b"\x00" - ) - digest = pyblake2.blake2s(hdr).digest() - try: - cosi.verify( - firmware.sig, - digest, - vheader.vsig_m, - vheader.vpub, - firmware.sigmask, - ) - 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() diff --git a/core/tools/keyctl b/core/tools/keyctl deleted file mode 100755 index d1c9aaf0c7..0000000000 --- a/core/tools/keyctl +++ /dev/null @@ -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(" Date: Fri, 3 Jan 2020 17:42:52 +0100 Subject: [PATCH 17/26] core/tools: add help texts to headertool --- core/tools/headertool.py | 58 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/core/tools/headertool.py b/core/tools/headertool.py index aafda9b3d0..a24dc05f31 100755 --- a/core/tools/headertool.py +++ b/core/tools/headertool.py @@ -116,14 +116,20 @@ def do_replace_vendorheader(fw, vh_file) -> None: "-S", "--sign-private", "privkey_data", + metavar="INDEX:PRIVKEY_HEX", multiple=True, - help="Private key to use for signing.", + 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, help="Insert external signature." + "-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( @@ -131,9 +137,15 @@ def do_replace_vendorheader(fw, vh_file) -> None: "--digest", "print_digest", is_flag=True, - help="Only output fingerprint for signing.", + 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.option("-r", "--remote", multiple=True, help="IP address of remote signer.") @click.argument("firmware_file", type=click.File("rb+")) def cli( firmware_file, @@ -147,6 +159,44 @@ def cli( 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: From 388843f772a20ced7efea7e1019fdc9413e52f78 Mon Sep 17 00:00:00 2001 From: matejcik Date: Mon, 6 Jan 2020 16:03:56 +0100 Subject: [PATCH 18/26] core/tools: make keyctl-proxy output nicer --- core/tools/keyctl-proxy | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/core/tools/keyctl-proxy b/core/tools/keyctl-proxy index 05cf02f36e..5b49ba858b 100755 --- a/core/tools/keyctl-proxy +++ b/core/tools/keyctl-proxy @@ -28,7 +28,7 @@ indexmap = { PATH = "10018h/{}h" -def make_commit(index, digest): +def make_commit(name, index, digest): path = PATH.format(index) address_n = parse_path(path) first_pass = True @@ -39,13 +39,20 @@ def make_commit(index, digest): t.clear_session() first_pass = False - click.echo(f"\n\n\nCommiting to hash {digest.hex()} with path {path}") + click.echo(f"\n\n\nCommiting to {click.style(name, bold=True)} hash:") + for partid in range(4): + digest_part = digest[partid * 8 : (partid + 1) * 8] + color = "red" if partid % 2 else "blue" + digest_str = click.style(digest_part.hex().upper(), fg=color) + click.echo(digest_str) + + click.echo(f"Using path: {click.style(path, bold=True)}") commit = cosi.commit(t, address_n, digest) return commit.pubkey, commit.commitment except Exception as e: - print(e) + click.echo(e) traceback.print_exc() - print("Trying again ...") + click.echo("Trying again ...") @Pyro4.expose @@ -58,13 +65,13 @@ class KeyctlProxy: def _check_name_digest(self, name, digest): if name != self.name or digest != self.digest: - print(f"ERROR! Remote wants to sign {name} with digest {digest.hex()}") - print(f"Expected: {self.name} with digest {self.digest.hex()}") + 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) - print("Sending commitment!") + click.echo("Sending commitment!") return self.commit def get_signature(self, name, digest, global_R, global_pk): @@ -72,14 +79,14 @@ class KeyctlProxy: while True: try: t = get_default_client() - print("\n\n\nSigning...") + click.echo("\n\n\nSigning...") signature = cosi.sign(t, self.address_n, digest, global_R, global_pk) - print("Sending signature!") + click.echo("Sending signature!") return signature.signature except Exception as e: - print(e) + click.echo(e) traceback.print_exc() - print("Trying again ...") + click.echo("Trying again ...") @click.command() From e9c68d739711df74d3adf89560fc4279fefcdd50 Mon Sep 17 00:00:00 2001 From: matejcik Date: Mon, 6 Jan 2020 16:05:14 +0100 Subject: [PATCH 19/26] core/tools: pass fw instance properly in keyctl-proxy --- core/tools/keyctl-proxy | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/tools/keyctl-proxy b/core/tools/keyctl-proxy index 5b49ba858b..0e14ebe623 100755 --- a/core/tools/keyctl-proxy +++ b/core/tools/keyctl-proxy @@ -93,10 +93,10 @@ class KeyctlProxy: @click.option( "-l", "--listen", "ipaddr", default="0.0.0.0", help="Bind to particular ip address" ) -@click.option("-t", "--header-type", type=click.Choice(indexmap.keys())) +@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, header_type, digest): +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 @@ -104,20 +104,20 @@ def cli(ipaddr, fw_file, header_type, digest): """ public_keys = None if fw_file: - if header_type or digest: + if fw_or_type or digest: raise click.ClickException("Do not specify fw_file together with -t/-d") - fw = parse_image(fw_file.read()) - digest = fw.digest() - public_keys = fw.public_keys + fw_or_type = parse_image(fw_file.read()) + digest = fw_or_type.digest() + public_keys = fw_or_type.public_keys - click.echo(fw.format()) + click.echo(fw_or_type.format()) - if not fw_file and (not header_type or not digest): + if not fw_file and (not fw_or_type or not digest): raise click.ClickException("Please specify either fw_file or -t and -h") while True: - pubkey, R = make_commit(header_type.BIP32_INDEX, digest) + pubkey, R = make_commit(fw_or_type.NAME, fw_or_type.BIP32_INDEX, digest) if public_keys is not None and pubkey not in public_keys: click.echo(f"\n\nPublic key {pubkey.hex()} is unknown.") if click.confirm("Retry with a different passphrase?"): @@ -125,7 +125,7 @@ def cli(ipaddr, fw_file, header_type, digest): break daemon = Pyro4.Daemon(host=ipaddr, port=PORT) - proxy = KeyctlProxy(header_type, digest, (pubkey, R)) + proxy = KeyctlProxy(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.") From ccacada37c6084e999207a22740a56da4bbab624 Mon Sep 17 00:00:00 2001 From: matejcik Date: Mon, 6 Jan 2020 16:05:43 +0100 Subject: [PATCH 20/26] core/tools: cleanly shut down keyctl-proxy after signing --- core/tools/headertool.py | 3 +++ core/tools/keyctl-proxy | 12 ++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/tools/headertool.py b/core/tools/headertool.py index a24dc05f31..825483f3c8 100755 --- a/core/tools/headertool.py +++ b/core/tools/headertool.py @@ -90,6 +90,9 @@ def process_remote_signers(fw, addrs: List[str]) -> Tuple[int, List[bytes]]: sigs.append(sig) click.echo("OK") + for _, proxy in proxies: + proxy.finish() + # compute global signature return sigmask, cosi.combine_sig(global_R, sigs) diff --git a/core/tools/keyctl-proxy b/core/tools/keyctl-proxy index 0e14ebe623..6e10d0eea9 100755 --- a/core/tools/keyctl-proxy +++ b/core/tools/keyctl-proxy @@ -57,7 +57,10 @@ def make_commit(name, index, digest): @Pyro4.expose class KeyctlProxy: - def __init__(self, image_type, digest: bytes, commit: Tuple[bytes, bytes]) -> None: + 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 @@ -88,6 +91,11 @@ class KeyctlProxy: traceback.print_exc() click.echo("Trying again ...") + @Pyro4.oneway + def finish(self): + click.echo("Done! \\(^o^)/") + self.daemon.shutdown() + @click.command() @click.option( @@ -125,7 +133,7 @@ def cli(ipaddr, fw_file, fw_or_type, digest): break daemon = Pyro4.Daemon(host=ipaddr, port=PORT) - proxy = KeyctlProxy(fw_or_type, digest, (pubkey, R)) + 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.") From 4d7e3c8a23048e4cd9be6fc5bfe8b320043c19b0 Mon Sep 17 00:00:00 2001 From: matejcik Date: Mon, 6 Jan 2020 16:05:54 +0100 Subject: [PATCH 21/26] python: use TREZOR_PATH in get_default_client --- python/src/trezorlib/client.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/python/src/trezorlib/client.py b/python/src/trezorlib/client.py index 2eaaf2f40b..24b4ccbb48 100644 --- a/python/src/trezorlib/client.py +++ b/python/src/trezorlib/client.py @@ -15,6 +15,7 @@ # If not, see . 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() From 3f85db1b62e935dd4cd84b361ebd869671f48c36 Mon Sep 17 00:00:00 2001 From: matejcik Date: Mon, 6 Jan 2020 16:30:55 +0100 Subject: [PATCH 22/26] core/tools: retain client handle, only ask for passphrase once --- core/tools/keyctl-proxy | 65 ++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/core/tools/keyctl-proxy b/core/tools/keyctl-proxy index 6e10d0eea9..8aa3e0bac4 100755 --- a/core/tools/keyctl-proxy +++ b/core/tools/keyctl-proxy @@ -27,32 +27,42 @@ indexmap = { PATH = "10018h/{}h" +TREZOR = None -def make_commit(name, index, digest): - path = PATH.format(index) + +def make_commit(fw_or_type, digest, public_keys): + path = PATH.format(fw_or_type.BIP32_INDEX) address_n = parse_path(path) - first_pass = True + + # 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 "blue" + 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)}") + try: - t = get_default_client() - if first_pass: - t.clear_session() - first_pass = False + 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 - click.echo(f"\n\n\nCommiting to {click.style(name, bold=True)} hash:") - for partid in range(4): - digest_part = digest[partid * 8 : (partid + 1) * 8] - color = "red" if partid % 2 else "blue" - digest_str = click.style(digest_part.hex().upper(), fg=color) - click.echo(digest_str) - - click.echo(f"Using path: {click.style(path, bold=True)}") - commit = cosi.commit(t, address_n, digest) return commit.pubkey, commit.commitment except Exception as e: click.echo(e) traceback.print_exc() - click.echo("Trying again ...") + click.echo("Trying again ...\n\n") @Pyro4.expose @@ -81,9 +91,10 @@ class KeyctlProxy: self._check_name_digest(name, digest) while True: try: - t = get_default_client() click.echo("\n\n\nSigning...") - signature = cosi.sign(t, self.address_n, digest, global_R, global_pk) + signature = cosi.sign( + TREZOR, self.address_n, digest, global_R, global_pk + ) click.echo("Sending signature!") return signature.signature except Exception as e: @@ -110,6 +121,8 @@ def cli(ipaddr, fw_file, fw_or_type, digest): 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: @@ -124,13 +137,13 @@ def cli(ipaddr, fw_file, fw_or_type, digest): if not fw_file and (not fw_or_type or not digest): raise click.ClickException("Please specify either fw_file or -t and -h") - while True: - pubkey, R = make_commit(fw_or_type.NAME, fw_or_type.BIP32_INDEX, digest) - if public_keys is not None and pubkey not in public_keys: - click.echo(f"\n\nPublic key {pubkey.hex()} is unknown.") - if click.confirm("Retry with a different passphrase?"): - continue - break + 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)) From 611b734d212824df14b9fc77df26356f4c4b34a8 Mon Sep 17 00:00:00 2001 From: matejcik Date: Mon, 6 Jan 2020 16:45:41 +0100 Subject: [PATCH 23/26] add Pyro4 to pipenv, and make headertool work without it --- Pipfile | 1 + Pipfile.lock | 30 +++++++++++++++++++++++------- core/tools/headertool.py | 10 ++++++++-- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/Pipfile b/Pipfile index 0af72c645a..f73d38446a 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,7 @@ trezor = {editable = true,path = "./python"} scons = "*" protobuf = "==3.6.1" pyblake2 = "*" +Pyro4 = "*" ## test tools pytest = "*" diff --git a/Pipfile.lock b/Pipfile.lock index f2db5a77cc..8a8edaeb5c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -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" } } } diff --git a/core/tools/headertool.py b/core/tools/headertool.py index 825483f3c8..431f6c1154 100755 --- a/core/tools/headertool.py +++ b/core/tools/headertool.py @@ -1,13 +1,17 @@ #!/usr/bin/env python3 import click -import Pyro4 from trezorlib import cosi, firmware from trezorlib._internal import firmware_headers from typing import List, Tuple -Pyro4.config.SERIALIZER = "marshal" + +try: + import Pyro4 + Pyro4.config.SERIALIZER = "marshal" +except ImportError: + Pyro4 = None PORT = 5001 @@ -245,6 +249,8 @@ def cli( 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) From 8a5242ed0f9084bc17891f13a626dd67df528081 Mon Sep 17 00:00:00 2001 From: matejcik Date: Mon, 6 Jan 2020 18:18:22 +0100 Subject: [PATCH 24/26] core/tools: make keyctl remote signing more resilient --- core/tools/headertool.py | 19 +++++++++++-------- core/tools/keyctl-proxy | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/core/tools/headertool.py b/core/tools/headertool.py index 431f6c1154..5b2cf99ea9 100755 --- a/core/tools/headertool.py +++ b/core/tools/headertool.py @@ -64,14 +64,15 @@ def process_remote_signers(fw, addrs: List[str]) -> Tuple[int, List[bytes]]: digest = fw.digest() name = fw.NAME + def mkproxy(addr): + return Pyro4.Proxy(f"PYRO:keyctl@{addr}:{PORT}") + sigmask = 0 - proxies = [] pks, Rs = [], [] for addr in addrs: click.echo(f"Connecting to {addr}...") - proxy = Pyro4.Proxy(f"PYRO:keyctl@{addr}:{PORT}") - proxies.append((addr, proxy)) - pk, R = proxy.get_commit(name, digest) + 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()}" @@ -88,14 +89,16 @@ def process_remote_signers(fw, addrs: List[str]) -> Tuple[int, List[bytes]]: # collect signatures sigs = [] - for addr, proxy in proxies: + for addr in addrs: click.echo(f"Waiting for {addr} to sign... ", nl=False) - sig = proxy.get_signature(name, digest, global_R, global_pk) + with mkproxy(addr) as proxy: + sig = proxy.get_signature(name, digest, global_R, global_pk) sigs.append(sig) click.echo("OK") - for _, proxy in proxies: - proxy.finish() + for addr in addrs: + with mkproxy(addr) as proxy: + proxy.finish() # compute global signature return sigmask, cosi.combine_sig(global_R, sigs) diff --git a/core/tools/keyctl-proxy b/core/tools/keyctl-proxy index 8aa3e0bac4..2f9836c642 100755 --- a/core/tools/keyctl-proxy +++ b/core/tools/keyctl-proxy @@ -75,6 +75,8 @@ class KeyctlProxy: 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 _check_name_digest(self, name, digest): if name != self.name or digest != self.digest: @@ -87,21 +89,29 @@ class KeyctlProxy: click.echo("Sending commitment!") return self.commit - def get_signature(self, name, digest, global_R, global_pk): - self._check_name_digest(name, digest) + def _make_signature(self, global_R, global_pk): while True: try: click.echo("\n\n\nSigning...") signature = cosi.sign( - TREZOR, self.address_n, digest, global_R, global_pk + TREZOR, self.address_n, self.digest, global_R, global_pk ) - click.echo("Sending signature!") return signature.signature except Exception as e: click.echo(e) traceback.print_exc() 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^)/") From a95405b6932a4f4307140e9f3d6af72ab75ea62d Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Tue, 21 Jan 2020 17:20:45 +0100 Subject: [PATCH 25/26] python: don't use py3.6+ format strings yet --- .../trezorlib/_internal/firmware_headers.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/python/src/trezorlib/_internal/firmware_headers.py b/python/src/trezorlib/_internal/firmware_headers.py index 14843365e6..69a6165967 100644 --- a/python/src/trezorlib/_internal/firmware_headers.py +++ b/python/src/trezorlib/_internal/firmware_headers.py @@ -147,7 +147,7 @@ def _format_version(version: c.Container) -> str: str(version[k]) for k in ("major", "minor", "patch") if k in version ) if "build" in version: - version_str += f" build {version.build}" + version_str += " build {}".format(version.build) return version_str @@ -207,7 +207,7 @@ class VendorHeader(SignableImage): vhash = compute_vhash(vh) output = [ "Vendor Header " + _format_container(vh), - f"Pubkey bundle hash: {vhash.hex()}", + "Pubkey bundle hash: {}".format(vhash.hex()), ] else: output = [ @@ -221,11 +221,13 @@ class VendorHeader(SignableImage): fingerprint = firmware.header_digest(vh) if not terse: - output.append(f"Fingerprint: {click.style(fingerprint.hex(), bold=True)}") + 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(f"{sym} Signature is {sig_status.value}") + output.append("{} Signature is {}".format(sym, sig_status.value)) return "\n".join(output) @@ -271,7 +273,7 @@ class BinImage(SignableImage): 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()}")) + hashes_out.append(LiteralStr("{} {}".format(status, expected.hex()))) if all(all_zero(h) for h in self.header.hashes): hash_status = Status.MISSING @@ -287,8 +289,10 @@ class BinImage(SignableImage): 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}", + "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) From 4b1159b94da3d30c6e257df797bb5f7615c2c67a Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Wed, 22 Jan 2020 16:05:41 +0000 Subject: [PATCH 26/26] tools/keyctl-proxy: blue is not readable on my display :) --- core/tools/keyctl-proxy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/tools/keyctl-proxy b/core/tools/keyctl-proxy index 2f9836c642..40b6d45740 100755 --- a/core/tools/keyctl-proxy +++ b/core/tools/keyctl-proxy @@ -45,7 +45,7 @@ def make_commit(fw_or_type, digest, public_keys): 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 "blue" + 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)}")