WalletConnectUnity personal signed data incorrect / cannot be used to recover public wallet address
See original GitHub issueHi,
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:
- Created 2 years ago
- Comments:5
Top GitHub Comments
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: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:Stepping into
HashAndHashPrefixedMessage
gives us:Stepping into
HashPrefixedMessage
(which should probably be calledPrefixAndHashMessage
) gives us:And finally,
Hash
(from EthereumMessageSigner’s baseMessageSigner
class) is:This is what we want to do - an operation equivalent to
HashAndSign
- summarising the above, the sequence is:HashAndHashPrefixedMessage
) then,HashPrefixedMessage
), thenBut, as mentioned, what WCU actually does is equivalent to
EncodeUTF8AndSign
, which is:The sequence of operations for
EncodeUTF8AndSign
is:HashPrefixedMessage
, thenGiven these steps, you would think that to make
EncodeUTF8AndSign
function as perHashAndSign
, all you would need do is pre-hash the provided input so thatEncodeUTF8AndSign
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 ofHashAndSign
!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
andweb3.eth.accounts.sign
both generate the identical signatures from the same input)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 ofpersonal_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