arnaucube
arnaucube/plonky2-crypto-gadgetsPublic Self-hosted repository
Add hashed ElGamal
Ahmad root@ahmadafuni.com
Fri, 20 Feb 2026 05:52:08 +1000
commit

fdff3c5a468990c8b02af93b43d3da1e3f694a9b

parent

9de19bea537b5bdb4d867c2ec61ccfaad0bf204d

M README.md → README.md

@@ -5,4 +5,5 @@

- [aes-gcm](/aes-gcm): Plonky2 circuits for AES & AES-GCM encryption system as described at - AES: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197-upd1.pdf - AES-GCM (Galois Counter Mode): https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf +- [ecgfp5](/ecgfp5): Plonky2 circuits for ElGamal and (a finite field analogue of) hashed ElGamal cryptosystems. - [feistel](/feistel): a Plonky2 library for a finite field analogue of the Feistel cipher as described [here](https://en.wikipedia.org/wiki/Feistel_cipher).
A ecgfp5/src/circuit.rs

@@ -0,0 +1,99 @@

+use plonky2::{ + field::extension::Extendable, hash::hash_types::RichField, iop::witness::PartialWitness, + plonk::circuit_builder::CircuitBuilder, +}; +use pod2::backends::plonky2::{ + basetypes::{D, F}, + primitives::ec::{ + bits::{BigUInt320Target, CircuitBuilderBits}, + curve::{CircuitBuilderElliptic, Point, PointTarget, WitnessWriteCurve}, + }, +}; + +use crate::ECGFP5SecretKey; + +/// EcGFp5 ecret key target type +pub struct ECGFP5SecretKeyTarget(pub BigUInt320Target); + +/// EcGFp5 public key (point) type alias +pub type PublicKeyTarget = PointTarget; + +pub trait CircuitBuilderECGFP5PublicKey<F: RichField + Extendable<D>, const D: usize> { + /// Adds secret key target + fn add_secret_key(&mut self) -> ECGFP5SecretKeyTarget; + + /// Derives public key from secret key + fn public_key(&mut self, sk: &ECGFP5SecretKeyTarget) -> PublicKeyTarget; +} + +impl CircuitBuilderECGFP5PublicKey<F, D> for CircuitBuilder<F, D> { + fn add_secret_key(&mut self) -> ECGFP5SecretKeyTarget { + ECGFP5SecretKeyTarget(self.add_virtual_biguint320_target()) + } + + fn public_key(&mut self, sk: &ECGFP5SecretKeyTarget) -> PublicKeyTarget { + let generator = self.constant_point(Point::generator()); + self.multiply_point(&sk.0.bits, &generator) + } +} + +pub trait PartialWitnessECGFP5PublicKey { + /// Sets EcGFp5 secret key witnesses + fn set_secret_key_target( + &mut self, + target: &ECGFP5SecretKeyTarget, + value: &ECGFP5SecretKey, + ) -> anyhow::Result<()>; +} + +impl PartialWitnessECGFP5PublicKey for PartialWitness<F> { + fn set_secret_key_target( + &mut self, + target: &ECGFP5SecretKeyTarget, + value: &ECGFP5SecretKey, + ) -> anyhow::Result<()> { + self.set_biguint320_target(&target.0, &value.0) + } +} + +#[cfg(test)] +mod tests { + use plonky2::{ + iop::witness::PartialWitness, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, + }; + use pod2::backends::plonky2::{ + basetypes::{D, F}, + primitives::ec::curve::WitnessWriteCurve, + }; + + use crate::{ + ECGFP5SecretKey, + circuit::{CircuitBuilderECGFP5PublicKey, PartialWitnessECGFP5PublicKey}, + }; + + #[test] + fn public_key_calculation() -> anyhow::Result<()> { + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let sk_target = builder.add_secret_key(); + let pk_target = builder.public_key(&sk_target); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + (0..10).try_for_each(|_| { + let sk = ECGFP5SecretKey::rand(); + let pk = sk.public_key(); + + let mut pw = PartialWitness::new(); + pw.set_secret_key_target(&sk_target, &sk)?; + pw.set_point_target(&pk_target, &pk)?; + + let proof = data.prove(pw)?; + data.verify(proof) + }) + } +}
M ecgfp5/src/elgamal.rs → ecgfp5/src/elgamal.rs

@@ -1,38 +1,11 @@

-use std::array; - use num::bigint::BigUint; -use num_bigint::RandBigInt; -use plonky2::field::{ - extension::quintic::QuinticExtension, - goldilocks_field::GoldilocksField as F, - types::{Field, Field64}, -}; use pod2::backends::plonky2::primitives::ec::curve::{GROUP_ORDER, Point}; -use rand::{Rng, rngs::OsRng}; -pub mod circuit; - -// Message encoding constants -pub const BITS_PER_LIMB: usize = 32; -pub const TOTAL_BITS: usize = 5 * BITS_PER_LIMB; - -pub struct ECGFP5SecretKey(pub BigUint); - -impl ECGFP5SecretKey { - pub fn new(s: BigUint) -> Self { - assert!(&s < &GROUP_ORDER); - Self(s) - } - - pub fn rand() -> Self { - Self(OsRng.gen_biguint_below(&GROUP_ORDER)) - } +use crate::ECGFP5SecretKey; - pub fn public_key(&self) -> Point { - &self.0 * Point::generator() - } -} +pub mod circuit; +/// ElGamal encryption on EcGFp5 pub fn elgamal_encrypt(pk: Point, nonce: &BigUint, msg: Point) -> (Point, Point) { assert!(nonce < &GROUP_ORDER); let nonce_times_gen = nonce * Point::generator();

@@ -40,81 +13,19 @@ let nonce_times_pk = nonce * pk;

(nonce_times_gen, msg + nonce_times_pk) } +/// ElGamal decryption on EcGFp5 pub fn elgamal_decrypt(sk: ECGFP5SecretKey, ct: (Point, Point)) -> Point { let nonce_times_pk = &sk.0 * ct.0; ct.1 + nonce_times_pk.inverse() } -/// Convert `TOTAL_BITS`-bit biguint to a point by padding with random bits. -pub fn biguint_to_point(x: &BigUint) -> Point { - let num_bits = x.bits() as usize; - assert!(num_bits <= TOTAL_BITS); - - let bs_bits = [x.to_radix_le(2), vec![0; TOTAL_BITS - num_bits]].concat(); - let bs_fields: Vec<F> = bs_bits - .chunks(BITS_PER_LIMB) - .map(|bits| { - F::from_noncanonical_biguint( - BigUint::from_radix_le(bits, 2).expect("Should be a valid bit decomposition"), - ) - }) - .collect(); - - std::iter::repeat_with(|| { - let ec_fields: [_; 5] = array::from_fn(|i| { - let random_element: u64 = OsRng.gen_range(0..F::ORDER - bs_fields[i].0); - F::from_canonical_u64( - bs_fields[i].0 + ((random_element >> BITS_PER_LIMB) << BITS_PER_LIMB), - ) - }); - QuinticExtension(ec_fields) - }) - .find_map(|w| Point::decompress_into_subgroup(&w).ok()) - .expect("Should be a match!") -} - -pub fn point_to_biguint(p: Point) -> BigUint { - let msg_bits: Vec<_> = p - .compress_from_subgroup() - .unwrap() - .0 - .into_iter() - .flat_map(|f| { - BigUint::from(f.0) - .to_radix_le(2) - .into_iter() - .take(BITS_PER_LIMB) - .collect::<Vec<_>>() - }) - .collect(); - BigUint::from_bytes_le( - &BigUint::from_radix_le(&msg_bits, 2) - .expect("Should be a valid bit slice.") - .to_radix_le(256), - ) -} - #[cfg(test)] mod tests { - use num::BigUint; use num_bigint::RandBigInt; use pod2::backends::plonky2::primitives::ec::curve::{GROUP_ORDER, Point}; use rand::rngs::OsRng; - use crate::elgamal::{ - ECGFP5SecretKey, TOTAL_BITS, biguint_to_point, elgamal_decrypt, elgamal_encrypt, - point_to_biguint, - }; - - #[test] - fn msg_encoding_roundtrip() { - (0..100).for_each(|_| { - let test_val = OsRng.gen_biguint_below(&BigUint::from(2u32).pow(TOTAL_BITS as u32)); - let pt = biguint_to_point(&test_val); - let maybe_test_val = point_to_biguint(pt); - assert_eq!(test_val, maybe_test_val); - }) - } + use crate::elgamal::{ECGFP5SecretKey, elgamal_decrypt, elgamal_encrypt}; #[test] fn round_trip_encryption() {
M ecgfp5/src/elgamal/circuit.rs → ecgfp5/src/elgamal/circuit.rs

