Java tutorial
/* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ package org.elasticsearch.xpack.core.ssl; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.ssl.SSLContextBuilder; import org.elasticsearch.common.CheckedRunnable; import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.http.MockResponse; import org.elasticsearch.test.http.MockWebServer; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.junit.After; import org.junit.Before; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.security.AccessController; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.sameInstance; /** * Unit tests for the reloading of SSL configuration */ public class SSLConfigurationReloaderTests extends ESTestCase { private ThreadPool threadPool; private ResourceWatcherService resourceWatcherService; @Before public void setup() { threadPool = new TestThreadPool("reload tests"); resourceWatcherService = new ResourceWatcherService( Settings.builder().put("resource.reload.interval.high", "1s").build(), threadPool); resourceWatcherService.start(); } @After public void cleanup() throws Exception { if (threadPool != null) { terminate(threadPool); } } /** * Tests reloading a keystore that is used in the KeyManager of SSLContext */ public void testReloadingKeyStore() throws Exception { final Path tempDir = createTempDir(); final Path keystorePath = tempDir.resolve("testnode.jks"); final Path updatedKeystorePath = tempDir.resolve("testnode_updated.jks"); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks"), keystorePath); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode_updated.jks"), updatedKeystorePath); MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("xpack.ssl.keystore.secure_password", "testnode"); final Settings settings = Settings.builder().put("path.home", createTempDir()) .put("xpack.ssl.keystore.path", keystorePath).setSecureSettings(secureSettings).build(); final Environment env = randomBoolean() ? null : TestEnvironment.newEnvironment(settings); //Load HTTPClient only once. Client uses the same store as a truststore try (CloseableHttpClient client = getSSLClient(keystorePath, "testnode")) { final Consumer<SSLContext> keyMaterialPreChecks = (context) -> { try (MockWebServer server = new MockWebServer(context, true)) { server.enqueue(new MockResponse().setResponseCode(200).setBody("body")); server.start(); privilegedConnect( () -> client.execute(new HttpGet("https://localhost:" + server.getPort())).close()); } catch (Exception e) { throw new RuntimeException("Exception starting or connecting to the mock server", e); } }; final Runnable modifier = () -> { try { atomicMoveIfPossible(updatedKeystorePath, keystorePath); } catch (Exception e) { throw new RuntimeException("modification failed", e); } }; // The new server certificate is not in the client's truststore so SSLHandshake should fail final Consumer<SSLContext> keyMaterialPostChecks = (updatedContext) -> { try (MockWebServer server = new MockWebServer(updatedContext, true)) { server.enqueue(new MockResponse().setResponseCode(200).setBody("body")); server.start(); SSLHandshakeException sslException = expectThrows(SSLHandshakeException.class, () -> privilegedConnect(() -> client .execute(new HttpGet("https://localhost:" + server.getPort())).close())); assertThat(sslException.getCause().getMessage(), containsString("PKIX path validation failed")); } catch (Exception e) { throw new RuntimeException("Exception starting or connecting to the mock server", e); } }; validateSSLConfigurationIsReloaded(settings, env, keyMaterialPreChecks, modifier, keyMaterialPostChecks); } } /** * Tests the reloading of SSLContext when a PEM key and certificate are used. */ public void testPEMKeyConfigReloading() throws Exception { Path tempDir = createTempDir(); Path keyPath = tempDir.resolve("testnode.pem"); Path updatedKeyPath = tempDir.resolve("testnode_updated.pem"); Path certPath = tempDir.resolve("testnode.crt"); Path updatedCertPath = tempDir.resolve("testnode_updated.crt"); final Path clientTruststorePath = tempDir.resolve("testnode.jks"); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.pem"), keyPath); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode_updated.pem"), updatedKeyPath); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode_updated.crt"), updatedCertPath); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"), certPath); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks"), clientTruststorePath); MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("xpack.ssl.secure_key_passphrase", "testnode"); final Settings settings = Settings.builder().put("path.home", createTempDir()).put("xpack.ssl.key", keyPath) .put("xpack.ssl.certificate", certPath).setSecureSettings(secureSettings).build(); final Environment env = randomBoolean() ? null : TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build()); // Load HTTPClient once. Client uses a keystore containing testnode key/cert as a truststore try (CloseableHttpClient client = getSSLClient(clientTruststorePath, "testnode")) { final Consumer<SSLContext> keyMaterialPreChecks = (context) -> { try (MockWebServer server = new MockWebServer(context, false)) { server.enqueue(new MockResponse().setResponseCode(200).setBody("body")); server.start(); privilegedConnect( () -> client.execute(new HttpGet("https://localhost:" + server.getPort())).close()); } catch (Exception e) { throw new RuntimeException("Exception starting or connecting to the mock server", e); } }; final Runnable modifier = () -> { try { atomicMoveIfPossible(updatedKeyPath, keyPath); atomicMoveIfPossible(updatedCertPath, certPath); } catch (Exception e) { throw new RuntimeException("failed to modify file", e); } }; // The new server certificate is not in the client's truststore so SSLHandshake should fail final Consumer<SSLContext> keyMaterialPostChecks = (updatedContext) -> { try (MockWebServer server = new MockWebServer(updatedContext, false)) { server.enqueue(new MockResponse().setResponseCode(200).setBody("body")); server.start(); SSLHandshakeException sslException = expectThrows(SSLHandshakeException.class, () -> privilegedConnect(() -> client .execute(new HttpGet("https://localhost:" + server.getPort())).close())); assertThat(sslException.getCause().getMessage(), containsString("PKIX path validation failed")); } catch (Exception e) { throw new RuntimeException("Exception starting or connecting to the mock server", e); } }; validateSSLConfigurationIsReloaded(settings, env, keyMaterialPreChecks, modifier, keyMaterialPostChecks); } } /** * Tests the reloading of SSLContext when the trust store is modified. The same store is used as a TrustStore (for the * reloadable SSLContext used in the HTTPClient) and as a KeyStore for the MockWebServer */ public void testReloadingTrustStore() throws Exception { Path tempDir = createTempDir(); Path trustStorePath = tempDir.resolve("testnode.jks"); Path updatedTruststorePath = tempDir.resolve("testnode_updated.jks"); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks"), trustStorePath); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode_updated.jks"), updatedTruststorePath); MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("xpack.ssl.truststore.secure_password", "testnode"); Settings settings = Settings.builder().put("xpack.ssl.truststore.path", trustStorePath) .put("path.home", createTempDir()).setSecureSettings(secureSettings).build(); Environment env = randomBoolean() ? null : TestEnvironment.newEnvironment(settings); // Create the MockWebServer once for both pre and post checks try (MockWebServer server = getSslServer(trustStorePath, "testnode")) { final Consumer<SSLContext> trustMaterialPreChecks = (context) -> { try (CloseableHttpClient client = HttpClients.custom().setSSLContext(context).build()) { privilegedConnect( () -> client.execute(new HttpGet("https://localhost:" + server.getPort())).close()); } catch (Exception e) { throw new RuntimeException("Error connecting to the mock server", e); } }; final Runnable modifier = () -> { try { atomicMoveIfPossible(updatedTruststorePath, trustStorePath); } catch (Exception e) { throw new RuntimeException("failed to modify file", e); } }; // Client's truststore doesn't contain the server's certificate anymore so SSLHandshake should fail final Consumer<SSLContext> trustMaterialPostChecks = (updatedContext) -> { try (CloseableHttpClient client = HttpClients.custom().setSSLContext(updatedContext).build()) { SSLHandshakeException sslException = expectThrows(SSLHandshakeException.class, () -> privilegedConnect(() -> client .execute(new HttpGet("https://localhost:" + server.getPort())).close())); assertThat(sslException.getCause().getMessage(), containsString("PKIX path building failed")); } catch (Exception e) { throw new RuntimeException("Error closing CloseableHttpClient", e); } }; validateSSLConfigurationIsReloaded(settings, env, trustMaterialPreChecks, modifier, trustMaterialPostChecks); } } /** * Test the reloading of SSLContext whose trust config is backed by PEM certificate files. */ public void testReloadingPEMTrustConfig() throws Exception { Path tempDir = createTempDir(); Path clientCertPath = tempDir.resolve("testnode.crt"); Path keyStorePath = tempDir.resolve("testnode.jks"); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks"), keyStorePath); //Our keystore contains two Certificates it can present. One build from the RSA keypair and one build from the EC keypair. EC is // used since it keyManager presents the first one in alias alphabetical order (and testnode_ec comes before testnode_rsa) Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode_ec.crt"), clientCertPath); Settings settings = Settings.builder() .putList("xpack.ssl.certificate_authorities", clientCertPath.toString()) .put("path.home", createTempDir()).build(); Environment env = randomBoolean() ? null : TestEnvironment.newEnvironment(settings); // Create the MockWebServer once for both pre and post checks try (MockWebServer server = getSslServer(keyStorePath, "testnode")) { final Consumer<SSLContext> trustMaterialPreChecks = (context) -> { try (CloseableHttpClient client = HttpClients.custom().setSSLContext(context).build()) { privilegedConnect( () -> client.execute(new HttpGet("https://localhost:" + server.getPort())).close()); } catch (Exception e) { throw new RuntimeException("Exception connecting to the mock server", e); } }; final Runnable modifier = () -> { try { Path updatedCert = tempDir.resolve("updated.crt"); Files.copy(getDataPath( "/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode_updated.crt"), updatedCert, StandardCopyOption.REPLACE_EXISTING); atomicMoveIfPossible(updatedCert, clientCertPath); } catch (Exception e) { throw new RuntimeException("failed to modify file", e); } }; // Client doesn't trust the Server certificate anymore so SSLHandshake should fail final Consumer<SSLContext> trustMaterialPostChecks = (updatedContext) -> { try (CloseableHttpClient client = HttpClients.custom().setSSLContext(updatedContext).build()) { SSLHandshakeException sslException = expectThrows(SSLHandshakeException.class, () -> privilegedConnect(() -> client .execute(new HttpGet("https://localhost:" + server.getPort())).close())); assertThat(sslException.getCause().getMessage(), containsString("PKIX path building failed")); } catch (Exception e) { throw new RuntimeException("Error closing CloseableHttpClient", e); } }; validateSSLConfigurationIsReloaded(settings, env, trustMaterialPreChecks, modifier, trustMaterialPostChecks); } } /** * Tests the reloading of a keystore when there is an exception during reloading. An exception is caused by truncating the keystore * that is being monitored */ public void testReloadingKeyStoreException() throws Exception { Path tempDir = createTempDir(); Path keystorePath = tempDir.resolve("testnode.jks"); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks"), keystorePath); MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("xpack.ssl.keystore.secure_password", "testnode"); Settings settings = Settings.builder().put("xpack.ssl.keystore.path", keystorePath) .setSecureSettings(secureSettings).put("path.home", createTempDir()).build(); Environment env = randomBoolean() ? null : TestEnvironment.newEnvironment(settings); final SSLService sslService = new SSLService(settings, env); final SSLConfiguration config = sslService.sslConfiguration(Settings.EMPTY); new SSLConfigurationReloader(settings, env, sslService, resourceWatcherService) { @Override void reloadSSLContext(SSLConfiguration configuration) { fail("reload should not be called! [keystore reload exception]"); } }; final SSLContext context = sslService.sslContextHolder(config).sslContext(); // truncate the keystore try (OutputStream out = Files.newOutputStream(keystorePath, StandardOpenOption.TRUNCATE_EXISTING)) { } // we intentionally don't wait here as we rely on concurrency to catch a failure assertThat(sslService.sslContextHolder(config).sslContext(), sameInstance(context)); } /** * Tests the reloading of a key config backed by pem files when there is an exception during reloading. An exception is caused by * truncating the key file that is being monitored */ public void testReloadingPEMKeyConfigException() throws Exception { Path tempDir = createTempDir(); Path keyPath = tempDir.resolve("testnode.pem"); Path certPath = tempDir.resolve("testnode.crt"); Path clientCertPath = tempDir.resolve("testclient.crt"); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.pem"), keyPath); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"), certPath); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testclient.crt"), clientCertPath); MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("xpack.ssl.secure_key_passphrase", "testnode"); Settings settings = Settings.builder().put("xpack.ssl.key", keyPath).put("xpack.ssl.certificate", certPath) .putList("xpack.ssl.certificate_authorities", certPath.toString(), clientCertPath.toString()) .put("path.home", createTempDir()).setSecureSettings(secureSettings).build(); Environment env = randomBoolean() ? null : TestEnvironment.newEnvironment(settings); final SSLService sslService = new SSLService(settings, env); final SSLConfiguration config = sslService.sslConfiguration(Settings.EMPTY); new SSLConfigurationReloader(settings, env, sslService, resourceWatcherService) { @Override void reloadSSLContext(SSLConfiguration configuration) { fail("reload should not be called! [pem key reload exception]"); } }; final SSLContext context = sslService.sslContextHolder(config).sslContext(); // truncate the file try (OutputStream os = Files.newOutputStream(keyPath, StandardOpenOption.TRUNCATE_EXISTING)) { } // we intentionally don't wait here as we rely on concurrency to catch a failure assertThat(sslService.sslContextHolder(config).sslContext(), sameInstance(context)); } /** * Tests the reloading of a truststore when there is an exception during reloading. An exception is caused by truncating the truststore * that is being monitored */ public void testTrustStoreReloadException() throws Exception { Path tempDir = createTempDir(); Path trustStorePath = tempDir.resolve("testnode.jks"); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks"), trustStorePath); MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("xpack.ssl.truststore.secure_password", "testnode"); Settings settings = Settings.builder().put("xpack.ssl.truststore.path", trustStorePath) .put("path.home", createTempDir()).setSecureSettings(secureSettings).build(); Environment env = randomBoolean() ? null : TestEnvironment.newEnvironment(settings); final SSLService sslService = new SSLService(settings, env); final SSLConfiguration config = sslService.sslConfiguration(Settings.EMPTY); new SSLConfigurationReloader(settings, env, sslService, resourceWatcherService) { @Override void reloadSSLContext(SSLConfiguration configuration) { fail("reload should not be called! [truststore reload exception]"); } }; final SSLContext context = sslService.sslContextHolder(config).sslContext(); // truncate the truststore try (OutputStream os = Files.newOutputStream(trustStorePath, StandardOpenOption.TRUNCATE_EXISTING)) { } // we intentionally don't wait here as we rely on concurrency to catch a failure assertThat(sslService.sslContextHolder(config).sslContext(), sameInstance(context)); } /** * Tests the reloading of a trust config backed by pem files when there is an exception during reloading. An exception is caused by * truncating the certificate file that is being monitored */ public void testPEMTrustReloadException() throws Exception { Path tempDir = createTempDir(); Path clientCertPath = tempDir.resolve("testclient.crt"); Files.copy(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testclient.crt"), clientCertPath); Settings settings = Settings.builder() .putList("xpack.ssl.certificate_authorities", clientCertPath.toString()) .put("path.home", createTempDir()).build(); Environment env = randomBoolean() ? null : TestEnvironment.newEnvironment(settings); final SSLService sslService = new SSLService(settings, env); final SSLConfiguration config = sslService.sslConfiguration(Settings.EMPTY); new SSLConfigurationReloader(settings, env, sslService, resourceWatcherService) { @Override void reloadSSLContext(SSLConfiguration configuration) { fail("reload should not be called! [pem trust reload exception]"); } }; final SSLContext context = sslService.sslContextHolder(config).sslContext(); // write bad file Path updatedCert = tempDir.resolve("updated.crt"); try (OutputStream os = Files.newOutputStream(updatedCert)) { os.write(randomByte()); } atomicMoveIfPossible(updatedCert, clientCertPath); // we intentionally don't wait here as we rely on concurrency to catch a failure assertThat(sslService.sslContextHolder(config).sslContext(), sameInstance(context)); } private void validateSSLConfigurationIsReloaded(Settings settings, Environment env, Consumer<SSLContext> preChecks, Runnable modificationFunction, Consumer<SSLContext> postChecks) throws Exception { final CountDownLatch reloadLatch = new CountDownLatch(1); final SSLService sslService = new SSLService(settings, env); final SSLConfiguration config = sslService.sslConfiguration(Settings.EMPTY); new SSLConfigurationReloader(settings, env, sslService, resourceWatcherService) { @Override void reloadSSLContext(SSLConfiguration configuration) { super.reloadSSLContext(configuration); reloadLatch.countDown(); } }; // Baseline checks preChecks.accept(sslService.sslContextHolder(config).sslContext()); assertEquals("nothing should have called reload", 1, reloadLatch.getCount()); // modify modificationFunction.run(); reloadLatch.await(); // checks after reload postChecks.accept(sslService.sslContextHolder(config).sslContext()); } private static void atomicMoveIfPossible(Path source, Path target) throws IOException { try { Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); } catch (AtomicMoveNotSupportedException e) { Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); } } private static MockWebServer getSslServer(Path keyStorePath, String keyStorePass) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, KeyManagementException, UnrecoverableKeyException { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); try (InputStream is = Files.newInputStream(keyStorePath)) { keyStore.load(is, keyStorePass.toCharArray()); } final SSLContext sslContext = new SSLContextBuilder().loadKeyMaterial(keyStore, keyStorePass.toCharArray()) .build(); MockWebServer server = new MockWebServer(sslContext, false); server.enqueue(new MockResponse().setResponseCode(200).setBody("body")); server.start(); return server; } private static CloseableHttpClient getSSLClient(Path trustStorePath, String trustStorePass) throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException, IOException, CertificateException { KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); try (InputStream is = Files.newInputStream(trustStorePath)) { trustStore.load(is, trustStorePass.toCharArray()); } final SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(trustStore, null).build(); return HttpClients.custom().setSSLContext(sslContext).build(); } private static void privilegedConnect(CheckedRunnable<Exception> runnable) throws Exception { try { AccessController.doPrivileged((PrivilegedExceptionAction<Void>) () -> { runnable.run(); return null; }); } catch (PrivilegedActionException e) { throw (Exception) e.getCause(); } } }