Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
## ZKEmail

/contracts/utils/cryptography/ZKEmailUtils.sol @zkfriendly @benceharomi
/contracts/utils/cryptography/ZKJWTUtils.sol @zkfriendly @benceharomi
/contracts/utils/cryptography/signers/SignerZKEmail.sol @zkfriendly @benceharomi
/contracts/utils/cryptography/verifiers/ERC7913ZKEmailVerifier.sol @zkfriendly @benceharomi
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@
path = lib/zk-email-verify
branch = v6.3.2
url = https://github.com/zkemail/zk-email-verify
[submodule "lib/zk-jwt"]
path = lib/zk-jwt
url = https://github.com/zkemail/zk-jwt
1 change: 1 addition & 0 deletions contracts/mocks/import.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pragma solidity ^0.8.20;

import {ECDSAOwnedDKIMRegistry} from "@zk-email/email-tx-builder/src/utils/ECDSAOwnedDKIMRegistry.sol";
import {JwtRegistry} from "@zk-email/zk-jwt/src/utils/JwtRegistry.sol";
import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
Expand Down
16 changes: 16 additions & 0 deletions contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {IVerifier, EmailProof} from "@zk-email/email-tx-builder/src/interfaces/IVerifier.sol";

contract ZKJWTVerifierMock is IVerifier {
function commandBytes() external pure returns (uint256) {
// Same as in https://github.com/zkemail/email-tx-builder/blob/1452943807a5fdc732e1113c34792c76cf7dd031/packages/contracts/src/utils/Verifier.sol#L15
return 605;
}

function verifyEmailProof(EmailProof memory proof) external pure returns (bool) {
return proof.proof.length > 0 && bytes1(proof.proof[0]) == 0x01; // boolean true
}
}
147 changes: 147 additions & 0 deletions contracts/utils/cryptography/ZKJWTUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol";
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/src/interfaces/IVerifier.sol";
import {CommandUtils} from "@zk-email/email-tx-builder/src/libraries/CommandUtils.sol";

