We're currently working on a project that needs to interoperate with other platforms and we're using Metamask through a Signer. However, when running something like
wallet.signMessage(message)
message will always be salted with '\x19Ethereum Signed Message:\n' + <message.length>:
https://github.com/ethers-io/ethers.js/blob/master/src.ts/utils/hash.ts#L59-L63
This will produce signatures that will not match other generic libraries.
We could use myWallet.signingKey.signDigest(hashBytes) , but unfortunately this method is not available when using a Signer attached to MetaMask.
Would it be possible to pass a flag to Wallet.prototype.signMessage to allow bypassing the salt? I can provide a PR if needed.
Thank you!
This is not, in general, possible and is incredibly unsafe. :s
Basically, allowing signing raw messages, without a prefix, enables an app to steal all ether, tokens and assets, which is why MetaMask does not permit you to perform this operation, and it will always force prefixing a signed message (even when the message is a hash, it will still prefix it, just with the embedded message length of 32).
So, there is no safe way to adjust the signer API to do this, since most Signers do not support this type of unsafe operation anyways. The only Signer that actually allows this, is the Wallet, using the technique you described (accessing the raw signing key), but that is highly unrecommended.
Which generic libraries are you using? No modern Ethereum library should allow signing an unprefixed message. I have an article about it here, if you need to implement this in Solidity.
In addition to being unsafe, it also presents a UX issue, since any UI popping up and saying "Do you agree to sign this hash: 0x1234567899?" would allow users to sign away anything, and without any meaningful description...
Which generic libraries are you using? No modern Ethereum library should allow signing an unprefixed message.
We are using go-ethereum to sign and validate requests on the backend side and go-mobile on iOS/Android. The signature on the official go-ethereum example won't match the one obtained with Ethers.
Even the own examples on the Ethers.js manual assume that Solidity contracts have to add the salt on top, because ecrecover won't be compatible out of the box:
Prefixing payloads make sense to prevent replay attacks on different chains. But we don't need to deal with transactions or tokens. All we need to sign is JSON payloads between Gateways and clients so they can check integrity and authenticity. These payloads contain an (ephemeral) timestamp, which is much better salt than static text.
signing raw messages, without a prefix, enables an app to steal all ether, tokens and assets
How would this be? If so, this means that keccak256 and EC encryption are both broken?
Thank you
Geth may provide the ability to sign digests, but there should be a call to correctly use the message signing API. If not, it is easy enough to add (I have added it to Web3J, and implemented in the Objective-C ethers, and the C implementation for the Firefly hardware wallet).
That is completely correct, the Solidity ecrecover requires prefixing to match eth_signMessage, since it operates on digests. This allows it to be used on signed messages and signed transactions (and also Ethereum_signTypedData and other future standards). The precompiles are intended to be generic.
The prefixing has nothing to do with replay protection (EIP-155), and for signed messages, if there are cross-chain concerns, it is important to add replay protection on top of it.
Keccak256 and ECC are not broken, but when doing low-level operations, it is important to understand how they operate.
When you create a transaction, you create an unsigned transaction, and hash it, which is then signed. When you create a message to sign, you prefix it, hash it and then sign it. The important reason for prefixing is so that a cleverly designed message cannot possibly be a valid transaction. The x19 prevents this.
For example:
let unsignedTransaction = "0xe980850218711a00825208948ba1f109551bd432803012645ac136ddd64dba72880de0b6b3a764000080";
let hash = utils.keccak256(unsignedTransaction);
If I give this hash to you, and you sign it, I can staple your signature to the above unsigned transaction, and it is now a valid signed transaction which will send me 1 ether. So, if I could give you an arbitrary hash, and have you sign it, I can have you send transactions to the network.
By signing the hash of a string that begins with "\x19Ethereum Signed Message:", I do not have to worry about that being a transaction, no matter what the message is. :)
Also, keep in mind the prefix also has nothing to do with being a salt. It's purpose is entirely to invalidate any payload as a valid RLP encoded transaction. Also, all future protocols (such as eth_signTypedData) specifically ensure backwards and forwards compatibility. Using a custom prefix, it is important that you familiarize yourself with these restrictions, so you don't expose people to future risk and you aren't exposed to risk from the current schemes. A timestamp is definitely not a safe prefix, especially if you do not use a distinguished encoding (i.e. allowing "1234" and "01234" to both represent the same time opens you up much more significant attacks).
Even if you are just signing a hash of a JSON payload, it is perfectly safe to use signMessage and verifyMessage for your gateway protocol.
Anyways, hope that helps. There is a lot to keep in mind when exposing these types of data, which is why it is nice to layer them on top of known safe, higher-level operations, like signMessage and verifyMessage.
Insightful response @ricmoo :)
I understand now the concerns you express, but yet this forces everyone to use the ethereum prefix for signing generic messages.
As I said, we don't intend to use signatures for transfers or transactions, but the motivations for the current approach make total sense. We'll need to consider other alternatives for signing on a browser in this use case.
And by the way, thank you for the huge effort that has been done to make Ethers.js nice and solid :)
Closing now
So, if you don't plan to use it for Ethereum transactions or messages, you could simply not use the Ethereum parts. You could use ethers.utils.SigningKey directly, and just use the signDigest and recoverPublicKey functions instead of using the Wallet object. You could also get rid of ethers as a dependency altogether, and just import elliptic (the package ethers uses). It's an awesome library. And you can just look at the SigningKey code for examples how to use it.
Another option may be to specify a mnemonic path and use your own custom prefix. For example, Ethereum is just doing what Bitcoin does (which uses a path of 44'/0'/0'/0/0 and a prefix of. \x18Bitcoin Signed Message:\n; note the first byte is always the string length of the static prefix component).
Just an idea. :)
Hey there, I have a similar question.
In my contract, I am implementing the EIP-2612 permit() function, which requires the signature to be prefixed with "\x19\x01". But when I sign the message using wallet.signMessage(<hashed message>), it is automatically prefixed with '\x19Ethereum Signed Message:\n' + <message.length>, as a result, the contract recovers a whole different address from what is expected.
I understand you mention security being the reason for it, but I am also wondering if there's any other method from ethers.js that is compatible to sign messages with the EIP-2612 compliant prefix?
@preston4896 I鈥檓 not familiar with that EIP, but checking it out, it is just using EIP-712, so as long as you format your EIP-712 domain correctly, you should be able to use the signer._signTypedData method, which will handle it, including the domain separator calculation and data serialization it for you. :)
Most helpful comment
This is not, in general, possible and is incredibly unsafe. :s
Basically, allowing signing raw messages, without a prefix, enables an app to steal all ether, tokens and assets, which is why MetaMask does not permit you to perform this operation, and it will always force prefixing a signed message (even when the message is a hash, it will still prefix it, just with the embedded message length of 32).
So, there is no safe way to adjust the signer API to do this, since most Signers do not support this type of unsafe operation anyways. The only Signer that actually allows this, is the Wallet, using the technique you described (accessing the raw signing key), but that is highly unrecommended.
Which generic libraries are you using? No modern Ethereum library should allow signing an unprefixed message. I have an article about it here, if you need to implement this in Solidity.
In addition to being unsafe, it also presents a UX issue, since any UI popping up and saying "Do you agree to sign this hash: 0x1234567899?" would allow users to sign away anything, and without any meaningful description...