基于秘密共享OPRF的模糊隐私集合求交(Fuzzy PSI)协议设计与实现

发布时间:2026/6/21 3:22:07
基于秘密共享OPRF的模糊隐私集合求交(Fuzzy PSI)协议设计与实现 1. 项目概述当“模糊匹配”遇上“隐私计算”最近在做一个挺有意思的隐私计算项目核心目标是在两个互不信任的参与方之间安全地计算他们各自持有的数据集合的交集而且这个交集还不是精确匹配是“模糊”的。比如A公司有一批用户手机号B公司有一批用户邮箱他们想找出共同的用户但手机号和邮箱之间没有直接的对应关系可能只有部分信息如用户名相似。这听起来像是数据合作中的刚需对吧但难点在于双方都不想把自己的原始数据泄露给对方这就是“隐私集合求交”PSI要解决的问题。而“模糊”的要求让问题变得更加棘手。传统的PSI协议要求双方数据的标识符完全一致比如都是经过哈希处理的手机号。但在现实业务中数据往往存在格式不一致、拼写错误、别名等问题。这时候“模糊隐私集合求交”Fuzzy PSI就派上用场了。我们这个项目就是设计并实现一个基于秘密共享OPRF的高效Fuzzy PSI协议。简单来说OPRF不经意伪随机函数就像一个“盲盒”加工器能把你的数据加密成一个别人看不懂的乱码但还能保证相同的输入得到相同的乱码输出。秘密共享则把密钥拆成几份分散持有需要多方合作才能完成计算进一步提升了安全性。把这两者结合起来我们就能在保护数据隐私的前提下高效地完成模糊匹配。这个协议适合谁呢如果你是在金融风控、医疗研究、广告营销等领域的数据工程师或隐私计算研究员面临跨机构数据合作但又受制于隐私法规和数据安全那这个设计和实现过程应该能给你带来不少启发。它不只是一个理论玩具我们最终把它跑通了并且对性能做了深度优化。接下来我就把这个项目的设计思路、核心实现细节、踩过的坑以及优化心得毫无保留地分享出来。2. 协议核心设计思路拆解2.1 为什么是“秘密共享”“OPRF”在设计之初我们评估了几种主流的PSI技术路径。基于公钥加密的如RSA盲签名方案通信开销大基于哈希的简单协议安全性不足。OPRF因其单向性和不经意性成为平衡安全与效率的优选。但传统的OPRF方案通常需要一个可信的中心方持有密钥或者采用两方计算将密钥完全交给其中一方这仍然存在单点信任或密钥泄露风险。秘密共享的引入正是为了消除这个“可信中心”。我们将OPRF所需的密钥k通过Shamir秘密共享方案拆分成多个份额分发给多个计算参与方在我们的两方场景中可以模拟为将密钥份额分给双方或者引入一个无状态的第三方协助者。任何一方都无法独自恢复出完整的密钥k从而无法独自构造OPRF函数或解密他人的中间结果。只有当双方或所有必要方协作时才能完成OPRF计算。这实现了“分布式信任”将安全假设从“信任某个中心节点”降低为“不共谋”更符合去中心化数据协作的场景。模糊匹配的需求则通过将数据转换为可比较的“模糊表示”来实现。我们不是直接比对原始字符串而是先提取特征比如将字符串转换为一组n-gram字符片段集合或使用MinHash算法生成签名。这些特征集合或签名就成为了OPRF的输入。即使原始数据有细微差别它们的特征集合也会有大量重叠从而在模糊比对阶段被识别出来。因此整体设计思路是参与方先各自对数据进行模糊特征提取然后通过一个基于秘密共享的OPRF协议将这些特征加密/转换为一个共同的、可比较的伪随机空间最后在密文或转换后的域内进行交集计算。整个过程中任何一方都看不到对方的原始数据甚至原始特征。2.2 整体协议流程架构我们的协议主要包含两个参与方客户端Client持有集合X和服务端Server持有集合Y。为了演示秘密共享我们假设OPRF的密钥k被拆分为k1和k2分别由Client和Server持有。协议流程分为离线阶段和在线阶段这是提升效率的常见技巧。离线阶段这个阶段与双方的具体数据无关主要用于准备一些“原材料”。密钥份额生成与分发一个可信的初始化过程或通过安全多方计算生成产生OPRF密钥k并生成份额(k1, k2)分别安全地发送给Client和Server。此后k被销毁。基础材料预计算双方可以预先计算一些依赖于密钥份额但独立于输入数据的值例如在某些椭圆曲线实现中预计算标量乘法的基点倍点表以加速在线计算。在线阶段这是处理实际数据的阶段。数据预处理与模糊化Client和Server分别对自己的原始数据集合进行模糊特征提取。例如对于字符串“hello”提取2-gram特征集合{“he”, “el”, “ll”, “lo”}。每个特征作为一个独立的元素参与后续PSI。Client发起OPRF请求Client将自己的每个模糊特征x与一个随机盲化因子r结合例如计算H(x)^r其中H是哈希到椭圆曲线群的函数将盲化后的值blinded H(x)^r发送给Server。Client本地保存每个x对应的r。Server协作计算使用份额k2Server收到blinded后用自己的密钥份额k2对其进行部分计算例如计算partial_eval blinded^k2然后将结果返回给Client。关键点Server在此过程中看不到x也无法推断出最终结果因为它没有r和k1。Client完成OPRF计算使用份额k1Client收到partial_eval后首先用自己的密钥份额k1进行计算intermediate partial_eval^k1。然后利用之前存储的盲化因子r进行去盲化。由于(H(x)^r)^(k1 * k2) H(x)^(r * k1 * k2)去盲化需要计算final_oprf intermediate^(r^{-1})这里r^{-1}是r在模群阶数下的乘法逆元。最终得到F(k, x) H(x)^k的某种形式具体取决于群运算。这是x的OPRF值。Server准备自己的OPRF值与此同时Server可以直接对自己的每个模糊特征y计算F(k, y)吗不能因为k是分散的。因此Server需要与Client进行一轮类似的交互角色对调或者采用一种更高效的“单向”OPRF变种。在我们的设计中为了对称和简化我们让Server也作为客户端向Client发起一次请求流程同上。最终双方各自得到了自己集合元素的OPRF值{F(k, x)}和{F(k, y)}。模糊交集计算现在Client和Server各自拥有了一组OPRF值。这些值是伪随机的但相同的原始特征会产生相同的OPRF值。双方将这些OPRF值或它们的哈希进行本地排序。然后可以通过一种隐私集合求交协议例如基于排序列表的简单PSI来找出共同的OPRF值。由于OPRF值的相同性反映了原始模糊特征的相同性因此对应的原始数据可能多个特征对应一个原始数据就被认为是模糊匹配的。最终双方可以得知交集的大小或者在特定协议下Client可以得知哪些自己的数据匹配上了。这个架构将复杂的密码学操作封装在OPRF环节模糊处理在数据预处理环节使得协议模块清晰并且能够利用秘密共享来增强密钥安全性。3. 核心模块实现与关键技术细节3.1 模糊特征提取模块实现模糊匹配的效果很大程度上取决于特征提取。我们实现了两种常见的策略并提供了可配置的接口。N-gram特征提取这是处理字符串模糊匹配最直观的方法。我们将每个字符串转换为固定滑动窗口得到的子串集合。def generate_ngrams(text, n2): 生成字符串的n-gram集合 if len(text) n: return {text} # 或填充处理 return {text[i:in] for i in range(len(text) - n 1)}关键参数选择n的大小n越小对拼写错误的容忍度越高“hello”和“hallo”的2-gram交集较多但也会增加误匹配率和计算/通信开销特征集合变大。我们经过测试对于人名、产品名等短文本n2或3是常用选择。标准化处理在生成n-gram前必须对字符串进行统一标准化如转为小写、去除空格和标点。否则“Hello”和“hello”会被当作完全不同的词。特征编码生成的n-gram字符串需要被编码为OPRF函数可以接受的输入例如一个字节串。我们使用统一的UTF-8编码后直接作为哈希函数的输入。MinHash签名当需要处理更长的文本如文档摘要或对集合相似度有更理论化的要求如Jaccard相似度时我们采用MinHash。它将一个特征集合可以是n-gram集合压缩成固定长度的签名数组两个签名的Jaccard相似度可以通过其对应位置相同值的概率来估计。import mmh3 # 使用MurmurHash3这种非密码学哈希因为这里不需要抗碰撞攻击 import numpy as np class MinHashGenerator: def __init__(self, num_perm128): self.num_perm num_perm # 签名长度 # 生成num_perm个不同的哈希函数通过不同的种子模拟 self.seeds np.arange(num_perm) def signature(self, feature_set): 将特征集合转换为MinHash签名向量 sig np.full(self.num_perm, np.inf) for feature in feature_set: hash_values np.array([mmh3.hash(feature, seeds) for s in self.seeds]) sig np.minimum(sig, hash_values) return sig实现要点签名长度num_perm越长对相似度的估计越精确但通信和计算成本也线性增加。通常128或256是一个平衡点。哈希函数选择MinHash对哈希函数的要求是均匀随机不要求密码学安全因此使用MurmurHash3、CityHash等快速哈希函数能极大提升性能。OPRF输入整个MinHash签名向量一个整数数组不能直接作为OPRF输入。我们的做法是将签名向量的每个元素或每几个元素打包单独进行OPRF处理。这意味着一个原始数据项会对应多个OPRF元素后续交集判定需要根据匹配的OPRF元素数量是否超过阈值来判断是否为模糊匹配。注意模糊特征提取会显著扩大数据量。一个字符串可能变成几十个n-gram或一个128维的签名。这是Fuzzy PSI通信开销高于精确PSI的主要原因必须在设计时予以充分考虑。3.2 基于椭圆曲线的秘密共享OPRF实现我们选择在椭圆曲线群上实现OPRF因为其天然支持我们所需的数学结构离散对数、双线性映射可选并且有成熟的库如OpenSSL, libsodium, relic-toolkit支持。椭圆曲线与群的选择我们使用了NIST P-256曲线secp256r1它在安全性和性能上有较好的平衡并且广泛支持。群运算是加法群但为了叙述方便下面用乘法群符号表示标量乘法。Shamir秘密共享密钥实际上在22门限的秘密共享中我们通常直接使用加法秘密共享即生成随机数k1然后令k2 k - k1在有限域上分别作为份额。这样更简单高效。密钥k是一个大的随机标量256位整数。OPRF函数定义我们采用F(k, x) H(x) * k的形式其中H是将输入哈希到曲线上的一个点的函数Hash-to-Point*表示椭圆曲线上的标量乘法。结果是一个曲线点。核心计算步骤详解Hash-to-Point (H)这是关键且容易出错的一步。不能简单哈希后映射为坐标可能不在曲线上。我们采用“尝试递增”的方法计算hash SHA256(x || counter)尝试将哈希值解释为x坐标检查是否在曲线上如果不在则增加counter重试直到找到有效的点。也有更标准化的算法如Elligator2但实现更复杂。客户端盲化Client选择随机盲化因子r一个随机标量。计算P H(x)然后计算盲化点B P * r。将B发送给Server。r必须保密。服务端部分计算Server收到B后用自己的密钥份额k2计算C B * k2。发送C给Client。客户端去盲化与完成Client首先计算D C * k1。因为C B * k2 (P * r) * k2所以D (P * r * k2) * k1 P * (r * k1 * k2)。然后Client计算r的逆元r_inv在标量域中。最终OPRF值为O D * r_inv P * (k1 * k2) H(x) * k。至此Client得到了F(k, x)而Server在整个过程中从未知晓x、r以及最终的O。代码片段示意使用Python的cryptography库from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec import os # 初始化曲线和密钥份额示例 curve ec.SECP256R1() # 假设k1, k2是预先分发的标量份额 k1 int.from_bytes(os.urandom(32), big) % curve.order # k2 在真实场景中应由k - k1生成此处示意 k2 int.from_bytes(os.urandom(32), big) % curve.order def hash_to_point(data): 简易的尝试递增Hash-to-Point函数 counter 0 while True: digest hashes.Hash(hashes.SHA256()) digest.update(data) digest.update(counter.to_bytes(4, big)) h digest.finalize() # 尝试将前32字节作为x坐标 (简化处理实际需按曲线参数处理) x int.from_bytes(h[:32], big) % curve.order # 尝试计算y坐标 (这里需要椭圆曲线方程省略具体实现) # 如果成功找到曲线上点返回点坐标 # 否则 counter 1 继续循环 # 此处为示意返回一个虚拟点 break return ec.EllipticCurvePublicNumbers(x, y, curve).public_key() def client_blind(x): P hash_to_point(x) r int.from_bytes(os.urandom(32), big) % curve.order B P * r # 标量乘法 return B, r def server_partial_eval(B, k2_share): C B * k2_share return C def client_finalize(C, r, k1_share): D C * k1_share r_inv pow(r, -1, curve.order) # 计算模逆 O D * r_inv return O # O H(x) * k实操心得椭圆曲线标量乘法是性能瓶颈。一定要使用优化过的库如OpenSSL的EC_POINT_mul并考虑使用固定基点预计算表来加速。在我们的实现中将H(x)的计算结果缓存起来至关重要因为同一个数据可能在多个地方被重复处理。3.3 高效模糊交集计算策略得到双方数据的OPRF值集合后如何高效、隐私地找出交集精确匹配阶段双方集合{F(k, x)}和{F(k, y)}已经是伪随机值可以直接进行精确PSI。我们采用了基于排序-比较的简单协议因为它在两方场景下通信复杂度最优O(n)。Client和Server分别将自己的OPRF值集合表示为字节串如压缩曲线点的坐标进行本地排序。Client将自己的排序后列表发送给Server。Server本地进行归并比较找出两个排序列表中的共同元素。由于Server现在知道Client的整个列表为了不泄露额外信息比如Server知道哪些是自己的元素在交集中通常需要让Client来获取结果。一种方法是Server将交集对应的OPRF值发回给ClientClient通过比对得知自己的哪些原始数据在交集中。但这样Server知道了交集内容。更公平的做法是使用额外的轻量级密码学协议如盲签名或简单的OT让双方都能获得交集而不泄露给对方。在我们的项目中根据场景假设Client是查询方采用了Server计算交集大小并告知Client或由Server将交集元素发回的方式。模糊判定集成对于n-gram方法一个原始数据对应多个OPRF值每个n-gram一个。我们不能直接对OPRF值集合求交集因为那得到的是共同n-gram的集合。我们需要的是“拥有足够多共同n-gram的原始数据对”。数据关联在Client和Server内部必须记录每个OPRF值是由哪个原始数据项的哪个特征产生的。这需要一个本地的映射表。交集后处理双方获得共同的OPRF值集合后即匹配上的n-gram各自在本地统计每个原始数据项“命中”了多少个共同n-gram。阈值判断设定一个阈值T例如超过60%的n-gram匹配。对于每个原始数据项如果其命中的n-gram数量超过T * (该数据项的总n-gram数)则认为该数据项与对方集合中的某个些数据项模糊匹配。由于双方只知道自己数据的命中情况他们不知道对方具体是哪个匹配项除非进行额外的协议如如果匹配则透露对应的OPRF值进行反向查找但这可能泄露信息需谨慎设计。对于MinHash方法判定更直接一些。双方比较的是签名向量中对应位置OPRF值相同的数量。如果相同数量超过某个阈值则认为原始数据相似。通信优化技巧OPRF值压缩传输椭圆曲线点开销大。可以只传输点的x坐标如果y坐标可以从x恢复如压缩格式或者对点进行哈希如SHA256后传输哈希值。后者更节省带宽但要求OPRF后续步骤只需要比较相等性而不需要点的代数结构。在我们的流程中交集比较只需要相等性判断因此传输哈希值32字节比传输压缩点33字节或未压缩点65字节更优。批量传输与流水线在处理大量数据时将多个OPRF请求/响应打包成一个消息批量发送可以极大减少网络延迟的影响。我们实现了异步IO和批处理操作将等待时间重叠。4. 性能优化与工程化实践4.1 计算性能瓶颈分析与优化实测下来整个协议的耗时主要集中在三个环节模糊特征生成、Hash-to-Point计算、椭圆曲线标量乘法。1. 模糊特征生成优化N-gram生成使用滑动窗口视图如Python的memoryview避免创建大量子串切片减少内存分配。对于超长字符串考虑设置长度上限或分段处理。MinHashnum_perm次哈希计算是主要开销。我们使用向量化计算利用numpy一次性为所有特征生成所有哈希函数的输出然后通过np.minimum.reduceat进行聚合比纯Python循环快数十倍。# 优化后的MinHash签名计算片段 def fast_signature(feature_list, seeds): # feature_list是特征列表 # 将所有特征对所有种子进行哈希得到一个 (len(feature_list), num_perm) 的矩阵 hash_matrix np.array([[mmh3.hash(f, seeds) for s in seeds] for f in feature_list]) # 按列取最小值得到签名 signature np.min(hash_matrix, axis0) return signature2. Hash-to-Point优化这是椭圆曲线OPRF中最耗时的部分之一因为可能涉及多次哈希和点解压尝试。缓存对相同的输入xH(x)是确定的。在本地计算中对x的哈希结果进行缓存避免重复计算。这在同一个数据集内重复处理时效果显著。使用更快的哈希到曲线算法我们最终集成了hash-to-curve草案RFC 9380的实现它提供了标准、安全且相对高效的算法替代了自研的“尝试递增”方法虽然单次可能稍慢但保证了确定性和安全性避免了最坏情况下的多次尝试。3. 椭圆曲线运算优化使用高性能库放弃纯Python实现核心的标量乘法运算使用通过CFFI或Cython绑定的本地库如OpenSSL或专门优化的椭圆曲线库如relic。预计算对于固定的基点在某些OPRF变种中可以预计算基点倍点表将标量乘法转化为查表和点加大幅加速。批量求逆在客户端去盲化步骤需要对每个盲化因子r计算模逆元r_inv。单独对每个r求逆非常慢。我们采用了“蒙哥马利批量求逆”技巧将n个求逆运算转化为1次求逆和3*(n-1)次乘法性能提升一个数量级。def batch_inverse(elements, modulus): 蒙哥马利批量求逆算法 n len(elements) prefixes [1] * (n 1) for i in range(n): prefixes[i1] (prefixes[i] * elements[i]) % modulus inv pow(prefixes[n], -1, modulus) inverses [0] * n for i in range(n-1, -1, -1): inverses[i] (inv * prefixes[i]) % modulus inv (inv * elements[i]) % modulus return inverses4.2 通信开销控制与网络优化Fuzzy PSI的通信量远大于精确PSI主要因为特征膨胀。数据压缩在传输OPRF值或其哈希之前使用高效的压缩算法如zstd对排序后的列表进行整体压缩。由于列表已排序相邻值前缀相同率高压缩比非常可观。布隆过滤器可选在精确PSI阶段前可以先使用布隆过滤器进行“粗筛”。双方交换自己OPRF值集合的布隆过滤器本地先过滤掉肯定不在对方集合中的元素只对可能存在的元素进行后续传输和比较。这可以显著减少在线通信量但会引入一定的误报率False Positive需要权衡。协议流水线不要等所有OPRF请求都完成再开始下一轮。可以实现流水线操作一边接收Server的部分计算结果一边就开始去盲化和下一批请求的发送充分利用网络带宽。4.3 系统实现与测试踩坑实录我们最终用Python实现了协议的核心逻辑关键路径椭圆曲线运算、批量求逆用C扩展进行加速并提供了简单的Socket通信接口。遇到的典型问题与解决方案问题在测试大规模数据百万级时内存占用爆炸。排查发现是在Python中存储了所有中间状态每个特征的原始数据、n-gram、OPRF中间值、盲化因子等的列表。特征膨胀导致列表元素数量是原始数据量的数十倍。解决改为流式处理。一次只加载一批原始数据例如1万条完成这批数据的特征提取、OPRF交互、结果存储后再处理下一批。并大量使用生成器generator来传递数据避免在内存中构建巨大的中间列表。问题网络传输时偶尔出现数据错位导致后续比较失败。排查发现是TCP粘包/拆包问题。我们直接发送Python的pickle序列化对象或拼接的字节串没有定义明确的消息边界。解决设计简单的应用层协议每个消息由“4字节长度头”“实际数据体”组成。接收方先读取4字节得到长度N再准确读取后续N字节。彻底解决了数据错乱问题。问题模糊匹配的准确率不稳定有时误匹配很多。排查检查发现是字符串预处理不一致。一方在生成n-gram前做了小写化和去除空格另一方只做了小写化。导致“hello world”和“helloworld”的n-gram集完全不同。解决制定并强制使用统一的预处理管道包括Unicode规范化NFKC、转为小写、移除所有非字母数字字符根据场景调整、修剪首尾空格。并将这个管道作为协议的一部分明确下来。问题椭圆曲线运算在不同机器上结果不一致。排查发现是序列化/反序列化曲线点时使用的坐标格式压缩、未压缩和字节序不一致。解决规定所有传输的点都使用标准的压缩格式SEC1标准并在代码中统一使用大端字节序big-endian进行编解码。同时编写了详细的单元测试覆盖了从数据到OPRF值的完整链条确保跨平台一致性。性能测试数据仅供参考在一台4核CPU/8GB内存的测试机上对两个各包含10万条短文本平均长度15字符的数据集进行模糊PSI使用2-gram。特征生成约2秒。OPRF计算双方约45秒其中Hash-to-Point和标量乘法占90%以上。网络通信传输约2000万个OPRF值哈希32字节每个压缩后约180MB传输时间取决于网络。总耗时在局域网环境下端到端总时间约为60-70秒。 这个性能对于许多离线数据核对场景已经是可用的但离实时交互还有距离进一步优化需要更底层的代码和硬件加速。5. 安全考量与协议扩展讨论5.1 协议安全性浅析我们的协议安全性建立在几个密码学假设之上离散对数问题的困难性从椭圆曲线点H(x)^k和H(x)反推密钥k是计算不可行的。OPRF的安全性Server在不知道r的情况下从H(x)^r无法获取关于H(x)或F(k, x)的任何信息不经意性。Client在不知道k的情况下无法从交互中学习到关于k的信息伪随机性。秘密共享的安全性在22加法秘密共享下任何单一份额k1或k2都是完全随机的不泄露任何关于k的信息。潜在攻击与缓解中间人攻击通过TLS/SSL加密通信信道来防止。恶意客户端客户端可能发送精心构造的blinded值来探测Server的密钥份额。标准的OPRF安全模型通常考虑恶意敌手。我们的基础协议在半诚实诚实但好奇模型下是安全的。要防御恶意敌手需要引入零知识证明ZKP来让Client证明其发送的blinded值是正确生成的但这会极大增加复杂度。在实际合作场景中半诚实模型通常是可接受的。集合大小推断即使交集内容不泄露双方交换的OPRF列表大小会暴露各自集合的大小。这是大多数PSI协议固有的信息泄露可以通过在集合中添加假数据dummy items来隐藏真实大小但会增加开销。5.2 扩展方向探讨这个基础协议可以朝多个方向扩展支持多方2Fuzzy PSI秘密共享可以自然地扩展到多方。密钥k被拆分为多个份额分发给多个参与方。计算OPRF值时需要所有方或达到门限的多数依次进行部分计算。通信轮数会增加但核心原理不变。与差分隐私结合在公布交集结果如交集大小、统计信息前加入经过校准的拉普拉斯噪声可以提供差分隐私保障防止从结果中反推个体信息。非对称场景优化在很多应用中一方客户端集合很小另一方服务端集合巨大。可以使用“不经意传输”OT扩展技术来构建更高效的PSI协议其通信开销主要与小集合大小相关例如KKRT16协议。将模糊匹配集成到这类协议中是一个前沿研究方向。利用GPU/FPGA加速椭圆曲线标量乘法和批量哈希计算是高度并行的非常适合用GPU加速。可以将这些计算密集型任务卸载到GPU上有望获得一个数量级以上的性能提升。实现一个基于秘密共享OPRF的模糊PSI协议是一次将密码学理论、数据结构和系统工程紧密结合的实践。它让我深刻体会到隐私计算技术要真正落地不仅需要精巧的协议设计更需要扎实的工程实现和对性能瓶颈的持续优化。希望这个分享能为你自己的隐私计算项目提供一些可行的思路和避坑指南。在实际部署时务必进行充分的安全审计和性能测试从小规模数据开始逐步迭代。