Puny Obsidian Beaver
Medium
The protocol can sign fake attestations as a result of a hash collision in _keccakForCreateAttestation
resulting in fraudulent identity verification
The attestation system allows attackers to create fraudulent attestations by exploiting a hash collision in the _keccakForCreateAttestation
function. The vulnerability stems from using abi.encodePacked()
with multiple variable-length strings, combined with predictable fixed-length parameters. An attacker can create colliding attestations that reuse valid signatures, effectively bypassing the protocol's identity verification system.
The way the function _keccakForCreateAttestation
is implemented results in a hash collision, i.e
return keccak256(abi.encodePacked(profileId, randValue, account, service, evidence));
where:-
evidence
is a stringservice
is a stringaccount
is a stringrandValue
is an integer which is just the value ofDate.now
profileId
is a well known integer
In EthosAttestations:528 there is a hash collision when abi.encodePacked
is used in conjunction with keccak256
The protocol must have signed at least one valid attestation
N/A
When creating an attestation, the function createAttestation
is called in EthosAttestation.sol.
There is a subsequent signature check, i.e. validateAndSaveSignature
implemented as:
validateAndSaveSignature(
_keccakForCreateAttestation(
profileId,
randValue,
attestationDetails.account,
attestationDetails.service,
evidence
),
signature
);
which is supposed to verify the attestation's signature.
The parameters this function takes is another function _keccakForCreateAttestation
and a signature.
The _keccakForCreateAttestation
looks like below:
function _keccakForCreateAttestation(
uint256 profileId,
uint256 randValue,
string calldata account,
string calldata service,
string calldata evidence
) private pure returns (bytes32) {
return keccak256(abi.encodePacked(profileId, randValue, account, service, evidence));
}
Notice how it returns, i.e
return keccak256(abi.encodePacked(profileId, randValue, account, service, evidence));
From the solidity docs, when abi.encodePacked()
is used with multiple variable-length arguments (such as strings), the packed encoding does not include information about the boundaries between different arguments. This can lead to situations where different combinations of arguments result in the same encoded output, causing hash collisions.
As all the parameters in the _keccakForCreateAttestation
are user specified(conveniently, the randValue
isn't actually random but rather the current timestamp as seen in BlockchainManager.ts#L354 a hash collision arises.
There is no check at all to check for the hash collision in the associated EthosAttestation.ts
/**
* Creates a new attestation and links it to the current sender's profile.
* @param profileId Profile id. Use max uint for non-existing profile.
* @param randValue Random value.
* @param attestationDetails Attestation details with service name and account.
* @param evidence Evidence of attestation.
* @param signature Signature of the attestation.
* @returns Transaction response.
*/
async createAttestation(
profileId: number,
randValue: number,
attestationDetails: { account: string; service: AttestationService },
evidence: string,
signature: string,
): Promise<ContractTransactionResponse> {
return await this.contract.createAttestation(
profileId,
randValue,
attestationDetails,
evidence,
signature,
);
}
Attestations in the Ethos protocol are very important as seen in their whitepaper and this breaks a core invariant of the protocol! Fake user attestations can be signed by the protocol for that matter.
The protocol signs fake user attestations breaking attestation's vital purpose of preventing fraud and impersonation.
From the whitepaper
Lying about your identity, however, is the most expensive action you can take on Ethos. Fraudulent attestation warrants Social Validation and Slashing (see: Vouch), which costs both reputation and staked Ethereum.
- Obtain a valid attestation onchain. Lets take this transaction for example where
profileId: 978,
randValue: 1729872420831,
account: "257724631",
service: "x.com",
evidence: "https://x.com/inmarelibero/status/1849845065037795662",
signature: "0xe323e3bb04be25ef4b40680624b2f07a9ff1f302d20566a3d4c7f5cfd669747866e6a40321c94782c41e98c3988e1fe63ea19e0184420580bb790ea9117f029c1b"
- Create colliding parameters that produce the same hash:
profileId: 978,
randValue: 1729872420831,
account: "257724631x.com",
service: "",
evidence: "https://x.com/inmarelibero/status/1849845065037795662"
- Submit the colliding attestation using the original signature:
await ethosAttestations.createAttestation(
978,
1729872420831,
{
account: "257724631x.com",
service: ""
},
"https://x.com/inmarelibero/status/1849845065037795662",
"0xe323e3bb04be25ef4b40680624b2f07a9ff1f302d20566a3d4c7f5cfd669747866e6a40321c94782c41e98c3988e1fe63ea19e0184420580bb790ea9117f029c1b"
);
The transaction succeeds because:
- The hash matches the original due to collision
- The signature is valid for this hash
- The signature hasn't been used before
- Avoid using
abi.encodePacked()
, instead useabi.encode()
. Unlikeabi.encodePacked()
,abi.encode()
includes additional type information and length prefixes in the encoding, making it much less prone to hash collisions. - Use a truly random number and not just
Date.now
. Consider a Verifiable Random Function(VRF) from Chainlink.