导航菜单
首页 » 图解 » 正文

隐私计算:使用混淆电路开源框架Obliv-C解决百万富翁难题

void func() obliv { …}

规则7:非obliv函数不能在obliv if或者obliv函数体内被调用。

规则8:obliv函数内部不能对在其之外声明的非obliv变量进行赋值修改。

另外,对于函数引用传参也需要注意。比如下面这个函数func被调用时对于p1指针指向是有限制的(p2是常量指针,不用担心其泄露数据,所以可以引用外部变量),p1指针只能指向obliv if语句块内声明的变量,如代码清单6所示。

代码清单 6 函数引用传参时指针指向限制示例

void func (int* p1, const int* p2) obliv {…}
int x, y;
obliv int a;
//非法,p1指针指向了外部声明的变量x
obliv if (a < 0) { func(&x, &y); } 
//合法,p1指针指向了obliv if内部声明的变量i
obliv if (a < 0) { int i; func(&i, &y); }

规则9:obliv变量不能被用于数组索引(Obliv-C开发者认为虽然可以实现,但性能太慢)、指针偏移量、或者数字移位运算中的移位次数中。需要注意的是,普通int整型数是可以被用在obliv数组中的索引中的。

那么如果需要根据obliv变量对数组进行访问需要如何处理呢?Obliv-C的作者也给出了解决方法,如代码清单7所示。

代码清单 7根据obliv变量对数组进行访问的示例

void writeArray (obliv int* arr, int size, obliv int index, 
obliv int value) obliv {
for (int i = 0; i < size; ++i) {
obliv if (i == index) {
arr[i] = value;
}
}
}

显然,根据obliv变量对数组进行访问的时间复杂度不再是O(1)而是O(n)了。

Obliv-C引入关键词对变量进行修饰,其含义与const类似。其引入的原因主要是考虑到结构类型在某些使用场景下需要是深度常类型(deep-const),比如代码清单8所示的场景,修饰符的作用被递归应用到了变量b内的所有指针从而保证b的内部指针p不会被赋值。

代码清单 8 关键词使用示例

struct S { int x, *p; };
void func (const struct S* a, frozen struct S* b) {
a->x = 5;//非法
b->x = 5;//非法
*a->p = 5;//合法,a->p的类型是 int *const 而不是const int*
*b->p = 5;//非法,frozen是递归应用到了struct/union内的所有指针
}

对于、union以及指向指针的指针等,const和存在差异,比如int **与const int *const *const相同,而与const int**或者int **const不同。

规则10:通常情况下,任何非obliv变量在进入obliv作用域(obliv-if或者obliv函数)时,都可以视作被修饰符修饰。

这个规则也比较好理解,因为如果obliv作用域的非obliv变量不是被视作被修饰的话,信息就有可能通过给非obliv变量赋值的方式泄露出去。

规则11:对于任何类型T,指针T * 的解引用()获得的是一个T 类型的左值。

规则12:对于obliv数据,修饰符会被忽略。

这部分是Obliv-C与C区别最大之处。无条件代码段是指在obliv条件性代码段中拆分出一块代码段进行无条件执行,如代码清单9所示。

代码清单 9 无条件代码段的示例

int x = 10;
obliv int y;
obliv if (y > 0) {
x = 15;//非法:不能在obliv作用域修改非obliv变量x
~obliv (c) {//开启无条件代码段
x = 15;//合法:c即使为false的情况下赋值仍然会发生
}
}

规则13:无条件代码段的执行不依赖于任何obliv变量值,变量的限制在无条件代码段中不再生效。

但是这里有个地方需要小心,在上面第3行代码obliv if (y > 0)中,即使y不是正数,无条件代码段仍然会被执行(即使在c为false的情况下也会执行)。一般情况,~obliv ()语法中也声明了一个obliv的布尔变量,该可被用在无条件代码段内部的obliv if的条件判断上。示例如代码清单10所示。

代码清单 10 无条件代码段中声明的obliv布尔变量的使用示例

void swapInt(obliv int* a,obliv int* b) obliv {
~obliv(en) {
obliv int t = 0;
obliv if(en) t=*a^*b;
*a^=t;
*b^=t;
}
}

在基本了解Obliv-C的语法后,我们以求向量内积的案例为例,进一步了解使用Obliv-C进行编程时的项目基本结构。读者在刚开始接触Obliv-C项目时,建议同时通过src/ext/目录下的obliv.oh、obliv.h两个文件来了解Obliv-C提供的接口。然后通过进一步阅读test/目录下的几个测试案例熟悉接口的使用方法。

实现求向量内积共需4个文件,即.c、.h、.oc、文件,接下来我们来看一下每个文件的作用。

(1).h文件

编程规则与C语言完全相同,本文件用于声明函数、定义混淆电路相关结构体()以及参与混淆计算的全部参数(包括各方的隐私输入以及最后的共享结果)。特别地,各方的隐私输入可以定义为同一变量名,也可以定义为不同的变量。对应的代码如代码清单11所示。

