我的隐私计算学习——同态加密

此篇是我笔记目录里的安全保护技术(四),前篇可见:

隐私计算安全保护技术(一):我的隐私计算学习——混淆电路-CSDN博客

隐私计算安全保护技术(二):我的隐私计算学习——秘密共享-CSDN博客

隐私计算安全保护技术(三):我的隐私计算学习——门限签名-CSDN博客

笔记内容来自多本书籍、学术资料、白皮书及ChatGPT等工具,经由自己阅读后整理而成。


(四)同态加密

笔记分享 | 组队学习密码学 —— 以CKKS为例,FHE基础知识介绍 

同态加密算法有着神奇的特性:在密文上进行计算且计算结果解密后所得的结果与明文计算一致。同态加密算法对数学知识要求极高,它实现了数据的“可算不可见”。按照性质的不同,同态加密算法一般可分为以下几类:

  1. 半同态加密(PHE)

    支持对密文进行部分形式的计算,例如只支持加法或只支持乘法操作,它们分别被称为加法同态加密算法和乘法同态加密算法。其中,满足乘法同态特性的典型加密算法包括 RSA 算法和 ElGamal 算法。

    (1)乘法同态加密算法

    目前只有未采用填充模式的原始RSA算法才满足乘法同态特性。由于原始 RSA 算法在加密过程中没有使用随机因子,相同密钥加密相同明文所得的结果也是相同的,因此利用 RSA 的乘法同态性质实现同态加密运算存在安全弱点,攻击者可能通过选择明文攻击得到原始数据。以下是 RSA 乘法同态的原理:

    image-20230315155622785

    ElGamal 算法是一种基于 Diffie-Hellman 离散对数困难问题的公钥加密算法,可实现公钥加密以及数字签名功能。它是一种随机化加密算法(相同密钥、相同明文两次加密所得的密文不同),以满足乘法同态特性,是同态加密国标标准中唯一指定的乘法同态加密算法。

    import gmpy2
    from Crypto.PublicKey import RSA
    # 比特位数
    RSA_BITS = 2048
    # 大素数 私钥
    RSA_EXPONENT = 65537
    # 根据RSA生成私钥
    private_key = RSA.generate(bits=RSA_BITS, e=RSA_EXPONENT)
    public_key = private_key.publickey()
    a = 15
    b = 5
    encrypted_a = gmpy2.powmod(a, public_key.e, public_key.n)
    encrypted_b = gmpy2.powmod(b, public_key.e, public_key.n)
    encrypted_c = encrypted_a * encrypted_b  # RSA支持乘法同态
    # 解密
    decrypted = gmpy2.powmod(encrypted_c, private_key.d, private_key.n)
    print(decrypted)

    (2)加法同态加密算法

    满足加法同态特性的典型加密算法有 Paillier 算法。Paillier 算法是一种基于合数剩余类问题的公钥加密算法,也是目前最为常用且最具实用性的加法同态加密算法,Paillier 算法通过将复杂计算需求以一定方式转化为纯加法的形式来实现。此外,Paillier 算法还可支持数乘同态,即支持密文和明文相乘。Paillier 算法也是同态加密国标标准中唯一指定的加法同态加密算法。

    image-20230315192745755

    # Paillier 算法的应用比较广泛,在Python 的 phe 库中比较容易找到实现该算法的函数库。
    from phe import paillier
    public_key, private_key = paillier.generate_paillier_keypair()
    a = 3.141592653
    b = 300
    encrypted_a = public_key.encrypt(a)
    encrypted_b = public_key.encrypt(b) * 2 # paillier支持数乘
    encrypted_c = encrypted_a + encrypted_b # paillier支持加法同态
    print(private_key.decrypt(encrypted_c))
  2. 类同态加密(SWHE)

    类同态加密也称为有限次同态加密,只支持在密文上进行有限次的加法和乘法操作(操作次数过多,则会导致噪声过大而无法解密)。Boneh-Goh-Nissim 方案是第一个同时支持加法同态和乘法同态的加密算法,支持任意次加法操作和一次乘法操作。该方案中的加法同态基于类似 Paillier 算法的思想,而一次乘法同态基于双线性映射的运算性质。虽然该方案是双同态的(同时支持加法同态和乘法同态),但只能进行一次乘法操作,属于类同态加密算法。

  3. 全同态加密(FHE)

    全同态加密算法支持对密文进行任意形式的计算,且运算操作次数不受限制(同时支持加法操作和乘法操作,理论上只要支持加法和乘法操作就能支持其他类型的操作)。

    (1)Gentry 方案(第一代全同态加密方案)

    基于电路模型,支持对每个比特进行加法和乘法同态。基本思想是在类同态加密算法的基础上引入 Bootstrapping 方法来控制运算过程中的噪声增长。Bootstrapping 方法通过将解密过程本身转化为同态运算电路,并生成新的公私钥对对原私钥和含有噪声的原密文进行加密,然后用原私钥的密文对原密文的密文进行解密过程的同态运算,即可得到不含噪声的新密文。也就是说,每计算一次就消除一次噪声,依此避免多次运算使噪声扩大。但是,这个解密过程本身的运算就十分复杂,运算也会产生大量噪声,所以需要预留足够的噪声增长空间并对预先解密电路进行压缩、简化,尽量把解密过程中的一些操作提前到加密时完成。

    (2)BGV 方案和 BFV 方案(第二代全同态加密方案)

    第二代方案通常基于 LWE(Learning With Error,容错学习问题)和 RLWE(Ring Learning With Error,环上容错学习问题)假设,其安全性基于格困难问题。BGV 方案中密文和密钥均以向量形式表示,且 BGV 方案采用模交换技术替代 Gentry 方案中的 Bootstrapping 过程,用于控制密文同态运算产生的噪声增长,而不需要通过复杂的解密电路实现。BFV 方案与 BGV 方案类似,但它不需要通过模交换技术控制噪声增长。目前最主流的两个全同态加密开源库 HElib 和 SEAL 分别实现了 BGV 方案和 BFV 方案。

    image-20230721114935599

    (3)GSW 方案和 CKKS 方案(第三代全同态加密方案)

    GSW 方案是一种近似特征向量的全同态加密方案。该方案基于 LWE 并可推广至 RLWE。GSW 方案的密文为矩阵形式,而矩阵相乘并不会导致矩阵维数的改变,因此 GSW 方案解决了以往方案中密文向量相乘导致的密文维数膨胀问题,无须进行降低密文维数的密钥交换。

    CKKS 方案支持针对实数或复数的浮点数加法和乘法同态运算,但是得到的计算结果是近似值。因此它适用于不需要精确结果的场景,比如机器学习模型训练等。HElib 和 SEAL 这两个开源库也都支持 CKKS 方案。

