Merge branch 'main' into poseidon-cipher
arnaucube git@arnaucube.com
Tue, 24 Feb 2026 11:22:03 +0000
12 files changed,
556 insertions(+),
3 deletions(-)
M
Cargo.toml
→
Cargo.toml
@@ -1,6 +1,7 @@
[workspace] members = [ "aes-gcm", + "ecgfp5", "feistel", "poseidon-cipher" ]@@ -10,3 +11,4 @@
[workspace.dependencies] plonky2 = { git = "https://github.com/0xPARC/plonky2.git", rev = "109d517d09c210ae4c2cee381d3e3fbc04aa3812" } anyhow = "1.0.56" +pod2 = { git = "https://github.com/0xPARC/pod2", rev = "cdf227e3530247e07e375daed007cae7d443b4ae" }
M
README.md
→
README.md
@@ -1,9 +1,12 @@
# plonky2-crypto-gadgets -> Warning: this code is unaudited. +> Warning: unaudited code. - [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 as described at + - ElGamal: §1.4.3 of Menezes - *Elliptic Curve Public Key Cryptosystems* (1993) + - hashed ElGamal: Abdalla, Bellare & Rogaway - *The oracle Diffie-Hellman assumptions and an analysis of DHIES* (2001) - [feistel](/feistel): Plonky2 library for a finite field analogue of the Feistel cipher as described [here](https://en.wikipedia.org/wiki/Feistel_cipher). -- [poseidon-cipher](/poseidon-cipher): Plonky2 library & circuits for Poseidon encryption as described [here](https://drive.google.com/file/d/1EVrP3DzoGbmzkRmYnyEDcIQcXVU7GlOd/view). +- [poseidon-cipher](/poseidon-cipher): Plonky2 library & circuits for Poseidon encryption as described at [*"Encryption with Poseidon"* by Dmitry Khovratovich (2019)](https://drive.google.com/file/d/1EVrP3DzoGbmzkRmYnyEDcIQcXVU7GlOd/view).
A
ecgfp5/Cargo.toml
@@ -0,0 +1,12 @@
+[package] +name = "plonky2-ecgfp5" +version = "0.1.0" +edition = "2024" + +[dependencies] +plonky2 = { workspace = true } +anyhow = { workspace = true } +num = { version = "0.4.3", features = ["num-bigint"] } +pod2 = { workspace = true } # for ecgfp5 +num-bigint = { version = "0.4.6", features = ["rand"] } +rand = "0.8.5"
A
ecgfp5/src/circuit.rs
@@ -0,0 +1,101 @@
+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + +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 secret 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) + }) + } +}
A
ecgfp5/src/elgamal.rs
@@ -0,0 +1,47 @@
+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + +use num::bigint::BigUint; +use pod2::backends::plonky2::primitives::ec::curve::{GROUP_ORDER, Point}; + +use crate::ECGFP5SecretKey; + +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(); + 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() +} + +#[cfg(test)] +mod tests { + use num_bigint::RandBigInt; + use pod2::backends::plonky2::primitives::ec::curve::{GROUP_ORDER, Point}; + use rand::rngs::OsRng; + + use crate::elgamal::{ECGFP5SecretKey, elgamal_decrypt, elgamal_encrypt}; + + #[test] + fn round_trip_encryption() { + (0..100).for_each(|_| { + let sk = ECGFP5SecretKey::rand(); + let pk = sk.public_key(); + + let msg = Point::new_rand_from_subgroup(); + let nonce = OsRng.gen_biguint_below(&GROUP_ORDER); + + let encrypted_msg = elgamal_encrypt(pk, &nonce, msg); + let decrypted_msg = elgamal_decrypt(sk, encrypted_msg); + + assert_eq!(msg, decrypted_msg); + }) + } +}
A
ecgfp5/src/elgamal/circuit.rs
@@ -0,0 +1,98 @@
+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + +use plonky2::{ + field::extension::Extendable, hash::hash_types::RichField, + 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 CircuitBuilderElGamal<F: RichField + Extendable<D>, const D: usize> { + /// Encrypts an EcGFp5 point according to the ElGamal scheme. + fn elgamal_encrypt( + &mut self, + pk: &PublicKeyTarget, + nonce: &BigUInt320Target, + msg: &PointTarget, + ) -> (PointTarget, PointTarget); +} + +impl CircuitBuilderElGamal<F, D> for CircuitBuilder<F, D> { + fn elgamal_encrypt( + &mut self, + pk: &PublicKeyTarget, + nonce: &BigUInt320Target, + msg: &PointTarget, + ) -> (PointTarget, PointTarget) { + 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); + (nonce_times_gen, self.add_point(msg, &nonce_times_pk)) + } +} + +#[cfg(test)] +mod tests { + use num_bigint::RandBigInt; + use plonky2::{ + iop::witness::PartialWitness, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, + }; + use pod2::backends::plonky2::{ + basetypes::{D, F}, + primitives::ec::{ + bits::CircuitBuilderBits, + curve::{CircuitBuilderElliptic, GROUP_ORDER, Point, WitnessWriteCurve}, + }, + }; + use rand::rngs::OsRng; + + use crate::{ + ECGFP5SecretKey, + elgamal::{circuit::CircuitBuilderElGamal, elgamal_encrypt}, + }; + + #[test] + fn 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_point_target(); + + let ct_target = builder.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 = Point::new_rand_from_subgroup(); + let nonce = OsRng.gen_biguint_below(&GROUP_ORDER); + + let encrypted_msg = elgamal_encrypt(pk, &nonce, msg); + + let mut pw = PartialWitness::new(); + pw.set_point_target(&pk_target, &pk)?; + pw.set_point_target(&msg_target, &msg)?; + pw.set_biguint320_target(&nonce_target, &nonce)?; + pw.set_point_target(&ct_target.0, &encrypted_msg.0)?; + pw.set_point_target(&ct_target.1, &encrypted_msg.1)?; + + let proof = data.prove(pw)?; + data.verify(proof) + }) + } +}
A
ecgfp5/src/hashed_elgamal.rs
@@ -0,0 +1,64 @@
+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + +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,108 @@
+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + +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) + }) + } +}
A
ecgfp5/src/lib.rs
@@ -0,0 +1,114 @@
+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + +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); + }) + } +}
M
feistel/src/circuit.rs
→
feistel/src/circuit.rs
@@ -1,3 +1,5 @@
+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + use std::array; use plonky2::{
M
feistel/src/lib.rs
→
feistel/src/lib.rs
@@ -1,3 +1,5 @@
+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + #![allow(incomplete_features)] #![feature(generic_const_exprs)]
M
poseidon-cipher/Cargo.toml
→
poseidon-cipher/Cargo.toml
@@ -7,7 +7,7 @@ [dependencies]
plonky2 = { workspace = true } anyhow = { workspace = true } num = { version = "0.4.3", features = ["num-bigint"] } -pod2 = { git = "https://github.com/0xPARC/pod2", rev = "cdf227e3530247e07e375daed007cae7d443b4ae" } # for ecgfp5 +pod2 = { workspace = true } # for ecgfp5 num-bigint = { version = "0.4.6", features = ["rand"] } rand = "0.8.5"