安全多方计算这几年在隐私计算圈子里热度一直不低而加法秘密共享几乎是所有实用化方案的起点。很多人第一次接触秘密共享是从把秘密拆成几份凑齐才能还原这个直觉开始的但真正落到工程里最让人挠头的往往不是拆分本身而是拆完之后怎么在密文状态下做乘法。我前后在几个隐私求交、联邦统计的项目里反复用过这套东西踩过的坑不算少这里把加法秘密共享以及加法共享上的乘法运算从头到尾捋一遍包括原理、参数选择、实操步骤和排查经验尽量让刚入门的人也能跟着复现做过一阵子的人也能找到几个容易忽略的细节。1. 加法秘密共享的整体设计与思路拆解1.1 为什么是加法而不是别的拆分方式秘密共享的家族里常见的有Shamir多项式共享、中国剩余定理共享还有最朴素的加法共享。Shamir的好处是门限灵活比如3个参与方里任意2个就能还原但代价是每次运算都要处理多项式插值乘法还需要做降次工程实现复杂度高。加法共享的逻辑则简单到近乎粗暴一个秘密值x在模p的整数环上随机切成若干份所有份额相加模p等于x。要还原就所有份额相加少一份都还原不出来。我选加法共享作为主力方案核心原因是它和后续的乘法协议配合得最自然。加法共享天然支持加法同态——两个共享值相加各参与方本地把份额相加即可不需要任何通信。这个性质在统计类任务里太重要了求和、求均值、算频次全是加法本地一加就完事通信开销几乎为零。相比之下Shamir共享做加法虽然也能本地完成但份额结构更重传输和存储都更占资源。另一个现实考量是参与方数量。加法共享在两方场景下特别干净一方持有份额a另一方持有份额babx。两方乘法协议比如经典的Beaver三元组方案在加法共享上的表达非常简洁。而Shamir在少量参与方时反而显得杀鸡用牛刀。所以如果你的场景是两方或者少量固定参与方加法共享基本是首选。1.2 加法共享到底解决了什么问题一句话概括它让多个互不信任的参与方能够在不暴露各自原始数据的前提下共同完成计算。举个具体场景两家机构想统计各自用户的平均消费金额但谁都不愿意把用户明细给对方。用加法共享双方各自把数据拆成两份互相交换一份然后本地相加得到共享的中间结果最后通过安全协议还原出总和与总数整个过程没有任何一方看到对方的原始数据。这里的关键在于互不信任四个字。如果参与方之间完全信任那直接把数据汇总就行根本不需要秘密共享。秘密共享的价值恰恰在于它把信任需求降到了最低——你只需要相信协议本身不需要相信对方的人品。这也是为什么它在隐私计算、联邦学习、安全拍卖这些领域被反复使用。1.3 方案选型的几个关键权衡实际落地时加法共享有几个绕不开的权衡点我列个表对比一下方便你根据场景做决定。权衡维度加法共享的表现需要注意的地方通信开销加法零通信乘法需要交互乘法是主要瓶颈计算复杂度加法极低乘法依赖三元组三元组预生成有成本参与方数量两方最自然多方需扩展多方乘法协议更复杂安全性假设半诚实模型下很成熟恶意模型需额外校验实现难度低适合快速原型生产环境要处理边界半诚实模型和恶意模型的区别这里要强调一下。半诚实模型假设参与方会老老实实按协议执行只是会偷看中间数据恶意模型则假设参与方可能篡改消息、伪造份额。绝大多数加法共享的入门实现都是半诚实模型如果你要做生产系统必须明确你的威胁模型否则安全性论证是不成立的。我见过不少项目直接拿半诚实的代码上生产这是很危险的。2. 加法秘密共享的核心细节与实操要点2.1 份额拆分模数选择是第一道坎拆分本身很简单但模数p的选择直接决定了后续所有运算的正确性。常见的选择有两类一类是素数模比如一个大素数另一类是2的幂次模比如2^64。这两者各有讲究。用素数模的好处是它构成一个有限域除法、求逆这些运算都有良好定义很多密码学协议默认在域上工作。用2的幂次模的好处是它能直接对应计算机的整数运算溢出行为天然就是模运算实现起来快而且和定点数编码配合得好。我在做机器学习相关的安全计算时基本都用2的幂次模因为要处理浮点数的定点化表示用素数模反而要额外做编码转换。拆分代码大概长这样以两方为例import secrets def split_additive(secret, p, n_parties2): # 生成 n-1 个随机份额 shares [secrets.randbelow(p) for _ in range(n_parties - 1)] # 最后一份用 secret 减去前面所有份额 last (secret - sum(shares)) % p shares.append(last) return shares def reconstruct_additive(shares, p): return sum(shares) % p这段代码里有个细节值得说最后一份是用减法算出来的不是随机生成的。这是为了保证所有份额之和严格等于秘密值。如果你图省事把每一份都随机生成那还原出来的就不是原秘密了。这个错误我在早期实现里犯过调试了半天才发现。注意随机数的质量直接决定安全性。用语言自带的伪随机函数比如某些场景下的random模块是不够的必须用密码学安全的随机源Python里就是secrets模块。伪随机数可预测攻击者一旦预测出随机份额就能反推出秘密。2.2 份额的存储与传输格式份额本质上就是一个大整数但工程上怎么存、怎么传有讲究。如果模数是2^64那每个份额正好是一个64位无符号整数可以直接用定长字节序列表示传输效率最高。如果模数是素数份额的位长取决于素数大小通常要按固定字节数对齐避免解析时出错。我一般会把份额序列化成固定长度的字节串比如模数小于2^64就统一用8字节大端序。这样接收方解析时不用猜长度直接按8字节切分即可。跨语言交互时尤其要注意字节序我遇到过Python端按小端序打包、C端按大端序解析结果份额全错的情况排查了很久才定位到。传输层面份额的机密性依赖于通道安全但份额本身不应该再被加密一层——因为份额已经是秘密的随机化表示再加密纯属浪费。不过份额的完整性需要保护防止传输中被篡改通常用消息认证码MAC来保证。这一点在半诚实模型下可以省略但恶意模型下必须加。2.3 加法同态的实操验证加法同态是加法共享最实用的性质验证起来也简单。假设两方分别持有x和y的份额想计算xy的共享。做法是每一方把自己持有的x份额和y份额本地相加得到新的份额。所有新份额之和就等于xy。# 参与方1持有 x1, y1参与方2持有 x2, y2 # 计算 xy 的共享 new_share_1 (x1 y1) % p new_share_2 (x2 y2) % p # 验证new_share_1 new_share_2 (xy) % p这个操作零通信纯本地计算速度快到可以忽略。我在做大规模统计时几百万条记录的求和就是靠这个性质各参与方本地累加自己的份额最后只还原一次总和通信量从O(n)降到O(1)。这是加法共享相比其他方案最爽的地方。但要注意加法同态只对加法成立减法也可以减去一个份额等价于加上它的负元但乘法和除法不行。这就是为什么乘法需要单独的协议也是下一节的重点。3. 加法共享上的乘法运算实现3.1 乘法为什么不能本地完成先想清楚一个问题为什么两个加法共享相乘不能像加法那样本地搞定假设x被拆成x1x2y被拆成y1y2。如果双方各自本地计算x1y1和x2y2然后相加得到的是x1y1 x2y2而我们要的是(x1x2)(y1y2) x1y1 x1y2 x2y1 x2y2。差了交叉项x1y2 x2*y1。问题就出在交叉项上。x1在参与方1手里y2在参与方2手里参与方1算不出x1*y2因为它不知道y2参与方2同理。所以乘法必须引入通信让双方能协作算出交叉项同时又不暴露各自的份额。这就是所有安全乘法协议的核心难点。3.2 Beaver三元组最经典的乘法方案Beaver三元组是解决这个问题的经典方法思路非常巧妙。核心思想是预先准备一组乘法三元组(a, b, c)满足c a*b其中a、b、c都以加法共享的形式分散在各参与方手里。注意这个三元组是提前生成的与要计算的x、y无关。有了三元组计算x*y的共享分几步走第一步双方各自计算d x - a和e y - b的共享。因为x、a都是加法共享减法可以本地完成得到d和e的共享。第二步双方互相公开d和e的值注意是公开d和e不是公开x和y。这一步需要通信双方交换各自的d份额和e份额然后各自相加得到完整的d和e。第三步双方本地计算z c db ea d*e的共享。这里c、a、b都是共享形式d和e是公开值所以每一项都能本地算出来。为什么这样能得到xy展开验证一下x a dy b e所以xy (ad)(be) ab ae db de c ae db de。完全对上了。关键在于公开d和e不会泄露x和y因为a和b是随机的、参与方不知道的。d x - a只要a足够随机d就是均匀分布的看不出x的任何信息。这就是三元组的安全性来源。3.3 三元组的预生成与消耗管理三元组不能临时生成因为生成三元组本身就需要安全乘法要保证c a*b且各方不知道a、b这是个鸡生蛋的问题。实际做法是提前用其他手段生成一大批三元组存起来备用。生成方式有几种一种是用同态加密一种是用不经意传输还有一种是用可信第三方直接分发。我在项目里最常用的是基于不经意传输的方案因为它不依赖额外的密码学假设纯靠对称密码学原语就能搞定速度快。生成一批三元组后存到一个池子里每次需要乘法就从池子里取一个用完即弃。这里有个重要的安全原则三元组绝对不能重复使用。一旦同一个三元组被用于两次乘法攻击者就能通过两次公开的d、e值联立方程反推出a、b进而威胁到x、y的隐私。所以三元组的消耗管理很关键。我一般会预估整个计算过程需要多少次乘法然后多生成20%到30%的冗余量防止中途不够用。如果计算过程中三元组耗尽要么暂停重新生成会阻塞要么提前分批生成。生产系统里三元组的生成速率往往决定了整个安全计算的吞吐上限这是性能优化的重点。三元组生成方式优点缺点适用场景不经意传输无额外假设速度快实现较复杂两方高频计算同态加密概念直观计算开销大低频或原型验证可信第三方分发实现最简单引入信任假设内部可信环境3.4 完整乘法流程的代码实现把上面的步骤串起来两方Beaver乘法的大致实现如下def beaver_multiply(x_shares, y_shares, triple, p): # x_shares [x1, x2], y_shares [y1, y2] # triple ([a1,a2], [b1,b2], [c1,c2]) a_shares, b_shares, c_shares triple # 第一步本地计算 d x - a, e y - b 的份额 d_shares [(x_shares[i] - a_shares[i]) % p for i in range(2)] e_shares [(y_shares[i] - b_shares[i]) % p for i in range(2)] # 第二步互相公开 d 和 e双方都能得到完整的 d, e d (d_shares[0] d_shares[1]) % p e (e_shares[0] e_shares[1]) % p # 第三步本地计算 z c d*b e*a d*e 的份额 z_shares [] for i in range(2): zi (c_shares[i] d * b_shares[i] e * a_shares[i]) % p z_shares.append(zi) # 注意 d*e 是公开值双方都加上但只能加一次 z_shares[0] (z_shares[0] d * e) % p return z_shares这里有个容易出错的细节de是公开值双方都知道但它在最终的共享里只能出现一次。如果双方都加上de结果就会多算一份。我的处理方式是只让参与方1加上de参与方2不加。这样z_shares[0] z_shares[1] c db ea de正好等于xy。这个坑我在第一次实现时踩过还原出来的结果总是偏大查了好久才发现是de加重复了。提示de的归属问题在多方场景下更微妙。n方乘法时de只能由其中一方承担或者按某种约定分摊绝不能每方都加。建议在协议文档里明确写清楚避免实现分歧。4. 常见问题与排查技巧实录4.1 还原结果不对的排查思路乘法实现完第一次跑出来结果不对是常态。我总结了一套排查顺序基本能覆盖九成以上的问题。先查模数一致性。所有参与方的模数p必须完全相同包括三元组生成时用的模数。我遇到过三元组用2^64生成、乘法用素数模计算的情况结果自然全错。模数不一致是最隐蔽的bug之一因为单看每一方的代码都没问题。再查de的加法次数。前面说过de只能加一次。如果结果偏大或偏小一个de基本就是这个原因。可以打印中间值验证单独算一次xy和协议算出来的对比差值如果是d*e那就定位到了。然后查三元组是否满足c ab。三元组生成环节如果有bugc和ab对不上整个乘法就崩了。建议在生成三元组后加一个自检随机抽取几个三元组还原a、b、c验证c a*b % p。这个自检成本很低但能拦住大量问题。最后查份额的符号处理。模运算下减法要加p再取模如果直接相减得到负数后续运算就会出错。Python的%运算符对负数会返回正余数但C等语言不一定跨语言时尤其要注意。4.2 性能瓶颈的定位与优化安全乘法的性能瓶颈通常在两个地方三元组生成和网络通信。定位方法很简单分别计时。如果三元组生成占了大部分时间那就优化生成环节比如用更快的密码学原语或者预生成更多三元组做缓存。如果通信占大头那就考虑批量处理——把多个乘法打包减少往返次数。我做过一个优化把1000次乘法需要的d、e值批量交换而不是每次乘法都通信一次。这样网络往返从1000次降到1次整体速度提升了一个数量级。代价是需要缓存更多中间状态内存占用上升但在这个场景下完全值得。另一个优化点是三元组的复用策略。虽然三元组不能重复用于不同的乘法但可以预生成一个足够大的池子用多线程并行生成让生成和消耗重叠起来。这样计算过程不会因为等三元组而停顿。4.3 安全性自查清单功能跑通不代表安全下面这份清单建议每次上线前过一遍。检查项合格标准常见问题随机数来源密码学安全随机源用了伪随机三元组复用绝不重复使用为省事复用模数一致性全局统一混用不同模数中间值公开范围只公开d、e误公开份额威胁模型明确半诚实或恶意模型不清份额完整性恶意模型下有MAC裸传份额特别说一下中间值公开范围。Beaver协议里公开的是d和e不是x、y也不是a、b。我见过有人图方便直接把x的份额公开了那等于把秘密拱手送人。公开的每一个值都要问一句它泄露原始数据了吗d x - a只要a随机且保密d就是安全的。这个推理链条要能说清楚否则不要上线。4.4 多方场景下的额外坑两方乘法相对干净扩展到三方及以上时问题会成倍增加。首先是三元组的生成多方三元组需要所有参与方协作通信轮数增加。其次是d、e的公开多方场景下需要所有方都拿到完整的d、e广播通信的开销不可忽视。最后是恶意模型下的校验多方场景里只要有一方作恶整个结果就可能被污染需要额外的可验证机制。我的建议是如果参与方超过三个认真评估是否真的需要秘密共享或者是否可以用更简单的方案比如可信执行环境替代。秘密共享的通信复杂度随参与方数量增长很快超过一定规模后性能会变得难以接受。这不是协议本身的缺陷而是分布式安全计算的固有代价。5. 从加法共享到实际应用的落地建议5.1 定点数编码让秘密共享能算小数加法共享原生只能处理整数但实际业务里小数无处不在。解决办法是定点数编码把小数乘以一个缩放因子转成整数再共享。比如保留6位小数精度就乘以10^6。计算完成后还原结果再除以缩放因子。这里有个精度陷阱。乘法会让缩放因子平方两个各带10^6缩放的值相乘结果带10^12缩放如果模数不够大就会溢出。所以模数的位长要预留足够空间。以2^64模数为例如果输入值本身占32位缩放占20位那乘法后可能超过64位必须提前规划。我一般会算清楚最坏情况下的位宽需求宁可模数选大一点也不要中途溢出。5.2 批量计算的组织方式实际任务往往是批量乘法比如矩阵乘法、多项式求值。这时候不要一个一个乘法单独走协议而是把整个批次组织起来共享三元组、批量通信。矩阵乘法尤其适合这种优化因为大量乘法可以并行通信可以合并。我做过一个安全矩阵乘法的实现把左矩阵的每一行和右矩阵的每一列做内积内积里的所有乘法共享同一批通信轮次。这样通信次数从O(mnk)降到O(轮数)性能提升非常明显。代价是实现复杂度上升需要仔细管理中间状态的生命周期。5.3 与其他隐私计算技术的配合加法共享不是孤立的它经常和其他技术配合使用。比如和同态加密结合用同态加密生成三元组用加法共享做在线计算或者和安全比较协议结合实现隐私求交、隐私集合运算。理解加法共享在整个技术栈里的位置比孤立地掌握它更重要。我在项目里的经验是加法共享适合做计算层负责高效的加减乘同态加密适合做预处理层负责生成三元组这类需要复杂密码学操作的素材不经意传输适合做通信层负责高效的数据交换。三者各司其职组合起来才能撑起一个完整的隐私计算系统。5.4 工程化落地的几个提醒最后说几个工程上的实在建议。第一协议实现要有完整的单元测试尤其是边界情况比如份额为0、模数边界、负数输入。第二日志要记录关键中间值但绝不能记录份额和秘密值否则日志本身就成了泄露渠道。第三性能测试要覆盖最坏情况比如三元组耗尽、网络延迟高、参与方掉线。第四版本管理要严格协议一旦上线任何改动都可能破坏兼容性参与方之间的协议版本必须一致。我个人在实际操作中的体会是加法秘密共享的入门门槛很低半天就能写出能跑的代码但要做到生产级的安全和性能需要关注的细节非常多。乘法协议尤其如此Beaver三元组看起来简单但三元组生成、消耗管理、d*e归属这些环节每一个都有坑。建议新手先用小规模数据把整个流程跑通验证正确性再逐步优化性能和扩展参与方数量。不要一上来就追求高性能正确性永远是第一位的。