summaryrefslogtreecommitdiff
path: root/packages/DynamicSystemInstallationService
diff options
context:
space:
mode:
authorYo Chiang <yochiang@google.com>2020-02-05 19:01:54 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2020-02-05 19:01:54 +0000
commit739c45ea2364d880d08bfc9a27fdc3cec0f7eab6 (patch)
treec69711e66c727a15a10302ab1765985751623237 /packages/DynamicSystemInstallationService
parent05cf3e896c6d3e40c11ca3767bde535433636a6d (diff)
parent61fc692d6675a6b2f35c7ff53ea8fa88728a1e39 (diff)
Merge "DSU to support GSI key revocation list"
Diffstat (limited to 'packages/DynamicSystemInstallationService')
-rw-r--r--packages/DynamicSystemInstallationService/res/values/strings.xml3
-rw-r--r--packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java19
-rw-r--r--packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java30
-rw-r--r--packages/DynamicSystemInstallationService/src/com/android/dynsystem/KeyRevocationList.java148
-rw-r--r--packages/DynamicSystemInstallationService/tests/Android.bp15
-rw-r--r--packages/DynamicSystemInstallationService/tests/AndroidManifest.xml29
-rw-r--r--packages/DynamicSystemInstallationService/tests/res/values/strings.xml19
-rw-r--r--packages/DynamicSystemInstallationService/tests/src/com/android/dynsystem/KeyRevocationListTest.java132
8 files changed, 393 insertions, 2 deletions
diff --git a/packages/DynamicSystemInstallationService/res/values/strings.xml b/packages/DynamicSystemInstallationService/res/values/strings.xml
index 9bd5be7b0dd7..7595d2b1eea3 100644
--- a/packages/DynamicSystemInstallationService/res/values/strings.xml
+++ b/packages/DynamicSystemInstallationService/res/values/strings.xml
@@ -35,4 +35,7 @@
<!-- Toast when we fail to launch into Dynamic System [CHAR LIMIT=64] -->
<string name="toast_failed_to_reboot_to_dynsystem">Can\u2019t restart or load dynamic system</string>
+ <!-- URL of Dynamic System Key Revocation List [DO NOT TRANSLATE] -->
+ <string name="key_revocation_list_url" translatable="false">https://dl.google.com/developers/android/gsi/gsi-keyblacklist.json</string>
+
</resources>
diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java
index 9ccb837cf613..9bae223a0a3e 100644
--- a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java
+++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java
@@ -46,6 +46,7 @@ import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
+import android.net.http.HttpResponseCache;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -60,6 +61,8 @@ import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
+import java.io.File;
+import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -146,10 +149,26 @@ public class DynamicSystemInstallationService extends Service
prepareNotification();
mDynSystem = (DynamicSystemManager) getSystemService(Context.DYNAMIC_SYSTEM_SERVICE);
+
+ // Install an HttpResponseCache in the application cache directory so we can cache
+ // gsi key revocation list. The http(s) protocol handler uses this cache transparently.
+ // The cache size is chosen heuristically. Since we don't have too much traffic right now,
+ // a moderate size of 1MiB should be enough.
+ try {
+ File httpCacheDir = new File(getCacheDir(), "httpCache");
+ long httpCacheSize = 1 * 1024 * 1024; // 1 MiB
+ HttpResponseCache.install(httpCacheDir, httpCacheSize);
+ } catch (IOException e) {
+ Log.d(TAG, "HttpResponseCache.install() failed: " + e);
+ }
}
@Override
public void onDestroy() {
+ HttpResponseCache cache = HttpResponseCache.getInstalled();
+ if (cache != null) {
+ cache.flush();
+ }
// Cancel the persistent notification.
mNM.cancel(NOTIFICATION_ID);
}
diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java
index 9aea0e713179..438c435ef0e4 100644
--- a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java
+++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java
@@ -25,6 +25,8 @@ import android.os.image.DynamicSystemManager;
import android.util.Log;
import android.webkit.URLUtil;
+import org.json.JSONException;
+
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
@@ -100,7 +102,9 @@ class InstallationAsyncTask extends AsyncTask<String, InstallationAsyncTask.Prog
private final Context mContext;
private final DynamicSystemManager mDynSystem;
private final ProgressListener mListener;
+ private final boolean mIsNetworkUrl;
private DynamicSystemManager.Session mInstallationSession;
+ private KeyRevocationList mKeyRevocationList;
private boolean mIsZip;
private boolean mIsCompleted;
@@ -123,6 +127,7 @@ class InstallationAsyncTask extends AsyncTask<String, InstallationAsyncTask.Prog
mContext = context;
mDynSystem = dynSystem;
mListener = listener;
+ mIsNetworkUrl = URLUtil.isNetworkUrl(mUrl);
}
@Override
@@ -152,9 +157,11 @@ class InstallationAsyncTask extends AsyncTask<String, InstallationAsyncTask.Prog
return null;
}
+ // TODO(yochiang): do post-install public key check (revocation list / boot-ramdisk)
+
mDynSystem.finishInstallation();
} catch (Exception e) {
- e.printStackTrace();
+ Log.e(TAG, e.toString(), e);
mDynSystem.remove();
return e;
} finally {
@@ -220,7 +227,7 @@ class InstallationAsyncTask extends AsyncTask<String, InstallationAsyncTask.Prog
String.format(Locale.US, "Unsupported file format: %s", mUrl));
}
- if (URLUtil.isNetworkUrl(mUrl)) {
+ if (mIsNetworkUrl) {
mStream = new URL(mUrl).openStream();
} else if (URLUtil.isFileUrl(mUrl)) {
if (mIsZip) {
@@ -234,6 +241,25 @@ class InstallationAsyncTask extends AsyncTask<String, InstallationAsyncTask.Prog
throw new UnsupportedUrlException(
String.format(Locale.US, "Unsupported URL: %s", mUrl));
}
+
+ // TODO(yochiang): Bypass this check if device is unlocked
+ try {
+ String listUrl = mContext.getString(R.string.key_revocation_list_url);
+ mKeyRevocationList = KeyRevocationList.fromUrl(new URL(listUrl));
+ } catch (IOException | JSONException e) {
+ Log.d(TAG, "Failed to fetch Dynamic System Key Revocation List");
+ mKeyRevocationList = new KeyRevocationList();
+ keyRevocationThrowOrWarning(e);
+ }
+ }
+
+ private void keyRevocationThrowOrWarning(Exception e) throws Exception {
+ if (mIsNetworkUrl) {
+ throw e;
+ } else {
+ // If DSU is being installed from a local file URI, then be permissive
+ Log.w(TAG, e.toString());
+ }
}
private void installUserdata() throws Exception {
diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/KeyRevocationList.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/KeyRevocationList.java
new file mode 100644
index 000000000000..522bc547325b
--- /dev/null
+++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/KeyRevocationList.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2019 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 com.android.dynsystem;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.HashMap;
+
+class KeyRevocationList {
+
+ private static final String TAG = "KeyRevocationList";
+
+ private static final String JSON_ENTRIES = "entries";
+ private static final String JSON_PUBLIC_KEY = "public_key";
+ private static final String JSON_STATUS = "status";
+ private static final String JSON_REASON = "reason";
+
+ private static final String STATUS_REVOKED = "REVOKED";
+
+ @VisibleForTesting
+ HashMap<String, RevocationStatus> mEntries;
+
+ static class RevocationStatus {
+ final String mStatus;
+ final String mReason;
+
+ RevocationStatus(String status, String reason) {
+ mStatus = status;
+ mReason = reason;
+ }
+ }
+
+ KeyRevocationList() {
+ mEntries = new HashMap<String, RevocationStatus>();
+ }
+
+ /**
+ * Returns the revocation status of a public key.
+ *
+ * @return a RevocationStatus for |publicKey|, null if |publicKey| doesn't exist.
+ */
+ RevocationStatus getRevocationStatusForKey(String publicKey) {
+ return mEntries.get(publicKey);
+ }
+
+ /** Test if a public key is revoked or not. */
+ boolean isRevoked(String publicKey) {
+ RevocationStatus entry = getRevocationStatusForKey(publicKey);
+ return entry != null && TextUtils.equals(entry.mStatus, STATUS_REVOKED);
+ }
+
+ @VisibleForTesting
+ void addEntry(String publicKey, String status, String reason) {
+ mEntries.put(publicKey, new RevocationStatus(status, reason));
+ }
+
+ /**
+ * Creates a KeyRevocationList from a JSON String.
+ *
+ * @param jsonString the revocation list, for example:
+ * <pre>{@code
+ * {
+ * "entries": [
+ * {
+ * "public_key": "00fa2c6637c399afa893fe83d85f3569998707d5",
+ * "status": "REVOKED",
+ * "reason": "Revocation Reason"
+ * }
+ * ]
+ * }
+ * }</pre>
+ *
+ * @throws JSONException if |jsonString| is malformed.
+ */
+ static KeyRevocationList fromJsonString(String jsonString) throws JSONException {
+ JSONObject jsonObject = new JSONObject(jsonString);
+ KeyRevocationList list = new KeyRevocationList();
+ Log.d(TAG, "Begin of revocation list");
+ if (jsonObject.has(JSON_ENTRIES)) {
+ JSONArray entries = jsonObject.getJSONArray(JSON_ENTRIES);
+ for (int i = 0; i < entries.length(); ++i) {
+ JSONObject entry = entries.getJSONObject(i);
+ String publicKey = entry.getString(JSON_PUBLIC_KEY);
+ String status = entry.getString(JSON_STATUS);
+ String reason = entry.has(JSON_REASON) ? entry.getString(JSON_REASON) : "";
+ list.addEntry(publicKey, status, reason);
+ Log.d(TAG, "Revocation entry: " + entry.toString());
+ }
+ }
+ Log.d(TAG, "End of revocation list");
+ return list;
+ }
+
+ /**
+ * Creates a KeyRevocationList from a URL.
+ *
+ * @throws IOException if |url| is inaccessible.
+ * @throws JSONException if fetched content is malformed.
+ */
+ static KeyRevocationList fromUrl(URL url) throws IOException, JSONException {
+ Log.d(TAG, "Fetch from URL: " + url.toString());
+ // Force "conditional GET"
+ // Force validate the cached result with server each time, and use the cached result
+ // only if it is validated by server, else fetch new data from server.
+ // Ref: https://developer.android.com/reference/android/net/http/HttpResponseCache#force-a-network-response
+ URLConnection connection = url.openConnection();
+ connection.setUseCaches(true);
+ connection.addRequestProperty("Cache-Control", "max-age=0");
+ try (InputStream stream = connection.getInputStream()) {
+ return fromJsonString(readFully(stream));
+ }
+ }
+
+ private static String readFully(InputStream in) throws IOException {
+ int n;
+ byte[] buffer = new byte[4096];
+ StringBuilder builder = new StringBuilder();
+ while ((n = in.read(buffer, 0, 4096)) > -1) {
+ builder.append(new String(buffer, 0, n));
+ }
+ return builder.toString();
+ }
+}
diff --git a/packages/DynamicSystemInstallationService/tests/Android.bp b/packages/DynamicSystemInstallationService/tests/Android.bp
new file mode 100644
index 000000000000..3bdf82966889
--- /dev/null
+++ b/packages/DynamicSystemInstallationService/tests/Android.bp
@@ -0,0 +1,15 @@
+android_test {
+ name: "DynamicSystemInstallationServiceTests",
+
+ srcs: ["src/**/*.java"],
+ static_libs: [
+ "androidx.test.runner",
+ "androidx.test.rules",
+ "mockito-target-minus-junit4",
+ ],
+
+ resource_dirs: ["res"],
+ platform_apis: true,
+ instrumentation_for: "DynamicSystemInstallationService",
+ certificate: "platform",
+}
diff --git a/packages/DynamicSystemInstallationService/tests/AndroidManifest.xml b/packages/DynamicSystemInstallationService/tests/AndroidManifest.xml
new file mode 100644
index 000000000000..f5f0ae6adaba
--- /dev/null
+++ b/packages/DynamicSystemInstallationService/tests/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dynsystem.tests">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.dynsystem"
+ android:label="Tests for DynamicSystemInstallationService" />
+
+</manifest>
diff --git a/packages/DynamicSystemInstallationService/tests/res/values/strings.xml b/packages/DynamicSystemInstallationService/tests/res/values/strings.xml
new file mode 100644
index 000000000000..fdb620bfe094
--- /dev/null
+++ b/packages/DynamicSystemInstallationService/tests/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- testFromJsonString -->
+ <string name="blacklist_json_string" translatable="false">
+ {
+ \"entries\":[
+ {
+ \"public_key\":\"00fa2c6637c399afa893fe83d85f3569998707d5\",
+ \"status\":\"REVOKED\",
+ \"reason\":\"Key revocation test key\"
+ },
+ {
+ \"public_key\":\"key2\",
+ \"status\":\"REVOKED\"
+ }
+ ]
+ }
+ </string>
+</resources>
diff --git a/packages/DynamicSystemInstallationService/tests/src/com/android/dynsystem/KeyRevocationListTest.java b/packages/DynamicSystemInstallationService/tests/src/com/android/dynsystem/KeyRevocationListTest.java
new file mode 100644
index 000000000000..82ce542cf5de
--- /dev/null
+++ b/packages/DynamicSystemInstallationService/tests/src/com/android/dynsystem/KeyRevocationListTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019 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 com.android.dynsystem;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.json.JSONException;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+/**
+ * A test for KeyRevocationList.java
+ */
+@RunWith(AndroidJUnit4.class)
+public class KeyRevocationListTest {
+
+ private static final String TAG = "KeyRevocationListTest";
+
+ private static Context sContext;
+
+ private static String sBlacklistJsonString;
+
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ sContext = InstrumentationRegistry.getInstrumentation().getContext();
+ sBlacklistJsonString =
+ sContext.getString(com.android.dynsystem.tests.R.string.blacklist_json_string);
+ }
+
+ @Test
+ @SmallTest
+ public void testFromJsonString() throws JSONException {
+ KeyRevocationList blacklist;
+ blacklist = KeyRevocationList.fromJsonString(sBlacklistJsonString);
+ Assert.assertNotNull(blacklist);
+ Assert.assertFalse(blacklist.mEntries.isEmpty());
+ blacklist = KeyRevocationList.fromJsonString("{}");
+ Assert.assertNotNull(blacklist);
+ Assert.assertTrue(blacklist.mEntries.isEmpty());
+ }
+
+ @Test
+ @SmallTest
+ public void testFromUrl() throws IOException, JSONException {
+ URLConnection mockConnection = mock(URLConnection.class);
+ doReturn(new ByteArrayInputStream(sBlacklistJsonString.getBytes()))
+ .when(mockConnection).getInputStream();
+ URL mockUrl = new URL(
+ "http", // protocol
+ "foo.bar", // host
+ 80, // port
+ "baz", // file
+ new URLStreamHandler() {
+ @Override
+ protected URLConnection openConnection(URL url) {
+ return mockConnection;
+ }
+ });
+ URL mockBadUrl = new URL(
+ "http", // protocol
+ "foo.bar", // host
+ 80, // port
+ "baz", // file
+ new URLStreamHandler() {
+ @Override
+ protected URLConnection openConnection(URL url) throws IOException {
+ throw new IOException();
+ }
+ });
+
+ KeyRevocationList blacklist = KeyRevocationList.fromUrl(mockUrl);
+ Assert.assertNotNull(blacklist);
+ Assert.assertFalse(blacklist.mEntries.isEmpty());
+
+ blacklist = null;
+ try {
+ blacklist = KeyRevocationList.fromUrl(mockBadUrl);
+ // Up should throw, down should be unreachable
+ Assert.fail("Expected IOException not thrown");
+ } catch (IOException e) {
+ // This is expected, do nothing
+ }
+ Assert.assertNull(blacklist);
+ }
+
+ @Test
+ @SmallTest
+ public void testIsRevoked() {
+ KeyRevocationList blacklist = new KeyRevocationList();
+ blacklist.addEntry("key1", "REVOKED", "reason for key1");
+
+ KeyRevocationList.RevocationStatus revocationStatus =
+ blacklist.getRevocationStatusForKey("key1");
+ Assert.assertNotNull(revocationStatus);
+ Assert.assertEquals(revocationStatus.mReason, "reason for key1");
+
+ revocationStatus = blacklist.getRevocationStatusForKey("key2");
+ Assert.assertNull(revocationStatus);
+
+ Assert.assertTrue(blacklist.isRevoked("key1"));
+ Assert.assertFalse(blacklist.isRevoked("key2"));
+ }
+}