-
Notifications
You must be signed in to change notification settings - Fork 17
ERC-7913 signature verifier for ZKEmail #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
27d2e52
documentation
Amxx a0d9c1d
Add ERC7913SignatureVerifierZKEmail
Amxx b24564c
Update ERC7913SignatureVerifierZKEmail.sol
Amxx e2f86b9
Apply ZKEmailUtils changes to ERC7913SignatureVerifierZKEmail
Amxx 4a10716
Rebased
ernestognw 330bedf
Review and testsd
ernestognw 359af07
Pragma up
ernestognw 238b86d
review
ernestognw 214188d
Add templateId and verifier as part of the key in ERC7913SignatureVer…
ernestognw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
112 changes: 112 additions & 0 deletions
112
contracts/utils/cryptography/ERC7913SignatureVerifierZKEmail.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.24; | ||
|
||
import {IDKIMRegistry} from "@zk-email/contracts/DKIMRegistry.sol"; | ||
import {IVerifier} from "@zk-email/email-tx-builder/interfaces/IVerifier.sol"; | ||
import {EmailAuthMsg} from "@zk-email/email-tx-builder/interfaces/IEmailTypes.sol"; | ||
import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol"; | ||
import {ZKEmailUtils} from "./ZKEmailUtils.sol"; | ||
|
||
/** | ||
* @dev ERC-7913 signature verifier that supports ZKEmail accounts. | ||
* | ||
* This contract verifies signatures produced through ZKEmail's zero-knowledge | ||
* proofs which allows users to authenticate using their email addresses. | ||
* | ||
* The key decoding logic is customizable: users may override the {_decodeKey} function | ||
* to enforce restrictions or validation on the decoded values (e.g., requiring a specific | ||
* verifier, templateId, or registry). To remain compliant with ERC-7913's statelessness, | ||
* it is recommended to enforce such restrictions using immutable variables only. | ||
* | ||
* Example of overriding _decodeKey to enforce a specific verifier, registry, (or templateId): | ||
* | ||
* ```solidity | ||
* function _decodeKey(bytes calldata key) internal view override returns ( | ||
* IDKIMRegistry registry, | ||
* bytes32 accountSalt, | ||
* IVerifier verifier, | ||
* uint256 templateId | ||
* ) { | ||
* (registry, accountSalt, verifier, templateId) = super._decodeKey(key); | ||
* require(verifier == _verifier, "Invalid verifier"); | ||
* require(registry == _registry, "Invalid registry"); | ||
* return (registry, accountSalt, verifier, templateId); | ||
* } | ||
* ``` | ||
*/ | ||
abstract contract ERC7913SignatureVerifierZKEmail is IERC7913SignatureVerifier { | ||
using ZKEmailUtils for EmailAuthMsg; | ||
|
||
/** | ||
* @dev Verifies a zero-knowledge proof of an email signature validated by a {DKIMRegistry} contract. | ||
* | ||
* The key format is ABI-encoded (IDKIMRegistry, bytes32, IVerifier, uint256) where: | ||
* | ||
* * IDKIMRegistry: The registry contract that validates DKIM public key hashes | ||
* * bytes32: The account salt that uniquely identifies the user's email address | ||
* * IVerifier: The verifier contract instance for ZK proof verification. | ||
* * uint256: The template ID for the command | ||
* | ||
* See {_decodeKey} for the key encoding format. | ||
* | ||
* The signature is an ABI-encoded {ZKEmailUtils-EmailAuthMsg} struct containing | ||
* the command parameters, template ID, and proof details. | ||
* | ||
* Signature encoding: | ||
* | ||
* ```solidity | ||
* bytes memory signature = abi.encode(EmailAuthMsg({ | ||
* templateId: 1, | ||
* commandParams: [hash], | ||
* proof: { | ||
* domainName: "example.com", // The domain name of the email sender | ||
* publicKeyHash: bytes32(0x...), // Hash of the DKIM public key used to sign the email | ||
* timestamp: block.timestamp, // When the email was sent | ||
* maskedCommand: "Sign hash", // The command being executed, with sensitive data masked | ||
* emailNullifier: bytes32(0x...), // Unique identifier for the email to prevent replay attacks | ||
* accountSalt: bytes32(0x...), // Unique identifier derived from email and account code | ||
* isCodeExist: true, // Whether the account code exists in the proof | ||
* proof: bytes(0x...) // The zero-knowledge proof verifying the email's authenticity | ||
* } | ||
* })); | ||
* ``` | ||
*/ | ||
function verify( | ||
bytes calldata key, | ||
bytes32 hash, | ||
bytes calldata signature | ||
) public view virtual override returns (bytes4) { | ||
(IDKIMRegistry registry_, bytes32 accountSalt_, IVerifier verifier_, uint256 templateId_) = abi.decode( | ||
key, | ||
(IDKIMRegistry, bytes32, IVerifier, uint256) | ||
); | ||
EmailAuthMsg memory emailAuthMsg = abi.decode(signature, (EmailAuthMsg)); | ||
|
||
return | ||
(abi.decode(emailAuthMsg.commandParams[0], (bytes32)) == hash && | ||
emailAuthMsg.templateId == templateId_ && | ||
emailAuthMsg.proof.accountSalt == accountSalt_ && | ||
emailAuthMsg.isValidZKEmail(registry_, verifier_) == ZKEmailUtils.EmailProofError.NoError) | ||
? IERC7913SignatureVerifier.verify.selector | ||
: bytes4(0xffffffff); | ||
} | ||
|
||
/** | ||
* @dev Decodes the key into its components. | ||
* | ||
* ```solidity | ||
* bytes memory key = abi.encode(registry, accountSalt, verifier, templateId); | ||
* ``` | ||
*/ | ||
function _decodeKey( | ||
bytes calldata key | ||
) | ||
internal | ||
view | ||
virtual | ||
returns (IDKIMRegistry registry, bytes32 accountSalt, IVerifier verifier, uint256 templateId) | ||
{ | ||
return abi.decode(key, (IDKIMRegistry, bytes32, IVerifier, uint256)); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ 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 { NonNativeSigner, P256SigningKey, RSASHA256SigningKey, ZKEmailSigningKey } = require('../helpers/signers'); | ||
const { PackedUserOperation } = require('../helpers/eip712-types'); | ||
|
||
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior'); | ||
|
@@ -15,15 +15,36 @@ const signerECDSA = ethers.Wallet.createRandom(); | |
const signerP256 = new NonNativeSigner(P256SigningKey.random()); | ||
const signerRSA = new NonNativeSigner(RSASHA256SigningKey.random()); | ||
|
||
// Constants for ZKEmail | ||
const accountSalt = '0x046582bce36cdd0a8953b9d40b8f20d58302bacf3bcecffeb6741c98a52725e2'; // keccak256("[email protected]") | ||
const selector = '12345'; | ||
const domainName = 'gmail.com'; | ||
const publicKeyHash = '0x0ea9c777dc7110e5a9e89b13f0cfc540e3845ba120b2b6dc24024d61488d4788'; | ||
const emailNullifier = '0x00a83fce3d4b1c9ef0f600644c1ecc6c8115b57b1596e0e3295e2c5105fbfd8a'; | ||
const templateId = ethers.solidityPackedKeccak256(['string', 'uint256'], ['TEST', 0n]); | ||
|
||
// Minimal fixture common to the different signer verifiers | ||
async function fixture() { | ||
// EOAs and environment | ||
const [beneficiary, other] = await ethers.getSigners(); | ||
const [admin, beneficiary, other] = await ethers.getSigners(); | ||
const target = await ethers.deployContract('CallReceiverMockExtended'); | ||
|
||
// DKIM Registry for ZKEmail | ||
const dkim = await ethers.deployContract('ECDSAOwnedDKIMRegistry'); | ||
await dkim.initialize(admin, admin); | ||
await dkim | ||
.SET_PREFIX() | ||
.then(prefix => dkim.computeSignedMsg(prefix, domainName, publicKeyHash)) | ||
.then(message => admin.signMessage(message)) | ||
.then(signature => dkim.setDKIMPublicKeyHash(selector, domainName, publicKeyHash, signature)); | ||
|
||
// ZKEmail Verifier | ||
const zkEmailVerifier = await ethers.deployContract('ZKEmailVerifierMock'); | ||
|
||
// ERC-7913 verifiers | ||
const verifierP256 = await ethers.deployContract('ERC7913SignatureVerifierP256'); | ||
const verifierRSA = await ethers.deployContract('ERC7913SignatureVerifierRSA'); | ||
const verifierZKEmail = await ethers.deployContract('$ERC7913SignatureVerifierZKEmail'); | ||
|
||
// ERC-4337 env | ||
const helper = new ERC4337Helper(); | ||
|
@@ -43,7 +64,20 @@ async function fixture() { | |
.then(signature => Object.assign(userOp, { signature })); | ||
}; | ||
|
||
return { helper, verifierP256, verifierRSA, domain, target, beneficiary, other, makeMock, signUserOp }; | ||
return { | ||
helper, | ||
verifierP256, | ||
verifierRSA, | ||
verifierZKEmail, | ||
dkim, | ||
zkEmailVerifier, | ||
domain, | ||
target, | ||
beneficiary, | ||
other, | ||
makeMock, | ||
signUserOp, | ||
}; | ||
} | ||
|
||
describe('AccountERC7913', function () { | ||
|
@@ -103,4 +137,36 @@ describe('AccountERC7913', function () { | |
shouldBehaveLikeERC1271({ erc7739: true }); | ||
shouldBehaveLikeERC7821(); | ||
}); | ||
|
||
// Using ZKEmail with an ERC-7913 verifier | ||
describe('ZKEmail', function () { | ||
beforeEach(async function () { | ||
// Create ZKEmail signer | ||
this.signer = new NonNativeSigner( | ||
new ZKEmailSigningKey(domainName, publicKeyHash, emailNullifier, accountSalt, templateId), | ||
); | ||
|
||
// Create account with ZKEmail verifier | ||
this.mock = await this.makeMock( | ||
ethers.concat([ | ||
this.verifierZKEmail.target, | ||
ethers.AbiCoder.defaultAbiCoder().encode( | ||
['address', 'bytes32', 'address', 'uint256'], | ||
[this.dkim.target, accountSalt, this.zkEmailVerifier.target, templateId], | ||
), | ||
]), | ||
); | ||
|
||
// Override the signUserOp function to use the ZKEmail signer | ||
this.signUserOp = async userOp => { | ||
const hash = await userOp.hash(); | ||
return Object.assign(userOp, { signature: this.signer.signingKey.sign(hash).serialized }); | ||
}; | ||
}); | ||
|
||
shouldBehaveLikeAccountCore(); | ||
shouldBehaveLikeAccountHolder(); | ||
shouldBehaveLikeERC1271({ erc7739: true }); | ||
shouldBehaveLikeERC7821(); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I missed this change from the last PR regarding the use of
ECDSAOwnedDKIMRegistry
.We should actually use
UserOverrideableDKIMRegistry
which comes from@zk-email/contracts
.We soft deprecated
ECDSAOwnedDKIMRegistry
in favour ofUserOverrideableDKIMRegistry
as the latter is overridable and thus provides an escape hatch to users. We should make a stronger note of this in our repo/docsSince
UserOverrideableDKIMRegistry
is the contract we would use, would it be possible to update all references ofECDSAOwnedDKIMRegistry
to the that instead?Example import
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried using the
UserOverrideableDKIMRegistry
but it has an initializer and the constructor is already set. I think using ECDSAOwnedDKIMRegistry in the tests is fine. Note that the actual ZKEmailUtils and SignerZKEmail only require theIDKIMRegistry
interface, so both should be compatible.It's something we can also emphasize more in our contracts. For example, giving concrete instructions to use the
UserOverrideableDKIMRegistry
wherever theIDKIMRegistry
is declared, wdyt?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this point, can you clarify?
The main point I would make is that we should not use
ECDSAOwnedDKIMRegistry
in the production contracts and examples. For tests I am not as opinionated about so happy to go with your call, as long as there are explicit docs that stateUserOverrideableDKIMRegistry
is the registry used in productionUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, so the
UserOverrideableDKIMRegistry
requires initialization and usesonlyInitializing
through__Ownable_init
, which complicates the tests setup. I think I should either be able to write true to the_initializing
bool inInitializable
(the other alternative is a top-level call). Creating a proxy has the same issue, we could work around it if theUserOverrideableDKIMRegistry
was abstract so we can initialize during construction.Yeah, that makes sense. I'm happy to note it in the documentation in a follow up PR 😄