diff --git a/package.json b/package.json index 464762654..174a816f9 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "6.1.1", + "version": "6.1.2", "license": "MIT", "private": true, "scripts": { diff --git a/packages/circuits/package.json b/packages/circuits/package.json index 1aa4f6687..4adbd3603 100644 --- a/packages/circuits/package.json +++ b/packages/circuits/package.json @@ -1,6 +1,6 @@ { "name": "@zk-email/circuits", - "version": "6.1.1", + "version": "6.1.2", "license": "MIT", "scripts": { "publish": "yarn npm publish --access=public", diff --git a/packages/contracts/README.md b/packages/contracts/README.md index a8e2508fc..a659545f1 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -16,6 +16,13 @@ For a detailed overview of its functionalities, please refer to the source file: [DKIMRegistry.sol](./DKIMRegistry.sol) +## UserOverrideableDKIMRegistry.sol + +`UserOverrideableDKIMRegistry.sol` is a Solidity contract within the `@zk-email/contracts` package. +This functions similarly to [DKIMRegistry](./DKIMRegistry.sol), but it allows users to set their own public keys. Even if the main authorizer, who is the contract owner, has already approved a public key, the user's signature is still required for setting it. Additionally, the public key can be revoked by the signature of either the user or the main authorizer alone. + +[UserOverrideableDKIMRegistry.sol](./UserOverrideableDKIMRegistry.sol) + ## StringUtils.sol `StringUtils.sol` is a Solidity library that offers a range of string manipulation functions, including conversion between bytes and strings, and numerical string operations, for use across the `@zk-email/contracts` package. diff --git a/packages/contracts/UserOverrideableDKIMRegistry.sol b/packages/contracts/UserOverrideableDKIMRegistry.sol new file mode 100644 index 000000000..1a519893d --- /dev/null +++ b/packages/contracts/UserOverrideableDKIMRegistry.sol @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./interfaces/IDKIMRegistry.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +/** + A Registry that store the hash(dkim_public_key) for each domain + The hash is calculated by taking Poseidon of DKIM key split into 9 chunks of 242 bits each + + https://zkrepl.dev/?gist=43ce7dce2466c63812f6efec5b13aa73 can be used to generate the public key hash. + The same code is used in EmailVerifier.sol + Input is DKIM pub key split into 17 chunks of 121 bits. You can use `helpers` package to fetch/split DKIM keys + */ +contract UserOverrideableDKIMRegistry is IDKIMRegistry, Ownable { + using Strings for *; + using ECDSA for *; + + event DKIMPublicKeyHashRegistered( + string domainName, + bytes32 publicKeyHash, + address register + ); + event DKIMPublicKeyHashRevoked(bytes32 publicKeyHash, address register); + event DKIMPublicKeyHashReactivated(bytes32 publicKeyHash, address register); + + // Main authorizer address. + address public mainAuthorizer; + + // Mapping from domain name to DKIM public key hash + mapping(string => mapping(bytes32 => mapping(address => bool))) + public dkimPublicKeyHashes; + + // DKIM public that are revoked (eg: in case of private key compromise) + mapping(bytes32 => mapping(address => bool)) + public revokedDKIMPublicKeyHashes; + + // DKIM public that are reactivated (eg: in case that a malicious `mainAuthorizer` revokes a valid public key but a user reactivates it.) + mapping(bytes32 => mapping(address => bool)) + public reactivatedDKIMPublicKeyHashes; + + string public constant SET_PREFIX = "SET:"; + string public constant REVOKE_PREFIX = "REVOKE:"; + string public constant REACTIVATE_PREFIX = "REACTIVATE"; + + constructor(address _owner, address _mainAuthorizer) Ownable(_owner) { + mainAuthorizer = _mainAuthorizer; + } + + function isDKIMPublicKeyHashValid( + string memory domainName, + bytes32 publicKeyHash + ) public view returns (bool) { + address ownerOfSender = Ownable(msg.sender).owner(); + return + isDKIMPublicKeyHashValid(domainName, publicKeyHash, ownerOfSender); + } + + function isDKIMPublicKeyHashValid( + string memory domainName, + bytes32 publicKeyHash, + address authorizer + ) public view returns (bool) { + require(bytes(domainName).length > 0, "domain name cannot be zero"); + require(publicKeyHash != bytes32(0), "public key hash cannot be zero"); + require(authorizer != address(0), "authorizer address cannot be zero"); + uint256 revokeThreshold = _computeRevokeThreshold( + publicKeyHash, + authorizer + ); + uint256 setThreshold = _computeSetThreshold( + domainName, + publicKeyHash, + authorizer + ); + if (revokeThreshold >= 1) { + return false; + } else if (setThreshold < 2) { + return false; + } else { + return true; + } + } + + /** + * @notice Sets the DKIM public key hash for a given domain with authorization. + * @dev This function allows an authorized user or a contract to set a DKIM public key hash. It uses EIP-1271 or ECDSA for signature verification. + * @param domainName The domain name for which the DKIM public key hash is being set. + * @param publicKeyHash The hash of the DKIM public key to be set. + * @param authorizer The address of the authorizer who can set the DKIM public key hash. + * @param signature The signature proving the authorization to set the DKIM public key hash. + * @custom:require The domain name, public key hash, and authorizer address must not be zero. + * @custom:require The public key hash must not be revoked. + * @custom:require The signature must be valid according to EIP-1271 if the authorizer is a contract, or ECDSA if the authorizer is an EOA. + * @custom:event DKIMPublicKeyHashRegistered Emitted when a DKIM public key hash is successfully set. + */ + function setDKIMPublicKeyHash( + string memory domainName, + bytes32 publicKeyHash, + address authorizer, + bytes memory signature + ) public { + require(bytes(domainName).length > 0, "domain name cannot be zero"); + require(publicKeyHash != bytes32(0), "public key hash cannot be zero"); + require(authorizer != address(0), "authorizer address cannot be zero"); + require( + revokedDKIMPublicKeyHashes[publicKeyHash][authorizer] == false, + "public key hash is already revoked" + ); + if (msg.sender != authorizer) { + string memory signedMsg = computeSignedMsg( + SET_PREFIX, + domainName, + publicKeyHash + ); + bytes32 digest = MessageHashUtils.toEthSignedMessageHash( + bytes(signedMsg) + ); + if (authorizer.code.length > 0) { + require( + IERC1271(authorizer).isValidSignature(digest, signature) == + 0x1626ba7e, + "invalid eip1271 signature" + ); + } else { + address recoveredSigner = digest.recover(signature); + require( + recoveredSigner == authorizer, + "invalid ecdsa signature" + ); + } + } + + dkimPublicKeyHashes[domainName][publicKeyHash][authorizer] = true; + + emit DKIMPublicKeyHashRegistered(domainName, publicKeyHash, authorizer); + } + + /** + * @dev Sets the DKIM public key hashes in batch. + * @param domainNames An array of the domain name for which the DKIM public key hash is being set. + * @param publicKeyHashes An array of the hash of the DKIM public key to be set. + * @param authorizers An array of the address of the authorizer who can set the DKIM public key hash. + * @param signatures An array of the signature proving the authorization to set the DKIM public key hash. + * @custom:require The domain name, public key hash, and authorizer address must not be zero. + * @custom:require The public key hash must not be revoked. + * @custom:require The signature must be valid according to EIP-1271 if the authorizer is a contract, or ECDSA if the authorizer is an EOA. + * @custom:event DKIMPublicKeyHashRegistered Emitted when a DKIM public key hash is successfully set. + */ + function setDKIMPublicKeyHashes( + string[] memory domainNames, + bytes32[] memory publicKeyHashes, + address[] memory authorizers, + bytes[] memory signatures + ) public { + require( + domainNames.length == publicKeyHashes.length, + "invalid publicKeyHashes length" + ); + require( + domainNames.length == authorizers.length, + "invalid authorizers length" + ); + require( + domainNames.length == signatures.length, + "invalid signatures length" + ); + for (uint256 i = 0; i < domainNames.length; i++) { + setDKIMPublicKeyHash( + domainNames[i], + publicKeyHashes[i], + authorizers[i], + signatures[i] + ); + } + } + + /** + * @notice Revokes a DKIM public key hash. + * @dev This function allows the owner to revoke a DKIM public key hash for all users, or an individual user to revoke it for themselves. + * @param domainName The domain name associated with the DKIM public key hash. + * @param publicKeyHash The hash of the DKIM public key to be revoked. + * @param authorizer The address of the authorizer who can revoke the DKIM public key hash. + * @param signature The signature proving the authorization to revoke the DKIM public key hash. + * @custom:require The domain name, public key hash, and authorizer address must not be zero. + * @custom:require The public key hash must not already be revoked. + * @custom:require The signature must be valid according to EIP-1271 if the authorizer is a contract, or ECDSA if the authorizer is an EOA. + * @custom:event DKIMPublicKeyHashRevoked Emitted when a DKIM public key hash is successfully revoked. + */ + function revokeDKIMPublicKeyHash( + string memory domainName, + bytes32 publicKeyHash, + address authorizer, + bytes memory signature + ) public { + require(bytes(domainName).length > 0, "domain name cannot be zero"); + require(publicKeyHash != bytes32(0), "public key hash cannot be zero"); + require(authorizer != address(0), "authorizer address cannot be zero"); + require( + revokedDKIMPublicKeyHashes[publicKeyHash][authorizer] == false, + "public key hash is already revoked" + ); + if (msg.sender != authorizer) { + string memory signedMsg = computeSignedMsg( + REVOKE_PREFIX, + domainName, + publicKeyHash + ); + bytes32 digest = MessageHashUtils.toEthSignedMessageHash( + bytes(signedMsg) + ); + if (authorizer.code.length > 0) { + require( + IERC1271(authorizer).isValidSignature(digest, signature) == + 0x1626ba7e, + "invalid eip1271 signature" + ); + } else { + address recoveredSigner = digest.recover(signature); + require( + recoveredSigner == authorizer, + "invalid ecdsa signature" + ); + } + } + revokedDKIMPublicKeyHashes[publicKeyHash][authorizer] = true; + + emit DKIMPublicKeyHashRevoked(publicKeyHash, authorizer); + } + + function reactivateDKIMPublicKeyHash( + string memory domainName, + bytes32 publicKeyHash, + address authorizer, + bytes memory signature + ) public { + require(bytes(domainName).length > 0, "domain name cannot be zero"); + require(publicKeyHash != bytes32(0), "public key hash cannot be zero"); + require(authorizer != address(0), "authorizer address cannot be zero"); + require( + reactivatedDKIMPublicKeyHashes[publicKeyHash][authorizer] == false, + "public key hash is already reactivated" + ); + require( + authorizer != mainAuthorizer, + "mainAuthorizer cannot reactivate the public key hash" + ); + require( + _computeRevokeThreshold(publicKeyHash, authorizer) == 1, + "revoke threshold must be one" + ); + require( + _computeSetThreshold(domainName, publicKeyHash, authorizer) >= 2, + "set threshold must be larger than two" + ); + if (msg.sender != authorizer) { + string memory signedMsg = computeSignedMsg( + REACTIVATE_PREFIX, + domainName, + publicKeyHash + ); + bytes32 digest = MessageHashUtils.toEthSignedMessageHash( + bytes(signedMsg) + ); + if (authorizer.code.length > 0) { + require( + IERC1271(authorizer).isValidSignature(digest, signature) == + 0x1626ba7e, + "invalid eip1271 signature" + ); + } else { + address recoveredSigner = digest.recover(signature); + require( + recoveredSigner == authorizer, + "invalid ecdsa signature" + ); + } + } + reactivatedDKIMPublicKeyHashes[publicKeyHash][authorizer] = true; + + emit DKIMPublicKeyHashReactivated(publicKeyHash, authorizer); + } + + /** + * @notice Computes a signed message string for setting or revoking a DKIM public key hash. + * @param prefix The operation prefix (SET: or REVOKE:). + * @param domainName The domain name related to the operation. + * @param publicKeyHash The DKIM public key hash involved in the operation. + * @return string The computed signed message. + * @dev This function is used internally to generate the message that needs to be signed for setting or revoking a public key hash. + */ + function computeSignedMsg( + string memory prefix, + string memory domainName, + bytes32 publicKeyHash + ) public pure returns (string memory) { + return + string.concat( + prefix, + ";domain=", + domainName, + ";public_key_hash=", + uint256(publicKeyHash).toHexString(), + ";" + ); + } + + function _computeSetThreshold( + string memory domainName, + bytes32 publicKeyHash, + address authorizer + ) private view returns (uint256) { + uint256 threshold = 0; + if ( + dkimPublicKeyHashes[domainName][publicKeyHash][mainAuthorizer] == + true + ) { + threshold += 1; + } + if ( + dkimPublicKeyHashes[domainName][publicKeyHash][authorizer] == true + ) { + threshold += 2; + } + return threshold; + } + + function _computeRevokeThreshold( + bytes32 publicKeyHash, + address authorizer + ) private view returns (uint256) { + uint256 threshold = 0; + if (revokedDKIMPublicKeyHashes[publicKeyHash][mainAuthorizer] == true) { + threshold += 1; + } + if (revokedDKIMPublicKeyHashes[publicKeyHash][authorizer] == true) { + threshold += 2; + } + if ( + threshold == 1 && + reactivatedDKIMPublicKeyHashes[publicKeyHash][authorizer] == true + ) { + threshold -= 1; + } + return threshold; + } + + function _stringEq( + string memory a, + string memory b + ) internal pure returns (bool) { + return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)); + } +} diff --git a/packages/contracts/foundry.toml b/packages/contracts/foundry.toml index 0fcf08be4..bf2fd4cd0 100644 --- a/packages/contracts/foundry.toml +++ b/packages/contracts/foundry.toml @@ -3,4 +3,4 @@ src = './' out = 'out' allow_paths = ['../../node_modules'] libs = ['../../node_modules'] -solc_version = '0.8.21' +solc_version = '0.8.23' diff --git a/packages/contracts/package.json b/packages/contracts/package.json index a6c907ca1..b51bc460d 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@zk-email/contracts", - "version": "6.1.1", + "version": "6.1.2", "license": "MIT", "scripts": { "build": "forge build", @@ -12,6 +12,7 @@ }, "files": [ "DKIMRegistry.sol", + "UserOverrideableDKIMRegistry.sol", "/utils", "/interfaces" ], diff --git a/packages/contracts/test/UserOverrideableDKIMRegistry.t.sol b/packages/contracts/test/UserOverrideableDKIMRegistry.t.sol new file mode 100644 index 000000000..dceed7a94 --- /dev/null +++ b/packages/contracts/test/UserOverrideableDKIMRegistry.t.sol @@ -0,0 +1,745 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; + +import "forge-std/src/Test.sol"; +import "forge-std/src/console.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../UserOverrideableDKIMRegistry.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import "./helpers/ExampleERC1271.sol"; +import "./helpers/ExampleOwnable.sol"; + +contract UserOverrideableDKIMRegistryTest is Test { + UserOverrideableDKIMRegistry registry; + using console for *; + using ECDSA for *; + using Strings for *; + + string public domainName = "example.com"; + bytes32 public publicKeyHash = bytes32(uint256(1)); + bytes32 public publicKeyHash2 = bytes32(uint256(2)); + + address deployer; + address mainAuthorizer; + address user1; + address user2; + + UserOverrideableDKIMRegistry registryWithContract; + ExampleERC1271 mainAuthorizerContract; + ExampleOwnable exampleOwnable; + + function setUp() public { + deployer = vm.addr(1); + mainAuthorizer = vm.addr(9); + user1 = vm.addr(2); + user2 = vm.addr(3); + registry = new UserOverrideableDKIMRegistry(deployer, mainAuthorizer); + exampleOwnable = new ExampleOwnable(mainAuthorizer); + mainAuthorizerContract = new ExampleERC1271(mainAuthorizer); + registryWithContract = new UserOverrideableDKIMRegistry( + deployer, + address(mainAuthorizerContract) + ); + } + + function testSetDKIMPublicKeyHashByMainAuthorizer() public { + vm.startPrank(mainAuthorizer); + + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash, + mainAuthorizer + ); + registry.setDKIMPublicKeyHash( + domainName, + publicKeyHash, + mainAuthorizer, + new bytes(0) + ); + vm.stopPrank(); + + // Call by a Ownable contract + vm.startPrank(address(exampleOwnable)); + // setThreshold = 2 + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testSetDKIMPublicKeyHashByMainAuthorizerContract() public { + vm.startPrank(address(mainAuthorizerContract)); + + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash, + address(mainAuthorizerContract) + ); + registry.setDKIMPublicKeyHash( + domainName, + publicKeyHash, + address(mainAuthorizerContract), + new bytes(0) + ); + vm.stopPrank(); + + require( + registry.isDKIMPublicKeyHashValid( + domainName, + publicKeyHash, + address(mainAuthorizerContract) + ), + "Invalid public key hash" + ); + } + + function testSetDKIMPublicKeyHashByUser1() public { + vm.startPrank(user1); + + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash, + user1 + ); + registry.setDKIMPublicKeyHash( + domainName, + publicKeyHash, + user1, + new bytes(0) + ); + vm.stopPrank(); + + // setThreshold = 2 + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash, user1), + "Invalid public key hash" + ); + } + + function testIsDKIMPublicKeyHashValidByUser2() public { + testSetDKIMPublicKeyHashByUser1(); + + vm.startPrank(user1); + + // setThreshold = 2 + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash, user1), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testFailIsDKIMPublicKeyHashValidByUser2() public { + testSetDKIMPublicKeyHashByUser1(); + + vm.startPrank(user2); + + // setThreshold = 0 + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testExpectRevertDomainNameCannotBeZeroSetDKIMPublicKeyHashByUser1() + public + { + vm.startPrank(user1); + + vm.expectRevert("domain name cannot be zero"); + registry.setDKIMPublicKeyHash("", publicKeyHash, user1, new bytes(0)); + vm.stopPrank(); + } + + function testExpectRevertPublicKeyHashCannotBeZeroSetDKIMPublicKeyHashByUser1() + public + { + vm.startPrank(user1); + + vm.expectRevert("public key hash cannot be zero"); + registry.setDKIMPublicKeyHash( + domainName, + bytes32(uint256(0)), + user1, + new bytes(0) + ); + vm.stopPrank(); + } + + function testExpectRevertAuthorizerAddressCannotBeZeroSetDKIMPublicKeyHashByUser1() + public + { + vm.startPrank(user1); + + vm.expectRevert("authorizer address cannot be zero"); + registry.setDKIMPublicKeyHash( + domainName, + publicKeyHash, + address(0), + new bytes(0) + ); + vm.stopPrank(); + } + + function testExpectRevertPublicKeyHashIsAlreadyRevokedSetDKIMPublicKeyHashByUser1() + public + { + testRevokeDKIMPublicKeyHashByUser1(); + vm.startPrank(user1); + + vm.expectRevert("public key hash is already revoked"); + registry.setDKIMPublicKeyHash( + domainName, + publicKeyHash, + user1, + new bytes(0) + ); + vm.stopPrank(); + } + + function testSetDKIMPublicKeyHashOfMainAuthorizerByUser1() public { + vm.startPrank(user1); + + string memory signedMsg = registry.computeSignedMsg( + registry.SET_PREFIX(), + domainName, + publicKeyHash + ); + bytes32 digest = MessageHashUtils.toEthSignedMessageHash( + bytes(signedMsg) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(9, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash, + mainAuthorizer + ); + registry.setDKIMPublicKeyHash( + domainName, + publicKeyHash, + mainAuthorizer, + signature + ); + // setThreshold = 3 + require( + registry.isDKIMPublicKeyHashValid( + domainName, + publicKeyHash, + mainAuthorizer + ), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testFailSetDKIMPublicKeyHashOfMainAuthorizerByUser1() public { + vm.startPrank(user1); + + string memory signedMsg = registry.computeSignedMsg( + registry.SET_PREFIX(), + domainName, + publicKeyHash + ); + bytes32 digest = MessageHashUtils.toEthSignedMessageHash( + bytes(signedMsg) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(9, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash, + mainAuthorizer + ); + registry.setDKIMPublicKeyHash( + domainName, + publicKeyHash, + mainAuthorizer, + signature + ); + // setThreshold = 1 + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testSetDKIMPublicKeyHashOfMainAuthorizerAsContractByUser1() + public + { + vm.startPrank(user1); + + string memory signedMsg = registryWithContract.computeSignedMsg( + registryWithContract.SET_PREFIX(), + domainName, + publicKeyHash + ); + bytes32 digest = MessageHashUtils.toEthSignedMessageHash( + bytes(signedMsg) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(9, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash, + address(mainAuthorizerContract) + ); + registryWithContract.setDKIMPublicKeyHash( + domainName, + publicKeyHash, + address(mainAuthorizerContract), + signature + ); + // setThreshold = 3 + require( + registryWithContract.isDKIMPublicKeyHashValid( + domainName, + publicKeyHash, + address(mainAuthorizerContract) + ), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testFailSetDKIMPublicKeyHashOfMainAuthorizerAsContractByUser1() + public + { + vm.startPrank(user1); + + string memory signedMsg = registryWithContract.computeSignedMsg( + registryWithContract.SET_PREFIX(), + domainName, + publicKeyHash + ); + bytes32 digest = MessageHashUtils.toEthSignedMessageHash( + bytes(signedMsg) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(9, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash, + address(mainAuthorizerContract) + ); + registryWithContract.setDKIMPublicKeyHash( + domainName, + publicKeyHash, + address(mainAuthorizerContract), + signature + ); + // setThreshold = 1 + require( + registryWithContract.isDKIMPublicKeyHashValid( + domainName, + publicKeyHash + ), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testRevokeDKIMPublicKeyHashByMainAuthorizer() public { + testSetDKIMPublicKeyHashByMainAuthorizer(); + + vm.startPrank(mainAuthorizer); + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRevoked( + publicKeyHash, + mainAuthorizer + ); + registry.revokeDKIMPublicKeyHash( + domainName, + publicKeyHash, + mainAuthorizer, + new bytes(0) + ); + vm.stopPrank(); + } + + function testRevokeDKIMPublicKeyHashByUser1() public { + testSetDKIMPublicKeyHashByUser1(); + + vm.startPrank(user1); + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRevoked( + publicKeyHash, + user1 + ); + registry.revokeDKIMPublicKeyHash( + domainName, + publicKeyHash, + user1, + new bytes(0) + ); + vm.stopPrank(); + } + + function testExpectRevertDomainNameCannotBeZeroRevokeDKIMPublicKeyHashByUser1() + public + { + testSetDKIMPublicKeyHashByUser1(); + + vm.startPrank(user1); + vm.expectRevert("domain name cannot be zero"); + registry.revokeDKIMPublicKeyHash( + "", + publicKeyHash, + user1, + new bytes(0) + ); + vm.stopPrank(); + } + + function testExpectRevertPublicKeyHashCannotBeZeroRevokeDKIMPublicKeyHashByUser1() + public + { + testSetDKIMPublicKeyHashByUser1(); + + vm.startPrank(user1); + vm.expectRevert("public key hash cannot be zero"); + registry.revokeDKIMPublicKeyHash( + domainName, + bytes32(uint256(0)), + user1, + new bytes(0) + ); + vm.stopPrank(); + } + + function testExpectRevertAuthorizerAddressCannotBeZeroRevokeDKIMPublicKeyHashByUser1() + public + { + testSetDKIMPublicKeyHashByUser1(); + + vm.startPrank(user1); + vm.expectRevert("authorizer address cannot be zero"); + registry.revokeDKIMPublicKeyHash( + domainName, + publicKeyHash, + address(0), + new bytes(0) + ); + vm.stopPrank(); + } + + function testExpectRevertPublicKeyHashIsAlreadyRevokedRevokeDKIMPublicKeyHashByUser1() + public + { + testRevokeDKIMPublicKeyHashByUser1(); + + vm.startPrank(user1); + vm.expectRevert("public key hash is already revoked"); + registry.revokeDKIMPublicKeyHash( + domainName, + publicKeyHash, + user1, + new bytes(0) + ); + vm.stopPrank(); + } + + function testRevokeDKIMPublicKeyHashOfMainAuthorizerByUser1() public { + testSetDKIMPublicKeyHashByUser1(); + + vm.startPrank(user1); + + string memory signedMsg = registry.computeSignedMsg( + registry.REVOKE_PREFIX(), + domainName, + publicKeyHash + ); + bytes32 digest = MessageHashUtils.toEthSignedMessageHash( + bytes(signedMsg) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(9, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRevoked( + publicKeyHash, + mainAuthorizer + ); + registry.revokeDKIMPublicKeyHash( + domainName, + publicKeyHash, + mainAuthorizer, + signature + ); + vm.stopPrank(); + } + + function testFailIsDKIMPublicKeyHashValidByUser1AfterRevokedByMainAuthorizer() + public + { + testRevokeDKIMPublicKeyHashOfMainAuthorizerByUser1(); + + vm.startPrank(user1); + // removeThreshold = 1 + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testRevokeDKIMPublicKeyHashOfMainAuthorizerAsContractByUser1() + public + { + testSetDKIMPublicKeyHashOfMainAuthorizerAsContractByUser1(); + + vm.startPrank(user1); + + string memory signedMsg = registryWithContract.computeSignedMsg( + registryWithContract.REVOKE_PREFIX(), + domainName, + publicKeyHash + ); + bytes32 digest = MessageHashUtils.toEthSignedMessageHash( + bytes(signedMsg) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(9, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRevoked( + publicKeyHash, + address(mainAuthorizerContract) + ); + registryWithContract.revokeDKIMPublicKeyHash( + domainName, + publicKeyHash, + address(mainAuthorizerContract), + signature + ); + vm.stopPrank(); + } + + function testSetDKIMPublicKeyHashesByUser1() public { + vm.startPrank(user1); + string[] memory domainNames = new string[](2); + domainNames[0] = domainName; + domainNames[1] = domainName; + bytes32[] memory publicKeyHashes = new bytes32[](2); + publicKeyHashes[0] = publicKeyHash; + publicKeyHashes[1] = publicKeyHash2; + address[] memory authorizers = new address[](2); + authorizers[0] = user1; + authorizers[1] = user1; + bytes[] memory signatures = new bytes[](2); + signatures[0] = new bytes(0); + signatures[1] = new bytes(0); + + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHashes[0], + authorizers[0] + ); + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHashes[1], + authorizers[1] + ); + registry.setDKIMPublicKeyHashes( + domainNames, + publicKeyHashes, + authorizers, + signatures + ); + + // setThreshold = 2 + require( + registry.isDKIMPublicKeyHashValid( + domainName, + publicKeyHashes[0], + user1 + ), + "Invalid public key hash" + ); + // setThreshold = 2 + require( + registry.isDKIMPublicKeyHashValid( + domainName, + publicKeyHashes[1], + user1 + ), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testFailisDKIMPublicKeyHashValidByUser2() public { + testSetDKIMPublicKeyHashesByUser1(); + + vm.startPrank(user2); + // setThreshold = 0 + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testcomputeSignedMsg() public { + string memory signedMsg = registry.computeSignedMsg( + registry.SET_PREFIX(), + domainName, + publicKeyHash + ); + require( + Strings.equal( + signedMsg, + "SET:;domain=example.com;public_key_hash=0x01;" + ), + "Invalid signed message" + ); + console.log(signedMsg); + } + + function testReactivateDKIMPublicKeyHashByUser1() public { + testSetDKIMPublicKeyHashByUser1(); + testRevokeDKIMPublicKeyHashByMainAuthorizer(); + + require( + !registry.isDKIMPublicKeyHashValid( + domainName, + publicKeyHash, + user1 + ), + "public key hash is not revoked" + ); + + vm.startPrank(user1); + + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashReactivated( + publicKeyHash, + user1 + ); + registry.reactivateDKIMPublicKeyHash( + domainName, + publicKeyHash, + user1, + new bytes(0) + ); + vm.stopPrank(); + + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash, user1), + "Invalid public key hash" + ); + } + + function testReactivateDKIMPublicKeyHashByUser2WithUser1Signature() public { + testSetDKIMPublicKeyHashByUser1(); + testRevokeDKIMPublicKeyHashByMainAuthorizer(); + + require( + !registry.isDKIMPublicKeyHashValid( + domainName, + publicKeyHash, + user1 + ), + "public key hash is not revoked" + ); + + vm.startPrank(user2); + + string memory signedMsg = registryWithContract.computeSignedMsg( + registryWithContract.REACTIVATE_PREFIX(), + domainName, + publicKeyHash + ); + bytes32 digest = MessageHashUtils.toEthSignedMessageHash( + bytes(signedMsg) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(2, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashReactivated( + publicKeyHash, + user1 + ); + registry.reactivateDKIMPublicKeyHash( + domainName, + publicKeyHash, + user1, + signature + ); + vm.stopPrank(); + + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash, user1), + "Invalid public key hash" + ); + } + + function testReactivateDKIMPublicKeyHashByUser1WithMainAuthorizerSignature() + public + { + testSetDKIMPublicKeyHashByMainAuthorizerContract(); + testRevokeDKIMPublicKeyHashByMainAuthorizer(); + + require( + !registry.isDKIMPublicKeyHashValid( + domainName, + publicKeyHash, + address(mainAuthorizerContract) + ), + "public key hash is not revoked" + ); + + vm.startPrank(user1); + + string memory signedMsg = registryWithContract.computeSignedMsg( + registryWithContract.REACTIVATE_PREFIX(), + domainName, + publicKeyHash + ); + bytes32 digest = MessageHashUtils.toEthSignedMessageHash( + bytes(signedMsg) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(9, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashReactivated( + publicKeyHash, + address(mainAuthorizerContract) + ); + registry.reactivateDKIMPublicKeyHash( + domainName, + publicKeyHash, + address(mainAuthorizerContract), + signature + ); + vm.stopPrank(); + + require( + registry.isDKIMPublicKeyHashValid( + domainName, + publicKeyHash, + address(mainAuthorizerContract) + ), + "Invalid public key hash" + ); + } +} diff --git a/packages/contracts/test/helpers/ExampleERC1271.sol b/packages/contracts/test/helpers/ExampleERC1271.sol new file mode 100644 index 000000000..d53cb9a9a --- /dev/null +++ b/packages/contracts/test/helpers/ExampleERC1271.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +// https://eips.ethereum.org/EIPS/eip-1271 +contract ExampleERC1271 is IERC1271, Ownable { + using ECDSA for *; + + constructor(address _owner) Ownable(_owner) {} + + /** + * @notice Verifies that the signer is the owner of the signing contract. + */ + function isValidSignature( + bytes32 _hash, + bytes calldata _signature + ) external view override returns (bytes4) { + // Validate signatures + if (_hash.recover(_signature) == owner()) { + return 0x1626ba7e; + } else { + return 0xffffffff; + } + } +} diff --git a/packages/contracts/test/helpers/ExampleOwnable.sol b/packages/contracts/test/helpers/ExampleOwnable.sol new file mode 100644 index 000000000..a4210e8de --- /dev/null +++ b/packages/contracts/test/helpers/ExampleOwnable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract ExampleOwnable is Ownable { + constructor(address _owner) Ownable(_owner) {} +} diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 4537c79bd..7bd623306 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zk-email/helpers", - "version": "6.1.1", + "version": "6.1.2", "license": "MIT", "main": "dist", "scripts": {