Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix DoS vulnerability at the commit stage #60

Merged
merged 18 commits into from
Jul 27, 2021
Merged

Conversation

polymorpher
Copy link
Owner

See #59

@polymorpher polymorpher merged commit 1ed4417 into master Jul 27, 2021
@polymorpher polymorpher changed the title Resolve DoS vulnerability at the commit stage of the commit-reveal mechanism Fix DoS vulnerability at the commit stage of the commit-reveal mechanism Jul 27, 2021
@polymorpher polymorpher changed the title Fix DoS vulnerability at the commit stage of the commit-reveal mechanism Fix DoS vulnerability at the commit stage Jul 27, 2021
Copy link
Collaborator

@ivan-homoliak-sutd ivan-homoliak-sutd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security-wise, I do not see any critical issues, so I provide suggestions for optimizations and questions to possible minor issues.

require(c.timestamp > 0, "Invalid commit hash");
function _completeReveal(bytes32 commitHash, uint32 commitIndex) internal {
Commit[] storage cc = commitLocker[commitHash];
require(cc.length > 0, "Invalid commit hash");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the following line will never evaluate to false since the function verifyReveal ensures that returned index is always within the allowed range. The same for timestamp check. Since require is related to the workflow of the code and asserts check internal errors, I'd recommend here to use asserts instead of requires. So, it will not confuse the auditors seeking for the code logic.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree

require(cc.length > commitIndex, "Invalid commitIndex");
Commit storage c = cc[commitIndex];
require(c.timestamp > 0, "Invalid commit timestamp");
// should not happen
uint32 index = uint32(c.timestamp) / interval - t0;
_incrementNonce(index);
_cleanupNonces();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In function _cleanupNonces (4th line), can it happen that (indexMinUnadjusted > t0) is not true?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is not true, indexMin would remain at 0. _cleanupNonces would do nothing, so there is no harm.

uint32 counter = c.timestamp / interval - t0;
require(counter == index, "Index - timestamp mismatch");
uint8 expectedNonce = nonces[counter];
require(nonce >= expectedNonce, "Nonce too low");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you enforce this condition? It might have negative consequences on the use case of the shared wallet that you mentioned before. I'd say that since each nonce corresponds to its own EOTP there is no need for such a constraint. EOTPs aggregated by the Merkle enable selective acknowledgment (see SmartOTPs paper.)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is a remnant of an early version to prevent some types of attacks. It should be modified to require(nonce < maxOperationsPerInterval, "Nonce is invalid");

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My previous comment in this thread is wrong. It would always be true that nonce < maxOperationsPerInterval since nonce is the remainder of modulus maxOperationsPerInterval. I think the only remaining purpose now for nonce is to track how many operations are already performed in the current time interval. The difference this line makes is whether we enforce nonce to be monotonically increasing. Suppose if we remove this line, then clients (possessing proofs) would be free to choose which nonce to use. It would be chaotic, since clients would have no idea what nonces other clients are using. If two clients choose the same nonce at the same time, one of them would fail. So in that situation, if two clients are trying nonces incrementally, it would not really be any better than the current situation enforced by this line: two clients would be forced to query the current nonce of the current block, and one of them would be forced to wait until the other finishes. On the other hand, if two clients coordinate with each other and try different nonces at the same time, then both transactions could go through. But then the client implementation would get complicated - WebSocket or WebRTC would have to be used for coordination, and we would inevitably need a server to broker the communication before the clients connect with each other. In view of that, I don't think we should remove the current constraint imposed by this line, until we do have proper WebSocket or WebRTC implementations to coordinate multiple clients. For now, the user(s) should coordinate manually and let one transaction finishes before the other in the multi-client use case. This approach is better for security reason as well - an attacker with the capability to intercept OTPs and read client side data would be forced to block the commit from the user's client, as otherwise the user's transaction would race with the attacker's. It makers this kind of attack, which is already very difficult to pull off, even easier to detect and harder to succeed.

Commit[] storage cc = commitLocker[commits[i]];
for (uint32 j = 0; j < cc.length; j++) {
Commit storage c = cc[j];
hashes[index] = c.hash;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hash can be removed from the Commit structure. It is never utilized for any verification, so storage can be saved. Moreover, the hash is a key to commitLocker map, so you already have that info.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed over chat and I agree.

for (uint32 j = 0; j < cc.length; j++) {
Commit storage c = cc[j];
hashes[index] = c.hash;
paramHashes[index] = c.paramsHash;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit structure contains paramHash, which should hide the details of the operation and thus ensure privacy. However, this is not met in all the cases, since paramHash might contain a repeated transfer to the same address with the same amount or the operation with a single parameter (recovery), which can be easily determined by the attacker. It can be resolved by adding some random 32B salt into paramHash, generated by the client.

Anyway, I still think that there is no reason for hiding the operation details in commit:

  1. confirmation by OTPs is supposed to be used only for small amounts, so the attacker will not pay for overturning the blockchain to make some double-spending.
  2. if you compare it with the native transfer of ONE, signed by ECDSA, the details of the transfer are leaked as well, so the MITM attacker can eclipse the victim in the same manner as in the case of OTPs.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree in parts. A salt seems reasonable. For the later part regarding reasons to hide operations at commit time, note a distinction between the situation here and that of ECDSA transfer in a single transaction:

  1. There is one block confirmation difference (ECDSA transaction would be executed in the same block, here the reveal only happens in the next block).
  2. Gas is already paid for the commit (which will be deducted from the wallet, as soon as Account Abstraction is implemented) prior to the occurrence of the fund-transfer transaction.

These two factors made it very easy for an opponent (in an application which the user is competing with others) to act in response to the user's intent (consider auctions, gamblings, games, etc.). Previously (or in the ECDSA case) the opponent has to operate a very active mining (validation) node, actively scan and parse all transactions in the current block, and pay a much higher gas if the attacker wants to utilize the revealed information to do anything (such as changing price for an auction, place a slightly higher bid, changing odds in a game the attacker hosts, or make advanced movements in a game, etc.). Now, with the parameters revealed in commit, the attacker can just lay back and examine a block that is already confirmed and index where the commit resides.

require(commits.length < MAX_COMMIT_SIZE, "Too many commits");
commits.push(hash);
commitLocker[hash] = nc;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hash under you store to commitLocker is sufficient when it contains only h(EOTP). The true commitment hash that commits to the parameters using EOTP is stored as the verification hash in commit structure. So, removing the neighbor and indexWithNonce from hash should be made. (Also, the hash field can be dropped from the commit structure - as I already mentioned in other review.)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also discussed over chat, and I agree.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought - I think indexWithNonce should be kept. There may be repetitions in EOTP (especially if double OTP and controlled randomness are not used), though the probability that they occur less than two time intervals apart are very unlikely. The computation for 32 bytes more data in keccak256 is cheap anyway. And for backward compatibility reasons, it doesn't hurt to have 32 more bytes for neighbor as well. Let's keep this as is for now.

@polymorpher polymorpher mentioned this pull request Aug 6, 2021
@polymorpher
Copy link
Owner Author

All comments are addressed in branch https://github.com/polymorpher/one-wallet/tree/v9.0 , except those commented above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants