From fbfac43ef2157c4daa4c39d24f2e6b4e5ef83260 Mon Sep 17 00:00:00 2001 From: flourishbar Date: Tue, 23 Jun 2026 04:07:35 +0100 Subject: [PATCH] feat(access-prover): Implement ZK authorization circuit and CLI --- Cargo.lock | 250 +++++++++++++ Cargo.toml | 1 + crates/access-prover/Cargo.toml | 27 ++ crates/access-prover/cli/main.rs | 136 +++++++ .../examples/generate_session.rs | 56 +++ crates/access-prover/src/circuit/mod.rs | 327 +++++++++++++++++ crates/access-prover/src/lib.rs | 3 + crates/access-prover/src/proof.rs | 336 ++++++++++++++++++ crates/access-prover/src/types.rs | 80 +++++ 9 files changed, 1216 insertions(+) create mode 100644 crates/access-prover/Cargo.toml create mode 100644 crates/access-prover/cli/main.rs create mode 100644 crates/access-prover/examples/generate_session.rs create mode 100644 crates/access-prover/src/circuit/mod.rs create mode 100644 crates/access-prover/src/lib.rs create mode 100644 crates/access-prover/src/proof.rs create mode 100644 crates/access-prover/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index b229cdc..05f006a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,29 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "access-prover" +version = "1.0.0" +dependencies = [ + "ark-bn254", + "ark-crypto-primitives", + "ark-ec", + "ark-ff", + "ark-groth16", + "ark-r1cs-std", + "ark-relations", + "ark-serialize", + "ark-snark", + "ark-std", + "clap", + "hex", + "rand", + "rand_chacha", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "ahash" version = "0.8.12" @@ -23,6 +46,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "arbitrary" version = "1.3.2" @@ -55,6 +84,27 @@ dependencies = [ "ark-std", ] +[[package]] +name = "ark-crypto-primitives" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3a13b34da09176a8baba701233fdffbaa7c1b1192ce031a3da4e55ce1f1a56" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-r1cs-std", + "ark-relations", + "ark-serialize", + "ark-snark", + "ark-std", + "blake2", + "derivative", + "digest", + "rayon", + "sha2", + "tracing", +] + [[package]] name = "ark-ec" version = "0.4.2" @@ -69,6 +119,7 @@ dependencies = [ "hashbrown 0.13.2", "itertools", "num-traits", + "rayon", "zeroize", ] @@ -88,6 +139,7 @@ dependencies = [ "num-bigint", "num-traits", "paste", + "rayon", "rustc_version", "zeroize", ] @@ -115,6 +167,22 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-groth16" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ceafa83848c3e390f1cbf124bc3193b3e639b3f02009e0e290809a501b95fc" +dependencies = [ + "ark-crypto-primitives", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-relations", + "ark-serialize", + "ark-std", + "rayon", +] + [[package]] name = "ark-poly" version = "0.4.2" @@ -126,6 +194,36 @@ dependencies = [ "ark-std", "derivative", "hashbrown 0.13.2", + "rayon", +] + +[[package]] +name = "ark-r1cs-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de1d1472e5cb020cb3405ce2567c91c8d43f21b674aef37b0202f5c3304761db" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-relations", + "ark-std", + "derivative", + "num-bigint", + "num-integer", + "num-traits", + "tracing", +] + +[[package]] +name = "ark-relations" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00796b6efc05a3f48225e59cb6a2cda78881e7c390872d5786aaf112f31fb4f0" +dependencies = [ + "ark-ff", + "ark-std", + "tracing", + "tracing-subscriber", ] [[package]] @@ -151,6 +249,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-snark" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d3cc6833a335bb8a600241889ead68ee89a3cf8448081fb7694c0fe503da63" +dependencies = [ + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-std", +] + [[package]] name = "ark-std" version = "0.4.0" @@ -159,6 +269,7 @@ checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", "rand", + "rayon", ] [[package]] @@ -185,6 +296,15 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -266,6 +386,44 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "const-oid" version = "0.9.6" @@ -298,6 +456,31 @@ dependencies = [ "serde_json", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1084,6 +1267,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -1713,6 +1916,47 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "tracing-core", +] + [[package]] name = "typenum" version = "1.20.1" @@ -1725,6 +1969,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index ea98ca0..aed1d8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "contracts/tokens", "contracts/marketplace", + "crates/access-prover", ] [workspace.dependencies] diff --git a/crates/access-prover/Cargo.toml b/crates/access-prover/Cargo.toml new file mode 100644 index 0000000..75274a9 --- /dev/null +++ b/crates/access-prover/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "access-prover" +version = "1.0.0" +edition = "2021" + +[[bin]] +name = "access-prover" +path = "cli/main.rs" + +[dependencies] +ark-ff = "0.4.0" +ark-ec = "0.4.0" +ark-std = "0.4.0" +ark-relations = "0.4.0" +ark-r1cs-std = "0.4.0" +ark-groth16 = "0.4.0" +ark-crypto-primitives = { version = "0.4.0", features = ["r1cs"] } +ark-bn254 = "0.4.0" +rand = "0.8" +rand_chacha = "0.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +clap = { version = "4.0", default-features = false, features = ["derive", "std", "help"] } +thiserror = "1.0" +hex = "0.4" +ark-serialize = "0.4.0" +ark-snark = "0.4.0" diff --git a/crates/access-prover/cli/main.rs b/crates/access-prover/cli/main.rs new file mode 100644 index 0000000..968800c --- /dev/null +++ b/crates/access-prover/cli/main.rs @@ -0,0 +1,136 @@ +use clap::{Parser, Subcommand}; +use access_prover::proof::{run_setup, run_prove, run_verify}; +use access_prover::types::{SessionInput, ProofOutput}; + +#[derive(Parser)] +#[command(name = "access-prover")] +#[command(about = "ZK Anonymous Access Prover CLI", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Generate proving and verification keys + Setup { + #[arg(long, default_value = "proving_key.bin")] + pk: String, + + #[arg(long, default_value = "verification_key.bin")] + vk: String, + + #[arg(long, default_value = "vk_const.rs")] + vk_const: String, + }, + /// Generate a proof of access for a session + Prove { + #[arg(long)] + session: String, + + #[arg(long, default_value = "proving_key.bin")] + pk: String, + + #[arg(long, default_value = "proof.json")] + proof_out: String, + }, + /// Verify a proof of access + Verify { + #[arg(long)] + proof: String, + + #[arg(long, default_value = "verification_key.bin")] + vk: String, + }, +} + +fn main() { + let cli = Cli::parse(); + match cli.command { + Commands::Setup { pk, vk, vk_const } => { + println!("Starting setup..."); + match run_setup(&pk, &vk, Some(&vk_const)) { + Ok(_) => { + println!("Setup completed successfully!"); + println!("Proving key written to: {}", pk); + println!("Verification key written to: {}", vk); + println!("Verification key constant written to: {}", vk_const); + } + Err(e) => { + eprintln!("Error during setup: {}", e); + std::process::exit(1); + } + } + } + Commands::Prove { session, pk, proof_out } => { + println!("Generating proof for session: {}", session); + + // Read session JSON + let session_data = match std::fs::read_to_string(&session) { + Ok(data) => data, + Err(e) => { + eprintln!("Failed to read session file: {}", e); + std::process::exit(1); + } + }; + + let session_input: SessionInput = match serde_json::from_str(&session_data) { + Ok(input) => input, + Err(e) => { + eprintln!("Failed to parse session JSON: {}", e); + std::process::exit(1); + } + }; + + match run_prove(&pk, &session_input) { + Ok(proof_output) => { + let serialized = serde_json::to_string_pretty(&proof_output).unwrap(); + if let Err(e) = std::fs::write(&proof_out, serialized) { + eprintln!("Failed to write proof output: {}", e); + std::process::exit(1); + } + println!("Proof generated and written to: {}", proof_out); + } + Err(e) => { + eprintln!("Error generating proof: {}", e); + std::process::exit(1); + } + } + } + Commands::Verify { proof, vk } => { + println!("Verifying proof: {}", proof); + + // Read proof JSON + let proof_data = match std::fs::read_to_string(&proof) { + Ok(data) => data, + Err(e) => { + eprintln!("Failed to read proof file: {}", e); + std::process::exit(1); + } + }; + + let proof_output: ProofOutput = match serde_json::from_str(&proof_data) { + Ok(out) => out, + Err(e) => { + eprintln!("Failed to parse proof JSON: {}", e); + std::process::exit(1); + } + }; + + match run_verify(&vk, &proof_output) { + Ok(is_valid) => { + if is_valid { + println!("Verification RESULT: SUCCESS"); + } else { + println!("Verification RESULT: FAILURE (Invalid Proof)"); + std::process::exit(1); + } + } + Err(e) => { + eprintln!("Error during verification: {}", e); + std::process::exit(1); + } + } + } + } +} diff --git a/crates/access-prover/examples/generate_session.rs b/crates/access-prover/examples/generate_session.rs new file mode 100644 index 0000000..31ae62e --- /dev/null +++ b/crates/access-prover/examples/generate_session.rs @@ -0,0 +1,56 @@ +use ark_ff::UniformRand; +use ark_bn254::Fr; +use rand::thread_rng; +use access_prover::circuit::{get_poseidon_config, poseidon_hash_native}; +use access_prover::proof::fr_to_hex; +use access_prover::types::{SessionInput, MerklePathJson}; + +fn main() { + let config = get_poseidon_config::(3); + let mut rng = thread_rng(); + + // Generate some test keys and secrets + let user_secret = Fr::from(123456789u64); + let authorization_note = poseidon_hash_native(&[user_secret], &config); + + // Construct a valid Merkle path of depth 16 + let leaf_index = 7u32; + let mut siblings = Vec::new(); + let mut indices = Vec::new(); + let mut current = authorization_note; + + for i in 0..16 { + let sibling = Fr::rand(&mut rng); + let is_right = ((leaf_index >> i) & 1) == 1; + + siblings.push(sibling); + indices.push(is_right); + + current = if is_right { + poseidon_hash_native(&[sibling, current], &config) + } else { + poseidon_hash_native(&[current, sibling], &config) + }; + } + let policy_root = current; + + // Nonce and hash representing request session + let session_nonce = Fr::from(987654321u64); + let ciphertext_hash = Fr::from(555555555u64); + + let session_input = SessionInput { + policy_root: fr_to_hex(&policy_root), + session_nonce: fr_to_hex(&session_nonce), + ciphertext_hash: fr_to_hex(&ciphertext_hash), + user_secret: fr_to_hex(&user_secret), + authorization_note: fr_to_hex(&authorization_note), + merkle_path: MerklePathJson { + siblings: siblings.iter().map(fr_to_hex).collect(), + indices, + }, + }; + + let serialized = serde_json::to_string_pretty(&session_input).unwrap(); + std::fs::write("session.json", serialized).expect("failed to write session.json"); + println!("Generated session.json successfully!"); +} diff --git a/crates/access-prover/src/circuit/mod.rs b/crates/access-prover/src/circuit/mod.rs new file mode 100644 index 0000000..31be934 --- /dev/null +++ b/crates/access-prover/src/circuit/mod.rs @@ -0,0 +1,327 @@ +use ark_ff::PrimeField; +use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; +use ark_r1cs_std::prelude::*; +use ark_r1cs_std::fields::fp::FpVar; +use ark_r1cs_std::alloc::AllocVar; +use serde::{Deserialize, Serialize}; +use rand::SeedableRng; +use rand_chacha::ChaCha8Rng; + +// Standard Poseidon configuration +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PoseidonConfig { + pub full_rounds: usize, + pub partial_rounds: usize, + pub alpha: u64, + pub mds: Vec>, + pub ark: Vec>, +} + +// Deterministically generate Poseidon parameters for a given state width +pub fn get_poseidon_config(width: usize) -> PoseidonConfig { + let mut rng = ChaCha8Rng::seed_from_u64(1337); // fixed seed for reproducibility + let full_rounds = 8; + let partial_rounds = if width <= 3 { 56 } else { 60 }; + let alpha = 5; + + // Generate ARK (round constants) + let mut ark = Vec::new(); + for _ in 0..(full_rounds + partial_rounds) { + let mut round_constants = Vec::new(); + for _ in 0..width { + round_constants.push(F::rand(&mut rng)); + } + ark.push(round_constants); + } + + // Generate MDS matrix using Cauchy matrix construction: MDS[i][j] = 1 / (x_i + y_j) + let mut x = Vec::new(); + let mut y = Vec::new(); + while x.len() < width { + let val = F::rand(&mut rng); + if !x.contains(&val) { + x.push(val); + } + } + while y.len() < width { + let val = F::rand(&mut rng); + if !x.contains(&val) && !y.contains(&val) { + // Check that it won't cause x_i + y_j = 0 for any existing x_i + let mut ok = true; + for &xi in &x { + if xi + val == F::zero() { + ok = false; + break; + } + } + if ok { + y.push(val); + } + } + } + + let mut mds = vec![vec![F::zero(); width]; width]; + for i in 0..width { + for j in 0..width { + mds[i][j] = (x[i] + y[j]).inverse().expect("failed to invert Cauchy matrix element"); + } + } + + PoseidonConfig { + full_rounds, + partial_rounds, + alpha, + mds, + ark, + } +} + +// Native Poseidon permutation +fn poseidon_permute_native( + state: &mut [F], + config: &PoseidonConfig, +) { + let t = state.len(); + let mut round_idx = 0; + + // 1. Full rounds (first RF/2 rounds) + for _ in 0..(config.full_rounds / 2) { + for i in 0..t { + state[i] += config.ark[round_idx][i]; + } + for i in 0..t { + let x2 = state[i].square(); + let x4 = x2.square(); + state[i] *= x4; + } + let mut new_state = vec![F::zero(); t]; + for i in 0..t { + for j in 0..t { + new_state[i] += state[j] * config.mds[i][j]; + } + } + state.copy_from_slice(&new_state); + round_idx += 1; + } + + // 2. Partial rounds (RP rounds) + for _ in 0..config.partial_rounds { + for i in 0..t { + state[i] += config.ark[round_idx][i]; + } + let x2 = state[0].square(); + let x4 = x2.square(); + state[0] *= x4; + + let mut new_state = vec![F::zero(); t]; + for i in 0..t { + for j in 0..t { + new_state[i] += state[j] * config.mds[i][j]; + } + } + state.copy_from_slice(&new_state); + round_idx += 1; + } + + // 3. Full rounds (last RF/2 rounds) + for _ in 0..(config.full_rounds / 2) { + for i in 0..t { + state[i] += config.ark[round_idx][i]; + } + for i in 0..t { + let x2 = state[i].square(); + let x4 = x2.square(); + state[i] *= x4; + } + let mut new_state = vec![F::zero(); t]; + for i in 0..t { + for j in 0..t { + new_state[i] += state[j] * config.mds[i][j]; + } + } + state.copy_from_slice(&new_state); + round_idx += 1; + } +} + +// Native Poseidon hash +pub fn poseidon_hash_native( + inputs: &[F], + config: &PoseidonConfig, +) -> F { + let t = 3; + let mut state = vec![F::zero(); t]; + for i in 0..inputs.len() { + if i + 1 < t { + state[i + 1] = inputs[i]; + } + } + poseidon_permute_native(&mut state, config); + state[1] +} + +// Circuit Poseidon permutation +fn poseidon_permute_circuit( + state: &mut [FpVar], + config: &PoseidonConfig, +) -> Result<(), SynthesisError> { + let t = state.len(); + let mut round_idx = 0; + + // 1. Full rounds (first RF/2 rounds) + for _ in 0..(config.full_rounds / 2) { + for i in 0..t { + state[i] = &state[i] + &FpVar::Constant(config.ark[round_idx][i]); + } + for i in 0..t { + let x2 = state[i].square()?; + let x4 = x2.square()?; + state[i] = &state[i] * &x4; + } + let mut new_state = vec![FpVar::zero(); t]; + for i in 0..t { + for j in 0..t { + new_state[i] = &new_state[i] + &(&state[j] * &FpVar::Constant(config.mds[i][j])); + } + } + state.clone_from_slice(&new_state); + round_idx += 1; + } + + // 2. Partial rounds (RP rounds) + for _ in 0..config.partial_rounds { + for i in 0..t { + state[i] = &state[i] + &FpVar::Constant(config.ark[round_idx][i]); + } + let x2 = state[0].square()?; + let x4 = x2.square()?; + state[0] = &state[0] * &x4; + + let mut new_state = vec![FpVar::zero(); t]; + for i in 0..t { + for j in 0..t { + new_state[i] = &new_state[i] + &(&state[j] * &FpVar::Constant(config.mds[i][j])); + } + } + state.clone_from_slice(&new_state); + round_idx += 1; + } + + // 3. Full rounds (last RF/2 rounds) + for _ in 0..(config.full_rounds / 2) { + for i in 0..t { + state[i] = &state[i] + &FpVar::Constant(config.ark[round_idx][i]); + } + for i in 0..t { + let x2 = state[i].square()?; + let x4 = x2.square()?; + state[i] = &state[i] * &x4; + } + let mut new_state = vec![FpVar::zero(); t]; + for i in 0..t { + for j in 0..t { + new_state[i] = &new_state[i] + &(&state[j] * &FpVar::Constant(config.mds[i][j])); + } + } + state.clone_from_slice(&new_state); + round_idx += 1; + } + + Ok(()) +} + +// Circuit Poseidon hash +pub fn poseidon_hash_circuit( + inputs: &[FpVar], + config: &PoseidonConfig, +) -> Result, SynthesisError> { + let t = 3; + let mut state = vec![FpVar::Constant(F::zero()); t]; + for i in 0..inputs.len() { + if i + 1 < t { + state[i + 1] = inputs[i].clone(); + } + } + poseidon_permute_circuit(&mut state, config)?; + Ok(state[1].clone()) +} + +#[derive(Clone, Debug)] +pub struct AccessCircuit { + // Public inputs + pub policy_root: Option, + pub session_nonce: Option, + pub ciphertext_hash: Option, + + // Private inputs + pub user_secret: Option, + pub authorization_note: Option, + pub merkle_siblings: Vec>, + pub merkle_indices: Vec>, +} + +impl ConstraintSynthesizer for AccessCircuit { + fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { + let config = get_poseidon_config::(3); + + // 1. Allocate public inputs + let policy_root_var = FpVar::new_input(cs.clone(), || { + self.policy_root.ok_or(SynthesisError::AssignmentMissing) + })?; + let session_nonce_var = FpVar::new_input(cs.clone(), || { + self.session_nonce.ok_or(SynthesisError::AssignmentMissing) + })?; + let ciphertext_hash_var = FpVar::new_input(cs.clone(), || { + self.ciphertext_hash.ok_or(SynthesisError::AssignmentMissing) + })?; + + // 2. Allocate private inputs + let user_secret_var = FpVar::new_witness(cs.clone(), || { + self.user_secret.ok_or(SynthesisError::AssignmentMissing) + })?; + let authorization_note_var = FpVar::new_witness(cs.clone(), || { + self.authorization_note.ok_or(SynthesisError::AssignmentMissing) + })?; + + let mut sibling_vars = Vec::new(); + for sibling in &self.merkle_siblings { + let var = FpVar::new_witness(cs.clone(), || { + sibling.ok_or(SynthesisError::AssignmentMissing) + })?; + sibling_vars.push(var); + } + + let mut index_vars = Vec::new(); + for index in &self.merkle_indices { + let var = Boolean::new_witness(cs.clone(), || { + index.ok_or(SynthesisError::AssignmentMissing) + })?; + index_vars.push(var); + } + + // 3. Verify: authorization_note = Poseidon(user_secret) + let computed_note = poseidon_hash_circuit(&[user_secret_var.clone()], &config)?; + computed_note.enforce_equal(&authorization_note_var)?; + + // 4. Verify Merkle path to policy_root + let mut current_hash = authorization_note_var; + for i in 0..sibling_vars.len() { + let sibling = &sibling_vars[i]; + let is_right = &index_vars[i]; + + // If index is true (right), then sibling is on the right, current_hash on the left + // Else sibling is on the left, current_hash on the right + let left = is_right.select(sibling, ¤t_hash)?; + let right = is_right.select(¤t_hash, sibling)?; + + current_hash = poseidon_hash_circuit(&[left, right], &config)?; + } + current_hash.enforce_equal(&policy_root_var)?; + + // 5. Bind session_nonce and ciphertext_hash with user_secret + let binding1 = poseidon_hash_circuit(&[user_secret_var, session_nonce_var], &config)?; + let _binding2 = poseidon_hash_circuit(&[binding1, ciphertext_hash_var], &config)?; + + Ok(()) + } +} diff --git a/crates/access-prover/src/lib.rs b/crates/access-prover/src/lib.rs new file mode 100644 index 0000000..d59d729 --- /dev/null +++ b/crates/access-prover/src/lib.rs @@ -0,0 +1,3 @@ +pub mod circuit; +pub mod proof; +pub mod types; diff --git a/crates/access-prover/src/proof.rs b/crates/access-prover/src/proof.rs new file mode 100644 index 0000000..00719b6 --- /dev/null +++ b/crates/access-prover/src/proof.rs @@ -0,0 +1,336 @@ +use std::fs::File; +use std::io::Write; +use std::path::Path; +use std::str::FromStr; +use ark_ff::{BigInteger, PrimeField}; +use ark_bn254::{Bn254, Fr}; +use ark_groth16::{Groth16, ProvingKey, VerifyingKey}; +use ark_serialize::{CanonicalSerialize, CanonicalDeserialize}; +use ark_snark::{CircuitSpecificSetupSNARK, SNARK}; +use rand::thread_rng; +use thiserror::Error; + +use crate::circuit::AccessCircuit; +use crate::types::{SessionInput, ProofOutput, PublicInputsJson, serialize_proof, deserialize_proof}; + +#[derive(Error, Debug)] +pub enum ProverError { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + #[error("Serialization error: {0}")] + Serialization(#[from] ark_serialize::SerializationError), + #[error("Hex decoding error: {0}")] + Hex(#[from] hex::FromHexError), + #[error("Parsing error: {0}")] + Parsing(String), + #[error("ZKP Synthesis error: {0}")] + Synthesis(#[from] ark_relations::r1cs::SynthesisError), + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + #[error("Type conversion error: {0}")] + TypeError(#[from] crate::types::TypeError), +} + +// Robust helper to parse field element from hex or decimal string +pub fn parse_fr(s: &str) -> Result { + let s = s.trim(); + if s.starts_with("0x") || s.starts_with("0X") { + let hex_part = &s[2..]; + let bytes = hex::decode(hex_part).map_err(|e| format!("Invalid hex: {}", e))?; + let mut padded = vec![0u8; 32]; + if bytes.len() > 32 { + return Err("Hex string too long for field element".to_string()); + } + let start = 32 - bytes.len(); + padded[start..].copy_from_slice(&bytes); + Ok(Fr::from_be_bytes_mod_order(&padded)) + } else if let Ok(val) = Fr::from_str(s) { + Ok(val) + } else if s.chars().all(|c| c.is_ascii_hexdigit()) { + let bytes = hex::decode(s).map_err(|e| format!("Invalid hex: {}", e))?; + let mut padded = vec![0u8; 32]; + if bytes.len() > 32 { + return Err("Hex string too long for field element".to_string()); + } + let start = 32 - bytes.len(); + padded[start..].copy_from_slice(&bytes); + Ok(Fr::from_be_bytes_mod_order(&padded)) + } else { + Err(format!("Failed to parse field element from string: {}", s)) + } +} + +pub fn fr_to_hex(fr: &Fr) -> String { + let bytes_be = fr.into_bigint().to_bytes_be(); + format!("0x{}", hex::encode(bytes_be)) +} + +pub fn run_setup>( + pk_path: P, + vk_path: P, + vk_const_path: Option

, +) -> Result<(), ProverError> { + let mut rng = thread_rng(); + + // Create empty circuit for setup + // Merkle tree depth of 16 + let circuit = AccessCircuit:: { + policy_root: None, + session_nonce: None, + ciphertext_hash: None, + user_secret: None, + authorization_note: None, + merkle_siblings: vec![None; 16], + merkle_indices: vec![None; 16], + }; + + println!("Generating parameter keys (this may take a few seconds)..."); + let (pk, vk) = Groth16::::setup(circuit, &mut rng)?; + + // Write PK to file + let mut pk_file = File::create(pk_path)?; + pk.serialize_compressed(&mut pk_file)?; + + // Write VK to file + let mut vk_file = File::create(vk_path)?; + vk.serialize_compressed(&mut vk_file)?; + + // Write VK as Rust constant if requested + if let Some(const_path) = vk_const_path { + let mut vk_bytes = Vec::new(); + vk.serialize_compressed(&mut vk_bytes)?; + + let mut const_file = File::create(const_path)?; + writeln!(const_file, "/// Generated Verification Key for anonymous access verification")?; + writeln!(const_file, "pub const VERIFICATION_KEY_BYTES: &[u8] = &[")?; + for chunk in vk_bytes.chunks(12) { + let chunk_str = chunk.iter().map(|b| format!("0x{:02x}", b)).collect::>().join(", "); + writeln!(const_file, " {},", chunk_str)?; + } + writeln!(const_file, "];")?; + } + + Ok(()) +} + +pub fn run_prove>( + pk_path: P, + session: &SessionInput, +) -> Result { + // 1. Parse inputs to Fr + let policy_root = parse_fr(&session.policy_root).map_err(ProverError::Parsing)?; + let session_nonce = parse_fr(&session.session_nonce).map_err(ProverError::Parsing)?; + let ciphertext_hash = parse_fr(&session.ciphertext_hash).map_err(ProverError::Parsing)?; + let user_secret = parse_fr(&session.user_secret).map_err(ProverError::Parsing)?; + let authorization_note = parse_fr(&session.authorization_note).map_err(ProverError::Parsing)?; + + let mut merkle_siblings = Vec::new(); + for s in &session.merkle_path.siblings { + merkle_siblings.push(Some(parse_fr(s).map_err(ProverError::Parsing)?)); + } + + let mut merkle_indices = Vec::new(); + for &idx in &session.merkle_path.indices { + merkle_indices.push(Some(idx)); + } + + // Make sure path is padded to 16 + while merkle_siblings.len() < 16 { + merkle_siblings.push(Some(Fr::from(0u32))); + merkle_indices.push(Some(false)); + } + + // 2. Load Proving Key + let mut pk_file = File::open(pk_path)?; + let pk = ProvingKey::::deserialize_compressed(&mut pk_file)?; + + // 3. Create circuit instance + let circuit = AccessCircuit { + policy_root: Some(policy_root), + session_nonce: Some(session_nonce), + ciphertext_hash: Some(ciphertext_hash), + user_secret: Some(user_secret), + authorization_note: Some(authorization_note), + merkle_siblings, + merkle_indices, + }; + + // 4. Generate proof + let mut rng = thread_rng(); + let proof = Groth16::::prove(&pk, circuit, &mut rng)?; + + // 5. Build output JSON + let proof_json = serialize_proof(&proof)?; + let public_inputs = PublicInputsJson { + policy_root: fr_to_hex(&policy_root), + session_nonce: fr_to_hex(&session_nonce), + ciphertext_hash: fr_to_hex(&ciphertext_hash), + }; + + Ok(ProofOutput { + version: "1.0.0".to_string(), + proof: proof_json, + public_inputs, + }) +} + +pub fn run_verify>( + vk_path: P, + proof_out: &ProofOutput, +) -> Result { + // 1. Load Verification Key + let mut vk_file = File::open(vk_path)?; + let vk = VerifyingKey::::deserialize_compressed(&mut vk_file)?; + + verify_with_vk(&vk, proof_out) +} + +pub fn verify_with_vk( + vk: &VerifyingKey, + proof_out: &ProofOutput, +) -> Result { + // 2. Deserialize proof + let proof = deserialize_proof(&proof_out.proof)?; + + // 3. Parse public inputs in order: policy_root, session_nonce, ciphertext_hash + let policy_root = parse_fr(&proof_out.public_inputs.policy_root).map_err(ProverError::Parsing)?; + let session_nonce = parse_fr(&proof_out.public_inputs.session_nonce).map_err(ProverError::Parsing)?; + let ciphertext_hash = parse_fr(&proof_out.public_inputs.ciphertext_hash).map_err(ProverError::Parsing)?; + + let public_inputs = vec![policy_root, session_nonce, ciphertext_hash]; + + // 4. Verify + let is_valid = Groth16::::verify(vk, &public_inputs, &proof)?; + Ok(is_valid) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::circuit::{get_poseidon_config, poseidon_hash_native}; + use crate::types::MerklePathJson; + use rand::thread_rng; + use ark_ff::UniformRand; + use ark_r1cs_std::prelude::*; + use ark_r1cs_std::fields::fp::FpVar; + use ark_r1cs_std::alloc::AllocVar; + + #[test] + fn test_parse_fr() { + // Decimal + let val = parse_fr("12345").unwrap(); + assert_eq!(val, Fr::from(12345u64)); + + // Hex with 0x + let val2 = parse_fr("0x3039").unwrap(); // 12345 in hex is 0x3039 + assert_eq!(val2, Fr::from(12345u64)); + + // Hex without 0x (must contain non-decimal chars to bypass Fr::from_str) + let val3 = parse_fr("ff").unwrap(); + assert_eq!(val3, Fr::from(255u64)); + + // Invalid + assert!(parse_fr("abcg").is_err()); + } + + #[test] + fn test_poseidon_native_and_circuit() { + use ark_relations::r1cs::ConstraintSystem; + + let config = get_poseidon_config::(3); + let in1 = Fr::from(42u64); + let in2 = Fr::from(100u64); + + let hash_native = poseidon_hash_native(&[in1, in2], &config); + + let cs = ConstraintSystem::::new_ref(); + let in1_var = FpVar::new_witness(cs.clone(), || Ok(in1)).unwrap(); + let in2_var = FpVar::new_witness(cs.clone(), || Ok(in2)).unwrap(); + + let hash_var = crate::circuit::poseidon_hash_circuit(&[in1_var, in2_var], &config).unwrap(); + assert_eq!(hash_var.value().unwrap(), hash_native); + assert!(cs.is_satisfied().unwrap()); + } + + #[test] + fn test_proof_e2e_happy_and_sad() { + let config = get_poseidon_config::(3); + let mut rng = thread_rng(); + + // Setup secrets + let user_secret = Fr::from(99999u64); + let authorization_note = poseidon_hash_native(&[user_secret], &config); + + // Construct a valid Merkle path of depth 16 + let leaf_index = 5u32; // binary: 101 + let mut siblings = Vec::new(); + let mut indices = Vec::new(); + let mut current = authorization_note; + + for i in 0..16 { + let sibling = Fr::rand(&mut rng); + let is_right = ((leaf_index >> i) & 1) == 1; + + siblings.push(sibling); + indices.push(is_right); + + current = if is_right { + poseidon_hash_native(&[sibling, current], &config) + } else { + poseidon_hash_native(&[current, sibling], &config) + }; + } + let policy_root = current; + + // Session params + let session_nonce = Fr::from(12345678u64); + let ciphertext_hash = Fr::from(88888888u64); + + // Create session input JSON + let session_input = SessionInput { + policy_root: fr_to_hex(&policy_root), + session_nonce: fr_to_hex(&session_nonce), + ciphertext_hash: fr_to_hex(&ciphertext_hash), + user_secret: fr_to_hex(&user_secret), + authorization_note: fr_to_hex(&authorization_note), + merkle_path: MerklePathJson { + siblings: siblings.iter().map(fr_to_hex).collect(), + indices, + }, + }; + + // Temporary keys in target directory + let dir = std::env::temp_dir(); + let pk_path = dir.join("test_pk.bin"); + let vk_path = dir.join("test_vk.bin"); + let vk_const_path = dir.join("test_vk_const.rs"); + + // Setup + run_setup(&pk_path, &vk_path, Some(&vk_const_path)).unwrap(); + + // Prove + let proof_out = run_prove(&pk_path, &session_input).unwrap(); + + // Verify (Happy path) + let is_valid = run_verify(&vk_path, &proof_out).unwrap(); + assert!(is_valid, "Happy-path verification failed"); + + // Sad path: Tampered policy_root + let mut proof_bad_root = proof_out.clone(); + proof_bad_root.public_inputs.policy_root = fr_to_hex(&Fr::from(11111u64)); + let is_valid_bad_root = run_verify(&vk_path, &proof_bad_root).unwrap(); + assert!(!is_valid_bad_root, "Verification should fail with bad policy root"); + + // Sad path: Tampered nonce + let mut proof_bad_nonce = proof_out.clone(); + proof_bad_nonce.public_inputs.session_nonce = fr_to_hex(&Fr::from(11111u64)); + let is_valid_bad_nonce = run_verify(&vk_path, &proof_bad_nonce).unwrap(); + assert!(!is_valid_bad_nonce, "Verification should fail with bad nonce"); + + // Clean up temp files if they exist + let _ = std::fs::remove_file(pk_path); + let _ = std::fs::remove_file(vk_path); + let _ = std::fs::remove_file(vk_const_path); + } +} + diff --git a/crates/access-prover/src/types.rs b/crates/access-prover/src/types.rs new file mode 100644 index 0000000..b8f34fa --- /dev/null +++ b/crates/access-prover/src/types.rs @@ -0,0 +1,80 @@ +use serde::{Deserialize, Serialize}; +use ark_serialize::{CanonicalSerialize, CanonicalDeserialize}; +use ark_bn254::Bn254; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum TypeError { + #[error("Serialization error: {0}")] + Serialization(#[from] ark_serialize::SerializationError), + #[error("Hex decoding error: {0}")] + Hex(#[from] hex::FromHexError), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MerklePathJson { + pub siblings: Vec, + pub indices: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SessionInput { + pub policy_root: String, + pub session_nonce: String, + pub ciphertext_hash: String, + pub user_secret: String, + pub authorization_note: String, + pub merkle_path: MerklePathJson, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PublicInputsJson { + pub policy_root: String, + pub session_nonce: String, + pub ciphertext_hash: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ProofJson { + pub a: String, // hex-encoded compressed G1Affine + pub b: String, // hex-encoded compressed G2Affine + pub c: String, // hex-encoded compressed G1Affine +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ProofOutput { + pub version: String, + pub proof: ProofJson, + pub public_inputs: PublicInputsJson, +} + +// Serialization helpers +pub fn serialize_proof(proof: &ark_groth16::Proof) -> Result { + let mut a_bytes = Vec::new(); + proof.a.serialize_compressed(&mut a_bytes)?; + + let mut b_bytes = Vec::new(); + proof.b.serialize_compressed(&mut b_bytes)?; + + let mut c_bytes = Vec::new(); + proof.c.serialize_compressed(&mut c_bytes)?; + + Ok(ProofJson { + a: hex::encode(a_bytes), + b: hex::encode(b_bytes), + c: hex::encode(c_bytes), + }) +} + +pub fn deserialize_proof(proof_json: &ProofJson) -> Result, TypeError> { + let a_bytes = hex::decode(&proof_json.a)?; + let a = ::G1Affine::deserialize_compressed(&a_bytes[..])?; + + let b_bytes = hex::decode(&proof_json.b)?; + let b = ::G2Affine::deserialize_compressed(&b_bytes[..])?; + + let c_bytes = hex::decode(&proof_json.c)?; + let c = ::G1Affine::deserialize_compressed(&c_bytes[..])?; + + Ok(ark_groth16::Proof { a, b, c }) +}