Fix issue with detection of RIP7212 precompile (#5620)

Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com>
Co-authored-by: Ernesto García <ernestognw@gmail.com>
This commit is contained in:
Hadrien Croubois
2025-04-03 17:02:04 +02:00
committed by GitHub
parent 450b833278
commit aa29301672
7 changed files with 249 additions and 164 deletions

View File

@ -6,6 +6,7 @@ import {Test} from "forge-std/Test.sol";
import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {Errors} from "@openzeppelin/contracts/utils/Errors.sol";
contract P256Test is Test {
/// forge-config: default.fuzz.runs = 512
@ -31,6 +32,22 @@ contract P256Test is Test {
assertTrue((qx0 == bytes32(x) && qy0 == bytes32(y)) || (qx1 == bytes32(x) && qy1 == bytes32(y)));
}
function testVerifyNativeUnsupportedRIP7212(bytes32 digest, uint256 seed) public {
// By default, the precompile at address 0x100 is not supported.
uint256 privateKey = _asPrivateKey(seed);
(uint256 x, uint256 y) = vm.publicKeyP256(privateKey);
(bytes32 r, bytes32 s) = vm.signP256(privateKey, digest);
s = _ensureLowerS(s);
(bool success, bytes memory returndata) = address(this).call(
abi.encodeCall(P256Test.verifyNative, (digest, r, s, bytes32(x), bytes32(y)))
);
assertFalse(success);
assertEq(returndata, abi.encodeWithSelector(Errors.MissingPrecompile.selector, address(0x100)));
}
function _asPrivateKey(uint256 seed) private pure returns (uint256) {
return bound(seed, 1, P256.N - 1);
}
@ -41,4 +58,9 @@ contract P256Test is Test {
return _s > P256.N / 2 ? bytes32(P256.N - _s) : s;
}
}
// See https://github.com/foundry-rs/foundry/issues/10237
function verifyNative(bytes32 digest, bytes32 r, bytes32 s, bytes32 x, bytes32 y) external view {
P256.verifyNative(digest, r, s, x, y);
}
}

View File

@ -44,26 +44,52 @@ describe('P256', function () {
});
it('verify valid signature', async function () {
expect(await this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.be.true;
expect(await this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.be.true;
await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey))
.to.be.revertedWithCustomError(this.mock, 'MissingPrecompile')
.withArgs('0x0000000000000000000000000000000000000100');
await expect(this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be.true;
await expect(this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
.true;
await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
.true;
});
it('verify improper signature', async function () {
const signature = this.signature;
this.signature[0] = ethers.toBeHex(N, 0x20); // r = N
await expect(this.mock.$verify(this.messageHash, ...signature, ...this.publicKey)).to.eventually.be.false;
await expect(this.mock.$verifySolidity(this.messageHash, ...signature, ...this.publicKey)).to.eventually.be.false;
await expect(this.mock.$verifyNative(this.messageHash, ...signature, ...this.publicKey)).to.eventually.be.false;
});
it('recover public key', async function () {
expect(await this.mock.$recovery(this.messageHash, this.recovery, ...this.signature)).to.deep.equal(
await expect(this.mock.$recovery(this.messageHash, this.recovery, ...this.signature)).to.eventually.deep.equal(
this.publicKey,
);
});
it('recovers (0,0) for invalid recovery bit', async function () {
await expect(this.mock.$recovery(this.messageHash, 2, ...this.signature)).to.eventually.deep.equal([
ethers.ZeroHash,
ethers.ZeroHash,
]);
});
it('recovers (0,0) for improper signature', async function () {
const signature = this.signature;
this.signature[0] = ethers.toBeHex(N, 0x20); // r = N
await expect(this.mock.$recovery(this.messageHash, this.recovery, ...signature)).to.eventually.deep.equal([
ethers.ZeroHash,
ethers.ZeroHash,
]);
});
it('reject signature with flipped public key coordinates ([x,y] >> [y,x])', async function () {
// flip public key
this.publicKey.reverse();
expect(await this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false;
expect(await this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false;
expect(await this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false; // Flipped public key is not in the curve
await expect(this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be.false;
await expect(this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
.false;
await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
.false;
});
it('reject signature with flipped signature values ([r,s] >> [s,r])', async function () {
@ -81,42 +107,42 @@ describe('P256', function () {
];
// Make sure it works
expect(await this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.be.true;
await expect(this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be.true;
// Flip signature
this.signature.reverse();
expect(await this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false;
expect(await this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false;
await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey))
.to.be.revertedWithCustomError(this.mock, 'MissingPrecompile')
.withArgs('0x0000000000000000000000000000000000000100');
expect(await this.mock.$recovery(this.messageHash, this.recovery, ...this.signature)).to.not.deep.equal(
this.publicKey,
);
await expect(this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be.false;
await expect(this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
.false;
await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
.false;
await expect(
this.mock.$recovery(this.messageHash, this.recovery, ...this.signature),
).to.eventually.not.deep.equal(this.publicKey);
});
it('reject signature with invalid message hash', async function () {
// random message hash
this.messageHash = ethers.hexlify(ethers.randomBytes(32));
expect(await this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false;
expect(await this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false;
await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey))
.to.be.revertedWithCustomError(this.mock, 'MissingPrecompile')
.withArgs('0x0000000000000000000000000000000000000100');
expect(await this.mock.$recovery(this.messageHash, this.recovery, ...this.signature)).to.not.deep.equal(
this.publicKey,
);
await expect(this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be.false;
await expect(this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
.false;
await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
.false;
await expect(
this.mock.$recovery(this.messageHash, this.recovery, ...this.signature),
).to.eventually.not.deep.equal(this.publicKey);
});
it('fail to recover signature with invalid recovery bit', async function () {
// flip recovery bit
this.recovery = 1 - this.recovery;
expect(await this.mock.$recovery(this.messageHash, this.recovery, ...this.signature)).to.not.deep.equal(
this.publicKey,
);
await expect(
this.mock.$recovery(this.messageHash, this.recovery, ...this.signature),
).to.eventually.not.deep.equal(this.publicKey);
});
});
@ -148,7 +174,7 @@ describe('P256', function () {
const messageHash = ethers.sha256('0x' + msg);
// check verify
expect(await this.mock.$verify(messageHash, r, s, x, y)).to.equal(result == 'valid');
await expect(this.mock.$verify(messageHash, r, s, x, y)).to.eventually.equal(result == 'valid');
});
}
}