question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

WalletConnectUnity personal signed data incorrect / cannot be used to recover public wallet address

See original GitHub issue

Hi,

I think there’s an issue with the WalletConnectSession.EthPersonalSign() method. When I hash and then sign a message, performing a HashAndEcRecover operation does not result in the original wallet address used to sign the data.

I’ll put some example reproduction code in a sec, but one thing that struck me about WalletConnectSession.EthPersonalSign is that it calls WalletConnectSharp.Core.Models.Ethereum.EthPersonalSign, and when it does that it passes the hexData argument as address, and the address argument as hexData. If you switch these around as you’d expect them to be then the calls to a wallet app to sign anything don’t work at all - maybe something to do with the JsonRpcRequest argument order? I don’t know, but I thought perhaps it might be a lead…

Anyway, here’s some example code to reproduce the issue - this can just be plugged into the DemoActions.PersonalSign method, but you’ll have to generate a ‘good’ expected signature as it uses your wallet address’ private key to sign.

    var ethSigner = new EthereumMessageSigner();
    var address = WalletConnect.ActiveSession.Accounts[0];
    var message = "This is a test";
    string expectedSHA3OfMessage = "0x93b90fab55adf4e98787d33a38e71106e8c016f1a124dfc784f3cca4d938b1af";
    string expectedSignature = "YOUR_KNOWN_GOOD_SIGNATURE";
    
    // Test 0 - Confirm SHA3 matches expected result (it does, but we need to prefix "0x" to the result)
    /*
    // This will get you the correct hash
    var hashedMessage = "0x" + new Sha3Keccack().CalculateHash(message);
    // These three lines hashing using the ethSigner will also get you the correct hash (just FYI)
    //var messageByteArray = Encoding.UTF8.GetBytes(message);
    //var hashedMBA = ethSigner.Hash(messageByteArray);
    //var hashedMessage = "0x" + ethSigner.Hash(messageByteArray).ToHex();
    bool sha3IsCorrect = hashedMessage.ToLower().Equals(expectedSHA3OfMessage.ToLower());
    Debug.LogWarning("Expected SHA3: " + expectedSHA3OfMessage +
                            "\nGot SHA3: " + hashedMessage +
                            "\nSHA3 correct?: " + sha3IsCorrect;
    */

    // Test 1 - Personal sign plain message [FAILS]
    /*
    var signature = await WalletConnect.ActiveSession.EthPersonalSign(address, message);
    var recoveredAddress = ethSigner.HashAndEcRecover(message, signature);
    */

    /*** Please note: Regardless of whether we prefix "0x" to the message hash or not, the recovered address is incorrect ***/
    
    // Test 2 - Personal sign message hashed via Sha3keccack [FAILS]
    var hashedMessage = "0x" + new Sha3Keccack().CalculateHash(message); // Can add this: .ToHexUTF8() - still fails!
    Debug.LogWarning("Hash matches expected?: " + hashedMessage.ToLower().Equals(expectedSHA3OfMessage.ToLower())); // Result: true
    var signature = await WalletConnect.ActiveSession.EthPersonalSign(address, hashedMessage);
    var recoveredAddress = ethSigner.HashAndEcRecover(message, signature);
    
    
    // Test 3 - Personal sign message hashed via EthereumMessageSigner [FAILS]
    /*
    var messageByteArray = Encoding.UTF8.GetBytes(message);
    var hashedMBA = ethSigner.Hash(messageByteArray);
    var hashedMBAString = "0x" + ethSigner.Hash(messageByteArray).ToHex();
    Debug.LogWarning("Hash matches expected?: " + hashedMBAString.ToLower().Equals(expectedSHA3OfMessage.ToLower())); // Result: true
    var signature = await WalletConnect.ActiveSession.EthPersonalSign(address, hashedMBAString);
    var recoveredAddress = ethSigner.HashAndEcRecover(message, signature);
    */

    // Test 4 - Personal sign hashed message via HashAndHashPrefixedMessage [FAILS]
    /*
    var messageBytes = Encoding.UTF8.GetBytes(message);
    var hashedPrefixedMessageByteArray = ethSigner.HashAndHashPrefixedMessage(messageBytes);
    var hashedPMBAString = Encoding.UTF8.GetString(hashedPrefixedMessageByteArray);  // Can add this: .ToHexUTF8() - still fails!
    Debug.LogWarning("Hash matches expected?: " + hashedPMBAString.ToLower().Equals(expectedSHA3OfMessage.ToLower())); // Result: false
    var signature = await WalletConnect.ActiveSession.EthPersonalSign(address, hashedPMBAString);
    var recoveredAddress = ethSigner.HashAndEcRecover(message, signature);
    */
    
    bool recoverySuccessful = address.ToLower().Equals(recoveredAddress.ToLower());
    resultText.text = "Address: " + address + "\nRecovered address: " + recoveredAddress + "\nSuccess? " + recoverySuccessful;
    resultText.gameObject.SetActive(true);

