From 01f51131d1cbe65e073e0f7f075a9aa93f9023d8 Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:56:33 -0700 Subject: [PATCH] feat(crt): add minTlsVersion option to CRT HTTP clients Add minTlsVersion(TlsVersion) on AwsCrtHttpClient.Builder and AwsCrtAsyncHttpClient.Builder so customers can enforce a minimum TLS protocol version for outbound connections. Fixes #5619 --- .../feature-AWSCRTHTTPClient-d471f89.json | 6 +++ .../http/crt/AwsCrtAsyncHttpClient.java | 22 ++++++++ .../awssdk/http/crt/AwsCrtHttpClient.java | 22 ++++++++ .../awssdk/http/crt/AwsCrtHttpClientBase.java | 2 + .../amazon/awssdk/http/crt/TlsVersion.java | 38 +++++++++++++ .../crt/internal/AwsCrtClientBuilderBase.java | 11 ++++ .../internal/AwsCrtConfigurationUtils.java | 19 +++++++ .../http/crt/AwsCrtAsyncHttpClientTest.java | 54 ++++++++++++++++++- .../awssdk/http/crt/AwsCrtHttpClientTest.java | 53 ++++++++++++++++++ .../AwsCrtConfigurationUtilsTest.java | 22 ++++++++ 10 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 .changes/next-release/feature-AWSCRTHTTPClient-d471f89.json create mode 100644 http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/TlsVersion.java diff --git a/.changes/next-release/feature-AWSCRTHTTPClient-d471f89.json b/.changes/next-release/feature-AWSCRTHTTPClient-d471f89.json new file mode 100644 index 000000000000..54477618ecbb --- /dev/null +++ b/.changes/next-release/feature-AWSCRTHTTPClient-d471f89.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS CRT HTTP Client", + "contributor": "", + "description": "Add a minTlsVersion(TlsVersion) builder option on AwsCrtHttpClient and AwsCrtAsyncHttpClient that enforces a minimum TLS protocol version for outbound connections. Currently supports TlsVersion.TLS_1_3 and TlsVersion.SYSTEM_DEFAULT (the default). Fixes [#5619](https://github.com/aws/aws-sdk-java-v2/issues/5619)." +} diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java index c0fd0f9ee902..36b7700f49c4 100644 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java @@ -259,6 +259,28 @@ AwsCrtAsyncHttpClient.Builder tcpKeepAliveConfiguration(ConsumerIf not set, the platform + CRT default is used (equivalent to + * {@link TlsVersion#SYSTEM_DEFAULT}). + * + *

