Dapp Design – How to Verify a Signed Message and Ensure It Hasn’t Been Reused or Tampered

dapp-designprivate-keypublic-keysignatureverify

I have seen this question and others but none answers my qustion.

How can I sign a message and make sure that the public key I recover is actually belongs to the private key that signed the message?

The code below has been inspired by this github thread.

// function in the front-end.
async signMessage(){
    const message = `Hello World`;
    const messageHash = await ethers.utils.hashMessage(message)
    const signedMessage = await this.signer.signMessage(ethers.utils.arrayify(messageHash));
    //...........
    //Send messageHash and signedMessage to the server backend
}

Backend code:

// function in the back-end.
async verifyMessage(messageHash, signedMessage){
    const pubkey = ethers.utils.recoverPublicKey(
        ethers.utils.arrayify(
            ethers.utils.hashMessage(ethers.utils.arrayify(messageHash))
        ),
        signedMessage
    )
    const address = ethers.utils.computeAddress(pubkey) 
}

1st problem with the code above is that someone(e.g a malicious party) can use the signature again and again(without resigning it) and the backend would not be able to know so.

2nd problem you can submit a message that is different to the one that is signed and the back-end would not be able to know. In fact a valid ethereum address would still be recovered.

How can this be solved?

Best Answer

1st sign the message. The message has to have to signer address in it. We will parse this message and get the signer address and compare it to the one we get from decoding the signed message. If they match then the message is authentic and has not been tampered with.

2nd. In the message we also need to include a nonce that we will save in a db use it to make sure messages are not reused.

Front end code:

// Front end code
async signMessageAnsSendToServer() {
    const nonce = new Date().getTime()
    const message = `
        Message:
 
            Wallet address:
                ${this.connectedAddress}

            Nonce:
                ${nonce}
`
    this.web3 = new ethers.providers.Web3Provider(this.provider)// create web3 instance
    this.signer = this.web3.getSigner();
    const signedMessage = await this.signer.signMessage(message);
    // send signed message to server

}

Backend code:

// Backend code
async function getSignerAddressAndNonce(message, signedMessage){
    const signerAddress = ethers.utils.verifyMessage(message, signedMessage);

    const addressFromMessage = message.replace(/\n|\r/g, "").split("Wallet address:").pop().split("Nonce:")[0].trim();

    const nonce = message.split("Nonce:").pop().trim();
  
    if(signerAddressA !== signerAddressB){
        // this means that the message was not signed
    }

    return {address: signerAddressA, nonce: Number(nonce)}
}
Related Topic