As an aside, to replicate the (correct) EthereumMessageSigner.HashAndSign functionality, we again cannot hash and then sign the hashed message, we have to go through the following process:

        // This "one hit" Hash-and-Sign operation hashes and signs correctly. It works...
        var signer = new EthereumMessageSigner();
        var oneHitSignature = signer.HashAndSign(nonce, walletPrivateKey);
        Console.WriteLine("One hit signature is: " + oneHitSignature);

        // Replicate HashAndSign - see the following URL for details:
        // https://github.com/Nethereum/Nethereum/blob/master/src/Nethereum.Signer/EthereumMessageSigner.cs
        var plainNonceBytes = Encoding.UTF8.GetBytes(nonce);            
        var hashedMessageByteArray = signer.HashAndHashPrefixedMessage(plainNonceBytes);
        //var hashedMessageString = Encoding.UTF8.GetString(hashedMessageByteArray); // Not used atm

        // Signing with a EthereumMessageSigner DOES NOT result in the expected signature
        //var multiStepSignature = signer.Sign(hashedMessageByteArray, walletPrivateKey);

        // But signing with the (base) MessageSigner does because it doesn't first hash the message!
        var baseMessageSigner = new MessageSigner();
        var multiStepSignature = baseMessageSigner.Sign(hashedMessageByteArray, walletPrivateKey);

        Assert.AreEqual(oneHitSignature, multiStepSignature); // PASSES!

Best wishes, Al

Issue Analytics

  • State:open
  • Created 2 years ago
  • Comments:5

github_iconTop GitHub Comments

1reaction
ghostcommented, Nov 15, 2021

Actually, this may be a problem after all =/

In the below code example, the returned signature is equivalent to that of Nethereum’s EncodeUTF8AndSign operation:

string msg = "This is a test!";
var signature = await WalletConnect.ActiveSession.EthPersonalSign(address, msg);

However, when signing via a wallet such as MetaMask in JavaScript (without using WalletConnect) signing the exact same data via HashAndSign generates a completely different signature.

If you dig into web3.eth.personal.sign.HashAndSign it’s as follows:

public override string HashAndSign(byte[] plainMessage, EthECKey key)
{
    return base.Sign(HashAndHashPrefixedMessage(plainMessage), key);
}

Stepping into HashAndHashPrefixedMessage gives us:

public byte[] HashAndHashPrefixedMessage(byte[] message)
{
     return HashPrefixedMessage(Hash(message));
}

Stepping into HashPrefixedMessage (which should probably be called PrefixAndHashMessage) gives us:

public byte[] HashPrefixedMessage(byte[] message)
{
     var byteList = new List<byte>();
     var bytePrefix = "0x19".HexToByteArray();
     var textBytePrefix = Encoding.UTF8.GetBytes("Ethereum Signed Message:\n" + message.Length);
     byteList.AddRange(bytePrefix);
     byteList.AddRange(textBytePrefix);
     byteList.AddRange(message);
     return Hash(byteList.ToArray());
}

And finally, Hash (from EthereumMessageSigner’s base MessageSigner class) is:

public byte[] Hash(byte[] plainMessage)
{
     var hash = new Sha3Keccack().CalculateHash(plainMessage);
     return hash;
}

This is what we want to do - an operation equivalent to HashAndSign - summarising the above, the sequence is:

  1. Hash the nonce (in HashAndHashPrefixedMessage) then,
  2. Prefix and hash the hash of the nonce (in HashPrefixedMessage), then
  3. Sign the prefixed-and-hashed hash of the original plain nonce.

But, as mentioned, what WCU actually does is equivalent to EncodeUTF8AndSign, which is:

public string EncodeUTF8AndSign(string message, EthECKey key)
{
     return base.Sign(HashPrefixedMessage(Encoding.UTF8.GetBytes(message)), key);
}

The sequence of operations for EncodeUTF8AndSign is:

  1. Prefix and hash the message received in HashPrefixedMessage, then
  2. Sign the prefixed-and-hashed message.

Given these steps, you would think that to make EncodeUTF8AndSign function as per HashAndSign, all you would need do is pre-hash the provided input so that EncodeUTF8AndSign prefixes and hashes the (now already hashed) input.

Unfortunately, this does not result in a signature that matches the output of HashAndSign - and I’m damned if I know why, because on paper the steps are identical.

Any thoughts you might have about this would be incredibly gratefully received as I’ve tried absolutely everything I can think of and cannot find any way whatsoever to get WCU’s EncodeUTF8AndSign-like functionality to match that of HashAndSign!

References https://github.com/Nethereum/Nethereum/blob/master/src/Nethereum.Signer/EthereumMessageSigner.cs https://github.com/Nethereum/Nethereum/blob/master/src/Nethereum.Signer/MessageSigner.cs https://web3js.readthedocs.io/en/v1.2.11/web3-eth-personal.html#sign https://web3js.readthedocs.io/en/v1.2.11/web3-eth-accounts.html#sign

(BTW: Both web3.eth.personal.sign and web3.eth.accounts.sign both generate the identical signatures from the same input)

0reactions
ecp4224commented, Nov 28, 2021

Thank you for looking into this thoroughly! I believe what you say is correct, the current implementation does not include the 0x19Ethereum Signed Message:\n prefix because the wallet is supposed to include this data in the original message (at least that was my understanding of personal_sign)

I believe the reason you were having trouble getting the same hash is because of a bug that was discovered in #21 where the parameters for the RPC call were swapped

Read more comments on GitHub >

github_iconTop Results From Across the Web

No results found

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found