Repository: netty/netty
Issue: Mutual SSL with OpenSSL
Labels: feature

Hi guys,

I tried to use OpenSSL provider for mutual SSL auth and getting error : 

```
io.netty.handler.codec.DecoderException: javax.net.ssl.SSLException: error:140890B2:SSL routines:SSL3_GET_CLIENT_CERTIFICATE:no certificate returned
    at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:346)
    at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:229)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:339)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:324)
    at io.netty.handler.timeout.ReadTimeoutHandler.channelRead(ReadTimeoutHandler.java:150)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:339)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:324)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:847)
    at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:618)
    at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:329)
    at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:250)
    at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:111)
    at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:137)
    at java.lang.Thread.run(Thread.java:745)
Caused by: javax.net.ssl.SSLException: error:140890B2:SSL routines:SSL3_GET_CLIENT_CERTIFICATE:no certificate returned
    at io.netty.handler.ssl.OpenSslEngine.unwrap(OpenSslEngine.java:590)
    at io.netty.handler.ssl.OpenSslEngine.unwrap(OpenSslEngine.java:660)
    at javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:624)
    at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1114)
    at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:981)
    at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:934)
    at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:315)
    ... 13 more
```

With JDK provider everything seems fine, with OpenSSL provider without mutual SSL also everything is working. Does OpenSSL provider mutual auth requires some additional configuration? 
My code is almost same as in JdkSslEngineTest.class specially in mySetupMutualAuth method.


Issue comments:
@doom369 can you show me the code ?

@normanmaurer, sure.

Server : 

```
    private static SslContext initSslContext(String serverCertPath, String serverKeyPath, String serverPass,
                                             String clientCertPath,
                                             SslProvider sslProvider) {
        try {
            if (sslProvider == SslProvider.OPENSSL) {
                log.warn("Using native openSSL provider.");
            }

            File serverCert =  new File(serverCertPath);
            File serverKey = new File(serverKeyPath);
            File clientCert =  new File(clientCertPath);

            if (!serverCert.exists() || !serverKey.exists() || !clientCert.exists()) {
                log.warn("ATTENTION. Certificate path not valid. Using embedded certs. This is not secure. Please replace it with your own certs.");
                SelfSignedCertificate ssc = new SelfSignedCertificate();
                return SslContext.newServerContext(sslProvider, ssc.certificate(), ssc.privateKey());
            }

            return SslContext.newServerContext(sslProvider, clientCert, null, serverCert, serverKey, serverPass,
                    null, null, IdentityCipherSuiteFilter.INSTANCE, null, 0 ,0);
        } catch (CertificateException | SSLException | IllegalArgumentException e) {
            log.error("Error initializing ssl context. Reason : {}", e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

 protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        pipeline.addLast(new ReadTimeoutHandler(appTimeoutSecs));

        if (sslCtx != null) {
            SSLEngine engine = sslCtx.newEngine(ch.alloc());
            engine.setUseClientMode(false);
            engine.setNeedClientAuth(true);
            pipeline.addLast(new SslHandler(engine));
        }

        //non-sharable handlers
        pipeline.addLast(new ClientChannelStateHandler(sessionsHolder));
        pipeline.addLast(new MessageDecoder(stats));
        pipeline.addLast(new MessageEncoder());
}
```

Client:

```
    public AppClient(String host, int port, Random msgIdGenerator, boolean disableAppSsl) {
        super(host, port, msgIdGenerator);
        log.info("Creating app client. Host {}, sslPort : {}", host, port);

        if (!disableAppSsl) {
            //todo think how to simplify with real certs?
            //sslCtx = SslContext.newClientContext(getFileFromResources("/test.crt"));
            try {
                //this.sslCtx = SslContext.newClientContext(InsecureTrustManagerFactory.INSTANCE);
                this.sslCtx = SslContext.newClientContext(SslProvider.JDK, new File("/home/doom369/server/certs/server/server.crt"), null,
                        new File("/home/doom369/server/certs/client/client.crt"), new File("/home/doom369/server/certs/client/client.pem"), "my_pass",
                        null, null, IdentityCipherSuiteFilter.INSTANCE, null, 0, 0);
            } catch (SSLException e) {
                log.error("Error initializing SSL context. Reason : {}", e.getMessage());
                log.debug(e);
                throw new RuntimeException();
            }
        }
    }

            public void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline pipeline = ch.pipeline();
                if (sslCtx != null) {
                    pipeline.addLast(sslCtx.newHandler(ch.alloc(), host, port));
                }
                pipeline.addLast(new ClientReplayingMessageDecoder());
                pipeline.addLast(new MessageEncoder());
            }
```

@doom369 thanks will check.

@doom369 also which version? :)

@normanmaurer 4.0.27.Final

Maybe this also may have some impact.

```
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-tcnative</artifactId>
            <version>1.1.32.Fork1</version>
            <classifier>linux-x86_64</classifier>
        </dependency>
```

