Intro to Cryptography and Signatures in Ethereum

Immunefi
Immunefi
Published in
11 min readDec 16, 2021

Everyone who has ever dealt with a blockchain system like Ethereum knows what blockchain consists of, such as blocks, transactions, and accounts. But we don’t often think about the basics of blockchain systems, just like we don’t often think about how our organs work. As organs need blood and oxygen to function, blockchain needs cryptography to function properly. Without it, the system would crumble, and you would not be able to claim ownership of that shiny new NFT you just minted.

It was inevitable that sooner or later, we would need to talk about cryptography and signatures. But bear with us, as we’re going to make this as digestible as possible.

Public Key Cryptography

Two of the main purposes of cryptography are to prove knowledge of a secret without revealing that secret and to prove the authenticity of data (digital signature). Cryptography is used extensively within Ethereum, and one place that users have contact with it is via Ethereum accounts.

Proof of ownership of Externally Owned Accounts (EOAs) is established through private keys and digital signatures. The private keys are used almost everywhere within Ethereum during user interactions, and the Ethereum address of an EOA is derived from the private key. In other words, the Ethereum address is the last 20 bytes of hash of the public key controlling the account with 0x appended in front.

To prove you are the true owner of an EOA, you need to sign a message with the corresponding private key. This means that only you have access to the funds on your account. When making a transaction sending 1 Ether to a contract to mint a new NFT, under the hood, Ethereum verifies the digital signature you created (using the private key) against the corresponding account’s public key hash (the address).

It is similar to going to a bank and asking for a withdrawal of $1,000 from John Doe’s account. The bank needs to verify first that the person asking for the withdrawal is John Doe and not somebody else. Public key cryptography is based on mathematical functions that allow for unique public/private key pairs. Those pairs of keys have special properties, like ease of creation, but it’s extremely hard (nearly impossible) to create a private key from its public key. Having a private key makes it easy to create a public key, but just from knowing a public key, we cannot know which private key was used to create that public key.

One of the most common mathematical ways to compute secure keys is using prime numbers. If we gave you the number 6747437 and told you it was computed using two prime numbers, it would be extremely hard for you to guess which two were used. Calculating the result of multiplying two prime numbers is easy, but doing that in reverse is hard. Of course, we used one of the lower prime numbers from Wikipedia, but if we were to use two large prime numbers, finding them is hard, even for a computer.

As we learned, public key cryptography (also known as asymmetric encryption) is a cryptographic method that uses a key pair system. The one key, called the private key, signs the message. The other key, called the public key, verifies the signature. When we sign any message, whether a transaction on Ethereum or any form of data, we create a digital signature. This is done by hashing the message and running the ECDSA algorithm to combine the hash with the private key, producing a signature. By doing this, any changes to the message will result in a different hash value.

As we can read from the Mastering Ethereum book, “A digital signature can be created to sign any message. For Ethereum transactions, the details of the transaction itself are used as the message. The mathematics of cryptography — in this case, elliptic curve cryptography — provides a way for the message (i.e., the transaction details) to be combined with the private key to create a code that can only be produced with knowledge of the private key. That code is called the digital signature.”

Above is another explanation of digital signatures, but in the context of Ethereum transactions. This explanation introduces us to another, very important subject — elliptic curve cryptography.

Digital Signatures

Smart contracts on Ethereum have access to the built-in ECDSA signature verification algorithm through the system method ecrecover. The built-in function lets you verify the integrity of the signed hashed data and recover the signer’s public key.

It uses V,R,S from ECDSA and the hash of the message. Remember, digital signatures don’t need to only relate to transactions. With a private key, we can sign any arbitrary data. And thanks to ecrecover, we have a way of verifying signatures from within smart contracts! This opens the door to whole new opportunities and also potential pitfalls. Let’s focus on the positive side for now.

