Skip to content

Commit 3bdbde5

Browse files
paschal533pacrob
authored andcommitted
feat: Add Windows compatibility using coincurve
1 parent 479b12f commit 3bdbde5

File tree

8 files changed

+176
-84
lines changed

8 files changed

+176
-84
lines changed

.github/workflows/tox.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,29 @@ jobs:
4141
python -m pip install tox
4242
- run: |
4343
python -m tox run -r
44+
45+
windows:
46+
runs-on: windows-latest
47+
strategy:
48+
matrix:
49+
python-version: ['3.11'] # Using a stable Python version for Windows testing
50+
toxenv: [core, wheel]
51+
fail-fast: false
52+
steps:
53+
- uses: actions/checkout@v4
54+
- name: Set up Python ${{ matrix.python-version }}
55+
uses: actions/setup-python@v5
56+
with:
57+
python-version: ${{ matrix.python-version }}
58+
- name: Install dependencies
59+
run: |
60+
python -m pip install --upgrade pip
61+
python -m pip install tox
62+
- name: Test with tox
63+
shell: bash
64+
run: |
65+
if [[ "${{ matrix.toxenv }}" == "wheel" ]]; then
66+
python -m tox run -e windows-wheel
67+
else
68+
python -m tox run -e py311-${{ matrix.toxenv }}
69+
fi

libp2p/crypto/ecc.py

Lines changed: 100 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
from fastecdsa import (
2-
keys,
3-
point,
4-
)
5-
from fastecdsa import curve as curve_types
6-
from fastecdsa.encoding.sec1 import (
7-
SEC1Encoder,
8-
)
1+
import sys
92

103
from libp2p.crypto.keys import (
114
KeyPair,
@@ -14,67 +7,126 @@
147
PublicKey,
158
)
169

17-
18-
def infer_local_type(curve: str) -> curve_types.Curve:
10+
if sys.platform != "win32":
11+
from fastecdsa import (
12+
keys,
13+
point,
14+
)
15+
from fastecdsa import curve as curve_types
16+
from fastecdsa.encoding.sec1 import (
17+
SEC1Encoder,
18+
)
19+
else:
20+
from coincurve import PrivateKey as CPrivateKey
21+
from coincurve import PublicKey as CPublicKey
22+
23+
24+
def infer_local_type(curve: str) -> object:
1925
"""
20-
Convert a ``str`` representation of some elliptic curve to a
26+
Convert a str representation of some elliptic curve to a
2127
representation understood by the backend of this module.
2228
"""
23-
if curve == "P-256":
29+
if curve != "P-256":
30+
raise NotImplementedError("Only P-256 curve is supported")
31+
32+
if sys.platform != "win32":
2433
return curve_types.P256
25-
else:
26-
raise NotImplementedError()
34+
return "P-256" # coincurve only supports P-256
35+
36+
37+
if sys.platform != "win32":
38+
39+
class ECCPublicKey(PublicKey):
40+
def __init__(self, impl: point.Point, curve: curve_types.Curve) -> None:
41+
self.impl = impl
42+
self.curve = curve
43+
44+
def to_bytes(self) -> bytes:
45+
return SEC1Encoder.encode_public_key(self.impl, compressed=False)
46+
47+
@classmethod
48+
def from_bytes(cls, data: bytes, curve: str) -> "ECCPublicKey":
49+
curve_type = infer_local_type(curve)
50+
public_key_impl = SEC1Encoder.decode_public_key(data, curve_type)
51+
return cls(public_key_impl, curve_type)
52+
53+
def get_type(self) -> KeyType:
54+
return KeyType.ECC_P256
55+
56+
def verify(self, data: bytes, signature: bytes) -> bool:
57+
raise NotImplementedError()
58+
59+
class ECCPrivateKey(PrivateKey):
60+
def __init__(self, impl: int, curve: curve_types.Curve) -> None:
61+
self.impl = impl
62+
self.curve = curve
63+
64+
@classmethod
65+
def new(cls, curve: str) -> "ECCPrivateKey":
66+
curve_type = infer_local_type(curve)
67+
private_key_impl = keys.gen_private_key(curve_type)
68+
return cls(private_key_impl, curve_type)
69+
70+
def to_bytes(self) -> bytes:
71+
return keys.export_key(self.impl, self.curve)
72+
73+
def get_type(self) -> KeyType:
74+
return KeyType.ECC_P256
75+
76+
def sign(self, data: bytes) -> bytes:
77+
raise NotImplementedError()
2778

79+
def get_public_key(self) -> PublicKey:
80+
public_key_impl = keys.get_public_key(self.impl, self.curve)
81+
return ECCPublicKey(public_key_impl, self.curve)
2882

29-
class ECCPublicKey(PublicKey):
30-
def __init__(self, impl: point.Point, curve: curve_types.Curve) -> None:
31-
self.impl = impl
32-
self.curve = curve
83+
else:
3384

34-
def to_bytes(self) -> bytes:
35-
return SEC1Encoder.encode_public_key(self.impl, compressed=False)
85+
class ECCPublicKey(PublicKey):
86+
def __init__(self, impl: CPublicKey, curve: str) -> None:
87+
self.impl = impl
88+
self.curve = curve
3689

37-
@classmethod
38-
def from_bytes(cls, data: bytes, curve: str) -> "ECCPublicKey":
39-
curve_type = infer_local_type(curve)
40-
public_key_impl = SEC1Encoder.decode_public_key(data, curve_type)
41-
return cls(public_key_impl, curve_type)
90+
def to_bytes(self) -> bytes:
91+
return self.impl.format(compressed=False)
4292

43-
def get_type(self) -> KeyType:
44-
return KeyType.ECC_P256
93+
@classmethod
94+
def from_bytes(cls, data: bytes, curve: str) -> "ECCPublicKey":
95+
curve_type = infer_local_type(curve)
96+
return cls(CPublicKey(data), curve_type) # type: ignore[arg-type]
4597

46-
def verify(self, data: bytes, signature: bytes) -> bool:
47-
raise NotImplementedError()
98+
def get_type(self) -> KeyType:
99+
return KeyType.ECC_P256
48100

101+
def verify(self, data: bytes, signature: bytes) -> bool:
102+
raise NotImplementedError()
49103

50-
class ECCPrivateKey(PrivateKey):
51-
def __init__(self, impl: int, curve: curve_types.Curve) -> None:
52-
self.impl = impl
53-
self.curve = curve
104+
class ECCPrivateKey(PrivateKey):
105+
def __init__(self, impl: CPrivateKey, curve: str) -> None:
106+
self.impl = impl
107+
self.curve = curve
54108

55-
@classmethod
56-
def new(cls, curve: str) -> "ECCPrivateKey":
57-
curve_type = infer_local_type(curve)
58-
private_key_impl = keys.gen_private_key(curve_type)
59-
return cls(private_key_impl, curve_type)
109+
@classmethod
110+
def new(cls, curve: str) -> "ECCPrivateKey":
111+
curve_type = infer_local_type(curve)
112+
return cls(CPrivateKey(), curve_type) # type: ignore[arg-type]
60113

61-
def to_bytes(self) -> bytes:
62-
return keys.export_key(self.impl, self.curve)
114+
def to_bytes(self) -> bytes:
115+
return self.impl.secret
63116

64-
def get_type(self) -> KeyType:
65-
return KeyType.ECC_P256
117+
def get_type(self) -> KeyType:
118+
return KeyType.ECC_P256
66119

67-
def sign(self, data: bytes) -> bytes:
68-
raise NotImplementedError()
120+
def sign(self, data: bytes) -> bytes:
121+
raise NotImplementedError()
69122

70-
def get_public_key(self) -> PublicKey:
71-
public_key_impl = keys.get_public_key(self.impl, self.curve)
72-
return ECCPublicKey(public_key_impl, self.curve)
123+
def get_public_key(self) -> PublicKey:
124+
return ECCPublicKey(self.impl.public_key, self.curve)
73125

74126

75127
def create_new_key_pair(curve: str) -> KeyPair:
76128
"""
77-
Return a new ECC keypair with the requested ``curve`` type, e.g.
129+
Return a new ECC keypair with the requested curve type, e.g.
78130
"P-256".
79131
"""
80132
private_key = ECCPrivateKey.new(curve)

libp2p/crypto/key_exchange.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
1+
import sys
12
from typing import (
23
Callable,
34
cast,
45
)
56

6-
from fastecdsa.encoding import (
7-
util,
8-
)
7+
if sys.platform != "win32":
8+
from fastecdsa.encoding import (
9+
util,
10+
)
11+
12+
int_bytelen = util.int_bytelen
13+
else:
14+
from math import (
15+
ceil,
16+
log2,
17+
)
18+
19+
def int_bytelen(n: int) -> int:
20+
if n == 0:
21+
return 1
22+
return ceil(log2(abs(n) + 1) / 8)
23+
924

1025
from libp2p.crypto.ecc import (
1126
ECCPrivateKey,
@@ -18,8 +33,6 @@
1833

1934
SharedKeyGenerator = Callable[[bytes], bytes]
2035

21-
int_bytelen = util.int_bytelen
22-
2336

2437
def create_ephemeral_key_pair(curve_type: str) -> tuple[PublicKey, SharedKeyGenerator]:
2538
"""Facilitates ECDH key exchange."""
@@ -32,9 +45,15 @@ def _key_exchange(serialized_remote_public_key: bytes) -> bytes:
3245
private_key = cast(ECCPrivateKey, key_pair.private_key)
3346

3447
remote_point = ECCPublicKey.from_bytes(serialized_remote_public_key, curve_type)
35-
secret_point = remote_point.impl * private_key.impl
36-
secret_x_coordinate = secret_point.x
37-
byte_size = int_bytelen(secret_x_coordinate)
38-
return secret_x_coordinate.to_bytes(byte_size, byteorder="big")
48+
49+
if sys.platform != "win32":
50+
secret_point = remote_point.impl * private_key.impl
51+
secret_x_coordinate = secret_point.x
52+
byte_size = int_bytelen(secret_x_coordinate)
53+
return secret_x_coordinate.to_bytes(byte_size, byteorder="big")
54+
else:
55+
# Windows implementation using coincurve
56+
shared_key = private_key.impl.ecdh(remote_point.impl.public_key)
57+
return shared_key
3958

4059
return key_pair.public_key, _key_exchange

newsfragments/498.docs.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Updates ``Feature Breakdown`` in ``README`` to more closely match the list of standard modules.

newsfragments/507.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added Windows compatibility by using coincurve instead of fastecdsa on Windows platforms

newsfragments/513.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed import path in the examples to use updated `net_stream` module path, resolving ModuleNotFoundError when running the examples.

newsfragments/522.internal.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes broken CI lint run, bumps ``pre-commit-hooks`` version to ``5.0.0`` and ``mdformat`` to ``0.7.22``.

setup.py

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
#!/usr/bin/env python
2-
import os
2+
import sys
33

44
from setuptools import (
55
find_packages,
66
setup,
77
)
88

9+
description = "libp2p: The Python implementation of the libp2p networking stack"
10+
11+
# Platform-specific dependencies
12+
if sys.platform == "win32":
13+
crypto_requires = [] # We'll use coincurve instead of fastecdsa on Windows
14+
else:
15+
crypto_requires = ["fastecdsa==1.7.5"]
16+
917
extras_require = {
1018
"dev": [
1119
"build>=0.9.0",
@@ -35,23 +43,11 @@
3543
extras_require["dev"] + extras_require["docs"] + extras_require["test"]
3644
)
3745

38-
fastecdsa = [
39-
# No official fastecdsa==1.7.4,1.7.5 wheels for Windows, using a pypi package that includes
40-
# the original library, but also windows-built wheels (32+64-bit) on those versions.
41-
# Fixme: Remove section when fastecdsa has released a windows-compatible wheel
42-
# (specifically: both win32 and win_amd64 targets)
43-
# See the following issues for more information;
44-
# https://github.com/libp2p/py-libp2p/issues/363
45-
# https://github.com/AntonKueltz/fastecdsa/issues/11
46-
"fastecdsa-any==1.7.5;sys_platform=='win32'",
47-
# Wheels are provided for these platforms, or compiling one is minimally frustrating in a
48-
# default python installation.
49-
"fastecdsa==1.7.5;sys_platform!='win32'",
50-
]
51-
52-
with open("./README.md") as readme:
53-
long_description = readme.read()
54-
46+
try:
47+
with open("./README.md", encoding="utf-8") as readme:
48+
long_description = readme.read()
49+
except FileNotFoundError:
50+
long_description = description
5551

5652
install_requires = [
5753
"base58>=1.0.3",
@@ -70,19 +66,14 @@
7066
"trio>=0.26.0",
7167
]
7268

73-
74-
# NOTE: Some dependencies break RTD builds. We can not install system dependencies on the
75-
# RTD system so we have to exclude these dependencies when we are in an RTD environment.
76-
readthedocs_is_building = os.environ.get("READTHEDOCS", False)
77-
if not readthedocs_is_building:
78-
install_requires.extend(fastecdsa)
79-
69+
# Add platform-specific dependencies
70+
install_requires.extend(crypto_requires)
8071

8172
setup(
8273
name="libp2p",
8374
# *IMPORTANT*: Don't manually change the version here. See Contributing docs for the release process.
8475
version="0.2.3",
85-
description="""libp2p: The Python implementation of the libp2p networking stack""",
76+
description=description,
8677
long_description=long_description,
8778
long_description_content_type="text/markdown",
8879
author="The Ethereum Foundation",
@@ -111,7 +102,7 @@
111102
"Programming Language :: Python :: 3.12",
112103
"Programming Language :: Python :: 3.13",
113104
],
114-
platforms=["unix", "linux", "osx"],
105+
platforms=["unix", "linux", "osx", "win32"],
115106
entry_points={
116107
"console_scripts": [
117108
"chat-demo=examples.chat.chat:main",

0 commit comments

Comments
 (0)