深入浅出隐私计算:技术解析与应用实践
上QQ阅读APP看书,第一时间看更新

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项目的文件结构有了基本的了解,接下来就可以尝试一个小的应用案例了。