简介:面向毕业设计与期末大作业场景的基于同态加密的联邦学习安全聚合系统源码,定位为可直接运行与二次学习的完整项目,适合具备基础Python知识的本科生完成课程设计、期末大作业或毕业设计参考。整个压缩包共55个文件,仅2.08MB,核心包括9个Python源文件、31个模型相关文件、4个Windows启动脚本,以及XML配置、Git忽略规则、证书密钥与说明文档等,便于快速部署和按模块理解隐私聚合流程。项目代码注释完整,新手也能看懂训练与安全聚合的关键环节,系统功能完善、界面简洁,配合启动脚本可一键运行客户端与服务端。通过源码、模型文件和说明文档,读者可以掌握同态加密与联邦学习结合的实现思路,并直接用于课程答辩或高分大作业展示。目前已有97人浏览学习,适合需要完整可运行项目作为参考的在校学生。
1. 用同态加密包住联邦学习安全聚合:先解决明文梯度可见问题
聚合服务器手里只有一串密文,却仍然能算出所有参与方的平均模型——这是同态加密在联邦学习里最常见的价值描述。真正落到系统源码层面,问题会变成:私钥放谁手里、梯度怎么编码、聚合节点拿什么保证收到的密文来自有效参与方。许多高分项目把安全聚合做成一次密文求和,演示效果不错,但离可运行的系统还差密钥管理和精度还原。下面按理论选型、协议设计、工程实现、验证调优这条链路,把基于同态加密的联邦学习安全聚合系统源码应该具备的模块、参数和调试方法讲清楚。适合把安全聚合做成毕设项目或正式系统的工程师,也适合想确认 Paillier 和 CKKS 到底该选谁的从业者。
2. 同态加密在联邦学习安全聚合里的选型:Paillier 与 CKKS 怎么取舍
2.1 联邦平均只做加减法,为什么明文上传仍是不够的
典型联邦学习系统里,参与方把本地训练的梯度上传给中心节点,中心节点执行 FedAvg:把同一位置的参数相加,再除以参与方数量。整个流程只有加法和常数乘。这个特征让同态加密的接入点非常明确:算法本身不需要在密文里做非线性变换,加法同态就足够覆盖。
常见误区是把“数据不出本地”当成隐私保护的全部。明文梯度本身就是一种数据泄漏形式:攻击者拿到梯度向量后,可以重建出近似的原始训练样本;在 NLP 场景里,也能通过梯度反推语料中的 token 分布。所以安全聚合要解决的是“聚合节点也看不见中间明文”的问题,不是传输加密。联邦学习综述里讨论系统安全时,安全聚合通常和差分隐私分开列,原因就在这里:一个保护传输链路,一个保护计算过程。
差分隐私是另一个常被拿来对比的思路:给梯度加独立噪声后上传。它能在输出上抵消单条样本的影响,但聚合节点仍然能看到加了噪声的明文梯度。噪声量与数据敏感度强相关,参与方数量小时,精度损失很快让模型无法收敛。既然 FedAvg 的运算很简单,直接用加法同态加密处理更干净:参与方上传的是密文,服务器拿着密文做平均,从头到尾接触不到明文。
2.2 Paillier 与 CKKS 的参数化对比:精度、噪声与密文膨胀
Paillier 是加法同态加密:密文域上的乘法对应明文域上的加法,密文上做幂运算对应明文与常数的乘法。对于 FedAvg,把参与方密文逐个相乘,再乘一个参与方数量的模逆,就完成了“求平均”。CKKS 则支持浮点逼近运算下的加法和乘法,可以做更深度的密文计算,代价是必须维护噪声预算:每做一次乘法,密文里的噪声就涨一截,层级用完就得刷新密钥,参数集因此很敏感。
选型不能只看“支持乘法”这一条,要连精度、膨胀倍数、实现成本一起看。
| 维度 | Paillier | CKKS |
|---|---|---|
| 支持运算 | 加法、标量乘 | 加法、乘法、逼近运算 |
| 精度 | 精确保密整数运算,不引入舍入误差 | 浮点逼近,有舍入误差 |
| 密文膨胀倍数 | 约 2 到 4 倍 | 通常 10 倍起步 |
| 噪声管理 | 加法几乎不消耗噪声 | 每次乘法消耗噪声预算 |
| 实现成本 | 低,适合快速落地 | 高,参数集与缩放因子都要调 |
| FedAvg 匹配度 | 刚好匹配 | 可扩展,但性能代价大 |
如果聚合逻辑只有均值、加权均值,Paillier 是更扎实的选择。CKKS 适合后续要做密文上的动量聚合或更复杂的一阶优化计算,但在系统源码里,CKKS 的 scale、level、明文模数这些参数会让调试难度明显上升。我一般会先列清聚合算法里出现的运算,再决定方案。下面这个判断函数可以快速收敛选型:
def pick_scheme(operations: set[str]) -> str: """根据聚合运算集合选择同态加密方案""" needs_full_mul = "mul" in operations if not needs_full_mul: return "paillier" # 只做加法和标量乘:直接上 Paillier if needs_full_mul and len(operations) <= 2: return "bgv_bfv" # 少量乘法:考虑整数的 BGV/BFV return "ckks" # 多层乘法或浮点逼近:CKKSoperations 里的标签来自聚合协议定义,FedAvg 对应的集合是 {"add", "scalar_mul"}。scalar_mul 在 Paillier 里用模幂实现,不需要引入乘法同态,所以函数里只检测 "mul" 这种真正需要密文乘密文的操作。选型时注意区分这两点,否则会为了一个用不上的特性付出 10 倍的密文膨胀代价。
2.3 偏置压缩与同态加密叠加:通信优化不是隐私保护
联邦学习里通信往往是最大的工程瓶颈,常见做法是在上报前对本地更新做量化、稀疏化或偏置压缩。这类方法在联邦学习中采用偏置压缩技术,通过传输经过压缩的本地更新数据来减少通信开销。关键点在于:压缩是在明文侧完成的,完成后再进入编码和加密流程,两件事可以叠加,不会互相抵消。
很多实现把通信压缩和隐私保护混在一个环节里处理,结果往往都不理想。压缩减少的是传输字节数,同态加密关心的是计算发生在什么域里。正确顺序是:训练 → 压缩/量化 → 安全编码 → 加密 → 上传 → 服务端密文聚合 → 解密还原。如果跳过压缩直接加密,密文体积会比明文大几倍,网络带宽压力反而更高;反过来,如果以为压缩自带隐私保护,就没有必要再引入同态加密,这会在安全评审时被打回。
3. 基于同态加密的安全聚合协议设计:密钥归属、掩码与明文编码
3.1 私钥归属决定安全边界
先回答系统源码里最关键的设计问题:私钥能不能放在中心服务器上?不能。如果中心服务器持有私钥,它收到密文后可以先解密再看,安全聚合就回落到传输加密。协议要保证聚合节点在计算过程中看不见明文,所以默认结构是“参与方各自拿着自己的私钥,公钥公开”,或者由一个独立的协调方保管主私钥,并且只解密最终聚合结果。
更完整的实现是引入门限解密:把私钥分片分给 N 个参与方,任何 t 个参与方联合起来才能解密一段密文。安全性从“中心服务器可信”提升到“多数参与方可信”,但复杂度也上去了,需要 Shamir 秘密共享和针对 Paillier 的分布式解密协议。毕设或企业内部原型,通常先做“私钥在客户端、服务端只能使用公钥”的结构,把“最终模型要让所有参与方都拿到”的问题放到下一版解决。
实现上的细节是给每个参与方发一份数字证书,系统参数文件(公钥 n、生成元 g、精度参数)下发到客户端。服务器只存公钥和参与方注册信息,聚合过程调用加密库的密文加法。密钥更新周期挂到训练轮次上,每轮训练结束做一次公钥轮换,避免长期使用同一把密钥增加泄漏面。
3.2 密文随机化与掩码:阻止聚合节点反推明文
Paillier 是随机化加密:同一明文在同一公钥下每次加密得到的密文都不同。这让服务器没法通过比对两次密文判断梯度是否变化。但随机化不改变明文值的分布:如果某参与方的梯度只在很小的整数集合里取值,比如 0 和 1,服务器可以自己加密候选明文再比对密文,穷举出真实值。安全聚合里常见的一套防御是掩码。
掩码在所有参与方之间协商生成,满足“所有参与方掩码相加等于 0”。服务端把密文加起来之后,掩码在明文层自动抵消,不影响聚合结果。加入掩码后,服务器即便知道梯度取值范围,也无法把某条密文和具体明文对应起来。这就是安全聚合里最核心的“不可区分性”来源。
协商方式有两种。一种是训练开始前各参与方两两共享随机种子,互相生成掩码的加法项和减法项;另一种是设一个掩码协调方,给每个参与方发放随机向量并保证向量和为零。第一种去中心化程度高,但实现复杂;第二种实现简单,代价是协调方要被额外信任。下面是参与方单侧的最小流程:
def upload_gradient(client_id, gradient, mask, public_key, server_url): scale = 1e5 # 从系统参数下发,这里直接给出常见取值 # 1. 明文梯度量化到整数域,scale 控制小数位 q = np.rint(gradient * scale).astype(np.int64) # 2. 叠加掩码,掩码向量全量求和为 0 masked = q + mask # 3. 逐参数加密,这里直接取 Python int 更保险 ciphertexts = [public_key.encrypt(int(v)) for v in masked] # 4. 上传密文,round_id 由服务端先下发 requests.post(f"{server_url}/api/v1/gradients/upload", json={"round": round_id, "payload": dump(ciphertexts)})逻辑说明:量化让浮点梯度能映射到 Paillier 的整数明文空间;掩码加在量化后的整数明文上,加密之后才进入网络,所以服务器看到的只有“明文+掩码”的密文。参数说明:scale 取值 1e5 或 1e6 保留梯度 5 到 6 位小数;掩码值域必须计入明文空间预算,否则量化值和掩码相加超过模数 n,解密取模会直接把结果截错。
提示:服务端不能存储掩码向量,掩码只在参与方之间传递;把掩码放进数据库就等于削弱了不可区分性。
3.3 梯度编码的三个必调参数:scale、bit_len 与 clamp
Paillier 明文空间是模 n 的整数环,而梯度是浮点和负数混合。先做浮点转定点:每个参数乘以 scale 后四舍五入取整,得 int64;再处理负数:解密结果落在 [0, n-1],负整数要映射成 n-|v|,解密后如果值大于 n/2 就减 n 还原。如果系统里叠加了掩码,负数还原要放在掩码抵消之后统一做。
精度预算的估算方法是:假设 K 个参与方,每方量化后绝对值上限为 B,掩码绝对值上限为 M,密文求和后的最大绝对值就是 K*(B+M)。明文模数 n 的位数由密钥长度决定,一般取 2048 位,n 约 2.5e616,容量足够。需要注意的问题反而是小参数:scale 太小丢精度,太大则量化值提前接近模数上限,中间还夹着一个 clamp 要防止极端梯度撑爆明文空间。常用参数如下:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| scale | 1e5 或 1e6 | 浮点转定点的放大倍数,决定可保留的小数位数 |
| bit_len | 2048 | 公钥模数位数,决定明文容量上限 |
| batch_size | 4096 | 每批加密的参数个数,控制请求体和 CPU 占用 |
| clamp | 10 * std | 截断超过该范围的梯度,防明文溢出 |
batch_size 主要是工程参数:Paillier 加密是模幂运算,单次耗时要几毫秒甚至几十毫秒,一次性加密整份大模型参数会让客户端长时间无响应,按 4096 一批处理并逐个上传,前端能感知到进度。clamp 的 10 * std 以当前批次梯度的标准差为阈值,截断极端值会引入轻微偏差,但能换来说明文空间不会被打穿,这个取舍在工程上是划算的。
4. 把协议实现成可运行的系统源码:接口、聚合服务与数据表
4.1 模块规划与 HTTP 接口
把安全聚合变成系统源码,重点是把密码协议包进可测试的工程结构而不是写一个演示脚本。常见做法是拆成四个模块:client 负责训练、量化、加掩码、加密与上传;server 负责参与方注册、密文收集、密文聚合与任务状态管理;crypto 封装 Paillier 运算和密钥管理;storage 存放中间结果。通信协议用 HTTP+JSON 作默认,吞吐吃紧时再切 gRPC。原因很简单:这系统的瓶颈是同态加密运算本身,协议层的收益有限,不值得前期就引入额外依赖。
接口表如下,全部挂在统一的 /api/v1 前缀下:
| 端点 | 方法 | 请求体 | 行为 |
|---|---|---|---|
| /participants/register | POST | {name, pubkey} | 注册参与方,返回 participant_id |
| /gradients/upload | POST | {round_id, payload} | 接收本轮密文片段 |
| /aggregate/start | POST | {round_id} | 触发本轮聚合,服务端执行密文求和 |
| /aggregate/result | GET | round_id 查询参数 | 返回聚合结果与轮次元数据 |
接口必须显式带 round_id,否则上一轮的迟到包会混进新一轮聚合。服务端收到 upload 请求时只把 payload 写入存储,等 start 触发时才做密码学运算,这样上传和聚合在时间上解耦。
4.2 客户端加密上传的 Python 实现
这里以 Phe 库作为 Paillier 实现示例,给出客户端最核心的一段:
import requests import numpy as np from phe import paillier def build_ciphertexts(gradient, mask, pubkey, scale=1e5): # 转定点,先截断再量化,避免 float 累加误差 q = np.clip(np.rint(gradient * scale), -1e10, 1e10).astype(np.int64) masked = q + mask # encrypt 接收 Python int,numpy int 在某些绑定里会类型报错 return [pubkey.encrypt(int(v)) for v in masked] def upload(client_id, round_id, grads, mask, server_url, batch_size=4096): ciphertexts = build_ciphertexts(grads, mask, pubkey) for start in range(0, len(ciphertexts), batch_size): chunk = ciphertexts[start:start + batch_size] payload = [{"c": str(item.ciphertext()), "e": item.exponent} for item in chunk] requests.post(f"{server_url}/api/v1/gradients/upload", json={"round_id": round_id, "payload": payload}, timeout=30)逻辑说明:build_ciphertexts 把 numpy 数组换算成整数明文并叠加掩码,逐参数调用公钥加密;upload 按 batch 切片上传,让单请求体积受限。参数说明:scale 直接决定可恢复的小数位,后续解密要除以同一个 scale 才能还原成 float;ciphertext() 和 exponent 是 Paillier 密文对象的两个核心字段,只传这两个字段可以避免把对象序列化进去,这也是密文体积突然膨胀的最常见原因。
4.3 服务端聚合路由:密文相加怎么处理掉线
服务端只需要处理一件事:拿到本轮所有参与方的密文列表,在密文域上求和。Paillier 下密文乘法等价于明文加法,所以聚合就是逐位置相乘,最后乘一个参与方数量的模逆。模逆在注册阶段就能算好:K 个参与方时,n_inv 满足 n_inv*K ≡ 1 (mod n),它只与 K 和模数 n 有关。
from phe import paillier def aggregate_payloads(payloads, pubkey, n_inv): """payloads: 按参与方组织的密文列表,逐位相乘求和""" agg = None for client_entries in payloads: for idx, item in enumerate(client_entries): enc = paillier.EncryptedNumber(pubkey, item["c"], item["e"]) agg = enc if agg is None else agg + enc # 乘模逆等价于除参与方数量,Paillier 下用整数的模乘实现 return agg * n_inv逻辑说明:agg 累加的是“密文加法”,Paillier 的 EncryptedNumber 重载了加号,底层把两个密文相乘;最后的 agg * n_inv 是密文乘常数,对应明文平均。参数说明:n_inv 由服务端根据本轮参与方注册列表预计算,掉线参与方必须在聚合前剔除。剔除之后掩码关系会被打破,所以生产环境里要配合“掉线参与方把自己的掩码贡献也公开出来”的恢复流程,这是安全聚合从演示走向工程的一道分水岭。
4.4 数据库表设计与结果交付
服务端需要持久化参与方、轮次和密文三块数据。简单表结构如下:
create table participants ( participant_id text primary key, public_key text not null, status text not null default 'active', created_at timestamptz not null default now() ); create table aggregation_round ( round_id integer primary key, participant_cnt integer not null, started_at timestamptz not null default now(), finished_at timestamptz ); create table gradient_upload ( round_id integer not null, participant text not null, payload bytea not null, uploaded_at timestamptz not null default now(), primary key (round_id, participant) );复合主键 (round_id, participant) 保证同一参与方不会往同一轮重复上传。payload 存序列化后的密文。mask 数据不允许落到服务端表里,它只在参与方之间传递,这是协议安全的基本要求。结果交付走 /aggregate/result:服务端自行判断本轮是否具备解密条件,如果条件是门限解密,则把聚合密文广播给参与方,由参与方协作解密。
5. 正确性验证、性能观察与惰性聚合调优
5.1 端到端对齐:密文结果直接对标明文 FedAvg
同态加密系统最怕的不是慢,而是“错得神不知鬼不觉”。第一步就是端到端对齐:同一份数据,先按明文做一次 FedAvg,再走一遍完整的量化、加密、聚合、解密,两个结果必须落在同一精度范围内。
def e2e_check(K, dim, bit_len=2048, scale=1e5): pub, priv = paillier.generate_paillier_keypair(n_length=bit_len) grads = [np.random.randn(dim) for _ in range(K)] # 明文基线 expected = np.mean(grads, axis=0) # 密文链路 mask = np.zeros(dim, dtype=np.int64) # 单机调试先用零掩码 enc_list = [pub.encrypt(int(np.rint(g * scale))) for g in grads] n_inv = pow(K, -1, pub.n) agg = sum(enc_list) * n_inv result = np.array([pub.decrypt(ct) for ct in agg.ciphertexts()]) / scale assert np.linalg.norm(result - expected) < 1e-3逻辑说明:expected 是明文平均,result 是从密文聚合解密还原的平均,二者做二范数比较。参数说明:单机调试阶段掩码置零,上线前要换真掩码再复跑一遍;scale 越大断言阈值可以设得越紧,但也越容易暴露出量化精度问题。
5.2 加密是随机的,这个测试值得写进回归
第二类验证是随机性验证:同一明文加密两次,密文必须不同。把两组密文的 c 字段拿出来比对即可。如果两次生成的密文完全一样,说明随机源的熵没有被注入加密过程,这时候聚合系统仍然可用,但安全性退化为确定性加密,服务器可以通过字典攻击还原小值域明文。这条测试成本极低,但经常被忽略。
5.3 通信量与聚合耗时:三个值得记录的数字
性能压测抓三个数:密文膨胀倍数、单次加密耗时、聚合阶段 CPU 峰值。膨胀倍数用户往往最敏感——同样的梯度,明文 100 MB,加密后变 300 MB,传输耗时直接和带宽挂钩。Paillier 的密文膨胀通常在 2 到 4 倍,如果看到 10 倍以上,先怀疑序列化方式。聚合耗时则与服务端架构强相关:密文求和是逐位置模乘,参与方多的时候 CPU 峰值很高,一个有效的调优是惰性聚合。
惰性聚合的思路很简单:把 upload 接口做成“只落盘,不算数”,等 /aggregate/start 触发时再把所有密文一次性取出来算。上传阶段的请求处理速度不再被子密码学运算拖慢,聚合阶段的 CPU 峰值也被集中到一次批量任务里。这个改动不到 20 行代码,但能把聚合接口的“等待感”降下来:上传阶段不再被加密运算拖住,密码学耗时被集中到后台批量任务里。
把明文对齐、加密随机性、膨胀倍数这三个检查固定进 CI,之后改密钥长度、改 scale、甚至把 Paillier 换成 CKKS 时,都有回归测试兜底。最后还要补一个边界用例:只有一个参与方上传时,聚合结果应当等于该参与方的明文梯度;这能验证掩码抵消逻辑在最小参与方集合上依然正确。
本文还有配套的精品资源,点击获取