WHU W.F.
2020.12.30
本文的目的并非深入探究CKKS原理,而是力求帮助开发者在完全不懂原理的情况下,快速构建CKKS工程,并掌握基本的使用技巧。
阅读本文要求读者:
如无特殊说明,本文用m代指明文,c代指密文
看代码之前我们先回顾一些概念:
首先,CKKS是一个公钥加密体系,具有公钥加密体系的一切特点,例如公钥加密、私钥解密等。因此,我们的代码中需要以下组件:
密钥生成器 keygenerator
加密模块 encryptor
解密模块 decryptor
其次,CKKS是一个(level)全同态加密算法(level表示其运算深度仍然存在限制),可以实现数据的“可算不可见”,因此我们还需要引入:
密文计算模块 evaluator
最后,加密体系都是基于某一数学困难问题构造的,CKKS所基于的数学困难问题在一个“多项式环”上(没有数论知识也没有关系,只需要明白环上的元素与实数并不相同),因此我们需要引入:
编码器 encoder
来实现数字和环上元素的相互转换。
总结下来,整个构建过程为:
① 选择CKKS参数 parms
② 生成CKKS框架 context
③ 构建CKKS模块 keygenerator,encoder,encryptor,evaluator和decryptor
④ 使用encoder 将 数据n 编码为 明文m
⑤ 使用encryptor 将 明文m 加密为 密文c
⑥ 使用evaluator 对 密文c 运算为 密文c’
⑦ 使用decryptor 将 密文c’ 解密为 明文m’
⑧ 使用encoder 将 明文m’ 解码为 数据n’
同态加密算法最直观的应用是云计算,其基本流程为:
①发送方利用公钥pk 加密 明文m 为 密文c
②发送方把密文c发送到服务器
③服务器执行密文运算,生成结果密文 c’
④服务器将结果密文c’发送给接收方
⑤接收方利用私钥sk 解密密文c’为明文结果m’
当发送方与接收方相同时,则该客户利用全同态加密算法完成了一次安全计算,即既利用了云计算的算力,又保障了数据的安全性,这对云计算的安全应用有重要意义。
下面的例子实现了上述情景:
#include "examples.h"
/*
This file can be found in SEAL/native/example/
该文件可以在SEAL/native/example目录下找到
*/
#include
using namespace std;
using namespace seal;
#define N 3
//本例的目的是计算x,y,z的乘积
int main(){
//客户端的视角: 要进行计算的数据
vector x, y, z;
x = { 1.0, 2.0, 3.0 };
y = { 2.0, 3.0, 4.0 };
z = { 3.0, 4.0, 5.0 };
//构建参数容器 parms
EncryptionParameters parms(scheme_type::CKKS);
/*CKKS有三个重要参数:
1.poly_module_degree(多项式模数)
2.coeff_modulus(参数模数)
3.scale(规模)
下一小节会详细解释*/
size_t poly_modulus_degree = 8192;
parms.set_poly_modulus_degree(poly_modulus_degree);
parms.set_coeff_modulus(CoeffModulus::Create(poly_modulus_degree, { 60, 40, 40, 60 }));
//选用2^40进行编码
double scale = pow(2.0, 40);
//用参数生成CKKS框架context
auto context = SEALContext::Create(parms);
//构建各模块
//首先构建keygenerator,生成公钥、私钥和重线性化密钥
KeyGenerator keygen(context);
auto public_key = keygen.public_key();
auto secret_key = keygen.secret_key();
auto relin_keys = keygen.relin_keys();
//构建编码器,加密模块、运算器和解密模块
//注意加密需要公钥pk;解密需要私钥sk;编码器需要scale
Encryptor encryptor(context, public_key);
Evaluator evaluator(context);
Decryptor decryptor(context, secret_key);
CKKSEncoder encoder(context);
//对向量x、y、z进行编码
Plaintext xp, yp, zp;
encoder.encode(x, scale, xp);
encoder.encode(y, scale, yp);
encoder.encode(z, scale, zp);
//对明文xp、yp、zp进行加密
Ciphertext xc, yc, zc;
encryptor.encrypt(xp, xc);
encryptor.encrypt(yp, yc);
encryptor.encrypt(zp, zc);
/*对密文进行计算,要说明的原则是:
1.加法可以连续运算,但乘法不能连续运算
2.密文乘法后要进行relinearize操作
3.执行乘法后要进行rescaling操作
4.进行运算的密文必需执行过相同次数的rescaling(位于相同level)
*/
//基于上述原则进行运算
//至此,客户端将pk、CKKS参数发送给服务器,服务器开始运算
//服务器的视角:先设中间变量
Ciphertext temp;
Ciphertext result_c;
//计算x*y,密文相乘,要进行relinearize和rescaling操作
evaluator.multiply(xc,yc,temp);
evaluator.relinearize_inplace(temp, relin_keys);
evaluator.rescale_to_next_inplace(temp);
//在计算x*y * z之前,z没有进行过rescaling操作,所以需要对z进行一次乘法和rescaling操作,目的是 make x*y and z at the same level
Plaintext wt;
encoder.encode(1.0, scale, wt);
//我们可以查看框架中不同数据的层级:
cout << " + Modulus chain index for zc: "
<< context->get_context_data(zc.parms_id())->chain_index() << endl;
cout << " + Modulus chain index for temp(x*y): "
<< context->get_context_data(temp.parms_id())->chain_index() << endl;
cout << " + Modulus chain index for wt: "
<< context->get_context_data(wt.parms_id())->chain_index() << endl;
//执行乘法和rescaling操作:
evaluator.multiply_plain_inplace(zc, wt);
evaluator.rescale_to_next_inplace(zc);
//再次查看zc的层级,可以发现zc与temp层级变得相同
cout << " + Modulus chain index for zc after zc*wt and rescaling: "
<< context->get_context_data(zc.parms_id())->chain_index() << endl;
//最后执行temp(x*y)* zc(z*1.0)
evaluator.multiply_inplace(temp, zc);
evaluator.relinearize_inplace(temp,relin_keys);
evaluator.rescale_to_next(temp, result_c);
//计算完毕,服务器把结果发回客户端
//客户端进行解密和解码
Plaintext result_p;
decryptor.decrypt(result_c, result_p);
//注意要解码到一个向量上
vector result;
encoder.decode(result_p, result);
//得到结果
//正确的话将输出:{6.000,24.000,60.000,...,0.000,0.000,0.000}
cout << "结果是:" << endl;
print_vector(result,3,3);
return 0;
}
读完样例代码,对CKKS同态加密计算流程应该有了基本的了解。
本小节对三个参数进行简单的解释
该参数必须是2的幂,如1024, 2048, 4096, 8192, 16384, 32768,当然再大点也没问题。
更大的 poly_modulus_degree 会增加密文的尺寸,这会让计算变慢, 但也能让你执行更复杂的计算,关于这点稍后会有更直观的理解。
每个密文和明文(即Plaintext和Cipertext)本质上是一个长为poly_modules/2的向量,如果要利用例子中的“包加密技术”将一个向量直接加密到密文上,就需要保证向量的长度不超过poly_modules/2,在本例中为4096。否则就要先进行拆分再加密(即用for循环遍历数据,逐一加密),或选取更大的poly_modules参数。
这是一组重要参数,因为rescaling操作依赖于coeff_modules。
简单来说,coeff_modules的个数决定了你能进行rescaling的次数,进而决定了你能执行的乘法操作的次数。
coeff_modules的最大位数与poly_modules有直接关系,列表如下:
poly_modulus_degree | max coeff_modulus bit-length |
---|---|
1024 | 27 |
2048 | 54 |
4096 | 109 |
8192 | 218 |
16384 | 438 |
32768 | 881 |
本文例子中的{60,40,40,60}有以下含义:
① coeff_modules总位长200(60+40+40+60)位
② 最多进行两次(两层)乘法操作
该系列数字的选择不是随意的,有以下要求:
① 总位长不能超过上表限制
② 最后一个参数为特殊模数,其值应该与中间模数的最大值相等
③ 中间模数与scale尽量相近
本例中,每次Rescaling的示意图如下:
\ special prime:60
coeff_modulus: { 60, 40, 40, 60 } ±–+ Level 3 (key level)
----------------------rescaling↓--------------------------------------
coeff_modulus: { 60, 40, 40 } ±–+ Level 2 (data level)
-----------------rescaling↓---------------------------------------------
coeff_modulus: { 60, 40 } ±–+ Level 1
------------rescaling↓--------------------------------------------
coeff_modulus: { 60 } ±–+ Level 0 (lowest level)
Encoder利用该参数对浮点数进行缩放,每次相乘后密文的scale都会翻倍,因此需要执行rescaling操作约减一部分,约模的大素数位长由coeff_modules中的参数决定。
Scale不应太小,虽然大的scale会导致运算时间增加
但能确保噪声在约模的过程中被正确地舍去,同时不影响正确解密。
因此,两组推荐的参数为:
Poly_module_degree = 8196; coeff_modulus={60,40,40,60};scale = 2^40
Poly_module_degree = 8196; coeff_modulus={50,30,30,30.50};scale = 2^30
如示例代码中所述,每次进行运算前,要保证参与运算的数据位于同一“level”上。
加法不需要进行rescaling操作,因此不会改变数据的level
数据的level只能降低无法升高,所以要小心设计计算的先后顺序
可以通过输出p.scale()、p.parms_id()以及
context->get_context_data(p.parms_id())->chain_index()
来确认即将进行操作的数据满足:
1)用同一组参数进行加密
2)位于(chain)上的同一level
3)scale相同
的计算条件。
要想把不同level的数据拉到同一level,可以利用乘法单位元1把层数较高的操作数拉到较低的level(如本例),也可以通过内置函数进行直接转换,但笔者推荐前者,有利于提高代码的可读性,便于维护。
目前,SEAL提供了reverse、square等有限的计算操作,大部分复杂运算需要自己编写代码实现,在实现过程中要根据数据量把握好精度和性能的取舍。