Skip to content

Commit 01f5113

Browse files
committed
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
1 parent aabeb50 commit 01f5113

10 files changed

Lines changed: 247 additions & 2 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS CRT HTTP Client",
4+
"contributor": "",
5+
"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)."
6+
}

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,28 @@ AwsCrtAsyncHttpClient.Builder tcpKeepAliveConfiguration(Consumer<TcpKeepAliveCon
259259
* @return The builder of the method chaining.
260260
*/
261261
AwsCrtAsyncHttpClient.Builder postQuantumTlsEnabled(Boolean postQuantumTlsEnabled);
262+
263+
/**
264+
* Configure the minimum TLS protocol version the client will accept when negotiating
265+
* a TLS connection. Handshakes that would negotiate a lower version will fail.
266+
*
267+
* <p>If not set, the platform + CRT default is used (equivalent to
268+
* {@link TlsVersion#SYSTEM_DEFAULT}).
269+
*
270+
* <p>This option is mutually exclusive with {@link #postQuantumTlsEnabled(Boolean)
271+
* postQuantumTlsEnabled(false)}. Attempting to set both will cause client construction
272+
* to fail with an {@link IllegalStateException}.
273+
*
274+
* <p><b>macOS:</b> the default CRT TLS backend on macOS (Apple Secure Transport) does not
275+
* support TLS 1.3. To use {@link TlsVersion#TLS_1_3} on macOS you must set the environment
276+
* variable {@code AWS_CRT_USE_NON_FIPS_TLS_13} to any non-empty value at process startup so
277+
* the CRT selects its s2n-tls backend. See {@link TlsVersion} for details.
278+
*
279+
* @param minTlsVersion the minimum acceptable TLS version; {@code null} clears
280+
* the value and reverts to the platform default
281+
* @return this builder for method chaining
282+
*/
283+
AwsCrtAsyncHttpClient.Builder minTlsVersion(TlsVersion minTlsVersion);
262284
}
263285

264286
/**

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,28 @@ AwsCrtHttpClient.Builder tcpKeepAliveConfiguration(Consumer<TcpKeepAliveConfigur
303303
* @return The builder of the method chaining.
304304
*/
305305
AwsCrtHttpClient.Builder postQuantumTlsEnabled(Boolean postQuantumTlsEnabled);
306+
307+
/**
308+
* Configure the minimum TLS protocol version the client will accept when negotiating
309+
* a TLS connection. Handshakes that would negotiate a lower version will fail.
310+
*
311+
* <p>If not set, the platform + CRT default is used (equivalent to
312+
* {@link TlsVersion#SYSTEM_DEFAULT}).
313+
*
314+
* <p>This option is mutually exclusive with {@link #postQuantumTlsEnabled(Boolean)
315+
* postQuantumTlsEnabled(false)}. Attempting to set both will cause client construction
316+
* to fail with an {@link IllegalStateException}.
317+
*
318+
* <p><b>macOS:</b> the default CRT TLS backend on macOS (Apple Secure Transport) does not
319+
* support TLS 1.3. To use {@link TlsVersion#TLS_1_3} on macOS you must set the environment
320+
* variable {@code AWS_CRT_USE_NON_FIPS_TLS_13} to any non-empty value at process startup so
321+
* the CRT selects its s2n-tls backend. See {@link TlsVersion} for details.
322+
*
323+
* @param minTlsVersion the minimum acceptable TLS version; {@code null} clears
324+
* the value and reverts to the platform default
325+
* @return this builder for method chaining
326+
*/
327+
AwsCrtHttpClient.Builder minTlsVersion(TlsVersion minTlsVersion);
306328
}
307329

308330
/**

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildSocketOptions;
2222
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildTlsConnectionOptions;
2323
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveCipherPreference;
24+
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveMinTlsVersion;
2425
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
2526

2627
import java.net.URI;
@@ -94,6 +95,7 @@ abstract class AwsCrtHttpClientBase implements SdkAutoCloseable {
9495
TlsContextOptions clientTlsContextOptions =
9596
TlsContextOptions.createDefaultClient()
9697
.withCipherPreference(resolveCipherPreference(builder.getPostQuantumTlsEnabled()))
98+
.withMinimumTlsVersion(resolveMinTlsVersion(builder.getMinTlsVersion()))
9799
.withVerifyPeer(!config.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES));
98100
this.protocol = config.get(PROTOCOL);
99101
if (protocol == Protocol.HTTP2) {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.http.crt;
17+
18+
import software.amazon.awssdk.annotations.SdkPublicApi;
19+
20+
/**
21+
* A TLS protocol version that {@link AwsCrtHttpClient} and {@link AwsCrtAsyncHttpClient} uses.
22+
*
23+
* @see AwsCrtHttpClient.Builder#minTlsVersion(TlsVersion)
24+
* @see AwsCrtAsyncHttpClient.Builder#minTlsVersion(TlsVersion)
25+
*/
26+
@SdkPublicApi
27+
public enum TlsVersion {
28+
29+
/**
30+
* TLS 1.3.
31+
*/
32+
TLS_1_3,
33+
34+
/**
35+
* The underlying OS/platform + CRT TLS default.
36+
*/
37+
SYSTEM_DEFAULT
38+
}

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtClientBuilderBase.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import software.amazon.awssdk.http.crt.ConnectionHealthConfiguration;
2323
import software.amazon.awssdk.http.crt.ProxyConfiguration;
2424
import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration;
25+
import software.amazon.awssdk.http.crt.TlsVersion;
2526
import software.amazon.awssdk.utils.AttributeMap;
2627
import software.amazon.awssdk.utils.Validate;
2728