/**
* @dev Library for https://docs.zk.email[ZK JWT] validation utilities.
*
* ZK JWT is a protocol that enables JWT-based authentication and authorization for smart contracts
* using zero-knowledge proofs. It allows users to prove ownership of a JWT token without revealing
* the token content or private keys. See https://datatracker.ietf.org/doc/html/rfc7519[RFC-7519] for
* details on the JWT verification process.
*
* The validation process involves several key components:
*
* * A https://docs.zk.email/jwt-tx-builder/architecture[JWT Registry] verification mechanism to ensure
* the JWT was issued from a valid issuer with valid public key. The registry validates the `kid|iss|azp` format
* used in JWT verification (i.e. key id, issuer, and authorized party).
* * A https://docs.zk.email/email-tx-builder/architecture/command-templates[command template] validation
* mechanism to ensure the JWT command matches the expected format and parameters.
* * A https://docs.zk.email/jwt-tx-builder/architecture[zero-knowledge proof] verification mechanism to ensure
* the JWT was actually issued and received without revealing its contents through RSA signature validation
* and selective claim disclosure.
*
* NOTE: This library adapts the email authentication infrastructure for JWT verification,
* reusing the EmailProof structure which contains JWT-specific data encoded in the domainName field
* as "kid|iss|azp" format.
*/
library ZKJWTUtils {
using CommandUtils for bytes[];
using Bytes for bytes;
using Strings for string;

/// @dev Enumeration of possible JWT proof validation errors.
/// See https://docs.zk.email/jwt-tx-builder/architecture[ZK JWT Architecture] for validation flow details.
enum JWTProofError {
NoError,
JWTPublicKeyHash, // The JWT public key hash verification fails
MaskedCommandLength, // The masked command length exceeds the maximum allowed by the circuit
MismatchedCommand, // The command does not match the proof command
JWTProof // The JWT proof verification fails
}

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

/// @dev Validates a ZK JWT proof with default "signHash" command template.
function isValidZKJWT(
EmailProof memory jwtProof,
IDKIMRegistry jwtRegistry,
IVerifier verifier,
bytes32 hash
) internal view returns (JWTProofError) {
string[] memory signHashTemplate = new string[](2);
signHashTemplate[0] = "signHash";
signHashTemplate[1] = CommandUtils.UINT_MATCHER; // UINT_MATCHER is always lowercase
return isValidZKJWT(jwtProof, jwtRegistry, verifier, signHashTemplate, _asSingletonArray(hash), Case.LOWERCASE);
}

/**
* @dev Validates a ZK JWT proof against a command template.
*
* This function takes a JWT proof, a JWT registry contract, and a verifier contract
* as inputs. It performs several validation checks and returns a {JWTProofError} indicating the result.
* Returns {JWTProofError.NoError} if all validations pass, or a specific {JWTProofError} indicating
* which validation check failed.
*
* NOTE: Attempts to validate the command for all possible string {Case} values.
*/
function isValidZKJWT(
EmailProof memory jwtProof,
IDKIMRegistry jwtRegistry,
IVerifier verifier,
string[] memory template,
bytes[] memory templateParams
) internal view returns (JWTProofError) {
return isValidZKJWT(jwtProof, jwtRegistry, verifier, template, templateParams, Case.ANY);
}

/**
* @dev Validates a ZK JWT proof against a template with a specific string {Case}.
*
* Useful for templates with Ethereum address matchers (i.e. `{ethAddr}`), which are case-sensitive
* (e.g., `["someCommand", "{address}"]`).
*/
function isValidZKJWT(
EmailProof memory jwtProof,
IDKIMRegistry jwtRegistry,
IVerifier verifier,
string[] memory template,
bytes[] memory templateParams,
Case stringCase
) internal view returns (JWTProofError) {
if (bytes(jwtProof.maskedCommand).length > verifier.commandBytes()) {
return JWTProofError.MaskedCommandLength;
} else if (!_commandMatch(jwtProof, template, templateParams, stringCase)) {
return JWTProofError.MismatchedCommand;
} else if (!jwtRegistry.isDKIMPublicKeyHashValid(jwtProof.domainName, jwtProof.publicKeyHash)) {
// Validate JWT public key and authorized party through registry
// The domainName contains "kid|iss|azp" format for JWT validation
return JWTProofError.JWTPublicKeyHash;
}

// Verify the zero-knowledge proof of JWT signature
// TODO: Is `verifyEmailProof` supposed to be non-view?
return verifier.verifyEmailProof(jwtProof) ? JWTProofError.NoError : JWTProofError.JWTProof;
}

/// @dev Compares the command in the JWT proof with the expected command template.
function _commandMatch(
EmailProof memory jwtProof,
string[] memory template,
bytes[] memory templateParams,
Case stringCase
) private pure returns (bool) {
// Convert template to expected command format
uint256 commandPrefixLength = bytes(jwtProof.maskedCommand).indexOf(bytes1(" "));
string memory command = string(bytes(jwtProof.maskedCommand).slice(commandPrefixLength + 1));

if (stringCase != Case.ANY)
return templateParams.computeExpectedCommand(template, uint8(stringCase)).equal(command);
return
templateParams.computeExpectedCommand(template, uint8(Case.LOWERCASE)).equal(command) ||
templateParams.computeExpectedCommand(template, uint8(Case.UPPERCASE)).equal(command) ||
templateParams.computeExpectedCommand(template, uint8(Case.CHECKSUM)).equal(command);
}

/// @dev Creates an array in memory with only one value for each of the elements provided.
function _asSingletonArray(bytes32 element) private pure returns (bytes[] memory array) {
assembly ("memory-safe") {
array := mload(0x40) // Load free memory pointer
mstore(array, 1) // Set array length to 1
mstore(add(array, 0x20), element) // Store the single element
mstore(0x40, add(array, 0x40)) // Update memory pointer
}
}
}
2 changes: 1 addition & 1 deletion lib/@openzeppelin-contracts
2 changes: 1 addition & 1 deletion lib/@openzeppelin-contracts-upgradeable
1 change: 1 addition & 0 deletions lib/zk-jwt
Submodule zk-jwt added at 2f20f0
Loading
Loading