async def set_highwater(*a): # rarely? used command import callgate have = version.get_mpy_version()[0] ts = version.get_header_value('timestamp') hw = callgate.get_highwater() if hw == ts: await ux_show_story( '''Current version (%s) already marked as high-water mark.''' % have) return ok = await ux_confirm( '''Mark current version (%s) as the minimum, and prevent any downgrades below this version. Rarely needed as critical security updates will set this automatically.''' % have) if not ok: return rv = callgate.set_highwater(ts) # add error display here? meh. assert rv == 0, "Failed: %r" % rv
async def show_version(*a): # show firmware, bootload versions. from main import settings import callgate, version from ubinascii import hexlify as b2a_hex built, rel, *_ = version.get_mpy_version() bl = callgate.get_bl_version()[0] chk = str(b2a_hex(callgate.get_bl_checksum(0))[-8:], 'ascii') msg = '''\ Coldcard Firmware {rel} {built} Bootloader: {bl} {chk} Serial: {ser} Hardware: {hw} ''' await ux_show_story( msg.format(rel=rel, built=built, bl=bl, chk=chk, ser=version.serial_number(), hw=version.hw_label))
def splash(self): # display a splash screen with some version numbers self.clear() y = 4 self.text(None, y, 'Coldcard', font=FontLarge) self.text(None, y+20, 'Wallet', font=FontLarge) from version import get_mpy_version timestamp, label, *_ = get_mpy_version() y = self.HEIGHT-10 self.text(0, y, 'Version '+label, font=FontTiny) self.text(-1, y, timestamp, font=FontTiny) self.show()
def generate_bitcoin_core_wallet(example_addrs, account_num): # Generate the data for an RPC command to import keys into Bitcoin Core # - yields dicts for json purposes from descriptor import append_checksum from main import settings import ustruct from public_constants import AF_P2WPKH chain = chains.current_chain() derive = "84'/{coin_type}'/{account}'".format(account=account_num, coin_type=chain.b44_cointype) with stash.SensitiveValues() as sv: prefix = sv.derive_path(derive) xpub = chain.serialize_public(prefix) for i in range(3): sp = '0/%d' % i node = sv.derive_path(sp, master=prefix) a = chain.address(node, AF_P2WPKH) example_addrs.append(('m/%s/%s' % (derive, sp), a)) xfp = settings.get('xfp') txt_xfp = xfp2str(xfp).lower() chain = chains.current_chain() _, vers, _ = version.get_mpy_version() for internal in [False, True]: desc = "wpkh([{fingerprint}/{derive}]{xpub}/{change}/*)".format( derive=derive.replace("'", "h"), fingerprint=txt_xfp, coin_type=chain.b44_cointype, account=0, xpub=xpub, change=(1 if internal else 0)) yield { 'desc': append_checksum(desc), 'range': [0, 1000], 'timestamp': 'now', 'internal': internal, 'keypool': True, 'watchonly': True }
def generate_wasabi_wallet(): # Generate the data for a JSON file which Wasabi can open directly as a new wallet. import ustruct, version # bitcoin (xpub) is used, even for testnet case (ie. no tpub) # - altho, doesn't matter; the wallet operates based on it's own settings for test/mainnet # regardless of the contents of the wallet file btc = chains.BitcoinMain with stash.SensitiveValues() as sv: xpub = btc.serialize_public(sv.derive_path("84'/0'/0'")) xfp = settings.get('xfp') txt_xfp = xfp2str(xfp) chain = chains.current_chain() assert chain.ctype in {'BTC', 'XTN'}, "Only Bitcoin supported" _,vers,_ = version.get_mpy_version() return dict(MasterFingerprint=txt_xfp, ColdCardFirmwareVersion=vers, ExtPubKey=xpub)
def render_backup_contents(): # simple text format: # key = value # or #comments # but value is JSON from main import settings, pa rv = StringIO() def COMMENT(val=None): if val: rv.write('\n# %s\n' % val) else: rv.write('\n') def ADD(key, val): rv.write('%s = %s\n' % (key, ujson.dumps(val))) rv.write('# Coldcard backup file! DO NOT CHANGE.\n') chain = chains.current_chain() COMMENT('Private key details: ' + chain.name) with stash.SensitiveValues(for_backup=True) as sv: if sv.mode == 'words': ADD('mnemonic', tcc.bip39.from_data(sv.raw)) if sv.mode == 'master': ADD('bip32_master_key', b2a_hex(sv.raw)) ADD('chain', chain.ctype) ADD('xprv', chain.serialize_private(sv.node)) ADD('xpub', chain.serialize_public(sv.node)) # BTW: everything is really a duplicate of this value ADD('raw_secret', b2a_hex(sv.secret).rstrip(b'0')) if pa.has_duress_pin(): COMMENT('Duress Wallet (informational)') dpk = sv.duress_root() ADD('duress_xprv', chain.serialize_private(dpk)) ADD('duress_xpub', chain.serialize_public(dpk)) if version.has_608: # save the so-called long-secret ADD('long_secret', b2a_hex(pa.ls_fetch())) COMMENT('Firmware version (informational)') date, vers, timestamp = version.get_mpy_version()[0:3] ADD('fw_date', date) ADD('fw_version', vers) ADD('fw_timestamp', timestamp) ADD('serial', version.serial_number()) COMMENT('User preferences') # user preferences for k, v in settings.current.items(): if k[0] == '_': continue # debug stuff in simulator if k == 'xpub': continue # redundant, and wrong if bip39pw if k == 'xfp': continue # redundant, and wrong if bip39pw ADD('setting.' + k, v) if version.has_fatram: import hsm if hsm.hsm_policy_available(): ADD('hsm_policy', hsm.capture_backup()) rv.write('\n# EOF\n') return rv.getvalue()
async def handle(self, cmd, args): # Dispatch incoming message, and provide reply. from main import hsm_active, is_devmode try: cmd = bytes(cmd).decode() except: raise FramingError('decode') if cmd[0].isupper() and (is_simulator() or is_devmode): # special hacky commands to support testing w/ the simulator try: from usb_test_commands import do_usb_command return do_usb_command(cmd, args) except: raise pass if hsm_active: # only a few commands are allowed during HSM mode if cmd not in HSM_WHITELIST: raise HSMDenied if cmd == 'dfu_': # only useful in factory, undocumented. return self.call_after(callgate.enter_dfu) if cmd == 'rebo': import machine return self.call_after(machine.reset) if cmd == 'logo': from utils import clean_shutdown return self.call_after(clean_shutdown) if cmd == 'ping': return b'biny' + args if cmd == 'upld': offset, total_size = unpack_from('<II', args) data = memoryview(args)[4 + 4:] return await self.handle_upload(offset, total_size, data) if cmd == 'dwld': offset, length, fileno = unpack_from('<III', args) return await self.handle_download(offset, length, fileno) if cmd == 'ncry': version, his_pubkey = unpack_from('<I64s', args) return self.handle_crypto_setup(version, his_pubkey) if cmd == 'vers': from version import get_mpy_version, hw_label from callgate import get_bl_version # Returning: date, version(human), bootloader version, full date version # BUT: be ready for additions! rv = list(get_mpy_version()) rv.insert(2, get_bl_version()[0]) rv.append(hw_label) return b'asci' + ('\n'.join(rv)).encode() if cmd == 'sha2': return b'biny' + self.file_checksum.digest() if cmd == 'xpub': assert self.encrypted_req, 'must encrypt' return self.handle_xpub(args) if cmd == 'mitm': assert self.encrypted_req, 'must encrypt' return await self.handle_mitm_check() if cmd == 'smsg': # sign message addr_fmt, len_subpath, len_msg = unpack_from('<III', args) subpath = args[12:12 + len_subpath] msg = args[12 + len_subpath:] assert len(msg) == len_msg, "badlen" from auth import sign_msg sign_msg(msg, subpath, addr_fmt) return None if cmd == 'p2sh': # show P2SH (probably multisig) address on screen (also provides it back) # - must provide redeem script, and list of [xfp+path] from auth import start_show_p2sh_address if hsm_active and not hsm_active.approve_address_share( is_p2sh=True): raise HSMDenied # new multsig goodness, needs mapping from xfp->path and M values addr_fmt, M, N, script_len = unpack_from('<IBBH', args) assert addr_fmt & AFC_SCRIPT assert 1 <= M <= N <= 20 assert 30 <= script_len <= 520 offset = 8 witdeem_script = args[offset:offset + script_len] offset += script_len assert len(witdeem_script) == script_len xfp_paths = [] for i in range(N): ln = args[offset] assert 1 <= ln <= 16, 'badlen' xfp_paths.append(unpack_from('<%dI' % ln, args, offset + 1)) offset += (ln * 4) + 1 assert offset == len(args) return b'asci' + start_show_p2sh_address(M, N, addr_fmt, xfp_paths, witdeem_script) if cmd == 'show': # simple cases, older code: text subpath from auth import start_show_address addr_fmt, = unpack_from('<I', args) assert not (addr_fmt & AFC_SCRIPT) return b'asci' + start_show_address(addr_fmt, subpath=args[4:]) if cmd == 'enrl': # Enroll new xpubkey to be involved in multisigs. # - text config file must already be uploaded file_len, file_sha = unpack_from('<I32s', args) if file_sha != self.file_checksum.digest(): return b'err_Checksum' assert 100 < file_len <= (20 * 200), "badlen" # Start an UX interaction, return immediately here from auth import maybe_enroll_xpub maybe_enroll_xpub(sf_len=file_len, ux_reset=True) return None if cmd == 'msck': # Quick check to test if we have a wallet already installed. from multisig import MultisigWallet M, N, xfp_xor = unpack_from('<3I', args) return int(MultisigWallet.quick_check(M, N, xfp_xor)) if cmd == 'stxn': # sign transaction txn_len, flags, txn_sha = unpack_from('<II32s', args) if txn_sha != self.file_checksum.digest(): return b'err_Checksum' assert 50 < txn_len <= MAX_TXN_LEN, "bad txn len" from auth import sign_transaction sign_transaction(txn_len, (flags & STXN_FLAGS_MASK), txn_sha) return None if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok': # Have we finished (whatever) the transaction, # which needed user approval? If so, provide result. from auth import UserAuthorizedAction req = UserAuthorizedAction.active_request if not req: return b'err_No active request' if req.refused: UserAuthorizedAction.cleanup() return b'refu' if req.failed: rv = b'err_' + req.failed.encode() UserAuthorizedAction.cleanup() return rv if not req.result: # STILL waiting on user return None if cmd == 'pwok': # return new root xpub xpub = req.result UserAuthorizedAction.cleanup() return b'asci' + bytes(xpub, 'ascii') elif cmd == 'smok': # signed message done: just give them the signature addr, sig = req.address, req.result UserAuthorizedAction.cleanup() return pack('<4sI', 'smrx', len(addr)) + addr.encode() + sig else: # generic file response resp_len, sha = req.result UserAuthorizedAction.cleanup() return pack('<4sI32s', 'strx', resp_len, sha) if cmd == 'pass': # bip39 passphrase provided, maybe use it if authorized assert self.encrypted_req, 'must encrypt' from auth import start_bip39_passphrase from main import settings assert settings.get('words', True), 'no seed' assert len(args) < 400, 'too long' pw = str(args, 'utf8') assert len(pw) < 100, 'too long' return start_bip39_passphrase(pw) if cmd == 'back': # start backup: asks user, takes long time. from auth import start_remote_backup return start_remote_backup() if cmd == 'blkc': # report which blockchain we are configured for from chains import current_chain chain = current_chain() return b'asci' + chain.ctype if cmd == 'bagi': return self.handle_bag_number(args) if has_fatram: # HSM and user-related features only larger-memory Mk3 if cmd == 'hsms': # HSM mode "start" -- requires user approval if args: file_len, file_sha = unpack_from('<I32s', args) if file_sha != self.file_checksum.digest(): return b'err_Checksum' assert 2 <= file_len <= (200 * 1000), "badlen" else: file_len = 0 # Start an UX interaction but return (mostly) immediately here from hsm_ux import start_hsm_approval await start_hsm_approval(sf_len=file_len, usb_mode=True) return None if cmd == 'hsts': # can always query HSM mode from hsm import hsm_status_report import ujson return b'asci' + ujson.dumps(hsm_status_report()) if cmd == 'gslr': # get the value held in the Storage Locker assert hsm_active, 'need hsm' return b'biny' + hsm_active.fetch_storage_locker() # User Mgmt if cmd == 'nwur': # new user from users import Users auth_mode, ul, sl = unpack_from('<BBB', args) username = bytes(args[3:3 + ul]).decode('ascii') secret = bytes(args[3 + ul:3 + ul + sl]) return b'asci' + Users.create(username, auth_mode, secret).encode('ascii') if cmd == 'rmur': # delete user from users import Users ul, = unpack_from('<B', args) username = bytes(args[1:1 + ul]).decode('ascii') return Users.delete(username) if cmd == 'user': # auth user (HSM mode) from users import Users totp_time, ul, tl = unpack_from('<IBB', args) username = bytes(args[6:6 + ul]).decode('ascii') token = bytes(args[6 + ul:6 + ul + tl]) if hsm_active: # just queues these details, can't be checked until PSBT on-hand hsm_active.usb_auth_user(username, token, totp_time) return None else: # dryrun/testing purposes: validate only, doesn't unlock nothing return b'asci' + Users.auth_okay(username, token, totp_time).encode('ascii') print("USB garbage: %s +[%d]" % (cmd, len(args))) return b'err_Unknown cmd'
async def handle(self, cmd, args): # Dispatch incoming message, and provide reply. try: cmd = bytes(cmd).decode() except: raise FramingError('decode') if cmd == 'dfu_': # only useful in factory, undocumented. return self.call_after(callgate.enter_dfu) if cmd == 'rebo': import machine return self.call_after(machine.reset) if cmd == 'logo': return self.call_after(callgate.show_logout) if cmd == 'ping': return b'biny' + args if cmd == 'upld': offset, total_size = unpack_from('<II', args) data = memoryview(args)[4 + 4:] return await self.handle_upload(offset, total_size, data) if cmd == 'dwld': offset, length, fileno = unpack_from('<III', args) return await self.handle_download(offset, length, fileno) if cmd == 'ncry': version, his_pubkey = unpack_from('<I64s', args) return self.handle_crypto_setup(version, his_pubkey) if cmd == 'vers': from version import get_mpy_version from callgate import get_bl_version # Returning: date, version(human), bootloader version, full date version # BUT: be ready for additions! rv = list(get_mpy_version()) rv.insert(2, get_bl_version()[0]) return b'asci' + ('\n'.join(rv)).encode() if cmd == 'sha2': return b'biny' + self.file_checksum.digest() if cmd == 'xpub': assert self.encrypted_req, 'must encrypt' return self.handle_xpub(str(args, 'ascii')) if cmd == 'mitm': assert self.encrypted_req, 'must encrypt' return await self.handle_mitm_check() if cmd == 'smsg': # sign message addr_fmt, len_subpath, len_msg = unpack_from('<III', args) subpath = args[12:12 + len_subpath] msg = args[12 + len_subpath:] assert len(msg) == len_msg, "badlen" from auth import sign_msg sign_msg(msg, subpath, addr_fmt) return None if cmd == 'p2sh': # show P2SH (probably multisig) address on screen (also provides it back) # - must provide redeem script, and list of [xfp+path] from auth import start_show_p2sh_address # new multsig goodness, needs mapping from xfp->path and M values addr_fmt, M, N, script_len = unpack_from('<IBBH', args) assert addr_fmt & AFC_SCRIPT assert 1 <= M <= N <= 20 assert 30 <= script_len <= 520 offset = 8 witdeem_script = args[offset:offset + script_len] offset += script_len assert len(witdeem_script) == script_len xfp_paths = [] for i in range(N): ln = args[offset] assert 2 <= ln <= 16, 'badlen' xfp_paths.append(unpack_from('<%dI' % ln, args, offset + 1)) offset += (ln * 4) + 1 assert offset == len(args) return b'asci' + start_show_p2sh_address(M, N, addr_fmt, xfp_paths, witdeem_script) if cmd == 'show': # simple cases, older code: text subpath from auth import start_show_address addr_fmt, = unpack_from('<I', args) assert not (addr_fmt & AFC_SCRIPT) return b'asci' + start_show_address(addr_fmt, subpath=args[4:]) if cmd == 'enrl': # Enroll new xpubkey to be involved in multisigs. # - text config file must already be uploaded file_len, file_sha = unpack_from('<I32s', args) if file_sha != self.file_checksum.digest(): return b'err_Checksum' assert 100 < file_len <= (20 * 200), "badlen" # Start an UX interaction, return immediately here from auth import maybe_enroll_xpub maybe_enroll_xpub(sf_len=file_len, ux_reset=True) return None if cmd == 'msck': # Quick check to test if we have a wallet already installed. from multisig import MultisigWallet M, N, xfp_xor = unpack_from('<3I', args) return int(MultisigWallet.quick_check(M, N, xfp_xor)) if cmd == 'stxn': # sign transaction txn_len, finalize, txn_sha = unpack_from('<II32s', args) if txn_sha != self.file_checksum.digest(): return b'err_Checksum' assert 50 < txn_len <= MAX_TXN_LEN, "bad txn len" from auth import sign_transaction sign_transaction(txn_len, bool(finalize)) return None if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok' or cmd == 'enok': # Have we finished (whatever) the transaction, # which needed user approval? If so, provide result. from auth import active_request, UserAuthorizedAction if not active_request: return b'err_No active request' if active_request.refused: UserAuthorizedAction.cleanup() return b'refu' if active_request.failed: rv = b'err_' + active_request.failed.encode() UserAuthorizedAction.cleanup() return rv if not active_request.result: # STILL waiting on user return None if cmd == 'pwok' or cmd == 'enok': # return new root xpub xpub = active_request.result UserAuthorizedAction.cleanup() return b'asci' + bytes(xpub, 'ascii') elif cmd == 'smok': # signed message done: just give them the signature addr, sig = active_request.address, active_request.result UserAuthorizedAction.cleanup() return pack('<4sI', 'smrx', len(addr)) + addr.encode() + sig else: # generic file response resp_len, sha = active_request.result UserAuthorizedAction.cleanup() return pack('<4sI32s', 'strx', resp_len, sha) if cmd == 'pass': # bip39 passphrase provided, maybe use it if authorized assert self.encrypted_req, 'must encrypt' from auth import start_bip39_passphrase from main import settings assert settings.get('words', True), 'no seed' assert len(args) < 400, 'too long' pw = str(args, 'utf8') assert len(pw) < 100, 'too long' return start_bip39_passphrase(pw) if cmd == 'back': # start backup: asks user, takes long time. from auth import start_remote_backup return start_remote_backup() if cmd == 'bagi': return self.handle_bag_number(args) if is_simulator() and cmd[0].isupper(): # special hacky commands to support testing w/ the simulator from sim_usb import do_usb_command return do_usb_command(cmd, args) print("USB garbage: %s +[%d]" % (cmd, len(args))) return b'err_Unknown cmd'
async def handle(self, cmd, args): # Dispatch incoming message, and provide reply. cmd = bytes(cmd).decode() if cmd == 'dfu_': # only useful in factory, undocumented. return self.call_after(callgate.enter_dfu) if cmd == 'rebo': import machine return self.call_after(machine.reset) if cmd == 'logo': return self.call_after(callgate.show_logout) if cmd == 'ping': return b'biny' + args if cmd == 'upld': offset, total_size = unpack_from('<II', args) data = memoryview(args)[4 + 4:] return await self.handle_upload(offset, total_size, data) if cmd == 'dwld': offset, length, fileno = unpack_from('<III', args) return await self.handle_download(offset, length, fileno) if cmd == 'ncry': version, his_pubkey = unpack_from('<I64s', args) return self.handle_crypto_setup(version, his_pubkey) if cmd == 'vers': from version import get_mpy_version from callgate import get_bl_version # Returning: date, version(human), bootloader version, full date version # BUT: be ready for additions! rv = list(get_mpy_version()) rv.insert(2, get_bl_version()[0]) return b'asci' + ('\n'.join(rv)).encode() if cmd == 'sha2': return b'biny' + self.file_checksum.digest() if cmd == 'xpub': assert self.encrypted_req, 'must encrypt' return self.handle_xpub(str(args, 'ascii')) if cmd == 'mitm': assert self.encrypted_req, 'must encrypt' return await self.handle_mitm_check() if cmd == 'smsg': # sign message addr_fmt, len_subpath, len_msg = unpack_from('<III', args) subpath = args[12:12 + len_subpath] msg = args[12 + len_subpath:] assert len(msg) == len_msg, "badlen" from auth import sign_msg sign_msg(msg, subpath, addr_fmt) return None if cmd == 'show': # show address on screen (also provides it back) addr_fmt, = unpack_from('<I', args) subpath = args[4:] from auth import start_show_address return b'asci' + start_show_address(subpath, addr_fmt) if cmd == 'stxn': # sign transaction txn_len, finalize, txn_sha = unpack_from('<II32s', args) if txn_sha != self.file_checksum.digest(): return b'err_Checksum' assert 50 < txn_len <= MAX_TXN_LEN, "bad txn len" from auth import sign_transaction sign_transaction(txn_len, bool(finalize)) return None if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok': # Have we finished (whatever) the transaction, # which needed user approval? If so, provide result. from auth import active_request, UserAuthorizedAction if not active_request: return b'err_No active request' if active_request.refused: UserAuthorizedAction.cleanup() return b'refu' if active_request.failed: rv = b'err_' + active_request.failed.encode() UserAuthorizedAction.cleanup() return rv if not active_request.result: # STILL waiting on user return None if cmd == 'pwok': # return new root xpub xpub = active_request.result UserAuthorizedAction.cleanup() return b'asci' + bytes(xpub, 'ascii') elif cmd == 'smok': # signed message done: just give them the signature addr, sig = active_request.address, active_request.result UserAuthorizedAction.cleanup() return pack('<4sI', 'smrx', len(addr)) + addr.encode() + sig else: # generic file response resp_len, sha = active_request.result UserAuthorizedAction.cleanup() return pack('<4sI32s', 'strx', resp_len, sha) if cmd == 'pass': # bip39 passphrase provided, maybe use it if authorized assert self.encrypted_req, 'must encrypt' from auth import start_bip39_passphrase assert len(args) < 400, 'too long' pw = str(args, 'utf8') assert len(pw) < 100, 'too long' return start_bip39_passphrase(pw) if cmd == 'back': # start backup: asks user, takes long time. from auth import start_remote_backup return start_remote_backup() if cmd == 'bagi': return self.handle_bag_number(args) if is_simulator() and cmd[0].isupper(): # special hacky commands to support testing w/ the simulator from sim_usb import do_usb_command return do_usb_command(cmd, args) print("USB garbage: %s +[%d]" % (cmd, len(args))) return b'err_Unknown cmd'