@@ -1,31 +1,19 @@

use plonky2::{ - field::extension::Extendable, hash::hash_types::RichField, iop::witness::PartialWitness, + field::extension::Extendable, hash::hash_types::RichField, plonk::circuit_builder::CircuitBuilder, }; use pod2::backends::plonky2::{ basetypes::{D, F}, primitives::ec::{ - bits::{BigUInt320Target, CircuitBuilderBits}, - curve::{CircuitBuilderElliptic, Point, PointTarget, WitnessWriteCurve}, + bits::BigUInt320Target, + curve::{CircuitBuilderElliptic, Point, PointTarget}, }, }; -use crate::elgamal::ECGFP5SecretKey; - -pub struct ECGFP5SecretKeyTarget(pub BigUInt320Target); - -pub type PublicKeyTarget = PointTarget; - -pub trait CircuitBuilderECGFP5PublicKey<F: RichField + Extendable<D>, const D: usize> { - /// Adds secret key target - fn add_secret_key(&mut self) -> ECGFP5SecretKeyTarget; - - /// Derives public key from secret key - fn public_key(&mut self, sk: &ECGFP5SecretKeyTarget) -> PublicKeyTarget; -} +use crate::circuit::PublicKeyTarget; pub trait CircuitBuilderElGamal<F: RichField + Extendable<D>, const D: usize> { - /// Encrypts an EcGFp5 point using a public key and nonce. + /// Encrypts an EcGFp5 point according to the ElGamal scheme. fn elgamal_encrypt( &mut self, pk: &PublicKeyTarget,

@@ -34,17 +22,6 @@ msg: &PointTarget,

) -> (PointTarget, PointTarget); } -impl CircuitBuilderECGFP5PublicKey<F, D> for CircuitBuilder<F, D> { - fn add_secret_key(&mut self) -> ECGFP5SecretKeyTarget { - ECGFP5SecretKeyTarget(self.add_virtual_biguint320_target()) - } - - fn public_key(&mut self, sk: &ECGFP5SecretKeyTarget) -> PublicKeyTarget { - let generator = self.constant_point(Point::generator()); - self.multiply_point(&sk.0.bits, &generator) - } -} - impl CircuitBuilderElGamal<F, D> for CircuitBuilder<F, D> { fn elgamal_encrypt( &mut self,

@@ -59,24 +36,6 @@ (nonce_times_gen, self.add_point(msg, &nonce_times_pk))

} } -pub trait PartialWitnessECGFP5PublicKey { - fn set_secret_key_target( - &mut self, - target: &ECGFP5SecretKeyTarget, - value: &ECGFP5SecretKey, - ) -> anyhow::Result<()>; -} - -impl PartialWitnessECGFP5PublicKey for PartialWitness<F> { - fn set_secret_key_target( - &mut self, - target: &ECGFP5SecretKeyTarget, - value: &ECGFP5SecretKey, - ) -> anyhow::Result<()> { - self.set_biguint320_target(&target.0, &value.0) - } -} - #[cfg(test)] mod tests { use num_bigint::RandBigInt;

