Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
89cecfc
Add ZKEmailSigner
ernestognw Mar 25, 2025
bea0548
Remove yarn.lock
ernestognw Mar 25, 2025
b968068
Update ZKEmailSigner.sol
Amxx Mar 26, 2025
b99b9f8
Add initializer
ernestognw Mar 27, 2025
f5a2643
Merge branch 'master' into feature/zk-email
ernestognw Apr 3, 2025
9577e06
Run `forge upate lib/email-tx-builder` to bring recent changes
ernestognw Apr 3, 2025
813b226
Add ZKEmailUtils and update ZKEmailSigner
ernestognw Apr 3, 2025
ef5fe72
Add BN256 curve reference
ernestognw Apr 3, 2025
d9cba45
Add .prettierignore to ignore lib linting
ernestognw Apr 3, 2025
ccc43fa
Exclude zk-email from pragma consistency checks
ernestognw Apr 3, 2025
24ed077
Update contracts/utils/cryptography/ZKEmailSigner.sol
Amxx Apr 3, 2025
82850cb
Update contracts/utils/cryptography/ZKEmailSigner.sol
Amxx Apr 3, 2025
b6a3fdf
Update ZKEmailSigner.sol
Amxx Apr 3, 2025
3dd1fb9
Run `forge update`
ernestognw Apr 3, 2025
6bc3140
Document ZKEmailSigner and ZKEmailUtils
ernestognw Apr 3, 2025
27dcc0d
Rename to SignerZKEmail for consistency
ernestognw Apr 3, 2025
35b0bd6
Document ZKEmailSigner and ZKEmailUtils
ernestognw Apr 3, 2025
900b668
Update README to include SignerZKEmail and ZKEmailUtils documentation
ernestognw Apr 3, 2025
e8fa099
Simplify return type for ZKEmailUtils.isValidZKEmail
Amxx Apr 4, 2025
4b6426c
Setup tests
ernestognw Apr 4, 2025
581123d
Rename commandTemplate to templateId
ernestognw Apr 4, 2025
11b197f
Address https://github.com/OpenZeppelin/openzeppelin-community-contra…
Amxx Apr 4, 2025
6d5f974
Fix compilation
ernestognw Apr 4, 2025
3e9d0e3
Refactor ZKEmailUtils
ernestognw Apr 4, 2025
72a4cfc
Adjustment
ernestognw Apr 4, 2025
9742daa
Refactor ZKEmailUtils
ernestognw Apr 4, 2025
34aa505
Note _matchCase
ernestognw Apr 4, 2025
fa8b58b
Remove _commandMatch
ernestognw Apr 4, 2025
471e4de
refactor
Amxx Apr 4, 2025
aa7cdaa
Readd refactored _commandMatch
ernestognw Apr 4, 2025
190f0b3
Remove extra line
ernestognw Apr 4, 2025
202b59f
Add missing logic to remove prefix using Bytes library
ernestognw Apr 4, 2025
8f0a057
Fix compilation
ernestognw Apr 4, 2025
ed5e027
Pragma increase for mcopy support
ernestognw Apr 4, 2025
4ec7569
Apply pragma to SignerZKEmail too
ernestognw Apr 4, 2025
8a202bd
Fix pragma in mocks
ernestognw Apr 4, 2025
d3e0ddf
Add @JohnGuilding and @zkfriendly to CODEOWNERS
ernestognw Apr 4, 2025
6d4f9f6
Try alternative syntax
ernestognw Apr 4, 2025
683f3fc
Revert CODEOWNERS
ernestognw Apr 4, 2025
c3275d2
Address review suggestions
ernestognw Apr 8, 2025
1d428d4
Add ZKEmailUtils tests
ernestognw Apr 8, 2025
521fbdd
Improve coverage
ernestognw Apr 8, 2025
7842fa6
Moar coverage
ernestognw Apr 8, 2025
6bb0c00
Remove verifyEmail from signer
ernestognw Apr 8, 2025
8a5a283
Merge branch 'master' into feature/zk-email
ernestognw Apr 8, 2025
c039530
Remove unnecessary imports
ernestognw Apr 10, 2025
cd6f66f
Reorder ZKEmailUtils checks
ernestognw Apr 10, 2025
de69659
Moar tests
ernestognw Apr 10, 2025
e70f6d1
Simplify ZKEmailUtils.t.sol
ernestognw Apr 10, 2025
a6dcddd
Update syntax in _commandMatch to test codecov
ernestognw Apr 10, 2025
de157d8
Improve fuzz tests
ernestognw Apr 11, 2025
c9672d9
Improve fuzz tests
ernestognw Apr 11, 2025
9ee4ccd
Remove DKIM mock
ernestognw Apr 11, 2025
f26d26b
Add Hardhat tests
ernestognw Apr 11, 2025
799cf6c
Fix codespell
ernestognw Apr 11, 2025
f27f1a1
Add AccountZKEmail.test.js
ernestognw Apr 11, 2025
e1d6297
Fix codespell
ernestognw Apr 11, 2025
c924e97
Merge branch 'master' into feature/zk-email
ernestognw Apr 12, 2025
e7f44f2
nit
ernestognw Apr 12, 2025
9fed941
Reorder README.adoc
ernestognw Apr 12, 2025
0138787
forge update
ernestognw Apr 16, 2025
9f1171e
Merge branch 'master' into feature/zk-email
ernestognw Apr 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std.git
[submodule "lib/email-tx-builder"]
path = lib/email-tx-builder
url = https://github.com/zkemail/email-tx-builder
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib
97 changes: 97 additions & 0 deletions contracts/utils/cryptography/ZKEmailSigner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {IDKIMRegistry} from "@zk-email/contracts/DKIMRegistry.sol";
import {IVerifier, EmailProof} from "@zk-email/email-tx-builder/interfaces/IVerifier.sol";
import {EmailAuthMsg} from "@zk-email/email-tx-builder/interfaces/IEmailTypes.sol";
import {AbstractSigner} from "./AbstractSigner.sol";
import {ZKEmailUtils} from "./ZKEmailUtils.sol";

