fix: prevent inbound QUIC connections without peer certificate verification#1346
Conversation
acul71
left a comment
There was a problem hiding this comment.
AI-assisted review summary
Reviewed quic-fix @ 2c4b94b7 with local validation (lint, typecheck, 2888 tests passed, docs build). Full report: maintainer copy at downloads/AI-PR-REVIEWS/1346/AI-PR-REVIEW-1346-0.md.
Verdict: Sound security fix for #1345 — request changes before merge (missing regression test).
What works well
- Root-cause fix:
ServerQuicConnection._initialize()sets_request_client_certificate = TruebeforeServerHellois emitted insidereceive_datagram(). Preferable to switching tossl.CERT_OPTIONAL, which could reintroduce system-CA validation issues while libp2p still relies on manual identity verification withCERT_NONE. - Defense in depth: (1) TLS handshake requests client cert, (2) pre-callback cert guard in
_promote_pending_connection, (3) fail-closedQUICPeerVerificationErrorfor inbound connections without a peer certificate. - Housekeeping:
Fixes #1345, validnewsfragments/1345.bugfix.rst, clean merge withmain(0 behind / 3 ahead),connections_rejectednow incremented on verification failure.
Blocking
Missing regression test for the reported vulnerability
Issue #1345 documents a PoC where a raw aioquic client sends no peer certificate yet the listener callback was invoked. This PR adds no test covering that scenario.
Please add a test that:
- Starts a QUIC listener with a real security manager.
- Dials with a minimal client that does not present a libp2p certificate.
- Asserts the handler is not called and
connections_rejectedis incremented.
For a security fix, this should be a permanent regression test mirroring the issue PoC.
Should address
test_listener.pypromotion mocks (lines ~362–538): Mocks lack TLS handshake state / peer certificates. With the new pre-callback guard,_promote_pending_connectiontests may pass vacuously (connection_obj is None, tests skip). Update mocks (_handshake_complete,tls._peer_certificate, or patchget_peer_certificate) so happy-path promotion is still exercised alongside a new rejection test.- CI:
tox (3.11, core)failed on unrelatedtest_kad_dht.py::test_put_and_get_valueflake; wheel/lint jobs timed out (infra). Please re-run failed jobs.
Minor / follow-up
- PR title typo: certifate → certificate
- Relies on aioquic private APIs (
_initialize,tls._request_client_certificate) — consider noting aioquic version compatibility - Optional doc note in QUIC transport docs that inbound connections require a peer certificate
peer_id = remote_peer_id or local_peer_idfallback (#1345) unchanged — acceptable follow-up since unauthenticated connections are now rejected before the callback
Validation
| Check | Result |
|---|---|
make lint |
pass |
make typecheck |
pass |
make test |
2888 passed, 16 skipped |
make linux-docs |
pass |
Merge conflicts with main |
none |
Questions
- Was
ssl.CERT_OPTIONAL(suggested in #1345) rejected in favor of_request_client_certificatebecause of CA-store side effects? Worth documenting for future reviewers. - Which aioquic version(s) were tested — is
_initializestable across the pinned range? - Can you confirm the original PoC now shows
callback_invoked: falseon this branch?
Overall
| Quality | Good |
| Security impact | Fixes high-severity auth bypass |
| Merge readiness | Needs regression test + green CI |
Happy to re-review once the test lands.
Add integration and unit tests mirroring the issue libp2p#1345 PoC, fix promotion mock TLS state, reject unauthenticated inbound connections before connect() so connections_rejected is incremented, and document inbound cert requirements. Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up: blocking review feedback addressedPushed What was addedRegression coverage (#1345 PoC)
Unit / mock fixes
Small listener hardening (found while writing the integration test)
Docs
Local validation (before push)
CI statusCI is in progress on Merge readiness
Pending: green CI. If checks pass, this should be ready to merge from my side. |
acul71
left a comment
There was a problem hiding this comment.
Approved
CI is green on aa3da8f5. The blocking feedback from my earlier review is addressed.
Security fix (#1345)
The original fix correctly closes the inbound QUIC authentication bypass:
ServerQuicConnectionsets_request_client_certificatebeforeServerHello- Pre-callback cert guard in
_promote_pending_connection - Fail-closed
QUICPeerVerificationErrorfor inbound connections without a peer certificate
Follow-up commit (aa3da8f5)
- Regression test mirroring the #1345 PoC (raw aioquic client, no cert → handler not called,
connections_rejectedincremented) - Positive control for legitimate libp2p peers
- Unit test for
ServerQuicConnectionand inbound verification raise path - Promotion mock fixes in
test_listener.py - Listener hardening: cert guard before
connect()soconnections_rejectedis counted correctly - QUIC security documentation
Validation
All CI checks pass (core/lint/wheel/docs/interop/demos/utils matrix + Windows + RTD).
Merge readiness: Ready from my side.
What was wrong?
Fixes #1345
Inbound QUIC connections were being accepted and promoted to the application callback even when the remote client sent no peer certificate. This was an authentication bypass on the official QUIC listener path, allowing unauthenticated connections to appear legitimate.
The root cause involved a combination of
aioquicdefault behavior and libp2p security requirements:py-libp2p,verify_modeis intentionally set tossl.CERT_NONEso thataioquicdoes not validate self-signed libp2p certificates against the system CA store. We perform manual libp2p peer identity verification insideconnection.pyinstead.verify_modeisCERT_NONE,aioquic's TLS context (quic_conn.tls) defaults to_request_client_certificate = False.quic_conn.tls._request_client_certificate = Trueafter callingreceive_datagramon the initial packet.receive_datagramparses the initial datagram and synchronously processes the TLSClientHello, generating and sending theServerHello. Since our flag was not set yet, theServerHelloomitted theCertificateRequestblock.connection.pysilently returnedNonewhen_peer_certificatewas missing, failing open and invoking the user handler with an unauthenticated connection.What changed?
libp2p/transport/quic/connection.py_verify_peer_identity_with_securitynow explicitly raisesQUICPeerVerificationErrorwhen no peer certificate is present on an inbound (server-side) connection.None) since the client may not always have the remote cert immediately.get_peer_certificateto extract the certificate directly from the TLS context for early access before background tasks have flagged_handshake_completed.libp2p/transport/quic/listener.pyServerQuicConnectionsubclass ofaioquic'sQuicConnectionto cleanly hook into TLS initialization.aioquicinstantiates the TLS context internally inside a private_initialize()method on the very first datagram, right before it generates theServerHello. We override_initializeto immediately flipself.tls._request_client_certificate = Trueas soon as the context is created. This ensures theServerHelloaccurately includes theCertificateRequestblock._initializefrom the outside breaksaioquic's internal "first packet" state machine handling for internal variables like_versionand_network_paths._promote_pending_connection: inbound connections with no peer certificate are explicitly rejected and closed without invoking the user handler.connections_rejectedis correctly incremented.To-Do