From e2abd2a9add8102cc56eae63fc1035439bcea445 Mon Sep 17 00:00:00 2001 From: Ondrej Mikle Date: Tue, 20 Sep 2022 13:30:36 +0200 Subject: [PATCH] feat(legacy): bootloader with v3 SignMessage signatures + signatures debug Removed oldest v1 style of firmware signature and presence checks. Added debug helpers for T1 signatures. Support for v2 and v3 signatures, but can only update FW to v3-style signed. Support for debugging T1 signatures. Scripts and README for debugging v2/v3 FW signing scheme. Firmware in GetFeatures counts only v3 signatures as signed. Add documentation and comments about signing schemes like a sane person --- legacy/Makefile.include | 6 + legacy/bootloader/.changelog.d/2568.added | 1 + legacy/bootloader/bootloader.c | 12 +- legacy/bootloader/firmware_align.py | 1 + legacy/bootloader/usb.c | 19 +- legacy/debug_signing/README.md | 77 ++++++++ legacy/debug_signing/fill_t1_fw_signatures.py | 93 +++++++++ .../firmware_hash_sign_trezor.py | 51 +++++ legacy/debug_signing/firmware_hash_verify.py | 22 +++ .../sign_firmware_v2_signature.py | 105 ++++++++++ legacy/firmware/.changelog.d/2568.added | 1 + legacy/firmware/fsm_msg_common.h | 3 +- legacy/fw_signatures.c | 186 ++++++++++++------ legacy/fw_signatures.h | 72 ++++++- 14 files changed, 571 insertions(+), 78 deletions(-) create mode 100644 legacy/bootloader/.changelog.d/2568.added create mode 100644 legacy/debug_signing/README.md create mode 100755 legacy/debug_signing/fill_t1_fw_signatures.py create mode 100755 legacy/debug_signing/firmware_hash_sign_trezor.py create mode 100755 legacy/debug_signing/firmware_hash_verify.py create mode 100755 legacy/debug_signing/sign_firmware_v2_signature.py create mode 100644 legacy/firmware/.changelog.d/2568.added diff --git a/legacy/Makefile.include b/legacy/Makefile.include index b5149c4fe0..f7cbd6ec23 100644 --- a/legacy/Makefile.include +++ b/legacy/Makefile.include @@ -162,6 +162,12 @@ else CFLAGS += -DDEBUG_RNG=0 endif +ifeq ($(DEBUG_T1_SIGNATURES), 1) +CFLAGS += -DDEBUG_T1_SIGNATURES=1 +else +CFLAGS += -DDEBUG_T1_SIGNATURES=0 +endif + all: $(NAME).bin openocd: diff --git a/legacy/bootloader/.changelog.d/2568.added b/legacy/bootloader/.changelog.d/2568.added new file mode 100644 index 0000000000..77b0e992fa --- /dev/null +++ b/legacy/bootloader/.changelog.d/2568.added @@ -0,0 +1 @@ +T1 bootloader: verify firmware signatures based on SignMessage, add signature debugging diff --git a/legacy/bootloader/bootloader.c b/legacy/bootloader/bootloader.c index bd7b987b8e..31c046f459 100644 --- a/legacy/bootloader/bootloader.c +++ b/legacy/bootloader/bootloader.c @@ -65,6 +65,12 @@ void show_unplug(const char *line1, const char *line2) { } static void show_unofficial_warning(const uint8_t *hash) { +// On production bootloader, show warning and wait for user +// to accept or reject it +// On non-production we only use unofficial firmwares, +// so just show hash for a while to see bootloader started +// but continue +#if PRODUCTION layoutDialog(&bmp_icon_warning, "Abort", "I'll take the risk", NULL, "WARNING!", NULL, "Unofficial firmware", "detected.", NULL, NULL); @@ -82,6 +88,10 @@ static void show_unofficial_warning(const uint8_t *hash) { } // everything is OK, user pressed 2x Continue -> continue program +#else + layoutFirmwareFingerprint(hash); + delay(100000000); +#endif } static void __attribute__((noreturn)) load_app(int signed_firmware) { @@ -147,7 +157,7 @@ int main(void) { (const image_header *)FLASH_PTR(FLASH_FWHEADER_START); uint8_t fingerprint[32] = {0}; - int signed_firmware = signatures_new_ok(hdr, fingerprint); + int signed_firmware = signatures_match(hdr, fingerprint); if (SIG_OK != signed_firmware) { show_unofficial_warning(fingerprint); } diff --git a/legacy/bootloader/firmware_align.py b/legacy/bootloader/firmware_align.py index b30be55824..d9509132d9 100755 --- a/legacy/bootloader/firmware_align.py +++ b/legacy/bootloader/firmware_align.py @@ -8,6 +8,7 @@ MAXSIZE = TOTALSIZE - 32 infile = sys.argv[1] outfile = sys.argv[2] fs = os.stat(infile).st_size +print(f"Current bootloader size before align is {fs} bytes") if fs > MAXSIZE: raise Exception( f"bootloader has to be smaller than {MAXSIZE} bytes (current size is {fs})" diff --git a/legacy/bootloader/usb.c b/legacy/bootloader/usb.c index 3253ac508e..e3290bd387 100644 --- a/legacy/bootloader/usb.c +++ b/legacy/bootloader/usb.c @@ -185,14 +185,11 @@ static int should_keep_storage(int old_was_signed, if (SIG_OK != old_was_signed) return SIG_FAIL; const image_header *new_hdr = (const image_header *)FW_HEADER; - // if the new header is unsigned, erase storage - if (SIG_OK != signatures_new_ok(new_hdr, NULL)) return SIG_FAIL; + // new header must be signed by v3 signmessage/verifymessage scheme + if (SIG_OK != signatures_ok(new_hdr, NULL, true)) return SIG_FAIL; // if the new header hashes don't match flash contents, erase storage if (SIG_OK != check_firmware_hashes(new_hdr)) return SIG_FAIL; - // going from old-style header to new-style is always an upgrade, keep storage - if (firmware_present_old()) return SIG_OK; - // if the current fix_version is higher than the new one, erase storage if (version_compare(new_hdr->version, fix_version_current) < 0) { return SIG_FAIL; @@ -279,12 +276,10 @@ static void rx_callback(usbd_device *dev, uint8_t ep) { if (firmware_present_new()) { const image_header *hdr = (const image_header *)FLASH_PTR(FLASH_FWHEADER_START); + // previous firmware was signed either v2 or v3 scheme old_was_signed = - signatures_new_ok(hdr, NULL) & check_firmware_hashes(hdr); + signatures_match(hdr, NULL) & check_firmware_hashes(hdr); fix_version_current = hdr->fix_version; - } else if (firmware_present_old()) { - old_was_signed = signatures_old_ok(); - fix_version_current = 0; } else { old_was_signed = SIG_FAIL; fix_version_current = 0xffffffff; @@ -405,7 +400,8 @@ static void rx_callback(usbd_device *dev, uint8_t ep) { } flash_state = STATE_CHECK; const image_header *hdr = (const image_header *)FW_HEADER; - if (SIG_OK != signatures_new_ok(hdr, NULL)) { + // allow only v3 signmessage/verifymessage signature for new FW + if (SIG_OK != signatures_ok(hdr, NULL, true)) { send_msg_buttonrequest_firmwarecheck(dev); return; } @@ -420,7 +416,8 @@ static void rx_callback(usbd_device *dev, uint8_t ep) { bool hash_check_ok; // show fingerprint of unsigned firmware - if (SIG_OK != signatures_new_ok(hdr, NULL)) { + // allow only v3 signmessage/verifymessage signatures + if (SIG_OK != signatures_ok(hdr, NULL, true)) { if (msg_id != 0x001B) { // ButtonAck message (id 27) return; } diff --git a/legacy/debug_signing/README.md b/legacy/debug_signing/README.md new file mode 100644 index 0000000000..7b4044e708 --- /dev/null +++ b/legacy/debug_signing/README.md @@ -0,0 +1,77 @@ +# Debugging T1 signatures + +## Signing with the "SignMessage" (v3) method +1. T1 firmware+bootloader must be built with `DEBUG_T1_SIGNATURES=1` to be able to debug them +1. Load signing device or emulator (must have `PYOPT=0` for core or `DEBUG_LINK=1` + for T1 legacy) with: + `trezorctl device load -m "table table table table table table table table table table table advance"` +1. **FW header hash is different from whole FW hash in the one output by cibuild** +1. Run the emulator or device (make sure not to confuse which are you using for signing) +1. Run `firmware_hash_sign_trezor.py ../firmware/trezor.bin ../firmware/trezor.bin.signed` +1. Accept 3 signature requests on signing device +1. This will show you a list of 3 signatures for 3 keys +1. It will output `../firmware.trezor.bin.signed` + +By default the scripts uses the `[1, 2, 3]` sigindices, you can modify `sig_indices` +inside to have different order or different keys (1 <= index <= 5 ) + +Update FW on T1 either via `trezorctl device firmware-update` or +`make flash_firmware_jlink`. + +## Signing with the v2 method (called "new" for confusing historical reasons) + +This method is currently (Oct 2022) used for signing official T1 firmwares. +To debug it, you also need `DEBUG_T1_SIGNATURES=1` build (bootloader and FW) + +Use this to sign FW: + + sign_firmware_v2_signature.py ../firmware/trezor.bin ../firmware/trezor.bin.signed.v2 + +## Notes on signatures patching + +`fill_t1_fw_signatures.py` script will allow you to paste wrong signatures +because that is also needed for testing. For example you may repeat +sigindex or use wrong signature (as long as it's some 64 bytes) + +Using e.g. this signatures generated by `firmware_hash_sign_trezor` +1. Copy three of the signatures and their indices into `trezor.bin.signatures` file, e.g. +``` +1 bc8ed893fedc088ea4b45f775ea62ef84d8113a6c0f2d88d0fb6b8f4c26549eb02e88dffa3c06517729ce5b41da3678d88ac4a7ce3b0ad05a1ee0507f7165dd3 +2 58f89a229b1d47011bd7771395c20bdce461bde2f150331e26a4cfc58456bdb0456e886f1d558b47f80982ec80dff941028fb4b1ef05e79fa32b6298dbf0bc5f +4 2a5ca0d3f7cad6b440a417779942158d70442e2ccd48875131d83a1644ae00022c531590a605d2ad415d778afda8b8118b47e4c47442014be64e90fa09b3a4ab +``` + +Finally use this file to patch signatures into the unsigned `trezor.bin`: + + fill_t1_fw_signatures.py firmware/trezor.bin trezor.bin.signatures + +Example output for this hash: + +``` +Loaded FW image with header hash 9e82a06e05a73b6fc5236508c3d1f3cdd15868523191783cfa2bda78d6e349c6 +Parsing sig line 1 - 1 bc8ed893fedc088ea4b45f775ea62ef84d8113a6c0f2d88d0fb6b8f4c26549eb02e88dffa3c06517729ce5b41da3678d88ac4a7ce3b0ad05a1ee0507f7165dd3 + +Parsing sig line 2 - 2 58f89a229b1d47011bd7771395c20bdce461bde2f150331e26a4cfc58456bdb0456e886f1d558b47f80982ec80dff941028fb4b1ef05e79fa32b6298dbf0bc5f + +Parsing sig line 3 - 4 2a5ca0d3f7cad6b440a417779942158d70442e2ccd48875131d83a1644ae00022c531590a605d2ad415d778afda8b8118b47e4c47442014be64e90fa09b3a4ab + +Patching sigindex 1 at offset 736 +Patching signature bc8ed893fedc088ea4b45f775ea62ef84d8113a6c0f2d88d0fb6b8f4c26549eb02e88dffa3c06517729ce5b41da3678d88ac4a7ce3b0ad05a1ee0507f7165dd3 at offset 544 +Patching sigindex 2 at offset 737 +Patching signature 58f89a229b1d47011bd7771395c20bdce461bde2f150331e26a4cfc58456bdb0456e886f1d558b47f80982ec80dff941028fb4b1ef05e79fa32b6298dbf0bc5f at offset 608 +Patching sigindex 4 at offset 738 +Patching signature 2a5ca0d3f7cad6b440a417779942158d70442e2ccd48875131d83a1644ae00022c531590a605d2ad415d778afda8b8118b47e4c47442014be64e90fa09b3a4ab at offset 672 +Writing output signed FW file firmware/trezor.bin.signed +``` + +## Manually verifying signature + +Use `firmware_hash_verify.py`: + +* arg 1 - hex digest of firmware header with zeroed sigslots +* arg 2 - public key (compressed or uncompressed) +* arg 3 - signature 64 bytes in hex + +``` +./firmware_hash_verify.py 0029608e2c879b6f6f636faba08d3434319e4694f6e0cdd626e4216640c403a1 02e62488c7a4aee638457f8b6afdb6dc41993971b5175dd4b941e14441c2df22b2 1ce267b37d712776f856ace87c8633413699b7212c63577fcac654c91ae9d35347f208fc7fb6d2315514a111c3d951a2c6451cc2a6178bdf12dc0c76406a6c08 +``` diff --git a/legacy/debug_signing/fill_t1_fw_signatures.py b/legacy/debug_signing/fill_t1_fw_signatures.py new file mode 100755 index 0000000000..97f1ceec7e --- /dev/null +++ b/legacy/debug_signing/fill_t1_fw_signatures.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +import sys +from hashlib import sha256 + + +class Signatures: + # offsets from T1 firmware hash + sig_offsets = [544, 608, 672] + sigindex_offsets = [736, 737, 738] + signature_pairs = [] # list of tupes (int, bytes) + + def __init__(self, filename): + """Load FW, zero out signature fiels, compute header hash""" + self.fw_image = None # mutable bytearray + self.load_fw(filename) + self.header_hash = sha256(self.get_header()).digest() + print(f"Loaded FW image with header hash {self.header_hash_hex()}") + + def load_fw(self, filename): + """Load FW and zero out signature fiels""" + with open(filename, "rb") as f: + data = f.read() + self.fw_image = bytearray(data) + self.zero_sig_fields() + + def zero_sig_fields(self): + """Zero out signature fields to be able to compute header hash""" + for i in range(3): + sigindex_ofs = self.sigindex_offsets[i] + sig_ofs = self.sig_offsets[i] + self.fw_image[sigindex_ofs] = 0 + + self.fw_image[sig_ofs : sig_ofs + 64] = b"\x00" * 64 + + def header_hash_hex(self): + return self.header_hash.hex() + + def get_header(self): + """Return header with zeroed out signatures as copy""" + return bytes(self.fw_image[:1024]) + + def patch_signatures(self): + """ + Patch signatures from signature_pairs. + Requires filling signature_pairs beforehand. + """ + assert len(self.signature_pairs) == 3 + + for i in range(3): + sigindex_ofs = self.sigindex_offsets[i] + sig_ofs = self.sig_offsets[i] + (sigindex, sig) = self.signature_pairs[i] + + print(f"Patching sigindex {sigindex} at offset {sigindex_ofs}") + assert 1 <= sigindex <= 5 + self.fw_image[sigindex_ofs] = sigindex + + print(f"Patching signature {sig.hex()} at offset {sig_ofs}") + assert len(sig) == 64 + self.fw_image[sig_ofs : sig_ofs + 64] = sig + + def write_output_fw(self, filename): + print(f"Writing output signed FW file {filename}") + with open(filename, "wb") as signed_fw_file: + signed_fw_file.write(self.fw_image) + + +if __name__ == "__main__": + # arg1 - unsigned trezor.bin FW + # arg2 - list of 3 signatures and indexes in this format (split by single space): + # index_num signature + # e.g. + # 1 adec956df6282c15ee4344b4cf6edbe435ed4bb13b2b7bebb9920f3d1c4a791a446e492f3ff9b86ca43f28cfce1be97c4eefa65e505e8a936876f01833366d5d + + in_fw_fname = sys.argv[1] + signatures_fname = sys.argv[2] + + signatures = Signatures(in_fw_fname) + i = 0 + for line in open(signatures_fname): + i += 1 + print(f"Parsing sig line {i} - {line}") + idx, sig = line.rstrip().split(" ") + idx = int(idx) + sig = bytes.fromhex(sig) + assert idx in range(1, 6) # 1 <= idx <= 5 + assert len(sig) == 64 + signatures.signature_pairs.append((idx, sig)) + + out_fw_name = in_fw_fname + ".signed" + signatures.patch_signatures() + signatures.write_output_fw(out_fw_name) diff --git a/legacy/debug_signing/firmware_hash_sign_trezor.py b/legacy/debug_signing/firmware_hash_sign_trezor.py new file mode 100755 index 0000000000..b662565a1d --- /dev/null +++ b/legacy/debug_signing/firmware_hash_sign_trezor.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +import sys + +from fill_t1_fw_signatures import Signatures +from trezorlib.btc import get_public_node, sign_message +from trezorlib.client import get_default_client +from trezorlib.tools import parse_path + +# arg1 is input trezor.bin filename to be signed +# arg2 is output filename, if omitted, will use input file + ".signed" +client = get_default_client() +in_fw_fname = sys.argv[1] +try: + out_fw_fname = sys.argv[2] +except IndexError: + out_fw_fname = in_fw_fname + ".signed" + +# These 3 keys will be used in this order to sign the FW +# each index can be >= 1 and <= 5 +sig_indices = [1, 2, 3] + +print(f"Input trezor.bin file: {in_fw_fname}") +print(f"Output signed trezor.bin file: {out_fw_fname}") + +signatures = Signatures(in_fw_fname) +digest = signatures.header_hash +assert len(digest) == 32 + +for i in sig_indices: + index = i - 1 # in FW indices are indexed from 1, 0 means none + print(f"--- Key {index}, sigindex {i}") + path_text = f"m/44'/0'/{index}'/0/0" + ADDRESS_N = parse_path(path_text) + print("Addres_n", path_text, ADDRESS_N) + + node = get_public_node(client, ADDRESS_N) + print("Public key:", node.node.public_key.hex()) + print("xpub:", node.xpub) + + signature = sign_message(client, "Bitcoin", ADDRESS_N, digest) + sig_64bytes = signature.signature[ + 1: + ] # first byte stripped to match normal secp256k1 + assert len(sig_64bytes) == 64 + print("Signature:", sig_64bytes.hex()) + print("=================================") + + signatures.signature_pairs.append((i, sig_64bytes)) + +signatures.patch_signatures() +signatures.write_output_fw(out_fw_fname) diff --git a/legacy/debug_signing/firmware_hash_verify.py b/legacy/debug_signing/firmware_hash_verify.py new file mode 100755 index 0000000000..185746f0ad --- /dev/null +++ b/legacy/debug_signing/firmware_hash_verify.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +import sys +from hashlib import sha256 + +import ecdsa + +# arg 1 - hex digest of firmware header with zeroed sigslots +# arg 2 - public key (compressed or uncompressed) +# arg 3 - signature 64 bytes in hex +digest = bytes.fromhex(sys.argv[1]) +assert len(digest) == 32 +public_key = bytes.fromhex(sys.argv[2]) +sig = bytes.fromhex(sys.argv[3]) +assert len(sig) == 64 +# 0x18 - coin info, 0x20 - length of digest following +prefix = b"\x18Bitcoin Signed Message:\n\x20" +message_predigest = prefix + digest +message = sha256(message_predigest).digest() + +vk = ecdsa.VerifyingKey.from_string(public_key, curve=ecdsa.SECP256k1, hashfunc=sha256) +result = vk.verify(sig, message) +print("Signature verification result", result) diff --git a/legacy/debug_signing/sign_firmware_v2_signature.py b/legacy/debug_signing/sign_firmware_v2_signature.py new file mode 100755 index 0000000000..686ebfe968 --- /dev/null +++ b/legacy/debug_signing/sign_firmware_v2_signature.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +import os +import pprint +import sys +from hashlib import sha1, sha256 + +import ecdsa + +from fill_t1_fw_signatures import Signatures + +secret_keys_hex = [ + "ba7994923c91771ad77c483f7d2b41f5506b82aa900e6f12edeae96c5c9f8f66", + "81a825d359da7ec9534e6cf7dd190bdbad62e134265764a5ec3e63317b060a51", + "37107a021e50ca3571102691606083f6a8d9cd600e35cd2c8e8f7b87796a045b", + "5518381d95e93e8eb68a294354989906e3828f36b4556a2ad85d8333294eb1b7", + "1d1d34168760dec092c9ff89377d8659076d2dfd95e0281719c15f90d067e211", +] + +secret_keys = [ + ecdsa.SigningKey.from_string( + bytes.fromhex(sk), curve=ecdsa.SECP256k1, hashfunc=sha256 + ) + for sk in secret_keys_hex +] +public_keys = [sk.get_verifying_key() for sk in secret_keys] +public_keys_hex = [pk.to_string("compressed").hex() for pk in public_keys] + +# arg1 is input trezor.bin filename to be signed +# arg2 is output filename, if omitted, will use input file + ".signed" +in_fw_fname = sys.argv[1] +try: + out_fw_fname = sys.argv[2] +except IndexError: + out_fw_fname = in_fw_fname + ".signed" + +# These 3 keys will be used in this order to sign the FW +# each index can be >= 1 and <= 5 +sig_indices = [1, 2, 3] + +print(f"Input trezor.bin file: {in_fw_fname}") +print(f"Output signed trezor.bin file: {out_fw_fname}") + +# print("Public keys compressed:") +# pprint.pprint(public_keys_hex) + +# Should be these public keys +assert public_keys_hex == [ + "0391cdaf3cac08c2712ee1e88cd6d346eb2c798fdaf95c0eb6efeea0d7014dac87", + "02186ff4e2b08bc5ae0e21a508f1ced48ff451eab7d794deb0b7cbe8efd729aba7", + "0324009f0b398ca1c335fb17a1021c3f7fb0831ddb28348a4f058b149ea4c589a0", + "0366635d999417b65566866c65630d977a7ae723fe5f6c4cd17fa00f088ba184c1", + "03f36c7d0fb615ada43d7188580f15ebda22d6f6b9b1a92bff16c6937799dcbc66", +] + +print("Sanity check") +for (sk, pk_hex) in zip(secret_keys, public_keys_hex): + pk = ecdsa.VerifyingKey.from_string( + bytes.fromhex(pk_hex), curve=ecdsa.SECP256k1, hashfunc=sha256 + ) + message = bytes(os.urandom(64)) + + # These should work + sig = sk.sign_deterministic(message, hashfunc=sha256) + pk.verify(sig, message, hashfunc=sha256) # throws exception if wrong + + # These should fail + try: + sig = sk.sign_deterministic(message, hashfunc=sha1) + pk.verify(sig, message, hashfunc=sha256) # should throw + raise RuntimeError("These should not have matched!") + except ecdsa.keys.BadSignatureError: + # print("Bad sig check fail test ok") + pass # fine, should have failed + +print("Sanity check successful") + +signatures = Signatures(in_fw_fname) +digest = signatures.header_hash +header = signatures.get_header() +assert len(digest) == 32 +assert sha256(header).digest() == digest + +print("Full header hex") +pprint.pprint(header.hex()) + +for i in sig_indices: + index = i - 1 # in FW indices are indexed from 1, 0 means none + print(f"--- Key {index}, sigindex {i}") + sk = secret_keys[index] + sig_64bytes = sk.sign_deterministic(header, hashfunc=sha256) + assert len(sig_64bytes) == 64 + print("Signature:", sig_64bytes.hex()) + pk = ecdsa.VerifyingKey.from_string( + bytes.fromhex(public_keys_hex[index]), curve=ecdsa.SECP256k1, hashfunc=sha256 + ) + pk.verify(sig_64bytes, header, hashfunc=sha256) # throws exception if wrong + print(f"Public key {public_keys_hex[index]}") + print("Verified created sig with public key") + print("=================================") + + signatures.signature_pairs.append((i, sig_64bytes)) + +signatures.patch_signatures() +signatures.write_output_fw(out_fw_fname) diff --git a/legacy/firmware/.changelog.d/2568.added b/legacy/firmware/.changelog.d/2568.added new file mode 100644 index 0000000000..77b0e992fa --- /dev/null +++ b/legacy/firmware/.changelog.d/2568.added @@ -0,0 +1 @@ +T1 bootloader: verify firmware signatures based on SignMessage, add signature debugging diff --git a/legacy/firmware/fsm_msg_common.h b/legacy/firmware/fsm_msg_common.h index 67f88561b8..16c712927b 100644 --- a/legacy/firmware/fsm_msg_common.h +++ b/legacy/firmware/fsm_msg_common.h @@ -24,7 +24,8 @@ bool get_features(Features *resp) { #else const image_header *hdr = (const image_header *)FLASH_PTR(FLASH_FWHEADER_START); - if (SIG_OK == signatures_new_ok(hdr, NULL)) { + // only v3 signatures count as signed + if (SIG_OK == signatures_ok(hdr, NULL, true)) { strlcpy(resp->fw_vendor, "SatoshiLabs", sizeof(resp->fw_vendor)); } else { strlcpy(resp->fw_vendor, "UNSAFE, DO NOT USE!", sizeof(resp->fw_vendor)); diff --git a/legacy/fw_signatures.c b/legacy/fw_signatures.c index 29cb37a30e..d940dcc6c6 100644 --- a/legacy/fw_signatures.c +++ b/legacy/fw_signatures.c @@ -26,12 +26,69 @@ #include "secp256k1.h" #include "sha2.h" -const uint32_t FIRMWARE_MAGIC_OLD = 0x525a5254; // TRZR const uint32_t FIRMWARE_MAGIC_NEW = 0x465a5254; // TRZF +/* + * There are 3 schemes in history of T1, for clarity naming: + * + * - v1 - previously called "old" with TRZR magic header (no longer here) + * - v2 - previously called "new" with TRZF magic header + * - v3 - the latest scheme using Trezor's SignMessage and VerifyMessage + * style signatures + * + * See `debug_signing/README.md` and the scripts there for signatures debug. + * + * Latest scheme v3 ref: https://github.com/trezor/trezor-firmware/issues/2513 + */ #define PUBKEYS 5 -static const uint8_t * const pubkey[PUBKEYS] = { +#if DEBUG_T1_SIGNATURES + +// Make build explode if combining debug sigs with production +#if PRODUCTION +#error "Can't have production device with debug keys! Build aborted" +#endif + +// These are **only** for debugging signatures with SignMessage +// Use this mnemonic for testing signing: +// "table table table table table table table table table table table advance" +// the "SignMessage"-style public keys, third signing scheme +// See legacy/debug_signing/README.md +static const uint8_t * const pubkey_v3[PUBKEYS] = { + (const uint8_t *)"\x03\x73\x08\xe1\x40\x77\x16\x1c\x36\x5d\xea\x0f\x5c\x80\xaa\x6c\x5d\xba\x34\x71\x9e\x82\x5b\xd2\x3a\xe5\xf7\xe7\xd2\x98\x8a\xdb\x0f", + (const uint8_t *)"\x03\x9c\x1b\x24\x60\xe3\x43\x71\x2e\x98\x2e\x07\x32\xe7\xed\x17\xf6\x0d\xe4\xc9\x33\x06\x5b\x71\x70\xd9\x9c\x6e\x7f\xe7\xcc\x7f\x4b", + (const uint8_t *)"\x03\x15\x2b\x37\xfd\xf1\x26\x11\x12\x74\xc8\x94\xc3\x48\xdc\xc9\x75\xb5\x7c\x11\x5e\xe2\x4c\xeb\x19\xb5\x19\x0a\xc7\xf7\xb6\x51\x73", + (const uint8_t *)"\x02\x83\x91\x8a\xbf\x1b\x6e\x1d\x2a\x1d\x08\xea\x29\xc9\xd2\xae\x2e\x97\xe3\xc0\x1f\x4e\xa8\x59\x2b\x08\x8d\x28\x03\x5b\x44\x4e\xd0", + (const uint8_t *)"\x03\xc6\x83\x63\x85\x07\x8a\x18\xe8\x1d\x74\x77\x68\x1e\x0d\x30\x86\x66\xf3\x99\x59\x4b\xe9\xe8\xab\xcb\x45\x58\xa6\xe2\x47\x32\xfc" +}; + +// the "new", or second signing scheme keys + +/* + Debug private keys for v2 (previously called "new") scheme + corresponding to pubkeys below as python hexstring array: + + ['ba7994923c91771ad77c483f7d2b41f5506b82aa900e6f12edeae96c5c9f8f66', + '81a825d359da7ec9534e6cf7dd190bdbad62e134265764a5ec3e63317b060a51', + '37107a021e50ca3571102691606083f6a8d9cd600e35cd2c8e8f7b87796a045b', + '5518381d95e93e8eb68a294354989906e3828f36b4556a2ad85d8333294eb1b7', + '1d1d34168760dec092c9ff89377d8659076d2dfd95e0281719c15f90d067e211'] + */ +static const uint8_t * const pubkey_v2[PUBKEYS] = { + (const uint8_t *)"\x03\x91\xcd\xaf\x3c\xac\x08\xc2\x71\x2e\xe1\xe8\x8c\xd6\xd3\x46\xeb\x2c\x79\x8f\xda\xf9\x5c\x0e\xb6\xef\xee\xa0\xd7\x01\x4d\xac\x87", + (const uint8_t *)"\x02\x18\x6f\xf4\xe2\xb0\x8b\xc5\xae\x0e\x21\xa5\x08\xf1\xce\xd4\x8f\xf4\x51\xea\xb7\xd7\x94\xde\xb0\xb7\xcb\xe8\xef\xd7\x29\xab\xa7", + (const uint8_t *)"\x03\x24\x00\x9f\x0b\x39\x8c\xa1\xc3\x35\xfb\x17\xa1\x02\x1c\x3f\x7f\xb0\x83\x1d\xdb\x28\x34\x8a\x4f\x05\x8b\x14\x9e\xa4\xc5\x89\xa0", + (const uint8_t *)"\x03\x66\x63\x5d\x99\x94\x17\xb6\x55\x66\x86\x6c\x65\x63\x0d\x97\x7a\x7a\xe7\x23\xfe\x5f\x6c\x4c\xd1\x7f\xa0\x0f\x08\x8b\xa1\x84\xc1", + (const uint8_t *)"\x03\xf3\x6c\x7d\x0f\xb6\x15\xad\xa4\x3d\x71\x88\x58\x0f\x15\xeb\xda\x22\xd6\xf6\xb9\xb1\xa9\x2b\xff\x16\xc6\x93\x77\x99\xdc\xbc\x66" +}; +#else + +// These public keys are production keys +// - used in production devices +// - used in debug non-production builds for QA testing + +// the "SignMessage"-style public keys, third signing scheme +static const uint8_t * const pubkey_v3[PUBKEYS] = { (const uint8_t *)"\x02\xd5\x71\xb7\xf1\x48\xc5\xe4\x23\x2c\x38\x14\xf7\x77\xd8\xfa\xea\xf1\xa8\x42\x16\xc7\x8d\x56\x9b\x71\x04\x1f\xfc\x76\x8a\x5b\x2d", (const uint8_t *)"\x03\x63\x27\x9c\x0c\x08\x66\xe5\x0c\x05\xc7\x99\xd3\x2b\xd6\xba\xb0\x18\x8b\x6d\xe0\x65\x36\xd1\x10\x9d\x2e\xd9\xce\x76\xcb\x33\x5c", (const uint8_t *)"\x02\x43\xae\xdb\xb6\xf7\xe7\x1c\x56\x3f\x8e\xd2\xef\x64\xec\x99\x81\x48\x25\x19\xe7\xef\x4f\x4a\xa9\x8b\x27\x85\x4e\x8c\x49\x12\x6d", @@ -39,6 +96,16 @@ static const uint8_t * const pubkey[PUBKEYS] = { (const uint8_t *)"\x03\x73\x84\xc5\x1a\xe8\x1a\xdd\x0a\x52\x3a\xdb\xb1\x86\xc9\x1b\x90\x6f\xfb\x64\xc2\xc7\x65\x80\x2b\xf2\x6d\xbd\x13\xbd\xf1\x2c\x31" }; +// the "new", or second signing scheme keys +static const uint8_t * const pubkey_v2[PUBKEYS] = { + (const uint8_t *)"\x02\xd5\x71\xb7\xf1\x48\xc5\xe4\x23\x2c\x38\x14\xf7\x77\xd8\xfa\xea\xf1\xa8\x42\x16\xc7\x8d\x56\x9b\x71\x04\x1f\xfc\x76\x8a\x5b\x2d", + (const uint8_t *)"\x03\x63\x27\x9c\x0c\x08\x66\xe5\x0c\x05\xc7\x99\xd3\x2b\xd6\xba\xb0\x18\x8b\x6d\xe0\x65\x36\xd1\x10\x9d\x2e\xd9\xce\x76\xcb\x33\x5c", + (const uint8_t *)"\x02\x43\xae\xdb\xb6\xf7\xe7\x1c\x56\x3f\x8e\xd2\xef\x64\xec\x99\x81\x48\x25\x19\xe7\xef\x4f\x4a\xa9\x8b\x27\x85\x4e\x8c\x49\x12\x6d", + (const uint8_t *)"\x02\x87\x7c\x39\xfd\x7c\x62\x23\x7e\x03\x82\x35\xe9\xc0\x75\xda\xb2\x61\x63\x0f\x78\xee\xb8\xed\xb9\x24\x87\x15\x9f\xff\xed\xfd\xf6", + (const uint8_t *)"\x03\x73\x84\xc5\x1a\xe8\x1a\xdd\x0a\x52\x3a\xdb\xb1\x86\xc9\x1b\x90\x6f\xfb\x64\xc2\xc7\x65\x80\x2b\xf2\x6d\xbd\x13\xbd\xf1\x2c\x31" +}; +#endif + #define SIGNATURES 3 #define FLASH_META_START 0x08008000 @@ -51,62 +118,16 @@ static const uint8_t * const pubkey[PUBKEYS] = { #define FLASH_META_SIG2 (FLASH_META_START + 0x0080) #define FLASH_META_SIG3 (FLASH_META_START + 0x00C0) -bool firmware_present_old(void) { - if (memcmp(FLASH_PTR(FLASH_META_START), &FIRMWARE_MAGIC_OLD, - 4)) { // magic does not match - return false; - } - if (*((const uint32_t *)FLASH_PTR(FLASH_META_CODELEN)) < - 8192) { // firmware reports smaller size than 8192 - return false; - } - if (*((const uint32_t *)FLASH_PTR(FLASH_META_CODELEN)) > - FLASH_APP_LEN) { // firmware reports bigger size than flash size - return false; - } - - return true; -} - -int signatures_old_ok(void) { - const uint32_t codelen = *((const uint32_t *)FLASH_META_CODELEN); - const uint8_t sigindex1 = *((const uint8_t *)FLASH_META_SIGINDEX1); - const uint8_t sigindex2 = *((const uint8_t *)FLASH_META_SIGINDEX2); - const uint8_t sigindex3 = *((const uint8_t *)FLASH_META_SIGINDEX3); - - if (codelen > FLASH_APP_LEN) { - return false; - } - - uint8_t hash[32] = {0}; - sha256_Raw(FLASH_PTR(FLASH_OLD_APP_START), codelen, hash); - - if (sigindex1 < 1 || sigindex1 > PUBKEYS) return SIG_FAIL; // invalid index - if (sigindex2 < 1 || sigindex2 > PUBKEYS) return SIG_FAIL; // invalid index - if (sigindex3 < 1 || sigindex3 > PUBKEYS) return SIG_FAIL; // invalid index - - if (sigindex1 == sigindex2) return SIG_FAIL; // duplicate use - if (sigindex1 == sigindex3) return SIG_FAIL; // duplicate use - if (sigindex2 == sigindex3) return SIG_FAIL; // duplicate use - - if (0 != ecdsa_verify_digest(&secp256k1, pubkey[sigindex1 - 1], - (const uint8_t *)FLASH_META_SIG1, - hash)) { // failure - return SIG_FAIL; - } - if (0 != ecdsa_verify_digest(&secp256k1, pubkey[sigindex2 - 1], - (const uint8_t *)FLASH_META_SIG2, - hash)) { // failure - return SIG_FAIL; - } - if (0 != ecdsa_verify_digest(&secp256k1, pubkey[sigindex3 - 1], - (const uint8_t *)FLASH_META_SIG3, - hash)) { // failure - return SIG_FAIL; - } - - return SIG_OK; -} +/* + * 0x18 in message prefix is coin info, 0x20 is the length of hash + * that follows. + * See `core/src/apps/bitcoin/sign_message.py`. + */ +#define VERIFYMESSAGE_PREFIX \ + ("\x18" \ + "Bitcoin Signed Message:\n\x20") +#define PREFIX_LENGTH (sizeof(VERIFYMESSAGE_PREFIX) - 1) +#define SIGNED_LENGTH (PREFIX_LENGTH + 32) void compute_firmware_fingerprint(const image_header *hdr, uint8_t hash[32]) { image_header copy = {0}; @@ -120,6 +141,21 @@ void compute_firmware_fingerprint(const image_header *hdr, uint8_t hash[32]) { sha256_Raw((const uint8_t *)©, sizeof(image_header), hash); } +void compute_firmware_fingerprint_for_verifymessage(const image_header *hdr, + uint8_t hash[32]) { + uint8_t prefixed_header[SIGNED_LENGTH] = VERIFYMESSAGE_PREFIX; + uint8_t header_hash[32]; + uint8_t hash_before_double_hashing[32]; + compute_firmware_fingerprint(hdr, header_hash); + memcpy(prefixed_header + PREFIX_LENGTH, header_hash, sizeof(header_hash)); + sha256_Raw(prefixed_header, sizeof(prefixed_header), + hash_before_double_hashing); + // We need to do hash the previous result again because SignMessage + // computes it this way, see `core/src/apps/bitcoin/sign_message.py` + sha256_Raw(hash_before_double_hashing, sizeof(hash_before_double_hashing), + hash); +} + bool firmware_present_new(void) { const image_header *hdr = (const image_header *)FLASH_PTR(FLASH_FWHEADER_START); @@ -135,9 +171,18 @@ bool firmware_present_new(void) { return true; } -int signatures_new_ok(const image_header *hdr, uint8_t store_fingerprint[32]) { +int signatures_ok(const image_header *hdr, uint8_t store_fingerprint[32], + bool use_verifymessage) { uint8_t hash[32] = {0}; - compute_firmware_fingerprint(hdr, hash); + // which set of public keys depend on scheme + const uint8_t *const *pubkey_ptr = NULL; + if (use_verifymessage) { + pubkey_ptr = pubkey_v3; + compute_firmware_fingerprint_for_verifymessage(hdr, hash); + } else { + pubkey_ptr = pubkey_v2; + compute_firmware_fingerprint(hdr, hash); + } if (store_fingerprint) { memcpy(store_fingerprint, hash, 32); @@ -154,15 +199,15 @@ int signatures_new_ok(const image_header *hdr, uint8_t store_fingerprint[32]) { if (hdr->sigindex1 == hdr->sigindex3) return SIG_FAIL; // duplicate use if (hdr->sigindex2 == hdr->sigindex3) return SIG_FAIL; // duplicate use - if (0 != ecdsa_verify_digest(&secp256k1, pubkey[hdr->sigindex1 - 1], + if (0 != ecdsa_verify_digest(&secp256k1, pubkey_ptr[hdr->sigindex1 - 1], hdr->sig1, hash)) { // failure return SIG_FAIL; } - if (0 != ecdsa_verify_digest(&secp256k1, pubkey[hdr->sigindex2 - 1], + if (0 != ecdsa_verify_digest(&secp256k1, pubkey_ptr[hdr->sigindex2 - 1], hdr->sig2, hash)) { // failure return SIG_FAIL; } - if (0 != ecdsa_verify_digest(&secp256k1, pubkey[hdr->sigindex3 - 1], + if (0 != ecdsa_verify_digest(&secp256k1, pubkey_ptr[hdr->sigindex3 - 1], hdr->sig3, hash)) { // failure return SIG_FAIL; } @@ -170,6 +215,21 @@ int signatures_new_ok(const image_header *hdr, uint8_t store_fingerprint[32]) { return SIG_OK; } +int signatures_match(const image_header *hdr, uint8_t store_fingerprint[32]) { + int result = 0; + // Return success if v3 ("verify message") or the v2 ("new") style matches. + // Use XOR to always force computing both signatures to avoid potential + // timing side channels. + // Return only the hash for the v2 computation so that it is + // the same shown in previous bootloader. + result ^= signatures_ok(hdr, store_fingerprint, false); + result ^= signatures_ok(hdr, NULL, true); + if (result != SIG_OK) { + return SIG_FAIL; + } + return SIG_OK; +} + int mem_is_empty(const uint8_t *src, uint32_t len) { for (uint32_t i = 0; i < len; i++) { if (src[i]) return 0; diff --git a/legacy/fw_signatures.h b/legacy/fw_signatures.h index 1148dd2b47..8d32e9fa7b 100644 --- a/legacy/fw_signatures.h +++ b/legacy/fw_signatures.h @@ -23,7 +23,6 @@ #include #include -extern const uint32_t FIRMWARE_MAGIC_OLD; // TRZR extern const uint32_t FIRMWARE_MAGIC_NEW; // TRZF #define SIG_OK 0x5A3CA5C3 @@ -59,11 +58,80 @@ typedef struct { #define FW_CHUNK_SIZE 65536 +/** + * Check if firmware with FIRMWARE_MAGIC_NEW is installed + * @return true if magic present with some size checks + */ bool firmware_present_new(void); + +/** + * Compute fingerprint for given header. Fingerprint is done + * from header that has signature and sigindex fields zeroed. + * + * The "v2" scheme is used. This is what is shown as firmware + * fingerprint on device. + * + * @param hdr header + * @param hash store resulting hash here + */ void compute_firmware_fingerprint(const image_header *hdr, uint8_t hash[32]); -int signatures_new_ok(const image_header *hdr, uint8_t store_fingerprint[32]); + +/** + * Compute fingerprint for given header. Fingerprint is done + * from header that has signature and sigindex fields zeroed. + * + * Then it's prefixed using the SignMessage/VerifyMessage method + * as described here: + * + * https://github.com/trezor/trezor-firmware/issues/2513 + * + * @param hdr header + * @param hash store resulting hash here + */ +void compute_firmware_fingerprint_for_verifymessage(const image_header *hdr, + uint8_t hash[32]); + +/** + * Check if header is signed by v2 or v3 scheme based on `use_verifymessage`. + * + * Both are 3-of-5 scheme, where 3 signatures specified by sigindex fields + * must match corresponding secp256k1 pubkey. + * + * @param hdr header to check + * @param store_fingerprint if non-NULL, store hash computed with chosen method + * here + * @param use_verifymessage false - use v2 signature scheme, true - use v3 + * SignMessage/VerifyMessage scheme + * @return SIG_OK or SIG_FAIL + */ +int signatures_ok(const image_header *hdr, uint8_t store_fingerprint[32], + bool use_verifymessage); +/** + * Check is either v2 or v3 signature of header is valid. + * + * Stored fingerprint is the "of v2 scheme" which we still display as hash + * and use as "firmware hash". + * + * @param hdr header to check + * @param store_fingerprint if non-NULL, store v2 fingerprint here (not v3) + * @return SIG_OK or SIG_FAIL + */ +int signatures_match(const image_header *hdr, uint8_t store_fingerprint[32]); + +/** + * Check hashes of FW chunks according to what header says they should be. + * @param hdr header with chunk hashes + * @return SIG_OK or SIG_FAIL + */ int check_firmware_hashes(const image_header *hdr); +/** + * Check that block of memory is zeroed. Not constant-time. + * + * @param src start pointer + * @param len length in bytes + * @return 0 for false or 1 for true + */ int mem_is_empty(const uint8_t *src, uint32_t len); #endif