Add public API for SSLEngines and SSLSockets

Bug: 110403421
Test: cts -m CtsLibcoreTestCases -t android.net.ssl
Change-Id: If7982975ed125d0e4a1ab6e79afbe0835b8807c4
diff --git a/Android.bp b/Android.bp
index 552a686..94050cc 100644
--- a/Android.bp
+++ b/Android.bp
@@ -191,6 +191,11 @@
     ],
 }
 
+filegroup {
+    name: "conscrypt_public_api_files",
+    srcs: ["publicapi/src/main/java/**/*.java"]
+}
+
 // Create the conscrypt library from the source produced by the srcgen/generate_android_src.sh
 // script.
 java_library {
@@ -198,7 +203,10 @@
     installable: true,
     hostdex: true,
 
-    srcs: [":conscrypt_java_files"],
+    srcs: [
+        ":conscrypt_java_files",
+        ":conscrypt_public_api_files",
+    ],
 
     // Conscrypt can be updated independently from the other core libraries so it must only depend
     // on public SDK and intra-core APIs.
@@ -322,6 +330,7 @@
         "repackaged/common/src/test/java/**/*.java",
         "repackaged/openjdk-integ-tests/src/test/java/**/*.java",
         "repackaged/testing/src/main/java/**/*.java",
+        "publicapi/src/test/java/**/*.java",
     ],
     java_resource_dirs: [
         // Resource directories do not need repackaging.
diff --git a/README.android b/README.android
new file mode 100644
index 0000000..f20bc2a
--- /dev/null
+++ b/README.android
@@ -0,0 +1,11 @@
+Conscrypt in Android is made up of the contents of the Conscrypt
+GitHub repo (https://github.com/google/conscrypt) plus some
+Android-specific additions.  Specifically, the following are
+Android-only:
+
+Android.bp
+OWNERS
+README.android
+publicapi/...
+repackaged/...
+srcgen/...
diff --git a/publicapi/src/main/java/android/net/ssl/SSLEngines.java b/publicapi/src/main/java/android/net/ssl/SSLEngines.java
new file mode 100644
index 0000000..bcdcc1a
--- /dev/null
+++ b/publicapi/src/main/java/android/net/ssl/SSLEngines.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2018 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 android.net.ssl;
+
+import com.android.org.conscrypt.Conscrypt;
+import javax.net.ssl.SSLEngine;
+
+/**
+ * Static utility methods for accessing additional functionality of supported instances of
+ * {@link SSLEngine}.  Engines from the platform TLS provider will be compatible with all
+ * methods in this class.
+ */
+public class SSLEngines {
+    private SSLEngines() {}
+
+    /**
+     * Returns whether the given engine can be used with the methods in this class.  In general,
+     * only engines from the platform TLS provider are supported.
+     */
+    public static boolean isSupportedEngine(SSLEngine engine) {
+        return Conscrypt.isConscrypt(engine);
+    }
+
+    private static void checkSupported(SSLEngine e) {
+        if (!isSupportedEngine(e)) {
+            throw new IllegalArgumentException("Engine is not a supported engine.");
+        }
+    }
+
+    /**
+     * Enables or disables the use of session tickets.
+     *
+     * <p>This function must be called before the handshake is started or it will have no effect.
+     *
+     * @param engine the engine
+     * @param useSessionTickets whether to enable or disable the use of session tickets
+     * @throws IllegalArgumentException if the given engine is not a platform engine
+     */
+    public static void setUseSessionTickets(SSLEngine engine, boolean useSessionTickets) {
+        checkSupported(engine);
+        Conscrypt.setUseSessionTickets(engine, useSessionTickets);
+    }
+}
diff --git a/publicapi/src/main/java/android/net/ssl/SSLSockets.java b/publicapi/src/main/java/android/net/ssl/SSLSockets.java
new file mode 100644
index 0000000..948f61e
--- /dev/null
+++ b/publicapi/src/main/java/android/net/ssl/SSLSockets.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2018 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 android.net.ssl;
+
+import com.android.org.conscrypt.Conscrypt;
+import javax.net.ssl.SSLSocket;
+
+/**
+ * Static utility methods for accessing additional functionality of supported instances of
+ * {@link SSLSocket}.  Sockets from the platform TLS provider will be compatible with all
+ * methods in this class.
+ */
+public class SSLSockets {
+    private SSLSockets() {}
+
+    /**
+     * Returns whether the given socket can be used with the methods in this class.  In general,
+     * only sockets from the platform TLS provider are supported.
+     */
+    public static boolean isSupportedSocket(SSLSocket socket) {
+        return Conscrypt.isConscrypt(socket);
+    }
+
+    private static void checkSupported(SSLSocket s) {
+        if (!isSupportedSocket(s)) {
+            throw new IllegalArgumentException("Socket is not a supported socket.");
+        }
+    }
+
+    /**
+     * Enables or disables the use of session tickets.
+     *
+     * <p>This function must be called before the handshake is started or it will have no effect.
+     *
+     * @param socket the socket
+     * @param useSessionTickets whether to enable or disable the use of session tickets
+     * @throws IllegalArgumentException if the given socket is not a platform socket
+     */
+    public static void setUseSessionTickets(SSLSocket socket, boolean useSessionTickets) {
+        checkSupported(socket);
+        Conscrypt.setUseSessionTickets(socket, useSessionTickets);
+    }
+}
diff --git a/publicapi/src/test/java/android/net/ssl/SSLEnginesTest.java b/publicapi/src/test/java/android/net/ssl/SSLEnginesTest.java
new file mode 100644
index 0000000..66abec6
--- /dev/null
+++ b/publicapi/src/test/java/android/net/ssl/SSLEnginesTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2018 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 android.net.ssl;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.org.conscrypt.tlswire.TlsTester;
+import com.android.org.conscrypt.tlswire.handshake.ClientHello;
+import com.android.org.conscrypt.tlswire.handshake.HelloExtension;
+import java.nio.ByteBuffer;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SSLEnginesTest {
+
+    private static class BrokenSSLEngine extends SSLEngine {
+        @Override public SSLEngineResult wrap(ByteBuffer[] byteBuffers, int i, int i1,
+                ByteBuffer byteBuffer) throws SSLException { throw new AssertionError(); }
+        @Override public SSLEngineResult unwrap(ByteBuffer byteBuffer, ByteBuffer[] byteBuffers,
+                int i, int i1) throws SSLException { throw new AssertionError(); }
+        @Override public Runnable getDelegatedTask() { throw new AssertionError(); }
+        @Override public void closeInbound() throws SSLException { throw new AssertionError(); }
+        @Override public boolean isInboundDone() { throw new AssertionError(); }
+        @Override public void closeOutbound() { throw new AssertionError(); }
+        @Override public boolean isOutboundDone() { throw new AssertionError(); }
+        @Override public String[] getSupportedCipherSuites() { throw new AssertionError(); }
+        @Override public String[] getEnabledCipherSuites() { throw new AssertionError(); }
+        @Override public void setEnabledCipherSuites(String[] strings) { throw new AssertionError(); }
+        @Override public String[] getSupportedProtocols() { throw new AssertionError(); }
+        @Override public String[] getEnabledProtocols() { throw new AssertionError(); }
+        @Override public void setEnabledProtocols(String[] strings) { throw new AssertionError(); }
+        @Override public SSLSession getSession() { throw new AssertionError(); }
+        @Override public void beginHandshake() throws SSLException { throw new AssertionError(); }
+        @Override public SSLEngineResult.HandshakeStatus getHandshakeStatus() { throw new AssertionError(); }
+        @Override public void setUseClientMode(boolean b) { throw new AssertionError(); }
+        @Override public boolean getUseClientMode() { throw new AssertionError(); }
+        @Override public void setNeedClientAuth(boolean b) { throw new AssertionError(); }
+        @Override public boolean getNeedClientAuth() { throw new AssertionError(); }
+        @Override public void setWantClientAuth(boolean b) { throw new AssertionError(); }
+        @Override public boolean getWantClientAuth() { throw new AssertionError(); }
+        @Override public void setEnableSessionCreation(boolean b) { throw new AssertionError(); }
+        @Override public boolean getEnableSessionCreation() { throw new AssertionError(); }
+    }
+
+    private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
+
+    @Test
+    public void testIsSupported() throws Exception {
+        SSLEngine e = SSLContext.getDefault().createSSLEngine();
+        assertTrue(SSLEngines.isSupportedEngine(e));
+
+        e = new BrokenSSLEngine();
+        assertFalse(SSLEngines.isSupportedEngine(e));
+    }
+
+    @Test
+    public void testUseSessionTickets() throws Exception {
+        try {
+            SSLEngines.setUseSessionTickets(new BrokenSSLEngine(), true);
+            fail();
+        } catch (IllegalArgumentException expected) {
+        }
+
+        SSLEngine e = SSLContext.getDefault().createSSLEngine();
+        e.setUseClientMode(true);
+        SSLEngines.setUseSessionTickets(e, true);
+
+        ClientHello hello = getClientHello(e);
+        assertNotNull(hello.findExtensionByType(HelloExtension.TYPE_SESSION_TICKET));
+
+        e = SSLContext.getDefault().createSSLEngine();
+        e.setUseClientMode(true);
+        SSLEngines.setUseSessionTickets(e, false);
+
+        hello = getClientHello(e);
+        assertNull(hello.findExtensionByType(HelloExtension.TYPE_SESSION_TICKET));
+    }
+
+    private static ClientHello getClientHello(SSLEngine e) throws Exception {
+        ByteBuffer out = ByteBuffer.allocate(64 * 1024);
+
+        e.wrap(EMPTY_BUFFER, out);
+        out.flip();
+        byte[] data = new byte[out.limit()];
+        out.get(data);
+
+        return TlsTester.parseClientHello(data);
+    }
+}
diff --git a/publicapi/src/test/java/android/net/ssl/SSLSocketsTest.java b/publicapi/src/test/java/android/net/ssl/SSLSocketsTest.java
new file mode 100644
index 0000000..02df047
--- /dev/null
+++ b/publicapi/src/test/java/android/net/ssl/SSLSocketsTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2018 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 android.net.ssl;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.org.conscrypt.tlswire.TlsTester;
+import com.android.org.conscrypt.tlswire.handshake.ClientHello;
+import com.android.org.conscrypt.tlswire.handshake.HelloExtension;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.HandshakeCompletedListener;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import tests.net.DelegatingSSLSocketFactory;
+
+@RunWith(JUnit4.class)
+public class SSLSocketsTest {
+
+    private static class BrokenSSLSocket extends SSLSocket {
+        @Override public String[] getSupportedCipherSuites() { throw new AssertionError(); }
+        @Override public String[] getEnabledCipherSuites() { throw new AssertionError(); }
+        @Override public void setEnabledCipherSuites(String[] strings) { throw new AssertionError(); }
+        @Override public String[] getSupportedProtocols() { throw new AssertionError(); }
+        @Override public String[] getEnabledProtocols() { throw new AssertionError(); }
+        @Override public void setEnabledProtocols(String[] strings) { throw new AssertionError(); }
+        @Override public SSLSession getSession() { throw new AssertionError(); }
+        @Override public void addHandshakeCompletedListener(
+                HandshakeCompletedListener handshakeCompletedListener) { throw new AssertionError(); }
+        @Override public void removeHandshakeCompletedListener(
+                HandshakeCompletedListener handshakeCompletedListener) { throw new AssertionError(); }
+        @Override public void startHandshake() { throw new AssertionError(); }
+        @Override public void setUseClientMode(boolean b) { throw new AssertionError(); }
+        @Override public boolean getUseClientMode() { throw new AssertionError(); }
+        @Override public void setNeedClientAuth(boolean b) { throw new AssertionError(); }
+        @Override public boolean getNeedClientAuth() { throw new AssertionError(); }
+        @Override public void setWantClientAuth(boolean b) { throw new AssertionError(); }
+        @Override public boolean getWantClientAuth() { throw new AssertionError(); }
+        @Override public void setEnableSessionCreation(boolean b) { throw new AssertionError(); }
+        @Override public boolean getEnableSessionCreation() { throw new AssertionError(); }
+    }
+
+    private ExecutorService executor;
+
+    @Before
+    public void setUp() {
+        executor = Executors.newCachedThreadPool();
+    }
+
+    @After
+    public void tearDown() throws InterruptedException {
+        executor.shutdown();
+        executor.awaitTermination(1, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testIsSupported() throws Exception {
+        SSLSocket s = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
+        assertTrue(SSLSockets.isSupportedSocket(s));
+
+        s = new BrokenSSLSocket();
+        assertFalse(SSLSockets.isSupportedSocket(s));
+    }
+
+    @Test
+    public void testUseSessionTickets() throws Exception {
+        try {
+            SSLSockets.setUseSessionTickets(new BrokenSSLSocket(), true);
+            fail();
+        } catch (IllegalArgumentException expected) {
+        }
+
+        SSLSocket s = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
+        SSLSockets.setUseSessionTickets(s, true);
+
+        ClientHello hello = TlsTester.captureTlsHandshakeClientHello(executor,
+                new DelegatingSSLSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault()) {
+                    @Override public SSLSocket configureSocket(SSLSocket socket) {
+                        SSLSockets.setUseSessionTickets(socket, true);
+                        return socket;
+                    }
+                });
+        assertNotNull(hello.findExtensionByType(HelloExtension.TYPE_SESSION_TICKET));
+
+        hello = TlsTester.captureTlsHandshakeClientHello(executor,
+                new DelegatingSSLSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault()) {
+                    @Override public SSLSocket configureSocket(SSLSocket socket) {
+                        SSLSockets.setUseSessionTickets(socket, false);
+                        return socket;
+                    }
+                });
+        assertNull(hello.findExtensionByType(HelloExtension.TYPE_SESSION_TICKET));
+    }
+}