async def make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.txt'): from glob import dis import ustruct xfp = xfp2str(settings.get('xfp')) dis.fullscreen('Generating...') # make the data examples = [] payload = ujson.dumps(list(generate_bitcoin_core_wallet(examples, account_num))) body = '''\ # Bitcoin Core Wallet Import File https://github.com/Coldcard/firmware/blob/master/docs/bitcoin-core-usage.md ## For wallet with master key fingerprint: {xfp} Wallet operates on blockchain: {nb} ## Bitcoin Core RPC The following command can be entered after opening Window -> Console in Bitcoin Core, or using bitcoin-cli: importmulti '{payload}' ## Resulting Addresses (first 3) '''.format(payload=payload, xfp=xfp, nb=chains.current_chain().name) body += '\n'.join('%s => %s' % t for t in examples) body += '\n' await write_text_file(fname_pattern, body, 'Bitcoin Core')
async def done_apply(self, *a): # apply the passphrase. # - important to work on empty string here too. from stash import bip39_passphrase old_pw = str(bip39_passphrase) set_bip39_passphrase(pp_sofar) xfp = settings.get('xfp') msg = '''Above is the master key fingerprint of the new wallet. Press X to abort and keep editing passphrase, OK to use the new wallet, or 1 to use and save to MicroSD''' ch = await ux_show_story(msg, title="[%s]" % xfp2str(xfp), escape='1') if ch == 'x': # go back! set_bip39_passphrase(old_pw) return if ch == '1': await PassphraseSaver().append(xfp, pp_sofar) goto_top_menu()
def generate_wasabi_wallet(): # Generate the data for a JSON file which Wasabi can open directly as a new wallet. from main import settings 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)
if '-s' in sys.argv: # MicroSD menu from main import numpad numpad.inject('4') numpad.inject('y') numpad.inject('4') numpad.inject('y') if '-a' in sys.argv: # Address Explorer from main import numpad numpad.inject('4') numpad.inject('y') numpad.inject('4') numpad.inject('8') numpad.inject('y') numpad.inject('y') if '--xfp' in sys.argv: # --xfp aabbccdd => pretend we know that key (won't be able to sign) from ustruct import unpack from utils import xfp2str from ubinascii import unhexlify as a2b_hex xfp = sys.argv[sys.argv.index('--xfp') + 1] sim_defaults['xfp'] = unpack(">I", a2b_hex(xfp))[0] print("Override XFP: " + xfp2str(sim_defaults['xfp'])) # EOF
# assert keys == ['mpsMLTNqBNrsQuYNmZPj7ifqqMTSnZMMWH', 'mjoj9a1cFNPhvFkbrwzNPTBCWxhteAJHE5', # 'mkjqteuKMDApEzsZbdphtufvVPmCFafLhM', 'mvRSS7xmYBjDUEQsxvNefXLbwQHpwm76wb', # 'mhGBcrA9xDuBWttQLZFGRBJHcGEZyQpT3b', 'mkYFhxXQY6mMZbKxcuk6j6FD2Ff1gX6zgC', # 'mmgkFCdHKxCHuTMcJ9CPncRMA2UPainW6j', 'mg5fNCy7TJiZ8L4uxU3XerW2twNYAY3hmU', # 'myY1Xmhx6CdvFn6uzdUDo5EM2HxmsPXPJB', 'mozpwp3z32g9vBZxbpN6ySxx7A5EWw4Zfi'] addr = BitcoinMain.p2sh_address(AF_P2SH, script) assert addr[0] == '3' assert addr == '3Kt6KxjirrFS7GexJiXLLhmuaMzSbjp275' addr = BitcoinTestnet.p2sh_address(AF_P2SH, script) assert addr[0] == '2' assert addr == '2NBSJPhfkUJknK4HVyr9CxemAniCcRfhqp4' addr = BitcoinMain.p2sh_address(AF_P2WSH, script) assert addr[0:4] == 'bc1q', addr assert len(addr) >= 62 assert addr == 'bc1qnjw7wy4e9tf4kkqaf43n2cyjwug0ystugum08c5j5hwhfncc4mkqftu4jr' addr = BitcoinTestnet.p2sh_address(AF_P2WSH, script) assert addr[0:4] == 'tb1q', addr assert len(addr) >= 62 assert addr == 'tb1qnjw7wy4e9tf4kkqaf43n2cyjwug0ystugum08c5j5hwhfncc4mkq7r26gv' if 1: from utils import xfp2str, str2xfp assert xfp2str(0x10203040) == '40302010' for i in 0, 1, 0x12345678: assert str2xfp(xfp2str(i)) == i
sim_defaults['multisig'] = [["CC-2-of-4", [2, 4], [[1130956047, "tpubDF2rnouQaaYrUEy2JM1YD3RFzew4onawGM4X2Re67gguTf5CbHonBRiFGe3Xjz7DK88dxBFGf2i7K1hef3PM4cFKyUjcbJXddaY9F5tJBoP"], [3503269483, "tpubDFcrvj5n7gyatVbr8dHCUfHT4CGvL8hREBjtxc4ge7HZgqNuPhFimPRtVg6fRRwfXiQthV9EBjNbwbpgV2VoQeL1ZNXoAWXxP2L9vMtRjax"], [2389277556, "tpubDExj5FnaUnPAjjgzELoSiNRkuXJG8Cm1pbdiA4Hc5vkAZHphibeVcUp6mqH5LuNVKbtLVZxVSzyja5X26Cfmx6pzRH6gXBUJAH7MiqwNyuM"], [3190206587, "tpubDFiuHYSJhNbHaGtB5skiuDLg12tRboh2uVZ6KGXxr8WVr28pLcS7F3gv8SsHFa2tm1jtx3VAuw56YfgRkdo6DXyfp51oygTKY3nJFT5jBMt"]], {"pp": "48'/1'/0'/1'", "ch": "XTN", "ft": 26}]] else: # P2SH: 2of4 using BIP39 passwords: "Me", "Myself", "and I", and (empty string) on simulator sim_defaults['multisig'] = [['MeMyself', [2, 4], [[3503269483, 'tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9'], [2389277556, 'tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc'], [3190206587, 'tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa'], [1130956047, 'tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n']], {'ch': 'XTN', 'pp': "45'"}]] sim_defaults['fee_limit'] = -1 if '--xfp' in sys.argv: # --xfp aabbccdd => pretend we know that key (won't be able to sign) from ustruct import unpack from utils import xfp2str from ubinascii import unhexlify as a2b_hex xfp = sys.argv[sys.argv.index('--xfp') + 1] sim_defaults['xfp'] = unpack("<I", a2b_hex(xfp))[0] print("Override XFP: " + xfp2str(sim_defaults['xfp'])) if '--seed' in sys.argv: # --xfp aabbccdd => pretend we know that key (won't be able to sign) from ustruct import unpack from utils import xfp2str from seed import set_seed_value from main import pa, settings words = sys.argv[sys.argv.index('--seed') + 1].split(' ') assert len(words) == 24, "Expected 24 space-separated words: add some quotes" pa.pin = b'12-12' set_seed_value(words) settings.set('terms_ok', 1) settings.set('_skip_pin', '12-12') settings.set('chain', 'XTN')
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # load up the simulator w/ indicated encoded secret. could be xprv/words/etc. from sim_settings import sim_defaults import stash, chains from h import b2a_hex from pincodes import pa from nvstore import settings from stash import SecretStash, SensitiveValues from utils import xfp2str settings.current = dict(sim_defaults) settings.overrides.clear() import main raw = main.ENCODED_SECRET pa.change(new_secret=raw) pa.new_main_secret(raw) print("New key in effect: %s" % settings.get('xpub', 'MISSING')) print("Fingerprint: %s" % xfp2str(settings.get('xfp', 0)))
def generate_public_contents(): # Generate public details about wallet. # # simple text format: # key = value # or #comments # but value is JSON from main import settings from public_constants import AF_CLASSIC num_rx = 5 chain = chains.current_chain() with stash.SensitiveValues() as sv: yield ('''\ # Coldcard Wallet Summary File ## For wallet with master key fingerprint: {xfp} Wallet operates on blockchain: {nb} For BIP44, this is coin_type '{ct}', and internally we use symbol {sym} for this blockchain. ## IMPORTANT WARNING Do **not** deposit to any address in this file unless you have a working wallet system that is ready to handle the funds at that address! ## Top-level, 'master' extended public key ('m/'): {xpub} What follows are derived public keys and payment addresses, as may be needed for different systems. '''.format(nb=chain.name, xpub=chain.serialize_public(sv.node), sym=chain.ctype, ct=chain.b44_cointype, xfp=xfp2str(sv.node.my_fingerprint()))) for name, path, addr_fmt in chains.CommonDerivations: if '{coin_type}' in path: path = path.replace('{coin_type}', str(chain.b44_cointype)) if '{' in name: name = name.format(core_name=chain.core_name) show_slip132 = ('Core' not in name) yield ('''## For {name}: {path}\n\n'''.format(name=name, path=path)) yield ( '''First %d receive addresses (account=0, change=0):\n\n''' % num_rx) submaster = None for i in range(num_rx): subpath = path.format(account=0, change=0, idx=i) # find the prefix of the path that is hardneded if "'" in subpath: hard_sub = subpath.rsplit("'", 1)[0] + "'" else: hard_sub = 'm' if hard_sub != submaster: # dump the xpub needed if submaster: yield "\n" node = sv.derive_path(hard_sub, register=False) yield ("%s => %s\n" % (hard_sub, chain.serialize_public(node))) if show_slip132 and addr_fmt != AF_CLASSIC and ( addr_fmt in chain.slip132): yield ( "%s => %s ##SLIP-132##\n" % (hard_sub, chain.serialize_public(node, addr_fmt))) submaster = hard_sub node.blank() del node # show the payment address node = sv.derive_path(subpath, register=False) yield ('%s => %s\n' % (subpath, chain.address(node, addr_fmt))) node.blank() del node yield ('\n\n') from multisig import MultisigWallet if MultisigWallet.exists(): yield '\n# Your Multisig Wallets\n\n' from uio import StringIO for ms in MultisigWallet.get_all(): fp = StringIO() ms.render_export(fp) print("\n---\n", file=fp) yield fp.getvalue() del fp
async def confirm_import(self): # prompt them about a new wallet, let them see details and then commit change. M, N = self.M, self.N if M == N == 1: exp = 'The one signer must approve spends.' if M == N: exp = 'All %d co-signers must approve spends.' % N elif M == 1: exp = 'Any signature from %d co-signers will approve spends.' % N else: exp = '{M} signatures, from {N} possible co-signers, will be required to approve spends.'.format( M=M, N=N) # Look for duplicate case. is_dup, diff_count = self.has_dup() if not is_dup: story = 'Create new multisig wallet?' elif diff_count: story = '''\ CAUTION: This updated wallet has %d different XPUB values, but matching fingerprints \ and same M of N. Perhaps the derivation path has changed legitimately, otherwise, much \ DANGER!''' % diff_count else: story = 'Update existing multisig wallet?' story += '''\n Wallet Name: {name} Policy: {M} of {N} {exp} Derivation: m/{deriv} Press (1) to see extended public keys, \ OK to approve, X to cancel.'''.format(M=M, N=N, name=self.name, exp=exp, deriv=self.common_prefix or 'unknown') ux_clear_keys(True) while 1: ch = await ux_show_story(story, escape='1') if ch == '1': # Show the xpubs; might be 2k or more rendered. msg = uio.StringIO() for idx, (xfp, xpub) in enumerate(self.xpubs): if idx: msg.write('\n\n') # Not showing index numbers here because order # is non-deterministic both here, our storage, and in usage. msg.write('%s:\n%s' % (xfp2str(xfp), xpub)) await ux_show_story(msg, title='%d of %d' % (self.M, self.N)) continue if ch == 'y': # save to nvram, may raise MultisigOutOfSpace if is_dup: is_dup.delete() self.commit() await ux_dramatic_pause("Saved.", 2) break return ch
def validate_script(self, redeem_script, subpaths=None, xfp_paths=None): # Check we can generate all pubkeys in the redeem script, raise on errors. # - working from pubkeys in the script, because duplicate XFP can happen # # redeem_script: what we expect and we were given # subpaths: pubkey => (xfp, *path) # xfp_paths: (xfp, *path) in same order as pubkeys in redeem script from psbt import path_to_str subpath_help = [] used = set() ch = self.chain M, N, pubkeys = disassemble_multisig(redeem_script) assert M == self.M and N == self.N, 'wrong M/N in script' for pk_order, pubkey in enumerate(pubkeys): check_these = [] if subpaths: # in PSBT, we are given a map from pubkey to xfp/path, use it # while remembering it's potentially one-2-many assert pubkey in subpaths, "Unexpected pubkey" xfp, *path = subpaths[pubkey] for xp_idx, (wxfp, xpub) in enumerate(self.xpubs): if wxfp != xfp: continue if xp_idx in used: continue # only allow once check_these.append((xp_idx, path)) else: # Without PSBT, USB caller must provide xfp+path # in same order as they occur inside redeem script. # Working solely from the redeem script's pubkeys, we # wouldn't know which xpub to use, nor correct path for it. xfp, *path = xfp_paths[pk_order] for xp_idx in self.xpubs_with_xfp(xfp): if xp_idx in used: continue # only allow once check_these.append((xp_idx, path)) here = None for xp_idx, path in check_these: # matched fingerprint, try to make pubkey that needs to match xpub = self.xpubs[xp_idx][1] node = ch.deserialize_node(xpub, AF_P2SH) assert node dp = node.depth() if not (1 <= dp <= len(path)): # obscure case: xpub isn't deep enough to represent # indicated path... not wrong really. continue for sp in path[dp:]: assert not (sp & 0x80000000), 'hard deriv' node.derive(sp) # works in-place found_pk = node.public_key() # Document path(s) used. Not sure this is useful info to user tho. # - Do not show what we can't verify: we don't really know the hardeneded # part of the path from fingerprint to here. here = '(m=%s)\n' % xfp2str(xfp) if dp != len(path): here += 'm' + ('/_' * dp) + path_to_str(path[dp:], '/', 0) if found_pk != pubkey: # Not a match but not an error by itself, since might be # another dup xfp to look at still. #print('pk mismatch: %s => %s != %s' % ( # here, b2a_hex(found_pk), b2a_hex(pubkey))) continue subpath_help.append(here) used.add(xp_idx) break else: msg = 'pk#%d wrong' % (pk_order + 1) if here: msg += ', tried: ' + here raise AssertionError(msg) if pk_order: # verify sorted order assert bytes(pubkey) > bytes( pubkeys[pk_order - 1]), 'BIP67 violation' assert len(used) == self.N, 'not all keys used: %d of %d' % (len(used), self.N) return subpath_help
async def export_multisig_xpubs(*a): # WAS: Create a single text file with lots of docs, and all possible useful xpub values. # THEN: Just create the one-liner xpub export value they need/want to support BIP45 # NOW: Export JSON with one xpub per useful address type and semi-standard derivation path # # Consumer for this file is supposed to be ourselves, when we build on-device multisig. # from main import settings xfp = xfp2str(settings.get('xfp', 0)) chain = chains.current_chain() fname_pattern = 'ccxp-%s.json' % xfp msg = '''\ This feature creates a small file containing \ the extended public keys (XPUB) you would need to join \ a multisig wallet using the 'Create Airgapped' feature. The public keys exported are: BIP45: m/45' P2WSH-P2SH: m/48'/{coin}'/0'/1' P2WSH: m/48'/{coin}'/0'/2' OK to continue. X to abort. '''.format(coin=chain.b44_cointype) resp = await ux_show_story(msg) if resp != 'y': return try: with CardSlot() as card: fname, nice = card.pick_filename(fname_pattern) # do actual write: manual JSON here so more human-readable. with open(fname, 'wt') as fp: fp.write('{\n') with stash.SensitiveValues() as sv: for deriv, name, fmt in [ ("m/45'", 'p2sh', AF_P2SH), ("m/48'/{coin}'/0'/1'", 'p2wsh_p2sh', AF_P2WSH_P2SH), ("m/48'/{coin}'/0'/2'", 'p2wsh', AF_P2WSH) ]: dd = deriv.format(coin=chain.b44_cointype) node = sv.derive_path(dd) xp = chain.serialize_public(node, fmt) fp.write(' "%s_deriv": "%s",\n' % (name, dd)) fp.write(' "%s": "%s",\n' % (name, xp)) fp.write(' "xfp": "%s"\n}\n' % xfp) except CardMissingError: await needs_microsd() return except Exception as e: await ux_show_story('Failed to write!\n\n\n' + str(e)) return msg = '''BIP45 multisig xpub file written:\n\n%s''' % nice await ux_show_story(msg)
def check_xpub(cls, xfp, xpub, deriv, expect_chain, my_xfp, xpubs): # Shared code: consider an xpub for inclusion into a wallet, if ok, append # to list: xpubs with a tuple: (xfp, deriv, xpub) # return T if it's our own key # - deriv can be None, and in very limited cases can recover derivation path # - could enforce all same depth, and/or all depth >= 1, but # seems like more restrictive than needed, so "m" is allowed try: # Note: addr fmt detected here via SLIP-132 isn't useful node, chain, _ = import_xpub(xpub) except: raise AssertionError('unable to parse xpub') assert node.private_key() == None # 'no privkeys plz' assert chain.ctype == expect_chain # 'wrong chain' depth = node.depth() if depth == 1: if not xfp: # allow a shortcut: zero/omit xfp => use observed parent value xfp = swab32(node.fingerprint()) else: # generally cannot check fingerprint values, but if we can, do so. assert swab32(node.fingerprint()) == xfp, 'xfp depth=1 wrong' assert xfp, 'need fingerprint' # happens if bare xpub given # In most cases, we cannot verify the derivation path because it's hardened # and we know none of the private keys involved. if depth == 1: # but derivation is implied at depth==1 guess = keypath_to_str([node.child_num()], skip=0) if deriv: assert guess == deriv, '%s != %s' % (guess, deriv) else: deriv = guess # reachable? doubt it assert deriv, 'empty deriv' # or force to be 'm'? # path length of derivation given needs to match xpub's depth assert deriv[0] == 'm' p_len = deriv.count('/') assert p_len == depth, 'deriv %d != %d xpub depth (xfp=%s)' % ( p_len, depth, xfp2str(xfp)) if xfp == my_xfp: # its supposed to be my key, so I should be able to generate pubkey # - might indicate collision on xfp value between co-signers, # and that's not supported with stash.SensitiveValues() as sv: chk_node = sv.derive_path(deriv) assert node.public_key() == chk_node.public_key(), \ "(m=%s)/%s wrong pubkey" % (xfp2str(xfp), deriv[2:]) # serialize xpub w/ BIP32 standard now. # - this has effect of stripping SLIP-132 confusion away xpubs.append((xfp, deriv, chain.serialize_public(node, AF_P2SH))) return (xfp == my_xfp)
def generate_bitcoin_core_wallet(account_num, example_addrs): # Generate the data for an RPC command to import keys into Bitcoin Core # - yields dicts for json purposes from descriptor import append_checksum 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)) desc = append_checksum(desc) # for importmulti imm = { 'desc': desc, 'range': [0, 1000], 'timestamp': 'now', 'internal': internal, 'keypool': True, 'watchonly': True } # for importdescriptors imd = { 'desc': desc, 'active': True, 'timestamp': 'now', 'internal': internal, } if not internal: imd['label'] = "Coldcard " + txt_xfp yield (imm, imd)