Skip to content

QUIC: Inbound callback exposed before peer identity verification (authentication bypass) #1345

@sumanjeet0012

Description

@sumanjeet0012

Summary

A remote client can complete the QUIC/TLS handshake without presenting a
peer certificate
, and the QUIC listener still promotes the connection, invokes
the application callback, and accepts inbound stream data before a remote peer
identity exists.

Affected entry-point: QUICTransport.create_listener(...).listen(/quic-v1)

Validated against upstream main at commit
1e113ca309c866dd6f6125d57882adbf26f668fa (libp2p version 0.6.0).

Observed result

Running the PoC (raw aioquic client, no libp2p peer certificate sent):¯

field value
client_result.peer_cert_sent false
server_observation.callback_invoked true
server_observation.has_peer_certificate false
server_observation.remote_peer_id null
server_observation.peer_verified false
server_observation.peer_id falls back to the local peer ID
server_observation.inbound_stream_received "unauth-quic-stream-data"

Expected behavior

  • Inbound QUIC connections that present no peer certificate must be rejected
    before the application callback is invoked.
  • The listener must not surface a connection whose remote peer identity has not
    been established.

Actual behavior

  • config.py defaults verify_mode to ssl.CERT_NONE, so aioquic never
    requests a client certificate.
  • transport.py:_apply_tls_configuration hardcodes config.verify_mode = ssl.CERT_NONE,
    overriding whatever the transport config specifies.
  • connection.py:_verify_peer_identity_with_security silently returns None
    when no peer certificate is available — no error is raised.
  • listener.py:_promote_pending_connection calls the user callback regardless,
    because the verification method returns None rather than raising.
  • connection.py:__init__ sets self.peer_id = remote_peer_id or local_peer_id,
    masking the missing remote identity with the local peer ID.

Root Cause Analysis

libp2p/transport/quic/config.py:78       verify_mode = ssl.CERT_NONE  ← never requests cert
libp2p/transport/quic/transport.py:224   config.verify_mode = ssl.CERT_NONE  ← hardcoded override
libp2p/transport/quic/connection.py:596  if not self._peer_certificate: return None  ← silent fail
libp2p/transport/quic/listener.py:1017   remote_peer_id=None  ← no identity at construction
libp2p/transport/quic/listener.py:1064   await self._handler(connection)  ← unconditional callback

Impact

This is an authentication bypass on the official QUIC listener path.

  • An unauthenticated remote peer reaches the application callback.
  • The unauthenticated remote peer can deliver inbound QUIC stream data to that
    callback path.
  • Connection slots, callback work, and early per-peer state are consumed before
    authentication can succeed.
  • Application code cannot safely assume that an inbound QUIC callback
    represents an authenticated libp2p peer.
  • The connection object is created with identity state that is incomplete or
    misleading while still being surfaced to higher layers.
  • connection.peer_id silently falls back to the local peer ID, making
    the caller believe they know who they are talking to.

For libp2p QUIC, peer identity is not optional. Exposing the connection and
stream data to user code before that identity exists defeats the transport's
trust boundary.

Recommended Fix

  1. Change the default verify_mode to ssl.CERT_OPTIONAL so aioquic requests
    a client certificate during the TLS handshake.
  2. Remove the hardcoded ssl.CERT_NONE override in _apply_tls_configuration.
  3. Make _verify_peer_identity_with_security raise QUICPeerVerificationError
    (not silently return None) when no peer certificate is present on an
    inbound connection.
  4. Add an explicit pre-callback guard in _promote_pending_connection that
    closes the connection and increments connections_rejected without invoking
    the handler when no peer certificate is present.

Credit

This issue was highlighted by Yann Lorwyn.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions