def test_mix_parameters(self) -> None: zksnark = Groth16() ext_proof = ExtendedProof(proof=Groth16.proof_from_json_dict({ "a": ["1234", "2345"], "b": [["3456", "4567"], ["5678", "6789"]], "c": ["789a", "89ab"], }), inputs=[ "9abc", "abcd", "bcde", "cdef", ]) sig_keypair = gen_signing_keypair() sig_vk = sig_keypair.vk sig = sign(sig_keypair.sk, bytes.fromhex("00112233")) receiver_enc_keypair = generate_encryption_keypair() ciphertexts = [ encrypt(token_bytes(NOTE_LENGTH_BYTES), receiver_enc_keypair.k_pk), encrypt(token_bytes(NOTE_LENGTH_BYTES), receiver_enc_keypair.k_pk), ] mix_params = MixParameters(ext_proof, sig_vk, sig, ciphertexts) mix_params_json = mix_params.to_json() mix_params_2 = MixParameters.from_json(zksnark, mix_params_json) self.assertEqual(mix_params.extended_proof.to_json_dict(), mix_params_2.extended_proof.to_json_dict()) self.assertEqual(encode_vk_to_bytes(mix_params.signature_vk), encode_vk_to_bytes(mix_params_2.signature_vk)) self.assertEqual(mix_params.signature, mix_params_2.signature) self.assertEqual(mix_params.ciphertexts, mix_params_2.ciphertexts)
def create_nested_tx(zeth_tx_file: str, prover_config_file: str, output_file: str) -> None: """ Create a Zecale nested transaction from a zeth MixParameters object """ # Load prover config (which is assumed to already exist) with open(prover_config_file, "r") as prover_config_f: prover_config = \ ProverConfiguration.from_json_dict(json.load(prover_config_f)) zksnark = zksnark = get_zksnark_provider(prover_config.zksnark_name) # Read the MixParameters with open(zeth_tx_file, "r") as zeth_tx_f: zeth_mix_params = \ MixParameters.from_json_dict(zksnark, json.load(zeth_tx_f)) # Convert to a nested transaction, and write to output file nested_tx = _create_zeth_nested_tx(zeth_mix_params, 0) with open(output_file, "w") as output_f: json.dump(nested_tx.to_json_dict(), output_f)
def charlie_corrupt_bob_deposit( zeth_client: MixerClient, prover_client: ProverClient, zksnark: IZKSnarkProvider, mk_tree: MerkleTree, bob_eth_address: str, charlie_eth_address: str, keystore: mock.KeyStore) -> MixResult: """ Charlie tries to break transaction malleability and corrupt the coins bob is sending in a transaction She does so by intercepting bob's transaction and either: - case 1: replacing the ciphertexts (or sender_eph_pk) by garbage/arbitrary data - case 2: replacing the ciphertexts by garbage/arbitrary data and using a new OT-signature - case 3: Charlie replays the mix call of Bob, to try to receive the vout Both attacks should fail, - case 1: the signature check should fail, else Charlie broke UF-CMA of the OT signature - case 2: the h_sig/vk verification should fail, as h_sig is not a function of vk any longer - case 3: the signature check should fail, because `msg.sender` will no match the value used in the mix parameters (Bob's Ethereum Address). NB. If the adversary were to corrupt the ciphertexts (or the encryption key), replace the OT-signature by a new one and modify the h_sig accordingly so that the check on the signature verification (key h_sig/vk) passes, the proof would not verify, which is why we do not test this case. """ print( f"=== Bob deposits {BOB_DEPOSIT_ETH} ETH for himself and split into " + f"note1: {BOB_SPLIT_1_ETH}ETH, note2: {BOB_SPLIT_2_ETH}ETH " + "but Charlie attempts to corrupt the transaction ===") bob_addr_pk = keystore["Bob"] bob_apk = bob_addr_pk.addr_pk.a_pk # Get pairing parameters pp = prover_client.get_configuration().pairing_parameters # Create the JoinSplit dummy inputs for the deposit input1 = get_dummy_input_and_address(bob_apk) input2 = get_dummy_input_and_address(bob_apk) note1_value = EtherValue(BOB_SPLIT_1_ETH) note2_value = EtherValue(BOB_SPLIT_2_ETH) v_in = EtherValue(BOB_DEPOSIT_ETH) output_note1, output_note2, proof, public_data, joinsplit_keypair = \ get_mix_parameters_components( zeth_client, prover_client, mk_tree, keystore["Bob"].ownership_keypair(), [input1, input2], [(bob_addr_pk.addr_pk, note1_value), (bob_addr_pk.addr_pk, note2_value)], v_in, EtherValue(0)) # v_out # Encrypt the coins to bob pk_bob = keystore["Bob"].addr_pk.k_pk ciphertexts = encrypt_notes([ (output_note1, pk_bob), (output_note2, pk_bob)]) # ### ATTACK BLOCK # Charlie intercepts Bob's deposit, corrupts it and # sends her transaction before Bob's transaction is accepted # Case 1: replacing the ciphertexts by garbage/arbitrary data # Corrupt the ciphertexts # (another way would have been to overwrite sender_eph_pk) fake_ciphertext0 = urandom(32) fake_ciphertext1 = urandom(32) result_corrupt1 = None try: joinsplit_sig_charlie = joinsplit_sign( zksnark, pp, joinsplit_keypair, charlie_eth_address, ciphertexts, proof, public_data) mix_params = MixParameters( proof, public_data, joinsplit_keypair.vk, joinsplit_sig_charlie, [fake_ciphertext0, fake_ciphertext1]) tx_hash = zeth_client.mix( mix_params, charlie_eth_address, None, EtherValue(BOB_DEPOSIT_ETH)) result_corrupt1 = \ wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) except Exception as e: print( "Charlie's first corruption attempt" + f" successfully rejected! (msg: {e})" ) assert(result_corrupt1 is None), \ "Charlie managed to corrupt Bob's deposit the first time!" print("") # Case 2: replacing the ciphertexts by garbage/arbitrary data and # using a new OT-signature # Corrupt the ciphertexts fake_ciphertext0 = urandom(32) fake_ciphertext1 = urandom(32) new_joinsplit_keypair = signing.gen_signing_keypair() # Sign the primary inputs, sender_eph_pk and the ciphertexts result_corrupt2 = None try: joinsplit_sig_charlie = joinsplit_sign( zksnark, pp, new_joinsplit_keypair, charlie_eth_address, [fake_ciphertext0, fake_ciphertext1], proof, public_data) mix_params = MixParameters( proof, public_data, new_joinsplit_keypair.vk, joinsplit_sig_charlie, [fake_ciphertext0, fake_ciphertext1]) tx_hash = zeth_client.mix( mix_params, charlie_eth_address, None, EtherValue(BOB_DEPOSIT_ETH)) result_corrupt2 = \ wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) except Exception as e: print( "Charlie's second corruption attempt" + f" successfully rejected! (msg: {e})" ) assert(result_corrupt2 is None), \ "Charlie managed to corrupt Bob's deposit the second time!" # Case3: Charlie uses the correct mix data, but attempts to send the mix # call from his own address (thereby receiving the output). result_corrupt3 = None try: joinsplit_sig_bob = joinsplit_sign( zksnark, pp, joinsplit_keypair, bob_eth_address, ciphertexts, proof, public_data) mix_params = MixParameters( proof, public_data, joinsplit_keypair.vk, joinsplit_sig_bob, ciphertexts) tx_hash = zeth_client.mix( mix_params, charlie_eth_address, None, EtherValue(BOB_DEPOSIT_ETH), 4000000) result_corrupt3 = \ wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) except Exception as e: print( "Charlie's third corruption attempt" + f" successfully rejected! (msg: {e})" ) assert(result_corrupt3 is None), \ "Charlie managed to corrupt Bob's deposit the third time!" # ### ATTACK BLOCK # Bob transaction is finally mined joinsplit_sig_bob = joinsplit_sign( zksnark, pp, joinsplit_keypair, bob_eth_address, ciphertexts, proof, public_data) mix_params = MixParameters( proof, public_data, joinsplit_keypair.vk, joinsplit_sig_bob, ciphertexts) tx_hash = zeth_client.mix( mix_params, bob_eth_address, None, EtherValue(BOB_DEPOSIT_ETH)) return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash)
def charlie_double_withdraw( zeth_client: MixerClient, prover_client: ProverClient, zksnark: IZKSnarkProvider, mk_tree: MerkleTree, input1: Tuple[int, ZethNote], charlie_eth_address: str, keystore: mock.KeyStore) -> MixResult: """ Charlie tries to carry out a double spending by modifying the value of the nullifier of the previous payment """ pp = zeth_client.prover_config.pairing_parameters scalar_field_mod = pp.scalar_field_mod() scalar_field_capacity = pp.scalar_field_capacity print( f" === Charlie attempts to withdraw {CHARLIE_WITHDRAW_ETH}ETH once " + "more (double spend) one of his note on the Mixer ===") charlie_addr = keystore["Charlie"] charlie_apk = charlie_addr.addr_pk.a_pk # Create the an additional dummy input for the MixerClient input2 = get_dummy_input_and_address(charlie_apk) note1_value = EtherValue(CHARLIE_WITHDRAW_CHANGE_ETH) v_out = EtherValue(CHARLIE_WITHDRAW_ETH) # ### ATTACK BLOCK # Add malicious nullifiers: we reuse old nullifiers to double spend by # adding $r$ to them so that they have the same value as before in Z_r, # and so the zksnark verification passes, but have different values in # {0;1}^256 so that they appear different to the contract. # See: https://github.com/clearmatics/zeth/issues/38 attack_primary_input3: int = 0 attack_primary_input4: int = 0 def compute_h_sig_attack_nf( nfs: List[bytes], sign_vk: JoinsplitSigVerificationKey) -> bytes: # We disassemble the nfs to get the formatting of the primary inputs assert len(nfs) == 2 nf0 = nfs[0] nf1 = nfs[1] input_nullifier0 = nf0.hex() input_nullifier1 = nf1.hex() nf0_rev = "{0:0256b}".format(int(input_nullifier0, 16)) primary_input3_bits = nf0_rev[:scalar_field_capacity] primary_input3_res_bits = nf0_rev[scalar_field_capacity:] nf1_rev = "{0:0256b}".format(int(input_nullifier1, 16)) primary_input4_bits = nf1_rev[:scalar_field_capacity] primary_input4_res_bits = nf1_rev[scalar_field_capacity:] # We perform the attack, recoding the modified public input values nonlocal attack_primary_input3 nonlocal attack_primary_input4 attack_primary_input3 = int(primary_input3_bits, 2) + scalar_field_mod attack_primary_input4 = int(primary_input4_bits, 2) + scalar_field_mod # We reassemble the nfs attack_primary_input3_bits = "{0:0256b}".format(attack_primary_input3) attack_nf0_bits = attack_primary_input3_bits[ len(attack_primary_input3_bits) - scalar_field_capacity:] +\ primary_input3_res_bits attack_nf0 = "{0:064x}".format(int(attack_nf0_bits, 2)) attack_primary_input4_bits = "{0:0256b}".format(attack_primary_input4) attack_nf1_bits = attack_primary_input4_bits[ len(attack_primary_input4_bits) - scalar_field_capacity:] +\ primary_input4_res_bits attack_nf1 = "{0:064x}".format(int(attack_nf1_bits, 2)) return compute_h_sig( [bytes.fromhex(attack_nf0), bytes.fromhex(attack_nf1)], sign_vk) output_note1, output_note2, proof, public_data, signing_keypair = \ get_mix_parameters_components( zeth_client, prover_client, mk_tree, keystore["Charlie"].ownership_keypair(), # sender [input1, input2], [(charlie_addr.addr_pk, note1_value), (charlie_addr.addr_pk, EtherValue(0))], EtherValue(0), v_out, compute_h_sig_attack_nf) # Update the primary inputs to the modified nullifiers, since libsnark # overwrites them with values in Z_p assert attack_primary_input3 != 0 assert attack_primary_input4 != 0 print("proof = ", proof) print("public_data[3] = ", public_data[3]) print("public_data[4] = ", public_data[4]) public_data[3] = attack_primary_input3 public_data[4] = attack_primary_input4 # ### ATTACK BLOCK # construct pk object from bytes pk_charlie = keystore["Charlie"].addr_pk.k_pk # encrypt the coins ciphertexts = encrypt_notes([ (output_note1, pk_charlie), (output_note2, pk_charlie)]) # Compute the joinSplit signature joinsplit_sig_charlie = joinsplit_sign( zksnark, pp, signing_keypair, charlie_eth_address, ciphertexts, proof, public_data) mix_params = MixParameters( proof, public_data, signing_keypair.vk, joinsplit_sig_charlie, ciphertexts) tx_hash = zeth_client.mix( mix_params, charlie_eth_address, # Pay an arbitrary amount (1 wei here) that will be refunded since the # `mix` function is payable None, EtherValue(1, 'wei')) return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash)