Skip to content

Commit 08cbdaa

Browse files
authored
Merge branch 'main' into fix/cross-platform-path-handling-944
2 parents d4912c2 + 71d3d50 commit 08cbdaa

File tree

15 files changed

+488
-19
lines changed

15 files changed

+488
-19
lines changed

docs/libp2p.security.pnet.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
libp2p.security.pnet package
2+
================================
3+
4+
Submodules
5+
----------
6+
7+
libp2p.security.pnet.protector module
8+
-------------------------------------
9+
10+
.. automodule:: libp2p.security.pnet.protector
11+
:members:
12+
:undoc-members:
13+
:show-inheritance:
14+
15+
libp2p.security.pnet.psk_conn module
16+
------------------------------------
17+
18+
.. automodule:: libp2p.security.pnet.psk_conn
19+
:members:
20+
:undoc-members:
21+
:show-inheritance:
22+
23+
Module contents
24+
---------------
25+
26+
.. automodule:: libp2p.security.pnet
27+
:members:
28+
:undoc-members:
29+
:show-inheritance:

docs/libp2p.security.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Subpackages
99

1010
libp2p.security.insecure
1111
libp2p.security.noise
12+
libp2p.security.pnet
1213
libp2p.security.secio
1314
libp2p.security.tls
1415

examples/ping/ping.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import multiaddr
55
import trio
66

7+
from examples.advanced.network_discover import get_optimal_binding_address
78
from libp2p import (
89
new_host,
910
)
@@ -25,6 +26,7 @@
2526
PING_PROTOCOL_ID = TProtocol("/ipfs/ping/1.0.0")
2627
PING_LENGTH = 32
2728
RESP_TIMEOUT = 60
29+
PSK = "dffb7e3135399a8b1612b2aaca1c36a3a8ac2cd0cca51ceeb2ced87d308cac6d"
2830

2931

3032
async def handle_ping(stream: INetStream) -> None:
@@ -60,18 +62,27 @@ async def send_ping(stream: INetStream) -> None:
6062
print(f"error occurred : {e}")
6163

6264

63-
async def run(port: int, destination: str) -> None:
65+
async def run(port: int, destination: str, psk: int, transport: str) -> None:
6466
from libp2p.utils.address_validation import (
6567
find_free_port,
6668
get_available_interfaces,
67-
get_optimal_binding_address,
6869
)
6970

7071
if port <= 0:
7172
port = find_free_port()
7273

73-
listen_addrs = get_available_interfaces(port)
74-
host = new_host(listen_addrs=listen_addrs)
74+
_ = get_available_interfaces(8000)
75+
_ = get_optimal_binding_address(8000)
76+
77+
if transport == "tcp":
78+
listen_addrs = get_available_interfaces(port)
79+
if transport == "ws":
80+
listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{port}/ws")]
81+
82+
if psk == 1:
83+
host = new_host(listen_addrs=listen_addrs, psk=PSK)
84+
else:
85+
host = new_host(listen_addrs=listen_addrs)
7586

7687
async with host.run(listen_addrs=listen_addrs), trio.open_nursery() as nursery:
7788
# Start the peer-store cleanup task
@@ -87,12 +98,9 @@ async def run(port: int, destination: str) -> None:
8798
for addr in all_addrs:
8899
print(f"{addr}")
89100

90-
# Use optimal address for the client command
91-
optimal_addr = get_optimal_binding_address(port)
92-
optimal_addr_with_peer = f"{optimal_addr}/p2p/{host.get_id().to_string()}"
93101
print(
94102
f"\nRun this from the same folder in another console:\n\n"
95-
f"ping-demo -d {optimal_addr_with_peer}\n"
103+
f"ping-demo -d {host.get_addrs()[0]} -psk {psk} -t {transport}\n"
96104
)
97105
print("Waiting for incoming connection...")
98106

@@ -130,10 +138,23 @@ def main() -> None:
130138
type=str,
131139
help=f"destination multiaddr string, e.g. {example_maddr}",
132140
)
141+
142+
parser.add_argument(
143+
"-psk", "--psk", default=0, type=int, help="Enable PSK in the transport layer"
144+
)
145+
146+
parser.add_argument(
147+
"-t",
148+
"--transport",
149+
default="tcp",
150+
type=str,
151+
help="Choose the transport layer for ping TCP/WS",
152+
)
153+
133154
args = parser.parse_args()
134155

135156
try:
136-
trio.run(run, *(args.port, args.destination))
157+
trio.run(run, *(args.port, args.destination, args.psk, args.transport))
137158
except KeyboardInterrupt:
138159
pass
139160

