HADOOP-12886. Exclude weak ciphers in SSLFactory through ssl-server.xml. Contributed by Wei-Chiu Chuang.

This commit is contained in:
Zhe ZHang 2016-03-30 14:13:11 -07:00
parent 37e23ce45c
commit e4fc609d5d
2 changed files with 175 additions and 6 deletions

View File

@ -23,6 +23,8 @@
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;
@ -34,6 +36,11 @@
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.LinkedList;
import java.util.List;
/**
* Factory that creates SSLEngine and SSLSocketFactory instances using
@ -48,6 +55,7 @@
@InterfaceAudience.Private
@InterfaceStability.Evolving
public class SSLFactory implements ConnectionConfigurator {
static final Logger LOG = LoggerFactory.getLogger(SSLFactory.class);
@InterfaceAudience.Private
public static enum Mode { CLIENT, SERVER }
@ -60,7 +68,7 @@ public static enum Mode { CLIENT, SERVER }
"hadoop.ssl.client.conf";
public static final String SSL_SERVER_CONF_KEY =
"hadoop.ssl.server.conf";
public static final String SSLCERTIFICATE = IBM_JAVA?"ibmX509":"SunX509";
public static final String SSLCERTIFICATE = IBM_JAVA?"ibmX509":"SunX509";
public static final boolean DEFAULT_SSL_REQUIRE_CLIENT_CERT = false;
@ -71,6 +79,8 @@ public static enum Mode { CLIENT, SERVER }
"hadoop.ssl.enabled.protocols";
public static final String DEFAULT_SSL_ENABLED_PROTOCOLS =
"TLSv1,SSLv2Hello,TLSv1.1,TLSv1.2";
public static final String SSL_SERVER_EXCLUDE_CIPHER_LIST =
"ssl.server.exclude.cipher.list";
private Configuration conf;
private Mode mode;
@ -80,6 +90,7 @@ public static enum Mode { CLIENT, SERVER }
private KeyStoresFactory keystoresFactory;
private String[] enabledProtocols = null;
private List<String> excludeCiphers;
/**
* Creates an SSLFactory.
@ -105,6 +116,14 @@ public SSLFactory(Mode mode, Configuration conf) {
enabledProtocols = conf.getStrings(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) {
@ -195,11 +214,32 @@ public SSLEngine createSSLEngine()
} 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.
*

View File

@ -17,24 +17,31 @@
*/
package org.apache.hadoop.security.ssl;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileUtil;
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.JavaKeyStoreProvider;
import org.apache.hadoop.test.GenericTestUtils;
import org.apache.log4j.Level;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.net.URL;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
@ -42,13 +49,21 @@
import java.util.Map;
public class TestSSLFactory {
private static final Logger LOG = LoggerFactory
.getLogger(TestSSLFactory.class);
private static final String BASEDIR =
System.getProperty("test.build.dir", "target/test-dir") + "/" +
TestSSLFactory.class.getSimpleName();
private static final String KEYSTORES_DIR =
new File(BASEDIR).getAbsolutePath();
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
public static void setUp() throws Exception {
@ -62,7 +77,7 @@ private Configuration createConfiguration(boolean clientCert,
throws Exception {
Configuration conf = new Configuration();
KeyStoreTestUtil.setupSSLConfig(KEYSTORES_DIR, sslConfsDir, conf,
clientCert, trustStore);
clientCert, trustStore, excludeCiphers);
return conf;
}
@ -125,6 +140,120 @@ public void serverModeWithClientCertsVerifier() throws Exception {
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
public void validHostnameVerifier() throws Exception {
Configuration conf = createConfiguration(false, true);