|
|
@@ -0,0 +1,148 @@
|
|
|
+# This module defines comparison between a SharedScalar and public integer,
|
|
|
+# using the PrivateCompare algorithm in SecureNN [1].
|
|
|
+#
|
|
|
+# The notation used here is as close to the paper's as possible.
|
|
|
+#
|
|
|
+# [1] Algorithm 3 in https://eprint.iacr.org/2018/442.pdf
|
|
|
+
|
|
|
+# Security note:
|
|
|
+#
|
|
|
+# The PrivateCompare algorithm [1] requires a bitwise share representation.
|
|
|
+# However, this is not the share representation of SharedScalars, so we use
|
|
|
+# the workaround of reconstructing the private value on a temporarily created
|
|
|
+# VirtualMachine, and then resharing with the bitwise representation.
|
|
|
+#
|
|
|
+# Technically speaking, this isn't really secure. However, it's still useful
|
|
|
+# for educational purposes, and enables a nice high-level API like `x > 10`,
|
|
|
+# where x is any normal SharedScalar (even the output of an arithmetic op).
|
|
|
+#
|
|
|
+# I'd like to implement a better solution, eventually. Here are the options:
|
|
|
+# 1) Be able to convert from SharedScalar's Shares -> bitwise Shares directly.
|
|
|
+# ^I don't know if this is possible.
|
|
|
+# 2) Make SharedScalar have two share representations. The normal/current one,
|
|
|
+# and a bitwise one. And update all arithmetic operations to support the
|
|
|
+# bitwise sharing scheme.
|
|
|
+# ^This would add too much complexity.
|
|
|
+#
|
|
|
+# Alternatively, you can also directly use _share_bitwise() and _private_compare()
|
|
|
+# from this module on unshared integers to generate fresh bitwise shares.
|
|
|
+
|
|
|
+# Small hack:
|
|
|
+#
|
|
|
+# In the other shared_* modules, we use the `type(sh)` hack. However,
|
|
|
+# PrivateCompare requires fairly heavy operations on Shares, SharedScalars,
|
|
|
+# etc, so we instead import these classes at function runtime.
|
|
|
+#
|
|
|
+# Personally, I don't like this style, but it's the price to pay for modularity.
|
|
|
+# (Dependency-wise, these functions should really be part of tinysmpc.py,
|
|
|
+# but it's so much cleaner to split them out.)
|
|
|
+
|
|
|
+from .finite_ring import MIN_INT64
|
|
|
+from .secret_sharing import Share
|
|
|
+from random import random, randint, shuffle
|
|
|
+
|
|
|
+P = 67 # Smaller prime field size to encode bit values
|
|
|
+L = 64 # Number of bits of the integers we're using
|
|
|
+
|
|
|
+def greater_than(x_sh, pub):
|
|
|
+ '''Provides the high-level API for comparing x_sh (SharedScalar) > pub (int).
|
|
|
+ This basically does some TinySMPC-specific setup before calling PrivateCompare.'''
|
|
|
+ assert len(x_sh.owners) == 2, 'PrivateCompare only works for 2-party shares'
|
|
|
+
|
|
|
+ # Reconstruct the private value on a temporary VM (see the Security Note above)
|
|
|
+ from .tinysmpc import VirtualMachine
|
|
|
+ tmp_vm = VirtualMachine('tmp_vm')
|
|
|
+ x = x_sh.reconstruct(tmp_vm).value
|
|
|
+
|
|
|
+ # The paper's implementation only works on positive numbers, but we want negatives too!
|
|
|
+ # So, just shift TinySMPC's int64s into the positive range (int64 + -MIN_INT64).
|
|
|
+ if pub < 0 or x < 0: pub += -MIN_INT64; x += -MIN_INT64
|
|
|
+
|
|
|
+ # Decompose x into its bit representation, and share each bit independently
|
|
|
+ x_sh = _share_bitwise(x, list(x_sh.owners))
|
|
|
+
|
|
|
+ return _private_compare(x_sh, pub)
|
|
|
+
|
|
|
+def _private_compare(x_sh, r, β=None):
|
|
|
+ '''Compares x_sh > r, where x_sh is bitwise shared and r is a public integer.
|
|
|
+ Returns 0 or 1 as a PrivateScalar on a temporary VirtualMachine.
|
|
|
+ This is the PrivateCompare algorithm in [1].'''
|
|
|
+ # A necessary evil; see the "small hack" note above
|
|
|
+ from .tinysmpc import PrivateScalar, SharedScalar, VirtualMachine
|
|
|
+
|
|
|
+ # Decompose r into its bit representation (public)
|
|
|
+ rb = _get_bits(r)
|
|
|
+
|
|
|
+ # Common randomness (public)
|
|
|
+ β = randint(0, 1) if β is None else β
|
|
|
+ s = _randlist()
|
|
|
+ u = _randlist()
|
|
|
+ π = _fixed_shuffle()
|
|
|
+
|
|
|
+ # Line 1
|
|
|
+ t = (r + 1) % 2**L
|
|
|
+ tb = _get_bits(t)
|
|
|
+
|
|
|
+ # Line 2
|
|
|
+ p0, p1 = tuple(x_sh[0].owners)
|
|
|
+ w_c = {p0: {'w': [None] * L, 'c': [None] * L},
|
|
|
+ p1: {'w': [None] * L, 'c': [None] * L}}
|
|
|
+ for j, machine in enumerate([p0, p1]):
|
|
|
+ w, c = w_c[machine]['w'], w_c[machine]['c']
|
|
|
+
|
|
|
+ # Line 3
|
|
|
+ for i in range(L-1, -1, -1):
|
|
|
+ sh = x_sh[i].share_of[machine]
|
|
|
+
|
|
|
+ # Line 4
|
|
|
+ if β == 0:
|
|
|
+ w[i] = sh + j*rb[i] - 2*rb[i]*sh
|
|
|
+ c[i] = j*rb[i] - sh + j + sum(w[i+1:])
|
|
|
+
|
|
|
+ # Line 7
|
|
|
+ elif (β == 1) and (r != 2**L - 1):
|
|
|
+ w[i] = sh + j*tb[i] - 2*tb[i]*sh
|
|
|
+ c[i] = -1*j*tb[i] + sh + j + sum(w[i+1:])
|
|
|
+
|
|
|
+ # Line 10
|
|
|
+ else:
|
|
|
+ if i != 1: c_val = ((1 - j)*(u[i] + 1) - j*u[i]) % P
|
|
|
+ else: c_val = ((-1)**j * u[i]) % P
|
|
|
+ c[i] = Share(c_val, machine, Q=P)
|
|
|
+
|
|
|
+ # Line 14
|
|
|
+ d_p0 = [s[i] * w_c[p0]['c'][i] for i in range(L)]
|
|
|
+ d_p1 = [s[i] * w_c[p1]['c'][i] for i in range(L)]
|
|
|
+ π(d_p0); π(d_p1)
|
|
|
+ d_shared = [SharedScalar([d0, d1], Q=P) for d0, d1 in zip(d_p0, d_p1)]
|
|
|
+
|
|
|
+ # Line 15
|
|
|
+ p2 = VirtualMachine('p2')
|
|
|
+ d = [d_sh.reconstruct(p2) for d_sh in d_shared]
|
|
|
+ β_prime = any(ps.value == 0 for ps in d) # (we break the abstraction of only operating on PrivateScalars a bit)
|
|
|
+
|
|
|
+ # Return x > r
|
|
|
+ return PrivateScalar(β ^ β_prime, p2)
|
|
|
+
|
|
|
+def _share_bitwise(n, machines):
|
|
|
+ '''Split integer n into bitwise secret shares, returns a list of SharedScalars (one per bit).'''
|
|
|
+ from .tinysmpc import PrivateScalar
|
|
|
+ bits = _get_bits(n)
|
|
|
+ ps_bits = [PrivateScalar(bit, machines[0]) for bit in bits]
|
|
|
+ sh_bits = [ps_bit.share(machines, P) for ps_bit in ps_bits]
|
|
|
+ return sh_bits
|
|
|
+
|
|
|
+def _get_bits(n):
|
|
|
+ '''Returns the (reverse) binary representation of n as an L-sized list.'''
|
|
|
+ bits = bin(n).replace('0b', '')
|
|
|
+ bits = '0' * (L - len(bits)) + bits
|
|
|
+ return list(map(int, reversed(bits))) # FYI: the paper requires reversed binary, but doesn't say this!
|
|
|
+
|
|
|
+def _randlist():
|
|
|
+ '''Returns a list of L random integers in [1, P-1].'''
|
|
|
+ return [randint(1, P-1) for _ in range(L)]
|
|
|
+
|
|
|
+def _fixed_shuffle():
|
|
|
+ '''Returns a deterministic shuffle function that always permutes a list in the same way.'''
|
|
|
+ seed = random()
|
|
|
+ return lambda x: shuffle(x, lambda: seed)
|