Gihub链接:https://github.com/microsoft/SEAL
核心概念
大多数加密方案由三种功能组成:密钥生成、加密和解密。对称密钥加密方案使用相同的密钥进行加密和解密;公钥加密方案分别使用公钥加密和私钥解密。因此,公钥加密方案允许任何知道公钥的人对数据进行加密,但是只有知道私钥的人才能解密和读取数据。对称密钥加密可以有效地加密大量数据,并支持安全的外包云存储。公钥加密是当今实现安全在线通信的一个基本概念,但其效率通常比对称密钥加密低得多。
虽然传统的对称和公钥加密可以用于安全的存储和通信,但是任何外包的计算都必须在进行计算之前删除这些加密层。因此,提供外包计算能力的云服务必须能够访问这些密钥,并实现访问策略以防止未经授权的员工访问这些密钥。、
同态加密
同态加密是指允许云直接在加密数据上计算,而不需要先对数据进行解密的加密方案。这种加密计算的结果仍然是加密的,并且只能使用私钥(由数据所有者)解密。在过去的十年中,人们发明了多种具有不同功能和优缺点的同态加密方案;其中大部分是公钥加密方案,尽管可能并不总是需要公钥功能。
同态加密不是一种通用技术:只能对加密数据进行一些计算。它还带来了巨大的性能开销,因此在未加密的数据上执行已经非常昂贵的计算可能在加密数据上不可行。此外,使用同态加密加密的数据比未加密的数据大很多倍,因此使用这种技术加密整个大型数据库可能没有意义。相反,有意义的用例是在严格的隐私要求完全禁止未加密的云计算的场景中,但是计算本身是相当轻量级的。
通常,同态加密方案有一个由数据所有者持有的私钥。对于多个不同的私有数据所有者希望参与协作计算的场景,同态加密可能不是一个合理的解决方案。
同态加密不能用于使数据科学家绕过GDPR。例如,云服务无法使用同态加密从加密的客户数据中获取信息。相反,加密计算的结果仍然是加密的,只能由数据所有者(例如云服务客户)解密。
Microsoft SEAL
Microsoft SEAL是一个同态加密库,它允许在加密的整数或实数上执行加法和乘法。其他操作,如加密比较、排序或正则表达式,在大多数情况下都无法使用此技术对加密数据进行评估。因此,只有程序中特定的隐私关键型云计算部分才能使用Microsoft SEAL实现。
将未加密的计算转换为加密数据上的计算并不总是容易或直接的,例如,不可能在加密数据上进行分支。Microsoft SEAL本身有一个陡峭的学习曲线,它要求用户理解许多同态加密的特定概念,尽管最终的API并不太复杂。即使用户能够使用Microsoft SEAL编写和运行特定的计算,高效实现和低效实现之间的差异也可能是数量级的,而且新用户很难知道如何改进他们的计算性能。
Microsoft SEAL提供了两种不同的同态加密方案,它们具有非常不同的属性。BFV方案允许在加密的整数上执行模运算。CKKS方案允许对加密的实数或复数进行加法和乘法,但只能得到近似的结果。在诸如汇总加密的真实数字、评估加密数据上的机器学习模型或计算加密位置的距离等应用程序中,ckk将是最佳选择。对于需要精确值的应用程序,BFV方案是惟一的选择。
Installing Microsoft SEAL
Windows
Linux and macOS
(1) Global install
(2) Local install
Installing Microsoft SEAL for .NET
(1)Windows
(2)Linux and macOS
程序结构:
SEAL/native/examples/examples.h
六个例子:
void example_bfv_basics();
void example_encoders();
void example_levels();
void example_ckks_basics();
void example_rotation();
void example_performance_test();
Helper函数:
Helper function: Prints the name of the example in a fancy banner.
inline void print_example_banner(std::string title)
Helper function: Prints the parameters in a SEALContext.
inline void print_parameters(std::shared_ptr<:sealcontext> context)\
Helper function: Prints the `parms_id' to std::ostream.
inline std::ostream &operator <
Helper function: Prints a vector of floating-point values.
template
inline void print_vector(std::vector vec, std::size_t print_size = 4, int prec = 3)
Helper function: Prints a matrix of values.
template
inline void print_matrix(std::vector matrix, std::size_t row_size)
Helper function: Print line number.
inline void print_line(int line_number)
SEAL/native/examples/examples.cpp
打印使用说明:
int main()
cout << "+---------------------------------------------------------+" << endl;
cout << "| The following examples should be executed while reading |" << endl;
cout << "| comments in associated files in native/examples/. |" << endl;
cout << "+---------------------------------------------------------+" << endl;
cout << "| Examples | Source Files |" << endl;
cout << "+----------------------------+----------------------------+" << endl;
cout << "| 1. BFV Basics | 1_bfv_basics.cpp |" << endl;
cout << "| 2. Encoders | 2_encoders.cpp |" << endl;
cout << "| 3. Levels | 3_levels.cpp |" << endl;
cout << "| 4. CKKS Basics | 4_ckks_basics.cpp |" << endl;
cout << "| 5. Rotation | 5_rotation.cpp |" << endl;
cout << "| 6. Performance Test | 6_performance.cpp |" << endl;
cout << "+----------------------------+----------------------------+" << endl;
选择例子:
switch (selection)
{
case 1:
example_bfv_basics();
break;
case 2:
example_encoders();
break;
case 3:
example_levels();
break;
case 4:
example_ckks_basics();
break;
case 5:
example_rotation();
break;
case 6:
example_performance_test();
break;
例1 example_bfv_basic:
#include "examples.h"
using namespace std;
using namespace seal;
void example_bfv_basics()
......
在本例中,我们演示了使用BFV加密方案对加密的整数执行简单计算(多项式求值)。
第一个任务是设置EncryptionParameters类的实例。
了解不同参数的行为方式、它们如何影响加密方案、性能和安全级别非常重要。需要设置三个加密参数:
poly_modulus_degree(多项式模度);
系数模量([密文]系数模量);
明文模量(明文模量;只适用于BFV方案)。
BFV方案不能对加密数据执行任意计算。相反,每个密文都有一个特定的量,称为“不变噪声预算”——简称为“噪声预算”——以比特来衡量。新加密密文(初始噪声预算)中的噪声预算由加密参数决定。同态操作以同样由加密参数决定的速率消耗噪声预算。在BFV中,允许对加密数据进行的两种基本操作是加法和乘法,一般认为加法与乘法相比,在噪声预算消耗方面几乎是免费的。由于顺序乘法的噪声预算消耗是复合的,因此选择合适的加密参数时最重要的因素是用户希望对加密数据进行评估的算术电路的乘性深度。一旦密文的噪声预算达到零,它就会被破坏得无法解密。因此,必须选择足够大的参数来支持所需的计算;否则,即使使用密钥,结果也不可能有意义。
EncryptionParameter parms(scheme_type::BFV);
我们设置的第一个参数是‘多项式模’的次数。这一定是2的正次幂,表示2的幂次多项式的次数;没有必要去理解这是什么意思。
较大的poly_modulus_degree使密文的大小更大,所有的操作都更慢,但允许更复杂的加密计算。推荐值为1024、2048、4096、8192、16384、32768,但是也可能超出这个范围。
在这个例子中,我们使用一个相对较小的多项式模。任何小于此值的操作都只允许非常有限的加密计算。
size_t poly_modulus_degree = 4096;
parms.set_poly_modulus_degree(poly_modulus_degree);
接下来,我们设置[密文]“系数模量”(coeff_modules)。该参数是一个大整数,它是不同素数的乘积,每个素数的大小最大为60位。它被表示为这些素数的一个向量,每个素数由smallmodules类的一个实例表示。coeff_modules的位长是指它的质数因子的位长之和。
更大的coeff_模数意味着更大的噪声预算,因此更加密的计算能力。然而,coeff_modulus_degree确定了coeff_modul_modules总比特长度的上限,具体如下:
±---------------------------------------------------+
| poly_modulus_degree | max coeff_modulus bit-length |
±--------------------±-----------------------------+
| 1024 | 27 |
| 2048 | 54 |
| 4096 | 109 |
| 8192 | 218 |
| 16384 | 438 |
| 32768 | 881 |
±--------------------±-----------------------------+
1743/5000
这些数字也可以在函数SEAL_HE_STD_PARMS_128_TC中的native/src/seal/util/hestdpar .h中找到,也可以从函数coeff::MaxBitCount(poly_modulus_degree)中获得。
例如,如果poly_modulus_degree为4096,则coeff_modul_modules可以由3个36位素数(108位)组成。
Microsoft SEAL附带用于选择coeff_modules的辅助函数。
对于新用户来说,最简单的方法就是使用
CoeffModulus::BFVDefault(poly_modulus_degree),
它返回std::vector,对于给定的poly_modulus_degree,这是一个很好的选择。
parms.set_coeff_modulus (CoeffModulus:: BFVDefault (poly_modulus_degree));
明文模数可以是任何正整数,即使这里我们取它为2的幂。事实上,在很多情况下,我们可能希望它是质数;我们将在后面的例子中看到这一点。明文模量决定了明文数据类型的大小和乘法中噪声预算的消耗。因此,为了获得最佳性能,必须尽量保持明文数据类型尽可能小。新加密密文的噪声预算是
~ log2 (coeff_modulus / plain_modulus)(位)
同态乘法的噪声预算消耗为log2(plain - modulo) + (other terms)。
明文模量是特定于BFV方案的,在使用CKKS方案时无法设置。
parms.set_plain_modulus (256);
既然所有的参数都设置好了,我们就可以开始构建一个SEALContext对象了。这是一个繁重的类,它检查我们刚刚设置的参数的有效性和属性。
auto context = SEALContext::Create(parms);
Print the parameters that we have chosen.
print_line(__LINE__);
cout << "Set encryption parameters and print" << endl;
print_parameters(context);
cout << endl;
cout << "~~~~~~ A naive way to calculate 2(x^2+1)(x+1)^2. ~~~~~~" << endl;
Microsoft SEAL中的加密方案是公钥加密方案。对于不熟悉这个术语的用户,公钥加密方案有一个用于加密数据的单独公钥和一个用于解密数据的单独密钥。通过这种方式,多个参与方可以使用相同的共享公钥对数据进行加密,但是只有正确的数据接收方可以使用密钥对数据进行解密。
现在,我们已经准备好生成密钥和公钥。为此,我们需要KeyGenerator类的一个实例。构造密钥生成器会自动生成公钥和密钥,可以立即将其读入本地变量。
KeyGenerator keygen(context);
PublicKey public_key = keygen.public_key();
SecretKey secret_key = keygen.secret_key();
为了能够加密,我们需要构造Encryptor的一个实例。注意,正如预期的那样,Encryptor只需要公钥。
Encryptor encryptor(context, public_key);
对密文的计算是用求值程序类执行的。在实际的用例中,评估器不会由持有密钥的同一方构建。
Evaluator evaluator(context);
当然,我们希望对结果进行解密,以验证一切都工作正常,因此还需要构造Decryptor实例。注意,解密器需要密钥。
Decryptor decryptor(context, secret_key);
作为一个例子,我们求4次多项式的值
2x^4 + 4x^3 + 4x^2 + 4x + 2
通过加密的x = 6。多项式的系数可以看作明文输入,如下所示。计算是以明文模256为模数进行的。
虽然这个例子简单易懂,但是没有什么实用价值。在后面的示例中,我们将演示如何更有效地计算加密的整数和实数或复数。
BFV方案中的明文是次数小于多项式模的次数的多项式,系数整数模的明文模。对于有环理论背景的读者,明文空间为多项式商环Z_T[X]/(X^N+1),其中N为多项式模数,T为明文模数。
首先,我们创建一个包含常数6的明文。对于明文元素,我们使用一个构造函数,该构造函数将所需的多项式作为一个字符串,其系数表示为十六进制数。
print_line(__LINE__);
int x = 6;
Plaintext x_plain(to_string(x));
cout << "Express x = " + to_string(x) +
" as a plaintext polynomial 0x" + x_plain.to_string() + "." << endl;
/*
然后对明文进行加密,生成密文。
print_line(__LINE__);
Ciphertext x_encrypted;
cout << "Encrypt x_plain to x_encrypted." << endl;
encryptor.encrypt(x_plain, x_encrypted);
在Microsoft Seal中,一个有效的密文由两个或多个多项式组成,其中它们的系数是整数 mod(在coeff_modulus中素数的乘积)。密文中多项式的个数称为其“size”,由Ciphertext::size()给出。新加密的密文的大小总是2。
cout << " + size of freshly encrypted x: " << x_encrypted.size() << endl;
/*
There is plenty of noise budget left in this freshly encrypted ciphertext.
*/
cout << " + noise budget in freshly encrypted x: "
<< decryptor.invariant_noise_budget(x_encrypted) << " bits" << endl;
/*
We decrypt the ciphertext and print the resulting plaintext in order to
demonstrate correctness of the encryption.
*/
Plaintext x_decrypted;
cout << " + decryption of x_encrypted: ";
decryptor.decrypt(x_encrypted, x_decrypted);
cout << "0x" << x_decrypted.to_string() << " ...... Correct." << endl;
当使用Microsoft SEAL时,以最小化最长的顺序乘法链的方式进行计算通常是有利的。换句话说,加密计算最好以最小化计算乘法深度的方式进行评估,因为总噪声预算消耗与乘法深度成正比。例如,在我们的例子计算中,因式分解多项式是有利的
2 x ^ 4 + 4 x ^ 3 + 4 x ^ 2 + 4 + 2 = 2 (x + 1) ^ 2 * (x ^ 2 + 1)
来获得一个简单的深度2表示。因此,我们分别计算(x + 1)^2
和(x^2 + 1)然后再乘以2。
首先,我们计算x^2并添加一个明文“1”。从打印结果中我们可以清楚地看到,乘法消耗了大量的噪音预算。用户可以改变plain_modules参数来查看它对噪声预算消耗率的影响
print_line(__LINE__);
cout << "Compute x_sq_plus_one (x^2+1)." << endl;
Ciphertext x_sq_plus_one; //把密文记作x_sq_plus_one
evaluator.square(x_encrypted, x_sq_plus_one);
//先利用内置函数square ,平方x_encrypted,刷新入x_sq_plus_one
Plaintext plain_one("1"); //添加明文 1
evaluator.add_plain_inplace(x_sq_plus_one, plain_one);
//利用内置函数密文加
加密的乘法会导致输出密文的大小增加。更准确地说,如果输入密文的大小为M和N,那么同态乘法后的输出密文的大小为M+N-1。在这种情况下,我们执行平方,并观察大小增长和噪声预算消耗。
cout << " + size of x_sq_plus_one: " << x_sq_plus_one.size() << endl;
cout << " + noise budget in x_sq_plus_one: "
<< decryptor.invariant_noise_budget(x_sq_plus_one) << " bits" << endl;
Even though the size has grown, decryption works as usual as long as noise
budget has not reached 0.
Plaintext decrypted_result;
cout << " + decryption of x_sq_plus_one: ";
decryptor.decrypt(x_sq_plus_one, decrypted_result);
cout << "0x" << decrypted_result.to_string() << " ...... Correct." << endl;
/*
Next, we compute (x + 1)^2.
*/
print_line(__LINE__);
cout << "Compute x_plus_one_sq ((x+1)^2)." << endl;
Ciphertext x_plus_one_sq;
evaluator.add_plain(x_encrypted, plain_one, x_plus_one_sq);
evaluator.square_inplace(x_plus_one_sq);
cout << " + size of x_plus_one_sq: " << x_plus_one_sq.size() << endl;
cout << " + noise budget in x_plus_one_sq: "
<< decryptor.invariant_noise_budget(x_plus_one_sq)
<< " bits" << endl;
cout << " + decryption of x_plus_one_sq: ";
decryptor.decrypt(x_plus_one_sq, decrypted_result);
cout << "0x" << decrypted_result.to_string() << " ...... Correct." << endl;
/*
Finally, we multiply (x^2 + 1) * (x + 1)^2 * 2.
*/
print_line(__LINE__);
cout << "Compute encrypted_result (2(x^2+1)(x+1)^2)." << endl;
Ciphertext encrypted_result;
Plaintext plain_two("2");
evaluator.multiply_plain_inplace(x_sq_plus_one, plain_two);
evaluator.multiply(x_sq_plus_one, x_plus_one_sq, encrypted_result);
cout << " + size of encrypted_result: " << encrypted_result.size() << endl;
cout << " + noise budget in encrypted_result: "
<< decryptor.invariant_noise_budget(encrypted_result) << " bits" << endl;
cout << "NOTE: Decryption can be incorrect if noise budget is zero." << endl;
cout << endl;
cout << "~~~~~~ A better way to calculate 2(x^2+1)(x+1)^2. ~~~~~~" << endl;
噪声预算已经达到0,这意味着不能期望解密给出正确的结果。这是因为密文x_sq_plus_one和x_plus_one_sq都是由3个多项式组成,这是由于前面的平方运算造成的,而对大密文的同态运算比小密文的计算消耗更多的噪声预算。在更小的密文上计算也要便宜得多。
“Relinearization”是一种将密文的大小乘回初始大小2的操作。因此,在下一次乘法之前对一个或两个输入密文进行再初始化,可以对噪声增长和性能产生巨大的积极影响,即使再初始化本身有很大的计算成本。只可能将大小为3的密文重新初始化为大小为2的密文,因此用户通常希望在每次乘法之后重新初始化,以将密文大小保持为2。
Relinearization需要特殊的“Relinearization密钥”,可以将其视为一种公钥。使用密钥生成器可以很容易地创建重新初始化密钥。
Relinearization在BFV和CKKS方案中都是类似地使用,但是在本例中我们继续使用BFV。我们重复以前的计算,但这次是在每次乘法之后重新查找。
我们*使用KeyGenerator::relin_keys()来创建relinization密钥*。
print_line(__LINE__);
cout << "Generate relinearization keys." << endl;
auto relin_keys = keygen.relin_keys();
/*
We now repeat the computation relinearizing after each multiplication.
*/
print_line(__LINE__);
cout << "Compute and relinearize x_squared (x^2)," << endl;
cout << string(13, ' ') << "then compute x_sq_plus_one (x^2+1)" << endl;
Ciphertext x_squared;
evaluator.square(x_encrypted, x_squared);
cout << " + size of x_squared: " << x_squared.size() << endl;
evaluator.relinearize_inplace(x_squared, relin_keys);
cout << " + size of x_squared (after relinearization): "
<< x_squared.size() << endl;
evaluator.add_plain(x_squared, plain_one, x_sq_plus_one);
cout << " + noise budget in x_sq_plus_one: "
<< decryptor.invariant_noise_budget(x_sq_plus_one) << " bits" << endl;
cout << " + decryption of x_sq_plus_one: ";
decryptor.decrypt(x_sq_plus_one, decrypted_result);
cout << "0x" << decrypted_result.to_string() << " ...... Correct." << endl;
print_line(__LINE__);
Ciphertext x_plus_one;
cout << "Compute x_plus_one (x+1)," << endl;
cout << string(13, ' ')
<< "then compute and relinearize x_plus_one_sq ((x+1)^2)." << endl;
evaluator.add_plain(x_encrypted, plain_one, x_plus_one);
evaluator.square(x_plus_one, x_plus_one_sq);
cout << " + size of x_plus_one_sq: " << x_plus_one_sq.size() << endl;
evaluator.relinearize_inplace(x_plus_one_sq, relin_keys);
cout << " + noise budget in x_plus_one_sq: "
<< decryptor.invariant_noise_budget(x_plus_one_sq) << " bits" << endl;
cout << " + decryption of x_plus_one_sq: ";
decryptor.decrypt(x_plus_one_sq, decrypted_result);
cout << "0x" << decrypted_result.to_string() << " ...... Correct." << endl;
print_line(__LINE__);
cout << "Compute and relinearize encrypted_result (2(x^2+1)(x+1)^2)." << endl;
evaluator.multiply_plain_inplace(x_sq_plus_one, plain_two);
evaluator.multiply(x_sq_plus_one, x_plus_one_sq, encrypted_result);
cout << " + size of encrypted_result: " << encrypted_result.size() << endl;
evaluator.relinearize_inplace(encrypted_result, relin_keys);
cout << " + size of encrypted_result (after relinearization): "
<< encrypted_result.size() << endl;
cout << " + noise budget in encrypted_result: "
<< decryptor.invariant_noise_budget(encrypted_result) << " bits" << endl;
cout << endl;
cout << "NOTE: Notice the increase in remaining noise budget." << endl;
/*
Relinearization clearly improved our noise consumption. We have still plenty
of noise budget left, so we can expect the correct answer when decrypting.
*/
print_line(__LINE__);
cout << "Decrypt encrypted_result (2(x^2+1)(x+1)^2)." << endl;
decryptor.decrypt(encrypted_result, decrypted_result);
cout << " + decryption of 2(x^2+1)(x+1)^2 = 0x"
<< decrypted_result.to_string() << " ...... Correct." << endl;
cout << endl;
/*
For x=6, 2(x^2+1)(x+1)^2 = 3626. Since the plaintext modulus is set to 256,
this result is computed in integers modulo 256. Therefore the expected output
should be 3626 % 256 == 42, or 0x2A in hexadecimal.
*/
运行结果