From b98e5a42d423b565c97891dd6c50796853b5b4f4 Mon Sep 17 00:00:00 2001 From: Jason Eric Klaes Hoetger Date: Sun, 27 Dec 2015 16:17:26 -0800 Subject: [PATCH] Added MITM module for LittleProxy MITM. Integrated MITM module with BMP. Replaced Cybervillains certs with new certificates. --- README.md | 6 +- browsermob-core-littleproxy/pom.xml | 16 + .../lightbody/bmp/BrowserMobProxyServer.java | 53 ++- .../filters/ModifiedRequestAwareFilter.java | 5 +- .../bmp/ssl/BrowserMobProxyMitmManager.java | 26 -- .../bmp/ssl/BrowserMobSslEngineSource.java | 102 ----- .../bmp/filters/RewriteUrlFilterTest.groovy | 1 + .../lightbody/bmp/proxy/BlacklistTest.groovy | 2 + .../net/lightbody/bmp/proxy/NewHarTest.groovy | 6 +- .../lightbody/bmp/proxy/WhitelistTest.groovy | 3 + .../lightbody/bmp/proxy/InterceptorTest.java | 4 + .../src/test/resources/log4j2-test.json | 23 + browsermob-core/pom.xml | 23 +- .../net/lightbody/bmp/BrowserMobProxy.java | 21 + .../bmp/proxy/BrowserMobProxyHandler.java | 4 +- .../net/lightbody/bmp/proxy/ProxyServer.java | 11 + .../bmp/proxy/jetty/http/SslListener.java | 4 +- .../bmp/proxy/selenium/KeyStoreManager.java | 55 +-- .../proxy/selenium/SeleniumProxyHandler.java | 28 +- .../main/resources/sslSupport/blank_crl.dec | Bin 444 -> 0 bytes .../main/resources/sslSupport/blank_crl.pem | 12 - .../sslSupport/ca-certificate-ec.cer | 13 + .../sslSupport/ca-certificate-rsa.cer | 21 + .../resources/sslSupport/ca-keystore-ec.p12 | Bin 0 -> 1019 bytes .../resources/sslSupport/ca-keystore-rsa.p12 | Bin 0 -> 2582 bytes .../resources/sslSupport/cybervillainsCA.cer | Bin 665 -> 0 bytes .../resources/sslSupport/cybervillainsCA.jks | Bin 2148 -> 0 bytes .../net/lightbody/bmp/proxy/BrowserTest.java | 3 +- .../lightbody/bmp/proxy/PhantomJSTest.java | 1 + .../bmp/proxy/test/util/ProxyServerTest.java | 8 +- mitm/README.md | 135 ++++++ mitm/pom.xml | 106 +++++ .../lightbody/bmp/mitm/CertificateAndKey.java | 25 ++ .../bmp/mitm/CertificateAndKeySource.java | 16 + .../lightbody/bmp/mitm/CertificateInfo.java | 114 +++++ .../bmp/mitm/CertificateInfoGenerator.java | 19 + .../bmp/mitm/ExistingCertificateSource.java | 31 ++ .../HostnameCertificateInfoGenerator.java | 54 +++ .../bmp/mitm/KeyStoreCertificateSource.java | 77 ++++ .../mitm/KeyStoreFileCertificateSource.java | 146 +++++++ .../bmp/mitm/PemFileCertificateSource.java | 83 ++++ .../bmp/mitm/RootCertificateGenerator.java | 259 +++++++++++ .../CertificateCreationException.java | 23 + .../exception/CertificateSourceException.java | 24 + .../bmp/mitm/exception/ExportException.java | 23 + .../bmp/mitm/exception/ImportException.java | 23 + .../mitm/exception/KeyGeneratorException.java | 23 + .../exception/KeyStoreAccessException.java | 23 + .../bmp/mitm/exception/MitmException.java | 24 + .../SslContextInitializationException.java | 23 + .../bmp/mitm/keys/ECKeyGenerator.java | 55 +++ .../lightbody/bmp/mitm/keys/KeyGenerator.java | 15 + .../bmp/mitm/keys/RSAKeyGenerator.java | 56 +++ .../manager/ImpersonatingMitmManager.java | 412 ++++++++++++++++++ .../CertificateGenerationStatistics.java | 57 +++ .../BouncyCastleSecurityProviderTool.java | 384 ++++++++++++++++ .../tools/DefaultSecurityProviderTool.java | 166 +++++++ .../bmp/mitm/tools/SecurityProviderTool.java | 142 ++++++ .../bmp/mitm/util/EncryptionUtil.java | 110 +++++ .../lightbody/bmp/mitm/util/KeyStoreUtil.java | 103 +++++ .../bmp/mitm/util/MitmConstants.java | 33 ++ .../net/lightbody/bmp/mitm/util/SslUtil.java | 98 +++++ .../mitm/ExistingCertificateSourceTest.groovy | 36 ++ .../mitm/ImpersonatingMitmManagerTest.groovy | 48 ++ .../mitm/KeyStoreCertificateSourceTest.groovy | 33 ++ .../KeyStoreFileCertificateSourceTest.groovy | 64 +++ .../mitm/PemFileCertificateSourceTest.groovy | 83 ++++ .../mitm/RootCertificateGeneratorTest.groovy | 88 ++++ .../bmp/mitm/tools/ECKeyGeneratorTest.groovy | 27 ++ .../bmp/mitm/tools/RSAKeyGeneratorTest.groovy | 27 ++ .../mitm/ImpersonationPerformanceTests.java | 158 +++++++ .../mitm/example/CustomCAKeyStoreExample.java | 39 ++ .../mitm/example/CustomCAPemFileExample.java | 38 ++ .../EllipticCurveCAandServerExample.java | 47 ++ .../LittleProxyDefaultConfigExample.java | 30 ++ .../mitm/example/SaveGeneratedCAExample.java | 37 ++ .../LittleProxyIntegrationTest.java | 129 ++++++ .../mitm/test/util/CertificateTestUtil.java | 44 ++ mitm/src/test/resources/log4j2-test.json | 23 + .../net/lightbody/bmp/mitm/certificate.crt | 20 + .../bmp/mitm/encrypted-private-key.key | 30 ++ .../net/lightbody/bmp/mitm/keystore.jks | Bin 0 -> 2191 bytes .../net/lightbody/bmp/mitm/keystore.p12 | Bin 0 -> 2514 bytes .../bmp/mitm/unencrypted-private-key.key | 27 ++ pom.xml | 17 +- 85 files changed, 4164 insertions(+), 214 deletions(-) delete mode 100644 browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/ssl/BrowserMobProxyMitmManager.java delete mode 100644 browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/ssl/BrowserMobSslEngineSource.java create mode 100644 browsermob-core-littleproxy/src/test/resources/log4j2-test.json delete mode 100644 browsermob-core/src/main/resources/sslSupport/blank_crl.dec delete mode 100644 browsermob-core/src/main/resources/sslSupport/blank_crl.pem create mode 100644 browsermob-core/src/main/resources/sslSupport/ca-certificate-ec.cer create mode 100644 browsermob-core/src/main/resources/sslSupport/ca-certificate-rsa.cer create mode 100644 browsermob-core/src/main/resources/sslSupport/ca-keystore-ec.p12 create mode 100644 browsermob-core/src/main/resources/sslSupport/ca-keystore-rsa.p12 delete mode 100644 browsermob-core/src/main/resources/sslSupport/cybervillainsCA.cer delete mode 100644 browsermob-core/src/main/resources/sslSupport/cybervillainsCA.jks create mode 100644 mitm/README.md create mode 100644 mitm/pom.xml create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/CertificateAndKey.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/CertificateAndKeySource.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/CertificateInfo.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/CertificateInfoGenerator.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/ExistingCertificateSource.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/HostnameCertificateInfoGenerator.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/KeyStoreCertificateSource.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/KeyStoreFileCertificateSource.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/PemFileCertificateSource.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/RootCertificateGenerator.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/exception/CertificateCreationException.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/exception/CertificateSourceException.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/exception/ExportException.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/exception/ImportException.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/exception/KeyGeneratorException.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/exception/KeyStoreAccessException.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/exception/MitmException.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/exception/SslContextInitializationException.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/keys/ECKeyGenerator.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/keys/KeyGenerator.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/keys/RSAKeyGenerator.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/manager/ImpersonatingMitmManager.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/stats/CertificateGenerationStatistics.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/tools/BouncyCastleSecurityProviderTool.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/tools/DefaultSecurityProviderTool.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/tools/SecurityProviderTool.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/util/EncryptionUtil.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/util/KeyStoreUtil.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/util/MitmConstants.java create mode 100644 mitm/src/main/java/net/lightbody/bmp/mitm/util/SslUtil.java create mode 100644 mitm/src/test/groovy/net/lightbody/bmp/mitm/ExistingCertificateSourceTest.groovy create mode 100644 mitm/src/test/groovy/net/lightbody/bmp/mitm/ImpersonatingMitmManagerTest.groovy create mode 100644 mitm/src/test/groovy/net/lightbody/bmp/mitm/KeyStoreCertificateSourceTest.groovy create mode 100644 mitm/src/test/groovy/net/lightbody/bmp/mitm/KeyStoreFileCertificateSourceTest.groovy create mode 100644 mitm/src/test/groovy/net/lightbody/bmp/mitm/PemFileCertificateSourceTest.groovy create mode 100644 mitm/src/test/groovy/net/lightbody/bmp/mitm/RootCertificateGeneratorTest.groovy create mode 100644 mitm/src/test/groovy/net/lightbody/bmp/mitm/tools/ECKeyGeneratorTest.groovy create mode 100644 mitm/src/test/groovy/net/lightbody/bmp/mitm/tools/RSAKeyGeneratorTest.groovy create mode 100644 mitm/src/test/java/net/lightbody/bmp/mitm/ImpersonationPerformanceTests.java create mode 100644 mitm/src/test/java/net/lightbody/bmp/mitm/example/CustomCAKeyStoreExample.java create mode 100644 mitm/src/test/java/net/lightbody/bmp/mitm/example/CustomCAPemFileExample.java create mode 100644 mitm/src/test/java/net/lightbody/bmp/mitm/example/EllipticCurveCAandServerExample.java create mode 100644 mitm/src/test/java/net/lightbody/bmp/mitm/example/LittleProxyDefaultConfigExample.java create mode 100644 mitm/src/test/java/net/lightbody/bmp/mitm/example/SaveGeneratedCAExample.java create mode 100644 mitm/src/test/java/net/lightbody/bmp/mitm/integration/LittleProxyIntegrationTest.java create mode 100644 mitm/src/test/java/net/lightbody/bmp/mitm/test/util/CertificateTestUtil.java create mode 100644 mitm/src/test/resources/log4j2-test.json create mode 100644 mitm/src/test/resources/net/lightbody/bmp/mitm/certificate.crt create mode 100644 mitm/src/test/resources/net/lightbody/bmp/mitm/encrypted-private-key.key create mode 100644 mitm/src/test/resources/net/lightbody/bmp/mitm/keystore.jks create mode 100644 mitm/src/test/resources/net/lightbody/bmp/mitm/keystore.p12 create mode 100644 mitm/src/test/resources/net/lightbody/bmp/mitm/unencrypted-private-key.key diff --git a/README.md b/README.md index ce3b11ece..b15f2c6c0 100644 --- a/README.md +++ b/README.md @@ -367,9 +367,11 @@ Consult the Java API docs for more info. ### SSL Support -**LittleProxy support for MITM:** In the current beta release, the `browsermob-core-littleproxy` module supports MITM but does not support dynamic certificate spoofing. In most cases this will not affect your tests, but browsers accessing HTTPS websites through BrowserMob Proxy will be notified that the certificates cannot be verified. +**BrowserMob with LittleProxy now supports full MITM:** For most users, MITM will work out-of-the-box with default settings. Install the [ca-certificate-rsa.cer](/sslSupport/ca-certificate-rsa.cer) file in your browser or HTTP client to avoid untrusted certificate warnings. Generally, it is safer to generate your own private key, rather than using the .cer files distributed with BrowserMob Proxy. See the [README file in the `mitm` module](/mitm/README.md) for instructions on generating or using your own root certificate and private key with MITM. -**Legacy Jetty-based support for MITM:** The legacy `ProxyServer` implementation using the `browsermob-core` module supports MITM and dynamic certificate spoofing. To avoid browser certificate warnings, a Certificate Authority must be installed in the browser. This allows the browser to trust all the SSL traffic coming from the proxy, which will be proxied using a classic man-in-the1-middle technique. IT IS CRITICAL THAT YOU NOT INSTALL THIS CERTIFICATE AUTHORITY ON A BROWSER THAT IS USED FOR ANYTHING OTHER THAN TESTING. +**Legacy Jetty-based ProxyServer support for MITM:** As of version 2.1.0-beta-4, the legacy `ProxyServer` implementation uses the same `ca-certificate-rsa.cer` root certificate as the LittleProxy implementation. The previous cybervillainsCA.cer certificate has been removed. + +**Note: DO NOT** permanently install the .cer files distributed with BrowserMob Proxy in users' browsers. They should be used for testing only and must not be used with general web browsing. If you're doing testing with Selenium, you'll want to make sure that the browser profile that gets set up by Selenium not only has the proxy configured, but also has the CA installed. Unfortuantely, there is no API for doing this in Selenium, so you'll have to solve it uniquely for each browser type. We hope to make this easier in upcoming releases. diff --git a/browsermob-core-littleproxy/pom.xml b/browsermob-core-littleproxy/pom.xml index 7c5756c20..2cf1641ba 100644 --- a/browsermob-core-littleproxy/pom.xml +++ b/browsermob-core-littleproxy/pom.xml @@ -266,6 +266,22 @@ netty-all + + org.bouncycastle + bcprov-jdk15on + + + + org.bouncycastle + bcpkix-jdk15on + + + + net.lightbody.bmp + mitm + ${project.version} + + diff --git a/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/BrowserMobProxyServer.java b/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/BrowserMobProxyServer.java index 94de60f66..77e9c19ff 100644 --- a/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/BrowserMobProxyServer.java +++ b/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/BrowserMobProxyServer.java @@ -29,6 +29,9 @@ import net.lightbody.bmp.filters.RewriteUrlFilter; import net.lightbody.bmp.filters.UnregisterRequestFilter; import net.lightbody.bmp.filters.WhitelistFilter; +import net.lightbody.bmp.mitm.KeyStoreFileCertificateSource; +import net.lightbody.bmp.mitm.keys.RSAKeyGenerator; +import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager; import net.lightbody.bmp.proxy.ActivityMonitor; import net.lightbody.bmp.proxy.BlacklistEntry; import net.lightbody.bmp.proxy.CaptureType; @@ -41,7 +44,6 @@ import net.lightbody.bmp.proxy.http.RequestInterceptor; import net.lightbody.bmp.proxy.http.ResponseInterceptor; import net.lightbody.bmp.proxy.util.BrowserMobProxyUtil; -import net.lightbody.bmp.ssl.BrowserMobProxyMitmManager; import org.apache.http.HttpRequestInterceptor; import org.apache.http.HttpResponseInterceptor; import org.java_bandwidthlimiter.StreamManager; @@ -53,6 +55,7 @@ import org.littleshoot.proxy.HttpFiltersSourceAdapter; import org.littleshoot.proxy.HttpProxyServer; import org.littleshoot.proxy.HttpProxyServerBootstrap; +import org.littleshoot.proxy.MitmManager; import org.littleshoot.proxy.impl.DefaultHttpProxyServer; import org.littleshoot.proxy.impl.ProxyUtils; import org.littleshoot.proxy.impl.ThreadPoolConfiguration; @@ -88,7 +91,13 @@ public class BrowserMobProxyServer implements BrowserMobProxy, LegacyProxyServer private static final Logger log = LoggerFactory.getLogger(BrowserMobProxyServer.class); //TODO: extract the version string into a more suitable location - private static final HarNameVersion HAR_CREATOR_VERSION = new HarNameVersion("BrowserMob Proxy", "2.1.0-beta-3-littleproxy"); + private static final HarNameVersion HAR_CREATOR_VERSION = new HarNameVersion("BrowserMob Proxy", "2.1.0-beta-4-littleproxy"); + + /* Default MITM resources */ + private static final String KEYSTORE_RESOURCE = "/sslSupport/ca-keystore-rsa.p12"; + private static final String KEYSTORE_TYPE = "PKCS12"; + private static final String KEYSTORE_PRIVATE_KEY_ALIAS = "key"; + private static final String KEYSTORE_PASSWORD = "password"; /** * The default pseudonym to use when adding the Via header to proxied requests. @@ -115,6 +124,11 @@ public class BrowserMobProxyServer implements BrowserMobProxy, LegacyProxyServer */ private volatile boolean mitmDisabled = false; + /** + * The MITM manager that will be used for HTTPS requests. + */ + private volatile MitmManager mitmManager; + /** * The list of filterFactories that will generate the filters that implement browsermob-proxy behavior. */ @@ -226,6 +240,11 @@ public class BrowserMobProxyServer implements BrowserMobProxy, LegacyProxyServer */ private volatile boolean errorOnUnsupportedOperation = false; + /** + * When true, will not validate upstream servers' certificates. Currently only applicable when MITMing. + */ + private volatile boolean trustAllServers = true; + /** * Resolver to use when resolving hostnames to IP addresses. This is a bridge between {@link org.littleshoot.proxy.HostResolver} and * {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver}. It allows the resolvers to be changed on-the-fly without re-bootstrapping the @@ -345,7 +364,15 @@ public int getMaximumResponseBufferSizeInBytes() { if (!mitmDisabled) { - bootstrap.withManInTheMiddle(new BrowserMobProxyMitmManager()); + if (mitmManager == null) { + mitmManager = ImpersonatingMitmManager.builder() + .rootCertificateSource(new KeyStoreFileCertificateSource(KEYSTORE_TYPE, KEYSTORE_RESOURCE, KEYSTORE_PRIVATE_KEY_ALIAS, KEYSTORE_PASSWORD)) + .serverKeyGenerator(new RSAKeyGenerator()) + .trustAllServers(trustAllServers) + .build(); + } + + bootstrap.withManInTheMiddle(mitmManager); } if (readBandwidthLimitBps > 0 || writeBandwidthLimitBps > 0) { @@ -868,7 +895,7 @@ public void setIdleConnectionTimeout(int idleConnectionTimeout, TimeUnit timeUni public void setRequestTimeout(int requestTimeout, TimeUnit timeUnit) { //TODO: implement Request Timeouts using LittleProxy. currently this only sets an idle connection timeout, if the idle connection // timeout is higher than the specified requestTimeout. - if (idleConnectionTimeoutSec == 0 || idleConnectionTimeoutSec > TimeUnit.SECONDS.convert(requestTimeout, timeUnit)) { + if (idleConnectionTimeoutSec == 0 || idleConnectionTimeoutSec > TimeUnit.SECONDS.convert(requestTimeout, timeUnit)) { setIdleConnectionTimeout(requestTimeout, timeUnit); } } @@ -1148,7 +1175,7 @@ public boolean waitForQuiescence(long quietPeriod, long timeout, TimeUnit timeUn /** * Instructs this proxy to route traffic through an upstream proxy. Proxy chaining is not compatible with man-in-the-middle * SSL, so HAR capture will be disabled for HTTPS traffic when using an upstream proxy. - *

+ *

* Note: Using {@link #setChainedProxyManager(ChainedProxyManager)} will supersede any value set by this method. * * @param chainedProxyAddress address of the upstream proxy @@ -1181,7 +1208,6 @@ public void setChainedProxyManager(ChainedProxyManager chainedProxyManager) { * Configures the Netty thread pool used by the LittleProxy back-end. See {@link ThreadPoolConfiguration} for details. * * @param threadPoolConfiguration thread pool configuration to use - * */ public void setThreadPoolConfiguration(ThreadPoolConfiguration threadPoolConfiguration) { if (isStarted()) { @@ -1362,6 +1388,20 @@ public void setMitmDisabled(boolean mitmDisabled) throws IllegalStateException { this.mitmDisabled = mitmDisabled; } + @Override + public void setMitmManager(MitmManager mitmManager) { + this.mitmManager = mitmManager; + } + + @Override + public void setTrustAllServers(boolean trustAllServers) { + if (isStarted()) { + throw new IllegalStateException("Cannot disable upstream server verification after the proxy has been started"); + } + + this.trustAllServers = trustAllServers; + } + public boolean isMitmDisabled() { return this.mitmDisabled; } @@ -1501,4 +1541,5 @@ public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerCont }); } } + } diff --git a/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/filters/ModifiedRequestAwareFilter.java b/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/filters/ModifiedRequestAwareFilter.java index 1f21ac8a7..c117c0b56 100644 --- a/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/filters/ModifiedRequestAwareFilter.java +++ b/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/filters/ModifiedRequestAwareFilter.java @@ -1,12 +1,11 @@ package net.lightbody.bmp.filters; -import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; /** * Indicates that a filter wishes to capture the final HttpRequest that is sent to the server, reflecting all - * modifications from request filters. {@link BrowserMobHttpFilterChain#clientToProxyRequest(HttpObject)} will invoke - * the {@link #setModifiedHttpRequest(HttpRequest)} method after all filters have processed the initial + * modifications from request filters. {@link BrowserMobHttpFilterChain#clientToProxyRequest(io.netty.handler.codec.http.HttpObject)} + * will invoke the {@link #setModifiedHttpRequest(HttpRequest)} method after all filters have processed the initial * {@link HttpRequest} object. */ public interface ModifiedRequestAwareFilter { diff --git a/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/ssl/BrowserMobProxyMitmManager.java b/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/ssl/BrowserMobProxyMitmManager.java deleted file mode 100644 index 99b31bf71..000000000 --- a/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/ssl/BrowserMobProxyMitmManager.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.lightbody.bmp.ssl; - -import org.littleshoot.proxy.MitmManager; - -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLSession; - -/** - * This implementation mirrors the implementation of {@link org.littleshoot.proxy.extras.SelfSignedMitmManager}, but uses the - * cybervillainsCA.jks keystore that the Jetty implementaion uses. - */ -public class BrowserMobProxyMitmManager implements MitmManager { - private final BrowserMobSslEngineSource bmpSslEngineSource = - new BrowserMobSslEngineSource(); - - @Override - public SSLEngine serverSslEngine(String host, int port) { - return bmpSslEngineSource.newSslEngine(host, port); - } - - @Override - public SSLEngine clientSslEngineFor(SSLSession serverSslSession) { - return bmpSslEngineSource.newSslEngine(); - } -} - diff --git a/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/ssl/BrowserMobSslEngineSource.java b/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/ssl/BrowserMobSslEngineSource.java deleted file mode 100644 index 79a27c8b1..000000000 --- a/browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/ssl/BrowserMobSslEngineSource.java +++ /dev/null @@ -1,102 +0,0 @@ -package net.lightbody.bmp.ssl; - -import org.littleshoot.proxy.SslEngineSource; - -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; -import java.io.InputStream; -import java.security.KeyStore; -import java.security.Security; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -/** - * This implementation mirrors the implementation of {@link org.littleshoot.proxy.extras.SelfSignedSslEngineSource}, but uses the - * cybervillainsCA.jks keystore that the Jetty implementaion uses. - */ -public class BrowserMobSslEngineSource implements SslEngineSource { - private static final String KEYSTORE_RESOURCE = "/sslSupport/cybervillainsCA.jks"; - - private static final char[] KEYSTORE_PASSWORD = "password".toCharArray(); - - private final SSLContext sslContext; - - public BrowserMobSslEngineSource() { - this.sslContext = initializeSSLContext(); - } - - @Override - public SSLEngine newSslEngine() { - return sslContext.createSSLEngine(); - } - - @Override - public SSLEngine newSslEngine(String host, int port) { - return sslContext.createSSLEngine(host, port); - } - - private SSLContext initializeSSLContext() { - String algorithm = Security - .getProperty("ssl.KeyManagerFactory.algorithm"); - if (algorithm == null) { - algorithm = "SunX509"; - } - - InputStream keystoreStream = getClass().getResourceAsStream(KEYSTORE_RESOURCE); - if (keystoreStream == null) { - throw new RuntimeException("Unable to load keystore from classpath resource: " + KEYSTORE_RESOURCE); - } - - try { - final KeyStore ks = KeyStore.getInstance("JKS"); - // ks.load(new FileInputStream("keystore.jks"), - // "changeit".toCharArray()); - ks.load(keystoreStream, KEYSTORE_PASSWORD); - - // Set up key manager factory to use our key store - final KeyManagerFactory kmf = - KeyManagerFactory.getInstance(algorithm); - kmf.init(ks, KEYSTORE_PASSWORD); - - // Set up a trust manager factory to use our key store - TrustManagerFactory tmf = TrustManagerFactory - .getInstance(algorithm); - tmf.init(ks); - - TrustManager[] trustManagers = new TrustManager[]{new X509TrustManager() { - // TrustManager that trusts all servers - @Override - public void checkClientTrusted(X509Certificate[] arg0, - String arg1) - throws CertificateException { - } - - @Override - public void checkServerTrusted(X509Certificate[] arg0, - String arg1) - throws CertificateException { - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return null; - } - }}; - - KeyManager[] keyManagers = kmf.getKeyManagers(); - - // Initialize the SSLContext to work with our key managers. - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(keyManagers, trustManagers, null); - - return sslContext; - } catch (Exception e) { - throw new RuntimeException("Failed to initialize the server-side SSLContext", e); - } - } -} diff --git a/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/filters/RewriteUrlFilterTest.groovy b/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/filters/RewriteUrlFilterTest.groovy index f9b4cead0..9d6350540 100644 --- a/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/filters/RewriteUrlFilterTest.groovy +++ b/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/filters/RewriteUrlFilterTest.groovy @@ -156,6 +156,7 @@ class RewriteUrlFilterTest extends MockServerTest { .withBody("success")) proxy = new BrowserMobProxyServer() + proxy.setTrustAllServers(true) proxy.rewriteUrl('https://localhost:(\\d+)/badresource', 'https://localhost:$1/rewrittenresource') proxy.start() diff --git a/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/proxy/BlacklistTest.groovy b/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/proxy/BlacklistTest.groovy index 23f97bd05..c6e86249c 100644 --- a/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/proxy/BlacklistTest.groovy +++ b/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/proxy/BlacklistTest.groovy @@ -56,6 +56,7 @@ class BlacklistTest extends MockServerTest { .withBody("this URL should never be called")) proxy = new BrowserMobProxyServer() + proxy.setTrustAllServers(true) proxy.start() int proxyPort = proxy.getPort() @@ -128,6 +129,7 @@ class BlacklistTest extends MockServerTest { .withBody("not blacklisted")) proxy = new BrowserMobProxyServer() + proxy.setTrustAllServers(true) proxy.start() int proxyPort = proxy.getPort() diff --git a/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/proxy/NewHarTest.groovy b/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/proxy/NewHarTest.groovy index f466b5313..6d5ad4fd5 100644 --- a/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/proxy/NewHarTest.groovy +++ b/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/proxy/NewHarTest.groovy @@ -115,6 +115,7 @@ class NewHarTest extends MockServerTest { proxy = new BrowserMobProxyServer(); proxy.setHarCaptureTypes([CaptureType.RESPONSE_COOKIES] as Set) + proxy.setTrustAllServers(true) proxy.start() proxy.newHar() @@ -457,7 +458,8 @@ class NewHarTest extends MockServerTest { .withStatusCode(200) .withBody("success")) - proxy = new BrowserMobProxyServer(); + proxy = new BrowserMobProxyServer() + proxy.setTrustAllServers(true) proxy.start() proxy.newHar() @@ -497,6 +499,7 @@ class NewHarTest extends MockServerTest { proxy = new BrowserMobProxyServer(); proxy.rewriteUrl("https://localhost:${mockServerPort}/originalurl(.*)", "https://localhost:${mockServerPort}/httpsrewrittenurlcaptured\$1") + proxy.setTrustAllServers(true) proxy.start() proxy.newHar() @@ -855,6 +858,7 @@ class NewHarTest extends MockServerTest { .withBody("success")) proxy = new BrowserMobProxyServer(); + proxy.setTrustAllServers(true) proxy.setIdleConnectionTimeout(3, TimeUnit.SECONDS) proxy.start() diff --git a/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/proxy/WhitelistTest.groovy b/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/proxy/WhitelistTest.groovy index 0215d63a9..e5aac303d 100644 --- a/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/proxy/WhitelistTest.groovy +++ b/browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/proxy/WhitelistTest.groovy @@ -77,6 +77,7 @@ class WhitelistTest extends MockServerTest { .withBody("should never be returned")) proxy = new BrowserMobProxyServer() + proxy.setTrustAllServers(true) proxy.start() int proxyPort = proxy.getPort() @@ -127,6 +128,7 @@ class WhitelistTest extends MockServerTest { .withBody("whitelisted")) proxy = new BrowserMobProxyServer() + proxy.setTrustAllServers(true) proxy.start() int proxyPort = proxy.getPort() @@ -199,6 +201,7 @@ class WhitelistTest extends MockServerTest { .withBody("should never be returned")) proxy = new BrowserMobProxyServer() + proxy.setTrustAllServers(true) proxy.start() int proxyPort = proxy.getPort() diff --git a/browsermob-core-littleproxy/src/test/java/net/lightbody/bmp/proxy/InterceptorTest.java b/browsermob-core-littleproxy/src/test/java/net/lightbody/bmp/proxy/InterceptorTest.java index 41179b977..9f8a2bae0 100644 --- a/browsermob-core-littleproxy/src/test/java/net/lightbody/bmp/proxy/InterceptorTest.java +++ b/browsermob-core-littleproxy/src/test/java/net/lightbody/bmp/proxy/InterceptorTest.java @@ -249,6 +249,7 @@ public void testRequestFilterCanModifyHttpsRequestBody() throws IOException { .withBody("success")); proxy = new BrowserMobProxyServer(); + proxy.setTrustAllServers(true); proxy.start(); proxy.addRequestFilter(new RequestFilter() { @@ -367,6 +368,7 @@ public void testResponseFilterCanModifyHttpsTextContents() throws IOException { .withBody(originalText)); proxy = new BrowserMobProxyServer(); + proxy.setTrustAllServers(true); proxy.start(); proxy.addResponseFilter(new ResponseFilter() { @@ -839,6 +841,7 @@ public void testHttpsResponseFilterUrlReflectsModifications() throws IOException .withBody("success")); proxy = new BrowserMobProxyServer(); + proxy.setTrustAllServers(true); proxy.start(); final AtomicReference requestFilterOriginalUrl = new AtomicReference<>(); @@ -903,6 +906,7 @@ public void testHttpsResponseFilterMessageInfoPopulated() throws IOException { .withBody("success")); proxy = new BrowserMobProxyServer(); + proxy.setTrustAllServers(true); proxy.start(); final AtomicReference requestCtx = new AtomicReference<>(); diff --git a/browsermob-core-littleproxy/src/test/resources/log4j2-test.json b/browsermob-core-littleproxy/src/test/resources/log4j2-test.json new file mode 100644 index 000000000..f3e5e72ec --- /dev/null +++ b/browsermob-core-littleproxy/src/test/resources/log4j2-test.json @@ -0,0 +1,23 @@ +{ + "configuration" : { + "name": "test", + "appenders": { + "Console": { + "name": "console", + "target": "SYSTEM_OUT", + "PatternLayout": { + "pattern": "%-7r %date %level [%thread] %logger - %msg%n" + } + } + }, + + "loggers": { + "root": { + "level": "info", + "appender-ref": { + "ref": "console" + } + } + } + } +} \ No newline at end of file diff --git a/browsermob-core/pom.xml b/browsermob-core/pom.xml index 48b224900..62cca62cb 100644 --- a/browsermob-core/pom.xml +++ b/browsermob-core/pom.xml @@ -1,5 +1,6 @@ - + jar @@ -179,6 +180,26 @@ + + net.lightbody.bmp + mitm + ${project.version} + + + net.lightbody.bmp + littleproxy + + + org.bouncycastle + bcprov-jdk15on + + + org.bouncycastle + bcpkix-jdk15on + + + + junit junit diff --git a/browsermob-core/src/main/java/net/lightbody/bmp/BrowserMobProxy.java b/browsermob-core/src/main/java/net/lightbody/bmp/BrowserMobProxy.java index 2747615ff..89c2c72ee 100644 --- a/browsermob-core/src/main/java/net/lightbody/bmp/BrowserMobProxy.java +++ b/browsermob-core/src/main/java/net/lightbody/bmp/BrowserMobProxy.java @@ -3,11 +3,13 @@ import net.lightbody.bmp.core.har.Har; import net.lightbody.bmp.filters.RequestFilter; import net.lightbody.bmp.filters.ResponseFilter; +import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager; import net.lightbody.bmp.proxy.BlacklistEntry; import net.lightbody.bmp.proxy.CaptureType; import net.lightbody.bmp.proxy.auth.AuthType; import net.lightbody.bmp.proxy.dns.AdvancedHostResolver; import org.littleshoot.proxy.HttpFiltersSource; +import org.littleshoot.proxy.MitmManager; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -597,4 +599,23 @@ public interface BrowserMobProxy { * @throws java.lang.IllegalStateException if the proxy is already started */ void setMitmDisabled(boolean mitmDisabled); + + /** + * Sets the MITM manager, which is responsible for generating forged SSL certificates to present to clients. By default, + * BrowserMob Proxy uses the ca-certificate-rsa.cer root certificate for impersonation. See the documentation at + * {@link ImpersonatingMitmManager} and {@link ImpersonatingMitmManager.Builder} + * for details on customizing the root and server certificate generation. + * + * @param mitmManager MITM manager to use + */ + void setMitmManager(MitmManager mitmManager); + + /** + * Disables verification of all upstream servers' SSL certificates. All upstream servers will be trusted, even if they + * do not present valid certificates signed by certification authorities in the JDK's trust store. This option + * exposes the proxy to MITM attacks and should only be used when testing in trusted environments. + * + * @param trustAllServers when true, disables upstream server certificate verification + */ + void setTrustAllServers(boolean trustAllServers); } diff --git a/browsermob-core/src/main/java/net/lightbody/bmp/proxy/BrowserMobProxyHandler.java b/browsermob-core/src/main/java/net/lightbody/bmp/proxy/BrowserMobProxyHandler.java index 9104b94db..41578f7c4 100644 --- a/browsermob-core/src/main/java/net/lightbody/bmp/proxy/BrowserMobProxyHandler.java +++ b/browsermob-core/src/main/java/net/lightbody/bmp/proxy/BrowserMobProxyHandler.java @@ -97,7 +97,7 @@ public void handleConnect(String pathInContext, String pathParams, HttpRequest r } @Override - protected void wireUpSslWithCyberVilliansCA(String host, SeleniumProxyHandler.SslRelay listener) { + protected void wireUpSslWithImpersonationCA(String host, SeleniumProxyHandler.SslRelay listener) { List originalHosts = httpClient.originalHosts(host); if (originalHosts != null && !originalHosts.isEmpty()) { if (originalHosts.size() == 1) { @@ -109,7 +109,7 @@ protected void wireUpSslWithCyberVilliansCA(String host, SeleniumProxyHandler.Ss host = "*" + first.substring(first.indexOf('.')); } } - super.wireUpSslWithCyberVilliansCA(host, listener); + super.wireUpSslWithImpersonationCA(host, listener); } @Override diff --git a/browsermob-core/src/main/java/net/lightbody/bmp/proxy/ProxyServer.java b/browsermob-core/src/main/java/net/lightbody/bmp/proxy/ProxyServer.java index 15c4c27b9..11da3d56a 100644 --- a/browsermob-core/src/main/java/net/lightbody/bmp/proxy/ProxyServer.java +++ b/browsermob-core/src/main/java/net/lightbody/bmp/proxy/ProxyServer.java @@ -29,6 +29,7 @@ import org.java_bandwidthlimiter.BandwidthLimiter; import org.java_bandwidthlimiter.StreamManager; import org.littleshoot.proxy.HttpFiltersSource; +import org.littleshoot.proxy.MitmManager; import org.openqa.selenium.Proxy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -922,6 +923,16 @@ public void setMitmDisabled(boolean mitmDisabled) { LOG.warn("The legacy ProxyServer implementation does not support disabling MITM."); } + @Override + public void setMitmManager(MitmManager mitmManager) { + LOG.warn("The legacy ProxyServer implementation does not support custom MITM managers."); + } + + @Override + public void setTrustAllServers(boolean trustAllServers) { + LOG.warn("The legacy ProxyServer implementation does not support the trustAllServers option."); + } + public void cleanSslCertificates() { handler.cleanSslWithCyberVilliansCA(); } diff --git a/browsermob-core/src/main/java/net/lightbody/bmp/proxy/jetty/http/SslListener.java b/browsermob-core/src/main/java/net/lightbody/bmp/proxy/jetty/http/SslListener.java index ad166962f..cc42a01b4 100644 --- a/browsermob-core/src/main/java/net/lightbody/bmp/proxy/jetty/http/SslListener.java +++ b/browsermob-core/src/main/java/net/lightbody/bmp/proxy/jetty/http/SslListener.java @@ -77,7 +77,7 @@ public class SslListener extends SocketListener private boolean _wantClientAuth = false; // Set to true if we would like client certificate authentication. private String _protocol= "TLS"; private String _algorithm = (Security.getProperty("ssl.KeyManagerFactory.algorithm")==null?"SunX509":Security.getProperty("ssl.KeyManagerFactory.algorithm")); // cert algorithm - private String _keystoreType = "JKS"; // type of the key store + private String _keystoreType = "PKCS12"; // type of the key store private String _provider = null; @@ -256,7 +256,7 @@ protected SSLServerSocketFactory createFactory() } KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(_algorithm); - KeyStore keyStore = KeyStore.getInstance(_keystoreType); + KeyStore keyStore = KeyStore.getInstance(_keystoreType, "SunJSSE"); keyStore.load(Resource.newResource(_keystore).getInputStream(), _password.toString().toCharArray()); keyManagerFactory.init(keyStore,_keypassword.toString().toCharArray()); diff --git a/browsermob-core/src/main/java/net/lightbody/bmp/proxy/selenium/KeyStoreManager.java b/browsermob-core/src/main/java/net/lightbody/bmp/proxy/selenium/KeyStoreManager.java index 9889f4ae1..96c332969 100644 --- a/browsermob-core/src/main/java/net/lightbody/bmp/proxy/selenium/KeyStoreManager.java +++ b/browsermob-core/src/main/java/net/lightbody/bmp/proxy/selenium/KeyStoreManager.java @@ -10,7 +10,7 @@ import java.util.HashMap; /** - * This is the main entry point into the Cybervillains CA. + * This is the main entry point into the impersonated CA. * * This class handles generation, storage and the persistent * mapping of input to duplicated certificates and mapped public @@ -37,13 +37,10 @@ public class KeyStoreManager { private final String CERTMAP_SER_FILE = "certmap.ser"; private final String SUBJMAP_SER_FILE = "subjmap.ser"; - private final String EXPORTED_CERT_NAME = "cybervillainsCA.cer"; - private final char[] _keypassword = "password".toCharArray(); private final char[] _keystorepass = "password".toCharArray(); - private final String _caPrivateKeystore = "cybervillainsCA.jks"; - private final String _caCertAlias = "signingCert"; - public static final String _caPrivKeyAlias = "signingCertPrivKey"; + private final String _caPrivateKeystore = "ca-keystore-rsa.p12"; + public static final String _caPrivKeyAlias = "key"; X509Certificate _caCert; PrivateKey _caPrivKey; @@ -143,7 +140,7 @@ public KeyStoreManager(File root) { try { - _ks = KeyStore.getInstance("JKS"); + _ks = KeyStore.getInstance("PKCS12", "SunJSSE"); reloadKeystore(); } @@ -226,14 +223,14 @@ public KeyStoreManager(File root) { } - private void reloadKeystore() throws FileNotFoundException, IOException, NoSuchAlgorithmException, CertificateException, KeyStoreException, UnrecoverableKeyException { + private void reloadKeystore() throws FileNotFoundException, IOException, NoSuchAlgorithmException, CertificateException, KeyStoreException, UnrecoverableEntryException { InputStream is = new FileInputStream(new File(root, _caPrivateKeystore)); - if (is != null) { - _ks.load(is, _keystorepass); - _caCert = (X509Certificate)_ks.getCertificate(_caCertAlias); - _caPrivKey = (PrivateKey)_ks.getKey(_caPrivKeyAlias, _keypassword); - } + _ks.load(is, _keystorepass); + + KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) _ks.getEntry(_caPrivKeyAlias, new KeyStore.PasswordProtection(_keypassword)); + _caCert = (X509Certificate) privateKeyEntry.getCertificate(); + _caPrivKey = privateKeyEntry.getPrivateKey(); } /** @@ -259,7 +256,6 @@ protected void createKeystore() { _ks.load(null, _keystorepass); - _ks.setCertificateEntry(_caCertAlias, signingCert); _ks.setKeyEntry(_caPrivKeyAlias, caPrivKey, _keypassword, new java.security.cert.Certificate[] {signingCert}); File caKsFile = new File(root, _caPrivateKeystore); @@ -267,24 +263,9 @@ protected void createKeystore() { OutputStream os = new FileOutputStream(caKsFile); _ks.store(os, _keystorepass); - log.debug("Wrote JKS keystore to: " + + log.debug("Wrote keystore to: " + caKsFile.getAbsolutePath()); - // also export a .cer that can be imported as a trusted root - // to disable all warning dialogs for interception - - File signingCertFile = new File(root, EXPORTED_CERT_NAME); - - FileOutputStream cerOut = new FileOutputStream(signingCertFile); - - byte[] buf = signingCert.getEncoded(); - - log.debug("Wrote signing cert to: " + signingCertFile.getAbsolutePath()); - - cerOut.write(buf); - cerOut.flush(); - cerOut.close(); - _caCert = (X509Certificate)signingCert; _caPrivKey = caPrivKey; } @@ -314,12 +295,13 @@ protected void createKeystore() { public synchronized void addCertAndPrivateKey(String hostname, final X509Certificate cert, final PrivateKey privKey) throws KeyStoreException, CertificateException, NoSuchAlgorithmException { -// String alias = ThumbprintUtil.getThumbprint(cert); - - _ks.deleteEntry(hostname); + try { + _ks.deleteEntry(hostname); + } catch (KeyStoreException e) { + // ignore errors deleting the existing entry + } - _ks.setCertificateEntry(hostname, cert); - _ks.setKeyEntry(hostname, privKey, _keypassword, new java.security.cert.Certificate[] {cert}); + _ks.setKeyEntry(hostname, privKey, _keypassword, new java.security.cert.Certificate[] {cert, getSigningCert()}); if(persistImmediately) { @@ -573,8 +555,7 @@ public X509Certificate getMappedCertificateForHostname(String hostname) throws C } private String getSubjectForHostname(String hostname) { - //String subject = "C=USA, ST=WA, L=Seattle, O=Cybervillains, OU=CertificationAutority, CN=" + hostname + ", EmailAddress=evilRoot@cybervillains.com"; - String subject = "CN=" + hostname + ", OU=Test, O=CyberVillainsCA, L=Seattle, S=Washington, C=US"; + String subject = "CN=" + hostname + ", OU=BrowserMob Proxy, O=Impersonated Certificate, L=Seattle, S=Washington, C=US"; return subject; } diff --git a/browsermob-core/src/main/java/net/lightbody/bmp/proxy/selenium/SeleniumProxyHandler.java b/browsermob-core/src/main/java/net/lightbody/bmp/proxy/selenium/SeleniumProxyHandler.java index 65205734d..f5601cc1b 100755 --- a/browsermob-core/src/main/java/net/lightbody/bmp/proxy/selenium/SeleniumProxyHandler.java +++ b/browsermob-core/src/main/java/net/lightbody/bmp/proxy/selenium/SeleniumProxyHandler.java @@ -40,14 +40,14 @@ import java.net.UnknownHostException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.List; -import java.util.ArrayList; -import java.util.Collections; /* ------------------------------------------------------------ */ @@ -72,7 +72,7 @@ public class SeleniumProxyHandler extends AbstractHttpHandler { private final Map _sslMap = new LinkedHashMap(); @SuppressWarnings("unused") private String sslKeystorePath; - private boolean useCyberVillains = true; + private boolean useImpersonatingCA = true; private boolean trustAllSSLCertificates = false; private final String dontInjectRegex; private final String debugURL; @@ -547,8 +547,8 @@ protected SslRelay getSslRelayOrCreateNew(URI uri, InetAddrPort addrPort, HttpSe listener = new SslRelay(addrPort); - if (useCyberVillains) { - wireUpSslWithCyberVilliansCA(host, listener); + if (useImpersonatingCA) { + wireUpSslWithImpersonationCA(host, listener); } else { wireUpSslWithRemoteService(host, listener); } @@ -603,7 +603,7 @@ protected void wireUpSslWithRemoteService(String host, SslRelay listener) throws listener.setNukeDirOrFile(keystore); } - protected void wireUpSslWithCyberVilliansCA(String host, SslRelay listener) { + protected void wireUpSslWithImpersonationCA(String host, SslRelay listener) { try { // see https://github.com/webmetrics/browsermob-proxy/issues/105 String escapedHost = host.replace('*', '_'); @@ -616,23 +616,17 @@ protected void wireUpSslWithCyberVilliansCA(String host, SslRelay listener) { deleteDirectoryTasks.add(deleteDirectoryTask); Runtime.getRuntime().addShutdownHook(new Thread(deleteDirectoryTask)); - // copy the cybervillains cert files to the temp directory from the classpath - Path cybervillainsCer = tempDir.resolve("cybervillainsCA.cer"); - Path cybervillainsJks = tempDir.resolve("cybervillainsCA.jks"); - Path blankDec = tempDir.resolve("blank_crl.dec"); - Path blankPem = tempDir.resolve("blank_crl.pem"); + // copy the CA keystore to the temp directory from the classpath + Path rsaKeystorePath = tempDir.resolve("ca-keystore-rsa.p12"); - Files.copy(getClass().getResourceAsStream("/sslSupport/cybervillainsCA.cer"), cybervillainsCer); - Files.copy(getClass().getResourceAsStream("/sslSupport/cybervillainsCA.jks"), cybervillainsJks); - Files.copy(getClass().getResourceAsStream("/sslSupport/blank_crl.dec"), blankDec); - Files.copy(getClass().getResourceAsStream("/sslSupport/blank_crl.pem"), blankPem); + Files.copy(getClass().getResourceAsStream("/sslSupport/ca-keystore-rsa.p12"), rsaKeystorePath); KeyStoreManager mgr = new KeyStoreManager(root); mgr.getCertificateByHostname(host); mgr.getKeyStore().deleteEntry(KeyStoreManager._caPrivKeyAlias); mgr.persist(); - listener.setKeystore(new File(root, "cybervillainsCA.jks").getAbsolutePath()); + listener.setKeystore(rsaKeystorePath.toFile().getAbsolutePath()); listener.setNukeDirOrFile(root); } catch (Exception e) { log.error("Error occurred wiring CA", e); diff --git a/browsermob-core/src/main/resources/sslSupport/blank_crl.dec b/browsermob-core/src/main/resources/sslSupport/blank_crl.dec deleted file mode 100644 index 4485d7662827ecfda3c664cce428351f3acb3140..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 444 zcmXqLV%%ZS#Hh%`$Y{XJ#;Mij(e|B}k&%^^!64F5%0PmRIh2KqN6@)4DYYmpGbblA zF|SxJIX~A>&p-zx$IYXPBB$V-T2zvmmYJMbl9`{U;8Tc=(nzJx=Ni+=|*#ry`E|3#MSeP+GhqU1i zpXT#*J`-!h0m%?QJqNv#h|60Jon+&^wR!gQgoW0}c@-V!3;njZ(&zg$>g_(hW!D|0 U9~M>_rw6G|@1AmU$0avI06&74od5s; diff --git a/browsermob-core/src/main/resources/sslSupport/blank_crl.pem b/browsermob-core/src/main/resources/sslSupport/blank_crl.pem deleted file mode 100644 index ba8593074..000000000 --- a/browsermob-core/src/main/resources/sslSupport/blank_crl.pem +++ /dev/null @@ -1,12 +0,0 @@ ------BEGIN X509 CRL----- -MIIBuDCCASECAQEwDQYJKoZIhvcNAQEFBQAwWTEaMBgGA1UECgwRQ3liZXJWaWxs -aWFucy5jb20xLjAsBgNVBAsMJUN5YmVyVmlsbGlhbnMgQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkxCzAJBgNVBAYTAlVTFw0xMjAyMDUwMzAwMTBaFw0zMTEwMjMwMzAw -MTBaoIGTMIGQMIGBBgNVHSMEejB4gBQKvBeVNGu8hxtbTP31Y4UttI/1bKFdpFsw -WTEaMBgGA1UECgwRQ3liZXJWaWxsaWFucy5jb20xLjAsBgNVBAsMJUN5YmVyVmls -bGlhbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxCzAJBgNVBAYTAlVTggEBMAoG -A1UdFAQDAgEBMA0GCSqGSIb3DQEBBQUAA4GBAEtCmbwTrP7xgkGH3uWH7QU7i52j -aqraBnfheTv6jMFvXq/Nc9hvcmhkkw/+UezjZ/tK1a8a1+vJDPYXUzeAV/eWTPWf -AgWAwBlUTi5ALnRY07TCyQYN2rOb52ChO8cNIUGfEvs41I5N5Vrtvg6m10Eb4XF6 -M2dSJ5eLlMm40kYx ------END X509 CRL----- diff --git a/browsermob-core/src/main/resources/sslSupport/ca-certificate-ec.cer b/browsermob-core/src/main/resources/sslSupport/ca-certificate-ec.cer new file mode 100644 index 000000000..63d902340 --- /dev/null +++ b/browsermob-core/src/main/resources/sslSupport/ca-certificate-ec.cer @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB8jCCAZigAwIBAgIUUridrS1mqPKh8aA7igVOQRUc8P8wCgYIKoZIzj0EAwQw +RjEZMBcGA1UEAwwQTGl0dGxlUHJveHkgTUlUTTEpMCcGA1UECgwgTGl0dGxlUHJv +eHkgRUNDIEltcGVyc29uYXRpb24gQ0EwHhcNMTUwMTAyMDAwMDAwWhcNMjUwMTAy +MDAwMDAwWjBGMRkwFwYDVQQDDBBMaXR0bGVQcm94eSBNSVRNMSkwJwYDVQQKDCBM +aXR0bGVQcm94eSBFQ0MgSW1wZXJzb25hdGlvbiBDQTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABB9DdlM/uhkMWTYFo9ETzPrMWBlfhCD0z3J2F1aH9a3OPiPYBio6 +fzTVSZO2rU9ItfcRRpCGeMzY+pilfUNkPXyjZDBiMB0GA1UdDgQWBBQ0TT/oOVF2 +mT10+X9W3NDESql7ZzAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBtjAjBgNV +HSUEHDAaBggrBgEFBQcDAQYIKwYBBQUHAwIGBFUdJQAwCgYIKoZIzj0EAwQDSAAw +RQIhAOb/s6H8v1XeEPGEmMdVEhRnhJgTYAktQKQLZid8QBzsAiA7zc1mFLRAKs98 +5d9+qGFsv7Fy0yTNO3vFyL7DL2mykg== +-----END CERTIFICATE----- diff --git a/browsermob-core/src/main/resources/sslSupport/ca-certificate-rsa.cer b/browsermob-core/src/main/resources/sslSupport/ca-certificate-rsa.cer new file mode 100644 index 000000000..b962a08f2 --- /dev/null +++ b/browsermob-core/src/main/resources/sslSupport/ca-certificate-rsa.cer @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfzCCAmegAwIBAgIVAMFQpicWi3EjPX08LgeuA8nAOEfIMA0GCSqGSIb3DQEB +DQUAMEYxGTAXBgNVBAMMEExpdHRsZVByb3h5IE1JVE0xKTAnBgNVBAoMIExpdHRs +ZVByb3h5IFJTQSBJbXBlcnNvbmF0aW9uIENBMB4XDTE1MDEwMjAwMDAwMFoXDTI1 +MDEwMjAwMDAwMFowRjEZMBcGA1UEAwwQTGl0dGxlUHJveHkgTUlUTTEpMCcGA1UE +CgwgTGl0dGxlUHJveHkgUlNBIEltcGVyc29uYXRpb24gQ0EwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC141M+lc046DJaNqIARozRPROGt/s5Ng1UOE84 +tKhd+M/REaOeNovW+42uMa4ZifJAK7Csc0dx54Iq35LXy0tMw6ly/MB0pFi+aFCJ +VzXZhbAWIsUmjU8t6z2Y0sjKVX/g3HkdXqaX94jlDtsTjeQXvFhiJNRlX/Locc/f +/oNYZWhg7IPGyQglRY9Dco9kZMSbh5y0yfM8002PNPbNOP4dMX4yYqovT90XbvQ2 +rCBbiS6Cys7j44vwOcra9srlb3YQiOCOsYCf7eIhT1GH8tqQ84CHblufqxcGIvXv +V1ex6bDFy63tiPySsOwuVnZglkQ0MDl1GMKVySdPw/qQM5v9AgMBAAGjZDBiMB0G +A1UdDgQWBBRFMQtpkCyZIK9NxaEJDvbfaV1QOzAPBgNVHRMBAf8EBTADAQH/MAsG +A1UdDwQEAwIBtjAjBgNVHSUEHDAaBggrBgEFBQcDAQYIKwYBBQUHAwIGBFUdJQAw +DQYJKoZIhvcNAQENBQADggEBAJuYv1NuxPHom579iAjs19YrFGewHpv4aZC7aWTt +oC1y9418w7QzVOAz2VzluURazUdg/HS9s8abJ8IS0iD0xLz0B1cvJ6F2BezjAwyG +2LxZggmBdLqwjdRkX0Mx3a2HqUpEqaNeKyE8VmzwPuDHN1AqbFcuOPHN7fm7kAtL +4bxFmjgSt7PjEdYwysdjkLC6m+236tuFydpVkXMjuBthsk/hZ1Y/3tbCj/B9a9// +5O+HhYEy+Oa64iFvxfgDfKKUQR3VmwThj1Dh2iJw/kbPJEuQ/PtfcnQhOqyliwg6 +Edxd1kaO4HU8Am6TwpmpPFWHRqhM2xj2PAGyfFtN1WfBEQ4= +-----END CERTIFICATE----- diff --git a/browsermob-core/src/main/resources/sslSupport/ca-keystore-ec.p12 b/browsermob-core/src/main/resources/sslSupport/ca-keystore-ec.p12 new file mode 100644 index 0000000000000000000000000000000000000000..ef708aad871120e54b044de30dd517e6b5e19759 GIT binary patch literal 1019 zcmXqLV*bv=$ZXKWypfGltIebBJ1-+Ut4B%QT*1X*_Pwc*LOb zAR9MS0}mG?(}FUC5(5o3E|?I|Oeq$TCH6TcQnO+f`Ca|#Vf)NgAoOy~0wyLF1{RMZ zk;#E2;*mR7slV#Fkn+OR?#v89X;EF7j7=_o-{<}@%`MI3(Qi@y8MEPk$H!cwqwiH@BgcfhgR4oFaxCa%>FQ45zG61QC2pL8ckFkL~<}A0qUo_{? zat?oQ36m>@+ZI1c-39VkLlaYmLZ)si8_PDOt~L{!;P?7JrIKGB?+E1lv|y3gZW~S& z&q;;WZ|C{nUZ<~MUsue0dv2MW_meG*63Q*71&@hUcpURGF5U0Qt2in6;=G%GI_@iX z)i|%QJz03`jJ@fWVCC2DqW$mx^tJ!E;_)RnO}*mqI>~~)(^uYR{~+M{s9#QM$_fVF z3uy|H9@5#LnHPRt5utqS@`goy_0j9!?TU6$$`YO~aJQ}VgVlc=XEH|p2;a}~$z8xQo6Yx}N!sIurK^%G<~vJp zmEQ3UtI7~%PhDMeYPbI5-X*R4Nqa)2I8DBsK5l(jA$ZNyPp=fCF5GWx$+77)-=Mr# zYZa68*l#9?Lj_<;E1ZJHgkj6?UI>VEDxYr$KlkTw>RZ#*G9M;;6G z3ACxaGv7X^>fEHOQ=U!x?xJ#i`a+FlhN<_JHm>|$$|0u5QqvGKr-)NjMEGS5hcXqPwkGi=dVK$FY;n&%sOoFTU zKSm~83DT(lspFnd%JAUZ!pi@_xkd>=zx|kWb<`|d4t30tw|LDSm3qDH;>(WD0X@-g zj=nGXYTWf;&Fb}$3mJ>K9l{@6;p?qhRo2d5n73p8#5_im%dS(yn>PJge62pWfh&G; zQmft#8_%i@_I)PzJ}|Vr4mmB?GVO9r%nprP1;Wzh7b|9{e!J@O@kWz@t%0HeCmX9a zA2X8_D+7xN(-OVO7w25$Ju#!LMZccA>_oc23l@=y8xy1?&zD>)vn^7)cz^xedl%O{ H2PIVia9yWo literal 0 HcmV?d00001 diff --git a/browsermob-core/src/main/resources/sslSupport/ca-keystore-rsa.p12 b/browsermob-core/src/main/resources/sslSupport/ca-keystore-rsa.p12 new file mode 100644 index 0000000000000000000000000000000000000000..f995c1774fd080c7a5f67ccbd4d6b4a6a36915a4 GIT binary patch literal 2582 zcmY+EXEYlM8^?tRiM>*z#7eCq)Sg9cO6@joN#+bWo|r)~c=18rM~+ zRE$=O+OK=g``&xr56^kd`ThUT^YMqkK$yt^6bKA>0t6OK)k{6115g1fF<>kZ1Ma!P z&k-0(xBrVM&4Cz7lPhd^HM$_0|6b8T0OXYz;0Xc)JVXeCApgTx&pCmtK|z@s8qiw9 zr1p>pq#*Xl&grmEl?k7hDf;YD=V$$PoF$%)gCmn#Y!IRTEl zihYHiTN`n#=eHbiy8C_68E<~3rwI|_2;WIMet~_PR-%95m}BpTfxZ1e2!wdyG|oj5 zPVK?26CyC|YV`*HqAjzRO9Rv-1w^j4xpgIF&5dVN{pF^SF)gt0Cze=zZ6BPPb^rWZ zqp(^~Dg9K7cIUIn*QBcXkpitOL05x-6r{V1zrXuVv;fZng?tPx#E?=~h zraNbzpju(vsSnued3Ss3dA6epg%dR*8GF!sTmfa+wh`v__L^sqKGc`HW&MkkLYZ10 zPa{JTU~iSagJ|GKk|d{HojLcIqY~QCoukp1$ z8qBgMd9-k1vR=WBI%M8DM&?HkbI{%^y@{lo)pm)=ga%g3juYj^L*4B9RRW``=MOa@ z(r@+c=cBj-pPo;;o!WQ1jM$gPqMJ1Wc&tsIX0q+bc?fFJZK(O!CVbT6=FzF59jLuS zB}lk22aD8~@<%W*z%lIiAFFJ)4#dI^(XmaZ z4W7%B#ov!4$}O?tf?OR`eB>Q=<_zkFY$r)8URrwGZUAav5xx5kYE~T1!#Le#aYRU% ziJSI|1eJ_#xeQr0&#zM?vZiiukG?F?!s2KQPQ{!So8qb!iJiPraz0@7NMiv0Ol2{itu$5(KHgR1f} zSAumaEWTP1&D0?l4jWL&rCQur1ISy;yHKH5hiD1(4m<<`<$1;Kx7rk}3B|XRN`Px+ z!1#x=6?^5(*&wO`qgT5*y_;e@WhjVk0=z1D^Yb+KWwE*&HOJMv=0eNGBU%iWxj5p}Id}=&Ko)IMry861 z+Cx<*Kdgj&EDiUACP}VE=LWFD%*~Xb4h%M{>jQ(j^=dX*`}B+17i=Z(RJPiG5fsp1 zhD3Ftst!ATzK&4KBV6HLjbtc|r zFI$}enH&lYiBN%Y6z24D_vQ?ICt(_mYGy_802ObF#4&PUwkZNsx?#AlTt#k{bNcn zs}wa4h|HJFlPns+_b+CXVg)i=p!_{~rDSBKq~v526r>atWv;Ag_)m!nbR|-iE2KaU zAiG*p|0aO{Q!D6iwff0PJHA92GlzeI#i@qR(@2q$75`Q%8bk4Wq>sHNZwH^^NaX>! z{(PHrnsbaPSm`ArJYgqdmEqGWxVc6I4j>os0~rd!wtK?pRN}^N@4>C;kaiRpM8xe3>OZK~rx( zE)_S~6rNZjalU<@vhQ=sC=L(Ij&QeVM`7Hf4r1l^6=N!rF_R9;fEEwiqyRXC1gX{U^4X}Km`$q9D%3@3DQuxQ?K3w+c($}&W zh`HKJOJA(HR(Oqvk@Z?w{u{9zU=#4%Faj4|>d(mumHT;^a~??ZtS4c6Fo z^59UMH&-bkUp0a&r0P@kE2^FDB-{HQOH*R67k<_|3=w+fv z9W)O5&|C3ja^0sp=Rl*?6b0ATSpK5ZX$U)mUV=?pPb9gG+re`NU4+^eKc{05naM@> zH~*0@HNBHaht0WY6L`vFJ69%jP%7%BS$XGE zJ+8a-uI4bW+tG%`yCO-+?l#qGuxH<*-$JmDmfaa|53k$Fa1LJ6U&`Z|*ZL%_F}iT9 zQB?Qy5r|GgnPV$2kN&r$kmHI&&Mn?RBRN!XNJM?VL{)UvvFXlM5o1Q!q`;7=@+=y1 zX$1H~^zFTDw|3qfO_r1RZJMKl>dVKJzUYY0Vr=cPoI=!W%}U9q*z11$vHw5!G{2YD8=Y0$T=y=fUJ)LDJ{I5e|J+2zOit+fwzl3aIyfh cYK={>xf^#hzR&c9^ndl#XX#z!_*)(S0RkQ_IUDvF$fb81mZW?E))Vo7Fxo`Pd(Nk)EAW=W+Xw*ec;uV{$yljU~cSXFlg*#YHVa^ z4QNf1I@5k;&s0YHSqoRnEWCA7JE6ZIbbh^;ZR(?&Vabi>xEFkS@{G|%bHz=2-w&>@ znMH4EEagAPIr|=yJ|j~t&%X_e6hED|j#Qt2Cr>i-gWGYI?qd;aFJE|-c+ZjFR<|`` z#UuIuE5ldKxY%7c&F9WxsmCd83;&n8JC-ufKKhl3nUR5UajrqOfh;hrWcgUcSVXw? zh)*@i-qS7}?eq6*a;xr^{;xR(LLg~jM#ldv90qJaiiwfIfEy&n&%y$XO*R8f5J#0o z$v}aPLz@j4o$Snva26*US3;WyW7`i`Mh^5?X9flX|G#A)&wRXod!pU5_Rx+T!MXrx zri$cX*Ub!!C%7lEGSq7H``^$zG3)u)j8{5S9d`Ij9(u*^{`~8+DQk;!=Wk1Tz-YPG zhST&QcTRaVQ~K%VoX) P?LrqaYZ$lr`uqa`tD@ZL diff --git a/browsermob-core/src/main/resources/sslSupport/cybervillainsCA.jks b/browsermob-core/src/main/resources/sslSupport/cybervillainsCA.jks deleted file mode 100644 index ed1a64601ecba8eea2a102e6dac0340f10226c9f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2148 zcmezO_TO6u1_mZL<}S`m&&$k9Pfjf=VPIf1^)88WVPK8WGc~XTDw}E0#5C2QiSge8 zW+p}^CPqdBUN%mxHjlRNyo`*jtPBQ`hEfI+Y|No7Ts(r#l}V{ZVVOBOnTdJDddc~@ zhI$4%AUSRxRTMb|XP{-7X_?81C7Jno3XY{E8Tmz-C6$KU2Am-EY{E>T!3J{TyoQDb zMur9k#s&t422tX?#vr~iluJvi8|NeYlaZByxv`hQps|ywv5}!QpfyeEO#7KVQyJ}N zEnF$H@YYT3g#Loi`So75sgG`kB{!bqUhwJ3Ge#H96*ujDKe)bT7QLyll>Z#(?0Zc5 zj7+sW|28aA{B+tnQholNJju)tZpT@=k43D#eBo8%Jx6|9-PVW|kL3Ta3|}?lVt3s% zpF4-89;dV|{9o$sSjs&6=vO9YMh3>kxdzz=vcRyCjI>iDw2a;H#0Dv;GV?F zP^;1Je?#xYtmj`dUg=DA*x@gE=oP>F^RLgQtS!!+zb)wjqvc*3PSb9g-K zOMD1CXPoEcS#hX?ecgWaH;@f*9^0!T`ZOT%no$vWAm-YU)3th;pVch2H^ADJS z85x9-5^q6KW?6P>B|Hr?Fzp5=-t7i_Y+OLku`p^e2?CQW14|RrV%ulJpRQdHIsM@p zi_!9&!@`~`r?{LFzo)+6aq1TVM*T&SGj~T#FO;3Q>!`874)!n3E114bPcBupKl*FQ z?buJUr<|S2T#iToI=gPx$`h=wBul>8F`h`DaYXO;g;I_DRpp-?Woqi%4(-lJ+j!Cz z>|C3Ri56N7Haq@q+3~62D1-UNxdw#`kBfHyW;MH_*Slrw*^4g5@BO#zRr6G=+v$4$ zqKDo0yYGrl_bJp~Ir7(Qzu@V;T~l94eb;*X+VwB@j@})TdUp&@mnF%cWU=%=k$vq& z`|GZ0Vcy|sbNGs!Ed?0Yt$F{94x$ia82nn z=3QH^Og`|M@ebpQ=LKu6So&f`_qS#AR(h3&FWyykwsb{o{<{CywAkZ>=Dky&BrIVm z littleProxyImplClass = (Class) Class.forName("net.lightbody.bmp.BrowserMobProxyServer"); LegacyProxyServer littleProxyImpl = littleProxyImplClass.newInstance(); + // set the trustAllServers option on the LP implementation to "true", to avoid certificate verification issues with MITM + Method setTrustAllServersMethod = littleProxyImplClass.getMethod("setTrustAllServers", Boolean.TYPE); + setTrustAllServersMethod.invoke(littleProxyImpl, true); + log.info("Using LittleProxy implementation to execute test for class: " + getClass().getSimpleName()); return littleProxyImpl; - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { throw new RuntimeException("The System property bmp.use.littleproxy was true, but the LittleProxy implementation could not be loaded.", e); } } else { diff --git a/mitm/README.md b/mitm/README.md new file mode 100644 index 000000000..aa4175aea --- /dev/null +++ b/mitm/README.md @@ -0,0 +1,135 @@ +# MITM with LittleProxy +The MITM module is a LittleProxy-compatible module that enables man-in-the-middle interception of HTTPS requests. Though it is developed and distributed with BrowserMob Proxy, it has no dependency on BMP and can be used in a LittleProxy-only environment. (The only transitive dependency of the MITM module is the Bouncy Castle encryption library.) + +## Quick start +The MITM module uses "sensible" default settings that should work for the vast majority of users without any further configuration. + +### LittleProxy (without BrowserMob Proxy) +**Note:** The MITM module requires Java 7 + +To use MITM with standalone LittleProxy, add a dependency to the mitm module in your pom: + +```xml + + + org.littleshoot + littleproxy + 1.1.0-beta1 + + + <-- new dependency on the MITM module --> + + net.lightbody.bmp + mitm + 2.1.0-beta-4 + +``` + +When creating your LittleProxy server, set the MitmManager to an instance of `net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager`: + +```java + HttpProxyServerBootstrap bootstrap = DefaultHttpProxyServer.bootstrap() + .withManInTheMiddle(ImpersonatingMitmManager.builder().build()); +``` + +The default implementation of `ImpersonatingMitmManager` will generate a new CA Root Certificate when the first request is made to the proxy. See below for instructions on saving the generated root certificate, or using your own root certificate and private key. + +### BrowserMob Proxy +The MITM module is enabled by default with BrowserMob Proxy. No additional steps are required to enable MITM with BrowserMob Proxy. + +By default, BrowserMob Proxy will use the `ca-keystore-rsa.p12` file to load its CA Root Certificate and Private Key. The corresponding certificate file is `ca-certificate-rsa.cer`, which can be installed as a trusted Certification Authority in browsers or other HTTP clients to avoid HTTPS warnings when using BrowserMob Proxy. + +## Examples +Several examples are available to help you get started: + +Example File | Configuration +-------------|-------------- +[LittleProxyDefaultConfigExample.java](src/test/java/net/lightbody/bmp/mitm/example/CustomCAKeyStoreExample.java) | Default configuration with LittleProxy +[SaveGeneratedCAExample.java](src/test/java/net/lightbody/bmp/mitm/example/SaveGeneratedCAExample.java) | Save a dynamically-generated CA root certificate for installation in a browser +[CustomCAKeyStoreExample.java](src/test/java/net/lightbody/bmp/mitm/example/CustomCAKeyStoreExample.java) and [CustomCAPemFileExample.java](src/test/java/net/lightbody/bmp/mitm/example/CustomCAPemFileExample.java) | Use an existing CA certificate and private key +[EllipticCurveCAandServerExample.java](src/test/java/net/lightbody/bmp/mitm/example/EllipticCurveCAandServerExample.java) | Use EC cryptography when generating the CA private key and when impersonating server certificates + + +## Generating and Saving Root Certificates +By default, when using the MITM module with LittleProxy, the CA Root Certificate and Private Key are generated dynamically. The dynamically generated Root Certificate and Private Key can be saved for installation in a browser or later reuse by using the methods on the `RootCertificateGenerator` class. For example: + +```java + // create a CA Root Certificate using default settings + RootCertificateGenerator rootCertificateGenerator = RootCertificateGenerator.builder().build(); + + // save the newly-generated Root Certificate and Private Key -- the .cer file can be imported + // directly into a browser + rootCertificateGenerator.saveRootCertificateAsPemFile(new File("/tmp/certificate.cer"); + rootCertificateGenerator.savePrivateKeyAsPemFile(new File("/tmp/private-key.pem", "password"); + + // or save the certificate and private key as a PKCS12 keystore, for later use + rootCertificateGenerator.saveRootCertificateAndKey("PKCS12", new File("/tmp/keystore.p12", + "privateKeyAlias", "password"); + + // tell the ImpersonatingMitmManager use the RootCertificateGenerator we just configured + ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder() + .rootCertificateSource(rootCertificateGenerator) + .build(); + + // tell LittleProxy to use the ImpersonatingMitmManager when MITMing + HttpProxyServerBootstrap bootstrap = DefaultHttpProxyServer.bootstrap() + .withManInTheMiddle(mitmManager); +``` + +## Using a Custom Certification Authority +Whether you are using the MITM module with LittleProxy or BrowserMob Proxy, you can provide your own root certificate and private key to use when signing impersonated server certificates. To use a root certificate and private key from a key store (PKCS12 or JKS), use the `KeyStoreFileCertificateSource` class: + +```java + CertificateAndKeySource existingCertificateSource = + new KeyStoreFileCertificateSource("PKCS12", new File("/path/to/keystore.p12", "privateKeyAlias", "password"); + + // configure the MitmManager to use the custom KeyStore source + ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder() + .rootCertificateSource(existingCertificateSource) + .build(); + + // when using LittleProxy, use the .withManInTheMiddle method on the bootstrap: + HttpProxyServerBootstrap bootstrap = DefaultHttpProxyServer.bootstrap() + .withManInTheMiddle(mitmManager); + + // when using BrowserMob Proxy, use .setMitmManager() on the BrowserMobProxy object: + BrowserMobProxy proxyServer = new BrowserMobProxyServer(); + proxyServer.setMitmManager(mitmManager); +``` + +You can also load the root certificate and private key from separate PEM-encoded files using the `PemFileCertificateSource` class, or create an implementation of `CertificateAndKeySource` that loads the certificate and private key from another source. + +## Improving Performance with Elliptic Curve (EC) Cryptography +By default, the certificates generated by the MITM module use RSA private keys for both impersonated server certificates and for generated CA root certificates. However, all modern browsers support Elliptic Curve Cryptography, which uses smaller key sizes. As a result, impersonated EC server certificates can be generated significantly faster (approximately 50x faster is common, typically <10ms per impersonated certificate). + +Unforunately, due to a bug in Java's SSL handshake, EC keys cannot be used with RSA Certification Authorities (i.e. impersonated EC server certificates must be digitally signed by a CA's EC private key -- see https://bugs.openjdk.java.net/browse/JDK-8136442). + +The MITM module's RootCertificateGenerator can be configured to generate an EC root certificate for use with EC server certificates. If you are using your own CA root certificate and private key, make sure to generate an EC private key if you intend to use impersonated EC server certificates. + +To generate EC certificates for impersonated servers, set the `serverKeyGenerator` to `ECKeyGenerator` in ImpersonatingMitmManager. To generate an EC root certificate and private key, set the `keyGenerator` to `ECKeyGenerator` in RootCertificateGenerator: + +```java + // create a RootCertificateGenerator that generates EC Certification Authorities; you may also load your + // own EC certificate and private key using any other CertificateAndKeySource implementation + // (KeyStoreFileCertificateSource, PemFileCertificateSource, etc.). + CertificateAndKeySource rootCertificateGenerator = RootCertificateGenerator.builder() + .keyGenerator(new ECKeyGenerator()) + .build(); + + // tell the ImpersonatingMitmManager to generate EC keys and to use the EC RootCertificateGenerator + ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder() + .rootCertificateSource(rootCertificateGenerator) + .serverKeyGenerator(new ECKeyGenerator()) + .build(); + + // when using LittleProxy: + HttpProxyServerBootstrap bootstrap = DefaultHttpProxyServer.bootstrap() + .withManInTheMiddle(mitmManager); + + // when using BrowserMob Proxy: + BrowserMobProxy proxy = new BrowserMobProxyServer(); + proxy.setMitmManager(mitmManager); +``` + +## Acknowledgements +The MITM module would not have been possible without the efforts of Frank Ganske, the Zed Attack Proxy, and Brad Hill. Thank you for all your excellent work! \ No newline at end of file diff --git a/mitm/pom.xml b/mitm/pom.xml new file mode 100644 index 000000000..05152cf6e --- /dev/null +++ b/mitm/pom.xml @@ -0,0 +1,106 @@ + + + + browsermob-proxy + net.lightbody.bmp + 2.1.0-beta-4-SNAPSHOT + + 4.0.0 + + LittleProxy MITM Module + + mitm + + + + org.littleshoot + littleproxy + 1.1.0-beta1 + true + + + + org.bouncycastle + bcprov-jdk15on + + + + org.bouncycastle + bcpkix-jdk15on + + + + junit + junit + test + + + + org.mockito + mockito-core + test + + + + org.apache.logging.log4j + log4j-api + test + + + org.apache.logging.log4j + log4j-core + test + + + org.apache.logging.log4j + log4j-slf4j-impl + test + + + + com.fasterxml.jackson.core + jackson-core + test + + + + com.fasterxml.jackson.core + jackson-databind + test + + + + com.fasterxml.jackson.core + jackson-annotations + test + + + + org.hamcrest + hamcrest-library + test + + + + org.apache.httpcomponents + httpclient + + + commons-logging + commons-logging + + + test + + + + org.slf4j + jcl-over-slf4j + test + + + + + \ No newline at end of file diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/CertificateAndKey.java b/mitm/src/main/java/net/lightbody/bmp/mitm/CertificateAndKey.java new file mode 100644 index 000000000..8c5b1696d --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/CertificateAndKey.java @@ -0,0 +1,25 @@ +package net.lightbody.bmp.mitm; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +/** + * A simple container for an X.509 certificate and its corresponding private key. + */ +public class CertificateAndKey { + private final X509Certificate certificate; + private final PrivateKey privateKey; + + public CertificateAndKey(X509Certificate certificate, PrivateKey privateKey) { + this.certificate = certificate; + this.privateKey = privateKey; + } + + public X509Certificate getCertificate() { + return certificate; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/CertificateAndKeySource.java b/mitm/src/main/java/net/lightbody/bmp/mitm/CertificateAndKeySource.java new file mode 100644 index 000000000..04e901d0a --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/CertificateAndKeySource.java @@ -0,0 +1,16 @@ +package net.lightbody.bmp.mitm; + +/** + * A CertificateAndKeySource generates {@link CertificateAndKey}s, i.e. the root certificate and private key used + * to sign impersonated certificates of upstream servers. Implementations of this interface load impersonation materials + * from various sources, including Java KeyStores, JKS files, etc., or generate them on-the-fly. + */ +public interface CertificateAndKeySource { + /** + * Loads a certificate and its corresponding private key. Every time this method is called, it should return the same + * certificate and private key (although it may be a different {@link CertificateAndKey} instance). + * + * @return certificate and its corresponding private key + */ + CertificateAndKey load(); +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/CertificateInfo.java b/mitm/src/main/java/net/lightbody/bmp/mitm/CertificateInfo.java new file mode 100644 index 000000000..288e86e35 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/CertificateInfo.java @@ -0,0 +1,114 @@ +package net.lightbody.bmp.mitm; + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * Container for X.509 Certificate information. + */ +public class CertificateInfo { + private String commonName; + private String organization; + private String organizationalUnit; + + private String email; + private String locality; + private String state; + private String countryCode; + + private Date notBefore; + private Date notAfter; + + private List subjectAlternativeNames = Collections.emptyList(); + + public String getCommonName() { + return commonName; + } + + public String getOrganization() { + return organization; + } + + public String getOrganizationalUnit() { + return organizationalUnit; + } + + public Date getNotBefore() { + return notBefore; + } + + public Date getNotAfter() { + return notAfter; + } + + public String getEmail() { + return email; + } + + public String getLocality() { + return locality; + } + + public String getState() { + return state; + } + + public String getCountryCode() { + return countryCode; + } + + public List getSubjectAlternativeNames() { + return subjectAlternativeNames; + } + + public CertificateInfo commonName(String commonName) { + this.commonName = commonName; + return this; + } + + public CertificateInfo organization(String organization) { + this.organization = organization; + return this; + } + + public CertificateInfo organizationalUnit(String organizationalUnit) { + this.organizationalUnit = organizationalUnit; + return this; + } + + public CertificateInfo notBefore(Date notBefore) { + this.notBefore = notBefore; + return this; + } + + public CertificateInfo notAfter(Date notAfter) { + this.notAfter = notAfter; + return this; + } + + public CertificateInfo email(String email) { + this.email = email; + return this; + } + + public CertificateInfo locality(String locality) { + this.locality = locality; + return this; + } + + public CertificateInfo state(String state) { + this.state = state; + return this; + } + + public CertificateInfo countryCode(String countryCode) { + this.countryCode = countryCode; + return this; + } + + public CertificateInfo subjectAlternativeNames(List subjectAlternativeNames) { + this.subjectAlternativeNames = subjectAlternativeNames; + return this; + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/CertificateInfoGenerator.java b/mitm/src/main/java/net/lightbody/bmp/mitm/CertificateInfoGenerator.java new file mode 100644 index 000000000..21c064f89 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/CertificateInfoGenerator.java @@ -0,0 +1,19 @@ +package net.lightbody.bmp.mitm; + +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * A functional interface to allow customization of the certificates generated by the + * {@link net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager}. + */ +public interface CertificateInfoGenerator { + /** + * Generate a certificate for the specified hostnames, optionally using parameters from the originalCertificate. + * + * @param hostnames the hostnames to generate the certificate for, which may include wildcards + * @param originalCertificate original X.509 certificate sent by the upstream server, which may be null + * @return CertificateInfo to be used to create an X509Certificate for the specified hostnames + */ + CertificateInfo generate(List hostnames, X509Certificate originalCertificate); +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/ExistingCertificateSource.java b/mitm/src/main/java/net/lightbody/bmp/mitm/ExistingCertificateSource.java new file mode 100644 index 000000000..9ffbba28b --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/ExistingCertificateSource.java @@ -0,0 +1,31 @@ +package net.lightbody.bmp.mitm; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +/** + * A simple adapter that produces a {@link CertificateAndKey} from existing {@link X509Certificate} and {@link PrivateKey} + * java objects. + */ +public class ExistingCertificateSource implements CertificateAndKeySource { + private final X509Certificate rootCertificate; + private final PrivateKey privateKey; + + public ExistingCertificateSource(X509Certificate rootCertificate, PrivateKey privateKey) { + if (rootCertificate == null) { + throw new IllegalArgumentException("CA root certificate cannot be null"); + } + + if (privateKey == null) { + throw new IllegalArgumentException("Private key cannot be null"); + } + + this.rootCertificate = rootCertificate; + this.privateKey = privateKey; + } + + @Override + public CertificateAndKey load() { + return new CertificateAndKey(rootCertificate, privateKey); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/HostnameCertificateInfoGenerator.java b/mitm/src/main/java/net/lightbody/bmp/mitm/HostnameCertificateInfoGenerator.java new file mode 100644 index 000000000..e5ac4a35a --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/HostnameCertificateInfoGenerator.java @@ -0,0 +1,54 @@ +package net.lightbody.bmp.mitm; + +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.List; + +/** + * A {@link CertificateInfoGenerator} that uses only a hostname to populate a new {@link CertificateInfo}. The + * values in the upstream server's original X.509 certificate will be ignored. + */ +public class HostnameCertificateInfoGenerator implements CertificateInfoGenerator { + /** + * The 'O' to use for the impersonated server certificate when doing "simple" certificate impersonation (i.e. + * not copying values from actual server certificate). + */ + private static final String DEFAULT_IMPERSONATED_CERT_ORG = "Impersonated Certificate"; + + /** + * The 'O' to use for the impersonated server certificate when doing "simple" certificate impersonation. + */ + private static final String DEFAULT_IMPERSONATED_CERT_ORG_UNIT = "LittleProxy MITM"; + + @Override + public CertificateInfo generate(List hostnames, X509Certificate originalCertificate) { + if (hostnames == null || hostnames.size() < 1) { + throw new IllegalArgumentException("Cannot create X.509 certificate without server hostname"); + } + + // take the first entry as the CN + String commonName = hostnames.get(0); + + return new CertificateInfo() + .commonName(commonName) + .organization(DEFAULT_IMPERSONATED_CERT_ORG) + .organizationalUnit(DEFAULT_IMPERSONATED_CERT_ORG_UNIT) + .notBefore(getNotBefore()) + .notAfter(getNotAfter()) + .subjectAlternativeNames(hostnames); + } + + /** + * Returns the default Not Before date for impersonated certificates. Defaults to the current date minus 1 year. + */ + protected Date getNotBefore() { + return new Date(System.currentTimeMillis() - 365L * 24L * 60L * 60L * 1000L); + } + + /** + * Returns the default Not After date for impersonated certificates. Defaults to the current date plus 1 year. + */ + protected Date getNotAfter() { + return new Date(System.currentTimeMillis() + 365L * 24L * 60L * 60L * 1000L); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/KeyStoreCertificateSource.java b/mitm/src/main/java/net/lightbody/bmp/mitm/KeyStoreCertificateSource.java new file mode 100644 index 000000000..f92a4f4ec --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/KeyStoreCertificateSource.java @@ -0,0 +1,77 @@ +package net.lightbody.bmp.mitm; + +import net.lightbody.bmp.mitm.exception.CertificateSourceException; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableEntryException; +import java.security.cert.X509Certificate; + +/** + * A {@link CertificateAndKeySource} that loads the root certificate and private key from a Java KeyStore. The + * KeyStore must contain a certificate and a private key, specified by the privateKeyAlias value. The KeyStore must + * already be loaded and initialized; to load the KeyStore from a file or classpath resource, use + * {@link KeyStoreFileCertificateSource}, {@link PemFileCertificateSource}, or a custom + * implementation of {@link CertificateAndKeySource}. + */ +public class KeyStoreCertificateSource implements CertificateAndKeySource { + private final KeyStore keyStore; + private final String keyStorePassword; + private final String privateKeyAlias; + + public KeyStoreCertificateSource(KeyStore keyStore, String privateKeyAlias, String keyStorePassword) { + if (keyStore == null) { + throw new IllegalArgumentException("KeyStore cannot be null"); + } + + if (privateKeyAlias == null) { + throw new IllegalArgumentException("Private key alias cannot be null"); + } + + if (keyStorePassword == null) { + throw new IllegalArgumentException("KeyStore password cannot be null"); + } + + this.keyStore = keyStore; + this.keyStorePassword = keyStorePassword; + this.privateKeyAlias = privateKeyAlias; + } + + @Override + public CertificateAndKey load() { + try { + KeyStore.Entry entry; + try { + entry = keyStore.getEntry(privateKeyAlias, new KeyStore.PasswordProtection(keyStorePassword.toCharArray())); + } catch (UnrecoverableEntryException e) { + throw new CertificateSourceException("Unable to load private key with alias " + privateKeyAlias + " from KeyStore. Verify the KeyStore password is correct.", e); + } + + if (entry == null) { + throw new CertificateSourceException("Unable to find entry in keystore with alias: " + privateKeyAlias); + } + + if (!(entry instanceof KeyStore.PrivateKeyEntry)) { + throw new CertificateSourceException("Entry in KeyStore with alias " + privateKeyAlias + " did not contain a private key entry"); + } + + KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) entry; + + PrivateKey privateKey = privateKeyEntry.getPrivateKey(); + + if (!(privateKeyEntry.getCertificate() instanceof X509Certificate)) { + throw new CertificateSourceException("Certificate for private key in KeyStore was not an X509Certificate. Private key alias: " + privateKeyAlias + + ". Certificate type: " + (privateKeyEntry.getCertificate() != null ? privateKeyEntry.getCertificate().getClass().getName() : null)); + } + + X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate(); + + return new CertificateAndKey(x509Certificate, privateKey); + } catch (KeyStoreException | NoSuchAlgorithmException e) { + throw new CertificateSourceException("Error accessing keyStore", e); + } + } + +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/KeyStoreFileCertificateSource.java b/mitm/src/main/java/net/lightbody/bmp/mitm/KeyStoreFileCertificateSource.java new file mode 100644 index 000000000..57717230f --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/KeyStoreFileCertificateSource.java @@ -0,0 +1,146 @@ +package net.lightbody.bmp.mitm; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import net.lightbody.bmp.mitm.exception.CertificateSourceException; +import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; +import net.lightbody.bmp.mitm.tools.SecurityProviderTool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.KeyStore; + +/** + * Loads a KeyStore from a file or classpath resource. If configured with a File object, attempts to load the KeyStore + * from the specified file. Otherwise, attempts to load the KeyStore from a classpath resource. + */ +public class KeyStoreFileCertificateSource implements CertificateAndKeySource { + private static final Logger log = LoggerFactory.getLogger(KeyStoreFileCertificateSource.class); + + private final String keyStoreClasspathResource; + private final File keyStoreFile; + + private final String keyStoreType; + + private final String keyStorePassword; + private final String privateKeyAlias; + + private SecurityProviderTool securityProviderTool = new DefaultSecurityProviderTool(); + + private final Supplier certificateAndKey = Suppliers.memoize(new Supplier() { + @Override + public CertificateAndKey get() { + return loadKeyStore(); + } + }); + + /** + * Creates a {@link CertificateAndKeySource} that loads an existing {@link KeyStore} from a classpath resource. + * @param keyStoreType the KeyStore type, such as PKCS12 or JKS + * @param keyStoreClasspathResource classpath resource to load (for example, "/keystore.jks") + * @param privateKeyAlias the alias of the private key in the KeyStore + * @param keyStorePassword te KeyStore password + */ + public KeyStoreFileCertificateSource(String keyStoreType, String keyStoreClasspathResource, String privateKeyAlias, String keyStorePassword) { + if (keyStoreClasspathResource == null) { + throw new IllegalArgumentException("The classpath location of the KeyStore cannot be null"); + } + + if (keyStoreType == null) { + throw new IllegalArgumentException("KeyStore type cannot be null"); + } + + if (privateKeyAlias == null) { + throw new IllegalArgumentException("Alias of the private key in the KeyStore cannot be null"); + } + + this.keyStoreClasspathResource = keyStoreClasspathResource; + this.keyStoreFile = null; + + this.keyStoreType = keyStoreType; + this.keyStorePassword = keyStorePassword; + this.privateKeyAlias = privateKeyAlias; + } + + /** + * Creates a {@link CertificateAndKeySource} that loads an existing {@link KeyStore} from a classpath resource. + * @param keyStoreType the KeyStore type, such as PKCS12 or JKS + * @param keyStoreFile KeyStore file to load + * @param privateKeyAlias the alias of the private key in the KeyStore + * @param keyStorePassword te KeyStore password + */ + public KeyStoreFileCertificateSource(String keyStoreType, File keyStoreFile, String privateKeyAlias, String keyStorePassword) { + if (keyStoreFile == null) { + throw new IllegalArgumentException("The KeyStore file cannot be null"); + } + + if (keyStoreType == null) { + throw new IllegalArgumentException("KeyStore type cannot be null"); + } + + if (privateKeyAlias == null) { + throw new IllegalArgumentException("Alias of the private key in the KeyStore cannot be null"); + } + + this.keyStoreFile = keyStoreFile; + this.keyStoreClasspathResource = null; + + this.keyStoreType = keyStoreType; + this.keyStorePassword = keyStorePassword; + this.privateKeyAlias = privateKeyAlias; + } + + /** + * Override the default {@link SecurityProviderTool} used to load the KeyStore. + */ + public KeyStoreFileCertificateSource certificateTool(SecurityProviderTool securityProviderTool) { + this.securityProviderTool = securityProviderTool; + return this; + } + + @Override + public CertificateAndKey load() { + return certificateAndKey.get(); + + } + + /** + * Loads the {@link CertificateAndKey} from the KeyStore using the {@link SecurityProviderTool}. + */ + private CertificateAndKey loadKeyStore() { + // load the KeyStore from the file or classpath resource, then delegate to a KeyStoreCertificateSource + KeyStore keyStore; + if (keyStoreFile != null) { + keyStore = securityProviderTool.loadKeyStore(keyStoreFile, keyStoreType, keyStorePassword); + } else { + // copy the classpath resource to a temporary file and load the keystore from that temp file + Path tempKeyStoreFile = null; + try (InputStream keystoreAsStream = KeyStoreFileCertificateSource.class.getResourceAsStream(keyStoreClasspathResource)) { + tempKeyStoreFile = Files.createTempFile("keystore", "temp"); + Files.copy(keystoreAsStream, tempKeyStoreFile, StandardCopyOption.REPLACE_EXISTING); + + keyStore = securityProviderTool.loadKeyStore(tempKeyStoreFile.toFile(), keyStoreType, keyStorePassword); + } catch (IOException e) { + throw new CertificateSourceException("Unable to open KeyStore classpath resource: " + keyStoreClasspathResource, e); + } finally { + if (tempKeyStoreFile != null) { + try { + Files.deleteIfExists(tempKeyStoreFile); + } catch (IOException e) { + log.warn("Unable to delete temporary KeyStore file: {}.", tempKeyStoreFile.toAbsolutePath()); + } + } + } + } + + KeyStoreCertificateSource keyStoreCertificateSource = new KeyStoreCertificateSource(keyStore, privateKeyAlias, keyStorePassword); + + return keyStoreCertificateSource.load(); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/PemFileCertificateSource.java b/mitm/src/main/java/net/lightbody/bmp/mitm/PemFileCertificateSource.java new file mode 100644 index 000000000..6bf5253a5 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/PemFileCertificateSource.java @@ -0,0 +1,83 @@ +package net.lightbody.bmp.mitm; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; +import net.lightbody.bmp.mitm.tools.SecurityProviderTool; +import net.lightbody.bmp.mitm.util.EncryptionUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.StringReader; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +/** + * Loads impersonation materials from two separate, PEM-encoded files: a CA root certificate and its corresponding + * private key. + */ +public class PemFileCertificateSource implements CertificateAndKeySource { + private static final Logger log = LoggerFactory.getLogger(PemFileCertificateSource.class); + + private final File certificateFile; + private final File privateKeyFile; + private final String privateKeyPassword; + + private SecurityProviderTool securityProviderTool = new DefaultSecurityProviderTool(); + + private final Supplier certificateAndKey = Suppliers.memoize(new Supplier() { + @Override + public CertificateAndKey get() { + return loadCertificateAndKeyFiles(); + } + }); + + /** + * Creates a {@link CertificateAndKeySource} that loads the certificate and private key from PEM files. + * + * @param certificateFile PEM-encoded file containing the root certificate + * @param privateKeyFile PEM-encoded file continaing the certificate's private key + * @param privateKeyPassword password for the private key + */ + public PemFileCertificateSource(File certificateFile, File privateKeyFile, String privateKeyPassword) { + this.certificateFile = certificateFile; + this.privateKeyFile = privateKeyFile; + this.privateKeyPassword = privateKeyPassword; + } + + /** + * Override the default {@link SecurityProviderTool} used to load the PEM files. + */ + public PemFileCertificateSource certificateTool(SecurityProviderTool securityProviderTool) { + this.securityProviderTool = securityProviderTool; + return this; + } + + @Override + public CertificateAndKey load() { + return certificateAndKey.get(); + } + + private CertificateAndKey loadCertificateAndKeyFiles() { + if (certificateFile == null) { + throw new IllegalArgumentException("PEM root certificate file cannot be null"); + } + + if (privateKeyFile == null) { + throw new IllegalArgumentException("PEM private key file cannot be null"); + } + + if (privateKeyPassword == null) { + log.warn("Attempting to load private key from file without password. Private keys should be password-protected."); + } + + String pemEncodedCertificate = EncryptionUtil.readPemStringFromFile(certificateFile); + X509Certificate certificate = securityProviderTool.decodePemEncodedCertificate(new StringReader(pemEncodedCertificate)); + + String pemEncodedPrivateKey = EncryptionUtil.readPemStringFromFile(privateKeyFile); + PrivateKey privateKey = securityProviderTool.decodePemEncodedPrivateKey(new StringReader(pemEncodedPrivateKey), privateKeyPassword); + + return new CertificateAndKey(certificate, privateKey); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/RootCertificateGenerator.java b/mitm/src/main/java/net/lightbody/bmp/mitm/RootCertificateGenerator.java new file mode 100644 index 000000000..5f1877c4c --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/RootCertificateGenerator.java @@ -0,0 +1,259 @@ +package net.lightbody.bmp.mitm; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; +import net.lightbody.bmp.mitm.keys.KeyGenerator; +import net.lightbody.bmp.mitm.keys.RSAKeyGenerator; +import net.lightbody.bmp.mitm.tools.SecurityProviderTool; +import net.lightbody.bmp.mitm.util.EncryptionUtil; +import net.lightbody.bmp.mitm.util.MitmConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.KeyPair; +import java.security.KeyStore; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * A {@link CertificateAndKeySource} that dynamically generates a CA root certificate and private key. The certificate + * and key will only be generated once; all subsequent calls to {@link #load()} will return the same materials. To save + * the generated certificate and/or private key for installation in a browser or other client, use one of the encode + * or save methods: + *

+ */ +public class RootCertificateGenerator implements CertificateAndKeySource { + private static final Logger log = LoggerFactory.getLogger(RootCertificateGenerator.class); + + private final CertificateInfo rootCertificateInfo; + + private final String messageDigest; + + private final KeyGenerator keyGenerator; + + private final SecurityProviderTool securityProviderTool; + + /** + * The default algorithm to use when encrypting objects in PEM files (such as private keys). + */ + private static final String DEFAULT_PEM_ENCRYPTION_ALGORITHM = "AES-128-CBC"; + + /** + * The new root certificate and private key are generated only once, even across multiple calls to {@link #load()}}, + * to allow users to save the new generated root certificate for use in browsers/other HTTP clients. + */ + private final Supplier generatedCertificateAndKey = Suppliers.memoize(new Supplier() { + @Override + public CertificateAndKey get() { + return generateRootCertificate(); + } + }); + + public RootCertificateGenerator(CertificateInfo rootCertificateInfo, + String messageDigest, + KeyGenerator keyGenerator, + SecurityProviderTool securityProviderTool) { + if (rootCertificateInfo == null) { + throw new IllegalArgumentException("CA root certificate cannot be null"); + } + + if (messageDigest == null) { + throw new IllegalArgumentException("Message digest cannot be null"); + } + + if (keyGenerator == null) { + throw new IllegalArgumentException("Key generator cannot be null"); + } + + if (securityProviderTool == null) { + throw new IllegalArgumentException("Certificate tool cannot be null"); + } + + this.rootCertificateInfo = rootCertificateInfo; + this.messageDigest = messageDigest; + this.keyGenerator = keyGenerator; + this.securityProviderTool = securityProviderTool; + } + + @Override + public CertificateAndKey load() { + // only generate the materials once, so they can can be saved if desired + return generatedCertificateAndKey.get(); + } + + /** + * Generates a new CA root certificate and private key. + * + * @return new root certificate and private key + */ + private CertificateAndKey generateRootCertificate() { + long generationStart = System.currentTimeMillis(); + + // create the public and private key pair that will be used to sign the generated certificate + KeyPair caKeyPair = keyGenerator.generate(); + + // delegate the creation and signing of the X.509 certificate to the certificate tool + CertificateAndKey certificateAndKey = securityProviderTool.createCARootCertificate( + rootCertificateInfo, + caKeyPair, + messageDigest); + + long generationFinished = System.currentTimeMillis(); + + log.info("Generated CA root certificate and private key in {}ms. Key generator: {}. Signature algorithm: {}.", + generationFinished - generationStart, keyGenerator, messageDigest); + + return certificateAndKey; + } + + /** + * Returns the generated root certificate as a PEM-encoded String. + */ + public String encodeRootCertificateAsPem() { + return securityProviderTool.encodeCertificateAsPem(generatedCertificateAndKey.get().getCertificate()); + } + + /** + * Returns the generated private key as a PEM-encoded String, encrypted using the specified password and the + * {@link #DEFAULT_PEM_ENCRYPTION_ALGORITHM}. + * + * @param privateKeyPassword password to use to encrypt the private key + */ + public String encodePrivateKeyAsPem(String privateKeyPassword) { + return securityProviderTool.encodePrivateKeyAsPem(generatedCertificateAndKey.get().getPrivateKey(), privateKeyPassword, DEFAULT_PEM_ENCRYPTION_ALGORITHM); + } + + /** + * Saves the root certificate as PEM-encoded data to the specified file. + */ + public void saveRootCertificateAsPemFile(File file) { + String pemEncodedCertificate = securityProviderTool.encodeCertificateAsPem(generatedCertificateAndKey.get().getCertificate()); + + EncryptionUtil.writePemStringToFile(file, pemEncodedCertificate); + } + + /** + * Saves the private key as PEM-encoded data to a file, using the specified password to encrypt the private key and + * the {@link #DEFAULT_PEM_ENCRYPTION_ALGORITHM}. If the password is null, the private key will be stored unencrypted. + * In general, private keys should not be stored unencrypted. + * + * @param file file to save the private key to + * @param passwordForPrivateKey password to protect the private key + */ + public void savePrivateKeyAsPemFile(File file, String passwordForPrivateKey) { + String pemEncodedPrivateKey = securityProviderTool.encodePrivateKeyAsPem(generatedCertificateAndKey.get().getPrivateKey(), passwordForPrivateKey, DEFAULT_PEM_ENCRYPTION_ALGORITHM); + + EncryptionUtil.writePemStringToFile(file, pemEncodedPrivateKey); + } + + /** + * Saves the generated certificate and private key as a file, using the specified password to protect the key store. + * + * @param keyStoreType the KeyStore type, such as PKCS12 or JKS + * @param file file to export the root certificate and private key to + * @param privateKeyAlias alias for the private key in the KeyStore + * @param password password for the private key and the KeyStore + */ + public void saveRootCertificateAndKey(String keyStoreType, + File file, + String privateKeyAlias, + String password) { + CertificateAndKey certificateAndKey = generatedCertificateAndKey.get(); + + KeyStore keyStore = securityProviderTool.createRootCertificateKeyStore(keyStoreType, certificateAndKey, privateKeyAlias, password); + + securityProviderTool.saveKeyStore(file, keyStore, password); + } + + /** + * Convenience method to return a new {@link Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A Builder for {@link RootCertificateGenerator}s. Initialized with suitable default values suitable for most purposes. + */ + public static class Builder { + private CertificateInfo certificateInfo = new CertificateInfo() + .commonName(getDefaultCommonName()) + .organization("CA dynamically generated by LittleProxy") + .notBefore(new Date(System.currentTimeMillis() - 365L * 24L * 60L * 60L)) + .notAfter(new Date(System.currentTimeMillis() + 365L * 24L * 60L * 60L)); + + private KeyGenerator keyGenerator = new RSAKeyGenerator(); + + private String messageDigest = MitmConstants.DEFAULT_MESSAGE_DIGEST; + + private SecurityProviderTool securityProviderTool = new DefaultSecurityProviderTool(); + + /** + * Certificate info to use to generate the root certificate. Reasonable default values will be used if certificate + * info is not supplied. + */ + public Builder certificateInfo(CertificateInfo certificateInfo) { + this.certificateInfo = certificateInfo; + return this; + } + + /** + * The {@link KeyGenerator} that will be used to generate the root certificate's public and private keys. + */ + public Builder keyGenerator(KeyGenerator keyGenerator) { + this.keyGenerator = keyGenerator; + return this; + } + + /** + * The message digest that will be used when self-signing the root certificates. + */ + public Builder messageDigest(String messageDigest) { + this.messageDigest = messageDigest; + return this; + } + + /** + * The {@link SecurityProviderTool} implementation that will be used to generate certificates. + */ + public Builder certificateTool(SecurityProviderTool securityProviderTool) { + this.securityProviderTool = securityProviderTool; + return this; + } + + public RootCertificateGenerator build() { + return new RootCertificateGenerator(certificateInfo, messageDigest, keyGenerator, securityProviderTool); + } + } + + /** + * Creates a default CN field for a certificate, using the hostname of this machine and the current time. + */ + private static String getDefaultCommonName() { + String hostname; + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + hostname = "localhost"; + } + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz"); + + String currentDateTime = dateFormat.format(new Date()); + + String defaultCN = "Generated CA (" + hostname + ") " + currentDateTime; + + // CN fields can only be 64 characters + return defaultCN.length() <= 64 ? defaultCN : defaultCN.substring(0, 63); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/exception/CertificateCreationException.java b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/CertificateCreationException.java new file mode 100644 index 000000000..3dc7740c3 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/CertificateCreationException.java @@ -0,0 +1,23 @@ +package net.lightbody.bmp.mitm.exception; + +/** + * Indicates a problem creating a certificate (server or CA). + */ +public class CertificateCreationException extends RuntimeException { + private static final long serialVersionUID = 592999944486567944L; + + public CertificateCreationException() { + } + + public CertificateCreationException(String message) { + super(message); + } + + public CertificateCreationException(String message, Throwable cause) { + super(message, cause); + } + + public CertificateCreationException(Throwable cause) { + super(cause); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/exception/CertificateSourceException.java b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/CertificateSourceException.java new file mode 100644 index 000000000..e136d3710 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/CertificateSourceException.java @@ -0,0 +1,24 @@ +package net.lightbody.bmp.mitm.exception; + +/** + * Indicates that a {@link net.lightbody.bmp.mitm.CertificateAndKeySource} encountered an error while loading a + * certificate and/or private key from a KeyStore, PEM file, or other source. + */ +public class CertificateSourceException extends RuntimeException { + private static final long serialVersionUID = 6195838041376082083L; + + public CertificateSourceException() { + } + + public CertificateSourceException(String message) { + super(message); + } + + public CertificateSourceException(String message, Throwable cause) { + super(message, cause); + } + + public CertificateSourceException(Throwable cause) { + super(cause); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/exception/ExportException.java b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/ExportException.java new file mode 100644 index 000000000..37998c0ab --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/ExportException.java @@ -0,0 +1,23 @@ +package net.lightbody.bmp.mitm.exception; + +/** + * Indicates an error occurred while exporting/serializing a certificate, private key, KeyStore, etc. + */ +public class ExportException extends RuntimeException { + private static final long serialVersionUID = -3505301862887355206L; + + public ExportException() { + } + + public ExportException(String message) { + super(message); + } + + public ExportException(String message, Throwable cause) { + super(message, cause); + } + + public ExportException(Throwable cause) { + super(cause); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/exception/ImportException.java b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/ImportException.java new file mode 100644 index 000000000..d3f3d480b --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/ImportException.java @@ -0,0 +1,23 @@ +package net.lightbody.bmp.mitm.exception; + +/** + * Indicates that an error occurred while importing a certificate, private key, or KeyStore. + */ +public class ImportException extends RuntimeException { + private static final long serialVersionUID = 584414535648926010L; + + public ImportException() { + } + + public ImportException(String message) { + super(message); + } + + public ImportException(String message, Throwable cause) { + super(message, cause); + } + + public ImportException(Throwable cause) { + super(cause); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/exception/KeyGeneratorException.java b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/KeyGeneratorException.java new file mode 100644 index 000000000..9b97f8056 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/KeyGeneratorException.java @@ -0,0 +1,23 @@ +package net.lightbody.bmp.mitm.exception; + +/** + * Indicates an exception occurred while generating a key pair. + */ +public class KeyGeneratorException extends RuntimeException { + private static final long serialVersionUID = 7607159769324427808L; + + public KeyGeneratorException() { + } + + public KeyGeneratorException(String message) { + super(message); + } + + public KeyGeneratorException(String message, Throwable cause) { + super(message, cause); + } + + public KeyGeneratorException(Throwable cause) { + super(cause); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/exception/KeyStoreAccessException.java b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/KeyStoreAccessException.java new file mode 100644 index 000000000..17070e77b --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/KeyStoreAccessException.java @@ -0,0 +1,23 @@ +package net.lightbody.bmp.mitm.exception; + +/** + * Indicates an error occurred while accessing a java KeyStore. + */ +public class KeyStoreAccessException extends RuntimeException { + private static final long serialVersionUID = -5560417886988154298L; + + public KeyStoreAccessException() { + } + + public KeyStoreAccessException(String message) { + super(message); + } + + public KeyStoreAccessException(String message, Throwable cause) { + super(message, cause); + } + + public KeyStoreAccessException(Throwable cause) { + super(cause); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/exception/MitmException.java b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/MitmException.java new file mode 100644 index 000000000..8c8d2712c --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/MitmException.java @@ -0,0 +1,24 @@ +package net.lightbody.bmp.mitm.exception; + +/** + * Indicates a general problem occurred while attempting to man-in-the-middle communications between the client and the + * upstream server. + */ +public class MitmException extends RuntimeException { + private static final long serialVersionUID = -1960691906515767537L; + + public MitmException() { + } + + public MitmException(String message) { + super(message); + } + + public MitmException(String message, Throwable cause) { + super(message, cause); + } + + public MitmException(Throwable cause) { + super(cause); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/exception/SslContextInitializationException.java b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/SslContextInitializationException.java new file mode 100644 index 000000000..65ae9b9dd --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/exception/SslContextInitializationException.java @@ -0,0 +1,23 @@ +package net.lightbody.bmp.mitm.exception; + +/** + * Indicates an error occurred while attempting to create a new {@link javax.net.ssl.SSLContext}. + */ +public class SslContextInitializationException extends RuntimeException { + private static final long serialVersionUID = 6744059714710316821L; + + public SslContextInitializationException() { + } + + public SslContextInitializationException(String message) { + super(message); + } + + public SslContextInitializationException(String message, Throwable cause) { + super(message, cause); + } + + public SslContextInitializationException(Throwable cause) { + super(cause); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/keys/ECKeyGenerator.java b/mitm/src/main/java/net/lightbody/bmp/mitm/keys/ECKeyGenerator.java new file mode 100644 index 000000000..1a52c4b9f --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/keys/ECKeyGenerator.java @@ -0,0 +1,55 @@ +package net.lightbody.bmp.mitm.keys; + +import net.lightbody.bmp.mitm.exception.KeyGeneratorException; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.spec.ECGenParameterSpec; + +/** + * A {@link KeyGenerator} that creates Elliptic Curve key pairs. + */ +public class ECKeyGenerator implements KeyGenerator { + private static final String EC_KEY_GEN_ALGORITHM = "EC"; + + private static final String DEFAULT_NAMED_CURVE = "secp256r1"; + + private final String namedCurve; + + /** + * Create a {@link KeyGenerator} that will create EC key pairs using the secp256r1 named curve (NIST P-256) + * supported by modern web browsers. + */ + public ECKeyGenerator() { + this.namedCurve = DEFAULT_NAMED_CURVE; + } + + /** + * Create a {@link KeyGenerator} that will create EC key pairs using the specified named curve. + */ + public ECKeyGenerator(String namedCurve) { + this.namedCurve = namedCurve; + } + + @Override + public KeyPair generate() { + // obtain an EC key pair generator for the specified named curve + KeyPairGenerator generator; + try { + generator = java.security.KeyPairGenerator.getInstance(EC_KEY_GEN_ALGORITHM); + ECGenParameterSpec ecName = new ECGenParameterSpec(namedCurve); + generator.initialize(ecName); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { + throw new KeyGeneratorException("Unable to generate EC public/private key pair using named curve: " + namedCurve, e); + } + + return generator.generateKeyPair(); + } + + @Override + public String toString() { + return "EC (" + namedCurve + ")"; + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/keys/KeyGenerator.java b/mitm/src/main/java/net/lightbody/bmp/mitm/keys/KeyGenerator.java new file mode 100644 index 000000000..fcc203757 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/keys/KeyGenerator.java @@ -0,0 +1,15 @@ +package net.lightbody.bmp.mitm.keys; + +import java.security.KeyPair; + +/** + * A functional interface for key pair generators. + */ +public interface KeyGenerator { + /** + * Generates a new public/private key pair. This method should not cache or reuse any previously-generated key pairs. + * + * @return a new public/private key pair + */ + KeyPair generate(); +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/keys/RSAKeyGenerator.java b/mitm/src/main/java/net/lightbody/bmp/mitm/keys/RSAKeyGenerator.java new file mode 100644 index 000000000..a8c8aaf03 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/keys/RSAKeyGenerator.java @@ -0,0 +1,56 @@ +package net.lightbody.bmp.mitm.keys; + +import net.lightbody.bmp.mitm.exception.KeyGeneratorException; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; + +/** + * A {@link KeyGenerator} that creates RSA key pairs. + */ +public class RSAKeyGenerator implements KeyGenerator { + private static final String RSA_KEY_GEN_ALGORITHM = "RSA"; + + /** + * Use a default RSA key size of 2048, since Chrome, Firefox, and possibly other browsers have begun to distrust + * certificates signed with 1024-bit RSA keys. + */ + private static final int DEFAULT_KEY_SIZE = 2048; + + private final int keySize; + + /** + * Create a {@link KeyGenerator} that will create a 2048-bit RSA key pair. + */ + public RSAKeyGenerator() { + this.keySize = DEFAULT_KEY_SIZE; + } + + /** + * Create a {@link KeyGenerator} that will create an RSA key pair of the specified keySize. + */ + public RSAKeyGenerator(int keySize) { + this.keySize = keySize; + } + + @Override + public KeyPair generate() { + // obtain an RSA key pair generator for the specified key size + KeyPairGenerator generator; + try { + generator = KeyPairGenerator.getInstance(RSA_KEY_GEN_ALGORITHM); + generator.initialize(keySize); + } catch (NoSuchAlgorithmException e) { + throw new KeyGeneratorException("Unable to generate " + keySize + "-bit RSA public/private key pair", e); + } + + return generator.generateKeyPair(); + } + + @Override + public String toString() { + return "RSA (" + keySize + ")"; + } +} + diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/manager/ImpersonatingMitmManager.java b/mitm/src/main/java/net/lightbody/bmp/mitm/manager/ImpersonatingMitmManager.java new file mode 100644 index 000000000..d337526de --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/manager/ImpersonatingMitmManager.java @@ -0,0 +1,412 @@ +package net.lightbody.bmp.mitm.manager; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import net.lightbody.bmp.mitm.CertificateAndKey; +import net.lightbody.bmp.mitm.CertificateAndKeySource; +import net.lightbody.bmp.mitm.CertificateInfo; +import net.lightbody.bmp.mitm.CertificateInfoGenerator; +import net.lightbody.bmp.mitm.HostnameCertificateInfoGenerator; +import net.lightbody.bmp.mitm.RootCertificateGenerator; +import net.lightbody.bmp.mitm.exception.MitmException; +import net.lightbody.bmp.mitm.exception.SslContextInitializationException; +import net.lightbody.bmp.mitm.keys.KeyGenerator; +import net.lightbody.bmp.mitm.keys.RSAKeyGenerator; +import net.lightbody.bmp.mitm.stats.CertificateGenerationStatistics; +import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; +import net.lightbody.bmp.mitm.tools.SecurityProviderTool; +import net.lightbody.bmp.mitm.util.EncryptionUtil; +import net.lightbody.bmp.mitm.util.MitmConstants; +import net.lightbody.bmp.mitm.util.SslUtil; +import org.littleshoot.proxy.MitmManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +/** + * An {@link MitmManager} that will create SSLEngines for clients that present impersonated certificates for upstream servers. The impersonated + * certificates will be signed using the certificate and private key specified in an {@link #rootCertificateSource}. The impersonated server + * certificates will be created by the {@link #securityProviderTool} based on the {@link CertificateInfo} returned by the {@link #certificateInfoGenerator}. + */ +public class ImpersonatingMitmManager implements MitmManager { + private static final Logger log = LoggerFactory.getLogger(ImpersonatingMitmManager.class); + + /** + * The KeyStore password for impersonated server KeyStores. This value can be anything, since it is only used to store and immediately extract + * the Java KeyManagers after creating an impersonated server certificate. + */ + private static final String IMPERSONATED_SERVER_KEYSTORE_PASSWORD = "impersonationPassword"; + + /** + * The alias for the impersonated server certificate. This value can be anything, since it is only used to store the cert in the KeyStore. + */ + private static final String IMPERSONATED_CERTIFICATE_ALIAS = "impersonatedCertificate"; + + /** + * The SSLContext that will be used for communications with all upstream servers. This can be reused, so store it as a lazily-loaded singleton. + */ + private final Supplier upstreamServerSslContext = Suppliers.memoize(new Supplier() { + @Override + public SSLContext get() { + return SslUtil.getUpstreamServerSslContext(trustAllUpstreamServers); + } + }); + + /** + * Cache for impersonating SSLContexts. SSLContexts can be safely reused, so caching the impersonating contexts avoids + * repeatedly re-impersonating upstream servers. + */ + private final Cache sslContextCache; + + /** + * Generator used to create public and private keys for the server certificates. + */ + private final KeyGenerator serverKeyGenerator; + + /** + * The source of the CA's {@link CertificateAndKey} that will be used to sign generated server certificates. + */ + private final CertificateAndKeySource rootCertificateSource; + + /** + * The message digest used to sign the server certificate, such as SHA512. + * See https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#MessageDigest for information + * on supported message digests. + */ + private final String serverCertificateMessageDigest; + + /** + * Disables all upstream certificate validation. Should only be used during testing. + */ + private final boolean trustAllUpstreamServers; + + /** + * Utility used to generate {@link CertificateInfo} objects when impersonating an upstream server. + */ + private final CertificateInfoGenerator certificateInfoGenerator; + + /** + * Tool implementation that is used to generate, sign, and otherwise manipulate server certificates. + */ + private final SecurityProviderTool securityProviderTool; + + /** + * The CA root root certificate used to sign generated server certificates. {@link CertificateAndKeySource#load()} + * is only called once to retrieve the CA root certificate, which will be used to impersonate all server certificates. + */ + private Supplier rootCertificate = Suppliers.memoize(new Supplier() { + @Override + public CertificateAndKey get() { + return rootCertificateSource.load(); + } + }); + + /** + * Simple server certificate generation statistics. + */ + private final CertificateGenerationStatistics statistics = new CertificateGenerationStatistics(); + + /** + * Creates a new ImpersonatingMitmManager. In general, use {@link ImpersonatingMitmManager.Builder} + * to construct new instances. + */ + public ImpersonatingMitmManager(CertificateAndKeySource rootCertificateSource, + KeyGenerator serverKeyGenerator, + String serverMessageDigest, + boolean trustAllUpstreamServers, + int sslContextCacheConcurrencyLevel, + long cacheExpirationIntervalMs, + SecurityProviderTool securityProviderTool, + CertificateInfoGenerator certificateInfoGenerator) { + if (rootCertificateSource == null) { + throw new IllegalArgumentException("CA root certificate source cannot be null"); + } + + if (serverKeyGenerator == null) { + throw new IllegalArgumentException("Server key generator cannot be null"); + } + + if (serverMessageDigest == null) { + throw new IllegalArgumentException("Server certificate message digest cannot be null"); + } + + if (securityProviderTool == null) { + throw new IllegalArgumentException("The certificate tool implementation cannot be null"); + } + + if (certificateInfoGenerator == null) { + throw new IllegalArgumentException("Certificate info generator cannot be null"); + } + + this.rootCertificateSource = rootCertificateSource; + + this.trustAllUpstreamServers = trustAllUpstreamServers; + + this.serverCertificateMessageDigest = serverMessageDigest; + + this.serverKeyGenerator = serverKeyGenerator; + + this.sslContextCache = CacheBuilder.newBuilder() + .concurrencyLevel(sslContextCacheConcurrencyLevel) + .expireAfterAccess(cacheExpirationIntervalMs, TimeUnit.MILLISECONDS) + .build(); + + this.securityProviderTool = securityProviderTool; + + this.certificateInfoGenerator = certificateInfoGenerator; + } + + @Override + public SSLEngine serverSslEngine(String peerHost, int peerPort) { + try { + SSLEngine sslEngine = upstreamServerSslContext.get().createSSLEngine(peerHost, peerPort); + + // support SNI by setting the endpoint identification algorithm. this requires Java 7+. + SSLParameters sslParams = new SSLParameters(); + sslParams.setEndpointIdentificationAlgorithm("HTTPS"); + sslEngine.setSSLParameters(sslParams); + + return sslEngine; + } catch (RuntimeException e) { + throw new MitmException("Error creating SSLEngine for connection to upstream server: " + peerHost + ":" + peerPort, e); + } + } + + @Override + public SSLEngine clientSslEngineFor(SSLSession sslSession) { + try { + SSLContext ctx = getHostnameImpersonatingSslContext(sslSession); + + return ctx.createSSLEngine(); + } catch (RuntimeException e) { + throw new MitmException("Error creating SSLEngine for connection to client to impersonate upstream host: " + sslSession.getPeerHost(), e); + } + } + + /** + * Retrieves an SSLContext that impersonates the specified hostname. If an impersonating SSLContext has already been + * created for this hostname and is stored in the cache, it will be reused. Otherwise, a certificate will be created + * which impersonates the specified hostname. + * + * @param sslSession the upstream server SSLSession + * @return SSLContext which will present an impersonated certificate + */ + private SSLContext getHostnameImpersonatingSslContext(final SSLSession sslSession) { + final String hostnameToImpersonate = sslSession.getPeerHost(); + + //TODO: generate wildcard certificates, rather than one certificate per host, to reduce the number of certs generated + + try { + return sslContextCache.get(hostnameToImpersonate, new Callable() { + @Override + public SSLContext call() throws Exception { + return createImpersonatingSslContext(sslSession, hostnameToImpersonate); + } + }); + } catch (ExecutionException e) { + throw new SslContextInitializationException("An error occurred while impersonating the remote host: " + hostnameToImpersonate, e); + } + } + + /** + * Creates an SSLContext that will present an impersonated certificate for the specified hostname to the client. + * + * @param sslSession sslSession between the proxy and the upstream server + * @param hostnameToImpersonate hostname (supplied by the client's HTTP CONNECT) that will be impersonated + * @return an SSLContext presenting a certificate matching the hostnameToImpersonate + */ + private SSLContext createImpersonatingSslContext(SSLSession sslSession, String hostnameToImpersonate) { + long impersonationStart = System.currentTimeMillis(); + + // generate a Java KeyStore which contains the impersonated server certificate and the certificate's private key. + // the SSLContext will send the impersonated certificate to clients to impersonate the real upstream server, and + // will use the private key to encrypt the channel. + + // get the upstream server's certificate so the certificateInfoGenerator can (optionally) use it to construct a forged certificate + X509Certificate originalCertificate = SslUtil.getServerCertificate(sslSession); + + // get the CertificateInfo that will be used to populate the impersonated X509Certificate + CertificateInfo certificateInfo = certificateInfoGenerator.generate(Collections.singletonList(hostnameToImpersonate), originalCertificate); + + // generate a public and private key pair for the forged certificate + KeyPair serverKeyPair = serverKeyGenerator.generate(); + + // get the CA root certificate and private key that will be used to sign the forced certificate + X509Certificate caRootCertificate = rootCertificate.get().getCertificate(); + PrivateKey caPrivateKey = rootCertificate.get().getPrivateKey(); + if (caRootCertificate == null || caPrivateKey == null) { + throw new IllegalStateException("A CA root certificate and private key are required to sign a server certificate. Root certificate was: " + + caRootCertificate + ". Private key was: " + caPrivateKey); + } + + // determine if the server private key was signed with an RSA private key. though TLS no longer requires the server + // certificate to use the same private key type as the root certificate, Java bug JDK-8136442 prevents Java from creating a opening an SSL socket + // if the CA and server certificates are not of the same type. see https://bugs.openjdk.java.net/browse/JDK-8136442 + // note this only applies to RSA CAs signing EC server certificates; Java seems to properly handle EC CAs signing + // RSA server certificates. + if (EncryptionUtil.isEcKey(serverKeyPair.getPrivate()) && EncryptionUtil.isRsaKey(caPrivateKey)) { + log.warn("CA private key is an RSA key and impersonated server private key is an Elliptic Curve key. JDK bug 8136442 may prevent the proxy server from creating connections to clients due to 'no cipher suites in common'."); + } + + // create the forged server certificate and sign it with the root certificate and private key + CertificateAndKey impersonatedCertificateAndKey = securityProviderTool.createServerCertificate( + certificateInfo, + caRootCertificate, + caPrivateKey, + serverKeyPair, + serverCertificateMessageDigest); + + // bundle the newly-forged server certificate into a java KeyStore, for use by the SSLContext + KeyStore impersonatedServerKeyStore = securityProviderTool.createServerKeyStore( + MitmConstants.DEFAULT_KEYSTORE_TYPE, + impersonatedCertificateAndKey, + caRootCertificate, + IMPERSONATED_CERTIFICATE_ALIAS, IMPERSONATED_SERVER_KEYSTORE_PASSWORD + ); + + long impersonationFinish = System.currentTimeMillis(); + + statistics.certificateCreated(impersonationStart, impersonationFinish); + + log.debug("Impersonated certificate for {} in {}ms", hostnameToImpersonate, impersonationFinish - impersonationStart); + + // retrieve the Java KeyManagers that the SSLContext will use to retrieve the impersonated certificate and private key + KeyManager[] keyManagers = securityProviderTool.getKeyManagers(impersonatedServerKeyStore, IMPERSONATED_SERVER_KEYSTORE_PASSWORD); + + // create an SSLContext for this communication with the client that will present the impersonated upstream server credentials + return SslUtil.getClientSslContext(keyManagers); + } + + /** + * Returns basic certificate generation statistics for this MitmManager. + */ + public CertificateGenerationStatistics getStatistics() { + return this.statistics; + } + + /** + * Convenience method to return a new {@link Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A Builder for {@link ImpersonatingMitmManager}s. Initialized with suitable default values suitable for most purposes. + */ + public static class Builder { + private CertificateAndKeySource rootCertificateSource = RootCertificateGenerator.builder().build(); + + private KeyGenerator serverKeyGenerator = new RSAKeyGenerator(); + + private boolean trustAllServers = false; + + private int cacheConcurrencyLevel = 8; + private long cacheExpirationIntervalMs = TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES); + + private String serverMessageDigest = MitmConstants.DEFAULT_MESSAGE_DIGEST; + + private SecurityProviderTool securityProviderTool = new DefaultSecurityProviderTool(); + + private CertificateInfoGenerator certificateInfoGenerator = new HostnameCertificateInfoGenerator(); + + /** + * The source of the CA root certificate that will be used to sign the impersonated server certificates. Custom + * certificates can be used by supplying an implementation of {@link CertificateAndKeySource}, such as + * {@link net.lightbody.bmp.mitm.PemFileCertificateSource}. Alternatively, a new root certificate can be generated + * and saved (for later import into browsers) using {@link RootCertificateGenerator}. + * + * @param certificateAndKeySource impersonation materials source to use + */ + public Builder rootCertificateSource(CertificateAndKeySource certificateAndKeySource) { + this.rootCertificateSource = certificateAndKeySource; + return this; + } + + /** + * The message digest that will be used when signing server certificates with the root certificate's private key. + */ + public Builder serverMessageDigest(String serverMessageDigest) { + this.serverMessageDigest = serverMessageDigest; + return this; + } + + /** + * When true, no upstream certificate verification will be performed. This will make it possible for + * attackers to MITM communications with the upstream server, so use trustAllServers only when testing. + */ + public Builder trustAllServers(boolean trustAllServers) { + this.trustAllServers = trustAllServers; + return this; + } + + /** + * The {@link KeyGenerator} that will be used to generate the server public and private keys. + */ + public Builder serverKeyGenerator(KeyGenerator serverKeyGenerator) { + this.serverKeyGenerator = serverKeyGenerator; + return this; + } + + /** + * The concurrency level for the SSLContext cache. Increase this beyond the default value for high-volume proxy servers. + */ + public Builder cacheConcurrencyLevel(int cacheConcurrencyLevel) { + this.cacheConcurrencyLevel = cacheConcurrencyLevel; + return this; + } + + /** + * The length of time SSLContexts with forged certificates will be kept in the cache. + */ + public Builder cacheExpirationInterval(long cacheExpirationInterval, TimeUnit timeUnit) { + this.cacheExpirationIntervalMs = TimeUnit.MILLISECONDS.convert(cacheExpirationInterval, timeUnit); + return this; + } + + /** + * The {@link CertificateInfoGenerator} that will populate {@link CertificateInfo} objects containing certificate data for + * forced X509Certificates. + */ + public Builder certificateInfoGenerator(CertificateInfoGenerator certificateInfoGenerator) { + this.certificateInfoGenerator = certificateInfoGenerator; + return this; + } + + /** + * The {@link SecurityProviderTool} implementation that will be used to generate certificates. + */ + public Builder certificateTool(SecurityProviderTool securityProviderTool) { + this.securityProviderTool = securityProviderTool; + return this; + } + + public ImpersonatingMitmManager build() { + return new ImpersonatingMitmManager( + rootCertificateSource, + serverKeyGenerator, + serverMessageDigest, + trustAllServers, + cacheConcurrencyLevel, + cacheExpirationIntervalMs, + securityProviderTool, + certificateInfoGenerator + ); + } + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/stats/CertificateGenerationStatistics.java b/mitm/src/main/java/net/lightbody/bmp/mitm/stats/CertificateGenerationStatistics.java new file mode 100644 index 000000000..bb3b99e60 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/stats/CertificateGenerationStatistics.java @@ -0,0 +1,57 @@ +package net.lightbody.bmp.mitm.stats; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Tracks basic certificate generation statistics. + */ +public class CertificateGenerationStatistics { + private AtomicLong certificateGenerationTimeMs = new AtomicLong(); + private AtomicInteger certificatesGenerated = new AtomicInteger(); + + private AtomicLong firstCertificateGeneratedTimestamp = new AtomicLong(); + + /** + * Records a certificate generation that started at startTimeMs and completed at finishTimeMs. + */ + public void certificateCreated(long startTimeMs, long finishTimeMs) { + certificatesGenerated.incrementAndGet(); + certificateGenerationTimeMs.addAndGet(finishTimeMs - startTimeMs); + + // record the timestamp of the first certificate generation + firstCertificateGeneratedTimestamp.compareAndSet(0L, System.currentTimeMillis()); + } + + /** + * Returns the total number of certificates created. + */ + public int getCertificatesGenerated() { + return certificatesGenerated.get(); + } + + /** + * Returns the total number of ms spent generating all certificates. + */ + public long getTotalCertificateGenerationTimeMs() { + return certificateGenerationTimeMs.get(); + } + + /** + * Returns the average number of ms per certificate generated. + */ + public long getAvgCertificateGenerationTimeMs() { + if (certificatesGenerated.get() > 0) { + return certificateGenerationTimeMs.get() / certificatesGenerated.get(); + } else { + return 0L; + } + } + + /** + * Returns the timestamp (ms since epoch) when the first certificate was generated, or 0 if none have been generated. + */ + public long firstCertificateGeneratedTimestamp() { + return firstCertificateGeneratedTimestamp.get(); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/tools/BouncyCastleSecurityProviderTool.java b/mitm/src/main/java/net/lightbody/bmp/mitm/tools/BouncyCastleSecurityProviderTool.java new file mode 100644 index 000000000..a0488723e --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/tools/BouncyCastleSecurityProviderTool.java @@ -0,0 +1,384 @@ +package net.lightbody.bmp.mitm.tools; + +import net.lightbody.bmp.mitm.CertificateAndKey; +import net.lightbody.bmp.mitm.CertificateInfo; +import net.lightbody.bmp.mitm.exception.CertificateCreationException; +import net.lightbody.bmp.mitm.exception.ExportException; +import net.lightbody.bmp.mitm.exception.ImportException; +import net.lightbody.bmp.mitm.util.EncryptionUtil; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.bc.BcX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMDecryptorProvider; +import org.bouncycastle.openssl.PEMEncryptedKeyPair; +import org.bouncycastle.openssl.PEMEncryptor; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; +import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import javax.net.ssl.KeyManager; +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.StringWriter; +import java.math.BigInteger; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +public class BouncyCastleSecurityProviderTool implements SecurityProviderTool { + static { + Security.addProvider(new BouncyCastleProvider()); + } + + /** + * The size of certificate serial numbers, in bits. + */ + private static final int CERTIFICATE_SERIAL_NUMBER_SIZE = 160; + + @Override + public CertificateAndKey createServerCertificate(CertificateInfo certificateInfo, + X509Certificate caRootCertificate, + PrivateKey caPrivateKey, + KeyPair serverKeyPair, + String messageDigest) { + // make sure certificateInfo contains all fields necessary to generate the certificate + if (certificateInfo.getCommonName() == null) { + throw new IllegalArgumentException("Must specify CN for server certificate"); + } + + if (certificateInfo.getNotBefore() == null) { + throw new IllegalArgumentException("Must specify Not Before for server certificate"); + } + + if (certificateInfo.getNotAfter() == null) { + throw new IllegalArgumentException("Must specify Not After for server certificate"); + } + + // create the subject for the new server certificate. when impersonating an upstream server, this should contain + // the hostname of the server we are trying to impersonate in the CN field + X500Name serverCertificateSubject = createX500NameForCertificate(certificateInfo); + + // get the algorithm that will be used to sign the new certificate, which is a combination of the message digest + // and the digital signature from the CA's private key + String signatureAlgorithm = EncryptionUtil.getSignatureAlgorithm(messageDigest, caPrivateKey); + + // get a ContentSigner with our CA private key that will be used to sign the new server certificate + ContentSigner signer = getCertificateSigner(caPrivateKey, signatureAlgorithm); + + // generate a serial number for the new certificate. serial numbers only need to be unique within our + // certification authority; a large random integer will satisfy that requirement. + BigInteger serialNumber = EncryptionUtil.getRandomBigInteger(CERTIFICATE_SERIAL_NUMBER_SIZE); + + // create the X509Certificate using Bouncy Castle. the BC X509CertificateHolder can be converted to a JCA X509Certificate. + X509CertificateHolder certificateHolder; + try { + certificateHolder = new JcaX509v3CertificateBuilder(caRootCertificate, + serialNumber, + certificateInfo.getNotBefore(), + certificateInfo.getNotAfter(), + serverCertificateSubject, + serverKeyPair.getPublic()) + .addExtension(Extension.subjectAlternativeName, false, getDomainNameSANsAsASN1Encodable(certificateInfo.getSubjectAlternativeNames())) + .addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(serverKeyPair.getPublic())) + .addExtension(Extension.basicConstraints, false, new BasicConstraints(false)) + .build(signer); + } catch (CertIOException e) { + throw new CertificateCreationException("Error creating new server certificate", e); + } + + // convert the Bouncy Castle certificate holder into a JCA X509Certificate + X509Certificate serverCertificate = convertToJcaCertificate(certificateHolder); + + return new CertificateAndKey(serverCertificate, serverKeyPair.getPrivate()); + } + + @Override + public KeyStore createServerKeyStore(String keyStoreType, CertificateAndKey serverCertificateAndKey, X509Certificate rootCertificate, String privateKeyAlias, String password) { + throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method"); + } + + @Override + public KeyStore createRootCertificateKeyStore(String keyStoreType, CertificateAndKey rootCertificateAndKey, String privateKeyAlias, String password) { + throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method"); + } + + @Override + public CertificateAndKey createCARootCertificate(CertificateInfo certificateInfo, + KeyPair keyPair, + String messageDigest) { + if (certificateInfo.getNotBefore() == null) { + throw new IllegalArgumentException("Must specify Not Before for server certificate"); + } + + if (certificateInfo.getNotAfter() == null) { + throw new IllegalArgumentException("Must specify Not After for server certificate"); + } + + // create the X500Name that will be both the issuer and the subject of the new root certificate + X500Name issuer = createX500NameForCertificate(certificateInfo); + + BigInteger serial = EncryptionUtil.getRandomBigInteger(CERTIFICATE_SERIAL_NUMBER_SIZE); + + PublicKey rootCertificatePublicKey = keyPair.getPublic(); + + String signatureAlgorithm = EncryptionUtil.getSignatureAlgorithm(messageDigest, keyPair.getPrivate()); + + // this is a CA root certificate, so it is self-signed + ContentSigner selfSigner = getCertificateSigner(keyPair.getPrivate(), signatureAlgorithm); + + ASN1EncodableVector extendedKeyUsages = new ASN1EncodableVector(); + extendedKeyUsages.add(KeyPurposeId.id_kp_serverAuth); + extendedKeyUsages.add(KeyPurposeId.id_kp_clientAuth); + extendedKeyUsages.add(KeyPurposeId.anyExtendedKeyUsage); + + X509CertificateHolder certificateHolder; + try { + certificateHolder = new JcaX509v3CertificateBuilder( + issuer, + serial, + certificateInfo.getNotBefore(), + certificateInfo.getNotAfter(), + issuer, + rootCertificatePublicKey) + .addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(rootCertificatePublicKey)) + .addExtension(Extension.basicConstraints, true, new BasicConstraints(true)) + .addExtension(Extension.keyUsage, false, new KeyUsage( + KeyUsage.keyCertSign + | KeyUsage.digitalSignature + | KeyUsage.keyEncipherment + | KeyUsage.dataEncipherment + | KeyUsage.cRLSign)) + .addExtension(Extension.extendedKeyUsage, false, new DERSequence(extendedKeyUsages)) + .build(selfSigner); + } catch (CertIOException e) { + throw new CertificateCreationException("Error creating root certificate", e); + } + + // convert the Bouncy Castle X590CertificateHolder to a JCA cert + X509Certificate cert = convertToJcaCertificate(certificateHolder); + + return new CertificateAndKey(cert, keyPair.getPrivate()); + } + + @Override + public String encodePrivateKeyAsPem(PrivateKey privateKey, String passwordForPrivateKey, String encryptionAlgorithm) { + if (passwordForPrivateKey == null) { + throw new IllegalArgumentException("You must specify a password when serializing a private key"); + } + + PEMEncryptor encryptor = new JcePEMEncryptorBuilder(encryptionAlgorithm) + .build(passwordForPrivateKey.toCharArray()); + + return encodeObjectAsPemString(privateKey, encryptor); + } + + @Override + public String encodeCertificateAsPem(Certificate certificate) { + return encodeObjectAsPemString(certificate, null); + } + + @Override + public PrivateKey decodePemEncodedPrivateKey(Reader privateKeyReader, String password) { + try (PEMParser pemParser = new PEMParser(privateKeyReader)) { + Object keyPair = pemParser.readObject(); + + // retrieve the PrivateKeyInfo from the returned keyPair object. if the key is encrypted, it needs to be + // decrypted using the specified password first. + PrivateKeyInfo keyInfo; + if (keyPair instanceof PEMEncryptedKeyPair) { + if (password == null) { + throw new ImportException("Unable to import private key. Key is encrypted, but no password was provided."); + } + + PEMDecryptorProvider decryptor = new JcePEMDecryptorProviderBuilder().build(password.toCharArray()); + + PEMKeyPair decryptedKeyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptor); + + keyInfo = decryptedKeyPair.getPrivateKeyInfo(); + } else { + keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo(); + } + + return new JcaPEMKeyConverter().getPrivateKey(keyInfo); + } catch (IOException e) { + throw new ImportException("Unable to read PEM-encoded PrivateKey", e); + } + } + + @Override + public X509Certificate decodePemEncodedCertificate(Reader certificateReader) { + // JCA provides this functionality already, but it can be easily implemented using BC as well + throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method"); + } + + @Override + public KeyStore loadKeyStore(File file, String keyStoreType, String password) { + throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method"); + } + + @Override + public void saveKeyStore(File file, KeyStore keyStore, String keystorePassword) { + throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method"); + } + + @Override + public KeyManager[] getKeyManagers(KeyStore keyStore, String keyStorePassword) { + return new KeyManager[0]; + } + + + /** + * Creates an X500Name based on the specified certificateInfo. + * + * @param certificateInfo information to populate the X500Name with + * @return a new X500Name object for use as a subject or issuer + */ + private static X500Name createX500NameForCertificate(CertificateInfo certificateInfo) { + X500NameBuilder x500NameBuilder = new X500NameBuilder(BCStyle.INSTANCE); + + if (certificateInfo.getCommonName() != null) { + x500NameBuilder.addRDN(BCStyle.CN, certificateInfo.getCommonName()); + } + + if (certificateInfo.getOrganization() != null) { + x500NameBuilder.addRDN(BCStyle.O, certificateInfo.getOrganization()); + } + + if (certificateInfo.getOrganizationalUnit() != null) { + x500NameBuilder.addRDN(BCStyle.OU, certificateInfo.getOrganizationalUnit()); + } + + if (certificateInfo.getEmail() != null) { + x500NameBuilder.addRDN(BCStyle.E, certificateInfo.getEmail()); + } + + if (certificateInfo.getLocality() != null) { + x500NameBuilder.addRDN(BCStyle.L, certificateInfo.getLocality()); + } + + if (certificateInfo.getState() != null) { + x500NameBuilder.addRDN(BCStyle.ST, certificateInfo.getState()); + } + + if (certificateInfo.getCountryCode() != null) { + x500NameBuilder.addRDN(BCStyle.C, certificateInfo.getCountryCode()); + } + + // TODO: Add more X.509 certificate fields as needed + + return x500NameBuilder.build(); + } + + /** + * Converts a list of domain name Subject Alternative Names into ASN1Encodable GeneralNames objects, for use with + * the Bouncy Castle certificate builder. + * + * @param subjectAlternativeNames domain name SANs to convert + * @return a GeneralNames instance that includes the specifie dsubjectAlternativeNames as DNS name fields + */ + private static GeneralNames getDomainNameSANsAsASN1Encodable(List subjectAlternativeNames) { + List encodedSANs = new ArrayList<>(subjectAlternativeNames.size()); + for (String subjectAlternativeName : subjectAlternativeNames) { + GeneralName generalName = new GeneralName(GeneralName.dNSName, subjectAlternativeName); + encodedSANs.add(generalName); + } + + return new GeneralNames(encodedSANs.toArray(new GeneralName[encodedSANs.size()])); + } + + /** + * Creates a ContentSigner that can be used to sign certificates with the given private key and signature algorithm. + * + * @param certAuthorityPrivateKey the private key to use to sign certificates + * @param signatureAlgorithm the algorithm to use to sign certificates + * @return a ContentSigner + */ + private static ContentSigner getCertificateSigner(PrivateKey certAuthorityPrivateKey, String signatureAlgorithm) { + try { + return new JcaContentSignerBuilder(signatureAlgorithm) + .build(certAuthorityPrivateKey); + } catch (OperatorCreationException e) { + throw new CertificateCreationException("Unable to create ContentSigner using signature algorithm: " + signatureAlgorithm, e); + } + } + + /** + * Converts a Bouncy Castle X509CertificateHolder into a JCA X590Certificate. + * + * @param bouncyCastleCertificate BC X509CertificateHolder + * @return JCA X509Certificate + */ + private static X509Certificate convertToJcaCertificate(X509CertificateHolder bouncyCastleCertificate) { + try { + return new JcaX509CertificateConverter() + .getCertificate(bouncyCastleCertificate); + } catch (CertificateException e) { + throw new CertificateCreationException("Unable to convert X590CertificateHolder to JCA X590Certificate", e); + } + } + + /** + * Creates the SubjectKeyIdentifier for a Bouncy Castle X590CertificateHolder. + * + * @param key public key to identify + * @return SubjectKeyIdentifier for the specified key + */ + private static SubjectKeyIdentifier createSubjectKeyIdentifier(Key key) { + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(key.getEncoded()); + + return new BcX509ExtensionUtils().createSubjectKeyIdentifier(publicKeyInfo); + } + + /** + * Encodes the specified security object in PEM format, using the specified encryptor. If the encryptor is null, + * the object will not be encrypted in the generated String. + * + * @param object object to encrypt (certificate, private key, etc.) + * @param encryptor engine to encrypt the resulting PEM String, or null if no encryption should be used + * @return a PEM-encoded String + */ + private static String encodeObjectAsPemString(Object object, PEMEncryptor encryptor) { + StringWriter stringWriter = new StringWriter(); + + try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { + pemWriter.writeObject(object, encryptor); + pemWriter.flush(); + } catch (IOException e) { + throw new ExportException("Unable to generate PEM string representing object", e); + } + + return stringWriter.toString(); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/tools/DefaultSecurityProviderTool.java b/mitm/src/main/java/net/lightbody/bmp/mitm/tools/DefaultSecurityProviderTool.java new file mode 100644 index 000000000..501f5a6c0 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/tools/DefaultSecurityProviderTool.java @@ -0,0 +1,166 @@ +package net.lightbody.bmp.mitm.tools; + +import com.google.common.io.CharStreams; +import net.lightbody.bmp.mitm.CertificateAndKey; +import net.lightbody.bmp.mitm.CertificateInfo; +import net.lightbody.bmp.mitm.exception.ImportException; +import net.lightbody.bmp.mitm.exception.KeyStoreAccessException; +import net.lightbody.bmp.mitm.util.KeyStoreUtil; + +import javax.net.ssl.KeyManager; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +/** + * A {@link SecurityProviderTool} implementation that uses the default system Security provider where possible, but uses the + * Bouncy Castle provider for operations that the JCA does not provide or implement (e.g. certificate generation and signing). + */ +public class DefaultSecurityProviderTool implements SecurityProviderTool { + private final SecurityProviderTool bouncyCastle = new BouncyCastleSecurityProviderTool(); + + @Override + public CertificateAndKey createCARootCertificate(CertificateInfo certificateInfo, KeyPair keyPair, String messageDigest) { + return bouncyCastle.createCARootCertificate(certificateInfo, keyPair, messageDigest); + } + + @Override + public CertificateAndKey createServerCertificate(CertificateInfo certificateInfo, X509Certificate caRootCertificate, PrivateKey caPrivateKey, KeyPair serverKeyPair, String messageDigest) { + return bouncyCastle.createServerCertificate(certificateInfo, caRootCertificate, caPrivateKey, serverKeyPair, messageDigest); + } + + @Override + public KeyStore createServerKeyStore(String keyStoreType, + CertificateAndKey serverCertificateAndKey, + X509Certificate rootCertificate, + String privateKeyAlias, + String password) { + if (password == null) { + throw new IllegalArgumentException("KeyStore password cannot be null"); + } + + if (privateKeyAlias == null) { + throw new IllegalArgumentException("Private key alias cannot be null"); + } + + // create a KeyStore containing the impersonated certificate's private key and a certificate chain with the + // impersonated cert and our root certificate + KeyStore impersonatedCertificateKeyStore = KeyStoreUtil.createEmptyKeyStore(keyStoreType, null); + + // create the certificate chain back for the impersonated certificate back to the root certificate + Certificate[] chain = {serverCertificateAndKey.getCertificate(), rootCertificate}; + + try { + // place the impersonated certificate and its private key in the KeyStore + impersonatedCertificateKeyStore.setKeyEntry(privateKeyAlias, serverCertificateAndKey.getPrivateKey(), password.toCharArray(), chain); + } catch (KeyStoreException e) { + throw new KeyStoreAccessException("Error storing impersonated certificate and private key in KeyStore", e); + } + + return impersonatedCertificateKeyStore; + } + + @Override + public KeyStore createRootCertificateKeyStore(String keyStoreType, CertificateAndKey rootCertificateAndKey, String privateKeyAlias, String password) { + return KeyStoreUtil.createRootCertificateKeyStore(keyStoreType, rootCertificateAndKey.getCertificate(), privateKeyAlias, rootCertificateAndKey.getPrivateKey(), password, null); + } + + @Override + public String encodePrivateKeyAsPem(PrivateKey privateKey, String passwordForPrivateKey, String encryptionAlgorithm) { + return bouncyCastle.encodePrivateKeyAsPem(privateKey, passwordForPrivateKey, encryptionAlgorithm); + } + + @Override + public String encodeCertificateAsPem(Certificate certificate) { + return bouncyCastle.encodeCertificateAsPem(certificate); + } + + @Override + public PrivateKey decodePemEncodedPrivateKey(Reader privateKeyReader, String password) { + return bouncyCastle.decodePemEncodedPrivateKey(privateKeyReader, password); + } + + @Override + public X509Certificate decodePemEncodedCertificate(Reader certificateReader) { + // JCA supports reading PEM-encoded X509Certificates fairly easily, so there is no need to use BC to read the cert + Certificate certificate; + + // the JCA CertificateFactory takes an InputStream, so convert the reader to a stream first. converting to a String first + // is not ideal, but is relatively straightforward. (PEM certificates should only contain US_ASCII-compatible characters.) + try (InputStream certificateAsStream = new ByteArrayInputStream(CharStreams.toString(certificateReader).getBytes(StandardCharsets.US_ASCII))) { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + certificate = certificateFactory.generateCertificate(certificateAsStream); + } catch (CertificateException | IOException e) { + throw new ImportException("Unable to read PEM-encoded X509Certificate", e); + } + + if (!(certificate instanceof X509Certificate)) { + throw new ImportException("Attempted to import non-X.509 certificate as X.509 certificate"); + } + + return (X509Certificate) certificate; + } + + /** + * Loads the KeyStore from the specified InputStream. The InputStream is not closed after the KeyStore has been read. + * + * @param file file containing a KeyStore + * @param keyStoreType KeyStore type, such as "JKS" or "PKCS12" + * @param password password of the KeyStore + * @return KeyStore loaded from the input stream + */ + @Override + public KeyStore loadKeyStore(File file, String keyStoreType, String password) { + KeyStore keyStore; + try { + keyStore = KeyStore.getInstance(keyStoreType); + } catch (KeyStoreException e) { + throw new KeyStoreAccessException("Unable to get KeyStore instance of type: " + keyStoreType, e); + } + + try (InputStream keystoreAsStream = new FileInputStream(file)) { + keyStore.load(keystoreAsStream, password.toCharArray()); + } catch (IOException e) { + throw new ImportException("Unable to read KeyStore from file: " + file.getName(), e); + } catch (CertificateException | NoSuchAlgorithmException e) { + throw new ImportException("Error while reading KeyStore", e); + } + + return keyStore; + } + + /** + * Exports the keyStore to the specified file. + * + * @param file file to save the KeyStore to + * @param keyStore KeyStore to export + * @param keystorePassword the password for the KeyStore + */ + @Override + public void saveKeyStore(File file, KeyStore keyStore, String keystorePassword) { + try (FileOutputStream fos = new FileOutputStream(file)) { + keyStore.store(fos, keystorePassword.toCharArray()); + } catch (CertificateException | NoSuchAlgorithmException | IOException | KeyStoreException e) { + throw new KeyStoreAccessException("Unable to save KeyStore to file: " + file.getName(), e); + } + } + + @Override + public KeyManager[] getKeyManagers(KeyStore keyStore, String keyStorePassword) { + return KeyStoreUtil.getKeyManagers(keyStore, keyStorePassword, null, null); + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/tools/SecurityProviderTool.java b/mitm/src/main/java/net/lightbody/bmp/mitm/tools/SecurityProviderTool.java new file mode 100644 index 000000000..8b6df2a87 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/tools/SecurityProviderTool.java @@ -0,0 +1,142 @@ +package net.lightbody.bmp.mitm.tools; + +import net.lightbody.bmp.mitm.CertificateAndKey; +import net.lightbody.bmp.mitm.CertificateInfo; + +import javax.net.ssl.KeyManager; +import java.io.File; +import java.io.Reader; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +/** + * Generic interface for functionality provided by a Security Provider. + */ +public interface SecurityProviderTool { + /** + * Creates a new self-signed CA root certificate, suitable for use signing new server certificates. + * + * @param certificateInfo certificate info to populate in the new root cert + * @param keyPair root certificate's public and private keys + * @param messageDigest digest to use when signing the new root certificate, such as SHA512 + * @return a new root certificate and private key + */ + CertificateAndKey createCARootCertificate(CertificateInfo certificateInfo, + KeyPair keyPair, + String messageDigest); + + /** + * Creates a new server X.509 certificate using the serverKeyPair. The new certificate will be populated with + * information from the specified certificateInfo and will be signed using the specified caPrivateKey and messageDigest. + * + * @param certificateInfo basic X.509 certificate info that will be used to create the server certificate + * @param caRootCertificate root certificate that will be used to populate the issuer field of the server certificate + * @param serverKeyPair server's public and private keys + * @param messageDigest message digest to use when signing the server certificate, such as SHA512 + * @param caPrivateKey root certificate private key that will be used to sign the server certificate + * @return a new server certificate and its private key + */ + CertificateAndKey createServerCertificate(CertificateInfo certificateInfo, + X509Certificate caRootCertificate, + PrivateKey caPrivateKey, + KeyPair serverKeyPair, + String messageDigest); + + /** + * Assembles a Java KeyStore containing a server's certificate, private key, and the certificate authority's certificate, + * which can be used to create an {@link javax.net.ssl.SSLContext}. + * + * @param keyStoreType the KeyStore type, such as JKS or PKCS12 + * @param serverCertificateAndKey certificate and private key for the server, which will be placed in the KeyStore + * @param rootCertificate CA root certificate of the private key that signed the server certificate + * @param privateKeyAlias alias to assign the private key (with accompanying certificate chain) to in the KeyStore + * @param password password for the new KeyStore and private key + * @return a new KeyStore with the server's certificate and password-protected private key + */ + KeyStore createServerKeyStore(String keyStoreType, + CertificateAndKey serverCertificateAndKey, + X509Certificate rootCertificate, + String privateKeyAlias, + String password); + + /** + * Assembles a Java KeyStore containing a CA root certificate and its private key. + * + * @param keyStoreType the KeyStore type, such as JKS or PKCS12 + * @param rootCertificateAndKey certification authority's root certificate and private key, which will be placed in the KeyStore + * @param privateKeyAlias alias to assign the private key (with accompanying certificate chain) to in the KeyStore + * @param password password for the new KeyStore and private key + * @return a new KeyStore with the root certificate and password-protected private key + */ + KeyStore createRootCertificateKeyStore(String keyStoreType, + CertificateAndKey rootCertificateAndKey, + String privateKeyAlias, + String password); + + /** + * Encodes a private key in PEM format, encrypting it with the specified password. The private key will be encrypted + * using the specified algorithm. + * + * @param privateKey private key to encode + * @param passwordForPrivateKey password to protect the private key + * @param encryptionAlgorithm algorithm to use to encrypt the private key + * @return PEM-encoded private key as a String + */ + String encodePrivateKeyAsPem(PrivateKey privateKey, String passwordForPrivateKey, String encryptionAlgorithm); + + /** + * Encodes a certificate in PEM format. + * + * @param certificate certificate to encode + * @return PEM-encoded certificate as a String + */ + String encodeCertificateAsPem(Certificate certificate); + + /** + * Decodes a PEM-encoded private key into a {@link PrivateKey}. The password may be null if the PEM-encoded private key + * is not password-encrypted. + * + * @param privateKeyReader a reader for a PEM-encoded private key + * @param password password protecting the private key @return the decoded private key + */ + PrivateKey decodePemEncodedPrivateKey(Reader privateKeyReader, String password); + + /** + * Decodes a PEM-encoded X.509 Certificate into a {@link X509Certificate}. + * + * @param certificateReader a reader for a PEM-encoded certificate + * @return the decoded X.509 certificate + */ + X509Certificate decodePemEncodedCertificate(Reader certificateReader); + + /** + * Loads a Java KeyStore object from a file. + * + * @param file KeyStore file to load + * @param keyStoreType KeyStore type (PKCS12, JKS, etc.) + * @param password the KeyStore password + * @return an initialized Java KeyStore object + */ + KeyStore loadKeyStore(File file, String keyStoreType, String password); + + /** + * Saves a Java KeyStore to a file, protecting it with the specified password. + * + * @param file file to save the KeyStore to + * @param keyStore KeyStore to save + * @param keystorePassword password for the KeyStore + */ + void saveKeyStore(File file, KeyStore keyStore, String keystorePassword); + + /** + * Retrieve the KeyManagers for the specified KeyStore. + * + * @param keyStore the KeyStore to retrieve KeyManagers from + * @param keyStorePassword the KeyStore password + * @return KeyManagers for the specified KeyStore + */ + KeyManager[] getKeyManagers(KeyStore keyStore, String keyStorePassword); +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/util/EncryptionUtil.java b/mitm/src/main/java/net/lightbody/bmp/mitm/util/EncryptionUtil.java new file mode 100644 index 000000000..751aade07 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/util/EncryptionUtil.java @@ -0,0 +1,110 @@ +package net.lightbody.bmp.mitm.util; + +import net.lightbody.bmp.mitm.exception.ExportException; +import net.lightbody.bmp.mitm.exception.ImportException; + +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.Key; +import java.security.interfaces.DSAKey; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAKey; +import java.util.Random; + +/** + * A collection of simple JCA-related utilities. + */ +public class EncryptionUtil { + /** + * Creates a signature algorithm string using the specified message digest and the encryption type corresponding + * to the supplied signingKey. Useful when generating the signature algorithm to be used to sign server certificates + * using the CA root certificate's signingKey. + *

+ * For example, if the root certificate has an RSA private key, and you + * wish to use the SHA256 message digest, this method will return the string "SHA256withRSA". See the + * "Signature Algorithms" section of http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html + * for a list of JSSE-supported signature algorithms. + * + * @param messageDigest digest to use to sign the certificate, such as SHA512 + * @param signingKey private key that will be used to sign the certificate + * @return a JCA-compatible signature algorithm + */ + public static String getSignatureAlgorithm(String messageDigest, Key signingKey) { + return messageDigest + "with" + getDigitalSignatureType(signingKey); + } + + /** + * Returns the type of digital signature used with the specified signing key. + * + * @param signingKey private key that will be used to sign a certificate (or something else) + * @return a string representing the digital signature type (ECDSA, RSA, etc.) + */ + public static String getDigitalSignatureType(Key signingKey) { + if (signingKey instanceof ECKey) { + return "ECDSA"; + } else if (signingKey instanceof RSAKey) { + return "RSA"; + } else if (signingKey instanceof DSAKey) { + return "DSA"; + } else { + throw new IllegalArgumentException("Cannot determine digital signature encryption type for unknown key type: " + signingKey.getClass().getCanonicalName()); + } + + } + + /** + * Creates a random BigInteger greater than 0 with the specified number of bits. + * + * @param bits number of bits to generate + * @return random BigInteger + */ + public static BigInteger getRandomBigInteger(int bits) { + return new BigInteger(bits, new Random()); + } + + /** + * Returns true if the key is an RSA public or private key. + */ + public static boolean isRsaKey(Key key) { + return "RSA".equals(key.getAlgorithm()); + } + + /** + * Returns true if the key is an elliptic curve public or private key. + */ + public static boolean isEcKey(Key key) { + return "EC".equals(key.getAlgorithm()); + } + + /** + * Convenience method to write PEM data to a file. The file will be encoded in the US_ASCII character set. + * + * @param file file to write to + * @param pemDataToWrite PEM data to write to the file + */ + public static void writePemStringToFile(File file, String pemDataToWrite) { + try { + Files.write(file.toPath(), pemDataToWrite.getBytes(StandardCharsets.US_ASCII)); + } catch (IOException e) { + throw new ExportException("Unable to write PEM string to file: " + file.getName(), e); + } + } + + /** + * Convenience method to read PEM data from a file. The file encoding must be US_ASCII. + * + * @param file file to read from + * @return PEM data from file + */ + public static String readPemStringFromFile(File file) { + try { + byte[] fileContents = Files.readAllBytes(file.toPath()); + return new String(fileContents, StandardCharsets.US_ASCII); + } catch (IOException e) { + throw new ImportException("Unable to read PEM-encoded data from file: " + file.getName()); + } + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/util/KeyStoreUtil.java b/mitm/src/main/java/net/lightbody/bmp/mitm/util/KeyStoreUtil.java new file mode 100644 index 000000000..7edfc47c9 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/util/KeyStoreUtil.java @@ -0,0 +1,103 @@ +package net.lightbody.bmp.mitm.util; + +import net.lightbody.bmp.mitm.exception.KeyStoreAccessException; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * Utility for loading, saving, and manipulating {@link KeyStore}s. + */ +public class KeyStoreUtil { + /** + * Creates and initializes an empty KeyStore using the specified keyStoreType. + * + * @param keyStoreType type of key store to initialize, or null to use the system default + * @param provider JCA provider to use, or null to use the system default + * @return a new KeyStore + */ + public static KeyStore createEmptyKeyStore(String keyStoreType, String provider) { + if (keyStoreType == null) { + keyStoreType = KeyStore.getDefaultType(); + } + + KeyStore keyStore; + try { + if (provider == null) { + keyStore = KeyStore.getInstance(keyStoreType); + } else { + keyStore = KeyStore.getInstance(keyStoreType, provider); + } + keyStore.load(null, null); + } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | NoSuchProviderException | IOException e) { + throw new KeyStoreAccessException("Error creating or initializing new KeyStore of type: " + keyStoreType, e); + } + return keyStore; + } + + /** + * Creates a new KeyStore containing the specified root certificate and private key. + * + * @param keyStoreType type of the generated KeyStore, such as PKCS12 or JKS + * @param certificate root certificate to add to the KeyStore + * @param privateKeyAlias alias for the private key in the KeyStore + * @param privateKey private key to add to the KeyStore + * @param privateKeyPassword password for the private key + * @param provider JCA provider to use, or null to use the system default + * @return new KeyStore containing the root certificate and private key + */ + public static KeyStore createRootCertificateKeyStore(String keyStoreType, X509Certificate certificate, String privateKeyAlias, PrivateKey privateKey, String privateKeyPassword, String provider) { + if (privateKeyPassword == null) { + throw new IllegalArgumentException("Must specify a KeyStore password"); + } + + KeyStore newKeyStore = KeyStoreUtil.createEmptyKeyStore(keyStoreType, provider); + + try { + newKeyStore.setKeyEntry(privateKeyAlias, privateKey, privateKeyPassword.toCharArray(), new Certificate[]{certificate}); + } catch (KeyStoreException e) { + throw new KeyStoreAccessException("Unable to store certificate and private key in KeyStore", e); + } + return newKeyStore; + } + + /** + * Retrieve the KeyManagers for the specified KeyStore. + * + * @param keyStore the KeyStore to retrieve KeyManagers from + * @param keyStorePassword the KeyStore password + * @param keyManagerAlgorithm key manager algorithm to use, or null to use the system default + * @param provider JCA provider to use, or null to use the system default + * @return KeyManagers for the specified KeyStore + */ + public static KeyManager[] getKeyManagers(KeyStore keyStore, String keyStorePassword, String keyManagerAlgorithm, String provider) { + if (keyManagerAlgorithm == null) { + keyManagerAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); + } + + try { + KeyManagerFactory kmf; + if (provider == null) { + kmf = KeyManagerFactory.getInstance(keyManagerAlgorithm); + } else { + kmf = KeyManagerFactory.getInstance(keyManagerAlgorithm, provider); + } + + kmf.init(keyStore, keyStorePassword.toCharArray()); + + return kmf.getKeyManagers(); + } catch (NoSuchAlgorithmException | UnrecoverableKeyException | KeyStoreException | NoSuchProviderException e) { + throw new KeyStoreAccessException("Unable to get KeyManagers for KeyStore", e); + } + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/util/MitmConstants.java b/mitm/src/main/java/net/lightbody/bmp/mitm/util/MitmConstants.java new file mode 100644 index 000000000..85f024bd7 --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/util/MitmConstants.java @@ -0,0 +1,33 @@ +package net.lightbody.bmp.mitm.util; + +/** + * Default values for basic MITM properties. + */ +public class MitmConstants { + /** + * The default message digest to use when signing certificates (CA or server). On 64-bit systems this is set to + * SHA512, on 32-bit systems this is SHA256. On 64-bit systems, SHA512 generally performs better than SHA256; see + * this question for details: http://crypto.stackexchange.com/questions/26336/sha512-faster-than-sha256 + */ + public static final String DEFAULT_MESSAGE_DIGEST = is32BitJvm() ? "SHA256": "SHA512"; + + /** + * The default {@link java.security.KeyStore} type to use when creating KeyStores (e.g. for impersonated server + * certificates). PKCS12 is widely supported. + */ + public static final String DEFAULT_KEYSTORE_TYPE = "PKCS12"; + + /** + * Uses the non-portable system property sun.arch.data.model to help determine if we are running on a 32-bit JVM. + * Since the majority of modern systems are 64 bits, this method "assumes" 64 bits and only returns true if + * sun.arch.data.model explicitly indicates a 32-bit JVM. + * + * @return true if we can determine definitively that this is a 32-bit JVM, otherwise false + */ + private static boolean is32BitJvm() { + Integer bits = Integer.getInteger("sun.arch.data.model"); + + return bits != null && bits == 32; + + } +} diff --git a/mitm/src/main/java/net/lightbody/bmp/mitm/util/SslUtil.java b/mitm/src/main/java/net/lightbody/bmp/mitm/util/SslUtil.java new file mode 100644 index 000000000..14e7fe6ef --- /dev/null +++ b/mitm/src/main/java/net/lightbody/bmp/mitm/util/SslUtil.java @@ -0,0 +1,98 @@ +package net.lightbody.bmp.mitm.util; + +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import net.lightbody.bmp.mitm.exception.SslContextInitializationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +/** + * Utility for creating SSLContexts. + */ +public class SslUtil { + private static final Logger log = LoggerFactory.getLogger(SslUtil.class); + + /** + * Creates an SSLContext for use when connecting to upstream servers. When trustAllServers is true, no upstream certificate + * verification will be performed. This will make it possible for attackers to MITM communications with the upstream + * server, so use trustAllServers only when testing. + * + * @param trustAllServers when true, no upstream server certificate validation will be performed + * @return an SSLContext to connect to upstream servers with + */ + public static SSLContext getUpstreamServerSslContext(boolean trustAllServers) { + //TODO: add the ability to specify an explicit additional trust source, so clients don't need to import trust into the JDK trust source or forgo trust entirely + + try { + if (trustAllServers) { + log.warn("Disabling upstream server certificate verification. This will allow attackers to intercept communications with upstream servers."); + + TrustManager[] trustManagers = InsecureTrustManagerFactory.INSTANCE.getTrustManagers(); + + // start with the default SSL context, but override the default TrustManager with the "always trust everything" TrustManager + SSLContext newSslContext = SSLContext.getInstance("TLS"); + newSslContext.init(null, trustManagers, null); + + return newSslContext; + } else { + return SSLContext.getDefault(); + } + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new SslContextInitializationException("Error creating new SSL context for connection to upstream server", e); + } + } + + /** + * Creates an SSLContext for use with clients' connections to this server. The specified keyManagers should contain + * the impersonated server certificate and private key used to encrypt communications with the client. + * + * @param keyManagers keyManagers that will be used to encrypt communications with the client; should contain the impersonated upstream server certificate + * @return SSLContext for use with clients' connections to this server + */ + public static SSLContext getClientSslContext(KeyManager[] keyManagers) { + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagers, null, null); + + return sslContext; + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new SslContextInitializationException("Error creating new SSL context for connection to client", e); + } + + + } + + /** + * Returns the X509Certificate for the server this session is connected to. The certificate may be null. + * + * @param sslSession SSL session connected to upstream server + * @return the X.509 certificate from the upstream server, or null if no certificate is available + */ + public static X509Certificate getServerCertificate(SSLSession sslSession) { + Certificate[] peerCertificates; + try { + peerCertificates = sslSession.getPeerCertificates(); + } catch (SSLPeerUnverifiedException e) { + peerCertificates = null; + } + + if (peerCertificates != null && peerCertificates.length > 0) { + Certificate peerCertificate = peerCertificates[0]; + if (peerCertificate != null && peerCertificate instanceof X509Certificate) { + return (X509Certificate) peerCertificates[0]; + } + } + + // no X.509 certificate was found for this server + return null; + } +} diff --git a/mitm/src/test/groovy/net/lightbody/bmp/mitm/ExistingCertificateSourceTest.groovy b/mitm/src/test/groovy/net/lightbody/bmp/mitm/ExistingCertificateSourceTest.groovy new file mode 100644 index 000000000..9337ec724 --- /dev/null +++ b/mitm/src/test/groovy/net/lightbody/bmp/mitm/ExistingCertificateSourceTest.groovy @@ -0,0 +1,36 @@ +package net.lightbody.bmp.mitm + +import org.junit.Test + +import java.security.PrivateKey +import java.security.cert.X509Certificate + +import static org.junit.Assert.assertEquals +import static org.mockito.Mockito.mock + +class ExistingCertificateSourceTest { + + X509Certificate mockCertificate = mock(X509Certificate) + PrivateKey mockPrivateKey = mock(PrivateKey) + + @Test + void testLoadExistingCertificateAndKey() { + ExistingCertificateSource certificateSource = new ExistingCertificateSource(mockCertificate, mockPrivateKey) + CertificateAndKey certificateAndKey = certificateSource.load() + + assertEquals(mockCertificate, certificateAndKey.certificate) + assertEquals(mockPrivateKey, certificateAndKey.privateKey) + } + + @Test(expected = IllegalArgumentException) + void testMustSupplyCertificate() { + ExistingCertificateSource certificateSource = new ExistingCertificateSource(null, mockPrivateKey) + certificateSource.load() + } + + @Test(expected = IllegalArgumentException) + void testMustSupplyPrivateKey() { + ExistingCertificateSource certificateSource = new ExistingCertificateSource(mockCertificate, null) + certificateSource.load() + } +} diff --git a/mitm/src/test/groovy/net/lightbody/bmp/mitm/ImpersonatingMitmManagerTest.groovy b/mitm/src/test/groovy/net/lightbody/bmp/mitm/ImpersonatingMitmManagerTest.groovy new file mode 100644 index 000000000..28adf650b --- /dev/null +++ b/mitm/src/test/groovy/net/lightbody/bmp/mitm/ImpersonatingMitmManagerTest.groovy @@ -0,0 +1,48 @@ +package net.lightbody.bmp.mitm + +import net.lightbody.bmp.mitm.keys.ECKeyGenerator +import net.lightbody.bmp.mitm.keys.RSAKeyGenerator +import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager +import org.junit.Test + +import javax.net.ssl.SSLEngine +import javax.net.ssl.SSLSession + +import static org.junit.Assert.assertNotNull +import static org.mockito.Mockito.mock +import static org.mockito.Mockito.when + +class ImpersonatingMitmManagerTest { + SSLSession mockSession = mock(SSLSession) + + @Test + void testCreateDefaultServerEngine() { + ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder().build() + + SSLEngine serverSslEngine = mitmManager.serverSslEngine("hostname", 80) + assertNotNull(serverSslEngine) + } + + @Test + void testCreateDefaultClientEngine() { + ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder().build() + + when(mockSession.getPeerHost()).thenReturn("hostname") + + SSLEngine clientSslEngine = mitmManager.clientSslEngineFor(mockSession) + assertNotNull(clientSslEngine) + } + + @Test + void testCreateCAAndServerCertificatesOfDifferentTypes() { + ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder() + .rootCertificateSource(RootCertificateGenerator.builder().keyGenerator(new RSAKeyGenerator()).build()) + .serverKeyGenerator(new ECKeyGenerator()) + .build() + + when(mockSession.getPeerHost()).thenReturn("hostname") + + SSLEngine clientSslEngine = mitmManager.clientSslEngineFor(mockSession) + assertNotNull(clientSslEngine) + } +} diff --git a/mitm/src/test/groovy/net/lightbody/bmp/mitm/KeyStoreCertificateSourceTest.groovy b/mitm/src/test/groovy/net/lightbody/bmp/mitm/KeyStoreCertificateSourceTest.groovy new file mode 100644 index 000000000..3f0540fab --- /dev/null +++ b/mitm/src/test/groovy/net/lightbody/bmp/mitm/KeyStoreCertificateSourceTest.groovy @@ -0,0 +1,33 @@ +package net.lightbody.bmp.mitm + +import org.junit.Test + +import java.security.KeyStore + +import static org.mockito.Mockito.mock + +class KeyStoreCertificateSourceTest { + + KeyStore mockKeyStore = mock(KeyStore) + + // the happy-path test cases are already covered implicitly as part of KeyStoreFileCertificateSourceTest, so just test negative cases + + @Test(expected = IllegalArgumentException) + void testMustSupplyKeystore() { + KeyStoreCertificateSource keyStoreCertificateSource = new KeyStoreCertificateSource(null, "privatekey", "password") + keyStoreCertificateSource.load() + } + + @Test(expected = IllegalArgumentException) + void testMustSupplyPassword() { + KeyStoreCertificateSource keyStoreCertificateSource = new KeyStoreCertificateSource(mockKeyStore, "privatekey", null) + keyStoreCertificateSource.load() + } + + @Test(expected = IllegalArgumentException) + void testMustSupplyPrivateKeyAlias() { + KeyStoreCertificateSource keyStoreCertificateSource = new KeyStoreCertificateSource(mockKeyStore, null, "password") + keyStoreCertificateSource.load() + } + +} diff --git a/mitm/src/test/groovy/net/lightbody/bmp/mitm/KeyStoreFileCertificateSourceTest.groovy b/mitm/src/test/groovy/net/lightbody/bmp/mitm/KeyStoreFileCertificateSourceTest.groovy new file mode 100644 index 000000000..9b6bcc39b --- /dev/null +++ b/mitm/src/test/groovy/net/lightbody/bmp/mitm/KeyStoreFileCertificateSourceTest.groovy @@ -0,0 +1,64 @@ +package net.lightbody.bmp.mitm + +import net.lightbody.bmp.mitm.test.util.CertificateTestUtil +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +public class KeyStoreFileCertificateSourceTest { + + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder() + + File pkcs12File + File jksFile + + @Before + void stageFiles() { + pkcs12File = tmpDir.newFile("keystore.p12") + jksFile = tmpDir.newFile("keystore.jks") + + Files.copy(KeyStoreFileCertificateSourceTest.getResourceAsStream("/net/lightbody/bmp/mitm/keystore.p12"), pkcs12File.toPath(), StandardCopyOption.REPLACE_EXISTING) + Files.copy(KeyStoreFileCertificateSourceTest.getResourceAsStream("/net/lightbody/bmp/mitm/keystore.jks"), jksFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + + @Test + void testPkcs12FileOnClasspath() { + KeyStoreFileCertificateSource keyStoreFileCertificateSource = new KeyStoreFileCertificateSource("PKCS12", "/net/lightbody/bmp/mitm/keystore.p12", "privateKey", "password") + + CertificateAndKey certificateAndKey = keyStoreFileCertificateSource.load(); + + CertificateTestUtil.verifyTestRSACertWithCNandO(certificateAndKey) + } + + @Test + void testPkcs12FileOnDisk() { + KeyStoreFileCertificateSource keyStoreFileCertificateSource = new KeyStoreFileCertificateSource("PKCS12", pkcs12File, "privateKey", "password") + + CertificateAndKey certificateAndKey = keyStoreFileCertificateSource.load(); + + CertificateTestUtil.verifyTestRSACertWithCNandO(certificateAndKey) + } + + @Test + void testJksFileOnClasspath() { + KeyStoreFileCertificateSource keyStoreFileCertificateSource = new KeyStoreFileCertificateSource("JKS", "/net/lightbody/bmp/mitm/keystore.jks", "privateKey", "password") + + CertificateAndKey certificateAndKey = keyStoreFileCertificateSource.load(); + + CertificateTestUtil.verifyTestRSACertWithCNandO(certificateAndKey) + } + + @Test + void testJksFileOnDisk() { + KeyStoreFileCertificateSource keyStoreFileCertificateSource = new KeyStoreFileCertificateSource("JKS", jksFile, "privateKey", "password") + + CertificateAndKey certificateAndKey = keyStoreFileCertificateSource.load(); + + CertificateTestUtil.verifyTestRSACertWithCNandO(certificateAndKey) + } +} diff --git a/mitm/src/test/groovy/net/lightbody/bmp/mitm/PemFileCertificateSourceTest.groovy b/mitm/src/test/groovy/net/lightbody/bmp/mitm/PemFileCertificateSourceTest.groovy new file mode 100644 index 000000000..dfa33b8b9 --- /dev/null +++ b/mitm/src/test/groovy/net/lightbody/bmp/mitm/PemFileCertificateSourceTest.groovy @@ -0,0 +1,83 @@ +package net.lightbody.bmp.mitm + +import net.lightbody.bmp.mitm.exception.ImportException +import net.lightbody.bmp.mitm.test.util.CertificateTestUtil +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +import static org.junit.Assert.assertNotNull + +class PemFileCertificateSourceTest { + + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder() + + File certificateFile + File encryptedPrivateKeyFile + File unencryptedPrivateKeyFile + + @Before + void stageFiles() { + certificateFile = tmpDir.newFile("certificate.crt") + encryptedPrivateKeyFile = tmpDir.newFile("encrypted-private-key.key") + unencryptedPrivateKeyFile = tmpDir.newFile("unencrypted-private-key.key") + + Files.copy(KeyStoreFileCertificateSourceTest.getResourceAsStream("/net/lightbody/bmp/mitm/certificate.crt"), certificateFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + Files.copy(KeyStoreFileCertificateSourceTest.getResourceAsStream("/net/lightbody/bmp/mitm/encrypted-private-key.key"), encryptedPrivateKeyFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + Files.copy(KeyStoreFileCertificateSourceTest.getResourceAsStream("/net/lightbody/bmp/mitm/unencrypted-private-key.key"), unencryptedPrivateKeyFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + + @Test + void testCanLoadCertificateAndPasswordProtectedKey() { + PemFileCertificateSource pemFileCertificateSource = new PemFileCertificateSource(certificateFile, encryptedPrivateKeyFile, "password") + + CertificateAndKey certificateAndKey = pemFileCertificateSource.load() + assertNotNull(certificateAndKey) + + CertificateTestUtil.verifyTestRSACertWithCNandO(certificateAndKey) + } + + @Test + void testCanLoadCertificateAndUnencryptedKey() { + PemFileCertificateSource pemFileCertificateSource = new PemFileCertificateSource(certificateFile, unencryptedPrivateKeyFile, null) + + CertificateAndKey certificateAndKey = pemFileCertificateSource.load() + assertNotNull(certificateAndKey) + + CertificateTestUtil.verifyTestRSACertWithCNandO(certificateAndKey) + } + + @Test(expected = ImportException) + void testCannotLoadEncryptedKeyWithoutPassword() { + PemFileCertificateSource pemFileCertificateSource = new PemFileCertificateSource(certificateFile, encryptedPrivateKeyFile, "wrongpassword") + + pemFileCertificateSource.load() + } + + @Test(expected = ImportException) + void testIncorrectCertificateFile() { + PemFileCertificateSource pemFileCertificateSource = new PemFileCertificateSource(new File("does-not-exist.crt"), encryptedPrivateKeyFile, "password") + + pemFileCertificateSource.load() + } + + @Test(expected = IllegalArgumentException) + void testNullCertificateFile() { + PemFileCertificateSource pemFileCertificateSource = new PemFileCertificateSource(null, encryptedPrivateKeyFile, "password") + + pemFileCertificateSource.load() + } + + @Test(expected = IllegalArgumentException) + void testNullPrivateKeyFile() { + PemFileCertificateSource pemFileCertificateSource = new PemFileCertificateSource(certificateFile, null, "password") + + pemFileCertificateSource.load() + } +} + diff --git a/mitm/src/test/groovy/net/lightbody/bmp/mitm/RootCertificateGeneratorTest.groovy b/mitm/src/test/groovy/net/lightbody/bmp/mitm/RootCertificateGeneratorTest.groovy new file mode 100644 index 000000000..f406e0344 --- /dev/null +++ b/mitm/src/test/groovy/net/lightbody/bmp/mitm/RootCertificateGeneratorTest.groovy @@ -0,0 +1,88 @@ +package net.lightbody.bmp.mitm + +import net.lightbody.bmp.mitm.test.util.CertificateTestUtil +import net.lightbody.bmp.mitm.keys.RSAKeyGenerator +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +import static org.hamcrest.Matchers.greaterThan +import static org.hamcrest.Matchers.isEmptyOrNullString +import static org.hamcrest.Matchers.not +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNotNull +import static org.junit.Assert.assertThat + +class RootCertificateGeneratorTest { + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder() + + CertificateInfo certificateInfo = new CertificateInfo() + .commonName("littleproxy-test") + .notAfter(new Date()) + .notBefore(new Date()) + + @Test + void testGenerateRootCertificate() { + RootCertificateGenerator generator = RootCertificateGenerator.builder() + .certificateInfo(certificateInfo) + .keyGenerator(new RSAKeyGenerator()) + .messageDigest("SHA256") + .build() + + CertificateAndKey certificateAndKey = generator.load() + + CertificateTestUtil.verifyTestRSACertWithCN(certificateAndKey) + + CertificateAndKey secondLoad = generator.load() + + assertEquals("Expected RootCertificateGenerator to return the same instance between calls to .load()", certificateAndKey, secondLoad) + } + + @Test + void testCanUseDefaultValues() { + RootCertificateGenerator generator = RootCertificateGenerator.builder().build() + + CertificateAndKey certificateAndKey = generator.load() + + assertNotNull(certificateAndKey) + } + + @Test + void testCanSaveAsPKCS12File() { + RootCertificateGenerator generator = RootCertificateGenerator.builder().build() + + File file = tmpDir.newFile() + + generator.saveRootCertificateAndKey("PKCS12", file, "privateKey", "password") + + // trivial verification that something was written to the file + assertThat("Expected file to be >0 bytes after writing certificate and private key", file.length(), greaterThan(0L)) + } + + @Test + void testCanSaveAsJKSFile() { + RootCertificateGenerator generator = RootCertificateGenerator.builder().build() + + File file = tmpDir.newFile() + + generator.saveRootCertificateAndKey("JKS", file, "privateKey", "password") + + // trivial verification that something was written to the file + assertThat("Expected file to be >0 bytes after writing certificate and private key", file.length(), greaterThan(0L)) + } + + @Test + void testCanEncodeAsPem() { + RootCertificateGenerator generator = RootCertificateGenerator.builder().build() + + String pemEncodedPrivateKey = generator.encodePrivateKeyAsPem("password") + + // trivial verification that something was written to the string + assertThat("Expected string containing PEM-encoded private key to contain characters", pemEncodedPrivateKey, not(isEmptyOrNullString())) + + String pemEncodedCertificate = generator.encodeRootCertificateAsPem() + assertThat("Expected string containing PEM-encoded certificate to contain characters", pemEncodedCertificate , not(isEmptyOrNullString())) + } + +} diff --git a/mitm/src/test/groovy/net/lightbody/bmp/mitm/tools/ECKeyGeneratorTest.groovy b/mitm/src/test/groovy/net/lightbody/bmp/mitm/tools/ECKeyGeneratorTest.groovy new file mode 100644 index 000000000..aa10e9849 --- /dev/null +++ b/mitm/src/test/groovy/net/lightbody/bmp/mitm/tools/ECKeyGeneratorTest.groovy @@ -0,0 +1,27 @@ +package net.lightbody.bmp.mitm.tools + +import net.lightbody.bmp.mitm.keys.ECKeyGenerator +import org.junit.Test + +import java.security.KeyPair + +import static org.junit.Assert.assertNotNull + +class ECKeyGeneratorTest { + @Test + void testGenerateWithDefaults() { + ECKeyGenerator keyGenerator = new ECKeyGenerator() + KeyPair keyPair = keyGenerator.generate() + + assertNotNull(keyPair) + } + + @Test + void testGenerateWithExplicitNamedCurve() { + ECKeyGenerator keyGenerator = new ECKeyGenerator("secp384r1") + KeyPair keyPair = keyGenerator.generate() + + assertNotNull(keyPair) + // not much else to verify, other than successful generation + } +} diff --git a/mitm/src/test/groovy/net/lightbody/bmp/mitm/tools/RSAKeyGeneratorTest.groovy b/mitm/src/test/groovy/net/lightbody/bmp/mitm/tools/RSAKeyGeneratorTest.groovy new file mode 100644 index 000000000..a2bf8ed7b --- /dev/null +++ b/mitm/src/test/groovy/net/lightbody/bmp/mitm/tools/RSAKeyGeneratorTest.groovy @@ -0,0 +1,27 @@ +package net.lightbody.bmp.mitm.tools + +import net.lightbody.bmp.mitm.keys.RSAKeyGenerator +import org.junit.Test + +import java.security.KeyPair + +import static org.junit.Assert.assertNotNull + +class RSAKeyGeneratorTest { + @Test + void testGenerateWithDefaults() { + RSAKeyGenerator keyGenerator = new RSAKeyGenerator() + KeyPair keyPair = keyGenerator.generate() + + assertNotNull(keyPair) + } + + @Test + void testGenerateWithExplicitKeySize() { + RSAKeyGenerator keyGenerator = new RSAKeyGenerator(1024) + KeyPair keyPair = keyGenerator.generate() + + assertNotNull(keyPair) + // not much else to verify, other than successful generation + } +} diff --git a/mitm/src/test/java/net/lightbody/bmp/mitm/ImpersonationPerformanceTests.java b/mitm/src/test/java/net/lightbody/bmp/mitm/ImpersonationPerformanceTests.java new file mode 100644 index 000000000..3adefaecb --- /dev/null +++ b/mitm/src/test/java/net/lightbody/bmp/mitm/ImpersonationPerformanceTests.java @@ -0,0 +1,158 @@ +package net.lightbody.bmp.mitm; + +import net.lightbody.bmp.mitm.keys.ECKeyGenerator; +import net.lightbody.bmp.mitm.keys.KeyGenerator; +import net.lightbody.bmp.mitm.keys.RSAKeyGenerator; +import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager; +import net.lightbody.bmp.mitm.tools.BouncyCastleSecurityProviderTool; +import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; +import net.lightbody.bmp.mitm.util.MitmConstants; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLSession; +import java.security.KeyPair; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.concurrent.atomic.AtomicInteger; + +// ignored as a quick work-around to running these tests with unit tests +@Ignore +@RunWith(Parameterized.class) +public class ImpersonationPerformanceTests { + private static final Logger log = LoggerFactory.getLogger(ImpersonationPerformanceTests.class); + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + {new RSAKeyGenerator(), "SHA512", new RSAKeyGenerator(), "SHA512"}, + {new RSAKeyGenerator(), "SHA512", new RSAKeyGenerator(1024), "SHA512"}, + {new RSAKeyGenerator(1024), "SHA512", new RSAKeyGenerator(1024), "SHA512"}, + {new RSAKeyGenerator(), "SHA512", new ECKeyGenerator(), "SHA512"}, + {new ECKeyGenerator(), "SHA512", new ECKeyGenerator(), "SHA512"}, + {new ECKeyGenerator(), "SHA512", new RSAKeyGenerator(), "SHA512"} + }); + } + + @Parameter + public KeyGenerator rootCertKeyGen; + + @Parameter(1) + public String rootCertDigest; + + @Parameter(2) + public KeyGenerator serverCertKeyGen; + + @Parameter(3) + public String serverCertDigest; + + private static final int WARM_UP_ITERATIONS = 5; + + private static final int ITERATIONS = 50; + + @Test + public void testImpersonatingMitmManagerPerformance() { + ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder() + .rootCertificateSource(RootCertificateGenerator.builder() + .keyGenerator(rootCertKeyGen) + .messageDigest(rootCertDigest) + .build()) + .serverKeyGenerator(serverCertKeyGen) + .serverMessageDigest(serverCertDigest) + .build(); + + final AtomicInteger iteration = new AtomicInteger(); + + SSLSession mockSession = Mockito.mock(SSLSession.class); + Mockito.when(mockSession.getPeerHost()).thenAnswer(new Answer() { + @Override + public String answer(InvocationOnMock invocationOnMock) throws Throwable { + return String.valueOf(iteration.get() + ".com"); + } + }); + + log.info("Test parameters:\n\tRoot Cert Key Gen: {}\n\tRoot Cert Digest: {}\n\tServer Cert Key Gen: {}\n\tServer Cert Digest: {}", + rootCertKeyGen, rootCertDigest, serverCertKeyGen, serverCertDigest); + + // warm up, init root cert, etc. + log.info("Executing {} warm up iterations", WARM_UP_ITERATIONS); + for (iteration.set(0); iteration.get() < WARM_UP_ITERATIONS; iteration.incrementAndGet()) { + + mitmManager.clientSslEngineFor(mockSession); + } + + log.info("Executing {} performance test iterations", ITERATIONS); + + long start = System.currentTimeMillis(); + + for (iteration.set(0); iteration.get() < ITERATIONS; iteration.incrementAndGet()) { + mitmManager.clientSslEngineFor(mockSession); + } + + long finish = System.currentTimeMillis(); + + log.info("Finished performance test:\n\tRoot Cert Key Gen: {}\n\tRoot Cert Digest: {}\n\tServer Cert Key Gen: {}\n\tServer Cert Digest: {}", + rootCertKeyGen, rootCertDigest, serverCertKeyGen, serverCertDigest); + log.info("Generated {} certificates in {}ms. Average time per certificate: {}ms", iteration.get(), finish - start, (finish - start) / iteration.get()); + } + + @Test + public void testServerCertificateCreationAndAssembly() { + CertificateAndKey rootCert = RootCertificateGenerator.builder() + .keyGenerator(rootCertKeyGen) + .messageDigest(rootCertDigest) + .build() + .load(); + + log.info("Test parameters:\n\tRoot Cert Key Gen: {}\n\tRoot Cert Digest: {}\n\tServer Cert Key Gen: {}\n\tServer Cert Digest: {}", + rootCertKeyGen, rootCertDigest, serverCertKeyGen, serverCertDigest); + + log.info("Executing {} warm up iterations", WARM_UP_ITERATIONS); + for (int i = 0; i < WARM_UP_ITERATIONS; i++) { + KeyPair serverCertKeyPair = serverCertKeyGen.generate(); + CertificateAndKey serverCert = new BouncyCastleSecurityProviderTool().createServerCertificate( + createCertificateInfo(i + ".com"), + rootCert.getCertificate(), + rootCert.getPrivateKey(), + serverCertKeyPair, + serverCertDigest); + + new DefaultSecurityProviderTool().createServerKeyStore(MitmConstants.DEFAULT_KEYSTORE_TYPE, serverCert, rootCert.getCertificate(), "alias", "password"); + } + + log.info("Executing {} performance test iterations", ITERATIONS); + + long start = System.currentTimeMillis(); + + for (int i = 0; i < ITERATIONS; i++) { + KeyPair serverCertKeyPair = serverCertKeyGen.generate(); + CertificateAndKey serverCert = new BouncyCastleSecurityProviderTool().createServerCertificate( + createCertificateInfo(i + ".com"), + rootCert.getCertificate(), + rootCert.getPrivateKey(), + serverCertKeyPair, + serverCertDigest); + + new DefaultSecurityProviderTool().createServerKeyStore(MitmConstants.DEFAULT_KEYSTORE_TYPE, serverCert, rootCert.getCertificate(), "alias", "password"); + } + + long finish = System.currentTimeMillis(); + + log.info("Finished performance test:\n\tRoot Cert Key Gen: {}\n\tRoot Cert Digest: {}\n\tServer Cert Key Gen: {}\n\tServer Cert Digest: {}", + rootCertKeyGen, rootCertDigest, serverCertKeyGen, serverCertDigest); + log.info("Assembled {} Key Stores in {}ms. Average time per Key Store: {}ms", ITERATIONS, finish - start, (finish - start) / ITERATIONS); + } + + private static CertificateInfo createCertificateInfo(String hostname) { + return new CertificateInfo().commonName(hostname).notBefore(new Date()).notAfter(new Date()); + } +} diff --git a/mitm/src/test/java/net/lightbody/bmp/mitm/example/CustomCAKeyStoreExample.java b/mitm/src/test/java/net/lightbody/bmp/mitm/example/CustomCAKeyStoreExample.java new file mode 100644 index 000000000..2ceca87ca --- /dev/null +++ b/mitm/src/test/java/net/lightbody/bmp/mitm/example/CustomCAKeyStoreExample.java @@ -0,0 +1,39 @@ +package net.lightbody.bmp.mitm.example; + +import net.lightbody.bmp.mitm.KeyStoreFileCertificateSource; +import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager; +import org.littleshoot.proxy.HttpProxyServer; +import org.littleshoot.proxy.impl.DefaultHttpProxyServer; + +import java.io.File; + +/** + * This example creates an ImpersonatingMitmManager which loads the CA Root Certificate and Private Key + * from a custom KeyStore. + */ +public class CustomCAKeyStoreExample { + public static void main(String[] args) { + // load the root certificate and private key from an existing KeyStore + KeyStoreFileCertificateSource fileCertificateSource = new KeyStoreFileCertificateSource( + "PKCS12", // KeyStore type. for .jks files (Java KeyStore), use "JKS" + new File("/path/to/my/keystore.p12"), + "keyAlias", // alias of the private key in the KeyStore; if you did not specify an alias when creating it, use "1" + "keystorePassword"); + + + // tell the MitmManager to use the custom certificate and private key + ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder() + .rootCertificateSource(fileCertificateSource) + .build(); + + // tell the HttpProxyServerBootstrap to use the new MitmManager + HttpProxyServer proxyServer = DefaultHttpProxyServer.bootstrap() + .withManInTheMiddle(mitmManager) + .start(); + + // make your requests to the proxy server + //... + + proxyServer.abort(); + } +} diff --git a/mitm/src/test/java/net/lightbody/bmp/mitm/example/CustomCAPemFileExample.java b/mitm/src/test/java/net/lightbody/bmp/mitm/example/CustomCAPemFileExample.java new file mode 100644 index 000000000..3ad488fa6 --- /dev/null +++ b/mitm/src/test/java/net/lightbody/bmp/mitm/example/CustomCAPemFileExample.java @@ -0,0 +1,38 @@ +package net.lightbody.bmp.mitm.example; + +import net.lightbody.bmp.mitm.PemFileCertificateSource; +import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager; +import org.littleshoot.proxy.HttpProxyServer; +import org.littleshoot.proxy.impl.DefaultHttpProxyServer; + +import java.io.File; + +/** + * This example creates an ImpersonatingMitmManager which loads the CA Root Certificate and Private Key + * from a PEM-encoded certificate and a PEM-encoded private key file. + */ +public class CustomCAPemFileExample { + public static void main(String[] args) { + // load the root certificate and private key from existing PEM-encoded certificate and private key files + PemFileCertificateSource fileCertificateSource = new PemFileCertificateSource( + new File("/path/to/my/certificate.cer"), // the PEM-encoded certificate file + new File("/path/to/my/private-key.pem"), // the PEM-encoded private key file + "privateKeyPassword"); // the password for the private key -- can be null if the private key is not encrypted + + + // tell the MitmManager to use the custom certificate and private key + ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder() + .rootCertificateSource(fileCertificateSource) + .build(); + + // tell the HttpProxyServerBootstrap to use the new MitmManager + HttpProxyServer proxyServer = DefaultHttpProxyServer.bootstrap() + .withManInTheMiddle(mitmManager) + .start(); + + // make your requests to the proxy server + //... + + proxyServer.abort(); + } +} diff --git a/mitm/src/test/java/net/lightbody/bmp/mitm/example/EllipticCurveCAandServerExample.java b/mitm/src/test/java/net/lightbody/bmp/mitm/example/EllipticCurveCAandServerExample.java new file mode 100644 index 000000000..f3fff3af5 --- /dev/null +++ b/mitm/src/test/java/net/lightbody/bmp/mitm/example/EllipticCurveCAandServerExample.java @@ -0,0 +1,47 @@ +package net.lightbody.bmp.mitm.example; + +import net.lightbody.bmp.mitm.RootCertificateGenerator; +import net.lightbody.bmp.mitm.keys.ECKeyGenerator; +import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager; +import org.littleshoot.proxy.HttpProxyServer; +import org.littleshoot.proxy.impl.DefaultHttpProxyServer; + +import java.io.File; + +/** + * This example creates a dynamically-generated Elliptic Curve CA Root Certificate and Private Key and saves them to + * PEM files for import into a browser and later reuse. It also uses Elliptic Curve keys when impersonating server + * certificates. + */ +public class EllipticCurveCAandServerExample { + public static void main(String[] args) { + // create a dyamic CA root certificate generator using Elliptic Curve keys + RootCertificateGenerator ecRootCertificateGenerator = RootCertificateGenerator.builder() + .keyGenerator(new ECKeyGenerator()) // use EC keys, instead of the default RSA + .build(); + + // save the dynamically-generated CA root certificate for installation in a browser + ecRootCertificateGenerator.saveRootCertificateAsPemFile(new File("/tmp/my-dynamic-ca.cer")); + + // save the dynamically-generated CA private key for use in future LittleProxy executions + // (see CustomCAPemFileExample.java for an example loading a previously-generated CA cert + key from a PEM file) + ecRootCertificateGenerator.savePrivateKeyAsPemFile(new File("/tmp/my-ec-private-key.pem"), "secretPassword"); + + // tell the MitmManager to use the root certificate we just generated, and to use EC keys when + // creating impersonated server certs + ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder() + .rootCertificateSource(ecRootCertificateGenerator) + .serverKeyGenerator(new ECKeyGenerator()) + .build(); + + // tell the HttpProxyServerBootstrap to use the new MitmManager + HttpProxyServer proxyServer = DefaultHttpProxyServer.bootstrap() + .withManInTheMiddle(mitmManager) + .start(); + + // make your requests to the proxy server + //... + + proxyServer.abort(); + } +} diff --git a/mitm/src/test/java/net/lightbody/bmp/mitm/example/LittleProxyDefaultConfigExample.java b/mitm/src/test/java/net/lightbody/bmp/mitm/example/LittleProxyDefaultConfigExample.java new file mode 100644 index 000000000..31cc9f618 --- /dev/null +++ b/mitm/src/test/java/net/lightbody/bmp/mitm/example/LittleProxyDefaultConfigExample.java @@ -0,0 +1,30 @@ +package net.lightbody.bmp.mitm.example; + +import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager; +import org.littleshoot.proxy.HttpProxyServer; +import org.littleshoot.proxy.impl.DefaultHttpProxyServer; + +/** + * This example creates an ImpersonatingMitmManager with all-default settings: + * - Dynamically-generated CA Root Certificate and Private Key (2048-bit RSA. SHA512 signature) + * - Server certificate impersonation by domain name (2048-bit RSA, SHA512 signature) + * - Default Java trust store (upstream servers' certificates validated against Java's trusted CAs) + */ +public class LittleProxyDefaultConfigExample { + public static void main(String[] args) { + // initialize an MitmManager with default settings + ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder().build(); + + // to save the generated CA certificate for installation in a browser, see SaveGeneratedCAExample.java + + // tell the HttpProxyServerBootstrap to use the new MitmManager + HttpProxyServer proxyServer = DefaultHttpProxyServer.bootstrap() + .withManInTheMiddle(mitmManager) + .start(); + + // make your requests to the proxy server + //... + + proxyServer.abort(); + } +} diff --git a/mitm/src/test/java/net/lightbody/bmp/mitm/example/SaveGeneratedCAExample.java b/mitm/src/test/java/net/lightbody/bmp/mitm/example/SaveGeneratedCAExample.java new file mode 100644 index 000000000..1cecef147 --- /dev/null +++ b/mitm/src/test/java/net/lightbody/bmp/mitm/example/SaveGeneratedCAExample.java @@ -0,0 +1,37 @@ +package net.lightbody.bmp.mitm.example; + +import net.lightbody.bmp.mitm.RootCertificateGenerator; +import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager; +import org.littleshoot.proxy.HttpProxyServer; +import org.littleshoot.proxy.impl.DefaultHttpProxyServer; + +import java.io.File; + +/** + * This example creates an ImpersonatingMitmManager with all-default settings and saves the dynamically generated + * CA Root Certificate as a PEM file for installation in a browser. + */ +public class SaveGeneratedCAExample { + public static void main(String[] args) { + // create a dynamic CA root certificate generator using default settings (2048-bit RSA keys) + RootCertificateGenerator rootCertificateGenerator = RootCertificateGenerator.builder().build(); + + // save the dynamically-generated CA root certificate for installation in a browser + rootCertificateGenerator.saveRootCertificateAsPemFile(new File("/tmp/my-dynamic-ca.cer")); + + // tell the MitmManager to use the root certificate we just generated + ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder() + .rootCertificateSource(rootCertificateGenerator) + .build(); + + // tell the HttpProxyServerBootstrap to use the new MitmManager + HttpProxyServer proxyServer = DefaultHttpProxyServer.bootstrap() + .withManInTheMiddle(mitmManager) + .start(); + + // make your requests to the proxy server + //... + + proxyServer.abort(); + } +} diff --git a/mitm/src/test/java/net/lightbody/bmp/mitm/integration/LittleProxyIntegrationTest.java b/mitm/src/test/java/net/lightbody/bmp/mitm/integration/LittleProxyIntegrationTest.java new file mode 100644 index 000000000..3836b9f94 --- /dev/null +++ b/mitm/src/test/java/net/lightbody/bmp/mitm/integration/LittleProxyIntegrationTest.java @@ -0,0 +1,129 @@ +package net.lightbody.bmp.mitm.integration; + +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager; +import org.apache.http.HttpHost; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContexts; +import org.apache.http.conn.ssl.TrustStrategy; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.junit.Test; +import org.littleshoot.proxy.HttpFilters; +import org.littleshoot.proxy.HttpFiltersAdapter; +import org.littleshoot.proxy.HttpFiltersSource; +import org.littleshoot.proxy.HttpFiltersSourceAdapter; +import org.littleshoot.proxy.HttpProxyServer; +import org.littleshoot.proxy.impl.DefaultHttpProxyServer; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Tests out-of-the-box integration with LittleProxy. + */ +public class LittleProxyIntegrationTest { + @Test + public void testLittleProxyMitm() throws IOException, InterruptedException { + final AtomicBoolean interceptedGetRequest = new AtomicBoolean(); + final AtomicBoolean interceptedGetResponse = new AtomicBoolean(); + + HttpFiltersSource filtersSource = new HttpFiltersSourceAdapter() { + @Override + public HttpFilters filterRequest(HttpRequest originalRequest) { + return new HttpFiltersAdapter(originalRequest) { + @Override + public HttpResponse proxyToServerRequest(HttpObject httpObject) { + if (httpObject instanceof HttpRequest) { + HttpRequest httpRequest = (HttpRequest) httpObject; + if (httpRequest.getMethod().equals(HttpMethod.GET)) { + interceptedGetRequest.set(true); + } + } + + return super.proxyToServerRequest(httpObject); + } + + @Override + public HttpObject serverToProxyResponse(HttpObject httpObject) { + if (httpObject instanceof HttpResponse) { + HttpResponse httpResponse = (HttpResponse) httpObject; + if (httpResponse.getStatus().code() == 200) { + interceptedGetResponse.set(true); + } + } + return super.serverToProxyResponse(httpObject); + } + }; + } + }; + + ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder().build(); + + HttpProxyServer proxyServer = DefaultHttpProxyServer.bootstrap() + .withManInTheMiddle(mitmManager) + .withFiltersSource(filtersSource) + .start(); + + try (CloseableHttpClient httpClient = getNewHttpClient(proxyServer.getListenAddress().getPort())) { + try (CloseableHttpResponse response = httpClient.execute(new HttpGet("https://www.google.com"))) { + assertEquals("Expected to receive an HTTP 200 from http://www.google.com", 200, response.getStatusLine().getStatusCode()); + + EntityUtils.consume(response.getEntity()); + } + } + + Thread.sleep(500); + + assertTrue("Expected HttpFilters to successfully intercept the HTTP GET request", interceptedGetRequest.get()); + assertTrue("Expected HttpFilters to successfully intercept the server's response to the HTTP GET", interceptedGetResponse.get()); + + proxyServer.abort(); + } + + /** + * Creates an HTTP client that trusts all upstream servers and uses a localhost proxy on the specified port. + */ + private static CloseableHttpClient getNewHttpClient(int proxyPort) { + try { + // Trust all certs -- under no circumstances should this ever be used outside of testing + SSLContext sslcontext = SSLContexts.custom() + .useTLS() + .loadTrustMaterial(null, new TrustStrategy() { + @Override + public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException { + return true; + } + }) + .build(); + + SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( + sslcontext, + SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + + CloseableHttpClient httpclient = HttpClients.custom() + .setSSLSocketFactory(sslsf) + .setProxy(new HttpHost("127.0.0.1", proxyPort)) + // disable decompressing content, since some tests want uncompressed content for testing purposes + .disableContentCompression() + .disableAutomaticRetries() + .build(); + + return httpclient; + } catch (Exception e) { + throw new RuntimeException("Unable to create new HTTP client", e); + } + } +} diff --git a/mitm/src/test/java/net/lightbody/bmp/mitm/test/util/CertificateTestUtil.java b/mitm/src/test/java/net/lightbody/bmp/mitm/test/util/CertificateTestUtil.java new file mode 100644 index 000000000..8b92a1dc1 --- /dev/null +++ b/mitm/src/test/java/net/lightbody/bmp/mitm/test/util/CertificateTestUtil.java @@ -0,0 +1,44 @@ +package net.lightbody.bmp.mitm.test.util; + +import net.lightbody.bmp.mitm.CertificateAndKey; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Utility methods for X.509 certificate verification in unit tests. + */ +public class CertificateTestUtil { + /** + * Asserts that the specified {@link CertificateAndKey} contains an RSA private key and an X.509 certificate + * with CN="littleproxy-test" and O="LittleProxy test". + */ + public static void verifyTestRSACertWithCNandO(CertificateAndKey certificateAndKey) { + X509Certificate certificate = certificateAndKey.getCertificate(); + assertNotNull(certificate); + assertNotNull(certificate.getIssuerDN()); + assertEquals("CN=littleproxy-test, O=LittleProxy test", certificate.getIssuerDN().getName()); + + PrivateKey privateKey = certificateAndKey.getPrivateKey(); + assertNotNull(privateKey); + assertEquals("RSA", privateKey.getAlgorithm()); + } + + /** + * Asserts that the specified {@link CertificateAndKey} contains an RSA private key and an X.509 certificate + * with CN="littleproxy-test". + */ + public static void verifyTestRSACertWithCN(CertificateAndKey certificateAndKey) { + X509Certificate certificate = certificateAndKey.getCertificate(); + assertNotNull(certificate); + assertNotNull(certificate.getIssuerDN()); + assertEquals("CN=littleproxy-test", certificate.getIssuerDN().getName()); + + PrivateKey privateKey = certificateAndKey.getPrivateKey(); + assertNotNull(privateKey); + assertEquals("RSA", privateKey.getAlgorithm()); + } +} diff --git a/mitm/src/test/resources/log4j2-test.json b/mitm/src/test/resources/log4j2-test.json new file mode 100644 index 000000000..f3e5e72ec --- /dev/null +++ b/mitm/src/test/resources/log4j2-test.json @@ -0,0 +1,23 @@ +{ + "configuration" : { + "name": "test", + "appenders": { + "Console": { + "name": "console", + "target": "SYSTEM_OUT", + "PatternLayout": { + "pattern": "%-7r %date %level [%thread] %logger - %msg%n" + } + } + }, + + "loggers": { + "root": { + "level": "info", + "appender-ref": { + "ref": "console" + } + } + } + } +} \ No newline at end of file diff --git a/mitm/src/test/resources/net/lightbody/bmp/mitm/certificate.crt b/mitm/src/test/resources/net/lightbody/bmp/mitm/certificate.crt new file mode 100644 index 000000000..f632cb32d --- /dev/null +++ b/mitm/src/test/resources/net/lightbody/bmp/mitm/certificate.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDPzCCAiegAwIBAgIJALPOOC+0RqAQMA0GCSqGSIb3DQEBCwUAMDYxGTAXBgNV +BAoMEExpdHRsZVByb3h5IHRlc3QxGTAXBgNVBAMMEGxpdHRsZXByb3h5LXRlc3Qw +HhcNMTUxMjE4MDIxMDI4WhcNMjUxMjE1MDIxMDI4WjA2MRkwFwYDVQQKDBBMaXR0 +bGVQcm94eSB0ZXN0MRkwFwYDVQQDDBBsaXR0bGVwcm94eS10ZXN0MIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2vcm/qETdg92EjYka3Zt+UEraLKzTovz +gvVx4F0EmCC3KDU2v2+WeX4wmVjP6qtMr6nJOSYkFedAvIRGi7hbf3JKQ1nqhH1q +U2zP9HXNt+/LCw8kOFJFii9cp6/h8OnlF8hoIWz4lRTMMcjoiBzV5vfyTb2zWE0l +1DXGypKTxjqg8Pd/tqsMl2uc4+xnEL/ZK9cK8wxg0suUqaGQRaX2R3SovbFKZ1c3 +VApS8zHksSm6qQStbisuEEHSYLpFCrMnXsLJ9KfzTUDwBqrKaMyDAEjoS2LpoijF +YalTW3bF721GiYl2GtcwCIzqpHcIbHAPn1PxQY4UHUt3z4YSjTsuXwIDAQABo1Aw +TjAdBgNVHQ4EFgQUElRcE71sJsuWUVOyYALgQQhPHUUwHwYDVR0jBBgwFoAUElRc +E71sJsuWUVOyYALgQQhPHUUwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AQEArU29g5/V5rywfQizo5J/XEbWjfN0zvuFithS7Zc2OqrSKV8c3NY6NdER/0jj +CmhohnUkduMW+/bizWcuiyt5KDwg6Ar2kEXYY0YgOLbxTmoNvBG84zO+54BV0qy7 +16D8ItrPbcIn83eW8TXug1dWISxWhVRIpHdH2Ok4/XDRFgTxORsC08v2OwwQ566g +PqphR6eIJN8MwThcT9D9rv0HcX9esBJJlNc9OigB9T3O2Xg7+qPJtgqktkN8vlTb +Co0BWoR43xVPTNam533ioUX7woUWnlsQtclN//vazr+uofJ8jnHjpAkh9gZ/l6kn +AdZVrExA+SSUqtWgk9cD0WGSEQ== +-----END CERTIFICATE----- diff --git a/mitm/src/test/resources/net/lightbody/bmp/mitm/encrypted-private-key.key b/mitm/src/test/resources/net/lightbody/bmp/mitm/encrypted-private-key.key new file mode 100644 index 000000000..12c4afc1b --- /dev/null +++ b/mitm/src/test/resources/net/lightbody/bmp/mitm/encrypted-private-key.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,2D5055AA227DC3421AEBE5C8480F57B1 + +VZnrQPjan9fUvkJVKwJrJLWqWx7qgw9M8eeqZoqjrueZLNFu74mp2qbKeJ4hkbqf +dBvMuBaSaqrS2SxfHf/T8uZg+K0vMB0hH+1pVt+AtJRYLybQLmjZSnU/8CDEp358 +59FX5rYFvMRD3fUkESWeQSlSDuE/YG5YlqmxnSVOKEmAQQ0QTGEDIDKl77gRrg7r +mr6ey8NgW2OBaJvLbXLAldHd2lBlj/NH7sXwWq1LgbF/CMm5LLtIEMkOSZ5jgFvU +UqMOTTSGrhtPa4vvy1m8XFx9Z33usi/mEvXQpVeLCmE4BQkS7rSMV1E/ETVkzsfF +wq3c6cWNmh/JvPYUIa6dXjDZPwCz/WpVH7fVwTqBkBTbTUuIJSP//jJB7npZoQrE +rfpdJ9eX9QAjbFI6goYnc4SVWhpzR9LDSk9mpEikGysN8yDYabKbl0OzT9KovGn0 +CLKYxo2LQBwGJR171y5yyC/3s39b9pPC6JQAzji4S7uJQjayYx8Yv3xb1f+4t2Xa +/3M46QtAUJMGAqdmpTXhvNzPyGzJFK1gwNoUf6oM/Vww8KRvVNJTfPljEARxTyRO +jzan4Dd5jtSqw87E2b76ECrj92C7qp3iC/4zkkCHTulPMjwvIzY9WQ2wx1qyVy9B +4qwat9h90AnrAX9q6kvDTglD3n605vgU6P/u1PCzTsTLT1DL5OyMfT3MVSS/MyNs +SKLx5vJh93aq7EyWurWVCzUfYDIFngb+WFINEGb2XYFD3Ah/y5D6kD24vooATgFJ +KQBQdCBWWjZbYS4nZUTOSJqSMp5V/RzjjKr9QpCKpXLvxwfQ9ME6MaobgfUSy1er +S+btHNItD/xA7/rO/eAfmlkFLJgGi6cfvnyBxHsR7+/T5yDyZAPbDwp8kErxFFeY +xqQmY6hvaSiQXek23AIiTOCuzyYluMsTnxRfQGlL/A77R+t7gJv5in0FPQqHzD1T +h5V4fIBcpOSmc6Qk7U7vYNi3AyvpWDsr03E+bNlY5dzNguxsO9QzJMWGd17VZiwc +Q2qkKxl5JC0c3h4mYh4e3V5C0c9z32ffYS6sICsNaAw0r8C2svULBCtm29oDDc5X +oC5EzfZb5vG9qpQ8H/LE2I215YOwsvmda9gdpnxrbk4y8MlSMOJwMXVYGKNSEg2h +wWPo/4qsNV8hX7IlOYPOnVRCzpjCWTc2CzvhHISxv1CrXKV0D2qAxkX/ezRYsaiE +T7E0K5kyXquVjhjnUTMjpOc/LM2bQ9v0lmmJIcnJFSlCTS5+DvUrngYSnqfyPgkS +7dbSuW8E1UusxnY8664snWugxCLB4E0PacKTr+0E77qukFAE02j8hwHbn+TplWaH +jOVSyLicsqwi0TTxWoWk/fTk6StSGVuUTaPPoz6gkrN3H01V8vhcx/PPqWWbfjjz +yGqOm6pXXaNwPYjgQUWFPc3QHOaPbTAl1Y4teCaAqnSDlEBYqDUGK0gSMAQ/vgdC +uvUAGctb2Kikp4sQcvzKRHsStHrHDVdeum842r5zyHKMuEbhTGjmDR70FknpDaoS +vQIfgasKqXjRKgQfSSiGTX5CsvYvSRqFSD4qDbQbYnR0XMNphJxK/3rWP86oEXk1 +-----END RSA PRIVATE KEY----- diff --git a/mitm/src/test/resources/net/lightbody/bmp/mitm/keystore.jks b/mitm/src/test/resources/net/lightbody/bmp/mitm/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..e886fe14c7f2e1afda756ebcb65b6fd5b7b4f6fc GIT binary patch literal 2191 zcmbW1c{tPy7sux}i3lSIbYCQCx@ploByB(mHrF|ss9UNK!;7;aw4(k1)a z*dnfM;Tj29CPs>c>|==t7gs&+`#krL_pkSl^T#=#^PJ~A-*cYtK6jrB0)fDX0sIKK zU%*Y83)RirE%>lx*+|U@0fAruJ{-D-7vO;_@BmOi9xMm|Fc35xI`2YzoVuLMsg^g` z2tq@M_L&p6zgQwVovU-WFL6qQm#(~wK10>`F{BVCHMYkl8cDR1sFPuJ0eiZYm;idL zW3oAqc#0=UX@x)I$)t5F&7o+T*1f^ZiyN9V_wzZEy$5XujknE0fz0l7$9I1o1#T-yZ0;em>N%)yD9

ZKdtkxagO11uZpl9?_#^kLh|jiU;mY1{ z$;ma_VCfd8*4fX@%JlWdU7RxM7wO>pE7<|8O^hq33C(M+oDxlveq=Q$x=%Q6RYn@> zLF|=*8!OU(cdMAtFSPlit8G9;Ab*=H~URL^9W*6176|VMsX08SJc~Z%9@Q(%48c z$k2Vh|5ngTXKa(s8AR1Iy)aP~Q-SM#qS>H(DvlEZx{+p1w04c|;@pS1_|Vnhqejd) z+KQLG+)GXc-JQWVrZ5%JU2c#PEkx$Yty6&3r)z8qxW0GwMcgezHhYY|5aZtcw@sHz zE3GIsrjQ!;=>SXcF%8Sl1y8JDxTaTE(B?H?$K_e(Txd-Q`T~2QV(Z~+7F2GdLF=T? z!q<}ej~Dbft~0P&kfWxtsBi*MiOujRf0ab$Ypi>g)>?U^y~oN3=UX-68gen$+g3bx zX|}&-CmGQoR3$#wuON_uEY{3nDz5d~v#u}(O%mhknXR&}XIdE7M#5?hNgo5(N)}rP zgTF7X{E131Uh|ocInvO185t$Xv@)ri6j-i#>P_)Bk&Zm;pR}tK++FI3pqjq#V(R5+ z3+ooM9}cs{EV2?w2dkHxPQWD|9@M4EE+Lo23TpV!W7gSC5Ad~uEy)HP+-07Zof3fv zXZtIOgTi7Xp(?CV^0>iKei9e<0=1eo2k8<@TUm+zInW5v?ER_ggD*(pjLfT`^m`^3 zPdXhjxZ1O{Smq^w@&%TMTML{tF<93r)ndB$iH=ksQ6-YT_&ZE!p=XeS@MNi4u_bmE zEwZ^Yjg54tGTgUu4j=wvs8{K7$P?sc?huU2`*^)+5ePl{i1XMm17Z8a9ARXfI$je5 zf*9b#A-ec*@ab#_7yyI$K-F(FlxhsKQFwkHKKW?l=q-K#I0^&d3Cd!43=hN>3P+$! zZ&IlgH&TG_onR@d+imK95d?vv{6K!+k>YoRKaSy7R##S0*1)SM<5e{5G5jjuQ}zF( z|M%Q*;N(A7JalnLIDiDO;)Cn!VDZyd*baLOqQ`3r)E=^hNv01&wNJGXS*XRM44X!RkhI(03`ZfZ4>sGpt zBC#4)#5g5~qKZ$e^Pe%VJf$h$9trm-znY7Y7@FDoV%AhmHaqoJo%K5DAxkTJb&Fp6 z6p`wkF}37@YI&!?fv+Q+2fLF?auSIJn?}^)rmD*xcIRy1R_n^sRcBt5K+A3^;86O5 z&M%1Y>eJWTx;Bc|&Gc4zN?&{SM1YLvOkC%4HAQ3%%`{eq@>b_1(H#tj z6V|D3cBA4(thlKJt5_auyIPYdG?;|AhUs!{nyFSKA*PJ>p>}N>dO&!SESEgiLx=y4>B5= z*VywL5P_~~{sd-sZ)zh@v*ptMJNl-jJm<^Z z-~6ZY`J^{_=&2>Afni&wsopj=xpXM|AqO(xk|bCno4AQ0iZ??~M4ZP2NRt*rE_nV8 DK{MB% literal 0 HcmV?d00001 diff --git a/mitm/src/test/resources/net/lightbody/bmp/mitm/keystore.p12 b/mitm/src/test/resources/net/lightbody/bmp/mitm/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..6667aa3f6622789cc8224f932e814621a3d28213 GIT binary patch literal 2514 zcmY+^c{CJ?7YFbe#xi4jSxSweG(ut)YonKSY&~NuTiHn@WT%wIqZwOBWXp)`3E7v4 zEMqJqBg;buMb<%OU*^?0zu)h@-yiqfbME(^```T_@n9MokR6E!r*MMLM;k>wV0e6ZQM!r?atIHCH~EefJfCPKlN+?=96me;sl2 zXQk9sg7xnAwTXe>O)ic=3!lp{9wJDO9{KDMe0qeOlp%Ij84ZjnfzX^8dB|<-*aN7B znl)ysR0Q_I=xF#860uB@6-z%`+l3ZtL)5qU8_mzCGaHI}HpSvgca%Qo{E?C_Q*qPl zMbM3|ZQj%r7Chy{zQPA@p~-~+hue+gJ!!MJ{A;m<=mrL5jRBRy)+pp7TCFdyR9?iQ0z z(A4F2EL#b3URSm2@_u@9Q8wE_EdeG$5Igqxi9qXmi0zR~?@bvStem%0Z$;2_T|=V~ zf@kSW$3dS)j>(ifZC5NmXm0Q}36 zFgVPRue0>CVUe#xiC6A=ggL58X|<$f==c} zs*N1naf$Y^)~-m?#a}{nYW;Ms}JsQVDs{0!XSUa z{0zkBl{(ik2B}|^K>iV@-zW8)edBJm@?J)CdQUa`b#VYr|yt9HQME<72Ak&LU! z-q;F?Fu@$)$7)_x9`*~X}!;Pmv5sj zo=THS+Fx|CtgIXGd2m{e_vkpy-(=Y$%I5eJW-`mxfBY}vt@pN>VjW`-4k22&e;I%P*?xM#{7J~t|LMT~ONW>DV^`n;!m+<}0O3L1wD21{#I8cK3sB*x zgm$?yJ<7)E6h8yw+|dU5>;dIl7}aPqn6jx*q>`POmUyKAW3C$ezC^5+;K19B$X2Sn zU8%Sv)YeQ%lT_TO*K9x+Pff1_@Nbh>6 zAkp!%c*amMu`RX|lUoT}s~A$x_gjb9-Lqn$60T*60FpRL!_@pUbj#&e)Gck25P$dv z&-Q$U^?G(He1+1KX!|vK{1Z*6}vM#ymJ|t)!MX$ciOT^T|#6by*}QHbqUxHyST1 zTpwzPJk#*5p$FC0lk_@(U(G9cL`m@>=u8d7li#l16uPgid0S(h^r(eH@j_=!XKQ3x zmllyyz~qExns9t2=S%IFxKxQh~%D= zu3mPuXtR%^fieE`!4A1yIJ4F{FX9y0P(n(%n3HWnDh1?}N* zMMDy`{143-hgIDwYJ7BS^iuO}1&fVgMpH(UW_S)#)R*%Of8b=tX|uS zCf+kVPB%&rHZ(DPS8a0Fw|(%!Y4fb+2r8vi(bn)*gmkk?^K{Rf(Ip&Farlm?vW&4E-no41^59x06_pJKmfoEaOG!({(DITbsQv|FKy@m z)7>a^Er2$=dR literal 0 HcmV?d00001 diff --git a/mitm/src/test/resources/net/lightbody/bmp/mitm/unencrypted-private-key.key b/mitm/src/test/resources/net/lightbody/bmp/mitm/unencrypted-private-key.key new file mode 100644 index 000000000..ff3fb4b16 --- /dev/null +++ b/mitm/src/test/resources/net/lightbody/bmp/mitm/unencrypted-private-key.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA2vcm/qETdg92EjYka3Zt+UEraLKzTovzgvVx4F0EmCC3KDU2 +v2+WeX4wmVjP6qtMr6nJOSYkFedAvIRGi7hbf3JKQ1nqhH1qU2zP9HXNt+/LCw8k +OFJFii9cp6/h8OnlF8hoIWz4lRTMMcjoiBzV5vfyTb2zWE0l1DXGypKTxjqg8Pd/ +tqsMl2uc4+xnEL/ZK9cK8wxg0suUqaGQRaX2R3SovbFKZ1c3VApS8zHksSm6qQSt +bisuEEHSYLpFCrMnXsLJ9KfzTUDwBqrKaMyDAEjoS2LpoijFYalTW3bF721GiYl2 +GtcwCIzqpHcIbHAPn1PxQY4UHUt3z4YSjTsuXwIDAQABAoIBAB/Opyt12o3b0Rr0 +InY5zd/XR6b9zm4qhkUPwmsFGBXBKtn8YOeOHh2n5wdfj1RXbdxWnZRfpf5IiW7Z +CCZjsWbiA0elWBvG3BsiQ1MPicKeYrBIkspbqR5Zouv48Kk+ULkTs4ynd7SwQLk6 +pgyfo7LZcak5VUQOcOBSr33drPmuZcTyozuEs+XmOaUIH9SHr5dLP3mysb7dEZQS +gaHMR1a3BQQrQk9H5oVVU0kI0iqqX5NltiWodl3shUHqlXrlCEop+RxHhkfBgz8H +w4MlyFSiYrrzTyL7yTkMa6JTcP0s4oNH2E+foIMyf46z/lxH7Nk0bg4pmsfXb0o7 +bKqJzAECgYEA8kRvRpDB9z6ddQkJ1I++fGh8vJMUuB6MNlS7Shhp6ClHQwdUK80f +dpWT8CxIOX6sKJgm9HGbxaPxn7RnYc4v+M0L/QsPU8RGfVggNvQ9xQEaocp3CD9D +TUeZUtTcTmKd86h9ICjlkbtF2coKNcIfM3teVtGnMKjf5VowupYyzj8CgYEA52CU +6Su4ewgTcEPRqWWyg/GHTPN8TJaXjutgvhnt7w8po8xMmIK1qSeOP4paF/UOuGsT +rX6IoXmSzSz5MkFkjGLBd/wUd4p9YQbgjtv/lv52XBZ8mqffjDGTGl/n0r803yMH +So7Fup9f8kXkgw+vszxzCqnHq3usx1WjCKNj1+ECgYAT1wTh24L283rDldznOmpY +F9p3OvhMZ7wFywSXec5ag97hH12GRMMZ3AAEgCveAYCpxmQSSqd+FQH5mTWKLe+B +yZD8xQYZTw6Svz/MIE5ars92hnUfCMdDMeTdgq8UAEF9LcQpeQ/r0lFTF5ekdWRG +vAiqxXqSopHLX4p0DU7V0wKBgGi16c44Tg3H0tw8pPbPomFZ/gxSKM+UW1R/q1F8 +9JP6vbJ2M7fVd5bs4tBYsXskGRxWwRoEKJtDJK+cCc63j2SFEN9XAoAy+ZjefuPI +JjxUPoZgWtW24VFV4ifOfWB/zdKpzJPuVwelNsuy276Aa9hmo/2QZl9x4fh4BgdT +wkyhAoGBAKrzqA0+kotqocA9cT7GuJb7mfjewXZx1jztQMC5mHs+L/tRGL85dZZk +5hK8Rtessw5TKmfA53bKbOEklEcsjSNWgIq3wYjJVtBDUvcmA2Hgm3psjUCrl1iv +h/31bf05fYaWFFEeuHE/Pz5ev8ecY8gsKtX63HVVWLYtDxKnYuXk +-----END RSA PRIVATE KEY----- diff --git a/pom.xml b/pom.xml index be39e6b09..c9f45bba1 100644 --- a/pom.xml +++ b/pom.xml @@ -8,6 +8,7 @@ browsermob-rest browsermob-core-littleproxy browsermob-dist + mitm BrowserMob Proxy Parent Project A programmatic HTTP/S designed for performance and functional testing @@ -68,6 +69,8 @@ 2.4.3-01 4.0.33.Final + + 1.52 @@ -264,7 +267,7 @@ net.lightbody.bmp littleproxy - 1.1.0-beta-bmp-9 + 1.1.0-beta-bmp-10 @@ -396,6 +399,18 @@ ${netty.version} + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle.version} + + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + +