diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SecurityUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SecurityUtil.java new file mode 100644 index 0000000000..7a010fa07e --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SecurityUtil.java @@ -0,0 +1,81 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.http2; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Provides utilities related to security requirements specific to HTTP/2. + */ +public final class Http2SecurityUtil { + private Http2SecurityUtil() { } + + /** + * The following list is derived from SunJSSE Supported + * Ciphers and Mozilla Cipher + * Suites in accordance with the HTTP/2 Specification. + */ + public static final List CIPHERS; + + private static final List CIPHERS_JAVA_MOZILLA_INCREASED_SECURITY = Collections.unmodifiableList(Arrays + .asList( + /* Java 8 */ + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", /* openssl = ECDHE-ECDSA-AES256-GCM-SHA384 */ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", /* openssl = ECDHE-ECDSA-AES128-GCM-SHA256 */ + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", /* openssl = ECDHE-RSA-AES256-GCM-SHA384 */ + "TLS_RSA_WITH_AES_256_GCM_SHA384", /* openssl = AES256-GCM-SHA384 */ + /* REQUIRED BY HTTP/2 SPEC */ + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", /* openssl = ECDHE-RSA-AES128-GCM-SHA256 */ + /* REQUIRED BY HTTP/2 SPEC */ + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", /* openssl = DHE-RSA-AES128-GCM-SHA256 */ + "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256" /* openssl = DHE-DSS-AES128-GCM-SHA256 */)); + + private static final List CIPHERS_JAVA_NO_MOZILLA_INCREASED_SECURITY = Collections.unmodifiableList(Arrays + .asList( + /* Java 6,7,8 */ + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", /* openssl = ECDHE-ECDSA-RC4-SHA */ + "TLS_ECDH_ECDSA_WITH_RC4_128_SHA", /* openssl = ECDH-ECDSA-RC4-SHA */ + /* Java 8 */ + "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", /* openssl = ECDH-ECDSA-AES256-GCM-SHA384 */ + "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", /* openssl = ECDH-RSA-AES256-GCM-SHA384 */ + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", /* openssl = DHE-RSA-AES256-GCM-SHA384 */ + "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", /* openssl = DHE-DSS-AES256-GCM-SHA384 */ + "TLS_RSA_WITH_AES_128_GCM_SHA256", /* openssl = AES128-GCM-SHA256 */ + "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", /* openssl = ECDH-ECDSA-AES128-GCM-SHA256 */ + "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256" /* openssl = ECDH-RSA-AES128-GCM-SHA256 */)); + + private static final List CIPHERS_JAVA_DISABLED_DEFAULT = Collections.unmodifiableList(Arrays.asList( + /* Java 8 */ + "TLS_DH_anon_WITH_AES_256_GCM_SHA384", /* openssl = ADH-AES256-GCM-SHA384 */ + "TLS_DH_anon_WITH_AES_128_GCM_SHA256", /* openssl = ADH-AES128-GCM-SHA256 */ + /* Java 6,7,8 */ + "TLS_ECDH_anon_WITH_RC4_128_SHA" /* openssl = AECDH-RC4-SHA */)); + + static { + List ciphers = new ArrayList(CIPHERS_JAVA_MOZILLA_INCREASED_SECURITY.size() + + CIPHERS_JAVA_NO_MOZILLA_INCREASED_SECURITY.size() + CIPHERS_JAVA_DISABLED_DEFAULT.size()); + ciphers.addAll(CIPHERS_JAVA_MOZILLA_INCREASED_SECURITY); + ciphers.addAll(CIPHERS_JAVA_NO_MOZILLA_INCREASED_SECURITY); + ciphers.addAll(CIPHERS_JAVA_DISABLED_DEFAULT); + CIPHERS = Collections.unmodifiableList(ciphers); + } +} diff --git a/example/src/main/java/io/netty/example/http2/client/Http2Client.java b/example/src/main/java/io/netty/example/http2/client/Http2Client.java index 9203e1d0d6..9cf50ce58b 100644 --- a/example/src/main/java/io/netty/example/http2/client/Http2Client.java +++ b/example/src/main/java/io/netty/example/http2/client/Http2Client.java @@ -28,8 +28,10 @@ import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http2.Http2OrHttpChooser.SelectedProtocol; +import io.netty.handler.codec.http2.Http2SecurityUtil; import io.netty.handler.ssl.JettyAlpnSslEngineWrapper; import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.util.CharsetUtil; @@ -57,12 +59,10 @@ public final class Http2Client { if (SSL) { sslCtx = SslContext.newClientContext( null, InsecureTrustManagerFactory.INSTANCE, - Arrays.asList("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - // NOTE: Block ciphers are prohibited by the HTTP/2 specification - // http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-9.2.2. - // The following cipher exists to allow these examples to run with older JREs. - // Please consult the HTTP/2 specification when selecting cipher suites. - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA"), + Http2SecurityUtil.CIPHERS, + /* NOTE: the following filter may not include all ciphers required by the HTTP/2 specification + * Please refer to the HTTP/2 specification for cipher requirements. */ + SupportedCipherSuiteFilter.INSTANCE, Arrays.asList(SelectedProtocol.HTTP_2.protocolName(), SelectedProtocol.HTTP_1_1.protocolName()), JettyAlpnSslEngineWrapper.instance(), 0, 0); diff --git a/example/src/main/java/io/netty/example/http2/server/Http2Server.java b/example/src/main/java/io/netty/example/http2/server/Http2Server.java index 4fec25cd67..c38b85a581 100644 --- a/example/src/main/java/io/netty/example/http2/server/Http2Server.java +++ b/example/src/main/java/io/netty/example/http2/server/Http2Server.java @@ -23,10 +23,12 @@ import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http2.Http2OrHttpChooser.SelectedProtocol; +import io.netty.handler.codec.http2.Http2SecurityUtil; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.JettyAlpnSslEngineWrapper; import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; import io.netty.handler.ssl.util.SelfSignedCertificate; import java.util.Arrays; @@ -47,12 +49,10 @@ public final class Http2Server { SelfSignedCertificate ssc = new SelfSignedCertificate(); sslCtx = SslContext.newServerContext( ssc.certificate(), ssc.privateKey(), null, - Arrays.asList("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - // NOTE: Block ciphers are prohibited by the HTTP/2 specification - // http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-9.2.2. - // The following cipher exists to allow these examples to run with older JREs. - // Please consult the HTTP/2 specification when selecting cipher suites. - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA"), + Http2SecurityUtil.CIPHERS, + /* NOTE: the following filter may not include all ciphers required by the HTTP/2 specification + * Please refer to the HTTP/2 specification for cipher requirements. */ + SupportedCipherSuiteFilter.INSTANCE, Arrays.asList( SelectedProtocol.HTTP_2.protocolName(), SelectedProtocol.HTTP_1_1.protocolName()), diff --git a/example/src/main/java/io/netty/example/memcache/binary/MemcacheClient.java b/example/src/main/java/io/netty/example/memcache/binary/MemcacheClient.java index 95ea49dfe3..9fe42b07a2 100644 --- a/example/src/main/java/io/netty/example/memcache/binary/MemcacheClient.java +++ b/example/src/main/java/io/netty/example/memcache/binary/MemcacheClient.java @@ -24,11 +24,13 @@ import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http2.Http2SecurityUtil; import io.netty.handler.codec.http2.Http2OrHttpChooser.SelectedProtocol; import io.netty.handler.codec.memcache.binary.BinaryMemcacheClientCodec; import io.netty.handler.codec.memcache.binary.BinaryMemcacheObjectAggregator; import io.netty.handler.ssl.JettyAlpnSslEngineWrapper; import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import java.io.BufferedReader; @@ -49,7 +51,10 @@ public final class MemcacheClient { final SslContext sslCtx; if (SSL) { sslCtx = SslContext.newClientContext( - null, InsecureTrustManagerFactory.INSTANCE, null, + null, InsecureTrustManagerFactory.INSTANCE, Http2SecurityUtil.CIPHERS, + /* NOTE: the following filter may not include all ciphers required by the HTTP/2 specification + * Please refer to the HTTP/2 specification for cipher requirements. */ + SupportedCipherSuiteFilter.INSTANCE, Arrays.asList(SelectedProtocol.HTTP_2.protocolName(), SelectedProtocol.HTTP_1_1.protocolName()), JettyAlpnSslEngineWrapper.instance(), 0, 0); diff --git a/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java b/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java index bccdf208d6..b8f66619c2 100644 --- a/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java +++ b/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java @@ -27,6 +27,7 @@ import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.spdy.SpdyOrHttpChooser.SelectedProtocol; +import io.netty.handler.ssl.IdentityCipherSuiteFilter; import io.netty.handler.ssl.JettyNpnSslEngineWrapper; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; @@ -55,7 +56,7 @@ public final class SpdyClient { public static void main(String[] args) throws Exception { // Configure SSL. final SslContext sslCtx = SslContext.newClientContext( - null, InsecureTrustManagerFactory.INSTANCE, null, + null, InsecureTrustManagerFactory.INSTANCE, null, IdentityCipherSuiteFilter.INSTANCE, Arrays.asList(SelectedProtocol.SPDY_3_1.protocolName(), SelectedProtocol.HTTP_1_1.protocolName()), JettyNpnSslEngineWrapper.instance(), 0, 0); diff --git a/example/src/main/java/io/netty/example/spdy/server/SpdyServer.java b/example/src/main/java/io/netty/example/spdy/server/SpdyServer.java index 05d9c69037..bcd3257018 100644 --- a/example/src/main/java/io/netty/example/spdy/server/SpdyServer.java +++ b/example/src/main/java/io/netty/example/spdy/server/SpdyServer.java @@ -24,6 +24,7 @@ import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.spdy.SpdyOrHttpChooser.SelectedProtocol; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.IdentityCipherSuiteFilter; import io.netty.handler.ssl.JettyNpnSslEngineWrapper; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.util.SelfSignedCertificate; @@ -56,7 +57,7 @@ public final class SpdyServer { // Configure SSL. SelfSignedCertificate ssc = new SelfSignedCertificate(); SslContext sslCtx = SslContext.newServerContext( - ssc.certificate(), ssc.privateKey(), null, null, + ssc.certificate(), ssc.privateKey(), null, null, IdentityCipherSuiteFilter.INSTANCE, Arrays.asList(SelectedProtocol.SPDY_3_1.protocolName(), SelectedProtocol.HTTP_1_1.protocolName()), JettyNpnSslEngineWrapper.instance(), 0, 0); diff --git a/handler/src/main/java/io/netty/handler/ssl/CipherSuiteFilter.java b/handler/src/main/java/io/netty/handler/ssl/CipherSuiteFilter.java new file mode 100644 index 0000000000..13c539cd32 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/CipherSuiteFilter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ssl; + +import java.util.List; +import java.util.Set; + +/** + * Provides a means to filter the supplied cipher suite based upon the supported and default cipher suites. + */ +public interface CipherSuiteFilter { + /** + * Filter the requested {@code ciphers} based upon other cipher characteristics. + * @param ciphers The requested ciphers + * @param defaultCiphers The default recommended ciphers for the current {@link SSLEngine} as determined by Netty + * @param supportedCiphers The supported ciphers for the current {@link SSLEngine} + * @return The filter list of ciphers. Must not return {@code null}. + */ + String[] filterCipherSuites(Iterable ciphers, List defaultCiphers, Set supportedCiphers); +} diff --git a/handler/src/main/java/io/netty/handler/ssl/DefaultSslWrapperFactory.java b/handler/src/main/java/io/netty/handler/ssl/DefaultSslWrapperFactory.java index c7db27c58a..e6654edd5d 100644 --- a/handler/src/main/java/io/netty/handler/ssl/DefaultSslWrapperFactory.java +++ b/handler/src/main/java/io/netty/handler/ssl/DefaultSslWrapperFactory.java @@ -20,18 +20,14 @@ import java.util.List; import javax.net.ssl.SSLEngine; /** - * Factory for not wrapping {@link SSLEngine} object and just returning it + * Factory for not wrapping {@link SSLEngine} object and just returning it. */ public final class DefaultSslWrapperFactory implements SslEngineWrapperFactory { - private static final DefaultSslWrapperFactory INSTANCE = new DefaultSslWrapperFactory(); + public static final DefaultSslWrapperFactory INSTANCE = new DefaultSslWrapperFactory(); private DefaultSslWrapperFactory() { } - public static DefaultSslWrapperFactory instance() { - return INSTANCE; - } - @Override public SSLEngine wrapSslEngine(SSLEngine engine, List protocols, boolean isServer) { return engine; diff --git a/handler/src/main/java/io/netty/handler/ssl/IdentityCipherSuiteFilter.java b/handler/src/main/java/io/netty/handler/ssl/IdentityCipherSuiteFilter.java new file mode 100644 index 0000000000..c9ebfb0da2 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/IdentityCipherSuiteFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ssl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * This class will not do any filtering of ciphers suites. + */ +public final class IdentityCipherSuiteFilter implements CipherSuiteFilter { + public static final IdentityCipherSuiteFilter INSTANCE = new IdentityCipherSuiteFilter(); + + private IdentityCipherSuiteFilter() { } + + @Override + public String[] filterCipherSuites(Iterable ciphers, List defaultCiphers, + Set supportedCiphers) { + if (ciphers == null) { + return defaultCiphers.toArray(new String[defaultCiphers.size()]); + } else { + List newCiphers = new ArrayList(supportedCiphers.size()); + for (String c : ciphers) { + if (c == null) { + break; + } + newCiphers.add(c); + } + return newCiphers.toArray(new String[newCiphers.size()]); + } + } + +} diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java index 99affaa2cd..28ab305e5f 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java @@ -79,7 +79,8 @@ public final class JdkSslClientContext extends JdkSslContext { * {@code null} to use the default. */ public JdkSslClientContext(File certChainFile, TrustManagerFactory trustManagerFactory) throws SSLException { - this(certChainFile, trustManagerFactory, null, null, DefaultSslWrapperFactory.instance(), 0, 0); + this(certChainFile, trustManagerFactory, null, IdentityCipherSuiteFilter.INSTANCE, + null, DefaultSslWrapperFactory.INSTANCE, 0, 0); } /** @@ -92,6 +93,7 @@ public final class JdkSslClientContext extends JdkSslContext { * {@code null} to use the default. * @param ciphers the cipher suites to enable, in the order of preference. * {@code null} to use the default cipher suites. + * @param cipherFilter a filter to apply over the supplied list of ciphers * @param nextProtocols the application layer protocols to accept, in the order of preference. * {@code null} to disable TLS NPN/ALPN extension. * @param wrapperFactory a factory used to wrap the underlying {@link SSLEngine}. @@ -103,11 +105,11 @@ public final class JdkSslClientContext extends JdkSslContext { */ public JdkSslClientContext( File certChainFile, TrustManagerFactory trustManagerFactory, - Iterable ciphers, Iterable nextProtocols, - SslEngineWrapperFactory wrapperFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, + Iterable nextProtocols, SslEngineWrapperFactory wrapperFactory, long sessionCacheSize, long sessionTimeout) throws SSLException { - super(ciphers, wrapperFactory); + super(ciphers, cipherFilter, wrapperFactory); this.nextProtocols = translateProtocols(nextProtocols); diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java index 0ce4119a85..05c8edc471 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java @@ -23,7 +23,9 @@ import io.netty.util.internal.logging.InternalLoggerFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; @@ -39,9 +41,11 @@ public abstract class JdkSslContext extends SslContext { static final String PROTOCOL = "TLS"; static final String[] PROTOCOLS; static final List DEFAULT_CIPHERS; + static final Set SUPPORTED_CIPHERS; static { SSLContext context; + int i; try { context = SSLContext.getInstance(PROTOCOL); context.init(null, null, null); @@ -52,10 +56,14 @@ public abstract class JdkSslContext extends SslContext { SSLEngine engine = context.createSSLEngine(); // Choose the sensible default list of protocols. - String[] supportedProtocols = engine.getSupportedProtocols(); + final String[] supportedProtocols = engine.getSupportedProtocols(); + Set supportedProtocolsSet = new HashSet(supportedProtocols.length); + for (i = 0; i < supportedProtocols.length; ++i) { + supportedProtocolsSet.add(supportedProtocols[i]); + } List protocols = new ArrayList(); addIfSupported( - supportedProtocols, protocols, + supportedProtocolsSet, protocols, "TLSv1.2", "TLSv1.1", "TLSv1", "SSLv3"); if (!protocols.isEmpty()) { @@ -65,10 +73,14 @@ public abstract class JdkSslContext extends SslContext { } // Choose the sensible default list of cipher suites. - String[] supportedCiphers = engine.getSupportedCipherSuites(); + final String[] supportedCiphers = engine.getSupportedCipherSuites(); + SUPPORTED_CIPHERS = new HashSet(supportedCiphers.length); + for (i = 0; i < supportedCiphers.length; ++i) { + SUPPORTED_CIPHERS.add(supportedCiphers[i]); + } List ciphers = new ArrayList(); addIfSupported( - supportedCiphers, ciphers, + SUPPORTED_CIPHERS, ciphers, // XXX: Make sure to sync this list with OpenSslEngineFactory. // GCM (Galois/Counter Mode) requires JDK 8. "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", @@ -98,13 +110,11 @@ public abstract class JdkSslContext extends SslContext { } } - private static void addIfSupported(String[] supported, List enabled, String... names) { - for (String n: names) { - for (String s: supported) { - if (n.equals(s)) { - enabled.add(s); - break; - } + private static void addIfSupported(Set supported, List enabled, String... names) { + for (int i = 0; i < names.length; ++i) { + String n = names[i]; + if (supported.contains(n)) { + enabled.add(n); } } } @@ -113,11 +123,14 @@ public abstract class JdkSslContext extends SslContext { private final List unmodifiableCipherSuites; private final SslEngineWrapperFactory wrapperFactory; - JdkSslContext(Iterable ciphers, SslEngineWrapperFactory wrapperFactory) { + JdkSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, SslEngineWrapperFactory wrapperFactory) { if (wrapperFactory == null) { throw new NullPointerException("wrapperFactory"); } - cipherSuites = toCipherSuiteArray(ciphers); + if (cipherFilter == null) { + throw new NullPointerException("cipherFilter"); + } + cipherSuites = cipherFilter.filterCipherSuites(ciphers, DEFAULT_CIPHERS, SUPPORTED_CIPHERS); unmodifiableCipherSuites = Collections.unmodifiableList(Arrays.asList(cipherSuites)); this.wrapperFactory = wrapperFactory; } @@ -174,19 +187,4 @@ public abstract class JdkSslContext extends SslContext { private SSLEngine wrapEngine(SSLEngine engine) { return wrapperFactory.wrapSslEngine(engine, nextProtocols(), isServer()); } - - private static String[] toCipherSuiteArray(Iterable ciphers) { - if (ciphers == null) { - return DEFAULT_CIPHERS.toArray(new String[DEFAULT_CIPHERS.size()]); - } else { - List newCiphers = new ArrayList(); - for (String c: ciphers) { - if (c == null) { - break; - } - newCiphers.add(c); - } - return newCiphers.toArray(new String[newCiphers.size()]); - } - } } diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java index 955cc16a31..5009f2c030 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java @@ -74,7 +74,8 @@ public final class JdkSslServerContext extends JdkSslContext { * {@code null} if it's not password-protected. */ public JdkSslServerContext(File certChainFile, File keyFile, String keyPassword) throws SSLException { - this(certChainFile, keyFile, keyPassword, null, null, DefaultSslWrapperFactory.instance(), 0, 0); + this(certChainFile, keyFile, keyPassword, null, IdentityCipherSuiteFilter.INSTANCE, + null, DefaultSslWrapperFactory.INSTANCE, 0, 0); } /** @@ -86,6 +87,7 @@ public final class JdkSslServerContext extends JdkSslContext { * {@code null} if it's not password-protected. * @param ciphers the cipher suites to enable, in the order of preference. * {@code null} to use the default cipher suites. + * @param cipherFilter a filter to apply over the supplied list of ciphers * @param nextProtocols the application layer protocols to accept, in the order of preference. * {@code null} to disable TLS NPN/ALPN extension. * @param wrapperFactory a factory used to wrap the underlying {@link SSLEngine}. @@ -97,11 +99,11 @@ public final class JdkSslServerContext extends JdkSslContext { */ public JdkSslServerContext( File certChainFile, File keyFile, String keyPassword, - Iterable ciphers, Iterable nextProtocols, - SslEngineWrapperFactory wrapperFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, + Iterable nextProtocols, SslEngineWrapperFactory wrapperFactory, long sessionCacheSize, long sessionTimeout) throws SSLException { - super(ciphers, wrapperFactory); + super(ciphers, cipherFilter, wrapperFactory); this.nextProtocols = translateProtocols(nextProtocols); diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContext.java b/handler/src/main/java/io/netty/handler/ssl/SslContext.java index 7b04827fd7..e9f03aef7a 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContext.java @@ -133,6 +133,7 @@ public abstract class SslContext { * {@code null} if it's not password-protected. * @param ciphers the cipher suites to enable, in the order of preference. * {@code null} to use the default cipher suites. + * @param cipherFilter a filter to apply over the supplied list of ciphers * @param nextProtocols the application layer protocols to accept, in the order of preference. * {@code null} to disable TLS NPN/ALPN extension. * @param wrapperFactory a factory used to wrap the underlying {@link SSLEngine}. @@ -146,12 +147,12 @@ public abstract class SslContext { */ public static SslContext newServerContext( File certChainFile, File keyFile, String keyPassword, - Iterable ciphers, Iterable nextProtocols, - SslEngineWrapperFactory wrapperFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, + Iterable nextProtocols, SslEngineWrapperFactory wrapperFactory, long sessionCacheSize, long sessionTimeout) throws SSLException { return newServerContext( null, certChainFile, keyFile, keyPassword, - ciphers, nextProtocols, wrapperFactory, sessionCacheSize, sessionTimeout); + ciphers, cipherFilter, nextProtocols, wrapperFactory, sessionCacheSize, sessionTimeout); } /** @@ -181,8 +182,8 @@ public abstract class SslContext { */ public static SslContext newServerContext( SslProvider provider, File certChainFile, File keyFile, String keyPassword) throws SSLException { - return newServerContext(provider, certChainFile, keyFile, keyPassword, null, null, - DefaultSslWrapperFactory.instance(), 0, 0); + return newServerContext(provider, certChainFile, keyFile, keyPassword, null, IdentityCipherSuiteFilter.INSTANCE, + null, DefaultSslWrapperFactory.INSTANCE, 0, 0); } /** @@ -196,6 +197,8 @@ public abstract class SslContext { * {@code null} if it's not password-protected. * @param ciphers the cipher suites to enable, in the order of preference. * {@code null} to use the default cipher suites. + * @param cipherFilter a filter to apply over the supplied list of ciphers + * Only required if {@code provider} is {@link SslProvider#JDK} * @param nextProtocols the application layer protocols to accept, in the order of preference. * {@code null} to disable TLS NPN/ALPN extension. * @param wrapperFactory a factory used to wrap the underlying {@link SSLEngine}. @@ -210,8 +213,8 @@ public abstract class SslContext { public static SslContext newServerContext( SslProvider provider, File certChainFile, File keyFile, String keyPassword, - Iterable ciphers, Iterable nextProtocols, - SslEngineWrapperFactory wrapperFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, + Iterable nextProtocols, SslEngineWrapperFactory wrapperFactory, long sessionCacheSize, long sessionTimeout) throws SSLException { if (provider == null) { @@ -222,7 +225,7 @@ public abstract class SslContext { case JDK: return new JdkSslServerContext( certChainFile, keyFile, keyPassword, - ciphers, nextProtocols, wrapperFactory, sessionCacheSize, sessionTimeout); + ciphers, cipherFilter, nextProtocols, wrapperFactory, sessionCacheSize, sessionTimeout); case OPENSSL: return new OpenSslServerContext( certChainFile, keyFile, keyPassword, @@ -291,6 +294,7 @@ public abstract class SslContext { * {@code null} to use the default. * @param ciphers the cipher suites to enable, in the order of preference. * {@code null} to use the default cipher suites. + * @param cipherFilter a filter to apply over the supplied list of ciphers * @param nextProtocols the application layer protocols to accept, in the order of preference. * {@code null} to disable TLS NPN/ALPN extension. * @param wrapperFactory a factory used to wrap the underlying {@link SSLEngine}. @@ -305,12 +309,12 @@ public abstract class SslContext { */ public static SslContext newClientContext( File certChainFile, TrustManagerFactory trustManagerFactory, - Iterable ciphers, Iterable nextProtocols, - SslEngineWrapperFactory wrapperFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, + Iterable nextProtocols, SslEngineWrapperFactory wrapperFactory, long sessionCacheSize, long sessionTimeout) throws SSLException { return newClientContext( null, certChainFile, trustManagerFactory, - ciphers, nextProtocols, wrapperFactory, sessionCacheSize, sessionTimeout); + ciphers, cipherFilter, nextProtocols, wrapperFactory, sessionCacheSize, sessionTimeout); } /** @@ -370,8 +374,8 @@ public abstract class SslContext { */ public static SslContext newClientContext( SslProvider provider, File certChainFile, TrustManagerFactory trustManagerFactory) throws SSLException { - return newClientContext(provider, certChainFile, trustManagerFactory, null, null, - DefaultSslWrapperFactory.instance(), 0, 0); + return newClientContext(provider, certChainFile, trustManagerFactory, null, IdentityCipherSuiteFilter.INSTANCE, + null, DefaultSslWrapperFactory.INSTANCE, 0, 0); } /** @@ -386,6 +390,7 @@ public abstract class SslContext { * {@code null} to use the default. * @param ciphers the cipher suites to enable, in the order of preference. * {@code null} to use the default cipher suites. + * @param cipherFilter a filter to apply over the supplied list of ciphers * @param nextProtocols the application layer protocols to accept, in the order of preference. * {@code null} to disable TLS NPN/ALPN extension. * @param wrapperFactory a factory used to wrap the underlying {@link SSLEngine}. @@ -401,8 +406,8 @@ public abstract class SslContext { public static SslContext newClientContext( SslProvider provider, File certChainFile, TrustManagerFactory trustManagerFactory, - Iterable ciphers, Iterable nextProtocols, - SslEngineWrapperFactory wrapperFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, + Iterable nextProtocols, SslEngineWrapperFactory wrapperFactory, long sessionCacheSize, long sessionTimeout) throws SSLException { if (provider != null && provider != SslProvider.JDK) { @@ -411,7 +416,7 @@ public abstract class SslContext { return new JdkSslClientContext( certChainFile, trustManagerFactory, - ciphers, nextProtocols, wrapperFactory, sessionCacheSize, sessionTimeout); + ciphers, cipherFilter, nextProtocols, wrapperFactory, sessionCacheSize, sessionTimeout); } SslContext() { } diff --git a/handler/src/main/java/io/netty/handler/ssl/SupportedCipherSuiteFilter.java b/handler/src/main/java/io/netty/handler/ssl/SupportedCipherSuiteFilter.java new file mode 100644 index 0000000000..7c904327c3 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/SupportedCipherSuiteFilter.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ssl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * This class will filter all requested ciphers out that are not supported by the current {@link SSLEngine}. + */ +public final class SupportedCipherSuiteFilter implements CipherSuiteFilter { + public static final SupportedCipherSuiteFilter INSTANCE = new SupportedCipherSuiteFilter(); + + private SupportedCipherSuiteFilter() { } + + @Override + public String[] filterCipherSuites(Iterable ciphers, List defaultCiphers, + Set supportedCiphers) { + if (defaultCiphers == null) { + throw new NullPointerException("defaultCiphers"); + } + if (supportedCiphers == null) { + throw new NullPointerException("supportedCiphers"); + } + + final List newCiphers; + if (ciphers == null) { + newCiphers = new ArrayList(defaultCiphers.size()); + ciphers = defaultCiphers; + } else { + newCiphers = new ArrayList(supportedCiphers.size()); + } + for (String c : ciphers) { + if (c == null) { + break; + } + if (supportedCiphers.contains(c)) { + newCiphers.add(c); + } + } + return newCiphers.toArray(new String[newCiphers.size()]); + } + +} diff --git a/handler/src/test/java/io/netty/handler/ssl/CipherSuiteFilterTest.java b/handler/src/test/java/io/netty/handler/ssl/CipherSuiteFilterTest.java new file mode 100644 index 0000000000..679fced7f0 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/CipherSuiteFilterTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ssl; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import org.junit.Test; +import org.mockito.internal.util.collections.Sets; + +public class CipherSuiteFilterTest { + @Test + public void supportedCipherSuiteFilterNonEmpty() { + List ciphers = Arrays.asList("foo", "goo", "noooo", "aaaa"); + List defaultCiphers = Arrays.asList("FOO", "GOO"); + Set supportedCiphers = Sets.newSet("FOO", "aaaa", "bbbbbb", "GOO", "goo"); + CipherSuiteFilter filter = SupportedCipherSuiteFilter.INSTANCE; + String[] results = filter.filterCipherSuites(ciphers, defaultCiphers, supportedCiphers); + assertEquals(2, results.length); + assertEquals("goo", results[0]); + assertEquals("aaaa", results[1]); + } + + @Test + public void supportedCipherSuiteFilterToEmpty() { + List ciphers = Arrays.asList("foo", "goo", "nooooo"); + List defaultCiphers = Arrays.asList("FOO", "GOO"); + Set supportedCiphers = Sets.newSet("FOO", "aaaa", "bbbbbb", "GOO"); + CipherSuiteFilter filter = SupportedCipherSuiteFilter.INSTANCE; + String[] results = filter.filterCipherSuites(ciphers, defaultCiphers, supportedCiphers); + assertEquals(0, results.length); + } + + @Test + public void supportedCipherSuiteFilterToDefault() { + List defaultCiphers = Arrays.asList("FOO", "GOO"); + Set supportedCiphers = Sets.newSet("GOO", "FOO", "aaaa", "bbbbbb"); + CipherSuiteFilter filter = SupportedCipherSuiteFilter.INSTANCE; + String[] results = filter.filterCipherSuites(null, defaultCiphers, supportedCiphers); + assertEquals(2, results.length); + assertEquals("FOO", results[0]); + assertEquals("GOO", results[1]); + } + + @Test + public void defaulCipherSuiteNoFilter() { + List ciphers = Arrays.asList("foo", "goo", "nooooo"); + List defaultCiphers = Arrays.asList("FOO", "GOO"); + Set supportedCiphers = Sets.newSet("FOO", "aaaa", "bbbbbb", "GOO"); + CipherSuiteFilter filter = IdentityCipherSuiteFilter.INSTANCE; + String[] results = filter.filterCipherSuites(ciphers, defaultCiphers, supportedCiphers); + assertEquals(ciphers.size(), results.length); + for (int i = 0; i < ciphers.size(); ++i) { + assertEquals(ciphers.get(i), results[i]); + } + } + + @Test + public void defaulCipherSuiteTakeDefault() { + List defaultCiphers = Arrays.asList("FOO", "GOO"); + Set supportedCiphers = Sets.newSet("FOO", "aaaa", "bbbbbb", "GOO"); + CipherSuiteFilter filter = IdentityCipherSuiteFilter.INSTANCE; + String[] results = filter.filterCipherSuites(null, defaultCiphers, supportedCiphers); + assertEquals(defaultCiphers.size(), results.length); + for (int i = 0; i < defaultCiphers.size(); ++i) { + assertEquals(defaultCiphers.get(i), results[i]); + } + } +} diff --git a/handler/src/test/java/io/netty/handler/ssl/JettySslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/JettySslEngineTest.java index a5d3d5d688..9e92bd1116 100644 --- a/handler/src/test/java/io/netty/handler/ssl/JettySslEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/JettySslEngineTest.java @@ -15,12 +15,10 @@ */ package io.netty.handler.ssl; -import static org.junit.Assume.assumeNoException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.verify; +import static org.junit.Assume.assumeNoException; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; @@ -41,6 +39,7 @@ import io.netty.util.NetUtil; import java.net.InetSocketAddress; import java.security.cert.CertificateException; import java.util.Arrays; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -49,12 +48,13 @@ import javax.net.ssl.SSLException; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import static org.mockito.Mockito.verify; public class JettySslEngineTest { private static final String APPLICATION_LEVEL_PROTOCOL = "my-protocol"; - @Mock private MessageReciever serverReceiver; @Mock @@ -116,7 +116,7 @@ public class JettySslEngineTest { try { wrapper = JettyNpnSslEngineWrapper.instance(); } catch (SSLException e) { - // NPN availability is dependent on the java version. If NPN is not available because of + // NPN availability is dependent on the java version. If NPN is not available because of // java version incompatibility don't fail the test, but instead just skip the test assumeNoException(e); } @@ -130,7 +130,7 @@ public class JettySslEngineTest { try { wrapper = JettyAlpnSslEngineWrapper.instance(); } catch (SSLException e) { - // ALPN availability is dependent on the java version. If NPN is not available because of + // ALPN availability is dependent on the java version. If NPN is not available because of // java version incompatibility don't fail the test, but instead just skip the test assumeNoException(e); } @@ -139,12 +139,12 @@ public class JettySslEngineTest { } private void mySetup(SslEngineWrapperFactory wrapper) throws InterruptedException, SSLException, - CertificateException { + CertificateException { SelfSignedCertificate ssc = new SelfSignedCertificate(); serverSslCtx = SslContext.newServerContext(SslProvider.JDK, ssc.certificate(), ssc.privateKey(), null, null, - Arrays.asList(APPLICATION_LEVEL_PROTOCOL), wrapper, 0, 0); + IdentityCipherSuiteFilter.INSTANCE, Arrays.asList(APPLICATION_LEVEL_PROTOCOL), wrapper, 0, 0); clientSslCtx = SslContext.newClientContext(SslProvider.JDK, null, InsecureTrustManagerFactory.INSTANCE, null, - Arrays.asList(APPLICATION_LEVEL_PROTOCOL), wrapper, 0, 0); + IdentityCipherSuiteFilter.INSTANCE, Arrays.asList(APPLICATION_LEVEL_PROTOCOL), wrapper, 0, 0); serverConnectedChannel = null; sb = new ServerBootstrap(); @@ -182,8 +182,8 @@ public class JettySslEngineTest { } private void runTest() throws Exception { - ByteBuf clientMessage = Unpooled.copiedBuffer("I am a client".getBytes()); - ByteBuf serverMessage = Unpooled.copiedBuffer("I am a server".getBytes()); + final ByteBuf clientMessage = Unpooled.copiedBuffer("I am a client".getBytes()); + final ByteBuf serverMessage = Unpooled.copiedBuffer("I am a server".getBytes()); try { writeAndVerifyReceived(clientMessage.retain(), clientChannel, serverLatch, serverReceiver); writeAndVerifyReceived(serverMessage.retain(), serverConnectedChannel, clientLatch, clientReceiver); @@ -206,9 +206,21 @@ public class JettySslEngineTest { private static void writeAndVerifyReceived(ByteBuf message, Channel sendChannel, CountDownLatch receiverLatch, MessageReciever receiver) throws Exception { - sendChannel.writeAndFlush(message); - receiverLatch.await(5, TimeUnit.SECONDS); - message.resetReaderIndex(); - verify(receiver).messageReceived(eq(message)); + List dataCapture = null; + try { + sendChannel.writeAndFlush(message); + receiverLatch.await(5, TimeUnit.SECONDS); + message.resetReaderIndex(); + ArgumentCaptor captor = ArgumentCaptor.forClass(ByteBuf.class); + verify(receiver).messageReceived(captor.capture()); + dataCapture = captor.getAllValues(); + assertEquals(message, dataCapture.get(0)); + } finally { + if (dataCapture != null) { + for (ByteBuf data : dataCapture) { + data.release(); + } + } + } } }