diff --git a/bip-0374/gen_test_vectors.py b/bip-0374/gen_test_vectors.py index 792a59a45b..bbd874666a 100755 --- a/bip-0374/gen_test_vectors.py +++ b/bip-0374/gen_test_vectors.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 """Generate the BIP-0374 test vectors.""" import csv -import os +from pathlib import Path import sys from reference import ( TaggedHash, dleq_generate_proof, dleq_verify_proof, ) -from secp256k1 import G as GENERATOR, GE +from secp256k1lab.secp256k1 import G as GENERATOR, GE NUM_SUCCESS_TEST_VECTORS = 8 DLEQ_TAG_TESTVECTORS_RNG = "BIP0374/testvectors_rng" -FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv') -FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv') +FILENAME_GENERATE_PROOF_TEST = Path(__file__).parent / 'test_vectors_generate_proof.csv' +FILENAME_VERIFY_PROOF_TEST = Path(__file__).parent / 'test_vectors_verify_proof.csv' def random_scalar_int(vector_i, purpose): diff --git a/bip-0374/reference.py b/bip-0374/reference.py index 51354bd256..7470498a09 100755 --- a/bip-0374/reference.py +++ b/bip-0374/reference.py @@ -3,11 +3,15 @@ """Reference implementation of DLEQ BIP for secp256k1 with unit tests.""" from hashlib import sha256 +from pathlib import Path import random -from secp256k1 import G, GE import sys import unittest +# Prefer the vendored copy of secp256k1lab +sys.path.insert(0, str(Path(__file__).parent / "../python/secp256k1lab/src")) +from secp256k1lab.secp256k1 import G, GE + DLEQ_TAG_AUX = "BIP0374/aux" DLEQ_TAG_NONCE = "BIP0374/nonce" diff --git a/bip-0374/run_test_vectors.py b/bip-0374/run_test_vectors.py index 53e5e2dc46..94a6ebeca7 100755 --- a/bip-0374/run_test_vectors.py +++ b/bip-0374/run_test_vectors.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 """Run the BIP-DLEQ test vectors.""" import csv -import os +from pathlib import Path import sys from reference import ( dleq_generate_proof, dleq_verify_proof, ) -from secp256k1 import GE +from secp256k1lab.secp256k1 import GE -FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv') -FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv') +FILENAME_GENERATE_PROOF_TEST = Path(__file__).parent / 'test_vectors_generate_proof.csv' +FILENAME_VERIFY_PROOF_TEST = Path(__file__).parent / 'test_vectors_verify_proof.csv' all_passed = True diff --git a/python/secp256k1lab/.github/workflows/main.yml b/python/secp256k1lab/.github/workflows/main.yml new file mode 100644 index 0000000000..4950b96550 --- /dev/null +++ b/python/secp256k1lab/.github/workflows/main.yml @@ -0,0 +1,17 @@ +name: Tests +on: [push, pull_request] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v5 + - run: uvx ruff check . + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v5 + - run: uvx mypy . diff --git a/python/secp256k1lab/.python-version b/python/secp256k1lab/.python-version new file mode 100644 index 0000000000..bd28b9c5c2 --- /dev/null +++ b/python/secp256k1lab/.python-version @@ -0,0 +1 @@ +3.9 diff --git a/python/secp256k1lab/CHANGELOG.md b/python/secp256k1lab/CHANGELOG.md new file mode 100644 index 0000000000..15779717c4 --- /dev/null +++ b/python/secp256k1lab/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-03-31 + +Initial release. diff --git a/python/secp256k1lab/COPYING b/python/secp256k1lab/COPYING new file mode 100644 index 0000000000..e8f2163641 --- /dev/null +++ b/python/secp256k1lab/COPYING @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2009-2024 The Bitcoin Core developers +Copyright (c) 2009-2024 Bitcoin Developers +Copyright (c) 2025- The secp256k1lab Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/python/secp256k1lab/README.md b/python/secp256k1lab/README.md new file mode 100644 index 0000000000..dbc9dbd04c --- /dev/null +++ b/python/secp256k1lab/README.md @@ -0,0 +1,13 @@ +secp256k1lab +============ + +![Dependencies: None](https://img.shields.io/badge/dependencies-none-success) + +An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes written in Python, intended for prototyping, experimentation and education. + +Features: +* Low-level secp256k1 field and group arithmetic. +* Schnorr signing/verification and key generation according to [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki). +* ECDH key exchange. + +WARNING: The code in this library is slow and trivially vulnerable to side channel attacks. diff --git a/python/secp256k1lab/pyproject.toml b/python/secp256k1lab/pyproject.toml new file mode 100644 index 0000000000..a0bdd19f42 --- /dev/null +++ b/python/secp256k1lab/pyproject.toml @@ -0,0 +1,34 @@ +[project] +name = "secp256k1lab" +version = "1.0.0" +description = "An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes, intended for prototyping, experimentation and education" +readme = "README.md" +authors = [ + { name = "Pieter Wuille", email = "pieter@wuille.net" }, + { name = "Tim Ruffing", email = "me@real-or-random.org" }, + { name = "Jonas Nick", email = "jonasd.nick@gmail.com" }, + { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" } +] +maintainers = [ + { name = "Tim Ruffing", email = "me@real-or-random.org" }, + { name = "Jonas Nick", email = "jonasd.nick@gmail.com" }, + { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" } +] +requires-python = ">=3.9" +license = "MIT" +license-files = ["COPYING"] +keywords = ["secp256k1", "elliptic curves", "cryptography", "Bitcoin"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Topic :: Security :: Cryptography", +] +dependencies = [] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/python/secp256k1lab/src/secp256k1lab/__init__.py b/python/secp256k1lab/src/secp256k1lab/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/secp256k1lab/src/secp256k1lab/bip340.py b/python/secp256k1lab/src/secp256k1lab/bip340.py new file mode 100644 index 0000000000..ba839d16e1 --- /dev/null +++ b/python/secp256k1lab/src/secp256k1lab/bip340.py @@ -0,0 +1,73 @@ +# The following functions are based on the BIP 340 reference implementation: +# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py + +from .secp256k1 import FE, GE, G +from .util import int_from_bytes, bytes_from_int, xor_bytes, tagged_hash + + +def pubkey_gen(seckey: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + P = d0 * G + assert not P.infinity + return P.to_bytes_xonly() + + +def schnorr_sign( + msg: bytes, seckey: bytes, aux_rand: bytes, tag_prefix: str = "BIP0340" +) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + if len(aux_rand) != 32: + raise ValueError("aux_rand must be 32 bytes instead of %i." % len(aux_rand)) + P = d0 * G + assert not P.infinity + d = d0 if P.has_even_y() else GE.ORDER - d0 + t = xor_bytes(bytes_from_int(d), tagged_hash(tag_prefix + "/aux", aux_rand)) + k0 = ( + int_from_bytes(tagged_hash(tag_prefix + "/nonce", t + P.to_bytes_xonly() + msg)) + % GE.ORDER + ) + if k0 == 0: + raise RuntimeError("Failure. This happens only with negligible probability.") + R = k0 * G + assert not R.infinity + k = k0 if R.has_even_y() else GE.ORDER - k0 + e = ( + int_from_bytes( + tagged_hash( + tag_prefix + "/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg + ) + ) + % GE.ORDER + ) + sig = R.to_bytes_xonly() + bytes_from_int((k + e * d) % GE.ORDER) + assert schnorr_verify(msg, P.to_bytes_xonly(), sig, tag_prefix=tag_prefix) + return sig + + +def schnorr_verify( + msg: bytes, pubkey: bytes, sig: bytes, tag_prefix: str = "BIP0340" +) -> bool: + if len(pubkey) != 32: + raise ValueError("The public key must be a 32-byte array.") + if len(sig) != 64: + raise ValueError("The signature must be a 64-byte array.") + try: + P = GE.from_bytes_xonly(pubkey) + except ValueError: + return False + r = int_from_bytes(sig[0:32]) + s = int_from_bytes(sig[32:64]) + if (r >= FE.SIZE) or (s >= GE.ORDER): + return False + e = ( + int_from_bytes(tagged_hash(tag_prefix + "/challenge", sig[0:32] + pubkey + msg)) + % GE.ORDER + ) + R = s * G - e * P + if R.infinity or (not R.has_even_y()) or (R.x != r): + return False + return True diff --git a/python/secp256k1lab/src/secp256k1lab/ecdh.py b/python/secp256k1lab/src/secp256k1lab/ecdh.py new file mode 100644 index 0000000000..73f47fa1a7 --- /dev/null +++ b/python/secp256k1lab/src/secp256k1lab/ecdh.py @@ -0,0 +1,16 @@ +import hashlib + +from .secp256k1 import GE, Scalar + + +def ecdh_compressed_in_raw_out(seckey: bytes, pubkey: bytes) -> GE: + """TODO""" + shared_secret = Scalar.from_bytes_checked(seckey) * GE.from_bytes_compressed(pubkey) + assert not shared_secret.infinity # prime-order group + return shared_secret + + +def ecdh_libsecp256k1(seckey: bytes, pubkey: bytes) -> bytes: + """TODO""" + shared_secret = ecdh_compressed_in_raw_out(seckey, pubkey) + return hashlib.sha256(shared_secret.to_bytes_compressed()).digest() diff --git a/python/secp256k1lab/src/secp256k1lab/keys.py b/python/secp256k1lab/src/secp256k1lab/keys.py new file mode 100644 index 0000000000..3e28897e99 --- /dev/null +++ b/python/secp256k1lab/src/secp256k1lab/keys.py @@ -0,0 +1,15 @@ +from .secp256k1 import GE, G +from .util import int_from_bytes + +# The following function is based on the BIP 327 reference implementation +# https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py + + +# Return the plain public key corresponding to a given secret key +def pubkey_gen_plain(seckey: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + P = d0 * G + assert not P.infinity + return P.to_bytes_compressed() diff --git a/python/secp256k1lab/src/secp256k1lab/py.typed b/python/secp256k1lab/src/secp256k1lab/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0374/secp256k1.py b/python/secp256k1lab/src/secp256k1lab/secp256k1.py old mode 100755 new mode 100644 similarity index 55% rename from bip-0374/secp256k1.py rename to python/secp256k1lab/src/secp256k1lab/secp256k1.py index b83d028f92..6e262bf51e --- a/bip-0374/secp256k1.py +++ b/python/secp256k1lab/src/secp256k1lab/secp256k1.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright (c) 2022-2023 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -17,31 +15,29 @@ * G: the secp256k1 generator point """ -import unittest -from hashlib import sha256 - -class FE: - """Objects of this class represent elements of the field GF(2**256 - 2**32 - 977). +# TODO Docstrings of methods still say "field element" +class APrimeFE: + """Objects of this class represent elements of a prime field. They are represented internally in numerator / denominator form, in order to delay inversions. """ # The size of the field (also its modulus and characteristic). - SIZE = 2**256 - 2**32 - 977 + SIZE: int def __init__(self, a=0, b=1): """Initialize a field element a/b; both a and b can be ints or field elements.""" - if isinstance(a, FE): + if isinstance(a, type(self)): num = a._num den = a._den else: - num = a % FE.SIZE + num = a % self.SIZE den = 1 - if isinstance(b, FE): - den = (den * b._num) % FE.SIZE - num = (num * b._den) % FE.SIZE + if isinstance(b, type(self)): + den = (den * b._num) % self.SIZE + num = (num * b._den) % self.SIZE else: - den = (den * b) % FE.SIZE + den = (den * b) % self.SIZE assert den != 0 if num == 0: den = 1 @@ -50,71 +46,74 @@ def __init__(self, a=0, b=1): def __add__(self, a): """Compute the sum of two field elements (second may be int).""" - if isinstance(a, FE): - return FE(self._num * a._den + self._den * a._num, self._den * a._den) - return FE(self._num + self._den * a, self._den) + if isinstance(a, type(self)): + return type(self)(self._num * a._den + self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num + self._den * a, self._den) + return NotImplemented def __radd__(self, a): """Compute the sum of an integer and a field element.""" - return FE(a) + self + return type(self)(a) + self + + @classmethod + # REVIEW This should be + # def sum(cls, *es: Iterable[Self]) -> Self: + # but Self needs the typing_extension package on Python <= 3.12. + def sum(cls, *es): + """Compute the sum of field elements. + + sum(a, b, c, ...) is identical to (0 + a + b + c + ...).""" + return sum(es, start=cls(0)) def __sub__(self, a): """Compute the difference of two field elements (second may be int).""" - if isinstance(a, FE): - return FE(self._num * a._den - self._den * a._num, self._den * a._den) - return FE(self._num - self._den * a, self._den) + if isinstance(a, type(self)): + return type(self)(self._num * a._den - self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num - self._den * a, self._den) + return NotImplemented def __rsub__(self, a): """Compute the difference of an integer and a field element.""" - return FE(a) - self + return type(self)(a) - self def __mul__(self, a): """Compute the product of two field elements (second may be int).""" - if isinstance(a, FE): - return FE(self._num * a._num, self._den * a._den) - return FE(self._num * a, self._den) + if isinstance(a, type(self)): + return type(self)(self._num * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num * a, self._den) + return NotImplemented def __rmul__(self, a): """Compute the product of an integer with a field element.""" - return FE(a) * self + return type(self)(a) * self def __truediv__(self, a): """Compute the ratio of two field elements (second may be int).""" - return FE(self, a) + if isinstance(a, type(self)) or isinstance(a, int): + return type(self)(self, a) + return NotImplemented def __pow__(self, a): """Raise a field element to an integer power.""" - return FE(pow(self._num, a, FE.SIZE), pow(self._den, a, FE.SIZE)) + return type(self)(pow(self._num, a, self.SIZE), pow(self._den, a, self.SIZE)) def __neg__(self): """Negate a field element.""" - return FE(-self._num, self._den) + return type(self)(-self._num, self._den) def __int__(self): - """Convert a field element to an integer in range 0..p-1. The result is cached.""" + """Convert a field element to an integer in range 0..SIZE-1. The result is cached.""" if self._den != 1: - self._num = (self._num * pow(self._den, -1, FE.SIZE)) % FE.SIZE + self._num = (self._num * pow(self._den, -1, self.SIZE)) % self.SIZE self._den = 1 return self._num def sqrt(self): - """Compute the square root of a field element if it exists (None otherwise). - - Due to the fact that our modulus is of the form (p % 4) == 3, the Tonelli-Shanks - algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply - raising the argument to the power (p + 1) / 4. - - To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group, - and thus only half of the non-zero field elements are squares. An element a is - a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're - looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent - to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to - x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p.""" - v = int(self) - s = pow(v, (FE.SIZE + 1) // 4, FE.SIZE) - if s**2 % FE.SIZE == v: - return FE(s) - return None + """Compute the square root of a field element if it exists (None otherwise).""" + raise NotImplementedError def is_square(self): """Determine if this field element has a square root.""" @@ -122,26 +121,42 @@ def is_square(self): return self.sqrt() is not None def is_even(self): - """Determine whether this field element, represented as integer in 0..p-1, is even.""" + """Determine whether this field element, represented as integer in 0..SIZE-1, is even.""" return int(self) & 1 == 0 def __eq__(self, a): """Check whether two field elements are equal (second may be an int).""" - if isinstance(a, FE): - return (self._num * a._den - self._den * a._num) % FE.SIZE == 0 - return (self._num - self._den * a) % FE.SIZE == 0 + if isinstance(a, type(self)): + return (self._num * a._den - self._den * a._num) % self.SIZE == 0 + return (self._num - self._den * a) % self.SIZE == 0 def to_bytes(self): """Convert a field element to a 32-byte array (BE byte order).""" return int(self).to_bytes(32, 'big') - @staticmethod - def from_bytes(b): + @classmethod + def from_int_checked(cls, v): + """Convert an integer to a field element (no overflow allowed).""" + if v >= cls.SIZE: + raise ValueError + return cls(v) + + @classmethod + def from_int_wrapping(cls, v): + """Convert an integer to a field element (reduced modulo SIZE).""" + return cls(v % cls.SIZE) + + @classmethod + def from_bytes_checked(cls, b): """Convert a 32-byte array to a field element (BE byte order, no overflow allowed).""" v = int.from_bytes(b, 'big') - if v >= FE.SIZE: - return None - return FE(v) + return cls.from_int_checked(v) + + @classmethod + def from_bytes_wrapping(cls, b): + """Convert a 32-byte array to a field element (BE byte order, reduced modulo SIZE).""" + v = int.from_bytes(b, 'big') + return cls.from_int_wrapping(v) def __str__(self): """Convert this field element to a 64 character hex string.""" @@ -149,12 +164,40 @@ def __str__(self): def __repr__(self): """Get a string representation of this field element.""" - return f"FE(0x{int(self):x})" + return f"{type(self).__qualname__}(0x{int(self):x})" + + +class FE(APrimeFE): + SIZE = 2**256 - 2**32 - 977 + + def sqrt(self): + # Due to the fact that our modulus p is of the form (p % 4) == 3, the Tonelli-Shanks + # algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply + # raising the argument to the power (p + 1) / 4. + + # To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group, + # and thus only half of the non-zero field elements are squares. An element a is + # a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're + # looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent + # to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to + # x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p. + v = int(self) + s = pow(v, (self.SIZE + 1) // 4, self.SIZE) + if s**2 % self.SIZE == v: + return type(self)(s) + return None + + +class Scalar(APrimeFE): + """TODO Docstring""" + SIZE = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 class GE: """Objects of this class represent secp256k1 group elements (curve points or infinity) + GE objects are immutable. + Normal points on the curve have fields: * x: the x coordinate (a field element) * y: the y coordinate (a field element, satisfying y^2 = x^3 + 7) @@ -164,26 +207,47 @@ class GE: * infinity: True """ + # TODO The following two class attributes should probably be just getters as + # classmethods to enforce immutability. Unfortunately Python makes it hard + # to create "classproperties". `G` could then also be just a classmethod. + # Order of the group (number of points on the curve, plus 1 for infinity) - ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + ORDER = Scalar.SIZE # Number of valid distinct x coordinates on the curve. ORDER_HALF = ORDER // 2 + @property + def infinity(self): + """Whether the group element is the point at infinity.""" + return self._infinity + + @property + def x(self): + """The x coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._x + + @property + def y(self): + """The y coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._y + def __init__(self, x=None, y=None): """Initialize a group element with specified x and y coordinates, or infinity.""" if x is None: # Initialize as infinity. assert y is None - self.infinity = True + self._infinity = True else: # Initialize as point on the curve (and check that it is). fx = FE(x) fy = FE(y) assert fy**2 == fx**3 + 7 - self.infinity = False - self.x = fx - self.y = fy + self._infinity = False + self._x = fx + self._y = fy def __add__(self, a): """Add two group elements together.""" @@ -209,13 +273,20 @@ def __add__(self, a): return GE(x, y) @staticmethod - def mul(*aps): + def sum(*ps): + """Compute the sum of group elements. + + GE.sum(a, b, c, ...) is identical to (GE() + a + b + c + ...).""" + return sum(ps, start=GE()) + + @staticmethod + def batch_mul(*aps): """Compute a (batch) scalar group element multiplication. - GE.mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3, + GE.batch_mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3, but more efficient.""" # Reduce all the scalars modulo order first (so we can deal with negatives etc). - naps = [(a % GE.ORDER, p) for a, p in aps] + naps = [(int(a), p) for a, p in aps] # Start with point at infinity. r = GE() # Iterate over all bit positions, from high to low. @@ -231,8 +302,8 @@ def mul(*aps): def __rmul__(self, a): """Multiply an integer with a group element.""" if self == G: - return FAST_G.mul(a) - return GE.mul((a, self)) + return FAST_G.mul(Scalar(a)) + return GE.batch_mul((Scalar(a), self)) def __neg__(self): """Compute the negation of a group element.""" @@ -244,11 +315,26 @@ def __sub__(self, a): """Subtract a group element from another.""" return self + (-a) + def __eq__(self, a): + """Check if two group elements are equal.""" + return (self - a).infinity + + def has_even_y(self): + """Determine whether a non-infinity group element has an even y coordinate.""" + assert not self.infinity + return self.y.is_even() + def to_bytes_compressed(self): """Convert a non-infinite group element to 33-byte compressed encoding.""" assert not self.infinity return bytes([3 - self.y.is_even()]) + self.x.to_bytes() + def to_bytes_compressed_with_infinity(self): + """Convert a group element to 33-byte compressed encoding, mapping infinity to zeros.""" + if self.infinity: + return 33 * b"\x00" + return self.to_bytes_compressed() + def to_bytes_uncompressed(self): """Convert a non-infinite group element to 65-byte uncompressed encoding.""" assert not self.infinity @@ -264,44 +350,51 @@ def lift_x(x): """Return group element with specified field element as x coordinate (and even y).""" y = (FE(x)**3 + 7).sqrt() if y is None: - return None + raise ValueError if not y.is_even(): y = -y return GE(x, y) + @staticmethod + def from_bytes_compressed(b): + """Convert a compressed to a group element.""" + assert len(b) == 33 + if b[0] != 2 and b[0] != 3: + raise ValueError + x = FE.from_bytes_checked(b[1:]) + r = GE.lift_x(x) + if b[0] == 3: + r = -r + return r + + @staticmethod + def from_bytes_uncompressed(b): + """Convert an uncompressed to a group element.""" + assert len(b) == 65 + if b[0] != 4: + raise ValueError + x = FE.from_bytes_checked(b[1:33]) + y = FE.from_bytes_checked(b[33:]) + if y**2 != x**3 + 7: + raise ValueError + return GE(x, y) + @staticmethod def from_bytes(b): """Convert a compressed or uncompressed encoding to a group element.""" assert len(b) in (33, 65) if len(b) == 33: - if b[0] != 2 and b[0] != 3: - return None - x = FE.from_bytes(b[1:]) - if x is None: - return None - r = GE.lift_x(x) - if r is None: - return None - if b[0] == 3: - r = -r - return r + return GE.from_bytes_compressed(b) else: - if b[0] != 4: - return None - x = FE.from_bytes(b[1:33]) - y = FE.from_bytes(b[33:]) - if y**2 != x**3 + 7: - return None - return GE(x, y) + return GE.from_bytes_uncompressed(b) @staticmethod def from_bytes_xonly(b): """Convert a point given in xonly encoding to a group element.""" assert len(b) == 32 - x = FE.from_bytes(b) - if x is None: - return None - return GE.lift_x(x) + x = FE.from_bytes_checked(b) + r = GE.lift_x(x) + return r @staticmethod def is_valid_x(x): @@ -320,6 +413,13 @@ def __repr__(self): return "GE()" return f"GE(0x{int(self.x):x},0x{int(self.y):x})" + def __hash__(self): + """Compute a non-cryptographic hash of the group element.""" + if self.infinity: + return 0 # 0 is not a valid x coordinate + return int(self.x) + + # The secp256k1 generator point G = GE.lift_x(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798) @@ -344,7 +444,7 @@ def __init__(self, p): def mul(self, a): result = GE() - a = a % GE.ORDER + a = int(a) for bit in range(a.bit_length()): if a & (1 << bit): result += self.table[bit] @@ -352,9 +452,3 @@ def mul(self, a): # Precomputed table with multiples of G for fast multiplication FAST_G = FastGEMul(G) - -class TestFrameworkSecp256k1(unittest.TestCase): - def test_H(self): - H = sha256(G.to_bytes_uncompressed()).digest() - assert GE.lift_x(FE.from_bytes(H)) is not None - self.assertEqual(H.hex(), "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0") diff --git a/python/secp256k1lab/src/secp256k1lab/util.py b/python/secp256k1lab/src/secp256k1lab/util.py new file mode 100644 index 0000000000..d8c744b795 --- /dev/null +++ b/python/secp256k1lab/src/secp256k1lab/util.py @@ -0,0 +1,24 @@ +import hashlib + + +# This implementation can be sped up by storing the midstate after hashing +# tag_hash instead of rehashing it all the time. +def tagged_hash(tag: str, msg: bytes) -> bytes: + tag_hash = hashlib.sha256(tag.encode()).digest() + return hashlib.sha256(tag_hash + tag_hash + msg).digest() + + +def bytes_from_int(x: int) -> bytes: + return x.to_bytes(32, byteorder="big") + + +def xor_bytes(b0: bytes, b1: bytes) -> bytes: + return bytes(x ^ y for (x, y) in zip(b0, b1)) + + +def int_from_bytes(b: bytes) -> int: + return int.from_bytes(b, byteorder="big") + + +def hash_sha256(b: bytes) -> bytes: + return hashlib.sha256(b).digest()