From 33b230341a246ea91864e1ab6a47d8f48ab9c495 Mon Sep 17 00:00:00 2001 From: sid030sid <78762408+sid030sid@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:36:06 +0100 Subject: [PATCH 01/12] fix: created set up for ASCS project goal + implemented group sig proof creation for windows OS --- README.md | 19 +++++- package.json | 4 +- scripts/groupsig/windows_build_groupsig.sh | 72 ++++++++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 scripts/groupsig/windows_build_groupsig.sh diff --git a/README.md b/README.md index d9c2438..9fa5c66 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,22 @@ Implementation of ECDSA operations in circom. +## Set up for ASCS project goal: +- Run `yarn` at the top level to install npm dependencies (`snarkjs` and `circomlib`). + +- Download `circom` version `>= 2.0.2` on your system. Installation instructions [here](https://docs.circom.io/getting-started/installation/). + +- Download ``ptau`` file with power 21 from the Hermez trusted setup from [this repository](https://github.com/iden3/snarkjs#7-prepare-phase-2) and copy it into the `circuits` subdirectory of the project, with the name `pot20_final.ptau`. + +- Build key and wittness by running `yarn build:groupsig` at the top level. This build process will create `r1cs` and `wasm` files for witness generation, as well as a `zkey` file (proving and verifying keys) in a the folder `./build/groupsig`. If no `zkey` file was generated and you are on windows, then: + 1. Install snarkjs gloablly like so: `npm install -g snarkjs` + 2. Instead of `yarn build:groupsig`, run `cd ./scripts/groupsig && ./windows_build_groupsig.sh` in Git Bash terminal + +- Test set up by running groupsig demo through `yarn groupsig-demo` at the top level and follow the instructions in your terminal. [Randomly generated](https://privatekeys.pw/keys/ethereum/random) valid inptuis for demo: + 1. private key: 0x3d87d34a290b124ad0b29b87053363d5dca57cd02650e4b1f4cc75e9c8275648 --> associated eth address: 0x68F3A3AfD9Cbf1cb27b5359b79B563A5E423115a + 2. addr1: 0x0F2D3bF9ce11737566E5bcef7222Df31C0D90395 + 3. addr2: 0x46a8801DA492f6d2eADbd3ec30f4255c29aB656b + ## Project overview This repository provides proof-of-concept implementations of ECDSA operations in circom. **These implementations are for demonstration purposes only**. These circuits are not audited, and this is not intended to be used as a library for production-grade applications. @@ -9,8 +25,7 @@ This repository provides proof-of-concept implementations of ECDSA operations in Circuits can be found in `circuits`. `scripts` contains various utility scripts (most importantly, scripts for building a few example zkSNARKs using the ECDSA circuit primitives). `test` contains some unit tests for the circuits, mostly for witness generation. ## Install dependencies - -- Run `yarn` at the top level to install npm dependencies (`snarkjs` and `circomlib`). +- Run `yarn` at the top level to install npm dependencies (`snarkjs` and `circomlib`). - You'll also need `circom` version `>= 2.0.2` on your system. Installation instructions [here](https://docs.circom.io/getting-started/installation/). - If you want to build the `pubkeygen`, `eth_addr`, and `groupsig` circuits, you'll need to download a Powers of Tau file with `2^20` constraints and copy it into the `circuits` subdirectory of the project, with the name `pot20_final.ptau`. We do not provide such a file in this repo due to its large size. You can download and copy Powers of Tau files from the Hermez trusted setup from [this repository](https://github.com/iden3/snarkjs#7-prepare-phase-2). - If you want to build the `verify` circuits, you'll also need a Powers of Tau file that can support at least `2^21` constraints (place it in the same directory as above with the same naming convention). diff --git a/package.json b/package.json index 3c04cd6..feb591d 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "build:pubkeygen": "cd ./scripts/pubkeygen && ./build_pubkeygen.sh", "build:groupsig": "cd ./scripts/groupsig && ./build_groupsig.sh", + "build-windows:groupsig": "cd ./scripts/groupsig && ./windows_build_groupsig.sh", "build:verify": "cd ./scripts/verify && ./build_verify.sh", "build:eth_addr": "cd ./scripts/eth_addr && ./build_eth_addr.sh", "test": "NODE_OPTIONS=--max_old_space_size=56000 mocha -r ts-node/register 'test/**/*.ts'", @@ -29,5 +30,6 @@ "mocha": "^9.1.3", "ts-node": "^10.4.0", "typescript": "^4.5.4" - } + }, + "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" } diff --git a/scripts/groupsig/windows_build_groupsig.sh b/scripts/groupsig/windows_build_groupsig.sh new file mode 100644 index 0000000..ae70240 --- /dev/null +++ b/scripts/groupsig/windows_build_groupsig.sh @@ -0,0 +1,72 @@ +#!/bin/bash +export MSYS_NO_PATHCONV=1 # To avoid path conversion issues on Windows Git Bash + +PHASE1=../../circuits/pot20_final.ptau +BUILD_DIR=../../build/groupsig +CIRCUIT_NAME=groupsig + +if [ -f "$PHASE1" ]; then + echo "Found Phase 1 ptau file" +else + echo "No Phase 1 ptau file found. Exiting..." + exit 1 +fi + +if [ ! -d "$BUILD_DIR" ]; then + echo "No build directory found. Creating build directory..." + mkdir -p "$BUILD_DIR" +fi + +echo "****COMPILING CIRCUIT****" +start=`date +%s` +circom "$CIRCUIT_NAME".circom --r1cs --wasm --sym --c --wat --output "$BUILD_DIR" +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****GENERATING WITNESS FOR SAMPLE INPUT****" +start=`date +%s` +node "$BUILD_DIR"/"$CIRCUIT_NAME"_js/generate_witness.js "$BUILD_DIR"/"$CIRCUIT_NAME"_js/"$CIRCUIT_NAME".wasm input_groupsig.json "$BUILD_DIR"/witness.wtns +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****GENERATING ZKEY 0****" +start=`date +%s` +snarkjs groth16 setup "$BUILD_DIR"/"$CIRCUIT_NAME".r1cs "$PHASE1" "$BUILD_DIR"/"$CIRCUIT_NAME"_0.zkey +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****CONTRIBUTE TO THE PHASE 2 CEREMONY****" +start=`date +%s` +echo "test" | snarkjs zkey contribute "$BUILD_DIR"/"$CIRCUIT_NAME"_0.zkey "$BUILD_DIR"/"$CIRCUIT_NAME"_1.zkey --name="1st Contributor Name" +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****GENERATING FINAL ZKEY****" +start=`date +%s` +snarkjs zkey beacon "$BUILD_DIR"/"$CIRCUIT_NAME"_1.zkey "$BUILD_DIR"/"$CIRCUIT_NAME".zkey 0102030405060708090a0b0c0d0e0f101112231415161718221a1b1c1d1e1f 10 -n="Final Beacon phase2" +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****VERIFYING FINAL ZKEY****" +start=`date +%s` +snarkjs zkey verify "$BUILD_DIR"/"$CIRCUIT_NAME".r1cs "$PHASE1" "$BUILD_DIR"/"$CIRCUIT_NAME".zkey +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****EXPORTING VKEY****" +start=`date +%s` +snarkjs zkey export verificationkey "$BUILD_DIR"/"$CIRCUIT_NAME".zkey "$BUILD_DIR"/vkey.json +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****GENERATING PROOF FOR SAMPLE INPUT****" +start=`date +%s` +snarkjs groth16 prove "$BUILD_DIR"/"$CIRCUIT_NAME".zkey "$BUILD_DIR"/witness.wtns "$BUILD_DIR"/proof.json "$BUILD_DIR"/public.json +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****VERIFYING PROOF FOR SAMPLE INPUT****" +start=`date +%s` +snarkjs groth16 verify "$BUILD_DIR"/vkey.json "$BUILD_DIR"/public.json "$BUILD_DIR"/proof.json +end=`date +%s` +echo "DONE ($((end-start))s)" From 4a593b48c18509c6bf1013c49231976a51c10185 Mon Sep 17 00:00:00 2001 From: sid030sid <78762408+sid030sid@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:36:34 +0100 Subject: [PATCH 02/12] fix: added nonce logic to circuit --- README.md | 31 +++++++++++- scripts/groupsig/groupsig.circom | 22 +++++++-- scripts/groupsig/input_groupsig.json | 2 +- scripts/groupsign_cli.ts | 70 ++++++++++++++-------------- 4 files changed, 83 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 9fa5c66..aa37f18 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Implementation of ECDSA operations in circom. -## Set up for ASCS project goal: +## Set up for ASCS project goal - Run `yarn` at the top level to install npm dependencies (`snarkjs` and `circomlib`). - Download `circom` version `>= 2.0.2` on your system. Installation instructions [here](https://docs.circom.io/getting-started/installation/). @@ -12,11 +12,40 @@ Implementation of ECDSA operations in circom. - Build key and wittness by running `yarn build:groupsig` at the top level. This build process will create `r1cs` and `wasm` files for witness generation, as well as a `zkey` file (proving and verifying keys) in a the folder `./build/groupsig`. If no `zkey` file was generated and you are on windows, then: 1. Install snarkjs gloablly like so: `npm install -g snarkjs` 2. Instead of `yarn build:groupsig`, run `cd ./scripts/groupsig && ./windows_build_groupsig.sh` in Git Bash terminal + 3. Optional: move back to root folder (required for next step): `cd ../..` - Test set up by running groupsig demo through `yarn groupsig-demo` at the top level and follow the instructions in your terminal. [Randomly generated](https://privatekeys.pw/keys/ethereum/random) valid inptuis for demo: 1. private key: 0x3d87d34a290b124ad0b29b87053363d5dca57cd02650e4b1f4cc75e9c8275648 --> associated eth address: 0x68F3A3AfD9Cbf1cb27b5359b79B563A5E423115a 2. addr1: 0x0F2D3bF9ce11737566E5bcef7222Df31C0D90395 3. addr2: 0x46a8801DA492f6d2eADbd3ec30f4255c29aB656b + 4. nonce: 6789 + +## Requirements for ASCS project goal +1. Enhanced security: + 1. replay attack: solved with nonce logic +2. Variable number of eth addresses as public input of + +## Security analysis +The upper requirements ensure enhanced security but other security matters must be consdiered and are divide in security issues for production and for the potential future of the project: + +### Security issues for production +1. **Trusted Setup** +Circom with Groth16 requires a Phase 2 Trusted Setup. If the trust ceremony is not done correctly, or if the "Power of Tau" file is compromised (as it is the case for this prototype since we download a public ptau file), someone could generate fake proofs (forgeries) without knowing any private key. + +### Potential future security issues +1. **Deterministic Signatures (MiMC Vulnerability)** + - *Risk*: The groupsig circuit uses mimc(msg, privkey). If you ever sign the same msg with the same privkey but a different nonce, you aren't leaking the key, but you are creating a linkable trail. + - *Recommendation*: Ensure your attestation always includes all unique context (like a domain_separator) to ensure signatures cannot be "imported" from one app to another. + - *Note*: In ASCS's use case, we can neglect this security issue (as of now) because the msg always contains the CID of the digital data asset uploaded to IPFS and to be published on the digital data asset marketplace. Therefore, the CID ensures domain seperation! +2. **Forgery via Public Input Manipulation** + - *Risk*: The groupsig circuit proves membership in a list of ETH addresses but an attacker could take your valid proof and simply change the list of ETH addresses to different addresses. If the verifier doesn't check the entire set of addresses against a trusted root (like a Merkle Root), the proof is useless. + - *Recommendation*: Instead of passing a lsit of ETH addresses, pass a Merkle Root as a public input and use a Merkle Proof (private input) to prove your address is in the set. + - *Note*: In ASCS's use case, we can neglect this security issue (as of now) because the verifier is an on-chain smart contract which checks if the ETH addresses are actually part of the same and correct company by considering the did:ethr-based on-chain company structre. +3. **The "Frozen Heart" (Input Aliasing)** + - *Risk*: Circom signals are in a prime field.If msg or nonce are larger than the field size ($p \approx 2^{254}$), they will wrap around (modulo $p$). An attacker could provide a msg that is OriginalMsg + p, and the circuit would produce the exact same attestation. + - *Recommendation*: Always constrain your public inputs to be within a specific bit range (e.g., 253 bits) if they are derived from external data like Ethereum hashes. + - *Note*: TODO: think about if this is an issue for the ASCS's use case? + ## Project overview diff --git a/scripts/groupsig/groupsig.circom b/scripts/groupsig/groupsig.circom index d29eda0..86a2faa 100644 --- a/scripts/groupsig/groupsig.circom +++ b/scripts/groupsig/groupsig.circom @@ -10,6 +10,7 @@ include "../../circuits/eth_addr.circom"; - addr2 (pub) - addr3 (pub) - msg (pub) + - nonce (pub) - privkey Intermediate values: @@ -24,15 +25,25 @@ include "../../circuits/eth_addr.circom"; - msgAttestation == mimc(msg, privkey) */ +// TODO: +// 1. add nonce for safegarding against replac attacks +// 2. make scalable so that instead of addr1, addr2, and addr3 an array is used called: addrs +// 3. fix: No domain separation issue casued by cross-context replay +// 4. check validity of public key + template Main(n, k) { assert(n * k >= 256); assert(n * (k-1) < 256); + // private inputs signal input privkey[k]; + + // public inputs signal input addr1; signal input addr2; signal input addr3; signal input msg; + signal input nonce; // to protect against replay attacks signal myAddr; @@ -50,21 +61,22 @@ template Main(n, k) { for (var i = 0; i < k; i++) { privToAddr.privkey[i] <== privkey[i]; } - myAddr <== privToAddr.addr; + myAddr <== privToAddr.addr; // enforces: "I know a private key whose Ethereum address is myAddr" // verify address is one of the provided signal temp; temp <== (myAddr - addr1) * (myAddr - addr2); - 0 === temp * (myAddr - addr3); + 0 === temp * (myAddr - addr3); // enforces: "I know myAddr is part of group (= addr1-3)" // produce signature - component mimcAttestation = MiMCSponge(k+1, 220, 1); + component mimcAttestation = MiMCSponge(k+2, 220, 1); //+2 bcause of msg and nonce mimcAttestation.ins[0] <== msg; + mimcAttestation.ins[1] <== nonce; // bind the proof to this specific nonce for (var i = 0; i < k; i++) { - mimcAttestation.ins[i+1] <== privkey[i]; + mimcAttestation.ins[i+2] <== privkey[i]; } mimcAttestation.k <== 0; msgAttestation <== mimcAttestation.outs[0]; } -component main {public [addr1, addr2, addr3, msg]} = Main(64, 4); \ No newline at end of file +component main {public [addr1, addr2, addr3, msg, nonce]} = Main(64, 4); \ No newline at end of file diff --git a/scripts/groupsig/input_groupsig.json b/scripts/groupsig/input_groupsig.json index 0b1179e..36fc495 100644 --- a/scripts/groupsig/input_groupsig.json +++ b/scripts/groupsig/input_groupsig.json @@ -1 +1 @@ -{"privkey": ["7", "0", "0", "0"], "addr1": "50", "addr2": "1210930943336347771396396330116102456817544228795", "addr3": "51", "msg": "42"} +{"privkey": ["7", "0", "0", "0"], "addr1": "50", "addr2": "1210930943336347771396396330116102456817544228795", "addr3": "51", "msg": "42", "nonce":"12345"} diff --git a/scripts/groupsign_cli.ts b/scripts/groupsign_cli.ts index 5a0b29c..4a37c06 100644 --- a/scripts/groupsign_cli.ts +++ b/scripts/groupsign_cli.ts @@ -1,6 +1,5 @@ const snarkjs = require('snarkjs'); const readline = require('readline'); -const util = require('util'); const { BigNumber, Wallet } = require('ethers'); const fs = require('fs'); const wc = require('../build/groupsig/groupsig_js/witness_calculator.js'); @@ -11,21 +10,14 @@ const vkey = './build/groupsig/vkey.json'; const wtnsFile = './build/groupsig/witness.wtns'; function isHex(str: string): boolean { - if (str.length % 2 !== 0) return false; - if (str.slice(0, 2) !== '0x') return false; + if (str.startsWith('0x')) str = str.slice(2); const allowedChars = '0123456789abcdefABCDEF'; - for (let i = 2; i < str.length; i++) + for (let i = 0; i < str.length; i++) if (!allowedChars.includes(str[i])) return false; return true; } -function isValidPrivateKey(privkey: string): boolean { - if (privkey.length !== 66) return false; - if (!isHex(privkey)) return false; - return true; -} - function isValidAddr(addr: string): boolean { if (addr.length !== 42) return false; if (!isHex(addr)) return false; @@ -53,23 +45,28 @@ async function generateWitness(inputs: any) { fs.writeFileSync(wtnsFile, buff); } +// --- MAIN LOGIC --- + async function run() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + // 1. Private Key Input const privKeyStr = await new Promise((res) => { rl.question("Enter an ETH private key:\n", (ans: string) => { res(ans); }) - }) + }); const wallet = new Wallet(privKeyStr); console.log(`Your address is: ${wallet.address}`); + // 2. Group Addresses Input const groupAddr1 = await new Promise((res) => { rl.question("Enter address 1 for your group:\n", (ans: string) => { res(ans); }) }); if (!isValidAddr(groupAddr1)) throw new Error('not a valid ETH address'); + const groupAddr2 = await new Promise((res) => { rl.question("Enter address 2 for your group:\n", (ans: string) => { res(ans); @@ -77,6 +74,7 @@ async function run() { }); if (!isValidAddr(groupAddr2)) throw new Error('not a valid ETH address'); + // Randomize position of your address in the group of 3 const idx1 = Math.floor(Math.random() * 3); let idx2 = Math.floor(Math.random() * 2); if (idx2 >= idx1) idx2++; @@ -87,8 +85,15 @@ async function run() { groupAddresses[idx2] = BigInt(groupAddr1); groupAddresses[idx3] = BigInt(groupAddr2); + // 3. Message and Nonce Input const msg = await new Promise((res) => { - rl.question("Enter a message to sign (number between 0 and babyjubjubprime - 1):\n", (ans: string) => { + rl.question("Enter a message to sign:\n", (ans: string) => { + res(ans); + }) + }); + + const nonce = await new Promise((res) => { + rl.question("Enter a nonce (e.g., 0, 1, 2...):\n", (ans: string) => { res(ans); }) }); @@ -98,41 +103,36 @@ async function run() { addr1: groupAddresses[0], addr2: groupAddresses[1], addr3: groupAddresses[2], - msg + msg, + nonce }; - console.log(input); - - // for some reason fullprove is broken currently: https://github.com/iden3/snarkjs/issues/107 + // 4. Witness and Proof Generation console.log('generating witness...'); - const wtnsStart = Date.now(); await generateWitness(input); - console.log(`generated witness. took ${Date.now() - wtnsStart}ms`); - const pfStart = Date.now(); console.log('generating proof...'); const { proof, publicSignals } = await snarkjs.groth16.prove(zkey, wtnsFile); - console.log(proof); - console.log(publicSignals); - console.log(`generated proof. took ${Date.now() - pfStart}ms`); - - const verifyStart = Date.now(); - console.log('verifying proof...'); + // 5. Verification const vkeyJson = JSON.parse(fs.readFileSync(vkey)); - const res = await snarkjs.groth16.verify(vkeyJson, publicSignals, proof); - if (res === true) { - console.log("Verification OK"); - console.log(`verified that one of these addresses signed ${publicSignals[4]}:`); - console.log(BigNumber.from(publicSignals[1]).toHexString()); - console.log(BigNumber.from(publicSignals[2]).toHexString()); - console.log(BigNumber.from(publicSignals[3]).toHexString()); + const verified = await snarkjs.groth16.verify(vkeyJson, publicSignals, proof); + + if (verified === true) { + console.log("-----------------------------------------"); + console.log("βœ… Verification SUCCESSFUL"); + console.log(`Message: ${publicSignals[4]}`); + console.log(`Nonce: ${publicSignals[5]}`); + console.log("Signed by one of these members:"); + console.log(`- ${BigNumber.from(publicSignals[1]).toHexString()}`); + console.log(`- ${BigNumber.from(publicSignals[2]).toHexString()}`); + console.log(`- ${BigNumber.from(publicSignals[3]).toHexString()}`); + console.log("-----------------------------------------"); } else { - console.log("Invalid proof"); + console.log("❌ Invalid proof"); } - console.log(`verification took ${Date.now() - verifyStart}ms`); process.exit(0); } -run(); +run(); \ No newline at end of file From 93d661b17c4680d0737cc31943b0386aab66aeac Mon Sep 17 00:00:00 2001 From: sid030sid <78762408+sid030sid@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:49:20 +0100 Subject: [PATCH 03/12] added more thoughts to security analysis --- README.md | 18 ++++++++++++++++-- scripts/groupsig/groupsig.circom | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aa37f18..f3022c7 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,29 @@ Implementation of ECDSA operations in circom. ## Requirements for ASCS project goal 1. Enhanced security: 1. replay attack: solved with nonce logic -2. Variable number of eth addresses as public input of +2. Variable number of eth addresses as public input of circuit ## Security analysis The upper requirements ensure enhanced security but other security matters must be consdiered and are divide in security issues for production and for the potential future of the project: ### Security issues for production -1. **Trusted Setup** +### Trusted Setup Circom with Groth16 requires a Phase 2 Trusted Setup. If the trust ceremony is not done correctly, or if the "Power of Tau" file is compromised (as it is the case for this prototype since we download a public ptau file), someone could generate fake proofs (forgeries) without knowing any private key. +### Public key validation +The risk of missing public key validation boils down to **Proof Malleability** and **Identity Forgery**. In a production environment, if you don't constrain the public key to the rules of the elliptic curve ( for Ethereum), you leave the door open for mathematically "illegal" inputs that can trick the circuit. + +The Risks are: +1. **Scalar Malleability:** The curve order (n) is slightly smaller than the field size (2^256). If you don't check that , a user could provide . Both might result in the same Ethereum address, allowing a user to generate two different valid proofs for the same "action," potentially bypassing double-spending or replay protections. +2. **Non-existent Points:** Without point validation (y^2 = x^3 + 7), a prover could potentially input a "fake" public key that doesn't exist on the curve but, through a collision in the hashing process (PubKey to Address), matches a target address. +3. **Edge Case Exploits:** Values like `0` or `1` can cause certain elliptic curve library components to behave unexpectedly (e.g., returning a "point at infinity"), which might result in an address that doesn't actually belong to anyone but can be claimed by a malicious prover. + +The potential solutions for production are: +1. Range Constraints (The Scalar Check): You must ensure the private key is within the valid range of the secp256k1 group order. **How:** Use a `LessThan` component or a dedicated `BigLessThan` (since it's 256 bits) to compare the `privkey` against the constant (the curve order). +2. Point-on-Curve Verification: If your `PrivKeyToAddr` component doesn't already do it, you must explicitly check the public key coordinates. **How:** Add a constraint that verifies the coordinates of the public key satisfy the equation . +3. Use Audited Libraries: Don't write the curve math from scratch. For production, integrate tested circuits from established repositories. **Recommendation:** Use the `PrivKeyToAddr` or `VerifyPubkey` templates from the **[circom-ecdsa](https://github.com/0xPARC/circom-ecdsa)** library. These components are specifically designed to handle the 256-bit "BigInt" math and curve constraints required for Ethereum-compatible keys. + + ### Potential future security issues 1. **Deterministic Signatures (MiMC Vulnerability)** - *Risk*: The groupsig circuit uses mimc(msg, privkey). If you ever sign the same msg with the same privkey but a different nonce, you aren't leaking the key, but you are creating a linkable trail. diff --git a/scripts/groupsig/groupsig.circom b/scripts/groupsig/groupsig.circom index 86a2faa..b29d7e4 100644 --- a/scripts/groupsig/groupsig.circom +++ b/scripts/groupsig/groupsig.circom @@ -73,7 +73,7 @@ template Main(n, k) { mimcAttestation.ins[0] <== msg; mimcAttestation.ins[1] <== nonce; // bind the proof to this specific nonce for (var i = 0; i < k; i++) { - mimcAttestation.ins[i+2] <== privkey[i]; + mimcAttestation.ins[i+2] <== privkey[i]; // enforces: "I know that the private key of myAddr signed msg" } mimcAttestation.k <== 0; msgAttestation <== mimcAttestation.outs[0]; From 3880bd9fd7e8b1388788ee03064374ffe899ff24 Mon Sep 17 00:00:00 2001 From: sid030sid <78762408+sid030sid@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:13:13 +0100 Subject: [PATCH 04/12] enabled variable groupsignature verification --- README.md | 12 +- package.json | 3 +- scripts/groupsig/build_circuits.sh | 55 ++++++ .../groupsig/secure_variable_groupsig.circom | 87 ++++++++++ scripts/variable_groupsign_cli.ts | 162 ++++++++++++++++++ 5 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 scripts/groupsig/build_circuits.sh create mode 100644 scripts/groupsig/secure_variable_groupsig.circom create mode 100644 scripts/variable_groupsign_cli.ts diff --git a/README.md b/README.md index f3022c7..17b9302 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,22 @@ Implementation of ECDSA operations in circom. 2. Instead of `yarn build:groupsig`, run `cd ./scripts/groupsig && ./windows_build_groupsig.sh` in Git Bash terminal 3. Optional: move back to root folder (required for next step): `cd ../..` -- Test set up by running groupsig demo through `yarn groupsig-demo` at the top level and follow the instructions in your terminal. [Randomly generated](https://privatekeys.pw/keys/ethereum/random) valid inptuis for demo: +- Test set up by running groupsig demo through `yarn groupsig-demo` at the top level and follow the instructions in your terminal. [Randomly generated](https://privatekeys.pw/keys/ethereum/random) valid inputs for demo: 1. private key: 0x3d87d34a290b124ad0b29b87053363d5dca57cd02650e4b1f4cc75e9c8275648 --> associated eth address: 0x68F3A3AfD9Cbf1cb27b5359b79B563A5E423115a 2. addr1: 0x0F2D3bF9ce11737566E5bcef7222Df31C0D90395 3. addr2: 0x46a8801DA492f6d2eADbd3ec30f4255c29aB656b 4. nonce: 6789 +- For prototype: Build key and wittnesses for variable number of company admins by + 1. setting `SIZES=(2 3 4)` in script: `./scripts/groupsig/build_circuits.sh` (default is 2 to 4) + 2. executing the script by running ``cd ./scripts/groupsig && ./build_circuits.sh`` while root folder of repo +- For prototype demo run `yarn prototype-demo` at the top level and follow the instructions in your terminal. [Randomly generated](https://privatekeys.pw/keys/ethereum/random) valid inputs for demo: + 1. private key: 0x3d87d34a290b124ad0b29b87053363d5dca57cd02650e4b1f4cc75e9c8275648 --> associated eth address: 0x68F3A3AfD9Cbf1cb27b5359b79B563A5E423115a + 2. addr1: 0x0F2D3bF9ce11737566E5bcef7222Df31C0D90395 + 3. addr2: 0x46a8801DA492f6d2eADbd3ec30f4255c29aB656b + 4. addr3: 0xC8a6ab61Cc685586F3399857A4e80a75327fE4C0 + 4. nonce: 6789 + ## Requirements for ASCS project goal 1. Enhanced security: 1. replay attack: solved with nonce logic diff --git a/package.json b/package.json index feb591d..6f983f2 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "build:verify": "cd ./scripts/verify && ./build_verify.sh", "build:eth_addr": "cd ./scripts/eth_addr && ./build_eth_addr.sh", "test": "NODE_OPTIONS=--max_old_space_size=56000 mocha -r ts-node/register 'test/**/*.ts'", - "groupsig-demo": "npx ts-node scripts/groupsign_cli.ts" + "groupsig-demo": "npx ts-node scripts/groupsign_cli.ts", + "prototype-demo": "npx ts-node scripts/variable_groupsign_cli.ts" }, "repository": "git@github.com:0xPARC/circom-ecdsa.git", "author": "0xPARC ", diff --git a/scripts/groupsig/build_circuits.sh b/scripts/groupsig/build_circuits.sh new file mode 100644 index 0000000..16ae103 --- /dev/null +++ b/scripts/groupsig/build_circuits.sh @@ -0,0 +1,55 @@ +#!/bin/bash +set -e # Exit on error + +export MSYS_NO_PATHCONV=1 +PHASE1=../../circuits/pot20_final.ptau +CIRCUIT_NAME=secure_variable_groupsig +BUILD_BASE=../../build/groupsig + +# Check for Phase 1 +if [ ! -f "$PHASE1" ]; then + echo "Error: Phase 1 ptau file not found at $PHASE1" + exit 1 +fi + +# Sizes we want to support +SIZES=(2 3 4) + +for m in "${SIZES[@]}" +do + echo "-------------------------------------------" + echo "πŸ”¨ BUILDING FOR GROUP SIZE: m = $m" + echo "-------------------------------------------" + + # Create a specific directory for this size + TARGET_DIR="$BUILD_BASE/m_$m" + mkdir -p "$TARGET_DIR" + + # 1. Modify the circom file temporarily to set the group size + # This replaces the 'Main(64, 4, X)' line with the current size 'm' + sed -i "s/component main {public \[addrs, msg, nonce\]} = Main(64, 4, [0-9]*);/component main {public [addrs, msg, nonce]} = Main(64, 4, $m);/" "$CIRCUIT_NAME".circom + + echo "**** COMPILING ****" + circom "$CIRCUIT_NAME".circom --r1cs --wasm --output "$TARGET_DIR" + + echo "**** GENERATING ZKEY ****" + # Groth16 Setup + snarkjs groth16 setup "$TARGET_DIR/$CIRCUIT_NAME.r1cs" "$PHASE1" "$TARGET_DIR/temp_0.zkey" + + # Quick Contribution (using 'test' as entropy) + echo "test" | snarkjs zkey contribute "$TARGET_DIR/temp_0.zkey" "$TARGET_DIR/temp_1.zkey" --name="Builder" -v -e="entropy" + + # Final Beacon and ZKey + snarkjs zkey beacon "$TARGET_DIR/temp_1.zkey" "$TARGET_DIR/$CIRCUIT_NAME.zkey" 0102030405060708090a0b0c0d0e0f101112231415161718221a1b1c1d1e1f 10 + + echo "**** EXPORTING VKEY ****" + snarkjs zkey export verificationkey "$TARGET_DIR/$CIRCUIT_NAME.zkey" "$TARGET_DIR/vkey.json" + + # Cleanup intermediate files to save space + rm "$TARGET_DIR/temp_0.zkey" "$TARGET_DIR/temp_1.zkey" "$TARGET_DIR/$CIRCUIT_NAME.r1cs" + + echo "βœ… Done for m=$m" +done + +echo "-------------------------------------------" +echo "All builds complete in $BUILD_BASE" \ No newline at end of file diff --git a/scripts/groupsig/secure_variable_groupsig.circom b/scripts/groupsig/secure_variable_groupsig.circom new file mode 100644 index 0000000..e316359 --- /dev/null +++ b/scripts/groupsig/secure_variable_groupsig.circom @@ -0,0 +1,87 @@ +pragma circom 2.0.2; + +include "../../node_modules/circomlib/circuits/mimcsponge.circom"; +include "../../node_modules/circomlib/circuits/bitify.circom"; +include "../../circuits/eth_addr.circom"; + +/* + Inputs: + - addrs[m] (pub) + - msg (pub) + - nonce (pub) + - privkey + + Intermediate values: + - myAddr (supposed to be addr of privkey) + + Output: + - msgAttestation + + Prove: + - PrivKeyToAddr(privkey) == myAddr + - (myAddr - addrs[0]) * (myAddr - addrs[1]) * ... * (myAddr - addrs[m-1]) == 0 + - msgAttestation == mimc(msg, nonce, privkey) +*/ + +// TODO: +// 1. add nonce for safegarding against replac attacks +// 2. make scalable so that instead of addr1, addr2, and addr3 an array is used called: addrs +// 3. fix: No domain separation issue casued by cross-context replay +// 4. check validity of public key + +// n: bits per word, k: number of words for privkey, m: number of addresses in group +template Main(n, k, m) { + assert(n * k >= 256); + assert(n * (k-1) < 256); + + // private inputs + signal input privkey[k]; + + // public inputs + signal input addrs[m]; // Scalable array of addresses + signal input msg; + signal input nonce; // to protect against replay attacks + + signal myAddr; + + signal output msgAttestation; + + // check that privkey properly represents a 256-bit number + component n2bs[k]; + for (var i = 0; i < k; i++) { + n2bs[i] = Num2Bits(i == k-1 ? 256 - (k-1) * n : n); + n2bs[i].in <== privkey[i]; + } + + // compute addr + component privToAddr = PrivKeyToAddr(n, k); + for (var i = 0; i < k; i++) { + privToAddr.privkey[i] <== privkey[i]; + } + myAddr <== privToAddr.addr; // enforces: "I know a private key whose Ethereum address is myAddr" + + // verify address is one of the provided (SCALABLE MEMBERSHIP CHECK) + // We check: (myAddr - addrs[0]) * (myAddr - addrs[1]) * ... * (myAddr - addrs[m-1]) === 0 + // If myAddr is equal to any one of the addrs, the entire product becomes zero. + signal products[m]; + products[0] <== myAddr - addrs[0]; + for (var i = 1; i < m; i++) { + products[i] <== products[i-1] * (myAddr - addrs[i]); + } + + // The final result must be 0 + 0 === products[m-1]; // enforces: "I know myAddr is part of group (= addrs[m])" + + // produce signature + component mimcAttestation = MiMCSponge(k+2, 220, 1); //+2 bcause of msg and nonce + mimcAttestation.ins[0] <== msg; + mimcAttestation.ins[1] <== nonce; // bind the proof to this specific nonce + for (var i = 0; i < k; i++) { + mimcAttestation.ins[i+2] <== privkey[i]; // enforces: "I know that the private key of myAddr signed msg" + } + mimcAttestation.k <== 0; + msgAttestation <== mimcAttestation.outs[0]; +} + +// Set 'm' (third argument) to the number of addresses you want in your group +component main {public [addrs, msg, nonce]} = Main(64, 4, 4); \ No newline at end of file diff --git a/scripts/variable_groupsign_cli.ts b/scripts/variable_groupsign_cli.ts new file mode 100644 index 0000000..2925548 --- /dev/null +++ b/scripts/variable_groupsign_cli.ts @@ -0,0 +1,162 @@ +const snarkjs = require('snarkjs'); +const readline = require('readline'); +const { BigNumber, Wallet } = require('ethers'); +const fs = require('fs'); +const path = require('path'); + +// --- PATH CONFIGURATION --- +// Use resolve to get absolute paths from the start +const PROJECT_ROOT = path.resolve(__dirname, '..'); +const BUILD_BASE = path.join(PROJECT_ROOT, 'build', 'groupsig'); +const wtnsFile = path.join(BUILD_BASE, 'witness.wtns'); + +// --- HELPER FUNCTIONS --- + +function getAvailableGroupSizes(): number[] { + if (!fs.existsSync(BUILD_BASE)) return []; + return fs.readdirSync(BUILD_BASE) + .filter((name: string) => name.startsWith('m_')) + .map((name: string) => parseInt(name.split('_')[1])) + .sort((a: number, b: number) => a - b); +} + +function isHex(str: string): boolean { + if (str.startsWith('0x')) str = str.slice(2); + const allowedChars = '0123456789abcdefABCDEF'; + for (let i = 0; i < str.length; i++) + if (!allowedChars.includes(str[i])) return false; + return true; +} + +function isValidAddr(addr: string): boolean { + if (addr.length !== 42) return false; + if (!isHex(addr)) return false; + return true; +} + +function toWordArray(x: bigint, nWords: number, bitsPerWord: number): string[] { + const res: string[] = []; + let remaining = x; + const base = 2n ** BigInt(bitsPerWord); + for (let i = 0; i < nWords; i++) { + res.push((remaining % base).toString()); + remaining /= base; + } + return res; +} + +async function generateWitness(inputs: any, wasmPath: string, sizeDir: string) { + const wcPath = path.join(sizeDir, 'secure_variable_groupsig_js', 'witness_calculator.js'); + + console.log(`Using WASM: ${wasmPath}`); + + if (!fs.existsSync(wcPath)) { + throw new Error(`witness_calculator.js not found at ${wcPath}`); + } + + // Dynamic require using absolute path + const builder = require(wcPath); + + const buffer = fs.readFileSync(wasmPath); + const witnessCalculator = await builder(buffer); + const buff = await witnessCalculator.calculateWTNSBin(inputs, 0); + fs.writeFileSync(wtnsFile, buff); +} + +// --- MAIN LOGIC --- + +async function run() { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const availableSizes = getAvailableGroupSizes(); + + if (availableSizes.length === 0) { + console.error(`❌ No compiled circuits found. Searched in: ${BUILD_BASE}`); + process.exit(1); + } + + console.log(`Found compiled circuits for group sizes: ${availableSizes.join(', ')}`); + + // 1. Private Key Input + const privKeyStr = await new Promise((res) => { + rl.question("Enter an ETH private key:\n", (ans: string) => res(ans)); + }); + const wallet = new Wallet(privKeyStr); + console.log(`Your address is: ${wallet.address}`); + + // 2. Select Group Size + const mStr = await new Promise((res) => { + rl.question(`How many total addresses (${availableSizes[0]}-${availableSizes[availableSizes.length-1]})?\n`, (ans: string) => res(ans)); + }); + const m = parseInt(mStr); + + if (!availableSizes.includes(m)) { + console.error(`❌ Circuit for size ${m} not found.`); + process.exit(1); + } + + // Define ABSOLUTE paths for the selected size + const sizeDir = path.join(BUILD_BASE, `m_${m}`); + const wasm = path.join(sizeDir, 'secure_variable_groupsig_js', 'secure_variable_groupsig.wasm'); + const zkey = path.join(sizeDir, 'secure_variable_groupsig.zkey'); + const vkey = path.join(sizeDir, 'vkey.json'); + + // 3. Collect Other Addresses + const otherAddrs: string[] = []; + for (let i = 0; i < m - 1; i++) { + const addr = await new Promise((res) => { + rl.question(`Enter address ${i + 1}/${m - 1} of other members:\n`, (ans: string) => res(ans)); + }); + if (!isValidAddr(addr)) throw new Error('Invalid address'); + otherAddrs.push(addr); + } + + // 4. Group Construction (Shuffle) + const groupAddresses: string[] = otherAddrs.map(a => BigInt(a).toString()); + const myIdx = Math.floor(Math.random() * m); + groupAddresses.splice(myIdx, 0, BigInt(wallet.address).toString()); + + // 5. Message & Nonce + const msg = await new Promise((res) => { + rl.question("Enter message:\n", (ans: string) => res(ans)); + }); + const nonce = await new Promise((res) => { + rl.question("Enter nonce:\n", (ans: string) => res(ans)); + }); + + const input = { + privkey: toWordArray(BigInt(privKeyStr), 4, 64), + addrs: groupAddresses, + msg, + nonce + }; + + // 6. Witness & Proof + console.log('⚑ Generating witness...'); + await generateWitness(input, wasm, sizeDir); + + console.log('⚑ Generating proof...'); + const { proof, publicSignals } = await snarkjs.groth16.prove(zkey, wtnsFile); + + // 7. Verify + const vkeyJson = JSON.parse(fs.readFileSync(vkey)); + const verified = await snarkjs.groth16.verify(vkeyJson, publicSignals, proof); + + if (verified) { + console.log("\n-----------------------------------------"); + console.log("βœ… Verification SUCCESSFUL"); + // Public signals for Main(n, k, m) are: + // [0] msgAttestation, [1...m] addrs, [m+1] msg, [m+2] nonce + console.log(`Message: ${publicSignals[m+1]}`); + console.log(`Nonce: ${publicSignals[m+2]}`); + console.log("Proved membership in group:"); + for (let i = 0; i < m; i++) { + console.log(`- ${BigNumber.from(publicSignals[i+1]).toHexString()}`); + } + console.log("-----------------------------------------"); + } else { + console.log("❌ Invalid proof"); + } + process.exit(0); +} + +run(); \ No newline at end of file From d5a308df24a913c43b379c9730cf5a545a40353f Mon Sep 17 00:00:00 2001 From: sid030sid <78762408+sid030sid@users.noreply.github.com> Date: Sat, 7 Feb 2026 21:33:28 +0100 Subject: [PATCH 05/12] fix: corrected security analysis and updated the TODOs for circuit engineering --- README.md | 26 ++++++++++--------- .../groupsig/secure_variable_groupsig.circom | 6 ++--- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 17b9302..a628b47 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ Implementation of ECDSA operations in circom. 2. addr1: 0x0F2D3bF9ce11737566E5bcef7222Df31C0D90395 3. addr2: 0x46a8801DA492f6d2eADbd3ec30f4255c29aB656b 4. addr3: 0xC8a6ab61Cc685586F3399857A4e80a75327fE4C0 - 4. nonce: 6789 + 6. msg: bafybeicn7i3soqdgr7dwnrwytgq4zxy7a5jpkizrvhm5mv6bgjd32wm3q4 + 6. nonce: 6789 ## Requirements for ASCS project goal 1. Enhanced security: @@ -39,22 +40,23 @@ Implementation of ECDSA operations in circom. The upper requirements ensure enhanced security but other security matters must be consdiered and are divide in security issues for production and for the potential future of the project: ### Security issues for production -### Trusted Setup +1. **Trusted Setup** Circom with Groth16 requires a Phase 2 Trusted Setup. If the trust ceremony is not done correctly, or if the "Power of Tau" file is compromised (as it is the case for this prototype since we download a public ptau file), someone could generate fake proofs (forgeries) without knowing any private key. -### Public key validation -The risk of missing public key validation boils down to **Proof Malleability** and **Identity Forgery**. In a production environment, if you don't constrain the public key to the rules of the elliptic curve ( for Ethereum), you leave the door open for mathematically "illegal" inputs that can trick the circuit. +2. **MiMic vs. ECDSA** +MiMC is used because it is cheap and easy in zero-knowledge circuits and sufficient for demonstrating private-key ownership. However, it does not provide a standard digital signature. For real-world Ethereum applications, MiMC should ideally be replaced with in-circuit ECDSA verification to ensure compatibility, interoperability, and strong cryptographic guarantees. However, in-circuit ECDSA verification comes at the cost of more circuit constraints and thus worse performance. -The Risks are: -1. **Scalar Malleability:** The curve order (n) is slightly smaller than the field size (2^256). If you don't check that , a user could provide . Both might result in the same Ethereum address, allowing a user to generate two different valid proofs for the same "action," potentially bypassing double-spending or replay protections. -2. **Non-existent Points:** Without point validation (y^2 = x^3 + 7), a prover could potentially input a "fake" public key that doesn't exist on the curve but, through a collision in the hashing process (PubKey to Address), matches a target address. -3. **Edge Case Exploits:** Values like `0` or `1` can cause certain elliptic curve library components to behave unexpectedly (e.g., returning a "point at infinity"), which might result in an address that doesn't actually belong to anyone but can be claimed by a malicious prover. +3. **Scalar Malleability** + - *Risk:* The curve order \\( n \\) is slightly smaller than the field size \\( 2^{256} \\). If the circuit does not enforce that the private key lies in the range \\( 1 \\leq d < n \\), a prover can submit values such as \\( d \\) and \\( d + n \\). Both values generate the same public key and Ethereum address. This allows multiple valid witnesses for the same identity, enabling proof malleability, replay attacks, and potential bypass of double-spending protections. + - *Recommendation:* Enforce a strict range check ensuring that the private key satisfies \\( 1 \\leq d < n \\), where \\( n \\) is the secp256k1 curve order. This should be implemented using multi-limb comparison circuits (e.g., `BigLessThan`) and zero-value checks to guarantee a unique canonical representation. -The potential solutions for production are: -1. Range Constraints (The Scalar Check): You must ensure the private key is within the valid range of the secp256k1 group order. **How:** Use a `LessThan` component or a dedicated `BigLessThan` (since it's 256 bits) to compare the `privkey` against the constant (the curve order). -2. Point-on-Curve Verification: If your `PrivKeyToAddr` component doesn't already do it, you must explicitly check the public key coordinates. **How:** Add a constraint that verifies the coordinates of the public key satisfy the equation . -3. Use Audited Libraries: Don't write the curve math from scratch. For production, integrate tested circuits from established repositories. **Recommendation:** Use the `PrivKeyToAddr` or `VerifyPubkey` templates from the **[circom-ecdsa](https://github.com/0xPARC/circom-ecdsa)** library. These components are specifically designed to handle the 256-bit "BigInt" math and curve constraints required for Ethereum-compatible keys. +4. **Non-existent Points** + - *Risk:* Without explicitly verifying that a derived public key lies on the secp256k1 curve (i.e., satisfies \\( y^2 = x^3 + 7 \\)), a prover may use mathematically invalid points. These β€œfake” points can sometimes be mapped through the public-key-to-address hashing process to a valid Ethereum address, enabling identity forgery without possession of a legitimate private key. + - *Recommendation:* Enforce point-on-curve validation by constraining the public key coordinates to satisfy the elliptic curve equation. If public keys are derived internally, ensure that the scalar multiplication circuit includes built-in curve membership checks. Prefer using audited elliptic curve components that guarantee valid point generation. +5. **Edge Case Exploits** + - *Risk:* Special values such as `0`, `1`, or multiples of the curve order may trigger undefined or unintended behavior in elliptic curve arithmetic, including producing the point at infinity or degenerate keys. These edge cases can result in addresses that do not correspond to any real user but can still be claimed by a malicious prover. + - *Recommendation:* Explicitly forbid invalid scalar values by enforcing lower and upper bounds on private keys. Disallow zero and other degenerate values through dedicated zero-check constraints. Ensure that all elliptic curve operations are well-defined for the permitted input range. ### Potential future security issues 1. **Deterministic Signatures (MiMC Vulnerability)** diff --git a/scripts/groupsig/secure_variable_groupsig.circom b/scripts/groupsig/secure_variable_groupsig.circom index e316359..dd686c0 100644 --- a/scripts/groupsig/secure_variable_groupsig.circom +++ b/scripts/groupsig/secure_variable_groupsig.circom @@ -24,10 +24,8 @@ include "../../circuits/eth_addr.circom"; */ // TODO: -// 1. add nonce for safegarding against replac attacks -// 2. make scalable so that instead of addr1, addr2, and addr3 an array is used called: addrs -// 3. fix: No domain separation issue casued by cross-context replay -// 4. check validity of public key +// - Make msg private to avoid public being able to find out its signer by checking the public msg with every eth address in the public addrs[m] input +// - Adapt msg logic for use case and ensure domain separation by that msg is "publish-${cid}-for-${company}-on-${marketplace}" // n: bits per word, k: number of words for privkey, m: number of addresses in group template Main(n, k, m) { From 82c306665927e64b63e186d1ac74e07c5589b7fc Mon Sep 17 00:00:00 2001 From: sid030sid <78762408+sid030sid@users.noreply.github.com> Date: Thu, 12 Feb 2026 08:42:04 +0100 Subject: [PATCH 06/12] docu: added potential optimization strategies --- README.md | 13 +++ .../private_secure_variable_groupsig.circom | 85 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 scripts/groupsig/private_secure_variable_groupsig.circom diff --git a/README.md b/README.md index a628b47..9a333ce 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,9 @@ Implementation of ECDSA operations in circom. ## Requirements for ASCS project goal 1. Enhanced security: 1. replay attack: solved with nonce logic + 2. domain seperation 2. Variable number of eth addresses as public input of circuit +3. Enhanced privacy by making the signature a private input of the circuit ## Security analysis The upper requirements ensure enhanced security but other security matters must be consdiered and are divide in security issues for production and for the potential future of the project: @@ -72,6 +74,17 @@ MiMC is used because it is cheap and easy in zero-knowledge circuits and suffici - *Recommendation*: Always constrain your public inputs to be within a specific bit range (e.g., 253 bits) if they are derived from external data like Ethereum hashes. - *Note*: TODO: think about if this is an issue for the ASCS's use case? +## Potential optimizations +1. **Membership Proof Is O(m)** +The current circuit verifies group membership by computing the product \\((myAddr - addrs[0]) \\times (myAddr - addrs[1]) \\times \\dots \\times (myAddr - addrs[m-1])\\), which requires one multiplication per group member. This means that the proving cost and circuit size grow linearly with the number of addresses in the group. As the group becomes large, this approach quickly becomes impractical due to high constraint counts, slower proof generation, and higher verification costs. A more scalable alternative is to use a Merkle tree or cryptographic accumulator. In this design, all valid addresses are committed into a single root hash, and the prover supplies a Merkle proof showing that their address is included in the tree. This reduces the complexity from O(m) to O(log m), making it feasible to support thousands or millions of members. Merkle-based membership proofs are widely used in modern zero-knowledge systems and are well-supported by existing Circom libraries, making them a practical and secure upgrade. + +2. **Private Key Inside Circuit** +The circuit directly includes the full Ethereum private key as a private witness and uses it both to derive the address and to generate the message attestation. While this is functionally correct, it significantly increases circuit complexity and proof generation time, because elliptic-curve operations and full key handling are expensive in zero-knowledge environments. It also increases the risk surface: if the prover’s environment is compromised, exposure of the witness leaks the actual Ethereum private key, which may control real funds. A better approach is to avoid embedding the long-term private key in the circuit and instead use derived or committed secrets. For example, the user can generate a random secret, commit to it on-chain, and link it to their address through a one-time signature or registration step. The circuit then proves knowledge of this secret rather than the main private key. Another option is to use nullifiers or hierarchical keys, where a dedicated ZK key is derived from the Ethereum key and used only for proofs. These approaches reduce computational cost, improve security isolation, and prevent direct exposure of high-value private keys inside zero-knowledge circuits. + +--- +--- +--- +# README OF CRICOM-ECSDSA ## Project overview diff --git a/scripts/groupsig/private_secure_variable_groupsig.circom b/scripts/groupsig/private_secure_variable_groupsig.circom new file mode 100644 index 0000000..dd686c0 --- /dev/null +++ b/scripts/groupsig/private_secure_variable_groupsig.circom @@ -0,0 +1,85 @@ +pragma circom 2.0.2; + +include "../../node_modules/circomlib/circuits/mimcsponge.circom"; +include "../../node_modules/circomlib/circuits/bitify.circom"; +include "../../circuits/eth_addr.circom"; + +/* + Inputs: + - addrs[m] (pub) + - msg (pub) + - nonce (pub) + - privkey + + Intermediate values: + - myAddr (supposed to be addr of privkey) + + Output: + - msgAttestation + + Prove: + - PrivKeyToAddr(privkey) == myAddr + - (myAddr - addrs[0]) * (myAddr - addrs[1]) * ... * (myAddr - addrs[m-1]) == 0 + - msgAttestation == mimc(msg, nonce, privkey) +*/ + +// TODO: +// - Make msg private to avoid public being able to find out its signer by checking the public msg with every eth address in the public addrs[m] input +// - Adapt msg logic for use case and ensure domain separation by that msg is "publish-${cid}-for-${company}-on-${marketplace}" + +// n: bits per word, k: number of words for privkey, m: number of addresses in group +template Main(n, k, m) { + assert(n * k >= 256); + assert(n * (k-1) < 256); + + // private inputs + signal input privkey[k]; + + // public inputs + signal input addrs[m]; // Scalable array of addresses + signal input msg; + signal input nonce; // to protect against replay attacks + + signal myAddr; + + signal output msgAttestation; + + // check that privkey properly represents a 256-bit number + component n2bs[k]; + for (var i = 0; i < k; i++) { + n2bs[i] = Num2Bits(i == k-1 ? 256 - (k-1) * n : n); + n2bs[i].in <== privkey[i]; + } + + // compute addr + component privToAddr = PrivKeyToAddr(n, k); + for (var i = 0; i < k; i++) { + privToAddr.privkey[i] <== privkey[i]; + } + myAddr <== privToAddr.addr; // enforces: "I know a private key whose Ethereum address is myAddr" + + // verify address is one of the provided (SCALABLE MEMBERSHIP CHECK) + // We check: (myAddr - addrs[0]) * (myAddr - addrs[1]) * ... * (myAddr - addrs[m-1]) === 0 + // If myAddr is equal to any one of the addrs, the entire product becomes zero. + signal products[m]; + products[0] <== myAddr - addrs[0]; + for (var i = 1; i < m; i++) { + products[i] <== products[i-1] * (myAddr - addrs[i]); + } + + // The final result must be 0 + 0 === products[m-1]; // enforces: "I know myAddr is part of group (= addrs[m])" + + // produce signature + component mimcAttestation = MiMCSponge(k+2, 220, 1); //+2 bcause of msg and nonce + mimcAttestation.ins[0] <== msg; + mimcAttestation.ins[1] <== nonce; // bind the proof to this specific nonce + for (var i = 0; i < k; i++) { + mimcAttestation.ins[i+2] <== privkey[i]; // enforces: "I know that the private key of myAddr signed msg" + } + mimcAttestation.k <== 0; + msgAttestation <== mimcAttestation.outs[0]; +} + +// Set 'm' (third argument) to the number of addresses you want in your group +component main {public [addrs, msg, nonce]} = Main(64, 4, 4); \ No newline at end of file From b717c9e786b0717b63889abb459151bf746f1cf3 Mon Sep 17 00:00:00 2001 From: sid030sid <78762408+sid030sid@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:59:26 +0100 Subject: [PATCH 07/12] implemented and tested privsate secure variable groupsignature ZKP --- README.md | 5 + .../build_private_groupsig_circuits.sh | 57 ++++ .../private_secure_variable_groupsig.circom | 154 ++++++++-- .../groupsig/secure_variable_groupsig.circom | 2 +- .../test_private_secure_variable_groupsig.js | 285 ++++++++++++++++++ 5 files changed, 469 insertions(+), 34 deletions(-) create mode 100644 scripts/groupsig/build_private_groupsig_circuits.sh create mode 100644 scripts/test_private_secure_variable_groupsig.js diff --git a/README.md b/README.md index 9a333ce..f99ec37 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,11 @@ Implementation of ECDSA operations in circom. 6. msg: bafybeicn7i3soqdgr7dwnrwytgq4zxy7a5jpkizrvhm5mv6bgjd32wm3q4 6. nonce: 6789 +## Build and test final circuit for ASCS project goal +1. setting `SIZES=(2 3 4)` in script: `./scripts/groupsig/build_private_groupsig_circuits.sh` (default is 2 to 4) +2. executing the script by running ``cd ./scripts/groupsig && ./build_private_groupsig_circuits.sh`` while root folder of repo +3. test by running `node ./scripts/test_private_secure_variable_groupsig.js` while root folder of repo + ## Requirements for ASCS project goal 1. Enhanced security: 1. replay attack: solved with nonce logic diff --git a/scripts/groupsig/build_private_groupsig_circuits.sh b/scripts/groupsig/build_private_groupsig_circuits.sh new file mode 100644 index 0000000..2d6242a --- /dev/null +++ b/scripts/groupsig/build_private_groupsig_circuits.sh @@ -0,0 +1,57 @@ +#!/bin/bash +set -e # Exit on error + +export MSYS_NO_PATHCONV=1 + +# Paths +PHASE1=../../circuits/pot20_final.ptau +CIRCUIT_NAME=private_secure_variable_groupsig +BUILD_BASE=../../build/groupsig + +# Check for Phase 1 +if [ ! -f "$PHASE1" ]; then + echo "Error: Phase 1 ptau file not found at $PHASE1" + exit 1 +fi + +# Group sizes we want to support +SIZES=(3) + +for m in "${SIZES[@]}" +do + echo "-------------------------------------------" + echo "πŸ”¨ BUILDING FOR GROUP SIZE: m = $m" + echo "-------------------------------------------" + + # Create a specific directory for this size + TARGET_DIR="$BUILD_BASE/p_m_$m" + mkdir -p "$TARGET_DIR" + + # 1. Temporarily modify the circom file to set the group size + # Replace the 'Main(64, 4, X)' line with current size + sed -i "s/component main {public \[addrs, pubHashHi, pubHashLo, nonce\]} = Main(64, 4, [0-9]*);/component main {public [addrs, pubHashHi, pubHashLo, nonce]} = Main(64, 4, $m);/" "$CIRCUIT_NAME".circom + + echo "**** COMPILING ****" + circom "$CIRCUIT_NAME".circom --r1cs --wasm --output "$TARGET_DIR" + + echo "**** GENERATING ZKEY ****" + # Groth16 Setup + snarkjs groth16 setup "$TARGET_DIR/$CIRCUIT_NAME.r1cs" "$PHASE1" "$TARGET_DIR/temp_0.zkey" + + # Quick Contribution (using 'test' as entropy) + echo "test" | snarkjs zkey contribute "$TARGET_DIR/temp_0.zkey" "$TARGET_DIR/temp_1.zkey" --name="Builder" -v -e="entropy" + + # Final Beacon and ZKey + snarkjs zkey beacon "$TARGET_DIR/temp_1.zkey" "$TARGET_DIR/$CIRCUIT_NAME.zkey" 0102030405060708090a0b0c0d0e0f101112231415161718221a1b1c1d1e1f 10 + + echo "**** EXPORTING VKEY ****" + snarkjs zkey export verificationkey "$TARGET_DIR/$CIRCUIT_NAME.zkey" "$TARGET_DIR/vkey.json" + + # Cleanup intermediate files to save space + rm "$TARGET_DIR/temp_0.zkey" "$TARGET_DIR/temp_1.zkey" "$TARGET_DIR/$CIRCUIT_NAME.r1cs" + + echo "βœ… Done for m=$m" +done + +echo "-------------------------------------------" +echo "All builds complete in $BUILD_BASE" \ No newline at end of file diff --git a/scripts/groupsig/private_secure_variable_groupsig.circom b/scripts/groupsig/private_secure_variable_groupsig.circom index dd686c0..4d85c52 100644 --- a/scripts/groupsig/private_secure_variable_groupsig.circom +++ b/scripts/groupsig/private_secure_variable_groupsig.circom @@ -7,79 +7,167 @@ include "../../circuits/eth_addr.circom"; /* Inputs: - addrs[m] (pub) - - msg (pub) + - pubHashHi, pubHashLo (pub) // keccak(message) split - nonce (pub) + + - privHashHi, privHashLo (priv) - privkey Intermediate values: - - myAddr (supposed to be addr of privkey) - + - myAddr + Output: - msgAttestation - + Prove: - PrivKeyToAddr(privkey) == myAddr - - (myAddr - addrs[0]) * (myAddr - addrs[1]) * ... * (myAddr - addrs[m-1]) == 0 - - msgAttestation == mimc(msg, nonce, privkey) + - myAddr ∈ addrs + - privHash == pubHash + - msgAttestation == mimc(hash, nonce, privkey) */ -// TODO: -// - Make msg private to avoid public being able to find out its signer by checking the public msg with every eth address in the public addrs[m] input -// - Adapt msg logic for use case and ensure domain separation by that msg is "publish-${cid}-for-${company}-on-${marketplace}" -// n: bits per word, k: number of words for privkey, m: number of addresses in group template Main(n, k, m) { + assert(n * k >= 256); assert(n * (k-1) < 256); - // private inputs + // -------------------------------------------------- + // Private Inputs + // -------------------------------------------------- + signal input privkey[k]; - // public inputs - signal input addrs[m]; // Scalable array of addresses - signal input msg; - signal input nonce; // to protect against replay attacks + // Private keccak hash (split) + signal input privHashHi; + signal input privHashLo; + + + // -------------------------------------------------- + // Public Inputs + // -------------------------------------------------- + + signal input addrs[m]; + + // Public keccak hash (split) + signal input pubHashHi; + signal input pubHashLo; + + signal input nonce; + + + // -------------------------------------------------- + // Internal + // -------------------------------------------------- signal myAddr; signal output msgAttestation; - // check that privkey properly represents a 256-bit number + + // -------------------------------------------------- + // Validate private key size + // -------------------------------------------------- + component n2bs[k]; + for (var i = 0; i < k; i++) { - n2bs[i] = Num2Bits(i == k-1 ? 256 - (k-1) * n : n); + + n2bs[i] = Num2Bits( + i == k-1 ? 256 - (k-1)*n : n + ); + n2bs[i].in <== privkey[i]; } - // compute addr + + // -------------------------------------------------- + // Compute Ethereum address + // -------------------------------------------------- + component privToAddr = PrivKeyToAddr(n, k); + for (var i = 0; i < k; i++) { privToAddr.privkey[i] <== privkey[i]; } - myAddr <== privToAddr.addr; // enforces: "I know a private key whose Ethereum address is myAddr" - // verify address is one of the provided (SCALABLE MEMBERSHIP CHECK) - // We check: (myAddr - addrs[0]) * (myAddr - addrs[1]) * ... * (myAddr - addrs[m-1]) === 0 - // If myAddr is equal to any one of the addrs, the entire product becomes zero. + myAddr <== privToAddr.addr; + + + // -------------------------------------------------- + // Group membership check + // -------------------------------------------------- + signal products[m]; + products[0] <== myAddr - addrs[0]; + for (var i = 1; i < m; i++) { products[i] <== products[i-1] * (myAddr - addrs[i]); } - - // The final result must be 0 - 0 === products[m-1]; // enforces: "I know myAddr is part of group (= addrs[m])" - - // produce signature - component mimcAttestation = MiMCSponge(k+2, 220, 1); //+2 bcause of msg and nonce - mimcAttestation.ins[0] <== msg; - mimcAttestation.ins[1] <== nonce; // bind the proof to this specific nonce + + 0 === products[m-1]; + + + // -------------------------------------------------- + // Enforce hash consistency + // -------------------------------------------------- + + // Proves: + // I know the preimage of pubHash + privHashHi === pubHashHi; + privHashLo === pubHashLo; + + + // -------------------------------------------------- + // Produce ZK signature + // -------------------------------------------------- + + /* + We sign: + + H( + hash_hi, + hash_lo, + nonce, + privkey + ) + */ + + component mimcAttestation = MiMCSponge( + k + 3, // hi, lo, nonce, privkey[] + 220, + 1 + ); + + + // Bind message hash + mimcAttestation.ins[0] <== privHashHi; + mimcAttestation.ins[1] <== privHashLo; + + // Bind nonce + mimcAttestation.ins[2] <== nonce; + + + // Bind private key for (var i = 0; i < k; i++) { - mimcAttestation.ins[i+2] <== privkey[i]; // enforces: "I know that the private key of myAddr signed msg" + mimcAttestation.ins[i+3] <== privkey[i]; } + mimcAttestation.k <== 0; + msgAttestation <== mimcAttestation.outs[0]; } -// Set 'm' (third argument) to the number of addresses you want in your group -component main {public [addrs, msg, nonce]} = Main(64, 4, 4); \ No newline at end of file + + +// -------------------------------------------------- +// Instantiation +// -------------------------------------------------- + +component main {public [ + addrs, + pubHashHi, + pubHashLo, + nonce +]} = Main(64, 4, 4); diff --git a/scripts/groupsig/secure_variable_groupsig.circom b/scripts/groupsig/secure_variable_groupsig.circom index dd686c0..209771d 100644 --- a/scripts/groupsig/secure_variable_groupsig.circom +++ b/scripts/groupsig/secure_variable_groupsig.circom @@ -71,7 +71,7 @@ template Main(n, k, m) { 0 === products[m-1]; // enforces: "I know myAddr is part of group (= addrs[m])" // produce signature - component mimcAttestation = MiMCSponge(k+2, 220, 1); //+2 bcause of msg and nonce + component mimcAttestation = MiMCSponge(k+2, 220, 1); //+2 because of msg and nonce mimcAttestation.ins[0] <== msg; mimcAttestation.ins[1] <== nonce; // bind the proof to this specific nonce for (var i = 0; i < k; i++) { diff --git a/scripts/test_private_secure_variable_groupsig.js b/scripts/test_private_secure_variable_groupsig.js new file mode 100644 index 0000000..844dbe5 --- /dev/null +++ b/scripts/test_private_secure_variable_groupsig.js @@ -0,0 +1,285 @@ +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); +const snarkjs = require("snarkjs"); +const { ethers } = require("ethers"); + +// -------------------------------------------------- +// Utils +// -------------------------------------------------- + +function splitHash(hashHex) { + const hashBigInt = BigInt(hashHex); + const lo = hashBigInt & ((1n << 128n) - 1n); + const hi = hashBigInt >> 128n; + + return { + hi: hi.toString(), + lo: lo.toString() + }; +} + +function generatePrivKey() { + let privKey; + + do { + privKey = BigInt("0x" + crypto.randomBytes(32).toString("hex")); + } while (privKey === 0n || privKey >= ethers.constants.MaxUint256); + + return privKey; +} + +function privKeyToAddress(privKey) { + const wallet = new ethers.Wallet( + privKey.toString(16).padStart(64, "0") + ); + + return BigInt(wallet.address); +} + +function now() { + return process.hrtime.bigint(); +} + +function elapsedMs(start, end) { + return Number(end - start) / 1e6; +} + + +// -------------------------------------------------- +// Main +// -------------------------------------------------- + +async function main() { + + const TOTAL_START = now(); + + console.log("================================="); + console.log(" ZK GroupSig Performance Test"); + console.log("=================================\n"); + + + // -------------------------------------------------- + // Message + // -------------------------------------------------- + + const nonce = 42; + + const cid = "QmTestCid123"; + const company = "0x1234567890123456789012345678901234567890"; + const marketplace = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; + + const msgStr = + `ZK_PUBLISH_V1-${nonce}-${cid}-for-${company}-on-${marketplace}`; + + const msgHash = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes(msgStr) + ); + + const { hi, lo } = splitHash(msgHash); + + + // -------------------------------------------------- + // Private key + // -------------------------------------------------- + + const privKey = generatePrivKey(); + + const privKeyWords = []; + + let pk = privKey; + const mask64 = (1n << 64n) - 1n; + + for (let i = 0; i < 4; i++) { + privKeyWords.push((pk & mask64).toString()); + pk >>= 64n; + } + + + // -------------------------------------------------- + // Group + // -------------------------------------------------- + + const addr = privKeyToAddress(privKey); + + const addrs = [ + addr, + addr + 1n, + addr + 2n, + addr + 3n + ]; + + + // -------------------------------------------------- + // Circuit input + // -------------------------------------------------- + + const input = { + privkey: privKeyWords, + + privHashHi: hi, + privHashLo: lo, + + pubHashHi: hi, + pubHashLo: lo, + + addrs: addrs.map(a => a.toString()), + + nonce: nonce.toString() + }; + + + // -------------------------------------------------- + // Paths + // -------------------------------------------------- + + const BASE = path.join( + __dirname, + "..", + "build", + "groupsig", + "p_m_3" + ); + + const wasmPath = path.join( + BASE, + "private_secure_variable_groupsig_js", + "private_secure_variable_groupsig.wasm" + ); + + const wcPath = path.join( + BASE, + "private_secure_variable_groupsig_js", + "witness_calculator.js" + ); + + const zkeyPath = path.join( + BASE, + "private_secure_variable_groupsig.zkey" + ); + + const vkeyPath = path.join( + BASE, + "vkey.json" + ); + + const wtnsPath = path.join( + BASE, + "bench.wtns" + ); + + + // -------------------------------------------------- + // Witness + // -------------------------------------------------- + + console.log("⚑ Generating witness..."); + + const tWitStart = now(); + + const builder = require(wcPath); + + const buffer = fs.readFileSync(wasmPath); + + const witnessCalculator = await builder(buffer); + + const witness = await witnessCalculator.calculateWTNSBin( + input, + 0 + ); + + fs.writeFileSync(wtnsPath, witness); + + const tWitEnd = now(); + + console.log("βœ… Witness done:", + elapsedMs(tWitStart, tWitEnd).toFixed(2), + "ms" + ); + + + // -------------------------------------------------- + // Proof + // -------------------------------------------------- + + console.log("\n⚑ Generating proof..."); + + const tProofStart = now(); + + const { proof, publicSignals } = + await snarkjs.groth16.prove( + zkeyPath, + wtnsPath + ); + + const tProofEnd = now(); + + console.log("βœ… Proof done:", + elapsedMs(tProofStart, tProofEnd).toFixed(2), + "ms" + ); + + + // -------------------------------------------------- + // Verify + // -------------------------------------------------- + + console.log("\n⚑ Verifying proof..."); + + const tVerStart = now(); + + const vkey = JSON.parse( + fs.readFileSync(vkeyPath) + ); + + const verified = await snarkjs.groth16.verify( + vkey, + publicSignals, + proof + ); + + const tVerEnd = now(); + + console.log("βœ… Verify done:", + elapsedMs(tVerStart, tVerEnd).toFixed(2), + "ms" + ); + + + // -------------------------------------------------- + // Summary + // -------------------------------------------------- + + const TOTAL_END = now(); + + console.log("\n================================="); + console.log(" Benchmark Results"); + console.log("=================================\n"); + + console.log("Witness: ", + elapsedMs(tWitStart, tWitEnd).toFixed(2), "ms"); + + console.log("Proof: ", + elapsedMs(tProofStart, tProofEnd).toFixed(2), "ms"); + + console.log("Verify: ", + elapsedMs(tVerStart, tVerEnd).toFixed(2), "ms"); + + console.log("Total: ", + elapsedMs(TOTAL_START, TOTAL_END).toFixed(2), "ms"); + + + console.log("\nVerified:", verified ? "YES βœ…" : "NO ❌"); + + console.log("\n=================================\n"); + + process.exit(0); +} + + +// -------------------------------------------------- + +main().catch(err => { + console.error("ERROR:", err); + process.exit(1); +}); From eb49f48101985003e386596ab188037f446cdbd4 Mon Sep 17 00:00:00 2001 From: sid030sid <78762408+sid030sid@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:52:35 +0100 Subject: [PATCH 08/12] feature: adapted circuit building process to be interactive, i.e. script asks for number of group members and whether a verifier contract in solidity should be generated --- README.md | 8 +- .../build_private_groupsig_circuits.sh | 171 +++++++++++++++--- 2 files changed, 151 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index f99ec37..e5e3650 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,11 @@ Implementation of ECDSA operations in circom. 6. nonce: 6789 ## Build and test final circuit for ASCS project goal -1. setting `SIZES=(2 3 4)` in script: `./scripts/groupsig/build_private_groupsig_circuits.sh` (default is 2 to 4) -2. executing the script by running ``cd ./scripts/groupsig && ./build_private_groupsig_circuits.sh`` while root folder of repo -3. test by running `node ./scripts/test_private_secure_variable_groupsig.js` while root folder of repo +1. executing the script by running ``cd ./scripts/groupsig && ./build_private_groupsig_circuits.sh`` while root folder of repo +2. test by running `node ./scripts/test_private_secure_variable_groupsig.js` while root folder of repo (note: the test script solely works for three group members which must be considered during the first step) +3. copy the `./build/groupsig/verifiers` folder and add the folder to `./packages/trust-anchor-did-ethr/contracts` of the [on-chain-ssi repo](https://github.com/ASCS-eV/on-chain-ssi/tree/main/packages/trust-anchor-did-ethr/contracts) + +## Produce files for on-chain verification of ## Requirements for ASCS project goal 1. Enhanced security: diff --git a/scripts/groupsig/build_private_groupsig_circuits.sh b/scripts/groupsig/build_private_groupsig_circuits.sh index 2d6242a..cd800af 100644 --- a/scripts/groupsig/build_private_groupsig_circuits.sh +++ b/scripts/groupsig/build_private_groupsig_circuits.sh @@ -1,57 +1,178 @@ #!/bin/bash -set -e # Exit on error +set -e export MSYS_NO_PATHCONV=1 -# Paths + +# -------------------------------- +# Config +# -------------------------------- + PHASE1=../../circuits/pot20_final.ptau CIRCUIT_NAME=private_secure_variable_groupsig BUILD_BASE=../../build/groupsig +VERIFIER_DIR="$BUILD_BASE/verifiers" + + +# -------------------------------- +# Check Phase1 +# -------------------------------- -# Check for Phase 1 if [ ! -f "$PHASE1" ]; then - echo "Error: Phase 1 ptau file not found at $PHASE1" + echo "❌ Phase1 ptau not found: $PHASE1" exit 1 fi -# Group sizes we want to support -SIZES=(3) -for m in "${SIZES[@]}" +# -------------------------------- +# User Input +# -------------------------------- + +read -p "Enter maximum group size (>=2): " MAX_M + +if ! [[ "$MAX_M" =~ ^[0-9]+$ ]] || [ "$MAX_M" -lt 2 ]; then + echo "❌ Invalid group size" + exit 1 +fi + + +read -p "Create Solidity verifiers? (y/n): " MAKE_VERIFIERS + + +if [[ "$MAKE_VERIFIERS" == "y" || "$MAKE_VERIFIERS" == "Y" ]]; then + MAKE_VERIFIERS=true + mkdir -p "$VERIFIER_DIR" +else + MAKE_VERIFIERS=false +fi + + +echo +echo "===================================" +echo " Max group size: $MAX_M" +echo " Generate verifiers: $MAKE_VERIFIERS" +echo "===================================" +echo + + +# -------------------------------- +# Build Loop +# -------------------------------- + +for (( m=2; m<=MAX_M; m++ )) do + echo "-------------------------------------------" echo "πŸ”¨ BUILDING FOR GROUP SIZE: m = $m" echo "-------------------------------------------" - # Create a specific directory for this size + TARGET_DIR="$BUILD_BASE/p_m_$m" + mkdir -p "$TARGET_DIR" - # 1. Temporarily modify the circom file to set the group size - # Replace the 'Main(64, 4, X)' line with current size - sed -i "s/component main {public \[addrs, pubHashHi, pubHashLo, nonce\]} = Main(64, 4, [0-9]*);/component main {public [addrs, pubHashHi, pubHashLo, nonce]} = Main(64, 4, $m);/" "$CIRCUIT_NAME".circom - echo "**** COMPILING ****" - circom "$CIRCUIT_NAME".circom --r1cs --wasm --output "$TARGET_DIR" + # -------------------------------- + # Patch circuit + # -------------------------------- + + echo "βš™οΈ Setting group size..." + + sed -i \ + "s/component main {public \[addrs, pubHashHi, pubHashLo, nonce\]} = Main(64, 4, [0-9]*);/component main {public [addrs, pubHashHi, pubHashLo, nonce]} = Main(64, 4, $m);/" \ + "$CIRCUIT_NAME.circom" + + + # -------------------------------- + # Compile + # -------------------------------- - echo "**** GENERATING ZKEY ****" + echo "βš™οΈ Compiling..." + + circom "$CIRCUIT_NAME.circom" \ + --r1cs \ + --wasm \ + --sym \ + --output "$TARGET_DIR" + + + # -------------------------------- # Groth16 Setup - snarkjs groth16 setup "$TARGET_DIR/$CIRCUIT_NAME.r1cs" "$PHASE1" "$TARGET_DIR/temp_0.zkey" + # -------------------------------- + + echo "βš™οΈ Groth16 setup..." + + snarkjs groth16 setup \ + "$TARGET_DIR/$CIRCUIT_NAME.r1cs" \ + "$PHASE1" \ + "$TARGET_DIR/temp_0.zkey" + + + echo "test" | snarkjs zkey contribute \ + "$TARGET_DIR/temp_0.zkey" \ + "$TARGET_DIR/temp_1.zkey" \ + --name="Builder" \ + -v \ + -e="entropy" + + + snarkjs zkey beacon \ + "$TARGET_DIR/temp_1.zkey" \ + "$TARGET_DIR/$CIRCUIT_NAME.zkey" \ + 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f \ + 10 + + + # -------------------------------- + # Export vkey + # -------------------------------- + + echo "βš™οΈ Exporting verification key..." - # Quick Contribution (using 'test' as entropy) - echo "test" | snarkjs zkey contribute "$TARGET_DIR/temp_0.zkey" "$TARGET_DIR/temp_1.zkey" --name="Builder" -v -e="entropy" + snarkjs zkey export verificationkey \ + "$TARGET_DIR/$CIRCUIT_NAME.zkey" \ + "$TARGET_DIR/vkey.json" - # Final Beacon and ZKey - snarkjs zkey beacon "$TARGET_DIR/temp_1.zkey" "$TARGET_DIR/$CIRCUIT_NAME.zkey" 0102030405060708090a0b0c0d0e0f101112231415161718221a1b1c1d1e1f 10 - echo "**** EXPORTING VKEY ****" - snarkjs zkey export verificationkey "$TARGET_DIR/$CIRCUIT_NAME.zkey" "$TARGET_DIR/vkey.json" + # -------------------------------- + # Export Solidity Verifier + # -------------------------------- + + if [ "$MAKE_VERIFIERS" = true ]; then + + echo "βš™οΈ Exporting Solidity verifier..." + + snarkjs zkey export solidityverifier \ + "$TARGET_DIR/$CIRCUIT_NAME.zkey" \ + "$VERIFIER_DIR/VerifierM$m.sol" \ + --name "VerifierM$m" + + echo " β†’ $VERIFIER_DIR/VerifierM$m.sol" + fi + + + # -------------------------------- + # Cleanup + # -------------------------------- + + rm "$TARGET_DIR/temp_0.zkey" + rm "$TARGET_DIR/temp_1.zkey" + rm "$TARGET_DIR/$CIRCUIT_NAME.r1cs" - # Cleanup intermediate files to save space - rm "$TARGET_DIR/temp_0.zkey" "$TARGET_DIR/temp_1.zkey" "$TARGET_DIR/$CIRCUIT_NAME.r1cs" echo "βœ… Done for m=$m" + echo + done -echo "-------------------------------------------" -echo "All builds complete in $BUILD_BASE" \ No newline at end of file + +echo "===================================" +echo " All builds complete" +echo " Output: $BUILD_BASE" +echo "===================================" + +if [ "$MAKE_VERIFIERS" = true ]; then + echo " Verifiers: $VERIFIER_DIR" +fi + +echo From 719e1c9e9690845625e2a1d17b12cdab43b48407 Mon Sep 17 00:00:00 2001 From: sid030sid <78762408+sid030sid@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:42:27 +0100 Subject: [PATCH 09/12] fix: made readme more clearly for reviewers --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e5e3650..931dfd8 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ Implementation of ECDSA operations in circom. 6. nonce: 6789 ## Build and test final circuit for ASCS project goal +note: the final circuit is in `./scripts/groupsig/prviate_secure_variable_groupsig.circom` + 1. executing the script by running ``cd ./scripts/groupsig && ./build_private_groupsig_circuits.sh`` while root folder of repo 2. test by running `node ./scripts/test_private_secure_variable_groupsig.js` while root folder of repo (note: the test script solely works for three group members which must be considered during the first step) 3. copy the `./build/groupsig/verifiers` folder and add the folder to `./packages/trust-anchor-did-ethr/contracts` of the [on-chain-ssi repo](https://github.com/ASCS-eV/on-chain-ssi/tree/main/packages/trust-anchor-did-ethr/contracts) From 365ee8449d9d89f65ad278fc24c5c634f8d686e8 Mon Sep 17 00:00:00 2001 From: sid030sid <78762408+sid030sid@users.noreply.github.com> Date: Sun, 15 Feb 2026 10:35:26 +0100 Subject: [PATCH 10/12] chore: cleaned readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 931dfd8..874c10f 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,6 @@ note: the final circuit is in `./scripts/groupsig/prviate_secure_variable_groups 2. test by running `node ./scripts/test_private_secure_variable_groupsig.js` while root folder of repo (note: the test script solely works for three group members which must be considered during the first step) 3. copy the `./build/groupsig/verifiers` folder and add the folder to `./packages/trust-anchor-did-ethr/contracts` of the [on-chain-ssi repo](https://github.com/ASCS-eV/on-chain-ssi/tree/main/packages/trust-anchor-did-ethr/contracts) -## Produce files for on-chain verification of - ## Requirements for ASCS project goal 1. Enhanced security: 1. replay attack: solved with nonce logic From 4d4fb4d4641a48b8a709372c69bb0122f807cdff Mon Sep 17 00:00:00 2001 From: sid030sid <78762408+sid030sid@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:13:19 +0100 Subject: [PATCH 11/12] fix: corrected build script, corrected test script, improved documentation --- README.md | 4 +++- scripts/groupsig/build_private_groupsig_circuits.sh | 11 +++++++---- scripts/test_private_secure_variable_groupsig.js | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 874c10f..9429eda 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,14 @@ Implementation of ECDSA operations in circom. 6. nonce: 6789 ## Build and test final circuit for ASCS project goal -note: the final circuit is in `./scripts/groupsig/prviate_secure_variable_groupsig.circom` +**note**: the final circuit is in `./scripts/groupsig/private_secure_variable_groupsig.circom` 1. executing the script by running ``cd ./scripts/groupsig && ./build_private_groupsig_circuits.sh`` while root folder of repo 2. test by running `node ./scripts/test_private_secure_variable_groupsig.js` while root folder of repo (note: the test script solely works for three group members which must be considered during the first step) 3. copy the `./build/groupsig/verifiers` folder and add the folder to `./packages/trust-anchor-did-ethr/contracts` of the [on-chain-ssi repo](https://github.com/ASCS-eV/on-chain-ssi/tree/main/packages/trust-anchor-did-ethr/contracts) +*note*: the final build script was used to generate the ``./build/groupsig/p_m_4`` folder which was added as `circom-zkp-generator` in `./packages/trust-anchor-did-ethr/test` folder of the [on-chain-ssi repo](https://github.com/ASCS-eV/on-chain-ssi/tree/main/packages/trust-anchor-did-ethr/contracts)` + ## Requirements for ASCS project goal 1. Enhanced security: 1. replay attack: solved with nonce logic diff --git a/scripts/groupsig/build_private_groupsig_circuits.sh b/scripts/groupsig/build_private_groupsig_circuits.sh index cd800af..e262b1a 100644 --- a/scripts/groupsig/build_private_groupsig_circuits.sh +++ b/scripts/groupsig/build_private_groupsig_circuits.sh @@ -137,17 +137,20 @@ do # -------------------------------- # Export Solidity Verifier # -------------------------------- - if [ "$MAKE_VERIFIERS" = true ]; then echo "βš™οΈ Exporting Solidity verifier..." + # 1. Export with default name (usually 'Groth16Verifier') snarkjs zkey export solidityverifier \ "$TARGET_DIR/$CIRCUIT_NAME.zkey" \ - "$VERIFIER_DIR/VerifierM$m.sol" \ - --name "VerifierM$m" + "$VERIFIER_DIR/VerifierM$m.sol" + + # 2. Use sed to rename the contract inside the file + # This replaces 'contract Groth16Verifier' with 'contract VerifierMm' + sed -i "s/contract Groth16Verifier/contract VerifierM$m/g" "$VERIFIER_DIR/VerifierM$m.sol" - echo " β†’ $VERIFIER_DIR/VerifierM$m.sol" + echo " β†’ $VERIFIER_DIR/VerifierM$m.sol (renamed to VerifierM$m)" fi diff --git a/scripts/test_private_secure_variable_groupsig.js b/scripts/test_private_secure_variable_groupsig.js index 844dbe5..6e4a852 100644 --- a/scripts/test_private_secure_variable_groupsig.js +++ b/scripts/test_private_secure_variable_groupsig.js @@ -55,7 +55,7 @@ async function main() { const TOTAL_START = now(); console.log("================================="); - console.log(" ZK GroupSig Performance Test"); + console.log(" ZK GroupSig Performance Test (group size = 4)"); console.log("=================================\n"); @@ -138,7 +138,7 @@ async function main() { "..", "build", "groupsig", - "p_m_3" + "p_m_4" ); const wasmPath = path.join( From 7ff8b6550833cd9bb0e021f1d2aa54a49a46e0d3 Mon Sep 17 00:00:00 2001 From: sid030sid <78762408+sid030sid@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:02:33 +0100 Subject: [PATCH 12/12] added potential optimization --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 9429eda..0c643fe 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,9 @@ The current circuit verifies group membership by computing the product \\((myAdd 2. **Private Key Inside Circuit** The circuit directly includes the full Ethereum private key as a private witness and uses it both to derive the address and to generate the message attestation. While this is functionally correct, it significantly increases circuit complexity and proof generation time, because elliptic-curve operations and full key handling are expensive in zero-knowledge environments. It also increases the risk surface: if the prover’s environment is compromised, exposure of the witness leaks the actual Ethereum private key, which may control real funds. A better approach is to avoid embedding the long-term private key in the circuit and instead use derived or committed secrets. For example, the user can generate a random secret, commit to it on-chain, and link it to their address through a one-time signature or registration step. The circuit then proves knowledge of this secret rather than the main private key. Another option is to use nullifiers or hierarchical keys, where a dedicated ZK key is derived from the Ethereum key and used only for proofs. These approaches reduce computational cost, improve security isolation, and prevent direct exposure of high-value private keys inside zero-knowledge circuits. +3. **Improven Variable Group Size ZKP-Generation with Unified Circuit with Dynamic Group Padding** +Currently, the system generates separate WASM and ZKey pairs for every possible group size (`m`). This requires the `DIDMultisigController` to maintain a mapping of verifier contracts and forces the client to download specific proving keys based on the admin count. We propose moving to a **Unified Max-Size Circuit** (e.g., `MAX_M = 100`). Instead of a strict product check, the circuit will be updated to ignore "Null Addresses" (`0x0`). This is achieved by implementing a conditional product factor. The final constraint remains $\\prod factor_i = 0$, but it will only be satisfied if `myAddr` matches one of the **non-zero** entries in the provided array. Benefits are **Single Verifier:** Only one `Verifier.sol` needs to be deployed on-chain, **Simplified Client:** The frontend no longer needs to switch between different WASM/ZKey files, and **Privacy:** A fixed array size of 100 hides the *actual* number of admins in a group, providing better metadata private (e.g., a group of 2 looks identical to a group of 50 on-chain). However, the trade-offs are **Proof Generation Time:** Proving for 100 slots takes longer than proving for 2, and **Gas Cost:** The public input array sent to the `verifyProof` function will always be 100 elements long, increasing calldata gas costs. + --- --- ---