One such opportunity with signature verification on Ethereum smart contracts is a way to create meta-transactions. A meta-transaction is a method for separating the person who pays for the gas of a transaction from the person who benefits from the transaction’s execution. A user signs the inner, meta-transaction and then sends it to an operator or something similar — no gas and blockchain interaction required. The operator takes this signed meta-transaction and submits it to the blockchain, paying for the fees of the outer, regular transaction himself.

An example of the above would be ERC20-Permit, standardized as ERC2612. One awkward problem with the standard ERC20 is that it takes a two-step process to allow a smart contract to use a user’s funds. First, we need to create an approve() transaction. We need to wait for the transaction to mine, and after it, we can call transferFrom() from the contract itself to do some operations. One of the main examples with this workflow is the usage of DEX.

When we want to exchange USDC for WETH, we first need to call approve() on the USDC contract to let the DEX trade our USDC. Then, to make the actual exchange, the DEX will call transferFrom() under the hood in the second transaction. We need two transactions to perform one simple action.

With ERC20-Permit’s permit function, you just sign the meta-transaction with your wallet, and someone else (such as DEX or another application) can submit it to the blockchain on your behalf. This would save you gas and eliminate the need for two transactions, as in the previous case. The permit function is designed to make user experience more frictionless and to enable gas-free transactions.

If you want to read a tutorial about ERC20-Permit, we recommend reading the linked blog post on the topic.

Now we’re coming to the first common issue: a valid signature might be used several times in other places where it’s not intended to be used.

Replay Attacks

Imagine a scenario where we have a function that transfers funds, but only when a valid signature is provided.

At first glance, the code looks good. We check the address of the ECDSA signature by providing v,r,s values. We compare the returned address with the owner address, and if it’s an owner, we proceed with the transfer of funds.

The problem with the above code is in the message that is signed by the owner using the ECDSA algorithm. The message only contains the receiver address and the amount to be unlocked. There is nothing in the message that could prevent the same signatures from being used multiple times.

Imagine a scenario where the owner sends 1 Ether to Alice using transferFunds, Alice could re-use the same signature (V,R,S) and send to herself another 1 Ether, or even repeat this many times to drain the contract.

To prevent the signature replay attack, we can store the signature we used in the executed mapping. This way, whenever someone would want to replay our signature, it would fail, as we can check if this signature was already used by simply checking the mapping.

Combining the above results in a code function which looks like this:

If you want to check the full code example, we recommend you check the Solidity-by-example signature replay attack chapter.

There are still issues with the code above. It does not follow the recommended best practice for signature verification, especially the S value.

Signature Malleability

Within Solidity, an ECDSA signature is represented by its r, s and v values. The precise mathematical relationship between the public key, the message hash, r, s, and v can be checked to ensure that only the person who knows the corresponding private key calculates r, s, and v. However, due to the symmetric structure of elliptic curves, for every set of r, s, and v, there is another, easy-to-compute set of r, s, and v that also has the same precise mathematical relationship. This results in TWO valid signatures and violates the idea that only the person with the private key can compute a signature.

Fortunately, it’s easy to detect the duplicate signature.

We’re only interested in one, so we need a way to show which of the two signatures we’re being shown. Like we said, the elliptic curve is symmetric. The v simply indicates which side of the mirror the signature is on. The v can be either 27 (0x1b) or 28 (0x1c). More information on v can be found in the Ethereum Yellow Paper Appendix F.

https://github.com/ethereumbook/ethereumbook/blob/develop/images/simple_elliptic_curve.png

Choosing the proper “half” is important when talking about an s point. As seen above, an Elliptic Curve is symmetric on the X-axis, meaning two points can exist with the same X value. We can carefully adjust s to produce a valid signature for the same r on the other side of the X-axis.

The meaning behind all of this is we can invert one valid signature to get another valid signature which will still be valid and basically replay a signature! There is a way to prevent that, and the first major hard-fork of Ethereum introduced a solution to this: EIP-2.

