基于 2-of-2 多方安全计算的 MACI 匿名化方案

感谢 Felix Cai 对 MACI 和隐私投票中多个问题的讨论。

这篇文章将展示基于 2-of-2 MPC 技术的 MACI 匿名化方案的具体实现。本文核心内容主要分为三个部分:从任意算法到逻辑电路的实现;从逻辑电路到混淆电路的实现;利用不经意传输实现多方安全计算。最后,我们总结基于多方安全计算的匿名化方案。

算法

MACI 系统中,操作员 Operator(记为 )管理着一个数组,这个数组记为 deactivate[],其元素个数与 registry 数组中的元素个数相同。数组中每个位置的编号代表了 key 值(用户的编号),而每个位置存储着对应的公钥。为了更好地理解 deactivate[] 数组,可参考如下例子:在 deactivate[] 数组中,假设 registry 中的用户 deactivate[] 数组中的第一个元素)发起了 deactivate key 操作,操作员 收到该操作请求后,如果操作的合法性被 承认,deactivate[] 数组中的第一个位置填入 registry 数组中第一个位置所包含的 value 值(用户 的公钥)。另外,假设用户 没有发起 deactivate key 操作,那么 deactivate[] 数组中的第二个元素的值为 0。这样一来, deactivate[] 数组的元素个数和 registry 数组中的元素个数相同就可以理解了。

现在,为了实现 MACI 系统对管理员的匿名性,需要设计一个 2-of-2 的 MPC 方案来实现以下两个性质:

  1. 假设拥有 的用户 想将 更改为 ,他需要实现让操作员(记为 )不知道更新的 是替换的
  2. 拥有 的用户(称为 )不能知道 deactivate[] 数组中的除了 外的其他任何元素。

先暂且不考虑这两个要实现的目标。先考虑更换密钥的操作: 发起更换 deactivate[] 数组中属于 的公钥的请求, 验证 的请求是否合法,如果合法,则执行更换公钥操作;否则,拒绝请求。

因此, 需要将即将被更换的公钥 和新的公钥 同时发送给 首先查看在 deactivate[] 数组中,是否有元素 ,如果有,就将 换成 ;否则,拒绝 的请求。

根据上述逻辑,我们先设计一份伪代码来实现这个操作,如下:

用户 发送给 deactivate[] 数组中的值进行比较,得出输出(,或 ),也就是说,如果 确实存在于 deactivate[] 数组中,则执行公钥更换操作;否则就不执行。

现在,假设 管理的数组 deactivate[],我们可以写出伪代码和其 Rust 实现:

function findElement(array, PK_1, PK_1′) {
  for (let i = 1; i < array.length; i++) {
    if (array[i] === PK_1) {
      return PK_1′;
    }
  }
  return false;
}
fn find_element(array: &[i32], a: i32, b: i32) -> bool {
  let mut result = false;
  for i in 1..array.len() {
      result = result || (array[i] == a && b == a);
  }
  result
}

至此,我们已经实现了简单的在明文下的公钥更换操作。下面,我们将介绍逻辑电路的生成原理,混淆电路的生成原理,以及混淆电路输入的加密。这些步骤是实现上述 MACI 匿名性质 1 的关键,也就是让 无从得知用户 的新公钥 对应的是 deactivate[] 数组中的哪一个元素。

逻辑电路的生成原理

现在需要将上述算法转换成逻辑电路,转换的逻辑就是用逻辑运算符构造逻辑电路。一个逻辑运算符(如 )有两个输入和一个输出。每一个输入都有 01 两种可能。一个 逻辑门的计算规则可以用下表表示:

打个比方,在上面的例子中,假设 deactivate[] 中的元素个数是二,由 保管。而输入是由用户 共同完成。用户 输入的是公钥 和新公钥 输入的是 deactivate[] 数组中的所有元素。

为了简单起见,暂且假设一个公钥由二位二进制数来表示(实际的公钥是 256 位二进制数)。那么,现在就需要用用户 的输入公钥,对比 deactivate[] 数组中的二把公钥。因此,需要对比二次。

对于一把公钥的对比,由于假设一把公钥有二位,那么就需要两个 表示的逻辑运算符和一个 的逻辑运算符来表示。两个 的输出是一个 的输入。用户将自己的输入输入到该组逻辑运算的子电路中。

