3.2 开发框架Obliv-C
Obliv-C是美国弗吉尼亚大学安全研究小组的研究项目,简单实用。它是一款GCC包装器,该框架开发者在C语言基础上进行了一定的类C语言处理,添加了一些规则限制来实现混淆电路。Obliv-C支持双方的半诚实安全模型,源码采用商业友好的BSD许可证,并公开在GitHub中(https://github.com/samee/obliv-c)。
3.2.1 通过Docker构建环境
为了方便读者快速使用,这里列出用于构建Obliv-C运行环境的Docker镜像代码供读者参考,如代码清单3-1所示。
代码清单3-1 用于构建Obliv-C运行环境的Docker镜像代码
FROM ubuntu:20.04 WORKDIR /root RUN apt-get update && apt-get install -y \ ocaml \ libgcrypt20-dev \ libgmp-dev \ ocaml-findlib \ opam \ m4 \ git \ vim \ gcc \ make RUN git clone https://github.com/samee/obliv-c #如果访问GitHub速度慢,可以使用这个加速地址 #RUN git clone https://github.com.cnpmjs.org/samee/obliv-c WORKDIR /root/obliv-c RUN opam init -a --disable-sandboxing && \ opam switch create 4.06.0 && \ opam switch 4.06.0 && \ eval `opam config env` && \ opam install -y camlp4 ocamlfind batteries bignum ocamlbuild && \ ./configure && make #可以将宿主机中的代码挂载到容器的projects目录,使用容器进行编译和运行 VOLUME ["/root/projects"] WORKDIR /root/projects #为了后续测试方便,特此安装一些网络工具 RUN apt-get update && \ apt-get install -y iputils-ping && \ apt-get install -y telnet && \ apt-get install -y net-tools && \ apt-get install -y tcpdump
其实,不使用Docker,直接在Ubuntu系统中安装也非常简单,只需安装相应的依赖、下载源代码并编译即可。Obliv-C在GitHub的开源项目中有相关安装说明,读者可以根据代码清单3-1推断出安装步骤,这里不再赘述。
注意
在构建Docker镜像时如果访问GitHub速度慢或者网络连接不稳定,可以使用代理地址https://github.com.cnpmjs.org。读者如果有能稳定连接GitHub的网络,还是建议使用GitHub官网地址。
然后使用如下命令编译Docker镜像:
docker build -t obliv-c
接下来介绍一下Obliv-C的编程语法和相关规则。
3.2.2 使用obliv修饰隐私输入数据
任何依赖隐私输入(指只有数据拥有方才知道其具体值,不对其他参与方公开。下面统一以“隐私输入数据”代指此类数据)的变量都应该使用obliv修饰符来声明。比如下面声明的函数中变量a依赖隐私输入数据,而变量b是各方都知晓的公开数据,返回结果也依赖隐私输入数据,需要使用obliv来修饰:
obliv bool compare (obliv int a, int b) { return a < b; }
使用obliv修饰符修饰的相关规则如下。
规则1:只有C语言中的基础类型可以使用obliv进行修饰,比如int、char、float等。注意,struct和指针也是不被支持的,但是struct中包含obliv字段或者指针指向obliv变量是可以支持的。另外,函数也是可以用obliv修饰符修饰的,这部分会在下面进一步说明。
规则2:任何由obliv变量和非obliv变量组合而成的表达式最终也被视为obliv变量。
规则3:非obliv变量可以隐式地转换成obliv变量,但反过来只能是在各方同意调用revealObliv系列函数时才可以,如代码清单3-2所示。
代码清单3-2 revealOblivInt使用示例
int a=50, b; obliv int c; c=a; // 可以,非obliv变量a可以隐式地转换成obliv变量b b=c; // 不可以,obliv变量c不可以直接转换成非obliv变量 revealOblivInt (&b, c, 0); // 可以,使用revealObliv函数公开
使用revealObliv函数后,变量b就是一个普通的整型数,值与变量c的值相同。上面代码中revealOblivInt中的第三个参数用来指定公开变量的接收方,如果传入0则表示各参与方都会收到变量c的备份并赋值到变量b;如果传入1,那么只有1号参与方才能收到变量c的备份,而其他参与方只能收到一个默认值0。这里的参与方编号是各参与方通过调用setCurrentParty函数进行设定的,比如代码清单3-3根据程序运行时传入的命令行参数将两个参与方分别设为1号和2号。
代码清单3-3 setCurrentParty使用示例
ProtocolDesc pd; const char* remote_host=(strcmp(argv[2], "--")==0?NULL:argv[2]); setCurrentParty(&pd,remote_host?2:1); //设置自己的编号
顾名思义,revealOblivInt是用于公开整型数的函数。根据需要公开的变量类型的不同,revealObliv系列函数可分为revealOblivBool、revealOblivChar、revealOblivShort、revealOblivLong、revealOblivLLong、revealOblivFloat、revealOblivBoolArray、revealOblivCharArray、revealOblivIntArray、revealOblivShortArray、revealOblivLongArray、revealOblivLLongArray。
3.2.3 提供隐私输入数据
那么,参与方如何提供输入数据呢?Obliv-C提供了一系列函数。以整型数为例,我们可以通过feedOblivInt函数将参与方本地的明文整型数转化成obliv int:
obliv int feedOblivInt (int value, int p) //value:明文整型数,p:参与方编号
feedOblivInt被调用时只会加载本方数据,如果编号p与执行方编号不同,则该函数会被忽略。类似地,根据变量类型的不同,feedObliv系列函数可分为feedOblivBool、feedOblivChar、feedOblivShort、feedOblivLong、feedOblivLLong、feedOblivFloat、feedOblivBoolArray、feedOblivCharArray、feedOblivIntArray、feedOblivShortArray、feedOblivLongArray、feedOblivLLongArray。
3.2.4 计算过程中的流程控制
正如上面所述,只有在调用revealObliv系列函数后,才能揭示隐私输入数据具体值。任何在计算过程中出现的中间状态也都是对各参与方隐藏的,这样,类似while、for等循环流程控制就会无法使用obliv变量。
提示
因为每一个参与方都要执行Obliv-C编制的混淆电路的协议,各参与方都知道循环执行所花的时间以及循环中迭代执行的次数,所以如果框架允许在循环流程控制中使用obliv变量,就会导致数据泄露。在其他一些隐私计算框架中,这个限制往往也存在。
规则4:Obliv-C不支持任何obliv变量被用到类似for、while等循环流程控制语句中。
但是,例外的是Obliv-C支持obliv if,其语法结构与普通的if语句非常类似,如代码清单3-4所示。
代码清单3-4 obliv if语法结构
obliv if (…) { … } else obliv if (…) { … } else { … }
这里的if条件判断中允许使用obliv变量。然而,需要特别注意的是,在执行的时候任何一个参与方都无法获知其条件判断语句是true还是false。不论其条件是否为true,Obliv-C都会执行相应的代码块。比如下面这段代码:
obliv int x, y; … obliv if (x < 0) y=10;
这里不论x是正数还是负数,Obliv-C都会执行一段代码,同时确保任何参与方都无法知晓y的值是否发生了变化。如果x为负数,y将被修改为10。如果x不为负数,y值不变。y是obliv变量,任何一个参与方都不会知晓其具体的值。
区别于普通的if语句,obliv if有一些特殊的限制。
规则5:不能对在obliv if语句块声明之外的非obliv变量进行赋值(因为这可能导致信息通过非obliv变量泄露出去),但是对在其声明之内的非obliv变量进行赋值是合法的。
非obliv变量在obliv if语句块中的赋值示例如代码清单3-5所示。
代码清单3-5 非obliv变量在obliv if语句块中的赋值示例
obliv int x; int y=10; obliv if (x > 0) y=20; //非法,y不是obliv变量,不能在obliv if语句块中被赋值 obliv if (x > 0) { //合法,非obliv变量i在obliv if语句块中被声明 for (int i=0; i<10; i++) {…} }
规则6:不能在obliv if语句块中执行普通的函数(防止普通函数执行时泄露信息),只能执行obliv函数。
上面提到Obliv-C不支持任何obliv变量被用到类似for、while等循环流程控制语句中。但是,Obliv-C支持在循环体内执行obliv if语句,因此,假设有obliv变量n,下面的循环语句:
for (i=0; i < n; i++) {…}
可以改写成如下的形式:
for (i=0; i < MAX_BOUND; i++) { obliv if (i < n) {…} }
显然,通过上面的改写,for循环会固定迭代MAX_BOUND次,不会泄露obliv变量n的信息。
3.2.5 obliv函数
obliv函数声明方式如下:
void func() obliv {…}
规则7:非obliv函数不能在obliv if或者obliv函数体内被调用。
规则8:obliv函数内部不能对在其之外声明的非obliv变量进行赋值、修改。
另外,对于函数引用传参,我们也需要注意。比如下面func函数被调用时,p1指针的指向是有限制的(p2是常量指针,不用担心其泄露数据,所以可以引用外部变量),p1指针只能指向obliv if语句块内声明的变量,如代码清单3-6所示。
代码清单3-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); }
3.2.6 对数组的访问
规则9:obliv变量不能被用于数组索引(Obliv-C开发者认为虽然可以实现,但性能太差)、指针偏移量或者表示数字移位运算的移位次数中。需要注意的是,普通整型数是可以被用在obliv数组索引中的。
那么,如果需要根据obliv变量对数组进行访问,该如何处理呢?Obliv-C开发者也给出了解决方法,如代码清单3-7所示。
代码清单3-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)。
3.2.7 关键词frozen
Obliv-C引入的关键词frozen对变量进行修饰,其含义与const含义类似。其引入原因主要是考虑到struct类型在某些场景下需要使用深度常类型(deep-const),比如代码清单3-8所示的场景,frozen关键词的作用被递归应用到了变量b内的所有指针,从而保证变量b的内部指针p不会被赋值。
代码清单3-8 关键词frozen使用示例
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内的所有指针 }
对于struct、union以及指向指针的指针等,关键词const和frozen存在差异,比如int **frozen与const int *const *const相同,而与const int**或者int **const不同。
规则10:通常情况下,任何非obliv变量在进入obliv作用域(obliv-if或者obliv函数)时,都可以视作被frozen修饰符修饰。
这个规则也比较好理解,因为如果obliv作用域的非obliv变量不是被视作被frozen修饰,信息就有可能通过给非obliv变量赋值的方式泄露出去。
规则11:对于任何类型T,frozen指针T *frozen 的解引用获得的是一个T frozen类型的左值。
规则12:对于obliv数据,frozen修饰符会被忽略。
3.2.8 高级功能:无条件代码段
无条件代码段是指在obliv条件代码段中拆分出一块代码段进行无条件执行,它是Obliv-C与C区别最大之处,如代码清单3-9所示。
代码清单3-9 无条件代码段的示例
int x=10; obliv int y; obliv if (y > 0) { x=15; //非法,不能在obliv作用域修改非obliv变量x ~obliv (c){ //开启无条件代码段 x=15; //合法,c即使为false,赋值仍然会发生 } }
规则13:无条件代码段的执行不依赖任何obliv变量值,frozen变量的限制在无条件代码段中不再生效。
但是,这里需要注意,在上面第3行代码中,即使y不是正数,无条件代码段仍然会被执行。一般情况,~obliv (varname)语法中也声明了一个obliv布尔变量varname。该变量varname可被用在无条件代码段内部的obliv if条件判断上。示例如代码清单3-10所示。
代码清单3-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; } }
3.2.9 Obliv-C项目的文件结构
在基本了解Obliv-C的语法后,我们以求向量内积的案例为例,进一步了解使用Obliv-C进行编程时的项目基本结构。读者在刚开始接触Obliv-C项目时,建议通过src/ext/oblivc目录下的obliv.oh、obliv.h文件来了解Obliv-C提供的接口,然后通过进一步阅读test/oblivc目录下的几个测试案例熟悉接口的使用方法。
实现求向量内积共需4个文件,即innerProd.c、innerProd.h、innerProd.oc、Makefile。接下来,我们来看一下每个文件的作用。
1. innerProd.h文件
编程规则与C语言完全相同,本文件中的程序用于声明函数、混淆电路相关结构体(protocolIO)以及参与混淆计算的全部参数(包括各方的隐私输入以及最后的共享结果)。特别地,各方的隐私输入可以定义为同一变量,也可以定义为不同变量。对应的代码如代码清单3-11所示。
代码清单3-11 求向量内积的innerProd.h文件脚本
#pragma once #include<obliv.h> void dotProd(void *args); //混淆计算函数的声明,具体的函数定义见innerProd.oc typedef struct vector{ int size; int* arr; } vector; typedef struct protocolIO{ vector input; int result; } protocolIO; //包含了混淆电路输入和输出的相关结构体
注意
对于protocolIO结构体,其中的变量不能使用指针,需要用数组,否则编译可能不会报错,但最终运行结果错误。
2. innerProd.c文件
编程规则与C语言完全相同,本文件中的程序用于获取命令行参数、设置混淆电路环境、输出混淆计算结果等。其主要执行顺序如下。
1)获取并校验命令行参数。
2)与另一个参与方进行网络连接,如代码清单3-12所示。
代码清单3-12 innerProd.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); } }
提示
protocolAcceptTcp2P和protocolConnectTcp2P是Obliv-C提供的为两个隐私计算参与方建立连接的接口。
3)设置自己的编号。
int currentParty=remote_host?2:1; setCurrentParty(&pd, currentParty); //两个参与方分别设置自己的编号
4)从文件中读取向量内容,如代码清单3-13所示。
代码清单3-13 innerProd.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<v.size; i++){ //从文件中读取向量值 if(fscanf(file, "%d\n", &(v.arr[i]))==EOF){ return 2; } }
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. innerProd.oc文件
编程规则与C语言类似,本文件中的程序用于定义混淆计算函数(即本例中的dotProd函数),相关语法在前面几节已有描述。对应的代码如代码清单3-14所示。
代码清单3-14 求向量内积的innerProd.oc文件脚本
#include<obliv.oh> #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=v1Size<v2Size?v1Size:v2Size; //如果两方向量长度不同,以小的为准 obliv int sum=0; for(int i=0; i<vMinSize; i++){ sum +=v1[i]*v2[i]; } revealOblivInt(&(io->result), sum, 0); //揭示计算结果 }
提示
在上面的代码中,ocBroadcastInt函数用于将非obliv数据传给其他参与方。类似的还有ocBroadcastFloat等函数。
4. Makefile文件
本文件中的程序用于编译。Makefile文件中的编译程序只需在对应的文件目录下打开命令行终端,输入make后按回车键即可执行。编译成功后产生一个a.out可执行程序文件。对应的代码如代码清单3-15所示。
代码清单3-15 求向量内积的Makefile文件脚本
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项目的文件结构有了基本的了解,接下来就可以尝试一个小的应用案例了。