-
Notifications
You must be signed in to change notification settings - Fork 24
Add SignerZKEmail and ZKEmailUtils #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 27 commits
89cecfc
bea0548
b968068
b99b9f8
f5a2643
9577e06
813b226
ef5fe72
d9cba45
ccc43fa
24ed077
82850cb
b6a3fdf
3dd1fb9
6bc3140
27dcc0d
35b0bd6
900b668
e8fa099
4b6426c
581123d
11b197f
6d5f974
3e9d0e3
72a4cfc
9742daa
34aa505
fa8b58b
471e4de
aa7cdaa
190f0b3
202b59f
8f0a057
ed5e027
4ec7569
8a202bd
d3e0ddf
6d4f9f6
683f3fc
c3275d2
1d428d4
521fbdd
7842fa6
6bb0c00
8a5a283
c039530
cd6f66f
de69659
e70f6d1
a6dcddd
de157d8
c9672d9
9ee4ccd
f26d26b
799cf6c
f27f1a1
e1d6297
c924e97
e7f44f2
9fed941
0138787
9f1171e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
lib |
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); | ||
} | ||
} |
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); | ||
} | ||
} |
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"; | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* 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. | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* * 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); | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
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( | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
} | ||
} |
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"; | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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"; | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* @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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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( | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
EmailAuthMsg memory emailAuthMsg, | ||
string[] memory template, | ||
Case stringCase | ||
) private pure returns (bool) { | ||
return | ||
CommandUtils.computeExpectedCommand(emailAuthMsg.commandParams, template, uint8(stringCase)).equal( | ||
emailAuthMsg.proof.maskedCommand | ||
); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.