diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3127f5d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +members = [ + "h2s2", +] + + +[workspace.dependencies] +ark-std = {version ="0.5.0", features = ["parallel"]} +ark-ec = {version = "0.5.0", features = ["parallel"]} +ark-ff = { version = "0.5", features = [ "parallel" ] } +blake2 = "0.10.6" +digest = "0.10.7" +rayon = "1.1" +ark-bn254 = "0.5.0" diff --git a/h2s2/Cargo.toml b/h2s2/Cargo.toml new file mode 100644 index 0000000..363014f --- /dev/null +++ b/h2s2/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "h2s2" +version = "0.1.0" +authors = [ + "Bryan Cole ", + "Severiano Sisneros ", + "Alexis Asseman ", + "Tomasz Kornuta ", + "Pedro Bufulin ", +] +license = "Apache-2.0" +description = "" +edition = "2021" +keywords = ["holographic", "homomorphic", "signature-scheme"] +catagories = ["cryptography", "cryptography::cryptocurrencies"] + +[dependencies] +ark-ec = {workspace = true} +ark-std = { workspace = true} +ark-ff = { workspace = true} +ark-bn254 = { workspace = true} +blake2 = {workspace = true} +rayon = { workspace = true} +digest = { workspace = true} +once_cell = "1.20.2" +ark-bls12-381 = "0.5.0" diff --git a/h2s2/src/holographic_homomorphic_signature_scheme.rs b/h2s2/src/holographic_homomorphic_signature_scheme.rs new file mode 100644 index 0000000..a1fe813 --- /dev/null +++ b/h2s2/src/holographic_homomorphic_signature_scheme.rs @@ -0,0 +1,69 @@ +use ark_ec::pairing::Pairing; +use ark_std::rand::Rng; +use digest::{Digest, FixedOutputReset}; +use std::error::Error; + +pub trait HolographicHomomorphicSignatureScheme< + P: Pairing, + D: FixedOutputReset + Digest + Send + Sync, +> +{ + type Parameters; + type PublicKey; + type SecretKey; + type Signature; + type Message; + type Weight; + type AggregatedSignature; + type Projective; + + /// Generate one G2 element and `n` G1 elements + fn setup(n: usize) -> Result>; + + /// Generate hash aggregate (H_a) with `tag` and `n` lanes, and a + /// allocation_id as a ScalarField + fn precompute( + pp: &Self::Parameters, + rng: &mut R, + n: usize, + ) -> Result<(P::G1, P::ScalarField), Box>; + + /// Generate private and public receipt keys using `pp` parameters from `setup` + fn keygen( + pp: &Self::Parameters, + rng: &mut R, + ) -> Result<(Self::PublicKey, Self::SecretKey), Box>; + + /// Sign `message` with `tag` at `index` + fn sign( + pp: &Self::Parameters, + tag: P::ScalarField, + index: usize, + message: Self::Message, + ) -> Result>; + + /// Verify a single `signature` matches `message` with `tag` at `index` using `pp` parameter and `pk` public key + /// TODO: index should be restricted to a number from 1 to N (max number of lanes) + fn verify( + pp: &Self::Parameters, + tag: P::ScalarField, + index: usize, + message: &Self::Message, + signature: &Self::Signature, + ) -> Result>; + + /// Verify aggregate `signature` matches `message_aggregate` + /// contained in [`AggregatedSignature`] with `tag` and `hash_aggregate` using `pp` parameter and `pk` public key + fn verify_aggregate( + pp: &Self::Parameters, + // tag: &[u8], + hash_aggregate: &P::G1, + signature: &Self::AggregatedSignature, + ) -> Result>; + + /// Aggregate `signatures` with `weights` + fn evaluate( + signatures: &[Self::Signature], + weights: &[Self::Weight], + ) -> Result>; +} diff --git a/h2s2/src/lib.rs b/h2s2/src/lib.rs new file mode 100644 index 0000000..23c9e0d --- /dev/null +++ b/h2s2/src/lib.rs @@ -0,0 +1,2 @@ +pub mod holographic_homomorphic_signature_scheme; +pub mod ncs; diff --git a/h2s2/src/ncs.rs b/h2s2/src/ncs.rs new file mode 100644 index 0000000..0aae80c --- /dev/null +++ b/h2s2/src/ncs.rs @@ -0,0 +1,469 @@ +use std::ops::{Add, Mul, MulAssign}; +use std::{error::Error, marker::PhantomData}; + +use crate::holographic_homomorphic_signature_scheme::HolographicHomomorphicSignatureScheme; +use ark_ec::hashing::curve_maps::wb::WBConfig; +// use ark_bls12_381::Bn254; +use ark_ec::hashing::map_to_curve_hasher::{MapToCurve, MapToCurveBasedHasher}; +use ark_ec::hashing::HashToCurveError; +use ark_ec::pairing::Pairing; +use ark_ec::short_weierstrass::Projective; +use ark_ec::PrimeGroup; +use ark_ec::{AffineRepr, CurveGroup}; +use ark_ff::field_hashers::{DefaultFieldHasher, HashToField}; +use ark_ff::PrimeField; +use ark_ff::{BigInteger, UniformRand, Zero}; +use ark_std::rand::Rng; +use blake2::Blake2b512; +use digest::{Digest, FixedOutputReset}; // Use 512-bit Blake2b for digest + +use ark_bls12_381::{g1, Config, G1Projective}; +use ark_ec::hashing::{curve_maps::wb::WBMap, HashToCurve}; + +fn hash_to_g1(message_data: Vec) -> P::G1Affine { + let mut g1_point: Option = None; + let mut counter = 0; + while g1_point.is_some() == false { + let mut tmp_message = message_data.clone(); + tmp_message.push(counter); + let hash_out = D::digest(&tmp_message); + g1_point = P::G1Affine::from_random_bytes(&hash_out); + counter += 1; + } + g1_point.unwrap() +} +// fn hash_to_g1(message_data: Vec) -> P::G1Affine +// where +// P: Pairing, +// <

::G1 as CurveGroup>::Config: WBConfig, +// D: digest::FixedOutputReset + Default + Clone, +// { +// // Initialize the hash-to-curve hasher for G1 using the dynamic configuration +// let hasher = MapToCurveBasedHasher::< +// Projective<::Config>, // Dynamically resolved G1 curve configuration +// DefaultFieldHasher, // Hash-to-field +// WBMap<::Config>, // Map-to-curve +// >::new(&[1]) // Domain separation tag or personalization +// .expect("Failed to create hash-to-curve hasher"); + +// // Hash the input data to a G1 affine point +// let point = hasher +// .hash(&message_data) +// .expect("Failed to hash the message to the curve"); + +// // Cast the result to P::G1Affine +// point +// } + +// Use SWUMap for BN254 G1 curve mapping +// type MyMapToCurve = SWUMap<::G1>; + +pub struct NCS { + _pairing: PhantomData

, + _hash: PhantomData, +} + +pub struct H2S2Parameters { + pub g1_generators: Vec, + pub g2_generator: P::G2, + // this public_key is the `u` param in the notes. + // both the indexer and verifier need it. The secret key + // remains with the gateway + pub public_key: P::G2, + pub secret_key: Option, + pub max_lanes: usize, +} + +#[derive(Clone)] +pub struct Signature { + pub signature: P::G1, + pub lane_id: usize, + pub value: P::ScalarField, +} + +#[derive(Clone)] +pub struct AggregatedSignature { + pub signature: P::G1, + pub total_value: P::ScalarField, +} + +#[derive(Clone)] +pub struct AllocationParameters { + pub allocation_hash: P::G1, + pub allocation_id: P::ScalarField, +} + +impl + HolographicHomomorphicSignatureScheme for NCS +{ + type Parameters = H2S2Parameters

; + type PublicKey = P::G2; + type SecretKey = P::ScalarField; + type Signature = Signature

; + type Message = P::ScalarField; + type Weight = usize; + type AggregatedSignature = AggregatedSignature

; + type Projective = G1Projective; + + // n represents the max_lanes amount + fn setup(n: usize) -> Result> { + // Use the hardcoded G2 generator from the Pairing trait + let g2_generator = P::G2::generator(); + + // Generate a deterministic set of G1 generators based on the hardcoded G1 generator + let g1_base_generator = P::G1::generator(); + let g1_generators: Vec = (0..=n) + .map(|i| g1_base_generator.mul(&P::ScalarField::from(i as u64))) + .collect(); + + // Initialize parameters without secret/public keys + let pp: H2S2Parameters

= H2S2Parameters { + g1_generators, + g2_generator, + secret_key: Some(P::ScalarField::zero()), // Temporary placeholder + public_key: P::G2::zero(), // Temporary placeholder + max_lanes: n, + }; + + Ok(pp) + } + + //TODO: allocationn_ids (tag in this case) must be unpredictable + // some random value has to be appended during initialization, prior + // to the precompute in this function + fn precompute( + _pp: &Self::Parameters, + rng: &mut R, + n: usize, + ) -> Result<(P::G1, P::ScalarField), Box> { + let allocation_id = P::ScalarField::rand(rng); + let hash_vec = (0..n) + .into_iter() + .map(|lane_id| { + let mut message_data = allocation_id.into_bigint().to_bytes_be(); + message_data.append(&mut lane_id.to_be_bytes().to_vec()); + hash_to_g1::(message_data) + }) + .collect::>(); + let mut allocation_hash = P::G1::zero(); + for hash_val in hash_vec { + allocation_hash += hash_val; + } + Ok((allocation_hash, allocation_id)) + } + + fn keygen( + pp: &Self::Parameters, + rng: &mut R, + ) -> Result<(Self::PublicKey, Self::SecretKey), Box> { + // Generate the private key as a random scalar + let secret_key = P::ScalarField::rand(rng); + + // Compute the public key as the secret_key multiplied by the G2 generator + let public_key = pp.g2_generator.mul(secret_key); + + Ok((public_key, secret_key)) + } + + fn sign( + pp: &Self::Parameters, + tag: P::ScalarField, + index: usize, + message:

::ScalarField, + ) -> Result> { + let mut lane_data = tag.into_bigint().to_bytes_be(); + lane_data.append(&mut index.to_be_bytes().to_vec()); + let lane_point = hash_to_g1::(lane_data); + + let mut value_point = pp.g1_generators[0].clone(); + value_point.mul_assign(message); + + let message_point = lane_point.into_group() + value_point; + let mut signature = message_point.clone(); + signature.mul_assign(pp.secret_key.unwrap()); + Ok(Signature { + signature, + lane_id: index, + value: message, + }) + } + + fn verify( + pp: &Self::Parameters, + tag: P::ScalarField, + index: usize, + message: &Self::Message, + signature: &Self::Signature, + ) -> Result> { + let mut lane_data = tag.into_bigint().to_bytes_be(); + lane_data.append(&mut index.to_be_bytes().to_vec()); + let lane_point = hash_to_g1::(lane_data); + + let mut value_point = pp.g1_generators[0].clone(); + value_point.mul_assign(message); + + let rhs_pairing = P::pairing(lane_point.into_group() + value_point, pp.public_key); + let lhs_pairing = P::pairing(signature.signature, pp.g2_generator); + Ok(lhs_pairing == rhs_pairing) + } + + fn verify_aggregate( + pp: &Self::Parameters, + hash_aggregate: &P::G1, + signature: &Self::AggregatedSignature, + ) -> Result> { + let lane_point = hash_aggregate; + let mut value_point = pp.g1_generators[0].clone(); + value_point.mul_assign(signature.total_value); + + let rhs_pairing = P::pairing(lane_point.add(value_point), pp.public_key); + let lhs_pairing = P::pairing(signature.signature, pp.g2_generator); + Ok(lhs_pairing == rhs_pairing) + } + + fn evaluate( + signatures: &[Self::Signature], + weights: &[Self::Weight], + ) -> Result> { + // Ensure that the lengths of the inputs match + if signatures.len() != weights.len() { + return Err("Signatures and weights must have the same length".into()); + } + + let mut aggregate_signature = P::G1::zero(); + let mut total_value = P::ScalarField::zero(); + + for (sig, &wt) in signatures.iter().zip(weights.iter()) { + let weight_scalar = P::ScalarField::from(wt as u64); + aggregate_signature += sig.signature.mul(weight_scalar); + total_value += weight_scalar.mul(sig.value); + } + + Ok(AggregatedSignature { + signature: aggregate_signature, + total_value, + }) + } +} + +mod tests { + use super::*; + use ark_bls12_381::{g1, Config}; + use ark_ec::{bls12::Bls12, hashing::curve_maps::wb::WBConfig}; + //we could also use the ark_bls12_381 curve which was intended to substitute this one: + //https://docs.rs/ark-bls12-381/latest/ark_bls12_381/ + // Ethereum is reviewing using it: + // https://eips.ethereum.org/EIPS/eip-2537 + // use ark_bls12_381::Bn254; + use ark_std::test_rng; + use blake2::Blake2b512; // Use 512-bit Blake2b for digest + use once_cell::sync::Lazy; + type Curve = ark_bls12_381::Bls12_381; + type Hasher = blake2::Blake2b512; + static N: usize = 10; // Define the number of generators + + static PARAMS: Lazy> = Lazy::new(|| { + let mut rng = test_rng(); + + let mut params = NCS::::setup(N).expect("Setup failed"); + + // Generate the secret and public keys using keygen + let (pk, sk) = NCS::::keygen(¶ms, &mut rng).expect("Keygen failed"); + + params.secret_key = Some(sk); + params.public_key = pk; + params + }); + + // fn hash_message_to_g1( + // message: &[u8], + // domain: &[u8], + // ) -> Result<::G1Affine, HashToCurveError> { + // // Create a MapToCurveBasedHasher using the chosen hash-to-field and map-to-curve + // let test_wb_to_curve_hasher = MapToCurveBasedHasher::< + // Projective, + // DefaultFieldHasher, + // WBMap, + // >::new(&[1]) + // .unwrap(); + + // // Hash the message into a curve point using the standardized method + // let point = hasher.hash(message)?; + // Ok(point) + // } + + #[test] + fn test_setup_and_keygen() { + // Use the correct WBConfig implementation for G1 + + let test_wb_to_curve_hasher = MapToCurveBasedHasher::< + Projective, // G1 curve configuration + DefaultFieldHasher, // Hash-to-field + WBMap, // Map-to-curve + >::new(&[1]) // Domain separation tag or personalization + .unwrap(); + + let hash_result = test_wb_to_curve_hasher + .hash(b"message") + .expect("fail to hash the string to curve"); + + let mut rng = test_rng(); + let n = 10; + + let params = NCS::::setup(n).expect("Setup failed"); + + let (pk, sk) = NCS::::keygen(¶ms, &mut rng).expect("Keygen failed"); + + assert_eq!( + params.g1_generators.len(), + n + 1, + "Incorrect number of G1 generators" + ); + assert_eq!(params.max_lanes, n, "Max lanes value mismatch"); + + // Verify the public key matches the secret key and G2 generator relationship + let calculated_public_key = params.g2_generator.mul(sk); + assert_eq!( + calculated_public_key, pk, + "Public key does not match the calculated value from secret key and G2 generator" + ); + + println!("Setup and Keygen tests passed!"); + } + + #[test] + fn test_precompute() { + let params = &*PARAMS; + let mut rng = test_rng(); + let (hash_aggregate, alloc_id) = + NCS::::precompute(¶ms, &mut rng, N).expect("Precompute failed"); + + println!("Precomputed Hash Aggregate: {:?}", hash_aggregate); + println!("allocation_id {:?}", alloc_id); + } + + #[test] + fn test_sign_and_verify() { + let mut rng = test_rng(); + let params = &*PARAMS; + + // Precompute the hash aggregate and allocation ID + let (_, allocation_id) = + NCS::::precompute(¶ms, &mut rng, N).expect("Precompute failed"); + + // Generate messages for each lane/index + let messages: Vec = + (0..N).map(|_| ark_bls12_381::Fr::rand(&mut rng)).collect(); + + // Iterate through indices and sign each message + for index in 0..N { + // Sign the message with the current index + let signature = + NCS::::sign(¶ms, allocation_id, index, messages[index]) + .expect("Sign failed"); + + // Verify the signature with the same index + let is_valid = NCS::::verify( + ¶ms, + allocation_id, + index, + &messages[index], + &signature, + ) + .expect("Verify failed"); + + assert!( + is_valid, + "Signature verification failed for index {}!", + index + ); + } + + println!("All signatures successfully verified for indices 0..{}!", N); + } + + #[test] + fn test_aggregate() { + let mut rng = test_rng(); + let params = &*PARAMS; + + // Generate random messages for each lane/index + let messages: Vec = + (0..N).map(|_| ark_bls12_381::Fr::rand(&mut rng)).collect(); + + // Precompute the hash aggregate and allocation ID + let (hash_aggregate, allocation_id) = + NCS::::precompute(¶ms, &mut rng, N).expect("Precompute failed"); + + // Generate individual signatures for each message + let mut signatures: Vec<_> = (0..N) + .map(|index| { + NCS::::sign(¶ms, allocation_id, index, messages[index]) + .expect("Sign failed") + }) + .collect(); + + // Verify each individual signature + for (index, signature) in signatures.iter().enumerate() { + let is_valid = NCS::::verify( + ¶ms, + allocation_id, + index, + &messages[index], + signature, + ) + .expect("Verify failed"); + assert!(is_valid, "Invalid signature for index {}!", index); + } + + // Generate weights (all set to 1 for uniform aggregation) + let weights: Vec = vec![1; N]; + + // Aggregate the signatures + let aggregated_signature = + NCS::::evaluate(&signatures, &weights).expect("Evaluate failed"); + + // Verify the aggregated signature + let is_valid = + NCS::::verify_aggregate(¶ms, &hash_aggregate, &aggregated_signature) + .expect("Verify failed"); + + assert!( + is_valid, + "Aggregated signature verification failed for the entire set of messages!" + ); + + println!( + "Aggregated signature successfully verified for all {} messages!", + N + ); + + // this next signature aggregation test should fail + // Introduce a duplicate signature to simulate a lying indexer + let random_index = rng.gen_range(0..N); + let duplicate_signature = signatures[random_index].clone(); + signatures.push(duplicate_signature); + // adds 1 more weight to match the added signature + let weights: Vec = vec![1; N + 1]; + + // Aggregate the signatures, including the duplicate + let tampered_aggregate_signature = + NCS::::evaluate(&signatures, &weights).expect("Evaluate failed"); + + // Verify the aggregated signature with the tampered signature table + let is_valid = NCS::::verify_aggregate( + ¶ms, + &hash_aggregate, + &tampered_aggregate_signature, + ) + .expect("Verify failed"); + + // Assert that verification fails + assert!( + !is_valid, + "Aggregated signature verification should fail with a tampered signature table!" + ); + + println!("Tampered aggregated signature verification correctly failed as expected!"); + } +}