diff --git a/.changes/next-release/feature-AWSCRTHTTPClient-5e6e380.json b/.changes/next-release/feature-AWSCRTHTTPClient-5e6e380.json new file mode 100644 index 000000000000..82add9dbef36 --- /dev/null +++ b/.changes/next-release/feature-AWSCRTHTTPClient-5e6e380.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS CRT HTTP Client", + "contributor": "", + "description": "Allow users to enable Post Quantum TLS via `AwsCrtAsyncHttpClient.builder().postQuantumTlsEnabled(true).build()`." +} 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 5c09dd463a7c..4333a909aea7 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 @@ -17,6 +17,10 @@ import static software.amazon.awssdk.http.HttpMetric.HTTP_CLIENT_NAME; import static software.amazon.awssdk.http.SdkHttpConfigurationOption.PROTOCOL; +import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildProxyOptions; +import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildSocketOptions; +import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveCipherPreference; +import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveHttpMonitoringOptions; import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; import static software.amazon.awssdk.utils.Validate.paramNotNull; @@ -36,7 +40,6 @@ import software.amazon.awssdk.crt.http.HttpProxyOptions; import software.amazon.awssdk.crt.io.ClientBootstrap; import software.amazon.awssdk.crt.io.SocketOptions; -import software.amazon.awssdk.crt.io.TlsCipherPreference; import software.amazon.awssdk.crt.io.TlsContext; import software.amazon.awssdk.crt.io.TlsContextOptions; import software.amazon.awssdk.http.Protocol; @@ -97,10 +100,11 @@ private AwsCrtAsyncHttpClient(DefaultBuilder builder, AttributeMap config) { } try (ClientBootstrap clientBootstrap = new ClientBootstrap(null, null); - SocketOptions clientSocketOptions = buildSocketOptions(builder, config); + SocketOptions clientSocketOptions = buildSocketOptions(builder.tcpKeepAliveConfiguration, + config.get(SdkHttpConfigurationOption.CONNECTION_TIMEOUT)); TlsContextOptions clientTlsContextOptions = TlsContextOptions.createDefaultClient() - .withCipherPreference(TlsCipherPreference.TLS_CIPHER_SYSTEM_DEFAULT) + .withCipherPreference(resolveCipherPreference(builder.postQuantumTlsEnabled)) .withVerifyPeer(!config.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES)); TlsContext clientTlsContext = new TlsContext(clientTlsContextOptions)) { @@ -109,69 +113,12 @@ private AwsCrtAsyncHttpClient(DefaultBuilder builder, AttributeMap config) { this.tlsContext = registerOwnedResource(clientTlsContext); this.readBufferSize = builder.readBufferSize == null ? DEFAULT_STREAM_WINDOW_SIZE : builder.readBufferSize; this.maxConnectionsPerEndpoint = config.get(SdkHttpConfigurationOption.MAX_CONNECTIONS); - this.monitoringOptions = revolveHttpMonitoringOptions(builder.connectionHealthConfiguration); + this.monitoringOptions = resolveHttpMonitoringOptions(builder.connectionHealthConfiguration); this.maxConnectionIdleInMilliseconds = config.get(SdkHttpConfigurationOption.CONNECTION_MAX_IDLE_TIMEOUT).toMillis(); - this.proxyOptions = buildProxyOptions(builder.proxyConfiguration); + this.proxyOptions = buildProxyOptions(builder.proxyConfiguration, tlsContext); } } - private HttpMonitoringOptions revolveHttpMonitoringOptions(ConnectionHealthConfiguration config) { - if (config == null) { - return null; - } - - HttpMonitoringOptions httpMonitoringOptions = new HttpMonitoringOptions(); - httpMonitoringOptions.setMinThroughputBytesPerSecond(config.minimumThroughputInBps()); - int seconds = (int) config.minimumThroughputTimeout().getSeconds(); - httpMonitoringOptions.setAllowableThroughputFailureIntervalSeconds(seconds); - return httpMonitoringOptions; - } - - private HttpProxyOptions buildProxyOptions(ProxyConfiguration proxyConfiguration) { - if (proxyConfiguration == null) { - return null; - } - - HttpProxyOptions clientProxyOptions = new HttpProxyOptions(); - - clientProxyOptions.setHost(proxyConfiguration.host()); - clientProxyOptions.setPort(proxyConfiguration.port()); - - if ("https".equalsIgnoreCase(proxyConfiguration.scheme())) { - clientProxyOptions.setTlsContext(tlsContext); - } - - if (proxyConfiguration.username() != null && proxyConfiguration.password() != null) { - clientProxyOptions.setAuthorizationUsername(proxyConfiguration.username()); - clientProxyOptions.setAuthorizationPassword(proxyConfiguration.password()); - clientProxyOptions.setAuthorizationType(HttpProxyOptions.HttpProxyAuthorizationType.Basic); - } else { - clientProxyOptions.setAuthorizationType(HttpProxyOptions.HttpProxyAuthorizationType.None); - } - - return clientProxyOptions; - } - - private SocketOptions buildSocketOptions(DefaultBuilder builder, AttributeMap config) { - SocketOptions clientSocketOptions = new SocketOptions(); - - Duration connectionTimeout = config.get(SdkHttpConfigurationOption.CONNECTION_TIMEOUT); - if (connectionTimeout != null) { - clientSocketOptions.connectTimeoutMs = NumericUtils.saturatedCast(connectionTimeout.toMillis()); - } - - TcpKeepAliveConfiguration tcpKeepAliveConfiguration = builder.tcpKeepAliveConfiguration; - if (tcpKeepAliveConfiguration != null) { - clientSocketOptions.keepAliveIntervalSecs = - NumericUtils.saturatedCast(tcpKeepAliveConfiguration.keepAliveInterval().getSeconds()); - clientSocketOptions.keepAliveTimeoutSecs = - NumericUtils.saturatedCast(tcpKeepAliveConfiguration.keepAliveTimeout().getSeconds()); - - } - - return clientSocketOptions; - } - /** * Marks a Native CrtResource as owned by the current Java Object. * @@ -418,6 +365,22 @@ Builder connectionHealthConfiguration(Consumer tcpKeepAliveConfigurationBuilder); + + /** + * Configure whether to enable a hybrid post-quantum key exchange option for the Transport Layer Security (TLS) network + * encryption protocol when communicating with services that support Post Quantum TLS. If Post Quantum cipher suites are + * not supported on the platform, the SDK will use the default TLS cipher suites. + * + *

