Skip to content

Commit b0c00eb

Browse files
authored
Remove templateId and simplify EmailProof validation in ZKEmailUtils (#210)
1 parent 7ccb105 commit b0c00eb

File tree

11 files changed

+601
-390
lines changed

11 files changed

+601
-390
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 14-08-2025
2+
3+
- `ZKEmailUtils`: Add `tryDecodeEmailProof` function for safe calldata decoding with comprehensive bounds checking and validation for `EmailProof` struct.
4+
- `ZKEmailUtils`: Update `isValidZKEmail` to receive `EmailProof` struct directly instead of `EmailAuthMsg` struct.
5+
- `SignerZKEmail`: Remove `templateId` functionality and switch from `EmailAuthMsg` to direct `EmailProof` validation for streamlined signature verification.
6+
- `ERC7913ZKEmailVerifier`: Remove `templateId` from signature validation logic and update `_decodeKey` function to directly decode `EmailProof` struct.
7+
18
## 09-08-2025
29

310
- `ZKEmailUtils`: Simplify library implementation and remove `Verifier.sol` indirection for cleaner integration with a Groth16Verifier.

contracts/mocks/account/AccountZKEmailMock.sol

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,11 @@ contract AccountZKEmailMock is Account, SignerZKEmail, ERC7739, ERC7821, ERC721H
1515
constructor(
1616
bytes32 accountSalt_,
1717
IDKIMRegistry registry_,
18-
IGroth16Verifier groth16Verifier_,
19-
uint256 templateId_
18+
IGroth16Verifier groth16Verifier_
2019
) EIP712("AccountZKEmailMock", "1") {
2120
_setAccountSalt(accountSalt_);
2221
_setDKIMRegistry(registry_);
2322
_setVerifier(groth16Verifier_);
24-
_setTemplateId(templateId_);
2523
}
2624

2725
/// @inheritdoc ERC7821

contracts/utils/cryptography/ZKEmailUtils.sol

Lines changed: 100 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol";
66
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
77
import {IDKIMRegistry} from "@zk-email/contracts/DKIMRegistry.sol";
88
import {IGroth16Verifier} from "@zk-email/email-tx-builder/src/interfaces/IGroth16Verifier.sol";
9-
import {EmailAuthMsg, EmailProof} from "@zk-email/email-tx-builder/src/interfaces/IEmailTypes.sol";
9+
import {EmailProof} from "@zk-email/email-tx-builder/src/interfaces/IEmailTypes.sol";
1010
import {CommandUtils} from "@zk-email/email-tx-builder/src/libraries/CommandUtils.sol";
1111

1212
/**
@@ -43,7 +43,6 @@ library ZKEmailUtils {
4343
NoError,
4444
DKIMPublicKeyHash, // The DKIM public key hash verification fails
4545
MaskedCommandLength, // The masked command length exceeds the maximum
46-
SkippedCommandPrefixSize, // The skipped command prefix size is invalid
4746
MismatchedCommand, // The command does not match the proof command
4847
InvalidFieldPoint, // The Groth16 field point is invalid
4948
EmailProof // The email proof verification fails
@@ -59,33 +58,38 @@ library ZKEmailUtils {
5958

6059
/// @dev Variant of {isValidZKEmail} that validates the `["signHash", "{uint}"]` command template.
6160
function isValidZKEmail(
62-
EmailAuthMsg memory emailAuthMsg,
61+
EmailProof memory emailProof,
6362
IDKIMRegistry dkimregistry,
64-
IGroth16Verifier groth16Verifier
63+
IGroth16Verifier groth16Verifier,
64+
bytes32 hash
6565
) internal view returns (EmailProofError) {
6666
string[] memory signHashTemplate = new string[](2);
6767
signHashTemplate[0] = "signHash";
6868
signHashTemplate[1] = CommandUtils.UINT_MATCHER; // UINT_MATCHER is always lowercase
69-
return isValidZKEmail(emailAuthMsg, dkimregistry, groth16Verifier, signHashTemplate, Case.LOWERCASE);
69+
bytes[] memory signHashParams = new bytes[](1);
70+
signHashParams[0] = abi.encode(hash);
71+
return
72+
isValidZKEmail(emailProof, dkimregistry, groth16Verifier, signHashTemplate, signHashParams, Case.LOWERCASE);
7073
}
7174

7275
/**
73-
* @dev Validates a ZKEmail authentication message.
76+
* @dev Validates a ZKEmail proof against a command template.
7477
*
75-
* This function takes an email authentication message, a DKIM registry contract, and a verifier contract
76-
* as inputs. It performs several validation checks and returns a tuple containing a boolean success flag
77-
* and an {EmailProofError} if validation failed. Returns {EmailProofError.NoError} if all validations pass,
78-
* or false with a specific {EmailProofError} indicating which validation check failed.
78+
* This function takes an email proof, a DKIM registry contract, and a verifier contract
79+
* as inputs. It performs several validation checks and returns an {EmailProofError} indicating the result.
80+
* Returns {EmailProofError.NoError} if all validations pass, or a specific {EmailProofError} indicating
81+
* which validation check failed.
7982
*
8083
* NOTE: Attempts to validate the command for all possible string {Case} values.
8184
*/
8285
function isValidZKEmail(
83-
EmailAuthMsg memory emailAuthMsg,
86+
EmailProof memory emailProof,
8487
IDKIMRegistry dkimregistry,
8588
IGroth16Verifier groth16Verifier,
86-
string[] memory template
89+
string[] memory template,
90+
bytes[] memory templateParams
8791
) internal view returns (EmailProofError) {
88-
return isValidZKEmail(emailAuthMsg, dkimregistry, groth16Verifier, template, Case.ANY);
92+
return isValidZKEmail(emailProof, dkimregistry, groth16Verifier, template, templateParams, Case.ANY);
8993
}
9094

9195
/**
@@ -94,66 +98,99 @@ library ZKEmailUtils {
9498
* Useful for templates with Ethereum address matchers (i.e. `{ethAddr}`), which are case-sensitive (e.g., `["someCommand", "{address}"]`).
9599
*/
96100
function isValidZKEmail(
97-
EmailAuthMsg memory emailAuthMsg,
101+
EmailProof memory emailProof,
98102
IDKIMRegistry dkimregistry,
99103
IGroth16Verifier groth16Verifier,
100104
string[] memory template,
105+
bytes[] memory templateParams,
101106
Case stringCase
102107
) internal view returns (EmailProofError) {
103-
if (emailAuthMsg.skippedCommandPrefix >= COMMAND_BYTES) {
104-
return EmailProofError.SkippedCommandPrefixSize;
105-
} else if (bytes(emailAuthMsg.proof.maskedCommand).length > COMMAND_BYTES) {
108+
if (bytes(emailProof.maskedCommand).length > COMMAND_BYTES) {
106109
return EmailProofError.MaskedCommandLength;
107-
} else if (!_commandMatch(emailAuthMsg, template, stringCase)) {
110+
} else if (!_commandMatch(emailProof, template, templateParams, stringCase)) {
108111
return EmailProofError.MismatchedCommand;
109-
} else if (
110-
!dkimregistry.isDKIMPublicKeyHashValid(emailAuthMsg.proof.domainName, emailAuthMsg.proof.publicKeyHash)
111-
) {
112+
} else if (!dkimregistry.isDKIMPublicKeyHashValid(emailProof.domainName, emailProof.publicKeyHash)) {
112113
return EmailProofError.DKIMPublicKeyHash;
113114
}
115+
114116
(uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC) = abi.decode(
115-
emailAuthMsg.proof.proof,
117+
emailProof.proof,
116118
(uint256[2], uint256[2][2], uint256[2])
117119
);
118-
119-
uint256 q = Q - 1; // upper bound of the field elements
120-
if (
121-
pA[0] > q ||
122-
pA[1] > q ||
123-
pB[0][0] > q ||
124-
pB[0][1] > q ||
125-
pB[1][0] > q ||
126-
pB[1][1] > q ||
127-
pC[0] > q ||
128-
pC[1] > q
129-
) return EmailProofError.InvalidFieldPoint;
120+
if (!_isValidFieldPoint(pA, pB, pC)) {
121+
return EmailProofError.InvalidFieldPoint;
122+
}
130123

131124
return
132-
groth16Verifier.verifyProof(pA, pB, pC, toPubSignals(emailAuthMsg.proof))
125+
groth16Verifier.verifyProof(pA, pB, pC, toPubSignals(emailProof))
133126
? EmailProofError.NoError
134127
: EmailProofError.EmailProof;
135128
}
136129

137-
/// @dev Compares the command in the email authentication message with the expected command.
130+
/**
131+
* @dev Verifies that calldata bytes (`input`) represents a valid `EmailProof` object. If encoding is valid,
132+
* returns true and the calldata view at the object. Otherwise, returns false and an invalid calldata object.
133+
*
134+
* NOTE: The returned `emailProof` object should not be accessed if `success` is false. Trying to access the data may
135+
* cause revert/panic.
136+
*/
137+
function tryDecodeEmailProof(
138+
bytes calldata input
139+
) internal pure returns (bool success, EmailProof calldata emailProof) {
140+
assembly ("memory-safe") {
141+
emailProof := input.offset
142+
}
143+
144+
// Minimum length to hold 8 objects (32 bytes each)
145+
if (input.length < 0x100) return (false, emailProof);
146+
147+
// Get offset of non-value-type elements relative to the input buffer
148+
uint256 domainNameOffset = uint256(bytes32(input[0x00:]));
149+
uint256 maskedCommandOffset = uint256(bytes32(input[0x60:]));
150+
uint256 proofOffset = uint256(bytes32(input[0xe0:]));
151+
152+
// The elements length (at the offset) should be 32 bytes long. We check that this is within the
153+
// buffer bounds. Since we know input.length is at least 32, we can subtract with no overflow risk.
154+
if (
155+
input.length - 0x20 < domainNameOffset ||
156+
input.length - 0x20 < maskedCommandOffset ||
157+
input.length - 0x20 < proofOffset
158+
) return (false, emailProof);
159+
160+
// Get the lengths. offset + 32 is bounded by input.length so it does not overflow.
161+
uint256 domainNameLength = uint256(bytes32(input[domainNameOffset:]));
162+
uint256 maskedCommandLength = uint256(bytes32(input[maskedCommandOffset:]));
163+
uint256 proofLength = uint256(bytes32(input[proofOffset:]));
164+
165+
// Check that the input buffer is long enough to store the non-value-type elements
166+
// Since we know input.length is at least xxxOffset + 32, we can subtract with no overflow risk.
167+
if (
168+
input.length - domainNameOffset - 0x20 < domainNameLength ||
169+
input.length - maskedCommandOffset - 0x20 < maskedCommandLength ||
170+
input.length - proofOffset - 0x20 < proofLength
171+
) return (false, emailProof);
172+
173+
return (true, emailProof);
174+
}
175+
176+
/// @dev Compares the command in the email proof with the expected command template.
138177
function _commandMatch(
139-
EmailAuthMsg memory emailAuthMsg,
178+
EmailProof memory proof,
140179
string[] memory template,
180+
bytes[] memory templateParams,
141181
Case stringCase
142182
) private pure returns (bool) {
143-
bytes[] memory commandParams = emailAuthMsg.commandParams; // Not a memory copy
144-
uint256 skippedCommandPrefix = emailAuthMsg.skippedCommandPrefix; // Not a memory copy
145-
string memory command = string(bytes(emailAuthMsg.proof.maskedCommand).slice(skippedCommandPrefix)); // Not a memory copy
146-
147183
if (stringCase != Case.ANY)
148-
return commandParams.computeExpectedCommand(template, uint8(stringCase)).equal(command);
184+
return templateParams.computeExpectedCommand(template, uint8(stringCase)).equal(proof.maskedCommand);
185+
149186
return
150-
commandParams.computeExpectedCommand(template, uint8(Case.LOWERCASE)).equal(command) ||
151-
commandParams.computeExpectedCommand(template, uint8(Case.UPPERCASE)).equal(command) ||
152-
commandParams.computeExpectedCommand(template, uint8(Case.CHECKSUM)).equal(command);
187+
templateParams.computeExpectedCommand(template, uint8(Case.LOWERCASE)).equal(proof.maskedCommand) ||
188+
templateParams.computeExpectedCommand(template, uint8(Case.UPPERCASE)).equal(proof.maskedCommand) ||
189+
templateParams.computeExpectedCommand(template, uint8(Case.CHECKSUM)).equal(proof.maskedCommand);
153190
}
154191

155192
/**
156-
* @dev Builds the expected public signals array for the Groth16 verifier from the given EmailAuthMsg.
193+
* @dev Builds the expected public signals array for the Groth16 verifier from the given EmailProof.
157194
*
158195
* Packs the domain, public key hash, email nullifier, timestamp, masked command, account salt, and isCodeExist fields
159196
* into a uint256 array in the order expected by the verifier circuit.
@@ -216,4 +253,21 @@ library ZKEmailUtils {
216253
}
217254
return fields;
218255
}
256+
257+
/// @dev Checks if the field points are valid in the range of [0, Q).
258+
function _isValidFieldPoint(
259+
uint256[2] memory pA,
260+
uint256[2][2] memory pB,
261+
uint256[2] memory pC
262+
) private pure returns (bool) {
263+
return
264+
pA[0] < Q &&
265+
pA[1] < Q &&
266+
pB[0][0] < Q &&
267+
pB[0][1] < Q &&
268+
pB[1][0] < Q &&
269+
pB[1][1] < Q &&
270+
pC[0] < Q &&
271+
pC[1] < Q;
272+
}
219273
}

contracts/utils/cryptography/signers/SignerZKEmail.sol

Lines changed: 14 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pragma solidity ^0.8.24;
44

55
import {IDKIMRegistry} from "@zk-email/contracts/DKIMRegistry.sol";
66
import {IGroth16Verifier} from "@zk-email/email-tx-builder/src/interfaces/IGroth16Verifier.sol";
7-
import {EmailAuthMsg} from "@zk-email/email-tx-builder/src/interfaces/IEmailTypes.sol";
7+
import {EmailProof} from "@zk-email/email-tx-builder/src/interfaces/IVerifier.sol";
88
import {AbstractSigner} from "@openzeppelin/contracts/utils/cryptography/signers/AbstractSigner.sol";
99
import {ZKEmailUtils} from "../ZKEmailUtils.sol";
1010

@@ -23,7 +23,6 @@ import {ZKEmailUtils} from "../ZKEmailUtils.sol";
2323
* * {accountSalt} - A unique identifier derived from the user's email address and account code.
2424
* * {DKIMRegistry} - An instance of the DKIM registry contract for domain verification.
2525
* * {verifier} - An instance of the Groth16Verifier contract for zero-knowledge proof validation.
26-
* * {templateId} - The template ID of the sign hash command, defining the expected format.
2726
*
2827
* Example of usage:
2928
*
@@ -32,29 +31,26 @@ import {ZKEmailUtils} from "../ZKEmailUtils.sol";
3231
* function initialize(
3332
* bytes32 accountSalt,
3433
* IDKIMRegistry registry,
35-
* IGroth16Verifier groth16Verifier,
36-
* uint256 templateId
34+
* IGroth16Verifier groth16Verifier
3735
* ) public initializer {
3836
* // Will revert if the signer is already initialized
3937
* _setAccountSalt(accountSalt);
4038
* _setDKIMRegistry(registry);
41-
* _setGroth16Verifier(groth16Verifier);
42-
* _setTemplateId(templateId);
39+
* _setVerifier(groth16Verifier);
4340
* }
4441
* }
4542
* ```
4643
*
47-
* IMPORTANT: Avoiding to call {_setAccountSalt}, {_setDKIMRegistry}, {_setVerifier} and {_setTemplateId}
44+
* IMPORTANT: Failing to call {_setAccountSalt}, {_setDKIMRegistry}, and {_setVerifier}
4845
* either during construction (if used standalone) or during initialization (if used as a clone) may
4946
* leave the signer either front-runnable or unusable.
5047
*/
5148
abstract contract SignerZKEmail is AbstractSigner {
52-
using ZKEmailUtils for EmailAuthMsg;
49+
using ZKEmailUtils for EmailProof;
5350

5451
bytes32 private _accountSalt;
5552
IDKIMRegistry private _registry;
5653
IGroth16Verifier private _groth16Verifier;
57-
uint256 private _templateId;
5854

5955
/// @dev Proof verification error.
6056
error InvalidEmailProof(ZKEmailUtils.EmailProofError err);
@@ -94,11 +90,6 @@ abstract contract SignerZKEmail is AbstractSigner {
9490
return _groth16Verifier;
9591
}
9692

97-
/// @dev The command template of the sign hash command.
98-
function templateId() public view virtual returns (uint256) {
99-
return _templateId;
100-
}
101-
10293
/// @dev Set the {accountSalt}.
10394
function _setAccountSalt(bytes32 accountSalt_) internal virtual {
10495
_accountSalt = accountSalt_;
@@ -114,47 +105,22 @@ abstract contract SignerZKEmail is AbstractSigner {
114105
_groth16Verifier = verifier_;
115106
}
116107

117-
/// @dev Set the command's {templateId}.
118-
function _setTemplateId(uint256 templateId_) internal virtual {
119-
_templateId = templateId_;
120-
}
121-
122108
/**
123109
* @dev See {AbstractSigner-_rawSignatureValidation}. Validates a raw signature by:
124110
*
125-
* 1. Decoding the email authentication message from the signature
126-
* 2. Verifying the hash matches the command parameters
127-
* 3. Checking the template ID matches
128-
* 4. Validating the account salt
129-
* 5. Verifying the email proof
111+
* 1. Decoding the email proof from the signature
112+
* 2. Validating the account salt matches
113+
* 3. Verifying the email proof using ZKEmail utilities
130114
*/
131115
function _rawSignatureValidation(
132116
bytes32 hash,
133117
bytes calldata signature
134118
) internal view virtual override returns (bool) {
135-
// Check if the signature is long enough to contain the EmailAuthMsg
136-
// The minimum length is 512 bytes (initial part + pointer offsets)
137-
// - `templateId` is a uint256 (32 bytes).
138-
// - `commandParams` is a dynamic array of bytes32 (32 bytes offset).
139-
// - `skippedCommandPrefixSize` is a uint256 (32 bytes).
140-
// - `proof` is a struct with the following fields (32 bytes offset):
141-
// - `domainName` is a dynamic string (32 bytes offset).
142-
// - `publicKeyHash` is a bytes32 (32 bytes).
143-
// - `timestamp` is a uint256 (32 bytes).
144-
// - `maskedCommand` is a dynamic string (32 bytes offset).
145-
// - `emailNullifier` is a bytes32 (32 bytes).
146-
// - `accountSalt` is a bytes32 (32 bytes).
147-
// - `isCodeExist` is a boolean, so its length is 1 byte padded to 32 bytes.
148-
// - `proof` is a dynamic bytes (32 bytes offset).
149-
// There are 128 bytes for the EmailAuthMsg type and 256 bytes for the proof.
150-
// Considering all dynamic elements are empty (i.e. `commandParams` = [], `domainName` = "", `maskedCommand` = "", `proof` = []),
151-
// then we have 128 bytes for the EmailAuthMsg type, 256 bytes for the proof and 4 * 32 for the length of the dynamic elements.
152-
// So the minimum length is 128 + 256 + 4 * 32 = 512 bytes.
153-
if (signature.length < 512) return false;
154-
EmailAuthMsg memory emailAuthMsg = abi.decode(signature, (EmailAuthMsg));
155-
return (abi.decode(emailAuthMsg.commandParams[0], (bytes32)) == hash &&
156-
emailAuthMsg.templateId == templateId() &&
157-
emailAuthMsg.proof.accountSalt == accountSalt() &&
158-
emailAuthMsg.isValidZKEmail(DKIMRegistry(), verifier()) == ZKEmailUtils.EmailProofError.NoError);
119+
(bool decodeSuccess, EmailProof calldata emailProof) = ZKEmailUtils.tryDecodeEmailProof(signature);
120+
121+
return
122+
decodeSuccess &&
123+
emailProof.accountSalt == accountSalt() &&
124+
emailProof.isValidZKEmail(DKIMRegistry(), verifier(), hash) == ZKEmailUtils.EmailProofError.NoError;
159125
}
160126
}

0 commit comments

Comments
 (0)