Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement updatefirmware command and firmwaredl binary #300

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ Please also see [docs](docs/) for additional information about each device.
| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |
| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | Yes | Yes | Yes | Yes | Yes |
| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes | Yes |
| Firmware Downloading | N/A | N/A | Yes | Yes | Yes | No | Yes | Yes |
| Firmware Sig Verify | N/A | N/A | Yes | Yes | Yes | No |Yes | Yes |
| Firmware Upgrade | N/A | N/A | Yes | Yes | Yes | No | Yes | Yes |

## Using with Bitcoin Core

Expand Down
3 changes: 2 additions & 1 deletion contrib/build_bin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export PYTHONHASHSEED=42
poetry run pyinstaller hwi.spec
poetry run contrib/generate-ui.sh
poetry run pyinstaller hwi-qt.spec
poetry run pyinstaller firmwaredl.spec
unset PYTHONHASHSEED

# Make the final compressed package
Expand All @@ -36,5 +37,5 @@ OS=`uname | tr '[:upper:]' '[:lower:]'`
if [[ $OS == "darwin" ]]; then
OS="mac"
fi
tar -czf "hwi-${VERSION}-${OS}-amd64.tar.gz" hwi hwi-qt
tar -czf "hwi-${VERSION}-${OS}-amd64.tar.gz" hwi hwi-qt firmwaredl
popd
3 changes: 2 additions & 1 deletion contrib/build_wine.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,11 @@ popd
export PYTHONHASHSEED=42
$POETRY run pyinstaller hwi.spec
$POETRY run pyinstaller hwi-qt.spec
$POETRY run pyinstaller firmwaredl.spec
unset PYTHONHASHSEED

# Make the final compressed package
pushd dist
VERSION=`$POETRY run hwi --version | cut -d " " -f 2 | dos2unix`
zip "hwi-${VERSION}-windows-amd64.zip" hwi.exe hwi-qt.exe
zip "hwi-${VERSION}-windows-amd64.zip" hwi.exe hwi-qt.exe firmwaredl.exe
popd
9 changes: 9 additions & 0 deletions firmwaredl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#! /usr/bin/env python3

# Firmware downloader script

if __name__ == '__main__':
from hwilib.firmware import main
main()
else:
raise ImportError('firmwaredl is not importable')
32 changes: 32 additions & 0 deletions firmwaredl.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['firmwaredl.py'],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=['contrib/pyinstaller-hooks/'],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='firmwaredl',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True )
8 changes: 8 additions & 0 deletions hwilib/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
setup_device,
signmessage,
signtx,
update_firmware,
wipe_device,
install_udev_rules,
)
Expand Down Expand Up @@ -88,6 +89,9 @@ def send_pin_handler(args, client):
def install_udev_rules_handler(args):
return install_udev_rules('udev', args.location)

def update_firmware_handler(args, client):
return update_firmware(client, args.file)

class HWIHelpFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
pass

Expand Down Expand Up @@ -208,6 +212,10 @@ def process_commands(cli_args):
sendpin_parser.add_argument('pin', help='The numeric positions of the PIN')
sendpin_parser.set_defaults(func=send_pin_handler)

update_firmware_parser = subparsers.add_parser('updatefirmware', help='Verify and load firmware from a file onto a device')
update_firmware_parser.add_argument('file', help='The path to the firmware file')
update_firmware_parser.set_defaults(func=update_firmware_handler)

if sys.platform.startswith("linux"):
udevrules_parser = subparsers.add_parser('installudevrules', help='Install and load the udev rule files for the hardware wallet devices')
udevrules_parser.add_argument('--location', help='The path where the udev rules files will be copied', default='/etc/udev/rules.d/')
Expand Down
3 changes: 3 additions & 0 deletions hwilib/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,3 +329,6 @@ def install_udev_rules(source, location):
from .udevinstaller import UDevInstaller
return UDevInstaller.install(source, location)
return {'error': 'udev rules are not needed on your platform', 'code': NOT_IMPLEMENTED}

