package com.example.sslreload; import com.google.common.io.CharStreams; import com.google.common.io.Files; import com.google.common.io.Resources; import io.dropwizard.Configuration; import io.dropwizard.testing.ConfigOverride; import io.dropwizard.testing.ResourceHelpers; import io.dropwizard.testing.junit.DropwizardAppRule; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.junit.After; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import static org.assertj.core.api.Java6Assertions.assertThat; public class SslReloadAppTest { @ClassRule public static final TemporaryFolder FOLDER = new TemporaryFolder(); private static final X509TrustManager TRUST_ALL = new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }; private static File keystore; @Rule public final DropwizardAppRule<Configuration> rule = new DropwizardAppRule<>(SslReloadApp.class, ResourceHelpers.resourceFilePath("sslreload/config.yml"), ConfigOverride.config("server.applicationConnectors[0].keyStorePath", keystore.getAbsolutePath()), ConfigOverride.config("server.adminConnectors[0].keyStorePath", keystore.getAbsolutePath())); @BeforeClass public static void setupClass() throws IOException { keystore = FOLDER.newFile("keystore.jks"); final byte[] keystoreBytes = Resources.toByteArray(Resources.getResource("sslreload/keystore.jks")); Files.write(keystoreBytes, keystore); } @After public void after() throws IOException { // Reset keystore to known good keystore final byte[] keystoreBytes = Resources.toByteArray(Resources.getResource("sslreload/keystore.jks")); Files.write(keystoreBytes, keystore); } @Test public void reloadCertificateChangesTheServerCertificate() throws Exception { // Copy over our new keystore that has our new certificate to the current // location of our keystore final byte[] keystore2Bytes = Resources.toByteArray(Resources.getResource("sslreload/keystore2.jks")); Files.write(keystore2Bytes, keystore); // Get the bytes for the first certificate, and trigger a reload byte[] firstCertBytes = certBytes(200, "Reloaded certificate configuration\n"); // Get the bytes from our newly reloaded certificate byte[] secondCertBytes = certBytes(200, "Reloaded certificate configuration\n"); // Get the bytes from the reloaded certificate, but it should be the same // as the second cert because we didn't change anything! byte[] thirdCertBytes = certBytes(200, "Reloaded certificate configuration\n"); assertThat(firstCertBytes).isNotEqualTo(secondCertBytes); assertThat(secondCertBytes).isEqualTo(thirdCertBytes); } @Test public void badReloadDoesNotChangeTheServerCertificate() throws Exception { // This keystore has a different password than what jetty has been configured with // the password is "password2" final byte[] badKeystore = Resources.toByteArray(Resources.getResource("sslreload/keystore-diff-pwd.jks")); Files.write(badKeystore, keystore); // Get the bytes for the first certificate. The reload should fail byte[] firstCertBytes = certBytes(500, "Keystore was tampered with, or password was incorrect"); // Issue another request. The returned certificate should be the same as before byte[] secondCertBytes = certBytes(500, "Keystore was tampered with, or password was incorrect"); // And just to triple check, a third request will continue with // the same original certificate byte[] thirdCertBytes = certBytes(500, "Keystore was tampered with, or password was incorrect"); assertThat(firstCertBytes) .isEqualTo(secondCertBytes) .isEqualTo(thirdCertBytes); } /** Issues a POST against the reload ssl admin task, asserts that the code and content * are as expected, and finally returns the server certificate */ private byte[] certBytes(int code, String content) throws Exception { final URL url = new URL("https://localhost:" + rule.getAdminPort() + "/tasks/reload-ssl"); final HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); try { postIt(conn); assertThat(conn.getResponseCode()).isEqualTo(code); if (code == 200) { assertThat(CharStreams.toString(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) .isEqualTo(content); } else { assertThat(CharStreams.toString(new InputStreamReader(conn.getErrorStream(), StandardCharsets.UTF_8))) .contains(content); } // The certificates are self signed, so are the only cert in the chain. // Thus, we return the one and only certificate. return conn.getServerCertificates()[0].getEncoded(); } finally { conn.disconnect(); } } /** Configure SSL and POST request parameters */ private void postIt(HttpsURLConnection conn) throws Exception { final SSLContext sslCtx = SSLContext.getInstance("TLS"); sslCtx.init(null, new TrustManager[]{TRUST_ALL}, null); conn.setHostnameVerifier(new NoopHostnameVerifier()); conn.setSSLSocketFactory(sslCtx.getSocketFactory()); // Make it a POST conn.setDoOutput(true); conn.getOutputStream().write(new byte[]{}); } }