diff --git a/pyproject.toml b/pyproject.toml index 0ba9472..4ed243b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,8 @@ repository = "https://github.com/trezor/python-slip10" keywords = ["bitcoin", "slip10", "hdwallet"] [tool.poetry.dependencies] -cryptography = "*" -ecdsa = "*" -python = ">=3.8,<4.0" +cryptography = ">=45" +python = ">3.9.0,<3.9.1 || >3.9.1,<3.14.0" [tool.poetry.group.dev.dependencies] pytest = "*" diff --git a/slip10/utils.py b/slip10/utils.py index 8d79016..2e7c196 100644 --- a/slip10/utils.py +++ b/slip10/utils.py @@ -2,7 +2,7 @@ import hmac import re -import ecdsa +from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PrivateKey, Ed25519PublicKey, @@ -72,8 +72,10 @@ 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.curve.group_order + if tweak <= self.curve.group_order and child_private != 0: break payload = hmac.new( chaincode, @@ -92,8 +94,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 +104,64 @@ 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.privkey_to_pubkey(payload[:32])) + if tweak <= self.curve.group_order and point != None: 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.curve.group_order + + def _add_points(self, first: bytes, second: bytes) -> bytes | None: + 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: + return None + + if self.curve.name == "secp256k1": + p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F + elif self.curve.name == "secp256r1": + p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF + else: + raise NotImplementedError(f"Curve {self.curve.name} is not supported") + + slope = (y2 - y1) * pow(x2 - x1, -1, p) % p + x3 = (slope * slope - x1 - x2) % p + y3 = (slope * (x1 - x3) - y1) % p + + return bytes([0x02 if y3 % 2 == 0 else 0x03]) + x3.to_bytes(32, "big") 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") + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import ec + + sk = ec.derive_private_key( + int.from_bytes(privkey, "big"), self.curve, default_backend() + ) + return sk.public_key().public_bytes( + encoding=serialization.Encoding.X962, + format=serialization.PublicFormat.CompressedPoint, + ) class EdwardsCurve: @@ -197,8 +230,8 @@ def privkey_to_pubkey(self, privkey): 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()) +SECP256R1 = WeierstrassCurve("secp256r1", b"Nist256p1 seed", ec.SECP256R1()) 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..d85c143 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'")