diff --git a/packages/contracts/src/EmailAuthSigner.sol b/packages/contracts/src/EmailAuthSigner.sol new file mode 100644 index 0000000..cb55f25 --- /dev/null +++ b/packages/contracts/src/EmailAuthSigner.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; + +import {EmailProof} from "./utils/Verifier.sol"; +import {IDKIMRegistry} from "@zk-email/contracts/DKIMRegistry.sol"; +import {IVerifier, EmailProof} from "./interfaces/IVerifier.sol"; +import {CommandUtils} from "./libraries/CommandUtils.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +/// @notice Struct to hold the email authentication/authorization message. +struct EmailAuthMsg { + /// @notice The ID of the command template that the command in the email body should satisfy. + uint templateId; + /// @notice The parameters in the command of the email body, which should be taken according to the specified command template. + bytes[] commandParams; + /// @notice The number of skipped bytes in the command. + uint skippedCommandPrefix; + /// @notice The email proof containing the zk proof and other necessary information for the email verification by the verifier contract. + EmailProof proof; +} + +/// @title Email Authentication/Authorization Contract +/// @notice This contract provides functionalities for the authentication of the email sender and the authorization of the message in the command part of the email body using DKIM and custom verification logic. +/// @dev Inherits from OwnableUpgradeable and UUPSUpgradeable for upgradeability and ownership management. +contract EmailAuthSigner is OwnableUpgradeable, UUPSUpgradeable { + /// The CREATE2 salt of this contract defined as a hash of an email address and an account code. + bytes32 public accountSalt; + /// An instance of the DKIM registry contract. + address public dkimRegistryAddr; + /// An instance of the Verifier contract. + address public verifierAddr; + + event DKIMRegistryUpdated(address indexed dkimRegistry); + event VerifierUpdated(address indexed verifier); + event EmailAuthed( + bytes32 indexed emailNullifier, + bytes32 indexed accountSalt, + bool isCodeExist, + uint templateId + ); + + constructor() {} + + /// @notice Initialize the contract with an initial owner, account salt, DKIM registry address, and verifier address. + /// @param _initialOwner The address of the initial owner. + /// @param _accountSalt The account salt to derive CREATE2 address of this contract. + /// @param _dkimRegistryAddr The address of the DKIM registry contract. + /// @param _verifierAddr The address of the verifier contract. + function initialize( + address _initialOwner, + bytes32 _accountSalt, + address _dkimRegistryAddr, + address _verifierAddr + ) public initializer { + __Ownable_init(_initialOwner); + accountSalt = _accountSalt; + + require( + _dkimRegistryAddr != address(0), + "invalid dkim registry address" + ); + require( + address(dkimRegistryAddr) == address(0), + "dkim registry already initialized" + ); + dkimRegistryAddr = _dkimRegistryAddr; + emit DKIMRegistryUpdated(_dkimRegistryAddr); + + require(_verifierAddr != address(0), "invalid verifier address"); + require( + address(verifierAddr) == address(0), + "verifier already initialized" + ); + verifierAddr = _verifierAddr; + emit VerifierUpdated(_verifierAddr); + } + + /// @notice Updates the address of the DKIM registry contract. + /// @param _dkimRegistryAddr The new address of the DKIM registry contract. + function updateDKIMRegistry(address _dkimRegistryAddr) public onlyOwner { + require( + _dkimRegistryAddr != address(0), + "invalid dkim registry address" + ); + dkimRegistryAddr = _dkimRegistryAddr; + emit DKIMRegistryUpdated(_dkimRegistryAddr); + } + + /// @notice Updates the address of the verifier contract. + /// @param _verifierAddr The new address of the verifier contract. + function updateVerifier(address _verifierAddr) public onlyOwner { + require(_verifierAddr != address(0), "invalid verifier address"); + verifierAddr = _verifierAddr; + emit VerifierUpdated(_verifierAddr); + } + + /// @notice Authenticate the email sender and authorize the message in the email command based on the provided email auth message. + /// @param emailAuthMsg The email auth message containing all necessary information for authentication and authorization. + function authEmail(EmailAuthMsg memory emailAuthMsg) public { + string[] memory signHashTemplate = new string[](2); + signHashTemplate[0] = "signHash"; + signHashTemplate[1] = "{uint}"; + require( + IDKIMRegistry(dkimRegistryAddr).isDKIMPublicKeyHashValid( + emailAuthMsg.proof.domainName, + emailAuthMsg.proof.publicKeyHash + ) == true, + "invalid dkim public key hash" + ); + require( + accountSalt == emailAuthMsg.proof.accountSalt, + "invalid account salt" + ); + require( + bytes(emailAuthMsg.proof.maskedCommand).length <= + IVerifier(verifierAddr).commandBytes(), + "invalid masked command length" + ); + require( + emailAuthMsg.skippedCommandPrefix < + IVerifier(verifierAddr).commandBytes(), + "invalid size of the skipped command prefix" + ); + + // Construct an expectedCommand from template and the values of emailAuthMsg.commandParams. + string memory trimmedMaskedCommand = removePrefix( + emailAuthMsg.proof.maskedCommand, + emailAuthMsg.skippedCommandPrefix + ); + string memory expectedCommand = ""; + for (uint stringCase = 0; stringCase < 3; stringCase++) { + expectedCommand = CommandUtils.computeExpectedCommand( + emailAuthMsg.commandParams, + signHashTemplate, + stringCase + ); + if (Strings.equal(expectedCommand, trimmedMaskedCommand)) { + break; + } + if (stringCase == 2) { + revert("invalid command"); + } + } + + require( + IVerifier(verifierAddr).verifyEmailProof(emailAuthMsg.proof) == + true, + "invalid email proof" + ); + + emit EmailAuthed( + emailAuthMsg.proof.emailNullifier, + emailAuthMsg.proof.accountSalt, + emailAuthMsg.proof.isCodeExist, + emailAuthMsg.templateId + ); + } + + /// @notice Upgrade the implementation of the proxy. + /// @param newImplementation Address of the new implementation. + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} + + /// @notice Remove a prefix from a string. + /// @param str The original string. + /// @param numBytes The number of bytes to remove from the start of the string. + /// @return The string with the prefix removed. + function removePrefix( + string memory str, + uint numBytes + ) private pure returns (string memory) { + require( + numBytes <= bytes(str).length, + "Invalid size of the removed bytes" + ); + + bytes memory strBytes = bytes(str); + bytes memory result = new bytes(strBytes.length - numBytes); + + for (uint i = numBytes; i < strBytes.length; i++) { + result[i - numBytes] = strBytes[i]; + } + + return string(result); + } +} diff --git a/packages/contracts/test/EmailAuthSigner.t.sol b/packages/contracts/test/EmailAuthSigner.t.sol new file mode 100644 index 0000000..d16c022 --- /dev/null +++ b/packages/contracts/test/EmailAuthSigner.t.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../src/utils/Verifier.sol"; +import "../src/utils/ECDSAOwnedDKIMRegistry.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "./helpers/SignerStructHelper.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract EmailAuthSignerTest is SignerStructHelper { + function setUp() public override { + super.setUp(); + + vm.startPrank(deployer); + // TODO: expect emit + emailAuthSigner.initialize( + deployer, + accountSalt, + address(dkim), + address(verifier) + ); + vm.stopPrank(); + } + + function testDkimRegistryAddr() public view { + address dkimAddr = emailAuthSigner.dkimRegistryAddr(); + assertEq(dkimAddr, address(dkim)); + } + + function testVerifierAddr() public view { + address verifierAddr = emailAuthSigner.verifierAddr(); + assertEq(verifierAddr, address(verifier)); + } + + function testUpdateDKIMRegistryToECDSA() public { + assertEq(emailAuthSigner.dkimRegistryAddr(), address(dkim)); + + vm.startPrank(deployer); + ECDSAOwnedDKIMRegistry newDKIM; + { + ECDSAOwnedDKIMRegistry dkimImpl = new ECDSAOwnedDKIMRegistry(); + ERC1967Proxy dkimProxy = new ERC1967Proxy( + address(dkimImpl), + abi.encodeCall(dkimImpl.initialize, (msg.sender, msg.sender)) + ); + newDKIM = ECDSAOwnedDKIMRegistry(address(dkimProxy)); + } + vm.expectEmit(true, false, false, false); + emit EmailAuthSigner.DKIMRegistryUpdated(address(newDKIM)); + emailAuthSigner.updateDKIMRegistry(address(newDKIM)); + vm.stopPrank(); + + assertEq(emailAuthSigner.dkimRegistryAddr(), address(newDKIM)); + } + + function testExpectRevertUpdateDKIMRegistryInvalidDkimRegistryAddress() + public + { + assertEq(emailAuthSigner.dkimRegistryAddr(), address(dkim)); + + vm.startPrank(deployer); + vm.expectRevert(bytes("invalid dkim registry address")); + emailAuthSigner.updateDKIMRegistry(address(0)); + vm.stopPrank(); + } + + function testUpdateVerifier() public { + assertEq(emailAuthSigner.verifierAddr(), address(verifier)); + + vm.startPrank(deployer); + Verifier newVerifier = new Verifier(); + vm.expectEmit(true, false, false, false); + emit EmailAuthSigner.VerifierUpdated(address(newVerifier)); + emailAuthSigner.updateVerifier(address(newVerifier)); + vm.stopPrank(); + + assertEq(emailAuthSigner.verifierAddr(), address(newVerifier)); + } + + function testExpectRevertUpdateVerifierInvalidVerifierAddress() public { + assertEq(emailAuthSigner.verifierAddr(), address(verifier)); + + vm.startPrank(deployer); + vm.expectRevert(bytes("invalid verifier address")); + emailAuthSigner.updateVerifier(address(0)); + vm.stopPrank(); + } + + function testAuthEmail() public { + EmailAuthMsg memory emailAuthMsg = buildEmailAuthMsg(); + + vm.expectEmit(true, true, true, true); + emit EmailAuthSigner.EmailAuthed( + emailAuthMsg.proof.emailNullifier, + emailAuthMsg.proof.accountSalt, + emailAuthMsg.proof.isCodeExist, + emailAuthMsg.templateId + ); + emailAuthSigner.authEmail(emailAuthMsg); + } + + function testExpectRevertAuthEmailInvalidDkimPublicKeyHash() public { + EmailAuthMsg memory emailAuthMsg = buildEmailAuthMsg(); + emailAuthMsg.proof.domainName = "invalid.com"; + + vm.expectRevert(bytes("invalid dkim public key hash")); + emailAuthSigner.authEmail(emailAuthMsg); + } + + function testExpectRevertAuthEmailInvalidAccountSalt() public { + EmailAuthMsg memory emailAuthMsg = buildEmailAuthMsg(); + emailAuthMsg.proof.accountSalt = bytes32(uint256(1234)); + + vm.expectRevert(bytes("invalid account salt")); + emailAuthSigner.authEmail(emailAuthMsg); + } + + function testExpectRevertAuthEmailInvalidCommand() public { + EmailAuthMsg memory emailAuthMsg = buildEmailAuthMsg(); + emailAuthMsg.commandParams[0] = abi.encode(2 ether); + + vm.expectRevert(bytes("invalid command")); + emailAuthSigner.authEmail(emailAuthMsg); + } + + function testExpectRevertAuthEmailInvalidEmailProof() public { + EmailAuthMsg memory emailAuthMsg = buildEmailAuthMsg(); + + vm.mockCall( + address(verifier), + abi.encodeWithSelector( + Verifier.verifyEmailProof.selector, + emailAuthMsg.proof + ), + abi.encode(false) + ); + vm.expectRevert(bytes("invalid email proof")); + emailAuthSigner.authEmail(emailAuthMsg); + } + + function testExpectRevertAuthEmailInvalidMaskedCommandLength() public { + EmailAuthMsg memory emailAuthMsg = buildEmailAuthMsg(); + + // Set masked command length to 606, which should be 605 or less defined in the verifier. + emailAuthMsg.proof.maskedCommand = string(new bytes(606)); + + vm.expectRevert(bytes("invalid masked command length")); + emailAuthSigner.authEmail(emailAuthMsg); + } + + function testExpectRevertAuthEmailInvalidSizeOfTheSkippedCommandPrefix() + public + { + EmailAuthMsg memory emailAuthMsg = buildEmailAuthMsg(); + + // Set skipped command prefix length to 605, it should be less than 605. + emailAuthMsg.skippedCommandPrefix = 605; + + vm.expectRevert(bytes("invalid size of the skipped command prefix")); + emailAuthSigner.authEmail(emailAuthMsg); + } + + function testUpgradeEmailAuth() public { + vm.startPrank(deployer); + + // Deploy new implementation + EmailAuthSigner newImplementation = new EmailAuthSigner(); + + // Execute upgrade using proxy + // Upgrade implementation through proxy contract + ERC1967Proxy proxy = new ERC1967Proxy( + address(emailAuthSigner), + abi.encodeCall( + emailAuthSigner.initialize, + (deployer, accountSalt, address(dkim), address(verifier)) + ) + ); + EmailAuthSigner emailAuthSignerProxy = EmailAuthSigner(payable(proxy)); + bytes32 beforeAccountSalt = emailAuthSignerProxy.accountSalt(); + + // Upgrade to new implementation through proxy + emailAuthSignerProxy.upgradeToAndCall( + address(newImplementation), + new bytes(0) + ); + + bytes32 afterAccountSalt = emailAuthSignerProxy.accountSalt(); + + // Verify the upgrade + assertEq(beforeAccountSalt, afterAccountSalt); + + vm.stopPrank(); + } +} diff --git a/packages/contracts/test/helpers/SignerDeploymentHelper.sol b/packages/contracts/test/helpers/SignerDeploymentHelper.sol new file mode 100644 index 0000000..64bece9 --- /dev/null +++ b/packages/contracts/test/helpers/SignerDeploymentHelper.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {EmailAuthSigner, EmailAuthMsg} from "../../src/EmailAuthSigner.sol"; +import {Verifier, EmailProof} from "../../src/utils/Verifier.sol"; +import {Groth16Verifier} from "../../src/utils/Groth16Verifier.sol"; +import {ECDSAOwnedDKIMRegistry} from "../../src/utils/ECDSAOwnedDKIMRegistry.sol"; +import {UserOverrideableDKIMRegistry} from "@zk-email/contracts/UserOverrideableDKIMRegistry.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract SignerDeploymentHelper is Test { + using ECDSA for *; + + EmailAuthSigner emailAuthSigner; + Verifier verifier; + ECDSAOwnedDKIMRegistry dkim; + UserOverrideableDKIMRegistry overrideableDkimImpl; + + address deployer = vm.addr(1); + address receiver = vm.addr(2); + address guardian; + address newSigner = vm.addr(4); + address someRelayer = vm.addr(5); + + bytes32 accountSalt; + uint templateId; + string[] commandTemplate; + bytes mockProof = abi.encodePacked(bytes1(0x01)); + + string selector = "12345"; + string domainName = "gmail.com"; + bytes32 publicKeyHash = + 0x0ea9c777dc7110e5a9e89b13f0cfc540e3845ba120b2b6dc24024d61488d4788; + bytes32 emailNullifier = + 0x00a83fce3d4b1c9ef0f600644c1ecc6c8115b57b1596e0e3295e2c5105fbfd8a; + uint256 setTimestampDelay = 3 days; + + bytes32 public proxyBytecodeHash = + vm.envOr("PROXY_BYTECODE_HASH", bytes32(0)); + + function setUp() public virtual { + vm.startPrank(deployer); + address signer = deployer; + + // Create DKIM registry + { + ECDSAOwnedDKIMRegistry ecdsaDkimImpl = new ECDSAOwnedDKIMRegistry(); + ERC1967Proxy ecdsaDkimProxy = new ERC1967Proxy( + address(ecdsaDkimImpl), + abi.encodeCall(ecdsaDkimImpl.initialize, (deployer, signer)) + ); + dkim = ECDSAOwnedDKIMRegistry(address(ecdsaDkimProxy)); + } + string memory signedMsg = dkim.computeSignedMsg( + dkim.SET_PREFIX(), + domainName, + publicKeyHash + ); + bytes32 digest = MessageHashUtils.toEthSignedMessageHash( + bytes(signedMsg) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, digest); + bytes memory signature = abi.encodePacked(r, s, v); + dkim.setDKIMPublicKeyHash( + selector, + domainName, + publicKeyHash, + signature + ); + + // Create userOverrideable dkim registry implementation + overrideableDkimImpl = new UserOverrideableDKIMRegistry(); + // { + // UserOverrideableDKIMRegistry overrideableDkimImpl = new UserOverrideableDKIMRegistry(); + // ERC1967Proxy overrideableDkimProxy = new ERC1967Proxy( + // address(overrideableDkimImpl), + // abi.encodeCall( + // overrideableDkimImpl.initialize, + // (deployer, signer, setTimestampDelay) + // ) + // ); + // overrideableDkim = UserOverrideableDKIMRegistry( + // address(overrideableDkimProxy) + // ); + // } + // overrideableDkim.setDKIMPublicKeyHash( + // domainName, + // publicKeyHash, + // deployer, + // new bytes(0) + // ); + + // Create Verifier + { + Verifier verifierImpl = new Verifier(); + console.log( + "Verifier implementation deployed at: %s", + address(verifierImpl) + ); + Groth16Verifier groth16Verifier = new Groth16Verifier(); + ERC1967Proxy verifierProxy = new ERC1967Proxy( + address(verifierImpl), + abi.encodeCall( + verifierImpl.initialize, + (msg.sender, address(groth16Verifier)) + ) + ); + verifier = Verifier(address(verifierProxy)); + } + accountSalt = 0x2c3abbf3d1171bfefee99c13bf9c47f1e8447576afd89096652a34f27b297971; + + // Create EmailAuth implementation + EmailAuthSigner emailAuthSignerImpl = new EmailAuthSigner(); + emailAuthSigner = emailAuthSignerImpl; + + uint templateIdx = 0; + templateId = uint256(keccak256(abi.encodePacked("TEST", templateIdx))); + commandTemplate = ["signHash", "{uint}"]; + vm.stopPrank(); + } + + function isZkSync() public view returns (bool) { + return block.chainid == 324 || block.chainid == 300; + } + + function skipIfZkSync() public { + if (isZkSync()) { + vm.skip(true); + } else { + vm.skip(false); + } + } + + function skipIfNotZkSync() public { + if (!isZkSync()) { + vm.skip(true); + } else { + vm.skip(false); + } + } + + function resetEnviromentVariables() public { + vm.setEnv("PRIVATE_KEY", vm.toString(uint256(0))); + vm.setEnv("INITIAL_OWNER", vm.toString(uint256(0))); + vm.setEnv("DKIM_SIGNER", vm.toString(address(0))); + vm.setEnv("DKIM", vm.toString(address(0))); + vm.setEnv("DKIM_DELAY", vm.toString(uint256(0))); + vm.setEnv("ECDSA_DKIM", vm.toString(address(0))); + vm.setEnv("VERIFIER", vm.toString(address(0))); + vm.setEnv("EMAIL_AUTH_SIGNER_IMPL", vm.toString(address(0))); + vm.setEnv("RECOVERY_CONTROLLER", vm.toString(address(0))); + vm.setEnv("RECOVERY_CONTROLLER_ZKSYNC", vm.toString(address(0))); + vm.setEnv("ZKSYNC_CREATE2_FACTORY", vm.toString(address(0))); + vm.setEnv("SIMPLE_WALLET", vm.toString(address(0))); + } +} diff --git a/packages/contracts/test/helpers/SignerStructHelper.sol b/packages/contracts/test/helpers/SignerStructHelper.sol new file mode 100644 index 0000000..5a01669 --- /dev/null +++ b/packages/contracts/test/helpers/SignerStructHelper.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; + +import "./SignerDeploymentHelper.sol"; + +contract SignerStructHelper is SignerDeploymentHelper { + function buildEmailAuthMsg() + public + returns (EmailAuthMsg memory emailAuthMsg) + { + bytes[] memory commandParams = new bytes[](1); + commandParams[0] = abi.encode(1234567890); + + EmailProof memory emailProof = EmailProof({ + domainName: "gmail.com", + publicKeyHash: publicKeyHash, + timestamp: 1694989812, + maskedCommand: "signHash 1234567890", + emailNullifier: emailNullifier, + accountSalt: accountSalt, + isCodeExist: true, + proof: mockProof + }); + + emailAuthMsg = EmailAuthMsg({ + templateId: templateId, + commandParams: commandParams, + skippedCommandPrefix: 0, + proof: emailProof + }); + + vm.mockCall( + address(verifier), + abi.encodeCall(Verifier.verifyEmailProof, (emailProof)), + abi.encode(true) + ); + } +}