Implement P256 verification via RIP-7212 precompile with Solidity fallback (#4881)
Co-authored-by: Ernesto García <ernestognw@gmail.com> Co-authored-by: cairo <cairoeth@protonmail.com> Co-authored-by: sudo rm -rf --no-preserve-root / <pcaversaccio@users.noreply.github.com>
This commit is contained in:
135
test/utils/cryptography/P256.t.sol
Normal file
135
test/utils/cryptography/P256.t.sol
Normal file
@ -0,0 +1,135 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
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";
|
||||
|
||||
contract P256Test is Test {
|
||||
/// forge-config: default.fuzz.runs = 512
|
||||
function testVerify(uint256 seed, bytes32 digest) public {
|
||||
uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.N - 1);
|
||||
|
||||
(bytes32 x, bytes32 y) = P256PublicKey.getPublicKey(privateKey);
|
||||
(bytes32 r, bytes32 s) = vm.signP256(privateKey, digest);
|
||||
s = _ensureLowerS(s);
|
||||
assertTrue(P256.verify(digest, r, s, x, y));
|
||||
assertTrue(P256.verifySolidity(digest, r, s, x, y));
|
||||
}
|
||||
|
||||
/// forge-config: default.fuzz.runs = 512
|
||||
function testRecover(uint256 seed, bytes32 digest) public {
|
||||
uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.N - 1);
|
||||
|
||||
(bytes32 x, bytes32 y) = P256PublicKey.getPublicKey(privateKey);
|
||||
(bytes32 r, bytes32 s) = vm.signP256(privateKey, digest);
|
||||
s = _ensureLowerS(s);
|
||||
(bytes32 qx0, bytes32 qy0) = P256.recovery(digest, 0, r, s);
|
||||
(bytes32 qx1, bytes32 qy1) = P256.recovery(digest, 1, r, s);
|
||||
assertTrue((qx0 == x && qy0 == y) || (qx1 == x && qy1 == y));
|
||||
}
|
||||
|
||||
function _ensureLowerS(bytes32 s) private pure returns (bytes32) {
|
||||
uint256 _s = uint256(s);
|
||||
unchecked {
|
||||
return _s > P256.N / 2 ? bytes32(P256.N - _s) : s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Library to derive P256 public key from private key
|
||||
* Should be removed if Foundry adds this functionality
|
||||
* See https://github.com/foundry-rs/foundry/issues/7908
|
||||
*/
|
||||
library P256PublicKey {
|
||||
function getPublicKey(uint256 privateKey) internal view returns (bytes32, bytes32) {
|
||||
(uint256 x, uint256 y, uint256 z) = _jMult(P256.GX, P256.GY, 1, privateKey);
|
||||
return _affineFromJacobian(x, y, z);
|
||||
}
|
||||
|
||||
function _jMult(
|
||||
uint256 x,
|
||||
uint256 y,
|
||||
uint256 z,
|
||||
uint256 k
|
||||
) private pure returns (uint256 rx, uint256 ry, uint256 rz) {
|
||||
unchecked {
|
||||
for (uint256 i = 0; i < 256; ++i) {
|
||||
if (rz > 0) {
|
||||
(rx, ry, rz) = _jDouble(rx, ry, rz);
|
||||
}
|
||||
if (k >> 255 > 0) {
|
||||
if (rz == 0) {
|
||||
(rx, ry, rz) = (x, y, z);
|
||||
} else {
|
||||
(rx, ry, rz) = _jAdd(rx, ry, rz, x, y, z);
|
||||
}
|
||||
}
|
||||
k <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// From P256.sol
|
||||
|
||||
function _affineFromJacobian(uint256 jx, uint256 jy, uint256 jz) private view returns (bytes32 ax, bytes32 ay) {
|
||||
if (jz == 0) return (0, 0);
|
||||
uint256 zinv = Math.invModPrime(jz, P256.P);
|
||||
uint256 zzinv = mulmod(zinv, zinv, P256.P);
|
||||
uint256 zzzinv = mulmod(zzinv, zinv, P256.P);
|
||||
ax = bytes32(mulmod(jx, zzinv, P256.P));
|
||||
ay = bytes32(mulmod(jy, zzzinv, P256.P));
|
||||
}
|
||||
|
||||
function _jDouble(uint256 x, uint256 y, uint256 z) private pure returns (uint256 rx, uint256 ry, uint256 rz) {
|
||||
uint256 p = P256.P;
|
||||
uint256 a = P256.A;
|
||||
assembly ("memory-safe") {
|
||||
let yy := mulmod(y, y, p)
|
||||
let zz := mulmod(z, z, p)
|
||||
let s := mulmod(4, mulmod(x, yy, p), p) // s = 4*x*y²
|
||||
let m := addmod(mulmod(3, mulmod(x, x, p), p), mulmod(a, mulmod(zz, zz, p), p), p) // m = 3*x²+a*z⁴
|
||||
let t := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) // t = m²-2*s
|
||||
|
||||
// x' = t
|
||||
rx := t
|
||||
// y' = m*(s-t)-8*y⁴
|
||||
ry := addmod(mulmod(m, addmod(s, sub(p, t), p), p), sub(p, mulmod(8, mulmod(yy, yy, p), p)), p)
|
||||
// z' = 2*y*z
|
||||
rz := mulmod(2, mulmod(y, z, p), p)
|
||||
}
|
||||
}
|
||||
|
||||
function _jAdd(
|
||||
uint256 x1,
|
||||
uint256 y1,
|
||||
uint256 z1,
|
||||
uint256 x2,
|
||||
uint256 y2,
|
||||
uint256 z2
|
||||
) private pure returns (uint256 rx, uint256 ry, uint256 rz) {
|
||||
uint256 p = P256.P;
|
||||
assembly ("memory-safe") {
|
||||
let zz1 := mulmod(z1, z1, p) // zz1 = z1²
|
||||
let zz2 := mulmod(z2, z2, p) // zz2 = z2²
|
||||
let u1 := mulmod(x1, zz2, p) // u1 = x1*z2²
|
||||
let u2 := mulmod(x2, zz1, p) // u2 = x2*z1²
|
||||
let s1 := mulmod(y1, mulmod(zz2, z2, p), p) // s1 = y1*z2³
|
||||
let s2 := mulmod(y2, mulmod(zz1, z1, p), p) // s2 = y2*z1³
|
||||
let h := addmod(u2, sub(p, u1), p) // h = u2-u1
|
||||
let hh := mulmod(h, h, p) // h²
|
||||
let hhh := mulmod(h, hh, p) // h³
|
||||
let r := addmod(s2, sub(p, s1), p) // r = s2-s1
|
||||
|
||||
// x' = r²-h³-2*u1*h²
|
||||
rx := addmod(addmod(mulmod(r, r, p), sub(p, hhh), p), sub(p, mulmod(2, mulmod(u1, hh, p), p)), p)
|
||||
// y' = r*(u1*h²-x')-s1*h³
|
||||
ry := addmod(mulmod(r, addmod(mulmod(u1, hh, p), sub(p, rx), p), p), sub(p, mulmod(s1, hhh, p)), p)
|
||||
// z' = h*z1*z2
|
||||
rz := mulmod(h, mulmod(z1, z2, p), p)
|
||||
}
|
||||
}
|
||||
}
|
||||
156
test/utils/cryptography/P256.test.js
Normal file
156
test/utils/cryptography/P256.test.js
Normal file
@ -0,0 +1,156 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { secp256r1 } = require('@noble/curves/p256');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
||||
|
||||
// As in ECDSA, signatures are malleable and the tooling produce both high and low S values.
|
||||
// We need to ensure that the s value is in the lower half of the order of the curve.
|
||||
const ensureLowerOrderS = ({ s, recovery, ...rest }) => {
|
||||
if (s > N / 2n) {
|
||||
s = N - s;
|
||||
recovery = 1 - recovery;
|
||||
}
|
||||
return { s, recovery, ...rest };
|
||||
};
|
||||
|
||||
const prepareSignature = (
|
||||
privateKey = secp256r1.utils.randomPrivateKey(),
|
||||
messageHash = ethers.hexlify(ethers.randomBytes(0x20)),
|
||||
) => {
|
||||
const publicKey = [
|
||||
secp256r1.getPublicKey(privateKey, false).slice(0x01, 0x21),
|
||||
secp256r1.getPublicKey(privateKey, false).slice(0x21, 0x41),
|
||||
].map(ethers.hexlify);
|
||||
const { r, s, recovery } = ensureLowerOrderS(secp256r1.sign(messageHash.replace(/0x/, ''), privateKey));
|
||||
const signature = [r, s].map(v => ethers.toBeHex(v, 0x20));
|
||||
|
||||
return { privateKey, publicKey, signature, recovery, messageHash };
|
||||
};
|
||||
|
||||
describe('P256', function () {
|
||||
async function fixture() {
|
||||
return { mock: await ethers.deployContract('$P256') };
|
||||
}
|
||||
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
describe('with signature', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, prepareSignature());
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
it('recover public key', async function () {
|
||||
expect(await this.mock.$recovery(this.messageHash, this.recovery, ...this.signature)).to.deep.equal(
|
||||
this.publicKey,
|
||||
);
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
it('reject signature with flipped signature values ([r,s] >> [s,r])', async function () {
|
||||
// Preselected signature where `r < N/2` and `s < N/2`
|
||||
this.signature = [
|
||||
'0x45350225bad31e89db662fcc4fb2f79f349adbb952b3f652eed1f2aa72fb0356',
|
||||
'0x513eb68424c42630012309eee4a3b43e0bdc019d179ef0e0c461800845e237ee',
|
||||
];
|
||||
|
||||
// Corresponding hash and public key
|
||||
this.messageHash = '0x2ad1f900fe63745deeaedfdf396cb6f0f991c4338a9edf114d52f7d1812040a0';
|
||||
this.publicKey = [
|
||||
'0x9e30de165e521257996425d9bf12a7d366925614bf204eabbb78172b48e52e59',
|
||||
'0x94bf0fe72f99654d7beae4780a520848e306d46a1275b965c4f4c2b8e9a2c08d',
|
||||
];
|
||||
|
||||
// Make sure it works
|
||||
expect(await this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.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,
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// test cases for https://github.com/C2SP/wycheproof/blob/4672ff74d68766e7785c2cac4c597effccef2c5c/testvectors/ecdsa_secp256r1_sha256_p1363_test.json
|
||||
describe('wycheproof tests', function () {
|
||||
for (const { key, tests } of require('./ecdsa_secp256r1_sha256_p1363_test.json').testGroups) {
|
||||
// parse public key
|
||||
let [x, y] = [key.wx, key.wy].map(v => ethers.stripZerosLeft('0x' + v, 32));
|
||||
if (x.length > 66 || y.length > 66) continue;
|
||||
x = ethers.zeroPadValue(x, 32);
|
||||
y = ethers.zeroPadValue(y, 32);
|
||||
|
||||
// run all tests for this key
|
||||
for (const { tcId, comment, msg, sig, result } of tests) {
|
||||
// only keep properly formatted signatures
|
||||
if (sig.length != 128) continue;
|
||||
|
||||
it(`${tcId}: ${comment}`, async function () {
|
||||
// split signature, and reduce modulo N
|
||||
let [r, s] = Array(2)
|
||||
.fill()
|
||||
.map((_, i) => ethers.toBigInt('0x' + sig.substring(64 * i, 64 * (i + 1))));
|
||||
// move s to lower part of the curve if needed
|
||||
if (s <= N && s > N / 2n) s = N - s;
|
||||
// prepare signature
|
||||
r = ethers.toBeHex(r, 32);
|
||||
s = ethers.toBeHex(s, 32);
|
||||
// hash
|
||||
const messageHash = ethers.sha256('0x' + msg);
|
||||
|
||||
// check verify
|
||||
expect(await this.mock.$verify(messageHash, r, s, x, y)).to.equal(result == 'valid');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
3719
test/utils/cryptography/ecdsa_secp256r1_sha256_p1363_test.json
Normal file
3719
test/utils/cryptography/ecdsa_secp256r1_sha256_p1363_test.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user