diff --git a/common/src/main/java/io/netty/util/internal/ObjectUtil.java b/common/src/main/java/io/netty/util/internal/ObjectUtil.java new file mode 100644 index 0000000000..db127c109b --- /dev/null +++ b/common/src/main/java/io/netty/util/internal/ObjectUtil.java @@ -0,0 +1,35 @@ +/* + * 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.util.internal; + +/** + * A grab-bag of useful utility methods. + */ +public final class ObjectUtil { + + private ObjectUtil() { + } + + /** + * Checks that the given argument is not null. If it is, throws {@link NullPointerException}. + * Otherwise, returns the argument. + */ + public static T checkNotNull(T arg, String text) { + if (arg == null) { + throw new NullPointerException(text); + } + return arg; + } +} 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 bccbe9e560..1eddc5283d 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,11 +27,14 @@ 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.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.IdentityCipherSuiteFilter; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import java.util.Arrays; - /** * An SPDY client that allows you to send HTTP GET to a SPDY server. *

@@ -54,8 +57,13 @@ public final class SpdyClient { public static void main(String[] args) throws Exception { // Configure SSL. final SslContext sslCtx = SslContext.newClientContext( - null, InsecureTrustManagerFactory.INSTANCE, null, - Arrays.asList(SelectedProtocol.SPDY_3_1.protocolName(), SelectedProtocol.HTTP_1_1.protocolName()), + null, InsecureTrustManagerFactory.INSTANCE, null, IdentityCipherSuiteFilter.INSTANCE, + new ApplicationProtocolConfig( + Protocol.NPN, + SelectorFailureBehavior.FATAL_ALERT, + SelectedListenerFailureBehavior.FATAL_ALERT, + SelectedProtocol.SPDY_3_1.protocolName(), + SelectedProtocol.HTTP_1_1.protocolName()), 0, 0); HttpResponseClientHandler httpResponseHandler = new HttpResponseClientHandler(); 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 991d727a22..40ce2a9cc3 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,11 +24,14 @@ 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.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.IdentityCipherSuiteFilter; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.util.SelfSignedCertificate; -import java.util.Arrays; - /** * A SPDY Server that responds to a GET request with a Hello World. *

@@ -55,8 +58,13 @@ public final class SpdyServer { // Configure SSL. SelfSignedCertificate ssc = new SelfSignedCertificate(); SslContext sslCtx = SslContext.newServerContext( - ssc.certificate(), ssc.privateKey(), null, null, - Arrays.asList(SelectedProtocol.SPDY_3_1.protocolName(), SelectedProtocol.HTTP_1_1.protocolName()), + ssc.certificate(), ssc.privateKey(), null, null, IdentityCipherSuiteFilter.INSTANCE, + new ApplicationProtocolConfig( + Protocol.NPN, + SelectorFailureBehavior.FATAL_ALERT, + SelectedListenerFailureBehavior.FATAL_ALERT, + SelectedProtocol.SPDY_3_1.protocolName(), + SelectedProtocol.HTTP_1_1.protocolName()), 0, 0); // Configure the server. diff --git a/handler/pom.xml b/handler/pom.xml index 24918fc508..382b187714 100644 --- a/handler/pom.xml +++ b/handler/pom.xml @@ -66,6 +66,22 @@ provided true + + org.eclipse.jetty.alpn + alpn-api + true + + + org.mortbay.jetty.alpn + alpn-boot + provided + true + + + org.mockito + mockito-all + test + diff --git a/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolConfig.java b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolConfig.java new file mode 100644 index 0000000000..9552c75179 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolConfig.java @@ -0,0 +1,122 @@ +/* + * 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 io.netty.handler.ssl.ApplicationProtocolUtil.toList; +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import java.util.Collections; +import java.util.List; + +import javax.net.ssl.SSLEngine; + +/** + * Provides an {@link SSLEngine} agnostic way to configure a {@link ApplicationProtocolNegotiator}. + */ +public final class ApplicationProtocolConfig { + private final List supportedProtocols; + private final Protocol protocol; + private final SelectorFailureBehavior selectorBehavior; + private final SelectedListenerFailureBehavior selectedBehavior; + + /** + * Create a new instance. + * @param protocol The application protocol functionality to use. + * @param selectorBehavior How the peer selecting the protocol should behave. + * @param selectedBehavior How the peer being notified of the selected protocol should behave. + * @param supportedProtocols The order of iteration determines the preference of support for protocols. + */ + public ApplicationProtocolConfig(Protocol protocol, SelectorFailureBehavior selectorBehavior, + SelectedListenerFailureBehavior selectedBehavior, Iterable supportedProtocols) { + this(protocol, selectorBehavior, selectedBehavior, toList(supportedProtocols)); + } + + /** + * Create a new instance. + * @param protocol The application protocol functionality to use. + * @param selectorBehavior How the peer selecting the protocol should behave. + * @param selectedBehavior How the peer being notified of the selected protocol should behave. + * @param supportedProtocols The order of iteration determines the preference of support for protocols. + */ + public ApplicationProtocolConfig(Protocol protocol, SelectorFailureBehavior selectorBehavior, + SelectedListenerFailureBehavior selectedBehavior, String... supportedProtocols) { + this(protocol, selectorBehavior, selectedBehavior, toList(supportedProtocols)); + } + + /** + * Create a new instance. + * @param protocol The application protocol functionality to use. + * @param selectorBehavior How the peer selecting the protocol should behave. + * @param selectedBehavior How the peer being notified of the selected protocol should behave. + * @param supportedProtocols The order of iteration determines the preference of support for protocols. + */ + private ApplicationProtocolConfig(Protocol protocol, SelectorFailureBehavior selectorBehavior, + SelectedListenerFailureBehavior selectedBehavior, List supportedProtocols) { + this.supportedProtocols = Collections.unmodifiableList(checkNotNull(supportedProtocols, "supportedProtocols")); + this.protocol = checkNotNull(protocol, "protocol"); + this.selectorBehavior = checkNotNull(selectorBehavior, "selectorBehavior"); + this.selectedBehavior = checkNotNull(selectedBehavior, "selectedBehavior"); + } + + /** + * Defines which application level protocol negotiation to use. + */ + public enum Protocol { + NONE, NPN, ALPN, NPN_AND_ALPN + } + + /** + * Defines the most common behaviors for the peer that selects the application protocol. + */ + public enum SelectorFailureBehavior { + FATAL_ALERT, NO_ADVERTISE, CHOOSE_MY_LAST_PROTOCOL + } + + /** + * Defines the most common behaviors for the peer which is notified of the selected protocol. + */ + public enum SelectedListenerFailureBehavior { + ACCEPT, FATAL_ALERT, CHOOSE_MY_LAST_PROTOCOL + } + + /** + * The application level protocols supported. + */ + public List supportedProtocols() { + return supportedProtocols; + } + + /** + * Get which application level protocol negotiation to use. + */ + public Protocol protocol() { + return protocol; + } + + /** + * Get the desired behavior for the peer who selects the application protocol. + */ + public SelectorFailureBehavior selectorFailureBehavior() { + return selectorBehavior; + } + + /** + * Get the desired behavior for the peer who is notified of the selected protocol. + */ + public SelectedListenerFailureBehavior selectedListenerFailureBehavior() { + return selectedBehavior; + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiator.java b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiator.java new file mode 100644 index 0000000000..55f9e059d8 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiator.java @@ -0,0 +1,34 @@ +/* + * 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; + +/** + * Interface to support Application Protocol Negotiation. + *

+ * Default implementations are provided for: + *

+ */ +public interface ApplicationProtocolNegotiator { + /** + * Get the collection of application protocols supported by this application (in preference order). + */ + List protocols(); +} diff --git a/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolUtil.java b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolUtil.java new file mode 100644 index 0000000000..0581e9b2c2 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolUtil.java @@ -0,0 +1,77 @@ +/* + * 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; + +/** + * Utility class for application protocol common operations. + */ +final class ApplicationProtocolUtil { + private static final int DEFAULT_LIST_SIZE = 2; + + private ApplicationProtocolUtil() { + } + + static List toList(Iterable protocols) { + return toList(DEFAULT_LIST_SIZE, protocols); + } + + static List toList(int initialListSize, Iterable protocols) { + if (protocols == null) { + return null; + } + + List result = new ArrayList(initialListSize); + for (String p : protocols) { + if (p == null || p.isEmpty()) { + throw new IllegalArgumentException("protocol cannot be null or empty"); + } + result.add(p); + } + + if (result.isEmpty()) { + throw new IllegalArgumentException("protocols cannot empty"); + } + + return result; + } + + static List toList(String... protocols) { + return toList(DEFAULT_LIST_SIZE, protocols); + } + + static List toList(int initialListSize, String... protocols) { + if (protocols == null) { + return null; + } + + List result = new ArrayList(initialListSize); + for (String p : protocols) { + if (p == null || p.isEmpty()) { + throw new IllegalArgumentException("protocol cannot be null or empty"); + } + result.add(p); + } + + if (result.isEmpty()) { + throw new IllegalArgumentException("protocols cannot empty"); + } + + return result; + } +} 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/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/JdkAlpnApplicationProtocolNegotiator.java b/handler/src/main/java/io/netty/handler/ssl/JdkAlpnApplicationProtocolNegotiator.java new file mode 100644 index 0000000000..aaaf5b754e --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/JdkAlpnApplicationProtocolNegotiator.java @@ -0,0 +1,120 @@ +/* + * 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 javax.net.ssl.SSLEngine; + +/** + * The {@link JdkApplicationProtocolNegotiator} to use if you need ALPN and are using {@link SslProvider#JDK}. + */ +public final class JdkAlpnApplicationProtocolNegotiator extends JdkBaseApplicationProtocolNegotiator { + private static final SslEngineWrapperFactory ALPN_WRAPPER = new SslEngineWrapperFactory() { + { + if (!JdkAlpnSslEngine.isAvailable()) { + throw new RuntimeException("ALPN unsupported. Is your classpatch configured correctly?" + + " See http://www.eclipse.org/jetty/documentation/current/alpn-chapter.html#alpn-starting"); + } + } + + @Override + public SSLEngine wrapSslEngine(SSLEngine engine, JdkApplicationProtocolNegotiator applicationNegotiator, + boolean isServer) { + return new JdkAlpnSslEngine(engine, applicationNegotiator, isServer); + } + }; + + /** + * Create a new instance. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkAlpnApplicationProtocolNegotiator(Iterable protocols) { + this(false, protocols); + } + + /** + * Create a new instance. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkAlpnApplicationProtocolNegotiator(String... protocols) { + this(false, protocols); + } + + /** + * Create a new instance. + * @param failIfNoCommonProtocols Fail with a fatal alert if not common protocols are detected. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkAlpnApplicationProtocolNegotiator(boolean failIfNoCommonProtocols, Iterable protocols) { + this(failIfNoCommonProtocols, failIfNoCommonProtocols, protocols); + } + + /** + * Create a new instance. + * @param failIfNoCommonProtocols Fail with a fatal alert if not common protocols are detected. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkAlpnApplicationProtocolNegotiator(boolean failIfNoCommonProtocols, String... protocols) { + this(failIfNoCommonProtocols, failIfNoCommonProtocols, protocols); + } + + /** + * Create a new instance. + * @param clientFailIfNoCommonProtocols Client side fail with a fatal alert if not common protocols are detected. + * @param serverFailIfNoCommonProtocols Server side fail with a fatal alert if not common protocols are detected. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkAlpnApplicationProtocolNegotiator(boolean clientFailIfNoCommonProtocols, + boolean serverFailIfNoCommonProtocols, Iterable protocols) { + this(serverFailIfNoCommonProtocols ? FAIL_SELECTOR_FACTORY : NO_FAIL_SELECTOR_FACTORY, + clientFailIfNoCommonProtocols ? FAIL_SELECTION_LISTENER_FACTORY : NO_FAIL_SELECTION_LISTENER_FACTORY, + protocols); + } + + /** + * Create a new instance. + * @param clientFailIfNoCommonProtocols Client side fail with a fatal alert if not common protocols are detected. + * @param serverFailIfNoCommonProtocols Server side fail with a fatal alert if not common protocols are detected. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkAlpnApplicationProtocolNegotiator(boolean clientFailIfNoCommonProtocols, + boolean serverFailIfNoCommonProtocols, String... protocols) { + this(serverFailIfNoCommonProtocols ? FAIL_SELECTOR_FACTORY : NO_FAIL_SELECTOR_FACTORY, + clientFailIfNoCommonProtocols ? FAIL_SELECTION_LISTENER_FACTORY : NO_FAIL_SELECTION_LISTENER_FACTORY, + protocols); + } + + /** + * Create a new instance. + * @param selectorFactory The factory which provides classes responsible for selecting the protocol. + * @param listenerFactory The factory which provides to be notified of which protocol was selected. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkAlpnApplicationProtocolNegotiator(ProtocolSelectorFactory selectorFactory, + ProtocolSelectionListenerFactory listenerFactory, Iterable protocols) { + super(ALPN_WRAPPER, selectorFactory, listenerFactory, protocols); + } + + /** + * Create a new instance. + * @param selectorFactory The factory which provides classes responsible for selecting the protocol. + * @param listenerFactory The factory which provides to be notified of which protocol was selected. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkAlpnApplicationProtocolNegotiator(ProtocolSelectorFactory selectorFactory, + ProtocolSelectionListenerFactory listenerFactory, String... protocols) { + super(ALPN_WRAPPER, selectorFactory, listenerFactory, protocols); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkAlpnSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/JdkAlpnSslEngine.java new file mode 100644 index 0000000000..580b3f23f5 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/JdkAlpnSslEngine.java @@ -0,0 +1,122 @@ +/* + * 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 io.netty.util.internal.ObjectUtil.checkNotNull; +import io.netty.handler.ssl.JdkApplicationProtocolNegotiator.ProtocolSelectionListener; +import io.netty.handler.ssl.JdkApplicationProtocolNegotiator.ProtocolSelector; +import io.netty.util.internal.PlatformDependent; + +import java.util.HashSet; +import java.util.List; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import org.eclipse.jetty.alpn.ALPN; +import org.eclipse.jetty.alpn.ALPN.ClientProvider; +import org.eclipse.jetty.alpn.ALPN.ServerProvider; + +final class JdkAlpnSslEngine extends JdkSslEngine { + private static boolean available; + + static boolean isAvailable() { + updateAvailability(); + return available; + } + + private static void updateAvailability() { + if (available) { + return; + } + + try { + // Try to get the bootstrap class loader. + ClassLoader bootloader = ClassLoader.getSystemClassLoader().getParent(); + if (bootloader == null) { + // If failed, use the system class loader, + // although it's not perfect to tell if APLN extension has been loaded. + bootloader = ClassLoader.getSystemClassLoader(); + } + Class.forName("sun.security.ssl.ALPNExtension", true, bootloader); + available = true; + } catch (Exception ignore) { + // alpn-boot was not loaded. + } + } + + JdkAlpnSslEngine(SSLEngine engine, final JdkApplicationProtocolNegotiator applicationNegotiator, boolean server) { + super(engine); + checkNotNull(applicationNegotiator, "applicationNegotiator"); + + if (server) { + final ProtocolSelector protocolSelector = checkNotNull(applicationNegotiator.protocolSelectorFactory() + .newSelector(this, new HashSet(applicationNegotiator.protocols())), "protocolSelector"); + ALPN.put(engine, new ServerProvider() { + @Override + public String select(List protocols) { + try { + return protocolSelector.select(protocols); + } catch (Throwable t) { + PlatformDependent.throwException(t); + return null; + } + } + + @Override + public void unsupported() { + protocolSelector.unsupported(); + } + }); + } else { + final ProtocolSelectionListener protocolListener = checkNotNull(applicationNegotiator + .protocolListenerFactory().newListener(this, applicationNegotiator.protocols()), + "protocolListener"); + ALPN.put(engine, new ClientProvider() { + @Override + public List protocols() { + return applicationNegotiator.protocols(); + } + + @Override + public void selected(String protocol) { + try { + protocolListener.selected(protocol); + } catch (Throwable t) { + PlatformDependent.throwException(t); + } + } + + @Override + public void unsupported() { + protocolListener.unsupported(); + } + }); + } + } + + @Override + public void closeInbound() throws SSLException { + ALPN.remove(getWrappedEngine()); + super.closeInbound(); + } + + @Override + public void closeOutbound() { + ALPN.remove(getWrappedEngine()); + super.closeOutbound(); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkApplicationProtocolNegotiator.java b/handler/src/main/java/io/netty/handler/ssl/JdkApplicationProtocolNegotiator.java new file mode 100644 index 0000000000..cd7bf71404 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/JdkApplicationProtocolNegotiator.java @@ -0,0 +1,135 @@ +/* + * 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; + +import javax.net.ssl.SSLEngine; + +/** + * JDK extension methods to support {@link ApplicationProtocolNegotiator} + */ +public interface JdkApplicationProtocolNegotiator extends ApplicationProtocolNegotiator { + /** + * Abstract factory pattern for wrapping an {@link SSLEngine} object. This is useful for NPN/APLN JDK support. + */ + public interface SslEngineWrapperFactory { + /** + * Abstract factory pattern for wrapping an {@link SSLEngine} object. This is useful for NPN/APLN support. + * + * @param engine The engine to wrap. + * @param applicationNegotiator The application level protocol negotiator + * @param isServer
    + *
  • {@code true} if the engine is for server side of connections
  • + *
  • {@code false} if the engine is for client side of connections
  • + *
+ * @return The resulting wrapped engine. This may just be {@code engine}. + */ + SSLEngine wrapSslEngine(SSLEngine engine, JdkApplicationProtocolNegotiator applicationNegotiator, + boolean isServer); + } + + /** + * Interface to define the role of an application protocol selector in the SSL handshake process. Either + * {@link ProtocolSelector#unsupported()} OR {@link ProtocolSelector#select(List)} will be called for each SSL + * handshake. + */ + public interface ProtocolSelector { + /** + * Callback invoked to let the application know that the peer does not support this + * {@link ApplicationProtocolNegotiator}. + */ + void unsupported(); + + /** + * Callback invoked to select the application level protocol from the {@code protocols} provided. + * + * @param protocols the protocols sent by the protocol advertiser + * @return the protocol selected by this {@link ProtocolSelector}. A {@code null} value will indicate the no + * protocols were selected but the handshake should not fail. The decision to fail the handshake is left to the + * other end negotiating the SSL handshake. + * @throws Exception If the {@code protocols} provide warrant failing the SSL handshake with a fatal alert. + */ + String select(List protocols) throws Exception; + } + + /** + * A listener to be notified by which protocol was select by its peer. Either the + * {@link ProtocolSelectionListener#unsupported()} OR the {@link ProtocolSelectionListener#selected(String)} method + * will be called for each SSL handshake. + */ + public interface ProtocolSelectionListener { + /** + * Callback invoked to let the application know that the peer does not support this + * {@link ApplicationProtocolNegotiator}. + */ + void unsupported(); + + /** + * Callback invoked to let this application know the protocol chosen by the peer. + * + * @param protocol the protocol selected by the peer. May be {@code null} or empty as supported by the + * application negotiation protocol. + * @throws Exception This may be thrown if the selected protocol is not acceptable and the desired behavior is + * to fail the handshake with a fatal alert. + */ + void selected(String protocol) throws Exception; + } + + /** + * Factory interface for {@link ProtocolSelector} objects. + */ + public interface ProtocolSelectorFactory { + /** + * Generate a new instance of {@link ProtocolSelector}. + * @param engine The {@link SSLEngine} that the returned {@link ProtocolSelector} will be used to create an + * instance for. + * @param supportedProtocols The protocols that are supported. + * @return A new instance of {@link ProtocolSelector}. + */ + ProtocolSelector newSelector(SSLEngine engine, Set supportedProtocols); + } + + /** + * Factory interface for {@link ProtocolSelectionListener} objects. + */ + public interface ProtocolSelectionListenerFactory { + /** + * Generate a new instance of {@link ProtocolSelectionListener}. + * @param engine The {@link SSLEngine} that the returned {@link ProtocolSelectionListener} will be used to + * create an instance for. + * @param supportedProtocols The protocols that are supported in preference order. + * @return A new instance of {@link ProtocolSelectionListener}. + */ + ProtocolSelectionListener newListener(SSLEngine engine, List supportedProtocols); + } + + /** + * Get the {@link SslEngineWrapperFactory}. + */ + SslEngineWrapperFactory wrapperFactory(); + + /** + * Get the {@link ProtocolSelectorFactory}. + */ + ProtocolSelectorFactory protocolSelectorFactory(); + + /** + * Get the {@link ProtocolSelectionListenerFactory}. + */ + ProtocolSelectionListenerFactory protocolListenerFactory(); +} diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkBaseApplicationProtocolNegotiator.java b/handler/src/main/java/io/netty/handler/ssl/JdkBaseApplicationProtocolNegotiator.java new file mode 100644 index 0000000000..aacff48426 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/JdkBaseApplicationProtocolNegotiator.java @@ -0,0 +1,209 @@ +/* + * 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 io.netty.handler.ssl.ApplicationProtocolUtil.toList; +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLHandshakeException; + +/** + * Common base class for {@link JdkApplicationProtocolNegotiator} classes to inherit from. + */ +class JdkBaseApplicationProtocolNegotiator implements JdkApplicationProtocolNegotiator { + private final List protocols; + private final ProtocolSelectorFactory selectorFactory; + private final ProtocolSelectionListenerFactory listenerFactory; + private final SslEngineWrapperFactory wrapperFactory; + + /** + * Create a new instance. + * @param wrapperFactory Determines which application protocol will be used by wrapping the SSLEngine in use. + * @param selectorFactory How the peer selecting the protocol should behave. + * @param listenerFactory How the peer being notified of the selected protocol should behave. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + protected JdkBaseApplicationProtocolNegotiator(SslEngineWrapperFactory wrapperFactory, + ProtocolSelectorFactory selectorFactory, ProtocolSelectionListenerFactory listenerFactory, + Iterable protocols) { + this(wrapperFactory, selectorFactory, listenerFactory, toList(protocols)); + } + + /** + * Create a new instance. + * @param wrapperFactory Determines which application protocol will be used by wrapping the SSLEngine in use. + * @param selectorFactory How the peer selecting the protocol should behave. + * @param listenerFactory How the peer being notified of the selected protocol should behave. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + protected JdkBaseApplicationProtocolNegotiator(SslEngineWrapperFactory wrapperFactory, + ProtocolSelectorFactory selectorFactory, ProtocolSelectionListenerFactory listenerFactory, + String... protocols) { + this(wrapperFactory, selectorFactory, listenerFactory, toList(protocols)); + } + + /** + * Create a new instance. + * @param wrapperFactory Determines which application protocol will be used by wrapping the SSLEngine in use. + * @param selectorFactory How the peer selecting the protocol should behave. + * @param listenerFactory How the peer being notified of the selected protocol should behave. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + private JdkBaseApplicationProtocolNegotiator(SslEngineWrapperFactory wrapperFactory, + ProtocolSelectorFactory selectorFactory, ProtocolSelectionListenerFactory listenerFactory, + List protocols) { + this.wrapperFactory = checkNotNull(wrapperFactory, "wrapperFactory"); + this.selectorFactory = checkNotNull(selectorFactory, "selectorFactory"); + this.listenerFactory = checkNotNull(listenerFactory, "listenerFactory"); + this.protocols = Collections.unmodifiableList(checkNotNull(protocols, "protocols")); + } + + @Override + public List protocols() { + return protocols; + } + + @Override + public ProtocolSelectorFactory protocolSelectorFactory() { + return selectorFactory; + } + + @Override + public ProtocolSelectionListenerFactory protocolListenerFactory() { + return listenerFactory; + } + + @Override + public SslEngineWrapperFactory wrapperFactory() { + return wrapperFactory; + } + + static final ProtocolSelectorFactory FAIL_SELECTOR_FACTORY = new ProtocolSelectorFactory() { + @Override + public ProtocolSelector newSelector(SSLEngine engine, Set supportedProtocols) { + return new FailProtocolSelector((JdkSslEngine) engine, supportedProtocols); + } + }; + + static final ProtocolSelectorFactory NO_FAIL_SELECTOR_FACTORY = new ProtocolSelectorFactory() { + @Override + public ProtocolSelector newSelector(SSLEngine engine, Set supportedProtocols) { + return new NoFailProtocolSelector((JdkSslEngine) engine, supportedProtocols); + } + }; + + static final ProtocolSelectionListenerFactory FAIL_SELECTION_LISTENER_FACTORY = + new ProtocolSelectionListenerFactory() { + @Override + public ProtocolSelectionListener newListener(SSLEngine engine, List supportedProtocols) { + return new FailProtocolSelectionListener((JdkSslEngine) engine, supportedProtocols); + } + }; + + static final ProtocolSelectionListenerFactory NO_FAIL_SELECTION_LISTENER_FACTORY = + new ProtocolSelectionListenerFactory() { + @Override + public ProtocolSelectionListener newListener(SSLEngine engine, List supportedProtocols) { + return new NoFailProtocolSelectionListener((JdkSslEngine) engine, supportedProtocols); + } + }; + + protected static class NoFailProtocolSelector implements ProtocolSelector { + private final JdkSslEngine jettyWrapper; + private final Set supportedProtocols; + + public NoFailProtocolSelector(JdkSslEngine jettyWrapper, Set supportedProtocols) { + this.jettyWrapper = jettyWrapper; + this.supportedProtocols = supportedProtocols; + } + + @Override + public void unsupported() { + jettyWrapper.getSession().setApplicationProtocol(null); + } + + @Override + public String select(List protocols) throws Exception { + for (int i = 0; i < protocols.size(); ++i) { + String p = protocols.get(i); + if (supportedProtocols.contains(p)) { + jettyWrapper.getSession().setApplicationProtocol(p); + return p; + } + } + return noSelectMatchFound(); + } + + public String noSelectMatchFound() throws Exception { + jettyWrapper.getSession().setApplicationProtocol(null); + return null; + } + } + + protected static final class FailProtocolSelector extends NoFailProtocolSelector { + public FailProtocolSelector(JdkSslEngine jettyWrapper, Set supportedProtocols) { + super(jettyWrapper, supportedProtocols); + } + + @Override + public String noSelectMatchFound() throws Exception { + throw new SSLHandshakeException("Selected protocol is not supported"); + } + } + + protected static class NoFailProtocolSelectionListener implements ProtocolSelectionListener { + private final JdkSslEngine jettyWrapper; + private final List supportedProtocols; + + public NoFailProtocolSelectionListener(JdkSslEngine jettyWrapper, List supportedProtocols) { + this.jettyWrapper = jettyWrapper; + this.supportedProtocols = supportedProtocols; + } + + @Override + public void unsupported() { + jettyWrapper.getSession().setApplicationProtocol(null); + } + + @Override + public void selected(String protocol) throws Exception { + if (supportedProtocols.contains(protocol)) { + jettyWrapper.getSession().setApplicationProtocol(protocol); + } else { + noSelectedMatchFound(protocol); + } + } + + public void noSelectedMatchFound(String protocol) throws Exception { + } + } + + protected static final class FailProtocolSelectionListener extends NoFailProtocolSelectionListener { + public FailProtocolSelectionListener(JdkSslEngine jettyWrapper, List supportedProtocols) { + super(jettyWrapper, supportedProtocols); + } + + @Override + public void noSelectedMatchFound(String protocol) throws Exception { + throw new SSLHandshakeException("No compatible protocols found"); + } + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkDefaultApplicationProtocolNegotiator.java b/handler/src/main/java/io/netty/handler/ssl/JdkDefaultApplicationProtocolNegotiator.java new file mode 100644 index 0000000000..4e2a0bfe9d --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/JdkDefaultApplicationProtocolNegotiator.java @@ -0,0 +1,60 @@ +/* + * 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.Collections; +import java.util.List; + +import javax.net.ssl.SSLEngine; + +/** + * The {@link JdkApplicationProtocolNegotiator} to use if you do not care about NPN or ALPN and are using + * {@link SslProvider#JDK}. + */ +final class JdkDefaultApplicationProtocolNegotiator implements JdkApplicationProtocolNegotiator { + public static final JdkDefaultApplicationProtocolNegotiator INSTANCE = + new JdkDefaultApplicationProtocolNegotiator(); + private static final SslEngineWrapperFactory DEFAULT_SSL_ENGINE_WRAPPER_FACTORY = new SslEngineWrapperFactory() { + @Override + public SSLEngine wrapSslEngine(SSLEngine engine, JdkApplicationProtocolNegotiator applicationNegotiator, + boolean isServer) { + return engine; + } + }; + + private JdkDefaultApplicationProtocolNegotiator() { + } + + @Override + public SslEngineWrapperFactory wrapperFactory() { + return DEFAULT_SSL_ENGINE_WRAPPER_FACTORY; + } + + @Override + public ProtocolSelectorFactory protocolSelectorFactory() { + throw new UnsupportedOperationException("Application protocol negotiation unsupported"); + } + + @Override + public ProtocolSelectionListenerFactory protocolListenerFactory() { + throw new UnsupportedOperationException("Application protocol negotiation unsupported"); + } + + @Override + public List protocols() { + return Collections.emptyList(); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkNpnApplicationProtocolNegotiator.java b/handler/src/main/java/io/netty/handler/ssl/JdkNpnApplicationProtocolNegotiator.java new file mode 100644 index 0000000000..c893f057c4 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/JdkNpnApplicationProtocolNegotiator.java @@ -0,0 +1,120 @@ +/* + * 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 javax.net.ssl.SSLEngine; + +/** + * The {@link JdkApplicationProtocolNegotiator} to use if you need NPN and are using {@link SslProvider#JDK}. + */ +public final class JdkNpnApplicationProtocolNegotiator extends JdkBaseApplicationProtocolNegotiator { + private static final SslEngineWrapperFactory NPN_WRAPPER = new SslEngineWrapperFactory() { + { + if (!JdkNpnSslEngine.isAvailable()) { + throw new RuntimeException("NPN unsupported. Is your classpatch configured correctly?" + + " See http://www.eclipse.org/jetty/documentation/current/npn-chapter.html#npn-starting"); + } + } + + @Override + public SSLEngine wrapSslEngine(SSLEngine engine, JdkApplicationProtocolNegotiator applicationNegotiator, + boolean isServer) { + return new JdkNpnSslEngine(engine, applicationNegotiator, isServer); + } + }; + + /** + * Create a new instance. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkNpnApplicationProtocolNegotiator(Iterable protocols) { + this(false, protocols); + } + + /** + * Create a new instance. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkNpnApplicationProtocolNegotiator(String... protocols) { + this(false, protocols); + } + + /** + * Create a new instance. + * @param failIfNoCommonProtocols Fail with a fatal alert if not common protocols are detected. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkNpnApplicationProtocolNegotiator(boolean failIfNoCommonProtocols, Iterable protocols) { + this(failIfNoCommonProtocols, failIfNoCommonProtocols, protocols); + } + + /** + * Create a new instance. + * @param failIfNoCommonProtocols Fail with a fatal alert if not common protocols are detected. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkNpnApplicationProtocolNegotiator(boolean failIfNoCommonProtocols, String... protocols) { + this(failIfNoCommonProtocols, failIfNoCommonProtocols, protocols); + } + + /** + * Create a new instance. + * @param clientFailIfNoCommonProtocols Client side fail with a fatal alert if not common protocols are detected. + * @param serverFailIfNoCommonProtocols Server side fail with a fatal alert if not common protocols are detected. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkNpnApplicationProtocolNegotiator(boolean clientFailIfNoCommonProtocols, + boolean serverFailIfNoCommonProtocols, Iterable protocols) { + this(clientFailIfNoCommonProtocols ? FAIL_SELECTOR_FACTORY : NO_FAIL_SELECTOR_FACTORY, + serverFailIfNoCommonProtocols ? FAIL_SELECTION_LISTENER_FACTORY : NO_FAIL_SELECTION_LISTENER_FACTORY, + protocols); + } + + /** + * Create a new instance. + * @param clientFailIfNoCommonProtocols Client side fail with a fatal alert if not common protocols are detected. + * @param serverFailIfNoCommonProtocols Server side fail with a fatal alert if not common protocols are detected. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkNpnApplicationProtocolNegotiator(boolean clientFailIfNoCommonProtocols, + boolean serverFailIfNoCommonProtocols, String... protocols) { + this(clientFailIfNoCommonProtocols ? FAIL_SELECTOR_FACTORY : NO_FAIL_SELECTOR_FACTORY, + serverFailIfNoCommonProtocols ? FAIL_SELECTION_LISTENER_FACTORY : NO_FAIL_SELECTION_LISTENER_FACTORY, + protocols); + } + + /** + * Create a new instance. + * @param selectorFactory The factory which provides classes responsible for selecting the protocol. + * @param listenerFactory The factory which provides to be notified of which protocol was selected. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkNpnApplicationProtocolNegotiator(ProtocolSelectorFactory selectorFactory, + ProtocolSelectionListenerFactory listenerFactory, Iterable protocols) { + super(NPN_WRAPPER, selectorFactory, listenerFactory, protocols); + } + + /** + * Create a new instance. + * @param selectorFactory The factory which provides classes responsible for selecting the protocol. + * @param listenerFactory The factory which provides to be notified of which protocol was selected. + * @param protocols The order of iteration determines the preference of support for protocols. + */ + public JdkNpnApplicationProtocolNegotiator(ProtocolSelectorFactory selectorFactory, + ProtocolSelectionListenerFactory listenerFactory, String... protocols) { + super(NPN_WRAPPER, selectorFactory, listenerFactory, protocols); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkNpnSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/JdkNpnSslEngine.java new file mode 100644 index 0000000000..4864c6b568 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/JdkNpnSslEngine.java @@ -0,0 +1,127 @@ +/* + * 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 io.netty.util.internal.ObjectUtil.checkNotNull; +import io.netty.handler.ssl.JdkApplicationProtocolNegotiator.ProtocolSelectionListener; +import io.netty.handler.ssl.JdkApplicationProtocolNegotiator.ProtocolSelector; +import io.netty.util.internal.PlatformDependent; + +import java.util.HashSet; +import java.util.List; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import org.eclipse.jetty.npn.NextProtoNego; +import org.eclipse.jetty.npn.NextProtoNego.ClientProvider; +import org.eclipse.jetty.npn.NextProtoNego.ServerProvider; + +final class JdkNpnSslEngine extends JdkSslEngine { + private static boolean available; + + static boolean isAvailable() { + updateAvailability(); + return available; + } + + private static void updateAvailability() { + if (available) { + return; + } + try { + // Try to get the bootstrap class loader. + ClassLoader bootloader = ClassLoader.getSystemClassLoader().getParent(); + if (bootloader == null) { + // If failed, use the system class loader, + // although it's not perfect to tell if NPN extension has been loaded. + bootloader = ClassLoader.getSystemClassLoader(); + } + Class.forName("sun.security.ssl.NextProtoNegoExtension", true, bootloader); + available = true; + } catch (Exception ignore) { + // npn-boot was not loaded. + } + } + + JdkNpnSslEngine(SSLEngine engine, final JdkApplicationProtocolNegotiator applicationNegotiator, boolean server) { + super(engine); + checkNotNull(applicationNegotiator, "applicationNegotiator"); + + if (server) { + final ProtocolSelectionListener protocolListener = checkNotNull(applicationNegotiator + .protocolListenerFactory().newListener(this, applicationNegotiator.protocols()), + "protocolListener"); + NextProtoNego.put(engine, new ServerProvider() { + @Override + public void unsupported() { + protocolListener.unsupported(); + } + + @Override + public List protocols() { + return applicationNegotiator.protocols(); + } + + @Override + public void protocolSelected(String protocol) { + try { + protocolListener.selected(protocol); + } catch (Throwable t) { + PlatformDependent.throwException(t); + } + } + }); + } else { + final ProtocolSelector protocolSelector = checkNotNull(applicationNegotiator.protocolSelectorFactory() + .newSelector(this, new HashSet(applicationNegotiator.protocols())), "protocolSelector"); + NextProtoNego.put(engine, new ClientProvider() { + @Override + public boolean supports() { + return true; + } + + @Override + public void unsupported() { + protocolSelector.unsupported(); + } + + @Override + public String selectProtocol(List protocols) { + try { + return protocolSelector.select(protocols); + } catch (Throwable t) { + PlatformDependent.throwException(t); + return null; + } + } + }); + } + } + + @Override + public void closeInbound() throws SSLException { + NextProtoNego.remove(getWrappedEngine()); + super.closeInbound(); + } + + @Override + public void closeOutbound() { + NextProtoNego.remove(getWrappedEngine()); + super.closeOutbound(); + } +} 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 07a0d1db98..479bb24e57 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java @@ -16,22 +16,16 @@ package io.netty.handler.ssl; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufInputStream; +import java.io.File; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSessionContext; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; -import javax.security.auth.x500.X500Principal; -import java.io.File; -import java.security.KeyStore; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; /** * A client-side {@link SslContext} which uses JDK's SSL/TLS implementation. @@ -39,13 +33,12 @@ import java.util.List; public final class JdkSslClientContext extends JdkSslContext { private final SSLContext ctx; - private final List nextProtocols; /** * Creates a new instance. */ public JdkSslClientContext() throws SSLException { - this(null, null, null, null, 0, 0); + this(null, null); } /** @@ -79,7 +72,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, 0, 0); + this(certChainFile, trustManagerFactory, null, IdentityCipherSuiteFilter.INSTANCE, + JdkDefaultApplicationProtocolNegotiator.INSTANCE, 0, 0); } /** @@ -92,8 +86,8 @@ 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 nextProtocols the application layer protocols to accept, in the order of preference. - * {@code null} to disable TLS NPN/ALPN extension. + * @param cipherFilter a filter to apply over the supplied list of ciphers + * @param apn Provides a means to configure parameters related to application protocol negotiation. * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. @@ -101,65 +95,120 @@ public final class JdkSslClientContext extends JdkSslContext { */ public JdkSslClientContext( File certChainFile, TrustManagerFactory trustManagerFactory, - Iterable ciphers, Iterable nextProtocols, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout) throws SSLException { + this(certChainFile, trustManagerFactory, ciphers, cipherFilter, + toNegotiator(apn, false), sessionCacheSize, sessionTimeout); + } - super(ciphers); + /** + * Creates a new instance. + * + * @param certChainFile an X.509 certificate chain file in PEM format. + * {@code null} to use the system default + * @param trustManagerFactory the {@link TrustManagerFactory} that provides the {@link TrustManager}s + * that verifies the certificates sent from servers. + * {@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 apn Application Protocol Negotiator object. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + */ + public JdkSslClientContext( + File certChainFile, TrustManagerFactory trustManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(certChainFile, trustManagerFactory, null, null, null, null, + ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); + } - if (nextProtocols != null && nextProtocols.iterator().hasNext()) { - if (!JettyNpnSslEngine.isAvailable()) { - throw new SSLException("NPN/ALPN unsupported: " + nextProtocols); - } + /** + * Creates a new instance. + * @param trustCertChainFile an X.509 certificate chain file in PEM format. + * {@code null} to use the system default + * @param trustManagerFactory the {@link TrustManagerFactory} that provides the {@link TrustManager}s + * that verifies the certificates sent from servers. + * {@code null} to use the default or the results of parsing {@code trustCertChainFile} + * @param keyCertChainFile an X.509 certificate chain file in PEM format. + * This provides the public key for mutual authentication. + * {@code null} to use the system default + * @param keyFile a PKCS#8 private key file in PEM format. + * This provides the private key for mutual authentication. + * {@code null} for no mutual authentication. + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * Ignored if {@code keyFile} is {@code null}. + * @param keyManagerFactory the {@link KeyManagerFactory} that provides the {@link KeyManager}s + * that is used to encrypt data being sent to servers. + * {@code null} to use the default or the results of parsing + * {@code keyCertChainFile} and {@code keyFile}. + * @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 apn Provides a means to configure parameters related to application protocol negotiation. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + */ + public JdkSslClientContext(File trustCertChainFile, TrustManagerFactory trustManagerFactory, + File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(trustCertChainFile, trustManagerFactory, keyCertChainFile, keyFile, keyPassword, keyManagerFactory, + ciphers, cipherFilter, toNegotiator(apn, false), sessionCacheSize, sessionTimeout); + } - List nextProtoList = new ArrayList(); - for (String p: nextProtocols) { - if (p == null) { - break; - } - nextProtoList.add(p); - } - this.nextProtocols = Collections.unmodifiableList(nextProtoList); - } else { - this.nextProtocols = Collections.emptyList(); - } + /** + * Creates a new instance. + * @param trustCertChainFile an X.509 certificate chain file in PEM format. + * {@code null} to use the system default + * @param trustManagerFactory the {@link TrustManagerFactory} that provides the {@link TrustManager}s + * that verifies the certificates sent from servers. + * {@code null} to use the default or the results of parsing {@code trustCertChainFile} + * @param keyCertChainFile an X.509 certificate chain file in PEM format. + * This provides the public key for mutual authentication. + * {@code null} to use the system default + * @param keyFile a PKCS#8 private key file in PEM format. + * This provides the private key for mutual authentication. + * {@code null} for no mutual authentication. + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * Ignored if {@code keyFile} is {@code null}. + * @param keyManagerFactory the {@link KeyManagerFactory} that provides the {@link KeyManager}s + * that is used to encrypt data being sent to servers. + * {@code null} to use the default or the results of parsing + * {@code keyCertChainFile} and {@code keyFile}. + * @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 apn Application Protocol Negotiator object. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + */ + public JdkSslClientContext(File trustCertChainFile, TrustManagerFactory trustManagerFactory, + File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + super(ciphers, cipherFilter, apn); try { - if (certChainFile == null) { - ctx = SSLContext.getInstance(PROTOCOL); - if (trustManagerFactory == null) { - ctx.init(null, null, null); - } else { - trustManagerFactory.init((KeyStore) null); - ctx.init(null, trustManagerFactory.getTrustManagers(), null); - } - } else { - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(null, null); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - - ByteBuf[] certs = PemReader.readCertificates(certChainFile); - try { - for (ByteBuf buf: certs) { - X509Certificate cert = (X509Certificate) cf.generateCertificate(new ByteBufInputStream(buf)); - X500Principal principal = cert.getSubjectX500Principal(); - ks.setCertificateEntry(principal.getName("RFC2253"), cert); - } - } finally { - for (ByteBuf buf: certs) { - buf.release(); - } - } - - // Set up trust manager factory to use our key store. - if (trustManagerFactory == null) { - trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - } - trustManagerFactory.init(ks); - - // Initialize the SSLContext to work with the trust managers. - ctx = SSLContext.getInstance(PROTOCOL); - ctx.init(null, trustManagerFactory.getTrustManagers(), null); + if (trustCertChainFile != null) { + trustManagerFactory = buildTrustManagerFactory(trustCertChainFile, trustManagerFactory); } + if (keyFile != null) { + keyManagerFactory = buildKeyManagerFactory(keyCertChainFile, keyFile, keyPassword, keyManagerFactory); + } + ctx = SSLContext.getInstance(PROTOCOL); + ctx.init(keyManagerFactory == null ? null : keyManagerFactory.getKeyManagers(), + trustManagerFactory == null ? null : trustManagerFactory.getTrustManagers(), + null); SSLSessionContext sessCtx = ctx.getClientSessionContext(); if (sessionCacheSize > 0) { @@ -169,7 +218,7 @@ public final class JdkSslClientContext extends JdkSslContext { sessCtx.setSessionTimeout((int) Math.min(sessionTimeout, Integer.MAX_VALUE)); } } catch (Exception e) { - throw new SSLException("failed to initialize the server-side SSL context", e); + throw new SSLException("failed to initialize the client-side SSL context", e); } } @@ -178,11 +227,6 @@ public final class JdkSslClientContext extends JdkSslContext { return true; } - @Override - public List nextProtocols() { - return nextProtocols; - } - @Override public SSLContext context() { return ctx; 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 0ccd30c58d..08ae41b112 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java @@ -16,17 +16,50 @@ package io.netty.handler.ssl; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufInputStream; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLSessionContext; +import java.io.File; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyException; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Security; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; 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.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.TrustManagerFactory; +import javax.security.auth.x500.X500Principal; /** * An {@link SslContext} which uses JDK's SSL/TLS implementation. @@ -38,9 +71,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); @@ -51,10 +86,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"); if (!protocols.isEmpty()) { @@ -64,10 +103,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", @@ -97,22 +140,28 @@ 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); } } } private final String[] cipherSuites; private final List unmodifiableCipherSuites; + private final JdkApplicationProtocolNegotiator apn; - JdkSslContext(Iterable ciphers) { - cipherSuites = toCipherSuiteArray(ciphers); + JdkSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig config, + boolean isServer) { + this(ciphers, cipherFilter, toNegotiator(config, isServer)); + } + + JdkSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn) { + this.apn = checkNotNull(apn, "apn"); + cipherSuites = checkNotNull(cipherFilter, "cipherFilter").filterCipherSuites( + ciphers, DEFAULT_CIPHERS, SUPPORTED_CIPHERS); unmodifiableCipherSuites = Collections.unmodifiableList(Arrays.asList(cipherSuites)); } @@ -166,25 +215,227 @@ public abstract class JdkSslContext extends SslContext { } private SSLEngine wrapEngine(SSLEngine engine) { - if (nextProtocols().isEmpty()) { - return engine; - } else { - return new JettyNpnSslEngine(engine, nextProtocols(), isServer()); + return apn.wrapperFactory().wrapSslEngine(engine, apn, isServer()); + } + + @Override + public JdkApplicationProtocolNegotiator applicationProtocolNegotiator() { + return apn; + } + + /** + * Translate a {@link ApplicationProtocolConfiguration} object to a {@link JdkApplicationProtocolNegotiator} object. + * @param config The configuration which defines the translation + * @param isServer {@code true} if a server {@code false} otherwise. + * @return The results of the translation + */ + static JdkApplicationProtocolNegotiator toNegotiator(ApplicationProtocolConfig config, boolean isServer) { + if (config == null) { + return JdkDefaultApplicationProtocolNegotiator.INSTANCE; + } + + switch(config.protocol()) { + case NONE: + return JdkDefaultApplicationProtocolNegotiator.INSTANCE; + case ALPN: + if (isServer) { + switch(config.selectorFailureBehavior()) { + case FATAL_ALERT: + return new JdkAlpnApplicationProtocolNegotiator(true, config.supportedProtocols()); + case NO_ADVERTISE: + return new JdkAlpnApplicationProtocolNegotiator(false, config.supportedProtocols()); + default: + throw new UnsupportedOperationException(new StringBuilder("JDK provider does not support ") + .append(config.selectorFailureBehavior()).append(" failure behavior").toString()); + } + } else { + switch(config.selectedListenerFailureBehavior()) { + case ACCEPT: + return new JdkAlpnApplicationProtocolNegotiator(false, config.supportedProtocols()); + case FATAL_ALERT: + return new JdkAlpnApplicationProtocolNegotiator(true, config.supportedProtocols()); + default: + throw new UnsupportedOperationException(new StringBuilder("JDK provider does not support ") + .append(config.selectedListenerFailureBehavior()).append(" failure behavior").toString()); + } + } + case NPN: + if (isServer) { + switch(config.selectedListenerFailureBehavior()) { + case ACCEPT: + return new JdkNpnApplicationProtocolNegotiator(false, config.supportedProtocols()); + case FATAL_ALERT: + return new JdkNpnApplicationProtocolNegotiator(true, config.supportedProtocols()); + default: + throw new UnsupportedOperationException(new StringBuilder("JDK provider does not support ") + .append(config.selectedListenerFailureBehavior()).append(" failure behavior").toString()); + } + } else { + switch(config.selectorFailureBehavior()) { + case FATAL_ALERT: + return new JdkNpnApplicationProtocolNegotiator(true, config.supportedProtocols()); + case NO_ADVERTISE: + return new JdkNpnApplicationProtocolNegotiator(false, config.supportedProtocols()); + default: + throw new UnsupportedOperationException(new StringBuilder("JDK provider does not support ") + .append(config.selectorFailureBehavior()).append(" failure behavior").toString()); + } + } + default: + throw new UnsupportedOperationException(new StringBuilder("JDK provider does not support ") + .append(config.protocol()).append(" protocol").toString()); } } - 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()]); + /** + * Build a {@link KeyManagerFactory} based upon a key file, key file password, and a certificate chain. + * @param certChainFile a X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param kmf The existing {@link KeyManagerFactory} that will be used if not {@code null} + * @return A {@link KeyManagerFactory} based upon a key file, key file password, and a certificate chain. + */ + protected static KeyManagerFactory buildKeyManagerFactory(File certChainFile, File keyFile, String keyPassword, + KeyManagerFactory kmf) + throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeySpecException, InvalidAlgorithmParameterException, + CertificateException, KeyException, IOException { + String algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm"); + if (algorithm == null) { + algorithm = "SunX509"; } + return buildKeyManagerFactory(certChainFile, algorithm, keyFile, keyPassword, kmf); + } + + /** + * Build a {@link KeyManagerFactory} based upon a key algorithm, key file, key file password, + * and a certificate chain. + * @param certChainFile a X.509 certificate chain file in PEM format + * @param keyAlgorithm the standard name of the requested algorithm. See the Java Secure Socket Extension + * Reference Guide for information about standard algorithm names. + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param kmf The existing {@link KeyManagerFactory} that will be used if not {@code null} + * @return A {@link KeyManagerFactory} based upon a key algorithm, key file, key file password, + * and a certificate chain. + */ + protected static KeyManagerFactory buildKeyManagerFactory(File certChainFile, + String keyAlgorithm, File keyFile, String keyPassword, KeyManagerFactory kmf) + throws KeyStoreException, NoSuchAlgorithmException, NoSuchPaddingException, + InvalidKeySpecException, InvalidAlgorithmParameterException, IOException, + CertificateException, KeyException, UnrecoverableKeyException { + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(null, null); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + KeyFactory rsaKF = KeyFactory.getInstance("RSA"); + KeyFactory dsaKF = KeyFactory.getInstance("DSA"); + + ByteBuf encodedKeyBuf = PemReader.readPrivateKey(keyFile); + byte[] encodedKey = new byte[encodedKeyBuf.readableBytes()]; + encodedKeyBuf.readBytes(encodedKey).release(); + + char[] keyPasswordChars = keyPassword == null ? new char[0] : keyPassword.toCharArray(); + PKCS8EncodedKeySpec encodedKeySpec = generateKeySpec(keyPasswordChars, encodedKey); + + PrivateKey key; + try { + key = rsaKF.generatePrivate(encodedKeySpec); + } catch (InvalidKeySpecException ignore) { + key = dsaKF.generatePrivate(encodedKeySpec); + } + + List certChain = new ArrayList(); + ByteBuf[] certs = PemReader.readCertificates(certChainFile); + try { + for (ByteBuf buf: certs) { + certChain.add(cf.generateCertificate(new ByteBufInputStream(buf))); + } + } finally { + for (ByteBuf buf: certs) { + buf.release(); + } + } + + ks.setKeyEntry("key", key, keyPasswordChars, certChain.toArray(new Certificate[certChain.size()])); + + // Set up key manager factory to use our key store + if (kmf == null) { + kmf = KeyManagerFactory.getInstance(keyAlgorithm); + } + kmf.init(ks, keyPasswordChars); + + return kmf; + } + + /** + * Build a {@link TrustManagerFactory} from a certificate chain file. + * @param certChainFile The certificate file to build from. + * @param trustManagerFactory The existing {@link TrustManagerFactory} that will be used if not {@code null}. + * @return A {@link TrustManagerFactory} which contains the certificates in {@code certChainFile} + */ + protected static TrustManagerFactory buildTrustManagerFactory(File certChainFile, + TrustManagerFactory trustManagerFactory) + throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException { + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(null, null); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + ByteBuf[] certs = PemReader.readCertificates(certChainFile); + try { + for (ByteBuf buf: certs) { + X509Certificate cert = (X509Certificate) cf.generateCertificate(new ByteBufInputStream(buf)); + X500Principal principal = cert.getSubjectX500Principal(); + ks.setCertificateEntry(principal.getName("RFC2253"), cert); + } + } finally { + for (ByteBuf buf: certs) { + buf.release(); + } + } + + // Set up trust manager factory to use our key store. + if (trustManagerFactory == null) { + trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + } + trustManagerFactory.init(ks); + + return trustManagerFactory; + } + + /** + * Generates a key specification for an (encrypted) private key. + * + * @param password characters, if {@code null} or empty an unencrypted key is assumed + * @param key bytes of the DER encoded private key + * + * @return a key specification + * + * @throws IOException if parsing {@code key} fails + * @throws NoSuchAlgorithmException if the algorithm used to encrypt {@code key} is unkown + * @throws NoSuchPaddingException if the padding scheme specified in the decryption algorithm is unkown + * @throws InvalidKeySpecException if the decryption key based on {@code password} cannot be generated + * @throws InvalidKeyException if the decryption key based on {@code password} cannot be used to decrypt + * {@code key} + * @throws InvalidAlgorithmParameterException if decryption algorithm parameters are somehow faulty + */ + private static PKCS8EncodedKeySpec generateKeySpec(char[] password, byte[] key) + throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, + InvalidKeyException, InvalidAlgorithmParameterException { + + if (password == null || password.length == 0) { + return new PKCS8EncodedKeySpec(key); + } + + EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(key); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName()); + PBEKeySpec pbeKeySpec = new PBEKeySpec(password); + SecretKey pbeKey = keyFactory.generateSecret(pbeKeySpec); + + Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName()); + cipher.init(Cipher.DECRYPT_MODE, pbeKey, encryptedPrivateKeyInfo.getAlgParameters()); + + return encryptedPrivateKeyInfo.getKeySpec(cipher); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/JettyNpnSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslEngine.java similarity index 63% rename from handler/src/main/java/io/netty/handler/ssl/JettyNpnSslEngine.java rename to handler/src/main/java/io/netty/handler/ssl/JdkSslEngine.java index 49e9c3dc2d..680eea6195 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JettyNpnSslEngine.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslEngine.java @@ -13,12 +13,9 @@ * License for the specific language governing permissions and limitations * under the License. */ - package io.netty.handler.ssl; -import org.eclipse.jetty.npn.NextProtoNego; -import org.eclipse.jetty.npn.NextProtoNego.ClientProvider; -import org.eclipse.jetty.npn.NextProtoNego.ServerProvider; +import java.nio.ByteBuffer; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; @@ -26,105 +23,32 @@ import javax.net.ssl.SSLEngineResult.HandshakeStatus; import javax.net.ssl.SSLException; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; -import java.nio.ByteBuffer; -import java.util.List; - -final class JettyNpnSslEngine extends SSLEngine { - - private static boolean available; - - static boolean isAvailable() { - updateAvailability(); - return available; - } - - private static void updateAvailability() { - if (available) { - return; - } - try { - // Try to get the bootstrap class loader. - ClassLoader bootloader = ClassLoader.getSystemClassLoader().getParent(); - if (bootloader == null) { - // If failed, use the system class loader, - // although it's not perfect to tell if NPN extension has been loaded. - bootloader = ClassLoader.getSystemClassLoader(); - } - Class.forName("sun.security.ssl.NextProtoNegoExtension", true, bootloader); - available = true; - } catch (Exception ignore) { - // npn-boot was not loaded. - } - } +class JdkSslEngine extends SSLEngine { private final SSLEngine engine; - private final JettyNpnSslSession session; - - JettyNpnSslEngine(SSLEngine engine, final List nextProtocols, boolean server) { - assert !nextProtocols.isEmpty(); + private final JdkSslSession session; + JdkSslEngine(SSLEngine engine) { this.engine = engine; - session = new JettyNpnSslSession(engine); - - if (server) { - NextProtoNego.put(engine, new ServerProvider() { - @Override - public void unsupported() { - getSession().setApplicationProtocol(nextProtocols.get(nextProtocols.size() - 1)); - } - - @Override - public List protocols() { - return nextProtocols; - } - - @Override - public void protocolSelected(String protocol) { - getSession().setApplicationProtocol(protocol); - } - }); - } else { - final String[] list = nextProtocols.toArray(new String[nextProtocols.size()]); - final String fallback = list[list.length - 1]; - - NextProtoNego.put(engine, new ClientProvider() { - @Override - public boolean supports() { - return true; - } - - @Override - public void unsupported() { - session.setApplicationProtocol(null); - } - - @Override - public String selectProtocol(List protocols) { - for (String p: list) { - if (protocols.contains(p)) { - return p; - } - } - return fallback; - } - }); - } + session = new JdkSslSession(engine); } @Override - public JettyNpnSslSession getSession() { + public JdkSslSession getSession() { return session; } + public SSLEngine getWrappedEngine() { + return engine; + } + @Override public void closeInbound() throws SSLException { - NextProtoNego.remove(engine); engine.closeInbound(); } @Override public void closeOutbound() { - NextProtoNego.remove(engine); engine.closeOutbound(); } 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 34b08f29c3..118f8f37f0 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java @@ -16,35 +16,15 @@ package io.netty.handler.ssl; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufInputStream; +import java.io.File; -import javax.crypto.Cipher; -import javax.crypto.EncryptedPrivateKeyInfo; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; +import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSessionContext; -import java.io.File; -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.KeyFactory; -import java.security.KeyStore; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.Security; -import java.security.cert.Certificate; -import java.security.cert.CertificateFactory; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; /** * A server-side {@link SslContext} which uses JDK's SSL/TLS implementation. @@ -52,7 +32,6 @@ import java.util.List; public final class JdkSslServerContext extends JdkSslContext { private final SSLContext ctx; - private final List nextProtocols; /** * Creates a new instance. @@ -73,7 +52,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, 0, 0); + this(certChainFile, keyFile, keyPassword, null, IdentityCipherSuiteFilter.INSTANCE, + JdkDefaultApplicationProtocolNegotiator.INSTANCE, 0, 0); } /** @@ -85,8 +65,8 @@ 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 nextProtocols the application layer protocols to accept, in the order of preference. - * {@code null} to disable TLS NPN/ALPN extension. + * @param cipherFilter a filter to apply over the supplied list of ciphers + * @param apn Provides a means to configure parameters related to application protocol negotiation. * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. @@ -94,87 +74,118 @@ public final class JdkSslServerContext extends JdkSslContext { */ public JdkSslServerContext( File certChainFile, File keyFile, String keyPassword, - Iterable ciphers, Iterable nextProtocols, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout) throws SSLException { + this(certChainFile, keyFile, keyPassword, ciphers, cipherFilter, + toNegotiator(apn, true), sessionCacheSize, sessionTimeout); + } - super(ciphers); + /** + * Creates a new instance. + * + * @param certChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@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 apn Application Protocol Negotiator object. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + */ + public JdkSslServerContext( + File certChainFile, File keyFile, String keyPassword, + Iterable ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(null, null, certChainFile, keyFile, keyPassword, null, + ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); + } - if (certChainFile == null) { - throw new NullPointerException("certChainFile"); - } - if (keyFile == null) { - throw new NullPointerException("keyFile"); - } + /** + * Creates a new instance. + * @param trustCertChainFile an X.509 certificate chain file in PEM format. + * This provides the certificate chains used for mutual authentication. + * {@code null} to use the system default + * @param trustManagerFactory the {@link TrustManagerFactory} that provides the {@link TrustManager}s + * that verifies the certificates sent from clients. + * {@code null} to use the default or the results of parsing {@code trustCertChainFile}. + * @param keyCertChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param keyManagerFactory the {@link KeyManagerFactory} that provides the {@link KeyManager}s + * that is used to encrypt data being sent to clients. + * {@code null} to use the default or the results of parsing + * {@code keyCertChainFile} and {@code keyFile}. + * @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 apn Provides a means to configure parameters related to application protocol negotiation. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + */ + public JdkSslServerContext(File trustCertChainFile, TrustManagerFactory trustManagerFactory, + File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(trustCertChainFile, trustManagerFactory, keyCertChainFile, keyFile, keyPassword, keyManagerFactory, + ciphers, cipherFilter, toNegotiator(apn, true), sessionCacheSize, sessionTimeout); + } - if (keyPassword == null) { - keyPassword = ""; - } - - if (nextProtocols != null && nextProtocols.iterator().hasNext()) { - if (!JettyNpnSslEngine.isAvailable()) { - throw new SSLException("NPN/ALPN unsupported: " + nextProtocols); - } - - List list = new ArrayList(); - for (String p: nextProtocols) { - if (p == null) { - break; - } - list.add(p); - } - - this.nextProtocols = Collections.unmodifiableList(list); - } else { - this.nextProtocols = Collections.emptyList(); - } - - String algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm"); - if (algorithm == null) { - algorithm = "SunX509"; + /** + * Creates a new instance. + * @param trustCertChainFile an X.509 certificate chain file in PEM format. + * This provides the certificate chains used for mutual authentication. + * {@code null} to use the system default + * @param trustManagerFactory the {@link TrustManagerFactory} that provides the {@link TrustManager}s + * that verifies the certificates sent from clients. + * {@code null} to use the default or the results of parsing {@code trustCertChainFile} + * @param keyCertChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param keyManagerFactory the {@link KeyManagerFactory} that provides the {@link KeyManager}s + * that is used to encrypt data being sent to clients. + * {@code null} to use the default or the results of parsing + * {@code keyCertChainFile} and {@code keyFile}. + * @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 apn Application Protocol Negotiator object. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + */ + public JdkSslServerContext(File trustCertChainFile, TrustManagerFactory trustManagerFactory, + File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + super(ciphers, cipherFilter, apn); + if (keyFile == null && keyManagerFactory == null) { + throw new NullPointerException("keyFile, keyManagerFactory"); } try { - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(null, null); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - KeyFactory rsaKF = KeyFactory.getInstance("RSA"); - KeyFactory dsaKF = KeyFactory.getInstance("DSA"); - - ByteBuf encodedKeyBuf = PemReader.readPrivateKey(keyFile); - byte[] encodedKey = new byte[encodedKeyBuf.readableBytes()]; - encodedKeyBuf.readBytes(encodedKey).release(); - - char[] keyPasswordChars = keyPassword.toCharArray(); - PKCS8EncodedKeySpec encodedKeySpec = generateKeySpec(keyPasswordChars, encodedKey); - - PrivateKey key; - try { - key = rsaKF.generatePrivate(encodedKeySpec); - } catch (InvalidKeySpecException ignore) { - key = dsaKF.generatePrivate(encodedKeySpec); + if (trustCertChainFile != null) { + trustManagerFactory = buildTrustManagerFactory(trustCertChainFile, trustManagerFactory); } - - List certChain = new ArrayList(); - ByteBuf[] certs = PemReader.readCertificates(certChainFile); - try { - for (ByteBuf buf: certs) { - certChain.add(cf.generateCertificate(new ByteBufInputStream(buf))); - } - } finally { - for (ByteBuf buf: certs) { - buf.release(); - } + if (keyFile != null) { + keyManagerFactory = buildKeyManagerFactory(keyCertChainFile, keyFile, keyPassword, keyManagerFactory); } - ks.setKeyEntry("key", key, keyPasswordChars, certChain.toArray(new Certificate[certChain.size()])); - - // Set up key manager factory to use our key store - KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); - kmf.init(ks, keyPasswordChars); - // Initialize the SSLContext to work with our key managers. ctx = SSLContext.getInstance(PROTOCOL); - ctx.init(kmf.getKeyManagers(), null, null); + ctx.init(keyManagerFactory.getKeyManagers(), + trustManagerFactory == null ? null : trustManagerFactory.getTrustManagers(), + null); SSLSessionContext sessCtx = ctx.getServerSessionContext(); if (sessionCacheSize > 0) { @@ -193,48 +204,8 @@ public final class JdkSslServerContext extends JdkSslContext { return false; } - @Override - public List nextProtocols() { - return nextProtocols; - } - @Override public SSLContext context() { return ctx; } - - /** - * Generates a key specification for an (encrypted) private key. - * - * @param password characters, if {@code null} or empty an unencrypted key is assumed - * @param key bytes of the DER encoded private key - * - * @return a key specification - * - * @throws IOException if parsing {@code key} fails - * @throws NoSuchAlgorithmException if the algorithm used to encrypt {@code key} is unkown - * @throws NoSuchPaddingException if the padding scheme specified in the decryption algorithm is unkown - * @throws InvalidKeySpecException if the decryption key based on {@code password} cannot be generated - * @throws InvalidKeyException if the decryption key based on {@code password} cannot be used to decrypt - * {@code key} - * @throws InvalidAlgorithmParameterException if decryption algorithm parameters are somehow faulty - */ - private static PKCS8EncodedKeySpec generateKeySpec(char[] password, byte[] key) - throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, - InvalidKeyException, InvalidAlgorithmParameterException { - - if (password == null || password.length == 0) { - return new PKCS8EncodedKeySpec(key); - } - - EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(key); - SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName()); - PBEKeySpec pbeKeySpec = new PBEKeySpec(password); - SecretKey pbeKey = keyFactory.generateSecret(pbeKeySpec); - - Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName()); - cipher.init(Cipher.DECRYPT_MODE, pbeKey, encryptedPrivateKeyInfo.getAlgParameters()); - - return encryptedPrivateKeyInfo.getKeySpec(cipher); - } } diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslSession.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslSession.java new file mode 100644 index 0000000000..ba54b9b83f --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslSession.java @@ -0,0 +1,169 @@ +/* + * 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 javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionContext; +import javax.security.cert.X509Certificate; +import java.security.Principal; +import java.security.cert.Certificate; + +final class JdkSslSession implements SSLSession { + private final SSLEngine engine; + private volatile String applicationProtocol; + + JdkSslSession(SSLEngine engine) { + this.engine = engine; + } + + void setApplicationProtocol(String applicationProtocol) { + if (applicationProtocol != null) { + applicationProtocol = applicationProtocol.replace(':', '_'); + } + this.applicationProtocol = applicationProtocol; + } + + @Override + public String getProtocol() { + final String protocol = unwrap().getProtocol(); + final String applicationProtocol = this.applicationProtocol; + + if (applicationProtocol == null) { + if (protocol != null) { + return protocol.replace(':', '_'); + } else { + return null; + } + } + + final StringBuilder buf = new StringBuilder(32); + if (protocol != null) { + buf.append(protocol.replace(':', '_')); + buf.append(':'); + } else { + buf.append("null:"); + } + buf.append(applicationProtocol); + return buf.toString(); + } + + private SSLSession unwrap() { + return engine.getSession(); + } + + @Override + public byte[] getId() { + return unwrap().getId(); + } + + @Override + public SSLSessionContext getSessionContext() { + return unwrap().getSessionContext(); + } + + @Override + public long getCreationTime() { + return unwrap().getCreationTime(); + } + + @Override + public long getLastAccessedTime() { + return unwrap().getLastAccessedTime(); + } + + @Override + public void invalidate() { + unwrap().invalidate(); + } + + @Override + public boolean isValid() { + return unwrap().isValid(); + } + + @Override + public void putValue(String s, Object o) { + unwrap().putValue(s, o); + } + + @Override + public Object getValue(String s) { + return unwrap().getValue(s); + } + + @Override + public void removeValue(String s) { + unwrap().removeValue(s); + } + + @Override + public String[] getValueNames() { + return unwrap().getValueNames(); + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { + return unwrap().getPeerCertificates(); + } + + @Override + public Certificate[] getLocalCertificates() { + return unwrap().getLocalCertificates(); + } + + @Override + public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException { + return unwrap().getPeerCertificateChain(); + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + return unwrap().getPeerPrincipal(); + } + + @Override + public Principal getLocalPrincipal() { + return unwrap().getLocalPrincipal(); + } + + @Override + public String getCipherSuite() { + return unwrap().getCipherSuite(); + } + + @Override + public String getPeerHost() { + return unwrap().getPeerHost(); + } + + @Override + public int getPeerPort() { + return unwrap().getPeerPort(); + } + + @Override + public int getPacketBufferSize() { + return unwrap().getPacketBufferSize(); + } + + @Override + public int getApplicationBufferSize() { + return unwrap().getApplicationBufferSize(); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslApplicationProtocolNegotiator.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslApplicationProtocolNegotiator.java new file mode 100644 index 0000000000..7cd2e1126e --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslApplicationProtocolNegotiator.java @@ -0,0 +1,28 @@ +/* + * 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; + +/** + * OpenSSL version of {@link ApplicationProtocolNegotiator}. + */ +public interface OpenSslApplicationProtocolNegotiator extends ApplicationProtocolNegotiator { + // The current need for this interface is primarily for consistency with the JDK provider + // How the OpenSsl provider will provide extensibility to control the application selection + // and notification algorithms is not yet known (JNI, pure java, tcnative hooks, etc...). + // OpenSSL itself is currently not in compliance with the specification for the 2 supported + // protocols (ALPN, NPN) with respect to allowing the handshakes to fail during the application + // protocol negotiation process. Issue https://github.com/openssl/openssl/issues/188 has been created for this. +} diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslDefaultApplicationProtocolNegotiator.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslDefaultApplicationProtocolNegotiator.java new file mode 100644 index 0000000000..65191d91c0 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslDefaultApplicationProtocolNegotiator.java @@ -0,0 +1,35 @@ +/* + * 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.Collections; +import java.util.List; + +/** + * Provides no {@link ApplicationProtocolNegotiator} functionality for OpenSSL. + */ +final class OpenSslDefaultApplicationProtocolNegotiator implements OpenSslApplicationProtocolNegotiator { + static final OpenSslDefaultApplicationProtocolNegotiator INSTANCE = + new OpenSslDefaultApplicationProtocolNegotiator(); + + private OpenSslDefaultApplicationProtocolNegotiator() { + } + + @Override + public List protocols() { + return Collections.emptyList(); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslNpnApplicationProtocolNegotiator.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslNpnApplicationProtocolNegotiator.java new file mode 100644 index 0000000000..e4b4e8f043 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslNpnApplicationProtocolNegotiator.java @@ -0,0 +1,41 @@ +/* + * 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 io.netty.handler.ssl.ApplicationProtocolUtil.toList; +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import java.util.List; + +/** + * OpenSSL {@link ApplicationProtocolNegotiator} for NPN. + */ +public final class OpenSslNpnApplicationProtocolNegotiator implements OpenSslApplicationProtocolNegotiator { + private final List protocols; + + public OpenSslNpnApplicationProtocolNegotiator(Iterable protocols) { + this.protocols = checkNotNull(toList(protocols), "protocols"); + } + + public OpenSslNpnApplicationProtocolNegotiator(String... protocols) { + this.protocols = checkNotNull(toList(protocols), "protocols"); + } + + @Override + public List protocols() { + return protocols; + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java index 7012f5c267..5295a81883 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java @@ -15,20 +15,23 @@ */ package io.netty.handler.ssl; +import static io.netty.util.internal.ObjectUtil.checkNotNull; import io.netty.buffer.ByteBufAllocator; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; -import org.apache.tomcat.jni.Pool; -import org.apache.tomcat.jni.SSL; -import org.apache.tomcat.jni.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLException; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import org.apache.tomcat.jni.Pool; +import org.apache.tomcat.jni.SSL; +import org.apache.tomcat.jni.SSLContext; + /** * A server-side {@link SslContext} which uses OpenSSL's SSL/TLS implementation. */ @@ -65,7 +68,7 @@ public final class OpenSslServerContext extends SslContext { private final List unmodifiableCiphers = Collections.unmodifiableList(ciphers); private final long sessionCacheSize; private final long sessionTimeout; - private final List nextProtocols; + private final OpenSslApplicationProtocolNegotiator apn; /** The OpenSSL SSL_CTX object */ private final long ctx; @@ -90,7 +93,7 @@ public final class OpenSslServerContext extends SslContext { * {@code null} if it's not password-protected. */ public OpenSslServerContext(File certChainFile, File keyFile, String keyPassword) throws SSLException { - this(certChainFile, keyFile, keyPassword, null, null, 0, 0); + this(certChainFile, keyFile, keyPassword, null, OpenSslDefaultApplicationProtocolNegotiator.INSTANCE, 0, 0); } /** @@ -102,8 +105,7 @@ public final class OpenSslServerContext extends 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 nextProtocols the application layer protocols to accept, in the order of preference. - * {@code null} to disable TLS NPN/ALPN extension. + * @param apn Provides a means to configure parameters related to application protocol negotiation. * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. @@ -111,20 +113,39 @@ public final class OpenSslServerContext extends SslContext { */ public OpenSslServerContext( File certChainFile, File keyFile, String keyPassword, - Iterable ciphers, Iterable nextProtocols, + Iterable ciphers, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(certChainFile, keyFile, keyPassword, ciphers, toNegotiator(apn, false), sessionCacheSize, sessionTimeout); + } + + /** + * Creates a new instance. + * + * @param certChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@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 apn Application protocol negotiator. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + */ + public OpenSslServerContext( + File certChainFile, File keyFile, String keyPassword, + Iterable ciphers, OpenSslApplicationProtocolNegotiator apn, long sessionCacheSize, long sessionTimeout) throws SSLException { OpenSsl.ensureAvailability(); - if (certChainFile == null) { - throw new NullPointerException("certChainFile"); - } + checkNotNull(certChainFile, "certChainFile"); if (!certChainFile.isFile()) { throw new IllegalArgumentException("certChainFile is not a file: " + certChainFile); } - if (keyFile == null) { - throw new NullPointerException("keyPath"); - } + checkNotNull(keyFile, "keyFile"); + this.apn = checkNotNull(apn, "apn"); if (!keyFile.isFile()) { throw new IllegalArgumentException("keyPath is not a file: " + keyFile); } @@ -135,9 +156,6 @@ public final class OpenSslServerContext extends SslContext { if (keyPassword == null) { keyPassword = ""; } - if (nextProtocols == null) { - nextProtocols = Collections.emptyList(); - } for (String c: ciphers) { if (c == null) { @@ -146,15 +164,6 @@ public final class OpenSslServerContext extends SslContext { this.ciphers.add(c); } - List nextProtoList = new ArrayList(); - for (String p: nextProtocols) { - if (p == null) { - break; - } - nextProtoList.add(p); - } - this.nextProtocols = Collections.unmodifiableList(nextProtoList); - // Allocate a new APR pool. aprPool = Pool.create(0); @@ -219,11 +228,12 @@ public final class OpenSslServerContext extends SslContext { } /* Set next protocols for next protocol negotiation extension, if specified */ - if (!nextProtoList.isEmpty()) { + List protocols = apn.protocols(); + if (!protocols.isEmpty()) { // Convert the protocol list into a comma-separated string. StringBuilder nextProtocolBuf = new StringBuilder(); - for (String p: nextProtoList) { - nextProtocolBuf.append(p); + for (int i = 0; i < protocols.size(); ++i) { + nextProtocolBuf.append(protocols.get(i)); nextProtocolBuf.append(','); } nextProtocolBuf.setLength(nextProtocolBuf.length() - 1); @@ -283,9 +293,8 @@ public final class OpenSslServerContext extends SslContext { return sessionTimeout; } - @Override - public List nextProtocols() { - return nextProtocols; + public ApplicationProtocolNegotiator applicationProtocolNegotiator() { + return apn; } /** @@ -307,10 +316,11 @@ public final class OpenSslServerContext extends SslContext { */ @Override public SSLEngine newEngine(ByteBufAllocator alloc) { - if (nextProtocols.isEmpty()) { + List protocols = apn.protocols(); + if (protocols.isEmpty()) { return new OpenSslEngine(ctx, alloc, null); } else { - return new OpenSslEngine(ctx, alloc, nextProtocols.get(nextProtocols.size() - 1)); + return new OpenSslEngine(ctx, alloc, protocols.get(protocols.size() - 1)); } } @@ -347,4 +357,38 @@ public final class OpenSslServerContext extends SslContext { Pool.destroy(aprPool); } } + + /** + * Translate a {@link ApplicationProtocolConfiguration} object to a + * {@link OpenSslApplicationProtocolNegotiator} object. + * @param config The configuration which defines the translation + * @param isServer {@code true} if a server {@code false} otherwise. + * @return The results of the translation + */ + private static OpenSslApplicationProtocolNegotiator toNegotiator(ApplicationProtocolConfig config, + boolean isServer) { + if (config == null) { + return OpenSslDefaultApplicationProtocolNegotiator.INSTANCE; + } + + switch(config.protocol()) { + case NONE: + return OpenSslDefaultApplicationProtocolNegotiator.INSTANCE; + case NPN: + if (isServer) { + switch(config.selectedListenerFailureBehavior()) { + case CHOOSE_MY_LAST_PROTOCOL: + return new OpenSslNpnApplicationProtocolNegotiator(config.supportedProtocols()); + default: + throw new UnsupportedOperationException(new StringBuilder("OpenSSL provider does not support ") + .append(config.selectedListenerFailureBehavior()).append(" behavior").toString()); + } + } else { + throw new UnsupportedOperationException("OpenSSL provider does not support client mode"); + } + default: + throw new UnsupportedOperationException(new StringBuilder("OpenSSL provider does not support ") + .append(config.protocol()).append(" protocol").toString()); + } + } } 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 fde09e8d8d..da8c4f57f0 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContext.java @@ -20,13 +20,16 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; +import java.io.File; +import java.util.List; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; -import java.io.File; -import java.util.List; /** * A secure socket protocol implementation which acts as a factory for {@link SSLEngine} and {@link SslHandler}. @@ -51,7 +54,6 @@ import java.util.List; * */ public abstract class SslContext { - /** * Returns the default server-side implementation provider currently in use. * @@ -82,7 +84,7 @@ public abstract class SslContext { * @return a new server-side {@link SslContext} */ public static SslContext newServerContext(File certChainFile, File keyFile) throws SSLException { - return newServerContext(null, certChainFile, keyFile, null, null, null, 0, 0); + return newServerContext(certChainFile, keyFile, null); } /** @@ -96,7 +98,7 @@ public abstract class SslContext { */ public static SslContext newServerContext( File certChainFile, File keyFile, String keyPassword) throws SSLException { - return newServerContext(null, certChainFile, keyFile, keyPassword, null, null, 0, 0); + return newServerContext(null, certChainFile, keyFile, keyPassword); } /** @@ -108,8 +110,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 nextProtocols the application layer protocols to accept, in the order of preference. - * {@code null} to disable TLS NPN/ALPN extension. + * @param cipherFilter a filter to apply over the supplied list of ciphers + * @param apn Provides a means to configure parameters related to application protocol negotiation. * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. @@ -118,11 +120,11 @@ public abstract class SslContext { */ public static SslContext newServerContext( File certChainFile, File keyFile, String keyPassword, - Iterable ciphers, Iterable nextProtocols, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout) throws SSLException { return newServerContext( null, certChainFile, keyFile, keyPassword, - ciphers, nextProtocols, sessionCacheSize, sessionTimeout); + ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); } /** @@ -136,7 +138,7 @@ public abstract class SslContext { */ public static SslContext newServerContext( SslProvider provider, File certChainFile, File keyFile) throws SSLException { - return newServerContext(provider, certChainFile, keyFile, null, null, null, 0, 0); + return newServerContext(provider, certChainFile, keyFile, null); } /** @@ -152,7 +154,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, 0, 0); + return newServerContext(provider, certChainFile, keyFile, keyPassword, null, IdentityCipherSuiteFilter.INSTANCE, + null, 0, 0); } /** @@ -166,18 +169,58 @@ 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 nextProtocols the application layer protocols to accept, in the order of preference. - * {@code null} to disable TLS NPN/ALPN extension. + * @param cipherFilter a filter to apply over the supplied list of ciphers + * Only required if {@code provider} is {@link SslProvider#JDK} + * @param apn Provides a means to configure parameters related to application protocol negotiation. * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. * {@code 0} to use the default value. * @return a new server-side {@link SslContext} */ - public static SslContext newServerContext( - SslProvider provider, + public static SslContext newServerContext(SslProvider provider, File certChainFile, File keyFile, String keyPassword, - Iterable ciphers, Iterable nextProtocols, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + return newServerContext(provider, null, null, certChainFile, keyFile, keyPassword, null, + ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); + } + + /** + * Creates a new server-side {@link SslContext}. + * @param provider the {@link SslContext} implementation to use. + * {@code null} to use the current default one. + * @param trustCertChainFile an X.509 certificate chain file in PEM format. + * This provides the certificate chains used for mutual authentication. + * {@code null} to use the system default + * @param trustManagerFactory the {@link TrustManagerFactory} that provides the {@link TrustManager}s + * that verifies the certificates sent from clients. + * {@code null} to use the default or the results of parsing {@code trustCertChainFile}. + * This parameter is ignored if {@code provider} is not {@link SslProvider#JDK}. + * @param keyCertChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param keyManagerFactory the {@link KeyManagerFactory} that provides the {@link KeyManager}s + * that is used to encrypt data being sent to clients. + * {@code null} to use the default or the results of parsing + * {@code keyCertChainFile} and {@code keyFile}. + * This parameter is ignored if {@code provider} is not {@link SslProvider#JDK}. + * @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 apn Provides a means to configure parameters related to application protocol negotiation. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + * @return a new server-side {@link SslContext} + */ + public static SslContext newServerContext(SslProvider provider, + File trustCertChainFile, TrustManagerFactory trustManagerFactory, + File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout) throws SSLException { if (provider == null) { @@ -187,12 +230,15 @@ public abstract class SslContext { switch (provider) { case JDK: return new JdkSslServerContext( - certChainFile, keyFile, keyPassword, - ciphers, nextProtocols, sessionCacheSize, sessionTimeout); + trustCertChainFile, trustManagerFactory, keyCertChainFile, keyFile, keyPassword, + keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); case OPENSSL: + if (trustCertChainFile != null) { + throw new UnsupportedOperationException("OpenSSL provider does not support mutual authentication"); + } return new OpenSslServerContext( - certChainFile, keyFile, keyPassword, - ciphers, nextProtocols, sessionCacheSize, sessionTimeout); + keyCertChainFile, keyFile, keyPassword, + ciphers, apn, sessionCacheSize, sessionTimeout); default: throw new Error(provider.toString()); } @@ -204,7 +250,7 @@ public abstract class SslContext { * @return a new client-side {@link SslContext} */ public static SslContext newClientContext() throws SSLException { - return newClientContext(null, null, null, null, null, 0, 0); + return newClientContext(null, null, null); } /** @@ -215,7 +261,7 @@ public abstract class SslContext { * @return a new client-side {@link SslContext} */ public static SslContext newClientContext(File certChainFile) throws SSLException { - return newClientContext(null, certChainFile, null, null, null, 0, 0); + return newClientContext(null, certChainFile); } /** @@ -228,7 +274,7 @@ public abstract class SslContext { * @return a new client-side {@link SslContext} */ public static SslContext newClientContext(TrustManagerFactory trustManagerFactory) throws SSLException { - return newClientContext(null, null, trustManagerFactory, null, null, 0, 0); + return newClientContext(null, null, trustManagerFactory); } /** @@ -244,7 +290,7 @@ public abstract class SslContext { */ public static SslContext newClientContext( File certChainFile, TrustManagerFactory trustManagerFactory) throws SSLException { - return newClientContext(null, certChainFile, trustManagerFactory, null, null, 0, 0); + return newClientContext(null, certChainFile, trustManagerFactory); } /** @@ -257,8 +303,8 @@ 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 nextProtocols the application layer protocols to accept, in the order of preference. - * {@code null} to disable TLS NPN/ALPN extension. + * @param cipherFilter a filter to apply over the supplied list of ciphers + * @param apn Provides a means to configure parameters related to application protocol negotiation. * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. @@ -268,11 +314,11 @@ public abstract class SslContext { */ public static SslContext newClientContext( File certChainFile, TrustManagerFactory trustManagerFactory, - Iterable ciphers, Iterable nextProtocols, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout) throws SSLException { return newClientContext( null, certChainFile, trustManagerFactory, - ciphers, nextProtocols, sessionCacheSize, sessionTimeout); + ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); } /** @@ -284,7 +330,7 @@ public abstract class SslContext { * @return a new client-side {@link SslContext} */ public static SslContext newClientContext(SslProvider provider) throws SSLException { - return newClientContext(provider, null, null, null, null, 0, 0); + return newClientContext(provider, null, null); } /** @@ -298,7 +344,7 @@ public abstract class SslContext { * @return a new client-side {@link SslContext} */ public static SslContext newClientContext(SslProvider provider, File certChainFile) throws SSLException { - return newClientContext(provider, certChainFile, null, null, null, 0, 0); + return newClientContext(provider, certChainFile, null); } /** @@ -314,7 +360,7 @@ public abstract class SslContext { */ public static SslContext newClientContext( SslProvider provider, TrustManagerFactory trustManagerFactory) throws SSLException { - return newClientContext(provider, null, trustManagerFactory, null, null, 0, 0); + return newClientContext(provider, trustManagerFactory); } /** @@ -332,7 +378,8 @@ public abstract class SslContext { */ public static SslContext newClientContext( SslProvider provider, File certChainFile, TrustManagerFactory trustManagerFactory) throws SSLException { - return newClientContext(provider, certChainFile, trustManagerFactory, null, null, 0, 0); + return newClientContext(provider, certChainFile, trustManagerFactory, null, IdentityCipherSuiteFilter.INSTANCE, + null, 0, 0); } /** @@ -347,8 +394,8 @@ 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 nextProtocols the application layer protocols to accept, in the order of preference. - * {@code null} to disable TLS NPN/ALPN extension. + * @param cipherFilter a filter to apply over the supplied list of ciphers + * @param apn Provides a means to configure parameters related to application protocol negotiation. * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. @@ -356,19 +403,61 @@ public abstract class SslContext { * * @return a new client-side {@link SslContext} */ - public static SslContext newClientContext( - SslProvider provider, + public static SslContext newClientContext(SslProvider provider, File certChainFile, TrustManagerFactory trustManagerFactory, - Iterable ciphers, Iterable nextProtocols, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + return newClientContext(provider, certChainFile, trustManagerFactory, null, null, null, null, + ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); + } + + /** + * Creates a new client-side {@link SslContext}. + * @param provider the {@link SslContext} implementation to use. + * {@code null} to use the current default one. + * @param trustCertChainFile an X.509 certificate chain file in PEM format. + * {@code null} to use the system default + * @param trustManagerFactory the {@link TrustManagerFactory} that provides the {@link TrustManager}s + * that verifies the certificates sent from servers. + * {@code null} to use the default or the results of parsing {@code trustCertChainFile}. + * This parameter is ignored if {@code provider} is not {@link SslProvider#JDK}. + * @param keyCertChainFile an X.509 certificate chain file in PEM format. + * This provides the public key for mutual authentication. + * {@code null} to use the system default + * @param keyFile a PKCS#8 private key file in PEM format. + * This provides the private key for mutual authentication. + * {@code null} for no mutual authentication. + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * Ignored if {@code keyFile} is {@code null}. + * @param keyManagerFactory the {@link KeyManagerFactory} that provides the {@link KeyManager}s + * that is used to encrypt data being sent to servers. + * {@code null} to use the default or the results of parsing + * {@code keyCertChainFile} and {@code keyFile}. + * This parameter is ignored if {@code provider} is not {@link SslProvider#JDK}. + * @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 apn Provides a means to configure parameters related to application protocol negotiation. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + * + * @return a new client-side {@link SslContext} + */ + public static SslContext newClientContext(SslProvider provider, + File trustCertChainFile, TrustManagerFactory trustManagerFactory, + File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout) throws SSLException { if (provider != null && provider != SslProvider.JDK) { throw new SSLException("client context unsupported for: " + provider); } - return new JdkSslClientContext( - certChainFile, trustManagerFactory, - ciphers, nextProtocols, sessionCacheSize, sessionTimeout); + return new JdkSslClientContext(trustCertChainFile, trustManagerFactory, keyCertChainFile, keyFile, keyPassword, + keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); } SslContext() { } @@ -401,12 +490,9 @@ public abstract class SslContext { public abstract long sessionTimeout(); /** - * Returns the list of application layer protocols for the TLS NPN/ALPN extension, in the order of preference. - * - * @return the list of application layer protocols. - * {@code null} if NPN/ALPN extension has been disabled. + * Returns the object responsible for negotiating application layer protocols for the TLS NPN/ALPN extensions. */ - public abstract List nextProtocols(); + public abstract ApplicationProtocolNegotiator applicationProtocolNegotiator(); /** * Creates a new {@link SSLEngine}. 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/JdkSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java new file mode 100644 index 0000000000..847d0b0920 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java @@ -0,0 +1,568 @@ +/* + * 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 static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeNoException; +import static org.mockito.Mockito.verify; +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.ssl.JdkApplicationProtocolNegotiator.ProtocolSelector; +import io.netty.handler.ssl.JdkApplicationProtocolNegotiator.ProtocolSelectorFactory; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.util.NetUtil; +import io.netty.util.concurrent.Future; + +import java.io.File; +import java.net.InetSocketAddress; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class JdkSslEngineTest { + private static final String APPLICATION_LEVEL_PROTOCOL = "my-protocol"; + private static final String APPLICATION_LEVEL_PROTOCOL_NOT_COMPATIBLE = "my-protocol-FOO"; + + @Mock + private MessageReciever serverReceiver; + @Mock + private MessageReciever clientReceiver; + + private Throwable serverException; + private Throwable clientException; + private SslContext serverSslCtx; + private SslContext clientSslCtx; + private ServerBootstrap sb; + private Bootstrap cb; + private Channel serverChannel; + private Channel serverConnectedChannel; + private Channel clientChannel; + private CountDownLatch serverLatch; + private CountDownLatch clientLatch; + + private interface MessageReciever { + void messageReceived(ByteBuf msg); + } + + private final class MessageDelegatorChannelHandler extends SimpleChannelInboundHandler { + private final MessageReciever receiver; + private final CountDownLatch latch; + + public MessageDelegatorChannelHandler(MessageReciever receiver, CountDownLatch latch) { + super(false); + this.receiver = receiver; + this.latch = latch; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { + receiver.messageReceived(msg); + latch.countDown(); + } + } + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + serverLatch = new CountDownLatch(1); + clientLatch = new CountDownLatch(1); + } + + @After + public void tearDown() throws InterruptedException { + if (serverChannel != null) { + serverChannel.close().sync(); + Future serverGroup = sb.group().shutdownGracefully(0, 0, TimeUnit.MILLISECONDS); + Future serverChildGroup = sb.childGroup().shutdownGracefully(0, 0, TimeUnit.MILLISECONDS); + Future clientGroup = cb.group().shutdownGracefully(0, 0, TimeUnit.MILLISECONDS); + serverGroup.sync(); + serverChildGroup.sync(); + clientGroup.sync(); + } + clientChannel = null; + serverChannel = null; + serverConnectedChannel = null; + } + + @Test + public void testNpn() throws Exception { + try { + // Typical code will not have to check this, but will get a initialization error on class load. + // Check in this test just in case we have multiple tests that just the class and we already ignored the + // initialization error. + if (!JdkNpnSslEngine.isAvailable()) { + throw new RuntimeException("NPN not on classpath"); + } + JdkApplicationProtocolNegotiator apn = new JdkNpnApplicationProtocolNegotiator(true, true, + APPLICATION_LEVEL_PROTOCOL); + mySetup(apn); + runTest(); + } catch (RuntimeException e) { + // 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); + } + } + + @Test + public void testNpnNoCompatibleProtocolsNoHandshakeFailure() throws Exception { + try { + // Typical code will not have to check this, but will get a initialization error on class load. + // Check in this test just in case we have multiple tests that just the class and we already ignored the + // initialization error. + if (!JdkNpnSslEngine.isAvailable()) { + throw new RuntimeException("NPN not on classpath"); + } + JdkApplicationProtocolNegotiator clientApn = new JdkNpnApplicationProtocolNegotiator(false, false, + APPLICATION_LEVEL_PROTOCOL); + JdkApplicationProtocolNegotiator serverApn = new JdkNpnApplicationProtocolNegotiator(false, false, + APPLICATION_LEVEL_PROTOCOL_NOT_COMPATIBLE); + mySetup(serverApn, clientApn); + runTest(null); + } catch (Exception e) { + // ALPN availability is dependent on the java version. If ALPN is not available because of + // java version incompatibility don't fail the test, but instead just skip the test + assumeNoException(e); + } + } + + @Test + public void testNpnNoCompatibleProtocolsClientHandshakeFailure() throws Exception { + try { + // Typical code will not have to check this, but will get a initialization error on class load. + // Check in this test just in case we have multiple tests that just the class and we already ignored the + // initialization error. + if (!JdkNpnSslEngine.isAvailable()) { + throw new RuntimeException("NPN not on classpath"); + } + JdkApplicationProtocolNegotiator clientApn = new JdkNpnApplicationProtocolNegotiator(true, true, + APPLICATION_LEVEL_PROTOCOL); + JdkApplicationProtocolNegotiator serverApn = new JdkNpnApplicationProtocolNegotiator(false, false, + APPLICATION_LEVEL_PROTOCOL_NOT_COMPATIBLE); + mySetup(serverApn, clientApn); + assertTrue(clientLatch.await(2, TimeUnit.SECONDS)); + assertTrue(clientException instanceof SSLHandshakeException); + } catch (RuntimeException e) { + // 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); + } + } + + @Test + public void testNpnNoCompatibleProtocolsServerHandshakeFailure() throws Exception { + try { + // Typical code will not have to check this, but will get a initialization error on class load. + // Check in this test just in case we have multiple tests that just the class and we already ignored the + // initialization error. + if (!JdkNpnSslEngine.isAvailable()) { + throw new RuntimeException("NPN not on classpath"); + } + JdkApplicationProtocolNegotiator clientApn = new JdkNpnApplicationProtocolNegotiator(false, false, + APPLICATION_LEVEL_PROTOCOL); + JdkApplicationProtocolNegotiator serverApn = new JdkNpnApplicationProtocolNegotiator(true, true, + APPLICATION_LEVEL_PROTOCOL_NOT_COMPATIBLE); + mySetup(serverApn, clientApn); + assertTrue(serverLatch.await(2, TimeUnit.SECONDS)); + assertTrue(serverException instanceof SSLHandshakeException); + } catch (RuntimeException e) { + // 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); + } + } + + @Test + public void testAlpn() throws Exception { + try { + // Typical code will not have to check this, but will get a initialization error on class load. + // Check in this test just in case we have multiple tests that just the class and we already ignored the + // initialization error. + if (!JdkAlpnSslEngine.isAvailable()) { + throw new RuntimeException("ALPN not on classpath"); + } + JdkApplicationProtocolNegotiator apn = new JdkAlpnApplicationProtocolNegotiator(true, true, + APPLICATION_LEVEL_PROTOCOL); + mySetup(apn); + runTest(); + } catch (Exception e) { + // ALPN availability is dependent on the java version. If ALPN is not available because of + // java version incompatibility don't fail the test, but instead just skip the test + assumeNoException(e); + } + } + + @Test + public void testAlpnNoCompatibleProtocolsNoHandshakeFailure() throws Exception { + try { + // Typical code will not have to check this, but will get a initialization error on class load. + // Check in this test just in case we have multiple tests that just the class and we already ignored the + // initialization error. + if (!JdkAlpnSslEngine.isAvailable()) { + throw new RuntimeException("ALPN not on classpath"); + } + JdkApplicationProtocolNegotiator clientApn = new JdkAlpnApplicationProtocolNegotiator(false, false, + APPLICATION_LEVEL_PROTOCOL); + JdkApplicationProtocolNegotiator serverApn = new JdkAlpnApplicationProtocolNegotiator(false, false, + APPLICATION_LEVEL_PROTOCOL_NOT_COMPATIBLE); + mySetup(serverApn, clientApn); + runTest(null); + } catch (Exception e) { + // ALPN availability is dependent on the java version. If ALPN is not available because of + // java version incompatibility don't fail the test, but instead just skip the test + assumeNoException(e); + } + } + + @Test + public void testAlpnNoCompatibleProtocolsServerHandshakeFailure() throws Exception { + try { + // Typical code will not have to check this, but will get a initialization error on class load. + // Check in this test just in case we have multiple tests that just the class and we already ignored the + // initialization error. + if (!JdkAlpnSslEngine.isAvailable()) { + throw new RuntimeException("ALPN not on classpath"); + } + JdkApplicationProtocolNegotiator clientApn = new JdkAlpnApplicationProtocolNegotiator(false, false, + APPLICATION_LEVEL_PROTOCOL); + JdkApplicationProtocolNegotiator serverApn = new JdkAlpnApplicationProtocolNegotiator(true, true, + APPLICATION_LEVEL_PROTOCOL_NOT_COMPATIBLE); + mySetup(serverApn, clientApn); + assertTrue(serverLatch.await(2, TimeUnit.SECONDS)); + assertTrue(serverException instanceof SSLHandshakeException); + } catch (Exception e) { + // ALPN availability is dependent on the java version. If ALPN is not available because of + // java version incompatibility don't fail the test, but instead just skip the test + assumeNoException(e); + } + } + + @Test + public void testAlpnNoCompatibleProtocolsClientHandshakeFailure() throws Exception { + try { + // Typical code will not have to check this, but will get a initialization error on class load. + // Check in this test just in case we have multiple tests that just the class and we already ignored the + // initialization error. + if (!JdkAlpnSslEngine.isAvailable()) { + throw new RuntimeException("ALPN not on classpath"); + } + JdkApplicationProtocolNegotiator clientApn = new JdkAlpnApplicationProtocolNegotiator(true, true, + APPLICATION_LEVEL_PROTOCOL); + JdkApplicationProtocolNegotiator serverApn = new JdkAlpnApplicationProtocolNegotiator( + new ProtocolSelectorFactory() { + @Override + public ProtocolSelector newSelector(SSLEngine engine, Set supportedProtocols) { + return new ProtocolSelector() { + @Override + public void unsupported() { + } + + @Override + public String select(List protocols) { + return APPLICATION_LEVEL_PROTOCOL_NOT_COMPATIBLE; + } + }; + } + }, JdkBaseApplicationProtocolNegotiator.FAIL_SELECTION_LISTENER_FACTORY, + APPLICATION_LEVEL_PROTOCOL_NOT_COMPATIBLE); + mySetup(serverApn, clientApn); + assertTrue(clientLatch.await(2, TimeUnit.SECONDS)); + assertTrue(clientException instanceof SSLHandshakeException); + } catch (Exception e) { + // ALPN availability is dependent on the java version. If ALPN is not available because of + // java version incompatibility don't fail the test, but instead just skip the test + assumeNoException(e); + } + } + + @Test + public void testMutualAuthSameCerts() throws Exception { + mySetupMutualAuth(new File(getClass().getResource("test_unencrypted.pem").getFile()), + new File(getClass().getResource("test.crt").getFile()), + null); + runTest(null); + } + + @Test + public void testMutualAuthDiffCerts() throws Exception { + File serverKeyFile = new File(getClass().getResource("test_encrypted.pem").getFile()); + File serverCrtFile = new File(getClass().getResource("test.crt").getFile()); + String serverKeyPassword = "12345"; + File clientKeyFile = new File(getClass().getResource("test2_encrypted.pem").getFile()); + File clientCrtFile = new File(getClass().getResource("test2.crt").getFile()); + String clientKeyPassword = "12345"; + mySetupMutualAuth(clientCrtFile, serverKeyFile, serverCrtFile, serverKeyPassword, + serverCrtFile, clientKeyFile, clientCrtFile, clientKeyPassword); + runTest(null); + } + + @Test + public void testMutualAuthDiffCertsServerFailure() throws Exception { + File serverKeyFile = new File(getClass().getResource("test_encrypted.pem").getFile()); + File serverCrtFile = new File(getClass().getResource("test.crt").getFile()); + String serverKeyPassword = "12345"; + File clientKeyFile = new File(getClass().getResource("test2_encrypted.pem").getFile()); + File clientCrtFile = new File(getClass().getResource("test2.crt").getFile()); + String clientKeyPassword = "12345"; + // Client trusts server but server only trusts itself + mySetupMutualAuth(serverCrtFile, serverKeyFile, serverCrtFile, serverKeyPassword, + serverCrtFile, clientKeyFile, clientCrtFile, clientKeyPassword); + assertTrue(serverLatch.await(2, TimeUnit.SECONDS)); + assertTrue(serverException instanceof SSLHandshakeException); + } + + @Test + public void testMutualAuthDiffCertsClientFailure() throws Exception { + File serverKeyFile = new File(getClass().getResource("test_unencrypted.pem").getFile()); + File serverCrtFile = new File(getClass().getResource("test.crt").getFile()); + String serverKeyPassword = null; + File clientKeyFile = new File(getClass().getResource("test2_unencrypted.pem").getFile()); + File clientCrtFile = new File(getClass().getResource("test2.crt").getFile()); + String clientKeyPassword = null; + // Server trusts client but client only trusts itself + mySetupMutualAuth(clientCrtFile, serverKeyFile, serverCrtFile, serverKeyPassword, + clientCrtFile, clientKeyFile, clientCrtFile, clientKeyPassword); + assertTrue(clientLatch.await(2, TimeUnit.SECONDS)); + assertTrue(clientException instanceof SSLHandshakeException); + } + + private void mySetup(JdkApplicationProtocolNegotiator apn) throws InterruptedException, SSLException, + CertificateException { + mySetup(apn, apn); + } + + private void mySetup(JdkApplicationProtocolNegotiator serverApn, JdkApplicationProtocolNegotiator clientApn) + throws InterruptedException, SSLException, CertificateException { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + serverSslCtx = new JdkSslServerContext(ssc.certificate(), ssc.privateKey(), null, null, + IdentityCipherSuiteFilter.INSTANCE, serverApn, 0, 0); + clientSslCtx = new JdkSslClientContext(null, InsecureTrustManagerFactory.INSTANCE, null, + IdentityCipherSuiteFilter.INSTANCE, clientApn, 0, 0); + + serverConnectedChannel = null; + sb = new ServerBootstrap(); + cb = new Bootstrap(); + + sb.group(new NioEventLoopGroup(), new NioEventLoopGroup()); + sb.channel(NioServerSocketChannel.class); + sb.childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(serverSslCtx.newHandler(ch.alloc())); + p.addLast(new MessageDelegatorChannelHandler(serverReceiver, serverLatch)); + p.addLast(new ChannelHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (cause.getCause() instanceof SSLHandshakeException) { + serverException = cause.getCause(); + serverLatch.countDown(); + } else { + ctx.fireExceptionCaught(cause); + } + } + }); + serverConnectedChannel = ch; + } + }); + + cb.group(new NioEventLoopGroup()); + cb.channel(NioSocketChannel.class); + cb.handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(clientSslCtx.newHandler(ch.alloc())); + p.addLast(new MessageDelegatorChannelHandler(clientReceiver, clientLatch)); + p.addLast(new ChannelHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (cause.getCause() instanceof SSLHandshakeException) { + clientException = cause.getCause(); + clientLatch.countDown(); + } else { + ctx.fireExceptionCaught(cause); + } + } + }); + } + }); + + serverChannel = sb.bind(new InetSocketAddress(0)).sync().channel(); + int port = ((InetSocketAddress) serverChannel.localAddress()).getPort(); + + ChannelFuture ccf = cb.connect(new InetSocketAddress(NetUtil.LOCALHOST, port)); + assertTrue(ccf.awaitUninterruptibly().isSuccess()); + clientChannel = ccf.channel(); + } + + private void mySetupMutualAuth(File keyFile, File crtFile, String keyPassword) + throws SSLException, CertificateException, InterruptedException { + mySetupMutualAuth(crtFile, keyFile, crtFile, keyPassword, crtFile, keyFile, crtFile, keyPassword); + } + + private void mySetupMutualAuth( + File servertTrustCrtFile, File serverKeyFile, File serverCrtFile, String serverKeyPassword, + File clientTrustCrtFile, File clientKeyFile, File clientCrtFile, String clientKeyPassword) + throws InterruptedException, SSLException, CertificateException { + serverSslCtx = new JdkSslServerContext(servertTrustCrtFile, null, + serverCrtFile, serverKeyFile, serverKeyPassword, null, + null, IdentityCipherSuiteFilter.INSTANCE, (ApplicationProtocolConfig) null, 0, 0); + clientSslCtx = new JdkSslClientContext(clientTrustCrtFile, null, + clientCrtFile, clientKeyFile, clientKeyPassword, null, + null, IdentityCipherSuiteFilter.INSTANCE, (ApplicationProtocolConfig) null, 0, 0); + + serverConnectedChannel = null; + sb = new ServerBootstrap(); + cb = new Bootstrap(); + + sb.group(new NioEventLoopGroup(), new NioEventLoopGroup()); + sb.channel(NioServerSocketChannel.class); + sb.childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + SSLEngine engine = serverSslCtx.newEngine(ch.alloc()); + engine.setUseClientMode(false); + engine.setNeedClientAuth(true); + p.addLast(new SslHandler(engine)); + p.addLast(new MessageDelegatorChannelHandler(serverReceiver, serverLatch)); + p.addLast(new ChannelHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (cause.getCause() instanceof SSLHandshakeException) { + serverException = cause.getCause(); + serverLatch.countDown(); + } else { + ctx.fireExceptionCaught(cause); + } + } + }); + serverConnectedChannel = ch; + } + }); + + cb.group(new NioEventLoopGroup()); + cb.channel(NioSocketChannel.class); + cb.handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(clientSslCtx.newHandler(ch.alloc())); + p.addLast(new MessageDelegatorChannelHandler(clientReceiver, clientLatch)); + p.addLast(new ChannelHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (cause.getCause() instanceof SSLHandshakeException) { + clientException = cause.getCause(); + clientLatch.countDown(); + } else { + ctx.fireExceptionCaught(cause); + } + } + }); + } + }); + + serverChannel = sb.bind(new InetSocketAddress(0)).sync().channel(); + int port = ((InetSocketAddress) serverChannel.localAddress()).getPort(); + + ChannelFuture ccf = cb.connect(new InetSocketAddress(NetUtil.LOCALHOST, port)); + assertTrue(ccf.awaitUninterruptibly().isSuccess()); + clientChannel = ccf.channel(); + } + + private void runTest() throws Exception { + runTest(APPLICATION_LEVEL_PROTOCOL); + } + + private void runTest(String expectedApplicationProtocol) throws Exception { + 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); + if (expectedApplicationProtocol != null) { + verifyApplicationLevelProtocol(clientChannel, expectedApplicationProtocol); + verifyApplicationLevelProtocol(serverConnectedChannel, expectedApplicationProtocol); + } + } finally { + clientMessage.release(); + serverMessage.release(); + } + } + + private void verifyApplicationLevelProtocol(Channel channel, String expectedApplicationProtocol) { + SslHandler handler = channel.pipeline().get(SslHandler.class); + assertNotNull(handler); + String[] protocol = handler.engine().getSession().getProtocol().split(":"); + assertNotNull(protocol); + if (expectedApplicationProtocol != null && !expectedApplicationProtocol.isEmpty()) { + assertTrue("protocol.length must be greater than 1 but is " + protocol.length, protocol.length > 1); + assertEquals(expectedApplicationProtocol, protocol[1]); + } else { + assertEquals(1, protocol.length); + } + } + + private static void writeAndVerifyReceived(ByteBuf message, Channel sendChannel, CountDownLatch receiverLatch, + MessageReciever receiver) throws Exception { + 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(); + } + } + } + } +} diff --git a/handler/src/test/java/io/netty/handler/ssl/JdkSslServerContextTest.java b/handler/src/test/java/io/netty/handler/ssl/JdkSslServerContextTest.java index c196b40f28..9eccd91b03 100644 --- a/handler/src/test/java/io/netty/handler/ssl/JdkSslServerContextTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/JdkSslServerContextTest.java @@ -31,6 +31,14 @@ public class JdkSslServerContextTest { new JdkSslServerContext(crtFile, keyFile, "12345"); } + @Test + public void testJdkSslServerWithEncryptedPrivateKey2() throws SSLException { + File keyFile = new File(getClass().getResource("test2_encrypted.pem").getFile()); + File crtFile = new File(getClass().getResource("test2.crt").getFile()); + + new JdkSslServerContext(crtFile, keyFile, "12345"); + } + @Test public void testJdkSslServerWithUnencryptedPrivateKey() throws SSLException { File keyFile = new File(getClass().getResource("test_unencrypted.pem").getFile()); diff --git a/handler/src/test/resources/io/netty/handler/ssl/test2.crt b/handler/src/test/resources/io/netty/handler/ssl/test2.crt new file mode 100644 index 0000000000..7f4b30d446 --- /dev/null +++ b/handler/src/test/resources/io/netty/handler/ssl/test2.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC6DCCAdACCQCp0Mn/2UCl2TANBgkqhkiG9w0BAQsFADA2MTQwMgYDVQQDDCtj +ZmYyNGEwY2I4NGFmNjExZDdhODFjMGI4MDY4OTA2OC5uZXR0eS50ZXN0MB4XDTE0 +MTAxNzE4NDczM1oXDTE0MTExNjE4NDczM1owNjE0MDIGA1UEAwwrY2ZmMjRhMGNi +ODRhZjYxMWQ3YTgxYzBiODA2ODkwNjgubmV0dHkudGVzdDCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBALgddI5XJcUK45ONr4QTfZZxbJJeOYKPEWVIWK/P +Wz6EJXt3hDdpmnaRUKAv4mMIFlxWVkxTqa/dB3hjcm5hPvNgPAUaEWzMtGd32p95 +sJzbxiWvxhf5rqF0n1Zk5KX+EcasiCupNg3TL7gfTSSZfaGSWf460oaCS6WCU4X9 +XTUhys7N5BFM+uQLE048CnkBCO1An980Fau/0+BLXgW+iJC6XWTJbpZ+r7rDpBKl ++HmQQ5tgGlCZcnhmS9bzYT3hoag6JkDoIwbFsVOkwemxZGb8GsGE74/rrzUJ9MdR +/ETCA2km1na6ESst0/wm0qD3clJahP8xEoaJ+W1TFGizRWkCAwEAATANBgkqhkiG +9w0BAQsFAAOCAQEAmeGPRWXzu7+f20ZJA/u6WjcmsUhSTtt0YcBNtii4Pm0snIE9 +UyRBGlvS2uFHTilD7MOYOHX6ATlHZAsfegpiPE5jCvE4CzFPpQaVAT/sKNtsWH43 +ZQHn4NK1DAFIVDysO3AGGhL0mub8iWEYHs81+6tSSFlbDFqwYtw7ueerhVLUIaIa +S0SvtXUVitX2LzMlYCEto2s50fcqOnj8uve/dG8BmiwR1DqqVKkAWAXf8uGhwwD+ +659E3g9vNz6QUchd8K/TIv52i8EDuWu3FElohmfFUXu43A+Z+lbuDrEW3suqTC3y +0JIa2DfHWA7WTyF4UD32aAC+U6BLIOA6WoPi1Q== +-----END CERTIFICATE----- diff --git a/handler/src/test/resources/io/netty/handler/ssl/test2_encrypted.pem b/handler/src/test/resources/io/netty/handler/ssl/test2_encrypted.pem new file mode 100644 index 0000000000..a17f9dc27e --- /dev/null +++ b/handler/src/test/resources/io/netty/handler/ssl/test2_encrypted.pem @@ -0,0 +1,29 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIE6jAcBgoqhkiG9w0BDAEDMA4ECCqT2dycwPtCAgIIAASCBMg/Z60Q85kVL5kv +q8WIIY9tbXo/2Q+6rspxdit9SRd86MV9QRdfZ5Vjwt0JTa+Rd1gMaNK4PySW23bq +F2+dD0sjVBcE24Qg0h4BcmL+YBdTftBfk7NDH/rHhsew7DZru9fdDvkO9bV3jXIz +fARW9U7JIfgAi6CfJ8Q1PS7sg6dVtrcjMRIie32x0TSbZrn+h9AaXpLHsC8oXiyY +BhWe4i9B7PobyJ0r/CTBFhbfUCGwRyHac0+bZXvlcwX9wy3W7jagc6RDlznOpowU +FP35CQGeKsJ9WD+yy5MU8X8M8v+eeaJk4oX+PSWJX669CxbYocVP/+LUtOXpe+4h +7yMmVNLUtsgBlY6tNsU0XBQkrqqb+voSxVBEVZ1WTKgLWsE/EiQ2P2GU8Gnr+J6c +/yHxw0D4q9J3jV40SiuXQlgFwlf8u9FuVjOcGxTidfKXyvNqPKqgkf9QD+7E09q3 +JQoNbI/A8BXrpdx9h87Gt0TblPwVJP2nf5whig9W62R4y9SWybUUNr2MFNkvEfKe +1QK8isf+HlvIO+VBYi4jof9HkWLwnAszlkpC+k1cOiSjNRn8QyLzsqX7A/VuS6W8 +6kKeND4yRNA4b7rfQqhyGg7gBwiwN+22UF6SKiikX4TB1ZyLdzlbPe0L+X/Gq0Jz +Kf+8/slgzB5K9WpDtKsARH/lRPAx1rcascvFxMuCJL5O9MO9l4xWDJor71WgPC2N +KwXxvEW3Kyvs3pSgWc8MC0BKcD9WIAahAlAVmSQBxDNWvJlGTgUVhzPqan7h03Fd +nWAxSn315ObfK9rjbqUBO9x/nkSZFS9nApmeiWkOIwVzgNfAfb9md07TYyC/rpK3 +nGIsThekqqQULMQaAPmEFqUj6A/0KlpBj1gZwddYvVvEL/MuQO0QBdz4n/OncxYP +TVoQEqXsndmNQnkuk2Kr4FACV2M9rbr84HJUIZVGGVSM5h80GrRqK03qpTzM8Nkc +e04R4KDpLDKHm+G4xYZbbraIGXNTkhxTqdNA2FyjJWFurmpQyFay55vC6WBFBVNA +BGVIqD1/9K3dJJGlpiHyymRCK9YGvflZlSr7dm7PW7PPEthwTijbAHkABOKsFSiu +xaUj027WIVuDb5FFIAaF3Wmn4GFXvsSH+8L95CQuXGB8J/5Buo+/Hg6S7PeDwrf+ +qNRAfg9vxo+AZOWpWfGEYGHQeX6BxVjdffar9RwL99cele4h2FgBLtIuAXvgLPyx +b+MIjDliCe1Nqx0PCCuaB1xRnaKiwbl7itDidzI8BUAaFcKxbBH2lpr44+vYPVHb +70Xrw55RLvrVYKAcaZgryTNOvbRatifJIMg3kf8V++2rwUMoZ+DQfXin/C4S/2/b +c6I1OvYaGxmI1YiI6qSpOryDSzTNlDEWcdh5feuixiP5RbyaQFswq2fH0hsWWHS4 +OsCeqT0nm5vd1CdUFQJ4Nuh/TTdgCAVKk5yJZJvH2BX77I2d4T0ZRGHLDKUm8P0E +n6ntrMqLFR+QooONAZg0DTaxvbsCvaupRJCn9NgiwtXyYJKbvf5F8NEOe57NoGwd +LqQ332mVTuJ1DiqnChLoe7Mz7OY21RsTa/AK5Q/onClvBATrLD0ynK4WiLn4+hGs +HK5t3audgdnrLxs4UoA= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/handler/src/test/resources/io/netty/handler/ssl/test2_unencrypted.pem b/handler/src/test/resources/io/netty/handler/ssl/test2_unencrypted.pem new file mode 100644 index 0000000000..209a9c05be --- /dev/null +++ b/handler/src/test/resources/io/netty/handler/ssl/test2_unencrypted.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC4HXSOVyXFCuOT +ja+EE32WcWySXjmCjxFlSFivz1s+hCV7d4Q3aZp2kVCgL+JjCBZcVlZMU6mv3Qd4 +Y3JuYT7zYDwFGhFszLRnd9qfebCc28Ylr8YX+a6hdJ9WZOSl/hHGrIgrqTYN0y+4 +H00kmX2hkln+OtKGgkulglOF/V01IcrOzeQRTPrkCxNOPAp5AQjtQJ/fNBWrv9Pg +S14FvoiQul1kyW6Wfq+6w6QSpfh5kEObYBpQmXJ4ZkvW82E94aGoOiZA6CMGxbFT +pMHpsWRm/BrBhO+P6681CfTHUfxEwgNpJtZ2uhErLdP8JtKg93JSWoT/MRKGiflt +UxRos0VpAgMBAAECggEAYiTZd/L+oD3AuGwjrp0RKjwGKzPtJiqLlFjvZbB8LCQX +Muyv3zX8781glDNSU4YBHXGsiP1kC+ofzE3+ttZBz0xyUinmNgAc/rbGJJKi0crZ +okdDqo4fR9O6CDy6Ib4Azc40vEl0FgSIgHa3EZZ8gL9aF4pVpPwZxP1m9prrr6EP +SOlJP7rJNA/sTpuy0gz+UAu2Xf53pdkREUW7E2uzIGwrHxQVserN7Xxtft/zT79/ +oIHF09pHfiqE8a2TuVvVavjwV6787PSewFs7j8iKId9bpo1O7iqvj0UKOE+/63Lf +1pWRn7lRGS9ACw8EoyTY/M0njUbDEfaObJUzt08pjQKBgQDevZLRQjbGDtKOfQe6 +PKb/6PeFEE466NPFKH1bEz26VmC5vzF8U7lk71S11Dma51+vbOENzS5VlqOWqO+N +CyXTzb8a0rHXXUEP4+V6CazesTOEoBKViDswt2ffJfQYoCOFfKrcKq0j1Ps8Svhq +yzcMjAfX8eKIDWxK3qk+09SBtwKBgQDTm2Te4ENYwV5be+Z5L5See8gHNU5w3RtU +koO54TYBeJOTsTTtGDqEg60MoWIcx69OAJlHwTp5nPV5fhrjB8I9WUmI+2sPK7sU +OmhV/QzPjr6HW7fpbvbZ6fT+/Ay3aREa+qsJMypXsoqML1/fAeBno3hvHQt5Neog +leu3m0/x3wKBgQCCc8b8FeqcfuvkleejtIgeU2Q8I3ud1uTIkNkyMQezDYni385s +wWBQdDdJsvz181LAHGWGvsfHSs2OnGyIT6Ic9WBaplGQD8beNpwcqHP9jQzePR4F +Q99evdvw/nqCva9wK76p6bizxrZJ7qKlcVVRXOXvHHSPOEVXaCb5a/kG6wKBgGN6 +2G8XC1I8hfmIRA+Q2NOw6ZbJ7riMmf6mapsGT3ddkjOKyZD1JP2LUd1wOUnCbp3D +FkxvgOgPbC/Toxw8V4qz4Sgu2mPlcSvPUaGrN0yUlOnZqpppek9z96OwJuJK2KnQ +Unweu7dCznOdCfszTKYsacAC7ZPsTsdG8+v7bhgNAoGBAL8wlTp3tfQ2iuGDnQaf +268BBUtqp2qPlGPXCdkc5XXbnHXLFY/UYGw27Vh+UNW8UORTFYEb8XPvUxB4q2Mx +8ZZdcjFB1J4dM2+KGr51CEuzzpFuhFU8Nn4D/hcfYNKg733gTeSoI0Gs2Y9R+bDo ++cA9UxmyFSgS+Dq/7BOmPCDI +-----END PRIVATE KEY----- diff --git a/pom.xml b/pom.xml index 890a0fb397..df99d402d3 100644 --- a/pom.xml +++ b/pom.xml @@ -109,7 +109,20 @@ false - -D_ + 8.1.0.v20141016 + -Xbootclasspath/p:${jetty.alpn.path} + + + + alpn-8u25 + + + java.version + 1.8.0_25 + + + + 8.1.1.v20141016 @@ -281,6 +294,8 @@ 1.1.6.v20130911 + 7.1.0.v20141016 + @@ -293,6 +308,8 @@ 1.1.6.v20130911 + 7.1.0.v20141016 + @@ -305,6 +322,109 @@ 1.1.6.v20130911 + 7.1.0.v20141016 + + + + + npn-alpn-7u55 + + + java.version + 1.7.0_55 + + + + 1.1.8.v20141013 + 7.1.0.v20141016 + + + + + npn-alpn-7u60 + + + java.version + 1.7.0_60 + + + + 1.1.8.v20141013 + 7.1.0.v20141016 + + + + + npn-alpn-7u65 + + + java.version + 1.7.0_65 + + + + 1.1.8.v20141013 + 7.1.0.v20141016 + + + + + npn-alpn-7u67 + + + java.version + 1.7.0_67 + + + + 1.1.8.v20141013 + 7.1.0.v20141016 + + + + + npn-alpn-7u71 + + + java.version + 1.7.0_71 + + + + 1.1.9.v20141016 + 7.1.0.v20141016 + + + + + npn-alpn-7u72 + + + java.version + 1.7.0_72 + + + + 1.1.9.v20141016 + 7.1.0.v20141016 + + + + + + forcenpn + + + forcenpn + true + + + + -Xbootclasspath/p:${jetty.npn.path} @@ -313,8 +433,10 @@ UTF-8 UTF-8 1.3.18.GA - 1.1.7.v20140316 + 1.1.9.v20141016 ${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${jetty.npn.version}/npn-boot-${jetty.npn.version}.jar + 8.1.0.v20141016 + ${settings.localRepository}/org/mortbay/jetty/alpn/alpn-boot/${jetty.alpn.version}/alpn-boot-${jetty.alpn.version}.jar -server -dsa -da -ea:io.netty... @@ -372,13 +494,23 @@ org.eclipse.jetty.npn npn-api - 1.1.0.v20120525 + 1.1.1.v20141010 org.mortbay.jetty.npn npn-boot ${jetty.npn.version} + + org.eclipse.jetty.alpn + alpn-api + 1.1.0.v20141014 + + + org.mortbay.jetty.alpn + alpn-boot + ${jetty.alpn.version} + @@ -635,6 +767,14 @@ org.codehaus.mojo animal-sniffer-maven-plugin 1.9 + + + + org.ow2.asm + asm-all + 5.0.3 + + org.codehaus.mojo.signature @@ -761,7 +901,7 @@ ${project.groupId}.* - sun.misc.*;resolution:=optional,sun.nio.ch;resolution:=optional,sun.security.*;resolution:=optional,* + sun.misc.*;resolution:=optional,sun.nio.ch;resolution:=optional,sun.security.*;resolution:=optional,org.eclipse.jetty.npn;version="[1,)";resolution=optional,org.eclipse.jetty.alpn;version="[1,)";resolution=optional,* !*