# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard # and is covered by GPLv3 license found in COPYING. # # psbt.py - understand PSBT file format: verify and generate them # from serializations import ser_compact_size, deser_compact_size, hash160, hash256 from serializations import CTxIn, CTxInWitness, CTxOut, SIGHASH_ALL, ser_uint256 from serializations import ser_sig_der, uint256_from_str, ser_push_data, uint256_from_str from serializations import ser_string from ustruct import unpack_from, unpack, pack from ubinascii import hexlify as b2a_hex import tcc, stash, gc from uio import BytesIO from sffile import SizerFile from sram2 import psbt_tmp256 from public_constants import ( PSBT_GLOBAL_UNSIGNED_TX, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO, PSBT_IN_PARTIAL_SIG, PSBT_IN_SIGHASH_TYPE, PSBT_IN_REDEEM_SCRIPT, PSBT_IN_WITNESS_SCRIPT, PSBT_IN_BIP32_DERIVATION, PSBT_IN_FINAL_SCRIPTSIG, PSBT_IN_FINAL_SCRIPTWITNESS, PSBT_OUT_REDEEM_SCRIPT, PSBT_OUT_WITNESS_SCRIPT, PSBT_OUT_BIP32_DERIVATION ) # Max miner's fee, as percentage of output value, that we will allow to be signed. # Amounts over 1% are warned regardless. DEFAULT_MAX_FEE_PERCENTAGE = const(10) B2A = lambda x: str(b2a_hex(x), 'ascii') class FatalPSBTIssue(RuntimeError): pass class FraudulentChangeOutput(FatalPSBTIssue): pass class HashNDump: def __init__(self, d=None): self.rv = tcc.sha256() print('Hashing: ', end='') if d: self.update(d) def update(self, d): print(b2a_hex(d), end=' ') self.rv.update(d) def digest(self): print(' END') return self.rv.digest() def read_varint(v): # read "compact sized" int from a few bytes. assert not isinstance(v, tuple), v nit = v[0] if nit == 253: return unpack_from(" ll: here = ll rv.update(memoryview(psbt_tmp256)[0:here]) ll -= here if hasher: return return tcc.sha256(rv.digest()).digest() def parse_subpaths(self, my_xfp, first_known=False): # reformat self.subpaths into a more useful form for us; return # of them # that are ours. # - works in-place, on self.subpaths # - just return first result if used for outputs our_keys = 0 for idx, pk in enumerate(self.subpaths): assert len(pk) in {33, 65}, "hdpath pubkey len" if len(pk) == 33: assert pk[0] in {0x02, 0x03}, "uncompressed pubkey" vl = self.subpaths[pk][1] # force them to use a derived key, never the master assert vl >= 8, 'too short key path' assert (vl % 4) == 0, 'corrupt key path' # promote to a list of ints v = self.get(self.subpaths[pk]) here = list(unpack_from('= 30 if self.redeem_script: assert self.redeem_script[1] >= 22 # require path for each addr, check some are ours if self.our_keys is None: # can only do once self.our_keys = self.parse_subpaths(my_xfp) # sighash, but we're probably going to ignore anyway. self.sighash = SIGHASH_ALL if self.sighash is None else self.sighash if self.part_sig or txin.scriptSig: # no need for other parts # TODO multisig here. self.already_signed = True else: self.already_signed = False if not self.subpaths: raise FatalPSBTIssue('We require subpaths to be specified in the PSBT') if self.sighash != SIGHASH_ALL: raise FatalPSBTIssue('Can only do SIGHASH_ALL') if self.utxo: # Important: they might be trying to trick us with an un-related # funding transaction (UTXO) that does not match the input signature we're making # (but if it's segwit, the ploy wouldn't work, Segwit FtW) # - challenge: it's a straight dsha256() for old serializations, but not for newer # segwit txn's... plus I don't want to deserialize it here. observed = uint256_from_str(self.calc_txid(self.utxo)) assert txin.prevout.hash == observed, "utxo hash mismatch for input #%d" % idx def calc_txid(self, poslen): # Given the (pos,len) of a transaction, return the txid for that. # - doesn't validate data # - does detected witness txn vs. old style # - simple dsha256() if old style txn, other wise witness must be skipped # see if witness encoding in effect fd = self.fd fd.seek(poslen[0]) txn_version, marker, flags = unpack(": # # Please note that for a P2SH-P2WPKH, the scriptCode is always 26 # bytes including the leading size byte, as 0x1976a914{20-byte keyhash}88ac, # NOT the redeemScript nor scriptPubKey # # Also need this scriptCode for native segwit p2pkh # assert not self.is_multisig self.scriptCode = b'\x19\x76\xa9\x14' + addr + b'\x88\xac' elif not self.scriptCode: # Segwit P2SH segwit. We need the script! if not self.witness_script: raise AssertionError('Need witness script for input #%d' % self.my_index) self.scriptCode = self.get(self.witness_script) # Could probably free self.subpaths and self.redeem_script now, but only if we don't # need to re-serialize as a PSBT. def store(self, kt, key, val): # Capture what we are interested in. if kt == PSBT_IN_NON_WITNESS_UTXO: self.utxo = val elif kt == PSBT_IN_WITNESS_UTXO: self.witness_utxo = val elif kt == PSBT_IN_PARTIAL_SIG: self.part_sig[key[1:]] = val elif kt == PSBT_IN_BIP32_DERIVATION: self.subpaths[key[1:]] = val elif kt == PSBT_IN_REDEEM_SCRIPT: self.redeem_script = val elif kt == PSBT_IN_WITNESS_SCRIPT: self.witness_script = val elif kt == PSBT_IN_SIGHASH_TYPE: self.sighash = unpack(' 0, "no ins?" self.num_inputs = num_in # all the ins are in sequence starting at this position self.vin_start = _skip_n_objs(fd, num_in, 'CTxIn') # next is outputs self.num_outputs = deser_compact_size(fd) self.vout_start = _skip_n_objs(fd, self.num_outputs, 'CTxOut') end_pos = sum(self.txn) # remainder is the witness data, and then the lock time if self.had_witness: # we'll need to come back to this pos if we # want to read the witness data later. self.wit_start = _skip_n_objs(fd, num_in, 'CTxInWitness') # we are at end of outputs, and no witness data, so locktime is here self.lock_time = unpack(" 63, 'too short' # this parses the input TXN in-place for idx, txin in self.input_iter(): self.inputs[idx].validate(idx, txin, self.my_xfp) gc.collect() assert len(self.inputs) == self.num_inputs, 'ni mismatch' assert self.num_outputs >= 1, 'need outs' for idx, txo in self.output_iter(): gc.collect() if self.outputs[idx]: self.outputs[idx].validate(idx, txo, self.my_xfp) our_keys = sum(i.our_keys for i in self.inputs) print("PSBT: %d inputs, %d output, %d signed, %d ours" % (self.num_inputs, self.num_outputs, sum(1 for i in self.inputs if i and i.already_signed), our_keys)) def consider_outputs(self): # scan ouputs: # - is it a change address, defined by redeem script (p2sh) or key we know is ours # - mark change outputs, so perhaps we don't show them to users if self.total_value_out is None: # this happens, but would expect this to have done already? for out_idx, txo in self.output_iter(): pass # check fee is reasonable if self.total_value_out == 0: per_fee = 100 else: per_fee = self.calculate_fee() * 100 / self.total_value_out #print("percent fee: %f" % per_fee) from main import settings fee_limit = settings.get('fee_limit', DEFAULT_MAX_FEE_PERCENTAGE) if fee_limit != -1 and per_fee >= fee_limit: raise FatalPSBTIssue("Network fee bigger than %d%% of total amount (it is %.0f%%)." % (fee_limit, per_fee)) if per_fee >= 1: self.warnings.append(('Big Fee', 'Network fee is more than ' '1%% of total value (%.1f%%).' % per_fee)) def consider_inputs(self): # Look an the UTXO's that we are spending. Do we have them? Do the # hashes match, and what values are we getting? # Important: parse incoming UTXO to build total input value missing = 0 total_in = 0 for i, txi in self.input_iter(): if txi.scriptSig: # consider anythign in scriptsig of the input to be a complete signature self.presigned_inputs.add(i) inp = self.inputs[i] if inp.already_signed: self.presigned_inputs.add(i) if not inp.has_utxo(): # maybe they didn't provide the UTXO missing += 1 continue # pull out just the CTXOut object (expensive) utxo = inp.get_utxo(txi.prevout.n) assert utxo.nValue > 0 total_in += utxo.nValue # Look at what kind of input this will be, and therefore what # type of signing will be required, and which key we need. inp.determine_my_signing_key(utxo) # XXX scan witness data provided, and consider those ins signed if not multisig? if missing: # Should probably be a fatal msg; so risky... but # - maybe we aren't expected to sign that input? (coinjoin) # - assume for now, probably funny business so we should stop raise FatalPSBTIssue('Missing UTXO(s). Cannot determine value being signed') # self.warnings.append(('Missing UTXOs', # "We don't know enough about the inputs to this transaction to be sure " # "of their value. This means the network fee could be huge, or resulting " # "transaction's signatures invalid.")) #self.total_value_in = None else: assert total_in > 0 self.total_value_in = total_in if len(self.presigned_inputs) == self.num_inputs: # TODO: maybe wrong for multisig cases? raise FatalPSBTIssue('Transaction looks completely signed already?') # We should know pubkey required for each input now. # - but we may not be the signer for those inputs, which is fine. no_keys = set(inp.my_index for inp in self.inputs if inp and inp.required_key == None) if self.presigned_inputs - no_keys: self.warnings.append(('Missing Keys', 'We do not know the keypair for some inputs: %r' % list(no_keys))) if self.presigned_inputs: # this isn't really even an issue for some complex usage cases self.warnings.append(('Partly Signed Already', 'Some input(s) provided were already signed by another party: %r' % list(self.presigned_inputs))) def calculate_fee(self): # what miner's reward is included in txn? if self.total_value_in is None: return None return self.total_value_in - self.total_value_out def consider_keys(self): # check we process the right keys for the inputs # - check our derivation leads to same pubkey? cnt = sum(i.our_keys for i in self.inputs) if not cnt: raise FatalPSBTIssue('None of the keys involved in this transaction ' 'belong to this Coldcard (expect 0x%08x).' % self.my_xfp) @classmethod def read_psbt(cls, fd): # read in a PSBT file. Captures fd and keeps it open. hdr = fd.read(5) if hdr != b'psbt\xff': raise ValueError("bad hdr") rv = cls() # read main body (globals) rv.parse(fd) assert rv.txn, 'missing reqd section' # learn about the bitcoin transaction we are signing. rv.parse_txn() rv.inputs = [psbtInputProxy(fd, idx) for idx in range(rv.num_inputs)] rv.outputs = [psbtOutputProxy.maybe(fd, idx) for idx in range(rv.num_outputs)] return rv def serialize(self, out_fd, upgrade_txn=False): # Ouput into a file. wr = lambda *a: self.write(out_fd, *a) out_fd.write(b'psbt\xff') if upgrade_txn and self.is_complete(): # write out the ready-to-transmit txn # - means we are also a combiner in this case # - hard tho, due to variable length data. # - XXX probably a bad idea, so disabled for now out_fd.write(b'\x01\x00') # keylength=1, key=b'', PSBT_GLOBAL_UNSIGNED_TX with SizerFile() as fd: self.finalize(fd) txn_len = fd.tell() out_fd.write(ser_compact_size(txn_len)) self.finalize(out_fd) else: # provide original txn (unchanged) wr(PSBT_GLOBAL_UNSIGNED_TX, self.txn) for k in self.unknown: wr(k[0], self.unknown[k], k[1:]) # sep between globals in inputs out_fd.write(b'\0') for idx, inp in enumerate(self.inputs): inp.serialize(out_fd, idx) out_fd.write(b'\0') for idx, outp in enumerate(self.outputs): if outp: outp.serialize(out_fd, idx) out_fd.write(b'\0') def sign_it(self): # txn is approved. sign all inputs we can sign. add signatures # - hash the txn first # - sign all inputs we have the key for # - inputs might be p2sh, p2pkh and/or segwit style # - save partial inputs somewhere (append?) # - update our state with new partial sigs from main import dis dis.fullscreen('Signing...') # Double check the change outputs are right. This is slow, but critical because # it detects bad actors, not bugs or mistakes. change_paths = [(n, o.is_change) for n,o in enumerate(self.outputs) if o and o.is_change] if change_paths: with stash.SensitiveValues() as sv: for out_idx, (pubkey, subpath) in change_paths: skp = path_to_str(subpath) node = sv.derive_path(skp) # check the pubkey of this BIP32 node pu = node.public_key() if pu != pubkey: raise FraudulentChangeOutput( "Deception regarding change output #%d. " "BIP32 path doesn't match actual address." % out_idx) sigs = 0 success = set() for in_idx, txi in self.input_iter(): dis.progress_bar_show(in_idx / self.num_inputs) inp = self.inputs[in_idx] if not inp.has_utxo(): # maybe they didn't provide the UTXO continue if not inp.required_key: # we don't know the key for this input continue if inp.already_signed and not inp.is_multisig: # for multisig, it's possible I need to add another sig # but in other cases, no more signatures are possible continue which_key = inp.required_key assert not inp.added_sig, "already done??" assert which_key in inp.subpaths, 'unk key' if inp.subpaths[which_key][0] != self.my_xfp: # we don't have the key for this subkey continue txi.scriptSig = inp.scriptSig assert txi.scriptSig, "no scriptsig?" if not inp.is_segwit: # Hash by serializing/blanking various subparts of the transaction digest = self.make_txn_sighash(in_idx, txi, inp.sighash) else: # Hash the inputs and such in totally new ways, based on BIP-143 digest = self.make_txn_segwit_sighash(in_idx, txi, inp.amount, inp.scriptCode, inp.sighash) # Do the ACTUAL signature ... finally!!! with stash.SensitiveValues() as sv: skp = path_to_str(inp.subpaths[which_key]) node = sv.derive_path(skp) pk = node.private_key() sv.register(pk) # expensive test, but works... and important pu = node.public_key() assert pu == which_key, "Path (%s) led to wrong pubkey for input#%d"%(skp, in_idx) #print("privkey %s" % b2a_hex(pk).decode('ascii')) #print(" pubkey %s" % b2a_hex(which_key).decode('ascii')) #print(" digest %s" % b2a_hex(digest).decode('ascii')) result = tcc.secp256k1.sign(pk, digest) #print("result %s" % b2a_hex(result).decode('ascii')) # convert to DER format assert len(result) == 65 r = result[1:33] s = result[33:65] assert len(r) == 32 assert len(s) == 32 inp.added_sig = (which_key, ser_sig_der(r, s, inp.sighash)) success.add(in_idx) if len(success) != self.num_inputs: print("Wasn't able to sign input(s): %s" % ', '.join('#'+str(i) for i in set(range(self.num_inputs)) - success)) # done. dis.progress_bar_show(1) def make_txn_sighash(self, replace_idx, replacement, sighash_type): # calculate the hash value for one input of current transaction # - blank all script inputs # - except one single tx in, which is provided # - serialize that without witness data # - append SIGHASH_ALL=1 value (LE32) # - sha256 over that fd = self.fd old_pos = fd.tell() rv = tcc.sha256() # version number rv.update(pack(' # fd = self.fd old_pos = fd.tell() assert sighash_type == SIGHASH_ALL, "only SIGHASH_ALL supported" if self.hashPrevouts is None: # First time thru, we'll need to hash up this stuff. po = tcc.sha256() sq = tcc.sha256() # input side for in_idx, txi in self.input_iter(): po.update(txi.prevout.serialize()) sq.update(pack("