CTS test for libmedia OOB write anywhere

Set up a DRM session, spoof the non-secure device as
secure and pass an invalid pointer as the decrypt
destination buffer address which causes a segfault in
mediaserver on devices without the patch.

This test only runs on non-secure (L3) devices, it is
skipped on secure devices since the vulnerability doesn't
exist there.

b/23223325

Change-Id: Ie88a09ffa9ff722d2fb4642c6655c95cc2478eed
diff --git a/tests/tests/security/jni/android_security_cts_MediaCryptoTest.cpp b/tests/tests/security/jni/android_security_cts_MediaCryptoTest.cpp
new file mode 100644
index 0000000..abb26eb
--- /dev/null
+++ b/tests/tests/security/jni/android_security_cts_MediaCryptoTest.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+/* Original code copied from NDK Native-media sample code */
+
+//#define LOG_NDEBUG 0
+#define TAG "NativeMediaCrypto"
+#include <log/log.h>
+
+#include <android_media_MediaCrypto.h>
+#include <assert.h>
+#include <binder/MemoryDealer.h>
+#include <jni.h>
+#include <media/ICrypto.h>
+#include <media/stagefright/foundation/AString.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <utils/StrongPointer.h>
+#include <semaphore.h>
+
+
+static const size_t kBufferSize = 1024;
+
+using namespace android;
+
+static jboolean testCrypto(sp<ICrypto> icrypto,
+        const CryptoPlugin::SubSample *subSample, CryptoPlugin::Mode mode)
+{
+    // Allocate source buffer
+    sp<MemoryDealer> memDealer = new MemoryDealer(kBufferSize, "MediaCryptoTest");
+    sp<IMemory> srcBuffer = memDealer->allocate(kBufferSize);
+    if (!srcBuffer->pointer()) {
+        ALOGE("Failed to allocate source buffer");
+        return false;
+    }
+    memset(srcBuffer->pointer(), 's', kBufferSize);
+
+    // Invalid dest pointer should fault if mediaserver attempts
+    // to write to it.  Don't use NULL because that's probably
+    // checked for.
+    void *dstPtr = reinterpret_cast<void *>(1);
+
+    // Spoof the device as being secure
+    bool secure = true;
+
+    uint8_t key[16] = {0};
+    uint8_t iv[16] = {0};
+    uint32_t offset = 0;
+    AString errorDetailMsg;
+
+    ssize_t result = icrypto->decrypt(secure, key, iv, mode, srcBuffer, offset,
+            subSample, 1, dstPtr, &errorDetailMsg);
+
+    // call should return an error and shouldn't kill media server
+    return (result != OK && result != DEAD_OBJECT);
+}
+
+// Test for icrypto interface vulnerabilities
+extern "C" jboolean Java_android_security_cts_MediaCryptoTest_validateCryptoNative(JNIEnv *env,
+        jclass /*clazz*/, jobject crypto)
+{
+    bool result = false;
+    sp<ICrypto> icrypto = JCrypto::GetCrypto(env, crypto);
+    if (icrypto != NULL) {
+        if (icrypto->requiresSecureDecoderComponent("video/avc")) {
+            ALOGI("device is secure, bypassing test");
+            return true;
+        }
+
+        CryptoPlugin::Mode unencryptedMode = CryptoPlugin::kMode_Unencrypted;
+        CryptoPlugin::Mode aesCtrMode = CryptoPlugin::kMode_AES_CTR;
+
+        CryptoPlugin::SubSample clrSubSample = {kBufferSize, 0};
+        CryptoPlugin::SubSample encSubSample = {0, kBufferSize};
+
+        result =
+            testCrypto(icrypto, &clrSubSample, unencryptedMode) &&
+            testCrypto(icrypto, &clrSubSample, aesCtrMode) &&
+            testCrypto(icrypto, &encSubSample, unencryptedMode) &&
+            testCrypto(icrypto, &encSubSample, aesCtrMode);
+    } else {
+        ALOGE("Failed to get icrypto interface");
+    }
+    return result;
+}
+
+
+
diff --git a/tests/tests/security/src/android/security/cts/HttpPost.java b/tests/tests/security/src/android/security/cts/HttpPost.java
new file mode 100644
index 0000000..7f855d9
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/HttpPost.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2015 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.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * HttpPost utility functions.
+ */
+public final class HttpPost {
+    private static final String TAG = "HttpPost";
+
+    private HttpPost() {}
+
+    /**
+     * Executes a post request using {@link HttpURLConnection}.
+     *
+     * @param url The request URL.
+     * @param data The request body, or null.
+     * @param requestProperties Request properties, or null.
+     * @return The response code and body.
+     * @throws IOException If an error occurred making the request.
+     */
+    public static Pair<Integer, byte[]> execute(String url, byte[] data,
+            Map<String, String> requestProperties) throws IOException {
+        HttpURLConnection urlConnection = null;
+        try {
+            urlConnection = (HttpURLConnection) new URL(url).openConnection();
+            urlConnection.setRequestMethod("POST");
+            urlConnection.setDoOutput(data != null);
+            urlConnection.setDoInput(true);
+            urlConnection.setConnectTimeout(5000);
+            urlConnection.setReadTimeout(5000);
+            if (requestProperties != null) {
+                for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
+                    urlConnection.setRequestProperty(requestProperty.getKey(),
+                            requestProperty.getValue());
+                }
+            }
+            // Write the request body, if there is one.
+            if (data != null) {
+                OutputStream out = urlConnection.getOutputStream();
+                try {
+                    out.write(data);
+                } finally {
+                    out.close();
+                }
+            }
+            // Read the response code.
+            int responseCode = urlConnection.getResponseCode();
+            // Read the response body.
+            InputStream inputStream = urlConnection.getInputStream();
+            try {
+                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+                byte scratch[] = new byte[1024];
+                int bytesRead;
+                while ((bytesRead = inputStream.read(scratch)) != -1) {
+                    byteArrayOutputStream.write(scratch, 0, bytesRead);
+                }
+                byte[] responseBody = byteArrayOutputStream.toByteArray();
+                Log.d(TAG, "responseCode=" + responseCode + ", length=" + responseBody.length);
+                return Pair.create(responseCode, responseBody);
+            } finally {
+                inputStream.close();
+            }
+        } finally {
+            if (urlConnection != null) {
+                urlConnection.disconnect();
+            }
+        }
+    }
+
+}
diff --git a/tests/tests/security/src/android/security/cts/KeyRequester.java b/tests/tests/security/src/android/security/cts/KeyRequester.java
new file mode 100644
index 0000000..26a3337
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/KeyRequester.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2015 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.media.DeniedByServerException;
+import android.media.MediaDrm;
+import android.media.NotProvisionedException;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class KeyRequester {
+    private final String TAG = "KeyRequester";
+    private final int MAX_RETRY_COUNT = 3;
+    private final int POOL_SIZE = 1;
+    private final int POOL_TERMINATION_MS_TIMEOUT = 3000;  // in milliseconds
+    private final int KEYREQUEST_MS_TIMEOUT = 5000;  // in milliseconds
+
+    private byte[] mPssh;
+    private ExecutorService mExecutorService;
+    private Future<byte[]> mFuture;
+    private String mDefaultHeartbeatUrl;
+    private String mServerUrl;
+
+    public KeyRequester(byte[] pssh, String url) {
+        mPssh = pssh;
+        mServerUrl = url;
+    }
+
+    public final String getDefaultHeartbeatUrl() {
+        return mDefaultHeartbeatUrl;
+    }
+
+    public byte[] doTransact(final MediaDrm drm, final byte[] sessionId, final int keyType) {
+        boolean retryRequest;
+        boolean retryTransaction;
+        byte[] keySetIdResult;
+        int getKeyRequestRetryCount;
+        int provisioningRetryCount = 0;
+        MediaDrm.KeyRequest drmRequest;
+
+        mExecutorService = Executors.newFixedThreadPool(POOL_SIZE);
+
+        do {
+            drmRequest = null;
+            getKeyRequestRetryCount = 0;
+            keySetIdResult = null;
+            retryTransaction = false;
+
+            do {
+                retryRequest = false;
+
+                try {
+                    drmRequest = drm.getKeyRequest(sessionId, mPssh,
+                        "video/avc", keyType, null);
+                } catch (NotProvisionedException e) {
+                    Log.i(TAG, "Invalid certificate, reprovisioning");
+                    ProvisionRequester provisionRequester = new ProvisionRequester();
+                    provisionRequester.doTransact(drm);
+                    retryRequest = true;
+                }
+            } while (retryRequest && ++getKeyRequestRetryCount < MAX_RETRY_COUNT);
+
+            if (drmRequest == null) {
+                Log.e(TAG, "Failed to get key request");
+                return null;
+            }
+
+            try {
+                mFuture = mExecutorService.submit(new KeyRequesterTask(mServerUrl, drmRequest));
+            } catch (RejectedExecutionException e) {
+                Log.e(TAG, "Failed to submit KeyRequesterTask for execution", e);
+                if (++provisioningRetryCount < MAX_RETRY_COUNT) {
+                    continue;
+                } else {
+                    break;
+                }
+            }
+
+            try {
+                byte[] responseBody = mFuture.get(KEYREQUEST_MS_TIMEOUT, TimeUnit.MILLISECONDS);
+                if (responseBody == null) {
+                    Log.e(TAG, "No response from license server!");
+                    retryTransaction = true;
+                } else {
+                    byte[] drmResponse = parseResponseBody(responseBody);
+                    try {
+                        keySetIdResult = drm.provideKeyResponse(sessionId, drmResponse);
+                    } catch (NotProvisionedException e) {
+                        Log.i(TAG, "Response invalidated the certificate, reprovisioning");
+                        ProvisionRequester provisionRequester = new ProvisionRequester();
+                        provisionRequester.doTransact(drm);
+                        retryTransaction = true;
+                    } catch (DeniedByServerException e) {
+                        // informational, the event handler will take care of provisioning
+                        Log.i(TAG, "Server rejected the key request");
+                    }  catch (IllegalStateException e) {
+                        Log.e(TAG, "provideKeyResponse failed", e);
+                    }
+
+                    try {
+                        // first call to getKeyRequest does not return heartbeat url
+                        drmRequest = drm.getKeyRequest(sessionId, mPssh, "video/avc",
+                                keyType, null);
+                        try {
+                            mDefaultHeartbeatUrl = drmRequest.getDefaultUrl();
+                        } catch (Exception e) {
+                            // ignore
+                        }
+                    } catch (NotProvisionedException e) {
+                        Log.e(TAG, "Fails to get heartbeat url");
+                    }
+                    break;
+                }
+            } catch (ExecutionException | InterruptedException ex) {
+                Log.e(TAG, "Failed to execute KeyRequesterTask", ex);
+                shutdownAndAwaitTermination(mExecutorService);
+                return null;
+            } catch (TimeoutException te) {
+                // The request timed out. The network is possibly too slow.
+                // Cancel the running task.
+                Log.d(TAG, "Request timed out, retry...");
+                mFuture.cancel(true);
+                retryTransaction = true;
+            }
+        } while (retryTransaction && ++provisioningRetryCount < MAX_RETRY_COUNT);
+
+        shutdownAndAwaitTermination(mExecutorService);
+        return keySetIdResult;
+    }
+
+    private void shutdownAndAwaitTermination(ExecutorService pool) {
+        pool.shutdown();  // disable new tasks from being submitted
+        try {
+            // wait for existing tasks to terminate
+            if (!pool.awaitTermination(POOL_TERMINATION_MS_TIMEOUT, TimeUnit.MILLISECONDS)) {
+                pool.shutdownNow();
+                // wait for tasks to respond to being cancelled
+                if (!pool.awaitTermination(POOL_TERMINATION_MS_TIMEOUT, TimeUnit.MILLISECONDS))
+                    Log.e(TAG, "Pool did not terminate");
+            }
+        } catch (InterruptedException ie) {
+            // (Re-)Cancel if current thread also interrupted
+            pool.shutdownNow();
+            // Preserve interrupt status
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    // Validate the response body and return the drmResponse blob.
+    private byte[] parseResponseBody(byte[] responseBody) {
+        String bodyString = null;
+        try {
+            bodyString = new String(responseBody, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+        }
+
+        if (bodyString == null) {
+            return null;
+        }
+
+        if (bodyString.startsWith("GLS/")) {
+            if (!bodyString.startsWith("GLS/1.")) {
+                Log.e(TAG, "Invalid server version, expected 1.x");
+                return null;
+            }
+            int drmMessageOffset = bodyString.indexOf("\r\n\r\n");
+            if (drmMessageOffset == -1) {
+                Log.e(TAG, "Invalid server response, could not locate drm message");
+                return null;
+            }
+            responseBody = Arrays.copyOfRange(responseBody, drmMessageOffset + 4,
+                    responseBody.length);
+        }
+        return responseBody;
+    }
+}
diff --git a/tests/tests/security/src/android/security/cts/KeyRequesterTask.java b/tests/tests/security/src/android/security/cts/KeyRequesterTask.java
new file mode 100644
index 0000000..c4d96dd
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/KeyRequesterTask.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2015 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.media.MediaDrm;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.IOException;
+import java.util.concurrent.Callable;
+import java.util.HashMap;
+
+public class KeyRequesterTask implements Callable<byte[]> {
+    private static final String TAG = "KeyRequesterTask";
+    private final MediaDrm.KeyRequest mDrmRequest;
+    private final String mUrl;
+
+    public KeyRequesterTask(String url, MediaDrm.KeyRequest drmRequest) {
+        mDrmRequest = drmRequest;
+        mUrl = url;
+    }
+
+    /**
+     * @return a byte array containing the license response if successful,
+     * {@code null} otherwise.
+     */
+    @Override
+    public byte[] call() throws Exception {
+        byte[] drmRequest = mDrmRequest.getData();
+        Log.d(TAG, "PostRequest:" + mUrl);
+
+        HashMap<String, String> headers = new HashMap<>();
+        headers.put("User-Agent", "Widevine CDM v1.0");
+        headers.put("Connection", "close");
+
+        try {
+            Pair<Integer, byte[]> response = HttpPost.execute(mUrl, drmRequest, headers);
+            int responseCode = response.first;
+            if (responseCode != 200) {
+                Log.d(TAG, "Server returned HTTP error code " + responseCode);
+                return null;
+            }
+            return response.second;
+        } catch (IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+}
+
diff --git a/tests/tests/security/src/android/security/cts/MediaCryptoTest.java b/tests/tests/security/src/android/security/cts/MediaCryptoTest.java
new file mode 100644
index 0000000..b5639a7
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/MediaCryptoTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2015 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.media.MediaCrypto;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.MediaDrmException;
+import android.media.NotProvisionedException;
+import android.media.ResourceBusyException;
+import android.test.AndroidTestCase;
+import android.util.Log;
+import java.util.UUID;
+
+public class MediaCryptoTest extends AndroidTestCase {
+    private static final String TAG = "MediaCryptoTest";
+
+    private static final UUID CLEARKEY_SCHEME_UUID =
+        new UUID(0x1077efecc0b24d02L, 0xace33c1e52e2fb4bL);
+    private static final UUID WIDEVINE_SCHEME_UUID =
+        new UUID(0xedef8ba979d64aceL, 0xa3c827dcd51d21edL);
+
+    static {
+        System.loadLibrary("ctssecurity_jni");
+    }
+
+    private native boolean validateCryptoNative(MediaCrypto crypto);
+
+    public void testMediaCryptoClearKey() throws Exception {
+        MediaCrypto crypto = null;
+        if (!MediaDrm.isCryptoSchemeSupported(CLEARKEY_SCHEME_UUID)) {
+            Log.i(TAG, "No ClearKey plugin, skipping test");
+            return;
+        }
+        try {
+            byte[] initData = new byte[0];
+            crypto = new MediaCrypto(CLEARKEY_SCHEME_UUID, initData);
+        } catch (MediaCryptoException e) {
+            throw new Error("Failed to create MediaCrypto using ClearKey plugin");
+        }
+
+        assertTrue("MediaCrypto validation failed", validateCryptoNative(crypto));
+    }
+
+    public void testMediaCryptoWidevine() throws Exception {
+        if (!MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID)) {
+            Log.i(TAG, "No Widevine plugin, skipping test");
+            return;
+        }
+
+        MediaDrm drm = null;
+        byte[] sessionId = null;
+
+        try {
+            drm = new MediaDrm(WIDEVINE_SCHEME_UUID);
+            sessionId = openSession(drm);
+            getWidevineKeys(drm, sessionId);
+            MediaCrypto crypto = new MediaCrypto(WIDEVINE_SCHEME_UUID, sessionId);
+            assertTrue("MediaCrypto validation failed", validateCryptoNative(crypto));
+        } catch (MediaCryptoException | MediaDrmException e) {
+            if (drm != null && sessionId != null) {
+                drm.closeSession(sessionId);
+            }
+            throw e;
+        }
+    }
+
+    private byte[] openSession(MediaDrm drm) throws Exception {
+        byte[] sessionId = null;
+        int retryCount = 3;
+        while (retryCount-- > 0) {
+            try {
+                return drm.openSession();
+            } catch (NotProvisionedException e) {
+                Log.i(TAG, "Missing certificate, provisioning");
+                ProvisionRequester provisionRequester = new ProvisionRequester();
+                provisionRequester.doTransact(drm);
+            } catch (ResourceBusyException e) {
+                Log.w(TAG, "Resource busy in openSession, retrying...");
+                try {
+                    Thread.sleep(1000);
+                } catch (InterruptedException ie) {
+                    // ignore
+                }
+            }
+        }
+        throw new Error("Failed to open session");
+    }
+
+    private void getWidevineKeys(MediaDrm drm, byte[] sessionId) throws Exception {
+        final String kKeyServerUrl = "https://jmt17.google.com/video/license/GetCencLicense";
+        final byte[] kPssh = hex2ba("08011210e02562e04cd55351b14b3d748d36ed8e");
+        final String kClientAuth = "?source=YOUTUBE&video_id=EGHC6OHNbOo&oauth=ya.gtsqawidevine";
+        final String kPort = "80";
+        KeyRequester keyRequester = new KeyRequester(kPssh, kKeyServerUrl + ":" + kPort + kClientAuth);
+        if (keyRequester.doTransact(drm, sessionId, MediaDrm.KEY_TYPE_STREAMING) == null) {
+            throw new Error("Failed to get keys from license server!");
+        }
+    }
+
+    private static byte[] hex2ba(String s) {
+        int len = s.length();
+        byte[] data = new byte[len / 2];
+        for (int i = 0; i < len; i += 2) {
+            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+                                  + Character.digit(s.charAt(i+1), 16));
+        }
+        return data;
+    }
+}
diff --git a/tests/tests/security/src/android/security/cts/ProvisionRequester.java b/tests/tests/security/src/android/security/cts/ProvisionRequester.java
new file mode 100644
index 0000000..ee1cb9e
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/ProvisionRequester.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2015 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.media.DeniedByServerException;
+import android.media.MediaDrm;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+public class ProvisionRequester {
+    private final String TAG = "ProvisionRequester";
+
+    public ProvisionRequester() {
+    }
+
+    public void doTransact(final MediaDrm drm) {
+        Thread t = new Thread() {
+            @Override
+            public void run() {
+                MediaDrm.ProvisionRequest drmRequest;
+                drmRequest = drm.getProvisionRequest();
+                byte[] responseBody = postRequest(drmRequest.getDefaultUrl(),
+                        drmRequest.getData());
+
+                if (responseBody == null) {
+                    Log.e(TAG, "No response from provisioning server!");
+                } else {
+                    try {
+                        drm.provideProvisionResponse(responseBody);
+                    } catch (DeniedByServerException e) {
+                        Log.e(TAG, "Server denied provisioning request");
+                    }
+                }
+            }
+        };
+        t.start();
+
+        try {
+            t.join();
+        } catch (InterruptedException e) {
+        }
+    }
+
+    // TODO May want to throw exceptions without having try/catch in body.
+    private byte[] postRequest(String url, byte[] drmRequest) {
+        String signedUrl = url + "&signedRequest=" + new String(drmRequest);
+        Log.d(TAG, "PostRequest:" + signedUrl);
+
+        HashMap<String, String> headers = new HashMap<>();
+        headers.put("Accept", "*/*");
+        headers.put("User-Agent", "Widevine CDM v1.0");
+        headers.put("Content-Type", "application/json");
+        headers.put("Connection", "close");
+
+        try {
+            Pair<Integer, byte[]> response = HttpPost.execute(signedUrl, null, headers);
+            int responseCode = response.first;
+            if (responseCode != 200) {
+                Log.e(TAG, "Server returned HTTP error code " + responseCode);
+                return null;
+            }
+            return response.second;
+        } catch (IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    private void sleep(int msec) {
+        try {
+            Thread.sleep(msec);
+        } catch (InterruptedException e) {
+        }
+    }
+}