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..471eb8e 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,13 @@ 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 def generate_master(self, seed): """Master key generation in SLIP-0010 @@ -72,8 +76,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 +96,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 +106,58 @@ 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.order and point != bytes(1): 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: + 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 bytes(1) # the 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 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") + 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 +218,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..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'") diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..4a85ac9 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,17 @@ +import pytest + +from slip10.utils import SECP256K1, SECP256R1 + + +@pytest.mark.parametrize("curve", (SECP256K1, SECP256R1)) +def test_curve_arithmetic(curve): + generator = curve.privkey_to_pubkey((1).to_bytes(32, "big")) + minus_generator = curve.privkey_to_pubkey( + (curve.curve.group_order - 1).to_bytes(32, "big") + ) + double_generator = curve.privkey_to_pubkey((2).to_bytes(32, "big")) + triple_generator = curve.privkey_to_pubkey((3).to_bytes(32, "big")) + + assert curve.add_points(generator, minus_generator) == bytes.fromhex("00") + assert curve.add_points(generator, generator) == double_generator + assert curve.add_points(generator, double_generator) == triple_generator