image-20230315154848320

接下来隆重介绍全同态加密开发框架 SEAL (Simple Encrypted Arithmetic Library,简单加密运算库)

开源框架 SEAL

        SEAL 是微软密码学与隐私组开发的开源全同态加密库,支持 BFV 方案和 CKKS 方案,支持基于整数的精确同态运算和基于浮点数的近似同态运算。相关代码在 GItHub 上开源:[ GitHub - microsoft/SEAL: Microsoft SEAL is an easy-to-use and powerful homomorphic encryption library. ]

        SEAL 基于 C++ 实现,不需要其他依赖库。在噪声管理方面,SEAL 库中每个密文拥有一个特定的噪声预算量,需要在程序编写过程中通过重线性化操作自行控制乘法运算产生的噪声。基于 SEAL 库实现同态加密运算的性能在很大程度上取决于程序编写的优劣,且存在不同的优化方法。SEAL 库提供的加密类型非常有限。总体而言,SEAL库存在一定的学习和使用难度。

------------------------加密参数如何设置?---------------------------

        SEAL 将同态加密算法涉及的相关参数统一封装在 EncryptionParameters 类中(构造该类的对象时需要传入方案类型),目前支持 BFV 和 CKKS 两种方案。EncryptionParameters 主要涉及 3 个加密参数:

  1. poly_modulus_degree,多项式模的次数,该值越大,性能越差但安全性越高。多项式模的次数必须为 2 的幂次方。

  2. coeff_modulus,系数模数,是一系列不同素数的乘积,其中每个素数最多占 60 个比特长。系数模数值越大,意味着加密计算能容纳计算产生的噪声越多,即计算能力越强。注意,系数模数的比特位长度也是有上限的,这取决于 poly_modulus_degree 的值。

    image-20230315214251140

    对于 BFV 方案,可以借用 CoeffModulus::BFVDefault(poly_modulus_degree) 函数来根据 poly_modulus_degree 的值返回一个素数向量,以便用来设置系数模数。对于 CKSS 方案,用户需要手工设置系数模数。

    // 采用CKKS方案
    EncryptionParameters parms(scheme_type::ckks);
    // 多项式模的次数8192对应的最长比特位长为218
    size_t poly_modulus_degree = 8192;
    parms.set_poly_modulus_degree(poly_modulus_degree);
    // 以下所有素数比特位长的总和为200,低于上限218,符合要求
    parms.set_coeff_modulus(CoeffModulus::Create(poly_modulus_degree, {60, 40, 40, 60}));
    ​
    /*
    ​
    此外,需要注意的是,组成coeff_modulus的素数向量中的素数个数决定了能进行重缩放的次数,进而决定能执行的乘法操作的次数。因此该系列数字的选择不是随意的,有以下 3 点要求:
    (1)总位长不能超过上图表所述的限制
    (2)最后一个参数为特殊模数,其值应该与其他模数中的最大值相等
    (3)中间模数与scale尽量相近
    ​
    */
  3. plain_modulus,明文模数,仅适用于 BFV 方案,该值越大,容纳噪声越多,但也意味着计算性能越差。SEAL 也提供了 PlainModulus::Batching 这样的辅助函数来帮助我们获取这样的素数。

