diff --git a/script/SignatureChecker.s.sol b/script/SignatureChecker.s.sol index bac8bde..c3556ce 100644 --- a/script/SignatureChecker.s.sol +++ b/script/SignatureChecker.s.sol @@ -12,7 +12,7 @@ contract SignatureCheckerScript is Script { function run() public { vm.startBroadcast(); - signatureChecker = new SignatureChecker(); + signatureChecker = new SignatureChecker(0xfdf07A5dCfa7b74f4c28DAb23eaD8B1c43Be801F); vm.stopBroadcast(); } diff --git a/src/ECDSA.sol b/src/ECDSA.sol deleted file mode 100644 index 175db73..0000000 --- a/src/ECDSA.sol +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/ECDSA.sol) - -pragma solidity ^0.8.20; - -/** - * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. - * - * These functions can be used to verify that a message was signed by the holder - * of the private keys of a given address. - */ -library ECDSA { - enum RecoverError { - NoError, - InvalidSignature, - InvalidSignatureLength, - InvalidSignatureS - } - - /** - * @dev The signature derives the `address(0)`. - */ - error ECDSAInvalidSignature(); - - /** - * @dev The signature has an invalid length. - */ - error ECDSAInvalidSignatureLength(uint256 length); - - /** - * @dev The signature has an S value that is in the upper half order. - */ - error ECDSAInvalidSignatureS(bytes32 s); - - /** - * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not - * return address(0) without also returning an error description. Errors are documented using an enum (error type) - * and a bytes32 providing additional information about the error. - * - * If no error is returned, then the address can be used for verification purposes. - * - * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: - * this function rejects them by requiring the `s` value to be in the lower - * half order, and the `v` value to be either 27 or 28. - * - * IMPORTANT: `hash` _must_ be the result of a hash operation for the - * verification to be secure: it is possible to craft signatures that - * recover to arbitrary addresses for non-hashed data. A safe way to ensure - * this is by receiving a hash of the original message (which may otherwise - * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. - * - * Documentation for signature generation: - * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] - * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] - */ - function tryRecover(bytes32 hash, bytes memory signature) - internal - pure - returns (address recovered, RecoverError err, bytes32 errArg) - { - if (signature.length == 65) { - bytes32 r; - bytes32 s; - uint8 v; - // ecrecover takes the signature parameters, and the only way to get them - // currently is to use assembly. - assembly ("memory-safe") { - r := mload(add(signature, 0x20)) - s := mload(add(signature, 0x40)) - v := byte(0, mload(add(signature, 0x60))) - } - return tryRecover(hash, v, r, s); - } else { - return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length)); - } - } - - /** - * @dev Returns the address that signed a hashed message (`hash`) with - * `signature`. This address can then be used for verification purposes. - * - * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: - * this function rejects them by requiring the `s` value to be in the lower - * half order, and the `v` value to be either 27 or 28. - * - * IMPORTANT: `hash` _must_ be the result of a hash operation for the - * verification to be secure: it is possible to craft signatures that - * recover to arbitrary addresses for non-hashed data. A safe way to ensure - * this is by receiving a hash of the original message (which may otherwise - * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. - */ - function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { - (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature); - _throwError(error, errorArg); - return recovered; - } - - /** - * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. - * - * See https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signatures] - */ - function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) - internal - pure - returns (address recovered, RecoverError err, bytes32 errArg) - { - unchecked { - bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); - // We do not check for an overflow here since the shift operation results in 0 or 1. - uint8 v = uint8((uint256(vs) >> 255) + 27); - return tryRecover(hash, v, r, s); - } - } - - /** - * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. - */ - function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { - (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs); - _throwError(error, errorArg); - return recovered; - } - - /** - * @dev Overload of {ECDSA-tryRecover} that receives the `v`, - * `r` and `s` signature fields separately. - */ - function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) - internal - pure - returns (address recovered, RecoverError err, bytes32 errArg) - { - // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature - // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines - // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most - // signatures from current libraries generate a unique signature with an s-value in the lower half order. - // - // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value - // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or - // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept - // these malleable signatures as well. - if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { - return (address(0), RecoverError.InvalidSignatureS, s); - } - - // If the signature is valid (and not malleable), return the signer address - address signer = ecrecover(hash, v, r, s); - if (signer == address(0)) { - return (address(0), RecoverError.InvalidSignature, bytes32(0)); - } - - return (signer, RecoverError.NoError, bytes32(0)); - } - - /** - * @dev Overload of {ECDSA-recover} that receives the `v`, - * `r` and `s` signature fields separately. - */ - function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { - (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s); - _throwError(error, errorArg); - return recovered; - } - - /** - * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided. - */ - function _throwError(RecoverError error, bytes32 errorArg) private pure { - if (error == RecoverError.NoError) { - return; // no error: do nothing - } else if (error == RecoverError.InvalidSignature) { - revert ECDSAInvalidSignature(); - } else if (error == RecoverError.InvalidSignatureLength) { - revert ECDSAInvalidSignatureLength(uint256(errorArg)); - } else if (error == RecoverError.InvalidSignatureS) { - revert ECDSAInvalidSignatureS(errorArg); - } - } -} diff --git a/src/SignatureChecker.sol b/src/SignatureChecker.sol index 05c4ceb..b0ba0ff 100644 --- a/src/SignatureChecker.sol +++ b/src/SignatureChecker.sol @@ -2,23 +2,86 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/Ownable.sol"; -import {ECDSA} from "./ECDSA.sol"; -// todo: Do we need to have some stateful capabilities? -contract SignatureChecker { - // todo Replace with our notary address - // 0xfdf07A5dCfa7b74f4c28DAb23eaD8B1c43Be801F - address public constant NOTARY_ADDRESS = address(0xfdf07A5dCfa7b74f4c28DAb23eaD8B1c43Be801F); +/// @title SignatureChecker +/// @notice A contract that verifies signatures +contract SignatureChecker is Ownable { + /// @notice Mapping of notary addresses to their validity + mapping(address => bool) public isNotary; - function isValidSignatureNow(bytes32 hash, bytes memory signature) external pure returns (bool) { - (address recovered, ECDSA.RecoverError err,) = ECDSA.tryRecover(hash, signature); - return err == ECDSA.RecoverError.NoError && recovered == NOTARY_ADDRESS; + /// valid digests for a given address + mapping(address => bytes32) public digests; + + uint256 public constant BN254_MODULUS = + 21888242871839275222246405745257275088548364400416034343698204186575808495617; + + /// @notice Error for invalid signatures + error InvalidSignature(); + /// @notice Error for invalid notary addresses + error InvalidNotary(); + /// @notice Error for invalid digest + error InvalidDigest(); + /// @notice Error for invalid signature length + error InvalidSignatureLength(); + + /// @notice Constructor configures the notary address + /// @param _notaryAddress The address of the notary to add + constructor(address _notaryAddress) Ownable(msg.sender) { + isNotary[_notaryAddress] = true; + } + + /// @notice Adds a notary + /// @param _notaryAddress The address of the notary to add + function addNotary(address _notaryAddress) external onlyOwner { + isNotary[_notaryAddress] = true; + } + + /// @notice Removes a notary + /// @param _notaryAddress The address of the notary to remove + function removeNotary(address _notaryAddress) external onlyOwner { + isNotary[_notaryAddress] = false; + } + + // Check to see that the digest is a merkle root of a keccak256 hash of a leafs = (keccak(value), keccak(manifest)) + function verify_digest(bytes32 _digest, bytes32 _manifest, bytes32 _value) internal pure returns (bool) { + bytes32 root = keccak256(abi.encodePacked(_value, _manifest)); + return _digest == root; + } + + /// @notice Verifies a signature + /// @param digest The hash of the data that was signed + /// @param v The recovery id + /// @param r The R value of the signature + /// @param s The S value of the signature + /// @param signer The address that signed the data + /// @param manifest The manifest of the data + /// @param value The value of the data + function verifyNotarySignature( + bytes32 digest, + uint8 v, + bytes32 r, + bytes32 s, + address signer, + bytes32 manifest, + bytes32 value + ) external returns (bool) { + // check if the signer is a notary + if (!isNotary[signer]) { + revert InvalidNotary(); + } + if (!verify_digest(digest, manifest, value)) { + revert InvalidDigest(); + } - /// this decomposes the signature into r, s, v // r is the x-coordinate of the curve point // s is the y-coordinate of the curve point - - // v is 0 for an even y-value - // v is 1 for an odd y-value + // v is 27 or 28 based on the y-value being even or odd + // verify the signature + address recoveredSigner = ecrecover(digest, v, r, s); + if (recoveredSigner != signer) { + revert InvalidSignature(); + } + digests[msg.sender] = digest; + return true; } } diff --git a/test/SignatureChecker.t.sol b/test/SignatureChecker.t.sol index 281494b..0c6744e 100644 --- a/test/SignatureChecker.t.sol +++ b/test/SignatureChecker.t.sol @@ -8,21 +8,20 @@ contract SignatureCheckerTest is Test { SignatureChecker public signatureChecker; function setUp() public { - signatureChecker = new SignatureChecker(); + signatureChecker = new SignatureChecker(0xfdf07A5dCfa7b74f4c28DAb23eaD8B1c43Be801F); } - function test_isValidSignatureNow() public view { - // Replace with our notary proof hash - bytes32 hash = keccak256("test"); + function test_isValidSignatureNow() public { + // TEST vector from web-prover @ githash 2dc768e818d6f9fef575a88a2ceb80c0ed11974f + address signer = 0xfdf07A5dCfa7b74f4c28DAb23eaD8B1c43Be801F; + bytes32 digest = bytes32(0xe45537be7b5cd288c9c46b7e027b4f5a66202146012f792c1b1cabb65828994b); + bytes32 r = bytes32(0x36e820b3524e9ffffe0b4ee49e4131cc362fd161821c1dfc8757dc6186f31c96); + bytes32 s = bytes32(0x416e537065673e3028eca37cf3cbe805a3d2fafbc47235fee5e89df5f0509a9c); + uint8 v = 27; - // Replace with our notary signature - // 9039BD2DED8FD5E09B62A04A3D25D2D13F0F45F40C1B4CA6C3AECE4A71F92C1416D62561A8939ED49C97D9621C47988CF74B5B5074F89F2B2C6B9B18A6EEB1B2 - // 9039BD2DED8FD5E09B62A04A3D25D2D13F0F45F40C1B4CA6C3AECE4A71F92C1416D62561A8939ED49C97D9621C47988CF74B5B5074F89F2B2C6B9B18A6EEB1B2 - // There are interestingly 129 characters in this signature, the ercrecover opcode expects 65 where 32 are R, 32 are S, and 1 is V - bytes memory signature = - abi.encodePacked(bytes32(0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef)); + bytes32 value = 0x8452c9b9140222b08593a26daa782707297be9f7b3e8281d7b4974769f19afd0; + bytes32 manifest = 0x7df909980a1642d0370a4a510422201ce525da6b319a7b9e9656771fa7336d5a; - // verify signature - assertEq(signatureChecker.isValidSignatureNow(hash, signature), true); + assertEq(signatureChecker.verifyNotarySignature(digest, v, r, s, signer, manifest, value), true); } } diff --git a/test_vectors.json b/test_vectors.json new file mode 100644 index 0000000..94a73c8 --- /dev/null +++ b/test_vectors.json @@ -0,0 +1,9 @@ +{ + "digest": "0x0ad25b24a05589ed9f2332ac85f5690c8400019f32858c2f6bf24877362d41db", + "signature": "0x304502210086c6ab86ac26bfdfd245ab65a05e90cd18afe9f810acb42532adf7570cd0ed77022017370b1c7a7d7d96155e6144a9bfc9265f81c354b1cb4af7cebe52e601dabfef", + "signature_r": "0x86c6ab86ac26bfdfd245ab65a05e90cd18afe9f810acb42532adf7570cd0ed77", + "signature_s": "0x17370b1c7a7d7d96155e6144a9bfc9265f81c354b1cb4af7cebe52e601dabfef", + "signature_v": 27, + "signer": "0xfdf07a5dcfa7b74f4c28dab23ead8b1c43be801f" +} +