Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to sign messages without the "\x19Ethereum Signed Message" prefix? #555

Closed
ghost opened this issue Jul 5, 2019 · 11 comments
Closed
Labels
discussion Questions, feedback and general information.

Comments

@ghost
Copy link

ghost commented Jul 5, 2019

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!

@ghost ghost changed the title Allow wallets to sign unsalted messages Allow signers/wallets to sign unsalted messages Jul 5, 2019
@ghost ghost changed the title Allow signers/wallets to sign unsalted messages Allow signers to sign unsalted messages Jul 5, 2019
@ricmoo
Copy link
Member

ricmoo commented Jul 5, 2019

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...

@ricmoo ricmoo added the discussion Questions, feedback and general information. label Jul 5, 2019
@ricmoo ricmoo changed the title Allow signers to sign unsalted messages How to sign messages without the "\x19Ethereum Signed Message" prefix? (Answer: in general, you cannot Jul 5, 2019
@ricmoo ricmoo changed the title How to sign messages without the "\x19Ethereum Signed Message" prefix? (Answer: in general, you cannot How to sign messages without the "\x19Ethereum Signed Message" prefix? (Answer: in general, you cannot) Jul 5, 2019
@ricmoo ricmoo changed the title How to sign messages without the "\x19Ethereum Signed Message" prefix? (Answer: in general, you cannot) How to sign messages without the "\x19Ethereum Signed Message" prefix? Jul 5, 2019
@ghost
Copy link
Author

ghost commented Jul 5, 2019

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

@ricmoo
Copy link
Member

ricmoo commented Jul 6, 2019

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.

@ghost
Copy link
Author

ghost commented Jul 6, 2019

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

@ghost ghost closed this as completed Jul 8, 2019
@ricmoo
Copy link
Member

ricmoo commented Jul 9, 2019

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. :)

@preston4896
Copy link

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?

@ricmoo
Copy link
Member

ricmoo commented May 6, 2021

@preston4896 I’m 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. :)

@kbanta11
Copy link

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?

Was there ever a solution here? Uniswap prefixes with \x19\x01 for their position NFTs, and so it is impossible to sign an off-chain signature to pass to permit() on their ERC721Permit, since this prefix is always different.

@ricmoo
Copy link
Member

ricmoo commented Apr 19, 2023

@kbanta11 The 0x1901 prefix is for Eip-191, which you use the signer.signTypedData for. I’m pretty sure permit works just fine, I seem to recall using it via MetaMask before.

@dovigod
Copy link

dovigod commented Oct 2, 2023

@ricmoo Hi, I have some question of your comment

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.

  1. how does \x19 makes message not to be valid transaction ??

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. :)

  1. I can't understand, how does prefix "\x19Ethereum Signed Message:" makes transaction invalid.

btw, I'have read your article https://blog.ricmoo.com/verifying-messages-in-solidity-50a94f82b2ca, It helped me a lot to understand, but still, I don't get it why '\x19' makes transaction invalid

@SyedAsadKazmi
Copy link

@ricmoo Hi, I have some question of your comment

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.

  1. how does \x19 makes message not to be valid transaction ??

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. :)

  1. I can't understand, how does prefix "\x19Ethereum Signed Message:" makes transaction invalid.

btw, I'have read your article https://blog.ricmoo.com/verifying-messages-in-solidity-50a94f82b2ca, It helped me a lot to understand, but still, I don't get it why '\x19' makes transaction invalid

@dovigod, The prefix explicitly declares that the hash represents a message, not a transaction. Since the prefix and message length are not part of any transaction structure, the resulting hash will not match the hash of a valid transaction and cannot be interpreted as a transaction, even if someone tries to repurpose the signature.

In contrast, if you directly hash the message (without the prefix), there is no guarantee that the hash won't match the hash of a valid transaction under specific circumstances. This introduces the risk of the signature being misused to authorize a transaction.

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Questions, feedback and general information.
Projects
None yet
Development

No branches or pull requests

5 participants