@doom369 working on it...

Cool, thanks. I haven't yet a time to investigate by myself.

@doom369 fixed via #3732

@normanmaurer,
Just checked it with latest SNAPSHOT. It works! Thank you for quick fix!


Pull request review comments:
Use `@RunWith(Parameterized.class)` instead.

Use `@RunWith(Parameterized.class)` instead.

`"keycertChainFile"`

`"keyCertChainFile is not a file: "`

Do we need to support `SSL_AIDX_DSA` either?

Can you show me an example?

> Am 06.05.2015 um 07:56 schrieb Trustin Lee notifications@github.com:
> 
> In handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java:
> 
> > - \* 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;
> >   +
> >   +public class OpenSslEngineTest extends SSLEngineTest {
> > -    @Override
> > -    protected SslProvider sslProvider() {
> > -        return SslProvider.OPENSSL;
> >   Use @RunWith(Parameterized.class) instead.
> 
> —
> Reply to this email directly or view it on GitHub.

and maybe others like: SSL_AIDX_EC, SSL_AIDX_GOST94, and SSL_AIDX_GOST01

Hmm.. We did before I not changed it

> Am 06.05.2015 um 07:59 schrieb Trustin Lee notifications@github.com:
> 
> In handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java:
> 
> > ```
> >                  }
> >              }
> > 
> >              /* Load the certificate file and private key. */
> >              try {
> >                  if (!SSLContext.setCertificate(
> > ```
> > -                            ctx, certChainFile.getPath(), keyFile.getPath(), keyPassword, SSL.SSL_AIDX_RSA)) {
> > -                            ctx, keyCertChainFile.getPath(), keyFile.getPath(), keyPassword, SSL.SSL_AIDX_RSA)) {
> >   Do we need to support SSL_AIDX_DSA either?
> 
> —
> Reply to this email directly or view it on GitHub.

`SocketSslEchoTest`

OK. Maybe later then.

Let me open an issue to track this.

fixed

fixed

Change to javadoc style?

Shouldn't these apply to OpenSSL too?

I will follow up with NPN and ALPN tests for openssl with another PR as it is not related to this feature.

Thanks! SGTM!


Patch that resolved the issue:
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 e031bd54411..54ee2befbf6 100644
--- a/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java
@@ -16,9 +16,7 @@
 
 package io.netty.handler.ssl;
 
-import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
-import io.netty.buffer.ByteBufInputStream;
 import io.netty.util.internal.EmptyArrays;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -28,8 +26,6 @@
 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;
 import java.io.File;
 import java.io.IOException;
 import java.security.InvalidAlgorithmParameterException;
@@ -40,8 +36,6 @@
 import java.security.Security;
 import java.security.UnrecoverableKeyException;
 import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
 import java.security.spec.InvalidKeySpecException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -325,39 +319,4 @@ protected static KeyManagerFactory buildKeyManagerFactory(File certChainFile,
 
         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;
-    }
 }
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java
index d88e289b3e4..85f15d103b3 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java
@@ -20,6 +20,7 @@
 import org.apache.tomcat.jni.SSL;
 import org.apache.tomcat.jni.SSLContext;
 
+import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLException;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.TrustManagerFactory;
@@ -39,13 +40,12 @@
  */
 public final class OpenSslClientContext extends OpenSslContext {
     private final OpenSslSessionContext sessionContext;
-    private final OpenSslEngineMap engineMap;
 
     /**
      * Creates a new instance.
      */
     public OpenSslClientContext() throws SSLException {
-        this(null, null, null, IdentityCipherSuiteFilter.INSTANCE, null, 0, 0);
+        this(null, null, null, null, null, null, null, IdentityCipherSuiteFilter.INSTANCE, null, 0, 0);
     }
 
     /**
@@ -79,7 +79,8 @@ public OpenSslClientContext(TrustManagerFactory trustManagerFactory) throws SSLE
      *                            {@code null} to use the default.
      */
     public OpenSslClientContext(File certChainFile, TrustManagerFactory trustManagerFactory) throws SSLException {
-        this(certChainFile, trustManagerFactory, null, IdentityCipherSuiteFilter.INSTANCE, null, 0, 0);
+        this(certChainFile, trustManagerFactory, null, null, null, null, null,
+             IdentityCipherSuiteFilter.INSTANCE, null, 0, 0);
     }
 
     /**
@@ -104,11 +105,14 @@ public OpenSslClientContext(File certChainFile, TrustManagerFactory trustManager
     public OpenSslClientContext(File certChainFile, TrustManagerFactory trustManagerFactory, Iterable<String> ciphers,
                                 ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout)
             throws SSLException {
-        this(certChainFile, trustManagerFactory, ciphers, IdentityCipherSuiteFilter.INSTANCE,
+        this(certChainFile, trustManagerFactory, null, null, null, null, ciphers, IdentityCipherSuiteFilter.INSTANCE,
                 apn, sessionCacheSize, sessionTimeout);
     }
 
     /**
+     * @deprecated use {@link #OpenSslClientContext(File, TrustManagerFactory, File, File, String,
+     * KeyManagerFactory, Iterable, CipherSuiteFilter, ApplicationProtocolConfig,long, long)}
+     *
      * Creates a new instance.
      *
      * @param certChainFile an X.509 certificate chain file in PEM format
@@ -124,29 +128,98 @@ public OpenSslClientContext(File certChainFile, TrustManagerFactory trustManager
      * @param sessionTimeout the timeout for the cached SSL session objects, in seconds.
      *                       {@code 0} to use the default value.
      */
+    @Deprecated
     public OpenSslClientContext(File certChainFile, TrustManagerFactory trustManagerFactory, Iterable<String> ciphers,
+                                CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
+                                long sessionCacheSize, long sessionTimeout) throws SSLException {
+        this(certChainFile, trustManagerFactory, null, null, null, null,
+             ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout);
+    }
+
+    /**
+     * 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 javax.net.ssl.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 OpenSslClientContext(File trustCertChainFile, TrustManagerFactory trustManagerFactory,
+                                File keyCertChainFile, File keyFile, String keyPassword,
+                                KeyManagerFactory keyManagerFactory, Iterable<String> ciphers,
                                 CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
                                 long sessionCacheSize, long sessionTimeout)
             throws SSLException {
         super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_CLIENT);
         boolean success = false;
         try {
-            if (certChainFile != null && !certChainFile.isFile()) {
-                throw new IllegalArgumentException("certChainFile is not a file: " + certChainFile);
+            if (trustCertChainFile != null && !trustCertChainFile.isFile()) {
+                throw new IllegalArgumentException("trustCertChainFile is not a file: " + trustCertChainFile);
+            }
+
+            if (keyCertChainFile != null && !keyCertChainFile.isFile()) {
+                throw new IllegalArgumentException("keyCertChainFile is not a file: " + keyCertChainFile);
             }
 
+            if (keyFile != null && !keyFile.isFile()) {
+                throw new IllegalArgumentException("keyFile is not a file: " + keyFile);
+            }
+            if (keyFile == null && keyCertChainFile != null || keyFile != null && keyCertChainFile == null) {
+                throw new IllegalArgumentException(
+                        "Either both keyCertChainFile and keyFile needs to be null or none of them");
+            }
             synchronized (OpenSslContext.class) {
-                if (certChainFile != null) {
+                if (trustCertChainFile != null) {
                     /* Load the certificate chain. We must skip the first cert when server mode */
-                    if (!SSLContext.setCertificateChainFile(ctx, certChainFile.getPath(), true)) {
+                    if (!SSLContext.setCertificateChainFile(ctx, trustCertChainFile.getPath(), true)) {
                         long error = SSL.getLastErrorNumber();
                         if (OpenSsl.isError(error)) {
                             throw new SSLException(
                                     "failed to set certificate chain: "
-                                            + certChainFile + " (" + SSL.getErrorString(error) + ')');
+                                            + trustCertChainFile + " (" + SSL.getErrorString(error) + ')');
+                        }
+                    }
+                }
+                if (keyCertChainFile != null && keyFile != null) {
+                    /* Load the certificate file and private key. */
+                    try {
+                        if (!SSLContext.setCertificate(
+                                ctx, keyCertChainFile.getPath(), keyFile.getPath(), keyPassword, SSL.SSL_AIDX_RSA)) {
+                            long error = SSL.getLastErrorNumber();
+                            if (OpenSsl.isError(error)) {
+                                throw new SSLException("failed to set certificate: " +
+                                                       keyCertChainFile + " and " + keyFile +
+                                                       " (" + SSL.getErrorString(error) + ')');
+                            }
                         }
+                    } catch (SSLException e) {
+                        throw e;
+                    } catch (Exception e) {
+                        throw new SSLException("failed to set certificate: " + keyCertChainFile + " and " + keyFile, e);
                     }
                 }
