Ethers.js Issue – Can’t Validate Authenticated Message with Ethers.js

ethers.jshardhatsoliditywallets

I need to be able to retrive a document from the contract, have it signed by a users wallet and then send back to the contract and the signature validated.

Here's how I have address0 sign it on the client side:

   let message : string = "check this message signed by account0";
   let messageHash = keccak256(utils.toUtf8Bytes(message));
   let signature = await address0.signMessage(messageHash);
   await hm.connect(address0).verifyMessage(message, signature);

Here's the validator in my contract:

   function verifyMessage(string memory message, 
      bytes memory signature) 
      public view  returns(bool) {
      
        //hash the plain text message
        bytes32 messagehash =  keccak256(bytes(message));
        //hash the prefix and messagehash together   
        bytes32 messagehash2 = keccak256(abi.encodePacked("x19Ethereum Signed Messsage:\\n32", messagehash));
        //extract the signing contract address
        address signeraddress = ECDSA.recover( messagehash2, signature);
        if (msg.sender==signeraddress) {
            //The message is authentic
            return true;
        } else {
            //msg.sender didnt sign this message.
            return false;
        }
    }

Regretably, the value returned by ECDSA.recover for signeraddress is not account0's address, and despite a lot
of experimentation I've not been able to derive the correct
address of the message sender.

Would appreciate any pointers.

Best Answer

I was able to get an answer from folks over on Openzeppelin.

In case any body else runs into the situation, one issue is the way that the hash is calculated and the signature is computed on the client side

Instead of

   let messageHash = keccak256(utils.toUtf8Bytes(message));

use

    let messageHash = ethers.utils.solidityKeccak256(['string'], [message]);

and instead of

    let signature = await address0.signMessage(messageHash);

use

     let signature = await address0.signMessage(ethers.utils.arrayify(messageHash));

On the server side, the addition of the prefix can be done more simply with `ECDSA.toEthSignedMessageHash() as in the solution below:

using ECDSA for bytes32; 

function verifyMessage(string memory message, bytes memory signature) public view  returns(address, bool) {
        //hash the plain text message
        bytes32 messagehash =  keccak256(bytes(message));
       
        address signeraddress = messagehash.toEthSignedMessageHash().recover(signature);
              
        if (msg.sender==signeraddress) {
            //The message is authentic
            return (signeraddress, true);
        } else {
            //msg.sender didnt sign this message.
            return (signeraddress, false);
        }
    }