HADOOP-12886. Exclude weak ciphers in SSLFactory through ssl-server.xml. Contributed by Wei-Chiu Chuang.
(cherry picked from commit 9216993b02
)
Conflicts:
hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/SSLFactory.java
This commit is contained in:
parent
5cf55c5fb6
commit
2da1194699
|
@ -23,6 +23,8 @@ import org.apache.hadoop.conf.Configuration;
|
||||||
import org.apache.hadoop.security.authentication.client.ConnectionConfigurator;
|
import org.apache.hadoop.security.authentication.client.ConnectionConfigurator;
|
||||||
import org.apache.hadoop.util.ReflectionUtils;
|
import org.apache.hadoop.util.ReflectionUtils;
|
||||||
import org.apache.hadoop.util.StringUtils;
|
import org.apache.hadoop.util.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import static org.apache.hadoop.util.PlatformName.IBM_JAVA;
|
import static org.apache.hadoop.util.PlatformName.IBM_JAVA;
|
||||||
|
|
||||||
import javax.net.ssl.HostnameVerifier;
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
@ -34,6 +36,11 @@ import javax.net.ssl.SSLSocketFactory;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory that creates SSLEngine and SSLSocketFactory instances using
|
* Factory that creates SSLEngine and SSLSocketFactory instances using
|
||||||
|
@ -48,6 +55,7 @@ import java.security.GeneralSecurityException;
|
||||||
@InterfaceAudience.Private
|
@InterfaceAudience.Private
|
||||||
@InterfaceStability.Evolving
|
@InterfaceStability.Evolving
|
||||||
public class SSLFactory implements ConnectionConfigurator {
|
public class SSLFactory implements ConnectionConfigurator {
|
||||||
|
static final Logger LOG = LoggerFactory.getLogger(SSLFactory.class);
|
||||||
|
|
||||||
@InterfaceAudience.Private
|
@InterfaceAudience.Private
|
||||||
public static enum Mode { CLIENT, SERVER }
|
public static enum Mode { CLIENT, SERVER }
|
||||||
|
@ -70,6 +78,8 @@ public class SSLFactory implements ConnectionConfigurator {
|
||||||
public static final String SSL_ENABLED_PROTOCOLS =
|
public static final String SSL_ENABLED_PROTOCOLS =
|
||||||
"hadoop.ssl.enabled.protocols";
|
"hadoop.ssl.enabled.protocols";
|
||||||
public static final String DEFAULT_SSL_ENABLED_PROTOCOLS = "TLSv1";
|
public static final String DEFAULT_SSL_ENABLED_PROTOCOLS = "TLSv1";
|
||||||
|
public static final String SSL_SERVER_EXCLUDE_CIPHER_LIST =
|
||||||
|
"ssl.server.exclude.cipher.list";
|
||||||
|
|
||||||
private Configuration conf;
|
private Configuration conf;
|
||||||
private Mode mode;
|
private Mode mode;
|
||||||
|
@ -79,6 +89,7 @@ public class SSLFactory implements ConnectionConfigurator {
|
||||||
private KeyStoresFactory keystoresFactory;
|
private KeyStoresFactory keystoresFactory;
|
||||||
|
|
||||||
private String[] enabledProtocols = null;
|
private String[] enabledProtocols = null;
|
||||||
|
private List<String> excludeCiphers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an SSLFactory.
|
* Creates an SSLFactory.
|
||||||
|
@ -104,6 +115,14 @@ public class SSLFactory implements ConnectionConfigurator {
|
||||||
|
|
||||||
enabledProtocols = conf.getStrings(SSL_ENABLED_PROTOCOLS,
|
enabledProtocols = conf.getStrings(SSL_ENABLED_PROTOCOLS,
|
||||||
DEFAULT_SSL_ENABLED_PROTOCOLS);
|
DEFAULT_SSL_ENABLED_PROTOCOLS);
|
||||||
|
String excludeCiphersConf =
|
||||||
|
sslConf.get(SSL_SERVER_EXCLUDE_CIPHER_LIST, "");
|
||||||
|
if (excludeCiphersConf.isEmpty()) {
|
||||||
|
excludeCiphers = new LinkedList<String>();
|
||||||
|
} else {
|
||||||
|
LOG.debug("will exclude cipher suites: {}", excludeCiphersConf);
|
||||||
|
excludeCiphers = Arrays.asList(excludeCiphersConf.split(","));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Configuration readSSLConfiguration(Mode mode) {
|
private Configuration readSSLConfiguration(Mode mode) {
|
||||||
|
@ -194,11 +213,32 @@ public class SSLFactory implements ConnectionConfigurator {
|
||||||
} else {
|
} else {
|
||||||
sslEngine.setUseClientMode(false);
|
sslEngine.setUseClientMode(false);
|
||||||
sslEngine.setNeedClientAuth(requireClientCert);
|
sslEngine.setNeedClientAuth(requireClientCert);
|
||||||
|
disableExcludedCiphers(sslEngine);
|
||||||
}
|
}
|
||||||
sslEngine.setEnabledProtocols(enabledProtocols);
|
sslEngine.setEnabledProtocols(enabledProtocols);
|
||||||
return sslEngine;
|
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.
|
* Returns a configured SSLServerSocketFactory.
|
||||||
*
|
*
|
||||||
|
|
|
@ -17,24 +17,31 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.hadoop.security.ssl;
|
package org.apache.hadoop.security.ssl;
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
|
|
||||||
import org.apache.hadoop.conf.Configuration;
|
import org.apache.hadoop.conf.Configuration;
|
||||||
import org.apache.hadoop.fs.FileUtil;
|
import org.apache.hadoop.fs.FileUtil;
|
||||||
import org.apache.hadoop.fs.Path;
|
import org.apache.hadoop.fs.Path;
|
||||||
import org.apache.hadoop.security.alias.CredentialProvider;
|
|
||||||
import org.apache.hadoop.security.alias.CredentialProviderFactory;
|
import org.apache.hadoop.security.alias.CredentialProviderFactory;
|
||||||
import org.apache.hadoop.security.alias.JavaKeyStoreProvider;
|
import org.apache.hadoop.security.alias.JavaKeyStoreProvider;
|
||||||
|
import org.apache.hadoop.test.GenericTestUtils;
|
||||||
|
import org.apache.log4j.Level;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
import javax.net.ssl.SSLEngine;
|
||||||
|
import javax.net.ssl.SSLEngineResult;
|
||||||
|
import javax.net.ssl.SSLHandshakeException;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
@ -42,13 +49,21 @@ import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class TestSSLFactory {
|
public class TestSSLFactory {
|
||||||
|
private static final Logger LOG = LoggerFactory
|
||||||
|
.getLogger(TestSSLFactory.class);
|
||||||
private static final String BASEDIR =
|
private static final String BASEDIR =
|
||||||
System.getProperty("test.build.dir", "target/test-dir") + "/" +
|
System.getProperty("test.build.dir", "target/test-dir") + "/" +
|
||||||
TestSSLFactory.class.getSimpleName();
|
TestSSLFactory.class.getSimpleName();
|
||||||
private static final String KEYSTORES_DIR =
|
private static final String KEYSTORES_DIR =
|
||||||
new File(BASEDIR).getAbsolutePath();
|
new File(BASEDIR).getAbsolutePath();
|
||||||
private String sslConfsDir;
|
private String sslConfsDir;
|
||||||
|
private static final String excludeCiphers = "TLS_ECDHE_RSA_WITH_RC4_128_SHA,"
|
||||||
|
+ "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA,"
|
||||||
|
+ "SSL_RSA_WITH_DES_CBC_SHA,"
|
||||||
|
+ "SSL_DHE_RSA_WITH_DES_CBC_SHA,"
|
||||||
|
+ "SSL_RSA_EXPORT_WITH_RC4_40_MD5,"
|
||||||
|
+ "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA,"
|
||||||
|
+ "SSL_RSA_WITH_RC4_128_MD5";
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
|
@ -62,7 +77,7 @@ public class TestSSLFactory {
|
||||||
throws Exception {
|
throws Exception {
|
||||||
Configuration conf = new Configuration();
|
Configuration conf = new Configuration();
|
||||||
KeyStoreTestUtil.setupSSLConfig(KEYSTORES_DIR, sslConfsDir, conf,
|
KeyStoreTestUtil.setupSSLConfig(KEYSTORES_DIR, sslConfsDir, conf,
|
||||||
clientCert, trustStore);
|
clientCert, trustStore, excludeCiphers);
|
||||||
return conf;
|
return conf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,6 +140,120 @@ public class TestSSLFactory {
|
||||||
serverMode(true, false);
|
serverMode(true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void runDelegatedTasks(SSLEngineResult result, SSLEngine engine)
|
||||||
|
throws Exception {
|
||||||
|
Runnable runnable;
|
||||||
|
if (result.getHandshakeStatus() ==
|
||||||
|
SSLEngineResult.HandshakeStatus.NEED_TASK) {
|
||||||
|
while ((runnable = engine.getDelegatedTask()) != null) {
|
||||||
|
LOG.info("running delegated task...");
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
SSLEngineResult.HandshakeStatus hsStatus = engine.getHandshakeStatus();
|
||||||
|
if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK) {
|
||||||
|
throw new Exception("handshake shouldn't need additional tasks");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isEngineClosed(SSLEngine engine) {
|
||||||
|
return engine.isOutboundDone() && engine.isInboundDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkTransfer(ByteBuffer a, ByteBuffer b)
|
||||||
|
throws Exception {
|
||||||
|
a.flip();
|
||||||
|
b.flip();
|
||||||
|
assertTrue("transfer did not complete", a.equals(b));
|
||||||
|
|
||||||
|
a.position(a.limit());
|
||||||
|
b.position(b.limit());
|
||||||
|
a.limit(a.capacity());
|
||||||
|
b.limit(b.capacity());
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
public void testServerWeakCiphers() throws Exception {
|
||||||
|
// a simple test case to verify that SSL server rejects weak cipher suites,
|
||||||
|
// inspired by https://docs.oracle.com/javase/8/docs/technotes/guides/
|
||||||
|
// security/jsse/samples/sslengine/SSLEngineSimpleDemo.java
|
||||||
|
|
||||||
|
// set up a client and a server SSLEngine object, and let them exchange
|
||||||
|
// data over ByteBuffer instead of network socket.
|
||||||
|
GenericTestUtils.setLogLevel(SSLFactory.LOG, Level.DEBUG);
|
||||||
|
final Configuration conf = createConfiguration(true, true);
|
||||||
|
|
||||||
|
SSLFactory serverSSLFactory = new SSLFactory(SSLFactory.Mode.SERVER, conf);
|
||||||
|
SSLFactory clientSSLFactory = new SSLFactory(SSLFactory.Mode.CLIENT, conf);
|
||||||
|
|
||||||
|
serverSSLFactory.init();
|
||||||
|
clientSSLFactory.init();
|
||||||
|
|
||||||
|
SSLEngine serverSSLEngine = serverSSLFactory.createSSLEngine();
|
||||||
|
SSLEngine clientSSLEngine = clientSSLFactory.createSSLEngine();
|
||||||
|
// client selects cipher suites excluded by server
|
||||||
|
clientSSLEngine.setEnabledCipherSuites(excludeCiphers.split(","));
|
||||||
|
|
||||||
|
// use the same buffer size for server and client.
|
||||||
|
SSLSession session = clientSSLEngine.getSession();
|
||||||
|
int appBufferMax = session.getApplicationBufferSize();
|
||||||
|
int netBufferMax = session.getPacketBufferSize();
|
||||||
|
|
||||||
|
ByteBuffer clientOut = ByteBuffer.wrap("client".getBytes());
|
||||||
|
ByteBuffer clientIn = ByteBuffer.allocate(appBufferMax);
|
||||||
|
ByteBuffer serverOut = ByteBuffer.wrap("server".getBytes());
|
||||||
|
ByteBuffer serverIn = ByteBuffer.allocate(appBufferMax);
|
||||||
|
|
||||||
|
// send data from client to server
|
||||||
|
ByteBuffer cTOs = ByteBuffer.allocateDirect(netBufferMax);
|
||||||
|
// send data from server to client
|
||||||
|
ByteBuffer sTOc = ByteBuffer.allocateDirect(netBufferMax);
|
||||||
|
|
||||||
|
boolean dataDone = false;
|
||||||
|
try {
|
||||||
|
/**
|
||||||
|
* Server and client engines call wrap()/unwrap() to perform handshaking,
|
||||||
|
* until both engines are closed.
|
||||||
|
*/
|
||||||
|
while (!isEngineClosed(clientSSLEngine) ||
|
||||||
|
!isEngineClosed(serverSSLEngine)) {
|
||||||
|
LOG.info("client wrap " + wrap(clientSSLEngine, clientOut, cTOs));
|
||||||
|
LOG.info("server wrap " + wrap(serverSSLEngine, serverOut, sTOc));
|
||||||
|
cTOs.flip();
|
||||||
|
sTOc.flip();
|
||||||
|
LOG.info("client unwrap " + unwrap(clientSSLEngine, sTOc, clientIn));
|
||||||
|
LOG.info("server unwrap " + unwrap(serverSSLEngine, cTOs, serverIn));
|
||||||
|
cTOs.compact();
|
||||||
|
sTOc.compact();
|
||||||
|
if (!dataDone && (clientOut.limit() == serverIn.position()) &&
|
||||||
|
(serverOut.limit() == clientIn.position())) {
|
||||||
|
checkTransfer(serverOut, clientIn);
|
||||||
|
checkTransfer(clientOut, serverIn);
|
||||||
|
|
||||||
|
LOG.info("closing client");
|
||||||
|
clientSSLEngine.closeOutbound();
|
||||||
|
dataDone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assert.fail("The exception was not thrown");
|
||||||
|
} catch (SSLHandshakeException e) {
|
||||||
|
GenericTestUtils.assertExceptionContains("no cipher suites in common", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SSLEngineResult wrap(SSLEngine engine, ByteBuffer from,
|
||||||
|
ByteBuffer to) throws Exception {
|
||||||
|
SSLEngineResult result = engine.wrap(from, to);
|
||||||
|
runDelegatedTasks(result, engine);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SSLEngineResult unwrap(SSLEngine engine, ByteBuffer from,
|
||||||
|
ByteBuffer to) throws Exception {
|
||||||
|
SSLEngineResult result = engine.unwrap(from, to);
|
||||||
|
runDelegatedTasks(result, engine);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validHostnameVerifier() throws Exception {
|
public void validHostnameVerifier() throws Exception {
|
||||||
Configuration conf = createConfiguration(false, true);
|
Configuration conf = createConfiguration(false, true);
|
||||||
|
|
Loading…
Reference in New Issue