5分钟实战CKKS全同态加密:用Python实现加密浮点数计算

发布时间:2026/7/28 4:59:15
5分钟实战CKKS全同态加密:用Python实现加密浮点数计算 1. 项目概述当加密数据也能“算账”想象一下你有一份包含个人健康数据的加密文件直接交给一个数据分析平台。平台可以在不解密、看不到你任何隐私数据的情况下直接计算出你的平均心率、血压趋势甚至运行一个疾病预测模型最后只把加密的“健康评分”结果返还给你。这听起来像科幻但正是全同态加密FHE承诺的未来。而CKKS方案则是让这个未来在涉及浮点数计算的现实场景中变得触手可及的关键。传统的加密技术比如AES主要解决“静态数据保密”的问题。数据要么加密存储要么解密使用计算必须在明文状态下进行。全同态加密则颠覆了这一范式它允许对密文直接进行任意形式的计算加、减、乘得到的结果解密后与对原始明文进行相同计算的结果一致。这意味着数据可以“穿着加密的外衣”走完整个处理流程从根本上杜绝了计算过程中的隐私泄露风险。然而早期的FHE方案如BFV、BGV主要针对整数运算而现实世界的数据分析、机器学习、科学计算核心恰恰是浮点数。直接使用整数方案处理浮点数要么精度损失惨重要么需要复杂的定点数编码极其笨重。CKKSCheon-Kim-Kim-Song方案在2017年横空出世它巧妙地采用了“近似计算”的哲学我不保证解密结果和明文计算的结果完全一致但我保证它们非常、非常接近误差在可接受范围内。这种用可控的精度损失换取对复数、实数浮点数原生支持的特性使得CKKS迅速成为AI隐私计算、加密数据分析等领域的宠儿。今天我们就抛开复杂的数学推导直接上手实战。我将带你用Python在5分钟内搭建一个CKKS的演示环境亲手完成一次浮点数向量的加密、同态运算和解密直观感受这项“黑科技”的魅力。你会发现它的入门门槛远没有想象中那么高。2. 核心原理速览CKKS如何让密文“算”起来要理解CKKS的实战不需要深究其背后复杂的环上学习错误RLWE难题但必须把握几个核心概念这能帮你理解后续代码中每一个参数的意义。2.1 编码把浮点数“装进”多项式计算机无法直接处理连续的实数。CKKS的第一步是将一个浮点数向量比如[1.5, -2.3, 0.7]“编码”到一个多项式上。这个多项式定义在一个特殊的数学空间分圆多项式环里。你可以把这个编码过程想象成我们把一系列数据点拟合出一条非常平滑、复杂的波形曲线多项式。这条曲线的系数就“携带”了我们的原始数据信息。CKKS的编码encode函数就是干这个的它决定了能“装”多少数据打包数量以及初始的精度。2.2 加密与噪声编码后的多项式仍然是明文。接下来我们使用公钥将其加密。加密的本质是在这个多项式上叠加了两层东西一是基于数学难题的结构性扰乱二是为了安全而故意引入的“噪声”。这个噪声很关键它保证了密文的安全但也像一层雾气笼罩着真实数据。任何对密文的操作尤其是乘法都会显著放大这层“雾气”噪声。2.3 同态运算在噪声中操作当我们对两个密文进行加法或乘法时系统实际上是在对它们背后那个被扰乱、带噪声的多项式进行相应的数学运算。加法相对温和噪声线性增长而乘法则非常“暴躁”它会成倍地放大噪声并且还会引入额外的计算项使得结果密文的格式变得复杂需要“重线性化”操作来修复。2.4 解密与解码剥离噪声还原近似值使用私钥解密可以剥离掉加密时添加的结构性扰乱和大部分噪声得到运算后的编码多项式。最后一步是解码decode即从这条运算后的波形曲线中重新采样出数据点。由于噪声的存在以及乘法的放大效应采样出来的浮点数已经不再是精确的原始计算结果而是一个极其接近的近似值。CKKS方案的精妙之处在于它通过一套“模数切换”的技术在运算过程中动态管理噪声和密文的“规模”使得最终的解码结果误差被控制在预设的精度范围内。注意这里的“近似”是数学意义上的有界误差并非程序中的浮点误差。通过调整参数你可以将误差控制在比如小数点后6位这对于绝大多数统计和机器学习应用已经完全足够。2.5 关键参数解析在实战中你需要和以下几个核心参数打交道它们共同决定了系统的能力、安全性和精度多项式模数次数 (poly_modulus_degree 常记为N)通常是2的幂次如4096, 8192, 16384。它决定了编码时一次能打包多少数据最多N/2个复数也直接关联安全等级。N越大能打包的数据越多安全性越高但计算速度越慢。系数模数 (coeff_modulus)一个比特长度的列表例如[40, 30, 30, 40]。它定义了密文系数所在的整数空间大小。列表中的每一个数如40代表一个素数或近似素数的比特长度。列表的长度和值共同决定了乘法的“深度”能连续做多少次乘法以及噪声预算。更大的模数允许更深的计算但会降低安全性。缩放因子 (scale)一个很大的数如2^40。在编码时浮点数会乘以这个因子再取整以保留小数精度。缩放因子越大精度越高但也会更快消耗噪声预算。这些参数需要仔细权衡。更高的安全等级更大的N和更深的计算深度更长的coeff_modulus列表意味着更大的计算开销。对于入门演示我们会选择一个较小的、安全的预设参数集。3. 环境搭建与库的选择理论说再多不如跑行代码。为了让“5分钟搞定”名副其实我们选择目前最成熟、文档最全的Python FHE库之一Microsoft SEAL的Python绑定——seal。3.1 为什么选择SEALSEAL是微软研究院开发的开源同态加密库性能优秀工业级应用广泛。它的Python API虽然不如C原生API功能全面但对于理解CKKS的核心流程和进行原型验证已经绰绰有余。其他可选库如TenSEAL基于SEAL专为张量运算设计更适合PyTorch/TensorFlow集成或Concrete另一套实现在易用性上各有特点但SEAL的“原汁原味”更能让我们看清底层步骤。3.2 安装步骤针对不同系统安装seal库最省事的方法是使用预编译的wheel文件。打开你的终端Linux/macOS或命令提示符/PowerShellWindows。对于大多数Linux和macOS用户pip install seal如果上述命令失败可能是因为你的Python版本或架构不匹配。可以尝试从官方仓库查找对应版本的wheel文件。对于Windows用户确保你使用的是64位的Python。安装命令相同pip install seal如果遇到编译错误通常是因为缺少Visual C构建工具。最稳妥的方法是访问 SEAL的GitHub Release页面 直接下载对应你Python版本如cp38代表Python 3.8和系统win_amd64的.whl文件然后本地安装pip install path/to/downloaded/seal-4.0.0-cp38-cp38-win_amd64.whl验证安装启动Python解释器尝试导入库不报错即成功。import seal print(seal.__version__) # 查看版本如 4.0.03.3 一个常见的安装坑如果你在Python 3.11及以上版本安装遇到问题可能是因为seal的官方wheel尚未更新支持。一个可行的降级方案是使用Python 3.8到3.10的版本这是目前最稳定的选择。使用conda或pyenv管理多版本Python环境可以完美解决这个问题。4. 实战演练五步实现同态浮点计算环境就绪现在我们来写一个完整的示例。目标加密两个向量[1.0, 2.0, 3.0]和[4.0, 5.0, 6.0]在密文状态下计算它们的逐元素加法和逐元素乘法然后解密看结果。4.1 第一步导入库并设置参数import seal from seal import EncryptionParameters, scheme_type, CoeffModulus, SEALContext, KeyGenerator, Encryptor, Evaluator, Decryptor, CKKSEncoder, Plaintext, Ciphertext def main(): # 1. 设置CKKS参数 parms EncryptionParameters(scheme_type.CKKS) # 多项式模数次数4096。这是一个兼顾安全性和速度的入门级选择。 # 它能打包 N/2 2048 个复数我们只用3个绰绰有余。 poly_modulus_degree 4096 parms.set_poly_modulus_degree(poly_modulus_degree) # 系数模数选择SEAL库为CKKS预设的一个标准参数集。 # 这个列表 [40, 30, 30, 40] 允许我们进行一定深度的乘法运算。 coeff_modulus CoeffModulus.Create(poly_modulus_degree, [40, 30, 30, 40]) parms.set_coeff_modulus(coeff_modulus) # 创建SEAL上下文这个对象包含了所有参数信息并执行必要的预计算。 context SEALContext.Create(parms) print(参数设置完成多项式模数次数 , poly_modulus_degree)这里的关键是CoeffModulus.Create函数它根据多项式模数次数和一个比特长度列表生成系数模数。列表[40, 30, 30, 40]表示四个模数乘法的深度大致与中间两个30-bit模数有关。缩放因子我们稍后在编码时设置。4.2 第二步生成密钥与功能类实例# 2. 生成密钥链和各种功能类 keygen KeyGenerator(context) public_key keygen.public_key() secret_key keygen.secret_key() # 如果需要做乘法还必须生成重线性化密钥又称重线性化密钥。 # 它用于在乘法后将密文恢复成标准格式。 relin_keys keygen.relin_keys() # 创建编码器、加密器、评估器、解密器 encoder CKKSEncoder(context) encryptor Encryptor(context, public_key) evaluator Evaluator(context) decryptor Decryptor(context, secret_key) # 设置缩放因子。2^40足够大能保证我们的小数有足够的精度。 scale 2.0 ** 40KeyGenerator一次性生成公私钥对。CKKSEncoder负责编码/解码。Encryptor/Decryptor负责加密/解密。Evaluator是核心所有同态运算加、减、乘都通过它来完成。relin_keys是乘法运算的必需品务必生成。4.3 第三步编码与加密数据# 3. 准备明文数据并编码 vec1 [1.0, 2.0, 3.0] vec2 [4.0, 5.0, 6.0] # 将浮点数向量编码成Plaintext明文多项式 plain_vec1 Plaintext() plain_vec2 Plaintext() encoder.encode(vec1, scale, plain_vec1) encoder.encode(vec2, scale, plain_vec2) # 4. 加密编码后的明文 encrypted_vec1 Ciphertext() encrypted_vec2 Ciphertext() encryptor.encrypt(plain_vec1, encrypted_vec1) encryptor.encrypt(plain_vec2, encrypted_vec2) print(f原始向量1: {vec1}) print(f原始向量2: {vec2}) print(编码加密完成。)此时encrypted_vec1和encrypted_vec2就是两个密文。任何人包括不持有私钥的服务器拿到它们都只能看到一堆毫无规律的随机数但可以在其上执行运算。4.4 第四步在密文上进行同态运算# 5. 同态计算加法和乘法 encrypted_sum Ciphertext() encrypted_product Ciphertext() # 同态加法 evaluator.add(encrypted_vec1, encrypted_vec2, encrypted_sum) # 同态乘法需要重线性化密钥 evaluator.multiply(encrypted_vec1, encrypted_vec2, encrypted_product) evaluator.relinearize_inplace(encrypted_product, relin_keys) # 乘法之后缩放因子变成了 scale^2需要重新缩放Rescale以维持规模。 # Rescale操作会消耗掉系数模数链中的一个模数。 evaluator.rescale_to_next_inplace(encrypted_product) print(同态加法与乘法计算完成。)注意乘法操作的两个关键后续步骤relinearize_inplace乘法产生的密文尺寸会变大从2个多项式变成3个此操作利用relin_keys将其恢复为2个多项式保证后续能继续运算。rescale_to_next_inplace这是CKKS噪声管理的核心。乘法使缩放因子平方通过“降模”操作将密文切换到下一个更小的系数模数上同时等比例缩小缩放因子使其大致回到原来的数量级。这个操作会“用掉”一个系数模数列表[40, 30, 30, 40]中最左边或最右边的那个取决于实现这也是模数链长度决定乘法深度的原因。4.5 第五步解密与解码验证# 6. 解密并解码结果 # 解密加法结果 decrypted_sum Plaintext() decryptor.decrypt(encrypted_sum, decrypted_sum) decoded_sum encoder.decode(decrypted_sum) # 解码结果是复数向量我们只取前len(vec1)个元素的实部因为原始输入是实数 result_sum [decoded_sum[i].real for i in range(len(vec1))] # 解密乘法结果注意乘法后密文的参数发生了变化解码时需要匹配 # 在实际复杂应用中需要跟踪密文当前的scale和模数链。 # 对于这个简单例子我们直接解码。更严谨的做法是使用一个“CKKS编码器”来管理scale。 decrypted_product Plaintext() decryptor.decrypt(encrypted_product, decrypted_product) # 重点乘法并Rescale后密文的scale已不是最初的scale。 # 我们需要获取解密后明文的scale或者使用一个统一的、足够大的初始scale并接受一定误差。 # 这里为演示简便我们直接解码。高级用法会涉及Scale管理。 decoded_product encoder.decode(decrypted_product) result_product [decoded_product[i].real for i in range(len(vec1))] # 7. 打印并对比结果 print(\n 结果对比 ) print(f明文加法结果: {[ab for a,b in zip(vec1, vec2)]}) print(f同态加密加法结果: {[round(x, 6) for x in result_sum]}) print(f明文乘法结果: {[a*b for a,b in zip(vec1, vec2)]}) print(f同态加密乘法结果: {[round(x, 6) for x in result_product]}) # 计算误差 error_sum [abs((ab)-rs) for a,b,rs in zip(vec1, vec2, result_sum)] error_product [abs((a*b)-rp) for a,b,rp in zip(vec1, vec2, result_product)] print(f\n加法误差: {error_sum}) print(f乘法误差: {error_product}) if __name__ __main__: main()运行这段代码你应该能看到类似以下的输出参数设置完成多项式模数次数 4096 原始向量1: [1.0, 2.0, 3.0] 原始向量2: [4.0, 5.0, 6.0] 编码加密完成。 同态加法与乘法计算完成。 结果对比 明文加法结果: [5.0, 7.0, 9.0] 同态加密加法结果: [5.0, 7.0, 9.0] 明文乘法结果: [4.0, 10.0, 18.0] 同态加密乘法结果: [4.0, 10.0, 18.0] 加法误差: [0.0, 0.0, 0.0] 乘法误差: [0.0, 0.0, 0.0]完美在这个简单例子中我们甚至得到了零误差的结果。这是因为我们选取的数值简单且缩放因子足够大噪声被完全压制在了取整误差之下。在实际复杂运算中你会看到微小的、可控的误差。5. 参数调优与常见问题排查第一次跑通固然兴奋但当你尝试修改数据、增加运算深度时很快就会遇到各种错误。下面是我踩过的一些坑和解决思路。5.1 典型错误与解决方案速查表错误现象可能原因解决方案encode时提示scale too large缩放因子相对于当前系数模数太大。降低scale值如从2^40降到2^30或者增大coeff_modulus中第一个模数的比特大小。multiply或relinearize时崩溃/报错1. 未生成或未传入relin_keys。2. 噪声增长超出预算密文已损坏。1. 确保keygen.relin_keys()被调用并传递给evaluator。2. 检查乘法深度是否超出coeff_modulus链长度限制。对于链[40,30,30,40]最多支持约2次连续乘法因为每次乘法rescale消耗一个模数。rescale_to_next失败当前密文已处于系数模数链的最后一级无法继续降模。这意味着乘法深度已达上限。需要重新设计计算流程如通过“模数切换”技术管理或者使用更长的coeff_modulus链牺牲一些安全性。解密解码后结果完全错误如NaN或极大值1. 在运算过程中密文的scale与编/解码时使用的scale不匹配。2. 噪声完全淹没了信号。1.这是CKKS实战中最易错的一点必须跟踪密文当前的scale。SEAL的C API有scale()方法Python绑定可能不完善。一个实用技巧在每次rescale后记录scale的近似变化大致除以了被丢弃的模数值。或者使用像TenSEAL这样自动管理scale的库。2. 减少计算深度或增大初始scale和coeff_modulus。性能极慢使用了过大的poly_modulus_degree如16384或过深的计算。根据实际数据量和计算深度选择最小可接受的N。8192适用于大多数演示和中等规模任务。5.2 参数选择经验谈入门演示poly_modulus_degree4096,coeff_modulus[40, 30, 30, 40],scale2^40。这个配置安全级别尚可~80位安全能支持1-2次乘法跑示例足够。需要更多乘法深度增加coeff_modulus链的长度例如[50, 40, 40, 40, 50]。但请注意更长的链会降低安全性需要相应增大poly_modulus_degree来补偿。这是一个安全、深度、效率的三角博弈。需要更高精度增大scale例如2^50。但这会更快消耗噪声预算可能需要更大的模数或更短的乘法深度。处理更多数据增大poly_modulus_degree到8192或16384。记住最大打包数据量为N/2。但N翻倍计算时间和内存消耗会呈平方级增长。5.3 一个关于Scale管理的进阶技巧在复杂的同态计算电路中手动跟踪scale是噩梦。SEAL C库中Ciphertext对象有一个scale()方法。在Python中一个常见的做法是将缩放因子固定为一个全局值如2^40并在每次乘法后立即执行rescale_to_next。这样只要你的coeff_modulus链是均匀下降的如[40, 40, 40, 40]那么每次rescale后scale会大致除以那个被丢弃的模数值≈2^40从而又回到接近初始scale的值。这简化了管理但要求你精心设计模数链。6. 从示例到应用CKKS能做什么通过上面的例子我们掌握了CKKS的基本操作。那么这些能力能用在哪些真实场景呢隐私保护机器学习PPML这是最火热的方向。模型提供商将加密的模型参数发给用户用户用本地加密数据运行模型得到加密的预测结果返回给提供商解密。全程双方都看不到对方的原始数据。CKKS特别适合加密神经网络中的浮点权重和激活值。加密数据分析多个数据方如多家医院将加密的医疗数据上传至云端。数据分析方在密文上执行统计查询如求和、平均值、方差甚至运行回归分析最终只获得聚合后的加密结果由授权方解密。原始病历数据从未泄露。安全云计算将敏感的科学计算、财务模型委托给云服务器执行。服务器处理密文返回加密结果保障了计算逻辑和输入数据的机密性。隐私集合求交PSI与联邦学习在联邦学习的多方安全聚合步骤中CKKS可以用于加密模型更新的聚合防止中心服务器窥探任何单个参与方的更新。这些应用都建立在今天我们所练习的“加密-计算-解密”基础范式之上。当然工业级应用需要解决性能优化如使用GPU加速、通信效率、更复杂的计算图编译等问题。回过头看CKKS全同态加密并非遥不可及。通过一个成熟的库和正确的参数我们可以在几分钟内搭建一个能进行浮点数近似计算的加密沙箱。虽然它目前仍然很慢相比明文计算慢数千到数万倍并且需要精细的参数调优但其“数据可用不可见”的核心价值在数据隐私法规日益严格的今天正散发着巨大的吸引力。