/* * 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 java.io.File; import java.util.Random; 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 javax.net.ssl.SSLContext; import java.net.MalformedURLException; 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.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.SSLContexts; import org.apache.http.conn.ssl.SSLContextBuilder; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.solr.client.solrj.embedded.SSLConfig; import org.apache.solr.client.solrj.impl.HttpClientUtil; import org.apache.solr.client.solrj.impl.HttpClientUtil.SchemaRegistryProvider; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.security.CertificateUtils; import org.eclipse.jetty.util.ssl.SslContextFactory; /** * An {@link SSLConfig} that supports reading key/trust store information directly from resource * files provided with the Solr test-framework classes */ public class SSLTestConfig extends SSLConfig { /** @deprecated No longer used except by {@link #setSSLSystemProperties} */ public static File TEST_KEYSTORE = ExternalPaths.SERVER_HOME == null ? null : new File(ExternalPaths.SERVER_HOME, "../etc/test/solrtest.keystore"); /** @deprecated No longer used except by {@link #setSSLSystemProperties} */ private static String TEST_KEYSTORE_PATH = TEST_KEYSTORE != null && TEST_KEYSTORE.exists() ? TEST_KEYSTORE.getAbsolutePath() : null; private static final String TEST_KEYSTORE_RESOURCE = "SSLTestConfig.testing.keystore"; private static final String TEST_KEYSTORE_PASSWORD = "secret"; private final Resource keyStore; private final Resource trustStore; /** 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. As needed, * keystore/truststore information will be pulled from a hardocded resource file provided * by the solr test-framework. * * @param useSSL - wether SSL should be required. * @param clientAuth - whether client authentication should be required. */ public SSLTestConfig(boolean useSSL, boolean clientAuth) { super(useSSL, clientAuth, null, TEST_KEYSTORE_PASSWORD, null, TEST_KEYSTORE_PASSWORD); trustStore = keyStore = Resource.newClassPathResource(TEST_KEYSTORE_RESOURCE); if (null == keyStore || ! keyStore.exists() ) { throw new IllegalStateException("Unable to locate keystore resource file in classpath: " + TEST_KEYSTORE_RESOURCE); } } /** * Create an SSLTestConfig using explicit paths for files * @deprecated - use {@link SSLConfig} directly */ @Deprecated public SSLTestConfig(boolean useSSL, boolean clientAuth, String keyStore, String keyStorePassword, String trustStore, String trustStorePassword) { super(useSSL, clientAuth, keyStore, keyStorePassword, trustStore, trustStorePassword); this.keyStore = tryNewResource(keyStore, "KeyStore"); this.trustStore = tryNewResource(trustStore, "TrustStore"); } /** * Helper utility for building resources from arbitrary user input paths/urls * if input is null, returns null; otherwise attempts to build Resource and verifies that Resource exists. */ private static final Resource tryNewResource(String userInput, String type) { if (null == userInput) { return null; } Resource result; try { result = Resource.newResource(userInput); } catch (MalformedURLException e) { throw new IllegalArgumentException("Can't build " + type + " Resource: " + e.getMessage(), e); } if (! result.exists()) { throw new IllegalArgumentException(type + " Resource does not exist " + result.getName()); } return result; } /** NOTE: This method is meaningless unless you explicitly provide paths when constructing this instance * @see #SSLTestConfig(boolean,boolean,String,String,String,String) */ @Override public String getKeyStore() { return super.getKeyStore(); } /** NOTE: This method is meaningless unless you explicitly provide paths when constructing this instance * @see #SSLTestConfig(boolean,boolean,String,String,String,String) */ @Override public String getTrustStore() { return super.getTrustStore(); } /** * Creates a {@link SchemaRegistryProvider} 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>SchemaRegistryProvider</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 SchemaRegistryProvider buildClientSchemaRegistryProvider() { if (isSSLMode()) { SSLConnectionSocketFactory sslConnectionFactory = buildClientSSLConnectionSocketFactory(); assert null != sslConnectionFactory; return new SSLSchemaRegistryProvider(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, getKeyStorePassword()), new TrustSelfSignedStrategy()).build(); if (isClientAuthMode()) { builder.loadKeyMaterial(buildKeyStore(trustStore, getTrustStorePassword()), getTrustStorePassword().toCharArray()); } return builder.build(); } /** * 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 SSLContext buildServerSSLContext() throws KeyManagementException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { assert isSSLMode(); SSLContextBuilder builder = SSLContexts.custom(); builder.setSecureRandom(NotSecurePsuedoRandom.INSTANCE); builder.loadKeyMaterial(buildKeyStore(keyStore, getKeyStorePassword()), getKeyStorePassword().toCharArray()); if (isClientAuthMode()) { builder.loadTrustMaterial(buildKeyStore(trustStore, getTrustStorePassword()), new TrustSelfSignedStrategy()).build(); } return builder.build(); } /** * Returns an SslContextFactory using {@link #buildServerSSLContext} if SSL should be used, else returns null. */ @Override public SslContextFactory createContextFactory() { if (!isSSLMode()) { return null; } // else... SslContextFactory factory = new SslContextFactory(false); try { factory.setSslContext(buildServerSSLContext()); } 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 */ protected 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 { boolean sslCheckPeerName = toBooleanDefaultIfNull(toBooleanObject(System.getProperty(HttpClientUtil.SYS_PROP_CHECK_PEER_NAME)), true); SSLContext sslContext = buildClientSSLContext(); if (sslCheckPeerName == false) { sslConnectionFactory = new SSLConnectionSocketFactory (sslContext, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); } 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 SchemaRegistryProvider that only knows about SSL using a specified SSLConnectionSocketFactory */ private static class SSLSchemaRegistryProvider extends SchemaRegistryProvider { private final SSLConnectionSocketFactory sslConnectionFactory; public SSLSchemaRegistryProvider(SSLConnectionSocketFactory sslConnectionFactory) { this.sslConnectionFactory = sslConnectionFactory; } @Override public Registry<ConnectionSocketFactory> getSchemaRegistry() { return RegistryBuilder.<ConnectionSocketFactory>create() .register("https", sslConnectionFactory).build(); } } /** A SchemaRegistryProvider that only knows about HTTP */ private static final SchemaRegistryProvider HTTP_ONLY_SCHEMA_PROVIDER = new SchemaRegistryProvider() { @Override public Registry<ConnectionSocketFactory> getSchemaRegistry() { return RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()).build(); } }; public static boolean toBooleanDefaultIfNull(Boolean bool, boolean valueIfNull) { if (bool == null) { return valueIfNull; } return bool.booleanValue() ? true : false; } public static Boolean toBooleanObject(String str) { if ("true".equalsIgnoreCase(str)) { return Boolean.TRUE; } else if ("false".equalsIgnoreCase(str)) { return Boolean.FALSE; } // no match return null; } /** * @deprecated this method has very little practical use, in most cases you'll want to use * {@link SSLContext#setDefault} with {@link #buildClientSSLContext} instead. */ @Deprecated public static void setSSLSystemProperties() { System.setProperty("javax.net.ssl.keyStore", TEST_KEYSTORE_PATH); System.setProperty("javax.net.ssl.keyStorePassword", TEST_KEYSTORE_PASSWORD); System.setProperty("javax.net.ssl.trustStore", TEST_KEYSTORE_PATH); System.setProperty("javax.net.ssl.trustStorePassword", TEST_KEYSTORE_PASSWORD); } /** * @deprecated this method has very little practical use, in most cases you'll want to use * {@link SSLContext#setDefault} with {@link #buildClientSSLContext} instead. */ @Deprecated public static void clearSSLSystemProperties() { System.clearProperty("javax.net.ssl.keyStore"); System.clearProperty("javax.net.ssl.keyStorePassword"); System.clearProperty("javax.net.ssl.trustStore"); System.clearProperty("javax.net.ssl.trustStorePassword"); } /** * 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 */ } } }