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 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 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 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 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 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 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)
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()
def user_auth(username, token=None, password=None, prompt=None, totp=None, psbt_file=None, debug=False): ''' Indicate specific user is present (for HSM). Username and 2FA (TOTP, 6-digits) value or password are required. To use password, the PSBT file in question must be provided. ''' import time from hmac import HMAC from hashlib import pbkdf2_hmac, sha256 dryrun = True dev = ColdcardDevice(sn=force_serial) dev.check_mitm() if psbt_file or password: if psbt_file: psbt_hash = sha256(psbt_file.read()).digest() dryrun = False else: psbt_hash = bytes(32) pw = token or click.prompt('Password (hidden)', hide_input=True) secret = dev.hash_password(pw.encode('utf8')) token = HMAC(secret, msg=psbt_hash, digestmod=sha256).digest() if debug: click.echo(" secret = %s" % B2A(secret)) click.echo(" salt = %s" % B2A(salt)) totp_time = 0 else: if not token: token = click.prompt('2FA Token (6 digits)', hide_input=False) if len(token) != 6 or not token.isdigit(): raise click.UsageError("2FA Token must be 6 decimal digits") token = token.encode('ascii') now = int(time.time()) if now % 30 < 5: click.echo("NOTE: TOTP was on edge of expiry limit! Might not work.") totp_time = now // 30 #raise click.UsageError("Need PSBT file as part of HMAC for password") assert token and len(token) in {6, 32} username = username.encode('ascii') if debug: click.echo(" username = %s" % username.decode('ascii')) click.echo(" token = %s" % (B2A(token) if len(token) > 6 else token.decode('ascii'))) click.echo("totp_time = %d" % totp_time) resp = dev.send_recv(CCProtocolPacker.user_auth(username, token, totp_time)) if not resp: click.echo("Correct or queued") else: click.echo(f'Problem: {resp}')
def new_user(username, totp_create=False, totp_secret=None, text_secret=None, ask_pass=False, do_delete=False, debug=False, show_qr=False, hotp=False, pick_pass=False): ''' Create a new user on the Coldcard for HSM policy (also delete). You can input a password (interactively), or one can be picked by the Coldcard. When possible the QR to enrol your 2FA app will be shown on the Coldcard screen. ''' from base64 import b32encode, b32decode username = username.encode('ascii') assert 1 <= len(username) <= MAX_USERNAME_LEN, "Username length wrong" dev = ColdcardDevice(sn=force_serial) dev.check_mitm() if do_delete: dev.send_recv(CCProtocolPacker.delete_user(username)) click.echo('Deleted, if it was there') return if ask_pass: assert not text_secret, "dont give and ask for password" text_secret = click.prompt('Password (hidden)', hide_input=True, confirmation_prompt=True) mode = USER_AUTH_HMAC if totp_secret: secret = b32decode(totp_secret, casefold=True) assert len(secret) in {10, 20} mode = USER_AUTH_TOTP elif hotp: mode = USER_AUTH_HOTP secret = b'' elif pick_pass or text_secret: mode = USER_AUTH_HMAC else: # default is TOTP secret = b'' mode = USER_AUTH_TOTP if mode == USER_AUTH_HMAC: # default is text passwords secret = dev.hash_password(text_secret.encode('utf8')) if text_secret else b'' assert not show_qr, 'QR not appropriate for text passwords' if not secret and not show_qr: # ask the Coldcard to show the QR (for password or TOTP shared secret) mode |= USER_AUTH_SHOW_QR new_secret = dev.send_recv(CCProtocolPacker.create_user(username, mode, secret)) if show_qr and new_secret: # format the URL thing ... needs a spec username = username.decode('ascii') secret = new_secret or b32encode(secret).decode('ascii') mode = 'hotp' if mode == USER_AUTH_HOTP else 'totp' click.echo(f'otpauth://{mode}/{username}?secret={secret}&issuer=Coldcard%20{dev.serial}') elif not text_secret and new_secret: click.echo(f'New password is: {new_secret}') else: click.echo('Done')
class ColdcardClient(HardwareWalletClient): 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) # 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): self.device.check_mitm() 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 if self.device.is_simulator: self.device.send_recv(CCProtocolPacker.sim_keypress(b'y')) 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): self.device.check_mitm() keypath = keypath.replace('h', '\'') keypath = keypath.replace('H', '\'') try: ok = self.device.send_recv(CCProtocolPacker.sign_message( message.encode(), keypath, AF_CLASSIC), timeout=None) assert ok == None if self.device.is_simulator: self.device.send_recv(CCProtocolPacker.sim_keypress(b'y')) except CCProtoError as e: raise ValueError(str(e)) while 1: time.sleep(0.250) done = self.device.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None) if done == None: continue break if len(done) != 2: raise ValueError('Failed: %r' % done) addr, raw = done sig = str(base64.b64encode(raw), 'ascii').replace('\n', '') return {"signature": sig} # Display address of specified type on the device. Only supports single-key based addresses. def display_address(self, keypath, p2sh_p2wpkh, bech32): self.device.check_mitm() keypath = keypath.replace('h', '\'') keypath = keypath.replace('H', '\'') if p2sh_p2wpkh: format = AF_P2WPKH_P2SH elif bech32: format = AF_P2WPKH else: format = AF_CLASSIC address = self.device.send_recv(CCProtocolPacker.show_address( keypath, format), timeout=None) if self.device.is_simulator: self.device.send_recv(CCProtocolPacker.sim_keypress(b'y')) return {'address': address} # Setup a new device def setup_device(self, label='', passphrase=''): raise UnavailableActionError( 'The Coldcard does not support software setup') # Wipe this device def wipe_device(self): raise UnavailableActionError( 'The Coldcard does not support wiping via software') # Restore device from mnemonic or xprv def restore_device(self, label=''): raise UnavailableActionError( 'The Coldcard does not support restoring via software') # Begin backup process def backup_device(self, label='', passphrase=''): self.device.check_mitm() ok = self.device.send_recv(CCProtocolPacker.start_backup()) assert ok == None while 1: time.sleep(0.250) done = self.device.send_recv(CCProtocolPacker.get_backup_file(), 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=0) filename = time.strftime('backup-%Y%m%d-%H%M.7z') open(filename, 'wb').write(result) return { 'success': True, 'message': 'The backup has be written to {}'.format(filename) } # Close the device def close(self): self.device.close()