diff --git a/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java index 2648ffc..6e03b02 100644 --- a/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java +++ b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java @@ -2541,7 +2541,9 @@ private void configureHttpClient(HttpClient client, ClientConnector connector) { client.setDestinationIdleTimeout(idleTimeout); } client.setIdleTimeout(idleTimeout); - addConnectionLogging(client); + if (LowLevelDebugLog.isEnabled()) { + addConnectionLogging(client); + } } private static void addConnectionLogging(HttpClient client) { @@ -2589,6 +2591,9 @@ public void handshakeFailed(Event event, Throwable failure) { } private static void logAlpnLine(String message) { + if (!LowLevelDebugLog.isEnabled()) { + return; + } try { Path parent = ALPN_DEBUG_LOG_PATH.getParent(); if (parent != null) { diff --git a/src/main/java/com/blazemeter/jmeter/http2/core/JMeterJettySslContextFactory.java b/src/main/java/com/blazemeter/jmeter/http2/core/JMeterJettySslContextFactory.java index 10aa005..6c86dac 100644 --- a/src/main/java/com/blazemeter/jmeter/http2/core/JMeterJettySslContextFactory.java +++ b/src/main/java/com/blazemeter/jmeter/http2/core/JMeterJettySslContextFactory.java @@ -1,30 +1,50 @@ package com.blazemeter.jmeter.http2.core; +import static com.blazemeter.jmeter.http2.core.LowLevelDebugLog.lowLevelDebug; + import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.Socket; import java.security.KeyStore; import java.security.Principal; import java.security.PrivateKey; +import java.security.cert.CRL; import java.security.cert.X509Certificate; +import java.util.Collection; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; import javax.net.ssl.X509ExtendedKeyManager; import javax.net.ssl.X509KeyManager; import org.apache.jmeter.util.JsseSSLManager; import org.apache.jmeter.util.SSLManager; import org.apache.jmeter.util.keystore.JmeterKeyStore; import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class JMeterJettySslContextFactory extends SslContextFactory.Client { + private static final Logger LOG = LoggerFactory.getLogger(JMeterJettySslContextFactory.class); + private final JmeterKeyStore keys; public JMeterJettySslContextFactory() { setTrustAll(true); + setValidatePeerCerts(false); String keyStorePath = System.getProperty("javax.net.ssl.keyStore"); if (keyStorePath != null && !keyStorePath.isEmpty()) { - setKeyStorePath("file://" + keyStorePath); + if (SslStorePathResolver.isFileBasedStoreLocation(keyStorePath)) { + String jettyKeyStoreUri = SslStorePathResolver.toJettyFileUri(keyStorePath); + String keyStoreType = SslStorePathResolver.resolveKeyStoreType(keyStorePath); + lowLevelDebug( + "SSL keyStore path resolved: javax.net.ssl.keyStore='{}' -> jettyUri='{}'", + keyStorePath, jettyKeyStoreUri); + lowLevelDebug( + "SSL keyStore type resolved: javax.net.ssl.keyStoreType='{}' -> jettyType='{}'", + System.getProperty("javax.net.ssl.keyStoreType"), keyStoreType); + configureKeyStorePathForJetty(keyStorePath, jettyKeyStoreUri, keyStoreType); + } keys = getKeyStore((JsseSSLManager) SSLManager.getInstance()); /* we need to set password after getting keystore since getKeystore may ask the user for the @@ -37,7 +57,17 @@ public JMeterJettySslContextFactory() { String truststore = System.getProperty("javax.net.ssl.trustStore"); if (truststore != null && !truststore.isEmpty()) { - setTrustStorePath("file://" + truststore); + if (SslStorePathResolver.isFileBasedStoreLocation(truststore)) { + String jettyTrustStoreUri = SslStorePathResolver.toJettyFileUri(truststore); + String trustStoreType = SslStorePathResolver.resolveTrustStoreType(truststore); + lowLevelDebug( + "SSL trustStore path resolved: javax.net.ssl.trustStore='{}' -> jettyUri='{}'", + truststore, jettyTrustStoreUri); + lowLevelDebug( + "SSL trustStore type resolved: javax.net.ssl.trustStoreType='{}' -> jettyType='{}'", + System.getProperty("javax.net.ssl.trustStoreType"), trustStoreType); + configureTrustStorePathForJetty(truststore, jettyTrustStoreUri, trustStoreType); + } getTrustStore((JsseSSLManager) SSLManager.getInstance()); /* we need to set password after getting truststore since getTrustStore may ask the user for the @@ -47,6 +77,30 @@ public JMeterJettySslContextFactory() { } } + void configureKeyStorePathForJetty(String originalPath, String jettyUri, String storeType) { + try { + setKeyStorePath(jettyUri); + setKeyStoreType(storeType); + } catch (RuntimeException e) { + LOG.warn("Could not set Jetty keyStore path for '{}': {}. " + + "Client certificate authentication may not work.", + originalPath, e.getMessage()); + lowLevelDebug("Could not set Jetty keyStore path for '{}'", originalPath, e); + } + } + + void configureTrustStorePathForJetty(String originalPath, String jettyUri, String storeType) { + try { + setTrustStorePath(jettyUri); + setTrustStoreType(storeType); + } catch (RuntimeException e) { + LOG.warn("Could not set Jetty trustStore path for '{}': {}. " + + "Trust-all SSL configuration will still be used.", + originalPath, e.getMessage()); + lowLevelDebug("Could not set Jetty trustStore path for '{}'", originalPath, e); + } + } + private JmeterKeyStore getKeyStore(JsseSSLManager sslManager) { try { Method keystoreMethod = SSLManager.class.getDeclaredMethod("getKeyStore"); @@ -77,6 +131,18 @@ protected void checkTrustAll() { protected void checkEndPointIdentificationAlgorithm() { } + // JMeter HTTP uses CustomX509TrustManager which does not validate server certificates. + // Jetty still runs PKIX when a keyStore is configured unless trust managers are overridden. + @Override + protected TrustManager[] getTrustManagers(KeyStore trustStore, + Collection crls) throws Exception { + if (isTrustAll()) { + lowLevelDebug("SSL trust managers: using TRUST_ALL_CERTS (JMeter HTTP parity)"); + return TRUST_ALL_CERTS; + } + return super.getTrustManagers(trustStore, crls); + } + // Overwritten to provide jmeter SSLManager configured keyManagers @Override protected KeyManager[] getKeyManagers(KeyStore keyStore) throws Exception { diff --git a/src/main/java/com/blazemeter/jmeter/http2/core/SslStorePathResolver.java b/src/main/java/com/blazemeter/jmeter/http2/core/SslStorePathResolver.java new file mode 100644 index 0000000..f298b44 --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/core/SslStorePathResolver.java @@ -0,0 +1,77 @@ +package com.blazemeter.jmeter.http2.core; + +import java.io.File; +import java.util.Locale; + +/** + * Converts {@code javax.net.ssl.*} store paths to Jetty {@code file:} URIs. + */ +public final class SslStorePathResolver { + + /** JSSE / JMeter sentinel for non-file keystores (e.g. PKCS#11). */ + public static final String NON_FILE_KEYSTORE_LOCATION = "NONE"; + + private static final String KEY_STORE_TYPE_PROPERTY = "javax.net.ssl.keyStoreType"; + private static final String TRUST_STORE_TYPE_PROPERTY = "javax.net.ssl.trustStoreType"; + private static final String PKCS12 = "pkcs12"; + private static final String JKS = "JKS"; + + private SslStorePathResolver() { + // Utility class. + } + + /** + * Whether the property points at a filesystem keystore (not PKCS#11 {@code NONE}). + */ + public static boolean isFileBasedStoreLocation(final String storePath) { + return storePath != null + && !storePath.isEmpty() + && !NON_FILE_KEYSTORE_LOCATION.equalsIgnoreCase(storePath); + } + + /** + * Same as JMeter ({@code new File(path)}), then {@link File#toURI()} for Jetty. + * + * @param storePath filesystem path from {@code javax.net.ssl.keyStore} or trustStore + * @return Jetty-compatible {@code file:} URI + */ + public static String toJettyFileUri(final String storePath) { + return new File(storePath).getAbsoluteFile().toURI().toString(); + } + + /** + * Resolves the Jetty key store type from {@link #KEY_STORE_TYPE_PROPERTY}, matching + * {@link org.apache.jmeter.util.SSLManager} when unset (.p12 → PKCS12, else JKS). + * + * @param keyStorePath filesystem path from {@code javax.net.ssl.keyStore} + * @return type string for {@link org.eclipse.jetty.util.ssl.SslContextFactory#setKeyStoreType} + */ + public static String resolveKeyStoreType(final String keyStorePath) { + return resolveStoreType(KEY_STORE_TYPE_PROPERTY, keyStorePath, false); + } + + /** + * Resolves the Jetty trust store type from {@link #TRUST_STORE_TYPE_PROPERTY}, or by file + * extension when unset (.p12 / .pfx → PKCS12, else JKS). + * + * @param trustStorePath filesystem path from {@code javax.net.ssl.trustStore} + * @return type string for {@link org.eclipse.jetty.util.ssl.SslContextFactory#setTrustStoreType} + */ + public static String resolveTrustStoreType(final String trustStorePath) { + return resolveStoreType(TRUST_STORE_TYPE_PROPERTY, trustStorePath, true); + } + + private static String resolveStoreType(final String typeProperty, final String storePath, + final boolean inferPfxAsPkcs12) { + String explicit = System.getProperty(typeProperty); + if (explicit != null && !explicit.isEmpty()) { + return explicit; + } + String lower = storePath.toLowerCase(Locale.ENGLISH); + if (lower.endsWith(".p12") || (inferPfxAsPkcs12 && lower.endsWith(".pfx"))) { + return PKCS12; + } + return JKS; + } + +} diff --git a/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClientTest.java b/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClientTest.java index 2233d21..1aa3ed0 100644 --- a/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClientTest.java +++ b/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClientTest.java @@ -73,6 +73,7 @@ import org.apache.jmeter.protocol.http.util.HTTPFileArg; import org.apache.jmeter.samplers.SampleResult; import org.apache.jmeter.util.JMeterUtils; +import org.apache.jmeter.util.SSLManager; import org.assertj.core.api.JUnitSoftAssertions; import org.eclipse.jetty.client.Request; import org.eclipse.jetty.client.ContentResponse; @@ -1391,6 +1392,7 @@ public void shouldGetSuccessResponseWhenServerRequiresClientCertAndOneIsConfigur } finally { System.setProperty(keyStorePropertyName, ""); System.setProperty(keyStorePasswordPropertyName, ""); + SSLManager.reset(); } } diff --git a/src/test/java/com/blazemeter/jmeter/http2/core/JMeterJettySslContextFactoryTest.java b/src/test/java/com/blazemeter/jmeter/http2/core/JMeterJettySslContextFactoryTest.java new file mode 100644 index 0000000..595798c --- /dev/null +++ b/src/test/java/com/blazemeter/jmeter/http2/core/JMeterJettySslContextFactoryTest.java @@ -0,0 +1,170 @@ +package com.blazemeter.jmeter.http2.core; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import com.blazemeter.jmeter.http2.HTTP2TestBase; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.cert.CRL; +import java.util.Collection; +import javax.net.ssl.TrustManager; +import org.apache.jmeter.util.SSLManager; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.After; +import org.junit.Test; + +public class JMeterJettySslContextFactoryTest extends HTTP2TestBase { + + private String previousKeyStore; + private String previousKeyStorePassword; + private String previousTrustStore; + private String previousTrustStorePassword; + private Path tempKeystore; + + @After + public void restoreSystemProperties() throws IOException { + restoreProperty("javax.net.ssl.keyStore", previousKeyStore); + restoreProperty("javax.net.ssl.keyStorePassword", previousKeyStorePassword); + restoreProperty("javax.net.ssl.trustStore", previousTrustStore); + restoreProperty("javax.net.ssl.trustStorePassword", previousTrustStorePassword); + SSLManager.reset(); + if (tempKeystore != null) { + Files.deleteIfExists(tempKeystore); + tempKeystore = null; + } + } + + @Test + public void shouldConstructWithFileBasedKeyStorePath() throws IOException { + tempKeystore = Files.createTempFile("jmeter-jetty-ssl", ".p12"); + try (InputStream in = getClass().getResourceAsStream("keystore.p12")) { + if (in == null) { + throw new IllegalStateException("classpath resource keystore.p12 not found"); + } + in.transferTo(Files.newOutputStream(tempKeystore)); + } + + previousKeyStore = System.getProperty("javax.net.ssl.keyStore"); + previousKeyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword"); + + SSLManager.reset(); + System.setProperty("javax.net.ssl.keyStore", tempKeystore.toString()); + System.setProperty("javax.net.ssl.keyStorePassword", ServerBuilder.KEYSTORE_PASSWORD); + + assertThatCode(JMeterJettySslContextFactory::new).doesNotThrowAnyException(); + } + + @Test + public void shouldUseTrustAllManagersWhenKeyStoreIsConfigured() throws Exception { + tempKeystore = Files.createTempFile("jmeter-jetty-ssl-trust", ".p12"); + try (InputStream in = getClass().getResourceAsStream("keystore.p12")) { + if (in == null) { + throw new IllegalStateException("classpath resource keystore.p12 not found"); + } + in.transferTo(Files.newOutputStream(tempKeystore)); + } + + previousKeyStore = System.getProperty("javax.net.ssl.keyStore"); + previousKeyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword"); + + SSLManager.reset(); + System.setProperty("javax.net.ssl.keyStore", tempKeystore.toString()); + System.setProperty("javax.net.ssl.keyStorePassword", ServerBuilder.KEYSTORE_PASSWORD); + + TestableJMeterJettySslContextFactory factory = new TestableJMeterJettySslContextFactory(); + TrustManager[] trustManagers = factory.getTrustManagersForTest(null, null); + + assertThat(trustManagers).isSameAs(SslContextFactory.TRUST_ALL_CERTS); + } + + @Test + public void shouldConstructWhenKeyStoreIsPkcs11None() { + previousKeyStore = System.getProperty("javax.net.ssl.keyStore"); + previousKeyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword"); + + SSLManager.reset(); + System.setProperty("javax.net.ssl.keyStore", + SslStorePathResolver.NON_FILE_KEYSTORE_LOCATION); + + assertThatCode(JMeterJettySslContextFactory::new).doesNotThrowAnyException(); + } + + @Test + public void shouldNotThrowWhenJettyKeyStorePathDoesNotExist() { + String missingPath = "certs/nonexistent-jmeter-jetty-keystore.p12"; + TestableJMeterJettySslContextFactory factory = new TestableJMeterJettySslContextFactory(); + + assertThatCode(() -> factory.configureKeyStorePathForJetty( + missingPath, + SslStorePathResolver.toJettyFileUri(missingPath), + "pkcs12")) + .doesNotThrowAnyException(); + } + + @Test + public void shouldNotThrowWhenJettyTrustStorePathDoesNotExist() { + String missingPath = "certs/nonexistent-jmeter-jetty-truststore.jks"; + TestableJMeterJettySslContextFactory factory = new TestableJMeterJettySslContextFactory(); + + assertThatCode(() -> factory.configureTrustStorePathForJetty( + missingPath, + SslStorePathResolver.toJettyFileUri(missingPath), + "JKS")) + .doesNotThrowAnyException(); + } + + @Test + public void shouldNotThrowWhenSetKeyStorePathFails() { + FailingKeyStorePathFactory factory = new FailingKeyStorePathFactory(); + + assertThatCode(() -> factory.configureKeyStorePathForJetty( + "certs/keystore.p12", "file:///certs/keystore.p12", "pkcs12")) + .doesNotThrowAnyException(); + } + + @Test + public void shouldNotThrowWhenSetTrustStorePathFails() { + FailingTrustStorePathFactory factory = new FailingTrustStorePathFactory(); + + assertThatCode(() -> factory.configureTrustStorePathForJetty( + "certs/truststore.jks", "file:///certs/truststore.jks", "JKS")) + .doesNotThrowAnyException(); + } + + private static final class TestableJMeterJettySslContextFactory + extends JMeterJettySslContextFactory { + + TrustManager[] getTrustManagersForTest(java.security.KeyStore trustStore, + Collection crls) throws Exception { + return getTrustManagers(trustStore, crls); + } + } + + private static final class FailingKeyStorePathFactory extends JMeterJettySslContextFactory { + + @Override + public void setKeyStorePath(String path) { + throw new IllegalArgumentException("Could not find keyStore at " + path); + } + } + + private static final class FailingTrustStorePathFactory extends JMeterJettySslContextFactory { + + @Override + public void setTrustStorePath(String path) { + throw new IllegalArgumentException("Could not find trustStore at " + path); + } + } + + private static void restoreProperty(String key, String value) { + if (value == null) { + System.clearProperty(key); + } else { + System.setProperty(key, value); + } + } + +} diff --git a/src/test/java/com/blazemeter/jmeter/http2/core/ServerBuilder.java b/src/test/java/com/blazemeter/jmeter/http2/core/ServerBuilder.java index a091dd2..65fc790 100644 --- a/src/test/java/com/blazemeter/jmeter/http2/core/ServerBuilder.java +++ b/src/test/java/com/blazemeter/jmeter/http2/core/ServerBuilder.java @@ -106,6 +106,8 @@ public class ServerBuilder { private final TeardownableServer server = new TeardownableServer(); private final HttpConfiguration httpsConfig = new HttpConfiguration(); private boolean withSSL; + private String serverKeyStorePathOverride; + private String serverKeyStoreTypeOverride; private HTTP2ServerConnectionFactory http2ConnectionFactory; private HttpConnectionFactory http1ConnectionFactory; private HTTP2CServerConnectionFactory http2cConnectionFactory; @@ -139,6 +141,15 @@ public ServerBuilder withSSL() { return this; } + /** + * Uses a filesystem keystore for the server TLS certificate (e.g. an untrusted JKS in tests). + */ + public ServerBuilder withServerKeyStorePath(String keyStorePath, String keyStoreType) { + this.serverKeyStorePathOverride = keyStorePath; + this.serverKeyStoreTypeOverride = keyStoreType; + return this; + } + public ServerBuilder withALPN() { this.ALPN = true; return this; @@ -244,7 +255,13 @@ private List buildConnectionFactories() { private SslContextFactory.Server buildServerSslContextFactory() { SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - sslContextFactory.setKeyStorePath(getKeyStorePathAsUriPathInSSLContextFactoryFormat()); + if (serverKeyStorePathOverride != null) { + sslContextFactory.setKeyStorePath( + SslStorePathResolver.toJettyFileUri(serverKeyStorePathOverride)); + sslContextFactory.setKeyStoreType(serverKeyStoreTypeOverride); + } else { + sslContextFactory.setKeyStorePath(getKeyStorePathAsUriPathInSSLContextFactoryFormat()); + } sslContextFactory.setKeyStorePassword(KEYSTORE_PASSWORD); return sslContextFactory; } diff --git a/src/test/java/com/blazemeter/jmeter/http2/core/SslPkixSimulationTest.java b/src/test/java/com/blazemeter/jmeter/http2/core/SslPkixSimulationTest.java new file mode 100644 index 0000000..42736c4 --- /dev/null +++ b/src/test/java/com/blazemeter/jmeter/http2/core/SslPkixSimulationTest.java @@ -0,0 +1,216 @@ +package com.blazemeter.jmeter.http2.core; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.blazemeter.jmeter.http2.HTTP2TestBase; +import com.blazemeter.jmeter.http2.core.ServerBuilder.TeardownableServer; +import com.blazemeter.jmeter.http2.sampler.HTTP2Sampler; +import com.blazemeter.jmeter.http2.sampler.JMeterTestUtils; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSocket; +import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult; +import org.apache.jmeter.protocol.http.util.HTTPConstants; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.jmeter.util.SSLManager; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Reproduces BlazeMeter-like PKIX failures: client {@code javax.net.ssl.keyStore} configured + * (JKS) while the server presents a certificate that is not in the JVM trust store. + */ +public class SslPkixSimulationTest extends HTTP2TestBase { + + private static final String KEY_STORE_PROPERTY = "javax.net.ssl.keyStore"; + private static final String KEY_STORE_PASSWORD_PROPERTY = "javax.net.ssl.keyStorePassword"; + private static final String KEY_STORE_TYPE_PROPERTY = "javax.net.ssl.keyStoreType"; + + private String previousKeyStore; + private String previousKeyStorePassword; + private String previousKeyStoreType; + private String previousSharedThreadPoolProperty; + + private Path serverKeystore; + private Path clientKeystore; + private TeardownableServer server; + private int serverPort = -1; + private HTTP2JettyClient client; + private HTTP2Sampler sampler; + + @Before + public void setUp() throws Exception { + JMeterTestUtils.setupJmeterEnv(); + previousKeyStore = System.getProperty(KEY_STORE_PROPERTY); + previousKeyStorePassword = System.getProperty(KEY_STORE_PASSWORD_PROPERTY); + previousKeyStoreType = System.getProperty(KEY_STORE_TYPE_PROPERTY); + previousSharedThreadPoolProperty = JMeterUtils.getProperty("httpJettyClient.sharedThreadPool"); + JMeterUtils.setProperty("httpJettyClient.sharedThreadPool", "false"); + + serverKeystore = TestSslKeyStores.createJks("server", "CN=localhost, O=SslSim, C=US"); + clientKeystore = TestSslKeyStores.createJks("client", "CN=client, O=SslSim, C=US"); + + configureClientKeyStoreLikeBlazeMeter(clientKeystore); + startUntrustedHttpsServer(); + + sampler = new HTTP2Sampler(); + sampler.setMethod(HTTPConstants.GET); + sampler.setDomain(ServerBuilder.HOST_NAME); + sampler.setProtocol(HTTPConstants.PROTOCOL_HTTPS); + sampler.setPort(serverPort); + sampler.setPath(""); + } + + @After + public void tearDown() throws Exception { + if (client != null) { + client.stop(); + client = null; + } + if (sampler != null) { + sampler.threadFinished(); + sampler = null; + } + if (server != null) { + server.stop(); + server = null; + } + restoreProperty(KEY_STORE_PROPERTY, previousKeyStore); + restoreProperty(KEY_STORE_PASSWORD_PROPERTY, previousKeyStorePassword); + restoreProperty(KEY_STORE_TYPE_PROPERTY, previousKeyStoreType); + SSLManager.reset(); + if (previousSharedThreadPoolProperty == null) { + JMeterUtils.getJMeterProperties().remove("httpJettyClient.sharedThreadPool"); + } else { + JMeterUtils.setProperty("httpJettyClient.sharedThreadPool", previousSharedThreadPoolProperty); + } + deleteIfExists(serverKeystore); + deleteIfExists(clientKeystore); + } + + @Test + public void shouldReproducePkixWhenTrustAllIsSetButTrustManagersAreNotOverridden() { + TrustAllWithoutTrustManagerOverride factory = new TrustAllWithoutTrustManagerOverride(); + + assertThatThrownBy(() -> performTlsHandshake(factory)) + .isInstanceOf(SSLHandshakeException.class) + .hasMessageContaining("PKIX path building failed"); + } + + @Test + public void shouldHandshakeWhenJMeterJettySslContextFactoryIsUsed() { + assertThatCode(() -> performTlsHandshake(new JMeterJettySslContextFactory())) + .doesNotThrowAnyException(); + } + + @Test + public void shouldCompleteHttp2RequestWhenClientKeyStoreConfiguredAgainstUntrustedServer() + throws Exception { + client = new HTTP2JettyClient(); + client.start(); + + HTTPSampleResult result = client.sample( + sampler, + buildBaseResult(createUrl(ServerBuilder.SERVER_PATH_200), HTTPConstants.GET), + false, + 0); + + assertThat(result.isSuccessful()).isTrue(); + assertThat(result.getResponseDataAsString()).isEqualTo(ServerBuilder.SERVER_RESPONSE); + } + + private void startUntrustedHttpsServer() throws Exception { + server = new ServerBuilder() + .withHTTP1() + .withHTTP2() + .withALPN() + .withHTTP2C() + .withSSL() + .withServerKeyStorePath(serverKeystore.toString(), "JKS") + .buildServer(); + server.start(); + } + + private void configureClientKeyStoreLikeBlazeMeter(Path keyStorePath) { + SSLManager.reset(); + System.setProperty(KEY_STORE_PROPERTY, keyStorePath.toString()); + System.setProperty(KEY_STORE_PASSWORD_PROPERTY, TestSslKeyStores.PASSWORD); + System.setProperty(KEY_STORE_TYPE_PROPERTY, "JKS"); + } + + private void performTlsHandshake(SslContextFactory.Client factory) throws Exception { + if (serverPort < 0) { + ServerConnector connector = (ServerConnector) server.getConnectors()[0]; + serverPort = connector.getLocalPort(); + } + factory.start(); + try { + try (SSLSocket socket = (SSLSocket) factory.getSslContext().getSocketFactory() + .createSocket(ServerBuilder.HOST_NAME, serverPort)) { + socket.setSoTimeout(10_000); + socket.startHandshake(); + } + } finally { + factory.stop(); + } + } + + private URL createUrl(String path) throws Exception { + if (serverPort < 0) { + ServerConnector connector = (ServerConnector) server.getConnectors()[0]; + serverPort = connector.getLocalPort(); + } + sampler.setPort(serverPort); + return new URI(HTTPConstants.PROTOCOL_HTTPS, null, ServerBuilder.HOST_NAME, serverPort, path, + null, null).toURL(); + } + + private static HTTPSampleResult buildBaseResult(URL url, String method) { + HTTPSampleResult result = new HTTPSampleResult(); + result.setURL(url); + result.setHTTPMethod(method); + return result; + } + + private static void restoreProperty(String key, String value) { + if (value == null) { + System.clearProperty(key); + } else { + System.setProperty(key, value); + } + } + + private static void deleteIfExists(Path path) throws IOException { + if (path != null) { + Files.deleteIfExists(path); + } + } + + /** + * Mirrors {@code setTrustAll(true)} plus client keyStore configuration, but without the + * {@link JMeterJettySslContextFactory#getTrustManagers} override (pre-fix Jetty behaviour). + */ + private static final class TrustAllWithoutTrustManagerOverride extends SslContextFactory.Client { + + private TrustAllWithoutTrustManagerOverride() { + setTrustAll(true); + String keyStorePath = System.getProperty(KEY_STORE_PROPERTY); + if (keyStorePath != null && !keyStorePath.isEmpty() + && SslStorePathResolver.isFileBasedStoreLocation(keyStorePath)) { + setKeyStorePath(SslStorePathResolver.toJettyFileUri(keyStorePath)); + setKeyStoreType(SslStorePathResolver.resolveKeyStoreType(keyStorePath)); + setKeyStorePassword(System.getProperty(KEY_STORE_PASSWORD_PROPERTY)); + } + } + } + +} diff --git a/src/test/java/com/blazemeter/jmeter/http2/core/SslStorePathResolverTest.java b/src/test/java/com/blazemeter/jmeter/http2/core/SslStorePathResolverTest.java new file mode 100644 index 0000000..f9c27b1 --- /dev/null +++ b/src/test/java/com/blazemeter/jmeter/http2/core/SslStorePathResolverTest.java @@ -0,0 +1,124 @@ +package com.blazemeter.jmeter.http2.core; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assume.assumeTrue; + +import java.io.File; +import java.net.URI; +import java.nio.file.Paths; +import org.junit.Test; + +public class SslStorePathResolverTest { + + private static final String RELATIVE_STORE = "certs/keystore.p12"; + + @Test + public void naiveFilePrefixOnRelativePathTreatsFirstSegmentAsUriAuthority() { + URI malformed = URI.create("file://" + RELATIVE_STORE); + assertThat(malformed.getAuthority()).isEqualTo("certs"); + assertThat(malformed.getPath()).isEqualTo("/keystore.p12"); + } + + @Test + public void naiveFilePrefixOnRelativePathRejectsAuthorityOnUnix() { + assumeTrue(!isWindows()); + assertThatThrownBy(() -> Paths.get(URI.create("file://" + RELATIVE_STORE))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("authority"); + } + + @Test + public void toJettyFileUriMatchesFileToUriForRelativePath() { + assertThat(SslStorePathResolver.toJettyFileUri(RELATIVE_STORE)) + .isEqualTo(new File(RELATIVE_STORE).getAbsoluteFile().toURI().toString()); + } + + @Test + public void toJettyFileUriMatchesFileToUriForAbsolutePath() { + String absolute = new File(System.getProperty("user.dir"), RELATIVE_STORE) + .getAbsolutePath(); + assertThat(SslStorePathResolver.toJettyFileUri(absolute)) + .isEqualTo(new File(absolute).getAbsoluteFile().toURI().toString()); + } + + @Test + public void toJettyFileUriHasNoUriAuthorityForRelativePath() { + URI jettyUri = URI.create(SslStorePathResolver.toJettyFileUri(RELATIVE_STORE)); + assertThat(jettyUri.getAuthority()).isNull(); + assertThat(Paths.get(jettyUri)) + .isEqualTo(new File(RELATIVE_STORE).getAbsoluteFile().toPath()); + } + + @Test + public void noneIsNotAFileBasedStoreLocation() { + assertThat(SslStorePathResolver.isFileBasedStoreLocation("NONE")).isFalse(); + assertThat(SslStorePathResolver.isFileBasedStoreLocation("none")).isFalse(); + assertThat(SslStorePathResolver.isFileBasedStoreLocation(RELATIVE_STORE)).isTrue(); + } + + @Test + public void resolveKeyStoreTypeUsesPropertyWhenSet() { + String previous = System.getProperty("javax.net.ssl.keyStoreType"); + try { + System.setProperty("javax.net.ssl.keyStoreType", "PKCS12"); + assertThat(SslStorePathResolver.resolveKeyStoreType("certs/keystore.jks")) + .isEqualTo("PKCS12"); + } finally { + restoreProperty("javax.net.ssl.keyStoreType", previous); + } + } + + @Test + public void resolveKeyStoreTypeInfersPkcs12FromP12ExtensionLikeJMeter() { + String previous = System.getProperty("javax.net.ssl.keyStoreType"); + try { + System.clearProperty("javax.net.ssl.keyStoreType"); + assertThat(SslStorePathResolver.resolveKeyStoreType(RELATIVE_STORE)).isEqualTo("pkcs12"); + assertThat(SslStorePathResolver.resolveKeyStoreType("certs/keystore.jks")).isEqualTo("JKS"); + assertThat(SslStorePathResolver.resolveKeyStoreType("certs/keystore.pfx")).isEqualTo("JKS"); + } finally { + restoreProperty("javax.net.ssl.keyStoreType", previous); + } + } + + @Test + public void resolveTrustStoreTypeUsesPropertyWhenSet() { + String previous = System.getProperty("javax.net.ssl.trustStoreType"); + try { + System.setProperty("javax.net.ssl.trustStoreType", "JKS"); + assertThat(SslStorePathResolver.resolveTrustStoreType("certs/trust.p12")) + .isEqualTo("JKS"); + } finally { + restoreProperty("javax.net.ssl.trustStoreType", previous); + } + } + + @Test + public void resolveTrustStoreTypeInfersPkcs12FromP12OrPfxWhenUnset() { + String previous = System.getProperty("javax.net.ssl.trustStoreType"); + try { + System.clearProperty("javax.net.ssl.trustStoreType"); + assertThat(SslStorePathResolver.resolveTrustStoreType("certs/trust.p12")) + .isEqualTo("pkcs12"); + assertThat(SslStorePathResolver.resolveTrustStoreType("certs/trust.pfx")) + .isEqualTo("pkcs12"); + assertThat(SslStorePathResolver.resolveTrustStoreType("certs/trust.jks")).isEqualTo("JKS"); + } finally { + restoreProperty("javax.net.ssl.trustStoreType", previous); + } + } + + private static void restoreProperty(String key, String value) { + if (value == null) { + System.clearProperty(key); + } else { + System.setProperty(key, value); + } + } + + private static boolean isWindows() { + return File.separatorChar == '\\'; + } + +} diff --git a/src/test/java/com/blazemeter/jmeter/http2/core/TestSslKeyStores.java b/src/test/java/com/blazemeter/jmeter/http2/core/TestSslKeyStores.java new file mode 100644 index 0000000..1bd1cf2 --- /dev/null +++ b/src/test/java/com/blazemeter/jmeter/http2/core/TestSslKeyStores.java @@ -0,0 +1,61 @@ +package com.blazemeter.jmeter.http2.core; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * Generates ephemeral JKS keystores for SSL handshake simulations. + */ +final class TestSslKeyStores { + + static final String PASSWORD = ServerBuilder.KEYSTORE_PASSWORD; + private static final String KEYTOOL = resolveKeytoolExecutable(); + + private TestSslKeyStores() { + } + + static Path createJks(String alias, String distinguishedName) throws IOException, InterruptedException { + Path keystore = Files.createTempDirectory("ssl-sim-").resolve(alias + ".jks"); + runKeytool(List.of( + KEYTOOL, + "-genkeypair", + "-alias", alias, + "-keyalg", "RSA", + "-keysize", "2048", + "-validity", "365", + "-keystore", keystore.toAbsolutePath().toString(), + "-storetype", "JKS", + "-storepass", PASSWORD, + "-keypass", PASSWORD, + "-dname", distinguishedName + )); + return keystore; + } + + private static void runKeytool(List command) throws IOException, InterruptedException { + Process process = new ProcessBuilder(command) + .redirectErrorStream(true) + .start(); + String output = new String(process.getInputStream().readAllBytes()); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("keytool failed (exit " + exitCode + "): " + output); + } + } + + private static String resolveKeytoolExecutable() { + String javaHome = System.getProperty("java.home"); + Path candidate = Path.of(javaHome, "bin", isWindows() ? "keytool.exe" : "keytool"); + if (Files.isRegularFile(candidate)) { + return candidate.toString(); + } + return isWindows() ? "keytool.exe" : "keytool"; + } + + private static boolean isWindows() { + return System.getProperty("os.name", "").toLowerCase().contains("win"); + } + +}