[Ethereum] Metamask Signing gives different signature than web3.js

signaturetruffle

I have successfully been using metamask to sign EIP712 data. I am currently trying to write truffle tests and need to replicate the signing process outside the browser. The issue I am facing is that the signatures using EthUtil.ecsign and web3.eth.accounts.sign are not matching the one that is generated in the browser.
Here is the simplified code explaining my situation:

Here is the example data to be signed (replace the chainId with the updated one from your local ganache. Can be easily found by opening remix and connecting to injected web3. Otherwise you will not be able to sign it in metamask.)

Example Data – NEED TO UPDATE CHAIN ID

{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"},{"name":"salt","type":"bytes32"}],"MultiSigTransaction":[{"name":"destination","type":"address"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"},{"name":"nonce","type":"uint256"},{"name":"executor","type":"address"},{"name":"gasLimit","type":"uint256"}]},"domain":{"name":"Simple MultiSig","version":"1","chainId":1576101149278,"verifyingContract":"0xE50b7c93982806FB643Ac36932f3C2Df83c23744","salt":"0x251543af6a222378665a76fe38dbceae4871a070b7fdaf5c6c30cf758dc33cc0"},"primaryType":"MultiSigTransaction","message":{"destination":"0xa76dC869E4986618d8f6Fa7d0168Df9257395b98","value":0,"data":"0x8456cb59","nonce":"0","executor":"0x0000000000000000000000000000000000000000","gasLimit":3000000}}

Truffle Test:

let privateKey = '0x1929daf274be3d61bae47f37922bea3c76082d14408416273501692803268e4a'
var Web3 = require('web3');
const web3 = new Web3('http://localhost:8545');
var EthUtil = require('ethereumjs-util');

//UPDATE CHAINID and copy paste from above
let data = ''

var msgHash = EthUtil.hashPersonalMessage(new Buffer(data));
var signature = EthUtil.ecsign(msgHash, new Buffer(privateKey.substring(2), 'hex'));
let _r = "0x" + signature.r.toString('hex')
let _s = "0x" + signature.s.toString('hex')
let _v = signature.v

console.log(_r)
console.log(_s)
console.log(_v)

console.log(web3.eth.accounts.sign(data,privateKey))

Output:

0x2c5c25efe9556bbf81c3fd156c463e102091eecaead11ef3798d151ad2933eb2
0x402a71b9da4d9bf17968a0323a89e62fc12a3c738b21c6a0dc5c874e0b258fea
28
{
  message: '{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"na
me":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"},{"name":"salt","type":"bytes32"
}],"MultiSigTransaction":[{"name":"destination","type":"address"},{"name":"value","type":"uint256"},{"name":"
data","type":"bytes"},{"name":"nonce","type":"uint256"},{"name":"executor","type":"address"},{"name":"gasLimi
t","type":"uint256"}]},"domain":{"name":"Simple MultiSig","version":"1","chainId":1576101149278,"verifyingCon
tract":"0xE50b7c93982806FB643Ac36932f3C2Df83c23744","salt":"0x251543af6a222378665a76fe38dbceae4871a070b7fdaf5
c6c30cf758dc33cc0"},"primaryType":"MultiSigTransaction","message":{"destination":"0xa76dC869E4986618d8f6Fa7d0
168Df9257395b98","value":0,"data":"0x8456cb59","nonce":"0","executor":"0x000000000000000000000000000000000000
0000","gasLimit":3000000}}',
  messageHash: '0xa995afdfa772f73961a28f5e71ab2f5af2b63d86f7e424bc42ff6a6ddc0c6c34',
  v: '0x1c',
  r: '0x2c5c25efe9556bbf81c3fd156c463e102091eecaead11ef3798d151ad2933eb2',
  s: '0x402a71b9da4d9bf17968a0323a89e62fc12a3c738b21c6a0dc5c874e0b258fea',
  signature: '0x2c5c25efe9556bbf81c3fd156c463e102091eecaead11ef3798d151ad2933eb2402a71b9da4d9bf17968a0323a89e
62fc12a3c738b21c6a0dc5c874e0b258fea1c'

Index.html – run using http-server (https://www.npmjs.com/package/http-server)

<head>
    <meta charset="utf-8">
    <title>EIP712 browser demo</title>
  </head>
  <body>
    <script src="./sign.js" language="javascript"></script>
    <p>
      This page tests signing a SimpleMultiSig transaction using EIP712. It is based on <a href="https://weijiekoh.github.io/eip712-signing-demo/index.html">this EIP712 demo</a> by Wei Jie Koh.
    </p>

    <table>
      <thead>
      </thead>
      <tbody>
        <tr>
          <td>Wallet address</td
          <td><input type="text" id="data" size="40" value=""></td>
        </tr>
      </tbody>
    </table>

    <button id="signBtn">Sign data</button>

    <h3>Signed Data</h3>
    <div><textarea id='signedData' rows=8 cols=40></textarea></div>


  </body>

Sign.js

function parseSignature(signature) {
    var r = signature.substring(0, 64);
    var s = signature.substring(64, 128);
    var v = signature.substring(128, 130);

    return {
        r: "0x" + r,
        s: "0x" + s,
        v: parseInt(v, 16)
    }
}

window.onload = function (e) {

    // force the user to unlock their MetaMask
    if (web3.eth.accounts[0] == null) {
        alert("Please unlock MetaMask first");
        web3.currentProvider.enable().catch(alert);
    }

    var signBtn = document.getElementById("signBtn");
    signBtn.onclick = function (e) {
        if (web3.eth.accounts[0] == null) {
            return;
        }

        const signer = web3.eth.accounts[0];

        let data = document.getElementById("data").value

        web3.currentProvider.sendAsync(
            {
                method: "eth_signTypedData_v3",
                params: [signer, data],
                from: signer
            },
            function (err, result) {
                if (err || result.error) {
                    return console.error(result);
                }

                console.log(result)

                const signature = parseSignature(result.result.substring(2));
                document.getElementById("signedData").value = "r: " + signature.r + "\ns: " + signature.s + "\nv: " + signature.v
            }
        );
    };
}

Browser Output:

r: 0x5855c270946c62d3af0d1f01e152389c86a0a0962a0607e9185dfce958cc2eeb
s: 0x11dfb96cd5ffb054ffe0aff67964c8b10c6a225be4fd1d04e058d83e92989a86
v: 27

Best Answer

The algorithm ECDSA used by Ethereum to sign has a random component. So it is possible the same data signed by the same account will have different signatures.

To validate signatures I'd suggest to use ecRecover to retrieve an address and verifies that it matches the signer address.