add modulus switching to GLWE ciphertexts (and Zq,Rq)
arnaucube git@arnaucube.com
Wed, 16 Jul 2025 18:15:51 +0200
8 files changed,
60 insertions(+),
16 deletions(-)
M
Cargo.toml
→
Cargo.toml
@@ -1,7 +1,7 @@
[workspace] members = [ "arith", - "generalized-fhe", + "gfhe", "bfv", "ckks" ]
M
README.md
→
README.md
@@ -2,7 +2,7 @@ # fhe-study
Implementations from scratch done while studying some FHE papers; do not use in production. - `arith`: contains $\mathbb{Z}_q$, $R_q=\mathbb{Z}_q[X]/(X^N+1)$ and $R=\mathbb{Z}[X]/(X^N+1)$ arithmetic implementations, together with the NTT implementation. -- `generalized-fhe`: contains the structs and logic for RLWE, GLWE, GLev, GGSW, RGSW cryptosystems, which can be used by concrete FHE schemes. +- `gfhe`: (gfhe=generalized-fhe) contains the structs and logic for RLWE, GLWE, GLev, GGSW, RGSW cryptosystems, and modulus switching and key switching methods, which can be used by concrete FHE schemes. - `bfv`: https://eprint.iacr.org/2012/144.pdf scheme implementation - `ckks`: https://eprint.iacr.org/2016/421.pdf scheme implementation
M
arith/src/ringq.rs
→
arith/src/ringq.rs
@@ -49,9 +49,6 @@ }
} } -// TODO define a trait "PolynomialRingTrait" or similar, so that when other structs use it can just -// use the trait and not need to add '<Q, N>' to their params - impl<const Q: u64, const N: usize> From<crate::ring::R<N>> for Rq<Q, N> { fn from(r: crate::ring::R<N>) -> Self { Self::from_vec(@@ -165,7 +162,7 @@ Rq::<P, N>::from_vec_u64(self.coeffs().iter().map(|m_i| m_i.0).collect())
} /// perform the mod switch operation from Q to Q', where Q2=Q' - fn mod_switch<const Q2: u64>(&self) -> Rq<Q2, N> { + pub fn mod_switch<const Q2: u64>(&self) -> Rq<Q2, N> { Rq::<Q2, N> { coeffs: array::from_fn(|i| self.coeffs[i].mod_switch::<Q2>()), evals: None,
M
arith/src/traits.rs
→
arith/src/traits.rs
@@ -4,7 +4,7 @@ use std::fmt::Debug;
use std::iter::Sum; use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; -/// represents a ring element +/// Represents a ring element. Currently implemented by ring.rs#R and ringq.rs#Rq. pub trait Ring: Sized + Add<Output = Self>@@ -25,6 +25,6 @@ type C: Debug + Clone;
fn coeffs(&self) -> Vec<Self::C>; fn zero() -> Self; - fn rand(rng: impl Rng, dist: impl Distribution<f64>) -> Self; // note/wip/warning: dist (0,q) with f64, will output more '0=q' elements than other values + fn rand(rng: impl Rng, dist: impl Distribution<f64>) -> Self; }
M
generalized-fhe/Cargo.toml
→
gfhe/Cargo.toml
@@ -1,5 +1,5 @@
[package] -name = "generalized-fhe" +name = "gfhe" version = "0.1.0" edition = "2024"
M
generalized-fhe/README.md
→
gfhe/README.md
@@ -1,2 +1,2 @@
-# common +# gfhe - generalized-fhe Contains the structs and logic for RLWE, GLWE, GLev, GGSW, RGSW cryptosystems, which can be used by concrete FHE schemes.
M
generalized-fhe/src/glwe.rs
→
gfhe/src/glwe.rs
@@ -69,13 +69,21 @@ }
pub fn decrypt<const T: u64>(&self, sk: &SecretKey<Q, N, K>, delta: u64) -> Rq<T, N> { let (d, b): (TR<Rq<Q, N>, K>, Rq<Q, N>) = (self.0.clone(), self.1); let r: Rq<Q, N> = b - &d * &sk.0; - let r_scaled: Vec<f64> = r - .coeffs() - .iter() - .map(|e| (e.0 as f64 / delta as f64).round()) - .collect(); - let r = Rq::<Q, N>::from_vec_f64(r_scaled); + let r = r.mul_div_round(T, Q); + // let r_scaled: Vec<f64> = r + // .coeffs() + // .iter() + // // .map(|e| (e.0 as f64 / delta as f64).round()) + // .map(|e| e.mul_div_round(T, Q)) + // .collect(); + // let r = Rq::<Q, N>::from_vec_f64(r_scaled); r.remodule::<T>() + } + + pub fn mod_switch<const P: u64>(&self) -> GLWE<P, N, K> { + let a: TR<Rq<P, N>, K> = TR(self.0 .0.iter().map(|r| r.mod_switch::<P>()).collect()); + let b: Rq<P, N> = self.1.mod_switch::<P>(); + GLWE(a, b) } }@@ -229,6 +237,45 @@ let c3 = c1 * m2;
let m3_recovered: Rq<T, N> = c3.decrypt(&sk, delta); assert_eq!((m1.to_r() * m2.to_r()).to_rq::<T>(), m3_recovered); + } + + Ok(()) + } + + #[test] + fn test_mod_switch() -> Result<()> { + const Q: u64 = 2u64.pow(16) + 1; + const P: u64 = 2u64.pow(8) + 1; + // note: wip, Q and P chosen so that P/Q is an integer + const N: usize = 8; + const T: u64 = 8; // plaintext modulus, must be a prime or power of a prime + const K: usize = 16; + type S = GLWE<Q, N, K>; + + let delta: u64 = Q / T; // floored + let mut rng = rand::thread_rng(); + + dbg!(P as f64 / Q as f64); + dbg!(delta); + dbg!(delta as f64 * P as f64 / Q as f64); + dbg!(delta as f64 * (P as f64 / Q as f64)); + + for _ in 0..200 { + let (sk, pk) = S::new_key(&mut rng)?; + + let msg_dist = Uniform::new(0_u64, T); + let m = Rq::<T, N>::rand_u64(&mut rng, msg_dist)?; + + let c = S::encrypt(&mut rng, &pk, &m, delta)?; + + let c2 = c.mod_switch::<P>(); + let sk2: SecretKey<P, N, K> = + SecretKey(TR(sk.0 .0.iter().map(|s_i| s_i.remodule::<P>()).collect())); + let delta2: u64 = ((P as f64 * delta as f64) / Q as f64).round() as u64; + + let m_recovered = c2.decrypt(&sk2, delta2); + + assert_eq!(m, m_recovered); } Ok(())