Merge upstream master

Includes a lot of refactorings of the core SSL classes (especially
session code), some bug fixes, and AlgorithmParameters.GCM.  There
shouldn't be any large changes in behavior.

A number of classes have been renamed, but shims are left in place to
handle cases where users are using reflection to access unexposed
APIs (see, eg, OpenSSLSocketImpl.java).


Bug: 62852271
Bug: 62369410
Test: cts -m CtsLibcoreTestCases
Test: cts -m CtsLibcoreOkHttpTestCases
Test: cts -m CtsLibcoreWycheproofConscryptTestCases
Change-Id: Ib6eb0819f2b38d1b8e49b49a0eba769eb8afc4cf
diff --git a/.travis.yml b/.travis.yml
index 60d7d81..4789ae0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -38,7 +38,7 @@
         - $ANDROID_HOME/tools/bin/sdkmanager 'platforms;android-25'
         - $ANDROID_HOME/tools/bin/sdkmanager 'extras;android;m2repository'
         - $ANDROID_HOME/tools/bin/sdkmanager --channel=1 ndk-bundle
-        - $ANDROID_HOME/tools/bin/sdkmanager 'cmake;3.6.3155560'
+        - $ANDROID_HOME/tools/bin/sdkmanager 'cmake;3.6.4111459'
 
       addons:
         apt:
diff --git a/Dockerfile b/Dockerfile
index e5501ef..87a3308 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -21,9 +21,9 @@
 # Install Java 8
 RUN wget -q --no-cookies --no-check-certificate \
     --header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie" \
-    "http://download.oracle.com/otn-pub/java/jdk/8u121-b13/e9e7ea248e2c4826b92b3f075a80e441/jdk-8u121-linux-x64.tar.gz" \
+    "http://download.oracle.com/otn-pub/java/jdk/8u131-b11/d54c1d3a095b4ff2b6607d096fa80163/jdk-8u131-linux-x64.tar.gz" \
     -O - | tar xz -C /var/local
-ENV JAVA_HOME /var/local/jdk1.8.0_121
+ENV JAVA_HOME /var/local/jdk1.8.0_131
 ENV PATH $JAVA_HOME/bin:$PATH
 
 # Install GCC 4.8
diff --git a/README.md b/README.md
index d377821..2dc45c4 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 
 Conscrypt is a Java Security Provider (JSP) that implements parts of the
 Java Cryptography Extension (JCE) and Java Secure Socket Extension (JSSE).
-It uses BoringSSL to provide cryptograhpic primitives and Transport Layer
+It uses BoringSSL to provide cryptographical primitives and Transport Layer
 Security (TLS) for Java applications on Android and OpenJDK.
 
 The core SSL engine has borrowed liberally from the [Netty](http://netty.io/) project and their
@@ -32,14 +32,14 @@
 
 #### Download JARs
 You can download
-[the JARs](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.conscrypt%22%20AND%20v%3A%221.1.0%22)
+[the JARs](http://search.maven.org/#search%7Cga%7C1%7Cg:%22org.conscrypt%22%20AND%20v:%221.1.0%22)
 directly from the Maven repositories.
 
 #### OpenJDK (i.e. non-Android)
 
 ##### Native Classifiers
 
-The OpenJDK artifacts are platform-dependent, since each embeds a native library for a particular
+The OpenJDK artifacts are platform-dependent since each embeds a native library for a particular
 platform. We publish artifacts to Maven Central for the following platforms:
 
 Classifier | Description
@@ -143,10 +143,10 @@
 
 ### OpenJDK
 
-This modules provides the `Platform` class for non-Android (OpenJDK-based) systems. It also provides
+These modules provide the `Platform` class for non-Android (OpenJDK-based) systems. It also provides
 a native library loader supports bundling the shared library with the JAR.
 
 ### Platform
 
 This is not an actual module and is not part of the default build. This is used for building
- Conscrypt as an embedded component of the Android platform.
\ No newline at end of file
+ Conscrypt as an embedded component of the Android platform.
diff --git a/RELEASING.md b/RELEASING.md
index 67ede26..19c4b8c 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -188,10 +188,18 @@
 The following command will build the whole project and upload it to Maven
 Central. Parallel building [is not safe during
 uploadArchives](https://issues.gradle.org/browse/GRADLE-3420).
+
+**Linux/Mac:**
 ```bash
 conscrypt$ ./gradlew build && ./gradlew -Dorg.gradle.parallel=false uploadArchives
 ```
 
+**Windows:**
+```bat
+C:\conscrypt>gradlew build && gradlew -Dorg.gradle.parallel=false uploadArchives
+
+```
+
 If the version has the `-SNAPSHOT` suffix, the artifacts will automatically
 go to the snapshot repository. Otherwise it's a release deployment and the
 artifacts will go to a freshly created staging repository.
@@ -201,18 +209,23 @@
 it and the architecture of your JVM. For a fully fledged deployment, you will
 need to deploy for each supported OS/architecture.
 
-To deploy the codegen for an OS and architecture, you must run the following
-commands on that OS and specify the architecture by the flag `-PtargetArch=<arch>`.
-
 When deploying a Release, the first deployment will create
 [a new staging repository](https://oss.sonatype.org/#stagingRepositories). You'll need
 to look up the ID in the OSSRH UI (usually in the form of `orgconscrypt-*`). Codegen
 deployment commands should include `-PrepositoryId=<repository-id>` in order to
 ensure that the artifacts are pushed to the same staging repository.
 
+**Linux/Mac:**
 ```bash
-conscrypt$ ./gradlew build conscrypt-openjdk:uploadArchives \
-    -Dorg.gradle.parallel=false -PrepositoryId=<repository-id>
+conscrypt$ ./gradlew build && ./gradlew -Dorg.gradle.parallel=false \
+    conscrypt-openjdk:uploadArchives -PrepositoryId=<repository-id>
+```
+
+**Windows:**
+```bat
+C:\conscrypt>gradlew build && gradlew -Dorg.gradle.parallel=false ^
+             conscrypt-openjdk:uploadArchives -PrepositoryId=<repository-id>
+
 ```
 
 Now finish [Releasing on Maven Central](#releasing-on-maven-central).
@@ -221,6 +234,7 @@
 Once all of the native JARs appear on Maven Central, you can build and deploy
 the Uber JAR that contains all of them.
 
+**Linux/Mac:**
 ```bash
 conscrypt$ ./gradlew conscrypt-openjdk-uber:build \
            -Dorg.conscrypt.openjdk.buildUberJar=true
@@ -230,6 +244,17 @@
            -Dorg.conscrypt.openjdk.buildUberJar=true
 ```
 
+**Windows:**
+```bat
+C:\conscrypt>gradlew conscrypt-openjdk-uber:build ^
+             -Dorg.conscrypt.openjdk.buildUberJar=true
+
+C:\conscrypt>gradlew conscrypt-openjdk-uber:uploadArchives ^
+             -Dorg.gradle.parallel=false ^
+             -Dorg.conscrypt.openjdk.buildUberJar=true
+
+```
+
 This will create
 [a new staging repository](https://oss.sonatype.org/#stagingRepositories),
 so you'll need to [close and release](#releasing-on-maven-central) the
diff --git a/android-stub/src/main/java/com/android/org/conscrypt/NativeCrypto.java b/android-stub/src/main/java/com/android/org/conscrypt/NativeCrypto.java
index 831dd17..851e27b 100644
--- a/android-stub/src/main/java/com/android/org/conscrypt/NativeCrypto.java
+++ b/android-stub/src/main/java/com/android/org/conscrypt/NativeCrypto.java
@@ -20,6 +20,7 @@
 import java.security.cert.CertificateException;
 import javax.net.ssl.SSLException;
 
+@SuppressWarnings("unused")
 final class NativeCrypto {
     public interface SSLHandshakeCallbacks {
         /**
diff --git a/android-stub/src/main/java/com/android/org/conscrypt/SSLParametersImpl.java b/android-stub/src/main/java/com/android/org/conscrypt/SSLParametersImpl.java
index 079f89f..651d431 100644
--- a/android-stub/src/main/java/com/android/org/conscrypt/SSLParametersImpl.java
+++ b/android-stub/src/main/java/com/android/org/conscrypt/SSLParametersImpl.java
@@ -16,7 +16,7 @@
 
 package com.android.org.conscrypt;
 
-class SSLParametersImpl {
+final class SSLParametersImpl {
     public static SSLParametersImpl getDefault() {
         throw new RuntimeException("Stub!");
     }
diff --git a/android/build.gradle b/android/build.gradle
index d764845..de33f02 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -100,7 +100,12 @@
         compile fileTree(dir: 'libs', include: ['*.jar'])
         publicApiDocs project(':conscrypt-api-doclet')
         androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
-            exclude group: 'com.android.support', module: 'support-annotations'
+            exclude module: 'support-annotations'
+            exclude module: 'support-v4'
+            exclude module: 'support-v13'
+            exclude module: 'recyclerview-v7'
+            exclude module: 'appcompat-v7'
+            exclude module: 'design'
         })
         provided project(':conscrypt-android-stub')
 
diff --git a/android/lint.xml b/android/lint.xml
index d899843..0f057a9 100644
--- a/android/lint.xml
+++ b/android/lint.xml
@@ -2,7 +2,7 @@
 <lint>
     <!-- ExtendedSSLSession only gets instantiated in new APIs on Android. -->
     <issue id="NewApi">
-        <ignore path="**/org/conscrypt/OpenSSLExtendedSessionImpl.java" />
+        <ignore path="**/org/conscrypt/DelegatingExtendedSSLSession.java" />
     </issue>
 
     <!-- Android SparseArrays can't be used in common directory. -->
diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro
index c3bdd2c..3bc75b2 100644
--- a/android/proguard-rules.pro
+++ b/android/proguard-rules.pro
@@ -20,5 +20,7 @@
 -dontwarn dalvik.system.BlockGuard
 -dontwarn dalvik.system.BlockGuard$Policy
 -dontwarn dalvik.system.CloseGuard
+-dontwarn com.android.org.conscrypt.AbstractConscryptSocket
+-dontwarn com.android.org.conscrypt.ConscryptFileDescriptorSocket
 -dontwarn com.android.org.conscrypt.OpenSSLSocketImpl
 -dontwarn org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl
diff --git a/android/src/main/java/org/conscrypt/KitKatPlatformOpenSSLSocketImplAdapter.java b/android/src/main/java/org/conscrypt/KitKatPlatformOpenSSLSocketImplAdapter.java
index 556b815..db7dc85 100644
--- a/android/src/main/java/org/conscrypt/KitKatPlatformOpenSSLSocketImplAdapter.java
+++ b/android/src/main/java/org/conscrypt/KitKatPlatformOpenSSLSocketImplAdapter.java
@@ -45,9 +45,9 @@
         extends com.android.org.conscrypt.OpenSSLSocketImpl {
 
 
-    private final org.conscrypt.OpenSSLSocketImpl delegate;
+    private final AbstractConscryptSocket delegate;
 
-    public KitKatPlatformOpenSSLSocketImplAdapter(org.conscrypt.OpenSSLSocketImpl delegate)
+    public KitKatPlatformOpenSSLSocketImplAdapter(AbstractConscryptSocket delegate)
             throws IOException {
         super(null);
         this.delegate = delegate;
diff --git a/android/src/main/java/org/conscrypt/Platform.java b/android/src/main/java/org/conscrypt/Platform.java
index fa4a83a..32b4b6d 100644
--- a/android/src/main/java/org/conscrypt/Platform.java
+++ b/android/src/main/java/org/conscrypt/Platform.java
@@ -82,9 +82,8 @@
         }
     }
 
-    public static FileDescriptor getFileDescriptorFromSSLSocket(
-            OpenSSLSocketImpl openSSLSocketImpl) {
-        return getFileDescriptor(openSSLSocketImpl);
+    public static FileDescriptor getFileDescriptorFromSSLSocket(AbstractConscryptSocket socket) {
+        return getFileDescriptor(socket);
     }
 
     public static String getCurveName(ECParameterSpec spec) {
@@ -195,7 +194,7 @@
     }
 
     public static void setSSLParameters(
-            SSLParameters params, SSLParametersImpl impl, OpenSSLSocketImpl socket) {
+            SSLParameters params, SSLParametersImpl impl, AbstractConscryptSocket socket) {
         try {
             setSSLParametersOnImpl(params, impl);
 
@@ -213,14 +212,14 @@
     }
 
     public static void setSSLParameters(
-            SSLParameters params, SSLParametersImpl impl, OpenSSLEngineImpl engine) {
+            SSLParameters params, SSLParametersImpl impl, ConscryptEngine engine) {
         try {
             setSSLParametersOnImpl(params, impl);
 
             if (Build.VERSION.SDK_INT >= 24) {
                 String sniHostname = getSniHostnameFromParams(params);
                 if (sniHostname != null) {
-                    engine.setSniHostname(sniHostname);
+                    engine.setHostname(sniHostname);
                 }
             }
         } catch (NoSuchMethodException ignored) {
@@ -260,7 +259,7 @@
     }
 
     public static void getSSLParameters(
-            SSLParameters params, SSLParametersImpl impl, OpenSSLSocketImpl socket) {
+            SSLParameters params, SSLParametersImpl impl, AbstractConscryptSocket socket) {
         try {
             getSSLParametersFromImpl(params, impl);
 
@@ -276,17 +275,18 @@
 
     @TargetApi(24)
     private static void setParametersSniHostname(
-            SSLParameters params, SSLParametersImpl impl, OpenSSLSocketImpl socket)
+            SSLParameters params, SSLParametersImpl impl, AbstractConscryptSocket socket)
             throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
         if (impl.getUseSni() && AddressUtils.isValidSniHostname(socket.getHostname())) {
             Method m_setServerNames = params.getClass().getMethod("setServerNames", List.class);
-            m_setServerNames.invoke(params, Collections.<SNIServerName>singletonList(
-                                                    new SNIHostName(socket.getHostname())));
+            m_setServerNames.invoke(params,
+                    Collections.<SNIServerName>singletonList(
+                            new SNIHostName(socket.getHostname())));
         }
     }
 
     public static void getSSLParameters(
-            SSLParameters params, SSLParametersImpl impl, OpenSSLEngineImpl engine) {
+            SSLParameters params, SSLParametersImpl impl, ConscryptEngine engine) {
         try {
             getSSLParametersFromImpl(params, impl);
 
@@ -302,13 +302,13 @@
 
     @TargetApi(24)
     private static void setParametersSniHostname(
-            SSLParameters params, SSLParametersImpl impl, OpenSSLEngineImpl engine)
+            SSLParameters params, SSLParametersImpl impl, ConscryptEngine engine)
             throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
-        if (impl.getUseSni() && AddressUtils.isValidSniHostname(engine.getSniHostname())) {
+        if (impl.getUseSni() && AddressUtils.isValidSniHostname(engine.getHostname())) {
             Method m_setServerNames = params.getClass().getMethod("setServerNames", List.class);
             m_setServerNames.invoke(params,
                     Collections.<SNIServerName>singletonList(
-                            new SNIHostName(engine.getSniHostname())));
+                            new SNIHostName(engine.getHostname())));
         }
     }
 
@@ -359,9 +359,9 @@
         return false;
     }
 
-    @SuppressLint("NewApi") // OpenSSLSocketImpl defines getHandshakeSession()
+    @SuppressLint("NewApi") // AbstractConscryptSocket defines getHandshakeSession()
     public static void checkClientTrusted(X509TrustManager tm, X509Certificate[] chain,
-            String authType, OpenSSLSocketImpl socket) throws CertificateException {
+            String authType, AbstractConscryptSocket socket) throws CertificateException {
         if (!checkTrusted("checkClientTrusted", tm, chain, authType, Socket.class, socket)
                 && !checkTrusted("checkClientTrusted", tm, chain, authType, String.class,
                            socket.getHandshakeSession().getPeerHost())) {
@@ -369,9 +369,9 @@
         }
     }
 
-    @SuppressLint("NewApi") // OpenSSLSocketImpl defines getHandshakeSession()
+    @SuppressLint("NewApi") // AbstractConscryptSocket defines getHandshakeSession()
     public static void checkServerTrusted(X509TrustManager tm, X509Certificate[] chain,
-            String authType, OpenSSLSocketImpl socket) throws CertificateException {
+            String authType, AbstractConscryptSocket socket) throws CertificateException {
         if (!checkTrusted("checkServerTrusted", tm, chain, authType, Socket.class, socket)
                 && !checkTrusted("checkServerTrusted", tm, chain, authType, String.class,
                            socket.getHandshakeSession().getPeerHost())) {
@@ -379,9 +379,9 @@
         }
     }
 
-    @SuppressLint("NewApi") // OpenSSLSocketImpl defines getHandshakeSession()
+    @SuppressLint("NewApi") // AbstractConscryptSocket defines getHandshakeSession()
     public static void checkClientTrusted(X509TrustManager tm, X509Certificate[] chain,
-            String authType, OpenSSLEngineImpl engine) throws CertificateException {
+            String authType, ConscryptEngine engine) throws CertificateException {
         if (!checkTrusted("checkClientTrusted", tm, chain, authType, SSLEngine.class, engine)
                 && !checkTrusted("checkClientTrusted", tm, chain, authType, String.class,
                            engine.getHandshakeSession().getPeerHost())) {
@@ -389,9 +389,9 @@
         }
     }
 
-    @SuppressLint("NewApi") // OpenSSLSocketImpl defines getHandshakeSession()
+    @SuppressLint("NewApi") // AbstractConscryptSocket defines getHandshakeSession()
     public static void checkServerTrusted(X509TrustManager tm, X509Certificate[] chain,
-            String authType, OpenSSLEngineImpl engine) throws CertificateException {
+            String authType, ConscryptEngine engine) throws CertificateException {
         if (!checkTrusted("checkServerTrusted", tm, chain, authType, SSLEngine.class, engine)
                 && !checkTrusted("checkServerTrusted", tm, chain, authType, String.class,
                            engine.getHandshakeSession().getPeerHost())) {
@@ -430,7 +430,8 @@
             // provider, which should be the default. That could happen if an
             // OEM decided
             // to implement a different default provider. Also highly unlikely.
-            Log.e(TAG, "Private key is not an OpenSSLRSAPrivateKey instance, its class name is:"
+            Log.e(TAG,
+                    "Private key is not an OpenSSLRSAPrivateKey instance, its class name is:"
                             + javaKey.getClass().getCanonicalName());
             return null;
         }
@@ -695,12 +696,12 @@
      * Pre-Java 8 backward compatibility.
      */
 
-    public static SSLSession wrapSSLSession(AbstractOpenSSLSession sslSession) {
+    public static SSLSession wrapSSLSession(ActiveSession sslSession) {
         if (Build.VERSION.SDK_INT <= 23) {
             return sslSession;
         }
 
-        return new OpenSSLExtendedSessionImpl(sslSession);
+        return new DelegatingExtendedSSLSession(sslSession);
     }
 
     public static SSLSession unwrapSSLSession(SSLSession sslSession) {
@@ -708,9 +709,10 @@
             return sslSession;
         }
 
-        if (sslSession instanceof OpenSSLExtendedSessionImpl) {
-            return ((OpenSSLExtendedSessionImpl) sslSession).getDelegate();
+        if (sslSession instanceof DelegatingExtendedSSLSession) {
+            return ((DelegatingExtendedSSLSession) sslSession).getDelegate();
         }
+
         return sslSession;
     }
 
diff --git a/android/src/main/java/org/conscrypt/PreKitKatPlatformOpenSSLSocketImplAdapter.java b/android/src/main/java/org/conscrypt/PreKitKatPlatformOpenSSLSocketImplAdapter.java
index 1a89208..9353a91 100644
--- a/android/src/main/java/org/conscrypt/PreKitKatPlatformOpenSSLSocketImplAdapter.java
+++ b/android/src/main/java/org/conscrypt/PreKitKatPlatformOpenSSLSocketImplAdapter.java
@@ -45,9 +45,9 @@
         extends org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl {
 
 
-    private final org.conscrypt.OpenSSLSocketImpl delegate;
+    private final AbstractConscryptSocket delegate;
 
-    public PreKitKatPlatformOpenSSLSocketImplAdapter(org.conscrypt.OpenSSLSocketImpl delegate)
+    public PreKitKatPlatformOpenSSLSocketImplAdapter(AbstractConscryptSocket delegate)
             throws IOException {
         super(null);
         this.delegate = delegate;
diff --git a/api-doclet/build.gradle b/api-doclet/build.gradle
index 235040b..74f015a 100644
--- a/api-doclet/build.gradle
+++ b/api-doclet/build.gradle
@@ -3,3 +3,7 @@
 dependencies {
     compile fileTree(dir: "${System.properties['java.home']}/../lib", include: '*tools.jar')
 }
+
+// Don't include this artifact in the distribution.
+tasks.install.enabled = false
+tasks.uploadArchives.enabled = false;
diff --git a/appveyor.yml b/appveyor.yml
index 3f8cbf5..7cb3de1 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -45,6 +45,9 @@
   # Install yasm
   - choco install -y yasm
 
+  # Install Go for BoringSSL compile (embedding test data)
+  - choco install -y golang
+
   # Clone BoringSSL
   - git clone --depth 1 https://boringssl.googlesource.com/boringssl.git "%BORINGSSL_HOME%"
 
@@ -86,7 +89,7 @@
   - '%ANDROID_HOME%\tools\bin\sdkmanager.bat platforms;android-25'
   - '%ANDROID_HOME%\tools\bin\sdkmanager.bat extras;android;m2repository'
   - '%ANDROID_HOME%\tools\bin\sdkmanager.bat --channel=1 ndk-bundle'
-  - '%ANDROID_HOME%\tools\bin\sdkmanager.bat cmake;3.6.3155560'
+  - '%ANDROID_HOME%\tools\bin\sdkmanager.bat cmake;3.6.4111459'
 
 build_script:
   - gradlew.bat assemble
diff --git a/benchmark-base/build.gradle b/benchmark-base/build.gradle
new file mode 100644
index 0000000..57eb24f
--- /dev/null
+++ b/benchmark-base/build.gradle
@@ -0,0 +1,32 @@
+description = 'Conscrypt: Benchmarks Base'
+
+evaluationDependsOn(':conscrypt-openjdk')
+
+ext {
+    preferredNativeConfiguration = project(':conscrypt-openjdk').preferredNativeConfiguration
+    preferredNativeFileDir = project(':conscrypt-openjdk').preferredNativeFileDir
+}
+
+sourceSets {
+    main {
+        resources {
+            // This shouldn't be needed but seems to help IntelliJ locate the native artifact.
+            srcDirs += preferredNativeFileDir
+        }
+    }
+}
+
+dependencies {
+    compile project(':conscrypt-openjdk'),
+            project(':conscrypt-testing'),
+            libraries.junit,
+            libraries.netty_handler,
+            libraries.netty_tcnative
+
+    // Add the preferred native openjdk configuration for this platform.
+    compile project(path: ':conscrypt-openjdk', configuration: "$preferredNativeConfiguration")
+}
+
+// Don't include this artifact in the distribution.
+tasks.install.enabled = false
+tasks.uploadArchives.enabled = false;
diff --git a/benchmark-base/src/main/java/org/conscrypt/BufferType.java b/benchmark-base/src/main/java/org/conscrypt/BufferType.java
new file mode 100644
index 0000000..a20bd01
--- /dev/null
+++ b/benchmark-base/src/main/java/org/conscrypt/BufferType.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.conscrypt;
+
+import java.nio.ByteBuffer;
+import javax.net.ssl.SSLEngine;
+
+/**
+ * Enumeration that provides allocation of direct or heap buffers.
+ */
+@SuppressWarnings("unused")
+public enum BufferType {
+    HEAP {
+        @Override
+        ByteBuffer newBuffer(int size) {
+            return ByteBuffer.allocate(size);
+        }
+    },
+    DIRECT {
+        @Override
+        ByteBuffer newBuffer(int size) {
+            return ByteBuffer.allocateDirect(size);
+        }
+    };
+
+    abstract ByteBuffer newBuffer(int size);
+
+    ByteBuffer newApplicationBuffer(SSLEngine engine) {
+        return newBuffer(engine.getSession().getApplicationBufferSize());
+    }
+
+    ByteBuffer newPacketBuffer(SSLEngine engine) {
+        return newBuffer(engine.getSession().getPacketBufferSize());
+    }
+}
diff --git a/testing/src/main/java/org/conscrypt/TestClient.java b/benchmark-base/src/main/java/org/conscrypt/ClientEndpoint.java
similarity index 76%
rename from testing/src/main/java/org/conscrypt/TestClient.java
rename to benchmark-base/src/main/java/org/conscrypt/ClientEndpoint.java
index 6ef37f9..3b75908 100644
--- a/testing/src/main/java/org/conscrypt/TestClient.java
+++ b/benchmark-base/src/main/java/org/conscrypt/ClientEndpoint.java
@@ -19,32 +19,39 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.InetAddress;
 import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
 
 /**
  * Client-side endpoint. Provides basic services for sending/receiving messages from the client
  * socket.
  */
-public final class TestClient {
+final class ClientEndpoint {
     private final SSLSocket socket;
     private InputStream input;
     private OutputStream output;
 
-    public TestClient(SSLSocket socket) {
-        this.socket = socket;
+    ClientEndpoint(SSLSocketFactory socketFactory, ChannelType channelType, int port,
+            String[] protocols, String[] ciphers) throws IOException {
+        socket = channelType.newClientSocket(
+                socketFactory, InetAddress.getLoopbackAddress(), port);
+        socket.setEnabledProtocols(protocols);
+        socket.setEnabledCipherSuites(ciphers);
     }
 
-    public void start() {
+    void start() {
         try {
             socket.startHandshake();
             input = socket.getInputStream();
             output = socket.getOutputStream();
         } catch (IOException e) {
+            e.printStackTrace();
             throw new RuntimeException(e);
         }
     }
 
-    public void stop() {
+    void stop() {
         try {
             socket.close();
         } catch (IOException e) {
@@ -52,7 +59,7 @@
         }
     }
 
-    public int readMessage(byte[] buffer) {
+    int readMessage(byte[] buffer) {
         try {
             int totalBytesRead = 0;
             while (totalBytesRead < buffer.length) {
@@ -69,7 +76,7 @@
         }
     }
 
-    public void sendMessage(byte[] data) {
+    void sendMessage(byte[] data) {
         try {
             output.write(data);
         } catch (IOException e) {
@@ -77,7 +84,7 @@
         }
     }
 
-    public void flush() {
+    void flush() {
         try {
             output.flush();
         } catch (IOException e) {
diff --git a/benchmark-base/src/main/java/org/conscrypt/ClientSocketBenchmark.java b/benchmark-base/src/main/java/org/conscrypt/ClientSocketBenchmark.java
new file mode 100644
index 0000000..14bf98a
--- /dev/null
+++ b/benchmark-base/src/main/java/org/conscrypt/ClientSocketBenchmark.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import static org.conscrypt.TestUtils.getProtocols;
+import static org.conscrypt.TestUtils.newTextMessage;
+
+import java.io.OutputStream;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Benchmark for comparing performance of client socket implementations.
+ */
+public final class ClientSocketBenchmark {
+    /**
+     * Provider for the benchmark configuration
+     */
+    interface Config {
+        SocketType socketType();
+        int messageSize();
+        String cipher();
+        ChannelType channelType();
+    }
+
+    private ClientEndpoint client;
+    private ServerEndpoint server;
+    private byte[] message;
+    private ExecutorService executor;
+    private Future<?> sendingFuture;
+    private volatile boolean stopping;
+
+    private static final AtomicLong bytesCounter = new AtomicLong();
+    private AtomicBoolean recording = new AtomicBoolean();
+
+    ClientSocketBenchmark(Config config) throws Exception {
+        recording.set(false);
+
+        message = newTextMessage(config.messageSize());
+
+        // Always use the same server for consistency across the benchmarks.
+        server = SocketType.CONSCRYPT_ENGINE.newServer(
+                ChannelType.CHANNEL, config.messageSize(), getProtocols(), ciphers(config));
+
+        server.setMessageProcessor(new ServerEndpoint.MessageProcessor() {
+            @Override
+            public void processMessage(byte[] inMessage, int numBytes, OutputStream os) {
+                if (recording.get()) {
+                    // Server received a message, increment the count.
+                    bytesCounter.addAndGet(numBytes);
+                }
+            }
+        });
+        Future<?> connectedFuture = server.start();
+
+        client = config.socketType().newClient(
+            config.channelType(), server.port(), getProtocols(), ciphers(config));
+        client.start();
+
+        // Wait for the initial connection to complete.
+        connectedFuture.get(5, TimeUnit.SECONDS);
+
+        executor = Executors.newSingleThreadExecutor();
+        sendingFuture = executor.submit(new Runnable() {
+            @Override
+            public void run() {
+                Thread thread = Thread.currentThread();
+                while (!stopping && !thread.isInterrupted()) {
+                    client.sendMessage(message);
+                }
+            }
+        });
+    }
+
+    void close() throws Exception {
+        stopping = true;
+        client.stop();
+        server.stop();
+        executor.shutdown();
+        executor.awaitTermination(5, TimeUnit.SECONDS);
+        sendingFuture.get(5, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Simple benchmark for throughput.
+     */
+    void throughput() throws Exception {
+        recording.set(true);
+        // Send as many messages as we can in a second.
+        Thread.sleep(1001);
+        recording.set(false);
+    }
+
+    static void reset() {
+        bytesCounter.set(0);
+    }
+
+    static long bytesPerSecond() {
+        return bytesCounter.get();
+    }
+
+    private String[] ciphers(Config config) {
+        return new String[] {config.cipher()};
+    }
+
+    /**
+     * A simple main for profiling.
+     */
+    public static void main(String[] args) throws Exception {
+        ClientSocketBenchmark bm = new ClientSocketBenchmark(new Config() {
+            @Override
+            public SocketType socketType() {
+                return SocketType.CONSCRYPT_ENGINE;
+            }
+
+            @Override
+            public int messageSize() {
+                return 512;
+            }
+
+            @Override
+            public String cipher() {
+                return TestUtils.TEST_CIPHER;
+            }
+
+            @Override
+            public ChannelType channelType() {
+                return ChannelType.CHANNEL;
+            }
+        });
+
+        // Just run forever for profiling.
+        while (true) {
+            bm.throughput();
+        }
+    }
+}
diff --git a/benchmark-base/src/main/java/org/conscrypt/EngineHandshakeBenchmark.java b/benchmark-base/src/main/java/org/conscrypt/EngineHandshakeBenchmark.java
new file mode 100644
index 0000000..c5c566e
--- /dev/null
+++ b/benchmark-base/src/main/java/org/conscrypt/EngineHandshakeBenchmark.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed 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.
+ */
+
+/*
+ * Copyright 2017 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 org.conscrypt;
+
+import static org.conscrypt.TestUtils.doEngineHandshake;
+
+import java.nio.ByteBuffer;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+
+/**
+ * Benchmark comparing handshake performance of various engine implementations to conscrypt.
+ */
+public final class EngineHandshakeBenchmark {
+    /**
+     * Provider for the benchmark configuration
+     */
+    interface Config {
+        BufferType bufferType();
+        EngineType engineType();
+        String cipher();
+    }
+
+    private final EngineType engineType;
+    private final String cipher;
+
+    private final ByteBuffer clientApplicationBuffer;
+    private final ByteBuffer clientPacketBuffer;
+    private final ByteBuffer serverApplicationBuffer;
+    private final ByteBuffer serverPacketBuffer;
+
+    EngineHandshakeBenchmark(Config config) throws Exception {
+        engineType = config.engineType();
+        cipher = config.cipher();
+        BufferType bufferType = config.bufferType();
+
+        SSLEngine clientEngine = engineType.newClientEngine(cipher);
+        SSLEngine serverEngine = engineType.newServerEngine(cipher);
+
+        // Create the application and packet buffers for both endpoints.
+        clientApplicationBuffer = bufferType.newApplicationBuffer(clientEngine);
+        serverApplicationBuffer = bufferType.newApplicationBuffer(serverEngine);
+        clientPacketBuffer = bufferType.newPacketBuffer(clientEngine);
+        serverPacketBuffer = bufferType.newPacketBuffer(serverEngine);
+
+        engineType.dispose(clientEngine);
+        engineType.dispose(serverEngine);
+    }
+
+    void handshake() throws SSLException {
+        SSLEngine client = engineType.newClientEngine(cipher);
+        SSLEngine server = engineType.newServerEngine(cipher);
+        clientApplicationBuffer.clear();
+        clientPacketBuffer.clear();
+        serverApplicationBuffer.clear();
+        serverPacketBuffer.clear();
+
+        doEngineHandshake(client, server, clientApplicationBuffer, clientPacketBuffer,
+                serverApplicationBuffer, serverPacketBuffer);
+        engineType.dispose(client);
+        engineType.dispose(server);
+    }
+
+    /**
+     * A simple main for profiling.
+     */
+    public static void main(String[] args) throws Exception {
+        EngineHandshakeBenchmark bm = new EngineHandshakeBenchmark(new Config() {
+            @Override
+            public BufferType bufferType() {
+                return BufferType.HEAP;
+            }
+
+            @Override
+            public EngineType engineType() {
+                return EngineType.NETTY;
+            }
+
+            @Override
+            public String cipher() {
+                return TestUtils.TEST_CIPHER;
+            }
+        });
+
+        // Just run forever for profiling.
+        while (true) {
+            bm.handshake();
+        }
+    }
+}
diff --git a/benchmark-base/src/main/java/org/conscrypt/EngineType.java b/benchmark-base/src/main/java/org/conscrypt/EngineType.java
new file mode 100644
index 0000000..8d3698f
--- /dev/null
+++ b/benchmark-base/src/main/java/org/conscrypt/EngineType.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.conscrypt;
+
+import static org.conscrypt.TestUtils.PROTOCOL_TLS_V1_2;
+import static org.conscrypt.TestUtils.initClientSslContext;
+import static org.conscrypt.TestUtils.initEngine;
+import static org.conscrypt.TestUtils.initServerSslContext;
+
+import io.netty.buffer.PooledByteBufAllocator;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.util.ReferenceCountUtil;
+import java.security.KeyStore.PrivateKeyEntry;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+import libcore.java.security.TestKeyStore;
+
+/**
+ * Enumeration of various types of engines for use with engine-based benchmarks.
+ */
+@SuppressWarnings({"ImmutableEnumChecker", "unused"})
+public enum EngineType {
+    JDK {
+        private final SSLContext clientContext = initClientSslContext(newContext());
+        private final SSLContext serverContext = initServerSslContext(newContext());
+
+        @Override
+        SSLEngine newClientEngine(String cipher) {
+            return initEngine(clientContext.createSSLEngine(), cipher, true);
+        }
+
+        @Override
+        SSLEngine newServerEngine(String cipher) {
+            return initEngine(serverContext.createSSLEngine(), cipher, false);
+        }
+
+        private SSLContext newContext() {
+            try {
+                return SSLContext.getInstance(PROTOCOL_TLS_V1_2);
+            } catch (NoSuchAlgorithmException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    },
+    CONSCRYPT_UNPOOLED {
+        private final SSLContext clientContext = initClientSslContext(newContext());
+        private final SSLContext serverContext = initServerSslContext(newContext());
+
+        @Override
+        SSLEngine newClientEngine(String cipher) {
+            return initEngine(clientContext.createSSLEngine(), cipher, true);
+        }
+
+        @Override
+        SSLEngine newServerEngine(String cipher) {
+            return initEngine(serverContext.createSSLEngine(), cipher, false);
+        }
+
+        private SSLContext newContext() {
+            try {
+                return SSLContext.getInstance(PROTOCOL_TLS_V1_2, new OpenSSLProvider());
+            } catch (NoSuchAlgorithmException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    },
+    CONSCRYPT_POOLED {
+        private final SSLContext clientContext = initClientSslContext(newContext());
+        private final SSLContext serverContext = initServerSslContext(newContext());
+
+        @Override
+        SSLEngine newClientEngine(String cipher) {
+            SSLEngine engine = initEngine(clientContext.createSSLEngine(), cipher, true);
+            Conscrypt.Engines.setBufferAllocator(engine, PooledAllocator.getInstance());
+            return engine;
+        }
+
+        @Override
+        SSLEngine newServerEngine(String cipher) {
+            SSLEngine engine = initEngine(serverContext.createSSLEngine(), cipher, false);
+            Conscrypt.Engines.setBufferAllocator(engine, PooledAllocator.getInstance());
+            return engine;
+        }
+
+        private SSLContext newContext() {
+            try {
+                return SSLContext.getInstance(PROTOCOL_TLS_V1_2, new OpenSSLProvider());
+            } catch (NoSuchAlgorithmException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    },
+    NETTY {
+        private final SslContext clientContext =
+                newNettyClientContext(io.netty.handler.ssl.SslProvider.OPENSSL);
+        private final SslContext serverContext =
+                newNettyServerContext(io.netty.handler.ssl.SslProvider.OPENSSL);
+
+        @Override
+        SSLEngine newClientEngine(String cipher) {
+            return initEngine(
+                    clientContext.newEngine(PooledByteBufAllocator.DEFAULT), cipher, true);
+        }
+
+        @Override
+        SSLEngine newServerEngine(String cipher) {
+            return initEngine(
+                    serverContext.newEngine(PooledByteBufAllocator.DEFAULT), cipher, false);
+        }
+    },
+    NETTY_REF_CNT {
+        private final SslContext clientContext =
+                newNettyClientContext(io.netty.handler.ssl.SslProvider.OPENSSL_REFCNT);
+        private final SslContext serverContext =
+                newNettyServerContext(io.netty.handler.ssl.SslProvider.OPENSSL_REFCNT);
+
+        @Override
+        SSLEngine newClientEngine(String cipher) {
+            return initEngine(
+                    clientContext.newEngine(PooledByteBufAllocator.DEFAULT), cipher, true);
+        }
+
+        @Override
+        SSLEngine newServerEngine(String cipher) {
+            return initEngine(
+                    serverContext.newEngine(PooledByteBufAllocator.DEFAULT), cipher, false);
+        }
+
+        @Override
+        void dispose(SSLEngine engine) {
+            ReferenceCountUtil.release(engine);
+        }
+    };
+
+    abstract SSLEngine newClientEngine(String cipher);
+
+    abstract SSLEngine newServerEngine(String cipher);
+
+    void dispose(SSLEngine engine) {}
+
+    private static SslContext newNettyClientContext(io.netty.handler.ssl.SslProvider sslProvider) {
+        try {
+            TestKeyStore server = TestKeyStore.getServer();
+            SslContextBuilder ctx =
+                    SslContextBuilder.forClient()
+                            .sslProvider(sslProvider)
+                            .trustManager((X509Certificate[]) server.getPrivateKey("RSA", "RSA")
+                                                  .getCertificateChain());
+            return ctx.build();
+        } catch (SSLException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static SslContext newNettyServerContext(io.netty.handler.ssl.SslProvider sslProvider) {
+        try {
+            PrivateKeyEntry server = TestKeyStore.getServer().getPrivateKey("RSA", "RSA");
+            SslContextBuilder ctx =
+                    SslContextBuilder
+                            .forServer(server.getPrivateKey(),
+                                    (X509Certificate[]) server.getCertificateChain())
+                            .sslProvider(sslProvider);
+            return ctx.build();
+        } catch (SSLException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/benchmark-base/src/main/java/org/conscrypt/EngineWrapBenchmark.java b/benchmark-base/src/main/java/org/conscrypt/EngineWrapBenchmark.java
new file mode 100644
index 0000000..f77bc25
--- /dev/null
+++ b/benchmark-base/src/main/java/org/conscrypt/EngineWrapBenchmark.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed 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.
+ */
+
+/*
+ * Copyright 2017 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 org.conscrypt;
+
+import static org.conscrypt.TestUtils.doEngineHandshake;
+import static org.conscrypt.TestUtils.newTextMessage;
+import static org.junit.Assert.assertEquals;
+
+import java.nio.ByteBuffer;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLException;
+
+/**
+ * Benchmark comparing performance of various engine implementations to conscrypt.
+ */
+public final class EngineWrapBenchmark {
+    /**
+     * Provider for the benchmark configuration
+     */
+    interface Config {
+        BufferType bufferType();
+        EngineType engineType();
+        int messageSize();
+        String cipher();
+    }
+
+    private final EngineType engineType;
+    private final String cipher;
+    private final SSLEngine clientEngine;
+    private final SSLEngine serverEngine;
+
+    private final ByteBuffer messageBuffer;
+    private final ByteBuffer clientApplicationBuffer;
+    private final ByteBuffer clientPacketBuffer;
+    private final ByteBuffer serverApplicationBuffer;
+    private final ByteBuffer serverPacketBuffer;
+    private final ByteBuffer preEncryptedBuffer;
+
+    EngineWrapBenchmark(Config config) throws Exception {
+        engineType = config.engineType();
+        cipher = config.cipher();
+        BufferType bufferType = config.bufferType();
+
+        clientEngine = engineType.newClientEngine(cipher);
+        serverEngine = engineType.newServerEngine(cipher);
+
+        // Create the application and packet buffers for both endpoints.
+        clientApplicationBuffer = bufferType.newApplicationBuffer(clientEngine);
+        serverApplicationBuffer = bufferType.newApplicationBuffer(serverEngine);
+        clientPacketBuffer = bufferType.newPacketBuffer(clientEngine);
+        serverPacketBuffer = bufferType.newPacketBuffer(serverEngine);
+
+        // Generate the message to be sent from the client.
+        int messageSize = config.messageSize();
+        messageBuffer = bufferType.newBuffer(messageSize);
+        messageBuffer.put(newTextMessage(messageSize));
+        messageBuffer.flip();
+
+        // Complete the initial TLS handshake.
+        doEngineHandshake(clientEngine, serverEngine, clientApplicationBuffer, clientPacketBuffer,
+                serverApplicationBuffer, serverPacketBuffer);
+
+        // Populate the pre-encrypted buffer for use with the unwrap benchmark.
+        preEncryptedBuffer = bufferType.newBuffer(clientEngine.getSession().getPacketBufferSize());
+        doWrap(messageBuffer, preEncryptedBuffer);
+        doUnwrap(preEncryptedBuffer, serverApplicationBuffer);
+    }
+
+    void teardown() {
+        engineType.dispose(clientEngine);
+        engineType.dispose(serverEngine);
+    }
+
+    void wrap() throws SSLException {
+        // Reset the buffers.
+        messageBuffer.position(0);
+        clientPacketBuffer.clear();
+
+        // Wrap the original message and create the encrypted data.
+        doWrap(messageBuffer, clientPacketBuffer);
+
+        // Lightweight comparison - just make sure the data length is correct.
+        assertEquals(preEncryptedBuffer.limit(), clientPacketBuffer.limit());
+    }
+
+    /**
+     * Simple benchmark that sends a single message from client to server.
+     */
+    void wrapAndUnwrap() throws SSLException {
+        // Reset the buffers.
+        messageBuffer.position(0);
+        clientPacketBuffer.clear();
+        serverApplicationBuffer.clear();
+
+        // Wrap the original message and create the encrypted data.
+        doWrap(messageBuffer, clientPacketBuffer);
+
+        // Unwrap the encrypted data and get back the original result.
+        doUnwrap(clientPacketBuffer, serverApplicationBuffer);
+
+        // Lightweight comparison - just make sure the unencrypted data length is correct.
+        assertEquals(messageBuffer.limit(), serverApplicationBuffer.limit());
+    }
+
+    private void doWrap(ByteBuffer src, ByteBuffer dst) throws SSLException {
+        // Wrap the original message and create the encrypted data.
+        verifyResult(src, clientEngine.wrap(src, dst));
+        dst.flip();
+    }
+
+    private void doUnwrap(ByteBuffer src, ByteBuffer dst) throws SSLException {
+        verifyResult(src, serverEngine.unwrap(src, dst));
+        dst.flip();
+    }
+
+    private void verifyResult(ByteBuffer src, SSLEngineResult result) {
+        if (result.getStatus() != SSLEngineResult.Status.OK) {
+            throw new RuntimeException("Operation returned unexpected result " + result);
+        }
+        if (result.bytesConsumed() != src.limit()) {
+            throw new RuntimeException(
+                    String.format("Operation didn't consume all bytes. Expected %d, consumed %d.",
+                            src.limit(), result.bytesConsumed()));
+        }
+    }
+
+    /**
+     * A simple main for profiling.
+     */
+    public static void main(String[] args) throws Exception {
+        EngineWrapBenchmark bm = new EngineWrapBenchmark(new Config() {
+            @Override
+            public BufferType bufferType() {
+                return BufferType.HEAP;
+            }
+
+            @Override
+            public EngineType engineType() {
+                return EngineType.CONSCRYPT_POOLED;
+            }
+
+            @Override
+            public int messageSize() {
+                return 512;
+            }
+
+            @Override
+            public String cipher() {
+                return TestUtils.TEST_CIPHER;
+            }
+        });
+
+        // Just run forever for profiling.
+        while (true) {
+            bm.wrapAndUnwrap();
+        }
+    }
+}
diff --git a/benchmark-base/src/main/java/org/conscrypt/PooledAllocator.java b/benchmark-base/src/main/java/org/conscrypt/PooledAllocator.java
new file mode 100644
index 0000000..ed84d7d
--- /dev/null
+++ b/benchmark-base/src/main/java/org/conscrypt/PooledAllocator.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.conscrypt;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.buffer.PooledByteBufAllocator;
+import java.nio.ByteBuffer;
+
+/**
+ * A {@link BufferAllocator} that is backed by a Netty buffer pool.
+ */
+final class PooledAllocator extends BufferAllocator {
+    private static final ByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT;
+    private static final PooledAllocator instance = new PooledAllocator();
+
+    static PooledAllocator getInstance() {
+        return instance;
+    }
+
+    private PooledAllocator() {}
+
+    @Override
+    public AllocatedBuffer allocateDirectBuffer(int capacity) {
+        return new ByteBufAdapter(alloc.directBuffer(capacity));
+    }
+
+    private static final class ByteBufAdapter extends AllocatedBuffer {
+        private final ByteBuf nettyBuffer;
+        private final ByteBuffer buffer;
+
+        private ByteBufAdapter(ByteBuf nettyBuffer) {
+            this.nettyBuffer = nettyBuffer;
+            nettyBuffer.writerIndex(nettyBuffer.capacity());
+            this.buffer = nettyBuffer.nioBuffer();
+        }
+
+        @Override
+        public ByteBuffer nioBuffer() {
+            return buffer;
+        }
+
+        @Override
+        public AllocatedBuffer retain() {
+            nettyBuffer.retain();
+            return this;
+        }
+
+        @Override
+        public AllocatedBuffer release() {
+            nettyBuffer.release();
+            return this;
+        }
+    }
+}
diff --git a/testing/src/main/java/org/conscrypt/TestServer.java b/benchmark-base/src/main/java/org/conscrypt/ServerEndpoint.java
similarity index 75%
rename from testing/src/main/java/org/conscrypt/TestServer.java
rename to benchmark-base/src/main/java/org/conscrypt/ServerEndpoint.java
index c706ba9..29cf10e 100644
--- a/testing/src/main/java/org/conscrypt/TestServer.java
+++ b/benchmark-base/src/main/java/org/conscrypt/ServerEndpoint.java
@@ -19,21 +19,25 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.ServerSocket;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
-import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
 import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
 
 /**
  * A simple socket-based test server.
  */
-public final class TestServer {
+final class ServerEndpoint {
     /**
      * A processor for receipt of a single message.
      */
-    public interface MessageProcessor { void processMessage(byte[] message, int numBytes, OutputStream os); }
+    public interface MessageProcessor {
+        void processMessage(byte[] message, int numBytes, OutputStream os);
+    }
 
     /**
      * A {@link MessageProcessor} that simply echos back the received message to the client.
@@ -50,8 +54,12 @@
         }
     }
 
-    private final SSLServerSocket serverSocket;
+    private final ServerSocket serverSocket;
+    private final ChannelType channelType;
+    private final SSLSocketFactory socketFactory;
     private final int messageSize;
+    private final String[] protocols;
+    private final String[] cipherSuites;
     private final byte[] buffer;
     private SSLSocket socket;
     private ExecutorService executor;
@@ -60,22 +68,28 @@
     private volatile boolean stopping;
     private volatile MessageProcessor messageProcessor = new EchoProcessor();
 
-    public TestServer(SSLServerSocket serverSocket, int messageSize) {
-        this.serverSocket = serverSocket;
+    ServerEndpoint(SSLSocketFactory socketFactory, SSLServerSocketFactory serverSocketFactory,
+            ChannelType channelType, int messageSize, String[] protocols,
+            String[] cipherSuites) throws IOException {
+        this.serverSocket = channelType.newServerSocket(serverSocketFactory);
+        this.socketFactory = socketFactory;
+        this.channelType = channelType;
         this.messageSize = messageSize;
+        this.protocols = protocols;
+        this.cipherSuites = cipherSuites;
         buffer = new byte[messageSize];
     }
 
-    public void setMessageProcessor(MessageProcessor messageProcessor) {
+    void setMessageProcessor(MessageProcessor messageProcessor) {
         this.messageProcessor = messageProcessor;
     }
 
-    public Future<?> start() {
+    Future<?> start() throws IOException {
         executor = Executors.newSingleThreadExecutor();
         return executor.submit(new AcceptTask());
     }
 
-    public void stop() {
+    void stop() {
         try {
             stopping = true;
 
@@ -106,7 +120,12 @@
                 if (stopping) {
                     return;
                 }
-                socket = (SSLSocket) serverSocket.accept();
+                socket = channelType.accept(serverSocket, socketFactory);
+                socket.setEnabledProtocols(protocols);
+                socket.setEnabledCipherSuites(cipherSuites);
+
+                socket.startHandshake();
+
                 inputStream = socket.getInputStream();
                 outputStream = socket.getOutputStream();
 
@@ -115,6 +134,7 @@
                 }
                 executor.execute(new ProcessTask());
             } catch (IOException e) {
+                e.printStackTrace();
                 throw new RuntimeException(e);
             }
         }
diff --git a/benchmark-base/src/main/java/org/conscrypt/ServerSocketBenchmark.java b/benchmark-base/src/main/java/org/conscrypt/ServerSocketBenchmark.java
new file mode 100644
index 0000000..9059b75
--- /dev/null
+++ b/benchmark-base/src/main/java/org/conscrypt/ServerSocketBenchmark.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import static org.conscrypt.TestUtils.getProtocols;
+import static org.conscrypt.TestUtils.newTextMessage;
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import org.conscrypt.ServerEndpoint.MessageProcessor;
+
+/**
+ * Benchmark for comparing performance of server socket implementations.
+ */
+public final class ServerSocketBenchmark {
+    /**
+     * Provider for the benchmark configuration
+     */
+    interface Config {
+        SocketType socketType();
+        int messageSize();
+        String cipher();
+        ChannelType channelType();
+    }
+
+    private ClientEndpoint client;
+    private ServerEndpoint server;
+    private ExecutorService executor;
+    private Future<?> receivingFuture;
+    private volatile boolean stopping;
+    private static final AtomicLong bytesCounter = new AtomicLong();
+    private AtomicBoolean recording = new AtomicBoolean();
+
+    ServerSocketBenchmark(Config config) throws Exception {
+        recording.set(false);
+
+        byte[] message = newTextMessage(config.messageSize());
+
+        final ChannelType channelType = config.channelType();
+
+        server = config.socketType().newServer(
+            channelType, config.messageSize(), getProtocols(), ciphers(config));
+        server.setMessageProcessor(new MessageProcessor() {
+            @Override
+            public void processMessage(byte[] inMessage, int numBytes, OutputStream os) {
+                try {
+                    while (!stopping) {
+                        os.write(inMessage, 0, numBytes);
+                    }
+                } catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        });
+
+        Future<?> connectedFuture = server.start();
+
+        // Always use the same client for consistency across the benchmarks.
+        client = SocketType.CONSCRYPT_ENGINE.newClient(
+                ChannelType.CHANNEL, server.port(), getProtocols(), ciphers(config));
+        client.start();
+
+        // Wait for the initial connection to complete.
+        connectedFuture.get(5, TimeUnit.SECONDS);
+
+        // Start the server-side streaming by sending a message to the server.
+        client.sendMessage(message);
+        client.flush();
+
+        executor = Executors.newSingleThreadExecutor();
+        receivingFuture = executor.submit(new Runnable() {
+            @Override
+            public void run() {
+                Thread thread = Thread.currentThread();
+                byte[] buffer = new byte[config.messageSize()];
+                while (!stopping && !thread.isInterrupted()) {
+                    int numBytes = client.readMessage(buffer);
+                    assertEquals(config.messageSize(), numBytes);
+
+                    // Increment the message counter if we're recording.
+                    if (recording.get()) {
+                        bytesCounter.addAndGet(numBytes);
+                    }
+                }
+            }
+        });
+    }
+
+    void close() throws Exception {
+        stopping = true;
+        client.stop();
+        server.stop();
+        executor.shutdown();
+        executor.awaitTermination(5, TimeUnit.SECONDS);
+        receivingFuture.get(5, TimeUnit.SECONDS);
+    }
+
+    void throughput() throws Exception {
+        recording.set(true);
+        // Send as many messages as we can in a second.
+        Thread.sleep(1001);
+        recording.set(false);
+    }
+
+    static void reset() {
+        bytesCounter.set(0);
+    }
+
+    static long bytesPerSecond() {
+        return bytesCounter.get();
+    }
+
+    private String[] ciphers(Config config) {
+        return new String[] {config.cipher()};
+    }
+
+    /**
+     * A simple main for profiling.
+     */
+    public static void main(String[] args) throws Exception {
+        ServerSocketBenchmark bm = new ServerSocketBenchmark(new Config() {
+            @Override
+            public SocketType socketType() {
+                return SocketType.CONSCRYPT_ENGINE;
+            }
+
+            @Override
+            public int messageSize() {
+                return 512;
+            }
+
+            @Override
+            public String cipher() {
+                return TestUtils.TEST_CIPHER;
+            }
+
+            @Override
+            public ChannelType channelType() {
+                return ChannelType.CHANNEL;
+            }
+        });
+
+        // Just run forever for profiling.
+        while (true) {
+            bm.throughput();
+        }
+    }
+}
diff --git a/benchmark-base/src/main/java/org/conscrypt/SocketType.java b/benchmark-base/src/main/java/org/conscrypt/SocketType.java
new file mode 100644
index 0000000..da0c3e9
--- /dev/null
+++ b/benchmark-base/src/main/java/org/conscrypt/SocketType.java
@@ -0,0 +1,71 @@
+package org.conscrypt;
+
+import java.io.IOException;
+import java.security.Provider;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * Utility for creating test client and server instances.
+ */
+@SuppressWarnings("ImmutableEnumChecker")
+public enum SocketType {
+    JDK(newJdkFactories()),
+    CONSCRYPT(newConscryptFactories(false)),
+    CONSCRYPT_ENGINE(newConscryptFactories(true));
+
+    private final Factories factories;
+
+    SocketType(Factories factories) {
+        this.factories = factories;
+    }
+
+    ClientEndpoint newClient(ChannelType channelType, int port, String[] protocols,
+            String[] ciphers) throws IOException {
+        return new ClientEndpoint(
+                factories.clientFactory, channelType, port, protocols, ciphers);
+    }
+
+    ServerEndpoint newServer(ChannelType channelType, int messageSize,
+            String[] protocols, String[] ciphers) throws IOException {
+        return new ServerEndpoint(factories.serverFactory, factories.serverSocketFactory,
+            channelType, messageSize, protocols, ciphers);
+    }
+
+    private static final class Factories {
+        final SSLSocketFactory clientFactory;
+        final SSLSocketFactory serverFactory;
+        final SSLServerSocketFactory serverSocketFactory;
+
+        private Factories(SSLSocketFactory clientFactory, SSLSocketFactory serverFactory,
+                SSLServerSocketFactory serverSocketFactory) {
+            this.clientFactory = clientFactory;
+            this.serverFactory = serverFactory;
+            this.serverSocketFactory = serverSocketFactory;
+        }
+    }
+
+    private static Factories newJdkFactories() {
+        Provider provider = TestUtils.getJdkProvider();
+        SSLContext clientContext = TestUtils.newClientSslContext(provider);
+        SSLContext serverContext = TestUtils.newServerSslContext(provider);
+        final SSLSocketFactory clientFactory = clientContext.getSocketFactory();
+        final SSLSocketFactory serverFactory = serverContext.getSocketFactory();
+        final SSLServerSocketFactory serverSocketFactory = serverContext.getServerSocketFactory();
+        return new Factories(clientFactory, serverFactory, serverSocketFactory);
+    }
+
+    private static Factories newConscryptFactories(boolean useEngineSocket) {
+        Provider provider = TestUtils.getConscryptProvider();
+        SSLContext clientContext = TestUtils.newClientSslContext(provider);
+        SSLContext serverContext = TestUtils.newServerSslContext(provider);
+        final SSLSocketFactory clientFactory = clientContext.getSocketFactory();
+        final SSLSocketFactory serverFactory = serverContext.getSocketFactory();
+        final SSLServerSocketFactory serverSocketFactory = serverContext.getServerSocketFactory();
+        TestUtils.setUseEngineSocket(clientFactory, useEngineSocket);
+        TestUtils.setUseEngineSocket(serverFactory, useEngineSocket);
+        TestUtils.setUseEngineSocket(serverSocketFactory, useEngineSocket);
+        return new Factories(clientFactory, serverFactory, serverSocketFactory);
+    }
+}
diff --git a/benchmark-graphs/build.gradle b/benchmark-graphs/build.gradle
index 9f5ee15..1625bea 100644
--- a/benchmark-graphs/build.gradle
+++ b/benchmark-graphs/build.gradle
@@ -6,3 +6,7 @@
 }
 
 mainClassName = 'org.conscrypt.graphgen.Main'
+
+// Don't include this artifact in the distribution.
+tasks.install.enabled = false
+tasks.uploadArchives.enabled = false;
diff --git a/benchmark-jmh/build.gradle b/benchmark-jmh/build.gradle
new file mode 100644
index 0000000..070211b
--- /dev/null
+++ b/benchmark-jmh/build.gradle
@@ -0,0 +1,71 @@
+plugins {
+    id 'me.champeau.gradle.jmh' version '0.3.1'
+}
+
+apply plugin: 'idea'
+
+description = 'Conscrypt: OpenJDK Benchmarks'
+
+ext {
+    genDir = "${buildDir}/jmh-generated-classes"
+    jmhInclude = System.getProperty('jmh.include')
+    jmhWarmupIterations = System.getProperty('jmh.wi', '10')
+    jmhIterations = System.getProperty('jmh.i', '10')
+    jmhFork = System.getProperty('jmh.f', '1')
+    jmhJvmArgs = System.getProperty('jmh.jvmArgs', '-server -Xms2g -Xmx2g')
+}
+
+jmh {
+    jmhVersion = "$jmhVersion"
+    if (jmhInclude != null) {
+        setInclude(jmhInclude.toString())
+    }
+    warmupIterations = "$jmhWarmupIterations".toInteger()
+    iterations = "$jmhIterations".toInteger();
+    fork = "$jmhFork".toInteger()
+    jvmArgs = jmhJvmArgs.toString()
+    duplicateClassesStrategy = 'warn'
+}
+
+configurations {
+    // The JMH plugin by defaults depends on all of the generators for an old version of JMH.
+    // Need to remove all the generators that we're not explicitly overriding to eliminate the
+    // dependency on the old version of JMH.
+    jmh.exclude module:'jmh-generator-asm'
+
+    jmhGeneratorAnnprocess
+}
+
+sourceSets {
+    sourceSets {
+        main {
+            resources {
+                // This shouldn't be needed but seems to help IntelliJ locate
+                // META_INF/BenchmarkList.
+                srcDirs += genDir
+            }
+        }
+    }
+}
+
+dependencies {
+    compile project(':conscrypt-benchmark-base'),
+            libraries.junit
+
+    jmhGeneratorAnnprocess libraries.jmh_generator_annprocess
+
+    // Override the default JMH dependencies with the new versions.
+    jmh libraries.jmh_core,
+            libraries.jmh_generator_reflection,
+            libraries.jmh_generator_bytecode
+}
+
+// Running benchmarks in IntelliJ seems broken without this.
+// See https://github.com/melix/jmh-gradle-plugin/issues/39
+idea.module {
+    scopes.PROVIDED.plus += [ configurations.compile, configurations.jmh ]//, configurations.jmhGeneratorAnnprocess ]
+}
+
+// Don't include this artifact in the distribution.
+tasks.install.enabled = false
+tasks.uploadArchives.enabled = false;
diff --git a/benchmark-jmh/src/jmh/java/org/conscrypt/JmhClientSocketBenchmark.java b/benchmark-jmh/src/jmh/java/org/conscrypt/JmhClientSocketBenchmark.java
new file mode 100644
index 0000000..425aba3
--- /dev/null
+++ b/benchmark-jmh/src/jmh/java/org/conscrypt/JmhClientSocketBenchmark.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import org.conscrypt.ClientSocketBenchmark.Config;
+import org.openjdk.jmh.annotations.AuxCounters;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+
+/**
+ * Benchmark for comparing performance of client socket implementations. All benchmarks use Netty
+ * with tcnative as the server.
+ */
+@State(Scope.Benchmark)
+@Fork(1)
+@Threads(1)
+public class JmhClientSocketBenchmark {
+    /**
+     * Use an AuxCounter so we can measure that bytes per second as they accumulate without
+     * consuming CPU in the benchmark method.
+     */
+    @AuxCounters
+    @State(Scope.Thread)
+    public static class BytesPerSecondCounter {
+        @Setup(Level.Iteration)
+        public void clean() {
+            ClientSocketBenchmark.reset();
+        }
+
+        @SuppressWarnings("unused")
+        public long bytesPerSecond() {
+            return ClientSocketBenchmark.bytesPerSecond();
+        }
+    }
+
+    private final JmhConfig config = new JmhConfig();
+
+    @Param
+    public SocketType socketType;
+
+    @Param({"64", "512", "4096"})
+    public int messageSize;
+
+    @Param({TestUtils.TEST_CIPHER})
+    public String cipher;
+
+    @Param
+    public ChannelType channelType;
+
+    private ClientSocketBenchmark benchmark;
+
+    @Setup(Level.Iteration)
+    public void setup() throws Exception {
+        benchmark = new ClientSocketBenchmark(config);
+    }
+
+    @TearDown(Level.Iteration)
+    public void teardown() throws Exception {
+        benchmark.close();
+    }
+
+    @Benchmark
+    public final void bm(@SuppressWarnings("unused") BytesPerSecondCounter counter)
+            throws Exception {
+        benchmark.throughput();
+    }
+
+    private final class JmhConfig implements Config {
+        @Override
+        public SocketType socketType() {
+            return socketType;
+        }
+
+        @Override
+        public int messageSize() {
+            return messageSize;
+        }
+
+        @Override
+        public String cipher() {
+            return cipher;
+        }
+
+        @Override
+        public ChannelType channelType() {
+            return channelType;
+        }
+    }
+}
diff --git a/benchmark-jmh/src/jmh/java/org/conscrypt/JmhEngineHandshakeBenchmark.java b/benchmark-jmh/src/jmh/java/org/conscrypt/JmhEngineHandshakeBenchmark.java
new file mode 100644
index 0000000..69c2773
--- /dev/null
+++ b/benchmark-jmh/src/jmh/java/org/conscrypt/JmhEngineHandshakeBenchmark.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed 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.
+ */
+
+/*
+ * Copyright 2017 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 org.conscrypt;
+
+import javax.net.ssl.SSLException;
+import org.conscrypt.EngineHandshakeBenchmark.Config;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+
+/**
+ * Benchmark comparing performance of various engine implementations to conscrypt.
+ */
+@State(Scope.Benchmark)
+@Fork(1)
+@Threads(1)
+public class JmhEngineHandshakeBenchmark {
+    private final JmhConfig config = new JmhConfig();
+
+    @Param({TestUtils.TEST_CIPHER})
+    public String a_cipher;
+
+    @Param
+    public BufferType b_buffer;
+
+    @Param
+    public EngineType c_engine;
+
+    private EngineHandshakeBenchmark benchmark;
+
+    @Setup(Level.Iteration)
+    public void setup() throws Exception {
+        benchmark = new EngineHandshakeBenchmark(config);
+    }
+
+    @Benchmark
+    public void hs() throws SSLException {
+        benchmark.handshake();
+    }
+
+    private final class JmhConfig implements Config {
+
+        @Override
+        public BufferType bufferType() {
+            return b_buffer;
+        }
+
+        @Override
+        public EngineType engineType() {
+            return c_engine;
+        }
+
+        @Override
+        public String cipher() {
+            return a_cipher;
+        }
+    }
+}
diff --git a/benchmark-jmh/src/jmh/java/org/conscrypt/JmhEngineWrapBenchmark.java b/benchmark-jmh/src/jmh/java/org/conscrypt/JmhEngineWrapBenchmark.java
new file mode 100644
index 0000000..7be5498
--- /dev/null
+++ b/benchmark-jmh/src/jmh/java/org/conscrypt/JmhEngineWrapBenchmark.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed 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.
+ */
+
+/*
+ * Copyright 2017 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 org.conscrypt;
+
+import javax.net.ssl.SSLException;
+import org.conscrypt.EngineWrapBenchmark.Config;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+
+/**
+ * Benchmark comparing performance of various engine implementations to conscrypt.
+ */
+@State(Scope.Benchmark)
+@Fork(1)
+@Threads(1)
+public class JmhEngineWrapBenchmark {
+    private final JmhConfig config = new JmhConfig();
+
+    @Param({TestUtils.TEST_CIPHER})
+    public String a_cipher;
+
+    @Param
+    public BufferType b_buffer;
+
+    @Param({"64", "512", "4096"})
+    public int c_message;
+
+    @Param
+    public EngineType d_engine;
+
+    private EngineWrapBenchmark benchmark;
+
+    @Setup(Level.Iteration)
+    public void setup() throws Exception {
+        benchmark = new EngineWrapBenchmark(config);
+    }
+
+    @TearDown(Level.Iteration)
+    public void teardown() {
+        benchmark.teardown();
+    }
+
+    @Benchmark
+    public void wrap() throws SSLException {
+        benchmark.wrap();
+    }
+
+    @Benchmark
+    public void wrapAndUnwrap() throws SSLException {
+        benchmark.wrapAndUnwrap();
+    }
+
+    private final class JmhConfig implements Config {
+
+        @Override
+        public BufferType bufferType() {
+            return b_buffer;
+        }
+
+        @Override
+        public EngineType engineType() {
+            return d_engine;
+        }
+
+        @Override
+        public int messageSize() {
+            return c_message;
+        }
+
+        @Override
+        public String cipher() {
+            return a_cipher;
+        }
+    }
+}
diff --git a/benchmark-jmh/src/jmh/java/org/conscrypt/JmhServerSocketBenchmark.java b/benchmark-jmh/src/jmh/java/org/conscrypt/JmhServerSocketBenchmark.java
new file mode 100644
index 0000000..5452316
--- /dev/null
+++ b/benchmark-jmh/src/jmh/java/org/conscrypt/JmhServerSocketBenchmark.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import org.conscrypt.ServerSocketBenchmark.Config;
+import org.openjdk.jmh.annotations.AuxCounters;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+
+/**
+ * Benchmark for comparing performance of server socket implementations. All benchmarks use the
+ * standard JDK TLS implementation.
+ */
+@State(Scope.Benchmark)
+@Fork(1)
+@Threads(1)
+public class JmhServerSocketBenchmark {
+    /**
+     * Use an AuxCounter so we can measure that bytes per second as they accumulate without
+     * consuming CPU in the benchmark method.
+     */
+    @AuxCounters
+    @State(Scope.Thread)
+    public static class BytesPerSecondCounter {
+        @Setup(Level.Iteration)
+        public void clean() {
+            ServerSocketBenchmark.reset();
+        }
+
+        @SuppressWarnings("unused")
+        public long bytesPerSecond() {
+            return ServerSocketBenchmark.bytesPerSecond();
+        }
+    }
+
+    private final JmhConfig config = new JmhConfig();
+
+    @Param
+    public SocketType socketType;
+
+    @Param
+    public ChannelType channelType;
+
+    @Param({"64", "512", "4096"})
+    public int messageSize;
+
+    @Param({"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"})
+    public String cipher;
+
+    private ServerSocketBenchmark benchmark;
+
+    @Setup(Level.Iteration)
+    public void setup() throws Exception {
+        benchmark = new ServerSocketBenchmark(config);
+    }
+
+    @TearDown(Level.Iteration)
+    public void teardown() throws Exception {
+        benchmark.close();
+    }
+
+    @Benchmark
+    public final void bm(@SuppressWarnings("unused") BytesPerSecondCounter counter)
+            throws Exception {
+        benchmark.throughput();
+    }
+
+    private final class JmhConfig implements Config {
+        @Override
+        public SocketType socketType() {
+            return socketType;
+        }
+
+        @Override
+        public int messageSize() {
+            return messageSize;
+        }
+
+        @Override
+        public String cipher() {
+            return cipher;
+        }
+
+        @Override
+        public ChannelType channelType() {
+            return channelType;
+        }
+    }
+}
diff --git a/build.gradle b/build.gradle
index e821006..a5ea976 100644
--- a/build.gradle
+++ b/build.gradle
@@ -58,11 +58,11 @@
         }
 
         boringsslHome = "$System.env.BORINGSSL_HOME"
-        boringsslIncludeDir = "$boringsslHome/include"
-        boringssl32BuildDir = "$boringsslHome/build32"
-        boringssl64BuildDir = "$boringsslHome/build64"
+        boringsslIncludeDir = normalizePath("$boringsslHome/include")
+        boringssl32BuildDir = normalizePath("$boringsslHome/build32")
+        boringssl64BuildDir = normalizePath("$boringsslHome/build64")
         jdkHome = "$System.env.JAVA_HOME"
-        jdkIncludeDir = "$jdkHome/include"
+        jdkIncludeDir = normalizePath("$jdkHome/include")
         // Needs to be binary compatible with androidMinSdkVersion
         androidMinJavaVersion = JavaVersion.VERSION_1_7
 
@@ -80,16 +80,16 @@
         boringSslGit = org.ajoberstar.grgit.Grgit.open(file("$boringsslHome"))
         boringSslVersion = boringSslGit.head().id
 
-        jmhVersion = '1.17.4'
+        jmhVersion = '1.19'
         libraries = [
                 roboelectric: 'org.robolectric:android-all:7.1.0_r7-robolectric-0',
 
                 // Test dependencies.
+                bouncycastle_apis: 'org.bouncycastle:bcpkix-jdk15on:1.56',
+                bouncycastle_provider: 'org.bouncycastle:bcprov-jdk15on:1.56',
                 junit  : 'junit:junit:4.12',
                 mockito: 'org.mockito:mockito-core:1.9.5',
                 truth  : 'com.google.truth:truth:0.28',
-                bouncycastle_provider: 'org.bouncycastle:bcprov-jdk15on:1.56',
-                bouncycastle_apis: 'org.bouncycastle:bcpkix-jdk15on:1.56',
 
                 // Benchmark dependencies
                 jmh_core: "org.openjdk.jmh:jmh-core:${jmhVersion}",
@@ -234,3 +234,7 @@
         }
     }
 }
+
+static String normalizePath(path) {
+    new File(path.toString()).absolutePath
+}
diff --git a/common/src/jni/main/cpp/NativeCrypto.cpp b/common/src/jni/main/cpp/NativeCrypto.cpp
index aeb6205..521295f 100644
--- a/common/src/jni/main/cpp/NativeCrypto.cpp
+++ b/common/src/jni/main/cpp/NativeCrypto.cpp
@@ -308,6 +308,35 @@
 }
 
 /**
+ * Finishes a pending CBB and returns a jbyteArray with the contents.
+ */
+jbyteArray CBBToByteArray(JNIEnv* env, CBB* cbb) {
+    uint8_t *data;
+    size_t len;
+    if (!CBB_finish(cbb, &data, &len)) {
+        Errors::jniThrowRuntimeException(env, "CBB_finish failed");
+        JNI_TRACE("creating byte array failed");
+        return nullptr;
+    }
+    bssl::UniquePtr<uint8_t> free_data(data);
+
+    ScopedLocalRef<jbyteArray> byteArray(env, env->NewByteArray(static_cast<jsize>(len)));
+    if (byteArray.get() == nullptr) {
+        JNI_TRACE("creating byte array failed");
+        return nullptr;
+    }
+
+    ScopedByteArrayRW bytes(env, byteArray.get());
+    if (bytes.get() == nullptr) {
+        JNI_TRACE("using byte array failed");
+        return nullptr;
+    }
+
+    memcpy(bytes.get(), data, len);
+    return byteArray.release();
+}
+
+/**
  * Converts ASN.1 BIT STRING to a jbooleanArray.
  */
 jbooleanArray ASN1BitStringToBooleanArray(JNIEnv* env, ASN1_BIT_STRING* bitStr) {
@@ -568,22 +597,6 @@
   return ex_data->cached_size;
 }
 
-// TODO(davidben): Remove this once
-// https://boringssl-review.googlesource.com/c/15864/ is in all Conscrypt
-// consumers.
-#if BORINGSSL_API_VERSION < 4
-int RsaMethodEncrypt(RSA* /* rsa */,
-                     size_t* /* out_len */,
-                     uint8_t* /* out */,
-                     size_t /* max_out */,
-                     const uint8_t* /* in */,
-                     size_t /* in_len */,
-                     int /* padding */) {
-  OPENSSL_PUT_ERROR(RSA, RSA_R_UNKNOWN_ALGORITHM_TYPE);
-  return 0;
-}
-#endif
-
 int RsaMethodSignRaw(RSA* rsa,
                      size_t* out_len,
                      uint8_t* out,
@@ -755,12 +768,6 @@
 
     g_rsa_method.common.is_static = 1;
     g_rsa_method.size = RsaMethodSize;
-    // TODO(davidben): Remove this once
-    // https://boringssl-review.googlesource.com/c/15864/ is in all Conscrypt
-    // consumers.
-#if BORINGSSL_API_VERSION < 4
-    g_rsa_method.encrypt = RsaMethodEncrypt;
-#endif
     g_rsa_method.sign_raw = RsaMethodSignRaw;
     g_rsa_method.decrypt = RsaMethodDecrypt;
     g_rsa_method.flags = RSA_FLAG_OPAQUE;
@@ -981,22 +988,6 @@
     return result;
 }
 
-/**
- * private static native int EVP_PKEY_size(int pkey);
- */
-static int NativeCrypto_EVP_PKEY_size(JNIEnv* env, jclass, jobject pkeyRef) {
-    EVP_PKEY* pkey = fromContextObject<EVP_PKEY>(env, pkeyRef);
-    JNI_TRACE("EVP_PKEY_size(%p)", pkey);
-
-    if (pkey == nullptr) {
-        return -1;
-    }
-
-    int result = EVP_PKEY_size(pkey);
-    JNI_TRACE("EVP_PKEY_size(%p) => %d", pkey, result);
-    return result;
-}
-
 typedef int print_func(BIO*, const EVP_PKEY*, int, ASN1_PCTX*);
 
 static jstring evp_print_func(JNIEnv* env, jobject pkeyRef, print_func* func,
@@ -1066,90 +1057,106 @@
 }
 
 /*
- * static native byte[] i2d_PKCS8_PRIV_KEY_INFO(int, byte[])
+ * static native byte[] EVP_marshal_private_key(long)
  */
-static jbyteArray NativeCrypto_i2d_PKCS8_PRIV_KEY_INFO(JNIEnv* env, jclass, jobject pkeyRef) {
+static jbyteArray NativeCrypto_EVP_marshal_private_key(JNIEnv* env, jclass, jobject pkeyRef) {
     EVP_PKEY* pkey = fromContextObject<EVP_PKEY>(env, pkeyRef);
-    JNI_TRACE("i2d_PKCS8_PRIV_KEY_INFO(%p)", pkey);
+    JNI_TRACE("EVP_marshal_private_key(%p)", pkey);
 
     if (pkey == nullptr) {
         return nullptr;
     }
 
-    bssl::UniquePtr<PKCS8_PRIV_KEY_INFO> pkcs8(EVP_PKEY2PKCS8(pkey));
-    if (pkcs8.get() == nullptr) {
-        Errors::throwExceptionIfNecessary(env, "NativeCrypto_i2d_PKCS8_PRIV_KEY_INFO");
-        JNI_TRACE("key=%p i2d_PKCS8_PRIV_KEY_INFO => error from key to PKCS8", pkey);
+    bssl::ScopedCBB cbb;
+    if (!CBB_init(cbb.get(), 64)) {
+        Errors::jniThrowOutOfMemory(env, "CBB_init failed");
+        JNI_TRACE("CBB_init failed");
         return nullptr;
     }
 
-    return ASN1ToByteArray<PKCS8_PRIV_KEY_INFO>(env, pkcs8.get(), i2d_PKCS8_PRIV_KEY_INFO);
+    if (!EVP_marshal_private_key(cbb.get(), pkey)) {
+        Errors::throwExceptionIfNecessary(env, "EVP_marshal_private_key");
+        JNI_TRACE("key=%p EVP_marshal_private_key => error", pkey);
+        return nullptr;
+    }
+
+    return CBBToByteArray(env, cbb.get());
 }
 
 /*
- * static native int d2i_PKCS8_PRIV_KEY_INFO(byte[])
+ * static native long EVP_parse_private_key(byte[])
  */
-static jlong NativeCrypto_d2i_PKCS8_PRIV_KEY_INFO(JNIEnv* env, jclass, jbyteArray keyJavaBytes) {
-    JNI_TRACE("d2i_PKCS8_PRIV_KEY_INFO(%p)", keyJavaBytes);
+static jlong NativeCrypto_EVP_parse_private_key(JNIEnv* env, jclass, jbyteArray keyJavaBytes) {
+    JNI_TRACE("EVP_parse_private_key(%p)", keyJavaBytes);
 
     ScopedByteArrayRO bytes(env, keyJavaBytes);
     if (bytes.get() == nullptr) {
-        JNI_TRACE("bytes=%p d2i_PKCS8_PRIV_KEY_INFO => threw exception", keyJavaBytes);
+        JNI_TRACE("bytes=%p EVP_parse_private_key => threw exception", keyJavaBytes);
         return 0;
     }
 
-    const unsigned char* tmp = reinterpret_cast<const unsigned char*>(bytes.get());
-    bssl::UniquePtr<PKCS8_PRIV_KEY_INFO> pkcs8(
-            d2i_PKCS8_PRIV_KEY_INFO(nullptr, &tmp, static_cast<long>(bytes.size())));
-    if (pkcs8.get() == nullptr) {
-        Errors::throwExceptionIfNecessary(env, "d2i_PKCS8_PRIV_KEY_INFO");
-        JNI_TRACE("ssl=%p d2i_PKCS8_PRIV_KEY_INFO => error from DER to PKCS8", keyJavaBytes);
+    CBS cbs;
+    CBS_init(&cbs, reinterpret_cast<const uint8_t*>(bytes.get()), bytes.size());
+    bssl::UniquePtr<EVP_PKEY> pkey(EVP_parse_private_key(&cbs));
+    if (!pkey || CBS_len(&cbs) != 0) {
+        Errors::throwParsingException(env, "Error parsing private key");
+        JNI_TRACE("bytes=%p EVP_parse_private_key => threw exception", keyJavaBytes);
         return 0;
     }
 
-    bssl::UniquePtr<EVP_PKEY> pkey(EVP_PKCS82PKEY(pkcs8.get()));
-    if (pkey.get() == nullptr) {
-        Errors::throwExceptionIfNecessary(env, "d2i_PKCS8_PRIV_KEY_INFO");
-        JNI_TRACE("ssl=%p d2i_PKCS8_PRIV_KEY_INFO => error from PKCS8 to key", keyJavaBytes);
-        return 0;
-    }
-
-    JNI_TRACE("bytes=%p d2i_PKCS8_PRIV_KEY_INFO => %p", keyJavaBytes, pkey.get());
+    JNI_TRACE("bytes=%p EVP_parse_private_key => %p", keyJavaBytes, pkey.get());
     return reinterpret_cast<uintptr_t>(pkey.release());
 }
 
 /*
- * static native byte[] i2d_PUBKEY(int)
+ * static native byte[] EVP_marshal_public_key(long)
  */
-static jbyteArray NativeCrypto_i2d_PUBKEY(JNIEnv* env, jclass, jobject pkeyRef) {
+static jbyteArray NativeCrypto_EVP_marshal_public_key(JNIEnv* env, jclass, jobject pkeyRef) {
     EVP_PKEY* pkey = fromContextObject<EVP_PKEY>(env, pkeyRef);
-    JNI_TRACE("i2d_PUBKEY(%p)", pkey);
+    JNI_TRACE("EVP_marshal_public_key(%p)", pkey);
+
     if (pkey == nullptr) {
         return nullptr;
     }
-    return ASN1ToByteArray<EVP_PKEY>(env, pkey, reinterpret_cast<int (*) (EVP_PKEY*, uint8_t **)>(i2d_PUBKEY));
+
+    bssl::ScopedCBB cbb;
+    if (!CBB_init(cbb.get(), 64)) {
+        Errors::jniThrowOutOfMemory(env, "CBB_init failed");
+        JNI_TRACE("CBB_init failed");
+        return nullptr;
+    }
+
+    if (!EVP_marshal_public_key(cbb.get(), pkey)) {
+        Errors::throwExceptionIfNecessary(env, "EVP_marshal_public_key");
+        JNI_TRACE("key=%p EVP_marshal_public_key => error", pkey);
+        return nullptr;
+    }
+
+    return CBBToByteArray(env, cbb.get());
 }
 
 /*
- * static native int d2i_PUBKEY(byte[])
+ * static native long EVP_parse_public_key(byte[])
  */
-static jlong NativeCrypto_d2i_PUBKEY(JNIEnv* env, jclass, jbyteArray javaBytes) {
-    JNI_TRACE("d2i_PUBKEY(%p)", javaBytes);
+static jlong NativeCrypto_EVP_parse_public_key(JNIEnv* env, jclass, jbyteArray keyJavaBytes) {
+    JNI_TRACE("EVP_parse_public_key(%p)", keyJavaBytes);
 
-    ScopedByteArrayRO bytes(env, javaBytes);
+    ScopedByteArrayRO bytes(env, keyJavaBytes);
     if (bytes.get() == nullptr) {
-        JNI_TRACE("d2i_PUBKEY(%p) => threw error", javaBytes);
+        JNI_TRACE("bytes=%p EVP_parse_public_key => threw exception", keyJavaBytes);
         return 0;
     }
 
-    const unsigned char* tmp = reinterpret_cast<const unsigned char*>(bytes.get());
-    bssl::UniquePtr<EVP_PKEY> pkey(d2i_PUBKEY(nullptr, &tmp, static_cast<long>(bytes.size())));
-    if (pkey.get() == nullptr) {
-        JNI_TRACE("bytes=%p d2i_PUBKEY => threw exception", javaBytes);
-        Errors::throwExceptionIfNecessary(env, "d2i_PUBKEY");
+    CBS cbs;
+    CBS_init(&cbs, reinterpret_cast<const uint8_t*>(bytes.get()), bytes.size());
+    bssl::UniquePtr<EVP_PKEY> pkey(EVP_parse_public_key(&cbs));
+    if (!pkey || CBS_len(&cbs) != 0) {
+        Errors::throwParsingException(env, "Error parsing public key");
+        JNI_TRACE("bytes=%p EVP_parse_public_key => threw exception", keyJavaBytes);
         return 0;
     }
 
+    JNI_TRACE("bytes=%p EVP_parse_public_key => %p", keyJavaBytes, pkey.get());
     return reinterpret_cast<uintptr_t>(pkey.release());
 }
 
@@ -2366,23 +2373,6 @@
     return result;
 }
 
-/*
- * public static int void EVP_MD_block_size(long)
- */
-static jint NativeCrypto_EVP_MD_block_size(JNIEnv* env, jclass, jlong evpMdRef) {
-    EVP_MD* evp_md = reinterpret_cast<EVP_MD*>(evpMdRef);
-    JNI_TRACE("NativeCrypto_EVP_MD_block_size(%p)", evp_md);
-
-    if (evp_md == nullptr) {
-        Errors::jniThrowNullPointerException(env, nullptr);
-        return -1;
-    }
-
-    jint result = static_cast<jint>(EVP_MD_block_size(evp_md));
-    JNI_TRACE("NativeCrypto_EVP_MD_block_size(%p) => %d", evp_md, result);
-    return result;
-}
-
 static jlong evpDigestSignVerifyInit(
         JNIEnv* env,
         int (*init_func)(EVP_MD_CTX*, EVP_PKEY_CTX**, const EVP_MD*, ENGINE*, EVP_PKEY*),
@@ -3210,18 +3200,6 @@
     return nonceLength;
 }
 
-static jint NativeCrypto_EVP_AEAD_max_tag_len(JNIEnv* env, jclass, jlong evpAeadRef) {
-    const EVP_AEAD* evpAead = reinterpret_cast<const EVP_AEAD*>(evpAeadRef);
-    JNI_TRACE("EVP_AEAD_max_tag_len(%p)", evpAead);
-    if (evpAead == nullptr) {
-        Errors::jniThrowNullPointerException(env, "evpAead == null");
-        return 0;
-    }
-    jint maxTagLen = static_cast<jint>(EVP_AEAD_max_tag_len(evpAead));
-    JNI_TRACE("EVP_AEAD_max_tag_len(%p) => %d", evpAead, maxTagLen);
-    return maxTagLen;
-}
-
 typedef int (*evp_aead_ctx_op_func)(const EVP_AEAD_CTX *ctx, uint8_t *out,
                                     size_t *out_len, size_t max_out_len,
                                     const uint8_t *nonce, size_t nonce_len,
@@ -3471,41 +3449,6 @@
     JNI_TRACE("NativeCrypto_RAND_bytes(%p) => success", output);
 }
 
-static jint NativeCrypto_OBJ_txt2nid(JNIEnv* env, jclass, jstring oidStr) {
-    JNI_TRACE("OBJ_txt2nid(%p)", oidStr);
-
-    ScopedUtfChars oid(env, oidStr);
-    if (oid.c_str() == nullptr) {
-        return 0;
-    }
-
-    int nid = OBJ_txt2nid(oid.c_str());
-    JNI_TRACE("OBJ_txt2nid(%s) => %d", oid.c_str(), nid);
-    return nid;
-}
-
-static jstring NativeCrypto_OBJ_txt2nid_longName(JNIEnv* env, jclass, jstring oidStr) {
-    JNI_TRACE("OBJ_txt2nid_longName(%p)", oidStr);
-
-    ScopedUtfChars oid(env, oidStr);
-    if (oid.c_str() == nullptr) {
-        return nullptr;
-    }
-
-    JNI_TRACE("OBJ_txt2nid_longName(%s)", oid.c_str());
-
-    int nid = OBJ_txt2nid(oid.c_str());
-    if (nid == NID_undef) {
-        JNI_TRACE("OBJ_txt2nid_longName(%s) => NID_undef", oid.c_str());
-        ERR_clear_error();
-        return nullptr;
-    }
-
-    const char* longName = OBJ_nid2ln(nid);
-    JNI_TRACE("OBJ_txt2nid_longName(%s) => %s", oid.c_str(), longName);
-    return env->NewStringUTF(longName);
-}
-
 static jstring ASN1_OBJECT_to_OID_string(JNIEnv* env, const ASN1_OBJECT* obj) {
     /*
      * The OBJ_obj2txt API doesn't "measure" if you pass in nullptr as the buffer.
@@ -3566,71 +3509,6 @@
     return static_cast<jlong>(reinterpret_cast<uintptr_t>(bio.release()));
 }
 
-static int NativeCrypto_BIO_read(JNIEnv* env, jclass, jlong bioRef, jbyteArray outputJavaBytes) {
-    BIO* bio = reinterpret_cast<BIO*>(static_cast<uintptr_t>(bioRef));
-    JNI_TRACE("BIO_read(%p, %p)", bio, outputJavaBytes);
-
-    if (outputJavaBytes == nullptr) {
-        Errors::jniThrowNullPointerException(env, "output == null");
-        JNI_TRACE("BIO_read(%p, %p) => output == null", bio, outputJavaBytes);
-        return 0;
-    }
-
-    jsize outputSize = env->GetArrayLength(outputJavaBytes);
-
-    std::unique_ptr<unsigned char[]> buffer(
-            new unsigned char[static_cast<unsigned int>(outputSize)]);
-    if (buffer.get() == nullptr) {
-        Errors::jniThrowOutOfMemory(env, "Unable to allocate buffer for read");
-        return 0;
-    }
-
-    int read = BIO_read(bio, buffer.get(), static_cast<int>(outputSize));
-    if (read <= 0) {
-        Errors::throwIOException(env, "BIO_read");
-        JNI_TRACE("BIO_read(%p, %p) => threw IO exception", bio, outputJavaBytes);
-        return 0;
-    }
-
-    env->SetByteArrayRegion(outputJavaBytes, 0, read, reinterpret_cast<jbyte*>(buffer.get()));
-    JNI_TRACE("BIO_read(%p, %p) => %d", bio, outputJavaBytes, read);
-    return read;
-}
-
-static void NativeCrypto_BIO_write(JNIEnv* env, jclass, jlong bioRef, jbyteArray inputJavaBytes,
-        jint offset, jint length) {
-    BIO* bio = reinterpret_cast<BIO*>(static_cast<uintptr_t>(bioRef));
-    JNI_TRACE("BIO_write(%p, %p, %d, %d)", bio, inputJavaBytes, offset, length);
-
-    if (inputJavaBytes == nullptr) {
-        Errors::jniThrowNullPointerException(env, "input == null");
-        return;
-    }
-
-    int inputSize = env->GetArrayLength(inputJavaBytes);
-    if (offset < 0 || offset > inputSize || length < 0 || length > inputSize - offset) {
-        Errors::jniThrowException(env, "java/lang/ArrayIndexOutOfBoundsException", "inputJavaBytes");
-        JNI_TRACE("BIO_write(%p, %p, %d, %d) => IOOB", bio, inputJavaBytes, offset, length);
-        return;
-    }
-
-    std::unique_ptr<unsigned char[]> buffer(new unsigned char[static_cast<unsigned int>(length)]);
-    if (buffer.get() == nullptr) {
-        Errors::jniThrowOutOfMemory(env, "Unable to allocate buffer for write");
-        return;
-    }
-
-    env->GetByteArrayRegion(inputJavaBytes, offset, length, reinterpret_cast<jbyte*>(buffer.get()));
-    if (BIO_write(bio, buffer.get(), length) != length) {
-        ERR_clear_error();
-        Errors::throwIOException(env, "BIO_write");
-        JNI_TRACE("BIO_write(%p, %p, %d, %d) => IO error", bio, inputJavaBytes, offset, length);
-        return;
-    }
-
-    JNI_TRACE("BIO_write(%p, %p, %d, %d) => success", bio, inputJavaBytes, offset, length);
-}
-
 static void NativeCrypto_BIO_free_all(JNIEnv* env, jclass, jlong bioRef) {
     BIO* bio = reinterpret_cast<BIO*>(static_cast<uintptr_t>(bioRef));
     JNI_TRACE("BIO_free_all(%p)", bio);
@@ -4456,46 +4334,200 @@
                         sec);
 }
 
-static jstring NativeCrypto_OBJ_txt2nid_oid(JNIEnv* env, jclass, jstring oidStr) {
-    JNI_TRACE("OBJ_txt2nid_oid(%p)", oidStr);
+// A CbsHandle is a structure used to manage resources allocated by asn1_read-*
+// functions so that they can be freed properly when finished.  This struct owns
+// all objects pointed to by its members.
+struct CbsHandle {
+    // A pointer to the CBS.
+    std::unique_ptr<CBS> cbs;
+    // A pointer to the data held by the CBS.  If the data held by the CBS
+    // is owned by a different CbsHandle, data will be null.
+    std::unique_ptr<unsigned char[]> data;
+};
 
-    ScopedUtfChars oid(env, oidStr);
-    if (oid.c_str() == nullptr) {
-        return nullptr;
+static jlong NativeCrypto_asn1_read_init(JNIEnv* env, jclass, jbyteArray data) {
+    JNI_TRACE("asn1_read_init(%p)", data);
+
+    ScopedByteArrayRO bytes(env, data);
+    if (bytes.get() == nullptr) {
+        Errors::throwIOException(env, "Error reading ASN.1 encoding");
+        return 0;
     }
 
-    JNI_TRACE("OBJ_txt2nid_oid(%s)", oid.c_str());
+    std::unique_ptr<CbsHandle> cbs(new CbsHandle());
+    cbs->data.reset(new unsigned char[bytes.size()]);
+    memcpy(cbs->data.get(), bytes.get(), bytes.size());
 
-    int nid = OBJ_txt2nid(oid.c_str());
-    if (nid == NID_undef) {
-        JNI_TRACE("OBJ_txt2nid_oid(%s) => NID_undef", oid.c_str());
-        ERR_clear_error();
-        return nullptr;
-    }
-
-    const ASN1_OBJECT* obj = OBJ_nid2obj(nid);
-    if (obj == nullptr) {
-        Errors::throwExceptionIfNecessary(env, "OBJ_nid2obj");
-        return nullptr;
-    }
-
-    ScopedLocalRef<jstring> ouputStr(env, ASN1_OBJECT_to_OID_string(env, obj));
-    JNI_TRACE("OBJ_txt2nid_oid(%s) => %p", oid.c_str(), ouputStr.get());
-    return ouputStr.release();
+    cbs->cbs.reset(new CBS());
+    CBS_init(cbs->cbs.get(), cbs->data.get(), bytes.size());
+    JNI_TRACE("asn1_read_init(%p) => %p", data, cbs.get());
+    return reinterpret_cast<uintptr_t>(cbs.release());
 }
 
-static jstring NativeCrypto_X509_NAME_print_ex(JNIEnv* env, jclass, jlong x509NameRef, jlong jflags) {
-    X509_NAME* x509name = reinterpret_cast<X509_NAME*>(static_cast<uintptr_t>(x509NameRef));
-    unsigned long flags = static_cast<unsigned long>(jflags);
-    JNI_TRACE("X509_NAME_print_ex(%p, %ld)", x509name, flags);
+static jlong NativeCrypto_asn1_read_sequence(JNIEnv* env, jclass, jlong cbsRef) {
+    CbsHandle* cbs = reinterpret_cast<CbsHandle*>(static_cast<uintptr_t>(cbsRef));
+    JNI_TRACE("asn1_read_sequence(%p)", cbs);
 
-    if (x509name == nullptr) {
-        Errors::jniThrowNullPointerException(env, "x509name == null");
-        JNI_TRACE("X509_NAME_print_ex(%p, %ld) => x509name == null", x509name, flags);
-        return nullptr;
+    std::unique_ptr<CbsHandle> seq(new CbsHandle());
+    seq->cbs.reset(new CBS());
+    if (!CBS_get_asn1(cbs->cbs.get(), seq->cbs.get(), CBS_ASN1_SEQUENCE)) {
+        Errors::throwIOException(env, "Error reading ASN.1 encoding");
+        return 0;
+    }
+    JNI_TRACE("asn1_read_sequence(%p) => %p", cbs, seq.get());
+    return reinterpret_cast<uintptr_t>(seq.release());
+}
+
+static jbyteArray NativeCrypto_asn1_read_octetstring(JNIEnv* env, jclass, jlong cbsRef) {
+    CbsHandle* cbs = reinterpret_cast<CbsHandle*>(static_cast<uintptr_t>(cbsRef));
+    JNI_TRACE("asn1_read_octetstring(%p)", cbs);
+
+    std::unique_ptr<CBS> str(new CBS());
+    if (!CBS_get_asn1(cbs->cbs.get(), str.get(), CBS_ASN1_OCTETSTRING)) {
+        Errors::throwIOException(env, "Error reading ASN.1 encoding");
+        return 0;
+    }
+    ScopedLocalRef<jbyteArray> out(env, env->NewByteArray(static_cast<jsize>(CBS_len(str.get()))));
+    if (out.get() == nullptr) {
+        Errors::throwIOException(env, "Error reading ASN.1 encoding");
+        return 0;
+    }
+    ScopedByteArrayRW outBytes(env, out.get());
+    if (outBytes.get() == nullptr) {
+        Errors::throwIOException(env, "Error reading ASN.1 encoding");
+        return 0;
+    }
+    memcpy(outBytes.get(), CBS_data(str.get()), CBS_len(str.get()));
+    JNI_TRACE("asn1_read_octetstring(%p) => %p", cbs, out.get());
+    return out.release();
+}
+
+static jlong NativeCrypto_asn1_read_uint64(JNIEnv* env, jclass, jlong cbsRef) {
+    CbsHandle* cbs = reinterpret_cast<CbsHandle*>(static_cast<uintptr_t>(cbsRef));
+    JNI_TRACE("asn1_read_uint64(%p)", cbs);
+
+    uint64_t value;
+    if (!CBS_get_asn1_uint64(cbs->cbs.get(), &value)) {
+        Errors::throwIOException(env, "Error reading ASN.1 encoding");
+        return 0;
+    }
+    return value;
+}
+
+static jboolean NativeCrypto_asn1_read_is_empty(CONSCRYPT_UNUSED JNIEnv* env, jclass, jlong cbsRef) {
+    CbsHandle* cbs = reinterpret_cast<CbsHandle*>(static_cast<uintptr_t>(cbsRef));
+    JNI_TRACE("asn1_read_is_empty(%p)", cbs);
+
+    bool empty = (CBS_len(cbs->cbs.get()) == 0);
+    JNI_TRACE("asn1_read_is_empty(%p) => %s", cbs, empty ? "true" : "false");
+    return empty;
+}
+
+static void NativeCrypto_asn1_read_free(CONSCRYPT_UNUSED JNIEnv* env, jclass, jlong cbsRef) {
+    if (cbsRef == 0) {
+        JNI_TRACE("asn1_read_free(0)");
+        return;
+    }
+    CbsHandle* cbs = reinterpret_cast<CbsHandle*>(static_cast<uintptr_t>(cbsRef));
+    JNI_TRACE("asn1_read_free(%p)", cbs);
+    delete cbs;
+}
+
+static jlong NativeCrypto_asn1_write_init(JNIEnv* env, jclass) {
+    JNI_TRACE("asn1_write_init");
+    std::unique_ptr<CBB> cbb(new CBB());
+    if (!CBB_init(cbb.get(), 128)) {
+        Errors::throwIOException(env, "Error writing ASN.1 encoding");
+        return 0;
+    }
+    JNI_TRACE("asn1_write_init => %p", cbb.get());
+    return reinterpret_cast<uintptr_t>(cbb.release());
+}
+
+static jlong NativeCrypto_asn1_write_sequence(JNIEnv* env, jclass, jlong cbbRef) {
+    CBB* cbb = reinterpret_cast<CBB*>(static_cast<uintptr_t>(cbbRef));
+    JNI_TRACE("asn1_write_sequence(%p)", cbb);
+
+    std::unique_ptr<CBB> seq(new CBB());
+    if (!CBB_add_asn1(cbb, seq.get(), CBS_ASN1_SEQUENCE)) {
+        Errors::throwIOException(env, "Error writing ASN.1 encoding");
+        return 0;
+    }
+    JNI_TRACE("asn1_write_sequence(%p) => %p", cbb, seq.get());
+    return reinterpret_cast<uintptr_t>(seq.release());
+}
+
+static void NativeCrypto_asn1_write_octetstring(JNIEnv* env, jclass, jlong cbbRef, jbyteArray data) {
+    CBB* cbb = reinterpret_cast<CBB*>(static_cast<uintptr_t>(cbbRef));
+    JNI_TRACE("asn1_write_octetstring(%p, %p)", cbb, data);
+
+    ScopedByteArrayRO bytes(env, data);
+    if (bytes.get() == nullptr) {
+        JNI_TRACE("asn1_write_octetstring(%p, %p) => using byte array failed", cbb, data);
+        return;
     }
 
-    return X509_NAME_to_jstring(env, x509name, flags);
+    std::unique_ptr<CBB> octetstring(new CBB());
+    if (!CBB_add_asn1(cbb, octetstring.get(), CBS_ASN1_OCTETSTRING)) {
+        Errors::throwIOException(env, "Error writing ASN.1 encoding");
+        return;
+    }
+    if (!CBB_add_bytes(octetstring.get(), reinterpret_cast<const uint8_t*>(bytes.get()), bytes.size())) {
+        Errors::throwIOException(env, "Error writing ASN.1 encoding");
+        return;
+    }
+}
+
+static void NativeCrypto_asn1_write_uint64(JNIEnv* env, jclass, jlong cbbRef, jlong data) {
+    CBB* cbb = reinterpret_cast<CBB*>(static_cast<uintptr_t>(cbbRef));
+    JNI_TRACE("asn1_write_uint64(%p)", cbb);
+
+    if (!CBB_add_asn1_uint64(cbb, static_cast<uint64_t>(data))) {
+        Errors::throwIOException(env, "Error writing ASN.1 encoding");
+        return;
+    }
+}
+
+static jbyteArray NativeCrypto_asn1_write_finish(JNIEnv* env, jclass, jlong cbbRef) {
+    CBB* cbb = reinterpret_cast<CBB*>(static_cast<uintptr_t>(cbbRef));
+    JNI_TRACE("asn1_write_finish(%p)", cbb);
+
+    uint8_t* data;
+    size_t data_len;
+    if (!CBB_finish(cbb, &data, &data_len)) {
+        Errors::throwIOException(env, "Error writing ASN.1 encoding");
+        return 0;
+    }
+    bssl::UniquePtr<uint8_t> data_storage(data);
+    ScopedLocalRef<jbyteArray> out(env, env->NewByteArray(static_cast<jsize>(data_len)));
+    if (out.get() == nullptr) {
+        Errors::throwIOException(env, "Error writing ASN.1 encoding");
+        return 0;
+    }
+    ScopedByteArrayRW outBytes(env, out.get());
+    if (outBytes.get() == nullptr) {
+        Errors::throwIOException(env, "Error writing ASN.1 encoding");
+        return 0;
+    }
+    memcpy(outBytes.get(), data, data_len);
+    return out.release();
+}
+
+static void NativeCrypto_asn1_write_cleanup(CONSCRYPT_UNUSED JNIEnv* env, jclass, jlong cbbRef) {
+    CBB* cbb = reinterpret_cast<CBB*>(static_cast<uintptr_t>(cbbRef));
+    JNI_TRACE("asn1_write_cleanup(%p)", cbb);
+
+    CBB_cleanup(cbb);
+}
+
+static void NativeCrypto_asn1_write_free(CONSCRYPT_UNUSED JNIEnv* env, jclass, jlong cbbRef) {
+    if (cbbRef == 0) {
+        JNI_TRACE("asn1_write_free(0)");
+        return;
+    }
+    CBB* cbb = reinterpret_cast<CBB*>(static_cast<uintptr_t>(cbbRef));
+    JNI_TRACE("asn1_write_free(%p)", cbb);
+    delete cbb;
 }
 
 template <typename T, T* (*d2i_func)(BIO*, T**)>
@@ -4646,29 +4678,7 @@
 
     sk_X509_free(stack);
 
-    uint8_t *derBytes;
-    size_t derLen;
-    if (!CBB_finish(out.get(), &derBytes, &derLen)) {
-        Errors::throwExceptionIfNecessary(env, "CBB_finish");
-        return nullptr;
-    }
-
-    ScopedLocalRef<jbyteArray> byteArray(env, env->NewByteArray(static_cast<jsize>(derLen)));
-    if (byteArray.get() == nullptr) {
-        JNI_TRACE("creating byte array failed");
-        return nullptr;
-    }
-
-    ScopedByteArrayRW bytes(env, byteArray.get());
-    if (bytes.get() == nullptr) {
-        JNI_TRACE("using byte array failed");
-        return nullptr;
-    }
-
-    uint8_t* p = reinterpret_cast<unsigned char*>(bytes.get());
-    memcpy(p, derBytes, derLen);
-
-    return byteArray.release();
+    return CBBToByteArray(env, out.get());
 }
 
 static jlongArray NativeCrypto_PEM_read_bio_PKCS7(JNIEnv* env, jclass, jlong bioRef, jint which) {
@@ -4847,29 +4857,7 @@
         }
     }
 
-    uint8_t *out;
-    size_t out_len;
-    if (!CBB_finish(result.get(), &out, &out_len)) {
-        return nullptr;
-    }
-    std::unique_ptr<uint8_t> out_storage(out);
-
-    ScopedLocalRef<jbyteArray> byteArray(env, env->NewByteArray(static_cast<jsize>(out_len)));
-    if (byteArray.get() == nullptr) {
-        JNI_TRACE("ASN1_seq_pack_X509(%p) => creating byte array failed", certs);
-        return nullptr;
-    }
-
-    ScopedByteArrayRW bytes(env, byteArray.get());
-    if (bytes.get() == nullptr) {
-        JNI_TRACE("ASN1_seq_pack_X509(%p) => using byte array failed", certs);
-        return nullptr;
-    }
-
-    uint8_t *p = reinterpret_cast<uint8_t*>(bytes.get());
-    memcpy(p, out, out_len);
-
-    return byteArray.release();
+    return CBBToByteArray(env, result.get());
 }
 
 static void NativeCrypto_X509_free(JNIEnv* env, jclass, jlong x509Ref) {
@@ -5846,6 +5834,81 @@
     return static_cast<unsigned int>(keyLen);
 }
 
+static int new_session_callback(SSL* ssl, SSL_SESSION* session) {
+    JNI_TRACE("ssl=%p new_session_callback session=%p", ssl, session);
+
+    AppData* appData = toAppData(ssl);
+    JNIEnv* env = appData->env;
+    if (env == nullptr) {
+        ALOGE("AppData->env missing in new_session_callback");
+        JNI_TRACE("ssl=%p new_session_callback env error", ssl);
+        return 0;
+    }
+    if (env->ExceptionCheck()) {
+        JNI_TRACE("ssl=%p new_session_callback already pending exception", ssl);
+        return 0;
+    }
+
+    jobject sslHandshakeCallbacks = appData->sslHandshakeCallbacks;
+    jclass cls = env->GetObjectClass(sslHandshakeCallbacks);
+    jmethodID methodID = env->GetMethodID(cls, "onNewSessionEstablished", "(J)V");
+    JNI_TRACE("ssl=%p new_session_callback calling onNewSessionEstablished", ssl);
+    env->CallVoidMethod(sslHandshakeCallbacks, methodID, reinterpret_cast<jlong>(session));
+    if (env->ExceptionCheck()) {
+        JNI_TRACE("ssl=%p new_session_callback exception cleared", ssl);
+        env->ExceptionClear();
+    }
+    JNI_TRACE("ssl=%p new_session_callback completed", ssl);
+
+    // Always returning 0 (not taking ownership). The Java code is responsible for incrementing
+    // the reference count.
+    return 0;
+}
+
+static SSL_SESSION* server_session_requested_callback(SSL* ssl, uint8_t* id, int id_len,
+                                                      int* out_copy) {
+    JNI_TRACE("ssl=%p server_session_requested_callback", ssl);
+
+    // Always set to out_copy to zero. The Java callback will be responsible for incrementing
+    // the reference count (and any required synchronization).
+    *out_copy = 0;
+
+    AppData* appData = toAppData(ssl);
+    JNIEnv* env = appData->env;
+    if (env == nullptr) {
+        ALOGE("AppData->env missing in server_session_requested_callback");
+        JNI_TRACE("ssl=%p server_session_requested_callback env error", ssl);
+        return 0;
+    }
+    if (env->ExceptionCheck()) {
+        JNI_TRACE("ssl=%p server_session_requested_callback already pending exception", ssl);
+        return 0;
+    }
+
+    // Copy the ID to a byte[].
+    jbyteArray id_array = env->NewByteArray(static_cast<jsize>(id_len));
+    if (id_array == nullptr) {
+        JNI_TRACE("ssl=%p id_array bytes == null => 0", ssl);
+        return 0;
+    }
+    env->SetByteArrayRegion(id_array, 0, static_cast<jsize>(id_len),
+                            reinterpret_cast<const jbyte*>(id));
+
+    jobject sslHandshakeCallbacks = appData->sslHandshakeCallbacks;
+    jclass cls = env->GetObjectClass(sslHandshakeCallbacks);
+    jmethodID methodID = env->GetMethodID(cls, "serverSessionRequested", "([B)J");
+    JNI_TRACE("ssl=%p server_session_requested_callback calling serverSessionRequested", ssl);
+    jlong ssl_session_address = env->CallLongMethod(sslHandshakeCallbacks, methodID, id_array);
+    if (env->ExceptionCheck()) {
+        JNI_TRACE("ssl=%p server_session_requested_callback exception cleared", ssl);
+        env->ExceptionClear();
+    }
+    SSL_SESSION* ssl_session_ptr = reinterpret_cast<SSL_SESSION*>(
+            static_cast<uintptr_t>(ssl_session_address));
+    JNI_TRACE("ssl=%p server_session_requested_callback completed => %p", ssl, ssl_session_ptr);
+    return ssl_session_ptr;
+}
+
 static jint NativeCrypto_EVP_has_aes_hardware(JNIEnv*, jclass) {
     int ret = 0;
     ret = EVP_has_aes_hardware();
@@ -5959,6 +6022,14 @@
         SSL_CTX_set_keylog_callback(sslCtx.get(), debug_print_session_key);
     }
 
+    // By default BoringSSL will cache in server mode, but we want to get
+    // notified of new sessions being created in client mode. We set
+    // SSL_SESS_CACHE_BOTH in order to get the callback in client mode, but
+    // ignore it in server mode in favor of the internal cache.
+    SSL_CTX_set_session_cache_mode(sslCtx.get(), SSL_SESS_CACHE_BOTH);
+    SSL_CTX_sess_set_new_cb(sslCtx.get(), new_session_callback);
+    SSL_CTX_sess_set_get_cb(sslCtx.get(), server_session_requested_callback);
+
     // Disable RSA-PSS deliberately until CryptoUpcalls supports it.
     if (!SSL_CTX_set_signing_algorithm_prefs(
                 sslCtx.get(), kDefaultSignatureAlgorithms,
@@ -6016,6 +6087,18 @@
     JNI_TRACE("ssl_ctx=%p NativeCrypto_SSL_CTX_set_session_id_context => ok", ssl_ctx);
 }
 
+static jlong NativeCrypto_SSL_CTX_set_timeout(JNIEnv* env, jclass, jlong ssl_ctx_address,
+                                             jlong seconds)
+{
+    SSL_CTX* ssl_ctx = to_SSL_CTX(env, ssl_ctx_address, true);
+    JNI_TRACE("ssl_ctx=%p NativeCrypto_SSL_CTX_set_timeout seconds=%d", ssl_ctx, (int) seconds);
+    if (ssl_ctx == nullptr) {
+        return 0L;
+    }
+
+    return SSL_CTX_set_timeout(ssl_ctx, static_cast<uint32_t>(seconds));
+}
+
 /**
  * public static native int SSL_new(long ssl_ctx) throws SSLException;
  */
@@ -6315,20 +6398,6 @@
 }
 
 /**
- * public static native long SSL_get_mode(long ssl);
- */
-static jlong NativeCrypto_SSL_get_mode(JNIEnv* env, jclass, jlong ssl_address) {
-    SSL* ssl = to_SSL(env, ssl_address, true);
-    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_mode", ssl);
-    if (ssl == nullptr) {
-        return 0;
-    }
-    long mode = static_cast<long>(SSL_get_mode(ssl));
-    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_mode => 0x%lx", ssl, mode);
-    return mode;
-}
-
-/**
  * public static native long SSL_set_mode(long ssl, long mode);
  */
 static jlong NativeCrypto_SSL_set_mode(JNIEnv* env, jclass,
@@ -6344,36 +6413,6 @@
 }
 
 /**
- * public static native long SSL_clear_mode(long ssl, long mode);
- */
-static jlong NativeCrypto_SSL_clear_mode(JNIEnv* env, jclass,
-        jlong ssl_address, jlong mode) {
-    SSL* ssl = to_SSL(env, ssl_address, true);
-    JNI_TRACE("ssl=%p NativeCrypto_SSL_clear_mode mode=0x%llx", ssl, (long long) mode);
-    if (ssl == nullptr) {
-        return 0;
-    }
-    long result = static_cast<long>(SSL_clear_mode(ssl, static_cast<uint32_t>(mode)));
-    JNI_TRACE("ssl=%p NativeCrypto_SSL_clear_mode => 0x%lx", ssl, result);
-    return result;
-}
-
-/**
- * public static native long SSL_get_options(long ssl);
- */
-static jlong NativeCrypto_SSL_get_options(JNIEnv* env, jclass,
-        jlong ssl_address) {
-    SSL* ssl = to_SSL(env, ssl_address, true);
-    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_options", ssl);
-    if (ssl == nullptr) {
-        return 0;
-    }
-    long options = static_cast<long>(SSL_get_options(ssl));
-    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_options => 0x%lx", ssl, options);
-    return options;
-}
-
-/**
  * public static native long SSL_set_options(long ssl, long options);
  */
 static jlong NativeCrypto_SSL_set_options(JNIEnv* env, jclass,
@@ -7108,33 +7147,27 @@
     JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake => success", ssl);
 }
 
-/**
- * Perform SSL renegotiation
- */
-static void NativeCrypto_SSL_renegotiate(JNIEnv* env, jclass, jlong ssl_address)
-{
+static jstring NativeCrypto_SSL_get_current_cipher(JNIEnv* env, jclass, jlong ssl_address) {
     SSL* ssl = to_SSL(env, ssl_address, true);
-    JNI_TRACE("ssl=%p NativeCrypto_SSL_renegotiate", ssl);
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_current_cipher", ssl);
     if (ssl == nullptr) {
-        return;
+        return nullptr;
     }
-    int result = SSL_renegotiate(ssl);
-    if (result != 1) {
-        Errors::throwSSLExceptionStr(env, "Problem with SSL_renegotiate");
-        return;
+    const SSL_CIPHER* cipher = SSL_get_current_cipher(ssl);
+    const char* name = SSL_CIPHER_standard_name(cipher);
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_current_cipher => %s", ssl, name);
+    return env->NewStringUTF(name);
+}
+
+static jstring NativeCrypto_SSL_get_version(JNIEnv* env, jclass, jlong ssl_address) {
+    SSL* ssl = to_SSL(env, ssl_address, true);
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_version", ssl);
+    if (ssl == nullptr) {
+        return nullptr;
     }
-    // first call asks client to perform renegotiation
-    int ret = SSL_do_handshake(ssl);
-    if (ret != 1) {
-        OpenSslError sslError(ssl, ret);
-        Errors::throwSSLExceptionWithSslErrors(env, ssl, sslError.release(),
-                                       "Problem with SSL_do_handshake after SSL_renegotiate");
-        return;
-    }
-    // if client agrees, set ssl state and perform renegotiation
-    SSL_set_state(ssl, SSL_ST_ACCEPT);
-    SSL_do_handshake(ssl);
-    JNI_TRACE("ssl=%p NativeCrypto_SSL_renegotiate =>", ssl);
+    const char* protocol = SSL_get_version(ssl);
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_version => %s", ssl, protocol);
+    return env->NewStringUTF(protocol);
 }
 
 /**
@@ -7954,6 +7987,114 @@
 }
 
 /**
+ * Gets and returns in a long integer the creation's time of the
+ * actual SSL session.
+ */
+static jlong NativeCrypto_SSL_get_time(JNIEnv* env, jclass, jlong ssl_address) {
+    SSL* ssl = to_SSL(env, ssl_address, true);
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_time", ssl);
+    if (ssl == nullptr) {
+        return 0;
+    }
+
+    SSL_SESSION* ssl_session = SSL_get_session(ssl);
+    JNI_TRACE("ssl_session=%p NativeCrypto_SSL_get_time", ssl_session);
+    if (ssl_session == nullptr) {
+        // BoringSSL does not protect against a NULL session.
+        return 0;
+    }
+    // result must be jlong, not long or *1000 will overflow
+    jlong result = SSL_SESSION_get_time(ssl_session);
+    result *= 1000; // OpenSSL uses seconds, Java uses milliseconds.
+    JNI_TRACE("ssl_session=%p NativeCrypto_SSL_get_time => %lld", ssl_session, (long long) result);
+    return result;
+}
+
+/**
+ * Sets the timeout on the SSL session.
+ */
+static jlong NativeCrypto_SSL_set_timeout(JNIEnv* env, jclass, jlong ssl_address, jlong millis) {
+    SSL* ssl = to_SSL(env, ssl_address, true);
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_set_timeout", ssl);
+    if (ssl == nullptr) {
+        return 0;
+    }
+
+    SSL_SESSION* ssl_session = SSL_get_session(ssl);
+    JNI_TRACE("ssl_session=%p NativeCrypto_SSL_set_timeout", ssl_session);
+    if (ssl_session == nullptr) {
+        // BoringSSL does not protect against a NULL session.
+        return 0;
+    }
+
+    // Convert to seconds
+    long timeout = millis / 1000;
+    return SSL_set_timeout(ssl_session, timeout);
+}
+
+/**
+ * Gets the timeout for the SSL session.
+ */
+static jlong NativeCrypto_SSL_get_timeout(JNIEnv* env, jclass, jlong ssl_address) {
+    SSL* ssl = to_SSL(env, ssl_address, true);
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_timeout", ssl);
+    if (ssl == nullptr) {
+        return 0;
+    }
+
+    SSL_SESSION* ssl_session = SSL_get_session(ssl);
+    JNI_TRACE("ssl_session=%p NativeCrypto_SSL_get_timeout", ssl_session);
+    if (ssl_session == nullptr) {
+        // BoringSSL does not protect against a NULL session.
+        return 0;
+    }
+
+    jlong result = SSL_get_timeout(ssl_session);
+    result *= 1000; // OpenSSL uses seconds, Java uses milliseconds.
+    JNI_TRACE("ssl_session=%p NativeCrypto_SSL_get_timeout => %lld", ssl_session, (long long) result);
+    return result;
+}
+
+/**
+ * Gets the timeout for the SSL session.
+ */
+static jlong NativeCrypto_SSL_SESSION_get_timeout(JNIEnv* env, jclass, jlong ssl_session_address) {
+    SSL_SESSION* ssl_session = to_SSL_SESSION(env, ssl_session_address, true);
+    JNI_TRACE("ssl_session=%p NativeCrypto_SSL_SESSION_get_timeout", ssl_session);
+    if (ssl_session == nullptr) {
+        return 0;
+    }
+
+    return SSL_get_timeout(ssl_session);
+}
+
+/**
+ * Gets the ID for the SSL session, or null if no session is currently available.
+ */
+static jbyteArray NativeCrypto_SSL_session_id(JNIEnv* env, jclass, jlong ssl_address) {
+    SSL* ssl = to_SSL(env, ssl_address, true);
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_session_id", ssl);
+    if (ssl == nullptr) {
+        return nullptr;
+    }
+
+    SSL_SESSION* ssl_session = SSL_get_session(ssl);
+    JNI_TRACE("ssl_session=%p NativeCrypto_SSL_session_id", ssl_session);
+    if (ssl_session == nullptr) {
+        return nullptr;
+    }
+
+    jbyteArray result = env->NewByteArray(static_cast<jsize>(ssl_session->session_id_length));
+    if (result != nullptr) {
+        jbyte* src = reinterpret_cast<jbyte*>(ssl_session->session_id);
+        env->SetByteArrayRegion(result, 0, static_cast<jsize>(ssl_session->session_id_length), src);
+    }
+    JNI_TRACE("ssl_session=%p NativeCrypto_SSL_session_id => %p session_id_length=%d",
+             ssl_session, result, ssl_session->session_id_length);
+    return result;
+}
+
+/**
  * Gets and returns in a string the version of the SSL protocol. If it
  * returns the string "unknown" it means that no connection is established.
  */
@@ -7978,22 +8119,21 @@
         return nullptr;
     }
     const SSL_CIPHER* cipher = ssl_session->cipher;
-    const char* name = SSL_CIPHER_get_name(cipher);
+    const char* name = SSL_CIPHER_standard_name(cipher);
     JNI_TRACE("ssl_session=%p NativeCrypto_SSL_SESSION_cipher => %s", ssl_session, name);
     return env->NewStringUTF(name);
 }
 
-static jstring NativeCrypto_get_SSL_SESSION_tlsext_hostname(JNIEnv* env, jclass, jlong sessionJava) {
-    SSL_SESSION* ssl_session = to_SSL_SESSION(env, sessionJava, true);
-    JNI_TRACE("ssl_session=%p NativeCrypto_get_SSL_SESSION_tlsext_hostname", ssl_session);
-    if (ssl_session == nullptr || ssl_session->tlsext_hostname == nullptr) {
-        JNI_TRACE("ssl_session=%p NativeCrypto_get_SSL_SESSION_tlsext_hostname => null",
-                  ssl_session);
-        return nullptr;
+/**
+ * Increments the reference count of the session.
+ */
+static void NativeCrypto_SSL_SESSION_up_ref(JNIEnv* env, jclass, jlong ssl_session_address) {
+    SSL_SESSION* ssl_session = to_SSL_SESSION(env, ssl_session_address, true);
+    JNI_TRACE("ssl_session=%p NativeCrypto_SSL_SESSION_up_ref", ssl_session);
+    if (ssl_session == nullptr) {
+        return;
     }
-    JNI_TRACE("ssl_session=%p NativeCrypto_get_SSL_SESSION_tlsext_hostname => \"%s\"",
-              ssl_session, ssl_session->tlsext_hostname);
-    return env->NewStringUTF(ssl_session->tlsext_hostname);
+    SSL_SESSION_up_ref(ssl_session);
 }
 
 /**
@@ -8083,18 +8223,27 @@
 
     size_t size = sk_SSL_CIPHER_num(ciphers);
     ScopedLocalRef<jobjectArray> cipherNamesArray(
-            env, env->NewObjectArray(static_cast<jsize>(size), JniConstants::stringClass, nullptr));
+            env,
+            env->NewObjectArray(static_cast<jsize>(2 * size), JniConstants::stringClass, nullptr));
     if (cipherNamesArray.get() == nullptr) {
         return nullptr;
     }
 
+    // Return an array of standard and OpenSSL name pairs.
     for (size_t i = 0; i < size; i++) {
-        const char *name = SSL_CIPHER_get_name(sk_SSL_CIPHER_value(ciphers, i));
-        ScopedLocalRef<jstring> cipherName(env, env->NewStringUTF(name));
-        env->SetObjectArrayElement(cipherNamesArray.get(), static_cast<jsize>(i), cipherName.get());
+        const SSL_CIPHER* cipher = sk_SSL_CIPHER_value(ciphers, i);
+        ScopedLocalRef<jstring> cipherName(env,
+                                           env->NewStringUTF(SSL_CIPHER_standard_name(cipher)));
+        env->SetObjectArrayElement(cipherNamesArray.get(), static_cast<jsize>(2 * i),
+                                   cipherName.get());
+
+        ScopedLocalRef<jstring> opensslName(env, env->NewStringUTF(SSL_CIPHER_get_name(cipher)));
+        env->SetObjectArrayElement(cipherNamesArray.get(), static_cast<jsize>(2 * i + 1),
+                                   opensslName.get());
     }
 
-    JNI_TRACE("NativeCrypto_get_cipher_names(%s) => success (%zd entries)", selector.c_str(), size);
+    JNI_TRACE("NativeCrypto_get_cipher_names(%s) => success (%zd entries)", selector.c_str(),
+              2 * size);
     return cipherNamesArray.release();
 }
 
@@ -8389,22 +8538,6 @@
     return static_cast<jint>(BIO_ctrl_pending(bio));
 }
 
-static jlong NativeCrypto_SSL_get0_session(JNIEnv* env, jclass, jlong ssl_address) {
-    SSL* ssl = to_SSL(env, ssl_address, true);
-    if (ssl == nullptr) {
-        return 0;
-    }
-    return reinterpret_cast<uintptr_t>(SSL_get0_session(ssl));
-}
-
-static jlong NativeCrypto_SSL_get1_session(JNIEnv* env, jclass, jlong ssl_address) {
-    SSL* ssl = to_SSL(env, ssl_address, true);
-    if (ssl == nullptr) {
-        return 0;
-    }
-    return reinterpret_cast<uintptr_t>(SSL_get1_session(ssl));
-}
-
 static jint NativeCrypto_SSL_max_seal_overhead(JNIEnv* env, jclass, jlong ssl_address) {
     SSL* ssl = to_SSL(env, ssl_address, true);
     if (ssl == nullptr) {
@@ -8505,6 +8638,9 @@
         JNI_TRACE("ssl=%p NativeCrypto_ENGINE_SSL_do_handshake appData => 0", ssl);
         return 0;
     }
+
+    errno = 0;
+
     if (!appData->setCallbackState(env, shc, nullptr)) {
         Errors::throwSSLExceptionStr(env, "Unable to set appdata callback");
         ERR_clear_error();
@@ -8513,8 +8649,6 @@
         return 0;
     }
 
-    errno = 0;
-
     int ret = SSL_do_handshake(ssl);
     appData->clearCallbackState();
     if (env->ExceptionCheck()) {
@@ -8525,8 +8659,47 @@
         return 0;
     }
 
-    JNI_TRACE("ssl=%p NativeCrypto_ENGINE_SSL_do_handshake shc=%p => ret=%d", ssl, shc, ret);
-    return ret;
+    OpenSslError sslError(ssl, ret);
+    int code = sslError.get();
+
+    if (ret > 0 || code == SSL_ERROR_WANT_READ || code == SSL_ERROR_WANT_WRITE) {
+        // Non-exceptional case.
+        JNI_TRACE("ssl=%p NativeCrypto_ENGINE_SSL_do_handshake shc=%p => ret=%d", ssl, shc, code);
+        return code;
+    }
+
+    // Exceptional case...
+    if (ret == 0) {
+        // TODO(nmittler): Can this happen with memory BIOs?
+        /*
+         * Clean error. See SSL_do_handshake(3SSL) man page.
+         * The other side closed the socket before the handshake could be
+         * completed, but everything is within the bounds of the TLS protocol.
+         * We still might want to find out the real reason of the failure.
+         */
+        if (code == SSL_ERROR_NONE || (code == SSL_ERROR_SYSCALL && errno == 0) ||
+            (code == SSL_ERROR_ZERO_RETURN)) {
+            Errors::throwSSLHandshakeExceptionStr(env, "Connection closed by peer");
+        } else {
+            Errors::throwSSLExceptionWithSslErrors(env, ssl, sslError.release(),
+                                                   "SSL handshake terminated",
+                                                   Errors::throwSSLHandshakeExceptionStr);
+        }
+        safeSslClear(ssl);
+        JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake clean error => exception", ssl);
+        return code;
+    }
+
+    /*
+     * Unclean error. See SSL_do_handshake(3SSL) man page.
+     * Translate the error and throw exception. We are sure it is an error
+     * at this point.
+     */
+    Errors::throwSSLExceptionWithSslErrors(env, ssl, sslError.release(), "SSL handshake aborted",
+                                           Errors::throwSSLHandshakeExceptionStr);
+    safeSslClear(ssl);
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake unclean error => exception", ssl);
+    return code;
 }
 
 static void NativeCrypto_ENGINE_SSL_shutdown(JNIEnv* env, jclass, jlong ssl_address, jobject shc) {
@@ -8595,6 +8768,91 @@
     safeSslClear(ssl);
 }
 
+static int doEngineRead(JNIEnv* env, const char* methodName, SSL* ssl, jobject shc, char* destPtr,
+                        int length) {
+    if (shc == nullptr) {
+        Errors::jniThrowNullPointerException(env, "sslHandshakeCallbacks == null");
+        JNI_TRACE("ssl=%p %s => sslHandshakeCallbacks == null", ssl, methodName);
+        return -1;
+    }
+    AppData* appData = toAppData(ssl);
+    if (appData == nullptr) {
+        Errors::throwSSLExceptionStr(env, "Unable to retrieve application data");
+        safeSslClear(ssl);
+        JNI_TRACE("ssl=%p %s => appData == null", ssl, methodName);
+        return -1;
+    }
+    if (!appData->setCallbackState(env, shc, nullptr)) {
+        Errors::throwSSLExceptionStr(env, "Unable to set appdata callback");
+        ERR_clear_error();
+        safeSslClear(ssl);
+        JNI_TRACE("ssl=%p %s => exception", ssl, methodName);
+        return -1;
+    }
+
+    errno = 0;
+
+    int result = SSL_read(ssl, destPtr, length);
+    appData->clearCallbackState();
+    if (env->ExceptionCheck()) {
+        // An exception was thrown by one of the callbacks. Just propagate that exception.
+        safeSslClear(ssl);
+        JNI_TRACE("ssl=%p %s => THROWN_EXCEPTION", ssl, methodName);
+        return -1;
+    }
+
+    OpenSslError sslError(ssl, result);
+    switch (sslError.get()) {
+        case SSL_ERROR_NONE: {
+            // Successfully read at least one byte. Just return the result.
+            break;
+        }
+        case SSL_ERROR_ZERO_RETURN: {
+            // TODO(nmittler): Can this happen with memory BIOs?
+            // Read zero bytes. End of stream reached.
+            Errors::jniThrowException(env, "java/io/EOFException", "Read error");
+            break;
+        }
+        case SSL_ERROR_WANT_READ:
+        case SSL_ERROR_WANT_WRITE: {
+            // Return the negative of these values.
+            result = -sslError.get();
+            break;
+        }
+        case SSL_ERROR_SYSCALL: {
+            // A problem occurred during a system call, but this is not
+            // necessarily an error.
+            if (result == 0) {
+                // TODO(nmittler): Can this happen with memory BIOs?
+                // Connection closed without proper shutdown. Tell caller we
+                // have reached end-of-stream.
+                Errors::jniThrowException(env, "java/io/EOFException", "Read error");
+                break;
+            }
+
+            if (errno == EINTR) {
+                // TODO(nmittler): Can this happen with memory BIOs?
+                // System call has been interrupted. Simply retry.
+                Errors::jniThrowException(env, "java/io/InterruptedIOException", "Read error");
+                break;
+            }
+
+            // Note that for all other system call errors we fall through
+            // to the default case, which results in an Exception.
+            FALLTHROUGH_INTENDED;
+        }
+        default: {
+            // Everything else is basically an error.
+            Errors::throwSSLExceptionWithSslErrors(env, ssl, sslError.release(), "Read error");
+            break;
+        }
+    }
+
+    JNI_TRACE("ssl=%p %s address=%p length=%d shc=%p result=%d", ssl, methodName, destPtr, length,
+              shc, result);
+    return result;
+}
+
 static jint NativeCrypto_ENGINE_SSL_read_direct(JNIEnv* env, jclass, jlong sslRef, jlong address,
                                                 jint length, jobject shc) {
     SSL* ssl = to_SSL(env, sslRef, true);
@@ -8605,36 +8863,7 @@
     JNI_TRACE("ssl=%p NativeCrypto_ENGINE_SSL_read_direct address=%p length=%d shc=%p", ssl,
               destPtr, length, shc);
 
-    if (shc == nullptr) {
-        Errors::jniThrowNullPointerException(env, "sslHandshakeCallbacks == null");
-        JNI_TRACE("ssl=%p NativeCrypto_ENGINE_SSL_read_direct => sslHandshakeCallbacks == null",
-                  ssl);
-        return -1;
-    }
-
-    AppData* appData = toAppData(ssl);
-    if (appData == nullptr) {
-        Errors::throwSSLExceptionStr(env, "Unable to retrieve application data");
-        safeSslClear(ssl);
-        JNI_TRACE("ssl=%p NativeCrypto_ENGINE_SSL_read_direct => appData == null", ssl);
-        return -1;
-    }
-    if (!appData->setCallbackState(env, shc, nullptr)) {
-        Errors::throwSSLExceptionStr(env, "Unable to set appdata callback");
-        ERR_clear_error();
-        safeSslClear(ssl);
-        JNI_TRACE("ssl=%p NativeCrypto_ENGINE_SSL_read_direct => exception", ssl);
-        return -1;
-    }
-
-    errno = 0;
-
-    int result = SSL_read(ssl, destPtr, length);
-    appData->clearCallbackState();
-
-    JNI_TRACE("ssl=%p NativeCrypto_ENGINE_SSL_read_direct address=%p length=%d shc=%p result=%d",
-              ssl, destPtr, length, shc, result);
-    return result;
+    return doEngineRead(env, "NativeCrypto_ENGINE_SSL_read_direct", ssl, shc, destPtr, length);
 }
 
 static jint NativeCrypto_ENGINE_SSL_read_heap(JNIEnv* env, jclass, jlong sslRef,
@@ -8644,11 +8873,6 @@
     if (ssl == nullptr) {
         return -1;
     }
-    if (shc == nullptr) {
-        Errors::jniThrowNullPointerException(env, "sslHandshakeCallbacks == null");
-        JNI_TRACE("ssl=%p NativeCrypto_ENGINE_SSL_read_heap => sslHandshakeCallbacks == null", ssl);
-        return -1;
-    }
     ScopedByteArrayRW dest(env, destJava);
     if (dest.get() == nullptr) {
         JNI_TRACE("ssl=%p NativeCrypto_ENGINE_SSL_read_heap => threw exception", ssl);
@@ -8663,32 +8887,8 @@
         return -1;
     }
 
-    AppData* appData = toAppData(ssl);
-    if (appData == nullptr) {
-        Errors::throwSSLExceptionStr(env, "Unable to retrieve application data");
-        safeSslClear(ssl);
-        ERR_clear_error();
-        JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake_netty appData => null", ssl);
-        return -1;
-    }
-    if (!appData->setCallbackState(env, shc, nullptr)) {
-        Errors::throwSSLExceptionStr(env, "Unable to set appdata callback");
-        ERR_clear_error();
-        safeSslClear(ssl);
-        JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake_netty => exception", ssl);
-        return -1;
-    }
-
-    errno = 0;
-
-    int result = SSL_read(ssl, reinterpret_cast<char*>(dest.get()) + destOffset, destLength);
-    appData->clearCallbackState();
-
-    JNI_TRACE(
-            "ssl=%p NativeCrypto_ENGINE_SSL_read_heap dest=%p destOffset=%d destLength=%d shc=%p "
-            "=> ret=%d",
-            ssl, dest.get(), destOffset, destLength, shc, result);
-    return result;
+    return doEngineRead(env, "NativeCrypto_ENGINE_SSL_read_heap", ssl, shc,
+                        reinterpret_cast<char*>(dest.get()) + destOffset, destLength);
 }
 
 static int NativeCrypto_ENGINE_SSL_write_BIO_direct(JNIEnv* env, jclass, jlong sslRef, jlong bioRef,
@@ -9004,7 +9204,7 @@
         Errors::throwSSLExceptionStr(env, "Unable to set appdata callback");
         ERR_clear_error();
         safeSslClear(ssl);
-        JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake_netty => exception", ssl);
+        JNI_TRACE("ssl=%p NativeCrypto_ENGINE_SSL_write_heap => exception", ssl);
         return -1;
     }
 
@@ -9020,6 +9220,127 @@
     return result;
 }
 
+// TESTING METHODS BEGIN
+
+static int NativeCrypto_BIO_read(JNIEnv* env, jclass, jlong bioRef, jbyteArray outputJavaBytes) {
+    BIO* bio = reinterpret_cast<BIO*>(static_cast<uintptr_t>(bioRef));
+    JNI_TRACE("BIO_read(%p, %p)", bio, outputJavaBytes);
+
+    if (outputJavaBytes == nullptr) {
+        Errors::jniThrowNullPointerException(env, "output == null");
+        JNI_TRACE("BIO_read(%p, %p) => output == null", bio, outputJavaBytes);
+        return 0;
+    }
+
+    jsize outputSize = env->GetArrayLength(outputJavaBytes);
+
+    std::unique_ptr<unsigned char[]> buffer(
+            new unsigned char[static_cast<unsigned int>(outputSize)]);
+    if (buffer.get() == nullptr) {
+        Errors::jniThrowOutOfMemory(env, "Unable to allocate buffer for read");
+        return 0;
+    }
+
+    int read = BIO_read(bio, buffer.get(), static_cast<int>(outputSize));
+    if (read <= 0) {
+        Errors::throwIOException(env, "BIO_read");
+        JNI_TRACE("BIO_read(%p, %p) => threw IO exception", bio, outputJavaBytes);
+        return 0;
+    }
+
+    env->SetByteArrayRegion(outputJavaBytes, 0, read, reinterpret_cast<jbyte*>(buffer.get()));
+    JNI_TRACE("BIO_read(%p, %p) => %d", bio, outputJavaBytes, read);
+    return read;
+}
+
+static void NativeCrypto_BIO_write(JNIEnv* env, jclass, jlong bioRef, jbyteArray inputJavaBytes,
+        jint offset, jint length) {
+    BIO* bio = reinterpret_cast<BIO*>(static_cast<uintptr_t>(bioRef));
+    JNI_TRACE("BIO_write(%p, %p, %d, %d)", bio, inputJavaBytes, offset, length);
+
+    if (inputJavaBytes == nullptr) {
+        Errors::jniThrowNullPointerException(env, "input == null");
+        return;
+    }
+
+    int inputSize = env->GetArrayLength(inputJavaBytes);
+    if (offset < 0 || offset > inputSize || length < 0 || length > inputSize - offset) {
+        Errors::jniThrowException(env, "java/lang/ArrayIndexOutOfBoundsException", "inputJavaBytes");
+        JNI_TRACE("BIO_write(%p, %p, %d, %d) => IOOB", bio, inputJavaBytes, offset, length);
+        return;
+    }
+
+    std::unique_ptr<unsigned char[]> buffer(new unsigned char[static_cast<unsigned int>(length)]);
+    if (buffer.get() == nullptr) {
+        Errors::jniThrowOutOfMemory(env, "Unable to allocate buffer for write");
+        return;
+    }
+
+    env->GetByteArrayRegion(inputJavaBytes, offset, length, reinterpret_cast<jbyte*>(buffer.get()));
+    if (BIO_write(bio, buffer.get(), length) != length) {
+        ERR_clear_error();
+        Errors::throwIOException(env, "BIO_write");
+        JNI_TRACE("BIO_write(%p, %p, %d, %d) => IO error", bio, inputJavaBytes, offset, length);
+        return;
+    }
+
+    JNI_TRACE("BIO_write(%p, %p, %d, %d) => success", bio, inputJavaBytes, offset, length);
+}
+
+/**
+ * public static native long SSL_clear_mode(long ssl, long mode);
+ */
+static jlong NativeCrypto_SSL_clear_mode(JNIEnv* env, jclass,
+        jlong ssl_address, jlong mode) {
+    SSL* ssl = to_SSL(env, ssl_address, true);
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_clear_mode mode=0x%llx", ssl, (long long) mode);
+    if (ssl == nullptr) {
+        return 0;
+    }
+    long result = static_cast<long>(SSL_clear_mode(ssl, static_cast<uint32_t>(mode)));
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_clear_mode => 0x%lx", ssl, result);
+    return result;
+}
+
+/**
+ * public static native long SSL_get_mode(long ssl);
+ */
+static jlong NativeCrypto_SSL_get_mode(JNIEnv* env, jclass, jlong ssl_address) {
+    SSL* ssl = to_SSL(env, ssl_address, true);
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_mode", ssl);
+    if (ssl == nullptr) {
+        return 0;
+    }
+    long mode = static_cast<long>(SSL_get_mode(ssl));
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_mode => 0x%lx", ssl, mode);
+    return mode;
+}
+
+/**
+ * public static native long SSL_get_options(long ssl);
+ */
+static jlong NativeCrypto_SSL_get_options(JNIEnv* env, jclass,
+        jlong ssl_address) {
+    SSL* ssl = to_SSL(env, ssl_address, true);
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_options", ssl);
+    if (ssl == nullptr) {
+        return 0;
+    }
+    long options = static_cast<long>(SSL_get_options(ssl));
+    JNI_TRACE("ssl=%p NativeCrypto_SSL_get_options => 0x%lx", ssl, options);
+    return options;
+}
+
+static jlong NativeCrypto_SSL_get1_session(JNIEnv* env, jclass, jlong ssl_address) {
+    SSL* ssl = to_SSL(env, ssl_address, true);
+    if (ssl == nullptr) {
+        return 0;
+    }
+    return reinterpret_cast<uintptr_t>(SSL_get1_session(ssl));
+}
+
+// TESTING METHODS END
+
 #define CONSCRYPT_NATIVE_METHOD(className, functionName, signature) \
     {                                                               \
         (char*)#functionName, (char*)(signature),                   \
@@ -9043,15 +9364,14 @@
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_PKEY_new_RSA, "([B[B[B[B[B[B[B[B)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_PKEY_new_EC_KEY, "(" REF_EC_GROUP REF_EC_POINT "[B)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_PKEY_type, "(" REF_EVP_PKEY ")I"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_PKEY_size, "(" REF_EVP_PKEY ")I"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_PKEY_print_public, "(" REF_EVP_PKEY ")Ljava/lang/String;"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_PKEY_print_params, "(" REF_EVP_PKEY ")Ljava/lang/String;"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_PKEY_free, "(J)V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_PKEY_cmp, "(" REF_EVP_PKEY REF_EVP_PKEY ")I"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, i2d_PKCS8_PRIV_KEY_INFO, "(" REF_EVP_PKEY ")[B"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, d2i_PKCS8_PRIV_KEY_INFO, "([B)J"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, i2d_PUBKEY, "(" REF_EVP_PKEY ")[B"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, d2i_PUBKEY, "([B)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_marshal_private_key, "(" REF_EVP_PKEY ")[B"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_parse_private_key, "([B)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_marshal_public_key, "(" REF_EVP_PKEY ")[B"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_parse_public_key, "([B)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, PEM_read_bio_PUBKEY, "(J)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, PEM_read_bio_PrivateKey, "(J)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, getRSAPrivateKeyWrapper, "(Ljava/security/PrivateKey;[B)J"),
@@ -9098,7 +9418,6 @@
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_DigestUpdateDirect, "(" REF_EVP_MD_CTX "JI)V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_DigestFinal_ex, "(" REF_EVP_MD_CTX "[BI)I"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_get_digestbyname, "(Ljava/lang/String;)J"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_MD_block_size, "(J)I"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_MD_size, "(J)I"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_DigestSignInit, "(" REF_EVP_MD_CTX "J" REF_EVP_PKEY ")J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_DigestSignUpdate, "(" REF_EVP_MD_CTX "[BII)V"),
@@ -9134,7 +9453,6 @@
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_aead_aes_256_gcm, "()J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_AEAD_max_overhead, "(J)I"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_AEAD_nonce_length, "(J)I"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_AEAD_max_tag_len, "(J)I"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_AEAD_CTX_seal, "(J[BI[BI[B[BII[B)I"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_AEAD_CTX_open, "(J[BI[BI[B[BII[B)I"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, HMAC_CTX_new, "()J"),
@@ -9144,15 +9462,9 @@
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, HMAC_UpdateDirect, "(" REF_HMAC_CTX "JI)V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, HMAC_Final, "(" REF_HMAC_CTX ")[B"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, RAND_bytes, "([B)V"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, OBJ_txt2nid, "(Ljava/lang/String;)I"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, OBJ_txt2nid_longName, "(Ljava/lang/String;)Ljava/lang/String;"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, OBJ_txt2nid_oid, "(Ljava/lang/String;)Ljava/lang/String;"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, create_BIO_InputStream, ("(" REF_BIO_IN_STREAM "Z)J")),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, create_BIO_OutputStream, "(Ljava/io/OutputStream;)J"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, BIO_read, "(J[B)I"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, BIO_write, "(J[BII)V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, BIO_free_all, "(J)V"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, X509_NAME_print_ex, "(JJ)Ljava/lang/String;"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, d2i_X509_bio, "(J)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, d2i_X509, "([B)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, i2d_X509, "(J)[B"),
@@ -9221,10 +9533,24 @@
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, i2d_X509_REVOKED, "(J)[B"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, X509_supported_extension, "(J)I"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, ASN1_TIME_to_Calendar, "(JLjava/util/Calendar;)V"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, asn1_read_init, "([B)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, asn1_read_sequence, "(J)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, asn1_read_octetstring, "(J)[B"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, asn1_read_uint64, "(J)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, asn1_read_is_empty, "(J)Z"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, asn1_read_free, "(J)V"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, asn1_write_init, "()J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, asn1_write_sequence, "(J)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, asn1_write_octetstring, "(J[B)V"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, asn1_write_uint64, "(JJ)V"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, asn1_write_cleanup, "(J)V"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, asn1_write_finish, "(J)[B"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, asn1_write_free, "(J)V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, EVP_has_aes_hardware, "()I"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_CTX_new, "()J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_CTX_free, "(J)V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_CTX_set_session_id_context, "(J[B)V"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_CTX_set_timeout, "(JJ)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_new, "(J)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_enable_tls_channel_id, "(J)V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get_tls_channel_id, "(J)[B"),
@@ -9233,10 +9559,7 @@
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_use_certificate, "(J[J)V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_check_private_key, "(J)V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_set_client_CA_list, "(J[[B)V"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get_mode, "(J)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_set_mode, "(JJ)J"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_clear_mode, "(JJ)J"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get_options, "(J)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_set_options, "(JJ)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_clear_options, "(JJ)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_enable_signed_cert_timestamps, "(J)V"),
@@ -9260,7 +9583,8 @@
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_set_tlsext_host_name, "(JLjava/lang/String;)V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get_servername, "(J)Ljava/lang/String;"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_do_handshake, "(J" FILE_DESCRIPTOR SSL_CALLBACKS "I)V"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_renegotiate, "(J)V"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get_current_cipher, "(J)Ljava/lang/String;"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get_version, "(J)Ljava/lang/String;"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get_certificate, "(J)[J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get_peer_cert_chain, "(J)[J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_read, "(J" FILE_DESCRIPTOR SSL_CALLBACKS "[BIII)I"),
@@ -9272,9 +9596,14 @@
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_free, "(J)V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_SESSION_session_id, "(J)[B"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_SESSION_get_time, "(J)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get_time, "(J)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_set_timeout, "(JJ)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get_timeout, "(J)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_SESSION_get_timeout, "(J)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_session_id, "(J)[B"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_SESSION_get_version, "(J)Ljava/lang/String;"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_SESSION_cipher, "(J)Ljava/lang/String;"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, get_SSL_SESSION_tlsext_hostname, "(J)Ljava/lang/String;"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_SESSION_up_ref, "(J)V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_SESSION_free, "(J)V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, i2d_SSL_SESSION, "(J)[B"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, d2i_SSL_SESSION, "([B)J"),
@@ -9285,8 +9614,6 @@
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, get_ocsp_single_extension, "([BLjava/lang/String;JJ)[B"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, getDirectBufferAddress, "(Ljava/nio/Buffer;)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_BIO_new, "(J)J"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get0_session, "(J)J"),
-        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get1_session, "(J)J"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_max_seal_overhead, "(J)I"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_clear_error, "()V"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_pending_readable_bytes, "(J)I"),
@@ -9305,6 +9632,14 @@
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, ENGINE_SSL_write_BIO_heap, "(JJ[BII" SSL_CALLBACKS ")I"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, ENGINE_SSL_read_BIO_heap, "(JJ[BII" SSL_CALLBACKS ")I"),
         CONSCRYPT_NATIVE_METHOD(NativeCrypto, ENGINE_SSL_shutdown, "(J" SSL_CALLBACKS ")V"),
+
+        // Used for testing only.
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, BIO_read, "(J[B)I"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, BIO_write, "(J[BII)V"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_clear_mode, "(JJ)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get_mode, "(J)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get_options, "(J)J"),
+        CONSCRYPT_NATIVE_METHOD(NativeCrypto, SSL_get1_session, "(J)J"),
 };
 
 void NativeCrypto::registerNativeMethods(JNIEnv* env) {
diff --git a/common/src/main/java/org/conscrypt/AbstractConscryptSocket.java b/common/src/main/java/org/conscrypt/AbstractConscryptSocket.java
new file mode 100644
index 0000000..df6fbaa
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/AbstractConscryptSocket.java
@@ -0,0 +1,697 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import static org.conscrypt.Preconditions.checkArgument;
+import static org.conscrypt.Preconditions.checkNotNull;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.channels.SocketChannel;
+import java.security.PrivateKey;
+import java.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.HandshakeCompletedEvent;
+import javax.net.ssl.HandshakeCompletedListener;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+/**
+ * Abstract base class for all Conscrypt sockets that extends the basic {@link SSLSocket} API.
+ */
+abstract class AbstractConscryptSocket extends SSLSocket {
+    final Socket socket;
+    private final boolean autoClose;
+
+    /**
+     * The peer's DNS hostname if it was supplied during creation. Note that
+     * this may be a raw IP address, so it should be checked before use with
+     * extensions that don't use it like Server Name Indication (SNI).
+     */
+    private String peerHostname;
+
+    /**
+     * The peer's port if it was supplied during creation. Should only be set if
+     * {@link #peerHostname} is also set.
+     */
+    private final int peerPort;
+
+    private final PeerInfoProvider peerInfoProvider = new PeerInfoProvider() {
+        @Override
+        String getHostname() {
+            return AbstractConscryptSocket.this.getHostname();
+        }
+
+        @Override
+        String getHostnameOrIP() {
+            return AbstractConscryptSocket.this.getHostnameOrIP();
+        }
+
+        @Override
+        int getPort() {
+            return AbstractConscryptSocket.this.getPort();
+        }
+    };
+
+    private final List<HandshakeCompletedListener> listeners =
+            new ArrayList<HandshakeCompletedListener>(2);
+
+    /**
+     * Local cache of timeout to avoid getsockopt on every read and
+     * write for non-wrapped sockets. Note that this is not used when delegating
+     * to another socket.
+     */
+    private int readTimeoutMilliseconds;
+
+    AbstractConscryptSocket() throws IOException {
+        this.socket = this;
+        this.peerHostname = null;
+        this.peerPort = -1;
+        this.autoClose = false;
+    }
+
+    AbstractConscryptSocket(String hostname, int port) throws IOException {
+        super(hostname, port);
+        this.socket = this;
+        this.peerHostname = hostname;
+        this.peerPort = port;
+        this.autoClose = false;
+    }
+
+    AbstractConscryptSocket(InetAddress address, int port) throws IOException {
+        super(address, port);
+        this.socket = this;
+        this.peerHostname = null;
+        this.peerPort = -1;
+        this.autoClose = false;
+    }
+
+    AbstractConscryptSocket(String hostname, int port, InetAddress clientAddress, int clientPort)
+            throws IOException {
+        super(hostname, port, clientAddress, clientPort);
+        this.socket = this;
+        this.peerHostname = hostname;
+        this.peerPort = port;
+        this.autoClose = false;
+    }
+
+    AbstractConscryptSocket(InetAddress address, int port, InetAddress clientAddress,
+            int clientPort) throws IOException {
+        super(address, port, clientAddress, clientPort);
+        this.socket = this;
+        this.peerHostname = null;
+        this.peerPort = -1;
+        this.autoClose = false;
+    }
+
+    AbstractConscryptSocket(Socket socket, String hostname, int port, boolean autoClose)
+            throws IOException {
+        this.socket = checkNotNull(socket, "socket");
+        this.peerHostname = hostname;
+        this.peerPort = port;
+        this.autoClose = autoClose;
+    }
+
+    @Override
+    public final void connect(SocketAddress endpoint) throws IOException {
+        connect(endpoint, 0);
+    }
+
+    /**
+     * Try to extract the peer's hostname if it's available from the endpoint address.
+     */
+    @Override
+    public final void connect(SocketAddress endpoint, int timeout) throws IOException {
+        if (peerHostname == null && endpoint instanceof InetSocketAddress) {
+            peerHostname =
+                    Platform.getHostStringFromInetSocketAddress((InetSocketAddress) endpoint);
+        }
+
+        if (isDelegating()) {
+            socket.connect(endpoint, timeout);
+        } else {
+            super.connect(endpoint, timeout);
+        }
+    }
+
+    @Override
+    public void bind(SocketAddress bindpoint) throws IOException {
+        if (isDelegating()) {
+            socket.bind(bindpoint);
+        } else {
+            super.bind(bindpoint);
+        }
+    }
+
+    @Override
+    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
+    public void close() throws IOException {
+        if (isDelegating()) {
+            if (autoClose && !socket.isClosed()) {
+                socket.close();
+            }
+        } else {
+            if (!super.isClosed()) {
+                super.close();
+            }
+        }
+    }
+
+    @Override
+    public InetAddress getInetAddress() {
+        if (isDelegating()) {
+            return socket.getInetAddress();
+        }
+        return super.getInetAddress();
+    }
+
+    @Override
+    public InetAddress getLocalAddress() {
+        if (isDelegating()) {
+            return socket.getLocalAddress();
+        }
+        return super.getLocalAddress();
+    }
+
+    @Override
+    public int getLocalPort() {
+        if (isDelegating()) {
+            return socket.getLocalPort();
+        }
+        return super.getLocalPort();
+    }
+
+    @Override
+    public SocketAddress getRemoteSocketAddress() {
+        if (isDelegating()) {
+            return socket.getRemoteSocketAddress();
+        }
+        return super.getRemoteSocketAddress();
+    }
+
+    @Override
+    public SocketAddress getLocalSocketAddress() {
+        if (isDelegating()) {
+            return socket.getLocalSocketAddress();
+        }
+        return super.getLocalSocketAddress();
+    }
+
+    @Override
+    public final int getPort() {
+        if (isDelegating()) {
+            return socket.getPort();
+        }
+
+        if (peerPort != -1) {
+            // Return the port that has been explicitly set in the constructor.
+            return peerPort;
+        }
+        return super.getPort();
+    }
+
+    @Override
+    public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {
+        checkArgument(listener != null, "Provided listener is null");
+        listeners.add(listener);
+    }
+
+    @Override
+    public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {
+        checkArgument(listener != null, "Provided listener is null");
+        if (!listeners.remove(listener)) {
+            throw new IllegalArgumentException("Provided listener is not registered");
+        }
+    }
+
+    /* @Override */
+    @SuppressWarnings("MissingOverride") // For compilation with Java 6.
+    public abstract SSLSession getHandshakeSession();
+
+    /* @Override */
+    public FileDescriptor getFileDescriptor$() {
+        if (isDelegating()) {
+            return Platform.getFileDescriptor(socket);
+        }
+        return Platform.getFileDescriptorFromSSLSocket(this);
+    }
+
+    @Override
+    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
+    public final void setSoTimeout(int readTimeoutMilliseconds) throws SocketException {
+        if (isDelegating()) {
+            socket.setSoTimeout(readTimeoutMilliseconds);
+        } else {
+            super.setSoTimeout(readTimeoutMilliseconds);
+            this.readTimeoutMilliseconds = readTimeoutMilliseconds;
+        }
+    }
+
+    @Override
+    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
+    public final int getSoTimeout() throws SocketException {
+        if (isDelegating()) {
+            return socket.getSoTimeout();
+        }
+        return readTimeoutMilliseconds;
+    }
+
+    @Override
+    public final void sendUrgentData(int data) throws IOException {
+        throw new SocketException("Method sendUrgentData() is not supported.");
+    }
+
+    @Override
+    public final void setOOBInline(boolean on) throws SocketException {
+        throw new SocketException("Method setOOBInline() is not supported.");
+    }
+
+    @Override
+    public boolean getOOBInline() throws SocketException {
+        return false;
+    }
+
+    @Override
+    public SocketChannel getChannel() {
+        // TODO(nmittler): Support channels?
+        return null;
+    }
+
+    @Override
+    public InputStream getInputStream() throws IOException {
+        if (isDelegating()) {
+            return socket.getInputStream();
+        }
+        return super.getInputStream();
+    }
+
+    @Override
+    public OutputStream getOutputStream() throws IOException {
+        if (isDelegating()) {
+            return socket.getOutputStream();
+        }
+        return super.getOutputStream();
+    }
+
+    @Override
+    public void setTcpNoDelay(boolean on) throws SocketException {
+        if (isDelegating()) {
+            socket.setTcpNoDelay(on);
+        } else {
+            super.setTcpNoDelay(on);
+        }
+    }
+
+    @Override
+    public boolean getTcpNoDelay() throws SocketException {
+        if (isDelegating()) {
+            return socket.getTcpNoDelay();
+        }
+        return super.getTcpNoDelay();
+    }
+
+    @Override
+    public void setSoLinger(boolean on, int linger) throws SocketException {
+        if (isDelegating()) {
+            socket.setSoLinger(on, linger);
+        } else {
+            super.setSoLinger(on, linger);
+        }
+    }
+
+    @Override
+    public int getSoLinger() throws SocketException {
+        if (isDelegating()) {
+            return socket.getSoLinger();
+        }
+        return super.getSoLinger();
+    }
+
+    @Override
+    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
+    public void setSendBufferSize(int size) throws SocketException {
+        if (isDelegating()) {
+            socket.setSendBufferSize(size);
+        } else {
+            super.setSendBufferSize(size);
+        }
+    }
+
+    @Override
+    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
+    public int getSendBufferSize() throws SocketException {
+        if (isDelegating()) {
+            return socket.getSendBufferSize();
+        }
+        return super.getSendBufferSize();
+    }
+
+    @Override
+    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
+    public void setReceiveBufferSize(int size) throws SocketException {
+        if (isDelegating()) {
+            socket.setReceiveBufferSize(size);
+        } else {
+            super.setReceiveBufferSize(size);
+        }
+    }
+
+    @Override
+    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
+    public int getReceiveBufferSize() throws SocketException {
+        if (isDelegating()) {
+            return socket.getReceiveBufferSize();
+        }
+        return super.getReceiveBufferSize();
+    }
+
+    @Override
+    public void setKeepAlive(boolean on) throws SocketException {
+        if (isDelegating()) {
+            socket.setKeepAlive(on);
+        } else {
+            super.setKeepAlive(on);
+        }
+    }
+
+    @Override
+    public boolean getKeepAlive() throws SocketException {
+        if (isDelegating()) {
+            return socket.getKeepAlive();
+        }
+        return super.getKeepAlive();
+    }
+
+    @Override
+    public void setTrafficClass(int tc) throws SocketException {
+        if (isDelegating()) {
+            socket.setTrafficClass(tc);
+        } else {
+            super.setTrafficClass(tc);
+        }
+    }
+
+    @Override
+    public int getTrafficClass() throws SocketException {
+        if (isDelegating()) {
+            return socket.getTrafficClass();
+        }
+        return super.getTrafficClass();
+    }
+
+    @Override
+    public void setReuseAddress(boolean on) throws SocketException {
+        if (isDelegating()) {
+            socket.setReuseAddress(on);
+        } else {
+            super.setReuseAddress(on);
+        }
+    }
+
+    @Override
+    public boolean getReuseAddress() throws SocketException {
+        if (isDelegating()) {
+            return socket.getReuseAddress();
+        }
+        return super.getReuseAddress();
+    }
+
+    @Override
+    public void shutdownInput() throws IOException {
+        if (isDelegating()) {
+            socket.shutdownInput();
+        } else {
+            super.shutdownInput();
+        }
+    }
+
+    @Override
+    public void shutdownOutput() throws IOException {
+        if (isDelegating()) {
+            socket.shutdownOutput();
+        } else {
+            super.shutdownOutput();
+        }
+    }
+
+    @Override
+    public boolean isConnected() {
+        if (isDelegating()) {
+            return socket.isConnected();
+        }
+        return super.isConnected();
+    }
+
+    @Override
+    public boolean isBound() {
+        if (isDelegating()) {
+            return socket.isBound();
+        }
+        return super.isBound();
+    }
+
+    @Override
+    public boolean isClosed() {
+        if (isDelegating()) {
+            return socket.isClosed();
+        }
+        return super.isClosed();
+    }
+
+    @Override
+    public boolean isInputShutdown() {
+        if (isDelegating()) {
+            return socket.isInputShutdown();
+        }
+        return super.isInputShutdown();
+    }
+
+    @Override
+    public boolean isOutputShutdown() {
+        if (isDelegating()) {
+            return socket.isOutputShutdown();
+        }
+        return super.isOutputShutdown();
+    }
+
+    @Override
+    public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) {
+        if (isDelegating()) {
+            socket.setPerformancePreferences(connectionTime, latency, bandwidth);
+        } else {
+            super.setPerformancePreferences(connectionTime, latency, bandwidth);
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder("SSL socket over ");
+        if (isDelegating()) {
+            builder.append(socket.toString());
+        } else {
+            builder.append(super.toString());
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Returns the hostname that was supplied during socket creation. No DNS resolution is
+     * attempted before returning the hostname.
+     */
+    String getHostname() {
+        return peerHostname;
+    }
+
+    /**
+     * This method enables Server Name Indication
+     *
+     * @param hostname the desired SNI hostname, or null to disable
+     */
+    void setHostname(String hostname) {
+        peerHostname = hostname;
+    }
+
+    /**
+     * For the purposes of an SSLSession, we want a way to represent the supplied hostname
+     * or the IP address in a textual representation. We do not want to perform reverse DNS
+     * lookups on this address.
+     */
+    String getHostnameOrIP() {
+        if (peerHostname != null) {
+            return peerHostname;
+        }
+
+        InetAddress peerAddress = getInetAddress();
+        if (peerAddress != null) {
+            return peerAddress.getHostAddress();
+        }
+
+        return null;
+    }
+
+    /**
+     * Note write timeouts are not part of the javax.net.ssl.SSLSocket API
+     */
+    void setSoWriteTimeout(int writeTimeoutMilliseconds) throws SocketException {
+        throw new SocketException("Method setSoWriteTimeout() is not supported.");
+    }
+
+    /**
+     * Note write timeouts are not part of the javax.net.ssl.SSLSocket API
+     */
+    int getSoWriteTimeout() throws SocketException {
+        return 0;
+    }
+
+    /**
+     * Set the handshake timeout on this socket.  This timeout is specified in
+     * milliseconds and will be used only during the handshake process.
+     */
+    void setHandshakeTimeout(int handshakeTimeoutMilliseconds) throws SocketException {
+        throw new SocketException("Method setHandshakeTimeout() is not supported.");
+    }
+
+    /**
+     * This method enables session ticket support.
+     *
+     * @param useSessionTickets True to enable session tickets
+     */
+    abstract void setUseSessionTickets(boolean useSessionTickets);
+
+    /**
+     * Enables/disables TLS Channel ID for this server socket.
+     *
+     * <p>This method needs to be invoked before the handshake starts.
+     *
+     * @throws IllegalStateException if this is a client socket or if the handshake has already
+     *         started.
+     */
+    abstract void setChannelIdEnabled(boolean enabled);
+
+    /**
+     * Gets the TLS Channel ID for this server socket. Channel ID is only available once the
+     * handshake completes.
+     *
+     * @return channel ID or {@code null} if not available.
+     *
+     * @throws IllegalStateException if this is a client socket or if the handshake has not yet
+     *         completed.
+     * @throws SSLException if channel ID is available but could not be obtained.
+     */
+    abstract byte[] getChannelId() throws SSLException;
+
+    /**
+     * Sets the {@link PrivateKey} to be used for TLS Channel ID by this client socket.
+     *
+     * <p>This method needs to be invoked before the handshake starts.
+     *
+     * @param privateKey private key (enables TLS Channel ID) or {@code null} for no key (disables
+     *        TLS Channel ID). The private key must be an Elliptic Curve (EC) key based on the NIST
+     *        P-256 curve (aka SECG secp256r1 or ANSI X9.62 prime256v1).
+     *
+     * @throws IllegalStateException if this is a server socket or if the handshake has already
+     *         started.
+     */
+    abstract void setChannelIdPrivateKey(PrivateKey privateKey);
+
+    /**
+     * Returns null always for backward compatibility.
+     */
+    byte[] getNpnSelectedProtocol() {
+        return null;
+    }
+
+    /**
+     * This method does nothing and is kept for backward compatibility.
+     */
+    void setNpnProtocols(byte[] npnProtocols) {}
+
+    /**
+     * Returns the protocol agreed upon by client and server, or {@code null} if
+     * no protocol was agreed upon.
+     */
+    abstract byte[] getAlpnSelectedProtocol();
+
+    /**
+     * Sets the list of ALPN protocols. This method internally converts the protocols to their
+     * wire-format form.
+     *
+     * @param alpnProtocols the list of ALPN protocols
+     * @see #setAlpnProtocols(byte[])
+     */
+    abstract void setAlpnProtocols(String[] alpnProtocols);
+
+    /**
+     * Alternate version of {@link #setAlpnProtocols(String[])} that directly sets the list of
+     * ALPN in the wire-format form used by BoringSSL (length-prefixed 8-bit strings).
+     * Requires that all strings be encoded with US-ASCII.
+     *
+     * @param alpnProtocols the encoded form of the ALPN protocol list
+     * @see #setAlpnProtocols(String[])
+     */
+    abstract void setAlpnProtocols(byte[] alpnProtocols);
+
+    /**
+     * Called by {@link #notifyHandshakeCompletedListeners()} to get the currently active session.
+     * Unlike {@link #getSession()}, this method must not block.
+     */
+    abstract SSLSession getActiveSession();
+
+    final PeerInfoProvider peerInfoProvider() {
+        return peerInfoProvider;
+    }
+
+    final void checkOpen() throws SocketException {
+        if (isClosed()) {
+            throw new SocketException("Socket is closed");
+        }
+    }
+
+    final void notifyHandshakeCompletedListeners() {
+        if (listeners != null && !listeners.isEmpty()) {
+            // notify the listeners
+            HandshakeCompletedEvent event = new HandshakeCompletedEvent(this, getActiveSession());
+            for (HandshakeCompletedListener listener : listeners) {
+                try {
+                    listener.handshakeCompleted(event);
+                } catch (RuntimeException e) {
+                    // The RI runs the handlers in a separate thread,
+                    // which we do not. But we try to preserve their
+                    // behavior of logging a problem and not killing
+                    // the handshaking thread just because a listener
+                    // has a problem.
+                    Thread thread = Thread.currentThread();
+                    thread.getUncaughtExceptionHandler().uncaughtException(thread, e);
+                }
+            }
+        }
+    }
+
+    private boolean isDelegating() {
+        // Checking for null to handle the case of calling virtual methods in the super class
+        // constructor.
+        return socket != null && socket != this;
+    }
+}
diff --git a/common/src/main/java/org/conscrypt/AbstractOpenSSLSession.java b/common/src/main/java/org/conscrypt/AbstractOpenSSLSession.java
deleted file mode 100644
index e5de29a..0000000
--- a/common/src/main/java/org/conscrypt/AbstractOpenSSLSession.java
+++ /dev/null
@@ -1,362 +0,0 @@
-/*
- * Copyright 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.conscrypt;
-
-import java.security.Principal;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSession;
-import javax.net.ssl.SSLSessionBindingEvent;
-import javax.net.ssl.SSLSessionBindingListener;
-import javax.net.ssl.SSLSessionContext;
-import javax.security.cert.CertificateException;
-
-/**
- * Extends the base SSLSession with some methods used exclusively in Conscrypt.
- */
-abstract class AbstractOpenSSLSession implements SSLSession {
-    private final Map<String, Object> values = new HashMap<String, Object>();
-
-    private volatile javax.security.cert.X509Certificate[] peerCertificateChain;
-
-    private AbstractSessionContext sessionContext;
-
-    private boolean isValid = true;
-
-    /**
-     * Class constructor creates an SSL session context given the appropriate
-     * session context.
-     */
-    AbstractOpenSSLSession(AbstractSessionContext sessionContext) {
-        this.sessionContext = sessionContext;
-    }
-
-    protected abstract X509Certificate[] getX509PeerCertificates()
-            throws SSLPeerUnverifiedException;
-
-    protected abstract X509Certificate[] getX509LocalCertificates();
-
-    /**
-     * Throw SSLPeerUnverifiedException on null or empty peerCertificates array
-     */
-    private void checkPeerCertificatesPresent() throws SSLPeerUnverifiedException {
-        X509Certificate[] peerCertificates = getX509PeerCertificates();
-        if (peerCertificates == null || peerCertificates.length == 0) {
-            throw new SSLPeerUnverifiedException("No peer certificates");
-        }
-    }
-
-    /**
-     * Return the identity of the peer in this SSL session
-     * determined via certificate(s).
-     * @return an array of X509 certificates (the peer's one first and then
-     *         eventually that of the certification authority) or null if no
-     *         certificate were used during the SSL connection.
-     * @throws SSLPeerUnverifiedException if either a non-X.509 certificate
-     *         was used (i.e. Kerberos certificates) or the peer could not
-     *         be verified.
-     */
-    @Override
-    public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
-        return getX509PeerCertificates();
-    }
-
-    /**
-     * Returns the certificate(s) of the peer in this SSL session
-     * used in the handshaking phase of the connection.
-     * Please notice hat this method is superseded by
-     * <code>getPeerCertificates()</code>.
-     * @return an array of X509 certificates (the peer's one first and then
-     *         eventually that of the certification authority) or null if no
-     *         certificate were used during the SSL connection.
-     * @throws SSLPeerUnverifiedException if either a non-X.509 certificate
-     *         was used (i.e. Kerberos certificates) or the peer could not
-     *         be verified.
-     */
-    @Override
-    public javax.security.cert.X509Certificate[] getPeerCertificateChain()
-            throws SSLPeerUnverifiedException {
-        checkPeerCertificatesPresent();
-        javax.security.cert.X509Certificate[] result = peerCertificateChain;
-        if (result == null) {
-            // single-check idiom
-            peerCertificateChain = result = createPeerCertificateChain();
-        }
-        return result;
-    }
-
-    /**
-     * Provide a value to initialize the volatile peerCertificateChain
-     * field based on the native SSL_SESSION
-     */
-    private javax.security.cert.X509Certificate[] createPeerCertificateChain()
-            throws SSLPeerUnverifiedException {
-        X509Certificate[] peerCertificates = getX509PeerCertificates();
-        try {
-            javax.security.cert.X509Certificate[] chain =
-                    new javax.security.cert.X509Certificate[peerCertificates.length];
-
-            for (int i = 0; i < peerCertificates.length; i++) {
-                byte[] encoded = peerCertificates[i].getEncoded();
-                chain[i] = javax.security.cert.X509Certificate.getInstance(encoded);
-            }
-            return chain;
-        } catch (CertificateEncodingException e) {
-            SSLPeerUnverifiedException exception = new SSLPeerUnverifiedException(e.getMessage());
-            exception.initCause(exception);
-            throw exception;
-        } catch (CertificateException e) {
-            SSLPeerUnverifiedException exception = new SSLPeerUnverifiedException(e.getMessage());
-            exception.initCause(exception);
-            throw exception;
-        }
-    }
-
-    /**
-     * The identity of the principal that was used by the peer during the SSL
-     * handshake phase is returned by this method.
-     * @return a X500Principal of the last certificate for X509-based
-     *         cipher suites.
-     * @throws SSLPeerUnverifiedException if either a non-X.509 certificate
-     *         was used (i.e. Kerberos certificates) or the peer does not exist.
-     *
-     */
-    @Override
-    public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
-        checkPeerCertificatesPresent();
-        return getX509PeerCertificates()[0].getSubjectX500Principal();
-    }
-
-    /**
-     * Returns the principal (subject) of this concrete SSL session used in the
-     * handshaking phase of the connection.
-     * @return a X509 certificate or null if no principal was defined
-     */
-    @Override
-    public Principal getLocalPrincipal() {
-        X509Certificate[] localCertificates = getX509LocalCertificates();
-        if (localCertificates != null && localCertificates.length > 0) {
-            return localCertificates[0].getSubjectX500Principal();
-        } else {
-            return null;
-        }
-    }
-
-    /**
-     * Returns the certificate(s) of the principal (subject) of this concrete SSL
-     * session used in the handshaking phase of the connection. The OpenSSL
-     * native method supports only RSA certificates.
-     * @return an array of certificates (the local one first and then eventually
-     *         that of the certification authority) or null if no certificate
-     *         were used during the handshaking phase.
-     */
-    @Override
-    public Certificate[] getLocalCertificates() {
-        return getX509LocalCertificates();
-    }
-
-    /**
-     * Returns the largest buffer size for the application's data bound to this
-     * concrete SSL session.
-     * @return the largest buffer size
-     */
-    @Override
-    public int getApplicationBufferSize() {
-        return NativeConstants.SSL3_RT_MAX_PLAIN_LENGTH;
-    }
-
-    /**
-     * Returns the largest SSL/TLS packet size one can expect for this concrete
-     * SSL session.
-     * @return the largest packet size
-     */
-    @Override
-    public int getPacketBufferSize() {
-        return NativeConstants.SSL3_RT_MAX_PACKET_SIZE;
-    }
-
-    /**
-     * Returns the object which is bound to the the input parameter name.
-     * This name is a sort of link to the data of the SSL session's application
-     * layer, if any exists.
-     *
-     * @param name the name of the binding to find.
-     * @return the value bound to that name, or null if the binding does not
-     *         exist.
-     * @throws IllegalArgumentException if the argument is null.
-     */
-    @Override
-    public Object getValue(String name) {
-        if (name == null) {
-            throw new IllegalArgumentException("name == null");
-        }
-        return values.get(name);
-    }
-
-    /**
-     * Returns an array with the names (sort of links) of all the data
-     * objects of the application layer bound into the SSL session.
-     *
-     * @return a non-null (possibly empty) array of names of the data objects
-     *         bound to this SSL session.
-     */
-    @Override
-    public String[] getValueNames() {
-        return values.keySet().toArray(new String[values.size()]);
-    }
-
-    /**
-     * A link (name) with the specified value object of the SSL session's
-     * application layer data is created or replaced. If the new (or existing)
-     * value object implements the <code>SSLSessionBindingListener</code>
-     * interface, that object will be notified in due course.
-     *
-     * @param name the name of the link (no null are
-     *            accepted!)
-     * @param value data object that shall be bound to
-     *            name.
-     * @throws IllegalArgumentException if one or both argument(s) is null.
-     */
-    @Override
-    public void putValue(String name, Object value) {
-        if (name == null || value == null) {
-            throw new IllegalArgumentException("name == null || value == null");
-        }
-        Object old = values.put(name, value);
-        if (value instanceof SSLSessionBindingListener) {
-            ((SSLSessionBindingListener) value).valueBound(new SSLSessionBindingEvent(this, name));
-        }
-        if (old instanceof SSLSessionBindingListener) {
-            ((SSLSessionBindingListener) old).valueUnbound(new SSLSessionBindingEvent(this, name));
-        }
-    }
-
-    /**
-     * Removes a link (name) with the specified value object of the SSL
-     * session's application layer data.
-     *
-     * <p>If the value object implements the <code>SSLSessionBindingListener</code>
-     * interface, the object will receive a <code>valueUnbound</code> notification.
-     *
-     * @param name the name of the link (no null are
-     *            accepted!)
-     * @throws IllegalArgumentException if the argument is null.
-     */
-    @Override
-    public void removeValue(String name) {
-        if (name == null) {
-            throw new IllegalArgumentException("name == null");
-        }
-        Object old = values.remove(name);
-        if (old instanceof SSLSessionBindingListener) {
-            SSLSessionBindingListener listener = (SSLSessionBindingListener) old;
-            listener.valueUnbound(new SSLSessionBindingEvent(this, name));
-        }
-    }
-
-    /**
-     * Returns the context to which the actual SSL session is bound. A SSL
-     * context consists of (1) a possible delegate, (2) a provider and (3) a
-     * protocol.
-     * @return the SSL context used for this session, or null if it is
-     * unavailable.
-     */
-    @Override
-    public SSLSessionContext getSessionContext() {
-        return sessionContext;
-    }
-
-    /**
-     * Returns a boolean flag signaling whether a SSL session is valid
-     * and available for resuming or joining or not.
-     *
-     * @return true if this session may be resumed.
-     */
-    @Override
-    public boolean isValid() {
-        if (!isValid) {
-            return false;
-        }
-        // The session has't yet been invalidated -- check whether it timed out.
-
-        SSLSessionContext context = getSessionContext();
-        if (context == null) {
-            // Session not associated with a context -- no way to tell what its timeout should be.
-            return true;
-        }
-
-        int timeoutSeconds = context.getSessionTimeout();
-        if (timeoutSeconds == 0) {
-            // Infinite timeout -- session still valid
-            return true;
-        }
-
-        long creationTimestampMillis = getCreationTime();
-        long ageSeconds = (System.currentTimeMillis() - creationTimestampMillis) / 1000;
-        // NOTE: The age might be negative if something was/is wrong with the system clock. We time
-        // out such sessions to be safe.
-        if ((ageSeconds >= timeoutSeconds) || (ageSeconds < 0)) {
-            // Session timed out -- no longer valid
-            isValid = false;
-            return false;
-        }
-
-        // Session still valid
-        return true;
-    }
-
-    /**
-     * It invalidates a SSL session forbidding any resumption.
-     */
-    @Override
-    public void invalidate() {
-        isValid = false;
-        sessionContext = null;
-    }
-
-    /**
-     * Returns the name requested by the SNI extension.
-     */
-    public abstract String getRequestedServerName();
-
-    /**
-     * Returns the OCSP stapled response.
-     */
-    public abstract List<byte[]> getStatusResponses();
-
-    /**
-     * Returns the TLS Stapled Certificate Transparency data.
-     */
-    public abstract byte[] getTlsSctData();
-
-    /**
-     * Sets the last accessed time for this session in milliseconds since Jan 1,
-     * 1970 00:00:00 UTC.
-     */
-    public abstract void setLastAccessedTime(long accessTimeMillis);
-
-    /**
-     * Indicates that this session's ID may have changed and should be
-     * re-cached.
-     */
-    abstract void resetId();
-}
diff --git a/common/src/main/java/org/conscrypt/AbstractSessionContext.java b/common/src/main/java/org/conscrypt/AbstractSessionContext.java
index 61d595d..a7171f1 100644
--- a/common/src/main/java/org/conscrypt/AbstractSessionContext.java
+++ b/common/src/main/java/org/conscrypt/AbstractSessionContext.java
@@ -16,19 +16,10 @@
 
 package org.conscrypt;
 
-import java.io.ByteArrayOutputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.nio.BufferUnderflowException;
-import java.nio.ByteBuffer;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import java.util.Enumeration;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import javax.net.ssl.SSLSession;
@@ -50,28 +41,22 @@
 
     final long sslCtxNativePointer = NativeCrypto.SSL_CTX_new();
 
-    /** Identifies OpenSSL sessions. */
-    private static final int OPEN_SSL = 1;
-
-    /** Identifies OpenSSL sessions with OCSP stapled data. */
-    private static final int OPEN_SSL_WITH_OCSP = 2;
-
-    /** Identifies OpenSSL sessions with TLS SCT data. */
-    private static final int OPEN_SSL_WITH_TLS_SCT = 3;
-
     @SuppressWarnings("serial")
-    private final Map<ByteArray, SSLSession> sessions = new LinkedHashMap<ByteArray, SSLSession>() {
-        @Override
-        protected boolean removeEldestEntry(
-                Map.Entry<ByteArray, SSLSession> eldest) {
-            boolean remove = maximumSize > 0 && size() > maximumSize;
-            if (remove) {
-                remove(eldest.getKey());
-                sessionRemoved(eldest.getValue());
-            }
-            return false;
-        }
-    };
+    private final Map<ByteArray, SslSessionWrapper> sessions =
+            new LinkedHashMap<ByteArray, SslSessionWrapper>() {
+                @Override
+                protected boolean removeEldestEntry(
+                        Map.Entry<ByteArray, SslSessionWrapper> eldest) {
+                    // NOTE: does not take into account any session that may have become
+                    // invalid.
+                    if (maximumSize > 0 && size() > maximumSize) {
+                        // Let the subclass know.
+                        onBeforeRemoveSession(eldest.getValue());
+                        return true;
+                    }
+                    return false;
+                }
+            };
 
     /**
      * Constructs a new session context.
@@ -83,29 +68,27 @@
     }
 
     /**
-     * Returns the collection of sessions ordered from oldest to newest
+     * This method is provided for API-compatibility only, not intended for use. No guarantees
+     * are made WRT performance.
      */
-    private Iterator<SSLSession> sessionIterator() {
-        synchronized (sessions) {
-            SSLSession[] array = sessions.values().toArray(
-                    new SSLSession[sessions.size()]);
-            return Arrays.asList(array).iterator();
-        }
-    }
-
     @Override
     public final Enumeration<byte[]> getIds() {
-        final Iterator<SSLSession> i = sessionIterator();
+        // Make a copy of the IDs.
+        final Iterator<SslSessionWrapper> iter;
+        synchronized (sessions) {
+            iter = Arrays.asList(sessions.values().toArray(new SslSessionWrapper[sessions.size()]))
+                    .iterator();
+        }
         return new Enumeration<byte[]>() {
-            private SSLSession next;
+            private SslSessionWrapper next;
 
             @Override
             public boolean hasMoreElements() {
                 if (next != null) {
                     return true;
                 }
-                while (i.hasNext()) {
-                    SSLSession session = i.next();
+                while (iter.hasNext()) {
+                    SslSessionWrapper session = iter.next();
                     if (session.isValid()) {
                         next = session;
                         return true;
@@ -127,6 +110,26 @@
         };
     }
 
+    /**
+     * This is provided for API-compatibility only, not intended for use. No guarantees are
+     * made WRT performance or the validity of the returned session.
+     */
+    @Override
+    public final SSLSession getSession(byte[] sessionId) {
+        if (sessionId == null) {
+            throw new NullPointerException("sessionId");
+        }
+        ByteArray key = new ByteArray(sessionId);
+        SslSessionWrapper session;
+        synchronized (sessions) {
+            session = sessions.get(key);
+        }
+        if (session != null && session.isValid()) {
+            return session.toSSLSession();
+        }
+        return null;
+    }
+
     @Override
     public final int getSessionCacheSize() {
         return maximumSize;
@@ -137,55 +140,33 @@
         return timeout;
     }
 
-    /**
-     * Makes sure cache size is < maximumSize.
-     */
-    private void trimToSize() {
-        synchronized (sessions) {
-            int size = sessions.size();
-            if (size > maximumSize) {
-                int removals = size - maximumSize;
-                Iterator<SSLSession> i = sessions.values().iterator();
-                do {
-                    SSLSession session = i.next();
-                    i.remove();
-                    sessionRemoved(session);
-                } while (--removals > 0);
-            }
-        }
-    }
-
     @Override
-    public void setSessionTimeout(int seconds)
-            throws IllegalArgumentException {
+    public final void setSessionTimeout(int seconds) throws IllegalArgumentException {
         if (seconds < 0) {
             throw new IllegalArgumentException("seconds < 0");
         }
-        timeout = seconds;
 
         synchronized (sessions) {
-            Iterator<SSLSession> i = sessions.values().iterator();
+            // Set the timeout on this context.
+            timeout = seconds;
+            NativeCrypto.SSL_CTX_set_timeout(sslCtxNativePointer, seconds);
+
+            Iterator<SslSessionWrapper> i = sessions.values().iterator();
             while (i.hasNext()) {
-                SSLSession session = i.next();
+                SslSessionWrapper session = i.next();
                 // SSLSession's know their context and consult the
                 // timeout as part of their validity condition.
                 if (!session.isValid()) {
+                    // Let the subclass know.
+                    onBeforeRemoveSession(session);
                     i.remove();
-                    sessionRemoved(session);
                 }
             }
         }
     }
 
-    /**
-     * Called when a session is removed. Used by ClientSessionContext
-     * to update its host-and-port based cache.
-     */
-    protected abstract void sessionRemoved(SSLSession session);
-
     @Override
-    public final void setSessionCacheSize(int size)
-            throws IllegalArgumentException {
+    public final void setSessionCacheSize(int size) throws IllegalArgumentException {
         if (size < 0) {
             throw new IllegalArgumentException("size < 0");
         }
@@ -199,202 +180,6 @@
         }
     }
 
-    /**
-     * Converts the given session to bytes.
-     *
-     * @return session data as bytes or null if the session can't be converted
-     */
-    byte[] toBytes(SSLSession session) {
-        // TODO: Support SSLSessionImpl, too.
-        if (!(session instanceof OpenSSLSessionImpl)) {
-            return null;
-        }
-
-        OpenSSLSessionImpl sslSession = (OpenSSLSessionImpl) session;
-        try {
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            DataOutputStream daos = new DataOutputStream(baos);
-
-            daos.writeInt(OPEN_SSL_WITH_TLS_SCT); // session type ID
-
-            // Session data.
-            byte[] data = sslSession.getEncoded();
-            daos.writeInt(data.length);
-            daos.write(data);
-
-            // Certificates.
-            Certificate[] certs = session.getPeerCertificates();
-            daos.writeInt(certs.length);
-
-            for (Certificate cert : certs) {
-                data = cert.getEncoded();
-                daos.writeInt(data.length);
-                daos.write(data);
-            }
-
-            List<byte[]> ocspResponses = sslSession.getStatusResponses();
-            daos.writeInt(ocspResponses.size());
-            for (byte[] ocspResponse : ocspResponses) {
-                daos.writeInt(ocspResponse.length);
-                daos.write(ocspResponse);
-            }
-
-            byte[] tlsSctData = sslSession.getTlsSctData();
-            if (tlsSctData != null) {
-                daos.writeInt(tlsSctData.length);
-                daos.write(tlsSctData);
-            } else {
-                daos.writeInt(0);
-            }
-
-            // TODO: local certificates?
-
-            return baos.toByteArray();
-        } catch (IOException e) {
-            System.err.println("Failed to convert saved SSL Session: " + e.getMessage());
-            return null;
-        } catch (CertificateEncodingException e) {
-            log(e);
-            return null;
-        }
-    }
-
-    private static void checkRemaining(ByteBuffer buf, int length) throws IOException {
-        if (length < 0) {
-            throw new IOException("Length is negative: " + length);
-        }
-        if (length > buf.remaining()) {
-            throw new IOException(
-                    "Length of blob is longer than available: " + length + " > " + buf.remaining());
-        }
-    }
-
-    /**
-     * Creates a session from the given bytes.
-     *
-     * @return a session or null if the session can't be converted
-     */
-    OpenSSLSessionImpl toSession(byte[] data, String host, int port) {
-        ByteBuffer buf = ByteBuffer.wrap(data);
-        try {
-            int type = buf.getInt();
-            if (type != OPEN_SSL && type != OPEN_SSL_WITH_OCSP && type != OPEN_SSL_WITH_TLS_SCT) {
-                throw new IOException("Unexpected type ID: " + type);
-            }
-
-            int length = buf.getInt();
-            checkRemaining(buf, length);
-
-            byte[] sessionData = new byte[length];
-            buf.get(sessionData);
-
-            int count = buf.getInt();
-            checkRemaining(buf, count);
-
-            X509Certificate[] certs = new X509Certificate[count];
-            for (int i = 0; i < count; i++) {
-                length = buf.getInt();
-                checkRemaining(buf, length);
-
-                byte[] certData = new byte[length];
-                buf.get(certData);
-                try {
-                    certs[i] = OpenSSLX509Certificate.fromX509Der(certData);
-                } catch (Exception e) {
-                    throw new IOException("Can not read certificate " + i + "/" + count);
-                }
-            }
-
-            byte[] ocspData = null;
-            if (type >= OPEN_SSL_WITH_OCSP) {
-                // We only support one OCSP response now, but in the future
-                // we may support RFC 6961 which has multiple.
-                int countOcspResponses = buf.getInt();
-                checkRemaining(buf, countOcspResponses);
-
-                if (countOcspResponses >= 1) {
-                    int ocspLength = buf.getInt();
-                    checkRemaining(buf, ocspLength);
-
-                    ocspData = new byte[ocspLength];
-                    buf.get(ocspData);
-
-                    // Skip the rest of the responses.
-                    for (int i = 1; i < countOcspResponses; i++) {
-                        ocspLength = buf.getInt();
-                        checkRemaining(buf, ocspLength);
-                        buf.position(buf.position() + ocspLength);
-                    }
-                }
-            }
-
-            byte[] tlsSctData = null;
-            if (type == OPEN_SSL_WITH_TLS_SCT) {
-                int tlsSctDataLength = buf.getInt();
-                checkRemaining(buf, tlsSctDataLength);
-
-                if (tlsSctDataLength > 0) {
-                    tlsSctData = new byte[tlsSctDataLength];
-                    buf.get(tlsSctData);
-                }
-            }
-
-            if (buf.remaining() != 0) {
-                log(new AssertionError("Read entire session, but data still remains; rejecting"));
-                return null;
-            }
-
-            return new OpenSSLSessionImpl(sessionData, host, port, certs, ocspData, tlsSctData,
-                    this);
-        } catch (IOException e) {
-            log(e);
-            return null;
-        } catch (BufferUnderflowException e) {
-            log(e);
-            return null;
-        }
-    }
-
-    SSLSession wrapSSLSessionIfNeeded(SSLSession session) {
-        if (session instanceof AbstractOpenSSLSession) {
-            return Platform.wrapSSLSession((AbstractOpenSSLSession) session);
-        } else {
-            return session;
-        }
-    }
-
-    @Override
-    public SSLSession getSession(byte[] sessionId) {
-        if (sessionId == null) {
-            throw new NullPointerException("sessionId == null");
-        }
-        ByteArray key = new ByteArray(sessionId);
-        SSLSession session;
-        synchronized (sessions) {
-            session = sessions.get(key);
-        }
-        if (session != null && session.isValid()) {
-            return wrapSSLSessionIfNeeded(session);
-        }
-        return null;
-    }
-
-    void putSession(SSLSession session) {
-        byte[] id = session.getId();
-        if (id.length == 0) {
-            return;
-        }
-        ByteArray key = new ByteArray(id);
-        synchronized (sessions) {
-            sessions.put(key, session);
-        }
-    }
-
-    private static void log(Throwable t) {
-        System.out.println("Error inflating SSL session: "
-                + (t.getMessage() != null ? t.getMessage() : t.getClass().getName()));
-    }
-
     @Override
     protected void finalize() throws Throwable {
         try {
@@ -403,4 +188,85 @@
             super.finalize();
         }
     }
+
+    /**
+     * Adds the given session to the cache.
+     */
+    final void cacheSession(SslSessionWrapper session) {
+        byte[] id = session.getId();
+        if (id == null || id.length == 0) {
+            return;
+        }
+
+        // Let the subclass know.
+        onBeforeAddSession(session);
+
+        ByteArray key = new ByteArray(id);
+        synchronized (sessions) {
+            sessions.put(key, session);
+        }
+    }
+
+    /**
+     * Called for server sessions only. Retrieves the session by its ID. Overridden by
+     * {@link ServerSessionContext} to
+     */
+    final SslSessionWrapper getSessionFromCache(byte[] sessionId) {
+        if (sessionId == null) {
+            return null;
+        }
+
+        // First, look in the in-memory cache.
+        SslSessionWrapper session;
+        synchronized (sessions) {
+            session = sessions.get(new ByteArray(sessionId));
+        }
+        if (session != null && session.isValid()) {
+            return session;
+        }
+
+        // Not found in-memory - look it up in the persistent cache.
+        return getSessionFromPersistentCache(sessionId);
+    }
+
+    /**
+     * Called when the given session is about to be added. Used by {@link ClientSessionContext} to
+     * update its host-and-port based cache.
+     *
+     * <p>Visible for extension only, not intended to be called directly.
+     */
+    abstract void onBeforeAddSession(SslSessionWrapper session);
+
+    /**
+     * Called when a session is about to be removed. Used by {@link ClientSessionContext}
+     * to update its host-and-port based cache.
+     *
+     * <p>Visible for extension only, not intended to be called directly.
+     */
+    abstract void onBeforeRemoveSession(SslSessionWrapper session);
+
+    /**
+     * Called for server sessions only. Retrieves the session by ID from the persistent cache.
+     *
+     * <p>Visible for extension only, not intended to be called directly.
+     */
+    abstract SslSessionWrapper getSessionFromPersistentCache(byte[] sessionId);
+
+    /**
+     * Makes sure cache size is < maximumSize.
+     */
+    private void trimToSize() {
+        synchronized (sessions) {
+            int size = sessions.size();
+            if (size > maximumSize) {
+                int removals = size - maximumSize;
+                Iterator<SslSessionWrapper> i = sessions.values().iterator();
+                while (removals-- > 0) {
+                    SslSessionWrapper session = i.next();
+                    onBeforeRemoveSession(session);
+                    i.remove();
+                }
+            }
+        }
+    }
 }
diff --git a/common/src/main/java/org/conscrypt/ActiveSession.java b/common/src/main/java/org/conscrypt/ActiveSession.java
new file mode 100644
index 0000000..c7f6431
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/ActiveSession.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import static org.conscrypt.Preconditions.checkNotNull;
+
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSessionBindingEvent;
+import javax.net.ssl.SSLSessionBindingListener;
+import javax.net.ssl.SSLSessionContext;
+
+/**
+ * A session that is dedicated a single connection and operates directly on the underlying
+ * {@code SSL}.
+ */
+final class ActiveSession implements SSLSession {
+    private final SslWrapper ssl;
+    private AbstractSessionContext sessionContext;
+    private byte[] id;
+    private long creationTime;
+    private String cipherSuite;
+    private String protocol;
+    private String peerHost;
+    private int peerPort = -1;
+    private long lastAccessedTime = 0;
+    private volatile javax.security.cert.X509Certificate[] peerCertificateChain;
+    private X509Certificate[] localCertificates;
+    private X509Certificate[] peerCertificates;
+    private byte[] peerCertificateOcspData;
+    private byte[] peerTlsSctData;
+
+    // lazy init for memory reasons
+    private Map<String, Object> values;
+
+    ActiveSession(SslWrapper ssl, AbstractSessionContext sessionContext) {
+        this.ssl = checkNotNull(ssl, "ssl");
+        this.sessionContext = checkNotNull(sessionContext, "sessionContext");
+    }
+
+    @Override
+    public byte[] getId() {
+        if (id == null) {
+            id = ssl.getSessionId();
+        }
+        return id != null ? id.clone() : EmptyArray.BYTE;
+    }
+
+    /**
+     * Indicates that this session's ID may have changed and should be re-cached.
+     */
+    void resetId() {
+        id = null;
+    }
+
+    @Override
+    public SSLSessionContext getSessionContext() {
+        return isValid() ? sessionContext : null;
+    }
+
+    @Override
+    public long getCreationTime() {
+        if (creationTime == 0) {
+            creationTime = ssl.getTime();
+        }
+        return creationTime;
+    }
+
+    /**
+     * Returns the last time this SSL session was accessed. Accessing
+     * here is to mean that a new connection with the same SSL context data was
+     * established.
+     *
+     * @return the session's last access time in milliseconds since the epoch
+     */
+    // TODO(nathanmittler): Does lastAccessedTime need to account for session reuse?
+    @Override
+    public long getLastAccessedTime() {
+        return lastAccessedTime == 0 ? getCreationTime() : lastAccessedTime;
+    }
+
+    void setLastAccessedTime(long accessTimeMillis) {
+        lastAccessedTime = accessTimeMillis;
+    }
+
+    /**
+     * Returns the OCSP stapled response. Returns a copy of the internal arrays.
+     *
+     * The method signature matches
+     * <a
+     * href="http://download.java.net/java/jdk9/docs/api/javax/net/ssl/ExtendedSSLSession.html#getStatusResponses--">Java
+     * 9</a>.
+     *
+     * @see <a href="https://tools.ietf.org/html/rfc6066">RFC 6066</a>
+     * @see <a href="https://tools.ietf.org/html/rfc6961">RFC 6961</a>
+     */
+    /* @Override */
+    @SuppressWarnings("MissingOverride") // For Pre-Java9 compatibility.
+    public List<byte[]> getStatusResponses() {
+        if (peerCertificateOcspData == null) {
+            return Collections.<byte[]>emptyList();
+        }
+
+        return Collections.singletonList(peerCertificateOcspData.clone());
+    }
+
+    /**
+     * Returns the signed certificate timestamp (SCT) received from the peer. Returns a
+     * copy of the internal array.
+     *
+     * @see <a href="https://tools.ietf.org/html/rfc6962">RFC 6962</a>
+     */
+    byte[] getPeerSignedCertificateTimestamp() {
+        if (peerTlsSctData == null) {
+            return null;
+        }
+        return peerTlsSctData.clone();
+    }
+
+    String getRequestedServerName() {
+        return ssl.getRequestedServerName();
+    }
+
+    @Override
+    public void invalidate() {
+        ssl.setTimeout(0L);
+    }
+
+    @Override
+    public boolean isValid() {
+        long creationTimeMillis = ssl.getTime();
+        long timeoutMillis = ssl.getTimeout();
+        return (System.currentTimeMillis() - timeoutMillis) < creationTimeMillis;
+    }
+
+    @Override
+    public void putValue(String name, Object value) {
+        if (name == null) {
+            throw new NullPointerException("name");
+        }
+        if (value == null) {
+            throw new NullPointerException("value");
+        }
+        Map<String, Object> values = this.values;
+        if (values == null) {
+            // Use size of 2 to keep the memory overhead small
+            values = this.values = new HashMap<String, Object>(2);
+        }
+        Object old = values.put(name, value);
+        if (value instanceof SSLSessionBindingListener) {
+            ((SSLSessionBindingListener) value).valueBound(new SSLSessionBindingEvent(this, name));
+        }
+        if (old instanceof SSLSessionBindingListener) {
+            ((SSLSessionBindingListener) old).valueUnbound(new SSLSessionBindingEvent(this, name));
+        }
+        notifyUnbound(old, name);
+    }
+
+    @Override
+    public Object getValue(String name) {
+        if (name == null) {
+            throw new NullPointerException("name");
+        }
+        if (values == null) {
+            return null;
+        }
+        return values.get(name);
+    }
+
+    @Override
+    public void removeValue(String name) {
+        if (name == null) {
+            throw new NullPointerException("name");
+        }
+        Map<String, Object> values = this.values;
+        if (values == null) {
+            return;
+        }
+        Object old = values.remove(name);
+        notifyUnbound(old, name);
+    }
+
+    @Override
+    public String[] getValueNames() {
+        Map<String, Object> values = this.values;
+        if (values == null || values.isEmpty()) {
+            return EmptyArray.STRING;
+        }
+        return values.keySet().toArray(new String[values.size()]);
+    }
+
+    @Override
+    public X509Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
+        checkPeerCertificatesPresent();
+        return peerCertificates.clone();
+    }
+
+    @Override
+    public Certificate[] getLocalCertificates() {
+        return localCertificates == null ? null : localCertificates.clone();
+    }
+
+    /**
+     * Returns the certificate(s) of the peer in this SSL session
+     * used in the handshaking phase of the connection.
+     * Please notice hat this method is superseded by
+     * <code>getPeerCertificates()</code>.
+     * @return an array of X509 certificates (the peer's one first and then
+     *         eventually that of the certification authority) or null if no
+     *         certificate were used during the SSL connection.
+     * @throws SSLPeerUnverifiedException if either a non-X.509 certificate
+     *         was used (i.e. Kerberos certificates) or the peer could not
+     *         be verified.
+     */
+    @Override
+    public javax.security.cert.X509Certificate[] getPeerCertificateChain()
+            throws SSLPeerUnverifiedException {
+        checkPeerCertificatesPresent();
+        // TODO(nathanmittler): Should we clone?
+        javax.security.cert.X509Certificate[] result = peerCertificateChain;
+        if (result == null) {
+            // single-check idiom
+            peerCertificateChain = result = SSLUtils.toCertificateChain(peerCertificates);
+        }
+        return result;
+    }
+
+    @Override
+    public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+        checkPeerCertificatesPresent();
+        return peerCertificates[0].getSubjectX500Principal();
+    }
+
+    @Override
+    public Principal getLocalPrincipal() {
+        if (localCertificates != null && localCertificates.length > 0) {
+            return localCertificates[0].getSubjectX500Principal();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public String getCipherSuite() {
+        if (cipherSuite == null) {
+            cipherSuite = ssl.getCipherSuite();
+        }
+        return cipherSuite;
+    }
+
+    @Override
+    public String getProtocol() {
+        String protocol = this.protocol;
+        if (protocol == null) {
+            protocol = ssl.getVersion();
+            this.protocol = protocol;
+        }
+        return protocol;
+    }
+
+    @Override
+    public String getPeerHost() {
+        return peerHost;
+    }
+
+    @Override
+    public int getPeerPort() {
+        return peerPort;
+    }
+
+    @Override
+    public int getPacketBufferSize() {
+        return NativeConstants.SSL3_RT_MAX_PACKET_SIZE;
+    }
+
+    @Override
+    public int getApplicationBufferSize() {
+        return NativeConstants.SSL3_RT_MAX_PLAIN_LENGTH;
+    }
+
+    /**
+     * Configures the peer information once it has been received by the handshake.
+     */
+    void onPeerCertificatesReceived(
+            String peerHost, int peerPort, OpenSSLX509Certificate[] peerCertificates) {
+        configurePeer(peerHost, peerPort, peerCertificates);
+    }
+
+    /**
+     * Configures the peer and local state from a newly created BoringSSL session.
+     */
+    void onSessionEstablished(String peerHost, int peerPort) {
+        id = null;
+        this.localCertificates = ssl.getLocalCertificates();
+        configurePeer(peerHost, peerPort, ssl.getPeerCertificates());
+    }
+
+    private void configurePeer(
+            String peerHost, int peerPort, OpenSSLX509Certificate[] peerCertificates) {
+        this.peerHost = peerHost;
+        this.peerPort = peerPort;
+        this.peerCertificates = peerCertificates;
+        this.peerCertificateOcspData = ssl.getPeerCertificateOcspData();
+        this.peerTlsSctData = ssl.getPeerTlsSctData();
+    }
+
+    private X509Certificate[] getX509PeerCertificates() throws SSLPeerUnverifiedException {
+        if (peerCertificates == null || peerCertificates.length == 0) {
+            throw new SSLPeerUnverifiedException("No peer certificates");
+        }
+        return peerCertificates;
+    }
+
+    /**
+     * Throw SSLPeerUnverifiedException on null or empty peerCertificates array
+     */
+    private void checkPeerCertificatesPresent() throws SSLPeerUnverifiedException {
+        if (peerCertificates == null || peerCertificates.length == 0) {
+            throw new SSLPeerUnverifiedException("No peer certificates");
+        }
+    }
+
+    private void notifyUnbound(Object value, String name) {
+        if (value instanceof SSLSessionBindingListener) {
+            ((SSLSessionBindingListener) value)
+                    .valueUnbound(new SSLSessionBindingEvent(this, name));
+        }
+    }
+}
diff --git a/common/src/main/java/org/conscrypt/AllocatedBuffer.java b/common/src/main/java/org/conscrypt/AllocatedBuffer.java
new file mode 100644
index 0000000..a672678
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/AllocatedBuffer.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed 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.
+ */
+
+/*
+ * Copyright 2013 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 org.conscrypt;
+
+import static org.conscrypt.Preconditions.checkNotNull;
+
+import java.nio.ByteBuffer;
+
+/**
+ * A buffer that was allocated by a {@link BufferAllocator}.
+ */
+@ExperimentalApi
+public abstract class AllocatedBuffer {
+    /**
+     * Returns the {@link ByteBuffer} that backs this buffer.
+     */
+    public abstract ByteBuffer nioBuffer();
+
+    /**
+     * Increases the reference count by {@code 1}.
+     */
+    public abstract AllocatedBuffer retain();
+
+    /**
+     * Decreases the reference count by {@code 1} and deallocates this object if the reference count
+     * reaches at {@code 0}.
+     *
+     * @return {@code true} if and only if the reference count became {@code 0} and this object has
+     * been deallocated
+     */
+    public abstract AllocatedBuffer release();
+
+    /**
+     * Creates a new {@link AllocatedBuffer} that is backed by the given {@link ByteBuffer}.
+     */
+    public static AllocatedBuffer wrap(final ByteBuffer buffer) {
+        checkNotNull(buffer, "buffer");
+
+        return new AllocatedBuffer() {
+
+            @Override
+            public ByteBuffer nioBuffer() {
+                return buffer;
+            }
+
+            @Override
+            public AllocatedBuffer retain() {
+                // Do nothing.
+                return this;
+            }
+
+            @Override
+            public AllocatedBuffer release() {
+                // Do nothing.
+                return this;
+            }
+        };
+    }
+}
diff --git a/common/src/main/java/org/conscrypt/BufferAllocator.java b/common/src/main/java/org/conscrypt/BufferAllocator.java
new file mode 100644
index 0000000..a45544b
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/BufferAllocator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.conscrypt;
+
+import java.nio.ByteBuffer;
+
+/**
+ * An object responsible for allocation of buffers. This is an extension point to enable buffer
+ * pooling within an application.
+ */
+@ExperimentalApi
+public abstract class BufferAllocator {
+    private static BufferAllocator UNPOOLED = new BufferAllocator() {
+        @Override
+        public AllocatedBuffer allocateDirectBuffer(int capacity) {
+            return AllocatedBuffer.wrap(ByteBuffer.allocateDirect(capacity));
+        }
+    };
+
+    /**
+     * Returns an unpooled buffer allocator, which will create a new buffer for each request.
+     */
+    public static BufferAllocator unpooled() {
+        return UNPOOLED;
+    }
+
+    /**
+     * Allocates a direct (i.e. non-heap) buffer with the given capacity.
+     */
+    public abstract AllocatedBuffer allocateDirectBuffer(int capacity);
+}
diff --git a/common/src/main/java/org/conscrypt/ClientSessionContext.java b/common/src/main/java/org/conscrypt/ClientSessionContext.java
index 48bbe8e..6265a7b 100644
--- a/common/src/main/java/org/conscrypt/ClientSessionContext.java
+++ b/common/src/main/java/org/conscrypt/ClientSessionContext.java
@@ -17,7 +17,8 @@
 package org.conscrypt;
 
 import java.util.HashMap;
-import javax.net.ssl.SSLSession;
+import java.util.Map;
+import javax.net.ssl.SSLContext;
 
 /**
  * Caches client sessions. Indexes by host and port. Users are typically
@@ -26,39 +27,70 @@
  * @hide
  */
 @Internal
-public class ClientSessionContext extends AbstractSessionContext {
-
+public final class ClientSessionContext extends AbstractSessionContext {
     /**
      * Sessions indexed by host and port. Protect from concurrent
      * access by holding a lock on sessionsByHostAndPort.
      */
-    private final HashMap<HostAndPort, SSLSession> sessionsByHostAndPort = new HashMap<>();
+    @SuppressWarnings("serial")
+    private final Map<HostAndPort, SslSessionWrapper> sessionsByHostAndPort = new HashMap<>();
 
     private SSLClientSessionCache persistentCache;
 
-    public ClientSessionContext() {
+    ClientSessionContext() {
         super(10);
     }
 
-    public int size() {
-        return sessionsByHostAndPort.size();
-    }
-
+    /**
+     * Applications should not use this method. Instead use {@link
+     * Conscrypt.Contexts#setClientSessionCache(SSLContext, SSLClientSessionCache)}.
+     */
     public void setPersistentCache(SSLClientSessionCache persistentCache) {
         this.persistentCache = persistentCache;
     }
 
-    @Override
-    protected void sessionRemoved(SSLSession session) {
-        String host = session.getPeerHost();
-        int port = session.getPeerPort();
-        if (host == null) {
-            return;
+    /**
+     * Gets the suitable session reference from the session cache container.
+     */
+    SslSessionWrapper getCachedSession(String hostName, int port, SSLParametersImpl sslParameters) {
+        if (hostName == null) {
+            return null;
         }
-        HostAndPort hostAndPortKey = new HostAndPort(host, port);
-        synchronized (sessionsByHostAndPort) {
-            sessionsByHostAndPort.remove(hostAndPortKey);
+
+        SslSessionWrapper session = getSession(hostName, port);
+        if (session == null) {
+            return null;
         }
+
+        String protocol = session.getProtocol();
+        boolean protocolFound = false;
+        for (String enabledProtocol : sslParameters.enabledProtocols) {
+            if (protocol.equals(enabledProtocol)) {
+                protocolFound = true;
+                break;
+            }
+        }
+        if (!protocolFound) {
+            return null;
+        }
+
+        String cipherSuite = session.getCipherSuite();
+        boolean cipherSuiteFound = false;
+        for (String enabledCipherSuite : sslParameters.enabledCipherSuites) {
+            if (cipherSuite.equals(enabledCipherSuite)) {
+                cipherSuiteFound = true;
+                break;
+            }
+        }
+        if (!cipherSuiteFound) {
+            return null;
+        }
+
+        return session;
+    }
+
+    int size() {
+        return sessionsByHostAndPort.size();
     }
 
     /**
@@ -68,30 +100,30 @@
      * @param port of server
      * @return cached session or null if none found
      */
-    public SSLSession getSession(String host, int port) {
+    private SslSessionWrapper getSession(String host, int port) {
         if (host == null) {
             return null;
         }
-        SSLSession session;
-        HostAndPort hostAndPortKey = new HostAndPort(host, port);
+
+        HostAndPort key = new HostAndPort(host, port);
+        SslSessionWrapper session;
         synchronized (sessionsByHostAndPort) {
-            session = sessionsByHostAndPort.get(hostAndPortKey);
+            session = sessionsByHostAndPort.get(key);
         }
         if (session != null && session.isValid()) {
-            return wrapSSLSessionIfNeeded(session);
+            return session;
         }
 
         // Look in persistent cache.
         if (persistentCache != null) {
             byte[] data = persistentCache.getSessionData(host, port);
             if (data != null) {
-                session = toSession(data, host, port);
+                session = SslSessionWrapper.newInstance(this, data, host, port);
                 if (session != null && session.isValid()) {
-                    super.putSession(session);
                     synchronized (sessionsByHostAndPort) {
-                        sessionsByHostAndPort.put(hostAndPortKey, session);
+                        sessionsByHostAndPort.put(key, session);
                     }
-                    return wrapSSLSessionIfNeeded(session);
+                    return session;
                 }
             }
         }
@@ -100,30 +132,47 @@
     }
 
     @Override
-    public void putSession(SSLSession session) {
-        super.putSession(session);
-
+    void onBeforeAddSession(SslSessionWrapper session) {
         String host = session.getPeerHost();
         int port = session.getPeerPort();
         if (host == null) {
             return;
         }
 
-        HostAndPort hostAndPortKey = new HostAndPort(host, port);
+        HostAndPort key = new HostAndPort(host, port);
         synchronized (sessionsByHostAndPort) {
-            sessionsByHostAndPort.put(hostAndPortKey, session);
+            sessionsByHostAndPort.put(key, session);
         }
 
-        // TODO: This in a background thread.
+        // TODO: Do this in a background thread.
         if (persistentCache != null) {
-            byte[] data = toBytes(session);
+            byte[] data = session.toBytes();
             if (data != null) {
-                persistentCache.putSessionData(session, data);
+                persistentCache.putSessionData(session.toSSLSession(), data);
             }
         }
     }
 
-    static class HostAndPort {
+    @Override
+    void onBeforeRemoveSession(SslSessionWrapper session) {
+        String host = session.getPeerHost();
+        if (host == null) {
+            return;
+        }
+        int port = session.getPeerPort();
+        HostAndPort hostAndPortKey = new HostAndPort(host, port);
+        synchronized (sessionsByHostAndPort) {
+            sessionsByHostAndPort.remove(hostAndPortKey);
+        }
+    }
+
+    @Override
+    SslSessionWrapper getSessionFromPersistentCache(byte[] sessionId) {
+        // Not implemented for clients.
+        return null;
+    }
+
+    private static final class HostAndPort {
         final String host;
         final int port;
 
diff --git a/common/src/main/java/org/conscrypt/Conscrypt.java b/common/src/main/java/org/conscrypt/Conscrypt.java
index f2ede81..a0597c9 100644
--- a/common/src/main/java/org/conscrypt/Conscrypt.java
+++ b/common/src/main/java/org/conscrypt/Conscrypt.java
@@ -15,18 +15,18 @@
  */
 package org.conscrypt;
 
-import java.io.FileDescriptor;
 import java.io.UnsupportedEncodingException;
-import java.net.SocketException;
 import java.nio.ByteBuffer;
 import java.security.KeyManagementException;
 import java.security.PrivateKey;
 import java.security.Provider;
+import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLContextSpi;
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLEngineResult;
 import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.SSLSessionContext;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.X509TrustManager;
@@ -38,6 +38,32 @@
     private Conscrypt() {}
 
     /**
+     * Returns {@code true} if the Conscrypt native library has been successfully loaded.
+     */
+    public static boolean isAvailable() {
+        try {
+            checkAvailability();
+            return true;
+        } catch (Throwable e) {
+            return false;
+        }
+    }
+
+    /**
+     * Checks that the Conscrypt support is available for the system.
+     *
+     * @throws UnsatisfiedLinkError if unavailable
+     */
+    public static void checkAvailability() {
+        try {
+            NativeCrypto.checkAvailability();
+        } catch (Throwable e) {
+            throw (Error) new UnsatisfiedLinkError("failed to load the required native library")
+                    .initCause(e);
+        }
+    }
+
+    /**
      * Constructs a new {@link Provider} with the default name.
      */
     public static Provider newProvider() {
@@ -82,6 +108,44 @@
     }
 
     /**
+     * Utility methods for configuring Conscrypt {@link SSLContext} instances.
+     */
+    public static final class Contexts {
+        private Contexts() {}
+
+        /**
+         * Indicates whether the given object is a Conscrypt client-side session context.
+         */
+        public static boolean isConscrypt(SSLContext context) {
+            return context.getProvider() instanceof OpenSSLProvider;
+        }
+
+        /**
+         * Sets the client-side persistent cache to be used by the context.
+         */
+        public static void setClientSessionCache(SSLContext context, SSLClientSessionCache cache) {
+            SSLSessionContext clientContext = context.getClientSessionContext();
+            if (!(clientContext instanceof ClientSessionContext)) {
+                throw new IllegalArgumentException(
+                        "Not a conscrypt client context: " + clientContext.getClass().getName());
+            }
+            ((ClientSessionContext) clientContext).setPersistentCache(cache);
+        }
+
+        /**
+         * Sets the server-side persistent cache to be used by the context.
+         */
+        public static void setServerSessionCache(SSLContext context, SSLServerSessionCache cache) {
+            SSLSessionContext serverContext = context.getServerSessionContext();
+            if (!(serverContext instanceof ServerSessionContext)) {
+                throw new IllegalArgumentException(
+                        "Not a conscrypt client context: " + serverContext.getClass().getName());
+            }
+            ((ServerSessionContext) serverContext).setPersistentCache(cache);
+        }
+    }
+
+    /**
      * Utility methods for configuring Conscrypt socket factories.
      */
     public static final class SocketFactories {
@@ -168,15 +232,43 @@
          * Indicates whether the given socket is a Conscrypt socket.
          */
         public static boolean isConscrypt(SSLSocket socket) {
-            return socket instanceof OpenSSLSocketImpl;
+            return socket instanceof AbstractConscryptSocket;
         }
 
-        private static OpenSSLSocketImpl toConscrypt(SSLSocket socket) {
+        private static AbstractConscryptSocket toConscrypt(SSLSocket socket) {
             if (!isConscrypt(socket)) {
                 throw new IllegalArgumentException(
                         "Not a conscrypt socket: " + socket.getClass().getName());
             }
-            return (OpenSSLSocketImpl) socket;
+            return (AbstractConscryptSocket) socket;
+        }
+
+        /**
+         * This method enables Server Name Indication (SNI) and overrides the hostname supplied
+         * during socket creation.
+         *
+         * @param socket the socket
+         * @param hostname the desired SNI hostname, or null to disable
+         */
+        public static void setHostname(SSLSocket socket, String hostname) {
+            toConscrypt(socket).setHostname(hostname);
+        }
+
+        /**
+         * Returns either the hostname supplied during socket creation or via
+         * {@link #setHostname(SSLSocket, String)}. No DNS resolution is attempted before
+         * returning the hostname.
+         */
+        public static String getHostname(SSLSocket socket) {
+            return toConscrypt(socket).getHostname();
+        }
+
+        /**
+         * This method attempts to create a textual representation of the peer host or IP. Does
+         * not perform a reverse DNS lookup. This is typically used during session creation.
+         */
+        public static String getHostnameOrIP(SSLSocket socket) {
+            return toConscrypt(socket).getHostnameOrIP();
         }
 
         /**
@@ -190,64 +282,6 @@
         }
 
         /**
-         * This method enables Server Name Indication
-         *
-         * @param socket the socket
-         * @param hostname the desired SNI hostname, or null to disable
-         */
-        public static void setHostname(SSLSocket socket, String hostname) {
-            toConscrypt(socket).setHostname(hostname);
-        }
-
-        /**
-         * Returns the hostname that was supplied during socket creation. No DNS resolution is
-         * attempted before returning the hostname.
-         */
-        public static String getHostname(SSLSocket socket) {
-            return toConscrypt(socket).getHostname();
-        }
-
-        /**
-         * For the purposes of an SSLSession, we want a way to represent the supplied hostname
-         * or the IP address in a textual representation. We do not want to perform reverse DNS
-         * lookups on this address.
-         */
-        public static String getHostnameOrIP(SSLSocket socket) {
-            return toConscrypt(socket).getHostnameOrIP();
-        }
-
-        /**
-         * Note write timeouts are not part of the javax.net.ssl.SSLSocket API
-         */
-        public static void setSoWriteTimeout(SSLSocket socket, int writeTimeoutMilliseconds)
-                throws SocketException {
-            toConscrypt(socket).setSoWriteTimeout(writeTimeoutMilliseconds);
-        }
-
-        /**
-         * Note write timeouts are not part of the javax.net.ssl.SSLSocket API
-         */
-        public static int getSoWriteTimeout(SSLSocket socket) throws SocketException {
-            return toConscrypt(socket).getSoWriteTimeout();
-        }
-
-        /**
-         * Set the handshake timeout on this socket.  This timeout is specified in
-         * milliseconds and will be used only during the handshake process.
-         */
-        public static void setHandshakeTimeout(SSLSocket socket, int handshakeTimeoutMilliseconds)
-                throws SocketException {
-            toConscrypt(socket).setHandshakeTimeout(handshakeTimeoutMilliseconds);
-        }
-
-        /**
-         * Gets the underlying file descriptor for the given socket.
-         */
-        public static FileDescriptor getFileDescriptor(SSLSocket socket) {
-            return toConscrypt(socket).getFileDescriptor$();
-        }
-
-        /**
          * Enables/disables TLS Channel ID for the given server-side socket.
          *
          * <p>This method needs to be invoked before the handshake starts.
@@ -324,34 +358,44 @@
          * Indicates whether the given engine is a Conscrypt engine.
          */
         public static boolean isConscrypt(SSLEngine engine) {
-            return engine instanceof OpenSSLEngineImpl;
+            return engine instanceof ConscryptEngine;
         }
 
-        private static OpenSSLEngineImpl toConscrypt(SSLEngine engine) {
+        private static ConscryptEngine toConscrypt(SSLEngine engine) {
             if (!isConscrypt(engine)) {
                 throw new IllegalArgumentException(
                         "Not a conscrypt engine: " + engine.getClass().getName());
             }
-            return (OpenSSLEngineImpl) engine;
+            return (ConscryptEngine) engine;
         }
 
         /**
-         * This method enables Server Name Indication (SNI) and sets the host name used for
-         * SNI.
+         * Provides the given engine with the provided bufferAllocator.
+         * @param engine
+         * @param bufferAllocator
+         */
+        public static void setBufferAllocator(SSLEngine engine, BufferAllocator bufferAllocator) {
+            toConscrypt(engine).setBufferAllocator(bufferAllocator);
+        }
+
+        /**
+         * This method enables Server Name Indication (SNI) and overrides the hostname supplied
+         * during engine creation.
          *
          * @param engine the engine
          * @param hostname the desired SNI hostname, or {@code null} to disable
          */
         public static void setHostname(SSLEngine engine, String hostname) {
-            toConscrypt(engine).setSniHostname(hostname);
+            toConscrypt(engine).setHostname(hostname);
         }
 
         /**
-         * Returns the SNI hostname that was set for the {@code engine}. If no SNI hostname
-         * was set, it will return the hostname supplied during creation of the {@code engine}.
+         * Returns either the hostname supplied during socket creation or via
+         * {@link #setHostname(SSLEngine, String)}. No DNS resolution is attempted before
+         * returning the hostname.
          */
         public static String getHostname(SSLEngine engine) {
-            return toConscrypt(engine).getSniHostname();
+            return toConscrypt(engine).getHostname();
         }
 
         /**
diff --git a/common/src/main/java/org/conscrypt/OpenSSLEngineImpl.java b/common/src/main/java/org/conscrypt/ConscryptEngine.java
similarity index 64%
rename from common/src/main/java/org/conscrypt/OpenSSLEngineImpl.java
rename to common/src/main/java/org/conscrypt/ConscryptEngine.java
index d12d4d8..6e1a929 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLEngineImpl.java
+++ b/common/src/main/java/org/conscrypt/ConscryptEngine.java
@@ -32,6 +32,8 @@
 
 package org.conscrypt;
 
+import static java.lang.Math.max;
+import static java.lang.Math.min;
 import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED;
 import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_UNWRAP;
 import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_WRAP;
@@ -45,16 +47,27 @@
 import static org.conscrypt.NativeConstants.SSL3_RT_MAX_PLAIN_LENGTH;
 import static org.conscrypt.NativeConstants.SSL_CB_HANDSHAKE_DONE;
 import static org.conscrypt.NativeConstants.SSL_CB_HANDSHAKE_START;
-import static org.conscrypt.NativeConstants.SSL_ERROR_NONE;
 import static org.conscrypt.NativeConstants.SSL_ERROR_WANT_READ;
 import static org.conscrypt.NativeConstants.SSL_ERROR_WANT_WRITE;
 import static org.conscrypt.NativeConstants.SSL_ERROR_ZERO_RETURN;
-import static org.conscrypt.NativeConstants.SSL_RECEIVED_SHUTDOWN;
-import static org.conscrypt.NativeConstants.SSL_SENT_SHUTDOWN;
+import static org.conscrypt.Preconditions.checkArgument;
+import static org.conscrypt.Preconditions.checkNotNull;
+import static org.conscrypt.Preconditions.checkPositionIndexes;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_CLOSED;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_CLOSED_INBOUND;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_CLOSED_OUTBOUND;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_HANDSHAKE_COMPLETED;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_HANDSHAKE_STARTED;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_MODE_SET;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_NEW;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_READY;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_READY_HANDSHAKE_CUT_THROUGH;
 import static org.conscrypt.SSLUtils.calculateOutNetBufSize;
 import static org.conscrypt.SSLUtils.toSSLHandshakeException;
 
+import java.io.EOFException;
 import java.io.IOException;
+import java.io.InterruptedIOException;
 import java.nio.ByteBuffer;
 import java.nio.ReadOnlyBufferException;
 import java.security.InvalidKeyException;
@@ -76,15 +89,15 @@
 import javax.net.ssl.X509KeyManager;
 import javax.net.ssl.X509TrustManager;
 import javax.security.auth.x500.X500Principal;
+import org.conscrypt.NativeRef.SSL_SESSION;
+import org.conscrypt.SslWrapper.BioWrapper;
 
 /**
  * Implements the {@link SSLEngine} API using OpenSSL's non-blocking interfaces.
- *
- * @hide
  */
-final class OpenSSLEngineImpl extends SSLEngine implements NativeCrypto.SSLHandshakeCallbacks,
-                                                           SSLParametersImpl.AliasChooser,
-                                                           SSLParametersImpl.PSKCallbacks {
+final class ConscryptEngine extends SSLEngine implements NativeCrypto.SSLHandshakeCallbacks,
+                                                         SSLParametersImpl.AliasChooser,
+                                                         SSLParametersImpl.PSKCallbacks {
     private static final SSLEngineResult NEED_UNWRAP_OK =
             new SSLEngineResult(OK, NEED_UNWRAP, 0, 0);
     private static final SSLEngineResult NEED_UNWRAP_CLOSED =
@@ -95,59 +108,29 @@
     private static final SSLEngineResult CLOSED_NOT_HANDSHAKING =
             new SSLEngineResult(CLOSED, NOT_HANDSHAKING, 0, 0);
     private static final ByteBuffer EMPTY = ByteBuffer.allocateDirect(0);
-    private static final long EMPTY_ADDR = NativeCrypto.getDirectBufferAddress(EMPTY);
-
-    /**
-     * Hostname used with the TLS extension SNI hostname. {@link #getPeerHost()} is used if this is
-     * not set.
-     */
-    private String sniHostname;
 
     private final SSLParametersImpl sslParameters;
+    private BufferAllocator bufferAllocator;
 
     /**
-     * Protects {@link #engineState} and {@link #handshakeFinished}.
+     * A lazy-created direct buffer used as a bridge between heap buffers provided by the
+     * application and JNI. This avoids the overhead of calling JNI with heap buffers.
+     * Used only when no {@link #bufferAllocator} has been provided.
+     */
+    private ByteBuffer lazyDirectBuffer;
+
+    /**
+     * Hostname used with the TLS extension SNI hostname.
+     */
+    private String peerHostname;
+
+    /**
+     * Protects {@link #state} and {@link #handshakeFinished}.
      */
     private final Object stateLock = new Object();
 
-    private enum EngineState {
-        /**
-         * The {@link OpenSSLSocketImpl} object is constructed, but {@link #beginHandshake()} has
-         * not yet been called.
-         */
-        NEW,
-        /**
-         * {@link #setUseClientMode(boolean)} has been called at least once.
-         */
-        MODE_SET,
-        /**
-         * Handshake task has been started.
-         */
-        HANDSHAKE_STARTED,
-        /**
-         * Handshake has been completed, but {@link #beginHandshake()} hasn't returned yet.
-         */
-        HANDSHAKE_COMPLETED,
-        /**
-         * {@link #beginHandshake()} has completed but the task hasn't been called. This is expected
-         * behaviour in cut-through mode, where SSL_do_handshake returns before the handshake is
-         * complete. We can now start writing data to the socket.
-         */
-        READY_HANDSHAKE_CUT_THROUGH,
-        /**
-         * {@link #beginHandshake()} has completed and socket is ready to go.
-         */
-        READY,
-        CLOSED_INBOUND,
-        CLOSED_OUTBOUND,
-        /**
-         * Inbound and outbound has been called.
-         */
-        CLOSED,
-    }
-
     // @GuardedBy("stateLock");
-    private EngineState engineState = EngineState.NEW;
+    private int state = STATE_NEW;
     private boolean handshakeFinished;
 
     /**
@@ -155,24 +138,18 @@
      * close.
      */
     // @GuardedBy("stateLock");
-    private long sslNativePointer;
+    private final SslWrapper ssl;
 
     /**
-     * Protected by synchronizing on stateLock. Starts as 0, set by startHandshake, reset to 0 on
-     * close.
+     * The BIO used for reading/writing encrypted bytes.
      */
     // @GuardedBy("stateLock");
-    private long networkBio;
+    private final BioWrapper networkBio;
 
     /**
      * Set during startHandshake.
      */
-    private AbstractOpenSSLSession sslSession;
-
-    /**
-     * Used during handshake callbacks.
-     */
-    private AbstractOpenSSLSession handshakeSession;
+    private final ActiveSession sslSession;
 
     /**
      * Private key for the TLS Channel ID extension. This field is client-side only. Set during
@@ -186,14 +163,50 @@
 
     private final ByteBuffer[] singleSrcBuffer = new ByteBuffer[1];
     private final ByteBuffer[] singleDstBuffer = new ByteBuffer[1];
+    private final PeerInfoProvider peerInfoProvider;
 
-    OpenSSLEngineImpl(SSLParametersImpl sslParameters) {
+    private SSLException handshakeException;
+
+    ConscryptEngine(SSLParametersImpl sslParameters) {
         this.sslParameters = sslParameters;
+        peerInfoProvider = PeerInfoProvider.nullProvider();
+        this.ssl = newSsl(sslParameters, this);
+        this.networkBio = ssl.newBio();
+        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
     }
 
-    OpenSSLEngineImpl(String host, int port, SSLParametersImpl sslParameters) {
-        super(host, port);
+    ConscryptEngine(String host, int port, SSLParametersImpl sslParameters) {
         this.sslParameters = sslParameters;
+        this.peerInfoProvider = PeerInfoProvider.forHostAndPort(host, port);
+        this.ssl = newSsl(sslParameters, this);
+        this.networkBio = ssl.newBio();
+        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+    }
+
+    ConscryptEngine(SSLParametersImpl sslParameters, PeerInfoProvider peerInfoProvider) {
+        this.sslParameters = sslParameters;
+        this.peerInfoProvider = checkNotNull(peerInfoProvider, "peerInfoProvider");
+        this.ssl = newSsl(sslParameters, this);
+        this.networkBio = ssl.newBio();
+        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+    }
+
+    private static SslWrapper newSsl(SSLParametersImpl sslParameters, ConscryptEngine engine) {
+        try {
+            return SslWrapper.newInstance(sslParameters, engine, engine, engine);
+        } catch (SSLException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    void setBufferAllocator(BufferAllocator bufferAllocator) {
+        synchronized (stateLock) {
+            if (isHandshakeStarted()) {
+                throw new IllegalStateException(
+                        "Could not set buffer allocator after the initial handshake has begun.");
+            }
+            this.bufferAllocator = bufferAllocator;
+        }
     }
 
     /**
@@ -244,7 +257,7 @@
                 throw new IllegalStateException(
                         "Channel ID is only available after handshake completes");
             }
-            return NativeCrypto.SSL_get_tls_channel_id(sslNativePointer);
+            return ssl.getTlsChannelId();
         }
     }
 
@@ -310,15 +323,43 @@
     }
 
     private boolean isHandshakeStarted() {
-        switch (engineState) {
-            case NEW:
-            case MODE_SET:
+        switch (state) {
+            case STATE_NEW:
+            case STATE_MODE_SET:
                 return false;
             default:
                 return true;
         }
     }
 
+    /**
+     * This method enables Server Name Indication (SNI) and overrides the {@link PeerInfoProvider}
+     * supplied during engine creation.
+     */
+    void setHostname(String hostname) {
+        sslParameters.setUseSni(hostname != null);
+        this.peerHostname = hostname;
+    }
+
+    /**
+     * Returns the hostname from {@link #setHostname(String)} or supplied by the
+     * {@link PeerInfoProvider} upon creation. No DNS resolution is attempted before
+     * returning the hostname.
+     */
+    String getHostname() {
+        return peerHostname != null ? peerHostname : peerInfoProvider.getHostname();
+    }
+
+    @Override
+    public String getPeerHost() {
+        return peerHostname != null ? peerHostname : peerInfoProvider.getHostnameOrIP();
+    }
+
+    @Override
+    public int getPeerPort() {
+        return peerInfoProvider.getPort();
+    }
+
     @Override
     public void beginHandshake() throws SSLException {
         synchronized (stateLock) {
@@ -327,38 +368,38 @@
     }
 
     private void beginHandshakeInternal() throws SSLException {
-        switch (engineState) {
-            case MODE_SET:
+        switch (state) {
+            case STATE_MODE_SET:
                 // This is the only allowed state.
                 break;
-            case HANDSHAKE_STARTED:
+            case STATE_HANDSHAKE_STARTED:
                 throw new IllegalStateException("Handshake has already been started");
-            case CLOSED_INBOUND:
-            case CLOSED_OUTBOUND:
-            case CLOSED:
+            case STATE_CLOSED_INBOUND:
+            case STATE_CLOSED_OUTBOUND:
+            case STATE_CLOSED:
                 throw new IllegalStateException("Engine has already been closed");
             default:
                 throw new IllegalStateException("Client/server mode must be set before handshake");
         }
 
-        engineState = EngineState.HANDSHAKE_STARTED;
+        state = STATE_HANDSHAKE_STARTED;
 
         boolean releaseResources = true;
         try {
-            final AbstractSessionContext sessionContext = sslParameters.getSessionContext();
-            sslNativePointer = NativeCrypto.SSL_new(sessionContext.sslCtxNativePointer);
-            networkBio = NativeCrypto.SSL_BIO_new(sslNativePointer);
-            sslSession = sslParameters.getSessionToReuse(
-                    sslNativePointer, getSniHostname(), getPeerPort());
-            sslParameters.setSSLParameters(sslNativePointer, this, this, getSniHostname());
-            sslParameters.setCertificateValidation(sslNativePointer);
-            sslParameters.setTlsChannelId(sslNativePointer, channelIdPrivateKey);
+            // Prepare the SSL object for the handshake.
+            ssl.initialize(getHostname(), channelIdPrivateKey);
+
+            // For clients, offer to resume a previously cached session to avoid the
+            // full TLS handshake.
             if (getUseClientMode()) {
-                NativeCrypto.SSL_set_connect_state(sslNativePointer);
-            } else {
-                NativeCrypto.SSL_set_accept_state(sslNativePointer);
+                SslSessionWrapper cachedSession = clientSessionContext().getCachedSession(
+                        getHostname(), getPeerPort(), sslParameters);
+                if (cachedSession != null) {
+                    cachedSession.offerToResume(ssl);
+                }
             }
-            maxSealOverhead = NativeCrypto.SSL_max_seal_overhead(sslNativePointer);
+
+            maxSealOverhead = ssl.getMaxSealOverhead();
             handshake();
             releaseResources = false;
         } catch (IOException e) {
@@ -366,13 +407,13 @@
             String message = e.getMessage();
             // Must match error reason string of SSL_R_UNEXPECTED_CCS (in ssl/ssl_err.c)
             if (message.contains("unexpected CCS")) {
-                String logMessage = String.format("ssl_unexpected_ccs: host=%s", getSniHostname());
+                String logMessage = String.format("ssl_unexpected_ccs: host=%s", getPeerHost());
                 Platform.logEvent(logMessage);
             }
             throw SSLUtils.toSSLHandshakeException(e);
         } finally {
             if (releaseResources) {
-                engineState = EngineState.CLOSED;
+                state = STATE_CLOSED;
                 shutdownAndFreeSslNative();
             }
         }
@@ -381,13 +422,13 @@
     @Override
     public void closeInbound() throws SSLException {
         synchronized (stateLock) {
-            if (engineState == EngineState.CLOSED) {
+            if (state == STATE_CLOSED) {
                 return;
             }
-            if (engineState == EngineState.CLOSED_OUTBOUND) {
-                engineState = EngineState.CLOSED;
+            if (state == STATE_CLOSED_OUTBOUND) {
+                state = STATE_CLOSED;
             } else {
-                engineState = EngineState.CLOSED_INBOUND;
+                state = STATE_CLOSED_INBOUND;
             }
         }
         // TODO anything else to notify OpenSSL layer?
@@ -396,16 +437,16 @@
     @Override
     public void closeOutbound() {
         synchronized (stateLock) {
-            if (engineState == EngineState.CLOSED || engineState == EngineState.CLOSED_OUTBOUND) {
+            if (state == STATE_CLOSED || state == STATE_CLOSED_OUTBOUND) {
                 return;
             }
             if (isHandshakeStarted()) {
                 shutdownAndFreeSslNative();
             }
-            if (engineState == EngineState.CLOSED_INBOUND) {
-                engineState = EngineState.CLOSED;
+            if (state == STATE_CLOSED_INBOUND) {
+                state = STATE_CLOSED;
             } else {
-                engineState = EngineState.CLOSED_OUTBOUND;
+                state = STATE_CLOSED_OUTBOUND;
             }
         }
         shutdown();
@@ -439,14 +480,6 @@
         return params;
     }
 
-    public void setSniHostname(String sniHostname) {
-        this.sniHostname = sniHostname;
-    }
-
-    public String getSniHostname() {
-        return sniHostname != null ? sniHostname : getPeerHost();
-    }
-
     @Override
     public void setSSLParameters(SSLParameters p) {
         super.setSSLParameters(p);
@@ -464,31 +497,31 @@
         if (handshakeFinished) {
             return HandshakeStatus.NOT_HANDSHAKING;
         }
-        switch (engineState) {
-            case HANDSHAKE_STARTED:
+        switch (state) {
+            case STATE_HANDSHAKE_STARTED:
                 return pendingStatus(pendingOutboundEncryptedBytes());
-            case HANDSHAKE_COMPLETED:
+            case STATE_HANDSHAKE_COMPLETED:
                 return HandshakeStatus.NEED_WRAP;
-            case NEW:
-            case MODE_SET:
-            case CLOSED:
-            case CLOSED_INBOUND:
-            case CLOSED_OUTBOUND:
-            case READY:
-            case READY_HANDSHAKE_CUT_THROUGH:
+            case STATE_NEW:
+            case STATE_MODE_SET:
+            case STATE_CLOSED:
+            case STATE_CLOSED_INBOUND:
+            case STATE_CLOSED_OUTBOUND:
+            case STATE_READY:
+            case STATE_READY_HANDSHAKE_CUT_THROUGH:
                 return HandshakeStatus.NOT_HANDSHAKING;
             default:
                 break;
         }
-        throw new IllegalStateException("Unexpected engine state: " + engineState);
+        throw new IllegalStateException("Unexpected engine state: " + state);
     }
 
     private int pendingOutboundEncryptedBytes() {
-        return NativeCrypto.SSL_pending_written_bytes_in_BIO(networkBio);
+        return networkBio.getPendingWrittenBytes();
     }
 
     private int pendingInboundCleartextBytes() {
-        return NativeCrypto.SSL_pending_readable_bytes(sslNativePointer);
+        return ssl.getPendingReadableBytes();
     }
 
     private static SSLEngineResult.HandshakeStatus pendingStatus(int pendingOutboundBytes) {
@@ -501,13 +534,30 @@
         return sslParameters.getNeedClientAuth();
     }
 
+    /* @Override */
+    @SuppressWarnings("MissingOverride") // For compilation with Java 6.
+    public SSLSession getHandshakeSession() {
+        return handshakeSession();
+    }
+
+    /**
+     * Work-around to allow this method to be called on older versions of Android.
+     */
+    SSLSession handshakeSession() {
+        synchronized (stateLock) {
+            return state == STATE_HANDSHAKE_STARTED ? sslSession : null;
+        }
+    }
+
     @Override
     public SSLSession getSession() {
-        if (sslSession == null) {
-            return handshakeSession != null ? Platform.wrapSSLSession(handshakeSession)
-                                            : SSLNullSession.getNullSession();
+        synchronized (stateLock) {
+            if (state < STATE_HANDSHAKE_COMPLETED) {
+                // Return an invalid session with invalid cipher suite of "SSL_NULL_WITH_NULL_NULL"
+                return SSLNullSession.getNullSession();
+            }
+            return Platform.wrapSSLSession(sslSession);
         }
-        return Platform.wrapSSLSession(sslSession);
     }
 
     @Override
@@ -532,24 +582,22 @@
 
     @Override
     public boolean isInboundDone() {
-        if (sslNativePointer == 0) {
-            synchronized (stateLock) {
-                return engineState == EngineState.CLOSED
-                        || engineState == EngineState.CLOSED_INBOUND;
+        synchronized (stateLock) {
+            if (state == STATE_CLOSED || state == STATE_CLOSED_INBOUND) {
+                return true;
             }
         }
-        return (NativeCrypto.SSL_get_shutdown(sslNativePointer) & SSL_RECEIVED_SHUTDOWN) != 0;
+        return ssl.wasShutdownReceived();
     }
 
     @Override
     public boolean isOutboundDone() {
-        if (sslNativePointer == 0) {
-            synchronized (stateLock) {
-                return engineState == EngineState.CLOSED
-                        || engineState == EngineState.CLOSED_OUTBOUND;
+        synchronized (stateLock) {
+            if (state == STATE_CLOSED || state == STATE_CLOSED_OUTBOUND) {
+                return true;
             }
         }
-        return (NativeCrypto.SSL_get_shutdown(sslNativePointer) & SSL_SENT_SHUTDOWN) != 0;
+        return ssl.wasShutdownSent();
     }
 
     @Override
@@ -577,9 +625,9 @@
         synchronized (stateLock) {
             if (isHandshakeStarted()) {
                 throw new IllegalArgumentException(
-                        "Can not change mode after handshake: engineState == " + engineState);
+                        "Can not change mode after handshake: state == " + state);
             }
-            engineState = EngineState.MODE_SET;
+            state = STATE_MODE_SET;
         }
         sslParameters.setUseClientMode(mode);
     }
@@ -625,19 +673,18 @@
     }
 
     SSLEngineResult unwrap(final ByteBuffer[] srcs, final ByteBuffer[] dsts) throws SSLException {
-        checkNotNull(srcs, "srcs");
-        checkNotNull(dsts, "dsts");
+        checkArgument(srcs != null, "srcs is null");
+        checkArgument(dsts != null, "dsts is null");
         return unwrap(srcs, 0, srcs.length, dsts, 0, dsts.length);
     }
 
     SSLEngineResult unwrap(final ByteBuffer[] srcs, int srcsOffset, final int srcsLength,
             final ByteBuffer[] dsts, final int dstsOffset, final int dstsLength)
             throws SSLException {
-        checkNotNull(srcs, "srcs");
-        checkNotNull(dsts, "dsts");
-
-        checkIndex(srcs.length, srcsOffset, srcsLength, "srcs");
-        checkIndex(dsts.length, dstsOffset, dstsLength, "dsts");
+        checkArgument(srcs != null, "srcs is null");
+        checkArgument(dsts != null, "dsts is null");
+        checkPositionIndexes(srcsOffset, srcsOffset + srcsLength, srcs.length);
+        checkPositionIndexes(dstsOffset, dstsOffset + dstsLength, dsts.length);
 
         // Determine the output capacity.
         final int dstLength = calcDstsLength(dsts, dstsOffset, dstsLength);
@@ -647,16 +694,16 @@
         final long srcLength = calcSrcsLength(srcs, srcsOffset, srcsEndOffset);
 
         synchronized (stateLock) {
-            switch (engineState) {
-                case MODE_SET:
+            switch (state) {
+                case STATE_MODE_SET:
                     // Begin the handshake implicitly.
                     beginHandshakeInternal();
                     break;
-                case CLOSED_INBOUND:
-                case CLOSED:
+                case STATE_CLOSED_INBOUND:
+                case STATE_CLOSED:
                     // If the inbound direction is closed. we can't send anymore.
                     return new SSLEngineResult(Status.CLOSED, getHandshakeStatusInternal(), 0, 0);
-                case NEW:
+                case STATE_NEW:
                     throw new IllegalStateException(
                             "Client/server mode must be set before calling unwrap");
                 default:
@@ -669,7 +716,7 @@
                 if (handshakeStatus == NEED_WRAP) {
                     return NEED_WRAP_OK;
                 }
-                if (engineState == EngineState.CLOSED) {
+                if (state == STATE_CLOSED) {
                     return NEED_WRAP_CLOSED;
                 }
                 // NEED_UNWRAP - just fall through to perform the unwrap.
@@ -715,7 +762,7 @@
                         continue;
                     }
                     // Write the source encrypted data to the networkBio.
-                    int written = writeEncryptedData(src, Math.min(lenRemaining, remaining));
+                    int written = writeEncryptedData(src, min(lenRemaining, remaining));
                     if (written > 0) {
                         bytesConsumed += written;
                         lenRemaining -= written;
@@ -746,57 +793,65 @@
 
             // Now read any available plaintext data.
             int bytesProduced = 0;
-            if (dstLength > 0) {
-                // Write decrypted data to dsts buffers
-                for (int idx = dstsOffset; idx < endOffset; ++idx) {
-                    ByteBuffer dst = dsts[idx];
-                    if (!dst.hasRemaining()) {
-                        continue;
-                    }
+            try {
+                if (dstLength > 0) {
+                    // Write decrypted data to dsts buffers
+                    for (int idx = dstsOffset; idx < endOffset; ++idx) {
+                        ByteBuffer dst = dsts[idx];
+                        if (!dst.hasRemaining()) {
+                            continue;
+                        }
 
-                    int bytesRead = readPlaintextData(dst);
-                    if (bytesRead > 0) {
-                        bytesProduced += bytesRead;
-                        if (dst.hasRemaining()) {
-                            // We haven't filled this buffer fully, break out of the loop
-                            // and determine the correct response status below.
-                            break;
-                        }
-                    } else {
-                        // Return an appropriate result based on the error code.
-                        int sslError = NativeCrypto.SSL_get_error(sslNativePointer, bytesRead);
-                        switch (sslError) {
-                            case SSL_ERROR_ZERO_RETURN:
-                                // This means the connection was shutdown correctly, close inbound
-                                // and outbound
-                                closeAll();
-                                return newResult(bytesConsumed, bytesProduced, handshakeStatus);
-                            case SSL_ERROR_WANT_READ:
-                            case SSL_ERROR_WANT_WRITE:
-                                return newResult(bytesConsumed, bytesProduced, handshakeStatus);
-                            default:
-                                return sslReadErrorResult(NativeCrypto.SSL_get_last_error_number(),
-                                        bytesConsumed, bytesProduced);
+                        int bytesRead = readPlaintextData(dst);
+                        if (bytesRead > 0) {
+                            bytesProduced += bytesRead;
+                            if (dst.hasRemaining()) {
+                                // We haven't filled this buffer fully, break out of the loop
+                                // and determine the correct response status below.
+                                break;
+                            }
+                        } else {
+                            switch (bytesRead) {
+                                case -SSL_ERROR_WANT_READ:
+                                case -SSL_ERROR_WANT_WRITE: {
+                                    return newResult(bytesConsumed, bytesProduced, handshakeStatus);
+                                }
+                                default: {
+                                    // Should never get here.
+                                    throw shutdownWithError("SSL_read");
+                                }
+                            }
                         }
                     }
+                } else {
+                    // If the capacity of all destination buffers is 0 we need to trigger a SSL_read
+                    // anyway to ensure everything is flushed in the BIO pair and so we can detect
+                    // it in the pendingInboundCleartextBytes() call.
+                    readPlaintextData(EMPTY);
                 }
-            } else {
-                // If the capacity of all destination buffers is 0 we need to trigger a SSL_read
-                // anyway to ensure everything is flushed in the BIO pair and so we can detect it
-                // in the pendingInboundCleartextBytes() call.
-                try {
-                    if (NativeCrypto.ENGINE_SSL_read_direct(sslNativePointer, EMPTY_ADDR, 0, this)
-                            <= 0) {
-                        // We do not check SSL_get_error as we are not interested in any error that
-                        // is not fatal.
-                        int err = NativeCrypto.SSL_get_last_error_number();
-                        if (err != SSL_ERROR_NONE) {
-                            return sslReadErrorResult(err, bytesConsumed, bytesProduced);
-                        }
+            } catch (SSLException e) {
+                if (pendingOutboundEncryptedBytes() > 0) {
+                    // We need to flush any pending bytes to the remote endpoint in case
+                    // there is an alert that needs to be propagated.
+                    if (!handshakeFinished && handshakeException == null) {
+                        // Save the handshake exception. We will re-throw during the next
+                        // handshake.
+                        handshakeException = e;
                     }
-                } catch (IOException e) {
-                    throw new SSLException(e);
+                    return new SSLEngineResult(OK, NEED_WRAP, bytesConsumed, bytesProduced);
                 }
+
+                // Nothing to write, just shutdown and throw the exception.
+                shutdown();
+                throw convertException(e);
+            } catch (InterruptedIOException e) {
+                return newResult(bytesConsumed, bytesProduced, handshakeStatus);
+            } catch (EOFException e) {
+                closeAll();
+                throw convertException(e);
+            } catch (IOException e) {
+                shutdown();
+                throw convertException(e);
             }
 
             // There won't be any application data until we're done handshaking.
@@ -821,7 +876,7 @@
         int capacity = 0;
         for (int i = 0; i < dsts.length; i++) {
             ByteBuffer dst = dsts[i];
-            checkNotNull(dst, "one of the dst");
+            checkArgument(dst != null, "dsts[%d] is null", i);
             if (dst.isReadOnly()) {
                 throw new ReadOnlyBufferException();
             }
@@ -845,45 +900,59 @@
     }
 
     private SSLEngineResult.HandshakeStatus handshake() throws SSLException {
-        long sslSessionCtx = 0L;
         try {
             // Only actually perform the handshake if we haven't already just completed it
             // via BIO operations.
-            int code = NativeCrypto.ENGINE_SSL_do_handshake(sslNativePointer, this);
-            if (code <= 0) {
-                int sslError = NativeCrypto.SSL_get_error(sslNativePointer, code);
-                switch (sslError) {
-                    case SSL_ERROR_WANT_READ:
-                    case SSL_ERROR_WANT_WRITE:
-                        return pendingStatus(pendingOutboundEncryptedBytes());
-                    default:
-                        // Everything else is considered as error
-                        throw shutdownWithError("SSL_do_handshake");
+            try {
+                // First, check to see if we already have a pending alert that needs to be written.
+                if (handshakeException != null) {
+                    if (pendingOutboundEncryptedBytes() > 0) {
+                        // Need to finish writing the alert to the remote peer.
+                        return NEED_WRAP;
+                    }
+
+                    // We've finished writing the alert, just throw the exception.
+                    SSLException e = handshakeException;
+                    handshakeException = null;
+                    throw e;
                 }
+
+                int ssl_error_code = ssl.doHandshake();
+                switch (ssl_error_code) {
+                    case SSL_ERROR_WANT_READ:
+                        return pendingStatus(pendingOutboundEncryptedBytes());
+                    case SSL_ERROR_WANT_WRITE: {
+                        return NEED_WRAP;
+                    }
+                    default: {
+                        // SSL_ERROR_NONE.
+                    }
+                }
+            } catch (SSLException e) {
+                if (pendingOutboundEncryptedBytes() > 0) {
+                    // Delay throwing the exception since we appear to have an outbound alert
+                    // that needs to be written to the remote endpoint.
+                    handshakeException = e;
+                    return NEED_WRAP;
+                }
+
+                // There is no pending alert to write - just shutdown and throw.
+                shutdown();
+                throw e;
+            } catch (IOException e) {
+                shutdown();
+                throw e;
             }
 
-            // Handshake is finished!
-            sslSessionCtx = NativeCrypto.SSL_get1_session(sslNativePointer);
-            if (sslSessionCtx == 0) {
-                // TODO(nathanmittler): Should we throw here?
-                // return pendingStatus(pendingOutboundBytes());
-                throw shutdownWithError("Failed to obtain session after handshake completed");
-            }
-            sslSession = sslParameters.setupSession(sslSessionCtx, sslNativePointer, sslSession,
-                    getSniHostname(), getPeerPort(), true);
-            if (sslSession != null && engineState == EngineState.HANDSHAKE_STARTED) {
-                engineState = EngineState.READY_HANDSHAKE_CUT_THROUGH;
-            } else {
-                engineState = EngineState.READY;
-            }
+            // The handshake has completed successfully...
+
+            // Update the session from the current state of the SSL object.
+            sslSession.onSessionEstablished(getPeerHost(), getPeerPort());
+
             finishHandshake();
             return FINISHED;
         } catch (Exception e) {
             throw toSSLHandshakeException(e);
-        } finally {
-            if (sslSession == null && sslSessionCtx != 0) {
-                NativeCrypto.SSL_SESSION_free(sslSessionCtx);
-            }
         }
     }
 
@@ -894,6 +963,7 @@
             handshakeListener.onHandshakeFinished();
         }
     }
+
     /**
      * Write plaintext data to the OpenSSL internal BIO
      *
@@ -903,14 +973,10 @@
         try {
             final int pos = src.position();
             final int sslWrote;
-
             if (src.isDirect()) {
-                long addr = NativeCrypto.getDirectBufferAddress(src) + pos;
-                sslWrote = NativeCrypto.ENGINE_SSL_write_direct(sslNativePointer, addr, len, this);
+                sslWrote = writePlaintextDataDirect(src, pos, len);
             } else {
-                ByteBuffer heapSrc = toHeapBuffer(src, len);
-                sslWrote = NativeCrypto.ENGINE_SSL_write_heap(sslNativePointer, heapSrc.array(),
-                        heapSrc.arrayOffset() + heapSrc.position(), len, this);
+                sslWrote = writePlaintextDataHeap(src, pos, len);
             }
             if (sslWrote > 0) {
                 src.position(pos + sslWrote);
@@ -921,40 +987,105 @@
         }
     }
 
+    private int writePlaintextDataDirect(ByteBuffer src, int pos, int len) throws IOException {
+        return ssl.writeDirectByteBuffer(directByteBufferAddress(src, pos), len);
+    }
+
+    private int writePlaintextDataHeap(ByteBuffer src, int pos, int len) throws IOException {
+        AllocatedBuffer allocatedBuffer = null;
+        try {
+            final ByteBuffer buffer;
+            if (bufferAllocator != null) {
+                allocatedBuffer = bufferAllocator.allocateDirectBuffer(len);
+                buffer = allocatedBuffer.nioBuffer();
+            } else {
+                // We don't have a buffer allocator, but we don't want to send a heap
+                // buffer to JNI. So lazy-create a direct buffer that we will use from now
+                // on to copy plaintext data.
+                buffer = getOrCreateLazyDirectBuffer();
+            }
+
+            // Copy the data to the direct buffer.
+            int limit = src.limit();
+            int bytesToWrite = min(len, buffer.remaining());
+            src.limit(pos + bytesToWrite);
+            buffer.put(src);
+            buffer.flip();
+            // Restore the original position and limit.
+            src.limit(limit);
+            src.position(pos);
+
+            return writePlaintextDataDirect(buffer, 0, bytesToWrite);
+        } finally {
+            if (allocatedBuffer != null) {
+                // Release the buffer back to the pool.
+                allocatedBuffer.release();
+            }
+        }
+    }
+
     /**
      * Read plaintext data from the OpenSSL internal BIO
      */
-    private int readPlaintextData(final ByteBuffer dst) throws SSLException {
+    private int readPlaintextData(final ByteBuffer dst) throws IOException {
         try {
-            final int sslRead;
             final int pos = dst.position();
             final int limit = dst.limit();
-            final int len = Math.min(SSL3_RT_MAX_PACKET_SIZE, limit - pos);
+            final int len = min(SSL3_RT_MAX_PACKET_SIZE, limit - pos);
             if (dst.isDirect()) {
-                long addr = NativeCrypto.getDirectBufferAddress(dst) + pos;
-                sslRead = NativeCrypto.ENGINE_SSL_read_direct(sslNativePointer, addr, len, this);
-                if (sslRead > 0) {
-                    dst.position(pos + sslRead);
+                int bytesRead = readPlaintextDataDirect(dst, pos, len);
+                if (bytesRead > 0) {
+                    dst.position(pos + bytesRead);
                 }
-            } else if (dst.hasArray()) {
-                sslRead = NativeCrypto.ENGINE_SSL_read_heap(
-                        sslNativePointer, dst.array(), dst.arrayOffset() + pos, len, this);
-                if (sslRead > 0) {
-                    dst.position(pos + sslRead);
-                }
-            } else {
-                byte[] data = new byte[len];
-                sslRead = NativeCrypto.ENGINE_SSL_read_heap(sslNativePointer, data, 0, len, this);
-                if (sslRead > 0) {
-                    dst.put(data, 0, sslRead);
-                }
+                return bytesRead;
             }
-            return sslRead;
-        } catch (Exception e) {
+
+            // The heap method updates the dst position automatically.
+            return readPlaintextDataHeap(dst, len);
+        } catch (CertificateException e) {
             throw convertException(e);
         }
     }
 
+    private int readPlaintextDataDirect(ByteBuffer dst, int pos, int len)
+            throws IOException, CertificateException {
+        return ssl.readDirectByteBuffer(directByteBufferAddress(dst, pos), len);
+    }
+
+    private int readPlaintextDataHeap(ByteBuffer dst, int len)
+            throws IOException, CertificateException {
+        AllocatedBuffer allocatedBuffer = null;
+        try {
+            final ByteBuffer buffer;
+            if (bufferAllocator != null) {
+                allocatedBuffer = bufferAllocator.allocateDirectBuffer(len);
+                buffer = allocatedBuffer.nioBuffer();
+            } else {
+                // We don't have a buffer allocator, but we don't want to send a heap
+                // buffer to JNI. So lazy-create a direct buffer that we will use from now
+                // on to copy plaintext data.
+                buffer = getOrCreateLazyDirectBuffer();
+            }
+
+            // Read the data to the direct buffer.
+            int bytesToRead = min(len, buffer.remaining());
+            int bytesRead = readPlaintextDataDirect(buffer, 0, bytesToRead);
+            if (bytesRead > 0) {
+                // Copy the data to the heap buffer.
+                buffer.position(bytesRead);
+                buffer.flip();
+                dst.put(buffer);
+            }
+
+            return bytesRead;
+        } finally {
+            if (allocatedBuffer != null) {
+                // Release the buffer back to the pool.
+                allocatedBuffer.release();
+            }
+        }
+    }
+
     private SSLException convertException(Throwable e) {
         if (e instanceof SSLHandshakeException || !handshakeFinished) {
             return SSLUtils.toSSLHandshakeException(e);
@@ -968,27 +1099,78 @@
     private int writeEncryptedData(final ByteBuffer src, int len) throws SSLException {
         try {
             final int pos = src.position();
-            final int netWrote;
+            final int bytesWritten;
             if (src.isDirect()) {
-                long addr = NativeCrypto.getDirectBufferAddress(src) + pos;
-                netWrote = NativeCrypto.ENGINE_SSL_write_BIO_direct(
-                        sslNativePointer, networkBio, addr, len, this);
+                bytesWritten = writeEncryptedDataDirect(src, pos, len);
             } else {
-                ByteBuffer heapSrc = toHeapBuffer(src, len);
-                netWrote = NativeCrypto.ENGINE_SSL_write_BIO_heap(sslNativePointer, networkBio,
-                        heapSrc.array(), heapSrc.arrayOffset() + heapSrc.position(), len, this);
+                bytesWritten = writeEncryptedDataHeap(src, pos, len);
             }
 
-            if (netWrote >= 0) {
-                src.position(pos + netWrote);
+            if (bytesWritten > 0) {
+                src.position(pos + bytesWritten);
             }
 
-            return netWrote;
+            return bytesWritten;
         } catch (IOException e) {
             throw new SSLException(e);
         }
     }
 
+    private int writeEncryptedDataDirect(ByteBuffer src, int pos, int len) throws IOException {
+        return networkBio.writeDirectByteBuffer(directByteBufferAddress(src, pos), len);
+    }
+
+    private int writeEncryptedDataHeap(ByteBuffer src, int pos, int len) throws IOException {
+        AllocatedBuffer allocatedBuffer = null;
+        try {
+            final ByteBuffer buffer;
+            if (bufferAllocator != null) {
+                allocatedBuffer = bufferAllocator.allocateDirectBuffer(len);
+                buffer = allocatedBuffer.nioBuffer();
+            } else {
+                // We don't have a buffer allocator, but we don't want to send a heap
+                // buffer to JNI. So lazy-create a direct buffer that we will use from now
+                // on to copy encrypted packets.
+                buffer = getOrCreateLazyDirectBuffer();
+            }
+
+            int limit = src.limit();
+            int bytesToCopy = min(min(limit - pos, len), buffer.remaining());
+            src.limit(pos + bytesToCopy);
+            buffer.put(src);
+            // Restore the original limit.
+            src.limit(limit);
+
+            // Reset the original position on the source buffer.
+            src.position(pos);
+
+            int bytesWritten = writeEncryptedDataDirect(buffer, 0, bytesToCopy);
+
+            // Restore the original position.
+            src.position(pos);
+
+            return bytesWritten;
+        } finally {
+            if (allocatedBuffer != null) {
+                // Release the buffer back to the pool.
+                allocatedBuffer.release();
+            }
+        }
+    }
+
+    private ByteBuffer getOrCreateLazyDirectBuffer() {
+        if (lazyDirectBuffer == null) {
+            lazyDirectBuffer = ByteBuffer.allocateDirect(
+                    max(SSL3_RT_MAX_PLAIN_LENGTH, SSL3_RT_MAX_PACKET_SIZE));
+        }
+        lazyDirectBuffer.clear();
+        return lazyDirectBuffer;
+    }
+
+    private long directByteBufferAddress(ByteBuffer directBuffer, int pos) {
+        return NativeCrypto.getDirectBufferAddress(directBuffer) + pos;
+    }
+
     private SSLEngineResult readPendingBytesFromBIO(ByteBuffer dst, int bytesConsumed,
             int bytesProduced, SSLEngineResult.HandshakeStatus status) throws SSLException {
         try {
@@ -1033,47 +1215,67 @@
      */
     private int readEncryptedData(final ByteBuffer dst, final int pending) throws SSLException {
         try {
-            int bioRead = 0;
+            int bytesRead = 0;
+            final int pos = dst.position();
             if (dst.remaining() >= pending) {
-                final int pos = dst.position();
                 final int limit = dst.limit();
-                final int len = Math.min(pending, limit - pos);
+                final int len = min(pending, limit - pos);
                 if (dst.isDirect()) {
-                    long addr = NativeCrypto.getDirectBufferAddress(dst) + pos;
-                    bioRead = NativeCrypto.ENGINE_SSL_read_BIO_direct(
-                            sslNativePointer, networkBio, addr, len, this);
-                    if (bioRead > 0) {
-                        dst.position(pos + bioRead);
-                        return bioRead;
-                    }
-                } else if (dst.hasArray()) {
-                    bioRead = NativeCrypto.ENGINE_SSL_read_BIO_heap(sslNativePointer, networkBio,
-                            dst.array(), dst.arrayOffset() + pos, pending, this);
-                    if (bioRead > 0) {
-                        dst.position(pos + bioRead);
-                        return bioRead;
+                    bytesRead = readEncryptedDataDirect(dst, pos, len);
+                    // Need to update the position on the dst buffer.
+                    if (bytesRead > 0) {
+                        dst.position(pos + bytesRead);
                     }
                 } else {
-                    byte[] data = new byte[len];
-                    bioRead = NativeCrypto.ENGINE_SSL_read_BIO_heap(
-                            sslNativePointer, networkBio, data, 0, pending, this);
-                    if (bioRead > 0) {
-                        dst.put(data, 0, bioRead);
-                        return bioRead;
-                    }
+                    // The heap method will update the position on the dst buffer automatically.
+                    bytesRead = readEncryptedDataHeap(dst, pos, len);
                 }
             }
-            return bioRead;
+
+            return bytesRead;
         } catch (Exception e) {
             throw convertException(e);
         }
     }
 
+    private int readEncryptedDataDirect(ByteBuffer dst, int pos, int len) throws IOException {
+        return networkBio.readDirectByteBuffer(directByteBufferAddress(dst, pos), len);
+    }
+
+    private int readEncryptedDataHeap(ByteBuffer dst, int pos, int len) throws IOException {
+        AllocatedBuffer allocatedBuffer = null;
+        try {
+            final ByteBuffer buffer;
+            if (bufferAllocator != null) {
+                allocatedBuffer = bufferAllocator.allocateDirectBuffer(len);
+                buffer = allocatedBuffer.nioBuffer();
+            } else {
+                // We don't have a buffer allocator, but we don't want to send a heap
+                // buffer to JNI. So lazy-create a direct buffer that we will use from now
+                // on to copy encrypted packets.
+                buffer = getOrCreateLazyDirectBuffer();
+            }
+
+            int bytesToRead = min(len, buffer.remaining());
+            int bytesRead = readEncryptedDataDirect(buffer, pos, bytesToRead);
+            if (bytesRead > 0) {
+                buffer.position(bytesRead);
+                buffer.flip();
+                dst.put(buffer);
+            }
+
+            return bytesRead;
+        } finally {
+            if (allocatedBuffer != null) {
+                // Release the buffer back to the pool.
+                allocatedBuffer.release();
+            }
+        }
+    }
+
     private SSLEngineResult.HandshakeStatus mayFinishHandshake(
             SSLEngineResult.HandshakeStatus status) throws SSLException {
-        if (!handshakeFinished
-                && status
-                        == NOT_HANDSHAKING /*|| engineState == EngineState.HANDSHAKE_COMPLETED)*/) {
+        if (!handshakeFinished && status == NOT_HANDSHAKING) {
             // If the status was NOT_HANDSHAKING and we not finished the handshake we need to call
             // SSL_do_handshake() again
             return handshake();
@@ -1087,10 +1289,10 @@
     }
 
     private SSLEngineResult.Status getEngineStatus() {
-        switch (engineState) {
-            case CLOSED_INBOUND:
-            case CLOSED_OUTBOUND:
-            case CLOSED:
+        switch (state) {
+            case STATE_CLOSED_INBOUND:
+            case STATE_CLOSED_OUTBOUND:
+            case STATE_CLOSED:
                 return CLOSED;
             default:
                 return OK;
@@ -1102,18 +1304,10 @@
         closeInbound();
     }
 
-    private SSLEngineResult sslReadErrorResult(int err, int bytesConsumed, int bytesProduced)
-            throws SSLException {
-        if (!handshakeFinished && pendingOutboundEncryptedBytes() > 0) {
-            return new SSLEngineResult(OK, NEED_WRAP, bytesConsumed, bytesProduced);
-        }
-        throw shutdownWithError(NativeCrypto.SSL_get_error_string(err));
-    }
-
     private SSLException shutdownWithError(String err) {
         // There was an internal error -- shutdown
         shutdown();
-        if (getHandshakeStatusInternal() == HandshakeStatus.FINISHED) {
+        if (!handshakeFinished) {
             return new SSLException(err);
         }
         return new SSLHandshakeException(err);
@@ -1138,25 +1332,25 @@
     }
 
     @Override
-    public SSLEngineResult wrap(ByteBuffer[] srcs, int offset, int length, ByteBuffer dst)
+    public SSLEngineResult wrap(ByteBuffer[] srcs, int srcsOffset, int srcsLength, ByteBuffer dst)
             throws SSLException {
-        checkNotNull(srcs, "srcs");
-        checkNotNull(dst, "dst");
-        checkIndex(srcs.length, offset, length, "srcs");
+        checkArgument(srcs != null, "srcs is null");
+        checkArgument(dst != null, "dst is null");
+        checkPositionIndexes(srcsOffset, srcsOffset + srcsLength, srcs.length);
         if (dst.isReadOnly()) {
             throw new ReadOnlyBufferException();
         }
 
         synchronized (stateLock) {
-            switch (engineState) {
-                case MODE_SET:
+            switch (state) {
+                case STATE_MODE_SET:
                     // Begin the handshake implicitly.
                     beginHandshakeInternal();
                     break;
-                case CLOSED_OUTBOUND:
-                case CLOSED:
+                case STATE_CLOSED_OUTBOUND:
+                case STATE_CLOSED:
                     return new SSLEngineResult(Status.CLOSED, getHandshakeStatusInternal(), 0, 0);
-                case NEW:
+                case STATE_NEW:
                     throw new IllegalStateException(
                             "Client/server mode must be set before calling wrap");
                 default:
@@ -1172,15 +1366,15 @@
                     return NEED_UNWRAP_OK;
                 }
 
-                if (engineState == EngineState.CLOSED) {
+                if (state == STATE_CLOSED) {
                     return NEED_UNWRAP_CLOSED;
                 }
                 // NEED_WRAP - just fall through to perform the wrap.
             }
 
             int srcsLen = 0;
-            final int endOffset = offset + length;
-            for (int i = offset; i < endOffset; ++i) {
+            final int endOffset = srcsOffset + srcsLength;
+            for (int i = srcsOffset; i < endOffset; ++i) {
                 final ByteBuffer src = srcs[i];
                 if (src == null) {
                     throw new IllegalArgumentException("srcs[" + i + "] is null");
@@ -1207,14 +1401,14 @@
             int bytesProduced = 0;
             int bytesConsumed = 0;
         loop:
-            for (int i = offset; i < endOffset; ++i) {
+            for (int i = srcsOffset; i < endOffset; ++i) {
                 final ByteBuffer src = srcs[i];
-                checkNotNull(src, "srcs[%d] is null", i);
+                checkArgument(src != null, "srcs[%d] is null", i);
                 while (src.hasRemaining()) {
                     final SSLEngineResult pendingNetResult;
                     // Write plaintext application data to the SSL engine
-                    int result = writePlaintextData(src,
-                            Math.min(src.remaining(), SSL3_RT_MAX_PLAIN_LENGTH - bytesConsumed));
+                    int result = writePlaintextData(
+                            src, min(src.remaining(), SSL3_RT_MAX_PLAIN_LENGTH - bytesConsumed));
                     if (result > 0) {
                         bytesConsumed += result;
 
@@ -1232,7 +1426,7 @@
                             break loop;
                         }
                     } else {
-                        int sslError = NativeCrypto.SSL_get_error(sslNativePointer, result);
+                        int sslError = ssl.getError(result);
                         switch (sslError) {
                             case SSL_ERROR_ZERO_RETURN:
                                 // This means the connection was shutdown correctly, close inbound
@@ -1305,37 +1499,65 @@
 
     @Override
     public int clientPSKKeyRequested(String identityHint, byte[] identity, byte[] key) {
-        return sslParameters.clientPSKKeyRequested(identityHint, identity, key, this);
+        return ssl.clientPSKKeyRequested(identityHint, identity, key);
     }
 
     @Override
     public int serverPSKKeyRequested(String identityHint, String identity, byte[] key) {
-        return sslParameters.serverPSKKeyRequested(identityHint, identity, key, this);
+        return ssl.serverPSKKeyRequested(identityHint, identity, key);
     }
 
     @Override
     public void onSSLStateChange(int type, int val) {
         synchronized (stateLock) {
             switch (type) {
-                case SSL_CB_HANDSHAKE_START:
+                case SSL_CB_HANDSHAKE_START: {
                     // For clients, this will allow the NEED_UNWRAP status to be
                     // returned.
-                    engineState = EngineState.HANDSHAKE_STARTED;
+                    state = STATE_HANDSHAKE_STARTED;
                     break;
-                case SSL_CB_HANDSHAKE_DONE:
-                    if (engineState != EngineState.HANDSHAKE_STARTED
-                            && engineState != EngineState.READY_HANDSHAKE_CUT_THROUGH) {
+                }
+                case SSL_CB_HANDSHAKE_DONE: {
+                    if (state != STATE_HANDSHAKE_STARTED
+                            && state != STATE_READY_HANDSHAKE_CUT_THROUGH) {
                         throw new IllegalStateException(
-                                "Completed handshake while in mode " + engineState);
+                                "Completed handshake while in mode " + state);
                     }
-                    engineState = EngineState.HANDSHAKE_COMPLETED;
+                    state = STATE_HANDSHAKE_COMPLETED;
                     break;
-
+                }
             }
         }
     }
 
     @Override
+    public void onNewSessionEstablished(long sslSessionNativePtr) {
+        try {
+            // Increment the reference count to "take ownership" of the session resource.
+            NativeCrypto.SSL_SESSION_up_ref(sslSessionNativePtr);
+
+            // Create a native reference which will release the SSL_SESSION in its finalizer.
+            // This constructor will only throw if the native pointer passed in is NULL, which
+            // BoringSSL guarantees will not happen.
+            NativeRef.SSL_SESSION ref = new SSL_SESSION(sslSessionNativePtr);
+
+            SslSessionWrapper sessionWrapper = SslSessionWrapper.newInstance(ref, sslSession);
+
+            // Cache the newly established session.
+            AbstractSessionContext ctx = sessionContext();
+            ctx.cacheSession(sessionWrapper);
+        } catch (Exception ignored) {
+            // Ignore.
+        }
+    }
+
+    @Override
+    public long serverSessionRequested(byte[] id) {
+        // TODO(nathanmittler): Implement server-side caching for TLS < 1.3
+        return 0;
+    }
+
+    @Override
     public void verifyCertificateChain(long[] certRefs, String authMethod)
             throws CertificateException {
         try {
@@ -1349,13 +1571,8 @@
             OpenSSLX509Certificate[] peerCertChain =
                     OpenSSLX509Certificate.createCertChain(certRefs);
 
-            byte[] ocspData = NativeCrypto.SSL_get_ocsp_response(sslNativePointer);
-            byte[] tlsSctData = NativeCrypto.SSL_get_signed_cert_timestamp_list(sslNativePointer);
-
-            // Used for verifyCertificateChain callback
-            handshakeSession = new OpenSSLSessionImpl(
-                    NativeCrypto.SSL_get1_session(sslNativePointer), null, peerCertChain, ocspData,
-                    tlsSctData, getSniHostname(), getPeerPort(), null);
+            // Update the peer information on the session.
+            sslSession.onPeerCertificatesReceived(getPeerHost(), getPeerPort(), peerCertChain);
 
             if (getUseClientMode()) {
                 Platform.checkServerTrusted(x509tm, peerCertChain, authMethod, this);
@@ -1367,21 +1584,18 @@
             throw e;
         } catch (Exception e) {
             throw new CertificateException(e);
-        } finally {
-            handshakeSession = null;
         }
     }
 
     @Override
     public void clientCertificateRequested(byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals)
             throws CertificateEncodingException, SSLException {
-        sslParameters.chooseClientCertificate(
-                keyTypeBytes, asn1DerEncodedPrincipals, sslNativePointer, this);
+        ssl.chooseClientCertificate(keyTypeBytes, asn1DerEncodedPrincipals);
     }
 
     private void shutdown() {
         try {
-            NativeCrypto.ENGINE_SSL_shutdown(sslNativePointer, this);
+            ssl.shutdown();
         } catch (IOException ignored) {
             // TODO: The RI ignores close failures in SSLSocket, but need to
             // investigate whether it does for SSLEngine.
@@ -1397,13 +1611,10 @@
     }
 
     private void free() {
-        if (sslNativePointer == 0) {
-            return;
+        if (!ssl.isClosed()) {
+            ssl.close();
+            networkBio.close();
         }
-        NativeCrypto.SSL_free(sslNativePointer);
-        NativeCrypto.BIO_free_all(networkBio);
-        sslNativePointer = 0;
-        networkBio = 0;
     }
 
     @Override
@@ -1415,12 +1626,6 @@
         }
     }
 
-    /* @Override */
-    @SuppressWarnings("MissingOverride") // For compilation with Java 6.
-    public SSLSession getHandshakeSession() {
-        return handshakeSession;
-    }
-
     @Override
     public String chooseServerAlias(X509KeyManager keyManager, String keyType) {
         if (keyManager instanceof X509ExtendedKeyManager) {
@@ -1479,31 +1684,20 @@
     }
 
     /**
+     * Sets the list of ALPN protocols.
+     *
+     * @param alpnProtocols the list of ALPN protocols
+     */
+    void setAlpnProtocols(byte[] alpnProtocols) {
+        sslParameters.setAlpnProtocols(alpnProtocols);
+    }
+
+    /**
      * Returns the protocol agreed upon by client and server, or {@code null} if no protocol was
      * agreed upon.
      */
     byte[] getAlpnSelectedProtocol() {
-        return NativeCrypto.SSL_get0_alpn_selected(sslNativePointer);
-    }
-
-    private ByteBuffer toHeapBuffer(ByteBuffer buffer, int len) {
-        if (buffer.hasArray()) {
-            return buffer;
-        }
-
-        // Need to copy to a heap buffer.
-        final ByteBuffer heapBuffer = ByteBuffer.allocate(len);
-        final int pos = buffer.position();
-        final int limit = buffer.limit();
-        buffer.limit(pos + len);
-        try {
-            heapBuffer.put(buffer);
-            heapBuffer.flip();
-            return heapBuffer;
-        } finally {
-            buffer.limit(limit);
-            buffer.position(pos);
-        }
+        return ssl.getAlpnSelectedProtocol();
     }
 
     private ByteBuffer[] singleSrcBuffer(ByteBuffer src) {
@@ -1524,18 +1718,11 @@
         singleDstBuffer[0] = null;
     }
 
-    private static void checkIndex(int arrayLength, int offset, int length, String arrayName) {
-        if ((offset | length) < 0 || offset + length > arrayLength) {
-            throw new IndexOutOfBoundsException("offset: " + offset + ", length: " + length
-                    + " (expected: offset <= offset + length <= " + arrayName + ".length ("
-                    + arrayLength + "))");
-        }
+    private ClientSessionContext clientSessionContext() {
+        return sslParameters.getClientSessionContext();
     }
 
-    private static <T> T checkNotNull(T obj, String fmt, Object... args) {
-        if (obj == null) {
-            throw new IllegalArgumentException(String.format(fmt, args));
-        }
-        return obj;
+    private AbstractSessionContext sessionContext() {
+        return sslParameters.getSessionContext();
     }
 }
diff --git a/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java b/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java
new file mode 100644
index 0000000..f40efdd
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java
@@ -0,0 +1,729 @@
+/*
+ * Copyright 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import static javax.net.ssl.SSLEngineResult.Status.OK;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_CLOSED;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_HANDSHAKE_COMPLETED;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_HANDSHAKE_STARTED;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_NEW;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_READY;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_READY_HANDSHAKE_CUT_THROUGH;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.security.PrivateKey;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSession;
+
+/**
+ * Implements crypto handling by delegating to {@link ConscryptEngine}.
+ */
+final class ConscryptEngineSocket extends OpenSSLSocketImpl {
+    private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
+
+    private final ConscryptEngine engine;
+    private final Object stateLock = new Object();
+    private final Object handshakeLock = new Object();
+
+    private SSLOutputStream out;
+    private SSLInputStream in;
+
+    // @GuardedBy("stateLock");
+    private int state = STATE_NEW;
+
+    ConscryptEngineSocket(SSLParametersImpl sslParameters) throws IOException {
+        engine = newEngine(sslParameters, this);
+    }
+
+    ConscryptEngineSocket(String hostname, int port, SSLParametersImpl sslParameters)
+            throws IOException {
+        super(hostname, port);
+        engine = newEngine(sslParameters, this);
+    }
+
+    ConscryptEngineSocket(InetAddress address, int port, SSLParametersImpl sslParameters)
+            throws IOException {
+        super(address, port);
+        engine = newEngine(sslParameters, this);
+    }
+
+    ConscryptEngineSocket(String hostname, int port, InetAddress clientAddress, int clientPort,
+            SSLParametersImpl sslParameters) throws IOException {
+        super(hostname, port, clientAddress, clientPort);
+        engine = newEngine(sslParameters, this);
+    }
+
+    ConscryptEngineSocket(InetAddress address, int port, InetAddress clientAddress, int clientPort,
+            SSLParametersImpl sslParameters) throws IOException {
+        super(address, port, clientAddress, clientPort);
+        engine = newEngine(sslParameters, this);
+    }
+
+    ConscryptEngineSocket(Socket socket, String hostname, int port, boolean autoClose,
+            SSLParametersImpl sslParameters) throws IOException {
+        super(socket, hostname, port, autoClose);
+        engine = newEngine(sslParameters, this);
+    }
+
+    private static ConscryptEngine newEngine(
+            SSLParametersImpl sslParameters, final ConscryptEngineSocket socket) {
+        ConscryptEngine engine = new ConscryptEngine(sslParameters, socket.peerInfoProvider());
+
+        // When the handshake completes, notify any listeners.
+        engine.setHandshakeListener(new HandshakeListener() {
+            /**
+             * Protected by {@code stateLock}
+             */
+            @Override
+            public void onHandshakeFinished() {
+                // Just call the outer class method.
+                socket.onHandshakeFinished();
+            }
+        });
+
+        // Transition the engine state to MODE_SET
+        engine.setUseClientMode(sslParameters.getUseClientMode());
+        return engine;
+    }
+
+    @Override
+    public SSLParameters getSSLParameters() {
+        return engine.getSSLParameters();
+    }
+
+    @Override
+    public void setSSLParameters(SSLParameters sslParameters) {
+        engine.setSSLParameters(sslParameters);
+    }
+
+    @Override
+    public void startHandshake() throws IOException {
+        checkOpen();
+
+        if (isHandshakeFinished()) {
+            // TODO(nmittler): Handle renegotiation.
+            return;
+        }
+
+        try {
+            synchronized (handshakeLock) {
+                // Only lock stateLock when we begin the handshake. This is done so that we don't
+                // hold the stateLock when we invoke the handshake completion listeners.
+                synchronized (stateLock) {
+                    // Initialize the handshake if we haven't already.
+                    if (state == STATE_NEW) {
+                        state = STATE_HANDSHAKE_STARTED;
+                        engine.beginHandshake();
+                        in = new SSLInputStream();
+                        out = new SSLOutputStream();
+                    } else {
+                        // We've either started the handshake already or have been closed.
+                        // Do nothing in both cases.
+                        return;
+                    }
+                }
+
+                boolean finished = false;
+                while (!finished) {
+                    switch (engine.getHandshakeStatus()) {
+                        case NEED_UNWRAP:
+                            if (in.readInternal(EmptyArray.BYTE, 0, 0) < 0) {
+                                // Can't complete the handshake due to EOF.
+                                throw SSLUtils.toSSLHandshakeException(new EOFException());
+                            }
+                            break;
+                        case NEED_WRAP: {
+                            out.writeInternal(EMPTY_BUFFER);
+                            // Always flush handshake frames immediately.
+                            out.flushInternal();
+                            break;
+                        }
+                        case NEED_TASK:
+                            // Should never get here, since our engine never provides tasks.
+                            throw new IllegalStateException("Engine tasks are unsupported");
+                        case NOT_HANDSHAKING:
+                        case FINISHED:
+                            // Handshake is complete.
+                            finished = true;
+                            break;
+                        default: {
+                            throw new IllegalStateException(
+                                    "Unknown handshake status: " + engine.getHandshakeStatus());
+                        }
+                    }
+                }
+            }
+        } catch (SSLException e) {
+            close();
+            throw e;
+        } catch (IOException e) {
+            close();
+            throw e;
+        } catch (Exception e) {
+            close();
+            // Convert anything else to a handshake exception.
+            throw SSLUtils.toSSLHandshakeException(e);
+        }
+    }
+
+    @Override
+    public InputStream getInputStream() throws IOException {
+        checkOpen();
+
+        // Block waiting for a handshake without a lock held. It's possible that the socket
+        // is closed at this point. If that happens, we'll still return the input stream but
+        // all reads on it will throw.
+        waitForHandshake();
+        return in;
+    }
+
+    @Override
+    public OutputStream getOutputStream() throws IOException {
+        checkOpen();
+
+        // Block waiting for a handshake without a lock held. It's possible that the socket
+        // is closed at this point. If that happens, we'll still return the input stream but
+        // all reads on it will throw.
+        waitForHandshake();
+
+        return out;
+    }
+
+    @Override
+    public SSLSession getHandshakeSession() {
+        return engine.handshakeSession();
+    }
+
+    @Override
+    public SSLSession getSession() {
+        SSLSession session = engine.getSession();
+        if (SSLNullSession.isNullSession(session)) {
+            boolean handshakeCompleted = false;
+            try {
+                if (isConnected()) {
+                    waitForHandshake();
+                    handshakeCompleted = true;
+                }
+            } catch (IOException e) {
+                // Fall through.
+            }
+
+            if (!handshakeCompleted) {
+                // Return an invalid session with invalid cipher suite of "SSL_NULL_WITH_NULL_NULL"
+                return session;
+            }
+            session = engine.getSession();
+        }
+        return session;
+    }
+
+    @Override
+    SSLSession getActiveSession() {
+        return engine.getSession();
+    }
+
+    @Override
+    public boolean getEnableSessionCreation() {
+        return engine.getEnableSessionCreation();
+    }
+
+    @Override
+    public void setEnableSessionCreation(boolean flag) {
+        engine.setEnableSessionCreation(flag);
+    }
+
+    @Override
+    public String[] getSupportedCipherSuites() {
+        return engine.getSupportedCipherSuites();
+    }
+
+    @Override
+    public String[] getEnabledCipherSuites() {
+        return engine.getEnabledCipherSuites();
+    }
+
+    @Override
+    public void setEnabledCipherSuites(String[] suites) {
+        engine.setEnabledCipherSuites(suites);
+    }
+
+    @Override
+    public String[] getSupportedProtocols() {
+        return engine.getSupportedProtocols();
+    }
+
+    @Override
+    public String[] getEnabledProtocols() {
+        return engine.getEnabledProtocols();
+    }
+
+    @Override
+    public void setEnabledProtocols(String[] protocols) {
+        engine.setEnabledProtocols(protocols);
+    }
+
+    /**
+     * This method enables Server Name Indication
+     *
+     * @param hostname the desired SNI hostname, or null to disable
+     */
+    @Override
+    public void setHostname(String hostname) {
+        engine.setHostname(hostname);
+        super.setHostname(hostname);
+    }
+
+    @Override
+    public void setUseSessionTickets(boolean useSessionTickets) {
+        engine.setUseSessionTickets(useSessionTickets);
+    }
+
+    @Override
+    public void setChannelIdEnabled(boolean enabled) {
+        engine.setChannelIdEnabled(enabled);
+    }
+
+    @Override
+    public byte[] getChannelId() throws SSLException {
+        return engine.getChannelId();
+    }
+
+    @Override
+    public void setChannelIdPrivateKey(PrivateKey privateKey) {
+        engine.setChannelIdPrivateKey(privateKey);
+    }
+
+    @Override
+    public boolean getUseClientMode() {
+        return engine.getUseClientMode();
+    }
+
+    @Override
+    public void setUseClientMode(boolean mode) {
+        engine.setUseClientMode(mode);
+    }
+
+    @Override
+    public boolean getWantClientAuth() {
+        return engine.getWantClientAuth();
+    }
+
+    @Override
+    public boolean getNeedClientAuth() {
+        return engine.getNeedClientAuth();
+    }
+
+    @Override
+    public void setNeedClientAuth(boolean need) {
+        engine.setNeedClientAuth(need);
+    }
+
+    @Override
+    public void setWantClientAuth(boolean want) {
+        engine.setWantClientAuth(want);
+    }
+
+    @Override
+    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
+    public void close() throws IOException {
+        // TODO: Close SSL sockets using a background thread so they close gracefully.
+
+        synchronized (stateLock) {
+            if (state == STATE_CLOSED) {
+                // close() has already been called, so do nothing and return.
+                return;
+            }
+
+            state = STATE_CLOSED;
+
+            stateLock.notifyAll();
+        }
+
+        // Close the underlying socket.
+        super.close();
+
+        // Close the engine.
+        engine.closeInbound();
+        engine.closeOutbound();
+    }
+
+    @Override
+    public byte[] getAlpnSelectedProtocol() {
+        return engine.getAlpnSelectedProtocol();
+    }
+
+    @Override
+    public void setAlpnProtocols(byte[] alpnProtocols) {
+        engine.setAlpnProtocols(alpnProtocols);
+    }
+
+    @Override
+    public void setAlpnProtocols(String[] alpnProtocols) {
+        engine.setAlpnProtocols(alpnProtocols);
+    }
+
+    private boolean isHandshakeFinished() {
+        return state >= STATE_READY_HANDSHAKE_CUT_THROUGH;
+    }
+
+    private void onHandshakeFinished() {
+        boolean notify = false;
+        synchronized (stateLock) {
+            if (state != STATE_CLOSED) {
+                if (state == STATE_HANDSHAKE_STARTED) {
+                    state = STATE_READY_HANDSHAKE_CUT_THROUGH;
+                } else if (state == STATE_HANDSHAKE_COMPLETED) {
+                    state = STATE_READY;
+                }
+
+                // Unblock threads that are waiting for our state to transition
+                // into STATE_READY or STATE_READY_HANDSHAKE_CUT_THROUGH.
+                stateLock.notifyAll();
+                notify = true;
+            }
+        }
+
+        if (notify) {
+            notifyHandshakeCompletedListeners();
+        }
+    }
+
+    /**
+     * Waits for the handshake to complete.
+     */
+    private void waitForHandshake() throws IOException {
+        startHandshake();
+
+        synchronized (stateLock) {
+            while (state != STATE_READY && state != STATE_READY_HANDSHAKE_CUT_THROUGH
+                    && state != STATE_CLOSED) {
+                try {
+                    stateLock.wait();
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    throw new IOException("Interrupted waiting for handshake", e);
+                }
+            }
+
+            if (state == STATE_CLOSED) {
+                throw new SocketException("Socket is closed");
+            }
+        }
+    }
+
+    private OutputStream getUnderlyingOutputStream() throws IOException {
+        return super.getOutputStream();
+    }
+
+    private InputStream getUnderlyingInputStream() throws IOException {
+        return super.getInputStream();
+    }
+
+    private SocketChannel getUnderlyingChannel() throws IOException {
+        return super.getChannel();
+    }
+
+    /**
+     * Wrap bytes written to the underlying socket.
+     */
+    private final class SSLOutputStream extends OutputStream {
+        private final Object writeLock = new Object();
+        private ByteBuffer target;
+        private OutputStream socketOutputStream;
+        private SocketChannel socketChannel;
+
+        SSLOutputStream() {}
+
+        @Override
+        public void close() throws IOException {
+            ConscryptEngineSocket.this.close();
+        }
+
+        @Override
+        public void write(int b) throws IOException {
+            startHandshake();
+            synchronized (writeLock) {
+                write(new byte[] {(byte) b});
+            }
+        }
+
+        @Override
+        public void write(byte[] b) throws IOException {
+            startHandshake();
+            synchronized (writeLock) {
+                writeInternal(ByteBuffer.wrap(b));
+            }
+        }
+
+        @Override
+        public void write(byte[] b, int off, int len) throws IOException {
+            startHandshake();
+            synchronized (writeLock) {
+                writeInternal(ByteBuffer.wrap(b, off, len));
+            }
+        }
+
+        private void writeInternal(ByteBuffer buffer) throws IOException {
+            Platform.blockGuardOnNetwork();
+            checkOpen();
+            init();
+
+            // Need to loop through at least once to enable handshaking where no application
+            // bytes are
+            // processed.
+            int len = buffer.remaining();
+            SSLEngineResult engineResult;
+            do {
+                target.clear();
+                engineResult = engine.wrap(buffer, target);
+                if (engineResult.getStatus() != OK) {
+                    throw new SSLException("Unexpected engine result " + engineResult.getStatus());
+                }
+                if (target.position() != engineResult.bytesProduced()) {
+                    throw new SSLException("Engine bytesProduced " + engineResult.bytesProduced()
+                            + " does not match bytes written " + target.position());
+                }
+                len -= engineResult.bytesConsumed();
+                if (len != buffer.remaining()) {
+                    throw new SSLException("Engine did not read the correct number of bytes");
+                }
+
+                target.flip();
+
+                // Write the data to the socket.
+                writeToSocket();
+            } while (len > 0);
+        }
+
+        @Override
+        public void flush() throws IOException {
+            startHandshake();
+            synchronized (writeLock) {
+                flushInternal();
+            }
+        }
+
+        private void flushInternal() throws IOException {
+            checkOpen();
+            init();
+            socketOutputStream.flush();
+        }
+
+        private void init() throws IOException {
+            if (socketOutputStream == null) {
+                socketOutputStream = getUnderlyingOutputStream();
+                socketChannel = getUnderlyingChannel();
+                if (socketChannel != null) {
+                    // Optimization. Using direct buffers wherever possible to avoid passing
+                    // arrays to JNI.
+                    target = ByteBuffer.allocateDirect(engine.getSession().getPacketBufferSize());
+                } else {
+                    target = ByteBuffer.allocate(engine.getSession().getPacketBufferSize());
+                }
+            }
+        }
+
+        private void writeToSocket() throws IOException {
+            // Write the data to the socket.
+            if (socketChannel != null) {
+                // Loop until all of the data is written to the channel. Typically,
+                // SocketChannel writes will return only after all bytes are written,
+                // so we won't really loop here.
+                while (target.hasRemaining()) {
+                    socketChannel.write(target);
+                }
+            } else {
+                // Target is a heap buffer.
+                socketOutputStream.write(target.array(), 0, target.limit());
+            }
+        }
+    }
+
+    /**
+     * Unwrap bytes read from the underlying socket.
+     */
+    private final class SSLInputStream extends InputStream {
+        private final Object readLock = new Object();
+        private final byte[] singleByte = new byte[1];
+        private final ByteBuffer fromEngine;
+        private ByteBuffer fromSocket;
+        private InputStream socketInputStream;
+        private SocketChannel socketChannel;
+
+        SSLInputStream() {
+            fromEngine = ByteBuffer.allocateDirect(engine.getSession().getApplicationBufferSize());
+            // Initially fromEngine.remaining() == 0.
+            fromEngine.flip();
+        }
+
+        @Override
+        public void close() throws IOException {
+            ConscryptEngineSocket.this.close();
+        }
+
+        @Override
+        public int read() throws IOException {
+            startHandshake();
+            synchronized (readLock) {
+                // Handle returning of -1 if EOF is reached.
+                int count = read(singleByte, 0, 1);
+                if (count == -1) {
+                    // Handle EOF.
+                    return -1;
+                }
+                if (count != 1) {
+                    throw new SSLException("read incorrect number of bytes " + count);
+                }
+                return (int) singleByte[0];
+            }
+        }
+
+        @Override
+        public int read(byte[] b) throws IOException {
+            startHandshake();
+            synchronized (readLock) {
+                return read(b, 0, b.length);
+            }
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            startHandshake();
+            synchronized (readLock) {
+                return readInternal(b, off, len);
+            }
+        }
+
+        @Override
+        public int available() throws IOException {
+            startHandshake();
+            synchronized (readLock) {
+                init();
+                return fromEngine.remaining()
+                        + (fromSocket.hasRemaining() || socketInputStream.available() > 0 ? 1 : 0);
+            }
+        }
+
+        private int readInternal(byte[] b, int off, int len) throws IOException {
+            Platform.blockGuardOnNetwork();
+            checkOpen();
+
+            // Make sure the input stream has been created.
+            init();
+
+            for (;;) {
+                // Serve any remaining data from the engine first.
+                if (fromEngine.remaining() > 0) {
+                    int readFromEngine = Math.min(fromEngine.remaining(), len);
+                    fromEngine.get(b, off, readFromEngine);
+                    return readFromEngine;
+                }
+
+                // Try to unwrap any data already in the socket buffer.
+                boolean needMoreDataFromSocket = true;
+
+                // Unwrap the unencrypted bytes into the engine buffer.
+                fromSocket.flip();
+                fromEngine.clear();
+                SSLEngineResult engineResult = engine.unwrap(fromSocket, fromEngine);
+                // Shift any remaining data to the beginning of the buffer so that
+                // we can accommodate the next full packet. After this is called,
+                // limit will be restored to capacity and position will point just
+                // past the end of the data.
+                fromSocket.compact();
+                fromEngine.flip();
+
+                switch (engineResult.getStatus()) {
+                    case BUFFER_UNDERFLOW: {
+                        if (engineResult.bytesProduced() == 0) {
+                            // Need to read more data from the socket.
+                            break;
+                        }
+                        // Also serve the data that was produced.
+                        needMoreDataFromSocket = false;
+                        break;
+                    }
+                    case OK: {
+                        // We processed the entire packet successfully.
+                        needMoreDataFromSocket = false;
+                        break;
+                    }
+                    case CLOSED: {
+                        // EOF
+                        return -1;
+                    }
+                    default: {
+                        // Anything else is an error.
+                        throw new SSLException(
+                                "Unexpected engine result " + engineResult.getStatus());
+                    }
+                }
+
+                if (!needMoreDataFromSocket && engineResult.bytesProduced() == 0) {
+                    // Read successfully, but produced no data. Possibly part of a
+                    // handshake.
+                    return 0;
+                }
+
+                // Read more data from the socket.
+                if (needMoreDataFromSocket && readFromSocket() == -1) {
+                    // Failed to read the next encrypted packet before reaching EOF.
+                    return -1;
+                }
+
+                // Continue the loop and return the data from the engine buffer.
+            }
+        }
+
+        private void init() throws IOException {
+            if (socketInputStream == null) {
+                socketInputStream = getUnderlyingInputStream();
+                socketChannel = getUnderlyingChannel();
+                if (socketChannel != null) {
+                    fromSocket =
+                            ByteBuffer.allocateDirect(engine.getSession().getPacketBufferSize());
+                } else {
+                    fromSocket = ByteBuffer.allocate(engine.getSession().getPacketBufferSize());
+                }
+            }
+        }
+
+        private int readFromSocket() throws IOException {
+            if (socketChannel != null) {
+                return socketChannel.read(fromSocket);
+            }
+            // Read directly to the underlying array and increment the buffer position if
+            // appropriate.
+            int read = socketInputStream.read(
+                    fromSocket.array(), fromSocket.position(), fromSocket.remaining());
+            if (read > 0) {
+                fromSocket.position(fromSocket.position() + read);
+            }
+            return read;
+        }
+    }
+}
diff --git a/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java b/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java
new file mode 100644
index 0000000..3f800c5
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java
@@ -0,0 +1,1103 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import static org.conscrypt.SSLUtils.EngineStates.STATE_CLOSED;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_HANDSHAKE_STARTED;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_NEW;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_READY;
+import static org.conscrypt.SSLUtils.EngineStates.STATE_READY_HANDSHAKE_CUT_THROUGH;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketException;
+import java.security.InvalidKeyException;
+import java.security.PrivateKey;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.interfaces.ECKey;
+import java.security.spec.ECParameterSpec;
+import javax.crypto.SecretKey;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLProtocolException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+import javax.security.auth.x500.X500Principal;
+import org.conscrypt.NativeRef.SSL_SESSION;
+
+/**
+ * Implementation of the class OpenSSLSocketImpl based on OpenSSL.
+ * <p>
+ * Extensions to SSLSocket include:
+ * <ul>
+ * <li>handshake timeout
+ * <li>session tickets
+ * <li>Server Name Indication
+ * </ul>
+ */
+final class ConscryptFileDescriptorSocket extends OpenSSLSocketImpl
+        implements NativeCrypto.SSLHandshakeCallbacks, SSLParametersImpl.AliasChooser,
+                   SSLParametersImpl.PSKCallbacks {
+    private static final boolean DBG_STATE = false;
+
+    /**
+     * Protects handshakeStarted and handshakeCompleted.
+     */
+    private final Object stateLock = new Object();
+
+    // @GuardedBy("stateLock");
+    private int state = STATE_NEW;
+
+    /**
+     * Protected by synchronizing on stateLock. Starts as 0, set by
+     * startHandshake, reset to 0 on close.
+     */
+    // @GuardedBy("stateLock");
+    private final SslWrapper ssl;
+
+    /**
+     * Protected by synchronizing on stateLock. Starts as null, set by
+     * getInputStream.
+     */
+    // @GuardedBy("stateLock");
+    private SSLInputStream is;
+
+    /**
+     * Protected by synchronizing on stateLock. Starts as null, set by
+     * getInputStream.
+     */
+    // @GuardedBy("stateLock");
+    private SSLOutputStream os;
+
+    private final SSLParametersImpl sslParameters;
+
+    /*
+     * A CloseGuard object on Android. On other platforms, this is nothing.
+     */
+    private final Object guard = Platform.closeGuardGet();
+
+    /**
+     * Private key for the TLS Channel ID extension. This field is client-side
+     * only. Set during startHandshake.
+     */
+    private OpenSSLKey channelIdPrivateKey;
+
+    private final ActiveSession sslSession;
+
+    private int writeTimeoutMilliseconds = 0;
+    private int handshakeTimeoutMilliseconds = -1; // -1 = same as timeout; 0 = infinite
+
+    ConscryptFileDescriptorSocket(SSLParametersImpl sslParameters) throws IOException {
+        this.sslParameters = sslParameters;
+        this.ssl = newSsl(sslParameters, this);
+        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+    }
+
+    ConscryptFileDescriptorSocket(String hostname, int port, SSLParametersImpl sslParameters)
+            throws IOException {
+        super(hostname, port);
+        this.sslParameters = sslParameters;
+        this.ssl = newSsl(sslParameters, this);
+        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+    }
+
+    ConscryptFileDescriptorSocket(InetAddress address, int port, SSLParametersImpl sslParameters)
+            throws IOException {
+        super(address, port);
+        this.sslParameters = sslParameters;
+        this.ssl = newSsl(sslParameters, this);
+        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+    }
+
+    ConscryptFileDescriptorSocket(String hostname, int port, InetAddress clientAddress,
+            int clientPort, SSLParametersImpl sslParameters) throws IOException {
+        super(hostname, port, clientAddress, clientPort);
+        this.sslParameters = sslParameters;
+        this.ssl = newSsl(sslParameters, this);
+        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+    }
+
+    ConscryptFileDescriptorSocket(InetAddress address, int port, InetAddress clientAddress,
+            int clientPort, SSLParametersImpl sslParameters) throws IOException {
+        super(address, port, clientAddress, clientPort);
+        this.sslParameters = sslParameters;
+        this.ssl = newSsl(sslParameters, this);
+        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+    }
+
+    ConscryptFileDescriptorSocket(Socket socket, String hostname, int port, boolean autoClose,
+            SSLParametersImpl sslParameters) throws IOException {
+        super(socket, hostname, port, autoClose);
+        this.sslParameters = sslParameters;
+        this.ssl = newSsl(sslParameters, this);
+        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+    }
+
+    private static SslWrapper newSsl(SSLParametersImpl sslParameters,
+            ConscryptFileDescriptorSocket engine) {
+        try {
+            return SslWrapper.newInstance(sslParameters, engine, engine, engine);
+        } catch (SSLException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Starts a TLS/SSL handshake on this connection using some native methods
+     * from the OpenSSL library. It can negotiate new encryption keys, change
+     * cipher suites, or initiate a new session. The certificate chain is
+     * verified if the correspondent property in java.Security is set. All
+     * listeners are notified at the end of the TLS/SSL handshake.
+     */
+    @Override
+    public void startHandshake() throws IOException {
+        checkOpen();
+        synchronized (stateLock) {
+            if (state == STATE_NEW) {
+                state = STATE_HANDSHAKE_STARTED;
+            } else {
+                // We've either started the handshake already or have been closed.
+                // Do nothing in both cases.
+                return;
+            }
+        }
+
+        boolean releaseResources = true;
+        try {
+            Platform.closeGuardOpen(guard, "close");
+
+            // Prepare the SSL object for the handshake.
+            ssl.initialize(getHostname(), channelIdPrivateKey);
+
+            // For clients, offer to resume a previously cached session to avoid the
+            // full TLS handshake.
+            if (getUseClientMode()) {
+                SslSessionWrapper cachedSession = clientSessionContext().getCachedSession(
+                        getHostnameOrIP(), getPort(), sslParameters);
+                if (cachedSession != null) {
+                    cachedSession.offerToResume(ssl);
+                }
+            }
+
+            // Temporarily use a different timeout for the handshake process
+            int savedReadTimeoutMilliseconds = getSoTimeout();
+            int savedWriteTimeoutMilliseconds = getSoWriteTimeout();
+            if (handshakeTimeoutMilliseconds >= 0) {
+                setSoTimeout(handshakeTimeoutMilliseconds);
+                setSoWriteTimeout(handshakeTimeoutMilliseconds);
+            }
+
+            synchronized (stateLock) {
+                if (state == STATE_CLOSED) {
+                    return;
+                }
+            }
+
+            try {
+                ssl.doHandshake(Platform.getFileDescriptor(socket), getSoTimeout());
+            } catch (CertificateException e) {
+                SSLHandshakeException wrapper = new SSLHandshakeException(e.getMessage());
+                wrapper.initCause(e);
+                throw wrapper;
+            } catch (SSLException e) {
+                // Swallow this exception if it's thrown as the result of an interruption.
+                //
+                // TODO: SSL_read and SSL_write return -1 when interrupted, but SSL_do_handshake
+                // will throw the last sslError that it saw before sslSelect, usually SSL_WANT_READ
+                // (or WANT_WRITE). Catching that exception here doesn't seem much worse than
+                // changing the native code to return a "special" native pointer value when that
+                // happens.
+                synchronized (stateLock) {
+                    if (state == STATE_CLOSED) {
+                        return;
+                    }
+                }
+
+                // Write CCS errors to EventLog
+                String message = e.getMessage();
+                // Must match error string of SSL_R_UNEXPECTED_CCS
+                if (message.contains("unexpected CCS")) {
+                    String logMessage =
+                            String.format("ssl_unexpected_ccs: host=%s", getHostnameOrIP());
+                    Platform.logEvent(logMessage);
+                }
+
+                throw e;
+            }
+
+            synchronized (stateLock) {
+                if (state == STATE_CLOSED) {
+                    return;
+                }
+            }
+
+            // Restore the original timeout now that the handshake is complete
+            if (handshakeTimeoutMilliseconds >= 0) {
+                setSoTimeout(savedReadTimeoutMilliseconds);
+                setSoWriteTimeout(savedWriteTimeoutMilliseconds);
+            }
+
+            synchronized (stateLock) {
+                releaseResources = (state == STATE_CLOSED);
+
+                if (state == STATE_HANDSHAKE_STARTED) {
+                    state = STATE_READY_HANDSHAKE_CUT_THROUGH;
+                } else {
+                    state = STATE_READY;
+                }
+
+                if (!releaseResources) {
+                    // Unblock threads that are waiting for our state to transition
+                    // into STATE_READY or STATE_READY_HANDSHAKE_CUT_THROUGH.
+                    stateLock.notifyAll();
+                }
+            }
+        } catch (SSLProtocolException e) {
+            throw(SSLHandshakeException) new SSLHandshakeException("Handshake failed").initCause(e);
+        } finally {
+            // on exceptional exit, treat the socket as closed
+            if (releaseResources) {
+                synchronized (stateLock) {
+                    // Mark the socket as closed since we might have reached this as
+                    // a result on an exception thrown by the handshake process.
+                    //
+                    // The state will already be set to closed if we reach this as a result of
+                    // an early return or an interruption due to a concurrent call to close().
+                    state = STATE_CLOSED;
+                    stateLock.notifyAll();
+                }
+
+                try {
+                    shutdownAndFreeSslNative();
+                } catch (IOException ignored) {
+                    // Ignored.
+                }
+            }
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unused") // used by NativeCrypto.SSLHandshakeCallbacks / client_cert_cb
+    public void clientCertificateRequested(byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals)
+            throws CertificateEncodingException, SSLException {
+        ssl.chooseClientCertificate(keyTypeBytes, asn1DerEncodedPrincipals);
+    }
+
+    @Override
+    @SuppressWarnings("unused") // used by native psk_client_callback
+    public int clientPSKKeyRequested(String identityHint, byte[] identity, byte[] key) {
+        return ssl.clientPSKKeyRequested(identityHint, identity, key);
+    }
+
+    @Override
+    @SuppressWarnings("unused") // used by native psk_server_callback
+    public int serverPSKKeyRequested(String identityHint, String identity, byte[] key) {
+        return ssl.serverPSKKeyRequested(identityHint, identity, key);
+    }
+
+    @Override
+    @SuppressWarnings("unused") // used by NativeCrypto.SSLHandshakeCallbacks / info_callback
+    public void onSSLStateChange(int type, int val) {
+        if (type != NativeConstants.SSL_CB_HANDSHAKE_DONE) {
+            // We only care about successful completion.
+            return;
+        }
+
+        // The handshake has completed successfully ...
+
+        // Update the session from the current state of the SSL object.
+        sslSession.onSessionEstablished(getHostnameOrIP(), getPort());
+
+        // First, update the state.
+        synchronized (stateLock) {
+            if (state == STATE_CLOSED) {
+                // Someone called "close" but the handshake hasn't been interrupted yet.
+                return;
+            }
+
+            // Now that we've fixed up our state, we can tell waiting threads that
+            // we're ready.
+            state = STATE_READY;
+        }
+
+        // Let listeners know we are finally done
+        notifyHandshakeCompletedListeners();
+
+        synchronized (stateLock) {
+            // Notify all threads waiting for the handshake to complete.
+            stateLock.notifyAll();
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unused") // used by NativeCrypto.SSLHandshakeCallbacks / new_session_callback
+    public void onNewSessionEstablished(long sslSessionNativePtr) {
+        try {
+            // Increment the reference count to "take ownership" of the session resource.
+            NativeCrypto.SSL_SESSION_up_ref(sslSessionNativePtr);
+
+            // Create a native reference which will release the SSL_SESSION in its finalizer.
+            // This constructor will only throw if the native pointer passed in is NULL, which
+            // BoringSSL guarantees will not happen.
+            NativeRef.SSL_SESSION ref = new SSL_SESSION(sslSessionNativePtr);
+
+            SslSessionWrapper sessionWrapper = SslSessionWrapper.newInstance(ref, sslSession);
+
+            // Cache the newly established session.
+            AbstractSessionContext ctx = sessionContext();
+            ctx.cacheSession(sessionWrapper);
+        } catch (Exception ignored) {
+            // Ignore.
+        }
+    }
+
+    @Override
+    public long serverSessionRequested(byte[] id) {
+        // TODO(nathanmittler): Implement server-side caching for TLS < 1.3
+        return 0;
+    }
+
+    @SuppressWarnings("unused") // used by NativeCrypto.SSLHandshakeCallbacks
+    @Override
+    public void verifyCertificateChain(long[] certRefs, String authMethod)
+            throws CertificateException {
+        try {
+            X509TrustManager x509tm = sslParameters.getX509TrustManager();
+            if (x509tm == null) {
+                throw new CertificateException("No X.509 TrustManager");
+            }
+            if (certRefs == null || certRefs.length == 0) {
+                throw new SSLException("Peer sent no certificate");
+            }
+            OpenSSLX509Certificate[] peerCertChain =
+                    OpenSSLX509Certificate.createCertChain(certRefs);
+
+            // Update the peer information on the session.
+            sslSession.onPeerCertificatesReceived(getHostnameOrIP(), getPort(), peerCertChain);
+
+            if (getUseClientMode()) {
+                Platform.checkServerTrusted(x509tm, peerCertChain, authMethod, this);
+            } else {
+                String authType = peerCertChain[0].getPublicKey().getAlgorithm();
+                Platform.checkClientTrusted(x509tm, peerCertChain, authType, this);
+            }
+        } catch (CertificateException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new CertificateException(e);
+        }
+    }
+
+    @Override
+    public InputStream getInputStream() throws IOException {
+        checkOpen();
+
+        InputStream returnVal;
+        synchronized (stateLock) {
+            if (state == STATE_CLOSED) {
+                throw new SocketException("Socket is closed.");
+            }
+
+            if (is == null) {
+                is = new SSLInputStream();
+            }
+
+            returnVal = is;
+        }
+
+        // Block waiting for a handshake without a lock held. It's possible that the socket
+        // is closed at this point. If that happens, we'll still return the input stream but
+        // all reads on it will throw.
+        waitForHandshake();
+        return returnVal;
+    }
+
+    @Override
+    public OutputStream getOutputStream() throws IOException {
+        checkOpen();
+
+        OutputStream returnVal;
+        synchronized (stateLock) {
+            if (state == STATE_CLOSED) {
+                throw new SocketException("Socket is closed.");
+            }
+
+            if (os == null) {
+                os = new SSLOutputStream();
+            }
+
+            returnVal = os;
+        }
+
+        // Block waiting for a handshake without a lock held. It's possible that the socket
+        // is closed at this point. If that happens, we'll still return the output stream but
+        // all writes on it will throw.
+        waitForHandshake();
+        return returnVal;
+    }
+
+    private void assertReadableOrWriteableState() {
+        if (state == STATE_READY || state == STATE_READY_HANDSHAKE_CUT_THROUGH) {
+            return;
+        }
+
+        throw new AssertionError("Invalid state: " + state);
+    }
+
+    private void waitForHandshake() throws IOException {
+        startHandshake();
+
+        synchronized (stateLock) {
+            while (state != STATE_READY &&
+                    state != STATE_READY_HANDSHAKE_CUT_THROUGH &&
+                    state != STATE_CLOSED) {
+                try {
+                    stateLock.wait();
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    throw new IOException("Interrupted waiting for handshake", e);
+                }
+            }
+
+            if (state == STATE_CLOSED) {
+                throw new SocketException("Socket is closed");
+            }
+        }
+    }
+
+    /**
+     * This inner class provides input data stream functionality
+     * for the OpenSSL native implementation. It is used to
+     * read data received via SSL protocol.
+     */
+    private class SSLInputStream extends InputStream {
+        /**
+         * OpenSSL only lets one thread read at a time, so this is used to
+         * make sure we serialize callers of SSL_read. Thread is already
+         * expected to have completed handshaking.
+         */
+        private final Object readLock = new Object();
+
+        SSLInputStream() {
+        }
+
+        /**
+         * Reads one byte. If there is no data in the underlying buffer,
+         * this operation can block until the data will be
+         * available.
+         */
+        @Override
+        public int read() throws IOException {
+            byte[] buffer = new byte[1];
+            int result = read(buffer, 0, 1);
+            return (result != -1) ? buffer[0] & 0xff : -1;
+        }
+
+        /**
+         * Method acts as described in spec for superclass.
+         * @see java.io.InputStream#read(byte[],int,int)
+         */
+        @Override
+        public int read(byte[] buf, int offset, int byteCount) throws IOException {
+            Platform.blockGuardOnNetwork();
+
+            checkOpen();
+            ArrayUtils.checkOffsetAndCount(buf.length, offset, byteCount);
+            if (byteCount == 0) {
+                return 0;
+            }
+
+            synchronized (readLock) {
+                synchronized (stateLock) {
+                    if (state == STATE_CLOSED) {
+                        throw new SocketException("socket is closed");
+                    }
+
+                    if (DBG_STATE) {
+                        assertReadableOrWriteableState();
+                    }
+                }
+
+                int ret =  ssl.read(
+                        Platform.getFileDescriptor(socket), buf, offset, byteCount, getSoTimeout());
+                if (ret == -1) {
+                    synchronized (stateLock) {
+                        if (state == STATE_CLOSED) {
+                            throw new SocketException("socket is closed");
+                        }
+                    }
+                }
+                return ret;
+            }
+        }
+
+        void awaitPendingOps() {
+            if (DBG_STATE) {
+                synchronized (stateLock) {
+                    if (state != STATE_CLOSED) {
+                        throw new AssertionError("State is: " + state);
+                    }
+                }
+            }
+
+            synchronized (readLock) {}
+        }
+    }
+
+    /**
+     * This inner class provides output data stream functionality
+     * for the OpenSSL native implementation. It is used to
+     * write data according to the encryption parameters given in SSL context.
+     */
+    private class SSLOutputStream extends OutputStream {
+        /**
+         * OpenSSL only lets one thread write at a time, so this is used
+         * to make sure we serialize callers of SSL_write. Thread is
+         * already expected to have completed handshaking.
+         */
+        private final Object writeLock = new Object();
+
+        SSLOutputStream() {
+        }
+
+        /**
+         * Method acts as described in spec for superclass.
+         * @see java.io.OutputStream#write(int)
+         */
+        @Override
+        public void write(int oneByte) throws IOException {
+            byte[] buffer = new byte[1];
+            buffer[0] = (byte) (oneByte & 0xff);
+            write(buffer);
+        }
+
+        /**
+         * Method acts as described in spec for superclass.
+         * @see java.io.OutputStream#write(byte[],int,int)
+         */
+        @Override
+        public void write(byte[] buf, int offset, int byteCount) throws IOException {
+            Platform.blockGuardOnNetwork();
+            checkOpen();
+            ArrayUtils.checkOffsetAndCount(buf.length, offset, byteCount);
+            if (byteCount == 0) {
+                return;
+            }
+
+            synchronized (writeLock) {
+                synchronized (stateLock) {
+                    if (state == STATE_CLOSED) {
+                        throw new SocketException("socket is closed");
+                    }
+
+                    if (DBG_STATE) {
+                        assertReadableOrWriteableState();
+                    }
+                }
+
+                ssl.write(Platform.getFileDescriptor(socket), buf, offset, byteCount,
+                        writeTimeoutMilliseconds);
+
+                synchronized (stateLock) {
+                    if (state == STATE_CLOSED) {
+                        throw new SocketException("socket is closed");
+                    }
+                }
+            }
+        }
+
+        void awaitPendingOps() {
+            if (DBG_STATE) {
+                synchronized (stateLock) {
+                    if (state != STATE_CLOSED) {
+                        throw new AssertionError("State is: " + state);
+                    }
+                }
+            }
+
+            synchronized (writeLock) {}
+        }
+    }
+
+    @Override
+    public SSLSession getSession() {
+        boolean handshakeCompleted = false;
+        synchronized (stateLock) {
+            try {
+                handshakeCompleted = state >= STATE_READY;
+                if (!handshakeCompleted && isConnected()) {
+                    waitForHandshake();
+                    handshakeCompleted = true;
+                }
+            } catch (IOException e) {
+                // Fall through.
+            }
+        }
+
+        if (!handshakeCompleted) {
+            // return an invalid session with
+            // invalid cipher suite of "SSL_NULL_WITH_NULL_NULL"
+            return SSLNullSession.getNullSession();
+        }
+
+        return Platform.wrapSSLSession(sslSession);
+    }
+
+    @Override
+    SSLSession getActiveSession() {
+        return sslSession;
+    }
+
+    @Override
+    public SSLSession getHandshakeSession() {
+        synchronized (stateLock) {
+            return state >= STATE_HANDSHAKE_STARTED && state < STATE_READY ? sslSession : null;
+        }
+    }
+
+    @Override
+    public boolean getEnableSessionCreation() {
+        return sslParameters.getEnableSessionCreation();
+    }
+
+    @Override
+    public void setEnableSessionCreation(boolean flag) {
+        sslParameters.setEnableSessionCreation(flag);
+    }
+
+    @Override
+    public String[] getSupportedCipherSuites() {
+        return NativeCrypto.getSupportedCipherSuites();
+    }
+
+    @Override
+    public String[] getEnabledCipherSuites() {
+        return sslParameters.getEnabledCipherSuites();
+    }
+
+    @Override
+    public void setEnabledCipherSuites(String[] suites) {
+        sslParameters.setEnabledCipherSuites(suites);
+    }
+
+    @Override
+    public String[] getSupportedProtocols() {
+        return NativeCrypto.getSupportedProtocols();
+    }
+
+    @Override
+    public String[] getEnabledProtocols() {
+        return sslParameters.getEnabledProtocols();
+    }
+
+    @Override
+    public void setEnabledProtocols(String[] protocols) {
+        sslParameters.setEnabledProtocols(protocols);
+    }
+
+    /**
+     * This method enables session ticket support.
+     *
+     * @param useSessionTickets True to enable session tickets
+     */
+    @Override
+    public void setUseSessionTickets(boolean useSessionTickets) {
+        sslParameters.setUseSessionTickets(useSessionTickets);
+    }
+
+    /**
+     * This method enables Server Name Indication
+     *
+     * @param hostname the desired SNI hostname, or null to disable
+     */
+    @Override
+    public void setHostname(String hostname) {
+        sslParameters.setUseSni(hostname != null);
+        super.setHostname(hostname);
+    }
+
+    /**
+     * Enables/disables TLS Channel ID for this server socket.
+     *
+     * <p>This method needs to be invoked before the handshake starts.
+     *
+     * @throws IllegalStateException if this is a client socket or if the handshake has already
+     *         started.
+     */
+    @Override
+    public void setChannelIdEnabled(boolean enabled) {
+        if (getUseClientMode()) {
+            throw new IllegalStateException("Client mode");
+        }
+
+        synchronized (stateLock) {
+            if (state != STATE_NEW) {
+                throw new IllegalStateException(
+                        "Could not enable/disable Channel ID after the initial handshake has"
+                                + " begun.");
+            }
+        }
+        sslParameters.channelIdEnabled = enabled;
+    }
+
+    /**
+     * Gets the TLS Channel ID for this server socket. Channel ID is only available once the
+     * handshake completes.
+     *
+     * @return channel ID or {@code null} if not available.
+     *
+     * @throws IllegalStateException if this is a client socket or if the handshake has not yet
+     *         completed.
+     * @throws SSLException if channel ID is available but could not be obtained.
+     */
+    @Override
+    public byte[] getChannelId() throws SSLException {
+        if (getUseClientMode()) {
+            throw new IllegalStateException("Client mode");
+        }
+
+        synchronized (stateLock) {
+            if (state != STATE_READY) {
+                throw new IllegalStateException(
+                        "Channel ID is only available after handshake completes");
+            }
+        }
+        return ssl.getTlsChannelId();
+    }
+
+    /**
+     * Sets the {@link PrivateKey} to be used for TLS Channel ID by this client socket.
+     *
+     * <p>This method needs to be invoked before the handshake starts.
+     *
+     * @param privateKey private key (enables TLS Channel ID) or {@code null} for no key (disables
+     *        TLS Channel ID). The private key must be an Elliptic Curve (EC) key based on the NIST
+     *        P-256 curve (aka SECG secp256r1 or ANSI X9.62 prime256v1).
+     *
+     * @throws IllegalStateException if this is a server socket or if the handshake has already
+     *         started.
+     */
+    @Override
+    public void setChannelIdPrivateKey(PrivateKey privateKey) {
+        if (!getUseClientMode()) {
+            throw new IllegalStateException("Server mode");
+        }
+
+        synchronized (stateLock) {
+            if (state != STATE_NEW) {
+                throw new IllegalStateException(
+                        "Could not change Channel ID private key after the initial handshake has"
+                                + " begun.");
+            }
+        }
+
+        if (privateKey == null) {
+            sslParameters.channelIdEnabled = false;
+            channelIdPrivateKey = null;
+        } else {
+            sslParameters.channelIdEnabled = true;
+            try {
+                ECParameterSpec ecParams = null;
+                if (privateKey instanceof ECKey) {
+                    ecParams = ((ECKey) privateKey).getParams();
+                }
+                if (ecParams == null) {
+                    // Assume this is a P-256 key, as specified in the contract of this method.
+                    ecParams =
+                            OpenSSLECGroupContext.getCurveByName("prime256v1").getECParameterSpec();
+                }
+                channelIdPrivateKey =
+                        OpenSSLKey.fromECPrivateKeyForTLSStackOnly(privateKey, ecParams);
+            } catch (InvalidKeyException e) {
+                // Will have error in startHandshake
+            }
+        }
+    }
+
+    @Override
+    public boolean getUseClientMode() {
+        return sslParameters.getUseClientMode();
+    }
+
+    @Override
+    public void setUseClientMode(boolean mode) {
+        synchronized (stateLock) {
+            if (state != STATE_NEW) {
+                throw new IllegalArgumentException(
+                        "Could not change the mode after the initial handshake has begun.");
+            }
+        }
+        sslParameters.setUseClientMode(mode);
+    }
+
+    @Override
+    public boolean getWantClientAuth() {
+        return sslParameters.getWantClientAuth();
+    }
+
+    @Override
+    public boolean getNeedClientAuth() {
+        return sslParameters.getNeedClientAuth();
+    }
+
+    @Override
+    public void setNeedClientAuth(boolean need) {
+        sslParameters.setNeedClientAuth(need);
+    }
+
+    @Override
+    public void setWantClientAuth(boolean want) {
+        sslParameters.setWantClientAuth(want);
+    }
+
+    /**
+     * Note write timeouts are not part of the javax.net.ssl.SSLSocket API
+     */
+    @Override
+    public void setSoWriteTimeout(int writeTimeoutMilliseconds) throws SocketException {
+        this.writeTimeoutMilliseconds = writeTimeoutMilliseconds;
+
+        Platform.setSocketWriteTimeout(this, writeTimeoutMilliseconds);
+    }
+
+    /**
+     * Note write timeouts are not part of the javax.net.ssl.SSLSocket API
+     */
+    @Override
+    public int getSoWriteTimeout() throws SocketException {
+        return writeTimeoutMilliseconds;
+    }
+
+    /**
+     * Set the handshake timeout on this socket.  This timeout is specified in
+     * milliseconds and will be used only during the handshake process.
+     */
+    @Override
+    public void setHandshakeTimeout(int handshakeTimeoutMilliseconds) throws SocketException {
+        this.handshakeTimeoutMilliseconds = handshakeTimeoutMilliseconds;
+    }
+
+    @Override
+    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
+    public void close() throws IOException {
+        // TODO: Close SSL sockets using a background thread so they close gracefully.
+
+        SSLInputStream sslInputStream;
+        SSLOutputStream sslOutputStream;
+
+        synchronized (stateLock) {
+            if (state == STATE_CLOSED) {
+                // close() has already been called, so do nothing and return.
+                return;
+            }
+
+            int oldState = state;
+            state = STATE_CLOSED;
+
+            if (oldState == STATE_NEW) {
+                // The handshake hasn't been started yet, so there's no OpenSSL related
+                // state to clean up. We still need to close the underlying socket if
+                // we're wrapping it and were asked to autoClose.
+                free();
+                closeUnderlyingSocket();
+
+                stateLock.notifyAll();
+                return;
+            }
+
+            if (oldState != STATE_READY && oldState != STATE_READY_HANDSHAKE_CUT_THROUGH) {
+                // If we're in these states, we still haven't returned from startHandshake.
+                // We call SSL_interrupt so that we can interrupt SSL_do_handshake and then
+                // set the state to STATE_CLOSED. startHandshake will handle all cleanup
+                // after SSL_do_handshake returns, so we don't have anything to do here.
+                ssl.interrupt();
+
+                stateLock.notifyAll();
+                return;
+            }
+
+            stateLock.notifyAll();
+            // We've already returned from startHandshake, so we potentially have
+            // input and output streams to clean up.
+            sslInputStream = is;
+            sslOutputStream = os;
+        }
+
+        // Don't bother interrupting unless we have something to interrupt.
+        if (sslInputStream != null || sslOutputStream != null) {
+            ssl.interrupt();
+        }
+
+        // Wait for the input and output streams to finish any reads they have in
+        // progress. If there are no reads in progress at this point, future reads will
+        // throw because state == STATE_CLOSED
+        if (sslInputStream != null) {
+            sslInputStream.awaitPendingOps();
+        }
+        if (sslOutputStream != null) {
+            sslOutputStream.awaitPendingOps();
+        }
+
+        shutdownAndFreeSslNative();
+    }
+
+    private void shutdownAndFreeSslNative() throws IOException {
+        try {
+            Platform.blockGuardOnNetwork();
+            ssl.shutdown(Platform.getFileDescriptor(socket));
+        } catch (IOException ignored) {
+            /*
+             * Note that although close() can throw
+             * IOException, the RI does not throw if there
+             * is problem sending a "close notify" which
+             * can happen if the underlying socket is closed.
+             */
+        } finally {
+            free();
+            closeUnderlyingSocket();
+        }
+    }
+
+    private void closeUnderlyingSocket() throws IOException {
+        super.close();
+    }
+
+    private void free() {
+        if (!ssl.isClosed()) {
+            ssl.close();
+            Platform.closeGuardClose(guard);
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            /*
+             * Just worry about our own state. Notably we do not try and
+             * close anything. The SocketImpl, either our own
+             * PlainSocketImpl, or the Socket we are wrapping, will do
+             * that. This might mean we do not properly SSL_shutdown, but
+             * if you want to do that, properly close the socket yourself.
+             *
+             * The reason why we don't try to SSL_shutdown, is that there
+             * can be a race between finalizers where the PlainSocketImpl
+             * finalizer runs first and closes the socket. However, in the
+             * meanwhile, the underlying file descriptor could be reused
+             * for another purpose. If we call SSL_shutdown, the
+             * underlying socket BIOs still have the old file descriptor
+             * and will write the close notify to some unsuspecting
+             * reader.
+             */
+            if (guard != null) {
+                Platform.closeGuardWarnIfOpen(guard);
+            }
+            free();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    /**
+     * Returns the protocol agreed upon by client and server, or {@code null} if
+     * no protocol was agreed upon.
+     */
+    @Override
+    public byte[] getAlpnSelectedProtocol() {
+        return ssl.getAlpnSelectedProtocol();
+    }
+
+    /**
+     * Sets the list of ALPN protocols. This method internally converts the protocols to their
+     * wire-format form.
+     *
+     * @param alpnProtocols the list of ALPN protocols
+     * @see #setAlpnProtocols(byte[])
+     */
+    @Override
+    public void setAlpnProtocols(String[] alpnProtocols) {
+        sslParameters.setAlpnProtocols(alpnProtocols);
+    }
+
+    /**
+     * Alternate version of {@link #setAlpnProtocols(String[])} that directly sets the list of
+     * ALPN in the wire-format form used by BoringSSL (length-prefixed 8-bit strings).
+     * Requires that all strings be encoded with US-ASCII.
+     *
+     * @param alpnProtocols the encoded form of the ALPN protocol list
+     * @see #setAlpnProtocols(String[])
+     */
+    @Override
+    public void setAlpnProtocols(byte[] alpnProtocols) {
+        sslParameters.setAlpnProtocols(alpnProtocols);
+    }
+
+    @Override
+    public SSLParameters getSSLParameters() {
+        SSLParameters params = super.getSSLParameters();
+        Platform.getSSLParameters(params, sslParameters, this);
+        return params;
+    }
+
+    @Override
+    public void setSSLParameters(SSLParameters p) {
+        super.setSSLParameters(p);
+        Platform.setSSLParameters(p, sslParameters, this);
+    }
+
+    @Override
+    public String chooseServerAlias(X509KeyManager keyManager, String keyType) {
+        return keyManager.chooseServerAlias(keyType, null, this);
+    }
+
+    @Override
+    public String chooseClientAlias(X509KeyManager keyManager, X500Principal[] issuers,
+            String[] keyTypes) {
+        return keyManager.chooseClientAlias(keyTypes, null, this);
+    }
+
+    @Override
+    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
+    public String chooseServerPSKIdentityHint(PSKKeyManager keyManager) {
+        return keyManager.chooseServerKeyIdentityHint(this);
+    }
+
+    @Override
+    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
+    public String chooseClientPSKIdentity(PSKKeyManager keyManager, String identityHint) {
+        return keyManager.chooseClientKeyIdentity(identityHint, this);
+    }
+
+    @Override
+    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
+    public SecretKey getPSKKey(PSKKeyManager keyManager, String identityHint, String identity) {
+        return keyManager.getKey(identityHint, identity, this);
+    }
+
+    private ClientSessionContext clientSessionContext() {
+        return sslParameters.getClientSessionContext();
+    }
+
+    private AbstractSessionContext sessionContext() {
+        return sslParameters.getSessionContext();
+    }
+}
diff --git a/common/src/main/java/org/conscrypt/OpenSSLServerSocketImpl.java b/common/src/main/java/org/conscrypt/ConscryptServerSocket.java
similarity index 82%
rename from common/src/main/java/org/conscrypt/OpenSSLServerSocketImpl.java
rename to common/src/main/java/org/conscrypt/ConscryptServerSocket.java
index a7e3654..80a758f 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLServerSocketImpl.java
+++ b/common/src/main/java/org/conscrypt/ConscryptServerSocket.java
@@ -19,32 +19,33 @@
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.Socket;
+import javax.net.ssl.SSLServerSocket;
 
 /**
  * BoringSSL-based implementation of server sockets.
  */
-final class OpenSSLServerSocketImpl extends javax.net.ssl.SSLServerSocket {
+final class ConscryptServerSocket extends SSLServerSocket {
     private final SSLParametersImpl sslParameters;
     private boolean channelIdEnabled;
     private boolean useEngineSocket;
 
-    OpenSSLServerSocketImpl(SSLParametersImpl sslParameters) throws IOException {
+    ConscryptServerSocket(SSLParametersImpl sslParameters) throws IOException {
         this.sslParameters = sslParameters;
     }
 
-    OpenSSLServerSocketImpl(int port, SSLParametersImpl sslParameters)
+    ConscryptServerSocket(int port, SSLParametersImpl sslParameters)
         throws IOException {
         super(port);
         this.sslParameters = sslParameters;
     }
 
-    OpenSSLServerSocketImpl(int port, int backlog, SSLParametersImpl sslParameters)
+    ConscryptServerSocket(int port, int backlog, SSLParametersImpl sslParameters)
         throws IOException {
         super(port, backlog);
         this.sslParameters = sslParameters;
     }
 
-    OpenSSLServerSocketImpl(int port,
+    ConscryptServerSocket(int port,
                                       int backlog,
                                       InetAddress iAddress,
                                       SSLParametersImpl sslParameters)
@@ -56,7 +57,7 @@
     /**
      * Configures the socket to be created for this instance.
      */
-    OpenSSLServerSocketImpl setUseEngineSocket(boolean useEngineSocket) {
+    ConscryptServerSocket setUseEngineSocket(boolean useEngineSocket) {
         this.useEngineSocket = useEngineSocket;
         return this;
     }
@@ -174,21 +175,15 @@
 
     @Override
     public Socket accept() throws IOException {
+        final AbstractConscryptSocket socket;
         if (useEngineSocket) {
-            Socket rawSocket = new Socket();
-            implAccept(rawSocket);
-
-            // Enable channel ID.
-            OpenSSLEngineSocketImpl socket =
-                    new OpenSSLEngineSocketImpl(rawSocket, null, -1, true, sslParameters);
-            socket.setChannelIdEnabled(channelIdEnabled);
-            socket.startHandshake();
-            return socket;
+            socket = new ConscryptEngineSocket(sslParameters);
         } else {
-            OpenSSLSocketImpl socket = new OpenSSLSocketImpl(sslParameters);
-            socket.setChannelIdEnabled(channelIdEnabled);
-            implAccept(socket);
-            return socket;
+            socket = new ConscryptFileDescriptorSocket(sslParameters);
         }
+
+        socket.setChannelIdEnabled(channelIdEnabled);
+        implAccept(socket);
+        return socket;
     }
 }
diff --git a/common/src/main/java/org/conscrypt/DESEDESecretKeyFactory.java b/common/src/main/java/org/conscrypt/DESEDESecretKeyFactory.java
index 487e013..0c79294 100644
--- a/common/src/main/java/org/conscrypt/DESEDESecretKeyFactory.java
+++ b/common/src/main/java/org/conscrypt/DESEDESecretKeyFactory.java
@@ -38,7 +38,16 @@
             throw new InvalidKeySpecException("Null KeySpec");
         }
         if (keySpec instanceof SecretKeySpec) {
-            return (SecretKey) keySpec;
+            SecretKeySpec key = (SecretKeySpec) keySpec;
+            try {
+                if (!DESedeKeySpec.isParityAdjusted(key.getEncoded(), 0)) {
+                    throw new InvalidKeySpecException(
+                            "SecretKeySpec is not a parity-adjusted DESEDE key");
+                }
+            } catch (InvalidKeyException e) {
+                throw new InvalidKeySpecException(e);
+            }
+            return key;
         } else if (keySpec instanceof DESedeKeySpec) {
             DESedeKeySpec desKeySpec = (DESedeKeySpec) keySpec;
             return new SecretKeySpec(desKeySpec.getKey(), "DESEDE");
@@ -49,12 +58,19 @@
     }
 
     @Override
-    protected KeySpec engineGetKeySpec(SecretKey secretKey, Class<?> aClass)
-            throws InvalidKeySpecException {
+    protected KeySpec engineGetKeySpec(SecretKey secretKey,
+            @SuppressWarnings("rawtypes") Class aClass) throws InvalidKeySpecException {
         if (secretKey == null) {
             throw new InvalidKeySpecException("Null SecretKey");
         }
         if (aClass == SecretKeySpec.class) {
+            try {
+                if (!DESedeKeySpec.isParityAdjusted(secretKey.getEncoded(), 0)) {
+                    throw new InvalidKeySpecException("SecretKey is not a parity-adjusted DESEDE key");
+                }
+            } catch (InvalidKeyException e) {
+                throw new InvalidKeySpecException(e);
+            }
             if (secretKey instanceof SecretKeySpec) {
                 return (KeySpec) secretKey;
             } else {
diff --git a/common/src/main/java/org/conscrypt/OpenSSLExtendedSessionImpl.java b/common/src/main/java/org/conscrypt/DelegatingExtendedSSLSession.java
similarity index 81%
rename from common/src/main/java/org/conscrypt/OpenSSLExtendedSessionImpl.java
rename to common/src/main/java/org/conscrypt/DelegatingExtendedSSLSession.java
index dead13d..e0114fd 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLExtendedSessionImpl.java
+++ b/common/src/main/java/org/conscrypt/DelegatingExtendedSSLSession.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 The Android Open Source Project
+ * Copyright 2017 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -30,44 +30,46 @@
  * Implementation of the ExtendedSSLSession class for OpenSSL. Uses a delegate to maintain backward
  * compatibility with previous versions of Android which don't have ExtendedSSLSession.
  */
-final class OpenSSLExtendedSessionImpl extends ExtendedSSLSession {
-    private final AbstractOpenSSLSession delegate;
+final class DelegatingExtendedSSLSession extends ExtendedSSLSession {
+    // TODO: use BoringSSL API to actually fetch the real data
+    private static final String[] LOCAL_SUPPORTED_SIGNATURE_ALGORITHMS = new String[] {
+            "SHA512withRSA",
+            "SHA512withECDSA",
+            "SHA384withRSA",
+            "SHA384withECDSA",
+            "SHA256withRSA",
+            "SHA256withECDSA",
+            "SHA224withRSA",
+            "SHA224withECDSA",
+            "SHA1withRSA",
+            "SHA1withECDSA",
+    };
+    // TODO: use BoringSSL API to actually fetch the real data
+    private static final String[] PEER_SUPPORTED_SIGNATURE_ALGORITHMS = new String[] {
+            "SHA1withRSA",
+            "SHA1withECDSA"
+    };
 
-    OpenSSLExtendedSessionImpl(AbstractOpenSSLSession delegate) {
+    private final ActiveSession delegate;
+
+    DelegatingExtendedSSLSession(ActiveSession delegate) {
         this.delegate = delegate;
     }
 
-    AbstractOpenSSLSession getDelegate() {
+    ActiveSession getDelegate() {
         return delegate;
     }
 
     /* @Override */
     @SuppressWarnings("MissingOverride") // For Android backward-compatibility.
     public String[] getLocalSupportedSignatureAlgorithms() {
-        // From src/ssl/t1_lib.c tls12_sigalgs
-        // TODO: use BoringSSL API to actually fetch the real data
-        return new String[] {
-                "SHA512withRSA",
-                "SHA512withECDSA",
-                "SHA384withRSA",
-                "SHA384withECDSA",
-                "SHA256withRSA",
-                "SHA256withECDSA",
-                "SHA224withRSA",
-                "SHA224withECDSA",
-                "SHA1withRSA",
-                "SHA1withECDSA",
-        };
+        return LOCAL_SUPPORTED_SIGNATURE_ALGORITHMS.clone();
     }
 
     /* @Override */
     @SuppressWarnings("MissingOverride") // For Android backward-compatibility.
     public String[] getPeerSupportedSignatureAlgorithms() {
-        // TODO: use BoringSSL API to actually fetch the real data
-        return new String[] {
-                "SHA1withRSA",
-                "SHA1withECDSA",
-        };
+        return PEER_SUPPORTED_SIGNATURE_ALGORITHMS.clone();
     }
 
     /* @Override */
diff --git a/common/src/main/java/org/conscrypt/GCMParameters.java b/common/src/main/java/org/conscrypt/GCMParameters.java
index d3c95bd..a47ab8c 100644
--- a/common/src/main/java/org/conscrypt/GCMParameters.java
+++ b/common/src/main/java/org/conscrypt/GCMParameters.java
@@ -16,17 +16,34 @@
 
 package org.conscrypt;
 
+import java.io.IOException;
+import java.security.AlgorithmParametersSpi;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.InvalidParameterSpecException;
+
 /**
  * GCM parameters used during an ciphering operation with {@link OpenSSLCipher}.
- * This class exists solely for backward compatibility with Android versions
- * that did not have the {@code GCMParameterSpec} class.
+ * This class is used internally for backward compatibility with Android versions
+ * that did not have the {@code GCMParameterSpec} class, in addition to being the
+ * implementation of the GCM AlgorithmParameters implementation.
+ * <p>
+ * The only supported encoding format is ASN.1, as specified in RFC 5084 section 3.2.
+ *
+ * @hide
  */
-final class GCMParameters {
+@Internal
+public final class GCMParameters extends AlgorithmParametersSpi {
+
+    // The default value (in bits) for TLEN in the GCM ASN.1 module
+    private static final int DEFAULT_TLEN = 96;
+
     /** The tag length in bits. */
-    private final int tLen;
+    private int tLen;
 
     /** Actually the nonce value for the GCM operation. */
-    private final byte[] iv;
+    private byte[] iv;
+
+    public GCMParameters() { }
 
     GCMParameters(int tLen, byte[] iv) {
         this.tLen = tLen;
@@ -46,4 +63,92 @@
     byte[] getIV() {
         return iv;
     }
+
+    @Override
+    protected void engineInit(AlgorithmParameterSpec algorithmParameterSpec)
+            throws InvalidParameterSpecException {
+        GCMParameters params = Platform.fromGCMParameterSpec(algorithmParameterSpec);
+        if (params == null) {
+            throw new InvalidParameterSpecException("Only GCMParameterSpec is supported");
+        }
+        this.tLen = params.tLen;
+        this.iv = params.iv;
+    }
+
+    @Override
+    protected void engineInit(byte[] bytes) throws IOException {
+        long readRef = 0;
+        long seqRef = 0;
+        try {
+            readRef = NativeCrypto.asn1_read_init(bytes);
+            seqRef = NativeCrypto.asn1_read_sequence(readRef);
+            byte[] newIv = NativeCrypto.asn1_read_octetstring(seqRef);
+            int newTlen = DEFAULT_TLEN;
+            if (!NativeCrypto.asn1_read_is_empty(seqRef)) {
+                newTlen = 8 * (int) NativeCrypto.asn1_read_uint64(seqRef);
+            }
+            if (!NativeCrypto.asn1_read_is_empty(seqRef)
+                    || !NativeCrypto.asn1_read_is_empty(readRef)) {
+                throw new IOException("Error reading ASN.1 encoding");
+            }
+            this.iv = newIv;
+            this.tLen = newTlen;
+        } finally {
+            NativeCrypto.asn1_read_free(seqRef);
+            NativeCrypto.asn1_read_free(readRef);
+        }
+    }
+
+    @Override
+    protected void engineInit(byte[] bytes, String format) throws IOException {
+        if ((format == null) || format.equals("ASN.1")) {
+            engineInit(bytes);
+        } else {
+            throw new IOException("Unsupported format: " + format);
+        }
+    }
+
+    @Override
+    protected <T extends AlgorithmParameterSpec> T engineGetParameterSpec(Class<T> aClass)
+            throws InvalidParameterSpecException {
+        if ((aClass != null) && aClass.getName().equals("javax.crypto.spec.GCMParameterSpec")) {
+            return aClass.cast(Platform.toGCMParameterSpec(tLen, iv));
+        } else {
+            throw new InvalidParameterSpecException("Unsupported class: " + aClass);
+        }
+    }
+
+    @Override
+    protected byte[] engineGetEncoded() throws IOException {
+        long cbbRef = 0;
+        long seqRef = 0;
+        try {
+            cbbRef = NativeCrypto.asn1_write_init();
+            seqRef = NativeCrypto.asn1_write_sequence(cbbRef);
+            NativeCrypto.asn1_write_octetstring(seqRef, this.iv);
+            if (this.tLen != DEFAULT_TLEN) {
+                NativeCrypto.asn1_write_uint64(seqRef, this.tLen / 8);
+            }
+            return NativeCrypto.asn1_write_finish(cbbRef);
+        } catch (IOException e) {
+            NativeCrypto.asn1_write_cleanup(cbbRef);
+            throw e;
+        } finally {
+            NativeCrypto.asn1_write_free(seqRef);
+            NativeCrypto.asn1_write_free(cbbRef);
+        }
+    }
+
+    @Override
+    protected byte[] engineGetEncoded(String format) throws IOException {
+        if ((format == null) || format.equals("ASN.1")) {
+            return engineGetEncoded();
+        }
+        throw new IOException("Unsupported format: " + format);
+    }
+
+    @Override
+    protected String engineToString() {
+        return "Conscrypt GCM AlgorithmParameters";
+    }
 }
diff --git a/common/src/main/java/org/conscrypt/KeyGeneratorImpl.java b/common/src/main/java/org/conscrypt/KeyGeneratorImpl.java
index 1223b13..c1b1261 100644
--- a/common/src/main/java/org/conscrypt/KeyGeneratorImpl.java
+++ b/common/src/main/java/org/conscrypt/KeyGeneratorImpl.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package org.conscrypt;
 
 import java.security.InvalidAlgorithmParameterException;
@@ -126,7 +141,7 @@
             // Set the parity bit for each byte
             for (int i = 0; i < keyData.length; i++) {
                 if (Integer.bitCount(keyData[i]) % 2 == 0) {
-                    keyData[i] ^= 1;
+                    keyData[i] = (byte) (keyData[i] ^ 1);
                 }
             }
             if (keyBytes == 14) {
diff --git a/common/src/main/java/org/conscrypt/NativeCrypto.java b/common/src/main/java/org/conscrypt/NativeCrypto.java
index b68c594..5b25748 100644
--- a/common/src/main/java/org/conscrypt/NativeCrypto.java
+++ b/common/src/main/java/org/conscrypt/NativeCrypto.java
@@ -32,11 +32,8 @@
 import java.security.cert.CertificateParsingException;
 import java.util.ArrayList;
 import java.util.Calendar;
-import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import javax.crypto.BadPaddingException;
 import javax.crypto.IllegalBlockSizeException;
@@ -59,13 +56,17 @@
 
     private native static void clinit();
 
+    /**
+     * Does nothing. Just for forcing static initialization.
+     */
+    static void checkAvailability() {
+    }
+
     // --- DSA/RSA public/private key handling functions -----------------------
 
     static native long EVP_PKEY_new_RSA(byte[] n, byte[] e, byte[] d, byte[] p, byte[] q,
             byte[] dmp1, byte[] dmq1, byte[] iqmp);
 
-    static native int EVP_PKEY_size(NativeRef.EVP_PKEY pkey);
-
     static native int EVP_PKEY_type(NativeRef.EVP_PKEY pkey);
 
     static native String EVP_PKEY_print_public(NativeRef.EVP_PKEY pkeyRef);
@@ -76,13 +77,13 @@
 
     static native int EVP_PKEY_cmp(NativeRef.EVP_PKEY pkey1, NativeRef.EVP_PKEY pkey2);
 
-    static native byte[] i2d_PKCS8_PRIV_KEY_INFO(NativeRef.EVP_PKEY pkey);
+    static native byte[] EVP_marshal_private_key(NativeRef.EVP_PKEY pkey);
 
-    static native long d2i_PKCS8_PRIV_KEY_INFO(byte[] data);
+    static native long EVP_parse_private_key(byte[] data);
 
-    static native byte[] i2d_PUBKEY(NativeRef.EVP_PKEY pkey);
+    static native byte[] EVP_marshal_public_key(NativeRef.EVP_PKEY pkey);
 
-    static native long d2i_PUBKEY(byte[] data);
+    static native long EVP_parse_public_key(byte[] data);
 
     static native long PEM_read_bio_PUBKEY(long bioCtx);
 
@@ -118,10 +119,6 @@
      */
     static native byte[][] get_RSA_private_params(NativeRef.EVP_PKEY rsa);
 
-    static native byte[] i2d_RSAPublicKey(NativeRef.EVP_PKEY rsa);
-
-    static native byte[] i2d_RSAPrivateKey(NativeRef.EVP_PKEY rsa);
-
     // --- EC functions --------------------------
 
     static native long EVP_PKEY_new_EC_KEY(
@@ -180,8 +177,6 @@
 
     static native int EVP_MD_size(long evp_md_const);
 
-    static native int EVP_MD_block_size(long evp_md_const);
-
     // --- Message digest context functions --------------
 
     static native long EVP_MD_CTX_create();
@@ -294,8 +289,6 @@
 
     static native int EVP_AEAD_nonce_length(long evpAead);
 
-    static native int EVP_AEAD_max_tag_len(long evpAead);
-
     static native int EVP_AEAD_CTX_seal(long evpAead, byte[] key, int tagLengthInBytes, byte[] out,
             int outOffset, byte[] nonce, byte[] in, int inOffset, int inLength, byte[] ad)
             throws BadPaddingException;
@@ -322,14 +315,6 @@
 
     static native void RAND_bytes(byte[] output);
 
-    // --- ASN.1 objects -------------------------------------------------------
-
-    static native int OBJ_txt2nid(String oid);
-
-    static native String OBJ_txt2nid_longName(String oid);
-
-    static native String OBJ_txt2nid_oid(String oid);
-
     // --- X509_NAME -----------------------------------------------------------
 
     static int X509_NAME_hash(X500Principal principal) {
@@ -350,8 +335,6 @@
         }
     }
 
-    static native String X509_NAME_print_ex(long x509nameCtx, long flags);
-
     // --- X509 ----------------------------------------------------------------
 
     /** Used to request get_X509_GENERAL_NAME_stack get the "altname" field. */
@@ -536,17 +519,97 @@
 
     static native void ASN1_TIME_to_Calendar(long asn1TimeCtx, Calendar cal);
 
+    // --- ASN1 Encoding -------------------------------------------------------
+
+    /**
+     * Allocates and returns an opaque reference to an object that can be used with other
+     * asn1_read_* functions to read the ASN.1-encoded data in val.  The returned object must
+     * be freed after use by calling asn1_read_free.
+     */
+    static native long asn1_read_init(byte[] val);
+
+    /**
+     * Allocates and returns an opaque reference to an object that can be used with other
+     * asn1_read_* functions to read the ASN.1 sequence pointed to by cbsRef.  The returned
+     * object must be freed after use by calling asn1_read_free.
+     */
+    static native long asn1_read_sequence(long cbsRef) throws IOException;
+
+    /**
+     * Returns the contents of an ASN.1 octet string from the given reference.
+     */
+    static native byte[] asn1_read_octetstring(long cbsRef) throws IOException;
+
+    /**
+     * Returns an ASN.1 integer from the given reference.  If the integer doesn't fit
+     * in a uint64, this method will throw an IOException.
+     */
+    static native long asn1_read_uint64(long cbsRef) throws IOException;
+
+    /**
+     * Returns whether or not the given reference has been read completely.
+     */
+    static native boolean asn1_read_is_empty(long cbsRef);
+
+    /**
+     * Frees any resources associated with the given reference.  After calling, the reference
+     * must not be used again.  This may be called with a zero reference, in which case nothing
+     * will be done.
+     */
+    static native void asn1_read_free(long cbsRef);
+
+    /**
+     * Allocates and returns an opaque reference to an object that can be used with other
+     * asn1_write_* functions to write ASN.1-encoded data.  The returned object must be finalized
+     * after use by calling either asn1_write_finish or asn1_write_cleanup, and its resources
+     * must be freed by calling asn1_write_free.
+     */
+    static native long asn1_write_init() throws IOException;
+
+    /**
+     * Allocates and returns an opaque reference to an object that can be used with other
+     * asn1_write_* functions to write an ASN.1 sequence into the given reference.  The returned
+     * reference may only be used until the next call on the parent reference.  The returned
+     * object must be freed after use by calling asn1_write_free.
+     */
+    static native long asn1_write_sequence(long cbbRef) throws IOException;
+
+    /**
+     * Writes the given data into the given reference as an ASN.1-encoded octet string.
+     */
+    static native void asn1_write_octetstring(long cbbRef, byte[] data) throws IOException;
+
+    /**
+     * Writes the given value into the given reference as an ASN.1-encoded integer.
+     */
+    static native void asn1_write_uint64(long cbbRef, long value) throws IOException;
+
+    /**
+     * Completes any in-progress operations and returns the ASN.1-encoded data.  Either this
+     * or asn1_write_cleanup must be called on any reference returned from asn1_write_init
+     * before it is freed.
+     */
+    static native byte[] asn1_write_finish(long cbbRef) throws IOException;
+
+    /**
+     * Cleans up intermediate state in the given reference.  Either this or asn1_write_finish
+     * must be called on any reference returned from asn1_write_init before it is freed.
+     */
+    static native void asn1_write_cleanup(long cbbRef);
+
+    /**
+     * Frees resources associated with the given reference.  After calling, the reference
+     * must not be used again.  This may be called with a zero reference, in which case nothing
+     * will be done.
+     */
+    static native void asn1_write_free(long cbbRef);
+
     // --- BIO stream creation -------------------------------------------------
 
     static native long create_BIO_InputStream(OpenSSLBIOInputStream is, boolean isFinite);
 
     static native long create_BIO_OutputStream(OutputStream os);
 
-    static native int BIO_read(long bioRef, byte[] buffer);
-
-    static native void BIO_write(long bioRef, byte[] buffer, int offset, int length)
-            throws IOException;
-
     static native void BIO_free_all(long bioRef);
 
     // --- SSL handling --------------------------------------------------------
@@ -556,26 +619,12 @@
     private static final String SUPPORTED_PROTOCOL_TLSV1_1 = "TLSv1.1";
     private static final String SUPPORTED_PROTOCOL_TLSV1_2 = "TLSv1.2";
 
-    // STANDARD_TO_OPENSSL_CIPHER_SUITES is a map from OpenSSL-style
-    // cipher-suite names to the standard name for the same (i.e. the name that
-    // is registered with IANA).
-    static final Map<String, String> OPENSSL_TO_STANDARD_CIPHER_SUITES =
-            new HashMap<String, String>();
-
-    // STANDARD_TO_OPENSSL_CIPHER_SUITES is a map from "standard" cipher suite
-    // names (i.e. the names that are registered with IANA) to the
-    // OpenSSL-style name for the same.
-    static final Map<String, String> STANDARD_TO_OPENSSL_CIPHER_SUITES =
-            new LinkedHashMap<String, String>();
-
-    // SUPPORTED_CIPHER_SUITES_SET contains all the cipher suites supported by
-    // OpenSSL, named using "standard" (as opposed to OpenSSL-style) names.
+    // SUPPORTED_CIPHER_SUITES_SET contains all the supported cipher suites, using their Java names.
     static final Set<String> SUPPORTED_CIPHER_SUITES_SET = new HashSet<String>();
 
-    private static void add(String openssl, String standard) {
-        OPENSSL_TO_STANDARD_CIPHER_SUITES.put(openssl, standard);
-        STANDARD_TO_OPENSSL_CIPHER_SUITES.put(standard, openssl);
-    }
+    // SUPPORTED_LEGACY_CIPHER_SUITES_SET contains all the supported cipher suites using the legacy
+    // OpenSSL-style names.
+    static final Set<String> SUPPORTED_LEGACY_CIPHER_SUITES_SET = new HashSet<String>();
 
     /**
      * TLS_EMPTY_RENEGOTIATION_INFO_SCSV is RFC 5746's renegotiation
@@ -598,6 +647,21 @@
      */
     static final String TLS_EMPTY_RENEGOTIATION_INFO_SCSV = "TLS_EMPTY_RENEGOTIATION_INFO_SCSV";
 
+    static String cipherSuiteToJava(String cipherSuite) {
+        // For historical reasons, Java uses a different name for TLS_RSA_WITH_3DES_EDE_CBC_SHA.
+        if ("TLS_RSA_WITH_3DES_EDE_CBC_SHA".equals(cipherSuite)) {
+            return "SSL_RSA_WITH_3DES_EDE_CBC_SHA";
+        }
+        return cipherSuite;
+    }
+
+    static String cipherSuiteFromJava(String javaCipherSuite) {
+        if ("SSL_RSA_WITH_3DES_EDE_CBC_SHA".equals(javaCipherSuite)) {
+            return "TLS_RSA_WITH_3DES_EDE_CBC_SHA";
+        }
+        return javaCipherSuite;
+    }
+
     /**
      * TLS_FALLBACK_SCSV is from
      * https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
@@ -606,101 +670,26 @@
      */
     static final String TLS_FALLBACK_SCSV = "TLS_FALLBACK_SCSV";
 
-    static {
-        add("ADH-AES128-GCM-SHA256", "TLS_DH_anon_WITH_AES_128_GCM_SHA256");
-        add("ADH-AES128-SHA256", "TLS_DH_anon_WITH_AES_128_CBC_SHA256");
-        add("ADH-AES128-SHA", "TLS_DH_anon_WITH_AES_128_CBC_SHA");
-        add("ADH-AES256-GCM-SHA384", "TLS_DH_anon_WITH_AES_256_GCM_SHA384");
-        add("ADH-AES256-SHA256", "TLS_DH_anon_WITH_AES_256_CBC_SHA256");
-        add("ADH-AES256-SHA", "TLS_DH_anon_WITH_AES_256_CBC_SHA");
-        add("ADH-DES-CBC3-SHA", "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA");
-        add("ADH-DES-CBC-SHA", "SSL_DH_anon_WITH_DES_CBC_SHA");
-        add("AECDH-AES128-SHA", "TLS_ECDH_anon_WITH_AES_128_CBC_SHA");
-        add("AECDH-AES256-SHA", "TLS_ECDH_anon_WITH_AES_256_CBC_SHA");
-        add("AECDH-DES-CBC3-SHA", "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA");
-        add("AECDH-NULL-SHA", "TLS_ECDH_anon_WITH_NULL_SHA");
-        add("AES128-GCM-SHA256", "TLS_RSA_WITH_AES_128_GCM_SHA256");
-        add("AES128-SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA256");
-        add("AES128-SHA", "TLS_RSA_WITH_AES_128_CBC_SHA");
-        add("AES256-GCM-SHA384", "TLS_RSA_WITH_AES_256_GCM_SHA384");
-        add("AES256-SHA256", "TLS_RSA_WITH_AES_256_CBC_SHA256");
-        add("AES256-SHA", "TLS_RSA_WITH_AES_256_CBC_SHA");
-        add("DES-CBC3-SHA", "SSL_RSA_WITH_3DES_EDE_CBC_SHA");
-        add("DES-CBC-SHA", "SSL_RSA_WITH_DES_CBC_SHA");
-        add("ECDH-ECDSA-AES128-GCM-SHA256", "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256");
-        add("ECDH-ECDSA-AES128-SHA256", "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256");
-        add("ECDH-ECDSA-AES128-SHA", "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA");
-        add("ECDH-ECDSA-AES256-GCM-SHA384", "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384");
-        add("ECDH-ECDSA-AES256-SHA384", "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384");
-        add("ECDH-ECDSA-AES256-SHA", "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA");
-        add("ECDH-ECDSA-DES-CBC3-SHA", "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA");
-        add("ECDH-ECDSA-NULL-SHA", "TLS_ECDH_ECDSA_WITH_NULL_SHA");
-        add("ECDHE-ECDSA-AES128-GCM-SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
-        add("ECDHE-ECDSA-AES128-SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256");
-        add("ECDHE-ECDSA-AES128-SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA");
-        add("ECDHE-ECDSA-AES256-GCM-SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384");
-        add("ECDHE-ECDSA-AES256-SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384");
-        add("ECDHE-ECDSA-AES256-SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA");
-        add("ECDHE-ECDSA-CHACHA20-POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305");
-        add("ECDHE-ECDSA-CHACHA20-POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256");
-        add("ECDHE-ECDSA-DES-CBC3-SHA", "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA");
-        add("ECDHE-ECDSA-NULL-SHA", "TLS_ECDHE_ECDSA_WITH_NULL_SHA");
-        add("ECDHE-PSK-AES128-CBC-SHA", "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA");
-        add("ECDHE-PSK-AES128-GCM-SHA256", "TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256");
-        add("ECDHE-PSK-AES256-CBC-SHA", "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA");
-        add("ECDHE-PSK-AES256-GCM-SHA384", "TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384");
-        add("ECDHE-PSK-CHACHA20-POLY1305", "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256");
-        add("ECDHE-RSA-AES128-GCM-SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
-        add("ECDHE-RSA-AES128-SHA256", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256");
-        add("ECDHE-RSA-AES128-SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA");
-        add("ECDHE-RSA-AES256-GCM-SHA384", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384");
-        add("ECDHE-RSA-AES256-SHA384", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384");
-        add("ECDHE-RSA-AES256-SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA");
-        add("ECDHE-RSA-CHACHA20-POLY1305", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305");
-        add("ECDHE-RSA-CHACHA20-POLY1305", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256");
-        add("ECDHE-RSA-DES-CBC3-SHA", "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA");
-        add("ECDHE-RSA-NULL-SHA", "TLS_ECDHE_RSA_WITH_NULL_SHA");
-        add("ECDH-RSA-AES128-GCM-SHA256", "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256");
-        add("ECDH-RSA-AES128-SHA256", "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256");
-        add("ECDH-RSA-AES128-SHA", "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA");
-        add("ECDH-RSA-AES256-GCM-SHA384", "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384");
-        add("ECDH-RSA-AES256-SHA384", "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384");
-        add("ECDH-RSA-AES256-SHA", "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA");
-        add("ECDH-RSA-DES-CBC3-SHA", "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA");
-        add("ECDH-RSA-NULL-SHA", "TLS_ECDH_RSA_WITH_NULL_SHA");
-        add("EXP-ADH-DES-CBC-SHA", "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA");
-        add("EXP-DES-CBC-SHA", "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA");
-        add("NULL-MD5", "SSL_RSA_WITH_NULL_MD5");
-        add("NULL-SHA256", "TLS_RSA_WITH_NULL_SHA256");
-        add("NULL-SHA", "SSL_RSA_WITH_NULL_SHA");
-        add("PSK-3DES-EDE-CBC-SHA", "TLS_PSK_WITH_3DES_EDE_CBC_SHA");
-        add("PSK-AES128-CBC-SHA", "TLS_PSK_WITH_AES_128_CBC_SHA");
-        add("PSK-AES256-CBC-SHA", "TLS_PSK_WITH_AES_256_CBC_SHA");
-
-        // Signaling Cipher Suite Value for secure renegotiation handled as special case.
-        // add("TLS_EMPTY_RENEGOTIATION_INFO_SCSV", null);
-
-        // Similarly, the fallback SCSV is handled as a special case.
-        // add("TLS_FALLBACK_SCSV", null);
-    }
-
     private static final String[] SUPPORTED_CIPHER_SUITES;
     static {
-        String[] allOpenSSLCipherSuites = get_cipher_names("ALL:!DHE");
+        String[] allCipherSuites = get_cipher_names("ALL:!DHE");
 
-        int size = allOpenSSLCipherSuites.length;
-        SUPPORTED_CIPHER_SUITES = new String[size + 2];
-        for (int i = 0; i < size; i++) {
-            String standardName = OPENSSL_TO_STANDARD_CIPHER_SUITES.get(allOpenSSLCipherSuites[i]);
-            if (standardName == null) {
-                throw new IllegalArgumentException("Unknown cipher suite supported by native code: "
-                        + allOpenSSLCipherSuites[i]);
-            }
-            SUPPORTED_CIPHER_SUITES[i] = standardName;
-            SUPPORTED_CIPHER_SUITES_SET.add(standardName);
+        // get_cipher_names returns an array where even indices are the standard name and odd
+        // indices are the OpenSSL name.
+        int size = allCipherSuites.length;
+        if (size % 2 != 0) {
+            throw new IllegalArgumentException("Invalid cipher list returned by get_cipher_names");
         }
-        SUPPORTED_CIPHER_SUITES[size] = TLS_EMPTY_RENEGOTIATION_INFO_SCSV;
-        SUPPORTED_CIPHER_SUITES[size + 1] = TLS_FALLBACK_SCSV;
+        SUPPORTED_CIPHER_SUITES = new String[size / 2 + 2];
+        for (int i = 0; i < size; i += 2) {
+            String cipherSuite = cipherSuiteToJava(allCipherSuites[i]);
+            SUPPORTED_CIPHER_SUITES[i / 2] = cipherSuite;
+            SUPPORTED_CIPHER_SUITES_SET.add(cipherSuite);
+
+            SUPPORTED_LEGACY_CIPHER_SUITES_SET.add(allCipherSuites[i + 1]);
+        }
+        SUPPORTED_CIPHER_SUITES[size / 2] = TLS_EMPTY_RENEGOTIATION_INFO_SCSV;
+        SUPPORTED_CIPHER_SUITES[size / 2 + 1] = TLS_FALLBACK_SCSV;
     }
 
     /**
@@ -782,6 +771,8 @@
 
     static native void SSL_CTX_set_session_id_context(long ssl_ctx, byte[] sid_ctx);
 
+    static native long SSL_CTX_set_timeout(long ssl_ctx, long seconds);
+
     static native long SSL_new(long ssl_ctx) throws SSLException;
 
     static native void SSL_enable_tls_channel_id(long ssl) throws SSLException;
@@ -798,14 +789,8 @@
 
     static native void SSL_set_client_CA_list(long ssl, byte[][] asn1DerEncodedX500Principals);
 
-    static native long SSL_get_mode(long ssl);
-
     static native long SSL_set_mode(long ssl, long mode);
 
-    static native long SSL_clear_mode(long ssl, long mode);
-
-    static native long SSL_get_options(long ssl);
-
     static native long SSL_set_options(long ssl, long options);
 
     static native long SSL_clear_options(long ssl, long options);
@@ -830,25 +815,29 @@
 
     /** Protocols to enable by default when "TLSv1.2" is requested. */
     static final String[] TLSV12_PROTOCOLS = new String[] {
-            SUPPORTED_PROTOCOL_TLSV1, SUPPORTED_PROTOCOL_TLSV1_1, SUPPORTED_PROTOCOL_TLSV1_2,
+            SUPPORTED_PROTOCOL_TLSV1,
+            SUPPORTED_PROTOCOL_TLSV1_1,
+            SUPPORTED_PROTOCOL_TLSV1_2,
     };
 
     /** Protocols to enable by default when "TLSv1.1" is requested. */
     static final String[] TLSV11_PROTOCOLS = new String[] {
-            SUPPORTED_PROTOCOL_TLSV1, SUPPORTED_PROTOCOL_TLSV1_1, SUPPORTED_PROTOCOL_TLSV1_2,
+            SUPPORTED_PROTOCOL_TLSV1,
+            SUPPORTED_PROTOCOL_TLSV1_1,
+            SUPPORTED_PROTOCOL_TLSV1_2,
     };
 
     /** Protocols to enable by default when "TLSv1" is requested. */
     static final String[] TLSV1_PROTOCOLS = new String[] {
-            SUPPORTED_PROTOCOL_TLSV1, SUPPORTED_PROTOCOL_TLSV1_1, SUPPORTED_PROTOCOL_TLSV1_2,
+            SUPPORTED_PROTOCOL_TLSV1,
+            SUPPORTED_PROTOCOL_TLSV1_1,
+            SUPPORTED_PROTOCOL_TLSV1_2,
     };
 
     static final String[] DEFAULT_PROTOCOLS = TLSV12_PROTOCOLS;
 
     static String[] getSupportedProtocols() {
-        return new String[] {
-                SUPPORTED_PROTOCOL_TLSV1, SUPPORTED_PROTOCOL_TLSV1_1, SUPPORTED_PROTOCOL_TLSV1_2,
-        };
+        return TLSV12_PROTOCOLS.clone();
     }
 
     static void setEnabledProtocols(long ssl, String[] protocols) {
@@ -922,9 +911,7 @@
                 SSL_set_mode(ssl, NativeConstants.SSL_MODE_SEND_FALLBACK_SCSV);
                 continue;
             }
-            String openssl = STANDARD_TO_OPENSSL_CIPHER_SUITES.get(cipherSuite);
-            String cs = (openssl == null) ? cipherSuite : openssl;
-            opensslSuites.add(cs);
+            opensslSuites.add(cipherSuiteFromJava(cipherSuite));
         }
         SSL_set_cipher_lists(ssl, opensslSuites.toArray(new String[opensslSuites.size()]));
     }
@@ -935,26 +922,24 @@
         }
         // makes sure all suites are valid, throwing on error
         for (int i = 0; i < cipherSuites.length; i++) {
-            String cipherSuite = cipherSuites[i];
-            if (cipherSuite == null) {
+            if (cipherSuites[i] == null) {
                 throw new IllegalArgumentException("cipherSuites[" + i + "] == null");
             }
-            if (cipherSuite.equals(TLS_EMPTY_RENEGOTIATION_INFO_SCSV)
-                    || cipherSuite.equals(TLS_FALLBACK_SCSV)) {
+            if (cipherSuites[i].equals(TLS_EMPTY_RENEGOTIATION_INFO_SCSV)
+                    || cipherSuites[i].equals(TLS_FALLBACK_SCSV)) {
                 continue;
             }
-            if (SUPPORTED_CIPHER_SUITES_SET.contains(cipherSuite)) {
+            if (SUPPORTED_CIPHER_SUITES_SET.contains(cipherSuites[i])) {
                 continue;
             }
 
             // For backwards compatibility, it's allowed for |cipherSuite| to
             // be an OpenSSL-style cipher-suite name.
-            String standardName = OPENSSL_TO_STANDARD_CIPHER_SUITES.get(cipherSuite);
-            if (standardName != null && SUPPORTED_CIPHER_SUITES_SET.contains(standardName)) {
+            if (SUPPORTED_LEGACY_CIPHER_SUITES_SET.contains(cipherSuites[i])) {
                 // TODO log warning about using backward compatability
                 continue;
             }
-            throw new IllegalArgumentException("cipherSuite " + cipherSuite + " is not supported.");
+            throw new IllegalArgumentException("cipherSuite " + cipherSuites[i] + " is not supported.");
         }
         return cipherSuites;
     }
@@ -996,11 +981,9 @@
             long sslNativePointer, FileDescriptor fd, SSLHandshakeCallbacks shc, int timeoutMillis)
             throws SSLException, SocketTimeoutException, CertificateException;
 
-    /**
-     * Currently only intended for forcing renegotiation for testing.
-     * Not used within OpenSSLSocketImpl.
-     */
-    static native void SSL_renegotiate(long sslNativePointer) throws SSLException;
+    public static native String SSL_get_current_cipher(long sslNativePointer);
+
+    public static native String SSL_get_version(long sslNativePointer);
 
     /**
      * Returns the local X509 certificate references. Must X509_free when done.
@@ -1041,11 +1024,21 @@
 
     static native long SSL_SESSION_get_time(long sslSessionNativePointer);
 
+    static native long SSL_get_time(long sslNativePointer);
+
+    static native long SSL_set_timeout(long sslNativePointer, long millis);
+
+    static native long SSL_get_timeout(long sslNativePointer);
+
+    static native long SSL_SESSION_get_timeout(long sslSessionNativePointer);
+
+    static native byte[] SSL_session_id(long sslNativePointer);
+
     static native String SSL_SESSION_get_version(long sslSessionNativePointer);
 
     static native String SSL_SESSION_cipher(long sslSessionNativePointer);
 
-    static native String get_SSL_SESSION_tlsext_hostname(long sslSessionNativePointer);
+    static native void SSL_SESSION_up_ref(long sslSessionNativePointer);
 
     static native void SSL_SESSION_free(long sslSessionNativePointer);
 
@@ -1116,9 +1109,26 @@
          * Called when SSL state changes. This could be handshake completion.
          */
         void onSSLStateChange(int type, int val);
-    }
 
-    static native long ERR_peek_last_error();
+        /**
+         * Called when a new session has been established and may be added to the session cache.
+         * The callee is responsible for incrementing the reference count on the returned session.
+         */
+        void onNewSessionEstablished(long sslSessionNativePtr);
+
+        /**
+         * Called for servers where TLS < 1.3 (TLS 1.3 uses session tickets rather than
+         * application session caches).
+         *
+         * <p/>Looks up the session by ID in the application's session cache. If a valid session
+         * is returned, this callback is responsible for incrementing the reference count (and any
+         * required synchronization).
+         *
+         * @param id the ID of the session to find.
+         * @return the cached session or {@code 0} if no session was found matching the given ID.
+         */
+        long serverSessionRequested(byte[] id);
+    }
 
     static native String SSL_CIPHER_get_kx_name(long cipherAddress);
 
@@ -1150,10 +1160,6 @@
 
     static native int SSL_pending_written_bytes_in_BIO(long bio);
 
-    static native long SSL_get0_session(long ssl);
-
-    static native long SSL_get1_session(long ssl);
-
     /**
      * Returns the maximum overhead, in bytes, of sealing a record with SSL.
      */
@@ -1166,35 +1172,62 @@
             long sslNativePointer, boolean clientMode, byte[] alpnProtocols) throws IOException;
 
     /**
-     * Variant of the {@link #SSL_do_handshake} used by {@link OpenSSLEngineImpl}. This version
-     * does not lock and does no error preprocessing.
+     * Variant of the {@link #SSL_do_handshake} used by {@link ConscryptEngine}. This differs
+     * slightly from the raw BoringSSL API in that it returns the SSL error code from the
+     * operation, rather than the return value from {@code SSL_do_handshake}. This is done in
+     * order to allow to properly handle SSL errors and propagate useful exceptions.
+     *
+     * @return Returns the SSL error code for the operation when the error was {@code
+     * SSL_ERROR_NONE}, {@code SSL_ERROR_WANT_READ}, or {@code SSL_ERROR_WANT_WRITE}.
+     * @throws IOException when the error code is anything except those returned by this method.
      */
-    static native int ENGINE_SSL_do_handshake(long ssl, SSLHandshakeCallbacks shc);
+    static native int ENGINE_SSL_do_handshake(long ssl, SSLHandshakeCallbacks shc)
+            throws IOException;
 
     /**
      * Variant of the {@link #SSL_read} for a direct {@link java.nio.ByteBuffer} used by {@link
-     * OpenSSLEngineImpl}. This version does not lock or and does no error pre-processing.
+     * ConscryptEngine}.
+     *
+     * @return if positive, represents the number of bytes read into the given buffer.
+     * Returns {@code -SSL_ERROR_WANT_READ} if more data is needed. Returns
+     * {@code -SSL_ERROR_WANT_WRITE} if data needs to be written out to flush the BIO.
+     *
+     * @throws java.io.InterruptedIOException if the read was interrupted.
+     * @throws java.io.EOFException if the end of stream has been reached.
+     * @throws CertificateException if the application's certificate verification callback failed.
+     * Only occurs during handshake processing.
+     * @throws SSLException if any other error occurs.
      */
     static native int ENGINE_SSL_read_direct(long sslNativePointer, long address, int length,
-            SSLHandshakeCallbacks shc) throws IOException;
+            SSLHandshakeCallbacks shc) throws IOException, CertificateException;
 
     /**
      * Variant of the {@link #SSL_read} for a heap {@link java.nio.ByteBuffer} used by {@link
-     * OpenSSLEngineImpl}. This version does not lock or and does no error pre-processing.
+     * ConscryptEngine}.
+     *
+     * @return if positive, represents the number of bytes read into the given buffer.
+     * Returns {@code -SSL_ERROR_WANT_READ} if more data is needed. Returns
+     * {@code -SSL_ERROR_WANT_WRITE} if data needs to be written out to flush the BIO.
+     *
+     * @throws java.io.InterruptedIOException if the read was interrupted.
+     * @throws java.io.EOFException if the end of stream has been reached.
+     * @throws CertificateException if the application's certificate verification callback failed.
+     * Only occurs during handshake processing.
+     * @throws SSLException if any other error occurs.
      */
     static native int ENGINE_SSL_read_heap(long sslNativePointer, byte[] destJava, int destOffset,
-            int destLength, SSLHandshakeCallbacks shc) throws IOException;
+            int destLength, SSLHandshakeCallbacks shc) throws IOException, CertificateException;
 
     /**
      * Variant of the {@link #SSL_write} for a direct {@link java.nio.ByteBuffer} used by {@link
-     * OpenSSLEngineImpl}. This version does not lock or and does no error pre-processing.
+     * ConscryptEngine}. This version does not lock or and does no error pre-processing.
      */
     static native int ENGINE_SSL_write_direct(long sslNativePointer, long address, int length,
             SSLHandshakeCallbacks shc) throws IOException;
 
     /**
      * Variant of the {@link #SSL_write} for a heap {@link java.nio.ByteBuffer} used by {@link
-     * OpenSSLEngineImpl}. This version does not lock or and does no error pre-processing.
+     * ConscryptEngine}. This version does not lock or and does no error pre-processing.
      */
     static native int ENGINE_SSL_write_heap(long sslNativePointer, byte[] sourceJava,
             int sourceOffset, int sourceLength, SSLHandshakeCallbacks shc) throws IOException;
@@ -1224,9 +1257,21 @@
             int destOffset, int destLength, SSLHandshakeCallbacks shc) throws IOException;
 
     /**
-     * Variant of the {@link #SSL_shutdown} used by {@link OpenSSLEngineImpl}. This version does not
+     * Variant of the {@link #SSL_shutdown} used by {@link ConscryptEngine}. This version does not
      * lock.
      */
     static native void ENGINE_SSL_shutdown(long sslNativePointer, SSLHandshakeCallbacks shc)
             throws IOException;
+
+    /**
+     * Used for testing only.
+     */
+    static native int BIO_read(long bioRef, byte[] buffer);
+    static native void BIO_write(long bioRef, byte[] buffer, int offset, int length)
+            throws IOException;
+    static native long ERR_peek_last_error();
+    static native long SSL_clear_mode(long ssl, long mode);
+    static native long SSL_get_mode(long ssl);
+    static native long SSL_get_options(long ssl);
+    static native long SSL_get1_session(long ssl);
 }
diff --git a/common/src/main/java/org/conscrypt/NativeRef.java b/common/src/main/java/org/conscrypt/NativeRef.java
index ccc7cc0..62e9bd6 100644
--- a/common/src/main/java/org/conscrypt/NativeRef.java
+++ b/common/src/main/java/org/conscrypt/NativeRef.java
@@ -23,12 +23,12 @@
 abstract class NativeRef {
     final long context;
 
-    NativeRef(long ctx) {
-        if (ctx == 0) {
-            throw new NullPointerException("ctx == 0");
+    NativeRef(long context) {
+        if (context == 0) {
+            throw new NullPointerException("context == 0");
         }
 
-        this.context = ctx;
+        this.context = context;
     }
 
     @Override
@@ -45,108 +45,104 @@
         return (int) context;
     }
 
-    static class EC_GROUP extends NativeRef {
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (context != 0) {
+                doFree(context);
+            }
+        } finally {
+            super.finalize();
+        }
+    }
+
+    abstract void doFree(long context);
+
+    static final class EC_GROUP extends NativeRef {
         EC_GROUP(long ctx) {
             super(ctx);
         }
 
         @Override
-        protected void finalize() throws Throwable {
-            try {
-                NativeCrypto.EC_GROUP_clear_free(context);
-            } finally {
-                super.finalize();
-            }
+        void doFree(long context) {
+            NativeCrypto.EC_GROUP_clear_free(context);
         }
     }
 
-    static class EC_POINT extends NativeRef {
-        EC_POINT(long ctx) {
-            super(ctx);
+    static final class EC_POINT extends NativeRef {
+        EC_POINT(long nativePointer) {
+            super(nativePointer);
         }
 
         @Override
-        protected void finalize() throws Throwable {
-            try {
-                NativeCrypto.EC_POINT_clear_free(context);
-            } finally {
-                super.finalize();
-            }
+        void doFree(long context) {
+            NativeCrypto.EC_POINT_clear_free(context);
         }
     }
 
-    static class EVP_CIPHER_CTX extends NativeRef {
-        EVP_CIPHER_CTX(long ctx) {
-            super(ctx);
+    static final class EVP_CIPHER_CTX extends NativeRef {
+        EVP_CIPHER_CTX(long nativePointer) {
+            super(nativePointer);
         }
 
         @Override
-        protected void finalize() throws Throwable {
-            try {
-                NativeCrypto.EVP_CIPHER_CTX_free(context);
-            } finally {
-                super.finalize();
-            }
+        void doFree(long context) {
+            NativeCrypto.EVP_CIPHER_CTX_free(context);
         }
     }
 
-    static class EVP_MD_CTX extends NativeRef {
-        EVP_MD_CTX(long ctx) {
-            super(ctx);
+    static final class EVP_MD_CTX extends NativeRef {
+        EVP_MD_CTX(long nativePointer) {
+            super(nativePointer);
         }
 
         @Override
-        protected void finalize() throws Throwable {
-            try {
-                NativeCrypto.EVP_MD_CTX_destroy(context);
-            } finally {
-                super.finalize();
-            }
+        void doFree(long context) {
+            NativeCrypto.EVP_MD_CTX_destroy(context);
         }
     }
 
-    static class EVP_PKEY extends NativeRef {
-        EVP_PKEY(long ctx) {
-            super(ctx);
+    static final class EVP_PKEY extends NativeRef {
+        EVP_PKEY(long nativePointer) {
+            super(nativePointer);
         }
 
         @Override
-        protected void finalize() throws Throwable {
-            try {
-                NativeCrypto.EVP_PKEY_free(context);
-            } finally {
-                super.finalize();
-            }
+        void doFree(long context) {
+            NativeCrypto.EVP_PKEY_free(context);
         }
     }
 
-    static class EVP_PKEY_CTX extends NativeRef {
-        EVP_PKEY_CTX(long ctx) {
-            super(ctx);
+    static final class EVP_PKEY_CTX extends NativeRef {
+        EVP_PKEY_CTX(long nativePointer) {
+            super(nativePointer);
         }
 
         @Override
-        protected void finalize() throws Throwable {
-            try {
-                NativeCrypto.EVP_PKEY_CTX_free(context);
-            } finally {
-                super.finalize();
-            }
+        void doFree(long context) {
+            NativeCrypto.EVP_PKEY_CTX_free(context);
         }
     }
 
-    static class HMAC_CTX extends NativeRef {
-        HMAC_CTX(long ctx) {
-            super(ctx);
+    static final class HMAC_CTX extends NativeRef {
+        HMAC_CTX(long nativePointer) {
+            super(nativePointer);
         }
 
         @Override
-        protected void finalize() throws Throwable {
-            try {
-                NativeCrypto.HMAC_CTX_free(context);
-            } finally {
-                super.finalize();
-            }
+        void doFree(long context) {
+            NativeCrypto.HMAC_CTX_free(context);
+        }
+    }
+
+    static final class SSL_SESSION extends NativeRef {
+        SSL_SESSION(long nativePointer) {
+            super(nativePointer);
+        }
+
+        @Override
+        void doFree(long context) {
+            NativeCrypto.SSL_SESSION_free(context);
         }
     }
 }
diff --git a/common/src/main/java/org/conscrypt/OpenSSLCipherRSA.java b/common/src/main/java/org/conscrypt/OpenSSLCipherRSA.java
index dac03b7..c975c33 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLCipherRSA.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLCipherRSA.java
@@ -23,6 +23,8 @@
 import java.security.Key;
 import java.security.KeyFactory;
 import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
 import java.security.SecureRandom;
 import java.security.SignatureException;
 import java.security.interfaces.RSAPrivateCrtKey;
@@ -199,6 +201,10 @@
             usingPrivateKey = false;
             this.key = OpenSSLRSAPublicKey.getInstance(rsaPublicKey);
         } else {
+            if (null == key) {
+                throw new InvalidKeyException("RSA private or public key is null");
+            }
+            
             throw new InvalidKeyException("Need RSA private or public key");
         }
 
@@ -481,6 +487,21 @@
         }
 
         @Override
+        void engineInitInternal(int opmode, Key key, AlgorithmParameterSpec spec)
+                throws InvalidKeyException, InvalidAlgorithmParameterException {
+            if (opmode == Cipher.ENCRYPT_MODE || opmode == Cipher.WRAP_MODE) {
+                if (!(key instanceof PublicKey)) {
+                    throw new InvalidKeyException("Only public keys may be used to encrypt");
+                }
+            } else if (opmode == Cipher.DECRYPT_MODE || opmode == Cipher.UNWRAP_MODE) {
+                if (!(key instanceof PrivateKey)) {
+                    throw new InvalidKeyException("Only private keys may be used to decrypt");
+                }
+            }
+            super.engineInitInternal(opmode, key, spec);
+        }
+
+        @Override
         void doCryptoInit(AlgorithmParameterSpec spec)
                 throws InvalidAlgorithmParameterException {
             pkeyCtx = new NativeRef.EVP_PKEY_CTX(encrypting
diff --git a/common/src/main/java/org/conscrypt/OpenSSLContextImpl.java b/common/src/main/java/org/conscrypt/OpenSSLContextImpl.java
index 74050e3..143f481 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLContextImpl.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLContextImpl.java
@@ -30,11 +30,12 @@
 /**
  * OpenSSL-backed SSLContext service provider interface.
  *
+ * <p>Public to allow contruction via the provider framework.
+ *
  * @hide
  */
 @Internal
 public abstract class OpenSSLContextImpl extends SSLContextSpi {
-
     /**
      * The default SSLContextImpl for use with
      * SSLContext.getInstance("Default"). Protected by the
@@ -97,8 +98,8 @@
     @Override
     public void engineInit(KeyManager[] kms, TrustManager[] tms, SecureRandom sr)
             throws KeyManagementException {
-        sslParameters = new SSLParametersImpl(kms, tms, sr, clientSessionContext,
-                serverSessionContext, algorithms);
+        sslParameters = new SSLParametersImpl(
+                kms, tms, sr, clientSessionContext, serverSessionContext, algorithms);
     }
 
     @Override
@@ -124,7 +125,7 @@
         }
         SSLParametersImpl p = (SSLParametersImpl) sslParameters.clone();
         p.setUseClientMode(false);
-        return new OpenSSLEngineImpl(host, port, p);
+        return new ConscryptEngine(host, port, p);
     }
 
     @Override
@@ -134,7 +135,7 @@
         }
         SSLParametersImpl p = (SSLParametersImpl) sslParameters.clone();
         p.setUseClientMode(false);
-        return new OpenSSLEngineImpl(p);
+        return new ConscryptEngine(p);
     }
 
     @Override
@@ -147,18 +148,27 @@
         return clientSessionContext;
     }
 
+    /**
+     * Public to allow construction via the provider framework.
+     */
     public static final class TLSv12 extends OpenSSLContextImpl {
         public TLSv12() {
             super(NativeCrypto.TLSV12_PROTOCOLS);
         }
     }
 
+    /**
+     * Public to allow construction via the provider framework.
+     */
     public static final class TLSv11 extends OpenSSLContextImpl {
         public TLSv11() {
             super(NativeCrypto.TLSV11_PROTOCOLS);
         }
     }
 
+    /**
+     * Public to allow construction via the provider framework.
+     */
     public static final class TLSv1 extends OpenSSLContextImpl {
         public TLSv1() {
             super(NativeCrypto.TLSV1_PROTOCOLS);
diff --git a/common/src/main/java/org/conscrypt/OpenSSLECGroupContext.java b/common/src/main/java/org/conscrypt/OpenSSLECGroupContext.java
index 7ac70dd..71abafa 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLECGroupContext.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLECGroupContext.java
@@ -36,12 +36,10 @@
     }
 
     static OpenSSLECGroupContext getCurveByName(String curveName) {
-        // Workaround for OpenSSL not supporting SECG names for NIST P-192 and P-256
-        // (aka ANSI X9.62 prime192v1 and prime256v1) curve names.
+        // Workaround for OpenSSL not supporting SECG names for NIST P-256 (aka
+        // ANSI X9.62 prime256v1).
         if ("secp256r1".equals(curveName)) {
             curveName = "prime256v1";
-        } else if ("secp192r1".equals(curveName)) {
-            curveName = "prime192v1";
         }
 
         final long ctx = NativeCrypto.EC_GROUP_new_by_curve_name(curveName);
diff --git a/common/src/main/java/org/conscrypt/OpenSSLECPrivateKey.java b/common/src/main/java/org/conscrypt/OpenSSLECPrivateKey.java
index c0e1654..a466812 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLECPrivateKey.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLECPrivateKey.java
@@ -161,7 +161,7 @@
 
     @Override
     public byte[] getEncoded() {
-        return NativeCrypto.i2d_PKCS8_PRIV_KEY_INFO(key.getNativeRef());
+        return NativeCrypto.EVP_marshal_private_key(key.getNativeRef());
     }
 
     @Override
@@ -214,7 +214,7 @@
 
     @Override
     public int hashCode() {
-        return Arrays.hashCode(NativeCrypto.i2d_PKCS8_PRIV_KEY_INFO(key.getNativeRef()));
+        return Arrays.hashCode(NativeCrypto.EVP_marshal_private_key(key.getNativeRef()));
     }
 
     @Override
@@ -231,7 +231,7 @@
 
         byte[] encoded = (byte[]) stream.readObject();
 
-        key = new OpenSSLKey(NativeCrypto.d2i_PKCS8_PRIV_KEY_INFO(encoded));
+        key = new OpenSSLKey(NativeCrypto.EVP_parse_private_key(encoded));
         group = new OpenSSLECGroupContext(new NativeRef.EC_GROUP(
                 NativeCrypto.EC_KEY_get1_group(key.getNativeRef())));
     }
diff --git a/common/src/main/java/org/conscrypt/OpenSSLECPublicKey.java b/common/src/main/java/org/conscrypt/OpenSSLECPublicKey.java
index 6e32810..792a282 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLECPublicKey.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLECPublicKey.java
@@ -87,7 +87,7 @@
 
     @Override
     public byte[] getEncoded() {
-        return NativeCrypto.i2d_PUBKEY(key.getNativeRef());
+        return NativeCrypto.EVP_marshal_public_key(key.getNativeRef());
     }
 
     @Override
@@ -143,7 +143,7 @@
 
     @Override
     public int hashCode() {
-        return Arrays.hashCode(NativeCrypto.i2d_PUBKEY(key.getNativeRef()));
+        return Arrays.hashCode(NativeCrypto.EVP_marshal_public_key(key.getNativeRef()));
     }
 
     @Override
@@ -156,7 +156,7 @@
 
         byte[] encoded = (byte[]) stream.readObject();
 
-        key = new OpenSSLKey(NativeCrypto.d2i_PUBKEY(encoded));
+        key = new OpenSSLKey(NativeCrypto.EVP_parse_public_key(encoded));
         group = new OpenSSLECGroupContext(new NativeRef.EC_GROUP(
                 NativeCrypto.EC_KEY_get1_group(key.getNativeRef())));
     }
diff --git a/common/src/main/java/org/conscrypt/OpenSSLEngineSocketImpl.java b/common/src/main/java/org/conscrypt/OpenSSLEngineSocketImpl.java
deleted file mode 100644
index 7e711d8..0000000
--- a/common/src/main/java/org/conscrypt/OpenSSLEngineSocketImpl.java
+++ /dev/null
@@ -1,609 +0,0 @@
-/*
- * Copyright 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.conscrypt;
-
-import static javax.net.ssl.SSLEngineResult.Status.OK;
-
-import java.io.EOFException;
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.InetAddress;
-import java.net.Socket;
-import java.net.SocketException;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
-import java.security.PrivateKey;
-import java.security.cert.CertificateException;
-import javax.crypto.SecretKey;
-import javax.net.ssl.SSLEngineResult;
-import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLSession;
-import javax.net.ssl.X509KeyManager;
-import javax.security.auth.x500.X500Principal;
-
-/**
- * Implements crypto handling by delegating to OpenSSLEngine. Used for socket implementations
- * that are not backed by a real OS socket.
- */
-final class OpenSSLEngineSocketImpl extends OpenSSLSocketImplWrapper {
-    private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
-
-    private final OpenSSLEngineImpl engine;
-    private final Socket socket;
-    private final OutputStreamWrapper outputStreamWrapper;
-    private final InputStreamWrapper inputStreamWrapper;
-    private boolean handshakeComplete;
-
-    OpenSSLEngineSocketImpl(SSLParametersImpl sslParameters) throws IOException {
-        this(new Socket(), null, -1, false, sslParameters);
-    }
-
-    OpenSSLEngineSocketImpl(String host, int port, SSLParametersImpl sslParameters)
-            throws IOException {
-        this(new Socket(host, port), host, port, false, sslParameters);
-    }
-
-    OpenSSLEngineSocketImpl(String host, int port, InetAddress clientAddress, int clientPort,
-            SSLParametersImpl sslParameters) throws IOException {
-        this(new Socket(host, port, clientAddress, clientPort), host, port, false, sslParameters);
-    }
-
-    OpenSSLEngineSocketImpl(InetAddress address, int port, SSLParametersImpl sslParameters)
-            throws IOException {
-        this(new Socket(address, port), null, port, false, sslParameters);
-    }
-
-    OpenSSLEngineSocketImpl(InetAddress address, int port, InetAddress clientAddress,
-            int clientPort, SSLParametersImpl sslParameters) throws IOException {
-        this(new Socket(address, port, clientAddress, clientPort), null, port, false,
-                sslParameters);
-    }
-
-    OpenSSLEngineSocketImpl(Socket socket, String hostname, int port, boolean autoClose,
-            SSLParametersImpl sslParameters) throws IOException {
-        super(socket, hostname, port, autoClose, sslParameters);
-        this.socket = socket;
-        engine = new OpenSSLEngineImpl(hostname, port, sslParameters);
-
-        // When the handshake completes, notify any listeners.
-        engine.setHandshakeListener(new HandshakeListener() {
-            @Override
-            public void onHandshakeFinished() {
-                if (!handshakeComplete) {
-                    handshakeComplete = true;
-                    OpenSSLEngineSocketImpl.this.notifyHandshakeCompletedListeners();
-                }
-            }
-        });
-        outputStreamWrapper = new OutputStreamWrapper();
-        inputStreamWrapper = new InputStreamWrapper();
-        engine.setUseClientMode(sslParameters.getUseClientMode());
-    }
-
-    @Override
-    public void startHandshake() throws IOException {
-        try {
-            // Trigger the handshake
-            boolean beginHandshakeCalled = false;
-            while (!handshakeComplete) {
-                switch (engine.getHandshakeStatus()) {
-                    case NOT_HANDSHAKING: {
-                        if (!beginHandshakeCalled) {
-                            beginHandshakeCalled = true;
-                            engine.beginHandshake();
-                            break;
-                        }
-                        break;
-                    }
-                    case FINISHED: {
-                        return;
-                    }
-                    case NEED_WRAP: {
-                        outputStreamWrapper.write(EMPTY_BUFFER);
-                        break;
-                    }
-                    case NEED_UNWRAP: {
-                        if (inputStreamWrapper.read(EmptyArray.BYTE) == -1) {
-                            // Can't complete the handshake due to EOF.
-                            throw SSLUtils.toSSLHandshakeException(new EOFException());
-                        }
-                        break;
-                    }
-                    case NEED_TASK: {
-                        throw new IllegalStateException("OpenSSLEngineImpl returned NEED_TASK");
-                    }
-                    default: {
-                        break;
-                    }
-                }
-            }
-        } catch (Exception e) {
-            close();
-            throw SSLUtils.toSSLHandshakeException(e);
-        }
-    }
-
-    @Override
-    public void onSSLStateChange(int type, int val) {
-        throw new AssertionError("Should be handled by engine");
-    }
-
-    @Override
-    public void verifyCertificateChain(long[] certRefs, String authMethod)
-            throws CertificateException {
-        throw new AssertionError("Should be handled by engine");
-    }
-
-    @Override
-    public InputStream getInputStream() throws IOException {
-        return inputStreamWrapper;
-    }
-
-    @Override
-    public OutputStream getOutputStream() throws IOException {
-        return outputStreamWrapper;
-    }
-
-    @Override
-    public SSLSession getSession() {
-        return engine.getSession();
-    }
-
-    @Override
-    public boolean getEnableSessionCreation() {
-        return super.getEnableSessionCreation();
-    }
-
-    @Override
-    public void setEnableSessionCreation(boolean flag) {
-        super.setEnableSessionCreation(flag);
-    }
-
-    @Override
-    public String[] getSupportedCipherSuites() {
-        return super.getSupportedCipherSuites();
-    }
-
-    @Override
-    public String[] getEnabledCipherSuites() {
-        return super.getEnabledCipherSuites();
-    }
-
-    @Override
-    public void setEnabledCipherSuites(String[] suites) {
-        super.setEnabledCipherSuites(suites);
-    }
-
-    @Override
-    public String[] getSupportedProtocols() {
-        return super.getSupportedProtocols();
-    }
-
-    @Override
-    public String[] getEnabledProtocols() {
-        return super.getEnabledProtocols();
-    }
-
-    @Override
-    public void setEnabledProtocols(String[] protocols) {
-        super.setEnabledProtocols(protocols);
-    }
-
-    @Override
-    public void setUseSessionTickets(boolean useSessionTickets) {
-        super.setUseSessionTickets(useSessionTickets);
-    }
-
-    @Override
-    public void setHostname(String hostname) {
-        super.setHostname(hostname);
-    }
-
-    @Override
-    public void setChannelIdEnabled(boolean enabled) {
-        super.setChannelIdEnabled(enabled);
-    }
-
-    @Override
-    public byte[] getChannelId() throws SSLException {
-        return super.getChannelId();
-    }
-
-    @Override
-    public void setChannelIdPrivateKey(PrivateKey privateKey) {
-        super.setChannelIdPrivateKey(privateKey);
-    }
-
-    @Override
-    public boolean getUseClientMode() {
-        return super.getUseClientMode();
-    }
-
-    @Override
-    public void setUseClientMode(boolean mode) {
-        engine.setUseClientMode(mode);
-    }
-
-    @Override
-    public boolean getWantClientAuth() {
-        return super.getWantClientAuth();
-    }
-
-    @Override
-    public boolean getNeedClientAuth() {
-        return super.getNeedClientAuth();
-    }
-
-    @Override
-    public void setNeedClientAuth(boolean need) {
-        super.setNeedClientAuth(need);
-    }
-
-    @Override
-    public void setWantClientAuth(boolean want) {
-        super.setWantClientAuth(want);
-    }
-
-    @Override
-    public void sendUrgentData(int data) throws IOException {
-        super.sendUrgentData(data);
-    }
-
-    @Override
-    public void setOOBInline(boolean on) throws SocketException {
-        super.setOOBInline(on);
-    }
-
-    @Override
-    public void setSoWriteTimeout(int writeTimeoutMilliseconds) throws SocketException {
-        throw new UnsupportedOperationException("Not supported");
-    }
-
-    @Override
-    public int getSoWriteTimeout() throws SocketException {
-        return 0;
-    }
-
-    @Override
-    public void setHandshakeTimeout(int handshakeTimeoutMilliseconds) throws SocketException {
-        throw new UnsupportedOperationException("Not supported");
-    }
-
-    @Override
-    public synchronized void close() throws IOException {
-        // Closing Socket.
-        engine.closeInbound();
-        engine.closeOutbound();
-        socket.close();
-    }
-
-    @Override
-    protected void finalize() throws Throwable {
-        super.finalize();
-    }
-
-    @Override
-    public SocketChannel getChannel() {
-        return super.getChannel();
-    }
-
-    @Override
-    public FileDescriptor getFileDescriptor$() {
-        throw new UnsupportedOperationException("Not supported");
-    }
-
-    @Override
-    public byte[] getNpnSelectedProtocol() {
-        return null;
-    }
-
-    @Override
-    public byte[] getAlpnSelectedProtocol() {
-        return engine.getAlpnSelectedProtocol();
-    }
-
-    @Override
-    public void setNpnProtocols(byte[] npnProtocols) {
-        super.setNpnProtocols(npnProtocols);
-    }
-
-    @Override
-    public void setAlpnProtocols(byte[] alpnProtocols) {
-        super.setAlpnProtocols(alpnProtocols);
-    }
-
-    @Override
-    public String chooseServerAlias(X509KeyManager keyManager, String keyType) {
-        return engine.chooseServerAlias(keyManager, keyType);
-    }
-
-    @Override
-    public String chooseClientAlias(
-            X509KeyManager keyManager, X500Principal[] issuers, String[] keyTypes) {
-        return engine.chooseClientAlias(keyManager, issuers, keyTypes);
-    }
-
-    @Override
-    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
-    public String chooseServerPSKIdentityHint(PSKKeyManager keyManager) {
-        return engine.chooseServerPSKIdentityHint(keyManager);
-    }
-
-    @Override
-    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
-    public String chooseClientPSKIdentity(PSKKeyManager keyManager, String identityHint) {
-        return engine.chooseClientPSKIdentity(keyManager, identityHint);
-    }
-
-    @Override
-    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
-    public SecretKey getPSKKey(PSKKeyManager keyManager, String identityHint, String identity) {
-        return engine.getPSKKey(keyManager, identityHint, identity);
-    }
-
-    /**
-     * Wrap bytes written to the underlying socket.
-     */
-    private final class OutputStreamWrapper extends OutputStream {
-        private final Object stateLock = new Object();
-        private ByteBuffer target;
-        private OutputStream socketOutputStream;
-        private SocketChannel socketChannel;
-
-        OutputStreamWrapper() {}
-
-        @Override
-        public void write(int b) throws IOException {
-            write(new byte[] {(byte) b});
-        }
-
-        @Override
-        public void write(byte[] b) throws IOException {
-            write(ByteBuffer.wrap(b));
-        }
-
-        @Override
-        public void write(byte[] b, int off, int len) throws IOException {
-            write(ByteBuffer.wrap(b, off, len));
-        }
-
-        private void write(ByteBuffer buffer) throws IOException {
-            synchronized (stateLock) {
-                try {
-                    init();
-
-                    // Need to loop through at least once to enable handshaking where no application
-                    // bytes are
-                    // processed.
-                    int len = buffer.remaining();
-                    SSLEngineResult engineResult;
-                    do {
-                        target.clear();
-                        engineResult = engine.wrap(buffer, target);
-                        if (engineResult.getStatus() != OK) {
-                            throw new SSLException(
-                                    "Unexpected engine result " + engineResult.getStatus());
-                        }
-                        if (target.position() != engineResult.bytesProduced()) {
-                            throw new SSLException("Engine bytesProduced "
-                                    + engineResult.bytesProduced()
-                                    + " does not match bytes written " + target.position());
-                        }
-                        len -= engineResult.bytesConsumed();
-                        if (len != buffer.remaining()) {
-                            throw new SSLException(
-                                    "Engine did not read the correct number of bytes");
-                        }
-
-                        target.flip();
-
-                        // Write the data to the socket.
-                        if (socketChannel != null) {
-                            // Loop until all of the data is written to the channel. Typically,
-                            // SocketChannel writes will return only after all bytes are written,
-                            // so we won't really loop here.
-                            while (target.hasRemaining()) {
-                                socketChannel.write(target);
-                            }
-                        } else {
-                            // Target is a heap buffer.
-                            socketOutputStream.write(target.array(), 0, target.limit());
-                        }
-                    } while (len > 0);
-                } catch (IOException e) {
-                    throw e;
-                } catch (RuntimeException e) {
-                    throw e;
-                }
-            }
-        }
-
-        @Override
-        public void flush() throws IOException {
-            synchronized (stateLock) {
-                init();
-                socketOutputStream.flush();
-            }
-        }
-
-        @Override
-        public void close() throws IOException {
-            socket.close();
-        }
-
-        private void init() throws IOException {
-            if (socketOutputStream == null) {
-                socketOutputStream = socket.getOutputStream();
-                socketChannel = socket.getChannel();
-                if (socketChannel != null) {
-                    // Optimization. Using direct buffers wherever possible to avoid passing
-                    // arrays to JNI.
-                    target = ByteBuffer.allocateDirect(engine.getSession().getPacketBufferSize());
-                } else {
-                    target = ByteBuffer.allocate(engine.getSession().getPacketBufferSize());
-                }
-            }
-        }
-    }
-
-    /**
-     * Unwrap bytes read from the underlying socket.
-     */
-    private final class InputStreamWrapper extends InputStream {
-        private final Object stateLock = new Object();
-        private final byte[] singleByte = new byte[1];
-        private final ByteBuffer fromEngine;
-        private ByteBuffer fromSocket;
-        private InputStream socketInputStream;
-        private SocketChannel socketChannel;
-
-        InputStreamWrapper() {
-            fromEngine = ByteBuffer.allocateDirect(engine.getSession().getApplicationBufferSize());
-            // Initially fromEngine.remaining() == 0.
-            fromEngine.flip();
-        }
-
-        @Override
-        public int read() throws IOException {
-            synchronized (stateLock) {
-                // Handle returning of -1 if EOF is reached.
-                int count = read(singleByte, 0, 1);
-                if (count == -1) {
-                    // Handle EOF.
-                    return -1;
-                }
-                if (count != 1) {
-                    throw new SSLException("read incorrect number of bytes " + count);
-                }
-                return (int) singleByte[0];
-            }
-        }
-
-        @Override
-        public int read(byte[] b) throws IOException {
-            return read(b, 0, b.length);
-        }
-
-        @Override
-        public int read(byte[] b, int off, int len) throws IOException {
-            synchronized (stateLock) {
-                try {
-                    // Make sure the input stream has been created.
-                    init();
-
-                    for (;;) {
-                        // Serve any remaining data from the engine first.
-                        if (fromEngine.remaining() > 0) {
-                            int readFromEngine = Math.min(fromEngine.remaining(), len);
-                            fromEngine.get(b, off, readFromEngine);
-                            return readFromEngine;
-                        }
-
-                        // Try to unwrap any data already in the socket buffer.
-                        boolean needMoreData = true;
-                        if (fromSocket.position() > 0) {
-                            // Unwrap the unencrypted bytes into the engine buffer.
-                            fromSocket.flip();
-                            fromEngine.clear();
-                            SSLEngineResult engineResult = engine.unwrap(fromSocket, fromEngine);
-
-                            // Shift any remaining data to the beginning of the buffer so that
-                            // we can accommodate the next full packet. After this is called,
-                            // limit will be restored to capacity and position will point just
-                            // past the end of the data.
-                            fromSocket.compact();
-                            fromEngine.flip();
-
-                            switch (engineResult.getStatus()) {
-                                case BUFFER_UNDERFLOW: {
-                                    if (engineResult.bytesProduced() == 0) {
-                                        // Need to read more data from the socket.
-                                        break;
-                                    }
-                                    // Also serve the data that was produced.
-                                    needMoreData = false;
-                                    break;
-                                }
-                                case OK: {
-                                    // We processed the entire packet successfully.
-                                    needMoreData = false;
-                                    break;
-                                }
-                                case CLOSED: {
-                                    // EOF
-                                    return -1;
-                                }
-                                default: {
-                                    // Anything else is an error.
-                                    throw new SSLException(
-                                            "Unexpected engine result " + engineResult.getStatus());
-                                }
-                            }
-
-                            if (!needMoreData && engineResult.bytesProduced() == 0) {
-                                // Read successfully, but produced no data. Possibly part of a
-                                // handshake.
-                                return 0;
-                            }
-                        }
-
-                        // Read more data from the socket.
-                        if (needMoreData && readFromSocket() == -1) {
-                            // Failed to read the next encrypted packet before reaching EOF.
-                            return -1;
-                        }
-
-                        // Continue the loop and return the data from the engine buffer.
-                    }
-                } catch (IOException e) {
-                    throw e;
-                } catch (RuntimeException e) {
-                    throw e;
-                }
-            }
-        }
-
-        private void init() throws IOException {
-            if (socketInputStream == null) {
-                socketInputStream = socket.getInputStream();
-                socketChannel = socket.getChannel();
-                if (socketChannel != null) {
-                    fromSocket =
-                            ByteBuffer.allocateDirect(engine.getSession().getPacketBufferSize());
-                } else {
-                    fromSocket = ByteBuffer.allocate(engine.getSession().getPacketBufferSize());
-                }
-            }
-        }
-
-        private int readFromSocket() throws IOException {
-            if (socketChannel != null) {
-                return socketChannel.read(fromSocket);
-            }
-            // Read directly to the underlying array and increment the buffer position if
-            // appropriate.
-            int read = socketInputStream.read(
-                    fromSocket.array(), fromSocket.position(), fromSocket.remaining());
-            if (read > 0) {
-                fromSocket.position(fromSocket.position() + read);
-            }
-            return read;
-        }
-    }
-}
diff --git a/common/src/main/java/org/conscrypt/OpenSSLKey.java b/common/src/main/java/org/conscrypt/OpenSSLKey.java
index 1ca0b2c..54af772 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLKey.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLKey.java
@@ -73,7 +73,7 @@
             throw new InvalidKeyException("Key encoding is null");
         }
 
-        return new OpenSSLKey(NativeCrypto.d2i_PKCS8_PRIV_KEY_INFO(key.getEncoded()));
+        return new OpenSSLKey(NativeCrypto.EVP_parse_private_key(key.getEncoded()));
     }
 
     /**
@@ -177,7 +177,7 @@
         if (encoded == null) {
             return null;
         }
-        return new OpenSSLKey(NativeCrypto.d2i_PKCS8_PRIV_KEY_INFO(encoded));
+        return new OpenSSLKey(NativeCrypto.EVP_parse_private_key(encoded));
     }
 
     /**
@@ -222,7 +222,7 @@
         }
 
         try {
-            return new OpenSSLKey(NativeCrypto.d2i_PUBKEY(key.getEncoded()));
+            return new OpenSSLKey(NativeCrypto.EVP_parse_public_key(key.getEncoded()));
         } catch (Exception e) {
             throw new InvalidKeyException(e);
         }
@@ -267,7 +267,7 @@
 
         final OpenSSLKey key;
         try {
-            key = new OpenSSLKey(NativeCrypto.d2i_PUBKEY(x509KeySpec.getEncoded()));
+            key = new OpenSSLKey(NativeCrypto.EVP_parse_public_key(x509KeySpec.getEncoded()));
         } catch (Exception e) {
             throw new InvalidKeySpecException(e);
         }
@@ -300,7 +300,7 @@
 
         final OpenSSLKey key;
         try {
-            key = new OpenSSLKey(NativeCrypto.d2i_PKCS8_PRIV_KEY_INFO(pkcs8KeySpec.getEncoded()));
+            key = new OpenSSLKey(NativeCrypto.EVP_parse_private_key(pkcs8KeySpec.getEncoded()));
         } catch (Exception e) {
             throw new InvalidKeySpecException(e);
         }
diff --git a/common/src/main/java/org/conscrypt/OpenSSLProvider.java b/common/src/main/java/org/conscrypt/OpenSSLProvider.java
index 1c23df1..1b83c8d 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLProvider.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLProvider.java
@@ -72,6 +72,12 @@
         put("SSLContext.TLSv1.2", tls12SSLContext);
         put("SSLContext.Default", PREFIX + "DefaultSSLContextImpl");
 
+        /* === AlgorithmParameters === */
+        put("AlgorithmParameters.GCM", PREFIX + "GCMParameters");
+        put("Alg.Alias.AlgorithmParameters.2.16.840.1.101.3.4.1.6", "GCM");
+        put("Alg.Alias.AlgorithmParameters.2.16.840.1.101.3.4.1.26", "GCM");
+        put("Alg.Alias.AlgorithmParameters.2.16.840.1.101.3.4.1.46", "GCM");
+
         /* === Message Digests === */
         put("MessageDigest.SHA-1", PREFIX + "OpenSSLMessageDigestJDK$SHA1");
         put("Alg.Alias.MessageDigest.SHA1", "SHA-1");
@@ -382,17 +388,15 @@
         put("Alg.Alias.Cipher.1.2.840.113549.3.4", "ARC4");
         put("Alg.Alias.Cipher.OID.1.2.840.113549.3.4", "ARC4");
 
-        if (NativeConstants.HAS_EVP_AEAD) {
-            putSymmetricCipherImplClass("AES/GCM/NoPadding", "OpenSSLCipher$EVP_AEAD$AES$GCM");
-            put("Alg.Alias.Cipher.GCM", "AES/GCM/NoPadding");
-            put("Alg.Alias.Cipher.2.16.840.1.101.3.4.1.6", "AES/GCM/NoPadding");
-            put("Alg.Alias.Cipher.2.16.840.1.101.3.4.1.26", "AES/GCM/NoPadding");
-            put("Alg.Alias.Cipher.2.16.840.1.101.3.4.1.46", "AES/GCM/NoPadding");
-            putSymmetricCipherImplClass(
-                    "AES_128/GCM/NoPadding", "OpenSSLCipher$EVP_AEAD$AES$GCM$AES_128");
-            putSymmetricCipherImplClass(
-                    "AES_256/GCM/NoPadding", "OpenSSLCipher$EVP_AEAD$AES$GCM$AES_256");
-        }
+        putSymmetricCipherImplClass("AES/GCM/NoPadding", "OpenSSLCipher$EVP_AEAD$AES$GCM");
+        put("Alg.Alias.Cipher.GCM", "AES/GCM/NoPadding");
+        put("Alg.Alias.Cipher.2.16.840.1.101.3.4.1.6", "AES/GCM/NoPadding");
+        put("Alg.Alias.Cipher.2.16.840.1.101.3.4.1.26", "AES/GCM/NoPadding");
+        put("Alg.Alias.Cipher.2.16.840.1.101.3.4.1.46", "AES/GCM/NoPadding");
+        putSymmetricCipherImplClass(
+                "AES_128/GCM/NoPadding", "OpenSSLCipher$EVP_AEAD$AES$GCM$AES_128");
+        putSymmetricCipherImplClass(
+                "AES_256/GCM/NoPadding", "OpenSSLCipher$EVP_AEAD$AES$GCM$AES_256");
 
         /* === Mac === */
 
diff --git a/common/src/main/java/org/conscrypt/OpenSSLRSAPrivateKey.java b/common/src/main/java/org/conscrypt/OpenSSLRSAPrivateKey.java
index 259e38e..8c2e4df 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLRSAPrivateKey.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLRSAPrivateKey.java
@@ -198,7 +198,7 @@
 
     @Override
     public final byte[] getEncoded() {
-        return NativeCrypto.i2d_PKCS8_PRIV_KEY_INFO(key.getNativeRef());
+        return NativeCrypto.EVP_marshal_private_key(key.getNativeRef());
     }
 
     @Override
diff --git a/common/src/main/java/org/conscrypt/OpenSSLRSAPublicKey.java b/common/src/main/java/org/conscrypt/OpenSSLRSAPublicKey.java
index 40d878d..ffc672f 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLRSAPublicKey.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLRSAPublicKey.java
@@ -96,7 +96,7 @@
 
     @Override
     public byte[] getEncoded() {
-        return NativeCrypto.i2d_PUBKEY(key.getNativeRef());
+        return NativeCrypto.EVP_marshal_public_key(key.getNativeRef());
     }
 
     private void ensureReadParams() {
diff --git a/common/src/main/java/org/conscrypt/OpenSSLServerSocketFactoryImpl.java b/common/src/main/java/org/conscrypt/OpenSSLServerSocketFactoryImpl.java
index 008e92f..c2507d8 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLServerSocketFactoryImpl.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLServerSocketFactoryImpl.java
@@ -24,6 +24,9 @@
 
 /**
  * An implementation of {@link SSLServerSocketFactory} using BoringSSL.
+ *
+ * <p/>This name of this class cannot change in order to maintain backward-compatibility with GMS
+ * core {@code ProviderInstallerImpl}
  */
 final class OpenSSLServerSocketFactoryImpl extends SSLServerSocketFactory {
     private static boolean useEngineSocketByDefault = SSLUtils.USE_ENGINE_SOCKET_BY_DEFAULT;
@@ -37,8 +40,7 @@
             this.sslParameters = SSLParametersImpl.getDefault();
             this.sslParameters.setUseClientMode(false);
         } catch (KeyManagementException e) {
-            instantiationException =
-                new IOException("Delayed instantiation exception:");
+            instantiationException = new IOException("Delayed instantiation exception:");
             instantiationException.initCause(e);
         }
     }
@@ -75,26 +77,26 @@
 
     @Override
     public ServerSocket createServerSocket() throws IOException {
-        return new OpenSSLServerSocketImpl((SSLParametersImpl) sslParameters.clone())
+        return new ConscryptServerSocket((SSLParametersImpl) sslParameters.clone())
                 .setUseEngineSocket(useEngineSocket);
     }
 
     @Override
     public ServerSocket createServerSocket(int port) throws IOException {
-        return new OpenSSLServerSocketImpl(port, (SSLParametersImpl) sslParameters.clone())
+        return new ConscryptServerSocket(port, (SSLParametersImpl) sslParameters.clone())
                 .setUseEngineSocket(useEngineSocket);
     }
 
     @Override
     public ServerSocket createServerSocket(int port, int backlog) throws IOException {
-        return new OpenSSLServerSocketImpl(port, backlog, (SSLParametersImpl) sslParameters.clone())
+        return new ConscryptServerSocket(port, backlog, (SSLParametersImpl) sslParameters.clone())
                 .setUseEngineSocket(useEngineSocket);
     }
 
     @Override
     public ServerSocket createServerSocket(int port, int backlog, InetAddress iAddress)
             throws IOException {
-        return new OpenSSLServerSocketImpl(
+        return new ConscryptServerSocket(
                 port, backlog, iAddress, (SSLParametersImpl) sslParameters.clone())
                 .setUseEngineSocket(useEngineSocket);
     }
diff --git a/common/src/main/java/org/conscrypt/OpenSSLSessionImpl.java b/common/src/main/java/org/conscrypt/OpenSSLSessionImpl.java
deleted file mode 100644
index d94252b..0000000
--- a/common/src/main/java/org/conscrypt/OpenSSLSessionImpl.java
+++ /dev/null
@@ -1,333 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.conscrypt;
-
-import java.io.IOException;
-import java.security.cert.X509Certificate;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSessionBindingEvent;
-import javax.net.ssl.SSLSessionBindingListener;
-
-/**
- * Implementation of the class OpenSSLSessionImpl
- * based on BoringSSL.
- */
-class OpenSSLSessionImpl extends AbstractOpenSSLSession {
-    private long creationTime = 0;
-    long lastAccessedTime = 0;
-    final X509Certificate[] localCertificates;
-    final X509Certificate[] peerCertificates;
-
-    private final Map<String, Object> values = new HashMap<String, Object>();
-    private byte[] peerCertificateOcspData;
-    private byte[] peerTlsSctData;
-    long sslSessionNativePointer;
-    private String peerHost;
-    private int peerPort = -1;
-    private String cipherSuite;
-    private String protocol;
-    private byte[] id;
-
-    /**
-     * Class constructor creates an SSL session context given the appropriate
-     * SSL parameters.
-     */
-    OpenSSLSessionImpl(long sslSessionNativePointer, X509Certificate[] localCertificates,
-            X509Certificate[] peerCertificates, byte[] peerCertificateOcspData,
-            byte[] peerTlsSctData, String peerHost, int peerPort,
-            AbstractSessionContext sessionContext) {
-        super(sessionContext);
-        this.sslSessionNativePointer = sslSessionNativePointer;
-        this.localCertificates = localCertificates;
-        this.peerCertificates = peerCertificates;
-        this.peerCertificateOcspData = peerCertificateOcspData;
-        this.peerTlsSctData = peerTlsSctData;
-        this.peerHost = peerHost;
-        this.peerPort = peerPort;
-    }
-
-    /**
-     * Constructs a session from a byte[] containing an SSL session serialized with DER encoding.
-     * This allows loading of a previously saved OpenSSLSessionImpl.
-     *
-     * @throws IOException if the serialized session data can not be parsed
-     */
-    OpenSSLSessionImpl(byte[] derData, String peerHost, int peerPort,
-            X509Certificate[] peerCertificates, byte[] peerCertificateOcspData,
-            byte[] peerTlsSctData, AbstractSessionContext sessionContext)
-            throws IOException {
-        this(NativeCrypto.d2i_SSL_SESSION(derData), null, peerCertificates,
-                peerCertificateOcspData, peerTlsSctData, peerHost, peerPort, sessionContext);
-    }
-
-    /**
-     * Gets the identifier of the actual SSL session
-     * @return array of sessions' identifiers.
-     */
-    @Override
-    public byte[] getId() {
-        if (id == null) {
-            resetId();
-        }
-        return id;
-    }
-
-    /**
-     * Reset the id field to the current value found in the native
-     * SSL_SESSION. It can change during the lifetime of the session
-     * because while a session is created during initial handshake,
-     * with handshake_cutthrough, the SSL_do_handshake may return
-     * before we have read the session ticket from the server side and
-     * therefore have computed no id based on the SHA of the ticket.
-     */
-    @Override
-    void resetId() {
-        id = NativeCrypto.SSL_SESSION_session_id(sslSessionNativePointer);
-    }
-
-    /**
-     * Get the session object in DER format. This allows saving the session
-     * data or sharing it with other processes.
-     */
-    public byte[] getEncoded() {
-        return NativeCrypto.i2d_SSL_SESSION(sslSessionNativePointer);
-    }
-
-    /**
-     * Gets the creation time of the SSL session.
-     * @return the session's creation time in milliseconds since the epoch
-     */
-    @Override
-    public long getCreationTime() {
-        if (creationTime == 0) {
-            creationTime = NativeCrypto.SSL_SESSION_get_time(sslSessionNativePointer);
-        }
-        return creationTime;
-    }
-
-    /**
-     * Returns the last time this concrete SSL session was accessed. Accessing
-     * here is to mean that a new connection with the same SSL context data was
-     * established.
-     *
-     * @return the session's last access time in milliseconds since the epoch
-     */
-    @Override
-    public long getLastAccessedTime() {
-        return (lastAccessedTime == 0) ? getCreationTime() : lastAccessedTime;
-    }
-
-    @Override
-    public void setLastAccessedTime(long accessTimeMillis) {
-        lastAccessedTime = accessTimeMillis;
-    }
-
-    @Override
-    protected X509Certificate[] getX509LocalCertificates() {
-        return localCertificates;
-    }
-
-    @Override
-    protected X509Certificate[] getX509PeerCertificates() throws SSLPeerUnverifiedException {
-        if (peerCertificates == null || peerCertificates.length == 0) {
-            throw new SSLPeerUnverifiedException("No peer certificates");
-        }
-        return peerCertificates;
-    }
-
-    /**
-     * The peer's host name used in this SSL session is returned. It is the host
-     * name of the client for the server; and that of the server for the client.
-     * It is not a reliable way to get a fully qualified host name: it is mainly
-     * used internally to implement links for a temporary cache of SSL sessions.
-     *
-     * @return the host name of the peer, or {@code null} if no information is
-     *         available.
-     */
-    @Override
-    public String getPeerHost() {
-        return peerHost;
-    }
-
-    /**
-     * Returns the peer's port number for the actual SSL session. It is the port
-     * number of the client for the server; and that of the server for the
-     * client. It is not a reliable way to get a peer's port number: it is
-     * mainly used internally to implement links for a temporary cache of SSL
-     * sessions.
-     *
-     * @return the peer's port number, or {@code -1} if no one is available.
-     */
-    @Override
-    public int getPeerPort() {
-        return peerPort;
-    }
-
-    /**
-     * Returns a string identifier of the crypto tools used in the actual SSL
-     * session. For example AES_256_WITH_MD5.
-     */
-    @Override
-    public String getCipherSuite() {
-        if (cipherSuite == null) {
-            String name = NativeCrypto.SSL_SESSION_cipher(sslSessionNativePointer);
-            cipherSuite = NativeCrypto.OPENSSL_TO_STANDARD_CIPHER_SUITES.get(name);
-            if (cipherSuite == null) {
-                cipherSuite = name;
-            }
-        }
-        return cipherSuite;
-    }
-
-    /**
-     * Returns the standard version name of the SSL protocol used in all
-     * connections pertaining to this SSL session.
-     */
-    @Override
-    public String getProtocol() {
-        if (protocol == null) {
-            protocol = NativeCrypto.SSL_SESSION_get_version(sslSessionNativePointer);
-        }
-        return protocol;
-    }
-
-    /**
-     * Returns the object which is bound to the the input parameter name.
-     * This name is a sort of link to the data of the SSL session's application
-     * layer, if any exists.
-     *
-     * @param name the name of the binding to find.
-     * @return the value bound to that name, or null if the binding does not
-     *         exist.
-     * @throws IllegalArgumentException if the argument is null.
-     */
-    @Override
-    public Object getValue(String name) {
-        if (name == null) {
-            throw new IllegalArgumentException("name == null");
-        }
-        return values.get(name);
-    }
-
-    /**
-     * Returns an array with the names (sort of links) of all the data
-     * objects of the application layer bound into the SSL session.
-     *
-     * @return a non-null (possibly empty) array of names of the data objects
-     *         bound to this SSL session.
-     */
-    @Override
-    public String[] getValueNames() {
-        return values.keySet().toArray(new String[values.size()]);
-    }
-
-    /**
-     * A link (name) with the specified value object of the SSL session's
-     * application layer data is created or replaced. If the new (or existing)
-     * value object implements the <code>SSLSessionBindingListener</code>
-     * interface, that object will be notified in due course.
-     *
-     * @param name the name of the link (no null are
-     *            accepted!)
-     * @param value data object that shall be bound to
-     *            name.
-     * @throws IllegalArgumentException if one or both argument(s) is null.
-     */
-    @Override
-    public void putValue(String name, Object value) {
-        if (name == null || value == null) {
-            throw new IllegalArgumentException("name == null || value == null");
-        }
-        Object old = values.put(name, value);
-        if (value instanceof SSLSessionBindingListener) {
-            ((SSLSessionBindingListener) value)
-                    .valueBound(new SSLSessionBindingEvent(this, name));
-        }
-        if (old instanceof SSLSessionBindingListener) {
-            ((SSLSessionBindingListener) old)
-                    .valueUnbound(new SSLSessionBindingEvent(this, name));
-        }
-    }
-
-    /**
-     * Removes a link (name) with the specified value object of the SSL
-     * session's application layer data.
-     *
-     * <p>If the value object implements the <code>SSLSessionBindingListener</code>
-     * interface, the object will receive a <code>valueUnbound</code> notification.
-     *
-     * @param name the name of the link (no null are
-     *            accepted!)
-     * @throws IllegalArgumentException if the argument is null.
-     */
-    @Override
-    public void removeValue(String name) {
-        if (name == null) {
-            throw new IllegalArgumentException("name == null");
-        }
-        Object old = values.remove(name);
-        if (old instanceof SSLSessionBindingListener) {
-            SSLSessionBindingListener listener = (SSLSessionBindingListener) old;
-            listener.valueUnbound(new SSLSessionBindingEvent(this, name));
-        }
-    }
-
-    /**
-     * Returns the name requested by the SNI extension.
-     */
-    @Override
-    public String getRequestedServerName() {
-        return NativeCrypto.get_SSL_SESSION_tlsext_hostname(sslSessionNativePointer);
-    }
-
-    /**
-     * Returns the OCSP stapled response.
-     */
-    @Override
-    public List<byte[]> getStatusResponses() {
-        if (peerCertificateOcspData == null) {
-            return Collections.<byte[]>emptyList();
-        }
-
-        return Collections.singletonList(peerCertificateOcspData.clone());
-    }
-
-    @Override
-    public byte[] getTlsSctData() {
-        if (peerTlsSctData == null) {
-            return null;
-        }
-        return peerTlsSctData.clone();
-    }
-
-    @Override
-    protected void finalize() throws Throwable {
-        try {
-            // The constructor can throw an exception if this object is constructed from invalid
-            // saved session data.
-            if (sslSessionNativePointer != 0) {
-                NativeCrypto.SSL_SESSION_free(sslSessionNativePointer);
-            }
-        } finally {
-            super.finalize();
-        }
-    }
-}
diff --git a/common/src/main/java/org/conscrypt/OpenSSLSignatureRawECDSA.java b/common/src/main/java/org/conscrypt/OpenSSLSignatureRawECDSA.java
index 86d18c0..df35818 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLSignatureRawECDSA.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLSignatureRawECDSA.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package org.conscrypt;
 
 import java.io.ByteArrayOutputStream;
@@ -7,9 +22,6 @@
 import java.security.PublicKey;
 import java.security.SignatureException;
 import java.security.SignatureSpi;
-import java.security.interfaces.RSAPrivateCrtKey;
-import java.security.interfaces.RSAPrivateKey;
-import java.security.interfaces.RSAPublicKey;
 
 /**
  * Implements the JDK Signature interface needed for RAW ECDSA signature
diff --git a/common/src/main/java/org/conscrypt/OpenSSLSocketFactoryImpl.java b/common/src/main/java/org/conscrypt/OpenSSLSocketFactoryImpl.java
index 1794d9b..3236caf 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLSocketFactoryImpl.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLSocketFactoryImpl.java
@@ -19,12 +19,16 @@
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.Socket;
+import java.net.SocketException;
 import java.net.UnknownHostException;
 import java.security.KeyManagementException;
 import javax.net.ssl.SSLSocketFactory;
 
 /**
  * An implementation of {@link SSLSocketFactory} based on BoringSSL.
+ *
+ * <p/>This name of this class cannot change in order to maintain backward-compatibility with GMS
+ * core {@code ProviderInstallerImpl}
  */
 final class OpenSSLSocketFactoryImpl extends SSLSocketFactory {
     private static boolean useEngineSocketByDefault = SSLUtils.USE_ENGINE_SOCKET_BY_DEFAULT;
@@ -81,18 +85,20 @@
             throw instantiationException;
         }
         if (useEngineSocket) {
-            return new OpenSSLEngineSocketImpl((SSLParametersImpl) sslParameters.clone());
+            return new ConscryptEngineSocket((SSLParametersImpl) sslParameters.clone());
         } else {
-            return new OpenSSLSocketImpl((SSLParametersImpl) sslParameters.clone());
+            return new ConscryptFileDescriptorSocket((SSLParametersImpl) sslParameters.clone());
         }
     }
 
     @Override
     public Socket createSocket(String hostname, int port) throws IOException, UnknownHostException {
         if (useEngineSocket) {
-            return new OpenSSLEngineSocketImpl(hostname, port, (SSLParametersImpl) sslParameters.clone());
+            return new ConscryptEngineSocket(
+                    hostname, port, (SSLParametersImpl) sslParameters.clone());
         } else {
-            return new OpenSSLSocketImpl(hostname, port, (SSLParametersImpl) sslParameters.clone());
+            return new ConscryptFileDescriptorSocket(
+                    hostname, port, (SSLParametersImpl) sslParameters.clone());
         }
     }
 
@@ -100,16 +106,10 @@
     public Socket createSocket(String hostname, int port, InetAddress localHost, int localPort)
             throws IOException, UnknownHostException {
         if (useEngineSocket) {
-            return new OpenSSLEngineSocketImpl(hostname,
-                    port,
-                    localHost,
-                    localPort,
+            return new ConscryptEngineSocket(hostname, port, localHost, localPort,
                     (SSLParametersImpl) sslParameters.clone());
         } else {
-            return new OpenSSLSocketImpl(hostname,
-                    port,
-                    localHost,
-                    localPort,
+            return new ConscryptFileDescriptorSocket(hostname, port, localHost, localPort,
                     (SSLParametersImpl) sslParameters.clone());
         }
     }
@@ -117,48 +117,46 @@
     @Override
     public Socket createSocket(InetAddress address, int port) throws IOException {
         if (useEngineSocket) {
-            return new OpenSSLEngineSocketImpl(address, port, (SSLParametersImpl) sslParameters.clone());
+            return new ConscryptEngineSocket(
+                    address, port, (SSLParametersImpl) sslParameters.clone());
         } else {
-            return new OpenSSLSocketImpl(address, port, (SSLParametersImpl) sslParameters.clone());
+            return new ConscryptFileDescriptorSocket(
+                    address, port, (SSLParametersImpl) sslParameters.clone());
         }
     }
 
     @Override
-    public Socket createSocket(InetAddress address,
-                               int port,
-                               InetAddress localAddress,
-                               int localPort)
-            throws IOException {
+    public Socket createSocket(InetAddress address, int port, InetAddress localAddress,
+            int localPort) throws IOException {
         if (useEngineSocket) {
-            return new OpenSSLEngineSocketImpl(address,
-                    port,
-                    localAddress,
-                    localPort,
+            return new ConscryptEngineSocket(address, port, localAddress, localPort,
                     (SSLParametersImpl) sslParameters.clone());
         } else {
-            return new OpenSSLSocketImpl(address,
-                    port,
-                    localAddress,
-                    localPort,
+            return new ConscryptFileDescriptorSocket(address, port, localAddress, localPort,
                     (SSLParametersImpl) sslParameters.clone());
         }
     }
 
     @Override
-    public Socket createSocket(Socket s, String hostname, int port, boolean autoClose)
+    public Socket createSocket(Socket socket, String hostname, int port, boolean autoClose)
             throws IOException {
-        if (hasFileDescriptor(s) && !useEngineSocket) {
-            return new OpenSSLSocketImplWrapper(
-                    s, hostname, port, autoClose, (SSLParametersImpl) sslParameters.clone());
+        Preconditions.checkNotNull(socket, "socket");
+        if (!socket.isConnected()) {
+            throw new SocketException("Socket is not connected.");
+        }
+
+        if (hasFileDescriptor(socket) && !useEngineSocket) {
+            return new ConscryptFileDescriptorSocket(
+                    socket, hostname, port, autoClose, (SSLParametersImpl) sslParameters.clone());
         } else {
-            return new OpenSSLEngineSocketImpl(
-                    s, hostname, port, autoClose, (SSLParametersImpl) sslParameters.clone());
+            return new ConscryptEngineSocket(
+                    socket, hostname, port, autoClose, (SSLParametersImpl) sslParameters.clone());
         }
     }
 
     private boolean hasFileDescriptor(Socket s) {
         try {
-            // If socket has a file descriptor we can use OpenSSLSocketImplWrapper directly
+            // If socket has a file descriptor we can use it directly
             // otherwise we need to use the engine.
             Platform.getFileDescriptor(s);
             return true;
diff --git a/common/src/main/java/org/conscrypt/OpenSSLSocketImpl.java b/common/src/main/java/org/conscrypt/OpenSSLSocketImpl.java
index b1dd921..b33f9e2 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLSocketImpl.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLSocketImpl.java
@@ -18,1325 +18,114 @@
 
 import java.io.FileDescriptor;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.net.InetAddress;
-import java.net.InetSocketAddress;
 import java.net.Socket;
-import java.net.SocketAddress;
 import java.net.SocketException;
-import java.security.InvalidKeyException;
 import java.security.PrivateKey;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.CertificateException;
-import java.security.interfaces.ECKey;
-import java.security.spec.ECParameterSpec;
-import java.util.ArrayList;
-import javax.crypto.SecretKey;
-import javax.net.ssl.HandshakeCompletedEvent;
-import javax.net.ssl.HandshakeCompletedListener;
 import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLParameters;
-import javax.net.ssl.SSLProtocolException;
 import javax.net.ssl.SSLSession;
-import javax.net.ssl.X509KeyManager;
-import javax.net.ssl.X509TrustManager;
-import javax.security.auth.x500.X500Principal;
 
 /**
- * Implementation of the class OpenSSLSocketImpl based on OpenSSL.
- * <p>
- * Extensions to SSLSocket include:
- * <ul>
- * <li>handshake timeout
- * <li>session tickets
- * <li>Server Name Indication
- * </ul>
+ * Public shim allowing us to stay backward-compatible with legacy applications which were using
+ * Conscrypt's extended socket API before the introduction of the {@link Conscrypt} class.
  *
  * @hide
  */
 @Internal
-public class OpenSSLSocketImpl
-        extends javax.net.ssl.SSLSocket
-        implements NativeCrypto.SSLHandshakeCallbacks, SSLParametersImpl.AliasChooser,
-        SSLParametersImpl.PSKCallbacks {
-
-    private static final boolean DBG_STATE = false;
-
-    /**
-     * Protects handshakeStarted and handshakeCompleted.
-     */
-    private final Object stateLock = new Object();
-
-    /**
-     * The {@link OpenSSLSocketImpl} object is constructed, but {@link #startHandshake()}
-     * has not yet been called.
-     */
-    private static final int STATE_NEW = 0;
-
-    /**
-     * {@link #startHandshake()} has been called at least once.
-     */
-    private static final int STATE_HANDSHAKE_STARTED = 1;
-
-    /**
-     * {@link HandshakeCompletedListener#handshakeCompleted} has been called, but
-     * {@link #startHandshake()} hasn't returned yet.
-     */
-    private static final int STATE_HANDSHAKE_COMPLETED = 2;
-
-    /**
-     * {@link #startHandshake()} has completed but
-     * {@link HandshakeCompletedListener#handshakeCompleted} hasn't been called. This is expected
-     * behaviour in cut-through mode, where SSL_do_handshake returns before the handshake is
-     * complete. We can now start writing data to the socket.
-     */
-    private static final int STATE_READY_HANDSHAKE_CUT_THROUGH = 3;
-
-    /**
-     * {@link #startHandshake()} has completed and
-     * {@link HandshakeCompletedListener#handshakeCompleted} has been called.
-     */
-    private static final int STATE_READY = 4;
-
-    /**
-     * {@link #close()} has been called at least once.
-     */
-    private static final int STATE_CLOSED = 5;
-
-    // @GuardedBy("stateLock");
-    private int state = STATE_NEW;
-
-    /**
-     * Protected by synchronizing on stateLock. Starts as 0, set by
-     * startHandshake, reset to 0 on close.
-     */
-    // @GuardedBy("stateLock");
-    private long sslNativePointer;
-
-    /**
-     * Protected by synchronizing on stateLock. Starts as null, set by
-     * getInputStream.
-     */
-    // @GuardedBy("stateLock");
-    private SSLInputStream is;
-
-    /**
-     * Protected by synchronizing on stateLock. Starts as null, set by
-     * getInputStream.
-     */
-    // @GuardedBy("stateLock");
-    private SSLOutputStream os;
-
-    private final Socket socket;
-    private final boolean autoClose;
-
-    /**
-     * The peer's DNS hostname if it was supplied during creation. Note that
-     * this may be a raw IP address, so it should be checked before use with
-     * extensions that don't use it like Server Name Indication (SNI).
-     */
-    private String peerHostname;
-
-    /**
-     * The peer's port if it was supplied during creation. Should only be set if
-     * {@link #peerHostname} is also set.
-     */
-    private final int peerPort;
-
-    private final SSLParametersImpl sslParameters;
-
-    /*
-     * A CloseGuard object on Android. On other platforms, this is nothing.
-     */
-    private final Object guard = Platform.closeGuardGet();
-
-    private ArrayList<HandshakeCompletedListener> listeners;
-
-    /**
-     * Private key for the TLS Channel ID extension. This field is client-side
-     * only. Set during startHandshake.
-     */
-    private OpenSSLKey channelIdPrivateKey;
-
-    /** Set during startHandshake. */
-    private AbstractOpenSSLSession sslSession;
-
-    /** Used during handshake callbacks. */
-    private AbstractOpenSSLSession handshakeSession;
-
-    /**
-     * Local cache of timeout to avoid getsockopt on every read and
-     * write for non-wrapped sockets. Note that
-     * OpenSSLSocketImplWrapper overrides setSoTimeout and
-     * getSoTimeout to delegate to the wrapped socket.
-     */
-    private int readTimeoutMilliseconds = 0;
-    private int writeTimeoutMilliseconds = 0;
-
-    private int handshakeTimeoutMilliseconds = -1;  // -1 = same as timeout; 0 = infinite
-
-    OpenSSLSocketImpl(SSLParametersImpl sslParameters) throws IOException {
-        this.socket = this;
-        this.peerHostname = null;
-        this.peerPort = -1;
-        this.autoClose = false;
-        this.sslParameters = sslParameters;
+public abstract class OpenSSLSocketImpl extends AbstractConscryptSocket {
+    OpenSSLSocketImpl() throws IOException {
     }
 
-    OpenSSLSocketImpl(String hostname, int port, SSLParametersImpl sslParameters)
-            throws IOException {
+    OpenSSLSocketImpl(String hostname, int port) throws IOException {
         super(hostname, port);
-        this.socket = this;
-        this.peerHostname = hostname;
-        this.peerPort = port;
-        this.autoClose = false;
-        this.sslParameters = sslParameters;
     }
 
-    OpenSSLSocketImpl(InetAddress address, int port, SSLParametersImpl sslParameters)
-            throws IOException {
+    OpenSSLSocketImpl(InetAddress address, int port) throws IOException {
         super(address, port);
-        this.socket = this;
-        this.peerHostname = null;
-        this.peerPort = -1;
-        this.autoClose = false;
-        this.sslParameters = sslParameters;
     }
 
-
-    OpenSSLSocketImpl(String hostname, int port,
-                                InetAddress clientAddress, int clientPort,
-                                SSLParametersImpl sslParameters) throws IOException {
+    OpenSSLSocketImpl(String hostname, int port, InetAddress clientAddress, int clientPort)
+        throws IOException {
         super(hostname, port, clientAddress, clientPort);
-        this.socket = this;
-        this.peerHostname = hostname;
-        this.peerPort = port;
-        this.autoClose = false;
-        this.sslParameters = sslParameters;
     }
 
-    OpenSSLSocketImpl(InetAddress address, int port,
-                                InetAddress clientAddress, int clientPort,
-                                SSLParametersImpl sslParameters) throws IOException {
+    OpenSSLSocketImpl(InetAddress address, int port, InetAddress clientAddress,
+        int clientPort)
+        throws IOException {
         super(address, port, clientAddress, clientPort);
-        this.socket = this;
-        this.peerHostname = null;
-        this.peerPort = -1;
-        this.autoClose = false;
-        this.sslParameters = sslParameters;
     }
 
-    /**
-     * Create an SSL socket that wraps another socket. Invoked by
-     * OpenSSLSocketImplWrapper constructor.
-     */
-    OpenSSLSocketImpl(Socket socket, String hostname, int port,
-            boolean autoClose, SSLParametersImpl sslParameters) throws IOException {
-        this.socket = socket;
-        this.peerHostname = hostname;
-        this.peerPort = port;
-        this.autoClose = autoClose;
-        this.sslParameters = sslParameters;
-
-        // this.timeout is not set intentionally.
-        // OpenSSLSocketImplWrapper.getSoTimeout will delegate timeout
-        // to wrapped socket
+    OpenSSLSocketImpl(Socket socket, String hostname, int port, boolean autoClose)
+        throws IOException {
+        super(socket, hostname, port, autoClose);
     }
 
     @Override
-    public void connect(SocketAddress endpoint) throws IOException {
-        connect(endpoint, 0);
-    }
-
-    /**
-     * Try to extract the peer's hostname if it's available from the endpoint address.
-     */
-    @Override
-    public void connect(SocketAddress endpoint, int timeout) throws IOException {
-        if (peerHostname == null && endpoint instanceof InetSocketAddress) {
-            peerHostname = Platform.getHostStringFromInetSocketAddress(
-                    (InetSocketAddress) endpoint);
-        }
-
-        super.connect(endpoint, timeout);
-    }
-
-    private void checkOpen() throws SocketException {
-        if (isClosed()) {
-            throw new SocketException("Socket is closed");
-        }
-    }
-
-    /**
-     * Starts a TLS/SSL handshake on this connection using some native methods
-     * from the OpenSSL library. It can negotiate new encryption keys, change
-     * cipher suites, or initiate a new session. The certificate chain is
-     * verified if the correspondent property in java.Security is set. All
-     * listeners are notified at the end of the TLS/SSL handshake.
-     */
-    @Override
-    public void startHandshake() throws IOException {
-        checkOpen();
-        synchronized (stateLock) {
-            if (state == STATE_NEW) {
-                state = STATE_HANDSHAKE_STARTED;
-            } else {
-                // We've either started the handshake already or have been closed.
-                // Do nothing in both cases.
-                return;
-            }
-        }
-
-        final boolean client = sslParameters.getUseClientMode();
-
-        sslNativePointer = 0;
-        boolean releaseResources = true;
-        try {
-            final AbstractSessionContext sessionContext = sslParameters.getSessionContext();
-            sslNativePointer = NativeCrypto.SSL_new(sessionContext.sslCtxNativePointer);
-            Platform.closeGuardOpen(guard, "close");
-
-            boolean enableSessionCreation = getEnableSessionCreation();
-            if (!enableSessionCreation) {
-                NativeCrypto.SSL_set_session_creation_enabled(sslNativePointer,
-                        enableSessionCreation);
-            }
-
-            // Allow servers to trigger renegotiation. Some inadvisable server
-            // configurations cause them to attempt to renegotiate during
-            // certain protocols.
-            NativeCrypto.SSL_accept_renegotiations(sslNativePointer);
-
-            if (client) {
-                NativeCrypto.SSL_set_connect_state(sslNativePointer);
-
-                // Configure OCSP and CT extensions for client
-                NativeCrypto.SSL_enable_ocsp_stapling(sslNativePointer);
-                if (sslParameters.isCTVerificationEnabled(getHostname())) {
-                    NativeCrypto.SSL_enable_signed_cert_timestamps(sslNativePointer);
-                }
-            } else {
-                NativeCrypto.SSL_set_accept_state(sslNativePointer);
-
-                // Configure OCSP for server
-                if (sslParameters.getOCSPResponse() != null) {
-                    NativeCrypto.SSL_enable_ocsp_stapling(sslNativePointer);
-                }
-            }
-
-            final AbstractOpenSSLSession sessionToReuse =
-                    sslParameters.getSessionToReuse(sslNativePointer, getHostnameOrIP(), getPort());
-            sslParameters.setSSLParameters(sslNativePointer, this, this, getHostname());
-            sslParameters.setCertificateValidation(sslNativePointer);
-            sslParameters.setTlsChannelId(sslNativePointer, channelIdPrivateKey);
-
-            // Temporarily use a different timeout for the handshake process
-            int savedReadTimeoutMilliseconds = getSoTimeout();
-            int savedWriteTimeoutMilliseconds = getSoWriteTimeout();
-            if (handshakeTimeoutMilliseconds >= 0) {
-                setSoTimeout(handshakeTimeoutMilliseconds);
-                setSoWriteTimeout(handshakeTimeoutMilliseconds);
-            }
-
-            synchronized (stateLock) {
-                if (state == STATE_CLOSED) {
-                    return;
-                }
-            }
-
-            long sslSessionNativePointer;
-            try {
-                NativeCrypto.SSL_do_handshake(
-                        sslNativePointer, Platform.getFileDescriptor(socket), this, getSoTimeout());
-                sslSessionNativePointer = NativeCrypto.SSL_get1_session(sslNativePointer);
-            } catch (CertificateException e) {
-                SSLHandshakeException wrapper = new SSLHandshakeException(e.getMessage());
-                wrapper.initCause(e);
-                throw wrapper;
-            } catch (SSLException e) {
-                // Swallow this exception if it's thrown as the result of an interruption.
-                //
-                // TODO: SSL_read and SSL_write return -1 when interrupted, but SSL_do_handshake
-                // will throw the last sslError that it saw before sslSelect, usually SSL_WANT_READ
-                // (or WANT_WRITE). Catching that exception here doesn't seem much worse than
-                // changing the native code to return a "special" native pointer value when that
-                // happens.
-                synchronized (stateLock) {
-                    if (state == STATE_CLOSED) {
-                        return;
-                    }
-                }
-
-                // Write CCS errors to EventLog
-                String message = e.getMessage();
-                // Must match error string of SSL_R_UNEXPECTED_CCS
-                if (message.contains("unexpected CCS")) {
-                    String logMessage = String.format("ssl_unexpected_ccs: host=%s",
-                            getHostnameOrIP());
-                    Platform.logEvent(logMessage);
-                }
-
-                throw e;
-            }
-
-            boolean handshakeCompleted = false;
-            synchronized (stateLock) {
-                if (state == STATE_HANDSHAKE_COMPLETED) {
-                    handshakeCompleted = true;
-                } else if (state == STATE_CLOSED) {
-                    return;
-                }
-            }
-
-            sslSession = sslParameters.setupSession(sslSessionNativePointer, sslNativePointer,
-                    sessionToReuse, getHostnameOrIP(), getPort(), handshakeCompleted);
-
-            // Restore the original timeout now that the handshake is complete
-            if (handshakeTimeoutMilliseconds >= 0) {
-                setSoTimeout(savedReadTimeoutMilliseconds);
-                setSoWriteTimeout(savedWriteTimeoutMilliseconds);
-            }
-
-            // if not, notifyHandshakeCompletedListeners later in handshakeCompleted() callback
-            if (handshakeCompleted) {
-                notifyHandshakeCompletedListeners();
-            }
-
-            synchronized (stateLock) {
-                releaseResources = (state == STATE_CLOSED);
-
-                if (state == STATE_HANDSHAKE_STARTED) {
-                    state = STATE_READY_HANDSHAKE_CUT_THROUGH;
-                } else if (state == STATE_HANDSHAKE_COMPLETED) {
-                    state = STATE_READY;
-                }
-
-                if (!releaseResources) {
-                    // Unblock threads that are waiting for our state to transition
-                    // into STATE_READY or STATE_READY_HANDSHAKE_CUT_THROUGH.
-                    stateLock.notifyAll();
-                }
-            }
-        } catch (SSLProtocolException e) {
-            throw (SSLHandshakeException) new SSLHandshakeException("Handshake failed")
-                    .initCause(e);
-        } finally {
-            // on exceptional exit, treat the socket as closed
-            if (releaseResources) {
-                synchronized (stateLock) {
-                    // Mark the socket as closed since we might have reached this as
-                    // a result on an exception thrown by the handshake process.
-                    //
-                    // The state will already be set to closed if we reach this as a result of
-                    // an early return or an interruption due to a concurrent call to close().
-                    state = STATE_CLOSED;
-                    stateLock.notifyAll();
-                }
-
-                try {
-                    shutdownAndFreeSslNative();
-                } catch (IOException ignored) {
-                    // Ignored.
-                }
-            }
-        }
-    }
-
-    /**
-     * Returns the hostname that was supplied during socket creation. No DNS resolution is
-     * attempted before returning the hostname.
-     */
     public String getHostname() {
-        return peerHostname;
-    }
-
-    /**
-     * For the purposes of an SSLSession, we want a way to represent the supplied hostname
-     * or the IP address in a textual representation. We do not want to perform reverse DNS
-     * lookups on this address.
-     */
-    public String getHostnameOrIP() {
-        if (peerHostname != null) {
-            return peerHostname;
-        }
-
-        InetAddress peerAddress = getInetAddress();
-        if (peerAddress != null) {
-            return peerAddress.getHostAddress();
-        }
-
-        return null;
+        return super.getHostname();
     }
 
     @Override
-    public int getPort() {
-        return peerPort == -1 ? super.getPort() : peerPort;
-    }
-
-    @Override
-    @SuppressWarnings("unused") // used by NativeCrypto.SSLHandshakeCallbacks / client_cert_cb
-    public void clientCertificateRequested(byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals)
-            throws CertificateEncodingException, SSLException {
-        sslParameters.chooseClientCertificate(keyTypeBytes, asn1DerEncodedPrincipals,
-                sslNativePointer, this);
-    }
-
-    @Override
-    @SuppressWarnings("unused") // used by native psk_client_callback
-    public int clientPSKKeyRequested(String identityHint, byte[] identity, byte[] key) {
-        return sslParameters.clientPSKKeyRequested(identityHint, identity, key, this);
-    }
-
-    @Override
-    @SuppressWarnings("unused") // used by native psk_server_callback
-    public int serverPSKKeyRequested(String identityHint, String identity, byte[] key) {
-        return sslParameters.serverPSKKeyRequested(identityHint, identity, key, this);
-    }
-
-    @Override
-    @SuppressWarnings("unused") // used by NativeCrypto.SSLHandshakeCallbacks / info_callback
-    public void onSSLStateChange(int type, int val) {
-        if (type != NativeConstants.SSL_CB_HANDSHAKE_DONE) {
-            return;
-        }
-
-        synchronized (stateLock) {
-            if (state == STATE_HANDSHAKE_STARTED) {
-                // If sslSession is null, the handshake was completed during
-                // the call to NativeCrypto.SSL_do_handshake and not during a
-                // later read operation. That means we do not need to fix up
-                // the SSLSession and session cache or notify
-                // HandshakeCompletedListeners, it will be done in
-                // startHandshake.
-
-                state = STATE_HANDSHAKE_COMPLETED;
-                return;
-            } else if (state == STATE_READY_HANDSHAKE_CUT_THROUGH) {
-                // We've returned from startHandshake, which means we've set a sslSession etc.
-                // we need to fix them up, which we'll do outside this lock.
-            } else if (state == STATE_CLOSED) {
-                // Someone called "close" but the handshake hasn't been interrupted yet.
-                return;
-            }
-        }
-
-        // reset session id from the native pointer and update the
-        // appropriate cache.
-        sslSession.resetId();
-        AbstractSessionContext sessionContext =
-            (sslParameters.getUseClientMode())
-            ? sslParameters.getClientSessionContext()
-                : sslParameters.getServerSessionContext();
-        sessionContext.putSession(sslSession);
-
-        // let listeners know we are finally done
-        notifyHandshakeCompletedListeners();
-
-        synchronized (stateLock) {
-            // Now that we've fixed up our state, we can tell waiting threads that
-            // we're ready.
-            state = STATE_READY;
-            // Notify all threads waiting for the handshake to complete.
-            stateLock.notifyAll();
-        }
-    }
-
-    void notifyHandshakeCompletedListeners() {
-        if (listeners != null && !listeners.isEmpty()) {
-            // notify the listeners
-            HandshakeCompletedEvent event =
-                new HandshakeCompletedEvent(this, sslSession);
-            for (HandshakeCompletedListener listener : listeners) {
-                try {
-                    listener.handshakeCompleted(event);
-                } catch (RuntimeException e) {
-                    // The RI runs the handlers in a separate thread,
-                    // which we do not. But we try to preserve their
-                    // behavior of logging a problem and not killing
-                    // the handshaking thread just because a listener
-                    // has a problem.
-                    Thread thread = Thread.currentThread();
-                    thread.getUncaughtExceptionHandler().uncaughtException(thread, e);
-                }
-            }
-        }
-    }
-
-    @SuppressWarnings("unused") // used by NativeCrypto.SSLHandshakeCallbacks
-    @Override
-    public void verifyCertificateChain(long[] certRefs, String authMethod)
-            throws CertificateException {
-        try {
-            X509TrustManager x509tm = sslParameters.getX509TrustManager();
-            if (x509tm == null) {
-                throw new CertificateException("No X.509 TrustManager");
-            }
-            if (certRefs == null || certRefs.length == 0) {
-                throw new SSLException("Peer sent no certificate");
-            }
-            OpenSSLX509Certificate[] peerCertChain =
-                    OpenSSLX509Certificate.createCertChain(certRefs);
-
-            byte[] ocspData = NativeCrypto.SSL_get_ocsp_response(sslNativePointer);
-            byte[] tlsSctData = NativeCrypto.SSL_get_signed_cert_timestamp_list(sslNativePointer);
-
-            // Used for verifyCertificateChain callback
-            handshakeSession = new OpenSSLSessionImpl(
-                    NativeCrypto.SSL_get1_session(sslNativePointer), null, peerCertChain, ocspData,
-                    tlsSctData, getHostnameOrIP(), getPort(), null);
-
-            boolean client = sslParameters.getUseClientMode();
-            if (client) {
-                Platform.checkServerTrusted(x509tm, peerCertChain, authMethod, this);
-            } else {
-                String authType = peerCertChain[0].getPublicKey().getAlgorithm();
-                Platform.checkClientTrusted(x509tm, peerCertChain, authType, this);
-            }
-        } catch (CertificateException e) {
-            throw e;
-        } catch (Exception e) {
-            throw new CertificateException(e);
-        } finally {
-            // Clear this before notifying handshake completed listeners
-            handshakeSession = null;
-        }
-    }
-
-    @Override
-    public InputStream getInputStream() throws IOException {
-        checkOpen();
-
-        InputStream returnVal;
-        synchronized (stateLock) {
-            if (state == STATE_CLOSED) {
-                throw new SocketException("Socket is closed.");
-            }
-
-            if (is == null) {
-                is = new SSLInputStream();
-            }
-
-            returnVal = is;
-        }
-
-        // Block waiting for a handshake without a lock held. It's possible that the socket
-        // is closed at this point. If that happens, we'll still return the input stream but
-        // all reads on it will throw.
-        waitForHandshake();
-        return returnVal;
-    }
-
-    @Override
-    public OutputStream getOutputStream() throws IOException {
-        checkOpen();
-
-        OutputStream returnVal;
-        synchronized (stateLock) {
-            if (state == STATE_CLOSED) {
-                throw new SocketException("Socket is closed.");
-            }
-
-            if (os == null) {
-                os = new SSLOutputStream();
-            }
-
-            returnVal = os;
-        }
-
-        // Block waiting for a handshake without a lock held. It's possible that the socket
-        // is closed at this point. If that happens, we'll still return the output stream but
-        // all writes on it will throw.
-        waitForHandshake();
-        return returnVal;
-    }
-
-    private void assertReadableOrWriteableState() {
-        if (state == STATE_READY || state == STATE_READY_HANDSHAKE_CUT_THROUGH) {
-            return;
-        }
-
-        throw new AssertionError("Invalid state: " + state);
-    }
-
-
-    private void waitForHandshake() throws IOException {
-        startHandshake();
-
-        synchronized (stateLock) {
-            while (state != STATE_READY &&
-                    state != STATE_READY_HANDSHAKE_CUT_THROUGH &&
-                    state != STATE_CLOSED) {
-                try {
-                    stateLock.wait();
-                } catch (InterruptedException e) {
-                    Thread.currentThread().interrupt();
-                    throw new IOException("Interrupted waiting for handshake", e);
-                }
-            }
-
-            if (state == STATE_CLOSED) {
-                throw new SocketException("Socket is closed");
-            }
-        }
-    }
-
-    /**
-     * This inner class provides input data stream functionality
-     * for the OpenSSL native implementation. It is used to
-     * read data received via SSL protocol.
-     */
-    private class SSLInputStream extends InputStream {
-        /**
-         * OpenSSL only lets one thread read at a time, so this is used to
-         * make sure we serialize callers of SSL_read. Thread is already
-         * expected to have completed handshaking.
-         */
-        private final Object readLock = new Object();
-
-        SSLInputStream() {
-        }
-
-        /**
-         * Reads one byte. If there is no data in the underlying buffer,
-         * this operation can block until the data will be
-         * available.
-         */
-        @Override
-        public int read() throws IOException {
-            byte[] buffer = new byte[1];
-            int result = read(buffer, 0, 1);
-            return (result != -1) ? buffer[0] & 0xff : -1;
-        }
-
-        /**
-         * Method acts as described in spec for superclass.
-         * @see java.io.InputStream#read(byte[],int,int)
-         */
-        @Override
-        public int read(byte[] buf, int offset, int byteCount) throws IOException {
-            Platform.blockGuardOnNetwork();
-
-            checkOpen();
-            ArrayUtils.checkOffsetAndCount(buf.length, offset, byteCount);
-            if (byteCount == 0) {
-                return 0;
-            }
-
-            synchronized (readLock) {
-                synchronized (stateLock) {
-                    if (state == STATE_CLOSED) {
-                        throw new SocketException("socket is closed");
-                    }
-
-                    if (DBG_STATE) assertReadableOrWriteableState();
-                }
-
-                return NativeCrypto.SSL_read(sslNativePointer, Platform.getFileDescriptor(socket),
-                        OpenSSLSocketImpl.this, buf, offset, byteCount, getSoTimeout());
-            }
-        }
-
-        void awaitPendingOps() {
-            if (DBG_STATE) {
-                synchronized (stateLock) {
-                    if (state != STATE_CLOSED) throw new AssertionError("State is: " + state);
-                }
-            }
-
-            synchronized (readLock) { }
-        }
-    }
-
-    /**
-     * This inner class provides output data stream functionality
-     * for the OpenSSL native implementation. It is used to
-     * write data according to the encryption parameters given in SSL context.
-     */
-    private class SSLOutputStream extends OutputStream {
-
-        /**
-         * OpenSSL only lets one thread write at a time, so this is used
-         * to make sure we serialize callers of SSL_write. Thread is
-         * already expected to have completed handshaking.
-         */
-        private final Object writeLock = new Object();
-
-        SSLOutputStream() {
-        }
-
-        /**
-         * Method acts as described in spec for superclass.
-         * @see java.io.OutputStream#write(int)
-         */
-        @Override
-        public void write(int oneByte) throws IOException {
-            byte[] buffer = new byte[1];
-            buffer[0] = (byte) (oneByte & 0xff);
-            write(buffer);
-        }
-
-        /**
-         * Method acts as described in spec for superclass.
-         * @see java.io.OutputStream#write(byte[],int,int)
-         */
-        @Override
-        public void write(byte[] buf, int offset, int byteCount) throws IOException {
-            Platform.blockGuardOnNetwork();
-            checkOpen();
-            ArrayUtils.checkOffsetAndCount(buf.length, offset, byteCount);
-            if (byteCount == 0) {
-                return;
-            }
-
-            synchronized (writeLock) {
-                synchronized (stateLock) {
-                    if (state == STATE_CLOSED) {
-                        throw new SocketException("socket is closed");
-                    }
-
-                    if (DBG_STATE) assertReadableOrWriteableState();
-                }
-
-                NativeCrypto.SSL_write(sslNativePointer, Platform.getFileDescriptor(socket),
-                        OpenSSLSocketImpl.this, buf, offset, byteCount, writeTimeoutMilliseconds);
-            }
-        }
-
-
-        void awaitPendingOps() {
-            if (DBG_STATE) {
-                synchronized (stateLock) {
-                    if (state != STATE_CLOSED) throw new AssertionError("State is: " + state);
-                }
-            }
-
-            synchronized (writeLock) { }
-        }
-    }
-
-
-    @Override
-    public SSLSession getSession() {
-        if (sslSession == null) {
-            boolean handshakeCompleted = false;
-            try {
-                if (isConnected()) {
-                    waitForHandshake();
-                    handshakeCompleted = true;
-                }
-            } catch (IOException e) {
-                // Fall through.
-            }
-
-            if (!handshakeCompleted) {
-                // return an invalid session with
-                // invalid cipher suite of "SSL_NULL_WITH_NULL_NULL"
-                return SSLNullSession.getNullSession();
-            }
-        }
-        return Platform.wrapSSLSession(sslSession);
-    }
-
-    /* @Override */
-    @SuppressWarnings("MissingOverride")  // For compilation with Java 6.
-    public SSLSession getHandshakeSession() {
-        return handshakeSession;
-    }
-
-    @Override
-    public void addHandshakeCompletedListener(
-            HandshakeCompletedListener listener) {
-        if (listener == null) {
-            throw new IllegalArgumentException("Provided listener is null");
-        }
-        if (listeners == null) {
-            listeners = new ArrayList<HandshakeCompletedListener>();
-        }
-        listeners.add(listener);
-    }
-
-    @Override
-    public void removeHandshakeCompletedListener(
-            HandshakeCompletedListener listener) {
-        if (listener == null) {
-            throw new IllegalArgumentException("Provided listener is null");
-        }
-        if (listeners == null) {
-            throw new IllegalArgumentException(
-                    "Provided listener is not registered");
-        }
-        if (!listeners.remove(listener)) {
-            throw new IllegalArgumentException(
-                    "Provided listener is not registered");
-        }
-    }
-
-    @Override
-    public boolean getEnableSessionCreation() {
-        return sslParameters.getEnableSessionCreation();
-    }
-
-    @Override
-    public void setEnableSessionCreation(boolean flag) {
-        sslParameters.setEnableSessionCreation(flag);
-    }
-
-    @Override
-    public String[] getSupportedCipherSuites() {
-        return NativeCrypto.getSupportedCipherSuites();
-    }
-
-    @Override
-    public String[] getEnabledCipherSuites() {
-        return sslParameters.getEnabledCipherSuites();
-    }
-
-    @Override
-    public void setEnabledCipherSuites(String[] suites) {
-        sslParameters.setEnabledCipherSuites(suites);
-    }
-
-    @Override
-    public String[] getSupportedProtocols() {
-        return NativeCrypto.getSupportedProtocols();
-    }
-
-    @Override
-    public String[] getEnabledProtocols() {
-        return sslParameters.getEnabledProtocols();
-    }
-
-    @Override
-    public void setEnabledProtocols(String[] protocols) {
-        sslParameters.setEnabledProtocols(protocols);
-    }
-
-    /**
-     * This method enables session ticket support.
-     *
-     * @param useSessionTickets True to enable session tickets
-     */
-    public void setUseSessionTickets(boolean useSessionTickets) {
-        sslParameters.setUseSessionTickets(useSessionTickets);
-    }
-
-    /**
-     * This method enables Server Name Indication
-     *
-     * @param hostname the desired SNI hostname, or null to disable
-     */
     public void setHostname(String hostname) {
-        sslParameters.setUseSni(hostname != null);
-        peerHostname = hostname;
-    }
-
-    /**
-     * Enables/disables TLS Channel ID for this server socket.
-     *
-     * <p>This method needs to be invoked before the handshake starts.
-     *
-     * @throws IllegalStateException if this is a client socket or if the handshake has already
-     *         started.
-     */
-    public void setChannelIdEnabled(boolean enabled) {
-        if (getUseClientMode()) {
-            throw new IllegalStateException("Client mode");
-        }
-
-        synchronized (stateLock) {
-            if (state != STATE_NEW) {
-                throw new IllegalStateException(
-                        "Could not enable/disable Channel ID after the initial handshake has"
-                                + " begun.");
-            }
-        }
-        sslParameters.channelIdEnabled = enabled;
-    }
-
-    /**
-     * Gets the TLS Channel ID for this server socket. Channel ID is only available once the
-     * handshake completes.
-     *
-     * @return channel ID or {@code null} if not available.
-     *
-     * @throws IllegalStateException if this is a client socket or if the handshake has not yet
-     *         completed.
-     * @throws SSLException if channel ID is available but could not be obtained.
-     */
-    public byte[] getChannelId() throws SSLException {
-        if (getUseClientMode()) {
-            throw new IllegalStateException("Client mode");
-        }
-
-        synchronized (stateLock) {
-            if (state != STATE_READY) {
-                throw new IllegalStateException(
-                        "Channel ID is only available after handshake completes");
-            }
-        }
-        return NativeCrypto.SSL_get_tls_channel_id(sslNativePointer);
-    }
-
-    /**
-     * Sets the {@link PrivateKey} to be used for TLS Channel ID by this client socket.
-     *
-     * <p>This method needs to be invoked before the handshake starts.
-     *
-     * @param privateKey private key (enables TLS Channel ID) or {@code null} for no key (disables
-     *        TLS Channel ID). The private key must be an Elliptic Curve (EC) key based on the NIST
-     *        P-256 curve (aka SECG secp256r1 or ANSI X9.62 prime256v1).
-     *
-     * @throws IllegalStateException if this is a server socket or if the handshake has already
-     *         started.
-     */
-    public void setChannelIdPrivateKey(PrivateKey privateKey) {
-        if (!getUseClientMode()) {
-            throw new IllegalStateException("Server mode");
-        }
-
-        synchronized (stateLock) {
-            if (state != STATE_NEW) {
-                throw new IllegalStateException(
-                        "Could not change Channel ID private key after the initial handshake has"
-                                + " begun.");
-            }
-        }
-
-        if (privateKey == null) {
-            sslParameters.channelIdEnabled = false;
-            channelIdPrivateKey = null;
-        } else {
-            sslParameters.channelIdEnabled = true;
-            try {
-                ECParameterSpec ecParams = null;
-                if (privateKey instanceof ECKey) {
-                    ecParams = ((ECKey) privateKey).getParams();
-                }
-                if (ecParams == null) {
-                    // Assume this is a P-256 key, as specified in the contract of this method.
-                    ecParams =
-                            OpenSSLECGroupContext.getCurveByName("prime256v1").getECParameterSpec();
-                }
-                channelIdPrivateKey =
-                        OpenSSLKey.fromECPrivateKeyForTLSStackOnly(privateKey, ecParams);
-            } catch (InvalidKeyException e) {
-                // Will have error in startHandshake
-            }
-        }
+        super.setHostname(hostname);
     }
 
     @Override
-    public boolean getUseClientMode() {
-        return sslParameters.getUseClientMode();
+    public String getHostnameOrIP() {
+        return super.getHostnameOrIP();
     }
 
     @Override
-    public void setUseClientMode(boolean mode) {
-        synchronized (stateLock) {
-            if (state != STATE_NEW) {
-                throw new IllegalArgumentException(
-                        "Could not change the mode after the initial handshake has begun.");
-            }
-        }
-        sslParameters.setUseClientMode(mode);
-    }
-
-    @Override
-    public boolean getWantClientAuth() {
-        return sslParameters.getWantClientAuth();
-    }
-
-    @Override
-    public boolean getNeedClientAuth() {
-        return sslParameters.getNeedClientAuth();
-    }
-
-    @Override
-    public void setNeedClientAuth(boolean need) {
-        sslParameters.setNeedClientAuth(need);
-    }
-
-    @Override
-    public void setWantClientAuth(boolean want) {
-        sslParameters.setWantClientAuth(want);
-    }
-
-    @Override
-    public void sendUrgentData(int data) throws IOException {
-        throw new SocketException("Method sendUrgentData() is not supported.");
-    }
-
-    @Override
-    public void setOOBInline(boolean on) throws SocketException {
-        throw new SocketException("Methods sendUrgentData, setOOBInline are not supported.");
-    }
-
-    @Override
-    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
-    public void setSoTimeout(int readTimeoutMilliseconds) throws SocketException {
-        if (socket != this) {
-            socket.setSoTimeout(readTimeoutMilliseconds);
-        } else {
-            super.setSoTimeout(readTimeoutMilliseconds);
-        }
-
-        this.readTimeoutMilliseconds = readTimeoutMilliseconds;
-    }
-
-    @Override
-    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
-    public int getSoTimeout() throws SocketException {
-        return readTimeoutMilliseconds;
-    }
-
-    /**
-     * Note write timeouts are not part of the javax.net.ssl.SSLSocket API
-     */
-    public void setSoWriteTimeout(int writeTimeoutMilliseconds) throws SocketException {
-        this.writeTimeoutMilliseconds = writeTimeoutMilliseconds;
-
-        Platform.setSocketWriteTimeout(this, writeTimeoutMilliseconds);
-    }
-
-    /**
-     * Note write timeouts are not part of the javax.net.ssl.SSLSocket API
-     */
-    public int getSoWriteTimeout() throws SocketException {
-        return writeTimeoutMilliseconds;
-    }
-
-    /**
-     * Set the handshake timeout on this socket.  This timeout is specified in
-     * milliseconds and will be used only during the handshake process.
-     */
-    public void setHandshakeTimeout(int handshakeTimeoutMilliseconds) throws SocketException {
-        this.handshakeTimeoutMilliseconds = handshakeTimeoutMilliseconds;
-    }
-
-    @Override
-    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
-    public void close() throws IOException {
-        // TODO: Close SSL sockets using a background thread so they close gracefully.
-
-        SSLInputStream sslInputStream;
-        SSLOutputStream sslOutputStream;
-
-        synchronized (stateLock) {
-            if (state == STATE_CLOSED) {
-                // close() has already been called, so do nothing and return.
-                return;
-            }
-
-            int oldState = state;
-            state = STATE_CLOSED;
-
-            if (oldState == STATE_NEW) {
-                // The handshake hasn't been started yet, so there's no OpenSSL related
-                // state to clean up. We still need to close the underlying socket if
-                // we're wrapping it and were asked to autoClose.
-                closeUnderlyingSocket();
-
-                stateLock.notifyAll();
-                return;
-            }
-
-            if (oldState != STATE_READY && oldState != STATE_READY_HANDSHAKE_CUT_THROUGH) {
-                // If we're in these states, we still haven't returned from startHandshake.
-                // We call SSL_interrupt so that we can interrupt SSL_do_handshake and then
-                // set the state to STATE_CLOSED. startHandshake will handle all cleanup
-                // after SSL_do_handshake returns, so we don't have anything to do here.
-                NativeCrypto.SSL_interrupt(sslNativePointer);
-
-                stateLock.notifyAll();
-                return;
-            }
-
-            stateLock.notifyAll();
-            // We've already returned from startHandshake, so we potentially have
-            // input and output streams to clean up.
-            sslInputStream = is;
-            sslOutputStream = os;
-        }
-
-        // Don't bother interrupting unless we have something to interrupt.
-        if (sslInputStream != null || sslOutputStream != null) {
-            NativeCrypto.SSL_interrupt(sslNativePointer);
-        }
-
-        // Wait for the input and output streams to finish any reads they have in
-        // progress. If there are no reads in progress at this point, future reads will
-        // throw because state == STATE_CLOSED
-        if (sslInputStream != null) {
-            sslInputStream.awaitPendingOps();
-        }
-        if (sslOutputStream != null) {
-            sslOutputStream.awaitPendingOps();
-        }
-
-        shutdownAndFreeSslNative();
-    }
-
-    private void shutdownAndFreeSslNative() throws IOException {
-        try {
-            Platform.blockGuardOnNetwork();
-            NativeCrypto.SSL_shutdown(sslNativePointer, Platform.getFileDescriptor(socket),
-                    this);
-        } catch (IOException ignored) {
-            /*
-            * Note that although close() can throw
-            * IOException, the RI does not throw if there
-            * is problem sending a "close notify" which
-            * can happen if the underlying socket is closed.
-            */
-        } finally {
-            free();
-            closeUnderlyingSocket();
-        }
-    }
-
-    private void closeUnderlyingSocket() throws IOException {
-        if (socket != this) {
-            if (autoClose && !socket.isClosed()) {
-                socket.close();
-            }
-        } else {
-            if (!super.isClosed()) {
-                super.close();
-            }
-        }
-    }
-
-    private void free() {
-        if (sslNativePointer == 0) {
-            return;
-        }
-        NativeCrypto.SSL_free(sslNativePointer);
-        sslNativePointer = 0;
-        Platform.closeGuardClose(guard);
-    }
-
-    @Override
-    protected void finalize() throws Throwable {
-        try {
-            /*
-             * Just worry about our own state. Notably we do not try and
-             * close anything. The SocketImpl, either our own
-             * PlainSocketImpl, or the Socket we are wrapping, will do
-             * that. This might mean we do not properly SSL_shutdown, but
-             * if you want to do that, properly close the socket yourself.
-             *
-             * The reason why we don't try to SSL_shutdown, is that there
-             * can be a race between finalizers where the PlainSocketImpl
-             * finalizer runs first and closes the socket. However, in the
-             * meanwhile, the underlying file descriptor could be reused
-             * for another purpose. If we call SSL_shutdown, the
-             * underlying socket BIOs still have the old file descriptor
-             * and will write the close notify to some unsuspecting
-             * reader.
-             */
-            if (guard != null) {
-                Platform.closeGuardWarnIfOpen(guard);
-            }
-            free();
-        } finally {
-            super.finalize();
-        }
-    }
-
-    /* @Override */
     public FileDescriptor getFileDescriptor$() {
-        if (socket == this) {
-            return Platform.getFileDescriptorFromSSLSocket(this);
-        } else {
-            return Platform.getFileDescriptor(socket);
-        }
-    }
-
-    /**
-     * Returns null always for backward compatibility.
-     */
-    public byte[] getNpnSelectedProtocol() {
-        return null;
-    }
-
-    /**
-     * Returns the protocol agreed upon by client and server, or {@code null} if
-     * no protocol was agreed upon.
-     */
-    public byte[] getAlpnSelectedProtocol() {
-        return NativeCrypto.SSL_get0_alpn_selected(sslNativePointer);
-    }
-
-    /**
-     * This method does nothing and is kept for backward compatibility.
-     */
-    public void setNpnProtocols(byte[] npnProtocols) {
-    }
-
-    /**
-     * Sets the list of ALPN protocols. This method internally converts the protocols to their
-     * wire-format form.
-     *
-     * @param alpnProtocols the list of ALPN protocols
-     * @see #setAlpnProtocols(byte[])
-     */
-    public void setAlpnProtocols(String[] alpnProtocols) {
-        sslParameters.setAlpnProtocols(alpnProtocols);
-    }
-
-    /**
-     * Alternate version of {@link #setAlpnProtocols(String[])} that directly sets the list of
-     * ALPN in the wire-format form used by BoringSSL (length-prefixed 8-bit strings).
-     * Requires that all strings be encoded with US-ASCII.
-     *
-     * @param alpnProtocols the encoded form of the ALPN protocol list
-     * @see #setAlpnProtocols(String[])
-     */
-    public void setAlpnProtocols(byte[] alpnProtocols) {
-        sslParameters.setAlpnProtocols(alpnProtocols);
+        return super.getFileDescriptor$();
     }
 
     @Override
-    public SSLParameters getSSLParameters() {
-        SSLParameters params = super.getSSLParameters();
-        Platform.getSSLParameters(params, sslParameters, this);
-        return params;
+    public void setSoWriteTimeout(int writeTimeoutMilliseconds) throws SocketException {
+        super.setSoWriteTimeout(writeTimeoutMilliseconds);
     }
 
     @Override
-    public void setSSLParameters(SSLParameters p) {
-        super.setSSLParameters(p);
-        Platform.setSSLParameters(p, sslParameters, this);
+    public int getSoWriteTimeout() throws SocketException {
+        return super.getSoWriteTimeout();
     }
 
     @Override
-    public String chooseServerAlias(X509KeyManager keyManager, String keyType) {
-        return keyManager.chooseServerAlias(keyType, null, this);
+    public void setHandshakeTimeout(int handshakeTimeoutMilliseconds) throws SocketException {
+        super.setHandshakeTimeout(handshakeTimeoutMilliseconds);
     }
 
     @Override
-    public String chooseClientAlias(X509KeyManager keyManager, X500Principal[] issuers,
-            String[] keyTypes) {
-        return keyManager.chooseClientAlias(keyTypes, null, this);
+    public abstract SSLSession getHandshakeSession();
+
+    @Override
+    public abstract void setUseSessionTickets(boolean useSessionTickets);
+
+    @Override
+    public abstract void setChannelIdEnabled(boolean enabled);
+
+    @Override
+    public abstract byte[] getChannelId() throws SSLException;
+
+    @Override
+    public abstract void setChannelIdPrivateKey(PrivateKey privateKey);
+
+    @Override
+    public final byte[] getNpnSelectedProtocol() {
+        return super.getNpnSelectedProtocol();
     }
 
     @Override
-    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
-    public String chooseServerPSKIdentityHint(PSKKeyManager keyManager) {
-        return keyManager.chooseServerKeyIdentityHint(this);
+    public final void setNpnProtocols(byte[] npnProtocols) {
+        super.setNpnProtocols(npnProtocols);
     }
 
     @Override
-    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
-    public String chooseClientPSKIdentity(PSKKeyManager keyManager, String identityHint) {
-        return keyManager.chooseClientKeyIdentity(identityHint, this);
-    }
+    public abstract byte[] getAlpnSelectedProtocol();
 
     @Override
-    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
-    public SecretKey getPSKKey(PSKKeyManager keyManager, String identityHint, String identity) {
-        return keyManager.getKey(identityHint, identity, this);
-    }
+    public abstract void setAlpnProtocols(String[] alpnProtocols);
+
+    @Override
+    public abstract void setAlpnProtocols(byte[] alpnProtocols);
 }
diff --git a/common/src/main/java/org/conscrypt/OpenSSLSocketImplWrapper.java b/common/src/main/java/org/conscrypt/OpenSSLSocketImplWrapper.java
deleted file mode 100644
index 848ef7f..0000000
--- a/common/src/main/java/org/conscrypt/OpenSSLSocketImplWrapper.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package org.conscrypt;
-
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.Socket;
-import java.net.SocketAddress;
-import java.net.SocketException;
-
-/**
- * This class wraps the SSL functionality over an existing connected socket.
- */
-class OpenSSLSocketImplWrapper extends OpenSSLSocketImpl {
-
-    private Socket socket;
-
-    OpenSSLSocketImplWrapper(Socket socket, String hostname, int port,
-            boolean autoClose, SSLParametersImpl sslParameters) throws IOException {
-        super(socket, hostname, port, autoClose, sslParameters);
-        if (!socket.isConnected()) {
-            throw new SocketException("Socket is not connected.");
-        }
-        this.socket = socket;
-    }
-
-    @Override
-    public void connect(SocketAddress sockaddr, int timeout)
-        throws IOException {
-        socket.connect(sockaddr, timeout);
-    }
-
-    @Override
-    public void connect(SocketAddress sockaddr) throws IOException {
-        socket.connect(sockaddr);
-    }
-
-    @Override
-    public void bind(SocketAddress sockaddr) throws IOException {
-        socket.bind(sockaddr);
-    }
-
-    @Override
-    public SocketAddress getRemoteSocketAddress() {
-        return socket.getRemoteSocketAddress();
-    }
-
-    @Override
-    public SocketAddress getLocalSocketAddress() {
-        return socket.getLocalSocketAddress();
-    }
-
-    @Override
-    public InetAddress getLocalAddress() {
-        return socket.getLocalAddress();
-    }
-
-    @Override
-    public InetAddress getInetAddress() {
-        return socket.getInetAddress();
-    }
-
-    @Override
-    public String toString() {
-        return "SSL socket over " + socket.toString();
-    }
-
-    @Override
-    public void setSoLinger(boolean on, int linger) throws SocketException {
-        socket.setSoLinger(on, linger);
-    }
-
-    @Override
-    public void setTcpNoDelay(boolean on) throws SocketException {
-        socket.setTcpNoDelay(on);
-    }
-
-    @Override
-    public void setReuseAddress(boolean on) throws SocketException {
-        socket.setReuseAddress(on);
-    }
-
-    @Override
-    public void setKeepAlive(boolean on) throws SocketException {
-        socket.setKeepAlive(on);
-    }
-
-    @Override
-    public void setTrafficClass(int tos) throws SocketException {
-        socket.setTrafficClass(tos);
-    }
-
-    @Override
-    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
-    public void setSendBufferSize(int size) throws SocketException {
-        socket.setSendBufferSize(size);
-    }
-
-    @Override
-    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
-    public void setReceiveBufferSize(int size) throws SocketException {
-        socket.setReceiveBufferSize(size);
-    }
-
-    @Override
-    public boolean getTcpNoDelay() throws SocketException {
-        return socket.getTcpNoDelay();
-    }
-
-    @Override
-    public boolean getReuseAddress() throws SocketException {
-        return socket.getReuseAddress();
-    }
-
-    @Override
-    public boolean getOOBInline() throws SocketException {
-        return socket.getOOBInline();
-    }
-
-    @Override
-    public boolean getKeepAlive() throws SocketException {
-        return socket.getKeepAlive();
-    }
-
-    @Override
-    public int getTrafficClass() throws SocketException {
-        return socket.getTrafficClass();
-    }
-
-    @Override
-    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
-    public int getSoTimeout() throws SocketException {
-        return socket.getSoTimeout();
-    }
-
-    @Override
-    public int getSoLinger() throws SocketException {
-        return socket.getSoLinger();
-    }
-
-    @Override
-    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
-    public int getSendBufferSize() throws SocketException {
-        return socket.getSendBufferSize();
-    }
-
-    @Override
-    @SuppressWarnings("UnsynchronizedOverridesSynchronized")
-    public int getReceiveBufferSize() throws SocketException {
-        return socket.getReceiveBufferSize();
-    }
-
-    @Override
-    public boolean isConnected() {
-        return socket.isConnected();
-    }
-
-    @Override
-    public boolean isClosed() {
-        return socket.isClosed();
-    }
-
-    @Override
-    public boolean isBound() {
-        return socket.isBound();
-    }
-
-    @Override
-    public boolean isOutputShutdown() {
-        return socket.isOutputShutdown();
-    }
-
-    @Override
-    public boolean isInputShutdown() {
-        return socket.isInputShutdown();
-    }
-
-    @Override
-    public int getPort() {
-        return socket.getPort();
-    }
-
-    @Override
-    public int getLocalPort() {
-        return socket.getLocalPort();
-    }
-}
diff --git a/common/src/main/java/org/conscrypt/PeerInfoProvider.java b/common/src/main/java/org/conscrypt/PeerInfoProvider.java
new file mode 100644
index 0000000..e70f0cd
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/PeerInfoProvider.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.conscrypt;
+
+/**
+ * A provider for the peer host and port information.
+ */
+abstract class PeerInfoProvider {
+    private static final PeerInfoProvider NULL_PEER_INFO_PROVIDER = new PeerInfoProvider() {
+        @Override
+        String getHostname() {
+            return null;
+        }
+
+        @Override
+        public String getHostnameOrIP() {
+            return null;
+        }
+
+        @Override
+        public int getPort() {
+            return -1;
+        }
+    };
+
+    /**
+     * Returns the hostname supplied during engine/socket creation. No DNS resolution is
+     * attempted before returning the hostname.
+     */
+    abstract String getHostname();
+
+    /**
+     * This method attempts to create a textual representation of the peer host or IP. Does
+     * not perform a reverse DNS lookup. This is typically used during session creation.
+     */
+    abstract String getHostnameOrIP();
+
+    /**
+     * Gets the port of the peer.
+     */
+    abstract int getPort();
+
+    static PeerInfoProvider nullProvider() {
+        return NULL_PEER_INFO_PROVIDER;
+    }
+
+    static PeerInfoProvider forHostAndPort(final String host, final int port) {
+        return new PeerInfoProvider() {
+            @Override
+            String getHostname() {
+                return host;
+            }
+
+            @Override
+            public String getHostnameOrIP() {
+                return host;
+            }
+
+            @Override
+            public int getPort() {
+                return port;
+            }
+        };
+    }
+}
diff --git a/common/src/main/java/org/conscrypt/Preconditions.java b/common/src/main/java/org/conscrypt/Preconditions.java
new file mode 100644
index 0000000..39fe712
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/Preconditions.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+/**
+ * Static convenience methods that help a method or constructor check whether it was invoked
+ * correctly (that is, whether its <i>preconditions</i> were met).
+ */
+final class Preconditions {
+    private Preconditions() {}
+
+    /**
+     * Ensures that an object reference passed as a parameter to the calling method is not null.
+     *
+     * @param reference an object reference
+     * @param errorMessage the exception message to use if the check fails.
+     * @return the non-null reference that was validated
+     * @throws NullPointerException if {@code reference} is null
+     */
+    static <T> T checkNotNull(T reference, String errorMessage) {
+        if (reference == null) {
+            throw new NullPointerException(errorMessage);
+        }
+        return reference;
+    }
+
+    /**
+     * Ensures the truth of an expression involving one or more parameters to the calling method.
+     *
+     * @param condition to condition to be tested
+     * @param errorMessage the exception message to use if the check fails.
+     * @throws IllegalArgumentException if the condition is {@code false}
+     */
+    static void checkArgument(boolean condition, String errorMessage) {
+        if (!condition) {
+            throw new IllegalArgumentException(errorMessage);
+        }
+    }
+
+    /**
+     * Ensures the truth of an expression involving one or more parameters to the calling method.
+     *
+     * @param condition to condition to be tested
+     * @param errorMessageTemplate the format string to be passed to {@link String#format(String,
+     * Object...)}
+     * @param arg the format argument to be passed to {@link String#format(String, Object...)}
+     * @throws IllegalArgumentException if the condition is {@code false}
+     */
+    static void checkArgument(boolean condition, String errorMessageTemplate, Object arg) {
+        if (!condition) {
+            throw new IllegalArgumentException(String.format(errorMessageTemplate, arg));
+        }
+    }
+
+    /**
+     * Ensures that {@code start} and {@code end} specify a valid <i>positions</i> in an array, list
+     * or string of size {@code size}, and are in order. A position index may range from zero to
+     * {@code size}, inclusive.
+     *
+     * @param start a user-supplied index identifying a starting position in an array, list or string
+     * @param end a user-supplied index identifying a ending position in an array, list or string
+     * @param size the size of that array, list or string
+     * @throws IndexOutOfBoundsException if either index is negative or is greater than {@code size},
+     *     or if {@code end} is less than {@code start}
+     * @throws IllegalArgumentException if {@code size} is negative
+     */
+    static void checkPositionIndexes(int start, int end, int size) {
+        // Carefully optimized for execution by hotspot (explanatory comment above)
+        if (start < 0 || end < start || end > size) {
+            throw new IndexOutOfBoundsException(badPositionIndexes(start, end, size));
+        }
+    }
+
+    private static String badPositionIndexes(int start, int end, int size) {
+        if (start < 0 || start > size) {
+            return badPositionIndex(start, size, "start index");
+        }
+        if (end < 0 || end > size) {
+            return badPositionIndex(end, size, "end index");
+        }
+        // end < start
+        return String.format("end index (%s) must not be less than start index (%s)", end, start);
+    }
+
+    private static String badPositionIndex(int index, int size, String desc) {
+        if (index < 0) {
+            return String.format("%s (%s) must not be negative", desc, index);
+        } else if (size < 0) {
+            throw new IllegalArgumentException("negative size: " + size);
+        } else { // index > size
+            return String.format("%s (%s) must not be greater than size (%s)", desc, index, size);
+        }
+    }
+}
diff --git a/common/src/main/java/org/conscrypt/SSLNullSession.java b/common/src/main/java/org/conscrypt/SSLNullSession.java
index 88a0a9c..93650d6 100644
--- a/common/src/main/java/org/conscrypt/SSLNullSession.java
+++ b/common/src/main/java/org/conscrypt/SSLNullSession.java
@@ -28,6 +28,7 @@
  * SSLSocket#getSession()} before {@link SSLSocket#startHandshake()} is called.
  */
 final class SSLNullSession implements SSLSession, Cloneable {
+    static final String INVALID_CIPHER = "SSL_NULL_WITH_NULL_NULL";
 
     /*
      * Holds default instances so class preloading doesn't create an instance of
@@ -46,7 +47,11 @@
         return DefaultHolder.NULL_SESSION;
     }
 
-    SSLNullSession() {
+    static boolean isNullSession(SSLSession session) {
+        return session == DefaultHolder.NULL_SESSION;
+    }
+
+    private SSLNullSession() {
         creationTime = System.currentTimeMillis();
         lastAccessedTime = creationTime;
     }
@@ -58,7 +63,7 @@
 
     @Override
     public String getCipherSuite() {
-        return "SSL_NULL_WITH_NULL_NULL";
+        return INVALID_CIPHER;
     }
 
     @Override
diff --git a/common/src/main/java/org/conscrypt/SSLParametersImpl.java b/common/src/main/java/org/conscrypt/SSLParametersImpl.java
index 939401e..33e4c78 100644
--- a/common/src/main/java/org/conscrypt/SSLParametersImpl.java
+++ b/common/src/main/java/org/conscrypt/SSLParametersImpl.java
@@ -17,29 +17,17 @@
 
 package org.conscrypt;
 
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.security.InvalidKeyException;
 import java.security.KeyManagementException;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
 import java.security.SecureRandom;
 import java.security.UnrecoverableKeyException;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
 import javax.crypto.SecretKey;
 import javax.net.ssl.KeyManager;
 import javax.net.ssl.KeyManagerFactory;
-import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLSession;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.TrustManagerFactory;
 import javax.net.ssl.X509ExtendedKeyManager;
@@ -61,8 +49,6 @@
     private static volatile X509KeyManager defaultX509KeyManager;
     // default source of X.509 certificate based authentication trust decisions
     private static volatile X509TrustManager defaultX509TrustManager;
-    // default source of random numbers
-    private static volatile SecureRandom defaultSecureRandom;
     // default SSL parameters
     private static volatile SSLParametersImpl defaultParameters;
 
@@ -83,11 +69,11 @@
     private SecureRandom secureRandom;
 
     // protocols enabled for SSL connection
-    private String[] enabledProtocols;
+    String[] enabledProtocols;
     // set to indicate when obsolete protocols are filtered
-    private boolean isEnabledProtocolsFiltered;
+    boolean isEnabledProtocolsFiltered;
     // cipher suites enabled for SSL connection
-    private String[] enabledCipherSuites;
+    String[] enabledCipherSuites;
 
     // if the peer with this parameters tuned to work in client mode
     private boolean client_mode = true;
@@ -106,11 +92,11 @@
     private boolean ctVerificationEnabled;
 
     // server-side only. SCT and OCSP data to send to clients which request it
-    private byte[] sctExtension;
-    private byte[] ocspResponse;
+    byte[] sctExtension;
+    byte[] ocspResponse;
 
-    private byte[] alpnProtocols;
-    private boolean useSessionTickets;
+    byte[] alpnProtocols;
+    boolean useSessionTickets;
     private Boolean useSni;
 
     /**
@@ -189,13 +175,6 @@
     }
 
     /**
-     * @return server session context
-     */
-    ServerSessionContext getServerSessionContext() {
-        return serverSessionContext;
-    }
-
-    /**
      * @return client session context
      */
     ClientSessionContext getClientSessionContext() {
@@ -225,29 +204,6 @@
     }
 
     /**
-     * @return secure random
-     */
-    SecureRandom getSecureRandom() {
-        if (secureRandom != null) {
-            return secureRandom;
-        }
-        SecureRandom result = defaultSecureRandom;
-        if (result == null) {
-            // single-check idiom
-            defaultSecureRandom = result = new SecureRandom();
-        }
-        secureRandom = result;
-        return secureRandom;
-    }
-
-    /**
-     * @return the secure random member reference, even it is null
-     */
-    SecureRandom getSecureRandomMember() {
-        return secureRandom;
-    }
-
-    /**
      * @return the names of enabled cipher suites
      */
     String[] getEnabledCipherSuites() {
@@ -308,6 +264,10 @@
         this.alpnProtocols = alpnProtocols;
     }
 
+    byte[] getAlpnProtocols() {
+        return alpnProtocols;
+    }
+
     /**
      * Tunes the peer holding this parameters to work in client mode.
      * @param   mode if the peer is configured to work in client mode
@@ -383,7 +343,7 @@
      * extension Server Name Indication (SNI).
      */
     void setUseSni(boolean flag) {
-        useSni = Boolean.valueOf(flag);
+        useSni = flag;
     }
 
     /**
@@ -391,17 +351,26 @@
      * extension Server Name Indication (SNI).
      */
     boolean getUseSni() {
-        return useSni != null ? useSni.booleanValue() : isSniEnabledByDefault();
+        return useSni != null ? useSni : isSniEnabledByDefault();
     }
 
+    /**
+     * For testing only.
+     */
     void setCTVerificationEnabled(boolean enabled) {
         ctVerificationEnabled = enabled;
     }
 
+    /**
+     * For testing only.
+     */
     void setSCTExtension(byte[] extension) {
         sctExtension = extension;
     }
 
+    /**
+     * For testing only.
+     */
     void setOCSPResponse(byte[] response) {
         ocspResponse = response;
     }
@@ -410,111 +379,6 @@
         return ocspResponse;
     }
 
-    static byte[][] encodeIssuerX509Principals(X509Certificate[] certificates)
-            throws CertificateEncodingException {
-        byte[][] principalBytes = new byte[certificates.length][];
-        for (int i = 0; i < certificates.length; i++) {
-            principalBytes[i] = certificates[i].getIssuerX500Principal().getEncoded();
-        }
-        return principalBytes;
-    }
-
-    AbstractOpenSSLSession getSessionToReuse(long sslNativePointer, String hostname, int port)
-            throws SSLException {
-        OpenSSLSessionImpl sessionToReuse = null;
-
-        if (client_mode) {
-            // look for client session to reuse
-            SSLSession cachedSession = getCachedClientSession(clientSessionContext, hostname, port);
-            if (cachedSession != null) {
-                // The native pointer is used here, so we have to make sure it's not a delegate
-                // SSLSession class.
-                cachedSession = Platform.unwrapSSLSession(cachedSession);
-
-                if (cachedSession instanceof OpenSSLSessionImpl) {
-                    sessionToReuse = (OpenSSLSessionImpl) cachedSession;
-                    NativeCrypto.SSL_set_session(sslNativePointer,
-                            sessionToReuse.sslSessionNativePointer);
-                }
-            }
-        }
-
-        return sessionToReuse;
-    }
-
-    void setTlsChannelId(long sslNativePointer, OpenSSLKey channelIdPrivateKey)
-            throws SSLHandshakeException, SSLException {
-        // TLS Channel ID
-        if (channelIdEnabled) {
-            if (client_mode) {
-                // Client-side TLS Channel ID
-                if (channelIdPrivateKey == null) {
-                    throw new SSLHandshakeException("Invalid TLS channel ID key specified");
-                }
-                NativeCrypto.SSL_set1_tls_channel_id(sslNativePointer,
-                        channelIdPrivateKey.getNativeRef());
-            } else {
-                // Server-side TLS Channel ID
-                NativeCrypto.SSL_enable_tls_channel_id(sslNativePointer);
-            }
-        }
-    }
-
-    void setCertificate(long sslNativePointer, String alias) throws CertificateEncodingException,
-            SSLException {
-        if (alias == null) {
-            return;
-        }
-        X509KeyManager keyManager = getX509KeyManager();
-        if (keyManager == null) {
-            return;
-        }
-        PrivateKey privateKey = keyManager.getPrivateKey(alias);
-        if (privateKey == null) {
-            return;
-        }
-        X509Certificate[] certificates = keyManager.getCertificateChain(alias);
-        if (certificates == null) {
-            return;
-        }
-        PublicKey publicKey = (certificates.length > 0) ? certificates[0].getPublicKey() : null;
-
-        /*
-         * Make sure we keep a reference to the OpenSSLX509Certificate by using
-         * this array. Otherwise, if they're not OpenSSLX509Certificate
-         * instances originally, they may be garbage collected before we
-         * complete our JNI calls.
-         */
-        OpenSSLX509Certificate[] openSslCerts = new OpenSSLX509Certificate[certificates.length];
-        long[] x509refs = new long[certificates.length];
-        for (int i = 0; i < certificates.length; i++) {
-            OpenSSLX509Certificate openSslCert = OpenSSLX509Certificate
-                    .fromCertificate(certificates[i]);
-            openSslCerts[i] = openSslCert;
-            x509refs[i] = openSslCert.getContext();
-        }
-
-        // Note that OpenSSL says to use SSL_use_certificate before
-        // SSL_use_PrivateKey.
-        NativeCrypto.SSL_use_certificate(sslNativePointer, x509refs);
-
-        final OpenSSLKey key;
-        try {
-            key = OpenSSLKey.fromPrivateKeyForTLSStackOnly(privateKey, publicKey);
-            NativeCrypto.SSL_use_PrivateKey(sslNativePointer, key.getNativeRef());
-        } catch (InvalidKeyException e) {
-            throw new SSLException(e);
-        }
-
-        // We may not have access to all the information to check the private key
-        // if it's a wrapped platform key, so skip this check.
-        if (!key.isWrapped()) {
-            // Makes sure the set PrivateKey and X509Certificate refer to the same
-            // key by comparing the public values.
-            NativeCrypto.SSL_check_private_key(sslNativePointer);
-        }
-    }
-
     /**
      * This filters {@code obsoleteProtocol} from the list of {@code protocols}
      * down to help with app compatibility.
@@ -535,95 +399,6 @@
 
     private static final String[] EMPTY_STRING_ARRAY = new String[0];
 
-    void setSSLParameters(long sslNativePointer, AliasChooser chooser, PSKCallbacks pskCallbacks,
-            String sniHostname) throws SSLException, IOException {
-        if (enabledProtocols.length == 0 && isEnabledProtocolsFiltered) {
-            throw new SSLHandshakeException("No enabled protocols; "
-                    + NativeCrypto.OBSOLETE_PROTOCOL_SSLV3
-                    + " is no longer supported and was filtered from the list");
-        }
-        NativeCrypto.SSL_configure_alpn(sslNativePointer, client_mode, alpnProtocols);
-        NativeCrypto.setEnabledProtocols(sslNativePointer, enabledProtocols);
-        NativeCrypto.setEnabledCipherSuites(sslNativePointer, enabledCipherSuites);
-
-        // setup server certificates and private keys.
-        // clients will receive a call back to request certificates.
-        if (!client_mode) {
-            Set<String> keyTypes = new HashSet<String>();
-            for (long sslCipherNativePointer : NativeCrypto.SSL_get_ciphers(sslNativePointer)) {
-                String keyType = getServerX509KeyType(sslCipherNativePointer);
-                if (keyType != null) {
-                    keyTypes.add(keyType);
-                }
-            }
-            X509KeyManager keyManager = getX509KeyManager();
-            if (keyManager != null) {
-                for (String keyType : keyTypes) {
-                    try {
-                        setCertificate(sslNativePointer,
-                                chooser.chooseServerAlias(x509KeyManager, keyType));
-                    } catch (CertificateEncodingException e) {
-                        throw new IOException(e);
-                    }
-                }
-            }
-
-            NativeCrypto.SSL_set_options(sslNativePointer,
-                    NativeConstants.SSL_OP_CIPHER_SERVER_PREFERENCE);
-
-            if (sctExtension != null) {
-                NativeCrypto.SSL_set_signed_cert_timestamp_list(sslNativePointer, sctExtension);
-            }
-
-            if (ocspResponse != null) {
-                NativeCrypto.SSL_set_ocsp_response(sslNativePointer, ocspResponse);
-            }
-        }
-
-        enablePSKKeyManagerIfRequested(sslNativePointer, pskCallbacks);
-
-        if (useSessionTickets) {
-            NativeCrypto.SSL_clear_options(sslNativePointer, NativeConstants.SSL_OP_NO_TICKET);
-        }
-        if (getUseSni() && AddressUtils.isValidSniHostname(sniHostname)) {
-            NativeCrypto.SSL_set_tlsext_host_name(sslNativePointer, sniHostname);
-        }
-
-        // BEAST attack mitigation (1/n-1 record splitting for CBC cipher suites
-        // with TLSv1 and SSLv3).
-        NativeCrypto.SSL_set_mode(sslNativePointer, NativeConstants.SSL_MODE_CBC_RECORD_SPLITTING);
-
-        boolean enableSessionCreation = getEnableSessionCreation();
-        if (!enableSessionCreation) {
-            NativeCrypto.SSL_set_session_creation_enabled(sslNativePointer, enableSessionCreation);
-        }
-    }
-
-    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
-    private void enablePSKKeyManagerIfRequested(long sslNativePointer, PSKCallbacks pskCallbacks)
-            throws SSLException {
-        // Enable Pre-Shared Key (PSK) key exchange if requested
-        PSKKeyManager pskKeyManager = getPSKKeyManager();
-        if (pskKeyManager != null) {
-            boolean pskEnabled = false;
-            for (String enabledCipherSuite : enabledCipherSuites) {
-                if ((enabledCipherSuite != null) && (enabledCipherSuite.contains("PSK"))) {
-                    pskEnabled = true;
-                    break;
-                }
-            }
-            if (pskEnabled) {
-                if (client_mode) {
-                    NativeCrypto.set_SSL_psk_client_callback_enabled(sslNativePointer, true);
-                } else {
-                    NativeCrypto.set_SSL_psk_server_callback_enabled(sslNativePointer, true);
-                    String identityHint = pskCallbacks.chooseServerPSKIdentityHint(pskKeyManager);
-                    NativeCrypto.SSL_use_psk_identity_hint(sslNativePointer, identityHint);
-                }
-            }
-        }
-    }
-
     /**
      * Returns whether Server Name Indication (SNI) is enabled by default for
      * sockets. For more information on SNI, see RFC 6066 section 3.
@@ -644,202 +419,6 @@
         }
     }
 
-    void setCertificateValidation(long sslNativePointer) throws IOException {
-        // setup peer certificate verification
-        if (!client_mode) {
-            // needing client auth takes priority...
-            boolean certRequested;
-            if (getNeedClientAuth()) {
-                NativeCrypto.SSL_set_verify(sslNativePointer,
-                                            NativeCrypto.SSL_VERIFY_PEER
-                                            | NativeCrypto.SSL_VERIFY_FAIL_IF_NO_PEER_CERT);
-                certRequested = true;
-            // ... over just wanting it...
-            } else if (getWantClientAuth()) {
-                NativeCrypto.SSL_set_verify(sslNativePointer, NativeCrypto.SSL_VERIFY_PEER);
-                certRequested = true;
-            // ... and we must disable verification if we don't want client auth.
-            } else {
-                NativeCrypto.SSL_set_verify(sslNativePointer, NativeCrypto.SSL_VERIFY_NONE);
-                certRequested = false;
-            }
-
-            if (certRequested) {
-                X509TrustManager trustManager = getX509TrustManager();
-                X509Certificate[] issuers = trustManager.getAcceptedIssuers();
-                if (issuers != null && issuers.length != 0) {
-                    byte[][] issuersBytes;
-                    try {
-                        issuersBytes = encodeIssuerX509Principals(issuers);
-                    } catch (CertificateEncodingException e) {
-                        throw new IOException("Problem encoding principals", e);
-                    }
-                    NativeCrypto.SSL_set_client_CA_list(sslNativePointer, issuersBytes);
-                }
-            }
-        }
-    }
-
-    AbstractOpenSSLSession setupSession(long sslSessionNativePointer, long sslNativePointer,
-            final AbstractOpenSSLSession sessionToReuse, String hostname, int port,
-            boolean handshakeCompleted) throws IOException {
-        AbstractOpenSSLSession sslSession = null;
-        if (sessionToReuse != null && NativeCrypto.SSL_session_reused(sslNativePointer)) {
-            sslSession = sessionToReuse;
-            sslSession.setLastAccessedTime(System.currentTimeMillis());
-            NativeCrypto.SSL_SESSION_free(sslSessionNativePointer);
-        } else {
-            if (!getEnableSessionCreation()) {
-                // Should have been prevented by
-                // NativeCrypto.SSL_set_session_creation_enabled
-                throw new IllegalStateException("SSL Session may not be created");
-            }
-            X509Certificate[] localCertificates = OpenSSLX509Certificate.createCertChain(
-                    NativeCrypto.SSL_get_certificate(sslNativePointer));
-            X509Certificate[] peerCertificates = OpenSSLX509Certificate.createCertChain(
-                    NativeCrypto.SSL_get_peer_cert_chain(sslNativePointer));
-            byte[] ocspData = NativeCrypto.SSL_get_ocsp_response(sslNativePointer);
-            byte[] tlsSctData = NativeCrypto.SSL_get_signed_cert_timestamp_list(sslNativePointer);
-            sslSession = new OpenSSLSessionImpl(sslSessionNativePointer, localCertificates,
-                    peerCertificates, ocspData, tlsSctData, hostname, port, getSessionContext());
-            // if not, putSession later in handshakeCompleted() callback
-            if (handshakeCompleted) {
-                getSessionContext().putSession(sslSession);
-            }
-        }
-        return sslSession;
-    }
-
-    void chooseClientCertificate(byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals,
-            long sslNativePointer, AliasChooser chooser) throws SSLException,
-            CertificateEncodingException {
-        Set<String> keyTypesSet = getSupportedClientKeyTypes(keyTypeBytes);
-        String[] keyTypes = keyTypesSet.toArray(new String[keyTypesSet.size()]);
-
-        X500Principal[] issuers;
-        if (asn1DerEncodedPrincipals == null) {
-            issuers = null;
-        } else {
-            issuers = new X500Principal[asn1DerEncodedPrincipals.length];
-            for (int i = 0; i < asn1DerEncodedPrincipals.length; i++) {
-                issuers[i] = new X500Principal(asn1DerEncodedPrincipals[i]);
-            }
-        }
-        X509KeyManager keyManager = getX509KeyManager();
-        String alias = (keyManager != null) ? chooser.chooseClientAlias(keyManager, issuers,
-                keyTypes) : null;
-        setCertificate(sslNativePointer, alias);
-    }
-
-    /**
-     * @see NativeCrypto.SSLHandshakeCallbacks#clientPSKKeyRequested(String, byte[], byte[])
-     */
-    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
-    int clientPSKKeyRequested(
-            String identityHint, byte[] identityBytesOut, byte[] key, PSKCallbacks pskCallbacks) {
-        PSKKeyManager pskKeyManager = getPSKKeyManager();
-        if (pskKeyManager == null) {
-            return 0;
-        }
-
-        String identity = pskCallbacks.chooseClientPSKIdentity(pskKeyManager, identityHint);
-        // Store identity in NULL-terminated modified UTF-8 representation into ientityBytesOut
-        byte[] identityBytes;
-        if (identity == null) {
-            identity = "";
-            identityBytes = EmptyArray.BYTE;
-        } else if (identity.isEmpty()) {
-            identityBytes = EmptyArray.BYTE;
-        } else {
-            try {
-                identityBytes = identity.getBytes("UTF-8");
-            } catch (UnsupportedEncodingException e) {
-                throw new RuntimeException("UTF-8 encoding not supported", e);
-            }
-        }
-        if (identityBytes.length + 1 > identityBytesOut.length) {
-            // Insufficient space in the output buffer
-            return 0;
-        }
-        if (identityBytes.length > 0) {
-            System.arraycopy(identityBytes, 0, identityBytesOut, 0, identityBytes.length);
-        }
-        identityBytesOut[identityBytes.length] = 0;
-
-        SecretKey secretKey = pskCallbacks.getPSKKey(pskKeyManager, identityHint, identity);
-        byte[] secretKeyBytes = secretKey.getEncoded();
-        if (secretKeyBytes == null) {
-            return 0;
-        } else if (secretKeyBytes.length > key.length) {
-            // Insufficient space in the output buffer
-            return 0;
-        }
-        System.arraycopy(secretKeyBytes, 0, key, 0, secretKeyBytes.length);
-        return secretKeyBytes.length;
-    }
-
-    /**
-     * @see NativeCrypto.SSLHandshakeCallbacks#serverPSKKeyRequested(String, String, byte[])
-     */
-    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
-    int serverPSKKeyRequested(
-            String identityHint, String identity, byte[] key, PSKCallbacks pskCallbacks) {
-        PSKKeyManager pskKeyManager = getPSKKeyManager();
-        if (pskKeyManager == null) {
-            return 0;
-        }
-        SecretKey secretKey = pskCallbacks.getPSKKey(pskKeyManager, identityHint, identity);
-        byte[] secretKeyBytes = secretKey.getEncoded();
-        if (secretKeyBytes == null) {
-            return 0;
-        } else if (secretKeyBytes.length > key.length) {
-            return 0;
-        }
-        System.arraycopy(secretKeyBytes, 0, key, 0, secretKeyBytes.length);
-        return secretKeyBytes.length;
-    }
-
-    /**
-     * Gets the suitable session reference from the session cache container.
-     */
-    SSLSession getCachedClientSession(ClientSessionContext sessionContext, String hostName,
-            int port) {
-        if (hostName == null) {
-            return null;
-        }
-
-        SSLSession session = sessionContext.getSession(hostName, port);
-        if (session == null) {
-            return null;
-        }
-
-        String protocol = session.getProtocol();
-        boolean protocolFound = false;
-        for (String enabledProtocol : enabledProtocols) {
-            if (protocol.equals(enabledProtocol)) {
-                protocolFound = true;
-                break;
-            }
-        }
-        if (!protocolFound) {
-            return null;
-        }
-
-        String cipherSuite = session.getCipherSuite();
-        boolean cipherSuiteFound = false;
-        for (String enabledCipherSuite : enabledCipherSuites) {
-            if (cipherSuite.equals(enabledCipherSuite)) {
-                cipherSuiteFound = true;
-                break;
-            }
-        }
-        if (!cipherSuiteFound) {
-            return null;
-        }
-
-        return session;
-    }
-
     /**
      * For abstracting the X509KeyManager calls between
      * {@link X509KeyManager#chooseClientAlias(String[], java.security.Principal[], java.net.Socket)}
@@ -1004,93 +583,6 @@
         this.useCipherSuitesOrder = useCipherSuitesOrder;
     }
 
-    /** Key type: RSA certificate. */
-    private static final String KEY_TYPE_RSA = "RSA";
-
-    /** Key type: Diffie-Hellman certificate signed by issuer with RSA signature. */
-    private static final String KEY_TYPE_DH_RSA = "DH_RSA";
-
-    /** Key type: Elliptic Curve certificate. */
-    private static final String KEY_TYPE_EC = "EC";
-
-    /** Key type: Elliptic Curve certificate signed by issuer with ECDSA signature. */
-    private static final String KEY_TYPE_EC_EC = "EC_EC";
-
-    /** Key type: Elliptic Curve certificate signed by issuer with RSA signature. */
-    private static final String KEY_TYPE_EC_RSA = "EC_RSA";
-
-    /**
-     * Returns key type constant suitable for calling X509KeyManager.chooseServerAlias or
-     * X509ExtendedKeyManager.chooseEngineServerAlias. Returns {@code null} for key exchanges that
-     * do not use X.509 for server authentication.
-     */
-    private static String getServerX509KeyType(long sslCipherNative) throws SSLException {
-        String kx_name = NativeCrypto.SSL_CIPHER_get_kx_name(sslCipherNative);
-        if (kx_name.equals("RSA") || kx_name.equals("DHE_RSA") || kx_name.equals("ECDHE_RSA")) {
-            return KEY_TYPE_RSA;
-        } else if (kx_name.equals("ECDHE_ECDSA")) {
-            return KEY_TYPE_EC;
-        } else if (kx_name.equals("ECDH_RSA")) {
-            return KEY_TYPE_EC_RSA;
-        } else if (kx_name.equals("ECDH_ECDSA")) {
-            return KEY_TYPE_EC_EC;
-        } else if (kx_name.equals("DH_RSA")) {
-            return KEY_TYPE_DH_RSA;
-        } else {
-            return null;
-        }
-    }
-
-    /**
-     * Similar to getServerKeyType, but returns value given TLS
-     * ClientCertificateType byte values from a CertificateRequest
-     * message for use with X509KeyManager.chooseClientAlias or
-     * X509ExtendedKeyManager.chooseEngineClientAlias.
-     * <p>
-     * Visible for testing.
-     */
-    static String getClientKeyType(byte clientCertificateType) {
-        // See also http://www.ietf.org/assignments/tls-parameters/tls-parameters.xml
-        switch (clientCertificateType) {
-            case NativeConstants.TLS_CT_RSA_SIGN:
-                return KEY_TYPE_RSA; // RFC rsa_sign
-            case NativeConstants.TLS_CT_RSA_FIXED_DH:
-                return KEY_TYPE_DH_RSA; // RFC rsa_fixed_dh
-            case NativeConstants.TLS_CT_ECDSA_SIGN:
-                return KEY_TYPE_EC; // RFC ecdsa_sign
-            case NativeConstants.TLS_CT_RSA_FIXED_ECDH:
-                return KEY_TYPE_EC_RSA; // RFC rsa_fixed_ecdh
-            case NativeConstants.TLS_CT_ECDSA_FIXED_ECDH:
-                return KEY_TYPE_EC_EC; // RFC ecdsa_fixed_ecdh
-            default:
-                return null;
-        }
-    }
-
-    /**
-     * Gets the supported key types for client certificates based on the
-     * {@code ClientCertificateType} values provided by the server.
-     *
-     * @param clientCertificateTypes {@code ClientCertificateType} values provided by the server.
-     *        See https://www.ietf.org/assignments/tls-parameters/tls-parameters.xml.
-     * @return supported key types that can be used in {@code X509KeyManager.chooseClientAlias} and
-     *         {@code X509ExtendedKeyManager.chooseEngineClientAlias}.
-     *
-     * Visible for testing.
-     */
-    static Set<String> getSupportedClientKeyTypes(byte[] clientCertificateTypes) {
-        Set<String> result = new HashSet<String>(clientCertificateTypes.length);
-        for (byte keyTypeCode : clientCertificateTypes) {
-            String keyType = getClientKeyType(keyTypeCode);
-            if (keyType == null) {
-                // Unsupported client key type -- ignore
-                continue;
-            }
-            result.add(keyType);
-        }
-        return result;
-    }
-
     private static String[] getDefaultCipherSuites(
             boolean x509CipherSuitesNeeded,
             boolean pskCipherSuitesNeeded) {
diff --git a/common/src/main/java/org/conscrypt/SSLUtils.java b/common/src/main/java/org/conscrypt/SSLUtils.java
index c770260..7e9822f 100644
--- a/common/src/main/java/org/conscrypt/SSLUtils.java
+++ b/common/src/main/java/org/conscrypt/SSLUtils.java
@@ -41,8 +41,14 @@
 import static org.conscrypt.NativeConstants.SSL3_RT_MAX_PACKET_SIZE;
 
 import java.nio.ByteBuffer;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.HashSet;
+import java.util.Set;
 import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.security.cert.CertificateException;
 
 /**
  * Utility methods for SSL packet processing. Copied from the Netty project.
@@ -50,10 +56,95 @@
  * This is a public class to allow testing to occur on Android via CTS.
  */
 final class SSLUtils {
-    static final boolean USE_ENGINE_SOCKET_BY_DEFAULT =
-            Boolean.parseBoolean(System.getProperty("org.conscrypt.useEngineSocketByDefault"));
+    static final boolean USE_ENGINE_SOCKET_BY_DEFAULT = Boolean.parseBoolean(
+            System.getProperty("org.conscrypt.useEngineSocketByDefault", "false"));
     private static final int MAX_PROTOCOL_LENGTH = 255;
 
+    // TODO(nathanmittler): Should these be in NativeConstants?
+    enum SessionType {
+        /**
+         * Identifies OpenSSL sessions.
+         */
+        OPEN_SSL(1),
+
+        /**
+         * Identifies OpenSSL sessions with OCSP stapled data.
+         */
+        OPEN_SSL_WITH_OCSP(2),
+
+        /**
+         * Identifies OpenSSL sessions with TLS SCT data.
+         */
+        OPEN_SSL_WITH_TLS_SCT(3);
+
+        SessionType(int value) {
+            this.value = value;
+        }
+
+        static final boolean isSupportedType(int type) {
+            return type == OPEN_SSL.value || type == OPEN_SSL_WITH_OCSP.value
+                    || type == OPEN_SSL_WITH_TLS_SCT.value;
+        }
+
+        final int value;
+    }
+
+    /**
+     * States for SSL engines.
+     */
+    static final class EngineStates {
+        private EngineStates() {}
+
+        /**
+         * The engine is constructed, but the initial handshake hasn't been started
+         */
+        static final int STATE_NEW = 0;
+
+        /**
+         * The client/server mode of the engine has been set.
+         */
+        static final int STATE_MODE_SET = 1;
+
+        /**
+         * The handshake has been started
+         */
+        static final int STATE_HANDSHAKE_STARTED = 2;
+
+        /**
+         * Listeners of the handshake have been notified of completion but the handshake call
+         * hasn't returned.
+         */
+        static final int STATE_HANDSHAKE_COMPLETED = 3;
+
+        /**
+         * The handshake call returned but the listeners have not yet been notified. This is expected
+         * behaviour in cut-through mode, where SSL_do_handshake returns before the handshake is
+         * complete. We can now start writing data to the socket.
+         */
+        static final int STATE_READY_HANDSHAKE_CUT_THROUGH = 4;
+
+        /**
+         * The handshake call has returned and the listeners have been notified. Ready to begin
+         * writing data.
+         */
+        static final int STATE_READY = 5;
+
+        /**
+         * The inbound direction of the engine has been closed.
+         */
+        static final int STATE_CLOSED_INBOUND = 6;
+
+        /**
+         * The outbound direction of the engine has been closed.
+         */
+        static final int STATE_CLOSED_OUTBOUND = 7;
+
+        /**
+         * The engine has been closed.
+         */
+        static final int STATE_CLOSED = 8;
+    }
+
     /**
      * This is the maximum overhead when encrypting plaintext as defined by
      * <a href="https://www.ietf.org/rfc/rfc5246.txt">rfc5264</a>,
@@ -74,6 +165,106 @@
     private static final int MAX_ENCRYPTION_OVERHEAD_DIFF =
             Integer.MAX_VALUE - MAX_ENCRYPTION_OVERHEAD_LENGTH;
 
+    /** Key type: RSA certificate. */
+    private static final String KEY_TYPE_RSA = "RSA";
+
+    /** Key type: Elliptic Curve certificate. */
+    private static final String KEY_TYPE_EC = "EC";
+
+    /**
+     * Returns key type constant suitable for calling X509KeyManager.chooseServerAlias or
+     * X509ExtendedKeyManager.chooseEngineServerAlias. Returns {@code null} for key exchanges that
+     * do not use X.509 for server authentication.
+     */
+    static String getServerX509KeyType(long sslCipherNative) throws SSLException {
+        String kx_name = NativeCrypto.SSL_CIPHER_get_kx_name(sslCipherNative);
+        if (kx_name.equals("RSA") || kx_name.equals("DHE_RSA") || kx_name.equals("ECDHE_RSA")) {
+            return KEY_TYPE_RSA;
+        } else if (kx_name.equals("ECDHE_ECDSA")) {
+            return KEY_TYPE_EC;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Similar to getServerKeyType, but returns value given TLS
+     * ClientCertificateType byte values from a CertificateRequest
+     * message for use with X509KeyManager.chooseClientAlias or
+     * X509ExtendedKeyManager.chooseEngineClientAlias.
+     * <p>
+     * Visible for testing.
+     */
+    static String getClientKeyType(byte clientCertificateType) {
+        // See also http://www.ietf.org/assignments/tls-parameters/tls-parameters.xml
+        switch (clientCertificateType) {
+            case NativeConstants.TLS_CT_RSA_SIGN:
+                return KEY_TYPE_RSA; // RFC rsa_sign
+            case NativeConstants.TLS_CT_ECDSA_SIGN:
+                return KEY_TYPE_EC; // RFC ecdsa_sign
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * Gets the supported key types for client certificates based on the
+     * {@code ClientCertificateType} values provided by the server.
+     *
+     * @param clientCertificateTypes {@code ClientCertificateType} values provided by the server.
+     *        See https://www.ietf.org/assignments/tls-parameters/tls-parameters.xml.
+     * @return supported key types that can be used in {@code X509KeyManager.chooseClientAlias} and
+     *         {@code X509ExtendedKeyManager.chooseEngineClientAlias}.
+     *
+     * Visible for testing.
+     */
+    static Set<String> getSupportedClientKeyTypes(byte[] clientCertificateTypes) {
+        Set<String> result = new HashSet<String>(clientCertificateTypes.length);
+        for (byte keyTypeCode : clientCertificateTypes) {
+            String keyType = SSLUtils.getClientKeyType(keyTypeCode);
+            if (keyType == null) {
+                // Unsupported client key type -- ignore
+                continue;
+            }
+            result.add(keyType);
+        }
+        return result;
+    }
+
+    static byte[][] encodeIssuerX509Principals(X509Certificate[] certificates)
+            throws CertificateEncodingException {
+        byte[][] principalBytes = new byte[certificates.length][];
+        for (int i = 0; i < certificates.length; i++) {
+            principalBytes[i] = certificates[i].getIssuerX500Principal().getEncoded();
+        }
+        return principalBytes;
+    }
+
+    /**
+     * Converts the peer certificates into a cert chain.
+     */
+    static javax.security.cert.X509Certificate[] toCertificateChain(X509Certificate[] certificates)
+            throws SSLPeerUnverifiedException {
+        try {
+            javax.security.cert.X509Certificate[] chain =
+                    new javax.security.cert.X509Certificate[certificates.length];
+
+            for (int i = 0; i < certificates.length; i++) {
+                byte[] encoded = certificates[i].getEncoded();
+                chain[i] = javax.security.cert.X509Certificate.getInstance(encoded);
+            }
+            return chain;
+        } catch (CertificateEncodingException e) {
+            SSLPeerUnverifiedException exception = new SSLPeerUnverifiedException(e.getMessage());
+            exception.initCause(exception);
+            throw exception;
+        } catch (CertificateException e) {
+            SSLPeerUnverifiedException exception = new SSLPeerUnverifiedException(e.getMessage());
+            exception.initCause(exception);
+            throw exception;
+        }
+    }
+
     /**
      * Calculates the minimum bytes required in the encrypted output buffer for the given number of
      * plaintext source bytes.
diff --git a/common/src/main/java/org/conscrypt/ServerSessionContext.java b/common/src/main/java/org/conscrypt/ServerSessionContext.java
index 20fc3ae..dc4cafb 100644
--- a/common/src/main/java/org/conscrypt/ServerSessionContext.java
+++ b/common/src/main/java/org/conscrypt/ServerSessionContext.java
@@ -16,7 +16,7 @@
 
 package org.conscrypt;
 
-import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLContext;
 
 /**
  * Caches server sessions. Indexes by session ID. Users typically look up
@@ -26,10 +26,9 @@
  */
 @Internal
 public final class ServerSessionContext extends AbstractSessionContext {
-
     private SSLServerSessionCache persistentCache;
 
-    public ServerSessionContext() {
+    ServerSessionContext() {
         super(100);
 
         // TODO make sure SSL_CTX does not automaticaly clear sessions we want it to cache
@@ -48,30 +47,23 @@
         NativeCrypto.SSL_CTX_set_session_id_context(sslCtxNativePointer, new byte[] { ' ' });
     }
 
+    /**
+     * Applications should not use this method. Instead use {@link
+     * Conscrypt.Contexts#setServerSessionCache(SSLContext, SSLServerSessionCache)}.
+     */
     public void setPersistentCache(SSLServerSessionCache persistentCache) {
         this.persistentCache = persistentCache;
     }
 
     @Override
-    protected void sessionRemoved(SSLSession session) {}
-
-    @Override
-    public SSLSession getSession(byte[] sessionId) {
-        // First see if AbstractSessionContext can satisfy the request.
-        SSLSession cachedSession = super.getSession(sessionId);
-        if (cachedSession != null) {
-            // This will already have gone through Platform#wrapSSLSession
-            return cachedSession;
-        }
-
-        // Then check the persistent cache.
+    SslSessionWrapper getSessionFromPersistentCache(byte[] sessionId) {
         if (persistentCache != null) {
             byte[] data = persistentCache.getSessionData(sessionId);
             if (data != null) {
-                OpenSSLSessionImpl session = toSession(data, null, -1);
+                SslSessionWrapper session = SslSessionWrapper.newInstance(this, data, null, -1);
                 if (session != null && session.isValid()) {
-                    super.putSession(session);
-                    return Platform.wrapSSLSession(session);
+                    cacheSession(session);
+                    return session;
                 }
             }
         }
@@ -80,15 +72,18 @@
     }
 
     @Override
-    void putSession(SSLSession session) {
-        super.putSession(session);
-
-        // TODO: In background thread.
+    void onBeforeAddSession(SslSessionWrapper session) {
+        // TODO: Do this in background thread.
         if (persistentCache != null) {
-            byte[] data = toBytes(session);
+            byte[] data = session.toBytes();
             if (data != null) {
-                persistentCache.putSessionData(session, data);
+                persistentCache.putSessionData(session.toSSLSession(), data);
             }
         }
     }
+
+    @Override
+    void onBeforeRemoveSession(SslSessionWrapper session) {
+        // Do nothing.
+    }
 }
diff --git a/common/src/main/java/org/conscrypt/SslSessionWrapper.java b/common/src/main/java/org/conscrypt/SslSessionWrapper.java
new file mode 100644
index 0000000..8758fe8
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/SslSessionWrapper.java
@@ -0,0 +1,472 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License", "www.google.com", 443);
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import static org.conscrypt.SSLUtils.SessionType.OPEN_SSL_WITH_OCSP;
+import static org.conscrypt.SSLUtils.SessionType.OPEN_SSL_WITH_TLS_SCT;
+import static org.conscrypt.SSLUtils.SessionType.isSupportedType;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.util.List;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSessionContext;
+import javax.security.cert.X509Certificate;
+
+/**
+ * A utility wrapper that abstracts operations on the underlying native SSL_SESSION instance.
+ *
+ * This is abstract only to support mocking for tests.
+ */
+abstract class SslSessionWrapper {
+    /**
+     * Creates a new instance. Since BoringSSL does not provide an API to get access to all
+     * session information via the SSL_SESSION, we get some values (e.g. peer certs) from
+     * the active session instead (i.e. the SSL object).
+     */
+    static SslSessionWrapper newInstance(NativeRef.SSL_SESSION ref, ActiveSession activeSession)
+            throws SSLPeerUnverifiedException {
+        AbstractSessionContext context = (AbstractSessionContext) activeSession.getSessionContext();
+        if (context instanceof ClientSessionContext) {
+            return new Impl(context, ref, activeSession.getPeerHost(),
+                    activeSession.getPeerPort(), activeSession.getPeerCertificates(),
+                    getOcspResponse(activeSession),
+                    activeSession.getPeerSignedCertificateTimestamp());
+        }
+
+        // Server's will be cached by ID and won't have any of the extra fields.
+        return new Impl(context, ref, null, -1, null, null, null);
+    }
+
+    private static byte[] getOcspResponse(ActiveSession activeSession) {
+        List<byte[]> ocspResponseList = activeSession.getStatusResponses();
+        if (ocspResponseList.size() >= 1) {
+            return ocspResponseList.get(0);
+        }
+        return null;
+    }
+
+    /**
+     * Creates a new {@link SslSessionWrapper} instance from the provided serialized bytes, which
+     * were generated by {@link #toBytes()}.
+     *
+     * @return The new instance if successful. If unable to parse the bytes for any reason, returns
+     * {@code null}.
+     */
+    static SslSessionWrapper newInstance(
+            AbstractSessionContext context, byte[] data, String host, int port) {
+        ByteBuffer buf = ByteBuffer.wrap(data);
+        try {
+            int type = buf.getInt();
+            if (!isSupportedType(type)) {
+                throw new IOException("Unexpected type ID: " + type);
+            }
+
+            int length = buf.getInt();
+            checkRemaining(buf, length);
+
+            byte[] sessionData = new byte[length];
+            buf.get(sessionData);
+
+            int count = buf.getInt();
+            checkRemaining(buf, count);
+
+            java.security.cert.X509Certificate[] peerCerts =
+                    new java.security.cert.X509Certificate[count];
+            for (int i = 0; i < count; i++) {
+                length = buf.getInt();
+                checkRemaining(buf, length);
+
+                byte[] certData = new byte[length];
+                buf.get(certData);
+                try {
+                    peerCerts[i] = OpenSSLX509Certificate.fromX509Der(certData);
+                } catch (Exception e) {
+                    throw new IOException("Can not read certificate " + i + "/" + count);
+                }
+            }
+
+            byte[] ocspData = null;
+            if (type >= OPEN_SSL_WITH_OCSP.value) {
+                // We only support one OCSP response now, but in the future
+                // we may support RFC 6961 which has multiple.
+                int countOcspResponses = buf.getInt();
+                checkRemaining(buf, countOcspResponses);
+
+                if (countOcspResponses >= 1) {
+                    int ocspLength = buf.getInt();
+                    checkRemaining(buf, ocspLength);
+
+                    ocspData = new byte[ocspLength];
+                    buf.get(ocspData);
+
+                    // Skip the rest of the responses.
+                    for (int i = 1; i < countOcspResponses; i++) {
+                        ocspLength = buf.getInt();
+                        checkRemaining(buf, ocspLength);
+                        buf.position(buf.position() + ocspLength);
+                    }
+                }
+            }
+
+            byte[] tlsSctData = null;
+            if (type == OPEN_SSL_WITH_TLS_SCT.value) {
+                int tlsSctDataLength = buf.getInt();
+                checkRemaining(buf, tlsSctDataLength);
+
+                if (tlsSctDataLength > 0) {
+                    tlsSctData = new byte[tlsSctDataLength];
+                    buf.get(tlsSctData);
+                }
+            }
+
+            if (buf.remaining() != 0) {
+                log(new AssertionError("Read entire session, but data still remains; rejecting"));
+                return null;
+            }
+
+            NativeRef.SSL_SESSION ref =
+                    new NativeRef.SSL_SESSION(NativeCrypto.d2i_SSL_SESSION(sessionData));
+            return new Impl(context, ref, host, port, peerCerts, ocspData, tlsSctData);
+        } catch (IOException e) {
+            log(e);
+            return null;
+        } catch (BufferUnderflowException e) {
+            log(e);
+            return null;
+        }
+    }
+
+    abstract byte[] getId();
+
+    abstract boolean isValid();
+
+    abstract void offerToResume(SslWrapper ssl) throws SSLException;
+
+    abstract String getCipherSuite();
+
+    abstract String getProtocol();
+
+    abstract String getPeerHost();
+
+    abstract int getPeerPort();
+
+    /**
+     * Returns the OCSP stapled response. The returned array is not copied; the caller must
+     * either not modify the returned array or make a copy.
+     *
+     * @see <a href="https://tools.ietf.org/html/rfc6066">RFC 6066</a>
+     * @see <a href="https://tools.ietf.org/html/rfc6961">RFC 6961</a>
+     */
+    abstract byte[] getPeerOcspStapledResponse();
+
+    /**
+     * Returns the signed certificate timestamp (SCT) received from the peer. The returned array
+     * is not copied; the caller must either not modify the returned array or make a copy.
+     *
+     * @see <a href="https://tools.ietf.org/html/rfc6962">RFC 6962</a>
+     */
+    abstract byte[] getPeerSignedCertificateTimestamp();
+
+    /**
+     * Converts the given session to bytes.
+     *
+     * @return session data as bytes or null if the session can't be converted
+     */
+    abstract byte[] toBytes();
+
+    /**
+     * Converts this object to a {@link SSLSession}. The returned session will support only a
+     * subset of the {@link SSLSession} API.
+     */
+    abstract SSLSession toSSLSession();
+
+    /**
+     * The session wrapper implementation.
+     */
+    private static final class Impl extends SslSessionWrapper {
+        private final NativeRef.SSL_SESSION ref;
+
+        // BoringSSL offers no API to obtain these values directly from the SSL_SESSION.
+        private final AbstractSessionContext context;
+        private final String host;
+        private final int port;
+        private final String protocol;
+        private final String cipherSuite;
+        private final java.security.cert.X509Certificate[] peerCertificates;
+        private final byte[] peerOcspStapledResponse;
+        private final byte[] peerSignedCertificateTimestamp;
+
+        private Impl(AbstractSessionContext context, NativeRef.SSL_SESSION ref, String host,
+                int port, java.security.cert.X509Certificate[] peerCertificates,
+                byte[] peerOcspStapledResponse, byte[] peerSignedCertificateTimestamp) {
+            this.context = context;
+            this.host = host;
+            this.port = port;
+            this.peerCertificates = peerCertificates;
+            this.peerOcspStapledResponse = peerOcspStapledResponse;
+            this.peerSignedCertificateTimestamp = peerSignedCertificateTimestamp;
+            this.protocol = NativeCrypto.SSL_SESSION_get_version(ref.context);
+            this.cipherSuite =
+                    NativeCrypto.cipherSuiteToJava(NativeCrypto.SSL_SESSION_cipher(ref.context));
+            this.ref = ref;
+        }
+
+        @Override
+        byte[] getId() {
+            return NativeCrypto.SSL_SESSION_session_id(ref.context);
+        }
+
+        private long getCreationTime() {
+            return NativeCrypto.SSL_SESSION_get_time(ref.context);
+        }
+
+        @Override
+        boolean isValid() {
+            long creationTimeMillis = getCreationTime();
+            // Use the minimum of the timeout from the context and the session.
+            long timeoutMillis =
+                    Math.max(0,
+                            Math.min(context.getSessionTimeout(),
+                                    NativeCrypto.SSL_SESSION_get_timeout(ref.context)))
+                    * 1000;
+            return (System.currentTimeMillis() - timeoutMillis) < creationTimeMillis;
+        }
+
+        @Override
+        void offerToResume(SslWrapper ssl) throws SSLException {
+            ssl.offerToResumeSession(ref.context);
+        }
+
+        @Override
+        String getCipherSuite() {
+            return cipherSuite;
+        }
+
+        @Override
+        String getProtocol() {
+            return protocol;
+        }
+
+        @Override
+        String getPeerHost() {
+            return host;
+        }
+
+        @Override
+        int getPeerPort() {
+            return port;
+        }
+
+        @Override
+        byte[] getPeerOcspStapledResponse() {
+            return peerOcspStapledResponse;
+        }
+
+        @Override
+        byte[] getPeerSignedCertificateTimestamp() {
+            return peerSignedCertificateTimestamp;
+        }
+
+        @Override
+        byte[] toBytes() {
+            try {
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                DataOutputStream daos = new DataOutputStream(baos);
+
+                daos.writeInt(OPEN_SSL_WITH_TLS_SCT.value); // session type ID
+
+                // Session data.
+                byte[] data = NativeCrypto.i2d_SSL_SESSION(ref.context);
+                daos.writeInt(data.length);
+                daos.write(data);
+
+                // Certificates.
+                daos.writeInt(peerCertificates.length);
+
+                for (Certificate cert : peerCertificates) {
+                    data = cert.getEncoded();
+                    daos.writeInt(data.length);
+                    daos.write(data);
+                }
+
+                if (peerOcspStapledResponse != null) {
+                    daos.writeInt(1);
+                    daos.writeInt(peerOcspStapledResponse.length);
+                    daos.write(peerOcspStapledResponse);
+                } else {
+                    daos.writeInt(0);
+                }
+
+                if (peerSignedCertificateTimestamp != null) {
+                    daos.writeInt(peerSignedCertificateTimestamp.length);
+                    daos.write(peerSignedCertificateTimestamp);
+                } else {
+                    daos.writeInt(0);
+                }
+
+                // TODO: local certificates?
+
+                return baos.toByteArray();
+            } catch (IOException e) {
+                // TODO(nathanmittler): Better error handling?
+                System.err.println("Failed to convert saved SSL Session: " + e.getMessage());
+                return null;
+            } catch (CertificateEncodingException e) {
+                log(e);
+                return null;
+            }
+        }
+
+        @Override
+        SSLSession toSSLSession() {
+            return new SSLSession() {
+                @Override
+                public byte[] getId() {
+                    return Impl.this.getId();
+                }
+
+                @Override
+                public String getCipherSuite() {
+                    return Impl.this.getCipherSuite();
+                }
+
+                @Override
+                public String getProtocol() {
+                    return Impl.this.getProtocol();
+                }
+
+                @Override
+                public String getPeerHost() {
+                    return Impl.this.getPeerHost();
+                }
+
+                @Override
+                public int getPeerPort() {
+                    return Impl.this.getPeerPort();
+                }
+
+                @Override
+                public long getCreationTime() {
+                    return Impl.this.getCreationTime();
+                }
+
+                @Override
+                public boolean isValid() {
+                    return Impl.this.isValid();
+                }
+
+                // UNSUPPORTED OPERATIONS
+
+                @Override
+                public SSLSessionContext getSessionContext() {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public long getLastAccessedTime() {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public void invalidate() {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public void putValue(String s, Object o) {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public Object getValue(String s) {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public void removeValue(String s) {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public String[] getValueNames() {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public Certificate[] getLocalCertificates() {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public X509Certificate[] getPeerCertificateChain()
+                        throws SSLPeerUnverifiedException {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public Principal getLocalPrincipal() {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public int getPacketBufferSize() {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public int getApplicationBufferSize() {
+                    throw new UnsupportedOperationException();
+                }
+            };
+        }
+    }
+
+    private static void log(Throwable t) {
+        // TODO(nathanmittler): Better error handling?
+        System.out.println("Error inflating SSL session: "
+                + (t.getMessage() != null ? t.getMessage() : t.getClass().getName()));
+    }
+
+    private static void checkRemaining(ByteBuffer buf, int length) throws IOException {
+        if (length < 0) {
+            throw new IOException("Length is negative: " + length);
+        }
+        if (length > buf.remaining()) {
+            throw new IOException(
+                    "Length of blob is longer than available: " + length + " > " + buf.remaining());
+        }
+    }
+}
diff --git a/common/src/main/java/org/conscrypt/SslWrapper.java b/common/src/main/java/org/conscrypt/SslWrapper.java
new file mode 100644
index 0000000..c03dbed
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/SslWrapper.java
@@ -0,0 +1,580 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import static org.conscrypt.NativeConstants.SSL_RECEIVED_SHUTDOWN;
+import static org.conscrypt.NativeConstants.SSL_SENT_SHUTDOWN;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.SocketTimeoutException;
+import java.security.InvalidKeyException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.HashSet;
+import java.util.Set;
+import javax.crypto.SecretKey;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+import javax.security.auth.x500.X500Principal;
+import org.conscrypt.NativeCrypto.SSLHandshakeCallbacks;
+import org.conscrypt.SSLParametersImpl.AliasChooser;
+import org.conscrypt.SSLParametersImpl.PSKCallbacks;
+
+/**
+ * A utility wrapper that abstracts operations on the underlying native SSL instance.
+ */
+final class SslWrapper {
+    private final SSLParametersImpl parameters;
+    private final SSLHandshakeCallbacks handshakeCallbacks;
+    private final AliasChooser aliasChooser;
+    private final PSKCallbacks pskCallbacks;
+    private long ssl;
+
+    static SslWrapper newInstance(SSLParametersImpl parameters,
+            SSLHandshakeCallbacks handshakeCallbacks, AliasChooser chooser,
+            PSKCallbacks pskCallbacks) throws SSLException {
+        long ctx = parameters.getSessionContext().sslCtxNativePointer;
+        long ssl = NativeCrypto.SSL_new(ctx);
+        return new SslWrapper(ssl, parameters, handshakeCallbacks, chooser, pskCallbacks);
+    }
+
+    private SslWrapper(long ssl, SSLParametersImpl parameters,
+            SSLHandshakeCallbacks handshakeCallbacks, AliasChooser aliasChooser,
+            PSKCallbacks pskCallbacks) {
+        this.ssl = ssl;
+        this.parameters = parameters;
+        this.handshakeCallbacks = handshakeCallbacks;
+        this.aliasChooser = aliasChooser;
+        this.pskCallbacks = pskCallbacks;
+    }
+
+    long ssl() {
+        return ssl;
+    }
+
+    BioWrapper newBio() {
+        try {
+            return new BioWrapper();
+        } catch (SSLException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    void offerToResumeSession(long sslSessionNativePointer) throws SSLException {
+        NativeCrypto.SSL_set_session(ssl, sslSessionNativePointer);
+    }
+
+    byte[] getSessionId() {
+        return NativeCrypto.SSL_session_id(ssl);
+    }
+
+    long getTime() {
+        return NativeCrypto.SSL_get_time(ssl);
+    }
+
+    long getTimeout() {
+        return NativeCrypto.SSL_get_timeout(ssl);
+    }
+
+    void setTimeout(long millis) {
+        NativeCrypto.SSL_set_timeout(ssl, millis);
+    }
+
+    String getCipherSuite() {
+        return NativeCrypto.cipherSuiteToJava(NativeCrypto.SSL_get_current_cipher(ssl));
+    }
+
+    OpenSSLX509Certificate[] getLocalCertificates() {
+        return OpenSSLX509Certificate.createCertChain(NativeCrypto.SSL_get_certificate(ssl));
+    }
+
+    OpenSSLX509Certificate[] getPeerCertificates() {
+        return OpenSSLX509Certificate.createCertChain(NativeCrypto.SSL_get_peer_cert_chain(ssl));
+    }
+
+    byte[] getPeerCertificateOcspData() {
+        return NativeCrypto.SSL_get_ocsp_response(ssl);
+    }
+
+    byte[] getPeerTlsSctData() {
+        return NativeCrypto.SSL_get_signed_cert_timestamp_list(ssl);
+    }
+
+    /**
+     * @see NativeCrypto.SSLHandshakeCallbacks#clientPSKKeyRequested(String, byte[], byte[])
+     */
+    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
+    int clientPSKKeyRequested(String identityHint, byte[] identityBytesOut, byte[] key) {
+        PSKKeyManager pskKeyManager = parameters.getPSKKeyManager();
+        if (pskKeyManager == null) {
+            return 0;
+        }
+
+        String identity = pskCallbacks.chooseClientPSKIdentity(pskKeyManager, identityHint);
+        // Store identity in NULL-terminated modified UTF-8 representation into ientityBytesOut
+        byte[] identityBytes;
+        if (identity == null) {
+            identity = "";
+            identityBytes = EmptyArray.BYTE;
+        } else if (identity.isEmpty()) {
+            identityBytes = EmptyArray.BYTE;
+        } else {
+            try {
+                identityBytes = identity.getBytes("UTF-8");
+            } catch (UnsupportedEncodingException e) {
+                throw new RuntimeException("UTF-8 encoding not supported", e);
+            }
+        }
+        if (identityBytes.length + 1 > identityBytesOut.length) {
+            // Insufficient space in the output buffer
+            return 0;
+        }
+        if (identityBytes.length > 0) {
+            System.arraycopy(identityBytes, 0, identityBytesOut, 0, identityBytes.length);
+        }
+        identityBytesOut[identityBytes.length] = 0;
+
+        SecretKey secretKey = pskCallbacks.getPSKKey(pskKeyManager, identityHint, identity);
+        byte[] secretKeyBytes = secretKey.getEncoded();
+        if (secretKeyBytes == null) {
+            return 0;
+        } else if (secretKeyBytes.length > key.length) {
+            // Insufficient space in the output buffer
+            return 0;
+        }
+        System.arraycopy(secretKeyBytes, 0, key, 0, secretKeyBytes.length);
+        return secretKeyBytes.length;
+    }
+
+    /**
+     * @see NativeCrypto.SSLHandshakeCallbacks#serverPSKKeyRequested(String, String, byte[])
+     */
+    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
+    int serverPSKKeyRequested(String identityHint, String identity, byte[] key) {
+        PSKKeyManager pskKeyManager = parameters.getPSKKeyManager();
+        if (pskKeyManager == null) {
+            return 0;
+        }
+        SecretKey secretKey = pskCallbacks.getPSKKey(pskKeyManager, identityHint, identity);
+        byte[] secretKeyBytes = secretKey.getEncoded();
+        if (secretKeyBytes == null) {
+            return 0;
+        } else if (secretKeyBytes.length > key.length) {
+            return 0;
+        }
+        System.arraycopy(secretKeyBytes, 0, key, 0, secretKeyBytes.length);
+        return secretKeyBytes.length;
+    }
+
+    void chooseClientCertificate(byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals)
+            throws SSLException, CertificateEncodingException {
+        Set<String> keyTypesSet = SSLUtils.getSupportedClientKeyTypes(keyTypeBytes);
+        String[] keyTypes = keyTypesSet.toArray(new String[keyTypesSet.size()]);
+
+        X500Principal[] issuers;
+        if (asn1DerEncodedPrincipals == null) {
+            issuers = null;
+        } else {
+            issuers = new X500Principal[asn1DerEncodedPrincipals.length];
+            for (int i = 0; i < asn1DerEncodedPrincipals.length; i++) {
+                issuers[i] = new X500Principal(asn1DerEncodedPrincipals[i]);
+            }
+        }
+        X509KeyManager keyManager = parameters.getX509KeyManager();
+        String alias = (keyManager != null)
+                ? aliasChooser.chooseClientAlias(keyManager, issuers, keyTypes)
+                : null;
+        setCertificate(alias);
+    }
+
+    void setCertificate(String alias) throws CertificateEncodingException, SSLException {
+        if (alias == null) {
+            return;
+        }
+        X509KeyManager keyManager = parameters.getX509KeyManager();
+        if (keyManager == null) {
+            return;
+        }
+        PrivateKey privateKey = keyManager.getPrivateKey(alias);
+        if (privateKey == null) {
+            return;
+        }
+        X509Certificate[] certificates = keyManager.getCertificateChain(alias);
+        if (certificates == null) {
+            return;
+        }
+        PublicKey publicKey = (certificates.length > 0) ? certificates[0].getPublicKey() : null;
+
+        /*
+         * Make sure we keep a reference to the OpenSSLX509Certificate by using
+         * this array. Otherwise, if they're not OpenSSLX509Certificate
+         * instances originally, they may be garbage collected before we
+         * complete our JNI calls.
+         */
+        OpenSSLX509Certificate[] openSslCerts = new OpenSSLX509Certificate[certificates.length];
+        long[] x509refs = new long[certificates.length];
+        for (int i = 0; i < certificates.length; i++) {
+            OpenSSLX509Certificate openSslCert =
+                    OpenSSLX509Certificate.fromCertificate(certificates[i]);
+            openSslCerts[i] = openSslCert;
+            x509refs[i] = openSslCert.getContext();
+        }
+
+        // Note that OpenSSL says to use SSL_use_certificate before
+        // SSL_use_PrivateKey.
+        NativeCrypto.SSL_use_certificate(ssl, x509refs);
+
+        final OpenSSLKey key;
+        try {
+            key = OpenSSLKey.fromPrivateKeyForTLSStackOnly(privateKey, publicKey);
+            NativeCrypto.SSL_use_PrivateKey(ssl, key.getNativeRef());
+        } catch (InvalidKeyException e) {
+            throw new SSLException(e);
+        }
+
+        // We may not have access to all the information to check the private key
+        // if it's a wrapped platform key, so skip this check.
+        if (!key.isWrapped()) {
+            // Makes sure the set PrivateKey and X509Certificate refer to the same
+            // key by comparing the public values.
+            NativeCrypto.SSL_check_private_key(ssl);
+        }
+    }
+
+    String getVersion() {
+        return NativeCrypto.SSL_get_version(ssl);
+    }
+
+    boolean isReused() {
+        return NativeCrypto.SSL_session_reused(ssl);
+    }
+
+    String getRequestedServerName() {
+        return NativeCrypto.SSL_get_servername(ssl);
+    }
+
+    byte[] getTlsChannelId() throws SSLException {
+        return NativeCrypto.SSL_get_tls_channel_id(ssl);
+    }
+
+    void initialize(String hostname, OpenSSLKey channelIdPrivateKey) throws IOException {
+        boolean enableSessionCreation = parameters.getEnableSessionCreation();
+        if (!enableSessionCreation) {
+            NativeCrypto.SSL_set_session_creation_enabled(ssl, false);
+        }
+
+        // Allow servers to trigger renegotiation. Some inadvisable server
+        // configurations cause them to attempt to renegotiate during
+        // certain protocols.
+        NativeCrypto.SSL_accept_renegotiations(ssl);
+
+        if (isClient()) {
+            NativeCrypto.SSL_set_connect_state(ssl);
+
+            // Configure OCSP and CT extensions for client
+            NativeCrypto.SSL_enable_ocsp_stapling(ssl);
+            if (parameters.isCTVerificationEnabled(hostname)) {
+                NativeCrypto.SSL_enable_signed_cert_timestamps(ssl);
+            }
+        } else {
+            NativeCrypto.SSL_set_accept_state(ssl);
+
+            // Configure OCSP for server
+            if (parameters.getOCSPResponse() != null) {
+                NativeCrypto.SSL_enable_ocsp_stapling(ssl);
+            }
+        }
+
+        if (parameters.getEnabledProtocols().length == 0 && parameters.isEnabledProtocolsFiltered) {
+            throw new SSLHandshakeException("No enabled protocols; "
+                    + NativeCrypto.OBSOLETE_PROTOCOL_SSLV3
+                    + " is no longer supported and was filtered from the list");
+        }
+        NativeCrypto.SSL_configure_alpn(ssl, isClient(), parameters.alpnProtocols);
+        NativeCrypto.setEnabledProtocols(ssl, parameters.enabledProtocols);
+        NativeCrypto.setEnabledCipherSuites(ssl, parameters.enabledCipherSuites);
+
+        // setup server certificates and private keys.
+        // clients will receive a call back to request certificates.
+        if (!isClient()) {
+            Set<String> keyTypes = new HashSet<String>();
+            for (long sslCipherNativePointer : NativeCrypto.SSL_get_ciphers(ssl)) {
+                String keyType = SSLUtils.getServerX509KeyType(sslCipherNativePointer);
+                if (keyType != null) {
+                    keyTypes.add(keyType);
+                }
+            }
+            X509KeyManager keyManager = parameters.getX509KeyManager();
+            if (keyManager != null) {
+                for (String keyType : keyTypes) {
+                    try {
+                        setCertificate(aliasChooser.chooseServerAlias(keyManager, keyType));
+                    } catch (CertificateEncodingException e) {
+                        throw new IOException(e);
+                    }
+                }
+            }
+
+            NativeCrypto.SSL_set_options(ssl, NativeConstants.SSL_OP_CIPHER_SERVER_PREFERENCE);
+
+            if (parameters.sctExtension != null) {
+                NativeCrypto.SSL_set_signed_cert_timestamp_list(ssl, parameters.sctExtension);
+            }
+
+            if (parameters.ocspResponse != null) {
+                NativeCrypto.SSL_set_ocsp_response(ssl, parameters.ocspResponse);
+            }
+        }
+
+        enablePSKKeyManagerIfRequested();
+
+        if (parameters.useSessionTickets) {
+            NativeCrypto.SSL_clear_options(ssl, NativeConstants.SSL_OP_NO_TICKET);
+        } else {
+            NativeCrypto.SSL_set_options(
+                    ssl, NativeCrypto.SSL_get_options(ssl) | NativeConstants.SSL_OP_NO_TICKET);
+        }
+
+        if (parameters.getUseSni() && AddressUtils.isValidSniHostname(hostname)) {
+            NativeCrypto.SSL_set_tlsext_host_name(ssl, hostname);
+        }
+
+        // BEAST attack mitigation (1/n-1 record splitting for CBC cipher suites
+        // with TLSv1 and SSLv3).
+        NativeCrypto.SSL_set_mode(ssl, NativeConstants.SSL_MODE_CBC_RECORD_SPLITTING);
+
+        setCertificateValidation(ssl);
+        setTlsChannelId(channelIdPrivateKey);
+    }
+
+    // TODO(nathanmittler): Remove once after we switch to the engine socket.
+    void doHandshake(FileDescriptor fd, int timeoutMillis)
+            throws CertificateException, SocketTimeoutException, SSLException {
+        NativeCrypto.SSL_do_handshake(ssl, fd, handshakeCallbacks, timeoutMillis);
+    }
+
+    int doHandshake() throws IOException {
+        return NativeCrypto.ENGINE_SSL_do_handshake(ssl, handshakeCallbacks);
+    }
+
+    // TODO(nathanmittler): Remove once after we switch to the engine socket.
+    int read(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)
+            throws IOException {
+        return NativeCrypto.SSL_read(ssl, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
+    }
+
+    // TODO(nathanmittler): Remove once after we switch to the engine socket.
+    void write(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)
+            throws IOException {
+        NativeCrypto.SSL_write(ssl, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
+    }
+
+    @SuppressWarnings("deprecation") // PSKKeyManager is deprecated, but in our own package
+    private void enablePSKKeyManagerIfRequested() throws SSLException {
+        // Enable Pre-Shared Key (PSK) key exchange if requested
+        PSKKeyManager pskKeyManager = parameters.getPSKKeyManager();
+        if (pskKeyManager != null) {
+            boolean pskEnabled = false;
+            for (String enabledCipherSuite : parameters.enabledCipherSuites) {
+                if ((enabledCipherSuite != null) && (enabledCipherSuite.contains("PSK"))) {
+                    pskEnabled = true;
+                    break;
+                }
+            }
+            if (pskEnabled) {
+                if (isClient()) {
+                    NativeCrypto.set_SSL_psk_client_callback_enabled(ssl, true);
+                } else {
+                    NativeCrypto.set_SSL_psk_server_callback_enabled(ssl, true);
+                    String identityHint = pskCallbacks.chooseServerPSKIdentityHint(pskKeyManager);
+                    NativeCrypto.SSL_use_psk_identity_hint(ssl, identityHint);
+                }
+            }
+        }
+    }
+
+    private void setTlsChannelId(OpenSSLKey channelIdPrivateKey) throws SSLException {
+        if (!parameters.channelIdEnabled) {
+            return;
+        }
+
+        if (parameters.getUseClientMode()) {
+            // Client-side TLS Channel ID
+            if (channelIdPrivateKey == null) {
+                throw new SSLHandshakeException("Invalid TLS channel ID key specified");
+            }
+            NativeCrypto.SSL_set1_tls_channel_id(ssl, channelIdPrivateKey.getNativeRef());
+        } else {
+            // Server-side TLS Channel ID
+            NativeCrypto.SSL_enable_tls_channel_id(ssl);
+        }
+    }
+
+    private void setCertificateValidation(long sslNativePointer) throws SSLException {
+        // setup peer certificate verification
+        if (!isClient()) {
+            // needing client auth takes priority...
+            boolean certRequested;
+            if (parameters.getNeedClientAuth()) {
+                NativeCrypto.SSL_set_verify(sslNativePointer, NativeCrypto.SSL_VERIFY_PEER
+                                | NativeCrypto.SSL_VERIFY_FAIL_IF_NO_PEER_CERT);
+                certRequested = true;
+                // ... over just wanting it...
+            } else if (parameters.getWantClientAuth()) {
+                NativeCrypto.SSL_set_verify(sslNativePointer, NativeCrypto.SSL_VERIFY_PEER);
+                certRequested = true;
+                // ... and we must disable verification if we don't want client auth.
+            } else {
+                NativeCrypto.SSL_set_verify(sslNativePointer, NativeCrypto.SSL_VERIFY_NONE);
+                certRequested = false;
+            }
+
+            if (certRequested) {
+                X509TrustManager trustManager = parameters.getX509TrustManager();
+                X509Certificate[] issuers = trustManager.getAcceptedIssuers();
+                if (issuers != null && issuers.length != 0) {
+                    byte[][] issuersBytes;
+                    try {
+                        issuersBytes = SSLUtils.encodeIssuerX509Principals(issuers);
+                    } catch (CertificateEncodingException e) {
+                        throw new SSLException("Problem encoding principals", e);
+                    }
+                    NativeCrypto.SSL_set_client_CA_list(sslNativePointer, issuersBytes);
+                }
+            }
+        }
+    }
+
+    void interrupt() {
+        NativeCrypto.SSL_interrupt(ssl);
+    }
+
+    // TODO(nathanmittler): Remove once after we switch to the engine socket.
+    void shutdown(FileDescriptor fd) throws IOException {
+        NativeCrypto.SSL_shutdown(ssl, fd, handshakeCallbacks);
+    }
+
+    void shutdown() throws IOException {
+        NativeCrypto.ENGINE_SSL_shutdown(ssl, handshakeCallbacks);
+    }
+
+    boolean wasShutdownReceived() {
+        return (NativeCrypto.SSL_get_shutdown(ssl) & SSL_RECEIVED_SHUTDOWN) != 0;
+    }
+
+    boolean wasShutdownSent() {
+        return (NativeCrypto.SSL_get_shutdown(ssl) & SSL_SENT_SHUTDOWN) != 0;
+    }
+
+    int readDirectByteBuffer(long destAddress, int destLength)
+            throws IOException, CertificateException {
+        return NativeCrypto.ENGINE_SSL_read_direct(
+                ssl, destAddress, destLength, handshakeCallbacks);
+    }
+
+    int readArray(byte[] destJava, int destOffset, int destLength)
+            throws IOException, CertificateException {
+        return NativeCrypto.ENGINE_SSL_read_heap(
+                ssl, destJava, destOffset, destLength, handshakeCallbacks);
+    }
+
+    int writeDirectByteBuffer(long sourceAddress, int sourceLength) throws IOException {
+        return NativeCrypto.ENGINE_SSL_write_direct(
+                ssl, sourceAddress, sourceLength, handshakeCallbacks);
+    }
+
+    int writeArray(byte[] sourceJava, int sourceOffset, int sourceLength) throws IOException {
+        return NativeCrypto.ENGINE_SSL_write_heap(
+                ssl, sourceJava, sourceOffset, sourceLength, handshakeCallbacks);
+    }
+
+    int getPendingReadableBytes() {
+        return NativeCrypto.SSL_pending_readable_bytes(ssl);
+    }
+
+    int getMaxSealOverhead() {
+        return NativeCrypto.SSL_max_seal_overhead(ssl);
+    }
+
+    void close() {
+        NativeCrypto.SSL_free(ssl);
+        ssl = 0L;
+    }
+
+    boolean isClosed() {
+        return ssl == 0L;
+    }
+
+    int getError(int result) {
+        return NativeCrypto.SSL_get_error(ssl, result);
+    }
+
+    byte[] getAlpnSelectedProtocol() {
+        return NativeCrypto.SSL_get0_alpn_selected(ssl);
+    }
+
+    private boolean isClient() {
+        return parameters.getUseClientMode();
+    }
+
+    /**
+     * A utility wrapper that abstracts operations on the underlying native BIO instance.
+     */
+    final class BioWrapper {
+        private long bio;
+
+        private BioWrapper() throws SSLException {
+            this.bio = NativeCrypto.SSL_BIO_new(ssl);
+        }
+
+        int getPendingWrittenBytes() {
+            return NativeCrypto.SSL_pending_written_bytes_in_BIO(bio);
+        }
+
+        int writeDirectByteBuffer(long address, int length) throws IOException {
+            return NativeCrypto.ENGINE_SSL_write_BIO_direct(
+                    ssl, bio, address, length, handshakeCallbacks);
+        }
+
+        int writeArray(byte[] sourceJava, int sourceOffset, int sourceLength) throws IOException {
+            return NativeCrypto.ENGINE_SSL_write_BIO_heap(
+                    ssl, bio, sourceJava, sourceOffset, sourceLength, handshakeCallbacks);
+        }
+
+        int readDirectByteBuffer(long destAddress, int destLength) throws IOException {
+            return NativeCrypto.ENGINE_SSL_read_BIO_direct(
+                    ssl, bio, destAddress, destLength, handshakeCallbacks);
+        }
+
+        int readArray(byte[] destJava, int destOffset, int destLength) throws IOException {
+            return NativeCrypto.ENGINE_SSL_read_BIO_heap(
+                    ssl, bio, destJava, destOffset, destLength, handshakeCallbacks);
+        }
+
+        void close() {
+            NativeCrypto.BIO_free_all(bio);
+            bio = 0L;
+        }
+    }
+}
diff --git a/constants/src/gen/cpp/generate_constants.cpp b/constants/src/gen/cpp/generate_constants.cpp
index 92b5c7f..5725abd 100644
--- a/constants/src/gen/cpp/generate_constants.cpp
+++ b/constants/src/gen/cpp/generate_constants.cpp
@@ -46,21 +46,8 @@
   printf("package org.conscrypt;\n\n");
   printf("final class NativeConstants {\n");
 
-  printf("    static final boolean HAS_EVP_AEAD = %s;\n",
-#if defined(EVP_AEAD_DEFAULT_TAG_LENGTH)
-         "true"
-#else
-         "false"
-#endif
-  );
-
 #define CONST(x) \
   printf("    static final int %s = %ld;\n", #x, (long int)(x))
-#define CONST_MINUS_1(x) printf("    static final int %s = -1;\n", #x)
-  CONST(OPENSSL_EC_NAMED_CURVE);
-
-  CONST(POINT_CONVERSION_COMPRESSED);
-  CONST(POINT_CONVERSION_UNCOMPRESSED);
 
   CONST(EXFLAG_CA);
   CONST(EXFLAG_CRITICAL);
@@ -79,7 +66,6 @@
 
   CONST(SSL_OP_CIPHER_SERVER_PREFERENCE);
   CONST(SSL_OP_NO_TICKET);
-  CONST(SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);
   CONST(SSL_OP_NO_SSLv3);
   CONST(SSL_OP_NO_TLSv1);
   CONST(SSL_OP_NO_TLSv1_1);
@@ -96,43 +82,10 @@
   CONST(TLS_CT_RSA_SIGN);
   CONST(TLS_CT_ECDSA_SIGN);
 
-#if defined(TLS_CT_RSA_FIXED_DH)
-  CONST(TLS_CT_RSA_FIXED_DH);
-#else
-  CONST_MINUS_1(TLS_CT_RSA_FIXED_DH);
-#endif
-#if defined(TLS_CT_RSA_FIXED_ECDH)
-  CONST(TLS_CT_RSA_FIXED_ECDH);
-#else
-  CONST_MINUS_1(TLS_CT_RSA_FIXED_ECDH);
-#endif
-#if defined(TLS_CT_ECDSA_FIXED_ECDH)
-  CONST(TLS_CT_ECDSA_FIXED_ECDH);
-#else
-  CONST_MINUS_1(TLS_CT_ECDSA_FIXED_ECDH);
-#endif
-
   CONST(SSL_VERIFY_NONE);
   CONST(SSL_VERIFY_PEER);
   CONST(SSL_VERIFY_FAIL_IF_NO_PEER_CERT);
 
-  CONST(SSL_ST_CONNECT);
-  CONST(SSL_ST_ACCEPT);
-  CONST(SSL_ST_MASK);
-  CONST(SSL_ST_INIT);
-  CONST(SSL_ST_OK);
-  CONST(SSL_ST_RENEGOTIATE);
-  CONST(SSL_CB_LOOP);
-  CONST(SSL_CB_EXIT);
-  CONST(SSL_CB_READ);
-  CONST(SSL_CB_WRITE);
-  CONST(SSL_CB_ALERT);
-  CONST(SSL_CB_READ_ALERT);
-  CONST(SSL_CB_WRITE_ALERT);
-  CONST(SSL_CB_ACCEPT_LOOP);
-  CONST(SSL_CB_ACCEPT_EXIT);
-  CONST(SSL_CB_CONNECT_LOOP);
-  CONST(SSL_CB_CONNECT_EXIT);
   CONST(SSL_CB_HANDSHAKE_START);
   CONST(SSL_CB_HANDSHAKE_DONE);
 
diff --git a/libcore-stub/build.gradle b/libcore-stub/build.gradle
index 92e5344..d1927de 100644
--- a/libcore-stub/build.gradle
+++ b/libcore-stub/build.gradle
@@ -1,12 +1,23 @@
 description = 'Conscrypt: libcore Stub'
 
-dependencies {
-    // Only compile against this. Other modules will embed the generated code directly.
-    compileOnly project(':conscrypt-constants')
+configurations {
+    publicApiDocs
+}
 
-    compile libraries.bouncycastle_provider,
-            libraries.bouncycastle_apis,
-            libraries.junit
+dependencies {
+    // This is used for the @hide annotation processing in JavaDoc
+    publicApiDocs project(':conscrypt-api-doclet')
+
+    // Only compile against this. Other modules will embed the generated code directly.
+    compileOnly project(':conscrypt-constants'),
+                configurations.publicApiDocs
+
+    compile libraries.junit
+}
+
+javadoc {
+    options.doclet = "org.conscrypt.doclet.FilterDoclet"
+    options.docletpath = configurations.publicApiDocs.files as List
 }
 
 // Don't include this artifact in the distribution.
diff --git a/openjdk-benchmarks/build.gradle b/openjdk-benchmarks/build.gradle
deleted file mode 100644
index 73a0809..0000000
--- a/openjdk-benchmarks/build.gradle
+++ /dev/null
@@ -1,58 +0,0 @@
-plugins {
-    id 'me.champeau.gradle.jmh' version '0.3.1'
-}
-
-apply plugin: 'idea'
-
-description = 'Conscrypt: OpenJDK Benchmarks'
-
-evaluationDependsOn(':conscrypt-openjdk')
-
-def preferredNativeConfiguration = project(':conscrypt-openjdk').preferredNativeConfiguration
-def preferredNativeFileDir = project(':conscrypt-openjdk').preferredNativeFileDir
-
-sourceSets {
-    main {
-        resources {
-            // This shouldn't be needed but seems to help IntelliJ locate the native artifact.
-            srcDirs += preferredNativeFileDir
-        }
-    }
-}
-
-jmh {
-    jmhVersion = "$jmhVersion"
-    warmupIterations = 10
-    iterations = 10
-    fork = 1
-    jvmArgs = '-server -Xms2g -Xmx2g'
-    duplicateClassesStrategy = 'warn'
-}
-
-configurations {
-    jmhGenerators
-}
-
-dependencies {
-    compile project(':conscrypt-openjdk'),
-            project(':conscrypt-testing'),
-            libraries.junit,
-            libraries.netty_handler,
-            libraries.netty_tcnative
-
-    // Add the preferred native openjdk configuration for this platform.
-    compile project(path: ':conscrypt-openjdk', configuration: "$preferredNativeConfiguration")
-
-    jmh libraries.jmh_core
-
-    jmhGenerators libraries.jmh_generator_asm,
-            libraries.jmh_generator_bytecode,
-            libraries.jmh_generator_reflection,
-            libraries.jmh_generator_annprocess
-}
-
-// Running benchmarks in IntelliJ seems broken without this.
-// See https://github.com/melix/jmh-gradle-plugin/issues/39
-idea.module {
-    scopes.PROVIDED.plus += [ configurations.jmh, configurations.jmhGenerators ]
-}
diff --git a/openjdk-benchmarks/src/jmh/java/org/conscrypt/benchmarks/ClientSocketThroughputBenchmark.java b/openjdk-benchmarks/src/jmh/java/org/conscrypt/benchmarks/ClientSocketThroughputBenchmark.java
deleted file mode 100644
index d9db3f7..0000000
--- a/openjdk-benchmarks/src/jmh/java/org/conscrypt/benchmarks/ClientSocketThroughputBenchmark.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.conscrypt.benchmarks;
-
-import static org.conscrypt.TestUtils.LOCALHOST;
-import static org.conscrypt.TestUtils.getConscryptServerSocketFactory;
-import static org.conscrypt.TestUtils.getConscryptSocketFactory;
-import static org.conscrypt.TestUtils.getJdkServerSocketFactory;
-import static org.conscrypt.TestUtils.getJdkSocketFactory;
-import static org.conscrypt.TestUtils.getProtocols;
-import static org.conscrypt.TestUtils.newTextMessage;
-import static org.conscrypt.TestUtils.pickUnusedPort;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
-import javax.net.SocketFactory;
-import javax.net.ssl.SSLServerSocket;
-import javax.net.ssl.SSLServerSocketFactory;
-import javax.net.ssl.SSLSocket;
-import javax.net.ssl.SSLSocketFactory;
-import org.conscrypt.TestClient;
-import org.conscrypt.TestServer;
-import org.openjdk.jmh.annotations.AuxCounters;
-import org.openjdk.jmh.annotations.Benchmark;
-import org.openjdk.jmh.annotations.Fork;
-import org.openjdk.jmh.annotations.Level;
-import org.openjdk.jmh.annotations.Param;
-import org.openjdk.jmh.annotations.Scope;
-import org.openjdk.jmh.annotations.Setup;
-import org.openjdk.jmh.annotations.State;
-import org.openjdk.jmh.annotations.TearDown;
-
-/**
- * Benchmark for comparing performance of client socket implementations. All benchmarks use Netty
- * with tcnative as the server.
- */
-@State(Scope.Benchmark)
-@Fork(1)
-public class ClientSocketThroughputBenchmark {
-    /**
-     * Use an AuxCounter so we can measure that bytes per second as they accumulate without
-     * consuming CPU in the benchmark method.
-     */
-    @AuxCounters
-    @State(Scope.Thread)
-    public static class BytesPerSecondCounter {
-        @Setup(Level.Iteration)
-        public void clean() {
-            bytesCounter.set(0);
-        }
-
-        public long bytesPerSecond() {
-            return bytesCounter.get();
-        }
-    }
-
-    /**
-     * Various factories for SSL sockets.
-     */
-    public enum SslProvider {
-        JDK(getJdkSocketFactory(), getJdkServerSocketFactory()),
-        CONSCRYPT(getConscryptSocketFactory(false), getConscryptServerSocketFactory(false)),
-        CONSCRYPT_ENGINE(getConscryptSocketFactory(true), getConscryptServerSocketFactory(true)) {
-            @Override
-            SSLSocket newClientSocket(String host, int port, SSLSocketFactory socketFactory)  throws IOException {
-                return (SSLSocket) socketFactory.createSocket(
-                    SocketFactory.getDefault().createSocket(host, port), host, port, true);
-            }
-        };
-
-        private final SSLSocketFactory clientSocketFactory;
-        private final SSLServerSocketFactory serverSocketFactory;
-
-        SslProvider(SSLSocketFactory clientSocketFactory, SSLServerSocketFactory serverSocketFactory) {
-            this.clientSocketFactory = clientSocketFactory;
-            this.serverSocketFactory = serverSocketFactory;
-        }
-
-        final SSLSocket newClientSocket(String host, int port, String cipher) {
-            try {
-                SSLSocket sslSocket = newClientSocket(host, port, clientSocketFactory);
-                sslSocket.setEnabledProtocols(getProtocols());
-                sslSocket.setEnabledCipherSuites(new String[] {cipher});
-                return sslSocket;
-            } catch (Exception e) {
-                throw new RuntimeException(e);
-            }
-        }
-
-        SSLSocket newClientSocket(String host, int port, SSLSocketFactory socketFactory)  throws IOException {
-            return (SSLSocket) socketFactory.createSocket(host, port);
-        }
-
-        final SSLServerSocket newServerSocket(String cipher) {
-            try {
-                int port = pickUnusedPort();
-                SSLServerSocket sslSocket =
-                    (SSLServerSocket) serverSocketFactory.createServerSocket(port);
-                sslSocket.setEnabledProtocols(getProtocols());
-                sslSocket.setEnabledCipherSuites(new String[] {cipher});
-                return sslSocket;
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
-        }
-    }
-
-    @Param public SslProvider sslProvider;
-
-    @Param({"64", "1024"}) public int messageSize;
-
-    @Param({"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}) public String cipher;
-
-    private TestClient client;
-    private TestServer server;
-    private byte[] message;
-    private ExecutorService executor;
-    private volatile boolean stopping;
-
-    private static final AtomicLong bytesCounter = new AtomicLong();
-    private AtomicBoolean recording = new AtomicBoolean();
-
-    @Setup(Level.Trial)
-    public void setup() throws Exception {
-        recording.set(false);
-
-        message = newTextMessage(messageSize);
-
-        server = new TestServer(sslProvider.newServerSocket(cipher), messageSize);
-        server.setMessageProcessor(new TestServer.MessageProcessor() {
-            @Override
-            public void processMessage(byte[] inMessage, int numBytes, OutputStream os) {
-                if (recording.get()) {
-                    // Server received a message, increment the count.
-                    bytesCounter.addAndGet(numBytes);
-                }
-            }
-        });
-        Future<?> connectedFuture = server.start();
-
-        client = new TestClient(sslProvider.newClientSocket(LOCALHOST, server.port(), cipher));
-        client.start();
-
-        // Wait for the initial connection to complete.
-        connectedFuture.get(5, TimeUnit.SECONDS);
-
-        executor = Executors.newSingleThreadExecutor();
-        executor.submit(new Runnable() {
-            @Override
-            public void run() {
-                Thread thread = Thread.currentThread();
-                while (!stopping && !thread.isInterrupted()) {
-                    client.sendMessage(message);
-                }
-            }
-        });
-    }
-
-    @TearDown(Level.Trial)
-    public void teardown() throws Exception {
-        stopping = true;
-        client.stop();
-        server.stop();
-        executor.shutdown();
-        executor.awaitTermination(5, TimeUnit.SECONDS);
-    }
-
-    @Benchmark
-    public final void throughput(BytesPerSecondCounter counter) throws Exception {
-        recording.set(true);
-        // No need to do anything, just sleep here.
-        Thread.sleep(1001);
-        recording.set(false);
-    }
-}
diff --git a/openjdk-benchmarks/src/jmh/java/org/conscrypt/benchmarks/ServerSocketThroughputBenchmark.java b/openjdk-benchmarks/src/jmh/java/org/conscrypt/benchmarks/ServerSocketThroughputBenchmark.java
deleted file mode 100644
index 8778da9..0000000
--- a/openjdk-benchmarks/src/jmh/java/org/conscrypt/benchmarks/ServerSocketThroughputBenchmark.java
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.conscrypt.benchmarks;
-
-import static org.conscrypt.TestUtils.LOCALHOST;
-import static org.conscrypt.TestUtils.getConscryptServerSocketFactory;
-import static org.conscrypt.TestUtils.getJdkServerSocketFactory;
-import static org.conscrypt.TestUtils.getJdkSocketFactory;
-import static org.conscrypt.TestUtils.getProtocols;
-import static org.conscrypt.TestUtils.newTextMessage;
-import static org.conscrypt.TestUtils.pickUnusedPort;
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
-import javax.net.ssl.SSLServerSocket;
-import javax.net.ssl.SSLServerSocketFactory;
-import javax.net.ssl.SSLSocket;
-import javax.net.ssl.SSLSocketFactory;
-import org.conscrypt.TestClient;
-import org.conscrypt.TestServer;
-import org.conscrypt.TestServer.MessageProcessor;
-import org.openjdk.jmh.annotations.AuxCounters;
-import org.openjdk.jmh.annotations.Benchmark;
-import org.openjdk.jmh.annotations.Level;
-import org.openjdk.jmh.annotations.Param;
-import org.openjdk.jmh.annotations.Scope;
-import org.openjdk.jmh.annotations.Setup;
-import org.openjdk.jmh.annotations.State;
-import org.openjdk.jmh.annotations.TearDown;
-
-/**
- * Benchmark for comparing performance of server socket implementations. All benchmarks use the
- * standard JDK TLS implementation.
- */
-@State(Scope.Benchmark)
-public class ServerSocketThroughputBenchmark {
-    /**
-     * Use an AuxCounter so we can measure that bytes per second as they accumulate without
-     * consuming CPU in the benchmark method.
-     */
-    @AuxCounters
-    @State(Scope.Thread)
-    public static class BytesPerSecondCounter {
-        @Setup(Level.Iteration)
-        public void clean() {
-            bytesCounter.set(0);
-        }
-
-        public long bytesPerSecond() {
-            return bytesCounter.get();
-        }
-    }
-
-    /**
-     * Various factories for SSL server sockets.
-     */
-    public enum SslProvider {
-        JDK(getJdkServerSocketFactory()),
-        CONSCRYPT(getConscryptServerSocketFactory(false)),
-        CONSCRYPT_ENGINE(getConscryptServerSocketFactory(true));
-
-        private final SSLServerSocketFactory serverSocketFactory;
-
-        SslProvider(SSLServerSocketFactory serverSocketFactory) {
-            this.serverSocketFactory = serverSocketFactory;
-        }
-
-        final SSLServerSocket newServerSocket(String cipher) {
-            try {
-                int port = pickUnusedPort();
-                SSLServerSocket sslSocket =
-                        (SSLServerSocket) serverSocketFactory.createServerSocket(port);
-                sslSocket.setEnabledProtocols(getProtocols());
-                sslSocket.setEnabledCipherSuites(new String[] {cipher});
-                return sslSocket;
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
-        }
-    }
-
-    @Param public SslProvider sslProvider;
-
-    @Param({"64", "1024"}) public int messageSize;
-
-    @Param({"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}) public String cipher;
-
-    private TestClient client;
-    private TestServer server;
-    private byte[] message;
-    private ExecutorService executor;
-    private volatile boolean stopping;
-    private static final AtomicLong bytesCounter = new AtomicLong();
-    private AtomicBoolean recording = new AtomicBoolean();
-
-    @Setup
-    public void setup() throws Exception {
-        recording.set(false);
-
-        message = newTextMessage(messageSize);
-
-        server = new TestServer(sslProvider.newServerSocket(cipher), messageSize);
-        server.setMessageProcessor(new MessageProcessor() {
-            @Override
-            public void processMessage(byte[] inMessage, int numBytes, OutputStream os) {
-                try {
-                    while (!stopping) {
-                        os.write(inMessage, 0, numBytes);
-                    }
-                } catch (IOException e) {
-                    throw new RuntimeException(e);
-                }
-            }
-        });
-
-        Future<?> connectedFuture = server.start();
-
-        SSLSocket socket;
-        try {
-            SSLSocketFactory socketFactory = getJdkSocketFactory();
-            socket = (SSLSocket) socketFactory.createSocket(LOCALHOST, server.port());
-            socket.setEnabledProtocols(getProtocols());
-            socket.setEnabledCipherSuites(new String[] {cipher});
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
-
-        client = new TestClient(socket);
-        client.start();
-
-        // Wait for the initial connection to complete.
-        connectedFuture.get(5, TimeUnit.SECONDS);
-
-        // Start the server-side streaming by sending a message to the server.
-        client.sendMessage(message);
-        client.flush();
-
-        executor = Executors.newSingleThreadExecutor();
-        executor.submit(new Runnable() {
-            @Override
-            public void run() {
-                Thread thread = Thread.currentThread();
-                byte[] buffer = new byte[messageSize];
-                while (!stopping && !thread.isInterrupted()) {
-                    int numBytes = client.readMessage(buffer);
-                    assertEquals(messageSize, numBytes);
-
-                    // Increment the message counter if we're recording.
-                    if (recording.get()) {
-                        bytesCounter.addAndGet(numBytes);
-                    }
-                }
-            }
-        });
-    }
-
-    @TearDown
-    public void teardown() throws Exception {
-        stopping = true;
-        client.stop();
-        server.stop();
-        executor.shutdown();
-        executor.awaitTermination(5, TimeUnit.SECONDS);
-    }
-
-    @Benchmark
-    public final void throughput(BytesPerSecondCounter counter) throws Exception {
-        recording.set(true);
-        // No need to do anything, just sleep here.
-        Thread.sleep(1001);
-        recording.set(false);
-    }
-}
diff --git a/openjdk-benchmarks/src/jmh/java/org/conscrypt/benchmarks/SslEngineBenchmark.java b/openjdk-benchmarks/src/jmh/java/org/conscrypt/benchmarks/SslEngineBenchmark.java
deleted file mode 100644
index 679dec1..0000000
--- a/openjdk-benchmarks/src/jmh/java/org/conscrypt/benchmarks/SslEngineBenchmark.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed 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.
- */
-
-/*
- * Copyright 2017 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 org.conscrypt.benchmarks;
-
-import static java.lang.Math.max;
-import static org.conscrypt.TestUtils.PROTOCOL_TLS_V1_2;
-import static org.conscrypt.TestUtils.doEngineHandshake;
-import static org.conscrypt.TestUtils.initClientSslContext;
-import static org.conscrypt.TestUtils.initEngine;
-import static org.conscrypt.TestUtils.initServerSslContext;
-import static org.conscrypt.TestUtils.newTextMessage;
-import static org.junit.Assert.assertEquals;
-
-import io.netty.buffer.UnpooledByteBufAllocator;
-import io.netty.handler.ssl.SslContext;
-import io.netty.handler.ssl.SslContextBuilder;
-import java.nio.ByteBuffer;
-import java.security.KeyStore.PrivateKeyEntry;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.X509Certificate;
-import java.util.Collections;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLEngineResult;
-import javax.net.ssl.SSLException;
-import libcore.java.security.TestKeyStore;
-import org.conscrypt.OpenSSLProvider;
-import org.openjdk.jmh.annotations.Benchmark;
-import org.openjdk.jmh.annotations.Param;
-import org.openjdk.jmh.annotations.Scope;
-import org.openjdk.jmh.annotations.Setup;
-import org.openjdk.jmh.annotations.State;
-
-/**
- * Benchmark comparing performance of various engine implementations to conscrypt.
- */
-@State(Scope.Benchmark)
-public class SslEngineBenchmark {
-    public enum SslProvider {
-        JDK {
-            private final SSLContext clientContext = initClientSslContext(newContext());
-            private final SSLContext serverContext = initServerSslContext(newContext());
-
-            @Override
-            SSLEngine newClientEngine(String cipher) {
-                return initEngine(clientContext.createSSLEngine(), cipher, true);
-            }
-
-            @Override
-            SSLEngine newServerEngine(String cipher) {
-                return initEngine(serverContext.createSSLEngine(), cipher, false);
-            }
-
-            private SSLContext newContext() {
-                try {
-                    return SSLContext.getInstance(PROTOCOL_TLS_V1_2);
-                } catch (NoSuchAlgorithmException e) {
-                    throw new RuntimeException(e);
-                }
-            }
-        },
-        CONSCRYPT {
-            private final SSLContext clientContext = initClientSslContext(newContext());
-            private final SSLContext serverContext = initServerSslContext(newContext());
-
-            @Override
-            SSLEngine newClientEngine(String cipher) {
-                return initEngine(clientContext.createSSLEngine(), cipher, true);
-            }
-
-            @Override
-            SSLEngine newServerEngine(String cipher) {
-                return initEngine(serverContext.createSSLEngine(), cipher, false);
-            }
-
-            private SSLContext newContext() {
-                try {
-                    return SSLContext.getInstance(PROTOCOL_TLS_V1_2, new OpenSSLProvider());
-                } catch (NoSuchAlgorithmException e) {
-                    throw new RuntimeException(e);
-                }
-            }
-        },
-        NETTY {
-            private final SslContext clientContext = newClientContext(null);
-            private final SslContext serverContext = newServerContext(null);
-
-            @Override
-            SSLEngine newClientEngine(String cipher) {
-                return initEngine(
-                        clientContext.newEngine(UnpooledByteBufAllocator.DEFAULT), cipher, true);
-            }
-
-            @Override
-            SSLEngine newServerEngine(String cipher) {
-                return initEngine(
-                        serverContext.newEngine(UnpooledByteBufAllocator.DEFAULT), cipher, false);
-            }
-        };
-
-        abstract SSLEngine newClientEngine(String cipher);
-        abstract SSLEngine newServerEngine(String cipher);
-    }
-
-    public enum BufferType {
-        HEAP {
-            @Override
-            ByteBuffer newBuffer(int size) {
-                return ByteBuffer.allocate(size);
-            }
-        },
-        DIRECT {
-            @Override
-            ByteBuffer newBuffer(int size) {
-                return ByteBuffer.allocateDirect(size);
-            }
-        };
-
-        abstract ByteBuffer newBuffer(int size);
-    }
-
-    @Param public SslProvider sslProvider;
-
-    @Param public BufferType bufferType;
-
-    @Param({"64", "128", "512", "1024", "4096"}) public int messageSize;
-
-    @Param({"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}) public String cipher;
-
-    private SSLEngine clientEngine;
-    private SSLEngine serverEngine;
-
-    private ByteBuffer clientCleartextBuffer;
-    private ByteBuffer encryptedBuffer;
-    private ByteBuffer serverCleartextBuffer;
-
-    @Setup
-    public void setup() throws Exception {
-        clientEngine = sslProvider.newClientEngine(cipher);
-        serverEngine = sslProvider.newServerEngine(cipher);
-
-        encryptedBuffer = bufferType.newBuffer(clientEngine.getSession().getPacketBufferSize());
-
-        // Generate the message to be sent from the client.
-        serverCleartextBuffer = bufferType.newBuffer(
-                max(messageSize, serverEngine.getSession().getApplicationBufferSize()));
-        clientCleartextBuffer = bufferType.newBuffer(messageSize);
-        clientCleartextBuffer.put(newTextMessage(messageSize));
-        clientCleartextBuffer.flip();
-
-        // Complete the initial TLS handshake.
-        doEngineHandshake(clientEngine, serverEngine);
-    }
-
-    private static SslContext newClientContext(String cipher) {
-        try {
-            TestKeyStore server = TestKeyStore.getServer();
-            SslContextBuilder ctx =
-                    SslContextBuilder.forClient()
-                            .sslProvider(io.netty.handler.ssl.SslProvider.OPENSSL)
-                            .trustManager((X509Certificate[]) server.getPrivateKey("RSA", "RSA")
-                                                  .getCertificateChain());
-            if (cipher != null) {
-                ctx.ciphers(Collections.singletonList(cipher));
-            }
-            return ctx.build();
-        } catch (SSLException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    private static SslContext newServerContext(String cipher) {
-        try {
-            PrivateKeyEntry server = TestKeyStore.getServer().getPrivateKey("RSA", "RSA");
-            SslContextBuilder ctx =
-                    SslContextBuilder
-                            .forServer(server.getPrivateKey(),
-                                    (X509Certificate[]) server.getCertificateChain())
-                           .sslProvider(io.netty.handler.ssl.SslProvider.OPENSSL);
-            if (cipher != null) {
-                ctx.ciphers(Collections.singletonList(cipher));
-            }
-            return ctx.build();
-        } catch (SSLException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-
-    /**
-     * Simple benchmark that sends a single message from client to server.
-     */
-    @Benchmark
-    public void sendMessage() throws SSLException {
-        // Reset the buffers.
-        clientCleartextBuffer.position(0);
-        encryptedBuffer.clear();
-        serverCleartextBuffer.clear();
-
-        // Wrap the original message and create the encrypted data.
-        SSLEngineResult wrapResult = clientEngine.wrap(clientCleartextBuffer, encryptedBuffer);
-        if (wrapResult.getStatus() != SSLEngineResult.Status.OK) {
-            throw new RuntimeException("Wrap returned unexpected result " + wrapResult);
-        }
-
-        // Unwrap the encrypted data and get back the original result.
-        encryptedBuffer.flip();
-        SSLEngineResult unwrapResult = serverEngine.unwrap(encryptedBuffer, serverCleartextBuffer);
-        if (unwrapResult.getStatus() != SSLEngineResult.Status.OK) {
-            throw new RuntimeException("Unwrap returned unexpected result " + wrapResult);
-        }
-        serverCleartextBuffer.flip();
-
-        // Lightweight comparison - just make sure the unencrypted data length is correct.
-        assertEquals(clientCleartextBuffer.limit(), serverCleartextBuffer.limit());
-    }
-}
diff --git a/openjdk-integ-tests/build.gradle b/openjdk-integ-tests/build.gradle
index a4ca546..fb55ab6 100644
--- a/openjdk-integ-tests/build.gradle
+++ b/openjdk-integ-tests/build.gradle
@@ -24,6 +24,18 @@
                 project(':conscrypt-testing')
 }
 
+// Add a second round of tests using the engine-based socket.
+task testEngineSocket(type: Test) {
+    dependsOn testClasses
+    // Use the engine socket by default.
+    jvmArgs "-Dorg.conscrypt.useEngineSocketByDefault=true"
+    testClassesDir = test.testClassesDir
+    doFirst {
+        classpath = test.classpath
+    }
+}
+test.dependsOn testEngineSocket
+
 // Don't include this artifact in the distribution.
 tasks.install.enabled = false
 tasks.uploadArchives.enabled = false;
diff --git a/openjdk-integ-tests/src/test/java/libcore/javax/net/ssl/SSLSessionContextTest.java b/openjdk-integ-tests/src/test/java/libcore/javax/net/ssl/SSLSessionContextTest.java
index 72de22f..91f00ba 100644
--- a/openjdk-integ-tests/src/test/java/libcore/javax/net/ssl/SSLSessionContextTest.java
+++ b/openjdk-integ-tests/src/test/java/libcore/javax/net/ssl/SSLSessionContextTest.java
@@ -39,14 +39,13 @@
 
 @RunWith(JUnit4.class)
 public class SSLSessionContextTest extends AbstractSSLTest {
-
     @Test
     public void test_SSLSessionContext_getIds() {
         TestSSLContext c = TestSSLContext.create();
         assertSSLSessionContextSize(0, c);
         c.close();
 
-        TestSSLSocketPair s = TestSSLSocketPair.create();
+        TestSSLSocketPair s = TestSSLSocketPair.create().connect();
         assertSSLSessionContextSize(1, s.c);
         Enumeration<byte[]> clientIds = s.c.clientContext.getClientSessionContext().getIds();
         Enumeration<byte[]> serverIds = s.c.serverContext.getServerSessionContext().getIds();
@@ -83,7 +82,7 @@
         assertNull(c.serverContext.getServerSessionContext().getSession(new byte[1]));
         c.close();
 
-        TestSSLSocketPair s = TestSSLSocketPair.create();
+        TestSSLSocketPair s = TestSSLSocketPair.create().connect();
         SSLSessionContext client = s.c.clientContext.getClientSessionContext();
         SSLSessionContext server = s.c.serverContext.getServerSessionContext();
         byte[] clientId = client.getIds().nextElement();
@@ -110,7 +109,7 @@
                 c.serverContext.getServerSessionContext().getSessionCacheSize());
         c.close();
 
-        TestSSLSocketPair s = TestSSLSocketPair.create();
+        TestSSLSocketPair s = TestSSLSocketPair.create().connect();
         assertEquals(expectedClientSessionCacheSize,
                 s.c.clientContext.getClientSessionContext().getSessionCacheSize());
         assertEquals(expectedServerSessionCacheSize,
@@ -124,11 +123,9 @@
         int expectedClientSessionCacheSize = expectedClientSslSessionCacheSize(c);
         int expectedServerSessionCacheSize = expectedServerSslSessionCacheSize(c);
         assertNoConnectSetSessionCacheSizeBehavior(
-                expectedClientSessionCacheSize,
-                c.clientContext.getClientSessionContext());
+                expectedClientSessionCacheSize, c.clientContext.getClientSessionContext());
         assertNoConnectSetSessionCacheSizeBehavior(
-                expectedServerSessionCacheSize,
-                c.serverContext.getServerSessionContext());
+                expectedServerSessionCacheSize, c.serverContext.getServerSessionContext());
         c.close();
     }
 
@@ -147,15 +144,13 @@
 
     @Test
     public void test_SSLSessionContext_setSessionCacheSize_oneConnect() {
-        TestSSLSocketPair s = TestSSLSocketPair.create();
+        TestSSLSocketPair s = TestSSLSocketPair.create().connect();
         int expectedClientSessionCacheSize = expectedClientSslSessionCacheSize(s.c);
         int expectedServerSessionCacheSize = expectedServerSslSessionCacheSize(s.c);
         SSLSessionContext client = s.c.clientContext.getClientSessionContext();
         SSLSessionContext server = s.c.serverContext.getServerSessionContext();
-        assertEquals(expectedClientSessionCacheSize,
-                client.getSessionCacheSize());
-        assertEquals(expectedServerSessionCacheSize,
-                server.getSessionCacheSize());
+        assertEquals(expectedClientSessionCacheSize, client.getSessionCacheSize());
+        assertEquals(expectedServerSessionCacheSize, server.getSessionCacheSize());
         assertSSLSessionContextSize(1, s.c);
         s.close();
     }
@@ -212,11 +207,14 @@
         String cipherSuite3 = uniqueCipherSuites.get(2);
 
         List<SSLSocket[]> toClose = new ArrayList<>();
-        toClose.add(TestSSLSocketPair.connect(c, new String[] {cipherSuite1}, null));
+        toClose.add(
+                TestSSLSocketPair.create(c).connect(new String[] {cipherSuite1}, null).sockets());
         assertSSLSessionContextSize(1, c);
-        toClose.add(TestSSLSocketPair.connect(c, new String[] {cipherSuite2}, null));
+        toClose.add(
+                TestSSLSocketPair.create(c).connect(new String[] {cipherSuite2}, null).sockets());
         assertSSLSessionContextSize(2, c);
-        toClose.add(TestSSLSocketPair.connect(c, new String[] {cipherSuite3}, null));
+        toClose.add(
+                TestSSLSocketPair.create(c).connect(new String[] {cipherSuite3}, null).sockets());
         assertSSLSessionContextSize(3, c);
 
         client.setSessionCacheSize(1);
@@ -224,14 +222,17 @@
         assertEquals(1, client.getSessionCacheSize());
         assertEquals(1, server.getSessionCacheSize());
         assertSSLSessionContextSize(1, c);
-        toClose.add(TestSSLSocketPair.connect(c, new String[] {cipherSuite1}, null));
+        toClose.add(
+                TestSSLSocketPair.create(c).connect(new String[] {cipherSuite1}, null).sockets());
         assertSSLSessionContextSize(1, c);
 
         client.setSessionCacheSize(2);
         server.setSessionCacheSize(2);
-        toClose.add(TestSSLSocketPair.connect(c, new String[] {cipherSuite2}, null));
+        toClose.add(
+                TestSSLSocketPair.create(c).connect(new String[] {cipherSuite2}, null).sockets());
         assertSSLSessionContextSize(2, c);
-        toClose.add(TestSSLSocketPair.connect(c, new String[] {cipherSuite3}, null));
+        toClose.add(
+                TestSSLSocketPair.create(c).connect(new String[] {cipherSuite3}, null).sockets());
         assertSSLSessionContextSize(2, c);
 
         for (SSLSocket[] pair : toClose) {
@@ -252,7 +253,7 @@
                 c.serverContext.getServerSessionContext().getSessionTimeout());
         c.close();
 
-        TestSSLSocketPair s = TestSSLSocketPair.create();
+        TestSSLSocketPair s = TestSSLSocketPair.create().connect();
         assertEquals(expectedCacheTimeout,
                 s.c.clientContext.getClientSessionContext().getSessionTimeout());
         assertEquals(expectedCacheTimeout,
@@ -287,7 +288,7 @@
         }
         c.close();
 
-        TestSSLSocketPair s = TestSSLSocketPair.create();
+        TestSSLSocketPair s = TestSSLSocketPair.create().connect();
         assertSSLSessionContextSize(1, s.c);
         Thread.sleep(1000);
         s.c.clientContext.getClientSessionContext().setSessionTimeout(1);
@@ -311,11 +312,10 @@
 
     private static void assertSSLSessionContextSize(
             int expected, SSLSessionContext s, boolean server) {
-        int size = Collections.list(s.getIds()).size();
         if (server && TestSSLContext.sslServerSocketSupportsSessionTickets()) {
-            assertEquals(0, size);
+            assertEquals(0, numSessions(s));
         } else {
-            assertEquals(expected, size);
+            assertEquals(expected, numSessions(s));
         }
     }
 
@@ -331,6 +331,10 @@
         return (isConscrypt(c.serverContext.getProvider())) ? 8 * 3600 : 24 * 3600;
     }
 
+    private static int numSessions(SSLSessionContext s) {
+        return Collections.list(s.getIds()).size();
+    }
+
     private boolean isConscrypt(Provider provider) {
         return "AndroidOpenSSL".equals(provider.getName());
     }
diff --git a/openjdk-integ-tests/src/test/java/libcore/javax/net/ssl/SSLSocketTest.java b/openjdk-integ-tests/src/test/java/libcore/javax/net/ssl/SSLSocketTest.java
index e0c3656..928a50a 100644
--- a/openjdk-integ-tests/src/test/java/libcore/javax/net/ssl/SSLSocketTest.java
+++ b/openjdk-integ-tests/src/test/java/libcore/javax/net/ssl/SSLSocketTest.java
@@ -25,7 +25,8 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeNotNull;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeNoException;
 import static org.junit.Assume.assumeTrue;
 
 import java.io.ByteArrayInputStream;
@@ -94,6 +95,7 @@
 import javax.net.ssl.SNIHostName;
 import javax.net.ssl.SNIServerName;
 import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.SSLParameters;
@@ -121,7 +123,6 @@
 import libcore.tlswire.util.TlsProtocolVersion;
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -151,12 +152,14 @@
         SSLConfigurationAsserts.assertSSLSocketDefaultConfiguration(
                 (SSLSocket) SSLSocketFactory.getDefault().createSocket());
     }
+
     @Test
     public void test_SSLSocket_getSupportedCipherSuites_returnsCopies() throws Exception {
         SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
         SSLSocket ssl = (SSLSocket) sf.createSocket();
         assertNotSame(ssl.getSupportedCipherSuites(), ssl.getSupportedCipherSuites());
     }
+
     @Test
     public void test_SSLSocket_getSupportedCipherSuites_connect() throws Exception {
         // note the rare usage of non-RSA keys
@@ -183,6 +186,16 @@
                     @Override
                     protected SecretKey getKey(
                             String identityHint, String identity, Socket socket) {
+                        return newKey();
+                    }
+
+                    @Override
+                    protected SecretKey getKey(
+                            String identityHint, String identity, SSLEngine engine) {
+                        return newKey();
+                    }
+
+                    private SecretKey newKey() {
                         return new SecretKeySpec("Just an arbitrary key".getBytes(UTF_8), "RAW");
                     }
                 });
@@ -221,10 +234,10 @@
                 }
                 String[] clientCipherSuiteArray =
                         new String[] {cipherSuite, StandardNames.CIPHER_SUITE_SECURE_RENEGOTIATION};
-                SSLSocket[] pair = TestSSLSocketPair.connect(
-                        c, clientCipherSuiteArray, clientCipherSuiteArray);
-                SSLSocket server = pair[0];
-                SSLSocket client = pair[1];
+                TestSSLSocketPair socketPair = TestSSLSocketPair.create(c).connect(
+                        clientCipherSuiteArray, clientCipherSuiteArray);
+                SSLSocket server = socketPair.server;
+                SSLSocket client = socketPair.client;
                 // Check that the client can read the message sent by the server
                 server.getOutputStream().write(serverToClient);
                 byte[] clientFromServer = new byte[serverToClient.length];
@@ -265,12 +278,14 @@
         }
         c.close();
     }
+
     @Test
     public void test_SSLSocket_getEnabledCipherSuites_returnsCopies() throws Exception {
         SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
         SSLSocket ssl = (SSLSocket) sf.createSocket();
         assertNotSame(ssl.getEnabledCipherSuites(), ssl.getEnabledCipherSuites());
     }
+
     @Test
     public void test_SSLSocket_setEnabledCipherSuites_storesCopy() throws Exception {
         SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
@@ -281,6 +296,7 @@
         array[0] = "Modified after having been set";
         assertEquals(originalFirstElement, ssl.getEnabledCipherSuites()[0]);
     }
+
     @Test
     public void test_SSLSocket_setEnabledCipherSuites() throws Exception {
         SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
@@ -311,18 +327,21 @@
         ssl.setEnabledCipherSuites(cipherSuites);
         assertEquals(Arrays.asList(cipherSuites), Arrays.asList(ssl.getEnabledCipherSuites()));
     }
+
     @Test
     public void test_SSLSocket_getSupportedProtocols_returnsCopies() throws Exception {
         SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
         SSLSocket ssl = (SSLSocket) sf.createSocket();
         assertNotSame(ssl.getSupportedProtocols(), ssl.getSupportedProtocols());
     }
+
     @Test
     public void test_SSLSocket_getEnabledProtocols_returnsCopies() throws Exception {
         SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
         SSLSocket ssl = (SSLSocket) sf.createSocket();
         assertNotSame(ssl.getEnabledProtocols(), ssl.getEnabledProtocols());
     }
+
     @Test
     public void test_SSLSocket_setEnabledProtocols_storesCopy() throws Exception {
         SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
@@ -333,6 +352,7 @@
         array[0] = "Modified after having been set";
         assertEquals(originalFirstElement, ssl.getEnabledProtocols()[0]);
     }
+
     @Test
     public void test_SSLSocket_setEnabledProtocols() throws Exception {
         SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
@@ -375,6 +395,7 @@
             }
         }
     }
+
     @Test
     public void test_SSLSocket_getSession() throws Exception {
         SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
@@ -383,6 +404,7 @@
         assertNotNull(session);
         assertFalse(session.isValid());
     }
+
     @Test
     public void test_SSLSocket_getHandshakeSession() throws Exception {
         SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
@@ -390,6 +412,7 @@
         SSLSession session = ssl.getHandshakeSession();
         assertNull(session);
     }
+
     @Test
     public void test_SSLSocket_startHandshake() throws Exception {
         final TestSSLContext c = TestSSLContext.create();
@@ -410,8 +433,7 @@
             assertNotNull(localCertificates);
             TestKeyStore.assertChainLength(localCertificates);
             assertNotNull(localCertificates[0]);
-            TestSSLContext.assertServerCertificateChain(
-                    c.serverTrustManager, localCertificates);
+            TestSSLContext.assertServerCertificateChain(c.serverTrustManager, localCertificates);
             TestSSLContext.assertCertificateInKeyStore(localCertificates[0], c.serverKeyStore);
             return null;
         });
@@ -442,6 +464,7 @@
             return server.getSession().getId();
         }
     }
+
     @Test
     public void test_SSLSocket_confirmSessionReuse() throws Exception {
         final TestSSLContext c = TestSSLContext.create();
@@ -472,6 +495,7 @@
         assertTrue(Arrays.equals(clientSessionId1, clientSessionId2));
         c.close();
     }
+
     @Test
     public void test_SSLSocket_NoEnabledCipherSuites_Failure() throws Exception {
         TestSSLContext c = TestSSLContext.newBuilder()
@@ -503,13 +527,14 @@
         client.close();
         c.close();
     }
+
     @Test
     public void test_SSLSocket_startHandshake_noKeyStore() throws Exception {
         TestSSLContext c = TestSSLContext.newBuilder()
-                .useDefaults(false)
-                .clientContext(SSLContext.getDefault())
-                .serverContext(SSLContext.getDefault())
-                .build();
+                                   .useDefaults(false)
+                                   .clientContext(SSLContext.getDefault())
+                                   .serverContext(SSLContext.getDefault())
+                                   .build();
         SSLSocket client =
                 (SSLSocket) c.clientContext.getSocketFactory().createSocket(c.host, c.port);
         final SSLSocket server = (SSLSocket) c.serverSocket.accept();
@@ -533,6 +558,7 @@
         client.close();
         c.close();
     }
+
     @Test
     public void test_SSLSocket_startHandshake_noClientCertificate() throws Exception {
         TestSSLContext c = TestSSLContext.create();
@@ -550,6 +576,7 @@
         server.close();
         c.close();
     }
+
     @Test
     public void test_SSLSocket_HandshakeCompletedListener() throws Exception {
         final TestSSLContext c = TestSSLContext.create();
@@ -576,20 +603,16 @@
                 byte[] id = session.getId();
                 assertNotNull(id);
                 assertEquals(32, id.length);
-                assertNotNull(c.clientContext.getClientSessionContext().getSession(id));
                 assertNotNull(cipherSuite);
-                assertTrue(
-                        Arrays.asList(client.getEnabledCipherSuites()).contains(cipherSuite));
+                assertTrue(Arrays.asList(client.getEnabledCipherSuites()).contains(cipherSuite));
                 assertTrue(Arrays.asList(c.serverSocket.getEnabledCipherSuites())
                                    .contains(cipherSuite));
                 assertNull(localCertificates);
                 assertNotNull(peerCertificates);
                 TestKeyStore.assertChainLength(peerCertificates);
                 assertNotNull(peerCertificates[0]);
-                TestSSLContext.assertServerCertificateChain(
-                        c.clientTrustManager, peerCertificates);
-                TestSSLContext.assertCertificateInKeyStore(
-                        peerCertificates[0], c.serverKeyStore);
+                TestSSLContext.assertServerCertificateChain(c.clientTrustManager, peerCertificates);
+                TestSSLContext.assertCertificateInKeyStore(peerCertificates[0], c.serverKeyStore);
                 assertNotNull(peerCertificateChain);
                 TestKeyStore.assertChainLength(peerCertificateChain);
                 assertNotNull(peerCertificateChain[0]);
@@ -614,8 +637,8 @@
         });
         client.startHandshake();
         future.get();
-        assertNotNull(c.serverContext.getServerSessionContext().getSession(
-                    client.getSession().getId()));
+        assertNotNull(
+                c.serverContext.getServerSessionContext().getSession(client.getSession().getId()));
         synchronized (handshakeCompletedListenerCalled) {
             while (!handshakeCompletedListenerCalled[0]) {
                 handshakeCompletedListenerCalled.wait();
@@ -633,6 +656,7 @@
             actualException = ex;
         }
     }
+
     @Test
     public void test_SSLSocket_HandshakeCompletedListener_RuntimeException() throws Exception {
         final Thread self = Thread.currentThread();
@@ -648,9 +672,7 @@
             server.startHandshake();
             return null;
         });
-        client.addHandshakeCompletedListener(event -> {
-            throw expectedException;
-        });
+        client.addHandshakeCompletedListener(event -> { throw expectedException; });
         client.startHandshake();
         future.get();
         client.close();
@@ -659,6 +681,7 @@
         assertSame(expectedException, test.actualException);
         self.setUncaughtExceptionHandler(original);
     }
+
     @Test
     public void test_SSLSocket_getUseClientMode() throws Exception {
         TestSSLContext c = TestSSLContext.create();
@@ -671,27 +694,38 @@
         server.close();
         c.close();
     }
+
     @Test
-    public void test_SSLSocket_setUseClientMode() throws Exception {
-        // client is client, server is server
+    public void testClientMode_normal() throws Exception {
+        // Client is client and server is server.
         test_SSLSocket_setUseClientMode(true, false);
-        // client is server, server is client
-        test_SSLSocket_setUseClientMode(true, false);
-        // both are client
-        try {
-            test_SSLSocket_setUseClientMode(true, true);
-            fail();
-        } catch (SSLProtocolException | SSLHandshakeException expected) {
-            // Ignored.
-        }
-        // both are server
+    }
+
+    @Test(expected = SSLHandshakeException.class)
+    public void testClientMode_reverse() throws Exception {
+        // Client is server and server is client.
+        test_SSLSocket_setUseClientMode(false, true);
+    }
+
+    @Test(expected = SSLHandshakeException.class)
+    public void testClientMode_bothClient() throws Exception {
+        test_SSLSocket_setUseClientMode(true, true);
+    }
+
+    @Test
+    public void testClientMode_bothServer() throws Exception {
         try {
             test_SSLSocket_setUseClientMode(false, false);
             fail();
         } catch (SocketTimeoutException expected) {
-            // Ignored.
+            // Ignore
+        } catch (SSLHandshakeException expected) {
+            // Depending on the timing of the socket closures, this can happen as well.
+            assertTrue("Unexpected handshake error: " + expected.getMessage(),
+                    expected.getMessage().toLowerCase().contains("connection closed"));
         }
     }
+
     private void test_SSLSocket_setUseClientMode(
             final boolean clientClientMode, final boolean serverClientMode) throws Exception {
         TestSSLContext c = TestSSLContext.create();
@@ -723,10 +757,11 @@
         server.close();
         c.close();
     }
+
     @Test
     public void test_SSLSocket_setUseClientMode_afterHandshake() throws Exception {
         // can't set after handshake
-        TestSSLSocketPair pair = TestSSLSocketPair.create();
+        TestSSLSocketPair pair = TestSSLSocketPair.create().connect();
         try {
             pair.server.setUseClientMode(false);
             fail();
@@ -740,6 +775,7 @@
             // Ignored.
         }
     }
+
     @Test
     public void test_SSLSocket_untrustedServer() throws Exception {
         TestSSLContext c =
@@ -767,6 +803,7 @@
         server.close();
         c.close();
     }
+
     @Test
     public void test_SSLSocket_clientAuth() throws Exception {
         TestSSLContext c = TestSSLContext.create(
@@ -802,6 +839,7 @@
         server.close();
         c.close();
     }
+
     @Test
     public void test_SSLSocket_clientAuth_bogusAlias() throws Exception {
         TestSSLContext c = TestSSLContext.create();
@@ -861,14 +899,17 @@
         server.close();
         c.close();
     }
+
     @Test
     public void test_SSLSocket_clientAuth_OpaqueKey_RSA() throws Exception {
         run_SSLSocket_clientAuth_OpaqueKey(TestKeyStore.getClientCertificate());
     }
+
     @Test
     public void test_SSLSocket_clientAuth_OpaqueKey_EC_RSA() throws Exception {
         run_SSLSocket_clientAuth_OpaqueKey(TestKeyStore.getClientEcRsaCertificate());
     }
+
     @Test
     public void test_SSLSocket_clientAuth_OpaqueKey_EC_EC() throws Exception {
         run_SSLSocket_clientAuth_OpaqueKey(TestKeyStore.getClientEcEcCertificate());
@@ -1137,6 +1178,7 @@
             return delegate;
         }
     }
+
     @Test
     public void test_SSLSocket_TrustManagerRuntimeException() throws Exception {
         TestSSLContext c = TestSSLContext.create();
@@ -1181,6 +1223,7 @@
         server.close();
         c.close();
     }
+
     @Test
     public void test_SSLSocket_getEnableSessionCreation() throws Exception {
         TestSSLContext c = TestSSLContext.create();
@@ -1193,6 +1236,7 @@
         server.close();
         c.close();
     }
+
     @Test
     public void test_SSLSocket_setEnableSessionCreation_server() throws Exception {
         TestSSLContext c = TestSSLContext.create();
@@ -1220,6 +1264,7 @@
         server.close();
         c.close();
     }
+
     @Test
     public void test_SSLSocket_setEnableSessionCreation_client() throws Exception {
         TestSSLContext c = TestSSLContext.create();
@@ -1247,6 +1292,7 @@
         server.close();
         c.close();
     }
+
     @Test
     public void test_SSLSocket_getSSLParameters() throws Exception {
         SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
@@ -1269,6 +1315,7 @@
         p.setEndpointIdentificationAlgorithm("FOO");
         assertEquals("FOO", p.getEndpointIdentificationAlgorithm());
     }
+
     @Test
     public void test_SSLSocket_setSSLParameters() throws Exception {
         SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
@@ -1314,9 +1361,10 @@
             assertFalse(ssl.getWantClientAuth());
         }
     }
+
     @Test
     public void test_SSLSocket_close() throws Exception {
-        TestSSLSocketPair pair = TestSSLSocketPair.create();
+        TestSSLSocketPair pair = TestSSLSocketPair.create().connect();
         SSLSocket server = pair.server;
         SSLSocket client = pair.client;
         assertFalse(server.isClosed());
@@ -1433,12 +1481,14 @@
         // because the peer has closed, but it shouldn't throw.
         server.close();
     }
+
     @Test
     public void test_SSLSocket_endpointIdentification_Success() throws Exception {
         final TestSSLContext c = TestSSLContext.create();
         SSLSocket client = (SSLSocket) c.clientContext.getSocketFactory().createSocket();
         SSLParameters p = client.getSSLParameters();
         p.setEndpointIdentificationAlgorithm("HTTPS");
+        client.setSSLParameters(p);
         client.connect(new InetSocketAddress(c.host, c.port));
         final SSLSocket server = (SSLSocket) c.serverSocket.accept();
         Future<Void> future = runAsync(() -> {
@@ -1470,6 +1520,7 @@
         server.close();
         c.close();
     }
+
     @Test
     public void test_SSLSocket_endpointIdentification_Failure() throws Exception {
         final TestSSLContext c = TestSSLContext.create();
@@ -1503,6 +1554,7 @@
             }
         }
     }
+
     @Test
     public void test_SSLSocket_setSoTimeout_basic() throws Exception {
         ServerSocket listening = new ServerSocket(0);
@@ -1522,6 +1574,7 @@
         assertEquals(0, wrapping.getSoTimeout());
         assertEquals(0, underlying.getSoTimeout());
     }
+
     @Test
     public void test_SSLSocket_setSoTimeout_wrapper() throws Exception {
         ServerSocket listening = new ServerSocket(0);
@@ -1543,6 +1596,7 @@
         underlying.close();
         listening.close();
     }
+
     @Test(expected = SocketTimeoutException.class)
     public void test_SSLSocket_setSoWriteTimeout() throws Exception {
         // Only run this test on Linux since it relies on non-posix methods.
@@ -1557,9 +1611,6 @@
         SSLSocket client =
                 (SSLSocket) c.clientContext.getSocketFactory().createSocket(c.host, c.port);
 
-        Method writeTimeoutMethod = getWriteTimeoutSetter(client);
-        assumeNotNull("Client socket does not support setting write timeout", writeTimeoutMethod);
-
         // Try to make the client SO_SNDBUF size as small as possible
         // (it can default to 512k or even megabytes).  Note that
         // socket(7) says that the kernel will double the request to
@@ -1578,8 +1629,9 @@
         });
         server.startHandshake();
 
-        writeTimeoutMethod.invoke(client, 1);
         try {
+            setWriteTimeout(client, 1);
+
             // Add extra space to the write to exceed the send buffer
             // size and cause the write to block.
             final int extra = 1;
@@ -1592,37 +1644,45 @@
         }
     }
 
-    private static Method getWriteTimeoutSetter(Object socket) {
-        try {
-            return socket.getClass().getDeclaredMethod("setSoWriteTimeout", int.class);
-        } catch (Exception e) {
-            return null;
-        }
-    }
-
-    private static String osName() {
-        return System.getProperty("os.name").toLowerCase(Locale.US).replaceAll("[^a-z0-9]+", "");
-    }
-
-    private static boolean isLinux() {
-        return osName().startsWith("linux");
-    }
-
-    @Ignore("TODO(nmittler): Fix this.")
+    // TODO(nmittler): Conscrypt socket read may return -1 instead of SocketException.
     @Test
-    public void test_SSLSocket_interrupt() throws Exception {
+    public void test_SSLSocket_interrupt_readUnderlyingAndCloseUnderlying() throws Exception {
         test_SSLSocket_interrupt_case(true, true);
+    }
+
+    // TODO(nmittler): Conscrypt socket read may return -1 instead of SocketException.
+    @Test
+    public void test_SSLSocket_interrupt_readUnderlyingAndCloseWrapper() throws Exception {
         test_SSLSocket_interrupt_case(true, false);
+    }
+
+    // TODO(nmittler): FD socket gets stuck in read on Windows and OSX.
+    @Test
+    public void test_SSLSocket_interrupt_readWrapperAndCloseUnderlying() throws Exception {
         test_SSLSocket_interrupt_case(false, true);
+    }
+
+    // TODO(nmittler): Conscrypt socket read may return -1 instead of SocketException.
+    @Test
+    public void test_SSLSocket_interrupt_readWrapperAndCloseWrapper() throws Exception {
         test_SSLSocket_interrupt_case(false, false);
     }
+
     private void test_SSLSocket_interrupt_case(boolean readUnderlying, boolean closeUnderlying)
             throws Exception {
         final int readingTimeoutMillis = 5000;
         TestSSLContext c = TestSSLContext.create();
         final Socket underlying = new Socket(c.host, c.port);
-        final SSLSocket clientWrapping = (SSLSocket) c.clientContext.getSocketFactory().createSocket(
-                underlying, c.host.getHostName(), c.port, false);
+        final SSLSocket clientWrapping =
+                (SSLSocket) c.clientContext.getSocketFactory().createSocket(
+                        underlying, c.host.getHostName(), c.port, true);
+
+        if (isConscryptFdSocket(clientWrapping) && !readUnderlying && closeUnderlying) {
+            // TODO(nmittler): FD socket gets stuck in the read on Windows and OSX.
+            assumeFalse("Skipping interrupt test on Windows", isWindows());
+            assumeFalse("Skipping interrupt test on OSX", isOsx());
+        }
+
         SSLSocket server = (SSLSocket) c.serverSocket.accept();
 
         // Start the handshake.
@@ -1646,12 +1706,10 @@
         // Read from the socket.
         try {
             toRead.setSoTimeout(readingTimeoutMillis);
-            final InputStream inputStream = toRead.getInputStream();
-            @SuppressWarnings("unused")
-            int value = inputStream.read();
+            toRead.getInputStream().read();
             fail();
         } catch (SocketException expected) {
-            // Ignored.
+            // Expected
         }
 
         future.get();
@@ -1659,22 +1717,33 @@
         underlying.close();
         server.close();
     }
+
     /**
      * b/7014266 Test to confirm that an SSLSocket.close() on one
      * thread will interrupt another thread blocked reading on the same
      * socket.
      */
+    // TODO(nmittler): Interrupts do not work with the engine-based socket.
     @Test
-    public void test_SSLSocket_interrupt_read() throws Exception {
+    public void test_SSLSocket_interrupt_read_withoutAutoClose() throws Exception {
         final int readingTimeoutMillis = 5000;
         TestSSLContext c = TestSSLContext.create();
         final Socket underlying = new Socket(c.host, c.port);
         final SSLSocket wrapping = (SSLSocket) c.clientContext.getSocketFactory().createSocket(
                 underlying, c.host.getHostName(), c.port, false);
+
+        // TODO(nmittler): Interrupts do not work with the engine-based socket.
+        assumeFalse(isConscryptEngineSocket(wrapping));
+
         Future<Void> clientFuture = runAsync(() -> {
             wrapping.startHandshake();
-            wrapping.setSoTimeout(readingTimeoutMillis);
-            assertEquals(-1, wrapping.getInputStream().read());
+            try {
+                wrapping.setSoTimeout(readingTimeoutMillis);
+                wrapping.getInputStream().read();
+                fail();
+            } catch (SocketException expected) {
+                // Expected
+            }
             return null;
         });
         SSLSocket server = (SSLSocket) c.serverSocket.accept();
@@ -1701,12 +1770,14 @@
         }
 
         wrapping.close();
+
         clientFuture.get();
         server.close();
     }
+
     @Test
     public void test_TestSSLSocketPair_create() {
-        TestSSLSocketPair test = TestSSLSocketPair.create();
+        TestSSLSocketPair test = TestSSLSocketPair.create().connect();
         assertNotNull(test.c);
         assertNotNull(test.server);
         assertNotNull(test.client);
@@ -1720,6 +1791,7 @@
         assertTrue(test.client.getSession().isValid());
         test.close();
     }
+
     @Test
     public void test_SSLSocket_ClientHello_record_size() throws Exception {
         // This test checks the size of ClientHello of the default SSLSocket. TLS/SSL handshakes
@@ -1733,25 +1805,11 @@
             protected SSLSocket configureSocket(SSLSocket socket) {
                 // Enable SNI extension on the socket (this is typically enabled by default)
                 // to increase the size of ClientHello.
-                try {
-                    Method setHostname = socket.getClass().getMethod("setHostname", String.class);
-                    setHostname.invoke(socket, "sslsockettest.androidcts.google.com");
-                } catch (NoSuchMethodException ignored) {
-                    // Ignored.
-                } catch (Exception e) {
-                    throw new RuntimeException("Failed to enable SNI", e);
-                }
+                setHostname(socket);
+
                 // Enable Session Tickets extension on the socket (this is typically enabled
                 // by default) to increase the size of ClientHello.
-                try {
-                    Method setUseSessionTickets =
-                            socket.getClass().getMethod("setUseSessionTickets", boolean.class);
-                    setUseSessionTickets.invoke(socket, true);
-                } catch (NoSuchMethodException ignored) {
-                    // Ignored.
-                } catch (Exception e) {
-                    throw new RuntimeException("Failed to enable Session Tickets", e);
-                }
+                enableSessionTickets(socket);
                 return socket;
             }
         };
@@ -1767,6 +1825,7 @@
                     + " bytes");
         }
     }
+
     @Test
     public void test_SSLSocket_ClientHello_cipherSuites() throws Exception {
         ForEachRunner.runNamed(sslSocketFactory -> {
@@ -1777,8 +1836,7 @@
             // indicate that a certain TLS extension should be used.
             HelloExtension renegotiationInfoExtension =
                     clientHello.findExtensionByType(HelloExtension.TYPE_RENEGOTIATION_INFO);
-            if (renegotiationInfoExtension != null
-                    && renegotiationInfoExtension.data.length == 1
+            if (renegotiationInfoExtension != null && renegotiationInfoExtension.data.length == 1
                     && renegotiationInfoExtension.data[0] == 0) {
                 cipherSuites = new String[clientHello.cipherSuites.size() + 1];
                 cipherSuites[clientHello.cipherSuites.size()] =
@@ -1793,6 +1851,7 @@
             StandardNames.assertDefaultCipherSuites(cipherSuites);
         }, getSSLSocketFactoriesToTest());
     }
+
     @Test
     public void test_SSLSocket_ClientHello_supportedCurves() throws Exception {
         ForEachRunner.runNamed(sslSocketFactory -> {
@@ -1814,6 +1873,7 @@
             StandardNames.assertDefaultEllipticCurves(supportedCurves);
         }, getSSLSocketFactoriesToTest());
     }
+
     @Test
     public void test_SSLSocket_ClientHello_clientProtocolVersion() throws Exception {
         ForEachRunner.runNamed(sslSocketFactory -> {
@@ -1821,6 +1881,7 @@
             assertEquals(TlsProtocolVersion.TLSv1_2, clientHello.clientVersion);
         }, getSSLSocketFactoriesToTest());
     }
+
     @Test
     public void test_SSLSocket_ClientHello_compressionMethods() throws Exception {
         ForEachRunner.runNamed(sslSocketFactory -> {
@@ -1829,6 +1890,7 @@
                     clientHello.compressionMethods);
         }, getSSLSocketFactoriesToTest());
     }
+
     @Test
     public void test_SSLSocket_ClientHello_SNI() throws Exception {
         ForEachRunner.runNamed(sslSocketFactory -> {
@@ -1843,8 +1905,7 @@
     }
     private List<Pair<String, SSLSocketFactory>> getSSLSocketFactoriesToTest()
             throws NoSuchAlgorithmException, KeyManagementException {
-        List<Pair<String, SSLSocketFactory>> result =
-                new ArrayList<>();
+        List<Pair<String, SSLSocketFactory>> result = new ArrayList<>();
         result.add(Pair.of("default", (SSLSocketFactory) SSLSocketFactory.getDefault()));
         for (String sslContextProtocol : StandardNames.SSL_CONTEXT_PROTOCOLS) {
             SSLContext sslContext = SSLContext.getInstance(sslContextProtocol);
@@ -1898,21 +1959,20 @@
             listeningSocket = ServerSocketFactory.getDefault().createServerSocket(0);
             final ServerSocket finalListeningSocket = listeningSocket;
             // 2. (in background) Wait for an incoming connection and read its first chunk.
-            final Future<byte[]> readFirstReceivedChunkFuture =
-                    runAsync(() -> {
-                        Socket socket = finalListeningSocket.accept();
-                        sockets[1] = socket;
-                        try {
-                            byte[] buffer = new byte[64 * 1024];
-                            int bytesRead = socket.getInputStream().read(buffer);
-                            if (bytesRead == -1) {
-                                throw new EOFException("Failed to read anything");
-                            }
-                            return Arrays.copyOf(buffer, bytesRead);
-                        } finally {
-                            closeQuietly(socket);
-                        }
-                    });
+            final Future<byte[]> readFirstReceivedChunkFuture = runAsync(() -> {
+                Socket socket = finalListeningSocket.accept();
+                sockets[1] = socket;
+                try {
+                    byte[] buffer = new byte[64 * 1024];
+                    int bytesRead = socket.getInputStream().read(buffer);
+                    if (bytesRead == -1) {
+                        throw new EOFException("Failed to read anything");
+                    }
+                    return Arrays.copyOf(buffer, bytesRead);
+                } finally {
+                    closeQuietly(socket);
+                }
+            });
             // 3. Create a client socket, connect it to the server socket, and start the TLS/SSL
             //    handshake.
             runAsync((Callable<Void>) () -> {
@@ -1924,8 +1984,7 @@
                     // server socket receives a ClientHello.
                     try {
                         SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(client,
-                                "localhost.localdomain", finalListeningSocket.getLocalPort(),
-                                true);
+                                "localhost.localdomain", finalListeningSocket.getLocalPort(), true);
                         sslSocket.startHandshake();
                         fail();
                         return null;
@@ -1949,28 +2008,22 @@
     @Test
     public void test_SSLSocket_getPortWithSNI() throws Exception {
         TestSSLContext context = TestSSLContext.create();
-        try (SSLSocket client = (SSLSocket) context.clientContext.getSocketFactory()
-                .createSocket()) {
+        try (SSLSocket client =
+                        (SSLSocket) context.clientContext.getSocketFactory().createSocket()) {
             client.connect(new InetSocketAddress(context.host, context.port));
-            try {
-                // This is crucial to reproducing issue 18428603.
-                Method setHostname = client.getClass().getMethod("setHostname", String.class);
-                setHostname.invoke(client, "sslsockettest.androidcts.google.com");
-            } catch (NoSuchMethodException ignored) {
-                // Ignored.
-            }
+            setHostname(client);
             assertTrue(client.getPort() > 0);
         } finally {
             context.close();
         }
     }
+
     @Test
     public void test_SSLSocket_SNIHostName() throws Exception {
         TestSSLContext c = TestSSLContext.create();
         final SSLSocket client = (SSLSocket) c.clientContext.getSocketFactory().createSocket();
         SSLParameters clientParams = client.getSSLParameters();
-        clientParams.setServerNames(
-                Collections.singletonList(new SNIHostName("www.example.com")));
+        clientParams.setServerNames(Collections.singletonList(new SNIHostName("www.example.com")));
         client.setSSLParameters(clientParams);
         SSLParameters serverParams = c.serverSocket.getSSLParameters();
         serverParams.setSNIMatchers(
@@ -1996,6 +2049,7 @@
         SNIHostName serverHostName = (SNIHostName) serverName;
         assertEquals("www.example.com", serverHostName.getAsciiName());
     }
+
     @Test
     public void test_SSLSocket_sendsTlsFallbackScsv_Fallback_Success() throws Exception {
         TestSSLContext context = TestSSLContext.create();
@@ -2050,10 +2104,13 @@
         server.close();
         context.close();
     }
+
     private static void assertInappropriateFallbackIsCause(Throwable cause) {
-        assertTrue(cause.getMessage(), cause.getMessage().contains("inappropriate fallback")
+        assertTrue(cause.getMessage(),
+                cause.getMessage().contains("inappropriate fallback")
                         || cause.getMessage().contains("INAPPROPRIATE_FALLBACK"));
     }
+
     @Test
     public void test_SSLSocket_sendsTlsFallbackScsv_InappropriateFallback_Failure()
             throws Exception {
@@ -2098,6 +2155,7 @@
         server.close();
         context.close();
     }
+
     @Test
     public void test_SSLSocket_ClientGetsAlertDuringHandshake_HasGoodExceptionMessage()
             throws Exception {
@@ -2123,11 +2181,9 @@
             int bytesRead = server.getInputStream().read(scratch);
             // Write a bogus TLS alert:
             // TLSv1.2 Record Layer: Alert (Level: Warning, Description: Protocol Version)
-            server.getOutputStream().write(
-                    new byte[] {0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x46});
+            server.getOutputStream().write(new byte[] {0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x46});
             // TLSv1.2 Record Layer: Alert (Level: Warning, Description: Close Notify)
-            server.getOutputStream().write(
-                    new byte[] {0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x00});
+            server.getOutputStream().write(new byte[] {0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x00});
             return null;
         });
         c.get(5, TimeUnit.SECONDS);
@@ -2137,6 +2193,7 @@
         listener.close();
         context.close();
     }
+
     @Test
     public void test_SSLSocket_ServerGetsAlertDuringHandshake_HasGoodExceptionMessage()
             throws Exception {
@@ -2203,12 +2260,10 @@
             // Write a bogus TLS alert:
             // TLSv1.2 Record Layer: Alert (Level: Warning, Description:
             // Protocol Version)
-            client.getOutputStream().write(
-                    new byte[] {0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x46});
+            client.getOutputStream().write(new byte[] {0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x46});
             // TLSv1.2 Record Layer: Alert (Level: Warning, Description:
             // Close Notify)
-            client.getOutputStream().write(
-                    new byte[] {0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x00});
+            client.getOutputStream().write(new byte[] {0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x00});
             return null;
         });
         c.get(5, TimeUnit.SECONDS);
@@ -2217,6 +2272,7 @@
         server.close();
         context.close();
     }
+
     @Test
     public void test_SSLSocket_SSLv3Unsupported() throws Exception {
         TestSSLContext context = TestSSLContext.create();
@@ -2233,6 +2289,73 @@
         }
     }
 
+    private static void setWriteTimeout(Object socket, int timeout) {
+        Exception ex = null;
+        try {
+            Method method = socket.getClass().getMethod("setSoWriteTimeout", int.class);
+            method.setAccessible(true);
+            method.invoke(socket, timeout);
+        } catch (Exception e) {
+            ex = e;
+        }
+        // Engine-based socket currently has the method but throws UnsupportedOperationException.
+        assumeNoException("Client socket does not support setting write timeout", ex);
+    }
+
+    private static void setHostname(SSLSocket socket) {
+        try {
+            Method method = socket.getClass().getMethod("setHostname", String.class);
+            method.setAccessible(true);
+            method.invoke(socket, "sslsockettest.androidcts.google.com");
+        } catch (NoSuchMethodException ignored) {
+            // Ignored.
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to enable SNI", e);
+        }
+    }
+
+    private static void enableSessionTickets(SSLSocket socket) {
+        try {
+            Method method =
+                    socket.getClass().getMethod("setUseSessionTickets", boolean.class);
+            method.setAccessible(true);
+            method.invoke(socket, true);
+        } catch (NoSuchMethodException ignored) {
+            // Ignored.
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to enable Session Tickets", e);
+        }
+    }
+
+    private static boolean isConscryptSocket(Socket socket) {
+        return isConscryptFdSocket(socket) || isConscryptEngineSocket(socket);
+    }
+
+    private static boolean isConscryptFdSocket(Socket socket) {
+        return "ConscryptFileDescriptorSocket".equals(socket.getClass().getSimpleName());
+    }
+
+    private static boolean isConscryptEngineSocket(Socket socket) {
+        return "ConscryptEngineSocket".equals(socket.getClass().getSimpleName());
+    }
+
+    private static String osName() {
+        return System.getProperty("os.name").toLowerCase(Locale.US).replaceAll("[^a-z0-9]+", "");
+    }
+
+    private static boolean isLinux() {
+        return osName().startsWith("linux");
+    }
+
+    private static boolean isWindows() {
+        return osName().startsWith("windows");
+    }
+
+    private static boolean isOsx() {
+        String name = osName();
+        return name.startsWith("macosx") || name.startsWith("osx");
+    }
+
     private <T> Future<T> runAsync(Callable<T> callable) {
         return executor.submit(callable);
     }
diff --git a/openjdk/build.gradle b/openjdk/build.gradle
index 02777a7..cf7b8b6 100644
--- a/openjdk/build.gradle
+++ b/openjdk/build.gradle
@@ -117,7 +117,8 @@
     // This is used for the @hide annotation processing in JavaDoc
     publicApiDocs project(':conscrypt-api-doclet')
 
-    compileOnly project(':conscrypt-constants')
+    compileOnly project(':conscrypt-constants'),
+                configurations.publicApiDocs
 
     testCompile project(':conscrypt-constants'),
             project(':conscrypt-testing'),
@@ -186,7 +187,7 @@
     def testTaskName = "${testSourceSet.name}"
     def javaExecutable
     def javaArchFlag
-    if (testSourceSet.name.endsWith(arch32Name)) {
+    if (testSourceSet.name.endsWith("${arch32Name}Test")) {
         // 32-bit test
         javaExecutable = javaExecutable32 != null ? javaExecutable32 : test.executable
         javaArchFlag = '-d32'
@@ -268,11 +269,13 @@
                     cppCompiler.define "CONSCRYPT_OPENJDK"
 
                     // Set up 32-bit vs 64-bit build
+                    def building64Bit = false
                     def libPath
                     if (targetPlatform.getArchitecture().getName() == "x86") {
                         libPath = "$boringssl32BuildDir"
                     } else if (targetPlatform.getArchitecture().getName() == "x86-64") {
                         libPath = "$boringssl64BuildDir"
+                        building64Bit = true
                     } else {
                         throw new GradleException("Unknown architecture: " +
                                 targetPlatform.getArchitecture().name)
@@ -281,7 +284,7 @@
                     if (toolChain in Clang || toolChain in Gcc) {
                         cppCompiler.args "-Wall",
                                 "-fPIC",
-                                "-O2",
+                                "-O3",
                                 "-std=c++11",
                                 "-I$jniSourceDir/main/include",
                                 "-I$jniSourceDir/unbundled/include",
@@ -292,7 +295,7 @@
                                 "-I$jdkIncludeDir/win32"
 
                         // Static link to BoringSSL
-                        linker.args "-O2",
+                        linker.args "-O3",
                                 "-fvisibility=hidden",
                                 "-lstdc++",
                                 libPath + "/ssl/libssl.a",
@@ -300,7 +303,9 @@
                     } else if (toolChain in VisualCpp) {
                         cppCompiler.define "DLL_EXPORT"
                         cppCompiler.define "WIN32_LEAN_AND_MEAN"
-                        cppCompiler.define "WIN64"
+                        if (building64Bit) {
+                            cppCompiler.define "WIN64"
+                        }
                         cppCompiler.define "_WINDOWS"
                         cppCompiler.define "UNICODE"
                         cppCompiler.define "_UNICODE"
@@ -337,8 +342,8 @@
                         linker.args "-WX",
                                 "ws2_32.lib",
                                 "advapi32.lib",
-                                libPath + "\\ssl\\ssl.lib",
-                                libPath + "\\crypto\\crypto.lib"
+                                "${libPath}\\ssl\\ssl.lib",
+                                "${libPath}\\crypto\\crypto.lib"
                     }
                 }
 
diff --git a/openjdk/src/main/java/org/conscrypt/Platform.java b/openjdk/src/main/java/org/conscrypt/Platform.java
index 090980a..4bba5c8 100644
--- a/openjdk/src/main/java/org/conscrypt/Platform.java
+++ b/openjdk/src/main/java/org/conscrypt/Platform.java
@@ -87,8 +87,8 @@
         }
     }
 
-    static FileDescriptor getFileDescriptorFromSSLSocket(OpenSSLSocketImpl openSSLSocketImpl) {
-        return getFileDescriptor(openSSLSocketImpl);
+    static FileDescriptor getFileDescriptorFromSSLSocket(AbstractConscryptSocket socket) {
+        return getFileDescriptor(socket);
     }
 
     static String getCurveName(ECParameterSpec spec) {
@@ -117,7 +117,7 @@
 
     @SuppressWarnings("unchecked")
     public static void setSSLParameters(
-            SSLParameters params, SSLParametersImpl impl, OpenSSLSocketImpl socket) {
+            SSLParameters params, SSLParametersImpl impl, AbstractConscryptSocket socket) {
         impl.setEndpointIdentificationAlgorithm(params.getEndpointIdentificationAlgorithm());
         try {
             Method getUseCipherSuitesOrder =
@@ -147,7 +147,7 @@
 
     @SuppressWarnings({"LiteralClassName", "rawtypes"})
     public static void getSSLParameters(
-            SSLParameters params, SSLParametersImpl impl, OpenSSLSocketImpl socket) {
+            SSLParameters params, SSLParametersImpl impl, AbstractConscryptSocket socket) {
         params.setEndpointIdentificationAlgorithm(impl.getEndpointIdentificationAlgorithm());
         try {
             Method setUseCipherSuitesOrder =
@@ -171,7 +171,7 @@
 
     @SuppressWarnings("unchecked")
     public static void setSSLParameters(
-            SSLParameters params, SSLParametersImpl impl, OpenSSLEngineImpl engine) {
+            SSLParameters params, SSLParametersImpl impl, ConscryptEngine engine) {
         impl.setEndpointIdentificationAlgorithm(params.getEndpointIdentificationAlgorithm());
         try {
             Method getUseCipherSuitesOrder =
@@ -185,7 +185,7 @@
                 for (Object serverName : serverNames) {
                     if ((int) serverName.getClass().getMethod("getType").invoke(serverName)
                             == hostNameType) {
-                        engine.setSniHostname((String) serverName.getClass()
+                        engine.setHostname((String) serverName.getClass()
                                                       .getMethod("getAsciiName")
                                                       .invoke(serverName));
                         break;
@@ -200,19 +200,19 @@
 
     @SuppressWarnings({"LiteralClassName", "rawtypes"})
     public static void getSSLParameters(
-            SSLParameters params, SSLParametersImpl impl, OpenSSLEngineImpl engine) {
+            SSLParameters params, SSLParametersImpl impl, ConscryptEngine engine) {
         params.setEndpointIdentificationAlgorithm(impl.getEndpointIdentificationAlgorithm());
         try {
             Method setUseCipherSuitesOrder =
                     SSLParameters.class.getMethod("setUseCipherSuitesOrder", boolean.class);
             setUseCipherSuitesOrder.invoke(params, impl.getUseCipherSuitesOrder());
             Method setServerNames = SSLParameters.class.getMethod("setServerNames", List.class);
-            if (impl.getUseSni() && AddressUtils.isValidSniHostname(engine.getSniHostname())) {
+            if (impl.getUseSni() && AddressUtils.isValidSniHostname(engine.getHostname())) {
                 Constructor sniHostNameConstructor =
                         Class.forName("javax.net.ssl.SNIHostName").getConstructor(String.class);
                 setServerNames.invoke(params,
                         (Collections.singletonList(
-                                sniHostNameConstructor.newInstance(engine.getSniHostname()))));
+                                sniHostNameConstructor.newInstance(engine.getHostname()))));
             }
         } catch (NoSuchMethodException ignored) {
         } catch (IllegalAccessException ignored) {
@@ -234,7 +234,7 @@
     }
 
     static void checkClientTrusted(X509TrustManager tm, X509Certificate[] chain, String authType,
-            OpenSSLSocketImpl socket) throws CertificateException {
+            AbstractConscryptSocket socket) throws CertificateException {
         if (tm instanceof X509ExtendedTrustManager) {
             X509ExtendedTrustManager x509etm = (X509ExtendedTrustManager) tm;
             x509etm.checkClientTrusted(chain, authType, socket);
@@ -244,7 +244,7 @@
     }
 
     static void checkServerTrusted(X509TrustManager tm, X509Certificate[] chain, String authType,
-            OpenSSLSocketImpl socket) throws CertificateException {
+            AbstractConscryptSocket socket) throws CertificateException {
         if (tm instanceof X509ExtendedTrustManager) {
             X509ExtendedTrustManager x509etm = (X509ExtendedTrustManager) tm;
             x509etm.checkServerTrusted(chain, authType, socket);
@@ -254,7 +254,7 @@
     }
 
     static void checkClientTrusted(X509TrustManager tm, X509Certificate[] chain, String authType,
-            OpenSSLEngineImpl engine) throws CertificateException {
+            ConscryptEngine engine) throws CertificateException {
         if (tm instanceof X509ExtendedTrustManager) {
             X509ExtendedTrustManager x509etm = (X509ExtendedTrustManager) tm;
             x509etm.checkClientTrusted(chain, authType, engine);
@@ -264,7 +264,7 @@
     }
 
     static void checkServerTrusted(X509TrustManager tm, X509Certificate[] chain, String authType,
-            OpenSSLEngineImpl engine) throws CertificateException {
+            ConscryptEngine engine) throws CertificateException {
         if (tm instanceof X509ExtendedTrustManager) {
             X509ExtendedTrustManager x509etm = (X509ExtendedTrustManager) tm;
             x509etm.checkServerTrusted(chain, authType, engine);
@@ -362,15 +362,16 @@
      * Pre-Java-8 backward compatibility.
      */
 
-    static SSLSession wrapSSLSession(AbstractOpenSSLSession sslSession) {
-        return new OpenSSLExtendedSessionImpl(sslSession);
+    static SSLSession wrapSSLSession(ActiveSession sslSession) {
+        return new DelegatingExtendedSSLSession(sslSession);
     }
 
     @SuppressWarnings("unused")
     static SSLSession unwrapSSLSession(SSLSession sslSession) {
-        if (sslSession instanceof OpenSSLExtendedSessionImpl) {
-            return ((OpenSSLExtendedSessionImpl) sslSession).getDelegate();
+        if (sslSession instanceof DelegatingExtendedSSLSession) {
+            return ((DelegatingExtendedSSLSession) sslSession).getDelegate();
         }
+
         return sslSession;
     }
 
diff --git a/openjdk/src/test/java/org/conscrypt/AbstractSessionContextTest.java b/openjdk/src/test/java/org/conscrypt/AbstractSessionContextTest.java
index 756e294..1cda6f9 100644
--- a/openjdk/src/test/java/org/conscrypt/AbstractSessionContextTest.java
+++ b/openjdk/src/test/java/org/conscrypt/AbstractSessionContextTest.java
@@ -1,7 +1,7 @@
 /*
- * Copyright 2016 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
  *
- * Licensed under the Apache License, Version 2.0 (the "License", "www.google.com", 443);
+ * Licensed 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
  *
@@ -17,649 +17,118 @@
 package org.conscrypt;
 
 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.Assert.assertSame;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
+import java.security.cert.Certificate;
 import javax.net.ssl.SSLSession;
-import org.junit.After;
-import org.junit.BeforeClass;
+import org.junit.Before;
 import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
-@RunWith(JUnit4.class)
-public class AbstractSessionContextTest {
-    /*
-     * Taken from external/boringssl/src/ssl/ssl_test.cc: kOpenSSLSession is a
-     * serialized SSL_SESSION.
-     */
-    private static final byte[] kOpenSSLSession = new byte[] {(byte) 0x30, (byte) 0x82, (byte) 0x05,
-            (byte) 0xAA, (byte) 0x02, (byte) 0x01, (byte) 0x01, (byte) 0x02, (byte) 0x02,
-            (byte) 0x03, (byte) 0x03, (byte) 0x04, (byte) 0x02, (byte) 0xC0, (byte) 0x2F,
-            (byte) 0x04, (byte) 0x20, (byte) 0x06, (byte) 0xE5, (byte) 0x0D, (byte) 0x67,
-            (byte) 0x76, (byte) 0xAE, (byte) 0x18, (byte) 0x7E, (byte) 0x66, (byte) 0xDE,
-            (byte) 0xA3, (byte) 0x5C, (byte) 0xF0, (byte) 0x2E, (byte) 0x43, (byte) 0x51,
-            (byte) 0x2A, (byte) 0x60, (byte) 0x97, (byte) 0x19, (byte) 0xD3, (byte) 0x60,
-            (byte) 0x5A, (byte) 0xF1, (byte) 0x93, (byte) 0xDD, (byte) 0xCB, (byte) 0x24,
-            (byte) 0x57, (byte) 0x4C, (byte) 0x90, (byte) 0x90, (byte) 0x04, (byte) 0x30,
-            (byte) 0x26, (byte) 0x5A, (byte) 0xE5, (byte) 0xCE, (byte) 0x40, (byte) 0x16,
-            (byte) 0x04, (byte) 0xE5, (byte) 0xA2, (byte) 0x2E, (byte) 0x3F, (byte) 0xE3,
-            (byte) 0x27, (byte) 0xBE, (byte) 0x83, (byte) 0xEE, (byte) 0x5F, (byte) 0x94,
-            (byte) 0x5E, (byte) 0x88, (byte) 0xB3, (byte) 0x3F, (byte) 0x62, (byte) 0x88,
-            (byte) 0xD8, (byte) 0x2E, (byte) 0xC8, (byte) 0xD8, (byte) 0x57, (byte) 0x1C,
-            (byte) 0xA8, (byte) 0xC9, (byte) 0x88, (byte) 0x7C, (byte) 0x59, (byte) 0xA6,
-            (byte) 0x91, (byte) 0x4C, (byte) 0xB7, (byte) 0xDA, (byte) 0x72, (byte) 0x09,
-            (byte) 0xD2, (byte) 0x66, (byte) 0x47, (byte) 0x21, (byte) 0x6A, (byte) 0x09,
-            (byte) 0xA1, (byte) 0x06, (byte) 0x02, (byte) 0x04, (byte) 0x54, (byte) 0x43,
-            (byte) 0x3B, (byte) 0x8E, (byte) 0xA2, (byte) 0x04, (byte) 0x02, (byte) 0x02,
-            (byte) 0x01, (byte) 0x2C, (byte) 0xA3, (byte) 0x82, (byte) 0x04, (byte) 0x7A,
-            (byte) 0x30, (byte) 0x82, (byte) 0x04, (byte) 0x76, (byte) 0x30, (byte) 0x82,
-            (byte) 0x03, (byte) 0x5E, (byte) 0xA0, (byte) 0x03, (byte) 0x02, (byte) 0x01,
-            (byte) 0x02, (byte) 0x02, (byte) 0x08, (byte) 0x2B, (byte) 0xD7, (byte) 0x54,
-            (byte) 0xBE, (byte) 0xC3, (byte) 0xD6, (byte) 0x4A, (byte) 0x55, (byte) 0x30,
-            (byte) 0x0D, (byte) 0x06, (byte) 0x09, (byte) 0x2A, (byte) 0x86, (byte) 0x48,
-            (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01, (byte) 0x01, (byte) 0x05,
-            (byte) 0x05, (byte) 0x00, (byte) 0x30, (byte) 0x49, (byte) 0x31, (byte) 0x0B,
-            (byte) 0x30, (byte) 0x09, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04,
-            (byte) 0x06, (byte) 0x13, (byte) 0x02, (byte) 0x55, (byte) 0x53, (byte) 0x31,
-            (byte) 0x13, (byte) 0x30, (byte) 0x11, (byte) 0x06, (byte) 0x03, (byte) 0x55,
-            (byte) 0x04, (byte) 0x0A, (byte) 0x13, (byte) 0x0A, (byte) 0x47, (byte) 0x6F,
-            (byte) 0x6F, (byte) 0x67, (byte) 0x6C, (byte) 0x65, (byte) 0x20, (byte) 0x49,
-            (byte) 0x6E, (byte) 0x63, (byte) 0x31, (byte) 0x25, (byte) 0x30, (byte) 0x23,
-            (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x03, (byte) 0x13,
-            (byte) 0x1C, (byte) 0x47, (byte) 0x6F, (byte) 0x6F, (byte) 0x67, (byte) 0x6C,
-            (byte) 0x65, (byte) 0x20, (byte) 0x49, (byte) 0x6E, (byte) 0x74, (byte) 0x65,
-            (byte) 0x72, (byte) 0x6E, (byte) 0x65, (byte) 0x74, (byte) 0x20, (byte) 0x41,
-            (byte) 0x75, (byte) 0x74, (byte) 0x68, (byte) 0x6F, (byte) 0x72, (byte) 0x69,
-            (byte) 0x74, (byte) 0x79, (byte) 0x20, (byte) 0x47, (byte) 0x32, (byte) 0x30,
-            (byte) 0x1E, (byte) 0x17, (byte) 0x0D, (byte) 0x31, (byte) 0x34, (byte) 0x31,
-            (byte) 0x30, (byte) 0x30, (byte) 0x38, (byte) 0x31, (byte) 0x32, (byte) 0x30,
-            (byte) 0x37, (byte) 0x35, (byte) 0x37, (byte) 0x5A, (byte) 0x17, (byte) 0x0D,
-            (byte) 0x31, (byte) 0x35, (byte) 0x30, (byte) 0x31, (byte) 0x30, (byte) 0x36,
-            (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30,
-            (byte) 0x5A, (byte) 0x30, (byte) 0x68, (byte) 0x31, (byte) 0x0B, (byte) 0x30,
-            (byte) 0x09, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x06,
-            (byte) 0x13, (byte) 0x02, (byte) 0x55, (byte) 0x53, (byte) 0x31, (byte) 0x13,
-            (byte) 0x30, (byte) 0x11, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04,
-            (byte) 0x08, (byte) 0x0C, (byte) 0x0A, (byte) 0x43, (byte) 0x61, (byte) 0x6C,
-            (byte) 0x69, (byte) 0x66, (byte) 0x6F, (byte) 0x72, (byte) 0x6E, (byte) 0x69,
-            (byte) 0x61, (byte) 0x31, (byte) 0x16, (byte) 0x30, (byte) 0x14, (byte) 0x06,
-            (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x07, (byte) 0x0C, (byte) 0x0D,
-            (byte) 0x4D, (byte) 0x6F, (byte) 0x75, (byte) 0x6E, (byte) 0x74, (byte) 0x61,
-            (byte) 0x69, (byte) 0x6E, (byte) 0x20, (byte) 0x56, (byte) 0x69, (byte) 0x65,
-            (byte) 0x77, (byte) 0x31, (byte) 0x13, (byte) 0x30, (byte) 0x11, (byte) 0x06,
-            (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x0A, (byte) 0x0C, (byte) 0x0A,
-            (byte) 0x47, (byte) 0x6F, (byte) 0x6F, (byte) 0x67, (byte) 0x6C, (byte) 0x65,
-            (byte) 0x20, (byte) 0x49, (byte) 0x6E, (byte) 0x63, (byte) 0x31, (byte) 0x17,
-            (byte) 0x30, (byte) 0x15, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04,
-            (byte) 0x03, (byte) 0x0C, (byte) 0x0E, (byte) 0x77, (byte) 0x77, (byte) 0x77,
-            (byte) 0x2E, (byte) 0x67, (byte) 0x6F, (byte) 0x6F, (byte) 0x67, (byte) 0x6C,
-            (byte) 0x65, (byte) 0x2E, (byte) 0x63, (byte) 0x6F, (byte) 0x6D, (byte) 0x30,
-            (byte) 0x82, (byte) 0x01, (byte) 0x22, (byte) 0x30, (byte) 0x0D, (byte) 0x06,
-            (byte) 0x09, (byte) 0x2A, (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xF7,
-            (byte) 0x0D, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x00,
-            (byte) 0x03, (byte) 0x82, (byte) 0x01, (byte) 0x0F, (byte) 0x00, (byte) 0x30,
-            (byte) 0x82, (byte) 0x01, (byte) 0x0A, (byte) 0x02, (byte) 0x82, (byte) 0x01,
-            (byte) 0x01, (byte) 0x00, (byte) 0x9C, (byte) 0x29, (byte) 0xE2, (byte) 0xEB,
-            (byte) 0xA6, (byte) 0x50, (byte) 0x02, (byte) 0xF8, (byte) 0xBA, (byte) 0x1F,
-            (byte) 0xCB, (byte) 0xCB, (byte) 0x7F, (byte) 0xC0, (byte) 0x3C, (byte) 0x2D,
-            (byte) 0x07, (byte) 0xA7, (byte) 0xAE, (byte) 0xEF, (byte) 0x60, (byte) 0x95,
-            (byte) 0xA7, (byte) 0x47, (byte) 0x09, (byte) 0xE1, (byte) 0x5D, (byte) 0xE5,
-            (byte) 0x92, (byte) 0x73, (byte) 0x7A, (byte) 0x86, (byte) 0xE1, (byte) 0xFD,
-            (byte) 0x72, (byte) 0xDE, (byte) 0x85, (byte) 0x16, (byte) 0x4E, (byte) 0xF4,
-            (byte) 0xA1, (byte) 0x12, (byte) 0x21, (byte) 0xFD, (byte) 0x50, (byte) 0x4D,
-            (byte) 0x04, (byte) 0x1C, (byte) 0xFD, (byte) 0xD3, (byte) 0x48, (byte) 0xD8,
-            (byte) 0xCB, (byte) 0xEE, (byte) 0xF5, (byte) 0xD7, (byte) 0x52, (byte) 0x66,
-            (byte) 0xD5, (byte) 0xBF, (byte) 0x22, (byte) 0xA8, (byte) 0xE4, (byte) 0xD0,
-            (byte) 0xF5, (byte) 0xA4, (byte) 0xF9, (byte) 0x0B, (byte) 0xB4, (byte) 0x84,
-            (byte) 0x84, (byte) 0xD7, (byte) 0x10, (byte) 0x14, (byte) 0x9B, (byte) 0xEA,
-            (byte) 0xCC, (byte) 0x7D, (byte) 0xDE, (byte) 0x30, (byte) 0xF9, (byte) 0x1B,
-            (byte) 0xE9, (byte) 0x94, (byte) 0x96, (byte) 0x1A, (byte) 0x6D, (byte) 0x72,
-            (byte) 0x18, (byte) 0x5E, (byte) 0xCC, (byte) 0x09, (byte) 0x04, (byte) 0xC6,
-            (byte) 0x41, (byte) 0x71, (byte) 0x76, (byte) 0xD1, (byte) 0x29, (byte) 0x3F,
-            (byte) 0x3B, (byte) 0x5E, (byte) 0x85, (byte) 0x4A, (byte) 0x30, (byte) 0x32,
-            (byte) 0x9D, (byte) 0x4F, (byte) 0xDB, (byte) 0xDE, (byte) 0x82, (byte) 0x66,
-            (byte) 0x39, (byte) 0xCB, (byte) 0x5C, (byte) 0xC9, (byte) 0xC5, (byte) 0x98,
-            (byte) 0x91, (byte) 0x8D, (byte) 0x32, (byte) 0xB5, (byte) 0x2F, (byte) 0xE4,
-            (byte) 0xDC, (byte) 0xB0, (byte) 0x6E, (byte) 0x21, (byte) 0xDE, (byte) 0x39,
-            (byte) 0x3C, (byte) 0x96, (byte) 0xA8, (byte) 0x32, (byte) 0xA8, (byte) 0xC1,
-            (byte) 0xD1, (byte) 0x6C, (byte) 0xA9, (byte) 0xAA, (byte) 0xF3, (byte) 0x5E,
-            (byte) 0x24, (byte) 0x70, (byte) 0xB7, (byte) 0xAB, (byte) 0x92, (byte) 0x63,
-            (byte) 0x08, (byte) 0x1E, (byte) 0x11, (byte) 0x3F, (byte) 0xB3, (byte) 0x5F,
-            (byte) 0xC7, (byte) 0x98, (byte) 0xE3, (byte) 0x1D, (byte) 0x2A, (byte) 0xC2,
-            (byte) 0x32, (byte) 0x1C, (byte) 0x3C, (byte) 0x95, (byte) 0x43, (byte) 0x16,
-            (byte) 0xE0, (byte) 0x46, (byte) 0x83, (byte) 0xC6, (byte) 0x36, (byte) 0x91,
-            (byte) 0xF4, (byte) 0xA0, (byte) 0xE1, (byte) 0x3C, (byte) 0xB8, (byte) 0x23,
-            (byte) 0xB2, (byte) 0x4F, (byte) 0x8B, (byte) 0x0C, (byte) 0x8C, (byte) 0x92,
-            (byte) 0x45, (byte) 0x24, (byte) 0x43, (byte) 0x68, (byte) 0x24, (byte) 0x06,
-            (byte) 0x84, (byte) 0x43, (byte) 0x96, (byte) 0x2C, (byte) 0x96, (byte) 0x55,
-            (byte) 0x2F, (byte) 0x32, (byte) 0xE8, (byte) 0xE0, (byte) 0xDE, (byte) 0xBF,
-            (byte) 0x52, (byte) 0x57, (byte) 0x2D, (byte) 0x08, (byte) 0x71, (byte) 0x25,
-            (byte) 0x96, (byte) 0x90, (byte) 0x54, (byte) 0x4A, (byte) 0xF1, (byte) 0x0E,
-            (byte) 0xC8, (byte) 0x58, (byte) 0x1A, (byte) 0xE7, (byte) 0x6A, (byte) 0xAB,
-            (byte) 0xA0, (byte) 0x68, (byte) 0xE0, (byte) 0xAD, (byte) 0xFD, (byte) 0xD6,
-            (byte) 0x39, (byte) 0x0F, (byte) 0x76, (byte) 0xE4, (byte) 0xC1, (byte) 0x70,
-            (byte) 0xCD, (byte) 0xDE, (byte) 0x80, (byte) 0x2B, (byte) 0xE2, (byte) 0x1C,
-            (byte) 0x87, (byte) 0x48, (byte) 0x03, (byte) 0x46, (byte) 0x0F, (byte) 0x2C,
-            (byte) 0x41, (byte) 0xF7, (byte) 0x4B, (byte) 0x1F, (byte) 0x93, (byte) 0xAE,
-            (byte) 0x3F, (byte) 0x57, (byte) 0x1F, (byte) 0x2D, (byte) 0xF5, (byte) 0x35,
-            (byte) 0x02, (byte) 0x03, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0xA3,
-            (byte) 0x82, (byte) 0x01, (byte) 0x41, (byte) 0x30, (byte) 0x82, (byte) 0x01,
-            (byte) 0x3D, (byte) 0x30, (byte) 0x1D, (byte) 0x06, (byte) 0x03, (byte) 0x55,
-            (byte) 0x1D, (byte) 0x25, (byte) 0x04, (byte) 0x16, (byte) 0x30, (byte) 0x14,
-            (byte) 0x06, (byte) 0x08, (byte) 0x2B, (byte) 0x06, (byte) 0x01, (byte) 0x05,
-            (byte) 0x05, (byte) 0x07, (byte) 0x03, (byte) 0x01, (byte) 0x06, (byte) 0x08,
-            (byte) 0x2B, (byte) 0x06, (byte) 0x01, (byte) 0x05, (byte) 0x05, (byte) 0x07,
-            (byte) 0x03, (byte) 0x02, (byte) 0x30, (byte) 0x19, (byte) 0x06, (byte) 0x03,
-            (byte) 0x55, (byte) 0x1D, (byte) 0x11, (byte) 0x04, (byte) 0x12, (byte) 0x30,
-            (byte) 0x10, (byte) 0x82, (byte) 0x0E, (byte) 0x77, (byte) 0x77, (byte) 0x77,
-            (byte) 0x2E, (byte) 0x67, (byte) 0x6F, (byte) 0x6F, (byte) 0x67, (byte) 0x6C,
-            (byte) 0x65, (byte) 0x2E, (byte) 0x63, (byte) 0x6F, (byte) 0x6D, (byte) 0x30,
-            (byte) 0x68, (byte) 0x06, (byte) 0x08, (byte) 0x2B, (byte) 0x06, (byte) 0x01,
-            (byte) 0x05, (byte) 0x05, (byte) 0x07, (byte) 0x01, (byte) 0x01, (byte) 0x04,
-            (byte) 0x5C, (byte) 0x30, (byte) 0x5A, (byte) 0x30, (byte) 0x2B, (byte) 0x06,
-            (byte) 0x08, (byte) 0x2B, (byte) 0x06, (byte) 0x01, (byte) 0x05, (byte) 0x05,
-            (byte) 0x07, (byte) 0x30, (byte) 0x02, (byte) 0x86, (byte) 0x1F, (byte) 0x68,
-            (byte) 0x74, (byte) 0x74, (byte) 0x70, (byte) 0x3A, (byte) 0x2F, (byte) 0x2F,
-            (byte) 0x70, (byte) 0x6B, (byte) 0x69, (byte) 0x2E, (byte) 0x67, (byte) 0x6F,
-            (byte) 0x6F, (byte) 0x67, (byte) 0x6C, (byte) 0x65, (byte) 0x2E, (byte) 0x63,
-            (byte) 0x6F, (byte) 0x6D, (byte) 0x2F, (byte) 0x47, (byte) 0x49, (byte) 0x41,
-            (byte) 0x47, (byte) 0x32, (byte) 0x2E, (byte) 0x63, (byte) 0x72, (byte) 0x74,
-            (byte) 0x30, (byte) 0x2B, (byte) 0x06, (byte) 0x08, (byte) 0x2B, (byte) 0x06,
-            (byte) 0x01, (byte) 0x05, (byte) 0x05, (byte) 0x07, (byte) 0x30, (byte) 0x01,
-            (byte) 0x86, (byte) 0x1F, (byte) 0x68, (byte) 0x74, (byte) 0x74, (byte) 0x70,
-            (byte) 0x3A, (byte) 0x2F, (byte) 0x2F, (byte) 0x63, (byte) 0x6C, (byte) 0x69,
-            (byte) 0x65, (byte) 0x6E, (byte) 0x74, (byte) 0x73, (byte) 0x31, (byte) 0x2E,
-            (byte) 0x67, (byte) 0x6F, (byte) 0x6F, (byte) 0x67, (byte) 0x6C, (byte) 0x65,
-            (byte) 0x2E, (byte) 0x63, (byte) 0x6F, (byte) 0x6D, (byte) 0x2F, (byte) 0x6F,
-            (byte) 0x63, (byte) 0x73, (byte) 0x70, (byte) 0x30, (byte) 0x1D, (byte) 0x06,
-            (byte) 0x03, (byte) 0x55, (byte) 0x1D, (byte) 0x0E, (byte) 0x04, (byte) 0x16,
-            (byte) 0x04, (byte) 0x14, (byte) 0x3B, (byte) 0x6B, (byte) 0xE0, (byte) 0x9C,
-            (byte) 0xC6, (byte) 0xC6, (byte) 0x41, (byte) 0xC8, (byte) 0xEA, (byte) 0x5C,
-            (byte) 0xFB, (byte) 0x1A, (byte) 0x58, (byte) 0x15, (byte) 0xC2, (byte) 0x1B,
-            (byte) 0x9D, (byte) 0x43, (byte) 0x19, (byte) 0x85, (byte) 0x30, (byte) 0x0C,
-            (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x1D, (byte) 0x13, (byte) 0x01,
-            (byte) 0x01, (byte) 0xFF, (byte) 0x04, (byte) 0x02, (byte) 0x30, (byte) 0x00,
-            (byte) 0x30, (byte) 0x1F, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x1D,
-            (byte) 0x23, (byte) 0x04, (byte) 0x18, (byte) 0x30, (byte) 0x16, (byte) 0x80,
-            (byte) 0x14, (byte) 0x4A, (byte) 0xDD, (byte) 0x06, (byte) 0x16, (byte) 0x1B,
-            (byte) 0xBC, (byte) 0xF6, (byte) 0x68, (byte) 0xB5, (byte) 0x76, (byte) 0xF5,
-            (byte) 0x81, (byte) 0xB6, (byte) 0xBB, (byte) 0x62, (byte) 0x1A, (byte) 0xBA,
-            (byte) 0x5A, (byte) 0x81, (byte) 0x2F, (byte) 0x30, (byte) 0x17, (byte) 0x06,
-            (byte) 0x03, (byte) 0x55, (byte) 0x1D, (byte) 0x20, (byte) 0x04, (byte) 0x10,
-            (byte) 0x30, (byte) 0x0E, (byte) 0x30, (byte) 0x0C, (byte) 0x06, (byte) 0x0A,
-            (byte) 0x2B, (byte) 0x06, (byte) 0x01, (byte) 0x04, (byte) 0x01, (byte) 0xD6,
-            (byte) 0x79, (byte) 0x02, (byte) 0x05, (byte) 0x01, (byte) 0x30, (byte) 0x30,
-            (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x1D, (byte) 0x1F, (byte) 0x04,
-            (byte) 0x29, (byte) 0x30, (byte) 0x27, (byte) 0x30, (byte) 0x25, (byte) 0xA0,
-            (byte) 0x23, (byte) 0xA0, (byte) 0x21, (byte) 0x86, (byte) 0x1F, (byte) 0x68,
-            (byte) 0x74, (byte) 0x74, (byte) 0x70, (byte) 0x3A, (byte) 0x2F, (byte) 0x2F,
-            (byte) 0x70, (byte) 0x6B, (byte) 0x69, (byte) 0x2E, (byte) 0x67, (byte) 0x6F,
-            (byte) 0x6F, (byte) 0x67, (byte) 0x6C, (byte) 0x65, (byte) 0x2E, (byte) 0x63,
-            (byte) 0x6F, (byte) 0x6D, (byte) 0x2F, (byte) 0x47, (byte) 0x49, (byte) 0x41,
-            (byte) 0x47, (byte) 0x32, (byte) 0x2E, (byte) 0x63, (byte) 0x72, (byte) 0x6C,
-            (byte) 0x30, (byte) 0x0D, (byte) 0x06, (byte) 0x09, (byte) 0x2A, (byte) 0x86,
-            (byte) 0x48, (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01, (byte) 0x01,
-            (byte) 0x05, (byte) 0x05, (byte) 0x00, (byte) 0x03, (byte) 0x82, (byte) 0x01,
-            (byte) 0x01, (byte) 0x00, (byte) 0x9A, (byte) 0x39, (byte) 0x70, (byte) 0x81,
-            (byte) 0x76, (byte) 0x8A, (byte) 0x94, (byte) 0xCB, (byte) 0x96, (byte) 0xF1,
-            (byte) 0xCA, (byte) 0xAF, (byte) 0x96, (byte) 0xAE, (byte) 0x1D, (byte) 0x73,
-            (byte) 0xB3, (byte) 0x2C, (byte) 0x82, (byte) 0x16, (byte) 0x29, (byte) 0xB5,
-            (byte) 0x3C, (byte) 0x7E, (byte) 0x55, (byte) 0x53, (byte) 0x6F, (byte) 0xB2,
-            (byte) 0xBC, (byte) 0x34, (byte) 0x96, (byte) 0xAE, (byte) 0x00, (byte) 0xD8,
-            (byte) 0xF2, (byte) 0x26, (byte) 0xD1, (byte) 0x18, (byte) 0x99, (byte) 0x9F,
-            (byte) 0x7D, (byte) 0xFD, (byte) 0xEB, (byte) 0xE0, (byte) 0xBB, (byte) 0x9D,
-            (byte) 0xE6, (byte) 0x46, (byte) 0xA5, (byte) 0x74, (byte) 0xAB, (byte) 0x3D,
-            (byte) 0x93, (byte) 0xC6, (byte) 0x25, (byte) 0x28, (byte) 0x3D, (byte) 0xC8,
-            (byte) 0x4C, (byte) 0x6E, (byte) 0xCF, (byte) 0xD1, (byte) 0x84, (byte) 0xFF,
-            (byte) 0x46, (byte) 0x4F, (byte) 0x21, (byte) 0x2E, (byte) 0x07, (byte) 0xC4,
-            (byte) 0xB8, (byte) 0xB7, (byte) 0x2A, (byte) 0xE5, (byte) 0xC7, (byte) 0x34,
-            (byte) 0xC6, (byte) 0xA9, (byte) 0x84, (byte) 0xE3, (byte) 0x6C, (byte) 0x49,
-            (byte) 0xF8, (byte) 0x4A, (byte) 0x36, (byte) 0xBB, (byte) 0x3A, (byte) 0xBD,
-            (byte) 0xAD, (byte) 0x8A, (byte) 0x2B, (byte) 0x73, (byte) 0x97, (byte) 0xA6,
-            (byte) 0x30, (byte) 0x2C, (byte) 0x5F, (byte) 0xE4, (byte) 0xBD, (byte) 0x13,
-            (byte) 0x24, (byte) 0xE5, (byte) 0xD9, (byte) 0xA8, (byte) 0x74, (byte) 0x29,
-            (byte) 0x38, (byte) 0x47, (byte) 0x2E, (byte) 0xA6, (byte) 0xD6, (byte) 0x50,
-            (byte) 0xE0, (byte) 0xE8, (byte) 0xDD, (byte) 0x60, (byte) 0xC7, (byte) 0xD2,
-            (byte) 0xC6, (byte) 0x4E, (byte) 0x54, (byte) 0xCE, (byte) 0xE7, (byte) 0x94,
-            (byte) 0x84, (byte) 0x0D, (byte) 0xE8, (byte) 0x81, (byte) 0x92, (byte) 0x91,
-            (byte) 0x71, (byte) 0x19, (byte) 0x1D, (byte) 0x07, (byte) 0x75, (byte) 0x9E,
-            (byte) 0x59, (byte) 0x1A, (byte) 0x7E, (byte) 0x9D, (byte) 0x84, (byte) 0x61,
-            (byte) 0xC7, (byte) 0x84, (byte) 0xAD, (byte) 0xA3, (byte) 0x6A, (byte) 0xED,
-            (byte) 0xD8, (byte) 0x0D, (byte) 0x0C, (byte) 0x2A, (byte) 0x66, (byte) 0x3D,
-            (byte) 0xD7, (byte) 0xAE, (byte) 0x46, (byte) 0x1D, (byte) 0x4A, (byte) 0x8C,
-            (byte) 0x2B, (byte) 0xD6, (byte) 0x1A, (byte) 0x69, (byte) 0x71, (byte) 0xC3,
-            (byte) 0x5E, (byte) 0xA0, (byte) 0x6E, (byte) 0xED, (byte) 0x27, (byte) 0x9F,
-            (byte) 0xAF, (byte) 0x5B, (byte) 0x92, (byte) 0xA0, (byte) 0x03, (byte) 0xFD,
-            (byte) 0x83, (byte) 0x22, (byte) 0x09, (byte) 0x29, (byte) 0xE8, (byte) 0xA1,
-            (byte) 0x32, (byte) 0x2B, (byte) 0xEC, (byte) 0x1A, (byte) 0xA2, (byte) 0x75,
-            (byte) 0x4C, (byte) 0x3E, (byte) 0x99, (byte) 0x71, (byte) 0xCE, (byte) 0x8B,
-            (byte) 0x31, (byte) 0xEF, (byte) 0x9D, (byte) 0x37, (byte) 0x63, (byte) 0xFC,
-            (byte) 0x71, (byte) 0x91, (byte) 0x10, (byte) 0x1E, (byte) 0xD0, (byte) 0xF5,
-            (byte) 0xCB, (byte) 0x6F, (byte) 0x7A, (byte) 0xBA, (byte) 0x5E, (byte) 0x0C,
-            (byte) 0x8A, (byte) 0xFA, (byte) 0xA4, (byte) 0xDE, (byte) 0x36, (byte) 0xAD,
-            (byte) 0x51, (byte) 0x52, (byte) 0xFC, (byte) 0xFE, (byte) 0x10, (byte) 0xB0,
-            (byte) 0x81, (byte) 0xC8, (byte) 0x7D, (byte) 0x03, (byte) 0xC3, (byte) 0xB8,
-            (byte) 0x3C, (byte) 0x66, (byte) 0x6A, (byte) 0xF5, (byte) 0x6A, (byte) 0x81,
-            (byte) 0x7C, (byte) 0x45, (byte) 0xA6, (byte) 0x23, (byte) 0x21, (byte) 0xE1,
-            (byte) 0xD5, (byte) 0xD3, (byte) 0xED, (byte) 0x6E, (byte) 0x0D, (byte) 0x65,
-            (byte) 0x39, (byte) 0x77, (byte) 0x58, (byte) 0x09, (byte) 0x6B, (byte) 0x63,
-            (byte) 0xA4, (byte) 0x02, (byte) 0x04, (byte) 0x00, (byte) 0xA5, (byte) 0x03,
-            (byte) 0x02, (byte) 0x01, (byte) 0x14, (byte) 0xA9, (byte) 0x05, (byte) 0x02,
-            (byte) 0x03, (byte) 0x01, (byte) 0x89, (byte) 0xC0, (byte) 0xAA, (byte) 0x81,
-            (byte) 0xA7, (byte) 0x04, (byte) 0x81, (byte) 0xA4, (byte) 0x1C, (byte) 0x14,
-            (byte) 0x42, (byte) 0xFA, (byte) 0x1E, (byte) 0x3A, (byte) 0x4D, (byte) 0x0A,
-            (byte) 0x83, (byte) 0x7E, (byte) 0x92, (byte) 0x61, (byte) 0x37, (byte) 0x0B,
-            (byte) 0x12, (byte) 0x45, (byte) 0xEA, (byte) 0x2B, (byte) 0x03, (byte) 0x81,
-            (byte) 0x7C, (byte) 0x5F, (byte) 0x6F, (byte) 0x13, (byte) 0x82, (byte) 0x97,
-            (byte) 0xD0, (byte) 0xDC, (byte) 0x5E, (byte) 0x2F, (byte) 0x08, (byte) 0xDC,
-            (byte) 0x0D, (byte) 0x3A, (byte) 0x6C, (byte) 0xBA, (byte) 0x1D, (byte) 0xEA,
-            (byte) 0x5C, (byte) 0x46, (byte) 0x99, (byte) 0xF7, (byte) 0xDD, (byte) 0xAB,
-            (byte) 0xD4, (byte) 0xDD, (byte) 0xFC, (byte) 0x54, (byte) 0x37, (byte) 0x32,
-            (byte) 0x4B, (byte) 0xA3, (byte) 0xFB, (byte) 0x23, (byte) 0xA1, (byte) 0xC1,
-            (byte) 0x60, (byte) 0xDF, (byte) 0x41, (byte) 0xB0, (byte) 0xD1, (byte) 0xCC,
-            (byte) 0xDF, (byte) 0xAD, (byte) 0xB3, (byte) 0x66, (byte) 0x76, (byte) 0x36,
-            (byte) 0xEC, (byte) 0x6A, (byte) 0x53, (byte) 0xC3, (byte) 0xE2, (byte) 0xB0,
-            (byte) 0x77, (byte) 0xBE, (byte) 0x75, (byte) 0x08, (byte) 0xBA, (byte) 0x17,
-            (byte) 0x14, (byte) 0xFA, (byte) 0x1A, (byte) 0x30, (byte) 0xE7, (byte) 0xB9,
-            (byte) 0xED, (byte) 0xD6, (byte) 0xC1, (byte) 0xA5, (byte) 0x7A, (byte) 0x2B,
-            (byte) 0xA3, (byte) 0xA3, (byte) 0xDD, (byte) 0xDC, (byte) 0x14, (byte) 0xDB,
-            (byte) 0x7F, (byte) 0xF4, (byte) 0xF3, (byte) 0xAF, (byte) 0xCF, (byte) 0x0A,
-            (byte) 0xD3, (byte) 0xAC, (byte) 0x84, (byte) 0x39, (byte) 0x30, (byte) 0xCA,
-            (byte) 0x3C, (byte) 0xD8, (byte) 0xF7, (byte) 0xFA, (byte) 0x29, (byte) 0xDB,
-            (byte) 0x31, (byte) 0xA5, (byte) 0x62, (byte) 0x82, (byte) 0xD2, (byte) 0xB8,
-            (byte) 0x3C, (byte) 0xBC, (byte) 0x8F, (byte) 0xAB, (byte) 0xE4, (byte) 0xE8,
-            (byte) 0xA7, (byte) 0x2C, (byte) 0xEF, (byte) 0xC7, (byte) 0xD5, (byte) 0x12,
-            (byte) 0x16, (byte) 0x04, (byte) 0x6F, (byte) 0xCA, (byte) 0xEA, (byte) 0x31,
-            (byte) 0x9F, (byte) 0x41, (byte) 0xE0, (byte) 0x6F, (byte) 0xE4, (byte) 0x74,
-            (byte) 0x03, (byte) 0x78, (byte) 0xFA, (byte) 0x48, (byte) 0xB4, (byte) 0x6E,
-            (byte) 0xC8, (byte) 0xE7, (byte) 0x40, (byte) 0x8B, (byte) 0x88, (byte) 0x2F,
-            (byte) 0xED, (byte) 0x8E, (byte) 0x68, (byte) 0x96, (byte) 0x2C, (byte) 0xA7,
-            (byte) 0xB6, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x00};
+public abstract class AbstractSessionContextTest<T extends AbstractSessionContext> {
+    private T context;
 
-    private static final byte[] DUMMY_CERT =
-            new byte[] {(byte) 0x30, (byte) 0x82, (byte) 0x02, (byte) 0x58, (byte) 0x30,
-                    (byte) 0x82, (byte) 0x01, (byte) 0xC1, (byte) 0xA0, (byte) 0x03, (byte) 0x02,
-                    (byte) 0x01, (byte) 0x02, (byte) 0x02, (byte) 0x09, (byte) 0x00, (byte) 0xFB,
-                    (byte) 0xB0, (byte) 0x4C, (byte) 0x2E, (byte) 0xAB, (byte) 0x10, (byte) 0x9B,
-                    (byte) 0x0C, (byte) 0x30, (byte) 0x0D, (byte) 0x06, (byte) 0x09, (byte) 0x2A,
-                    (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01,
-                    (byte) 0x01, (byte) 0x05, (byte) 0x05, (byte) 0x00, (byte) 0x30, (byte) 0x45,
-                    (byte) 0x31, (byte) 0x0B, (byte) 0x30, (byte) 0x09, (byte) 0x06, (byte) 0x03,
-                    (byte) 0x55, (byte) 0x04, (byte) 0x06, (byte) 0x13, (byte) 0x02, (byte) 0x41,
-                    (byte) 0x55, (byte) 0x31, (byte) 0x13, (byte) 0x30, (byte) 0x11, (byte) 0x06,
-                    (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x08, (byte) 0x0C, (byte) 0x0A,
-                    (byte) 0x53, (byte) 0x6F, (byte) 0x6D, (byte) 0x65, (byte) 0x2D, (byte) 0x53,
-                    (byte) 0x74, (byte) 0x61, (byte) 0x74, (byte) 0x65, (byte) 0x31, (byte) 0x21,
-                    (byte) 0x30, (byte) 0x1F, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04,
-                    (byte) 0x0A, (byte) 0x0C, (byte) 0x18, (byte) 0x49, (byte) 0x6E, (byte) 0x74,
-                    (byte) 0x65, (byte) 0x72, (byte) 0x6E, (byte) 0x65, (byte) 0x74, (byte) 0x20,
-                    (byte) 0x57, (byte) 0x69, (byte) 0x64, (byte) 0x67, (byte) 0x69, (byte) 0x74,
-                    (byte) 0x73, (byte) 0x20, (byte) 0x50, (byte) 0x74, (byte) 0x79, (byte) 0x20,
-                    (byte) 0x4C, (byte) 0x74, (byte) 0x64, (byte) 0x30, (byte) 0x1E, (byte) 0x17,
-                    (byte) 0x0D, (byte) 0x31, (byte) 0x34, (byte) 0x30, (byte) 0x34, (byte) 0x32,
-                    (byte) 0x33, (byte) 0x32, (byte) 0x30, (byte) 0x35, (byte) 0x30, (byte) 0x34,
-                    (byte) 0x30, (byte) 0x5A, (byte) 0x17, (byte) 0x0D, (byte) 0x31, (byte) 0x37,
-                    (byte) 0x30, (byte) 0x34, (byte) 0x32, (byte) 0x32, (byte) 0x32, (byte) 0x30,
-                    (byte) 0x35, (byte) 0x30, (byte) 0x34, (byte) 0x30, (byte) 0x5A, (byte) 0x30,
-                    (byte) 0x45, (byte) 0x31, (byte) 0x0B, (byte) 0x30, (byte) 0x09, (byte) 0x06,
-                    (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x06, (byte) 0x13, (byte) 0x02,
-                    (byte) 0x41, (byte) 0x55, (byte) 0x31, (byte) 0x13, (byte) 0x30, (byte) 0x11,
-                    (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x08, (byte) 0x0C,
-                    (byte) 0x0A, (byte) 0x53, (byte) 0x6F, (byte) 0x6D, (byte) 0x65, (byte) 0x2D,
-                    (byte) 0x53, (byte) 0x74, (byte) 0x61, (byte) 0x74, (byte) 0x65, (byte) 0x31,
-                    (byte) 0x21, (byte) 0x30, (byte) 0x1F, (byte) 0x06, (byte) 0x03, (byte) 0x55,
-                    (byte) 0x04, (byte) 0x0A, (byte) 0x0C, (byte) 0x18, (byte) 0x49, (byte) 0x6E,
-                    (byte) 0x74, (byte) 0x65, (byte) 0x72, (byte) 0x6E, (byte) 0x65, (byte) 0x74,
-                    (byte) 0x20, (byte) 0x57, (byte) 0x69, (byte) 0x64, (byte) 0x67, (byte) 0x69,
-                    (byte) 0x74, (byte) 0x73, (byte) 0x20, (byte) 0x50, (byte) 0x74, (byte) 0x79,
-                    (byte) 0x20, (byte) 0x4C, (byte) 0x74, (byte) 0x64, (byte) 0x30, (byte) 0x81,
-                    (byte) 0x9F, (byte) 0x30, (byte) 0x0D, (byte) 0x06, (byte) 0x09, (byte) 0x2A,
-                    (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01,
-                    (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x00, (byte) 0x03, (byte) 0x81,
-                    (byte) 0x8D, (byte) 0x00, (byte) 0x30, (byte) 0x81, (byte) 0x89, (byte) 0x02,
-                    (byte) 0x81, (byte) 0x81, (byte) 0x00, (byte) 0xD8, (byte) 0x2B, (byte) 0xC8,
-                    (byte) 0xA6, (byte) 0x32, (byte) 0xE4, (byte) 0x62, (byte) 0xFF, (byte) 0x4D,
-                    (byte) 0xF3, (byte) 0xD0, (byte) 0xAD, (byte) 0x59, (byte) 0x8B, (byte) 0x45,
-                    (byte) 0xA7, (byte) 0xBD, (byte) 0xF1, (byte) 0x47, (byte) 0xBF, (byte) 0x09,
-                    (byte) 0x58, (byte) 0x7B, (byte) 0x22, (byte) 0xBD, (byte) 0x35, (byte) 0xAE,
-                    (byte) 0x97, (byte) 0x25, (byte) 0x86, (byte) 0x94, (byte) 0xA0, (byte) 0x80,
-                    (byte) 0xC0, (byte) 0xB4, (byte) 0x1F, (byte) 0x76, (byte) 0x91, (byte) 0x67,
-                    (byte) 0x46, (byte) 0x31, (byte) 0xD0, (byte) 0x10, (byte) 0x84, (byte) 0xB7,
-                    (byte) 0x22, (byte) 0x1E, (byte) 0x70, (byte) 0x23, (byte) 0x91, (byte) 0x72,
-                    (byte) 0xC8, (byte) 0xE9, (byte) 0x6D, (byte) 0x79, (byte) 0x3A, (byte) 0x85,
-                    (byte) 0x77, (byte) 0x80, (byte) 0x0F, (byte) 0xC4, (byte) 0x95, (byte) 0x16,
-                    (byte) 0x75, (byte) 0xC5, (byte) 0x4A, (byte) 0x71, (byte) 0x4C, (byte) 0xC8,
-                    (byte) 0x63, (byte) 0x3F, (byte) 0xA3, (byte) 0xF2, (byte) 0x63, (byte) 0x9C,
-                    (byte) 0x2A, (byte) 0x4F, (byte) 0x9A, (byte) 0xFA, (byte) 0xCB, (byte) 0xC1,
-                    (byte) 0x71, (byte) 0x6E, (byte) 0x28, (byte) 0x85, (byte) 0x28, (byte) 0xA0,
-                    (byte) 0x27, (byte) 0x1E, (byte) 0x65, (byte) 0x1C, (byte) 0xAE, (byte) 0x07,
-                    (byte) 0xD5, (byte) 0x5B, (byte) 0x6F, (byte) 0x2D, (byte) 0x43, (byte) 0xED,
-                    (byte) 0x2B, (byte) 0x90, (byte) 0xB1, (byte) 0x8C, (byte) 0xAF, (byte) 0x24,
-                    (byte) 0x6D, (byte) 0xAE, (byte) 0xE9, (byte) 0x17, (byte) 0x3A, (byte) 0x05,
-                    (byte) 0xC1, (byte) 0xBF, (byte) 0xB8, (byte) 0x1C, (byte) 0xAE, (byte) 0x65,
-                    (byte) 0x3B, (byte) 0x1B, (byte) 0x58, (byte) 0xC2, (byte) 0xD9, (byte) 0xAE,
-                    (byte) 0xD6, (byte) 0xAA, (byte) 0x67, (byte) 0x88, (byte) 0xF1, (byte) 0x02,
-                    (byte) 0x03, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0xA3, (byte) 0x50,
-                    (byte) 0x30, (byte) 0x4E, (byte) 0x30, (byte) 0x1D, (byte) 0x06, (byte) 0x03,
-                    (byte) 0x55, (byte) 0x1D, (byte) 0x0E, (byte) 0x04, (byte) 0x16, (byte) 0x04,
-                    (byte) 0x14, (byte) 0x8B, (byte) 0x75, (byte) 0xD5, (byte) 0xAC, (byte) 0xCB,
-                    (byte) 0x08, (byte) 0xBE, (byte) 0x0E, (byte) 0x1F, (byte) 0x65, (byte) 0xB7,
-                    (byte) 0xFA, (byte) 0x56, (byte) 0xBE, (byte) 0x6C, (byte) 0xA7, (byte) 0x75,
-                    (byte) 0xDA, (byte) 0x85, (byte) 0xAF, (byte) 0x30, (byte) 0x1F, (byte) 0x06,
-                    (byte) 0x03, (byte) 0x55, (byte) 0x1D, (byte) 0x23, (byte) 0x04, (byte) 0x18,
-                    (byte) 0x30, (byte) 0x16, (byte) 0x80, (byte) 0x14, (byte) 0x8B, (byte) 0x75,
-                    (byte) 0xD5, (byte) 0xAC, (byte) 0xCB, (byte) 0x08, (byte) 0xBE, (byte) 0x0E,
-                    (byte) 0x1F, (byte) 0x65, (byte) 0xB7, (byte) 0xFA, (byte) 0x56, (byte) 0xBE,
-                    (byte) 0x6C, (byte) 0xA7, (byte) 0x75, (byte) 0xDA, (byte) 0x85, (byte) 0xAF,
-                    (byte) 0x30, (byte) 0x0C, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x1D,
-                    (byte) 0x13, (byte) 0x04, (byte) 0x05, (byte) 0x30, (byte) 0x03, (byte) 0x01,
-                    (byte) 0x01, (byte) 0xFF, (byte) 0x30, (byte) 0x0D, (byte) 0x06, (byte) 0x09,
-                    (byte) 0x2A, (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xF7, (byte) 0x0D,
-                    (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x05, (byte) 0x00, (byte) 0x03,
-                    (byte) 0x81, (byte) 0x81, (byte) 0x00, (byte) 0x3B, (byte) 0xE8, (byte) 0x78,
-                    (byte) 0x6D, (byte) 0x95, (byte) 0xD6, (byte) 0x3D, (byte) 0x6A, (byte) 0xF7,
-                    (byte) 0x13, (byte) 0x19, (byte) 0x2C, (byte) 0x1B, (byte) 0xC2, (byte) 0x88,
-                    (byte) 0xAE, (byte) 0x22, (byte) 0xAB, (byte) 0xF4, (byte) 0x8D, (byte) 0x32,
-                    (byte) 0xF5, (byte) 0x7C, (byte) 0x71, (byte) 0x67, (byte) 0xCF, (byte) 0x2D,
-                    (byte) 0xD1, (byte) 0x1C, (byte) 0xC2, (byte) 0xC3, (byte) 0x87, (byte) 0xE2,
-                    (byte) 0xE9, (byte) 0xBE, (byte) 0x89, (byte) 0x5C, (byte) 0xE4, (byte) 0x34,
-                    (byte) 0xAB, (byte) 0x48, (byte) 0x91, (byte) 0xC2, (byte) 0x3F, (byte) 0x95,
-                    (byte) 0xAE, (byte) 0x2B, (byte) 0x47, (byte) 0x9E, (byte) 0x25, (byte) 0x78,
-                    (byte) 0x6B, (byte) 0x4F, (byte) 0x9A, (byte) 0x10, (byte) 0xA4, (byte) 0x72,
-                    (byte) 0xFD, (byte) 0xCF, (byte) 0xF7, (byte) 0x02, (byte) 0x0C, (byte) 0xB0,
-                    (byte) 0x0A, (byte) 0x08, (byte) 0xA4, (byte) 0x5A, (byte) 0xE2, (byte) 0xE5,
-                    (byte) 0x74, (byte) 0x7E, (byte) 0x11, (byte) 0x1D, (byte) 0x39, (byte) 0x60,
-                    (byte) 0x6A, (byte) 0xC9, (byte) 0x1F, (byte) 0x69, (byte) 0xF3, (byte) 0x2E,
-                    (byte) 0x63, (byte) 0x26, (byte) 0xDC, (byte) 0x9E, (byte) 0xEF, (byte) 0x6B,
-                    (byte) 0x7A, (byte) 0x0A, (byte) 0xE1, (byte) 0x54, (byte) 0x57, (byte) 0x98,
-                    (byte) 0xAA, (byte) 0x72, (byte) 0x91, (byte) 0x78, (byte) 0x04, (byte) 0x7E,
-                    (byte) 0x1F, (byte) 0x8F, (byte) 0x65, (byte) 0x4D, (byte) 0x1F, (byte) 0x0B,
-                    (byte) 0x12, (byte) 0xAC, (byte) 0x9C, (byte) 0x24, (byte) 0x0F, (byte) 0x84,
-                    (byte) 0x14, (byte) 0x1A, (byte) 0x55, (byte) 0x2D, (byte) 0x1F, (byte) 0xBB,
-                    (byte) 0xF0, (byte) 0x9D, (byte) 0x09, (byte) 0xB2, (byte) 0x08, (byte) 0x5C,
-                    (byte) 0x59, (byte) 0x32, (byte) 0x65, (byte) 0x80, (byte) 0x26};
-
-    private static final byte[] DUMMY_OCSP_DATA = new byte[1];
-
-    private static final byte[] DUMMY_TLS_SCT_DATA = new byte[1];
-
-    private static ClientSessionContext clientCtx;
-
-    @BeforeClass
-    public static void setup() {
-        clientCtx = new ClientSessionContext();
+    @Before
+    public void setup() {
+        context = newContext();
     }
 
-    @After
-    public void tearDown() throws Exception {
-        assertEquals(0, NativeCrypto.ERR_peek_last_error());
-    }
+    abstract T newContext();
+    abstract int size(T context);
+    abstract SslSessionWrapper getCachedSession(T context, SslSessionWrapper s);
 
-    private static TestSessionBuilder getType1() {
-        return new TestSessionBuilder()
-                .setType(0x01)
-                .setSessionData(kOpenSSLSession)
-                .addCertificate(DUMMY_CERT);
-    }
+    @Test
+    public void testSimpleAddition() {
+        SslSessionWrapper a = newSession("a");
+        SslSessionWrapper b = newSession("b");
 
-    private static TestSessionBuilder getType2() {
-        return new TestSessionBuilder()
-                .setType(0x02)
-                .setSessionData(kOpenSSLSession)
-                .addCertificate(DUMMY_CERT)
-                .addOcspData(DUMMY_OCSP_DATA);
-    }
+        context.cacheSession(a);
+        assertSessionContextContents(toArray(a), toArray(b));
 
-    private static TestSessionBuilder getType3() {
-        return new TestSessionBuilder()
-                .setType(0x03)
-                .setSessionData(kOpenSSLSession)
-                .addCertificate(DUMMY_CERT)
-                .addOcspData(DUMMY_OCSP_DATA)
-                .setTlsSctData(DUMMY_TLS_SCT_DATA);
+        context.cacheSession(b);
+        assertSessionContextContents(toArray(a, b), toArray());
     }
 
     @Test
-    public void toSession_EmptyArray_Invalid_Failure() throws Exception {
-        assertInvalidSession(new byte[0]);
+    public void testTrimToSize() {
+        SslSessionWrapper a = newSession("a");
+        SslSessionWrapper b = newSession("b");
+        SslSessionWrapper c = newSession("c");
+        SslSessionWrapper d = newSession("d");
+
+        context.cacheSession(a);
+        context.cacheSession(b);
+        context.cacheSession(c);
+        context.cacheSession(d);
+        assertSessionContextContents(toArray(a, b, c, d), toArray());
+
+        context.setSessionCacheSize(2);
+        assertSessionContextContents(toArray(c, d), toArray(a, b));
     }
 
     @Test
-    public void toSession_Type1_Valid_Success() throws Exception {
-        assertValidSession(getType1().build());
+    public void testImplicitRemovalOfOldest() {
+        context.setSessionCacheSize(2);
+        SslSessionWrapper a = newSession("a");
+        SslSessionWrapper b = newSession("b");
+        SslSessionWrapper c = newSession("c");
+        SslSessionWrapper d = newSession("d");
+
+        context.cacheSession(a);
+        assertSessionContextContents(toArray(a), toArray(b, c, d));
+
+        context.cacheSession(b);
+        assertSessionContextContents(toArray(a, b), toArray(c, d));
+
+        context.cacheSession(c);
+        assertSessionContextContents(toArray(b, c), toArray(a, d));
+
+        context.cacheSession(d);
+        assertSessionContextContents(toArray(c, d), toArray(a, b));
     }
 
     @Test
-    public void toSession_Type2_Valid_Success() throws Exception {
-        assertValidSession(getType2().build());
+    public void testSerializeSession() throws Exception {
+        Certificate mockCert = mock(Certificate.class);
+        when(mockCert.getEncoded()).thenReturn(new byte[] {0x05, 0x06, 0x07, 0x10});
+
+        byte[] encodedBytes = new byte[] {0x01, 0x02, 0x03};
+        SslSessionWrapper session = new MockSessionBuilder()
+                .id(new byte[] {0x11, 0x09, 0x03, 0x20})
+                .host("ssl.example.com")
+                .encodedBytes(encodedBytes)
+                .build();
+
+        SSLClientSessionCache mockCache = mock(SSLClientSessionCache.class);
+        ClientSessionContext context = new ClientSessionContext();
+        context.setPersistentCache(mockCache);
+
+        context.cacheSession(session);
+        verify(mockCache).putSessionData(any(SSLSession.class), same(encodedBytes));
     }
 
-    @Test
-    public void toSession_Type3_Valid_Success() throws Exception {
-        assertValidSession(getType3().build());
-    }
+    private void assertSessionContextContents(
+            SslSessionWrapper[] contains, SslSessionWrapper[] exludes) {
+        assertEquals(contains.length, size(context));
 
-    private void assertTruncatedSessionFails(byte[] validSession) {
-        for (int i = 0; i < validSession.length - 1; i++) {
-            byte[] truncatedSession = new byte[i];
-            System.arraycopy(validSession, 0, truncatedSession, 0, i);
-            assertNull("Truncating to " + i + " bytes of " + validSession.length
-                            + " should not succeed",
-                    clientCtx.toSession(truncatedSession, "www.google.com", 443));
+        for (SslSessionWrapper s : contains) {
+            assertSame(s.getPeerHost(), s, getCachedSession(context, s));
+        }
+        for (SslSessionWrapper s : exludes) {
+            assertNull(s.getPeerHost(), getCachedSession(context, s));
         }
     }
 
-    @Test
-    public void toSession_Type3_Truncated_Failure() throws Exception {
-        assertTruncatedSessionFails(getType3().build());
+    private static SslSessionWrapper[] toArray(SslSessionWrapper... sessions) {
+        return sessions;
     }
 
-    private static void assertValidSession(byte[] data) {
-        assertNotNull(clientCtx.toSession(data, "www.google.com", 443));
-    }
-
-    private static void assertInvalidSession(byte[] data) {
-        assertNull(clientCtx.toSession(data, "www.google.com", 443));
-    }
-
-    @Test
-    public void toSession_UnknownType_Failure() throws Exception {
-        assertInvalidSession(getType3().setType((byte) 0xEE).build());
-    }
-
-    @Test
-    public void toSession_CertificatesCountTooLarge_Failure() throws Exception {
-        assertInvalidSession(getType3().setCertificatesLength(16834).build());
-    }
-
-    @Test
-    public void toSession_CertificatesCountNegative_Failure() throws Exception {
-        assertInvalidSession(getType3().setCertificatesLength(-1).build());
-    }
-
-    @Test
-    public void toSession_CertificateSizeNegative_Failure() throws Exception {
-        assertInvalidSession(getType3().setCertificateLength(0, -1).build());
-    }
-
-    @Test
-    public void toSession_CertificateSizeTooLarge_Failure() throws Exception {
-        assertInvalidSession(getType3().setCertificateLength(0, 16834).build());
-    }
-
-    @Test
-    public void toSession_SessionDataSizeTooLarge_Failure() throws Exception {
-        assertInvalidSession(getType3().setSessionDataLength(16834).build());
-    }
-
-    @Test
-    public void toSession_SessionDataSizeNegative_Failure() throws Exception {
-        assertInvalidSession(getType3().setSessionDataLength(-1).build());
-    }
-
-    @Test
-    public void toSession_OcspDatasNumberTooMany_Failure() throws Exception {
-        assertInvalidSession(getType3().setOcspDatasLength(32791).build());
-    }
-
-    @Test
-    public void toSession_OcspDatasNumberNegative_Failure() throws Exception {
-        assertInvalidSession(getType3().setOcspDatasLength(-1).build());
-    }
-
-    @Test
-    public void toSession_OcspDataSizeNegative_Failure() throws Exception {
-        assertInvalidSession(getType3().setOcspDataLength(0, -1).build());
-    }
-
-    @Test
-    public void toSession_OcspDataSizeTooLarge_Failure() throws Exception {
-        assertInvalidSession(getType3().setOcspDataLength(0, 92948).build());
-    }
-
-    @Test
-    public void toSession_TlsSctDataSizeNegative_Failure() throws Exception {
-        assertInvalidSession(getType3().setTlsSctDataLength(-1).build());
-    }
-
-    @Test
-    public void toSession_TlsSctDataSizeTooLarge_Failure() throws Exception {
-        assertInvalidSession(getType3().setTlsSctDataLength(931148).build());
-    }
-
-    @Test
-    public void toSession_Type2OcspDataEmpty_Success() throws Exception {
-        assertValidSession(getType1().setType(0x02).setOcspDataEmpty().build());
-    }
-
-    @Test
-    public void toSession_Type3TlsSctDataEmpty_Success() throws Exception {
-        assertValidSession(getType2().setType(0x03).setTlsSctDataEmpty().build());
-    }
-
-    @Test
-    public void toSession_Type3OcspAndTlsSctDataEmpty_Success() throws Exception {
-        assertValidSession(
-                getType1().setType(0x03).setOcspDataEmpty().setTlsSctDataEmpty().build());
-    }
-
-    private static void assertTrailingDataFails(byte[] validSession) {
-        byte[] invalidSession = new byte[validSession.length + 1];
-        System.arraycopy(validSession, 0, invalidSession, 0, validSession.length);
-        assertInvalidSession(invalidSession);
-    }
-
-    @Test
-    public void toSession_Type1TrailingData_Failure() throws Exception {
-        assertTrailingDataFails(getType1().build());
-    }
-
-    @Test
-    public void toSession_Type2TrailingData_Failure() throws Exception {
-        assertTrailingDataFails(getType2().build());
-    }
-
-    @Test
-    public void toSession_Type3TrailingData_Failure() throws Exception {
-        assertTrailingDataFails(getType3().build());
-    }
-
-    @Test
-    public void test_reserializableFromByteArray_roundTrip_type1() throws Exception {
-        // Converting OPEN_SSL (type 1) -> OPEN_SSL_WITH_TLS_SCT (type 3) adds
-        // eight zero-bytes:
-        //  1.) 4 bytes for int32 value 0 == countOcspResponses
-        //  2.) 4 bytes for int32 value 0 == tlsSctDataLength
-        // since OPEN_SSL (type 1) cannot contain OSCP or TLS SCT data.
-        check_reserializableFromByteArray_roundTrip(getType1().build(), new byte[8]);
-    }
-
-    @Test
-    public void test_reserializableFromByteArray_roundTrip_type2() throws Exception {
-        // Converting OPEN_SSL_WITH_OCSP (type 2) -> OPEN_SSL_WITH_TLS_SCT (type 3) adds
-        // four zero-bytes for int32 value 0 == tlsSctDataLength
-        // since OPEN_SSL_WITH_OCSP (type 2) cannot contain TLS SCT data.
-        check_reserializableFromByteArray_roundTrip(getType2().build(), new byte[4]);
-    }
-
-    @Test
-    public void test_reserializableFromByteArray_roundTrip_type3() throws Exception {
-        check_reserializableFromByteArray_roundTrip(getType3().build(), new byte[0]);
-    }
-
-    private static void check_reserializableFromByteArray_roundTrip(
-            byte[] data, byte[] expectedTrailingBytesAfterReserialization) throws Exception {
-        SSLSession session = clientCtx.toSession(data, "www.example.com", 12345);
-        byte[] sessionBytes = clientCtx.toBytes(session);
-
-        SSLSession session2 = clientCtx.toSession(sessionBytes, "www.example.com", 12345);
-        byte[] sessionBytes2 = clientCtx.toBytes(session);
-
-        assertSSLSessionEquals(session, session2);
-        assertByteArrayEquals(sessionBytes, sessionBytes2);
-
-        assertEquals("www.example.com", session.getPeerHost());
-        assertEquals(12345, session.getPeerPort());
-        assertTrue(sessionBytes.length >= data.length);
-
-        byte[] expectedReserializedData = concat(data, expectedTrailingBytesAfterReserialization);
-        // AbstractSessionContext.toBytes() always writes type 3 == OPEN_SSL_WITH_TLS_SCT
-        expectedReserializedData[3] = 3;
-        assertByteArrayEquals(expectedReserializedData, sessionBytes);
-    }
-
-    private static byte[] concat(byte[] a, byte[] b) {
-        byte[] result = new byte[a.length + b.length];
-        System.arraycopy(a, 0, result, 0, a.length);
-        System.arraycopy(b, 0, result, a.length, b.length);
-        return result;
-    }
-
-    private static void assertSSLSessionEquals(SSLSession a, SSLSession b) throws Exception {
-        assertEquals(a.getApplicationBufferSize(), b.getApplicationBufferSize());
-        assertEquals(a.getCipherSuite(), b.getCipherSuite());
-        assertEquals(a.getCreationTime(), b.getCreationTime());
-        assertByteArrayEquals(a.getId(), b.getId());
-        assertEquals(a.getLastAccessedTime(), b.getLastAccessedTime());
-        assertArrayEquals(a.getLocalCertificates(), b.getLocalCertificates());
-        assertEquals(a.getLocalPrincipal(), b.getLocalPrincipal());
-        assertArrayEquals(a.getPeerCertificateChain(), b.getPeerCertificateChain());
-        assertArrayEquals(a.getPeerCertificates(), b.getPeerCertificates());
-        assertEquals(a.getPeerHost(), b.getPeerHost());
-        assertEquals(a.getPeerPort(), b.getPeerPort());
-        assertEquals(a.getPeerPrincipal(), b.getPeerPrincipal());
-        assertEquals(a.getProtocol(), b.getProtocol());
-        assertEquals(getValueMap(a), getValueMap(b));
-        assertEquals(a.isValid(), b.isValid());
-
-        assertEquals(a.getClass(), b.getClass());
-
-        // Could potentially cast to AbstractOpenSSLSession here and compare additional fields.
-    }
-
-    private static Map<String, Object> getValueMap(SSLSession sslSession) {
-        Map<String, Object> result = new HashMap<>();
-        for (String valueName : sslSession.getValueNames()) {
-            result.put(valueName, sslSession.getValue(valueName));
-        }
-        return Collections.unmodifiableMap(result);
-    }
-
-    private static <T> void assertArrayEquals(T[] expected, T[] actual) {
-        assertTrue("Expected " + Arrays.toString(expected) + ", got " + Arrays.toString(actual),
-                Arrays.equals(expected, actual));
-    }
-
-    private static void assertByteArrayEquals(byte[] expected, byte[] actual) {
-        // If running on OpenJDK 8+, could use java.util.Base64 for better failure messages:
-        // assertEquals(Base64.encode(expected), Base64.encode(actual));
-        assertTrue("Expected " + Arrays.toString(expected) + ", got " + Arrays.toString(actual),
-                Arrays.equals(expected, actual));
+    private SslSessionWrapper newSession(String host) {
+        return new MockSessionBuilder().host(host).build();
     }
 }
diff --git a/openjdk/src/test/java/org/conscrypt/ClientSessionContextTest.java b/openjdk/src/test/java/org/conscrypt/ClientSessionContextTest.java
index f214d7b..709b893 100644
--- a/openjdk/src/test/java/org/conscrypt/ClientSessionContextTest.java
+++ b/openjdk/src/test/java/org/conscrypt/ClientSessionContextTest.java
@@ -16,135 +16,36 @@
 
 package org.conscrypt;
 
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.conscrypt.MockSessionBuilder.DEFAULT_PORT;
 
-import java.security.cert.Certificate;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashSet;
-import java.util.Set;
-import javax.net.ssl.SSLSession;
-import junit.framework.TestCase;
-import libcore.javax.net.ssl.FakeSSLSession;
+import java.security.KeyManagementException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
-public final class ClientSessionContextTest extends TestCase {
+@RunWith(JUnit4.class)
+public class ClientSessionContextTest extends AbstractSessionContextTest<ClientSessionContext> {
 
-    public void testSimpleAddition() {
-        ClientSessionContext context = new ClientSessionContext();
-        SSLSession a = new ValidSSLSession("a");
-        SSLSession b = new ValidSSLSession("b");
-
-        context.putSession(a);
-        assertSessionContextContents(context, new SSLSession[] { a }, new SSLSession[] { b });
-
-        context.putSession(b);
-        assertSessionContextContents(context, new SSLSession[] { a, b }, new SSLSession[0]);
+    @Override
+    ClientSessionContext newContext() {
+        return new ClientSessionContext();
     }
 
-    public void testTrimToSize() {
-        ClientSessionContext context = new ClientSessionContext();
-        ValidSSLSession a = new ValidSSLSession("a");
-        ValidSSLSession b = new ValidSSLSession("b");
-        ValidSSLSession c = new ValidSSLSession("c");
-        ValidSSLSession d = new ValidSSLSession("d");
-
-        context.putSession(a);
-        assertSessionContextContents(context, new SSLSession[] { a }, new SSLSession[] { b, c, d });
-
-        context.putSession(b);
-        assertSessionContextContents(context, new SSLSession[] { a, b }, new SSLSession[] { c, d });
-
-        context.putSession(c);
-        assertSessionContextContents(context, new SSLSession[] { a, b, c }, new SSLSession[] { d });
-
-        context.putSession(d);
-        assertSessionContextContents(context, new SSLSession[] { a, b, c, d }, new SSLSession[0]);
-
-        context.setSessionCacheSize(2);
-        assertSessionContextContents(context, new SSLSession[] { c, d }, new SSLSession[] { a, b });
+    @Override
+    SslSessionWrapper getCachedSession(ClientSessionContext context, SslSessionWrapper s) {
+        return context.getCachedSession(s.getPeerHost(), DEFAULT_PORT,
+                getDefaultSSLParameters());
     }
 
-    public void testImplicitRemovalOfOldest() {
-        ClientSessionContext context = new ClientSessionContext();
-        context.setSessionCacheSize(2);
-        ValidSSLSession a = new ValidSSLSession("a");
-        ValidSSLSession b = new ValidSSLSession("b");
-        ValidSSLSession c = new ValidSSLSession("c");
-        ValidSSLSession d = new ValidSSLSession("d");
-
-        context.putSession(a);
-        assertSessionContextContents(context, new SSLSession[] { a }, new SSLSession[] { b, c, d });
-
-        context.putSession(b);
-        assertSessionContextContents(context, new SSLSession[] { a, b }, new SSLSession[] { c, d });
-
-        context.putSession(c);
-        assertSessionContextContents(context, new SSLSession[] { b, c }, new SSLSession[] { a, d });
-
-        context.putSession(d);
-        assertSessionContextContents(context, new SSLSession[] { c, d }, new SSLSession[] { a, b });
+    @Override
+    int size(ClientSessionContext context) {
+        return context.size();
     }
 
-    public void testSerializeSession_NoStatusResponses() throws Exception {
-        OpenSSLSessionImpl mockSession = mock(OpenSSLSessionImpl.class);
-        when(mockSession.getId()).thenReturn(new byte[] { 0x11, 0x09, 0x03, 0x20 });
-        when(mockSession.getPeerHost()).thenReturn("ssl.example.com");
-        when(mockSession.getPeerPort()).thenReturn(443);
-        when(mockSession.getEncoded()).thenReturn(new byte[] { 0x01, 0x02, 0x03 });
-        when(mockSession.getStatusResponses()).thenReturn(Collections.<byte[]>emptyList());
-
-        Certificate mockCert = mock(Certificate.class);
-        when(mockCert.getEncoded()).thenReturn(new byte[] { 0x05, 0x06, 0x07, 0x10 });
-
-        when(mockSession.getPeerCertificates()).thenReturn(new Certificate[] { mockCert });
-
-        SSLClientSessionCache mockCache = mock(SSLClientSessionCache.class);
-        ClientSessionContext context = new ClientSessionContext();
-        context.setPersistentCache(mockCache);
-
-        context.putSession(mockSession);
-        verify(mockCache).putSessionData(eq(mockSession), any(byte[].class));
-    }
-
-
-    private static void assertSessionContextContents(ClientSessionContext context,
-                                                     SSLSession[] contains,
-                                                     SSLSession[] exludes) {
-        assertEquals(contains.length, context.size());
-
-        for (SSLSession s : contains) {
-            assertSame(s.getPeerHost(), s, context.getSession(s.getId()));
-            assertSame(s.getPeerHost(), s, context.getSession(s.getPeerHost(), 443));
-        }
-        for (SSLSession s : exludes) {
-            assertNull(s.getPeerHost(), context.getSession(s.getId()));
-            assertNull(s.getPeerHost(), context.getSession(s.getPeerHost(), 443));
-        }
-
-        Set<SSLSession> sessions = new HashSet<SSLSession>();
-        Enumeration<byte[]> ids = context.getIds();
-        while (ids.hasMoreElements()) {
-            byte[] id = ids.nextElement();
-            sessions.add(context.getSession(id));
-        }
-
-        Set<SSLSession> expected = new HashSet<SSLSession>();
-        for (SSLSession s : sessions) {
-            expected.add(s);
-        }
-        assertEquals(expected, sessions);
-    }
-
-    static class ValidSSLSession extends FakeSSLSession {
-        ValidSSLSession(String host) {
-            super(host);
-        }
-        @Override public boolean isValid() {
-            return true;
+    private static SSLParametersImpl getDefaultSSLParameters() {
+        try {
+            return SSLParametersImpl.getDefault();
+        } catch (KeyManagementException e) {
+            throw new RuntimeException(e);
         }
     }
 }
diff --git a/openjdk/src/test/java/org/conscrypt/OpenSSLEngineImplTest.java b/openjdk/src/test/java/org/conscrypt/ConscryptEngineTest.java
similarity index 73%
rename from openjdk/src/test/java/org/conscrypt/OpenSSLEngineImplTest.java
rename to openjdk/src/test/java/org/conscrypt/ConscryptEngineTest.java
index 27df90e..ecbf8b3 100644
--- a/openjdk/src/test/java/org/conscrypt/OpenSSLEngineImplTest.java
+++ b/openjdk/src/test/java/org/conscrypt/ConscryptEngineTest.java
@@ -16,7 +16,9 @@
 
 package org.conscrypt;
 
+import static org.conscrypt.Conscrypt.Engines.setBufferAllocator;
 import static org.conscrypt.TestUtils.PROTOCOL_TLS_V1_2;
+import static org.conscrypt.TestUtils.TEST_CIPHER;
 import static org.conscrypt.TestUtils.initEngine;
 import static org.conscrypt.TestUtils.initSslContext;
 import static org.conscrypt.TestUtils.newTextMessage;
@@ -44,18 +46,24 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class OpenSSLEngineImplTest {
-    private static final String CIPHER = "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256";
+public class ConscryptEngineTest {
     private static final int MESSAGE_SIZE = 4096;
 
+    @SuppressWarnings("ImmutableEnumChecker")
     public enum BufferType {
-        HEAP {
+        HEAP_ALLOCATOR(BufferAllocator.unpooled()) {
             @Override
             ByteBuffer newBuffer(int size) {
                 return ByteBuffer.allocate(size);
             }
         },
-        DIRECT {
+        HEAP_NO_ALLOCATOR(null) {
+            @Override
+            ByteBuffer newBuffer(int size) {
+                return ByteBuffer.allocate(size);
+            }
+        },
+        DIRECT(null) {
             @Override
             ByteBuffer newBuffer(int size) {
                 return ByteBuffer.allocateDirect(size);
@@ -63,47 +71,54 @@
         };
 
         abstract ByteBuffer newBuffer(int size);
+
+        BufferType(BufferAllocator allocator) {
+            this.allocator = allocator;
+        }
+
+        private final BufferAllocator allocator;
     }
 
     private enum ClientAuth {
         NONE {
             @Override
-            SSLEngine apply(SSLEngine engine) {
+            void apply(SSLEngine engine) {
                 engine.setWantClientAuth(false);
                 engine.setNeedClientAuth(false);
-                return engine;
             }
         },
         OPTIONAL {
             @Override
-            SSLEngine apply(SSLEngine engine) {
+            void apply(SSLEngine engine) {
                 engine.setWantClientAuth(true);
                 engine.setNeedClientAuth(false);
-                return engine;
             }
         },
         REQUIRED {
             @Override
-            SSLEngine apply(SSLEngine engine) {
+            void apply(SSLEngine engine) {
                 engine.setWantClientAuth(false);
                 engine.setNeedClientAuth(true);
-                return engine;
             }
         };
 
-        abstract SSLEngine apply(SSLEngine engine);
+        abstract void apply(SSLEngine engine);
     }
 
     @Parameters(name = "{0}")
     public static Iterable<BufferType> data() {
-        return Arrays.asList(BufferType.HEAP, BufferType.DIRECT);
+        return Arrays.asList(
+                BufferType.HEAP_ALLOCATOR, BufferType.HEAP_NO_ALLOCATOR, BufferType.DIRECT);
     }
 
-    @Parameter
-    public BufferType bufferType;
+    @Parameter public BufferType bufferType;
 
     private SSLEngine clientEngine;
     private SSLEngine serverEngine;
+    private ByteBuffer clientApplicationBuffer;
+    private ByteBuffer clientPacketBuffer;
+    private ByteBuffer serverApplicationBuffer;
+    private ByteBuffer serverPacketBuffer;
 
     @Test
     public void mutualAuthWithSameCertsShouldSucceed() throws Exception {
@@ -117,7 +132,8 @@
 
     @Test(expected = SSLHandshakeException.class)
     public void mutualAuthWithUntrustedServerShouldFail() throws Exception {
-        doMutualAuthHandshake(TestKeyStore.getClientCA2(), TestKeyStore.getServer(), ClientAuth.NONE);
+        doMutualAuthHandshake(
+                TestKeyStore.getClientCA2(), TestKeyStore.getServer(), ClientAuth.NONE);
     }
 
     @Test(expected = SSLHandshakeException.class)
@@ -127,28 +143,32 @@
 
     @Test
     public void optionalClientAuthShouldSucceed() throws Exception {
-        doMutualAuthHandshake(TestKeyStore.getClient(), TestKeyStore.getServer(), ClientAuth.OPTIONAL);
+        doMutualAuthHandshake(
+                TestKeyStore.getClient(), TestKeyStore.getServer(), ClientAuth.OPTIONAL);
     }
 
     @Test(expected = SSLHandshakeException.class)
     public void optionalClientAuthShouldFail() throws Exception {
-        doMutualAuthHandshake(TestKeyStore.getClient(), TestKeyStore.getClient(), ClientAuth.OPTIONAL);
+        doMutualAuthHandshake(
+                TestKeyStore.getClient(), TestKeyStore.getClient(), ClientAuth.OPTIONAL);
     }
 
     @Test
     public void requiredClientAuthShouldSucceed() throws Exception {
-        doMutualAuthHandshake(TestKeyStore.getServer(), TestKeyStore.getServer(), ClientAuth.REQUIRED);
+        doMutualAuthHandshake(
+                TestKeyStore.getServer(), TestKeyStore.getServer(), ClientAuth.REQUIRED);
     }
 
     @Test(expected = SSLHandshakeException.class)
     public void requiredClientAuthShouldFail() throws Exception {
-        doMutualAuthHandshake(TestKeyStore.getClient(), TestKeyStore.getClient(), ClientAuth.REQUIRED);
+        doMutualAuthHandshake(
+                TestKeyStore.getClient(), TestKeyStore.getClient(), ClientAuth.REQUIRED);
     }
 
     @Test
     public void exchangeMessages() throws Exception {
         setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
-        TestUtils.doEngineHandshake(clientEngine, serverEngine);
+        doHandshake();
 
         ByteBuffer clientCleartextBuffer = bufferType.newBuffer(MESSAGE_SIZE);
         clientCleartextBuffer.put(newTextMessage(MESSAGE_SIZE));
@@ -171,12 +191,10 @@
         byte[] expectedMessage = toArray(clientCleartextBuffer);
 
         // Unwrap the all of the encrypted messages.
-        ByteBuffer[] cleartextBuffers = new ByteBuffer[numMessages];
         for (int i = 0; i < numMessages; ++i) {
             ByteBuffer out = bufferType.newBuffer(2 * MESSAGE_SIZE);
-            cleartextBuffers[i] = out;
-            SSLEngineResult unwrapResult = Conscrypt.Engines.unwrap(serverEngine, encryptedBuffers,
-                    new ByteBuffer[] {out});
+            SSLEngineResult unwrapResult = Conscrypt.Engines.unwrap(
+                    serverEngine, encryptedBuffers, new ByteBuffer[] {out});
             assertEquals(SSLEngineResult.Status.OK, unwrapResult.getStatus());
             assertEquals(MESSAGE_SIZE, unwrapResult.bytesProduced());
 
@@ -189,7 +207,7 @@
     @Test
     public void exchangeLargeMessage() throws Exception {
         setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
-        TestUtils.doEngineHandshake(clientEngine, serverEngine);
+        doHandshake();
 
         // Create the input message.
         final int largeMessageSize = 16413;
@@ -200,8 +218,9 @@
 
         // Encrypt the input message.
         List<ByteBuffer> encryptedBufferList = new ArrayList<ByteBuffer>();
-        while(inputBuffer.hasRemaining()) {
-            ByteBuffer encryptedBuffer = bufferType.newBuffer(clientEngine.getSession().getPacketBufferSize());
+        while (inputBuffer.hasRemaining()) {
+            ByteBuffer encryptedBuffer =
+                    bufferType.newBuffer(clientEngine.getSession().getPacketBufferSize());
             SSLEngineResult wrapResult = clientEngine.wrap(inputBuffer, encryptedBuffer);
             assertEquals(SSLEngineResult.Status.OK, wrapResult.getStatus());
             encryptedBuffer.flip();
@@ -210,7 +229,8 @@
 
         // Unwrap the all of the encrypted messages.
         ByteArrayOutputStream cleartextStream = new ByteArrayOutputStream();
-        ByteBuffer[] encryptedBuffers = encryptedBufferList.toArray(new ByteBuffer[encryptedBufferList.size()]);
+        ByteBuffer[] encryptedBuffers =
+                encryptedBufferList.toArray(new ByteBuffer[encryptedBufferList.size()]);
         int decryptedBufferSize = 8192;
         final ByteBuffer decryptedBuffer = bufferType.newBuffer(decryptedBufferSize);
         for (ByteBuffer encryptedBuffer : encryptedBuffers) {
@@ -220,8 +240,8 @@
                     decryptedBuffer.clear();
                 }
                 int prevPos = decryptedBuffer.position();
-                SSLEngineResult unwrapResult = Conscrypt.Engines.unwrap(serverEngine,
-                        encryptedBuffers, new ByteBuffer[]{decryptedBuffer});
+                SSLEngineResult unwrapResult = Conscrypt.Engines.unwrap(
+                        serverEngine, encryptedBuffers, new ByteBuffer[] {decryptedBuffer});
                 status = unwrapResult.getStatus();
                 int newPos = decryptedBuffer.position();
                 int bytesProduced = unwrapResult.bytesProduced();
@@ -249,20 +269,37 @@
         assertArrayEquals(message, actualMessage);
     }
 
-    private void doMutualAuthHandshake(TestKeyStore clientKs, TestKeyStore serverKs, ClientAuth clientAuth) throws Exception {
+    private void doMutualAuthHandshake(
+            TestKeyStore clientKs, TestKeyStore serverKs, ClientAuth clientAuth) throws Exception {
         setupEngines(clientKs, serverKs);
         clientAuth.apply(serverEngine);
-        TestUtils.doEngineHandshake(clientEngine, serverEngine);
+        doHandshake();
         assertEquals(HandshakeStatus.NOT_HANDSHAKING, clientEngine.getHandshakeStatus());
         assertEquals(HandshakeStatus.NOT_HANDSHAKING, serverEngine.getHandshakeStatus());
     }
 
-    private void setupEngines(TestKeyStore clientKeyStore, TestKeyStore serverKeyStore) throws SSLException {
+    private void doHandshake() throws SSLException {
+        TestUtils.doEngineHandshake(clientEngine, serverEngine, clientApplicationBuffer,
+                clientPacketBuffer, serverApplicationBuffer, serverPacketBuffer);
+    }
+
+    private void setupEngines(TestKeyStore clientKeyStore, TestKeyStore serverKeyStore)
+            throws SSLException {
         SSLContext clientContext = initSslContext(newContext(), clientKeyStore);
         SSLContext serverContext = initSslContext(newContext(), serverKeyStore);
 
-        clientEngine = initEngine(clientContext.createSSLEngine(), CIPHER, true);
-        serverEngine = initEngine(serverContext.createSSLEngine(), CIPHER, false);
+        clientEngine = initEngine(clientContext.createSSLEngine(), TEST_CIPHER, true);
+        serverEngine = initEngine(serverContext.createSSLEngine(), TEST_CIPHER, false);
+        setBufferAllocator(clientEngine, bufferType.allocator);
+        setBufferAllocator(serverEngine, bufferType.allocator);
+
+        // Create the application and packet buffers for both endpoints.
+        clientApplicationBuffer =
+            bufferType.newBuffer(clientEngine.getSession().getApplicationBufferSize());
+        serverApplicationBuffer =
+            bufferType.newBuffer(serverEngine.getSession().getApplicationBufferSize());
+        clientPacketBuffer = bufferType.newBuffer(clientEngine.getSession().getPacketBufferSize());
+        serverPacketBuffer = bufferType.newBuffer(serverEngine.getSession().getPacketBufferSize());
     }
 
     private static byte[] toArray(ByteBuffer buffer) {
diff --git a/openjdk/src/test/java/org/conscrypt/OpenSSLSocketImplTest.java b/openjdk/src/test/java/org/conscrypt/ConscryptSocketTest.java
similarity index 89%
rename from openjdk/src/test/java/org/conscrypt/OpenSSLSocketImplTest.java
rename to openjdk/src/test/java/org/conscrypt/ConscryptSocketTest.java
index 8c59e8f..bb0b3ce 100644
--- a/openjdk/src/test/java/org/conscrypt/OpenSSLSocketImplTest.java
+++ b/openjdk/src/test/java/org/conscrypt/ConscryptSocketTest.java
@@ -58,7 +58,7 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class OpenSSLSocketImplTest {
+public class ConscryptSocketTest {
     private static final long TIMEOUT_SECONDS = 5;
     private static final char[] EMPTY_PASSWORD = new char[0];
 
@@ -77,7 +77,7 @@
             @Override
             void assertSocketType(Socket socket) {
                 assertTrue("Unexpected socket type: " + socket.getClass().getName(),
-                        socket instanceof OpenSSLEngineSocketImpl);
+                        socket instanceof ConscryptEngineSocket);
             }
         };
 
@@ -87,23 +87,23 @@
             this.useEngineSocket = useEngineSocket;
         }
 
-        OpenSSLSocketImpl createClientSocket(OpenSSLContextImpl context, ServerSocket listener)
-                throws IOException {
+        AbstractConscryptSocket createClientSocket(
+                OpenSSLContextImpl context, ServerSocket listener) throws IOException {
             SSLSocketFactory factory = context.engineGetSocketFactory();
             Conscrypt.SocketFactories.setUseEngineSocket(factory, useEngineSocket);
-            OpenSSLSocketImpl socket = (OpenSSLSocketImpl) factory.createSocket(
+            AbstractConscryptSocket socket = (AbstractConscryptSocket) factory.createSocket(
                     listener.getInetAddress(), listener.getLocalPort());
             assertSocketType(socket);
             socket.setUseClientMode(true);
             return socket;
         }
 
-        OpenSSLSocketImpl createServerSocket(OpenSSLContextImpl context, ServerSocket listener)
-                throws IOException {
+        AbstractConscryptSocket createServerSocket(
+                OpenSSLContextImpl context, ServerSocket listener) throws IOException {
             SSLSocketFactory factory = context.engineGetSocketFactory();
             Conscrypt.SocketFactories.setUseEngineSocket(factory, useEngineSocket);
-            OpenSSLSocketImpl socket = (OpenSSLSocketImpl) factory.createSocket(listener.accept(),
-                    null, -1, // hostname, port
+            AbstractConscryptSocket socket = (AbstractConscryptSocket) factory.createSocket(
+                    listener.accept(), null, -1, // hostname, port
                     true); // autoclose
             assertSocketType(socket);
             socket.setUseClientMode(false);
@@ -152,7 +152,7 @@
         KeyManager[] keyManagers;
         TrustManager[] trustManagers;
 
-        abstract OpenSSLSocketImpl createSocket(ServerSocket listener) throws IOException;
+        abstract AbstractConscryptSocket createSocket(ServerSocket listener) throws IOException;
 
         OpenSSLContextImpl createContext() throws IOException {
             OpenSSLContextImpl context = OpenSSLContextImpl.getPreferred();
@@ -192,8 +192,9 @@
         }
 
         @Override
-        OpenSSLSocketImpl createSocket(ServerSocket listener) throws IOException {
-            OpenSSLSocketImpl socket = socketType.createClientSocket(createContext(), listener);
+        AbstractConscryptSocket createSocket(ServerSocket listener) throws IOException {
+            AbstractConscryptSocket socket =
+                    socketType.createClientSocket(createContext(), listener);
             socket.setHostname(hostname);
             return socket;
         }
@@ -217,7 +218,7 @@
         }
 
         @Override
-        OpenSSLSocketImpl createSocket(ServerSocket listener) throws IOException {
+        AbstractConscryptSocket createSocket(ServerSocket listener) throws IOException {
             return socketType.createServerSocket(createContext(), listener);
         }
     }
@@ -226,8 +227,8 @@
         ServerHooks serverHooks;
         ClientHooks clientHooks;
 
-        OpenSSLSocketImpl client;
-        OpenSSLSocketImpl server;
+        AbstractConscryptSocket client;
+        AbstractConscryptSocket server;
 
         Exception clientException;
         Exception serverException;
@@ -274,8 +275,8 @@
 
         void doHandshake() throws Exception {
             ServerSocket listener = newServerSocket();
-            Future<OpenSSLSocketImpl> clientFuture = handshake(listener, clientHooks);
-            Future<OpenSSLSocketImpl> serverFuture = handshake(listener, serverHooks);
+            Future<AbstractConscryptSocket> clientFuture = handshake(listener, clientHooks);
+            Future<AbstractConscryptSocket> serverFuture = handshake(listener, serverHooks);
 
             try {
                 client = getOrThrowCause(clientFuture, TIMEOUT_SECONDS, TimeUnit.SECONDS);
@@ -289,9 +290,9 @@
             }
         }
 
-        Future<OpenSSLSocketImpl> handshake(final ServerSocket listener, final Hooks hooks) {
+        Future<AbstractConscryptSocket> handshake(final ServerSocket listener, final Hooks hooks) {
             return executor.submit(() -> {
-                OpenSSLSocketImpl socket = hooks.createSocket(listener);
+                AbstractConscryptSocket socket = hooks.createSocket(listener);
                 socket.addHandshakeCompletedListener(hooks);
 
                 socket.startHandshake();
@@ -390,8 +391,8 @@
 
         connection.clientHooks = new ClientHooks() {
             @Override
-            public OpenSSLSocketImpl createSocket(ServerSocket listener) throws IOException {
-                OpenSSLSocketImpl socket = super.createSocket(listener);
+            public AbstractConscryptSocket createSocket(ServerSocket listener) throws IOException {
+                AbstractConscryptSocket socket = super.createSocket(listener);
                 socket.setEnabledProtocols(new String[] {"SSLv3"});
                 assertEquals(
                         "SSLv3 should be filtered out", 0, socket.getEnabledProtocols().length);
diff --git a/openjdk/src/test/java/org/conscrypt/MockSessionBuilder.java b/openjdk/src/test/java/org/conscrypt/MockSessionBuilder.java
new file mode 100644
index 0000000..5fb4caa
--- /dev/null
+++ b/openjdk/src/test/java/org/conscrypt/MockSessionBuilder.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.conscrypt;
+
+import static org.conscrypt.TestUtils.UTF_8;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Utility class for constructing mock sessions.
+ */
+final class MockSessionBuilder {
+    static final String DEFAULT_CIPHER_SUITE = "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256";
+    static final String DEFAULT_PROTOCOL = TestUtils.PROTOCOL_TLS_V1_2;
+    static final int DEFAULT_PORT = 443;
+
+    private byte[] id;
+    private boolean valid = true;
+    private String host;
+    private int port = DEFAULT_PORT;
+    private String cipherSuite = DEFAULT_CIPHER_SUITE;
+    private String protocol = DEFAULT_PROTOCOL;
+    private byte[] encodedBytes = EmptyArray.BYTE;
+
+    MockSessionBuilder id(byte[] id) {
+        this.id = id;
+        return this;
+    }
+
+    MockSessionBuilder protocol(String protocol) {
+        this.protocol = protocol;
+        return this;
+    }
+
+    MockSessionBuilder host(String host) {
+        this.host = host;
+        return this;
+    }
+
+    MockSessionBuilder port(int port) {
+        this.port = port;
+        return this;
+    }
+
+    MockSessionBuilder valid(boolean valid) {
+        this.valid = valid;
+        return this;
+    }
+
+    MockSessionBuilder cipherSuite(String cipherSuite) {
+        this.cipherSuite = cipherSuite;
+        return this;
+    }
+
+    MockSessionBuilder encodedBytes(byte[] encodedBytes) {
+        this.encodedBytes = encodedBytes;
+        return this;
+    }
+
+    SslSessionWrapper build() {
+        SslSessionWrapper session = mock(SslSessionWrapper.class);
+        byte[] id = this.id == null ? host.getBytes(UTF_8) : this.id;
+        when(session.getId()).thenReturn(id);
+        when(session.isValid()).thenReturn(valid);
+        when(session.getProtocol()).thenReturn(protocol);
+        when(session.getPeerHost()).thenReturn(host);
+        when(session.getPeerPort()).thenReturn(port);
+        when(session.getCipherSuite()).thenReturn(cipherSuite);
+        when(session.toBytes()).thenReturn(encodedBytes);
+        return session;
+    }
+}
diff --git a/openjdk/src/test/java/org/conscrypt/NativeCryptoTest.java b/openjdk/src/test/java/org/conscrypt/NativeCryptoTest.java
index e684e1a..8ec1029 100644
--- a/openjdk/src/test/java/org/conscrypt/NativeCryptoTest.java
+++ b/openjdk/src/test/java/org/conscrypt/NativeCryptoTest.java
@@ -35,6 +35,7 @@
 import java.io.UnsupportedEncodingException;
 import java.lang.reflect.Method;
 import java.math.BigInteger;
+import java.net.InetAddress;
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.net.SocketException;
@@ -53,6 +54,7 @@
 import java.security.spec.ECPrivateKeySpec;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.concurrent.Callable;
@@ -72,7 +74,6 @@
 import org.conscrypt.OpenSSLX509CertificateFactory.ParsingException;
 import org.junit.After;
 import org.junit.BeforeClass;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -251,6 +252,14 @@
         NativeCrypto.EVP_PKEY_cmp(null, null);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void EVP_PKEY_cmp_withNullShouldThrow() throws Exception {
+        RSAPrivateCrtKey privKey1 = generateRsaKey();
+        NativeRef.EVP_PKEY pkey1 = getRsaPkey(privKey1);
+        assertNotSame(NULL, pkey1);
+        NativeCrypto.EVP_PKEY_cmp(pkey1, null);
+    }
+
     @Test
     public void test_EVP_PKEY_cmp() throws Exception {
         RSAPrivateCrtKey privKey1 = generateRsaKey();
@@ -264,12 +273,6 @@
         NativeRef.EVP_PKEY pkey2 = getRsaPkey(generateRsaKey());
         assertNotSame(NULL, pkey2);
 
-        try {
-            NativeCrypto.EVP_PKEY_cmp(pkey1, null);
-            fail("Should throw NullPointerException when arguments are NULL");
-        } catch (NullPointerException expected) {
-        }
-
         assertEquals("Same keys should be the equal", 1, NativeCrypto.EVP_PKEY_cmp(pkey1, pkey1));
 
         assertEquals(
@@ -305,7 +308,7 @@
     }
 
     @Test(expected = NullPointerException.class)
-    public void SSL_CTX_set_session_id_context_NullSessionIdArgument() throws Exception {
+    public void SSL_CTX_set_session_id_context_withNullShouldThrow() throws Exception {
         long c = NativeCrypto.SSL_CTX_new();
         try {
             NativeCrypto.SSL_CTX_set_session_id_context(c, null);
@@ -314,6 +317,16 @@
         }
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void test_SSL_CTX_set_session_id_context_withInvalidIdShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        try {
+            NativeCrypto.SSL_CTX_set_session_id_context(c, new byte[33]);
+        } finally {
+            NativeCrypto.SSL_CTX_free(c);
+        }
+    }
+
     @Test
     public void test_SSL_CTX_set_session_id_context() throws Exception {
         byte[] empty = new byte[0];
@@ -322,12 +335,6 @@
         try {
             NativeCrypto.SSL_CTX_set_session_id_context(c, empty);
             NativeCrypto.SSL_CTX_set_session_id_context(c, new byte[32]);
-            try {
-                NativeCrypto.SSL_CTX_set_session_id_context(c, new byte[33]);
-                fail("Expected IllegalArgumentException");
-            } catch (IllegalArgumentException expected) {
-                // Expected.
-            }
         } finally {
             NativeCrypto.SSL_CTX_free(c);
         }
@@ -357,42 +364,55 @@
         NativeCrypto.SSL_use_certificate(NULL, null);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_use_certificate_withNullShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        try {
+            NativeCrypto.SSL_use_certificate(s, null);
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
+        }
+    }
+
     @Test
     public void test_SSL_use_certificate() throws Exception {
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
 
-        try {
-            NativeCrypto.SSL_use_certificate(s, null);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
         NativeCrypto.SSL_use_certificate(s, getServerCertificates());
 
         NativeCrypto.SSL_free(s);
         NativeCrypto.SSL_CTX_free(c);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_set1_tls_channel_id_withNullChannelShouldThrow() throws Exception {
+        NativeCrypto.SSL_set1_tls_channel_id(NULL, null);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void SSL_set1_tls_channel_id_withNullKeyShouldThrow() throws Exception {
+        initChannelIdKey();
+
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        try {
+            NativeCrypto.SSL_set1_tls_channel_id(s, null);
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
+        }
+    }
+
     @Test
     public void test_SSL_use_PrivateKey_for_tls_channel_id() throws Exception {
         initChannelIdKey();
 
-        try {
-            NativeCrypto.SSL_set1_tls_channel_id(NULL, null);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
 
-        try {
-            NativeCrypto.SSL_set1_tls_channel_id(s, null);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
         // Use the key natively. This works because the initChannelIdKey method ensures that the
         // key is backed by OpenSSL.
         NativeCrypto.SSL_set1_tls_channel_id(s, CHANNEL_ID_PRIVATE_KEY.getNativeRef());
@@ -401,21 +421,21 @@
         NativeCrypto.SSL_CTX_free(c);
     }
 
-    @Test
-    public void test_SSL_use_PrivateKey() throws Exception {
-        try {
-            NativeCrypto.SSL_use_PrivateKey(NULL, null);
-            fail();
-        } catch (NullPointerException expected) {
-        }
+    @Test(expected = NullPointerException.class)
+    public void SSL_use_PrivateKey_withNullSslShouldThrow() throws Exception {
+        NativeCrypto.SSL_use_PrivateKey(NULL, null);
+    }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_use_PrivateKeyWithNullKeyShouldThrow() throws Exception {
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
 
         try {
             NativeCrypto.SSL_use_PrivateKey(s, null);
-            fail();
-        } catch (NullPointerException expected) {
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
         }
 
         NativeCrypto.SSL_use_PrivateKey(s, getServerPrivateKey().getNativeRef());
@@ -424,62 +444,74 @@
         NativeCrypto.SSL_CTX_free(c);
     }
 
+    @Test
+    public void test_SSL_use_PrivateKey() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+
+        NativeCrypto.SSL_use_PrivateKey(s, getServerPrivateKey().getNativeRef());
+
+        NativeCrypto.SSL_free(s);
+        NativeCrypto.SSL_CTX_free(c);
+    }
+
     @Test(expected = NullPointerException.class)
-    public void SSL_check_private_key_NullArgument() throws Exception {
+    public void SSL_check_private_key_withNullShouldThrow() throws Exception {
         NativeCrypto.SSL_check_private_key(NULL);
     }
 
-    @Test
-    public void test_SSL_check_private_key_no_key_no_cert() throws Exception {
+    @Test(expected = SSLException.class)
+    public void SSL_check_private_key_withNoKeyOrCertShouldThrow() throws Exception {
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
 
         // neither private or certificate set
         try {
             NativeCrypto.SSL_check_private_key(s);
-            fail();
-        } catch (SSLException expected) {
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
         }
-
-        NativeCrypto.SSL_free(s);
-        NativeCrypto.SSL_CTX_free(c);
     }
 
-    @Test
-    public void test_SSL_check_private_key_cert_then_key() throws Exception {
+    @Test(expected = SSLException.class)
+    public void SSL_check_private_key_withNoKeyShouldThrow() throws Exception {
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
 
-        // first certificate, then private
+        // Certificate but no private key
         NativeCrypto.SSL_use_certificate(s, getServerCertificates());
 
         try {
             NativeCrypto.SSL_check_private_key(s);
-            fail();
-        } catch (SSLException expected) {
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
         }
-
-        NativeCrypto.SSL_use_PrivateKey(s, getServerPrivateKey().getNativeRef());
-        NativeCrypto.SSL_check_private_key(s);
-
-        NativeCrypto.SSL_free(s);
-        NativeCrypto.SSL_CTX_free(c);
     }
-    @Test
-    public void test_SSL_check_private_key_key_then_cert() throws Exception {
+
+    @Test(expected = SSLException.class)
+    public void test_SSL_check_private_NoCertificateShouldThrow() throws Exception {
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
 
         // first private, then certificate
         NativeCrypto.SSL_use_PrivateKey(s, getServerPrivateKey().getNativeRef());
-
         try {
             NativeCrypto.SSL_check_private_key(s);
-            fail();
-        } catch (SSLException expected) {
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
         }
+    }
+
+    @Test
+    public void test_SSL_check_private_key_certThenKey() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
 
         NativeCrypto.SSL_use_certificate(s, getServerCertificates());
+        NativeCrypto.SSL_use_PrivateKey(s, getServerPrivateKey().getNativeRef());
         NativeCrypto.SSL_check_private_key(s);
 
         NativeCrypto.SSL_free(s);
@@ -487,13 +519,26 @@
     }
 
     @Test
-    public void test_SSL_get_mode() throws Exception {
-        try {
-            NativeCrypto.SSL_get_mode(NULL);
-            fail();
-        } catch (NullPointerException expected) {
-        }
+    public void test_SSL_check_private_key_keyThenCert() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
 
+        // first private, then certificate
+        NativeCrypto.SSL_use_PrivateKey(s, getServerPrivateKey().getNativeRef());
+        NativeCrypto.SSL_use_certificate(s, getServerCertificates());
+        NativeCrypto.SSL_check_private_key(s);
+
+        NativeCrypto.SSL_free(s);
+        NativeCrypto.SSL_CTX_free(c);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void SSL_get_mode_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_get_mode(NULL);
+    }
+
+    @Test
+    public void test_SSL_get_mode() throws Exception {
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
         assertTrue(NativeCrypto.SSL_get_mode(s) != 0);
@@ -501,14 +546,13 @@
         NativeCrypto.SSL_CTX_free(c);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_set_mode_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_set_mode(NULL, 0);
+    }
+
     @Test
     public void test_SSL_set_mode_and_clear_mode() throws Exception {
-        try {
-            NativeCrypto.SSL_set_mode(NULL, 0);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
         // check SSL_MODE_ENABLE_FALSE_START on by default for BoringSSL
@@ -528,14 +572,13 @@
         NativeCrypto.SSL_CTX_free(c);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_get_options_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_get_options(NULL);
+    }
+
     @Test
     public void test_SSL_get_options() throws Exception {
-        try {
-            NativeCrypto.SSL_get_options(NULL);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
         assertTrue(NativeCrypto.SSL_get_options(s) != 0);
@@ -543,14 +586,13 @@
         NativeCrypto.SSL_CTX_free(c);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_set_options_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_set_options(NULL, 0);
+    }
+
     @Test
     public void test_SSL_set_options() throws Exception {
-        try {
-            NativeCrypto.SSL_set_options(NULL, 0);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
         assertTrue((NativeCrypto.SSL_get_options(s) & NativeConstants.SSL_OP_NO_SSLv3) == 0);
@@ -560,14 +602,13 @@
         NativeCrypto.SSL_CTX_free(c);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_clear_options_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_clear_options(NULL, 0);
+    }
+
     @Test
     public void test_SSL_clear_options() throws Exception {
-        try {
-            NativeCrypto.SSL_clear_options(NULL, 0);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
         assertTrue((NativeCrypto.SSL_get_options(s) & NativeConstants.SSL_OP_NO_SSLv3) == 0);
@@ -579,32 +620,52 @@
         NativeCrypto.SSL_CTX_free(c);
     }
 
-    @Test
-    public void test_SSL_set_cipher_lists() throws Exception {
-        try {
-            NativeCrypto.SSL_set_cipher_lists(NULL, null);
-            fail("Exception not thrown for null ssl and null list");
-        } catch (NullPointerException expected) {
-        }
+    @Test(expected = NullPointerException.class)
+    public void SSL_set_cipher_lists_withNullSslShouldThrow() throws Exception {
+        NativeCrypto.SSL_set_cipher_lists(NULL, null);
+    }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_set_cipher_lists_withNullCiphersShouldThrow() throws Exception {
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
-
         try {
             NativeCrypto.SSL_set_cipher_lists(s, null);
-            fail("Exception not thrown for null list");
-        } catch (NullPointerException expected) {
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
         }
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void test_SSL_set_cipher_lists_withNullCipherShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        try {
+            NativeCrypto.SSL_set_cipher_lists(s, new String[] {null});
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
+        }
+    }
+
+    @Test
+    public void SSL_set_cipher_lists_withEmptyCiphersShouldSucceed() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
 
         // Explicitly checking that the empty list is allowed.
         // b/21816861
         NativeCrypto.SSL_set_cipher_lists(s, new String[] {});
 
-        try {
-            NativeCrypto.SSL_set_cipher_lists(s, new String[] {null});
-            fail("Exception not thrown for list with null element");
-        } catch (NullPointerException expected) {
-        }
+        NativeCrypto.SSL_free(s);
+        NativeCrypto.SSL_CTX_free(c);
+    }
+
+    @Test
+    public void SSL_set_cipher_lists_withIllegalCipherShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
 
         // see OpenSSL ciphers man page
         String[] illegals = new String[] {// empty
@@ -619,25 +680,33 @@
                 NativeCrypto.SSL_set_cipher_lists(s, new String[] {illegal});
                 fail("Exception now thrown for illegal cipher: " + illegal);
             } catch (IllegalArgumentException expected) {
+                // Expected.
             }
         }
 
-        List<String> ciphers =
-                new ArrayList<String>(NativeCrypto.OPENSSL_TO_STANDARD_CIPHER_SUITES.keySet());
-        NativeCrypto.SSL_set_cipher_lists(s, ciphers.toArray(new String[ciphers.size()]));
-
         NativeCrypto.SSL_free(s);
         NativeCrypto.SSL_CTX_free(c);
     }
 
     @Test
-    public void test_SSL_set_verify() throws Exception {
-        try {
-            NativeCrypto.SSL_set_verify(NULL, 0);
-            fail();
-        } catch (NullPointerException expected) {
-        }
+    public void SSL_set_cipher_lists_withValidCiphersShouldSucceed() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
 
+        List<String> ciphers = new ArrayList<>(NativeCrypto.SUPPORTED_CIPHER_SUITES_SET);
+        NativeCrypto.SSL_set_cipher_lists(s, ciphers.toArray(new String[ciphers.size()]));
+
+        NativeCrypto.SSL_free(s);
+        NativeCrypto.SSL_CTX_free(c);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void SSL_set_verify_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_set_verify(NULL, 0);
+    }
+
+    @Test
+    public void test_SSL_set_verify() throws Exception {
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
         NativeCrypto.SSL_set_verify(s, NativeCrypto.SSL_VERIFY_NONE);
@@ -652,18 +721,19 @@
     private static final boolean DEBUG = false;
 
     public static class Hooks {
-        protected String negotiatedCipherSuite;
+        String negotiatedCipherSuite;
         private OpenSSLKey channelIdPrivateKey;
-        protected boolean pskEnabled;
-        protected byte[] pskKey;
-        protected List<String> enabledCipherSuites;
+         boolean pskEnabled;
+         byte[] pskKey;
+         List<String> enabledCipherSuites;
 
         /**
-         * @throws SSLException
+         * @throws SSLException if an error occurs creating the context.
          */
         public long getContext() throws SSLException {
             return NativeCrypto.SSL_CTX_new();
         }
+
         public long beforeHandshake(long context) throws SSLException {
             long s = NativeCrypto.SSL_new(context);
             // Limit cipher suites to a known set so authMethod is known.
@@ -678,7 +748,7 @@
             } else {
                 cipherSuites.addAll(enabledCipherSuites);
             }
-            NativeCrypto.SSL_set_cipher_lists(
+            NativeCrypto.setEnabledCipherSuites(
                     s, cipherSuites.toArray(new String[cipherSuites.size()]));
 
             if (channelIdPrivateKey != null) {
@@ -699,6 +769,7 @@
                 try {
                     NativeCrypto.SSL_shutdown(ssl, fd, callback);
                 } catch (IOException e) {
+                    // Expected.
                 }
                 NativeCrypto.SSL_free(ssl);
             }
@@ -711,20 +782,20 @@
         }
     }
 
-    public static class TestSSLHandshakeCallbacks implements SSLHandshakeCallbacks {
+    static class TestSSLHandshakeCallbacks implements SSLHandshakeCallbacks {
         private final Socket socket;
         private final long sslNativePointer;
         private final Hooks hooks;
 
-        public TestSSLHandshakeCallbacks(Socket socket, long sslNativePointer, Hooks hooks) {
+        TestSSLHandshakeCallbacks(Socket socket, long sslNativePointer, Hooks hooks) {
             this.socket = socket;
             this.sslNativePointer = sslNativePointer;
             this.hooks = hooks;
         }
 
-        public long[] certificateChainRefs;
-        public String authMethod;
-        public boolean verifyCertificateChainCalled;
+        private long[] certificateChainRefs;
+        private String authMethod;
+        private boolean verifyCertificateChainCalled;
 
         @Override
         public void verifyCertificateChain(long[] certChainRefs, String authMethod)
@@ -740,9 +811,9 @@
             this.verifyCertificateChainCalled = true;
         }
 
-        public byte[] keyTypes;
-        public byte[][] asn1DerEncodedX500Principals;
-        public boolean clientCertificateRequestedCalled;
+        private byte[] keyTypes;
+        private byte[][] asn1DerEncodedX500Principals;
+        private boolean clientCertificateRequestedCalled;
 
         @Override
         public void clientCertificateRequested(
@@ -762,7 +833,7 @@
             }
         }
 
-        public boolean handshakeCompletedCalled;
+        private boolean handshakeCompletedCalled;
 
         @Override
         public void onSSLStateChange(int type, int val) {
@@ -773,7 +844,7 @@
             this.handshakeCompletedCalled = true;
         }
 
-        public Socket getSocket() {
+        Socket getSocket() {
             return socket;
         }
 
@@ -809,6 +880,7 @@
         private byte[] serverPSKKeyRequestedResultKey;
         private String serverPSKKeyRequestedIdentityHint;
         private String serverPSKKeyRequestedIdentity;
+
         @Override
         public int serverPSKKeyRequested(String identityHint, String identity, byte[] key) {
             if (DEBUG) {
@@ -826,10 +898,35 @@
             }
             return serverPSKKeyRequestedResult;
         }
+
+        private boolean onNewSessionEstablishedInvoked;
+        private boolean onNewSessionEstablishedSaveSession;
+        private long onNewSessionEstablishedSessionNativePointer;
+
+        @Override
+        public void onNewSessionEstablished(long sslSessionNativePtr) {
+            if (DEBUG) {
+                System.out.println("ssl=0x" + Long.toString(sslNativePointer, 16)
+                        + " onNewSessionCreated"
+                        + " ssl=0x" + Long.toString(sslSessionNativePtr, 16));
+            }
+            onNewSessionEstablishedInvoked = true;
+
+            if (onNewSessionEstablishedSaveSession) {
+                NativeCrypto.SSL_SESSION_up_ref(sslSessionNativePtr);
+                onNewSessionEstablishedSessionNativePointer = sslSessionNativePtr;
+            }
+        }
+
+        @Override
+        public long serverSessionRequested(byte[] id) {
+            // TODO(nathanmittler): Implement server-side caching for TLS < 1.3
+            return 0;
+        }
     }
 
-    public static class ClientHooks extends Hooks {
-        protected String pskIdentity;
+    static class ClientHooks extends Hooks {
+        private String pskIdentity;
 
         @Override
         public void configureCallbacks(TestSSLHandshakeCallbacks callbacks) {
@@ -860,20 +957,20 @@
         }
     }
 
-    public static class ServerHooks extends Hooks {
+    static class ServerHooks extends Hooks {
         private final OpenSSLKey privateKey;
         private final long[] certificates;
         private boolean channelIdEnabled;
         private byte[] channelIdAfterHandshake;
         private Throwable channelIdAfterHandshakeException;
 
-        protected String pskIdentityHint;
+        private String pskIdentityHint;
 
         public ServerHooks() {
             this(null, null);
         }
 
-        public ServerHooks(OpenSSLKey privateKey, long[] certificates) {
+        ServerHooks(OpenSSLKey privateKey, long[] certificates) {
             this.privateKey = privateKey;
             this.certificates = certificates;
         }
@@ -934,45 +1031,53 @@
                 executor.submit(new Callable<TestSSLHandshakeCallbacks>() {
                     @Override
                     public TestSSLHandshakeCallbacks call() throws Exception {
-                        @SuppressWarnings("resource")
-                        // Socket needs to remain open after the handshake
-                        Socket socket = (client ? new Socket(listener.getInetAddress(),
-                                                          listener.getLocalPort())
-                                                : listener.accept());
-                        if (timeout == -1) {
-                            return new TestSSLHandshakeCallbacks(socket, 0, null);
-                        }
-                        FileDescriptor fd =
-                                (FileDescriptor) m_Platform_getFileDescriptor.invoke(null, socket);
-                        long c = hooks.getContext();
-                        long s = hooks.beforeHandshake(c);
-                        TestSSLHandshakeCallbacks callback =
-                                new TestSSLHandshakeCallbacks(socket, s, hooks);
-                        hooks.configureCallbacks(callback);
-                        if (DEBUG) {
-                            System.out.println("ssl=0x" + Long.toString(s, 16) + " handshake"
-                                    + " context=0x" + Long.toString(c, 16) + " socket=" + socket
-                                    + " fd=" + fd + " timeout=" + timeout + " client=" + client);
-                        }
-                        long session = NULL;
                         try {
-                            if (client) {
-                                NativeCrypto.SSL_set_connect_state(s);
-                            } else {
-                                NativeCrypto.SSL_set_accept_state(s);
+                            @SuppressWarnings("resource")
+                            // Socket needs to remain open after the handshake
+                            Socket socket = (client ? new Socket(listener.getInetAddress(),
+                                    listener.getLocalPort())
+                                    : listener.accept());
+                            if (timeout == -1) {
+                                return new TestSSLHandshakeCallbacks(socket, 0, null);
                             }
-                            NativeCrypto.SSL_configure_alpn(s, client, alpnProtocols);
-                            NativeCrypto.SSL_do_handshake(s, fd, callback, timeout);
-                            session = NativeCrypto.SSL_get1_session(s);
+                            FileDescriptor fd =
+                                    (FileDescriptor) m_Platform_getFileDescriptor
+                                            .invoke(null, socket);
+                            long c = hooks.getContext();
+                            long s = hooks.beforeHandshake(c);
+                            TestSSLHandshakeCallbacks callback =
+                                    new TestSSLHandshakeCallbacks(socket, s, hooks);
+                            hooks.configureCallbacks(callback);
                             if (DEBUG) {
                                 System.out.println("ssl=0x" + Long.toString(s, 16) + " handshake"
-                                        + " session=0x" + Long.toString(session, 16));
+                                        + " context=0x" + Long.toString(c, 16) + " socket=" + socket
+                                        + " fd=" + fd + " timeout=" + timeout + " client="
+                                        + client);
                             }
-                        } finally {
-                            // Ensure afterHandshake is called to free resources
-                            hooks.afterHandshake(session, s, c, socket, fd, callback);
+                            long session = NULL;
+                            try {
+                                if (client) {
+                                    NativeCrypto.SSL_set_connect_state(s);
+                                } else {
+                                    NativeCrypto.SSL_set_accept_state(s);
+                                }
+                                NativeCrypto.SSL_configure_alpn(s, client, alpnProtocols);
+                                NativeCrypto.SSL_do_handshake(s, fd, callback, timeout);
+                                session = NativeCrypto.SSL_get1_session(s);
+                                if (DEBUG) {
+                                    System.out
+                                            .println("ssl=0x" + Long.toString(s, 16) + " handshake"
+                                                    + " session=0x" + Long.toString(session, 16));
+                                }
+                            } finally {
+                                // Ensure afterHandshake is called to free resources
+                                hooks.afterHandshake(session, s, c, socket, fd, callback);
+                            }
+                            return callback;
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                            throw e;
                         }
-                        return callback;
                     }
                 });
         executor.shutdown();
@@ -984,32 +1089,36 @@
         NativeCrypto.SSL_do_handshake(NULL, null, null, 0);
     }
 
-    @Test
-    public void test_SSL_do_handshake_null_args() throws Exception {
+    @Test(expected = NullPointerException.class)
+    public void test_SSL_do_handshake_withNullFdShouldThrow() throws Exception {
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
         NativeCrypto.SSL_set_connect_state(s);
-
         try {
             NativeCrypto.SSL_do_handshake(s, null, null, 0);
-            fail();
-        } catch (NullPointerException expected) {
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
         }
+    }
 
+    @Test(expected = NullPointerException.class)
+    public void test_SSL_do_handshake_withNullShcShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        NativeCrypto.SSL_set_connect_state(s);
         try {
             NativeCrypto.SSL_do_handshake(s, INVALID_FD, null, 0);
-            fail();
-        } catch (NullPointerException expected) {
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
         }
-
-        NativeCrypto.SSL_free(s);
-        NativeCrypto.SSL_CTX_free(c);
     }
 
     @Test
     public void test_SSL_do_handshake_normal() throws Exception {
         // normal client and server case
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         Hooks cHooks = new Hooks();
         Hooks sHooks = new ServerHooks(getServerPrivateKey(), getServerCertificates());
         Future<TestSSLHandshakeCallbacks> client = handshake(listener, 0, true, cHooks, null);
@@ -1026,14 +1135,96 @@
         assertFalse(serverCallback.clientPSKKeyRequestedInvoked);
         assertFalse(clientCallback.serverPSKKeyRequestedInvoked);
         assertFalse(serverCallback.serverPSKKeyRequestedInvoked);
+        assertTrue(clientCallback.onNewSessionEstablishedInvoked);
+        assertTrue(serverCallback.onNewSessionEstablishedInvoked);
         assertTrue(clientCallback.handshakeCompletedCalled);
         assertTrue(serverCallback.handshakeCompletedCalled);
     }
 
     @Test
+    public void test_SSL_do_handshake_reusedSession() throws Exception {
+        // normal client and server case
+        final ServerSocket listener = newServerSocket();
+
+        Future<TestSSLHandshakeCallbacks> client1 =
+                handshake(listener, 0, true, new ClientHooks() {
+                    @Override
+                    public void configureCallbacks(TestSSLHandshakeCallbacks callbacks) {
+                        callbacks.onNewSessionEstablishedSaveSession = true;
+                    }
+                }, null);
+        Future<TestSSLHandshakeCallbacks> server1 = handshake(listener, 0,
+                false, new ServerHooks(getServerPrivateKey(), getServerCertificates()) {
+                    @Override
+                    public void configureCallbacks(TestSSLHandshakeCallbacks callbacks) {
+                        callbacks.onNewSessionEstablishedSaveSession = true;
+                    }
+                }, null);
+        TestSSLHandshakeCallbacks clientCallback1 = client1.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        TestSSLHandshakeCallbacks serverCallback1 = server1.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        assertTrue(clientCallback1.verifyCertificateChainCalled);
+        assertEqualCertificateChains(getServerCertificates(), clientCallback1.certificateChainRefs);
+        assertEquals("ECDHE_RSA", clientCallback1.authMethod);
+        assertFalse(serverCallback1.verifyCertificateChainCalled);
+        assertFalse(clientCallback1.clientCertificateRequestedCalled);
+        assertFalse(serverCallback1.clientCertificateRequestedCalled);
+        assertFalse(clientCallback1.clientPSKKeyRequestedInvoked);
+        assertFalse(serverCallback1.clientPSKKeyRequestedInvoked);
+        assertFalse(clientCallback1.serverPSKKeyRequestedInvoked);
+        assertFalse(serverCallback1.serverPSKKeyRequestedInvoked);
+        assertTrue(clientCallback1.onNewSessionEstablishedInvoked);
+        assertTrue(serverCallback1.onNewSessionEstablishedInvoked);
+        assertTrue(clientCallback1.handshakeCompletedCalled);
+        assertTrue(serverCallback1.handshakeCompletedCalled);
+
+        final long clientSessionContext =
+                clientCallback1.onNewSessionEstablishedSessionNativePointer;
+        final long serverSessionContext =
+                serverCallback1.onNewSessionEstablishedSessionNativePointer;
+
+        Future<TestSSLHandshakeCallbacks> client2 =
+                handshake(listener, 0, true, new ClientHooks() {
+                    @Override
+                    public long beforeHandshake(long c) throws SSLException {
+                        long sslNativePtr = super.beforeHandshake(c);
+                        NativeCrypto.SSL_set_session(sslNativePtr, clientSessionContext);
+                        return sslNativePtr;
+                    }
+                }, null);
+        Future<TestSSLHandshakeCallbacks> server2 = handshake(listener, 0,
+                false, new ServerHooks(getServerPrivateKey(), getServerCertificates()) {
+                    @Override
+                    public long beforeHandshake(long c) throws SSLException {
+                        long sslNativePtr = super.beforeHandshake(c);
+                        NativeCrypto.SSL_set_session(sslNativePtr, serverSessionContext);
+                        return sslNativePtr;
+                    }
+                }, null);
+        TestSSLHandshakeCallbacks clientCallback2 = client2.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        TestSSLHandshakeCallbacks serverCallback2 = server2.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        assertTrue(clientCallback2.verifyCertificateChainCalled);
+        assertEqualCertificateChains(getServerCertificates(), clientCallback2.certificateChainRefs);
+        assertEquals("ECDHE_RSA", clientCallback2.authMethod);
+        assertFalse(serverCallback2.verifyCertificateChainCalled);
+        assertFalse(clientCallback2.clientCertificateRequestedCalled);
+        assertFalse(serverCallback2.clientCertificateRequestedCalled);
+        assertFalse(clientCallback2.clientPSKKeyRequestedInvoked);
+        assertFalse(serverCallback2.clientPSKKeyRequestedInvoked);
+        assertFalse(clientCallback2.serverPSKKeyRequestedInvoked);
+        assertFalse(serverCallback2.serverPSKKeyRequestedInvoked);
+        assertTrue(clientCallback2.onNewSessionEstablishedInvoked);
+        assertTrue(serverCallback2.onNewSessionEstablishedInvoked);
+        assertTrue(clientCallback2.handshakeCompletedCalled);
+        assertTrue(serverCallback2.handshakeCompletedCalled);
+
+        NativeCrypto.SSL_SESSION_free(clientSessionContext);
+        NativeCrypto.SSL_SESSION_free(serverSessionContext);
+    }
+
+    @Test
     public void test_SSL_do_handshake_optional_client_certificate() throws Exception {
         // optional client certificate case
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
 
         Hooks cHooks = new Hooks() {
             @Override
@@ -1065,8 +1256,8 @@
 
         assertTrue(clientCallback.clientCertificateRequestedCalled);
         assertNotNull(clientCallback.keyTypes);
-        assertEquals(new HashSet<String>(Arrays.asList("EC", "RSA")),
-                SSLParametersImpl.getSupportedClientKeyTypes(clientCallback.keyTypes));
+        assertEquals(new HashSet<>(Arrays.asList("EC", "RSA")),
+                SSLUtils.getSupportedClientKeyTypes(clientCallback.keyTypes));
         assertEqualPrincipals(getCaPrincipals(), clientCallback.asn1DerEncodedX500Principals);
         assertFalse(serverCallback.clientCertificateRequestedCalled);
 
@@ -1074,7 +1265,8 @@
         assertFalse(serverCallback.clientPSKKeyRequestedInvoked);
         assertFalse(clientCallback.serverPSKKeyRequestedInvoked);
         assertFalse(serverCallback.serverPSKKeyRequestedInvoked);
-
+        assertTrue(clientCallback.onNewSessionEstablishedInvoked);
+        assertTrue(serverCallback.onNewSessionEstablishedInvoked);
         assertTrue(clientCallback.handshakeCompletedCalled);
         assertTrue(serverCallback.handshakeCompletedCalled);
     }
@@ -1082,7 +1274,7 @@
     @Test
     public void test_SSL_do_handshake_missing_required_certificate() throws Exception {
         // required client certificate negative case
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         try {
             Hooks cHooks = new Hooks();
             Hooks sHooks = new ServerHooks(getServerPrivateKey(), getServerCertificates()) {
@@ -1090,8 +1282,7 @@
                 public long beforeHandshake(long c) throws SSLException {
                     long s = super.beforeHandshake(c);
                     NativeCrypto.SSL_set_client_CA_list(s, getCaPrincipals());
-                    NativeCrypto.SSL_set_verify(s,
-                            NativeCrypto.SSL_VERIFY_PEER
+                    NativeCrypto.SSL_set_verify(s, NativeCrypto.SSL_VERIFY_PEER
                                     | NativeCrypto.SSL_VERIFY_FAIL_IF_NO_PEER_CERT);
                     return s;
                 }
@@ -1106,74 +1297,10 @@
         }
     }
 
-    /**
-     * Usually if a RuntimeException is thrown by the
-     * clientCertificateRequestedCalled callback, the caller sees it
-     * during the call to NativeCrypto_SSL_do_handshake.  However, IIS
-     * does not request client certs until after the initial
-     * handshake. It does an SSL renegotiation, which means we need to
-     * be able to deliver the callback's exception in cases like
-     * SSL_read, SSL_write, and SSL_shutdown.
-     */
-    @Ignore("TODO(nathanmittler): Determine why this doesn't pass on openjdk")
-    @Test
-    public void test_SSL_do_handshake_clientCertificateRequested_throws_after_renegotiate()
-            throws Exception {
-        final ServerSocket listener = new ServerSocket(0);
-
-        Hooks cHooks = new Hooks() {
-            @Override
-            public long beforeHandshake(long context) throws SSLException {
-                long s = super.beforeHandshake(context);
-                NativeCrypto.SSL_clear_mode(s, SSL_MODE_ENABLE_FALSE_START);
-                return s;
-            }
-            @Override
-            public void afterHandshake(long session, long s, long c, Socket sock, FileDescriptor fd,
-                    SSLHandshakeCallbacks callback) throws Exception {
-                NativeCrypto.SSL_read(s, fd, callback, new byte[1], 0, 1, 0);
-                fail();
-                super.afterHandshake(session, s, c, sock, fd, callback);
-            }
-            @Override
-            public void clientCertificateRequested(long s) {
-                super.clientCertificateRequested(s);
-                throw new RuntimeException("expected");
-            }
-        };
-        Hooks sHooks = new ServerHooks(getServerPrivateKey(), getServerCertificates()) {
-            @Override
-            public void afterHandshake(long session, long s, long c, Socket sock, FileDescriptor fd,
-                    SSLHandshakeCallbacks callback) throws Exception {
-                try {
-                    NativeCrypto.SSL_set_verify(s, NativeCrypto.SSL_VERIFY_PEER);
-                    NativeCrypto.SSL_set_options(
-                            s, NativeConstants.SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);
-                    NativeCrypto.SSL_renegotiate(s);
-                    NativeCrypto.SSL_write(s, fd, callback, new byte[] {42}, 0, 1,
-                            (int) ((TIMEOUT_SECONDS * 1000) / 2));
-                } catch (IOException expected) {
-                } finally {
-                    super.afterHandshake(session, s, c, sock, fd, callback);
-                }
-            }
-        };
-        Future<TestSSLHandshakeCallbacks> client = handshake(listener, 0, true, cHooks, null);
-        Future<TestSSLHandshakeCallbacks> server = handshake(listener, 0, false, sHooks, null);
-        try {
-            client.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
-        } catch (ExecutionException e) {
-            if (!"expected".equals(e.getCause().getMessage())) {
-                throw e;
-            }
-        }
-        server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
-    }
-
     @Test
     public void test_SSL_do_handshake_client_timeout() throws Exception {
         // client timeout
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         Socket serverSocket = null;
         try {
             Hooks cHooks = new Hooks();
@@ -1197,7 +1324,7 @@
     @Test
     public void test_SSL_do_handshake_server_timeout() throws Exception {
         // server timeout
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         Socket clientSocket = null;
         try {
             Hooks cHooks = new Hooks();
@@ -1220,11 +1347,11 @@
         initChannelIdKey();
 
         // Normal handshake with TLS Channel ID.
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         Hooks cHooks = new Hooks();
         cHooks.channelIdPrivateKey = CHANNEL_ID_PRIVATE_KEY;
         // TLS Channel ID currently requires ECDHE-based key exchanges.
-        cHooks.enabledCipherSuites = Arrays.asList(new String[] {"ECDHE-RSA-AES128-SHA"});
+        cHooks.enabledCipherSuites = Collections.singletonList("ECDHE-RSA-AES128-SHA");
         ServerHooks sHooks = new ServerHooks(getServerPrivateKey(), getServerCertificates());
         sHooks.channelIdEnabled = true;
         sHooks.enabledCipherSuites = cHooks.enabledCipherSuites;
@@ -1242,6 +1369,8 @@
         assertFalse(serverCallback.clientPSKKeyRequestedInvoked);
         assertFalse(clientCallback.serverPSKKeyRequestedInvoked);
         assertFalse(serverCallback.serverPSKKeyRequestedInvoked);
+        assertTrue(clientCallback.onNewSessionEstablishedInvoked);
+        assertTrue(serverCallback.onNewSessionEstablishedInvoked);
         assertTrue(clientCallback.handshakeCompletedCalled);
         assertTrue(serverCallback.handshakeCompletedCalled);
         assertNull(sHooks.channelIdAfterHandshakeException);
@@ -1253,11 +1382,11 @@
         initChannelIdKey();
 
         // Client tries to use TLS Channel ID but the server does not enable/offer the extension.
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         Hooks cHooks = new Hooks();
         cHooks.channelIdPrivateKey = CHANNEL_ID_PRIVATE_KEY;
         // TLS Channel ID currently requires ECDHE-based key exchanges.
-        cHooks.enabledCipherSuites = Arrays.asList(new String[] {"ECDHE-RSA-AES128-SHA"});
+        cHooks.enabledCipherSuites = Collections.singletonList("ECDHE-RSA-AES128-SHA");
         ServerHooks sHooks = new ServerHooks(getServerPrivateKey(), getServerCertificates());
         sHooks.channelIdEnabled = false;
         sHooks.enabledCipherSuites = cHooks.enabledCipherSuites;
@@ -1275,6 +1404,8 @@
         assertFalse(serverCallback.clientPSKKeyRequestedInvoked);
         assertFalse(clientCallback.serverPSKKeyRequestedInvoked);
         assertFalse(serverCallback.serverPSKKeyRequestedInvoked);
+        assertTrue(clientCallback.onNewSessionEstablishedInvoked);
+        assertTrue(serverCallback.onNewSessionEstablishedInvoked);
         assertTrue(clientCallback.handshakeCompletedCalled);
         assertTrue(serverCallback.handshakeCompletedCalled);
         assertNull(sHooks.channelIdAfterHandshakeException);
@@ -1286,11 +1417,11 @@
         initChannelIdKey();
 
         // Client does not use TLS Channel ID when the server has the extension enabled/offered.
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         Hooks cHooks = new Hooks();
         cHooks.channelIdPrivateKey = null;
         // TLS Channel ID currently requires ECDHE-based key exchanges.
-        cHooks.enabledCipherSuites = Arrays.asList(new String[] {"ECDHE-RSA-AES128-SHA"});
+        cHooks.enabledCipherSuites = Collections.singletonList("ECDHE-RSA-AES128-SHA");
         ServerHooks sHooks = new ServerHooks(getServerPrivateKey(), getServerCertificates());
         sHooks.channelIdEnabled = true;
         sHooks.enabledCipherSuites = cHooks.enabledCipherSuites;
@@ -1308,6 +1439,8 @@
         assertFalse(serverCallback.clientPSKKeyRequestedInvoked);
         assertFalse(clientCallback.serverPSKKeyRequestedInvoked);
         assertFalse(serverCallback.serverPSKKeyRequestedInvoked);
+        assertTrue(clientCallback.onNewSessionEstablishedInvoked);
+        assertTrue(serverCallback.onNewSessionEstablishedInvoked);
         assertTrue(clientCallback.handshakeCompletedCalled);
         assertTrue(serverCallback.handshakeCompletedCalled);
         assertNull(sHooks.channelIdAfterHandshakeException);
@@ -1317,7 +1450,7 @@
     @Test
     public void test_SSL_do_handshake_with_psk_normal() throws Exception {
         // normal TLS-PSK client and server case
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         Hooks cHooks = new ClientHooks();
         ServerHooks sHooks = new ServerHooks();
         cHooks.pskEnabled = true;
@@ -1332,6 +1465,8 @@
         assertFalse(serverCallback.verifyCertificateChainCalled);
         assertFalse(clientCallback.clientCertificateRequestedCalled);
         assertFalse(serverCallback.clientCertificateRequestedCalled);
+        assertTrue(clientCallback.onNewSessionEstablishedInvoked);
+        assertTrue(serverCallback.onNewSessionEstablishedInvoked);
         assertTrue(clientCallback.handshakeCompletedCalled);
         assertTrue(serverCallback.handshakeCompletedCalled);
         assertTrue(clientCallback.clientPSKKeyRequestedInvoked);
@@ -1349,7 +1484,7 @@
     public void test_SSL_do_handshake_with_psk_with_identity_and_hint() throws Exception {
         // normal TLS-PSK client and server case where the server provides the client with a PSK
         // identity hint, and the client provides the server with a PSK identity.
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         ClientHooks cHooks = new ClientHooks();
         ServerHooks sHooks = new ServerHooks();
         cHooks.pskEnabled = true;
@@ -1366,6 +1501,8 @@
         assertFalse(serverCallback.verifyCertificateChainCalled);
         assertFalse(clientCallback.clientCertificateRequestedCalled);
         assertFalse(serverCallback.clientCertificateRequestedCalled);
+        assertTrue(clientCallback.onNewSessionEstablishedInvoked);
+        assertTrue(serverCallback.onNewSessionEstablishedInvoked);
         assertTrue(clientCallback.handshakeCompletedCalled);
         assertTrue(serverCallback.handshakeCompletedCalled);
         assertTrue(clientCallback.clientPSKKeyRequestedInvoked);
@@ -1385,7 +1522,7 @@
             throws Exception {
         // normal TLS-PSK client and server case where the server provides the client with a PSK
         // identity hint, and the client provides the server with a PSK identity.
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         ClientHooks cHooks = new ClientHooks();
         ServerHooks sHooks = new ServerHooks();
         cHooks.pskEnabled = true;
@@ -1421,7 +1558,7 @@
 
     @Test
     public void test_SSL_do_handshake_with_psk_key_mismatch() throws Exception {
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         ClientHooks cHooks = new ClientHooks();
         ServerHooks sHooks = new ServerHooks();
         cHooks.pskEnabled = true;
@@ -1441,7 +1578,7 @@
 
     @Test
     public void test_SSL_do_handshake_with_psk_with_no_client_key() throws Exception {
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         ClientHooks cHooks = new ClientHooks();
         ServerHooks sHooks = new ServerHooks();
         cHooks.pskEnabled = true;
@@ -1461,7 +1598,7 @@
 
     @Test
     public void test_SSL_do_handshake_with_psk_with_no_server_key() throws Exception {
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         ClientHooks cHooks = new ClientHooks();
         ServerHooks sHooks = new ServerHooks();
         cHooks.pskEnabled = true;
@@ -1482,7 +1619,7 @@
     @Test
     @SuppressWarnings("deprecation")
     public void test_SSL_do_handshake_with_psk_key_too_long() throws Exception {
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         ClientHooks cHooks = new ClientHooks() {
             @Override
             public void configureCallbacks(TestSSLHandshakeCallbacks callbacks) {
@@ -1510,7 +1647,7 @@
     public void test_SSL_do_handshake_with_ocsp_response() throws Exception {
         final byte[] OCSP_TEST_DATA = new byte[] {1, 2, 3, 4};
 
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         Hooks cHooks = new Hooks() {
             @Override
             public long beforeHandshake(long c) throws SSLException {
@@ -1551,7 +1688,7 @@
         // Each SCT entry has a length (unsigned 16-bit) and data.
         final byte[] SCT_TEST_DATA = new byte[] {0, 6, 0, 4, 1, 2, 3, 4};
 
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         Hooks cHooks = new Hooks() {
             @Override
             public long beforeHandshake(long c) throws SSLException {
@@ -1583,6 +1720,8 @@
         TestSSLHandshakeCallbacks clientCallback = client.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
         TestSSLHandshakeCallbacks serverCallback = server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
 
+        assertTrue(clientCallback.onNewSessionEstablishedInvoked);
+        assertTrue(serverCallback.onNewSessionEstablishedInvoked);
         assertTrue(clientCallback.handshakeCompletedCalled);
         assertTrue(serverCallback.handshakeCompletedCalled);
     }
@@ -1606,6 +1745,7 @@
                 NativeCrypto.SSL_use_psk_identity_hint(s, pskIdentityHint.toString());
                 fail();
             } catch (SSLException expected) {
+                // Expected.
             }
         } finally {
             NativeCrypto.SSL_free(s);
@@ -1613,26 +1753,23 @@
         }
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_set_session_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_set_session(NULL, NULL);
+    }
+
     @Test
     public void test_SSL_set_session() throws Exception {
-        try {
-            NativeCrypto.SSL_set_session(NULL, NULL);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
-        {
-            long c = NativeCrypto.SSL_CTX_new();
-            long s = NativeCrypto.SSL_new(c);
-            NativeCrypto.SSL_set_session(s, NULL);
-            NativeCrypto.SSL_free(s);
-            NativeCrypto.SSL_CTX_free(c);
-        }
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        NativeCrypto.SSL_set_session(s, NULL);
+        NativeCrypto.SSL_free(s);
+        NativeCrypto.SSL_CTX_free(c);
 
         {
             final long clientContext = NativeCrypto.SSL_CTX_new();
             final long serverContext = NativeCrypto.SSL_CTX_new();
-            final ServerSocket listener = new ServerSocket(0);
+            final ServerSocket listener = newServerSocket();
             final long[] clientSession = new long[] {NULL};
             final long[] serverSession = new long[] {NULL};
             {
@@ -1713,24 +1850,21 @@
         }
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_set_session_creation_enabled_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_set_session_creation_enabled(NULL, false);
+    }
+
     @Test
     public void test_SSL_set_session_creation_enabled() throws Exception {
-        try {
-            NativeCrypto.SSL_set_session_creation_enabled(NULL, false);
-            fail();
-        } catch (NullPointerException expected) {
-        }
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        NativeCrypto.SSL_set_session_creation_enabled(s, false);
+        NativeCrypto.SSL_set_session_creation_enabled(s, true);
+        NativeCrypto.SSL_free(s);
+        NativeCrypto.SSL_CTX_free(c);
 
-        {
-            long c = NativeCrypto.SSL_CTX_new();
-            long s = NativeCrypto.SSL_new(c);
-            NativeCrypto.SSL_set_session_creation_enabled(s, false);
-            NativeCrypto.SSL_set_session_creation_enabled(s, true);
-            NativeCrypto.SSL_free(s);
-            NativeCrypto.SSL_CTX_free(c);
-        }
-
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
 
         // negative test case for SSL_set_session_creation_enabled(false) on client
         {
@@ -1789,46 +1923,53 @@
         }
     }
 
-    @Test
-    public void test_SSL_set_tlsext_host_name() throws Exception {
-        // NULL SSL
+    @Test(expected = NullPointerException.class)
+    public void SSL_set_tlsext_host_name_withNullSslShouldThrow() throws Exception {
+        NativeCrypto.SSL_set_tlsext_host_name(NULL, null);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void SSL_set_tlsext_host_name_withNullHostnameShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+
         try {
-            NativeCrypto.SSL_set_tlsext_host_name(NULL, null);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
-        final String hostname = "www.android.com";
-
-        {
-            long c = NativeCrypto.SSL_CTX_new();
-            long s = NativeCrypto.SSL_new(c);
-
-            // null hostname
-            try {
-                NativeCrypto.SSL_set_tlsext_host_name(s, null);
-                fail();
-            } catch (NullPointerException expected) {
-            }
-
-            // too long hostname
-            try {
-                char[] longHostname = new char[256];
-                Arrays.fill(longHostname, 'w');
-                NativeCrypto.SSL_set_tlsext_host_name(s, new String(longHostname));
-                fail();
-            } catch (SSLException expected) {
-            }
-
-            assertNull(NativeCrypto.SSL_get_servername(s));
-            NativeCrypto.SSL_set_tlsext_host_name(s, new String(hostname));
-            assertEquals(hostname, NativeCrypto.SSL_get_servername(s));
-
+            NativeCrypto.SSL_set_tlsext_host_name(s, null);
+        } finally {
             NativeCrypto.SSL_free(s);
             NativeCrypto.SSL_CTX_free(c);
         }
+    }
 
-        final ServerSocket listener = new ServerSocket(0);
+    @Test(expected = SSLException.class)
+    public void SSL_set_tlsext_host_name_withTooLongHostnameShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+
+        try {
+            char[] longHostname = new char[256];
+            Arrays.fill(longHostname, 'w');
+            NativeCrypto.SSL_set_tlsext_host_name(s, new String(longHostname));
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
+        }
+    }
+
+    @Test
+    public void test_SSL_set_tlsext_host_name() throws Exception {
+        final String hostname = "www.android.com";
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+
+        assertNull(NativeCrypto.SSL_get_servername(s));
+        NativeCrypto.SSL_set_tlsext_host_name(s, hostname);
+        assertEquals(hostname, NativeCrypto.SSL_get_servername(s));
+
+        NativeCrypto.SSL_free(s);
+        NativeCrypto.SSL_CTX_free(c);
+
+        final ServerSocket listener = newServerSocket();
 
         // normal
         Hooks cHooks = new Hooks() {
@@ -1882,7 +2023,7 @@
             }
         };
 
-        ServerSocket listener = new ServerSocket(0);
+        ServerSocket listener = newServerSocket();
         Future<TestSSLHandshakeCallbacks> client =
                 handshake(listener, 0, true, cHooks, clientAlpnProtocols);
         Future<TestSSLHandshakeCallbacks> server =
@@ -1891,15 +2032,13 @@
         server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
     }
 
-    @Test
-    public void test_SSL_get_servername_null() throws Exception {
-        // NULL SSL
-        try {
-            NativeCrypto.SSL_get_servername(NULL);
-            fail();
-        } catch (NullPointerException expected) {
-        }
+    @Test(expected = NullPointerException.class)
+    public void test_SSL_get_servername_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_get_servername(NULL);
+    }
 
+    @Test
+    public void SSL_get_servername_shouldReturnNull() throws Exception {
         long c = NativeCrypto.SSL_CTX_new();
         long s = NativeCrypto.SSL_new(c);
         assertNull(NativeCrypto.SSL_get_servername(s));
@@ -1909,50 +2048,14 @@
         // additional positive testing by test_SSL_set_tlsext_host_name
     }
 
-    @Ignore("TODO(nathanmittler): Determine why this doesn't pass on openjdk")
-    @Test
-    public void test_SSL_renegotiate() throws Exception {
-        try {
-            NativeCrypto.SSL_renegotiate(NULL);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
-        final ServerSocket listener = new ServerSocket(0);
-        Hooks cHooks = new Hooks() {
-            @Override
-            public void afterHandshake(long session, long s, long c, Socket sock, FileDescriptor fd,
-                    SSLHandshakeCallbacks callback) throws Exception {
-                byte[] buffer = new byte[1];
-                NativeCrypto.SSL_read(s, fd, callback, buffer, 0, 1, 0);
-                assertEquals(42, buffer[0]);
-                super.afterHandshake(session, s, c, sock, fd, callback);
-            }
-        };
-        Hooks sHooks = new ServerHooks(getServerPrivateKey(), getServerCertificates()) {
-            @Override
-            public void afterHandshake(long session, long s, long c, Socket sock, FileDescriptor fd,
-                    SSLHandshakeCallbacks callback) throws Exception {
-                NativeCrypto.SSL_renegotiate(s);
-                NativeCrypto.SSL_write(s, fd, callback, new byte[] {42}, 0, 1, 0);
-                super.afterHandshake(session, s, c, sock, fd, callback);
-            }
-        };
-        Future<TestSSLHandshakeCallbacks> client = handshake(listener, 0, true, cHooks, null);
-        Future<TestSSLHandshakeCallbacks> server = handshake(listener, 0, false, sHooks, null);
-        client.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
-        server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    @Test(expected = NullPointerException.class)
+    public void SSL_get_certificate_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_get_certificate(NULL);
     }
 
     @Test
     public void test_SSL_get_certificate() throws Exception {
-        try {
-            NativeCrypto.SSL_get_certificate(NULL);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
         Hooks cHooks = new Hooks() {
             @Override
             public void afterHandshake(long session, long s, long c, Socket sock, FileDescriptor fd,
@@ -1976,15 +2079,14 @@
         server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_get_peer_cert_chain_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_get_peer_cert_chain(NULL);
+    }
+
     @Test
     public void test_SSL_get_peer_cert_chain() throws Exception {
-        try {
-            NativeCrypto.SSL_get_peer_cert_chain(NULL);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
 
         Hooks cHooks = new Hooks() {
             @Override
@@ -2005,70 +2107,82 @@
         server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
     }
 
-    final byte[] BYTES = new byte[] {2, -3, 5, 127, 0, -128};
+    @Test
+    public void test_SSL_cipher_names() throws Exception {
+        final ServerSocket listener = newServerSocket();
+        Hooks cHooks = new Hooks();
+        Hooks sHooks = new ServerHooks(getServerPrivateKey(), getServerCertificates());
+        // Both legacy and standard names are accepted.
+        cHooks.enabledCipherSuites = Collections.singletonList("ECDHE-RSA-AES128-GCM-SHA256");
+        sHooks.enabledCipherSuites =
+                Collections.singletonList("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
+        Future<TestSSLHandshakeCallbacks> client = handshake(listener, 0, true, cHooks, null);
+        Future<TestSSLHandshakeCallbacks> server = handshake(listener, 0, false, sHooks, null);
+        client.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        // The standard name is always reported.
+        assertEquals("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", cHooks.negotiatedCipherSuite);
+        assertEquals("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", sHooks.negotiatedCipherSuite);
+    }
+
+    private final byte[] BYTES = new byte[] {2, -3, 5, 127, 0, -128};
+
+    @Test(expected = NullPointerException.class)
+    public void SSL_read_withNullSslShouldThrow() throws Exception {
+        NativeCrypto.SSL_read(NULL, null, null, null, 0, 0, 0);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void SSL_read_withNullFdShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        try {
+            NativeCrypto.SSL_read(s, null, DUMMY_CB, null, 0, 0, 0);
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
+        }
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void SSL_read_withNullCallbacksShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        try {
+            NativeCrypto.SSL_read(s, INVALID_FD, null, null, 0, 0, 0);
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
+        }
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void SSL_read_withNullBytesShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        try {
+            NativeCrypto.SSL_read(s, INVALID_FD, DUMMY_CB, null, 0, 0, 0);
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
+        }
+    }
+
+    @Test(expected = SSLException.class)
+    public void SSL_read_beforeHandshakeShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        try {
+            NativeCrypto.SSL_read(s, INVALID_FD, DUMMY_CB, new byte[1], 0, 1, 0);
+        } finally {
+            NativeCrypto.SSL_free(s);
+            NativeCrypto.SSL_CTX_free(c);
+        }
+    }
 
     @Test
     public void test_SSL_read() throws Exception {
-        // NULL ssl
-        try {
-            NativeCrypto.SSL_read(NULL, null, null, null, 0, 0, 0);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
-        // null FileDescriptor
-        {
-            long c = NativeCrypto.SSL_CTX_new();
-            long s = NativeCrypto.SSL_new(c);
-            try {
-                NativeCrypto.SSL_read(s, null, DUMMY_CB, null, 0, 0, 0);
-                fail();
-            } catch (NullPointerException expected) {
-            }
-            NativeCrypto.SSL_free(s);
-            NativeCrypto.SSL_CTX_free(c);
-        }
-
-        // null SSLHandshakeCallbacks
-        {
-            long c = NativeCrypto.SSL_CTX_new();
-            long s = NativeCrypto.SSL_new(c);
-            try {
-                NativeCrypto.SSL_read(s, INVALID_FD, null, null, 0, 0, 0);
-                fail();
-            } catch (NullPointerException expected) {
-            }
-            NativeCrypto.SSL_free(s);
-            NativeCrypto.SSL_CTX_free(c);
-        }
-
-        // null byte array
-        {
-            long c = NativeCrypto.SSL_CTX_new();
-            long s = NativeCrypto.SSL_new(c);
-            try {
-                NativeCrypto.SSL_read(s, INVALID_FD, DUMMY_CB, null, 0, 0, 0);
-                fail();
-            } catch (NullPointerException expected) {
-            }
-            NativeCrypto.SSL_free(s);
-            NativeCrypto.SSL_CTX_free(c);
-        }
-
-        // handshaking not yet performed
-        {
-            long c = NativeCrypto.SSL_CTX_new();
-            long s = NativeCrypto.SSL_new(c);
-            try {
-                NativeCrypto.SSL_read(s, INVALID_FD, DUMMY_CB, new byte[1], 0, 1, 0);
-                fail();
-            } catch (SSLException expected) {
-            }
-            NativeCrypto.SSL_free(s);
-            NativeCrypto.SSL_CTX_free(c);
-        }
-
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
 
         // normal case
         {
@@ -2127,84 +2241,78 @@
         }
     }
 
-    @Test
-    public void test_SSL_write() throws Exception {
+    @Test(expected = NullPointerException.class)
+    public void SSL_write_withNullSslShouldThrow() throws Exception {
+        NativeCrypto.SSL_write(NULL, null, null, null, 0, 0, 0);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void SSL_write_withNullFdShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
         try {
-            NativeCrypto.SSL_write(NULL, null, null, null, 0, 0, 0);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
-        // null FileDescriptor
-        {
-            long c = NativeCrypto.SSL_CTX_new();
-            long s = NativeCrypto.SSL_new(c);
-            try {
-                NativeCrypto.SSL_write(s, null, DUMMY_CB, null, 0, 1, 0);
-                fail();
-            } catch (NullPointerException expected) {
-            }
+            NativeCrypto.SSL_write(s, null, DUMMY_CB, null, 0, 1, 0);
+        } finally {
             NativeCrypto.SSL_free(s);
             NativeCrypto.SSL_CTX_free(c);
         }
+    }
 
-        // null SSLHandshakeCallbacks
-        {
-            long c = NativeCrypto.SSL_CTX_new();
-            long s = NativeCrypto.SSL_new(c);
-            try {
-                NativeCrypto.SSL_write(s, INVALID_FD, null, null, 0, 1, 0);
-                fail();
-            } catch (NullPointerException expected) {
-            }
+    @Test(expected = NullPointerException.class)
+    public void SSL_write_withNullCallbacksShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        try {
+            NativeCrypto.SSL_write(s, INVALID_FD, null, null, 0, 1, 0);
+        } finally {
             NativeCrypto.SSL_free(s);
             NativeCrypto.SSL_CTX_free(c);
         }
+    }
 
-        // null byte array
-        {
-            long c = NativeCrypto.SSL_CTX_new();
-            long s = NativeCrypto.SSL_new(c);
-            try {
-                NativeCrypto.SSL_write(s, INVALID_FD, DUMMY_CB, null, 0, 1, 0);
-                fail();
-            } catch (NullPointerException expected) {
-            }
+    @Test(expected = NullPointerException.class)
+    public void SSL_write_withNullBytesShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        try {
+            NativeCrypto.SSL_write(s, INVALID_FD, DUMMY_CB, null, 0, 1, 0);
+        } finally {
             NativeCrypto.SSL_free(s);
             NativeCrypto.SSL_CTX_free(c);
         }
+    }
 
-        // handshaking not yet performed
-        {
-            long c = NativeCrypto.SSL_CTX_new();
-            long s = NativeCrypto.SSL_new(c);
-            try {
-                NativeCrypto.SSL_write(s, INVALID_FD, DUMMY_CB, new byte[1], 0, 1, 0);
-                fail();
-            } catch (SSLException expected) {
-            }
+    @Test(expected = SSLException.class)
+    public void SSL_write_beforeHandshakeShouldThrow() throws Exception {
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        try {
+            NativeCrypto.SSL_write(s, INVALID_FD, DUMMY_CB, new byte[1], 0, 1, 0);
+        } finally {
             NativeCrypto.SSL_free(s);
             NativeCrypto.SSL_CTX_free(c);
         }
+    }
 
-        // positively tested by test_SSL_read
+    @Test
+    public void SSL_interrupt_withNullShouldSucceed() {
+        // SSL_interrupt is a rare case that tolerates a null SSL argument
+        NativeCrypto.SSL_interrupt(NULL);
+    }
+
+    @Test
+    public void SSL_interrupt_withoutHandshakeShouldSucceed() throws Exception {
+        // also works without handshaking
+        long c = NativeCrypto.SSL_CTX_new();
+        long s = NativeCrypto.SSL_new(c);
+        NativeCrypto.SSL_interrupt(s);
+        NativeCrypto.SSL_free(s);
+        NativeCrypto.SSL_CTX_free(c);
     }
 
     @Test
     public void test_SSL_interrupt() throws Exception {
-        // SSL_interrupt is a rare case that tolerates a null SSL argument
-        NativeCrypto.SSL_interrupt(NULL);
-
-        // also works without handshaking
-        {
-            long c = NativeCrypto.SSL_CTX_new();
-            long s = NativeCrypto.SSL_new(c);
-            NativeCrypto.SSL_interrupt(s);
-            NativeCrypto.SSL_free(s);
-            NativeCrypto.SSL_CTX_free(c);
-        }
-
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
 
         Hooks cHooks = new Hooks() {
             @Override
@@ -2222,9 +2330,10 @@
                     @Override
                     public void run() {
                         try {
-                            Thread.sleep(1 * 1000);
+                            Thread.sleep(1000);
                             NativeCrypto.SSL_interrupt(s);
                         } catch (Exception e) {
+                            // Expected.
                         }
                     }
                 }.start();
@@ -2254,7 +2363,7 @@
     }
 
     @Test
-    public void test_SSL_shutdown() throws Exception {
+    public void SSL_shutdown_withNullFdShouldSucceed() throws Exception {
         // We tolerate a null FileDescriptor
         wrapWithSSLSession(new SSLSessionWrappedTask() {
             @Override
@@ -2262,32 +2371,31 @@
                 NativeCrypto.SSL_shutdown(sslSession, null, DUMMY_CB);
             }
         });
+    }
 
-        // null SSLHandshakeCallbacks
+    @Test(expected = NullPointerException.class)
+    public void SSL_shutdown_withNullCallbacksShouldThrow() throws Exception {
         wrapWithSSLSession(new SSLSessionWrappedTask() {
             @Override
             public void run(long sslSession) throws Exception {
-                try {
-                    NativeCrypto.SSL_shutdown(sslSession, INVALID_FD, null);
-                    fail();
-                } catch (NullPointerException expected) {
-                    // Ignored.
-                }
+                NativeCrypto.SSL_shutdown(sslSession, INVALID_FD, null);
             }
         });
+    }
 
+    @Test
+    public void SSL_shutdown_withNullSslShouldSucceed() throws Exception {
         // SSL_shutdown is a rare case that tolerates a null SSL argument
         NativeCrypto.SSL_shutdown(NULL, INVALID_FD, DUMMY_CB);
+    }
 
+    @Test(expected = SocketException.class)
+    public void SSL_shutdown_beforeHandshakeShouldThrow() throws Exception {
         // handshaking not yet performed
         wrapWithSSLSession(new SSLSessionWrappedTask() {
             @Override
             public void run(long sslSession) throws Exception {
-                try {
-                    NativeCrypto.SSL_shutdown(sslSession, INVALID_FD, DUMMY_CB);
-                    fail();
-                } catch (SocketException expected) {
-                }
+                NativeCrypto.SSL_shutdown(sslSession, INVALID_FD, DUMMY_CB);
             }
         });
 
@@ -2295,14 +2403,13 @@
         // SSL_shutdown to ensure SSL_SESSIONs are reused.
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_free_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_free(NULL);
+    }
+
     @Test
     public void test_SSL_free() throws Exception {
-        try {
-            NativeCrypto.SSL_free(NULL);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
         long c = NativeCrypto.SSL_CTX_new();
         NativeCrypto.SSL_free(NativeCrypto.SSL_new(c));
         NativeCrypto.SSL_CTX_free(c);
@@ -2311,15 +2418,14 @@
         // uses use SSL_free to cleanup in afterHandshake.
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_SESSION_session_id_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_SESSION_session_id(NULL);
+    }
+
     @Test
     public void test_SSL_SESSION_session_id() throws Exception {
-        try {
-            NativeCrypto.SSL_SESSION_session_id(NULL);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
 
         Hooks cHooks = new Hooks() {
             @Override
@@ -2338,15 +2444,14 @@
         server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_SESSION_get_time_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_SESSION_get_time(NULL);
+    }
+
     @Test
     public void test_SSL_SESSION_get_time() throws Exception {
-        try {
-            NativeCrypto.SSL_SESSION_get_time(NULL);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
 
         {
             Hooks cHooks = new Hooks() {
@@ -2367,15 +2472,14 @@
         }
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_SESSION_get_version_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_SESSION_get_version(NULL);
+    }
+
     @Test
     public void test_SSL_SESSION_get_version() throws Exception {
-        try {
-            NativeCrypto.SSL_SESSION_get_version(NULL);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
 
         Hooks cHooks = new Hooks() {
             @Override
@@ -2393,22 +2497,24 @@
         server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void SSL_SESSION_cipher_withNullShouldThrow() throws Exception {
+        NativeCrypto.SSL_SESSION_cipher(NULL);
+    }
+
     @Test
     public void test_SSL_SESSION_cipher() throws Exception {
-        try {
-            NativeCrypto.SSL_SESSION_cipher(NULL);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
 
         Hooks cHooks = new Hooks() {
             @Override
             public void afterHandshake(long session, long s, long c, Socket sock, FileDescriptor fd,
                     SSLHandshakeCallbacks callback) throws Exception {
-                String a = NativeCrypto.SSL_SESSION_cipher(session);
-                assertTrue(NativeCrypto.OPENSSL_TO_STANDARD_CIPHER_SUITES.containsKey(a));
+                String nativeCipher = NativeCrypto.SSL_SESSION_cipher(session);
+                String javaCipher = NativeCrypto.cipherSuiteFromJava(nativeCipher);
+                assertTrue(NativeCrypto.SUPPORTED_CIPHER_SUITES_SET.contains(javaCipher));
+                // SSL_SESSION_cipher should return a standard name rather than an OpenSSL name.
+                assertTrue(nativeCipher.startsWith("TLS_"));
                 super.afterHandshake(session, s, c, sock, fd, callback);
             }
         };
@@ -2428,15 +2534,14 @@
         NativeCrypto.SSL_SESSION_free(NULL);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void i2d_SSL_Session_WithNullSessionShouldThrow() throws Exception {
+        NativeCrypto.i2d_SSL_SESSION(NULL);
+    }
+
     @Test
     public void test_i2d_SSL_SESSION() throws Exception {
-        try {
-            NativeCrypto.i2d_SSL_SESSION(NULL);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-
-        final ServerSocket listener = new ServerSocket(0);
+        final ServerSocket listener = newServerSocket();
 
         Hooks cHooks = new Hooks() {
             @Override
@@ -2494,8 +2599,8 @@
         NativeCrypto.RAND_bytes(output);
 
         boolean isZero = true;
-        for (int i = 0; i < output.length; i++) {
-            isZero &= (output[i] == 0);
+        for (byte anOutput : output) {
+            isZero &= (anOutput == 0);
         }
 
         assertFalse("Random output was zero. This is a very low probability event (1 in 2^128) "
@@ -2503,14 +2608,9 @@
                 isZero);
     }
 
-    @Test
-    public void test_RAND_bytes_Null_Failure() throws Exception {
-        byte[] output = null;
-        try {
-            NativeCrypto.RAND_bytes(output);
-            fail("Should be an error on null buffer input");
-        } catch (RuntimeException expected) {
-        }
+    @Test(expected = RuntimeException.class)
+    public void RAND_bytes_withNullShouldThrow() throws Exception {
+        NativeCrypto.RAND_bytes(null);
     }
 
     @Test(expected = NullPointerException.class)
@@ -2518,16 +2618,19 @@
         NativeCrypto.EVP_get_digestbyname(null);
     }
 
+    @Test(expected = RuntimeException.class)
+    public void EVP_get_digestbyname_withEmptyShouldThrow() throws Exception {
+        NativeCrypto.EVP_get_digestbyname("");
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void EVP_get_digestbyname_withInvalidDigestShouldThrow() throws Exception {
+        NativeCrypto.EVP_get_digestbyname("foobar");
+    }
+
     @Test
     public void test_EVP_get_digestbyname() throws Exception {
         assertTrue(NativeCrypto.EVP_get_digestbyname("sha256") != NULL);
-
-        try {
-            NativeCrypto.EVP_get_digestbyname("");
-            NativeCrypto.EVP_get_digestbyname("foobar");
-            fail();
-        } catch (RuntimeException expected) {
-        }
     }
 
     @Test
@@ -2555,12 +2658,14 @@
             NativeCrypto.EVP_DigestSignInit(ctx, 0, pkey);
             fail();
         } catch (RuntimeException expected) {
+            // Expected.
         }
 
         try {
             NativeCrypto.EVP_DigestSignInit(ctx, evpMd, null);
             fail();
         } catch (RuntimeException expected) {
+            // Expected.
         }
     }
 
@@ -2569,18 +2674,14 @@
         NativeCrypto.get_RSA_private_params(null);
     }
 
-    @Test
+    @Test(expected = RuntimeException.class)
     public void test_get_RSA_private_params() throws Exception {
         // Test getting params for the wrong kind of key.
         final long groupCtx = NativeCrypto.EC_GROUP_new_by_curve_name("prime256v1");
         assertFalse(groupCtx == NULL);
         NativeRef.EC_GROUP group = new NativeRef.EC_GROUP(groupCtx);
         NativeRef.EVP_PKEY ctx = new NativeRef.EVP_PKEY(NativeCrypto.EC_KEY_generate_key(group));
-        try {
-            NativeCrypto.get_RSA_private_params(ctx);
-            fail();
-        } catch (RuntimeException expected) {
-        }
+        NativeCrypto.get_RSA_private_params(ctx);
     }
 
     @Test(expected = NullPointerException.class)
@@ -2588,18 +2689,14 @@
         NativeCrypto.get_RSA_public_params(null);
     }
 
-    @Test
+    @Test(expected = RuntimeException.class)
     public void test_get_RSA_public_params() throws Exception {
         // Test getting params for the wrong kind of key.
         final long groupCtx = NativeCrypto.EC_GROUP_new_by_curve_name("prime256v1");
         assertFalse(groupCtx == NULL);
         NativeRef.EC_GROUP group = new NativeRef.EC_GROUP(groupCtx);
         NativeRef.EVP_PKEY ctx = new NativeRef.EVP_PKEY(NativeCrypto.EC_KEY_generate_key(group));
-        try {
-            NativeCrypto.get_RSA_public_params(ctx);
-            fail();
-        } catch (RuntimeException expected) {
-        }
+        NativeCrypto.get_RSA_public_params(ctx);
     }
 
     @Test(expected = NullPointerException.class)
@@ -2654,8 +2751,6 @@
         long groupRef = NativeCrypto.EC_GROUP_new_by_curve_name(name);
         assertFalse(groupRef == NULL);
         NativeRef.EC_GROUP group = new NativeRef.EC_GROUP(groupRef);
-        assertEquals(NativeCrypto.OBJ_txt2nid_longName(name),
-                NativeCrypto.EC_GROUP_get_curve_name(group));
 
         // prime
         BigInteger p = new BigInteger(pStr, 16);
@@ -2744,6 +2839,7 @@
             NativeCrypto.ECDH_compute_key(out, outOffset, null, pkey2Ref);
             fail();
         } catch (NullPointerException expected) {
+            // Expected.
         }
 
         // Assert that it fails when only the second key is null
@@ -2751,21 +2847,22 @@
             NativeCrypto.ECDH_compute_key(out, outOffset, pkey1Ref, null);
             fail();
         } catch (NullPointerException expected) {
+            // Expected.
         }
     }
 
+    @Test(expected = NullPointerException.class)
+    public void EVP_CipherInit_ex_withNullCtxShouldThrow() throws Exception {
+        final long evpCipher = NativeCrypto.EVP_get_cipherbyname("aes-128-ecb");
+        NativeCrypto.EVP_CipherInit_ex(null, evpCipher, null, null, true);
+    }
+
     @Test
     public void test_EVP_CipherInit_ex_Null_Failure() throws Exception {
         final NativeRef.EVP_CIPHER_CTX ctx =
                 new NativeRef.EVP_CIPHER_CTX(NativeCrypto.EVP_CIPHER_CTX_new());
         final long evpCipher = NativeCrypto.EVP_get_cipherbyname("aes-128-ecb");
 
-        try {
-            NativeCrypto.EVP_CipherInit_ex(null, evpCipher, null, null, true);
-            fail("Null context should throw NullPointerException");
-        } catch (NullPointerException expected) {
-        }
-
         /* Initialize encrypting. */
         NativeCrypto.EVP_CipherInit_ex(ctx, evpCipher, null, null, true);
         NativeCrypto.EVP_CipherInit_ex(ctx, NULL, null, null, true);
@@ -2936,7 +3033,7 @@
     @Test(expected = NullPointerException.class)
     public void EVP_PKEY_CTX_set_rsa_oaep_md_NullMdCtx() throws Exception {
         long pkeyCtx = getRawPkeyCtxForEncrypt();
-        NativeRef.EVP_PKEY_CTX holder = new NativeRef.EVP_PKEY_CTX(pkeyCtx);
+        new NativeRef.EVP_PKEY_CTX(pkeyCtx);
         NativeCrypto.EVP_PKEY_CTX_set_rsa_oaep_md(pkeyCtx, NULL);
     }
 
@@ -2954,4 +3051,8 @@
         }
         fail("\"" + actualValue + "\" does not contain \"" + expectedSubstring + "\"");
     }
+
+    private static ServerSocket newServerSocket() throws IOException {
+        return new ServerSocket(0, 50, InetAddress.getLoopbackAddress());
+    }
 }
diff --git a/openjdk/src/test/java/org/conscrypt/NativeRefTest.java b/openjdk/src/test/java/org/conscrypt/NativeRefTest.java
index ed84813..e13297b 100644
--- a/openjdk/src/test/java/org/conscrypt/NativeRefTest.java
+++ b/openjdk/src/test/java/org/conscrypt/NativeRefTest.java
@@ -21,7 +21,11 @@
 public class NativeRefTest extends TestCase {
     public void test_zeroContextThrowsNullPointException() {
         try {
-            new NativeRef(0) {};
+            new NativeRef(0) {
+                @Override
+                void doFree(long context) {
+                }
+            };
             fail("Should throw NullPointerException when arguments are NULL");
         } catch (NullPointerException expected) {
         }
diff --git a/openjdk/src/test/java/org/conscrypt/OpenSSLExtendedSessionImplTest.java b/openjdk/src/test/java/org/conscrypt/OpenSSLExtendedSessionImplTest.java
deleted file mode 100644
index f3414a9..0000000
--- a/openjdk/src/test/java/org/conscrypt/OpenSSLExtendedSessionImplTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2014 The Android Open Source Project
- *
- * Licensed 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 impli$
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.conscrypt;
-
-import java.util.List;
-import javax.net.ssl.ExtendedSSLSession;
-import javax.net.ssl.SNIHostName;
-import javax.net.ssl.SNIServerName;
-import junit.framework.TestCase;
-
-/**
- * Test for OpenSSLExtendedSessionImpl
- */
-public class OpenSSLExtendedSessionImplTest extends TestCase {
-  static class MockSSLSession extends OpenSSLSessionImpl {
-    MockSSLSession() {
-      super(0, null, null, null, null, null, 0, null);
-    }
-
-    @Override
-    public String getRequestedServerName() {
-      return "server.name";
-    }
-  }
-
-  public void test_getRequestedServerNames() {
-    AbstractOpenSSLSession session = new MockSSLSession();
-    ExtendedSSLSession extendedSession = new OpenSSLExtendedSessionImpl(session);
-    List<SNIServerName> names = extendedSession.getRequestedServerNames();
-    assertEquals("server.name", ((SNIHostName) names.get(0)).getAsciiName());
-  }
-}
diff --git a/openjdk/src/test/java/org/conscrypt/PlatformTest.java b/openjdk/src/test/java/org/conscrypt/PlatformTest.java
index c090d2d..1d7ea75 100644
--- a/openjdk/src/test/java/org/conscrypt/PlatformTest.java
+++ b/openjdk/src/test/java/org/conscrypt/PlatformTest.java
@@ -37,8 +37,8 @@
         params.setServerNames(names);
         params.setUseCipherSuitesOrder(false);
         params.setEndpointIdentificationAlgorithm("ABC");
-        Platform.setSSLParameters(params, impl, (OpenSSLSocketImpl)socket);
-        assertEquals("some.host", ((OpenSSLSocketImpl)socket).getHostname());
+        Platform.setSSLParameters(params, impl, (AbstractConscryptSocket)socket);
+        assertEquals("some.host", ((AbstractConscryptSocket)socket).getHostname());
         assertFalse(impl.getUseCipherSuitesOrder());
         assertEquals("ABC", impl.getEndpointIdentificationAlgorithm());
     }
@@ -49,8 +49,8 @@
         SSLParameters params = new SSLParameters();
         impl.setUseCipherSuitesOrder(false);
         impl.setEndpointIdentificationAlgorithm("ABC");
-        ((OpenSSLSocketImpl)socket).setHostname("some.host");
-        Platform.getSSLParameters(params, impl, (OpenSSLSocketImpl)socket);
+        ((AbstractConscryptSocket)socket).setHostname("some.host");
+        Platform.getSSLParameters(params, impl, (AbstractConscryptSocket)socket);
         assertEquals("some.host", ((SNIHostName)params.getServerNames().get(0)).getAsciiName());
         assertFalse(params.getUseCipherSuitesOrder());
         assertEquals("ABC", params.getEndpointIdentificationAlgorithm());
@@ -59,14 +59,14 @@
     public void test_setSSLParameters_Engine() throws Exception {
         SSLParametersImpl impl = SSLParametersImpl.getDefault();
         SSLParameters params = new SSLParameters();
-        OpenSSLEngineImpl engine = new OpenSSLEngineImpl(impl);
+        ConscryptEngine engine = new ConscryptEngine(impl);
         List<SNIServerName> names = new ArrayList<SNIServerName>();
         names.add(new SNIHostName("some.host"));
         params.setServerNames(names);
         params.setUseCipherSuitesOrder(false);
         params.setEndpointIdentificationAlgorithm("ABC");
         Platform.setSSLParameters(params, impl, engine);
-        assertEquals("some.host", engine.getSniHostname());
+        assertEquals("some.host", engine.getHostname());
         assertFalse(impl.getUseCipherSuitesOrder());
         assertEquals("ABC", impl.getEndpointIdentificationAlgorithm());
     }
@@ -74,10 +74,10 @@
     public void test_getSSLParameters_Engine() throws Exception {
         SSLParametersImpl impl = SSLParametersImpl.getDefault();
         SSLParameters params = new SSLParameters();
-        OpenSSLEngineImpl engine = new OpenSSLEngineImpl(impl);
+        ConscryptEngine engine = new ConscryptEngine(impl);
         impl.setUseCipherSuitesOrder(false);
         impl.setEndpointIdentificationAlgorithm("ABC");
-        engine.setSniHostname("some.host");
+        engine.setHostname("some.host");
         Platform.getSSLParameters(params, impl, engine);
         assertEquals("some.host", ((SNIHostName)params.getServerNames().get(0)).getAsciiName());
         assertFalse(params.getUseCipherSuitesOrder());
diff --git a/openjdk/src/test/java/org/conscrypt/SSLParametersImplTest.java b/openjdk/src/test/java/org/conscrypt/SSLParametersImplTest.java
deleted file mode 100644
index 47837c7..0000000
--- a/openjdk/src/test/java/org/conscrypt/SSLParametersImplTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.conscrypt;
-
-import java.util.Arrays;
-import java.util.HashSet;
-import junit.framework.TestCase;
-
-public class SSLParametersImplTest extends TestCase {
-
-  public void testGetClientKeyType() throws Exception {
-    // See http://www.ietf.org/assignments/tls-parameters/tls-parameters.xml
-    byte b = Byte.MIN_VALUE;
-    do {
-      String byteString = Byte.toString(b);
-      String keyType = SSLParametersImpl.getClientKeyType(b);
-      switch (b) {
-        case 1:
-          assertEquals(byteString, "RSA", keyType);
-          break;
-        case 3:
-          assertEquals(byteString, "DH_RSA", keyType);
-          break;
-        case 64:
-          assertEquals(byteString, "EC", keyType);
-          break;
-        case 65:
-          assertEquals(byteString, "EC_RSA", keyType);
-          break;
-        case 66:
-          assertEquals(byteString, "EC_EC", keyType);
-          break;
-        default:
-          assertNull(byteString, keyType);
-      }
-      b++;
-    } while (b != Byte.MIN_VALUE);
-  }
-
-  public void testGetSupportedClientKeyTypes() throws Exception {
-      // Create an array with all possible values. Also, duplicate all values.
-      byte[] allClientCertificateTypes = new byte[512];
-      for (int i = 0; i < allClientCertificateTypes.length; i++) {
-          allClientCertificateTypes[i] = (byte) i;
-      }
-      assertEquals(
-              new HashSet<String>(Arrays.asList("RSA", "DH_RSA", "EC", "EC_RSA", "EC_EC")),
-              SSLParametersImpl.getSupportedClientKeyTypes(allClientCertificateTypes));
-  }
-}
diff --git a/openjdk/src/test/java/org/conscrypt/SSLUtilsTest.java b/openjdk/src/test/java/org/conscrypt/SSLUtilsTest.java
index c7f4d9d..dcffec7 100644
--- a/openjdk/src/test/java/org/conscrypt/SSLUtilsTest.java
+++ b/openjdk/src/test/java/org/conscrypt/SSLUtilsTest.java
@@ -18,7 +18,11 @@
 
 import static org.conscrypt.TestUtils.UTF_8;
 import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
+import java.util.Arrays;
+import java.util.HashSet;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -62,6 +66,38 @@
         assertArrayEquals(expected, actual);
     }
 
+    @Test
+    public void testGetClientKeyType() throws Exception {
+        // See http://www.ietf.org/assignments/tls-parameters/tls-parameters.xml
+        byte b = Byte.MIN_VALUE;
+        do {
+            String byteString = Byte.toString(b);
+            String keyType = SSLUtils.getClientKeyType(b);
+            switch (b) {
+                case 1:
+                    assertEquals(byteString, "RSA", keyType);
+                    break;
+                case 64:
+                    assertEquals(byteString, "EC", keyType);
+                    break;
+                default:
+                    assertNull(byteString, keyType);
+            }
+            b++;
+        } while (b != Byte.MIN_VALUE);
+    }
+
+    @Test
+    public void testGetSupportedClientKeyTypes() throws Exception {
+        // Create an array with all possible values. Also, duplicate all values.
+        byte[] allClientCertificateTypes = new byte[512];
+        for (int i = 0; i < allClientCertificateTypes.length; i++) {
+            allClientCertificateTypes[i] = (byte) i;
+        }
+        assertEquals(new HashSet<String>(Arrays.asList("RSA", "EC")),
+                SSLUtils.getSupportedClientKeyTypes(allClientCertificateTypes));
+    }
+
     private static String[] toStrings(byte[][] protocols) {
         int numProtocols = protocols.length;
         String[] out = new String[numProtocols];
diff --git a/openjdk/src/test/java/org/conscrypt/ServerSessionContextTest.java b/openjdk/src/test/java/org/conscrypt/ServerSessionContextTest.java
new file mode 100644
index 0000000..7d1a8ce
--- /dev/null
+++ b/openjdk/src/test/java/org/conscrypt/ServerSessionContextTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import java.util.Enumeration;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ServerSessionContextTest extends AbstractSessionContextTest<ServerSessionContext> {
+
+    @Override
+    ServerSessionContext newContext() {
+        return new ServerSessionContext();
+    }
+
+    @Override
+    SslSessionWrapper getCachedSession(ServerSessionContext context, SslSessionWrapper s) {
+        return context.getSessionFromCache(s.getId());
+    }
+
+    @Override
+    int size(ServerSessionContext context) {
+        int count = 0;
+        Enumeration<byte[]> ids = context.getIds();
+        while (ids.hasMoreElements()) {
+            ids.nextElement();
+            count++;
+        }
+        return count;
+    }
+}
diff --git a/openjdk/src/test/java/org/conscrypt/SslSessionWrapperTest.java b/openjdk/src/test/java/org/conscrypt/SslSessionWrapperTest.java
new file mode 100644
index 0000000..dc600d4
--- /dev/null
+++ b/openjdk/src/test/java/org/conscrypt/SslSessionWrapperTest.java
@@ -0,0 +1,629 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License", "www.google.com", 443);
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+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 java.util.Arrays;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SslSessionWrapperTest {
+    /*
+     * Taken from external/boringssl/src/ssl/ssl_test.cc: kOpenSSLSession is a
+     * serialized SSL_SESSION.
+     */
+    private static final byte[] kOpenSSLSession = new byte[] {(byte) 0x30, (byte) 0x82, (byte) 0x05,
+            (byte) 0xAA, (byte) 0x02, (byte) 0x01, (byte) 0x01, (byte) 0x02, (byte) 0x02,
+            (byte) 0x03, (byte) 0x03, (byte) 0x04, (byte) 0x02, (byte) 0xC0, (byte) 0x2F,
+            (byte) 0x04, (byte) 0x20, (byte) 0x06, (byte) 0xE5, (byte) 0x0D, (byte) 0x67,
+            (byte) 0x76, (byte) 0xAE, (byte) 0x18, (byte) 0x7E, (byte) 0x66, (byte) 0xDE,
+            (byte) 0xA3, (byte) 0x5C, (byte) 0xF0, (byte) 0x2E, (byte) 0x43, (byte) 0x51,
+            (byte) 0x2A, (byte) 0x60, (byte) 0x97, (byte) 0x19, (byte) 0xD3, (byte) 0x60,
+            (byte) 0x5A, (byte) 0xF1, (byte) 0x93, (byte) 0xDD, (byte) 0xCB, (byte) 0x24,
+            (byte) 0x57, (byte) 0x4C, (byte) 0x90, (byte) 0x90, (byte) 0x04, (byte) 0x30,
+            (byte) 0x26, (byte) 0x5A, (byte) 0xE5, (byte) 0xCE, (byte) 0x40, (byte) 0x16,
+            (byte) 0x04, (byte) 0xE5, (byte) 0xA2, (byte) 0x2E, (byte) 0x3F, (byte) 0xE3,
+            (byte) 0x27, (byte) 0xBE, (byte) 0x83, (byte) 0xEE, (byte) 0x5F, (byte) 0x94,
+            (byte) 0x5E, (byte) 0x88, (byte) 0xB3, (byte) 0x3F, (byte) 0x62, (byte) 0x88,
+            (byte) 0xD8, (byte) 0x2E, (byte) 0xC8, (byte) 0xD8, (byte) 0x57, (byte) 0x1C,
+            (byte) 0xA8, (byte) 0xC9, (byte) 0x88, (byte) 0x7C, (byte) 0x59, (byte) 0xA6,
+            (byte) 0x91, (byte) 0x4C, (byte) 0xB7, (byte) 0xDA, (byte) 0x72, (byte) 0x09,
+            (byte) 0xD2, (byte) 0x66, (byte) 0x47, (byte) 0x21, (byte) 0x6A, (byte) 0x09,
+            (byte) 0xA1, (byte) 0x06, (byte) 0x02, (byte) 0x04, (byte) 0x54, (byte) 0x43,
+            (byte) 0x3B, (byte) 0x8E, (byte) 0xA2, (byte) 0x04, (byte) 0x02, (byte) 0x02,
+            (byte) 0x01, (byte) 0x2C, (byte) 0xA3, (byte) 0x82, (byte) 0x04, (byte) 0x7A,
+            (byte) 0x30, (byte) 0x82, (byte) 0x04, (byte) 0x76, (byte) 0x30, (byte) 0x82,
+            (byte) 0x03, (byte) 0x5E, (byte) 0xA0, (byte) 0x03, (byte) 0x02, (byte) 0x01,
+            (byte) 0x02, (byte) 0x02, (byte) 0x08, (byte) 0x2B, (byte) 0xD7, (byte) 0x54,
+            (byte) 0xBE, (byte) 0xC3, (byte) 0xD6, (byte) 0x4A, (byte) 0x55, (byte) 0x30,
+            (byte) 0x0D, (byte) 0x06, (byte) 0x09, (byte) 0x2A, (byte) 0x86, (byte) 0x48,
+            (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01, (byte) 0x01, (byte) 0x05,
+            (byte) 0x05, (byte) 0x00, (byte) 0x30, (byte) 0x49, (byte) 0x31, (byte) 0x0B,
+            (byte) 0x30, (byte) 0x09, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04,
+            (byte) 0x06, (byte) 0x13, (byte) 0x02, (byte) 0x55, (byte) 0x53, (byte) 0x31,
+            (byte) 0x13, (byte) 0x30, (byte) 0x11, (byte) 0x06, (byte) 0x03, (byte) 0x55,
+            (byte) 0x04, (byte) 0x0A, (byte) 0x13, (byte) 0x0A, (byte) 0x47, (byte) 0x6F,
+            (byte) 0x6F, (byte) 0x67, (byte) 0x6C, (byte) 0x65, (byte) 0x20, (byte) 0x49,
+            (byte) 0x6E, (byte) 0x63, (byte) 0x31, (byte) 0x25, (byte) 0x30, (byte) 0x23,
+            (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x03, (byte) 0x13,
+            (byte) 0x1C, (byte) 0x47, (byte) 0x6F, (byte) 0x6F, (byte) 0x67, (byte) 0x6C,
+            (byte) 0x65, (byte) 0x20, (byte) 0x49, (byte) 0x6E, (byte) 0x74, (byte) 0x65,
+            (byte) 0x72, (byte) 0x6E, (byte) 0x65, (byte) 0x74, (byte) 0x20, (byte) 0x41,
+            (byte) 0x75, (byte) 0x74, (byte) 0x68, (byte) 0x6F, (byte) 0x72, (byte) 0x69,
+            (byte) 0x74, (byte) 0x79, (byte) 0x20, (byte) 0x47, (byte) 0x32, (byte) 0x30,
+            (byte) 0x1E, (byte) 0x17, (byte) 0x0D, (byte) 0x31, (byte) 0x34, (byte) 0x31,
+            (byte) 0x30, (byte) 0x30, (byte) 0x38, (byte) 0x31, (byte) 0x32, (byte) 0x30,
+            (byte) 0x37, (byte) 0x35, (byte) 0x37, (byte) 0x5A, (byte) 0x17, (byte) 0x0D,
+            (byte) 0x31, (byte) 0x35, (byte) 0x30, (byte) 0x31, (byte) 0x30, (byte) 0x36,
+            (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30,
+            (byte) 0x5A, (byte) 0x30, (byte) 0x68, (byte) 0x31, (byte) 0x0B, (byte) 0x30,
+            (byte) 0x09, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x06,
+            (byte) 0x13, (byte) 0x02, (byte) 0x55, (byte) 0x53, (byte) 0x31, (byte) 0x13,
+            (byte) 0x30, (byte) 0x11, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04,
+            (byte) 0x08, (byte) 0x0C, (byte) 0x0A, (byte) 0x43, (byte) 0x61, (byte) 0x6C,
+            (byte) 0x69, (byte) 0x66, (byte) 0x6F, (byte) 0x72, (byte) 0x6E, (byte) 0x69,
+            (byte) 0x61, (byte) 0x31, (byte) 0x16, (byte) 0x30, (byte) 0x14, (byte) 0x06,
+            (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x07, (byte) 0x0C, (byte) 0x0D,
+            (byte) 0x4D, (byte) 0x6F, (byte) 0x75, (byte) 0x6E, (byte) 0x74, (byte) 0x61,
+            (byte) 0x69, (byte) 0x6E, (byte) 0x20, (byte) 0x56, (byte) 0x69, (byte) 0x65,
+            (byte) 0x77, (byte) 0x31, (byte) 0x13, (byte) 0x30, (byte) 0x11, (byte) 0x06,
+            (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x0A, (byte) 0x0C, (byte) 0x0A,
+            (byte) 0x47, (byte) 0x6F, (byte) 0x6F, (byte) 0x67, (byte) 0x6C, (byte) 0x65,
+            (byte) 0x20, (byte) 0x49, (byte) 0x6E, (byte) 0x63, (byte) 0x31, (byte) 0x17,
+            (byte) 0x30, (byte) 0x15, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04,
+            (byte) 0x03, (byte) 0x0C, (byte) 0x0E, (byte) 0x77, (byte) 0x77, (byte) 0x77,
+            (byte) 0x2E, (byte) 0x67, (byte) 0x6F, (byte) 0x6F, (byte) 0x67, (byte) 0x6C,
+            (byte) 0x65, (byte) 0x2E, (byte) 0x63, (byte) 0x6F, (byte) 0x6D, (byte) 0x30,
+            (byte) 0x82, (byte) 0x01, (byte) 0x22, (byte) 0x30, (byte) 0x0D, (byte) 0x06,
+            (byte) 0x09, (byte) 0x2A, (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xF7,
+            (byte) 0x0D, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x00,
+            (byte) 0x03, (byte) 0x82, (byte) 0x01, (byte) 0x0F, (byte) 0x00, (byte) 0x30,
+            (byte) 0x82, (byte) 0x01, (byte) 0x0A, (byte) 0x02, (byte) 0x82, (byte) 0x01,
+            (byte) 0x01, (byte) 0x00, (byte) 0x9C, (byte) 0x29, (byte) 0xE2, (byte) 0xEB,
+            (byte) 0xA6, (byte) 0x50, (byte) 0x02, (byte) 0xF8, (byte) 0xBA, (byte) 0x1F,
+            (byte) 0xCB, (byte) 0xCB, (byte) 0x7F, (byte) 0xC0, (byte) 0x3C, (byte) 0x2D,
+            (byte) 0x07, (byte) 0xA7, (byte) 0xAE, (byte) 0xEF, (byte) 0x60, (byte) 0x95,
+            (byte) 0xA7, (byte) 0x47, (byte) 0x09, (byte) 0xE1, (byte) 0x5D, (byte) 0xE5,
+            (byte) 0x92, (byte) 0x73, (byte) 0x7A, (byte) 0x86, (byte) 0xE1, (byte) 0xFD,
+            (byte) 0x72, (byte) 0xDE, (byte) 0x85, (byte) 0x16, (byte) 0x4E, (byte) 0xF4,
+            (byte) 0xA1, (byte) 0x12, (byte) 0x21, (byte) 0xFD, (byte) 0x50, (byte) 0x4D,
+            (byte) 0x04, (byte) 0x1C, (byte) 0xFD, (byte) 0xD3, (byte) 0x48, (byte) 0xD8,
+            (byte) 0xCB, (byte) 0xEE, (byte) 0xF5, (byte) 0xD7, (byte) 0x52, (byte) 0x66,
+            (byte) 0xD5, (byte) 0xBF, (byte) 0x22, (byte) 0xA8, (byte) 0xE4, (byte) 0xD0,
+            (byte) 0xF5, (byte) 0xA4, (byte) 0xF9, (byte) 0x0B, (byte) 0xB4, (byte) 0x84,
+            (byte) 0x84, (byte) 0xD7, (byte) 0x10, (byte) 0x14, (byte) 0x9B, (byte) 0xEA,
+            (byte) 0xCC, (byte) 0x7D, (byte) 0xDE, (byte) 0x30, (byte) 0xF9, (byte) 0x1B,
+            (byte) 0xE9, (byte) 0x94, (byte) 0x96, (byte) 0x1A, (byte) 0x6D, (byte) 0x72,
+            (byte) 0x18, (byte) 0x5E, (byte) 0xCC, (byte) 0x09, (byte) 0x04, (byte) 0xC6,
+            (byte) 0x41, (byte) 0x71, (byte) 0x76, (byte) 0xD1, (byte) 0x29, (byte) 0x3F,
+            (byte) 0x3B, (byte) 0x5E, (byte) 0x85, (byte) 0x4A, (byte) 0x30, (byte) 0x32,
+            (byte) 0x9D, (byte) 0x4F, (byte) 0xDB, (byte) 0xDE, (byte) 0x82, (byte) 0x66,
+            (byte) 0x39, (byte) 0xCB, (byte) 0x5C, (byte) 0xC9, (byte) 0xC5, (byte) 0x98,
+            (byte) 0x91, (byte) 0x8D, (byte) 0x32, (byte) 0xB5, (byte) 0x2F, (byte) 0xE4,
+            (byte) 0xDC, (byte) 0xB0, (byte) 0x6E, (byte) 0x21, (byte) 0xDE, (byte) 0x39,
+            (byte) 0x3C, (byte) 0x96, (byte) 0xA8, (byte) 0x32, (byte) 0xA8, (byte) 0xC1,
+            (byte) 0xD1, (byte) 0x6C, (byte) 0xA9, (byte) 0xAA, (byte) 0xF3, (byte) 0x5E,
+            (byte) 0x24, (byte) 0x70, (byte) 0xB7, (byte) 0xAB, (byte) 0x92, (byte) 0x63,
+            (byte) 0x08, (byte) 0x1E, (byte) 0x11, (byte) 0x3F, (byte) 0xB3, (byte) 0x5F,
+            (byte) 0xC7, (byte) 0x98, (byte) 0xE3, (byte) 0x1D, (byte) 0x2A, (byte) 0xC2,
+            (byte) 0x32, (byte) 0x1C, (byte) 0x3C, (byte) 0x95, (byte) 0x43, (byte) 0x16,
+            (byte) 0xE0, (byte) 0x46, (byte) 0x83, (byte) 0xC6, (byte) 0x36, (byte) 0x91,
+            (byte) 0xF4, (byte) 0xA0, (byte) 0xE1, (byte) 0x3C, (byte) 0xB8, (byte) 0x23,
+            (byte) 0xB2, (byte) 0x4F, (byte) 0x8B, (byte) 0x0C, (byte) 0x8C, (byte) 0x92,
+            (byte) 0x45, (byte) 0x24, (byte) 0x43, (byte) 0x68, (byte) 0x24, (byte) 0x06,
+            (byte) 0x84, (byte) 0x43, (byte) 0x96, (byte) 0x2C, (byte) 0x96, (byte) 0x55,
+            (byte) 0x2F, (byte) 0x32, (byte) 0xE8, (byte) 0xE0, (byte) 0xDE, (byte) 0xBF,
+            (byte) 0x52, (byte) 0x57, (byte) 0x2D, (byte) 0x08, (byte) 0x71, (byte) 0x25,
+            (byte) 0x96, (byte) 0x90, (byte) 0x54, (byte) 0x4A, (byte) 0xF1, (byte) 0x0E,
+            (byte) 0xC8, (byte) 0x58, (byte) 0x1A, (byte) 0xE7, (byte) 0x6A, (byte) 0xAB,
+            (byte) 0xA0, (byte) 0x68, (byte) 0xE0, (byte) 0xAD, (byte) 0xFD, (byte) 0xD6,
+            (byte) 0x39, (byte) 0x0F, (byte) 0x76, (byte) 0xE4, (byte) 0xC1, (byte) 0x70,
+            (byte) 0xCD, (byte) 0xDE, (byte) 0x80, (byte) 0x2B, (byte) 0xE2, (byte) 0x1C,
+            (byte) 0x87, (byte) 0x48, (byte) 0x03, (byte) 0x46, (byte) 0x0F, (byte) 0x2C,
+            (byte) 0x41, (byte) 0xF7, (byte) 0x4B, (byte) 0x1F, (byte) 0x93, (byte) 0xAE,
+            (byte) 0x3F, (byte) 0x57, (byte) 0x1F, (byte) 0x2D, (byte) 0xF5, (byte) 0x35,
+            (byte) 0x02, (byte) 0x03, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0xA3,
+            (byte) 0x82, (byte) 0x01, (byte) 0x41, (byte) 0x30, (byte) 0x82, (byte) 0x01,
+            (byte) 0x3D, (byte) 0x30, (byte) 0x1D, (byte) 0x06, (byte) 0x03, (byte) 0x55,
+            (byte) 0x1D, (byte) 0x25, (byte) 0x04, (byte) 0x16, (byte) 0x30, (byte) 0x14,
+            (byte) 0x06, (byte) 0x08, (byte) 0x2B, (byte) 0x06, (byte) 0x01, (byte) 0x05,
+            (byte) 0x05, (byte) 0x07, (byte) 0x03, (byte) 0x01, (byte) 0x06, (byte) 0x08,
+            (byte) 0x2B, (byte) 0x06, (byte) 0x01, (byte) 0x05, (byte) 0x05, (byte) 0x07,
+            (byte) 0x03, (byte) 0x02, (byte) 0x30, (byte) 0x19, (byte) 0x06, (byte) 0x03,
+            (byte) 0x55, (byte) 0x1D, (byte) 0x11, (byte) 0x04, (byte) 0x12, (byte) 0x30,
+            (byte) 0x10, (byte) 0x82, (byte) 0x0E, (byte) 0x77, (byte) 0x77, (byte) 0x77,
+            (byte) 0x2E, (byte) 0x67, (byte) 0x6F, (byte) 0x6F, (byte) 0x67, (byte) 0x6C,
+            (byte) 0x65, (byte) 0x2E, (byte) 0x63, (byte) 0x6F, (byte) 0x6D, (byte) 0x30,
+            (byte) 0x68, (byte) 0x06, (byte) 0x08, (byte) 0x2B, (byte) 0x06, (byte) 0x01,
+            (byte) 0x05, (byte) 0x05, (byte) 0x07, (byte) 0x01, (byte) 0x01, (byte) 0x04,
+            (byte) 0x5C, (byte) 0x30, (byte) 0x5A, (byte) 0x30, (byte) 0x2B, (byte) 0x06,
+            (byte) 0x08, (byte) 0x2B, (byte) 0x06, (byte) 0x01, (byte) 0x05, (byte) 0x05,
+            (byte) 0x07, (byte) 0x30, (byte) 0x02, (byte) 0x86, (byte) 0x1F, (byte) 0x68,
+            (byte) 0x74, (byte) 0x74, (byte) 0x70, (byte) 0x3A, (byte) 0x2F, (byte) 0x2F,
+            (byte) 0x70, (byte) 0x6B, (byte) 0x69, (byte) 0x2E, (byte) 0x67, (byte) 0x6F,
+            (byte) 0x6F, (byte) 0x67, (byte) 0x6C, (byte) 0x65, (byte) 0x2E, (byte) 0x63,
+            (byte) 0x6F, (byte) 0x6D, (byte) 0x2F, (byte) 0x47, (byte) 0x49, (byte) 0x41,
+            (byte) 0x47, (byte) 0x32, (byte) 0x2E, (byte) 0x63, (byte) 0x72, (byte) 0x74,
+            (byte) 0x30, (byte) 0x2B, (byte) 0x06, (byte) 0x08, (byte) 0x2B, (byte) 0x06,
+            (byte) 0x01, (byte) 0x05, (byte) 0x05, (byte) 0x07, (byte) 0x30, (byte) 0x01,
+            (byte) 0x86, (byte) 0x1F, (byte) 0x68, (byte) 0x74, (byte) 0x74, (byte) 0x70,
+            (byte) 0x3A, (byte) 0x2F, (byte) 0x2F, (byte) 0x63, (byte) 0x6C, (byte) 0x69,
+            (byte) 0x65, (byte) 0x6E, (byte) 0x74, (byte) 0x73, (byte) 0x31, (byte) 0x2E,
+            (byte) 0x67, (byte) 0x6F, (byte) 0x6F, (byte) 0x67, (byte) 0x6C, (byte) 0x65,
+            (byte) 0x2E, (byte) 0x63, (byte) 0x6F, (byte) 0x6D, (byte) 0x2F, (byte) 0x6F,
+            (byte) 0x63, (byte) 0x73, (byte) 0x70, (byte) 0x30, (byte) 0x1D, (byte) 0x06,
+            (byte) 0x03, (byte) 0x55, (byte) 0x1D, (byte) 0x0E, (byte) 0x04, (byte) 0x16,
+            (byte) 0x04, (byte) 0x14, (byte) 0x3B, (byte) 0x6B, (byte) 0xE0, (byte) 0x9C,
+            (byte) 0xC6, (byte) 0xC6, (byte) 0x41, (byte) 0xC8, (byte) 0xEA, (byte) 0x5C,
+            (byte) 0xFB, (byte) 0x1A, (byte) 0x58, (byte) 0x15, (byte) 0xC2, (byte) 0x1B,
+            (byte) 0x9D, (byte) 0x43, (byte) 0x19, (byte) 0x85, (byte) 0x30, (byte) 0x0C,
+            (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x1D, (byte) 0x13, (byte) 0x01,
+            (byte) 0x01, (byte) 0xFF, (byte) 0x04, (byte) 0x02, (byte) 0x30, (byte) 0x00,
+            (byte) 0x30, (byte) 0x1F, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x1D,
+            (byte) 0x23, (byte) 0x04, (byte) 0x18, (byte) 0x30, (byte) 0x16, (byte) 0x80,
+            (byte) 0x14, (byte) 0x4A, (byte) 0xDD, (byte) 0x06, (byte) 0x16, (byte) 0x1B,
+            (byte) 0xBC, (byte) 0xF6, (byte) 0x68, (byte) 0xB5, (byte) 0x76, (byte) 0xF5,
+            (byte) 0x81, (byte) 0xB6, (byte) 0xBB, (byte) 0x62, (byte) 0x1A, (byte) 0xBA,
+            (byte) 0x5A, (byte) 0x81, (byte) 0x2F, (byte) 0x30, (byte) 0x17, (byte) 0x06,
+            (byte) 0x03, (byte) 0x55, (byte) 0x1D, (byte) 0x20, (byte) 0x04, (byte) 0x10,
+            (byte) 0x30, (byte) 0x0E, (byte) 0x30, (byte) 0x0C, (byte) 0x06, (byte) 0x0A,
+            (byte) 0x2B, (byte) 0x06, (byte) 0x01, (byte) 0x04, (byte) 0x01, (byte) 0xD6,
+            (byte) 0x79, (byte) 0x02, (byte) 0x05, (byte) 0x01, (byte) 0x30, (byte) 0x30,
+            (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x1D, (byte) 0x1F, (byte) 0x04,
+            (byte) 0x29, (byte) 0x30, (byte) 0x27, (byte) 0x30, (byte) 0x25, (byte) 0xA0,
+            (byte) 0x23, (byte) 0xA0, (byte) 0x21, (byte) 0x86, (byte) 0x1F, (byte) 0x68,
+            (byte) 0x74, (byte) 0x74, (byte) 0x70, (byte) 0x3A, (byte) 0x2F, (byte) 0x2F,
+            (byte) 0x70, (byte) 0x6B, (byte) 0x69, (byte) 0x2E, (byte) 0x67, (byte) 0x6F,
+            (byte) 0x6F, (byte) 0x67, (byte) 0x6C, (byte) 0x65, (byte) 0x2E, (byte) 0x63,
+            (byte) 0x6F, (byte) 0x6D, (byte) 0x2F, (byte) 0x47, (byte) 0x49, (byte) 0x41,
+            (byte) 0x47, (byte) 0x32, (byte) 0x2E, (byte) 0x63, (byte) 0x72, (byte) 0x6C,
+            (byte) 0x30, (byte) 0x0D, (byte) 0x06, (byte) 0x09, (byte) 0x2A, (byte) 0x86,
+            (byte) 0x48, (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01, (byte) 0x01,
+            (byte) 0x05, (byte) 0x05, (byte) 0x00, (byte) 0x03, (byte) 0x82, (byte) 0x01,
+            (byte) 0x01, (byte) 0x00, (byte) 0x9A, (byte) 0x39, (byte) 0x70, (byte) 0x81,
+            (byte) 0x76, (byte) 0x8A, (byte) 0x94, (byte) 0xCB, (byte) 0x96, (byte) 0xF1,
+            (byte) 0xCA, (byte) 0xAF, (byte) 0x96, (byte) 0xAE, (byte) 0x1D, (byte) 0x73,
+            (byte) 0xB3, (byte) 0x2C, (byte) 0x82, (byte) 0x16, (byte) 0x29, (byte) 0xB5,
+            (byte) 0x3C, (byte) 0x7E, (byte) 0x55, (byte) 0x53, (byte) 0x6F, (byte) 0xB2,
+            (byte) 0xBC, (byte) 0x34, (byte) 0x96, (byte) 0xAE, (byte) 0x00, (byte) 0xD8,
+            (byte) 0xF2, (byte) 0x26, (byte) 0xD1, (byte) 0x18, (byte) 0x99, (byte) 0x9F,
+            (byte) 0x7D, (byte) 0xFD, (byte) 0xEB, (byte) 0xE0, (byte) 0xBB, (byte) 0x9D,
+            (byte) 0xE6, (byte) 0x46, (byte) 0xA5, (byte) 0x74, (byte) 0xAB, (byte) 0x3D,
+            (byte) 0x93, (byte) 0xC6, (byte) 0x25, (byte) 0x28, (byte) 0x3D, (byte) 0xC8,
+            (byte) 0x4C, (byte) 0x6E, (byte) 0xCF, (byte) 0xD1, (byte) 0x84, (byte) 0xFF,
+            (byte) 0x46, (byte) 0x4F, (byte) 0x21, (byte) 0x2E, (byte) 0x07, (byte) 0xC4,
+            (byte) 0xB8, (byte) 0xB7, (byte) 0x2A, (byte) 0xE5, (byte) 0xC7, (byte) 0x34,
+            (byte) 0xC6, (byte) 0xA9, (byte) 0x84, (byte) 0xE3, (byte) 0x6C, (byte) 0x49,
+            (byte) 0xF8, (byte) 0x4A, (byte) 0x36, (byte) 0xBB, (byte) 0x3A, (byte) 0xBD,
+            (byte) 0xAD, (byte) 0x8A, (byte) 0x2B, (byte) 0x73, (byte) 0x97, (byte) 0xA6,
+            (byte) 0x30, (byte) 0x2C, (byte) 0x5F, (byte) 0xE4, (byte) 0xBD, (byte) 0x13,
+            (byte) 0x24, (byte) 0xE5, (byte) 0xD9, (byte) 0xA8, (byte) 0x74, (byte) 0x29,
+            (byte) 0x38, (byte) 0x47, (byte) 0x2E, (byte) 0xA6, (byte) 0xD6, (byte) 0x50,
+            (byte) 0xE0, (byte) 0xE8, (byte) 0xDD, (byte) 0x60, (byte) 0xC7, (byte) 0xD2,
+            (byte) 0xC6, (byte) 0x4E, (byte) 0x54, (byte) 0xCE, (byte) 0xE7, (byte) 0x94,
+            (byte) 0x84, (byte) 0x0D, (byte) 0xE8, (byte) 0x81, (byte) 0x92, (byte) 0x91,
+            (byte) 0x71, (byte) 0x19, (byte) 0x1D, (byte) 0x07, (byte) 0x75, (byte) 0x9E,
+            (byte) 0x59, (byte) 0x1A, (byte) 0x7E, (byte) 0x9D, (byte) 0x84, (byte) 0x61,
+            (byte) 0xC7, (byte) 0x84, (byte) 0xAD, (byte) 0xA3, (byte) 0x6A, (byte) 0xED,
+            (byte) 0xD8, (byte) 0x0D, (byte) 0x0C, (byte) 0x2A, (byte) 0x66, (byte) 0x3D,
+            (byte) 0xD7, (byte) 0xAE, (byte) 0x46, (byte) 0x1D, (byte) 0x4A, (byte) 0x8C,
+            (byte) 0x2B, (byte) 0xD6, (byte) 0x1A, (byte) 0x69, (byte) 0x71, (byte) 0xC3,
+            (byte) 0x5E, (byte) 0xA0, (byte) 0x6E, (byte) 0xED, (byte) 0x27, (byte) 0x9F,
+            (byte) 0xAF, (byte) 0x5B, (byte) 0x92, (byte) 0xA0, (byte) 0x03, (byte) 0xFD,
+            (byte) 0x83, (byte) 0x22, (byte) 0x09, (byte) 0x29, (byte) 0xE8, (byte) 0xA1,
+            (byte) 0x32, (byte) 0x2B, (byte) 0xEC, (byte) 0x1A, (byte) 0xA2, (byte) 0x75,
+            (byte) 0x4C, (byte) 0x3E, (byte) 0x99, (byte) 0x71, (byte) 0xCE, (byte) 0x8B,
+            (byte) 0x31, (byte) 0xEF, (byte) 0x9D, (byte) 0x37, (byte) 0x63, (byte) 0xFC,
+            (byte) 0x71, (byte) 0x91, (byte) 0x10, (byte) 0x1E, (byte) 0xD0, (byte) 0xF5,
+            (byte) 0xCB, (byte) 0x6F, (byte) 0x7A, (byte) 0xBA, (byte) 0x5E, (byte) 0x0C,
+            (byte) 0x8A, (byte) 0xFA, (byte) 0xA4, (byte) 0xDE, (byte) 0x36, (byte) 0xAD,
+            (byte) 0x51, (byte) 0x52, (byte) 0xFC, (byte) 0xFE, (byte) 0x10, (byte) 0xB0,
+            (byte) 0x81, (byte) 0xC8, (byte) 0x7D, (byte) 0x03, (byte) 0xC3, (byte) 0xB8,
+            (byte) 0x3C, (byte) 0x66, (byte) 0x6A, (byte) 0xF5, (byte) 0x6A, (byte) 0x81,
+            (byte) 0x7C, (byte) 0x45, (byte) 0xA6, (byte) 0x23, (byte) 0x21, (byte) 0xE1,
+            (byte) 0xD5, (byte) 0xD3, (byte) 0xED, (byte) 0x6E, (byte) 0x0D, (byte) 0x65,
+            (byte) 0x39, (byte) 0x77, (byte) 0x58, (byte) 0x09, (byte) 0x6B, (byte) 0x63,
+            (byte) 0xA4, (byte) 0x02, (byte) 0x04, (byte) 0x00, (byte) 0xA5, (byte) 0x03,
+            (byte) 0x02, (byte) 0x01, (byte) 0x14, (byte) 0xA9, (byte) 0x05, (byte) 0x02,
+            (byte) 0x03, (byte) 0x01, (byte) 0x89, (byte) 0xC0, (byte) 0xAA, (byte) 0x81,
+            (byte) 0xA7, (byte) 0x04, (byte) 0x81, (byte) 0xA4, (byte) 0x1C, (byte) 0x14,
+            (byte) 0x42, (byte) 0xFA, (byte) 0x1E, (byte) 0x3A, (byte) 0x4D, (byte) 0x0A,
+            (byte) 0x83, (byte) 0x7E, (byte) 0x92, (byte) 0x61, (byte) 0x37, (byte) 0x0B,
+            (byte) 0x12, (byte) 0x45, (byte) 0xEA, (byte) 0x2B, (byte) 0x03, (byte) 0x81,
+            (byte) 0x7C, (byte) 0x5F, (byte) 0x6F, (byte) 0x13, (byte) 0x82, (byte) 0x97,
+            (byte) 0xD0, (byte) 0xDC, (byte) 0x5E, (byte) 0x2F, (byte) 0x08, (byte) 0xDC,
+            (byte) 0x0D, (byte) 0x3A, (byte) 0x6C, (byte) 0xBA, (byte) 0x1D, (byte) 0xEA,
+            (byte) 0x5C, (byte) 0x46, (byte) 0x99, (byte) 0xF7, (byte) 0xDD, (byte) 0xAB,
+            (byte) 0xD4, (byte) 0xDD, (byte) 0xFC, (byte) 0x54, (byte) 0x37, (byte) 0x32,
+            (byte) 0x4B, (byte) 0xA3, (byte) 0xFB, (byte) 0x23, (byte) 0xA1, (byte) 0xC1,
+            (byte) 0x60, (byte) 0xDF, (byte) 0x41, (byte) 0xB0, (byte) 0xD1, (byte) 0xCC,
+            (byte) 0xDF, (byte) 0xAD, (byte) 0xB3, (byte) 0x66, (byte) 0x76, (byte) 0x36,
+            (byte) 0xEC, (byte) 0x6A, (byte) 0x53, (byte) 0xC3, (byte) 0xE2, (byte) 0xB0,
+            (byte) 0x77, (byte) 0xBE, (byte) 0x75, (byte) 0x08, (byte) 0xBA, (byte) 0x17,
+            (byte) 0x14, (byte) 0xFA, (byte) 0x1A, (byte) 0x30, (byte) 0xE7, (byte) 0xB9,
+            (byte) 0xED, (byte) 0xD6, (byte) 0xC1, (byte) 0xA5, (byte) 0x7A, (byte) 0x2B,
+            (byte) 0xA3, (byte) 0xA3, (byte) 0xDD, (byte) 0xDC, (byte) 0x14, (byte) 0xDB,
+            (byte) 0x7F, (byte) 0xF4, (byte) 0xF3, (byte) 0xAF, (byte) 0xCF, (byte) 0x0A,
+            (byte) 0xD3, (byte) 0xAC, (byte) 0x84, (byte) 0x39, (byte) 0x30, (byte) 0xCA,
+            (byte) 0x3C, (byte) 0xD8, (byte) 0xF7, (byte) 0xFA, (byte) 0x29, (byte) 0xDB,
+            (byte) 0x31, (byte) 0xA5, (byte) 0x62, (byte) 0x82, (byte) 0xD2, (byte) 0xB8,
+            (byte) 0x3C, (byte) 0xBC, (byte) 0x8F, (byte) 0xAB, (byte) 0xE4, (byte) 0xE8,
+            (byte) 0xA7, (byte) 0x2C, (byte) 0xEF, (byte) 0xC7, (byte) 0xD5, (byte) 0x12,
+            (byte) 0x16, (byte) 0x04, (byte) 0x6F, (byte) 0xCA, (byte) 0xEA, (byte) 0x31,
+            (byte) 0x9F, (byte) 0x41, (byte) 0xE0, (byte) 0x6F, (byte) 0xE4, (byte) 0x74,
+            (byte) 0x03, (byte) 0x78, (byte) 0xFA, (byte) 0x48, (byte) 0xB4, (byte) 0x6E,
+            (byte) 0xC8, (byte) 0xE7, (byte) 0x40, (byte) 0x8B, (byte) 0x88, (byte) 0x2F,
+            (byte) 0xED, (byte) 0x8E, (byte) 0x68, (byte) 0x96, (byte) 0x2C, (byte) 0xA7,
+            (byte) 0xB6, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x00};
+
+    private static final byte[] DUMMY_CERT =
+            new byte[] {(byte) 0x30, (byte) 0x82, (byte) 0x02, (byte) 0x58, (byte) 0x30,
+                    (byte) 0x82, (byte) 0x01, (byte) 0xC1, (byte) 0xA0, (byte) 0x03, (byte) 0x02,
+                    (byte) 0x01, (byte) 0x02, (byte) 0x02, (byte) 0x09, (byte) 0x00, (byte) 0xFB,
+                    (byte) 0xB0, (byte) 0x4C, (byte) 0x2E, (byte) 0xAB, (byte) 0x10, (byte) 0x9B,
+                    (byte) 0x0C, (byte) 0x30, (byte) 0x0D, (byte) 0x06, (byte) 0x09, (byte) 0x2A,
+                    (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01,
+                    (byte) 0x01, (byte) 0x05, (byte) 0x05, (byte) 0x00, (byte) 0x30, (byte) 0x45,
+                    (byte) 0x31, (byte) 0x0B, (byte) 0x30, (byte) 0x09, (byte) 0x06, (byte) 0x03,
+                    (byte) 0x55, (byte) 0x04, (byte) 0x06, (byte) 0x13, (byte) 0x02, (byte) 0x41,
+                    (byte) 0x55, (byte) 0x31, (byte) 0x13, (byte) 0x30, (byte) 0x11, (byte) 0x06,
+                    (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x08, (byte) 0x0C, (byte) 0x0A,
+                    (byte) 0x53, (byte) 0x6F, (byte) 0x6D, (byte) 0x65, (byte) 0x2D, (byte) 0x53,
+                    (byte) 0x74, (byte) 0x61, (byte) 0x74, (byte) 0x65, (byte) 0x31, (byte) 0x21,
+                    (byte) 0x30, (byte) 0x1F, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04,
+                    (byte) 0x0A, (byte) 0x0C, (byte) 0x18, (byte) 0x49, (byte) 0x6E, (byte) 0x74,
+                    (byte) 0x65, (byte) 0x72, (byte) 0x6E, (byte) 0x65, (byte) 0x74, (byte) 0x20,
+                    (byte) 0x57, (byte) 0x69, (byte) 0x64, (byte) 0x67, (byte) 0x69, (byte) 0x74,
+                    (byte) 0x73, (byte) 0x20, (byte) 0x50, (byte) 0x74, (byte) 0x79, (byte) 0x20,
+                    (byte) 0x4C, (byte) 0x74, (byte) 0x64, (byte) 0x30, (byte) 0x1E, (byte) 0x17,
+                    (byte) 0x0D, (byte) 0x31, (byte) 0x34, (byte) 0x30, (byte) 0x34, (byte) 0x32,
+                    (byte) 0x33, (byte) 0x32, (byte) 0x30, (byte) 0x35, (byte) 0x30, (byte) 0x34,
+                    (byte) 0x30, (byte) 0x5A, (byte) 0x17, (byte) 0x0D, (byte) 0x31, (byte) 0x37,
+                    (byte) 0x30, (byte) 0x34, (byte) 0x32, (byte) 0x32, (byte) 0x32, (byte) 0x30,
+                    (byte) 0x35, (byte) 0x30, (byte) 0x34, (byte) 0x30, (byte) 0x5A, (byte) 0x30,
+                    (byte) 0x45, (byte) 0x31, (byte) 0x0B, (byte) 0x30, (byte) 0x09, (byte) 0x06,
+                    (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x06, (byte) 0x13, (byte) 0x02,
+                    (byte) 0x41, (byte) 0x55, (byte) 0x31, (byte) 0x13, (byte) 0x30, (byte) 0x11,
+                    (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x08, (byte) 0x0C,
+                    (byte) 0x0A, (byte) 0x53, (byte) 0x6F, (byte) 0x6D, (byte) 0x65, (byte) 0x2D,
+                    (byte) 0x53, (byte) 0x74, (byte) 0x61, (byte) 0x74, (byte) 0x65, (byte) 0x31,
+                    (byte) 0x21, (byte) 0x30, (byte) 0x1F, (byte) 0x06, (byte) 0x03, (byte) 0x55,
+                    (byte) 0x04, (byte) 0x0A, (byte) 0x0C, (byte) 0x18, (byte) 0x49, (byte) 0x6E,
+                    (byte) 0x74, (byte) 0x65, (byte) 0x72, (byte) 0x6E, (byte) 0x65, (byte) 0x74,
+                    (byte) 0x20, (byte) 0x57, (byte) 0x69, (byte) 0x64, (byte) 0x67, (byte) 0x69,
+                    (byte) 0x74, (byte) 0x73, (byte) 0x20, (byte) 0x50, (byte) 0x74, (byte) 0x79,
+                    (byte) 0x20, (byte) 0x4C, (byte) 0x74, (byte) 0x64, (byte) 0x30, (byte) 0x81,
+                    (byte) 0x9F, (byte) 0x30, (byte) 0x0D, (byte) 0x06, (byte) 0x09, (byte) 0x2A,
+                    (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01,
+                    (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x00, (byte) 0x03, (byte) 0x81,
+                    (byte) 0x8D, (byte) 0x00, (byte) 0x30, (byte) 0x81, (byte) 0x89, (byte) 0x02,
+                    (byte) 0x81, (byte) 0x81, (byte) 0x00, (byte) 0xD8, (byte) 0x2B, (byte) 0xC8,
+                    (byte) 0xA6, (byte) 0x32, (byte) 0xE4, (byte) 0x62, (byte) 0xFF, (byte) 0x4D,
+                    (byte) 0xF3, (byte) 0xD0, (byte) 0xAD, (byte) 0x59, (byte) 0x8B, (byte) 0x45,
+                    (byte) 0xA7, (byte) 0xBD, (byte) 0xF1, (byte) 0x47, (byte) 0xBF, (byte) 0x09,
+                    (byte) 0x58, (byte) 0x7B, (byte) 0x22, (byte) 0xBD, (byte) 0x35, (byte) 0xAE,
+                    (byte) 0x97, (byte) 0x25, (byte) 0x86, (byte) 0x94, (byte) 0xA0, (byte) 0x80,
+                    (byte) 0xC0, (byte) 0xB4, (byte) 0x1F, (byte) 0x76, (byte) 0x91, (byte) 0x67,
+                    (byte) 0x46, (byte) 0x31, (byte) 0xD0, (byte) 0x10, (byte) 0x84, (byte) 0xB7,
+                    (byte) 0x22, (byte) 0x1E, (byte) 0x70, (byte) 0x23, (byte) 0x91, (byte) 0x72,
+                    (byte) 0xC8, (byte) 0xE9, (byte) 0x6D, (byte) 0x79, (byte) 0x3A, (byte) 0x85,
+                    (byte) 0x77, (byte) 0x80, (byte) 0x0F, (byte) 0xC4, (byte) 0x95, (byte) 0x16,
+                    (byte) 0x75, (byte) 0xC5, (byte) 0x4A, (byte) 0x71, (byte) 0x4C, (byte) 0xC8,
+                    (byte) 0x63, (byte) 0x3F, (byte) 0xA3, (byte) 0xF2, (byte) 0x63, (byte) 0x9C,
+                    (byte) 0x2A, (byte) 0x4F, (byte) 0x9A, (byte) 0xFA, (byte) 0xCB, (byte) 0xC1,
+                    (byte) 0x71, (byte) 0x6E, (byte) 0x28, (byte) 0x85, (byte) 0x28, (byte) 0xA0,
+                    (byte) 0x27, (byte) 0x1E, (byte) 0x65, (byte) 0x1C, (byte) 0xAE, (byte) 0x07,
+                    (byte) 0xD5, (byte) 0x5B, (byte) 0x6F, (byte) 0x2D, (byte) 0x43, (byte) 0xED,
+                    (byte) 0x2B, (byte) 0x90, (byte) 0xB1, (byte) 0x8C, (byte) 0xAF, (byte) 0x24,
+                    (byte) 0x6D, (byte) 0xAE, (byte) 0xE9, (byte) 0x17, (byte) 0x3A, (byte) 0x05,
+                    (byte) 0xC1, (byte) 0xBF, (byte) 0xB8, (byte) 0x1C, (byte) 0xAE, (byte) 0x65,
+                    (byte) 0x3B, (byte) 0x1B, (byte) 0x58, (byte) 0xC2, (byte) 0xD9, (byte) 0xAE,
+                    (byte) 0xD6, (byte) 0xAA, (byte) 0x67, (byte) 0x88, (byte) 0xF1, (byte) 0x02,
+                    (byte) 0x03, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0xA3, (byte) 0x50,
+                    (byte) 0x30, (byte) 0x4E, (byte) 0x30, (byte) 0x1D, (byte) 0x06, (byte) 0x03,
+                    (byte) 0x55, (byte) 0x1D, (byte) 0x0E, (byte) 0x04, (byte) 0x16, (byte) 0x04,
+                    (byte) 0x14, (byte) 0x8B, (byte) 0x75, (byte) 0xD5, (byte) 0xAC, (byte) 0xCB,
+                    (byte) 0x08, (byte) 0xBE, (byte) 0x0E, (byte) 0x1F, (byte) 0x65, (byte) 0xB7,
+                    (byte) 0xFA, (byte) 0x56, (byte) 0xBE, (byte) 0x6C, (byte) 0xA7, (byte) 0x75,
+                    (byte) 0xDA, (byte) 0x85, (byte) 0xAF, (byte) 0x30, (byte) 0x1F, (byte) 0x06,
+                    (byte) 0x03, (byte) 0x55, (byte) 0x1D, (byte) 0x23, (byte) 0x04, (byte) 0x18,
+                    (byte) 0x30, (byte) 0x16, (byte) 0x80, (byte) 0x14, (byte) 0x8B, (byte) 0x75,
+                    (byte) 0xD5, (byte) 0xAC, (byte) 0xCB, (byte) 0x08, (byte) 0xBE, (byte) 0x0E,
+                    (byte) 0x1F, (byte) 0x65, (byte) 0xB7, (byte) 0xFA, (byte) 0x56, (byte) 0xBE,
+                    (byte) 0x6C, (byte) 0xA7, (byte) 0x75, (byte) 0xDA, (byte) 0x85, (byte) 0xAF,
+                    (byte) 0x30, (byte) 0x0C, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x1D,
+                    (byte) 0x13, (byte) 0x04, (byte) 0x05, (byte) 0x30, (byte) 0x03, (byte) 0x01,
+                    (byte) 0x01, (byte) 0xFF, (byte) 0x30, (byte) 0x0D, (byte) 0x06, (byte) 0x09,
+                    (byte) 0x2A, (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xF7, (byte) 0x0D,
+                    (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x05, (byte) 0x00, (byte) 0x03,
+                    (byte) 0x81, (byte) 0x81, (byte) 0x00, (byte) 0x3B, (byte) 0xE8, (byte) 0x78,
+                    (byte) 0x6D, (byte) 0x95, (byte) 0xD6, (byte) 0x3D, (byte) 0x6A, (byte) 0xF7,
+                    (byte) 0x13, (byte) 0x19, (byte) 0x2C, (byte) 0x1B, (byte) 0xC2, (byte) 0x88,
+                    (byte) 0xAE, (byte) 0x22, (byte) 0xAB, (byte) 0xF4, (byte) 0x8D, (byte) 0x32,
+                    (byte) 0xF5, (byte) 0x7C, (byte) 0x71, (byte) 0x67, (byte) 0xCF, (byte) 0x2D,
+                    (byte) 0xD1, (byte) 0x1C, (byte) 0xC2, (byte) 0xC3, (byte) 0x87, (byte) 0xE2,
+                    (byte) 0xE9, (byte) 0xBE, (byte) 0x89, (byte) 0x5C, (byte) 0xE4, (byte) 0x34,
+                    (byte) 0xAB, (byte) 0x48, (byte) 0x91, (byte) 0xC2, (byte) 0x3F, (byte) 0x95,
+                    (byte) 0xAE, (byte) 0x2B, (byte) 0x47, (byte) 0x9E, (byte) 0x25, (byte) 0x78,
+                    (byte) 0x6B, (byte) 0x4F, (byte) 0x9A, (byte) 0x10, (byte) 0xA4, (byte) 0x72,
+                    (byte) 0xFD, (byte) 0xCF, (byte) 0xF7, (byte) 0x02, (byte) 0x0C, (byte) 0xB0,
+                    (byte) 0x0A, (byte) 0x08, (byte) 0xA4, (byte) 0x5A, (byte) 0xE2, (byte) 0xE5,
+                    (byte) 0x74, (byte) 0x7E, (byte) 0x11, (byte) 0x1D, (byte) 0x39, (byte) 0x60,
+                    (byte) 0x6A, (byte) 0xC9, (byte) 0x1F, (byte) 0x69, (byte) 0xF3, (byte) 0x2E,
+                    (byte) 0x63, (byte) 0x26, (byte) 0xDC, (byte) 0x9E, (byte) 0xEF, (byte) 0x6B,
+                    (byte) 0x7A, (byte) 0x0A, (byte) 0xE1, (byte) 0x54, (byte) 0x57, (byte) 0x98,
+                    (byte) 0xAA, (byte) 0x72, (byte) 0x91, (byte) 0x78, (byte) 0x04, (byte) 0x7E,
+                    (byte) 0x1F, (byte) 0x8F, (byte) 0x65, (byte) 0x4D, (byte) 0x1F, (byte) 0x0B,
+                    (byte) 0x12, (byte) 0xAC, (byte) 0x9C, (byte) 0x24, (byte) 0x0F, (byte) 0x84,
+                    (byte) 0x14, (byte) 0x1A, (byte) 0x55, (byte) 0x2D, (byte) 0x1F, (byte) 0xBB,
+                    (byte) 0xF0, (byte) 0x9D, (byte) 0x09, (byte) 0xB2, (byte) 0x08, (byte) 0x5C,
+                    (byte) 0x59, (byte) 0x32, (byte) 0x65, (byte) 0x80, (byte) 0x26};
+
+    private static final byte[] DUMMY_OCSP_DATA = new byte[1];
+
+    private static final byte[] DUMMY_TLS_SCT_DATA = new byte[1];
+
+    @After
+    public void tearDown() throws Exception {
+        assertEquals(0, NativeCrypto.ERR_peek_last_error());
+    }
+
+    private static TestSessionBuilder getType1() {
+        return new TestSessionBuilder()
+                .setType(0x01)
+                .setSessionData(kOpenSSLSession)
+                .addCertificate(DUMMY_CERT);
+    }
+
+    private static TestSessionBuilder getType2() {
+        return new TestSessionBuilder()
+                .setType(0x02)
+                .setSessionData(kOpenSSLSession)
+                .addCertificate(DUMMY_CERT)
+                .addOcspData(DUMMY_OCSP_DATA);
+    }
+
+    private static TestSessionBuilder getType3() {
+        return new TestSessionBuilder()
+                .setType(0x03)
+                .setSessionData(kOpenSSLSession)
+                .addCertificate(DUMMY_CERT)
+                .addOcspData(DUMMY_OCSP_DATA)
+                .setTlsSctData(DUMMY_TLS_SCT_DATA);
+    }
+
+    @Test
+    public void toSession_EmptyArray_Invalid_Failure() throws Exception {
+        assertInvalidSession(new byte[0]);
+    }
+
+    @Test
+    public void toSession_Type1_Valid_Success() throws Exception {
+        assertValidSession(getType1().build());
+    }
+
+    @Test
+    public void toSession_Type2_Valid_Success() throws Exception {
+        assertValidSession(getType2().build());
+    }
+
+    @Test
+    public void toSession_Type3_Valid_Success() throws Exception {
+        assertValidSession(getType3().build());
+    }
+
+    private void assertTruncatedSessionFails(byte[] validSession) {
+        for (int i = 0; i < validSession.length - 1; i++) {
+            byte[] truncatedSession = new byte[i];
+            System.arraycopy(validSession, 0, truncatedSession, 0, i);
+            assertNull("Truncating to " + i + " bytes of " + validSession.length
+                            + " should not succeed",
+                    SslSessionWrapper.newInstance(null, truncatedSession, "www.google.com", 443));
+        }
+    }
+
+    @Test
+    public void toSession_Type3_Truncated_Failure() throws Exception {
+        assertTruncatedSessionFails(getType3().build());
+    }
+
+    private static void assertValidSession(byte[] data) {
+        assertNotNull(SslSessionWrapper.newInstance(null, data, "www.google.com", 443));
+    }
+
+    private static void assertInvalidSession(byte[] data) {
+        assertNull(SslSessionWrapper.newInstance(null, data, "www.google.com", 443));
+    }
+
+    @Test
+    public void toSession_UnknownType_Failure() throws Exception {
+        assertInvalidSession(getType3().setType((byte) 0xEE).build());
+    }
+
+    @Test
+    public void toSession_CertificatesCountTooLarge_Failure() throws Exception {
+        assertInvalidSession(getType3().setCertificatesLength(16834).build());
+    }
+
+    @Test
+    public void toSession_CertificatesCountNegative_Failure() throws Exception {
+        assertInvalidSession(getType3().setCertificatesLength(-1).build());
+    }
+
+    @Test
+    public void toSession_CertificateSizeNegative_Failure() throws Exception {
+        assertInvalidSession(getType3().setCertificateLength(0, -1).build());
+    }
+
+    @Test
+    public void toSession_CertificateSizeTooLarge_Failure() throws Exception {
+        assertInvalidSession(getType3().setCertificateLength(0, 16834).build());
+    }
+
+    @Test
+    public void toSession_SessionDataSizeTooLarge_Failure() throws Exception {
+        assertInvalidSession(getType3().setSessionDataLength(16834).build());
+    }
+
+    @Test
+    public void toSession_SessionDataSizeNegative_Failure() throws Exception {
+        assertInvalidSession(getType3().setSessionDataLength(-1).build());
+    }
+
+    @Test
+    public void toSession_OcspDatasNumberTooMany_Failure() throws Exception {
+        assertInvalidSession(getType3().setOcspDatasLength(32791).build());
+    }
+
+    @Test
+    public void toSession_OcspDatasNumberNegative_Failure() throws Exception {
+        assertInvalidSession(getType3().setOcspDatasLength(-1).build());
+    }
+
+    @Test
+    public void toSession_OcspDataSizeNegative_Failure() throws Exception {
+        assertInvalidSession(getType3().setOcspDataLength(0, -1).build());
+    }
+
+    @Test
+    public void toSession_OcspDataSizeTooLarge_Failure() throws Exception {
+        assertInvalidSession(getType3().setOcspDataLength(0, 92948).build());
+    }
+
+    @Test
+    public void toSession_TlsSctDataSizeNegative_Failure() throws Exception {
+        assertInvalidSession(getType3().setTlsSctDataLength(-1).build());
+    }
+
+    @Test
+    public void toSession_TlsSctDataSizeTooLarge_Failure() throws Exception {
+        assertInvalidSession(getType3().setTlsSctDataLength(931148).build());
+    }
+
+    @Test
+    public void toSession_Type2OcspDataEmpty_Success() throws Exception {
+        assertValidSession(getType1().setType(0x02).setOcspDataEmpty().build());
+    }
+
+    @Test
+    public void toSession_Type3TlsSctDataEmpty_Success() throws Exception {
+        assertValidSession(getType2().setType(0x03).setTlsSctDataEmpty().build());
+    }
+
+    @Test
+    public void toSession_Type3OcspAndTlsSctDataEmpty_Success() throws Exception {
+        assertValidSession(
+                getType1().setType(0x03).setOcspDataEmpty().setTlsSctDataEmpty().build());
+    }
+
+    private static void assertTrailingDataFails(byte[] validSession) {
+        byte[] invalidSession = new byte[validSession.length + 1];
+        System.arraycopy(validSession, 0, invalidSession, 0, validSession.length);
+        assertInvalidSession(invalidSession);
+    }
+
+    @Test
+    public void toSession_Type1TrailingData_Failure() throws Exception {
+        assertTrailingDataFails(getType1().build());
+    }
+
+    @Test
+    public void toSession_Type2TrailingData_Failure() throws Exception {
+        assertTrailingDataFails(getType2().build());
+    }
+
+    @Test
+    public void toSession_Type3TrailingData_Failure() throws Exception {
+        assertTrailingDataFails(getType3().build());
+    }
+
+    @Test
+    public void test_reserializableFromByteArray_roundTrip_type1() throws Exception {
+        // Converting OPEN_SSL (type 1) -> OPEN_SSL_WITH_TLS_SCT (type 3) adds
+        // eight zero-bytes:
+        //  1.) 4 bytes for int32 value 0 == countOcspResponses
+        //  2.) 4 bytes for int32 value 0 == tlsSctDataLength
+        // since OPEN_SSL (type 1) cannot contain OSCP or TLS SCT data.
+        check_reserializableFromByteArray_roundTrip(getType1().build(), new byte[8]);
+    }
+
+    @Test
+    public void test_reserializableFromByteArray_roundTrip_type2() throws Exception {
+        // Converting OPEN_SSL_WITH_OCSP (type 2) -> OPEN_SSL_WITH_TLS_SCT (type 3) adds
+        // four zero-bytes for int32 value 0 == tlsSctDataLength
+        // since OPEN_SSL_WITH_OCSP (type 2) cannot contain TLS SCT data.
+        check_reserializableFromByteArray_roundTrip(getType2().build(), new byte[4]);
+    }
+
+    @Test
+    public void test_reserializableFromByteArray_roundTrip_type3() throws Exception {
+        check_reserializableFromByteArray_roundTrip(getType3().build(), new byte[0]);
+    }
+
+    private static void check_reserializableFromByteArray_roundTrip(
+            byte[] data, byte[] expectedTrailingBytesAfterReserialization) throws Exception {
+        SslSessionWrapper session =
+                SslSessionWrapper.newInstance(null, data, "www.example.com", 12345);
+        byte[] sessionBytes = session.toBytes();
+
+        SslSessionWrapper session2 =
+                SslSessionWrapper.newInstance(null, sessionBytes, "www.example.com", 12345);
+        byte[] sessionBytes2 = session2.toBytes();
+
+        assertSSLSessionEquals(session, session2);
+        assertByteArrayEquals(sessionBytes, sessionBytes2);
+
+        assertEquals("www.example.com", session.getPeerHost());
+        assertEquals(12345, session.getPeerPort());
+        assertTrue(sessionBytes.length >= data.length);
+
+        byte[] expectedReserializedData = concat(data, expectedTrailingBytesAfterReserialization);
+        // AbstractSessionContext.toBytes() always writes type 3 == OPEN_SSL_WITH_TLS_SCT
+        expectedReserializedData[3] = 3;
+        assertByteArrayEquals(expectedReserializedData, sessionBytes);
+    }
+
+    private static byte[] concat(byte[] a, byte[] b) {
+        byte[] result = new byte[a.length + b.length];
+        System.arraycopy(a, 0, result, 0, a.length);
+        System.arraycopy(b, 0, result, a.length, b.length);
+        return result;
+    }
+
+    private static void assertSSLSessionEquals(SslSessionWrapper a, SslSessionWrapper b)
+            throws Exception {
+        assertEquals(a.getCipherSuite(), b.getCipherSuite());
+        assertByteArrayEquals(a.getId(), b.getId());
+        assertEquals(a.getPeerHost(), b.getPeerHost());
+        assertEquals(a.getPeerPort(), b.getPeerPort());
+        assertEquals(a.getProtocol(), b.getProtocol());
+    }
+
+    private static void assertByteArrayEquals(byte[] expected, byte[] actual) {
+        // If running on OpenJDK 8+, could use java.util.Base64 for better failure messages:
+        // assertEquals(Base64.encode(expected), Base64.encode(actual));
+        assertTrue("Expected " + Arrays.toString(expected) + ", got " + Arrays.toString(actual),
+                Arrays.equals(expected, actual));
+    }
+}
diff --git a/platform/build.gradle b/platform/build.gradle
index 78c8f1b..242012e 100644
--- a/platform/build.gradle
+++ b/platform/build.gradle
@@ -92,7 +92,12 @@
         compile project(path: ':conscrypt-openjdk', configuration: 'platform')
         publicApiDocs project(':conscrypt-api-doclet')
         androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
-            exclude group: 'com.android.support', module: 'support-annotations'
+            exclude module: 'support-annotations'
+            exclude module: 'support-v4'
+            exclude module: 'support-v13'
+            exclude module: 'recyclerview-v7'
+            exclude module: 'appcompat-v7'
+            exclude module: 'design'
         })
         testCompile project(':conscrypt-testing'),
                     libraries.junit
diff --git a/platform/proguard-rules.pro b/platform/proguard-rules.pro
index c3bdd2c..3bc75b2 100644
--- a/platform/proguard-rules.pro
+++ b/platform/proguard-rules.pro
@@ -20,5 +20,7 @@
 -dontwarn dalvik.system.BlockGuard
 -dontwarn dalvik.system.BlockGuard$Policy
 -dontwarn dalvik.system.CloseGuard
+-dontwarn com.android.org.conscrypt.AbstractConscryptSocket
+-dontwarn com.android.org.conscrypt.ConscryptFileDescriptorSocket
 -dontwarn com.android.org.conscrypt.OpenSSLSocketImpl
 -dontwarn org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl
diff --git a/platform/src/main/java/org/conscrypt/InternalUtil.java b/platform/src/main/java/org/conscrypt/InternalUtil.java
index 40937c8..dbc6137 100644
--- a/platform/src/main/java/org/conscrypt/InternalUtil.java
+++ b/platform/src/main/java/org/conscrypt/InternalUtil.java
@@ -30,7 +30,7 @@
 @Internal
 public final class InternalUtil {
     public static PublicKey logKeyToPublicKey(byte[] logKey) throws NoSuchAlgorithmException {
-        return new OpenSSLKey(NativeCrypto.d2i_PUBKEY(logKey)).getPublicKey();
+        return new OpenSSLKey(NativeCrypto.EVP_parse_public_key(logKey)).getPublicKey();
     }
 
     public static PublicKey readPublicKeyPem(InputStream pem) throws InvalidKeyException, NoSuchAlgorithmException {
diff --git a/platform/src/main/java/org/conscrypt/Platform.java b/platform/src/main/java/org/conscrypt/Platform.java
index 1d2942e..d53e69a 100644
--- a/platform/src/main/java/org/conscrypt/Platform.java
+++ b/platform/src/main/java/org/conscrypt/Platform.java
@@ -55,9 +55,7 @@
 import sun.security.x509.AlgorithmId;
 
 final class Platform {
-    private static class NoPreloadHolder {
-        public static final Platform MAPPER = new Platform();
-    }
+    private static class NoPreloadHolder { public static final Platform MAPPER = new Platform(); }
 
     /**
      * Runs all the setup for the platform that only needs to run once.
@@ -69,21 +67,19 @@
     /**
      * Just a placeholder to make sure the class is initialized.
      */
-    private void ping() {
-    }
+    private void ping() {}
 
-    private Platform() {
-    }
+    private Platform() {}
 
     static FileDescriptor getFileDescriptor(Socket s) {
         return s.getFileDescriptor$();
     }
 
-    static FileDescriptor getFileDescriptorFromSSLSocket(OpenSSLSocketImpl openSSLSocketImpl) {
+    static FileDescriptor getFileDescriptorFromSSLSocket(AbstractConscryptSocket socket) {
         try {
             Field f_impl = Socket.class.getDeclaredField("impl");
             f_impl.setAccessible(true);
-            Object socketImpl = f_impl.get(openSSLSocketImpl);
+            Object socketImpl = f_impl.get(socket);
             Field f_fd = SocketImpl.class.getDeclaredField("fd");
             f_fd.setAccessible(true);
             return (FileDescriptor) f_fd.get(socketImpl);
@@ -109,8 +105,8 @@
         }
     }
 
-    static void setSSLParameters(SSLParameters params, SSLParametersImpl impl,
-            OpenSSLSocketImpl socket) {
+    static void setSSLParameters(
+            SSLParameters params, SSLParametersImpl impl, AbstractConscryptSocket socket) {
         impl.setEndpointIdentificationAlgorithm(params.getEndpointIdentificationAlgorithm());
         impl.setUseCipherSuitesOrder(params.getUseCipherSuitesOrder());
         List<SNIServerName> serverNames = params.getServerNames();
@@ -124,25 +120,25 @@
         }
     }
 
-    static void getSSLParameters(SSLParameters params, SSLParametersImpl impl,
-            OpenSSLSocketImpl socket) {
+    static void getSSLParameters(
+            SSLParameters params, SSLParametersImpl impl, AbstractConscryptSocket socket) {
         params.setEndpointIdentificationAlgorithm(impl.getEndpointIdentificationAlgorithm());
         params.setUseCipherSuitesOrder(impl.getUseCipherSuitesOrder());
         if (impl.getUseSni() && AddressUtils.isValidSniHostname(socket.getHostname())) {
-            params.setServerNames(Collections.<SNIServerName> singletonList(
+            params.setServerNames(Collections.<SNIServerName>singletonList(
                     new SNIHostName(socket.getHostname())));
         }
     }
 
     static void setSSLParameters(
-            SSLParameters params, SSLParametersImpl impl, OpenSSLEngineImpl engine) {
+            SSLParameters params, SSLParametersImpl impl, ConscryptEngine engine) {
         impl.setEndpointIdentificationAlgorithm(params.getEndpointIdentificationAlgorithm());
         impl.setUseCipherSuitesOrder(params.getUseCipherSuitesOrder());
         List<SNIServerName> serverNames = params.getServerNames();
         if (serverNames != null) {
             for (SNIServerName serverName : serverNames) {
                 if (serverName.getType() == StandardConstants.SNI_HOST_NAME) {
-                    engine.setSniHostname(((SNIHostName) serverName).getAsciiName());
+                    engine.setHostname(((SNIHostName) serverName).getAsciiName());
                     break;
                 }
             }
@@ -150,12 +146,12 @@
     }
 
     static void getSSLParameters(
-            SSLParameters params, SSLParametersImpl impl, OpenSSLEngineImpl engine) {
+            SSLParameters params, SSLParametersImpl impl, ConscryptEngine engine) {
         params.setEndpointIdentificationAlgorithm(impl.getEndpointIdentificationAlgorithm());
         params.setUseCipherSuitesOrder(impl.getUseCipherSuitesOrder());
-        if (impl.getUseSni() && AddressUtils.isValidSniHostname(engine.getSniHostname())) {
+        if (impl.getUseSni() && AddressUtils.isValidSniHostname(engine.getHostname())) {
             params.setServerNames(Collections.<SNIServerName>singletonList(
-                    new SNIHostName(engine.getSniHostname())));
+                    new SNIHostName(engine.getHostname())));
         }
     }
 
@@ -168,66 +164,64 @@
             Object argumentInstance) throws CertificateException {
         // Use duck-typing to try and call the hostname-aware method if available.
         try {
-            Method method = tm.getClass().getMethod(methodName,
-                    X509Certificate[].class,
-                    String.class,
-                    argumentClass);
+            Method method = tm.getClass().getMethod(
+                    methodName, X509Certificate[].class, String.class, argumentClass);
             method.invoke(tm, chain, authType, argumentInstance);
             return true;
         } catch (NoSuchMethodException | IllegalAccessException ignored) {
         } catch (InvocationTargetException e) {
             if (e.getCause() instanceof CertificateException) {
-                throw (CertificateException) e.getCause();
+                throw(CertificateException) e.getCause();
             }
             throw new RuntimeException(e.getCause());
         }
         return false;
     }
 
-    static void checkClientTrusted(X509TrustManager tm, X509Certificate[] chain,
-            String authType, OpenSSLSocketImpl socket) throws CertificateException {
+    static void checkClientTrusted(X509TrustManager tm, X509Certificate[] chain, String authType,
+            AbstractConscryptSocket socket) throws CertificateException {
         if (tm instanceof X509ExtendedTrustManager) {
             X509ExtendedTrustManager x509etm = (X509ExtendedTrustManager) tm;
             x509etm.checkClientTrusted(chain, authType, socket);
         } else if (!checkTrusted("checkClientTrusted", tm, chain, authType, Socket.class, socket)
                 && !checkTrusted("checkClientTrusted", tm, chain, authType, String.class,
-                                 socket.getHandshakeSession().getPeerHost())) {
+                           socket.getHandshakeSession().getPeerHost())) {
             tm.checkClientTrusted(chain, authType);
         }
     }
 
-    static void checkServerTrusted(X509TrustManager tm, X509Certificate[] chain,
-            String authType, OpenSSLSocketImpl socket) throws CertificateException {
+    static void checkServerTrusted(X509TrustManager tm, X509Certificate[] chain, String authType,
+            AbstractConscryptSocket socket) throws CertificateException {
         if (tm instanceof X509ExtendedTrustManager) {
             X509ExtendedTrustManager x509etm = (X509ExtendedTrustManager) tm;
             x509etm.checkServerTrusted(chain, authType, socket);
         } else if (!checkTrusted("checkServerTrusted", tm, chain, authType, Socket.class, socket)
                 && !checkTrusted("checkServerTrusted", tm, chain, authType, String.class,
-                                 socket.getHandshakeSession().getPeerHost())) {
+                           socket.getHandshakeSession().getPeerHost())) {
             tm.checkServerTrusted(chain, authType);
         }
     }
 
-    static void checkClientTrusted(X509TrustManager tm, X509Certificate[] chain,
-            String authType, OpenSSLEngineImpl engine) throws CertificateException {
+    static void checkClientTrusted(X509TrustManager tm, X509Certificate[] chain, String authType,
+            ConscryptEngine engine) throws CertificateException {
         if (tm instanceof X509ExtendedTrustManager) {
             X509ExtendedTrustManager x509etm = (X509ExtendedTrustManager) tm;
             x509etm.checkClientTrusted(chain, authType, engine);
         } else if (!checkTrusted("checkClientTrusted", tm, chain, authType, SSLEngine.class, engine)
                 && !checkTrusted("checkClientTrusted", tm, chain, authType, String.class,
-                                 engine.getHandshakeSession().getPeerHost())) {
+                           engine.getHandshakeSession().getPeerHost())) {
             tm.checkClientTrusted(chain, authType);
         }
     }
 
-    static void checkServerTrusted(X509TrustManager tm, X509Certificate[] chain,
-            String authType, OpenSSLEngineImpl engine) throws CertificateException {
+    static void checkServerTrusted(X509TrustManager tm, X509Certificate[] chain, String authType,
+            ConscryptEngine engine) throws CertificateException {
         if (tm instanceof X509ExtendedTrustManager) {
             X509ExtendedTrustManager x509etm = (X509ExtendedTrustManager) tm;
             x509etm.checkServerTrusted(chain, authType, engine);
         } else if (!checkTrusted("checkServerTrusted", tm, chain, authType, SSLEngine.class, engine)
                 && !checkTrusted("checkServerTrusted", tm, chain, authType, String.class,
-                                 engine.getHandshakeSession().getPeerHost())) {
+                           engine.getHandshakeSession().getPeerHost())) {
             tm.checkServerTrusted(chain, authType);
         }
     }
@@ -252,10 +246,10 @@
 
             Class eventLogClass = Class.forName("android.util.EventLog");
             Object eventLogInstance = eventLogClass.newInstance();
-            Method writeEventMethod = eventLogClass.getMethod("writeEvent",
-                    new Class[] { Integer.TYPE, Object[].class });
+            Method writeEventMethod = eventLogClass.getMethod(
+                    "writeEvent", new Class[] {Integer.TYPE, Object[].class});
             writeEventMethod.invoke(eventLogInstance, 0x534e4554 /* SNET */,
-                    new Object[] { "conscrypt", uid, message });
+                    new Object[] {"conscrypt", uid, message});
         } catch (Exception e) {
             // Do not log and fail silently
         }
@@ -340,14 +334,15 @@
      * Pre-Java 8 backward compatibility.
      */
 
-    static SSLSession wrapSSLSession(AbstractOpenSSLSession sslSession) {
-        return new OpenSSLExtendedSessionImpl(sslSession);
+    static SSLSession wrapSSLSession(ActiveSession sslSession) {
+        return new DelegatingExtendedSSLSession(sslSession);
     }
 
     static SSLSession unwrapSSLSession(SSLSession sslSession) {
-        if (sslSession instanceof OpenSSLExtendedSessionImpl) {
-            return ((OpenSSLExtendedSessionImpl) sslSession).getDelegate();
+        if (sslSession instanceof DelegatingExtendedSSLSession) {
+            return ((DelegatingExtendedSSLSession) sslSession).getDelegate();
         }
+
         return sslSession;
     }
 
@@ -360,7 +355,7 @@
     }
 
     static boolean isCTVerificationRequired(String hostname) {
-        return NetworkSecurityPolicy.getInstance()
-                .isCertificateTransparencyVerificationRequired(hostname);
+        return NetworkSecurityPolicy.getInstance().isCertificateTransparencyVerificationRequired(
+                hostname);
     }
 }
diff --git a/platform/src/main/java/org/conscrypt/TrustManagerImpl.java b/platform/src/main/java/org/conscrypt/TrustManagerImpl.java
index 010c75a..95f7f48 100644
--- a/platform/src/main/java/org/conscrypt/TrustManagerImpl.java
+++ b/platform/src/main/java/org/conscrypt/TrustManagerImpl.java
@@ -420,8 +420,8 @@
 
     private byte[] getOcspDataFromSession(SSLSession session) {
         List<byte[]> ocspResponses = null;
-        if (session instanceof AbstractOpenSSLSession) {
-            AbstractOpenSSLSession opensslSession = (AbstractOpenSSLSession) session;
+        if (session instanceof ActiveSession) {
+            ActiveSession opensslSession = (ActiveSession) session;
             ocspResponses = opensslSession.getStatusResponses();
         } else {
             Method m_getResponses;
@@ -447,14 +447,14 @@
     }
 
     private byte[] getTlsSctDataFromSession(SSLSession session) {
-        if (session instanceof AbstractOpenSSLSession) {
-            AbstractOpenSSLSession opensslSession = (AbstractOpenSSLSession) session;
-            return opensslSession.getTlsSctData();
+        if (session instanceof ActiveSession) {
+            ActiveSession opensslSession = (ActiveSession) session;
+            return opensslSession.getPeerSignedCertificateTimestamp();
         }
 
         byte[] data = null;
         try {
-            Method m_getTlsSctData = session.getClass().getDeclaredMethod("getTlsSctData");
+            Method m_getTlsSctData = session.getClass().getDeclaredMethod("getPeerSignedCertificateTimestamp");
             m_getTlsSctData.setAccessible(true);
             Object rawData = m_getTlsSctData.invoke(session);
             if (rawData instanceof byte[]) {
@@ -789,7 +789,7 @@
 
 
     /**
-     * Comparator for sorting {@link TrustAnchor}s using a {@link CertificateComparator}.
+     * Comparator for sorting {@link TrustAnchor}s using a {@link CertificatePriorityComparator}.
      */
     private static class TrustAnchorComparator implements Comparator<TrustAnchor> {
         private static final CertificatePriorityComparator CERT_COMPARATOR =
diff --git a/settings.gradle b/settings.gradle
index f2cadcb..69f320a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -3,11 +3,12 @@
 include ":conscrypt-android-platform"
 include ":conscrypt-android-stub"
 include ":conscrypt-api-doclet"
+include ":conscrypt-benchmark-base"
 include ":conscrypt-benchmark-graphs"
+include ":conscrypt-benchmark-jmh"
 include ":conscrypt-constants"
 include ":conscrypt-libcore-stub"
 include ":conscrypt-openjdk"
-include ":conscrypt-openjdk-benchmarks"
 include ":conscrypt-openjdk-integ-tests"
 include ":conscrypt-openjdk-uber"
 include ":conscrypt-testing"
@@ -16,11 +17,12 @@
 project(':conscrypt-android-platform').projectDir = "$rootDir/platform" as File
 project(':conscrypt-android-stub').projectDir = "$rootDir/android-stub" as File
 project(':conscrypt-api-doclet').projectDir = "$rootDir/api-doclet" as File
+project(':conscrypt-benchmark-base').projectDir = "$rootDir/benchmark-base" as File
 project(':conscrypt-benchmark-graphs').projectDir = "$rootDir/benchmark-graphs" as File
+project(':conscrypt-benchmark-jmh').projectDir = "$rootDir/benchmark-jmh" as File
 project(':conscrypt-constants').projectDir = "$rootDir/constants" as File
 project(':conscrypt-libcore-stub').projectDir = "$rootDir/libcore-stub" as File
 project(':conscrypt-openjdk').projectDir = "$rootDir/openjdk" as File
-project(':conscrypt-openjdk-benchmarks').projectDir = "$rootDir/openjdk-benchmarks" as File
 project(':conscrypt-openjdk-integ-tests').projectDir = "$rootDir/openjdk-integ-tests" as File
 project(':conscrypt-openjdk-uber').projectDir = "$rootDir/openjdk-uber" as File
 project(':conscrypt-testing').projectDir = "$rootDir/testing" as File
diff --git a/testing/build.gradle b/testing/build.gradle
index a4fd599..12d08be 100644
--- a/testing/build.gradle
+++ b/testing/build.gradle
@@ -13,6 +13,8 @@
     compileOnly project(':conscrypt-constants')
 
     compile project(':conscrypt-libcore-stub'),
+            libraries.bouncycastle_apis,
+            libraries.bouncycastle_provider,
             libraries.netty_handler
 }
 
diff --git a/testing/src/main/java/libcore/java/security/TestKeyStore.java b/testing/src/main/java/libcore/java/security/TestKeyStore.java
index d6f4ad8..060a023 100644
--- a/testing/src/main/java/libcore/java/security/TestKeyStore.java
+++ b/testing/src/main/java/libcore/java/security/TestKeyStore.java
@@ -159,6 +159,7 @@
 
     private static final byte[] LOCAL_HOST_ADDRESS = {127, 0, 0, 1};
     private static final String LOCAL_HOST_NAME = "localhost";
+    private static final String LOCAL_HOST_NAME_IPV6 = "ip6-localhost";
 
     public final KeyStore keyStore;
     public final char[] storePassword;
@@ -376,7 +377,6 @@
         private BigInteger certificateSerialNumber = null;
 
         public Builder() {
-            subject = localhost();
         }
 
         /**
@@ -448,6 +448,11 @@
             return this;
         }
 
+        public Builder addSubjectAltNameDnsName(String dnsName) {
+            return addSubjectAltName(
+                    new GeneralName(GeneralName.dNSName, dnsName));
+        }
+
         public Builder addSubjectAltNameIpAddress(byte[] ipAddress) {
             return addSubjectAltName(
                     new GeneralName(GeneralName.iPAddress, new DEROctetString(ipAddress)));
@@ -554,6 +559,13 @@
                 caCertChain = (X509Certificate[]) signer.getCertificateChain();
             }
 
+            // Default to localhost if nothing was specified.
+            if (subject == null) {
+                subject = localhost();
+                addSubjectAltNameDnsName(LOCAL_HOST_NAME);
+                addSubjectAltNameDnsName(LOCAL_HOST_NAME_IPV6);
+            }
+
             final PrivateKey privateKey;
             final PublicKey publicKey;
             X509Certificate x509c;
@@ -709,9 +721,9 @@
             x509cg.addExtension(
                     Extension.extendedKeyUsage, critical, new ExtendedKeyUsage(keyPurposeId));
         }
-        for (GeneralName subjectAltName : subjectAltNames) {
+        if (!subjectAltNames.isEmpty()) {
             x509cg.addExtension(Extension.subjectAlternativeName, false,
-                    new GeneralNames(subjectAltName).getEncoded());
+                    new GeneralNames(subjectAltNames.toArray(new GeneralName[0])).getEncoded());
         }
         if (!permittedNameConstraints.isEmpty() || !excludedNameConstraints.isEmpty()) {
             x509cg.addExtension(Extension.nameConstraints, true,
diff --git a/testing/src/main/java/libcore/javax/net/ssl/TestSSLEnginePair.java b/testing/src/main/java/libcore/javax/net/ssl/TestSSLEnginePair.java
index 1146c69..69bef89 100644
--- a/testing/src/main/java/libcore/javax/net/ssl/TestSSLEnginePair.java
+++ b/testing/src/main/java/libcore/javax/net/ssl/TestSSLEnginePair.java
@@ -35,6 +35,7 @@
     public final TestSSLContext c;
     public final SSLEngine server;
     public final SSLEngine client;
+
     private TestSSLEnginePair(TestSSLContext c,
             SSLEngine server,
             SSLEngine client) {
@@ -42,23 +43,29 @@
         this.server = server;
         this.client = client;
     }
+
     public static TestSSLEnginePair create() throws IOException {
         return create(null);
     }
+
     public static TestSSLEnginePair create(Hooks hooks) throws IOException {
         return create(TestSSLContext.create(), hooks);
     }
+
     public static TestSSLEnginePair create(TestSSLContext c, Hooks hooks) throws IOException {
         return create(c, hooks, null);
     }
+
     public static TestSSLEnginePair create(TestSSLContext c, Hooks hooks, boolean[] finished)
             throws IOException {
         SSLEngine[] engines = connect(c, hooks, finished);
         return new TestSSLEnginePair(c, engines[0], engines[1]);
     }
+
     public static SSLEngine[] connect(TestSSLContext c, Hooks hooks) throws IOException {
         return connect(c, hooks, null);
     }
+
     /**
      * Create a new connected server/client engine pair within a
      * existing SSLContext. Optionally specify clientCipherSuites to
@@ -72,15 +79,20 @@
         if (hooks == null) {
             hooks = new Hooks();
         }
+
         // FINISHED state should be returned only once.
         boolean[] clientFinished = new boolean[1];
         boolean[] serverFinished = new boolean[1];
+
         SSLSession session = c.clientContext.createSSLEngine().getSession();
+
         int packetBufferSize = session.getPacketBufferSize();
         ByteBuffer clientToServer = ByteBuffer.allocate(packetBufferSize);
         ByteBuffer serverToClient = ByteBuffer.allocate(packetBufferSize);
+
         int applicationBufferSize = session.getApplicationBufferSize();
         ByteBuffer scratch = ByteBuffer.allocate(applicationBufferSize);
+
         SSLEngine client = c.clientContext.createSSLEngine(c.host.getHostName(), c.port);
         SSLEngine server = c.serverContext.createSSLEngine();
         client.setUseClientMode(true);
@@ -88,12 +100,14 @@
         hooks.beforeBeginHandshake(client, server);
         client.beginHandshake();
         server.beginHandshake();
+
         while (true) {
             boolean clientDone = client.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING;
             boolean serverDone = server.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING;
             if (clientDone && serverDone) {
                 break;
             }
+
             boolean progress = false;
             if (!clientDone) {
                 progress = handshakeCompleted(client,
@@ -113,6 +127,7 @@
                 break;
             }
         }
+
         if (finished != null) {
             assertEquals(2, finished.length);
             finished[0] = clientFinished[0];
@@ -120,13 +135,16 @@
         }
         return new SSLEngine[] { server, client };
     }
+
     public static class Hooks {
         void beforeBeginHandshake(SSLEngine client, SSLEngine server) {}
     }
+
     @Override
     public void close() throws SSLException {
         close(new SSLEngine[] { client, server });
     }
+
     public static void close(SSLEngine[] engines) {
         try {
             for (SSLEngine engine : engines) {
@@ -139,6 +157,7 @@
             throw new RuntimeException(e);
         }
     }
+
     private static boolean handshakeCompleted(SSLEngine engine,
             ByteBuffer output,
             ByteBuffer input,
@@ -147,8 +166,10 @@
         try {
             // make the other side's output into our input
             input.flip();
+
             HandshakeStatus status = engine.getHandshakeStatus();
             switch (status) {
+
                 case NEED_TASK: {
                     boolean progress = false;
                     while (true) {
@@ -160,6 +181,7 @@
                         progress = true;
                     }
                 }
+
                 case NEED_UNWRAP: {
                     // avoid underflow
                     if (input.remaining() == 0) {
@@ -174,6 +196,7 @@
                     assertFinishedOnce(finished, unwrapResult);
                     return true;
                 }
+
                 case NEED_WRAP: {
                     // avoid possible overflow
                     if (output.remaining() != output.capacity()) {
@@ -191,6 +214,7 @@
                     assertFinishedOnce(finished, wrapResult);
                     return true;
                 }
+
                 case NOT_HANDSHAKING:
                     // should have been checked by caller before calling
                 case FINISHED:
@@ -204,6 +228,7 @@
             input.compact();
         }
     }
+
     private static void assertFinishedOnce(boolean[] finishedOut, SSLEngineResult result) {
         if (result.getHandshakeStatus() == HandshakeStatus.FINISHED) {
             assertFalse("should only return FINISHED once", finishedOut[0]);
diff --git a/testing/src/main/java/libcore/javax/net/ssl/TestSSLSessions.java b/testing/src/main/java/libcore/javax/net/ssl/TestSSLSessions.java
index 34596bc..8f45a2e 100644
--- a/testing/src/main/java/libcore/javax/net/ssl/TestSSLSessions.java
+++ b/testing/src/main/java/libcore/javax/net/ssl/TestSSLSessions.java
@@ -27,19 +27,23 @@
      * An invalid session that is not connected
      */
     public final SSLSession invalid;
+
     /**
      * The server side of a connected session
      */
     public final SSLSession server;
+
     /**
      * The client side of a connected session
      */
     public final SSLSession client;
+
     /**
      * The associated SSLSocketTest.Helper that is the source of
      * the client and server SSLSessions.
      */
     public final TestSSLSocketPair s;
+
     private TestSSLSessions(SSLSession invalid,
             SSLSession server,
             SSLSession client,
@@ -49,15 +53,17 @@
         this.client = client;
         this.s = s;
     }
+
     public void close() {
         s.close();
     }
-    public static final TestSSLSessions create() {
+
+    public static TestSSLSessions create() {
         try {
             SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
             SSLSocket ssl = (SSLSocket) sf.createSocket();
             SSLSession invalid = ssl.getSession();
-            TestSSLSocketPair s = TestSSLSocketPair.create();
+            TestSSLSocketPair s = TestSSLSocketPair.create().connect();
             return new TestSSLSessions(invalid, s.server.getSession(), s.client.getSession(), s);
         } catch (Exception e) {
             throw new RuntimeException(e);
diff --git a/testing/src/main/java/libcore/javax/net/ssl/TestSSLSocketPair.java b/testing/src/main/java/libcore/javax/net/ssl/TestSSLSocketPair.java
index 66e7ac7..c6d59fe 100644
--- a/testing/src/main/java/libcore/javax/net/ssl/TestSSLSocketPair.java
+++ b/testing/src/main/java/libcore/javax/net/ssl/TestSSLSocketPair.java
@@ -15,6 +15,7 @@
  */
 package libcore.javax.net.ssl;
 
+import java.io.IOException;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -31,9 +32,7 @@
     public final TestSSLContext c;
     public final SSLSocket server;
     public final SSLSocket client;
-    private TestSSLSocketPair (TestSSLContext c,
-            SSLSocket server,
-            SSLSocket client) {
+    private TestSSLSocketPair(TestSSLContext c, SSLSocket server, SSLSocket client) {
         this.c = c;
         this.server = server;
         this.client = client;
@@ -47,14 +46,15 @@
             throw new RuntimeException(e);
         }
     }
-    /**
-     * based on test_SSLSocket_startHandshake
-     */
-    public static TestSSLSocketPair create () {
-        TestSSLContext c = TestSSLContext.create();
-        SSLSocket[] sockets = connect(c, null, null);
-        return new TestSSLSocketPair(c, sockets[0], sockets[1]);
+
+    public SSLSocket[] sockets() {
+        return new SSLSocket[] {server, client};
     }
+
+    public TestSSLSocketPair connect() {
+        return connect(null, null);
+    }
+
     /**
      * Create a new connected server/client socket pair within a
      * existing SSLContext. Optionally specify clientCipherSuites to
@@ -62,13 +62,9 @@
      * caching. Optionally specify serverCipherSuites for testing
      * cipher suite negotiation.
      */
-    public static SSLSocket[] connect (final TestSSLContext context,
-            final String[] clientCipherSuites,
-            final String[] serverCipherSuites) {
+    public TestSSLSocketPair connect(
+            final String[] clientCipherSuites, final String[] serverCipherSuites) {
         try {
-            final SSLSocket client = (SSLSocket)
-                    context.clientContext.getSocketFactory().createSocket(context.host, context.port);
-            final SSLSocket server = (SSLSocket) context.serverSocket.accept();
             ExecutorService executor = Executors.newFixedThreadPool(2);
             Future<Void> s = executor.submit(new Callable<Void>() {
                 @Override
@@ -115,11 +111,29 @@
             if (clientException != null) {
                 throw clientException;
             }
-            return new SSLSocket[] { server, client };
+            return this;
         } catch (RuntimeException e) {
             throw e;
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
     }
+
+    public static TestSSLSocketPair create() {
+        return create(TestSSLContext.create());
+    }
+
+    /**
+     * based on test_SSLSocket_startHandshake
+     */
+    public static TestSSLSocketPair create(TestSSLContext context) {
+        try {
+            SSLSocket client = (SSLSocket) context.clientContext.getSocketFactory().createSocket(
+                    context.host, context.port);
+            SSLSocket server = (SSLSocket) context.serverSocket.accept();
+            return new TestSSLSocketPair(context, server, client);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/testing/src/main/java/org/conscrypt/ChannelType.java b/testing/src/main/java/org/conscrypt/ChannelType.java
new file mode 100644
index 0000000..09dd582
--- /dev/null
+++ b/testing/src/main/java/org/conscrypt/ChannelType.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.conscrypt;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * The type of socket to be wrapped by the Conscrypt socket.
+ */
+@SuppressWarnings("unused")
+public enum ChannelType {
+    NONE {
+        @Override
+        SSLSocket newClientSocket(SSLSocketFactory factory, InetAddress address, int port)
+                throws IOException {
+            return clientMode(factory.createSocket(address, port));
+        }
+
+        @Override
+        ServerSocket newServerSocket(SSLServerSocketFactory factory) throws IOException {
+            return factory.createServerSocket(0, 50, InetAddress.getLoopbackAddress());
+        }
+
+        @Override
+        SSLSocket accept(ServerSocket socket, SSLSocketFactory unused) throws IOException {
+            return serverMode(socket.accept());
+        }
+    },
+    NO_CHANNEL {
+        @Override
+        SSLSocket newClientSocket(SSLSocketFactory factory, InetAddress address, int port)
+                throws IOException {
+            Socket wrapped = new Socket(address, port);
+            assertNull(wrapped.getChannel());
+
+            return clientMode(factory.createSocket(wrapped, address.getHostName(), port, true));
+        }
+
+        @Override
+        ServerSocket newServerSocket(SSLServerSocketFactory unused) throws IOException {
+            return ServerSocketFactory.getDefault().createServerSocket(
+                    0, 50, InetAddress.getLoopbackAddress());
+        }
+
+        @Override
+        SSLSocket accept(ServerSocket serverSocket, SSLSocketFactory factory) throws IOException {
+            assertFalse(serverSocket instanceof SSLServerSocket);
+            Socket wrapped = serverSocket.accept();
+            assertNull(wrapped.getChannel());
+
+            return serverMode(factory.createSocket(
+                    wrapped, wrapped.getInetAddress().getHostAddress(), wrapped.getPort(), true));
+        }
+    },
+    CHANNEL {
+        @Override
+        SSLSocket newClientSocket(SSLSocketFactory factory, InetAddress address, int port)
+                throws IOException {
+            Socket wrapped = SocketChannel.open(new InetSocketAddress(address, port)).socket();
+            return clientMode(factory.createSocket(wrapped, address.getHostName(), port, true));
+        }
+
+        @Override
+        ServerSocket newServerSocket(SSLServerSocketFactory unused) throws IOException {
+            return ServerSocketChannel.open()
+                    .bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0))
+                    .socket();
+        }
+
+        @Override
+        SSLSocket accept(ServerSocket serverSocket, SSLSocketFactory factory) throws IOException {
+            assertFalse(serverSocket instanceof SSLServerSocket);
+            ServerSocketChannel serverChannel = serverSocket.getChannel();
+
+            // Just loop until the accept completes.
+            SocketChannel channel;
+            do {
+                channel = serverChannel.accept();
+            } while (channel == null);
+
+            Socket wrapped = channel.socket();
+            return serverMode(factory.createSocket(
+                    wrapped, wrapped.getInetAddress().getHostAddress(), wrapped.getPort(), true));
+        }
+    };
+
+    abstract SSLSocket newClientSocket(SSLSocketFactory factory, InetAddress address, int port)
+            throws IOException;
+    abstract ServerSocket newServerSocket(SSLServerSocketFactory factory) throws IOException;
+    abstract SSLSocket accept(ServerSocket socket, SSLSocketFactory factory) throws IOException;
+
+    private static SSLSocket clientMode(Socket socket) {
+        SSLSocket sslSocket = (SSLSocket) socket;
+        sslSocket.setUseClientMode(true);
+        return sslSocket;
+    }
+
+    private static SSLSocket serverMode(Socket socket) {
+        SSLSocket sslSocket = (SSLSocket) socket;
+        sslSocket.setUseClientMode(false);
+        return sslSocket;
+    }
+}
diff --git a/testing/src/main/java/org/conscrypt/TestUtils.java b/testing/src/main/java/org/conscrypt/TestUtils.java
index ef14e4b..eaa4f2e 100644
--- a/testing/src/main/java/org/conscrypt/TestUtils.java
+++ b/testing/src/main/java/org/conscrypt/TestUtils.java
@@ -47,11 +47,14 @@
     private static final Provider JDK_PROVIDER = getDefaultTlsProvider();
     private static final byte[] CHARS =
             "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".getBytes(UTF_8);
+    private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0);
 
     public static final String PROTOCOL_TLS_V1_2 = "TLSv1.2";
     public static final String PROVIDER_PROPERTY = "SSLContext.TLSv1.2";
     public static final String LOCALHOST = "localhost";
 
+    static final String TEST_CIPHER = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256";
+
     private TestUtils() {}
 
     private static Provider getDefaultTlsProvider() {
@@ -63,6 +66,10 @@
         throw new RuntimeException("Unable to find a default provider for " + PROVIDER_PROPERTY);
     }
 
+    static Provider getJdkProvider() {
+        return JDK_PROVIDER;
+    }
+
     public static Provider getConscryptProvider() {
         try {
             return (Provider) conscryptClass("OpenSSLProvider")
@@ -127,32 +134,36 @@
         return getServerSocketFactory(JDK_PROVIDER);
     }
 
-    public static SSLSocketFactory getConscryptSocketFactory(boolean useEngineSocket) {
+    static SSLSocketFactory setUseEngineSocket(SSLSocketFactory conscryptFactory, boolean useEngineSocket) {
         try {
             Class<?> clazz = conscryptClass("Conscrypt$SocketFactories");
             Method method = clazz.getMethod("setUseEngineSocket", SSLSocketFactory.class, boolean.class);
-
-            SSLSocketFactory socketFactory = getSocketFactory(getConscryptProvider());
-            method.invoke(null, socketFactory, useEngineSocket);
-            return socketFactory;
+            method.invoke(null, conscryptFactory, useEngineSocket);
+            return conscryptFactory;
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
     }
 
-    public static SSLServerSocketFactory getConscryptServerSocketFactory(boolean useEngineSocket) {
+    static SSLServerSocketFactory setUseEngineSocket(SSLServerSocketFactory conscryptFactory, boolean useEngineSocket) {
         try {
             Class<?> clazz = conscryptClass("Conscrypt$ServerSocketFactories");
             Method method = clazz.getMethod("setUseEngineSocket", SSLServerSocketFactory.class, boolean.class);
-
-            SSLServerSocketFactory socketFactory = getServerSocketFactory(getConscryptProvider());
-            method.invoke(null, socketFactory, useEngineSocket);
-            return socketFactory;
+            method.invoke(null, conscryptFactory, useEngineSocket);
+            return conscryptFactory;
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
     }
 
+    public static SSLSocketFactory getConscryptSocketFactory(boolean useEngineSocket) {
+        return setUseEngineSocket(getSocketFactory(getConscryptProvider()), useEngineSocket);
+    }
+
+    public static SSLServerSocketFactory getConscryptServerSocketFactory(boolean useEngineSocket) {
+        return setUseEngineSocket(getServerSocketFactory(getConscryptProvider()), useEngineSocket);
+    }
+
     private static SSLSocketFactory getSocketFactory(Provider provider) {
         SSLContext clientContext = initClientSslContext(newContext(provider));
         return clientContext.getSocketFactory();
@@ -205,13 +216,23 @@
     /**
      * Initializes the given engine with the cipher and client mode.
      */
-    public static SSLEngine initEngine(SSLEngine engine, String cipher, boolean client) {
+    static SSLEngine initEngine(SSLEngine engine, String cipher, boolean client) {
         engine.setEnabledProtocols(getProtocols());
         engine.setEnabledCipherSuites(new String[] {cipher});
         engine.setUseClientMode(client);
         return engine;
     }
 
+    static SSLContext newClientSslContext(Provider provider) {
+        SSLContext context = newContext(provider);
+        return initClientSslContext(context);
+    }
+
+    static SSLContext newServerSslContext(Provider provider) {
+        SSLContext context = newContext(provider);
+        return initServerSslContext(context);
+    }
+
     /**
      * Initializes the given client-side {@code context} with a default cert.
      */
@@ -241,21 +262,12 @@
     /**
      * Performs the intial TLS handshake between the two {@link SSLEngine} instances.
      */
-    public static void doEngineHandshake(SSLEngine clientEngine, SSLEngine serverEngine)
-            throws SSLException {
-        ByteBuffer cTOs = ByteBuffer.allocate(clientEngine.getSession().getPacketBufferSize());
-        ByteBuffer sTOc = ByteBuffer.allocate(serverEngine.getSession().getPacketBufferSize());
-
-        ByteBuffer serverAppReadBuffer =
-                ByteBuffer.allocate(serverEngine.getSession().getApplicationBufferSize());
-        ByteBuffer clientAppReadBuffer =
-                ByteBuffer.allocate(clientEngine.getSession().getApplicationBufferSize());
-
+    public static void doEngineHandshake(SSLEngine clientEngine, SSLEngine serverEngine,
+            ByteBuffer clientAppBuffer, ByteBuffer clientPacketBuffer, ByteBuffer serverAppBuffer,
+            ByteBuffer serverPacketBuffer) throws SSLException {
         clientEngine.beginHandshake();
         serverEngine.beginHandshake();
 
-        ByteBuffer empty = ByteBuffer.allocate(0);
-
         SSLEngineResult clientResult;
         SSLEngineResult serverResult;
 
@@ -263,22 +275,22 @@
         boolean serverHandshakeFinished = false;
 
         do {
-            int cTOsPos = cTOs.position();
-            int sTOcPos = sTOc.position();
+            int cTOsPos = clientPacketBuffer.position();
+            int sTOcPos = serverPacketBuffer.position();
 
-            clientResult = clientEngine.wrap(empty, cTOs);
+            clientResult = clientEngine.wrap(EMPTY_BUFFER, clientPacketBuffer);
             runDelegatedTasks(clientResult, clientEngine);
-            serverResult = serverEngine.wrap(empty, sTOc);
+            serverResult = serverEngine.wrap(EMPTY_BUFFER, serverPacketBuffer);
             runDelegatedTasks(serverResult, serverEngine);
 
             // Verify that the consumed and produced number match what is in the buffers now.
-            assertEquals(empty.remaining(), clientResult.bytesConsumed());
-            assertEquals(empty.remaining(), serverResult.bytesConsumed());
-            assertEquals(cTOs.position() - cTOsPos, clientResult.bytesProduced());
-            assertEquals(sTOc.position() - sTOcPos, serverResult.bytesProduced());
+            assertEquals(0, clientResult.bytesConsumed());
+            assertEquals(0, serverResult.bytesConsumed());
+            assertEquals(clientPacketBuffer.position() - cTOsPos, clientResult.bytesProduced());
+            assertEquals(serverPacketBuffer.position() - sTOcPos, serverResult.bytesProduced());
 
-            cTOs.flip();
-            sTOc.flip();
+            clientPacketBuffer.flip();
+            serverPacketBuffer.flip();
 
             // Verify that we only had one SSLEngineResult.HandshakeStatus.FINISHED
             if (isHandshakeFinished(clientResult)) {
@@ -290,27 +302,27 @@
                 serverHandshakeFinished = true;
             }
 
-            cTOsPos = cTOs.position();
-            sTOcPos = sTOc.position();
+            cTOsPos = clientPacketBuffer.position();
+            sTOcPos = serverPacketBuffer.position();
 
-            int clientAppReadBufferPos = clientAppReadBuffer.position();
-            int serverAppReadBufferPos = serverAppReadBuffer.position();
+            int clientAppReadBufferPos = clientAppBuffer.position();
+            int serverAppReadBufferPos = serverAppBuffer.position();
 
-            clientResult = clientEngine.unwrap(sTOc, clientAppReadBuffer);
+            clientResult = clientEngine.unwrap(serverPacketBuffer, clientAppBuffer);
             runDelegatedTasks(clientResult, clientEngine);
-            serverResult = serverEngine.unwrap(cTOs, serverAppReadBuffer);
+            serverResult = serverEngine.unwrap(clientPacketBuffer, serverAppBuffer);
             runDelegatedTasks(serverResult, serverEngine);
 
             // Verify that the consumed and produced number match what is in the buffers now.
-            assertEquals(sTOc.position() - sTOcPos, clientResult.bytesConsumed());
-            assertEquals(cTOs.position() - cTOsPos, serverResult.bytesConsumed());
-            assertEquals(clientAppReadBuffer.position() - clientAppReadBufferPos,
+            assertEquals(serverPacketBuffer.position() - sTOcPos, clientResult.bytesConsumed());
+            assertEquals(clientPacketBuffer.position() - cTOsPos, serverResult.bytesConsumed());
+            assertEquals(clientAppBuffer.position() - clientAppReadBufferPos,
                     clientResult.bytesProduced());
-            assertEquals(serverAppReadBuffer.position() - serverAppReadBufferPos,
+            assertEquals(serverAppBuffer.position() - serverAppReadBufferPos,
                     serverResult.bytesProduced());
 
-            cTOs.compact();
-            sTOc.compact();
+            clientPacketBuffer.compact();
+            serverPacketBuffer.compact();
 
             // Verify that we only had one SSLEngineResult.HandshakeStatus.FINISHED
             if (isHandshakeFinished(clientResult)) {