Skip to content

Commit c63724f

Browse files
authored
Merge pull request #1107 from sumanjeet0012/universal_connectivity
Improve RSA Key Compatibility, Public Key Extraction, and Pubsub Connection Handling
2 parents f8ec326 + ab50b31 commit c63724f

File tree

5 files changed

+102
-6
lines changed

5 files changed

+102
-6
lines changed

libp2p/crypto/rsa.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
from Crypto.Signature import (
99
pkcs1_15,
1010
)
11+
from cryptography.hazmat.backends import default_backend
12+
from cryptography.hazmat.primitives import serialization
13+
from cryptography.hazmat.primitives.serialization import load_der_public_key
1114

1215
from libp2p.crypto.exceptions import (
1316
CryptographyError,
@@ -59,7 +62,20 @@ def to_bytes(self) -> bytes:
5962

6063
@classmethod
6164
def from_bytes(cls, key_bytes: bytes) -> "RSAPublicKey":
62-
rsakey = RSA.import_key(key_bytes)
65+
try:
66+
rsakey = RSA.import_key(key_bytes)
67+
except ValueError:
68+
# PyCryptodome might fail on some PKIX formats, try using cryptography lib
69+
try:
70+
crypto_key = load_der_public_key(key_bytes, backend=default_backend())
71+
# Re-export in PKCS1 format that PyCryptodome understands
72+
pkcs1_bytes = crypto_key.public_bytes(
73+
encoding=serialization.Encoding.DER,
74+
format=serialization.PublicFormat.PKCS1,
75+
)
76+
rsakey = RSA.import_key(pkcs1_bytes)
77+
except Exception:
78+
raise
6379
validate_rsa_key_size(rsakey)
6480
return cls(rsakey)
6581

libp2p/peer/id.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from libp2p.crypto.keys import (
88
PublicKey,
99
)
10+
from libp2p.crypto.serialization import deserialize_public_key
1011

1112
# NOTE: On inlining...
1213
# See: https://github.com/libp2p/specs/issues/138
@@ -90,6 +91,32 @@ def from_pubkey(cls, key: PublicKey) -> "ID":
9091
mh_digest = multihash.digest(serialized_key, algo)
9192
return cls(mh_digest.encode())
9293

94+
def extract_public_key(self) -> PublicKey | None:
95+
"""
96+
Extract the public key from this peer ID if it uses an identity multihash.
97+
98+
For Ed25519 and other small keys, the public key is embedded directly
99+
in the peer ID using an identity multihash. For larger keys like RSA,
100+
the peer ID uses a SHA-256 hash and the key cannot be extracted.
101+
102+
Returns:
103+
The public key if it can be extracted, None otherwise.
104+
105+
"""
106+
try:
107+
# Decode the multihash to check if it's an identity hash
108+
mh_decoded = multihash.decode(self._bytes)
109+
110+
# Identity multihash func code is 0x00
111+
if mh_decoded.func == IDENTITY_MULTIHASH_CODE:
112+
# The digest is the serialized public key protobuf
113+
return deserialize_public_key(mh_decoded.digest)
114+
else:
115+
# Not an identity hash, key cannot be extracted
116+
return None
117+
except Exception:
118+
return None
119+
93120

94121
def sha256_digest(data: str | bytes) -> bytes:
95122
if isinstance(data, str):

libp2p/pubsub/pubsub.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,11 @@ async def wait_until_ready(self) -> None:
454454
await self.event_handle_dead_peer_queue_started.wait()
455455

456456
async def _handle_new_peer(self, peer_id: ID) -> None:
457+
# Check if we already have a pubsub stream with this peer to avoid duplicates
458+
if peer_id in self.peers:
459+
logger.debug("Peer %s already has pubsub stream, skipping", peer_id)
460+
return
461+
457462
if self.is_peer_blacklisted(peer_id):
458463
logger.debug("Rejecting blacklisted peer %s", peer_id)
459464
return
@@ -521,11 +526,28 @@ async def handle_dead_peer_queue(self) -> None:
521526
"""
522527
Continuously read from dead peer channel and close the stream
523528
between that peer and remove peer info from pubsub and pubsub router.
529+
Only removes the peer if there are no remaining active connections.
524530
"""
525531
async with self.dead_peer_receive_channel:
526532
self.event_handle_dead_peer_queue_started.set()
527533
async for peer_id in self.dead_peer_receive_channel:
528-
# Remove Peer
534+
# Check if peer still has active connections before removing
535+
# This prevents premature removal when multiple connections exist
536+
network = self.host.get_network()
537+
remaining_connections = network.get_connections(peer_id)
538+
if remaining_connections:
539+
# Filter out closed connections
540+
active_connections = [
541+
c for c in remaining_connections if not c.muxed_conn.is_closed
542+
]
543+
if active_connections:
544+
logger.debug(
545+
"Peer %s still has %d active connections, not removing",
546+
peer_id,
547+
len(active_connections),
548+
)
549+
continue
550+
# Remove Peer - no more active connections
529551
self._handle_dead_peer(peer_id)
530552

531553
def handle_subscription(

libp2p/pubsub/validators.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,40 @@ def signature_validator(msg: rpc_pb2.Message) -> bool:
2828
logger.debug("Reject because no signature attached for msg: %s", msg)
2929
return False
3030

31-
# Validate if message sender matches message signer,
32-
# i.e., check if `msg.key` matches `msg.from_id`
33-
msg_pubkey = deserialize_public_key(msg.key)
34-
if ID.from_pubkey(msg_pubkey) != msg.from_id:
31+
# Get the public key - either from msg.key or extracted from peer ID
32+
# For Ed25519 and other small keys, the key is embedded in the peer ID
33+
# For RSA keys (which are too large), the key is sent in msg.key
34+
msg_pubkey = None
35+
sender_id = ID(msg.from_id)
36+
37+
# First, try to extract public key from the peer ID (from_id)
38+
try:
39+
msg_pubkey = sender_id.extract_public_key()
40+
if msg_pubkey:
41+
logger.debug(
42+
"Extracted public key from peer ID, type: %s", msg_pubkey.get_type()
43+
)
44+
except Exception as e:
45+
logger.debug("Could not extract key from peer ID: %s", e)
46+
47+
# If we couldn't extract from peer ID, use msg.key
48+
if msg_pubkey is None:
49+
if not msg.key or len(msg.key) == 0:
50+
logger.debug(
51+
"Reject because no key attached and cannot extract from peer "
52+
"ID for msg: %s",
53+
msg,
54+
)
55+
return False
56+
logger.debug(
57+
"Using msg.key for signature verification, length: %d", len(msg.key)
58+
)
59+
try:
60+
msg_pubkey = deserialize_public_key(msg.key)
61+
except Exception as e:
62+
logger.debug("Failed to deserialize public key: %s", e)
63+
raise
64+
if ID.from_pubkey(msg_pubkey) != sender_id:
3565
logger.debug(
3666
"Reject because signing key does not match sender ID for msg: %s", msg
3767
)

newsfragments/1106.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved RSA key compatibility with other libp2p implementations, added public key extraction from peer IDs for Ed25519/Secp256k1 keys, and enhanced pubsub connection management to prevent premature peer removal and service crashes.

0 commit comments

Comments
 (0)