From 9f34f2fe82c93706b8f0dee7179e3d331ea30a2f Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 23 Jun 2025 16:19:23 +0200 Subject: [PATCH 01/15] Update CHANGELOG --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5d0257c..a4ee7df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## 23-06-2025 + +- `AxelarGatewayBase`, `AxelarGatewayDestination`, `AxelarGatewaySource`, `ERC7786Receiver` and `ERC7786Aggregator`: Changed support from CAIP addresses to ERC-7930 addresses. +- `ERC7786Aggregator`: Renamed to `ERC7786OpenBridge`. + +## 20-06-2025 + +- `EnumerableMap`: Add `keys(uint256,uint256)` that returns a subset (slice) of the keys in the map. +- `EnumerableSet`: Add `values(uint256,uint256)` that returns a subset (slice) of the values in the set. + +## 03-06-2025 + +- Moved `ERC7739Utils`, `ERC7913Utils`, `ZKEmailUtils`, abstract signers and ERC-7913 verifiers to `/contracts/utils/cryptography` + ## 15-05-2025 - `ERC7579Multisig`: Add an abstract multisig module for ERC-7579 accounts using ERC-7913 signer keys. From 95ec54ac5dc9a3de7adab44567f06112baaeb414 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 24 Jun 2025 12:30:40 +0200 Subject: [PATCH 02/15] Add ZKJWTUtils --- .github/CODEOWNERS | 1 + .gitmodules | 3 + contracts/mocks/import.sol | 1 + .../utils/cryptography/ZKJWTVerifierMock.sol | 16 ++ contracts/utils/cryptography/ZKJWTUtils.sol | 141 +++++++++ lib/zk-jwt | 1 + remappings.txt | 1 + test/helpers/enums.js | 8 + test/utils/cryptography/ZKEmailUtils.t.sol | 1 - test/utils/cryptography/ZKJWTUtils.test.js | 270 ++++++++++++++++++ 10 files changed, 442 insertions(+), 1 deletion(-) create mode 100644 contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol create mode 100644 contracts/utils/cryptography/ZKJWTUtils.sol create mode 160000 lib/zk-jwt create mode 100644 test/utils/cryptography/ZKJWTUtils.test.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 817f3da2..875b812f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/.gitmodules b/.gitmodules index fc3a658b..ad957057 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/contracts/mocks/import.sol b/contracts/mocks/import.sol index 99fd1978..aa93d2b8 100644 --- a/contracts/mocks/import.sol +++ b/contracts/mocks/import.sol @@ -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"; diff --git a/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol b/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol new file mode 100644 index 00000000..d475fbed --- /dev/null +++ b/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IVerifier, EmailProof} from "@zk-email/zk-jwt/src/interfaces/IVerifier.sol"; + +contract ZKJWTVerifierMock is IVerifier { + function getCommandBytes() 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 + } +} diff --git a/contracts/utils/cryptography/ZKJWTUtils.sol b/contracts/utils/cryptography/ZKJWTUtils.sol new file mode 100644 index 00000000..12cd8ab5 --- /dev/null +++ b/contracts/utils/cryptography/ZKJWTUtils.sol @@ -0,0 +1,141 @@ +// 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/zk-jwt/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. + enum JWTProofError { + NoError, + JWTPublicKeyHash, // The JWT 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 + 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 + ) internal 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, 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 + ) internal returns (JWTProofError) { + return isValidZKJWT(jwtProof, jwtRegistry, verifier, template, 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, + Case stringCase + ) internal returns (JWTProofError) { + if (bytes(jwtProof.maskedCommand).length > verifier.getCommandBytes()) { + return JWTProofError.MaskedCommandLength; + } else if (!_commandMatch(jwtProof, template, 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, + Case stringCase + ) private pure returns (bool) { + // For JWT proofs, we extract command parameters from the maskedCommand + // Since JWTs don't use the same command structure as emails, we adapt the validation + string memory command = jwtProof.maskedCommand; + + // Convert template to expected command format + bytes[] memory commandParams = new bytes[](template.length); + for (uint256 i = 0; i < template.length; i++) { + commandParams[i] = bytes(template[i]); + } + + if (stringCase != Case.ANY) { + return commandParams.computeExpectedCommand(template, uint8(stringCase)).equal(command); + } + + return + commandParams.computeExpectedCommand(template, uint8(Case.LOWERCASE)).equal(command) || + commandParams.computeExpectedCommand(template, uint8(Case.UPPERCASE)).equal(command) || + commandParams.computeExpectedCommand(template, uint8(Case.CHECKSUM)).equal(command); + } +} diff --git a/lib/zk-jwt b/lib/zk-jwt new file mode 160000 index 00000000..16454e73 --- /dev/null +++ b/lib/zk-jwt @@ -0,0 +1 @@ +Subproject commit 16454e7393c8c10373361bf9c31d62c74b65f3ae diff --git a/remappings.txt b/remappings.txt index 18badd3f..2c0e4337 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,4 +3,5 @@ @openzeppelin/community-contracts/=contracts/ @axelar-network/axelar-gmp-sdk-solidity/=lib/axelar-gmp-sdk-solidity/ @zk-email/email-tx-builder/=lib/email-tx-builder/packages/contracts/ +@zk-email/zk-jwt/=lib/zk-jwt/packages/contracts/ @zk-email/contracts/=lib/zk-email-verify/packages/contracts/ diff --git a/test/helpers/enums.js b/test/helpers/enums.js index 808b79e9..db7a251d 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -10,6 +10,14 @@ module.exports = { 'MismatchedCommand', 'EmailProof', ), + JWTProofError: enums.Enum( + 'NoError', + 'JWTPublicKeyHash', + 'MaskedCommandLength', + 'SkippedCommandPrefixSize', + 'MismatchedCommand', + 'JWTProof', + ), Case: enums.EnumTyped('CHECKSUM', 'LOWERCASE', 'UPPERCASE', 'ANY'), OperationState: enums.Enum('Unknown', 'Scheduled', 'Ready', 'Expired', 'Executed', 'Canceled'), }; diff --git a/test/utils/cryptography/ZKEmailUtils.t.sol b/test/utils/cryptography/ZKEmailUtils.t.sol index 070edcd5..41ea7168 100644 --- a/test/utils/cryptography/ZKEmailUtils.t.sol +++ b/test/utils/cryptography/ZKEmailUtils.t.sol @@ -346,7 +346,6 @@ contract ZKEmailUtilsTest is Test { uint256[2][2] memory pB, uint256[2] memory pC ) public view { - // TODO: Remove these when the Verifier wrapper does not revert. pA[0] = bound(pA[0], 1, Q - 1); pA[1] = bound(pA[1], 1, Q - 1); pB[0][0] = bound(pB[0][0], 1, Q - 1); diff --git a/test/utils/cryptography/ZKJWTUtils.test.js b/test/utils/cryptography/ZKJWTUtils.test.js new file mode 100644 index 00000000..d9c238c7 --- /dev/null +++ b/test/utils/cryptography/ZKJWTUtils.test.js @@ -0,0 +1,270 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { JWTProofError, Case } = require('../../helpers/enums'); + +const accountSalt = '0x046582bce36cdd0a8953b9d40b8f20d58302bacf3bcecffeb6741c98a52725e2'; // keccak256("test@example.com") + +// JWT-specific test data - using kid|iss|azp format (from actual zk-jwt tests) +const kid = '12345'; +const iss = 'https://example.com'; +const azp = 'client-id-12345'; +const domainName = `${kid}|${iss}|${azp}`; // JWT format: kid|iss|azp +const publicKeyHash = '0x0ea9c777dc7110e5a9e89b13f0cfc540e3845ba120b2b6dc24024d61488d4788'; +const emailNullifier = '0x00a83fce3d4b1c9ef0f600644c1ecc6c8115b57b1596e0e3295e2c5105fbfd8a'; + +const SIGN_HASH_COMMAND = 'signHash'; +const UINT_MATCHER = '{uint}'; +const ETH_ADDR_MATCHER = '{ethAddr}'; + +async function fixture() { + const [admin, other, ...accounts] = await ethers.getSigners(); + + // JWT Registry (following actual zk-jwt pattern from JwtRegistryBase.t.sol) + const jwtRegistry = await ethers.deployContract('JwtRegistry', [admin.address]); + + // Set up the JWT public key following the actual test pattern + await jwtRegistry + .connect(admin) + .setJwtPublicKey(domainName, publicKeyHash) + .then(() => jwtRegistry.isDKIMPublicKeyHashValid(domainName, publicKeyHash)) + .then(() => jwtRegistry.isJwtPublicKeyValid(domainName, publicKeyHash)); + + // JWT Verifier + const verifier = await ethers.deployContract('ZKJWTVerifierMock'); + + // ZKJWTUtils mock contract + const mock = await ethers.deployContract('$ZKJWTUtils'); + + return { admin, other, accounts, jwtRegistry, verifier, mock }; +} + +function buildJWTProof(command) { + return { + domainName, // kid|iss|azp format (from actual zk-jwt tests) + publicKeyHash, + timestamp: Math.floor(Date.now() / 1000), + maskedCommand: command, + emailNullifier, + accountSalt, + isCodeExist: true, + proof: '0x01', // Mocked in ZKEmailVerifierMock + }; +} + +describe('ZKJWTUtils', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('JWT Proof Validation', function () { + it('should validate ZK JWT with default signHash template', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + const jwtProof = buildJWTProof(command); + + await expect( + this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), + ).to.eventually.equal(JWTProofError.NoError); + }); + + it('should validate ZK JWT with custom template', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const commandPrefix = 'jwtCommand'; + const command = commandPrefix + ' ' + ethers.toBigInt(hash).toString(); + const jwtProof = buildJWTProof(command); + const template = [commandPrefix, UINT_MATCHER]; + + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[])'; + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template), + ).to.eventually.equal(JWTProofError.NoError); + }); + + it('should validate JWT command with address match in different cases', async function () { + const commandPrefix = 'authorize'; + const template = [commandPrefix, ETH_ADDR_MATCHER]; + + const testCases = [ + { + caseType: Case.LOWERCASE, + address: this.other.address.toLowerCase(), + }, + { + caseType: Case.UPPERCASE, + address: this.other.address.toUpperCase().replace('0X', '0x'), + }, + { + caseType: Case.CHECKSUM, + address: ethers.getAddress(this.other.address), + }, + ]; + + for (const { caseType, address } of testCases) { + const command = commandPrefix + ' ' + address; + const jwtProof = buildJWTProof(command); + + await expect( + this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target, template, caseType), + ).to.eventually.equal(JWTProofError.NoError); + } + }); + + it('should validate JWT command with address match using ANY case', async function () { + const commandPrefix = 'grant'; + const template = [commandPrefix, ETH_ADDR_MATCHER]; + + // Test with different cases that should all work with ANY case + const addresses = [ + this.other.address.toLowerCase(), + this.other.address.toUpperCase().replace('0X', '0x'), + ethers.getAddress(this.other.address), + ]; + + for (const address of addresses) { + const command = commandPrefix + ' ' + address; + const jwtProof = buildJWTProof(command); + + await expect( + this.mock.$isValidZKJWT( + jwtProof, + this.jwtRegistry.target, + this.verifier.target, + template, + ethers.Typed.uint8(Case.ANY), + ), + ).to.eventually.equal(JWTProofError.NoError); + } + }); + }); + + describe('JWT Error Handling', function () { + it('should detect invalid JWT public key hash', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + const jwtProof = buildJWTProof(command); + jwtProof.publicKeyHash = ethers.hexlify(ethers.randomBytes(32)); // Invalid public key hash + + await expect( + this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), + ).to.eventually.equal(JWTProofError.JWTPublicKeyHash); + }); + + it('should detect unregistered domain format', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + const jwtProof = buildJWTProof(command); + // Use a domain that hasn't been registered + jwtProof.domainName = 'unregistered-kid|https://unregistered.com|unregistered-client'; + + await expect( + this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), + ).to.eventually.equal(JWTProofError.JWTPublicKeyHash); + }); + + it('should detect invalid masked command length', async function () { + // Create a command that's too long (exceeds circuit limits - 605 bytes max) + const longCommand = 'a'.repeat(606); + const jwtProof = buildJWTProof(longCommand); + + await expect( + this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), + ).to.eventually.equal(JWTProofError.MaskedCommandLength); + }); + + it('should detect mismatched command template', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const command = 'invalidJWTCommand ' + ethers.toBigInt(hash).toString(); + const jwtProof = buildJWTProof(command); + + await expect( + this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), + ).to.eventually.equal(JWTProofError.MismatchedCommand); + }); + + it('should detect invalid JWT zero-knowledge proof', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + const jwtProof = buildJWTProof(command); + jwtProof.proof = '0x00'; // Invalid proof that will fail verification + + await expect( + this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), + ).to.eventually.equal(JWTProofError.JWTProof); + }); + }); + + describe('JWT-Specific Domain Format', function () { + it('should validate proper kid|iss|azp domain format', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + + // Test various valid JWT domain formats + const validDomains = [ + '12345|https://example.com|client-id-12345', // From actual tests + 'test-kid|https://accounts.google.com|1234567890.apps.googleusercontent.com', // Google + 'auth0-key|https://your-domain.auth0.com/|your-auth0-client-id', // Auth0 + ]; + + for (const domain of validDomains) { + const jwtProof = buildJWTProof(command); + jwtProof.domainName = domain; + + await expect( + this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), + ).to.eventually.equal(JWTProofError.NoError); + } + }); + + it('should handle JWT with real Google OAuth format', async function () { + // Based on actual Google JWT structure + const googleKid = 'google-key-id-123'; + const googleIss = 'https://accounts.google.com'; + const googleAzp = '1234567890.apps.googleusercontent.com'; + const googleDomain = `${googleKid}|${googleIss}|${googleAzp}`; + + const nonce = ethers.hexlify(ethers.randomBytes(16)); + const command = `grant ${nonce}`; + const jwtProof = buildJWTProof(command); + jwtProof.domainName = googleDomain; + + const template = ['grant', UINT_MATCHER]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[])'; + + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template), + ).to.eventually.equal(JWTProofError.NoError); + }); + }); + + describe('JWT Template Matching', function () { + it('should validate complex JWT commands with multiple parameters', async function () { + const amount = ethers.parseEther('1.5'); + const recipient = this.other.address; + const command = `transfer ${amount.toString()} ${recipient}`; + const jwtProof = buildJWTProof(command); + const template = ['transfer', UINT_MATCHER, ETH_ADDR_MATCHER]; + + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[])'; + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template), + ).to.eventually.equal(JWTProofError.NoError); + }); + + it('should validate JWT maskedCommand from real proof structure', async function () { + // Based on actual JWT verifier test: "Send 0.12 ETH to 0x1234" + const command = 'Send 0.12 ETH to 0x1234'; + const jwtProof = buildJWTProof(command); + const template = ['Send', UINT_MATCHER, 'ETH', 'to', ETH_ADDR_MATCHER]; + + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[])'; + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template), + ).to.eventually.equal(JWTProofError.NoError); + }); + }); +}); From 49e42ca17f5256c90d3d732cdedfb9ef0021d1e0 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 22 Jul 2025 13:31:26 -0600 Subject: [PATCH 03/15] up --- contracts/utils/cryptography/ZKJWTUtils.sol | 50 +++++---- lib/@openzeppelin-contracts | 2 +- lib/@openzeppelin-contracts-upgradeable | 2 +- lib/zk-jwt | 2 +- test/helpers/enums.js | 9 +- test/utils/cryptography/ZKJWTUtils.test.js | 109 +++++++++++++++----- 6 files changed, 118 insertions(+), 56 deletions(-) diff --git a/contracts/utils/cryptography/ZKJWTUtils.sol b/contracts/utils/cryptography/ZKJWTUtils.sol index 12cd8ab5..dd414daa 100644 --- a/contracts/utils/cryptography/ZKJWTUtils.sol +++ b/contracts/utils/cryptography/ZKJWTUtils.sol @@ -37,11 +37,11 @@ library ZKJWTUtils { 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 - SkippedCommandPrefixSize, // The skipped command prefix size is invalid + 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 } @@ -58,12 +58,13 @@ library ZKJWTUtils { function isValidZKJWT( EmailProof memory jwtProof, IDKIMRegistry jwtRegistry, - IVerifier verifier + IVerifier verifier, + bytes32 hash ) internal 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, Case.LOWERCASE); + return isValidZKJWT(jwtProof, jwtRegistry, verifier, signHashTemplate, _asSingletonArray(hash), Case.LOWERCASE); } /** @@ -80,9 +81,10 @@ library ZKJWTUtils { EmailProof memory jwtProof, IDKIMRegistry jwtRegistry, IVerifier verifier, - string[] memory template + string[] memory template, + bytes[] memory templateParams ) internal returns (JWTProofError) { - return isValidZKJWT(jwtProof, jwtRegistry, verifier, template, Case.ANY); + return isValidZKJWT(jwtProof, jwtRegistry, verifier, template, templateParams, Case.ANY); } /** @@ -96,11 +98,12 @@ library ZKJWTUtils { IDKIMRegistry jwtRegistry, IVerifier verifier, string[] memory template, + bytes[] memory templateParams, Case stringCase ) internal returns (JWTProofError) { if (bytes(jwtProof.maskedCommand).length > verifier.getCommandBytes()) { return JWTProofError.MaskedCommandLength; - } else if (!_commandMatch(jwtProof, template, stringCase)) { + } 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 @@ -117,25 +120,28 @@ library ZKJWTUtils { function _commandMatch( EmailProof memory jwtProof, string[] memory template, + bytes[] memory templateParams, Case stringCase ) private pure returns (bool) { - // For JWT proofs, we extract command parameters from the maskedCommand - // Since JWTs don't use the same command structure as emails, we adapt the validation - string memory command = jwtProof.maskedCommand; - // Convert template to expected command format - bytes[] memory commandParams = new bytes[](template.length); - for (uint256 i = 0; i < template.length; i++) { - commandParams[i] = bytes(template[i]); - } - - if (stringCase != Case.ANY) { - return commandParams.computeExpectedCommand(template, uint8(stringCase)).equal(command); - } + 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 - commandParams.computeExpectedCommand(template, uint8(Case.LOWERCASE)).equal(command) || - commandParams.computeExpectedCommand(template, uint8(Case.UPPERCASE)).equal(command) || - commandParams.computeExpectedCommand(template, uint8(Case.CHECKSUM)).equal(command); + 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 + } } } diff --git a/lib/@openzeppelin-contracts b/lib/@openzeppelin-contracts index 6079eb3f..32e7a6ff 160000 --- a/lib/@openzeppelin-contracts +++ b/lib/@openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 6079eb3f01d5a37ae23e7e72d6909852566bc2e3 +Subproject commit 32e7a6ffbc5af9ab0e6dfdbc58508511d0f0b4a2 diff --git a/lib/@openzeppelin-contracts-upgradeable b/lib/@openzeppelin-contracts-upgradeable index da12828d..7bb7e907 160000 --- a/lib/@openzeppelin-contracts-upgradeable +++ b/lib/@openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit da12828d5d0fbd8a556e118756eedef9fb042ffc +Subproject commit 7bb7e9077dd93d116657bb181e32c84165b8dbba diff --git a/lib/zk-jwt b/lib/zk-jwt index 16454e73..2f20f059 160000 --- a/lib/zk-jwt +++ b/lib/zk-jwt @@ -1 +1 @@ -Subproject commit 16454e7393c8c10373361bf9c31d62c74b65f3ae +Subproject commit 2f20f059cf1ad3a4f1ea0216f907923e4c43a8db diff --git a/test/helpers/enums.js b/test/helpers/enums.js index db7a251d..24d65fdb 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -10,14 +10,7 @@ module.exports = { 'MismatchedCommand', 'EmailProof', ), - JWTProofError: enums.Enum( - 'NoError', - 'JWTPublicKeyHash', - 'MaskedCommandLength', - 'SkippedCommandPrefixSize', - 'MismatchedCommand', - 'JWTProof', - ), + JWTProofError: enums.Enum('NoError', 'JWTPublicKeyHash', 'MaskedCommandLength', 'MismatchedCommand', 'JWTProof'), Case: enums.EnumTyped('CHECKSUM', 'LOWERCASE', 'UPPERCASE', 'ANY'), OperationState: enums.Enum('Unknown', 'Scheduled', 'Ready', 'Expired', 'Executed', 'Canceled'), }; diff --git a/test/utils/cryptography/ZKJWTUtils.test.js b/test/utils/cryptography/ZKJWTUtils.test.js index d9c238c7..ac4cdacd 100644 --- a/test/utils/cryptography/ZKJWTUtils.test.js +++ b/test/utils/cryptography/ZKJWTUtils.test.js @@ -48,7 +48,7 @@ function buildJWTProof(command) { emailNullifier, accountSalt, isCodeExist: true, - proof: '0x01', // Mocked in ZKEmailVerifierMock + proof: '0x01', // Mocked in ZKJWTVerifierMock }; } @@ -58,14 +58,28 @@ describe('ZKJWTUtils', function () { }); describe('JWT Proof Validation', function () { - it('should validate ZK JWT with default signHash template', async function () { + it.only('should validate ZK JWT with default signHash template', async function () { const hash = ethers.hexlify(ethers.randomBytes(32)); const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); const jwtProof = buildJWTProof(command); - await expect( - this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), - ).to.eventually.equal(JWTProofError.NoError); + // Use the working overload with templateParams instead of the default function + const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + const tx = await this.mock[fnSig]( + jwtProof, + this.jwtRegistry.target, + this.verifier.target, + template, + templateParams, + ); + console.log(tx.provider); + // await expect(this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams)) + // .to.emit(this.mock, 'return$isValidZKJWT') + // .withArgs(JWTProofError.NoError); }); it('should validate ZK JWT with custom template', async function () { @@ -74,11 +88,12 @@ describe('ZKJWTUtils', function () { const command = commandPrefix + ' ' + ethers.toBigInt(hash).toString(); const jwtProof = buildJWTProof(command); const template = [commandPrefix, UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[])'; + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template), + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), ).to.eventually.equal(JWTProofError.NoError); }); @@ -104,9 +119,12 @@ describe('ZKJWTUtils', function () { for (const { caseType, address } of testCases) { const command = commandPrefix + ' ' + address; const jwtProof = buildJWTProof(command); + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['address'], [address])]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[],uint8)'; await expect( - this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target, template, caseType), + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams, caseType), ).to.eventually.equal(JWTProofError.NoError); } }); @@ -125,13 +143,17 @@ describe('ZKJWTUtils', function () { for (const address of addresses) { const command = commandPrefix + ' ' + address; const jwtProof = buildJWTProof(command); + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['address'], [address])]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[],uint8)'; await expect( - this.mock.$isValidZKJWT( + this.mock[fnSig]( jwtProof, this.jwtRegistry.target, this.verifier.target, template, + templateParams, ethers.Typed.uint8(Case.ANY), ), ).to.eventually.equal(JWTProofError.NoError); @@ -146,8 +168,13 @@ describe('ZKJWTUtils', function () { const jwtProof = buildJWTProof(command); jwtProof.publicKeyHash = ethers.hexlify(ethers.randomBytes(32)); // Invalid public key hash + const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + await expect( - this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), ).to.eventually.equal(JWTProofError.JWTPublicKeyHash); }); @@ -158,8 +185,13 @@ describe('ZKJWTUtils', function () { // Use a domain that hasn't been registered jwtProof.domainName = 'unregistered-kid|https://unregistered.com|unregistered-client'; + const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + await expect( - this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), ).to.eventually.equal(JWTProofError.JWTPublicKeyHash); }); @@ -168,8 +200,13 @@ describe('ZKJWTUtils', function () { const longCommand = 'a'.repeat(606); const jwtProof = buildJWTProof(longCommand); + const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [0])]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + await expect( - this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), ).to.eventually.equal(JWTProofError.MaskedCommandLength); }); @@ -177,9 +214,13 @@ describe('ZKJWTUtils', function () { const hash = ethers.hexlify(ethers.randomBytes(32)); const command = 'invalidJWTCommand ' + ethers.toBigInt(hash).toString(); const jwtProof = buildJWTProof(command); + const template = ['invalidJWTCommand', UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; await expect( - this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), ).to.eventually.equal(JWTProofError.MismatchedCommand); }); @@ -189,8 +230,13 @@ describe('ZKJWTUtils', function () { const jwtProof = buildJWTProof(command); jwtProof.proof = '0x00'; // Invalid proof that will fail verification + const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + await expect( - this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), ).to.eventually.equal(JWTProofError.JWTProof); }); }); @@ -207,12 +253,17 @@ describe('ZKJWTUtils', function () { 'auth0-key|https://your-domain.auth0.com/|your-auth0-client-id', // Auth0 ]; + const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + for (const domain of validDomains) { const jwtProof = buildJWTProof(command); jwtProof.domainName = domain; await expect( - this.mock.$isValidZKJWT(jwtProof, this.jwtRegistry.target, this.verifier.target), + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), ).to.eventually.equal(JWTProofError.NoError); } }); @@ -225,16 +276,18 @@ describe('ZKJWTUtils', function () { const googleDomain = `${googleKid}|${googleIss}|${googleAzp}`; const nonce = ethers.hexlify(ethers.randomBytes(16)); - const command = `grant ${nonce}`; + const nonceValue = ethers.toBigInt(nonce); + const command = `grant ${nonceValue.toString()}`; const jwtProof = buildJWTProof(command); jwtProof.domainName = googleDomain; const template = ['grant', UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [nonceValue])]; const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[])'; + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template), + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), ).to.eventually.equal(JWTProofError.NoError); }); }); @@ -246,24 +299,34 @@ describe('ZKJWTUtils', function () { const command = `transfer ${amount.toString()} ${recipient}`; const jwtProof = buildJWTProof(command); const template = ['transfer', UINT_MATCHER, ETH_ADDR_MATCHER]; + const templateParams = [ + ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [amount]), + ethers.AbiCoder.defaultAbiCoder().encode(['address'], [recipient]), + ]; const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[])'; + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template), + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), ).to.eventually.equal(JWTProofError.NoError); }); it('should validate JWT maskedCommand from real proof structure', async function () { // Based on actual JWT verifier test: "Send 0.12 ETH to 0x1234" - const command = 'Send 0.12 ETH to 0x1234'; + const amount = ethers.parseEther('0.12'); + const recipient = '0x1234000000000000000000000000000000000000'; + const command = `Send ${amount.toString()} ETH to ${recipient}`; const jwtProof = buildJWTProof(command); const template = ['Send', UINT_MATCHER, 'ETH', 'to', ETH_ADDR_MATCHER]; + const templateParams = [ + ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [amount]), + ethers.AbiCoder.defaultAbiCoder().encode(['address'], [recipient]), + ]; const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[])'; + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template), + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), ).to.eventually.equal(JWTProofError.NoError); }); }); From 9764154c8a94b607129d55913ff5887c1ea2bb77 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 22 Jul 2025 13:51:22 -0600 Subject: [PATCH 04/15] Use email-tx-builder IVerifier interface instead --- .../utils/cryptography/ZKJWTVerifierMock.sol | 4 +- contracts/utils/cryptography/ZKJWTUtils.sol | 10 +-- test/utils/cryptography/ZKJWTUtils.test.js | 65 +++++++++---------- 3 files changed, 38 insertions(+), 41 deletions(-) diff --git a/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol b/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol index d475fbed..7ddb24f9 100644 --- a/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol +++ b/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.20; -import {IVerifier, EmailProof} from "@zk-email/zk-jwt/src/interfaces/IVerifier.sol"; +import {IVerifier, EmailProof} from "@zk-email/email-tx-builder/src/interfaces/IVerifier.sol"; contract ZKJWTVerifierMock is IVerifier { - function getCommandBytes() external pure returns (uint256) { + 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; } diff --git a/contracts/utils/cryptography/ZKJWTUtils.sol b/contracts/utils/cryptography/ZKJWTUtils.sol index dd414daa..08d764d1 100644 --- a/contracts/utils/cryptography/ZKJWTUtils.sol +++ b/contracts/utils/cryptography/ZKJWTUtils.sol @@ -5,7 +5,7 @@ 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/zk-jwt/src/interfaces/IVerifier.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"; /** @@ -60,7 +60,7 @@ library ZKJWTUtils { IDKIMRegistry jwtRegistry, IVerifier verifier, bytes32 hash - ) internal returns (JWTProofError) { + ) internal view returns (JWTProofError) { string[] memory signHashTemplate = new string[](2); signHashTemplate[0] = "signHash"; signHashTemplate[1] = CommandUtils.UINT_MATCHER; // UINT_MATCHER is always lowercase @@ -83,7 +83,7 @@ library ZKJWTUtils { IVerifier verifier, string[] memory template, bytes[] memory templateParams - ) internal returns (JWTProofError) { + ) internal view returns (JWTProofError) { return isValidZKJWT(jwtProof, jwtRegistry, verifier, template, templateParams, Case.ANY); } @@ -100,8 +100,8 @@ library ZKJWTUtils { string[] memory template, bytes[] memory templateParams, Case stringCase - ) internal returns (JWTProofError) { - if (bytes(jwtProof.maskedCommand).length > verifier.getCommandBytes()) { + ) internal view returns (JWTProofError) { + if (bytes(jwtProof.maskedCommand).length > verifier.commandBytes()) { return JWTProofError.MaskedCommandLength; } else if (!_commandMatch(jwtProof, template, templateParams, stringCase)) { return JWTProofError.MismatchedCommand; diff --git a/test/utils/cryptography/ZKJWTUtils.test.js b/test/utils/cryptography/ZKJWTUtils.test.js index ac4cdacd..c2be1222 100644 --- a/test/utils/cryptography/ZKJWTUtils.test.js +++ b/test/utils/cryptography/ZKJWTUtils.test.js @@ -58,28 +58,20 @@ describe('ZKJWTUtils', function () { }); describe('JWT Proof Validation', function () { - it.only('should validate ZK JWT with default signHash template', async function () { + it('should validate ZK JWT with default signHash template', async function () { const hash = ethers.hexlify(ethers.randomBytes(32)); const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); const jwtProof = buildJWTProof(command); // Use the working overload with templateParams instead of the default function - const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const template = [UINT_MATCHER]; const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; const fnSig = '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; - const tx = await this.mock[fnSig]( - jwtProof, - this.jwtRegistry.target, - this.verifier.target, - template, - templateParams, - ); - console.log(tx.provider); - // await expect(this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams)) - // .to.emit(this.mock, 'return$isValidZKJWT') - // .withArgs(JWTProofError.NoError); + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(JWTProofError.NoError); }); it('should validate ZK JWT with custom template', async function () { @@ -87,7 +79,7 @@ describe('ZKJWTUtils', function () { const commandPrefix = 'jwtCommand'; const command = commandPrefix + ' ' + ethers.toBigInt(hash).toString(); const jwtProof = buildJWTProof(command); - const template = [commandPrefix, UINT_MATCHER]; + const template = [UINT_MATCHER]; const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; const fnSig = @@ -99,7 +91,7 @@ describe('ZKJWTUtils', function () { it('should validate JWT command with address match in different cases', async function () { const commandPrefix = 'authorize'; - const template = [commandPrefix, ETH_ADDR_MATCHER]; + const template = [ETH_ADDR_MATCHER]; const testCases = [ { @@ -131,7 +123,7 @@ describe('ZKJWTUtils', function () { it('should validate JWT command with address match using ANY case', async function () { const commandPrefix = 'grant'; - const template = [commandPrefix, ETH_ADDR_MATCHER]; + const template = [ETH_ADDR_MATCHER]; // Only parameter matchers // Test with different cases that should all work with ANY case const addresses = [ @@ -168,7 +160,7 @@ describe('ZKJWTUtils', function () { const jwtProof = buildJWTProof(command); jwtProof.publicKeyHash = ethers.hexlify(ethers.randomBytes(32)); // Invalid public key hash - const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const template = [UINT_MATCHER]; const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; const fnSig = '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; @@ -185,7 +177,7 @@ describe('ZKJWTUtils', function () { // Use a domain that hasn't been registered jwtProof.domainName = 'unregistered-kid|https://unregistered.com|unregistered-client'; - const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const template = [UINT_MATCHER]; const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; const fnSig = '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; @@ -200,7 +192,7 @@ describe('ZKJWTUtils', function () { const longCommand = 'a'.repeat(606); const jwtProof = buildJWTProof(longCommand); - const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const template = [UINT_MATCHER]; const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [0])]; const fnSig = '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; @@ -214,8 +206,8 @@ describe('ZKJWTUtils', function () { const hash = ethers.hexlify(ethers.randomBytes(32)); const command = 'invalidJWTCommand ' + ethers.toBigInt(hash).toString(); const jwtProof = buildJWTProof(command); - const template = ['invalidJWTCommand', UINT_MATCHER]; - const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + const template = ['{string}']; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['string'], ['differentValue'])]; const fnSig = '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; @@ -230,7 +222,7 @@ describe('ZKJWTUtils', function () { const jwtProof = buildJWTProof(command); jwtProof.proof = '0x00'; // Invalid proof that will fail verification - const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const template = [UINT_MATCHER]; const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; const fnSig = '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; @@ -242,23 +234,28 @@ describe('ZKJWTUtils', function () { }); describe('JWT-Specific Domain Format', function () { + beforeEach(async function () { + this.validDomains = [ + 'test-kid|https://accounts.google.com|1234567890.apps.googleusercontent.com', + 'auth0-key|https://your-domain.auth0.com/|your-auth0-client-id', + 'google-key-id-123|https://accounts.google.com|1234567890.apps.googleusercontent.com', + ]; + + for (const domain of this.validDomains) { + await this.jwtRegistry.connect(this.admin).setJwtPublicKey(domain, publicKeyHash); + } + }); + it('should validate proper kid|iss|azp domain format', async function () { const hash = ethers.hexlify(ethers.randomBytes(32)); const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); - // Test various valid JWT domain formats - const validDomains = [ - '12345|https://example.com|client-id-12345', // From actual tests - 'test-kid|https://accounts.google.com|1234567890.apps.googleusercontent.com', // Google - 'auth0-key|https://your-domain.auth0.com/|your-auth0-client-id', // Auth0 - ]; - - const template = [SIGN_HASH_COMMAND, UINT_MATCHER]; + const template = [UINT_MATCHER]; const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; const fnSig = '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; - for (const domain of validDomains) { + for (const domain of this.validDomains) { const jwtProof = buildJWTProof(command); jwtProof.domainName = domain; @@ -281,7 +278,7 @@ describe('ZKJWTUtils', function () { const jwtProof = buildJWTProof(command); jwtProof.domainName = googleDomain; - const template = ['grant', UINT_MATCHER]; + const template = [UINT_MATCHER]; const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [nonceValue])]; const fnSig = '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; @@ -298,7 +295,7 @@ describe('ZKJWTUtils', function () { const recipient = this.other.address; const command = `transfer ${amount.toString()} ${recipient}`; const jwtProof = buildJWTProof(command); - const template = ['transfer', UINT_MATCHER, ETH_ADDR_MATCHER]; + const template = [UINT_MATCHER, ETH_ADDR_MATCHER]; const templateParams = [ ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [amount]), ethers.AbiCoder.defaultAbiCoder().encode(['address'], [recipient]), @@ -317,7 +314,7 @@ describe('ZKJWTUtils', function () { const recipient = '0x1234000000000000000000000000000000000000'; const command = `Send ${amount.toString()} ETH to ${recipient}`; const jwtProof = buildJWTProof(command); - const template = ['Send', UINT_MATCHER, 'ETH', 'to', ETH_ADDR_MATCHER]; + const template = [UINT_MATCHER, 'ETH', 'to', ETH_ADDR_MATCHER]; const templateParams = [ ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [amount]), ethers.AbiCoder.defaultAbiCoder().encode(['address'], [recipient]), From 6e32f26323c52a7bc7c9718b7fb89aab9267e0d8 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 22 Jul 2025 13:57:11 -0600 Subject: [PATCH 05/15] Run npm i --- package-lock.json | 105 +++++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 57 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7965629d..ea296a74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ }, "lib/@openzeppelin-contracts": { "name": "openzeppelin-solidity", - "version": "5.3.0", + "version": "5.4.0", "dev": true, "license": "MIT", "devDependencies": { @@ -25,8 +25,8 @@ "@changesets/read": "^0.6.0", "@eslint/compat": "^1.2.1", "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", - "@nomicfoundation/hardhat-ethers": "^3.0.4", - "@nomicfoundation/hardhat-network-helpers": "^1.0.3", + "@nomicfoundation/hardhat-ethers": "^3.0.9", + "@nomicfoundation/hardhat-network-helpers": "^1.0.13", "@openzeppelin/docs-utils": "^0.1.5", "@openzeppelin/merkle-tree": "^1.0.7", "@openzeppelin/upgrade-safe-transpiler": "^0.3.32", @@ -38,7 +38,7 @@ "glob": "^11.0.0", "globals": "^16.0.0", "graphlib": "^2.1.8", - "hardhat": "^2.24.0", + "hardhat": "^2.24.3", "hardhat-exposed": "^0.3.15", "hardhat-gas-reporter": "^2.1.0", "hardhat-ignore-warnings": "^0.2.11", @@ -1764,28 +1764,28 @@ } }, "node_modules/@nomicfoundation/edr": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.11.0.tgz", - "integrity": "sha512-36WERf8ldvyHR6UAbcYsa+vpbW7tCrJGBwF4gXSsb8+STj1n66Hz85Y/O7B9+8AauX3PhglvV5dKl91tk43mWw==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.11.3.tgz", + "integrity": "sha512-kqILRkAd455Sd6v8mfP3C1/0tCOynJWY+Ir+k/9Boocu2kObCrsFgG+ZWB7fSBVdd9cPVSNrnhWS+V+PEo637g==", "dev": true, "license": "MIT", "dependencies": { - "@nomicfoundation/edr-darwin-arm64": "0.11.0", - "@nomicfoundation/edr-darwin-x64": "0.11.0", - "@nomicfoundation/edr-linux-arm64-gnu": "0.11.0", - "@nomicfoundation/edr-linux-arm64-musl": "0.11.0", - "@nomicfoundation/edr-linux-x64-gnu": "0.11.0", - "@nomicfoundation/edr-linux-x64-musl": "0.11.0", - "@nomicfoundation/edr-win32-x64-msvc": "0.11.0" + "@nomicfoundation/edr-darwin-arm64": "0.11.3", + "@nomicfoundation/edr-darwin-x64": "0.11.3", + "@nomicfoundation/edr-linux-arm64-gnu": "0.11.3", + "@nomicfoundation/edr-linux-arm64-musl": "0.11.3", + "@nomicfoundation/edr-linux-x64-gnu": "0.11.3", + "@nomicfoundation/edr-linux-x64-musl": "0.11.3", + "@nomicfoundation/edr-win32-x64-msvc": "0.11.3" }, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-darwin-arm64": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.11.0.tgz", - "integrity": "sha512-aYTVdcSs27XG7ayTzvZ4Yn9z/ABSaUwicrtrYK2NR8IH0ik4N4bWzo/qH8rax6rewVLbHUkGyGYnsy5ZN4iiMw==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.11.3.tgz", + "integrity": "sha512-w0tksbdtSxz9nuzHKsfx4c2mwaD0+l5qKL2R290QdnN9gi9AV62p9DHkOgfBdyg6/a6ZlnQqnISi7C9avk/6VA==", "dev": true, "license": "MIT", "engines": { @@ -1793,9 +1793,9 @@ } }, "node_modules/@nomicfoundation/edr-darwin-x64": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.11.0.tgz", - "integrity": "sha512-RxX7UYgvJrfcyT/uHUn44Nsy1XaoW+Q1khKMdHKxeW7BrgIi+Lz+siz3bX5vhSoAnKilDPhIVLrnC8zxQhjR2A==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.11.3.tgz", + "integrity": "sha512-QR4jAFrPbOcrO7O2z2ESg+eUeIZPe2bPIlQYgiJ04ltbSGW27FblOzdd5+S3RoOD/dsZGKAvvy6dadBEl0NgoA==", "dev": true, "license": "MIT", "engines": { @@ -1803,9 +1803,9 @@ } }, "node_modules/@nomicfoundation/edr-linux-arm64-gnu": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.11.0.tgz", - "integrity": "sha512-J0j+rs0s11FuSipt/ymqrFmpJ7c0FSz1/+FohCIlUXDxFv//+1R/8lkGPjEYFmy8DPpk/iO8mcpqHTGckREbqA==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.11.3.tgz", + "integrity": "sha512-Ktjv89RZZiUmOFPspuSBVJ61mBZQ2+HuLmV67InNlh9TSUec/iDjGIwAn59dx0bF/LOSrM7qg5od3KKac4LJDQ==", "dev": true, "license": "MIT", "engines": { @@ -1813,9 +1813,9 @@ } }, "node_modules/@nomicfoundation/edr-linux-arm64-musl": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.11.0.tgz", - "integrity": "sha512-4r32zkGMN7WT/CMEuW0VjbuEdIeCskHNDMW4SSgQSJOE/N9L1KSLJCSsAbPD3aYE+e4WRDTyOwmuLjeUTcLZKQ==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.11.3.tgz", + "integrity": "sha512-B3sLJx1rL2E9pfdD4mApiwOZSrX0a/KQSBWdlq1uAhFKqkl00yZaY4LejgZndsJAa4iKGQJlGnw4HCGeVt0+jA==", "dev": true, "license": "MIT", "engines": { @@ -1823,9 +1823,9 @@ } }, "node_modules/@nomicfoundation/edr-linux-x64-gnu": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.11.0.tgz", - "integrity": "sha512-SmdncQHLYtVNWLIMyGaY6LpAfamzTDe3fxjkirmJv3CWR5tcEyC6LMui/GsIVnJzXeNJBXAzwl8hTUAxHTM6kQ==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.11.3.tgz", + "integrity": "sha512-D/4cFKDXH6UYyKPu6J3Y8TzW11UzeQI0+wS9QcJzjlrrfKj0ENW7g9VihD1O2FvXkdkTjcCZYb6ai8MMTCsaVw==", "dev": true, "license": "MIT", "engines": { @@ -1833,9 +1833,9 @@ } }, "node_modules/@nomicfoundation/edr-linux-x64-musl": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.11.0.tgz", - "integrity": "sha512-w6hUqpn/trwiH6SRuRGysj37LsQVCX5XDCA3Xi81sbOaLhbHrNvK9TXWyZmcuzbdTKQQW6VNywcSxDdOiChcJg==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.11.3.tgz", + "integrity": "sha512-ergXuIb4nIvmf+TqyiDX5tsE49311DrBky6+jNLgsGDTBaN1GS3OFwFS8I6Ri/GGn6xOaT8sKu3q7/m+WdlFzg==", "dev": true, "license": "MIT", "engines": { @@ -1843,9 +1843,9 @@ } }, "node_modules/@nomicfoundation/edr-win32-x64-msvc": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.11.0.tgz", - "integrity": "sha512-BLmULjRKoH9BsX+c4Na2ypV7NGeJ+M6Zpqj/faPOwleVscDdSr/IhriyPaXCe8dyfwbge7lWsbekiADtPSnB2Q==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.11.3.tgz", + "integrity": "sha512-snvEf+WB3OV0wj2A7kQ+ZQqBquMcrozSLXcdnMdEl7Tmn+KDCbmFKBt3Tk0X3qOU4RKQpLPnTxdM07TJNVtung==", "dev": true, "license": "MIT", "engines": { @@ -1872,9 +1872,9 @@ } }, "node_modules/@nomicfoundation/hardhat-ethers": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.0.8.tgz", - "integrity": "sha512-zhOZ4hdRORls31DTOqg+GmEZM0ujly8GGIuRY7t7szEk2zW/arY1qDug/py8AEktT00v5K+b6RvbVog+va51IA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.1.0.tgz", + "integrity": "sha512-jx6fw3Ms7QBwFGT2MU6ICG292z0P81u6g54JjSV105+FbTZOF4FJqPksLfDybxkkOeq28eDxbqq7vpxRYyIlxA==", "dev": true, "license": "MIT", "dependencies": { @@ -1882,21 +1882,21 @@ "lodash.isequal": "^4.5.0" }, "peerDependencies": { - "ethers": "^6.1.0", - "hardhat": "^2.0.0" + "ethers": "^6.14.0", + "hardhat": "^2.26.0" } }, "node_modules/@nomicfoundation/hardhat-network-helpers": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.12.tgz", - "integrity": "sha512-xTNQNI/9xkHvjmCJnJOTyqDSl8uq1rKb2WOVmixQxFtRd7Oa3ecO8zM0cyC2YmOK+jHB9WPZ+F/ijkHg1CoORA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.1.0.tgz", + "integrity": "sha512-ZS+NulZuR99NUHt2VwcgZvgeD6Y63qrbORNRuKO+lTowJxNVsrJ0zbRx1j5De6G3dOno5pVGvuYSq2QVG0qCYg==", "dev": true, "license": "MIT", "dependencies": { "ethereumjs-util": "^7.1.4" }, "peerDependencies": { - "hardhat": "^2.9.5" + "hardhat": "^2.26.0" } }, "node_modules/@nomicfoundation/slang": { @@ -2546,13 +2546,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", @@ -5686,19 +5679,17 @@ } }, "node_modules/hardhat": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.24.0.tgz", - "integrity": "sha512-wDkD5GPmttYv21MR7tGDkyQ22tO2V86OEV8pA7NcXWYUpibe8XZ2EanXCeRHO61vwEx0f7/M+NqrhJwasaNMJg==", + "version": "2.26.1", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.26.1.tgz", + "integrity": "sha512-CXWuUaTtehxiHPCdlitntctfeYRgujmXkNX5gnrD5jdA6HhRQt+WWBZE/gHXbE29y/wDmmUL2d652rI0ctjqjw==", "dev": true, "license": "MIT", "dependencies": { "@ethereumjs/util": "^9.1.0", "@ethersproject/abi": "^5.1.2", - "@nomicfoundation/edr": "^0.11.0", + "@nomicfoundation/edr": "^0.11.3", "@nomicfoundation/solidity-analyzer": "^0.1.0", "@sentry/node": "^5.18.1", - "@types/bn.js": "^5.1.0", - "@types/lru-cache": "^5.1.0", "adm-zip": "^0.4.16", "aggregate-error": "^3.0.0", "ansi-escapes": "^4.3.0", From c535f778f09a239beeddbe22655a2703fb163e5b Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 22 Jul 2025 13:59:31 -0600 Subject: [PATCH 06/15] Fix paymaster pragma --- contracts/account/paymaster/PaymasterSigner.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/account/paymaster/PaymasterSigner.sol b/contracts/account/paymaster/PaymasterSigner.sol index e7d016fd..011b2883 100644 --- a/contracts/account/paymaster/PaymasterSigner.sol +++ b/contracts/account/paymaster/PaymasterSigner.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; From 2d3f46937e3417d63f34d48302ca2abd5859e107 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 25 Jul 2025 15:32:57 -0600 Subject: [PATCH 07/15] update dependencies --- lib/@openzeppelin-contracts | 2 +- lib/@openzeppelin-contracts-upgradeable | 2 +- lib/zk-jwt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/@openzeppelin-contracts b/lib/@openzeppelin-contracts index 32e7a6ff..da1dfe6c 160000 --- a/lib/@openzeppelin-contracts +++ b/lib/@openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 32e7a6ffbc5af9ab0e6dfdbc58508511d0f0b4a2 +Subproject commit da1dfe6c96f6f07f174a98428322b1386e12b024 diff --git a/lib/@openzeppelin-contracts-upgradeable b/lib/@openzeppelin-contracts-upgradeable index 7bb7e907..ec2bf908 160000 --- a/lib/@openzeppelin-contracts-upgradeable +++ b/lib/@openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 7bb7e9077dd93d116657bb181e32c84165b8dbba +Subproject commit ec2bf9084c43b411abe0cc17cb1ef3db3fdffc12 diff --git a/lib/zk-jwt b/lib/zk-jwt index 2f20f059..684f08c4 160000 --- a/lib/zk-jwt +++ b/lib/zk-jwt @@ -1 +1 @@ -Subproject commit 2f20f059cf1ad3a4f1ea0216f907923e4c43a8db +Subproject commit 684f08c46e448e6cc23412545fae0a33b4a4296c From 972466c791a03d8ca33d6c09d308ff776b528566 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 25 Jul 2025 15:39:13 -0600 Subject: [PATCH 08/15] up --- .../utils/cryptography/ZKJWTVerifierMock.sol | 4 +- package-lock.json | 118 +++++++++++++++++- 2 files changed, 115 insertions(+), 7 deletions(-) diff --git a/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol b/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol index 7ddb24f9..d475fbed 100644 --- a/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol +++ b/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.20; -import {IVerifier, EmailProof} from "@zk-email/email-tx-builder/src/interfaces/IVerifier.sol"; +import {IVerifier, EmailProof} from "@zk-email/zk-jwt/src/interfaces/IVerifier.sol"; contract ZKJWTVerifierMock is IVerifier { - function commandBytes() external pure returns (uint256) { + function getCommandBytes() 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; } diff --git a/package-lock.json b/package-lock.json index ea296a74..147c9ea3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,7 @@ "prettier-plugin-solidity": "^2.0.0", "rimraf": "^6.0.0", "semver": "^7.3.5", - "solhint": "^5.0.0", + "solhint": "^6.0.0", "solhint-plugin-openzeppelin": "file:scripts/solhint-custom", "solidity-ast": "^0.4.50", "solidity-coverage": "^0.8.14", @@ -1421,6 +1421,15 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@humanwhocodes/momoa": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", + "integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==", + "dev": true, + "engines": { + "node": ">=10.10.0" + } + }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", @@ -2714,6 +2723,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true, + "peerDependencies": { + "ajv": ">=5.0.0" + } + }, "node_modules/amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", @@ -2901,6 +2919,41 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/better-ajv-errors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-2.0.2.tgz", + "integrity": "sha512-1cLrJXEq46n0hjV8dDYwg9LKYjDb3KbeW7nZTv4kvfoDD9c2DXHIE31nxM+Y/cIfXMggLUfmxbm6h/JoM/yotA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@humanwhocodes/momoa": "^2.0.4", + "chalk": "^4.1.2", + "jsonpointer": "^5.0.1", + "leven": "^3.1.0 < 4" + }, + "engines": { + "node": ">= 18.20.6" + }, + "peerDependencies": { + "ajv": "4.11.8 - 8" + } + }, + "node_modules/better-ajv-errors/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/better-path-resolve": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", @@ -6935,6 +6988,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jsonschema": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", @@ -6997,6 +7059,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -9746,20 +9817,22 @@ } }, "node_modules/solhint": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/solhint/-/solhint-5.1.0.tgz", - "integrity": "sha512-KWg4gnOnznxHXzH0fUvnhnxnk+1R50GiPChcPeQgA7SKQTSF1LLIEh8R1qbkCEn/fFzz4CfJs+Gh7Rl9uhHy+g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-6.0.0.tgz", + "integrity": "sha512-PQGfwFqfeYdebi2tEG1fhVfMjqSzbW3Noz+LYf8UusKe5nkikCghdgEjYQPcGfFZj4snlVyJQt//AaxkubOtVQ==", "dev": true, - "license": "MIT", "dependencies": { "@solidity-parser/parser": "^0.20.0", "ajv": "^6.12.6", + "ajv-errors": "^1.0.1", "antlr4": "^4.13.1-patch-1", "ast-parents": "^0.0.1", + "better-ajv-errors": "^2.0.2", "chalk": "^4.1.2", "commander": "^10.0.0", "cosmiconfig": "^8.0.0", "fast-diff": "^1.2.0", + "fs-extra": "^11.1.0", "glob": "^8.0.3", "ignore": "^5.2.4", "js-yaml": "^4.1.0", @@ -9850,6 +9923,20 @@ "node": ">=14" } }, + "node_modules/solhint/node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/solhint/node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -9891,6 +9978,18 @@ "dev": true, "license": "MIT" }, + "node_modules/solhint/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/solhint/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -9921,6 +10020,15 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/solhint/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/solidity-ast": { "version": "0.4.60", "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.60.tgz", From 0c31be225a0f0fb50d1c38cff89526e5d1cc45c0 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 25 Jul 2025 15:44:10 -0600 Subject: [PATCH 09/15] fix tests --- contracts/utils/cryptography/ZKJWTUtils.sol | 4 ++-- test/utils/cryptography/ZKJWTUtils.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/utils/cryptography/ZKJWTUtils.sol b/contracts/utils/cryptography/ZKJWTUtils.sol index 08d764d1..00dfca93 100644 --- a/contracts/utils/cryptography/ZKJWTUtils.sol +++ b/contracts/utils/cryptography/ZKJWTUtils.sol @@ -5,7 +5,7 @@ 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 {IVerifier, EmailProof} from "@zk-email/zk-jwt/src/interfaces/IVerifier.sol"; import {CommandUtils} from "@zk-email/email-tx-builder/src/libraries/CommandUtils.sol"; /** @@ -101,7 +101,7 @@ library ZKJWTUtils { bytes[] memory templateParams, Case stringCase ) internal view returns (JWTProofError) { - if (bytes(jwtProof.maskedCommand).length > verifier.commandBytes()) { + if (bytes(jwtProof.maskedCommand).length > verifier.getCommandBytes()) { return JWTProofError.MaskedCommandLength; } else if (!_commandMatch(jwtProof, template, templateParams, stringCase)) { return JWTProofError.MismatchedCommand; diff --git a/test/utils/cryptography/ZKJWTUtils.test.js b/test/utils/cryptography/ZKJWTUtils.test.js index c2be1222..442ebcd5 100644 --- a/test/utils/cryptography/ZKJWTUtils.test.js +++ b/test/utils/cryptography/ZKJWTUtils.test.js @@ -31,7 +31,7 @@ async function fixture() { .then(() => jwtRegistry.isJwtPublicKeyValid(domainName, publicKeyHash)); // JWT Verifier - const verifier = await ethers.deployContract('ZKJWTVerifierMock'); + const verifier = await ethers.deployContract('$ZKJWTVerifierMock'); // ZKJWTUtils mock contract const mock = await ethers.deployContract('$ZKJWTUtils'); From a3947330612c2bd9dc4ba447e3e666b7e62183be Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 25 Jul 2025 18:24:35 -0600 Subject: [PATCH 10/15] Update tests --- contracts/utils/cryptography/ZKJWTUtils.sol | 14 +- test/utils/cryptography/ZKJWTUtils.test.js | 452 ++++++++++---------- 2 files changed, 223 insertions(+), 243 deletions(-) diff --git a/contracts/utils/cryptography/ZKJWTUtils.sol b/contracts/utils/cryptography/ZKJWTUtils.sol index 00dfca93..53dc4d3f 100644 --- a/contracts/utils/cryptography/ZKJWTUtils.sol +++ b/contracts/utils/cryptography/ZKJWTUtils.sol @@ -61,10 +61,9 @@ library ZKJWTUtils { 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); + string[] memory signHashTemplate = new string[](1); + signHashTemplate[0] = CommandUtils.UINT_MATCHER; + return isValidZKJWT(jwtProof, jwtRegistry, verifier, signHashTemplate, _asSingletonArray(hash), Case.LOWERCASE); // UINT_MATCHER is always lowercase } /** @@ -112,7 +111,6 @@ library ZKJWTUtils { } // Verify the zero-knowledge proof of JWT signature - // TODO: Is `verifyEmailProof` supposed to be non-view? return verifier.verifyEmailProof(jwtProof) ? JWTProofError.NoError : JWTProofError.JWTProof; } @@ -140,8 +138,10 @@ library ZKJWTUtils { 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 + mstore(add(array, 0x20), add(array, 0x40)) // Store the offset to the array content + mstore(add(array, 0x40), 32) // Store the length of the element + mstore(add(array, 0x60), element) // Store the single element + mstore(0x40, add(array, 0x80)) // Update memory pointer } } } diff --git a/test/utils/cryptography/ZKJWTUtils.test.js b/test/utils/cryptography/ZKJWTUtils.test.js index 442ebcd5..6e9bb2ad 100644 --- a/test/utils/cryptography/ZKJWTUtils.test.js +++ b/test/utils/cryptography/ZKJWTUtils.test.js @@ -57,274 +57,254 @@ describe('ZKJWTUtils', function () { Object.assign(this, await loadFixture(fixture)); }); - describe('JWT Proof Validation', function () { - it('should validate ZK JWT with default signHash template', async function () { - const hash = ethers.hexlify(ethers.randomBytes(32)); - const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); - const jwtProof = buildJWTProof(command); + it('should validate ZKJWT sign hash', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + const jwtProof = buildJWTProof(command); + + // Use the default function that handles signHash template internally + const fnSig = '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,bytes32)'; + await expect(this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, hash)).to.eventually.equal( + JWTProofError.NoError, + ); + }); - // Use the working overload with templateParams instead of the default function - const template = [UINT_MATCHER]; - const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + it('should validate ZKJWT with template', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const commandPrefix = 'jwtCommand'; + const command = commandPrefix + ' ' + ethers.toBigInt(hash).toString(); + const jwtProof = buildJWTProof(command); + const template = [UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(JWTProofError.NoError); + }); - const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; - await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), - ).to.eventually.equal(JWTProofError.NoError); - }); + it('should validate complex JWT commands with multiple parameters', async function () { + const amount = ethers.parseEther('1.5'); + const recipient = this.other.address; + const command = `transfer ${amount.toString()} ${recipient}`; + const jwtProof = buildJWTProof(command); + const template = [UINT_MATCHER, ETH_ADDR_MATCHER]; + const templateParams = [ + ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [amount]), + ethers.AbiCoder.defaultAbiCoder().encode(['address'], [recipient]), + ]; + + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(JWTProofError.NoError); + }); + + it('should validate JWT maskedCommand from real proof structure', async function () { + // Based on actual JWT verifier test: "Send 0.12 ETH to 0x1234" + const amount = ethers.parseEther('0.12'); + const recipient = '0x1234000000000000000000000000000000000000'; + const command = `Send ${amount.toString()} ETH to ${recipient}`; + const jwtProof = buildJWTProof(command); + const template = [UINT_MATCHER, 'ETH', 'to', ETH_ADDR_MATCHER]; + const templateParams = [ + ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [amount]), + ethers.AbiCoder.defaultAbiCoder().encode(['address'], [recipient]), + ]; + + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(JWTProofError.NoError); + }); - it('should validate ZK JWT with custom template', async function () { - const hash = ethers.hexlify(ethers.randomBytes(32)); - const commandPrefix = 'jwtCommand'; - const command = commandPrefix + ' ' + ethers.toBigInt(hash).toString(); + it('should validate command with address match in different cases', async function () { + const commandPrefix = 'authorize'; + const template = [ETH_ADDR_MATCHER]; + + const testCases = [ + { + caseType: Case.LOWERCASE, + address: this.other.address.toLowerCase(), + }, + { + caseType: Case.UPPERCASE, + address: this.other.address.toUpperCase().replace('0X', '0x'), + }, + { + caseType: Case.CHECKSUM, + address: ethers.getAddress(this.other.address), + }, + ]; + + for (const { caseType, address } of testCases) { + const command = commandPrefix + ' ' + address; const jwtProof = buildJWTProof(command); - const template = [UINT_MATCHER]; - const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['address'], [address])]; const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[],uint8)'; await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams, caseType), ).to.eventually.equal(JWTProofError.NoError); - }); - - it('should validate JWT command with address match in different cases', async function () { - const commandPrefix = 'authorize'; - const template = [ETH_ADDR_MATCHER]; - - const testCases = [ - { - caseType: Case.LOWERCASE, - address: this.other.address.toLowerCase(), - }, - { - caseType: Case.UPPERCASE, - address: this.other.address.toUpperCase().replace('0X', '0x'), - }, - { - caseType: Case.CHECKSUM, - address: ethers.getAddress(this.other.address), - }, - ]; - - for (const { caseType, address } of testCases) { - const command = commandPrefix + ' ' + address; - const jwtProof = buildJWTProof(command); - const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['address'], [address])]; - - const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[],uint8)'; - await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams, caseType), - ).to.eventually.equal(JWTProofError.NoError); - } - }); - - it('should validate JWT command with address match using ANY case', async function () { - const commandPrefix = 'grant'; - const template = [ETH_ADDR_MATCHER]; // Only parameter matchers - - // Test with different cases that should all work with ANY case - const addresses = [ - this.other.address.toLowerCase(), - this.other.address.toUpperCase().replace('0X', '0x'), - ethers.getAddress(this.other.address), - ]; - - for (const address of addresses) { - const command = commandPrefix + ' ' + address; - const jwtProof = buildJWTProof(command); - const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['address'], [address])]; - - const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[],uint8)'; - await expect( - this.mock[fnSig]( - jwtProof, - this.jwtRegistry.target, - this.verifier.target, - template, - templateParams, - ethers.Typed.uint8(Case.ANY), - ), - ).to.eventually.equal(JWTProofError.NoError); - } - }); + } }); - describe('JWT Error Handling', function () { - it('should detect invalid JWT public key hash', async function () { - const hash = ethers.hexlify(ethers.randomBytes(32)); - const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); - const jwtProof = buildJWTProof(command); - jwtProof.publicKeyHash = ethers.hexlify(ethers.randomBytes(32)); // Invalid public key hash - - const template = [UINT_MATCHER]; - const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; - const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + it('should validate command with address match using any case', async function () { + const commandPrefix = 'grant'; + const template = [ETH_ADDR_MATCHER]; // Only parameter matchers - await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), - ).to.eventually.equal(JWTProofError.JWTPublicKeyHash); - }); + // Test with different cases that should all work with ANY case + const addresses = [ + this.other.address.toLowerCase(), + this.other.address.toUpperCase().replace('0X', '0x'), + ethers.getAddress(this.other.address), + ]; - it('should detect unregistered domain format', async function () { - const hash = ethers.hexlify(ethers.randomBytes(32)); - const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + for (const address of addresses) { + const command = commandPrefix + ' ' + address; const jwtProof = buildJWTProof(command); - // Use a domain that hasn't been registered - jwtProof.domainName = 'unregistered-kid|https://unregistered.com|unregistered-client'; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['address'], [address])]; - const template = [UINT_MATCHER]; - const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; - + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[],uint8)'; await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), - ).to.eventually.equal(JWTProofError.JWTPublicKeyHash); - }); + this.mock[fnSig]( + jwtProof, + this.jwtRegistry.target, + this.verifier.target, + template, + templateParams, + ethers.Typed.uint8(Case.ANY), + ), + ).to.eventually.equal(JWTProofError.NoError); + } + }); - it('should detect invalid masked command length', async function () { - // Create a command that's too long (exceeds circuit limits - 605 bytes max) - const longCommand = 'a'.repeat(606); - const jwtProof = buildJWTProof(longCommand); + it('should detect invalid JWT public key hash', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + const jwtProof = buildJWTProof(command); + jwtProof.publicKeyHash = ethers.hexlify(ethers.randomBytes(32)); // Invalid public key hash - const template = [UINT_MATCHER]; - const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [0])]; - const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + const template = [UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; - await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), - ).to.eventually.equal(JWTProofError.MaskedCommandLength); - }); + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(JWTProofError.JWTPublicKeyHash); + }); - it('should detect mismatched command template', async function () { - const hash = ethers.hexlify(ethers.randomBytes(32)); - const command = 'invalidJWTCommand ' + ethers.toBigInt(hash).toString(); - const jwtProof = buildJWTProof(command); - const template = ['{string}']; - const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['string'], ['differentValue'])]; + it('should detect unregistered domain format', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + const jwtProof = buildJWTProof(command); + // Use a domain that hasn't been registered + jwtProof.domainName = 'unregistered-kid|https://unregistered.com|unregistered-client'; + + const template = [UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(JWTProofError.JWTPublicKeyHash); + }); - const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; - await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), - ).to.eventually.equal(JWTProofError.MismatchedCommand); - }); + it('should detect invalid masked command length', async function () { + // Create a command that's too long (exceeds circuit limits - 605 bytes max) + const longCommand = 'a'.repeat(606); + const jwtProof = buildJWTProof(longCommand); - it('should detect invalid JWT zero-knowledge proof', async function () { - const hash = ethers.hexlify(ethers.randomBytes(32)); - const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); - const jwtProof = buildJWTProof(command); - jwtProof.proof = '0x00'; // Invalid proof that will fail verification + const template = [UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [0])]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; - const template = [UINT_MATCHER]; - const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; - const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(JWTProofError.MaskedCommandLength); + }); - await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), - ).to.eventually.equal(JWTProofError.JWTProof); - }); + it('should detect mismatched command template', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const command = 'invalidJWTCommand ' + ethers.toBigInt(hash).toString(); + const jwtProof = buildJWTProof(command); + const template = ['{string}']; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['string'], ['differentValue'])]; + + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(JWTProofError.MismatchedCommand); }); - describe('JWT-Specific Domain Format', function () { - beforeEach(async function () { - this.validDomains = [ - 'test-kid|https://accounts.google.com|1234567890.apps.googleusercontent.com', - 'auth0-key|https://your-domain.auth0.com/|your-auth0-client-id', - 'google-key-id-123|https://accounts.google.com|1234567890.apps.googleusercontent.com', - ]; + it('should detect invalid JWT proof', async function () { + const hash = ethers.hexlify(ethers.randomBytes(32)); + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + const jwtProof = buildJWTProof(command); + jwtProof.proof = '0x00'; // Invalid proof that will fail verification - for (const domain of this.validDomains) { - await this.jwtRegistry.connect(this.admin).setJwtPublicKey(domain, publicKeyHash); - } - }); + const fnSig = '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,bytes32)'; + await expect(this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, hash)).to.eventually.equal( + JWTProofError.JWTProof, + ); + }); - it('should validate proper kid|iss|azp domain format', async function () { - const hash = ethers.hexlify(ethers.randomBytes(32)); - const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); + it('should validate proper kid|iss|azp domain format', async function () { + const domain = 'auth0-key|https://your-domain.auth0.com/|your-auth0-client-id'; + await this.jwtRegistry.connect(this.admin).setJwtPublicKey(domain, publicKeyHash); - const template = [UINT_MATCHER]; - const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; - const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; - - for (const domain of this.validDomains) { - const jwtProof = buildJWTProof(command); - jwtProof.domainName = domain; - - await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), - ).to.eventually.equal(JWTProofError.NoError); - } - }); - - it('should handle JWT with real Google OAuth format', async function () { - // Based on actual Google JWT structure - const googleKid = 'google-key-id-123'; - const googleIss = 'https://accounts.google.com'; - const googleAzp = '1234567890.apps.googleusercontent.com'; - const googleDomain = `${googleKid}|${googleIss}|${googleAzp}`; - - const nonce = ethers.hexlify(ethers.randomBytes(16)); - const nonceValue = ethers.toBigInt(nonce); - const command = `grant ${nonceValue.toString()}`; - const jwtProof = buildJWTProof(command); - jwtProof.domainName = googleDomain; + const hash = ethers.hexlify(ethers.randomBytes(32)); + const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString(); - const template = [UINT_MATCHER]; - const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [nonceValue])]; - const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + const template = [UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [ethers.toBigInt(hash)])]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; - await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), - ).to.eventually.equal(JWTProofError.NoError); - }); - }); + const jwtProof = buildJWTProof(command); + jwtProof.domainName = domain; - describe('JWT Template Matching', function () { - it('should validate complex JWT commands with multiple parameters', async function () { - const amount = ethers.parseEther('1.5'); - const recipient = this.other.address; - const command = `transfer ${amount.toString()} ${recipient}`; - const jwtProof = buildJWTProof(command); - const template = [UINT_MATCHER, ETH_ADDR_MATCHER]; - const templateParams = [ - ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [amount]), - ethers.AbiCoder.defaultAbiCoder().encode(['address'], [recipient]), - ]; - - const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; - await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), - ).to.eventually.equal(JWTProofError.NoError); - }); - - it('should validate JWT maskedCommand from real proof structure', async function () { - // Based on actual JWT verifier test: "Send 0.12 ETH to 0x1234" - const amount = ethers.parseEther('0.12'); - const recipient = '0x1234000000000000000000000000000000000000'; - const command = `Send ${amount.toString()} ETH to ${recipient}`; - const jwtProof = buildJWTProof(command); - const template = [UINT_MATCHER, 'ETH', 'to', ETH_ADDR_MATCHER]; - const templateParams = [ - ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [amount]), - ethers.AbiCoder.defaultAbiCoder().encode(['address'], [recipient]), - ]; + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(JWTProofError.NoError); + }); - const fnSig = - '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; - await expect( - this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), - ).to.eventually.equal(JWTProofError.NoError); - }); + it('should handle JWT with real Google OAuth format', async function () { + await this.jwtRegistry + .connect(this.admin) + .setJwtPublicKey( + 'google-key-id-123|https://accounts.google.com|1234567890.apps.googleusercontent.com', + publicKeyHash, + ); + + // Based on actual Google JWT structure + const googleKid = 'google-key-id-123'; + const googleIss = 'https://accounts.google.com'; + const googleAzp = '1234567890.apps.googleusercontent.com'; + const googleDomain = `${googleKid}|${googleIss}|${googleAzp}`; + + const nonce = ethers.hexlify(ethers.randomBytes(16)); + const nonceValue = ethers.toBigInt(nonce); + const command = `grant ${nonceValue.toString()}`; + const jwtProof = buildJWTProof(command); + jwtProof.domainName = googleDomain; + + const template = [UINT_MATCHER]; + const templateParams = [ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [nonceValue])]; + const fnSig = + '$isValidZKJWT((string,bytes32,uint256,string,bytes32,bytes32,bool,bytes),address,address,string[],bytes[])'; + + await expect( + this.mock[fnSig](jwtProof, this.jwtRegistry.target, this.verifier.target, template, templateParams), + ).to.eventually.equal(JWTProofError.NoError); }); }); From b70eacb783ec5e6dd1b58991f48d3ec1bdea5105 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 26 Jul 2025 09:12:39 -0600 Subject: [PATCH 11/15] WIP: Fuzzing --- .../utils/cryptography/ZKJWTVerifierMock.sol | 2 +- contracts/utils/cryptography/ZKJWTUtils.sol | 16 +- test/utils/cryptography/ZKEmailUtils.t.sol | 100 +----- test/utils/cryptography/ZKJWTUtils.t.sol | 302 ++++++++++++++++++ 4 files changed, 323 insertions(+), 97 deletions(-) create mode 100644 test/utils/cryptography/ZKJWTUtils.t.sol diff --git a/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol b/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol index d475fbed..5f865cbe 100644 --- a/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol +++ b/contracts/mocks/utils/cryptography/ZKJWTVerifierMock.sol @@ -6,7 +6,7 @@ import {IVerifier, EmailProof} from "@zk-email/zk-jwt/src/interfaces/IVerifier.s contract ZKJWTVerifierMock is IVerifier { function getCommandBytes() external pure returns (uint256) { - // Same as in https://github.com/zkemail/email-tx-builder/blob/1452943807a5fdc732e1113c34792c76cf7dd031/packages/contracts/src/utils/Verifier.sol#L15 + // Same as in https://github.com/zkemail/zk-jwt/blob/27436a2f23e78e89cf624f649ec1d125f13772dd/packages/contracts/src/utils/JwtVerifier.sol#L20 return 605; } diff --git a/contracts/utils/cryptography/ZKJWTUtils.sol b/contracts/utils/cryptography/ZKJWTUtils.sol index 53dc4d3f..a292d5f8 100644 --- a/contracts/utils/cryptography/ZKJWTUtils.sol +++ b/contracts/utils/cryptography/ZKJWTUtils.sol @@ -63,7 +63,9 @@ library ZKJWTUtils { ) internal view returns (JWTProofError) { string[] memory signHashTemplate = new string[](1); signHashTemplate[0] = CommandUtils.UINT_MATCHER; - return isValidZKJWT(jwtProof, jwtRegistry, verifier, signHashTemplate, _asSingletonArray(hash), Case.LOWERCASE); // UINT_MATCHER is always lowercase + bytes[] memory signHashParams = new bytes[](1); + signHashParams[0] = abi.encodePacked(hash); + return isValidZKJWT(jwtProof, jwtRegistry, verifier, signHashTemplate, signHashParams, Case.LOWERCASE); // UINT_MATCHER is always lowercase } /** @@ -132,16 +134,4 @@ library ZKJWTUtils { 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), add(array, 0x40)) // Store the offset to the array content - mstore(add(array, 0x40), 32) // Store the length of the element - mstore(add(array, 0x60), element) // Store the single element - mstore(0x40, add(array, 0x80)) // Update memory pointer - } - } } diff --git a/test/utils/cryptography/ZKEmailUtils.t.sol b/test/utils/cryptography/ZKEmailUtils.t.sol index 41ea7168..9a276499 100644 --- a/test/utils/cryptography/ZKEmailUtils.t.sol +++ b/test/utils/cryptography/ZKEmailUtils.t.sol @@ -128,11 +128,7 @@ contract ZKEmailUtilsTest is Test { _mockVerifyEmailProof(emailAuthMsg.proof); // Test validation - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - IVerifier(_verifier) - ); + ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail(emailAuthMsg, _dkimRegistry, _verifier); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.NoError)); } @@ -170,8 +166,8 @@ contract ZKEmailUtilsTest is Test { ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - IVerifier(_verifier), + _dkimRegistry, + _verifier, template ); @@ -191,7 +187,7 @@ contract ZKEmailUtilsTest is Test { commandParams[0] = abi.encode(addr); // Test with different cases - for (uint256 i = 0; i < uint8(type(ZKEmailUtils.Case).max) - 1; i++) { + for (uint256 i = 0; i < uint8(type(ZKEmailUtils.Case).max); i++) { EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock( string.concat(commandPrefix, " ", CommandUtils.addressToHexString(addr, i)), commandParams, @@ -213,8 +209,8 @@ contract ZKEmailUtilsTest is Test { ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - IVerifier(_verifier), + _dkimRegistry, + _verifier, template, ZKEmailUtils.Case(i) ); @@ -222,48 +218,6 @@ contract ZKEmailUtilsTest is Test { } } - function testCommandMatchWithAnyCase( - address addr, - uint256 timestamp, - bytes32 emailNullifier, - bytes32 accountSalt, - bool isCodeExist, - bytes memory proof, - string memory commandPrefix - ) public { - bytes[] memory commandParams = new bytes[](1); - commandParams[0] = abi.encode(addr); - - EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock( - string.concat(commandPrefix, " ", addr.toHexString()), - commandParams, - 0 - ); - - // Override with fuzzed values - emailAuthMsg.proof.timestamp = timestamp; - emailAuthMsg.proof.emailNullifier = emailNullifier; - emailAuthMsg.proof.accountSalt = accountSalt; - emailAuthMsg.proof.isCodeExist = isCodeExist; - emailAuthMsg.proof.proof = proof; - - string[] memory template = new string[](2); - template[0] = commandPrefix; - template[1] = CommandUtils.ETH_ADDR_MATCHER; - - _mockVerifyEmailProof(emailAuthMsg.proof); - - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - IVerifier(_verifier), - template, - ZKEmailUtils.Case.ANY - ); - - assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.NoError)); - } - function testInvalidDKIMPublicKeyHash(bytes32 hash, string memory domainName, bytes32 publicKeyHash) public view { bytes[] memory commandParams = new bytes[](1); commandParams[0] = abi.encode(hash); @@ -277,11 +231,7 @@ contract ZKEmailUtilsTest is Test { emailAuthMsg.proof.domainName = domainName; emailAuthMsg.proof.publicKeyHash = publicKeyHash; - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - IVerifier(_verifier) - ); + ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail(emailAuthMsg, _dkimRegistry, _verifier); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.DKIMPublicKeyHash)); } @@ -294,11 +244,7 @@ contract ZKEmailUtilsTest is Test { EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock(string(new bytes(length)), commandParams, 0); - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - IVerifier(_verifier) - ); + ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail(emailAuthMsg, _dkimRegistry, _verifier); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.MaskedCommandLength)); } @@ -316,29 +262,21 @@ contract ZKEmailUtilsTest is Test { skippedPrefix ); - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - IVerifier(_verifier) - ); + ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail(emailAuthMsg, _dkimRegistry, _verifier); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.SkippedCommandPrefixSize)); } - function testMismatchedCommand(bytes32 hash, string memory invalidCommand) public view { - bytes[] memory commandParams = new bytes[](1); - commandParams[0] = abi.encode(hash); + // function testMismatchedCommand(bytes32 hash, string memory invalidCommand) public view { + // bytes[] memory commandParams = new bytes[](1); + // commandParams[0] = abi.encode(hash); - EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock(invalidCommand, commandParams, 0); + // EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock(invalidCommand, commandParams, 0); - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - IVerifier(_verifier) - ); + // ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail(emailAuthMsg, _dkimRegistry, _verifier); - assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.MismatchedCommand)); - } + // assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.MismatchedCommand)); + // } function testInvalidEmailProof( bytes32 hash, @@ -366,11 +304,7 @@ contract ZKEmailUtilsTest is Test { emailAuthMsg.proof.proof = abi.encode(pA, pB, pC); - ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail( - emailAuthMsg, - IDKIMRegistry(_dkimRegistry), - IVerifier(_verifier) - ); + ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail(emailAuthMsg, _dkimRegistry, _verifier); assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.EmailProof)); } diff --git a/test/utils/cryptography/ZKJWTUtils.t.sol b/test/utils/cryptography/ZKJWTUtils.t.sol new file mode 100644 index 00000000..b62c02f1 --- /dev/null +++ b/test/utils/cryptography/ZKJWTUtils.t.sol @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {ZKJWTUtils} from "../../../contracts/utils/cryptography/ZKJWTUtils.sol"; +import {JwtRegistry} from "@zk-email/zk-jwt/src/utils/JwtRegistry.sol"; +import {JwtGroth16Verifier} from "@zk-email/zk-jwt/src/utils/JwtGroth16Verifier.sol"; +import {JwtVerifier} from "@zk-email/zk-jwt/src/utils/JwtVerifier.sol"; +import {IVerifier, EmailProof} from "@zk-email/zk-jwt/src/interfaces/IVerifier.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {CommandUtils} from "@zk-email/email-tx-builder/src/libraries/CommandUtils.sol"; + +contract ZKJWTUtilsTest is Test { + using Strings for *; + + // Base field size + uint256 constant Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + + JwtRegistry private _jwtRegistry; + IVerifier private _verifier; + bytes32 private _accountSalt; + + string private _kid = "12345"; + string private _iss = "https://example.com"; + string private _azp = "client-id-12345"; + string private _domainName = "12345|https://example.com|client-id-12345"; // kid|iss|azp format + bytes32 private _publicKeyHash = 0x0ea9c777dc7110e5a9e89b13f0cfc540e3845ba120b2b6dc24024d61488d4788; + bytes32 private _emailNullifier = 0x00a83fce3d4b1c9ef0f600644c1ecc6c8115b57b1596e0e3295e2c5105fbfd8a; + bytes private _mockProof; + + string private constant SIGN_HASH_COMMAND = "signHash "; + + function setUp() public { + // Deploy JWT Registry + _jwtRegistry = _createJwtRegistry(); + + // Deploy Verifier + _verifier = _createVerifier(); + + // Generate test data + _accountSalt = keccak256("test@example.com"); + _mockProof = abi.encodePacked(bytes1(0x01)); + } + + function testIsValidZKJWTSignHash( + bytes32 hash, + uint256 timestamp, + bytes32 emailNullifier, + bytes32 accountSalt, + bool isCodeExist, + bytes memory proof + ) public { + // Build JWT proof with fuzzed parameters + EmailProof memory jwtProof = _buildJWTProofMock(string.concat(SIGN_HASH_COMMAND, uint256(hash).toString())); + + // Override with fuzzed values + jwtProof.timestamp = timestamp; + jwtProof.emailNullifier = emailNullifier; + jwtProof.accountSalt = accountSalt; + jwtProof.isCodeExist = isCodeExist; + jwtProof.proof = proof; + + _mockVerifyEmailProof(jwtProof); + + // Test default signHash validation + ZKJWTUtils.JWTProofError err = ZKJWTUtils.isValidZKJWT(jwtProof, _jwtRegistry, _verifier, hash); + + assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.NoError)); + } + + function testIsValidZKJWTWithTemplate( + bytes32 hash, + uint256 timestamp, + bytes32 emailNullifier, + bytes32 accountSalt, + bool isCodeExist, + bytes memory proof + ) public { + // Use a simple, predictable command prefix + string memory commandPrefix = "testCmd"; + + string[] memory template = new string[](1); + template[0] = CommandUtils.UINT_MATCHER; + + bytes[] memory templateParams = new bytes[](1); + templateParams[0] = abi.encode(hash); + + EmailProof memory jwtProof = _buildJWTProofMock(string.concat(commandPrefix, " ", uint256(hash).toString())); + + // Override with fuzzed values + jwtProof.timestamp = timestamp; + jwtProof.emailNullifier = emailNullifier; + jwtProof.accountSalt = accountSalt; + jwtProof.isCodeExist = isCodeExist; + jwtProof.proof = proof; + + _mockVerifyEmailProof(jwtProof); + + ZKJWTUtils.JWTProofError err = ZKJWTUtils.isValidZKJWT( + jwtProof, + _jwtRegistry, + _verifier, + template, + templateParams + ); + + assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.NoError)); + } + + function testCommandMatchWithDifferentCases( + address addr, + uint256 timestamp, + bytes32 emailNullifier, + bytes32 accountSalt, + bool isCodeExist, + bytes memory proof + ) public { + string memory commandPrefix = "authorize"; + + string[] memory template = new string[](1); + template[0] = CommandUtils.ETH_ADDR_MATCHER; + + bytes[] memory templateParams = new bytes[](1); + templateParams[0] = abi.encode(addr); + + // Test with different cases + for (uint256 i = 0; i < uint8(type(ZKJWTUtils.Case).max); i++) { + EmailProof memory jwtProof = _buildJWTProofMock( + string.concat(commandPrefix, " ", CommandUtils.addressToHexString(addr, i)) + ); + + // Override with fuzzed values + jwtProof.timestamp = timestamp; + jwtProof.emailNullifier = emailNullifier; + jwtProof.accountSalt = accountSalt; + jwtProof.isCodeExist = isCodeExist; + jwtProof.proof = proof; + + _mockVerifyEmailProof(jwtProof); + + ZKJWTUtils.JWTProofError err = ZKJWTUtils.isValidZKJWT( + jwtProof, + _jwtRegistry, + _verifier, + template, + templateParams, + ZKJWTUtils.Case(i) + ); + assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.NoError)); + } + } + + function testInvalidJWTPublicKeyHash(bytes32 hash, bytes32 publicKeyHash) public view { + // Ensure we use a different public key hash than the registered one + vm.assume(publicKeyHash != _publicKeyHash); + + EmailProof memory jwtProof = _buildJWTProofMock(string.concat(SIGN_HASH_COMMAND, uint256(hash).toString())); + jwtProof.publicKeyHash = publicKeyHash; + + ZKJWTUtils.JWTProofError err = ZKJWTUtils.isValidZKJWT(jwtProof, _jwtRegistry, _verifier, hash); + + assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.JWTPublicKeyHash)); + } + + function testInvalidMaskedCommandLength(bytes32 hash, uint256 length) public view { + length = bound(length, 606, 1000); // Assuming commandBytes is 605 + + EmailProof memory jwtProof = _buildJWTProofMock(string(new bytes(length))); + + ZKJWTUtils.JWTProofError err = ZKJWTUtils.isValidZKJWT(jwtProof, _jwtRegistry, _verifier, hash); + + assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.MaskedCommandLength)); + } + + // function testMismatchedCommand(bytes32 hash) public view { + // // Use a fixed invalid command that won't match signHash pattern + // string memory invalidCommand = string(abi.encodePacked("invalidJWTCommand ", uint256(hash).toString())); + + // EmailProof memory jwtProof = _buildJWTProofMock(invalidCommand); + + // ZKJWTUtils.JWTProomnfError err = ZKJWTUtils.isValidZKJWT(jwtProof, _jwtRegistry, _verifier, hash); + + // assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.MismatchedCommand)); + // } + + function testMismatchedCommandWithTemplate(bytes32 hash) public view { + string[] memory template = new string[](1); + template[0] = CommandUtils.UINT_MATCHER; + + bytes[] memory templateParams = new bytes[](1); + templateParams[0] = abi.encode(uint256(12345)); // Different value than what's in command + + EmailProof memory jwtProof = _buildJWTProofMock(string(abi.encodePacked("testCmd", " ", hash))); // Different from templateParams + + ZKJWTUtils.JWTProofError err = ZKJWTUtils.isValidZKJWT( + jwtProof, + _jwtRegistry, + _verifier, + template, + templateParams + ); + + assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.MismatchedCommand)); + } + + // function testInvalidJWTProof( + // bytes32 hash, + // uint256[2] memory pA, + // uint256[2][2] memory pB, + // uint256[2] memory pC + // ) public view { + // pA[0] = bound(pA[0], 1, Q - 1); + // pA[1] = bound(pA[1], 1, Q - 1); + // pB[0][0] = bound(pB[0][0], 1, Q - 1); + // pB[0][1] = bound(pB[0][1], 1, Q - 1); + // pB[1][0] = bound(pB[1][0], 1, Q - 1); + // pB[1][1] = bound(pB[1][1], 1, Q - 1); + // pC[0] = bound(pC[0], 1, Q - 1); + // pC[1] = bound(pC[1], 1, Q - 1); + + // EmailProof memory jwtProof = _buildJWTProofMock(string.concat(SIGN_HASH_COMMAND, uint256(hash).toString())); + + // jwtProof.proof = abi.encode(pA, pB, pC); + + // ZKJWTUtils.JWTProofError err = ZKJWTUtils.isValidZKJWT(jwtProof, _jwtRegistry, _verifier, hash); + + // assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.JWTProof)); + // } + + function testComplexJWTCommand( + uint256 amount, + address recipient, + uint256 timestamp, + bytes32 emailNullifier, + bytes32 accountSalt, + bool isCodeExist, + bytes memory proof + ) public { + string[] memory template = new string[](4); + template[0] = CommandUtils.UINT_MATCHER; + template[1] = "ETH"; + template[2] = "to"; + template[3] = CommandUtils.ETH_ADDR_MATCHER; + + bytes[] memory templateParams = new bytes[](2); + templateParams[0] = abi.encode(amount); + templateParams[1] = abi.encode(recipient); + + EmailProof memory jwtProof = _buildJWTProofMock( + string.concat("Send ", amount.toString(), " ETH to ", recipient.toHexString()) + ); + + // Override with fuzzed values + jwtProof.timestamp = timestamp; + jwtProof.emailNullifier = emailNullifier; + jwtProof.accountSalt = accountSalt; + jwtProof.isCodeExist = isCodeExist; + jwtProof.proof = proof; + + _mockVerifyEmailProof(jwtProof); + + ZKJWTUtils.JWTProofError err = ZKJWTUtils.isValidZKJWT( + jwtProof, + _jwtRegistry, + _verifier, + template, + templateParams + ); + + assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.NoError)); + } + + function _createVerifier() private returns (IVerifier) { + JwtVerifier verifier = new JwtVerifier(); + JwtGroth16Verifier groth16Verifier = new JwtGroth16Verifier(); + verifier.initialize(msg.sender, address(groth16Verifier)); + return verifier; + } + + function _createJwtRegistry() private returns (JwtRegistry) { + JwtRegistry jwtRegistry = new JwtRegistry(address(this)); + jwtRegistry.setJwtPublicKey(_domainName, _publicKeyHash); + return jwtRegistry; + } + + function _mockVerifyEmailProof(EmailProof memory jwtProof) private { + vm.mockCall(address(_verifier), abi.encodeCall(IVerifier.verifyEmailProof, (jwtProof)), abi.encode(true)); + } + + function _buildJWTProofMock(string memory command) private view returns (EmailProof memory jwtProof) { + jwtProof = EmailProof({ + domainName: _domainName, + publicKeyHash: _publicKeyHash, + timestamp: block.timestamp, + maskedCommand: command, + emailNullifier: _emailNullifier, + accountSalt: _accountSalt, + isCodeExist: true, + proof: _mockProof + }); + } +} From c21ab750cdd050bf976635b1dd3903b65a4b5d0c Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 26 Jul 2025 09:22:20 -0600 Subject: [PATCH 12/15] up --- test/utils/cryptography/ZKEmailUtils.t.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/utils/cryptography/ZKEmailUtils.t.sol b/test/utils/cryptography/ZKEmailUtils.t.sol index 9a276499..be3b6902 100644 --- a/test/utils/cryptography/ZKEmailUtils.t.sol +++ b/test/utils/cryptography/ZKEmailUtils.t.sol @@ -267,16 +267,16 @@ contract ZKEmailUtilsTest is Test { assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.SkippedCommandPrefixSize)); } - // function testMismatchedCommand(bytes32 hash, string memory invalidCommand) public view { - // bytes[] memory commandParams = new bytes[](1); - // commandParams[0] = abi.encode(hash); + function testMismatchedCommand(bytes32 hash, string memory invalidCommand) public view { + bytes[] memory commandParams = new bytes[](1); + commandParams[0] = abi.encode(hash); - // EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock(invalidCommand, commandParams, 0); + EmailAuthMsg memory emailAuthMsg = _buildEmailAuthMsgMock(invalidCommand, commandParams, 0); - // ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail(emailAuthMsg, _dkimRegistry, _verifier); + ZKEmailUtils.EmailProofError err = ZKEmailUtils.isValidZKEmail(emailAuthMsg, _dkimRegistry, _verifier); - // assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.MismatchedCommand)); - // } + assertEq(uint256(err), uint256(ZKEmailUtils.EmailProofError.MismatchedCommand)); + } function testInvalidEmailProof( bytes32 hash, From 29335c25bc23c0a817588d1d2ccade5ace305131 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 26 Jul 2025 10:24:25 -0600 Subject: [PATCH 13/15] Almost there --- contracts/utils/cryptography/ZKJWTUtils.sol | 2 +- test/utils/cryptography/ZKJWTUtils.t.sol | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/contracts/utils/cryptography/ZKJWTUtils.sol b/contracts/utils/cryptography/ZKJWTUtils.sol index a292d5f8..a5c74ed5 100644 --- a/contracts/utils/cryptography/ZKJWTUtils.sol +++ b/contracts/utils/cryptography/ZKJWTUtils.sol @@ -64,7 +64,7 @@ library ZKJWTUtils { string[] memory signHashTemplate = new string[](1); signHashTemplate[0] = CommandUtils.UINT_MATCHER; bytes[] memory signHashParams = new bytes[](1); - signHashParams[0] = abi.encodePacked(hash); + signHashParams[0] = abi.encode(hash); return isValidZKJWT(jwtProof, jwtRegistry, verifier, signHashTemplate, signHashParams, Case.LOWERCASE); // UINT_MATCHER is always lowercase } diff --git a/test/utils/cryptography/ZKJWTUtils.t.sol b/test/utils/cryptography/ZKJWTUtils.t.sol index b62c02f1..56dcf407 100644 --- a/test/utils/cryptography/ZKJWTUtils.t.sol +++ b/test/utils/cryptography/ZKJWTUtils.t.sol @@ -172,16 +172,15 @@ contract ZKJWTUtilsTest is Test { assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.MaskedCommandLength)); } - // function testMismatchedCommand(bytes32 hash) public view { - // // Use a fixed invalid command that won't match signHash pattern - // string memory invalidCommand = string(abi.encodePacked("invalidJWTCommand ", uint256(hash).toString())); + function testMismatchedCommand(bytes32 hash) public view { + string memory invalidCommand = string(abi.encodePacked("invalidJWTCommand ", hash)); - // EmailProof memory jwtProof = _buildJWTProofMock(invalidCommand); + EmailProof memory jwtProof = _buildJWTProofMock(invalidCommand); - // ZKJWTUtils.JWTProomnfError err = ZKJWTUtils.isValidZKJWT(jwtProof, _jwtRegistry, _verifier, hash); + ZKJWTUtils.JWTProofError err = ZKJWTUtils.isValidZKJWT(jwtProof, _jwtRegistry, _verifier, hash); - // assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.MismatchedCommand)); - // } + assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.MismatchedCommand)); + } function testMismatchedCommandWithTemplate(bytes32 hash) public view { string[] memory template = new string[](1); From 7a809353ddb3504c1d38b2a41ae54412b4c2ae59 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 28 Jul 2025 12:23:52 -0600 Subject: [PATCH 14/15] Fix test (?) --- contracts/utils/cryptography/ZKJWTUtils.sol | 9 +++- test/utils/cryptography/ZKJWTUtils.t.sol | 54 ++++++++++----------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/contracts/utils/cryptography/ZKJWTUtils.sol b/contracts/utils/cryptography/ZKJWTUtils.sol index a5c74ed5..020ab550 100644 --- a/contracts/utils/cryptography/ZKJWTUtils.sol +++ b/contracts/utils/cryptography/ZKJWTUtils.sol @@ -73,7 +73,7 @@ library ZKJWTUtils { * * 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 + * 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. @@ -112,8 +112,13 @@ library ZKJWTUtils { return JWTProofError.JWTPublicKeyHash; } + // TODO: Can we remove the try catch? Or add it to ZKEmailUtils.sol? // Verify the zero-knowledge proof of JWT signature - return verifier.verifyEmailProof(jwtProof) ? JWTProofError.NoError : JWTProofError.JWTProof; + try verifier.verifyEmailProof(jwtProof) returns (bool isValid) { + return isValid ? JWTProofError.NoError : JWTProofError.JWTProof; + } catch { + return JWTProofError.JWTProof; + } } /// @dev Compares the command in the JWT proof with the expected command template. diff --git a/test/utils/cryptography/ZKJWTUtils.t.sol b/test/utils/cryptography/ZKJWTUtils.t.sol index 56dcf407..841ca288 100644 --- a/test/utils/cryptography/ZKJWTUtils.t.sol +++ b/test/utils/cryptography/ZKJWTUtils.t.sol @@ -20,10 +20,10 @@ contract ZKJWTUtilsTest is Test { IVerifier private _verifier; bytes32 private _accountSalt; - string private _kid = "12345"; + string private _kid = "123456"; string private _iss = "https://example.com"; - string private _azp = "client-id-12345"; - string private _domainName = "12345|https://example.com|client-id-12345"; // kid|iss|azp format + string private _azp = "client-id-123456"; + string private _domainName = "123456|https://example.com|client-id-123456"; // kid|iss|azp format bytes32 private _publicKeyHash = 0x0ea9c777dc7110e5a9e89b13f0cfc540e3845ba120b2b6dc24024d61488d4788; bytes32 private _emailNullifier = 0x00a83fce3d4b1c9ef0f600644c1ecc6c8115b57b1596e0e3295e2c5105fbfd8a; bytes private _mockProof; @@ -187,7 +187,7 @@ contract ZKJWTUtilsTest is Test { template[0] = CommandUtils.UINT_MATCHER; bytes[] memory templateParams = new bytes[](1); - templateParams[0] = abi.encode(uint256(12345)); // Different value than what's in command + templateParams[0] = abi.encode(uint256(123456)); // Different value than what's in command EmailProof memory jwtProof = _buildJWTProofMock(string(abi.encodePacked("testCmd", " ", hash))); // Different from templateParams @@ -202,29 +202,29 @@ contract ZKJWTUtilsTest is Test { assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.MismatchedCommand)); } - // function testInvalidJWTProof( - // bytes32 hash, - // uint256[2] memory pA, - // uint256[2][2] memory pB, - // uint256[2] memory pC - // ) public view { - // pA[0] = bound(pA[0], 1, Q - 1); - // pA[1] = bound(pA[1], 1, Q - 1); - // pB[0][0] = bound(pB[0][0], 1, Q - 1); - // pB[0][1] = bound(pB[0][1], 1, Q - 1); - // pB[1][0] = bound(pB[1][0], 1, Q - 1); - // pB[1][1] = bound(pB[1][1], 1, Q - 1); - // pC[0] = bound(pC[0], 1, Q - 1); - // pC[1] = bound(pC[1], 1, Q - 1); - - // EmailProof memory jwtProof = _buildJWTProofMock(string.concat(SIGN_HASH_COMMAND, uint256(hash).toString())); - - // jwtProof.proof = abi.encode(pA, pB, pC); - - // ZKJWTUtils.JWTProofError err = ZKJWTUtils.isValidZKJWT(jwtProof, _jwtRegistry, _verifier, hash); - - // assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.JWTProof)); - // } + function testInvalidJWTProof( + bytes32 hash, + uint256[2] memory pA, + uint256[2][2] memory pB, + uint256[2] memory pC + ) public view { + pA[0] = bound(pA[0], 1, Q - 1); + pA[1] = bound(pA[1], 1, Q - 1); + pB[0][0] = bound(pB[0][0], 1, Q - 1); + pB[0][1] = bound(pB[0][1], 1, Q - 1); + pB[1][0] = bound(pB[1][0], 1, Q - 1); + pB[1][1] = bound(pB[1][1], 1, Q - 1); + pC[0] = bound(pC[0], 1, Q - 1); + pC[1] = bound(pC[1], 1, Q - 1); + + EmailProof memory jwtProof = _buildJWTProofMock(string.concat(SIGN_HASH_COMMAND, uint256(hash).toString())); + + jwtProof.proof = abi.encode(pA, pB, pC); + + ZKJWTUtils.JWTProofError err = ZKJWTUtils.isValidZKJWT(jwtProof, _jwtRegistry, _verifier, hash); + + assertEq(uint256(err), uint256(ZKJWTUtils.JWTProofError.JWTProof)); + } function testComplexJWTCommand( uint256 amount, From afa5a78ffd822e306572614bd8bec9106d682438 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 10 Aug 2025 10:54:18 -0600 Subject: [PATCH 15/15] up --- test/utils/cryptography/ZKEmailUtils.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/utils/cryptography/ZKEmailUtils.test.js b/test/utils/cryptography/ZKEmailUtils.test.js index 2236e230..91a1b1a4 100644 --- a/test/utils/cryptography/ZKEmailUtils.test.js +++ b/test/utils/cryptography/ZKEmailUtils.test.js @@ -106,7 +106,7 @@ describe('ZKEmail', function () { caseType: Case.LOWERCASE, address: this.other.address.toLowerCase(), }, - { caseType: Case.UPPERCASE, address: this.other.address.toUpperCase() }, + { caseType: Case.UPPERCASE, address: this.other.address.toUpperCase().replace('0X', '0x') }, { caseType: Case.CHECKSUM, address: ethers.getAddress(this.other.address) }, ]) { const emailAuthMsg = buildEmailAuthMsg(commandPrefix + ' ' + address, [ethers.zeroPadValue(address, 32)], 0); @@ -123,7 +123,7 @@ describe('ZKEmail', function () { // Test with different cases that should all work with ANY case const addresses = [ this.other.address.toLowerCase(), - this.other.address.toUpperCase(), + this.other.address.toUpperCase().replace('0X', '0x'), ethers.getAddress(this.other.address), ];