设置上述 3 个参数时,需要综合考虑计算性能和计算能力的要求,在两者之间做好平衡,选定好参数后通过 SEALContext context(parms) 构造 SEALContext 上下文对象来实现参数设置。

// 采用BFV方案
EncryptionParameters parms(scheme_type::bfv);
size_t poly_modulus_degree = 8192;
parms.set_poly_modulus_degree(poly_modulus_degree);
// 采用默认的BFV系数模数
parms.set_coeff_modulus(CoeffModulus::BFVDefault(poly_modulus_degree));
// 辅助函数设置明文模数:选取20个比特位长的素数
parms.set_plain_modulus(PlainModulus::Batching(poly_modulus_degree, 20));
SEALContext context(parms);

------------------------密钥生成与加密解密?---------------------------

SEAL 属于公钥加密方案,密钥生成和加密解密涉及以下几个关键类:

  • KeyGenerator 类,统一生成密钥,创建该类实例时私钥会被自动创建,并且可以通过 create_public_key 函数创建多个公钥。

  • Encryptor 类,完成数据加密,创建该类对象实例时一般只需传入公钥,但在对加密数据进行序列化操作时如果能向 Encryptor 类对象实例提供私钥的话,则可以在序列化时使用私钥模式来压缩序列化数据的大小。

  • Plaintext 类,参与计算的明文数据需要通过 Plaintext 类进行封装。在 CKKS 方案中,也需要使用 CKKSEncoder 类对象来封装明文数据。同时,CKKS 方案中的明文浮点数需要放大成整数再进行处理。

  • Ciphertext 类,用来表示同态加密后的数据。

  • Decryptor 类,同态加密之后的计算结果需要使用 Decryptor 类进行解密,从而转换成明文数据对象。如果使用的是 CKKS 方案,我们还需要使用 CKKSEncoder 类对象对明文数据对象进行解码。

------------------------层的概念?---------------------------

        上面提到 SEAL 使用SEALContext类来记录各加密参数、维护上下文。其实,在创建 SEALContext 对象时 SEAL 还会自动创建一条模切换链,这是一条从原有加密参数集衍生出来的加密参数链。这条链可分成不同层。处于最高层的参数集(模系数)与原有参数集一致,被称作密钥层。上一层链的参数集去掉该层排在最后的参数构成下一层链的参数集,直到下一层的参数集不再有效(比如 plain modulus 明文模数比参数集中剩下的素数都大)。密钥层下面的各层被称作数据层。下面给出一个自定义模系数为“{50, 30, 30, 50, 50 }”时生成的模切换链示例,如图所示。

