def cu(s): if s.lower() == 'any': return s.lower() if extra_val and s.lower() == extra_val: return s.lower() try: return cleanup_deriv_path(s, allow_star=True) except: raise ValueError('%s: invalid path (%s)' % (fld_name, s))
def start_show_address(addr_format, subpath): try: assert addr_format in SUPPORTED_ADDR_FORMATS assert not (addr_format & AFC_SCRIPT) except: raise AssertionError('Unknown/unsupported addr format') # require a path to a key subpath = cleanup_deriv_path(subpath) # serAuthorizedAction.check_busy(ShowAddressBase) UserAuthorizedAction.active_request = ShowPKHAddress(addr_format, subpath) # kill any menu stack, and put our thing at the top abort_and_goto(UserAuthorizedAction.active_request) # provide the value back to attached desktop return UserAuthorizedAction.active_request.address
def handle_xpub(self, subpath): # Share the xpub for the indicated subpath. Expects # a text string which is the path derivation. # TODO: might not have a privkey yet from chains import current_chain from utils import cleanup_deriv_path subpath = cleanup_deriv_path(subpath) chain = current_chain() with stash.SensitiveValues() as sv: node = sv.derive_path(subpath) xpub = chain.serialize_public(node) return b'asci' + xpub.encode()
def sign_msg(text, subpath, addr_fmt): # Convert to strings try: text = str(text, 'ascii') except UnicodeError: raise AssertionError('must be ascii') subpath = cleanup_deriv_path(subpath) try: assert addr_fmt in SUPPORTED_ADDR_FORMATS assert not (addr_fmt & AFC_SCRIPT) except: raise AssertionError('Unknown/unsupported addr format') # Do some verification before we even show to the local user ApproveMessageSign.validate(text) UserAuthorizedAction.check_busy() UserAuthorizedAction.active_request = ApproveMessageSign(text, subpath, addr_fmt) # kill any menu stack, and put our thing at the top abort_and_goto(UserAuthorizedAction.active_request)
def from_file(cls, config, name=None): # Given a simple text file, parse contents and create instance (unsaved). # format is: label: value # where label is: # name: nameforwallet # policy: M of N # format: p2sh (+etc) # derivation: m/45'/0 (common prefix) # (8digithex): xpub of cosigner # # quick checks: # - name: 1-20 ascii chars # - M of N line (assume N of N if not spec'd) # - xpub: any bip32 serialization we understand, but be consistent # from main import settings my_xfp = settings.get('xfp') deriv = None xpubs = [] M, N = -1, -1 has_mine = 0 addr_fmt = AF_P2SH expect_chain = chains.current_chain().ctype lines = config.split('\n') for ln in lines: # remove comments comm = ln.find('#') if comm != -1: ln = ln[0:comm] ln = ln.strip() if ':' not in ln: if 'pub' in ln: # pointless optimization: allow bare xpub if we can calc xfp label = '0'*8 value = ln else: # complain? #if ln: print("no colon: " + ln) continue else: label, value = ln.split(':', 1) label = label.lower() value = value.strip() if label == 'name': name = value elif label == 'policy': try: # accepts: 2 of 3 2/3 2,3 2 3 etc mat = ure.search(r'(\d+)\D*(\d+)', value) assert mat M = int(mat.group(1)) N = int(mat.group(2)) assert 1 <= M <= N <= MAX_SIGNERS except: raise AssertionError('bad policy line') elif label == 'derivation': # reveal the path derivation for following key(s) try: assert value, 'blank' deriv = cleanup_deriv_path(value) except BaseException as exc: raise AssertionError('bad derivation line: ' + str(exc)) elif label == 'format': # pick segwit vs. classic vs. wrapped version value = value.lower() for fmt_code, fmt_label in cls.FORMAT_NAMES: if value == fmt_label: addr_fmt = fmt_code break else: raise AssertionError('bad format line') elif len(label) == 8: try: xfp = str2xfp(label) except: # complain? #print("Bad xfp: " + ln) continue # deserialize, update list and lots of checks is_mine = cls.check_xpub(xfp, value, deriv, expect_chain, my_xfp, xpubs) if is_mine: has_mine += 1 assert len(xpubs), 'need xpubs' if M == N == -1: # default policy: all keys N = M = len(xpubs) if not name: # provide a default name name = '%d-of-%d' % (M, N) try: name = str(name, 'ascii') assert 1 <= len(name) <= 20 except: raise AssertionError('name must be ascii, 1..20 long') assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range' assert N == len(xpubs), 'wrong # of xpubs, expect %d' % N assert addr_fmt & AFC_SCRIPT, 'script style addr fmt' # check we're included... do not insert ourselves, even tho we # have enough info, simply because other signers need to know my xpubkey anyway assert has_mine != 0, 'my key not included' assert has_mine == 1 # 'my key included more than once' # done. have all the parts return cls(name, (M, N), xpubs, addr_fmt=addr_fmt, chain_type=expect_chain)
async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH): # collect all xpub- exports on current SD card (must be > 1) # - ask for M value # - create wallet, save and also export # - also create electrum skel to go with that # - only expected to work with our ccxp-foo.json export files. from actions import file_picker import uos, ujson from utils import get_filesize from main import settings chain = chains.current_chain() my_xfp = settings.get('xfp') xpubs = [] files = [] has_mine = 0 deriv = None try: with CardSlot() as card: for path in card.get_paths(): for fn, ftype, *var in uos.ilistdir(path): if ftype == 0x4000: # ignore subdirs continue if not fn.startswith('ccxp-') or not fn.endswith('.json'): # wrong prefix/suffix: ignore continue full_fname = path + '/' + fn # Conside file size # sigh, OS/filesystem variations file_size = var[1] if len(var) == 2 else get_filesize(full_fname) if not (0 <= file_size <= 1000): # out of range size continue try: with open(full_fname, 'rt') as fp: vals = ujson.load(fp) ln = vals.get(mode) # value in file is BE32, but we want LE32 internally xfp = str2xfp(vals['xfp']) if not deriv: deriv = cleanup_deriv_path(vals[mode+'_deriv']) else: assert deriv == vals[mode+'_deriv'], "wrong derivation: %s != %s"%( deriv, vals[mode+'_deriv']) is_mine = MultisigWallet.check_xpub(xfp, ln, deriv, chain.ctype, my_xfp, xpubs) if is_mine: has_mine += 1 files.append(fn) except CardMissingError: raise except Exception as exc: # show something for coders, but no user feedback sys.print_exception(exc) continue except CardMissingError: await needs_microsd() return # remove dups; easy to happen if you double-tap the export delme = set() for i in range(len(xpubs)): for j in range(len(xpubs)): if j in delme: continue if i == j: continue if xpubs[i] == xpubs[j]: delme.add(j) if delme: xpubs = [x for idx,x in enumerate(xpubs) if idx not in delme] if not xpubs or len(xpubs) == 1 and has_mine: await ux_show_story("Unable to find any Coldcard exported keys on this card. Must have filename: ccxp-....json") return # add myself if not included already if not has_mine: with stash.SensitiveValues() as sv: node = sv.derive_path(deriv) xpubs.append( (my_xfp, deriv, chain.serialize_public(node, AF_P2SH)) ) else: assert has_mine == 1, "same coldcard included" N = len(xpubs) if N > MAX_SIGNERS: await ux_show_story("Too many signers, max is %d." % MAX_SIGNERS) return # pick useful M value to start assert N >= 2 M = (N - 1) if N < 4 else ((N//2)+1) while 1: msg = '''How many need to sign?\n %d of %d Press (7 or 9) to change M value, or OK \ to continue. If you expected more or less keys (N=%d #files=%d), \ then check card and file contents. Coldcard multisig setup file and an Electrum wallet file will be created automatically.\ ''' % (M, N, N, len(files)) ch = await ux_show_story(msg, escape='123479') if ch in '1234': M = min(N, int(ch)) # undocumented shortcut elif ch == '9': M = min(N, M+1) elif ch == '7': M = max(1, M-1) elif ch == 'x': await ux_dramatic_pause('Aborted.', 2) return elif ch == 'y': break # create appropriate object assert 1 <= M <= N <= MAX_SIGNERS name = 'CC-%d-of-%d' % (M, N) ms = MultisigWallet(name, (M, N), xpubs, chain_type=chain.ctype, addr_fmt=addr_fmt) from auth import NewEnrollRequest, UserAuthorizedAction UserAuthorizedAction.active_request = NewEnrollRequest(ms, auto_export=True) # menu item case: add to stack from ux import the_ux the_ux.push(UserAuthorizedAction.active_request)
def sign_txt_file(filename): # sign a one-line text file found on a MicroSD card # - not yet clear how to do address types other than 'classic' from files import CardSlot, CardMissingError from sram2 import tmp_buf UserAuthorizedAction.cleanup() addr_fmt = AF_CLASSIC # copy message into memory with CardSlot() as card: with open(filename, 'rt') as fd: text = fd.readline().strip() subpath = fd.readline().strip() if subpath: try: assert subpath[0:1] == 'm' subpath = cleanup_deriv_path(subpath) except: await ux_show_story("Second line of file, if included, must specify a subkey path, like: m/44'/0/0") return # if they are following BIP84 recommended derivation scheme, # then they probably would prefer a segwit/bech32 formatted address if subpath.startswith("m/84'/"): addr_fmt = AF_P2WPKH else: # default: top of wallet. subpath = 'm' try: try: text = str(text, 'ascii') except UnicodeError: raise AssertionError('non-ascii characters') ApproveMessageSign.validate(text) except AssertionError as exc: await ux_show_story("Problem: %s\n\nMessage to be signed must be a single line of ASCII text." % exc) return def done(signature, address): # complete. write out result from ubinascii import b2a_base64 orig_path, basename = filename.rsplit('/', 1) orig_path += '/' base = basename.rsplit('.', 1)[0] out_fn = None sig = b2a_base64(signature).decode('ascii').strip() while 1: # try to put back into same spot # add -signed to end. target_fname = base+'-signed.txt' for path in [orig_path, None]: try: with CardSlot() as card: out_full, out_fn = card.pick_filename(target_fname, path) out_path = path if out_full: break except CardMissingError: prob = 'Missing card.\n\n' out_fn = None if not out_fn: # need them to insert a card prob = '' else: # attempt write-out try: with CardSlot() as card: with open(out_full, 'wt') as fd: # save in full RFC style fd.write(RFC_SIGNATURE_TEMPLATE.format(addr=address, msg=text, blockchain='BITCOIN', sig=sig)) # success and done! break except OSError as exc: prob = 'Failed to write!\n\n%s\n\n' % exc sys.print_exception(exc) # fall thru to try again # prompt them to input another card? ch = await ux_show_story(prob+"Please insert an SDCard to receive signed message, " "and press OK.", title="Need Card") if ch == 'x': await ux_aborted() return # done. msg = "Created new file:\n\n%s" % out_fn await ux_show_story(msg, title='File Signed') UserAuthorizedAction.check_busy() UserAuthorizedAction.active_request = ApproveMessageSign(text, subpath, addr_fmt, approved_cb=done) # do not kill the menu stack! from ux import the_ux the_ux.push(UserAuthorizedAction.active_request)