Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 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
35 changes: 35 additions & 0 deletions contracts/mocks/account/AccountZKEmailMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Account} from "../../account/Account.sol";
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import {ERC7739, EIP712} from "../../utils/cryptography/ERC7739.sol";
import {ERC7821} from "../../account/extensions/ERC7821.sol";
import {SignerZKEmail} from "../../utils/cryptography/SignerZKEmail.sol";
import {IDKIMRegistry} from "@zk-email/contracts/DKIMRegistry.sol";
import {IVerifier} from "@zk-email/email-tx-builder/interfaces/IVerifier.sol";

contract AccountZKEmailMock is Account, SignerZKEmail, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
constructor(
bytes32 accountSalt_,
IDKIMRegistry registry_,
IVerifier verifier_,
uint256 templateId_
) EIP712("AccountZKEmailMock", "1") {
_setAccountSalt(accountSalt_);
_setDKIMRegistry(registry_);
_setVerifier(verifier_);
_setTemplateId(templateId_);
}

/// @inheritdoc ERC7821
function _erc7821AuthorizedExecutor(
address caller,
bytes32 mode,
bytes calldata executionData
) internal view virtual override returns (bool) {
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
}
}
40 changes: 40 additions & 0 deletions contracts/mocks/docs/account/MyAccountZKEmail.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// contracts/MyAccountZKEmail.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Account} from "../../../account/Account.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import {ERC7739} from "../../../utils/cryptography/ERC7739.sol";
import {ERC7821} from "../../../account/extensions/ERC7821.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {SignerZKEmail} from "../../../utils/cryptography/SignerZKEmail.sol";
import {IDKIMRegistry} from "@zk-email/contracts/DKIMRegistry.sol";
import {IVerifier} from "@zk-email/email-tx-builder/interfaces/IVerifier.sol";

