diff options
10 files changed, 400 insertions, 19 deletions
diff --git a/core/java/android/transparency/BinaryTransparencyManager.java b/core/java/android/transparency/BinaryTransparencyManager.java index f6d7c611e9d9..d77bbcc836ec 100644 --- a/core/java/android/transparency/BinaryTransparencyManager.java +++ b/core/java/android/transparency/BinaryTransparencyManager.java @@ -19,6 +19,7 @@ package android.transparency; import android.annotation.NonNull; import android.annotation.SystemService; import android.content.Context; +import android.os.Bundle; import android.os.RemoteException; import android.util.Slog; @@ -83,4 +84,36 @@ public class BinaryTransparencyManager { } } + /** + * Collects the APEX information on the device. + * + * @param includeTestOnly Whether to include test only data in the returned ApexInfo. + * @return A List containing the APEX info. + * @hide + */ + @NonNull + public List<IBinaryTransparencyService.ApexInfo> collectAllApexInfo(boolean includeTestOnly) { + try { + return mService.collectAllApexInfo(includeTestOnly); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Collects the updated preload information on the device. + * + * @return A List containing the preload info. + * @hide + */ + @NonNull + public List<IBinaryTransparencyService.AppInfo> collectAllUpdatedPreloadInfo( + Bundle packagesToSkip) { + try { + Slog.d(TAG, "Calling backend's collectAllUpdatedPreloadInfo()"); + return mService.collectAllUpdatedPreloadInfo(packagesToSkip); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/com/android/internal/os/IBinaryTransparencyService.aidl b/core/java/com/android/internal/os/IBinaryTransparencyService.aidl index b4a0aac4d477..fb343452342b 100644 --- a/core/java/com/android/internal/os/IBinaryTransparencyService.aidl +++ b/core/java/com/android/internal/os/IBinaryTransparencyService.aidl @@ -16,6 +16,8 @@ package com.android.internal.os; +import android.os.Bundle; + /** * "Backend" interface used by {@link android.os.BinaryTransparencyManager} to talk to the * BinaryTransparencyService that actually implements the measurement and information aggregation @@ -36,6 +38,9 @@ interface IBinaryTransparencyService { byte[] digest; int digestAlgorithm; String[] signerDigests; + + // Test only + String moduleName; } parcelable AppInfo { @@ -50,4 +55,8 @@ interface IBinaryTransparencyService { String installer; String originator; } + + /** Test only */ + List<ApexInfo> collectAllApexInfo(boolean includeTestOnly); + List<AppInfo> collectAllUpdatedPreloadInfo(in Bundle packagesToSkip); }
\ No newline at end of file diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index c17a2ece315c..6fdbc0df7c2f 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -93,10 +93,8 @@ import java.io.PrintWriter; import java.security.PublicKey; import java.security.cert.CertificateException; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; @@ -300,15 +298,16 @@ public class BinaryTransparencyService extends SystemService { + " and is now updated to: " + currentTimeMs); mMeasurementsLastRecordedMs = currentTimeMs; - Set<String> packagesMeasured = new HashSet<>(); + Bundle packagesMeasured = new Bundle(); // measure all APEXs first if (DEBUG) { Slog.d(TAG, "Measuring APEXs..."); } - List<IBinaryTransparencyService.ApexInfo> allApexInfo = collectAllApexInfo(); + List<IBinaryTransparencyService.ApexInfo> allApexInfo = collectAllApexInfo( + /* includeTestOnly */ false); for (IBinaryTransparencyService.ApexInfo apexInfo : allApexInfo) { - packagesMeasured.add(apexInfo.packageName); + packagesMeasured.putBoolean(apexInfo.packageName, true); recordApexInfo(apexInfo); } @@ -321,7 +320,7 @@ public class BinaryTransparencyService extends SystemService { List<IBinaryTransparencyService.AppInfo> allUpdatedPreloadInfo = collectAllUpdatedPreloadInfo(packagesMeasured); for (IBinaryTransparencyService.AppInfo appInfo : allUpdatedPreloadInfo) { - packagesMeasured.add(appInfo.packageName); + packagesMeasured.putBoolean(appInfo.packageName, true); writeAppInfoToLog(appInfo); } if (DEBUG) { @@ -334,7 +333,7 @@ public class BinaryTransparencyService extends SystemService { List<IBinaryTransparencyService.AppInfo> allMbaInfo = collectAllMbaInfo(packagesMeasured); for (IBinaryTransparencyService.AppInfo appInfo : allUpdatedPreloadInfo) { - packagesMeasured.add(appInfo.packageName); + packagesMeasured.putBoolean(appInfo.packageName, true); writeAppInfoToLog(appInfo); } } @@ -345,7 +344,9 @@ public class BinaryTransparencyService extends SystemService { } } - private List<IBinaryTransparencyService.ApexInfo> collectAllApexInfo() { + @Override + public List<IBinaryTransparencyService.ApexInfo> collectAllApexInfo( + boolean includeTestOnly) { var results = new ArrayList<IBinaryTransparencyService.ApexInfo>(); for (PackageInfo packageInfo : getCurrentInstalledApexs()) { PackageState packageState = mPackageManagerInternal.getPackageStateInternal( @@ -371,13 +372,19 @@ public class BinaryTransparencyService extends SystemService { apexInfo.signerDigests = computePackageSignerSha256Digests(packageState.getSigningInfo()); + if (includeTestOnly) { + apexInfo.moduleName = apexPackageNameToModuleName( + packageState.getPackageName()); + } + results.add(apexInfo); } return results; } - private List<IBinaryTransparencyService.AppInfo> collectAllUpdatedPreloadInfo( - Set<String> packagesToSkip) { + @Override + public List<IBinaryTransparencyService.AppInfo> collectAllUpdatedPreloadInfo( + Bundle packagesToSkip) { final var results = new ArrayList<IBinaryTransparencyService.AppInfo>(); PackageManager pm = mContext.getPackageManager(); @@ -385,7 +392,7 @@ public class BinaryTransparencyService extends SystemService { if (!packageState.isUpdatedSystemApp()) { return; } - if (packagesToSkip.contains(packageState.getPackageName())) { + if (packagesToSkip.containsKey(packageState.getPackageName())) { return; } @@ -413,11 +420,10 @@ public class BinaryTransparencyService extends SystemService { return results; } - private List<IBinaryTransparencyService.AppInfo> collectAllMbaInfo( - Set<String> packagesToSkip) { + public List<IBinaryTransparencyService.AppInfo> collectAllMbaInfo(Bundle packagesToSkip) { var results = new ArrayList<IBinaryTransparencyService.AppInfo>(); for (PackageInfo packageInfo : getNewlyInstalledMbas()) { - if (packagesToSkip.contains(packageInfo.packageName)) { + if (packagesToSkip.containsKey(packageInfo.packageName)) { continue; } PackageState packageState = mPackageManagerInternal.getPackageStateInternal( @@ -1654,11 +1660,7 @@ public class BinaryTransparencyService extends SystemService { private String getOriginalApexPreinstalledLocation(String packageName, String currentInstalledLocation) { try { - // It appears that only apexd knows the preinstalled location, and it uses module name - // as the identifier instead of package name. Given the input is a package name, we - // need to covert to module name. - final String moduleName = ApexManager.getInstance().getApexModuleNameForPackageName( - packageName); + final String moduleName = apexPackageNameToModuleName(packageName); IApexService apexService = IApexService.Stub.asInterface( Binder.allowBlocking(ServiceManager.waitForService("apexservice"))); for (ApexInfo info : apexService.getAllPackages()) { @@ -1672,6 +1674,13 @@ public class BinaryTransparencyService extends SystemService { return APEX_PRELOAD_LOCATION_ERROR; } + private String apexPackageNameToModuleName(String packageName) { + // It appears that only apexd knows the preinstalled location, and it uses module name as + // the identifier instead of package name. Given the input is a package name, we need to + // covert to module name. + return ApexManager.getInstance().getApexModuleNameForPackageName(packageName); + } + /** * Wrapper method to call into IBICS to get a list of all newly installed MBAs. * diff --git a/tests/BinaryTransparencyHostTest/Android.bp b/tests/BinaryTransparencyHostTest/Android.bp new file mode 100644 index 000000000000..142e3ddf9fe7 --- /dev/null +++ b/tests/BinaryTransparencyHostTest/Android.bp @@ -0,0 +1,42 @@ +// Copyright (C) 2023 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_test_host { + name: "BinaryTransparencyHostTest", + srcs: ["src/**/*.java"], + libs: [ + "tradefed", + "compatibility-tradefed", + "compatibility-host-util", + ], + static_libs: [ + "truth-prebuilt", + ], + data: [ + ":BinaryTransparencyTestApp", + ":EasterEgg", + ], + test_suites: [ + "general-tests", + ], +} diff --git a/tests/BinaryTransparencyHostTest/AndroidTest.xml b/tests/BinaryTransparencyHostTest/AndroidTest.xml new file mode 100644 index 000000000000..e0d11c0c1097 --- /dev/null +++ b/tests/BinaryTransparencyHostTest/AndroidTest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2023 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. +--> +<configuration description="Binary Transparency integration test"> + <option name="test-suite-tag" value="apct" /> + + <!-- Service is not exposed to apps. Disable SELinux for testing purpose. --> + <target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="BinaryTransparencyTestApp.apk" /> + </target_preparer> + + <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" > + <option name="jar" value="BinaryTransparencyHostTest.jar" /> + <option name="runtime-hint" value="1m" /> + </test> +</configuration> diff --git a/tests/BinaryTransparencyHostTest/OWNERS b/tests/BinaryTransparencyHostTest/OWNERS new file mode 100644 index 000000000000..ca84550c76a4 --- /dev/null +++ b/tests/BinaryTransparencyHostTest/OWNERS @@ -0,0 +1 @@ +include /core/java/android/transparency/OWNERS diff --git a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java new file mode 100644 index 000000000000..84bed92f5117 --- /dev/null +++ b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 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.transparency.test; + +import static org.junit.Assert.assertTrue; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; +import com.android.tradefed.testtype.junit4.DeviceTestRunOptions; +import com.android.tradefed.util.CommandResult; +import com.android.tradefed.util.CommandStatus; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +// TODO: Add @Presubmit +@RunWith(DeviceJUnit4ClassRunner.class) +public final class BinaryTransparencyHostTest extends BaseHostJUnit4Test { + private static final String PACKAGE_NAME = "android.transparency.test.app"; + + @After + public void tearDown() throws Exception { + uninstallPackage("com.android.egg"); + } + + @Test + public void testCollectAllApexInfo() throws Exception { + var options = new DeviceTestRunOptions(PACKAGE_NAME); + options.setTestClassName(PACKAGE_NAME + ".BinaryTransparencyTest"); + options.setTestMethodName("testCollectAllApexInfo"); + + // Collect APEX package names from /apex, then pass them as expectation to be verified. + CommandResult result = getDevice().executeShellV2Command( + "ls -d /apex/*/ |grep -v @ |grep -v /apex/sharedlibs |cut -d/ -f3"); + assertTrue(result.getStatus() == CommandStatus.SUCCESS); + String[] packageNames = result.getStdout().split("\n"); + for (var i = 0; i < packageNames.length; i++) { + options.addInstrumentationArg("apex-" + String.valueOf(i), packageNames[i]); + } + options.addInstrumentationArg("apex-number", Integer.toString(packageNames.length)); + runDeviceTests(options); + } + + @Test + public void testCollectAllUpdatedPreloadInfo() throws Exception { + installPackage("EasterEgg.apk"); + runDeviceTest("testCollectAllUpdatedPreloadInfo"); + } + + @Test + public void testMeasureMbas() throws Exception { + // TODO(265244016): figure out a way to install an MBA + } + + private void runDeviceTest(String method) throws DeviceNotAvailableException { + var options = new DeviceTestRunOptions(PACKAGE_NAME); + options.setTestClassName(PACKAGE_NAME + ".BinaryTransparencyTest"); + options.setTestMethodName(method); + runDeviceTests(options); + } +} diff --git a/tests/BinaryTransparencyHostTest/test-app/Android.bp b/tests/BinaryTransparencyHostTest/test-app/Android.bp new file mode 100644 index 000000000000..b5193ddf7c2c --- /dev/null +++ b/tests/BinaryTransparencyHostTest/test-app/Android.bp @@ -0,0 +1,40 @@ +// Copyright (C) 2023 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test_helper_app { + name: "BinaryTransparencyTestApp", + manifest: "AndroidManifest.xml", + srcs: ["src/**/*.java"], + static_libs: [ + "androidx.test.core", + "compatibility-device-util-axt", + "junit", + ], + test_suites: [ + "general-tests", + ], + platform_apis: true, + dex_preopt: { + enabled: false, + }, +} diff --git a/tests/BinaryTransparencyHostTest/test-app/AndroidManifest.xml b/tests/BinaryTransparencyHostTest/test-app/AndroidManifest.xml new file mode 100644 index 000000000000..42e616e24eb9 --- /dev/null +++ b/tests/BinaryTransparencyHostTest/test-app/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright (C) 2023 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="android.transparency.test.app"> + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:label="APCT tests for binary transparency" + android:targetPackage="android.transparency.test.app" /> +</manifest> diff --git a/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java new file mode 100644 index 000000000000..aedb3666a574 --- /dev/null +++ b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2023 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.transparency.test.app; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import android.content.Context; +import android.os.Bundle; +import android.transparency.BinaryTransparencyManager; +import android.util.Log; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.os.IBinaryTransparencyService.AppInfo; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashSet; +import java.util.HexFormat; +import java.util.stream.Collectors; + +@RunWith(AndroidJUnit4.class) +public class BinaryTransparencyTest { + private static final String TAG = "BinaryTransparencyTest"; + + private BinaryTransparencyManager mBt; + + @Before + public void setUp() { + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + mBt = context.getSystemService(BinaryTransparencyManager.class); + } + + @Test + public void testCollectAllApexInfo() { + // Prepare the expectation received from host's shell command + Bundle args = InstrumentationRegistry.getArguments(); + assertThat(args).isNotNull(); + int number = Integer.valueOf(args.getString("apex-number")); + assertThat(number).isGreaterThan(0); + var expectedApexNames = new HashSet<String>(); + for (var i = 0; i < number; i++) { + String moduleName = args.getString("apex-" + Integer.toString(i)); + expectedApexNames.add(moduleName); + } + + // Action + var apexInfoList = mBt.collectAllApexInfo(/* includeTestOnly */ true); + + // Verify actual apex names + var actualApexesNames = apexInfoList.stream().map((apex) -> apex.moduleName) + .collect(Collectors.toList()); + assertThat(actualApexesNames).containsExactlyElementsIn(expectedApexNames); + + // Perform more valitidy checks + var digestsSeen = new HashSet<String>(); + var hexFormatter = HexFormat.of(); + for (var apex : apexInfoList) { + Log.d(TAG, "Verifying " + apex.packageName + " / " + apex.moduleName); + + assertThat(apex.longVersion).isGreaterThan(0); + assertThat(apex.digestAlgorithm).isGreaterThan(0); + assertThat(apex.signerDigests).asList().containsNoneOf(null, ""); + + assertThat(apex.digest).isNotNull(); + String digestHex = hexFormatter.formatHex(apex.digest); + boolean isNew = digestsSeen.add(digestHex); + assertWithMessage( + "Digest should be unique, but received a dup: " + digestHex) + .that(isNew).isTrue(); + } + } + + @Test + public void testCollectAllUpdatedPreloadInfo() { + var preloadInfoList = mBt.collectAllUpdatedPreloadInfo(new Bundle()); + assertThat(preloadInfoList).isNotEmpty(); // because we just installed from the host side + AppInfo updatedPreload = null; + for (var preload : preloadInfoList) { + Log.d(TAG, "Received " + preload.packageName); + if (preload.packageName.equals("com.android.egg")) { + assertWithMessage("Received the same package").that(updatedPreload).isNull(); + updatedPreload = preload; + } + } + + // Verify + assertThat(updatedPreload.longVersion).isGreaterThan(0); + assertThat(updatedPreload.digestAlgorithm).isGreaterThan(0); + assertThat(updatedPreload.digest).isNotEmpty(); + assertThat(updatedPreload.mbaStatus).isEqualTo(/* MBA_STATUS_UPDATED_PRELOAD */ 2); + assertThat(updatedPreload.signerDigests).asList().containsNoneOf(null, ""); + } +} |