initial commit

This commit is contained in:
2025-05-21 22:51:23 +02:00
commit 09a5754cf9
6 changed files with 560 additions and 0 deletions

137
README Normal file
View File

@ -0,0 +1,137 @@
An implementation of the DGHV fully homomorphic scheme
This is an implementation of the DGHV fully homomorphic scheme with
compressed public-key. This implementation is described in the
following article:
[1] J.S. Coron, D. Naccache and M. Tibouchi, "Public-key Compression and
Modulus Switching for Fully Homomorphic Encryption over the Integers",
Proceedings of Eurocrypt 2012.
available at http://eprint.iacr.org/2011/440
The implementation is done with the SAGE 4.7.2 mathematical library
under Python, available at http://www.sagemath.org/.
WHAT IS FULLY HOMOMORPHIC ENCRYPTION ?
--------------------------------------
An encryption scheme is fully homomorphic when it is possible to
perform implicit addition and multiplication of plaintexts while
manipulating only ciphertexts. The first construction of a fully
homomorphic encryption (FHE) scheme was described by Gentry in 2009.
WHAT IS THE DGHV SCHEME ?
-------------------------
The DGHV scheme is a FHE scheme published in:
[2] M. van Dijk, C. Gentry, S. Halevi and V. Vaikuntanathan, "Fully
Homomorphic Encryption over the Integers". Proceedings of Eurocrypt
2010.
and available at http://eprint.iacr.org/2009/616
In DGHV a ciphertext has the form:
c= q*p + 2*r + m
where p is the secret-key, m is the bit plaintext (m=0 or 1), q is a
large random, and r is a small random.
To decrypt, compute m=(c mod p) mod 2. It is easy to see that
decryption works as long as the noise 2*r is smaller than p.
Given two ciphertexts
c1= q1*p + 2*r1 + m1
c2= q2*p + 2*r2 + m2
we have:
c1+c2=(q1+q2)*p+2*(r1+r2)+m1+m2
Therefore one can obtain the encryption of m1+m2 (mod 2)=m1 xor m2 simply by
computing c1+c2.
Similarly we have:
c1*c2=q12*p+2*(2*r1*r2+r1*m2+r2*m1)+m1*m2
Therefore one can obtain the encryption of m1*m2 simply by computing
c1*c2. However one gets a new ciphertext with noise roughly
twice larger than in the original ciphertexts c1 and c2. Since the
noise must remain below p, the number of permitted multiplications on
ciphertexts is therefore limited. This is called a somewhat
homomorphic encryption scheme.
To obtain a fully homomorphic encryption scheme, i.e. unlimited
addition and multiplication on ciphertexts, one must be able to reduce
the amount of noise in a ciphertext; this is called a ciphertext
refresh.
Gentry's key idea to refresh a ciphertext is to homomorphically
evaluate the decryption circuit on the ciphertext bits, using an
encryption of the secret-key bits. This is called bootstrapping.
Then instead of getting the plaintext bit (as we would get if we
would evaluate the decryption circuit with the secret-key bits in
clear), we get an __encryption__ of the plaintext bit, i.e. a new
ciphertext for the same plaintext. Now if the decryption circuit has a
small enough depth, then the amount of noise in the new ciphertext can
be actually smaller than in the original plaintext, hence a ciphertext refresh.
However the previous decryption algorithm m=(c mod p) mod 2 does not
have a small depth, therefore the decryption procedure must be "squashed" so
that it can be expressed as a low depth circuit. How this is done in
explained in [2].
So far we have only described a secret-key encryption scheme, i.e. to
encrypt one must know the secret-key p. However it is easy to obtain a
public-key encryption scheme. For this generate a public set of
ciphertexts xi which are all different encryptions of 0:
xi=qi*p+2*ri
and to encrypt a bit m compute:
c=m+2*r+random_subset_sum(xi)
It is easy to see that c is indeed an encryption of the bit m.
WHAT IS IMPLEMENTED ?
---------------------
We provide an implementation of the DGHV scheme with the fully
homomorphic capability, i.e. we implement the key generation,
encryption, decryption, add, mult and ciphertext refresh procedures.
The implementation is done using the Sage 4.7.2 mathematical library
under Python, available at http://www.sagemath.org/
First run sage, then type:
sage: load "dghv.sage"
sage: testPkRecrypt()
The testPkRecrypt() function demonstrates key generation, addition and
multiplication of ciphertexts, and ciphertext refresh. It runs for
four sets of parameters (toy, small, medium and large), as described
in [1].
WHAT IS A COMPRESSED PUBLIC-KEY ?
---------------------------------
In DGHV the ciphertext size must be huge to prevent attacks based on
lattice reduction algorithms; at least 10^7 bits. The secret p is
comparatively smaller, roughly 2000 bits. Since roughly 10^4
ciphertexts xi must be included in the public-key, that would give a
public-key size of 10^11 bits, i.e. 12.5 GB.
To reduce the public-key size we implement the following technique
described in [1]. Instead of generating xi as xi=qi*p+2*ri, one first
generates a pseudo-random Xi of the same size, and computes a small
correction di such that xi=Xi-di is small modulo p. Then only these
small corrections need to be stored in the public-key, with the seed
of the PRNG. Using this compression technique the public-key size
becomes 10^4*(2*10^3)=2*10^7 bits, i.e. 2.5 MB, which is more
manageable.

1
README.md Normal file
View File

@ -0,0 +1 @@
DGHV sage implementation ported to python3 from the original python2 version available here: https://github.com/coron/fhe

234
dghv.sage Normal file
View File

@ -0,0 +1,234 @@
#
# dghv.sage: an implementation of DGHV-FHE with compressed public-key
#
# as described in:
#
# J.S. Coron, D. Naccache and M. Tibouchi, "Public-key Compression and Modulus
# Switching for Fully Homomorphic Encryption over the Integers"
#
# available at http://eprint.iacr.org/2011/440
#
# Copyright (c) 2012 Jean-Sebastien Coron <jean-sebastien.coron@uni.lu>
# and Mehdi Tibouchi <mehdi.tibouchi@normalesup.org>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 as published
# by the Free Software Foundation.
load scalprod.spyx
load utils.sage
from itertools import chain, islice, count
from time import time
theta = 15
n = 4
class Pk(object):
"The PK of the DGHV somewhat homomorphic scheme, without the xi's"
def __init__(self, rho, eta, gam, modx0=True, verbose=True, *args, **kwargs):
print(" Pk: generation of p")
self.rho, self.eta, self.gam, self.modx0 = rho, eta, gam, modx0
self.p = random_prime(2^self.eta, lbound=2^(self.eta-1), proof=False)
if self.modx0:
self.x0 = self.p * RandomOdd(self.gam - self.eta)
def encrypt(self, m):
return self.p * ZZ.random_element(2^(self.gam - self.eta - 1)) + 2 * ZZ.random_element(-2^self.rho + 1, 2^self.rho) + m
def noise(self, c):
return modNear(c, self.p)
def decrypt(self, c):
return mod(self.noise(c), 2)
def add(self, c1, c2):
return self.modx0 and mod(c1 + c2, self.x0).lift() or (c1 + c2)
def mult(self, c1, c2):
return self.modx0 and mod(c1 * c2, self.x0).lift() or (c1 * c2)
def __repr__(self):
return "<Pk with rho=%d, eta=%d, gam=%d>" % (self.rho, self.eta, self.gam)
class Ciphertext():
def __init__(self, val_, pk_, degree_=1):
self.val, self.pk, self.degree = val_, pk_, degree_
@staticmethod
def encrypt(pk, m=None):
if m == None:
m = ZZ.random_element(2)
return Ciphertext(pk.encrypt(m), pk)
def noise(self):
return self.pk.noise(self.val)
def decrypt(self, verbose=False):
t = cputime(subprocesses=True)
m = self.pk.decrypt(self.val)
if verbose:
print("Decrypt", cputime(subprocesses=True) - t)
return m
def __repr__(self):
return "<Ciphertext with m=%d size=%d noise=%d deg=%d>" % (self.decrypt(), self.val.nbits(), self.noise().nbits(), self.degree)
def __add__(self, x):
return self.__class__(self.pk.add(self.val, x.val), self.pk, max(self.degree, x.degree))
def __mul__(self, x):
return self.__class__(self.pk.mult(self.val, x.val), self.pk, self.degree + x.degree)
def scalmult(self, x):
if isinstance(x, array):
return array([self.__class__(self.val * xi, self.pk, self.degree) for xi in x])
else:
return self.__class__(self.val * x, self.pk, self.degree)
def expand(self):
return self.pk.expand(self.val)
class PRIntegers:
"A list of pseudo-random integers."
def __init__(self, gam, ell):
self.gam, self.ell = gam, ell
self.li = [None for i in range(self.ell)]
set_random_seed()
self.se = initial_seed()
def __getitem__(self, i):
return self.li[i]
def __setitem__(self, i, val):
self.li[i] = val
def __iter__(self):
set_random_seed(self.se)
for i in range(self.ell):
a = ZZ.random_element(2^self.gam)
if self.li[i] != None:
yield self.li[i]
else:
yield a
set_random_seed()
class PRIntegersDelta(PRIntegers):
"""A list of pseudo-random integers, with their delta corrections"""
def __iter__(self):
return (c + d for c, d in zip(PRIntegers.__iter__(self), self.delta))
def ciphertexts(self, pk):
return (Ciphertext(cd, pk) for cd in self)
@staticmethod
def encrypt(pk, v):
pr = PRIntegersDelta(pk.gam, len(v))
r = [ZZ.random_element(-2^pk.rho + 1, 2^pk.rho) for i in range(len(v))]
pr.delta = [0 for i in range(len(v))]
temp = [-mod(xi, pk.p) for xi in PRIntegers.__iter__(pr)]
pr.delta = [te.lift() + 2 * ri + vi for te, ri, vi in zip(temp, r, v)]
return pr
class PkRecrypt(Pk):
"The Pk of the DGHV scheme, with the xi's, the yi's and the encrypted secret-key bits"
def __init__(self, rho, eta, gam, Theta, pkRecrypt=None, *args, **kwargs):
t = cputime(subprocesses=True)
super(PkRecrypt, self).__init__(rho, eta, gam, *args, **kwargs)
self.kap = 64 * (self.gam // 64 + 1) - 1
self.Theta = Theta
self.alpha, self.tau = kwargs['alpha'], kwargs['tau']
xp = QuotientNear(2^self.kap, self.p)
B = self.Theta // theta
assert self.Theta % theta == 0, "Theta must be a multiple of theta"
print(" PkRecrypt: generation of s")
self.s = [1] + [0 for i in range(B - 1)] + sum2([randSparse(B, 1) for i in range(theta - 1)])
t2 = cputime(subprocesses=True)
self.y = PRIntegers(self.kap, self.Theta)
self.y[0] = 0
self.y[0] = mod(xp - prodScal(self.s, self.y), 2^(self.kap + 1)).lift()
assert mod(prodScal(self.s, self.y), 2^(self.kap + 1)) == mod(xp, 2^(self.kap + 1)), "Equality not valid"
print(" PkRecrypt: generation of yis", cputime(subprocesses=True) - t2)
t2 = cputime(subprocesses=True)
self.x = PRIntegersDelta.encrypt(self, [0 for i in range(self.tau)])
print(" PkRecrypt: generation of xis", cputime(subprocesses=True) - t2)
t2 = cputime(subprocesses=True)
self.pkRecrypt = pkRecrypt
if not self.pkRecrypt:
self.pkRecrypt = self
self.se = PRIntegersDelta.encrypt(self.pkRecrypt, self.s)
print(" PkRecrypt: generation of seis", cputime(subprocesses=True) - t2)
print(" PkRecrypt: genkey", cputime(subprocesses=True) - t)
def encrypt_pk(self, m=None, verbose=True):
t = cputime(subprocesses=True)
if m == None:
m = ZZ.random_element(2)
rhop = self.eta - self.rho
f = [Ciphertext(ZZ.random_element(2^self.alpha), self) for i in range(self.tau)]
c = sum2(ci * fi for ci, fi in zip(self.x.ciphertexts(self), f)) + Ciphertext(ZZ.random_element(2^rhop), self) + Ciphertext(m, self)
if verbose:
print("Encrypt", cputime(subprocesses=True) - t)
c.degree = 1
return c
def expand(self, c, verbose=True):
m = 2^(n + 1) - 1
t = cputime(subprocesses=True)
ce = [(((directProd(c, yi, self.kap) >> (32 - (n + 2))) + 1) >> 1) & m for yi in self.y]
if verbose:
print("expand", cputime(subprocesses=True) - t)
return ce
def recrypt(self, c, verbose=False):
t = cputime(subprocesses=True)
B = self.Theta // theta
cebin = [array(toBinary(cei, n + 1)) for cei in c.expand()]
sec = (c.__class__(ci, self.pkRecrypt) for ci in self.se)
li = (ski.scalmult(cei) for ski, cei in zip(sec, cebin))
ly = [sum2(islice(li, B)) for i in range(theta)]
res = reduceSteps(sumBinary, ly, verbose)
v = res[-1] + res[-2]
v.val += c.val & 1
if verbose:
print("recrypt", cputime(subprocesses=True) - t)
return v
def testPkRecrypt():
toy = {'ty': "toy", 'lam': 42, 'rho': 26, 'eta': 988, 'gam': 147456, 'Theta': 150, 'pksize': 0.076519, 'seclevel': 42.0, 'alpha': 936, 'tau': 158}
small = {'ty': "small", 'lam': 52, 'rho': 41, 'eta': 1558, 'gam': 843033, 'Theta': 555, 'pksize': 0.437567, 'seclevel': 52.0, 'alpha': 1476, 'tau': 572}
medium = {'ty': "medium", 'lam': 62, 'rho': 56, 'eta': 2128, 'gam': 4251866, 'Theta': 2070, 'pksize': 2.207241, 'seclevel': 62.0, 'alpha': 2016, 'tau': 2110}
large = {'ty': "large", 'lam': 72, 'rho': 71, 'eta': 2698, 'gam': 19575950, 'Theta': 7965, 'pksize': 10.303797, 'seclevel': 72.0, 'alpha': 2556, 'tau': 7659}
for param in [toy, small, medium, large]:
ty, rho, eta, gam, Theta = [param[x] for x in ['ty', 'rho', 'eta', 'gam', 'Theta']]
print("type=", ty, "lam=", param['lam'], "rho=", rho, "eta=", eta, "gamma=", gam, "Theta=", Theta)
pk = PkRecrypt(rho, eta, gam, Theta, tau=param['tau'], alpha=param['alpha'])
c1 = Ciphertext.encrypt(pk)
c2 = Ciphertext.encrypt(pk)
print("c1=", c1)
print("c2=", c2)
print("c1+c2=", c1 + c2)
t = cputime(subprocesses=True)
print("c1*c2=", c1 * c2)
print("tmult:", cputime(subprocesses=True) - t)
c = pk.encrypt_pk()
c.decrypt(verbose=True)
print("c=", c)
newc = pk.recrypt(c, verbose=True)
print("newc=", newc)

47
directscal.c Normal file
View File

@ -0,0 +1,47 @@
/*
* directscale.c: faster implementation of ciphertext expand
*
* Copyright (c) 2012 Mehdi Tibouchi <mehdi.tibouchi@normalesup.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published
* by the Free Software Foundation.
*/
#include <gmp.h>
#define w (GMP_NUMB_BITS/2)
#define _BOTMASK ((1ul << w)-1)
#define _TOPMASK (~_BOTMASK)
#define BOT(x) ((x) & _BOTMASK)
#define TOP(x) ((x) >> w)
#define LIMB(z,i) (((i)<((z)->_mp_size))?((z)->_mp_d[i]):(0L))
#define BOTL(z,i) (BOT(LIMB(z,i)))
#define TOPL(z,i) (TOP(LIMB(z,i)))
#define HLIMB(z,j) ((j&1)?(TOPL(z,j>>1)):(BOTL(z,j>>1)))
unsigned getGMP_NUMB_BITS()
{
return GMP_NUMB_BITS;
}
unsigned long directScal(unsigned long kap, mpz_t cz, mpz_t yz)
{
unsigned long nW=(kap+1)/(2*w), val=0, i;
if(nW*w*2 != kap+1)
return 0;
for(i = 0; i < nW-1; i++) {
val += BOTL(cz,i) * LIMB(yz,nW-1-i);
val += (BOTL(cz,i) * TOPL(yz,nW-2-i)) >> w;
val += TOPL(cz,i) * ((BOTL(yz,nW-1-i) << w) + TOPL(yz,nW-2-i));
val += (TOPL(cz,i) * BOTL(yz,nW-2-i)) >> w;
}
val += BOTL(cz,nW-1) * LIMB(yz,0);
val += TOPL(cz,nW-1) * (BOTL(yz,0) << w);
return val;
}

55
scalprod.spyx Normal file
View File

@ -0,0 +1,55 @@
#
# scalprod.spyx: faster implementation of ciphertext expand
#
# Copyright (c) 2012 Jean-Sebastien Coron <jean-sebastien.coron@uni.lu>
# and Mehdi Tibouchi <mehdi.tibouchi@normalesup.org>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 as published
# by the Free Software Foundation.
from sage.libs.gmp.all cimport mpz_t, mpz_export
from sage.rings.integer cimport Integer
from sage.misc.misc import cputime
from sage.all import ZZ
from libc.stdlib cimport calloc, free
cdef extern from "directscal.c":
unsigned long directScal(unsigned long kap, mpz_t c, mpz_t y)
unsigned getGMP_NUMB_BITS()
def directProd(c, y, kap):
cdef Integer yz = <Integer>y
cdef Integer cz = <Integer>c
return ZZ(directScal(kap, cz.value, yz.value)) >> (getGMP_NUMB_BITS() - 32)
def partialProd(c, y, kap):
cdef int w = sizeof(int) * 4 # 16 bits
cdef int nw = (kap + 1) // w
assert nw * w == kap + 1
cdef Integer yz = <Integer>y
cdef size_t ny
cdef unsigned int* yw = <unsigned int*> calloc(nw, sizeof(int))
cdef int order = -1 # least significant word first
cdef int endian = 0
cdef size_t nails = sizeof(int) * 4
mpz_export(yw, &ny, order, sizeof(int), endian, nails, yz.value)
assert ny <= nw
cdef Integer cz = <Integer>c
cdef size_t nc
cdef unsigned int* cw = <unsigned int*> calloc(nw, sizeof(int))
mpz_export(cw, &nc, order, sizeof(int), endian, nails, cz.value)
assert nc <= nw
cdef unsigned int val = 0
for i in range(nw - 2):
val += cw[i] * ((yw[nw-1-i] << w) + yw[nw-2-i]) + ((cw[i] * yw[nw-3-i]) >> w)
val += cw[nw-2] * ((yw[1] << w) + yw[0]) + cw[nw-1] * (yw[0] << w)
free(cw)
free(yw)
return ZZ(val)

86
utils.sage Normal file
View File

@ -0,0 +1,86 @@
#
# utils.sage
#
# Copyright (c) 2012 Jean-Sebastien Coron <jean-sebastien.coron@uni.lu>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 as published
# by the Free Software Foundation.
from itertools import chain, islice, zip_longest
from sys import stdout
def RandomOdd(n):
return 2 * (2^(n-2) + ZZ.random_element(2^(n-2))) + 1
def sum2(li):
empty = True
for x in li:
if empty:
res = x
empty = False
else:
res = res + x
if empty:
return 0
else:
return res
class array(list):
def __mul__(self, x):
return array([x * ci for ci in self])
def __add__(self, xx):
return array([ai + xi for ai, xi in zip(self, xx)])
def reduceSteps(f, li, verbose=True):
i = 0
res = li[0]
if verbose:
print(i, end=' ')
for x in li[1:]:
stdout.flush()
i = i + 1
res = f(res, x)
if verbose:
print(i, end=' ')
if verbose:
print()
return res
def sumBinary(a, b):
"Computes the sum of the binary vectors a and b, modulo 2^n where n is the length of the vectors a and b"
c = [a[0] + b[0]]
carry = a[0] * b[0]
for i in range(1, len(a) - 1):
carry2 = (a[i] + b[i]) * carry + a[i] * b[i]
c.append(a[i] + b[i] + carry)
carry = carry2
c.append(a[-1] + b[-1] + carry)
return c
def QuotientNear(a, b):
"Gives the nearest integer to a/b"
return (2 * a + b) // (2 * b)
def modNear(a, b):
"Computes a mod b with a in ]-b/2,b/2]"
return a - b * QuotientNear(a, b)
def toBinary(x, l):
"Converts a positive integer x into binary with l digits"
return (x + 2^l).digits(2)[:-1]
def prodScal(x, y):
return sum2((xi * yi for xi, yi in zip(x, y)))
def randSparse(n, h):
v = [0 for i in range(n)]
while sum(v) < h:
i = int(ZZ.random_element(n))
if v[i] == 0:
v[i] = 1
return v