diff --git a/.changes/next-release/feature-AWSCRTHTTPClient-9fe7b47.json b/.changes/next-release/feature-AWSCRTHTTPClient-9fe7b47.json new file mode 100644 index 000000000000..a10caa951bc3 --- /dev/null +++ b/.changes/next-release/feature-AWSCRTHTTPClient-9fe7b47.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS CRT HTTP Client", + "contributor": "", + "description": "Add `useTrustStoreSystemProperties` to `AwsCrtHttpClient.Builder` and `AwsCrtAsyncHttpClient.Builder`. When enabled, the client trusts the certificates in the trust store specified by the `javax.net.ssl.trustStore*` JSSE system properties instead of the OS trust store. Disabled by default." +} diff --git a/http-clients/aws-crt-client/pom.xml b/http-clients/aws-crt-client/pom.xml index f9be3a7dc59f..1adb6d8a588e 100644 --- a/http-clients/aws-crt-client/pom.xml +++ b/http-clients/aws-crt-client/pom.xml @@ -133,6 +133,18 @@ ${awsjavasdk.version} test + + org.bouncycastle + bcpkix-jdk18on + ${bouncycastle-bcpkix.version} + test + + + org.bouncycastle + bcprov-jdk18on + ${bouncycastle-bcprov.version} + test + software.amazon.awssdk sdk-core 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 79a150eabab8..f8a653685160 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 @@ -305,6 +305,31 @@ AwsCrtAsyncHttpClient.Builder tcpKeepAliveConfiguration(Consumer + * It's disabled by default, and the client trusts the OS trust store even when these properties are set. When enabled, + * the properties are read once, when the client is built. If {@code javax.net.ssl.trustStore} is not set, the client + * trusts the OS trust store. The trust store also applies to the TLS connection to an HTTPS proxy. The trust store is not + * read when {@link SdkHttpConfigurationOption#TRUST_ALL_CERTIFICATES} is enabled. + * + *

