Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pipeline-templates/global-variables.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 13 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<revision>8.2.0-SNAPSHOT</revision>
<revision>8.2.1-SNAPSHOT</revision>
<gpgkeyname>keyname</gpgkeyname>
</properties>

Expand Down Expand Up @@ -67,6 +67,13 @@
<artifactId>gson</artifactId>
<version>2.14.0</version>
</dependency>
<!-- Test scope only -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>


Expand All @@ -84,6 +91,11 @@
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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;
Expand Down Expand Up @@ -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.
*
* <p><b>Security note on {@code ignoreSsl}.</b> 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; <b>development only</b>
* @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)
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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();

Expand All @@ -88,6 +99,33 @@ public RestClient(String connectionAddr, boolean ignoreSsl, HostnameVerifier val
client = createClientBuilder(connectionAddr, ignoreSsl, validationCallback).build();
}

/**
* Creates a REST client.
*
* <p><b>Security note on {@code ignoreSsl}.</b> 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}.
*
* <p>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; <b>development only</b>
* @param validationCallback optional custom hostname verifier; only
* consulted when {@code ignoreSsl=false}
*/
public RestClient(String connectionAddr, String userName, char[] password, boolean ignoreSsl, HostnameVerifier validationCallback) {


Expand Down Expand Up @@ -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) {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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());
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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}.
*
* <p>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());
}
}