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
pull/2565/head
Ondrej Mikle 2 years ago
parent 1bad41ddb0
commit e2abd2a9ad

@ -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:

@ -0,0 +1 @@
T1 bootloader: verify firmware signatures based on SignMessage, add signature debugging

@ -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);
}

@ -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})"

@ -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;
}

@ -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
```

@ -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)

@ -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)

@ -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)

@ -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)

@ -0,0 +1 @@
T1 bootloader: verify firmware signatures based on SignMessage, add signature debugging

@ -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));

@ -26,18 +26,85 @@
#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",
(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"
};
// 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
@ -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 *)&copy, 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;

@ -23,7 +23,6 @@
#include <stdbool.h>
#include <stdint.h>
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

Loading…
Cancel
Save