Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. ISTM that >3.9.0,<3.9.1 is not satisfiable, so only the latter condition makes sense. Or am I misunderstanding the syntax?
  2. Curious, why is 3.14 excluded?
  3. Let's update .github/workflows/python-package.yml accordingly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. ISTM that >3.9.0,<3.9.1 is not satisfiable, so only the latter condition makes sense. Or am I misunderstanding the syntax?
  2. Curious, why is 3.14 excluded?

I don't consider this to be the final solution. I only did what poetry advised me to do in order to upgrade the cryptography package.


[tool.poetry.group.dev.dependencies]
pytest = "*"
Expand Down
65 changes: 49 additions & 16 deletions slip10/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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.")
Expand All @@ -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")
Comment on lines +133 to +138
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the implementation of elliptic curves in the cryptography package is only a wrapper over openssl, the package doesn't even export the curves' moduli.


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:
Expand Down Expand Up @@ -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
Expand Down
9 changes: 4 additions & 5 deletions tests/test_slip10.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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'")
Expand Down
Loading