diff --git a/.agents/skills/a2a-workflow/SKILL.md b/.agents/skills/a2a-workflow/SKILL.md index 94b124b..12c6dfb 100644 --- a/.agents/skills/a2a-workflow/SKILL.md +++ b/.agents/skills/a2a-workflow/SKILL.md @@ -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 @@ -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 diff --git a/.agents/skills/api-patterns/SKILL.md b/.agents/skills/api-patterns/SKILL.md index be2feb9..aecde42 100644 --- a/.agents/skills/api-patterns/SKILL.md +++ b/.agents/skills/api-patterns/SKILL.md @@ -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: diff --git a/AGENTS.md b/AGENTS.md index 4401dd7..c2fefa2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/README.md b/README.md index dd0377d..a2481e0 100644 --- a/README.md +++ b/README.md @@ -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 | |---|---|---|---| @@ -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) diff --git a/pipeline-templates/global-variables.yml b/pipeline-templates/global-variables.yml index 8f75f1f..ca62c4c 100644 --- a/pipeline-templates/global-variables.yml +++ b/pipeline-templates/global-variables.yml @@ -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 diff --git a/pom.xml b/pom.xml index 652b01e..8171df1 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ UTF-8 - 8.3.1-SNAPSHOT + 8.4.0-SNAPSHOT keyname diff --git a/src/main/java/com/oneidentity/safeguard/safeguardjava/Safeguard.java b/src/main/java/com/oneidentity/safeguard/safeguardjava/Safeguard.java index bccb94c..b216000 100644 --- a/src/main/java/com/oneidentity/safeguard/safeguardjava/Safeguard.java +++ b/src/main/java/com/oneidentity/safeguard/safeguardjava/Safeguard.java @@ -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. + * + *

This is an opt-in, process-wide setting. When left unset (and no + * {@code safeguard.tls.minVersion} system property is present), the SDK + * negotiates TLS 1.2 only — 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). + * + *

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. + * + *

This is an opt-in, process-wide setting. When left unset (and no + * {@code safeguard.tls.maxVersion} system property is present), the SDK + * negotiates TLS 1.2 only. 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. * diff --git a/src/main/java/com/oneidentity/safeguard/safeguardjava/TlsConfiguration.java b/src/main/java/com/oneidentity/safeguard/safeguardjava/TlsConfiguration.java new file mode 100644 index 0000000..eedde47 --- /dev/null +++ b/src/main/java/com/oneidentity/safeguard/safeguardjava/TlsConfiguration.java @@ -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. + * + *

Default behavior. When neither a minimum nor a maximum version is + * configured, SafeguardJava negotiates TLS 1.2 only. 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 §4.6.2), so cert-auth on the + * Standard binding only succeeds at TLS 1.2. + * + *

