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
- Change the default
verify_mode to ssl.CERT_OPTIONAL so aioquic requests
a client certificate during the TLS handshake.
- Remove the hardcoded
ssl.CERT_NONE override in _apply_tls_configuration.
- Make
_verify_peer_identity_with_security raise QUICPeerVerificationError
(not silently return None) when no peer certificate is present on an
inbound connection.
- 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.
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
mainat commit1e113ca309c866dd6f6125d57882adbf26f668fa(libp2pversion0.6.0).Observed result
Running the PoC (raw aioquic client, no libp2p peer certificate sent):¯
client_result.peer_cert_sentfalseserver_observation.callback_invokedtrueserver_observation.has_peer_certificatefalseserver_observation.remote_peer_idnullserver_observation.peer_verifiedfalseserver_observation.peer_idserver_observation.inbound_stream_received"unauth-quic-stream-data"Expected behavior
before the application callback is invoked.
been established.
Actual behavior
config.pydefaultsverify_modetossl.CERT_NONE, so aioquic neverrequests a client certificate.
transport.py:_apply_tls_configurationhardcodesconfig.verify_mode = ssl.CERT_NONE,overriding whatever the transport config specifies.
connection.py:_verify_peer_identity_with_securitysilently returnsNonewhen no peer certificate is available — no error is raised.
listener.py:_promote_pending_connectioncalls the user callback regardless,because the verification method returns
Nonerather than raising.connection.py:__init__setsself.peer_id = remote_peer_id or local_peer_id,masking the missing remote identity with the local peer ID.
Root Cause Analysis
Impact
This is an authentication bypass on the official QUIC listener path.
callback path.
authentication can succeed.
represents an authenticated libp2p peer.
misleading while still being surfaced to higher layers.
connection.peer_idsilently falls back to the local peer ID, makingthe 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
verify_modetossl.CERT_OPTIONALso aioquic requestsa client certificate during the TLS handshake.
ssl.CERT_NONEoverride in_apply_tls_configuration._verify_peer_identity_with_securityraiseQUICPeerVerificationError(not silently return
None) when no peer certificate is present on aninbound connection.
_promote_pending_connectionthatcloses the connection and increments
connections_rejectedwithout invokingthe handler when no peer certificate is present.
Credit
This issue was highlighted by Yann Lorwyn.