diff --git a/.github/actions/install-sim/action.yml b/.github/actions/install-sim/action.yml index 1a85e3590..561e74fc8 100644 --- a/.github/actions/install-sim/action.yml +++ b/.github/actions/install-sim/action.yml @@ -18,7 +18,7 @@ runs: apt-get install -y libsdl2-image-2.0-0 libusb-1.0-0 tar -xvf trezor-firmware.tar.gz - - if: inputs.device == 'coldcard' + - if: startsWith(inputs.device, 'coldcard') shell: bash run: | apt-get update @@ -26,10 +26,17 @@ runs: git config --global user.email "ci@ci.com" git config --global user.name "ci" pushd test/work; git clone --recursive https://github.com/Coldcard/firmware.git; popd - tar -xvf coldcard-mpy.tar.gz - pushd test/work/firmware; git am ../../data/coldcard-multisig.patch; popd - poetry run pip install -r test/work/firmware/requirements.txt - pip install -r test/work/firmware/requirements.txt + if [[ inputs.device == "coldcard-edge"]]; then + archive="coldcard-edge-mpy.tar.gz + dir="firmware-edge" + else + archive="coldcard-mpy.tar.gz" + dir="firmware" + fi + tar -xvf ${archive} + pushd test/work/${dir}; git am ../../data/coldcard-multisig.patch; popd + poetry run pip install -r test/work/${dir}/requirements.txt + pip install -r test/work/${dir}/requirements.txt - if: inputs.device == 'bitbox01' shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 600d24841..eaf3d11e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,6 +149,7 @@ jobs: - { name: 'trezor-1', archive: 'trezor-firmware', paths: 'test/work/trezor-firmware' } - { name: 'trezor-t', archive: 'trezor-firmware', paths: 'test/work/trezor-firmware' } - { name: 'coldcard', archive: 'coldcard-mpy', paths: 'test/work/firmware/external/micropython/ports/unix/coldcard-mpy test/work/firmware/unix/coldcard-mpy test/work/firmware/unix/l-mpy test/work/firmware/unix/l-port' } + - { name: 'coldcard-edge', archive: 'coldcard-edge-mpy', paths: 'test/work/firmware-edge/external/micropython/ports/unix/coldcard-mpy test/work/firmware-edge/unix/coldcard-mpy test/work/firmware-edge/unix/l-mpy test/work/firmware-edge/unix/l-port' } - { name: 'bitbox01', archive: 'mcu', paths: 'test/work/mcu' } - { name: 'jade', archive: 'jade', paths: 'test/work/jade/simulator' } - { name: 'ledger', archive: 'speculos', paths: 'test/work/speculos' } @@ -213,6 +214,7 @@ jobs: - 'trezor-1' - 'trezor-t' - 'coldcard' + - 'coldcard-edge' - 'bitbox01' - 'jade' - 'ledger' @@ -281,6 +283,7 @@ jobs: - 'trezor-1' - 'trezor-t' - 'coldcard' + - 'coldcard-edge' - 'bitbox01' - 'jade' - 'ledger' diff --git a/hwilib/devices/ckcc/README.md b/hwilib/devices/ckcc/README.md index 8db249b74..d7516ee3f 100644 --- a/hwilib/devices/ckcc/README.md +++ b/hwilib/devices/ckcc/README.md @@ -2,7 +2,7 @@ This is a stripped down and modified version of the official [ckcc-protocol](https://github.com/Coldcard/ckcc-protocol) library. -This stripped down version was made at commit [ca8d2b7808784a9f4927f3250bf52d2623a4e15b](https://github.com/Coldcard/ckcc-protocol/tree/ca8d2b7808784a9f4927f3250bf52d2623a4e15b). +This stripped down version was made at commit [f924f6d35ca0a6804b9e25d476cb53ae2f8ae8d6](https://github.com/Coldcard/ckcc-protocol/commit/f924f6d35ca0a6804b9e25d476cb53ae2f8ae8d6). ## Changes diff --git a/hwilib/devices/ckcc/__init__.py b/hwilib/devices/ckcc/__init__.py index b2f0b70ea..3f5001d87 100644 --- a/hwilib/devices/ckcc/__init__.py +++ b/hwilib/devices/ckcc/__init__.py @@ -1,5 +1,6 @@ +# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC. -__version__ = '1.0.2' +__version__ = '1.4.0' __all__ = [ "client", "protocol", "constants" ] diff --git a/hwilib/devices/ckcc/client.py b/hwilib/devices/ckcc/client.py index 1f265fb25..48145bf6e 100644 --- a/hwilib/devices/ckcc/client.py +++ b/hwilib/devices/ckcc/client.py @@ -1,3 +1,4 @@ +# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # client.py # @@ -8,9 +9,10 @@ # # - ec_mult, ec_setup, aes_setup, mitm_verify # -import hid, sys, os, platform +import hid, sys, os from binascii import b2a_hex, a2b_hex from hashlib import sha256 +from .constants import USB_NCRY_V1, USB_NCRY_V2 from .protocol import CCProtocolPacker, CCProtocolUnpacker, CCProtoError, MAX_MSG_LEN, MAX_BLK_LEN from .utils import decode_xpub, get_pubkey_string @@ -22,13 +24,11 @@ CKCC_SIMULATOR_PATH = '/tmp/ckcc-simulator.sock' class ColdcardDevice: - def __init__(self, sn=None, dev=None, encrypt=True): + def __init__(self, sn=None, dev=None, encrypt=True, ncry_ver=USB_NCRY_V1): # Establish connection via USB (HID) or Unix Pipe self.is_simulator = False if not dev and sn and '/' in sn: - if platform.system() == 'Windows': - raise RuntimeError("Cannot connect to simulator. Is it running?") dev = UnixSimulatorPipe(sn) found = 'simulator' self.is_simulator = True @@ -49,7 +49,7 @@ def __init__(self, sn=None, dev=None, encrypt=True): break if not dev: - raise KeyError("Could not find Coldcard!" + raise KeyError("Could not find Coldcard!" if not sn else ('Cannot find CC with serial: '+sn)) else: found = dev.get_serial_number_string() @@ -58,6 +58,7 @@ def __init__(self, sn=None, dev=None, encrypt=True): self.serial = found # they will be defined after we've established a shared secret w/ device + self.ncry_ver = ncry_ver self.session_key = None self.encrypt_request = None self.decrypt_response = None @@ -67,7 +68,7 @@ def __init__(self, sn=None, dev=None, encrypt=True): self.resync() if encrypt: - self.start_encryption() + self.start_encryption(version=self.ncry_ver) def close(self): # close underlying HID device @@ -101,17 +102,21 @@ def send_recv(self, msg, expect_errors=False, verbose=0, timeout=3000, encrypt=T # first byte of each 64-byte packet encodes length or packet-offset assert 4 <= len(msg) <= MAX_MSG_LEN, "msg length: %d" % len(msg) - if not self.encrypt_request: + if self.encrypt_request is None: # disable encryption if not already enabled for this connection encrypt = False + if self.encrypt_request and self.ncry_ver == USB_NCRY_V2: + # ncry version 2 - everything needs to be encrypted + encrypt = True + if encrypt: msg = self.encrypt_request(msg) left = len(msg) offset = 0 while left > 0: - # Note: first byte always zero (HID report number), + # Note: first byte always zero (HID report number), # [1] is framing header (length+flags) # [2:65] payload (63 bytes, perhaps including padding) here = min(63, left) @@ -224,7 +229,7 @@ def aes_setup(self, session_key): self.encrypt_request = pyaes.AESModeOfOperationCTR(session_key, pyaes.Counter(0)).encrypt self.decrypt_response = pyaes.AESModeOfOperationCTR(session_key, pyaes.Counter(0)).decrypt - def start_encryption(self): + def start_encryption(self, version=USB_NCRY_V1): # setup encryption on the link # - pick our own key pair, IV for AES # - send IV and pubkey to device @@ -233,10 +238,12 @@ def start_encryption(self): pubkey = self.ec_setup() - msg = CCProtocolPacker.encrypt_start(pubkey) + msg = CCProtocolPacker.encrypt_start(pubkey, version=version) his_pubkey, fingerprint, xpub = self.send_recv(msg, encrypt=False) + self.ncry_ver = version + self.session_key = self.ec_mult(his_pubkey) # capture some public details of remote side's master key @@ -248,7 +255,6 @@ def start_encryption(self): self.aes_setup(self.session_key) def mitm_verify(self, sig, expected_xpub): - # If Pycoin is not available, do it using ecdsa from ecdsa import BadSignatureError, SECP256k1, VerifyingKey # of the returned (pubkey, chaincode) tuple, chaincode is not used pubkey, _ = decode_xpub(expected_xpub) @@ -318,14 +324,15 @@ def download_file(self, length, checksum, blksize=1024, file_number=1): return data - def hash_password(self, text_password): + def hash_password(self, text_password, v3=False): # Turn text password into a key for use in HSM auth protocol + # - changed from pbkdf2_hmac_sha256 to pbkdf2_hmac_sha512 in version 4 of CC firmware from hashlib import pbkdf2_hmac, sha256 from .constants import PBKDF2_ITER_COUNT salt = sha256(b'pepper' + self.serial.encode('ascii')).digest() - return pbkdf2_hmac('sha256', text_password, salt, PBKDF2_ITER_COUNT) + return pbkdf2_hmac('sha256' if v3 else 'sha512', text_password, salt, PBKDF2_ITER_COUNT)[:32] class UnixSimulatorPipe: @@ -341,15 +348,19 @@ def __init__(self, path): self.close() raise RuntimeError("Cannot connect to simulator. Is it running?") - instance = 0 - while instance < 10: + last_err = None + for instance in range(5): pn = '/tmp/ckcc-client-%d-%d.sock' % (os.getpid(), instance) try: self.pipe.bind(pn) # just needs any name break - except OSError: - instance += 1 + except OSError as err: + last_err = err + if os.path.exists(pn): + os.remove(pn) continue + else: + raise last_err # raise whatever was raised last in the loop self.pipe_name = pn atexit.register(self.close) @@ -383,7 +394,7 @@ def close(self): pass def get_serial_number_string(self): - return 'simulator' + return 'F1'*6 -# EOF +# EOF \ No newline at end of file diff --git a/hwilib/devices/ckcc/constants.py b/hwilib/devices/ckcc/constants.py index ebbd93770..02d86040d 100644 --- a/hwilib/devices/ckcc/constants.py +++ b/hwilib/devices/ckcc/constants.py @@ -1,3 +1,4 @@ +# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # Constants and various "limits" shared between embedded and desktop USB protocol # @@ -6,6 +7,22 @@ except ImportError: const = int +# USB encryption versions (default USB_NCRY_V1) +# +# This introduces a new ncry version to close a potential attack vector: +# +# A malicious program may re-initialize the connection encryption by sending the ncry command a second time during USB operation. +# This may prove particularly harmful in HSM mode. +# +# Sending version USB_NCRY_V2 changes the behavior in two ways: +# * All future commands must be encrypted +# * Returns an error if the ncry command is sent again for the duration of the power cycle +# +# USB_NCRY_V2 is most suitable for HSM mode as in case of any communication issue or simply by closing `ColdcardDevice` +# Coldcard will need to reboot to recover USB operation if USB_NCRY_V2. +USB_NCRY_V1 = const(0x01) +USB_NCRY_V2 = const(0x02) + # For upload/download this is the max size of the data block. MAX_BLK_LEN = const(2048) @@ -17,17 +34,29 @@ # - the max on the wire for mainnet is 100k # - but a PSBT might contain a full txn for each input MAX_TXN_LEN = const(384*1024) +MAX_TXN_LEN_MK4 = const(2*1024*1024) # Max size of any upload (firmware.dfu files in particular) MAX_UPLOAD_LEN = const(2*MAX_TXN_LEN) +MAX_UPLOAD_LEN_MK4 = const(2*MAX_TXN_LEN_MK4) # Max length of text messages for signing MSG_SIGNING_MAX_LENGTH = const(240) +# Bitcoin limitation: max number of signatures in P2SH redeem script (non-segwit) +# - 520 byte redeem script limit <= 15*34 bytes per pubkey == 510 bytes +# - serializations of M/N in redeem scripts assume this range +MAX_SIGNERS = const(15) +# taproot artificial multisig limit +MAX_TR_SIGNERS = const(32) + +TAPROOT_LEAF_MASK = 0xfe +TAPROOT_LEAF_TAPSCRIPT = 0xc0 + # Types of user auth we support USER_AUTH_TOTP = const(1) # RFC6238 USER_AUTH_HOTP = const(2) # RFC4226 -USER_AUTH_HMAC = const(3) # PBKDF2('hmac-sha256', secret, sha256(psbt), PBKDF2_ITER_COUNT) +USER_AUTH_HMAC = const(3) # PBKDF2('hmac-sha512', scrt, sha256(psbt), PBKDF2_ITER_COUNT)[:32] USER_AUTH_SHOW_QR = const(0x80) # show secret on Coldcard screen (best for TOTP enroll) MAX_USERNAME_LEN = 16 @@ -48,6 +77,7 @@ AFC_BECH32 = const(0x04) # just how we're encoding it? AFC_SCRIPT = const(0x08) # paying into a script AFC_WRAPPED = const(0x10) # for transition/compat types for segwit vs. old +AFC_BECH32M = const(0x16) # no difference between script/key path in taproot # Numeric codes for specific address types AF_CLASSIC = AFC_PUBKEY # 1addr @@ -56,11 +86,13 @@ AF_P2WSH = AFC_SCRIPT | AFC_SEGWIT | AFC_BECH32 # segwit multisig AF_P2WPKH_P2SH = AFC_WRAPPED | AFC_PUBKEY | AFC_SEGWIT # looks classic P2SH, but p2wpkh inside AF_P2WSH_P2SH = AFC_WRAPPED | AFC_SCRIPT | AFC_SEGWIT # looks classic P2SH, segwit multisig +AF_P2TR = AFC_PUBKEY | AFC_SEGWIT | AFC_BECH32M # bc1p SUPPORTED_ADDR_FORMATS = frozenset([ AF_CLASSIC, AF_P2SH, AF_P2WPKH, + AF_P2TR, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH, @@ -68,21 +100,66 @@ # BIP-174 aka PSBT defined values # -PSBT_GLOBAL_UNSIGNED_TX = const(0) -PSBT_GLOBAL_XPUB = const(1) - -PSBT_IN_NON_WITNESS_UTXO = const(0) -PSBT_IN_WITNESS_UTXO = const(1) -PSBT_IN_PARTIAL_SIG = const(2) -PSBT_IN_SIGHASH_TYPE = const(3) -PSBT_IN_REDEEM_SCRIPT = const(4) -PSBT_IN_WITNESS_SCRIPT = const(5) -PSBT_IN_BIP32_DERIVATION = const(6) -PSBT_IN_FINAL_SCRIPTSIG = const(7) -PSBT_IN_FINAL_SCRIPTWITNESS = const(8) - -PSBT_OUT_REDEEM_SCRIPT = const(0) -PSBT_OUT_WITNESS_SCRIPT = const(1) -PSBT_OUT_BIP32_DERIVATION = const(2) - -# EOF +# GLOBAL === +PSBT_GLOBAL_UNSIGNED_TX = const(0x00) +PSBT_GLOBAL_XPUB = const(0x01) +PSBT_GLOBAL_VERSION = const(0xfb) +PSBT_GLOBAL_PROPRIETARY = const(0xfc) +# BIP-370 +PSBT_GLOBAL_TX_VERSION = const(0x02) +PSBT_GLOBAL_FALLBACK_LOCKTIME = const(0x03) +PSBT_GLOBAL_INPUT_COUNT = const(0x04) +PSBT_GLOBAL_OUTPUT_COUNT = const(0x05) +PSBT_GLOBAL_TX_MODIFIABLE = const(0x06) + +# INPUTS === +PSBT_IN_NON_WITNESS_UTXO = const(0x00) +PSBT_IN_WITNESS_UTXO = const(0x01) +PSBT_IN_PARTIAL_SIG = const(0x02) +PSBT_IN_SIGHASH_TYPE = const(0x03) +PSBT_IN_REDEEM_SCRIPT = const(0x04) +PSBT_IN_WITNESS_SCRIPT = const(0x05) +PSBT_IN_BIP32_DERIVATION = const(0x06) +PSBT_IN_FINAL_SCRIPTSIG = const(0x07) +PSBT_IN_FINAL_SCRIPTWITNESS = const(0x08) +PSBT_IN_POR_COMMITMENT = const(0x09) +PSBT_IN_RIPEMD160 = const(0x0a) +PSBT_IN_SHA256 = const(0x0b) +PSBT_IN_HASH160 = const(0x0c) +PSBT_IN_HASH256 = const(0x0d) +# BIP-370 +PSBT_IN_PREVIOUS_TXID = const(0x0e) +PSBT_IN_OUTPUT_INDEX = const(0x0f) +PSBT_IN_SEQUENCE = const(0x10) +PSBT_IN_REQUIRED_TIME_LOCKTIME = const(0x11) +PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = const(0x12) +# BIP-371 +PSBT_IN_TAP_KEY_SIG = const(0x13) +PSBT_IN_TAP_SCRIPT_SIG = const(0x14) +PSBT_IN_TAP_LEAF_SCRIPT = const(0x15) +PSBT_IN_TAP_BIP32_DERIVATION = const(0x16) +PSBT_IN_TAP_INTERNAL_KEY = const(0x17) +PSBT_IN_TAP_MERKLE_ROOT = const(0x18) + +# OUTPUTS === +PSBT_OUT_REDEEM_SCRIPT = const(0x00) +PSBT_OUT_WITNESS_SCRIPT = const(0x01) +PSBT_OUT_BIP32_DERIVATION = const(0x02) +# BIP-370 +PSBT_OUT_AMOUNT = const(0x03) +PSBT_OUT_SCRIPT = const(0x04) +# BIP-371 +PSBT_OUT_TAP_INTERNAL_KEY = const(0x05) +PSBT_OUT_TAP_TREE = const(0x06) +PSBT_OUT_TAP_BIP32_DERIVATION = const(0x07) + +RFC_SIGNATURE_TEMPLATE = '''\ +-----BEGIN BITCOIN SIGNED MESSAGE----- +{msg} +-----BEGIN BITCOIN SIGNATURE----- +{addr} +{sig} +-----END BITCOIN SIGNATURE----- +''' + +# EOF \ No newline at end of file diff --git a/hwilib/devices/ckcc/protocol.py b/hwilib/devices/ckcc/protocol.py index 92431910e..7cb6918b4 100644 --- a/hwilib/devices/ckcc/protocol.py +++ b/hwilib/devices/ckcc/protocol.py @@ -1,3 +1,4 @@ +# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # Details of our USB level protocol. Shared file between desktop and embedded. # @@ -63,11 +64,15 @@ def check_mitm(): @staticmethod def start_backup(): - # prompts user with password for encrytped backup + # prompts user with password for encrypted backup return b'back' @staticmethod - def encrypt_start(device_pubkey, version=0x1): + def encrypt_start(device_pubkey, version=USB_NCRY_V1): + supported_versions = [USB_NCRY_V1, USB_NCRY_V2] + if version not in supported_versions: + raise ValueError("Unsupported USB encryption version. " + "Supported versions: %s" % (supported_versions)) assert len(device_pubkey) == 64, "want uncompressed 64-byte pubkey, no prefix byte" return pack('<4sI64s', b'ncry', version, device_pubkey) @@ -120,6 +125,36 @@ def multisig_enroll(length, file_sha): assert len(file_sha) == 32 return pack('<4sI32s', b'enrl', length, file_sha) + @staticmethod + def miniscript_ls(): + # list registered miniscript wallet names + return b'msls' + + @staticmethod + def miniscript_delete(name): + # delete registered miniscript wallet by name + assert 2 <= len(name) <= 40, "name len" + return b'msdl' + name.encode('ascii') + + @staticmethod + def miniscript_get(name): + # get registered miniscript wallet object by name + assert 2 <= len(name) <= 40, "name len" + return b'msgt' + name.encode('ascii') + + @staticmethod + def miniscript_address(name, change=False, idx=0): + # get miniscript address from internal or external chain by id + assert 2 <= len(name) <= 40, "name len" + assert 0 <= idx < (2**31), "child idx" + return pack('<4sII', b'msas', int(change), idx) + name.encode('ascii') + + @staticmethod + def miniscript_enroll(length, file_sha): + # miniscript details must already be uploaded as a text file, this starts approval process. + assert len(file_sha) == 32 + return pack('<4sI32s', b'mins', length, file_sha) + @staticmethod def multisig_check(M, N, xfp_xor): # do we have a wallet already that matches M+N and xor(*xfps)? @@ -140,7 +175,7 @@ def show_address(subpath, addr_fmt=AF_CLASSIC): @staticmethod def show_p2sh_address(M, xfp_paths, witdeem_script, addr_fmt=AF_P2SH): # For multisig (aka) P2SH cases, you will need all the info required to build - # the redeem script, and the Coldcard must already have been enrolled + # the redeem script, and the Coldcard must already have been enrolled # into the wallet. # - redeem script must be provided # - full subkey paths for each involved key is required in a list of lists of ints, where @@ -224,76 +259,89 @@ class CCProtocolUnpacker: # - given full rx message to work from # - this is done after un-framing - @classmethod - def decode(cls, msg): + @staticmethod + def decode(msg): assert len(msg) >= 4 sign = str(msg[0:4], 'utf8', 'ignore') - d = getattr(cls, sign, cls) - if d is cls: + d = getattr(CCProtocolUnpacker, sign, None) + if d is None: raise CCFramingError('Unknown response signature: ' + repr(sign)) return d(msg) - # struct info for each response - + + @staticmethod def okay(msg): # trivial response, w/ no content assert len(msg) == 4 return None # low-level errors + @staticmethod def fram(msg): - raise CCFramingError("Framing Error", str(msg[4:], 'utf8')) + raise CCFramingError("Framing Error: " + str(msg[4:], 'utf8')) + + @staticmethod def err_(msg): raise CCProtoError("Coldcard Error: " + str(msg[4:], 'utf8', 'ignore'), msg[4:]) + @staticmethod def refu(msg): # user didn't want to approve something raise CCUserRefused() + @staticmethod def busy(msg): # user didn't want to approve something raise CCBusyError() + @staticmethod def biny(msg): # binary string: length implied by msg framing return msg[4:] + @staticmethod def int1(msg): return unpack_from(' bool: def can_sign_taproot(self) -> bool: """ - The Coldard does not support Taproot yet. + Only COLDCARD edge support taproot. + Edge release has X suffix in version string. - :returns: False, always + :returns: Whether Taproot is supported """ - return False + if self.device.is_simulator: + cmd = "import version; RV.write(str(int(getattr(version, 'is_edge', 0))))" + rv = self.device.send_recv(b'EXEC' + cmd.encode('utf-8'), timeout=60000, encrypt=False) + return rv == b"1" + else: + _, ver, _, _, _ = self.device.send_recv(CCProtocolPacker.version()).split("\n") + return "X" == ver[-1] def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]: @@ -422,6 +433,10 @@ def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain try: client = ColdcardClient(path) d_data['fingerprint'] = client.get_master_fingerprint().hex() + if client.can_sign_taproot(): + d_data['label'] = 'edge' + d_data['model'] = 'edge_' + d_data['model'] + except RuntimeError as e: # Skip the simulator if it's not there if str(e) == 'Cannot connect to simulator. Is it running?': diff --git a/test/README.md b/test/README.md index c064d8786..63c3efe1c 100644 --- a/test/README.md +++ b/test/README.md @@ -24,7 +24,7 @@ It also tests usage with `bitcoind`. `setup_environment.sh` will build the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Digital Bitbox simulator, the Jade emulator, and `bitcoind`. if run in the `test/` directory, these will be built in `work/test/trezor-firmware`, `work/test/firmware`, `work/test/keepkey-firmware`, `work/test/mcu`, and `work/test/bitcoin` respectively. In order to build each simulator/emulator, you will need to use command line arguments. -These are `--trezor-1`, `--trezor-t`, `--coldcard`, `--keepkey`, `--bitbox01`, `--jade`, and `--bitcoind`. +These are `--trezor-1`, `--trezor-t`, `--coldcard`, `--coldcard-edge`, `--keepkey`, `--bitbox01`, `--jade`, and `--bitcoind`. If an environment variable is not present or not set, then the simulator/emulator or bitcoind that it guards will not be built. `run_tests.py` runs the tests. If run from the `test/` directory, it will be able to find the Trezor emulator, Coldcard simulator, Keepkey emulator, Digital Bitbox simulator, Jade emulator, and bitcoind. @@ -80,14 +80,7 @@ $ pipenv run script/cibuild In order to build the Coldcard simulator, the following packages will need to be installed: ``` -build-essential git python3 python3-pip libudev-dev gcc-arm-none-eabi -``` - -After cloning the Coldcard repo into this testing folder, the python packages can be installed with: - -``` -pip install -r ckcc_firmware/requirements.txt -pip install -r ckcc_firmware/unix/requirements.txt +build-essential git python3 python3-pip libudev-dev gcc-arm-none-eabi libffi-dev xterm swig libpcsclite-dev python-is-python3 autoconf libtool python3-venv ``` ### Building @@ -95,7 +88,10 @@ pip install -r ckcc_firmware/unix/requirements.txt Clone the repository: ``` -$ git clone https://github.com/coldcard/firmware +git clone --recursive https://github.com/Coldcard/firmware.git +cd firmware +pip install -r requirements.txt +pip install pysdl2-dll # Ubuntu needs this dependency ``` Build the emulator in headless mode: @@ -103,6 +99,7 @@ Build the emulator in headless mode: ``` $ cd firmware/unix $ make setup +$ make ngu-setup $ make ``` @@ -308,27 +305,6 @@ $ cmake -Bbuild -H. $ make -C build/ ``` -## Coldcard emulator - -Clone the repository: - -``` -git clone --recursive https://github.com/Coldcard/firmware.git -``` - -### Dependencies - -In order to build the Coldcard emulator, the following packages will need to be installed: - -``` -build-essential git python3 python3-pip libudev-dev gcc-arm-none-eabi libffi-dev xterm swig libpcsclite-dev -``` -You also have to install its python dependencies - -``` -pip install -r requirements.txt -``` - ## Bitcoin Core diff --git a/test/run_tests.py b/test/run_tests.py index cde4b65dc..b3a6f79a9 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -31,6 +31,10 @@ coldcard_group.add_argument('--no-coldcard', dest='coldcard', help='Do not run Coldcard test with simulator', action='store_false') coldcard_group.add_argument('--coldcard', dest='coldcard', help='Run Coldcard test with simulator', action='store_true') +coldcard_edge_group = parser.add_mutually_exclusive_group() +coldcard_edge_group.add_argument('--no-coldcard-edge', dest='coldcard_edge', help='Do not run Coldcard Edge test with simulator', action='store_false') +coldcard_edge_group.add_argument('--coldcard-edge', dest='coldcard_edge', help='Run Coldcard Edge test with simulator', action='store_true') + ledger_group = parser.add_mutually_exclusive_group() ledger_group.add_argument('--no-ledger', dest='ledger', help='Do not run Ledger test with emulator', action='store_false') ledger_group.add_argument('--ledger', dest='ledger', help='Run Ledger test with emulator', action='store_true') @@ -54,6 +58,7 @@ parser.add_argument('--trezor-1-path', dest='trezor_1_path', help='Path to Trezor 1 emulator', default='work/trezor-firmware/legacy/firmware/trezor.elf') parser.add_argument('--trezor-t-path', dest='trezor_t_path', help='Path to Trezor T emulator', default='work/trezor-firmware/core/emu.sh') parser.add_argument('--coldcard-path', dest='coldcard_path', help='Path to Coldcar simulator', default='work/firmware/unix/headless.py') +parser.add_argument('--coldcard-edge-path', dest='coldcard_edge_path', help='Path to Coldcar Edge simulator', default='work/firmware-edge/unix/headless.py') parser.add_argument('--keepkey-path', dest='keepkey_path', help='Path to Keepkey emulator', default='work/keepkey-firmware/bin/kkemu') parser.add_argument('--bitbox01-path', dest='bitbox01_path', help='Path to Digital Bitbox simulator', default='work/mcu/build/bin/simulator') parser.add_argument('--ledger-path', dest='ledger_path', help='Path to Ledger emulator', default='work/speculos/speculos.py') @@ -65,7 +70,7 @@ parser.add_argument("--device-only", help="Only run device tests", action="store_true") -parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None, ledger_legacy=None, jade=None) +parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, coldcard_edge=None, keepkey=None, bitbox01=None, ledger=None, ledger_legacy=None, jade=None) args = parser.parse_args() @@ -87,6 +92,7 @@ args.trezor_1 = True if args.trezor_1 is None else args.trezor_1 args.trezor_t = True if args.trezor_t is None else args.trezor_t args.coldcard = True if args.coldcard is None else args.coldcard + args.coldcard_edge = True if args.coldcard_edge is None else args.coldcard_edge args.keepkey = True if args.keepkey is None else args.keepkey args.bitbox01 = True if args.bitbox01 is None else args.bitbox01 args.ledger = True if args.ledger is None else args.ledger @@ -97,13 +103,14 @@ args.trezor_1 = False if args.trezor_1 is None else args.trezor_1 args.trezor_t = False if args.trezor_t is None else args.trezor_t args.coldcard = False if args.coldcard is None else args.coldcard + args.coldcard_edge = False if args.coldcard_edge is None else args.coldcard_edge args.keepkey = False if args.keepkey is None else args.keepkey args.bitbox01 = False if args.bitbox01 is None else args.bitbox01 args.ledger = False if args.ledger is None else args.ledger args.ledger_legacy = False if args.ledger_legacy is None else args.ledger_legacy args.jade = False if args.jade is None else args.jade -if args.trezor_1 or args.trezor_t or args.coldcard or args.ledger or args.ledger_legacy or args.keepkey or args.bitbox01 or args.jade: +if args.trezor_1 or args.trezor_t or args.coldcard or args.coldcard_edge or args.ledger or args.ledger_legacy or args.keepkey or args.bitbox01 or args.jade: # Start bitcoind bitcoind = Bitcoind.create(args.bitcoind) @@ -111,6 +118,8 @@ success &= digitalbitbox_test_suite(args.bitbox01_path, bitcoind, args.interface) if success and args.coldcard: success &= coldcard_test_suite(args.coldcard_path, bitcoind, args.interface) + if success and args.coldcard_edge: + success &= coldcard_test_suite(args.coldcard_edge_path, bitcoind, args.interface, is_edge=True) if success and args.trezor_1: success &= trezor_test_suite(args.trezor_1_path, bitcoind, args.interface, '1') if success and args.trezor_t: diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 23f636823..f48acca7c 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -14,6 +14,10 @@ while [[ $# -gt 0 ]]; do build_coldcard=1 shift ;; + --coldcard-edge) + build_coldcard_edge=1 + shift + ;; --bitbox01) build_bitbox01=1 shift @@ -42,6 +46,7 @@ while [[ $# -gt 0 ]]; do build_trezor_1=1 build_trezor_t=1 build_coldcard=1 + build_coldcard_edge=1 build_bitbox01=1 build_ledger=1 build_keepkey=1 @@ -115,47 +120,64 @@ if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then cd .. fi -if [[ -n ${build_coldcard} ]]; then +if [[ -n ${build_coldcard} || -n ${build_coldcard_edge} ]]; then # Clone coldcard firmware if it doesn't exist, or update it if it does - coldcard_setup_needed=false - if [ ! -d "firmware" ]; then - git clone --recursive https://github.com/Coldcard/firmware.git - cd firmware - coldcard_setup_needed=true - else - cd firmware - git reset --hard HEAD~3 # Undo git-am for checking and updating - git fetch - - # Determine if we need to pull. From https://stackoverflow.com/a/3278427 - UPSTREAM=${1:-'@{u}'} - LOCAL=$(git rev-parse @) - REMOTE=$(git rev-parse "$UPSTREAM") - BASE=$(git merge-base @ "$UPSTREAM") - - if [ $LOCAL = $REMOTE ]; then - echo "Up-to-date" - elif [ $LOCAL = $BASE ]; then - git pull + do_coldcard_firmware() { + coldcard_setup_needed=false + if [ ! -d $1 ]; then + git clone --branch $2 --single-branch --recursive https://github.com/Coldcard/firmware.git $1 + cd $1 coldcard_setup_needed=true + else + cd $1 + git reset --hard HEAD~3 # Undo git-am for checking and updating + git fetch origin $2 + + # Determine if we need to pull. From https://stackoverflow.com/a/3278427 + UPSTREAM=${1:-'@{u}'} + LOCAL=$(git rev-parse @) + REMOTE=$(git rev-parse "$UPSTREAM") + BASE=$(git merge-base @ "$UPSTREAM") + + if [ $LOCAL = $REMOTE ]; then + echo "Up-to-date" + elif [ $LOCAL = $BASE ]; then + git pull origin $2 + coldcard_setup_needed=true + fi fi - fi - # Apply patch to make simulator work in linux environments - git am ../../data/coldcard-multisig.patch - # Build the simulator. This is cached, but it is also fast - poetry run pip install -r requirements.txt - pip install -r requirements.txt - cd unix - if [ "$coldcard_setup_needed" == true ] ; then - pushd ../external/micropython/mpy-cross/ + git submodule update --init + + # Apply patch to make simulator work in linux environments + git am ../../data/coldcard-multisig.patch + + # Build the simulator. This is cached, but it is also fast + poetry run pip install -r requirements.txt + pip install -r requirements.txt + cd unix + if [ "$coldcard_setup_needed" == true ] ; then + pushd ../external/micropython/mpy-cross/ + make + popd + make setup + make ngu-setup + fi make - popd - make setup - make ngu-setup + cd ../.. + } + + if [[ -n ${build_coldcard_edge} ]]; then + BRANCH=edge + DIR=firmware-edge + do_coldcard_firmware ${DIR} ${BRANCH} + fi + + if [[ -n ${build_coldcard} ]]; then + BRANCH=master + DIR=firmware + do_coldcard_firmware ${DIR} ${BRANCH} fi - make - cd ../.. fi if [[ -n ${build_bitbox01} ]]; then diff --git a/test/test_coldcard.py b/test/test_coldcard.py index d049647b3..706b92abf 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -66,6 +66,8 @@ def start(self): for dev in enum_res: if dev["type"] == "coldcard" and "error" not in dev: found = True + if dev["label"] == "edge": + self.supports_taproot = True break if found: break @@ -136,21 +138,39 @@ def test_getxpub(self): self.assertEqual(result['chaincode'], '806b26507824f73bc331494afe122f428ef30dde80b2c1ce025d2d03aff411e7') self.assertEqual(result['pubkey'], '0368000bdff5e0b71421c37b8514de8acd4d98ba9908d183d9da56d02ca4fcfd08') -def coldcard_test_suite(simulator, bitcoind, interface): +def coldcard_test_suite(simulator, bitcoind, interface, is_edge=False): dev_emulator = ColdcardSimulator(simulator) signtx_cases = [ (["legacy"], ["legacy"], True, False), + (["legacy"], ["legacy"], False, True), + (["legacy"], ["legacy"], True, True), + (["legacy"], ["legacy"], False, False), (["segwit"], ["segwit"], True, False), - (["legacy", "segwit"], ["legacy", "segwit"], True, False), + (["segwit"], ["segwit"], False, True), + (["segwit"], ["segwit"], True, True), + (["segwit"], ["segwit"], False, False), ] + if is_edge: + signtx_cases += [ + (["tap"], [], False, True), + (["tap"], [], True, False), + (["tap"], [], True, True), + (["tap"], [], False, False), + ] + else: + signtx_cases += [ + (["legacy", "segwit"], ["legacy", "segwit"], True, False), + (["legacy", "segwit"], ["legacy", "segwit"], False, True), + (["legacy", "segwit"], ["legacy", "segwit"], True, True), + (["legacy", "segwit"], ["legacy", "segwit"], False, False), + ] # Generic device tests suite = unittest.TestSuite() suite.addTest(DeviceTestCase.parameterize(TestColdcardManCommands, bitcoind, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestColdcardGetXpub, bitcoind, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, bitcoind, emulator=dev_emulator, interface=interface, detect_type="coldcard")) - suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, bitcoind, emulator=dev_emulator, interface=interface, detect_type="coldcard")) suite.addTest(DeviceTestCase.parameterize(TestGetDescriptors, bitcoind, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, bitcoind, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, bitcoind, emulator=dev_emulator, interface=interface)) @@ -165,9 +185,10 @@ def coldcard_test_suite(simulator, bitcoind, interface): parser.add_argument('simulator', help='Path to the Coldcard simulator') parser.add_argument('bitcoind', help='Path to bitcoind binary') parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library') + parser.add_argument('--edge', help='Is this EDGE release', action='store_true') args = parser.parse_args() # Start bitcoind bitcoind = Bitcoind.create(args.bitcoind) - sys.exit(not coldcard_test_suite(args.simulator, bitcoind, args.interface)) + sys.exit(not coldcard_test_suite(args.simulator, bitcoind, args.interface, is_edge=args.edge))