Skip to content

Quiet the one-way TLS certificate probe on MQTT connect - #38

Open
ShvaykaD wants to merge 6 commits into
rpc-create-idempotencyfrom
mqtt-one-way-tls-cert-probe
Open

Quiet the one-way TLS certificate probe on MQTT connect#38
ShvaykaD wants to merge 6 commits into
rpc-create-idempotencyfrom
mqtt-one-way-tls-cert-probe

Conversation

@ShvaykaD

@ShvaykaD ShvaykaD commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Found while testing the MQTT gateway API in one-way TLS mode.

Problem

MqttSslHandlerProvider sets needClientAuth(false) with wantClientAuth(true), so a client that declines the optional certificate still connects. JSSE reports the absent cert by throwing SSLPeerUnverifiedException, which getX509Certificate() logged at warn — on an outcome the transport is configured to accept, and with no session or address to correlate it. It is vestigial: 22e5771120 carried the warn over verbatim during the JDK 11 migration.

It fired twice per failed connect, because onValidateDeviceResponse re-probed the session.

Approach

Log at debug, with the [address][sessionId] prefix used elsewhere in the class.

Drop the second probe. Its guard — sslHandler == null || getX509Certificate() == null — doesn't want the certificate; it wants to know whether this was a basic-credentials connect, which processConnect already decided. A clientCertPresented flag records that branch. It is set iff sslHandler != null && cert != null, whose negation is exactly the replaced condition, so return codes are unchanged. volatile because processConnect runs on the Netty event loop and onValidateDeviceResponse on transportCallbackExecutor.

No config, wire, or device-facing change.

Tests

Two tests in MqttTransportHandlerTest, asserting through the CONNACK so the flag stays private. The one-way case asserts BAD_USERNAME_OR_PASSWORD and pins getPeerCertificates() to a single invocation; the two-way case asserts NOT_AUTHORIZED_5 survives, which is what proves the flag is set. Both need MQTT 5 — ReturnCodeResolver collapses those two codes into one for older clients, which would make the assertions vacuous.

Mutation-checked: dropping the flag assignment fails the two-way test, restoring the old condition fails the one-way times(1). The refactor is behaviour-preserving, so the return-code assertions alone would pass against the old code too.

MqttSslHandlerProvider sets needClientAuth(false) with wantClientAuth(true),
so a client that declines the optional certificate is a supported one-way TLS
connect, not an anomaly. getPeerCertificates() reports that by throwing
SSLPeerUnverifiedException, which getX509Certificate() logged at warn on every
such CONNECT.

The warn is vestigial: 22e5771 carried it over verbatim while migrating from
getPeerCertificateChain() to getPeerCertificates(), and it was never a
deliberate choice for the one-way TLS case.

Log at debug and add the [address][sessionId] prefix used elsewhere in the
class -- the old message carried no correlation info at all.
onValidateDeviceResponse re-probed the TLS session only to decide whether to
refine the CONNACK return code, so a failed one-way TLS connect threw and
logged SSLPeerUnverifiedException twice.

The probe is redundant: the guard asks whether this was a basic-credentials
connect, which processConnect already decided. Record that branch in
clientCertPresented and read the flag instead. The flag is set iff
sslHandler != null && cert != null, whose negation is exactly the replaced
condition, so the return codes are unchanged.

volatile because processConnect runs on the Netty event loop while
onValidateDeviceResponse runs on transportCallbackExecutor (see
DefaultTransportService.doProcess). There is likely an incidental
happens-before edge through the request template and the executor submission,
but it runs through three layers of async machinery and should not be
load-bearing for one read per connect.

MqttTransportHandlerTest covers both branches through the CONNACK the channel
receives, so the flag stays private. MQTT 5 is required: ReturnCodeResolver
collapses BAD_USERNAME_OR_PASSWORD and NOT_AUTHORIZED_5 into one
NOT_AUTHORIZED code for older clients, which would make the two cases
indistinguishable and the assertions vacuous. The one-way TLS test also pins
getPeerCertificates() to a single invocation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant