-
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 14 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,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 && | ||||||||||
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. This would avoid some soldiity revert and handle them more gracefully (return false)
Suggested change
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. But in any case the 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 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 EDIT: Perhaps there's no difference since 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. Entrypoint is protected against the account reverting. 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. 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; | ||||||||||
} | ||||||||||
} | ||||||||||
} |
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"; | ||
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
|
||
|
||
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++) { | ||
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. Looping through 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. So we can avoid doing the loop and only need to check one index ? 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. correct. 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. 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();
} 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. Yes. Computing expected command with 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. @zkfriendly can you confirm this is correct 11b197f? 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.
Now that we have two 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. Another option is to have 3 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;
} 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. Tried the previous idea in this commit. I like how it looks. 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. 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); | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
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. According to 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. 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/*", | ||
|
@@ -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", | ||
|
Uh oh!
There was an error while loading. Please reload this page.