|
18 | 18 | generate_certificate, |
19 | 19 | verify_certificate_chain, |
20 | 20 | ) |
21 | | -from libp2p.security.tls.exceptions import MissingLibp2pExtensionError |
| 21 | +from libp2p.security.tls.exceptions import ( |
| 22 | + MissingLibp2pExtensionError, |
| 23 | + TLSHandshakeFailure, |
| 24 | +) |
22 | 25 | from libp2p.security.tls.io import TLSReadWriter |
23 | 26 | import libp2p.utils |
24 | 27 | import libp2p.utils.paths |
@@ -127,15 +130,20 @@ def create_ssl_context(self, server_side: bool = False) -> ssl.SSLContext: |
127 | 130 | ssl.PROTOCOL_TLS_SERVER if server_side else ssl.PROTOCOL_TLS_CLIENT |
128 | 131 | ) |
129 | 132 | ctx.minimum_version = ssl.TLSVersion.TLSv1_3 |
130 | | - # We do our own verification (like Go's InsecureSkipVerify). |
131 | | - # Python's ssl module can't request a client cert without CA verification. |
132 | | - # TODO: Implement proper mutual TLS with custom verification |
| 133 | + # We do our own post-handshake verification of the libp2p extension. |
| 134 | + # check_hostname is disabled because we use self-signed peer certificates |
| 135 | + # that are not bound to DNS names. |
133 | 136 | ctx.check_hostname = False |
134 | 137 |
|
135 | | - # TODO: Fix this default: no client cert verification |
136 | | - # INBOUND connection can't ask for tls cert from remote peers |
137 | | - # ctx.verify_mode = ssl.CERT_OPTIONAL if server_side else ssl.CERT_NONE |
138 | | - ctx.verify_mode = ssl.CERT_NONE |
| 138 | + # For server-side (inbound) contexts we use CERT_OPTIONAL so that the |
| 139 | + # TLS engine *requests* a client certificate from the remote peer. |
| 140 | + # Without a CA configured Python will not reject an absent cert at the |
| 141 | + # TLS layer, but the cert IS made available via getpeercert(binary_form=True) |
| 142 | + # if one is sent. Our post-handshake logic then enforces the hard |
| 143 | + # requirement. |
| 144 | + # For client-side (outbound) contexts CERT_NONE is fine because the |
| 145 | + # server always sends a certificate in TLS 1.3. |
| 146 | + ctx.verify_mode = ssl.CERT_OPTIONAL if server_side else ssl.CERT_NONE |
139 | 147 |
|
140 | 148 | # Load our cached self-signed certificate bound to libp2p identity |
141 | 149 | import os |
@@ -304,55 +312,66 @@ async def secure_inbound(self, conn: IRawConnection) -> ISecureConn: |
304 | 312 | # Extract peer information |
305 | 313 | peer_cert = tls_reader_writer.get_peer_certificate() |
306 | 314 | if not peer_cert: |
307 | | - logger.warning("[INBOUND] Server couldn't fetch dialer's certificate") |
308 | | - |
309 | | - # TODO: Python ssl can't request client cert without CA verification. |
310 | | - # Use placeholder peer ID - client can still verify server identity. |
311 | | - logger.warning("TLS inbound: no peer cert (Python ssl limitation)") |
| 315 | + # The libp2p TLS spec mandates mutual authentication: every inbound |
| 316 | + # connection MUST present a certificate that carries the libp2p |
| 317 | + # extension so we can derive and verify the remote Peer ID. |
| 318 | + # |
| 319 | + # AutoTLS bootstrap connections are a legitimate exception: when a |
| 320 | + # node registers with the ACME broker it uses the primitive |
| 321 | + # key-exchange side-channel instead of the certificate extension. |
| 322 | + # That path is kept here but is strictly guarded by enable_autotls. |
| 323 | + # The normal (non-AutoTLS) path raises a hard TLSHandshakeFailure and |
| 324 | + # never returns a synthetic placeholder identity. |
312 | 325 |
|
313 | | - # Extaract the keys from primitive key-exchange, if autotls enabled |
314 | 326 | if self.enable_autotls: |
315 | 327 | remote_public_key = tls_reader_writer.remote_primitive_pk |
316 | 328 | remote_peer_id = tls_reader_writer.remote_primitive_peerid |
317 | 329 | logger.warning( |
318 | | - "TLS inbound: using peerid obtained from primitive key-exchange" |
319 | | - ) |
320 | | - else: |
321 | | - placeholder_keypair = libp2p.generate_new_ed25519_identity() |
322 | | - remote_public_key = placeholder_keypair.public_key |
323 | | - remote_peer_id = ID.from_pubkey(remote_public_key) |
324 | | - logger.error( |
325 | | - "TLS inbound: using peerid obtained from placeholder keypair" |
| 330 | + "TLS inbound [autotls]: no peer cert; " |
| 331 | + "using peer ID from primitive key-exchange" |
326 | 332 | ) |
327 | 333 |
|
328 | | - # This is the case when the autotls is enabled, and we did a self-signed |
329 | | - # certificate handshake with AUTO-TLS BROKER, and naturally we didn't do the |
330 | | - # primitive peer-identify exchange, so again use a placeholde |
331 | | - if self.enable_autotls and remote_peer_id is None: |
332 | | - placeholder_keypair = libp2p.generate_new_ed25519_identity() |
333 | | - remote_public_key = placeholder_keypair.public_key |
334 | | - remote_peer_id = ID.from_pubkey(remote_public_key) |
| 334 | + # AutoTLS broker registration: primitive exchange also absent. |
| 335 | + # Use a placeholder exclusively for the broker session. |
| 336 | + if remote_peer_id is None: |
| 337 | + placeholder_keypair = libp2p.generate_new_ed25519_identity() |
| 338 | + remote_public_key = placeholder_keypair.public_key |
| 339 | + remote_peer_id = ID.from_pubkey(remote_public_key) |
| 340 | + logger.warning( |
| 341 | + "TLS inbound [autotls broker]: no cert and no primitive " |
| 342 | + "exchange; using placeholder identity for broker session only" |
| 343 | + ) |
| 344 | + |
| 345 | + if remote_peer_id is None or remote_public_key is None: |
| 346 | + raise TLSHandshakeFailure( |
| 347 | + "TLS inbound [autotls]: could not determine remote identity " |
| 348 | + "from either certificate or primitive key-exchange." |
| 349 | + ) |
335 | 350 |
|
336 | | - if remote_peer_id is None: |
337 | | - raise ValueError( |
338 | | - "remote peer ID must be known before creating SecureSession" |
| 351 | + session = SecureSession( |
| 352 | + local_peer=self.local_peer, |
| 353 | + local_private_key=self.libp2p_privkey, |
| 354 | + remote_peer=remote_peer_id, |
| 355 | + remote_permanent_pubkey=remote_public_key, |
| 356 | + is_initiator=False, |
| 357 | + conn=tls_reader_writer, |
339 | 358 | ) |
340 | | - |
341 | | - if remote_public_key is None: |
342 | | - raise ValueError( |
343 | | - "remote public-key must be known before creating SecureSession" |
| 359 | + logger.debug( |
| 360 | + "TLS secure_inbound [autotls]: returning SecureSession " |
| 361 | + "with primitive-exchange identity" |
344 | 362 | ) |
| 363 | + return session |
345 | 364 |
|
346 | | - session = SecureSession( |
347 | | - local_peer=self.local_peer, |
348 | | - local_private_key=self.libp2p_privkey, |
349 | | - remote_peer=remote_peer_id, |
350 | | - remote_permanent_pubkey=remote_public_key, |
351 | | - is_initiator=False, |
352 | | - conn=tls_reader_writer, |
| 365 | + # Normal (non-AutoTLS) path: no certificate → hard failure. |
| 366 | + # Do NOT create a session or assign a synthetic identity. |
| 367 | + logger.error( |
| 368 | + "[INBOUND] Rejecting connection: remote peer sent no TLS certificate. " |
| 369 | + "Mutual authentication is required by the libp2p TLS spec." |
| 370 | + ) |
| 371 | + raise TLSHandshakeFailure( |
| 372 | + "Inbound TLS connection presented no client certificate. " |
| 373 | + "Mutual authentication is required by the libp2p TLS spec." |
353 | 374 | ) |
354 | | - logger.debug("TLS secure_inbound: returning placeholder SecureSession") |
355 | | - return session |
356 | 375 |
|
357 | 376 | # Extract remote public key from certificate |
358 | 377 | logger.debug("TLS secure_inbound: extracting public key from certificate") |
|
0 commit comments