image-20230316152851455

        SEAL 在 Evaluator 类中提供 mod_switch_to_next 函数来将密文数据对象当前所在的层切换到下一层,还提供 mod_switch_to 函数来将密文数据对象当前所在的层切换到指定层(只能向低层切换)。为什么还要向低层切换呢?原因在于密文数据的大小与模系数中的素数个数成正比。如果后续不再需要进行密文计算,那么在把密文发给解密方前切换到最底层不但可以减小密文大小、提升发送效率,还可以为解密方提升解密效率(解密器在模切换链的任何一层都可以解密)。

        SEAL 在 SEALContext 类中提供了 key_context_data、first_context_data、next_context_data 等函数来遍历模切换链各层。

// 获取密钥层上下文数据
auto context_data = context.key_context_data();
// 输出密钥层所在层的编号
cout << context_data->chain_index() << endl;
cout << hex;
// 遍历该层模系数
for (const auto &prime : context_data->parms().coeff_modulus())
{
    // 输出模系数所用的素数
    cout << prime.value() << " ";
}
cout << dec << endl;
// 获取数据层上下文数据
context_data = context.first_context_data();
// 遍历数据层各层
while (context_data)
{
    // 输出密文数据对象当前所在层的编号
    cout << context_data->chain_index() << endl;
    // 转到下一层
    context_data = context_data->next_context_data();
}

------------------------密文如何计算?---------------------------

在SEAL中,基于加密数据的操作均使用 Evaluator 类来实现。一般情况下,实际应用中构建 Evaluator 对象并进行密文计算的计算方不持有私钥。目前,SEAL支持的密文计算操作主要有加法、减法、乘法、平方等(包括密文与密文之间的计算、密文与明文之间的计算),其他如除法、比较等运算均需要自己编写代码实现。密文计算有以下几个限制:

(1)参与运算的数据必须位于同一层,使用同一组参数进行加密。 (2)数据只能转到所在层的下层。 (3)参与加法或者减法的数据 scale 值必须相同。

针对上述限制,我们可以通过输出 Ciphertext 类的scale、parms_id 以及 SEALContext 类的 get_context_data 来确认即将进行操作的数据是否满足 scale 值相同、用同一组参数进行加密、位于链上的同一层这些计算条件。查看scale值的代码如下。

// 查看数据scale值
Ciphertext x, xsquare;
encryptor.encrypt(x_plain, x);
Evaluator evaluator(context);
evaluator.square(x, xsquare); // xsquare在level 2,scale为2^80
// 查看xsquare所在层的编号
cout << context.get_context_data(xsquare.parms_id())->chain_index() << endl;
// 查看xsquare的scale值
cout << xsquare.scale() << endl;

------------------------重线性化?---------------------------

密文乘法计算会使密文计算结果的所用空间变大,导致噪声容忍度变小。另外,大密文的计算资源消耗更大,因此 SEAL 引入了重线性化。重线性化是一种将密文的大小恢复到初始大小的操作。即使重线性化本身有较大的计算成本,在乘法计算后(下次乘法计算前)对密文计算结果进行重线性化,仍然可以对抑制噪声增长和提升性能产生积极影响。因此重线性化是采用 SEAL 编码时进行性能优化的重要手段。 ​ 重线性化需要特殊的重线性化公钥。使用 create_relin_keys 函数可以很容易地创建重线性化密钥。重线性化在 BFV 和 CKKS 方案中的使用方法类似。

Encryptor encryptor(context,public_key);
Ciphertext x_encrypted;
encryptor.encrypt(x_plain,x_encrypted);
RelinKeys relin_keys;
// 创建重线性化密钥
keygen.create_relin_keys(relin_keys);
Evaluator evaluator(context);
Ciphertext x_squared;
// 加密计算平方操作
evaluator.square(x_encrypted, x_squared);
// 平方操作后执行重线性化
evaluator.relinearize_inplace(x_squared, relin_keys);

