New integration test for fs-verity install and on-access verification
There are two categories of tests:
1. Package installation with .fsv_sig
* .apk, .dm, including the split ones should all or none be installed
with their corresponding .fsv_sig files
2. End-to-end fs-verity test of on-access verification
* When fs-verity is enabled to a file, if the on-disk content is
changed, the read should fail.
See class comment in ApkVerityTest.java for the test details.
Brief directory layout overview:
* src/
- Actual test
* ApkVerityTestApp/
- Dummy app for testing, including a split
* testdata/
- Some artifacts, signing key and fs-verity signatures
* block_device_writer/
- Helper binary for write a file directly on disk
Test: atest
Bug: 112039386
Change-Id: I3b8229037db682f36fda9d5cafd14caf6b39501d
This commit is contained in:
34
tests/ApkVerityTest/Android.bp
Normal file
34
tests/ApkVerityTest/Android.bp
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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.
|
||||
|
||||
java_test_host {
|
||||
name: "ApkVerityTests",
|
||||
srcs: ["src/**/*.java"],
|
||||
libs: ["tradefed", "compatibility-tradefed", "compatibility-host-util"],
|
||||
test_suites: ["general-tests"],
|
||||
target_required: [
|
||||
"block_device_writer_module",
|
||||
"ApkVerityTestApp",
|
||||
"ApkVerityTestAppSplit",
|
||||
],
|
||||
data: [
|
||||
":ApkVerityTestCertDer",
|
||||
":ApkVerityTestAppFsvSig",
|
||||
":ApkVerityTestAppDm",
|
||||
":ApkVerityTestAppDmFsvSig",
|
||||
":ApkVerityTestAppSplitFsvSig",
|
||||
":ApkVerityTestAppSplitDm",
|
||||
":ApkVerityTestAppSplitDmFsvSig",
|
||||
],
|
||||
}
|
||||
41
tests/ApkVerityTest/AndroidTest.xml
Normal file
41
tests/ApkVerityTest/AndroidTest.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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.
|
||||
-->
|
||||
<configuration description="APK fs-verity integration/regression test">
|
||||
<option name="test-suite-tag" value="apct" />
|
||||
|
||||
<!-- This test requires root to write against block device. -->
|
||||
<target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
|
||||
|
||||
<target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
|
||||
<!-- Disable package verifier prevents it holding the target APK's fd that prevents cache
|
||||
eviction. -->
|
||||
<option name="set-global-setting" key="package_verifier_enable" value="0" />
|
||||
<option name="restore-settings" value="true" />
|
||||
|
||||
<!-- Skip in order to prevent reboot that confuses the test flow. -->
|
||||
<option name="force-skip-system-props" value="true" />
|
||||
</target_preparer>
|
||||
|
||||
<target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
|
||||
<option name="cleanup" value="true" />
|
||||
<option name="push" value="block_device_writer->/data/local/tmp/block_device_writer" />
|
||||
<option name="push" value="ApkVerityTestCert.der->/data/local/tmp/ApkVerityTestCert.der" />
|
||||
</target_preparer>
|
||||
|
||||
<test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
|
||||
<option name="jar" value="ApkVerityTests.jar" />
|
||||
</test>
|
||||
</configuration>
|
||||
29
tests/ApkVerityTest/ApkVerityTestApp/Android.bp
Normal file
29
tests/ApkVerityTest/ApkVerityTestApp/Android.bp
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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.
|
||||
|
||||
android_test_helper_app {
|
||||
name: "ApkVerityTestApp",
|
||||
manifest: "AndroidManifest.xml",
|
||||
srcs: ["src/**/*.java"],
|
||||
}
|
||||
|
||||
android_test_helper_app {
|
||||
name: "ApkVerityTestAppSplit",
|
||||
manifest: "feature_split/AndroidManifest.xml",
|
||||
srcs: ["src/**/*.java"],
|
||||
aaptflags: [
|
||||
"--custom-package com.android.apkverity.feature_x",
|
||||
"--package-id 0x80",
|
||||
],
|
||||
}
|
||||
23
tests/ApkVerityTest/ApkVerityTestApp/AndroidManifest.xml
Normal file
23
tests/ApkVerityTest/ApkVerityTestApp/AndroidManifest.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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.apkverity">
|
||||
<application>
|
||||
<activity android:name=".DummyActivity"/>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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.apkverity"
|
||||
android:isFeatureSplit="true"
|
||||
split="feature_x">
|
||||
<application>
|
||||
<activity android:name=".feature_x.DummyActivity"/>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.apkverity.feature_x;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
/** Dummy class just to generate some dex */
|
||||
public class DummyActivity extends Activity {}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.apkverity;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
/** Dummy class just to generate some dex */
|
||||
public class DummyActivity extends Activity {}
|
||||
30
tests/ApkVerityTest/block_device_writer/Android.bp
Normal file
30
tests/ApkVerityTest/block_device_writer/Android.bp
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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.
|
||||
|
||||
// This is a cc_test just because it supports test_suites. This should be converted to something
|
||||
// like cc_binary_test_helper once supported.
|
||||
cc_test {
|
||||
// Depending on how the test runs, the executable may be uploaded to different location.
|
||||
// Before the bug in the file pusher is fixed, workaround by making the name unique.
|
||||
// See b/124718249#comment12.
|
||||
name: "block_device_writer_module",
|
||||
stem: "block_device_writer",
|
||||
|
||||
srcs: ["block_device_writer.cpp"],
|
||||
cflags: ["-Wall", "-Werror", "-Wextra", "-g"],
|
||||
shared_libs: ["libbase", "libutils"],
|
||||
|
||||
test_suites: ["general-tests"],
|
||||
gtest: false,
|
||||
}
|
||||
189
tests/ApkVerityTest/block_device_writer/block_device_writer.cpp
Normal file
189
tests/ApkVerityTest/block_device_writer/block_device_writer.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/fiemap.h>
|
||||
#include <linux/fs.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <android-base/unique_fd.h>
|
||||
|
||||
// This program modifies a file at given offset, but directly against the block
|
||||
// device, purposely to bypass the filesystem. Note that the change on block
|
||||
// device may not reflect the same way when read from filesystem, for example,
|
||||
// when the file is encrypted on disk.
|
||||
//
|
||||
// Only one byte is supported for now just so that we don't need to handle the
|
||||
// case when the range crosses different "extents".
|
||||
//
|
||||
// References:
|
||||
// https://www.kernel.org/doc/Documentation/filesystems/fiemap.txt
|
||||
// https://git.kernel.org/pub/scm/fs/xfs/xfsprogs-dev.git/tree/io/fiemap.c
|
||||
|
||||
ssize_t get_logical_block_size(const char* block_device) {
|
||||
android::base::unique_fd fd(open(block_device, O_RDONLY));
|
||||
if (fd.get() < 0) {
|
||||
fprintf(stderr, "open %s failed\n", block_device);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int size;
|
||||
if (ioctl(fd, BLKSSZGET, &size) < 0) {
|
||||
fprintf(stderr, "ioctl(BLKSSZGET) failed: %s\n", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
int64_t get_physical_offset(const char* file_name, uint64_t byte_offset) {
|
||||
android::base::unique_fd fd(open(file_name, O_RDONLY));
|
||||
if (fd.get() < 0) {
|
||||
fprintf(stderr, "open %s failed\n", file_name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const int map_size = sizeof(struct fiemap) + sizeof(struct fiemap_extent);
|
||||
char fiemap_buffer[map_size] = {0};
|
||||
struct fiemap* fiemap = reinterpret_cast<struct fiemap*>(&fiemap_buffer);
|
||||
|
||||
fiemap->fm_flags = FIEMAP_FLAG_SYNC;
|
||||
fiemap->fm_start = byte_offset;
|
||||
fiemap->fm_length = 1;
|
||||
fiemap->fm_extent_count = 1;
|
||||
|
||||
int ret = ioctl(fd.get(), FS_IOC_FIEMAP, fiemap);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "ioctl(FS_IOC_FIEMAP) failed: %s\n", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fiemap->fm_mapped_extents != 1) {
|
||||
fprintf(stderr, "fm_mapped_extents != 1 (is %d)\n",
|
||||
fiemap->fm_mapped_extents);
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct fiemap_extent* extent = &fiemap->fm_extents[0];
|
||||
printf(
|
||||
"logical offset: %llu, physical offset: %llu, length: %llu, "
|
||||
"flags: %x\n",
|
||||
extent->fe_logical, extent->fe_physical, extent->fe_length,
|
||||
extent->fe_flags);
|
||||
if (extent->fe_flags & (FIEMAP_EXTENT_UNKNOWN |
|
||||
FIEMAP_EXTENT_UNWRITTEN)) {
|
||||
fprintf(stderr, "Failed to locate physical offset safely\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return extent->fe_physical + (byte_offset - extent->fe_logical);
|
||||
}
|
||||
|
||||
int read_block_from_device(const char* device_path, uint64_t block_offset,
|
||||
ssize_t block_size, char* block_buffer) {
|
||||
assert(block_offset % block_size == 0);
|
||||
android::base::unique_fd fd(open(device_path, O_RDONLY | O_DIRECT));
|
||||
if (fd.get() < 0) {
|
||||
fprintf(stderr, "open %s failed\n", device_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ssize_t retval =
|
||||
TEMP_FAILURE_RETRY(pread(fd, block_buffer, block_size, block_offset));
|
||||
if (retval != block_size) {
|
||||
fprintf(stderr, "read returns error or incomplete result (%zu): %s\n",
|
||||
retval, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int write_block_to_device(const char* device_path, uint64_t block_offset,
|
||||
ssize_t block_size, char* block_buffer) {
|
||||
assert(block_offset % block_size == 0);
|
||||
android::base::unique_fd fd(open(device_path, O_WRONLY | O_DIRECT));
|
||||
if (fd.get() < 0) {
|
||||
fprintf(stderr, "open %s failed\n", device_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ssize_t retval = TEMP_FAILURE_RETRY(
|
||||
pwrite(fd.get(), block_buffer, block_size, block_offset));
|
||||
if (retval != block_size) {
|
||||
fprintf(stderr, "write returns error or incomplete result (%zu): %s\n",
|
||||
retval, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, const char** argv) {
|
||||
if (argc != 4) {
|
||||
fprintf(stderr,
|
||||
"Usage: %s block_dev filename byte_offset\n"
|
||||
"\n"
|
||||
"This program bypasses filesystem and damages the specified byte\n"
|
||||
"at the physical position on <block_dev> corresponding to the\n"
|
||||
"logical byte location in <filename>.\n",
|
||||
argv[0]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* block_device = argv[1];
|
||||
const char* file_name = argv[2];
|
||||
uint64_t byte_offset = strtoull(argv[3], nullptr, 10);
|
||||
|
||||
ssize_t block_size = get_logical_block_size(block_device);
|
||||
if (block_size < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int64_t physical_offset_signed = get_physical_offset(file_name, byte_offset);
|
||||
if (physical_offset_signed < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t physical_offset = static_cast<uint64_t>(physical_offset_signed);
|
||||
uint64_t offset_within_block = physical_offset % block_size;
|
||||
uint64_t physical_block_offset = physical_offset - offset_within_block;
|
||||
|
||||
// Direct I/O requires aligned buffer
|
||||
std::unique_ptr<char> buf(static_cast<char*>(
|
||||
aligned_alloc(block_size /* alignment */, block_size /* size */)));
|
||||
|
||||
if (read_block_from_device(block_device, physical_block_offset, block_size,
|
||||
buf.get()) < 0) {
|
||||
return -1;
|
||||
}
|
||||
char* p = buf.get() + offset_within_block;
|
||||
printf("before: %hhx\n", *p);
|
||||
*p ^= 0xff;
|
||||
printf("after: %hhx\n", *p);
|
||||
if (write_block_to_device(block_device, physical_block_offset, block_size,
|
||||
buf.get()) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
496
tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java
Normal file
496
tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java
Normal file
@@ -0,0 +1,496 @@
|
||||
/*
|
||||
* 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.apkverity;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
import android.platform.test.annotations.RootPermissionTest;
|
||||
|
||||
import com.android.tradefed.device.DeviceNotAvailableException;
|
||||
import com.android.tradefed.device.ITestDevice;
|
||||
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
|
||||
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
|
||||
import com.android.tradefed.util.CommandResult;
|
||||
import com.android.tradefed.util.CommandStatus;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* This test makes sure app installs with fs-verity signature, and on-access verification works.
|
||||
*
|
||||
* <p>When an app is installed, all or none of the files should have their corresponding .fsv_sig
|
||||
* signature file. Otherwise, install will fail.
|
||||
*
|
||||
* <p>Once installed, file protected by fs-verity is verified by kernel every time a block is loaded
|
||||
* from disk to memory. The file is immutable by design, enforced by filesystem.
|
||||
*
|
||||
* <p>In order to make sure a block of the file is readable only if the underlying block on disk
|
||||
* stay intact, the test needs to bypass the filesystem and tampers with the corresponding physical
|
||||
* address against the block device.
|
||||
*
|
||||
* <p>Requirements to run this test:
|
||||
* <ul>
|
||||
* <li>Device is rootable</li>
|
||||
* <li>The filesystem supports fs-verity</li>
|
||||
* <li>The feature flag is enabled</li>
|
||||
* </ul>
|
||||
*/
|
||||
@RootPermissionTest
|
||||
@RunWith(DeviceJUnit4ClassRunner.class)
|
||||
public class ApkVerityTest extends BaseHostJUnit4Test {
|
||||
private static final String TARGET_PACKAGE = "com.android.apkverity";
|
||||
|
||||
private static final String BASE_APK = "ApkVerityTestApp.apk";
|
||||
private static final String BASE_APK_DM = "ApkVerityTestApp.dm";
|
||||
private static final String SPLIT_APK = "ApkVerityTestAppSplit.apk";
|
||||
private static final String SPLIT_APK_DM = "ApkVerityTestAppSplit.dm";
|
||||
|
||||
private static final String INSTALLED_BASE_APK = "base.apk";
|
||||
private static final String INSTALLED_BASE_DM = "base.dm";
|
||||
private static final String INSTALLED_SPLIT_APK = "split_feature_x.apk";
|
||||
private static final String INSTALLED_SPLIT_DM = "split_feature_x.dm";
|
||||
private static final String INSTALLED_BASE_APK_FSV_SIG = "base.apk.fsv_sig";
|
||||
private static final String INSTALLED_BASE_DM_FSV_SIG = "base.dm.fsv_sig";
|
||||
private static final String INSTALLED_SPLIT_APK_FSV_SIG = "split_feature_x.apk.fsv_sig";
|
||||
private static final String INSTALLED_SPLIT_DM_FSV_SIG = "split_feature_x.dm.fsv_sig";
|
||||
|
||||
private static final String DAMAGING_EXECUTABLE = "/data/local/tmp/block_device_writer";
|
||||
private static final String CERT_PATH = "/data/local/tmp/ApkVerityTestCert.der";
|
||||
|
||||
private static final String APK_VERITY_STANDARD_MODE = "2";
|
||||
|
||||
/** Only 4K page is supported by fs-verity currently. */
|
||||
private static final int FSVERITY_PAGE_SIZE = 4096;
|
||||
|
||||
private ITestDevice mDevice;
|
||||
private String mKeyId;
|
||||
|
||||
@Before
|
||||
public void setUp() throws DeviceNotAvailableException {
|
||||
mDevice = getDevice();
|
||||
|
||||
String apkVerityMode = mDevice.getProperty("ro.apk_verity.mode");
|
||||
assumeTrue(APK_VERITY_STANDARD_MODE.equals(apkVerityMode));
|
||||
|
||||
mKeyId = expectRemoteCommandToSucceed(
|
||||
"mini-keyctl padd asymmetric fsv_test .fs-verity < " + CERT_PATH).trim();
|
||||
if (!mKeyId.matches("^\\d+$")) {
|
||||
String keyId = mKeyId;
|
||||
mKeyId = null;
|
||||
fail("Key ID is not decimal: " + keyId);
|
||||
}
|
||||
|
||||
uninstallPackage(TARGET_PACKAGE);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws DeviceNotAvailableException {
|
||||
uninstallPackage(TARGET_PACKAGE);
|
||||
|
||||
if (mKeyId != null) {
|
||||
expectRemoteCommandToSucceed("mini-keyctl unlink " + mKeyId + " .fs-verity");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFsverityKernelSupports() throws DeviceNotAvailableException {
|
||||
ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data");
|
||||
expectRemoteCommandToSucceed("test -f /sys/fs/" + mountPoint.type + "/features/verity");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallBase() throws DeviceNotAvailableException, FileNotFoundException {
|
||||
new InstallMultiple()
|
||||
.addFileAndSignature(BASE_APK)
|
||||
.run();
|
||||
assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
|
||||
|
||||
verifyInstalledFiles(
|
||||
INSTALLED_BASE_APK,
|
||||
INSTALLED_BASE_APK_FSV_SIG);
|
||||
verifyInstalledFilesHaveFsverity();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallBaseWithWrongSignature()
|
||||
throws DeviceNotAvailableException, FileNotFoundException {
|
||||
new InstallMultiple()
|
||||
.addFile(BASE_APK)
|
||||
.addFile(SPLIT_APK_DM + ".fsv_sig",
|
||||
BASE_APK + ".fsv_sig")
|
||||
.runExpectingFailure();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallBaseWithSplit()
|
||||
throws DeviceNotAvailableException, FileNotFoundException {
|
||||
new InstallMultiple()
|
||||
.addFileAndSignature(BASE_APK)
|
||||
.addFileAndSignature(SPLIT_APK)
|
||||
.run();
|
||||
assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
|
||||
|
||||
verifyInstalledFiles(
|
||||
INSTALLED_BASE_APK,
|
||||
INSTALLED_BASE_APK_FSV_SIG,
|
||||
INSTALLED_SPLIT_APK,
|
||||
INSTALLED_SPLIT_APK_FSV_SIG);
|
||||
verifyInstalledFilesHaveFsverity();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallBaseWithDm() throws DeviceNotAvailableException, FileNotFoundException {
|
||||
new InstallMultiple()
|
||||
.addFileAndSignature(BASE_APK)
|
||||
.addFileAndSignature(BASE_APK_DM)
|
||||
.run();
|
||||
assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
|
||||
|
||||
verifyInstalledFiles(
|
||||
INSTALLED_BASE_APK,
|
||||
INSTALLED_BASE_APK_FSV_SIG,
|
||||
INSTALLED_BASE_DM,
|
||||
INSTALLED_BASE_DM_FSV_SIG);
|
||||
verifyInstalledFilesHaveFsverity();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallEverything() throws DeviceNotAvailableException, FileNotFoundException {
|
||||
new InstallMultiple()
|
||||
.addFileAndSignature(BASE_APK)
|
||||
.addFileAndSignature(BASE_APK_DM)
|
||||
.addFileAndSignature(SPLIT_APK)
|
||||
.addFileAndSignature(SPLIT_APK_DM)
|
||||
.run();
|
||||
assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
|
||||
|
||||
verifyInstalledFiles(
|
||||
INSTALLED_BASE_APK,
|
||||
INSTALLED_BASE_APK_FSV_SIG,
|
||||
INSTALLED_BASE_DM,
|
||||
INSTALLED_BASE_DM_FSV_SIG,
|
||||
INSTALLED_SPLIT_APK,
|
||||
INSTALLED_SPLIT_APK_FSV_SIG,
|
||||
INSTALLED_SPLIT_DM,
|
||||
INSTALLED_SPLIT_DM_FSV_SIG);
|
||||
verifyInstalledFilesHaveFsverity();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallSplitOnly()
|
||||
throws DeviceNotAvailableException, FileNotFoundException {
|
||||
new InstallMultiple()
|
||||
.addFileAndSignature(BASE_APK)
|
||||
.run();
|
||||
assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
|
||||
verifyInstalledFiles(
|
||||
INSTALLED_BASE_APK,
|
||||
INSTALLED_BASE_APK_FSV_SIG);
|
||||
|
||||
new InstallMultiple()
|
||||
.inheritFrom(TARGET_PACKAGE)
|
||||
.addFileAndSignature(SPLIT_APK)
|
||||
.run();
|
||||
|
||||
verifyInstalledFiles(
|
||||
INSTALLED_BASE_APK,
|
||||
INSTALLED_BASE_APK_FSV_SIG,
|
||||
INSTALLED_SPLIT_APK,
|
||||
INSTALLED_SPLIT_APK_FSV_SIG);
|
||||
verifyInstalledFilesHaveFsverity();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallSplitOnlyMissingSignature()
|
||||
throws DeviceNotAvailableException, FileNotFoundException {
|
||||
new InstallMultiple()
|
||||
.addFileAndSignature(BASE_APK)
|
||||
.run();
|
||||
assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
|
||||
verifyInstalledFiles(
|
||||
INSTALLED_BASE_APK,
|
||||
INSTALLED_BASE_APK_FSV_SIG);
|
||||
|
||||
new InstallMultiple()
|
||||
.inheritFrom(TARGET_PACKAGE)
|
||||
.addFile(SPLIT_APK)
|
||||
.runExpectingFailure();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallSplitOnlyWithoutBaseSignature()
|
||||
throws DeviceNotAvailableException, FileNotFoundException {
|
||||
new InstallMultiple()
|
||||
.addFile(BASE_APK)
|
||||
.run();
|
||||
assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
|
||||
verifyInstalledFiles(INSTALLED_BASE_APK);
|
||||
|
||||
new InstallMultiple()
|
||||
.inheritFrom(TARGET_PACKAGE)
|
||||
.addFileAndSignature(SPLIT_APK)
|
||||
.run();
|
||||
verifyInstalledFiles(
|
||||
INSTALLED_BASE_APK,
|
||||
INSTALLED_SPLIT_APK,
|
||||
INSTALLED_SPLIT_APK_FSV_SIG);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallOnlyBaseHasFsvSig()
|
||||
throws DeviceNotAvailableException, FileNotFoundException {
|
||||
new InstallMultiple()
|
||||
.addFileAndSignature(BASE_APK)
|
||||
.addFile(BASE_APK_DM)
|
||||
.addFile(SPLIT_APK)
|
||||
.addFile(SPLIT_APK_DM)
|
||||
.runExpectingFailure();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallOnlyDmHasFsvSig()
|
||||
throws DeviceNotAvailableException, FileNotFoundException {
|
||||
new InstallMultiple()
|
||||
.addFile(BASE_APK)
|
||||
.addFileAndSignature(BASE_APK_DM)
|
||||
.addFile(SPLIT_APK)
|
||||
.addFile(SPLIT_APK_DM)
|
||||
.runExpectingFailure();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallOnlySplitHasFsvSig()
|
||||
throws DeviceNotAvailableException, FileNotFoundException {
|
||||
new InstallMultiple()
|
||||
.addFile(BASE_APK)
|
||||
.addFile(BASE_APK_DM)
|
||||
.addFileAndSignature(SPLIT_APK)
|
||||
.addFile(SPLIT_APK_DM)
|
||||
.runExpectingFailure();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallBaseWithFsvSigThenSplitWithout()
|
||||
throws DeviceNotAvailableException, FileNotFoundException {
|
||||
new InstallMultiple()
|
||||
.addFileAndSignature(BASE_APK)
|
||||
.run();
|
||||
assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
|
||||
verifyInstalledFiles(
|
||||
INSTALLED_BASE_APK,
|
||||
INSTALLED_BASE_APK_FSV_SIG);
|
||||
|
||||
new InstallMultiple()
|
||||
.addFile(SPLIT_APK)
|
||||
.runExpectingFailure();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallBaseWithoutFsvSigThenSplitWith()
|
||||
throws DeviceNotAvailableException, FileNotFoundException {
|
||||
new InstallMultiple()
|
||||
.addFile(BASE_APK)
|
||||
.run();
|
||||
assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
|
||||
verifyInstalledFiles(INSTALLED_BASE_APK);
|
||||
|
||||
new InstallMultiple()
|
||||
.addFileAndSignature(SPLIT_APK)
|
||||
.runExpectingFailure();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFsverityFileIsImmutableAndReadable() throws DeviceNotAvailableException {
|
||||
new InstallMultiple().addFileAndSignature(BASE_APK).run();
|
||||
String apkPath = getApkPath(TARGET_PACKAGE);
|
||||
|
||||
assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
|
||||
expectRemoteCommandToFail("echo -n '' >> " + apkPath);
|
||||
expectRemoteCommandToSucceed("cat " + apkPath + " > /dev/null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFsverityFailToReadModifiedBlockAtFront() throws DeviceNotAvailableException {
|
||||
new InstallMultiple().addFileAndSignature(BASE_APK).run();
|
||||
String apkPath = getApkPath(TARGET_PACKAGE);
|
||||
|
||||
long apkSize = getFileSizeInBytes(apkPath);
|
||||
long offsetFirstByte = 0;
|
||||
|
||||
// The first two pages should be both readable at first.
|
||||
assertTrue(canReadByte(apkPath, offsetFirstByte));
|
||||
if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) {
|
||||
assertTrue(canReadByte(apkPath, offsetFirstByte + FSVERITY_PAGE_SIZE));
|
||||
}
|
||||
|
||||
// Damage the file directly against the block device.
|
||||
damageFileAgainstBlockDevice(apkPath, offsetFirstByte);
|
||||
|
||||
// Expect actual read from disk to fail but only at damaged page.
|
||||
dropCaches();
|
||||
assertFalse(canReadByte(apkPath, offsetFirstByte));
|
||||
if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) {
|
||||
long lastByteOfTheSamePage =
|
||||
offsetFirstByte % FSVERITY_PAGE_SIZE + FSVERITY_PAGE_SIZE - 1;
|
||||
assertFalse(canReadByte(apkPath, lastByteOfTheSamePage));
|
||||
assertTrue(canReadByte(apkPath, lastByteOfTheSamePage + 1));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFsverityFailToReadModifiedBlockAtBack() throws DeviceNotAvailableException {
|
||||
new InstallMultiple().addFileAndSignature(BASE_APK).run();
|
||||
String apkPath = getApkPath(TARGET_PACKAGE);
|
||||
|
||||
long apkSize = getFileSizeInBytes(apkPath);
|
||||
long offsetOfLastByte = apkSize - 1;
|
||||
|
||||
// The first two pages should be both readable at first.
|
||||
assertTrue(canReadByte(apkPath, offsetOfLastByte));
|
||||
if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) {
|
||||
assertTrue(canReadByte(apkPath, offsetOfLastByte - FSVERITY_PAGE_SIZE));
|
||||
}
|
||||
|
||||
// Damage the file directly against the block device.
|
||||
damageFileAgainstBlockDevice(apkPath, offsetOfLastByte);
|
||||
|
||||
// Expect actual read from disk to fail but only at damaged page.
|
||||
dropCaches();
|
||||
assertFalse(canReadByte(apkPath, offsetOfLastByte));
|
||||
if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) {
|
||||
long firstByteOfTheSamePage = offsetOfLastByte - offsetOfLastByte % FSVERITY_PAGE_SIZE;
|
||||
assertFalse(canReadByte(apkPath, firstByteOfTheSamePage));
|
||||
assertTrue(canReadByte(apkPath, firstByteOfTheSamePage - 1));
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyInstalledFilesHaveFsverity() throws DeviceNotAvailableException {
|
||||
// Verify that all files are protected by fs-verity
|
||||
String apkPath = getApkPath(TARGET_PACKAGE);
|
||||
String appDir = apkPath.substring(0, apkPath.lastIndexOf("/"));
|
||||
long kTargetOffset = 0;
|
||||
for (String basename : expectRemoteCommandToSucceed("ls " + appDir).split("\n")) {
|
||||
if (basename.endsWith(".apk") || basename.endsWith(".dm")) {
|
||||
String path = appDir + "/" + basename;
|
||||
damageFileAgainstBlockDevice(path, kTargetOffset);
|
||||
|
||||
// Retry is sometimes needed to pass the test. Package manager may have FD leaks
|
||||
// (see b/122744005 as example) that prevents the file in question to be evicted
|
||||
// from filesystem cache. Forcing GC workarounds the problem.
|
||||
int retry = 5;
|
||||
for (; retry > 0; retry--) {
|
||||
dropCaches();
|
||||
if (!canReadByte(path, kTargetOffset)) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
String pid = expectRemoteCommandToSucceed("pidof system_server");
|
||||
mDevice.executeShellV2Command("kill -10 " + pid); // force GC
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
assertTrue("Read from " + path + " should fail", retry > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyInstalledFiles(String... filenames) throws DeviceNotAvailableException {
|
||||
String apkPath = getApkPath(TARGET_PACKAGE);
|
||||
String appDir = apkPath.substring(0, apkPath.lastIndexOf("/"));
|
||||
HashSet<String> actualFiles = new HashSet<>(Arrays.asList(
|
||||
expectRemoteCommandToSucceed("ls " + appDir).split("\n")));
|
||||
assertTrue(actualFiles.remove("lib"));
|
||||
assertTrue(actualFiles.remove("oat"));
|
||||
|
||||
HashSet<String> expectedFiles = new HashSet<>(Arrays.asList(filenames));
|
||||
assertEquals(expectedFiles, actualFiles);
|
||||
}
|
||||
|
||||
private void damageFileAgainstBlockDevice(String path, long offsetOfTargetingByte)
|
||||
throws DeviceNotAvailableException {
|
||||
assertTrue(path.startsWith("/data/"));
|
||||
ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data");
|
||||
expectRemoteCommandToSucceed(String.join(" ", DAMAGING_EXECUTABLE,
|
||||
mountPoint.filesystem, path, Long.toString(offsetOfTargetingByte)));
|
||||
}
|
||||
|
||||
private String getApkPath(String packageName) throws DeviceNotAvailableException {
|
||||
String line = expectRemoteCommandToSucceed("pm path " + packageName + " | grep base.apk");
|
||||
int index = line.trim().indexOf(":");
|
||||
assertTrue(index >= 0);
|
||||
return line.substring(index + 1);
|
||||
}
|
||||
|
||||
private long getFileSizeInBytes(String packageName) throws DeviceNotAvailableException {
|
||||
return Long.parseLong(expectRemoteCommandToSucceed("stat -c '%s' " + packageName).trim());
|
||||
}
|
||||
|
||||
private void dropCaches() throws DeviceNotAvailableException {
|
||||
expectRemoteCommandToSucceed("sync && echo 1 > /proc/sys/vm/drop_caches");
|
||||
}
|
||||
|
||||
private boolean canReadByte(String filePath, long offset) throws DeviceNotAvailableException {
|
||||
CommandResult result = mDevice.executeShellV2Command(
|
||||
"dd if=" + filePath + " bs=1 count=1 skip=" + Long.toString(offset));
|
||||
return result.getStatus() == CommandStatus.SUCCESS;
|
||||
}
|
||||
|
||||
private String expectRemoteCommandToSucceed(String cmd) throws DeviceNotAvailableException {
|
||||
CommandResult result = mDevice.executeShellV2Command(cmd);
|
||||
assertEquals("`" + cmd + "` failed: " + result.getStderr(), CommandStatus.SUCCESS,
|
||||
result.getStatus());
|
||||
return result.getStdout();
|
||||
}
|
||||
|
||||
private void expectRemoteCommandToFail(String cmd) throws DeviceNotAvailableException {
|
||||
CommandResult result = mDevice.executeShellV2Command(cmd);
|
||||
assertTrue("Unexpected success from `" + cmd + "`: " + result.getStderr(),
|
||||
result.getStatus() != CommandStatus.SUCCESS);
|
||||
}
|
||||
|
||||
private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> {
|
||||
InstallMultiple() {
|
||||
super(getDevice(), getBuild());
|
||||
}
|
||||
|
||||
InstallMultiple addFileAndSignature(String filename) {
|
||||
try {
|
||||
addFile(filename);
|
||||
addFile(filename + ".fsv_sig");
|
||||
} catch (FileNotFoundException e) {
|
||||
fail("Missing test file: " + e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.apkverity;
|
||||
|
||||
import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
|
||||
import com.android.tradefed.build.IBuildInfo;
|
||||
import com.android.tradefed.device.DeviceNotAvailableException;
|
||||
import com.android.tradefed.device.ITestDevice;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Base class for invoking the install-multiple command via ADB. Subclass this for less typing:
|
||||
*
|
||||
* <code> private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> { public
|
||||
* InstallMultiple() { super(getDevice(), null); } } </code>
|
||||
*/
|
||||
/*package*/ class BaseInstallMultiple<T extends BaseInstallMultiple<?>> {
|
||||
|
||||
private final ITestDevice mDevice;
|
||||
private final IBuildInfo mBuild;
|
||||
|
||||
private final List<String> mArgs = new ArrayList<>();
|
||||
private final Map<File, String> mFileToRemoteMap = new HashMap<>();
|
||||
|
||||
/*package*/ BaseInstallMultiple(ITestDevice device, IBuildInfo buildInfo) {
|
||||
mDevice = device;
|
||||
mBuild = buildInfo;
|
||||
addArg("-g");
|
||||
}
|
||||
|
||||
T addArg(String arg) {
|
||||
mArgs.add(arg);
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
T addFile(String filename) throws FileNotFoundException {
|
||||
return addFile(filename, filename);
|
||||
}
|
||||
|
||||
T addFile(String filename, String remoteName) throws FileNotFoundException {
|
||||
CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuild);
|
||||
mFileToRemoteMap.put(buildHelper.getTestFile(filename), remoteName);
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
T inheritFrom(String packageName) {
|
||||
addArg("-r");
|
||||
addArg("-p " + packageName);
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
void run() throws DeviceNotAvailableException {
|
||||
run(true);
|
||||
}
|
||||
|
||||
void runExpectingFailure() throws DeviceNotAvailableException {
|
||||
run(false);
|
||||
}
|
||||
|
||||
private void run(boolean expectingSuccess) throws DeviceNotAvailableException {
|
||||
final ITestDevice device = mDevice;
|
||||
|
||||
// Create an install session
|
||||
final StringBuilder cmd = new StringBuilder();
|
||||
cmd.append("pm install-create");
|
||||
for (String arg : mArgs) {
|
||||
cmd.append(' ').append(arg);
|
||||
}
|
||||
|
||||
String result = device.executeShellCommand(cmd.toString());
|
||||
TestCase.assertTrue(result, result.startsWith("Success"));
|
||||
|
||||
final int start = result.lastIndexOf("[");
|
||||
final int end = result.lastIndexOf("]");
|
||||
int sessionId = -1;
|
||||
try {
|
||||
if (start != -1 && end != -1 && start < end) {
|
||||
sessionId = Integer.parseInt(result.substring(start + 1, end));
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalStateException("Failed to parse install session: " + result);
|
||||
}
|
||||
if (sessionId == -1) {
|
||||
throw new IllegalStateException("Failed to create install session: " + result);
|
||||
}
|
||||
|
||||
// Push our files into session. Ideally we'd use stdin streaming,
|
||||
// but ddmlib doesn't support it yet.
|
||||
for (final Map.Entry<File, String> entry : mFileToRemoteMap.entrySet()) {
|
||||
final File file = entry.getKey();
|
||||
final String remoteName = entry.getValue();
|
||||
final String remotePath = "/data/local/tmp/" + file.getName();
|
||||
if (!device.pushFile(file, remotePath)) {
|
||||
throw new IllegalStateException("Failed to push " + file);
|
||||
}
|
||||
|
||||
cmd.setLength(0);
|
||||
cmd.append("pm install-write");
|
||||
cmd.append(' ').append(sessionId);
|
||||
cmd.append(' ').append(remoteName);
|
||||
cmd.append(' ').append(remotePath);
|
||||
|
||||
result = device.executeShellCommand(cmd.toString());
|
||||
TestCase.assertTrue(result, result.startsWith("Success"));
|
||||
}
|
||||
|
||||
// Everything staged; let's pull trigger
|
||||
cmd.setLength(0);
|
||||
cmd.append("pm install-commit");
|
||||
cmd.append(' ').append(sessionId);
|
||||
|
||||
result = device.executeShellCommand(cmd.toString());
|
||||
if (expectingSuccess) {
|
||||
TestCase.assertTrue(result, result.contains("Success"));
|
||||
} else {
|
||||
TestCase.assertFalse(result, result.contains("Success"));
|
||||
}
|
||||
}
|
||||
}
|
||||
77
tests/ApkVerityTest/testdata/Android.bp
vendored
Normal file
77
tests/ApkVerityTest/testdata/Android.bp
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
// 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.
|
||||
|
||||
filegroup {
|
||||
name: "ApkVerityTestKeyPem",
|
||||
srcs: ["ApkVerityTestKey.pem"],
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "ApkVerityTestCertPem",
|
||||
srcs: ["ApkVerityTestCert.pem"],
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "ApkVerityTestCertDer",
|
||||
srcs: ["ApkVerityTestCert.der"],
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "ApkVerityTestAppDm",
|
||||
srcs: ["ApkVerityTestApp.dm"],
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "ApkVerityTestAppSplitDm",
|
||||
srcs: ["ApkVerityTestAppSplit.dm"],
|
||||
}
|
||||
|
||||
genrule_defaults {
|
||||
name: "apk_verity_sig_gen_default",
|
||||
tools: ["fsverity"],
|
||||
tool_files: [":ApkVerityTestKeyPem", ":ApkVerityTestCertPem"],
|
||||
cmd: "$(location fsverity) sign $(in) $(out) " +
|
||||
"--key=$(location :ApkVerityTestKeyPem) " +
|
||||
"--cert=$(location :ApkVerityTestCertPem) " +
|
||||
"> /dev/null",
|
||||
}
|
||||
|
||||
genrule {
|
||||
name: "ApkVerityTestAppFsvSig",
|
||||
defaults: ["apk_verity_sig_gen_default"],
|
||||
srcs: [":ApkVerityTestApp"],
|
||||
out: ["ApkVerityTestApp.apk.fsv_sig"],
|
||||
}
|
||||
|
||||
genrule {
|
||||
name: "ApkVerityTestAppDmFsvSig",
|
||||
defaults: ["apk_verity_sig_gen_default"],
|
||||
srcs: [":ApkVerityTestAppDm"],
|
||||
out: ["ApkVerityTestApp.dm.fsv_sig"],
|
||||
}
|
||||
|
||||
genrule {
|
||||
name: "ApkVerityTestAppSplitFsvSig",
|
||||
defaults: ["apk_verity_sig_gen_default"],
|
||||
srcs: [":ApkVerityTestAppSplit"],
|
||||
out: ["ApkVerityTestAppSplit.apk.fsv_sig"],
|
||||
}
|
||||
|
||||
genrule {
|
||||
name: "ApkVerityTestAppSplitDmFsvSig",
|
||||
defaults: ["apk_verity_sig_gen_default"],
|
||||
srcs: [":ApkVerityTestAppSplitDm"],
|
||||
out: ["ApkVerityTestAppSplit.dm.fsv_sig"],
|
||||
}
|
||||
|
||||
BIN
tests/ApkVerityTest/testdata/ApkVerityTestApp.dm
vendored
Normal file
BIN
tests/ApkVerityTest/testdata/ApkVerityTestApp.dm
vendored
Normal file
Binary file not shown.
BIN
tests/ApkVerityTest/testdata/ApkVerityTestAppSplit.dm
vendored
Normal file
BIN
tests/ApkVerityTest/testdata/ApkVerityTestAppSplit.dm
vendored
Normal file
Binary file not shown.
BIN
tests/ApkVerityTest/testdata/ApkVerityTestCert.der
vendored
Normal file
BIN
tests/ApkVerityTest/testdata/ApkVerityTestCert.der
vendored
Normal file
Binary file not shown.
30
tests/ApkVerityTest/testdata/ApkVerityTestCert.pem
vendored
Normal file
30
tests/ApkVerityTest/testdata/ApkVerityTestCert.pem
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFLjCCAxagAwIBAgIJAKZbtMlZZwtdMA0GCSqGSIb3DQEBCwUAMCwxCzAJBgNV
|
||||
BAYTAlVTMQswCQYDVQQIDAJDQTEQMA4GA1UECgwHQW5kcm9pZDAeFw0xODEyMTky
|
||||
MTA5MzVaFw0xOTAxMTgyMTA5MzVaMCwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJD
|
||||
QTEQMA4GA1UECgwHQW5kcm9pZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
|
||||
ggIBAKnrw4WiFgFBq6vXqcLc97iwvcYPZmeIjQqYRF+CHwXBXx8IyDlMfPrgyIYo
|
||||
ZLkosnUK/Exuypdu6UEtdqtYPknC6w9z4YkxqsKtyxyB1b13ptcTHh3bf2N8bqGr
|
||||
8gWWLxj0QjumCtFi7Z/TCwB5t3b3gtC+0jVfABSWrm5PNkgk7jIP+4KeYLDCDfiJ
|
||||
XH3uHu6OASiSHTOnrmLWSaSw0y6G4OFthHqQnMywasly0r6m+Mif+K0ZUV7hBRi/
|
||||
SfqcJ1HTCXTJMskEyV6Qx2sHF/VbK2gdUv56z6OVRNSs/FxPBiWVMuZZKh1FpBVI
|
||||
gbGxusf2Awwtc+Soxr4/P1YFcrwfA/ff9FK3Yg/Cd3ZMGbzUkbEMEkE5BW7Gbjmx
|
||||
wz3mYTiRfa2L/Bl4MiMqNi0tfORLkmg+V/EItzfhZ/HsXMOCBsnuj4KnFslmbamz
|
||||
t9opypj2JLGk+lXhZ5gFNFw8tYH1AnG1AIXe5u+6Fq2nQ1y/ncGUTR5Sw4de/Gee
|
||||
C0UgR+KiFEdKupMKbXgSKl+0QPz/i2eSpcDOKMwZ4WiNrkbccbCyr38so+j5DfWF
|
||||
IeZA9a/IlysA6G8yU2TfXBc65VCIEQRJOQdUOZFDO8OSoqGP+fbA6edpmovGw+TH
|
||||
sM/NkmpEXpQm7BVOI4oVjdf4pKPp0zaW2YcaA3xU2w6eF17pAgMBAAGjUzBRMB0G
|
||||
A1UdDgQWBBRGpHYy7yiLEYalGuF1va6zJKGD/zAfBgNVHSMEGDAWgBRGpHYy7yiL
|
||||
EYalGuF1va6zJKGD/zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IC
|
||||
AQAao6ZBM122F0pYb2QLahIyyGEr3LfSdBGID4068pVik4ncIefFz36Xf9AFxRQd
|
||||
KHmwRYNPHiLRIEGdtqplC5pZDeHz41txIArNIZKzDWOYtdcFyCz8umuj912BmsoM
|
||||
YUQhT6F1sX53SWcKxEP/aJ2kltSlPFX99e3Vx9eRkceV1oe2NM6ZG8hnYCfCAMeJ
|
||||
jRTpbqCGaAsEHFtIx6wt3zEtUXIVg4aYFQs/qjTjeP8ByIj0b4lZrceEoTeRimuj
|
||||
+4aAI+jBxLkwaN3hseQHzRNpgPehIVV/0RU92yzOD/WN4YwE6rwjKEI1lihHNBDa
|
||||
+DwGtGbHmIUzjW1qArig+mzUIhfYIJAxrx20ynPz/Q+C7+iXhTDAYQlxTle0pX8m
|
||||
yM2DUdPo97eLOzQ4JDHxtcN3ntTEJKKvrmzKvWuxy/yoLwS7MtLH6RETTHabH3Qd
|
||||
CP83X7z8zTyxgPxHdfHo9sgR/4C9RHGJx4OpBTQaiqfjSpDqJSIQdbrHGOQDgYwL
|
||||
KQyiQuhukmNgRCB6dJoZJ/MyaNuMsXV9QobsDHW1oSuCvPAihVoWHJxt8m4Ma0jJ
|
||||
EIbEPT2Umw1F/P+CeXnVQwhPvzQKHCa+6cC/YdjTqIKLmQV8X3HUBUIMhP2JGDic
|
||||
MnUipTm/RwWZVOjCJaFqk5sVq3L0Lyd0XVUWSK1a4IcrsA==
|
||||
-----END CERTIFICATE-----
|
||||
52
tests/ApkVerityTest/testdata/ApkVerityTestKey.pem
vendored
Normal file
52
tests/ApkVerityTest/testdata/ApkVerityTestKey.pem
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
p2aUlQOLXU9SjQTirZbdCZP0XHtFsg==
|
||||
-----END PRIVATE KEY-----
|
||||
13
tests/ApkVerityTest/testdata/README.md
vendored
Normal file
13
tests/ApkVerityTest/testdata/README.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
This test only runs on rooted / debuggable device.
|
||||
|
||||
The test tries to install subsets of base.{apk,dm}, split.{apk,dm} and their
|
||||
corresponding .fsv_sig files (generated by build rule). If installed, the
|
||||
tests also tries to tamper with the file at absolute disk offset to verify
|
||||
if fs-verity is effective.
|
||||
|
||||
How to generate dex metadata (.dm)
|
||||
==================================
|
||||
|
||||
adb shell profman --generate-test-profile=/data/local/tmp/primary.prof
|
||||
adb pull /data/local/tmp/primary.prof
|
||||
zip foo.dm primary.prof
|
||||
Reference in New Issue
Block a user