diff --git a/CHANGELOG.md b/CHANGELOG.md index 79226a80..dc9338f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 10-08-2025 + +- `ZKEmailUtils`: Add `tryDecodeEmailProof` function for safe calldata decoding with comprehensive bounds checking and validation for `EmailProof` struct. +- `ZKEmailUtils`: Update `isValidZKEmail` to receive `EmailProof` struct directly instead of `EmailAuthMsg` struct. +- `SignerZKEmail`: Remove `templateId` functionality and switch from `EmailAuthMsg` to direct `EmailProof` validation for streamlined signature verification. +- `ERC7913ZKEmailVerifier`: Remove `templateId` from signature validation logic and update `_decodeKey` function to directly decode `EmailProof` struct. + ## 09-08-2025 - `ZKEmailUtils`: Simplify library implementation and remove `Verifier.sol` indirection for cleaner integration with a Groth16Verifier. diff --git a/contracts/mocks/account/AccountZKEmailMock.sol b/contracts/mocks/account/AccountZKEmailMock.sol index 549e7cfd..67e917fb 100644 --- a/contracts/mocks/account/AccountZKEmailMock.sol +++ b/contracts/mocks/account/AccountZKEmailMock.sol @@ -15,13 +15,11 @@ contract AccountZKEmailMock is Account, SignerZKEmail, ERC7739, ERC7821, ERC721H constructor( bytes32 accountSalt_, IDKIMRegistry registry_, - IGroth16Verifier groth16Verifier_, - uint256 templateId_ + IGroth16Verifier groth16Verifier_ ) EIP712("AccountZKEmailMock", "1") { _setAccountSalt(accountSalt_); _setDKIMRegistry(registry_); _setVerifier(groth16Verifier_); - _setTemplateId(templateId_); } /// @inheritdoc ERC7821 diff --git a/contracts/utils/cryptography/ZKEmailUtils.sol b/contracts/utils/cryptography/ZKEmailUtils.sol index 8e178aad..0440ea5d 100644 --- a/contracts/utils/cryptography/ZKEmailUtils.sol +++ b/contracts/utils/cryptography/ZKEmailUtils.sol @@ -6,7 +6,7 @@ import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {IDKIMRegistry} from "@zk-email/contracts/DKIMRegistry.sol"; import {IGroth16Verifier} from "@zk-email/email-tx-builder/src/interfaces/IGroth16Verifier.sol"; -import {EmailAuthMsg, EmailProof} from "@zk-email/email-tx-builder/src/interfaces/IEmailTypes.sol"; +import {EmailProof} from "@zk-email/email-tx-builder/src/interfaces/IEmailTypes.sol"; import {CommandUtils} from "@zk-email/email-tx-builder/src/libraries/CommandUtils.sol"; /** @@ -43,7 +43,6 @@ library ZKEmailUtils { NoError, DKIMPublicKeyHash, // The DKIM public key hash verification fails MaskedCommandLength, // The masked command length exceeds the maximum - SkippedCommandPrefixSize, // The skipped command prefix size is invalid MismatchedCommand, // The command does not match the proof command InvalidFieldPoint, // The Groth16 field point is invalid EmailProof // The email proof verification fails @@ -59,33 +58,38 @@ library ZKEmailUtils { /// @dev Variant of {isValidZKEmail} that validates the `["signHash", "{uint}"]` command template. function isValidZKEmail( - EmailAuthMsg memory emailAuthMsg, + EmailProof memory emailProof, IDKIMRegistry dkimregistry, - IGroth16Verifier groth16Verifier + IGroth16Verifier groth16Verifier, + bytes32 hash ) internal view returns (EmailProofError) { string[] memory signHashTemplate = new string[](2); signHashTemplate[0] = "signHash"; signHashTemplate[1] = CommandUtils.UINT_MATCHER; // UINT_MATCHER is always lowercase - return isValidZKEmail(emailAuthMsg, dkimregistry, groth16Verifier, signHashTemplate, Case.LOWERCASE); + bytes[] memory signHashParams = new bytes[](1); + signHashParams[0] = abi.encode(hash); + return + isValidZKEmail(emailProof, dkimregistry, groth16Verifier, signHashTemplate, signHashParams, Case.LOWERCASE); } /** - * @dev Validates a ZKEmail authentication message. + * @dev Validates a ZKEmail proof against a command template. * - * This function takes an email authentication message, a DKIM registry contract, and a verifier contract - * as inputs. It performs several validation checks and returns a tuple containing a boolean success flag - * and an {EmailProofError} if validation failed. Returns {EmailProofError.NoError} if all validations pass, - * or false with a specific {EmailProofError} indicating which validation check failed. + * This function takes an email proof, a DKIM registry contract, and a verifier contract + * as inputs. It performs several validation checks and returns an {EmailProofError} indicating the result. + * Returns {EmailProofError.NoError} if all validations pass, or a specific {EmailProofError} indicating + * which validation check failed. * * NOTE: Attempts to validate the command for all possible string {Case} values. */ function isValidZKEmail( - EmailAuthMsg memory emailAuthMsg, + EmailProof memory emailProof, IDKIMRegistry dkimregistry, IGroth16Verifier groth16Verifier, - string[] memory template + string[] memory template, + bytes[] memory templateParams ) internal view returns (EmailProofError) { - return isValidZKEmail(emailAuthMsg, dkimregistry, groth16Verifier, template, Case.ANY); + return isValidZKEmail(emailProof, dkimregistry, groth16Verifier, template, templateParams, Case.ANY); } /** @@ -94,66 +98,99 @@ library ZKEmailUtils { * Useful for templates with Ethereum address matchers (i.e. `{ethAddr}`), which are case-sensitive (e.g., `["someCommand", "{address}"]`). */ function isValidZKEmail( - EmailAuthMsg memory emailAuthMsg, + EmailProof memory emailProof, IDKIMRegistry dkimregistry, IGroth16Verifier groth16Verifier, string[] memory template, + bytes[] memory templateParams, Case stringCase ) internal view returns (EmailProofError) { - if (emailAuthMsg.skippedCommandPrefix >= COMMAND_BYTES) { - return EmailProofError.SkippedCommandPrefixSize; - } else if (bytes(emailAuthMsg.proof.maskedCommand).length > COMMAND_BYTES) { + if (bytes(emailProof.maskedCommand).length > COMMAND_BYTES) { return EmailProofError.MaskedCommandLength; - } else if (!_commandMatch(emailAuthMsg, template, stringCase)) { + } else if (!_commandMatch(emailProof, template, templateParams, stringCase)) { return EmailProofError.MismatchedCommand; - } else if ( - !dkimregistry.isDKIMPublicKeyHashValid(emailAuthMsg.proof.domainName, emailAuthMsg.proof.publicKeyHash) - ) { + } else if (!dkimregistry.isDKIMPublicKeyHashValid(emailProof.domainName, emailProof.publicKeyHash)) { return EmailProofError.DKIMPublicKeyHash; } + (uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC) = abi.decode( - emailAuthMsg.proof.proof, + emailProof.proof, (uint256[2], uint256[2][2], uint256[2]) ); - - uint256 q = Q - 1; // upper bound of the field elements - if ( - pA[0] > q || - pA[1] > q || - pB[0][0] > q || - pB[0][1] > q || - pB[1][0] > q || - pB[1][1] > q || - pC[0] > q || - pC[1] > q - ) return EmailProofError.InvalidFieldPoint; + if (!_isValidFieldPoint(pA, pB, pC)) { + return EmailProofError.InvalidFieldPoint; + } return - groth16Verifier.verifyProof(pA, pB, pC, toPubSignals(emailAuthMsg.proof)) + groth16Verifier.verifyProof(pA, pB, pC, toPubSignals(emailProof)) ? EmailProofError.NoError : EmailProofError.EmailProof; } - /// @dev Compares the command in the email authentication message with the expected command. + /** + * @dev Verifies that calldata bytes (`input`) represents a valid `EmailProof` object. If encoding is valid, + * returns true and the calldata view at the object. Otherwise, returns false and an invalid calldata object. + * + * NOTE: The returned `emailProof` object should not be accessed if `success` is false. Trying to access the data may + * cause revert/panic. + */ + function tryDecodeEmailProof( + bytes calldata input + ) internal pure returns (bool success, EmailProof calldata emailProof) { + assembly ("memory-safe") { + emailProof := input.offset + } + + // Minimum length to hold 8 objects (32 bytes each) + if (input.length < 0x100) return (false, emailProof); + + // Get offset of non-value-type elements relative to the input buffer + uint256 domainNameOffset = uint256(bytes32(input[0x00:])); + uint256 maskedCommandOffset = uint256(bytes32(input[0x60:])); + uint256 proofOffset = uint256(bytes32(input[0xe0:])); + + // The elements length (at the offset) should be 32 bytes long. We check that this is within the + // buffer bounds. Since we know input.length is at least 32, we can subtract with no overflow risk. + if ( + input.length - 0x20 < domainNameOffset || + input.length - 0x20 < maskedCommandOffset || + input.length - 0x20 < proofOffset + ) return (false, emailProof); + + // Get the lengths. offset + 32 is bounded by input.length so it does not overflow. + uint256 domainNameLength = uint256(bytes32(input[domainNameOffset:])); + uint256 maskedCommandLength = uint256(bytes32(input[maskedCommandOffset:])); + uint256 proofLength = uint256(bytes32(input[proofOffset:])); + + // Check that the input buffer is long enough to store the non-value-type elements + // Since we know input.length is at least xxxOffset + 32, we can subtract with no overflow risk. + if ( + input.length - domainNameOffset - 0x20 < domainNameLength || + input.length - maskedCommandOffset - 0x20 < maskedCommandLength || + input.length - proofOffset - 0x20 < proofLength + ) return (false, emailProof); + + return (true, emailProof); + } + + /// @dev Compares the command in the email proof with the expected command template. function _commandMatch( - EmailAuthMsg memory emailAuthMsg, + EmailProof memory proof, string[] memory template, + bytes[] memory templateParams, Case stringCase ) private pure returns (bool) { - bytes[] memory commandParams = emailAuthMsg.commandParams; // Not a memory copy - uint256 skippedCommandPrefix = emailAuthMsg.skippedCommandPrefix; // Not a memory copy - string memory command = string(bytes(emailAuthMsg.proof.maskedCommand).slice(skippedCommandPrefix)); // Not a memory copy - if (stringCase != Case.ANY) - return commandParams.computeExpectedCommand(template, uint8(stringCase)).equal(command); + return templateParams.computeExpectedCommand(template, uint8(stringCase)).equal(proof.maskedCommand); + return - commandParams.computeExpectedCommand(template, uint8(Case.LOWERCASE)).equal(command) || - commandParams.computeExpectedCommand(template, uint8(Case.UPPERCASE)).equal(command) || - commandParams.computeExpectedCommand(template, uint8(Case.CHECKSUM)).equal(command); + templateParams.computeExpectedCommand(template, uint8(Case.LOWERCASE)).equal(proof.maskedCommand) || + templateParams.computeExpectedCommand(template, uint8(Case.UPPERCASE)).equal(proof.maskedCommand) || + templateParams.computeExpectedCommand(template, uint8(Case.CHECKSUM)).equal(proof.maskedCommand); } /** - * @dev Builds the expected public signals array for the Groth16 verifier from the given EmailAuthMsg. + * @dev Builds the expected public signals array for the Groth16 verifier from the given EmailProof. * * Packs the domain, public key hash, email nullifier, timestamp, masked command, account salt, and isCodeExist fields * into a uint256 array in the order expected by the verifier circuit. @@ -216,4 +253,21 @@ library ZKEmailUtils { } return fields; } + + /// @dev Checks if the field points are valid in the range of [0, Q). + function _isValidFieldPoint( + uint256[2] memory pA, + uint256[2][2] memory pB, + uint256[2] memory pC + ) private pure returns (bool) { + return + pA[0] < Q && + pA[1] < Q && + pB[0][0] < Q && + pB[0][1] < Q && + pB[1][0] < Q && + pB[1][1] < Q && + pC[0] < Q && + pC[1] < Q; + } } diff --git a/contracts/utils/cryptography/signers/SignerZKEmail.sol b/contracts/utils/cryptography/signers/SignerZKEmail.sol index eb38daab..7c196d00 100644 --- a/contracts/utils/cryptography/signers/SignerZKEmail.sol +++ b/contracts/utils/cryptography/signers/SignerZKEmail.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.24; import {IDKIMRegistry} from "@zk-email/contracts/DKIMRegistry.sol"; import {IGroth16Verifier} from "@zk-email/email-tx-builder/src/interfaces/IGroth16Verifier.sol"; -import {EmailAuthMsg} from "@zk-email/email-tx-builder/src/interfaces/IEmailTypes.sol"; +import {EmailProof} from "@zk-email/email-tx-builder/src/interfaces/IVerifier.sol"; import {AbstractSigner} from "@openzeppelin/contracts/utils/cryptography/signers/AbstractSigner.sol"; import {ZKEmailUtils} from "../ZKEmailUtils.sol"; @@ -23,7 +23,6 @@ import {ZKEmailUtils} from "../ZKEmailUtils.sol"; * * {accountSalt} - A unique identifier derived from the user's email address and account code. * * {DKIMRegistry} - An instance of the DKIM registry contract for domain verification. * * {verifier} - An instance of the Groth16Verifier contract for zero-knowledge proof validation. - * * {templateId} - The template ID of the sign hash command, defining the expected format. * * Example of usage: * @@ -32,29 +31,26 @@ import {ZKEmailUtils} from "../ZKEmailUtils.sol"; * function initialize( * bytes32 accountSalt, * IDKIMRegistry registry, - * IGroth16Verifier groth16Verifier, - * uint256 templateId + * IGroth16Verifier groth16Verifier * ) public initializer { * // Will revert if the signer is already initialized * _setAccountSalt(accountSalt); * _setDKIMRegistry(registry); - * _setGroth16Verifier(groth16Verifier); - * _setTemplateId(templateId); + * _setVerifier(groth16Verifier); * } * } * ``` * - * IMPORTANT: Avoiding to call {_setAccountSalt}, {_setDKIMRegistry}, {_setVerifier} and {_setTemplateId} + * IMPORTANT: Failing to call {_setAccountSalt}, {_setDKIMRegistry}, and {_setVerifier} * either during construction (if used standalone) or during initialization (if used as a clone) may * leave the signer either front-runnable or unusable. */ abstract contract SignerZKEmail is AbstractSigner { - using ZKEmailUtils for EmailAuthMsg; + using ZKEmailUtils for EmailProof; bytes32 private _accountSalt; IDKIMRegistry private _registry; IGroth16Verifier private _groth16Verifier; - uint256 private _templateId; /// @dev Proof verification error. error InvalidEmailProof(ZKEmailUtils.EmailProofError err); @@ -94,11 +90,6 @@ abstract contract SignerZKEmail is AbstractSigner { return _groth16Verifier; } - /// @dev The command template of the sign hash command. - function templateId() public view virtual returns (uint256) { - return _templateId; - } - /// @dev Set the {accountSalt}. function _setAccountSalt(bytes32 accountSalt_) internal virtual { _accountSalt = accountSalt_; @@ -114,47 +105,22 @@ abstract contract SignerZKEmail is AbstractSigner { _groth16Verifier = verifier_; } - /// @dev Set the command's {templateId}. - function _setTemplateId(uint256 templateId_) internal virtual { - _templateId = templateId_; - } - /** * @dev See {AbstractSigner-_rawSignatureValidation}. Validates a raw signature by: * - * 1. Decoding the email authentication message from the signature - * 2. Verifying the hash matches the command parameters - * 3. Checking the template ID matches - * 4. Validating the account salt - * 5. Verifying the email proof + * 1. Decoding the email proof from the signature + * 2. Validating the account salt matches + * 3. Verifying the email proof using ZKEmail utilities */ function _rawSignatureValidation( bytes32 hash, bytes calldata signature ) internal view virtual override returns (bool) { - // Check if the signature is long enough to contain the EmailAuthMsg - // The minimum length is 512 bytes (initial part + pointer offsets) - // - `templateId` is a uint256 (32 bytes). - // - `commandParams` is a dynamic array of bytes32 (32 bytes offset). - // - `skippedCommandPrefixSize` is a uint256 (32 bytes). - // - `proof` is a struct with the following fields (32 bytes offset): - // - `domainName` is a dynamic string (32 bytes offset). - // - `publicKeyHash` is a bytes32 (32 bytes). - // - `timestamp` is a uint256 (32 bytes). - // - `maskedCommand` is a dynamic string (32 bytes offset). - // - `emailNullifier` is a bytes32 (32 bytes). - // - `accountSalt` is a bytes32 (32 bytes). - // - `isCodeExist` is a boolean, so its length is 1 byte padded to 32 bytes. - // - `proof` is a dynamic bytes (32 bytes offset). - // There are 128 bytes for the EmailAuthMsg type and 256 bytes for the proof. - // Considering all dynamic elements are empty (i.e. `commandParams` = [], `domainName` = "", `maskedCommand` = "", `proof` = []), - // then we have 128 bytes for the EmailAuthMsg type, 256 bytes for the proof and 4 * 32 for the length of the dynamic elements. - // So the minimum length is 128 + 256 + 4 * 32 = 512 bytes. - if (signature.length < 512) return false; - EmailAuthMsg memory emailAuthMsg = abi.decode(signature, (EmailAuthMsg)); - return (abi.decode(emailAuthMsg.commandParams[0], (bytes32)) == hash && - emailAuthMsg.templateId == templateId() && - emailAuthMsg.proof.accountSalt == accountSalt() && - emailAuthMsg.isValidZKEmail(DKIMRegistry(), verifier()) == ZKEmailUtils.EmailProofError.NoError); + (bool decodeSuccess, EmailProof calldata emailProof) = ZKEmailUtils.tryDecodeEmailProof(signature); + + return + decodeSuccess && + emailProof.accountSalt == accountSalt() && + emailProof.isValidZKEmail(DKIMRegistry(), verifier(), hash) == ZKEmailUtils.EmailProofError.NoError; } } diff --git a/contracts/utils/cryptography/verifiers/ERC7913ZKEmailVerifier.sol b/contracts/utils/cryptography/verifiers/ERC7913ZKEmailVerifier.sol index fce8457f..ccf0495e 100644 --- a/contracts/utils/cryptography/verifiers/ERC7913ZKEmailVerifier.sol +++ b/contracts/utils/cryptography/verifiers/ERC7913ZKEmailVerifier.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.24; +import {IERC7913SignatureVerifier} from "@openzeppelin/contracts/interfaces/IERC7913.sol"; import {IDKIMRegistry} from "@zk-email/contracts/DKIMRegistry.sol"; +import {EmailProof} from "@zk-email/email-tx-builder/src/interfaces/IEmailTypes.sol"; import {IGroth16Verifier} from "@zk-email/email-tx-builder/src/interfaces/IGroth16Verifier.sol"; -import {EmailAuthMsg} from "@zk-email/email-tx-builder/src/interfaces/IEmailTypes.sol"; -import {IERC7913SignatureVerifier} from "@openzeppelin/contracts/interfaces/IERC7913.sol"; import {ZKEmailUtils} from "../ZKEmailUtils.sol"; /** @@ -16,59 +16,53 @@ import {ZKEmailUtils} from "../ZKEmailUtils.sol"; * * 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, + * verifier 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): + * Example of overriding _decodeKey to enforce a specific verifier, registry: * * ```solidity * function _decodeKey(bytes calldata key) internal view override returns ( * IDKIMRegistry registry, * bytes32 accountSalt, - * IGroth16Verifier verifier, - * uint256 templateId + * IGroth16Verifier verifier * ) { - * (registry, accountSalt, verifier, templateId) = super._decodeKey(key); + * (registry, accountSalt, verifier) = super._decodeKey(key); * require(verifier == _verifier, "Invalid verifier"); * require(registry == _registry, "Invalid registry"); - * return (registry, accountSalt, verifier, templateId); + * return (registry, accountSalt, verifier); * } * ``` */ contract ERC7913ZKEmailVerifier is IERC7913SignatureVerifier { - using ZKEmailUtils for EmailAuthMsg; + using ZKEmailUtils for EmailProof; /** * @dev Verifies a zero-knowledge proof of an email signature validated by a {DKIMRegistry} contract. * - * The key format is ABI-encoded (IDKIMRegistry, bytes32, IGroth16Verifier, uint256) where: + * The key format is ABI-encoded (IDKIMRegistry, bytes32, IGroth16Verifier) where: * * * IDKIMRegistry: The registry contract that validates DKIM public key hashes * * bytes32: The account salt that uniquely identifies the user's email address * * IGroth16Verifier: 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. + * The signature is an ABI-encoded {EmailProof} struct containing + * the 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 - * } + * bytes memory signature = abi.encode(EmailProof({ + * 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: "signHash 12345...", // 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 * })); * ``` */ @@ -77,16 +71,13 @@ contract ERC7913ZKEmailVerifier is IERC7913SignatureVerifier { bytes32 hash, bytes calldata signature ) public view virtual override returns (bytes4) { - (IDKIMRegistry registry_, bytes32 accountSalt_, IGroth16Verifier verifier_, uint256 templateId_) = _decodeKey( - key - ); - EmailAuthMsg memory emailAuthMsg = abi.decode(signature, (EmailAuthMsg)); + (IDKIMRegistry registry_, bytes32 accountSalt_, IGroth16Verifier verifier_) = _decodeKey(key); + (bool decodeSuccess, EmailProof calldata emailProof) = ZKEmailUtils.tryDecodeEmailProof(signature); return - (abi.decode(emailAuthMsg.commandParams[0], (bytes32)) == hash && - emailAuthMsg.templateId == templateId_ && - emailAuthMsg.proof.accountSalt == accountSalt_ && - emailAuthMsg.isValidZKEmail(registry_, verifier_) == ZKEmailUtils.EmailProofError.NoError) + (decodeSuccess && + emailProof.accountSalt == accountSalt_ && + emailProof.isValidZKEmail(registry_, verifier_, hash) == ZKEmailUtils.EmailProofError.NoError) ? IERC7913SignatureVerifier.verify.selector : bytes4(0xffffffff); } @@ -95,17 +86,12 @@ contract ERC7913ZKEmailVerifier is IERC7913SignatureVerifier { * @dev Decodes the key into its components. * * ```solidity - * bytes memory key = abi.encode(registry, accountSalt, verifier, templateId); + * bytes memory key = abi.encode(registry, accountSalt, verifier); * ``` */ function _decodeKey( bytes calldata key - ) - internal - view - virtual - returns (IDKIMRegistry registry, bytes32 accountSalt, IGroth16Verifier verifier, uint256 templateId) - { - return abi.decode(key, (IDKIMRegistry, bytes32, IGroth16Verifier, uint256)); + ) internal view virtual returns (IDKIMRegistry registry, bytes32 accountSalt, IGroth16Verifier verifier) { + return abi.decode(key, (IDKIMRegistry, bytes32, IGroth16Verifier)); } } diff --git a/test/account/AccountERC7913.test.js b/test/account/AccountERC7913.test.js index 093a3398..4682839c 100644 --- a/test/account/AccountERC7913.test.js +++ b/test/account/AccountERC7913.test.js @@ -19,12 +19,11 @@ const selector = '12345'; const domainName = 'gmail.com'; const publicKeyHash = '0x0ea9c777dc7110e5a9e89b13f0cfc540e3845ba120b2b6dc24024d61488d4788'; const emailNullifier = '0x00a83fce3d4b1c9ef0f600644c1ecc6c8115b57b1596e0e3295e2c5105fbfd8a'; -const templateId = ethers.solidityPackedKeccak256(['string', 'uint256'], ['TEST', 0n]); // Prepare signer in advance const signerWebAuthn = new NonNativeSigner(WebAuthnSigningKey.random()); const signerZKEmail = new NonNativeSigner( - new ZKEmailSigningKey(domainName, publicKeyHash, emailNullifier, accountSalt, templateId), + new ZKEmailSigningKey(domainName, publicKeyHash, emailNullifier, accountSalt), ); // Minimal fixture common to the different signer verifiers @@ -117,8 +116,8 @@ describe('AccountERC7913', function () { ethers.concat([ this.verifierZKEmail.target, ethers.AbiCoder.defaultAbiCoder().encode( - ['address', 'bytes32', 'address', 'uint256'], - [this.dkim.target, accountSalt, this.zkEmailVerifier.target, templateId], + ['address', 'bytes32', 'address'], + [this.dkim.target, accountSalt, this.zkEmailVerifier.target], ), ]), ); diff --git a/test/account/AccountZKEmail.test.js b/test/account/AccountZKEmail.test.js index 28a4cd9d..26f731a1 100644 --- a/test/account/AccountZKEmail.test.js +++ b/test/account/AccountZKEmail.test.js @@ -17,7 +17,6 @@ const selector = '12345'; const domainName = 'gmail.com'; const publicKeyHash = '0x0ea9c777dc7110e5a9e89b13f0cfc540e3845ba120b2b6dc24024d61488d4788'; const emailNullifier = '0x00a83fce3d4b1c9ef0f600644c1ecc6c8115b57b1596e0e3295e2c5105fbfd8a'; -const templateId = ethers.solidityPackedKeccak256(['string', 'uint256'], ['TEST', 0n]); const SIGN_HASH_COMMAND = 'signHash'; @@ -38,13 +37,11 @@ async function fixture() { const verifier = await ethers.deployContract('ZKEmailGroth16VerifierMock'); // ERC-4337 signer - const signer = new NonNativeSigner( - new ZKEmailSigningKey(domainName, publicKeyHash, emailNullifier, accountSalt, templateId), - ); + const signer = new NonNativeSigner(new ZKEmailSigningKey(domainName, publicKeyHash, emailNullifier, accountSalt)); // ERC-4337 account const helper = new ERC4337Helper(); - const mock = await helper.newAccount('$AccountZKEmailMock', [accountSalt, dkim.target, verifier.target, templateId]); + const mock = await helper.newAccount('$AccountZKEmailMock', [accountSalt, dkim.target, verifier.target]); const signUserOp = async userOp => { // Create email auth message for the user operation hash @@ -71,17 +68,10 @@ async function fixture() { [pA, pB, pC], ); - // Encode the email auth message as the signature + // Encode the EmailProof as the signature return ethers.AbiCoder.defaultAbiCoder().encode( - ['tuple(uint256,bytes[],uint256,tuple(string,bytes32,uint256,string,bytes32,bytes32,bool,bytes))'], - [ - [ - templateId, - [hash], - 0, // skippedCommandPrefix - [domainName, publicKeyHash, timestamp, command, emailNullifier, accountSalt, isCodeExist, invalidProof], - ], - ], + ['string', 'bytes32', 'uint256', 'string', 'bytes32', 'bytes32', 'bool', 'bytes'], + [domainName, publicKeyHash, timestamp, command, emailNullifier, accountSalt, isCodeExist, invalidProof], ); }; diff --git a/test/helpers/enums.js b/test/helpers/enums.js index 9f02ca8c..a9377a19 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -6,7 +6,6 @@ module.exports = { 'NoError', 'DKIMPublicKeyHash', 'MaskedCommandLength', - 'SkippedCommandPrefixSize', 'MismatchedCommand', 'InvalidFieldPoint', 'EmailProof', diff --git a/test/helpers/signers.js b/test/helpers/signers.js index 2f45f534..585a8c03 100644 --- a/test/helpers/signers.js +++ b/test/helpers/signers.js @@ -17,14 +17,12 @@ class ZKEmailSigningKey { #publicKeyHash; #emailNullifier; #accountSalt; - #templateId; - constructor(domainName, publicKeyHash, emailNullifier, accountSalt, templateId) { + constructor(domainName, publicKeyHash, emailNullifier, accountSalt) { this.#domainName = domainName; this.#publicKeyHash = publicKeyHash; this.#emailNullifier = emailNullifier; this.#accountSalt = accountSalt; - this.#templateId = templateId; this.SIGN_HASH_COMMAND = 'signHash'; } @@ -60,26 +58,19 @@ class ZKEmailSigningKey { const pC = [7n, 8n]; const validProof = AbiCoder.defaultAbiCoder().encode(['uint256[2]', 'uint256[2][2]', 'uint256[2]'], [pA, pB, pC]); - // Encode the email auth message as the signature + // Encode the EmailProof as the signature return { serialized: AbiCoder.defaultAbiCoder().encode( - ['tuple(uint256,bytes[],uint256,tuple(string,bytes32,uint256,string,bytes32,bytes32,bool,bytes))'], + ['string', 'bytes32', 'uint256', 'string', 'bytes32', 'bytes32', 'bool', 'bytes'], [ - [ - this.#templateId, - [digest], - 0, // skippedCommandPrefix - [ - this.#domainName, - this.#publicKeyHash, - timestamp, - command, - this.#emailNullifier, - this.#accountSalt, - isCodeExist, - validProof, - ], - ], + this.#domainName, + this.#publicKeyHash, + timestamp, + command, + this.#emailNullifier, + this.#accountSalt, + isCodeExist, + validProof, ], ), }; diff --git a/test/utils/cryptography/ZKEmailUtils.t.sol b/test/utils/cryptography/ZKEmailUtils.t.sol index 9d2b0a2f..2ccbcd85 100644 --- a/test/utils/cryptography/ZKEmailUtils.t.sol +++ b/test/utils/cryptography/ZKEmailUtils.t.sol @@ -15,15 +15,11 @@ import {EmailAuthMsgFixtures, EmailAuthMsg} from "@zk-email/email-tx-builder/tes contract ZKEmailUtilsTest is Test { using Strings for *; - using ZKEmailUtils for EmailAuthMsg; - - // Base field size - uint256 constant Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + using ZKEmailUtils for EmailProof; IDKIMRegistry private _dkimRegistry; IGroth16Verifier private _verifier; bytes32 private _accountSalt; - uint256 private _templateId; // From https://github.com/zkemail/email-tx-builder/blob/main/packages/contracts/test/helpers/DeploymentHelper.sol#L36-L41 string private _selector = "1234"; string private _domainName = "gmail.com"; @@ -42,21 +38,28 @@ contract ZKEmailUtilsTest is Test { // Generate test data _accountSalt = keccak256("test@example.com"); - _templateId = 1; _mockProof = abi.encodePacked(bytes1(0x01)); } function testFixtureCase1SignHash() public { EmailAuthMsg memory authMsg = EmailAuthMsgFixtures.getCase1(); _setupDKIMRegistryForFixture(authMsg); - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail(authMsg, _dkimRegistry, _verifier); + ZKEmailUtils.EmailProofError err = authMsg.proof.isValidZKEmail( + _dkimRegistry, + _verifier, + abi.decode(authMsg.commandParams[0], (bytes32)) + ); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.NoError)); } function testFixtureCase2SignHash() public { EmailAuthMsg memory authMsg = EmailAuthMsgFixtures.getCase2(); _setupDKIMRegistryForFixture(authMsg); - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail(authMsg, _dkimRegistry, _verifier); + ZKEmailUtils.EmailProofError err = authMsg.proof.isValidZKEmail( + _dkimRegistry, + _verifier, + abi.decode(authMsg.commandParams[0], (bytes32)) + ); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.NoError)); } @@ -72,10 +75,11 @@ contract ZKEmailUtilsTest is Test { template[4] = CommandUtils.ETH_ADDR_MATCHER; ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - authMsg, + authMsg.proof, _dkimRegistry, _verifier, template, + authMsg.commandParams, ZKEmailUtils.Case.ANY ); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.NoError)); @@ -91,10 +95,11 @@ contract ZKEmailUtilsTest is Test { template[2] = CommandUtils.ETH_ADDR_MATCHER; ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - authMsg, + authMsg.proof, _dkimRegistry, _verifier, template, + authMsg.commandParams, ZKEmailUtils.Case.ANY ); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.NoError)); @@ -113,31 +118,20 @@ contract ZKEmailUtilsTest is Test { (pA, pB, pC) = _boundPoints(pA, pB, pC); bytes memory proof = abi.encode(pA, pB, pC); - // Build email auth message with fuzzed parameters - bytes[] memory commandParams = new bytes[](1); - commandParams[0] = abi.encode(hash); - - EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock( - string.concat(SIGN_HASH_COMMAND, uint256(hash).toString()), - commandParams, - 0 - ); + // Build email proof with fuzzed parameters + EmailProof memory emailProof = _buildEmailProofMock(string.concat(SIGN_HASH_COMMAND, uint256(hash).toString())); // Override with fuzzed values - emailAuthMsg.proof.timestamp = timestamp; - emailAuthMsg.proof.emailNullifier = emailNullifier; - emailAuthMsg.proof.accountSalt = accountSalt; - emailAuthMsg.proof.isCodeExist = isCodeExist; - emailAuthMsg.proof.proof = proof; + emailProof.timestamp = timestamp; + emailProof.emailNullifier = emailNullifier; + emailProof.accountSalt = accountSalt; + emailProof.isCodeExist = isCodeExist; + emailProof.proof = proof; _mockVerifyEmailProof(); // Test validation - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - _verifier - ); + ZKEmailUtils.EmailProofError err = emailProof.isValidZKEmail(IDKIMRegistry(_dkimRegistry), _verifier, hash); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.NoError)); } @@ -159,18 +153,16 @@ contract ZKEmailUtilsTest is Test { bytes[] memory commandParams = new bytes[](1); commandParams[0] = abi.encode(hash); - EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock( - string.concat(commandPrefix, " ", uint256(hash).toString()), - commandParams, - 0 + EmailProof memory emailProof = _buildEmailProofMock( + string.concat(commandPrefix, " ", uint256(hash).toString()) ); // Override with fuzzed values - emailAuthMsg.proof.timestamp = timestamp; - emailAuthMsg.proof.emailNullifier = emailNullifier; - emailAuthMsg.proof.accountSalt = accountSalt; - emailAuthMsg.proof.isCodeExist = isCodeExist; - emailAuthMsg.proof.proof = proof; + emailProof.timestamp = timestamp; + emailProof.emailNullifier = emailNullifier; + emailProof.accountSalt = accountSalt; + emailProof.isCodeExist = isCodeExist; + emailProof.proof = proof; string[] memory template = new string[](2); template[0] = commandPrefix; @@ -179,10 +171,11 @@ contract ZKEmailUtilsTest is Test { _mockVerifyEmailProof(); ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, + emailProof, IDKIMRegistry(_dkimRegistry), _verifier, - template + template, + commandParams ); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.NoError)); @@ -207,18 +200,16 @@ contract ZKEmailUtilsTest is Test { // Test with different cases for (uint256 i = 0; i < uint8(type(ZKEmailUtils.Case).max) - 1; i++) { - EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock( - string.concat(commandPrefix, " ", CommandUtils.addressToHexString(addr, i)), - commandParams, - 0 + EmailProof memory emailProof = _buildEmailProofMock( + string.concat(commandPrefix, " ", CommandUtils.addressToHexString(addr, i)) ); // Override with fuzzed values - emailAuthMsg.proof.timestamp = timestamp; - emailAuthMsg.proof.emailNullifier = emailNullifier; - emailAuthMsg.proof.accountSalt = accountSalt; - emailAuthMsg.proof.isCodeExist = isCodeExist; - emailAuthMsg.proof.proof = proof; + emailProof.timestamp = timestamp; + emailProof.emailNullifier = emailNullifier; + emailProof.accountSalt = accountSalt; + emailProof.isCodeExist = isCodeExist; + emailProof.proof = proof; _mockVerifyEmailProof(); @@ -227,10 +218,11 @@ contract ZKEmailUtilsTest is Test { template[1] = CommandUtils.ETH_ADDR_MATCHER; ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, + emailProof, IDKIMRegistry(_dkimRegistry), _verifier, template, + commandParams, ZKEmailUtils.Case(i) ); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.NoError)); @@ -254,18 +246,14 @@ contract ZKEmailUtilsTest is Test { bytes[] memory commandParams = new bytes[](1); commandParams[0] = abi.encode(addr); - EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock( - string.concat(commandPrefix, " ", addr.toHexString()), - commandParams, - 0 - ); + EmailProof memory emailProof = _buildEmailProofMock(string.concat(commandPrefix, " ", addr.toHexString())); // Override with fuzzed values - emailAuthMsg.proof.timestamp = timestamp; - emailAuthMsg.proof.emailNullifier = emailNullifier; - emailAuthMsg.proof.accountSalt = accountSalt; - emailAuthMsg.proof.isCodeExist = isCodeExist; - emailAuthMsg.proof.proof = proof; + emailProof.timestamp = timestamp; + emailProof.emailNullifier = emailNullifier; + emailProof.accountSalt = accountSalt; + emailProof.isCodeExist = isCodeExist; + emailProof.proof = proof; string[] memory template = new string[](2); template[0] = commandPrefix; @@ -274,10 +262,11 @@ contract ZKEmailUtilsTest is Test { _mockVerifyEmailProof(); ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, + emailProof, IDKIMRegistry(_dkimRegistry), _verifier, template, + commandParams, ZKEmailUtils.Case.ANY ); @@ -285,23 +274,12 @@ contract ZKEmailUtilsTest is Test { } function testInvalidDKIMPublicKeyHash(bytes32 hash, string memory domainName, bytes32 publicKeyHash) public view { - bytes[] memory commandParams = new bytes[](1); - commandParams[0] = abi.encode(hash); + EmailProof memory emailProof = _buildEmailProofMock(string.concat(SIGN_HASH_COMMAND, uint256(hash).toString())); - EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock( - string.concat(SIGN_HASH_COMMAND, uint256(hash).toString()), - commandParams, - 0 - ); + emailProof.domainName = domainName; + emailProof.publicKeyHash = publicKeyHash; - emailAuthMsg.proof.domainName = domainName; - emailAuthMsg.proof.publicKeyHash = publicKeyHash; - - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - _verifier - ); + ZKEmailUtils.EmailProofError err = emailProof.isValidZKEmail(IDKIMRegistry(_dkimRegistry), _verifier, hash); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.DKIMPublicKeyHash)); } @@ -309,53 +287,17 @@ contract ZKEmailUtilsTest is Test { function testInvalidMaskedCommandLength(bytes32 hash, uint256 length) public view { length = bound(length, 606, 1000); // Assuming commandBytes is 605 - bytes[] memory commandParams = new bytes[](1); - commandParams[0] = abi.encode(hash); + EmailProof memory emailProof = _buildEmailProofMock(string(new bytes(length))); - EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock(string(new bytes(length)), commandParams, 0); - - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - _verifier - ); + ZKEmailUtils.EmailProofError err = emailProof.isValidZKEmail(IDKIMRegistry(_dkimRegistry), _verifier, hash); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.MaskedCommandLength)); } - function testSkippedCommandPrefix(bytes32 hash, uint256 skippedPrefix) public view { - uint256 verifierCommandBytes = ZKEmailUtils.COMMAND_BYTES; - skippedPrefix = bound(skippedPrefix, verifierCommandBytes, verifierCommandBytes + 1000); - - bytes[] memory commandParams = new bytes[](1); - commandParams[0] = abi.encode(hash); - - EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock( - string.concat(SIGN_HASH_COMMAND, uint256(hash).toString()), - commandParams, - skippedPrefix - ); - - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - _verifier - ); - - assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.SkippedCommandPrefixSize)); - } - function testMismatchedCommand(bytes32 hash, string memory invalidCommand) public view { - bytes[] memory commandParams = new bytes[](1); - commandParams[0] = abi.encode(hash); + EmailProof memory emailProof = _buildEmailProofMock(invalidCommand); - EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock(invalidCommand, commandParams, 0); - - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - _verifier - ); + ZKEmailUtils.EmailProofError err = emailProof.isValidZKEmail(IDKIMRegistry(_dkimRegistry), _verifier, hash); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.MismatchedCommand)); } @@ -368,24 +310,236 @@ contract ZKEmailUtilsTest is Test { ) public view { (pA, pB, pC) = _boundPoints(pA, pB, pC); - bytes[] memory commandParams = new bytes[](1); - commandParams[0] = abi.encode(hash); + EmailProof memory emailProof = _buildEmailProofMock(string.concat(SIGN_HASH_COMMAND, uint256(hash).toString())); - EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock( - string.concat(SIGN_HASH_COMMAND, uint256(hash).toString()), - commandParams, - 0 + emailProof.proof = abi.encode(pA, pB, pC); + + ZKEmailUtils.EmailProofError err = emailProof.isValidZKEmail(IDKIMRegistry(_dkimRegistry), _verifier, hash); + + assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.EmailProof)); + } + + function testTryDecodeEmailProofValid( + string memory domainName, + bytes32 publicKeyHash, + uint256 timestamp, + string memory maskedCommand, + bytes32 emailNullifier, + bytes32 accountSalt, + bool isCodeExist, + bytes memory proof + ) public view { + (bool success, EmailProof memory emailProof) = this.tryDecodeEmailProof( + abi.encode( + domainName, + publicKeyHash, + timestamp, + maskedCommand, + emailNullifier, + accountSalt, + isCodeExist, + proof + ) ); + assertTrue(success); + assertEq(emailProof.domainName, domainName); + assertEq(emailProof.publicKeyHash, publicKeyHash); + assertEq(emailProof.timestamp, timestamp); + assertEq(emailProof.maskedCommand, maskedCommand); + assertEq(emailProof.emailNullifier, emailNullifier); + assertEq(emailProof.accountSalt, accountSalt); + assertEq(emailProof.isCodeExist, isCodeExist); + assertEq(emailProof.proof, proof); + } - emailAuthMsg.proof.proof = abi.encode(pA, pB, pC); + function testTryDecodeEmailProofInvalid() public view { + string memory domainName = "gmail.com"; + bytes32 publicKeyHash = keccak256("publicKeyHash"); + uint256 timestamp = block.timestamp; + string memory maskedCommand = "signHash 12345"; + bytes32 emailNullifier = keccak256("emailNullifier"); + bytes32 accountSalt = keccak256("accountSalt"); + bool isCodeExist = true; + bytes memory proof = hex"deadbeef"; + + // too short + assertFalse( + this.tryDecodeEmailProofDrop(abi.encodePacked(publicKeyHash, timestamp, emailNullifier, accountSalt)) + ); - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - _verifier + // offset out of bound for domainName (position 0x00) + bytes memory encoded = abi.encodePacked( + abi.encodePacked( + uint256(0x200), // domainName offset pointing outside + publicKeyHash, + timestamp, + uint256(0x160), // maskedCommand offset + emailNullifier, + accountSalt, + isCodeExist + ), + abi.encodePacked( + uint256(0x180), // proof offset + uint256(bytes(domainName).length), + domainName, + uint256(bytes(maskedCommand).length), + maskedCommand, + uint256(proof.length), + proof + ) + ); + assertFalse(this.tryDecodeEmailProofDrop(encoded)); + + // offset out of bound for maskedCommand (position 0x60) + encoded = abi.encodePacked( + abi.encodePacked( + uint256(0x100), // domainName offset + publicKeyHash, + timestamp, + uint256(0x200), // maskedCommand offset pointing outside + emailNullifier, + accountSalt, + isCodeExist + ), + abi.encodePacked( + uint256(0x140), // proof offset + uint256(bytes(domainName).length), + domainName, + uint256(bytes(maskedCommand).length), + maskedCommand, + uint256(proof.length), + proof + ) + ); + assertFalse(this.tryDecodeEmailProofDrop(encoded)); + + // offset out of bound for proof (position 0xe0) + encoded = abi.encodePacked( + abi.encodePacked( + uint256(0x100), // domainName offset + publicKeyHash, + timestamp, + uint256(0x120), // maskedCommand offset + emailNullifier, + accountSalt, + isCodeExist + ), + abi.encodePacked( + uint256(0x200), // proof offset pointing outside + uint256(bytes(domainName).length), + domainName, + uint256(bytes(maskedCommand).length), + maskedCommand, + uint256(proof.length), + proof + ) + ); + assertFalse(this.tryDecodeEmailProofDrop(encoded)); + + // minimal valid (all dynamic fields length 0, at the same position) + assertTrue( + this.tryDecodeEmailProofDrop( + abi.encodePacked( + uint256(0x100), // domainName offset + publicKeyHash, + timestamp, + uint256(0x100), // maskedCommand offset + emailNullifier, + accountSalt, + isCodeExist ? bytes32(uint256(1)) : bytes32(0), + uint256(0x100), // proof offset + uint256(0) // length 0 for all dynamic fields + ) + ) ); - assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.EmailProof)); + // length out of bound for domainName + assertTrue( + this.tryDecodeEmailProofDrop( + abi.encodePacked( + uint256(0x100), // domainName offset + publicKeyHash, + timestamp, + uint256(0x120), // maskedCommand offset + emailNullifier, + accountSalt, + isCodeExist ? bytes32(uint256(1)) : bytes32(0), + uint256(0x140), // proof offset + uint256(0x20), // domainName length 32 bytes + bytes32(0), // 32 bytes of domain data + uint256(0), // maskedCommand length 0 + uint256(0) // proof length 0 + ) + ) + ); + + // length pointing outside buffer for domainName + assertFalse( + this.tryDecodeEmailProofDrop( + abi.encodePacked( + uint256(0x100), // domainName offset + publicKeyHash, + timestamp, + uint256(0x120), // maskedCommand offset + emailNullifier, + accountSalt, + isCodeExist ? bytes32(uint256(1)) : bytes32(0), + uint256(0x140), // proof offset + uint256(0x61), // domainName length 97 (32 * 3 + 1) bytes (too long for available data) + bytes32(0), // only 32 bytes of domain data + uint256(0), // maskedCommand length 0 + uint256(0) // proof length 0 + ) + ) + ); + + // valid case with proper offsets and lengths + assertTrue( + this.tryDecodeEmailProofDrop( + abi.encodePacked( + uint256(0x100), // domainName offset + publicKeyHash, + timestamp, + uint256(0x120), // maskedCommand offset + emailNullifier, + accountSalt, + isCodeExist ? bytes32(uint256(1)) : bytes32(0), + uint256(0x140), // proof offset + uint256(0), // domainName length 0 + uint256(0), // maskedCommand length 0 + uint256(0) // proof length 0 + ) + ) + ); + + // invalid case with length pointing outside for proof + assertFalse( + this.tryDecodeEmailProofDrop( + abi.encodePacked( + uint256(0x100), // domainName offset + publicKeyHash, + timestamp, + uint256(0x120), // maskedCommand offset + emailNullifier, + accountSalt, + isCodeExist ? bytes32(uint256(1)) : bytes32(0), + uint256(0x140), // proof offset + uint256(0), // domainName length 0 + uint256(0), // maskedCommand length 0 + uint256(0x01) // proof length 1 (but no data provided) + ) + ) + ); + } + + function tryDecodeEmailProof( + bytes calldata encoded + ) public pure returns (bool success, EmailProof calldata emailProof) { + (success, emailProof) = ZKEmailUtils.tryDecodeEmailProof(encoded); + } + + function tryDecodeEmailProofDrop(bytes calldata encoded) public pure returns (bool success) { + (success, ) = ZKEmailUtils.tryDecodeEmailProof(encoded); } function _createECDSAOwnedDKIMRegistry() private returns (IDKIMRegistry) { @@ -407,12 +561,8 @@ contract ZKEmailUtilsTest is Test { ); } - function _buildEmailAuthMsgMock( - string memory command, - bytes[] memory params, - uint256 skippedPrefix - ) private view returns (EmailAuthMsg memory emailAuthMsg) { - EmailProof memory emailProof = EmailProof({ + function _buildEmailProofMock(string memory command) private view returns (EmailProof memory emailProof) { + emailProof = EmailProof({ domainName: _domainName, publicKeyHash: _publicKeyHash, timestamp: block.timestamp, @@ -422,13 +572,6 @@ contract ZKEmailUtilsTest is Test { isCodeExist: true, proof: _mockProof }); - - emailAuthMsg = EmailAuthMsg({ - templateId: _templateId, - commandParams: params, - skippedCommandPrefix: skippedPrefix, - proof: emailProof - }); } function _setupDKIMRegistryForFixture(EmailAuthMsg memory fixture) private { @@ -455,6 +598,7 @@ contract ZKEmailUtilsTest is Test { uint256[2][2] memory pB, uint256[2] memory pC ) private pure returns (uint256[2] memory, uint256[2][2] memory, uint256[2] memory) { + uint256 Q = ZKEmailUtils.Q; pA[0] = bound(pA[0], 1, Q - 1); pA[1] = bound(pA[1], 1, Q - 1); pB[0][0] = bound(pB[0][0], 1, Q - 1); diff --git a/test/utils/cryptography/ZKEmailUtils.test.js b/test/utils/cryptography/ZKEmailUtils.test.js index 91a1b1a4..dc26d882 100644 --- a/test/utils/cryptography/ZKEmailUtils.test.js +++ b/test/utils/cryptography/ZKEmailUtils.test.js @@ -11,9 +11,6 @@ const domainName = 'gmail.com'; const publicKeyHash = '0x0ea9c777dc7110e5a9e89b13f0cfc540e3845ba120b2b6dc24024d61488d4788'; const emailNullifier = '0x00a83fce3d4b1c9ef0f600644c1ecc6c8115b57b1596e0e3295e2c5105fbfd8a'; -const templateId = ethers.solidityPackedKeccak256(['string', 'uint256'], ['TEST', 0n]); -const commandBytes = 605; - const SIGN_HASH_COMMAND = 'signHash'; const UINT_MATCHER = '{uint}'; const ETH_ADDR_MATCHER = '{ethAddr}'; @@ -30,16 +27,16 @@ async function fixture() { .then(message => admin.signMessage(message)) .then(signature => dkim.setDKIMPublicKeyHash(selector, domainName, publicKeyHash, signature)); - // Use the correct mock + // Groth16 Verifier const verifier = await ethers.deployContract('ZKEmailGroth16VerifierMock'); - // Mock + // Mock ZKEmailUtils const mock = await ethers.deployContract('$ZKEmailUtils'); return { admin, other, accounts, dkim, verifier, mock }; } -function buildEmailAuthMsg(command, params, skippedPrefix) { +function buildEmailProof(command) { // Values specific to ZKEmailGroth16VerifierMock const pA = [1n, 2n]; const pB = [ @@ -48,12 +45,7 @@ function buildEmailAuthMsg(command, params, skippedPrefix) { ]; const pC = [7n, 8n]; - const validProof = ethers.AbiCoder.defaultAbiCoder().encode( - ['uint256[2]', 'uint256[2][2]', 'uint256[2]'], - [pA, pB, pC], - ); - - const emailProof = { + return { domainName, publicKeyHash, timestamp: Math.floor(Date.now() / 1000), @@ -61,63 +53,113 @@ function buildEmailAuthMsg(command, params, skippedPrefix) { emailNullifier, accountSalt, isCodeExist: true, - proof: validProof, - }; - - return { - templateId, - commandParams: params, - skippedCommandPrefix: skippedPrefix, - proof: emailProof, + proof: ethers.AbiCoder.defaultAbiCoder().encode(['uint256[2]', 'uint256[2][2]', 'uint256[2]'], [pA, pB, pC]), }; } -describe('ZKEmail', function () { +describe('ZKEmailUtils', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); it('should validate ZKEmail sign hash', async function () { const hash = ethers.hexlify(ethers.randomBytes(32)); - const emailAuthMsg = buildEmailAuthMsg(SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(), [hash], 0); - await expect(this.mock.$isValidZKEmail(emailAuthMsg, this.dkim.target, this.verifier.target)).to.eventually.equal( + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + const emailProof = buildEmailProof(command); + + // Use the default function that handles signHash template internally + const fnSig = '$isValidZKEmail((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,bytes32)'; + await expect(this.mock[fnSig](emailProof, this.dkim.target, this.verifier.target, hash)).to.eventually.equal( EmailProofError.NoError, ); }); it('should validate ZKEmail with template', async function () { const hash = ethers.hexlify(ethers.randomBytes(32)); - const commandPrefix = 'testCommand'; - const emailAuthMsg = buildEmailAuthMsg(commandPrefix + ' ' + ethers.toBigInt(hash).toString(), [hash], 0); + const commandPrefix = 'emailCommand'; + const command = commandPrefix + ' ' + ethers.toBigInt(hash).toString(); + const emailProof = buildEmailProof(command); const template = [commandPrefix, UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + const fnSig = - '$isValidZKEmail((uint256,bytes[],uint256,(string,bytes32,uint256,string,bytes32,bytes32,bool,bytes)),address,address,string[])'; - await expect(this.mock[fnSig](emailAuthMsg, this.dkim.target, this.verifier.target, template)).to.eventually.equal( - EmailProofError.NoError, - ); + '$isValidZKEmail((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + await expect( + this.mock[fnSig](emailProof, this.dkim.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(EmailProofError.NoError); }); - it('should validate command with address match with different cases', async function () { - const commandPrefix = 'testCommand'; + it('should validate complex email commands with multiple parameters', async function () { + const amount = ethers.parseEther('2.5'); + const recipient = this.other.address; + const command = `Send ${amount.toString()} ETH to ${recipient}`; + const emailProof = buildEmailProof(command); + const template = ['Send', UINT_MATCHER, 'ETH', 'to', ETH_ADDR_MATCHER]; + const templateParams = [ + ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [amount]), + ethers.AbiCoder.defaultAbiCoder().encode(['address'], [recipient]), + ]; + + const fnSig = + '$isValidZKEmail((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + await expect( + this.mock[fnSig](emailProof, this.dkim.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(EmailProofError.NoError); + }); + + it('should validate email maskedCommand from real proof structure', async function () { + // Based on actual email verifier test: "Send 0.1 ETH to 0xafBD210c60dD651892a61804A989eEF7bD63CBA0" + const amount = ethers.parseEther('0.1'); + const recipient = '0xafBD210c60dD651892a61804A989eEF7bD63CBA0'; + const command = `Send ${amount.toString()} ETH to ${recipient}`; + const emailProof = buildEmailProof(command); + const template = ['Send', UINT_MATCHER, 'ETH', 'to', ETH_ADDR_MATCHER]; + const templateParams = [ + ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [amount]), + ethers.AbiCoder.defaultAbiCoder().encode(['address'], [recipient]), + ]; + + const fnSig = + '$isValidZKEmail((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + await expect( + this.mock[fnSig](emailProof, this.dkim.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(EmailProofError.NoError); + }); + + it('should validate command with address match in different cases', async function () { + const commandPrefix = 'authorize'; const template = [commandPrefix, ETH_ADDR_MATCHER]; - for (const { caseType, address } of [ + const testCases = [ { caseType: Case.LOWERCASE, address: this.other.address.toLowerCase(), }, - { caseType: Case.UPPERCASE, address: this.other.address.toUpperCase().replace('0X', '0x') }, - { caseType: Case.CHECKSUM, address: ethers.getAddress(this.other.address) }, - ]) { - const emailAuthMsg = buildEmailAuthMsg(commandPrefix + ' ' + address, [ethers.zeroPadValue(address, 32)], 0); + { + caseType: Case.UPPERCASE, + address: this.other.address.toUpperCase().replace('0X', '0x'), + }, + { + caseType: Case.CHECKSUM, + address: ethers.getAddress(this.other.address), + }, + ]; + + for (const { caseType, address } of testCases) { + const command = commandPrefix + ' ' + address; + const emailProof = buildEmailProof(command); + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['address'], [address])]; + + const fnSig = + '$isValidZKEmail((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[],uint8)'; await expect( - this.mock.$isValidZKEmail(emailAuthMsg, this.dkim.target, this.verifier.target, template, caseType), + this.mock[fnSig](emailProof, this.dkim.target, this.verifier.target, template, templateParams, caseType), ).to.eventually.equal(EmailProofError.NoError); } }); - it('should validate command with address match with any case', async function () { - const commandPrefix = 'testCommand'; + it('should validate command with address match using any case', async function () { + const commandPrefix = 'grant'; const template = [commandPrefix, ETH_ADDR_MATCHER]; // Test with different cases that should all work with ANY case @@ -128,13 +170,19 @@ describe('ZKEmail', function () { ]; for (const address of addresses) { - const emailAuthMsg = buildEmailAuthMsg(commandPrefix + ' ' + address, [ethers.zeroPadValue(address, 32)], 0); + const command = commandPrefix + ' ' + address; + const emailProof = buildEmailProof(command); + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['address'], [address])]; + + const fnSig = + '$isValidZKEmail((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[],uint8)'; await expect( - this.mock.$isValidZKEmail( - emailAuthMsg, + this.mock[fnSig]( + emailProof, this.dkim.target, this.verifier.target, template, + templateParams, ethers.Typed.uint8(Case.ANY), ), ).to.eventually.equal(EmailProofError.NoError); @@ -143,45 +191,72 @@ describe('ZKEmail', function () { it('should detect invalid DKIM public key hash', async function () { const hash = ethers.hexlify(ethers.randomBytes(32)); - const emailAuthMsg = buildEmailAuthMsg(SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(), [hash], 0); - emailAuthMsg.proof.publicKeyHash = ethers.hexlify(ethers.randomBytes(32)); // Use a different public key hash - await expect(this.mock.$isValidZKEmail(emailAuthMsg, this.dkim.target, this.verifier.target)).to.eventually.equal( - EmailProofError.DKIMPublicKeyHash, - ); + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + const emailProof = buildEmailProof(command); + emailProof.publicKeyHash = ethers.hexlify(ethers.randomBytes(32)); // Invalid public key hash + + const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + const fnSig = + '$isValidZKEmail((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + + await expect( + this.mock[fnSig](emailProof, this.dkim.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(EmailProofError.DKIMPublicKeyHash); + }); + + it('should detect unregistered domain', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + const emailProof = buildEmailProof(command); + // Use a domain that hasn't been registered + emailProof.domainName = 'unregistered-domain.com'; + + const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + const fnSig = + '$isValidZKEmail((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + + await expect( + this.mock[fnSig](emailProof, this.dkim.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(EmailProofError.DKIMPublicKeyHash); }); it('should detect invalid masked command length', async function () { // Create a command that's too long (606 bytes) const longCommand = 'a'.repeat(606); - const emailAuthMsg = buildEmailAuthMsg(longCommand, [ethers.hexlify(ethers.randomBytes(32))], 0); - await expect(this.mock.$isValidZKEmail(emailAuthMsg, this.dkim.target, this.verifier.target)).to.eventually.equal( - EmailProofError.MaskedCommandLength, - ); - }); + const emailProof = buildEmailProof(longCommand); - it('should detect invalid skipped command prefix', async function () { - const hash = ethers.hexlify(ethers.randomBytes(32)); - const emailAuthMsg = buildEmailAuthMsg( - SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(), - [hash], - BigInt(commandBytes) + 1n, // Set skipped prefix to be larger than commandBytes - ); - await expect(this.mock.$isValidZKEmail(emailAuthMsg, this.dkim.target, this.verifier.target)).to.eventually.equal( - EmailProofError.SkippedCommandPrefixSize, - ); + const template = ['a'.repeat(606)]; + const templateParams = []; + const fnSig = + '$isValidZKEmail((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + + await expect( + this.mock[fnSig](emailProof, this.dkim.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(EmailProofError.MaskedCommandLength); }); - it('should detect mismatched command', async function () { + it('should detect mismatched command template', async function () { const hash = ethers.hexlify(ethers.randomBytes(32)); - const emailAuthMsg = buildEmailAuthMsg('invalidCommand ' + ethers.toBigInt(hash).toString(), [hash], 0); - await expect(this.mock.$isValidZKEmail(emailAuthMsg, this.dkim.target, this.verifier.target)).to.eventually.equal( - EmailProofError.MismatchedCommand, - ); + const command = 'invalidEmailCommand ' + ethers.toBigInt(hash).toString(); + const emailProof = buildEmailProof(command); + const template = ['differentCommand', UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + + const fnSig = + '$isValidZKEmail((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + await expect( + this.mock[fnSig](emailProof, this.dkim.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(EmailProofError.MismatchedCommand); }); it('should detect invalid email proof', async function () { const hash = ethers.hexlify(ethers.randomBytes(32)); - const emailAuthMsg = buildEmailAuthMsg(SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(), [hash], 0); + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + const emailProof = buildEmailProof(command); + + // Create invalid proof that will fail verification const pA = [1n, 1n]; const pB = [ [1n, 1n], @@ -192,8 +267,10 @@ describe('ZKEmail', function () { ['uint256[2]', 'uint256[2][2]', 'uint256[2]'], [pA, pB, pC], ); - emailAuthMsg.proof.proof = invalidProof; - await expect(this.mock.$isValidZKEmail(emailAuthMsg, this.dkim.target, this.verifier.target)).to.eventually.equal( + emailProof.proof = invalidProof; + + const fnSig = '$isValidZKEmail((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,bytes32)'; + await expect(this.mock[fnSig](emailProof, this.dkim.target, this.verifier.target, hash)).to.eventually.equal( EmailProofError.EmailProof, ); });