Skip to content

Commit 0784f1c

Browse files
committed
Fix webrtc-direct proto and certmanager
1 parent 55d1de3 commit 0784f1c

16 files changed

+298
-59
lines changed
File renamed without changes.

libp2p/transport/webrtc/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import sys
99
from .private_to_private.transport import WebRTCTransport
1010
from .private_to_public.transport import WebRTCDirectTransport
11-
from .constants import (
11+
from ..constants import (
1212
DEFAULT_ICE_SERVERS,
1313
SIGNALING_PROTOCOL,
1414
MUXER_PROTOCOL,
@@ -166,4 +166,4 @@ def webrtc(config: dict[str, Any] | None = None) -> WebRTCTransport:
166166

167167
def webrtc_direct(config: dict[str, Any] | None = None) -> WebRTCDirectTransport:
168168
"""Create a WebRTC-Direct transport instance (private-to-public)."""
169-
return WebRTCDirectTransport(config)
169+
return WebRTCDirectTransport(config)

libp2p/transport/webrtc/gen_certificate.py

Lines changed: 130 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import hashlib
44
import logging
55
from typing import Any
6-
6+
import trio
77
import base58
88
from cryptography import (
99
x509,
@@ -16,7 +16,7 @@
1616
serialization,
1717
)
1818
from cryptography.hazmat.primitives.asymmetric import (
19-
rsa,
19+
ec,
2020
)
2121
from cryptography.hazmat.primitives.asymmetric.rsa import (
2222
RSAPrivateKey as CryptoRSAPrivateKey,
@@ -37,56 +37,43 @@
3737
ID,
3838
)
3939

40+
from ..constants import (
41+
DEFAULT_CERTIFICATE_RENEWAL_THRESHOLD,
42+
DEFAULT_CERTIFICATE_LIFESPAN
43+
)
4044
SIGNAL_PROTOCOL = "/libp2p/webrtc/signal/1.0.0"
4145
logger = logging.getLogger("libp2p.transport.webrtc.certificate")
4246

43-
47+
# TODO: Once Datastore is implemented in python, add cert and priv_key storage
48+
# and management.
4449
class WebRTCCertificate:
4550
"""WebRTC certificate for connections"""
4651

47-
def __init__(self, cert: x509.Certificate, private_key: rsa.RSAPrivateKey) -> None:
52+
def __init__(self, cert: x509.Certificate, private_key: ec.EllipticCurvePrivateKey) -> None:
4853
self.cert = cert
49-
self.private_key = private_key
54+
self.private_key = private_key | None = None
5055
self._fingerprint: str | None = None
5156
self._certhash: str | None = None
52-
57+
self.cancel_scope: trio.CancelScope = None
5358
@classmethod
5459
def generate(cls) -> "WebRTCCertificate":
5560
"""Generate a new self-signed certificate for WebRTC"""
56-
# Generate private key
57-
private_key = rsa.generate_private_key(
58-
public_exponent=65537,
59-
key_size=2048,
60-
)
61-
61+
# Create instance first with None private key
62+
instance = cls.__new__(cls)
63+
instance._fingerprint = None
64+
instance._certhash = None
65+
66+
# Generate private key using the instance method
67+
private_key = instance.loadOrCreatePrivateKey()
68+
6269
# Create certificate
63-
common_name: Any = "libp2p-webrtc"
64-
subject = issuer = x509.Name(
65-
[
66-
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
67-
]
68-
)
69-
70-
cert = (
71-
x509.CertificateBuilder()
72-
.subject_name(subject)
73-
.issuer_name(issuer)
74-
.public_key(private_key.public_key())
75-
.serial_number(x509.random_serial_number())
76-
.not_valid_before(datetime.datetime.utcnow())
77-
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365))
78-
.add_extension(
79-
x509.SubjectAlternativeName(
80-
[
81-
x509.DNSName("localhost"),
82-
]
83-
),
84-
critical=False,
85-
)
86-
.sign(private_key, hashes.SHA256())
87-
)
88-
89-
return cls(cert, private_key)
70+
cert, pem = instance.loadOrCreateCertificate()
71+
72+
# Set the certificate and private key on the instance
73+
instance.cert = cert
74+
instance.private_key = private_key
75+
76+
return instance
9077

9178
@property
9279
def fingerprint(self) -> str:
@@ -208,7 +195,111 @@ def validate_pem_export(self) -> bool:
208195
raise ValueError("Invalid private key PEM footer")
209196

