CTS test for Heartbleed vulnerability in SSLSocket.
This tests for the Heartbleed vulnerability (CVE-2014-0160) in
OpenSSL by testing client- and server-mode SSLSocket which is
supposed to be backed by OpenSSL by default.
This test spawns an SSLSocket client, SSLServerSocket server, and a
Man-in-The-Middle (MiTM). The client connects to the MiTM which then
connects to the server, and starts forwarding all TLS records between
the client and the server, injecting a malformed HeartbeatRequest
when appropriate. The test passes only if no HeartbeatResponse is
emitted and the TLS handshake either succeeds (heartbeats supported)
or fails with fatal alert unexpected_message (heartbeats not
supported).
Bug: 13906893
(cherry picked from commit db119d1d2ca219091860c68fe7f1892484cb29b2)
Change-Id: Ied9050e299c6725c08bca73703803735393c4324
diff --git a/tests/tests/security/res/raw/openssl_heartbleed_test_cert.pem b/tests/tests/security/res/raw/openssl_heartbleed_test_cert.pem
new file mode 100644
index 0000000..553339c
--- /dev/null
+++ b/tests/tests/security/res/raw/openssl_heartbleed_test_cert.pem
@@ -0,0 +1,84 @@
+-----BEGIN CERTIFICATE-----
+MIIEJTCCAw2gAwIBAgIJAIyWCjpFJYdnMA0GCSqGSIb3DQEBBQUAMIGoMQswCQYD
+VQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFDAS
+BgNVBAoMC0dvb2dsZSBJbmMuMR4wHAYDVQQLDBVBbmRyb2lkIFNlY3VyaXR5IFRl
+YW0xPjA8BgNVBAMMNUFuZHJvaWQgQ1RTIHRlc3QgZm9yIE9wZW5TU0wgSGVhcnRi
+bGVlZCB2dWxuZXJhYmlsaXR5MB4XDTE0MDQxNDE5MjExNFoXDTI0MDQxMTE5MjEx
+NFowgagxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNTW91bnRh
+aW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xHjAcBgNVBAsMFUFuZHJvaWQg
+U2VjdXJpdHkgVGVhbTE+MDwGA1UEAww1QW5kcm9pZCBDVFMgdGVzdCBmb3IgT3Bl
+blNTTCBIZWFydGJsZWVkIHZ1bG5lcmFiaWxpdHkwggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQC3hfx9exE0EGokpMyNj6CAaSAdYL5bCCafZwpr5/U9GnKC
+5i5spopmqaQKUj3KsaLEBs9k1LsYqgVGeobOb/IonYrnNsRwqBawGLhhksz0/R5H
+2lOYDibsHd8igJkwWCxhhQLnT/h62x67B9/2mduV3nHcjjQ/+g4umVySKcoO79+4
+5rX4dX/dX3vrfKE5ootDLvTycavAPSUfCKlBG4+DQYI0Q2bfjLWSHYdhFwkCTaYs
+uTJPdDfzD8gRd/2egjYE/sNoKiNb3NiyHynZ849VDB1H/Y9ojO/3wAWpcDnP+9b9
+JDq4eszjYrmCc0PiDWXBMbqpCei4jaJLmUDymCRXAgMBAAGjUDBOMB0GA1UdDgQW
+BBR8Zq0HoE1D2c6/MLtWRXI2JWUcqjAfBgNVHSMEGDAWgBR8Zq0HoE1D2c6/MLtW
+RXI2JWUcqjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB7ceFMrL8I
+4why5v2M272EAay2o0AdKsZFlUpM3qJ3XvosLqcKEu9NVdVxiI4QJOh5Rgm7e48e
+7WXgDcZM5F6/1e8X2PjgtbhwCpq1yTtIEhMLpu0IbVsNfvIT/IemfGAQqWBjZByE
+jUoTW2AeuA7ZpzzmrNI+98Y2MocX9LcveK7Fnfsb7Y8LKVOvnRZ34VzC+py+w9p2
+K8tOZU22mIBdZtdXe1cEdb5FszU8XMp7nzgZn5QDDWIoipNiL9m226kwEPf3EoXz
+bcNhn6KrPah86kVwpeOsX2fM4xHUg7Zv2IcX66r9WzchyB7HrDyTjfzDQRt1fJCO
+s0xyq4f1EHH2
+-----END CERTIFICATE-----
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 8c:96:0a:3a:45:25:87:67
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: C=US, ST=CA, L=Mountain View, O=Google Inc., OU=Android Security Team, CN=Android CTS test for OpenSSL Heartbleed vulnerability
+ Validity
+ Not Before: Apr 14 19:21:14 2014 GMT
+ Not After : Apr 11 19:21:14 2024 GMT
+ Subject: C=US, ST=CA, L=Mountain View, O=Google Inc., OU=Android Security Team, CN=Android CTS test for OpenSSL Heartbleed vulnerability
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (2048 bit)
+ Modulus:
+ 00:b7:85:fc:7d:7b:11:34:10:6a:24:a4:cc:8d:8f:
+ a0:80:69:20:1d:60:be:5b:08:26:9f:67:0a:6b:e7:
+ f5:3d:1a:72:82:e6:2e:6c:a6:8a:66:a9:a4:0a:52:
+ 3d:ca:b1:a2:c4:06:cf:64:d4:bb:18:aa:05:46:7a:
+ 86:ce:6f:f2:28:9d:8a:e7:36:c4:70:a8:16:b0:18:
+ b8:61:92:cc:f4:fd:1e:47:da:53:98:0e:26:ec:1d:
+ df:22:80:99:30:58:2c:61:85:02:e7:4f:f8:7a:db:
+ 1e:bb:07:df:f6:99:db:95:de:71:dc:8e:34:3f:fa:
+ 0e:2e:99:5c:92:29:ca:0e:ef:df:b8:e6:b5:f8:75:
+ 7f:dd:5f:7b:eb:7c:a1:39:a2:8b:43:2e:f4:f2:71:
+ ab:c0:3d:25:1f:08:a9:41:1b:8f:83:41:82:34:43:
+ 66:df:8c:b5:92:1d:87:61:17:09:02:4d:a6:2c:b9:
+ 32:4f:74:37:f3:0f:c8:11:77:fd:9e:82:36:04:fe:
+ c3:68:2a:23:5b:dc:d8:b2:1f:29:d9:f3:8f:55:0c:
+ 1d:47:fd:8f:68:8c:ef:f7:c0:05:a9:70:39:cf:fb:
+ d6:fd:24:3a:b8:7a:cc:e3:62:b9:82:73:43:e2:0d:
+ 65:c1:31:ba:a9:09:e8:b8:8d:a2:4b:99:40:f2:98:
+ 24:57
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Subject Key Identifier:
+ 7C:66:AD:07:A0:4D:43:D9:CE:BF:30:BB:56:45:72:36:25:65:1C:AA
+ X509v3 Authority Key Identifier:
+ keyid:7C:66:AD:07:A0:4D:43:D9:CE:BF:30:BB:56:45:72:36:25:65:1C:AA
+
+ X509v3 Basic Constraints:
+ CA:TRUE
+ Signature Algorithm: sha1WithRSAEncryption
+ 7b:71:e1:4c:ac:bf:08:e3:08:72:e6:fd:8c:db:bd:84:01:ac:
+ b6:a3:40:1d:2a:c6:45:95:4a:4c:de:a2:77:5e:fa:2c:2e:a7:
+ 0a:12:ef:4d:55:d5:71:88:8e:10:24:e8:79:46:09:bb:7b:8f:
+ 1e:ed:65:e0:0d:c6:4c:e4:5e:bf:d5:ef:17:d8:f8:e0:b5:b8:
+ 70:0a:9a:b5:c9:3b:48:12:13:0b:a6:ed:08:6d:5b:0d:7e:f2:
+ 13:fc:87:a6:7c:60:10:a9:60:63:64:1c:84:8d:4a:13:5b:60:
+ 1e:b8:0e:d9:a7:3c:e6:ac:d2:3e:f7:c6:36:32:87:17:f4:b7:
+ 2f:78:ae:c5:9d:fb:1b:ed:8f:0b:29:53:af:9d:16:77:e1:5c:
+ c2:fa:9c:be:c3:da:76:2b:cb:4e:65:4d:b6:98:80:5d:66:d7:
+ 57:7b:57:04:75:be:45:b3:35:3c:5c:ca:7b:9f:38:19:9f:94:
+ 03:0d:62:28:8a:93:62:2f:d9:b6:db:a9:30:10:f7:f7:12:85:
+ f3:6d:c3:61:9f:a2:ab:3d:a8:7c:ea:45:70:a5:e3:ac:5f:67:
+ cc:e3:11:d4:83:b6:6f:d8:87:17:eb:aa:fd:5b:37:21:c8:1e:
+ c7:ac:3c:93:8d:fc:c3:41:1b:75:7c:90:8e:b3:4c:72:ab:87:
+ f5:10:71:f6
+SHA1 Fingerprint=01:E1:C6:46:02:BE:8F:F7:F6:82:22:C8:1C:AE:A2:85:0D:3E:D0:98
diff --git a/tests/tests/security/res/raw/openssl_heartbleed_test_key.der b/tests/tests/security/res/raw/openssl_heartbleed_test_key.der
new file mode 100644
index 0000000..c925724
--- /dev/null
+++ b/tests/tests/security/res/raw/openssl_heartbleed_test_key.der
Binary files differ
diff --git a/tests/tests/security/src/android/security/cts/OpenSSLHeartbleedTest.java b/tests/tests/security/src/android/security/cts/OpenSSLHeartbleedTest.java
new file mode 100644
index 0000000..86957a8
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/OpenSSLHeartbleedTest.java
@@ -0,0 +1,969 @@
+/*
+ * Copyright (C) 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 implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts;
+
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+import android.util.Log;
+
+import com.android.cts.security.R;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.security.KeyFactory;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ServerSocketFactory;
+import javax.net.SocketFactory;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Tests for the OpenSSL Heartbleed vulnerability.
+ */
+public class OpenSSLHeartbleedTest extends InstrumentationTestCase {
+
+ // IMPLEMENTATION NOTE: This test spawns an SSLSocket client, SSLServerSocket server, and a
+ // Man-in-The-Middle (MiTM). The client connects to the MiTM which then connects to the server
+ // and starts forwarding all TLS records between the client and the server. In tests that check
+ // for the Heartbleed vulnerability, the MiTM also injects a HeartbeatRequest message into the
+ // traffic.
+
+ // IMPLEMENTATION NOTE: This test spawns several background threads that perform network I/O
+ // on localhost. To ensure that these background threads are cleaned up at the end of the test
+ // tearDown() kills the sockets they may be using. To aid this behavior, all Socket and
+ // ServerSocket instances are available as fields of this class. These fields should be accessed
+ // via setters and getters to avoid memory visibility issues due to concurrency.
+
+ private static final String TAG = OpenSSLHeartbleedTest.class.getSimpleName();
+
+ private SSLServerSocket mServerListeningSocket;
+ private SSLSocket mServerSocket;
+ private SSLSocket mClientSocket;
+ private ServerSocket mMitmListeningSocket;
+ private Socket mMitmServerSocket;
+ private Socket mMitmClientSocket;
+ private ExecutorService mExecutorService;
+
+ private boolean mHeartbeatRequestWasInjected;
+ private boolean mHeartbeatResponseWasDetetected;
+ private int mFirstDetectedFatalAlertDescription = -1;
+
+ @Override
+ protected void tearDown() throws Exception {
+ Log.i(TAG, "Tearing down");
+ if (mExecutorService != null) {
+ mExecutorService.shutdownNow();
+ }
+ closeQuietly(getServerListeningSocket());
+ closeQuietly(getServerSocket());
+ closeQuietly(getClientSocket());
+ closeQuietly(getMitmListeningSocket());
+ closeQuietly(getMitmServerSocket());
+ closeQuietly(getMitmClientSocket());
+ super.tearDown();
+ Log.i(TAG, "Tear down completed");
+ }
+
+ /**
+ * Tests that TLS handshake succeeds when the MiTM simply forwards all data without tampering
+ * with it. This is to catch issues unrelated to TLS heartbeats.
+ */
+ public void testWithoutHeartbeats() throws Exception {
+ handshake(false, false);
+ }
+
+ /**
+ * Tests whether client sockets are vulnerable to Heartbleed.
+ */
+ public void testClientHeartbleed() throws Exception {
+ checkHeartbleed(true);
+ }
+
+ /**
+ * Tests whether server sockets are vulnerable to Heartbleed.
+ */
+ public void testServerHeartbleed() throws Exception {
+ checkHeartbleed(false);
+ }
+
+ /**
+ * Tests for Heartbleed.
+ *
+ * @param client {@code true} to test the client, {@code false} to test the server.
+ */
+ private void checkHeartbleed(boolean client) throws Exception {
+ // IMPLEMENTATION NOTE: The MiTM is forwarding all TLS records between the client and the
+ // server unmodified. Additionally, the MiTM transmits a malformed HeartbeatRequest to
+ // server (if "client" argument is false) right after client's ClientKeyExchange or to
+ // client (if "client" argument is true) right after server's ServerHello. The peer is
+ // expected to either ignore the HeartbeatRequest (if heartbeats are supported) or to abort
+ // the handshake with unexpected_message alert (if heartbeats are not supported).
+ try {
+ handshake(true, client);
+ } catch (ExecutionException e) {
+ assertFalse(
+ "SSLSocket is vulnerable to Heartbleed in " + ((client) ? "client" : "server")
+ + " mode",
+ wasHeartbeatResponseDetected());
+ if (e.getCause() instanceof SSLException) {
+ // TLS handshake or data exchange failed. Check whether the error was caused by
+ // fatal alert unexpected_message
+ int alertDescription = getFirstDetectedFatalAlertDescription();
+ if (alertDescription == -1) {
+ fail("Handshake failed without a fatal alert");
+ }
+ assertEquals(
+ "First fatal alert description received from server",
+ AlertMessage.DESCRIPTION_UNEXPECTED_MESSAGE,
+ alertDescription);
+ return;
+ } else {
+ throw e;
+ }
+ }
+
+ // TLS handshake succeeded
+ assertFalse(
+ "SSLSocket is vulnerable to Heartbleed in " + ((client) ? "client" : "server")
+ + " mode",
+ wasHeartbeatResponseDetected());
+ assertTrue("HeartbeatRequest not injected", wasHeartbeatRequestInjected());
+ }
+
+ /**
+ * Starts the client, server, and the MiTM. Makes the client and server perform a TLS handshake
+ * and exchange application-level data. The MiTM injects a HeartbeatRequest message if requested
+ * by {@code heartbeatRequestInjected}. The direction of the injected message is specified by
+ * {@code injectedIntoClient}.
+ */
+ private void handshake(
+ final boolean heartbeatRequestInjected,
+ final boolean injectedIntoClient) throws Exception {
+ mExecutorService = Executors.newFixedThreadPool(4);
+ setServerListeningSocket(serverBind());
+ final SocketAddress serverAddress = getServerListeningSocket().getLocalSocketAddress();
+ Log.i(TAG, "Server bound to " + serverAddress);
+
+ setMitmListeningSocket(mitmBind());
+ final SocketAddress mitmAddress = getMitmListeningSocket().getLocalSocketAddress();
+ Log.i(TAG, "MiTM bound to " + mitmAddress);
+
+ // Start the MiTM daemon in the background
+ mExecutorService.submit(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ mitmAcceptAndForward(
+ serverAddress,
+ heartbeatRequestInjected,
+ injectedIntoClient);
+ return null;
+ }
+ });
+ // Start the server in the background
+ Future<Void> serverFuture = mExecutorService.submit(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ serverAcceptAndHandshake();
+ return null;
+ }
+ });
+ // Start the client in the background
+ Future<Void> clientFuture = mExecutorService.submit(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ clientConnectAndHandshake(mitmAddress);
+ return null;
+ }
+ });
+
+ Log.i(TAG, "Waiting for client");
+ clientFuture.get(10, TimeUnit.SECONDS);
+ Log.i(TAG, "Waiting for server");
+ serverFuture.get(1, TimeUnit.SECONDS);
+ Log.i(TAG, "Handshake completed and application data exchanged");
+ }
+
+ private void clientConnectAndHandshake(SocketAddress serverAddress) throws Exception {
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(
+ null,
+ new TrustManager[] {new TrustAllX509TrustManager()},
+ null);
+ SSLSocket socket = (SSLSocket) sslContext.getSocketFactory().createSocket();
+ setClientSocket(socket);
+ try {
+ Log.i(TAG, "Client connecting to " + serverAddress);
+ socket.connect(serverAddress);
+ Log.i(TAG, "Client connected to server from " + socket.getLocalSocketAddress());
+ // Ensure a TLS handshake is performed and an exception is thrown if it fails.
+ socket.getOutputStream().write("client".getBytes());
+ socket.getOutputStream().flush();
+ Log.i(TAG, "Client sent request. Reading response");
+ int b = socket.getInputStream().read();
+ Log.i(TAG, "Client read response: " + b);
+ } catch (Exception e) {
+ Log.w(TAG, "Client failed", e);
+ throw e;
+ } finally {
+ socket.close();
+ }
+ }
+
+ private SSLServerSocket serverBind() throws Exception {
+ // Load the server's private key and cert chain
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(
+ readResource(
+ getInstrumentation().getContext(), R.raw.openssl_heartbleed_test_key)));
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ X509Certificate[] certChain = new X509Certificate[] {
+ (X509Certificate) certFactory.generateCertificate(
+ new ByteArrayInputStream(readResource(
+ getInstrumentation().getContext(),
+ R.raw.openssl_heartbleed_test_cert)))
+ };
+
+ // Initialize TLS context to use the private key and cert chain for server sockets
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(
+ new KeyManager[] {new HardcodedCertX509KeyManager(privateKey, certChain)},
+ null,
+ null);
+
+ Log.i(TAG, "Server binding to local port");
+ return (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket(0);
+ }
+
+ private void serverAcceptAndHandshake() throws Exception {
+ SSLSocket socket = null;
+ SSLServerSocket serverSocket = getServerListeningSocket();
+ try {
+ Log.i(TAG, "Server listening for incoming connection");
+ socket = (SSLSocket) serverSocket.accept();
+ setServerSocket(socket);
+ Log.i(TAG, "Server accepted connection from " + socket.getRemoteSocketAddress());
+ // Ensure a TLS handshake is performed and an exception is thrown if it fails.
+ socket.getOutputStream().write("server".getBytes());
+ socket.getOutputStream().flush();
+ Log.i(TAG, "Server sent reply. Reading response");
+ int b = socket.getInputStream().read();
+ Log.i(TAG, "Server read response: " + b);
+ } catch (Exception e) {
+ Log.w(TAG, "Server failed", e);
+ throw e;
+ } finally {
+ if (socket != null) {
+ socket.close();
+ }
+ }
+ }
+
+ private ServerSocket mitmBind() throws Exception {
+ Log.i(TAG, "MiTM binding to local port");
+ return ServerSocketFactory.getDefault().createServerSocket(0);
+ }
+
+ /**
+ * Accepts the connection on the MiTM listening socket, forwards the TLS records between the
+ * client and the server, and, if requested, injects a {@code HeartbeatRequest}.
+ *
+ * @param injectHeartbeat whether to inject a {@code HeartbeatRequest} message.
+ * @param injectIntoClient when {@code injectHeartbeat} is {@code true}, whether to inject the
+ * {@code HeartbeatRequest} message into client or into server.
+ */
+ private void mitmAcceptAndForward(
+ SocketAddress serverAddress,
+ final boolean injectHeartbeat,
+ final boolean injectIntoClient) throws Exception {
+ Socket clientSocket = null;
+ Socket serverSocket = null;
+ ServerSocket listeningSocket = getMitmListeningSocket();
+ try {
+ Log.i(TAG, "MiTM waiting for incoming connection");
+ clientSocket = listeningSocket.accept();
+ setMitmClientSocket(clientSocket);
+ Log.i(TAG, "MiTM accepted connection from " + clientSocket.getRemoteSocketAddress());
+ serverSocket = SocketFactory.getDefault().createSocket();
+ setMitmServerSocket(serverSocket);
+ Log.i(TAG, "MiTM connecting to server " + serverAddress);
+ serverSocket.connect(serverAddress, 10000);
+ Log.i(TAG, "MiTM connected to server from " + serverSocket.getLocalSocketAddress());
+ final InputStream serverInputStream = serverSocket.getInputStream();
+ final OutputStream clientOutputStream = clientSocket.getOutputStream();
+ Future<Void> serverToClientTask = mExecutorService.submit(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ // Inject HeatbeatRequest after ServerHello, if requested
+ forwardTlsRecords(
+ "MiTM S->C",
+ serverInputStream,
+ clientOutputStream,
+ (injectHeartbeat && injectIntoClient)
+ ? HandshakeMessage.TYPE_SERVER_HELLO : -1);
+ return null;
+ }
+ });
+ // Inject HeatbeatRequest after ClientKeyExchange, if requested
+ forwardTlsRecords(
+ "MiTM C->S",
+ clientSocket.getInputStream(),
+ serverSocket.getOutputStream(),
+ (injectHeartbeat && !injectIntoClient)
+ ? HandshakeMessage.TYPE_CLIENT_KEY_EXCHANGE : -1);
+ serverToClientTask.get(10, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ Log.w(TAG, "MiTM failed", e);
+ throw e;
+ } finally {
+ closeQuietly(clientSocket);
+ closeQuietly(serverSocket);
+ }
+ }
+
+ /**
+ * Forwards TLS records from the provided {@code InputStream} to the provided
+ * {@code OutputStream}. If requested, injects a {@code HeartbeatMessage}.
+ */
+ private void forwardTlsRecords(
+ String logPrefix,
+ InputStream in,
+ OutputStream out,
+ int handshakeMessageTypeAfterWhichToInjectHeartbeatRequest) throws Exception {
+ Log.i(TAG, logPrefix + ": record forwarding started");
+ boolean interestingRecordsLogged =
+ handshakeMessageTypeAfterWhichToInjectHeartbeatRequest == -1;
+ try {
+ TlsRecordReader reader = new TlsRecordReader(in);
+ byte[] recordBytes;
+ // Fragments contained in records may be encrypted after a certain point in the
+ // handshake. Once they are encrypted, this MiTM cannot inspect their plaintext which.
+ boolean fragmentEncryptionMayBeEnabled = false;
+ while ((recordBytes = reader.readRecord()) != null) {
+ TlsRecord record = TlsRecord.parse(recordBytes);
+ forwardTlsRecord(logPrefix,
+ recordBytes,
+ record,
+ fragmentEncryptionMayBeEnabled,
+ out,
+ interestingRecordsLogged,
+ handshakeMessageTypeAfterWhichToInjectHeartbeatRequest);
+ if (record.protocol == TlsProtocols.CHANGE_CIPHER_SPEC) {
+ fragmentEncryptionMayBeEnabled = true;
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, logPrefix + ": failed", e);
+ throw e;
+ } finally {
+ Log.d(TAG, logPrefix + ": record forwarding finished");
+ }
+ }
+
+ private void forwardTlsRecord(
+ String logPrefix,
+ byte[] recordBytes,
+ TlsRecord record,
+ boolean fragmentEncryptionMayBeEnabled,
+ OutputStream out,
+ boolean interestingRecordsLogged,
+ int handshakeMessageTypeAfterWhichToInjectHeartbeatRequest) throws IOException {
+ // Save information about the records if its of interest to this test
+ if (interestingRecordsLogged) {
+ switch (record.protocol) {
+ case TlsProtocols.ALERT:
+ if (!fragmentEncryptionMayBeEnabled) {
+ AlertMessage alert = AlertMessage.tryParse(record);
+ if ((alert != null) && (alert.level == AlertMessage.LEVEL_FATAL)) {
+ setFatalAlertDetected(alert.description);
+ }
+ }
+ break;
+ case TlsProtocols.HEARTBEAT:
+ // When TLS records are encrypted, we cannot determine whether a
+ // heartbeat is a HeartbeatResponse. In our setup, the client and the
+ // server are not expected to sent HeartbeatRequests. Thus, we err on
+ // the side of caution and assume that any heartbeat message sent by
+ // client or server is a HeartbeatResponse.
+ Log.e(TAG, logPrefix
+ + ": heartbeat response detected -- vulnerable to Heartbleed");
+ setHeartbeatResponseWasDetected();
+ break;
+ }
+ }
+
+ Log.i(TAG, logPrefix + ": Forwarding TLS record. "
+ + getRecordInfo(record, fragmentEncryptionMayBeEnabled));
+ out.write(recordBytes);
+ out.flush();
+
+ // Inject HeartbeatRequest, if necessary, after the specified handshake message type
+ if (handshakeMessageTypeAfterWhichToInjectHeartbeatRequest != -1) {
+ if ((!fragmentEncryptionMayBeEnabled) && (isHandshakeMessageType(
+ record, handshakeMessageTypeAfterWhichToInjectHeartbeatRequest))) {
+ // The Heartbeat Request message below is malformed because its declared
+ // length of payload one byte larger than the actual payload. The peer is
+ // supposed to reject such messages.
+ byte[] payload = "arbitrary".getBytes("US-ASCII");
+ byte[] heartbeatRequestRecordBytes = createHeartbeatRequestRecord(
+ record.versionMajor,
+ record.versionMinor,
+ payload.length + 1,
+ payload);
+ Log.i(TAG, logPrefix + ": Injecting malformed HeartbeatRequest: "
+ + getRecordInfo(
+ TlsRecord.parse(heartbeatRequestRecordBytes), false));
+ setHeartbeatRequestWasInjected();
+ out.write(heartbeatRequestRecordBytes);
+ out.flush();
+ }
+ }
+ }
+
+ private static String getRecordInfo(TlsRecord record, boolean mayBeEncrypted) {
+ StringBuilder result = new StringBuilder();
+ result.append(getProtocolName(record.protocol))
+ .append(", ")
+ .append(getFragmentInfo(record, mayBeEncrypted));
+ return result.toString();
+ }
+
+ private static String getProtocolName(int protocol) {
+ switch (protocol) {
+ case TlsProtocols.ALERT:
+ return "alert";
+ case TlsProtocols.APPLICATION_DATA:
+ return "application data";
+ case TlsProtocols.CHANGE_CIPHER_SPEC:
+ return "change cipher spec";
+ case TlsProtocols.HANDSHAKE:
+ return "handshake";
+ case TlsProtocols.HEARTBEAT:
+ return "heatbeat";
+ default:
+ return String.valueOf(protocol);
+ }
+ }
+
+ private static String getFragmentInfo(TlsRecord record, boolean mayBeEncrypted) {
+ StringBuilder result = new StringBuilder();
+ if (mayBeEncrypted) {
+ result.append("encrypted?");
+ } else {
+ switch (record.protocol) {
+ case TlsProtocols.ALERT:
+ result.append("level: " + ((record.fragment.length > 0)
+ ? String.valueOf(record.fragment[0] & 0xff) : "n/a")
+ + ", description: "
+ + ((record.fragment.length > 1)
+ ? String.valueOf(record.fragment[1] & 0xff) : "n/a"));
+ break;
+ case TlsProtocols.APPLICATION_DATA:
+ break;
+ case TlsProtocols.CHANGE_CIPHER_SPEC:
+ result.append("payload: " + ((record.fragment.length > 0)
+ ? String.valueOf(record.fragment[0] & 0xff) : "n/a"));
+ break;
+ case TlsProtocols.HANDSHAKE:
+ result.append("type: " + ((record.fragment.length > 0)
+ ? String.valueOf(record.fragment[0] & 0xff) : "n/a"));
+ break;
+ case TlsProtocols.HEARTBEAT:
+ result.append("type: " + ((record.fragment.length > 0)
+ ? String.valueOf(record.fragment[0] & 0xff) : "n/a")
+ + ", payload length: "
+ + ((record.fragment.length >= 3)
+ ? String.valueOf(
+ getUnsignedShortBigEndian(record.fragment, 1))
+ : "n/a"));
+ break;
+ }
+ }
+ result.append(", ").append("fragment length: " + record.fragment.length);
+ return result.toString();
+ }
+
+ private synchronized void setServerListeningSocket(SSLServerSocket socket) {
+ mServerListeningSocket = socket;
+ }
+
+ private synchronized SSLServerSocket getServerListeningSocket() {
+ return mServerListeningSocket;
+ }
+
+ private synchronized void setServerSocket(SSLSocket socket) {
+ mServerSocket = socket;
+ }
+
+ private synchronized SSLSocket getServerSocket() {
+ return mServerSocket;
+ }
+
+ private synchronized void setClientSocket(SSLSocket socket) {
+ mClientSocket = socket;
+ }
+
+ private synchronized SSLSocket getClientSocket() {
+ return mClientSocket;
+ }
+
+ private synchronized void setMitmListeningSocket(ServerSocket socket) {
+ mMitmListeningSocket = socket;
+ }
+
+ private synchronized ServerSocket getMitmListeningSocket() {
+ return mMitmListeningSocket;
+ }
+
+ private synchronized void setMitmServerSocket(Socket socket) {
+ mMitmServerSocket = socket;
+ }
+
+ private synchronized Socket getMitmServerSocket() {
+ return mMitmServerSocket;
+ }
+
+ private synchronized void setMitmClientSocket(Socket socket) {
+ mMitmClientSocket = socket;
+ }
+
+ private synchronized Socket getMitmClientSocket() {
+ return mMitmClientSocket;
+ }
+
+ private synchronized void setHeartbeatRequestWasInjected() {
+ mHeartbeatRequestWasInjected = true;
+ }
+
+ private synchronized boolean wasHeartbeatRequestInjected() {
+ return mHeartbeatRequestWasInjected;
+ }
+
+ private synchronized void setHeartbeatResponseWasDetected() {
+ mHeartbeatResponseWasDetetected = true;
+ }
+
+ private synchronized boolean wasHeartbeatResponseDetected() {
+ return mHeartbeatResponseWasDetetected;
+ }
+
+ private synchronized void setFatalAlertDetected(int description) {
+ if (mFirstDetectedFatalAlertDescription == -1) {
+ mFirstDetectedFatalAlertDescription = description;
+ }
+ }
+
+ private synchronized int getFirstDetectedFatalAlertDescription() {
+ return mFirstDetectedFatalAlertDescription;
+ }
+
+ private static abstract class TlsProtocols {
+ public static final int CHANGE_CIPHER_SPEC = 20;
+ public static final int ALERT = 21;
+ public static final int HANDSHAKE = 22;
+ public static final int APPLICATION_DATA = 23;
+ public static final int HEARTBEAT = 24;
+ private TlsProtocols() {}
+ }
+
+ private static class TlsRecord {
+ private int protocol;
+ private int versionMajor;
+ private int versionMinor;
+ private byte[] fragment;
+
+ private static TlsRecord parse(byte[] record) throws IOException {
+ TlsRecord result = new TlsRecord();
+ if (record.length < TlsRecordReader.RECORD_HEADER_LENGTH) {
+ throw new IOException("Record too short: " + record.length);
+ }
+ result.protocol = record[0] & 0xff;
+ result.versionMajor = record[1] & 0xff;
+ result.versionMinor = record[2] & 0xff;
+ int fragmentLength = getUnsignedShortBigEndian(record, 3);
+ int actualFragmentLength = record.length - TlsRecordReader.RECORD_HEADER_LENGTH;
+ if (fragmentLength != actualFragmentLength) {
+ throw new IOException("Fragment length mismatch. Expected: " + fragmentLength
+ + ", actual: " + actualFragmentLength);
+ }
+ result.fragment = new byte[fragmentLength];
+ System.arraycopy(
+ record, TlsRecordReader.RECORD_HEADER_LENGTH,
+ result.fragment, 0,
+ fragmentLength);
+ return result;
+ }
+
+ private static byte[] unparse(TlsRecord record) {
+ byte[] result = new byte[TlsRecordReader.RECORD_HEADER_LENGTH + record.fragment.length];
+ result[0] = (byte) record.protocol;
+ result[1] = (byte) record.versionMajor;
+ result[2] = (byte) record.versionMinor;
+ putUnsignedShortBigEndian(result, 3, record.fragment.length);
+ System.arraycopy(
+ record.fragment, 0,
+ result, TlsRecordReader.RECORD_HEADER_LENGTH,
+ record.fragment.length);
+ return result;
+ }
+ }
+
+ private static final boolean isHandshakeMessageType(TlsRecord record, int type) {
+ HandshakeMessage handshake = HandshakeMessage.tryParse(record);
+ if (handshake == null) {
+ return false;
+ }
+ return handshake.type == type;
+ }
+
+ private static class HandshakeMessage {
+ private static final int TYPE_SERVER_HELLO = 2;
+ private static final int TYPE_CLIENT_KEY_EXCHANGE = 16;
+
+ private int type;
+
+ /**
+ * Parses the provided TLS record as a handshake message.
+ *
+ * @return alert message or {@code null} if the record does not contain a handshake message.
+ */
+ private static HandshakeMessage tryParse(TlsRecord record) {
+ if (record.protocol != TlsProtocols.HANDSHAKE) {
+ return null;
+ }
+ if (record.fragment.length < 1) {
+ return null;
+ }
+ HandshakeMessage result = new HandshakeMessage();
+ result.type = record.fragment[0] & 0xff;
+ return result;
+ }
+ }
+
+ private static class AlertMessage {
+ private static final int LEVEL_FATAL = 2;
+ private static final int DESCRIPTION_UNEXPECTED_MESSAGE = 10;
+
+ private int level;
+ private int description;
+
+ /**
+ * Parses the provided TLS record as an alert message.
+ *
+ * @return alert message or {@code null} if the record does not contain an alert message.
+ */
+ private static AlertMessage tryParse(TlsRecord record) {
+ if (record.protocol != TlsProtocols.ALERT) {
+ return null;
+ }
+ if (record.fragment.length < 2) {
+ return null;
+ }
+ AlertMessage result = new AlertMessage();
+ result.level = record.fragment[0] & 0xff;
+ result.description = record.fragment[1] & 0xff;
+ return result;
+ }
+ }
+
+ private static abstract class HeartbeatProtocol {
+ private HeartbeatProtocol() {}
+
+ private static final int MESSAGE_TYPE_REQUEST = 1;
+ @SuppressWarnings("unused")
+ private static final int MESSAGE_TYPE_RESPONSE = 2;
+
+ private static final int MESSAGE_HEADER_LENGTH = 3;
+ private static final int MESSAGE_PADDING_LENGTH = 16;
+ }
+
+ private static byte[] createHeartbeatRequestRecord(
+ int versionMajor, int versionMinor,
+ int declaredPayloadLength, byte[] payload) {
+
+ byte[] fragment = new byte[HeartbeatProtocol.MESSAGE_HEADER_LENGTH
+ + payload.length + HeartbeatProtocol.MESSAGE_PADDING_LENGTH];
+ fragment[0] = HeartbeatProtocol.MESSAGE_TYPE_REQUEST;
+ putUnsignedShortBigEndian(fragment, 1, declaredPayloadLength); // payload_length
+ TlsRecord record = new TlsRecord();
+ record.protocol = TlsProtocols.HEARTBEAT;
+ record.versionMajor = versionMajor;
+ record.versionMinor = versionMinor;
+ record.fragment = fragment;
+ return TlsRecord.unparse(record);
+ }
+
+ /**
+ * Reader of TLS records.
+ */
+ private static class TlsRecordReader {
+ private static final int MAX_RECORD_LENGTH = 16384;
+ public static final int RECORD_HEADER_LENGTH = 5;
+
+ private final InputStream in;
+ private final byte[] buffer;
+ private int firstBufferedByteOffset;
+ private int bufferedByteCount;
+
+ public TlsRecordReader(InputStream in) {
+ this.in = in;
+ buffer = new byte[MAX_RECORD_LENGTH];
+ }
+
+ /**
+ * Reads the next TLS record.
+ *
+ * @return TLS record or {@code null} if EOF was encountered before any bytes of a record
+ * could be read.
+ */
+ public byte[] readRecord() throws IOException {
+ // Ensure that a TLS record header (or more) is in the buffer.
+ if (bufferedByteCount < RECORD_HEADER_LENGTH) {
+ boolean eofPermittedInstead = (bufferedByteCount == 0);
+ boolean eofEncounteredInstead =
+ !readAtLeast(RECORD_HEADER_LENGTH, eofPermittedInstead);
+ if (eofEncounteredInstead) {
+ // End of stream reached exactly before a TLS record start.
+ return null;
+ }
+ }
+
+ // TLS record header (or more) is in the buffer.
+ // Ensure that the rest of the record is in the buffer.
+ int fragmentLength = getUnsignedShortBigEndian(buffer, firstBufferedByteOffset + 3);
+ int recordLength = RECORD_HEADER_LENGTH + fragmentLength;
+ if (recordLength > MAX_RECORD_LENGTH) {
+ throw new IOException("TLS record too long: " + recordLength);
+ }
+ if (bufferedByteCount < recordLength) {
+ readAtLeast(recordLength - bufferedByteCount, false);
+ }
+
+ // TLS record (or more) is in the buffer.
+ byte[] record = new byte[recordLength];
+ System.arraycopy(buffer, firstBufferedByteOffset, record, 0, recordLength);
+ firstBufferedByteOffset += recordLength;
+ bufferedByteCount -= recordLength;
+ return record;
+ }
+
+ /**
+ * Reads at least the specified number of bytes from the underlying {@code InputStream} into
+ * the {@code buffer}.
+ *
+ * <p>Bytes buffered but not yet returned to the client in the {@code buffer} are relocated
+ * to the start of the buffer to make space if necessary.
+ *
+ * @param eofPermittedInstead {@code true} if it's permitted for an EOF to be encountered
+ * without any bytes having been read.
+ *
+ * @return {@code true} if the requested number of bytes (or more) has been read,
+ * {@code false} if {@code eofPermittedInstead} was {@code true} and EOF was
+ * encountered when no bytes have yet been read.
+ */
+ private boolean readAtLeast(int size, boolean eofPermittedInstead) throws IOException {
+ ensureRemainingBufferCapacityAtLeast(size);
+ boolean firstAttempt = true;
+ while (size > 0) {
+ int chunkSize = in.read(
+ buffer,
+ firstBufferedByteOffset + bufferedByteCount,
+ buffer.length - (firstBufferedByteOffset + bufferedByteCount));
+ if (chunkSize == -1) {
+ if ((firstAttempt) && (eofPermittedInstead)) {
+ return false;
+ } else {
+ throw new EOFException("Premature EOF");
+ }
+ }
+ firstAttempt = false;
+ bufferedByteCount += chunkSize;
+ size -= chunkSize;
+ }
+ return true;
+ }
+
+ /**
+ * Ensures that there is enough capacity in the buffer to store the specified number of
+ * bytes at the {@code firstBufferedByteOffset + bufferedByteCount} offset.
+ */
+ private void ensureRemainingBufferCapacityAtLeast(int size) throws IOException {
+ int bufferCapacityRemaining =
+ buffer.length - (firstBufferedByteOffset + bufferedByteCount);
+ if (bufferCapacityRemaining >= size) {
+ return;
+ }
+ // Insufficient capacity at the end of the buffer.
+ if (firstBufferedByteOffset > 0) {
+ // Some of the bytes at the start of the buffer have already been returned to the
+ // client of this reader. Check if moving the remaining buffered bytes to the start
+ // of the buffer will make enough space at the end of the buffer.
+ bufferCapacityRemaining += firstBufferedByteOffset;
+ if (bufferCapacityRemaining >= size) {
+ System.arraycopy(buffer, firstBufferedByteOffset, buffer, 0, bufferedByteCount);
+ firstBufferedByteOffset = 0;
+ return;
+ }
+ }
+
+ throw new IOException("Insuffucient remaining capacity in the buffer. Requested: "
+ + size + ", remaining: " + bufferCapacityRemaining);
+ }
+ }
+
+ private static int getUnsignedShortBigEndian(byte[] buf, int offset) {
+ return ((buf[offset] & 0xff) << 8) | (buf[offset + 1] & 0xff);
+ }
+
+ private static void putUnsignedShortBigEndian(byte[] buf, int offset, int value) {
+ buf[offset] = (byte) ((value >>> 8) & 0xff);
+ buf[offset + 1] = (byte) (value & 0xff);
+ }
+
+ // IMPLEMENTATION NOTE: We can't implement just one closeQueietly(Closeable) because on some
+ // older Android platforms Socket did not implement these interfaces. To make this patch easy to
+ // apply to these older platforms, we declare all the variants of closeQuietly that are needed
+ // without relying on the Closeable interface.
+
+ private static void closeQuietly(InputStream in) {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ignored) {}
+ }
+ }
+
+ private static void closeQuietly(ServerSocket socket) {
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException ignored) {}
+ }
+ }
+
+ private static void closeQuietly(Socket socket) {
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException ignored) {}
+ }
+ }
+
+ private static byte[] readResource(Context context, int resId) throws IOException {
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ InputStream in = null;
+ byte[] buf = new byte[16 * 1024];
+ try {
+ in = context.getResources().openRawResource(resId);
+ int chunkSize;
+ while ((chunkSize = in.read(buf)) != -1) {
+ result.write(buf, 0, chunkSize);
+ }
+ return result.toByteArray();
+ } finally {
+ closeQuietly(in);
+ }
+ }
+
+ /**
+ * {@link X509TrustManager} which trusts all certificate chains.
+ */
+ private static class TrustAllX509TrustManager implements X509TrustManager {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+ }
+
+ /**
+ * {@link X509KeyManager} which uses the provided private key and cert chain for all sockets.
+ */
+ private static class HardcodedCertX509KeyManager implements X509KeyManager {
+
+ private final PrivateKey mPrivateKey;
+ private final X509Certificate[] mCertChain;
+
+ private HardcodedCertX509KeyManager(PrivateKey privateKey, X509Certificate[] certChain) {
+ mPrivateKey = privateKey;
+ mCertChain = certChain;
+ }
+
+ @Override
+ public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
+ return null;
+ }
+
+ @Override
+ public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+ return "singleton";
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ return mCertChain;
+ }
+
+ @Override
+ public String[] getClientAliases(String keyType, Principal[] issuers) {
+ return null;
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ return mPrivateKey;
+ }
+
+ @Override
+ public String[] getServerAliases(String keyType, Principal[] issuers) {
+ return new String[] {"singleton"};
+ }
+ }
+}