Discovering Signature Verification Bugs in Ethereum Smart Contracts
Reentrancy or Integer Overflows are security issues in smart contracts that most developers are aware of or at least have heard about. Attacks on cryptographic signature implementations on the other hand do not immediately come to mind when you think about smart contract security. They are typically associated with network protocols. For example signature replay attacks, a malicious user eavesdrops a protocol sequence containing a valid signature and replays it against a target to gain an advantage.
This article will explain two types of weaknesses that can occur in smart contracts which process DApp-generated signatures. We will be looking into concrete real world examples from audits that were done by the Diligence team earlier this year. Further we will discuss how smart contracts can be designed to avoid these kinds vulnerabilities.
The Protocol Layer
Signatures are essential cryptographic building blocks in the Ethereum network. Every transaction that is sent to the network must have a valid signature. The figure below shows an example for such a transaction. Besides the transaction standard properties such as from, to, gas, value or input that are available in the global namespace and frequently found in smart contract code, the fields v,r and s make up the transaction signature.
The Ethereum network ensures that only transactions with valid signatures are included in new blocks. This provides the following security properties for transactions:
- Authentication: Ethereum nodes use the signature to verify that the transaction signer owns the private key associated with their public address. Developers can therefore trust that msg.sender is authentic.
- Integrity: The transaction was not altered after it has been signed otherwise the signature is invalid.
- Non-repudiation: It can’t be disputed that a transaction is signed by the private key belonging to the public address in the from field and that any state changes have been made by the signing party in possession of the private key.
The Contract Layer
The protocol layer is not the only place where signatures come into play. More and more they are used in smart contracts themselves. With increasing gas prices and scaling solutions still a work in progress it becomes increasingly important to avoid on-chain transactions. Signatures are very useful when it comes to doing things off-chain. EIP-191 and EIP-712 both specify how to handle signed data in smart contracts. The latter aims to improve the usability of off-chain message signing for use on-chain and a scheme to encode data along with its structure. So why is this useful and how does it save on-chain transactions?
Let’s look at a simple example. Alice creates a proposition for Bob that she encodes into a message. She also creates a signature for the message with her private key and sends it to Bob through a channel of their choosing. Bob can verify that Alice signed the message and further on if Bob finds the proposition agreeable he can create a new transaction, include his own message, Alice’s message and signature and send it to a smart contract. With the data the contract can verify that:
- Bob has signed his own message (or transaction in this case to be more specific). Authentication, integrity and non-repudiation are ensured by the network.
- Alice has signed the message that Bob sent along in his own message. The smart contract needs to implement the business logic to ensure authentication, integrity and non-repudiation.
The whole process only requires a single on-chain transaction which can provide a significantly better user experience and save gas. The caveat is that the smart contract needs to ensure that all three security properties are kept intact for the message that Alice sent to Bob.
Let’s analyze two real world examples of signature verification bugs and how they were fixed.
Missing Protection against Signature Replay Attacks (SWC-121)
The first example was discovered in an audit conducted by Consensys Diligence for Civil, a decentralized marketplace for sustainable journalism. The part of the system that is relevant for this example is called the Newsroom. Content editors can publish articles into the Newsroom and they can also cryptographically sign the content creation, proofing that the content is in fact created by them. The pushRevision() function handles updates or revisions to existing content. The parameters content hash, content URI, timestamp and signature create a new revision for a piece of content. Subsequently verifiyRevisionSignature() is called with the proposed revision and the content author that initially created the first signed revision. According to the design a new revision can only be signed by the author who created the initially signed content version.
verifiyRevisionSignature() creates a signed message hash from the content hash that the DApp generated and the address of the Newsroom contract. Then recover() is called which is a function within the ECRecovery library from OpenZeppelin. Subsequently ecrecover() is called and it’s verified if the author actually signed the message. There is no problem with the code of two functions that have been discussed, it’s rather what’s not there that becomes a security issue for the requirement that only authors who initially created content can also create new revisions for it. The contract does not keep track of content hashes. So a content hash and its user signature that have been submitted once can be submitted multiple times. A malicious content author could leverage this issue, take valid signatures and content hashes from another author and create new valid revisions for them without their knowledge.
Civil has fixed the issue by tracking content hashes and is rejecting hashes that are already part of a previous revision.
Lack of Proper Signature Verification (SWC-122)
An instance of this weakness type was discovered in our last audit for 0x. The following explanation is a summary of the issue described in 3.2 of the audit report. The 0x protocol has various signature validators for different signature types including web3 and EIP712. Another validator that exists is called SignatureType.Caller that allows an order to be valid if order.makerAddress equals msg.sender (order.makerAddress is the user that creates an order). If the SignatureType.Caller is set, no actual signature verification is performed for the order by the Exchange contract. It might not be immediately clear why this can lead to a vulnerability since it’s verified that msg.sender and the creator of the order are one and the same, at least that is the assumption.
Besides the Exchange contract there is another part of the 0x system called the Forwarder. With this contract, users can simply send ETH and the orders they want to fill and the Forwarder contract executes all orders in a single transaction.
Users who want to trade ETH for tokens can send along orders from other users and the Forwarder will make the trades on their behalf. The Exchange contract verifies every single order to ensure that the signatures for the orders are valid and that the other users have actually signed them. Let’s look at the above figure again and reevaluate the assumption that if order.makerAddress equals msg.sender we don’t need to do a proper signature verification in the Exchange contract because the user that sends the transaction is also the maker of the order. If a user sends an order directly to the Exchange contract then that the assumption holds. But what if I send the order through the Forwarder, set order.makerAddress to the Forwarder contract’s address and use the SignatureType.Caller signature validator?
The Forwarder calls into the Exchange contract during trade execution to settle individual orders. The Exchange contract verifies each time that the address in order.makerAddress is msg.sender which in this case can be set to the Forwarder address. The order.takerAddress is typically set to the Forwarder address since the contract acts as intermediary between trading parties. So a malicious user can make the Forwarder process orders where the contract trades with itself as it is both the taker and the maker. This works for the following reasons:
- There is no logic in Forwarder that prevents the contract from being a maker for an order.
- The ERC20 specs for transferFrom(address _from, address _to, uint256 _value) do not prevent users from doing “empty transfers”. _from and _to can be the same address.
- The Exchange contract allows an order to be processed based on the fact that the msg.sender has sent it not if the user has actually signed it.
The Forwarder will end up with exactly the same balance after the Exchange contract has settled the order and the Forwarder transfers the takerAmount to itself and makerAmount to a malicious user who could have used this scenario to create a “very favourable order” to obtain all ZRX tokens from the Forwarder in exchange for 1 Wei.
To recap, assuming that the sender of a message is also its creator without verifying its signature can be unsafe, in particular if transactions are forwarded through proxies. Proper signature recovery and validation needs to be performed at anytime when contracts process signed messages. 0x promptly fixed the issue by removing the signature validator SignatureType.Caller.
TL;DR
Off-chain message signing is certainly a good way to save on gas and improve user experience. From a security perspective though it definitely adds complexity and makes processing signed messages in smart contracts safely a more challenging task. If you are interested in more examples around signature based attacks or other smart contract weaknesses make sure to check out the SWC-registry Github repo and pages, it has a large collection of vulnerable samples and real world contracts along with more information on the Smart Contract Weakness Classification (SWC) scheme that we have been working on with the community. In case you want to learn more about the SWC or have ideas on how we can make it even better, then come and join the discussion on EIP-1470 at ethereum/EIPs and the Ethereum Magicians.
Also feel free to drop by our Discord 👋 and let us know which vulnerabilities you always wanted to have detected by security analysis tools 🙏 … but don’t say all 😂.
—
Disclaimer: The views expressed by the author above do not necessarily represent the views of Consensys AG. ConsenSys is a decentralized community with ConsenSys Media being a platform for members to freely express their diverse ideas and perspectives.