Skip to content

Commit cb71d4c

Browse files
committed
Remove ecdsa dependency, use cryptography only
1 parent 19561f0 commit cb71d4c

File tree

4 files changed

+158
-28
lines changed

4 files changed

+158
-28
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pip install slip10
3636

3737
### Dependencies
3838

39-
This package uses [`ecdsa`](https://pypi.org/project/ecdsa/) as a wrapper for secp256k1 and secp256r1 elliptic curve operations and [`cryptography`](https://pypi.org/project/cryptography/) for Ed25519 and curve25519 operations.
39+
This package relies on [`cryptography`](https://pypi.org/project/cryptography/) and built-in field arithmetic for elliptic curve operations across all supported curves.
4040

4141
### Running the test suite
4242

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ keywords = ["bitcoin", "slip10", "hdwallet"]
1414

1515
[tool.poetry.dependencies]
1616
cryptography = "*"
17-
ecdsa = "*"
1817
base58 = "^2"
1918
python = ">=3.8,<4.0"
2019

slip10/utils.py

Lines changed: 153 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import hashlib
22
import hmac
33
import re
4+
from typing import Optional, Tuple
45

5-
import ecdsa
66
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
77
Ed25519PrivateKey,
88
Ed25519PublicKey,
@@ -30,11 +30,29 @@ class SLIP10DerivationError(Exception):
3030
pass
3131

3232

33+
Point = Optional[Tuple[int, int]]
34+
35+
3336
class WeierstrassCurve:
34-
def __init__(self, name, modifier, curve):
37+
def __init__(
38+
self,
39+
name: str,
40+
modifier: bytes,
41+
*,
42+
p: int,
43+
a: int,
44+
b: int,
45+
generator: Tuple[int, int],
46+
order: int,
47+
):
3548
self.name = name
3649
self.modifier = modifier
37-
self.curve = curve
50+
self.p = p
51+
self.a = a
52+
self.b = b
53+
self.generator = generator
54+
self.order = order
55+
self.coordinate_size = (p.bit_length() + 7) // 8
3856

3957
def generate_master(self, seed):
4058
"""Master key generation in SLIP-0010
@@ -59,7 +77,6 @@ def derive_private_child(self, privkey, chaincode, index):
5977
:return: (child_privatekey, child_chaincode)
6078
"""
6179
assert isinstance(privkey, bytes) and isinstance(chaincode, bytes)
62-
# payload is the I from the SLIP. Index is 32 bits unsigned int, BE.
6380
if index & HARDENED_INDEX != 0:
6481
payload = hmac.new(
6582
chaincode, b"\x00" + privkey + index.to_bytes(4, "big"), hashlib.sha512
@@ -72,8 +89,8 @@ def derive_private_child(self, privkey, chaincode, index):
7289

7390
while True:
7491
tweak = int.from_bytes(payload[:32], "big")
75-
child_private = (tweak + int.from_bytes(privkey, "big")) % self.curve.order
76-
if tweak <= self.curve.order and child_private != 0:
92+
child_private = (tweak + int.from_bytes(privkey, "big")) % self.order
93+
if tweak <= self.order and child_private != 0:
7794
break
7895
payload = hmac.new(
7996
chaincode,
@@ -92,43 +109,136 @@ def derive_public_child(self, pubkey, chaincode, index):
92109
93110
:return: (child_pubkey, child_chaincode)
94111
"""
95-
from ecdsa.ellipticcurve import INFINITY
96-
97112
assert isinstance(pubkey, bytes) and isinstance(chaincode, bytes)
98113
if index & HARDENED_INDEX != 0:
99114
raise SLIP10DerivationError("Hardened derivation is not possible.")
100115

101-
# payload is the I from the SLIP. Index is 32 bits unsigned int, BE.
102116
payload = hmac.new(
103117
chaincode, pubkey + index.to_bytes(4, "big"), hashlib.sha512
104118
).digest()
119+
base_point = self._bytes_to_point(pubkey)
105120
while True:
106121
tweak = int.from_bytes(payload[:32], "big")
107-
point = ecdsa.VerifyingKey.from_string(pubkey, self.curve).pubkey.point
108-
point += self.curve.generator * tweak
109-
if tweak <= self.curve.order and point != INFINITY:
122+
tweak_point = self._scalar_mult(tweak, self.generator)
123+
child_point = self._point_add(base_point, tweak_point)
124+
if tweak <= self.order and child_point is not None:
110125
break
111126
payload = hmac.new(
112127
chaincode,
113128
b"\x01" + payload[32:] + index.to_bytes(4, "big"),
114129
hashlib.sha512,
115130
).digest()
116-
return point.to_bytes("compressed"), payload[32:]
131+
return self._point_to_bytes(child_point), payload[32:]
117132

118133
def privkey_is_valid(self, privkey):
119134
key = int.from_bytes(privkey, "big")
120-
return 0 < key < self.curve.order
135+
return 0 < key < self.order
121136

122137
def pubkey_is_valid(self, pubkey):
123138
try:
124-
ecdsa.VerifyingKey.from_string(pubkey, self.curve)
125-
return True
126-
except ecdsa.errors.MalformedPointError:
139+
point = self._bytes_to_point(pubkey)
140+
except ValueError:
127141
return False
142+
return point is not None and self._is_on_curve(point)
128143

129144
def privkey_to_pubkey(self, privkey):
130-
sk = ecdsa.SigningKey.from_string(privkey, self.curve)
131-
return sk.get_verifying_key().to_string("compressed")
145+
if not self.privkey_is_valid(privkey):
146+
raise ValueError("Invalid private key")
147+
scalar = int.from_bytes(privkey, "big")
148+
point = self._scalar_mult(scalar, self.generator)
149+
if point is None:
150+
raise ValueError("Point at infinity")
151+
return self._point_to_bytes(point)
152+
153+
def _is_on_curve(self, point: Point) -> bool:
154+
if point is None:
155+
return True
156+
x, y = point
157+
return (y * y - (x * x * x + self.a * x + self.b)) % self.p == 0
158+
159+
def _point_add(self, p1: Point, p2: Point) -> Point:
160+
if p1 is None:
161+
return p2
162+
if p2 is None:
163+
return p1
164+
if p1 == p2:
165+
return self._point_double(p1)
166+
167+
x1, y1 = p1
168+
x2, y2 = p2
169+
if x1 == x2:
170+
return None
171+
172+
slope = ((y2 - y1) * self._inverse_mod((x2 - x1) % self.p)) % self.p
173+
x3 = (slope * slope - x1 - x2) % self.p
174+
y3 = (slope * (x1 - x3) - y1) % self.p
175+
return x3, y3
176+
177+
def _point_double(self, point: Point) -> Point:
178+
if point is None:
179+
return None
180+
x, y = point
181+
if y == 0:
182+
return None
183+
184+
slope = ((3 * x * x + self.a) * self._inverse_mod((2 * y) % self.p)) % self.p
185+
x3 = (slope * slope - 2 * x) % self.p
186+
y3 = (slope * (x - x3) - y) % self.p
187+
return x3, y3
188+
189+
def _scalar_mult(self, scalar: int, point: Tuple[int, int]) -> Point:
190+
scalar %= self.order
191+
if scalar == 0 or point is None:
192+
return None
193+
194+
result: Point = None
195+
addend: Point = point
196+
while scalar:
197+
if scalar & 1:
198+
result = self._point_add(result, addend)
199+
addend = self._point_double(addend)
200+
scalar >>= 1
201+
return result
202+
203+
def _bytes_to_point(self, data: bytes) -> Point:
204+
if len(data) == self.coordinate_size * 2 + 1 and data[0] == 4:
205+
x = int.from_bytes(data[1 : 1 + self.coordinate_size], "big")
206+
y = int.from_bytes(data[1 + self.coordinate_size :], "big")
207+
point = (x, y)
208+
if not self._is_on_curve(point):
209+
raise ValueError("Point is not on curve")
210+
return point
211+
212+
if len(data) != self.coordinate_size + 1 or data[0] not in (2, 3):
213+
raise ValueError("Invalid public key encoding")
214+
215+
x = int.from_bytes(data[1:], "big")
216+
y = self._recover_y(x, data[0] == 3)
217+
point = (x, y)
218+
if not self._is_on_curve(point):
219+
raise ValueError("Point is not on curve")
220+
return point
221+
222+
def _point_to_bytes(self, point: Tuple[int, int]) -> bytes:
223+
x, y = point
224+
prefix = 0x03 if y & 1 else 0x02
225+
return bytes([prefix]) + x.to_bytes(self.coordinate_size, "big")
226+
227+
def _recover_y(self, x: int, is_odd: bool) -> int:
228+
if x >= self.p:
229+
raise ValueError("Invalid point")
230+
rhs = (pow(x, 3, self.p) + self.a * x + self.b) % self.p
231+
y = pow(rhs, (self.p + 1) // 4, self.p)
232+
if (y * y) % self.p != rhs:
233+
raise ValueError("Invalid point")
234+
if bool(y & 1) != is_odd:
235+
y = (-y) % self.p
236+
if bool(y & 1) != is_odd:
237+
raise ValueError("Invalid point")
238+
return y
239+
240+
def _inverse_mod(self, value: int) -> int:
241+
return pow(value % self.p, -1, self.p)
132242

133243

134244
class EdwardsCurve:
@@ -197,8 +307,30 @@ def privkey_to_pubkey(self, privkey):
197307
return b"\x00" + sk.public_key().public_bytes(key_encoding, key_format)
198308

199309

200-
SECP256K1 = WeierstrassCurve("secp256k1", b"Bitcoin seed", ecdsa.SECP256k1)
201-
SECP256R1 = WeierstrassCurve("secp256r1", b"Nist256p1 seed", ecdsa.NIST256p)
310+
SECP256K1 = WeierstrassCurve(
311+
"secp256k1",
312+
b"Bitcoin seed",
313+
p=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,
314+
a=0,
315+
b=7,
316+
generator=(
317+
0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
318+
0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
319+
),
320+
order=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,
321+
)
322+
SECP256R1 = WeierstrassCurve(
323+
"secp256r1",
324+
b"Nist256p1 seed",
325+
p=0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF,
326+
a=0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC,
327+
b=0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B,
328+
generator=(
329+
0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296,
330+
0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5,
331+
),
332+
order=0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551,
333+
)
202334
ED25519 = EdwardsCurve("ed25519", b"ed25519 seed", Ed25519PrivateKey, Ed25519PublicKey)
203335
X25519 = EdwardsCurve(
204336
"curve25519", b"curve25519 seed", X25519PrivateKey, X25519PublicKey

tests/test_slip10.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import os
22

3-
import ecdsa
43
import pytest
54

65
from slip10 import HARDENED_INDEX, SLIP10, InvalidInputError, PrivateDerivationError
6+
from slip10.utils import SECP256K1
77

88
SEED_1 = "000102030405060708090a0b0c0d0e0f"
99
SEED_2 = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
@@ -440,10 +440,9 @@ def test_sanity_checks():
440440
== slip10.get_xpriv_from_path([])
441441
)
442442
non_extended_pubkey = slip10.get_privkey_from_path("m")
443-
pubkey = ecdsa.SigningKey.from_string(
444-
non_extended_pubkey, ecdsa.SECP256k1
445-
).get_verifying_key()
446-
assert pubkey.to_string("compressed") == slip10.get_pubkey_from_path("m")
443+
assert SECP256K1.privkey_to_pubkey(
444+
non_extended_pubkey
445+
) == slip10.get_pubkey_from_path("m")
447446
# But getting from "m'" does not make sense
448447
with pytest.raises(ValueError, match="invalid format"):
449448
slip10.get_pubkey_from_path("m'")

0 commit comments

Comments
 (0)