Obliv-C Tutorial
Obliv-C is a simple GCC wrapper that makes it easy to embed secure computation protocols inside regular C programs. This tutorial explains how to set up and build your first Obliv-C program using privacy-preserving linear regression application as an example.
Installation
1. Install Dependencies
Before setting up Obliv-C, several other modules need to be installed. Here are the commands to do the installations, depending on your platform.
Ubuntu:
$ sudo apt install ocaml libgcrypt20-dev ocaml-findlib
Fedora:
$ sudo dnf install glibc-devel.i686 ocaml ocaml-ocamldoc ocaml-findlib ocaml-findlib-devel libgcrypt libgcrypt-devel perl-ExtUtils-MakeMaker perl-Data-Dumper
Mac OS (with Macports):
$ sudo port install gcc5 ocaml ocaml-findlib libgcrypt +devel
2. Cloning Obliv-C
Clone the Obliv-C source repository:
$ git clone https://github.com/samee/obliv-c
3. Build Obliv-C
Linux:
$ ./configure && make
Mac OS:
First, you need to install MacPorts. Then,
$ CC=/opt/local/bin/gcc-mp-5 CPP=/opt/local/bin/cpp-mp-5 LIBRARY_PATH=/opt/local/lib ./configure && make
You’re all set!
The compiler is a GCC wrapper script found in bin/oblivcc
. Example programs are in test/oblivc
.
Understanding Obliv-C Programs
Here, we explain how an Obliv-C program works in four steps. For this tutorial, we implement a sample linear regression in Obliv-C: https://github.com/samee/obliv-c/tree/obliv-c/test/oblivc/linreg.
This example is for instructional purposes only, and does not take advantage of all features of Obliv-C and favors simplicity over performance.
1. Connecting parties, storing arguments, and executing Yao’s protocol
The protocol is started in main()
in
linreg.c. The
#include <obliv.h>
header is needed to incorporate the Obliv-C library.
The first step to creating a protocol is to initialize a
ProtocolDesc
object, stored in the pd
variable in this code. This
is needed as an argument for several essential functions.
After storing the command-line arguments (hostname, TCP port, party,
and data filename) to protocolIO io
(a struct defined in
linreg.h),
a connection must be established between the two parties. The
ProtocolDesc
structure is a convenient way to represent
data used in both your C and Obliv-C files.
Connecting Parties
// code ...
const char *remote_host = strtok(argv[1], ":"); // parse hostname from cmdline
const char *port = strtok(NULL, ":"); // parse port name from cmdline
ProtocolDesc pd;
protocolIO io;
// code ...
log_info("Connecting to %s on port %s ...\n", remote_host, port);
if(argv[2][0] == '1') {
if(protocolAcceptTcp2P(&pd,port)!=0) {
log_err("TCP accept from %s failed\n", remote_host);
exit(1);
}
} else {
if(protocolConnectTcp2P(&pd,remote_host,port)!=0) {
log_err("TCP connect to %s failed\n", remote_host);
exit(1);
}
}
// code ...
cp = (argv[2][0]=='1'? 1 : 2);
setCurrentParty(&pd, cp); // only checks for a '1'
io.src = argv[3]; // filename
The first party calls protocolAcceptTcp2P()
to listen for a connection. The
log_err()
function is defined for this example in dbg.h.
The important takeaway from the above code snippet is that the accept
and connect functions were called by the appropriate parties). Then,
the second party calls protocolConnectTcp2P()
, supplying the
hostname from the commandline argument and connecting to the first
party. After a connection is made, setCurrentParty()
is called,
which allows the ProtocolDesc pd
to keep track of parties.
Executing Yao’s Protocol
// Execute Yao protocol and cleanup
execYaoProtocol(&pd, linReg, &io);
cleanupProtocol(&pd);
Now, execYaoProtocol()
is called; this function begins linReg.oc’s code at the supplied function name: linReg().
2. Loading local data and sharing obliv qualified data
Now, we switch to the Olbiv-C code in linreg.oc
. Code in the .oc
files can use the obliv
extensions, and it source-to-source translacted to C code as part of the Obliv-C compilation process.
Loading local data and obtaining protocolIO
struct values in Obliv-C
is done inside the void linReg()
function, called by
execYaoProtocol()
at the end of step 1:
protocolIO *io = (protocolIO*) args;
int *x = malloc(sizeof(int) * ALLOC);
int *y = malloc(sizeof(int) * ALLOC);
// code ...
load_data(io, &x, &y, ocCurrentParty());
Notice that the struct used for accessing variables across C and Obliv-C files (protocolIO io
)
is obtained from the call to linReg()
by execYaoProtocol()
.
To load the data points (the private input) from local files for each
party, two int arrays were created with ALLOC
being an initial size
defined in
linreg.h
.
The load_data()
function from
linreg.c
is called, with the int arrays created in the Obliv-C file being
passed in as parameters.
Note that calls can be
made to C file functions even while being in Yao’s protocol and
running code in an Obliv-C file. The majority of the load_data()
function is comprised of regular C procedures, with the exception of
this code snippet:
float a_float = (float) a;
if (party == 1) {
*(*x + io->n - 1) = a_float; // messy, but needed for dereferencing
} else if (party == 2) {
*(*y + io->n - 1) = a_float;
}
Sharing obliv
data
Now that the data for each party is stored locally in float arrays,
the data must be made oblivious (converted to obliv
-typed data) and
then shared with the other party as private data:
// code ...
check_input_count(io);
obliv float *ox = malloc(sizeof(obliv float) * io->n);
obliv float *oy = malloc(sizeof(obliv float) * io->n);
toObliv(io, ox, x, 1);
toObliv(io, oy, y, 2);
Two new float arrays that are obliv
qualified are created, with size
io->n
after loading the local data (note that the call to
check_input_count()
ensures both parties sent the same amount of
data using the ocBroadcastFloat()
function). Then, toObliv()
is
called:
// code ...
void toObliv(protocolIO *io, obliv float *oa, float *a, int party)
{
for(int i = 0; i < io->n; i++) {
oa[i] = feedOblivFloat(a[i], party);
}
}
The call to feedOblivFloat()
(see documentation)
allows both parties to share their data over their network connection
as obliv
qualified values.
The loop in toObliv()
goes through each value in each party’s
respective array. You may notice that while both parties call
toObliv()
twice, the party from which the local float array is
selected is hardcoded into the function call, so as to prevent party 1
from trying to convert its own y array data (which are all 0 because
nothing was ever stored to them) into obliv
values, and vice versa.
3. Computing linear regression
Computing the function of two parties (linear regression)
Our data has been shared obliviously into two arrays, and we are now
ready to perform our joint function on our private data (linear
regression). The code is very similar to what would be done in a
non-private implementation using standard C, except for the use of
obliv
type qualifiers for the oblivious data.
// code ...
int n = io->n;
obliv float sumx = sum(ox, n); // sum of x
obliv float sumxx = dotProd(ox, ox, n); // sum of each x squared
obliv float sumy = sum(oy, n); // sum of y
obliv float sumyy = dotProd(oy, oy, n); // sum of each y squared
obliv float sumxy = dotProd(ox, oy, n); // sum of each x * y
// Compute least-squares best fit straight line
om = (n * sumxy - (sumx * sumy)) / (n * sumxx - osqr(sumx)); // slope
ob = ((sumy * sumxx) - (sumx * sumxy)) / (n * sumxx - osqr(sumx)); // y-intercept
obliv float ocov = (n * sumxy) - (sumx * sumy);
obliv float ostddevs = ((n * sumxx) - osqr(sumx)) * ((n * sumyy) - osqr(sumy));
obliv float orsqr = osqr(ocov) * ostddevs; // sqrt(revealed rsqr) = r
The sum()
and dotProd()
function allow us to obtain summations
that we need in order to compute our three (oblivious) results: om
(slope),
ob
(obliv y-intercept), and orsqr
(correlation squared).
We then use these summations to calculate our results, which will be revealed at the end of the protocol execution.
4. Revealing results, cleaning up
Revealing Results
The revealOblivFloat
function is used to reveal the oblivious
results to both parties. The first parameter is the location where the
non-obliv result value is to be stored, a field in the protocolIO io
struct. The second parameter is the obliv
result that will be revealed and stored into that location. Lastly, ‘0’ specifies that all parties
will receive the result (alternatively a non-zero value could be used to reveal the result to just one of the paries).
revealOblivFloat(&io->rsqr, orsqr, 0);
revealOblivFloat(&io->m, om, 0);
revealOblivFloat(&io->b, ob, 0);
Cleaning up, recording runtime information
Having stored the results as non-obliv floats in the io
struct, we are now ready
to complete Yao’s protocol and print our results to the Terminal.
After freeing our float
arrays and obliv float
arrays, linReg()
finishes,
and as a result execYaoProtocol()
, from
linreg.c,
completes execution as well.
// Execute Yao's protocol and cleanup
execYaoProtocol(&pd, linReg, &io);
cleanupProtocol(&pd);
// code ...
log_info("Slope \tm = %15.6e\n", io.m); // print slope
log_info("y-intercept\tb = %15.6e\n", io.b); // print y-intercept
log_info("Correlation\tr = %15.6e\n", sqrt(io.rsqr)); // print correlation
To conclude, cleanupProtocol()
, serving a similar purpose to use of free()
after calling malloc()
on a given variable.
(Another useful built in function
to Obliv-C is yaoGateCount()
, which gives the number gates executed in the protocol). Finally, we print our results from the linear regression.
Conclusion
We hope that this tutorial of how to implement linear regression in Obliv-C will be a useful guide for starting to write your own Obliv-C programs! You’ll find many more example Obliv-C programs in the repository as well as the other repositories linked for other Obliv-C Projects.
Please let us know if you have any suggestions for improving Obliv-C or this tutorial. We also welcome issues and pull requests to the Obliv-C repository and deeply appreciate hearing about your experiences building applications with Obliv-C.