Skip to content

Commit

Permalink
Allow clients to customize s3mock server TLS certificate
Browse files Browse the repository at this point in the history
This enables to use a regular S3 client with full TLS verification
  • Loading branch information
vlsi committed Jul 22, 2020
1 parent 2c51c4e commit bb15d7e
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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.
*/
Expand Down Expand Up @@ -163,8 +192,14 @@ public static S3MockApplication start(final Map<String, Object> properties,
final String... args) {

final Map<String, Object> 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;

Expand Down
7 changes: 3 additions & 4 deletions server/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -227,6 +227,11 @@ public abstract static class BaseBuilder<T extends S3MockStarter> {

protected final Map<String, Object> arguments = new HashMap<>();

public BaseBuilder<T> withProperty(String name, String value) {
arguments.put(name, value);
return this;
}

public BaseBuilder<T> withInitialBuckets(final String... initialBuckets) {
arguments.put(S3MockApplication.PROP_INITIAL_BUCKETS, join(",", initialBuckets));
return this;
Expand All @@ -252,6 +257,24 @@ public BaseBuilder<T> 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<T> 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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 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.
*
* <p>The certificate was created with {@code keytool -genkey -keyalg RSA -alias customcert
* -keystore customcert.jks -storepass qwerty -keysize 2048 -ext san=dns:localhost}</p>
*/
@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;
}
}
Binary file not shown.

0 comments on commit bb15d7e

Please sign in to comment.