Ethers.js: Trouble with signature verification using `signer.signMessage` with `Web3Provider`

Created on 29 Jul 2018  路  10Comments  路  Source: ethers-io/ethers.js

I'm trying to do some simple signature verification using signer.signMessage with web3Provider but I think I'm missing something. I can't quite get the addresses to match up. Here's my code:

PERSON_1 private key: da4a4ffa13bcce17a4fd6f98484ad450a8b17e0bf3f953581d0566ae23c5ac6d

    const PERSON_1 = accounts[2]
    const signThing = "0x50e255a73d200fd6365e4c58f756df5dd7e26ed02bed3b5a9baca066394fba26";
    const signature = await signers[PERSON_1].signMessage(signThing);
    const solidityAddress = await exchangeWrappers[PERSON_1].signatureTest(signThing, signature)
    console.log("Address from solidity:" + solidityAddress);
    console.log("PERSON_1 public: " + PERSON_1);

exchangeWrappers is just an object of ether.js Contracts where the key is the public key. I'm in a truffle environment for reference. This outputs

Address from solidity:0xc43111BcB37D98a9581053cBD12b471c0ae7ceB5
PERSON_1 public: 0xf0c73d32c31cced98d86402dbac4f8e2f125b3a7

and signatureTest is simply:

    function signatureTest(
        bytes32 message, 
        bytes sig
    ) 
        public 
        pure 
        returns (address) 
    {
        return ECRecovery.recover(
            ECRecovery.toEthSignedMessageHash(message), 
            sig
        );
    }

where the ECRecovery library is from zeppelin:

pragma solidity ^0.4.24;


/**
 * @title Eliptic curve signature operations
 * @dev Based on https://gist.github.com/axic/5b33912c6f61ae6fd96d6c4a47afde6d
 * TODO Remove this library once solidity supports passing a signature to ecrecover.
 * See https://github.com/ethereum/solidity/issues/864
 */

library ECRecovery {

  /**
   * @dev Recover signer address from a message by using their signature
   * @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address.
   * @param sig bytes signature, the signature is generated using web3.eth.sign()
   */
  function recover(bytes32 hash, bytes sig)
    internal
    pure
    returns (address)
  {
    bytes32 r;
    bytes32 s;
    uint8 v;

    // Check the signature length
    if (sig.length != 65) {
      return (address(0));
    }

    // Divide the signature in r, s and v variables
    // ecrecover takes the signature parameters, and the only way to get them
    // currently is to use assembly.
    // solium-disable-next-line security/no-inline-assembly
    assembly {
      r := mload(add(sig, 32))
      s := mload(add(sig, 64))
      v := byte(0, mload(add(sig, 96)))
    }

    // Version of signature should be 27 or 28, but 0 and 1 are also possible versions
    if (v < 27) {
      v += 27;
    }

    // If the version is correct return the signer address
    if (v != 27 && v != 28) {
      return (address(0));
    } else {
      // solium-disable-next-line arg-overflow
      return ecrecover(hash, v, r, s);
    }
  }

  /**
   * toEthSignedMessageHash
   * @dev prefix a bytes32 value with "\x19Ethereum Signed Message:"
   * and hash the result
   */
  function toEthSignedMessageHash(bytes32 hash)
    internal
    pure
    returns (bytes32)
  {
    // 32 is the length in bytes of hash,
    // enforced by the type signature above
    return keccak256(
      abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
    );
  }
}

Any ideas? I'm baffled why the addresses don't match :confused: .

Thanks!

Most helpful comment

Oh, but one quick note I just saw above is that you are signing a string, which means your length is 66 bytes long. If you want to sign the 32 byte binary value of the string as a bytes32, you must pass in the bytes; you can use ethers.utils.arrayify to convert a hex string into a Uint8Array.

All 10 comments

I will look more into this when I get home, but a quick note, I believe, TestRPC (which is what is used by truffle) has a bug where 50% of all signed messages have the wrong signature... Can you try signing 3 or 4 different messages and see if some work?

(There will be an ethers replacement for TestRPC soon ;))

Oh, but one quick note I just saw above is that you are signing a string, which means your length is 66 bytes long. If you want to sign the 32 byte binary value of the string as a bytes32, you must pass in the bytes; you can use ethers.utils.arrayify to convert a hex string into a Uint8Array.

Wow I can't believe I missed that. Thank you! I'll close this now. Would you be open to a documentation PR on v4 to add a simple signing example like this one for future users? I can also add some snippets that I've pieced together from reading various issues in the past week and put together a truffle example.

@ricmoo I also just want to say thank you for all the support you give for this library and for the library itself. It is really a step up from web3, your responses to issues are incredibly helpful and inhumanly fast, and the docs are great.

The v4 documentation is still entirely local, so don't make any PR for it yet, otherwise it is just something else to merge. I think there is a task already open to add an example to the cookbook, but I'll add one now so I don't forget, then it will just exist once the v4 docs are up. :)

I'd also like to get some "from start to finish" examples for tutorials on how to build a dapp. I don't use truffle myself, so it'd be good to get someone with knowledge of it to give a full example using truffle.

I try to respond from my phone when I can, which is why sometimes the code snippets are a bit sparse (why is the triple-backtick so hard to type on iOS?), but I try to answer what I can as soon as I can. I do appreciate the appreciation though. Thanks! :)

@mrwillis Mind sharing an updated working snippet? I've been running into the exact same problem and tried to arrayify, hexlify, toUtf8Bytes and other combinations but I always end up in the same mess.

Thanks!

Are you using v4 or v3?

v3

I have the same signatureTest function as defined above by @mrwillis using OpenZeppelin's ECRecovery:

> w = ethers.Wallet.createRandom()
> msg = ethers.utils.id('hello world')
'0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad'
> sig = w.signMessage(msg)
'0xd30d4b49e51df0428a9aa8da22d45f872298ab58f9fe0d5c8799de67026c03fc279e950d2f983fcd0f39b196f5126f6df93c0ebe3d1f4d2d0b3c26a09ec79d911c'
> contract.signatureTest(msg, sig)
'0xa6a6d419ef2ff9f5f6fdb6fcae1aa8233cd040df'
> w.address
'0x026fefe9B869485Aa1fcF39c851a082BC2E29315'

Err, never mind. I thought I tried this before but it actually works (using w.signMessage(ethers.utils.arrayify(msg)) above)

Awesome! :)

I need to add a section to the documentation to explain why we need to do this, but basically strings are treated as strings and Uint8Arrays are treated like binary data.

The problem, I think, is that almost anywhere else in the library you can use a hex string as data, but in this one case both strings and binary data make sense. Any feedback or suggestions to better explain this in the docs are appreciated. I will likely load it up with lots of examples. :)

Was this page helpful?
0 / 5 - 0 ratings