HADOOP-12886. Exclude weak ciphers in SSLFactory through ssl-server.xml. Contributed by Wei-Chiu Chuang.
This commit is contained in:
parent
be5894c12d
commit
9216993b02
|
@ -23,6 +23,8 @@ 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;
|
||||
|
@ -34,6 +36,11 @@ 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.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Factory that creates SSLEngine and SSLSocketFactory instances using
|
||||
|
@ -48,6 +55,7 @@ import java.security.GeneralSecurityException;
|
|||
@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 class SSLFactory implements ConnectionConfigurator {
|
|||
"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 class SSLFactory implements ConnectionConfigurator {
|
|||
"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 class SSLFactory implements ConnectionConfigurator {
|
|||
private KeyStoresFactory keystoresFactory;
|
||||
|
||||
private String[] enabledProtocols = null;
|
||||
private List<String> excludeCiphers;
|
||||
|
||||
/**
|
||||
* Creates an SSLFactory.
|
||||
|
@ -105,6 +116,14 @@ public class SSLFactory implements ConnectionConfigurator {
|
|||
|
||||
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 class SSLFactory implements ConnectionConfigurator {
|
|||
} 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.
|
||||
*
|
||||
|
|
|
@ -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.Collections;
|
|||
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 @@ public class TestSSLFactory {
|
|||
throws Exception {
|
||||
Configuration conf = new Configuration();
|
||||
KeyStoreTestUtil.setupSSLConfig(KEYSTORES_DIR, sslConfsDir, conf,
|
||||
clientCert, trustStore);
|
||||
clientCert, trustStore, excludeCiphers);
|
||||
return conf;
|
||||
}
|
||||
|
||||
|
@ -125,6 +140,120 @@ public class TestSSLFactory {
|
|||
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);
|
||||
|
|
Loading…
Reference in New Issue