This option is mutually exclusive with {@link #postQuantumTlsEnabled(Boolean) + * postQuantumTlsEnabled(false)}. Attempting to set both will cause client construction + * to fail with an {@link IllegalStateException}. + * + *

macOS: the default CRT TLS backend on macOS (Apple Secure Transport) does not + * support TLS 1.3. To use {@link TlsVersion#TLS_1_3} on macOS you must set the environment + * variable {@code AWS_CRT_USE_NON_FIPS_TLS_13} to any non-empty value at process startup so + * the CRT selects its s2n-tls backend. See {@link TlsVersion} for details. + * + * @param minTlsVersion the minimum acceptable TLS version; {@code null} clears + * the value and reverts to the platform default + * @return this builder for method chaining + */ + AwsCrtAsyncHttpClient.Builder minTlsVersion(TlsVersion minTlsVersion); } /** diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java index 4d8ee9511a4b..999875889764 100644 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java @@ -303,6 +303,28 @@ AwsCrtHttpClient.Builder tcpKeepAliveConfiguration(ConsumerIf not set, the platform + CRT default is used (equivalent to + * {@link TlsVersion#SYSTEM_DEFAULT}). + * + *

This option is mutually exclusive with {@link #postQuantumTlsEnabled(Boolean) + * postQuantumTlsEnabled(false)}. Attempting to set both will cause client construction + * to fail with an {@link IllegalStateException}. + * + *

macOS: the default CRT TLS backend on macOS (Apple Secure Transport) does not + * support TLS 1.3. To use {@link TlsVersion#TLS_1_3} on macOS you must set the environment + * variable {@code AWS_CRT_USE_NON_FIPS_TLS_13} to any non-empty value at process startup so + * the CRT selects its s2n-tls backend. See {@link TlsVersion} for details. + * + * @param minTlsVersion the minimum acceptable TLS version; {@code null} clears + * the value and reverts to the platform default + * @return this builder for method chaining + */ + AwsCrtHttpClient.Builder minTlsVersion(TlsVersion minTlsVersion); } /** diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java index a8b6a859556f..3c5ec7ced8f8 100644 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java @@ -21,6 +21,7 @@ import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildSocketOptions; import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildTlsConnectionOptions; import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveCipherPreference; +import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveMinTlsVersion; import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; import java.net.URI; @@ -94,6 +95,7 @@ abstract class AwsCrtHttpClientBase implements SdkAutoCloseable { TlsContextOptions clientTlsContextOptions = TlsContextOptions.createDefaultClient() .withCipherPreference(resolveCipherPreference(builder.getPostQuantumTlsEnabled())) + .withMinimumTlsVersion(resolveMinTlsVersion(builder.getMinTlsVersion())) .withVerifyPeer(!config.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES)); this.protocol = config.get(PROTOCOL); if (protocol == Protocol.HTTP2) { diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/TlsVersion.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/TlsVersion.java new file mode 100644 index 000000000000..733e736fd1e8 --- /dev/null +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/TlsVersion.java @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * A TLS protocol version that {@link AwsCrtHttpClient} and {@link AwsCrtAsyncHttpClient} uses. + * + * @see AwsCrtHttpClient.Builder#minTlsVersion(TlsVersion) + * @see AwsCrtAsyncHttpClient.Builder#minTlsVersion(TlsVersion) + */ +@SdkPublicApi +public enum TlsVersion { + + /** + * TLS 1.3. + */ + TLS_1_3, + + /** + * The underlying OS/platform + CRT TLS default. + */ + SYSTEM_DEFAULT +} diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtClientBuilderBase.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtClientBuilderBase.java index f827cb01b8e8..8114bc53c208 100644 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtClientBuilderBase.java +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtClientBuilderBase.java @@ -22,6 +22,7 @@ import software.amazon.awssdk.http.crt.ConnectionHealthConfiguration; import software.amazon.awssdk.http.crt.ProxyConfiguration; import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration; +import software.amazon.awssdk.http.crt.TlsVersion; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.Validate; @@ -33,6 +34,7 @@ public class AwsCrtClientBuilderBase { private ConnectionHealthConfiguration connectionHealthConfiguration; private TcpKeepAliveConfiguration tcpKeepAliveConfiguration; private Boolean postQuantumTlsEnabled; + private TlsVersion minTlsVersion; protected AwsCrtClientBuilderBase() { } @@ -142,6 +144,15 @@ public Boolean getPostQuantumTlsEnabled() { return this.postQuantumTlsEnabled; } + public BuilderT minTlsVersion(TlsVersion minTlsVersion) { + this.minTlsVersion = minTlsVersion; + return thisBuilder(); + } + + public TlsVersion getMinTlsVersion() { + return this.minTlsVersion; + } + public BuilderT proxyConfiguration(Consumer proxyConfigurationBuilderConsumer) { ProxyConfiguration.Builder builder = ProxyConfiguration.builder(); proxyConfigurationBuilderConsumer.accept(builder); diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java index b3a23fc38878..c3de2a1aee2f 100644 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java @@ -22,7 +22,9 @@ import software.amazon.awssdk.crt.io.TlsCipherPreference; import software.amazon.awssdk.crt.io.TlsConnectionOptions; import software.amazon.awssdk.crt.io.TlsContext; +import software.amazon.awssdk.crt.io.TlsContextOptions; import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration; +import software.amazon.awssdk.http.crt.TlsVersion; import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.NumericUtils; @@ -80,4 +82,21 @@ public static TlsCipherPreference resolveCipherPreference(Boolean postQuantumTls return TlsCipherPreference.TLS_CIPHER_SYSTEM_DEFAULT; } + /** + * Translate the SDK-owned {@link TlsVersion} into the CRT-native {@link TlsContextOptions.TlsVersions} + */ + public static TlsContextOptions.TlsVersions resolveMinTlsVersion(TlsVersion minTlsVersion) { + if (minTlsVersion == null) { + return TlsContextOptions.TlsVersions.TLS_VER_SYS_DEFAULTS; + } + switch (minTlsVersion) { + case TLS_1_3: + return TlsContextOptions.TlsVersions.TLSv1_3; + case SYSTEM_DEFAULT: + return TlsContextOptions.TlsVersions.TLS_VER_SYS_DEFAULTS; + default: + throw new IllegalArgumentException("Unsupported minTlsVersion: " + minTlsVersion); + } + } + } diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientTest.java index f50f5a3aeac4..db0b668c004c 100644 --- a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientTest.java +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientTest.java @@ -16,15 +16,18 @@ package software.amazon.awssdk.http.crt; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.time.Duration; +import java.util.function.Consumer; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.crt.io.TlsCipherPreference; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.utils.AttributeMap; @@ -60,10 +63,57 @@ void asyncBuilder_buildWithDefaults_serviceDefaultsLacksTlsNegotiationTimeout_re } } + @ParameterizedTest(name = "{0}") + @MethodSource("minTlsVersionInputs") + void asyncBuilder_minTlsVersion_buildSucceeds(String description, Consumer configure) { + assertThatCode(() -> { + AwsCrtAsyncHttpClient.Builder builder = AwsCrtAsyncHttpClient.builder(); + configure.accept(builder); + builder.build().close(); + }).doesNotThrowAnyException(); + } + + @Test + void asyncBuilder_postQuantumTrueWithMinTls13_buildSucceeds() { + assertThatCode(() -> AwsCrtAsyncHttpClient.builder() + .postQuantumTlsEnabled(true) + .minTlsVersion(TlsVersion.TLS_1_3) + .build() + .close()) + .doesNotThrowAnyException(); + } + + // CRT enforces mutual exclusivity between a non-default cipher preference and a non-default minimum TLS version + // (aws-crt-java TlsContextOptions#getNativeHandle throws IllegalStateException). This surfaces only on platforms + // where TLS_CIPHER_NON_PQ_DEFAULT is supported; elsewhere resolveCipherPreference(false) falls back to + // TLS_CIPHER_SYSTEM_DEFAULT (see AwsCrtConfigurationUtils) and CRT accepts the combination. + @Test + void asyncBuilder_postQuantumFalseWithMinTls13_failsWhenCrtEnforcesMutualExclusivity() { + assumeTrue(TlsCipherPreference.TLS_CIPHER_NON_PQ_DEFAULT.isSupported()); + assertThatThrownBy(() -> AwsCrtAsyncHttpClient.builder() + .postQuantumTlsEnabled(false) + .minTlsVersion(TlsVersion.TLS_1_3) + .build()) + .isInstanceOf(IllegalStateException.class); + } + + static Stream minTlsVersionInputs() { + return Stream.of( + Arguments.of("unset -> build succeeds", + (Consumer) b -> { }), + Arguments.of("SYSTEM_DEFAULT -> build succeeds", + (Consumer) b -> b.minTlsVersion(TlsVersion.SYSTEM_DEFAULT)), + Arguments.of("TLS_1_3 -> build succeeds", + (Consumer) b -> b.minTlsVersion(TlsVersion.TLS_1_3)), + Arguments.of("TLS_1_3 then null -> build succeeds", + (Consumer) b -> + b.minTlsVersion(TlsVersion.TLS_1_3).minTlsVersion(null)) + ); + } + private static SdkAsyncHttpClient buildAsync(AwsCrtAsyncHttpClient.Builder builder, Duration serviceDefault) { return serviceDefault == null ? builder.build() : builder.buildWithDefaults(serviceDefaultsMap(serviceDefault)); } - } diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientTest.java index e223fdf8d8f0..040d1307afdb 100644 --- a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientTest.java +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientTest.java @@ -16,12 +16,18 @@ package software.amazon.awssdk.http.crt; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.time.Duration; +import java.util.function.Consumer; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.crt.io.TlsCipherPreference; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.utils.AttributeMap; @@ -69,6 +75,53 @@ void syncBuilder_resolvedTlsNegotiationTimeout_matchesPathBPrecedence(String des } } + @ParameterizedTest(name = "{0}") + @MethodSource("minTlsVersionInputs") + void syncBuilder_minTlsVersion_buildSucceeds(String description, Consumer configure) { + assertThatCode(() -> { + AwsCrtHttpClient.Builder builder = AwsCrtHttpClient.builder(); + configure.accept(builder); + builder.build().close(); + }).doesNotThrowAnyException(); + } + + @Test + void syncBuilder_postQuantumTrueWithMinTls13_buildSucceeds() { + assertThatCode(() -> AwsCrtHttpClient.builder() + .postQuantumTlsEnabled(true) + .minTlsVersion(TlsVersion.TLS_1_3) + .build() + .close()) + .doesNotThrowAnyException(); + } + + // CRT enforces mutual exclusivity between a non-default cipher preference and a non-default minimum TLS version + // (aws-crt-java TlsContextOptions#getNativeHandle throws IllegalStateException). This surfaces only on platforms + // where TLS_CIPHER_NON_PQ_DEFAULT is supported; elsewhere resolveCipherPreference(false) falls back to + // TLS_CIPHER_SYSTEM_DEFAULT (see AwsCrtConfigurationUtils) and CRT accepts the combination. + @Test + void syncBuilder_postQuantumFalseWithMinTls13_failsWhenCrtEnforcesMutualExclusivity() { + assumeTrue(TlsCipherPreference.TLS_CIPHER_NON_PQ_DEFAULT.isSupported()); + assertThatThrownBy(() -> AwsCrtHttpClient.builder() + .postQuantumTlsEnabled(false) + .minTlsVersion(TlsVersion.TLS_1_3) + .build()) + .isInstanceOf(IllegalStateException.class); + } + + static Stream minTlsVersionInputs() { + return Stream.of( + Arguments.of("unset -> build succeeds", + (Consumer) b -> { }), + Arguments.of("SYSTEM_DEFAULT -> build succeeds", + (Consumer) b -> b.minTlsVersion(TlsVersion.SYSTEM_DEFAULT)), + Arguments.of("TLS_1_3 -> build succeeds", + (Consumer) b -> b.minTlsVersion(TlsVersion.TLS_1_3)), + Arguments.of("TLS_1_3 then null -> build succeeds", + (Consumer) b -> b.minTlsVersion(TlsVersion.TLS_1_3).minTlsVersion(null)) + ); + } + private static SdkHttpClient buildSync(AwsCrtHttpClient.Builder builder, Duration serviceDefault) { return serviceDefault == null ? builder.build() diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java index 7e414e50bcc0..c2dcfa282a07 100644 --- a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java @@ -21,14 +21,17 @@ import java.time.Duration; import java.util.stream.Stream; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import software.amazon.awssdk.crt.http.HttpMonitoringOptions; import software.amazon.awssdk.crt.io.SocketOptions; import software.amazon.awssdk.crt.io.TlsCipherPreference; +import software.amazon.awssdk.crt.io.TlsContextOptions; import software.amazon.awssdk.http.SdkHttpConfigurationOption; import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration; +import software.amazon.awssdk.http.crt.TlsVersion; import software.amazon.awssdk.utils.AttributeMap; class AwsCrtConfigurationUtilsTest { @@ -99,6 +102,25 @@ private static Stream tcpKeepAliveConfiguration() { ); } + @Test + void resolveMinTlsVersion_null_returnsSystemDefaults() { + assertThat(AwsCrtConfigurationUtils.resolveMinTlsVersion(null)) + .isEqualTo(TlsContextOptions.TlsVersions.TLS_VER_SYS_DEFAULTS); + } + + @Test + void resolveMinTlsVersion_systemDefault_returnsSystemDefaults() { + assertThat(AwsCrtConfigurationUtils.resolveMinTlsVersion(TlsVersion.SYSTEM_DEFAULT)) + .isEqualTo(TlsContextOptions.TlsVersions.TLS_VER_SYS_DEFAULTS); + } + + @Test + void resolveMinTlsVersion_tls13_returnsTLSv1_3() { + assertThat(AwsCrtConfigurationUtils.resolveMinTlsVersion(TlsVersion.TLS_1_3)) + .isEqualTo(TlsContextOptions.TlsVersions.TLSv1_3); + } + + private static Stream defaultConnectionHealthConfigurationCases() { return Stream.of( Arguments.of(Duration.ofSeconds(30), Duration.ofSeconds(30), 30),