libp2p/__init__.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@
3333
from libp2p.crypto.keys import (
3434
KeyPair,
3535
)
36+
from libp2p.crypto.ed25519 import (
37+
create_new_key_pair as create_new_ed25519_key_pair,
38+
)
3639
from libp2p.crypto.rsa import (
37-
create_new_key_pair,
40+
create_new_key_pair as create_new_rsa_key_pair,
3841
)
3942
from libp2p.crypto.x25519 import create_new_key_pair as create_new_x25519_key_pair
4043
from libp2p.custom_types import (
@@ -154,7 +157,17 @@ def create_mplex_muxer_option() -> TMuxerOptions:
154157

155158

156159
def generate_new_rsa_identity() -> KeyPair:
157-
return create_new_key_pair()
160+
return create_new_rsa_key_pair()
161+
162+
163+
def generate_new_ed25519_identity() -> KeyPair:
164+
"""
165+
Generate a new Ed25519 identity key pair.
166+
167+
Ed25519 is preferred for better interoperability with other libp2p implementations
168+
(e.g., Rust, Go) which often disable RSA support.
169+
"""
170+
return create_new_ed25519_key_pair()
158171

159172

160173
def generate_peer_id_from(key_pair: KeyPair) -> ID:
@@ -186,6 +199,7 @@ def new_swarm(
186199
tls_client_config: ssl.SSLContext | None = None,
187200
tls_server_config: ssl.SSLContext | None = None,
188201
resource_manager: ResourceManager | None = None,
202+
psk: str | None = None
189203
) -> INetworkService:
190204
logger.debug(f"new_swarm: enable_quic={enable_quic}, listen_addrs={listen_addrs}")
191205
"""
@@ -201,15 +215,21 @@ def new_swarm(
201215
:param quic_transport_opt: options for transport
202216
:param resource_manager: optional resource manager for connection/stream limits
203217
:type resource_manager: :class:`libp2p.rcmgr.ResourceManager` or None
218+
:param psk: optional pre-shared key for PSK encryption in transport
204219
:return: return a default swarm instance
205220
206221
Note: Yamux (/yamux/1.0.0) is the preferred stream multiplexer
207222
due to its improved performance and features.
208223
Mplex (/mplex/6.7.0) is retained for backward compatibility
209224
but may be deprecated in the future.
225+
226+
Note: Ed25519 keys are used by default for better interoperability with
227+
other libp2p implementations (Rust, Go) which often disable RSA support.
210228
"""
211229
if key_pair is None:
212-
key_pair = generate_new_rsa_identity()
230+
# Use Ed25519 by default for better interoperability with Rust/Go libp2p
231+
# which often compile without RSA support
232+
key_pair = generate_new_ed25519_identity()
213233

214234
id_opt = generate_peer_id_from(key_pair)
215235

@@ -306,7 +326,8 @@ def new_swarm(
306326
upgrader,
307327
transport,
308328
retry_config=retry_config,
309-
connection_config=connection_config
329+
connection_config=connection_config,
330+
psk=psk
310331
)
311332

312333
# Set resource manager if provided
@@ -324,6 +345,21 @@ def new_swarm(
324345

325346
return swarm
326347

348+
# Set resource manager if provided
349+
# Auto-create a default ResourceManager if one was not provided
350+
if resource_manager is None:
351+
try:
352+
from libp2p.rcmgr import new_resource_manager as _new_rm
353+
354+
resource_manager = _new_rm()
355+
except Exception:
356+
resource_manager = None
357+
358+
if resource_manager is not None:
359+
swarm.set_resource_manager(resource_manager)
360+
361+
return swarm
362+
327363

328364
def new_host(
329365
key_pair: KeyPair | None = None,
@@ -342,6 +378,7 @@ def new_host(
342378
tls_client_config: ssl.SSLContext | None = None,
343379
tls_server_config: ssl.SSLContext | None = None,
344380
resource_manager: ResourceManager | None = None,
381+
psk: str | None = None
345382
) -> IHost:
346383
"""
347384
Create a new libp2p host based on the given parameters.
@@ -361,6 +398,7 @@ def new_host(
361398
:param tls_server_config: optional TLS server configuration for WebSocket transport
362399
:param resource_manager: optional resource manager for connection/stream limits
363400
:type resource_manager: :class:`libp2p.rcmgr.ResourceManager` or None
401+
:param psk: optional pre-shared key (PSK)
364402
:return: return a host instance
365403
"""
366404

@@ -390,6 +428,7 @@ def new_host(
390428
tls_client_config=tls_client_config,
391429
tls_server_config=tls_server_config,
392430
resource_manager=resource_manager,
431+
psk=psk
393432
)
394433

395434
if disc_opt is not None:

libp2p/host/basic_host.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def __init__(
108108
default_protocols: OrderedDict[TProtocol, StreamHandlerFn] | None = None,
109109
negotiate_timeout: int = DEFAULT_NEGOTIATE_TIMEOUT,
110110
resource_manager: ResourceManager | None = None,
111+
psk: str | None = None,
111112
) -> None:
112113
"""
113114
Initialize a BasicHost instance.
@@ -148,6 +149,7 @@ def __init__(
148149
self.bootstrap = None
149150
if bootstrap:
150151
self.bootstrap = BootstrapDiscovery(network, bootstrap)
152+
self.psk = psk
151153

152154
# Cache a signed-record if the local-node in the PeerStore
153155
envelope = create_signed_peer_record(

libp2p/network/swarm.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
PeerStoreError,
4040
)
4141
from libp2p.rcmgr.manager import ResourceManager
42+
from libp2p.security.pnet.protector import new_protected_conn
4243
from libp2p.tools.async_service import (
4344
Service,
4445
)
@@ -103,11 +104,13 @@ def __init__(
103104
transport: ITransport,
104105
retry_config: RetryConfig | None = None,
105106
connection_config: ConnectionConfig | QUICTransportConfig | None = None,
107+
psk: str | None = None,
106108
):
107109
self.self_id = peer_id
108110
self.peerstore = peerstore
109111
self.upgrader = upgrader
110112
self.transport = transport
113+
self.psk = psk
111114

112115
# Enhanced: Initialize retry and connection configuration
113116
self.retry_config = retry_config or RetryConfig()
@@ -355,6 +358,10 @@ async def _dial_addr_single_attempt(self, addr: Multiaddr, peer_id: ID) -> INetC
355358
try:
356359
addr = Multiaddr(f"{addr}/p2p/{peer_id}")
357360
raw_conn = await self.transport.dial(addr)
361+
362+
# Enable PNET if psk is provvided
363+
if self.psk is not None:
364+
raw_conn = new_protected_conn(raw_conn, self.psk)
358365
except OpenConnectionError as error:
359366
logger.debug("fail to dial peer %s over base transport", peer_id)
360367
# Release pre-upgrade scope on failure
@@ -678,6 +685,10 @@ async def upgrade_inbound_raw_conn(
678685
:raises SwarmException: raised when security or muxer upgrade fails
679686
:return: network connection with security and multiplexing established
680687
"""
688+
# Enable PNET is psk is provided
689+
if self.psk is not None:
690+
raw_conn = new_protected_conn(raw_conn, self.psk)
691+
681692
# secure the conn and then mux the conn
682693
try:
683694
secured_conn = await self.upgrader.upgrade_security(raw_conn, False)

libp2p/security/pnet/__init__.py

Whitespace-only changes.

libp2p/security/pnet/protector.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from libp2p.abc import IRawConnection
2+
from libp2p.network.connection.raw_connection import RawConnection
3+
from libp2p.security.pnet.psk_conn import PskConn
4+
5+
6+
def new_protected_conn(conn: RawConnection | IRawConnection, psk: str) -> PskConn:
7+
if len(psk) != 64:
8+
raise ValueError("Expected 32-byte pre shared key (PSK)")
9+
10+
return PskConn(conn, psk)

libp2p/security/pnet/psk_conn.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import os
2+
3+
from Crypto.Cipher import Salsa20
4+
5+
from libp2p.abc import IRawConnection
6+
from libp2p.network.connection.raw_connection import RawConnection
7+
8+
9+
class PskConn(RawConnection):
10+
_psk: bytes
11+
_conn: RawConnection | IRawConnection
12+
13+
def __init__(self, conn: RawConnection | IRawConnection, psk: str) -> None:
14+
self._psk = bytes.fromhex(psk)
15+
self._conn = conn
16+
17+
self.read_cipher: Salsa20.Salsa20Cipher | None = None
18+
self.write_cipher: Salsa20.Salsa20Cipher | None = None
19+
20+
async def write(self, data: bytes) -> None:
21+
"""
22+
Encrpyts and writes data to the stream.
23+
On the first call, generates a 24-byte nonce and sends it first.
24+
"""
25+
if self.write_cipher is None:
26+
nonce = os.urandom(8)
27+
await self._conn.write(nonce)
28+
self.write_cipher = Salsa20.new(key=self._psk, nonce=nonce)
29+
30+
assert self.write_cipher is not None
31+
ciphertext = self.write_cipher.encrypt(data)
32+
33+
await self._conn.write(ciphertext)
34+
35+
async def read(self, n: int | None = None) -> bytes:
36+
"""
37+
Reads and decrypts data. On the first call, it reads a 8-byte
38+
nonce to initialize the decryption stream
39+
"""
40+
if self.read_cipher is None:
41+
nonce = await self._conn.read(8)
42+
if len(nonce) != 8:
43+
raise ValueError("short nonce from stream")
44+
45+
self.read_cipher = Salsa20.new(key=self._psk, nonce=nonce)
46+
47+
data = await self._conn.read(n)
48+
if not data:
49+
return b""
50+
51+
plaintext = self.read_cipher.decrypt(data)
52+
return plaintext
53+
54+
async def close(self) -> None:
55+
await self._conn.close()
56+
57+
def get_remote_address(self) -> tuple[str, int] | None:
58+
return self._conn.get_remote_address()

0 commit comments

Comments
 (0)