blob: d168a317dfd7e78388edff3b0556bd4718a6c7f6 [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.hadoop.security.ssl;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.authentication.client.ConnectionConfigurator;
import org.apache.hadoop.util.ReflectionUtils;
import org.apache.hadoop.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.hadoop.util.PlatformName.IBM_JAVA;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
/**
* Factory that creates SSLEngine and SSLSocketFactory instances using
* Hadoop configuration information.
* <p>
* This SSLFactory uses a {@link ReloadingX509TrustManager} instance,
* which reloads public keys if the truststore file changes.
* <p>
* This factory is used to configure HTTPS in Hadoop HTTP based endpoints, both
* client and server.
*/
@InterfaceAudience.Private
@InterfaceStability.Evolving
public class SSLFactory implements ConnectionConfigurator {
static final Logger LOG = LoggerFactory.getLogger(SSLFactory.class);
@InterfaceAudience.Private
public enum Mode { CLIENT, SERVER }
public static final String SSL_CLIENT_CONF_KEY = "hadoop.ssl.client.conf";
public static final String SSL_CLIENT_CONF_DEFAULT = "ssl-client.xml";
public static final String SSL_SERVER_CONF_KEY = "hadoop.ssl.server.conf";
public static final String SSL_SERVER_CONF_DEFAULT = "ssl-server.xml";
public static final String SSL_REQUIRE_CLIENT_CERT_KEY =
"hadoop.ssl.require.client.cert";
public static final boolean SSL_REQUIRE_CLIENT_CERT_DEFAULT = false;
public static final String SSL_HOSTNAME_VERIFIER_KEY =
"hadoop.ssl.hostname.verifier";
public static final String SSL_ENABLED_PROTOCOLS_KEY =
"hadoop.ssl.enabled.protocols";
public static final String SSL_ENABLED_PROTOCOLS_DEFAULT =
"TLSv1.2";
public static final String SSL_SERVER_NEED_CLIENT_AUTH =
"ssl.server.need.client.auth";
public static final boolean SSL_SERVER_NEED_CLIENT_AUTH_DEFAULT = false;
public static final String SSL_SERVER_KEYSTORE_LOCATION =
"ssl.server.keystore.location";
public static final String SSL_SERVER_KEYSTORE_PASSWORD =
"ssl.server.keystore.password";
public static final String SSL_SERVER_KEYSTORE_TYPE =
"ssl.server.keystore.type";
public static final String SSL_SERVER_KEYSTORE_TYPE_DEFAULT = "jks";
public static final String SSL_SERVER_KEYSTORE_KEYPASSWORD =
"ssl.server.keystore.keypassword";
public static final String SSL_SERVER_TRUSTSTORE_LOCATION =
"ssl.server.truststore.location";
public static final String SSL_SERVER_TRUSTSTORE_PASSWORD =
"ssl.server.truststore.password";
public static final String SSL_SERVER_TRUSTSTORE_TYPE =
"ssl.server.truststore.type";
public static final String SSL_SERVER_TRUSTSTORE_TYPE_DEFAULT = "jks";
public static final String SSL_SERVER_EXCLUDE_CIPHER_LIST =
"ssl.server.exclude.cipher.list";
public static final String SSLCERTIFICATE = IBM_JAVA?"ibmX509":"SunX509";
public static final String KEYSTORES_FACTORY_CLASS_KEY =
"hadoop.ssl.keystores.factory.class";
private Configuration conf;
private Mode mode;
private boolean requireClientCert;
private SSLContext context;
// the java keep-alive cache relies on instance equivalence of the SSL socket
// factory. in many java versions, SSLContext#getSocketFactory always
// returns a new instance which completely breaks the cache...
private SSLSocketFactory socketFactory;
private HostnameVerifier hostnameVerifier;
private KeyStoresFactory keystoresFactory;
private String[] enabledProtocols = null;
private List<String> excludeCiphers;
/**
* Creates an SSLFactory.
*
* @param mode SSLFactory mode, client or server.
* @param conf Hadoop configuration from where the SSLFactory configuration
* will be read.
*/
public SSLFactory(Mode mode, Configuration conf) {
this.conf = conf;
if (mode == null) {
throw new IllegalArgumentException("mode cannot be NULL");
}
this.mode = mode;
Configuration sslConf = readSSLConfiguration(conf, mode);
requireClientCert = sslConf.getBoolean(SSL_REQUIRE_CLIENT_CERT_KEY,
SSL_REQUIRE_CLIENT_CERT_DEFAULT);
Class<? extends KeyStoresFactory> klass
= conf.getClass(KEYSTORES_FACTORY_CLASS_KEY,
FileBasedKeyStoresFactory.class, KeyStoresFactory.class);
keystoresFactory = ReflectionUtils.newInstance(klass, sslConf);
enabledProtocols = conf.getStrings(SSL_ENABLED_PROTOCOLS_KEY,
SSL_ENABLED_PROTOCOLS_DEFAULT);
excludeCiphers = Arrays.asList(
sslConf.getTrimmedStrings(SSL_SERVER_EXCLUDE_CIPHER_LIST));
if (LOG.isDebugEnabled()) {
LOG.debug("will exclude cipher suites: {}",
StringUtils.join(",", excludeCiphers));
}
}
public static Configuration readSSLConfiguration(Configuration conf,
Mode mode) {
Configuration sslConf = new Configuration(false);
sslConf.setBoolean(SSL_REQUIRE_CLIENT_CERT_KEY, conf.getBoolean(
SSL_REQUIRE_CLIENT_CERT_KEY, SSL_REQUIRE_CLIENT_CERT_DEFAULT));
String sslConfResource;
if (mode == Mode.CLIENT) {
sslConfResource = conf.get(SSL_CLIENT_CONF_KEY,
SSL_CLIENT_CONF_DEFAULT);
} else {
sslConfResource = conf.get(SSL_SERVER_CONF_KEY,
SSL_SERVER_CONF_DEFAULT);
}
sslConf.addResource(sslConfResource);
// Only fallback to input config if classpath SSL config does not load for
// backward compatibility.
if (sslConf.getResource(sslConfResource) == null) {
LOG.debug("{} can't be loaded form classpath, fallback using SSL" +
" config from input configuration.", sslConfResource);
sslConf = conf;
}
return sslConf;
}
/**
* Initializes the factory.
*
* @throws GeneralSecurityException thrown if an SSL initialization error
* happened.
* @throws IOException thrown if an IO error happened while reading the SSL
* configuration.
*/
public void init() throws GeneralSecurityException, IOException {
keystoresFactory.init(mode);
context = SSLContext.getInstance("TLS");
context.init(keystoresFactory.getKeyManagers(),
keystoresFactory.getTrustManagers(), null);
context.getDefaultSSLParameters().setProtocols(enabledProtocols);
if (mode == Mode.CLIENT) {
socketFactory = context.getSocketFactory();
}
hostnameVerifier = getHostnameVerifier(conf);
}
private HostnameVerifier getHostnameVerifier(Configuration conf)
throws GeneralSecurityException, IOException {
return getHostnameVerifier(StringUtils.toUpperCase(
conf.get(SSL_HOSTNAME_VERIFIER_KEY, "DEFAULT").trim()));
}
public static HostnameVerifier getHostnameVerifier(String verifier)
throws GeneralSecurityException, IOException {
HostnameVerifier hostnameVerifier;
if (verifier.equals("DEFAULT")) {
hostnameVerifier = SSLHostnameVerifier.DEFAULT;
} else if (verifier.equals("DEFAULT_AND_LOCALHOST")) {
hostnameVerifier = SSLHostnameVerifier.DEFAULT_AND_LOCALHOST;
} else if (verifier.equals("STRICT")) {
hostnameVerifier = SSLHostnameVerifier.STRICT;
} else if (verifier.equals("STRICT_IE6")) {
hostnameVerifier = SSLHostnameVerifier.STRICT_IE6;
} else if (verifier.equals("ALLOW_ALL")) {
hostnameVerifier = SSLHostnameVerifier.ALLOW_ALL;
} else {
throw new GeneralSecurityException("Invalid hostname verifier: " +
verifier);
}
return hostnameVerifier;
}
/**
* Releases any resources being used.
*/
public void destroy() {
keystoresFactory.destroy();
}
/**
* Returns the SSLFactory KeyStoresFactory instance.
*
* @return the SSLFactory KeyStoresFactory instance.
*/
public KeyStoresFactory getKeystoresFactory() {
return keystoresFactory;
}
/**
* Returns a configured SSLEngine.
*
* @return the configured SSLEngine.
* @throws GeneralSecurityException thrown if the SSL engine could not
* be initialized.
* @throws IOException thrown if and IO error occurred while loading
* the server keystore.
*/
public SSLEngine createSSLEngine()
throws GeneralSecurityException, IOException {
SSLEngine sslEngine = context.createSSLEngine();
if (mode == Mode.CLIENT) {
sslEngine.setUseClientMode(true);
} else {
sslEngine.setUseClientMode(false);
sslEngine.setNeedClientAuth(requireClientCert);
disableExcludedCiphers(sslEngine);
}
sslEngine.setEnabledProtocols(enabledProtocols);
return sslEngine;
}
private void disableExcludedCiphers(SSLEngine sslEngine) {
String[] cipherSuites = sslEngine.getEnabledCipherSuites();
ArrayList<String> defaultEnabledCipherSuites =
new ArrayList<String>(Arrays.asList(cipherSuites));
Iterator iterator = excludeCiphers.iterator();
while(iterator.hasNext()) {
String cipherName = (String)iterator.next();
if(defaultEnabledCipherSuites.contains(cipherName)) {
defaultEnabledCipherSuites.remove(cipherName);
LOG.debug("Disabling cipher suite {}.", cipherName);
}
}
cipherSuites = defaultEnabledCipherSuites.toArray(
new String[defaultEnabledCipherSuites.size()]);
sslEngine.setEnabledCipherSuites(cipherSuites);
}
/**
* Returns a configured SSLServerSocketFactory.
*
* @return the configured SSLSocketFactory.
* @throws GeneralSecurityException thrown if the SSLSocketFactory could not
* be initialized.
* @throws IOException thrown if and IO error occurred while loading
* the server keystore.
*/
public SSLServerSocketFactory createSSLServerSocketFactory()
throws GeneralSecurityException, IOException {
if (mode != Mode.SERVER) {
throw new IllegalStateException(
"Factory is not in SERVER mode. Actual mode is " + mode.toString());
}
return context.getServerSocketFactory();
}
/**
* Returns a configured SSLSocketFactory.
*
* @return the configured SSLSocketFactory.
* @throws GeneralSecurityException thrown if the SSLSocketFactory could not
* be initialized.
* @throws IOException thrown if and IO error occurred while loading
* the server keystore.
*/
public SSLSocketFactory createSSLSocketFactory()
throws GeneralSecurityException, IOException {
if (mode != Mode.CLIENT) {
throw new IllegalStateException(
"Factory is not in CLIENT mode. Actual mode is " + mode.toString());
}
return socketFactory;
}
/**
* Returns the hostname verifier it should be used in HttpsURLConnections.
*
* @return the hostname verifier.
*/
public HostnameVerifier getHostnameVerifier() {
if (mode != Mode.CLIENT) {
throw new IllegalStateException(
"Factory is not in CLIENT mode. Actual mode is " + mode.toString());
}
return hostnameVerifier;
}
/**
* Returns if client certificates are required or not.
*
* @return if client certificates are required or not.
*/
public boolean isClientCertRequired() {
return requireClientCert;
}
/**
* If the given {@link HttpURLConnection} is an {@link HttpsURLConnection}
* configures the connection with the {@link SSLSocketFactory} and
* {@link HostnameVerifier} of this SSLFactory, otherwise does nothing.
*
* @param conn the {@link HttpURLConnection} instance to configure.
* @return the configured {@link HttpURLConnection} instance.
*
* @throws IOException if an IO error occurred.
*/
@Override
public HttpURLConnection configure(HttpURLConnection conn)
throws IOException {
if (conn instanceof HttpsURLConnection) {
HttpsURLConnection sslConn = (HttpsURLConnection) conn;
try {
sslConn.setSSLSocketFactory(createSSLSocketFactory());
} catch (GeneralSecurityException ex) {
throw new IOException(ex);
}
sslConn.setHostnameVerifier(getHostnameVerifier());
conn = sslConn;
}
return conn;
}
}