Files
zk-email-verify/packages/contracts/UserOverrideableDKIMRegistry.sol
Ernesto García 850f7ce380 feat: Implement ERC-7969 DKIM Registry Standard (#278)
* Implement ERC-7969 DKIM Registry Standard

* Nits and update abi

* Nits and update abi
2025-08-22 10:56:03 +02:00

450 lines
20 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "./interfaces/IERC7969.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";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
/**
A Registry that store the hash(dkim_public_key) for each domain and each user.
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 until the predetermined delay time (`setTimestampDelay`) has passed.
Additionally, the public key can be revoked by the signature of either the user or the main authorizer alone.
*/
contract UserOverrideableDKIMRegistry is
IDKIMRegistry,
OwnableUpgradeable,
UUPSUpgradeable
{
using Strings for *;
using ECDSA for *;
/// @notice Emitted when a DKIM public key hash is successfully reactivated.
event KeyHashReactivated(
bytes32 indexed publicKeyHash,
address indexed authorizer
);
/// @notice Emitted when the main authorizer address is changed.
event MainAuthorizerChanged(address indexed newMainAuthorizer);
/// @notice Main authorizer address.
address public mainAuthorizer;
/// @notice Time delay until a DKIM public key hash set by the main authorizer is enabled
uint public setTimestampDelay;
/// @notice DKIM public key hashes that are set
mapping(bytes32 => mapping(bytes32 => mapping(address => bool)))
public dkimPublicKeyHashes;
/// @notice DKIM public key hashes that are revoked (eg: in case of private key compromise)
mapping(bytes32 => mapping(address => bool))
public revokedDKIMPublicKeyHashes;
/// @notice DKIM public key hashes 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;
/// @notice The timestamp from which the set DKIM public key hash is enabled
mapping(bytes32 => uint) public enabledTimeOfDKIMPublicKeyHash;
string public constant SET_PREFIX = "SET:";
string public constant REVOKE_PREFIX = "REVOKE:";
string public constant REACTIVATE_PREFIX = "REACTIVATE:";
constructor() {}
/// @notice Initializes the contract with a predefined signer and deploys a new DKIMRegistry.
/// @param _initialOwner The address of the initial owner of the contract.
/// @param _mainAuthorizer The address of the main authorizer.
/// @param _setTimestampDelay The time delay until a DKIM public key hash set by the main authorizer is enabled.
function initialize(
address _initialOwner,
address _mainAuthorizer,
uint _setTimestampDelay
) public initializer {
__Ownable_init(_initialOwner);
mainAuthorizer = _mainAuthorizer;
setTimestampDelay = _setTimestampDelay;
}
/// @notice Checks if a DKIM public key hash is valid for a given domain hash.
/// @param domainNameHash The hash of the domain name for which the DKIM public key hash is being checked.
/// @param publicKeyHash The hash of the DKIM public key to be checked.
/// @return bool True if the DKIM public key hash is valid, false otherwise.
/// @dev This function returns true if the owner of the given `msg.sender` approves the public key hash before `enabledTimeOfDKIMPublicKeyHash` and neither `mainAuthorizer` nor the owner of `msg.sender` revokes the public key hash. However, after `enabledTimeOfDKIMPublicKeyHash`, only one of their approvals is required. In addition, if the public key hash is reactivated by the owner of `msg.sender`, the public key hash revoked only by `mainAuthorizer` is considered valid.
function isKeyHashValid(
bytes32 domainNameHash,
bytes32 publicKeyHash
) public view returns (bool) {
address ownerOfSender = Ownable(msg.sender).owner();
return isKeyHashValid(domainNameHash, publicKeyHash, ownerOfSender);
}
/// @notice Checks if a DKIM public key hash is valid for a given domain.
/// @param domainNameHash The hash of the domain name for which the DKIM public key hash is being checked.
/// @param publicKeyHash The hash of the DKIM public key to be checked.
/// @param authorizer The address of the expected authorizer
/// @return bool True if the DKIM public key hash is valid, false otherwise.
/// @dev This function returns true if 1) at least the given `authorizer` approves the public key hash before `enabledTimeOfDKIMPublicKeyHash` and 2) neither `mainAuthorizer` nor `authorizer` revokes the public key hash. However, after `enabledTimeOfDKIMPublicKeyHash`, only one of their approvals is required. In addition, if the public key hash is reactivated by the `authorizer`, the public key hash revoked only by `mainAuthorizer` is considered valid.
/// @dev The domain name, public key hash, and authorizer address must not be zero.
/// @dev The authorizer address cannot be the mainAuthorizer.
function isKeyHashValid(
bytes32 domainNameHash,
bytes32 publicKeyHash,
address authorizer
) public view returns (bool) {
require(domainNameHash != bytes32(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(
authorizer != mainAuthorizer,
"authorizer cannot be mainAuthorizer"
);
uint256 revokeThreshold = _computeRevokeThreshold(
publicKeyHash,
authorizer
);
uint256 setThreshold = _computeSetThreshold(
domainNameHash,
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");
bytes32 domainNameHash = keccak256(bytes(domainName));
require(
dkimPublicKeyHashes[domainNameHash][publicKeyHash][authorizer] ==
false,
"public key hash is already set"
);
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[domainNameHash][publicKeyHash][authorizer] = true;
if (authorizer == mainAuthorizer) {
enabledTimeOfDKIMPublicKeyHash[publicKeyHash] =
block.timestamp +
setTimestampDelay;
}
emit KeyHashRegistered(domainNameHash, publicKeyHash);
}
/**
* @dev Sets the DKIM public key hashes in batch.
* @param domainNames An array of the domain names 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 KeyHashRevoked(keccak256(bytes(domainName)));
}
/**
* @notice Reactivates a DKIM public key hash.
* @dev This function allows an authorized user or a contract to reactivate a DKIM public key hash that was revoked by the main authorizer.
* @param domainName The domain name associated with the DKIM public key hash.
* @param publicKeyHash The hash of the DKIM public key to be reactivated.
* @param authorizer The address of the authorizer who can reactivate the DKIM public key hash.
* @param signature The signature proving the authorization to reactivate 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 be revoked by the main authorizer.
* @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 KeyHashReactivated Emitted when a DKIM public key hash is successfully reactivated.
*/
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(
authorizer != mainAuthorizer,
"mainAuthorizer cannot reactivate the public key hash"
);
require(
reactivatedDKIMPublicKeyHashes[publicKeyHash][authorizer] == false,
"public key hash is already reactivated"
);
require(
_computeRevokeThreshold(publicKeyHash, authorizer) == 1,
"revoke threshold must be one"
);
require(
_computeSetThreshold(keccak256(bytes(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 KeyHashReactivated(publicKeyHash, authorizer);
}
/**
* @notice Changes the main authorizer address.
* @param newMainAuthorizer The address of the new main authorizer.
* @custom:require Only the owner can change the main authorizer address.
* @custom:require The new main authorizer address cannot be zero.
* @custom:require The new main authorizer address cannot be the same as the current main authorizer.
* @custom:event MainAuthorizerChanged Emitted when the main authorizer address changes.
*/
function changeMainAuthorizer(address newMainAuthorizer) public onlyOwner {
require(
newMainAuthorizer != address(0),
"newMainAuthorizer address cannot be zero"
);
require(
newMainAuthorizer != mainAuthorizer,
"newMainAuthorizer address cannot be the same as the current mainAuthorizer"
);
mainAuthorizer = newMainAuthorizer;
emit MainAuthorizerChanged(newMainAuthorizer);
}
/**
* @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(
bytes32 domainNameHash,
bytes32 publicKeyHash,
address authorizer
) private view returns (uint256) {
uint256 threshold = 0;
if (
dkimPublicKeyHashes[domainNameHash][publicKeyHash][
mainAuthorizer
] == true
) {
if (
block.timestamp < enabledTimeOfDKIMPublicKeyHash[publicKeyHash]
) {
threshold += 1;
} else {
threshold += 2;
}
}
if (
dkimPublicKeyHashes[domainNameHash][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;
}
/// @notice Upgrade the implementation of the proxy.
/// @param newImplementation Address of the new implementation.
function _authorizeUpgrade(
address newImplementation
) internal override onlyOwner {}
}