Repository navigation
WSS transport ignores WebSocketConfig::tls_config (custom CA, client certificate, ALPN) #190
Description
Summary
WebSocketConfig lets a caller configure TLS for wss:// connections: with_ca_cert_from_file/_bytes, with_client_auth_from_files/_bytes, with_tls_config, with_tls_auto. With certificate verification on (the default), none of it is used. WebSocketTransport::connect hands the request to tokio_tungstenite::connect_async, which builds its own rustls config from the OS trust store. With verification off, the client certificate and ALPN are dropped. In both cases the settings are accepted and then silently ignored.
Found while working on #165. The results below are from running against #189's branch, whose TLS path is identical to main at f65445f.
Where
crates/mqtt5/src/transport/websocket.rs, impl Transport for WebSocketTransport::connect:
- Verification on (
websocket.rs:551):tokio_tungstenite::connect_async(request).tls_configis never read. The rustls config comes from tokio-tungstenite'srustls-tls-native-rootsfeature, so it ignoresroot_certs,use_system_roots,client_cert/client_keyandalpn_protocols. - Verification off (
websocket.rs:531-534): builds a freshClientConfigwithNoVerifierand.with_no_client_auth(). Onlyverify_server_certis read. The client certificate and ALPN are dropped.
The TCP+TLS transport already turns a TlsConfig into a rustls config correctly (TlsTransport::build_tls_config, tls.rs:352). The WSS path just doesn't use it.
Two related gaps make the settings hard to reach at all:
with_tls_autorejects host names (websocket.rs:173-175). It parses"{host}:{port}"as aSocketAddr, so it fails withInvalid host/port combinationfor any non-IP URL. The same goes forwith_ca_cert_*andwith_client_auth_*, which call it. With a URL likewss://broker.example.com/mqtt, these methods return an error beforeconnectis reached.MqttClientoverwss://ignores its stored TLS config (client/inner.rs:275-296).connect_websocket_securenever reads the config fromset_tls_config, whichconnect_tlsdoes use. Withset_insecure_tls(true), it builds a bare verification-off config instead.
Impact
- A configured CA is never trusted. A server whose certificate chains only to a CA set with
with_ca_cert_*is rejected (UnknownIssuer). (A configured CA is added to the system roots whileuse_system_rootsis on, its default, so the intended behaviour is "also trust this CA", not "trust only this CA".) - mTLS over WSS never presents the client certificate, so a broker that requires client authentication rejects the handshake.
use_system_roots(false)andalpn_protocolshave no effect.
The existing tests check only the config struct (tls_config().is_some(), client_cert.is_some()), so they pass whether or not anything reaches the handshake.
Reproduction
Run a WSS server with a certificate issued by test_certs/ca.pem, e.g. test_certs/server.pem:
let config = WebSocketConfig::new("wss://127.0.0.1:8443/mqtt")?
.with_ca_cert_from_file("test_certs/ca.pem")?;
WebSocketTransport::new(config).connect().await?;(This uses an IP-address URL because of the with_tls_auto limit above: with wss://localhost:8443, with_ca_cert_from_file itself fails with Invalid host/port combination.)
Expected: connects, because the CA was supplied. Actual: IO error: invalid peer certificate: UnknownIssuer, because the supplied CA is never added.
Each case, run against a loopback WSS server using the certificates from scripts/generate_test_certs.sh:
| Configuration | Result before the fix |
|---|---|
with_ca_cert_from_file(ca.pem), server cert from that CA |
invalid peer certificate: UnknownIssuer |
plus with_client_auth_from_files(client.pem, client.key), server requires client auth |
invalid peer certificate: UnknownIssuer. The CA is ignored, so the handshake never gets as far as the client certificate |
TlsConfig with verify_server_cert(false) and a client certificate, server requires client auth |
server rejects with received fatal alert: CertificateRequired. The certificate was never sent |
TlsConfig with alpn_protocols(["mqtt-test"]) and the CA |
invalid peer certificate: UnknownIssuer. This fails before ALPN matters; from the code, neither branch ever sets alpn_protocols |
Suggested fix
Build the WSS rustls config from tls_config through the same code path as TlsTransport, and pass it with connect_async_tls_with_config(request, None, false, Some(Connector::Rustls(config))) in both branches. That means:
- moving the body of
TlsTransport::build_tls_configinto a shared function onTlsConfig; - dropping the WSS module's duplicate
NoVerifier.
Roots: WSS uses the OS store via rustls-tls-native-roots, while TlsConfig's use_system_roots means the bundled webpki_roots on the TCP+TLS transport. Following review on #191, the fix keeps the OS store for WSS in both cases: with no tls_config, and for use_system_roots when one is set.
The fix is in #191, which builds on #189. It also covers the two related gaps. Its tests include one for each row above, which fails as shown before the fix and passes after it, plus two guard tests that pass both before and after, showing a certificate from another CA is still rejected.
Activity
accept host name URLs in with_tls_auto and use MqttClient's stored TL…
use platform roots for wss with a TLS config, keep stored ALPN off wss (