diff --git a/server/src/main/java/com/adobe/testing/s3mock/S3MockApplication.java b/server/src/main/java/com/adobe/testing/s3mock/S3MockApplication.java index 6b8f0eeae..3acc75e38 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/S3MockApplication.java +++ b/server/src/main/java/com/adobe/testing/s3mock/S3MockApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 Adobe. + * Copyright 2017-2020 Adobe. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,6 +82,11 @@ public class S3MockApplication { public static final int DEFAULT_HTTP_PORT = 9090; public static final int RANDOM_PORT = 0; + public static final String DEFAULT_SERVER_SSL_KEY_STORE = "classpath:s3mock.jks"; + public static final String DEFAULT_SERVER_SSL_KEY_STORE_PASSWORD = "password"; + public static final String DEFAULT_SERVER_SSL_KEY_ALIAS = "selfsigned"; + public static final String DEFAULT_SERVER_SSL_KEY_PASSWORD = "password"; + /** * Property name for passing a comma separated list of buckets that are to be created at startup. */ @@ -104,6 +109,30 @@ public class S3MockApplication { */ public static final String PROP_HTTP_PORT = "http.port"; + /** + * Property name for passing the path to the keystore to use. + * Defaults to {@value DEFAULT_SERVER_SSL_KEY_STORE}. + */ + public static final String SERVER_SSL_KEY_STORE = "server.ssl.key-store"; + + /** + * Property name for passing the password for the keystore. + * Defaults to {@value DEFAULT_SERVER_SSL_KEY_STORE_PASSWORD}. + */ + public static final String SERVER_SSL_KEY_STORE_PASSWORD = "server.ssl.key-store-password"; + + /** + * Property name for specifying the key to use. + * Defaults to {@value DEFAULT_SERVER_SSL_KEY_ALIAS}. + */ + public static final String SERVER_SSL_KEY_ALIAS = "server.ssl.key-alias"; + + /** + * Property name for passing the password for the key. + * Defaults to {@value DEFAULT_SERVER_SSL_KEY_PASSWORD}. + */ + public static final String SERVER_SSL_KEY_PASSWORD = "server.ssl.key-password"; + /** * Property name for using either HTTPS or HTTP connections. */ @@ -163,8 +192,14 @@ public static S3MockApplication start(final Map properties, final String... args) { final Map defaults = new HashMap<>(); - defaults.put(S3MockApplication.PROP_HTTPS_PORT, DEFAULT_HTTPS_PORT); - defaults.put(S3MockApplication.PROP_HTTP_PORT, DEFAULT_HTTP_PORT); + defaults.put(PROP_HTTPS_PORT, DEFAULT_HTTPS_PORT); + defaults.put(PROP_HTTP_PORT, DEFAULT_HTTP_PORT); + + // Specify the default SSL parameters here. Users can override them + defaults.put(SERVER_SSL_KEY_STORE, DEFAULT_SERVER_SSL_KEY_STORE); + defaults.put(SERVER_SSL_KEY_STORE_PASSWORD, DEFAULT_SERVER_SSL_KEY_STORE_PASSWORD); + defaults.put(SERVER_SSL_KEY_ALIAS, DEFAULT_SERVER_SSL_KEY_ALIAS); + defaults.put(SERVER_SSL_KEY_PASSWORD, DEFAULT_SERVER_SSL_KEY_PASSWORD); Banner.Mode bannerMode = Banner.Mode.CONSOLE; diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index b9341d33a..6c97ccbae 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -1,5 +1,5 @@ # -# Copyright 2017-2019 Adobe. +# Copyright 2017-2020 Adobe. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,9 +14,8 @@ # limitations under the License. # -server.ssl.key-store=classpath:s3mock.jks -server.ssl.key-store-password=password -server.ssl.key-password=password +# server.ssl.* are moved to S3MockApplication#start so users can override them +# The values in application.properties can't be overriden by SpringApplicationBuilder#properties logging.level.org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver=ERROR logging.level.org.eclipse.jetty.util.ssl.SslContextFactory.config=ERROR diff --git a/testsupport/common/src/main/java/com/adobe/testing/s3mock/testsupport/common/S3MockStarter.java b/testsupport/common/src/main/java/com/adobe/testing/s3mock/testsupport/common/S3MockStarter.java index 54f958a73..c65c4337f 100644 --- a/testsupport/common/src/main/java/com/adobe/testing/s3mock/testsupport/common/S3MockStarter.java +++ b/testsupport/common/src/main/java/com/adobe/testing/s3mock/testsupport/common/S3MockStarter.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 Adobe. + * Copyright 2017-2020 Adobe. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -227,6 +227,11 @@ public abstract static class BaseBuilder { protected final Map arguments = new HashMap<>(); + public BaseBuilder withProperty(String name, String value) { + arguments.put(name, value); + return this; + } + public BaseBuilder withInitialBuckets(final String... initialBuckets) { arguments.put(S3MockApplication.PROP_INITIAL_BUCKETS, join(",", initialBuckets)); return this; @@ -252,6 +257,24 @@ public BaseBuilder withSecureConnection(final boolean secureConnection) { return this; } + /** + * Configures SSL parameters for the mock server. + * @param keyStore value for server.ssl.key-store + * @param keyStorePassword value for server.ssl.key-store-password + * @param keyAlias value for server.ssl.key-alias + * @param keyPassword value for server.ssl.key-password + * @return this builder + */ + public BaseBuilder withSslParameters( + String keyStore, String keyStorePassword, String keyAlias, String keyPassword + ) { + arguments.put(S3MockApplication.SERVER_SSL_KEY_STORE, keyStore); + arguments.put(S3MockApplication.SERVER_SSL_KEY_STORE_PASSWORD, keyStorePassword); + arguments.put(S3MockApplication.SERVER_SSL_KEY_ALIAS, keyAlias); + arguments.put(S3MockApplication.SERVER_SSL_KEY_PASSWORD, keyPassword); + return this; + } + /** * Reduces logging level WARN and suppresses the startup banner. * diff --git a/testsupport/junit5/src/test/java/com/adobe/testing/s3mock/junit5/sdk1/CustomCertificateTest.java b/testsupport/junit5/src/test/java/com/adobe/testing/s3mock/junit5/sdk1/CustomCertificateTest.java new file mode 100644 index 000000000..1abb9ed08 --- /dev/null +++ b/testsupport/junit5/src/test/java/com/adobe/testing/s3mock/junit5/sdk1/CustomCertificateTest.java @@ -0,0 +1,115 @@ +/* + * Copyright 2017-2020 Adobe. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.adobe.testing.s3mock.junit5.sdk1; + +import com.adobe.testing.s3mock.junit5.S3MockExtension; +import com.amazonaws.ClientConfiguration; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import java.io.InputStream; +import java.security.KeyStore; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +/** + * Test ensures there's a way to configure user-provided certificate for S3Mock. + * + *

The certificate was created with {@code keytool -genkey -keyalg RSA -alias customcert + * -keystore customcert.jks -storepass qwerty -keysize 2048 -ext san=dns:localhost}

+ */ +@Execution(ExecutionMode.SAME_THREAD) +public class CustomCertificateTest { + private static final String KEYSTORE_FILE_NAME = "customcert.jks"; + private static final String KEYSTORE_PASSWORD = "qwerty"; + private static final String KEY_ALIAS = "customcert"; + + @RegisterExtension + public static final S3MockExtension s3mock = + S3MockExtension.builder() + .withSslParameters("classpath:" + KEYSTORE_FILE_NAME, KEYSTORE_PASSWORD, KEY_ALIAS, + KEYSTORE_PASSWORD) + .build(); + + @Test + void connectWithCustomSSLContext() throws Exception { + // We use regular Amazon S3 API to ensure it would be able to connect to S3 mock server + // with no hacks like "allow any certificate" + + // Note: we still have to configure ClientConfiguration as there's no reliable way + // to adjust the system-default TrustManager in the runtime. + // An alternative approach is to use javax.net.ssl.truststore properties + // at the Java process startup. + + AmazonS3 s3 = AmazonS3ClientBuilder.standard() + .withCredentials(new AWSStaticCredentialsProvider( + new BasicAWSCredentials("foo", "bar"))) + .withClientConfiguration(createClientConfiguration(KEYSTORE_FILE_NAME)) + .withEndpointConfiguration( + new AwsClientBuilder.EndpointConfiguration( + "https://localhost:" + s3mock.getPort(), + "us-west-1")) + .enablePathStyleAccess() + .build(); + + // Below is a smoke-test of the API. The point is to check if SSL connectivity works + String bucketName = "non-existent-bucket-to-verify-if-api-works"; + Assertions.assertFalse(s3.doesBucketExistV2(bucketName), + () -> "Bucket " + bucketName + " must not be present at the mock server"); + + s3.shutdown(); + } + + private static ClientConfiguration createClientConfiguration(String keystoreFileName) + throws Exception { + // It configures Apache Http Client to use our own trust store (==trusted certificate) + ClientConfiguration clientConfiguration = new ClientConfiguration(); + clientConfiguration.getApacheHttpClientConfig() + .setSslSocketFactory(new SSLConnectionSocketFactory(createSslContext(keystoreFileName))); + return clientConfiguration; + } + + /** + * Load a certificate from a given keystore and generate a {@code SSLContext} that trusts the + * certificate. + * + * @param keystoreFileName keystore name to use + * @return SSLContext + * @throws Exception in case something fails + */ + private static SSLContext createSslContext(String keystoreFileName) throws Exception { + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + try (InputStream jks = + CustomCertificateTest.class.getResourceAsStream("/" + keystoreFileName)) { + ks.load(jks, KEYSTORE_PASSWORD.toCharArray()); + } + TrustManagerFactory tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), null); + return sslContext; + } +} diff --git a/testsupport/junit5/src/test/resources/customcert.jks b/testsupport/junit5/src/test/resources/customcert.jks new file mode 100644 index 000000000..924e87f0a Binary files /dev/null and b/testsupport/junit5/src/test/resources/customcert.jks differ