@@ -33,6 +34,7 @@ public class AwsCrtClientBuilderBase<BuilderT> {
3334
private ConnectionHealthConfiguration connectionHealthConfiguration;
3435
private TcpKeepAliveConfiguration tcpKeepAliveConfiguration;
3536
private Boolean postQuantumTlsEnabled;
37+
private TlsVersion minTlsVersion;
3638

3739
protected AwsCrtClientBuilderBase() {
3840
}
@@ -142,6 +144,15 @@ public Boolean getPostQuantumTlsEnabled() {
142144
return this.postQuantumTlsEnabled;
143145
}
144146

147+
public BuilderT minTlsVersion(TlsVersion minTlsVersion) {
148+
this.minTlsVersion = minTlsVersion;
149+
return thisBuilder();
150+
}
151+
152+
public TlsVersion getMinTlsVersion() {
153+
return this.minTlsVersion;
154+
}
155+
145156
public BuilderT proxyConfiguration(Consumer<ProxyConfiguration.Builder> proxyConfigurationBuilderConsumer) {
146157
ProxyConfiguration.Builder builder = ProxyConfiguration.builder();
147158
proxyConfigurationBuilderConsumer.accept(builder);

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import software.amazon.awssdk.crt.io.TlsCipherPreference;
2323
import software.amazon.awssdk.crt.io.TlsConnectionOptions;
2424
import software.amazon.awssdk.crt.io.TlsContext;
25+
import software.amazon.awssdk.crt.io.TlsContextOptions;
2526
import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration;
27+
import software.amazon.awssdk.http.crt.TlsVersion;
2628
import software.amazon.awssdk.utils.Logger;
2729
import software.amazon.awssdk.utils.NumericUtils;
2830

@@ -80,4 +82,21 @@ public static TlsCipherPreference resolveCipherPreference(Boolean postQuantumTls
8082
return TlsCipherPreference.TLS_CIPHER_SYSTEM_DEFAULT;
8183
}
8284

85+
/**
86+
* Translate the SDK-owned {@link TlsVersion} into the CRT-native {@link TlsContextOptions.TlsVersions}
87+
*/
88+
public static TlsContextOptions.TlsVersions resolveMinTlsVersion(TlsVersion minTlsVersion) {
89+
if (minTlsVersion == null) {
90+
return TlsContextOptions.TlsVersions.TLS_VER_SYS_DEFAULTS;
91+
}
92+
switch (minTlsVersion) {
93+
case TLS_1_3:
94+
return TlsContextOptions.TlsVersions.TLSv1_3;
95+
case SYSTEM_DEFAULT:
96+
return TlsContextOptions.TlsVersions.TLS_VER_SYS_DEFAULTS;
97+
default:
98+
throw new IllegalArgumentException("Unsupported minTlsVersion: " + minTlsVersion);
99+
}
100+
}
101+
83102
}

http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientTest.java

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@
1616
package software.amazon.awssdk.http.crt;
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatCode;
1920
import static org.assertj.core.api.Assertions.assertThatThrownBy;
21+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
2022

2123
import java.time.Duration;
24+
import java.util.function.Consumer;
2225
import java.util.stream.Stream;
2326
import org.junit.jupiter.api.Test;
2427
import org.junit.jupiter.params.ParameterizedTest;
2528
import org.junit.jupiter.params.provider.Arguments;
2629
import org.junit.jupiter.params.provider.MethodSource;
27-
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
30+
import software.amazon.awssdk.crt.io.TlsCipherPreference;
2831
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
2932
import software.amazon.awssdk.utils.AttributeMap;
3033

@@ -60,10 +63,57 @@ void asyncBuilder_buildWithDefaults_serviceDefaultsLacksTlsNegotiationTimeout_re
6063
}
6164
}
6265

