资讯动态

基于Rust的安全多方计算实现隐私保护协作推理实践

发布时间:2026/9/10 7:02:47 来源:尧图企业网站定制
前阵子我在帮一个医疗 AI 团队设计推理服务的隐私方案对方提了个特别现实的需求手里有模型的一方不想把权重暴露出去手里有病人特征数据的一方又绝不允许原始样本离开本地但两方还都得靠这个模型拿到推理结果。这种场景光靠传统的 API 调用根本聊不拢最后我把目光落在了安全多方计算MPC上并且用 Rust 把这个协作推理的整个链路从零到一跑通了。这篇文章就来聊聊这个项目里我踩过的坑、想明白的原理以及一份可以直接抄作业的最小实现思路。先说明白这不是一篇纯啃论文的科普而是面向“真想动手做”的人。无论你是做隐私计算平台、边缘 AI 推理还是单纯想见识一下安全多方计算在 Rust 生态里能玩到什么程度下面这些内容应该都能帮到你。我尽量把协议跑通的细节讲透但也会坦诚说明哪些地方是教学级简化哪些地方真正上生产还需要补课。1. 隐私保护协作推理我为什么选择 Rust 来啃这块硬骨头1.1 协作推理到底在保护什么先捋一下参与方的真实利益关系。协作推理最常见的形态是模型持有方 A 训练好了一个模型数据持有方 B 有若干条样本输入双方都希望最终能获得推理输出但谁也不愿意把“能还原模型的中间信息”或“原始样本”交给对方。这里有个常被忽略的细节单纯加密传输并不能解决问题。如果 A 把模型权重加密后发给 BB 在自己本地解密推理那 B 在拿到权重的那一刻就拥有了复制模型的能力反过来如果 B 把样本加密发给 AA 在某个环节也必须解密才能执行计算样本照样会暴露。真正的隐私保护协作推理要求计算过程本身就在密文状态下完成参与者从头到尾只接触秘密分享的“碎片”而不是可还原的完整数据。MPC 的核心思路就是把一个计算函数拆成多个共享的输入碎片每个参与方各自持有碎片并执行本地计算最后通过特定协议把结果拼起来。只要协议设计正确任何一方都无法从自己手上的碎片和通信记录中推出别人的完整输入。1.2 这个场景为什么非 MPC 不可同态加密哪儿不够用很多人第一反应是“用全同态加密FHE啊”。确实FHE 可以直接在密文上做任意计算但落到工程上问题不少噪声管理复杂、单次乘法开销极大、电路深度一旦增加性能断崖式下跌。做一次简单的逻辑回归推理可能还能接受换成稍大一点的神经网络那个计算开销会让延迟直接失控。MPC 走的是另一条路线把计算拆成加法、乘法门用秘密分享和 Beaver Triple 等技巧以少量通信换取可接受的计算开销。它的单机计算量通常比 FHE 低一个量级以上代价是需要参与方之间多轮网络交互。在局域网、边缘节点通信稳定的场景里MPC 往往比 FHE 实用得多。我把两条路线放在一起对比过结论是如果你要保护的数据和模型都比较大又对实时性有要求关注 MPC 是更现实的选择。对比维度安全多方计算MPC全同态加密FHE计算开销本地秘密共享运算较低格运算开销极大乘法尤其昂贵通信开销每轮乘法需要通信多轮交互几乎不需要通信单方离线计算噪声管理无噪声问题噪声增长是硬限制实现复杂度协议协调较繁琐密码学细节极复杂典型场景多方联合推理、隐私求交垂直场景外包计算、数据发布1.3 Rust 在这个赛道上的定位为什么是 Rust坦白说Python 生态在 MPC 研究领域更热闹MP-SPDZ、TF-Encrypted、PySyft 这些框架都更好上手。但 Rust 的优势在于部署形态最终能编译成一个无运行时的单一二进制内存占用低交叉编译到 ARM 边缘设备也不痛苦。医疗、金融这类行业的部署环境往往带严格安全策略一个静态链接的二进制比带一堆 Python 依赖的服务容器好过审得多。再加上 Rust 的类型系统和所有权模型在写协议逻辑时能帮你挡住大量低级错误。你不需要在运行时靠“小心谨慎”来保证状态一致编译器在编译期就把数据竞争、生命周期问题拦住了。对密码学协议这种“错一个字节全线崩溃”的领域来说这种安全感很值钱。2. 安全多方计算的底层引擎秘密共享、Beaver Triple 与电路建模2.1 从加法秘密共享开始建立直觉MPC 的基石是秘密共享。最简单的是加法秘密共享把一个秘密值 x 拆成两份碎片 x0、x1满足 x0 x1 xmod p。拆数的人自己保留 x0把 x1 发给另一方。单独看任何一片都得不到 x 的任何信息。为什么模 p因为所有运算默认跑在一个有限域里取模可以让结果始终落在固定范围内也避免整数溢出导致的值域混乱。你可以把有限域理解成一个“循环世界”到顶了就绕回 0。所有秘密共享的加法和乘法都定义在这个世界里。加法秘密共享最漂亮的性质是两方本地把自己的碎片相加再合并结果就等价于明文相加。也就是说[a b] [a] [b]完全不需要通信。2.2 Shamir 秘密共享从两方到多方如果参与者超过两个加法碎片方案就不够用了。这时可以用 Shamir 秘密共享要在一个 t 人小组里共享秘密 s就随机选一个 t-1 次多项式让多项式常数项等于 s然后给每个人发一个多项式上的点。任意 t 个人凑齐就能通过拉格朗日插值恢复 s少于 t 个人则一无所获。Shamir 和加法秘密共享并不冲突它是加法秘密共享在多方阈值场景的自然推广。你在实际工程里经常看到两种方案切换着用两方场景用加法碎片简单直接三方以上场景用 Shamir 或复制秘密共享Replicated Secret Sharing来减少通信量。2.3 Beaver Triple为什么乘法需要两轮通信加法不需要通信但乘法的秘密共享就麻烦了。[a * b]并不等于[a] * [b]因为两个碎片各自做乘法之后结果里会出现交叉项 a0b1 和 a1b0这两项是无法由单一参与方本地消掉的。Beaver Triple 解决的就是这个交叉项问题。它的原理是选择一个预处理好的“三元组”(a, b, c)其中 c a * b并且 a、b 也被秘密共享给了双方。在线计算时双方先把输入 x、y 和三元组里的 a、b 做差值得到 e x - a 和 d y - b然后把 e、d 公开给对方。由于 e、d 被 mask 过公开它们不会泄露 x、y 的信息。紧接着本地算z e * d e * b d * a c展开后就是 (x - a)(y - b) (x - a)b (y - b)a ab xy。关键点在于整个计算过程只需要双方交换 e 和 d 这两个被掩盖后的差值真正的输入碎片从未直接暴露。这也是 MPC 性能优化的核心地带——所有乘法的通信都发生在交换 e、d 这一步。你不需要自己脑子里跑这个展开式但你要记住一个结论在 Beaver Triple 方案里每个乘法在线的通信量只有“交换两个域元素”通信轮数则看协议怎么组织能并行合并的乘法尽量合并成一轮。2.4 电路建模把一次推理拆成加法和乘法门MPC 计算本质上是在算一张“算术电路”节点是加法和乘法门。神经网络推理可以看作一系列矩阵乘法和非线性激活的组合矩阵乘法拆开就是无数个乘加操作天然适合映射到 MPC。但激活函数是麻烦制造者ReLU 这种分段函数需要比较大小而密码学比较操作很贵在 MP C 里通常要用混淆电路Garbled Circuit或者秘密共享-混淆电路之间的转换协议。纯算术秘密共享的世界里大家更愿意用低阶多项式近似激活函数比如 x^2 或者 0.5x 0.5x^2。用多项式的好处是只有加法和乘法和 Beaver Triple 无缝配合性能好得多。所以做 MPC 推理时不要直接拿 PyTorch 里训练好的 ReLU 网络硬套而是先在明文侧换一个可被 MPC 友好近似的激活函数重新训练一下。精度会有轻微下降但换来的是整个隐私推理过程可以跑在纯算术秘密共享上。2.5 安全模型的潜规则半诚实、恶意敌手与同步信道协议安全等级必须先说清楚否则后面都是空谈。半诚实模型假设参与方会老老实实按协议执行只是会偷看中间数据恶意模型则假设参与方可能篡改协议、伪造输入、中途退出。半诚实协议简单高效是学术界和早期工业界的主流选择。恶意安全协议一般在每个秘密碎片上绑一个消息认证码MAC接收方验证 MAC 来确认碎片确实来自正确的发送方没有在中途被篡改。实现成本和通信量至少翻倍。现实项目里我倾向于先按半诚实模型做出来因为很多行业合作场景本身有法律合同和审计约束参与方不太可能为了偷看几个中间值主动破坏协议。但如果你要给多参与方提供计算服务参与方之间没有信任基础那必须升级到恶意安全模型。另外MPC 协议默认参与方之间有一条加密且经过身份认证的通信信道。别以为 MPC 本身能替代传输加密它保护的是“计算过程中不泄露明文内容”但中间结果片段仍然不能被第三方窃听。实际部署时参与方之间至少要走 TLS 或 QUIC。3. Rust 生态里的 MPC 积木选型对比与自研边界3.1 哪些 Rust crate 真实可用我去翻遍了 crates.io 和 GitHubRust 的 MPC 生态没有 Python 那么厚但也不是空白。几个方向值得关注crate / 项目定位适合场景tlsn 系列TLS Notary用 MPC 做数据来源证明需要证明某个数据来自安全通道内的真实响应scryprt 下的 mpz 系列布尔/算术电路、混淆电路的可组合构建块想在 Rust 里搭自定义 MPC 协议garble 系列混淆电路Yaos Garbled Circuit实现两方布尔电路计算、比较类算子curve25519-dalek 等底层椭圆曲线、哈希、随机数自己实现定制密码学原语时的地基这些库大多还带研究性质接口不稳定、文档稀疏直接用于生产需要做大量安全审计。我在实际项目里是拿它们做参考和底层的密码学原语协议核心逻辑还是自己维护的。3.2 为什么这个 Demo 我选择从零实现最小秘密共享这个选择可能有点反直觉别人都是“能用现成的就别重复造轮子”但在这个项目里我特意把秘密共享和 Beaver Triple 手工实现了一遍。原因是协作推理的保密性最终要落在协议每个步骤的正确性上如果第一步就不理解碎片是怎么构造的后面 debug 时你会彻底束手无策。从零实现一个最小秘密共享其实代码量很小一个素数域运算一个 share 函数一个 reconstruct 函数再加一个 beaver triple 生成器加起来不超过 200 行。这个量级的代码自己维护完全可控而且能帮你建立对协议细节的直觉。等到真上了复杂的网络结构再考虑引入mpz这类库也不迟。3.3 动手前先把 Rust 开发环境补齐如果你刚接触 Rust下面这几件事建议先做好。首先是安装 rustup装好之后默认会带 cargo 和 rustc。Cargo 拉取依赖量的速度直接决定体验推荐在配置文件~/.cargo/config.toml里把 crates-io 源指向一个访问速度更快的镜像站常见的有中科大、清华、字节跳动等公开镜像服务。注意这只是在解决依赖下载速度问题跟网络代理完全是两码事。编辑器方面我用的是 VSCode rust-analyzer 插件rustfmt 和 clippy 也装上。rust-analyzer 的补全和诊断对写协议逻辑帮助很大。要是你长期在离线内网开发可以预先在有网环境里把依赖拉进~/.cargo/registry缓存目录再把整个目录拷到目标机器配合cargo build --offline使用整套流程能省非常多时间。4. 从零到一一个两方隐私推理的最小可运行 Demo4.1 场景定义与数学约定为了把原理讲透这个 Demo 我设定得非常保守两方参与一方持有模型权重 W 和偏置 b另一方持有输入特征 x。目标是计算一个 2 维线性回归 y W1x1 W2x2 b最终只有持有输入的那一方能拿到 y。为了让浮点数能落在有限域里所有明文小数先统一转成定点整数乘上一个固定的缩放因子 SCALE比如 10000四舍五入取整。这样 0.1234 就变成 1234。推理结束后把结果除以 SCALE 再还原成小数。这个缩放方法虽然简单但足够说明原理真实项目里你还得考虑溢出和精度损失第五节会细说。素域我选了PRIME 1_000_000_007一个经典的大质数足够教学演示也能兼容 32 位无符号整数的加减乘。所有运算都用取模后的值表示。4.2 秘密共享层实现先定义最基础的域运算函数。注意 Rust 的%运算符对负数会得到负余数所以统一用rem_euclid把结果归到非负区间const PRIME: i128 1_000_000_007; fn field_norm(x: i128) - u64 { x.rem_euclid(PRIME) as u64 } fn field_add(a: u64, b: u64) - u64 { field_norm(a as i128 b as i128) } fn field_mul(a: u64, b: u64) - u64 { ((a as u128 * b as u128) % (PRIME as u128)) as u64 } fn field_sub(a: u64, b: u64) - u64 { field_norm(a as i128 - b as i128) }秘密共享拆分函数如下。传入明文 x返回两片碎片 x0 和 x1满足 x0 x1 x mod PRIME。调用方保留 x0把 x1 发给对手方// 实际项目请使用 OsRng 等 CSPRNG 生成随机碎片 fn share(x: u64) - (u64, u64) { let x0 rand::random::u64() % (PRIME as u64); let x1 field_sub(x, x0); (x0, x1) } fn reconstruct(x0: u64, x1: u64) - u64 { field_add(x0, x1) }4.3 Beaver Triple 与安全乘法Beaver Triple 的离线生成阶段可以由一个可信第三方生成也可以通过两方之间的不经意传输协议来生成。教学 Demo 我就简化成由一方本地生成再秘密共享给双方struct Triple { a0: u64, a1: u64, b0: u64, b1: u64, c0: u64, c1: u64, } // 生成一个共享三元组满足 c0 c1 (a0 a1) * (b0 b1) fn generate_triple() - Triple { let a rand::random::u64() % (PRIME as u64); let b rand::random::u64() % (PRIME as u64); let c field_mul(a, b); let (a0, a1) share(a); let (b0, b1) share(b); let (c0, c1) share(c); Triple { a0, a1, b0, b1, c0, c1 } }双方拿到自己那份 triple 之后开始在线计算。下面以 P0 视角为例。P0 本地持有 x0、w0 和 triple 的 a0、b0、c0// P0 本地计算 e0 和 d0 let e0 field_sub(x0, triple.a0); let d0 field_sub(w0, triple.b0); // 双方交换 e 和 d 后P0 拿到对方发来的 e1、d1 let e field_add(e0, e1); let d field_add(d0, d1); // P0 计算出自己那一份乘法结果碎片 let z0 field_add( field_add(field_mul(e, d), field_mul(e, triple.b0)), field_add(field_mul(d, triple.a0), triple.c0), );P1 执行完全对称的计算得到 z1。最后两人合并 z0 z1就得到 W*x 的结果。这套逻辑展开后是恒等式如果你在本地用明文跑一遍会发现重建结果和直接乘法完全一致。4.4 线性层与激活函数的秘密共享有了安全乘法矩阵乘法的秘密共享实现就顺理成章了。每个乘积项调一次安全乘法然后把同一输出节点的所有乘积和和 bias 在本地碎片上相加// 两输入线性层y w1*x1 w2*x2 b // 返回当前参与方的 y 碎片 fn linear_layer_share( x1_share: u64, x2_share: u64, w1_share: u64, w2_share: u64, b_share: u64, // 若干 triple 与对端交换的消息省略 ) - u64 { let p1 secure_mul_share(x1_share, w1_share, /* triple ... */); let p2 secure_mul_share(x2_share, w2_share, /* triple ... */); field_add(field_add(p1, p2), b_share) }如果网络有两个全连接层中间需要非线性激活。工程经验是不要硬碰 ReLU而是用多项式近似。以平方函数为例秘密共享下平方层的实现很简单本地调用一次安全乘法输入是同一份碎片输出就是平方后的碎片。真实场景的近似激活还会加上一次项和常数项逻辑一样只是多几个乘加操作。4.5 明文对照与通信量观察整个 Demo 跑通后第一件事一定是做明文对照先明文计算 y w1x1 w2x2 b再走一遍秘密共享协议看重建结果是否一像素不差。这一步能筛掉 90% 的低级错误。我实测下来一个 2 维线性层整个协议只需要两轮通信一轮交换 Beaver 所需的 e/d一轮收集最后一方的碎片用于重建结果。如果你把多个乘法并行化打包整个网络可能只需要几轮网络 RTT 就完成推理。这一点非常重要因为很多时候 MPC 推理的延迟瓶颈根本不是计算而是每一轮网络通信的往返时延。5. 工程化逃不掉的坑性能评估、定点数与安全验证5.1 通信是 MPC 的头号瓶颈怎么优化乘法轮数我见过不少初学 MPC 的人上来就关心 CPU 能不能跑得动大模型结果把通信这个最核心的瓶颈忽略了。秘密共享加法和本地计算的成本确实很低但每做一次乘法双方就要交换 e 和 d这至少涉及一轮网络 RTT。如果你的模型有 1000 个乘法节点且这些节点有强依赖关系不能并行那单次推理就需要 1000 轮通信在异地机房之间这延迟就是灾难。优化的核心思路是减少乘法依赖深度以及把互不相关的乘法合并到同一轮通信里。你用协议前先画一张算子依赖图同一层的所有乘法节点互不依赖可以共享同一轮 e/d 交换不同层之间有依赖就必须等上一层结果出来才能算下一层。所以网络层数越深RTT 代价越高。分页批处理也能变相降低单样本的通信成本把 batch 内所有样本的乘法并行起来通信轮次不变通信带宽变大。5.2 定点数精度缩放因子与溢出陷阱把浮点数搬进有限域最大的坑就是定点数的精度和溢出。缩放因子选大了中间结果乘加后很容易溢出素数域选小了精度损失惨不忍睹。我的经验是先按模型中间激活的最大值估算一个安全范围再选择缩放因子。比如输入和权重都不超过 1.0三层全连接中间激活最多到几百那 SCALE10000 基本够用如果你用平方激活数值可能快速膨胀要么缩减模型规模要么用更大的素数域比如184467440726993756172^64 附近的安全质数把动态范围撑大。另一个隐蔽问题是负数表示。在有限域里负数是用模 PRIME 之后的“补数”表示的。比如 -1 在模 1000000007 下是 1000000006。如果你在后续计算里直接当成正常正数处理没有在域运算体系内统一走一遍结果必定错乱。所以我建议不要把域内数值转成 i32 做判断所有加减乘都保持在域函数内部最后一步再做还原。5.3 绝对不要自己发明随机数随机数复用是最大隐患MPC 协议的安全性几乎完全建立在随机数的不可预测性和“一次性”使用上。最典型的翻车案例是两方各自用同一个随机数生成器、同一个种子去生成碎片导致碎片之间出现线性相关性攻击者可以从一段相关性逆推出完整秘密。另一个高频错误是在一个进程里用循环批量生成 triple误把同一个随机流复用给多个乘法节点这会导致 Beaver Triple 的 mask 关系被攻破。真实项目里一定要用密码学安全的随机数来源Rust 生态下直接依赖getrandom或rand::rngs::OsRng顺手用ChaCha20Rng这类标准 CSPRNG 作为批量生成时的种子流。任何“为了可复现调试”而在生产代码里写死随机种子的行为在安全协议里都是致命的。5.4 三步自查法明文对照、协议日志审计、恶意输入测试最后分享一套我沉淀下来的验证流程适合所有 MPC 工程第一步明文对照。先在明文域把整个网络跑一遍记录每层中间值。再走秘密共享协议比对重建结果。任何不一致都说明协议实现有 bug这时候不要急着加功能先把协议算对。第二步协议日志审计。在每轮通信出口加日志记录发送方的值是否属于正确的域元素、是否真的满足 e x - a 的关系。这能帮你快速定位是碎片构造错误、Beaver triple 共享错误还是通信序错误。日志里不要直接打印明文秘密只打印“被 mask 后的公开值”。第三步恶意输入测试。模拟一个攻击者故意发送异常碎片比如越界的域值、或者不符合协议格式的数值。一个成熟的实现应该要么在协议层拒绝要么让最终结果仍能通过校验。这一步通过你的工程才敢从 Demo 走向试生产。我实际跑这个项目时这三步大概用了不到一周但协议从“能跑”到“敢部署”的差距恰恰就在这种细节验证里。如果你正准备踩进 Rust MPC 协作推理这个坑我建议你把上面的最小 Demo 先亲手敲一遍跑通后再去研究那些体积更大、安全性更强的框架会比直接上手框架少走很多弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价