Skip to content

Commit 1486330

Browse files
committed
Merge branch 'feature/zk-email' into feature/zk-email-7913
2 parents 7b8e7c3 + 190f0b3 commit 1486330

File tree

6 files changed

+215
-49
lines changed

6 files changed

+215
-49
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {Account} from "../../account/Account.sol";
6+
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
7+
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
8+
import {ERC7739, EIP712} from "../../utils/cryptography/ERC7739.sol";
9+
import {ERC7821} from "../../account/extensions/ERC7821.sol";
10+
import {SignerZKEmail} from "../../utils/cryptography/SignerZKEmail.sol";
11+
import {IDKIMRegistry} from "@zk-email/contracts/DKIMRegistry.sol";
12+
import {IVerifier} from "@zk-email/email-tx-builder/interfaces/IVerifier.sol";
13+
14+
contract AccountZKEmailMock is Account, SignerZKEmail, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
15+
constructor(
16+
bytes32 accountSalt_,
17+
IDKIMRegistry registry_,
18+
IVerifier verifier_,
19+
uint256 templateId_
20+
) EIP712("AccountZKEmailMock", "1") {
21+
_setAccountSalt(accountSalt_);
22+
_setDKIMRegistry(registry_);
23+
_setVerifier(verifier_);
24+
_setTemplateId(templateId_);
25+
}
26+
27+
/// @inheritdoc ERC7821
28+
function _erc7821AuthorizedExecutor(
29+
address caller,
30+
bytes32 mode,
31+
bytes calldata executionData
32+
) internal view virtual override returns (bool) {
33+
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
34+
}
35+
}

contracts/mocks/docs/account/MyAccountZKEmail.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ contract MyAccountZKEmail is Account, SignerZKEmail, ERC7739, ERC7821, ERC721Hol
2626
_setAccountSalt(accountSalt_);
2727
_setDKIMRegistry(registry_);
2828
_setVerifier(verifier_);
29-
_setCommandTemplate(templateId_);
29+
_setTemplateId(templateId_);
3030
}
3131

3232
/// @dev Allows the entry point as an authorized executor.

