Getting started with oblif

Oblif lets you write data-oblivious programs using non-data-oblivious constructs such as if and while.

Privacy-preserving cryptographic frameworks such as MPyC for multi-party computation and PySNARK for zk-SNARKs allow to write Python code that gets executed in a privacy-preserving way. For this, they make use of oblivious data types, e.g., mpc.SecInt for mpyc and PubVal for PySNARK. However, such frameworks do not allow to branch on such oblivious data types, e.g. to do x = a if mpc.SecInt(...) else b. This is necessary since the code itself is also not allowed to know whether a branch is taken or not. Such code that does not branch on oblivious data is called data-oblivious code.

Oblif makes it possible to write programs for such cryptographic frameworks that does branch over oblivious data types. Oblif provides a Python decorator, oblif.decorator.oblif, that re-writes such code such that it results in a data-oblivious program that performs the same computation (with some caveats, see below). For example, effectively, the following program:

if x==0:
  ret = 1
else
  ret = 0

gets translated into:

ret_if = 1
ret_else = 0
ret = (x==0).if_else(ret_if, ret_else)

Here, if_else is a function of oblivious data types that is provided e.g. by MPyC and PySNARK and that selects the first or the second argument based on whether the oblivious data is 1 or 0. (It does so by computing, in this case, ret_else + (ret_if-ret_else)*(x==0).)

Oblif supports not only if statements, but also for and while loops and oblivious return statements. For example, the following is a full example of a MPyC program using oblif:

from mpyc.runtime import mpc
from oblif.decorator import oblif

@oblif
def fac(x):
    ret=1
    for i in range(2,min(x+1, 10)):
        ret *= i
    return ret

mpc.run(mpc.start())
type=mpc.SecInt()
print("test(5) is", mpc.run(mpc.output(fac(type(5)))))
mpc.run(mpc.shutdown())

Obliv requires a recent version of Python (tested with Python 3.8 and 3.9) and uses the bytecode package (pip install bytecode).

Things you can do

if statements

It is possible to use if statements, including the ternary operator (a if x else b).

while loops

It is possible to use while loops. However, since Oblif does not know the values of data-oblivious value, the loop at some point needs to break based on a non-oblivious value. So the follwing code:

# i is a data-oblivious value
while i<10:
  i+=1

will run forever, since Oblif cannot figure out when to stop. However, the following is OK:

# i is a data-oblivious value
j = 0
while i<10:
  i+=1
  j+=1
  if j==100: break

In this case, the loop will be executed one hundred times, but changes occuring after i<10 was no longer the case, will be ignored.

for loops

It is possible to use for loops over ranges range(x) where x is an oblivious variable. Only positive integer strides are supported.

Since oblif cannot know when the bound is reached, it is necessary to provide an upper bound. There are two approaches. One is to include a break statement in the loop, e.g.:

for i in range(x):   # x is a data-oblivious value
  if x==5: break     # make sure loop executed at most 5 times: can be here ...
  ret = i
  if i==5: break     # ... or here (this is slightly more efficient)

An alternative is to use orange from oblif.iterators, which enables to provide the oblivious maximum and the non-oblivious upper bound as a tuple:

for i in orange((x,10)):   # x is a data-oblivious value; loop is run at most 5 times
  ret = i

For efficiency reasons, oblif does not check whether the initial value is smaller than the oblivious maximum (that would require an oblivious sign computation, which may be more efficient than an oblivious equality check), for example:

x=PrivVal(-1)
for i in orange((x,10)):
  # i never becomes equal to -1, so branch is fully executed
  ret = i
# after branch, ret=9

lambdas

For what it’s worth, oblif works on lambda’s as well, for example:

oblambda = oblif(lambda x: 1 if x==3 else 0)
xisthree = oblambda(x)

Things you cannot do

Skip code

It is important to realize that, if a branch is skipped based on oblivious data, the code is still executed! So for example:

if a: # a is a data-oblivious value
  print("a is true")
  ret = 1
else:
  print("a is false")
  ret = 0
# after this, ret is a data-oblivious value that is either 0 or 1

At the end of this code, ret will be a data-oblivious value that is equal to 0 or 1, as expected. However, both a is true and a is false will be printed! Because data-oblivious code cannot know whether or not the branch is taken, both branches are executed. Oblif just ensures that values from taken branches are preserved and values from non-taken branches are ignored.

Branch on non-binary oblivious data

Oblif uses guard.if_else(..., ...) to select or ignore data-oblivious assignments. Both in MPyC and in PySNARK, for this to work, guard needs to be equal to either 0 or 1. Results for guards that have other values is undefined, for example:

if a: # a is a data-oblivious value 5
  ret = 1
else:
  ret = 0
# branch on non-binary guard, value of ret is undefined

In fact, in most implementations, ret will be computed as elseval+guard*(ifval-elseval) and so in this example will be equal to 5…

Perform in-place operations on mutable objects

As mentioned above, oblif will also execute non-taken branches. As a consequence, in-place modifications to objects will be executed regardless of whether a branch is taken or not, for example:

lst = [1,2,3]
if a: # a is a data-oblivious value
  print(id(lst))  # prints id, executed regardless of a
  lst[1] = 0      # this is an in-place modification, always executed
else:
  print(id(lst))  # prints same id, executed regardless of a
  lst[1] = 4      # this is an in-place modification, always executed
# lst[1] is always equal to 4 regardless of a

In this example, the if and else branches act on the same object. After the branch, lst = a.if_else(lst, lst) will be executed, but this has no effect since lst in both cases refers to the same object.

To still be able to perform operations on a mutable object, it needs to be copied before use, for example:

lst = [1,2,3]
if a: # a is a data-oblivious value
  lst = deepcopy(lst)
  print(id(lst))  # prints id, executed regardless of a
  lst[1] = 0      # this is an in-place modification on the copy of lst
else:
  lst = deepcopy(lst)
  print(id(lst))  # prints different id, executed regardless of a
  lst[1] = 4      # this is an in-place modification on another copy of lst
# lst[1] is 0 or 4 depending on a

(There is some experimental work to automatically copy mutable objects before they are executed in branches. This may be implemented in a future version of oblif.)

Obliviously modify global variables

Oblif only monitors access to local variables, not to globals. Because, as mentioned above, oblif also executes non-taken branches, this means that modifications to global variables in non-taken branches will be executed regardless of their guard, for example:

@oblif def setb(a): # a is a data-oblivious variable
  global b
  if a==1:
    b = 3      # will be executed regardless of whether a==1
  else:
    b = 4      # will be executed regardless of whether a==1
  # end result is that b is a non-oblivious variable equal to 4

Of course, it is possible to set the value of a global variable to an obliviously computed value. The global variable will then become a data-oblivious value equal to the computed value, for example:

@oblif def setb(a): # a is a data-oblivious variable
  global b
  if a==1:
    bval = 3      # will be obliviously set to 3 if a==1
  else:
    bval = 4      # will be obliviously set to 4 if a!=1
  # bval is a data-oblivious variable equal to 3 or 4
  b = bval        # set b to this data-oblivious variable

Access variables that may be undefined

Values set in a branch can only be accessed if they also have a well-defined value in all other branches. For example:

@oblif
def test(x):
    if x==3:
      ret = 1
    return ret   # error: ret has no value if (x==3) does not hold

However, the following is OK:

@oblif
def test(x):
    ret = 0
    if x==3:
      ret = 1
    return ret   # ret has value 0 if (x==3) does not hold