Merge branch 'main' into ecgfp5
jump to
@@ -1,12 +1,12 @@
[workspace] members = [ "aes-gcm", - "ecgfp5" - + "ecgfp5", + "feistel" ] resolver = "2" [workspace.dependencies] plonky2 = { git = "https://github.com/0xPARC/plonky2.git", rev = "109d517d09c210ae4c2cee381d3e3fbc04aa3812" } -anyhow = "1.0.56"+anyhow = "1.0.56"
@@ -1,5 +1,8 @@
# plonky2-crypto-gadgets +> Warning: this code is unaudited. + - [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 +- [feistel](/feistel): a Plonky2 library for a finite field analogue of the Feistel cipher as described [here](https://en.wikipedia.org/wiki/Feistel_cipher).
@@ -0,0 +1,25 @@
+# plonky2-aes + +- [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 + + +> Warning: this code is unaudited. + +Full example of usage at [examples](https://github.com/0xPARC/plonky2-aes/blob/main/examples) directory. +```rust +// circuit declaration +let config = CircuitConfig::standard_recursion_zk_config(); +let mut builder = CircuitBuilder::<F, D>::new(config); +let aes_targets = AesGcm128Target::<L>::new_virtual(&mut builder); +aes_targets.build_circuit(&mut builder); +let data = builder.build::<PoseidonGoldilocksConfig>(); + +// set values to circuit +let mut pw = PartialWitness::<F>::new(); +aes_targets.set_targets(&mut pw, key, nonce, pt, ct, tag)?; + +let proof = data.prove(pw)?; +data.verify(proof) +```
@@ -0,0 +1,55 @@
+//! Run: `cargo run --release --example aes_gcm_128` +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] + +use anyhow::Result; +use plonky2::{ + field::goldilocks_field::GoldilocksField as F, + iop::witness::PartialWitness, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, +}; +use plonky2_aes::{AesGcm128Target, D, KEY_LEN_128}; + +fn main() -> Result<()> { + // max size (bytes) of plaintext supported by the instantiation of the circuit + const L: usize = 42; + + let key: &[u8; KEY_LEN_128] = &[123; KEY_LEN_128]; + let nonce: &[u8; 12] = &[111; 12]; + let pt: &[u8; L] = &[231u8; L]; // plaintext + + // use external rust library to compute the ciphertext & tag + let nonce_ext = aes_gcm::Nonce::from_slice(nonce); + use aes_gcm::{KeyInit, aead::Aead}; // needed traits + let cipher = aes_gcm::Aes128Gcm::new_from_slice(key.as_slice()).unwrap(); + let encrypt_res = cipher.encrypt(nonce_ext, pt.as_ref()).unwrap(); + let ct = &encrypt_res[..L]; // ciphertext + let tag = &encrypt_res[L..]; + + // alternatively we could use this repo's rust implementation (only for tests) + // let (ct, tag) = plonky2_aes::native_gcm::encrypt::<4, 4, 10>(key, nonce, pt); + + // circuit declaration + let config = CircuitConfig::standard_recursion_zk_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + let aes_targets = AesGcm128Target::<L>::new_virtual(&mut builder); + aes_targets.build_circuit(&mut builder); + + println!( + "AES-GCM-128 circuit (L:{}) num_gates: {}", + L, + builder.num_gates() + ); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + aes_targets.set_targets(&mut pw, key, nonce, pt, ct, tag)?; + + let proof = data.prove(pw)?; + data.verify(proof) +}
@@ -0,0 +1,55 @@
+//! Run: `cargo run --release --example aes_gcm_256` +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] + +use anyhow::Result; +use plonky2::{ + field::goldilocks_field::GoldilocksField as F, + iop::witness::PartialWitness, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, +}; +use plonky2_aes::{AesGcm256Target, D, KEY_LEN_256}; + +fn main() -> Result<()> { + // max size (bytes) of plaintext supported by the instantiation of the circuit + const L: usize = 42; + + let key: &[u8; KEY_LEN_256] = &[123; KEY_LEN_256]; + let nonce: &[u8; 12] = &[111; 12]; + let pt: &[u8; L] = &[231u8; L]; // plaintext + + // use external rust library to compute the ciphertext & tag + let nonce_ext = aes_gcm::Nonce::from_slice(nonce); + use aes_gcm::{KeyInit, aead::Aead}; // needed traits + let cipher = aes_gcm::Aes256Gcm::new_from_slice(key.as_slice()).unwrap(); + let encrypt_res = cipher.encrypt(nonce_ext, pt.as_ref()).unwrap(); + let ct = &encrypt_res[..L]; // ciphertext + let tag = &encrypt_res[L..]; + + // alternatively we could use this repo's rust implementation (only for tests) + // let (ct, tag) = plonky2_aes::native_gcm::encrypt::<8, 4, 14>(key, nonce, pt); + + // circuit declaration + let config = CircuitConfig::standard_recursion_zk_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + let aes_targets = AesGcm256Target::<L>::new_virtual(&mut builder); + aes_targets.build_circuit(&mut builder); + + println!( + "AES-GCM-256 circuit (L:{}) num_gates: {}", + L, + builder.num_gates() + ); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + aes_targets.set_targets(&mut pw, key, nonce, pt, ct, tag)?; + + let proof = data.prove(pw)?; + data.verify(proof) +}
@@ -1,4 +1,7 @@
-//! For LICENSE check out https://github.com/0xPARC/plonky2-aes/blob/main/LICENSE +//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE +//! +//! Plonky2 circuit implementation of +//! [AES](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197-upd1.pdf). use std::{array, sync::Arc};@@ -19,10 +22,19 @@ native_aes::{State, rot_word, shift_rows},
}; #[derive(Debug, Copy, Clone)] -pub struct StateTarget(pub [[ByteArrayTarget; 4]; 4]); +pub struct StateTarget(pub [[ByteTarget; 4]; 4]); -// TODO: Normalise terminology! -pub type ByteArrayTarget = [BoolTarget; 8]; +impl StateTarget { + pub(crate) fn flatten(self) -> [ByteTarget; 16] { + array::from_fn(|i| self.0[i % 4][i / 4]) + } + + pub(crate) fn from_flat(b: [ByteTarget; 16]) -> Self { + StateTarget(array::from_fn(|i| array::from_fn(|j| b[j * 4 + i]))) + } +} + +pub type ByteTarget = [BoolTarget; 8]; pub trait CircuitBuilderAESState<F: RichField + Extendable<D>, const D: usize> { /// Adds state target.@@ -32,42 +44,35 @@ /// AES cipher as in spec.
fn encrypt_block<const NR: usize>( &mut self, sbox_lut_idx: usize, - mix_matrix: [[ByteArrayTarget; 4]; 4], + mix_matrix: [[ByteTarget; 4]; 4], s: StateTarget, - w: [[ByteArrayTarget; 4]; 4 * (NR + 1)], + w: [[ByteTarget; 4]; 4 * (NR + 1)], ) -> StateTarget; /// Applies sub_bytes routine to state. fn state_sub_bytes(&mut self, sbox_lut_idx: usize, s: StateTarget) -> StateTarget; /// SubWord - fn state_sub_word( - &mut self, - sbox_lut_idx: usize, - word: [ByteArrayTarget; 4], - ) -> [ByteArrayTarget; 4]; + fn state_sub_word(&mut self, sbox_lut_idx: usize, word: [ByteTarget; 4]) -> [ByteTarget; 4]; /// MixColumns fn state_mix_columns( &mut self, - mix_matrix: [[ByteArrayTarget; 4]; 4], + mix_matrix: [[ByteTarget; 4]; 4], s: StateTarget, ) -> StateTarget; /// AddRoundKey - fn state_add_round_key( - &mut self, - round_key: &[[ByteArrayTarget; 4]], - s: StateTarget, - ) -> StateTarget; + fn state_add_round_key(&mut self, round_key: &[[ByteTarget; 4]], s: StateTarget) + -> StateTarget; /// KeyExpansion fn key_expansion<const NK: usize, const NB: usize, const NR: usize>( &mut self, sbox_lut_idx: usize, - key: [ByteArrayTarget; NK * NB], - ) -> [[ByteArrayTarget; 4]; 4 * (NR + 1)]; + key: [ByteTarget; NK * NB], + ) -> [[ByteTarget; 4]; 4 * (NR + 1)]; /// GF(2^8) addition fn gf_2_8_add(&mut self, x: Target, y: Target) -> Target;@@ -95,9 +100,14 @@
/// Bytearray matrix application. fn bytearray_matrix_apply_bits<const M: usize, const N: usize>( &mut self, - a: [[ByteArrayTarget; N]; M], - x: [ByteArrayTarget; N], - ) -> [ByteArrayTarget; M]; + a: [[ByteTarget; N]; M], + x: [ByteTarget; N], + ) -> [ByteTarget; M]; + + /// returns a 0u8 in the shape of a ByteTarget. + fn zero_byte(&mut self) -> ByteTarget; + + fn empty_state(&mut self) -> StateTarget; } impl CircuitBuilderAESState<F, D> for CircuitBuilder<F, D> {@@ -110,9 +120,9 @@
fn encrypt_block<const NR: usize>( &mut self, sbox_lut_idx: usize, - mix_matrix: [[ByteArrayTarget; 4]; 4], // constant mix_matrix + mix_matrix: [[ByteTarget; 4]; 4], // constant mix_matrix s: StateTarget, - w: [[ByteArrayTarget; 4]; 4 * (NR + 1)], // expanded key + w: [[ByteTarget; 4]; 4 * (NR + 1)], // expanded key ) -> StateTarget { let mut s = s; s = self.state_add_round_key(&w[0..4], s);@@ -133,11 +143,7 @@ self.state_sub_word(sbox_lut_idx, s.0[i])
})) } - fn state_sub_word( - &mut self, - sbox_lut_idx: usize, - word: [ByteArrayTarget; 4], - ) -> [ByteArrayTarget; 4] { + fn state_sub_word(&mut self, sbox_lut_idx: usize, word: [ByteTarget; 4]) -> [ByteTarget; 4] { array::from_fn(|i| { let byte_target = target_from_bitarray(self, &word[i]); let out_target = self.add_lookup_from_index(byte_target, sbox_lut_idx);@@ -147,7 +153,7 @@ }
fn state_mix_columns( &mut self, - mix_matrix: [[ByteArrayTarget; 4]; 4], + mix_matrix: [[ByteTarget; 4]; 4], s: StateTarget, ) -> StateTarget { let cols: [_; 4] = array::from_fn(|i| array::from_fn(|j| s.0[j][i]));@@ -158,7 +164,7 @@ }
fn state_add_round_key( &mut self, - round_key: &[[ByteArrayTarget; 4]], + round_key: &[[ByteTarget; 4]], s: StateTarget, ) -> StateTarget { StateTarget(array::from_fn(|i| {@@ -169,9 +175,9 @@
fn key_expansion<const NK: usize, const NB: usize, const NR: usize>( &mut self, sbox_lut_idx: usize, - key: [ByteArrayTarget; NK * NB], - ) -> [[ByteArrayTarget; 4]; 4 * (NR + 1)] { - let rcon: [ByteArrayTarget; 11] = array::from_fn(|i| { + key: [ByteTarget; NK * NB], + ) -> [[ByteTarget; 4]; 4 * (NR + 1)] { + let rcon: [ByteTarget; 11] = array::from_fn(|i| { let rcon_bits = le_bits_from_byte(RCON[i]); array::from_fn(|j| self.constant_bool(rcon_bits[j])) });@@ -294,19 +300,27 @@ }
fn bytearray_matrix_apply_bits<const M: usize, const N: usize>( &mut self, - a: [[ByteArrayTarget; N]; M], - x: [ByteArrayTarget; N], - ) -> [ByteArrayTarget; M] { + a: [[ByteTarget; N]; M], + x: [ByteTarget; N], + ) -> [ByteTarget; M] { std::array::from_fn(|i| self.bytearray_ip_bits(a[i], x)) } + + fn zero_byte(&mut self) -> ByteTarget { + array::from_fn(|_| self._false()) + } + fn empty_state(&mut self) -> StateTarget { + let zero_byte: [BoolTarget; 8] = array::from_fn(|_| self._false()); + StateTarget(array::from_fn(|_| array::from_fn(|_| zero_byte))) + } } pub trait PartialWitnessByteArray { - fn set_byte_array_target(&mut self, target: ByteArrayTarget, value: u8) -> anyhow::Result<()>; + fn set_byte_array_target(&mut self, target: ByteTarget, value: u8) -> anyhow::Result<()>; } impl<F: Field> PartialWitnessByteArray for PartialWitness<F> { - fn set_byte_array_target(&mut self, target: ByteArrayTarget, value: u8) -> anyhow::Result<()> { + fn set_byte_array_target(&mut self, target: ByteTarget, value: u8) -> anyhow::Result<()> { let value_bits = le_bits_from_byte(value); std::iter::zip(target, value_bits).try_for_each(|(t, v)| self.set_bool_target(t, v)) }@@ -400,8 +414,8 @@ };
use rand::RngExt; use super::{ - ByteArrayTarget, CircuitBuilderAESState, D, PartialWitnessAESState, - PartialWitnessByteArray, sbox_lut, state_mix_matrix_bits, + ByteTarget, CircuitBuilderAESState, D, PartialWitnessAESState, PartialWitnessByteArray, + sbox_lut, state_mix_matrix_bits, }; use crate::native_aes::{State, encrypt_block, key_expansion, mix_columns, sub_bytes};@@ -545,7 +559,7 @@ // Circuit declaration
let config = CircuitConfig::standard_recursion_config(); let mut builder = CircuitBuilder::<F, D>::new(config); - let key_target: [ByteArrayTarget; NK * NB] = + let key_target: [ByteTarget; NK * NB] = array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); let sbox_lut = sbox_lut(&mut builder); let expanded_key_target = builder.key_expansion::<NK, NB, NR>(sbox_lut, key_target);@@ -627,11 +641,11 @@ // Circuit declaration
let config = CircuitConfig::standard_recursion_config(); let mut builder = CircuitBuilder::<F, D>::new(config); - let key_target: [ByteArrayTarget; NK * NB] = + let key_target: [ByteTarget; NK * NB] = array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); let sbox_lut = sbox_lut(&mut builder); let mix_matrix = state_mix_matrix_bits(&mut builder); - let expanded_key_target: [[ByteArrayTarget; 4]; 4 * (NR + 1)] = + let expanded_key_target: [[ByteTarget; 4]; 4 * (NR + 1)] = builder.key_expansion::<NK, NB, NR>(sbox_lut, key_target); let input_state_target = builder.add_virtual_state();
@@ -0,0 +1,727 @@
+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE +//! +//! Plonky2 circuit implementation of +//! [AES-GCM (Galois Counter Mode)](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf). + +use std::array; + +use anyhow::Result; +use plonky2::{ + field::{goldilocks_field::GoldilocksField as F, types::Field}, + iop::{target::BoolTarget, witness::PartialWitness}, + plonk::circuit_builder::CircuitBuilder, +}; + +use crate::{ + D, + circuit_aes::{ + ByteTarget, CircuitBuilderAESState, PartialWitnessByteArray, StateTarget, + le_bits_from_byte, sbox_lut, state_mix_matrix_bits, xor, + }, + constants::TAG_LEN, +}; + +pub struct AesGcmTarget< + const NK: usize, + const NB: usize, + const NR: usize, + // L: max size of plaintext to cipher. + const L: usize, + const TAG: bool, +> where + [(); NK * NB]:, + [(); 4 * (NR + 1)]:, +{ + key: [ByteTarget; NK * NB], + nonce: [ByteTarget; 12], + pt: [ByteTarget; L], + + ct: [ByteTarget; L], + tag: [ByteTarget; TAG_LEN / 8], +} + +impl<const NK: usize, const NB: usize, const NR: usize, const L: usize, const TAG: bool> + AesGcmTarget<NK, NB, NR, L, TAG> +where + [(); NK * NB]:, + [(); 4 * (NR + 1)]:, +{ + pub fn new_virtual(builder: &mut CircuitBuilder<F, D>) -> Self { + let key: [ByteTarget; NK * NB] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + let nonce: [ByteTarget; 12] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + let pt: [ByteTarget; L] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + + let ct: [ByteTarget; L] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + let tag: [ByteTarget; TAG_LEN / 8] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + + Self { + key, + nonce, + pt, + ct, + tag, + } + } + + pub fn build_circuit(&self, builder: &mut CircuitBuilder<F, D>) { + let sbox_lut = sbox_lut(builder); + let mix_matrix = state_mix_matrix_bits(builder); + + let a: &[ByteTarget] = &[]; // additional authenticated data + + let expanded_key = builder.key_expansion::<NK, NB, NR>(sbox_lut, self.key); + + let empty_state = builder.empty_state(); + + // 1. CIPH_K + let h = builder + .encrypt_block::<NR>(sbox_lut, mix_matrix, empty_state, expanded_key) + .flatten(); + + // 2. J_0 + let zero_byte = builder.zero_byte(); + let one_byte = array::from_fn(|i| { + // 0x01 + if i == 0 { + builder._true() + } else { + builder._false() + } + }); + let j0: [ByteTarget; 16] = if self.nonce.len() * 8 == 96 { + // J_0 = IV || 0^31 || 1 + let mut out = [zero_byte; 16]; + out[..12].copy_from_slice(self.nonce.as_slice()); + out[12..16].copy_from_slice(&[zero_byte, zero_byte, zero_byte, one_byte]); + out + } else { + panic!("unsupported at initial version; nonce.len()=12 (96 bits)"); + }; + + // 3. C=GCTR() + let inc32_j0 = inc32_target(builder, j0); + let c = gctr_target::<NR, L>( + builder, + sbox_lut, + mix_matrix, + expanded_key, + &inc32_j0, + &self.pt, + ); + + // connect the ct value to the external ct value + #[allow(clippy::needless_range_loop)] + for byte in 0..L { + for bit in 0..8 { + builder.connect(self.ct[byte][bit].target, c[byte][bit].target); + } + } + + // the rest of the logic is for the tag; for some use cases we might skip it + // (saving a notable amount of gates, about double) + if !TAG { + return; + } + + // 4. u, v + let u: usize = 16 * (L as f64 / 16_f64).ceil() as usize - L; // L=x.len() + let v: usize = 16 * (a.len() as f64 / 16_f64).ceil() as usize - a.len(); + + // 5. S = GHASH() + let const_a_len_u8: [u8; 8] = (a.len() * 8).to_be_bytes(); // const + let const_c_len_u8: [u8; 8] = (c.len() * 8).to_be_bytes(); // const + let a_len: [ByteTarget; 8] = array::from_fn(|byte_i| { + let const_bits = le_bits_from_byte(const_a_len_u8[byte_i]); + let byte: ByteTarget = array::from_fn(|bit_i| { + let bit_goldilocks = if const_bits[bit_i] { F::ONE } else { F::ZERO }; + BoolTarget::new_unsafe(builder.constant(bit_goldilocks)) + }); + byte + }); + let c_len: [ByteTarget; 8] = array::from_fn(|byte_i| { + let const_bits = le_bits_from_byte(const_c_len_u8[byte_i]); + let byte: ByteTarget = array::from_fn(|bit_i| { + let bit_goldilocks = if const_bits[bit_i] { F::ONE } else { F::ZERO }; + BoolTarget::new_unsafe(builder.constant(bit_goldilocks)) + }); + byte + }); + let ghash_input_vec = [ + a.to_vec(), + vec![zero_byte; v], + c.to_vec(), + vec![zero_byte; u], + a_len.to_vec(), + c_len.to_vec(), + ] + .concat(); + let s = ghash_target(builder, h, ghash_input_vec); + + // 6. T=MSB(GCTR())) + let msb_input = gctr_target(builder, sbox_lut, mix_matrix, expanded_key, &j0, &s); + let t_vec = msb_t_target(builder, TAG_LEN / 8, &msb_input); + let mut t: [ByteTarget; TAG_LEN / 8] = [zero_byte; TAG_LEN / 8]; + t.copy_from_slice(&t_vec); + + // connect the computed tag value to the external tag value + #[allow(clippy::needless_range_loop)] + for byte in 0..TAG_LEN / 8 { + for bit in 0..8 { + builder.connect(self.tag[byte][bit].target, t[byte][bit].target); + } + } + } + + pub fn set_targets( + &self, + pw: &mut PartialWitness<F>, + key: &[u8; NK * NB], + nonce: &[u8; 12], + pt: &[u8], + ct: &[u8], + tag: &[u8], + ) -> Result<()> { + assert!(pt.len() <= L); + assert!(ct.len() <= L); + assert!(tag.len() <= TAG_LEN / 8); + + // extend to expected sizes + let mut pt_arr: [u8; L] = [0u8; L]; + pt_arr.copy_from_slice(pt); + let mut ct_arr: [u8; L] = [0u8; L]; + ct_arr.copy_from_slice(ct); + let mut tag_arr: [u8; TAG_LEN / 8] = [0u8; TAG_LEN / 8]; + tag_arr.copy_from_slice(tag); + + std::iter::zip(self.key, key).try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?; + std::iter::zip(self.nonce, nonce).try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?; + std::iter::zip(self.pt, pt_arr).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + + std::iter::zip(self.ct, ct_arr).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + std::iter::zip(self.tag, tag_arr).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + Ok(()) + } +} + +/// L: max size of plaintext to cipher. +fn gctr_target<const NR: usize, const L: usize>( + builder: &mut CircuitBuilder<F, D>, + sbox_lut_idx: usize, + mix_matrix: [[ByteTarget; 4]; 4], + key: [[ByteTarget; 4]; 4 * (NR + 1)], + icb: &[ByteTarget; 16], + x: &[ByteTarget; L], +) -> [ByteTarget; L] { + let n = ((L * 8) as f64 / 128_f64).ceil() as usize; + + let mut y: [ByteTarget; L] = *x; + let mut cb_i = *icb; + + let zero_byte = builder.zero_byte(); + for (i, x_i_raw) in x.chunks(16).enumerate() { + if i > 0 { + cb_i = inc32_target(builder, cb_i); + } + + let l = x_i_raw.len().min(16); + let mut x_i = [zero_byte; 16]; + x_i[..l].copy_from_slice(x_i_raw); + + let ciph_cb_i = builder + .encrypt_block::<NR>(sbox_lut_idx, mix_matrix, StateTarget::from_flat(cb_i), key) + .flatten(); + + let (y_i, n_bytes) = if i < n && x_i_raw.len() == 16 { + (xor_blocks(builder, x_i, ciph_cb_i), 16) + } else { + // last chunk, might be smaller than 16 bytes (L%16 bytes) + let msb_res = msb_t_target(builder, L % 16, ciph_cb_i.as_slice()); + let l = msb_res.len().min(16); + let mut m: [ByteTarget; 16] = [zero_byte; 16]; + m[..l].copy_from_slice(&msb_res); + (xor_blocks(builder, x_i, m), l) + }; + y[i * 16..i * 16 + n_bytes].clone_from_slice(&y_i[..n_bytes]); + } + y +} + +// fn ghash_target<const L: usize>( +fn ghash_target( + builder: &mut CircuitBuilder<F, D>, + h: [ByteTarget; 16], + x: Vec<ByteTarget>, +) -> [ByteTarget; 16] { + assert!(x.len().is_multiple_of(16)); // multiple of 128 bits + let m = x.len() / 16; + + let zero_byte = builder.zero_byte(); + let mut y = [zero_byte; 16]; // (128 bits) + for i in 0..m { + let mut xi = [zero_byte; 16]; + xi.clone_from_slice(&x[i * 16..i * 16 + 16]); + let y_xi = xor_blocks(builder, y, xi); + y = gf_2_128_mul_target(builder, y_xi, h); + } + y +} +fn gf_2_128_mul_target( + builder: &mut CircuitBuilder<F, D>, + x: [ByteTarget; 16], + y: [ByteTarget; 16], +) -> [ByteTarget; 16] { + let zero_byte = builder.zero_byte(); + let one_bool = builder._true(); + + // R: 10000111 || 0^120 (in little-endian) + let mut r = [zero_byte; 16]; + r[0][0] = one_bool; + r[0][5] = one_bool; + r[0][6] = one_bool; + r[0][7] = one_bool; + + let mut z = [zero_byte; 16]; + let mut v = y; + for i in 0..128 { + let byte_index = i / 8; + let bit_index = 7 - (i % 8); + let xi = x[byte_index][bit_index]; + + // set z = if xi==1: z^v, else: z + for b in 0..16 { + for k in 0..8 { + let z_xor_v = xor(builder, z[b][k], v[b][k]); + z[b][k] = + BoolTarget::new_unsafe(builder.select(xi, z_xor_v.target, z[b][k].target)); + } + } + + let lsb = v[15][0]; // (little-endian) + v = right_shift_one_target(builder, &v); + + // if lsb==1: v=v^R, else: v + for b in 0..16 { + for k in 0..8 { + let v_xor_r = xor(builder, v[b][k], r[b][k]); + v[b][k] = + BoolTarget::new_unsafe(builder.select(lsb, v_xor_r.target, v[b][k].target)); + } + } + } + z +} +pub fn right_shift_one_target( + builder: &mut CircuitBuilder<F, D>, + v: &[ByteTarget; 16], +) -> [ByteTarget; 16] { + let mut r: [ByteTarget; 16] = *v; + + let mut carry = builder._false(); + for i in 0..16 { + let current = v[i]; + + let next_carry = current[0]; + + let mut shifted = [builder._false(); 8]; + shifted[..7].copy_from_slice(¤t[1..(7 + 1)]); + shifted[7] = carry; + r[i] = shifted; + carry = next_carry; + } + r +} + +pub fn inc32_target( + builder: &mut CircuitBuilder<F, D>, + block: [ByteTarget; 16], +) -> [ByteTarget; 16] { + let mut r = block; + + let mut carry = BoolTarget::new_unsafe(builder.one()); + for byte_index in (12..16).rev() { + for bit_index in 0..8 { + let a = block[byte_index][bit_index]; + // builder.assert_bool(a); // Note: we can assume it's valid bool, skip check + + let sum = xor(builder, a, carry); + let carry_out = builder.and(a, carry); + r[byte_index][bit_index] = sum; + carry = carry_out; + } + } + r +} + +fn xor_byte(builder: &mut CircuitBuilder<F, D>, b1: ByteTarget, b2: ByteTarget) -> ByteTarget { + array::from_fn(|i| xor(builder, b1[i], b2[i])) +} + +fn xor_blocks( + builder: &mut CircuitBuilder<F, D>, + b1: [ByteTarget; 16], + b2: [ByteTarget; 16], +) -> [ByteTarget; 16] { + array::from_fn(|i| xor_byte(builder, b1[i], b2[i])) +} + +pub fn msb_t_target( + builder: &mut CircuitBuilder<F, D>, + t_bytes: usize, + block: &[ByteTarget], +) -> Vec<ByteTarget> { + let t = t_bytes * 8; + + let full_bytes = t / 8; + let rem_bits = t % 8; + + let mut out: Vec<ByteTarget> = Vec::new(); + for block_i in block.iter().take(full_bytes) { + out.push(*block_i); + } + + let zero_bool = BoolTarget::new_unsafe(builder.zero()); + if rem_bits != 0 { + let mut partial = [zero_bool; 8]; + + for bit in 0..8 { + if bit < rem_bits { + partial[bit] = block[full_bytes][bit]; + } else { + partial[bit] = zero_bool; + } + } + out.push(partial); + } + out +} + +#[cfg(test)] +mod tests { + use std::array; + + use anyhow::Result; + use plonky2::{ + field::goldilocks_field::GoldilocksField as F, + iop::witness::PartialWitness, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, + }; + + use super::{D, *}; + use crate::{ + circuit_aes::PartialWitnessByteArray, + native_aes::key_expansion, + native_gcm::{gctr, gf_2_128_mul, ghash, right_shift_one}, + }; + + #[test] + fn test_gctr() -> Result<()> { + // AES-GCM-128 + test_gctr_opt::<4, 4, 10, 13>()?; + test_gctr_opt::<4, 4, 10, 17>()?; + + // AES-GCM-256 + test_gctr_opt::<8, 4, 14, 13>()?; + + Ok(()) + } + fn test_gctr_opt<const NK: usize, const NB: usize, const NR: usize, const L: usize>() + -> Result<()> + where + [(); 4 * (NR + 1)]:, + [(); NK * NB]:, + { + let key: &[u8; NK * NB] = &[42; NK * NB]; + let expanded_key = key_expansion::<NK, NB, NR>(key); + let nonce: &[u8; 12] = &[111; 12]; + let icb: &[u8; 16] = &[222; 16]; + let pt: &[u8; L] = &[42u8; L]; + + let expected = gctr(expanded_key, icb, pt); + + // Circuit declaration + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let key_target: [ByteTarget; NK * NB] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + let nonce_target: [ByteTarget; 12] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + let icb_target: [ByteTarget; 16] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + let pt_target: [ByteTarget; L] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + let sbox_lut = sbox_lut(&mut builder); + let mix_matrix = state_mix_matrix_bits(&mut builder); + + let key_expanded_target = builder.key_expansion::<NK, NB, NR>(sbox_lut, key_target); + let gctr_out_target = gctr_target::<NR, L>( + &mut builder, + sbox_lut, + mix_matrix, + key_expanded_target, + &icb_target, + &pt_target, + ); + + println!( + "gctr (NK:{}, NB:{}, NR:{}, L:{}) num_gates: {}", + NK, + NB, + NR, + L, + builder.num_gates() + ); + let data = builder.build::<PoseidonGoldilocksConfig>(); + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + std::iter::zip(key_target, key).try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?; + std::iter::zip(nonce_target, nonce) + .try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?; + std::iter::zip(icb_target, icb).try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?; + std::iter::zip(pt_target, pt).try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?; + + std::iter::zip(gctr_out_target, expected) + .try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + + let proof = data.prove(pw)?; + data.verify(proof) + } + + #[test] + fn test_right_shift_one() -> Result<()> { + let x: [u8; 16] = [111; 16]; + + let mut expected = x; + right_shift_one(&mut expected); + + // Circuit declaration + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let x_target: [ByteTarget; 16] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + + let out_target: [ByteTarget; 16] = right_shift_one_target(&mut builder, &x_target); + + println!("right_shift_one num_gates: {}", builder.num_gates()); + let data = builder.build::<PoseidonGoldilocksConfig>(); + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + std::iter::zip(x_target, x).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + + std::iter::zip(out_target, expected) + .try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + + let proof = data.prove(pw)?; + data.verify(proof)?; + Ok(()) + } + + #[test] + fn test_gf_mul() -> Result<()> { + // AES-GCM-128 + test_gf_mul_opt::<4, 4, 10, 16>()?; + test_gf_mul_opt::<4, 4, 10, 32>()?; + + // AES-GCM-256 + test_gf_mul_opt::<8, 4, 14, 16>()?; + + Ok(()) + } + fn test_gf_mul_opt<const NK: usize, const NB: usize, const NR: usize, const L: usize>() + -> Result<()> + where + [(); 4 * (NR + 1)]:, + [(); NK * NB]:, + { + let x: [u8; 16] = [111; 16]; + let y: [u8; 16] = [222; 16]; + + let expected = gf_2_128_mul(x, y); + let x_bits: [[bool; 8]; 16] = + array::from_fn(|b| crate::circuit_aes::le_bits_from_byte(x[b])); + let y_bits: [[bool; 8]; 16] = + array::from_fn(|b| crate::circuit_aes::le_bits_from_byte(y[b])); + + // sanity check + let expected2 = crate::native_gcm::gf_2_128_mul_circuit_version(x_bits, y_bits); + let expected_bits: [[bool; 8]; 16] = + array::from_fn(|b| crate::circuit_aes::le_bits_from_byte(expected[b])); + assert_eq!(expected2, expected_bits); + + // Circuit declaration + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let x_target: [ByteTarget; 16] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + let y_target: [ByteTarget; 16] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + + let out_target = gf_2_128_mul_target(&mut builder, x_target, y_target); + + println!("gf_2_128_mul (L:{}) num_gates: {}", L, builder.num_gates()); + let data = builder.build::<PoseidonGoldilocksConfig>(); + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + std::iter::zip(x_target, x).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + std::iter::zip(y_target, y).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + + std::iter::zip(out_target, expected) + .try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + + let proof = data.prove(pw)?; + data.verify(proof) + } + + #[test] + fn test_ghash() -> Result<()> { + // AES-GCM-128 + test_ghash_opt::<4, 4, 10, 16>()?; + test_ghash_opt::<4, 4, 10, 32>()?; + + // AES-GCM-256 + test_ghash_opt::<8, 4, 14, 16>()?; + + Ok(()) + } + fn test_ghash_opt<const NK: usize, const NB: usize, const NR: usize, const L: usize>() + -> Result<()> + where + [(); 4 * (NR + 1)]:, + [(); NK * NB]:, + { + let h: [u8; 16] = [222; 16]; + let x: &[u8; L] = &[42u8; L]; + + let expected = ghash(h, x); + + // Circuit declaration + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let h_target: [ByteTarget; 16] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + let x_target: [ByteTarget; L] = + array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); + + // let ghash_out_target = ghash_target::<L>(&mut builder, h_target, &x_target); + let ghash_out_target = ghash_target(&mut builder, h_target, x_target.to_vec()); + + println!("ghash (L:{}) num_gates: {}", L, builder.num_gates()); + let data = builder.build::<PoseidonGoldilocksConfig>(); + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + std::iter::zip(h_target, h).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + std::iter::zip(x_target, x).try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?; + + std::iter::zip(ghash_out_target, expected) + .try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + + let proof = data.prove(pw)?; + data.verify(proof) + } + + #[test] + fn test_encrypt() -> Result<()> { + // AES-GCM-128 + test_encrypt_opt::<4, 4, 10, 13, false>(false)?; + test_encrypt_opt::<4, 4, 10, 13, true>(false)?; // with tag + test_encrypt_opt::<4, 4, 10, 17, false>(false)?; + + // AES-GCM-256 + test_encrypt_opt::<8, 4, 14, 13, false>(false)?; + + Ok(()) + } + // run it with: cargo test -- --ignored + #[ignore] + #[test] + fn test_encrypt_report_sizes() -> Result<()> { + // AES-GCM-128 + test_encrypt_opt::<4, 4, 10, 16, false>(true)?; + test_encrypt_opt::<4, 4, 10, 17, false>(true)?; + test_encrypt_opt::<4, 4, 10, 32, false>(true)?; + test_encrypt_opt::<4, 4, 10, 33, false>(true)?; + test_encrypt_opt::<4, 4, 10, 64, false>(true)?; + test_encrypt_opt::<4, 4, 10, 128, false>(true)?; + test_encrypt_opt::<4, 4, 10, 256, false>(true)?; + test_encrypt_opt::<4, 4, 10, 512, false>(true)?; + test_encrypt_opt::<4, 4, 10, 1024, false>(true)?; + test_encrypt_opt::<4, 4, 10, 2048, false>(true)?; + + // AES-GCM-256 + test_encrypt_opt::<8, 4, 14, 16, false>(true)?; + test_encrypt_opt::<8, 4, 14, 17, false>(true)?; + test_encrypt_opt::<8, 4, 14, 32, false>(true)?; + test_encrypt_opt::<8, 4, 14, 33, false>(true)?; + test_encrypt_opt::<8, 4, 14, 64, false>(true)?; + test_encrypt_opt::<8, 4, 14, 128, false>(true)?; + test_encrypt_opt::<8, 4, 14, 256, false>(true)?; + test_encrypt_opt::<8, 4, 14, 512, false>(true)?; + test_encrypt_opt::<8, 4, 14, 1024, false>(true)?; + test_encrypt_opt::<8, 4, 14, 2048, false>(true)?; + + Ok(()) + } + fn test_encrypt_opt< + const NK: usize, + const NB: usize, + const NR: usize, + const L: usize, + const TAG: bool, + >( + only_build: bool, + ) -> Result<()> + where + [(); 4 * (NR + 1)]:, + [(); NK * NB]:, + { + let key: &[u8; NK * NB] = &[42; NK * NB]; + let nonce: &[u8; 12] = &[111; 12]; + let pt: &[u8; L] = &[42u8; L]; + + let (ct, tag) = crate::native_gcm::encrypt::<NK, NB, NR>(key, nonce, pt); + + // Circuit declaration + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let aes_targets = AesGcmTarget::<NK, NB, NR, L, TAG>::new_virtual(&mut builder); + aes_targets.build_circuit(&mut builder); + + println!( + "encrypt (NK:{}, NB:{}, NR:{}, L:{}, TAG:{}) num_gates: {}", + NK, + NB, + NR, + L, + TAG, + builder.num_gates() + ); + let data = builder.build::<PoseidonGoldilocksConfig>(); + + if only_build { + return Ok(()); + } + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + aes_targets.set_targets(&mut pw, key, nonce, pt, &ct, &tag)?; + + let proof = data.prove(pw)?; + data.verify(proof) + } +}
@@ -1,7 +1,10 @@
-//! For LICENSE check out https://github.com/0xPARC/plonky2-aes/blob/main/LICENSE +//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE //! //! Constants for the implementation of AES following the specification from //! https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197-upd1.pdf. + +/// supported tag length +pub const TAG_LEN: usize = 128; /// table 4, page 14 pub(crate) const SBOX: [u8; 256] = [
@@ -1,12 +1,26 @@
-//! For LICENSE check out https://github.com/0xPARC/plonky2-aes/blob/main/LICENSE +//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE #![allow(incomplete_features)] #![feature(generic_const_exprs)] #![allow(dead_code)] // TMP -mod circuit_aes; -mod constants; -mod native_aes; -mod native_gcm; +pub mod circuit_aes; +pub mod circuit_gcm; +pub mod constants; +pub mod native_aes; +pub mod native_gcm; /// D defines the extension degree of the field used in the Plonky2 proofs (quadratic extension). pub const D: usize = 2; + +pub use circuit_gcm::AesGcmTarget; + +// expose pre-defined configurations (AES-128, AES-192, AES-256): + +pub type AesGcm128Target<const L: usize> = AesGcmTarget<4, 4, 10, L, false>; +pub const KEY_LEN_128: usize = 4 * 4; + +pub const KEY_LEN_192: usize = 6 * 4; +pub type AesGcm192Target<const L: usize> = AesGcmTarget<6, 4, 12, L, false>; + +pub const KEY_LEN_256: usize = 8 * 4; +pub type AesGcm256Target<const L: usize> = AesGcmTarget<8, 4, 14, L, false>;
@@ -1,4 +1,4 @@
-//! For LICENSE check out https://github.com/0xPARC/plonky2-aes/blob/main/LICENSE +//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE //! //! Rust native implementation of //! [AES](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197-upd1.pdf) having@@ -33,7 +33,6 @@
let mut s: State = [[0; 4]; 4]; for i in 0..4 { for j in 0..4 { - // TODO s[i][j] = input[i + 4 * j]; } }@@ -152,6 +151,14 @@
#[cfg(test)] mod tests { use super::*; + + #[test] + fn test_flatten_state() { + let s: [[u8; 4]; 4] = array::from_fn(|i| array::from_fn(|j| (i * 4 + j) as u8)); + + // the array::from_fn version is what we use later in the circuit + assert_eq!(flatten_state(s), array::from_fn(|i| s[i % 4][i / 4])); + } /// test against test vectors from Appendix A #[test]
@@ -1,4 +1,4 @@
-//! For LICENSE check out https://github.com/0xPARC/plonky2-aes/blob/main/LICENSE +//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE //! //! Rust native implementation of [AES-GCM (Galois Counter //! Mode)](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf)@@ -7,10 +7,10 @@ //! is not written in the most rust idiomatic way, nor memory-efficient, but
//! simulating the behavior that we will do later inside the circuit. //! -use crate::native_aes::{encrypt_block, flatten_state, key_expansion}; - -// supported tag length -const TAG_LEN: usize = 128; +use crate::{ + constants::TAG_LEN, + native_aes::{encrypt_block, flatten_state, key_expansion}, +}; /// Section 7.1 from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf pub fn encrypt<const NK: usize, const NB: usize, const NR: usize>(@@ -21,7 +21,7 @@ ) -> (Vec<u8>, Vec<u8>)
where [(); 4 * (NR + 1)]:, { - let a: &[u8] = &[]; // additional authenticated data // TODO maybe as input + let a: &[u8] = &[]; // additional authenticated data let expanded_key: [[u8; 4]; 4 * (NR + 1)] = key_expansion::<NK, NB, NR>(key); // 1. CIPH_K@@ -43,6 +43,7 @@ // 3. C=GCTR()
let c = gctr(expanded_key, &inc32(j0), pt); // 4. u, v + // note: c.len() will be x.len(), "L" in the circuit let u: usize = 16 * (c.len() as f64 / 16_f64).ceil() as usize - c.len(); let v: usize = 16 * (a.len() as f64 / 16_f64).ceil() as usize - a.len();@@ -67,7 +68,11 @@ (c, t)
} /// GCM-Ctr, Section 6.5, Algorithm 3 -fn gctr<const NR: usize>(key: [[u8; 4]; 4 * (NR + 1)], icb: &[u8; 16], x: &[u8]) -> Vec<u8> { +pub(crate) fn gctr<const NR: usize>( + key: [[u8; 4]; 4 * (NR + 1)], + icb: &[u8; 16], + x: &[u8], +) -> Vec<u8> { if x.is_empty() { return x.to_vec(); }@@ -103,7 +108,7 @@ y.concat()
} /// Section 6.4, Algorithm 2 -fn ghash(h: [u8; 16], x: &[u8]) -> [u8; 16] { +pub(crate) fn ghash(h: [u8; 16], x: &[u8]) -> [u8; 16] { assert!(x.len().is_multiple_of(16)); // multiple of 128 bits let m = x.len() / 16;@@ -151,7 +156,71 @@ }
} z } -fn right_shift_one(block: &mut [u8; 16]) { + +// auxiliary version for testing the circuit logic +pub(crate) fn gf_2_128_mul_circuit_version( + x: [[bool; 8]; 16], + y: [[bool; 8]; 16], +) -> [[bool; 8]; 16] { + let zero_byte = [false; 8]; + + // R: 10000111 || 0^120 (in little-endian) + let mut r = [zero_byte; 16]; + r[0][0] = true; + r[0][5] = true; + r[0][6] = true; + r[0][7] = true; + + let mut z = [zero_byte; 16]; + let mut v = y; + for i in 0..128 { + let byte_index = i / 8; + let bit_index = 7 - (i % 8); + let xi = x[byte_index][bit_index]; + + // set z = if xi==1: z^v, else: z + for b in 0..16 { + for k in 0..8 { + let z_xor_v = z[b][k] ^ v[b][k]; + z[b][k] = if xi { z_xor_v } else { z[b][k] }; + } + } + + let lsb = v[15][0]; // (little-endian) + v = right_shift_one_circuit_version(&v); + + // if lsb==1: v=v^R, else: v + for b in 0..16 { + for k in 0..8 { + let v_xor_r = v[b][k] ^ r[b][k]; + v[b][k] = if lsb { v_xor_r } else { v[b][k] }; + } + } + } + z +} +// method for debugging +fn bools_to_u8_le(bits: [bool; 8]) -> u8 { + bits.iter() + .enumerate() + .fold(0u8, |acc, (i, &bit)| if bit { acc | (1 << i) } else { acc }) +} +fn right_shift_one_circuit_version(v: &[[bool; 8]; 16]) -> [[bool; 8]; 16] { + let mut r: [[bool; 8]; 16] = *v; + let mut carry = false; + for i in 0..16 { + let current = v[i]; + let next_carry = current[0]; + let mut shifted = [false; 8]; + shifted[..7].copy_from_slice(¤t[1..(7 + 1)]); + shifted[7] = carry; + r[i] = shifted; + carry = next_carry; + } + r +} + +pub(crate) fn right_shift_one(block: &mut [u8; 16]) { let mut carry = 0u8; for byte in block.iter_mut() {@@ -162,7 +231,7 @@ }
} /// increment the right-most 32 bits of the given block (128 bits). Section 2. -pub fn inc32(b: [u8; 16]) -> [u8; 16] { +pub(crate) fn inc32(b: [u8; 16]) -> [u8; 16] { let mut r = b; // counter = last 32bits let counter = u32::from_be_bytes([r[12], r[13], r[14], r[15]]);@@ -173,7 +242,7 @@ r
} /// returns the t left-most (most-significant in BE) bits of the block -pub fn msb_t(t_bytes: usize, block: &[u8]) -> Vec<u8> { +pub(crate) fn msb_t(t_bytes: usize, block: &[u8]) -> Vec<u8> { let t = t_bytes * 8; let mut out = Vec::with_capacity(t.div_ceil(8));
@@ -0,0 +1,9 @@
+[package] +name = "plonky2-feistel" +version = "0.1.0" +edition = "2024" + +[dependencies] +plonky2 = { workspace = true } +anyhow = { workspace = true } +rand = "0.8.5"
@@ -0,0 +1,153 @@
+use std::array; + +use plonky2::{ + field::{extension::Extendable, goldilocks_field::GoldilocksField as F}, + hash::hash_types::RichField, + iop::{ + target::Target, + witness::{PartialWitness, WitnessWrite}, + }, + plonk::circuit_builder::CircuitBuilder, +}; + +pub(crate) const D: usize = 2; + +/// State on which the Feistel-like cipher operates +#[derive(Debug, Copy, Clone)] +pub struct FeistelStateTarget<const STATE_LEN: usize>([Target; STATE_LEN]); + +pub trait CircuitBuilderFeistel<F: RichField + Extendable<D>, const D: usize> { + /// Feistel state target constructor + fn add_feistel_state_target<const STATE_LEN: usize>(&mut self) + -> FeistelStateTarget<STATE_LEN>; + + /// Generates Feistel-like cipher as in `crate::feistel_cipher` + fn feistel_cipher<const STATE_HALF_LEN: usize, const KEY_LEN: usize>( + &mut self, + state: FeistelStateTarget<{ 2 * STATE_HALF_LEN }>, + key_schedule: &[[Target; KEY_LEN]], + f: &impl Fn(&mut Self, Vec<Target>) -> [Target; STATE_HALF_LEN], + ) -> FeistelStateTarget<{ 2 * STATE_HALF_LEN }>; +} + +impl CircuitBuilderFeistel<F, D> for CircuitBuilder<F, D> { + fn add_feistel_state_target<const STATE_LEN: usize>( + &mut self, + ) -> FeistelStateTarget<STATE_LEN> { + FeistelStateTarget(self.add_virtual_target_arr()) + } + + fn feistel_cipher<const STATE_HALF_LEN: usize, const KEY_LEN: usize>( + &mut self, + state: FeistelStateTarget<{ 2 * STATE_HALF_LEN }>, + key_schedule: &[[Target; KEY_LEN]], + f: &impl Fn(&mut Self, Vec<Target>) -> [Target; STATE_HALF_LEN], + ) -> FeistelStateTarget<{ 2 * STATE_HALF_LEN }> { + if key_schedule.is_empty() { + state + } else { + let k = key_schedule[0]; + let l = &state.0[..STATE_HALF_LEN]; + let r = &state.0[STATE_HALF_LEN..]; + + let new_l = r; + let round_offset = f(self, [r, &k].concat()); + let new_r: [_; STATE_HALF_LEN] = array::from_fn(|i| self.add(l[i], round_offset[i])); + self.feistel_cipher( + FeistelStateTarget(array::from_fn(|i| { + if i < STATE_HALF_LEN { + new_l[i] + } else { + new_r[i - STATE_HALF_LEN] + } + })), + &key_schedule[1..], + f, + ) + } + } +} + +pub trait PartialWitnessFeistel { + /// Sets witnesses for a Feistel state target + fn set_feistel_state_target<const STATE_SIZE: usize>( + &mut self, + target: &FeistelStateTarget<STATE_SIZE>, + value: &[F; STATE_SIZE], + ) -> anyhow::Result<()>; +} + +impl PartialWitnessFeistel for PartialWitness<F> { + fn set_feistel_state_target<const STATE_SIZE: usize>( + &mut self, + target: &FeistelStateTarget<STATE_SIZE>, + value: &[F; STATE_SIZE], + ) -> anyhow::Result<()> { + self.set_target_arr(&target.0, value) + } +} + +#[cfg(test)] +mod tests { + use plonky2::{ + field::goldilocks_field::GoldilocksField as F, + hash::{ + hashing::hash_n_to_hash_no_pad, + poseidon::{PoseidonHash, PoseidonPermutation}, + }, + iop::witness::{PartialWitness, WitnessWrite}, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, + }; + + use super::D; + use crate::{ + circuit::{CircuitBuilderFeistel, FeistelStateTarget, PartialWitnessFeistel}, + feistel_cipher, + tests::random_fields, + }; + + #[test] + fn feistel_poseidon_check() -> anyhow::Result<()> { + const NR: usize = 32; + const STATE_HALF_LEN: usize = 4; + const KEY_LEN: usize = 4; + + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let state_target: FeistelStateTarget<{ 2 * STATE_HALF_LEN }> = + builder.add_feistel_state_target(); + let key_schedule_target: [[_; KEY_LEN]; NR] = + std::array::from_fn(|_| builder.add_virtual_target_arr()); + + let f = |builder: &mut CircuitBuilder<F, D>, targets: Vec<_>| { + builder + .hash_n_to_hash_no_pad::<PoseidonHash>(targets) + .elements + }; + + let out_state_target = builder.feistel_cipher(state_target, &key_schedule_target, &f); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + (0..10).try_for_each(|_| { + let state: [_; 2 * STATE_HALF_LEN] = random_fields(); + let key_schedule: [[_; KEY_LEN]; NR] = std::array::from_fn(|_| random_fields()); + let eff = |fields: Vec<_>| { + hash_n_to_hash_no_pad::<F, PoseidonPermutation<_>>(&fields).elements + }; + let out_state = feistel_cipher(state, &key_schedule, &eff); + + let mut pw = PartialWitness::new(); + pw.set_feistel_state_target(&state_target, &state)?; + std::iter::zip(key_schedule_target, key_schedule) + .try_for_each(|(t, v)| pw.set_target_arr(&t, &v))?; + pw.set_feistel_state_target(&out_state_target, &out_state)?; + + let proof = data.prove(pw)?; + data.verify(proof) + }) + } +}
@@ -0,0 +1,119 @@
+#![allow(incomplete_features)] +#![feature(generic_const_exprs)] + +pub mod circuit; + +use std::array; + +use plonky2::field::types::Field; + +/// Feistel-type cipher for finite fields, where XORing is replaced +/// with addition, cf. https://en.wikipedia.org/wiki/Feistel_cipher or +/// §3.5.1 of Stinson's *Cryptography* (3rd edition). +pub fn feistel_cipher<const STATE_HALF_LEN: usize, const KEY_LEN: usize, F: Field>( + state: [F; 2 * STATE_HALF_LEN], + key_schedule: &[[F; KEY_LEN]], + f: &impl Fn(Vec<F>) -> [F; STATE_HALF_LEN], +) -> [F; 2 * STATE_HALF_LEN] { + if key_schedule.is_empty() { + state + } else { + let k = key_schedule[0]; + let l = &state[..STATE_HALF_LEN]; + let r = &state[STATE_HALF_LEN..]; + + let new_l = r; + let round_offset = f([r, &k].concat()); + let new_r: [_; STATE_HALF_LEN] = array::from_fn(|i| l[i] + round_offset[i]); + + feistel_cipher( + array::from_fn(|i| { + if i < STATE_HALF_LEN { + new_l[i] + } else { + new_r[i - STATE_HALF_LEN] + } + }), + &key_schedule[1..], + f, + ) + } +} + +/// Feistel-type inverse cipher for finite fields, where XORing is +/// replaced with subtraction. +pub fn feistel_inv_cipher<const STATE_HALF_LEN: usize, const KEY_LEN: usize, F: Field>( + state: [F; 2 * STATE_HALF_LEN], + reverse_key_schedule: &[[F; KEY_LEN]], + f: &impl Fn(Vec<F>) -> [F; STATE_HALF_LEN], +) -> [F; 2 * STATE_HALF_LEN] { + if reverse_key_schedule.is_empty() { + state + } else { + let k = reverse_key_schedule[0]; + let l = &state[..STATE_HALF_LEN]; + let r = &state[STATE_HALF_LEN..]; + + let new_r = l; + let round_offset = f([l, &k].concat()); + let new_l: [_; STATE_HALF_LEN] = array::from_fn(|i| r[i] - round_offset[i]); + + feistel_inv_cipher( + array::from_fn(|i| { + if i < STATE_HALF_LEN { + new_l[i] + } else { + new_r[i - STATE_HALF_LEN] + } + }), + &reverse_key_schedule[1..], + f, + ) + } +} + +#[cfg(test)] +mod tests { + use plonky2::{ + field::{ + goldilocks_field::GoldilocksField as F, + types::{Field, Field64}, + }, + hash::{ + hashing::hash_n_to_hash_no_pad, keccak::KeccakPermutation, + poseidon::PoseidonPermutation, + }, + }; + use rand::{Rng, rngs::OsRng}; + + use crate::{feistel_cipher, feistel_inv_cipher}; + + pub(crate) fn random_fields<const N: usize>() -> [F; N] { + std::array::from_fn(|_| F::from_canonical_u64(OsRng.gen_range(0..F::ORDER))) + } + + #[test] + fn feistel_keccak_poseidon_roundtrip() { + const NR: usize = 32; + const STATE_HALF_LEN: usize = 4; + const KEY_LEN: usize = 4; + + let f_keccak = + |fields: Vec<_>| hash_n_to_hash_no_pad::<F, KeccakPermutation<_>>(&fields).elements; + let f_poseidon = + |fields: Vec<_>| hash_n_to_hash_no_pad::<F, PoseidonPermutation<_>>(&fields).elements; + + [f_keccak, f_poseidon].into_iter().for_each(|f| { + (0..10).for_each(|_| { + let state: [_; 2 * STATE_HALF_LEN] = random_fields(); + let key_schedule: [[_; KEY_LEN]; NR] = std::array::from_fn(|_| random_fields()); + + let out_state = feistel_cipher(state, &key_schedule, &f); + let reverse_key_schedule = key_schedule.into_iter().rev().collect::<Vec<_>>(); + let maybe_original_state = feistel_inv_cipher(out_state, &reverse_key_schedule, &f); + + assert_eq!(state, maybe_original_state); + }) + }) + } +}