210197
return True
198+
199+
def _getCertRenewalTime(self) -> int:
200+
# Calculate the renewal time in milliseconds until certificate expiry minus the renewal threshold.
201+
renew_at = self.cert.not_valid_after - datetime.timedelta(milliseconds=DEFAULT_CERTIFICATE_RENEWAL_THRESHOLD)
202+
now = datetime.datetime.now(datetime.timezone.utc)
203+
renewal_time_ms = int((renew_at - now).total_seconds() * 1000)
204+
return renewal_time_ms if renewal_time_ms > 0 else 100
205+
206+
207+
def loadOrCreatePrivateKey(self, forceRenew = False) -> ec.EllipticCurvePrivateKey:
208+
"""
209+
Load the existing private key if available, or generate a new one.
210+
211+
Args:
212+
forceRenew (bool): If True, always generate a new private key even if one already exists.
213+
If False, return the existing private key if present.
211214
215+
Returns:
216+
ec.EllipticCurvePrivateKey: The loaded or newly generated elliptic curve private key.
217+
"""
218+
# If private key is already present and not enforced to create new
219+
if self.private_key != None and not forceRenew:
220+
return self.private_key
221+
222+
# Create a new private key
223+
self.private_key = ec.generate_private_key(ec.SECP256R1())
224+
return self.private_key
225+
226+
def loadOrCreateCertificate(
227+
self,
228+
private_key: ec.EllipticCurvePrivateKey | None,
229+
forceRenew: bool = False
230+
) -> tuple[x509.Certificate, str, str]:
231+
"""
232+
Generate or load a self-signed WebRTC certificate for libp2p direct connections.
233+
234+
If a valid certificate already exists and is not expired, and the public key matches,
235+
it will be reused unless forceRenew is True. Otherwise, a new certificate is generated.
236+
237+
Args:
238+
private_key (ec.EllipticCurvePrivateKey | None): The private key to use for signing the certificate.
239+
If None, uses self.private_key.
240+
forceRenew (bool): If True, always generate a new certificate even if the current one is valid.
241+
242+
Returns:
243+
tuple[x509.Certificate, str, str]: The certificate object, its PEM-encoded string, and the base64url-encoded SHA-256 hash of the certificate.
244+
245+
Raises:
246+
Exception: If no private key is available to issue a certificate.
247+
"""
248+
if private_key is None:
249+
if self.private_key is None:
250+
raise Exception("Can't issue certificate without private key")
251+
private_key = self.private_key
252+
253+
if self.cert is not None and not forceRenew:
254+
# Check if certificate has to be renewed
255+
renewal_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(milliseconds=DEFAULT_CERTIFICATE_RENEWAL_THRESHOLD)
256+
isExpired = renewal_time >= self.cert.not_valid_after
257+
if not isExpired:
258+
# Check if the certificate's public key matches with provided key pair
259+
if self.cert.public_key().public_numbers() == private_key.public_key().public_numbers():
260+
cert_pem, _ = self.to_pem()
261+
cert_hash = self.certhash()
262+
return (self.cert, cert_pem, cert_hash)
263+
264+
common_name: str = "libp2p-webrtc"
265+
subject = issuer = x509.Name(
266+
[
267+
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
268+
]
269+
)
270+
271+
cert = (
272+
x509.CertificateBuilder()
273+
.subject_name(subject)
274+
.issuer_name(issuer)
275+
.public_key(private_key.public_key())
276+
.serial_number(x509.random_serial_number())
277+
.not_valid_before(datetime.datetime.now(datetime.timezone.utc))
278+
.not_valid_after(
279+
datetime.datetime.now(datetime.timezone.utc) +
280+
datetime.timedelta(milliseconds=DEFAULT_CERTIFICATE_LIFESPAN)
281+
)
282+
.add_extension(
283+
x509.SubjectAlternativeName(
284+
[
285+
x509.DNSName("localhost"),
286+
]
287+
),
288+
critical=False,
289+
)
290+
.sign(private_key, hashes.SHA256())
291+
)
292+
self.cert = cert
293+
pem = cert.public_bytes(Encoding.PEM).decode('utf-8')
294+
cert_pem, _ = self.to_pem()
295+
cert_hash = self.certhash()
296+
return (cert, cert_pem, cert_hash)
297+
298+
async def renewal_loop(self):
299+
while True:
300+
await trio.sleep(self._getCertRenewalTime)
301+
logger.Debug("Renewing TLS certificate")
302+
await self.loadOrCreateCertificate(self.private_key, True)
212303

213304
def create_webrtc_multiaddr(
214305
ip: str, peer_id: ID, certhash: str, direct: bool = False

libp2p/transport/webrtc/private_to_private/initiate_connection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from ..async_bridge import TrioSafeWebRTCOperations
1919
from ..connection import WebRTCRawConnection
20-
from ..constants import (
20+
from ...constants import (
2121
DEFAULT_DIAL_TIMEOUT,
2222
SIGNALING_PROTOCOL,
2323
SDPHandshakeError,

libp2p/transport/webrtc/private_to_private/listener.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
)
1515
from libp2p.relay.circuit_v2.config import RelayConfig
1616

17-
from ..constants import (
17+
from ...constants import (
1818
DEFAULT_DIAL_TIMEOUT,
1919
DEFAULT_ICE_SERVERS,
2020
SIGNALING_PROTOCOL,

libp2p/transport/webrtc/private_to_private/signaling_stream_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from libp2p.peer.id import ID
1616

1717
from ..connection import WebRTCRawConnection
18-
from ..constants import WebRTCError
18+
from ...constants import WebRTCError
1919
from .pb import Message
2020

2121
logger = logging.getLogger("webrtc.private.signaling_stream_handler")

libp2p/transport/webrtc/private_to_private/transport.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from libp2p.host.basic_host import IHost
1818
from libp2p.transport.exceptions import OpenConnectionError
1919

20-
from ..constants import (
20+
from ...constants import (
2121
DEFAULT_DIAL_TIMEOUT,
2222
DEFAULT_ICE_SERVERS,
2323
SIGNALING_PROTOCOL,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
syntax = "proto3";
2+
3+
message Message {
4+
enum Flag {
5+
// The sender will no longer send messages on the stream. The recipient
6+
// should send a FIN_ACK back to the sender.
7+
FIN = 0;
8+
9+
// The sender will no longer read messages on the stream. Incoming data is
10+
// being discarded on receipt.
11+
STOP_SENDING = 1;
12+
13+
// The sender abruptly terminates the sending part of the stream. The
14+
// receiver can discard any data that it already received on that stream.
15+
RESET = 2;
16+
17+
// The sender previously received a FIN.
18+
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=1484907
19+
FIN_ACK = 3;
20+
}
21+
22+
optional Flag flag = 1;
23+
24+
optional bytes message = 2;
25+
}

libp2p/transport/webrtc/private_to_public/pb/message_pb2.py

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)