66+
@ParameterizedTest(name = "{0}")
67+
@MethodSource("minTlsVersionInputs")
68+
void asyncBuilder_minTlsVersion_buildSucceeds(String description, Consumer<AwsCrtAsyncHttpClient.Builder> configure) {
69+
assertThatCode(() -> {
70+
AwsCrtAsyncHttpClient.Builder builder = AwsCrtAsyncHttpClient.builder();
71+
configure.accept(builder);
72+
builder.build().close();
73+
}).doesNotThrowAnyException();
74+
}
75+
76+
@Test
77+
void asyncBuilder_postQuantumTrueWithMinTls13_buildSucceeds() {
78+
assertThatCode(() -> AwsCrtAsyncHttpClient.builder()
79+
.postQuantumTlsEnabled(true)
80+
.minTlsVersion(TlsVersion.TLS_1_3)
81+
.build()
82+
.close())
83+
.doesNotThrowAnyException();
84+
}
85+
86+
// CRT enforces mutual exclusivity between a non-default cipher preference and a non-default minimum TLS version
87+
// (aws-crt-java TlsContextOptions#getNativeHandle throws IllegalStateException). This surfaces only on platforms
88+
// where TLS_CIPHER_NON_PQ_DEFAULT is supported; elsewhere resolveCipherPreference(false) falls back to
89+
// TLS_CIPHER_SYSTEM_DEFAULT (see AwsCrtConfigurationUtils) and CRT accepts the combination.
90+
@Test
91+
void asyncBuilder_postQuantumFalseWithMinTls13_failsWhenCrtEnforcesMutualExclusivity() {
92+
assumeTrue(TlsCipherPreference.TLS_CIPHER_NON_PQ_DEFAULT.isSupported());
93+
assertThatThrownBy(() -> AwsCrtAsyncHttpClient.builder()
94+
.postQuantumTlsEnabled(false)
95+
.minTlsVersion(TlsVersion.TLS_1_3)
96+
.build())
97+
.isInstanceOf(IllegalStateException.class);
98+
}
99+
100+
static Stream<Arguments> minTlsVersionInputs() {
101+
return Stream.of(
102+
Arguments.of("unset -> build succeeds",
103+
(Consumer<AwsCrtAsyncHttpClient.Builder>) b -> { }),
104+
Arguments.of("SYSTEM_DEFAULT -> build succeeds",
105+
(Consumer<AwsCrtAsyncHttpClient.Builder>) b -> b.minTlsVersion(TlsVersion.SYSTEM_DEFAULT)),
106+
Arguments.of("TLS_1_3 -> build succeeds",
107+
(Consumer<AwsCrtAsyncHttpClient.Builder>) b -> b.minTlsVersion(TlsVersion.TLS_1_3)),
108+
Arguments.of("TLS_1_3 then null -> build succeeds",
109+
(Consumer<AwsCrtAsyncHttpClient.Builder>) b ->
110+
b.minTlsVersion(TlsVersion.TLS_1_3).minTlsVersion(null))
111+
);
112+
}
113+
63114
private static SdkAsyncHttpClient buildAsync(AwsCrtAsyncHttpClient.Builder builder, Duration serviceDefault) {
64115
return serviceDefault == null
65116
? builder.build()
66117
: builder.buildWithDefaults(serviceDefaultsMap(serviceDefault));
67118
}
68-
69119
}

http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,18 @@
1616
package software.amazon.awssdk.http.crt;
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatCode;
1920
import static org.assertj.core.api.Assertions.assertThatThrownBy;
21+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
2022

2123
import java.time.Duration;
24+
import java.util.function.Consumer;
25+
import java.util.stream.Stream;
2226
import org.junit.jupiter.api.Test;
2327
import org.junit.jupiter.params.ParameterizedTest;
28+
import org.junit.jupiter.params.provider.Arguments;
2429
import org.junit.jupiter.params.provider.MethodSource;
30+
import software.amazon.awssdk.crt.io.TlsCipherPreference;
2531
import software.amazon.awssdk.http.SdkHttpClient;
2632
import software.amazon.awssdk.utils.AttributeMap;
2733

@@ -69,6 +75,53 @@ void syncBuilder_resolvedTlsNegotiationTimeout_matchesPathBPrecedence(String des
6975
}
7076
}
7177