+
                 SSLContext.setVerify(ctx, SSL.SSL_VERIFY_NONE, VERIFY_DEPTH);
 
                 try {
@@ -155,25 +228,24 @@ public OpenSslClientContext(File certChainFile, TrustManagerFactory trustManager
                         trustManagerFactory = TrustManagerFactory.getInstance(
                                 TrustManagerFactory.getDefaultAlgorithm());
                     }
-                    initTrustManagerFactory(certChainFile, trustManagerFactory);
+                    initTrustManagerFactory(trustCertChainFile, trustManagerFactory);
                     final X509TrustManager manager = chooseTrustManager(trustManagerFactory.getTrustManagers());
 
-                    engineMap = newEngineMap(manager);
-
                     // Use this to prevent an error when running on java < 7
                     if (useExtendedTrustManager(manager)) {
                         final X509ExtendedTrustManager extendedManager = (X509ExtendedTrustManager) manager;
                         SSLContext.setCertVerifyCallback(ctx, new AbstractCertificateVerifier() {
                             @Override
-                            void verify(long ssl, X509Certificate[] peerCerts, String auth) throws Exception {
-                                OpenSslEngine engine = engineMap.remove(ssl);
+                            void verify(OpenSslEngine engine, X509Certificate[] peerCerts, String auth)
+                                    throws Exception {
                                 extendedManager.checkServerTrusted(peerCerts, auth, engine);
                             }
                         });
                     } else {
                         SSLContext.setCertVerifyCallback(ctx, new AbstractCertificateVerifier() {
                             @Override
-                            void verify(long ssl, X509Certificate[] peerCerts, String auth) throws Exception {
+                            void verify(OpenSslEngine engine, X509Certificate[] peerCerts, String auth)
+                                    throws Exception {
                                 manager.checkServerTrusted(peerCerts, auth);
                             }
                         });
@@ -218,11 +290,6 @@ public OpenSslSessionContext sessionContext() {
         return sessionContext;
     }
 
-    @Override
-    OpenSslEngineMap engineMap() {
-        return engineMap;
-    }
-
     // No cache is currently supported for client side mode.
     private static final class OpenSslClientSessionContext extends OpenSslSessionContext {
         private OpenSslClientSessionContext(long context) {
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java
index 06f9d510ccf..d642e223867 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java
@@ -26,6 +26,7 @@
 
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.X509ExtendedTrustManager;
 import javax.net.ssl.X509TrustManager;
@@ -56,6 +57,8 @@ public abstract class OpenSslContext extends SslContext {
     private final List<String> unmodifiableCiphers;
     private final long sessionCacheSize;
     private final long sessionTimeout;
+    private final OpenSslEngineMap engineMap = new DefaultOpenSslEngineMap();
+
     private final OpenSslApplicationProtocolNegotiator apn;
     /** The OpenSSL SSL_CTX object */
     protected final long ctx;
@@ -277,14 +280,11 @@ public final SSLEngine newEngine(ByteBufAllocator alloc, String peerHost, int pe
      */
     @Override
     public final SSLEngine newEngine(ByteBufAllocator alloc) {
-        OpenSslEngineMap engineMap = engineMap();
         final OpenSslEngine engine = new OpenSslEngine(ctx, alloc, isClient(), sessionContext(), apn, engineMap);
         engineMap.add(engine);
         return engine;
     }
 
-    abstract OpenSslEngineMap engineMap();
-
     /**
      * Returns the {@code SSL_CTX} object of this context.
      */
@@ -392,31 +392,28 @@ static OpenSslApplicationProtocolNegotiator toNegotiator(ApplicationProtocolConf
         }
     }
 
-    static OpenSslEngineMap newEngineMap(X509TrustManager trustManager) {
-        if (useExtendedTrustManager(trustManager)) {
-            return new DefaultOpenSslEngineMap();
-        }
-        return OpenSslEngineMap.EMPTY;
-    }
-
     static boolean useExtendedTrustManager(X509TrustManager trustManager) {
          return PlatformDependent.javaVersion() >= 7 && trustManager instanceof X509ExtendedTrustManager;
     }
 
-    abstract static class AbstractCertificateVerifier implements CertificateVerifier {
+    abstract class AbstractCertificateVerifier implements CertificateVerifier {
         @Override
         public final boolean verify(long ssl, byte[][] chain, String auth) {
             X509Certificate[] peerCerts = certificates(chain);
+            final OpenSslEngine engine = engineMap.remove(ssl);
             try {
-                verify(ssl, peerCerts, auth);
+                verify(engine, peerCerts, auth);
                 return true;
-            } catch (Exception e) {
-                logger.debug("verification of certificate failed", e);
+            } catch (Throwable cause) {
+                logger.debug("verification of certificate failed", cause);
+                SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem");
+                e.initCause(cause);
+                engine.handshakeException = e;
             }
             return false;
         }
 
-        abstract void verify(long ssl, X509Certificate[] peerCerts, String auth) throws Exception;
+        abstract void verify(OpenSslEngine engine, X509Certificate[] peerCerts, String auth) throws Exception;
     }
 
     private static final class DefaultOpenSslEngineMap implements OpenSslEngineMap {
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslEngine.java
index ded15e93862..94dc55a9a16 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslEngine.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslEngine.java
@@ -29,6 +29,7 @@
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLEngineResult;
 import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSession;
 import javax.net.ssl.SSLSessionBindingEvent;
@@ -160,6 +161,10 @@ enum ClientAuthMode {
     private final OpenSslApplicationProtocolNegotiator apn;
     private final SSLSession session = new OpenSslSession();
 
+    // This is package-private as we set it from OpenSslContext if an exception is thrown during
+    // the verification step.
+    SSLHandshakeException handshakeException;
+
     /**
      * Creates a new instance
      *
@@ -489,6 +494,22 @@ public synchronized SSLEngineResult wrap(
         return new SSLEngineResult(getEngineStatus(), handshakeStatus0(), bytesConsumed, bytesProduced);
     }
 
+    private SSLException newSSLException(String msg) {
+        if (!handshakeFinished) {
+            return new SSLHandshakeException(msg);
+        }
+        return new SSLException(msg);
+    }
+
+    private void checkPendingHandshakeException() throws SSLHandshakeException {
+        if (handshakeException != null) {
+            SSLHandshakeException exception = handshakeException;
+            handshakeException = null;
+            shutdown();
+            throw exception;
+        }
+    }
+
     public synchronized SSLEngineResult unwrap(
             final ByteBuffer[] srcs, int srcsOffset, final int srcsLength,
             final ByteBuffer[] dsts, final int dstsOffset, final int dstsLength) throws SSLException {
@@ -608,7 +629,9 @@ public synchronized SSLEngineResult unwrap(
 
                     // There was an internal error -- shutdown
                     shutdown();
-                    throw new SSLException(err);
+                    throw newSSLException(err);
+                } else {
+                    checkPendingHandshakeException();
                 }
             }
         } else {
@@ -954,8 +977,9 @@ private void handshake() throws SSLException {
 
                 // There was an internal error -- shutdown
                 shutdown();
-                throw new SSLException(err);
+                throw newSSLException(err);
             }
+            checkPendingHandshakeException();
         } else {
             // if SSL_do_handshake returns > 0 it means the handshake was finished. This means we can update
             // handshakeFinished directly and so eliminate uncessary calls to SSL.isInInit(...)
@@ -1037,6 +1061,7 @@ private SSLEngineResult.HandshakeStatus handshakeStatus0() throws SSLException {
         if (status == FINISHED) {
             handshakeFinished();
         }
+        checkPendingHandshakeException();
 
         return status;
     }
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 207c0f0355b..83ee505eaa9 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java
@@ -19,7 +19,10 @@
 import org.apache.tomcat.jni.SSL;
 import org.apache.tomcat.jni.SSLContext;
 
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLException;
+import javax.net.ssl.TrustManager;
 import javax.net.ssl.TrustManagerFactory;
 import javax.net.ssl.X509ExtendedTrustManager;
 import javax.net.ssl.X509TrustManager;
@@ -34,7 +37,6 @@
  */
 public final class OpenSslServerContext extends OpenSslContext {
     private final OpenSslServerSessionContext sessionContext;
-    private final OpenSslEngineMap engineMap;
 
     /**
      * Creates a new instance.
@@ -55,8 +57,8 @@ public OpenSslServerContext(File certChainFile, File keyFile) throws SSLExceptio
      *                    {@code null} if it's not password-protected.
      */
     public OpenSslServerContext(File certChainFile, File keyFile, String keyPassword) throws SSLException {
-        this(certChainFile, keyFile, keyPassword, null, null, IdentityCipherSuiteFilter.INSTANCE,
-            NONE_PROTOCOL_NEGOTIATOR, 0, 0);
+        this(certChainFile, keyFile, keyPassword, null, IdentityCipherSuiteFilter.INSTANCE,
+             ApplicationProtocolConfig.DISABLED, 0, 0);
     }
 
     /**
@@ -82,8 +84,8 @@ public OpenSslServerContext(
             File certChainFile, File keyFile, String keyPassword,
             Iterable<String> ciphers, ApplicationProtocolConfig apn,
             long sessionCacheSize, long sessionTimeout) throws SSLException {
-        this(certChainFile, keyFile, keyPassword, null, ciphers,
-            toNegotiator(apn), sessionCacheSize, sessionTimeout);
+        this(certChainFile, keyFile, keyPassword, ciphers, IdentityCipherSuiteFilter.INSTANCE,
+             apn, sessionCacheSize, sessionTimeout);
     }
 
     /**
@@ -128,9 +130,8 @@ public OpenSslServerContext(
      *                         {@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.
-     * @deprecated use {@link #OpenSslServerContext(
-     *             File, File, String, TrustManagerFactory, Iterable,
-     *             CipherSuiteFilter, ApplicationProtocolConfig, long, long)}
+     * @deprecated use {@link #OpenSslServerContext(File, TrustManagerFactory, File, File, String, KeyManagerFactory,
+     * Iterable, CipherSuiteFilter, ApplicationProtocolConfig, long, long)}
      */
     @Deprecated
     public OpenSslServerContext(
@@ -155,17 +156,16 @@ public OpenSslServerContext(
      *                         {@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.
-     * @deprecated use {@link #OpenSslServerContext(
-     *             File, File, String, TrustManagerFactory, Iterable,
-     *             CipherSuiteFilter, OpenSslApplicationProtocolNegotiator, long, long)}
+     * @deprecated use {@link #OpenSslServerContext(File, TrustManagerFactory, File, File, String, KeyManagerFactory,
+     * Iterable, CipherSuiteFilter, ApplicationProtocolConfig, long, long)}
      */
     @Deprecated
     public OpenSslServerContext(
             File certChainFile, File keyFile, String keyPassword, TrustManagerFactory trustManagerFactory,
             Iterable<String> ciphers, OpenSslApplicationProtocolNegotiator apn,
             long sessionCacheSize, long sessionTimeout) throws SSLException {
-        this(certChainFile, keyFile, keyPassword, trustManagerFactory, ciphers,
-                IdentityCipherSuiteFilter.INSTANCE, apn, sessionCacheSize, sessionTimeout);
+        this(null, trustManagerFactory, certChainFile, keyFile, keyPassword, null,
+             ciphers, null, apn, sessionCacheSize, sessionTimeout);
     }
 
     /**
@@ -188,32 +188,69 @@ public OpenSslServerContext(
             File certChainFile, File keyFile, String keyPassword,
             Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
             long sessionCacheSize, long sessionTimeout) throws SSLException {
-        this(certChainFile, keyFile, keyPassword, null, ciphers, cipherFilter,
-                toNegotiator(apn), sessionCacheSize, sessionTimeout);
+        this(null, null, certChainFile, keyFile, keyPassword, null,
+             ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout);
     }
 
     /**
      * Creates a new instance.
      *
-     * @param certChainFile an X.509 certificate chain file in PEM format
+     * @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
-     * @param config Application protocol config.
+     *                Only required if {@code provider} is {@link SslProvider#JDK}
+     * @param config 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 OpenSslServerContext(
-            File certChainFile, File keyFile, String keyPassword, TrustManagerFactory trustManagerFactory,
+            File trustCertChainFile, TrustManagerFactory trustManagerFactory,
+            File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory,
             Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig config,
             long sessionCacheSize, long sessionTimeout) throws SSLException {
-        this(certChainFile, keyFile, keyPassword, trustManagerFactory, ciphers, cipherFilter,
-                toNegotiator(config), sessionCacheSize, sessionTimeout);
+        this(trustCertChainFile, trustManagerFactory, keyCertChainFile, keyFile, keyPassword, keyManagerFactory,
+             ciphers, cipherFilter, toNegotiator(config), 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 cipherFilter a filter to apply over the supplied list of ciphers
+     * @param config Application protocol config.
+     * @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.
+     */
+    @Deprecated
+    public OpenSslServerContext(File certChainFile, File keyFile, String keyPassword,
+                                TrustManagerFactory trustManagerFactory, Iterable<String> ciphers,
+                                CipherSuiteFilter cipherFilter, ApplicationProtocolConfig config,
+                                long sessionCacheSize, long sessionTimeout) throws SSLException {
+        this(null, trustManagerFactory, certChainFile, keyFile, keyPassword, null, ciphers, cipherFilter,
+                      toNegotiator(config), sessionCacheSize, sessionTimeout);
     }
 
     /**
@@ -231,21 +268,61 @@ public OpenSslServerContext(
      *                         {@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.
+     * @deprecated use {@link #OpenSslServerContext(File, TrustManagerFactory, File, File, String, KeyManagerFactory,
+     * Iterable, CipherSuiteFilter, OpenSslApplicationProtocolNegotiator, long, long)}
      */
+    @Deprecated
     public OpenSslServerContext(
             File certChainFile, File keyFile, String keyPassword, TrustManagerFactory trustManagerFactory,
             Iterable<String> ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn,
             long sessionCacheSize, long sessionTimeout) throws SSLException {
+        this(null, trustManagerFactory, certChainFile, keyFile, keyPassword, null, ciphers, cipherFilter,
+             apn, sessionCacheSize, sessionTimeout);
+    }
+
+    /**
+     * 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 OpenSslServerContext(
+            File trustCertChainFile, TrustManagerFactory trustManagerFactory,
+            File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory,
+            Iterable<String> ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn,
+            long sessionCacheSize, long sessionTimeout) throws SSLException {
         super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_SERVER);
         OpenSsl.ensureAvailability();
 
-        checkNotNull(certChainFile, "certChainFile");
-        if (!certChainFile.isFile()) {
-            throw new IllegalArgumentException("certChainFile is not a file: " + certChainFile);
+        checkNotNull(keyCertChainFile, "keyCertChainFile");
+        if (!keyCertChainFile.isFile()) {
+            throw new IllegalArgumentException("keyCertChainFile is not a file: " + keyCertChainFile);
         }
         checkNotNull(keyFile, "keyFile");
         if (!keyFile.isFile()) {
-            throw new IllegalArgumentException("keyPath is not a file: " + keyFile);
+            throw new IllegalArgumentException("keyFile is not a file: " + keyFile);
         }
         if (keyPassword == null) {
             keyPassword = "";
@@ -259,59 +336,64 @@ public OpenSslServerContext(
                 SSLContext.setVerify(ctx, SSL.SSL_CVERIFY_NONE, VERIFY_DEPTH);
 
                 /* Load the certificate chain. We must skip the first cert when server mode */
-                if (!SSLContext.setCertificateChainFile(ctx, certChainFile.getPath(), true)) {
+                if (!SSLContext.setCertificateChainFile(ctx, keyCertChainFile.getPath(), true)) {
                     long error = SSL.getLastErrorNumber();
                     if (OpenSsl.isError(error)) {
                         String err = SSL.getErrorString(error);
                         throw new SSLException(
-                                "failed to set certificate chain: " + certChainFile + " (" + err + ')');
+                                "failed to set certificate chain: " + keyCertChainFile + " (" + err + ')');
                     }
                 }
 
                 /* Load the certificate file and private key. */
                 try {
                     if (!SSLContext.setCertificate(
-                            ctx, certChainFile.getPath(), keyFile.getPath(), keyPassword, SSL.SSL_AIDX_RSA)) {
+                            ctx, keyCertChainFile.getPath(), keyFile.getPath(), keyPassword, SSL.SSL_AIDX_RSA)) {
                         long error = SSL.getLastErrorNumber();
                         if (OpenSsl.isError(error)) {
                             String err = SSL.getErrorString(error);
                             throw new SSLException("failed to set certificate: " +
-                                    certChainFile + " and " + keyFile + " (" + err + ')');
+                                                   keyCertChainFile + " and " + keyFile + " (" + err + ')');
                         }
                     }
                 } catch (SSLException e) {
                     throw e;
                 } catch (Exception e) {
-                    throw new SSLException("failed to set certificate: " + certChainFile + " and " + keyFile, e);
+                    throw new SSLException("failed to set certificate: " + keyCertChainFile + " and " + keyFile, e);
                 }
                 try {
-                    char[] keyPasswordChars = keyPassword == null ? EmptyArrays.EMPTY_CHARS : keyPassword.toCharArray();
-
-                    KeyStore ks = buildKeyStore(certChainFile, keyFile, keyPasswordChars);
                     if (trustManagerFactory == null) {
                         // Mimic the way SSLContext.getInstance(KeyManager[], null, null) works
                         trustManagerFactory = TrustManagerFactory.getInstance(
                                 TrustManagerFactory.getDefaultAlgorithm());
                     }
-                    trustManagerFactory.init(ks);
+                    if (trustCertChainFile != null) {
+                        trustManagerFactory = buildTrustManagerFactory(trustCertChainFile, trustManagerFactory);
+                    } else {
+                        char[] keyPasswordChars =
+                                keyPassword == null ? EmptyArrays.EMPTY_CHARS : keyPassword.toCharArray();
+
+                        KeyStore ks = buildKeyStore(keyCertChainFile, keyFile, keyPasswordChars);
+                        trustManagerFactory.init(ks);
+                    }
 
                     final X509TrustManager manager = chooseTrustManager(trustManagerFactory.getTrustManagers());
 
-                    engineMap = newEngineMap(manager);
                     // Use this to prevent an error when running on java < 7
                     if (useExtendedTrustManager(manager)) {
                         final X509ExtendedTrustManager extendedManager = (X509ExtendedTrustManager) manager;
                         SSLContext.setCertVerifyCallback(ctx, new AbstractCertificateVerifier() {
                             @Override
-                            void verify(long ssl, X509Certificate[] peerCerts, String auth) throws Exception {
-                                OpenSslEngine engine = engineMap.remove(ssl);
+                            void verify(OpenSslEngine engine, X509Certificate[] peerCerts, String auth)
+                                    throws Exception {
                                 extendedManager.checkClientTrusted(peerCerts, auth, engine);
                             }
                         });
                     } else {
                         SSLContext.setCertVerifyCallback(ctx, new AbstractCertificateVerifier() {
                             @Override
-                            void verify(long ssl, X509Certificate[] peerCerts, String auth) throws Exception {
+                            void verify(OpenSslEngine engine, X509Certificate[] peerCerts, String auth)
+                                    throws Exception {
                                 manager.checkClientTrusted(peerCerts, auth);
                             }
                         });
@@ -333,9 +415,4 @@ void verify(long ssl, X509Certificate[] peerCerts, String auth) throws Exception
     public OpenSslServerSessionContext sessionContext() {
         return sessionContext;
     }
-
-    @Override
-    OpenSslEngineMap engineMap() {
-        return engineMap;
-    }
 }
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 bcf428694df..890b36242bf 100644
--- a/handler/src/main/java/io/netty/handler/ssl/SslContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/SslContext.java
@@ -39,6 +39,7 @@
 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.io.IOException;
 import java.security.InvalidAlgorithmParameterException;
@@ -52,6 +53,7 @@
 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;
@@ -399,8 +401,8 @@ static SslContext newServerContextInternal(
                     keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout);
         case OPENSSL:
             return new OpenSslServerContext(
-                    keyCertChainFile, keyFile, keyPassword, trustManagerFactory,
-                    ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout);
+                    trustCertChainFile, trustManagerFactory, keyCertChainFile, keyFile, keyPassword,
+                    keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout);
         default:
             throw new Error(provider.toString());
         }
@@ -729,8 +731,8 @@ static SslContext newClientContextInternal(
                         keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout);
             case OPENSSL:
                 return new OpenSslClientContext(
-                        trustCertChainFile, trustManagerFactory, ciphers, cipherFilter, apn,
-                        sessionCacheSize, sessionTimeout);
+                        trustCertChainFile, trustManagerFactory, keyCertChainFile, keyFile, keyPassword,
+                        keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout);
         }
         // Should never happen!!
         throw new Error();
@@ -925,4 +927,39 @@ static KeyStore buildKeyStore(File certChainFile, File keyFile, char[] keyPasswo
         ks.setKeyEntry("key", key, keyPasswordChars, certChain.toArray(new Certificate[certChain.size()]));
         return ks;
     }
+
+    /**
+     * 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;
+    }
 }


Test patch:
diff --git a/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java
index 4df0fc00a8d..9482f2b4450 100644
--- a/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java
@@ -15,23 +15,17 @@
  */
 package io.netty.handler.ssl;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 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;
@@ -40,94 +34,24 @@
 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 {
+public class JdkSslEngineTest extends SSLEngineTest {
     private static final String PREFERRED_APPLICATION_LEVEL_PROTOCOL = "my-protocol-http2";
     private static final String FALLBACK_APPLICATION_LEVEL_PROTOCOL = "my-protocol-http1_1";
     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<ByteBuf> {
-        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;
-        serverException = null;
-    }
-
     @Test
     public void testNpn() throws Exception {
         try {
@@ -344,57 +268,6 @@ public String select(List<String> protocols) {
         }
     }
 
-    @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);
@@ -465,132 +338,12 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws E
         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<Channel>() {
-            @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<Channel>() {
-            @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(PREFERRED_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<ByteBuf> dataCapture = null;
-        try {
-            sendChannel.writeAndFlush(message);
-            receiverLatch.await(5, TimeUnit.SECONDS);
-            message.resetReaderIndex();
-            ArgumentCaptor<ByteBuf> 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();
-                }
-            }
-        }
+    @Override
+    protected SslProvider sslProvider() {
+        return SslProvider.JDK;
     }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java
new file mode 100644
index 00000000000..0d6a76ca598
--- /dev/null
+++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2015 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;
+
+public class OpenSslEngineTest extends SSLEngineTest {
+    @Override
+    protected SslProvider sslProvider() {
+        return SslProvider.OPENSSL;
+    }
+}
diff --git a/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java
new file mode 100644
index 00000000000..490f3db0c0d
--- /dev/null
+++ b/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2015 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 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.util.NetUtil;
+import io.netty.util.concurrent.Future;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
+import java.io.File;
+import java.net.InetSocketAddress;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.verify;
+
+public abstract class SSLEngineTest {
+
+    @Mock
+    protected MessageReciever serverReceiver;
+    @Mock
+    protected MessageReciever clientReceiver;
+
+    protected Throwable serverException;
+    protected Throwable clientException;
+    protected SslContext serverSslCtx;
+    protected SslContext clientSslCtx;
+    protected ServerBootstrap sb;
+    protected Bootstrap cb;
+    protected Channel serverChannel;
+    protected Channel serverConnectedChannel;
+    protected Channel clientChannel;
+    protected CountDownLatch serverLatch;
+    protected CountDownLatch clientLatch;
+
+    interface MessageReciever {
+        void messageReceived(ByteBuf msg);
+    }
+
+    protected static final class MessageDelegatorChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
+        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;
+        serverException = null;
+    }
+
+    @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 mySetupMutualAuth(File keyFile, File crtFile, String keyPassword)
+            throws SSLException, 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 {
+        serverSslCtx = SslContext.newServerContext(sslProvider(), servertTrustCrtFile, null,
+                                                   serverCrtFile, serverKeyFile, serverKeyPassword, null,
+                                                   null, IdentityCipherSuiteFilter.INSTANCE, null, 0, 0);
+        clientSslCtx = SslContext.newClientContext(sslProvider(), clientTrustCrtFile, null,
+                                                   clientCrtFile, clientKeyFile, clientKeyPassword, null,
+                                                   null, IdentityCipherSuiteFilter.INSTANCE,
+                                                   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<Channel>() {
+            @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<Channel>() {
+            @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 {
+                        cause.printStackTrace();
+                        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();
+    }
+
+    protected 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 static 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<ByteBuf> dataCapture = null;
+        try {
+            sendChannel.writeAndFlush(message);
+            receiverLatch.await(5, TimeUnit.SECONDS);
+            message.resetReaderIndex();
+            ArgumentCaptor<ByteBuf> 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();
+                }
+            }
+        }
+    }
+
+    protected abstract SslProvider sslProvider();
+}