From bad52bf668e1d565b98232c8f17fa317f93e97b8 Mon Sep 17 00:00:00 2001 From: guha-rahul <69rahul16@gmail.com> Date: Wed, 23 Jul 2025 20:27:09 +0530 Subject: [PATCH] struct --- libp2p/security/exceptions.py | 4 + libp2p/security/tls/__init__.py | 36 ++++ libp2p/security/tls/certificate.py | 120 +++++++++++++ libp2p/security/tls/io.py | 133 +++++++++++++++ libp2p/security/tls/transport.py | 259 ++++++++++++++++------------- 5 files changed, 441 insertions(+), 111 deletions(-) create mode 100644 libp2p/security/tls/certificate.py create mode 100644 libp2p/security/tls/io.py diff --git a/libp2p/security/exceptions.py b/libp2p/security/exceptions.py index bff09d933..d874ce745 100644 --- a/libp2p/security/exceptions.py +++ b/libp2p/security/exceptions.py @@ -5,3 +5,7 @@ class HandshakeFailure(BaseLibp2pError): pass + + +class SecurityError(BaseLibp2pError): + pass diff --git a/libp2p/security/tls/__init__.py b/libp2p/security/tls/__init__.py index e69de29bb..5c8c92e12 100644 --- a/libp2p/security/tls/__init__.py +++ b/libp2p/security/tls/__init__.py @@ -0,0 +1,36 @@ +""" +TLS security transport for libp2p. + +This module provides a comprehensive TLS transport implementation +that follows the Go libp2p TLS specification. +""" + +from libp2p.security.tls.transport import ( + TLSTransport, + IdentityConfig, + create_tls_transport, + PROTOCOL_ID +) +from libp2p.security.tls.io import TLSReadWriter +from libp2p.security.tls.certificate import ( + generate_certificate, + create_cert_template, + verify_certificate_chain, + pub_key_from_cert_chain, + SignedKey, + ALPN_PROTOCOL +) + +__all__ = [ + "TLSTransport", + "IdentityConfig", + "TLSReadWriter", + "create_tls_transport", + "generate_certificate", + "create_cert_template", + "verify_certificate_chain", + "pub_key_from_cert_chain", + "SignedKey", + "PROTOCOL_ID", + "ALPN_PROTOCOL" +] diff --git a/libp2p/security/tls/certificate.py b/libp2p/security/tls/certificate.py new file mode 100644 index 000000000..f9a893a15 --- /dev/null +++ b/libp2p/security/tls/certificate.py @@ -0,0 +1,120 @@ +""" +TLS certificate utilities for libp2p. + +This module provides certificate generation and verification functions +that embed libp2p peer identity information in X.509 extensions. +""" + +from dataclasses import dataclass + +from cryptography import x509 +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.x509.oid import ObjectIdentifier + +from libp2p.crypto.keys import PrivateKey, PublicKey + +# ALPN protocol for libp2p TLS +ALPN_PROTOCOL = "libp2p" + +# Custom OID for libp2p peer identity extension (same as Rust implementation) +LIBP2P_EXTENSION_OID = ObjectIdentifier("1.3.6.1.4.1.53594.1.1") + + +@dataclass +class SignedKey: + """Represents a signed public key embedded in certificate extension.""" + + public_key_bytes: bytes + signature: bytes + + +def create_cert_template() -> x509.CertificateBuilder: + """ + Create a certificate template for libp2p TLS certificates. + + Returns: + Certificate builder template + + """ + raise NotImplementedError("TLS certificate template creation not implemented") + + +def add_libp2p_extension( + cert_builder: x509.CertificateBuilder, peer_public_key: PublicKey, signature: bytes +) -> x509.CertificateBuilder: + """ + Add libp2p peer identity extension to certificate. + + Args: + cert_builder: Certificate builder to modify + peer_public_key: Peer's public key to embed + signature: Signature over the certificate's public key + + Returns: + Certificate builder with libp2p extension + + """ + raise NotImplementedError("libp2p extension addition not implemented") + + +def generate_certificate( + private_key: PrivateKey, cert_template: x509.CertificateBuilder +) -> tuple[str, str]: + """ + Generate a self-signed certificate with libp2p extensions. + + Args: + private_key: Private key for signing + cert_template: Certificate template + + Returns: + Tuple of (certificate PEM, private key PEM) + + """ + raise NotImplementedError("Certificate generation not implemented") + + +def verify_certificate_chain(cert_chain: list[x509.Certificate]) -> PublicKey: + """ + Verify certificate chain and extract peer public key from libp2p extension. + + Args: + cert_chain: List of certificates in the chain + + Returns: + Public key from libp2p extension + + Raises: + SecurityError: If verification fails + + """ + raise NotImplementedError("Certificate chain verification not implemented") + + +def pub_key_from_cert_chain(cert_chain: list[x509.Certificate]) -> PublicKey: + """ + Extract public key from certificate chain. + + This is an alias for verify_certificate_chain for compatibility. + + Args: + cert_chain: Certificate chain + + Returns: + Public key + + """ + return verify_certificate_chain(cert_chain) + + +def generate_self_signed_cert() -> tuple[ec.EllipticCurvePrivateKey, x509.Certificate]: + """ + Generate a self-signed certificate for testing purposes. + + This is a utility function based on the guide examples. + + Returns: + Tuple of (private key, certificate) + + """ + raise NotImplementedError("Self-signed certificate generation not implemented") diff --git a/libp2p/security/tls/io.py b/libp2p/security/tls/io.py new file mode 100644 index 000000000..482075995 --- /dev/null +++ b/libp2p/security/tls/io.py @@ -0,0 +1,133 @@ +""" +TLS I/O utilities for libp2p. + +This module provides TLS-specific message reading and writing functionality, +similar to how noise handles encrypted communication. +""" + +import ssl + +from cryptography import x509 + +from libp2p.abc import IRawConnection +from libp2p.io.abc import EncryptedMsgReadWriter + + +class TLSReadWriter(EncryptedMsgReadWriter): + """ + TLS encrypted message reader/writer. + + This class handles TLS encryption/decryption over a raw connection, + similar to NoiseTransportReadWriter in the noise implementation. + """ + + def __init__( + self, + conn: IRawConnection, + ssl_context: ssl.SSLContext, + server_side: bool = False, + server_hostname: str | None = None, + ): + """ + Initialize TLS reader/writer. + + Args: + conn: Raw connection to wrap + ssl_context: SSL context for TLS operations + server_side: Whether to act as TLS server + server_hostname: Server hostname for client connections + + """ + self.raw_connection = conn + self.ssl_context = ssl_context + self.server_side = server_side + self.server_hostname = server_hostname + self._ssl_socket = None + self._peer_certificate: x509.Certificate | None = None + self._handshake_complete = False + + async def handshake(self) -> None: + """ + Perform TLS handshake. + + Raises: + HandshakeFailure: If handshake fails + + """ + raise NotImplementedError("TLS handshake not implemented") + + def get_peer_certificate(self) -> x509.Certificate | None: + """ + Get the peer's certificate after handshake. + + Returns: + Peer certificate or None if not available + + """ + return self._peer_certificate + + async def write_msg(self, msg: bytes) -> None: + """ + Write an encrypted message. + + Args: + msg: Message to encrypt and send + + """ + raise NotImplementedError("TLS write_msg not implemented") + + async def read_msg(self) -> bytes: + """ + Read and decrypt a message. + + Returns: + Decrypted message bytes + + """ + raise NotImplementedError("TLS read_msg not implemented") + + def encrypt(self, data: bytes) -> bytes: + """ + Encrypt data for transmission. + + Args: + data: Data to encrypt + + Returns: + Encrypted data + + """ + # In TLS, encryption is handled at the SSL layer during write_msg + # This method exists for interface compatibility + return data + + def decrypt(self, data: bytes) -> bytes: + """ + Decrypt received data. + + Args: + data: Encrypted data to decrypt + + Returns: + Decrypted data + + """ + # In TLS, decryption is handled at the SSL layer during read_msg + # This method exists for interface compatibility + return data + + async def close(self) -> None: + """Close the TLS connection.""" + raise NotImplementedError("TLS close not implemented") + + def get_remote_address(self) -> tuple[str, int] | None: + """ + Get remote address from underlying connection. + + Returns: + Remote address tuple or None + + """ + if hasattr(self.raw_connection, "get_remote_address"): + return self.raw_connection.get_remote_address() + return None diff --git a/libp2p/security/tls/transport.py b/libp2p/security/tls/transport.py index 3405c0035..7e5983992 100644 --- a/libp2p/security/tls/transport.py +++ b/libp2p/security/tls/transport.py @@ -1,140 +1,177 @@ +from dataclasses import dataclass import ssl -from typing import Optional +from typing import Any + +from cryptography import x509 from libp2p.abc import IRawConnection, ISecureConn, ISecureTransport +from libp2p.crypto.keys import KeyPair, PrivateKey from libp2p.custom_types import TProtocol -from libp2p.crypto.keys import KeyPair from libp2p.peer.id import ID from libp2p.security.secure_session import SecureSession +from libp2p.security.tls.io import TLSReadWriter + +# Protocol ID for TLS transport +PROTOCOL_ID = TProtocol("/tls/1.0.0") + -PROTOCOL_ID = TProtocol("/tls/1.3") # used by muxer to negotiate the security channel +@dataclass +class IdentityConfig: + """Configuration for TLS identity.""" + cert_template: x509.CertificateBuilder | None = None + key_log_writer: Any | None = None -class TLSStream: + +class TLSTransport(ISecureTransport): """ - Thin wrapper that feeds raw bytes through an in-memory TLS state machine. - Works for *unit-tests* and in-memory transports – no sockets needed. + TLS transport implementation following the noise pattern. + + Features: + - TLS 1.3 support + - Custom certificate generation with libp2p extensions + - Peer ID verification + - ALPN protocol negotiation """ - def __init__(self, raw: IRawConnection, ssl_obj: ssl.SSLObject): - self._raw = raw - self._tls = ssl_obj + libp2p_privkey: PrivateKey + local_peer: ID + early_data: bytes | None + + def __init__( + self, + libp2p_keypair: KeyPair, + early_data: bytes | None = None, + ): + """Initialize TLS transport.""" + self.libp2p_privkey = libp2p_keypair.private_key + self.local_peer = ID.from_pubkey(libp2p_keypair.public_key) + self.early_data = early_data + + def create_ssl_context(self, server_side: bool = False) -> ssl.SSLContext: + """ + Create SSL context for TLS connections. + + Args: + server_side: Whether this is for server-side connections + + Returns: + Configured SSL context - # -------- helpers used by read/write -------- - async def _pump_write(self) -> None: - buf = self._tls.bio_write # type: ignore[attr-defined] - if buf: - await self._raw.write(buf) - self._tls.bio_write = b"" # type: ignore[attr-defined] + """ + raise NotImplementedError("SSL context creation not implemented") - async def _pump_read(self) -> None: - data = await self._raw.read(65536) - if data: - self._tls.bio_read(data) # type: ignore[attr-defined] + async def secure_inbound(self, conn: IRawConnection) -> ISecureConn: + """ + Secure an inbound connection as server. - # -------- libp2p MsgReadWriteCloser-like API -------- - async def write(self, data: bytes) -> None: - self._tls.write(data) - await self._pump_write() + Args: + conn: Raw connection to secure - async def read(self, n: int = -1) -> bytes: - while True: - try: - return self._tls.read(n) - except ssl.SSLWantReadError: - await self._pump_read() + Returns: + Secured connection (SecureSession) - async def close(self) -> None: - await self._raw.close() + """ + # Create SSL context for server + ssl_context = self.create_ssl_context(server_side=True) + # Create TLS reader/writer + tls_reader_writer = TLSReadWriter( + conn=conn, ssl_context=ssl_context, server_side=True + ) -class TLSTransport(ISecureTransport): - """ - *Minimal* TLS-1.3 security transport. - – no X.509 PKI; the peer ID is carried in the SAN of a self-signed cert - – no 0-RTT yet; early-data parameter kept for symmetry - """ + # Perform handshake + await tls_reader_writer.handshake() - def __init__( - self, - keypair: KeyPair, - cert_pem: bytes, - key_pem: bytes, - *, - early_data: Optional[bytes] = None, - ) -> None: - self._kp = keypair - self._local_peer = ID.from_pubkey(keypair.public_key) - self._early_data = early_data - - ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - ctx.minimum_version = ctx.maximum_version = ssl.TLSVersion.TLSv1_3 - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE # peer auth is done via SAN → peer-id - ctx.set_alpn_protocols(["/libp2p-tls/1.0.0"]) - ctx.load_cert_chain(certfile=cert_pem, keyfile=key_pem) - self._ctx = ctx - - # ---------- ISecureTransport ---------- - async def secure_inbound(self, conn: IRawConnection) -> ISecureConn: - ssl_obj = self._ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), server_side=True) - secure_stream = TLSStream(conn, ssl_obj) - await self._do_handshake(secure_stream, ssl_obj) - remote_peer = self._peer_id_from_cert(ssl_obj) + # Extract peer information + peer_cert = tls_reader_writer.get_peer_certificate() + if not peer_cert: + raise NotImplementedError("Peer certificate extraction not implemented") + + # Extract remote public key from certificate + remote_public_key = self._extract_public_key_from_cert(peer_cert) + remote_peer_id = ID.from_pubkey(remote_public_key) + + # Return SecureSession like noise does return SecureSession( - local_peer=self._local_peer, - local_private_key=self._kp.private_key, - remote_peer=remote_peer, - remote_permanent_pubkey=None, + local_peer=self.local_peer, + local_private_key=self.libp2p_privkey, + remote_peer=remote_peer_id, + remote_permanent_pubkey=remote_public_key, is_initiator=False, - conn=secure_stream, + conn=tls_reader_writer, ) async def secure_outbound(self, conn: IRawConnection, peer_id: ID) -> ISecureConn: - ssl_obj = self._ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), server_side=False) - secure_stream = TLSStream(conn, ssl_obj) - await self._do_handshake(secure_stream, ssl_obj) - remote_peer = self._peer_id_from_cert(ssl_obj) - if remote_peer != peer_id: - raise ValueError("peer-id in certificate does not match expectation") + """ + Secure an outbound connection as client. + + Args: + conn: Raw connection to secure + peer_id: Expected peer ID + + Returns: + Secured connection (SecureSession) + + """ + # Create SSL context for client + ssl_context = self.create_ssl_context(server_side=False) + + # Create TLS reader/writer + tls_reader_writer = TLSReadWriter( + conn=conn, ssl_context=ssl_context, server_side=False + ) + + # Perform handshake + await tls_reader_writer.handshake() + + # Extract peer information + peer_cert = tls_reader_writer.get_peer_certificate() + if not peer_cert: + raise NotImplementedError("Peer certificate extraction not implemented") + + # Extract and verify remote public key + remote_public_key = self._extract_public_key_from_cert(peer_cert) + remote_peer_id = ID.from_pubkey(remote_public_key) + + if remote_peer_id != peer_id: + raise NotImplementedError("Peer ID verification not implemented") + + # Return SecureSession like noise does return SecureSession( - local_peer=self._local_peer, - local_private_key=self._kp.private_key, - remote_peer=remote_peer, - remote_permanent_pubkey=None, + local_peer=self.local_peer, + local_private_key=self.libp2p_privkey, + remote_peer=peer_id, + remote_permanent_pubkey=remote_public_key, is_initiator=True, - conn=secure_stream, + conn=tls_reader_writer, ) - # ---------- helpers ---------- - async def _do_handshake(self, stream: TLSStream, ssl_obj: ssl.SSLObject) -> None: - # Perform TLS handshake over the in-memory BIOs - while True: - try: - ssl_obj.do_handshake() - await stream._pump_write() - break - except ssl.SSLWantReadError: - await stream._pump_read() - except ssl.SSLWantWriteError: - await stream._pump_write() - - @staticmethod - def _peer_id_from_cert(ssl_obj: ssl.SSLObject) -> ID: - """ - Very small helper that extracts the libp2p peer-id from the leaf - certificate SAN (Authority Key / OtherName). - For a minimal demo we simply hash the *public key* inside the cert, - matching go-libp2p-tls reference. - """ - cert_bin = ssl_obj.getpeercert(binary_form=True) - # ▸ Depending on your crypto helpers you can parse `cert_bin` with - # cryptography.x509 and re-code the logic below later. - # - # Here we just hash the raw SPKI to keep the demo self-contained: - from hashlib import sha256 - from libp2p.peer.id import ID - - # First 32 bytes of SHA-256 multicodec - see specs/tls/tls.md - digest = sha256(cert_bin).digest() - return ID.from_bytes(digest[:16]) # NOT production! just demo. + def _extract_public_key_from_cert(self, cert: x509.Certificate) -> Any: + """Extract public key from certificate.""" + raise NotImplementedError( + "Public key extraction from certificate not implemented" + ) + + def get_protocol_id(self) -> TProtocol: + """Get the protocol ID for this transport.""" + return PROTOCOL_ID + + +# Factory function for creating TLS transport +def create_tls_transport( + libp2p_keypair: KeyPair, + early_data: bytes | None = None, +) -> TLSTransport: + """ + Create a new TLS transport. + + Args: + libp2p_keypair: Key pair for the local peer + early_data: Optional early data for TLS handshake + + Returns: + TLS transport instance + + """ + return TLSTransport(libp2p_keypair, early_data)