[FHE]使用Microsoft SEAL库实现的BFV方案写一个demo

Description

用SEAL中实现的BFV方案完成一个面向整数的同态加密方案。

初始化

设置公开参数

所有的参数包含在一个EncryptionParameter对象当中,初始化一个对象打包了三个参数poly_modulus_degreecoeff_modulusplain modulus
poly_modulus_degree:即多项式环\(\mathcal{R}=\mathbf{Z}[X]/X^N+1\)中的\(N\)
coeff_modulus:即\(q\),和密钥与加密过程中选取的随机数有关;
plain modulus:即明文模数,决定了明文空间\(\mathcal{R}_{t}=\mathbf{Z}_{t}[X]/X^N+1\)
参数对象的初始化可以用以下初始化语句完成:

EncryptionParameters parms(scheme_type::bfv);

表明初始化一个BFV方案的参数对象。

poly_modulus_degree为素数幂的形式,一般而言设置成2的幂次,方便分圆多项式的计算。SEAL中可以选择的值为1024、2048 、4096、8192或者更大,选择很小的值会报错。设置分圆多项式次数可以用以下方法:

size_t poly_modulus_degree = 8192;
parms.set_poly_modulus_degree(poly_modulus_degree);

coefficient modulus需要一个很大的整数,用来使噪声预算更加宽裕(因为噪声控制的要求是\(m+te<q\))。coefficient modulus的长度(以比特计算)上限由poly_modulus_degree决定,由于越大越好,其实这个参数不用费脑子去取,只需要用小帮手函数:

CoeffModulus::BFVDefault(poly_modulus_degree);

就可以了。

plain_modulus是BFV特有的(相较于CKKS)。是一个越小越好的正整数(但是会影响明文空间),可以取素数或者2的幂次。可以直接设置:

parms.set_plain_modulus(1024);

也可以使用小帮手生成一个合适的素数:

PlainModulus::Batching(poly_modulus_degree, bit_size);

bit_size是生成的素数的长度(以比特计)。

初始化加密方案对象

接受一个参数对象,可以初始化一个SEALContext对象,期间会检查参数,如果不符合要求就会报错。

SEALContext context(parms);

接着需要实例化一个KeyGenerator类来生成公私钥对。

KeyGenerator keygen(context);
SecretKey secret_key = keygen.secret_key();
PublicKey public_key;
keygen.create_public_key(public_key);

为了实现旋转操作还需要旋转密钥:

GaloisKeys galois_keys;
keygen.create_galois_keys(galois_keys);

为了实现重线性化还需要重线性化的辅助密钥(计算密钥):

RelinKeys relin_keys;
keygen.create_relin_keys(relin_keys);

接着用上面实例化好了的各种对象生成其他各种对象:加密器、解密器、求值器,只能说SEAL的逻辑还是很清楚明白的。

// 加密器
Encryptor encryptor(context, public_key);
// 解密器
Decryptor decryptor(context, secret_key);
// 求值器
Evaluator evaluator(context);

算子

根据src/seal/evaluate.h头文件可以查看求值器类的所有函数,当文档看(有选择)。

算子 Description
negate_inplace 取负
add_inplace 加上一个密文
sub_inplace 减去一个密文
multiply_inplace 乘上一个密文
square_inplace 平方
relinearize_inplace 重线性化
exponentiate_inplace 幂次
add_plain_inplace 加上一个明文
sub_plain_inplace 减去一个明文
multiply_plain_inplace 乘上一个明文
rotate_rows_inplace 矩阵行旋转
rotate_columns_inplace 矩阵列旋转
rotate_vector_inplace 向量旋转

实在太多了就只捡了点比较重要的。inplace的意思是直接替换,不加inplace就需要加一个输入存储结果。

计算过程

单个数据编码

BFV对于单个数据的编码的方式就是拆成进制表示,将对应进制位的数作为对应次数单项式的系数。如果是浮点数就将小数部分按照负幂次进行拆分,然后模上多项式\(X^N+1\)
这里的意思就是将数字6表示为16进制编码后,将编码的值(0x)0006转为string,当作多项式的系数表示输入。

uint64_t x = 6;
Plaintext x_plain(uint64_to_hex_string(x));

SIMD编码

BFV的SIMD编码和CKKS差别比较大,用的是多项式环上的中国剩余定理,将向量编码为多项式。
向量以vector进行存储,编码的多项式也以系数方式以vector进行存储。编码后的形式一律为\(2 \times (N/2)\)的矩阵,其中\(N=\)poly_modulus_degree,并以一个长度为\(N\)vector的形式进行存储。

// 先实例化编码器
BatchEncoder batch_encoder(context);
vector<uint64_t> v(slot_count, 0ULL);
batch_encoder.encode(v, plain_matrix);

解密

先初始化一个明文对象,储存结果。用编码器就可以直接解密,编码器的实例化过程使用了私钥:

decryptor.decrypt(encrypted_result, decrypted_result);

解码

单个数据不需要解码,解密后得到的就是16进制表示的string
BatchEncoder的解码则直接使用decode方法就好,将vector存储的系数表示多项式解码会vector存储的向量。

batch_encoder.decode(plain_result, decode_result);

加法与乘法

evaluator.add_plain(encrypted, plain, res);
evaluator.add(encrypted1, encrypted2, res);
evaluator.multiply_plain(encrypted, plain, res);
evaluator.multiply(encrypted1, encrypted2, res);

乘法之后需要重线性化以解决密文维数过大的问题。
运算需要注意控制噪声,模切换在BFV中不是必须的。

旋转

旋转需要rotation key,即生成的Galois key

evaluator.rotate_rows_inplace(encrypted_matrix, 3, galois_keys);
evaluator.rotate_columns_inplace(encrypted_matrix, galois_keys);
evaluator.rotate_vector_inplace(encrypted_vector, galois_keys);

每一个2的幂次的旋转步长需要一个对应的Galois Key,非2的幂次的旋转步长表示成多个2的幂次相加的形式。这是由slots的数目是2的幂次决定的。

乘法、密文维数与重线性化

密文-密文乘法、平方幂次运算都会造成密文维数膨胀。
直接对密文对象使用size()方法可以查看密文的维数:

x.size()

这样可以重线性化(3维 \(\rightarrow\) 2维):

evaluator.relinearize_inplace(x, relin_keys);

噪声预算

使用如下方法可以查看密文还可以增加的噪声(以比特计):

decryptor.invariant_noise_budget(x)
posted @ 2022-03-27 00:21  Akiho  阅读(406)  评论(0)    收藏  举报