逻辑电路由逻辑门组成,逻辑电路中的逻辑门被分为 层,比如上面的电路是两层。逻辑门第一层需要用户来添加输入, 其他层是中间节点,最后是根节点。对于用户输入的逻辑门,有两个输入和一个输出。其中一个输入由用户 完成。另外一个由 完成。这三个逻辑门组成了一个子逻辑电路。这个子逻辑电路负责一次公钥的对比。

因此,两个这样的子逻辑电路再配上一个 门,就可以完成一个简单的逻辑电路了。

为了便于理解,根据上述的假设,我们可以设计这样一个例子,假设用户 10,那么用户 在上图中第一个逻辑子电路的上方的 中输入 1,在上图中下方的 中输入 0。同时,用户 在上图中第二个逻辑子电路的上方的 中也输入 1,在上图中下方的 中也输入 0(用户 对于逻辑电路的输入)。

假设 deactivate[] 数组中的第一个和第二个元素分别是 1011。那么, 需要在上图中第一个逻辑子电路中上方的 中输入 1,在第一个逻辑子电路中下方的 中输入 0。同时, 在第二个逻辑子电路中上方的 输入 1,在第二个逻辑子电路中下方的 输入 1 对于逻辑电路的输入)。

这样一来,第一个 的两个输入都是 1,输出自然也是 1。同理,第二个 的两个输入都是 0,输出自然也是 1(根据表一的计算规则可知)。这样一来, 的两个输入都是 1,输出也是 1。因此,通过执行第一个逻辑子电路的对比(即对比用户 对第一个逻辑子电路的输入和 deactivate[] 数组中第一个元素对第一个逻辑子电路的输入),得到的输出结果是 1,从而可以证明,用户 A 的输入和 deactivate[] 数组中的第一个元素是完全一致的(都是 10),因此可以证明用户 的输入确实在 deactivate[] 数组中。 也不需要再去验证第二个逻辑子电路了。

逻辑电路的生成工具

在现实中,为了让一个固定的算法转换成逻辑电路,需要用到一些工具,如

  1. Verilog HDL 和 VHDL:这两种硬件描述语言被广泛用于数字电路的设计和仿真,可以使用它们来描述算法的行为,并将其转换为逻辑电路的形式。这些语言都支持从高级语言(如C、C++ 和 Java)转换为硬件描述语言的形式。使用 Verilog 或 VHDL 需要一定的硬件设计和编程经验。
  2. Xilinx Vivado Design Suite:这是一款商业软件,用于 FPGA(现场可编程门阵列)的设计和开发。它提供了一个综合工具,可以将高级语言或 RTL(寄存器传输级)代码转换为逻辑电路的形式。它支持多种编程语言,包括 C、C++、SystemC、Verilog 和 VHDL 等。
  3. Yosys:这是一款开源的 EDA(电子设计自动化)工具,用于数字电路的设计和仿真。它支持从 Verilog 和 VHDL 等硬件描述语言转换为逻辑电路的形式。它也支持从高级语言(如 C、C++ 和 Python)转换为 RTL 代码,然后转换为逻辑电路的形式。

混淆电路(Garbled Circuit)

逻辑门的加密(混淆)

回到现在的例子中,因为我们的公钥在 deactivate[] 数组中都是有序号的(序号等价于用户的编号),且数组 deactivate[] 中的最大元素个数是有上限的(用户的个数)。因此,假设最大元素个数是 ,那么,就需要构建 个子电路,然后合成一个最终的逻辑电路。

由于逻辑电路在每一个 epoch 中(一个 epoch 是区块链更新 registry 中元素的一个周期)是固定的,那么,所有用户都可以使用这个早已经生成好的,并存储在区块链中的逻辑电路。在本文例子中,由于 deactivate[] 数组中只有两个元素,因此子电路只有两个(每个子电路对应于一次公钥的对比)。

注:在一个 epoch 中,registry 数组中元素个数是固定的,因此用户的输入和数组中的元素的对比次数是固定的,因此逻辑子电路的个数是确定的,且逻辑子电路是事先可以确定好的,因此整个逻辑电路是唯一确定的。

接下来,就是将逻辑电路的每一个输入进行加密,然后将每一个逻辑门进行混淆。

先考虑加密。加密这个动作是由用户 完成的。他对每一个输入和输出的 01 用加密值表示。注意,最终的根逻辑门的输出不需要进行加密,在本案例中,就是 0(对比失败),和 1(更换新的公钥)。如图所示:

逻辑电路中的每一条引线的 01 都用加密值代替。同时,每一个逻辑电路都有一个固定的编号(这个是所有人都已知的)。例如,编号 1、2、5 就构成了一个子电路 1(比较用户的公钥和数组中的第一把公钥)。