EIP-2 introduced limits on the s values to prevent signature malleability by only considering lower levels of s as valid. By restricting the valid range in half, EIP-2 effectively removes half the points from the group, ensuring there is at most one valid point at each x coordinate.

Although the EIP-2 was introduced into EVM, it didn’t affect the precompiled contract ecrecover. So, whenever we’re using plain ecrecover, we’re still prone to signature malleability. Don’t worry, though, because OpenZeppelin created an appropriate library (ECDSA.sol) that solves this issue.

The trick is simple: we restrict the s value to be in the lower-end.

Nonces

Another way to combat signature malleability and replay is the usage of an application-level nonce. “Nonce” is cryptographer short-hand for “number used once”. We can use a nonce for every signature and store the next nonce inside the contract.

This covers the most common issues with signatures. But wait, there’s more. If two contracts use the same encoding of messages (maybe there’s a non-fungible token that uses keccak256(abi.encodePacked(_to, _tokenId, nonce[_from])) ), a signature used by one contract might also be valid for the other. So we have to go one step further. We have to hash some identifying information about the contract into our message to make sure that other contracts can’t use the signature.

You may ask yourself (or us): is there some standard to follow or should I, as a Solidity developer, implement everything by hand? No, and thank God for EIP and the standards it introduced to the ecosystem.

To help standardize the signature usage in Ethereum, EIP-712 was introduced and currently is widely used and considered a standard that helps developers avoid most common security issues when dealing with signatures.

EIP-712

EIP-712’s major goal is to ensure that users understand exactly what they’re signing, for which contract address and network, and that each signature can only be used by the intended contract. This is accomplished by signing hashes of the necessary configuration data (address, chain id, version, and data kinds), as well as the actual data.

EIP-712 is a hashing and signature standard for typed structured data rather than just byte strings. It includes a:

  • theoretical framework for correctness of encoding functions,
  • specification of structured data similar to and compatible with Solidity structs,
  • safe hashing algorithm for instances of those structures,
  • safe inclusion of those instances in the set of signable messages,
  • an extensible mechanism for domain separation,
  • new RPC call eth_signTypedData, and
  • an optimized implementation of the hashing algorithm in EVM.

The previously mentioned standard, ERC20-Permit, also relies on EIP-712. We will focus on OpenZeppelin’s implementation to explain how EIP-712 works. The code below contains the most important code snippets from ERC20Permit.sol and EIP712.sol.

Domain Separator: _domainSeparatorV4 This ensures that a signature is only utilized on the proper chain id for our provided token contract address. After the Ethereum Classic fork, which continued to utilize a chain id of 1, the chain id was introduced to precisely identify a network. Because after the hardfork there were contracts deployed to the same address on both networks, the chain id needs to be included to distinguish between them.

Struct Hash: bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));

This structHash ensures that the signature may only be used for the specified purpose, i.e. only for the permit() function, callable only from the owner, approved specified value to the spender. It also adds checks regarding the deadline and checks for nonce so it cannot be replayed.

Final EIP712 Hash: bytes32 hash = _hashTypedDataV4(structHash);

This uses EIP-191 signed data standard to define a version number and version-specific data. 0x19 as set prefix, and 0x01 as version byte to indicate that it’s an EIP-712 signature. Later, we pack together the domain separator and our struct hash. Hash we get is our final hashed message. Later still, we can continue to extract signer addresses. Notice the usage of the ECDSA library to account for all kinds of issues with signatures like signature malleability.

Summary

We hope that this post has given you a better understanding of digital signatures in Ethereum and how to manage them effectively. The need for this article arose from the fact that we are seeing more issues with signature mishandling in various projects. With all of the previous knowledge, you should be able to validate signature usage in the code and uncover a few issues. Remember that this is merely a brief overview of the subject; there is much more to learn.

--

--

Immunefi
Immunefi

Immunefi is the premier bug bounty platform for smart contracts, where hackers review code, disclose vulnerabilities, get paid, and make crypto safer.