@@ -96,35 +55,10 @@ },

}; use rand::rngs::OsRng; - use crate::elgamal::{ + use crate::{ ECGFP5SecretKey, - circuit::{ - CircuitBuilderECGFP5PublicKey, CircuitBuilderElGamal, PartialWitnessECGFP5PublicKey, - }, - elgamal_encrypt, + elgamal::{circuit::CircuitBuilderElGamal, elgamal_encrypt}, }; - - #[test] - fn public_key_calculation() -> anyhow::Result<()> { - let config = CircuitConfig::standard_recursion_config(); - let mut builder = CircuitBuilder::<F, D>::new(config); - - let sk_target = builder.add_secret_key(); - let pk_target = builder.public_key(&sk_target); - - let data = builder.build::<PoseidonGoldilocksConfig>(); - (0..10).try_for_each(|_| { - let sk = ECGFP5SecretKey::rand(); - let pk = sk.public_key(); - - let mut pw = PartialWitness::new(); - pw.set_secret_key_target(&sk_target, &sk)?; - pw.set_point_target(&pk_target, &pk)?; - - let proof = data.prove(pw)?; - data.verify(proof) - }) - } #[test] fn elgamal_encryption() -> anyhow::Result<()> {
A ecgfp5/src/hashed_elgamal.rs

@@ -0,0 +1,62 @@

+use std::array; + +use num::bigint::BigUint; +use plonky2::{ + field::goldilocks_field::GoldilocksField as F, + hash::{hashing::hash_n_to_m_no_pad, poseidon::PoseidonPermutation}, +}; +use pod2::backends::plonky2::primitives::ec::curve::{GROUP_ORDER, Point}; + +use crate::ECGFP5SecretKey; + +pub mod circuit; + +/// A variant of 'hashed ElGamal' with addition in place of +/// XORing. Assumes a message of 5 field elements and uses the +/// Poseidon hash function outputting 5 field elements. +pub fn hashed_elgamal_encrypt(pk: Point, nonce: &BigUint, msg: [F; 5]) -> (Point, [F; 5]) { + assert!(nonce < &GROUP_ORDER); + let nonce_times_gen = nonce * Point::generator(); + let nonce_times_pk = nonce * pk; + let h = hash_n_to_m_no_pad::<F, PoseidonPermutation<_>>(&nonce_times_pk.as_fields(), 5); + (nonce_times_gen, array::from_fn(|i| msg[i] + h[i])) +} + +/// Inverse to `hashed_elgamal_encrypt`. +pub fn hashed_elgamal_decrypt(sk: ECGFP5SecretKey, ct: (Point, [F; 5])) -> [F; 5] { + let nonce_times_pk = &sk.0 * ct.0; + let h = hash_n_to_m_no_pad::<F, PoseidonPermutation<_>>(&nonce_times_pk.as_fields(), 5); + array::from_fn(|i| ct.1[i] - h[i]) +} + +#[cfg(test)] +mod tests { + use num_bigint::RandBigInt; + use plonky2::field::{ + goldilocks_field::GoldilocksField as F, + types::{Field, Field64}, + }; + use pod2::backends::plonky2::primitives::ec::curve::GROUP_ORDER; + use rand::{Rng, rngs::OsRng}; + + use crate::{ + ECGFP5SecretKey, + hashed_elgamal::{hashed_elgamal_decrypt, hashed_elgamal_encrypt}, + }; + + #[test] + fn round_trip_encryption() { + (0..100).for_each(|_| { + let sk = ECGFP5SecretKey::rand(); + let pk = sk.public_key(); + + let msg = std::array::from_fn(|_| F::from_canonical_u64(OsRng.gen_range(0..F::ORDER))); + let nonce = OsRng.gen_biguint_below(&GROUP_ORDER); + + let encrypted_msg = hashed_elgamal_encrypt(pk, &nonce, msg); + let decrypted_msg = hashed_elgamal_decrypt(sk, encrypted_msg); + + assert_eq!(msg, decrypted_msg); + }) + } +}
A ecgfp5/src/hashed_elgamal/circuit.rs

@@ -0,0 +1,106 @@

+use std::array; + +use plonky2::{ + field::extension::Extendable, + hash::{hash_types::RichField, poseidon::PoseidonHash}, + iop::target::Target, + plonk::circuit_builder::CircuitBuilder, +}; +use pod2::backends::plonky2::{ + basetypes::{D, F}, + primitives::ec::{ + bits::BigUInt320Target, + curve::{CircuitBuilderElliptic, Point, PointTarget}, + }, +}; + +use crate::circuit::PublicKeyTarget; + +pub trait CircuitBuilderHashedElGamal<F: RichField + Extendable<D>, const D: usize> { + /// Encrypts an EcGFp5 point according to the 'hashed ElGamal ' + /// scheme described in `super::hashed_elgamal_encrypt`. + fn hashed_elgamal_encrypt( + &mut self, + pk: &PublicKeyTarget, + nonce: &BigUInt320Target, + msg: [Target; 5], + ) -> (PointTarget, [Target; 5]); +} + +impl CircuitBuilderHashedElGamal<F, D> for CircuitBuilder<F, D> { + fn hashed_elgamal_encrypt( + &mut self, + pk: &PublicKeyTarget, + nonce: &BigUInt320Target, + msg: [Target; 5], + ) -> (PointTarget, [Target; 5]) { + let generator = self.constant_point(Point::generator()); + let nonce_times_gen = self.multiply_point(&nonce.bits, &generator); + let nonce_times_pk = self.multiply_point(&nonce.bits, pk); + let h = self.hash_n_to_m_no_pad::<PoseidonHash>( + [nonce_times_pk.x.components, nonce_times_pk.u.components].concat(), + 5, + ); + (nonce_times_gen, array::from_fn(|i| self.add(msg[i], h[i]))) + } +} + +#[cfg(test)] +mod tests { + use num_bigint::RandBigInt; + use plonky2::{ + field::types::{Field, Field64}, + iop::witness::{PartialWitness, WitnessWrite}, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, + }; + use pod2::backends::plonky2::{ + basetypes::{D, F}, + primitives::ec::{ + bits::CircuitBuilderBits, + curve::{CircuitBuilderElliptic, GROUP_ORDER, WitnessWriteCurve}, + }, + }; + use rand::{Rng, rngs::OsRng}; + + use crate::{ + ECGFP5SecretKey, + hashed_elgamal::{circuit::CircuitBuilderHashedElGamal, hashed_elgamal_encrypt}, + }; + + #[test] + fn hashed_elgamal_encryption() -> anyhow::Result<()> { + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let pk_target = builder.add_virtual_point_target(); + let nonce_target = builder.add_virtual_biguint320_target(); + let msg_target = builder.add_virtual_target_arr(); + + let ct_target = builder.hashed_elgamal_encrypt(&pk_target, &nonce_target, msg_target); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + + (0..10).try_for_each(|_| { + let sk = ECGFP5SecretKey::rand(); + let pk = sk.public_key(); + + let msg = std::array::from_fn(|_| F::from_canonical_u64(OsRng.gen_range(0..F::ORDER))); + let nonce = OsRng.gen_biguint_below(&GROUP_ORDER); + + let encrypted_msg = hashed_elgamal_encrypt(pk, &nonce, msg); + + let mut pw = PartialWitness::new(); + pw.set_point_target(&pk_target, &pk)?; + pw.set_target_arr(&msg_target, &msg)?; + pw.set_biguint320_target(&nonce_target, &nonce)?; + pw.set_point_target(&ct_target.0, &encrypted_msg.0)?; + pw.set_target_arr(&ct_target.1, &encrypted_msg.1)?; + + let proof = data.prove(pw)?; + data.verify(proof) + }) + } +}
M ecgfp5/src/lib.rs → ecgfp5/src/lib.rs

@@ -1,1 +1,112 @@

+use std::array; + +use num::BigUint; +use num_bigint::RandBigInt; +use plonky2::field::{ + extension::quintic::QuinticExtension, + goldilocks_field::GoldilocksField as F, + types::{Field, Field64}, +}; +use pod2::backends::plonky2::primitives::ec::curve::{GROUP_ORDER, Point}; +use rand::{Rng, rngs::OsRng}; + +pub mod circuit; pub mod elgamal; +pub mod hashed_elgamal; + +// Message encoding constants +pub const BITS_PER_LIMB: usize = 32; +pub const TOTAL_BITS: usize = 5 * BITS_PER_LIMB; + +/// EcGFp5 secret key type +pub struct ECGFP5SecretKey(pub BigUint); + +impl ECGFP5SecretKey { + /// EcGFp5 secret key constructor + pub fn new(s: BigUint) -> Self { + assert!(&s < &GROUP_ORDER); + Self(s) + } + + /// Random EcGFp5 secret key generator + pub fn rand() -> Self { + Self(OsRng.gen_biguint_below(&GROUP_ORDER)) + } + + /// Derives public key from secret key + pub fn public_key(&self) -> Point { + &self.0 * Point::generator() + } +} + +/// Convert `TOTAL_BITS`-bit biguint to an EcGFp5 point by padding +/// with random bits. This is an adaptation of the probabilistic +/// algorithm described in §3 of Koblitz's *Elliptic Curve +/// Cryptosystems* (1987). +pub fn encode_binary(x: &BigUint) -> Point { + let num_bits = x.bits() as usize; + assert!(num_bits <= TOTAL_BITS); + + let bs_bits = [x.to_radix_le(2), vec![0; TOTAL_BITS - num_bits]].concat(); + let bs_fields: Vec<F> = bs_bits + .chunks(BITS_PER_LIMB) + .map(|bits| { + F::from_noncanonical_biguint( + BigUint::from_radix_le(bits, 2).expect("Should be a valid bit decomposition"), + ) + }) + .collect(); + + std::iter::repeat_with(|| { + let ec_fields: [_; 5] = array::from_fn(|i| { + let random_element: u64 = OsRng.gen_range(0..F::ORDER - bs_fields[i].0); + F::from_canonical_u64( + bs_fields[i].0 + ((random_element >> BITS_PER_LIMB) << BITS_PER_LIMB), + ) + }); + QuinticExtension(ec_fields) + }) + .find_map(|w| Point::decompress_into_subgroup(&w).ok()) + .expect("Should be a match!") +} + +/// Convert an EcGFp5 point to a `TOTAL_BITS`-bit biguint. This is the +/// inverse to `encode_binary`. +pub fn decode_binary(p: Point) -> BigUint { + let msg_bits: Vec<_> = p + .compress_from_subgroup() + .unwrap() + .0 + .into_iter() + .flat_map(|f| { + BigUint::from(f.0) + .to_radix_le(2) + .into_iter() + .take(BITS_PER_LIMB) + .collect::<Vec<_>>() + }) + .collect(); + BigUint::from_bytes_le( + &BigUint::from_radix_le(&msg_bits, 2) + .expect("Should be a valid bit slice.") + .to_radix_le(256), + ) +} +#[cfg(test)] +mod tests { + use num::BigUint; + use num_bigint::RandBigInt; + use rand::rngs::OsRng; + + use crate::{TOTAL_BITS, decode_binary, encode_binary}; + + #[test] + fn msg_encoding_roundtrip() { + (0..100).for_each(|_| { + let test_val = OsRng.gen_biguint_below(&BigUint::from(2u32).pow(TOTAL_BITS as u32)); + let pt = encode_binary(&test_val); + let maybe_test_val = decode_binary(pt); + assert_eq!(test_val, maybe_test_val); + }) + } +}