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_prover_inputs( mix_call_desc: MixCallDescription ) -> Tuple[api.ProofInputs, signing.SigningKeyPair]: """ Given the basic parameters for a mix call, compute the input to the prover server, and the signing key pair. """ # Compute Merkle paths mk_tree = mix_call_desc.mk_tree sender_ask = mix_call_desc.sender_ownership_keypair.a_sk def _create_api_input(input_address: int, input_note: api.ZethNote) -> api.JoinsplitInput: mk_path = compute_merkle_path(input_address, mk_tree) input_nullifier = compute_nullifier(input_note, sender_ask) return create_api_joinsplit_input(mk_path, input_address, input_note, sender_ask, input_nullifier) inputs = mix_call_desc.inputs api_inputs = [_create_api_input(addr, note) for addr, note in inputs] mk_root = mk_tree.get_root() # Extract (<ownership-address>, <value>) tuples outputs_with_a_pk = \ [(zeth_addr.a_pk, to_zeth_units(value)) for (zeth_addr, value) in mix_call_desc.outputs] # Public input and output values as Zeth units public_in_value_zeth_units = to_zeth_units(mix_call_desc.v_in) public_out_value_zeth_units = to_zeth_units(mix_call_desc.v_out) # Generate the signing key signing_keypair = signing.gen_signing_keypair() # Use the specified or default h_sig computation compute_h_sig_cb = mix_call_desc.compute_h_sig_cb or compute_h_sig h_sig = compute_h_sig_cb( [bytes.fromhex(input.nullifier) for input in api_inputs], signing_keypair.vk) phi = _phi_randomness() # Create the api.ZethNote objects api_outputs = _create_api_zeth_notes(phi, h_sig, outputs_with_a_pk) proof_inputs = api.ProofInputs( mk_root=mk_root.hex(), js_inputs=api_inputs, js_outputs=api_outputs, pub_in_value=int64_to_hex(public_in_value_zeth_units), pub_out_value=int64_to_hex(public_out_value_zeth_units), h_sig=h_sig.hex(), phi=phi.hex()) return (proof_inputs, signing_keypair)
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_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 get_proof_joinsplit_2_by_2( self, mk_root: 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_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_proto = self._prover_client.get_proof(proof_input) extproof = self._zksnark.proof_from_proto(proof_proto) # Sanity check our unpacking code against the prover server output. pub_inputs = extproof["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 extproof, 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: 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 " + "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, 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( 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, 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( 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, 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( 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, None, EtherValue(BOB_DEPOSIT_ETH)) return wait_for_tx_update_mk_tree(zeth_client, mk_tree, tx_hash)
def create_prover_inputs( mix_call_desc: MixCallDescription ) -> Tuple[ProofInputs, signing.SigningKeyPair]: """ Given the basic parameters for a mix call, compute the input to the prover server, and the signing key pair. """ # Compute Merkle paths mk_tree = mix_call_desc.mk_tree mk_root = mk_tree.get_root() inputs = mix_call_desc.inputs mk_paths = [compute_merkle_path(addr, mk_tree) for addr, _ in inputs] # Extract (<ownership-address>, <value>) tuples outputs_with_a_pk = \ [(zeth_addr.a_pk, to_zeth_units(value)) for (zeth_addr, value) in mix_call_desc.outputs] output0 = outputs_with_a_pk[0] output1 = outputs_with_a_pk[1] # Public input and output values as Zeth units public_in_value_zeth_units = to_zeth_units(mix_call_desc.v_in) public_out_value_zeth_units = to_zeth_units(mix_call_desc.v_out) # Generate the signing key signing_keypair = signing.gen_signing_keypair() sender_ask = mix_call_desc.sender_ownership_keypair.a_sk # Compute the input note nullifiers (input_address0, input_note0) = mix_call_desc.inputs[0] (input_address1, input_note1) = mix_call_desc.inputs[1] input_nullifier0 = compute_nullifier(input_note0, sender_ask) input_nullifier1 = compute_nullifier(input_note1, sender_ask) # Convert to JoinsplitInput objects js_inputs: List[JoinsplitInput] = [ create_joinsplit_input(mk_paths[0], input_address0, input_note0, sender_ask, input_nullifier0), create_joinsplit_input(mk_paths[1], input_address1, input_note1, sender_ask, input_nullifier1) ] # Use the specified or default h_sig computation compute_h_sig_cb = mix_call_desc.compute_h_sig_cb or compute_h_sig h_sig = compute_h_sig_cb(input_nullifier0, input_nullifier1, signing_keypair.vk) phi = _phi_randomness() # Joinsplit Output Notes output_note0, output_note1 = _create_zeth_notes( phi, h_sig, output0, output1) js_outputs = [output_note0, output_note1] proof_inputs = ProofInputs( mk_root=mk_root.hex(), js_inputs=js_inputs, js_outputs=js_outputs, pub_in_value=int64_to_hex(public_in_value_zeth_units), pub_out_value=int64_to_hex(public_out_value_zeth_units), h_sig=h_sig.hex(), phi=phi.hex()) return (proof_inputs, signing_keypair)