Skip to content

Commit 886d1c5

Browse files
Amxxernestognw
andauthored
ERC-7913 signature verifier for ZKEmail (#103)
Co-authored-by: ernestognw <[email protected]>
1 parent 9f1171e commit 886d1c5

File tree

3 files changed

+185
-5
lines changed

3 files changed

+185
-5
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {IDKIMRegistry} from "@zk-email/contracts/DKIMRegistry.sol";
6+
import {IVerifier} from "@zk-email/email-tx-builder/interfaces/IVerifier.sol";
7+
import {EmailAuthMsg} from "@zk-email/email-tx-builder/interfaces/IEmailTypes.sol";
8+
import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol";
9+
import {ZKEmailUtils} from "./ZKEmailUtils.sol";
10+
11+
/**
12+
* @dev ERC-7913 signature verifier that supports ZKEmail accounts.
13+
*
14+
* This contract verifies signatures produced through ZKEmail's zero-knowledge
15+
* proofs which allows users to authenticate using their email addresses.
16+
*
17+
* The key decoding logic is customizable: users may override the {_decodeKey} function
18+
* to enforce restrictions or validation on the decoded values (e.g., requiring a specific
19+
* verifier, templateId, or registry). To remain compliant with ERC-7913's statelessness,
20+
* it is recommended to enforce such restrictions using immutable variables only.
21+
*
22+
* Example of overriding _decodeKey to enforce a specific verifier, registry, (or templateId):
23+
*
24+
* ```solidity
25+
* function _decodeKey(bytes calldata key) internal view override returns (
26+
* IDKIMRegistry registry,
27+
* bytes32 accountSalt,
28+
* IVerifier verifier,
29+
* uint256 templateId
30+
* ) {
31+
* (registry, accountSalt, verifier, templateId) = super._decodeKey(key);
32+
* require(verifier == _verifier, "Invalid verifier");
33+
* require(registry == _registry, "Invalid registry");
34+
* return (registry, accountSalt, verifier, templateId);
35+
* }
36+
* ```
37+
*/
38+
abstract contract ERC7913SignatureVerifierZKEmail is IERC7913SignatureVerifier {
39+
using ZKEmailUtils for EmailAuthMsg;
40+
41+
/**
42+
* @dev Verifies a zero-knowledge proof of an email signature validated by a {DKIMRegistry} contract.
43+
*
44+
* The key format is ABI-encoded (IDKIMRegistry, bytes32, IVerifier, uint256) where:
45+
*
46+
* * IDKIMRegistry: The registry contract that validates DKIM public key hashes
47+
* * bytes32: The account salt that uniquely identifies the user's email address
48+
* * IVerifier: The verifier contract instance for ZK proof verification.
49+
* * uint256: The template ID for the command
50+
*
51+
* See {_decodeKey} for the key encoding format.
52+
*
53+
* The signature is an ABI-encoded {ZKEmailUtils-EmailAuthMsg} struct containing
54+
* the command parameters, template ID, and proof details.
55+
*
56+
* Signature encoding:
57+
*
58+
* ```solidity
59+
* bytes memory signature = abi.encode(EmailAuthMsg({
60+
* templateId: 1,
61+
* commandParams: [hash],
62+
* proof: {
63+
* domainName: "example.com", // The domain name of the email sender
64+
* publicKeyHash: bytes32(0x...), // Hash of the DKIM public key used to sign the email
65+
* timestamp: block.timestamp, // When the email was sent
66+
* maskedCommand: "Sign hash", // The command being executed, with sensitive data masked
67+
* emailNullifier: bytes32(0x...), // Unique identifier for the email to prevent replay attacks
68+
* accountSalt: bytes32(0x...), // Unique identifier derived from email and account code
69+
* isCodeExist: true, // Whether the account code exists in the proof
70+
* proof: bytes(0x...) // The zero-knowledge proof verifying the email's authenticity
71+
* }
72+
* }));
73+
* ```
74+
*/
75+
function verify(
76+
bytes calldata key,
77+
bytes32 hash,
78+
bytes calldata signature
79+
) public view virtual override returns (bytes4) {
80+
(IDKIMRegistry registry_, bytes32 accountSalt_, IVerifier verifier_, uint256 templateId_) = abi.decode(
81+
key,
82+
(IDKIMRegistry, bytes32, IVerifier, uint256)
83+
);
84+
EmailAuthMsg memory emailAuthMsg = abi.decode(signature, (EmailAuthMsg));
85+
86+
return
87+
(abi.decode(emailAuthMsg.commandParams[0], (bytes32)) == hash &&
88+
emailAuthMsg.templateId == templateId_ &&
89+
emailAuthMsg.proof.accountSalt == accountSalt_ &&
90+
emailAuthMsg.isValidZKEmail(registry_, verifier_) == ZKEmailUtils.EmailProofError.NoError)
91+
? IERC7913SignatureVerifier.verify.selector
92+
: bytes4(0xffffffff);
93+
}
94+
95+
/**
96+
* @dev Decodes the key into its components.
97+
*
98+
* ```solidity
99+
* bytes memory key = abi.encode(registry, accountSalt, verifier, templateId);
100+
* ```
101+
*/
102+
function _decodeKey(
103+
bytes calldata key
104+
)
105+
internal
106+
view
107+
virtual
108+
returns (IDKIMRegistry registry, bytes32 accountSalt, IVerifier verifier, uint256 templateId)
109+
{
110+
return abi.decode(key, (IDKIMRegistry, bytes32, IVerifier, uint256));
111+
}
112+
}

contracts/utils/cryptography/SignerZKEmail.sol

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,10 @@ abstract contract SignerZKEmail is AbstractSigner {
8181
return _registry;
8282
}
8383

84-
/// @dev An instance of the Verifier contract.
85-
/// See https://docs.zk.email/architecture/zk-proofs#how-zk-email-uses-zero-knowledge-proofs[ZK Proofs].
84+
/**
85+
* @dev An instance of the Verifier contract.
86+
* See https://docs.zk.email/architecture/zk-proofs#how-zk-email-uses-zero-knowledge-proofs[ZK Proofs].
87+
*/
8688
function verifier() public view virtual returns (IVerifier) {
8789
return _verifier;
8890
}

test/account/AccountERC7913.test.js

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
33

44
const { getDomain } = require('@openzeppelin/contracts/test/helpers/eip712');
55
const { ERC4337Helper } = require('../helpers/erc4337');
6-
const { NonNativeSigner, P256SigningKey, RSASHA256SigningKey } = require('../helpers/signers');
6+
const { NonNativeSigner, P256SigningKey, RSASHA256SigningKey, ZKEmailSigningKey } = require('../helpers/signers');
77
const { PackedUserOperation } = require('../helpers/eip712-types');
88

99
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
@@ -15,15 +15,36 @@ const signerECDSA = ethers.Wallet.createRandom();
1515
const signerP256 = new NonNativeSigner(P256SigningKey.random());
1616
const signerRSA = new NonNativeSigner(RSASHA256SigningKey.random());
1717

18+
// Constants for ZKEmail
19+
const accountSalt = '0x046582bce36cdd0a8953b9d40b8f20d58302bacf3bcecffeb6741c98a52725e2'; // keccak256("[email protected]")
20+
const selector = '12345';
21+
const domainName = 'gmail.com';
22+
const publicKeyHash = '0x0ea9c777dc7110e5a9e89b13f0cfc540e3845ba120b2b6dc24024d61488d4788';
23+
const emailNullifier = '0x00a83fce3d4b1c9ef0f600644c1ecc6c8115b57b1596e0e3295e2c5105fbfd8a';
24+
const templateId = ethers.solidityPackedKeccak256(['string', 'uint256'], ['TEST', 0n]);
25+
1826
// Minimal fixture common to the different signer verifiers
1927
async function fixture() {
2028
// EOAs and environment
21-
const [beneficiary, other] = await ethers.getSigners();
29+
const [admin, beneficiary, other] = await ethers.getSigners();
2230
const target = await ethers.deployContract('CallReceiverMockExtended');
2331

32+
// DKIM Registry for ZKEmail
33+
const dkim = await ethers.deployContract('ECDSAOwnedDKIMRegistry');
34+
await dkim.initialize(admin, admin);
35+
await dkim
36+
.SET_PREFIX()
37+
.then(prefix => dkim.computeSignedMsg(prefix, domainName, publicKeyHash))
38+
.then(message => admin.signMessage(message))
39+
.then(signature => dkim.setDKIMPublicKeyHash(selector, domainName, publicKeyHash, signature));
40+
41+
// ZKEmail Verifier
42+
const zkEmailVerifier = await ethers.deployContract('ZKEmailVerifierMock');
43+
2444
// ERC-7913 verifiers
2545
const verifierP256 = await ethers.deployContract('ERC7913SignatureVerifierP256');
2646
const verifierRSA = await ethers.deployContract('ERC7913SignatureVerifierRSA');
47+
const verifierZKEmail = await ethers.deployContract('$ERC7913SignatureVerifierZKEmail');
2748

2849
// ERC-4337 env
2950
const helper = new ERC4337Helper();
@@ -43,7 +64,20 @@ async function fixture() {
4364
.then(signature => Object.assign(userOp, { signature }));
4465
};
4566

46-
return { helper, verifierP256, verifierRSA, domain, target, beneficiary, other, makeMock, signUserOp };
67+
return {
68+
helper,
69+
verifierP256,
70+
verifierRSA,
71+
verifierZKEmail,
72+
dkim,
73+
zkEmailVerifier,
74+
domain,
75+
target,
76+
beneficiary,
77+
other,
78+
makeMock,
79+
signUserOp,
80+
};
4781
}
4882

4983
describe('AccountERC7913', function () {
@@ -103,4 +137,36 @@ describe('AccountERC7913', function () {
103137
shouldBehaveLikeERC1271({ erc7739: true });
104138
shouldBehaveLikeERC7821();
105139
});
140+
141+
// Using ZKEmail with an ERC-7913 verifier
142+
describe('ZKEmail', function () {
143+
beforeEach(async function () {
144+
// Create ZKEmail signer
145+
this.signer = new NonNativeSigner(
146+
new ZKEmailSigningKey(domainName, publicKeyHash, emailNullifier, accountSalt, templateId),
147+
);
148+
149+
// Create account with ZKEmail verifier
150+
this.mock = await this.makeMock(
151+
ethers.concat([
152+
this.verifierZKEmail.target,
153+
ethers.AbiCoder.defaultAbiCoder().encode(
154+
['address', 'bytes32', 'address', 'uint256'],
155+
[this.dkim.target, accountSalt, this.zkEmailVerifier.target, templateId],
156+
),
157+
]),
158+
);
159+
160+
// Override the signUserOp function to use the ZKEmail signer
161+
this.signUserOp = async userOp => {
162+
const hash = await userOp.hash();
163+
return Object.assign(userOp, { signature: this.signer.signingKey.sign(hash).serialized });
164+
};
165+
});
166+
167+
shouldBehaveLikeAccountCore();
168+
shouldBehaveLikeAccountHolder();
169+
shouldBehaveLikeERC1271({ erc7739: true });
170+
shouldBehaveLikeERC7821();
171+
});
106172
});

0 commit comments

Comments
 (0)