From ef097a5e5f647385777b36854746beead3142a8d Mon Sep 17 00:00:00 2001 From: wshino Date: Tue, 28 May 2024 15:01:40 +0900 Subject: [PATCH] Add UserOverrideableDKIMRegistry.sol. --- packages/contracts/README.md | 6 + .../UserOverrideableDKIMRegistry.sol | 129 +++++++++ packages/contracts/foundry.toml | 2 +- packages/contracts/package.json | 1 + .../test/UserOverrideableDKIMRegistry.t.sol | 259 ++++++++++++++++++ 5 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/UserOverrideableDKIMRegistry.sol create mode 100644 packages/contracts/test/UserOverrideableDKIMRegistry.t.sol diff --git a/packages/contracts/README.md b/packages/contracts/README.md index a8e2508fc..3e9d7d6de 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -16,6 +16,12 @@ 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. Basically the same as DKIMRegistry.sol above, but with the addition of `bool individual` argument to the calling function. Each user can set a publicKeyHash only for himself/herself. + +[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..913987618 --- /dev/null +++ b/packages/contracts/UserOverrideableDKIMRegistry.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./interfaces/IDKIMRegistry.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 { + constructor(address _owner) Ownable(_owner) {} + + event DKIMPublicKeyHashRegistered( + string domainName, + bytes32 publicKeyHash, + address register + ); + event DKIMPublicKeyHashRevoked(bytes32 publicKeyHash, address register); + + // 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; + + function _stringEq( + string memory a, + string memory b + ) internal pure returns (bool) { + return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)); + } + + function isDKIMPublicKeyHashValid( + string memory domainName, + bytes32 publicKeyHash + ) public view returns (bool) { + if ( + revokedDKIMPublicKeyHashes[publicKeyHash][address(0)] || + revokedDKIMPublicKeyHashes[publicKeyHash][msg.sender] + ) { + return false; + } + + if (dkimPublicKeyHashes[domainName][publicKeyHash][address(0)]) { + return true; + } + + if (dkimPublicKeyHashes[domainName][publicKeyHash][msg.sender]) { + return true; + } + + return false; + } + + /** + * @notice Sets the DKIM public key hash for a given domain. + * @dev This function allows the owner to set a DKIM public key hash for all users, or an individual user to set it for themselves. + * @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 individual A boolean indicating whether the hash is being set for an individual user (true) or for all users (false). + * @custom:require Only the owner can set the DKIM public key hash for all users when `individual` is false. + * @custom:require The public key hash must not be revoked. + * @custom:event DKIMPublicKeyHashRegistered Emitted when a DKIM public key hash is successfully set. + */ + function setDKIMPublicKeyHash( + string memory domainName, + bytes32 publicKeyHash, + bool individual + ) public { + address register = msg.sender; + if (!individual) { + require( + msg.sender == owner(), + "only owner can set DKIM public key hash for all users" + ); + register = address(0); + } + require( + !revokedDKIMPublicKeyHashes[publicKeyHash][register], + "cannot set revoked pubkey" + ); + + dkimPublicKeyHashes[domainName][publicKeyHash][register] = true; + + emit DKIMPublicKeyHashRegistered(domainName, publicKeyHash, register); + } + + function setDKIMPublicKeyHashes( + string memory domainName, + bytes32[] memory publicKeyHashes, + bool individual + ) public { + for (uint256 i = 0; i < publicKeyHashes.length; i++) { + setDKIMPublicKeyHash(domainName, publicKeyHashes[i], individual); + } + } + + /** + * @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 publicKeyHash The hash of the DKIM public key to be revoked. + * @param individual A boolean indicating whether the hash is being revoked for an individual user (true) or for all users (false). + * @custom:require Only the owner can revoke the DKIM public key hash for all users when `individual` is false. + * @custom:event DKIMPublicKeyHashRevoked Emitted when a DKIM public key hash is successfully revoked. + */ + function revokeDKIMPublicKeyHash( + bytes32 publicKeyHash, + bool individual + ) public { + address register = msg.sender; + if (!individual) { + require( + msg.sender == owner(), + "only owner can revoke DKIM public key hash for all users" + ); + register = address(0); + } + revokedDKIMPublicKeyHashes[publicKeyHash][register] = true; + + emit DKIMPublicKeyHashRevoked(publicKeyHash, register); + } +} 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 c88c8ac24..2fdbf0d88 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -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..857945789 --- /dev/null +++ b/packages/contracts/test/UserOverrideableDKIMRegistry.t.sol @@ -0,0 +1,259 @@ +// 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"; + +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 user1; + address user2; + + function setUp() public { + deployer = vm.addr(1); + user1 = vm.addr(2); + user2 = vm.addr(3); + registry = new UserOverrideableDKIMRegistry(deployer); + } + + function testSetDKIMPublicKeyHashForAll() public { + vm.startPrank(deployer); + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash, + address(0) + ); + registry.setDKIMPublicKeyHash(domainName, publicKeyHash, false); + vm.stopPrank(); + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash), + "Invalid public key hash" + ); + } + + function testFuzzSetDKIMPublicKeyHashForAll( + string memory randomDomainName, + bytes32 randomPublicKeyHash + ) public { + vm.startPrank(deployer); + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + randomDomainName, + randomPublicKeyHash, + address(0) + ); + registry.setDKIMPublicKeyHash( + randomDomainName, + randomPublicKeyHash, + false + ); + vm.stopPrank(); + require( + registry.isDKIMPublicKeyHashValid( + randomDomainName, + randomPublicKeyHash + ), + "Invalid public key hash" + ); + } + + function testFailDKIMPublicKeyHashForAllByUser1() public { + vm.startPrank(user1); + registry.setDKIMPublicKeyHash(domainName, publicKeyHash, false); + vm.stopPrank(); + } + + function testSetDKIMPublicKeyHashForPersonalByUser1() public { + vm.startPrank(user1); + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash, + user1 + ); + registry.setDKIMPublicKeyHash(domainName, publicKeyHash, true); + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testFuzzSetDKIMPublicKeyHashForPersonalByUser1( + string memory randomDomainName, + bytes32 randomPublicKeyHash + ) public { + vm.startPrank(user1); + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + randomDomainName, + randomPublicKeyHash, + user1 + ); + registry.setDKIMPublicKeyHash( + randomDomainName, + randomPublicKeyHash, + true + ); + require( + registry.isDKIMPublicKeyHashValid( + randomDomainName, + randomPublicKeyHash + ), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testFailSetDKIMPublicKeyHashForPersonalByUser1ReadByUser2() + public + { + vm.startPrank(user1); + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash, + user1 + ); + registry.setDKIMPublicKeyHash(domainName, publicKeyHash, true); + vm.stopPrank(); + vm.startPrank(user2); + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testRevokeDKIMPublicKeyHashForAll() public { + testSetDKIMPublicKeyHashForAll(); + vm.startPrank(deployer); + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRevoked( + publicKeyHash, + address(0) + ); + registry.revokeDKIMPublicKeyHash(publicKeyHash, false); + vm.stopPrank(); + require( + !registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash), + "Revoke failed" + ); + } + + function testFailRevokeDKIMPublicKeyHashForAllByUser1() public { + testSetDKIMPublicKeyHashForAll(); + vm.startPrank(user1); + registry.revokeDKIMPublicKeyHash(publicKeyHash, false); + vm.stopPrank(); + } + + function testSetDKIMPublicKeyHashesForAll() public { + vm.startPrank(deployer); + bytes32[] memory publicKeyHashes = new bytes32[](2); + publicKeyHashes[0] = publicKeyHash; + publicKeyHashes[1] = publicKeyHash2; + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash, + address(0) + ); + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash2, + address(0) + ); + registry.setDKIMPublicKeyHashes(domainName, publicKeyHashes, false); + vm.stopPrank(); + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash), + "Invalid public key hash" + ); + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash2), + "Invalid public key hash" + ); + } + + function testFailDKIMPublicKeyHashesForAllByUser1() public { + vm.startPrank(user1); + bytes32[] memory publicKeyHashes = new bytes32[](2); + publicKeyHashes[0] = publicKeyHash; + publicKeyHashes[1] = publicKeyHash2; + registry.setDKIMPublicKeyHashes(domainName, publicKeyHashes, false); + vm.stopPrank(); + } + + function testSetDKIMPublicKeyHashesForPersonalByUser1() public { + vm.startPrank(user1); + bytes32[] memory publicKeyHashes = new bytes32[](2); + publicKeyHashes[0] = publicKeyHash; + publicKeyHashes[1] = publicKeyHash2; + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash, + user1 + ); + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash2, + user1 + ); + registry.setDKIMPublicKeyHashes(domainName, publicKeyHashes, true); + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash), + "Invalid public key hash" + ); + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash2), + "Invalid public key hash" + ); + vm.stopPrank(); + } + + function testFailSetDKIMPublicKeyHashesForPersonalByUser1ReadByUser2() + public + { + vm.startPrank(user1); + bytes32[] memory publicKeyHashes = new bytes32[](2); + publicKeyHashes[0] = publicKeyHash; + publicKeyHashes[1] = publicKeyHash2; + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash, + user1 + ); + vm.expectEmit(); + emit UserOverrideableDKIMRegistry.DKIMPublicKeyHashRegistered( + domainName, + publicKeyHash2, + user1 + ); + registry.setDKIMPublicKeyHashes(domainName, publicKeyHashes, true); + vm.stopPrank(); + vm.startPrank(user2); + require( + registry.isDKIMPublicKeyHashValid(domainName, publicKeyHash), + "Invalid public key hash" + ); + vm.stopPrank(); + } +}