Skip to content

HADOOP-16371: Option to disable GCM for SSL connections when running on Java 8 #970

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions hadoop-common-project/hadoop-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,16 @@
<artifactId>dnsjava</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.wildfly.openssl</groupId>
<artifactId>wildfly-openssl</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* limitations under the License.
*/

package org.apache.hadoop.fs.azurebfs.utils;
package org.apache.hadoop.security.ssl;

import java.io.IOException;
import java.net.InetAddress;
Expand All @@ -38,30 +38,60 @@


/**
* Extension to use native OpenSSL library instead of JSSE for better
* performance.
* A {@link SSLSocketFactory} that can delegate to various SSL implementations.
* Specifically, either OpenSSL or JSSE can be used. OpenSSL offers better
* performance than JSSE and is made available via the
* <a href="https://github.com/wildfly/wildfly-openssl">wildlfy-openssl</a>
* library.
*
* <p>
* The factory has several different modes of operation:
* <ul>
* <li>OpenSSL: Uses the wildly-openssl library to delegate to the
* system installed OpenSSL. If the wildfly-openssl integration is not
* properly setup, an exception is thrown.</li>
* <li>Default: Attempts to use the OpenSSL mode, if it cannot load the
* necessary libraries, it falls back to the Default_JSEE mode.</li>
* <li>Default_JSSE: Delegates to the JSSE implementation of SSL, but
* it disables the GCM cipher when running on Java 8.</li>
* <li>Default_JSSE_with_GCM: Delegates to the JSSE implementation of
* SSL with no modification to the list of enabled ciphers.</li>
* </ul>
* </p>
*/
public final class SSLSocketFactoryEx extends SSLSocketFactory {
public final class DelegatingSSLSocketFactory extends SSLSocketFactory {

/**
* Default indicates Ordered, preferred OpenSSL, if failed to load then fall
* back to Default_JSSE
* back to Default_JSSE.
*
* <p>
* Default_JSSE is not truly the the default JSSE implementation because
* the GCM cipher is disabled when running on Java 8. However, the name
* was not changed in order to preserve backwards compatibility. Instead,
* a new mode called Default_JSSE_with_GCM delegates to the default JSSE
* implementation with no changes to the list of enabled ciphers.
* </p>
*/
public enum SSLChannelMode {
OpenSSL,
Default,
Default_JSSE
Default_JSSE,
Default_JSSE_with_GCM
}

private static SSLSocketFactoryEx instance = null;
private static DelegatingSSLSocketFactory instance = null;
private static final Logger LOG = LoggerFactory.getLogger(
SSLSocketFactoryEx.class);
DelegatingSSLSocketFactory.class);
private String providerName;
private SSLContext ctx;
private String[] ciphers;
private SSLChannelMode channelMode;

// This should only be modified within the #initializeDefaultFactory
// method which is synchronized
private boolean openSSLProviderRegistered;

/**
* Initialize a singleton SSL socket factory.
*
Expand All @@ -71,7 +101,7 @@ public enum SSLChannelMode {
public static synchronized void initializeDefaultFactory(
SSLChannelMode preferredMode) throws IOException {
if (instance == null) {
instance = new SSLSocketFactoryEx(preferredMode);
instance = new DelegatingSSLSocketFactory(preferredMode);
}
}

Expand All @@ -84,15 +114,11 @@ public static synchronized void initializeDefaultFactory(
* @return instance of the SSLSocketFactory, instance must be initialized by
* initializeDefaultFactory.
*/
public static SSLSocketFactoryEx getDefaultFactory() {
public static DelegatingSSLSocketFactory getDefaultFactory() {
return instance;
}

static {
OpenSSLProvider.register();
}

private SSLSocketFactoryEx(SSLChannelMode preferredChannelMode)
private DelegatingSSLSocketFactory(SSLChannelMode preferredChannelMode)
throws IOException {
try {
initializeSSLContext(preferredChannelMode);
Expand All @@ -118,33 +144,47 @@ private SSLSocketFactoryEx(SSLChannelMode preferredChannelMode)
private void initializeSSLContext(SSLChannelMode preferredChannelMode)
throws NoSuchAlgorithmException, KeyManagementException {
switch (preferredChannelMode) {
case Default:
try {
java.util.logging.Logger logger = java.util.logging.Logger.getLogger(SSL.class.getName());
logger.setLevel(Level.WARNING);
ctx = SSLContext.getInstance("openssl.TLS");
ctx.init(null, null, null);
// Strong reference needs to be kept to logger until initialization of SSLContext finished (see HADOOP-16174):
logger.setLevel(Level.INFO);
channelMode = SSLChannelMode.OpenSSL;
} catch (NoSuchAlgorithmException e) {
LOG.warn("Failed to load OpenSSL. Falling back to the JSSE default.");
ctx = SSLContext.getDefault();
channelMode = SSLChannelMode.Default_JSSE;
}
break;
case OpenSSL:
case Default:
if (!openSSLProviderRegistered) {
OpenSSLProvider.register();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By calling wildfly methods without using reflection, this class can only be loaded or used if wildfly is on the CP. While this is already a requirement of hadoop-azure, I don't want to add it to hadoop-aws. Somehow reflection is going to be needed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check in NetworkBinding#bindSSLChannelMode explicitly prevents S3A users from setting fs.s3a.ssl.channel.mode to default or OpenSSL, so there should be no way an S3A user can trigger the Wildfly jar from actually being used.

IIUC Java correctly, a class should still be able to load this class without Wildfly on the classpath. Java only looks for the Wildly classes when a Wildly class is initialized (in this case OpenSSLProvider). The import statements are only used during compilation. ref: https://stackoverflow.com/a/12620773/11511572

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

I always had the belief that integers and other simple constants may just get compiled in but that the class load Will kick off as soon as you reference a string in class. I should do more experiments. Maybe even learn Java assembly language. Anyway, as long as we don't try referencing, things like strings from the class, we should be okay. And as the little wild fly JAR is not currently on the hadoop-aws test CP, we are implicitly verifying this. We probably have to be careful once we had more support for it -we need to make sure we've not accidentally made it mandatory.

openSSLProviderRegistered = true;
}
try {
java.util.logging.Logger logger = java.util.logging.Logger.getLogger(
SSL.class.getName());
logger.setLevel(Level.WARNING);
ctx = SSLContext.getInstance("openssl.TLS");
ctx.init(null, null, null);
// Strong reference needs to be kept to logger until initialization of
// SSLContext finished (see HADOOP-16174):
logger.setLevel(Level.INFO);
channelMode = SSLChannelMode.OpenSSL;
break;
case Default_JSSE:
} catch (NoSuchAlgorithmException e) {
LOG.debug("Failed to load OpenSSL. Falling back to the JSSE default.");
ctx = SSLContext.getDefault();
channelMode = SSLChannelMode.Default_JSSE;
break;
default:
throw new AssertionError("Unknown channel mode: "
+ preferredChannelMode);
}
break;
case OpenSSL:
if (!openSSLProviderRegistered) {
OpenSSLProvider.register();
openSSLProviderRegistered = true;
}
ctx = SSLContext.getInstance("openssl.TLS");
ctx.init(null, null, null);
channelMode = SSLChannelMode.OpenSSL;
break;
case Default_JSSE:
ctx = SSLContext.getDefault();
channelMode = SSLChannelMode.Default_JSSE;
break;
case Default_JSSE_with_GCM:
ctx = SSLContext.getDefault();
channelMode = SSLChannelMode.Default_JSSE_with_GCM;
break;
default:
throw new NoSuchAlgorithmException("Unknown channel mode: "
+ preferredChannelMode);
}
}

Expand Down Expand Up @@ -234,7 +274,8 @@ private String[] alterCipherList(String[] defaultCiphers) {
// Remove GCM mode based ciphers from the supported list.
for (int i = 0; i < defaultCiphers.length; i++) {
if (defaultCiphers[i].contains("_GCM_")) {
LOG.debug("Removed Cipher - " + defaultCiphers[i]);
LOG.debug("Removed Cipher - {} from list of enabled SSLSocket ciphers",
defaultCiphers[i]);
} else {
preferredSuits.add(defaultCiphers[i]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1973,6 +1973,20 @@
</description>
</property>

<property>
<name>fs.s3a.ssl.channel.mode</name>
<value>default_jsse</value>
<description>
If secure connections to S3 are enabled, configures the SSL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like having to remember the case of all these options, especially as every other s3a config option is all lower case. these should all be case insensitive.

implementation used to encrypt connections to S3. Supported values are:
"default_jsse" and "default_jsse_with_gcm". "default_jsse" uses the Java
Secure Socket Extension package (JSSE). However, when running on Java 8,
the GCM cipher is removed from the list of enabled ciphers. This is due
to performance issues with GCM in Java 8. "default_jsse_with_gcm" uses
the JSSE with the default list of cipher suites.
</description>
</property>

<!-- Azure file system properties -->
<property>
<name>fs.AbstractFileSystem.wasb.impl</name>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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 java.io.IOException;
import java.util.Arrays;

import org.junit.Test;

import org.apache.hadoop.util.NativeCodeLoader;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assume.assumeTrue;

/**
* Tests for {@link DelegatingSSLSocketFactory}.
*/
public class TestDelegatingSSLSocketFactory {

@Test
public void testOpenSSL() throws IOException {
assumeTrue("Unable to load native libraries",
NativeCodeLoader.isNativeCodeLoaded());
assumeTrue("Build was not compiled with support for OpenSSL",
NativeCodeLoader.buildSupportsOpenssl());
DelegatingSSLSocketFactory.initializeDefaultFactory(
DelegatingSSLSocketFactory.SSLChannelMode.OpenSSL);
assertThat(DelegatingSSLSocketFactory.getDefaultFactory()
.getProviderName()).contains("openssl");
}

@Test
public void testJSEENoGCMJava8() throws IOException {
assumeTrue("Not running on Java 8",
System.getProperty("java.version").startsWith("1.8"));
DelegatingSSLSocketFactory.initializeDefaultFactory(
DelegatingSSLSocketFactory.SSLChannelMode.Default_JSSE);
assertThat(Arrays.stream(DelegatingSSLSocketFactory.getDefaultFactory()
.getSupportedCipherSuites())).noneMatch("GCM"::contains);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.security.ssl.DelegatingSSLSocketFactory;

import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -153,6 +154,12 @@ private Constants() {
"fs.s3a.connection.ssl.enabled";
public static final boolean DEFAULT_SECURE_CONNECTIONS = true;

// use OpenSSL or JSEE for secure connections
public static final String SSL_CHANNEL_MODE = "fs.s3a.ssl.channel.mode";
public static final DelegatingSSLSocketFactory.SSLChannelMode
DEFAULT_SSL_CHANNEL_MODE =
DelegatingSSLSocketFactory.SSLChannelMode.Default_JSSE;

//use a custom endpoint?
public static final String ENDPOINT = "fs.s3a.endpoint";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import org.apache.hadoop.fs.PathFilter;
import org.apache.hadoop.fs.RemoteIterator;
import org.apache.hadoop.fs.s3a.auth.IAMInstanceCredentialsProvider;
import org.apache.hadoop.fs.s3a.impl.NetworkBinding;
import org.apache.hadoop.fs.s3native.S3xLoginHelper;
import org.apache.hadoop.net.ConnectTimeoutException;
import org.apache.hadoop.security.ProviderUtils;
Expand Down Expand Up @@ -1218,14 +1219,15 @@ public static ClientConfiguration createAwsConf(Configuration conf,
*
* @param conf Hadoop configuration
* @param awsConf AWS SDK configuration
*
* @throws IOException if there was an error initializing the protocol
* settings
*/
public static void initConnectionSettings(Configuration conf,
ClientConfiguration awsConf) {
ClientConfiguration awsConf) throws IOException {
awsConf.setMaxConnections(intOption(conf, MAXIMUM_CONNECTIONS,
DEFAULT_MAXIMUM_CONNECTIONS, 1));
boolean secureConnections = conf.getBoolean(SECURE_CONNECTIONS,
DEFAULT_SECURE_CONNECTIONS);
awsConf.setProtocol(secureConnections ? Protocol.HTTPS : Protocol.HTTP);
initProtocolSettings(conf, awsConf);
awsConf.setMaxErrorRetry(intOption(conf, MAX_ERROR_RETRIES,
DEFAULT_MAX_ERROR_RETRIES, 0));
awsConf.setConnectionTimeout(intOption(conf, ESTABLISH_TIMEOUT,
Expand All @@ -1244,6 +1246,27 @@ public static void initConnectionSettings(Configuration conf,
}
}

/**
* Initializes the connection protocol settings when connecting to S3 (e.g.
* either HTTP or HTTPS). If secure connections are enabled, this method
* will load the configured SSL providers.
*
* @param conf Hadoop configuration
* @param awsConf AWS SDK configuration
*
* @throws IOException if there is an error initializing the configured
* {@link javax.net.ssl.SSLSocketFactory}
*/
private static void initProtocolSettings(Configuration conf,
ClientConfiguration awsConf) throws IOException {
boolean secureConnections = conf.getBoolean(SECURE_CONNECTIONS,
DEFAULT_SECURE_CONNECTIONS);
awsConf.setProtocol(secureConnections ? Protocol.HTTPS : Protocol.HTTP);
if (secureConnections) {
NetworkBinding.bindSSLChannelMode(conf, awsConf);
}
}

/**
* Initializes AWS SDK proxy support in the AWS client configuration
* if the S3A settings enable it.
Expand Down
Loading