Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSCRTHTTPClient-d471f89.json
Original file line number Diff line number Diff line change
@@ -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)."
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,28 @@ AwsCrtAsyncHttpClient.Builder tcpKeepAliveConfiguration(Consumer<TcpKeepAliveCon
* @return The builder of the method chaining.
*/
AwsCrtAsyncHttpClient.Builder postQuantumTlsEnabled(Boolean postQuantumTlsEnabled);

/**
* Configure the minimum TLS protocol version the client will accept when negotiating
* a TLS connection. Handshakes that would negotiate a lower version will fail.
*
* <p>If not set, the platform + CRT default is used (equivalent to
* {@link TlsVersion#SYSTEM_DEFAULT}).
*
* <p>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}.
*
* <p><b>macOS:</b> 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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,28 @@ AwsCrtHttpClient.Builder tcpKeepAliveConfiguration(Consumer<TcpKeepAliveConfigur
* @return The builder of the method chaining.
*/
AwsCrtHttpClient.Builder postQuantumTlsEnabled(Boolean postQuantumTlsEnabled);

/**
* Configure the minimum TLS protocol version the client will accept when negotiating
* a TLS connection. Handshakes that would negotiate a lower version will fail.
*
* <p>If not set, the platform + CRT default is used (equivalent to
* {@link TlsVersion#SYSTEM_DEFAULT}).
*
* <p>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}.
*
* <p><b>macOS:</b> 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);
}

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

Expand All @@ -33,6 +34,7 @@ public class AwsCrtClientBuilderBase<BuilderT> {
private ConnectionHealthConfiguration connectionHealthConfiguration;
private TcpKeepAliveConfiguration tcpKeepAliveConfiguration;
private Boolean postQuantumTlsEnabled;
private TlsVersion minTlsVersion;

protected AwsCrtClientBuilderBase() {
}
Expand Down Expand Up @@ -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<ProxyConfiguration.Builder> proxyConfigurationBuilderConsumer) {
ProxyConfiguration.Builder builder = ProxyConfiguration.builder();
proxyConfigurationBuilderConsumer.accept(builder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -60,10 +63,57 @@ void asyncBuilder_buildWithDefaults_serviceDefaultsLacksTlsNegotiationTimeout_re
}
}

@ParameterizedTest(name = "{0}")
@MethodSource("minTlsVersionInputs")
void asyncBuilder_minTlsVersion_buildSucceeds(String description, Consumer<AwsCrtAsyncHttpClient.Builder> 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<Arguments> minTlsVersionInputs() {
return Stream.of(
Arguments.of("unset -> build succeeds",
(Consumer<AwsCrtAsyncHttpClient.Builder>) b -> { }),
Arguments.of("SYSTEM_DEFAULT -> build succeeds",
(Consumer<AwsCrtAsyncHttpClient.Builder>) b -> b.minTlsVersion(TlsVersion.SYSTEM_DEFAULT)),
Arguments.of("TLS_1_3 -> build succeeds",
(Consumer<AwsCrtAsyncHttpClient.Builder>) b -> b.minTlsVersion(TlsVersion.TLS_1_3)),
Arguments.of("TLS_1_3 then null -> build succeeds",
(Consumer<AwsCrtAsyncHttpClient.Builder>) 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));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -69,6 +75,53 @@ void syncBuilder_resolvedTlsNegotiationTimeout_matchesPathBPrecedence(String des
}
}

@ParameterizedTest(name = "{0}")
@MethodSource("minTlsVersionInputs")
void syncBuilder_minTlsVersion_buildSucceeds(String description, Consumer<AwsCrtHttpClient.Builder> 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<Arguments> minTlsVersionInputs() {
return Stream.of(
Arguments.of("unset -> build succeeds",
(Consumer<AwsCrtHttpClient.Builder>) b -> { }),
Arguments.of("SYSTEM_DEFAULT -> build succeeds",
(Consumer<AwsCrtHttpClient.Builder>) b -> b.minTlsVersion(TlsVersion.SYSTEM_DEFAULT)),
Arguments.of("TLS_1_3 -> build succeeds",
(Consumer<AwsCrtHttpClient.Builder>) b -> b.minTlsVersion(TlsVersion.TLS_1_3)),
Arguments.of("TLS_1_3 then null -> build succeeds",
(Consumer<AwsCrtHttpClient.Builder>) b -> b.minTlsVersion(TlsVersion.TLS_1_3).minTlsVersion(null))
);
}

private static SdkHttpClient buildSync(AwsCrtHttpClient.Builder builder, Duration serviceDefault) {
return serviceDefault == null
? builder.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -99,6 +102,25 @@ private static Stream<Arguments> 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<Arguments> defaultConnectionHealthConfigurationCases() {
return Stream.of(
Arguments.of(Duration.ofSeconds(30), Duration.ofSeconds(30), 30),
Expand Down
Loading