Skip to content
Draft
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
23 changes: 22 additions & 1 deletion .agents/skills/a2a-workflow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,26 @@ ISafeguardA2AContext fromKeystore = Safeguard.A2A.getContext(
Windows thumbprint overloads require `SunMSCAPI` to be available. The SDK throws a
`SafeguardForJavaException` on non-Windows platforms or when the provider is missing.

### TLS 1.3, the Cert SNI hostname, and the JSSE post-handshake limitation

A2A is certificate-authenticated, so TLS version matters. SafeguardJava negotiates
**TLS 1.2 only by default**. On Safeguard 9.0 (which enables TLS 1.3), A2A/cert-auth
over TLS 1.3 fails on the **Standard binding** with `60094 Authorization is denied`,
because the server requests the client certificate *post-handshake* (RFC 8446
§4.6.2) and Java's JSSE never presents a certificate in response. This is a Java
**platform** limitation (verified on JDK 11 and JDK 21), not an SDK bug — no
SafeguardJava setting or JVM flag makes post-handshake client auth work. TLS 1.2
keeps working because the certificate request happens differently.

- **Default (TLS 1.2):** A2A works against 9.0 with no extra configuration.
- **TLS 1.3 A2A/cert-auth:** connect to the appliance **Cert SNI hostname**, where
the certificate is requested *in-handshake*. Then opt into 1.3:
`Safeguard.setMaxTlsVersion(TlsVersion.TLSv1_3)` (or
`setMinTlsVersion(TlsVersion.TLSv1_3)` to require it), or the
`safeguard.tls.min/maxVersion` system properties. Configure this **before**
calling `getContext(...)`; it is process-wide.
- Requests stay on **HTTP/1.1** (HTTP/2 disallows the post-handshake request).

## 3. Credential retrieval (programmatic access)

### Enumerate retrievable accounts
Expand Down Expand Up @@ -266,4 +286,5 @@ Troubleshooting checklist:
4. use `getRetrievableAccounts()` to prove what the certificate can actually see
5. switch from a transient listener to a persistent listener if outages matter
6. avoid `ignoreSsl=true` outside lab scenarios
7. clear API keys, passwords, and retrieved secrets from memory when finished
7. on Safeguard 9.0, if cert-auth returns `60094 Authorization is denied` only when TLS 1.3 is negotiated, use the Cert SNI hostname for 1.3 or keep the default TLS 1.2 (JSSE cannot present a client cert post-handshake)
8. clear API keys, passwords, and retrieved secrets from memory when finished
11 changes: 11 additions & 0 deletions .agents/skills/api-patterns/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ If you expect a long-running process, wrap the connection with `Safeguard.Persis
`PersistentSafeguardConnection` checks `getAccessTokenLifetimeRemaining()` before each
`invokeMethod*` call and refreshes expired tokens automatically.

### TLS version (default 1.2, opt-in 1.3)

All connections negotiate **TLS 1.2 only** by default. Password/token auth can safely
use TLS 1.3 on the Standard binding; certificate/A2A auth over TLS 1.3 requires the
appliance **Cert SNI hostname** because JSSE cannot present a client certificate
post-handshake. Opt into 1.3 process-wide **before** calling `connect(...)`:
`Safeguard.setMaxTlsVersion(TlsVersion.TLSv1_3)` /
`Safeguard.setMinTlsVersion(TlsVersion.TLSv1_3)`, or the
`safeguard.tls.min/maxVersion` system properties. See the README "TLS Protocol
Versions" section and the `a2a-workflow` skill for the full rationale.

### Management service calls

`Service.Management` is only valid on a management connection:
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ See `testing-guide` for setup and workflow details.
- expect `ArgumentException`, `SafeguardForJavaException`, and `ObjectDisposedException`
- preserve Java 8 compatibility and standard Java naming
- do not recommend `ignoreSsl=true` for production without a warning
- default TLS is **1.2 only** across all transports; TLS 1.3 is opt-in via `Safeguard.setMin/MaxTlsVersion(TlsVersion)` or `safeguard.tls.min/maxVersion` system properties. JSSE has no client post-handshake auth, so TLS 1.3 cert/A2A auth requires the appliance Cert SNI hostname
- keep repository text files on **LF** line endings, especially on Windows

## CI/CD
Expand Down
74 changes: 69 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,11 +301,12 @@ 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.
`ignoreSsl` (`boolean`) parameter. By default the SDK negotiates **TLS 1.2**
in all transports (REST and SignalR); weaker versions (TLS 1.0/1.1) are never
enabled, and TLS 1.3 is available as an opt-in (see
[TLS Protocol Versions](#tls-protocol-versions-tls-13-support) below). 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 |
|---|---|---|---|
Expand All @@ -331,6 +332,69 @@ 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.

### TLS Protocol Versions (TLS 1.3 Support)

Safeguard 9.0 (Windows 11 base OS) enables **TLS 1.3**. By default SafeguardJava
negotiates **TLS 1.2 only** across every transport (REST and SignalR). This is a
deliberate default, not just legacy behavior:

> **⚠️ Java limitation — no TLS 1.3 post-handshake client authentication.**
> Java's TLS engine (JSSE) **does not** present a client certificate in response
> to a TLS 1.3 post-handshake `CertificateRequest` (RFC 8446 §4.6.2). This has
> been verified on JDK 11 and JDK 21 and is a limitation of the Java platform
> itself, **not** of this SDK — there is no SafeguardJava setting or JVM flag that
> makes it work. As a direct consequence, **certificate-based and A2A
> authentication cannot use TLS 1.3 on the appliance Standard binding**; they must
> either run over TLS 1.2 (the default) or connect to the appliance **Cert SNI
> hostname** (see below). Password/token authentication is unaffected.

- **JSSE cannot present a client certificate post-handshake.** On TLS 1.3 with
the appliance **Standard binding**, the server requests the client certificate
*after* the handshake (post-handshake authentication, RFC 8446 §4.6.2). Java's
JSSE never answers that request, so certificate/A2A authentication fails on a
TLS 1.3 connection (`60094 Authorization is denied`) while succeeding on
TLS 1.2. Keeping the default at TLS 1.2 keeps cert-auth working out of the box.
- **Password/token authentication** carries no client certificate and can use
TLS 1.3 on the Standard binding without issue.
- **The only route to TLS 1.3 certificate/A2A auth** with this SDK is to connect
to the appliance **Cert SNI hostname**, where the certificate is requested
*in-handshake* (no post-handshake step). Password auth can also use it.
- Requests use **HTTP/1.1** (HTTP/2 disallows the post-handshake
`CertificateRequest`); this is unchanged.

You can raise (or pin) the allowed versions with an opt-in minimum/maximum bound.
The setting is process-wide and read when each connection or listener is created,
so configure it **before** calling `Safeguard.connect(...)`:

```java
import com.oneidentity.safeguard.safeguardjava.Safeguard;
import com.oneidentity.safeguard.safeguardjava.TlsVersion;

// Allow TLS 1.3 in addition to 1.2 (e.g. password/token auth on the Standard
// binding, or cert-auth against the Cert SNI hostname):
Safeguard.setMaxTlsVersion(TlsVersion.TLSv1_3);

// Require TLS 1.3 only:
Safeguard.setMinTlsVersion(TlsVersion.TLSv1_3);

// Restore the default (TLS 1.2 only):
Safeguard.setMaxTlsVersion(null);
```

For an interim rollout with no code change, the same bounds can be supplied as
JVM system properties (programmatic settings take precedence):

```
-Dsafeguard.tls.minVersion=TLSv1.2 -Dsafeguard.tls.maxVersion=TLSv1.3
```

| Configuration | Enabled versions |
|---|---|
| Default (both unset) | `TLSv1.2` |
| `setMaxTlsVersion(TLSv1_3)` | `TLSv1.2`, `TLSv1.3` |
| `setMinTlsVersion(TLSv1_3)` | `TLSv1.3` |
| `setMin/MaxTlsVersion(TLSv1_2)` | `TLSv1.2` |

### Installation

SafeguardJava is available from [Maven Central](https://central.sonatype.com/artifact/com.oneidentity.safeguard/safeguardjava)
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.4'
value: '8.4.0'
- name: isTagBuild
value: ${{ startsWith(variables['Build.SourceBranch'], 'refs/tags/') }}
- name: isPrerelease
Expand Down
2 changes: 1 addition & 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.3.1-SNAPSHOT</revision>
<revision>8.4.0-SNAPSHOT</revision>
<gpgkeyname>keyname</gpgkeyname>
</properties>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,67 @@ private static SafeguardConnection getConnection(IAuthenticationMechanism authen
return new SafeguardConnection(authenticationMechanism);
}

/**
* Set the minimum TLS protocol version SafeguardJava is allowed to
* negotiate on all subsequently created connections and event listeners.
*
* <p>This is an opt-in, process-wide setting. When left unset (and no
* {@code safeguard.tls.minVersion} system property is present), the SDK
* negotiates <b>TLS 1.2 only</b> &mdash; the historical default that keeps
* certificate/A2A authentication working on the appliance Standard binding
* (JSSE cannot present a client certificate post-handshake, so TLS 1.3
* cert-auth is only possible via the appliance Cert SNI hostname).
*
* <p>Password/token authentication can safely negotiate TLS 1.3 on the
* Standard binding. To require TLS 1.3, call
* {@code setMinTlsVersion(TlsVersion.TLSv1_3)}.
*
* @param minTlsVersion Minimum TLS version, or {@code null} to clear.
* @throws IllegalArgumentException If it is higher than a configured maximum.
*/
public static void setMinTlsVersion(TlsVersion minTlsVersion) {
TlsConfiguration.setMinTlsVersion(minTlsVersion);
}

/**
* Get the programmatically configured minimum TLS protocol version.
*
* @return Configured minimum, or {@code null} when unset (does not reflect
* the {@code safeguard.tls.minVersion} system-property fallback).
*/
public static TlsVersion getMinTlsVersion() {
return TlsConfiguration.getMinTlsVersion();
}

/**
* Set the maximum TLS protocol version SafeguardJava is allowed to
* negotiate on all subsequently created connections and event listeners.
*
* <p>This is an opt-in, process-wide setting. When left unset (and no
* {@code safeguard.tls.maxVersion} system property is present), the SDK
* negotiates <b>TLS 1.2 only</b>. Raise it to
* {@code TlsVersion.TLSv1_3} to allow TLS 1.3. Note that certificate/A2A
* authentication over TLS 1.3 requires connecting to the appliance Cert SNI
* hostname, because JSSE cannot present a client certificate in response to
* a TLS 1.3 post-handshake {@code CertificateRequest}.
*
* @param maxTlsVersion Maximum TLS version, or {@code null} to clear.
* @throws IllegalArgumentException If it is lower than a configured minimum.
*/
public static void setMaxTlsVersion(TlsVersion maxTlsVersion) {
TlsConfiguration.setMaxTlsVersion(maxTlsVersion);
}

/**
* Get the programmatically configured maximum TLS protocol version.
*
* @return Configured maximum, or {@code null} when unset (does not reflect
* the {@code safeguard.tls.maxVersion} system-property fallback).
*/
public static TlsVersion getMaxTlsVersion() {
return TlsConfiguration.getMaxTlsVersion();
}

/**
* Connect to Safeguard API using an API access token.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package com.oneidentity.safeguard.safeguardjava;

import java.util.ArrayList;
import java.util.List;

/**
* Central, process-wide configuration for the TLS protocol versions that
* SafeguardJava transports (REST clients and the SignalR event listener) are
* allowed to negotiate.
*
* <p><b>Default behavior.</b> When neither a minimum nor a maximum version is
* configured, SafeguardJava negotiates <b>TLS 1.2 only</b>. This preserves the
* SDK's historical behavior and, critically, keeps certificate/A2A
* authentication working on the appliance Standard binding: JSSE cannot present
* a client certificate in response to a TLS 1.3 post-handshake
* {@code CertificateRequest} (RFC 8446 &sect;4.6.2), so cert-auth on the
* Standard binding only succeeds at TLS 1.2.
*
* <p><b>Opting into TLS 1.3.</b> Callers may raise the maximum (and/or minimum)
* version via {@link #setMaxTlsVersion(TlsVersion)} /
* {@link #setMinTlsVersion(TlsVersion)} (surfaced publicly as
* {@link Safeguard#setMaxTlsVersion(TlsVersion)} /
* {@link Safeguard#setMinTlsVersion(TlsVersion)}), or via the
* {@value #MAX_TLS_VERSION_PROPERTY} / {@value #MIN_TLS_VERSION_PROPERTY} system
* properties for an interim, no-code-change rollout. Programmatic settings take
* precedence over system properties.
*
* <p>Password/token authentication (which carries no client certificate) can
* use TLS 1.3 on the Standard binding without issue. Certificate/A2A
* authentication over TLS 1.3 additionally requires connecting to the appliance
* Cert SNI hostname, where the certificate is requested in-handshake.
*
* <p>This class is thread-safe; the configured versions are held in
* {@code volatile} fields and read at connection-creation time.
*/
public final class TlsConfiguration {

/** System property that supplies the minimum TLS version when no value has
* been set programmatically. Accepts {@code TLSv1.2}, {@code TLSv1_2},
* {@code 1.2}, etc. (see {@link TlsVersion#fromString(String)}). */
public static final String MIN_TLS_VERSION_PROPERTY = "safeguard.tls.minVersion";

/** System property that supplies the maximum TLS version when no value has
* been set programmatically. Accepts {@code TLSv1.3}, {@code TLSv1_3},
* {@code 1.3}, etc. (see {@link TlsVersion#fromString(String)}). */
public static final String MAX_TLS_VERSION_PROPERTY = "safeguard.tls.maxVersion";

private static volatile TlsVersion minTlsVersion = null;
private static volatile TlsVersion maxTlsVersion = null;

private TlsConfiguration() {
}

/**
* Sets the minimum TLS protocol version the SDK is allowed to negotiate, or
* {@code null} to defer to the {@value #MIN_TLS_VERSION_PROPERTY} system
* property (and ultimately the default).
*
* @param version the minimum version, or {@code null} to clear.
* @throws IllegalArgumentException if a maximum is already configured and
* {@code version} is higher than it.
*/
public static void setMinTlsVersion(TlsVersion version) {
if (version != null && maxTlsVersion != null && version.ordinal() > maxTlsVersion.ordinal()) {
throw new IllegalArgumentException(String.format(
"Minimum TLS version %s cannot be higher than the configured maximum %s",
version.getProtocolName(), maxTlsVersion.getProtocolName()));
}
minTlsVersion = version;
}

/**
* @return the programmatically configured minimum TLS version, or
* {@code null} if unset. Does not reflect the system-property
* fallback.
*/
public static TlsVersion getMinTlsVersion() {
return minTlsVersion;
}

/**
* Sets the maximum TLS protocol version the SDK is allowed to negotiate, or
* {@code null} to defer to the {@value #MAX_TLS_VERSION_PROPERTY} system
* property (and ultimately the default).
*
* @param version the maximum version, or {@code null} to clear.
* @throws IllegalArgumentException if a minimum is already configured and
* {@code version} is lower than it.
*/
public static void setMaxTlsVersion(TlsVersion version) {
if (version != null && minTlsVersion != null && version.ordinal() < minTlsVersion.ordinal()) {
throw new IllegalArgumentException(String.format(
"Maximum TLS version %s cannot be lower than the configured minimum %s",
version.getProtocolName(), minTlsVersion.getProtocolName()));
}
maxTlsVersion = version;
}

/**
* @return the programmatically configured maximum TLS version, or
* {@code null} if unset. Does not reflect the system-property
* fallback.
*/
public static TlsVersion getMaxTlsVersion() {
return maxTlsVersion;
}

private static TlsVersion effectiveMin() {
return (minTlsVersion != null) ? minTlsVersion
: TlsVersion.fromString(System.getProperty(MIN_TLS_VERSION_PROPERTY));
}

private static TlsVersion effectiveMax() {
return (maxTlsVersion != null) ? maxTlsVersion
: TlsVersion.fromString(System.getProperty(MAX_TLS_VERSION_PROPERTY));
}

/**
* Resolves the ordered set of JSSE protocol names that transports should
* enable, honoring the configured (or system-property) minimum and maximum
* bounds.
*
* <p>When neither bound is set, this returns {@code ["TLSv1.2"]} (the legacy
* default). When at least one bound is set, the range spans
* {@code [min or TLSv1.2 .. max or TLSv1.3]}.
*
* @return a non-empty array of JSSE protocol names, lowest version first.
* @throws IllegalStateException if the resolved minimum is higher than the
* resolved maximum (only reachable via inconsistent system
* properties).
*/
public static String[] resolveEnabledProtocolNames() {
TlsVersion min = effectiveMin();
TlsVersion max = effectiveMax();

if (min == null && max == null) {
return new String[] { TlsVersion.TLSv1_2.getProtocolName() };
}

TlsVersion lo = (min != null) ? min : TlsVersion.TLSv1_2;
TlsVersion hi = (max != null) ? max : TlsVersion.TLSv1_3;
if (lo.ordinal() > hi.ordinal()) {
throw new IllegalStateException(String.format(
"Invalid TLS version range: minimum %s is higher than maximum %s",
lo.getProtocolName(), hi.getProtocolName()));
}

List<String> names = new ArrayList<>();
for (TlsVersion v : TlsVersion.values()) {
if (v.ordinal() >= lo.ordinal() && v.ordinal() <= hi.ordinal()) {
names.add(v.getProtocolName());
}
}
return names.toArray(new String[0]);
}
}
Loading