+ * Unlike the Apache, Netty, and URL connection HTTP clients, which use the JDK's TLS implementation, building the client + * fails with an {@link IllegalStateException} if the trust store cannot be loaded (for example, the file does not exist, + * the password is wrong, or the store contains no certificates or more than 1024 certificates) or if + * {@code javax.net.ssl.trustStore} is {@code NONE}, instead of falling back to a default trust store. Certificates are + * validated by the native TLS implementation, so JDK validation settings such as + * {@code jdk.certpath.disabledAlgorithms} do not apply. + * + * @param useTrustStoreSystemProperties whether to use the trust store specified by the JSSE system properties; + * {@code null} reverts to the default ({@code false}) + * @return The builder of the method chaining. + */ + AwsCrtAsyncHttpClient.Builder useTrustStoreSystemProperties(Boolean useTrustStoreSystemProperties); } /** 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 9fe64121b2ca..fad99ffc7f80 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 @@ -353,6 +353,31 @@ AwsCrtHttpClient.Builder tcpKeepAliveConfiguration(Consumer + * It's disabled by default, and the client trusts the OS trust store even when these properties are set. When enabled, + * the properties are read once, when the client is built. If {@code javax.net.ssl.trustStore} is not set, the client + * trusts the OS trust store. The trust store also applies to the TLS connection to an HTTPS proxy. The trust store is not + * read when {@link SdkHttpConfigurationOption#TRUST_ALL_CERTIFICATES} is enabled. + * + *

+ * Unlike the Apache, Netty, and URL connection HTTP clients, which use the JDK's TLS implementation, building the client + * fails with an {@link IllegalStateException} if the trust store cannot be loaded (for example, the file does not exist, + * the password is wrong, or the store contains no certificates or more than 1024 certificates) or if + * {@code javax.net.ssl.trustStore} is {@code NONE}, instead of falling back to a default trust store. Certificates are + * validated by the native TLS implementation, so JDK validation settings such as + * {@code jdk.certpath.disabledAlgorithms} do not apply. + * + * @param useTrustStoreSystemProperties whether to use the trust store specified by the JSSE system properties; + * {@code null} reverts to the default ({@code false}) + * @return The builder of the method chaining. + */ + AwsCrtHttpClient.Builder useTrustStoreSystemProperties(Boolean useTrustStoreSystemProperties); } /** 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 703a196dd102..31c4de353327 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 @@ -19,6 +19,7 @@ import static software.amazon.awssdk.http.SdkHttpConfigurationOption.PROTOCOL; 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.overrideTrustStoreFromSystemProperties; 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.http.crt.internal.AwsCrtConfigurationUtils.resolveMonitoringOptions; @@ -112,16 +113,21 @@ abstract class AwsCrtHttpClientBase implements SdkAutoCloseable { clientBootstrap = new ClientBootstrap(eventLoopGroup, null); clientSocketOptions = buildSocketOptions(builder.getTcpKeepAliveConfiguration(), config.get(SdkHttpConfigurationOption.CONNECTION_TIMEOUT)); + boolean trustAllCertificates = config.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES); clientTlsContextOptions = TlsContextOptions.createDefaultClient() .withCipherPreference(resolveCipherPreference(builder.getPostQuantumTlsEnabled())) .withMinimumTlsVersion(resolveMinTlsVersion(builder.getMinTlsVersion())) - .withVerifyPeer(!config.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES)); + .withVerifyPeer(!trustAllCertificates); this.protocol = config.get(PROTOCOL); if (protocol == Protocol.HTTP2) { clientTlsContextOptions = clientTlsContextOptions.withAlpnList("h2"); } + if (Boolean.TRUE.equals(builder.getUseTrustStoreSystemProperties()) && !trustAllCertificates) { + overrideTrustStoreFromSystemProperties(clientTlsContextOptions); + } + this.tlsContextOptions = registerOwnedResource(clientTlsContextOptions); clientTlsContext = new TlsContext(clientTlsContextOptions); 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 eff0e8a6d874..30e2bdd446c1 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 @@ -36,6 +36,7 @@ public class AwsCrtClientBuilderBase { private Boolean postQuantumTlsEnabled; private TlsVersion minTlsVersion; private Integer numEventLoopThreads; + private Boolean useTrustStoreSystemProperties; protected AwsCrtClientBuilderBase() { } @@ -165,6 +166,15 @@ public TlsVersion getMinTlsVersion() { return this.minTlsVersion; } + public BuilderT useTrustStoreSystemProperties(Boolean useTrustStoreSystemProperties) { + this.useTrustStoreSystemProperties = useTrustStoreSystemProperties; + return thisBuilder(); + } + + public Boolean getUseTrustStoreSystemProperties() { + return this.useTrustStoreSystemProperties; + } + 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 ce03372ae257..a78f40862a72 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 @@ -18,6 +18,7 @@ import java.time.Duration; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.crt.CrtRuntimeException; import software.amazon.awssdk.crt.http.HttpMonitoringOptions; import software.amazon.awssdk.crt.io.SocketOptions; import software.amazon.awssdk.crt.io.TlsCipherPreference; @@ -38,6 +39,8 @@ public final class AwsCrtConfigurationUtils { // CRT rejects a throughput-failure interval below two seconds (HttpMonitoringOptions). private static final int MIN_MONITORING_FAILURE_INTERVAL_SECONDS = 2; + private static final String TRUST_STORE_PROPERTY = "javax.net.ssl.trustStore"; + private AwsCrtConfigurationUtils() { } @@ -142,4 +145,16 @@ public static TlsContextOptions.TlsVersions resolveMinTlsVersion(TlsVersion minT } } + /** + * Replaces the OS trust store in {@code tlsContextOptions} with the trust store specified by the JSSE trust store system + * properties. The CRT keeps the OS trust store when {@code javax.net.ssl.trustStore} is unset or empty. + */ + public static void overrideTrustStoreFromSystemProperties(TlsContextOptions tlsContextOptions) { + try { + tlsContextOptions.overrideDefaultTrustStoreFromJavaSystemProperties(); + } catch (CrtRuntimeException | IllegalArgumentException e) { + throw new IllegalStateException("Unable to load the trust store specified by the " + TRUST_STORE_PROPERTY + + " system property: " + e.getMessage(), e); + } + } } diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientTrustStoreSystemPropertiesTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientTrustStoreSystemPropertiesTest.java new file mode 100644 index 000000000000..ed46378814e3 --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientTrustStoreSystemPropertiesTest.java @@ -0,0 +1,72 @@ +/* + * 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 static software.amazon.awssdk.http.SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES; +import static software.amazon.awssdk.http.crt.CrtHttpClientTestUtils.createRequest; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import software.amazon.awssdk.http.RecordingResponseHandler; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.utils.AttributeMap; + +class AwsCrtAsyncHttpClientTrustStoreSystemPropertiesTest + extends CrtTrustStoreSystemPropertiesTestSuite { + + @Override + protected SdkAsyncHttpClient buildClient(Boolean useTrustStoreSystemProperties) { + return AwsCrtAsyncHttpClient.builder() + .useTrustStoreSystemProperties(useTrustStoreSystemProperties) + .connectionAcquisitionTimeout(CONNECTION_ACQUISITION_TIMEOUT) + .build(); + } + + @Override + protected SdkAsyncHttpClient buildClientTrustingAllCertificates(Boolean useTrustStoreSystemProperties) { + return AwsCrtAsyncHttpClient.builder() + .useTrustStoreSystemProperties(useTrustStoreSystemProperties) + .buildWithDefaults(AttributeMap.builder().put(TRUST_ALL_CERTIFICATES, true).build()); + } + + @Override + protected SdkAsyncHttpClient buildClientWithPrivateEventLoopGroup(Boolean useTrustStoreSystemProperties) { + return AwsCrtAsyncHttpClient.builder() + .numEventLoopThreads(2) + .useTrustStoreSystemProperties(useTrustStoreSystemProperties) + .build(); + } + + @Override + protected GetResponse executeGet(SdkAsyncHttpClient client, URI uri) throws Exception { + RecordingResponseHandler recorder = new RecordingResponseHandler(); + CompletableFuture execution = client.execute(AsyncExecuteRequest.builder() + .request(createRequest(uri)) + .requestContentPublisher(new EmptyPublisher()) + .responseHandler(recorder) + .build()); + try { + execution.get(30, TimeUnit.SECONDS); + recorder.completeFuture().get(30, TimeUnit.SECONDS); + } catch (ExecutionException e) { + throw (Exception) e.getCause(); + } + return new GetResponse(recorder.responses().get(0).statusCode(), recorder.fullResponseAsString()); + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientTrustStoreSystemPropertiesTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientTrustStoreSystemPropertiesTest.java new file mode 100644 index 000000000000..bb8242ced3e1 --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientTrustStoreSystemPropertiesTest.java @@ -0,0 +1,64 @@ +/* + * 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 static software.amazon.awssdk.http.SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES; +import static software.amazon.awssdk.http.crt.CrtHttpClientTestUtils.createRequest; + +import java.net.URI; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.utils.AttributeMap; +import software.amazon.awssdk.utils.IoUtils; + +class AwsCrtHttpClientTrustStoreSystemPropertiesTest extends CrtTrustStoreSystemPropertiesTestSuite { + + @Override + protected SdkHttpClient buildClient(Boolean useTrustStoreSystemProperties) { + return AwsCrtHttpClient.builder() + .useTrustStoreSystemProperties(useTrustStoreSystemProperties) + .connectionAcquisitionTimeout(CONNECTION_ACQUISITION_TIMEOUT) + .build(); + } + + @Override + protected SdkHttpClient buildClientTrustingAllCertificates(Boolean useTrustStoreSystemProperties) { + return AwsCrtHttpClient.builder() + .useTrustStoreSystemProperties(useTrustStoreSystemProperties) + .buildWithDefaults(AttributeMap.builder().put(TRUST_ALL_CERTIFICATES, true).build()); + } + + @Override + protected SdkHttpClient buildClientWithPrivateEventLoopGroup(Boolean useTrustStoreSystemProperties) { + return AwsCrtHttpClient.builder() + .numEventLoopThreads(2) + .useTrustStoreSystemProperties(useTrustStoreSystemProperties) + .build(); + } + + @Override + protected GetResponse executeGet(SdkHttpClient client, URI uri) throws Exception { + HttpExecuteResponse response = client.prepareRequest(HttpExecuteRequest.builder() + .request(createRequest(uri)) + .build()) + .call(); + try (AbortableInputStream body = response.responseBody().orElseThrow(IllegalStateException::new)) { + return new GetResponse(response.httpResponse().statusCode(), IoUtils.toUtf8String(body)); + } + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/CrtTrustStoreSystemPropertiesTestSuite.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/CrtTrustStoreSystemPropertiesTestSuite.java new file mode 100644 index 000000000000..138a3af50f8b --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/CrtTrustStoreSystemPropertiesTestSuite.java @@ -0,0 +1,432 @@ +/* + * 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 static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.time.temporal.ChronoUnit.DAYS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.http.crt.CrtHttpClientTestUtils.liveEventLoopGroups; +import static software.amazon.awssdk.http.crt.CrtHttpClientTestUtils.newEventLoopGroups; + +import com.github.tomakehurst.wiremock.WireMockServer; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +import javax.net.ssl.SSLHandshakeException; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +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 org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import software.amazon.awssdk.crt.CrtResource; +import software.amazon.awssdk.crt.CrtRuntimeException; +import software.amazon.awssdk.crt.http.HttpException; +import software.amazon.awssdk.utils.SdkAutoCloseable; + +/** + * Verifies {@code useTrustStoreSystemProperties} against an HTTPS server whose self-signed certificate is not in the OS trust + * store, so a request succeeds only when the client trusts the trust store named by the JSSE system properties. + */ +abstract class CrtTrustStoreSystemPropertiesTestSuite { + private static final String TRUST_STORE = "javax.net.ssl.trustStore"; + private static final String TRUST_STORE_PASSWORD = "javax.net.ssl.trustStorePassword"; + private static final String TRUST_STORE_TYPE = "javax.net.ssl.trustStoreType"; + private static final String TRUST_STORE_PROVIDER = "javax.net.ssl.trustStoreProvider"; + private static final String[] TRUST_STORE_PROPERTIES = {TRUST_STORE, TRUST_STORE_PASSWORD, TRUST_STORE_TYPE, + TRUST_STORE_PROVIDER}; + private static final String PASSWORD = "changeit"; + private static final String STUB_BODY = "ok"; + // The CRT accepts at most 1024 certificates in a trust store, the s2n default chain limit. + private static final int MAX_TRUST_STORE_CERTIFICATES = 1024; + // Well above a loopback handshake (milliseconds) and well below s2n's 10s minimum blinding delay. + protected static final Duration CONNECTION_ACQUISITION_TIMEOUT = Duration.ofSeconds(2); + // AWS_ERROR_HTTP_CONNECTION_MANAGER_ACQUISITION_TIMEOUT + private static final int CONNECTION_ACQUISITION_TIMEOUT_ERROR = 2093; + + private static Path tempDir; + private static WireMockServer server; + private static Path serverKeyStore; + private static Path serverCertTrustStore; + private static Path serverCertJksTrustStore; + private static Path unrelatedCertTrustStore; + private static Path emptyTrustStore; + private static Path corruptTrustStore; + private static Path oversizedTrustStore; + + private final Map savedProperties = new HashMap<>(); + + protected abstract T buildClient(Boolean useTrustStoreSystemProperties); + + protected abstract T buildClientTrustingAllCertificates(Boolean useTrustStoreSystemProperties); + + protected abstract T buildClientWithPrivateEventLoopGroup(Boolean useTrustStoreSystemProperties); + + protected abstract GetResponse executeGet(T client, URI uri) throws Exception; + + @BeforeAll + static void setUpServer() throws Exception { + tempDir = Files.createTempDirectory("crt-truststore-test"); + KeyPair serverKeyPair = generateKeyPair(); + X509Certificate serverCert = selfSignedCertificate(serverKeyPair, "localhost", 1); + X509Certificate unrelatedCert = selfSignedCertificate(generateKeyPair(), "unrelated", 2); + + KeyStore keyStore = newKeyStore("PKCS12"); + keyStore.setKeyEntry("server", serverKeyPair.getPrivate(), PASSWORD.toCharArray(), new Certificate[] {serverCert}); + serverKeyStore = store(keyStore, "server.p12"); + + serverCertTrustStore = store(trustStoreOf("PKCS12", serverCert), "server-cert.p12"); + serverCertJksTrustStore = store(trustStoreOf("JKS", serverCert), "server-cert.jks"); + unrelatedCertTrustStore = store(trustStoreOf("PKCS12", unrelatedCert), "unrelated-cert.p12"); + emptyTrustStore = store(trustStoreOf("PKCS12"), "empty.p12"); + corruptTrustStore = Files.write(tempDir.resolve("corrupt.p12"), "not a keystore".getBytes(UTF_8)); + + KeyPair bulkKeyPair = generateKeyPair(); + X509Certificate[] bulkCerts = new X509Certificate[MAX_TRUST_STORE_CERTIFICATES + 1]; + for (int i = 0; i < bulkCerts.length; i++) { + bulkCerts[i] = selfSignedCertificate(bulkKeyPair, "bulk" + i, 100 + i); + } + oversizedTrustStore = store(trustStoreOf("PKCS12", bulkCerts), "oversized.p12"); + + server = new WireMockServer(wireMockConfig().dynamicPort() + .dynamicHttpsPort() + .keystorePath(serverKeyStore.toString()) + .keystorePassword(PASSWORD) + .keyManagerPassword(PASSWORD) + .keystoreType("PKCS12")); + server.start(); + server.stubFor(get(urlPathEqualTo("/")).willReturn(aResponse().withStatus(200).withBody(STUB_BODY))); + } + + @AfterAll + static void tearDownServer() throws IOException { + server.stop(); + try (Stream files = Files.list(tempDir)) { + for (Path file : (Iterable) files::iterator) { + Files.delete(file); + } + } + Files.delete(tempDir); + } + + @BeforeEach + void clearTrustStoreProperties() { + for (String property : TRUST_STORE_PROPERTIES) { + savedProperties.put(property, System.getProperty(property)); + System.clearProperty(property); + } + } + + @AfterEach + void restoreTrustStoreProperties() { + savedProperties.forEach((property, value) -> { + if (value == null) { + System.clearProperty(property); + } else { + System.setProperty(property, value); + } + }); + } + + @Test + void useTrustStoreSystemProperties_trustStoreHasServerCertificate_requestSucceeds() throws Exception { + setTrustStoreProperties(serverCertTrustStore, PASSWORD); + + try (T client = buildClient(true)) { + assertStubResponse(executeGet(client, serverUri())); + } + } + + @Test + void useTrustStoreSystemProperties_trustStoreHasServerKeyEntry_requestSucceeds() throws Exception { + setTrustStoreProperties(serverKeyStore, PASSWORD); + + try (T client = buildClient(true)) { + assertStubResponse(executeGet(client, serverUri())); + } + } + + @Test + void useTrustStoreSystemProperties_trustStoreTypeSet_requestSucceeds() throws Exception { + setTrustStoreProperties(serverCertJksTrustStore, PASSWORD); + System.setProperty(TRUST_STORE_TYPE, "JKS"); + + try (T client = buildClient(true)) { + assertStubResponse(executeGet(client, serverUri())); + } + } + + @Test + void useTrustStoreSystemProperties_trustStoreLacksServerCertificate_requestFailsHandshake() throws Exception { + setTrustStoreProperties(unrelatedCertTrustStore, PASSWORD); + + try (T client = buildClient(true)) { + assertHandshakeRejected(() -> executeGet(client, serverUri())); + } + } + + @ParameterizedTest + @NullSource + @ValueSource(booleans = false) + void useTrustStoreSystemProperties_notEnabled_ignoresTrustStoreProperties(Boolean useTrustStoreSystemProperties) + throws Exception { + setTrustStoreProperties(serverCertTrustStore, PASSWORD); + + try (T client = buildClient(useTrustStoreSystemProperties)) { + assertHandshakeRejected(() -> executeGet(client, serverUri())); + } + } + + // These cannot tell the OS trust store apart from another default such as the JDK cacerts, since neither contains the + // self-signed server certificate. They verify that build() succeeds and no store containing that certificate is loaded. + @Test + void useTrustStoreSystemProperties_trustStorePropertyUnset_buildSucceedsAndDoesNotTrustUnlistedCertificate() + throws Exception { + assertBuildSucceedsAndDoesNotTrustServerCertificate(); + } + + @Test + void useTrustStoreSystemProperties_trustStorePropertyEmpty_buildSucceedsAndDoesNotTrustUnlistedCertificate() + throws Exception { + System.setProperty(TRUST_STORE, ""); + + assertBuildSucceedsAndDoesNotTrustServerCertificate(); + } + + private void assertBuildSucceedsAndDoesNotTrustServerCertificate() throws Exception { + try (T client = buildClient(true)) { + assertHandshakeRejected(() -> executeGet(client, serverUri())); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("trustStoreLoadFailures") + void useTrustStoreSystemProperties_trustStoreLoadFails_buildThrowsIllegalStateExceptionNamingTrustStore( + String description, String trustStore, String password, String type, String provider, + Class expectedCause) { + setTrustStoreProperties(trustStore, password, type, provider); + + assertThatThrownBy(() -> buildClient(true)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(TRUST_STORE) + .hasMessageContaining(trustStore) + .hasCauseInstanceOf(expectedCause); + } + + private static Stream trustStoreLoadFailures() { + return Stream.of( + Arguments.of("file does not exist", missingTrustStore(), PASSWORD, null, null, CrtRuntimeException.class), + Arguments.of("wrong password", serverCertTrustStore.toString(), "wrong", null, null, CrtRuntimeException.class), + Arguments.of("corrupt file", corruptTrustStore.toString(), PASSWORD, null, null, CrtRuntimeException.class), + Arguments.of("type unavailable", serverCertTrustStore.toString(), PASSWORD, "NO-SUCH-TYPE", null, + CrtRuntimeException.class), + Arguments.of("provider unavailable", serverCertTrustStore.toString(), PASSWORD, null, "NoSuchProvider", + CrtRuntimeException.class), + Arguments.of("NONE", "NONE", null, null, null, IllegalArgumentException.class)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("trustStoreRejections") + void useTrustStoreSystemProperties_trustStoreRejected_buildThrowsIllegalStateException( + String description, String trustStore, Class expectedCause) { + setTrustStoreProperties(trustStore, PASSWORD, null, null); + + assertThatThrownBy(() -> buildClient(true)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(TRUST_STORE) + .hasCauseInstanceOf(expectedCause); + } + + private static Stream trustStoreRejections() { + return Stream.of( + Arguments.of("whitespace-only path", " ", CrtRuntimeException.class), + Arguments.of("no certificates", emptyTrustStore.toString(), IllegalArgumentException.class), + Arguments.of("more certificates than the CRT accepts", oversizedTrustStore.toString(), + IllegalArgumentException.class)); + } + + @Test + void useTrustStoreSystemProperties_trustStoreLoadFails_doesNotLeakPrivateEventLoopGroup() { + // Creates the shared static default event-loop group up front so the before/after diff only sees private groups. + buildClient(null).close(); + Set before = liveEventLoopGroups(); + System.setProperty(TRUST_STORE, missingTrustStore()); + + assertThatThrownBy(() -> buildClientWithPrivateEventLoopGroup(true)).isInstanceOf(IllegalStateException.class); + + assertThat(newEventLoopGroups(before)).isEmpty(); + } + + @Test + void useTrustStoreSystemProperties_trustAllCertificates_doesNotReadTrustStore() throws Exception { + System.setProperty(TRUST_STORE, missingTrustStore()); + + try (T client = buildClientTrustingAllCertificates(true)) { + assertStubResponse(executeGet(client, serverUri())); + } + } + + @Test + void useTrustStoreSystemProperties_propertiesChangedAfterBuild_keepsTrustStoreReadAtBuild() throws Exception { + setTrustStoreProperties(serverCertTrustStore, PASSWORD); + + try (T client = buildClient(true)) { + setTrustStoreProperties(unrelatedCertTrustStore, PASSWORD); + + assertStubResponse(executeGet(client, serverUri())); + } + } + + private static void assertStubResponse(GetResponse response) { + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body()).isEqualTo(STUB_BODY); + } + + // s2n reports a rejected certificate only after its 10-30s blinding delay, so the acquisition timeout can fire first. + private static void assertHandshakeRejected(ThrowingCallable request) { + assertThatThrownBy(request).satisfiesAnyOf( + e -> assertThat(e).isInstanceOf(SSLHandshakeException.class), + e -> { + assertThat(e).isInstanceOf(IOException.class).hasCauseInstanceOf(HttpException.class); + assertThat(((HttpException) e.getCause()).getErrorCode()).isEqualTo(CONNECTION_ACQUISITION_TIMEOUT_ERROR); + }); + } + + private static URI serverUri() { + return URI.create("https://localhost:" + server.httpsPort()); + } + + private static void setTrustStoreProperties(Path trustStore, String password) { + System.setProperty(TRUST_STORE, trustStore.toString()); + System.setProperty(TRUST_STORE_PASSWORD, password); + } + + private static String missingTrustStore() { + return tempDir.resolve("missing.p12").toString(); + } + + private static void setTrustStoreProperties(String trustStore, String password, String type, String provider) { + System.setProperty(TRUST_STORE, trustStore); + setIfNotNull(TRUST_STORE_PASSWORD, password); + setIfNotNull(TRUST_STORE_TYPE, type); + setIfNotNull(TRUST_STORE_PROVIDER, provider); + } + + private static void setIfNotNull(String property, String value) { + if (value != null) { + System.setProperty(property, value); + } + } + + private static KeyPair generateKeyPair() throws GeneralSecurityException { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + return generator.generateKeyPair(); + } + + private static X509Certificate selfSignedCertificate(KeyPair keyPair, String commonName, long serialNumber) + throws Exception { + Instant now = Instant.now(); + X500Name name = new X500Name("CN=" + commonName); + X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(name, BigInteger.valueOf(serialNumber), + Date.from(now.minus(1, DAYS)), + Date.from(now.plus(1, DAYS)), + name, keyPair.getPublic()); + builder.addExtension(Extension.subjectAlternativeName, false, + new GeneralNames(new GeneralName[] {new GeneralName(GeneralName.dNSName, "localhost"), + new GeneralName(GeneralName.iPAddress, "127.0.0.1")})); + builder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)); + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); + return new JcaX509CertificateConverter().getCertificate(builder.build(signer)); + } + + private static KeyStore newKeyStore(String type) throws Exception { + KeyStore keyStore = KeyStore.getInstance(type); + keyStore.load(null, null); + return keyStore; + } + + private static KeyStore trustStoreOf(String type, X509Certificate... certificates) throws Exception { + KeyStore keyStore = newKeyStore(type); + for (int i = 0; i < certificates.length; i++) { + keyStore.setCertificateEntry("cert" + i, certificates[i]); + } + return keyStore; + } + + private static Path store(KeyStore keyStore, String fileName) throws Exception { + Path file = tempDir.resolve(fileName); + try (OutputStream out = Files.newOutputStream(file)) { + keyStore.store(out, PASSWORD.toCharArray()); + } + return file; + } + + protected static final class GetResponse { + private final int statusCode; + private final String body; + + GetResponse(int statusCode, String body) { + this.statusCode = statusCode; + this.body = body; + } + + int statusCode() { + return statusCode; + } + + String body() { + return body; + } + } +} diff --git a/pom.xml b/pom.xml index f2a2e4715d19..e9692d9e0f77 100644 --- a/pom.xml +++ b/pom.xml @@ -130,7 +130,7 @@ 3.1.5 1.17.1 1.37 - 0.48.5 + 0.49.0 5.10.3 @@ -152,6 +152,9 @@ 1.3.0 1.5.4 2.0.3 + 1.80 + + 1.80.2 3.1.2 diff --git a/test/http-client-tests/pom.xml b/test/http-client-tests/pom.xml index f029dd6745b1..9dbfbd84c38e 100644 --- a/test/http-client-tests/pom.xml +++ b/test/http-client-tests/pom.xml @@ -126,7 +126,7 @@ org.bouncycastle bcpkix-jdk18on - 1.80 + ${bouncycastle-bcpkix.version} compile diff --git a/test/protocol-tests/pom.xml b/test/protocol-tests/pom.xml index 5d7e86b5fcfa..86863c6ca98e 100644 --- a/test/protocol-tests/pom.xml +++ b/test/protocol-tests/pom.xml @@ -294,7 +294,7 @@ org.bouncycastle bcpkix-jdk18on - 1.80 + ${bouncycastle-bcpkix.version} test