From d903cfa093a6e65a7967a3efa76119d860e92220 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Wed, 30 May 2018 08:24:38 +0300 Subject: [PATCH] Reintroduces SSL tests These tests were removed when backporting a PR to 6.x. The PR itself was regarding moving security tests from plugin/scr and did not originally touch these files. --- .../xpack/ssl/SSLClientAuthTests.java | 132 +++++++++ .../xpack/ssl/SSLReloadIntegTests.java | 183 +++++++++++++ .../xpack/ssl/SSLTrustRestrictionsTests.java | 257 ++++++++++++++++++ 3 files changed, 572 insertions(+) create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLClientAuthTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLReloadIntegTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLTrustRestrictionsTests.java diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLClientAuthTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLClientAuthTests.java new file mode 100644 index 0000000000000..6d98062ffb124 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLClientAuthTests.java @@ -0,0 +1,132 @@ +/* + * 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.ssl; + +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.message.BasicHeader; +import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy; +import org.apache.http.ssl.SSLContexts; +import org.apache.http.util.EntityUtils; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.test.SecurityIntegTestCase; +import org.elasticsearch.transport.Transport; +import org.elasticsearch.xpack.core.TestXPackTransportClient; +import org.elasticsearch.xpack.core.security.SecurityField; +import org.elasticsearch.xpack.core.ssl.SSLClientAuth; +import org.elasticsearch.xpack.security.LocalStateSecurity; + + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.CertPathBuilderException; + +import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; + +public class SSLClientAuthTests extends SecurityIntegTestCase { + @Override + protected Settings nodeSettings(int nodeOrdinal) { + return Settings.builder() + .put(super.nodeSettings(nodeOrdinal)) + // invert the require auth settings + .put("xpack.ssl.client_authentication", SSLClientAuth.REQUIRED) + .put("xpack.security.http.ssl.enabled", true) + .put("xpack.security.http.ssl.client_authentication", SSLClientAuth.REQUIRED) + .put("transport.profiles.default.xpack.security.ssl.client_authentication", SSLClientAuth.NONE) + .put(NetworkModule.HTTP_ENABLED.getKey(), true) + .build(); + } + + @Override + protected boolean transportSSLEnabled() { + return true; + } + + public void testThatHttpFailsWithoutSslClientAuth() throws IOException { + SSLIOSessionStrategy sessionStrategy = new SSLIOSessionStrategy(SSLContexts.createDefault(), NoopHostnameVerifier.INSTANCE); + try (RestClient restClient = createRestClient(httpClientBuilder -> httpClientBuilder.setSSLStrategy(sessionStrategy), "https")) { + restClient.performRequest("GET", "/"); + fail("Expected SSLHandshakeException"); + } catch (SSLHandshakeException e) { + Throwable t = ExceptionsHelper.unwrap(e, CertPathBuilderException.class); + assertThat(t, instanceOf(CertPathBuilderException.class)); + assertThat(t.getMessage(), containsString("unable to find valid certification path to requested target")); + } + } + + public void testThatHttpWorksWithSslClientAuth() throws IOException { + SSLIOSessionStrategy sessionStrategy = new SSLIOSessionStrategy(getSSLContext(), NoopHostnameVerifier.INSTANCE); + try (RestClient restClient = createRestClient(httpClientBuilder -> httpClientBuilder.setSSLStrategy(sessionStrategy), "https")) { + Response response = restClient.performRequest("GET", "/", + new BasicHeader("Authorization", basicAuthHeaderValue(transportClientUsername(), transportClientPassword()))); + assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); + assertThat(EntityUtils.toString(response.getEntity()), containsString("You Know, for Search")); + } + } + + public void testThatTransportWorksWithoutSslClientAuth() throws IOException { + // specify an arbitrary keystore, that does not include the certs needed to connect to the transport protocol + Path store = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testclient-client-profile.jks"); + + if (Files.notExists(store)) { + throw new ElasticsearchException("store path doesn't exist"); + } + + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("xpack.ssl.keystore.secure_password", "testclient-client-profile"); + Settings settings = Settings.builder() + .put("xpack.security.transport.ssl.enabled", true) + .put("xpack.ssl.client_authentication", SSLClientAuth.NONE) + .put("xpack.ssl.keystore.path", store) + .setSecureSettings(secureSettings) + .put("cluster.name", internalCluster().getClusterName()) + .put(SecurityField.USER_SETTING.getKey(), + transportClientUsername() + ":" + new String(transportClientPassword().getChars())) + .build(); + try (TransportClient client = new TestXPackTransportClient(settings, LocalStateSecurity.class)) { + Transport transport = internalCluster().getDataNodeInstance(Transport.class); + TransportAddress transportAddress = transport.boundAddress().publishAddress(); + client.addTransportAddress(transportAddress); + + assertGreenClusterState(client); + } + } + + private SSLContext getSSLContext() { + try (InputStream in = + Files.newInputStream(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testclient.jks"))) { + KeyStore keyStore = KeyStore.getInstance("jks"); + keyStore.load(in, "testclient".toCharArray()); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keyStore); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, "testclient".toCharArray()); + SSLContext context = SSLContext.getInstance("TLSv1.2"); + context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom()); + return context; + } catch (Exception e) { + throw new ElasticsearchException("failed to initialize a TrustManagerFactory", e); + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLReloadIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLReloadIntegTests.java new file mode 100644 index 0000000000000..c6746c49446d2 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLReloadIntegTests.java @@ -0,0 +1,183 @@ +/* + * 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.ssl; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Time; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.network.InetAddressHelper; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.test.SecurityIntegTestCase; +import org.elasticsearch.test.SecuritySettingsSource; +import org.elasticsearch.test.SecuritySettingsSourceField; +import org.elasticsearch.transport.Transport; +import org.elasticsearch.xpack.core.ssl.CertUtils; +import org.elasticsearch.xpack.core.ssl.SSLService; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.SocketException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Locale; +import java.util.concurrent.CountDownLatch; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +/** + * Integration tests for SSL reloading support + */ +public class SSLReloadIntegTests extends SecurityIntegTestCase { + + private Path nodeStorePath; + + @Override + public Settings nodeSettings(int nodeOrdinal) { + if (nodeStorePath == null) { + Path origPath = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks"); + Path tempDir = createTempDir(); + nodeStorePath = tempDir.resolve("testnode.jks"); + try { + Files.copy(origPath, nodeStorePath); + } catch (IOException e) { + throw new ElasticsearchException("failed to copy keystore"); + } + } + Settings settings = super.nodeSettings(nodeOrdinal); + Settings.Builder builder = Settings.builder() + .put(settings.filter((s) -> s.startsWith("xpack.ssl.") == false)); + + + SecuritySettingsSource.addSSLSettingsForStore(builder, + "/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks", "testnode"); + builder.put("resource.reload.interval.high", "1s") + .put("xpack.ssl.keystore.path", nodeStorePath); + + if (builder.get("xpack.ssl.truststore.path") != null) { + builder.put("xpack.ssl.truststore.path", nodeStorePath); + } + + return builder.build(); + } + + @Override + protected boolean transportSSLEnabled() { + return true; + } + + public void testThatSSLConfigurationReloadsOnModification() throws Exception { + KeyPair keyPair = CertUtils.generateKeyPair(randomFrom(1024, 2048)); + X509Certificate certificate = getCertificate(keyPair); + KeyStore keyStore = KeyStore.getInstance("jks"); + keyStore.load(null, null); + keyStore.setKeyEntry("key", keyPair.getPrivate(), SecuritySettingsSourceField.TEST_PASSWORD.toCharArray(), + new Certificate[] { certificate }); + Path keystorePath = createTempDir().resolve("newcert.jks"); + try (OutputStream out = Files.newOutputStream(keystorePath)) { + keyStore.store(out, SecuritySettingsSourceField.TEST_PASSWORD.toCharArray()); + } + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("xpack.ssl.keystore.secure_password", SecuritySettingsSourceField.TEST_PASSWORD); + secureSettings.setString("xpack.ssl.truststore.secure_password", "testnode"); + Settings settings = Settings.builder() + .put("path.home", createTempDir()) + .put("xpack.ssl.keystore.path", keystorePath) + .put("xpack.ssl.truststore.path", nodeStorePath) + .setSecureSettings(secureSettings) + .build(); + String node = randomFrom(internalCluster().getNodeNames()); + SSLService sslService = new SSLService(settings, TestEnvironment.newEnvironment(settings)); + SSLSocketFactory sslSocketFactory = sslService.sslSocketFactory(settings); + TransportAddress address = internalCluster() + .getInstance(Transport.class, node).boundAddress().publishAddress(); + try (SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(address.getAddress(), address.getPort())) { + assertThat(socket.isConnected(), is(true)); + socket.startHandshake(); + fail("handshake should not have been successful!"); + } catch (SSLHandshakeException | SocketException expected) { + logger.trace("expected exception", expected); + } + + KeyStore nodeStore = KeyStore.getInstance("jks"); + try (InputStream in = Files.newInputStream(nodeStorePath)) { + nodeStore.load(in, "testnode".toCharArray()); + } + nodeStore.setCertificateEntry("newcert", certificate); + Path path = nodeStorePath.getParent().resolve("updated.jks"); + try (OutputStream out = Files.newOutputStream(path)) { + nodeStore.store(out, "testnode".toCharArray()); + } + try { + Files.move(path, nodeStorePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(path, nodeStorePath, StandardCopyOption.REPLACE_EXISTING); + } + + CountDownLatch latch = new CountDownLatch(1); + assertBusy(() -> { + try (SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(address.getAddress(), address.getPort())) { + logger.info("opened socket for reloading [{}]", socket); + socket.addHandshakeCompletedListener(event -> { + try { + assertThat(event.getPeerPrincipal().getName(), containsString("Test Node")); + logger.info("ssl handshake completed on port [{}]", event.getSocket().getLocalPort()); + latch.countDown(); + } catch (Exception e) { + fail("caught exception in listener " + e.getMessage()); + } + }); + socket.startHandshake(); + + } catch (Exception e) { + fail("caught exception " + e.getMessage()); + } + }); + latch.await(); + } + + private X509Certificate getCertificate(KeyPair keyPair) throws Exception { + final DateTime notBefore = new DateTime(DateTimeZone.UTC); + final DateTime notAfter = notBefore.plusYears(1); + X500Name subject = new X500Name("CN=random cert"); + JcaX509v3CertificateBuilder builder = + new JcaX509v3CertificateBuilder(subject, CertUtils.getSerial(), + new Time(notBefore.toDate(), Locale.ROOT), new Time(notAfter.toDate(), Locale.ROOT), subject, keyPair.getPublic()); + + JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(); + builder.addExtension(Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(keyPair.getPublic())); + builder.addExtension(Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(keyPair.getPublic())); + builder.addExtension(Extension.subjectAlternativeName, false, + CertUtils.getSubjectAlternativeNames(true, Sets.newHashSet(InetAddressHelper.getAllAddresses()))); + + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); + X509CertificateHolder certificateHolder = builder.build(signer); + return new JcaX509CertificateConverter().getCertificate(certificateHolder); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLTrustRestrictionsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLTrustRestrictionsTests.java new file mode 100644 index 0000000000000..b97a190b86890 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLTrustRestrictionsTests.java @@ -0,0 +1,257 @@ +/* + * 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.ssl; + +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.security.auth.x500.X500Principal; +import java.io.BufferedWriter; +import java.io.IOException; +import java.net.SocketException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.test.SecurityIntegTestCase; +import org.elasticsearch.test.junit.annotations.TestLogging; +import org.elasticsearch.transport.Transport; +import org.elasticsearch.xpack.core.ssl.CertUtils; +import org.elasticsearch.xpack.core.ssl.SSLService; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import static org.elasticsearch.xpack.core.ssl.CertUtils.generateSignedCertificate; +import static org.hamcrest.Matchers.is; + +/** + * Integration tests for SSL trust restrictions + * + * @see RestrictedTrustManager + */ +@ESIntegTestCase.ClusterScope(numDataNodes = 1, numClientNodes = 0, supportsDedicatedMasters = false) +@TestLogging("org.elasticsearch.xpack.ssl.RestrictedTrustManager:DEBUG") +public class SSLTrustRestrictionsTests extends SecurityIntegTestCase { + + /** + * Use a small keysize for performance, since the keys are only used in this test, but a large enough keysize + * to get past the SSL algorithm checker + */ + private static final int KEYSIZE = 1024; + + private static final int RESOURCE_RELOAD_MILLIS = 3; + private static final TimeValue MAX_WAIT_RELOAD = TimeValue.timeValueSeconds(1); + + private static Path configPath; + private static Settings nodeSSL; + + private static CertificateInfo ca; + private static CertificateInfo trustedCert; + private static CertificateInfo untrustedCert; + private static Path restrictionsPath; + + @Override + protected int maxNumberOfNodes() { + // We are trying to test the SSL configuration for which clients/nodes may join a cluster + // We prefer the cluster to only have 1 node, so that the SSL checking doesn't happen until the test methods run + // (That's not _quite_ true, because the base setup code checks the cluster using transport client, but it's the best we can do) + return 1; + } + + @BeforeClass + public static void setupCertificates() throws Exception { + configPath = createTempDir(); + + final KeyPair caPair = CertUtils.generateKeyPair(KEYSIZE); + final X509Certificate caCert = CertUtils.generateCACertificate(new X500Principal("cn=CertAuth"), caPair, 30); + ca = writeCertificates("ca", caPair.getPrivate(), caCert); + + trustedCert = generateCertificate("trusted", "node.trusted"); + untrustedCert = generateCertificate("untrusted", "someone.else"); + + nodeSSL = Settings.builder() + .put("xpack.security.transport.ssl.enabled", true) + .put("xpack.security.transport.ssl.verification_mode", "certificate") + .putList("xpack.ssl.certificate_authorities", ca.getCertPath().toString()) + .put("xpack.ssl.key", trustedCert.getKeyPath()) + .put("xpack.ssl.certificate", trustedCert.getCertPath()) + .build(); + } + + @AfterClass + public static void cleanup() { + configPath = null; + nodeSSL = null; + ca = null; + trustedCert = null; + untrustedCert = null; + } + + @Override + public Settings nodeSettings(int nodeOrdinal) { + + Settings parentSettings = super.nodeSettings(nodeOrdinal); + Settings.Builder builder = Settings.builder() + .put(parentSettings.filter((s) -> s.startsWith("xpack.ssl.") == false)) + .put(nodeSSL); + + restrictionsPath = configPath.resolve("trust_restrictions.yml"); + writeRestrictions("*.trusted"); + builder.put("xpack.ssl.trust_restrictions.path", restrictionsPath); + builder.put("resource.reload.interval.high", RESOURCE_RELOAD_MILLIS + "ms"); + + return builder.build(); + } + + private void writeRestrictions(String trustedPattern) { + try { + Files.write(restrictionsPath, Collections.singleton("trust.subject_name: \"" + trustedPattern + "\"")); + } catch (IOException e) { + throw new ElasticsearchException("failed to write restrictions", e); + } + } + + @Override + protected Settings transportClientSettings() { + Settings parentSettings = super.transportClientSettings(); + Settings.Builder builder = Settings.builder() + .put(parentSettings.filter((s) -> s.startsWith("xpack.ssl.") == false)) + .put(nodeSSL); + return builder.build(); + } + + @Override + protected boolean transportSSLEnabled() { + return true; + } + + public void testCertificateWithTrustedNameIsAccepted() throws Exception { + writeRestrictions("*.trusted"); + try { + tryConnect(trustedCert); + } catch (SSLHandshakeException | SocketException ex) { + fail("handshake should have been successful, but failed with " + ex); + } + } + + public void testCertificateWithUntrustedNameFails() throws Exception { + writeRestrictions("*.trusted"); + try { + tryConnect(untrustedCert); + fail("handshake should have failed, but was successful"); + } catch (SSLHandshakeException | SocketException ex) { + // expected + } + } + + public void testRestrictionsAreReloaded() throws Exception { + writeRestrictions("*"); + assertBusy(() -> { + try { + tryConnect(untrustedCert); + } catch (SSLHandshakeException | SocketException ex) { + fail("handshake should have been successful, but failed with " + ex); + } + }, MAX_WAIT_RELOAD.millis(), TimeUnit.MILLISECONDS); + + writeRestrictions("*.trusted"); + assertBusy(() -> { + try { + tryConnect(untrustedCert); + fail("handshake should have failed, but was successful"); + } catch (SSLHandshakeException | SocketException ex) { + // expected + } + }, MAX_WAIT_RELOAD.millis(), TimeUnit.MILLISECONDS); + } + + private void tryConnect(CertificateInfo certificate) throws Exception { + Settings settings = Settings.builder() + .put("path.home", createTempDir()) + .put("xpack.ssl.key", certificate.getKeyPath()) + .put("xpack.ssl.certificate", certificate.getCertPath()) + .putList("xpack.ssl.certificate_authorities", ca.getCertPath().toString()) + .put("xpack.ssl.verification_mode", "certificate") + .build(); + + String node = randomFrom(internalCluster().getNodeNames()); + SSLService sslService = new SSLService(settings, TestEnvironment.newEnvironment(settings)); + SSLSocketFactory sslSocketFactory = sslService.sslSocketFactory(settings); + TransportAddress address = internalCluster().getInstance(Transport.class, node).boundAddress().publishAddress(); + try (SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(address.getAddress(), address.getPort())) { + assertThat(socket.isConnected(), is(true)); + // The test simply relies on this (synchronously) connecting (or not), so we don't need a handshake handler + socket.startHandshake(); + } + } + + + private static CertificateInfo generateCertificate(String name, String san) throws Exception { + final KeyPair keyPair = CertUtils.generateKeyPair(KEYSIZE); + final X500Principal principal = new X500Principal("cn=" + name); + final GeneralNames altNames = new GeneralNames(CertUtils.createCommonName(san)); + final X509Certificate cert = generateSignedCertificate(principal, altNames, keyPair, ca.getCertificate(), ca.getKey(), 30); + return writeCertificates(name, keyPair.getPrivate(), cert); + } + + private static CertificateInfo writeCertificates(String name, PrivateKey key, X509Certificate cert) throws IOException { + final Path keyPath = writePem(key, name + ".key"); + final Path certPath = writePem(cert, name + ".crt"); + return new CertificateInfo(key, keyPath, cert, certPath); + } + + private static Path writePem(Object obj, String filename) throws IOException { + Path path = configPath.resolve(filename); + Files.deleteIfExists(path); + try (BufferedWriter out = Files.newBufferedWriter(path); + JcaPEMWriter pemWriter = new JcaPEMWriter(out)) { + pemWriter.writeObject(obj); + } + return path; + } + + private static class CertificateInfo { + private final PrivateKey key; + private final Path keyPath; + private final X509Certificate certificate; + private final Path certPath; + + private CertificateInfo(PrivateKey key, Path keyPath, X509Certificate certificate, Path certPath) { + this.key = key; + this.keyPath = keyPath; + this.certificate = certificate; + this.certPath = certPath; + } + + private PrivateKey getKey() { + return key; + } + + private Path getKeyPath() { + return keyPath; + } + + private X509Certificate getCertificate() { + return certificate; + } + + private Path getCertPath() { + return certPath; + } + } +}