def test_sign_verify_random(self) -> None: """ Test the correct signing-verification flow with random message: verify(vk, sign(sk,m), m) = 1 """ m = urandom(32) keypair = signing.gen_signing_keypair() sigma = signing.sign(keypair.sk, m) self.assertTrue(signing.verify(keypair.vk, m, sigma)) keypair2 = signing.gen_signing_keypair() self.assertFalse(signing.verify(keypair2.vk, m, sigma))
def test_sign_verify(self) -> None: """ Test the correct signing-verification flow: verify(vk, sign(sk,m), m) = 1 """ m = sha256("clearmatics".encode()).digest() keypair = signing.gen_signing_keypair() sigma = signing.sign(keypair.sk, m) self.assertTrue(signing.verify(keypair.vk, m, sigma)) keypair2 = signing.gen_signing_keypair() self.assertFalse(signing.verify(keypair2.vk, m, sigma))
def get_proof_joinsplit_2_by_2( self, mk_root: str, input0: Tuple[int, ZethNote], mk_path0: List[str], input1: Tuple[int, ZethNote], mk_path1: List[str], sender_ask: OwnershipSecretKey, output0: Tuple[OwnershipPublicKey, int], output1: Tuple[OwnershipPublicKey, int], public_in_value_zeth_units: int, public_out_value_zeth_units: int, compute_h_sig_cb: Optional[ComputeHSigCB] = None ) -> Tuple[ZethNote, ZethNote, Dict[str, Any], JoinsplitSigKeyPair]: """ Query the prover server to generate a proof for the given joinsplit parameters. """ signing_keypair = signing.gen_signing_keypair() proof_input = compute_joinsplit2x2_inputs( mk_root, input0, mk_path0, input1, mk_path1, sender_ask, output0, output1, public_in_value_zeth_units, public_out_value_zeth_units, signing_keypair.vk, compute_h_sig_cb) proof_obj = self._prover_client.get_proof(proof_input) proof_json = self._zksnark.parse_proof(proof_obj) # We return the zeth notes to be able to spend them later # and the proof used to create them return ( proof_input.js_outputs[0], # pylint: disable=no-member proof_input.js_outputs[1], # pylint: disable=no-member proof_json, signing_keypair)
def test_mix_parameters(self) -> None: ext_proof = { "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(mix_params_json) self.assertEqual(mix_params.extended_proof, mix_params_2.extended_proof) 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 get_proof_joinsplit_2_by_2( self, mk_roots: List[bytes], input0: Tuple[int, ZethNote], mk_path0: List[str], input1: Tuple[int, ZethNote], mk_path1: List[str], sender_ask: OwnershipSecretKey, output0: Tuple[OwnershipPublicKey, int], output1: Tuple[OwnershipPublicKey, int], public_in_value_zeth_units: int, public_out_value_zeth_units: int, compute_h_sig_cb: Optional[ComputeHSigCB] = None ) -> Tuple[ZethNote, ZethNote, Dict[str, Any], JoinsplitSigKeyPair]: """ Query the prover server to generate a proof for the given joinsplit parameters. """ signing_keypair = signing.gen_signing_keypair() proof_input = compute_joinsplit2x2_inputs( mk_roots, input0, mk_path0, input1, mk_path1, sender_ask, output0, output1, public_in_value_zeth_units, public_out_value_zeth_units, signing_keypair.vk, compute_h_sig_cb) proof_obj = self._prover_client.get_proof(proof_input) proof_json = self._zksnark.parse_proof(proof_obj) # Sanity check our unpacking code against the prover server output. pub_inputs = proof_json["inputs"] print(f"pub_inputs: {pub_inputs}") # pub_inputs_bytes = [bytes.fromhex(x) for x in pub_inputs] (v_in, v_out) = public_inputs_extract_public_values(pub_inputs) assert public_in_value_zeth_units == v_in assert public_out_value_zeth_units == v_out # We return the zeth notes to be able to spend them later # and the proof used to create them return ( proof_input.js_outputs[0], # pylint: disable=no-member proof_input.js_outputs[1], # pylint: disable=no-member proof_json, signing_keypair)
class TestSigning(TestCase): keypair = signing.gen_signing_keypair() def test_sign_verify(self) -> None: """ Test the correct signing-verification flow: verify(vk, sign(sk,m), m) = 1 """ m = sha256("clearmatics".encode()).digest() sigma = signing.sign(self.keypair.sk, m) self.assertTrue(signing.verify(self.keypair.vk, m, sigma)) keypair2 = signing.gen_signing_keypair() self.assertFalse(signing.verify(keypair2.vk, m, sigma)) def test_sign_verify_random(self) -> None: """ Test the correct signing-verification flow with random message: verify(vk, sign(sk,m), m) = 1 """ m = urandom(32) sigma = signing.sign(self.keypair.sk, m) self.assertTrue(signing.verify(self.keypair.vk, m, sigma)) keypair2 = signing.gen_signing_keypair() self.assertFalse(signing.verify(keypair2.vk, m, sigma)) def test_signature_encoding(self) -> None: """ Test encoding and decoding of signatures. """ m = sha256("clearmatics".encode()).digest() sig = signing.sign(self.keypair.sk, m) sig_encoded = signing.encode_signature_to_bytes(sig) sig_decoded = signing.decode_signature_from_bytes(sig_encoded) self.assertEqual(sig, sig_decoded)
def charlie_corrupt_bob_deposit(zeth_client: joinsplit.ZethClient, mk_root: str, bob_eth_address: str, charlie_eth_address: str, keystore: mock.KeyStore, mk_tree_depth: int) -> contracts.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 pk_sender) by garbage/arbitrary data - case 2: replacing the ciphertexts by garbage/arbitrary data and using a new OT-signature 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 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" + f"but Charlie attempts to corrupt the transaction ===") bob_apk = keystore["Bob"].addr_pk.a_pk bob_ask = keystore["Bob"].addr_sk.a_sk # Create the JoinSplit dummy inputs for the deposit input1 = joinsplit.get_dummy_input_and_address(bob_apk) input2 = joinsplit.get_dummy_input_and_address(bob_apk) dummy_mk_path = mock.get_dummy_merkle_path(mk_tree_depth) note1_value = joinsplit.to_zeth_units(EtherValue(BOB_SPLIT_1_ETH)) note2_value = joinsplit.to_zeth_units(EtherValue(BOB_SPLIT_2_ETH)) v_in = joinsplit.to_zeth_units(EtherValue(BOB_DEPOSIT_ETH)) (output_note1, output_note2, proof_json, joinsplit_keypair) = \ zeth_client.get_proof_joinsplit_2_by_2( mk_root, input1, dummy_mk_path, input2, dummy_mk_path, bob_ask, # sender (bob_apk, note1_value), # recipient1 (bob_apk, note2_value), # recipient2 v_in, # v_in joinsplit.to_zeth_units(EtherValue(0)) # v_out ) # Encrypt the coins to bob pk_bob = keystore["Bob"].addr_pk.k_pk (pk_sender, ciphertexts) = joinsplit.encrypt_notes([(output_note1, pk_bob), (output_note2, pk_bob) ]) # Sign the primary inputs, pk_sender and the ciphertexts joinsplit_sig = joinsplit.joinsplit_sign(joinsplit_keypair, pk_sender, ciphertexts, proof_json) # ### 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 pk_sender) fake_ciphertext0 = urandom(32) fake_ciphertext1 = urandom(32) result_corrupt1 = None try: result_corrupt1 = zeth_client.mix( pk_sender, fake_ciphertext0, fake_ciphertext1, proof_json, joinsplit_keypair.vk, joinsplit_sig, charlie_eth_address, # Pay an arbitrary amount (1 wei here) that will be refunded # since the `mix` function is payable W3.toWei(BOB_DEPOSIT_ETH, 'ether'), 4000000) except Exception as e: print(f"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, pk_sender and the ciphertexts new_joinsplit_sig = joinsplit.joinsplit_sign( new_joinsplit_keypair, pk_sender, [fake_ciphertext0, fake_ciphertext1], proof_json) result_corrupt2 = None try: result_corrupt2 = zeth_client.mix( pk_sender, fake_ciphertext0, fake_ciphertext1, proof_json, new_joinsplit_keypair.vk, new_joinsplit_sig, charlie_eth_address, # Pay an arbitrary amount (1 wei here) that will be refunded since the # `mix` function is payable W3.toWei(BOB_DEPOSIT_ETH, 'ether'), 4000000) except Exception as e: print(f"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!" # ### ATTACK BLOCK # Bob transaction is finally mined return zeth_client.mix(pk_sender, ciphertexts[0], ciphertexts[1], proof_json, joinsplit_keypair.vk, joinsplit_sig, bob_eth_address, W3.toWei(BOB_DEPOSIT_ETH, 'ether'), 4000000)
def charlie_corrupt_bob_deposit( zeth_client: MixerClient, mk_tree: MerkleTree, bob_eth_address: str, charlie_eth_address: str, keystore: mock.KeyStore) -> contracts.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" + f"but Charlie attempts to corrupt the transaction ===") bob_apk = keystore["Bob"].addr_pk.a_pk bob_ask = keystore["Bob"].addr_sk.a_sk tree_depth = mk_tree.depth mk_root = mk_tree.get_root() # mk_tree_depth = zeth_client.mk_tree_depth # mk_root = zeth_client.merkle_root # Create the JoinSplit dummy inputs for the deposit input1 = get_dummy_input_and_address(bob_apk) input2 = get_dummy_input_and_address(bob_apk) dummy_mk_path = mock.get_dummy_merkle_path(tree_depth) note1_value = to_zeth_units(EtherValue(BOB_SPLIT_1_ETH)) note2_value = to_zeth_units(EtherValue(BOB_SPLIT_2_ETH)) v_in = to_zeth_units(EtherValue(BOB_DEPOSIT_ETH)) (output_note1, output_note2, proof_json, joinsplit_keypair) = \ zeth_client.get_proof_joinsplit_2_by_2( mk_root, input1, dummy_mk_path, input2, dummy_mk_path, bob_ask, # sender (bob_apk, note1_value), # recipient1 (bob_apk, note2_value), # recipient2 v_in, # v_in to_zeth_units(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(joinsplit_keypair, charlie_eth_address, ciphertexts, proof_json) mix_params = contracts.MixParameters( proof_json, joinsplit_keypair.vk, joinsplit_sig_charlie, [fake_ciphertext0, fake_ciphertext1]) tx_hash = zeth_client.mix(mix_params, charlie_eth_address, Web3.toWei(BOB_DEPOSIT_ETH, 'ether'), DEFAULT_MIX_GAS_WEI) result_corrupt1 = \ wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) except Exception as e: print(f"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( new_joinsplit_keypair, charlie_eth_address, [fake_ciphertext0, fake_ciphertext1], proof_json) mix_params = contracts.MixParameters( proof_json, new_joinsplit_keypair.vk, joinsplit_sig_charlie, [fake_ciphertext0, fake_ciphertext1]) tx_hash = zeth_client.mix(mix_params, charlie_eth_address, Web3.toWei(BOB_DEPOSIT_ETH, 'ether'), DEFAULT_MIX_GAS_WEI) result_corrupt2 = \ wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) except Exception as e: print(f"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(joinsplit_keypair, bob_eth_address, ciphertexts, proof_json) mix_params = contracts.MixParameters(proof_json, joinsplit_keypair.vk, joinsplit_sig_bob, ciphertexts) tx_hash = zeth_client.mix(mix_params, charlie_eth_address, Web3.toWei(BOB_DEPOSIT_ETH, 'ether'), 4000000) result_corrupt3 = \ wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash) except Exception as e: print(f"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(joinsplit_keypair, bob_eth_address, ciphertexts, proof_json) mix_params = contracts.MixParameters(proof_json, joinsplit_keypair.vk, joinsplit_sig_bob, ciphertexts) tx_hash = zeth_client.mix(mix_params, bob_eth_address, Web3.toWei(BOB_DEPOSIT_ETH, 'ether'), DEFAULT_MIX_GAS_WEI) return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash)