diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/SSLFactory.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/SSLFactory.java index ea658480d2..95cba80989 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/SSLFactory.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/SSLFactory.java @@ -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 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(); + } 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 defaultEnabledCipherSuites = + new ArrayList(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. * diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/ssl/TestSSLFactory.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/ssl/TestSSLFactory.java index 004888c7b7..b8a09ed92e 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/ssl/TestSSLFactory.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/ssl/TestSSLFactory.java @@ -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);