def doit(): dlen, _ = dev.upload_file(firmware, verify=True) assert dlen == len(firmware) # append the firmware header a second time result = dev.send_recv(CCProtocolPacker.upload(size, size+FW_HEADER_SIZE, hdr)) # make it reboot into bootlaoder which might install it dev.send_recv(CCProtocolPacker.reboot())
def bag_number(number): "Factory: set or read bag number -- single use only!" dev = ColdcardDevice(sn=force_serial) nn = b'' if not number else number.encode('ascii') resp = dev.send_recv(CCProtocolPacker.bag_number(nn)) print("Bag number: %r" % resp)
async def create_user(self, username, authmode, new_pw=None): # typically we'll let Coldcard pick password if authmode == USER_AUTH_HMAC and new_pw: secret = self.dev.hash_password(new_pw.encode('utf8')) else: secret = b'' await self.send_recv( CCProtocolPacker.create_user(username, authmode, secret))
def ping_check(self): # check connection is working assert self.dev.session_key, 'not encrypted?' req = b'1234 Electrum Plugin 4321' # free up to 59 bytes try: echo = self.dev.send_recv(CCProtocolPacker.ping(req)) assert echo == req except: raise RuntimeError("Communication trouble with Coldcard")
def get_pubkey_at_path(self, path): path = path.replace('h', '\'') path = path.replace('H', '\'') xpub = self.device.send_recv(CCProtocolPacker.get_xpub(path), timeout=None) if self.is_testnet: return {'xpub': xpub_main_2_test(xpub)} else: return {'xpub': xpub}
def file_upload(filename, blksize, multisig=False): "Send file to Coldcard (PSBT transaction or firmware)" # NOTE: mostly for debug/dev usage. dev = ColdcardDevice(sn=force_serial) file_len, sha = real_file_upload(filename, blksize, dev=dev) if multisig: dev.send_recv(CCProtocolPacker.multisig_enroll(file_len, sha))
def file_upload(filename, blksize, multisig=False): """Send file to Coldcard (PSBT transaction or firmware)""" # NOTE: mostly for debug/dev usage. with get_device() as dev: file_len, sha = real_file_upload(filename, dev, blksize=blksize) if multisig: dev.send_recv(CCProtocolPacker.multisig_enroll(file_len, sha))
async def run(self): # connect to, and maintain a connection to a single Coldcard logging.info("Connecting to Coldcard.") while 1: try: if not self.serial and os.path.exists(settings.SIMULATOR_SOCK): # if simulator is running, just use it. sn = settings.SIMULATOR_SOCK else: sn = self.serial d = ColdcardDevice(sn=sn) logging.info(f"Found Coldcard {d.serial}.") await asyncio.get_running_loop().run_in_executor( executor, d.check_mitm) async with self.lock: self.dev = d except: logging.error("Cannot connect to Coldcard (will retry)", exc_info=0) await asyncio.sleep(settings.RECONNECT_DELAY) continue # stay connected, and check we are working periodically logging.info(f"Connected to Coldcard {self.dev.serial}.") STATUS.connected = True # read static info about coldcard STATUS.xfp = xfp2str(self.dev.master_fingerprint) STATUS.serial_number = self.dev.serial STATUS.is_testnet = (self.dev.master_xpub[0] == 't') STATUS.hsm = {} STATUS.reset_pending_auth() STATUS.notify_watchers() await self.hsm_status() while 1: await asyncio.sleep(settings.PING_RATE) try: # use long timeout here, even tho simple command, because the CC may # we working on something else right now (thinking). h = await self.send_recv(CCProtocolPacker.hsm_status(), timeout=20000) logging.info("ping ok") await self.hsm_status(h) except MissingColdcard: self._conn_broken() break except: logging.error("Ping failed", exc_info=1)
def get_xpub(subpath): """Get the XPUB for this wallet (master level, or any derivation)""" with get_device() as dev: if len(subpath) == 1: if subpath[0] == 'bip44': subpath = BIP44_FIRST xpub = dev.send_recv(CCProtocolPacker.get_xpub(subpath), timeout=None) click.echo(xpub)
def doit(config): # upload the file, trigger import file_len, sha = dev.upload_file(config.encode('ascii')) dev.send_recv(CCProtocolPacker.multisig_enroll(file_len, sha)) time.sleep(.2) title, story = cap_story() #print(repr(story)) return title, story
def get_block_chain(): '''Get which blockchain (Bitcoin/Testnet) is configured. BTC=>Bitcoin or XTN=>Bitcoin Testnet ''' dev = ColdcardDevice(sn=force_serial) code = dev.send_recv(CCProtocolPacker.block_chain()) click.echo(code)
def doit(accept=True, in_psbt=None, finalize=False, accept_ms_import=False): if accept_ms_import: # XXX would be better to do cap_story here, but that would limit test to simulator need_keypress('y') time.sleep(0.050) if accept != None: need_keypress('y' if accept else 'x') if accept == False: with pytest.raises(CCUserRefused): done = None while done == None: time.sleep(0.050) done = dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None) return else: done = None while done == None: time.sleep(0.050) done = dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None) assert len(done) == 2 resp_len, chk = done psbt_out = dev.download_file(resp_len, chk) if not finalize: if in_psbt: from psbt import BasicPSBT assert BasicPSBT().parse(in_psbt) != None else: from pycoin.tx.Tx import Tx # parse it res = psbt_out assert res[0:4] != b'psbt', 'still a PSBT, but asked for finalize' t = Tx.from_bin(res) assert t.version in [1, 2] return psbt_out
def sign_transaction(psbt_in, psbt_out, verbose=False, hex_mode=False, finalize=True): "Approve a spending transaction (by signing it on Coldcard)" dev = ColdcardDevice(sn=force_serial) dev.check_mitm() # not enforcing policy here on msg contents, so we can define that on product taste = psbt_in.read(10) psbt_in.seek(0) if taste == b'70736274ff': # hex encoded; make binary psbt_in = io.BytesIO(a2b_hex(psbt_in.read())) hex_mode = True elif taste[0:5] != b'psbt\xff': click.echo("File doesn't have PSBT magic number at start.") sys.exit(1) # upload the transaction txn_len, sha = real_file_upload(psbt_in, dev=dev) # start the signing process ok = dev.send_recv(CCProtocolPacker.sign_transaction(txn_len, sha), timeout=None) assert ok == None result, _ = wait_and_download(dev, CCProtocolPacker.get_signed_txn(), 1) if finalize: # assume(?) transaction is completely signed, and output the # bitcoin transaction to be sent. # XXX maybe do this on embedded side, when txn is final? # XXX otherwise, need to parse PSBT and also handle combining properly pass # save it psbt_out.write(b2a_hex(result) if hex_mode else result)
def get_pubkey(subpath): """ Get the public key for a derivation path Dump 33-byte (compressed, SEC encoded) public key value. """ with get_device() as dev: xpub = dev.send_recv(CCProtocolPacker.get_xpub(subpath), timeout=None) pubkey, _ = decode_xpub(xpub) vk = VerifyingKey.from_string(get_pubkey_string(pubkey), curve=SECP256k1) click.echo(b2a_hex(vk.to_string("compressed")))
def get_xpub(subpath): "Get the XPUB for this wallet (master level, or any derivation)" dev = ColdcardDevice(sn=force_serial) if len(subpath) == 1: if subpath[0] == 'bip44': subpath = BIP44_FIRST xpub = dev.send_recv(CCProtocolPacker.get_xpub(subpath), timeout=None) click.echo(xpub)
def doit(M, addr_fmt=None, do_import=True): passwords = ['Me', 'Myself', 'And I', ''] if 0: # WORKING, but slow .. and it's constant data keys = [] for pw in passwords: xfp = set_bip39_pw(pw) sk = dev.send_recv(CCProtocolPacker.get_xpub("m/45'")) node = BIP32Node.from_wallet_key(sk) keys.append((xfp, None, node)) assert len(set(x for x,_,_ in keys)) == 4, keys pprint(keys) else: # Much, FASTER! assert dev.is_simulator keys = [(3503269483, None, BIP32Node.from_hwif('tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9')), (2389277556, None, BIP32Node.from_hwif('tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc')), (3190206587, None, BIP32Node.from_hwif('tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa')), (1130956047, None, BIP32Node.from_hwif('tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n')), ] if do_import: # render as a file for import config = f"name: Myself-{M}\npolicy: {M} / 4\n\n" if addr_fmt: config += f'format: {addr_fmt.upper()}\n' config += '\n'.join('%s: %s' % (xfp2str(xfp), sk.hwif()) for xfp, _, sk in keys) #print(config) title, story = offer_ms_import(config) #print(story) # dont care if update or create; accept it. time.sleep(.1) need_keypress('y') def select_wallet(idx): # select to specific pw xfp = set_bip39_pw(passwords[idx]) assert xfp == keys[idx][0] return (keys, select_wallet)
def hsm_status(): """ Get current status of HSM feature. Is it running, what is the policy (summary only). """ with get_device() as dev: dev.check_mitm() resp = dev.send_recv(CCProtocolPacker.hsm_status()) o = json.loads(resp) click.echo(pformat(o))
def enroll_xpub(name, min_signers, path, num_signers, output_file=None, verbose=False, just_add=False): ''' Create a skeleton file which defines a multisig wallet. When completed, use with: "ckcc upload -m wallet.txt" or put on SD card. ''' dev = ColdcardDevice(sn=force_serial) dev.check_mitm() xfp = dev.master_fingerprint my_xpub = dev.send_recv(CCProtocolPacker.get_xpub(path), timeout=None) new_line = "%s: %s" % (xfp2str(xfp), my_xpub) if just_add: click.echo(new_line) sys.exit(0) N = num_signers if N < min_signers: N = min_signers if not (1 <= N < 15): click.echo("N must be 1..15") sys.exit(1) if min_signers == 0: min_signers = N if not (1 <= min_signers <= N): click.echo(f"Minimum number of signers (M) must be between 1 and N={N}") sys.exit(1) if not (1 <= len(name) <= 20) or name != str(name.encode('utf8'), 'ascii', 'ignore'): click.echo("Name must be between 1 and 20 characters of ASCII.") sys.exit(1) # render into a template config = f'name: {name}\npolicy: {min_signers} of {N}\n\n#path: {path}\n{new_line}\n' if num_signers != 1: config += '\n'.join(f'#{i+2}# FINGERPRINT: xpub123123123123123' for i in range(num_signers-1)) config += '\n' if verbose or not output_file: click.echo(config[:-1]) if output_file: output_file.write(config) output_file.close() click.echo(f"Wrote to: {output_file.name}")
def get_xpub(self, bip32_path, xtype): assert xtype in ColdcardPlugin.SUPPORTED_XTYPES print_error('[coldcard]', 'Derive xtype = %r' % xtype) xpub = self.dev.send_recv(CCProtocolPacker.get_xpub(bip32_path), timeout=5000) # TODO handle timeout? # change type of xpub to the requested type try: __, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub) except InvalidMasterKeyVersionBytes: raise UserFacingException(_('Invalid xpub magic. Make sure your {} device is set to the correct chain.') .format(self.device)) from None if xtype != 'standard': xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) return xpub
def sign_transaction_start(self, raw_psbt, finalize=True): # Multiple steps to sign: # - upload binary # - start signing UX # - wait for coldcard to complete process, or have it refused. # - download resulting txn assert 20 <= len(raw_psbt) < MAX_TXN_LEN, 'PSBT is too big' dlen, chk = self.dev.upload_file(raw_psbt) resp = self.dev.send_recv(CCProtocolPacker.sign_transaction(dlen, chk, finalize=finalize), timeout=None) if resp != None: raise ValueError(resp)
def get_xpub(self, bip32_path, xtype): assert xtype in ColdcardPlugin.SUPPORTED_XTYPES print_error('[coldcard]', 'Derive xtype = %r' % xtype) xpub = self.dev.send_recv(CCProtocolPacker.get_xpub(bip32_path), timeout=5000) # TODO handle timeout? # change type of xpub to the requested type try: __, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub) except InvalidMasterKeyVersionBytes: raise Exception(_('Invalid xpub magic. Make sure your {} device is set to the correct chain.') .format(self.device)) from None if xtype != 'standard': xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) return xpub
def get_xpub(self, bip32_path, xtype): assert xtype in ColdcardPlugin.SUPPORTED_XTYPES print_error('[coldcard]', 'Derive xtype = %r' % xtype) xpub = self.dev.send_recv(CCProtocolPacker.get_xpub(bip32_path), timeout=5000) # TODO handle timeout? # change type of xpub to the requested type try: node = BIP32Node.from_xkey(xpub) except InvalidMasterKeyVersionBytes: raise UserFacingException(_('Invalid xpub magic. Make sure your {} device is set to the correct chain.') .format(self.device)) from None if xtype != 'standard': xpub = node._replace(xtype=xtype).to_xpub() return xpub
def get_xpub(self, bip32_path, xtype): assert xtype in ColdcardPlugin.SUPPORTED_XTYPES _logger.info('Derive xtype = %r' % xtype) xpub = self.dev.send_recv(CCProtocolPacker.get_xpub(bip32_path), timeout=5000) # TODO handle timeout? # change type of xpub to the requested type try: node = BIP32Node.from_xkey(xpub) except InvalidMasterKeyVersionBytes: raise UserFacingException(_('Invalid xpub magic. Make sure your {} device is set to the correct chain.') .format(self.device)) from None if xtype != 'standard': xpub = node._replace(xtype=xtype).to_xpub() return xpub
def doit(filename, finalize=False, stxn_flags=0x0): if filename[0:5] == b'psbt\xff': ip = filename filename = 'memory' else: ip = open(filename, 'rb').read() if ip[0:10] == b'70736274ff': ip = a2b_hex(ip.strip()) assert ip[0:5] == b'psbt\xff' ll, sha = dev.upload_file(ip) dev.send_recv(CCProtocolPacker.sign_transaction(ll, sha, finalize, flags=stxn_flags)) return ip
def hsm_status(): ''' Get current status of HSM feature. Is it running, what is the policy (summary only). ''' dev = ColdcardDevice(sn=force_serial) dev.check_mitm() resp = dev.send_recv(CCProtocolPacker.hsm_status()) o = json.loads(resp) click.echo(pformat(o))
async def user_auth(self, username, token, totp, psbt_hash): if len(token) == 6 and token.isdigit(): # assume TOTP if token (password) is 6-numeric digits totp_time = totp or int(time.time() // 30) token = token.encode('ascii') else: # assume it's a raw password. need to hash it up # TODO: move this hashing into browser secret = self.dev.hash_password(token.encode('utf8')) token = HMAC(secret, msg=psbt_hash, digestmod=sha256).digest() totp_time = 0 await self.send_recv( CCProtocolPacker.user_auth(username.encode('ascii'), token, totp_time))
def doit(M, N, addr_fmt=None, name=None, unique=0, accept=False, common=None, keys=None, do_import=True): keys = keys or make_multisig(M, N, unique=unique) if not do_import: return keys # render as a file for import name = name or f'test-{M}-{N}' config = f"name: {name}\npolicy: {M} / {N}\n\n" if addr_fmt: config += f'format: {addr_fmt.title()}\n' if common: config += f'derivation: {common}\n' config += '\n'.join('%s: %s' % (xfp2str(xfp), dd.hwif(as_private=False)) for xfp, m, dd in keys) #print(config) title, story = offer_ms_import(config) assert 'Create new multisig' in story assert name in story assert f'Policy: {M} of {N}\n' in story if accept: time.sleep(.1) need_keypress('y') # Test it worked. time.sleep(.1) # required xor = 0 for xfp, _, _ in keys: xor ^= xfp assert dev.send_recv(CCProtocolPacker.multisig_check(M, N, xor)) == 1 return keys
def master_xpub(dev): r = dev.send_recv(CCProtocolPacker.get_xpub('m'), timeout=None, encrypt=1) assert r[1:4] == 'pub', r if r[0:4] == dev.master_xpub[0:4]: assert r == dev.master_xpub elif dev.master_xpub: # testnet vs. mainnet difference from pycoin.key.BIP32Node import BIP32Node a = BIP32Node.from_wallet_key(r) b = BIP32Node.from_wallet_key(dev.master_xpub) assert a.secret_exponent() == b.secret_exponent() return r
def show_address(script, fingerprints, quiet=False, segwit=False, wrap=False): '''Show a multisig payment address on-screen. Needs a redeem script and list of fingerprint/path (4369050F/1/0/0 for example). This is provided as a demo or debug feature. You'll need need some way to generate the full redeem script (hex), and the fingerprints and paths used to generate each public key inside that. The order of fingerprint/paths must match order of pubkeys in the script. ''' dev = ColdcardDevice(sn=force_serial) addr_fmt = AF_P2SH if segwit: addr_fmt = AF_P2WSH if wrap: addr_fmt = AF_P2WSH_P2SH script = a2b_hex(script) N = len(fingerprints) assert 1 <= N <= 15, "bad N" min_signers = script[0] - 80 assert 1 <= min_signers <= N, "bad M" assert script[-1] == 0xAE, "expect script to end with OP_CHECKMULTISIG" assert script[-2] == 80 + N, "second last byte should encode N" xfp_paths = [] for idx, xfp in enumerate(fingerprints): assert '/' in xfp, 'Needs a XFP/path: ' + xfp xfp, p = xfp.split('/', 1) xfp_paths.append(str_to_int_path(xfp, p)) addr = dev.send_recv(CCProtocolPacker.show_p2sh_address(min_signers, xfp_paths, script, addr_fmt=addr_fmt), timeout=None) if quiet: click.echo(addr) else: click.echo('Displaying address:\n\n%s\n' % addr)
def doit(k): if hasattr(dev.dev, 'pipe'): # simulator has special USB command dev.send_recv(CCProtocolPacker.sim_keypress(k.encode('ascii'))) elif request.config.getoption("--manual"): # need actual user interaction print("==> NOW, on the Coldcard, press key: %r" % k, file=sys.stderr) else: # try to use debug interface to simulate the press # XXX for some reason, picocom must **already** be running for this to work. # - otherwise, this locks up devs = list(glob.glob('/dev/tty.usbmodem*')) if len(devs) == 1: with open(devs[0], 'wb', 0) as fd: fd.write(k.encode('ascii')) else: raise pytest.fail('need to provide keypresses')
def show_address(path, quiet=False, segwit=False, wrap=False): """Show the human version of an address""" with get_device() as dev: if wrap: addr_fmt = AF_P2WPKH_P2SH elif segwit: addr_fmt = AF_P2WPKH else: addr_fmt = AF_CLASSIC addr = dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None) if quiet: click.echo(addr) else: click.echo('Displaying address:\n\n%s\n' % addr)
def get_pubkey(subpath): '''Get the public key for a derivation path Dump 33-byte (compressed, SEC encoded) public key value. ''' try: from pycoin.key.BIP32Node import BIP32Node except: raise click.Abort("pycoin must be installed, not found.") dev = ColdcardDevice(sn=force_serial) xpub = dev.send_recv(CCProtocolPacker.get_xpub(subpath), timeout=None) node = BIP32Node.from_hwif(xpub) click.echo(b2a_hex(node.sec()))
def show_address(path, quiet=False, segwit=False, wrap=False): "Show the human version of an address" dev = ColdcardDevice(sn=force_serial) if wrap: addr_fmt = AF_P2WPKH_P2SH elif segwit: addr_fmt = AF_P2WPKH else: addr_fmt = AF_CLASSIC addr = dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None) if quiet: print(addr) else: print('Displaying address:\n\n%s\n' % addr)
def sign_transaction_poll(self): # poll device... if user has approved, will get tuple: (legnth, checksum) else None return self.dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None)
def show_address(self, path, addr_fmt): # prompt user w/ addres, also returns it immediately. return self.dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None)
def sign_message_start(self, path, msg): # this starts the UX experience. self.dev.send_recv(CCProtocolPacker.sign_message(msg, path), timeout=None)
def sign_message_poll(self): # poll device... if user has approved, will get tuple: (addr, sig) else None return self.dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None)
def get_version(self): # gives list of strings return self.dev.send_recv(CCProtocolPacker.version(), timeout=1000).split('\n')