Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix vout attack #160 #161

Merged
merged 4 commits into from
Feb 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,6 @@ addons:
apt:
packages:
- python3-virtualenv
homebrew:
packages:
- gmp
- grpc
- protobuf
- boost
- openssl
- cmake
- libtool
- autoconf
- automake
- python
# - llvm

matrix:
include:
Expand Down
107 changes: 72 additions & 35 deletions pyClient/test_commands/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,16 +245,20 @@ def compute_h_sig_attack_nf(
(output_note2, pk_charlie)])

# Compute the joinSplit signature
joinsplit_sig = joinsplit.joinsplit_sign(
signing_keypair, sender_eph_pk, ciphertexts, proof_json)
joinsplit_sig_charlie = joinsplit.joinsplit_sign(
signing_keypair,
AntoineRondelet marked this conversation as resolved.
Show resolved Hide resolved
charlie_eth_address,
sender_eph_pk,
AntoineRondelet marked this conversation as resolved.
Show resolved Hide resolved
ciphertexts,
proof_json)

tx_hash = zeth_client.mix(
sender_eph_pk,
ciphertexts[0],
ciphertexts[1],
proof_json,
signing_keypair.vk,
joinsplit_sig,
joinsplit_sig_charlie,
charlie_eth_address,
# Pay an arbitrary amount (1 wei here) that will be refunded since the
# `mix` function is payable
Expand All @@ -273,14 +277,18 @@ def charlie_corrupt_bob_deposit(
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 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
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 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
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
Expand Down Expand Up @@ -323,40 +331,36 @@ def charlie_corrupt_bob_deposit(

# Encrypt the coins to bob
pk_bob = keystore["Bob"].addr_pk.k_pk
(pk_sender, ciphertexts) = joinsplit.encrypt_notes([
(sender_eph_pk, 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)
# (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.joinsplit_sign(
joinsplit_keypair,
charlie_eth_address,
sender_eph_pk,
ciphertexts,
proof_json)
tx_hash = zeth_client.mix(
pk_sender,
sender_eph_pk,
fake_ciphertext0,
fake_ciphertext1,
proof_json,
joinsplit_keypair.vk,
joinsplit_sig,
joinsplit_sig_charlie,
charlie_eth_address,
# Pay an arbitrary amount (1 wei here) that will be refunded
# since the `mix` function is payable
Web3.toWei(BOB_DEPOSIT_ETH, 'ether'),
4000000)
result_corrupt1 = \
Expand All @@ -377,26 +381,24 @@ def charlie_corrupt_bob_deposit(
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
)
# Sign the primary inputs, sender_eph_pk and the ciphertexts

result_corrupt2 = None
try:
joinsplit_sig_charlie = joinsplit.joinsplit_sign(
new_joinsplit_keypair,
charlie_eth_address,
sender_eph_pk,
[fake_ciphertext0, fake_ciphertext1],
proof_json)
tx_hash = zeth_client.mix(
pk_sender,
sender_eph_pk,
fake_ciphertext0,
fake_ciphertext1,
proof_json,
new_joinsplit_keypair.vk,
new_joinsplit_sig,
joinsplit_sig_charlie,
charlie_eth_address,
# Pay an arbitrary amount (1 wei here) that will be refunded since the
# `mix` function is payable
Web3.toWei(BOB_DEPOSIT_ETH, 'ether'),
4000000)
result_corrupt2 = \
Expand All @@ -409,16 +411,51 @@ def charlie_corrupt_bob_deposit(
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
AntoineRondelet marked this conversation as resolved.
Show resolved Hide resolved
# call from his own address (thereby receiving the output).
result_corrupt3 = None
try:
joinsplit_sig_bob = joinsplit.joinsplit_sign(
joinsplit_keypair,
bob_eth_address,
rrtoledo marked this conversation as resolved.
Show resolved Hide resolved
sender_eph_pk,
ciphertexts,
proof_json)
tx_hash = zeth_client.mix(
sender_eph_pk,
ciphertexts[0],
ciphertexts[1],
proof_json,
joinsplit_keypair.vk,
joinsplit_sig_bob,
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.joinsplit_sign(
joinsplit_keypair,
bob_eth_address,
sender_eph_pk,
ciphertexts,
proof_json)
tx_hash = zeth_client.mix(
pk_sender,
sender_eph_pk,
ciphertexts[0],
ciphertexts[1],
proof_json,
joinsplit_keypair.vk,
joinsplit_sig,
joinsplit_sig_bob,
bob_eth_address,
Web3.toWei(BOB_DEPOSIT_ETH, 'ether'),
4000000)
Expand Down
11 changes: 9 additions & 2 deletions pyClient/zeth/joinsplit.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from zeth.zksnark import IZKSnarkProvider, GenericProof, GenericVerificationKey
from zeth.utils import EtherValue, get_trusted_setup_dir, \
hex_digest_to_binary_string, digest_to_binary_string, encrypt, \
decrypt, int64_to_hex, encode_message_to_bytes
decrypt, int64_to_hex, encode_message_to_bytes, encode_eth_address
from zeth.prover_client import ProverClient
from api.util_pb2 import ZethNote, JoinsplitInput
import api.prover_pb2 as prover_pb2
Expand Down Expand Up @@ -538,7 +538,11 @@ def joinsplit(

# Sign
signature = joinsplit_sign(
signing_keypair, sender_eph_pk, ciphertexts, proof_json)
signing_keypair,
sender_eth_address,
sender_eph_pk,
ciphertexts,
proof_json)

# By default transfer exactly v_in, otherwise allow caller to manually
# specify.
Expand Down Expand Up @@ -684,6 +688,7 @@ def _encode_proof_and_inputs(proof_json: GenericProof) -> Tuple[bytes, bytes]:

def joinsplit_sign(
signing_keypair: JoinsplitSigKeyPair,
sender_eth_address: str,
sender_eph_pk: EncryptionPublicKey,
ciphertexts: List[bytes],
proof_json: GenericProof,
Expand All @@ -698,11 +703,13 @@ def joinsplit_sign(
assert len(ciphertexts) == constants.JS_INPUTS

# The message to sign consists of (in order):
# - senders Ethereum address
# - senders public encryption key
# - ciphertexts
# - proof elements
# - public input elements
h = sha256()
h.update(encode_eth_address(sender_eth_address))
rrtoledo marked this conversation as resolved.
Show resolved Hide resolved
h.update(encode_encryption_public_key(sender_eph_pk))
for ciphertext in ciphertexts:
h.update(ciphertext)
Expand Down
8 changes: 8 additions & 0 deletions pyClient/zeth/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ def encode_abi(type_names: List[str], data: List[bytes]) -> bytes:
return eth_abi.encode_abi(type_names, data) # type: ignore


def encode_eth_address(eth_addr: str) -> bytes:
"""
Binary encoding of ethereum address to 32 bytes
"""
# Strip the leading '0x' and hex-decode.
return bytes.fromhex(hex_extend_32bytes(eth_addr[2:]))


def encode_g1_to_bytes(group_el: G1) -> bytes:
"""
Encode a group element into a byte string
Expand Down
34 changes: 26 additions & 8 deletions scripts/ci
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,14 @@ function build() {
cxx_flags="-Werror"

if [ "${platform}" == "Darwin" ] ; then
openssl_path=$(brew --prefix openssl)
export PATH="/usr/local/opt/llvm/bin:/usr/local/bin:${PATH}"
export PKG_CONFIG_PATH="/usr/local/opt/openssl/lib/pkgconfig"
export LIBRARY_PATH="/usr/local/opt/openssl/lib"
export LDFLAGS="-L/usr/local/lib"
export CPPFLAGS="-I/usr/local/include"
export PKG_CONFIG_PATH="${openssl_path}/lib/pkgconfig"
export LIBRARY_PATH="${openssl_path}/lib"
export LDFLAGS="-L/usr/local/lib -L${openssl_path}/lib"
export CPPFLAGS="-I/usr/local/include -I${openssl_path}/include"

cxx_flags="${cxx_flags} -I/usr/local/opt/openssl/include"
cxx_flags="${cxx_flags} -I${openssl_path}/include"
cxx_flags="${cxx_flags} -Wno-deprecated-declarations"
fi

Expand All @@ -103,11 +104,28 @@ function build() {

function ci_setup() {

if ! [ "${platform}" == "Linux" ] ; then
return
if [ "${platform}" == "Darwin" ] ; then
# Some of these commands can fail (if packages are already installed,
# etc), hence the `|| echo`.
brew unlink python@2
brew update || echo
brew install \
gmp \
grpc \
protobuf \
boost \
openssl \
cmake \
libtool \
autoconf \
automake \
python \
|| echo
fi

sudo apt install -y python3-venv
if [ "${platform}" == "Linux" ] ; then
sudo apt install -y python3-venv
fi
}


Expand Down
1 change: 1 addition & 0 deletions zeth-contracts/contracts/Groth16Mixer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ contract Groth16Mixer is BaseMixer {
// 2.a Verify the signature on the hash of data_to_be_signed
bytes32 hash_to_be_signed = sha256(
abi.encodePacked(
uint256(msg.sender),
rrtoledo marked this conversation as resolved.
Show resolved Hide resolved
pk_sender,
ciphertext0,
ciphertext1,
Expand Down