def update_firmware(client, file):
return client.update_firmware(file)
4 changes: 4 additions & 0 deletions hwilib/devices/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,3 +627,7 @@ def restore_device(
bb02.set_device_name(label)

return {"success": bb02.restore_from_mnemonic()}

# Verify firmware file then load it onto device
def update_firmware(self, filename: str) -> Dict[str, bool]:
raise NotImplementedYet("The BitBox02 does not implement this method yet")
6 changes: 3 additions & 3 deletions hwilib/devices/ckcc/sigheader.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
FW_MAX_LENGTH = (0x100000 - 0x8000)

# Arguments to be used w/ python's struct module.
FWH_PY_FORMAT = "<I8s8sII36s64s"
FWH_PY_VALUES = "magic_value timestamp version_string pubkey_num firmware_length future signature"
FWH_NUM_FUTURE = 9
FWH_PY_FORMAT = "<I8s8sIIII28s64s"
FWH_PY_VALUES = "magic_value timestamp version_string pubkey_num firmware_length install_flags hw_compat future signature"
FWH_NUM_FUTURE = 7
FWH_PK_NUM_OFFSET = 20

# There is a copy of the header at this location in RAM, copied by bootloader
Expand Down
135 changes: 135 additions & 0 deletions hwilib/devices/coldcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
AF_P2SH,
AF_P2WSH_P2SH,
)
from .ckcc.utils import dfu_parse
from .ckcc.sigheader import (
FW_HEADER_SIZE,
FW_HEADER_OFFSET,
FW_HEADER_MAGIC,
FWH_PY_FORMAT,
)
from ..base58 import (
get_xpub_fingerprint,
xpub_main_2_test,
Expand All @@ -45,6 +52,7 @@
from hashlib import sha256

import base64
import ecdsa
import hid
import io
import sys
Expand Down Expand Up @@ -79,6 +87,62 @@ def str_to_int_path(xfp, path):
return rv


FIRMWARE_KEYS = [
bytearray([0xb4, 0xcb, 0x41, 0x26, 0xf7, 0xe1, 0x6c, 0xf3, 0x8f, 0xf2, 0xb4,
0x71, 0x1d, 0xfb, 0x23, 0x01, 0x0d, 0x76, 0xd6, 0x66, 0xa7, 0x8a,
0xa3, 0x6c, 0x9b, 0x53, 0xf9, 0xf6, 0x7b, 0x58, 0x18, 0x05, 0x58,
0x0b, 0x3b, 0xe9, 0x31, 0xc4, 0x9f, 0xb8, 0x44, 0x04, 0x3c, 0x11,
0x96, 0x08, 0x0f, 0x47, 0x81, 0x25, 0xed, 0x37, 0x7a, 0x23, 0x9e,
0x4a, 0xaf, 0xb7, 0x18, 0x38, 0xba, 0x38, 0x04, 0xda]),
bytearray([0xd6, 0xa2, 0xc8, 0x1d, 0x1c, 0x81, 0x5e, 0xdf, 0xa6, 0x0c, 0x29,
0x6d, 0xb8, 0x57, 0x8f, 0x8d, 0x5e, 0x29, 0x69, 0x92, 0xce, 0xd1,
0x78, 0xc1, 0x7b, 0x20, 0xd7, 0x31, 0x7b, 0xa1, 0x96, 0xb5, 0x3d,
0xef, 0x1b, 0x0c, 0xaa, 0x79, 0x1a, 0xc3, 0x45, 0x58, 0xc4, 0xc8,
0x8a, 0x2d, 0xeb, 0xff, 0xfe, 0x9b, 0x82, 0x01, 0x87, 0x5f, 0x5e,
0xbc, 0x96, 0xa5, 0xe5, 0x4f, 0xc7, 0x68, 0xfe, 0x9f]),
bytearray([0x42, 0xef, 0x66, 0x01, 0x56, 0xc4, 0xcf, 0x95, 0xf4, 0xb5, 0xf0,
0x38, 0x64, 0x11, 0x26, 0xc5, 0x99, 0x39, 0xc1, 0x66, 0x32, 0x06,
0x12, 0x14, 0x4c, 0x25, 0x9c, 0x68, 0x35, 0x8c, 0xd3, 0xba, 0x24,
0x78, 0xde, 0x8c, 0x52, 0xab, 0xdf, 0x6c, 0xb8, 0xbf, 0x09, 0x78,
0x03, 0xbb, 0x63, 0x3a, 0x11, 0x01, 0xd9, 0x0e, 0xa4, 0x7a, 0x73,
0x8f, 0xbf, 0x18, 0x3b, 0x7f, 0xf0, 0x0a, 0x7b, 0xc8]),
bytearray([0x67, 0x60, 0x54, 0x56, 0x82, 0x0c, 0xec, 0xc5, 0x1d, 0xbc, 0x82,
0x08, 0x16, 0xc1, 0x39, 0xef, 0xf5, 0xbf, 0xba, 0x32, 0x7c, 0xce,
0x5f, 0xe3, 0x74, 0x1e, 0x62, 0xd7, 0xe9, 0xfc, 0xc5, 0x4c, 0x8a,
0xe8, 0x11, 0x8d, 0xc3, 0xad, 0xc2, 0x13, 0x92, 0x29, 0x4f, 0x2a,
0xea, 0xd2, 0xf8, 0xa4, 0xc4, 0xd5, 0x7c, 0xfe, 0x12, 0x05, 0x45,
0x3b, 0x54, 0x89, 0x59, 0x07, 0xda, 0xd6, 0xd7, 0x88]),
bytearray([0x43, 0xb1, 0xcf, 0x37, 0xd2, 0x7c, 0x89, 0x1f, 0x5b, 0xfe, 0xac,
0xf3, 0xba, 0x33, 0xfc, 0x95, 0x81, 0xd9, 0xe7, 0xdd, 0x25, 0x95,
0xef, 0x14, 0xdd, 0xef, 0x97, 0xbb, 0x33, 0xf3, 0xd8, 0xa7, 0x34,
0x2b, 0x7a, 0x97, 0xba, 0xb3, 0xaa, 0x73, 0xe7, 0x9d, 0x41, 0x32,
0xd8, 0xfc, 0xa1, 0x17, 0x66, 0xb5, 0x0b, 0xfe, 0x63, 0x40, 0x21,
0x89, 0xc9, 0x92, 0x7b, 0x8e, 0x72, 0xdf, 0x0b, 0x59])
]
DEV_KEY = FIRMWARE_KEYS[0] # Warn when this key is used for signing as it is publicly known key for development only


def verify_firmware(firmware_data):
# Skip DFU header
firmware_data = firmware_data[293:]

header = firmware_data[FW_HEADER_OFFSET:FW_HEADER_OFFSET + FW_HEADER_SIZE]
magic, _, _, pubkey_num, firmware_length, _, _, _, signature = struct.unpack(FWH_PY_FORMAT, header)
assert magic == FW_HEADER_MAGIC
msg = sha256(firmware_data[0:FW_HEADER_OFFSET + FW_HEADER_SIZE - 64])
msg.update(firmware_data[FW_HEADER_OFFSET + FW_HEADER_SIZE:firmware_length])
digest = sha256(msg.digest()).digest()

if pubkey_num == 0:
print("Warning: This firmware was signed using the publicly available dev key and not a Coinkite official key", file=sys.stderr)

key = ecdsa.VerifyingKey.from_string(FIRMWARE_KEYS[pubkey_num], curve=ecdsa.curves.SECP256k1)
try:
return key.verify_digest(signature, digest)
except ecdsa.BadSignatureError:
return False


def coldcard_exception(f):
def func(*args, **kwargs):
try:
Expand Down Expand Up @@ -340,6 +404,77 @@ def send_pin(self, pin):
def toggle_passphrase(self):
raise UnavailableActionError('The Coldcard does not support toggling passphrase from the host')

# Verify firmware file then load it onto device
@coldcard_exception
def update_firmware(self, filename: str) -> Dict[str, bool]:
with open(filename, 'rb') as fd:
# learn size (portable way)
offset = 0
sz = fd.seek(0, 2)
fd.seek(0)

# Unwrap DFU contents, if needed. Also handles raw binary file.
try:
if fd.read(5) == b'DfuSe':
# expecting a DFU-wrapped file.
fd.seek(0)
offset, sz, *_ = dfu_parse(fd)
else:
# assume raw binary
pass

assert sz % 256 == 0, "un-aligned size: %s" % sz
fd.seek(offset + FW_HEADER_OFFSET)
hdr = fd.read(FW_HEADER_SIZE)

magic = struct.unpack_from("<I", hdr)[0]
except Exception:
magic = None

if magic != FW_HEADER_MAGIC:
raise BadArgumentError('{} has an invalid magic header for a firmware file.'.format(filename))

# Read the whole firmware to verify the signature
fd.seek(0)
data = fd.read()
if not verify_firmware(data):
raise BadArgumentError('Firmware signature is invalid')

fd.seek(offset)

left = sz
chk = sha256()
for pos in range(0, sz, MAX_BLK_LEN):
here = fd.read(min(MAX_BLK_LEN, left))
if not here:
break
left -= len(here)
result = self.device.send_recv(CCProtocolPacker.upload(pos, sz, here))
assert result == pos, "Got back: %r" % result
chk.update(here)

# do a verify
expect = chk.digest()
result = self.device.send_recv(CCProtocolPacker.sha256())
assert len(result) == 32
if result != expect:
raise DeviceFailureError("Wrong checksum:\nexpect: %s\n got: %s" % (b2a_hex(expect).decode('ascii'), b2a_hex(result).decode('ascii')))

# AFTER fully uploaded and verified, write a copy of the signature header
# onto the end of flash. Bootrom uses this to check entire file uploaded.
result = self.device.send_recv(CCProtocolPacker.upload(sz, sz + FW_HEADER_SIZE, hdr))
assert result == sz, "failed to write trailer"

# check also SHA after that!
chk.update(hdr)
expect = chk.digest()
final_chk = self.device.send_recv(CCProtocolPacker.sha256())
assert expect == final_chk, "Checksum mismatch after all that?"

self.device.send_recv(CCProtocolPacker.reboot())

return {'success': True}

def enumerate(password=''):
results = []
devices = hid.enumerate(COINKITE_VID, CKCC_PID)
Expand Down
Loading