diff --git a/README.md b/README.md index 014ec08..b1b28fe 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,9 @@ provides an opportunity for reacting programmatically to any data modification in Safeguard. Events are also supported for access request workflow and for A2A password changes. +> **Note:** The event listener feature requires **Java 9 or later** at runtime +> due to the SignalR client dependency. All other SDK features work on Java 8+. + ## Getting Started ### PKCE Authentication (Recommended) @@ -254,6 +257,39 @@ public class CertificateValidator implements HostnameVerifier { ``` +### TLS Certificate Verification and the `ignoreSsl` Flag + +Every `Safeguard.connect` / `Safeguard.A2A.GetContext` overload accepts an +`ignoreSsl` (`boolean`) parameter. The SDK pins the minimum TLS version to +**TLS 1.2** in all transports (REST and SignalR), regardless of this flag — +weak TLS versions are never negotiated. What `ignoreSsl` controls is +**X.509 certificate chain validation**, not the TLS version and not hostname +verification on its own. + +| Setting | Chain validation | Hostname verification | Recommended use | +|---|---|---|---| +| `ignoreSsl = false` (default) | JVM default truststore | JVM default | **Production.** Trust the appliance via the JVM `cacerts` truststore or a custom truststore. | +| `ignoreSsl = false` + `HostnameVerifier validationCallback` | Caller-supplied callback decides | Caller-supplied | Production with a self-signed or internal-CA appliance whose cert chain is known. | +| `ignoreSsl = true` | **All certificates accepted** | `NoopHostnameVerifier` (any host) | **Development only.** Acceptable for local self-signed test appliances. Never enable in production — it accepts any server certificate, including one presented by an attacker performing a man-in-the-middle. | + +**How to do it right in production:** + +1. Import the appliance's CA certificate into the JVM truststore + (`$JAVA_HOME/lib/security/cacerts`) or into a custom truststore passed via + `-Djavax.net.ssl.trustStore=...`. The default-trust path then validates + the chain automatically and `ignoreSsl=false` is sufficient. +2. If you cannot modify the truststore, pass a `HostnameVerifier` + implementation (the `validationCallback` parameter) that pins the + expected certificate or SPKI fingerprint. Keep `ignoreSsl=false`. +3. Only fall back to `ignoreSsl=true` for ephemeral dev/test environments + where the appliance presents a self-signed certificate you cannot easily + add to the truststore. Document the usage and remove it before shipping. + +The SDK does **not** emit a runtime warning when `ignoreSsl=true` because +the flag is an explicit opt-in — by the time a caller passes `true`, the +trade-off has already been accepted. The responsibility for production +hardening lies with the integrating application. + ### Installation SafeguardJava is available from [Maven Central](https://central.sonatype.com/artifact/com.oneidentity.safeguard/safeguardjava) @@ -290,6 +326,7 @@ available for direct download from [GitHub Releases](https://github.com/OneIdent ### Building SafeguardJava Building SafeguardJava requires Java JDK 8 or greater and Maven 3.0.5 or greater. +The event listener feature requires Java 9+ at runtime (see note above). ```bash mvn clean package diff --git a/pipeline-templates/global-variables.yml b/pipeline-templates/global-variables.yml index 3e2658e..0fc7836 100644 --- a/pipeline-templates/global-variables.yml +++ b/pipeline-templates/global-variables.yml @@ -1,6 +1,6 @@ variables: - name: semanticVersion - value: '8.2.0' + value: '8.2.1' - name: isTagBuild value: ${{ startsWith(variables['Build.SourceBranch'], 'refs/tags/') }} - name: isPrerelease diff --git a/pom.xml b/pom.xml index 1bc28a2..1662f19 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ UTF-8 - 8.2.0-SNAPSHOT + 8.2.1-SNAPSHOT keyname @@ -67,6 +67,13 @@ gson 2.14.0 + + + junit + junit + 4.13.2 + test + @@ -84,6 +91,11 @@ 1.8 + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + org.apache.maven.plugins maven-dependency-plugin diff --git a/src/main/java/com/oneidentity/safeguard/safeguardjava/event/SafeguardEventListener.java b/src/main/java/com/oneidentity/safeguard/safeguardjava/event/SafeguardEventListener.java index 2ca3463..c86e99f 100644 --- a/src/main/java/com/oneidentity/safeguard/safeguardjava/event/SafeguardEventListener.java +++ b/src/main/java/com/oneidentity/safeguard/safeguardjava/event/SafeguardEventListener.java @@ -49,6 +49,15 @@ public class SafeguardEventListener implements ISafeguardEventListener, AutoClos private static final Logger logger = LoggerFactory.getLogger(SafeguardEventListener.class); + /** + * Minimum TLS protocol version pinned at the SDK layer. + * + *

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()); + } +}