Skip to content

Commit

Permalink
feat: simplified email signer authenticator
Browse files Browse the repository at this point in the history
  • Loading branch information
zkfriendly committed Jan 14, 2025
1 parent 352acc8 commit a2e4cd9
Show file tree
Hide file tree
Showing 4 changed files with 586 additions and 0 deletions.
189 changes: 189 additions & 0 deletions packages/contracts/src/EmailAuthSigner.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
198 changes: 198 additions & 0 deletions packages/contracts/test/EmailAuthSigner.t.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading

0 comments on commit a2e4cd9

Please sign in to comment.