+ * See Using hybrid post-quantum TLS with AWS KMS + * + *

+ * It's disabled by default. + * + * @param postQuantumTlsEnabled whether to prefer Post Quantum TLS + * @return The builder of the method chaining. + */ + Builder postQuantumTlsEnabled(Boolean postQuantumTlsEnabled); } /** @@ -430,6 +393,7 @@ private static final class DefaultBuilder implements Builder { private ProxyConfiguration proxyConfiguration; private ConnectionHealthConfiguration connectionHealthConfiguration; private TcpKeepAliveConfiguration tcpKeepAliveConfiguration; + private Boolean postQuantumTlsEnabled; private DefaultBuilder() { } @@ -509,6 +473,12 @@ public Builder tcpKeepAliveConfiguration(Consumer proxyConfigurationBuilderConsumer) { ProxyConfiguration.Builder builder = ProxyConfiguration.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 new file mode 100644 index 000000000000..d0784a7b2bf4 --- /dev/null +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java @@ -0,0 +1,113 @@ +/* + * 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.internal; + + +import java.time.Duration; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.crt.http.HttpMonitoringOptions; +import software.amazon.awssdk.crt.http.HttpProxyOptions; +import software.amazon.awssdk.crt.io.SocketOptions; +import software.amazon.awssdk.crt.io.TlsCipherPreference; +import software.amazon.awssdk.crt.io.TlsContext; +import software.amazon.awssdk.http.crt.AwsCrtAsyncHttpClient; +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.utils.Logger; +import software.amazon.awssdk.utils.NumericUtils; + +@SdkInternalApi +public final class AwsCrtConfigurationUtils { + private static final Logger log = Logger.loggerFor(AwsCrtAsyncHttpClient.class); + + private AwsCrtConfigurationUtils() { + } + + public static SocketOptions buildSocketOptions(TcpKeepAliveConfiguration tcpKeepAliveConfiguration, + Duration connectionTimeout) { + SocketOptions clientSocketOptions = new SocketOptions(); + + if (connectionTimeout != null) { + clientSocketOptions.connectTimeoutMs = NumericUtils.saturatedCast(connectionTimeout.toMillis()); + } + + if (tcpKeepAliveConfiguration != null) { + clientSocketOptions.keepAliveIntervalSecs = + NumericUtils.saturatedCast(tcpKeepAliveConfiguration.keepAliveInterval().getSeconds()); + clientSocketOptions.keepAliveTimeoutSecs = + NumericUtils.saturatedCast(tcpKeepAliveConfiguration.keepAliveTimeout().getSeconds()); + + } + + return clientSocketOptions; + } + + public static HttpProxyOptions buildProxyOptions(ProxyConfiguration proxyConfiguration, TlsContext tlsContext) { + if (proxyConfiguration == null) { + return null; + } + + HttpProxyOptions clientProxyOptions = new HttpProxyOptions(); + + clientProxyOptions.setHost(proxyConfiguration.host()); + clientProxyOptions.setPort(proxyConfiguration.port()); + + if ("https".equalsIgnoreCase(proxyConfiguration.scheme())) { + clientProxyOptions.setTlsContext(tlsContext); + } + + if (proxyConfiguration.username() != null && proxyConfiguration.password() != null) { + clientProxyOptions.setAuthorizationUsername(proxyConfiguration.username()); + clientProxyOptions.setAuthorizationPassword(proxyConfiguration.password()); + clientProxyOptions.setAuthorizationType(HttpProxyOptions.HttpProxyAuthorizationType.Basic); + } else { + clientProxyOptions.setAuthorizationType(HttpProxyOptions.HttpProxyAuthorizationType.None); + } + + return clientProxyOptions; + } + + public static HttpMonitoringOptions resolveHttpMonitoringOptions(ConnectionHealthConfiguration config) { + if (config == null) { + return null; + } + + HttpMonitoringOptions httpMonitoringOptions = new HttpMonitoringOptions(); + httpMonitoringOptions.setMinThroughputBytesPerSecond(config.minimumThroughputInBps()); + int seconds = (int) config.minimumThroughputTimeout().getSeconds(); + httpMonitoringOptions.setAllowableThroughputFailureIntervalSeconds(seconds); + return httpMonitoringOptions; + } + + public static TlsCipherPreference resolveCipherPreference(Boolean postQuantumTlsEnabled) { + TlsCipherPreference defaultTls = TlsCipherPreference.TLS_CIPHER_SYSTEM_DEFAULT; + if (postQuantumTlsEnabled == null || !postQuantumTlsEnabled) { + return defaultTls; + } + + // TODO: change this to the new PQ TLS Policy that stays up to date when it's ready + TlsCipherPreference pqTls = TlsCipherPreference.TLS_CIPHER_PREF_PQ_TLSv1_0_2021_05; + if (!pqTls.isSupported()) { + log.warn(() -> "Hybrid post-quantum cipher suites are not supported on this platform. The SDK will use the system " + + "default cipher suites instead"); + return defaultTls; + } + + return pqTls; + } + +} 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 new file mode 100644 index 000000000000..cfd28a69ca7e --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java @@ -0,0 +1,54 @@ +/* + * 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.internal; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static software.amazon.awssdk.crt.io.TlsCipherPreference.TLS_CIPHER_PREF_PQ_TLSv1_0_2021_05; +import static software.amazon.awssdk.crt.io.TlsCipherPreference.TLS_CIPHER_SYSTEM_DEFAULT; + +import java.util.stream.Stream; +import org.junit.jupiter.api.Assumptions; +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; + +class AwsCrtConfigurationUtilsTest { + + @ParameterizedTest + @MethodSource("cipherPreferences") + void resolveCipherPreference_pqNotSupported_shouldFallbackToSystemDefault(Boolean preferPqTls, + TlsCipherPreference tlsCipherPreference) { + Assumptions.assumeFalse(TLS_CIPHER_PREF_PQ_TLSv1_0_2021_05.isSupported()); + assertThat(AwsCrtConfigurationUtils.resolveCipherPreference(preferPqTls)).isEqualTo(tlsCipherPreference); + } + + @Test + void resolveCipherPreference_pqSupported_shouldHonor() { + Assumptions.assumeTrue(TLS_CIPHER_PREF_PQ_TLSv1_0_2021_05.isSupported()); + assertThat(AwsCrtConfigurationUtils.resolveCipherPreference(true)).isEqualTo(TLS_CIPHER_PREF_PQ_TLSv1_0_2021_05); + } + + private static Stream cipherPreferences() { + return Stream.of( + Arguments.of(null, TLS_CIPHER_SYSTEM_DEFAULT), + Arguments.of(false, TLS_CIPHER_SYSTEM_DEFAULT), + Arguments.of(true, TLS_CIPHER_SYSTEM_DEFAULT) + ); + } + +}