def unblind(pubkey: bytes, blinding_key: bytes, range_proof: bytes, value_commitment: bytes, asset_commitment: bytes, script_pubkey, message_length=64) -> tuple: """Unblinds a range proof and returns value, asset, value blinding factor, asset blinding factor, extra data, min and max values""" assert len(pubkey) in [33, 65] assert len(blinding_key) == 32 assert len(value_commitment) == 33 assert len(asset_commitment) == 33 pub = secp256k1.ec_pubkey_parse(pubkey) secp256k1.ec_pubkey_tweak_mul(pub, blinding_key) sec = secp256k1.ec_pubkey_serialize(pub) nonce = hashlib.sha256(hashlib.sha256(sec).digest()).digest() commit = secp256k1.pedersen_commitment_parse(value_commitment) gen = secp256k1.generator_parse(asset_commitment) value, vbf, msg, min_value, max_value = secp256k1.rangeproof_rewind( range_proof, nonce, commit, script_pubkey.data, gen, message_length) if len(msg) < 64: raise TransactionError("Rangeproof message is too small") asset = msg[:32] abf = msg[32:64] extra = msg[64:] # vbf[:16]+vbf[16:] is an ugly copy that works both in micropython and python3 # not sure why rewind() changes previous values after a fresh new call, but this is a fix... return value, asset, vbf[:16] + vbf[16:], abf, extra, min_value, max_value
def unblind(self, blinding_key): """ Unblinds the output and returns a tuple: (value, asset, value_blinding_factor, asset_blinding_factor, min_value, max_value) """ if not self.is_blinded: return self.value, self.asset, None, None, None, None pub = secp256k1.ec_pubkey_parse(self.nonce) secp256k1.ec_pubkey_tweak_mul(pub, blinding_key) sec = secp256k1.ec_pubkey_serialize(pub) nonce = hashlib.sha256(hashlib.sha256(sec).digest()).digest() commit = secp256k1.pedersen_commitment_parse(self.value) gen = secp256k1.generator_parse(self.asset) res = secp256k1.rangeproof_rewind(self.witness.range_proof.data, nonce, commit, self.script_pubkey.data, gen) value, vbf, msg, min_value, max_value = res if len(msg) < 64: raise TransactionError("Rangeproof message is too small") asset = msg[:32] abf = msg[32:64] extra = msg[64:] return value, asset, vbf, abf, extra, min_value, max_value
def fill_pset_scope(self, scope, desc, stream=None, rangeproof_offset=None, surj_proof_offset=None): # if we don't have a rangeproof offset - nothing we can really do if rangeproof_offset is None: return True # pointer and length of preallocated memory for rangeproof rewind memptr, memlen = get_preallocated_ram() # for inputs we check if rangeproof is there # check if we actually need to rewind if None not in [ scope.asset, scope.value, scope.asset_blinding_factor, scope.value_blinding_factor ]: # verify that asset and value blinding factors lead to value and asset commitments return True stream.seek(rangeproof_offset) l = compact.read_from(stream) vout = scope.utxo if isinstance(scope, LInputScope) else scope.blinded_vout blinding_key = desc.blinding_key.get_blinding_key( vout.script_pubkey).secret # get the nonce for unblinding pub = secp256k1.ec_pubkey_parse(vout.ecdh_pubkey) secp256k1.ec_pubkey_tweak_mul(pub, blinding_key) sec = secp256k1.ec_pubkey_serialize(pub) nonce = hashlib.sha256(hashlib.sha256(sec).digest()).digest() commit = secp256k1.pedersen_commitment_parse(vout.value) gen = secp256k1.generator_parse(vout.asset) try: value, vbf, msg, _, _ = secp256k1.rangeproof_rewind_from( stream, l, memptr, memlen, nonce, commit, vout.script_pubkey.data, gen) except ValueError as e: raise RewindError(str(e)) asset = msg[:32] abf = msg[32:64] scope.value = value scope.value_blinding_factor = vbf scope.asset = asset scope.asset_blinding_factor = abf return True
def reblind(self, nonce, blinding_pubkey=None, extra_message=b""): """ Re-generates range-proof with particular nonce and includes extra message in the range proof. This message can contain some useful info like a label or whatever else. """ if not self.is_blinded: return # check blinding pubkey is there blinding_pubkey = blinding_pubkey or self.blinding_pubkey if not blinding_pubkey: raise PSBTError("Blinding pubkey required") pub = secp256k1.ec_pubkey_parse(blinding_pubkey) self.ecdh_pubkey = ec.PrivateKey(nonce).sec() secp256k1.ec_pubkey_tweak_mul(pub, nonce) sec = secp256k1.ec_pubkey_serialize(pub) ecdh_nonce = hashlib.sha256(hashlib.sha256(sec).digest()).digest() msg = self.asset[-32:] + self.asset_blinding_factor + extra_message self.range_proof = secp256k1.rangeproof_sign( ecdh_nonce, self.value, secp256k1.pedersen_commitment_parse(self.value_commitment), self.value_blinding_factor, msg, self.script_pubkey.data, secp256k1.generator_parse(self.asset_commitment))
def open(self, mode=None): """Opens a secure channel. Mode can be "es" - ephemeral-static or "ee" - ephemeral-ephemenral """ # save mode for later - i.e. reestablish secure channel if mode is None: mode = self.mode else: self.mode = mode # check if we know pubkey already if self.card_pubkey is None: self.get_card_pubkey() # generate ephimerial key secret = get_random_bytes(32) host_prv = secret host_pub = secp256k1.ec_pubkey_create(secret) # ee mode - ask card to create ephimerial key and send it to us if mode == "ee": data = secp256k1.ec_pubkey_serialize(host_pub, secp256k1.EC_UNCOMPRESSED) # get ephimerial pubkey from the card res = self.applet.request(self.OPEN_EE + encode(data)) s = BytesIO(res) data = s.read(65) pub = secp256k1.ec_pubkey_parse(data) secp256k1.ec_pubkey_tweak_mul(pub, secret) shared_secret = hashlib.sha256( secp256k1.ec_pubkey_serialize(pub)[1:33]).digest() shared_fingerprint = self.derive_keys(shared_secret) recv_hmac = s.read(MAC_SIZE) h = hmac.new(self.card_mac_key, digestmod="sha256") h.update(data) expected_hmac = h.digest()[:MAC_SIZE] if expected_hmac != recv_hmac: raise SecureChannelError("Wrong HMAC.") data += recv_hmac raw_sig = s.read() sig = secp256k1.ecdsa_signature_parse_der(raw_sig) # in case card doesn't follow low s rule (but it should) sig = secp256k1.ecdsa_signature_normalize(sig) if not secp256k1.ecdsa_verify(sig, hashlib.sha256(data).digest(), self.card_pubkey): raise SecureChannelError("Signature is invalid.") # se mode - use our ephimerial key with card's static key else: data = secp256k1.ec_pubkey_serialize(host_pub, secp256k1.EC_UNCOMPRESSED) # ugly copy pub = secp256k1.ec_pubkey_parse( secp256k1.ec_pubkey_serialize(self.card_pubkey)) secp256k1.ec_pubkey_tweak_mul(pub, secret) shared_secret = secp256k1.ec_pubkey_serialize(pub)[1:33] res = self.applet.request(self.OPEN_SE + encode(data)) s = BytesIO(res) nonce_card = s.read(32) recv_hmac = s.read(MAC_SIZE) secret_with_nonces = hashlib.sha256(shared_secret + nonce_card).digest() shared_fingerprint = self.derive_keys(secret_with_nonces) data = nonce_card h = hmac.new(self.card_mac_key, digestmod="sha256") h.update(data) expected_hmac = h.digest()[:MAC_SIZE] if expected_hmac != recv_hmac: raise SecureChannelError("Wrong HMAC.") data += recv_hmac sig = secp256k1.ecdsa_signature_parse_der(s.read()) # in case card doesn't follow low s rule (but it should) sig = secp256k1.ecdsa_signature_normalize(sig) if not secp256k1.ecdsa_verify(sig, hashlib.sha256(data).digest(), self.card_pubkey): raise SecureChannelError("Signature is invalid") # reset iv self.iv = 0 self.is_open = True
def preprocess_psbt(self, stream, fout): """ Processes incoming PSBT, fills missing information and writes to fout. Returns: - wallets in inputs: list of tuples (wallet, amount) - metadata for tx display including warnings that require user confirmation """ self.show_loader(title="Parsing transaction...") # compress = True flag will make sure large fields won't be loaded to RAM psbtv = self.PSBTViewClass.view(stream, compress=True) # Start with global fields of PSBT # On Liquid we check if txseed is provided (for deterministic blinding) # It will be None if it is not there. blinding_seed = psbtv.get_value(b"\xfc\x07specter\x00") if blinding_seed: hseed = hashes.tagged_hash_init("liquid/txseed", blinding_seed) vals = [] # values abfs = [] # asset blinding factors vbfs = [] # value blinding factors in_tags = [] in_gens = [] # Write global scope first psbtv.stream.seek(psbtv.offset) res = read_write(psbtv.stream, fout, psbtv.first_scope-psbtv.offset) # here we will store all wallets that we detect in inputs # wallet: {"amount": {asset: amount}, "gaps" [gaps]} wallets = {} meta = { "inputs": [{} for i in range(psbtv.num_inputs)], "outputs": [{} for i in range(psbtv.num_outputs)], "issuance": False, "reissuance": False, } fingerprint = self.keystore.fingerprint # We need to detect wallets owning inputs and outputs, # in case of liquid - unblind them. # Fill all necessary information: # For Bitcoin: bip32 derivations, witness script, redeem script # For Liquid: same + values, assets, commitments, proofs etc. # At the end we should have the most complete PSBT / PSET possible for i in range(psbtv.num_inputs): self.show_loader(title="Parsing input %d..." % i) # load input to memory, verify it (check prevtx hash) inp = psbtv.input(i) metainp = meta["inputs"][i] # verify, do not require non_witness_utxo if witness_utxo is set inp.verify(ignore_missing=True) # check sighash in the input if inp.sighash_type is not None and inp.sighash_type != self.DEFAULT_SIGHASH: metainp["sighash"] = self.get_sighash_info(inp.sighash_type)["name"] if inp.issue_value: if inp.issue_entropy: meta["reissuance"] = True else: meta["issuance"] = True # in Liquid we may need to rewind the rangeproof to get values rangeproof_offset = None off = psbtv.seek_to_scope(i) # find offset of the rangeproof if it exists rangeproof_offset = psbtv.seek_to_value(b'\xfc\x04pset\x0e', from_current=True) # add scope offset if rangeproof_offset is not None: rangeproof_offset += off # Find wallets owning the inputs and fill scope data: # first we check already detected wallet owns the input # as in most common case all inputs are owned by the same wallet. wallet = None for w in wallets: # pass rangeproof offset if it's in the scope if w and w.fill_scope(inp, fingerprint, stream=psbtv.stream, rangeproof_offset=rangeproof_offset): wallet = w break # if it's a different wallet - go through all our wallets and check if wallet is None: # find wallet and append it to wallets for w in self.wallets: # pass rangeproof offset if it's in the scope if w.fill_scope(inp, fingerprint, stream=psbtv.stream, rangeproof_offset=rangeproof_offset): wallet = w break # get gaps gaps = None if wallet: gaps = [g for g in wallet.gaps] # copy res = wallet.get_derivation(inp.bip32_derivations) if res: idx, branch_idx = res gaps[branch_idx] = max(gaps[branch_idx], idx+wallet.GAP_LIMIT+1) # add wallet to tx wallets dict if wallet not in wallets: wallets[wallet] = {"amount": {}, "gaps": gaps} else: if wallets[wallet]["gaps"] is not None and gaps is not None: wallets[wallet]["gaps"] = [max(g1,g2) for g1,g2 in zip(gaps, wallets[wallet]["gaps"])] # Get values (and assets) and store in metadata and wallets dict # we don't know yet if we unblinded the input or not, and if it was even blinded asset = inp.asset or inp.utxo.asset value = inp.value or inp.utxo.value # blinded assets are 33-bytes long, unblinded - 32 if not (len(asset) == 32 and isinstance(value, int)): asset = None value = -1 # if at least one input can't be unblinded - we can't generate proofs blinding_seed = None if blinding_seed: # update blinding seed hseed.update(bytes(reversed(inp.txid))) hseed.update(inp.vout.to_bytes(4,'little')) vals.append(value) abfs.append(inp.asset_blinding_factor or b"\x00"*32) vbfs.append(inp.value_blinding_factor or b"\x00"*32) in_tags.append(asset) if inp.utxo.asset is None: raise WalletError("Missing input asset") if len(inp.utxo.asset) == 33: in_gens.append(secp256k1.generator_parse(inp.utxo.asset)) else: in_gens.append(secp256k1.generator_generate(inp.utxo.asset)) wallets[wallet]["amount"][asset] = wallets[wallet]["amount"].get(asset, 0) + value metainp.update({ "label": wallet.name if wallet else "Unknown wallet", "value": value, "asset": self.asset_label(asset), }) if wallet and wallet.is_watchonly: metainp["label"] += " (watch-only)" if asset not in self.assets: metainp.update({"raw_asset": asset}) inp.write_to(fout, version=psbtv.version) # if blinding seed is set we can generate all proofs if blinding_seed: self.show_loader(title="Doing blinding magic...") blinding_out_indexes = [] # first we go through all outputs and update the txseed for i in range(psbtv.num_outputs): out = psbtv.output(i) hseed.update(out.script_pubkey.serialize()) txseed = hseed.digest() # now we can blind everything for i in range(psbtv.num_outputs): out = psbtv.output(i) if out.blinding_pubkey: blinding_out_indexes.append(i) abf = hashes.tagged_hash("liquid/abf", txseed+i.to_bytes(4,'little')) vbf = hashes.tagged_hash("liquid/vbf", txseed+i.to_bytes(4,'little')) abfs.append(abf) vbfs.append(vbf) vals.append(out.value) # get last vbf from scope out = psbtv.output(blinding_out_indexes[-1]) if (None in vals or None in abfs or None in vbfs or None in in_tags): blinding_seed = None else: vbfs[-1] = secp256k1.pedersen_blind_generator_blind_sum(vals, abfs, vbfs, psbtv.num_inputs) # sanity check assert len(abfs) == psbtv.num_inputs + len(blinding_out_indexes) assert all([len(a)==32 for a in abfs]) assert all([len(a)==32 for a in vbfs]) memptr, memlen = get_preallocated_ram() # parse outputs and blind if necessary if blinding_seed: in_tags = b"".join(in_tags) for i in range(psbtv.num_outputs): gc.collect() self.show_loader(title="Parsing output %d..." % i) out = psbtv.output(i) metaout = meta["outputs"][i] # calculate commitments if blinding_seed and out.blinding_pubkey: # index of this output in the abfs, vbfs and vals list_idx = psbtv.num_inputs + blinding_out_indexes.index(i) self.show_loader(title="Generating asset commtiment %d..." % i) # asset commitment out.asset_blinding_factor = abfs[list_idx] gen = secp256k1.generator_generate_blinded(out.asset, out.asset_blinding_factor) out.asset_commitment = secp256k1.generator_serialize(gen) self.show_loader(title="Generating value commtiment %d..." % i) # value commitment out.value_blinding_factor = vbfs[list_idx] value_commitment = secp256k1.pedersen_commit(out.value_blinding_factor, out.value, gen) out.value_commitment = secp256k1.pedersen_commitment_serialize(value_commitment) # self.show_loader(title="Generating surjection proof %d..." % i) # # surjection proof # proof_seed = hashes.tagged_hash("liquid/surjection_proof", txseed+i.to_bytes(4,'little')) # plen, in_idx = secp256k1.surjectionproof_initialize_preallocated(memptr, memlen, in_tags, out.asset, proof_seed) # # proof, in_idx = secp256k1.surjectionproof_initialize(in_tags, out.asset, proof_seed) # secp256k1.surjectionproof_generate(memptr, in_idx, in_gens, gen, abfs[in_idx], out.asset_blinding_factor) # surjection_proof = secp256k1.surjectionproof_serialize(memptr) # # del proof # # write surjection proof # ser_string(fout, b'\xfc\x04pset\x05') # ser_string(fout, surjection_proof) # # del surjection_proof self.show_loader(title="Generating range proof %d..." % i) # generate range proof rangeproof_nonce = hashes.tagged_hash("liquid/range_proof", txseed+i.to_bytes(4,'little')) pub = secp256k1.ec_pubkey_parse(out.blinding_pubkey) out.ecdh_pubkey = ec.PrivateKey(rangeproof_nonce).sec() secp256k1.ec_pubkey_tweak_mul(pub, rangeproof_nonce) sec = secp256k1.ec_pubkey_serialize(pub) ecdh_nonce = hashes.double_sha256(sec) # proprietary field that stores extra message for recepient extra_message=out.unknown.get(b"\xfc\x07specter\x01", b"") msg = out.asset[-32:] + out.asset_blinding_factor + extra_message # write to temp file to get length first with open(self.tempdir+"/rangeproof_out", "wb") as frp: rplen = secp256k1.rangeproof_sign_to( frp, memptr, memlen, ecdh_nonce, out.value, secp256k1.pedersen_commitment_parse(out.value_commitment), out.value_blinding_factor, msg, out.script_pubkey.data, secp256k1.generator_parse(out.asset_commitment) ) # write to fout rangeproof field with open(self.tempdir+"/rangeproof_out", "rb") as frp: ser_string(fout, b'\xfc\x04pset\x04') fout.write(compact.to_bytes(rplen)) read_write(frp, fout, rplen) rangeproof_offset = None # we only need to verify rangeproof if we didn't generate it ourselves if not blinding_seed: self.show_loader(title="Verifying output %d..." % i) # find rangeproof and surjection proof # rangeproof off = psbtv.seek_to_scope(psbtv.num_inputs+i) # find offset of the rangeproof if it exists rangeproof_offset = self._copy_kv(fout, psbtv, b'\xfc\x04pset\x04') if rangeproof_offset is None: psbtv.seek_to_scope(psbtv.num_inputs+i) # alternative key definition (psetv0) rangeproof_offset = self._copy_kv(fout, psbtv, b'\xfc\x08elements\x04') if rangeproof_offset is not None: rangeproof_offset += off surj_proof_offset = None # surjection proof off = psbtv.seek_to_scope(psbtv.num_inputs+i) # find offset of the rangeproof if it exists surj_proof_offset = self._copy_kv(fout, psbtv, b'\xfc\x04pset\x05') if surj_proof_offset is None: psbtv.seek_to_scope(psbtv.num_inputs+i) # alternative key definition (psetv0) surj_proof_offset = self._copy_kv(fout, psbtv, b'\xfc\x08elements\x05') if surj_proof_offset is not None: surj_proof_offset += off wallet = None for w in wallets: # pass rangeproof offset if it's in the scope if w and w.fill_scope(out, fingerprint, stream=psbtv.stream, rangeproof_offset=rangeproof_offset, ): wallet = w break if wallet is None: # find wallet and append it to wallets for w in self.wallets: # pass rangeproof offset if it's in the scope if w.fill_scope(out, fingerprint, stream=psbtv.stream, rangeproof_offset=rangeproof_offset, ): wallet = w break # if we didn't blind it ourselves if not blinding_seed: try: out.verify() except: raise WalletError("Commitments in output %d are wrong" % i) # Get values (and assets) and store in metadata and wallets dict asset = out.asset or out.asset_commitment value = out.value or out.value_commitment # blinded assets are 33-bytes long, unblinded - 32 if not (asset and value) or not (len(asset) == 32 and isinstance(value, int)): asset = None value = -1 metaout.update({ "change": (wallet is not None and len(wallets) == 1 and wallet in wallets), "value": value, "address": self.get_address(out), "asset": self.asset_label(asset), }) if wallet: metaout["label"] = wallet.name res = wallet.get_derivation(out.bip32_derivations) if res: idx, branch_idx = res branch_txt = "" if branch_idx == 1: "change " elif branch_idx > 1: "branch %d " % branch_idx metaout["label"] = "%s %s#%d" % (wallet.name, branch_txt, idx) if wallet in wallets: allowed_idx = wallets[wallet]["gaps"][branch_idx] else: allowed_idx = wallet.gaps[branch_idx] if allowed_idx <= idx: metaout["warning"] = "Derivation index is by %d larger than last known used index %d!" % (idx-allowed_idx+wallet.GAP_LIMIT, allowed_idx-wallet.GAP_LIMIT) if wallet.is_watchonly: metaout["warning"] = "Watch-only wallet!" if asset and asset not in self.assets: metaout.update({"raw_asset": asset}) out.write_to(fout, skip_separator=True, version=psbtv.version) # write rangeproofs and surjection proofs # separator fout.write(b"\x00") return wallets, meta