ellswift.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2022-present The Bitcoin Core developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  """Test-only Elligator Swift implementation
   6  
   7  WARNING: This code is slow and uses bad randomness.
   8  Do not use for anything but tests."""
   9  
  10  import csv
  11  import os
  12  import random
  13  import unittest
  14  
  15  from test_framework.crypto.secp256k1 import FE, G, GE
  16  from test_framework.util import assert_equal
  17  
  18  # Precomputed constant square root of -3 (mod p).
  19  MINUS_3_SQRT = FE(-3).sqrt()
  20  
  21  def xswiftec(u, t):
  22      """Decode field elements (u, t) to an X coordinate on the curve."""
  23      if u == 0:
  24          u = FE(1)
  25      if t == 0:
  26          t = FE(1)
  27      if u**3 + t**2 + 7 == 0:
  28          t = 2 * t
  29      X = (u**3 + 7 - t**2) / (2 * t)
  30      Y = (X + t) / (MINUS_3_SQRT * u)
  31      for x in (u + 4 * Y**2, (-X / Y - u) / 2, (X / Y - u) / 2):
  32          if GE.is_valid_x(x):
  33              return x
  34      assert False
  35  
  36  def xswiftec_inv(x, u, case):
  37      """Given x and u, find t such that xswiftec(u, t) = x, or return None.
  38  
  39      Case selects which of the up to 8 results to return."""
  40  
  41      if case & 2 == 0:
  42          if GE.is_valid_x(-x - u):
  43              return None
  44          v = x
  45          s = -(u**3 + 7) / (u**2 + u*v + v**2)
  46      else:
  47          s = x - u
  48          if s == 0:
  49              return None
  50          r = (-s * (4 * (u**3 + 7) + 3 * s * u**2)).sqrt()
  51          if r is None:
  52              return None
  53          if case & 1 and r == 0:
  54              return None
  55          v = (-u + r / s) / 2
  56      w = s.sqrt()
  57      if w is None:
  58          return None
  59      if case & 5 == 0:
  60          return -w * (u * (1 - MINUS_3_SQRT) / 2 + v)
  61      if case & 5 == 1:
  62          return w * (u * (1 + MINUS_3_SQRT) / 2 + v)
  63      if case & 5 == 4:
  64          return w * (u * (1 - MINUS_3_SQRT) / 2 + v)
  65      if case & 5 == 5:
  66          return -w * (u * (1 + MINUS_3_SQRT) / 2 + v)
  67  
  68  def xelligatorswift(x):
  69      """Given a field element X on the curve, find (u, t) that encode them."""
  70      assert GE.is_valid_x(x)
  71      while True:
  72          u = FE(random.randrange(1, FE.SIZE))
  73          case = random.randrange(0, 8)
  74          t = xswiftec_inv(x, u, case)
  75          if t is not None:
  76              return u, t
  77  
  78  def ellswift_create():
  79      """Generate a (privkey, ellswift_pubkey) pair."""
  80      priv = random.randrange(1, GE.ORDER)
  81      u, t = xelligatorswift((priv * G).x)
  82      return priv.to_bytes(32, 'big'), u.to_bytes() + t.to_bytes()
  83  
  84  def ellswift_ecdh_xonly(pubkey_theirs, privkey):
  85      """Compute X coordinate of shared ECDH point between ellswift pubkey and privkey."""
  86      u = FE(int.from_bytes(pubkey_theirs[:32], 'big'))
  87      t = FE(int.from_bytes(pubkey_theirs[32:], 'big'))
  88      d = int.from_bytes(privkey, 'big')
  89      return (d * GE.lift_x(xswiftec(u, t))).x.to_bytes()
  90  
  91  
  92  class TestFrameworkEllSwift(unittest.TestCase):
  93      def test_xswiftec(self):
  94          """Verify that xswiftec maps all inputs to the curve."""
  95          for _ in range(32):
  96              u = FE(random.randrange(0, FE.SIZE))
  97              t = FE(random.randrange(0, FE.SIZE))
  98              x = xswiftec(u, t)
  99              self.assertTrue(GE.is_valid_x(x))
 100  
 101          # Check that inputs which are considered undefined in the original
 102          # SwiftEC paper can also be decoded successfully (by remapping)
 103          undefined_inputs = [
 104              (FE(0), FE(23)),  # u = 0
 105              (FE(42), FE(0)),  # t = 0
 106              (FE(5), FE(-132).sqrt()),  # u^3 + t^2 + 7 = 0
 107          ]
 108          assert_equal(undefined_inputs[-1][0]**3 + undefined_inputs[-1][1]**2 + 7, 0)
 109          for u, t in undefined_inputs:
 110              x = xswiftec(u, t)
 111              self.assertTrue(GE.is_valid_x(x))
 112  
 113      def test_elligator_roundtrip(self):
 114          """Verify that encoding using xelligatorswift decodes back using xswiftec."""
 115          for _ in range(32):
 116              while True:
 117                  # Loop until we find a valid X coordinate on the curve.
 118                  x = FE(random.randrange(1, FE.SIZE))
 119                  if GE.is_valid_x(x):
 120                      break
 121              # Encoding it to (u, t), decode it back, and compare.
 122              u, t = xelligatorswift(x)
 123              x2 = xswiftec(u, t)
 124              self.assertEqual(x2, x)
 125  
 126      def test_ellswift_ecdh_xonly(self):
 127          """Verify that shared secret computed by ellswift_ecdh_xonly match."""
 128          for _ in range(32):
 129              privkey1, encoding1 = ellswift_create()
 130              privkey2, encoding2 = ellswift_create()
 131              shared_secret1 = ellswift_ecdh_xonly(encoding1, privkey2)
 132              shared_secret2 = ellswift_ecdh_xonly(encoding2, privkey1)
 133              self.assertEqual(shared_secret1, shared_secret2)
 134  
 135      def test_elligator_encode_testvectors(self):
 136          """Implement the BIP324 test vectors for ellswift encoding (read from xswiftec_inv_test_vectors.csv)."""
 137          vectors_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'xswiftec_inv_test_vectors.csv')
 138          with open(vectors_file, newline='') as csvfile:
 139              reader = csv.DictReader(csvfile)
 140              for row in reader:
 141                  u = FE.from_bytes(bytes.fromhex(row['u']))
 142                  x = FE.from_bytes(bytes.fromhex(row['x']))
 143                  for case in range(8):
 144                      ret = xswiftec_inv(x, u, case)
 145                      if ret is None:
 146                          self.assertEqual(row[f"case{case}_t"], "")
 147                      else:
 148                          self.assertEqual(row[f"case{case}_t"], ret.to_bytes().hex())
 149                          self.assertEqual(xswiftec(u, ret), x)
 150  
 151      def test_elligator_decode_testvectors(self):
 152          """Implement the BIP324 test vectors for ellswift decoding (read from ellswift_decode_test_vectors.csv)."""
 153          vectors_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ellswift_decode_test_vectors.csv')
 154          with open(vectors_file, newline='') as csvfile:
 155              reader = csv.DictReader(csvfile)
 156              for row in reader:
 157                  encoding = bytes.fromhex(row['ellswift'])
 158                  assert_equal(len(encoding), 64)
 159                  expected_x = FE(int(row['x'], 16))
 160                  u = FE(int.from_bytes(encoding[:32], 'big'))
 161                  t = FE(int.from_bytes(encoding[32:], 'big'))
 162                  x = xswiftec(u, t)
 163                  self.assertEqual(x, expected_x)
 164                  self.assertTrue(GE.is_valid_x(x))
 165