代码清单 11求向量内积.h代码

#pragma once
#include
void dotProd(void *args);//混淆计算函数的声明,具体的函数定义见innerProd.oc
typedef struct vector{
  int size;
  int* arr;
} vector;
typedef struct protocolIO{
  vector input;
  int result;
} protocolIO;//包含了混淆电路计算输入和输出的相关结构体

注意:对于结构体,其中的变量不能使用指针,需要用数组代替,否则编译可能不会报错,但最终运行结果错误。

(2).c文件

编程规则与C语言完全相同,本文件程序用于获取命令行参数、设置混淆电路环境、输出混淆计算结果等。其主要执行顺序为:

1)获取并校验命令行参数。

2)与另一个参与方进行网络连接,如代码清单12所示。

代码清单 12 .c文件中进行网络连接的代码段

ProtocolDesc pd;//混淆电路的相关函数都需要使用这个变量
  protocolIO io;
  const char* remote_host = (strcmp(argv[2], "--")==0?NULL:argv[2]);
  if(!remote_host){//两个参与方进行网络连接
    if(protocolAcceptTcp2P(&pd, argv[1])){
      fprintf(stderr, "TCP accept failed\n");
      exit(1);
    }
  } else{
    if(protocolConnectTcp2P(&pd,remote_host,argv[1])!=0){
      fprintf(stderr,"TCP connect failed\n");
      exit(1);
    }
  }

提示:和是Obliv-C提供的为两个隐私计算参与方建立连接的接口。

3)设置自己的编号。

int currentParty = remote_host?2:1;
  setCurrentParty(&pd, currentParty);//两个参与方分别设置自己的编号

4)从文件中读取向量内容,如代码清单13所示。

代码清单 13 .c文件中读取向量内容的代码段

vector v;
  FILE* file = fopen(argv[3], "r");
  if(fscanf(file, "%d\n", &(v.size)) == EOF){//从文件中读取向量大小
    fprintf(stderr, "Invalid input file\n");
    return 2;
  }
  v.arr = malloc(sizeof(int) * v.size);
  for(int i=0; i

5)执行混淆电路代码。

io.input = v;
  execYaoProtocol(&pd, dotProd, &io);//执行混淆电路代码

6)输出计算结果。

int result = io.result;
  fprintf(stderr, "DotProduct is %d\n", result);//输出计算结果

7)清理。

cleanupProtocol(&pd);//固定用法,清理ProtocolDesc pd

(3).oc文件

编程规则与C语言类似,用于定义混淆计算函数(即本例中的函数),相关语法在前面部分已有描述。对应的代码如代码清单14所示。

代码清单 14求向量内积.oc代码

#include
#include"innerProd.h"
void dotProd(void *args){
  protocolIO *io = args;//混淆计算参数对应结构体获取
  int v1Size = ocBroadcastInt(io->input.size, 1);
  int v2Size = ocBroadcastInt(io->input.size, 2);
  obliv int* v1 = malloc(sizeof(obliv int) * v1Size);
  obliv int* v2 = malloc(sizeof(obliv int) * v2Size);
  //获取参与计算的向量,最后一个参数为提供数据的参与方编号
  feedOblivIntArray(v1, io->input.arr, v1Size, 1);
  feedOblivIntArray(v2, io->input.arr, v2Size, 2);
  int vMinSize = v1Sizeresult), sum, 0);//揭示计算结果
}

提示:在上面的代码中,函数是用来将非obliv数据传给其他参与方的。根据数据类型不同,类似的还有等函数。

(4)文件

本文件用于编译。文件的编译只需在对应的文件目录打开命令行终端,输入make后回车即可。编译成功产生一个a.out可执行程序文件。运行按相应的格式输入参数便可。对应的代码如代码清单15所示。

代码清单 15求向量内积程序的脚本

privacyProgram=innerProd
CILPATH=/root/obliv-c
REMOTE_HOST=localhost
CFLAGS=-DREMOTE_HOST=$(REMOTE_HOST) -O3  
./a.out: $(privacyProgram).oc $(privacyProgram).c 
$(CILPATH)/_build/libobliv.a
$(CILPATH)/bin/oblivcc 
$(CFLAGS) $(privacyProgram).oc $(privacyProgram).c -lm
clean:
rm -f a.out
clean-all:
rm -f *.cil.c *.i *.o

至此,相信读者应该对Obliv-C项目的文件结构有了基本的了解,接下来就可以尝试一个小的应用案例了。

2. 应用案例:解决百万富翁难题

通过上面的介绍,使用Obliv-C来实现百万富翁问题就变得非常简单了。首先在C:\ppct\obliv-c\目录下创建3个文件:.h、.c、.oc。

提示:C:\ppct是本文项目代码放置的目录,读者在代码实践时可根据自己的系统情况修改。

在.h文件中需要定义隐私输入(即两个富翁的财富值)和输出(即两个富翁中谁更富有)以及隐私计算函数,具体代码如代码清单16所示。

