ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

全同态加密与CKKS方案原理详解:从噪声预算到隐私计算实践

全同态加密与CKKS方案原理详解:从噪声预算到隐私计算实践 全同态加密这个词这几年不管是在学术论文里还是在实际项目里露脸频率都越来越高。大家聊隐私计算、安全多方计算、机器学习隐私保护的时候几乎绕不开它。我最初接触CKKS方案的时候光是搞懂“为什么它能做近似计算”就花了不少时间后来又因为参数没选好接连踩了好几个坑。这篇文章想把我的理解、实操过程和踩坑经验都摊开来讲尽量让准备入坑的同行少走点弯路。先说结论如果你要做的是拿密文跑机器学习的推理、算矩阵乘法、处理浮点数据CKKS基本是目前最实用的选择之一。它和另一类叫BGV/BFV的方案不一样天生面向实数/浮点数的近似运算所以行业里做隐私保护的很多都选它。1. 全同态加密到底在解决什么问题1.1 从“密文也能运算”说起传统加密大家都很熟把明文用密钥变成密文存起来或者传出去要用的时候解密。问题在于如果我想对别人手里的数据做计算比如医院想用AI模型分析病人的病历数据但数据本身属于病人不能明文交给医院那这件事就卡住了。以前的办法是把数据“脱敏”再交出去但脱敏后的数据失真模型算出来的结果可能不靠谱。全同态加密的思路是另一条路数据保持加密状态直接参与计算。也就是说你拿到的是密文 c Enc(m)然后你在不知道m的情况下可以直接算出 Enc(f(m))最后只有拥有私钥的人能解密出 f(m)。计算过程全程不暴露原始数据结果却是对的。这个特性在很多场景里就是救命稻草比如医疗数据联合分析、金融风控模型推理、联邦学习中间梯度的保护等。但要实现“任意函数都能在密文上算”数学上并不容易。早期的同态加密方案只能支持加法或者乘法后来Gentry在2009年用“自举”技术构造出第一个全同态加密方案理论上什么函数都能算了。不过自举性能开销极大所以这些年大家一直在做两件事一是把方案的性能优化到能用二是针对特定场景设计更高效的专用方案。CKKS就是沿着第二条路走出来的典型代表。1.2 方案代际划分与CKKS的独特定位业内经常把同态加密方案按“代数结构”分几代这里先不纠结技术术语简单梳理一下第一代Gentry的原始方案基于理想格结构复杂效率低基本停留在理论证明阶段。第二代BGV、BFV、NTRU类方案这些方案把明文空间放在多项式环上一次加密可以打包多个整数或者模p的元素加法和乘法都能做性能相比第一代有了质的飞跃。BGV/BFV的特点是精确运算适合处理整数。第三代GSW方案它在自举和电路深度控制上有优势但在通用场景效率一般。CKKS2017年由Cheon、Kim、Kim、Song提出它的最大特点是把“实数/浮点数”作为目标数据类型引入“近似计算 缩放因子”的机制让密文上的浮点运算像普通浮点运算一样自然。在机器学习场景里数据和模型参数基本都是浮点数所以CKKS一出来就非常受欢迎。很多人会问既然BGV/BFV能做精确整数运算那用它来做浮点行不行理论上可以比如把浮点数乘以一个很大的整数再取整然后做整数运算最后再转回来。但问题在于浮点数的乘法深度一旦上来整数的位宽就会爆炸参数要放大很多性能急剧下降。CKKS直接在设计层面解决了这个问题它允许计算过程中的精度损失可控而不是无限放大数位。所以我的判断是选BGV/BFV还是CKKS本质上是看你需要精确整数结果还是可以接受浮点近似结果。机器学习推理、统计分析这种场景CKKS天然合适而如果需要精确的整数运算比如计票、审计、金额结算BGV/BFV可能更稳。2. CKKS的核心原理把实数世界搬进多项式2.1 为什么CKKS能做“近似”计算要理解CKKS首先得知道同态加密方案基本都是构造在一个叫“多项式环”的代数结构上的。你可以把密文想象成一个多项式加法和乘法就对应多项式上的加法和乘法。问题来了多项式天然处理的是整数系数你怎么把实数向量塞进去而且还能做接近浮点语义的运算CKKS的核心思路是“缩放 取整 噪声伴随”。具体来说它先把每个实数分量乘以一个很大的缩放因子Δ比如2^40然后取整成整数再编码成多项式。计算过程都在这个放大后的整数域里进行最后解密后除以Δ恢复成实数。由于取整和计算过程中伴随噪声解密结果并不是精确值而是一个接近原值的近似数。这就是“近似计算”的由来。类比一下你在纸上做小数乘法比如1.23 × 4.56如果你把两个数都放大100倍变成123和456相乘得到56088再除以10000得到5.6088但4.56 × 1.23精确值是5.6088没问题。这里放大100倍就是缩放因子。CKKS的情况类似但多了噪声所以在加密域算出来的结果往往是“精确值加上小扰动”。设计目标就是让扰动足够小小到业务上可以忽略。2.2 编码与解码细节CKKS的编码过程是整个方案里最容易劝退新人的部分其实理解了就不难。它的目标是把一个长度为N/2的复数向量注意不一定非得是实数复数也能处理映射到一个N维多项式上这个N就是后面要讲的多项式模数次数。这里不展开全部数学推导只说关键逻辑选取分圆多项式环 R Z[X] / (X^N 1)其中N是2的幂比如4096、8192、16384。这个环有一个特殊性质X^N -1这让我们可以在N次单位根的位置上做类似DFT的变换。编码过程大体是利用“类似离散傅里叶变换”的框架把向量各分量插值到分圆域的某些嵌入位置上得到明文多项式m(X)。然后用缩放因子Δ乘上明文多项式再取整得到“放大后的明文多项式”。解码是编码的逆过程先去掉Δ再做逆变换恢复出向量同时噪声也被带出来。实操中你不需要自己实现编码器开源库都封装好了。以Microsoft SEAL为例调用Encoder::encode把向量变成明文再调用Encryptor::encrypt加密。但理解这个流程对你后面排查问题特别有用因为不少报错都源于你对“输入向量长度必须是N/2”这个约束不够敏感。2.3 密钥体系公钥、私钥与计算密钥CKKS和大多数公钥加密方案一样有一对公私钥。粗略说私钥 (secret key)负责解密绝不能泄露。公钥 (public key)负责加密可以公开分发。计算密钥 (evaluation key, relin key)这是同态加密特有的角色它本身也是公钥性质的用来在密文乘法后做“重线性化”把膨胀的密文压缩回去防止密文体积和噪声失控。实践里经常有新人问“我把公钥传给别人加密那我自己用私钥解密计算密钥放哪里”答案是在执行同态计算的节点上必须持有计算密钥并且这个节点可以不需要私钥。也就是说计算方拿着公钥加密的密文、拿着计算密钥进行计算但算不出明文。这是整个隐私保护能成立的关键。密钥生成阶段里计算密钥的大小比公钥大不少。比如在参数N8192时公钥可能几十KB而计算密钥上百KB甚至几MB这直接影响到通信开销和网络传输。如果你的系统里公钥和计算密钥要从一个节点分发到另一个节点要对带宽有心理准备。3. 实操起步用开源库跑通第一个CKKS例子3.1 工具链选择SEAL还是OpenFHE现在主流能跑CKKS的开源库有这么几个Microsoft SEAL代码质量高、文档较全、社区活跃C库也有Python绑定生成的是C动态库。适合想深入研究方案和做学术实验的人。OpenFHE吸收了SEAL、HElib等多个方案的后续发展支持同一接口切换多种方案设计更现代还在快速迭代中。适合要做产品原型、要在多个方案间对比的人。HElibIBM出品实现了很多高级功能但接口相对老派新项目用得不多了。Pyfhel / TenSEAL前者是SEAL/PALISADE的Python封装后者是面向PyTorch的加密扩展对纯Python用户很友好。我的建议是如果你的目标是快速验证一个隐私计算想法直接用TenSEAL或者Pyfhel如果想把它部署到生产环境且团队能接受C那SEAL是稳妥之选如果想长期维护、需要跨方案支持OpenFHE的路线更值得押注。我自己常用的是SEAL下面例子也以它为准。3.2 关键参数怎么定CKKS的参数选择没有一步到位的模板但每个参数的含义必须清楚参数名称作用N (poly_modulus_degree)多项式模数次数决定能够打包的槽位数N/2以及安全强度基线log(q)系数模数的总比特宽度决定噪声预算上限也就是能支撑多少层计算Δ (scale)缩放因子控制浮点数到整数的放大倍数直接影响精度L乘法深度你计划在密文上做多少次连续乘法各素数位宽coeff_modulus中的prime bit-length组合出一个总log(q)配合N保证安全级别需要注意N和log(q)不是越大越好。安全性规定N固定时log(q)有上限超过上限方案安全性就会跌破目标级别。比如N8192时很多资料建议log(q)不超过218位左右具体需用LWE估计器核算。这个约束直接影响你可以分配多少个素数栏位也就是深度精度怎么搭配。举个例子如果你想做深度为2的计算约等于两个连续乘法可以选N8192scale 2^40coeff_modulus [60, 40, 40, 60]总log(q)200。前一个60用于初始放大中间两个40对应两层乘法后的重缩放最后一个60留在最终结果上保证精度。这里总log(q)200低于安全上限没问题。3.3 完整流程拆解下面用SEAL的C API概念上兼容Pyfhel展示一个最小例子逻辑就四步参数生成、密钥生成、加密计算、解密解码。// 0. 创建上下文 EncryptionParameters parms(scheme_type::ckks); parms.set_poly_modulus_degree(8192); parms.set_coeff_modulus(CoeffModulus::Create(8192, {60, 40, 40, 60})); auto context SEALContext::Create(parms); // 1. 密钥生成 KeyGenerator keygen(context); auto secretKey keygen.secret_key(); auto publicKey keygen.public_key(); auto relinKey keygen.relin_keys(); // 2. 编码与加密 Encryptor encryptor(context, publicKey); CKKSEncoder encoder(context); double scale pow(2.0, 40); vectordouble input {1.1, 2.2, 3.3, 4.4}; Plaintext plain; encoder.encode(input, scale, plain); Ciphertext encrypted; encryptor.encrypt(plain, encrypted); // 3. 同态计算a*b c Evaluator evaluator(context); Ciphertext c1, c2, c3; // 假设已经各自加密好了 evaluator.multiply(c1, c2, result); evaluator.relinearize_inplace(result, relinKey); evaluator.rescale_to_next_inplace(result); // 记得把scale和result匹配好 evaluator.add_inplace(result, c3); // 4. 解密与解码 Decryptor decryptor(context, secretKey); Plaintext plainResult; decryptor.decrypt(result, plainResult); vectordouble output; encoder.decode(plainResult, output);这里面有几个细节往后经常踩坑乘法之后必须做relinearize否则密文尺寸膨胀到三个分量后续运算全乱。重缩放(rescale)是为了把scale降回去否则乘法后scale变成2^80后面所有数和它对齐会出问题。rescale后每个密文的scale会从2^40变成2^40但实际是把模数链削减一位。连续乘法中保证参与运算两个密文的scale一致或者明确处理scale不一致的情况否则噪声控制会变得很难看。4. 参数调优、噪声预算与性能平衡4.1 噪声预算是怎么回事同态加密的密文里除了真正信息位的多项式之外还有一层“噪声项”。每次同态乘法噪声都会被放大当噪声大到超过一定阈值解密就会失败或者精度崩坏。可以把它想象成信号里掺杂的底噪信号放大了底噪也放大最终信噪比太低你听到的全是噪音完全分辨不出原来的内容。在BFV/BGV里这个叫“噪声预算”的概念很直观每做一次乘法噪声预算就扣掉一块扣完就完。CKKS不一样它没有硬性失败点而是精度逐步劣化。数学上密文乘法带来的噪声会累积最终解密结果误差大致是 O(||m|| · ε) 级别其中ε跟缩放因子选择的位宽有关。所以CKKS参数调优的核心目标是让这个误差在你的业务容忍范围内。一个直观的经验法则是scale每减小1 bit结果的相对精度可能下降2^(-bit)。如果实际模型的精度要求不高比如分类任务最多到小数点后两位那scale用2^30甚至2^25都够如果你要算矩阵特征值、梯度等中间结果scale尽量给到2^40以上。4.2 重线性化与重缩放这里提醒得再多也不嫌多。很多新人在跑第一个例子时看到“microsoft SEAL: scale mismatch”这类异常就蒙了这个异常十有八九就是scale不匹配导致的。SEAL的设计是每个密文都带一个scale()属性表示当前密文的缩放因子的当前值。初始加密时scale等于你设置的值比如2^40。做一次乘法后两个2^40的密文相乘乘积的scale会变成2^80如果你不重缩放就继续加和scale2^40的密文相加SEAL会直接拒绝因为两个密文代表的数学量级不一致。解决方式就是重缩放它的数学本质是把密文里的每个系数除以scale的某个因子然后舍入到整数同时削减一部分噪声。在SEAL里对应rescale_to_next_inplace在Pyfhel里对应.rescale_to_next()。重缩放是可配置的每次削减一个素数模数的位宽比如从60减到40对应的scale也从2^60变到2^40。实操上设计coeff_modulus时就要想清楚你的乘法链有多深每个中间层要截多少位。一个常见设计就是[60, 40, 40, 60, 60]这种开头60用来保护明文放大中间每个40对应一层乘法后的重缩放最后剩60作为最终结果的精度兜底。每层重缩放就把一段噪声和scale同时“削”掉保证下一层运算有干净的起点。4.3 性能实测数据与调优建议我在一台主流服务器上做过一组简单测试参数是N8192、scale2^40、coeff_modulus总位宽200单次乘法大约需要几十毫秒级别C实现单次加法微秒到毫秒级别编码/解码和加密/解密都在毫秒级。这个量级对很多离线分析任务完全可接受但如果你要做在线实时推理比如每个请求都跑一次完整的密文计算开销就明显了。性能调优方向主要有几个尽量用批处理CKKS天然支持vector packing一个密文里能塞几千个值。如果你把一批数据一次性打包而不是逐个加密吞吐量能提升好几个数量级。比如你有一万个特征直接打包进一个密文做矩阵乘比一万次单值加密快太多。减少乘法深度模型评估前先做“深度分析”把计算图里的乘法深度压到最低能直接降低总log(q)从而减缓参数膨胀和噪声增长。N的选择要匹配数据量和精度N4096足够小数据量的轻量任务N16384适合更大批量、更高精度的场景但计算量会显著上升。矩阵运算的矩阵打包技巧用对角化diagonal packing处理矩阵乘法把矩阵分解成对角向量效率会好很多。这就不是几个小时的功夫了但值得投入。5. 实操中的坑与排查方法5.1 常见问题速查表下面这组问题是我在调试CKKS时反复遇到的列出来供你排查现象直接原因处理思路解密结果与原始值差别巨大scale设置偏小、乘法过深、噪声累积检查乘法深度和scale是否匹配降低深度或加大scale“scale mismatch”异常两个密文scale不一致对其中一个做rescale或调整scale后再相加/相乘加密时的输入向量长度超过N/2编码器容量限制把向量截断/分批或增大poly_modulus_degree性能突然暴增几十倍N过大 or 计算图深度过高先做计算图深度压缩再评估N是否过大安全级别跌破目标log(q)超限缩减coeff_modulus总位宽或调大N同一个程序不同运行结果有微小不同CKKS本身就是近似方案不是bug属于精度噪声确认误差范围即可5.2 定位问题的方法遇到诡异结果时我的习惯是先做一个“打开黑盒”的步骤先只做一次加密再解密确认编码解码和加密解密通路OK。再做一次加法或乘法单独验证单个运算的正确性。再组合两个运算逐步加深度。每步都打印中间结果的scale和噪声预算SEAL里decryptor.invariant_noise_budget()或Ciphertext::noise_budget这类接口。比如有一次我遇到解密结果全是“垃圾值”排查到最后才发现是乘法之后没有做relinearize密文第三项拖垮了后续运算。定位方法就是把计算结果和中间值都打出来和明文域的中间结果对比。这个习惯帮我省了不少时间。另外常见坑还包括ReLU这类非线性激活函数不能直接做CKKS的加法和乘法只能直接支持多项式函数。你需要用多项式近似替代比如用多项式逼近ReLU或者换成平方、三次方这种可计算函数。这一步对最终模型准确率影响很大要额外做精度验证。比较和取最大值运算代价极高即便能做也会急剧消耗噪声预算。能用算术近似解决的问题不要硬做比较运算。多个计算密钥不能confuserelinearize用的密钥要和当前方案参数一致否则结果会悄悄变错不报错但答案不对。5.3 方案组合与工程落地建议在真正的产品里你大概率不会只用CKKS解决所有问题。一个更务实的做法是把隐私计算方案好好搭配一下两端加密单方计算多个数据提供方把密文发给计算节点计算节点做完聚合返回这是目前最经典的用法。秘密共享 CKKS混合有时为了降低单方权限过高带来的风险会把数据同时用秘密共享和同态加密做双重保护。可信执行环境 CKKS对于性能极度敏感的场景考虑把CKKS的计算限制在必要路径普通计算走明文环境这样能兼顾安全与效率。工程上还有几个细节密文大小可能是明文的几百倍以上网络传输最好做压缩不然内部接口都可能成为瓶颈。计算节点的内存得管好N8192级别的一个密文大概几百KB如果你是批量处理几千个密文内存轻松上GB。定期做密钥轮换和权限分离私钥和计算密钥分开存放避免单点泄露导致整个体系失守。6. 经验小结与未来扩展路径我花了很长时间才适应CKKS那种“结果差不多对”的哲学。刚接触精确计算方案时看到解密结果和明文有误差总是心里不踏实。但后来做机器学习模型推理发现只要误差控制在百分之一以内模型准确率几乎不受影响心态就稳了。你在正式项目里使用CKKS之前我强烈建议先花一两天时间把下面这件事搞清楚把你自己的业务计算抽象成一张“计算图”标出每一步是加法还是乘法算出最大乘法深度然后根据深度去设计scale和coeff_modulus。这个前期设计花的时间远比你代码写完再回头调参数要少。接下来可以拓展的方向包括利用打包技巧做更复杂的矩阵运算、把自举技术引入CKKS实现深可信计算、甚至和联邦学习结合起来做真正的端到端隐私保护。如果你已经能吃透CKKS的基本原理去看这些高级主题会顺畅很多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进