2018-10-30 17:13:11 +00:00
|
|
|
#!/usr/bin/env python3
|
2014-10-23 16:09:41 +00:00
|
|
|
import argparse
|
|
|
|
import hashlib
|
|
|
|
import struct
|
2018-10-30 17:13:11 +00:00
|
|
|
|
2014-10-23 16:09:41 +00:00
|
|
|
import ecdsa
|
|
|
|
|
|
|
|
SLOTS = 3
|
|
|
|
|
|
|
|
pubkeys = {
|
2018-10-30 17:13:11 +00:00
|
|
|
1: "04d571b7f148c5e4232c3814f777d8faeaf1a84216c78d569b71041ffc768a5b2d810fc3bb134dd026b57e65005275aedef43e155f48fc11a32ec790a93312bd58",
|
|
|
|
2: "0463279c0c0866e50c05c799d32bd6bab0188b6de06536d1109d2ed9ce76cb335c490e55aee10cc901215132e853097d5432eda06b792073bd7740c94ce4516cb1",
|
|
|
|
3: "0443aedbb6f7e71c563f8ed2ef64ec9981482519e7ef4f4aa98b27854e8c49126d4956d300ab45fdc34cd26bc8710de0a31dbdf6de7435fd0b492be70ac75fde58",
|
|
|
|
4: "04877c39fd7c62237e038235e9c075dab261630f78eeb8edb92487159fffedfdf6046c6f8b881fa407c4a4ce6c28de0b19c1f4e29f1fcbc5a58ffd1432a3e0938a",
|
|
|
|
5: "047384c51ae81add0a523adbb186c91b906ffb64c2c765802bf26dbd13bdf12c319e80c2213a136c8ee03d7874fd22b70d68e7dee469decfbbb510ee9a460cda45",
|
2014-10-23 16:09:41 +00:00
|
|
|
}
|
|
|
|
|
2019-01-27 10:58:30 +00:00
|
|
|
FWHEADER_SIZE = 1024
|
|
|
|
SIGNATURES_START = 6 * 4 + 8 + 512
|
|
|
|
INDEXES_START = SIGNATURES_START + 3 * 64
|
2014-10-23 16:09:41 +00:00
|
|
|
|
2018-10-30 17:13:11 +00:00
|
|
|
|
2014-10-23 16:09:41 +00:00
|
|
|
def parse_args():
|
2018-10-30 17:13:11 +00:00
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
description="Commandline tool for signing Trezor firmware."
|
|
|
|
)
|
|
|
|
parser.add_argument("-f", "--file", dest="path", help="Firmware file to modify")
|
|
|
|
parser.add_argument(
|
|
|
|
"-s",
|
|
|
|
"--sign",
|
|
|
|
dest="sign",
|
|
|
|
action="store_true",
|
|
|
|
help="Add signature to firmware slot",
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"-p", "--pem", dest="pem", action="store_true", help="Use PEM instead of SECEXP"
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"-g",
|
|
|
|
"--generate",
|
|
|
|
dest="generate",
|
|
|
|
action="store_true",
|
|
|
|
help="Generate new ECDSA keypair",
|
|
|
|
)
|
2014-10-23 16:09:41 +00:00
|
|
|
|
|
|
|
return parser.parse_args()
|
|
|
|
|
2018-10-30 17:13:11 +00:00
|
|
|
|
2019-01-27 10:58:30 +00:00
|
|
|
def pad_to_size(data, size):
|
|
|
|
if len(data) > size:
|
|
|
|
raise ValueError("Chunk too big already")
|
|
|
|
if len(data) == size:
|
|
|
|
return data
|
|
|
|
return data + b"\xFF" * (size - len(data))
|
2014-10-23 16:09:41 +00:00
|
|
|
|
2019-01-27 10:58:30 +00:00
|
|
|
|
|
|
|
# see memory.h for details
|
|
|
|
|
|
|
|
|
|
|
|
def prepare_hashes(data):
|
|
|
|
# process chunks
|
|
|
|
start = 0
|
|
|
|
end = (64 - 1) * 1024
|
|
|
|
hashes = []
|
|
|
|
for i in range(16):
|
|
|
|
sector = data[start:end]
|
|
|
|
if len(sector) > 0:
|
|
|
|
chunk = pad_to_size(sector, end - start)
|
|
|
|
hashes.append(hashlib.sha256(chunk).digest())
|
|
|
|
else:
|
|
|
|
hashes.append(b"\x00" * 32)
|
|
|
|
start = end
|
|
|
|
end += 64 * 1024
|
|
|
|
return hashes
|
|
|
|
|
|
|
|
|
|
|
|
def check_hashes(data):
|
|
|
|
expected_hashes = data[0x20 : 0x20 + 16 * 32]
|
|
|
|
hashes = b""
|
|
|
|
for h in prepare_hashes(data[FWHEADER_SIZE:]):
|
|
|
|
hashes += h
|
|
|
|
|
|
|
|
if expected_hashes == hashes:
|
|
|
|
print("HASHES OK")
|
2014-10-23 16:09:41 +00:00
|
|
|
else:
|
2019-01-27 10:58:30 +00:00
|
|
|
print("HASHES NOT OK")
|
|
|
|
|
|
|
|
|
|
|
|
def update_hashes_in_header(data):
|
|
|
|
# Store hashes in the firmware header
|
|
|
|
data = bytearray(data)
|
|
|
|
o = 0
|
|
|
|
for h in prepare_hashes(data[FWHEADER_SIZE:]):
|
2019-04-18 14:27:27 +00:00
|
|
|
data[0x20 + o : 0x20 + o + 32] = h
|
2019-01-27 10:58:30 +00:00
|
|
|
o += 32
|
|
|
|
return bytes(data)
|
|
|
|
|
|
|
|
|
|
|
|
def get_header(data, zero_signatures=False):
|
|
|
|
if not zero_signatures:
|
|
|
|
return data[:FWHEADER_SIZE]
|
2014-10-23 16:09:41 +00:00
|
|
|
else:
|
2019-01-27 10:58:30 +00:00
|
|
|
data = bytearray(data[:FWHEADER_SIZE])
|
|
|
|
data[SIGNATURES_START : SIGNATURES_START + 3 * 64 + 3] = b"\x00" * (3 * 64 + 3)
|
|
|
|
return bytes(data)
|
2014-10-23 16:09:41 +00:00
|
|
|
|
|
|
|
|
2019-01-27 10:58:30 +00:00
|
|
|
def check_size(data):
|
|
|
|
size = struct.unpack("<L", data[12:16])[0]
|
|
|
|
assert size == len(data) - 1024
|
2018-10-30 17:13:11 +00:00
|
|
|
|
2019-04-18 14:27:27 +00:00
|
|
|
|
2014-10-23 16:09:41 +00:00
|
|
|
def check_signatures(data):
|
|
|
|
# Analyses given firmware and prints out
|
|
|
|
# status of included signatures
|
|
|
|
|
2019-01-27 10:58:30 +00:00
|
|
|
indexes = [x for x in data[INDEXES_START : INDEXES_START + SLOTS]]
|
2014-10-23 16:09:41 +00:00
|
|
|
|
2019-01-27 10:58:30 +00:00
|
|
|
to_sign = get_header(data, zero_signatures=True)
|
2014-10-23 16:09:41 +00:00
|
|
|
fingerprint = hashlib.sha256(to_sign).hexdigest()
|
|
|
|
|
2017-02-01 17:07:47 +00:00
|
|
|
print("Firmware fingerprint:", fingerprint)
|
2014-10-23 16:09:41 +00:00
|
|
|
|
|
|
|
used = []
|
|
|
|
for x in range(SLOTS):
|
2019-01-27 10:58:30 +00:00
|
|
|
signature = data[SIGNATURES_START + 64 * x : SIGNATURES_START + 64 * x + 64]
|
2014-10-23 16:09:41 +00:00
|
|
|
|
|
|
|
if indexes[x] == 0:
|
2021-09-27 10:13:51 +00:00
|
|
|
print(f"Slot #{x + 1}", "is empty")
|
2014-10-23 16:09:41 +00:00
|
|
|
else:
|
|
|
|
pk = pubkeys[indexes[x]]
|
2018-10-30 17:13:11 +00:00
|
|
|
verify = ecdsa.VerifyingKey.from_string(
|
|
|
|
bytes.fromhex(pk)[1:],
|
|
|
|
curve=ecdsa.curves.SECP256k1,
|
|
|
|
hashfunc=hashlib.sha256,
|
|
|
|
)
|
2014-10-23 16:09:41 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
verify.verify(signature, to_sign, hashfunc=hashlib.sha256)
|
|
|
|
|
|
|
|
if indexes[x] in used:
|
2021-09-27 10:13:51 +00:00
|
|
|
print(f"Slot #{x + 1} signature: DUPLICATE", signature.hex())
|
2014-10-23 16:09:41 +00:00
|
|
|
else:
|
|
|
|
used.append(indexes[x])
|
2021-09-27 10:13:51 +00:00
|
|
|
print(f"Slot #{x + 1} signature: VALID", signature.hex())
|
2014-10-23 16:09:41 +00:00
|
|
|
|
2019-04-18 14:27:27 +00:00
|
|
|
except Exception:
|
2021-09-27 10:13:51 +00:00
|
|
|
print(f"Slot #{x + 1} signature: INVALID", signature.hex())
|
2014-10-23 16:09:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
def modify(data, slot, index, signature):
|
2019-01-27 10:58:30 +00:00
|
|
|
data = bytearray(data)
|
|
|
|
# put index to data
|
|
|
|
data[INDEXES_START + slot - 1] = index
|
|
|
|
# put signature to data
|
|
|
|
data[SIGNATURES_START + 64 * (slot - 1) : SIGNATURES_START + 64 * slot] = signature
|
|
|
|
return bytes(data)
|
2014-10-23 16:09:41 +00:00
|
|
|
|
2018-10-30 17:13:11 +00:00
|
|
|
|
2014-10-23 16:09:41 +00:00
|
|
|
def sign(data, is_pem):
|
|
|
|
# Ask for index and private key and signs the firmware
|
|
|
|
|
2021-09-27 10:13:51 +00:00
|
|
|
slot = int(input(f"Enter signature slot (1-{SLOTS}): "))
|
2014-10-23 16:09:41 +00:00
|
|
|
if slot < 1 or slot > SLOTS:
|
|
|
|
raise Exception("Invalid slot")
|
|
|
|
|
|
|
|
if is_pem:
|
2017-02-01 17:07:47 +00:00
|
|
|
print("Paste ECDSA private key in PEM format and press Enter:")
|
|
|
|
print("(blank private key removes the signature on given index)")
|
2018-10-30 17:13:11 +00:00
|
|
|
pem_key = ""
|
2014-10-23 16:09:41 +00:00
|
|
|
while True:
|
2019-01-27 10:58:30 +00:00
|
|
|
key = input()
|
2014-10-23 16:09:41 +00:00
|
|
|
pem_key += key + "\n"
|
2018-10-30 17:13:11 +00:00
|
|
|
if key == "":
|
2014-10-23 16:09:41 +00:00
|
|
|
break
|
2018-10-30 17:13:11 +00:00
|
|
|
if pem_key.strip() == "":
|
2014-10-23 16:09:41 +00:00
|
|
|
# Blank key,let's remove existing signature from slot
|
2019-01-27 10:58:30 +00:00
|
|
|
return modify(data, slot, 0, b"\x00" * 64)
|
2014-10-23 16:09:41 +00:00
|
|
|
key = ecdsa.SigningKey.from_pem(pem_key)
|
|
|
|
else:
|
2017-02-01 17:07:47 +00:00
|
|
|
print("Paste SECEXP (in hex) and press Enter:")
|
|
|
|
print("(blank private key removes the signature on given index)")
|
2019-01-27 10:58:30 +00:00
|
|
|
secexp = input()
|
2018-10-30 17:13:11 +00:00
|
|
|
if secexp.strip() == "":
|
2014-10-23 16:09:41 +00:00
|
|
|
# Blank key,let's remove existing signature from slot
|
2019-01-27 10:58:30 +00:00
|
|
|
return modify(data, slot, 0, b"\x00" * 64)
|
2018-10-30 17:13:11 +00:00
|
|
|
key = ecdsa.SigningKey.from_secret_exponent(
|
|
|
|
secexp=int(secexp, 16),
|
|
|
|
curve=ecdsa.curves.SECP256k1,
|
|
|
|
hashfunc=hashlib.sha256,
|
|
|
|
)
|
2014-10-23 16:09:41 +00:00
|
|
|
|
2019-01-27 10:58:30 +00:00
|
|
|
to_sign = get_header(data, zero_signatures=True)
|
2014-10-23 16:09:41 +00:00
|
|
|
|
|
|
|
# Locate proper index of current signing key
|
2018-10-30 17:13:11 +00:00
|
|
|
pubkey = "04" + key.get_verifying_key().to_string().hex()
|
2014-10-23 16:09:41 +00:00
|
|
|
index = None
|
2017-02-01 17:07:47 +00:00
|
|
|
for i, pk in pubkeys.items():
|
2014-10-23 16:09:41 +00:00
|
|
|
if pk == pubkey:
|
|
|
|
index = i
|
|
|
|
break
|
|
|
|
|
2019-04-18 14:27:27 +00:00
|
|
|
if index is None:
|
2014-10-23 16:09:41 +00:00
|
|
|
raise Exception("Unable to find private key index. Unknown private key?")
|
|
|
|
|
|
|
|
signature = key.sign_deterministic(to_sign, hashfunc=hashlib.sha256)
|
|
|
|
|
|
|
|
return modify(data, slot, index, signature)
|
|
|
|
|
2018-10-30 17:13:11 +00:00
|
|
|
|
2014-10-23 16:09:41 +00:00
|
|
|
def main(args):
|
|
|
|
if args.generate:
|
|
|
|
key = ecdsa.SigningKey.generate(
|
2018-10-30 17:13:11 +00:00
|
|
|
curve=ecdsa.curves.SECP256k1, hashfunc=hashlib.sha256
|
|
|
|
)
|
2014-10-23 16:09:41 +00:00
|
|
|
|
2017-02-01 17:07:47 +00:00
|
|
|
print("PRIVATE KEY (SECEXP):")
|
2018-10-30 17:13:11 +00:00
|
|
|
print(key.to_string().hex())
|
2017-02-01 17:07:47 +00:00
|
|
|
print()
|
2014-10-23 16:09:41 +00:00
|
|
|
|
2017-02-01 17:07:47 +00:00
|
|
|
print("PRIVATE KEY (PEM):")
|
|
|
|
print(key.to_pem())
|
2014-10-23 16:09:41 +00:00
|
|
|
|
2017-02-01 17:07:47 +00:00
|
|
|
print("PUBLIC KEY:")
|
2018-10-30 17:13:11 +00:00
|
|
|
print("04" + key.get_verifying_key().to_string().hex())
|
2014-10-23 16:09:41 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
if not args.path:
|
|
|
|
raise Exception("-f/--file is required")
|
|
|
|
|
2018-10-30 17:13:11 +00:00
|
|
|
data = open(args.path, "rb").read()
|
2014-10-23 16:09:41 +00:00
|
|
|
assert len(data) % 4 == 0
|
|
|
|
|
2019-01-27 10:58:30 +00:00
|
|
|
if data[:4] != b"TRZF":
|
2014-10-23 16:09:41 +00:00
|
|
|
raise Exception("Firmware header expected")
|
|
|
|
|
2019-01-27 10:58:30 +00:00
|
|
|
data = update_hashes_in_header(data)
|
|
|
|
|
2021-09-27 10:13:51 +00:00
|
|
|
print(f"Firmware size {len(data)} bytes")
|
2014-10-23 16:09:41 +00:00
|
|
|
|
2019-01-27 10:58:30 +00:00
|
|
|
check_size(data)
|
2014-10-23 16:09:41 +00:00
|
|
|
check_signatures(data)
|
2019-01-27 10:58:30 +00:00
|
|
|
check_hashes(data)
|
2014-10-23 16:09:41 +00:00
|
|
|
|
|
|
|
if args.sign:
|
|
|
|
data = sign(data, args.pem)
|
|
|
|
check_signatures(data)
|
2019-01-27 10:58:30 +00:00
|
|
|
check_hashes(data)
|
2014-10-23 16:09:41 +00:00
|
|
|
|
2018-10-30 17:13:11 +00:00
|
|
|
fp = open(args.path, "wb")
|
2014-10-23 16:09:41 +00:00
|
|
|
fp.write(data)
|
|
|
|
fp.close()
|
|
|
|
|
2018-10-30 17:13:11 +00:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2014-10-23 16:09:41 +00:00
|
|
|
args = parse_args()
|
|
|
|
main(args)
|