Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.util.ssl.KeyStoreScanner;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.Scheduler;
Expand Down Expand Up @@ -245,6 +246,9 @@ public HttpServer(HttpServerInfo httpServerInfo,
if (config.getKeyManagerPassword() != null) {
sslContextFactory.setKeyManagerPassword(config.getKeyManagerPassword());
}
KeyStoreScanner keyStoreScanner = new KeyStoreScanner(sslContextFactory);
keyStoreScanner.setScanInterval(config.getKeystoreScanIntervalSeconds());
server.addBean(keyStoreScanner);
}
if (config.getTrustStorePath() != null) {
Optional<KeyStore> pemTrustStore = tryLoadPemTrustStore(config);
Expand Down Expand Up @@ -329,6 +333,9 @@ public HttpServer(HttpServerInfo httpServerInfo,
-1,
sslConnectionFactory,
new HttpConnectionFactory(adminConfiguration));
KeyStoreScanner keyStoreScanner = new KeyStoreScanner(sslContextFactory);
keyStoreScanner.setScanInterval(config.getKeystoreScanIntervalSeconds());
adminConnector.addBean(keyStoreScanner);
}
else {
HttpConnectionFactory http1 = new HttpConnectionFactory(adminConfiguration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ public enum AuthorizationPolicy
private Set<String> defaultAllowedRoles = ImmutableSet.of();
private boolean allowUnsecureRequestsInAuthorizer;

private int keyStoreScanIntervalSeconds;

public boolean isHttpEnabled()
{
return httpEnabled;
Expand Down Expand Up @@ -823,4 +825,17 @@ public List<HttpComplianceViolation> getHttpComplianceViolations()
{
return httpComplianceViolations;
}

@Config("http-server.https.keystore.scan-interval-seconds")
@ConfigDescription("Interval (in seconds) to check the keystore file for changes")
public HttpServerConfig setKeystoreScanIntervalSeconds(int keyStoreScanIntervalSeconds)
{
this.keyStoreScanIntervalSeconds = keyStoreScanIntervalSeconds;
return this;
}

public int getKeystoreScanIntervalSeconds()
{
return keyStoreScanIntervalSeconds;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ public void testDefaults()
.setAllowUnsecureRequestsInAuthorizer(false)
.setSniHostCheck(true)
.setUriComplianceMode(UriCompliance.DEFAULT)
.setHttpComplianceViolations(""));
.setHttpComplianceViolations("")
.setKeystoreScanIntervalSeconds(0));
}

@Test
Expand Down Expand Up @@ -149,6 +150,7 @@ public void testExplicitPropertyMappings()
.put("http-server.https.sni-host-check", "false")
.put("http-server.uri-compliance.mode", "LEGACY")
.put("http-server.http-compliance.violations", "UNSAFE_HOST_HEADER,MISMATCHED_AUTHORITY")
.put("http-server.https.keystore.scan-interval-seconds", "2")
.build();

HttpServerConfig expected = new HttpServerConfig()
Expand Down Expand Up @@ -203,7 +205,8 @@ public void testExplicitPropertyMappings()
.setAllowUnsecureRequestsInAuthorizer(true)
.setSniHostCheck(false)
.setUriComplianceMode(UriCompliance.LEGACY)
.setHttpComplianceViolations("UNSAFE_HOST_HEADER,MISMATCHED_AUTHORITY");
.setHttpComplianceViolations("UNSAFE_HOST_HEADER,MISMATCHED_AUTHORITY")
.setKeystoreScanIntervalSeconds(2);

ConfigAssertions.assertFullMapping(properties, expected);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,23 @@
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

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.UncheckedIOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.concurrent.ExecutionException;
Expand All @@ -60,6 +70,7 @@
import static com.google.common.io.Resources.getResource;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.createTempDirectory;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
Expand All @@ -76,6 +87,34 @@ public class TestHttpServerProvider
private HttpServerConfig config;
private HttpServerInfo httpServerInfo;

@DataProvider
public static Object[][] serverAdminMode()
{
return new Object[][] {{false}, {true}};
}

private static HttpServlet createCertTestServlet()
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accidental move with code formatting, but good to have this private static on top

{
return new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException
{
X509Certificate[] certs = (X509Certificate[]) request.getAttribute("jakarta.servlet.request.X509Certificate");
if ((certs == null) || (certs.length == 0)) {
throw new RuntimeException("No client certificate");
}
if (certs.length > 1) {
throw new RuntimeException("Received multiple client certificates");
}
X509Certificate cert = certs[0];
response.getWriter().write(cert.getSubjectX500Principal().getName());
response.setStatus(HttpServletResponse.SC_OK);
}
};
}

@BeforeSuite
public void setupSuite()
{
Expand Down Expand Up @@ -339,6 +378,50 @@ public void testClientCertificateJava()
}
}

