diff --git a/.github/workflows/test-with-security.yml b/.github/workflows/test-with-security.yml new file mode 100644 index 00000000..e396e526 --- /dev/null +++ b/.github/workflows/test-with-security.yml @@ -0,0 +1,32 @@ +name: BWC Test Workflow +# This workflow is triggered on pull requests and pushes to main or an OpenSearch release branch +on: + pull_request: + branches: + - "*" + push: + branches: + - "*" + +jobs: + build: + strategy: + matrix: + java: [ 11 ] + # Job name + name: Build and test Job-scheduler + # This job runs on Linux + runs-on: ubuntu-latest + steps: + # This step uses the setup-java Github action: https://github.com/actions/setup-java + - name: Set Up JDK ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + # This step uses the checkout Github action: https://github.com/actions/checkout + - name: Checkout Branch + uses: actions/checkout@v4 + - name: Run Job-scheduler Integ Tests with Security + run: | + echo "Running integ tests with security..." + ./gradlew :integTest --tests "*RestIT" -Dhttps=true -Dsecurity=true -Dtests.opensearch.secure=true -Dtests.opensearch.username=admin -Dtests.opensearch.password=admin -Duser=admin -Dpassword=admin diff --git a/build.gradle b/build.gradle index f578d14b..939f7d08 100644 --- a/build.gradle +++ b/build.gradle @@ -5,11 +5,20 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin import org.opensearch.gradle.test.RestIntegTestTask +import java.util.concurrent.Callable + buildscript { ext { opensearch_version = System.getProperty("opensearch.version", "3.0.0-SNAPSHOT") isSnapshot = "true" == System.getProperty("build.snapshot", "true") buildVersionQualifier = System.getProperty("build.version_qualifier", "") + // 2.2.0-SNAPSHOT -> 2.2.0.0-SNAPSHOT + version_tokens = opensearch_version.tokenize('-') + opensearch_build = version_tokens[0] + '.0' + if (isSnapshot) { + opensearch_build += "-SNAPSHOT" + } + security_plugin_version = System.getProperty("security.version", opensearch_build) } repositories { @@ -58,6 +67,10 @@ opensearchplugin { classname 'org.opensearch.jobscheduler.JobSchedulerPlugin' } +configurations { + opensearchPlugin +} + javaRestTest { // add "-Dtests.security.manager=false" to VM options if you want to run integ tests in IntelliJ systemProperty 'tests.security.manager', 'false' @@ -168,6 +181,7 @@ dependencies { implementation('com.google.googlejavaformat:google-java-format:1.23.0') { exclude group: 'com.google.guava' } + opensearchPlugin "org.opensearch.plugin:opensearch-security:${security_plugin_version}@zip" } // RPM & Debian build @@ -230,6 +244,66 @@ integTest.getClusters().forEach{c -> { c.plugin(project.getObjects().fileProperty().value(bundle.getArchiveFile())) }} +ext.resolvePluginFile = { pluginId -> + return new Callable() { + @Override + RegularFile call() throws Exception { + return new RegularFile() { + @Override + File getAsFile() { + return configurations.opensearchPlugin.resolvedConfiguration.resolvedArtifacts + .find { ResolvedArtifact f -> + f.name.startsWith(pluginId) + } + .file + } + } + } + } +} +def securityPluginFile = resolvePluginFile("opensearch-security") + +// === Setup security test === +// This flag indicates the existence of security plugin +def securityEnabled = System.getProperty("security", "false") == "true" || System.getProperty("https", "false") == "true" +afterEvaluate { + testClusters.integTest.nodes.each { node -> + def plugins = node.plugins + def firstPlugin = plugins.get(0) + if (firstPlugin.provider == project.bundlePlugin.archiveFile) { + plugins.remove(0) + plugins.add(firstPlugin) + } + + if (securityEnabled) { + node.extraConfigFile("kirk.pem", file("src/test/resources/security/kirk.pem")) + node.extraConfigFile("kirk-key.pem", file("src/test/resources/security/kirk-key.pem")) + node.extraConfigFile("esnode.pem", file("src/test/resources/security/esnode.pem")) + node.extraConfigFile("esnode-key.pem", file("src/test/resources/security/esnode-key.pem")) + node.extraConfigFile("root-ca.pem", file("src/test/resources/security/root-ca.pem")) + node.setting("network.bind_host", "127.0.0.1") + node.setting("network.publish_host", "127.0.0.1") + node.setting("plugins.security.ssl.transport.pemcert_filepath", "esnode.pem") + node.setting("plugins.security.ssl.transport.pemkey_filepath", "esnode-key.pem") + node.setting("plugins.security.ssl.transport.pemtrustedcas_filepath", "root-ca.pem") + node.setting("plugins.security.ssl.transport.enforce_hostname_verification", "false") + node.setting("plugins.security.ssl.http.enabled", "true") + node.setting("plugins.security.ssl.http.pemcert_filepath", "esnode.pem") + node.setting("plugins.security.ssl.http.pemkey_filepath", "esnode-key.pem") + node.setting("plugins.security.ssl.http.pemtrustedcas_filepath", "root-ca.pem") + node.setting("plugins.security.allow_unsafe_democertificates", "true") + node.setting("plugins.security.allow_default_init_securityindex", "true") + node.setting("plugins.security.authcz.admin_dn", "\n - CN=kirk,OU=client,O=client,L=test,C=de") + node.setting("plugins.security.audit.type", "internal_opensearch") + node.setting("plugins.security.enable_snapshot_restore_privilege", "true") + node.setting("plugins.security.check_snapshot_restore_write_privileges", "true") + node.setting("plugins.security.restapi.roles_enabled", "[\"all_access\", \"security_rest_api_access\"]") + node.setting("plugins.security.system_indices.enabled", "true") + // node.setting("plugins.security.system_indices.indices", "[\".opendistro-ism-config\"]") + } + } +} + testClusters.integTest { testDistribution = 'INTEG_TEST' @@ -245,6 +319,10 @@ testClusters.integTest { debugPort += 1 } } + + if (securityEnabled) { + plugin(provider(securityPluginFile)) + } setting 'path.repo', repo.absolutePath } diff --git a/sample-extension-plugin/build.gradle b/sample-extension-plugin/build.gradle index 32760c5b..e8f655ac 100644 --- a/sample-extension-plugin/build.gradle +++ b/sample-extension-plugin/build.gradle @@ -6,6 +6,7 @@ apply plugin: 'opensearch.opensearchplugin' apply plugin: 'opensearch.testclusters' apply plugin: 'opensearch.java-rest-test' +apply plugin: 'opensearch.pluginzip' import org.opensearch.gradle.test.RestIntegTestTask import org.opensearch.gradle.testclusters.StandaloneRestIntegTestTask @@ -21,10 +22,23 @@ opensearchplugin { extendedPlugins = ['opensearch-job-scheduler'] } +configurations { + opensearchPlugin +} + ext { projectSubstitutions = [:] licenseFile = rootProject.file('LICENSE.txt') noticeFile = rootProject.file('NOTICE.txt') + opensearch_version = System.getProperty("opensearch.version", "3.0.0-SNAPSHOT") + isSnapshot = "true" == System.getProperty("build.snapshot", "true") + // 2.2.0-SNAPSHOT -> 2.2.0.0-SNAPSHOT + version_tokens = opensearch_version.tokenize('-') + opensearch_build = version_tokens[0] + '.0' + if (isSnapshot) { + opensearch_build += "-SNAPSHOT" + } + security_plugin_version = System.getProperty("security.version", opensearch_build) } repositories { @@ -110,6 +124,64 @@ integTest.getClusters().forEach{c -> { c.plugin(project.getObjects().fileProperty().value(bundle.getArchiveFile())) }} +ext.resolvePluginFile = { pluginId -> + return new Callable() { + @Override + RegularFile call() throws Exception { + return new RegularFile() { + @Override + File getAsFile() { + return configurations.opensearchPlugin.resolvedConfiguration.resolvedArtifacts + .find { ResolvedArtifact f -> + f.name.startsWith(pluginId) + } + .file + } + } + } + } +} +def securityPluginFile = resolvePluginFile("opensearch-security") + +// === Setup security test === +// This flag indicates the existence of security plugin +def securityEnabled = System.getProperty("security", "false") == "true" || System.getProperty("https", "false") == "true" +afterEvaluate { + testClusters.integTest.nodes.each { node -> + def plugins = node.plugins + def firstPlugin = plugins.get(0) + if (firstPlugin.provider == project.bundlePlugin.archiveFile) { + plugins.remove(0) + plugins.add(firstPlugin) + } + + if (securityEnabled) { + node.extraConfigFile("kirk.pem", file("src/test/resources/security/kirk.pem")) + node.extraConfigFile("kirk-key.pem", file("src/test/resources/security/kirk-key.pem")) + node.extraConfigFile("esnode.pem", file("src/test/resources/security/esnode.pem")) + node.extraConfigFile("esnode-key.pem", file("src/test/resources/security/esnode-key.pem")) + node.extraConfigFile("root-ca.pem", file("src/test/resources/security/root-ca.pem")) + node.setting("plugins.security.ssl.transport.pemcert_filepath", "esnode.pem") + node.setting("plugins.security.ssl.transport.pemkey_filepath", "esnode-key.pem") + node.setting("plugins.security.ssl.transport.pemtrustedcas_filepath", "root-ca.pem") + node.setting("plugins.security.ssl.transport.enforce_hostname_verification", "false") + node.setting("plugins.security.ssl.http.enabled", "true") + node.setting("plugins.security.ssl.http.pemcert_filepath", "esnode.pem") + node.setting("plugins.security.ssl.http.pemkey_filepath", "esnode-key.pem") + node.setting("plugins.security.ssl.http.pemtrustedcas_filepath", "root-ca.pem") + node.setting("plugins.security.allow_unsafe_democertificates", "true") + node.setting("plugins.security.allow_default_init_securityindex", "true") + node.setting("plugins.security.authcz.admin_dn", "\n - CN=kirk,OU=client,O=client,L=test,C=de") + node.setting("plugins.security.audit.type", "internal_opensearch") + node.setting("plugins.security.enable_snapshot_restore_privilege", "true") + node.setting("plugins.security.check_snapshot_restore_write_privileges", "true") + node.setting("plugins.security.restapi.roles_enabled", "[\"all_access\", \"security_rest_api_access\"]") + node.setting("plugins.security.system_indices.enabled", "true") + // node.setting("plugins.security.system_indices.indices", "[\".opendistro-ism-config\"]") + } + } +} + testClusters.integTest { testDistribution = 'INTEG_TEST' @@ -125,6 +197,10 @@ testClusters.integTest { debugPort += 1 } } + if (securityEnabled) { + plugin(provider(securityPluginFile)) + } + setting 'path.repo', repo.absolutePath } @@ -161,6 +237,9 @@ String bwcDownloadUrl = "https://aws.oss.sonatype.org/service/local/artifact/mav } } })) + if (securityEnabled) { + plugin(provider(securityPluginFile)) + } setting 'path.repo', "${buildDir}/cluster/shared/repo/${baseName}" setting 'http.content_type.required', 'true' } @@ -295,18 +374,6 @@ run { useCluster testClusters.integTest } -// As of ES 7.7 the sample-extension-plugin is being added to the list of plugins for the testCluster during build before -// the job-scheduler plugin is causing build failures. -// The job-scheduler zip is added explicitly above but the sample-extension-plugin is added implicitly at some time during evaluation. -// Will need to do a deep dive to find out exactly what task adds the sample-extension-plugin and add job-scheduler there but a temporary hack is to -// reorder the plugins list after evaluation but prior to task execution when the plugins are installed. -afterEvaluate { - testClusters.javaRestTest.nodes.each { node -> - def nodePlugins = node.plugins - def firstPlugin = nodePlugins.get(0) - if (firstPlugin.provider == project.bundlePlugin.archiveFile) { - nodePlugins.remove(0) - nodePlugins.add(firstPlugin) - } - } +dependencies { + opensearchPlugin "org.opensearch.plugin:opensearch-security:${security_plugin_version}@zip" } diff --git a/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/SampleExtensionIntegTestCase.java b/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/SampleExtensionIntegTestCase.java index 3bca9556..e4bcaec5 100644 --- a/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/SampleExtensionIntegTestCase.java +++ b/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/SampleExtensionIntegTestCase.java @@ -8,17 +8,32 @@ */ package org.opensearch.jobscheduler.sampleextension; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.core5.function.Factory; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.reactor.ssl.TlsDetails; +import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.apache.hc.core5.util.Timeout; import org.junit.Assert; import org.opensearch.client.Request; import org.opensearch.client.RequestOptions; import org.opensearch.client.Response; import org.opensearch.client.RestClient; +import org.opensearch.client.RestClientBuilder; import org.opensearch.client.WarningsHandler; +import org.opensearch.common.io.PathUtils; import org.opensearch.common.settings.Settings; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.common.xcontent.LoggingDeprecationHandler; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.common.xcontent.json.JsonXContent; @@ -26,19 +41,138 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.test.rest.OpenSearchRestTestCase; +import javax.net.ssl.SSLEngine; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Timer; import java.util.TimerTask; public class SampleExtensionIntegTestCase extends OpenSearchRestTestCase { + protected boolean isHttps() { + boolean isHttps = Optional.ofNullable(System.getProperty("https")).map("true"::equalsIgnoreCase).orElse(false); + if (isHttps) { + // currently only external cluster is supported for security enabled testing + if (!Optional.ofNullable(System.getProperty("tests.rest.cluster")).isPresent()) { + throw new RuntimeException("cluster url should be provided for security enabled testing"); + } + } + + return isHttps; + } + + @Override + protected String getProtocol() { + return isHttps() ? "https" : "http"; + } + + @Override + protected Settings restAdminSettings() { + return Settings.builder() + .put("http.port", 9200) + .put("plugins.security.ssl.http.enabled", isHttps()) + .put("plugins.security.ssl.http.pemcert_filepath", "sample.pem") + .put("plugins.security.ssl.http.keystore_filepath", "test-kirk.jks") + .put("plugins.security.ssl.http.keystore_password", "changeit") + .build(); + // return Settings.builder().put("strictDeprecationMode", false).put("http.port", 9200).build(); + } + + @Override + protected RestClient buildClient(Settings settings, HttpHost[] hosts) throws IOException { + boolean strictDeprecationMode = settings.getAsBoolean("strictDeprecationMode", true); + RestClientBuilder builder = RestClient.builder(hosts); + if (isHttps()) { + String keystore = settings.get("plugins.security.ssl.http.keystore_filepath"); + if (Objects.nonNull(keystore)) { + URI uri = null; + try { + uri = this.getClass().getClassLoader().getResource("security/sample.pem").toURI(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + Path configPath = PathUtils.get(uri).getParent().toAbsolutePath(); + return new SecureRestClientBuilder(settings, configPath, hosts).build(); + } else { + configureHttpsClient(builder, settings); + builder.setStrictDeprecationMode(strictDeprecationMode); + return builder.build(); + } + } else { + configureClient(builder, settings); + builder.setStrictDeprecationMode(strictDeprecationMode); + return builder.build(); + } + + } + + protected static void configureHttpsClient(RestClientBuilder builder, Settings settings) throws IOException { + Map headers = new HashMap<>(ThreadContext.buildDefaultHeaders(settings)); + String userName = Optional.ofNullable(System.getProperty("user")).orElseThrow(() -> new RuntimeException("user name is missing")); + String password = Optional.ofNullable(System.getProperty("password")) + .orElseThrow(() -> new RuntimeException("password is missing")); + + headers.put( + "Authorization", + "Basic " + Base64.getEncoder().encodeToString((userName + ":" + password).getBytes(StandardCharsets.UTF_8)) + ); + Header[] defaultHeaders = new Header[headers.size()]; + int i = 0; + for (Map.Entry entry : headers.entrySet()) { + defaultHeaders[i++] = new BasicHeader(entry.getKey(), entry.getValue()); + } + builder.setDefaultHeaders(defaultHeaders); + builder.setHttpClientConfigCallback(httpClientBuilder -> { + try { + final TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create() + .setSslContext(SSLContextBuilder.create().loadTrustMaterial(null, (chains, authType) -> true).build()) + // disable the certificate since our testing cluster just uses the default security configuration + .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) + // See please https://issues.apache.org/jira/browse/HTTPCLIENT-2219 + .setTlsDetailsFactory(new Factory() { + @Override + public TlsDetails create(final SSLEngine sslEngine) { + return new TlsDetails(sslEngine.getSession(), sslEngine.getApplicationProtocol()); + } + }) + .build(); + + final PoolingAsyncClientConnectionManager connectionManager = PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy(tlsStrategy) + .build(); + + return httpClientBuilder.setConnectionManager(connectionManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + final String socketTimeoutString = settings.get(CLIENT_SOCKET_TIMEOUT); + final TimeValue socketTimeout = TimeValue.parseTimeValue( + socketTimeoutString == null ? "60s" : socketTimeoutString, + CLIENT_SOCKET_TIMEOUT + ); + builder.setRequestConfigCallback( + conf -> conf.setResponseTimeout(Timeout.ofMilliseconds(Math.toIntExact(socketTimeout.getMillis()))) + ); + if (settings.hasValue(CLIENT_PATH_PREFIX)) { + builder.setPathPrefix(settings.get(CLIENT_PATH_PREFIX)); + } + } + protected SampleJobParameter createWatcherJob(String jobId, SampleJobParameter jobParameter) throws IOException { return createWatcherJobWithClient(client(), jobId, jobParameter); } diff --git a/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/SampleJobRunnerIT.java b/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/SampleJobRunnerRestIT.java similarity index 98% rename from sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/SampleJobRunnerIT.java rename to sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/SampleJobRunnerRestIT.java index 9f63e9d5..9063e443 100644 --- a/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/SampleJobRunnerIT.java +++ b/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/SampleJobRunnerRestIT.java @@ -16,7 +16,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; -public class SampleJobRunnerIT extends SampleExtensionIntegTestCase { +public class SampleJobRunnerRestIT extends SampleExtensionIntegTestCase { public void testJobCreateWithCorrectParams() throws IOException { SampleJobParameter jobParameter = new SampleJobParameter(); diff --git a/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/SecureRestClientBuilder.java b/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/SecureRestClientBuilder.java new file mode 100644 index 00000000..7696196f --- /dev/null +++ b/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/SecureRestClientBuilder.java @@ -0,0 +1,235 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.jobscheduler.sampleextension; + +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; +import org.apache.hc.core5.function.Factory; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.reactor.ssl.TlsDetails; +import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.apache.hc.core5.util.Timeout; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.OpenSearchException; +import org.opensearch.client.RestClient; +import org.opensearch.client.RestClientBuilder; +import org.opensearch.common.io.PathUtils; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.Strings; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Provides builder to create low-level and high-level REST client to make calls to OpenSearch. + * + * Sample usage: + * SecureRestClientBuilder builder = new SecureRestClientBuilder(settings).build() + * RestClient restClient = builder.build(); + * + * Other usage: + * RestClient restClient = new SecureRestClientBuilder("localhost", 9200, false) + * .setUserPassword("admin", "myStrongPassword123") + * .setTrustCerts(trustStorePath) + * .build(); + * + * + * If https is enabled, creates RestClientBuilder using self-signed certificates or passed pem + * as trusted. + * + * If https is not enabled, creates a http based client. + */ +public class SecureRestClientBuilder { + + private final boolean httpSSLEnabled; + private final String user; + private final String passwd; + private final ArrayList hosts = new ArrayList<>(); + + private final Path configPath; + private final Settings settings; + + private int defaultConnectTimeOutMSecs = 5000; + private int defaultSoTimeoutMSecs = 10000; + private int defaultConnRequestTimeoutMSecs = 3 * 60 * 1000; /* 3 mins */ + private int defaultMaxConnPerRoute = RestClientBuilder.DEFAULT_MAX_CONN_PER_ROUTE; + private int defaultMaxConnTotal = RestClientBuilder.DEFAULT_MAX_CONN_TOTAL; + + private static final Logger log = LogManager.getLogger(SecureRestClientBuilder.class); + + public SecureRestClientBuilder(Settings settings, Path configPath, HttpHost[] httpHosts) { + this.httpSSLEnabled = settings.getAsBoolean("plugins.security.ssl.http.enabled", false); + this.settings = settings; + this.configPath = configPath; + this.user = null; + this.passwd = null; + hosts.addAll(Arrays.asList(httpHosts)); + } + + /** + * Creates a low-level Rest client. + * @return + * @throws IOException + */ + public RestClient build() throws IOException { + return createRestClientBuilder().build(); + } + + private RestClientBuilder createRestClientBuilder() throws IOException { + RestClientBuilder builder = RestClient.builder(hosts.toArray(new HttpHost[hosts.size()])); + + builder.setRequestConfigCallback(new RestClientBuilder.RequestConfigCallback() { + @Override + public RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder requestConfigBuilder) { + return requestConfigBuilder.setConnectTimeout(Timeout.ofMilliseconds(defaultConnectTimeOutMSecs)) + .setResponseTimeout(Timeout.ofMilliseconds(defaultSoTimeoutMSecs)) + .setConnectionRequestTimeout(Timeout.ofMilliseconds(defaultConnRequestTimeoutMSecs)); + } + }); + + final SSLContext sslContext; + try { + sslContext = createSSLContext(); + } catch (GeneralSecurityException | IOException ex) { + throw new IOException(ex); + } + final CredentialsProvider credentialsProvider = createCredsProvider(); + builder.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() { + @Override + public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) { + if (sslContext != null) { + TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create() + .setSslContext(sslContext) + // See please https://issues.apache.org/jira/browse/HTTPCLIENT-2219 + .setTlsDetailsFactory(new Factory() { + @Override + public TlsDetails create(final SSLEngine sslEngine) { + return new TlsDetails(sslEngine.getSession(), sslEngine.getApplicationProtocol()); + } + }) + .build(); + PoolingAsyncClientConnectionManager connectionManager = PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy(tlsStrategy) + .setMaxConnPerRoute(defaultMaxConnPerRoute) + .setMaxConnTotal(defaultMaxConnTotal) + .build(); + httpClientBuilder.setConnectionManager(connectionManager); + } + if (credentialsProvider != null) { + httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); + } + return httpClientBuilder; + } + }); + return builder; + } + + private SSLContext createSSLContext() throws IOException, GeneralSecurityException { + SSLContextBuilder builder = new SSLContextBuilder(); + if (httpSSLEnabled) { + // Handle trust store + String pemFile = getTrustPem(); + if (Strings.isNullOrEmpty(pemFile)) { + builder.loadTrustMaterial(null, new TrustSelfSignedStrategy()); + } else { + String pem = resolve(pemFile, configPath); + KeyStore trustStore = new TrustStore(pem).create(); + builder.loadTrustMaterial(trustStore, null); + } + + // Handle key store. + KeyStore keyStore = getKeyStore(); + if (keyStore != null) { + builder.loadKeyMaterial(keyStore, getKeystorePasswd().toCharArray()); + } + + } + return builder.build(); + } + + private CredentialsProvider createCredsProvider() { + if (Strings.isNullOrEmpty(user) || Strings.isNullOrEmpty(passwd)) return null; + + final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(new AuthScope(null, -1), new UsernamePasswordCredentials(user, passwd.toCharArray())); + return credentialsProvider; + } + + private String resolve(final String originalFile, final Path configPath) { + String path = null; + if (originalFile != null && originalFile.length() > 0) { + path = configPath.resolve(originalFile).toAbsolutePath().toString(); + log.debug("Resolved {} to {} against {}", originalFile, path, configPath.toAbsolutePath().toString()); + } + + if (path == null || path.length() == 0) { + throw new OpenSearchException("Empty file path for " + originalFile); + } + + if (Files.isDirectory(PathUtils.get(path), LinkOption.NOFOLLOW_LINKS)) { + throw new OpenSearchException("Is a directory: " + path + " Expected a file for " + originalFile); + } + + if (!Files.isReadable(PathUtils.get(path))) { + throw new OpenSearchException( + "Unable to read " + + path + + " (" + + PathUtils.get(path) + + "). Please make sure this files exists and is readable regarding to permissions. Property: " + + originalFile + ); + } + if ("".equals(path)) { + path = null; + } + return path; + } + + private String getTrustPem() { + return settings.get("plugins.security.ssl.http.pemcert_filepath", null); + } + + private String getKeystorePasswd() { + return settings.get("plugins.security.ssl.http.keystore_password", null); + } + + private KeyStore getKeyStore() throws IOException, GeneralSecurityException { + KeyStore keyStore = KeyStore.getInstance("jks"); + String keyStoreFile = settings.get("plugins.security.ssl.http.keystore_filepath", null); + String passwd = settings.get("plugins.security.ssl.http.keystore_password", null); + if (Strings.isNullOrEmpty(keyStoreFile) || Strings.isNullOrEmpty(passwd)) { + return null; + } + String keyStorePath = resolve(keyStoreFile, configPath); + try (InputStream is = Files.newInputStream(PathUtils.get(keyStorePath))) { + keyStore.load(is, passwd.toCharArray()); + } + return keyStore; + } +} diff --git a/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/TrustStore.java b/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/TrustStore.java new file mode 100644 index 00000000..6b1ca726 --- /dev/null +++ b/sample-extension-plugin/src/test/java/org/opensearch/jobscheduler/sampleextension/TrustStore.java @@ -0,0 +1,74 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.jobscheduler.sampleextension; + +import org.opensearch.common.io.PathUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collection; + +/** + * Helper class to read raw pem files to keystore. + */ +public class TrustStore { + + private final String effectiveKeyAlias = "al"; + private final String storeType = "JKS"; + private final String certType = "X.509"; + private final String cert; + + public TrustStore(final String file) { + cert = file; + } + + public KeyStore create() throws IOException, GeneralSecurityException { + X509Certificate[] trustCerts = loadCertificatesFromFile(cert); + return toTrustStore(effectiveKeyAlias, trustCerts); + } + + private X509Certificate[] loadCertificatesFromFile(String file) throws IOException, GeneralSecurityException { + if (file == null) { + return null; + } + CertificateFactory fact = CertificateFactory.getInstance(certType); + try (InputStream is = Files.newInputStream(PathUtils.get(file))) { + Collection certs = fact.generateCertificates(is); + X509Certificate[] x509Certs = new X509Certificate[certs.size()]; + int i = 0; + for (Certificate cert : certs) { + x509Certs[i++] = (X509Certificate) cert; + } + return x509Certs; + } + } + + private KeyStore toTrustStore(final String trustCertificatesAliasPrefix, final X509Certificate[] trustCertificates) throws IOException, + GeneralSecurityException { + if (trustCertificates == null) { + return null; + } + KeyStore ks = KeyStore.getInstance(storeType); + ks.load(null); + + if (trustCertificates != null) { + for (int i = 0; i < trustCertificates.length; i++) { + X509Certificate x509Certificate = trustCertificates[i]; + ks.setCertificateEntry(trustCertificatesAliasPrefix + "_" + i, x509Certificate); + } + } + return ks; + } +} diff --git a/src/test/java/org/opensearch/jobscheduler/JobSchedulerPluginRestIT.java b/src/test/java/org/opensearch/jobscheduler/JobSchedulerPluginRestIT.java index 02ec9593..60643413 100644 --- a/src/test/java/org/opensearch/jobscheduler/JobSchedulerPluginRestIT.java +++ b/src/test/java/org/opensearch/jobscheduler/JobSchedulerPluginRestIT.java @@ -14,13 +14,12 @@ import org.opensearch.common.xcontent.LoggingDeprecationHandler; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.common.xcontent.json.JsonXContent; -import org.opensearch.test.rest.OpenSearchRestTestCase; import java.io.IOException; import java.util.List; import java.util.Map; -public class JobSchedulerPluginRestIT extends OpenSearchRestTestCase { +public class JobSchedulerPluginRestIT extends ODFERestTestCase { @SuppressWarnings("unchecked") public void testPluginsAreInstalled() throws IOException { diff --git a/src/test/java/org/opensearch/jobscheduler/ODFERestTestCase.java b/src/test/java/org/opensearch/jobscheduler/ODFERestTestCase.java index 13d6f6ac..177ccf1c 100644 --- a/src/test/java/org/opensearch/jobscheduler/ODFERestTestCase.java +++ b/src/test/java/org/opensearch/jobscheduler/ODFERestTestCase.java @@ -9,15 +9,19 @@ package org.opensearch.jobscheduler; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Base64; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import javax.net.ssl.SSLEngine; -import org.apache.hc.client5.http.auth.AuthScope; -import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; -import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; @@ -35,6 +39,8 @@ import org.opensearch.client.Response; import org.opensearch.client.RestClient; import org.opensearch.client.RestClientBuilder; +import org.opensearch.client.WarningFailureException; +import org.opensearch.common.io.PathUtils; import org.opensearch.common.settings.Settings; import org.opensearch.common.unit.TimeValue; import org.opensearch.common.util.concurrent.ThreadContext; @@ -46,9 +52,6 @@ public abstract class ODFERestTestCase extends OpenSearchRestTestCase { - private static String localhostName = "localhost"; - private static int port = 9200; - protected boolean isHttps() { boolean isHttps = Optional.ofNullable(System.getProperty("https")).map("true"::equalsIgnoreCase).orElse(false); if (isHttps) { @@ -68,7 +71,14 @@ protected String getProtocol() { @Override protected Settings restAdminSettings() { - return Settings.builder().put("strictDeprecationMode", false).put("http.port", 9200).build(); + return Settings.builder() + .put("http.port", 9200) + .put("plugins.security.ssl.http.enabled", isHttps()) + .put("plugins.security.ssl.http.pemcert_filepath", "sample.pem") + .put("plugins.security.ssl.http.keystore_filepath", "test-kirk.jks") + .put("plugins.security.ssl.http.keystore_password", "changeit") + .build(); + // return Settings.builder().put("strictDeprecationMode", false).put("http.port", 9200).build(); } @Override @@ -76,9 +86,21 @@ protected RestClient buildClient(Settings settings, HttpHost[] hosts) throws IOE boolean strictDeprecationMode = settings.getAsBoolean("strictDeprecationMode", true); RestClientBuilder builder = RestClient.builder(hosts); if (isHttps()) { - configureHttpsClient(builder, settings); - builder.setStrictDeprecationMode(strictDeprecationMode); - return builder.build(); + String keystore = settings.get("plugins.security.ssl.http.keystore_filepath"); + if (Objects.nonNull(keystore)) { + URI uri = null; + try { + uri = this.getClass().getClassLoader().getResource("security/sample.pem").toURI(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + Path configPath = PathUtils.get(uri).getParent().toAbsolutePath(); + return new SecureRestClientBuilder(settings, configPath, hosts).build(); + } else { + configureHttpsClient(builder, settings); + builder.setStrictDeprecationMode(strictDeprecationMode); + return builder.build(); + } } else { configureClient(builder, settings); builder.setStrictDeprecationMode(strictDeprecationMode); @@ -111,14 +133,23 @@ protected void wipeAllODFEIndices() throws IOException { for (Map index : parserList) { String indexName = (String) index.get("index"); if (indexName != null && !".opendistro_security".equals(indexName)) { - adminClient().performRequest(new Request("DELETE", "/" + indexName)); + try { + adminClient().performRequest(new Request("DELETE", "/" + indexName)); + } catch (WarningFailureException ignore) {} } } } } protected static void configureHttpsClient(RestClientBuilder builder, Settings settings) throws IOException { - Map headers = ThreadContext.buildDefaultHeaders(settings); + Map headers = new HashMap<>(ThreadContext.buildDefaultHeaders(settings)); + String userName = Optional.ofNullable(System.getProperty("user")).orElseThrow(() -> new RuntimeException("user name is missing")); + String password = Optional.ofNullable(System.getProperty("password")) + .orElseThrow(() -> new RuntimeException("password is missing")); + headers.put( + "Authorization", + "Basic " + Base64.getEncoder().encodeToString((userName + ":" + password).getBytes(StandardCharsets.UTF_8)) + ); Header[] defaultHeaders = new Header[headers.size()]; int i = 0; for (Map.Entry entry : headers.entrySet()) { @@ -126,15 +157,6 @@ protected static void configureHttpsClient(RestClientBuilder builder, Settings s } builder.setDefaultHeaders(defaultHeaders); builder.setHttpClientConfigCallback(httpClientBuilder -> { - String userName = Optional.ofNullable(System.getProperty("user")) - .orElseThrow(() -> new RuntimeException("user name is missing")); - String password = Optional.ofNullable(System.getProperty("password")) - .orElseThrow(() -> new RuntimeException("password is missing")); - BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - credentialsProvider.setCredentials( - new AuthScope(new HttpHost(localhostName, port)), - new UsernamePasswordCredentials(userName, password.toCharArray()) - ); try { final TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create() .setSslContext(SSLContextBuilder.create().loadTrustMaterial(null, (chains, authType) -> true).build()) @@ -153,7 +175,7 @@ public TlsDetails create(final SSLEngine sslEngine) { .setTlsStrategy(tlsStrategy) .build(); - return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider).setConnectionManager(connectionManager); + return httpClientBuilder.setConnectionManager(connectionManager); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/test/java/org/opensearch/jobscheduler/SecureRestClientBuilder.java b/src/test/java/org/opensearch/jobscheduler/SecureRestClientBuilder.java new file mode 100644 index 00000000..b0cec7df --- /dev/null +++ b/src/test/java/org/opensearch/jobscheduler/SecureRestClientBuilder.java @@ -0,0 +1,236 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.jobscheduler; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Arrays; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; +import org.apache.hc.core5.function.Factory; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.reactor.ssl.TlsDetails; +import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.apache.hc.core5.util.Timeout; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.OpenSearchException; +import org.opensearch.client.RestClient; +import org.opensearch.client.RestClientBuilder; +import org.opensearch.common.io.PathUtils; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.Strings; + +/** + * Provides builder to create low-level and high-level REST client to make calls to OpenSearch. + * + * Sample usage: + * SecureRestClientBuilder builder = new SecureRestClientBuilder(settings).build() + * RestClient restClient = builder.build(); + * + * Other usage: + * RestClient restClient = new SecureRestClientBuilder("localhost", 9200, false) + * .setUserPassword("admin", "myStrongPassword123") + * .setTrustCerts(trustStorePath) + * .build(); + * + * + * If https is enabled, creates RestClientBuilder using self-signed certificates or passed pem + * as trusted. + * + * If https is not enabled, creates a http based client. + */ +public class SecureRestClientBuilder { + + private final boolean httpSSLEnabled; + private final String user; + private final String passwd; + private final ArrayList hosts = new ArrayList<>(); + + private final Path configPath; + private final Settings settings; + + private int defaultConnectTimeOutMSecs = 5000; + private int defaultSoTimeoutMSecs = 10000; + private int defaultConnRequestTimeoutMSecs = 3 * 60 * 1000; /* 3 mins */ + private int defaultMaxConnPerRoute = RestClientBuilder.DEFAULT_MAX_CONN_PER_ROUTE; + private int defaultMaxConnTotal = RestClientBuilder.DEFAULT_MAX_CONN_TOTAL; + + private static final Logger log = LogManager.getLogger(SecureRestClientBuilder.class); + + public SecureRestClientBuilder(Settings settings, Path configPath, HttpHost[] httpHosts) { + this.httpSSLEnabled = settings.getAsBoolean("plugins.security.ssl.http.enabled", false); + this.settings = settings; + this.configPath = configPath; + this.user = null; + this.passwd = null; + hosts.addAll(Arrays.asList(httpHosts)); + } + + /** + * Creates a low-level Rest client. + * @return + * @throws IOException + */ + public RestClient build() throws IOException { + return createRestClientBuilder().build(); + } + + private RestClientBuilder createRestClientBuilder() throws IOException { + RestClientBuilder builder = RestClient.builder(hosts.toArray(new HttpHost[hosts.size()])); + + builder.setRequestConfigCallback(new RestClientBuilder.RequestConfigCallback() { + @Override + public RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder requestConfigBuilder) { + return requestConfigBuilder.setConnectTimeout(Timeout.ofMilliseconds(defaultConnectTimeOutMSecs)) + .setResponseTimeout(Timeout.ofMilliseconds(defaultSoTimeoutMSecs)) + .setConnectionRequestTimeout(Timeout.ofMilliseconds(defaultConnRequestTimeoutMSecs)); + } + }); + + final SSLContext sslContext; + try { + sslContext = createSSLContext(); + } catch (GeneralSecurityException | IOException ex) { + throw new IOException(ex); + } + final CredentialsProvider credentialsProvider = createCredsProvider(); + builder.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() { + @Override + public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) { + if (sslContext != null) { + TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create() + .setSslContext(sslContext) + // See please https://issues.apache.org/jira/browse/HTTPCLIENT-2219 + .setTlsDetailsFactory(new Factory() { + @Override + public TlsDetails create(final SSLEngine sslEngine) { + return new TlsDetails(sslEngine.getSession(), sslEngine.getApplicationProtocol()); + } + }) + .build(); + PoolingAsyncClientConnectionManager connectionManager = PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy(tlsStrategy) + .setMaxConnPerRoute(defaultMaxConnPerRoute) + .setMaxConnTotal(defaultMaxConnTotal) + .build(); + httpClientBuilder.setConnectionManager(connectionManager); + } + if (credentialsProvider != null) { + httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); + } + return httpClientBuilder; + } + }); + return builder; + } + + private SSLContext createSSLContext() throws IOException, GeneralSecurityException { + SSLContextBuilder builder = new SSLContextBuilder(); + if (httpSSLEnabled) { + // Handle trust store + String pemFile = getTrustPem(); + if (Strings.isNullOrEmpty(pemFile)) { + builder.loadTrustMaterial(null, new TrustSelfSignedStrategy()); + } else { + String pem = resolve(pemFile, configPath); + KeyStore trustStore = new TrustStore(pem).create(); + builder.loadTrustMaterial(trustStore, null); + } + + // Handle key store. + KeyStore keyStore = getKeyStore(); + if (keyStore != null) { + builder.loadKeyMaterial(keyStore, getKeystorePasswd().toCharArray()); + } + + } + return builder.build(); + } + + private CredentialsProvider createCredsProvider() { + if (Strings.isNullOrEmpty(user) || Strings.isNullOrEmpty(passwd)) return null; + + final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(new AuthScope(null, -1), new UsernamePasswordCredentials(user, passwd.toCharArray())); + return credentialsProvider; + } + + private String resolve(final String originalFile, final Path configPath) { + String path = null; + if (originalFile != null && originalFile.length() > 0) { + path = configPath.resolve(originalFile).toAbsolutePath().toString(); + log.debug("Resolved {} to {} against {}", originalFile, path, configPath.toAbsolutePath().toString()); + } + + if (path == null || path.length() == 0) { + throw new OpenSearchException("Empty file path for " + originalFile); + } + + if (Files.isDirectory(PathUtils.get(path), LinkOption.NOFOLLOW_LINKS)) { + throw new OpenSearchException("Is a directory: " + path + " Expected a file for " + originalFile); + } + + if (!Files.isReadable(PathUtils.get(path))) { + throw new OpenSearchException( + "Unable to read " + + path + + " (" + + PathUtils.get(path) + + "). Please make sure this files exists and is readable regarding to permissions. Property: " + + originalFile + ); + } + if ("".equals(path)) { + path = null; + } + return path; + } + + private String getTrustPem() { + return settings.get("plugins.security.ssl.http.pemcert_filepath", null); + } + + private String getKeystorePasswd() { + return settings.get("plugins.security.ssl.http.keystore_password", null); + } + + private KeyStore getKeyStore() throws IOException, GeneralSecurityException { + KeyStore keyStore = KeyStore.getInstance("jks"); + String keyStoreFile = settings.get("plugins.security.ssl.http.keystore_filepath", null); + String passwd = settings.get("plugins.security.ssl.http.keystore_password", null); + if (Strings.isNullOrEmpty(keyStoreFile) || Strings.isNullOrEmpty(passwd)) { + return null; + } + String keyStorePath = resolve(keyStoreFile, configPath); + try (InputStream is = Files.newInputStream(PathUtils.get(keyStorePath))) { + keyStore.load(is, passwd.toCharArray()); + } + return keyStore; + } +} diff --git a/src/test/java/org/opensearch/jobscheduler/TrustStore.java b/src/test/java/org/opensearch/jobscheduler/TrustStore.java new file mode 100644 index 00000000..e84554da --- /dev/null +++ b/src/test/java/org/opensearch/jobscheduler/TrustStore.java @@ -0,0 +1,75 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.jobscheduler; + +import org.opensearch.common.io.PathUtils; + +import java.io.InputStream; +import java.nio.file.Files; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collection; + +/** + * Helper class to read raw pem files to keystore. + */ +public class TrustStore { + + private final String effectiveKeyAlias = "al"; + private final String storeType = "JKS"; + private final String certType = "X.509"; + private final String cert; + + public TrustStore(final String file) { + cert = file; + } + + public KeyStore create() throws IOException, GeneralSecurityException { + X509Certificate[] trustCerts = loadCertificatesFromFile(cert); + return toTrustStore(effectiveKeyAlias, trustCerts); + } + + private X509Certificate[] loadCertificatesFromFile(String file) throws IOException, GeneralSecurityException { + if (file == null) { + return null; + } + CertificateFactory fact = CertificateFactory.getInstance(certType); + + try (InputStream is = Files.newInputStream(PathUtils.get(file))) { + Collection certs = fact.generateCertificates(is); + X509Certificate[] x509Certs = new X509Certificate[certs.size()]; + int i = 0; + for (Certificate cert : certs) { + x509Certs[i++] = (X509Certificate) cert; + } + return x509Certs; + } + } + + private KeyStore toTrustStore(final String trustCertificatesAliasPrefix, final X509Certificate[] trustCertificates) throws IOException, + GeneralSecurityException { + if (trustCertificates == null) { + return null; + } + KeyStore ks = KeyStore.getInstance(storeType); + ks.load(null); + + if (trustCertificates != null) { + for (int i = 0; i < trustCertificates.length; i++) { + X509Certificate x509Certificate = trustCertificates[i]; + ks.setCertificateEntry(trustCertificatesAliasPrefix + "_" + i, x509Certificate); + } + } + return ks; + } +} diff --git a/src/test/resources/security/esnode-key.pem b/src/test/resources/security/esnode-key.pem new file mode 100644 index 00000000..e90562be --- /dev/null +++ b/src/test/resources/security/esnode-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCm93kXteDQHMAv +bUPNPW5pyRHKDD42XGWSgq0k1D29C/UdyL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0 +o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0HGkn47XVu3EwbfrTENg3jFu+Oem6a/50 +1SzITzJWtS0cn2dIFOBimTVpT/4Zv5qrXA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1 +MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8ndibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b +6l+KLo3IKpfTbAIJXIO+M67FLtWKtttDao94B069skzKk6FPgW/OZh6PRCD0oxOa +vV+ld2SjAgMBAAECggEAQK1+uAOZeaSZggW2jQut+MaN4JHLi61RH2cFgU3COLgo +FIiNjFn8f2KKU3gpkt1It8PjlmprpYut4wHI7r6UQfuv7ZrmncRiPWHm9PB82+ZQ +5MXYqj4YUxoQJ62Cyz4sM6BobZDrjG6HHGTzuwiKvHHkbsEE9jQ4E5m7yfbVvM0O +zvwrSOM1tkZihKSTpR0j2+taji914tjBssbn12TMZQL5ItGnhR3luY8mEwT9MNkZ +xg0VcREoAH+pu9FE0vPUgLVzhJ3be7qZTTSRqv08bmW+y1plu80GbppePcgYhEow +dlW4l6XPJaHVSn1lSFHE6QAx6sqiAnBz0NoTPIaLyQKBgQDZqDOlhCRciMRicSXn +7yid9rhEmdMkySJHTVFOidFWwlBcp0fGxxn8UNSBcXdSy7GLlUtH41W9PWl8tp9U +hQiiXORxOJ7ZcB80uNKXF01hpPj2DpFPWyHFxpDkWiTAYpZl68rOlYujxZUjJIej +VvcykBC2BlEOG9uZv2kxcqLyJwKBgQDEYULTxaTuLIa17wU3nAhaainKB3vHxw9B +Ksy5p3ND43UNEKkQm7K/WENx0q47TA1mKD9i+BhaLod98mu0YZ+BCUNgWKcBHK8c +uXpauvM/pLhFLXZ2jvEJVpFY3J79FSRK8bwE9RgKfVKMMgEk4zOyZowS8WScOqiy +hnQn1vKTJQKBgElhYuAnl9a2qXcC7KOwRsJS3rcKIVxijzL4xzOyVShp5IwIPbOv +hnxBiBOH/JGmaNpFYBcBdvORE9JfA4KMQ2fx53agfzWRjoPI1/7mdUk5RFI4gRb/ +A3jZRBoopgFSe6ArCbnyQxzYzToG48/Wzwp19ZxYrtUR4UyJct6f5n27AoGBAJDh +KIpQQDOvCdtjcbfrF4aM2DPCfaGPzENJriwxy6oEPzDaX8Bu/dqI5Ykt43i/zQrX +GpyLaHvv4+oZVTiI5UIvcVO9U8hQPyiz9f7F+fu0LHZs6f7hyhYXlbe3XFxeop3f +5dTKdWgXuTTRF2L9dABkA2deS9mutRKwezWBMQk5AoGBALPtX0FrT1zIosibmlud +tu49A/0KZu4PBjrFMYTSEWGNJez3Fb2VsJwylVl6HivwbP61FhlYfyksCzQQFU71 ++x7Nmybp7PmpEBECr3deoZKQ/acNHn0iwb0It+YqV5+TquQebqgwK6WCLsMuiYKT +bg/ch9Rhxbq22yrVgWHh6epp +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/src/test/resources/security/esnode.pem b/src/test/resources/security/esnode.pem new file mode 100644 index 00000000..44101f0b --- /dev/null +++ b/src/test/resources/security/esnode.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT +AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl +MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud +yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0 +HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr +XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n +dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD +ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R +BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA +AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF +BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo +wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ +KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz +pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi +7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh +hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L +camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg +PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/src/test/resources/security/kirk-key.pem b/src/test/resources/security/kirk-key.pem new file mode 100644 index 00000000..1949c261 --- /dev/null +++ b/src/test/resources/security/kirk-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCVXDgEJQorgfXp +gpY0TgF55bD2xuzxN5Dc9rDfgWxrsOvOloMpd7k6FR71bKWjJi1KptSmM/cDElky +AWYKSfYWGiGxsQ+EQW+6kwCfEOHXQldn+0+JcWqP+osSPjtJfwRvRN5kRqP69MPo +7U0N2kdqenqMWjmG1chDGLRSOEGU5HIBiDxsZtOcvMaJ8b1eaW0lvS+6gFQ80AvB +GBkDDCOHHLtDXBylrZk2CQP8AzxNicIZ4B8G3CG3OHA8+nBtEtxZoIihrrkqlMt+ +b/5N8u8zB0Encew0kdrc4R/2wS//ahr6U+9Siq8T7WsUtGwKj3BJClg6OyDJRhlu +y2gFnxoPAgMBAAECggEAP5TOycDkx+megAWVoHV2fmgvgZXkBrlzQwUG/VZQi7V4 +ZGzBMBVltdqI38wc5MtbK3TCgHANnnKgor9iq02Z4wXDwytPIiti/ycV9CDRKvv0 +TnD2hllQFjN/IUh5n4thHWbRTxmdM7cfcNgX3aZGkYbLBVVhOMtn4VwyYu/Mxy8j +xClZT2xKOHkxqwmWPmdDTbAeZIbSv7RkIGfrKuQyUGUaWhrPslvYzFkYZ0umaDgQ +OAthZew5Bz3OfUGOMPLH61SVPuJZh9zN1hTWOvT65WFWfsPd2yStI+WD/5PU1Doo +1RyeHJO7s3ug8JPbtNJmaJwHe9nXBb/HXFdqb976yQKBgQDNYhpu+MYSYupaYqjs +9YFmHQNKpNZqgZ4ceRFZ6cMJoqpI5dpEMqToFH7tpor72Lturct2U9nc2WR0HeEs +/6tiptyMPTFEiMFb1opQlXF2ae7LeJllntDGN0Q6vxKnQV+7VMcXA0Y8F7tvGDy3 +qJu5lfvB1mNM2I6y/eMxjBuQhwKBgQC6K41DXMFro0UnoO879pOQYMydCErJRmjG +/tZSy3Wj4KA/QJsDSViwGfvdPuHZRaG9WtxdL6kn0w1exM9Rb0bBKl36lvi7o7xv +M+Lw9eyXMkww8/F5d7YYH77gIhGo+RITkKI3+5BxeBaUnrGvmHrpmpgRXWmINqr0 +0jsnN3u0OQKBgCf45vIgItSjQb8zonLz2SpZjTFy4XQ7I92gxnq8X0Q5z3B+o7tQ +K/4rNwTju/sGFHyXAJlX+nfcK4vZ4OBUJjP+C8CTjEotX4yTNbo3S6zjMyGQqDI5 +9aIOUY4pb+TzeUFJX7If5gR+DfGyQubvvtcg1K3GHu9u2l8FwLj87sRzAoGAflQF +RHuRiG+/AngTPnZAhc0Zq0kwLkpH2Rid6IrFZhGLy8AUL/O6aa0IGoaMDLpSWUJp +nBY2S57MSM11/MVslrEgGmYNnI4r1K25xlaqV6K6ztEJv6n69327MS4NG8L/gCU5 +3pEm38hkUi8pVYU7in7rx4TCkrq94OkzWJYurAkCgYATQCL/rJLQAlJIGulp8s6h +mQGwy8vIqMjAdHGLrCS35sVYBXG13knS52LJHvbVee39AbD5/LlWvjJGlQMzCLrw +F7oILW5kXxhb8S73GWcuMbuQMFVHFONbZAZgn+C9FW4l7XyRdkrbR1MRZ2km8YMs +/AHmo368d4PSNRMMzLHw8Q== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/src/test/resources/security/kirk.pem b/src/test/resources/security/kirk.pem new file mode 100644 index 00000000..36b7e19a --- /dev/null +++ b/src/test/resources/security/kirk.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEmDCCA4CgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLcwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzA0MjRaFw0zNDAyMTcxNzA0MjRaME0xCzAJBgNVBAYT +AmRlMQ0wCwYDVQQHDAR0ZXN0MQ8wDQYDVQQKDAZjbGllbnQxDzANBgNVBAsMBmNs +aWVudDENMAsGA1UEAwwEa2lyazCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAJVcOAQlCiuB9emCljROAXnlsPbG7PE3kNz2sN+BbGuw686Wgyl3uToVHvVs +paMmLUqm1KYz9wMSWTIBZgpJ9hYaIbGxD4RBb7qTAJ8Q4ddCV2f7T4lxao/6ixI+ +O0l/BG9E3mRGo/r0w+jtTQ3aR2p6eoxaOYbVyEMYtFI4QZTkcgGIPGxm05y8xonx +vV5pbSW9L7qAVDzQC8EYGQMMI4ccu0NcHKWtmTYJA/wDPE2JwhngHwbcIbc4cDz6 +cG0S3FmgiKGuuSqUy35v/k3y7zMHQSdx7DSR2tzhH/bBL/9qGvpT71KKrxPtaxS0 +bAqPcEkKWDo7IMlGGW7LaAWfGg8CAwEAAaOCASswggEnMAwGA1UdEwEB/wQCMAAw +DgYDVR0PAQH/BAQDAgXgMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMB0GA1UdDgQW +BBSjMS8tgguX/V7KSGLoGg7K6XMzIDCBzwYDVR0jBIHHMIHEgBQXh9+gWutmEqfV +0Pi6EkU8tysAnKGBlaSBkjCBjzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmS +JomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1wbGUgQ29tIEluYy4xITAf +BgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEhMB8GA1UEAwwYRXhhbXBs +ZSBDb20gSW5jLiBSb290IENBghQNZAmZZn3EFOxBR4630XlhI+mo4jANBgkqhkiG +9w0BAQsFAAOCAQEACEUPPE66/Ot3vZqRGpjDjPHAdtOq+ebaglQhvYcnDw8LOZm8 +Gbh9M88CiO6UxC8ipQLTPh2yyeWArkpJzJK/Pi1eoF1XLiAa0sQ/RaJfQWPm9dvl +1ZQeK5vfD4147b3iBobwEV+CR04SKow0YeEEzAJvzr8YdKI6jqr+2GjjVqzxvRBy +KRVHWCFiR7bZhHGLq3br8hSu0hwjb3oGa1ZI8dui6ujyZt6nm6BoEkau3G/6+zq9 +E6vX3+8Fj4HKCAL6i0SwfGmEpTNp5WUhqibK/fMhhmMT4Mx6MxkT+OFnIjdUU0S/ +e3kgnG8qjficUr38CyEli1U0M7koIXUZI7r+LQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/src/test/resources/security/root-ca.pem b/src/test/resources/security/root-ca.pem new file mode 100644 index 00000000..d33f5f72 --- /dev/null +++ b/src/test/resources/security/root-ca.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIExjCCA66gAwIBAgIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzAwMzZaFw0zNDAyMTcxNzAwMzZaMIGPMRMwEQYKCZIm +iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ +RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290 +IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEPyN7J9VGPyJcQmCBl5TGwfSzvVdWwoQU +j9aEsdfFJ6pBCDQSsj8Lv4RqL0dZra7h7SpZLLX/YZcnjikrYC+rP5OwsI9xEE/4 +U98CsTBPhIMgqFK6SzNE5494BsAk4cL72dOOc8tX19oDS/PvBULbNkthQ0aAF1dg +vbrHvu7hq7LisB5ZRGHVE1k/AbCs2PaaKkn2jCw/b+U0Ml9qPuuEgz2mAqJDGYoA +WSR4YXrOcrmPuRqbws464YZbJW898/0Pn/U300ed+4YHiNYLLJp51AMkR4YEw969 +VRPbWIvLrd0PQBooC/eLrL6rvud/GpYhdQEUx8qcNCKd4bz3OaQ5AgMBAAGjggEW +MIIBEjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQU +F4ffoFrrZhKn1dD4uhJFPLcrAJwwgc8GA1UdIwSBxzCBxIAUF4ffoFrrZhKn1dD4 +uhJFPLcrAJyhgZWkgZIwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJ +k/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYD +VQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUg +Q29tIEluYy4gUm9vdCBDQYIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcN +AQELBQADggEBAL3Q3AHUhMiLUy6OlLSt8wX9I2oNGDKbBu0atpUNDztk/0s3YLQC +YuXgN4KrIcMXQIuAXCx407c+pIlT/T1FNn+VQXwi56PYzxQKtlpoKUL3oPQE1d0V +6EoiNk+6UodvyZqpdQu7fXVentRMk1QX7D9otmiiNuX+GSxJhJC2Lyzw65O9EUgG +1yVJon6RkUGtqBqKIuLksKwEr//ELnjmXit4LQKSnqKr0FTCB7seIrKJNyb35Qnq +qy9a/Unhokrmdda1tr6MbqU8l7HmxLuSd/Ky+L0eDNtYv6YfMewtjg0TtAnFyQov +rdXmeq1dy9HLo3Ds4AFz3Gx9076TxcRS/iI= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/src/test/resources/security/sample.pem b/src/test/resources/security/sample.pem new file mode 100644 index 00000000..44101f0b --- /dev/null +++ b/src/test/resources/security/sample.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT +AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl +MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud +yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0 +HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr +XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n +dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD +ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R +BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA +AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF +BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo +wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ +KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz +pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi +7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh +hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L +camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg +PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/src/test/resources/security/test-kirk.jks b/src/test/resources/security/test-kirk.jks new file mode 100644 index 00000000..6c8c5ef7 Binary files /dev/null and b/src/test/resources/security/test-kirk.jks differ