Skip to content

Commit d248ee9

Browse files
committed
Merge branch 'aa/erc7913-signer' into feature/zk-email-7913
2 parents ccc43fa + fb0e649 commit d248ee9

File tree

9 files changed

+336
-3
lines changed

9 files changed

+336
-3
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## XX-XX-XXXX
2+
3+
- `ERC7913Utils`: Utilities library for verifying signatures by ERC-7913 formatted signers.
4+
- `SignerERC7913`: Abstract signer that verifies signatures using the ERC-7913 workflow.
5+
- `ERC7913SignatureVerifierP256` and `ERC7913SignatureVerifierRSA`: Ready to use ERC-7913 verifiers that implement key verification for P256 (secp256r1) and RSA keys.
6+
17
## 28-03-2025
28

39
- Deprecate `Account` and rename `AccountCore` to `Account`.

contracts/interfaces/IERC7913.sol

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.0;
4+
5+
/**
6+
* @dev Signature verifier interface.
7+
*/
8+
interface IERC7913SignatureVerifier {
9+
/**
10+
* @dev Verifies `signature` as a valid signature of `hash` by `key`.
11+
*
12+
* MUST return the bytes4 magic value IERC7913SignatureVerifier.verify.selector if the signature is valid.
13+
* SHOULD return 0xffffffff or revert if the signature is not valid.
14+
* SHOULD return 0xffffffff or revert if the key is empty
15+
*/
16+
function verify(bytes calldata key, bytes32 hash, bytes calldata signature) external view returns (bytes4);
17+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {Account} from "../../account/Account.sol";
6+
import {ERC7821} from "../../account/extensions/ERC7821.sol";
7+
import {SignerERC7913} from "../../utils/cryptography/SignerERC7913.sol";
8+
9+
abstract contract AccountERC7913Mock is Account, SignerERC7913, ERC7821 {
10+
constructor(bytes memory _signer) {
11+
_setSigner(_signer);
12+
}
13+
14+
/// @inheritdoc ERC7821
15+
function _erc7821AuthorizedExecutor(
16+
address caller,
17+
bytes32 mode,
18+
bytes calldata executionData
19+
) internal view virtual override returns (bool) {
20+
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
21+
}
22+
}

contracts/utils/README.adoc

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,37 @@ Miscellaneous contracts and libraries containing utility functions you can use t
88
* {AbstractSigner}: Abstract contract for internal signature validation in smart contracts.
99
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`.
1010
* {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739.
11-
* {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
11+
* {ERC7913Utils}: utilities library that implements ERC-7913 signature verification with fallback to ERC-1271 and ECDSA.
12+
* {SignerECDSA}, {SignerERC7913}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
13+
* {ERC7913SignatureVerifierP256}, {ERC7913SignatureVerifierRSA}: Ready to use ERC-7913 signature verifiers for P256 and RSA keys
1214
* {Masks}: Library to handle `bytes32` masks.
1315

1416
== Cryptography
1517

16-
{{AbstractSigner}}
17-
1818
{{ERC7739}}
1919

2020
{{ERC7739Utils}}
2121

22+
=== Abstract signers
23+
24+
{{AbstractSigner}}
25+
2226
{{SignerECDSA}}
2327

28+
{{SignerERC7913}}
29+
2430
{{SignerP256}}
2531

2632
{{SignerRSA}}
2733

34+
=== ERC-7913
35+
36+
{{ERC7913Utils}}
37+
38+
{{ERC7913SignatureVerifierP256}}
39+
40+
{{ERC7913SignatureVerifierRSA}}
41+
2842
== Libraries
2943

3044
{{Masks}}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol";
6+
import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol";
7+
8+
/**
9+
* @dev ERC-7913 signature verifier that support P256 (secp256r1) keys.
10+
*/
11+
contract ERC7913SignatureVerifierP256 is IERC7913SignatureVerifier {
12+
/// @inheritdoc IERC7913SignatureVerifier
13+
function verify(bytes calldata key, bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) {
14+
// Signature length may be 0x40 or 0x41.
15+
if (key.length == 0x40 && signature.length > 0x3f && signature.length < 0x42) {
16+
bytes32 qx = bytes32(key[0x00:0x20]);
17+
bytes32 qy = bytes32(key[0x20:0x40]);
18+
bytes32 r = bytes32(signature[0x00:0x20]);
19+
bytes32 s = bytes32(signature[0x20:0x40]);
20+
if (P256.verify(hash, r, s, qx, qy)) {
21+
return IERC7913SignatureVerifier.verify.selector;
22+
}
23+
}
24+
return 0xFFFFFFFF;
25+
}
26+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol";
6+
import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol";
7+
8+
/**
9+
* @dev ERC-7913 signature verifier that support RSA keys.
10+
*/
11+
contract ERC7913SignatureVerifierRSA is IERC7913SignatureVerifier {
12+
/// @inheritdoc IERC7913SignatureVerifier
13+
function verify(bytes calldata key, bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) {
14+
(bytes memory e, bytes memory n) = abi.decode(key, (bytes, bytes));
15+
return
16+
RSA.pkcs1Sha256(abi.encodePacked(hash), signature, e, n)
17+
? IERC7913SignatureVerifier.verify.selector
18+
: bytes4(0xFFFFFFFF);
19+
}
20+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
6+
import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol";
7+
import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol";
8+
9+
/**
10+
* @dev Helper library to verify key signatures following the ERC-7913 standard, with fallback to ECDSA and ERC-1271
11+
* when the signer's key is empty (as specified in ERC-7913)
12+
*/
13+
library ERC7913Utils {
14+
using Bytes for bytes;
15+
/**
16+
* @dev Checks if a signature is valid for a given signer and data hash. The signer is interpreted following
17+
* ERC-7913:
18+
* * If the signer's key is not empty the signature is verified using the signer's verifier ERC-7913 interface.
19+
* * Otherwise, the signature is verified using the `SignatureChecker` library, which supports both ECDSA and
20+
* ERC-1271 signature verification
21+
*
22+
* NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
23+
* change through time. It could return true at block N and false at block N+1 (or the opposite).
24+
*/
25+
function isValidSignatureNow(
26+
bytes memory signer,
27+
bytes32 hash,
28+
bytes calldata signature
29+
) internal view returns (bool) {
30+
if (signer.length < 20) {
31+
return false;
32+
} else if (signer.length == 20) {
33+
return SignatureChecker.isValidSignatureNow(address(bytes20(signer)), hash, signature);
34+
} else {
35+
try IERC7913SignatureVerifier(address(bytes20(signer))).verify(signer.slice(20), hash, signature) returns (
36+
bytes4 magic
37+
) {
38+
return magic == IERC7913SignatureVerifier.verify.selector;
39+
} catch {
40+
return false;
41+
}
42+
}
43+
}
44+
45+
/**
46+
* @dev Checks if a signature is valid for a given signer and data hash. The signer is interpreted following
47+
* ERC-7913:
48+
* * If the signer's key is not empty the signature is verified using the signer's verifier ERC-7913 interface.
49+
* * Otherwise, the signature is verified using the `SignatureChecker` library, which supports both ECDSA and
50+
* ERC-1271 signature verification
51+
*
52+
* NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
53+
* change through time. It could return true at block N and false at block N+1 (or the opposite).
54+
*/
55+
function isValidSignatureNowCalldata(
56+
bytes calldata signer,
57+
bytes32 hash,
58+
bytes calldata signature
59+
) internal view returns (bool) {
60+
if (signer.length < 20) {
61+
return false;
62+
} else if (signer.length == 20) {
63+
return SignatureChecker.isValidSignatureNow(address(bytes20(signer)), hash, signature);
64+
} else {
65+
try IERC7913SignatureVerifier(address(bytes20(signer[0:20]))).verify(signer[20:], hash, signature) returns (
66+
bytes4 magic
67+
) {
68+
return magic == IERC7913SignatureVerifier.verify.selector;
69+
} catch {
70+
return false;
71+
}
72+
}
73+
}
74+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {AbstractSigner} from "./AbstractSigner.sol";
6+
import {ERC7913Utils} from "./ERC7913Utils.sol";
7+
8+
/**
9+
* @dev Implementation of {AbstractSigner} that supports ERC-7913 signers.
10+
*
11+
* For {Account} usage, an {_setSigner} function is provided to set the ERC-7913 formatted {signer}.
12+
* Doing so it's easier for a factory, whose likely to use initializable clones of this contract.
13+
*
14+
* Example of usage:
15+
*
16+
* ```solidity
17+
* contract MyAccountERC7913 is Account, SignerERC7913, Initializable {
18+
* constructor() EIP712("MyAccountERC7913", "1") {}
19+
*
20+
* function initialize(bytes memory signer) public initializer {
21+
* _setSigner(signer);
22+
* }
23+
* }
24+
* ```
25+
*
26+
* IMPORTANT: Avoiding to call {_setSigner} either during construction (if used standalone)
27+
* or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
28+
*/
29+
30+
abstract contract SignerERC7913 is AbstractSigner {
31+
bytes private _signer;
32+
33+
function _setSigner(bytes memory signer_) internal {
34+
_signer = signer_;
35+
}
36+
37+
function signer() public view virtual returns (bytes memory) {
38+
return _signer;
39+
}
40+
41+
/// @inheritdoc AbstractSigner
42+
function _rawSignatureValidation(
43+
bytes32 hash,
44+
bytes calldata signature
45+
) internal view virtual override returns (bool) {
46+
return ERC7913Utils.isValidSignatureNow(signer(), hash, signature);
47+
}
48+
}

test/account/AccountERC7913.test.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
const { ethers, entrypoint } = require('hardhat');
2+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
3+
4+
const { getDomain } = require('@openzeppelin/contracts/test/helpers/eip712');
5+
const { ERC4337Helper } = require('../helpers/erc4337');
6+
const { NonNativeSigner, P256SigningKey, RSASHA256SigningKey } = require('../helpers/signers');
7+
const { PackedUserOperation } = require('../helpers/eip712-types');
8+
9+
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
10+
const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
11+
const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
12+
13+
// Prepare signer in advance (RSA are long to initialize)
14+
const signerECDSA = ethers.Wallet.createRandom();
15+
const signerP256 = new NonNativeSigner(P256SigningKey.random());
16+
const signerRSA = new NonNativeSigner(RSASHA256SigningKey.random());
17+
18+
// Minimal fixture common to the different signer verifiers
19+
async function fixture() {
20+
// EOAs and environment
21+
const [beneficiary, other] = await ethers.getSigners();
22+
const target = await ethers.deployContract('CallReceiverMockExtended');
23+
24+
// ERC-7913 verifiers
25+
const verifierP256 = await ethers.deployContract('ERC7913SignatureVerifierP256');
26+
const verifierRSA = await ethers.deployContract('ERC7913SignatureVerifierRSA');
27+
28+
// ERC-4337 env
29+
const helper = new ERC4337Helper();
30+
await helper.wait();
31+
const entrypointDomain = await getDomain(entrypoint.v08);
32+
const domain = { name: 'AccountERC7913', version: '1', chainId: entrypointDomain.chainId }; // Missing verifyingContract,
33+
34+
const makeMock = signer =>
35+
helper.newAccount('$AccountERC7913Mock', ['AccountERC7913', '1', signer]).then(mock => {
36+
domain.verifyingContract = mock.address;
37+
return mock;
38+
});
39+
40+
const signUserOp = function (userOp) {
41+
return this.signer
42+
.signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
43+
.then(signature => Object.assign(userOp, { signature }));
44+
};
45+
46+
return { helper, verifierP256, verifierRSA, domain, target, beneficiary, other, makeMock, signUserOp };
47+
}
48+
49+
describe('AccountERC7913', function () {
50+
beforeEach(async function () {
51+
Object.assign(this, await loadFixture(fixture));
52+
});
53+
54+
// Using ECDSA key as verifier
55+
describe('ECDSA key', function () {
56+
beforeEach(async function () {
57+
this.signer = signerECDSA;
58+
this.mock = await this.makeMock(this.signer.address);
59+
});
60+
61+
shouldBehaveLikeAccountCore();
62+
shouldBehaveLikeAccountHolder();
63+
shouldBehaveLikeERC1271({ erc7739: true });
64+
shouldBehaveLikeERC7821();
65+
});
66+
67+
// Using P256 key with an ERC-7913 verifier
68+
describe('P256 key', function () {
69+
beforeEach(async function () {
70+
this.signer = signerP256;
71+
this.mock = await this.makeMock(
72+
ethers.concat([
73+
this.verifierP256.target,
74+
this.signer.signingKey.publicKey.qx,
75+
this.signer.signingKey.publicKey.qy,
76+
]),
77+
);
78+
});
79+
80+
shouldBehaveLikeAccountCore();
81+
shouldBehaveLikeAccountHolder();
82+
shouldBehaveLikeERC1271({ erc7739: true });
83+
shouldBehaveLikeERC7821();
84+
});
85+
86+
// Using RSA key with an ERC-7913 verifier
87+
describe('RSA key', function () {
88+
beforeEach(async function () {
89+
this.signer = signerRSA;
90+
this.mock = await this.makeMock(
91+
ethers.concat([
92+
this.verifierRSA.target,
93+
ethers.AbiCoder.defaultAbiCoder().encode(
94+
['bytes', 'bytes'],
95+
[this.signer.signingKey.publicKey.e, this.signer.signingKey.publicKey.n],
96+
),
97+
]),
98+
);
99+
});
100+
101+
shouldBehaveLikeAccountCore();
102+
shouldBehaveLikeAccountHolder();
103+
shouldBehaveLikeERC1271({ erc7739: true });
104+
shouldBehaveLikeERC7821();
105+
});
106+
});

0 commit comments

Comments
 (0)