Opting into TLS 1.3. 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. + * + *

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. + * + *

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. + * + *

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 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]); + } +} diff --git a/src/main/java/com/oneidentity/safeguard/safeguardjava/TlsVersion.java b/src/main/java/com/oneidentity/safeguard/safeguardjava/TlsVersion.java new file mode 100644 index 0000000..d3833dd --- /dev/null +++ b/src/main/java/com/oneidentity/safeguard/safeguardjava/TlsVersion.java @@ -0,0 +1,72 @@ +package com.oneidentity.safeguard.safeguardjava; + +/** + * TLS protocol versions that SafeguardJava can be constrained to negotiate. + * + *

The SDK exposes an opt-in minimum/maximum TLS version bound (see + * {@link Safeguard#setMinTlsVersion(TlsVersion)} and + * {@link Safeguard#setMaxTlsVersion(TlsVersion)}). Only the two versions + * relevant to modern Safeguard appliances are offered; TLS 1.0 and 1.1 are + * never enabled by the SDK. + * + *

The enum declaration order (lowest to highest) is significant: the range + * resolver in {@link TlsConfiguration} relies on {@link #ordinal()} to compare + * versions. + */ +public enum TlsVersion { + + /** TLS 1.2 — the SafeguardJava default and the only version that + * supports certificate/A2A authentication on the Standard binding. */ + TLSv1_2("TLSv1.2"), + + /** TLS 1.3 — opt-in. Certificate/A2A authentication over TLS 1.3 + * requires connecting to the appliance Cert SNI hostname because JSSE + * cannot present a client certificate post-handshake. */ + TLSv1_3("TLSv1.3"); + + private final String protocolName; + + TlsVersion(String protocolName) { + this.protocolName = protocolName; + } + + /** + * The JSSE protocol name for this version (e.g. {@code "TLSv1.2"}), as used + * by {@code SSLSocket.setEnabledProtocols(String[])}. + * + * @return the JSSE protocol name. + */ + public String getProtocolName() { + return protocolName; + } + + /** + * Parses a {@link TlsVersion} from a string, accepting the enum name + * ({@code TLSv1_2}), the JSSE protocol name ({@code TLSv1.2}), or a bare + * version number ({@code 1.2}). Parsing is case-insensitive and tolerant of + * surrounding whitespace. This is used to resolve the + * {@code safeguard.tls.minVersion} / {@code safeguard.tls.maxVersion} system + * properties. + * + * @param value the value to parse; may be {@code null}. + * @return the matching {@link TlsVersion}, or {@code null} when + * {@code value} is {@code null}, blank, or unrecognized. + */ + public static TlsVersion fromString(String value) { + if (value == null) { + return null; + } + String v = value.trim(); + if (v.isEmpty()) { + return null; + } + for (TlsVersion t : values()) { + if (t.name().equalsIgnoreCase(v) + || t.protocolName.equalsIgnoreCase(v) + || t.protocolName.substring("TLSv".length()).equalsIgnoreCase(v)) { + return t; + } + } + return null; + } +} diff --git a/src/main/java/com/oneidentity/safeguard/safeguardjava/authentication/PkceAuthenticator.java b/src/main/java/com/oneidentity/safeguard/safeguardjava/authentication/PkceAuthenticator.java index 38d8741..0efaa6f 100644 --- a/src/main/java/com/oneidentity/safeguard/safeguardjava/authentication/PkceAuthenticator.java +++ b/src/main/java/com/oneidentity/safeguard/safeguardjava/authentication/PkceAuthenticator.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.oneidentity.safeguard.safeguardjava.AgentBasedLoginUtils; import com.oneidentity.safeguard.safeguardjava.Utils; +import com.oneidentity.safeguard.safeguardjava.TlsConfiguration; import com.oneidentity.safeguard.safeguardjava.exceptions.ArgumentException; import com.oneidentity.safeguard.safeguardjava.exceptions.ObjectDisposedException; import com.oneidentity.safeguard.safeguardjava.exceptions.SafeguardForJavaException; @@ -371,6 +372,7 @@ private String rstsFormPost(CloseableHttpClient httpClient, String url, String f private CloseableHttpClient createPkceHttpClient(String appliance, String csrfToken) { try { SSLConnectionSocketFactory sslsf; + String[] enabledProtocols = TlsConfiguration.resolveEnabledProtocolNames(); if (isIgnoreSsl()) { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[]{new X509TrustManager() { @@ -378,7 +380,7 @@ private CloseableHttpClient createPkceHttpClient(String appliance, String csrfTo public void checkClientTrusted(X509Certificate[] certs, String authType) { } public void checkServerTrusted(X509Certificate[] certs, String authType) { } }}, new java.security.SecureRandom()); - sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); + sslsf = new SSLConnectionSocketFactory(sslContext, enabledProtocols, null, NoopHostnameVerifier.INSTANCE); } else if (getValidationCallback() != null) { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[]{new X509TrustManager() { @@ -386,9 +388,9 @@ public void checkServerTrusted(X509Certificate[] certs, String authType) { } public void checkClientTrusted(X509Certificate[] certs, String authType) { } public void checkServerTrusted(X509Certificate[] certs, String authType) { } }}, new java.security.SecureRandom()); - sslsf = new SSLConnectionSocketFactory(sslContext, getValidationCallback()); + sslsf = new SSLConnectionSocketFactory(sslContext, enabledProtocols, null, getValidationCallback()); } else { - sslsf = new SSLConnectionSocketFactory(SSLContext.getDefault()); + sslsf = new SSLConnectionSocketFactory(SSLContext.getDefault(), enabledProtocols, null, (HostnameVerifier) null); } Registry socketFactoryRegistry = RegistryBuilder.create() 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 c86e99f..0d82b26 100644 --- a/src/main/java/com/oneidentity/safeguard/safeguardjava/event/SafeguardEventListener.java +++ b/src/main/java/com/oneidentity/safeguard/safeguardjava/event/SafeguardEventListener.java @@ -6,6 +6,7 @@ import com.microsoft.signalr.HubConnectionBuilder; import com.microsoft.signalr.Action1; import com.microsoft.signalr.OnClosedCallback; +import com.oneidentity.safeguard.safeguardjava.TlsConfiguration; import com.oneidentity.safeguard.safeguardjava.data.CertificateContext; import com.oneidentity.safeguard.safeguardjava.data.SafeguardEventListenerState; import com.oneidentity.safeguard.safeguardjava.exceptions.ArgumentException; @@ -34,7 +35,9 @@ import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; +import okhttp3.ConnectionSpec; import okhttp3.Protocol; +import okhttp3.TlsVersion; import okhttp3.OkHttpClient.Builder; class DefaultDisconnectHandler implements IDisconnectHandler { @@ -50,13 +53,17 @@ public class SafeguardEventListener implements ISafeguardEventListener, AutoClos private static final Logger logger = LoggerFactory.getLogger(SafeguardEventListener.class); /** - * Minimum TLS protocol version pinned at the SDK layer. + * JSSE {@link SSLContext} algorithm used for the SignalR/WebSocket + * transport. * - *

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. + *

The generic {@code "TLS"} algorithm is requested so the context can + * negotiate the highest protocol the JVM supports. The enabled + * versions are constrained explicitly via an OkHttp {@link okhttp3.ConnectionSpec} + * built from {@link TlsConfiguration#resolveEnabledProtocolNames()} (default + * {@code TLSv1.2} only), so this transport tracks the same TLS policy as the + * REST clients. */ - static final String TLS_PROTOCOL = "TLSv1.2"; + static final String SSLCONTEXT_PROTOCOL = "TLS"; private boolean disposed; @@ -413,9 +420,21 @@ 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_PROTOCOL); + SSLContext sslContext = SSLContext.getInstance(SSLCONTEXT_PROTOCOL); sslContext.init(km, tm, null); builder.sslSocketFactory(sslContext.getSocketFactory(), x509tm); + + // Constrain the enabled TLS versions to the SDK-resolved set + // (default TLSv1.2 only; opt-in TLS 1.3 via TlsConfiguration) so the + // event transport matches the REST clients' TLS policy. + List enabledTlsVersions = new ArrayList<>(); + for (String protocolName : TlsConfiguration.resolveEnabledProtocolNames()) { + enabledTlsVersions.add(TlsVersion.forJavaName(protocolName)); + } + ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(enabledTlsVersions.toArray(new TlsVersion[0])) + .build(); + builder.connectionSpecs(Arrays.asList(tlsSpec)); } catch(NoSuchAlgorithmException ex) { logger.error(ex.getMessage()); 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 4b61cbc..fbb1a89 100644 --- a/src/main/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClient.java +++ b/src/main/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClient.java @@ -2,6 +2,7 @@ import static com.oneidentity.safeguard.safeguardjava.CertificateUtilities.WINDOWSKEYSTORE; import com.oneidentity.safeguard.safeguardjava.IProgressCallback; +import com.oneidentity.safeguard.safeguardjava.TlsConfiguration; import com.oneidentity.safeguard.safeguardjava.data.CertificateContext; import com.oneidentity.safeguard.safeguardjava.data.JsonObject; import com.oneidentity.safeguard.safeguardjava.exceptions.SafeguardForJavaException; @@ -74,15 +75,16 @@ public class RestClient { public static final int DEFAULT_TIMEOUT_MS = 100_000; /** - * Minimum TLS protocol version pinned at the SDK layer. + * JSSE {@link SSLContext} algorithm used for all Safeguard REST transports. * - *

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. + *

The generic {@code "TLS"} algorithm is requested so the context is + * capable of the highest protocol the JVM supports. The enabled + * protocol versions are then constrained explicitly at the socket-factory + * layer via {@link TlsConfiguration#resolveEnabledProtocolNames()}, which + * defaults to {@code TLSv1.2} only. This avoids relying on JVM defaults + * (which may permit TLS 1.0/1.1) while still allowing opt-in TLS 1.3. */ - static final String TLS_PROTOCOL = "TLSv1.2"; + static final String SSLCONTEXT_PROTOCOL = "TLS"; private CloseableHttpClient client = null; private BasicCookieStore cookieStore = new BasicCookieStore(); @@ -108,9 +110,10 @@ public RestClient(String connectionAddr, boolean ignoreSsl, HostnameVerifier val * {@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}. + * and must not be enabled in production. The enabled TLS protocol + * versions are unaffected by this flag and default to {@code TLSv1.2} + * only — see + * {@link TlsConfiguration#resolveEnabledProtocolNames()}. * *

For production with a self-signed or internal-CA appliance, * prefer importing the appliance certificate into the JVM truststore @@ -158,12 +161,12 @@ private HttpClientBuilder createClientBuilder(String connectionAddr, boolean ign SSLConnectionSocketFactory sslsf = null; if (ignoreSsl) { this.validationCallback = null; - sslsf = new SSLConnectionSocketFactory(getSSLContext(null, null, null, null), NoopHostnameVerifier.INSTANCE); + sslsf = buildSocketFactory(getSSLContext(null, null, null, null), NoopHostnameVerifier.INSTANCE); } else if (validationCallback != null) { this.validationCallback = validationCallback; - sslsf = new SSLConnectionSocketFactory(getSSLContext(null, null, null, null), validationCallback); + sslsf = buildSocketFactory(getSSLContext(null, null, null, null), validationCallback); } else { - sslsf = new SSLConnectionSocketFactory(getSSLContext(null, null, null, null)); + sslsf = buildSocketFactory(getSSLContext(null, null, null, null), null); } Registry socketFactoryRegistry = RegistryBuilder. create().register("https", sslsf).build(); BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(socketFactoryRegistry); @@ -527,11 +530,11 @@ private CloseableHttpClient getClientWithCertificate(CertificateContext certific SSLConnectionSocketFactory sslsf = null; if (ignoreSsl) { - sslsf = new SSLConnectionSocketFactory(getSSLContext(clientKs, keyPass, certificateAlias == null ? aliases.get(0) : certificateAlias, certificateContext), NoopHostnameVerifier.INSTANCE); + sslsf = buildSocketFactory(getSSLContext(clientKs, keyPass, certificateAlias == null ? aliases.get(0) : certificateAlias, certificateContext), NoopHostnameVerifier.INSTANCE); } else if (validationCallback != null) { - sslsf = new SSLConnectionSocketFactory(getSSLContext(clientKs, keyPass, certificateAlias == null ? aliases.get(0) : certificateAlias, certificateContext), validationCallback); + sslsf = buildSocketFactory(getSSLContext(clientKs, keyPass, certificateAlias == null ? aliases.get(0) : certificateAlias, certificateContext), validationCallback); } else { - sslsf = new SSLConnectionSocketFactory(getSSLContext(clientKs, keyPass, certificateAlias == null ? aliases.get(0) : certificateAlias, certificateContext)); + sslsf = buildSocketFactory(getSSLContext(clientKs, keyPass, certificateAlias == null ? aliases.get(0) : certificateAlias, certificateContext), null); } Registry socketFactoryRegistry = RegistryBuilder. create().register("https", sslsf).build(); BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(socketFactoryRegistry); @@ -572,6 +575,20 @@ private HttpClientContext createContext(Integer timeout) { return context; } + /** + * Builds an {@link SSLConnectionSocketFactory} whose enabled TLS protocol + * versions are constrained to the SDK-resolved set (default {@code TLSv1.2} + * only; opt-in TLS 1.3 via {@link TlsConfiguration}). + * + * @param ctx the initialized {@link SSLContext}. + * @param verifier the hostname verifier to use, or {@code null} to fall back + * to Apache HttpClient's default verifier. + * @return a socket factory pinned to the resolved protocol versions. + */ + private SSLConnectionSocketFactory buildSocketFactory(SSLContext ctx, HostnameVerifier verifier) { + return new SSLConnectionSocketFactory(ctx, TlsConfiguration.resolveEnabledProtocolNames(), null, verifier); + } + private SSLContext getSSLContext(KeyStore keyStorePath, char[] keyStorePassword, String alias, CertificateContext certificateContext) { TrustManager[] customTrustManager = null; @@ -606,7 +623,7 @@ public X509Certificate[] getAcceptedIssuers() { SSLContext ctx = null; try { - ctx = SSLContext.getInstance(TLS_PROTOCOL); + ctx = SSLContext.getInstance(SSLCONTEXT_PROTOCOL); ctx.init(customKeyManager, customTrustManager, new java.security.SecureRandom()); } catch (java.security.GeneralSecurityException ex) { } diff --git a/src/test/java/com/oneidentity/safeguard/safeguardjava/TlsConfigurationTest.java b/src/test/java/com/oneidentity/safeguard/safeguardjava/TlsConfigurationTest.java new file mode 100644 index 0000000..2e580e2 --- /dev/null +++ b/src/test/java/com/oneidentity/safeguard/safeguardjava/TlsConfigurationTest.java @@ -0,0 +1,121 @@ +package com.oneidentity.safeguard.safeguardjava; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for {@link TlsConfiguration} range resolution, validation, and + * system-property fallback, plus {@link TlsVersion#fromString(String)} parsing. + * + *

These verify the core policy of the TLS 1.3 support work: unset => + * TLS 1.2 only (preserving legacy behavior and Standard-binding cert-auth), and + * opt-in expansion to TLS 1.3 via programmatic setters or system properties. + */ +public class TlsConfigurationTest { + + @Before + @After + public void reset() { + TlsConfiguration.setMinTlsVersion(null); + TlsConfiguration.setMaxTlsVersion(null); + System.clearProperty(TlsConfiguration.MIN_TLS_VERSION_PROPERTY); + System.clearProperty(TlsConfiguration.MAX_TLS_VERSION_PROPERTY); + } + + @Test + public void defaultIsTls12Only() { + assertNull(TlsConfiguration.getMinTlsVersion()); + assertNull(TlsConfiguration.getMaxTlsVersion()); + assertArrayEquals(new String[] { "TLSv1.2" }, + TlsConfiguration.resolveEnabledProtocolNames()); + } + + @Test + public void maxTls13OpensRangeFrom12() { + TlsConfiguration.setMaxTlsVersion(TlsVersion.TLSv1_3); + assertArrayEquals(new String[] { "TLSv1.2", "TLSv1.3" }, + TlsConfiguration.resolveEnabledProtocolNames()); + } + + @Test + public void minTls13RequiresTls13Only() { + TlsConfiguration.setMinTlsVersion(TlsVersion.TLSv1_3); + assertArrayEquals(new String[] { "TLSv1.3" }, + TlsConfiguration.resolveEnabledProtocolNames()); + } + + @Test + public void explicitTls12BoundsPinTo12() { + TlsConfiguration.setMinTlsVersion(TlsVersion.TLSv1_2); + TlsConfiguration.setMaxTlsVersion(TlsVersion.TLSv1_2); + assertArrayEquals(new String[] { "TLSv1.2" }, + TlsConfiguration.resolveEnabledProtocolNames()); + } + + @Test + public void setterRejectsMinAboveMax() { + TlsConfiguration.setMaxTlsVersion(TlsVersion.TLSv1_2); + try { + TlsConfiguration.setMinTlsVersion(TlsVersion.TLSv1_3); + fail("Expected IllegalArgumentException for min > max"); + } catch (IllegalArgumentException expected) { + // ok + } + } + + @Test + public void setterRejectsMaxBelowMin() { + TlsConfiguration.setMinTlsVersion(TlsVersion.TLSv1_3); + try { + TlsConfiguration.setMaxTlsVersion(TlsVersion.TLSv1_2); + fail("Expected IllegalArgumentException for max < min"); + } catch (IllegalArgumentException expected) { + // ok + } + } + + @Test + public void systemPropertyFallbackAppliesWhenUnset() { + System.setProperty(TlsConfiguration.MAX_TLS_VERSION_PROPERTY, "1.3"); + assertArrayEquals(new String[] { "TLSv1.2", "TLSv1.3" }, + TlsConfiguration.resolveEnabledProtocolNames()); + } + + @Test + public void programmaticSettingOverridesSystemProperty() { + System.setProperty(TlsConfiguration.MAX_TLS_VERSION_PROPERTY, "1.3"); + TlsConfiguration.setMaxTlsVersion(TlsVersion.TLSv1_2); + assertArrayEquals(new String[] { "TLSv1.2" }, + TlsConfiguration.resolveEnabledProtocolNames()); + } + + @Test + public void inconsistentSystemPropertiesThrowOnResolve() { + System.setProperty(TlsConfiguration.MIN_TLS_VERSION_PROPERTY, "1.3"); + System.setProperty(TlsConfiguration.MAX_TLS_VERSION_PROPERTY, "1.2"); + try { + TlsConfiguration.resolveEnabledProtocolNames(); + fail("Expected IllegalStateException for min > max via system properties"); + } catch (IllegalStateException expected) { + // ok + } + } + + @Test + public void fromStringAcceptsMultipleForms() { + assertEquals(TlsVersion.TLSv1_2, TlsVersion.fromString("TLSv1.2")); + assertEquals(TlsVersion.TLSv1_2, TlsVersion.fromString("TLSv1_2")); + assertEquals(TlsVersion.TLSv1_2, TlsVersion.fromString(" 1.2 ")); + assertEquals(TlsVersion.TLSv1_3, TlsVersion.fromString("tlsv1.3")); + assertEquals(TlsVersion.TLSv1_3, TlsVersion.fromString("1.3")); + assertNull(TlsVersion.fromString(null)); + assertNull(TlsVersion.fromString("")); + assertNull(TlsVersion.fromString("TLSv1.1")); + } +} diff --git a/src/test/java/com/oneidentity/safeguard/safeguardjava/event/SafeguardEventListenerSSLContextTest.java b/src/test/java/com/oneidentity/safeguard/safeguardjava/event/SafeguardEventListenerSSLContextTest.java index fc8e451..874d9e9 100644 --- a/src/test/java/com/oneidentity/safeguard/safeguardjava/event/SafeguardEventListenerSSLContextTest.java +++ b/src/test/java/com/oneidentity/safeguard/safeguardjava/event/SafeguardEventListenerSSLContextTest.java @@ -1,32 +1,32 @@ 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. + * Regression test: TLS context wiring for the SignalR event listener path. * - *

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. + *

The listener builds its {@code SSLContext} with the generic {@code "TLS"} + * algorithm and constrains the enabled versions with an OkHttp + * {@code ConnectionSpec} derived from {@code TlsConfiguration} (default + * {@code TLSv1.2} only). This test guards the context algorithm constant so the + * transport cannot silently revert to a hard-pinned protocol string that would + * block opt-in TLS 1.3. * - *

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. + *

Note: The SignalR dependency may require Java 9+ at class-load time. This + * test uses 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 EXPECTED_CONTEXT_ALGORITHM = "TLS"; private static final String CLASS_NAME = "com.oneidentity.safeguard.safeguardjava.event.SafeguardEventListener"; @Test - public void tlsProtocolConstantIsPinnedToTls12() throws Exception { + public void sslContextAlgorithmConstantIsGenericTls() throws Exception { Class clazz; try { clazz = Class.forName(CLASS_NAME); @@ -35,28 +35,10 @@ public void tlsProtocolConstantIsPinnedToTls12() throws Exception { System.out.println("SKIP: " + e.getMessage()); return; } - Field f = clazz.getDeclaredField("TLS_PROTOCOL"); + Field f = clazz.getDeclaredField("SSLCONTEXT_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()); + assertEquals("SafeguardEventListener.SSLCONTEXT_PROTOCOL must be the generic \"TLS\" algorithm", + EXPECTED_CONTEXT_ALGORITHM, value); } } diff --git a/src/test/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClientSSLContextTest.java b/src/test/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClientSSLContextTest.java index 533aacd..f05e6af 100644 --- a/src/test/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClientSSLContextTest.java +++ b/src/test/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClientSSLContextTest.java @@ -10,42 +10,43 @@ import org.junit.Test; /** - * Regression test: TLS version pinning. + * Regression test: TLS context wiring for the REST transport. * - *

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. + *

{@link RestClient} builds its {@link SSLContext} with the generic + * {@code "TLS"} algorithm (so the context is capable of the JVM's highest + * protocol) and then constrains the enabled protocol versions at the + * socket-factory layer via {@code TlsConfiguration}. The enabled-version policy + * (default {@code TLSv1.2} only) is covered by + * {@link com.oneidentity.safeguard.safeguardjava.TlsConfigurationTest}; this + * test guards the context algorithm and the private {@code getSSLContext} call + * site against accidental regressions. */ public class RestClientSSLContextTest { - private static final String EXPECTED_PROTOCOL = "TLSv1.2"; + private static final String EXPECTED_CONTEXT_ALGORITHM = "TLS"; /** - * 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. + * Verifies that the package-private {@code SSLCONTEXT_PROTOCOL} constant is + * the generic {@code "TLS"} algorithm, keeping the source of truth for the + * context algorithm auditable in one place. */ @Test - public void tlsProtocolConstantIsPinnedToTls12() throws Exception { - Field f = RestClient.class.getDeclaredField("TLS_PROTOCOL"); + public void sslContextAlgorithmConstantIsGenericTls() throws Exception { + Field f = RestClient.class.getDeclaredField("SSLCONTEXT_PROTOCOL"); f.setAccessible(true); Object value = f.get(null); - assertEquals("RestClient.TLS_PROTOCOL must be pinned to TLSv1.2", - EXPECTED_PROTOCOL, value); + assertEquals("RestClient.SSLCONTEXT_PROTOCOL must be the generic \"TLS\" algorithm", + EXPECTED_CONTEXT_ALGORITHM, 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. + * Verifies that the SSLContext produced by RestClient.getSSLContext is + * initialized and reports the generic {@code "TLS"} algorithm. This catches + * regressions where the call site reverts to a hard-pinned protocol string, + * which would prevent opt-in TLS 1.3 from ever being negotiated. */ @Test - public void sslContextProtocolIsTls12() throws Exception { + public void sslContextReportsGenericTlsAlgorithm() throws Exception { RestClient client = new RestClient( "https://127.0.0.1:9999", true, // ignoreSsl — exercises the trust-all path @@ -60,8 +61,8 @@ public void sslContextProtocolIsTls12() throws Exception { 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()); + assertNotNull("getSSLContext returned null — TLS unsupported on this JVM?", ctx); + assertEquals("RestClient must build a generic TLS context and constrain versions at the socket factory", + EXPECTED_CONTEXT_ALGORITHM, ctx.getProtocol()); } }