abstract contract ZKEmailSigner is AbstractSigner {
using ZKEmailUtils for EmailAuthMsg;

bytes32 private _accountSalt;
IDKIMRegistry private _registry;
IVerifier private _verifier;
uint256 private _commandTemplate;

/// @dev Proof verification error.
error InvalidEmailProof(ZKEmailUtils.EmailProofError err);

/*
* @dev Unique identifier for owner of this contract defined as a hash of an email address and an account code.
*
* An account code is a random integer in a finite scalar field of https://neuromancer.sk/std/bn/bn254[BN254] curve.
* It is a private randomness to derive a CREATE2 salt of the user’s Ethereum address
* from the email address, i.e., userEtherAddr := CREATE2(hash(userEmailAddr, accountCode)).
*/
function accountSalt() public view virtual returns (bytes32) {
return _accountSalt;
}

/// @dev An instance of the DKIM registry contract.
// solhint-disable-next-line func-name-mixedcase
function DKIMRegistry() public view virtual returns (IDKIMRegistry) {
return _registry;
}

/// @dev An instance of the Verifier contract.
function verifier() public view virtual returns (IVerifier) {
return _verifier;
}

/// @dev The templateId of the sign hash command.
function commandTemplate() public view virtual returns (uint256) {
return _commandTemplate;
}

/// @dev Set the {accountSalt}.
function _setAccountSalt(bytes32 accountSalt_) internal virtual {
_accountSalt = accountSalt_;
}

/// @dev Set the {DKIMRegistry} contract address.
function _setDKIMRegistry(IDKIMRegistry registry_) internal virtual {
_registry = registry_;
}

/// @dev Set the {verifier} contract address.
function _setVerifier(IVerifier verifier_) internal virtual {
_verifier = verifier_;
}

/// @dev Set the {commandTemplate} ID.
function _setCommandTemplate(uint256 commandTemplate_) internal virtual {
_commandTemplate = commandTemplate_;
}

/** @dev Authenticate the email sender and authorize the message in the email command.
*
* NOTE: This function only verifies the authenticity of the email and command, without
* handling replay protection. The calling contract should implement its own mechanisms
* to prevent replay attacks, similar to how nonces are used with ECDSA signatures.
*/
function verifyEmail(EmailAuthMsg memory emailAuthMsg) public view virtual {
(bool verified, ZKEmailUtils.EmailProofError err) = emailAuthMsg.isValidZKEmail(DKIMRegistry(), verifier());
if (!verified) revert InvalidEmailProof(err);
}

/// @inheritdoc AbstractSigner
function _rawSignatureValidation(
bytes32 hash,
bytes calldata signature
) internal view virtual override returns (bool) {
EmailAuthMsg memory emailAuthMsg = abi.decode(signature, (EmailAuthMsg));
if (
abi.decode(emailAuthMsg.commandParams[0], (bytes32)) == hash &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would avoid some soldiity revert and handle them more gracefully (return false)

Suggested change
abi.decode(emailAuthMsg.commandParams[0], (bytes32)) == hash &&
emailAuthMsg.commandParams.length > 0 &&
emailAuthMsg.commandParams[0].length >= 0x20 &&
bytes32(emailAuthMsg.commandParams[0]) == hash &&

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But in any case the EmailAuthMsg memory emailAuthMsg = abi.decode(signature, (EmailAuthMsg)); part can always fail. So many its not worth it.

Copy link
Member Author

@ernestognw ernestognw Apr 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to fail more gracefully. If the signer is part of an account this would go through the Entrypoint, and it may revert if a wrong commandParams is sent.

EDIT: Perhaps there's no difference since abi.decode(signature, (EmailAuthMsg)) may also revert

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entrypoint is protected against the account reverting.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but reverting will affect reputation, so we want to avoid it, don't we?

emailAuthMsg.templateId == commandTemplate() &&
emailAuthMsg.proof.accountSalt == accountSalt()
) {
(bool verified, ) = emailAuthMsg.isValidZKEmail(DKIMRegistry(), verifier());
return verified;
} else {
return false;
}
}
}
58 changes: 58 additions & 0 deletions contracts/utils/cryptography/ZKEmailUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {IDKIMRegistry} from "@zk-email/contracts/DKIMRegistry.sol";
import {IVerifier, EmailProof} from "@zk-email/email-tx-builder/interfaces/IVerifier.sol";
import {EmailAuthMsg} from "@zk-email/email-tx-builder/interfaces/IEmailTypes.sol";
import {CommandUtils} from "@zk-email/email-tx-builder/libraries/CommandUtils.sol";
import {AbstractSigner} from "./AbstractSigner.sol";

library ZKEmailUtils {
using Strings for string;

enum EmailProofError {
NoError,
DKIMPublicKeyHash, // The DKIM public key hash verification fails
MaskedCommandLength, // The masked command length exceeds the maximum
SkippedCommandPrefixSize, // The skipped command prefix size is invalid
Command, // The command format is invalid
EmailProof // The email proof verification fails
}

function isValidZKEmail(
EmailAuthMsg memory emailAuthMsg,
IDKIMRegistry dkimregistry,
IVerifier verifier
) internal view returns (bool, EmailProofError) {
if (!dkimregistry.isDKIMPublicKeyHashValid(emailAuthMsg.proof.domainName, emailAuthMsg.proof.publicKeyHash)) {
return (false, EmailProofError.DKIMPublicKeyHash);
} else if (bytes(emailAuthMsg.proof.maskedCommand).length > verifier.commandBytes()) {
return (false, EmailProofError.MaskedCommandLength);
} else if (emailAuthMsg.skippedCommandPrefix >= verifier.commandBytes()) {
return (false, EmailProofError.SkippedCommandPrefixSize);
} else {
string[] memory signHashTemplate = new string[](2);
signHashTemplate[0] = "signHash";
signHashTemplate[1] = CommandUtils.UINT_MATCHER;

// Construct an expectedCommand from template and the values of emailAuthMsg.commandParams.
string memory trimmedMaskedCommand = CommandUtils.removePrefix(
emailAuthMsg.proof.maskedCommand,
emailAuthMsg.skippedCommandPrefix
);
for (uint256 stringCase = 0; stringCase < 2; stringCase++) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looping through stringCase here is redundant since it only matters if the expected matcher is an ETH address. However, that is not the case here, as we are hardcoding the template to support only one command, "signHash", which exclusively uses a uint matcher.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we can avoid doing the loop and only need to check one index ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch @zkfriendly. So we could do something like this?

        string memory trimmedMaskedCommand = CommandUtils.removePrefix(
            emailAuthMsg.proof.maskedCommand,
            emailAuthMsg.skippedCommandPrefix
        );
        uint256 stringCase = 0;
        string memory expectedCommand = CommandUtils.computeExpectedCommand(
            emailAuthMsg.commandParams,
            signHashTemplate,
            stringCase
        );
        if (trimmedMaskedCommand != expectedCommand) {
            revert InvalidCommand();
        }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Computing expected command with stringCase 0 will produce the expected command correctly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zkfriendly can you confirm this is correct 11b197f?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you confirm if this is correct? 11b197f?

Now that we have two isValidZKEmail functions—one with the signHash template hardcoded and the other that takes the template as an argument—we need to keep the loop. If we only want to support one template (signHash), we can safely remove the loop. However, if we want to support dynamic template verification as well, we'll need to keep the loop, because it's now possible for a template to use the ETH address matcher.

Copy link
Member Author

@ernestognw ernestognw Apr 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option is to have 3 isValidZKEmailFunctions, one that uses signHash by default, another that receives a template, and a third one that receives a template and a stringCase.

enum Case {
    LOWERCASE, // Converts the command to hex lowercase.
    UPPERCASE, // Converts the command to hex uppercase.
    CHECKSUM // Computes a checksum of the command.
}

// signHash by default
function isValidZKEmail(
    EmailAuthMsg memory emailAuthMsg,
    IDKIMRegistry dkimregistry,
    IVerifier verifier
) internal view returns (EmailProofError)  {
    string[] memory signHashTemplate = new string[](2);
    signHashTemplate[0] = "signHash";
    signHashTemplate[1] = CommandUtils.UINT_MATCHER;

    return isValidZKEmail(emailAuthMsg, dkimregistry, verifier, signHashTemplate, Case.LOWERCASE);
}

// custom template - try all stringCases
function isValidZKEmail(
    EmailAuthMsg memory emailAuthMsg,
    IDKIMRegistry dkimregistry,
    IVerifier verifier,
    string[] memory template
) internal view returns (EmailProofError) {
    EmailProofError err = _validateEmailAuthParams(emailAuthMsg, dkimregistry, verifier);
    for (uint256 i = 0; i < uint8(type(Case).max) && err != EmailProofErrror.NoError; i++) {
        err = _validateCommandAndProof(emailAuthMsg, verifier, template, Case(i));
    }
    return err;
}

// custom template - try only specific stringCase
function isValidZKEmail(
    EmailAuthMsg memory emailAuthMsg,
    IDKIMRegistry dkimregistry,
    Case stringCase
) internal view returns (EmailProofError)  {
    EmailProofError err = _validateEmailAuthParams(emailAuthMsg, dkimregistry, verifier);
    return
        err == EmailProofError.NoError
            ? _validateCommandAndProof(emailAuthMsg, verifier, template, stringCase)
            : err;
}
Code for `_validateEmailAuthParams` and `_validateCommandAndProof`
function _validateEmailAuthParams(
    EmailAuthMsg memory emailAuthMsg,
    IDKIMRegistry dkimregistry,
    IVerifier verifier
) private view returns (EmailProofError) {
    if (!dkimregistry.isDKIMPublicKeyHashValid(emailAuthMsg.proof.domainName, emailAuthMsg.proof.publicKeyHash)) {
        return EmailProofError.DKIMPublicKeyHash;
    } else if (bytes(emailAuthMsg.proof.maskedCommand).length > verifier.commandBytes()) {
        return EmailProofError.MaskedCommandLength;
    } else if (emailAuthMsg.skippedCommandPrefix >= verifier.commandBytes()) {
        return EmailProofError.SkippedCommandPrefixSize;
    }
    return EmailProofError.NoError;
}

function _validateCommandAndProof(
    EmailAuthMsg memory emailAuthMsg,
    IVerifier verifier,
    string[] memory template,
    Case stringCase
) private view returns (EmailProofError) {
    string memory expectedCommand = CommandUtils.computeExpectedCommand(
        emailAuthMsg.commandParams,
        template,
        uint8(stringCase)
    );
    if (!expectedCommand.equal(emailAuthMsg.proof.maskedCommand)) {
        return EmailProofError.InvalidCommand;
    }
    return verifier.verifyEmailProof(emailAuthMsg.proof) ? EmailProofError.NoError : EmailProofError.EmailProof;
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried the previous idea in this commit. I like how it looks.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop will override any error detected at an earlier stage.

if (
CommandUtils.computeExpectedCommand(emailAuthMsg.commandParams, signHashTemplate, stringCase).equal(
trimmedMaskedCommand
)
) {
if (verifier.verifyEmailProof(emailAuthMsg.proof)) return (true, EmailProofError.NoError);
else return (false, EmailProofError.EmailProof);
}
}
return (false, EmailProofError.Command);
}
}
}
1 change: 1 addition & 0 deletions lib/email-tx-builder
Submodule email-tx-builder added at db9b11
2 changes: 1 addition & 1 deletion lib/forge-std
Submodule forge-std updated 2 files
+57 −0 src/Vm.sol
+1 −1 test/Vm.t.sol
33 changes: 31 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
"prepare-docs": "scripts/prepare-docs.sh",
"lint": "npm run lint:js && npm run lint:sol",
"lint:fix": "npm run lint:js:fix && npm run lint:sol:fix",
"lint:js": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --check && eslint .",
"lint:js:fix": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --write && eslint . --fix",
"lint:sol": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --check && solhint '{contracts,test}/**/*.sol'",
"lint:sol:fix": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --write",
Comment on lines -20 to -23
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to prettier --help, the default value already is [.gitignore, .prettierignore]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should also change that on the vanila repo

