Repository navigation
fix(websocket): verify the server certificate on wss dials (#1550) - #1555
Conversation
|
@aojea — this is your #1550, and it is stacked on your #1549 because verification cannot go on before name-based dials do: while a cc @acul71 for review (you have the most history in @seetadev — flagging one maintainer decision: this changes a default, so it is filed as a CI is green (38/38). Local: |
|
awesome, thanks folks for iterating on this , looking forward for the next release |
|
Thanks @aojea 🙏 To be clear about the ordering for whoever picks this up: this one is blocked on #1549, not on review. #1549 is included here as its first commit because certificate verification cannot be turned on while names are still resolved to an IP before dialing. So the useful next step is merging #1549; I will then rebase and this collapses to a single commit. @acul71 @sumanjeet0012 — could one of you take a look at #1549 first? Both are small. CI here is green (38/38) and the branch is current with @seetadev one maintainer call on this one: it changes a default (an unverified |
b2b5142 to
ca98e76
Compare
A wss dial with no explicit TLS client configuration built its context with verify_mode = CERT_NONE and check_hostname = False, so new_host( enable_websocket=True) accepted any certificate from any server. go-libp2p dials with a zero tls.Config and js-libp2p uses the platform TLS stack; both verify against the system roots and check the hostname. Default to an unmodified ssl.create_default_context(). The previous behaviour stays reachable through WebsocketConfig(insecure_skip_verify=True), which logs a warning, and an explicit tls_client_config still wins over both. The flag name matches the one WebSocketTLSConfig already uses. The insecure default is why a name-based dial appeared to work while the transport resolved names to an IP first: hostname verification could not have succeeded against a certificate issued for the name. Stacked on libp2p#1549, which dials /dns, /dns4 and /dns6 by name; verification on its own would otherwise break those dials. libp2p's own handshake authenticates the peer inside the WebSocket, so this was never a stream integrity or confidentiality hole; what an unverified outer TLS allowed was terminating the connection on the path unnoticed.
After rebasing onto main (libp2p#1556), also document new_host trust/opt-out paths and note that dns_* timeouts apply only to /dnsaddr. Co-authored-by: Cursor <cursoragent@cursor.com>
ca98e76 to
8e1d85e
Compare
What was wrong?
Issue #1550
Fixes #1550
A
wssdial with no explicit TLS client configuration built its context withverify_mode = CERT_NONEandcheck_hostname = False, sonew_host(enable_websocket=True)accepted any certificate from any server.go-libp2p uses a zero
tls.Configand js-libp2p uses the platform TLS stack; bothverify against the system roots and check the hostname.
libp2p's own handshake runs inside the WebSocket, so this was never a stream
confidentiality or integrity hole. What an unverified outer TLS allowed was sitting
on the path unnoticed — and it is not what
wss://implies.How was it fixed?
The dial-time fallback moved into
_default_client_ssl_context()and returns anunmodified
ssl.create_default_context(). The old behaviour stays reachableexplicitly, with a warning logged:
The flag name matches the one
WebSocketTLSConfigalready uses. An explicittls_client_configstill wins, and the proxy dial path gets the same context as thedirect one.
Stacked on #1549, included below as the first commit: while the transport
resolved a name to an IP before dialing, hostname verification could not have
succeeded against a certificate issued for that name — most likely why verification
was off. Please merge #1549 first; I will rebase to drop that commit, leaving a
single-commit diff.
@aojea — your issue and your ordering, so say the word if you would rather take it
and I will close this.
Tests
Three unit tests (default verifies,
insecure_skip_verifyopts out, explicittls_client_configreturned untouched) and two real-TLS tests against a serverholding a fresh self-signed certificate: the default context refuses it with
ssl.SSLCertVerificationError, the opt-out completes the handshake.The TLS tests use plain sockets in a thread deliberately — a trio
SSLStreamserver's
do_handshakeblocks rather than raising when the client refuses thecertificate, while stdlib reports the decision precisely on both sides.
Confirmed the verification tests fail against the old default and pass after
(
2 failed, 2 passed→4 passed). On this branch, rebased onto main @ d9dd9fb:make pralso shows 4 failures intests/core/kad_dht/andtests/examples/test_dht_chat.py. They fail on a clean checkout too and pass whenrerun serially — a pre-existing race under
-n auto, unrelated to this change.Scope
/sni/<name>is still ignored on dial (#1551) and left for that issue. Whiletesting I filed #1554: a
wssdial that fails verification reports a handshaketimeout instead of the certificate error and leaks the socket, because the
handshake runs in the Swarm's background nursery — separate fix, but it makes this
new behaviour harder to diagnose than it should be.
To-Do
docs/examples.websocket.rstgained a "Dialing WSS" sectionbreaking, since this changes behaviour for anyone relying on self-signedwssendpointsCute Animal Picture