diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml index 8b7eac2..7d2a7cc 100644 --- a/.github/workflows/build-wasm.yml +++ b/.github/workflows/build-wasm.yml @@ -1,9 +1,9 @@ name: Build Wasm "on": - push: - branches: - - master + # push: + # branches: + # - master pull_request: branches: - master diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml new file mode 100644 index 0000000..6514f9b --- /dev/null +++ b/.github/workflows/publish-wasm.yml @@ -0,0 +1,40 @@ +name: Publish wasm package + +on: + push: + tags: + - 'v*' # Capture pushes to tags that start with 'v', e.g., v0.1.0 + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + + - name: Build wasm package + run: | + cd bbs_wasm + wasm-pack build --target web + + - name: Publish to npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + cd bbs_wasm + wasm-pack publish --access public diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index cb2e299..65627bc 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -1,14 +1,18 @@ name: Rust CI "on": - push: - branches: - - master + # push: + # branches: + # - master pull_request: branches: - master workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + jobs: build-and-test: name: Build and test signer diff --git a/.gitignore b/.gitignore index ef52176..ad491e9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ temp .vscode # rust -**/target \ No newline at end of file +**/target + +# mac +**/.DS_Store \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index ca1aaa5..a837518 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,6 +199,7 @@ dependencies = [ "ark-serialize", "ark-std", "criterion", + "hex", "serde", "serde_json", "sha3", @@ -238,24 +239,6 @@ dependencies = [ "wasm-bindgen-test", ] -[[package]] -name = "bbs_wasm" -version = "0.1.0" -dependencies = [ - "ark-bn254", - "ark-serialize", - "ark-std", - "bbs", - "console_error_panic_hook", - "getrandom", - "hex", - "rand", - "serde", - "serde-wasm-bindgen", - "serde_json", - "wasm-bindgen", -] - [[package]] name = "block-buffer" version = "0.10.4" diff --git a/README.md b/README.md index 13996d7..7b70cc9 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,14 @@ use bbs::bbs_bn254::*; > run `cargo bench -p bbs` to run the benchmarks. +## Npm Package + +> npm i bbs_wasm + +## Reference + ++ [Revisiting BBS Signatures](https://eprint.iacr.org/2023/275) + ## Thanks + [arkworks::algebra](https://github.com/arkworks-rs/algebra) \ No newline at end of file diff --git a/bbs/Cargo.toml b/bbs/Cargo.toml index b780375..f3e3238 100644 --- a/bbs/Cargo.toml +++ b/bbs/Cargo.toml @@ -14,6 +14,15 @@ substrate-bn = "0.6.0" sha3 = "0.10" serde_json = "1.0.149" serde = { version = "1.0.228", features = ["derive"] } +hex = "0.4.3" [dev-dependencies] -criterion = { version = "0.5", features = ["html_reports"] } \ No newline at end of file +criterion = { version = "0.5", features = ["html_reports"] } + +[[bin]] +name = "tools" +path = "script/tools.rs" + +[[bin]] +name = "hash_test" +path = "script/hash_test.rs" \ No newline at end of file diff --git a/bbs/script/hash_test.rs b/bbs/script/hash_test.rs new file mode 100644 index 0000000..7cc6693 --- /dev/null +++ b/bbs/script/hash_test.rs @@ -0,0 +1,38 @@ +use std::str::FromStr; +use ark_bn254::Fr as Scalar; +use ark_ff::BigInteger; +use ark_ff::PrimeField; +use sha3::{Digest, Keccak256}; + +fn main() { + let ctx = "LetsVerify".as_bytes(); + let m0 = Scalar::from_str("48305238028827234457057068308976830949311451167141786071899284143701496669988").unwrap(); + let m1 = Scalar::from_str("30362564611297414121344052595118041020684015714278959822893592029797294529055").unwrap(); + let m2 = Scalar::from_str("89017634352366059964332753369766654038457760534865200995547152036320389318152").unwrap(); + let m3 = Scalar::from_str("89017634352366059964332753369766654038457760534865200995547152036320389318152").unwrap(); + + let mut hasher = Keccak256::new(); + let mut payload = Vec::new(); + + let mut ctx_bytes = [0u8; 32]; + let len = ctx.len().min(32); + ctx_bytes[..len].copy_from_slice(&ctx[..len]); + payload.extend_from_slice(&ctx_bytes); + + for m in &[m0, m1, m2, m3] { + let m_bytes = m.into_bigint().to_bytes_be(); + let mut padded = [0u8; 32]; + let offset = 32 - m_bytes.len().min(32); + padded[offset..].copy_from_slice(&m_bytes); + payload.extend_from_slice(&padded); + } + + hasher.update(&payload); + let hash_result = hasher.finalize(); + println!("Payload Hex: {}", hex::encode(&payload)); + println!("Hash: {}", hex::encode(&hash_result)); + + let c = Scalar::from_be_bytes_mod_order(&hash_result); + // Print decimal representation + println!("C: {}", c); +} \ No newline at end of file diff --git a/bbs/script/tools.rs b/bbs/script/tools.rs new file mode 100644 index 0000000..f00f9a2 --- /dev/null +++ b/bbs/script/tools.rs @@ -0,0 +1,21 @@ +use sha3::{Digest, Keccak256}; + +pub fn main() { + let mut hasher = Keccak256::new(); + let m1 = String::from("Age=18"); + let m2 = String::from("Nationality=US"); + let m3 = String::from("Verified=true"); + let m4 = String::from("Empty"); + + hasher.update(m1.as_bytes()); + println!("Hash of Age=18: 0x{}", hex::encode(hasher.finalize_reset())); + + hasher.update(m2.as_bytes()); + println!("Hash of Nationality=US: 0x{}", hex::encode(hasher.finalize_reset())); + + hasher.update(m3.as_bytes()); + println!("Hash of Verified=true: 0x{}", hex::encode(hasher.finalize_reset())); + + hasher.update(m4.as_bytes()); + println!("Hash of Empty: 0x{}", hex::encode(hasher.finalize_reset())); +} \ No newline at end of file diff --git a/bbs/src/bbs.rs b/bbs/src/bbs.rs index b99c5e8..166e163 100644 --- a/bbs/src/bbs.rs +++ b/bbs/src/bbs.rs @@ -1,4 +1,4 @@ -//! Key gen related functions here. +//! Basic BBS scheme functions here. #![allow(non_snake_case)] use ark_bn254::Bn254; diff --git a/bbs/src/extend.rs b/bbs/src/extend.rs new file mode 100644 index 0000000..c4415a1 --- /dev/null +++ b/bbs/src/extend.rs @@ -0,0 +1,101 @@ +//! Extend funcs and methods for supporting our scheme. +#![allow(non_snake_case)] + +use crate::pub_use::*; +use ark_std::{UniformRand, rand::RngCore}; + +use crate::extend_structs::{PartialSignature, UserCommitment}; +use crate::structs::{Signature}; + +pub struct BBSPlusExtendedScheme; + +use ark_ff::{BigInteger, PrimeField}; +use ark_bn254::{Fr as Scalar, G1Affine as G1, G2Affine as G2}; + +/// Converts a Field element to BIG ENDIAN bytes (32 bytes for Fr/Fq) +pub fn field_to_bytes_be(f: &F) -> Vec { + f.into_bigint().to_bytes_be() +} + +/// Serializes G1 into 64 bytes (X || Y) for EVM compatibility +pub fn push_g1_to_bytes(p: &G1, buf: &mut Vec) { + buf.extend_from_slice(&field_to_bytes_be(&p.x)); + buf.extend_from_slice(&field_to_bytes_be(&p.y)); +} + +/// Serializes G2 into 128 bytes (X.c1 || X.c0 || Y.c1 || Y.c0) for EVM Bn254 compatibility +pub fn push_g2_to_bytes(p: &G2, buf: &mut Vec) { + buf.extend_from_slice(&field_to_bytes_be(&p.x.c1)); + buf.extend_from_slice(&field_to_bytes_be(&p.x.c0)); + buf.extend_from_slice(&field_to_bytes_be(&p.y.c1)); + buf.extend_from_slice(&field_to_bytes_be(&p.y.c0)); +} + +impl BBSPlusExtendedScheme { + /// Step 2: User computes blinded commitment C2 + pub fn user_commit( + m_null: &Scalar, + m_gamma: &Scalar, + lambda: &Scalar, + h_bases: &[G1], + ) -> UserCommitment { + let l = h_bases.len(); + let h_null = &h_bases[l - 2]; + let h_gamma = &h_bases[l - 1]; + + let mut c2_proj = *h_null * *m_null; + c2_proj += *h_gamma * *m_gamma; + c2_proj = c2_proj * *lambda; + UserCommitment { + C2: c2_proj.into_affine(), + } + } + + /// Step 3: Signer generates partial signature + pub fn signer_sign( + rng: &mut R, + sk: &Scalar, + messages: &[Scalar], // public messages m_0 to m_{l-3} + h_bases: &[G1], // H0 to H_{l-1} + g1: &G1, + c2: &G1, + ) -> PartialSignature { + let l = h_bases.len(); + assert_eq!(messages.len(), l - 2, "Public messages count must be l-2"); + + // C1 = G1 + \sum m_i H_i + let mut c1_proj = g1.into_group(); + for (m, h) in messages.iter().zip(h_bases.iter()) { + c1_proj += *h * *m; + } + + let e = Scalar::rand(rng); + let mut denominator = *sk; + denominator += e; + let inv_denominator = denominator.inverse().unwrap(); + + let a1 = (c1_proj * inv_denominator).into_affine(); + let a2_prime = (*c2 * inv_denominator).into_affine(); + + PartialSignature { + A1: a1, + A2_prime: a2_prime, + e, + } + } + + /// Step 4.1: User unblinds the partial signature to get the full signature (A, e) + pub fn user_unblind( + partial_sig: &PartialSignature, + lambda: &Scalar, + ) -> Signature { + let inv_lambda = lambda.inverse().unwrap(); + let a2 = (partial_sig.A2_prime * inv_lambda).into_affine(); + let a = (partial_sig.A1.into_group() + a2).into_affine(); + + Signature { + A: a, + e: partial_sig.e, + } + } +} \ No newline at end of file diff --git a/bbs/src/extend_structs.rs b/bbs/src/extend_structs.rs new file mode 100644 index 0000000..8930d07 --- /dev/null +++ b/bbs/src/extend_structs.rs @@ -0,0 +1,58 @@ +//! Extend structs for our scheme +#![allow(non_snake_case)] + +use ark_bn254::{Fr as Scalar, G1Affine as G1}; +use serde::{Deserialize, Serialize}; + +use crate::modified_serde::*; + +/// User's blinded commitment for partial signature +/// C2 = lambda * (m_null * H_null + m_gamma * H_gamma) +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct UserCommitment { + #[serde(serialize_with = "serialize_g1", deserialize_with = "deserialize_g1")] + pub C2: G1, +} + +/// Partial signature generated by the Signer +/// Contains two A points and the random scalar e +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PartialSignature { + #[serde(serialize_with = "serialize_g1", deserialize_with = "deserialize_g1")] + pub A1: G1, + #[serde(serialize_with = "serialize_g1", deserialize_with = "deserialize_g1")] + pub A2_prime: G1, + #[serde( + serialize_with = "serialize_scalar", + deserialize_with = "deserialize_scalar" + )] + pub e: Scalar, +} + +/// NIZK Proof for BBS+ partial disclosure +/// pi = (A_bar, B_bar, U, s, t, hidden_responses) +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BBSPlusNIZKProof { + #[serde(serialize_with = "serialize_g1", deserialize_with = "deserialize_g1")] + pub A_bar: G1, + #[serde(serialize_with = "serialize_g1", deserialize_with = "deserialize_g1")] + pub B_bar: G1, + #[serde(serialize_with = "serialize_g1", deserialize_with = "deserialize_g1")] + pub U: G1, + #[serde( + serialize_with = "serialize_scalar", + deserialize_with = "deserialize_scalar" + )] + pub s: Scalar, + #[serde( + serialize_with = "serialize_scalar", + deserialize_with = "deserialize_scalar" + )] + pub t: Scalar, + /// Store the responses for hidden values (e.g., u_gamma) + #[serde( + serialize_with = "serialize_vec_scalar", + deserialize_with = "deserialize_vec_scalar" + )] + pub hidden_responses: Vec, +} \ No newline at end of file diff --git a/bbs/src/lib.rs b/bbs/src/lib.rs index 74c0145..5190241 100644 --- a/bbs/src/lib.rs +++ b/bbs/src/lib.rs @@ -1,8 +1,10 @@ pub mod bbs; mod modified_serde; pub mod pok; -mod pub_use; +pub mod pub_use; pub mod structs; +pub mod extend; +pub mod extend_structs; pub fn add(left: u64, right: u64) -> u64 { left + right diff --git a/bbs/src/pok.rs b/bbs/src/pok.rs index 006b43c..1ed7418 100644 --- a/bbs/src/pok.rs +++ b/bbs/src/pok.rs @@ -53,9 +53,6 @@ pub struct NonInteractiveProof { pub u_i: HashMap, } -/// Context for FS heuristic -const POK_CTX: &[u8] = b"BBS_PoK_1"; - /// Compute the public commitment $C_J = G_1 + \sum_{j \in J} m_j \cdot H_j$ pub fn compute_c_j(params: &Params, disclosed: &DisclosedMessages) -> G1 { let mut c_j = params.G1.into_group(); @@ -199,6 +196,7 @@ pub fn pok_verify( /* ================== Non-Interactive Version (Fiat-Shamir) ================== */ fn compute_challenge( + pok_ctx: &[u8], pk: &PublicKey, disclosed: &DisclosedMessages, a_bar: &G1, @@ -206,7 +204,7 @@ fn compute_challenge( u: &G1, ) -> Scalar { let mut hasher = Keccak256::new(); - hasher.update(POK_CTX); + hasher.update(pok_ctx); // Convert components to bytes appropriately (this is simplified, ideal uses CanonicalSerialize) use ark_serialize::CanonicalSerialize; @@ -246,6 +244,7 @@ fn compute_challenge( /// NIZK Prove (Fiat-Shamir) pub fn nizk_prove( + pok_ctx: &[u8], params: &Params, pk: &PublicKey, messages: &Messages, @@ -264,6 +263,7 @@ pub fn nizk_prove( } let challenge = compute_challenge( + pok_ctx, pk, &disclosed_msgs, &commitment.A_bar, @@ -284,12 +284,13 @@ pub fn nizk_prove( /// NIZK Verify (Fiat-Shamir) pub fn nizk_verify( + pok_ctx: &[u8], params: &Params, pk: &PublicKey, disclosed: &DisclosedMessages, proof: &NonInteractiveProof, ) -> bool { - let challenge = compute_challenge(pk, disclosed, &proof.A_bar, &proof.B_bar, &proof.U); + let challenge = compute_challenge(pok_ctx, pk, disclosed, &proof.A_bar, &proof.B_bar, &proof.U); let commitment = PoKCommitment { A_bar: proof.A_bar, @@ -317,47 +318,65 @@ pub struct NonInteractiveProofPrefix { pub u_i: Vec, } -fn compute_challenge_prefix( - pk: &PublicKey, +use ark_ff::{BigInteger}; + +pub fn compute_challenge_prefix( + pok_ctx: &[u8], + _pk: &PublicKey, disclosed: &[Scalar], a_bar: &G1, b_bar: &G1, u: &G1, ) -> Scalar { let mut hasher = Keccak256::new(); - hasher.update(POK_CTX); - - use ark_serialize::CanonicalSerialize; - - let mut pk_bytes = Vec::new(); - pk.X.serialize_compressed(&mut pk_bytes).unwrap_or_default(); - hasher.update(&pk_bytes); - - for (i, m) in disclosed.iter().enumerate() { - hasher.update(&(i as u64).to_be_bytes()); - let mut m_bytes = Vec::new(); - m.serialize_compressed(&mut m_bytes).unwrap_or_default(); - hasher.update(&m_bytes); + let mut payload = Vec::new(); + + // 1. Hash ctx (Assuming pok_ctx is already 32 bytes externally or padded/hashed to 32 bytes) + // For compatibility with Solidity's `bytes32 ctx`, we expect `pok_ctx` to be exactly 32 bytes. + let mut ctx_bytes = [0u8; 32]; + if pok_ctx.len() == 32 { + ctx_bytes.copy_from_slice(pok_ctx); + } else { + // Pad with trailing zeros to match Solidity's bytes32("str") + let len = pok_ctx.len().min(32); + ctx_bytes[..len].copy_from_slice(&pok_ctx[..len]); + } + payload.extend_from_slice(&ctx_bytes); + + // 2. Hash disclosed messages. (Solidity `abi.encodePacked` of `uint256[]` is just flat consecutive 32-byte words) + for m in disclosed.iter() { + let m_bytes = m.into_bigint().to_bytes_be(); + // Since arkworks `to_bytes_be` might not pad to 32 bytes exactly depending on the field, + // we ensure it is 32 bytes for Solidity uint256: + let mut padded = [0u8; 32]; + let offset = 32 - m_bytes.len().min(32); + padded[offset..].copy_from_slice(&m_bytes); + payload.extend_from_slice(&padded); } - let mut a_bytes = Vec::new(); - a_bar.serialize_compressed(&mut a_bytes).unwrap_or_default(); - hasher.update(&a_bytes); + // 3. Hash A_bar.x, A_bar.y + let push_g1 = |payload: &mut Vec, p: &G1| { + let x_bytes = p.x.into_bigint().to_bytes_be(); + let mut px = [0u8; 32]; px[32 - x_bytes.len().min(32)..].copy_from_slice(&x_bytes); + payload.extend_from_slice(&px); - let mut b_bytes = Vec::new(); - b_bar.serialize_compressed(&mut b_bytes).unwrap_or_default(); - hasher.update(&b_bytes); + let y_bytes = p.y.into_bigint().to_bytes_be(); + let mut py = [0u8; 32]; py[32 - y_bytes.len().min(32)..].copy_from_slice(&y_bytes); + payload.extend_from_slice(&py); + }; - let mut u_bytes = Vec::new(); - u.serialize_compressed(&mut u_bytes).unwrap_or_default(); - hasher.update(&u_bytes); + push_g1(&mut payload, a_bar); + push_g1(&mut payload, b_bar); + push_g1(&mut payload, u); + hasher.update(&payload); let hash_result = hasher.finalize(); Scalar::from_be_bytes_mod_order(&hash_result) } /// NIZK Prove (Prefix Disclosure: 0..disclosed_count-1 are public) pub fn nizk_prove_prefix( + pok_ctx: &[u8], params: &Params, pk: &PublicKey, messages: &Messages, @@ -403,7 +422,7 @@ pub fn nizk_prove_prefix( let u = (c_j * alpha + a_bar * beta + u_term).into_affine(); let disclosed_msgs = &messages.0[0..disclosed_count]; - let challenge = compute_challenge_prefix(pk, disclosed_msgs, &a_bar, &b_bar, &u); + let challenge = compute_challenge_prefix(pok_ctx, pk, disclosed_msgs, &a_bar, &b_bar, &u); let s = alpha + r * challenge; let t = beta - signature.e * challenge; @@ -426,6 +445,7 @@ pub fn nizk_prove_prefix( /// NIZK Verify (Prefix Disclosure) pub fn nizk_verify_prefix( + pok_ctx: &[u8], params: &Params, pk: &PublicKey, disclosed_msgs: &[Scalar], @@ -437,7 +457,7 @@ pub fn nizk_verify_prefix( } let challenge = - compute_challenge_prefix(pk, disclosed_msgs, &proof.A_bar, &proof.B_bar, &proof.U); + compute_challenge_prefix(pok_ctx, pk, disclosed_msgs, &proof.A_bar, &proof.B_bar, &proof.U); let mut c_j = params.G1.into_group(); for (j, &m_j) in disclosed_msgs.iter().enumerate() { diff --git a/bbs/tests/bbs_extended_test.rs b/bbs/tests/bbs_extended_test.rs new file mode 100644 index 0000000..6d12c40 --- /dev/null +++ b/bbs/tests/bbs_extended_test.rs @@ -0,0 +1,115 @@ +use ark_bn254::{Fr as Scalar, G1Affine as G1, G2Affine as G2}; +use ark_std::UniformRand; +use ark_std::test_rng; +use bbs::extend::BBSPlusExtendedScheme; +use bbs::structs::{PublicKey, Messages, Params}; +use bbs::pok::{nizk_prove_prefix, nizk_verify_prefix}; +use ark_ec::{CurveGroup, AffineRepr}; + +#[test] +fn test_bbs_extended_scheme() { + let mut rng = test_rng(); + + // --------------------------------------------------------- + // 0. Setup parameters (Assume Trusted Setup / Issuer) + // --------------------------------------------------------- + let g1 = G1::generator(); + let g2 = G2::generator(); + + // We need 5 public messages (m0 to m4), + 2 extra messages (m_null, m_gamma) + let num_public_messages = 5; + let total_messages = num_public_messages + 2; + + let mut h_bases = Vec::with_capacity(total_messages); + for _ in 0..total_messages { + h_bases.push(G1::rand(&mut rng)); + } + + let params = Params { + G1: g1, + G2: g2, + L: total_messages, + H: h_bases.clone(), + }; + + // Signer keys + let sk = Scalar::rand(&mut rng); + let pk_point = (g2 * sk).into_affine(); + let pk = PublicKey { X: pk_point }; + + // --------------------------------------------------------- + // 1. User Application (Init & Blinding) + // --------------------------------------------------------- + // User attributes + let mut messages_vec = Vec::with_capacity(total_messages); + for _ in 0..num_public_messages { + messages_vec.push(Scalar::rand(&mut rng)); // e.g., age=18, nationality="CN", etc. + } + let m_null = Scalar::rand(&mut rng); + let m_gamma = Scalar::rand(&mut rng); + let lambda = Scalar::rand(&mut rng); + + // Push m_null and m_gamma as the last two messages + messages_vec.push(m_null); + messages_vec.push(m_gamma); + let messages = Messages(messages_vec.clone()); + + // User commits to m_null and m_gamma + let user_commitment = BBSPlusExtendedScheme::user_commit( + &m_null, + &m_gamma, + &lambda, + &h_bases + ); + + // --------------------------------------------------------- + // 2. Signer Generates Partial Signature + // --------------------------------------------------------- + // Signer checks public constraints, and issues partial sig + let public_messages = &messages_vec[..num_public_messages]; + let partial_sig = BBSPlusExtendedScheme::signer_sign( + &mut rng, + &sk, + public_messages, + &h_bases, + &g1, + &user_commitment.C2, + ); + + // --------------------------------------------------------- + // 3. User Unblinds the Partial Signature + // --------------------------------------------------------- + let full_signature = BBSPlusExtendedScheme::user_unblind(&partial_sig, &lambda); + + // --------------------------------------------------------- + // 4. User Generates NIZK Proof (Prove ownership of valid sig) + // --------------------------------------------------------- + // Disclosed count: num_public_messages + 1 (m_null is disclosed to verifier) + // The only hidden message is m_gamma at the end + let disclosed_count = num_public_messages + 1; + let ctx = "LetsVerify".as_bytes(); + + let proof = nizk_prove_prefix( + &ctx, + ¶ms, + &pk, + &messages, + &full_signature, + disclosed_count, + &mut rng + ).expect("nizk_prove_prefix failed"); + + // --------------------------------------------------------- + // 5. Verifier Validates the NIZK Proof + // --------------------------------------------------------- + let disclosed_msgs = &messages_vec[..disclosed_count]; + let is_valid = nizk_verify_prefix( + &ctx, + ¶ms, + &pk, + disclosed_msgs, + &proof + ); + + assert!(is_valid, "The extended BBS scheme NIZK proof failed to verify!"); +} diff --git a/bbs/tests/hash_test.rs b/bbs/tests/hash_test.rs new file mode 100644 index 0000000..67bedac --- /dev/null +++ b/bbs/tests/hash_test.rs @@ -0,0 +1,66 @@ +use ark_bn254::{Fr as Scalar, G1Affine as G1, G2Affine as G2}; +use ark_ec::AffineRepr; +use bbs::structs::PublicKey; +use sha3::{Digest, Keccak256}; +use ark_serialize::CanonicalSerialize; +use hex; + +#[test] +fn main() { + let pk = PublicKey { X: G2::generator() }; + let disclosed = vec![Scalar::from(42u64)]; + let a_bar = G1::generator(); + let b_bar = G1::generator(); + let u = G1::generator(); + + let mut hasher = Keccak256::new(); + let mut all_bytes = Vec::new(); + + let ctx = b"BBS_PoK_1"; + all_bytes.extend_from_slice(ctx); + + let mut pk_bytes = Vec::new(); + pk.X.serialize_compressed(&mut pk_bytes).unwrap(); + all_bytes.extend_from_slice(&pk_bytes); + + for (i, m) in disclosed.iter().enumerate() { + all_bytes.extend_from_slice(&(i as u64).to_be_bytes()); + let mut m_bytes = Vec::new(); + m.serialize_compressed(&mut m_bytes).unwrap(); + all_bytes.extend_from_slice(&m_bytes); + } + + let mut a_bytes = Vec::new(); + a_bar.serialize_compressed(&mut a_bytes).unwrap(); + all_bytes.extend_from_slice(&a_bytes); + + let mut b_bytes = Vec::new(); + b_bar.serialize_compressed(&mut b_bytes).unwrap(); + all_bytes.extend_from_slice(&b_bytes); + + let mut u_bytes = Vec::new(); + u.serialize_compressed(&mut u_bytes).unwrap(); + all_bytes.extend_from_slice(&u_bytes); + + hasher.update(&all_bytes); + let hash_result = hasher.finalize(); + + println!("All bytes: 0x{}", hex::encode(&all_bytes)); + println!("Hash result: 0x{}", hex::encode(&hash_result)); + + let hex_str = format!("0x{}", hex::encode(&all_bytes)); + println!("\nExecuting command: cast keccak {}", hex_str); + + // Call shell command directly using std::process::Command + let output = std::process::Command::new("cast") + .arg("keccak") + .arg(&hex_str) + .output() + .expect("Failed to execute cast command. Please Make sure foundry/cast is installed in your PATH."); + + if output.status.success() { + println!("cast output: {}", String::from_utf8_lossy(&output.stdout).trim()); + } else { + eprintln!("cast failed: {}", String::from_utf8_lossy(&output.stderr).trim()); + } +} diff --git a/bbs/tests/pok_test.rs b/bbs/tests/pok_test.rs index 78ee33e..3eef2d5 100644 --- a/bbs/tests/pok_test.rs +++ b/bbs/tests/pok_test.rs @@ -81,8 +81,11 @@ mod tests { disclosed_msgs.insert(1, msgs[1]); disclosed_msgs.insert(4, msgs[4]); + let ctx = "LestsVerify".as_bytes(); + // NIZK Prove let proof = nizk_prove( + ctx, ¶ms, &pk, &messages, @@ -93,7 +96,7 @@ mod tests { .unwrap(); // NIZK Verify - let is_valid = nizk_verify(¶ms, &pk, &disclosed_msgs, &proof); + let is_valid = nizk_verify(ctx, ¶ms, &pk, &disclosed_msgs, &proof); assert!(is_valid, "Non-Interactive PoK verification failed!"); } @@ -117,8 +120,10 @@ mod tests { let disclosed_count = 3; let disclosed_msgs = &msgs[0..disclosed_count]; + let ctx = "LestsVerify".as_bytes(); // NIZK Prove let proof = nizk_prove_prefix( + ctx, ¶ms, &pk, &messages, @@ -129,7 +134,7 @@ mod tests { .unwrap(); // NIZK Verify - let is_valid = nizk_verify_prefix(¶ms, &pk, disclosed_msgs, &proof); + let is_valid = nizk_verify_prefix(ctx, ¶ms, &pk, disclosed_msgs, &proof); assert!(is_valid, "Prefix Non-Interactive PoK verification failed!"); } } diff --git a/bbs_wasm/Cargo.toml b/bbs_wasm/Cargo.toml index 75f2bac..556c0f8 100644 --- a/bbs_wasm/Cargo.toml +++ b/bbs_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bbs_wasm" -version = "0.1.0" +version = "0.1.2" edition = "2024" [lib] @@ -14,7 +14,7 @@ console_error_panic_hook = "0.1" getrandom = { version = "0.2", features = ["js"] } rand = "0.8" ark-serialize = "0.5.0" - +web-sys = { version = "0.3", features = ["console"] } bbs = { path = "../bbs" } ark-bn254 = "0.5.0" ark-std = "0.5.0" diff --git a/bbs_wasm/READMD.md b/bbs_wasm/READMD.md new file mode 100644 index 0000000..5ab0277 --- /dev/null +++ b/bbs_wasm/READMD.md @@ -0,0 +1 @@ +> wasm-pack builde --target web \ No newline at end of file diff --git a/bbs_wasm/html/test.html b/bbs_wasm/html/test.html index 59496c9..8aa9087 100644 --- a/bbs_wasm/html/test.html +++ b/bbs_wasm/html/test.html @@ -13,10 +13,12 @@