contracts/utils/cryptography/SignerZKEmail.sol

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {ZKEmailUtils} from "./ZKEmailUtils.sol";
2121
* * {accountSalt} - A unique identifier derived from the user's email address and account code.
2222
* * {DKIMRegistry} - An instance of the DKIM registry contract for domain verification.
2323
* * {verifier} - An instance of the Verifier contract for zero-knowledge proof validation.
24-
* * {commandTemplate} - The template ID of the sign hash command, defining the expected format.
24+
* * {templateId} - The template ID of the sign hash command, defining the expected format.
2525
*
2626
* Example of usage:
2727
*
@@ -32,12 +32,12 @@ import {ZKEmailUtils} from "./ZKEmailUtils.sol";
3232
* _setAccountSalt(accountSalt);
3333
* _setDKIMRegistry(registry);
3434
* _setVerifier(verifier);
35-
* _setCommandTemplate(templateId);
35+
* _setTemplateId(templateId);
3636
* }
3737
* }
3838
* ```
3939
*
40-
* IMPORTANT: Avoiding to call {_setAccountSalt}, {_setDKIMRegistry}, {_setVerifier} and {_setCommandTemplate}
40+
* IMPORTANT: Avoiding to call {_setAccountSalt}, {_setDKIMRegistry}, {_setVerifier} and {_setTemplateId}
4141
* either during construction (if used standalone) or during initialization (if used as a clone) may
4242
* leave the signer either front-runnable or unusable.
4343
*/
@@ -47,7 +47,7 @@ abstract contract SignerZKEmail is AbstractSigner {
4747
bytes32 private _accountSalt;
4848
IDKIMRegistry private _registry;
4949
IVerifier private _verifier;
50-
uint256 private _commandTemplate;
50+
uint256 private _templateId;
5151

5252
/// @dev Proof verification error.
5353
error InvalidEmailProof(ZKEmailUtils.EmailProofError err);
@@ -84,9 +84,9 @@ abstract contract SignerZKEmail is AbstractSigner {
8484
return _verifier;
8585
}
8686

87-
/// @dev The templateId of the sign hash command.
88-
function commandTemplate() public view virtual returns (uint256) {
89-
return _commandTemplate;
87+
/// @dev The command template of the sign hash command.
88+
function templateId() public view virtual returns (uint256) {
89+
return _templateId;
9090
}
9191

9292
/// @dev Set the {accountSalt}.
@@ -104,9 +104,9 @@ abstract contract SignerZKEmail is AbstractSigner {
104104
_verifier = verifier_;
105105
}
106106

107-
/// @dev Set the {commandTemplate} ID.
108-
function _setCommandTemplate(uint256 commandTemplate_) internal virtual {
109-
_commandTemplate = commandTemplate_;
107+
/// @dev Set the command's {templateId}.
108+
function _setTemplateId(uint256 templateId_) internal virtual {
109+
_templateId = templateId_;
110110
}
111111

112112
/**
@@ -117,6 +117,9 @@ abstract contract SignerZKEmail is AbstractSigner {
117117
* to prevent replay attacks, similar to how nonces are used with ECDSA signatures.
118118
*/
119119
function verifyEmail(EmailAuthMsg memory emailAuthMsg) public view virtual {
120+
if (emailAuthMsg.templateId != templateId() || emailAuthMsg.proof.accountSalt != accountSalt()) {
121+
revert InvalidEmailProof(ZKEmailUtils.EmailProofError.EmailProof);
122+
}
120123
ZKEmailUtils.EmailProofError err = emailAuthMsg.isValidZKEmail(DKIMRegistry(), verifier());
121124
if (err != ZKEmailUtils.EmailProofError.NoError) revert InvalidEmailProof(err);
122125
}
@@ -136,7 +139,7 @@ abstract contract SignerZKEmail is AbstractSigner {
136139
) internal view virtual override returns (bool) {
137140
EmailAuthMsg memory emailAuthMsg = abi.decode(signature, (EmailAuthMsg));
138141
return (abi.decode(emailAuthMsg.commandParams[0], (bytes32)) == hash &&
139-
emailAuthMsg.templateId == commandTemplate() &&
142+
emailAuthMsg.templateId == templateId() &&
140143
emailAuthMsg.proof.accountSalt == accountSalt() &&
141144
emailAuthMsg.isValidZKEmail(DKIMRegistry(), verifier()) == ZKEmailUtils.EmailProofError.NoError);
142145
}

contracts/utils/cryptography/ZKEmailUtils.sol

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {AbstractSigner} from "./AbstractSigner.sol";
2626
* mechanism to ensure the email was actually sent and received without revealing its contents. Defined by an `IVerifier` interface.
2727
*/
2828
library ZKEmailUtils {
29+
using CommandUtils for bytes[];
2930
using Strings for string;
3031

3132
/// @dev Enumeration of possible email proof validation errors.
@@ -34,62 +35,84 @@ library ZKEmailUtils {
3435
DKIMPublicKeyHash, // The DKIM public key hash verification fails
3536
MaskedCommandLength, // The masked command length exceeds the maximum
3637
SkippedCommandPrefixSize, // The skipped command prefix size is invalid
37-
Command, // The command format is invalid
38+
MismatchedCommand, // The command does not match the proof command
3839
EmailProof // The email proof verification fails
3940
}
4041

42+
/// @dev Enumeration of possible string cases used to compare the command with the expected proven command.
43+
enum Case {
44+
LOWERCASE, // Converts the command to hex lowercase.
45+
UPPERCASE, // Converts the command to hex uppercase.
46+
CHECKSUM, // Computes a checksum of the command.
47+
ANY
48+
}
49+
50+
/// @dev Variant of {isValidZKEmail} that validates the `["signHash", "{uint}"]` command template.
51+
function isValidZKEmail(
52+
EmailAuthMsg memory emailAuthMsg,
53+
IDKIMRegistry dkimregistry,
54+
IVerifier verifier
55+
) internal view returns (EmailProofError) {
56+
string[] memory signHashTemplate = new string[](2);
57+
signHashTemplate[0] = "signHash";
58+
signHashTemplate[1] = CommandUtils.UINT_MATCHER;
59+
60+
// UINT_MATCHER is always lowercase
61+
return isValidZKEmail(emailAuthMsg, dkimregistry, verifier, signHashTemplate, Case.LOWERCASE);
62+
}
63+
4164
/**
42-
* @dev Validates a ZK-Email authentication message.
43-
*
44-
* Requirements:
45-
*
46-
* - The DKIM public key hash must be valid according to the registry
47-
* - The masked command length must not exceed the verifier's limit
48-
* - The skipped command prefix size must be valid
49-
* - The command format and parameters must be valid
50-
* - The email proof must be verified by the verifier
65+
* @dev Validates a ZKEmail authentication message.
5166
*
5267
* This function takes an email authentication message, a DKIM registry contract, and a verifier contract
5368
* as inputs. It performs several validation checks and returns a tuple containing a boolean success flag
54-
* and an {EmailProofError} if validation failed. The function will return true with {EmailProofError.NoError}
55-
* if all validations pass, or false with a specific {EmailProofError} indicating which validation check failed.
69+
* and an {EmailProofError} if validation failed. Returns {EmailProofError.NoError} if all validations pass,
70+
* or false with a specific {EmailProofError} indicating which validation check failed.
5671
*
57-
* NOTE: Validates `["signHash", "{uint}"]` command template by default.
72+
* NOTE: Attempts to validate the command for all possible string {Case} values.
5873
*/
5974
function isValidZKEmail(
6075
EmailAuthMsg memory emailAuthMsg,
6176
IDKIMRegistry dkimregistry,
62-
IVerifier verifier
77+
IVerifier verifier,
78+
string[] memory template
79+
) internal view returns (EmailProofError) {
80+
return isValidZKEmail(emailAuthMsg, dkimregistry, verifier, template, Case.ANY);
81+
}
82+
83+
/// @dev Variant of {isValidZKEmail} that validates a template with a specific string {Case}.
84+
function isValidZKEmail(
85+
EmailAuthMsg memory emailAuthMsg,
86+
IDKIMRegistry dkimregistry,
87+
IVerifier verifier,
88+
string[] memory template,
89+
Case stringCase
6390
) internal view returns (EmailProofError) {
6491
if (!dkimregistry.isDKIMPublicKeyHashValid(emailAuthMsg.proof.domainName, emailAuthMsg.proof.publicKeyHash)) {
6592
return EmailProofError.DKIMPublicKeyHash;
6693
} else if (bytes(emailAuthMsg.proof.maskedCommand).length > verifier.commandBytes()) {
6794
return EmailProofError.MaskedCommandLength;
6895
} else if (emailAuthMsg.skippedCommandPrefix >= verifier.commandBytes()) {
6996
return EmailProofError.SkippedCommandPrefixSize;
97+
} else if (!_commandMatch(emailAuthMsg, template, stringCase)) {
98+
return EmailProofError.MismatchedCommand;
7099
} else {
71-
string[] memory signHashTemplate = new string[](2);
72-
signHashTemplate[0] = "signHash";
73-
signHashTemplate[1] = CommandUtils.UINT_MATCHER;
74-
75-
// Construct an expectedCommand from template and the values of emailAuthMsg.commandParams.
76-
string memory trimmedMaskedCommand = CommandUtils.removePrefix(
77-
emailAuthMsg.proof.maskedCommand,
78-
emailAuthMsg.skippedCommandPrefix
79-
);
80-
for (uint256 stringCase = 0; stringCase < 3; stringCase++) {
81-
if (
82-
CommandUtils.computeExpectedCommand(emailAuthMsg.commandParams, signHashTemplate, stringCase).equal(
83-
trimmedMaskedCommand
84-
)
85-
) {
86-
return
87-
verifier.verifyEmailProof(emailAuthMsg.proof)
88-
? EmailProofError.NoError
89-
: EmailProofError.EmailProof;
90-
}
91-
}
92-
return EmailProofError.Command;
100+
return verifier.verifyEmailProof(emailAuthMsg.proof) ? EmailProofError.NoError : EmailProofError.EmailProof;
93101
}
94102
}
103+
104+
function _commandMatch(
105+
EmailAuthMsg memory emailAuthMsg,
106+
string[] memory template,
107+
Case stringCase
108+
) private pure returns (bool) {
109+
bytes[] memory commandParams = emailAuthMsg.commandParams; // Not a memory copy
110+
string memory maskedCommand = emailAuthMsg.proof.maskedCommand; // Not a memory copy
111+
return
112+
stringCase == Case.ANY
113+
? (commandParams.computeExpectedCommand(template, uint8(Case.LOWERCASE)).equal(maskedCommand) ||
114+
commandParams.computeExpectedCommand(template, uint8(Case.UPPERCASE)).equal(maskedCommand) ||
115+
commandParams.computeExpectedCommand(template, uint8(Case.CHECKSUM)).equal(maskedCommand))
116+
: commandParams.computeExpectedCommand(template, uint8(stringCase)).equal(maskedCommand);
117+
}
95118
}

docs/modules/ROOT/pages/account-abstraction.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ To use this signer, developers must set up several components during initializat
7272
* Deterministic Address Generation: Enables the creation of deterministic addresses based on email addresses
7373
* **DKIMRegistry**: An instance of the DKIM registry contract for domain verification
7474
* **verifier**: An instance of the Verifier contract for zero-knowledge proof validation
75-
* **commandTemplate**: The template ID of the sign hash command, defining the expected format
75+
* **templateId**: The command template of the sign hash command, defining the expected format
7676

7777
[source,solidity]
7878
----

test/account/AccountZKEmail.t.sol

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {Test} from "forge-std/Test.sol";
5+
import {AccountZKEmailMock} from "../../contracts/mocks/account/AccountZKEmailMock.sol";
6+
import {SignerZKEmail, IDKIMRegistry, IVerifier, EmailAuthMsg, EmailProof} from "../../contracts/utils/cryptography/SignerZKEmail.sol";
7+
import {CallReceiverMockExtended} from "../../contracts/mocks/CallReceiverMock.sol";
8+
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
9+
10+
contract AccountZKEmailTest is Test {
11+
using Strings for string;
12+
13+
AccountZKEmailMock private _account;
14+
CallReceiverMockExtended private _target;
15+
IDKIMRegistry private _dkimRegistry;
16+
IVerifier private _verifier;
17+
bytes32 private _accountSalt;
18+
uint256 private _templateId;
19+
bytes32 private _publicKeyHash;
20+
bytes32 private _emailNullifier;
21+
bytes private _mockProof;
22+
23+
function setUp() public {
24+
// Deploy DKIM Registry
25+
_dkimRegistry = IDKIMRegistry(address(new MockDKIMRegistry()));
26+
27+
// Deploy Verifier
28+
_verifier = IVerifier(address(new MockVerifier()));
29+
30+
// Generate test data
31+
_accountSalt = keccak256("[email protected]");
32+
_templateId = 1;
33+
_publicKeyHash = keccak256("publicKey");
34+
_emailNullifier = keccak256("emailNullifier");
35+
_mockProof = abi.encodePacked(bytes1(0x01));
36+
37+
// Deploy account
38+
_account = new AccountZKEmailMock(_accountSalt, _dkimRegistry, _verifier, _templateId);
39+
40+
// Deploy target
41+
_target = new CallReceiverMockExtended();
42+
}
43+
44+
function buildEmailAuthMsg(bytes32 hash) public returns (EmailAuthMsg memory emailAuthMsg) {
45+
bytes[] memory commandParams = new bytes[](1);
46+
commandParams[0] = abi.encode(hash);
47+
48+
EmailProof memory emailProof = EmailProof({
49+
domainName: "gmail.com",
50+
publicKeyHash: _publicKeyHash,
51+
timestamp: block.timestamp,
52+
maskedCommand: string.concat("signHash ", Strings.toString(uint256(hash))),
53+
emailNullifier: _emailNullifier,
54+
accountSalt: _accountSalt,
55+
isCodeExist: true,
56+
proof: _mockProof
57+
});
58+
59+
emailAuthMsg = EmailAuthMsg({
60+
templateId: _templateId,
61+
commandParams: commandParams,
62+
skippedCommandPrefix: 0,
63+
proof: emailProof
64+
});
65+
66+
// Setup mock verifier
67+
MockVerifier(address(_verifier)).setCommandBytes(1000);
68+
MockDKIMRegistry(address(_dkimRegistry)).setValidPublicKeyHash("gmail.com", _publicKeyHash);
69+
}
70+
71+
function testVerifyEmail(bytes32 hash) public {
72+
EmailAuthMsg memory emailAuthMsg = buildEmailAuthMsg(hash);
73+
_account.verifyEmail(emailAuthMsg);
74+
}
75+
}
76+
77+
// Mock DKIM Registry for testing
78+
contract MockDKIMRegistry is IDKIMRegistry {
79+
mapping(string => bytes32) private _validPublicKeyHashes;
80+
81+
function setValidPublicKeyHash(string memory domainName, bytes32 publicKeyHash) public {
82+
_validPublicKeyHashes[domainName] = publicKeyHash;
83+
}
84+
85+
function isDKIMPublicKeyHashValid(string memory domainName, bytes32 publicKeyHash) external view returns (bool) {
86+
return _validPublicKeyHashes[domainName] == publicKeyHash;
87+
}
88+
}
89+
90+
// Mock Verifier for testing
91+
contract MockVerifier is IVerifier {
92+
uint256 private _commandBytes;
93+
94+
function setCommandBytes(uint256 commandBytes_) public {
95+
_commandBytes = commandBytes_;
96+
}
97+
98+
function commandBytes() external view returns (uint256) {
99+
return _commandBytes;
100+
}
101+
102+
function verifyEmailProof(EmailProof memory /* proof */) external pure returns (bool) {
103+
return true; // Always return true for testing
104+
}
105+
}

0 commit comments

Comments
 (0)