mirror of
https://github.com/varun-r-mallya/py-libp2p.git
synced 2026-02-12 16:10:57 +00:00
temp: impl security modile
This commit is contained in:
@ -1,15 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
QUIC Connection implementation for py-libp2p Module 3.
|
QUIC Connection implementation.
|
||||||
Uses aioquic's sans-IO core with trio for async operations.
|
Uses aioquic's sans-IO core with trio for async operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any, Optional
|
||||||
|
|
||||||
from aioquic.quic import events
|
from aioquic.quic import events
|
||||||
from aioquic.quic.connection import QuicConnection
|
from aioquic.quic.connection import QuicConnection
|
||||||
|
from cryptography import x509
|
||||||
import multiaddr
|
import multiaddr
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ from .exceptions import (
|
|||||||
from .stream import QUICStream, StreamDirection
|
from .stream import QUICStream, StreamDirection
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .security import QUICTLSConfigManager
|
||||||
from .transport import QUICTransport
|
from .transport import QUICTransport
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -45,6 +47,7 @@ class QUICConnection(IRawConnection, IMuxedConn):
|
|||||||
|
|
||||||
Features:
|
Features:
|
||||||
- Native QUIC stream multiplexing
|
- Native QUIC stream multiplexing
|
||||||
|
- Integrated libp2p TLS security with peer identity verification
|
||||||
- Resource-aware stream management
|
- Resource-aware stream management
|
||||||
- Comprehensive error handling
|
- Comprehensive error handling
|
||||||
- Flow control integration
|
- Flow control integration
|
||||||
@ -69,10 +72,11 @@ class QUICConnection(IRawConnection, IMuxedConn):
|
|||||||
is_initiator: bool,
|
is_initiator: bool,
|
||||||
maddr: multiaddr.Multiaddr,
|
maddr: multiaddr.Multiaddr,
|
||||||
transport: "QUICTransport",
|
transport: "QUICTransport",
|
||||||
|
security_manager: Optional["QUICTLSConfigManager"] = None,
|
||||||
resource_scope: Any | None = None,
|
resource_scope: Any | None = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize enhanced QUIC connection.
|
Initialize enhanced QUIC connection with security integration.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
quic_connection: aioquic QuicConnection instance
|
quic_connection: aioquic QuicConnection instance
|
||||||
@ -82,6 +86,7 @@ class QUICConnection(IRawConnection, IMuxedConn):
|
|||||||
is_initiator: Whether this is the connection initiator
|
is_initiator: Whether this is the connection initiator
|
||||||
maddr: Multiaddr for this connection
|
maddr: Multiaddr for this connection
|
||||||
transport: Parent QUIC transport
|
transport: Parent QUIC transport
|
||||||
|
security_manager: Security manager for TLS/certificate handling
|
||||||
resource_scope: Resource manager scope for tracking
|
resource_scope: Resource manager scope for tracking
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -92,6 +97,7 @@ class QUICConnection(IRawConnection, IMuxedConn):
|
|||||||
self.__is_initiator = is_initiator
|
self.__is_initiator = is_initiator
|
||||||
self._maddr = maddr
|
self._maddr = maddr
|
||||||
self._transport = transport
|
self._transport = transport
|
||||||
|
self._security_manager = security_manager
|
||||||
self._resource_scope = resource_scope
|
self._resource_scope = resource_scope
|
||||||
|
|
||||||
# Trio networking - socket may be provided by listener
|
# Trio networking - socket may be provided by listener
|
||||||
@ -120,6 +126,11 @@ class QUICConnection(IRawConnection, IMuxedConn):
|
|||||||
self._established = False
|
self._established = False
|
||||||
self._started = False
|
self._started = False
|
||||||
self._handshake_completed = False
|
self._handshake_completed = False
|
||||||
|
self._peer_verified = False
|
||||||
|
|
||||||
|
# Security state
|
||||||
|
self._peer_certificate: Optional[x509.Certificate] = None
|
||||||
|
self._handshake_events = []
|
||||||
|
|
||||||
# Background task management
|
# Background task management
|
||||||
self._background_tasks_started = False
|
self._background_tasks_started = False
|
||||||
@ -141,7 +152,8 @@ class QUICConnection(IRawConnection, IMuxedConn):
|
|||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Created QUIC connection to {peer_id} "
|
f"Created QUIC connection to {peer_id} "
|
||||||
f"(initiator: {is_initiator}, addr: {remote_addr})"
|
f"(initiator: {is_initiator}, addr: {remote_addr}, "
|
||||||
|
"security: {security_manager is not None})"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _calculate_initial_stream_id(self) -> int:
|
def _calculate_initial_stream_id(self) -> int:
|
||||||
@ -183,6 +195,11 @@ class QUICConnection(IRawConnection, IMuxedConn):
|
|||||||
"""Check if connection has been started."""
|
"""Check if connection has been started."""
|
||||||
return self._started
|
return self._started
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_peer_verified(self) -> bool:
|
||||||
|
"""Check if peer identity has been verified."""
|
||||||
|
return self._peer_verified
|
||||||
|
|
||||||
def multiaddr(self) -> multiaddr.Multiaddr:
|
def multiaddr(self) -> multiaddr.Multiaddr:
|
||||||
"""Get the multiaddr for this connection."""
|
"""Get the multiaddr for this connection."""
|
||||||
return self._maddr
|
return self._maddr
|
||||||
@ -288,8 +305,8 @@ class QUICConnection(IRawConnection, IMuxedConn):
|
|||||||
f"{self.CONNECTION_HANDSHAKE_TIMEOUT}s"
|
f"{self.CONNECTION_HANDSHAKE_TIMEOUT}s"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify peer identity if required
|
# Verify peer identity using security manager
|
||||||
await self.verify_peer_identity()
|
await self._verify_peer_identity_with_security()
|
||||||
|
|
||||||
self._established = True
|
self._established = True
|
||||||
logger.info(f"QUIC connection established with {self._peer_id}")
|
logger.info(f"QUIC connection established with {self._peer_id}")
|
||||||
@ -354,6 +371,205 @@ class QUICConnection(IRawConnection, IMuxedConn):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in periodic maintenance: {e}")
|
logger.error(f"Error in periodic maintenance: {e}")
|
||||||
|
|
||||||
|
# Security and identity methods
|
||||||
|
|
||||||
|
async def _verify_peer_identity_with_security(self) -> None:
|
||||||
|
"""
|
||||||
|
Verify peer identity using integrated security manager.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
QUICPeerVerificationError: If peer verification fails
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self._security_manager:
|
||||||
|
logger.warning("No security manager available for peer verification")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Extract peer certificate from TLS handshake
|
||||||
|
await self._extract_peer_certificate()
|
||||||
|
|
||||||
|
if not self._peer_certificate:
|
||||||
|
logger.warning("No peer certificate available for verification")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Validate certificate format and accessibility
|
||||||
|
if not self._validate_peer_certificate():
|
||||||
|
raise QUICPeerVerificationError("Peer certificate validation failed")
|
||||||
|
|
||||||
|
# Verify peer identity using security manager
|
||||||
|
verified_peer_id = self._security_manager.verify_peer_identity(
|
||||||
|
self._peer_certificate,
|
||||||
|
self._peer_id, # Expected peer ID for outbound connections
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update peer ID if it wasn't known (inbound connections)
|
||||||
|
if not self._peer_id:
|
||||||
|
self._peer_id = verified_peer_id
|
||||||
|
logger.info(f"Discovered peer ID from certificate: {verified_peer_id}")
|
||||||
|
elif self._peer_id != verified_peer_id:
|
||||||
|
raise QUICPeerVerificationError(
|
||||||
|
f"Peer ID mismatch: expected {self._peer_id}, "
|
||||||
|
f"got {verified_peer_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._peer_verified = True
|
||||||
|
logger.info(f"Peer identity verified successfully: {verified_peer_id}")
|
||||||
|
|
||||||
|
except QUICPeerVerificationError:
|
||||||
|
# Re-raise verification errors as-is
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
# Wrap other errors in verification error
|
||||||
|
raise QUICPeerVerificationError(f"Peer verification failed: {e}") from e
|
||||||
|
|
||||||
|
async def _extract_peer_certificate(self) -> None:
|
||||||
|
"""Extract peer certificate from completed TLS handshake."""
|
||||||
|
try:
|
||||||
|
# Get peer certificate from aioquic TLS context
|
||||||
|
# Based on aioquic source code: QuicConnection.tls._peer_certificate
|
||||||
|
if hasattr(self._quic, "tls") and self._quic.tls:
|
||||||
|
tls_context = self._quic.tls
|
||||||
|
|
||||||
|
# Check if peer certificate is available in TLS context
|
||||||
|
if (
|
||||||
|
hasattr(tls_context, "_peer_certificate")
|
||||||
|
and tls_context._peer_certificate
|
||||||
|
):
|
||||||
|
# aioquic stores the peer certificate as cryptography
|
||||||
|
# x509.Certificate
|
||||||
|
self._peer_certificate = tls_context._peer_certificate
|
||||||
|
logger.debug(
|
||||||
|
f"Extracted peer certificate: {self._peer_certificate.subject}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug("No peer certificate found in TLS context")
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("No TLS context available for certificate extraction")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to extract peer certificate: {e}")
|
||||||
|
|
||||||
|
# Try alternative approach - check if certificate is in handshake events
|
||||||
|
try:
|
||||||
|
# Some versions of aioquic might expose certificate differently
|
||||||
|
if hasattr(self._quic, "configuration") and self._quic.configuration:
|
||||||
|
config = self._quic.configuration
|
||||||
|
if hasattr(config, "certificate") and config.certificate:
|
||||||
|
# This would be the local certificate, not peer certificate
|
||||||
|
# but we can use it for debugging
|
||||||
|
logger.debug("Found local certificate in configuration")
|
||||||
|
|
||||||
|
except Exception as inner_e:
|
||||||
|
logger.debug(
|
||||||
|
f"Alternative certificate extraction also failed: {inner_e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_peer_certificate(self) -> Optional[x509.Certificate]:
|
||||||
|
"""
|
||||||
|
Get the peer's TLS certificate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The peer's X.509 certificate, or None if not available
|
||||||
|
|
||||||
|
"""
|
||||||
|
# If we don't have a certificate yet, try to extract it
|
||||||
|
if not self._peer_certificate and self._handshake_completed:
|
||||||
|
await self._extract_peer_certificate()
|
||||||
|
|
||||||
|
return self._peer_certificate
|
||||||
|
|
||||||
|
def _validate_peer_certificate(self) -> bool:
|
||||||
|
"""
|
||||||
|
Validate that the peer certificate is properly formatted and accessible.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if certificate is valid and accessible, False otherwise
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self._peer_certificate:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Basic validation - try to access certificate properties
|
||||||
|
subject = self._peer_certificate.subject
|
||||||
|
serial_number = self._peer_certificate.serial_number
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Certificate validation - Subject: {subject}, Serial: {serial_number}"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Certificate validation failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_security_manager(self) -> Optional["QUICTLSConfigManager"]:
|
||||||
|
"""Get the security manager for this connection."""
|
||||||
|
return self._security_manager
|
||||||
|
|
||||||
|
def get_security_info(self) -> dict[str, Any]:
|
||||||
|
"""Get security-related information about the connection."""
|
||||||
|
info: dict[str, bool | Any | None]= {
|
||||||
|
"peer_verified": self._peer_verified,
|
||||||
|
"handshake_complete": self._handshake_completed,
|
||||||
|
"peer_id": str(self._peer_id) if self._peer_id else None,
|
||||||
|
"local_peer_id": str(self._local_peer_id),
|
||||||
|
"is_initiator": self.__is_initiator,
|
||||||
|
"has_certificate": self._peer_certificate is not None,
|
||||||
|
"security_manager_available": self._security_manager is not None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add certificate details if available
|
||||||
|
if self._peer_certificate:
|
||||||
|
try:
|
||||||
|
info.update(
|
||||||
|
{
|
||||||
|
"certificate_subject": str(self._peer_certificate.subject),
|
||||||
|
"certificate_issuer": str(self._peer_certificate.issuer),
|
||||||
|
"certificate_serial": str(self._peer_certificate.serial_number),
|
||||||
|
"certificate_not_before": (
|
||||||
|
self._peer_certificate.not_valid_before.isoformat()
|
||||||
|
),
|
||||||
|
"certificate_not_after": (
|
||||||
|
self._peer_certificate.not_valid_after.isoformat()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
info["certificate_error"] = str(e)
|
||||||
|
|
||||||
|
# Add TLS context debug info
|
||||||
|
try:
|
||||||
|
if hasattr(self._quic, "tls") and self._quic.tls:
|
||||||
|
tls_info = {
|
||||||
|
"tls_context_available": True,
|
||||||
|
"tls_state": getattr(self._quic.tls, "state", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for peer certificate in TLS context
|
||||||
|
if hasattr(self._quic.tls, "_peer_certificate"):
|
||||||
|
tls_info["tls_peer_certificate_available"] = (
|
||||||
|
self._quic.tls._peer_certificate is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
info["tls_debug"] = tls_info
|
||||||
|
else:
|
||||||
|
info["tls_debug"] = {"tls_context_available": False}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
info["tls_debug"] = {"error": str(e)}
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
# Legacy compatibility for existing code
|
||||||
|
async def verify_peer_identity(self) -> None:
|
||||||
|
"""
|
||||||
|
Legacy method for compatibility - delegates to security manager.
|
||||||
|
"""
|
||||||
|
await self._verify_peer_identity_with_security()
|
||||||
|
|
||||||
# Stream management methods (IMuxedConn interface)
|
# Stream management methods (IMuxedConn interface)
|
||||||
|
|
||||||
async def open_stream(self, timeout: float = 5.0) -> QUICStream:
|
async def open_stream(self, timeout: float = 5.0) -> QUICStream:
|
||||||
@ -520,9 +736,16 @@ class QUICConnection(IRawConnection, IMuxedConn):
|
|||||||
async def _handle_handshake_completed(
|
async def _handle_handshake_completed(
|
||||||
self, event: events.HandshakeCompleted
|
self, event: events.HandshakeCompleted
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle handshake completion."""
|
"""Handle handshake completion with security integration."""
|
||||||
logger.debug("QUIC handshake completed")
|
logger.debug("QUIC handshake completed")
|
||||||
self._handshake_completed = True
|
self._handshake_completed = True
|
||||||
|
|
||||||
|
# Store handshake event for security verification
|
||||||
|
self._handshake_events.append(event)
|
||||||
|
|
||||||
|
# Try to extract certificate information after handshake
|
||||||
|
await self._extract_peer_certificate()
|
||||||
|
|
||||||
self._connected_event.set()
|
self._connected_event.set()
|
||||||
|
|
||||||
async def _handle_connection_terminated(
|
async def _handle_connection_terminated(
|
||||||
@ -786,39 +1009,6 @@ class QUICConnection(IRawConnection, IMuxedConn):
|
|||||||
|
|
||||||
# Utility and monitoring methods
|
# Utility and monitoring methods
|
||||||
|
|
||||||
async def verify_peer_identity(self) -> None:
|
|
||||||
"""
|
|
||||||
Verify the remote peer's identity using TLS certificate.
|
|
||||||
This implements the libp2p TLS handshake verification.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Extract peer ID from TLS certificate
|
|
||||||
# This should match the expected peer ID
|
|
||||||
cert_peer_id = self._extract_peer_id_from_cert()
|
|
||||||
|
|
||||||
if self._peer_id and cert_peer_id != self._peer_id:
|
|
||||||
raise QUICPeerVerificationError(
|
|
||||||
f"Peer ID mismatch: expected {self._peer_id}, got {cert_peer_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self._peer_id:
|
|
||||||
self._peer_id = cert_peer_id
|
|
||||||
|
|
||||||
logger.debug(f"Verified peer identity: {self._peer_id}")
|
|
||||||
|
|
||||||
except NotImplementedError:
|
|
||||||
logger.warning("Peer identity verification not implemented - skipping")
|
|
||||||
# For now, we'll skip verification during development
|
|
||||||
except Exception as e:
|
|
||||||
raise QUICPeerVerificationError(f"Peer verification failed: {e}") from e
|
|
||||||
|
|
||||||
def _extract_peer_id_from_cert(self) -> ID:
|
|
||||||
"""Extract peer ID from TLS certificate."""
|
|
||||||
# TODO: Implement proper libp2p TLS certificate parsing
|
|
||||||
# This should extract the peer ID from the certificate extension
|
|
||||||
# according to the libp2p TLS specification
|
|
||||||
raise NotImplementedError("TLS certificate parsing not yet implemented")
|
|
||||||
|
|
||||||
def get_stream_stats(self) -> dict[str, Any]:
|
def get_stream_stats(self) -> dict[str, Any]:
|
||||||
"""Get stream statistics for monitoring."""
|
"""Get stream statistics for monitoring."""
|
||||||
return {
|
return {
|
||||||
@ -869,6 +1059,7 @@ class QUICConnection(IRawConnection, IMuxedConn):
|
|||||||
f"QUICConnection(peer={self._peer_id}, "
|
f"QUICConnection(peer={self._peer_id}, "
|
||||||
f"addr={self._remote_addr}, "
|
f"addr={self._remote_addr}, "
|
||||||
f"initiator={self.__is_initiator}, "
|
f"initiator={self.__is_initiator}, "
|
||||||
|
f"verified={self._peer_verified}, "
|
||||||
f"established={self._established}, "
|
f"established={self._established}, "
|
||||||
f"streams={len(self._streams)})"
|
f"streams={len(self._streams)})"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,35 +1,477 @@
|
|||||||
"""
|
"""
|
||||||
Basic QUIC Security implementation for Module 1.
|
QUIC Security implementation for py-libp2p Module 5.
|
||||||
This provides minimal TLS configuration for QUIC transport.
|
Implements libp2p TLS specification for QUIC transport with peer identity integration.
|
||||||
Full implementation will be in Module 5.
|
Based on go-libp2p and js-libp2p security patterns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import os
|
import logging
|
||||||
import tempfile
|
import time
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from libp2p.crypto.keys import PrivateKey
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
||||||
|
from cryptography.x509.oid import NameOID
|
||||||
|
|
||||||
|
from libp2p.crypto.ed25519 import Ed25519PublicKey
|
||||||
|
from libp2p.crypto.keys import PrivateKey, PublicKey
|
||||||
|
from libp2p.crypto.secp256k1 import Secp256k1PublicKey
|
||||||
from libp2p.peer.id import ID
|
from libp2p.peer.id import ID
|
||||||
|
|
||||||
from .exceptions import QUICSecurityError
|
from .exceptions import (
|
||||||
|
QUICCertificateError,
|
||||||
|
QUICPeerVerificationError,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# libp2p TLS Extension OID - Official libp2p specification
|
||||||
|
LIBP2P_TLS_EXTENSION_OID = x509.ObjectIdentifier("1.3.6.1.4.1.53594.1.1")
|
||||||
|
|
||||||
|
# Certificate validity period
|
||||||
|
CERTIFICATE_VALIDITY_DAYS = 365
|
||||||
|
CERTIFICATE_NOT_BEFORE_BUFFER = 3600 # 1 hour before now
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TLSConfig:
|
class TLSConfig:
|
||||||
"""TLS configuration for QUIC transport."""
|
"""TLS configuration for QUIC transport with libp2p extensions."""
|
||||||
|
|
||||||
cert_file: str
|
certificate: x509.Certificate
|
||||||
key_file: str
|
private_key: ec.EllipticCurvePrivateKey | rsa.RSAPrivateKey
|
||||||
ca_file: str | None = None
|
peer_id: ID
|
||||||
|
|
||||||
|
def get_certificate_der(self) -> bytes:
|
||||||
|
"""Get certificate in DER format for aioquic."""
|
||||||
|
return self.certificate.public_bytes(serialization.Encoding.DER)
|
||||||
|
|
||||||
|
def get_private_key_der(self) -> bytes:
|
||||||
|
"""Get private key in DER format for aioquic."""
|
||||||
|
return self.private_key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.DER,
|
||||||
|
format=serialization.PrivateFormat.PKCS8,
|
||||||
|
encryption_algorithm=serialization.NoEncryption(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LibP2PExtensionHandler:
|
||||||
|
"""
|
||||||
|
Handles libp2p-specific TLS extensions for peer identity verification.
|
||||||
|
|
||||||
|
Based on libp2p TLS specification:
|
||||||
|
https://github.com/libp2p/specs/blob/master/tls/tls.md
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_signed_key_extension(
|
||||||
|
libp2p_private_key: PrivateKey, cert_public_key: bytes
|
||||||
|
) -> bytes:
|
||||||
|
"""
|
||||||
|
Create the libp2p Public Key Extension with signed key proof.
|
||||||
|
|
||||||
|
The extension contains:
|
||||||
|
1. The libp2p public key
|
||||||
|
2. A signature proving ownership of the private key
|
||||||
|
|
||||||
|
Args:
|
||||||
|
libp2p_private_key: The libp2p identity private key
|
||||||
|
cert_public_key: The certificate's public key bytes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ASN.1 encoded extension value
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get the libp2p public key
|
||||||
|
libp2p_public_key = libp2p_private_key.get_public_key()
|
||||||
|
|
||||||
|
# Create the signature payload: "libp2p-tls-handshake:" + cert_public_key
|
||||||
|
signature_payload = b"libp2p-tls-handshake:" + cert_public_key
|
||||||
|
|
||||||
|
# Sign the payload with the libp2p private key
|
||||||
|
signature = libp2p_private_key.sign(signature_payload)
|
||||||
|
|
||||||
|
# Create the SignedKey structure (simplified ASN.1 encoding)
|
||||||
|
# In a full implementation, this would use proper ASN.1 encoding
|
||||||
|
public_key_bytes = libp2p_public_key.serialize()
|
||||||
|
|
||||||
|
# Simple encoding: [public_key_length][public_key][signature_length][signature]
|
||||||
|
extension_data = (
|
||||||
|
len(public_key_bytes).to_bytes(4, byteorder="big")
|
||||||
|
+ public_key_bytes
|
||||||
|
+ len(signature).to_bytes(4, byteorder="big")
|
||||||
|
+ signature
|
||||||
|
)
|
||||||
|
|
||||||
|
return extension_data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise QUICCertificateError(
|
||||||
|
f"Failed to create signed key extension: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_signed_key_extension(extension_data: bytes) -> Tuple[PublicKey, bytes]:
|
||||||
|
"""
|
||||||
|
Parse the libp2p Public Key Extension to extract public key and signature.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
extension_data: The extension data bytes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (libp2p_public_key, signature)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
QUICCertificateError: If extension parsing fails
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
# Parse public key length and data
|
||||||
|
if len(extension_data) < 4:
|
||||||
|
raise QUICCertificateError("Extension too short for public key length")
|
||||||
|
|
||||||
|
public_key_length = int.from_bytes(
|
||||||
|
extension_data[offset : offset + 4], byteorder="big"
|
||||||
|
)
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
if len(extension_data) < offset + public_key_length:
|
||||||
|
raise QUICCertificateError("Extension too short for public key data")
|
||||||
|
|
||||||
|
public_key_bytes = extension_data[offset : offset + public_key_length]
|
||||||
|
offset += public_key_length
|
||||||
|
|
||||||
|
# Parse signature length and data
|
||||||
|
if len(extension_data) < offset + 4:
|
||||||
|
raise QUICCertificateError("Extension too short for signature length")
|
||||||
|
|
||||||
|
signature_length = int.from_bytes(
|
||||||
|
extension_data[offset : offset + 4], byteorder="big"
|
||||||
|
)
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
if len(extension_data) < offset + signature_length:
|
||||||
|
raise QUICCertificateError("Extension too short for signature data")
|
||||||
|
|
||||||
|
signature = extension_data[offset : offset + signature_length]
|
||||||
|
|
||||||
|
# Deserialize the public key
|
||||||
|
# This is a simplified approach - full implementation would handle all key types
|
||||||
|
public_key = LibP2PKeyConverter.deserialize_public_key(public_key_bytes)
|
||||||
|
|
||||||
|
return public_key, signature
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise QUICCertificateError(
|
||||||
|
f"Failed to parse signed key extension: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
class LibP2PKeyConverter:
|
||||||
|
"""
|
||||||
|
Converts between libp2p key formats and cryptography library formats.
|
||||||
|
Handles different key types: Ed25519, Secp256k1, RSA, ECDSA.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def libp2p_to_tls_private_key(
|
||||||
|
libp2p_key: PrivateKey,
|
||||||
|
) -> ec.EllipticCurvePrivateKey | rsa.RSAPrivateKey:
|
||||||
|
"""
|
||||||
|
Convert libp2p private key to TLS-compatible private key.
|
||||||
|
|
||||||
|
For certificate generation, we create a separate ephemeral key
|
||||||
|
rather than using the libp2p identity key directly.
|
||||||
|
"""
|
||||||
|
# For QUIC, we prefer ECDSA keys for smaller certificates
|
||||||
|
# Generate ephemeral P-256 key for certificate signing
|
||||||
|
private_key = ec.generate_private_key(ec.SECP256R1())
|
||||||
|
return private_key
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def serialize_public_key(public_key: PublicKey) -> bytes:
|
||||||
|
"""Serialize libp2p public key to bytes."""
|
||||||
|
return public_key.serialize()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def deserialize_public_key(key_bytes: bytes) -> PublicKey:
|
||||||
|
"""
|
||||||
|
Deserialize libp2p public key from bytes.
|
||||||
|
|
||||||
|
This is a simplified implementation - full version would handle
|
||||||
|
all libp2p key types and proper deserialization.
|
||||||
|
"""
|
||||||
|
# For now, assume Ed25519 keys (most common in libp2p)
|
||||||
|
# Full implementation would detect key type from bytes
|
||||||
|
try:
|
||||||
|
return Ed25519PublicKey.deserialize(key_bytes)
|
||||||
|
except Exception:
|
||||||
|
# Fallback to other key types
|
||||||
|
try:
|
||||||
|
return Secp256k1PublicKey.deserialize(key_bytes)
|
||||||
|
except Exception:
|
||||||
|
raise QUICCertificateError("Unsupported key type in extension")
|
||||||
|
|
||||||
|
|
||||||
|
class CertificateGenerator:
|
||||||
|
"""
|
||||||
|
Generates X.509 certificates with libp2p peer identity extensions.
|
||||||
|
Follows libp2p TLS specification for QUIC transport.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.extension_handler = LibP2PExtensionHandler()
|
||||||
|
self.key_converter = LibP2PKeyConverter()
|
||||||
|
|
||||||
|
def generate_certificate(
|
||||||
|
self,
|
||||||
|
libp2p_private_key: PrivateKey,
|
||||||
|
peer_id: ID,
|
||||||
|
validity_days: int = CERTIFICATE_VALIDITY_DAYS,
|
||||||
|
) -> TLSConfig:
|
||||||
|
"""
|
||||||
|
Generate a TLS certificate with embedded libp2p peer identity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
libp2p_private_key: The libp2p identity private key
|
||||||
|
peer_id: The libp2p peer ID
|
||||||
|
validity_days: Certificate validity period in days
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TLSConfig with certificate and private key
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
QUICCertificateError: If certificate generation fails
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Generate ephemeral private key for certificate
|
||||||
|
cert_private_key = self.key_converter.libp2p_to_tls_private_key(
|
||||||
|
libp2p_private_key
|
||||||
|
)
|
||||||
|
cert_public_key = cert_private_key.public_key()
|
||||||
|
|
||||||
|
# Get certificate public key bytes for extension
|
||||||
|
cert_public_key_bytes = cert_public_key.public_bytes(
|
||||||
|
encoding=serialization.Encoding.DER,
|
||||||
|
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create libp2p extension with signed key proof
|
||||||
|
extension_data = self.extension_handler.create_signed_key_extension(
|
||||||
|
libp2p_private_key, cert_public_key_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set validity period
|
||||||
|
now = time.time()
|
||||||
|
not_before = time.gmtime(now - CERTIFICATE_NOT_BEFORE_BUFFER)
|
||||||
|
not_after = time.gmtime(now + (validity_days * 24 * 3600))
|
||||||
|
|
||||||
|
# Build certificate
|
||||||
|
certificate = (
|
||||||
|
x509.CertificateBuilder()
|
||||||
|
.subject_name(
|
||||||
|
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, str(peer_id))])
|
||||||
|
)
|
||||||
|
.issuer_name(
|
||||||
|
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, str(peer_id))])
|
||||||
|
)
|
||||||
|
.public_key(cert_public_key)
|
||||||
|
.serial_number(int(now)) # Use timestamp as serial number
|
||||||
|
.not_valid_before(time.struct_time(not_before))
|
||||||
|
.not_valid_after(time.struct_time(not_after))
|
||||||
|
.add_extension(
|
||||||
|
x509.UnrecognizedExtension(
|
||||||
|
oid=LIBP2P_TLS_EXTENSION_OID, value=extension_data
|
||||||
|
),
|
||||||
|
critical=True, # This extension is critical for libp2p
|
||||||
|
)
|
||||||
|
.sign(cert_private_key, hashes.SHA256())
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Generated libp2p TLS certificate for peer {peer_id}")
|
||||||
|
|
||||||
|
return TLSConfig(
|
||||||
|
certificate=certificate, private_key=cert_private_key, peer_id=peer_id
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise QUICCertificateError(f"Failed to generate certificate: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
|
class PeerAuthenticator:
|
||||||
|
"""
|
||||||
|
Authenticates remote peers using libp2p TLS certificates.
|
||||||
|
Validates both TLS certificate integrity and libp2p peer identity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.extension_handler = LibP2PExtensionHandler()
|
||||||
|
|
||||||
|
def verify_peer_certificate(
|
||||||
|
self, certificate: x509.Certificate, expected_peer_id: Optional[ID] = None
|
||||||
|
) -> ID:
|
||||||
|
"""
|
||||||
|
Verify a peer's TLS certificate and extract/validate peer identity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
certificate: The peer's TLS certificate
|
||||||
|
expected_peer_id: Expected peer ID (for outbound connections)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The verified peer ID
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
QUICPeerVerificationError: If verification fails
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Extract libp2p extension
|
||||||
|
libp2p_extension = None
|
||||||
|
for extension in certificate.extensions:
|
||||||
|
if extension.oid == LIBP2P_TLS_EXTENSION_OID:
|
||||||
|
libp2p_extension = extension
|
||||||
|
break
|
||||||
|
|
||||||
|
if not libp2p_extension:
|
||||||
|
raise QUICPeerVerificationError("Certificate missing libp2p extension")
|
||||||
|
|
||||||
|
# Parse the extension to get public key and signature
|
||||||
|
public_key, signature = self.extension_handler.parse_signed_key_extension(
|
||||||
|
libp2p_extension.value
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get certificate public key for signature verification
|
||||||
|
cert_public_key_bytes = certificate.public_key().public_bytes(
|
||||||
|
encoding=serialization.Encoding.DER,
|
||||||
|
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the signature proves ownership of the libp2p private key
|
||||||
|
signature_payload = b"libp2p-tls-handshake:" + cert_public_key_bytes
|
||||||
|
|
||||||
|
try:
|
||||||
|
public_key.verify(signature, signature_payload)
|
||||||
|
except Exception as e:
|
||||||
|
raise QUICPeerVerificationError(
|
||||||
|
f"Invalid signature in libp2p extension: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Derive peer ID from public key
|
||||||
|
derived_peer_id = ID.from_pubkey(public_key)
|
||||||
|
|
||||||
|
# Verify against expected peer ID if provided
|
||||||
|
if expected_peer_id and derived_peer_id != expected_peer_id:
|
||||||
|
raise QUICPeerVerificationError(
|
||||||
|
f"Peer ID mismatch: expected {expected_peer_id}, got {derived_peer_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Successfully verified peer certificate for {derived_peer_id}")
|
||||||
|
return derived_peer_id
|
||||||
|
|
||||||
|
except QUICPeerVerificationError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise QUICPeerVerificationError(
|
||||||
|
f"Certificate verification failed: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
class QUICTLSConfigManager:
|
||||||
|
"""
|
||||||
|
Manages TLS configuration for QUIC transport with libp2p security.
|
||||||
|
Integrates with aioquic's TLS configuration system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, libp2p_private_key: PrivateKey, peer_id: ID):
|
||||||
|
self.libp2p_private_key = libp2p_private_key
|
||||||
|
self.peer_id = peer_id
|
||||||
|
self.certificate_generator = CertificateGenerator()
|
||||||
|
self.peer_authenticator = PeerAuthenticator()
|
||||||
|
|
||||||
|
# Generate certificate for this peer
|
||||||
|
self.tls_config = self.certificate_generator.generate_certificate(
|
||||||
|
libp2p_private_key, peer_id
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_server_config(self) -> dict:
|
||||||
|
"""
|
||||||
|
Create aioquic server configuration with libp2p TLS settings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configuration dictionary for aioquic QuicConfiguration
|
||||||
|
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"certificate": self.tls_config.get_certificate_der(),
|
||||||
|
"private_key": self.tls_config.get_private_key_der(),
|
||||||
|
"alpn_protocols": ["libp2p"], # Required ALPN protocol
|
||||||
|
"verify_mode": True, # Require client certificates
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_client_config(self) -> dict:
|
||||||
|
"""
|
||||||
|
Create aioquic client configuration with libp2p TLS settings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configuration dictionary for aioquic QuicConfiguration
|
||||||
|
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"certificate": self.tls_config.get_certificate_der(),
|
||||||
|
"private_key": self.tls_config.get_private_key_der(),
|
||||||
|
"alpn_protocols": ["libp2p"], # Required ALPN protocol
|
||||||
|
"verify_mode": True, # Verify server certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
def verify_peer_identity(
|
||||||
|
self, peer_certificate: x509.Certificate, expected_peer_id: Optional[ID] = None
|
||||||
|
) -> ID:
|
||||||
|
"""
|
||||||
|
Verify remote peer's identity from their TLS certificate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
peer_certificate: Remote peer's TLS certificate
|
||||||
|
expected_peer_id: Expected peer ID (for outbound connections)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Verified peer ID
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.peer_authenticator.verify_peer_certificate(
|
||||||
|
peer_certificate, expected_peer_id
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_local_peer_id(self) -> ID:
|
||||||
|
"""Get the local peer ID."""
|
||||||
|
return self.peer_id
|
||||||
|
|
||||||
|
|
||||||
|
# Factory function for creating QUIC security transport
|
||||||
|
def create_quic_security_transport(
|
||||||
|
libp2p_private_key: PrivateKey, peer_id: ID
|
||||||
|
) -> QUICTLSConfigManager:
|
||||||
|
"""
|
||||||
|
Factory function to create QUIC security transport.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
libp2p_private_key: The libp2p identity private key
|
||||||
|
peer_id: The libp2p peer ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured QUIC TLS manager
|
||||||
|
|
||||||
|
"""
|
||||||
|
return QUICTLSConfigManager(libp2p_private_key, peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
# Legacy compatibility functions for existing code
|
||||||
def generate_libp2p_tls_config(private_key: PrivateKey, peer_id: ID) -> TLSConfig:
|
def generate_libp2p_tls_config(private_key: PrivateKey, peer_id: ID) -> TLSConfig:
|
||||||
"""
|
"""
|
||||||
Generate TLS configuration with libp2p peer identity.
|
Legacy function for compatibility with existing transport code.
|
||||||
|
|
||||||
This is a basic implementation for Module 1.
|
|
||||||
Full implementation with proper libp2p TLS spec compliance
|
|
||||||
will be provided in Module 5.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
private_key: libp2p private key
|
private_key: libp2p private key
|
||||||
@ -38,85 +480,17 @@ def generate_libp2p_tls_config(private_key: PrivateKey, peer_id: ID) -> TLSConfi
|
|||||||
Returns:
|
Returns:
|
||||||
TLS configuration
|
TLS configuration
|
||||||
|
|
||||||
Raises:
|
|
||||||
QUICSecurityError: If TLS configuration generation fails
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
generator = CertificateGenerator()
|
||||||
# TODO: Implement proper libp2p TLS certificate generation
|
return generator.generate_certificate(private_key, peer_id)
|
||||||
# This should follow the libp2p TLS specification:
|
|
||||||
# https://github.com/libp2p/specs/blob/master/tls/tls.md
|
|
||||||
|
|
||||||
# For now, create a basic self-signed certificate
|
|
||||||
# This is a placeholder implementation
|
|
||||||
|
|
||||||
# Create temporary files for cert and key
|
|
||||||
with tempfile.NamedTemporaryFile(
|
|
||||||
mode="w", suffix=".pem", delete=False
|
|
||||||
) as cert_file:
|
|
||||||
cert_path = cert_file.name
|
|
||||||
# Write placeholder certificate
|
|
||||||
cert_file.write(_generate_placeholder_cert(peer_id))
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(
|
|
||||||
mode="w", suffix=".key", delete=False
|
|
||||||
) as key_file:
|
|
||||||
key_path = key_file.name
|
|
||||||
# Write placeholder private key
|
|
||||||
key_file.write(_generate_placeholder_key(private_key))
|
|
||||||
|
|
||||||
return TLSConfig(cert_file=cert_path, key_file=key_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise QUICSecurityError(f"Failed to generate TLS config: {e}") from e
|
|
||||||
|
|
||||||
|
|
||||||
def _generate_placeholder_cert(peer_id: ID) -> str:
|
|
||||||
"""
|
|
||||||
Generate a placeholder certificate.
|
|
||||||
|
|
||||||
This is a temporary implementation for Module 1.
|
|
||||||
Real implementation will embed the peer ID in the certificate
|
|
||||||
following the libp2p TLS specification.
|
|
||||||
"""
|
|
||||||
# This is a placeholder - real implementation needed
|
|
||||||
return f"""-----BEGIN CERTIFICATE-----
|
|
||||||
# Placeholder certificate for peer {peer_id}
|
|
||||||
# TODO: Implement proper libp2p TLS certificate generation
|
|
||||||
# This should embed the peer ID in a certificate extension
|
|
||||||
# according to the libp2p TLS specification
|
|
||||||
-----END CERTIFICATE-----"""
|
|
||||||
|
|
||||||
|
|
||||||
def _generate_placeholder_key(private_key: PrivateKey) -> str:
|
|
||||||
"""
|
|
||||||
Generate a placeholder private key.
|
|
||||||
|
|
||||||
This is a temporary implementation for Module 1.
|
|
||||||
Real implementation will use the actual libp2p private key.
|
|
||||||
"""
|
|
||||||
# This is a placeholder - real implementation needed
|
|
||||||
return """-----BEGIN PRIVATE KEY-----
|
|
||||||
# Placeholder private key
|
|
||||||
# TODO: Convert libp2p private key to TLS-compatible format
|
|
||||||
-----END PRIVATE KEY-----"""
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_tls_config(config: TLSConfig) -> None:
|
def cleanup_tls_config(config: TLSConfig) -> None:
|
||||||
"""
|
"""
|
||||||
Clean up temporary TLS files.
|
Clean up TLS configuration.
|
||||||
|
|
||||||
Args:
|
|
||||||
config: TLS configuration to clean up
|
|
||||||
|
|
||||||
|
For the new implementation, this is mostly a no-op since we don't use
|
||||||
|
temporary files, but kept for compatibility.
|
||||||
"""
|
"""
|
||||||
try:
|
# New implementation doesn't use temporary files
|
||||||
if os.path.exists(config.cert_file):
|
logger.debug("TLS config cleanup completed")
|
||||||
os.unlink(config.cert_file)
|
|
||||||
if os.path.exists(config.key_file):
|
|
||||||
os.unlink(config.key_file)
|
|
||||||
if config.ca_file and os.path.exists(config.ca_file):
|
|
||||||
os.unlink(config.ca_file)
|
|
||||||
except Exception:
|
|
||||||
# Ignore cleanup errors
|
|
||||||
pass
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
QUIC Transport implementation for py-libp2p.
|
QUIC Transport implementation for py-libp2p with integrated security.
|
||||||
Uses aioquic's sans-IO core with trio for native async support.
|
Uses aioquic's sans-IO core with trio for native async support.
|
||||||
Based on aioquic library with interface consistency to go-libp2p and js-libp2p.
|
Based on aioquic library with interface consistency to go-libp2p and js-libp2p.
|
||||||
|
Updated to include Module 5 security integration.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
@ -33,6 +34,8 @@ from libp2p.transport.quic.utils import (
|
|||||||
is_quic_multiaddr,
|
is_quic_multiaddr,
|
||||||
multiaddr_to_quic_version,
|
multiaddr_to_quic_version,
|
||||||
quic_multiaddr_to_endpoint,
|
quic_multiaddr_to_endpoint,
|
||||||
|
quic_version_to_wire_format,
|
||||||
|
get_alpn_protocols,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .config import (
|
from .config import (
|
||||||
@ -44,10 +47,15 @@ from .connection import (
|
|||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
QUICDialError,
|
QUICDialError,
|
||||||
QUICListenError,
|
QUICListenError,
|
||||||
|
QUICSecurityError,
|
||||||
)
|
)
|
||||||
from .listener import (
|
from .listener import (
|
||||||
QUICListener,
|
QUICListener,
|
||||||
)
|
)
|
||||||
|
from .security import (
|
||||||
|
QUICTLSConfigManager,
|
||||||
|
create_quic_security_transport,
|
||||||
|
)
|
||||||
|
|
||||||
QUIC_V1_PROTOCOL = QUICTransportConfig.PROTOCOL_QUIC_V1
|
QUIC_V1_PROTOCOL = QUICTransportConfig.PROTOCOL_QUIC_V1
|
||||||
QUIC_DRAFT29_PROTOCOL = QUICTransportConfig.PROTOCOL_QUIC_DRAFT29
|
QUIC_DRAFT29_PROTOCOL = QUICTransportConfig.PROTOCOL_QUIC_DRAFT29
|
||||||
@ -62,13 +70,15 @@ class QUICTransport(ITransport):
|
|||||||
Uses aioquic's sans-IO core with trio for native async support.
|
Uses aioquic's sans-IO core with trio for native async support.
|
||||||
Supports both QUIC v1 (RFC 9000) and draft-29 for compatibility with
|
Supports both QUIC v1 (RFC 9000) and draft-29 for compatibility with
|
||||||
go-libp2p and js-libp2p implementations.
|
go-libp2p and js-libp2p implementations.
|
||||||
|
|
||||||
|
Includes integrated libp2p TLS security with peer identity verification.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, private_key: PrivateKey, config: QUICTransportConfig | None = None
|
self, private_key: PrivateKey, config: QUICTransportConfig | None = None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize QUIC transport.
|
Initialize QUIC transport with security integration.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
private_key: libp2p private key for identity and TLS cert generation
|
private_key: libp2p private key for identity and TLS cert generation
|
||||||
@ -83,6 +93,11 @@ class QUICTransport(ITransport):
|
|||||||
self._connections: dict[str, QUICConnection] = {}
|
self._connections: dict[str, QUICConnection] = {}
|
||||||
self._listeners: list[QUICListener] = []
|
self._listeners: list[QUICListener] = []
|
||||||
|
|
||||||
|
# Security manager for TLS integration
|
||||||
|
self._security_manager = create_quic_security_transport(
|
||||||
|
self._private_key, self._peer_id
|
||||||
|
)
|
||||||
|
|
||||||
# QUIC configurations for different versions
|
# QUIC configurations for different versions
|
||||||
self._quic_configs: dict[TProtocol, QuicConfiguration] = {}
|
self._quic_configs: dict[TProtocol, QuicConfiguration] = {}
|
||||||
self._setup_quic_configurations()
|
self._setup_quic_configurations()
|
||||||
@ -91,59 +106,121 @@ class QUICTransport(ITransport):
|
|||||||
self._closed = False
|
self._closed = False
|
||||||
self._nursery_manager = trio.CapacityLimiter(1)
|
self._nursery_manager = trio.CapacityLimiter(1)
|
||||||
|
|
||||||
logger.info(f"Initialized QUIC transport for peer {self._peer_id}")
|
logger.info(
|
||||||
|
f"Initialized QUIC transport with security for peer {self._peer_id}"
|
||||||
def _setup_quic_configurations(self) -> None:
|
|
||||||
"""Setup QUIC configurations for supported protocol versions."""
|
|
||||||
# Base configuration
|
|
||||||
base_config = QuicConfiguration(
|
|
||||||
is_client=False,
|
|
||||||
alpn_protocols=["libp2p"],
|
|
||||||
verify_mode=self._config.verify_mode,
|
|
||||||
max_datagram_frame_size=self._config.max_datagram_size,
|
|
||||||
idle_timeout=self._config.idle_timeout,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add TLS certificate generated from libp2p private key
|
def _setup_quic_configurations(self) -> None:
|
||||||
# self._setup_tls_configuration(base_config)
|
"""Setup QUIC configurations for supported protocol versions with TLS security."""
|
||||||
|
try:
|
||||||
|
# Get TLS configuration from security manager
|
||||||
|
server_tls_config = self._security_manager.create_server_config()
|
||||||
|
client_tls_config = self._security_manager.create_client_config()
|
||||||
|
|
||||||
# QUIC v1 (RFC 9000) configuration
|
# Base server configuration
|
||||||
quic_v1_config = copy.deepcopy(base_config)
|
base_server_config = QuicConfiguration(
|
||||||
quic_v1_config.supported_versions = [0x00000001] # QUIC v1
|
is_client=False,
|
||||||
self._quic_configs[QUIC_V1_PROTOCOL] = quic_v1_config
|
alpn_protocols=get_alpn_protocols(),
|
||||||
|
verify_mode=self._config.verify_mode,
|
||||||
|
max_datagram_frame_size=self._config.max_datagram_size,
|
||||||
|
idle_timeout=self._config.idle_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
# QUIC draft-29 configuration for compatibility
|
# Base client configuration
|
||||||
if self._config.enable_draft29:
|
base_client_config = QuicConfiguration(
|
||||||
draft29_config = copy.deepcopy(base_config)
|
is_client=True,
|
||||||
draft29_config.supported_versions = [0xFF00001D] # draft-29
|
alpn_protocols=get_alpn_protocols(),
|
||||||
self._quic_configs[QUIC_DRAFT29_PROTOCOL] = draft29_config
|
verify_mode=self._config.verify_mode,
|
||||||
|
max_datagram_frame_size=self._config.max_datagram_size,
|
||||||
|
idle_timeout=self._config.idle_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: SETUP TLS LISTENER
|
# Apply TLS configuration
|
||||||
# def _setup_tls_configuration(self, config: QuicConfiguration) -> None:
|
self._apply_tls_configuration(base_server_config, server_tls_config)
|
||||||
# """
|
self._apply_tls_configuration(base_client_config, client_tls_config)
|
||||||
# Setup TLS configuration with libp2p identity integration.
|
|
||||||
# Similar to go-libp2p's certificate generation approach.
|
|
||||||
# """
|
|
||||||
# from .security import (
|
|
||||||
# generate_libp2p_tls_config,
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # Generate TLS certificate with embedded libp2p peer ID
|
# QUIC v1 (RFC 9000) configurations
|
||||||
# # This follows the libp2p TLS spec for peer identity verification
|
quic_v1_server_config = copy.deepcopy(base_server_config)
|
||||||
# tls_config = generate_libp2p_tls_config(self._private_key, self._peer_id)
|
quic_v1_server_config.supported_versions = [
|
||||||
|
quic_version_to_wire_format(QUIC_V1_PROTOCOL)
|
||||||
|
]
|
||||||
|
|
||||||
# config.load_cert_chain(
|
quic_v1_client_config = copy.deepcopy(base_client_config)
|
||||||
# certfile=tls_config.cert_file,
|
quic_v1_client_config.supported_versions = [
|
||||||
# keyfile=tls_config.key_file
|
quic_version_to_wire_format(QUIC_V1_PROTOCOL)
|
||||||
# )
|
]
|
||||||
# if tls_config.ca_file:
|
|
||||||
# config.load_verify_locations(tls_config.ca_file)
|
# Store both server and client configs for v1
|
||||||
|
self._quic_configs[TProtocol(f"{QUIC_V1_PROTOCOL}_server")] = (
|
||||||
|
quic_v1_server_config
|
||||||
|
)
|
||||||
|
self._quic_configs[TProtocol(f"{QUIC_V1_PROTOCOL}_client")] = (
|
||||||
|
quic_v1_client_config
|
||||||
|
)
|
||||||
|
|
||||||
|
# QUIC draft-29 configurations for compatibility
|
||||||
|
if self._config.enable_draft29:
|
||||||
|
draft29_server_config = copy.deepcopy(base_server_config)
|
||||||
|
draft29_server_config.supported_versions = [
|
||||||
|
quic_version_to_wire_format(QUIC_DRAFT29_PROTOCOL)
|
||||||
|
]
|
||||||
|
|
||||||
|
draft29_client_config = copy.deepcopy(base_client_config)
|
||||||
|
draft29_client_config.supported_versions = [
|
||||||
|
quic_version_to_wire_format(QUIC_DRAFT29_PROTOCOL)
|
||||||
|
]
|
||||||
|
|
||||||
|
self._quic_configs[TProtocol(f"{QUIC_DRAFT29_PROTOCOL}_server")] = (
|
||||||
|
draft29_server_config
|
||||||
|
)
|
||||||
|
self._quic_configs[TProtocol(f"{QUIC_DRAFT29_PROTOCOL}_client")] = (
|
||||||
|
draft29_client_config
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("QUIC configurations initialized with libp2p TLS security")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise QUICSecurityError(
|
||||||
|
f"Failed to setup QUIC TLS configurations: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
def _apply_tls_configuration(
|
||||||
|
self, config: QuicConfiguration, tls_config: dict
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Apply TLS configuration to QuicConfiguration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: QuicConfiguration to update
|
||||||
|
tls_config: TLS configuration dictionary from security manager
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Set certificate and private key
|
||||||
|
if "certificate" in tls_config and "private_key" in tls_config:
|
||||||
|
# aioquic expects certificate and private key in specific formats
|
||||||
|
# This is a simplified approach - full implementation would handle
|
||||||
|
# proper certificate chain setup
|
||||||
|
config.load_cert_chain_from_der(
|
||||||
|
tls_config["certificate"], tls_config["private_key"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set ALPN protocols
|
||||||
|
if "alpn_protocols" in tls_config:
|
||||||
|
config.alpn_protocols = tls_config["alpn_protocols"]
|
||||||
|
|
||||||
|
# Set certificate verification
|
||||||
|
if "verify_mode" in tls_config:
|
||||||
|
config.verify_mode = tls_config["verify_mode"]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise QUICSecurityError(f"Failed to apply TLS configuration: {e}") from e
|
||||||
|
|
||||||
async def dial(
|
async def dial(
|
||||||
self, maddr: multiaddr.Multiaddr, peer_id: ID | None = None
|
self, maddr: multiaddr.Multiaddr, peer_id: ID | None = None
|
||||||
) -> IRawConnection:
|
) -> IRawConnection:
|
||||||
"""
|
"""
|
||||||
Dial a remote peer using QUIC transport.
|
Dial a remote peer using QUIC transport with security verification.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
maddr: Multiaddr of the remote peer (e.g., /ip4/1.2.3.4/udp/4001/quic-v1)
|
maddr: Multiaddr of the remote peer (e.g., /ip4/1.2.3.4/udp/4001/quic-v1)
|
||||||
@ -154,6 +231,7 @@ class QUICTransport(ITransport):
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
QUICDialError: If dialing fails
|
QUICDialError: If dialing fails
|
||||||
|
QUICSecurityError: If security verification fails
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self._closed:
|
if self._closed:
|
||||||
@ -167,23 +245,20 @@ class QUICTransport(ITransport):
|
|||||||
host, port = quic_multiaddr_to_endpoint(maddr)
|
host, port = quic_multiaddr_to_endpoint(maddr)
|
||||||
quic_version = multiaddr_to_quic_version(maddr)
|
quic_version = multiaddr_to_quic_version(maddr)
|
||||||
|
|
||||||
# Get appropriate QUIC configuration
|
# Get appropriate QUIC client configuration
|
||||||
config = self._quic_configs.get(quic_version)
|
config_key = TProtocol(f"{quic_version}_client")
|
||||||
|
config = self._quic_configs.get(config_key)
|
||||||
if not config:
|
if not config:
|
||||||
raise QUICDialError(f"Unsupported QUIC version: {quic_version}")
|
raise QUICDialError(f"Unsupported QUIC version: {quic_version}")
|
||||||
|
|
||||||
# Create client configuration
|
|
||||||
client_config = copy.deepcopy(config)
|
|
||||||
client_config.is_client = True
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Dialing QUIC connection to {host}:{port} (version: {quic_version})"
|
f"Dialing QUIC connection to {host}:{port} (version: {quic_version})"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create QUIC connection using aioquic's sans-IO core
|
# Create QUIC connection using aioquic's sans-IO core
|
||||||
quic_connection = QuicConnection(configuration=client_config)
|
quic_connection = QuicConnection(configuration=config)
|
||||||
|
|
||||||
# Create trio-based QUIC connection wrapper
|
# Create trio-based QUIC connection wrapper with security
|
||||||
connection = QUICConnection(
|
connection = QUICConnection(
|
||||||
quic_connection=quic_connection,
|
quic_connection=quic_connection,
|
||||||
remote_addr=(host, port),
|
remote_addr=(host, port),
|
||||||
@ -192,31 +267,66 @@ class QUICTransport(ITransport):
|
|||||||
is_initiator=True,
|
is_initiator=True,
|
||||||
maddr=maddr,
|
maddr=maddr,
|
||||||
transport=self,
|
transport=self,
|
||||||
|
security_manager=self._security_manager, # Pass security manager
|
||||||
)
|
)
|
||||||
|
|
||||||
# Establish connection using trio
|
# Establish connection using trio
|
||||||
# We need a nursery for this - in real usage, this would be provided
|
|
||||||
# by the caller or we'd use a transport-level nursery
|
|
||||||
async with trio.open_nursery() as nursery:
|
async with trio.open_nursery() as nursery:
|
||||||
await connection.connect(nursery)
|
await connection.connect(nursery)
|
||||||
|
|
||||||
|
# Verify peer identity after TLS handshake
|
||||||
|
if peer_id:
|
||||||
|
await self._verify_peer_identity(connection, peer_id)
|
||||||
|
|
||||||
# Store connection for management
|
# Store connection for management
|
||||||
conn_id = f"{host}:{port}:{peer_id}"
|
conn_id = f"{host}:{port}:{peer_id}"
|
||||||
self._connections[conn_id] = connection
|
self._connections[conn_id] = connection
|
||||||
|
|
||||||
# Perform libp2p handshake verification
|
logger.info(f"Successfully dialed secure QUIC connection to {peer_id}")
|
||||||
# await connection.verify_peer_identity()
|
|
||||||
|
|
||||||
logger.info(f"Successfully dialed QUIC connection to {peer_id}")
|
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to dial QUIC connection to {maddr}: {e}")
|
logger.error(f"Failed to dial QUIC connection to {maddr}: {e}")
|
||||||
raise QUICDialError(f"Dial failed: {e}") from e
|
raise QUICDialError(f"Dial failed: {e}") from e
|
||||||
|
|
||||||
|
async def _verify_peer_identity(
|
||||||
|
self, connection: QUICConnection, expected_peer_id: ID
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Verify remote peer identity after TLS handshake.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
connection: The established QUIC connection
|
||||||
|
expected_peer_id: Expected peer ID
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
QUICSecurityError: If peer verification fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get peer certificate from the connection
|
||||||
|
peer_certificate = await connection.get_peer_certificate()
|
||||||
|
|
||||||
|
if not peer_certificate:
|
||||||
|
raise QUICSecurityError("No peer certificate available")
|
||||||
|
|
||||||
|
# Verify peer identity using security manager
|
||||||
|
verified_peer_id = self._security_manager.verify_peer_identity(
|
||||||
|
peer_certificate, expected_peer_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if verified_peer_id != expected_peer_id:
|
||||||
|
raise QUICSecurityError(
|
||||||
|
f"Peer ID verification failed: expected {expected_peer_id}, got {verified_peer_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Peer identity verified: {verified_peer_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise QUICSecurityError(f"Peer identity verification failed: {e}") from e
|
||||||
|
|
||||||
def create_listener(self, handler_function: THandler) -> QUICListener:
|
def create_listener(self, handler_function: THandler) -> QUICListener:
|
||||||
"""
|
"""
|
||||||
Create a QUIC listener.
|
Create a QUIC listener with integrated security.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
handler_function: Function to handle new connections
|
handler_function: Function to handle new connections
|
||||||
@ -231,15 +341,23 @@ class QUICTransport(ITransport):
|
|||||||
if self._closed:
|
if self._closed:
|
||||||
raise QUICListenError("Transport is closed")
|
raise QUICListenError("Transport is closed")
|
||||||
|
|
||||||
|
# Get server configurations for the listener
|
||||||
|
server_configs = {
|
||||||
|
version: config
|
||||||
|
for version, config in self._quic_configs.items()
|
||||||
|
if version.endswith("_server")
|
||||||
|
}
|
||||||
|
|
||||||
listener = QUICListener(
|
listener = QUICListener(
|
||||||
transport=self,
|
transport=self,
|
||||||
handler_function=handler_function,
|
handler_function=handler_function,
|
||||||
quic_configs=self._quic_configs,
|
quic_configs=server_configs,
|
||||||
config=self._config,
|
config=self._config,
|
||||||
|
security_manager=self._security_manager, # Pass security manager
|
||||||
)
|
)
|
||||||
|
|
||||||
self._listeners.append(listener)
|
self._listeners.append(listener)
|
||||||
logger.debug("Created QUIC listener")
|
logger.debug("Created QUIC listener with security")
|
||||||
return listener
|
return listener
|
||||||
|
|
||||||
def can_dial(self, maddr: multiaddr.Multiaddr) -> bool:
|
def can_dial(self, maddr: multiaddr.Multiaddr) -> bool:
|
||||||
@ -303,59 +421,21 @@ class QUICTransport(ITransport):
|
|||||||
logger.info("QUIC transport closed")
|
logger.info("QUIC transport closed")
|
||||||
|
|
||||||
def get_stats(self) -> dict[str, int | list[str] | object]:
|
def get_stats(self) -> dict[str, int | list[str] | object]:
|
||||||
"""Get transport statistics."""
|
"""Get transport statistics including security info."""
|
||||||
protocols = self.protocols()
|
return {
|
||||||
str_protocols = []
|
|
||||||
|
|
||||||
for proto in protocols:
|
|
||||||
str_protocols.append(str(proto))
|
|
||||||
|
|
||||||
stats: dict[str, int | list[str] | object] = {
|
|
||||||
"active_connections": len(self._connections),
|
"active_connections": len(self._connections),
|
||||||
"active_listeners": len(self._listeners),
|
"active_listeners": len(self._listeners),
|
||||||
"supported_protocols": str_protocols,
|
"supported_protocols": self.protocols(),
|
||||||
|
"local_peer_id": str(self._peer_id),
|
||||||
|
"security_enabled": True,
|
||||||
|
"tls_configured": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Aggregate listener stats
|
def get_security_manager(self) -> QUICTLSConfigManager:
|
||||||
listener_stats = {}
|
"""
|
||||||
for i, listener in enumerate(self._listeners):
|
Get the security manager for this transport.
|
||||||
listener_stats[f"listener_{i}"] = listener.get_stats()
|
|
||||||
|
|
||||||
if listener_stats:
|
Returns:
|
||||||
# TODO: Fix type of listener_stats
|
The QUIC TLS configuration manager
|
||||||
# type: ignore
|
"""
|
||||||
stats["listeners"] = listener_stats
|
return self._security_manager
|
||||||
|
|
||||||
return stats
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""String representation of the transport."""
|
|
||||||
return f"QUICTransport(peer_id={self._peer_id}, protocols={self.protocols()})"
|
|
||||||
|
|
||||||
|
|
||||||
def new_transport(
|
|
||||||
private_key: PrivateKey,
|
|
||||||
config: QUICTransportConfig | None = None,
|
|
||||||
**kwargs: Unpack[QUICTransportKwargs],
|
|
||||||
) -> QUICTransport:
|
|
||||||
"""
|
|
||||||
Factory function to create a new QUIC transport.
|
|
||||||
Follows the naming convention from go-libp2p (NewTransport).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
private_key: libp2p private key
|
|
||||||
config: Transport configuration
|
|
||||||
**kwargs: Additional configuration options
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
New QUIC transport instance
|
|
||||||
|
|
||||||
"""
|
|
||||||
if config is None:
|
|
||||||
config = QUICTransportConfig(**kwargs)
|
|
||||||
|
|
||||||
return QUICTransport(private_key, config)
|
|
||||||
|
|
||||||
|
|
||||||
# Type aliases for consistency with go-libp2p
|
|
||||||
NewTransport = new_transport # go-libp2p style naming
|
|
||||||
|
|||||||
@ -1,20 +1,34 @@
|
|||||||
"""
|
"""
|
||||||
Multiaddr utilities for QUIC transport.
|
Multiaddr utilities for QUIC transport - Module 4.
|
||||||
Handles QUIC-specific multiaddr parsing and validation.
|
Essential utilities required for QUIC transport implementation.
|
||||||
|
Based on go-libp2p and js-libp2p QUIC implementations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
import multiaddr
|
import multiaddr
|
||||||
|
|
||||||
from libp2p.custom_types import TProtocol
|
from libp2p.custom_types import TProtocol
|
||||||
|
|
||||||
from .config import QUICTransportConfig
|
from .config import QUICTransportConfig
|
||||||
|
from .exceptions import QUICInvalidMultiaddrError, QUICUnsupportedVersionError
|
||||||
|
|
||||||
|
# Protocol constants
|
||||||
QUIC_V1_PROTOCOL = QUICTransportConfig.PROTOCOL_QUIC_V1
|
QUIC_V1_PROTOCOL = QUICTransportConfig.PROTOCOL_QUIC_V1
|
||||||
QUIC_DRAFT29_PROTOCOL = QUICTransportConfig.PROTOCOL_QUIC_DRAFT29
|
QUIC_DRAFT29_PROTOCOL = QUICTransportConfig.PROTOCOL_QUIC_DRAFT29
|
||||||
UDP_PROTOCOL = "udp"
|
UDP_PROTOCOL = "udp"
|
||||||
IP4_PROTOCOL = "ip4"
|
IP4_PROTOCOL = "ip4"
|
||||||
IP6_PROTOCOL = "ip6"
|
IP6_PROTOCOL = "ip6"
|
||||||
|
|
||||||
|
# QUIC version to wire format mappings (required for aioquic)
|
||||||
|
QUIC_VERSION_MAPPINGS = {
|
||||||
|
QUIC_V1_PROTOCOL: 0x00000001, # RFC 9000
|
||||||
|
QUIC_DRAFT29_PROTOCOL: 0xFF00001D, # draft-29
|
||||||
|
}
|
||||||
|
|
||||||
|
# ALPN protocols for libp2p over QUIC
|
||||||
|
LIBP2P_ALPN_PROTOCOLS = ["libp2p"]
|
||||||
|
|
||||||
|
|
||||||
def is_quic_multiaddr(maddr: multiaddr.Multiaddr) -> bool:
|
def is_quic_multiaddr(maddr: multiaddr.Multiaddr) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -34,7 +48,6 @@ def is_quic_multiaddr(maddr: multiaddr.Multiaddr) -> bool:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Get protocol names from the multiaddr string
|
|
||||||
addr_str = str(maddr)
|
addr_str = str(maddr)
|
||||||
|
|
||||||
# Check for required components
|
# Check for required components
|
||||||
@ -63,14 +76,13 @@ def quic_multiaddr_to_endpoint(maddr: multiaddr.Multiaddr) -> tuple[str, int]:
|
|||||||
Tuple of (host, port)
|
Tuple of (host, port)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If multiaddr is not a valid QUIC address
|
QUICInvalidMultiaddrError: If multiaddr is not a valid QUIC address
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not is_quic_multiaddr(maddr):
|
if not is_quic_multiaddr(maddr):
|
||||||
raise ValueError(f"Not a valid QUIC multiaddr: {maddr}")
|
raise QUICInvalidMultiaddrError(f"Not a valid QUIC multiaddr: {maddr}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Use multiaddr's value_for_protocol method to extract values
|
|
||||||
host = None
|
host = None
|
||||||
port = None
|
port = None
|
||||||
|
|
||||||
@ -89,19 +101,20 @@ def quic_multiaddr_to_endpoint(maddr: multiaddr.Multiaddr) -> tuple[str, int]:
|
|||||||
|
|
||||||
# Get UDP port
|
# Get UDP port
|
||||||
try:
|
try:
|
||||||
# The the package is exposed by types not availble
|
|
||||||
port_str = maddr.value_for_protocol(multiaddr.protocols.P_UDP) # type: ignore
|
port_str = maddr.value_for_protocol(multiaddr.protocols.P_UDP) # type: ignore
|
||||||
port = int(port_str)
|
port = int(port_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if host is None or port is None:
|
if host is None or port is None:
|
||||||
raise ValueError(f"Could not extract host/port from {maddr}")
|
raise QUICInvalidMultiaddrError(f"Could not extract host/port from {maddr}")
|
||||||
|
|
||||||
return host, port
|
return host, port
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Failed to parse QUIC multiaddr {maddr}: {e}") from e
|
raise QUICInvalidMultiaddrError(
|
||||||
|
f"Failed to parse QUIC multiaddr {maddr}: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
def multiaddr_to_quic_version(maddr: multiaddr.Multiaddr) -> TProtocol:
|
def multiaddr_to_quic_version(maddr: multiaddr.Multiaddr) -> TProtocol:
|
||||||
@ -112,10 +125,10 @@ def multiaddr_to_quic_version(maddr: multiaddr.Multiaddr) -> TProtocol:
|
|||||||
maddr: QUIC multiaddr
|
maddr: QUIC multiaddr
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
QUIC version identifier ("/quic-v1" or "/quic")
|
QUIC version identifier ("quic-v1" or "quic")
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If multiaddr doesn't contain QUIC protocol
|
QUICInvalidMultiaddrError: If multiaddr doesn't contain QUIC protocol
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@ -126,14 +139,16 @@ def multiaddr_to_quic_version(maddr: multiaddr.Multiaddr) -> TProtocol:
|
|||||||
elif f"/{QUIC_DRAFT29_PROTOCOL}" in addr_str:
|
elif f"/{QUIC_DRAFT29_PROTOCOL}" in addr_str:
|
||||||
return QUIC_DRAFT29_PROTOCOL # draft-29
|
return QUIC_DRAFT29_PROTOCOL # draft-29
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"No QUIC protocol found in {maddr}")
|
raise QUICInvalidMultiaddrError(f"No QUIC protocol found in {maddr}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Failed to determine QUIC version from {maddr}: {e}") from e
|
raise QUICInvalidMultiaddrError(
|
||||||
|
f"Failed to determine QUIC version from {maddr}: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
def create_quic_multiaddr(
|
def create_quic_multiaddr(
|
||||||
host: str, port: int, version: str = "/quic-v1"
|
host: str, port: int, version: str = "quic-v1"
|
||||||
) -> multiaddr.Multiaddr:
|
) -> multiaddr.Multiaddr:
|
||||||
"""
|
"""
|
||||||
Create a QUIC multiaddr from host, port, and version.
|
Create a QUIC multiaddr from host, port, and version.
|
||||||
@ -141,18 +156,16 @@ def create_quic_multiaddr(
|
|||||||
Args:
|
Args:
|
||||||
host: IP address (IPv4 or IPv6)
|
host: IP address (IPv4 or IPv6)
|
||||||
port: UDP port number
|
port: UDP port number
|
||||||
version: QUIC version ("/quic-v1" or "/quic")
|
version: QUIC version ("quic-v1" or "quic")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
QUIC multiaddr
|
QUIC multiaddr
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If invalid parameters provided
|
QUICInvalidMultiaddrError: If invalid parameters provided
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
import ipaddress
|
|
||||||
|
|
||||||
# Determine IP version
|
# Determine IP version
|
||||||
try:
|
try:
|
||||||
ip = ipaddress.ip_address(host)
|
ip = ipaddress.ip_address(host)
|
||||||
@ -161,42 +174,58 @@ def create_quic_multiaddr(
|
|||||||
else:
|
else:
|
||||||
ip_proto = IP6_PROTOCOL
|
ip_proto = IP6_PROTOCOL
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(f"Invalid IP address: {host}")
|
raise QUICInvalidMultiaddrError(f"Invalid IP address: {host}")
|
||||||
|
|
||||||
# Validate port
|
# Validate port
|
||||||
if not (0 <= port <= 65535):
|
if not (0 <= port <= 65535):
|
||||||
raise ValueError(f"Invalid port: {port}")
|
raise QUICInvalidMultiaddrError(f"Invalid port: {port}")
|
||||||
|
|
||||||
# Validate QUIC version
|
# Validate and normalize QUIC version
|
||||||
if version not in ["/quic-v1", "/quic"]:
|
if version == "quic-v1" or version == "/quic-v1":
|
||||||
raise ValueError(f"Invalid QUIC version: {version}")
|
quic_proto = QUIC_V1_PROTOCOL
|
||||||
|
elif version == "quic" or version == "/quic":
|
||||||
|
quic_proto = QUIC_DRAFT29_PROTOCOL
|
||||||
|
else:
|
||||||
|
raise QUICInvalidMultiaddrError(f"Invalid QUIC version: {version}")
|
||||||
|
|
||||||
# Construct multiaddr
|
# Construct multiaddr
|
||||||
quic_proto = (
|
|
||||||
QUIC_V1_PROTOCOL if version == "/quic-v1" else QUIC_DRAFT29_PROTOCOL
|
|
||||||
)
|
|
||||||
addr_str = f"/{ip_proto}/{host}/{UDP_PROTOCOL}/{port}/{quic_proto}"
|
addr_str = f"/{ip_proto}/{host}/{UDP_PROTOCOL}/{port}/{quic_proto}"
|
||||||
|
|
||||||
return multiaddr.Multiaddr(addr_str)
|
return multiaddr.Multiaddr(addr_str)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Failed to create QUIC multiaddr: {e}") from e
|
raise QUICInvalidMultiaddrError(f"Failed to create QUIC multiaddr: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
def is_quic_v1_multiaddr(maddr: multiaddr.Multiaddr) -> bool:
|
def quic_version_to_wire_format(version: TProtocol) -> int:
|
||||||
"""Check if multiaddr uses QUIC v1 (RFC 9000)."""
|
"""
|
||||||
try:
|
Convert QUIC version string to wire format integer for aioquic.
|
||||||
return multiaddr_to_quic_version(maddr) == "/quic-v1"
|
|
||||||
except ValueError:
|
Args:
|
||||||
return False
|
version: QUIC version string ("quic-v1" or "quic")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Wire format version number
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
QUICUnsupportedVersionError: If version is not supported
|
||||||
|
|
||||||
|
"""
|
||||||
|
wire_version = QUIC_VERSION_MAPPINGS.get(version)
|
||||||
|
if wire_version is None:
|
||||||
|
raise QUICUnsupportedVersionError(f"Unsupported QUIC version: {version}")
|
||||||
|
|
||||||
|
return wire_version
|
||||||
|
|
||||||
|
|
||||||
def is_quic_draft29_multiaddr(maddr: multiaddr.Multiaddr) -> bool:
|
def get_alpn_protocols() -> list[str]:
|
||||||
"""Check if multiaddr uses QUIC draft-29."""
|
"""
|
||||||
try:
|
Get ALPN protocols for libp2p over QUIC.
|
||||||
return multiaddr_to_quic_version(maddr) == "/quic"
|
|
||||||
except ValueError:
|
Returns:
|
||||||
return False
|
List of ALPN protocol identifiers
|
||||||
|
|
||||||
|
"""
|
||||||
|
return LIBP2P_ALPN_PROTOCOLS.copy()
|
||||||
|
|
||||||
|
|
||||||
def normalize_quic_multiaddr(maddr: multiaddr.Multiaddr) -> multiaddr.Multiaddr:
|
def normalize_quic_multiaddr(maddr: multiaddr.Multiaddr) -> multiaddr.Multiaddr:
|
||||||
@ -210,11 +239,11 @@ def normalize_quic_multiaddr(maddr: multiaddr.Multiaddr) -> multiaddr.Multiaddr:
|
|||||||
Normalized multiaddr
|
Normalized multiaddr
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If not a valid QUIC multiaddr
|
QUICInvalidMultiaddrError: If not a valid QUIC multiaddr
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not is_quic_multiaddr(maddr):
|
if not is_quic_multiaddr(maddr):
|
||||||
raise ValueError(f"Not a QUIC multiaddr: {maddr}")
|
raise QUICInvalidMultiaddrError(f"Not a QUIC multiaddr: {maddr}")
|
||||||
|
|
||||||
host, port = quic_multiaddr_to_endpoint(maddr)
|
host, port = quic_multiaddr_to_endpoint(maddr)
|
||||||
version = multiaddr_to_quic_version(maddr)
|
version = multiaddr_to_quic_version(maddr)
|
||||||
|
|||||||
@ -1,90 +1,334 @@
|
|||||||
import pytest
|
"""
|
||||||
from multiaddr.multiaddr import Multiaddr
|
Test suite for QUIC multiaddr utilities.
|
||||||
|
Focused tests covering essential functionality required for QUIC transport.
|
||||||
|
"""
|
||||||
|
|
||||||
from libp2p.transport.quic.config import QUICTransportConfig
|
# TODO: Enable this test after multiaddr repo supports protocol quic-v1
|
||||||
from libp2p.transport.quic.utils import (
|
|
||||||
create_quic_multiaddr,
|
# import pytest
|
||||||
is_quic_multiaddr,
|
# from multiaddr import Multiaddr
|
||||||
multiaddr_to_quic_version,
|
|
||||||
quic_multiaddr_to_endpoint,
|
# from libp2p.custom_types import TProtocol
|
||||||
)
|
# from libp2p.transport.quic.exceptions import (
|
||||||
|
# QUICInvalidMultiaddrError,
|
||||||
|
# QUICUnsupportedVersionError,
|
||||||
|
# )
|
||||||
|
# from libp2p.transport.quic.utils import (
|
||||||
|
# create_quic_multiaddr,
|
||||||
|
# get_alpn_protocols,
|
||||||
|
# is_quic_multiaddr,
|
||||||
|
# multiaddr_to_quic_version,
|
||||||
|
# normalize_quic_multiaddr,
|
||||||
|
# quic_multiaddr_to_endpoint,
|
||||||
|
# quic_version_to_wire_format,
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
class TestQUICUtils:
|
# class TestIsQuicMultiaddr:
|
||||||
"""Test suite for QUIC utility functions."""
|
# """Test QUIC multiaddr detection."""
|
||||||
|
|
||||||
def test_is_quic_multiaddr(self):
|
# def test_valid_quic_v1_multiaddrs(self):
|
||||||
"""Test QUIC multiaddr validation."""
|
# """Test valid QUIC v1 multiaddrs are detected."""
|
||||||
# Valid QUIC multiaddrs
|
# valid_addrs = [
|
||||||
valid = [
|
# "/ip4/127.0.0.1/udp/4001/quic-v1",
|
||||||
# TODO: Update Multiaddr package to accept quic-v1
|
# "/ip4/192.168.1.1/udp/8080/quic-v1",
|
||||||
Multiaddr(
|
# "/ip6/::1/udp/4001/quic-v1",
|
||||||
f"/ip4/127.0.0.1/udp/4001/{QUICTransportConfig.PROTOCOL_QUIC_DRAFT29}"
|
# "/ip6/2001:db8::1/udp/5000/quic-v1",
|
||||||
),
|
# ]
|
||||||
Multiaddr(
|
|
||||||
f"/ip4/192.168.1.1/udp/8080/{QUICTransportConfig.PROTOCOL_QUIC_DRAFT29}"
|
|
||||||
),
|
|
||||||
Multiaddr(f"/ip6/::1/udp/4001/{QUICTransportConfig.PROTOCOL_QUIC_DRAFT29}"),
|
|
||||||
Multiaddr(
|
|
||||||
f"/ip4/127.0.0.1/udp/4001/{QUICTransportConfig.PROTOCOL_QUIC_V1}"
|
|
||||||
),
|
|
||||||
Multiaddr(
|
|
||||||
f"/ip4/192.168.1.1/udp/8080/{QUICTransportConfig.PROTOCOL_QUIC_V1}"
|
|
||||||
),
|
|
||||||
Multiaddr(f"/ip6/::1/udp/4001/{QUICTransportConfig.PROTOCOL_QUIC_V1}"),
|
|
||||||
]
|
|
||||||
|
|
||||||
for addr in valid:
|
# for addr_str in valid_addrs:
|
||||||
assert is_quic_multiaddr(addr)
|
# maddr = Multiaddr(addr_str)
|
||||||
|
# assert is_quic_multiaddr(maddr), f"Should detect {addr_str} as QUIC"
|
||||||
|
|
||||||
# Invalid multiaddrs
|
# def test_valid_quic_draft29_multiaddrs(self):
|
||||||
invalid = [
|
# """Test valid QUIC draft-29 multiaddrs are detected."""
|
||||||
Multiaddr("/ip4/127.0.0.1/tcp/4001"),
|
# valid_addrs = [
|
||||||
Multiaddr("/ip4/127.0.0.1/udp/4001"),
|
# "/ip4/127.0.0.1/udp/4001/quic",
|
||||||
Multiaddr("/ip4/127.0.0.1/udp/4001/ws"),
|
# "/ip4/10.0.0.1/udp/9000/quic",
|
||||||
]
|
# "/ip6/::1/udp/4001/quic",
|
||||||
|
# "/ip6/fe80::1/udp/6000/quic",
|
||||||
|
# ]
|
||||||
|
|
||||||
for addr in invalid:
|
# for addr_str in valid_addrs:
|
||||||
assert not is_quic_multiaddr(addr)
|
# maddr = Multiaddr(addr_str)
|
||||||
|
# assert is_quic_multiaddr(maddr), f"Should detect {addr_str} as QUIC"
|
||||||
|
|
||||||
def test_quic_multiaddr_to_endpoint(self):
|
# def test_invalid_multiaddrs(self):
|
||||||
"""Test multiaddr to endpoint conversion."""
|
# """Test non-QUIC multiaddrs are not detected."""
|
||||||
addr = Multiaddr("/ip4/192.168.1.100/udp/4001/quic")
|
# invalid_addrs = [
|
||||||
host, port = quic_multiaddr_to_endpoint(addr)
|
# "/ip4/127.0.0.1/tcp/4001", # TCP, not QUIC
|
||||||
|
# "/ip4/127.0.0.1/udp/4001", # UDP without QUIC
|
||||||
|
# "/ip4/127.0.0.1/udp/4001/ws", # WebSocket
|
||||||
|
# "/ip4/127.0.0.1/quic-v1", # Missing UDP
|
||||||
|
# "/udp/4001/quic-v1", # Missing IP
|
||||||
|
# "/dns4/example.com/tcp/443/tls", # Completely different
|
||||||
|
# ]
|
||||||
|
|
||||||
assert host == "192.168.1.100"
|
# for addr_str in invalid_addrs:
|
||||||
assert port == 4001
|
# maddr = Multiaddr(addr_str)
|
||||||
|
# assert not is_quic_multiaddr(maddr), f"Should not detect {addr_str} as QUIC"
|
||||||
|
|
||||||
# Test IPv6
|
# def test_malformed_multiaddrs(self):
|
||||||
# TODO: Update Multiaddr project to handle ip6
|
# """Test malformed multiaddrs don't crash."""
|
||||||
# addr6 = Multiaddr("/ip6/::1/udp/8080/quic")
|
# # These should not raise exceptions, just return False
|
||||||
# host6, port6 = quic_multiaddr_to_endpoint(addr6)
|
# malformed = [
|
||||||
|
# Multiaddr("/ip4/127.0.0.1"),
|
||||||
|
# Multiaddr("/invalid"),
|
||||||
|
# ]
|
||||||
|
|
||||||
# assert host6 == "::1"
|
# for maddr in malformed:
|
||||||
# assert port6 == 8080
|
# assert not is_quic_multiaddr(maddr)
|
||||||
|
|
||||||
def test_create_quic_multiaddr(self):
|
|
||||||
"""Test QUIC multiaddr creation."""
|
|
||||||
# IPv4
|
|
||||||
addr = create_quic_multiaddr("127.0.0.1", 4001, "/quic")
|
|
||||||
assert str(addr) == "/ip4/127.0.0.1/udp/4001/quic"
|
|
||||||
|
|
||||||
# IPv6
|
# class TestQuicMultiaddrToEndpoint:
|
||||||
addr6 = create_quic_multiaddr("::1", 8080, "/quic")
|
# """Test endpoint extraction from QUIC multiaddrs."""
|
||||||
assert str(addr6) == "/ip6/::1/udp/8080/quic"
|
|
||||||
|
|
||||||
def test_multiaddr_to_quic_version(self):
|
# def test_ipv4_extraction(self):
|
||||||
"""Test QUIC version extraction."""
|
# """Test IPv4 host/port extraction."""
|
||||||
addr = Multiaddr("/ip4/127.0.0.1/udp/4001/quic")
|
# test_cases = [
|
||||||
version = multiaddr_to_quic_version(addr)
|
# ("/ip4/127.0.0.1/udp/4001/quic-v1", ("127.0.0.1", 4001)),
|
||||||
assert version in ["quic", "quic-v1"] # Depending on implementation
|
# ("/ip4/192.168.1.100/udp/8080/quic", ("192.168.1.100", 8080)),
|
||||||
|
# ("/ip4/10.0.0.1/udp/9000/quic-v1", ("10.0.0.1", 9000)),
|
||||||
|
# ]
|
||||||
|
|
||||||
def test_invalid_multiaddr_operations(self):
|
# for addr_str, expected in test_cases:
|
||||||
"""Test error handling for invalid multiaddrs."""
|
# maddr = Multiaddr(addr_str)
|
||||||
invalid_addr = Multiaddr("/ip4/127.0.0.1/tcp/4001")
|
# result = quic_multiaddr_to_endpoint(maddr)
|
||||||
|
# assert result == expected, f"Failed for {addr_str}"
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
# def test_ipv6_extraction(self):
|
||||||
quic_multiaddr_to_endpoint(invalid_addr)
|
# """Test IPv6 host/port extraction."""
|
||||||
|
# test_cases = [
|
||||||
|
# ("/ip6/::1/udp/4001/quic-v1", ("::1", 4001)),
|
||||||
|
# ("/ip6/2001:db8::1/udp/5000/quic", ("2001:db8::1", 5000)),
|
||||||
|
# ]
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
# for addr_str, expected in test_cases:
|
||||||
multiaddr_to_quic_version(invalid_addr)
|
# maddr = Multiaddr(addr_str)
|
||||||
|
# result = quic_multiaddr_to_endpoint(maddr)
|
||||||
|
# assert result == expected, f"Failed for {addr_str}"
|
||||||
|
|
||||||
|
# def test_invalid_multiaddr_raises_error(self):
|
||||||
|
# """Test invalid multiaddrs raise appropriate errors."""
|
||||||
|
# invalid_addrs = [
|
||||||
|
# "/ip4/127.0.0.1/tcp/4001", # Not QUIC
|
||||||
|
# "/ip4/127.0.0.1/udp/4001", # Missing QUIC protocol
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# for addr_str in invalid_addrs:
|
||||||
|
# maddr = Multiaddr(addr_str)
|
||||||
|
# with pytest.raises(QUICInvalidMultiaddrError):
|
||||||
|
# quic_multiaddr_to_endpoint(maddr)
|
||||||
|
|
||||||
|
|
||||||
|
# class TestMultiaddrToQuicVersion:
|
||||||
|
# """Test QUIC version extraction."""
|
||||||
|
|
||||||
|
# def test_quic_v1_detection(self):
|
||||||
|
# """Test QUIC v1 version detection."""
|
||||||
|
# addrs = [
|
||||||
|
# "/ip4/127.0.0.1/udp/4001/quic-v1",
|
||||||
|
# "/ip6/::1/udp/5000/quic-v1",
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# for addr_str in addrs:
|
||||||
|
# maddr = Multiaddr(addr_str)
|
||||||
|
# version = multiaddr_to_quic_version(maddr)
|
||||||
|
# assert version == "quic-v1", f"Should detect quic-v1 for {addr_str}"
|
||||||
|
|
||||||
|
# def test_quic_draft29_detection(self):
|
||||||
|
# """Test QUIC draft-29 version detection."""
|
||||||
|
# addrs = [
|
||||||
|
# "/ip4/127.0.0.1/udp/4001/quic",
|
||||||
|
# "/ip6/::1/udp/5000/quic",
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# for addr_str in addrs:
|
||||||
|
# maddr = Multiaddr(addr_str)
|
||||||
|
# version = multiaddr_to_quic_version(maddr)
|
||||||
|
# assert version == "quic", f"Should detect quic for {addr_str}"
|
||||||
|
|
||||||
|
# def test_non_quic_raises_error(self):
|
||||||
|
# """Test non-QUIC multiaddrs raise error."""
|
||||||
|
# maddr = Multiaddr("/ip4/127.0.0.1/tcp/4001")
|
||||||
|
# with pytest.raises(QUICInvalidMultiaddrError):
|
||||||
|
# multiaddr_to_quic_version(maddr)
|
||||||
|
|
||||||
|
|
||||||
|
# class TestCreateQuicMultiaddr:
|
||||||
|
# """Test QUIC multiaddr creation."""
|
||||||
|
|
||||||
|
# def test_ipv4_creation(self):
|
||||||
|
# """Test IPv4 QUIC multiaddr creation."""
|
||||||
|
# test_cases = [
|
||||||
|
# ("127.0.0.1", 4001, "quic-v1", "/ip4/127.0.0.1/udp/4001/quic-v1"),
|
||||||
|
# ("192.168.1.1", 8080, "quic", "/ip4/192.168.1.1/udp/8080/quic"),
|
||||||
|
# ("10.0.0.1", 9000, "/quic-v1", "/ip4/10.0.0.1/udp/9000/quic-v1"),
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# for host, port, version, expected in test_cases:
|
||||||
|
# result = create_quic_multiaddr(host, port, version)
|
||||||
|
# assert str(result) == expected
|
||||||
|
|
||||||
|
# def test_ipv6_creation(self):
|
||||||
|
# """Test IPv6 QUIC multiaddr creation."""
|
||||||
|
# test_cases = [
|
||||||
|
# ("::1", 4001, "quic-v1", "/ip6/::1/udp/4001/quic-v1"),
|
||||||
|
# ("2001:db8::1", 5000, "quic", "/ip6/2001:db8::1/udp/5000/quic"),
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# for host, port, version, expected in test_cases:
|
||||||
|
# result = create_quic_multiaddr(host, port, version)
|
||||||
|
# assert str(result) == expected
|
||||||
|
|
||||||
|
# def test_default_version(self):
|
||||||
|
# """Test default version is quic-v1."""
|
||||||
|
# result = create_quic_multiaddr("127.0.0.1", 4001)
|
||||||
|
# expected = "/ip4/127.0.0.1/udp/4001/quic-v1"
|
||||||
|
# assert str(result) == expected
|
||||||
|
|
||||||
|
# def test_invalid_inputs_raise_errors(self):
|
||||||
|
# """Test invalid inputs raise appropriate errors."""
|
||||||
|
# # Invalid IP
|
||||||
|
# with pytest.raises(QUICInvalidMultiaddrError):
|
||||||
|
# create_quic_multiaddr("invalid-ip", 4001)
|
||||||
|
|
||||||
|
# # Invalid port
|
||||||
|
# with pytest.raises(QUICInvalidMultiaddrError):
|
||||||
|
# create_quic_multiaddr("127.0.0.1", 70000)
|
||||||
|
|
||||||
|
# with pytest.raises(QUICInvalidMultiaddrError):
|
||||||
|
# create_quic_multiaddr("127.0.0.1", -1)
|
||||||
|
|
||||||
|
# # Invalid version
|
||||||
|
# with pytest.raises(QUICInvalidMultiaddrError):
|
||||||
|
# create_quic_multiaddr("127.0.0.1", 4001, "invalid-version")
|
||||||
|
|
||||||
|
|
||||||
|
# class TestQuicVersionToWireFormat:
|
||||||
|
# """Test QUIC version to wire format conversion."""
|
||||||
|
|
||||||
|
# def test_supported_versions(self):
|
||||||
|
# """Test supported version conversions."""
|
||||||
|
# test_cases = [
|
||||||
|
# ("quic-v1", 0x00000001), # RFC 9000
|
||||||
|
# ("quic", 0xFF00001D), # draft-29
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# for version, expected_wire in test_cases:
|
||||||
|
# result = quic_version_to_wire_format(TProtocol(version))
|
||||||
|
# assert result == expected_wire, f"Failed for version {version}"
|
||||||
|
|
||||||
|
# def test_unsupported_version_raises_error(self):
|
||||||
|
# """Test unsupported versions raise error."""
|
||||||
|
# with pytest.raises(QUICUnsupportedVersionError):
|
||||||
|
# quic_version_to_wire_format(TProtocol("unsupported-version"))
|
||||||
|
|
||||||
|
|
||||||
|
# class TestGetAlpnProtocols:
|
||||||
|
# """Test ALPN protocol retrieval."""
|
||||||
|
|
||||||
|
# def test_returns_libp2p_protocols(self):
|
||||||
|
# """Test returns expected libp2p ALPN protocols."""
|
||||||
|
# protocols = get_alpn_protocols()
|
||||||
|
# assert protocols == ["libp2p"]
|
||||||
|
# assert isinstance(protocols, list)
|
||||||
|
|
||||||
|
# def test_returns_copy(self):
|
||||||
|
# """Test returns a copy, not the original list."""
|
||||||
|
# protocols1 = get_alpn_protocols()
|
||||||
|
# protocols2 = get_alpn_protocols()
|
||||||
|
|
||||||
|
# # Modify one list
|
||||||
|
# protocols1.append("test")
|
||||||
|
|
||||||
|
# # Other list should be unchanged
|
||||||
|
# assert protocols2 == ["libp2p"]
|
||||||
|
|
||||||
|
|
||||||
|
# class TestNormalizeQuicMultiaddr:
|
||||||
|
# """Test QUIC multiaddr normalization."""
|
||||||
|
|
||||||
|
# def test_already_normalized(self):
|
||||||
|
# """Test already normalized multiaddrs pass through."""
|
||||||
|
# addr_str = "/ip4/127.0.0.1/udp/4001/quic-v1"
|
||||||
|
# maddr = Multiaddr(addr_str)
|
||||||
|
|
||||||
|
# result = normalize_quic_multiaddr(maddr)
|
||||||
|
# assert str(result) == addr_str
|
||||||
|
|
||||||
|
# def test_normalize_different_versions(self):
|
||||||
|
# """Test normalization works for different QUIC versions."""
|
||||||
|
# test_cases = [
|
||||||
|
# "/ip4/127.0.0.1/udp/4001/quic-v1",
|
||||||
|
# "/ip4/127.0.0.1/udp/4001/quic",
|
||||||
|
# "/ip6/::1/udp/5000/quic-v1",
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# for addr_str in test_cases:
|
||||||
|
# maddr = Multiaddr(addr_str)
|
||||||
|
# result = normalize_quic_multiaddr(maddr)
|
||||||
|
|
||||||
|
# # Should be valid QUIC multiaddr
|
||||||
|
# assert is_quic_multiaddr(result)
|
||||||
|
|
||||||
|
# # Should be parseable
|
||||||
|
# host, port = quic_multiaddr_to_endpoint(result)
|
||||||
|
# version = multiaddr_to_quic_version(result)
|
||||||
|
|
||||||
|
# # Should match original
|
||||||
|
# orig_host, orig_port = quic_multiaddr_to_endpoint(maddr)
|
||||||
|
# orig_version = multiaddr_to_quic_version(maddr)
|
||||||
|
|
||||||
|
# assert host == orig_host
|
||||||
|
# assert port == orig_port
|
||||||
|
# assert version == orig_version
|
||||||
|
|
||||||
|
# def test_non_quic_raises_error(self):
|
||||||
|
# """Test non-QUIC multiaddrs raise error."""
|
||||||
|
# maddr = Multiaddr("/ip4/127.0.0.1/tcp/4001")
|
||||||
|
# with pytest.raises(QUICInvalidMultiaddrError):
|
||||||
|
# normalize_quic_multiaddr(maddr)
|
||||||
|
|
||||||
|
|
||||||
|
# class TestIntegration:
|
||||||
|
# """Integration tests for utility functions working together."""
|
||||||
|
|
||||||
|
# def test_round_trip_conversion(self):
|
||||||
|
# """Test creating and parsing multiaddrs works correctly."""
|
||||||
|
# test_cases = [
|
||||||
|
# ("127.0.0.1", 4001, "quic-v1"),
|
||||||
|
# ("::1", 5000, "quic"),
|
||||||
|
# ("192.168.1.100", 8080, "quic-v1"),
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# for host, port, version in test_cases:
|
||||||
|
# # Create multiaddr
|
||||||
|
# maddr = create_quic_multiaddr(host, port, version)
|
||||||
|
|
||||||
|
# # Should be detected as QUIC
|
||||||
|
# assert is_quic_multiaddr(maddr)
|
||||||
|
|
||||||
|
# # Should extract original values
|
||||||
|
# extracted_host, extracted_port = quic_multiaddr_to_endpoint(maddr)
|
||||||
|
# extracted_version = multiaddr_to_quic_version(maddr)
|
||||||
|
|
||||||
|
# assert extracted_host == host
|
||||||
|
# assert extracted_port == port
|
||||||
|
# assert extracted_version == version
|
||||||
|
|
||||||
|
# # Should normalize to same value
|
||||||
|
# normalized = normalize_quic_multiaddr(maddr)
|
||||||
|
# assert str(normalized) == str(maddr)
|
||||||
|
|
||||||
|
# def test_wire_format_integration(self):
|
||||||
|
# """Test wire format conversion works with version detection."""
|
||||||
|
# addr_str = "/ip4/127.0.0.1/udp/4001/quic-v1"
|
||||||
|
# maddr = Multiaddr(addr_str)
|
||||||
|
|
||||||
|
# # Extract version and convert to wire format
|
||||||
|
# version = multiaddr_to_quic_version(maddr)
|
||||||
|
# wire_format = quic_version_to_wire_format(version)
|
||||||
|
|
||||||
|
# # Should be QUIC v1 wire format
|
||||||
|
# assert wire_format == 0x00000001
|
||||||
|
|||||||
Reference in New Issue
Block a user