Conversation
…Socket connection The Grizzly client wraps its wss:// connections in an SSLEngine, which validates the certificate chain but never checks the certificate against the host, so anyone holding a certificate the client trusts could sit in the middle of the connection. Have JSSE do "HTTPS" endpoint identification during the handshake, against the host of the remote URI also when the socket goes through a proxy. ConnectionManagerConfig gets a hostnameVerification switch that defaults to the system property the legacy client honours, org.identityconnectors.framework.remote.hostnameVerification. A failed handshake is logged at WARN and surfaces as the cause of the "before WebSocket is established" failure instead of a bare connection close. The test certificates expired in January 2025 and are regenerated; the server certificate now names localhost, 127.0.0.1 and ::1 as the client verifies them, and serverKeystore-cn-only.jks keeps one without subjectAltName for the negative test.
maximthomas
left a comment
There was a problem hiding this comment.
praise: The host check sits exactly where the engine is built, and a failed check now says why.
SwitchingSSLFilter.createClientSSLEngine(ConnectionManager.java:918-932) keys the engine on the remote URI host, not the socket peer, and turns on "HTTPS" endpoint identification, so a plainX509TrustManagergets the check through JSSE's wrapper too.notifyHandshakeFailedrecords the cause, andClientRemoteConnectorInfoManager.java:187-195chains theSSLHandshakeExceptioninstead of the bare "Connection is closed before WebSocket is established".- The expired test certificates are regenerated with SANs,
serverKeystore-cn-only.jksgives the negative row, andREADME.txtkeeps thekeytoolcommands.
suggestion (non-blocking): No test pins that the engine is created for the remote-URI host rather than the socket peer.
OpenICF-java-framework/connector-framework-server/src/main/java/org/forgerock/openicf/framework/client/ConnectionManager.java:918-926, OpenICF-java-framework/connector-server-grizzly/src/test/java/org/forgerock/openicf/framework/server/ClientHostnameVerificationTest.java:172
Every active wss test connects to wss://127.0.0.1:<port> with no proxy (AsyncConnectorInfoManagerTestBase.buildRemoteWSFrameworkConnectionInfo), so the socket peer and the URI host are the same string, and a mutant that always calls super.createClientSSLEngine(sslCtx, sslEngineConfigurator) keeps all three rows green (read, not run). The only proxy fixture, buildRemoteProxyFrameworkConnectionInfo, is called from *.java.disable files over plain ws. The IPv6 bracket strip needs no row: JSSE strips the brackets itself.
Pin: a proxied row: a CONNECT proxy on 127.0.0.1 and the URI wss://localhost:<cnOnlyPort> against serverKeystore-cn-only.jks (CN=localhost, no SAN). The code under review checks localhost, JSSE falls back to the CN and accepts. The always-super mutant checks the proxy's 127.0.0.1, which needs an IP SAN, and rejects.
suggestion (non-blocking): No test exercises the system-property default of hostnameVerification.
OpenICF-java-framework/connector-framework-server/src/main/java/org/forgerock/openicf/framework/client/ConnectionManagerConfig.java:85-86, OpenICF-java-framework/connector-server-grizzly/src/test/java/org/forgerock/openicf/framework/server/ClientHostnameVerificationTest.java:120
The disabled row switches verification off through setHostnameVerification(false), and nothing sets org.identityconnectors.framework.remote.hostnameVerification. So replacing the initialiser with the constant true keeps all three rows green (read, not run). Only dropping the ! goes red. The property is the escape hatch the Compatibility section gives operators.
package org.forgerock.openicf.framework.client;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import org.testng.annotations.Test;
public class ConnectionManagerConfigTest {
@Test
public void hostnameVerificationDefaultsFromSystemProperty() {
final String key = ConnectionManagerConfig.HOSTNAME_VERIFICATION_PROPERTY;
final String saved = System.getProperty(key);
try {
System.clearProperty(key);
assertTrue(new ConnectionManagerConfig().isHostnameVerification());
System.setProperty(key, "false");
assertFalse(new ConnectionManagerConfig().isHostnameVerification());
System.setProperty(key, "flase");
assertTrue(new ConnectionManagerConfig().isHostnameVerification());
} finally {
if (saved == null) {
System.clearProperty(key);
} else {
System.setProperty(key, saved);
}
}
}
}Pin: the "false" row kills the constant-true mutant; the "flase" row holds the claim that a typo can not weaken the check.
suggestion (non-blocking): The WebSocket client reads org.identityconnectors.framework.remote.hostnameVerification once per ConnectorFramework, while the #122 legacy client reads it on every connect.
OpenICF-java-framework/connector-framework-server/src/main/java/org/forgerock/openicf/framework/client/ConnectionManagerConfig.java:85-86, OpenICF-java-framework/connector-framework-server/src/main/java/org/forgerock/openicf/framework/client/ConnectionManager.java:315-317, OpenICF-java-framework/connector-framework-server/src/main/java/org/forgerock/openicf/framework/ConnectorFramework.java:495
A field initialiser reads the property. ConnectorFramework builds its config eagerly as a field, and createFilterChain copies the value into a final field of SwitchingSSLFilter. A property set after the framework exists turns verification off for the legacy client only. OpenIDM's servlet-filter systemProperties is one such route: it calls System.setProperty/clearProperty at component activation. Clearing the property later re-enables the legacy check, but the WebSocket client stays unverified until the framework is rebuilt. The usual -D / system.properties route is read in time, so this only bites late or runtime changes.
// ConnectionManagerConfig
private boolean hostnameVerificationSet;
public boolean isHostnameVerification() {
return hostnameVerificationSet
? hostnameVerification
: !"false".equalsIgnoreCase(System.getProperty(HOSTNAME_VERIFICATION_PROPERTY));
}
public void setHostnameVerification(boolean hostnameVerification) {
this.hostnameVerification = hostnameVerification;
this.hostnameVerificationSet = true;
}Or: keep the snapshot and say in the HOSTNAME_VERIFICATION_PROPERTY Javadoc that the WebSocket client reads it when the ConnectorFramework is created. With the change above, SwitchingSSLFilter should hold the ConnectionManagerConfig and call isHostnameVerification() inside createClientSSLEngine.
nitpick (non-blocking): The Javadoc says only the literal false disables verification, but the code accepts false in any letter case.
OpenICF-java-framework/connector-framework-server/src/main/java/org/forgerock/openicf/framework/client/ConnectionManagerConfig.java:36-41, :85-86
"false".equalsIgnoreCase(...) also accepts FALSE and False. That has no security effect, and #122 uses the same comparison, so the text should change, in the PR description as well.
/**
* System property that turns off TLS hostname verification for every
* client unless a {@link ConnectionManagerConfig} says otherwise. Shared
* with the legacy connector server client. Only {@code false}, in any
* letter case, disables verification, so a typo in the value can not
* weaken it.
*/- ConnectionManagerConfig follows org.identityconnectors.framework.remote.hostnameVerification on every connection unless set explicitly, like the legacy client in OpenIdentityPlatform#122, so a later change of the property reaches the WebSocket client too. Javadoc: `false` in any letter case. - SwitchingSSLFilter: reads bypass Grizzly's SSL transport wrapper while the connection is plain. The wrapper gave the connection a server-mode SSLEngine on its first read, which through a proxy is the answer to CONNECT, so every wss connection through a proxy hung. - ConnectionManagerConfigTest pins the property default, its letter case and live changes. - ClientHostnameVerificationTest connects through a CONNECT proxy to wss://localhost against the CN-only certificate: accepted for the URI host, rejected if the engine were keyed on the proxy's 127.0.0.1.
|
All four taken. Remote-URI host vs. socket peer — Writing the row turned up a separate bug: wss through a proxy never got past the tunnel. Grizzly's System-property default — Snapshot vs. per-connect read — Javadoc — now says connector-framework-server 32, connector-server-grizzly 38, connector-server-jetty 34 tests, all green. |
Companion to #122, which fixes the same class of problem in the legacy connector server client (CodeQL alert #24). This one covers the Grizzly WebSocket client in
connector-framework-server, which CodeQL does not model because theSSLEngineis created inside Grizzly.Problem
ConnectionManagerwrapswss://connections in a GrizzlySSLFilterwhoseSSLEnginevalidates the certificate chain but never checks the certificate against the host. Anyone holding a certificate the client trusts could sit in the middle of the connection to the connector server; with the usual setup (server certificate imported into the JVM truststore next to the public CAs) any publicly trusted certificate was enough.Change
ConnectionManager.SwitchingSSLFilter:createClientSSLEnginecreates the engine for the host of the remote URI (not for the proxy the socket may be connected to; IPv6 brackets stripped) and enables JSSE "HTTPS" endpoint identification viaSSLParameters, so the certificate is verified against that host during the handshake, before any application data is sent. Applies to custom trust managers too, plainX509TrustManagerincluded.createOptimizedTransportFilter: reads bypass Grizzly'sSSLTransportFilterWrapperwhile the connection is still plain. The wrapper gave a connection a server-modeSSLEngineon its first read; through a proxy that read is the plain answer to CONNECT, so the client handshake waited for a ClientHello nobody sends and everywss://connection through a proxy timed out (on master too).notifyHandshakeFailedlogsTLS handshake with connector server host:port failed: <cause>at WARN and records the cause on the connection.ClientRemoteConnectorInfoManager: the close listener reports that cause —TLS handshake failed before WebSocket is established: …with theSSLHandshakeExceptionchained — instead of the bareConnection is closed before WebSocket is established.ConnectionManagerConfig:hostnameVerificationgetter/setter, defaulting to the same system property the legacy client honours in Verify the connector server certificate against the host over the legacy SSL connection #122,org.identityconnectors.framework.remote.hostnameVerification. As in Verify the connector server certificate against the host over the legacy SSL connection #122 the property is read on every connection unless the config sets the value explicitly, so changing it takes effect without rebuilding the framework. Onlyfalse, in any letter case, disables the check.Tests
ClientHostnameVerificationTest(connector-server-grizzly): a server with a CN-only certificate is rejected when connecting towss://127.0.0.1(cause chain carriesSSLHandshakeException: No subject alternative names present), a certificate with matching subjectAltName is accepted and the test connector is looked up over it, the mismatching certificate is accepted whenhostnameVerification=false, and through a CONNECT proxy on127.0.0.1awss://localhostconnection to the CN-only certificate is accepted, i.e. checked against the host of the URI, not the proxy (an engine keyed on the proxy's127.0.0.1fails withNo subject alternative names present).ConnectionManagerConfigTest(connector-framework-server) pins the system-property default, its letter case, later changes of the property and the precedence of an explicit setting.The module's test certificates had expired in January 2025 (they only kept working because a self-signed end-entity certificate that is itself a trust anchor skips validity checks). They are regenerated with 30 years of validity;
serverKeystore.jksnow nameslocalhost,127.0.0.1and::1, which the client verifies,serverKeystore-cn-only.jkskeeps a certificate without subjectAltName for the negative test, andREADME.txthas thekeytoolcommands.AsyncRemoteSecureConnectorInfoManagerTestruns unchanged against the new certificate with verification on.Local runs: connector-framework-server 32 tests, connector-server-grizzly 38 tests, connector-server-jetty 34 tests, all green.
Compatibility
Deployments whose connector server certificate does not name the host used in the
wss://URI fail the handshake now, with the reason in the log and in the exception. The fix is a certificate that names the host; the config switch / system property is the temporary escape hatch. Worth a release-notes entry together with #122.Noticed, not changed
WebSocketConnectionGroup.shutdown()throwsNullPointerExceptionfromremote.getPromise().cancel(true)when a client is closed while a request is registered but not yet sent (RemoteRequest.promiseis null until the message is sent). Hit while writing the test; separate issue.connector-framework-internaland can be shared once Verify the connector server certificate against the host over the legacy SSL connection #122 is merged.