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

Add support for EIP2098 "short signatures" in the ECDSA library #2582

Merged
merged 15 commits into from
Apr 9, 2021
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* `IERC20Metadata`: add a new extended interface that includes the optional `name()`, `symbol()` and `decimals()` functions. ([#2561](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2561))
* `ERC777`: make reception acquirement optional in `_mint`. ([#2552](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2552))
* `ERC20Permit`: add a `_useNonce` to enable further usage of ERC712 signatures. ([#2565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2565))
* `ECDSA`: add support for ERC2098 short-signatures. ([#2582](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2582))

## 4.0.0 (2021-03-23)

Expand Down
36 changes: 24 additions & 12 deletions contracts/utils/cryptography/ECDSA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,35 @@ library ECDSA {
* be too long), and then calling {toEthSignedMessageHash} on it.
*/
function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
// Check the signature length
if (signature.length != 65) {
revert("ECDSA: invalid signature length");
}

// Divide the signature in r, s and v variables
bytes32 r;
bytes32 s;
uint8 v;

// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
// solhint-disable-next-line no-inline-assembly
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
// Check the signature length
// - case 65: r,s,v signature (standard)
// - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098)
if (signature.length == 65) {
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
// solhint-disable-next-line no-inline-assembly
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
} else if (signature.length == 64) {
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
// solhint-disable-next-line no-inline-assembly
assembly {
let vs := mload(add(signature, 0x40))
r := mload(add(signature, 0x20))
s := and(vs, 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
v := add(shr(7, byte(0, vs)), 27)
Amxx marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
revert("ECDSA: invalid signature length");
}

return recover(hash, v, r, s);
Expand Down
34 changes: 34 additions & 0 deletions test/utils/cryptography/ECDSA.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ const ECDSAMock = artifacts.require('ECDSAMock');
const TEST_MESSAGE = web3.utils.sha3('OpenZeppelin');
const WRONG_MESSAGE = web3.utils.sha3('Nope');

function to2098Format (signature) {
const long = web3.utils.hexToBytes(signature);
expect(long.length).to.be.equal(65);
const short = long.slice(0, 64);
short[32] |= (long[64] % 27) << 7;
Amxx marked this conversation as resolved.
Show resolved Hide resolved
return web3.utils.bytesToHex(short);
}

function from2098Format (signature) {
const short = web3.utils.hexToBytes(signature);
expect(short.length).to.be.equal(64);
short.push((short[32] >> 7) + 27);
short[32] &= (1 << 7) - 1;
Amxx marked this conversation as resolved.
Show resolved Hide resolved
return web3.utils.bytesToHex(short);
}

contract('ECDSA', function (accounts) {
const [ other ] = accounts;

Expand Down Expand Up @@ -61,6 +77,15 @@ contract('ECDSA', function (accounts) {
await expectRevert(this.ecdsa.recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature \'v\' value');
});
});

context('short 2098 format', function () {
Amxx marked this conversation as resolved.
Show resolved Hide resolved
it('works', async function () {
frangio marked this conversation as resolved.
Show resolved Hide resolved
const version = '1b'; // 27 = 1b.
const signature = signatureWithoutVersion + version;
expect(await this.ecdsa.recover(TEST_MESSAGE, to2098Format(signature))).to.equal(signer);
expect(await this.ecdsa.recover(TEST_MESSAGE, from2098Format(to2098Format(signature)))).to.equal(signer);
});
});
});

context('with v1 signature', function () {
Expand Down Expand Up @@ -93,6 +118,15 @@ contract('ECDSA', function (accounts) {
await expectRevert(this.ecdsa.recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature \'v\' value');
});
});

context('short 2098 format', function () {
Amxx marked this conversation as resolved.
Show resolved Hide resolved
it('works', async function () {
const version = '1c'; // 27 = 1b.
const signature = signatureWithoutVersion + version;
expect(await this.ecdsa.recover(TEST_MESSAGE, to2098Format(signature))).to.equal(signer);
expect(await this.ecdsa.recover(TEST_MESSAGE, from2098Format(to2098Format(signature)))).to.equal(signer);
});
});
});

context('with high-s value signature', function () {
Expand Down