diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 20c5911..8ef3240 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -6,6 +6,7 @@ jobs: test: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ ubuntu-latest ] python-version: ['3.8', '3.9', '3.10'] diff --git a/pyproject.toml b/pyproject.toml index 0ba9472..ab97259 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,13 +14,16 @@ keywords = ["bitcoin", "slip10", "hdwallet"] [tool.poetry.dependencies] cryptography = "*" -ecdsa = "*" python = ">=3.8,<4.0" [tool.poetry.group.dev.dependencies] pytest = "*" black = ">=20" isort = "^5" +# Fix pytest for python 3.10 +# See https://github.com/pytest-dev/pytest/issues/12177#issue-2220516002 +exceptiongroup = { version = "*", markers = "python_version == '3.10'" } +tomli = { version = "*", markers = "python_version == '3.10'" } [build-system] requires = ["poetry-core"] diff --git a/slip10/utils.py b/slip10/utils.py index 8d79016..7b04fc5 100644 --- a/slip10/utils.py +++ b/slip10/utils.py @@ -2,7 +2,8 @@ import hmac import re -import ecdsa +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PrivateKey, Ed25519PublicKey, @@ -31,10 +32,14 @@ class SLIP10DerivationError(Exception): class WeierstrassCurve: - def __init__(self, name, modifier, curve): + def __init__(self, name, modifier, curve, a, modulus, order): self.name = name self.modifier = modifier self.curve = curve + self.a = a + self.modulus = modulus + self.order = order + self.point_at_infinity = bytes(1) def generate_master(self, seed): """Master key generation in SLIP-0010 @@ -72,8 +77,8 @@ def derive_private_child(self, privkey, chaincode, index): while True: tweak = int.from_bytes(payload[:32], "big") - child_private = (tweak + int.from_bytes(privkey, "big")) % self.curve.order - if tweak <= self.curve.order and child_private != 0: + child_private = (tweak + int.from_bytes(privkey, "big")) % self.order + if tweak < self.order and child_private != 0: break payload = hmac.new( chaincode, @@ -92,8 +97,6 @@ def derive_public_child(self, pubkey, chaincode, index): :return: (child_pubkey, child_chaincode) """ - from ecdsa.ellipticcurve import INFINITY - assert isinstance(pubkey, bytes) and isinstance(chaincode, bytes) if index & HARDENED_INDEX != 0: raise SLIP10DerivationError("Hardened derivation is not possible.") @@ -104,31 +107,76 @@ def derive_public_child(self, pubkey, chaincode, index): ).digest() while True: tweak = int.from_bytes(payload[:32], "big") - point = ecdsa.VerifyingKey.from_string(pubkey, self.curve).pubkey.point - point += self.curve.generator * tweak - if tweak <= self.curve.order and point != INFINITY: + point = self.add_points(pubkey, self.multiply_generator(tweak)) + if tweak < self.order and point != self.point_at_infinity: break payload = hmac.new( chaincode, b"\x01" + payload[32:] + index.to_bytes(4, "big"), hashlib.sha512, ).digest() - return point.to_bytes("compressed"), payload[32:] + return point, payload[32:] def privkey_is_valid(self, privkey): key = int.from_bytes(privkey, "big") - return 0 < key < self.curve.order + return 0 < key < self.order + + def add_points(self, first: bytes, second: bytes) -> bytes: + if first == self.point_at_infinity: + return second + + if second == self.point_at_infinity: + return first + + p1 = ec.EllipticCurvePublicKey.from_encoded_point(self.curve, first) + p2 = ec.EllipticCurvePublicKey.from_encoded_point(self.curve, second) + + x1 = p1.public_numbers().x + y1 = p1.public_numbers().y + x2 = p2.public_numbers().x + y2 = p2.public_numbers().y + + if x1 == x2 and y1 == -y2 % self.modulus: + return self.point_at_infinity + + if x1 == x2 and y1 == y2: + # doubling + slope = ( + (3 * x1 * x1 + self.a) * pow(2 * y1, -1, self.modulus) % self.modulus + ) + else: + slope = (y2 - y1) * pow(x2 - x1, -1, self.modulus) % self.modulus + + x3 = (slope * slope - x1 - x2) % self.modulus + y3 = (slope * (x1 - x3) - y1) % self.modulus + + return bytes([0x02 if y3 % 2 == 0 else 0x03]) + x3.to_bytes(32, "big") + + def multiply_generator(self, scalar: int) -> bytes: + scalar %= self.order + + if scalar == 0: + return self.point_at_infinity + + sk = ec.derive_private_key(scalar, self.curve) + return sk.public_key().public_bytes( + encoding=serialization.Encoding.X962, + format=serialization.PublicFormat.CompressedPoint, + ) def pubkey_is_valid(self, pubkey): try: - ecdsa.VerifyingKey.from_string(pubkey, self.curve) + ec.EllipticCurvePublicKey.from_encoded_point(self.curve, pubkey) return True - except ecdsa.errors.MalformedPointError: + except ValueError: return False - def privkey_to_pubkey(self, privkey): - sk = ecdsa.SigningKey.from_string(privkey, self.curve) - return sk.get_verifying_key().to_string("compressed") + def privkey_to_pubkey(self, privkey: bytes) -> bytes: + sk = ec.derive_private_key(int.from_bytes(privkey, "big"), self.curve) + return sk.public_key().public_bytes( + encoding=serialization.Encoding.X962, + format=serialization.PublicFormat.CompressedPoint, + ) class EdwardsCurve: @@ -189,16 +237,28 @@ def pubkey_is_valid(self, pubkey): return True def privkey_to_pubkey(self, privkey): - from cryptography.hazmat.primitives import serialization - sk = self.private_key_class.from_private_bytes(privkey) key_encoding = serialization.Encoding.Raw key_format = serialization.PublicFormat.Raw return b"\x00" + sk.public_key().public_bytes(key_encoding, key_format) -SECP256K1 = WeierstrassCurve("secp256k1", b"Bitcoin seed", ecdsa.SECP256k1) -SECP256R1 = WeierstrassCurve("secp256r1", b"Nist256p1 seed", ecdsa.NIST256p) +SECP256K1 = WeierstrassCurve( + "secp256k1", + b"Bitcoin seed", + ec.SECP256K1(), + 0, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141, +) +SECP256R1 = WeierstrassCurve( + "secp256r1", + b"Nist256p1 seed", + ec.SECP256R1(), + 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC, + 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF, + 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551, +) ED25519 = EdwardsCurve("ed25519", b"ed25519 seed", Ed25519PrivateKey, Ed25519PublicKey) X25519 = EdwardsCurve( "curve25519", b"curve25519 seed", X25519PrivateKey, X25519PublicKey diff --git a/tests/test_slip10.py b/tests/test_slip10.py index 33f4836..2874659 100644 --- a/tests/test_slip10.py +++ b/tests/test_slip10.py @@ -1,9 +1,9 @@ import os -import ecdsa import pytest from slip10 import HARDENED_INDEX, SLIP10, InvalidInputError, PrivateDerivationError +from slip10.utils import SECP256K1 SEED_1 = "000102030405060708090a0b0c0d0e0f" SEED_2 = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542" @@ -440,10 +440,9 @@ def test_sanity_checks(): == slip10.get_xpriv_from_path([]) ) non_extended_pubkey = slip10.get_privkey_from_path("m") - pubkey = ecdsa.SigningKey.from_string( - non_extended_pubkey, ecdsa.SECP256k1 - ).get_verifying_key() - assert pubkey.to_string("compressed") == slip10.get_pubkey_from_path("m") + assert SECP256K1.privkey_to_pubkey( + non_extended_pubkey + ) == slip10.get_pubkey_from_path("m") # But getting from "m'" does not make sense with pytest.raises(ValueError, match="invalid format"): slip10.get_pubkey_from_path("m'") @@ -914,3 +913,22 @@ def test_slip10_vectors(): assert node.chaincode.hex() == chaincode assert node.privkey.hex() == privkey assert node.pubkey.hex() == pubkey + + +def test_secp256r1_derivation_retry(): + # Test retry in public key to public key derivation + # https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-derivation-retry-for-nist256p1 + chaincode, pubkey = SLIP10.from_seed( + bytes.fromhex(SEED_1), curve_name="secp256r1" + ).get_extended_pubkey_from_path("m/28578'") + chaincode, pubkey = SLIP10( + chaincode, pubkey=pubkey, curve_name="secp256r1" + ).get_extended_pubkey_from_path("m/33941") + assert ( + chaincode.hex() + == "9e87fe95031f14736774cd82f25fd885065cb7c358c1edf813c72af535e83071" + ) + assert ( + pubkey.hex() + == "0235bfee614c0d5b2cae260000bb1d0d84b270099ad790022c1ae0b2e782efe120" + ) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..3ccabef --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,21 @@ +import pytest + +from slip10.utils import SECP256K1, SECP256R1 + + +@pytest.mark.parametrize("curve", (SECP256K1, SECP256R1)) +def test_curve_arithmetic(curve): + generator = curve.multiply_generator(1) + minus_generator = curve.multiply_generator(curve.order - 1) + double_generator = curve.multiply_generator(2) + triple_generator = curve.multiply_generator(3) + + assert curve.add_points(curve.point_at_infinity, generator) == generator + assert curve.add_points(generator, curve.point_at_infinity) == generator + assert curve.add_points(generator, minus_generator) == curve.point_at_infinity + assert curve.add_points(generator, generator) == double_generator + assert curve.add_points(generator, double_generator) == triple_generator + + assert curve.multiply_generator(0) == curve.point_at_infinity + assert curve.multiply_generator(curve.order) == curve.point_at_infinity + assert curve.multiply_generator(curve.order + 1) == generator