Learning Developer Guides

Signing and Verifying Ethereum Messages

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 messages. This prioritises message readability at the point of signing, Do note that this is in contrast to hashing the message which allows for better efficiency and security on-chain while also providing an additional layer of privacy. If you would like to see a hashed message guide:

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:

  • Prompting the user to sign the message with [signMessage()](https://docs.ethers.io/v5/api/signer/#Signer-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. Notice that the custom message is also displayed to the user for signing in Metamask. Furthermore, 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()](https://docs.ethers.io/v5/api/utils/bytes/#utils-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 message and expanded signature to the server to be saved for future validation. Note that the unsignedMessage, signedMessage, and fullyExpandedSig was 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()](https://docs.ethers.io/v5/api/utils/signing-key/#utils-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 unsignedMessage and fullyExpandedSig which implies that validation can be processed without the need for sharing private keys.