基于此,将逻辑电路的每一个逻辑门的输入和输出(除了根逻辑门)进行加密后,在 Operator 的视角里,对于加密后的逻辑电路,他就不知道每个逻辑门的输入数字(加密值)对应的明文(是 0 还是 1)。

输入加密的方法

在混淆电路中,为每个输入值生成两个随机 key 的过程通常是使用伪随机数生成器(PRNG)来实现的。常用的生成随机 key 的实现和库有很多,例如 OpenSSL、Crypto++、libsodium 等。

输出加密的方法

基于伪随机函数 PRF 的方法是 Yao 的混淆电路中实现对输出二进制数 01 的加密的主要方法之一。具体来说,这种方法可以实现对每个逻辑门的输出进行加密,保证了数据的机密性和可靠性。

在基于 PRF 的方法中,每个逻辑门的输出都被混淆成为一组包含多个 label 的二元组,其中每个 label 都是一个随机的 01 字符串。在生成每个 label 时,可以使用伪随机函数(PRF)来实现。具体来说,可以使用一个密钥和输入值(如 01)作为 PRF 的输入,然后得到一个随机的 01 字符串作为 label。由于 PRF 是一种可以模拟真正随机数的算法,因此生成的 label 具有高度的随机性和不可预测性,保证了数据的机密性和安全性。

在混淆输出时,可以将每个逻辑门的输出表示为一个包含两个 label 的二元组(如 {label0, label1})。其中,当输入为 0 时,使用 label0 作为输出;当输入为 1 时,使用 label1 作为输出。在将逻辑门的输出发送给下一个参与方时,只发送一个包含正确 label 的二元组,保证了数据的机密性和保密性。常见的 PRF 包括 HMAC、SHA、AES 等。

需要注意的是,对逻辑门的输出采用上述方法的目的是为了便于不经意传输(oblivious transfer)的实现。

逻辑门的计算规则可以用下表表示:

现在,我们来举个例子:下面的图是对 真值表中输出的加密。也就是对 真值表中——

  • 第一行的输出 0 加密成
  • 第二行的输出 0 加密成
  • 第三行的输出 0 加密成
  • 第四行的输出 1 加密成

其中 就是对应于用户A的输入的二种可能的加密值, 可以是 AES 加密算法。

逻辑门的加密真值表

这里其实就可以推导不经意传输过程了。比如,用户 A 将这四个输出——

——分别发送给 ,同时 还将自己的输入的加密值假如是 发送给 。对于 而言,他并不知道 所代表的具体数值。但是 知道,他需要在 门的输入是 1。因此, 需要使用的加密密钥是 不需要将这个信息告诉 )。 利用加密密钥 进行加密,就能够得到 。然后将 发送给他的四个输出进行对比,如果他的计算结果与四个收到的加密输出中的其中一个相等,就证明他的加密是有效的。

此时对于 来说,无法通过 推断出 的输入。同理, 也不知道 是使用 进行的加密,因此 也无法推断 对于这个逻辑门的输入。

用户 A 输入的生成

现在,我们继续假设,用户 公钥的编号是 1,数值是 10,而数组中的二把公钥(编号分别为 0、1)的数值分别是 11, 10。也就是说用户 的公钥在数组中。

接着,用户将自己的输入,分别输入到两个子电路中(sub-circuit 1 和 2)。用户在第一个逻辑子电路(1、2、5)中进行输入。他的输入是:在编号为 1 的逻辑门输入 2674(代表二进制数 1);在编号为 2 的逻辑门输入 8798(代表二进制数 0)。用户在第二个逻辑子电路(3、4、6)中进行输入。那么,他的输入是:在编号为 3 的逻辑门中输入 9809(代表二进制数 1);在编号为 4 的逻辑门中输入 3453(代表二进制数 0)。

那么,用户 的输入密文 2674, 87989809, 3453 对应的明文均是二进制数 1, 0。而对于 来说,密文 2674, 87989809, 3453 对应的明文均有四种可能 0, 00, 11, 01, 1。如果密文的长度从二位变成 256 位,那么明文的可能性就有 。因此,对于 来说,我们可以假设用户 所给出的密文是无法被 反推出来的。

电路混淆

接下来的步骤就是将逻辑电路进行混淆。

对于一个 逻辑门,真值表如下,可对应上图来看。

表一: 逻辑门真值表

现在,这个加密版本的 逻辑门(以上图逻辑门 1 为例)如下:

表二: 逻辑门真值表的加密版本

由于上面的没有打乱顺序, 可以很轻易的推断出,2376 对应于用户的输入 02674 对应于用户的输入 1

现在来打乱表二的顺序,变成表三(混淆版本的逻辑门)。

表三: 逻辑门真值表的混淆版本

这样一来,根据这个表格的排序可能性, 想通过表三还原成真正的表一,会碰到四种可能性。那么,如果在一次证明中涉及到 个逻辑门,每个逻辑门用户 都做上述操作,那么就有 种可能性。这样,通过排序任意颠倒,使得 很难推断出每个数字到底代表 0 还是 1

第一次通信

用户将这七个逻辑门所对应的混淆之后的逻辑门表发送给 。并把自己的输入发送给 (密文 2674, 8798)和 9809, 3453)还将自己的新公钥 发送给 ,还将一个哈希值 发送给 。总结,发送——逻辑门表、自己对每个叶子逻辑门的输入、新公钥、哈希值 ——给 。需要注意的是, 有能力将七个逻辑门分为三个部分,即子电路 1、子电路 2 和根逻辑门。也知道逻辑门 1、2、5 代表第一个子电路,逻辑门 3、4、6 代表第二个子电路。逻辑门 7 代表根逻辑门。这些信息是不能随便保密的,否则就很难让 求解算法了。(假设能够保密,会进一步增强隐私性,但是计算复杂度如何控制在可以容忍的范围是重点。)

此刻,我们做一个总结, 对每个子电路的输入都传给了 。但是, 不能根据这些输入来推断出 的每个输入的明文是什么。 的输入的位数 越长, 就越难从 的密文中推断出 的明文输入。同时, 也会将 的每个逻辑门的输入加密值,也就是 均发送给

到这里为止,我们已经实现了第一个目的,通过设计混淆电路和对混淆电路输入的加密, 已经无法从 提供的信息中推断出 想替换的公钥是 。接下来,我们会通过不经意传输实现第二个目的,那就是让 也无法得知 deactivate[] 数组中除了 意外的其他任何元素。

不经意传输(Oblivious Transfer)

由于对于每个子电路, 都知道自己需要输入的是什么:比如第一个子电路, 应该输入 11(也就是说对于逻辑门 1 和逻辑门 2, 都应该输入 1);第二个子电路, 应该输入 10(也就是说对于逻辑门 3 和逻辑门 4, 应该分别输入 10)。但是,由于 不知道每个逻辑门的与他相关的引线的两个加密数字()哪个代表 0 哪个代表 1(如对于逻辑门 1 的 45433213 这两个加密值, 并不知道 4543 代表 03213 代表 1),因此, 需要求助 获得必要的信息。

此时,我们需要回顾两个一定要保证的性质:

  1. 假设拥有 的用户 1 想将 更改为 ,他需要实现让操作员(记为 )不知道更新的 是替换的
  2. 拥有 的用户(称为 )不能知道 deactivate[] 数组中的除了 外的其他任何元素。

因此,在 求助的过程中, 不能透露自己的公钥 不能透露在数组中的任何一把公钥信息给 。而这两个特性均可以由不经意传输(OT)来保证。

下面详述 OT 的全过程。以编号为 1 的逻辑门也就是 为例, 现在很清楚,自己要在这个逻辑门中输入 1

这个时候, 知道 对这个逻辑门的输入是 2674)( 不知道 2674 是啥意思), 需要问 1 的加密值到底是 4543,还是 3213。但是,为了保证第二条性质, 不能直接告诉 ,他需要 1 的加密值。同理, 也不能把 0 等于 4543 以及 1 等于 3213 告诉 ,这样的话, 可以根据这些信息,推断出 的密文所对应的明文。

  1. 生成一个哈希值 这个哈希值已经于之前传输给了 发送给 验证 是否成立。如果成立,则进行下一步,否则则不需要回复

对这一步进行说明,只要这个 值没有后门,那么,对于 来说,他想要满足公式 ,他就需要先生成一把私钥 ,然后生成公钥 ,然后再通过 得到 。也就是说, 只能拥有 这两把公钥中的一把私钥。否则,就意味着 能破解 DH 离散困难。即 能从公式 中求解出

  1. 利用密码学协议(例如 ElGamal 协议)传输消息。 选择一个随机数 。将 发送给
  2. 由于 只有 这两把公钥中的一把私钥,因此 可以求解出其中一个秘密。但是 并不知道 求解出了哪个秘密。这保证了特性 2。

