ZKProtection for MITM attacks

Distributed Lab Academy
4 min readJul 6, 2023

Recently we have released the second version of w3sign — the solution that enables the signing of various documents, files, etc, using MetaMask. The first version of the solution had a vulnerability that allowed to modify the document signers’ list by the man in the middle (front-running attack).

Now we describe how we resolved this issue using zero-knowledge proofs.

The best UX you have ever seen

Instance: https://w3sign.app/

Github: https://github.com/dl-w3sign

Original flow

The general user flow of the first version was the following:

  1. The user uploads the document to be signed and calculates the document hash using the keccak256 hash function.
  2. The user fills in the list of signers if needed.
  3. The user sends the transaction with the document hash value and list of signers and calls the w3sign contract with mentioned data.

Then any verifier can upload the same document, calculate its hash value, and check that it was stamped in the blockchain. Then the defined signer can sign the document via an additional transaction.

The signature process involves sending a transaction followed by a call to a specific function on a contract.

Problem statement

This logic was vulnerable, as anyone could intercept the transaction and send their own with the same hash but with a different list of signers, which made the original transaction invalid, as stamps cannot be changed.

There were two solutions to this problem (probably their amount is larger):

  1. Abandon the list of signers and allow all users to sign any stamps
  2. Use ZK proofs to verify that the user who sent the transaction owns the original document

The first option is cheaper for the end user but less convenient because all users can sign any document: 1 — additional FE requirements; 2 — inability to limit signing documents only by involved parties.

The second approach is right. It is slightly more expensive for users, but: 1 — it doesn’t reduce the logic of the system; 2 — it is secure.

New Paradigm

The updated flow isn’t different for the user (in the context of UX/UI), but under the hood, there are some changes:

  1. The user uploads the document to be signed and calculates the final document hash: 1.1) The hash of this document is calculated using keccak256 -> 1.2) Then the first hash is calculated using the Poseidon hash function -> 1.3) The final document hash is calculated by another hashing using Poseidon.
  2. The user fills in the list of signers if needed.
  3. Then a ZK proof is generated that the user owns the original file. To generate a ZK proof, the user must provide the first Poseidon document hash, which can only be obtained when the user owns the original file. Additionally, to generate a ZK proof, the user must provide an EVM address from which the transaction will be sent. This is done to prevent anyone from stealing the proof and reusing it.
  4. After generating the proof, a transaction is formed and sent to the contract.
  5. The stamp is created.

For verifiers and signers, the flow has no differences except the hashing process.

In this case, the Attacker also can take a final hash value of the document, but they can’t generate a correct proof for their account (that only an EXACT account can send the transaction with an EXACT stamp).

template Hash() {
signal input hash;
signal input msgSender;
signal output hashOfInput;

component poseidon = Poseidon(1);
poseidon.inputs[0] <== hash;
hashOfInput <== poseidon.out;
}

Smart contracts description

There are two main functions on the TimeStamping contract that users interact with createStamp and sign.

The createStamp function creates a stamp and accepts the following:

  1. ZK proof that the user owns the original document
  2. List of signers who can sign the document. If the list of signers is empty, the stamp is public, and anyone can sign it.
  3. Flag indicating whether the document needs to be signed on behalf of the stamp creator.

The sign function only accepts the hash of the document to be signed. This function call will be successful only when the user has yet to sign and is in the list of signers or the stamp is public.

There are also functions on the contract that can only be called by its owner — setVerifier, setFee, and withdrawFee.

The setVerifier and setFee functions are used to update the values of the corresponding fields.

The withdraw fee function is used to withdraw the fee stored on the contract.

In addition to all the above functions, there are view functions that can provide information about various stamps, get the final hash of a document by its bytes and much more.

Cost

Here are some computations about gas consumption for old and new versions:

--

--

Distributed Lab Academy

The mission of R&D company Distributed Lab is to make the Financial Internet a reality. We`ll talk about decentralized technologies that are changing the world.