shared_comparison.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. # This module defines comparison between a SharedScalar and public integer,
  2. # using the PrivateCompare algorithm in SecureNN [1].
  3. #
  4. # The notation used here is as close to the paper's as possible.
  5. #
  6. # [1] Algorithm 3 in https://eprint.iacr.org/2018/442.pdf
  7. # Security note:
  8. #
  9. # The PrivateCompare algorithm [1] requires a bitwise share representation.
  10. # However, this is not the share representation of SharedScalars, so we use
  11. # the workaround of reconstructing the private value on a temporarily created
  12. # VirtualMachine, and then resharing with the bitwise representation.
  13. #
  14. # Technically speaking, this isn't really secure. However, it's still useful
  15. # for educational purposes, and enables a nice high-level API like `x > 10`,
  16. # where x is any normal SharedScalar (even the output of an arithmetic op).
  17. #
  18. # I'd like to implement a better solution, eventually. Here are the options:
  19. # 1) Be able to convert from SharedScalar's Shares -> bitwise Shares directly.
  20. # ^I don't know if this is possible.
  21. # 2) Make SharedScalar have two share representations. The normal/current one,
  22. # and a bitwise one. And update all arithmetic operations to support the
  23. # bitwise sharing scheme.
  24. # ^This would add too much complexity.
  25. #
  26. # Alternatively, you can also directly use _share_bitwise() and _private_compare()
  27. # from this module on unshared integers to generate fresh bitwise shares.
  28. # Small hack:
  29. #
  30. # In the other shared_* modules, we use the `type(sh)` hack. However,
  31. # PrivateCompare requires fairly heavy operations on Shares, SharedScalars,
  32. # etc, so we instead import these classes at function runtime.
  33. #
  34. # Personally, I don't like this style, but it's the price to pay for modularity.
  35. # (Dependency-wise, these functions should really be part of tinysmpc.py,
  36. # but it's so much cleaner to split them out.)
  37. from .finite_ring import MIN_INT64
  38. from .secret_sharing import Share
  39. from random import random, randint, shuffle
  40. P = 67 # Smaller prime field size to encode bit values
  41. L = 64 # Number of bits of the integers we're using
  42. def greater_than(x_sh, pub):
  43. '''Provides the high-level API for comparing x_sh (SharedScalar) > pub (int).
  44. This basically does some TinySMPC-specific setup before calling PrivateCompare.'''
  45. assert len(x_sh.owners) == 2, 'PrivateCompare only works for 2-party shares'
  46. # Reconstruct the private value on a temporary VM (see the Security Note above)
  47. from .tinysmpc import VirtualMachine
  48. tmp_vm = VirtualMachine('tmp_vm')
  49. x = x_sh.reconstruct(tmp_vm).value
  50. # The paper's implementation only works on positive numbers, but we want negatives too!
  51. # So, just shift TinySMPC's int64s into the positive range (int64 + -MIN_INT64).
  52. if pub < 0 or x < 0: pub += -MIN_INT64; x += -MIN_INT64
  53. # Decompose x into its bit representation, and share each bit independently
  54. x_sh = _share_bitwise(x, list(x_sh.owners))
  55. return _private_compare(x_sh, pub)
  56. def _private_compare(x_sh, r, β=None):
  57. '''Compares x_sh > r, where x_sh is bitwise shared and r is a public integer.
  58. Returns 0 or 1 as a PrivateScalar on a temporary VirtualMachine.
  59. This is the PrivateCompare algorithm in [1].'''
  60. # A necessary evil; see the "small hack" note above
  61. from .tinysmpc import PrivateScalar, SharedScalar, VirtualMachine
  62. # Decompose r into its bit representation (public)
  63. rb = _get_bits(r)
  64. # Common randomness (public)
  65. β = randint(0, 1) if β is None else β
  66. s = _randlist()
  67. u = _randlist()
  68. π = _fixed_shuffle()
  69. # Line 1
  70. t = (r + 1) % 2**L
  71. tb = _get_bits(t)
  72. # Line 2
  73. p0, p1 = tuple(x_sh[0].owners)
  74. w_c = {p0: {'w': [None] * L, 'c': [None] * L},
  75. p1: {'w': [None] * L, 'c': [None] * L}}
  76. for j, machine in enumerate([p0, p1]):
  77. w, c = w_c[machine]['w'], w_c[machine]['c']
  78. # Line 3
  79. for i in range(L-1, -1, -1):
  80. sh = x_sh[i].share_of[machine]
  81. # Line 4
  82. if β == 0:
  83. w[i] = sh + j*rb[i] - 2*rb[i]*sh
  84. c[i] = j*rb[i] - sh + j + sum(w[i+1:])
  85. # Line 7
  86. elif (β == 1) and (r != 2**L - 1):
  87. w[i] = sh + j*tb[i] - 2*tb[i]*sh
  88. c[i] = -1*j*tb[i] + sh + j + sum(w[i+1:])
  89. # Line 10
  90. else:
  91. if i != 1: c_val = ((1 - j)*(u[i] + 1) - j*u[i]) % P
  92. else: c_val = ((-1)**j * u[i]) % P
  93. c[i] = Share(c_val, machine, Q=P)
  94. # Line 14
  95. d_p0 = [s[i] * w_c[p0]['c'][i] for i in range(L)]
  96. d_p1 = [s[i] * w_c[p1]['c'][i] for i in range(L)]
  97. π(d_p0); π(d_p1)
  98. d_shared = [SharedScalar([d0, d1], Q=P) for d0, d1 in zip(d_p0, d_p1)]
  99. # Line 15
  100. p2 = VirtualMachine('p2')
  101. d = [d_sh.reconstruct(p2) for d_sh in d_shared]
  102. β_prime = any(ps.value == 0 for ps in d) # (we break the abstraction of only operating on PrivateScalars a bit)
  103. # Return x > r
  104. return PrivateScalar(β ^ β_prime, p2)
  105. def _share_bitwise(n, machines):
  106. '''Split integer n into bitwise secret shares, returns a list of SharedScalars (one per bit).'''
  107. from .tinysmpc import PrivateScalar
  108. bits = _get_bits(n)
  109. ps_bits = [PrivateScalar(bit, machines[0]) for bit in bits]
  110. sh_bits = [ps_bit.share(machines, P) for ps_bit in ps_bits]
  111. return sh_bits
  112. def _get_bits(n):
  113. '''Returns the (reverse) binary representation of n as an L-sized list.'''
  114. bits = bin(n).replace('0b', '')
  115. bits = '0' * (L - len(bits)) + bits
  116. return list(map(int, reversed(bits))) # FYI: the paper requires reversed binary, but doesn't say this!
  117. def _randlist():
  118. '''Returns a list of L random integers in [1, P-1].'''
  119. return [randint(1, P-1) for _ in range(L)]
  120. def _fixed_shuffle():
  121. '''Returns a deterministic shuffle function that always permutes a list in the same way.'''
  122. seed = random()
  123. return lambda x: shuffle(x, lambda: seed)