BBS Wasm Test

init_panic_hook, setup_debug, keygen_debug, - sign_debug, + signer_sign_debug, verify, pok_nizk_prove_debug, - pok_nizk_verify + pok_nizk_verify, + user_commit, + user_unblind } from '../pkg/bbs_wasm.js'; async function runTest() { @@ -28,55 +30,75 @@

BBS Wasm Test

console.log("Panic hook initialized successfully"); try { - // 1. Setup - const numMessages = 3; - console.log(`\n=== 1. Setup (L=${numMessages}) ===`); - const params = setup_debug(numMessages); - console.log("Params:", params); + console.log("============================================="); + console.log(" BBS V2 Extended Scheme Pipeline "); + console.log("============================================="); - // 2. KeyGen - console.log("\n=== 2. Key Generation ==="); + // We need 4 public constraints + 2 hidden/semi-hidden (m_null, m_gamma) + const numPublicMessages = 4; + const totalMessages = numPublicMessages + 2; + + // 1. Setup Parameters & Keys + console.log(`\n=== 1. Init (L=${totalMessages}) ===`); + const params = setup_debug(totalMessages); const keys = keygen_debug(); - console.log("Public Key:", keys.pk); - console.log("Private Key:", keys.sk); + console.log("G1", params.G1); + console.log("G2", params.G2); + console.log("Params Vec H:", params.H); + console.log("Signer Public Key:", keys.pk); + console.log("Signer Secret Key:", keys.sk); + + // 2. User Apply (Commit) + console.log("\n=== 2. User Apply (Commit) ==="); + // Using scalar string representations for testing purposes + const m_null = "192837465"; + const m_gamma = "987654321"; + const lambda = "1122334455"; + const publicMessages = ["4528752285148684012564256818462280771918828852546138746521208354411044252822", + "8474321739458138899097646849860765932135651313862925479195387843221486033438", + "1464662865008959075347130388737553683672515905673906344791000457739484483820", + "1464662865008959075347130388737553683672515905673906344791000457739484483820"]; // Constrains + + console.log("User's m_null:", m_null, "| m_gamma:", m_gamma, "| lambda:", lambda); + const userCommitment = user_commit(m_null, m_gamma, lambda, params); + console.log("User Commitment (C2):", userCommitment); - // 3. Create Messages - console.log("\n=== 3. Create Messages ==="); - const messagesObj = [ - "1", - "2", - "3" - ]; - console.log("Messages:", messagesObj); + // 3. Signer Generates Partial Signature + console.log("\n=== 3. Signer Generates Partial Signature ==="); + console.log("Signer checking constraints:", publicMessages); + const partialSig = signer_sign_debug(keys.sk, publicMessages, params, userCommitment); + console.log("Partial Signature:", partialSig); - // 4. Sign - console.log("\n=== 4. Sign ==="); - const sig = sign_debug(messagesObj, params, keys.sk); - console.log("Signature:", sig); + // 4. User Unblinds to Get Full Signature + console.log("\n=== 4. User Unblind ==="); + const fullSig = user_unblind(partialSig, lambda); + console.log("Full Signature:", fullSig); - // 5. Verify - console.log("\n=== 5. Verify ==="); - const isValid = verify(messagesObj, sig, params, keys.pk); - console.log("Verify result:", isValid); + // Optional: Verify the standard full signature + const allMessages = [...publicMessages, m_null, m_gamma]; + const isSigValid = verify(allMessages, fullSig, params, keys.pk); + console.log("Standard Validity Check (Is sig valid?):", isSigValid); - // 6. PoK NIZK Prove (disclose the 1st message, hide the rest) - const disclosedCount = 1; - console.log(`\n=== 6. PoK Prove (Disclosed: ${disclosedCount}) ===`); - const proof = pok_nizk_prove_debug(params, keys.pk, messagesObj, sig, disclosedCount); - console.log("PoK Proof:", proof); + // 5. User Generates NIZK Proof (Prove ownership whilst hiding m_gamma) + console.log("\n=== 5. User Generate NIZK Proof ==="); + // Disclose constraints + m_null + const disclosedCount = numPublicMessages + 1; + const ctx = "LetsVerify"; + const proof = pok_nizk_prove_debug(ctx, params, keys.pk, allMessages, fullSig, disclosedCount); + console.log(`Proof generated. Disclosed ${disclosedCount} messages (m_gamma is hidden).`); + console.log("Proof Object:", proof); - // 7. PoK NIZK Verify - console.log("\n=== 7. PoK Verify ==="); - const disclosedMsgsObj = messagesObj.slice(0, disclosedCount); - console.log("Disclosed Messages:", disclosedMsgsObj); - - const isProofValid = pok_nizk_verify(params, keys.pk, disclosedMsgsObj, proof); + // 6. Verifier Verify NIZK Proof + console.log("\n=== 6. Verifier Validate Proof ==="); + const disclosedMsgs = allMessages.slice(0, disclosedCount); + console.log("Verifier checking disclosed msgs:", disclosedMsgs); + const isProofValid = pok_nizk_verify(ctx, params, keys.pk, disclosedMsgs, proof); console.log("PoK Verify result:", isProofValid); - if (isValid && isProofValid) { - console.log("\n✅ All tests passed!"); + if (isSigValid && isProofValid) { + console.log("\n✅ V2 Scheme all tests passed!"); } else { - console.log("\n❌ Tests failed!"); + console.log("\n❌ V2 Scheme tests failed!"); } } catch (err) { console.error("\n❌ An error occurred:", err); diff --git a/bbs_wasm/src/lib.rs b/bbs_wasm/src/lib.rs index 20e085b..6e90d61 100644 --- a/bbs_wasm/src/lib.rs +++ b/bbs_wasm/src/lib.rs @@ -1,14 +1,19 @@ mod utils; -use ark_std::test_rng; +use ark_std::{test_rng, UniformRand}; use bbs::bbs::{keygen as bbs_keygen, setup as bbs_setup, sign as bbs_sign, verify as bbs_verify}; -use bbs::pok::{nizk_prove_prefix, nizk_verify_prefix}; +use bbs::extend::BBSPlusExtendedScheme; +use bbs::extend_structs::{PartialSignature, UserCommitment}; +use bbs::pok::{nizk_prove_prefix, nizk_verify_prefix, compute_challenge_prefix}; use bbs::structs::{Messages, Params, PrivateKey, PublicKey, Signature}; +use ark_bn254::Fr as Scalar; +use std::str::FromStr; use rand::thread_rng; use serde::Serialize; use serde_wasm_bindgen::{from_value, to_value}; use std::convert::TryInto; use wasm_bindgen::prelude::*; +use web_sys::console; #[wasm_bindgen] pub fn init_panic_hook() { @@ -111,6 +116,10 @@ pub fn verify( #[wasm_bindgen] pub fn pok_nizk_prove( +<<<<<<< HEAD + ctx: String, +======= +>>>>>>> origin/master params_js: JsValue, pk_js: JsValue, messages_js: JsValue, @@ -144,6 +153,10 @@ pub fn pok_nizk_prove( .map_err(|e: String| JsValue::from_str(&e))?; let proof = nizk_prove_prefix( +<<<<<<< HEAD + ctx.as_bytes(), +======= +>>>>>>> origin/master ¶ms, &pk, &messages, @@ -159,6 +172,10 @@ pub fn pok_nizk_prove( #[wasm_bindgen] pub fn pok_nizk_verify( +<<<<<<< HEAD + ctx: String, +======= +>>>>>>> origin/master params_js: JsValue, pk_js: JsValue, disclosed_msgs_js: JsValue, @@ -188,9 +205,98 @@ pub fn pok_nizk_verify( .try_into() .map_err(|e: String| JsValue::from_str(&e))?; - Ok(nizk_verify_prefix(¶ms, &pk, &disclosed_msgs.0, &proof)) + Ok(nizk_verify_prefix(ctx.as_bytes(), ¶ms, &pk, &disclosed_msgs.0, &proof)) } +#[wasm_bindgen] +pub fn user_commit( + m_null_str: String, + m_gamma_str: String, + lambda_str: String, + params_js: JsValue, +) -> Result { + let params_dto: utils::ParamsJson = + from_value(params_js).map_err(|e| JsValue::from_str(&e.to_string()))?; + let params: Params = (¶ms_dto) + .try_into() + .map_err(|e: String| JsValue::from_str(&e))?; + + let m_null = Scalar::from_str(&m_null_str).map_err(|_| JsValue::from_str("Invalid m_null"))?; + let m_gamma = Scalar::from_str(&m_gamma_str).map_err(|_| JsValue::from_str("Invalid m_gamma"))?; + let lambda = Scalar::from_str(&lambda_str).map_err(|_| JsValue::from_str("Invalid lambda"))?; + + let commitment = BBSPlusExtendedScheme::user_commit(&m_null, &m_gamma, &lambda, ¶ms.H); + + let commit_dto: utils::UserCommitmentJson = (&commitment).into(); + to_value(&commit_dto).map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen] +pub fn signer_sign( + sk_js: JsValue, + messages_js: JsValue, + params_js: JsValue, + user_commit_js: JsValue, +) -> Result { + let mut rng = thread_rng(); + + let sk_dto: utils::PrivateKeyJson = + from_value(sk_js).map_err(|e| JsValue::from_str(&e.to_string()))?; + let sk: PrivateKey = (&sk_dto) + .try_into() + .map_err(|e: String| JsValue::from_str(&e))?; + + let msgs_dto: utils::MessagesJson = + from_value(messages_js).map_err(|e| JsValue::from_str(&e.to_string()))?; + let messages: Messages = (&msgs_dto) + .try_into() + .map_err(|e: String| JsValue::from_str(&e))?; + + let params_dto: utils::ParamsJson = + from_value(params_js).map_err(|e| JsValue::from_str(&e.to_string()))?; + let params: Params = (¶ms_dto) + .try_into() + .map_err(|e: String| JsValue::from_str(&e))?; + + let commit_dto: utils::UserCommitmentJson = + from_value(user_commit_js).map_err(|e| JsValue::from_str(&e.to_string()))?; + let commit: UserCommitment = (&commit_dto) + .try_into() + .map_err(|e: String| JsValue::from_str(&e))?; + + let partial_sig = BBSPlusExtendedScheme::signer_sign( + &mut rng, + &sk.x, + &messages.0, + ¶ms.H, + ¶ms.G1, + &commit.C2, + ); + + let partial_sig_dto: utils::PartialSignatureJson = (&partial_sig).into(); + to_value(&partial_sig_dto).map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen] +pub fn user_unblind( + partial_sig_js: JsValue, + lambda_str: String, +) -> Result { + let partial_sig_dto: utils::PartialSignatureJson = + from_value(partial_sig_js).map_err(|e| JsValue::from_str(&e.to_string()))?; + let partial_sig: PartialSignature = (&partial_sig_dto) + .try_into() + .map_err(|e: String| JsValue::from_str(&e))?; + + let lambda = Scalar::from_str(&lambda_str).map_err(|_| JsValue::from_str("Invalid lambda"))?; + + let full_sig = BBSPlusExtendedScheme::user_unblind(&partial_sig, &lambda); + + let sig_dto: utils::SignatureJson = (&full_sig).into(); + to_value(&sig_dto).map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// debug only, gen fixed vec #[wasm_bindgen] pub fn setup_debug(l: usize) -> Result { let mut rng = test_rng(); @@ -200,6 +306,7 @@ pub fn setup_debug(l: usize) -> Result { to_value(¶ms_dto).map_err(|e| JsValue::from_str(&e.to_string())) } +/// debug only, gen fixed pk/sk #[wasm_bindgen] pub fn keygen_debug() -> Result { let mut rng = test_rng(); @@ -216,6 +323,7 @@ pub fn keygen_debug() -> Result { to_value(&keys).map_err(|e| JsValue::from_str(&e.to_string())) } +/// debug only, gen sig with test_rng #[wasm_bindgen] pub fn sign_debug( messages_js: JsValue, @@ -251,6 +359,7 @@ pub fn sign_debug( #[wasm_bindgen] pub fn pok_nizk_prove_debug( + ctx: String, params_js: JsValue, pk_js: JsValue, messages_js: JsValue, @@ -284,6 +393,7 @@ pub fn pok_nizk_prove_debug( .map_err(|e: String| JsValue::from_str(&e))?; let proof = nizk_prove_prefix( + ctx.as_bytes(), ¶ms, &pk, &messages, @@ -293,6 +403,70 @@ pub fn pok_nizk_prove_debug( ) .map_err(|e| JsValue::from_str(&format!("NIZK Prove failed: {}", e)))?; + let challenge = compute_challenge_prefix( + ctx.as_bytes(), + &pk, + &messages.0[0..disclosed_count], + &proof.A_bar, + &proof.B_bar, + &proof.U, + ); + console::log_1(&format!("Challenge: {}", challenge).into()); + let proof_dto: utils::NonInteractiveProofPrefixJson = (&proof).into(); to_value(&proof_dto).map_err(|e| JsValue::from_str(&e.to_string())) } + +#[wasm_bindgen] +pub fn signer_sign_debug( + sk_js: JsValue, + messages_js: JsValue, + params_js: JsValue, + user_commit_js: JsValue, +) -> Result { + let mut rng = test_rng(); + + let sk_dto: utils::PrivateKeyJson = + from_value(sk_js).map_err(|e| JsValue::from_str(&e.to_string()))?; + let sk: PrivateKey = (&sk_dto) + .try_into() + .map_err(|e: String| JsValue::from_str(&e))?; + + let msgs_dto: utils::MessagesJson = + from_value(messages_js).map_err(|e| JsValue::from_str(&e.to_string()))?; + let messages: Messages = (&msgs_dto) + .try_into() + .map_err(|e: String| JsValue::from_str(&e))?; + + let params_dto: utils::ParamsJson = + from_value(params_js).map_err(|e| JsValue::from_str(&e.to_string()))?; + let params: Params = (¶ms_dto) + .try_into() + .map_err(|e: String| JsValue::from_str(&e))?; + + let commit_dto: utils::UserCommitmentJson = + from_value(user_commit_js).map_err(|e| JsValue::from_str(&e.to_string()))?; + let commit: UserCommitment = (&commit_dto) + .try_into() + .map_err(|e: String| JsValue::from_str(&e))?; + + let partial_sig = BBSPlusExtendedScheme::signer_sign( + &mut rng, + &sk.x, + &messages.0, + ¶ms.H, + ¶ms.G1, + &commit.C2, + ); + + let partial_sig_dto: utils::PartialSignatureJson = (&partial_sig).into(); + to_value(&partial_sig_dto).map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// Sample random Scalar +#[wasm_bindgen] +pub fn sample_random_scalar() -> String { + let mut rng = thread_rng(); + let scalar = Scalar::rand(&mut rng); + scalar.to_string() +} diff --git a/bbs_wasm/src/utils.rs b/bbs_wasm/src/utils.rs index da713a3..611c708 100644 --- a/bbs_wasm/src/utils.rs +++ b/bbs_wasm/src/utils.rs @@ -37,6 +37,49 @@ pub fn g1_to_json(p: &G1) -> G1Json { } } +pub fn g1_from_json(p: &G1Json) -> Result { + let x = Fq::from_str(&p.x).map_err(|_| format!("Invalid x: {}", p.x))?; + let y = Fq::from_str(&p.y).map_err(|_| format!("Invalid y: {}", p.y))?; + Ok(G1::new_unchecked(x, y)) +#![allow(non_snake_case, dead_code)] + +use ark_bn254::{Fq, Fq2, Fr as Scalar, G1Affine as G1, G2Affine as G2}; +use bbs::structs::{Messages, Params, PrivateKey, PublicKey, Signature}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + console_error_panic_hook::set_once(); +} + +#[derive(Serialize, Deserialize)] +pub struct G1Json { + pub x: String, + pub y: String, +} + +#[derive(Serialize, Deserialize)] +pub struct Fq2Json { + pub c0: String, + pub c1: String, +} + +#[derive(Serialize, Deserialize)] +pub struct G2Json { + pub x: Fq2Json, + pub y: Fq2Json, +} + +pub fn g1_to_json(p: &G1) -> G1Json { + G1Json { + x: p.x.to_string(), + y: p.y.to_string(), + } +} + pub fn g1_from_json(p: &G1Json) -> Result { let x = Fq::from_str(&p.x).map_err(|_| format!("Invalid x: {}", p.x))?; let y = Fq::from_str(&p.y).map_err(|_| format!("Invalid y: {}", p.y))?; @@ -235,3 +278,55 @@ impl TryFrom<&NonInteractiveProofPrefixJson> for NonInteractiveProofPrefix { }) } } + +use bbs::extend_structs::{UserCommitment, PartialSignature}; + +#[derive(Serialize, Deserialize)] +pub struct UserCommitmentJson { + pub C2: G1Json, +} + +impl From<&UserCommitment> for UserCommitmentJson { + fn from(commit: &UserCommitment) -> Self { + Self { + C2: g1_to_json(&commit.C2), + } + } +} + +impl TryFrom<&UserCommitmentJson> for UserCommitment { + type Error = String; + fn try_from(dto: &UserCommitmentJson) -> Result { + Ok(UserCommitment { + C2: g1_from_json(&dto.C2)?, + }) + } +} + +#[derive(Serialize, Deserialize)] +pub struct PartialSignatureJson { + pub A1: G1Json, + pub A2_prime: G1Json, + pub e: String, +} + +impl From<&PartialSignature> for PartialSignatureJson { + fn from(sig: &PartialSignature) -> Self { + Self { + A1: g1_to_json(&sig.A1), + A2_prime: g1_to_json(&sig.A2_prime), + e: sig.e.to_string(), + } + } +} + +impl TryFrom<&PartialSignatureJson> for PartialSignature { + type Error = String; + fn try_from(dto: &PartialSignatureJson) -> Result { + Ok(PartialSignature { + A1: g1_from_json(&dto.A1)?, + A2_prime: g1_from_json(&dto.A2_prime)?, + e: Scalar::from_str(&dto.e).map_err(|_| "Invalid e")?, + }) + } +}