diff --git a/CHANGELOG.md b/CHANGELOG.md index a81679bb..c4c9b73c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## 12-04-2025 +- `SignerERC7913`: Abstract signer that verifies signatures using the ERC-7913 workflow. +- `ERC7913SignatureVerifierP256` and `ERC7913SignatureVerifierRSA`: Ready to use ERC-7913 verifiers that implement key verification for P256 (secp256r1) and RSA keys. - `ERC7913Utils`: Utilities library for verifying signatures by ERC-7913 formatted signers. ## 11-04-2025 diff --git a/contracts/mocks/account/AccountERC7913Mock.sol b/contracts/mocks/account/AccountERC7913Mock.sol new file mode 100644 index 00000000..e25961d2 --- /dev/null +++ b/contracts/mocks/account/AccountERC7913Mock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {Account} from "../../account/Account.sol"; +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import {ERC7739} from "../../utils/cryptography/ERC7739.sol"; +import {ERC7821} from "../../account/extensions/ERC7821.sol"; +import {SignerERC7913} from "../../utils/cryptography/SignerERC7913.sol"; + +abstract contract AccountERC7913Mock is Account, SignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder { + constructor(bytes memory _signer) { + _setSigner(_signer); + } + + /// @inheritdoc ERC7821 + function _erc7821AuthorizedExecutor( + address caller, + bytes32 mode, + bytes calldata executionData + ) internal view virtual override returns (bool) { + return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData); + } +} diff --git a/contracts/mocks/docs/account/MyAccountERC7913.sol b/contracts/mocks/docs/account/MyAccountERC7913.sol new file mode 100644 index 00000000..165d77a2 --- /dev/null +++ b/contracts/mocks/docs/account/MyAccountERC7913.sol @@ -0,0 +1,30 @@ +// contracts/MyAccount.sol +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {Account} from "../../../account/Account.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import {ERC7739} from "../../../utils/cryptography/ERC7739.sol"; +import {ERC7821} from "../../../account/extensions/ERC7821.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {SignerERC7913} from "../../../utils/cryptography/SignerERC7913.sol"; + +contract MyAccount7913 is Account, SignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder, Initializable { + constructor() EIP712("MyAccount7913", "1") {} + + function initialize(bytes memory signer) public initializer { + _setSigner(signer); + } + + /// @dev Allows the entry point as an authorized executor. + function _erc7821AuthorizedExecutor( + address caller, + bytes32 mode, + bytes calldata executionData + ) internal view virtual override returns (bool) { + return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData); + } +} diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 4454470a..fd0c7d75 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -8,26 +8,41 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {AbstractSigner}: Abstract contract for internal signature validation in smart contracts. * {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`. * {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739. + * {ERC7913Utils}: utilities library that implements ERC-7913 signature verification with fallback to ERC-1271 and ECDSA. + * {SignerECDSA}, {SignerERC7913}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms. + * {ERC7913SignatureVerifierP256}, {ERC7913SignatureVerifierRSA}: Ready to use ERC-7913 signature verifiers for P256 and RSA keys * {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms. * {EnumerableSetExtended} and {EnumerableMapExtended}: Extensions of the `EnumerableSet` and `EnumerableMap` libraries with more types, including non-value types. * {Masks}: Library to handle `bytes32` masks. == Cryptography -{{AbstractSigner}} - {{ERC7739}} {{ERC7739Utils}} +=== Abstract Signers + +{{AbstractSigner}} + {{SignerECDSA}} +{{SignerERC7913}} + {{SignerP256}} {{SignerERC7702}} {{SignerRSA}} +=== ERC-7913 + +{{ERC7913Utils}} + +{{ERC7913SignatureVerifierP256}} + +{{ERC7913SignatureVerifierRSA}} + == Structs {{EnumerableSetExtended}} diff --git a/contracts/utils/cryptography/ERC7913SignatureVerifierP256.sol b/contracts/utils/cryptography/ERC7913SignatureVerifierP256.sol new file mode 100644 index 00000000..fc822a8a --- /dev/null +++ b/contracts/utils/cryptography/ERC7913SignatureVerifierP256.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; +import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol"; + +/** + * @dev ERC-7913 signature verifier that support P256 (secp256r1) keys. + */ +contract ERC7913SignatureVerifierP256 is IERC7913SignatureVerifier { + /// @inheritdoc IERC7913SignatureVerifier + function verify(bytes calldata key, bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) { + // Signature length may be 0x40 or 0x41. + if (key.length == 0x40 && signature.length >= 0x40) { + bytes32 qx = bytes32(key[0x00:0x20]); + bytes32 qy = bytes32(key[0x20:0x40]); + bytes32 r = bytes32(signature[0x00:0x20]); + bytes32 s = bytes32(signature[0x20:0x40]); + if (P256.verify(hash, r, s, qx, qy)) { + return IERC7913SignatureVerifier.verify.selector; + } + } + return 0xFFFFFFFF; + } +} diff --git a/contracts/utils/cryptography/ERC7913SignatureVerifierRSA.sol b/contracts/utils/cryptography/ERC7913SignatureVerifierRSA.sol new file mode 100644 index 00000000..597224e9 --- /dev/null +++ b/contracts/utils/cryptography/ERC7913SignatureVerifierRSA.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; +import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol"; + +/** + * @dev ERC-7913 signature verifier that support RSA keys. + */ +contract ERC7913SignatureVerifierRSA is IERC7913SignatureVerifier { + /// @inheritdoc IERC7913SignatureVerifier + function verify(bytes calldata key, bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) { + (bytes memory e, bytes memory n) = abi.decode(key, (bytes, bytes)); + return + RSA.pkcs1Sha256(abi.encodePacked(hash), signature, e, n) + ? IERC7913SignatureVerifier.verify.selector + : bytes4(0xFFFFFFFF); + } +} diff --git a/contracts/utils/cryptography/ERC7913Utils.sol b/contracts/utils/cryptography/ERC7913Utils.sol index 157dcbfb..efe7df09 100644 --- a/contracts/utils/cryptography/ERC7913Utils.sol +++ b/contracts/utils/cryptography/ERC7913Utils.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol"; /** @@ -16,6 +17,8 @@ import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol"; * See https://eips.ethereum.org/EIPS/eip-7913[ERC-7913]. */ library ERC7913Utils { + using Bytes for bytes; + /** * @dev Verifies a signature for a given signer and hash. * @@ -28,7 +31,7 @@ library ERC7913Utils { * - Otherwise: verification is done using {IERC7913SignatureVerifier} */ function isValidSignatureNow( - bytes calldata signer, + bytes memory signer, bytes32 hash, bytes memory signature ) internal view returns (bool) { @@ -37,7 +40,7 @@ library ERC7913Utils { } else if (signer.length == 20) { return SignatureChecker.isValidSignatureNow(address(bytes20(signer)), hash, signature); } else { - try IERC7913SignatureVerifier(address(bytes20(signer[0:20]))).verify(signer[20:], hash, signature) returns ( + try IERC7913SignatureVerifier(address(bytes20(signer))).verify(signer.slice(20), hash, signature) returns ( bytes4 magic ) { return magic == IERC7913SignatureVerifier.verify.selector; diff --git a/contracts/utils/cryptography/SignerERC7913.sol b/contracts/utils/cryptography/SignerERC7913.sol new file mode 100644 index 00000000..3ba43981 --- /dev/null +++ b/contracts/utils/cryptography/SignerERC7913.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {AbstractSigner} from "./AbstractSigner.sol"; +import {ERC7913Utils} from "./ERC7913Utils.sol"; + +/** + * @dev Implementation of {AbstractSigner} using + * https://eips.ethereum.org/EIPS/eip-7913[ERC-7913] signature verification. + * + * For {Account} usage, a {_setSigner} function is provided to set the ERC-7913 formatted {signer}. + * Doing so is easier for a factory, who is likely to use initializable clones of this contract. + * + * The signer is a `bytes` object that concatenates a verifier address and a key: `verifier || key`. + * + * Example of usage: + * + * ```solidity + * contract MyAccountERC7913 is Account, SignerERC7913, Initializable { + * constructor() EIP712("MyAccountERC7913", "1") {} + * + * function initialize(bytes memory signer_) public initializer { + * _setSigner(signer_); + * } + * } + * ``` + * + * IMPORTANT: Failing to call {_setSigner} either during construction (if used standalone) + * or during initialization (if used as a clone) may leave the signer either front-runnable or unusable. + */ + +abstract contract SignerERC7913 is AbstractSigner { + bytes private _signer; + + /// @dev Sets the signer (i.e. `verifier || key`) with an ERC-7913 formatted signer. + function _setSigner(bytes memory signer_) internal { + _signer = signer_; + } + + /// @dev Return the ERC-7913 signer (i.e. `verifier || key`). + function signer() public view virtual returns (bytes memory) { + return _signer; + } + + /// @dev Verifies a signature using {ERC7913Utils.isValidSignatureNow} with {signer}, `hash` and `signature`. + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { + return ERC7913Utils.isValidSignatureNow(signer(), hash, signature); + } +} diff --git a/docs/modules/ROOT/pages/account-abstraction.adoc b/docs/modules/ROOT/pages/account-abstraction.adoc index 55f7337f..eddfa58c 100644 --- a/docs/modules/ROOT/pages/account-abstraction.adoc +++ b/docs/modules/ROOT/pages/account-abstraction.adoc @@ -29,7 +29,7 @@ To setup an account, you can either bring your own validation logic and start wi === Selecting a signer -The library includes specializations of the `AbstractSigner` contract that use custom digital signature verification algorithms. These are xref:api:utils.adoc#SignerECDSA[`SignerECDSA`], xref:api:utils.adoc#SignerP256[`SignerP256`] and xref:api:utils.adoc#SignerRSA[`SignerRSA`]. +The library includes specializations of the `AbstractSigner` contract that use custom digital signature verification algorithms. These are xref:api:utils.adoc#SignerECDSA[`SignerECDSA`], xref:api:utils.adoc#SignerP256[`SignerP256`], xref:api:utils.adoc#SignerRSA[`SignerRSA`], xref:api:utils.adoc#SignerERC7702[`SignerERC7702`], and xref:api:utils.adoc#SignerERC7913[`SignerERC7913`]. Since smart accounts are deployed by a factory, the best practice is to create https://docs.openzeppelin.com/contracts/5.x/api/proxy#minimal_clones[minimal clones] of initializable contracts. These signer implementations provide an initializable design by default so that the factory can interact with the account to set it up after deployment in a single transaction. @@ -56,6 +56,13 @@ Similarly, some government and corporate public key infrastructures use RSA for include::api:example$account/MyAccountRSA.sol[] ---- +For more advanced use cases where you need to support keys that don't have their own Ethereum address (like hardware devices or non-Ethereum cryptographic curves), you can use xref:api:utils.adoc#SignerERC7913[`SignerERC7913`]. This implementation allows for signature verification using ERC-7913 compatible verifiers. + +[source,solidity] +---- +include::api:example$account/MyAccountERC7913.sol[] +---- + == Account Factory The first time a user sends an user operation, the account will be created deterministically (i.e. its code and address can be predicted) using the the `initCode` field in the UserOperation. This field contains both the address of a smart contract (the factory) and the data required to call it and deploy the smart account. diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 9dcf744e..42726a2a 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -34,3 +34,28 @@ In case your smart contract validates signatures, using xref:api:utils.adoc#ERC7 ---- include::api:example$utils/cryptography/ERC7739SignerECDSA.sol[] ---- + +==== ERC-7913 Signature Verifiers + +ERC-7913 extends the concept of signature verification to support keys that don't have their own Ethereum address. This is particularly useful for integrating non-Ethereum cryptographic curves, hardware devices, or other identity systems into smart accounts. + +The standard defines a verifier interface that can be implemented to support different types of keys. A signer is represented as a `bytes` object that concatenates a verifier address and a key: `verifier || key`. + +xref:api:utils.adoc#ERC7913Utils[`ERC7913Utils`] provides functions for verifying signatures using ERC-7913 compatible verifiers: + +[source,solidity] +---- +using ERC7913Utils for bytes; + +function _verify(bytes memory signer, bytes32 hash, bytes memory signature) internal view returns (bool) { + return signer.isValidSignatureNow(hash, signature); +} +---- + +The verification process works as follows: + +* If `signer.length < 20`: verification fails +* If `signer.length == 20`: verification is done using https://docs.openzeppelin.com/contracts/5.x/api/utils#SignatureChecker[SignatureChecker] +* Otherwise: verification is done using an ERC-7913 verifier. + +This allows for backward compatibility with EOAs and ERC-1271 contracts while supporting new types of keys. diff --git a/lib/forge-std b/lib/forge-std index 6abf6698..841c3a3e 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 6abf66980050ab03a35b52bdab814f55001d6929 +Subproject commit 841c3a3e8b7612f76599797b93da5a61899c2aa8 diff --git a/test/account/AccountERC7913.test.js b/test/account/AccountERC7913.test.js new file mode 100644 index 00000000..72004254 --- /dev/null +++ b/test/account/AccountERC7913.test.js @@ -0,0 +1,106 @@ +const { ethers, entrypoint } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { getDomain } = require('@openzeppelin/contracts/test/helpers/eip712'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { NonNativeSigner, P256SigningKey, RSASHA256SigningKey } = require('../helpers/signers'); +const { PackedUserOperation } = require('../helpers/eip712-types'); + +const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior'); +const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior'); +const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior'); + +// Prepare signer in advance (RSA are long to initialize) +const signerECDSA = ethers.Wallet.createRandom(); +const signerP256 = new NonNativeSigner(P256SigningKey.random()); +const signerRSA = new NonNativeSigner(RSASHA256SigningKey.random()); + +// Minimal fixture common to the different signer verifiers +async function fixture() { + // EOAs and environment + const [beneficiary, other] = await ethers.getSigners(); + const target = await ethers.deployContract('CallReceiverMockExtended'); + + // ERC-7913 verifiers + const verifierP256 = await ethers.deployContract('ERC7913SignatureVerifierP256'); + const verifierRSA = await ethers.deployContract('ERC7913SignatureVerifierRSA'); + + // ERC-4337 env + const helper = new ERC4337Helper(); + await helper.wait(); + const entrypointDomain = await getDomain(entrypoint.v08); + const domain = { name: 'AccountERC7913', version: '1', chainId: entrypointDomain.chainId }; // Missing verifyingContract, + + const makeMock = signer => + helper.newAccount('$AccountERC7913Mock', ['AccountERC7913', '1', signer]).then(mock => { + domain.verifyingContract = mock.address; + return mock; + }); + + const signUserOp = function (userOp) { + return this.signer + .signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed) + .then(signature => Object.assign(userOp, { signature })); + }; + + return { helper, verifierP256, verifierRSA, domain, target, beneficiary, other, makeMock, signUserOp }; +} + +describe('AccountERC7913', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + // Using ECDSA key as verifier + describe('ECDSA key', function () { + beforeEach(async function () { + this.signer = signerECDSA; + this.mock = await this.makeMock(this.signer.address); + }); + + shouldBehaveLikeAccountCore(); + shouldBehaveLikeAccountHolder(); + shouldBehaveLikeERC1271({ erc7739: true }); + shouldBehaveLikeERC7821(); + }); + + // Using P256 key with an ERC-7913 verifier + describe('P256 key', function () { + beforeEach(async function () { + this.signer = signerP256; + this.mock = await this.makeMock( + ethers.concat([ + this.verifierP256.target, + this.signer.signingKey.publicKey.qx, + this.signer.signingKey.publicKey.qy, + ]), + ); + }); + + shouldBehaveLikeAccountCore(); + shouldBehaveLikeAccountHolder(); + shouldBehaveLikeERC1271({ erc7739: true }); + shouldBehaveLikeERC7821(); + }); + + // Using RSA key with an ERC-7913 verifier + describe('RSA key', function () { + beforeEach(async function () { + this.signer = signerRSA; + this.mock = await this.makeMock( + ethers.concat([ + this.verifierRSA.target, + ethers.AbiCoder.defaultAbiCoder().encode( + ['bytes', 'bytes'], + [this.signer.signingKey.publicKey.e, this.signer.signingKey.publicKey.n], + ), + ]), + ); + }); + + shouldBehaveLikeAccountCore(); + shouldBehaveLikeAccountHolder(); + shouldBehaveLikeERC1271({ erc7739: true }); + shouldBehaveLikeERC7821(); + }); +});