Skip to content

Commit

Permalink
Issue #5019 - add testing for the SslKeyStoreScanner
Browse files Browse the repository at this point in the history
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
  • Loading branch information
lachlan-roberts committed Jul 10, 2020
1 parent 24d0e50 commit 13e034f
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//

package org.eclipse.jetty.ssl.reload;

import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.Calendar;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.log.StacklessLogging;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class KeystoreReloadTest
{
private static final int scanInterval = 1;
private Server server;

public static void useKeystore(String keystore) throws Exception
{
Path keystoreDir = MavenTestingUtils.getTestResourcePath("keystoreDir");
Path keystorePath = keystoreDir.resolve("keystore");
if (Files.exists(keystorePath))
Files.delete(keystorePath);

if (keystore == null)
return;

Files.copy(MavenTestingUtils.getTestResourceFile(keystore).toPath(), keystorePath);
keystorePath.toFile().deleteOnExit();
}

@BeforeEach
public void start() throws Exception
{
useKeystore("oldKeystore");

String keystore = MavenTestingUtils.getTestResourceFile("keystoreDir/keystore").getAbsolutePath();
SslContextFactory sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath(keystore);
sslContextFactory.setKeyStorePassword("storepwd");
sslContextFactory.setKeyManagerPassword("keypwd");

server = new Server();
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
HttpConfiguration httpsConfig = new HttpConfiguration();
httpsConfig.addCustomizer(new SecureRequestCustomizer());
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpsConfig);
ServerConnector connector = new ServerConnector(server, sslConnectionFactory, httpConnectionFactory);
connector.setPort(8443);
server.addConnector(connector);

// Configure Keystore Reload.
SslKeyStoreScanner keystoreScanner = new SslKeyStoreScanner(sslContextFactory);
keystoreScanner.setScanInterval(scanInterval);
server.addBean(keystoreScanner);

server.start();
}

@AfterEach
public void stop() throws Exception
{
server.stop();
}

@Test
public void testKeystoreHotReload() throws Exception
{
URL serverUrl = server.getURI().toURL();

// Check the original certificate expiry.
X509Certificate cert1 = getCertificateFromUrl(serverUrl);
assertThat(getExpiryYear(cert1), is(2015));

// Switch to use newKeystore which has a later expiry date.
useKeystore("newKeystore");
Thread.sleep(Duration.ofSeconds(scanInterval * 2).toMillis());

// The scanner should have detected the updated keystore, expiry should be renewed.
X509Certificate cert2 = getCertificateFromUrl(serverUrl);
assertThat(getExpiryYear(cert2), is(2020));
}

@Test
public void testReloadWithBadKeystore() throws Exception
{
URL serverUrl = server.getURI().toURL();

// Check the original certificate expiry.
X509Certificate cert1 = getCertificateFromUrl(serverUrl);
assertThat(getExpiryYear(cert1), is(2015));

// Switch to use badKeystore which has the incorrect passwords.
try (StacklessLogging ignored = new StacklessLogging(SslKeyStoreScanner.class))
{
useKeystore("badKeystore");
Thread.sleep(Duration.ofSeconds(scanInterval * 2).toMillis());
}

// The good keystore is removed, now the bad keystore now causes an SSL Handshake exception.
assertThrows(SSLHandshakeException.class, () -> getCertificateFromUrl(serverUrl));
}

@Test
public void testKeystoreRemoval() throws Exception
{
URL serverUrl = server.getURI().toURL();

// Check the original certificate expiry.
X509Certificate cert1 = getCertificateFromUrl(serverUrl);
assertThat(getExpiryYear(cert1), is(2015));

// Delete the keystore.
try (StacklessLogging ignored = new StacklessLogging(SslKeyStoreScanner.class))
{
useKeystore(null);
Thread.sleep(Duration.ofSeconds(scanInterval * 2).toMillis());
}

// The good keystore is removed, having no keystore causes an SSL Handshake exception.
assertThrows(SSLHandshakeException.class, () -> getCertificateFromUrl(serverUrl));

// Switch to use keystore2 which has a later expiry date.
useKeystore("newKeystore");
Thread.sleep(Duration.ofSeconds(scanInterval * 2).toMillis());
X509Certificate cert2 = getCertificateFromUrl(serverUrl);
assertThat(getExpiryYear(cert2), is(2020));
}

public static int getExpiryYear(X509Certificate cert)
{
Calendar instance = Calendar.getInstance();
instance.setTime(cert.getNotAfter());
return instance.get(Calendar.YEAR);
}

public static X509Certificate getCertificateFromUrl(URL serverUrl) throws Exception
{
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(new KeyManager[0], new TrustManager[] {new DefaultTrustManager()}, new SecureRandom());
SSLContext.setDefault(ctx);

HttpsURLConnection connection = (HttpsURLConnection)serverUrl.openConnection();
connection.setHostnameVerifier((a, b) -> true);
connection.connect();
Certificate[] certs = connection.getServerCertificates();
connection.disconnect();

assertThat(certs.length, is(1));
return (X509Certificate)certs[0];
}

private static class DefaultTrustManager implements X509TrustManager
{
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1)
{
}

@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1)
{
}

@Override
public X509Certificate[] getAcceptedIssuers()
{
return null;
}
}
}
Binary file added jetty-ssl-reload/src/test/resources/badKeystore
Binary file not shown.
4 changes: 4 additions & 0 deletions jetty-ssl-reload/src/test/resources/jetty-logging.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
org.eclipse.jetty.deploy.LEVEL=WARN
org.eclipse.jetty.util.Scanner=WARN
org.eclipse.jetty.ssl.reload.LEVEL=DEBUG
Binary file added jetty-ssl-reload/src/test/resources/newKeystore
Binary file not shown.
Binary file added jetty-ssl-reload/src/test/resources/oldKeystore
Binary file not shown.

0 comments on commit 13e034f

Please sign in to comment.