上述三个步骤,满足了第二个性质,但是没有满足第一个性质,也就是说, 在解密了其中一个加密值后,得到了明文 1,但是,也暴露了另一个明文 0。这样一来,混淆逻辑门表就暴露了很多信息给 ,这样让 有机会破解 的秘密,从而使得性质 1 无效。

对于此的解决方案是, 将加密值进行更改。将 改成 。这样一来, 会选择解密 得到 。然后在用 对这个逻辑门的输入 结合解密的信息 得到 。此时, 还是无法推断出 1 的加密值是 4543 还是 3213。他只知道,对于编号为 1 的逻辑门,他得到的输出是 2232。但是他又不知道,2232 代表 0 还是代表 1

从对一个逻辑门的 OT 通信可以看出整个 OT 通信的全貌,即每个逻辑门的 OT 通信都是如此,且在一次 OT 通信过程中全部完成。这样, 就能得到所有叶子逻辑门的输出,这样他就能进一步得到分支逻辑门的输出,从而最终得到根逻辑门的输出。

方案改进

上述方案中,需要且可以改进的地方有很多。例如,改进算法设计,降低算法的复杂度;改进逻辑电路的设计,让逻辑电路变得更加简单(减少门的个数,减少通信是需要传递的信息);增加方案的安全性,加大破解方案的计算复杂度;让交互变成非交互。

算法和逻辑电路的改进

当前逻辑电路实现的比较算法的复杂度是 ,如果数组有 个元素,那么通过循环对比,最多比较 次。

但是,如果采用 Merkle Patricia Tree 的数据结构的话,对比的次数要少很多(这是因为对于 256 位的公钥来说,形成的 MPT 一定是稀疏的,因此,可能筛选二到三轮就可以筛选出想要的值)。那么,一旦算法计算复杂度下降了,逻辑电路的设计也就要简单很多。

逻辑电路的设计也有化简的可能性。

的每个输入都不同, 也是。实际上,可以进行化简。变成如下图所示。

这样的化简可以让输入变少。

增加安全性

对于安全性,就是需要让 无法通过解密 的某些值而探索出 的秘密。当前设计上逻辑门的安全性尚待提高,比如 门。

如果 想输入的是 0,那么他得到的结果必然是 0,也就是说,他天然知道输出 0 的加密值,那么他就天然知道了 1 的加密值。同时,由于 的输入已经是确定的,那么他可以根据 的输入值,和1的输出的加密值所在的行,去分析,如果 1 的输出的加密值所在的行,不包含 的输入加密值,那么 的输入就是 0,否则就是 1

为了解决这类问题,我们需要对输出全部进行加密。比如使用该种方法:上表中的四个输出值 0001 分别加密为——

——这样一来,输出的四个值分别对应为四个不同的密文。因此,在此环境下, 无法通过上述推断来反推出 的输入值是 0 还是 1。但是,这种方法使得输出加密值的个数从二变成了四。通信复杂度会增大。

此外,非交互式的不经意传输协议也值得研究。

总结

上述方法首先不考虑需要实现 MACI 匿名化的两个目标,即在明文下设计一个算法,让 能够正确判断是否执行用户 的请求。基于此,再将所设计的算法转化成逻辑电路。再将逻辑电路转化成混淆电路。需要注意的是,逻辑电路在每一个区块链的 epoch 中是固定的且能够从区块链中直接获取的。用户 获取该 epoch 的逻辑电路,利用相应工具将逻辑电路转化成混淆电路。同时,用户 将每一个混淆电路的输入进行加密,并将加密好的信息发送给 。此时, 由于不知道加密后的输入对应的明文是什么, 也就不知道用户 具体的输入是什么了,这样就实现了匿名化的第一个性质。

进而,使用不经意传输是实现第二个目的的方法。通过不经意传输,用户 并不知道 具体采用了哪些加密值进行进一步的输入,因此用户 无法推断出 deactivate[] 数组中的其他元素信息。同时,由于 通过不经意传输获得了足够的有效输入,因此 将这些有效输入输入到被加密的混淆电路中,最终能够得到正确的输出,该输出和明文下执行第一节的算法所得到的输出是相同的。因此, 就能够根据输出来判断是否要执行用户 的请求。

最终, 将上述所有操作利用零知识证明生成操作的 proof。区块链对 proof 进行验证,验证通过后,区块链将根据 deactivate[] 数组中的元素和 registry 数组中的元素来更新 registry 数组并结束 epoch 和开启下一个 epoch。