@Test(dataProvider = "serverAdminMode")
public void testKeystoreUpdate(boolean serverAdminMode)
throws Exception
{
tempDir = createTempDirectory("test-keystore").toFile().getCanonicalFile();
Path tempKeyStore = tempDir.toPath().resolve("server.keystore").toAbsolutePath();
Path originalKeyStore = Path.of(getResource("clientcert-java/server.keystore").getPath());
Path updatedKeyStore = Path.of(getResource("clientcert-java/updatedServer.keystore").getPath());

// Start the server with the original keystore
Files.copy(originalKeyStore, tempKeyStore, REPLACE_EXISTING);

int keyStoreScanIntervalSeconds = 1;
config.setHttpEnabled(false)
.setAdminEnabled(serverAdminMode)
.setHttpsEnabled(true)
.setHttpsPort(0)
.setKeystorePath(tempKeyStore.toString())
.setKeystorePassword("airlift")
.setKeystoreScanIntervalSeconds(keyStoreScanIntervalSeconds);

createAndStartServer();

// Original certificate subject is returned
assertEquals(getServerCertSubject(), "CN=localhost,OU=Server,O=Airlift,L=Palo Alto,ST=CA,C=US");

// Replace the keystore with the replacement keystore
Files.copy(updatedKeyStore, tempKeyStore, REPLACE_EXISTING);

Thread.sleep(keyStoreScanIntervalSeconds * 1000L); // wait for the first scan to complete
// Poll every 100ms up to 2 seconds for the updated keystore to be picked up
for (int i = 0; i < 20; i++) {
try {
// Updated certificate subject is returned
assertEquals(getServerCertSubject(), "CN=localhost,OU=Server,O=updated,L=Palo Alto,ST=CA,C=US");
return;
}
catch (AssertionError e) {
Thread.sleep(100);
}
}
fail("Updated keystore was not picked up within expected time");
}

@Test
public void testClientCertificatePem()
throws Exception
Expand Down Expand Up @@ -373,28 +456,6 @@ private void assertClientCertificateRequest(HttpClientConfig clientConfig)
}
}

private static HttpServlet createCertTestServlet()
{
return new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException
{
X509Certificate[] certs = (X509Certificate[]) request.getAttribute("jakarta.servlet.request.X509Certificate");
if ((certs == null) || (certs.length == 0)) {
throw new RuntimeException("No client certificate");
}
if (certs.length > 1) {
throw new RuntimeException("Received multiple client certificates");
}
X509Certificate cert = certs[0];
response.getWriter().write(cert.getSubjectX500Principal().getName());
response.setStatus(HttpServletResponse.SC_OK);
}
};
}

@Test
public void testShowStackTraceEnabled()
throws Exception
Expand Down Expand Up @@ -529,6 +590,40 @@ private void createAndStartServer()
createAndStartServer(new DummyServlet());
}

private String getServerCertSubject()
throws NoSuchAlgorithmException, KeyManagementException, IOException
{
SSLContext trustAllSSLContext = SSLContext.getInstance("TLS");
trustAllSSLContext.init(null, new TrustManager[] {new X509TrustManager()
{
public void checkClientTrusted(X509Certificate[] certs, String authType)
{
}

public void checkServerTrusted(X509Certificate[] certs, String authType)
{
}

public X509Certificate[] getAcceptedIssuers()
{
return null;
}
}}, new SecureRandom());

URI uri = httpServerInfo.getHttpsUri();
HttpsURLConnection conn = (HttpsURLConnection) uri.toURL().openConnection();
conn.setSSLSocketFactory(trustAllSSLContext.getSocketFactory());
conn.connect();

X509Certificate[] certs = (X509Certificate[]) conn.getServerCertificates();
assertTrue(certs.length > 0, String.format("No certificates were obtained for URI: %s", uri));
// Return the leaf certificate subject
String subject = certs[0].getSubjectX500Principal().getName();
conn.disconnect();

return subject;
}

private void createAndStartServer(HttpServlet servlet)
throws Exception
{
Expand Down
10 changes: 10 additions & 0 deletions http-server/src/test/resources/clientcert-java/clientcert.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,13 @@ openssl pkcs12 -name client -inkey client.key -in client.crt -export -passout pa
# create client truststore
keytool -import -noprompt -alias ca -file ca.crt -storetype pkcs12 -storepass airlift -keystore client.truststore


## We also create a server kesytore for testing the KeyStoreScanner

# create updated server cert
openssl req -new -key server.key -subj "/C=US/ST=CA/L=Palo Alto/O=updated/OU=Server/CN=localhost" -out updatedServer.csr
openssl x509 -req -days 9999 -in updatedServer.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out updatedServer.crt

# create updated server keystore
openssl pkcs12 -name server -inkey server.key -in updatedServer.crt -export -passout pass:airlift -out updatedServer.keystore
keytool -import -noprompt -alias ca -file ca.crt -storetype pkcs12 -storepass airlift -keystore updatedServer.keystore
Binary file not shown.