| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- # 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)
|