78+
@ParameterizedTest(name = "{0}")
79+
@MethodSource("minTlsVersionInputs")
80+
void syncBuilder_minTlsVersion_buildSucceeds(String description, Consumer<AwsCrtHttpClient.Builder> configure) {
81+
assertThatCode(() -> {
82+
AwsCrtHttpClient.Builder builder = AwsCrtHttpClient.builder();
83+
configure.accept(builder);
84+
builder.build().close();
85+
}).doesNotThrowAnyException();
86+
}
87+
88+
@Test
89+
void syncBuilder_postQuantumTrueWithMinTls13_buildSucceeds() {
90+
assertThatCode(() -> AwsCrtHttpClient.builder()
91+
.postQuantumTlsEnabled(true)
92+
.minTlsVersion(TlsVersion.TLS_1_3)
93+
.build()
94+
.close())
95+
.doesNotThrowAnyException();
96+
}
97+
98+
// CRT enforces mutual exclusivity between a non-default cipher preference and a non-default minimum TLS version
99+
// (aws-crt-java TlsContextOptions#getNativeHandle throws IllegalStateException). This surfaces only on platforms
100+
// where TLS_CIPHER_NON_PQ_DEFAULT is supported; elsewhere resolveCipherPreference(false) falls back to
101+
// TLS_CIPHER_SYSTEM_DEFAULT (see AwsCrtConfigurationUtils) and CRT accepts the combination.
102+
@Test
103+
void syncBuilder_postQuantumFalseWithMinTls13_failsWhenCrtEnforcesMutualExclusivity() {
104+
assumeTrue(TlsCipherPreference.TLS_CIPHER_NON_PQ_DEFAULT.isSupported());
105+
assertThatThrownBy(() -> AwsCrtHttpClient.builder()
106+
.postQuantumTlsEnabled(false)
107+
.minTlsVersion(TlsVersion.TLS_1_3)
108+
.build())
109+
.isInstanceOf(IllegalStateException.class);
110+
}
111+
112+
static Stream<Arguments> minTlsVersionInputs() {
113+
return Stream.of(
114+
Arguments.of("unset -> build succeeds",
115+
(Consumer<AwsCrtHttpClient.Builder>) b -> { }),
116+
Arguments.of("SYSTEM_DEFAULT -> build succeeds",
117+
(Consumer<AwsCrtHttpClient.Builder>) b -> b.minTlsVersion(TlsVersion.SYSTEM_DEFAULT)),
118+
Arguments.of("TLS_1_3 -> build succeeds",
119+
(Consumer<AwsCrtHttpClient.Builder>) b -> b.minTlsVersion(TlsVersion.TLS_1_3)),
120+
Arguments.of("TLS_1_3 then null -> build succeeds",
121+
(Consumer<AwsCrtHttpClient.Builder>) b -> b.minTlsVersion(TlsVersion.TLS_1_3).minTlsVersion(null))
122+
);
123+
}
124+
72125
private static SdkHttpClient buildSync(AwsCrtHttpClient.Builder builder, Duration serviceDefault) {
73126
return serviceDefault == null
74127
? builder.build()

http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@
2121

2222
import java.time.Duration;
2323
import java.util.stream.Stream;
24+
import org.junit.jupiter.api.Test;
2425
import org.junit.jupiter.params.ParameterizedTest;
2526
import org.junit.jupiter.params.provider.Arguments;
2627
import org.junit.jupiter.params.provider.MethodSource;
2728
import software.amazon.awssdk.crt.http.HttpMonitoringOptions;
2829
import software.amazon.awssdk.crt.io.SocketOptions;
2930
import software.amazon.awssdk.crt.io.TlsCipherPreference;
31+
import software.amazon.awssdk.crt.io.TlsContextOptions;
3032
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
3133
import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration;
34+
import software.amazon.awssdk.http.crt.TlsVersion;
3235
import software.amazon.awssdk.utils.AttributeMap;
3336

3437
class AwsCrtConfigurationUtilsTest {
@@ -99,6 +102,25 @@ private static Stream<Arguments> tcpKeepAliveConfiguration() {
99102
);
100103
}
101104

105+
@Test
106+
void resolveMinTlsVersion_null_returnsSystemDefaults() {
107+
assertThat(AwsCrtConfigurationUtils.resolveMinTlsVersion(null))
108+
.isEqualTo(TlsContextOptions.TlsVersions.TLS_VER_SYS_DEFAULTS);
109+
}
110+
111+
@Test
112+
void resolveMinTlsVersion_systemDefault_returnsSystemDefaults() {
113+
assertThat(AwsCrtConfigurationUtils.resolveMinTlsVersion(TlsVersion.SYSTEM_DEFAULT))
114+
.isEqualTo(TlsContextOptions.TlsVersions.TLS_VER_SYS_DEFAULTS);
115+
}
116+
117+
@Test
118+
void resolveMinTlsVersion_tls13_returnsTLSv1_3() {
119+
assertThat(AwsCrtConfigurationUtils.resolveMinTlsVersion(TlsVersion.TLS_1_3))
120+
.isEqualTo(TlsContextOptions.TlsVersions.TLSv1_3);
121+
}
122+
123+
102124
private static Stream<Arguments> defaultConnectionHealthConfigurationCases() {
103125
return Stream.of(
104126
Arguments.of(Duration.ofSeconds(30), Duration.ofSeconds(30), 30),

0 commit comments

Comments
 (0)