Skip to content

WSS transport ignores WebSocketConfig::tls_config (custom CA, client certificate, ALPN) #190

Closed

Description

@bryan-ashby

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_config is never read. The rustls config comes from tokio-tungstenite's rustls-tls-native-roots feature, so it ignores root_certs, use_system_roots, client_cert/client_key and alpn_protocols.
  • Verification off (websocket.rs:531-534): builds a fresh ClientConfig with NoVerifier and .with_no_client_auth(). Only verify_server_cert is 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_auto rejects host names (websocket.rs:173-175). It parses "{host}:{port}" as a SocketAddr, so it fails with Invalid host/port combination for any non-IP URL. The same goes for with_ca_cert_* and with_client_auth_*, which call it. With a URL like wss://broker.example.com/mqtt, these methods return an error before connect is reached.
  • MqttClient over wss:// ignores its stored TLS config (client/inner.rs:275-296). connect_websocket_secure never reads the config from set_tls_config, which connect_tls does use. With set_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 while use_system_roots is 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) and alpn_protocols have 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_config into a shared function on TlsConfig;
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions