blob: 571bec14086b07b0272dd6935c5175187aab9440 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.util;
import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.SecureRandomSpi;
import java.security.UnrecoverableKeyException;
import java.util.Random;
import java.util.regex.Pattern;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.apache.lucene.util.Constants;
import org.apache.solr.client.solrj.embedded.SSLConfig;
import org.apache.solr.client.solrj.impl.HttpClientUtil;
import org.apache.solr.client.solrj.impl.HttpClientUtil.SocketFactoryRegistryProvider;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.security.CertificateUtils;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import com.carrotsearch.randomizedtesting.RandomizedTest;
/**
* An SSLConfig that provides {@link SSLConfig} and {@link SocketFactoryRegistryProvider} for both clients and servers
* that supports reading key/trust store information directly from resource files provided with the
* Solr test-framework classes
*/
public class SSLTestConfig {
private static final String TEST_KEYSTORE_BOGUSHOST_RESOURCE = "SSLTestConfig.hostname-and-ip-missmatch.keystore";
private static final String TEST_KEYSTORE_LOCALHOST_RESOURCE = "SSLTestConfig.testing.keystore";
private static final String TEST_PASSWORD = "secret";
private final boolean checkPeerName;
private final Resource keyStore;
private final Resource trustStore;
private final boolean useSsl;
private final boolean clientAuth;
/** Creates an SSLTestConfig that does not use SSL or client authentication */
public SSLTestConfig() {
this(false, false);
}
/**
* Create an SSLTestConfig based on a few caller specified options,
* implicitly assuming <code>checkPeerName=false</code>.
* <p>
* As needed, keystore/truststore information will be pulled from a hardcoded resource
* file provided by the solr test-framework
* </p>
*
* @param useSSL - whether SSL should be required.
* @param clientAuth - whether client authentication should be required.
*/
public SSLTestConfig(boolean useSSL, boolean clientAuth) {
this(useSSL, clientAuth, false);
}
// NOTE: if any javadocs below change, update create-keystores.sh
/**
* Create an SSLTestConfig based on a few caller specified options. As needed,
* keystore/truststore information will be pulled from a hardcoded resource files provided
* by the solr test-framework based on the value of <code>checkPeerName</code>:
* <ul>
* <li><code>true</code> - A keystore resource file will be used that specifies
* a CN of <code>localhost</code> and a SAN IP of <code>127.0.0.1</code>, to
* ensure that all connections should be valid regardless of what machine runs the tests.</li>
* <li><code>false</code> - A keystore resource file will be used that specifies
* a bogus hostname in the CN and reserved IP as the SAN, since no (valid) tests using this
* SSLTestConfig should care what CN/SAN are.</li>
* </ul>
*
* @param useSSL - whether SSL should be required.
* @param clientAuth - whether client authentication should be required.
* @param checkPeerName - whether the client should validate the 'peer name' of the SSL Certificate (and which testing Cert should be used)
* @see HttpClientUtil#SYS_PROP_CHECK_PEER_NAME
*/
public SSLTestConfig(boolean useSSL, boolean clientAuth, boolean checkPeerName) {
this.useSsl = useSSL;
this.clientAuth = clientAuth;
this.checkPeerName = checkPeerName;
if (useSsl) {
assumeSslIsSafeToTest();
}
final String resourceName = checkPeerName
? TEST_KEYSTORE_LOCALHOST_RESOURCE : TEST_KEYSTORE_BOGUSHOST_RESOURCE;
trustStore = keyStore = Resource.newClassPathResource(resourceName);
if (null == keyStore || ! keyStore.exists() ) {
throw new IllegalStateException("Unable to locate keystore resource file in classpath: "
+ resourceName);
}
}
/** If true, then servers hostname/ip should be validated against the SSL Cert metadata */
public boolean getCheckPeerName() {
return checkPeerName;
}
/** All other settings on this object are ignored unless this is true */
public boolean isSSLMode() {
return useSsl;
}
public boolean isClientAuthMode() {
return clientAuth;
}
/**
* Creates a {@link SocketFactoryRegistryProvider} for HTTP <b>clients</b> to use when communicating with servers
* which have been configured based on the settings of this object. When {@link #isSSLMode} is true, this
* <code>SocketFactoryRegistryProvider</code> will <i>only</i> support HTTPS (no HTTP scheme) using the
* appropriate certs. When {@link #isSSLMode} is false, <i>only</i> HTTP (no HTTPS scheme) will be
* supported.
*/
public SocketFactoryRegistryProvider buildClientSocketFactoryRegistryProvider() {
if (isSSLMode()) {
SSLConnectionSocketFactory sslConnectionFactory = buildClientSSLConnectionSocketFactory();
assert null != sslConnectionFactory;
return new SSLSocketFactoryRegistryProvider(sslConnectionFactory);
} else {
return HTTP_ONLY_SCHEMA_PROVIDER;
}
}
/**
* Builds a new SSLContext for HTTP <b>clients</b> to use when communicating with servers which have
* been configured based on the settings of this object.
*
* NOTE: Uses a completely insecure {@link SecureRandom} instance to prevent tests from blocking
* due to lack of entropy, also explicitly allows the use of self-signed
* certificates (since that's what is almost always used during testing).
*/
public SSLContext buildClientSSLContext() throws KeyManagementException,
UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
assert isSSLMode();
SSLContextBuilder builder = SSLContexts.custom();
builder.setSecureRandom(NotSecurePsuedoRandom.INSTANCE);
// NOTE: KeyStore & TrustStore are swapped because they are from configured from server perspective...
// we are a client - our keystore contains the keys the server trusts, and vice versa
builder.loadTrustMaterial(buildKeyStore(keyStore, TEST_PASSWORD), new TrustSelfSignedStrategy()).build();
if (isClientAuthMode()) {
builder.loadKeyMaterial(buildKeyStore(trustStore, TEST_PASSWORD), TEST_PASSWORD.toCharArray());
}
return builder.build();
}
public SSLConfig buildClientSSLConfig() {
if (!isSSLMode()) {
return null;
}
return new SSLConfig(isSSLMode(), isClientAuthMode(), null, null, null, null) {
@Override
public SslContextFactory.Client createClientContextFactory() {
SslContextFactory.Client factory = new SslContextFactory.Client(!checkPeerName);
try {
factory.setSslContext(buildClientSSLContext());
} catch (KeyManagementException | UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException e) {
throw new IllegalStateException("Unable to setup https scheme for HTTPClient to test SSL.", e);
}
return factory;
}
};
}
/**
* Builds a new SSLContext for jetty servers which have been configured based on the settings of
* this object.
*
* NOTE: Uses a completely insecure {@link SecureRandom} instance to prevent tests from blocking
* due to lack of entropy, also explicitly allows the use of self-signed
* certificates (since that's what is almost always used during testing).
* almost always used during testing).
*/
public SSLConfig buildServerSSLConfig() {
if (!isSSLMode()) {
return null;
}
return new SSLConfig(isSSLMode(), isClientAuthMode(), null, null, null, null) {
@Override
public SslContextFactory.Server createContextFactory() {
SslContextFactory.Server factory = new SslContextFactory.Server();
try {
SSLContextBuilder builder = SSLContexts.custom();
builder.setSecureRandom(NotSecurePsuedoRandom.INSTANCE);
builder.loadKeyMaterial(buildKeyStore(keyStore, TEST_PASSWORD), TEST_PASSWORD.toCharArray());
if (isClientAuthMode()) {
builder.loadTrustMaterial(buildKeyStore(trustStore, TEST_PASSWORD), new TrustSelfSignedStrategy()).build();
}
factory.setSslContext(builder.build());
} catch (Exception e) {
throw new RuntimeException("ssl context init failure: " + e.getMessage(), e);
}
factory.setNeedClientAuth(isClientAuthMode());
return factory;
}
};
}
/**
* Constructs a KeyStore using the specified filename and password
*/
private static KeyStore buildKeyStore(Resource resource, String password) {
try {
return CertificateUtils.getKeyStore(resource, "JKS", null, password);
} catch (Exception ex) {
throw new IllegalStateException("Unable to build KeyStore from resource: " + resource.getName(), ex);
}
}
/**
* Constructs a new SSLConnectionSocketFactory for HTTP <b>clients</b> to use when communicating
* with servers which have been configured based on the settings of this object. Will return null
* unless {@link #isSSLMode} is true.
*/
public SSLConnectionSocketFactory buildClientSSLConnectionSocketFactory() {
if (!isSSLMode()) {
return null;
}
SSLConnectionSocketFactory sslConnectionFactory;
try {
SSLContext sslContext = buildClientSSLContext();
if (checkPeerName == false) {
sslConnectionFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
} else {
sslConnectionFactory = new SSLConnectionSocketFactory(sslContext);
}
} catch (KeyManagementException | UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException e) {
throw new IllegalStateException("Unable to setup https scheme for HTTPClient to test SSL.", e);
}
return sslConnectionFactory;
}
/** A SocketFactoryRegistryProvider that only knows about SSL using a specified SSLConnectionSocketFactory */
private static class SSLSocketFactoryRegistryProvider extends SocketFactoryRegistryProvider {
private final SSLConnectionSocketFactory sslConnectionFactory;
public SSLSocketFactoryRegistryProvider(SSLConnectionSocketFactory sslConnectionFactory) {
this.sslConnectionFactory = sslConnectionFactory;
}
@Override
public Registry<ConnectionSocketFactory> getSocketFactoryRegistry() {
return RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", sslConnectionFactory).build();
}
}
/** A SocketFactoryRegistryProvider that only knows about HTTP */
private static final SocketFactoryRegistryProvider HTTP_ONLY_SCHEMA_PROVIDER = new SocketFactoryRegistryProvider() {
@Override
public Registry<ConnectionSocketFactory> getSocketFactoryRegistry() {
return RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory()).build();
}
};
/**
* A mocked up instance of SecureRandom that just uses {@link Random} under the covers.
* This is to prevent blocking issues that arise in platform default
* SecureRandom instances due to too many instances / not enough random entropy.
* Tests do not need secure SSL.
*/
private static class NotSecurePsuedoRandom extends SecureRandom {
public static final SecureRandom INSTANCE = new NotSecurePsuedoRandom();
private static final Random RAND = new Random(42);
/**
* Helper method that can be used to fill an array with non-zero data.
* (Attempted workarround of Solaris SSL Padding bug: SOLR-9068)
*/
private static final byte[] fillData(byte[] data) {
RAND.nextBytes(data);
return data;
}
/** SPI Used to init all instances */
private static final SecureRandomSpi NOT_SECURE_SPI = new SecureRandomSpi() {
/** returns a new byte[] filled with static data */
public byte[] engineGenerateSeed(int numBytes) {
return fillData(new byte[numBytes]);
}
/** fills the byte[] with static data */
public void engineNextBytes(byte[] bytes) {
fillData(bytes);
}
/** NOOP */
public void engineSetSeed(byte[] seed) { /* NOOP */ }
};
private NotSecurePsuedoRandom() {
super(NOT_SECURE_SPI, null) ;
}
/** returns a new byte[] filled with static data */
public byte[] generateSeed(int numBytes) {
return fillData(new byte[numBytes]);
}
/** fills the byte[] with static data */
synchronized public void nextBytes(byte[] bytes) {
fillData(bytes);
}
/** NOOP */
synchronized public void setSeed(byte[] seed) { /* NOOP */ }
/** NOOP */
synchronized public void setSeed(long seed) { /* NOOP */ }
}
/**
* Helper method for sanity checking if it's safe to use SSL on this JVM
*
* @see <a href="https://issues.apache.org/jira/browse/SOLR-12988">SOLR-12988</a>
* @throws org.junit.internal.AssumptionViolatedException if this JVM is known to have SSL problems
*/
public static void assumeSslIsSafeToTest() {
if (Constants.JVM_NAME.startsWith("OpenJDK") ||
Constants.JVM_NAME.startsWith("Java HotSpot(TM)")) {
RandomizedTest.assumeFalse("Test (or randomization for this seed) wants to use SSL, " +
"but SSL is known to fail on your JVM: " +
Constants.JVM_NAME + " / " + Constants.JVM_VERSION,
isOpenJdkJvmVersionKnownToHaveProblems(Constants.JVM_VERSION));
}
}
/**
* package visibility for tests
* @see Constants#JVM_VERSION
* @lucene.internal
*/
static boolean isOpenJdkJvmVersionKnownToHaveProblems(final String jvmVersion) {
// TODO: would be nice to replace with Runtime.Version once we don't have to
// worry about java8 support when backporting to branch_8x
return KNOWN_BAD_OPENJDK_JVMS.matcher(jvmVersion).matches();
}
private static final Pattern KNOWN_BAD_OPENJDK_JVMS
= Pattern.compile(// 11 to 11.0.2 were all definitely problematic
// - https://bugs.openjdk.java.net/browse/JDK-8212885
// - https://bugs.openjdk.java.net/browse/JDK-8213202
"(^11(\\.0(\\.0|\\.1|\\.2)?)?($|(\\_|\\+|\\-).*$))|" +
// early (pre-ea) "testing" builds of 11, 12, and 13 were also buggy
// - https://bugs.openjdk.java.net/browse/JDK-8224829
"(^(11|12|13).*-testing.*$)|" +
// So far, all 13-ea builds (up to 13-ea-26) have been buggy
// - https://bugs.openjdk.java.net/browse/JDK-8226338
"(^13-ea.*$)"
);
}