Signing and Verifying Ethereum Hashed Messages

Aw Kai Shin
6 min readOct 31, 2022

--

Photo by energepic.com

One exciting feature of Ethereum is the ability to prove consent directly between parties. In other words, you can easily validate that someone agreed to something without the need for a third party. As there is practically no limitation to the data being agreed to, this opens up endless possibilities when it comes to designing how users interact digitally.

Of note, this capability comes from the cryptography underlying Ethereum and not the EVM itself. While this complex maths is not in the scope of this article, we still require a very high level understanding of the functionality that Public Key Cryptography/ECDSA cryptography enables:

  • Messages are signed using a private key which results in a raw signature
  • Anyone with the raw signature and unsigned message will be able to validate the account which signed the message using the corresponding public key
  • The unsigned message can not be extracted from the raw signature without the corresponding private key

You can check out this amazing thread if you would like an introduction to ECDSA:

Consequently, this also enables for such proofs to be handled off-chain which means no gas (and therefore no money) is required when it comes to validating that a message has indeed been signed by a particular wallet. In effect, off-chain interactions which are secured by the option to settle on-chain.

This paves the way for use cases which are critical to mainstream adoption:

  • User onboarding: New crypto users are unlikely to have the required coins to interact with a smart contract. By signing a message, the operator can send the signed transaction to the network therefore paying gas fees on behalf of the user.
  • Decentralised identity: Assuming a wallet represents an individual, users will be able to create new relationships without the need to coordinate financially. By validating a message signature, users can be assured that the person on the other end is who they say they are.
  • Scaling via channels: Transactions can be accumulated in the form of signed messages with final settlement happening once on-chain. Intermediate state changes insured by the main chain security.
  • Pre-authorised agreements: Transactions can be signed with final settlement being decided by a third party. This enables non-custodial management of order flows.
  • Offline signing: Even in places where there is no internet, transaction computation can still take place on a local computer. Users can transact without the need for a network connection.

For this guide, we will be covering signing and validation of Ethereum hashed messages. Hashing the message allows for better efficiency and security on-chain while also providing an additional layer of privacy. Do note that by hashing the message, we are prioritising the above over message readability at the point of signing. If you would like the human readable message version:

This guide assumes basic familiarity with Express, Ethers.js, Browserify, and Metamask. If you need an introduction on how to setup these tools, you can refer to a previous guide:

The Github repository for this guide contains a few key files:

  • /client/signing/sign.ts: Client functions required to sign the message
  • /client/signing/validate.ts: Client functions triggering message validation on the server
  • /routes/api.ts: Client/Server APIs used to communicate data
  • /server/validate.ts: Server functions to store and validate the data

Sign Offline Message

For this guide, the Issuer will be allowed to customise their message to be signed with their own Metamask wallet. In order to determine the legitimacy of the signature, the payload and fully-extended signature will be stored on the server for future validation.

We first create the UI form for the custom message to be inputted:

Upon submission, our client code (/client/signing/sign.ts) will process the signature sequentially by:

  • Encoding the message as a DataHexString in preparation for hashing. The AbiCoder is required to translate between the binary data formats used to interoperate between the EVM and higher level libraries.
  • Hashing the payload with the keccak256 algorithm. This hashing enables the payload to be more efficiently and securely validated if the data is stored on the chain as bytes32 does not require assembly (not in scope but good to have). Alternatively, if the above is less of a priority, it is also possible to display the human readable message for the user to sign in Metamask by omitting these 2 steps, you can view this in a separate guide.
  • Prompting the user to sign the hashed payload with signMessage() which prefixes the Ethereum (EIP-191) specific identifier "\x19Ethereum Signed Message:\n" and byte length of the message. This eliminates the risk of replay attacks on other EVM platforms. Note that Metamask is used for signing without exposing the Issuer’s private keys.
  • Generate the fully expanded-format of the signed message with splitSignature(). The cryptography behind this is out of scope but effectively, this signature is unique to the signed message (i.e. private key and signed message combination) which then enables the message to be validated by another party.
  • Post the payload and expanded signature to the server to be saved for future validation. Note that only the payload, payloadHash, and fullyExpandedSig were sent to the server via /api/signedMessage.

The logic for saving to the server can be found in /routes/api.ts:

Validate Message Signer

As the signed message is now stored on the server, we can now validate the message signer. For simplicity, it is assumed that the Validator triggers the validation from the same UI. When in production, this message validation will likely form a prerequisite before any application specific function is triggered. The core validation logic is independently processed by the server (/server/validate.ts).

To enable validation to be triggered, we create a validation button as well as a span which will hold the signing address once processed:

This button will trigger validation on the server via the /api/validateSignature route specified in /client/signing/validate.ts:

Following the API specified, the server will attempt to validate the message with verifyMessage() which returns the address which produced the signed signature. In practice, we would then compare this returned address against the Issuer’s public key which would indicate the validity of the signature. Notice also that verifyMessage() only requires the payloadHash and fullyExpandedSig which implies that validation can be processed without the need for sharing private keys.

Extract Decoded Message

In the event that the server-side requires the human readable message, it can also be obtained using abiCoder.decode() as the encoded payload was also previously saved to the server.

In this case, the data type will need to be known by the server in order to decode the payload. With complex types, this makes it more difficult for the data to be read by any unwanted parties.

Thanks for staying till the end. Would love to hear your thought/comments so do drop a comment. I’m active on twitter @AwKaiShin if you would like to receive more digestible tidbits of crypto-related info or visit my personal website if you would like my services :)

--

--

Aw Kai Shin

Web3, Crypto & Blockchain: Building a More Equitable Web | Technical Writer @FactorDAO | www.awkaishin.com