------------------------重缩放?---------------------------

        上面提到 CKKS 方案在采用 CKKSEncoder 进行编码时需要设置 scale 参数,系统根据该参数将浮点数缩放成整数。然而,密文相乘后,scale 值也会增大,并且任何密文的 scale 值都不应该太接近于 coeff_modulus 的总比特位长,否则容易出现无法存放缩放后的新数据的情况。因此,CKKS 方案提供了重缩放功能来减缓 scale 值因为乘法操作而产生的扩张(密文加减操作不需要进行重缩放)。

        通常情况下,scale 值是需要精心控制的,这也就是 CKKS 方案中 coeff_modulus 需要精心选择的原因。假设密文的 scale 值为S,而当前数据层(不是密钥层,特殊模系数不参与重缩放)中参数集的最后一个素数是P,如果重缩放到下一层,密文的scale值将变为S/P。重缩放类似于之前提到的模切换操作。在模切换过程中,参数集中的最后一个素数被去除,相应的密文的 scale 值也会减小。由此可见,coeff_modulus 中素数的个数限制了重缩放的次数,从而限制了可进行的乘法操作的深度。

        经验总结,CKKS 方案中比较好的 coeff_modulus参数选择策略如下: (1)第一个参数选择长度稍长的素数(比如60比特),这样在解密时可以获得较高的精度; (2)最后一个参数长度同第一个参数(比如也选择60比特),这是特殊素数,必须和其他参数中最大的素数长度一样; (3)中间的素数长度一样长。

        SEAL 在 Evaluator 类中提供了rescale_to、rescale_to_next 等几个函数来实现密文的重缩放功能。但这里还有一个需要特别注意的问题,不同的密文在不同的层上进行重缩放(重缩放使用的素数 Р 可能不同)可能导致 scale 值不一样,或者各个密文本身设置的 scale 值就不一样,这都会导致各密文之间无法进行加法或者减法运算。

        此时,如果需要对各密文进行加法或者减法操作,我们就需要将 scale 值统一重缩放到相同的值。一般有两种方法:一种是直接设置数据的 scale 值,使 scale 值相同;另一种是将 1 编码成合适的 scale 值后与其中一个密文数据相乘,以达到两数 scale 值相同的目的。通常情况下,前一种方法更简单、直接。当然,两数操作需满足位于同一层的条件,如果不在同一层可以使用 mod_switch_to 函数进行切换。

parms.set_coeff_modulus(CoeffModulus::Create(poly_modulus_degree, {60, 40, 40, 60}));
// 假设生成的模系数素数分别为P0, P1, P2, P3, 这里P3是特殊模素数,不参与重新缩放
// scale值为2^40
double scale = pow(2.0, 40);
encoder.encode(1.1, scale, x_plain);
encryptor.encrypt(x_plain, x);              // x在level 2,scale值为2^40
evaluator.square(x, xsq);                   // xsq在level 2,scale为2^80
evaluator.rescale_to_next_inplace(xsq);     // xsq层变为level 1,且重新缩放到2^80/P2
// 获取xsq所在层的最后一个参数编号
parms_id_type last_parms_id = xsq.parms_id();
// x切换到xsq同一层
evaluator.mod_switch_to_inplace(x, last_parms_id);
// x与xsq的scale值不等,此处输出为0
cout << (xsq.scale() == x.scale()) << endl;
// 2^80/P2接近于2^40,设置xsq的scale值为2^40
xsq.scale() = pow(2.0, 40);
// xsq和x的scale值相同且在同一层,可相加
evaluator.add_inplace(xsq, x);

总结:

        在某些公钥密码算法的构造中,加密过程会引入一个随机源,这使得对同一个明文进行两次加密可能会得到不同的密文,这种类型的算法称为概率性加密算法;相反,如果在加密过程中没有引入随机源,那么对同一个明文进行加密的结果是不会改变的,这种类型的算法被称为确定性加密算法。确定性算法都不满足语义安全,比如 RSA 乘法同态加密算法。而 Paillier 加法同态加密算法是一种概率性加密算法,且实用性好,包括 SecureBoost 在内的众多算法选择用 Paillier 加法同态加密实现半同态加密。

        在联邦学习中,主要使用半同态加密算法实现对联邦其他参与方私有数据的操作,半同态加密技术主要应用在纵向联邦学习中。因为在这个过程中,不同的参与方有不同的特征,为了实现协同训练,不同参与方之间需要传输中间结果以聚合所有特征的效果,但这些中间结果往往会被恶意的联邦成员用来推理分析用户的隐私数据。为了避免隐私的泄露,联邦学习使用半同态加密算法对中间值进行处理,只传输中间结果的密文。具体可参考 SecureBoost 方案。


10月份新开了一个GitHub账号,里面已放了一些密码学,隐私计算电子书资料了,之后会整理一些我做过的、或是我觉得不错的论文复现、代码项目也放上去,欢迎一起交流!Ataraxia-github (Ataraxia-github) / Repositories · GitHub 

  • 20
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值