contract MyAccountZKEmail is Account, SignerZKEmail, ERC7739, ERC7821, ERC721Holder, ERC1155Holder, Initializable {
constructor() EIP712("MyAccountZKEmail", "1") {}

function initialize(
bytes32 accountSalt_,
IDKIMRegistry registry_,
IVerifier verifier_,
uint256 templateId_
) public initializer {
_setAccountSalt(accountSalt_);
_setDKIMRegistry(registry_);
_setVerifier(verifier_);
_setTemplateId(templateId_);
}

/// @dev Allows the entry point as an authorized executor.
function _erc7821AuthorizedExecutor(
address caller,
bytes32 mode,
bytes calldata executionData
) internal view virtual override returns (bool) {
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
}
}
6 changes: 6 additions & 0 deletions contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`.
* {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739.
* {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
* {SignerZKEmail}: Implementation of an {AbstractSigner} that enables email-based authentication through zero-knowledge proofs.
* {Masks}: Library to handle `bytes32` masks.
* {ZKEmailUtils}: Library for ZKEmail signature validation utilities, enabling email-based authentication through zero-knowledge proofs.

== Cryptography

Expand All @@ -25,6 +27,10 @@ Miscellaneous contracts and libraries containing utility functions you can use t

{{SignerRSA}}

{{SignerZKEmail}}

{{ZKEmailUtils}}

== Libraries

{{Masks}}
146 changes: 146 additions & 0 deletions contracts/utils/cryptography/SignerZKEmail.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// 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";

/**
* @dev Implementation of {AbstractSigner} using https://docs.zk.email[ZKEmail] signatures.
*
* ZKEmail enables secure authentication and authorization through email messages, leveraging
* DKIM signatures from a trusted {DKIMRegistry} and zero-knowledge proofs enabled by a {verifier}
* contract that ensures email authenticity without revealing sensitive information. This contract
* implements the core functionality for validating email-based signatures in smart contracts.
*
* Developers must set the following components during contract initialization:
*
* * {accountSalt} - A unique identifier derived from the user's email address and account code.
* * {DKIMRegistry} - An instance of the DKIM registry contract for domain verification.
* * {verifier} - An instance of the Verifier contract for zero-knowledge proof validation.
* * {templateId} - The template ID of the sign hash command, defining the expected format.
*
* Example of usage:
*
* ```solidity
* contract MyAccountZKEmail is Account, SignerZKEmail, Initializable {
* constructor(bytes32 accountSalt, IDKIMRegistry registry, IVerifier verifier, uint256 templateId) {
* // Will revert if the signer is already initialized
* _setAccountSalt(accountSalt);
* _setDKIMRegistry(registry);
* _setVerifier(verifier);
* _setTemplateId(templateId);
* }
* }
* ```
*
* IMPORTANT: Avoiding to call {_setAccountSalt}, {_setDKIMRegistry}, {_setVerifier} and {_setTemplateId}
* either during construction (if used standalone) or during initialization (if used as a clone) may
* leave the signer either front-runnable or unusable.
*/
abstract contract SignerZKEmail is AbstractSigner {
using ZKEmailUtils for EmailAuthMsg;

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

/// @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)).
*
* The account salt is used for:
*
* * User Identification: Links the email address to a specific Ethereum address securely and deterministically.
* * Security: Provides a unique identifier that cannot be easily guessed or brute-forced, as it's derived
* from both the email address and a random account code.
* * Deterministic Address Generation: Enables the creation of deterministic addresses based on email addresses,
* allowing users to recover their accounts using only their email.
*/
function accountSalt() public view virtual returns (bytes32) {
return _accountSalt;
}

/// @dev An instance of the DKIM registry contract.
/// See https://docs.zk.email/architecture/dkim-verification[DKIM Verification].
// solhint-disable-next-line func-name-mixedcase
function DKIMRegistry() public view virtual returns (IDKIMRegistry) {
return _registry;
}

/// @dev An instance of the Verifier contract.
/// See https://docs.zk.email/architecture/zk-proofs#how-zk-email-uses-zero-knowledge-proofs[ZK Proofs].
function verifier() public view virtual returns (IVerifier) {
return _verifier;
}

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

/// @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 command's {templateId}.
function _setTemplateId(uint256 templateId_) internal virtual {
_templateId = templateId_;
}

/**
* @dev Authenticates the email sender and authorizes 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 {
if (emailAuthMsg.templateId != templateId() || emailAuthMsg.proof.accountSalt != accountSalt()) {
revert InvalidEmailProof(ZKEmailUtils.EmailProofError.EmailProof);
}
ZKEmailUtils.EmailProofError err = emailAuthMsg.isValidZKEmail(DKIMRegistry(), verifier());
if (err != ZKEmailUtils.EmailProofError.NoError) revert InvalidEmailProof(err);
}

/**
* @dev See {AbstractSigner-_rawSignatureValidation}. Validates a raw signature by:
*
* 1. Decoding the email authentication message from the signature
* 2. Verifying the hash matches the command parameters
* 3. Checking the template ID matches
* 4. Validating the account salt
* 5. Verifying the email proof
*/
function _rawSignatureValidation(
bytes32 hash,
bytes calldata signature
) internal view virtual override returns (bool) {
EmailAuthMsg memory emailAuthMsg = abi.decode(signature, (EmailAuthMsg));
return (abi.decode(emailAuthMsg.commandParams[0], (bytes32)) == hash &&
emailAuthMsg.templateId == templateId() &&
emailAuthMsg.proof.accountSalt == accountSalt() &&
emailAuthMsg.isValidZKEmail(DKIMRegistry(), verifier()) == ZKEmailUtils.EmailProofError.NoError);
}
}
132 changes: 132 additions & 0 deletions contracts/utils/cryptography/ZKEmailUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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";

/**
* @dev Library for https://docs.zk.email[ZKEmail] signature validation utilities.
*
* ZKEmail is a protocol that enables email-based authentication and authorization for smart contracts
* using zero-knowledge proofs. It allows users to prove ownership of an email address without revealing
* the email content or private keys.
*
* The validation process involves several key components:
*
* * A https://docs.zk.email/architecture/dkim-verification[DKIMRegistry] (DomainKeys Identified Mail) verification
* mechanism to ensure the email was sent from a valid domain. Defined by an `IDKIMRegistry` interface.
* * A https://docs.zk.email/email-tx-builder/architecture/command-templates[command template] validation
* mechanism to ensure the email command matches the expected format and parameters.
* * A https://docs.zk.email/architecture/zk-proofs#how-zk-email-uses-zero-knowledge-proofs[zero-knowledge proof] verification
* mechanism to ensure the email was actually sent and received without revealing its contents. Defined by an `IVerifier` interface.
*/
library ZKEmailUtils {
using Strings for string;

/// @dev Enumeration of possible email proof validation errors.
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
MismatchedCommand, // The command does not match the proof command
EmailProof // The email proof verification fails
}

/// @dev Enumeration of possible string cases used to compare the command with the expected proven command.
enum Case {
LOWERCASE, // Converts the command to hex lowercase.
UPPERCASE, // Converts the command to hex uppercase.
CHECKSUM, // Computes a checksum of the command.
ANY
}

/// @dev Variant of {isValidZKEmail} that validates the `["signHash", "{uint}"]` command template.
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;

// UINT_MATCHER is always lowercase
return isValidZKEmail(emailAuthMsg, dkimregistry, verifier, signHashTemplate, Case.LOWERCASE);
}

/**
* @dev Validates a ZKEmail authentication message.
*
* This function takes an email authentication message, a DKIM registry contract, and a verifier contract
* as inputs. It performs several validation checks and returns a tuple containing a boolean success flag
* and an {EmailProofError} if validation failed. Returns {EmailProofError.NoError} if all validations pass,
* or false with a specific {EmailProofError} indicating which validation check failed.
*
* NOTE: Attempts to validate the command for all possible string {Case} values.
*/
function isValidZKEmail(
EmailAuthMsg memory emailAuthMsg,
IDKIMRegistry dkimregistry,
IVerifier verifier,
string[] memory template
) internal view returns (EmailProofError) {
return isValidZKEmail(emailAuthMsg, dkimregistry, verifier, template, Case.ANY);
}

/// @dev Variant of {isValidZKEmail} that validates a template with a specific string {Case}.
function isValidZKEmail(
EmailAuthMsg memory emailAuthMsg,
IDKIMRegistry dkimregistry,
IVerifier verifier,
string[] memory template,
Case stringCase
) internal 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;
} else if (!_commandMatch(emailAuthMsg, template, stringCase)) {
return EmailProofError.MismatchedCommand;
} else {
return verifier.verifyEmailProof(emailAuthMsg.proof) ? EmailProofError.NoError : EmailProofError.EmailProof;
Copy link
Collaborator

@benceharomi benceharomi Apr 10, 2025

Choose a reason for hiding this comment

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

@JohnGuilding do we care about the order of checks here? If we don't wouldn't it be cheaper to go in this order?

// 1. SkippedCommandPrefixSize
// 2. MaskedCommandLength
// 3. MismatchedCommand
// 4. DKIMPublicKeyHash

Choose a reason for hiding this comment

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

I don't think the order matters here and changing it doesn't effect the readability imo. And yeah it would be cheaper to fail fast with the less expensive checks. If this was our code, would make this change. Not sure what the OZ view is on optimisations like this? @ernestognw

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we can make this change. Generally, we don't worry about the cost of the failure case, especially because we don't know what branch is most likely to fail. For example, if the error distribution is 80% DKIMPublicKeyHash, we should prioritize that one first.

I'm updating the code since I don't think the order is a big issue.

}
}

function _commandMatch(
EmailAuthMsg memory emailAuthMsg,
string[] memory template,
Case stringCase
) private pure returns (bool) {
return
stringCase == Case.ANY
? _matchAnyCase(emailAuthMsg, template)
: _matchCase(emailAuthMsg, template, stringCase);
}

function _matchAnyCase(EmailAuthMsg memory emailAuthMsg, string[] memory template) private pure returns (bool) {
return
_matchCase(emailAuthMsg, template, Case.LOWERCASE) ||
_matchCase(emailAuthMsg, template, Case.UPPERCASE) ||
_matchCase(emailAuthMsg, template, Case.CHECKSUM);
}

/// @dev MUST NOT be called with `Case.ANY` to avoid unexpected behavior.
function _matchCase(
EmailAuthMsg memory emailAuthMsg,
string[] memory template,
Case stringCase
) private pure returns (bool) {
return
CommandUtils.computeExpectedCommand(emailAuthMsg.commandParams, template, uint8(stringCase)).equal(
emailAuthMsg.proof.maskedCommand
);
}
}
Loading
Loading