예제 #1
0
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
예제 #2
0
    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
예제 #3
0
 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
예제 #4
0
 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))
예제 #5
0
 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
예제 #6
0
    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