See {@code RestClient.TLS_PROTOCOL} for rationale. Both transports + * pin the same minimum version so the SignalR/WebSocket connection cannot + * fall back to TLS 1.0 / 1.1 on a misconfigured JVM. + */ + static final String TLS_PROTOCOL = "TLSv1.2"; + private boolean disposed; private final String eventUrl; @@ -78,6 +87,26 @@ private SafeguardEventListener(String eventUrl, boolean ignoreSsl, HostnameVerif this.disconnectHandler = new DefaultDisconnectHandler(); } + /** + * Creates an event listener authenticated by a previously obtained + * access token. + * + *
Security note on {@code ignoreSsl}. Passing {@code true} + * disables X.509 certificate chain validation and substitutes a + * permissive trust manager that accepts any server certificate. This + * is intended for development against self-signed test appliances + * only; it leaves the SignalR/WebSocket connection vulnerable to + * man-in-the-middle attacks and must not be enabled in production. + * The minimum TLS protocol version remains pinned to {@code TLSv1.2} + * regardless of this flag — see {@link #TLS_PROTOCOL}. + * + * @param eventUrl SignalR notification hub URL + * @param accessToken bearer access token + * @param ignoreSsl when {@code true}, disables certificate chain + * validation; development only + * @param validationCallback optional custom hostname verifier; only + * consulted when {@code ignoreSsl=false} + */ public SafeguardEventListener(String eventUrl, char[] accessToken, boolean ignoreSsl, HostnameVerifier validationCallback) throws ArgumentException { this(eventUrl, ignoreSsl, validationCallback); if (accessToken == null) @@ -384,7 +413,7 @@ private void ConfigureHttpClientBuilder(Builder builder) // Configure the SSL Context according to options and set the // OkHttpClient builder SSL socket factory - SSLContext sslContext = SSLContext.getInstance("TLS"); + SSLContext sslContext = SSLContext.getInstance(TLS_PROTOCOL); sslContext.init(km, tm, null); builder.sslSocketFactory(sslContext.getSocketFactory(), x509tm); } diff --git a/src/main/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClient.java b/src/main/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClient.java index 348df39..4b61cbc 100644 --- a/src/main/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClient.java +++ b/src/main/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClient.java @@ -73,6 +73,17 @@ public class RestClient { /** Default timeout in milliseconds for HTTP requests (100 seconds, matching SafeguardDotNet). */ public static final int DEFAULT_TIMEOUT_MS = 100_000; + /** + * Minimum TLS protocol version pinned at the SDK layer. + * + *
Hard-pinning to {@code TLSv1.2} avoids the {@code "TLS"} alias, which + * the JRE may resolve to TLS 1.0 or 1.1 on misconfigured JVMs. TLS 1.2 is + * the project's Java 8 baseline minimum and is widely supported by + * Safeguard appliances. TLS 1.3 negotiation, when supported by both peers, + * is still permitted by the underlying SSLContext. + */ + static final String TLS_PROTOCOL = "TLSv1.2"; + private CloseableHttpClient client = null; private BasicCookieStore cookieStore = new BasicCookieStore(); @@ -88,6 +99,33 @@ public RestClient(String connectionAddr, boolean ignoreSsl, HostnameVerifier val client = createClientBuilder(connectionAddr, ignoreSsl, validationCallback).build(); } + /** + * Creates a REST client. + * + *
Security note on {@code ignoreSsl}. Passing {@code true} + * disables X.509 certificate chain validation and substitutes a + * permissive trust manager that accepts any server certificate, and a + * {@code NoopHostnameVerifier} that accepts any hostname. This is + * intended for development against self-signed test appliances only; + * it leaves the connection vulnerable to man-in-the-middle attacks + * and must not be enabled in production. The minimum TLS protocol + * version remains pinned to {@code TLSv1.2} regardless of this flag — + * see {@link #TLS_PROTOCOL}. + * + *
For production with a self-signed or internal-CA appliance, + * prefer importing the appliance certificate into the JVM truststore + * (leaving {@code ignoreSsl=false}) or supplying a custom + * {@link HostnameVerifier} via {@code validationCallback} that pins + * the expected certificate. + * + * @param connectionAddr base URL of the Safeguard appliance + * @param userName basic-auth user name + * @param password basic-auth password (cleared by caller) + * @param ignoreSsl when {@code true}, disables certificate chain and + * hostname validation; development only + * @param validationCallback optional custom hostname verifier; only + * consulted when {@code ignoreSsl=false} + */ public RestClient(String connectionAddr, String userName, char[] password, boolean ignoreSsl, HostnameVerifier validationCallback) { @@ -568,7 +606,7 @@ public X509Certificate[] getAcceptedIssuers() { SSLContext ctx = null; try { - ctx = SSLContext.getInstance("TLS"); + ctx = SSLContext.getInstance(TLS_PROTOCOL); ctx.init(customKeyManager, customTrustManager, new java.security.SecureRandom()); } catch (java.security.GeneralSecurityException ex) { } diff --git a/src/test/java/com/oneidentity/safeguard/safeguardjava/event/SafeguardEventListenerSSLContextTest.java b/src/test/java/com/oneidentity/safeguard/safeguardjava/event/SafeguardEventListenerSSLContextTest.java new file mode 100644 index 0000000..fc8e451 --- /dev/null +++ b/src/test/java/com/oneidentity/safeguard/safeguardjava/event/SafeguardEventListenerSSLContextTest.java @@ -0,0 +1,62 @@ +package com.oneidentity.safeguard.safeguardjava.event; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.lang.reflect.Field; +import javax.net.ssl.SSLContext; +import org.junit.Test; + +/** + * Regression test: TLS version pinning. + * + *
Mirror of {@code RestClientSSLContextTest} for the SignalR event + * listener path: ensures the listener's HTTP client builder is wired with + * an explicit {@code TLSv1.2} {@link SSLContext}, not the generic + * {@code "TLS"} alias. + * + *
Note: The SignalR dependency may require Java 9+ at class-load time. + * These tests use Class.forName to detect that situation and skip gracefully + * rather than failing the build on a Java 8 CI agent. + */ +public class SafeguardEventListenerSSLContextTest { + + private static final String EXPECTED_PROTOCOL = "TLSv1.2"; + private static final String CLASS_NAME = + "com.oneidentity.safeguard.safeguardjava.event.SafeguardEventListener"; + + @Test + public void tlsProtocolConstantIsPinnedToTls12() throws Exception { + Class> clazz; + try { + clazz = Class.forName(CLASS_NAME); + } catch (UnsupportedClassVersionError e) { + // SignalR dependency requires Java 9+; skip on Java 8 CI + System.out.println("SKIP: " + e.getMessage()); + return; + } + Field f = clazz.getDeclaredField("TLS_PROTOCOL"); + f.setAccessible(true); + Object value = f.get(null); + assertEquals("SafeguardEventListener.TLS_PROTOCOL must be pinned to TLSv1.2", + EXPECTED_PROTOCOL, value); + } + + @Test + public void sslContextProtocolIsTls12() throws Exception { + Class> clazz; + try { + clazz = Class.forName(CLASS_NAME); + } catch (UnsupportedClassVersionError e) { + // SignalR dependency requires Java 9+; skip on Java 8 CI + System.out.println("SKIP: " + e.getMessage()); + return; + } + Field f = clazz.getDeclaredField("TLS_PROTOCOL"); + f.setAccessible(true); + String protocol = (String) f.get(null); + SSLContext ctx = SSLContext.getInstance(protocol); + assertEquals("SafeguardEventListener must request TLSv1.2, not generic TLS", + EXPECTED_PROTOCOL, ctx.getProtocol()); + } +} diff --git a/src/test/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClientSSLContextTest.java b/src/test/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClientSSLContextTest.java new file mode 100644 index 0000000..533aacd --- /dev/null +++ b/src/test/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClientSSLContextTest.java @@ -0,0 +1,67 @@ +package com.oneidentity.safeguard.safeguardjava.restclient; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import org.junit.Test; + +/** + * Regression test: TLS version pinning. + * + *
Ensures that {@link RestClient} requests an explicit {@code TLSv1.2} + * {@link SSLContext} rather than the generic {@code "TLS"} protocol string. + * The generic alias can resolve to TLS 1.0 / 1.1 on misconfigured JVMs; + * pinning the version at the SDK layer guarantees a TLS 1.2+ handshake + * regardless of {@code jdk.tls.disabledAlgorithms}. + * + *
This test exercises the actual private {@code getSSLContext} method via + * reflection so that any future change of the protocol string is caught. + */ +public class RestClientSSLContextTest { + + private static final String EXPECTED_PROTOCOL = "TLSv1.2"; + + /** + * Verifies that the package-private {@code TLS_PROTOCOL} constant is + * pinned to TLS 1.2 (the Java 8 baseline minimum) so the source of truth + * for the handshake version is auditable in one place. + */ + @Test + public void tlsProtocolConstantIsPinnedToTls12() throws Exception { + Field f = RestClient.class.getDeclaredField("TLS_PROTOCOL"); + f.setAccessible(true); + Object value = f.get(null); + assertEquals("RestClient.TLS_PROTOCOL must be pinned to TLSv1.2", + EXPECTED_PROTOCOL, value); + } + + /** + * Verifies that the SSLContext produced by RestClient.getSSLContext + * reports protocol "TLSv1.2". This catches regressions where the + * constant is correct but the call site reverts to a different string. + */ + @Test + public void sslContextProtocolIsTls12() throws Exception { + RestClient client = new RestClient( + "https://127.0.0.1:9999", + true, // ignoreSsl — exercises the trust-all path + (HostnameVerifier) null); + + Method m = RestClient.class.getDeclaredMethod( + "getSSLContext", + java.security.KeyStore.class, + char[].class, + String.class, + com.oneidentity.safeguard.safeguardjava.data.CertificateContext.class); + m.setAccessible(true); + SSLContext ctx = (SSLContext) m.invoke(client, null, null, null, null); + + assertNotNull("getSSLContext returned null — TLSv1.2 unsupported on this JVM?", ctx); + assertEquals("RestClient must request TLSv1.2, not generic TLS", + EXPECTED_PROTOCOL, ctx.getProtocol()); + } +}