2021SC@SDUSC
目录
介绍
同态加密是一个很有前途的领域,它允许对加密数据进行计算。
在本系列文章中,我们将会深入探讨 Cheon-Kim-Kim-Song (CKKS) 方案,该方案首次在论文Homomorphic Encryption for Arithmetic of Approximate Numbers 中讨论。CKKS允许我们对复数值(也是实数值)的向量执行计算。这个想法使我们将在Python中从头开始实现CKKS,然后通过使用这些加密原语,我们可以探索如何执行复杂的操作,例如线性回归、神经网络等。
上图提供了CKKS的高级视图。我们可以看到,消息m是我们想要对其执行某些计算(加密)的值向量,它首先被编码为明文多项式p(X),然后再使用公钥进行加密。
CKKS使用多项式,因为与向量的标准计算相比,它在安全性和效率之间提供了良好的权衡。
一旦消息m被加密c,CKKS提供了几个可以对其执行的操作,例如加法、乘法和旋转等等。
如果我们用f表示一个函数,它是同态运算的组合,那么用密钥解密c' = f(c)将产生p' = f(p)。因此,一旦我们对其进行解码,我们将得到m' = f(m)。
实现同态加密方案的中心思想是在编码器、解码器、加密器和解密器上具有同态特性。这样,对密文的操作将被正确解密和解码,并提供输出,就像直接对明文进行操作一样。
所以在本文中,我们将看到如何实现编码器和解码器,在后面的文章中,我们将继续实现加密器和解密器以及具有的同态加密方案。
CKKS编码
CKKS在其明文p(X)和密文c之间利用了整数多项式环的丰富结构。尽管如此,数据将以向量的形式出现,而不是以多项式的形式出现。
因此,我们有必要对输入进行编码,从z∈C^N/2到多项式m(X)∈Z[X] / (X^N+1)。
我们将用N表示多项式模的次数,N是2的幂。我们用ΦM(X)=X^N+1表示第m个分圆多项式(注意m=2N)。明文空间将是多项式环R=Z[X]/(X^N+1)。让我们用ξM表示,单位的第M根:ξM=e^2iπ/M。
为了理解我们如何将一个向量编码成一个多项式,以及如何在这个多项式上执行的计算将反映在底层向量上,我们将首先用一个简单的例子进行实验,我们简单地将一个向量z∈C^N编码成一个多项式m(X)∈C[X] / (X^N+1)。然后我们将覆盖CKKS的实际编码,它取一个向量z∈C^N/2,将其编码为一个多项式m(X)∈z [X] / (X^N+1)。
普通编码
在这里,我们将讨论将z∈C^N编码成一个多项式m(X)∈C[X] / (X^N+1)的简单情况。
为此,我们将使用正则嵌入σ:C[X]/(X^N+1)→C^N,它对我们的向量进行解码和编码。
这个想法很简单:要将一个多项式m(X)解码成一个向量z,我们在特定的值上计算这个多项式,这将是切圆多项式ΦM(X)=X^N+1的根。这些N的根为:ξ,ξ^3…,ξ ^(2n−1)。
所以解码一个多项式m (X)σ(m) = (m(ξ),m(ξ^3),…,m(ξ^(2n−1)))∈C^Nσ
棘手的部分是一个向量的编码
进一步研究这个问题,我们将得到以下系统:
Aα=z, A是(ξ^(2i−1)),i=1,…N, α是多项式系数的向量,z是我们要编码的向量。
因此,我们得到了:α=A^(−1)z,并且
例子
现在我们看一个例子,以更好地地接我们上述讨论的内容。
我们假设M = 8,则N = M/2 = 4,ΦM(X) = X^4+1, ω = e^(2iπ/8) = e^iπ/4
其中M表示有M个切圆多项式,N表示多项式摸的次数,ΦM(X)表示第m个切圆多项式, ω表示切圆多项式的根(因为N = 4,所以i = 1,3,5,7)。
我们的目标是编码以下向量:[1,2,3,4]和[−1,−2,−3,−4],解码他们,加和乘他们的多项式,解码它。
正如我们所看到的,为了解码一个多项式,我们只需要在一个单位的m次方根的幂上计算它。这里我们选择ξM = ω = e^(iπ/4)。
一旦我们有ξ和M,我们就可以定义σ和它的逆σ^(−1),分别是译码和编码。
相关代码执行(重要)
我们可以在python中实现Vanilla 编码器和解码器。
import numpy as np
# 我们先设置一下参数
M = 8
N = M //2
# 我们设置一下xi, 之后的代码我们可能会用到
xi = np.exp(2 * np.pi * 1j / M)
print(xi)
运行结果:
编码器和解码器的实现过程:
from numpy.polynomial import Polynomial
class CKKSEncoder:
""" 基本CKKS编码器将编码变为多项式。"""
def __init__(self, M: int):
"""初始化编码器M的一个2的幂。"""
"""xi,是单位的第m次根,将作为我们计算的基础。"""
self.xi = np.exp(2 *np.pi * 1j / M)
self.M = M
@staticmethod
def vandermonde(xi: np.complex128, M: int) -> np.array:
"""从单位的m次根计算万德蒙矩阵。"""
N = M//2
matrix = []
# 我们将生成矩阵的每一行
for i in range(N):
# 每一行我们选择一个不同的根
root = xi ** (2 * i + 1)
row = []
# 然后我们储存它的权值
for j in range(N):
row.append(root ** j)
matrix.append(row)
return matrix
def sigma_inverse(self, b: np.array) -> Polynomial:
"""用m次单位根将向量b编码成多项式。"""
# 首先我们先创建一个万德蒙矩阵
A = CKKSEncoder.vandermonde(self.xi, M)
# 然后我们解决这个方程
coeffs = np.linalg.solve(A, b)
# 最后我们输出这个多项式
p = Polynomial(coeffs)
return p
def sigma(self, p: Polynomial) -> np.array:
"""通过将多项式应用于单位的m次根来解码多项式"""
outputs = []
N = self.M // 2
# 我们只需要把多项式应用到根上
for i in range(N):
root = self.xi ** (2 * i + 1)
output = p(root)
outputs.append(output)
return np.array(outputs)
具体分析【假设输入的向量的(1,2,3,4)】
首先是生成万德蒙矩阵:
def vandermonde(xi: np.complex128, M: int) -> np.array:
"""从单位的m次根计算万德蒙矩阵。"""
N = M//2
matrix = []
# 我们将生成矩阵的每一行
for i in range(N):
# 每一行我们选择一个不同的根
root = xi ** (2 * i + 1)
row = []
# 然后我们储存它的权值
for j in range(N):
row.append(root ** j)
matrix.append(row)
return matrix
根据矩阵来得到所需要的多项式:
def sigma_inverse(self, b: np.array) -> Polynomial:
"""用m次单位根将向量b编码成多项式。"""
# 首先我们先创建一个万德蒙矩阵
A = CKKSEncoder.vandermonde(self.xi, M)
# 然后我们解决这个方程
coeffs = np.linalg.solve(A, b)
# 最后我们输出这个多项式
p = Polynomial(coeffs)
return p
根据多项式来解码得出向量:
def sigma(self, p: Polynomial) -> np.array:
"""通过将多项式应用于单位的m次根来解码多项式"""
outputs = []
N = self.M // 2
# 我们只需要把多项式应用到根上
for i in range(N):
root = self.xi ** (2 * i + 1)
output = p(root)
outputs.append(output)
return np.array(outputs)
测试过程
让我们首先编码一个向量,看看它是如何使用实数值编码的。
encoder = CKKSEncoder(M)
b = np.array([1, 2, 3, 4])
print(b)
现在我们对向量进行编码:
p = encoder.sigma_inverse(b)
print(p)
我们再从编码的多项式中进行解码,看看的能不能得到我们最初的向量:
b_reconstructed = encoder.sigma(p)
print(b_reconstructed)
我们可以看到,解码后和初始向量的值是非常相似的:
print(np.linalg.norm(b_reconstructed - b))
如前所述, σ不是随机选择来编码和解码的,但它有很多不错的特性。他们之中,σ 是同构,因此多项式的加法和乘法将导致编码向量上的系数明智的加法和乘法。
同态操作(编码和解码)
m1 = np.array([1, 2, 3, 4])
m2 = np.array([1, -2, 3, -4])
p1 = encoder.sigma_inverse(m1)
p2 = encoder.sigma_inverse(m2)
加法操作
我们可以看到加法非常简单。
p_add = p1 + p2
print(p_add)
正如我们预想的那样,我们看到p1 + p2 正确的解码为 [2, 0, 6, 0]
print(encoder.sigma(p_add))
乘法操作
因为在进行乘法时,我们可以会有度数高于N,我们需要使用 X^N + 1
要执行乘法操作,我们首先需要定义我们将使用的多项式模数。(这里我也不是很懂模数是如何定义的。)
poly_modulo = Polynomial([1, 0, 0, 0, 1])
print(poly_modulo)
现在我们可以执行乘法了。
p_mult = p1 * p2 % poly_modulo
最后,如果我们对其进行解码,我们可以看到我们得到了预期的结果 [1, -4, 9, -16]。
print(encoder.sigma(p_mult))
因此我们可以看到我们的简单编码器和解码器按预期工作,因为它具有同态属性并且是向量和多项式之间的一对一映射。