diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c939d3b..5f3ccc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,8 @@ jobs: id: build - run: | - forge test -vv - forge coverage --ir-minimum --report lcov + forge test -vv --fork-url "https://base-goerli.publicnode.com" + forge coverage --ir-minimum --report lcov --fork-url "https://base-goerli.publicnode.com" git diff --exit-code id: test diff --git a/README.md b/README.md index 456027b..cf286b0 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,24 @@ The secp256r1 elliptic curve, aka P256, is used by high-quality consumer enclave Available on any chain. If missing, see `deploy.sh`. +Install with: +- `forge install daimo-eth/p256-verifier` +- add `p256-verifier/=lib/p256-verifier/src/` to remappings.txt + ```solidity +import "p256-verifier/P256.sol"; + bytes32 hash; // message hash uint256 r, s; // signature uint256 x, y; // public key -address verifier = 0xc2b78104907F722DABAc4C69f826a522B2754De4; -bytes memory args = abi.encode(hash, r, s, x, y); -(bool success, bytes memory ret) = verifier.staticcall(args); -assert(success); // never reverts, always returns 0 or 1 -bool valid = abi.decode(ret, (uint256)) == 1; +bool valid = P256.verifySignature(hash, r, s, x, y); ``` +Alternately, calling `P256.verifySignatureAllowMalleability` ignores +malleability of signatures, matching the behavior specified by the NIST standard +exactly. + ## Development Run `foundryup` to ensure you have the latest foundry. Then, diff --git a/lcov.info b/lcov.info index 0310320..c295d1f 100644 --- a/lcov.info +++ b/lcov.info @@ -1,4 +1,28 @@ TN: +SF:src/P256.sol +FN:10,P256.verifySignatureAllowMalleability +FNDA:3,P256.verifySignatureAllowMalleability +DA:17,3 +DA:18,3 +DA:19,3 +BRDA:19,0,0,- +BRDA:19,0,1,- +DA:21,3 +FN:28,P256.verifySignature +FNDA:2,P256.verifySignature +DA:36,2 +BRDA:36,1,0,1 +BRDA:36,1,1,1 +DA:37,1 +DA:40,1 +FNF:2 +FNH:2 +LF:7 +LH:7 +BRF:4 +BRH:2 +end_of_record +TN: SF:src/P256Verifier.sol FN:26,P256Verifier. FNDA:2567,P256Verifier. diff --git a/src/P256.sol b/src/P256.sol new file mode 100644 index 0000000..c21231c --- /dev/null +++ b/src/P256.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +/** + * Helper library for external contracts to verify P256 signatures. + **/ +library P256 { + address constant VERIFIER = 0xc2b78104907F722DABAc4C69f826a522B2754De4; + + function verifySignatureAllowMalleability( + bytes32 message_hash, + uint256 r, + uint256 s, + uint256 x, + uint256 y + ) public view returns (bool) { + bytes memory args = abi.encode(message_hash, r, s, x, y); + (bool success, bytes memory ret) = VERIFIER.staticcall(args); + assert(success); // never reverts, always returns 0 or 1 + + return abi.decode(ret, (uint256)) == 1; + } + + /// P256 curve order n/2 for malleability check + uint256 constant P256_N_DIV_2 = + 57896044605178124381348723474703786764998477612067880171211129530534256022184; + + function verifySignature( + bytes32 message_hash, + uint256 r, + uint256 s, + uint256 x, + uint256 y + ) public view returns (bool) { + // check for signature malleability + if (s > P256_N_DIV_2) { + return false; + } + + return verifySignatureAllowMalleability(message_hash, r, s, x, y); + } +} diff --git a/test/P256.t.sol b/test/P256.t.sol new file mode 100644 index 0000000..3617b17 --- /dev/null +++ b/test/P256.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {P256} from "../src/P256.sol"; +import {P256Verifier} from "../src/P256Verifier.sol"; + +contract P256Test is Test { + uint256[2] public pubKey; + + function setUp() public { + pubKey = [ + 0x65a2fa44daad46eab0278703edb6c4dcf5e30b8a9aec09fdc71a56f52aa392e4, + 0x4a7a9e4604aa36898209997288e902ac544a555e4b5e0a9efef2b59233f3f437 + ]; + } + + function testMalleable() public { + // Malleable signature. s is > n/2 + uint256 r = 0x01655c1753db6b61a9717e4ccc5d6c4bf7681623dd54c2d6babc55125756661c; + uint256 s = 0xf073023b6de130f18510af41f64f067c39adccd59f8789a55dbbe822b0ea2317; + + bytes32 hash = 0x267f9ea080b54bbea2443dff8aa543604564329783b6a515c6663a691c555490; + + bool res = P256.verifySignatureAllowMalleability( + hash, + r, + s, + pubKey[0], + pubKey[1] + ); + assertEq(res, true); + + res = P256.verifySignature(hash, r, s, pubKey[0], pubKey[1]); + assertEq(res, false); + } + + function testNonMalleable() public { + // Non-malleable signature. s is <= n/2 + uint256 r = 0x01655c1753db6b61a9717e4ccc5d6c4bf7681623dd54c2d6babc55125756661c; + uint256 s = 7033802732221576339889804108463427183539365869906989872244893535944704590394; + + bytes32 hash = 0x267f9ea080b54bbea2443dff8aa543604564329783b6a515c6663a691c555490; + + bool res = P256.verifySignatureAllowMalleability( + hash, + r, + s, + pubKey[0], + pubKey[1] + ); + assertEq(res, true); + + res = P256.verifySignature(hash, r, s, pubKey[0], pubKey[1]); + assertEq(res, true); + } +}