代码清单 16百万富翁问题.h代码

typedef struct protocolIO {
  int cmp; //隐私计算输出,-1:Alice小于Bob, 0:Alice等于Bob,1:Alice大于Bob
  int mywealth;//隐私计算输入
} protocolIO;
void millionaire(void* args);//隐私计算函数

然后在.oc文件中编写具体的隐私计算函数(),具体代码如代码清单17所示。

代码清单 17百万富翁问题.oc代码

#include
#include"million.h"
void millionaire(void* args) {
  protocolIO *io=args;
  obliv int aliceWealth,bobWealth;
  aliceWealth = feedOblivInt(io->mywealth,1);//获取隐私输入
  bobWealth = feedOblivInt(io->mywealth,2);
  bool eq,lt;
  revealOblivBool(&eq, aliceWealth == bobWealth, 0);//首先比较两人是否同样富有
  revealOblivBool(<, aliceWealth < bobWealth, 0);//然后比较是否Bob更加富有
  io->cmp = (!eq? lt?-1:1 : 0);//输出最后比较结果
}

接下来在.c文件中进入主函数编写,其主要流程为参与方之间建立网络连接、设置自己的参与方编号、输入自己的财富值、执行混淆电路代码进行财富值比较、输出结果并清理。具体代码如代码清单18所示。

代码清单 18百万富翁问题.c代码

#include
#include
#include"million.h"
int main(int argc,char *argv[]) {
  ProtocolDesc pd;
  protocolIO io;
  const char* remote_host = (strcmp(argv[2], "--")==0?NULL:argv[2]);
  if(!remote_host){
    if(protocolAcceptTcp2P(&pd, argv[1])){  //Alice等待Bob连接
      fprintf(stderr, "TCP accept failed\n");
      exit(1);
    }
  }
  else{
    if(protocolConnectTcp2P(&pd,remote_host,argv[1])!=0){ //Bob主动连接Alice
      fprintf(stderr,"TCP connect failed\n");
      exit(1);
    }
  }
  setCurrentParty(&pd, remote_host?2:1); //设置参与方编号,Alice是1,Bob是2
  sscanf(argv[3],"%d",&io.mywealth);  //这里省略输入合法性检验
  execYaoProtocol(&pd,millionaire,&io); //执行百万富翁比较
  cleanupProtocol(&pd);
  fprintf(stderr,"Result: %d\n",io.cmp);
  return 0;
}

最后创建文件,修改一下上文提到的文件,将其中的值修改为“”即可。

利用上面的镜像,通过以下命令运行Alice方实例:

docker network create obliv-c-net
docker run -it --rm --name Alice --network obliv-c-net `
-v C:\ppct\obliv-c:/root/projects obliv-c
make

提示:上面的代码中因 run运行的参数较多,为方便阅读使用“`”进行换行处理。这是的环境下将命令拆分成多行的特殊字符。在的CMD环境则需要改用“^”字符,在Linux环境则需要改用“\”字符。

另外,为了方便Alice和Bob的两个容器间直接使用机器名进行网络通信,特意创建了obliv-c-net网络用于测试。在实际应用中,两个参与方一般位于不同的机器,直接基于IP或者域名进行通信,不需要创建obliv-c-net网络。

编译成功即可看到同目录下生成了隐私计算执行文件a.out。使用如下命令运行Bob方实例:

docker run -it --rm --name Bob --network obliv-c-net `
-v C:\ppct\obliv-c:/root/projects obliv-c

进入Alice方容器执行以下命令(其中,“1234”为端口号,“--”代表为服务方等待对方连接,“8”代表Alice的财富值):

./a.out 1234 -- 8

进入Bob方容器执行以下命令(其中“1234”为端口号,“Alice”为需要连接的对方服务地址,“15”代表Bob的财富值):

./a.out 1234 Alice 15

最后双方都输出了执行结果-1,即Alice财富值小于Bob。

本文介绍了如何使用混淆电路开源框架Obliv-C解决百万富翁难题。隐私计算技术作为重大科技趋势正逐渐受到政府和企业的重视,更成为商业世界和资本竞逐的热门赛道。了解并掌握隐私计算技术,将成为立足大数据时代的必备技能。如果您想要了解更多有关隐私计算技术的内容,快速了解并上手隐私计算,推荐您详细阅读李伟荣老师的新作《深入浅出隐私计算:技术解析与应用实践》。

关于作者:李伟荣,曾就职于微软、平安、港交所等大型公司,拥有十年以上金融项目架构和信息安全管理经验。精通信息安全、软件研发、项目管理,擅长大型软件架构开发,善于使用创新思维和创新方法解决问题。

曾在港交所深度参与隐私计算相关项目,致力于通过隐私计算技术解决大数据产品的确权、标准化、存证、溯源、定价、信用体系和利益分配等一系列问题,打造数据、金融资产交易的新型基础设施。

视频推荐

评论(0)

二维码