"lint:js": "prettier --log-level warn '**/*.{js,ts}' --check && eslint .",
"lint:js:fix": "prettier --log-level warn '**/*.{js,ts}' --write && eslint . --fix",
"lint:sol": "prettier --log-level warn '{contracts,test}/**/*.sol' --check && solhint '{contracts,test}/**/*.sol'",
"lint:sol:fix": "prettier --log-level warn '{contracts,test}/**/*.sol' --write",
"coverage": "scripts/checks/coverage.sh",
"test": "hardhat test",
"test:inheritance": "scripts/checks/inheritance-ordering.js artifacts/build-info/*",
Expand All @@ -43,7 +43,8 @@
"zeppelin"
],
"dependencies": {
"@axelar-network/axelar-gmp-sdk-solidity": "^6.0.6"
"@axelar-network/axelar-gmp-sdk-solidity": "^6.0.6",
"@zk-email/contracts": "^6.3.2"
},
"devDependencies": {
"@openzeppelin/contracts": "file:lib/@openzeppelin-contracts",
Expand Down
2 changes: 2 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
@openzeppelin/contracts-upgradeable/=lib/@openzeppelin-contracts-upgradeable/contracts/
@openzeppelin/community-contracts/=contracts/
@axelar-network/axelar-gmp-sdk-solidity/=node_modules/@axelar-network/axelar-gmp-sdk-solidity/
@zk-email/email-tx-builder/=lib/email-tx-builder/packages/contracts/src/
@zk-email/contracts/=node_modules/@zk-email/contracts/
2 changes: 1 addition & 1 deletion scripts/checks/pragma-consistency.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { findAll } = require('solidity-ast/utils');
const { _: artifacts } = require('yargs').argv;

// files to skip
const skipPatterns = ['contracts-exposed/**', 'contracts/mocks/WithInit.sol'];
const skipPatterns = ['contracts-exposed/**', 'contracts/mocks/WithInit.sol', '@zk-email/**'];

for (const artifact of artifacts) {
const { output: solcOutput } = require(path.resolve(__dirname, '../..', artifact));
Expand Down
Loading