def start_backup(outdir, outfile, verbose=False): '''Creates 7z encrypted backup file after prompting user to remember a massive passphrase. \ Downloads the AES-encrypted data backup and by default, saves into current directory using \ a filename based on today's date.''' dev = ColdcardDevice(sn=force_serial) dev.check_mitm() ok = dev.send_recv(CCProtocolPacker.start_backup()) assert ok == None result, chk = wait_and_download(dev, CCProtocolPacker.get_backup_file(), 0) if outfile: outfile.write(result) outfile.close() fn = outfile.name else: assert outdir # pick a useful filename, if they gave a dirname fn = os.path.join(outdir, time.strftime('backup-%Y%m%d-%H%M.7z')) open(fn, 'wb').write(result) click.echo("Wrote %d bytes into: %s\nSHA256: %s" % (len(result), fn, str(b2a_hex(chk), 'ascii')))
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 show_address(path_prefix, script, fingerprints, min_signers=0, quiet=False, segwit=False, wrap=False): '''Show a multisig payment address on-screen Append subkey path to fingerprint value (4369050F/1/0/0 for example) or omit for 0/0/0 This is provided as a demo or debug feature: you'll need the full redeem script (hex), and the fingerprints and paths used to generate each public key inside that. Order of fingerprint/path must match order of pubkeys in 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) if N <= 16: if not min_signers: assert N <= 15 min_signers = script[0] - 80 else: assert min_signers == script[0], "M conficts with script" 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): if '/' not in xfp: # this isn't BIP45 compliant but we don't know the cosigner's index # values, since they would have been suffled when the redeem script is sorted p = '0/0/0' else: # better if all paths provided xfp, p = xfp.split('/', 1) p = path_prefix + p 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 sign_transaction(psbt_in, psbt_out=None, verbose=False, b64_mode=False, hex_mode=False, finalize=False, visualize=False, signed=False): "Approve a spending transaction by signing it on Coldcard" dev = ColdcardDevice(sn=force_serial) dev.check_mitm() # Handle non-binary encodings, and incorrect files. taste = psbt_in.read(10) psbt_in.seek(0) if taste == b'70736274ff' or taste == b'70736274FF': # Looks hex encoded; make into binary again hx = ''.join(re.findall(r'[0-9a-fA-F]*', psbt_in.read().decode('ascii'))) psbt_in = io.BytesIO(a2b_hex(hx)) elif taste[0:6] == b'cHNidP': # Base64 encoded input psbt_in = io.BytesIO(b64decode(psbt_in.read())) 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) flags = 0x0 if visualize or signed: flags |= STXN_VISUALIZE if signed: flags |= STXN_SIGNED elif finalize: flags |= STXN_FINALIZE # start the signing process ok = dev.send_recv(CCProtocolPacker.sign_transaction(txn_len, sha, flags=flags), timeout=None) assert ok == None # errors will raise here, no need for error display result, _ = wait_and_download(dev, CCProtocolPacker.get_signed_txn(), 1) # If 'finalize' is set, we are outputing a bitcoin transaction, # ready for the p2p network. If the CC wasn't able to finalize it, # an exception would have occured. Most people will want hex here, but # resisting the urge to force it. if visualize: if psbt_out: psbt_out.write(result) else: click.echo(result, nl=False) else: # save it if hex_mode: result = b2a_hex(result) elif b64_mode or (not psbt_out and os.isatty(0)): result = b64encode(result) if psbt_out: psbt_out.write(result) else: click.echo(result)
def sign_message(message, path, verbose=True, just_sig=False, wrap=False, segwit=False): "Sign a short text message" dev = ColdcardDevice(sn=force_serial) if wrap: addr_fmt = AF_P2WPKH_P2SH elif segwit: addr_fmt = AF_P2WPKH else: addr_fmt = AF_CLASSIC # NOTE: initial version of firmware not expected to do segwit stuff right, since # standard very much still in flux, see: <https://github.com/bitcoin/bitcoin/issues/10542> # not enforcing policy here on msg contents, so we can define that on product message = message.encode('ascii') if not isinstance(message, bytes) else message ok = dev.send_recv(CCProtocolPacker.sign_message(message, path, addr_fmt), timeout=None) assert ok == None print("Waiting for OK on the Coldcard...", end='', file=sys.stderr) sys.stderr.flush() while 1: time.sleep(0.250) done = dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None) if done == None: continue break print("\r \r", end='', file=sys.stderr) sys.stderr.flush() if len(done) != 2: click.echo('Failed: %r' % done) sys.exit(1) addr, raw = done sig = str(b64encode(raw), 'ascii').replace('\n', '') if just_sig: click.echo(str(sig)) elif verbose: click.echo( '-----BEGIN SIGNED MESSAGE-----\n{msg}\n-----BEGIN ' 'SIGNATURE-----\n{addr}\n{sig}\n-----END SIGNED MESSAGE-----'. format(msg=message.decode('ascii'), addr=addr, sig=sig)) else: click.echo('%s\n%s\n%s' % (message.decode('ascii'), addr, sig))
def get_storage_locker(): "Get the value held in the Storage Locker (not Bitcoin related, reserved for HSM use)" dev = ColdcardDevice(sn=force_serial) ls = dev.send_recv(CCProtocolPacker.get_storage_locker(), timeout=None) click.echo(ls)
def get_version(): "Get the version of the firmware installed" dev = ColdcardDevice(sn=force_serial) v = dev.send_recv(CCProtocolPacker.version()) click.echo(v)
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)
def __init__(self, path, password=''): super(ColdcardClient, self).__init__(path, password) # Simulator hard coded pipe socket if path == CC_SIMULATOR_SOCK: self.device = ColdcardDevice(sn=path) else: device = hid.device() device.open_path(path.encode()) self.device = ColdcardDevice(dev=device)
def run_eval(stmt): "Simulator only: exec a python script" dev = ColdcardDevice(sn=force_serial) stmt = ' '.join(stmt) v = dev.send_recv(b'EXEC' + stmt.encode('utf-8')) click.echo(v)
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 run_eval(stmt): "Simulator only: eval a python statement" dev = ColdcardDevice(sn=force_serial) stmt = ' '.join(stmt) v = dev.send_recv(b'EVAL' + stmt.encode('utf-8')) print(v)
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 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 get_device(optional=False): # Open connection to Coldcard as a context with auto-close try: device = ColdcardDevice(sn=force_serial, encrypt=not force_plaintext) except KeyError: if not optional: raise device = None yield device if device: device.close()
def hsm_setup(policy=None, dry_run=False): ''' Enable Hardware Security Module (HSM) mode. Upload policy file (or use existing policy) and start HSM mode on device. User must approve startup. All PSBT's will be signed automatically based on that policy. ''' dev = ColdcardDevice(sn=force_serial) dev.check_mitm() if policy: if dry_run: # check it looks reasonable, but jsut a JSON check raw = open(policy, 'rt').read() j = json.loads(raw) click.echo("Policy ok") sys.exit(0) file_len, sha = real_file_upload(open(policy, 'rb'), dev=dev) dev.send_recv(CCProtocolPacker.hsm_start(file_len, sha)) else: if dry_run: raise click.UsageError("Dry run not useful without a policy file to check.") dev.send_recv(CCProtocolPacker.hsm_start()) click.echo("Approve HSM policy on Coldcard screen.")
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))
def debug(): "Start interactive (local) debug session" import code import readline import atexit import os class HistoryConsole(code.InteractiveConsole): def __init__(self, locals=None, filename="<console>", histfile=os.path.expanduser("~/.console-history")): code.InteractiveConsole.__init__(self, locals, filename) self.init_history(histfile) def init_history(self, histfile): readline.parse_and_bind("tab: complete") if hasattr(readline, "read_history_file"): try: readline.read_history_file(histfile) except IOError: pass atexit.register(self.save_history, histfile) def save_history(self, histfile): readline.write_history_file(histfile) # useful stuff import pdb from pdb import pm CC = ColdcardDevice(sn=force_serial, encrypt=False) SR = CC.send_recv cli = HistoryConsole(locals=dict(globals(), **locals())) cli.interact(banner="Go for it: 'CC' is the connected device, SR=CC.send_recv", exitmsg='')
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 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: 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()))
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 usb_test(single): "Test USB connection (debug/dev)" dev = ColdcardDevice(sn=force_serial) rng = [] rng.extend(range(55, 66)) # buggy lengths are around 64 rng.extend(range(1013, 1024)) # we have 4 bytes of overhead (args) for ping cmd, so this will be max-length rng.extend(range(MAX_MSG_LEN-10, MAX_MSG_LEN-4)) #print(repr(rng)) for i in rng: print("Ping with length: %d" % i, end='') body = os.urandom(i) if single is None else bytes([single]*i) rb = dev.send_recv(CCProtocolPacker.ping(body)) assert rb == body, "Fail @ len: %d, got back %d bytes\n%r !=\n%r" % ( i, len(rb), b2a_hex(body), b2a_hex(rb)) print(" Okay")
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 bip39_passphrase(passphrase, verbose=False): "Provide a BIP39 passphrase" dev = ColdcardDevice(sn=force_serial) dev.check_mitm() ok = dev.send_recv(CCProtocolPacker.bip39_passphrase(passphrase), timeout=None) assert ok == None print("Waiting for OK on the Coldcard...", end='', file=sys.stderr) sys.stderr.flush() while 1: time.sleep(0.250) done = dev.send_recv(CCProtocolPacker.get_passphrase_done(), timeout=None) if done == None: continue break print("\r \r", end='', file=sys.stderr) sys.stderr.flush() if verbose: xpub = done click.echo(xpub) else: click.echo('Done.')
def get_fingerprint(swab): "Get the fingerprint for this wallet (master level)" dev = ColdcardDevice(sn=force_serial) xfp = dev.master_fingerprint assert xfp if swab: xfp = struct.unpack("<I", struct.pack(">I", xfp))[0] click.echo('0x%08x' % xfp) click.echo(xfp2str(xfp))
def user_auth(psbt_file, next_code=None): ''' Generate the 6-digit code needed for a specific PSBT file to authorize it's signing on the Coldcard in HSM mode. ''' if not next_code: dev = ColdcardDevice(sn=force_serial) dev.check_mitm() resp = dev.send_recv(CCProtocolPacker.hsm_status()) o = json.loads(resp) assert o['active'], "Coldcard not in HSM mode" next_code = o['next_local_code'] psbt_hash = sha256(psbt_file.read()).digest() rv = calc_local_pincode(psbt_hash, next_code) print("Local authorization code is:\n\n\t%s\n" % rv)
def get_fingerprint(swab): "Get the fingerprint for this wallet (master level)" dev = ColdcardDevice(sn=force_serial) xfp = dev.master_fingerprint assert xfp if swab: # this is how we used to show XFP values: LE32 hex with 0x in front. click.echo('0x%08x' % xfp) else: # network order = BE32 = top 32-bits of hash160(pubkey) = 4 bytes in bip32 serialization click.echo(xfp2str(xfp))
def __init__(self, device): super(ColdCardClient, self).__init__(device) self.device = ColdcardDevice(dev=device)
class ColdCardClient(HardwareWalletClient): # device is an HID device that has already been opened. def __init__(self, device): super(ColdCardClient, self).__init__(device) self.device = ColdcardDevice(dev=device) # Must return a dict with the xpub # Retrieves the public key at the specified BIP 32 derivation path 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} # Must return a hex string with the signed transaction # The tx must be in the combined unsigned transaction format def sign_tx(self, tx): self.device.check_mitm() # Get psbt in hex and then make binary fd = io.BytesIO(base64.b64decode(tx.serialize())) # learn size (portable way) offset = 0 sz = fd.seek(0, 2) fd.seek(0) 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 chk.update(here) # do a verify expect = chk.digest() result = self.device.send_recv(CCProtocolPacker.sha256()) assert len(result) == 32 if result != expect: raise ValueError("Wrong checksum:\nexpect: %s\n got: %s" % (b2a_hex(expect).decode('ascii'), b2a_hex(result).decode('ascii'))) # start the signing process ok = self.device.send_recv(CCProtocolPacker.sign_transaction( sz, expect), timeout=None) assert ok == None print("Waiting for OK on the Coldcard...") while 1: time.sleep(0.250) done = self.device.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None) if done == None: continue break if len(done) != 2: raise ValueError('Failed: %r' % done) result_len, result_sha = done result = self.device.download_file(result_len, result_sha, file_number=1) return {'psbt': base64.b64encode(result).decode()} # Must return a base64 encoded string with the signed message # The message can be any string. keypath is the bip 32 derivation path for the key to sign with def sign_message(self, message, keypath): raise NotImplementedError( 'The HardwareWalletClient base class does not ' 'implement this method') # Display address of specified type on the device. Only supports single-key based addresses. def display_address(self, keypath, p2sh_p2wpkh, bech32): raise NotImplementedError( 'The HardwareWalletClient base class does not ' 'implement this method') # Setup a new device def setup_device(self): raise NotImplementedError( 'The HardwareWalletClient base class does not ' 'implement this method') # Wipe this device def wipe_device(self): raise NotImplementedError( 'The HardwareWalletClient base class does not ' 'implement this method') # Close the device def close(self): self.device.close()