diff --git a/packages/circuits/jwt-verifier-test.circom b/packages/circuits/jwt-verifier-test.circom index 3da7359..95aad1b 100644 --- a/packages/circuits/jwt-verifier-test.circom +++ b/packages/circuits/jwt-verifier-test.circom @@ -2,4 +2,4 @@ pragma circom 2.1.6; include "./jwt-verifier.circom"; -component main = JWTVerifier(121, 17, 1024, 128, 896, 14, 605); +component main = JWTVerifier(121, 17, 1024, 128, 896, 72, 605); diff --git a/packages/circuits/package.json b/packages/circuits/package.json index 09a8af8..fc96fe8 100644 --- a/packages/circuits/package.json +++ b/packages/circuits/package.json @@ -6,7 +6,8 @@ "build": "mkdir -p build && circom jwt-verifier-test.circom --r1cs --wasm --sym -l ../../node_modules -o ./build", "publish": "yarn npm publish --access=public", "test": "NODE_OPTIONS=--max_old_space_size=8192 jest --runInBand --detectOpenHandles --forceExit --verbose tests", - "dev-setup": "NODE_OPTIONS=--max_old_space_size=16384 npx ts-node scripts/dev-setup.ts --output ./build" + "dev-setup": "NODE_OPTIONS=--max_old_space_size=16384 npx ts-node scripts/dev-setup.ts --output ./build", + "gen-input": "NODE_OPTIONS=--max_old_space_size=8192 npx ts-node scripts/gen_input.ts" }, "dependencies": { "@zk-email/circuits": "6.1.5-nightly.2024-09-16", @@ -33,6 +34,7 @@ "/helpers", "/lib", "/utils", + "/scripts", "./jwt-verifier.circom" ], "babel": { diff --git a/packages/circuits/scripts/gen-input.ts b/packages/circuits/scripts/gen-input.ts new file mode 100644 index 0000000..e75d6d7 --- /dev/null +++ b/packages/circuits/scripts/gen-input.ts @@ -0,0 +1,181 @@ +// @ts-ignore +import { program } from 'commander'; +import { generateJWT } from '../../helpers/src/jwt'; // JWTを生成する関数のパスを指定 +import { generateJWTVerifierInputs } from '../../helpers/src/input-generators'; // 入力を生成する関数のパスを指定 +import { splitJWT } from "../../helpers/src/utils"; +import fs from "fs"; +const snarkjs = require("snarkjs"); +import { promisify } from "util"; +import path from "path"; +const relayerUtils = require("@zk-email/relayer-utils"); +import https from 'https'; + +program + .requiredOption( + "--input-file ", + "Path of a json file to write the generated input" + ) + .requiredOption('-a, --accountCode ', 'Account code as bigint string') + .option('-h, --header ', 'JWT header as JSON string') + .option('-p, --payload ', 'JWT payload as JSON string') + .option('-m, --maxMessageLength ', 'Maximum message length', '1024') + .option("--silent", "No console logs") + .option("--prove", "Also generate proof"); + +program.parse(process.argv); + +const options = program.opts(); + +function log(...message: any) { + if (!options.silent) { + console.log(...message); + } +} + +async function main() { + const kid = BigInt("0x5aaff47c21d06e266cce395b2145c7c6d4730ea5"); + const issuer = "random.website.com"; + const timestamp = 1694989812; + const azp = "demo-client-id"; + const email = "dummy@gmail.com"; + + const defaultHeader = { + alg: "RS256", + typ: "JWT", + kid: kid.toString(16), + }; + const header = defaultHeader; + const defaultPayload = { + email, + iat: timestamp, + azp, + iss: issuer, + }; + const payload = defaultPayload; + const accountCode = BigInt(options.accountCode); + const maxMessageLength = parseInt(options.maxMessageLength, 1024); + + + const { rawJWT, publicKey } = generateJWT(header, { + ...payload, + nonce: "Send 0.1 ETH to alice@gmail.com", + }); + + const jwtVerifierInputs = await generateJWTVerifierInputs( + rawJWT, + publicKey, + accountCode, + { maxMessageLength } + ); + + console.log('JWT Verifier Inputs:', jwtVerifierInputs); + + const publicKeyHash = relayerUtils.publicKeyHash( + "0x" + Buffer.from(publicKey.n, "base64").toString("hex") + ); + console.log("publicKeyHash"); + console.log(publicKeyHash); + const [, , signature] = splitJWT(rawJWT); + const expectedJwtNullifier = relayerUtils.emailNullifier( + "0x" + Buffer.from(signature, "base64").toString("hex") + ); + console.log("expectedJwtNullifier"); + console.log(expectedJwtNullifier); + + if (!options.inputFile.endsWith(".json")) { + throw new Error("--input file path arg must end with .json"); + } + + // log("Generating Inputs for:", options); + + // const circuitInputs = await genEmailCircuitInput(args.emailFile, args.accountCode, { + // maxHeaderLength: 1024, + // ignoreBodyHashCheck: true + // }); + // log("\n\nGenerated Inputs:", circuitInputs, "\n\n"); + const processedInputs = convertBigIntFieldsToString(jwtVerifierInputs); + + await promisify(fs.writeFile)(options.inputFile, JSON.stringify(processedInputs, null, 2)); + + log("Inputs written to", options.inputFile); + + if (options.prove) { + console.log("generate pub signal"); + const fileContent = fs.readFileSync(options.inputFile as string, 'utf-8'); + const jsonData = JSON.parse(fileContent); + const payload = JSON.stringify({ input: jsonData }); + const urlObject = new URL("https://zkemail--jwt-prover-v0-1-0-flask-app.modal.run/prove/jwt"); + const reqOptions = { + hostname: urlObject.hostname, + path: urlObject.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(payload) + } + }; + await new Promise((resolve, reject) => { + const req = https.request(reqOptions, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', async () => { + try { + const dir = path.dirname(options.inputFile); + const responseJson = JSON.parse(data); + const proof = responseJson.proof; + // console.log(proof); + const publicSignals = responseJson.pub_signals; + + await fs.promises.writeFile( + path.join(dir, "proof.json"), + JSON.stringify(convertBigIntFieldsToString(proof), null, 2) + ); + + await fs.promises.writeFile( + path.join(dir, "public.json"), + JSON.stringify(convertBigIntFieldsToString(publicSignals), null, 2) + ); + console.log('Files written successfully'); + resolve(); + } catch (error) { + console.error('Error processing response:', error); + reject(error); + } + }); + }); + + req.on('error', (error) => { + console.error('Error posting JSON data:', error); + reject(error); + }); + + req.write(payload); + req.end(); + }); + + }; + // Create the request + + process.exit(0); +} + +function convertBigIntFieldsToString(obj: any): any { + if (typeof obj === 'object' && obj !== null) { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [ + key, + typeof value === 'bigint' ? value.toString() : value + ]) + ); + } + return obj; +} + +main().catch((err) => { + console.error("Error generating inputs", err); + process.exit(1); +}); \ No newline at end of file diff --git a/packages/contracts/foundry.toml b/packages/contracts/foundry.toml index e210486..0c70942 100644 --- a/packages/contracts/foundry.toml +++ b/packages/contracts/foundry.toml @@ -4,6 +4,9 @@ out = "artifacts" libs = ["../../node_modules", "lib"] optimizer = true optimizer-runs = 20_000 +fs_permissions = [ + { access = "read", path = "./test/jwt_proofs" }, +] solc = "0.8.26" diff --git a/packages/contracts/test/JwtVerifier/JwtVerifier.t.sol b/packages/contracts/test/JwtVerifier/JwtVerifier.t.sol new file mode 100644 index 0000000..e0dcb7e --- /dev/null +++ b/packages/contracts/test/JwtVerifier/JwtVerifier.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import {EmailProof, JwtVerifier} from "../../src/utils/JwtVerifier.sol"; +import {JwtGroth16Verifier} from "../../src/utils/JwtGroth16Verifier.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract JwtVerifierTest_verifyEmailProof is Test { + JwtVerifier verifier; + + constructor() {} + + function setUp() public { + JwtVerifier verifierImpl = new JwtVerifier(); + JwtGroth16Verifier groth16Verifier = new JwtGroth16Verifier(); + ERC1967Proxy verifierProxy = new ERC1967Proxy( + address(verifierImpl), + abi.encodeCall( + verifierImpl.initialize, + (msg.sender, address(groth16Verifier)) + ) + ); + verifier = JwtVerifier(address(verifierProxy)); + } + + function test_verifyEmailProof() public { + // account code = 16351800486276213158813915254152097017375347006603152442842997572625254103242 + console.log("test_verifyEmailProof"); + EmailProof memory emailProof; + emailProof.domainName = "0x5aaff47c21d06e266cce395b2145c7c6d4730ea5|random.website.com|demo-client-id"; + emailProof.publicKeyHash = bytes32(0x17b17b71ba34d6771b91f2689fddf7266d561d4dcc5d43174d0e100468d89685); + emailProof.timestamp = 1694989812; + emailProof.maskedCommand = "Send 0.1 ETH to alice@gmail.com"; + emailProof.emailNullifier = bytes32(0x24dc5e63ebcbbe243ef41484ec2d97a6ea130387702a9cad6aea2193457d5aec); + emailProof.accountSalt = bytes32(0x2426ca85629574124b746006d8d50f7e7c0e3a0d91a9cdf6c477e314ed14b8ca); + emailProof.isCodeExist = true; + emailProof.proof = proofToBytes( + string.concat( + vm.projectRoot(), + "/test/jwt_proofs/proof.json" + ) + ); + verifier.verifyEmailProof(emailProof); + } + + function proofToBytes( + string memory proofPath + ) internal view returns (bytes memory) { + string memory proofFile = vm.readFile(proofPath); + string[] memory pi_a = abi.decode( + vm.parseJson(proofFile, ".pi_a"), + (string[]) + ); + uint256[2] memory pA = [vm.parseUint(pi_a[0]), vm.parseUint(pi_a[1])]; + string[][] memory pi_b = abi.decode( + vm.parseJson(proofFile, ".pi_b"), + (string[][]) + ); + uint256[2][2] memory pB = [ + [vm.parseUint(pi_b[0][1]), vm.parseUint(pi_b[0][0])], + [vm.parseUint(pi_b[1][1]), vm.parseUint(pi_b[1][0])] + ]; + string[] memory pi_c = abi.decode( + vm.parseJson(proofFile, ".pi_c"), + (string[]) + ); + uint256[2] memory pC = [vm.parseUint(pi_c[0]), vm.parseUint(pi_c[1])]; + bytes memory proof = abi.encode(pA, pB, pC); + return proof; + } + +} \ No newline at end of file