One great aspect of the Ethereum engineering community is that engineers are generally around to help each other out. In this blog post I’d like to continue that trend, explaining a process we recently went through in building something seemingly simple, yet in its details resulting in a fairly complicated engineering effort, which led to PRs to both Geth and Metamask’s codebases.
A bit of background: we’re actively working on an identity management system which has a feature requiring users to verify control of an Ethereum address. Our initial thought was to use Metamask’s simple
eth_sign API, however, we quickly realized that this wasn’t the right path to go down given that the method results in a red error message in Metamask’s UI:
Instead, we decided to work with the new EIP-712 standard, which presents the user with a much nicer interface and is easily verified on-chain if we ever need this ability in the future. By the end of this guide we’ll show you how to build the following flow:
- A Go backend generates an EIP-712 message and sends it to the user
- The user signs the message with Metamask
- The resulting signature is sent to the backend and verified to ensure its authenticity.
Pretty straightforward, right? Not quite! As always, the devil is in the details, and in this case, the details were fairly involved due to nascent documentation, upon which we hope to improve through this post.
Generating the EIP-712 in Go
A major advantage of building in Go is the use of Geth’s built-in libraries. Lucky for us, EIP-712 support was recently merged in, allowing us to piggy-back off of the standard library for our purposes. The first step is to generate a pseudorandom challenge for the user to sign. We will pass the challenge to the user for signing by Metamask and then verify the user-submitted result on the server.
A lot of stuff going on here, but let’s break it down a bit, starting with the
Here we’re defining the fields of the two data structures we’ll be passing in our challenge to Metamask for signing: a
Challenge and an
EIP712Domain. The former is entirely defined by us, the latter is following the suggested domain separator structure from the EIP-712 standard. The domain separator is designed to ensure messages being signed by clients can only be used for specific dApps (and their corresponding on-chain contracts). Since our particular use-case doesn't involve any on-chain verification, the domain wasn't quite as critical to get right. That being said, if you're developing a dApp and planning on verifying signatures in smart contracts, the domain structure should be carefully considered to ensure it is properly namespaced for both security and versioning purposes.
Next we specify the primary type. This is simply the name of the data structure which contains the meat of our EIP-712 message– in our case, this is our previously defined
Finally, we fill in the blanks!. The
Domain key sets the values for each of the fields we specified in our domain struct definition, and the
Message key sets the values for each of the fields we specified in our primary type's (
Challenge) struct definition.
At this point, we have a completed EIP-712 payload, but we’re not quite done yet. We’ll need to take a hash of its data in order to later verify the signature we receive from the user. Unfortunately, this bit was a bit trickier to figure out than it originally seemed, however, the result isn’t overly complicated:
Note that we’re ignoring the second value of each of the
HashStruct calls (their error response). In production code, we highly recommend that you check for these error values and handle them accordingly.
With that warning out of the way, let’s break this code chunk down a bit. When signing an EIP-712 message the client doesn’t sign the full payload– instead, it just signs a
keccak256 hashed version of its contents. Luckily geth's standard library handles most of this process for us, however the final step of hashing the data and putting everything together is left to us.
The first two lines handle encoding and hashing our domain and primary type structs. This is relatively straightforward, just calling
HashStruct with the name of the struct and a map of its data.
Once we’ve hashed the two structs we’ll need to format them into an EIP-712 compliant byte string. If you’re curious to learn why the
\x19\x01 is necessary, feel free to dive into this section of the spec.
Now that we have the byte string, we simply hash it and voila! We now have everything we need to complete our verification process. At this point, you should serialize the
signerData struct into JSON and send it off to your client for signing. Additionally, you'll need to store the
challengeHash in a database to be later retrieved once the user has submitted their signature for verification.
Signing the payload with Metamask
Now that we’ve generated the signing payload in our backend I’ll assume you’ve found some means of sending it to your user’s browser for signing. This piece of the puzzle is a bit more straightforward, just feeding the payload into Metamask and retrieving the resulting signature:
Once again, let’s break this code snippet down a bit:
In this first clause we’re going through the standard Metamask dance: ensuring the user has the extension installed, asking for their permission to connect to the Ethereum API, and retrieving an array of their wallet addresses (the
accounts variable) upon success.
Note that a more complete implementation would anticipate users having more than one account available and present the user with an option to select which account they wish to sign the message with. This step should be done prior to generating the message, as you’ll want to make sure the
Challenge.address field of our EIP-712 message matches the address used to sign it.
eth_signTypedData_v3 RPC method.
data object. Additionally, we noticed a bug with fields of type
string in the message struct definition: if it begins with a
0x, Metamask's signing library will automatically convert it into a byte string without notice, throwing off your signature and resulting in a bad time for you. This PR addresses the issue, but if you or your users are on an older version of Metamask you should be aware of this issue.
One last step! Let’s look into an example implementation of
This implementation is a bit of an educational display following Metamask’s lead in their intro blog post. The most important piece of this is setting the
signature variable, as you'll want to chop off the first two characters before sending it back to Go for verification.
Phew! That got a little messy, but we finally have a signature out of Metamask which we’ve sent off to our backend. The final step in the challenge/response process is going to be verifying this freshly baked signature on our backend.
Verifying the signature in Go
Back in our server-side codebase, I’ll assume you have some sort of means of receiving the signature:
Once again, let’s break this snippet down:
To begin, we’re converting the hex-encoded signature (directly from Metamask) into a
byte slice, which is the data type we'll need to manipulate it properly. Next we run two verifications on the signature, checking to make sure its of the correct length and making sure its recovery ID (the last byte) is set to 27 or 28. The latter check is to ensure the signature complies with the "legacy reasons" specified in the
Ecrecover function definition within Geth. Continuing to follow the spec, we subtract 27 from the recovery ID to convert it to a
1 , another oddity of
Finally, this is where the magic happens! We use geth’s
Ecrecover function to derive a public key from the provided signature. If this public key's Ethereum address matches our user's Ethereum address, we're all set! The message has been successfully verified. If the public key differs we know that the signature is invalid, either due to a malformed payload or an incorrect signing key.
We hope this blog post helps you in any future journey you may make through the complicated world of EIP-712 signature verification. This process was far more difficult than we originally thought when we first set out, but we’re hoping documenting our pain will help save others time.
That’s all for this guide! If you have any feedback, questions, or corrections please don’t hesitate to reach out to me on Twitter, my handle is @stevenleeg.
Special thanks to Kumavis from Metamask for spending hours on the phone digging through Geth and Metamask’s codebases in order to get this working properly.