1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.apkverity;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertTrue;
23 import static org.junit.Assert.fail;
24 import static org.junit.Assume.assumeTrue;
25 
26 import android.platform.test.annotations.RootPermissionTest;
27 
28 import com.android.tradefed.device.DeviceNotAvailableException;
29 import com.android.tradefed.device.ITestDevice;
30 import com.android.tradefed.log.LogUtil.CLog;
31 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
32 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
33 import com.android.tradefed.util.CommandResult;
34 import com.android.tradefed.util.CommandStatus;
35 
36 import org.junit.After;
37 import org.junit.Before;
38 import org.junit.Test;
39 import org.junit.runner.RunWith;
40 
41 import java.io.FileNotFoundException;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.HashSet;
45 
46 /**
47  * This test makes sure app installs with fs-verity signature, and on-access verification works.
48  *
49  * <p>When an app is installed, all or none of the files should have their corresponding .fsv_sig
50  * signature file. Otherwise, install will fail.
51  *
52  * <p>Once installed, file protected by fs-verity is verified by kernel every time a block is loaded
53  * from disk to memory. The file is immutable by design, enforced by filesystem.
54  *
55  * <p>In order to make sure a block of the file is readable only if the underlying block on disk
56  * stay intact, the test needs to bypass the filesystem and tampers with the corresponding physical
57  * address against the block device.
58  *
59  * <p>Requirements to run this test:
60  * <ul>
61  *   <li>Device is rootable</li>
62  *   <li>The filesystem supports fs-verity</li>
63  *   <li>The feature flag is enabled</li>
64  * </ul>
65  */
66 @RootPermissionTest
67 @RunWith(DeviceJUnit4ClassRunner.class)
68 public class ApkVerityTest extends BaseHostJUnit4Test {
69     private static final String TARGET_PACKAGE = "com.android.apkverity";
70 
71     private static final String BASE_APK = "ApkVerityTestApp.apk";
72     private static final String BASE_APK_DM = "ApkVerityTestApp.dm";
73     private static final String SPLIT_APK = "ApkVerityTestAppSplit.apk";
74     private static final String SPLIT_APK_DM = "ApkVerityTestAppSplit.dm";
75 
76     private static final String INSTALLED_BASE_APK = "base.apk";
77     private static final String INSTALLED_BASE_DM = "base.dm";
78     private static final String INSTALLED_SPLIT_APK = "split_feature_x.apk";
79     private static final String INSTALLED_SPLIT_DM = "split_feature_x.dm";
80     private static final String INSTALLED_BASE_APK_FSV_SIG = "base.apk.fsv_sig";
81     private static final String INSTALLED_BASE_DM_FSV_SIG = "base.dm.fsv_sig";
82     private static final String INSTALLED_SPLIT_APK_FSV_SIG = "split_feature_x.apk.fsv_sig";
83     private static final String INSTALLED_SPLIT_DM_FSV_SIG = "split_feature_x.dm.fsv_sig";
84 
85     private static final String DAMAGING_EXECUTABLE = "/data/local/tmp/block_device_writer";
86     private static final String CERT_PATH = "/data/local/tmp/ApkVerityTestCert.der";
87 
88     private static final String APK_VERITY_STANDARD_MODE = "2";
89 
90     /** Only 4K page is supported by fs-verity currently. */
91     private static final int FSVERITY_PAGE_SIZE = 4096;
92 
93     private ITestDevice mDevice;
94     private String mKeyId;
95 
96     @Before
setUp()97     public void setUp() throws DeviceNotAvailableException {
98         mDevice = getDevice();
99 
100         String apkVerityMode = mDevice.getProperty("ro.apk_verity.mode");
101         assumeTrue(mDevice.getLaunchApiLevel() >= 30
102                 || APK_VERITY_STANDARD_MODE.equals(apkVerityMode));
103 
104         mKeyId = expectRemoteCommandToSucceed(
105                 "mini-keyctl padd asymmetric fsv_test .fs-verity < " + CERT_PATH).trim();
106         if (!mKeyId.matches("^\\d+$")) {
107             String keyId = mKeyId;
108             mKeyId = null;
109             fail("Key ID is not decimal: " + keyId);
110         }
111 
112         uninstallPackage(TARGET_PACKAGE);
113     }
114 
115     @After
tearDown()116     public void tearDown() throws DeviceNotAvailableException {
117         uninstallPackage(TARGET_PACKAGE);
118 
119         if (mKeyId != null) {
120             expectRemoteCommandToSucceed("mini-keyctl unlink " + mKeyId + " .fs-verity");
121         }
122     }
123 
124     @Test
testFsverityKernelSupports()125     public void testFsverityKernelSupports() throws DeviceNotAvailableException {
126         ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data");
127         expectRemoteCommandToSucceed("test -f /sys/fs/" + mountPoint.type + "/features/verity");
128     }
129 
130     @Test
testInstallBase()131     public void testInstallBase() throws DeviceNotAvailableException, FileNotFoundException {
132         new InstallMultiple()
133                 .addFileAndSignature(BASE_APK)
134                 .run();
135         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
136 
137         verifyInstalledFiles(
138                 INSTALLED_BASE_APK,
139                 INSTALLED_BASE_APK_FSV_SIG);
140         verifyInstalledFilesHaveFsverity();
141     }
142 
143     @Test
testInstallBaseWithWrongSignature()144     public void testInstallBaseWithWrongSignature()
145             throws DeviceNotAvailableException, FileNotFoundException {
146         new InstallMultiple()
147                 .addFile(BASE_APK)
148                 .addFile(SPLIT_APK_DM + ".fsv_sig",
149                         BASE_APK + ".fsv_sig")
150                 .runExpectingFailure();
151     }
152 
153     @Test
testInstallBaseWithSplit()154     public void testInstallBaseWithSplit()
155             throws DeviceNotAvailableException, FileNotFoundException {
156         new InstallMultiple()
157                 .addFileAndSignature(BASE_APK)
158                 .addFileAndSignature(SPLIT_APK)
159                 .run();
160         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
161 
162         verifyInstalledFiles(
163                 INSTALLED_BASE_APK,
164                 INSTALLED_BASE_APK_FSV_SIG,
165                 INSTALLED_SPLIT_APK,
166                 INSTALLED_SPLIT_APK_FSV_SIG);
167         verifyInstalledFilesHaveFsverity();
168     }
169 
170     @Test
testInstallBaseWithDm()171     public void testInstallBaseWithDm() throws DeviceNotAvailableException, FileNotFoundException {
172         new InstallMultiple()
173                 .addFileAndSignature(BASE_APK)
174                 .addFileAndSignature(BASE_APK_DM)
175                 .run();
176         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
177 
178         verifyInstalledFiles(
179                 INSTALLED_BASE_APK,
180                 INSTALLED_BASE_APK_FSV_SIG,
181                 INSTALLED_BASE_DM,
182                 INSTALLED_BASE_DM_FSV_SIG);
183         verifyInstalledFilesHaveFsverity();
184     }
185 
186     @Test
testInstallEverything()187     public void testInstallEverything() throws DeviceNotAvailableException, FileNotFoundException {
188         new InstallMultiple()
189                 .addFileAndSignature(BASE_APK)
190                 .addFileAndSignature(BASE_APK_DM)
191                 .addFileAndSignature(SPLIT_APK)
192                 .addFileAndSignature(SPLIT_APK_DM)
193                 .run();
194         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
195 
196         verifyInstalledFiles(
197                 INSTALLED_BASE_APK,
198                 INSTALLED_BASE_APK_FSV_SIG,
199                 INSTALLED_BASE_DM,
200                 INSTALLED_BASE_DM_FSV_SIG,
201                 INSTALLED_SPLIT_APK,
202                 INSTALLED_SPLIT_APK_FSV_SIG,
203                 INSTALLED_SPLIT_DM,
204                 INSTALLED_SPLIT_DM_FSV_SIG);
205         verifyInstalledFilesHaveFsverity();
206     }
207 
208     @Test
testInstallSplitOnly()209     public void testInstallSplitOnly()
210             throws DeviceNotAvailableException, FileNotFoundException {
211         new InstallMultiple()
212                 .addFileAndSignature(BASE_APK)
213                 .run();
214         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
215         verifyInstalledFiles(
216                 INSTALLED_BASE_APK,
217                 INSTALLED_BASE_APK_FSV_SIG);
218 
219         new InstallMultiple()
220                 .inheritFrom(TARGET_PACKAGE)
221                 .addFileAndSignature(SPLIT_APK)
222                 .run();
223 
224         verifyInstalledFiles(
225                 INSTALLED_BASE_APK,
226                 INSTALLED_BASE_APK_FSV_SIG,
227                 INSTALLED_SPLIT_APK,
228                 INSTALLED_SPLIT_APK_FSV_SIG);
229         verifyInstalledFilesHaveFsverity();
230     }
231 
232     @Test
testInstallSplitOnlyMissingSignature()233     public void testInstallSplitOnlyMissingSignature()
234             throws DeviceNotAvailableException, FileNotFoundException {
235         new InstallMultiple()
236                 .addFileAndSignature(BASE_APK)
237                 .run();
238         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
239         verifyInstalledFiles(
240                 INSTALLED_BASE_APK,
241                 INSTALLED_BASE_APK_FSV_SIG);
242 
243         new InstallMultiple()
244                 .inheritFrom(TARGET_PACKAGE)
245                 .addFile(SPLIT_APK)
246                 .runExpectingFailure();
247     }
248 
249     @Test
testInstallSplitOnlyWithoutBaseSignature()250     public void testInstallSplitOnlyWithoutBaseSignature()
251             throws DeviceNotAvailableException, FileNotFoundException {
252         new InstallMultiple()
253                 .addFile(BASE_APK)
254                 .run();
255         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
256         verifyInstalledFiles(INSTALLED_BASE_APK);
257 
258         new InstallMultiple()
259                 .inheritFrom(TARGET_PACKAGE)
260                 .addFileAndSignature(SPLIT_APK)
261                 .run();
262         verifyInstalledFiles(
263                 INSTALLED_BASE_APK,
264                 INSTALLED_SPLIT_APK,
265                 INSTALLED_SPLIT_APK_FSV_SIG);
266 
267     }
268 
269     @Test
testInstallOnlyBaseHasFsvSig()270     public void testInstallOnlyBaseHasFsvSig()
271             throws DeviceNotAvailableException, FileNotFoundException {
272         new InstallMultiple()
273                 .addFileAndSignature(BASE_APK)
274                 .addFile(BASE_APK_DM)
275                 .addFile(SPLIT_APK)
276                 .addFile(SPLIT_APK_DM)
277                 .runExpectingFailure();
278     }
279 
280     @Test
testInstallOnlyDmHasFsvSig()281     public void testInstallOnlyDmHasFsvSig()
282             throws DeviceNotAvailableException, FileNotFoundException {
283         new InstallMultiple()
284                 .addFile(BASE_APK)
285                 .addFileAndSignature(BASE_APK_DM)
286                 .addFile(SPLIT_APK)
287                 .addFile(SPLIT_APK_DM)
288                 .runExpectingFailure();
289     }
290 
291     @Test
testInstallOnlySplitHasFsvSig()292     public void testInstallOnlySplitHasFsvSig()
293             throws DeviceNotAvailableException, FileNotFoundException {
294         new InstallMultiple()
295                 .addFile(BASE_APK)
296                 .addFile(BASE_APK_DM)
297                 .addFileAndSignature(SPLIT_APK)
298                 .addFile(SPLIT_APK_DM)
299                 .runExpectingFailure();
300     }
301 
302     @Test
testInstallBaseWithFsvSigThenSplitWithout()303     public void testInstallBaseWithFsvSigThenSplitWithout()
304             throws DeviceNotAvailableException, FileNotFoundException {
305         new InstallMultiple()
306                 .addFileAndSignature(BASE_APK)
307                 .run();
308         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
309         verifyInstalledFiles(
310                 INSTALLED_BASE_APK,
311                 INSTALLED_BASE_APK_FSV_SIG);
312 
313         new InstallMultiple()
314                 .addFile(SPLIT_APK)
315                 .runExpectingFailure();
316     }
317 
318     @Test
testInstallBaseWithoutFsvSigThenSplitWith()319     public void testInstallBaseWithoutFsvSigThenSplitWith()
320             throws DeviceNotAvailableException, FileNotFoundException {
321         new InstallMultiple()
322                 .addFile(BASE_APK)
323                 .run();
324         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
325         verifyInstalledFiles(INSTALLED_BASE_APK);
326 
327         new InstallMultiple()
328                 .addFileAndSignature(SPLIT_APK)
329                 .runExpectingFailure();
330     }
331 
332     @Test
testFsverityFileIsImmutableAndReadable()333     public void testFsverityFileIsImmutableAndReadable() throws DeviceNotAvailableException {
334         new InstallMultiple().addFileAndSignature(BASE_APK).run();
335         String apkPath = getApkPath(TARGET_PACKAGE);
336 
337         assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
338         expectRemoteCommandToFail("echo -n '' >> " + apkPath);
339         expectRemoteCommandToSucceed("cat " + apkPath + " > /dev/null");
340     }
341 
342     @Test
testFsverityFailToReadModifiedBlockAtFront()343     public void testFsverityFailToReadModifiedBlockAtFront() throws DeviceNotAvailableException {
344         new InstallMultiple().addFileAndSignature(BASE_APK).run();
345         String apkPath = getApkPath(TARGET_PACKAGE);
346 
347         long apkSize = getFileSizeInBytes(apkPath);
348         long offsetFirstByte = 0;
349 
350         // The first two pages should be both readable at first.
351         assertTrue(canReadByte(apkPath, offsetFirstByte));
352         if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) {
353             assertTrue(canReadByte(apkPath, offsetFirstByte + FSVERITY_PAGE_SIZE));
354         }
355 
356         // Damage the file directly against the block device.
357         damageFileAgainstBlockDevice(apkPath, offsetFirstByte);
358 
359         // Expect actual read from disk to fail but only at damaged page.
360         dropCaches();
361         assertFalse(canReadByte(apkPath, offsetFirstByte));
362         if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) {
363             long lastByteOfTheSamePage =
364                     offsetFirstByte % FSVERITY_PAGE_SIZE + FSVERITY_PAGE_SIZE - 1;
365             assertFalse(canReadByte(apkPath, lastByteOfTheSamePage));
366             assertTrue(canReadByte(apkPath, lastByteOfTheSamePage + 1));
367         }
368     }
369 
370     @Test
testFsverityFailToReadModifiedBlockAtBack()371     public void testFsverityFailToReadModifiedBlockAtBack() throws DeviceNotAvailableException {
372         new InstallMultiple().addFileAndSignature(BASE_APK).run();
373         String apkPath = getApkPath(TARGET_PACKAGE);
374 
375         long apkSize = getFileSizeInBytes(apkPath);
376         long offsetOfLastByte = apkSize - 1;
377 
378         // The first two pages should be both readable at first.
379         assertTrue(canReadByte(apkPath, offsetOfLastByte));
380         if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) {
381             assertTrue(canReadByte(apkPath, offsetOfLastByte - FSVERITY_PAGE_SIZE));
382         }
383 
384         // Damage the file directly against the block device.
385         damageFileAgainstBlockDevice(apkPath, offsetOfLastByte);
386 
387         // Expect actual read from disk to fail but only at damaged page.
388         dropCaches();
389         assertFalse(canReadByte(apkPath, offsetOfLastByte));
390         if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) {
391             long firstByteOfTheSamePage = offsetOfLastByte - offsetOfLastByte % FSVERITY_PAGE_SIZE;
392             assertFalse(canReadByte(apkPath, firstByteOfTheSamePage));
393             assertTrue(canReadByte(apkPath, firstByteOfTheSamePage - 1));
394         }
395     }
396 
verifyInstalledFilesHaveFsverity()397     private void verifyInstalledFilesHaveFsverity() throws DeviceNotAvailableException {
398         // Verify that all files are protected by fs-verity
399         String apkPath = getApkPath(TARGET_PACKAGE);
400         String appDir = apkPath.substring(0, apkPath.lastIndexOf("/"));
401         long kTargetOffset = 0;
402         for (String basename : expectRemoteCommandToSucceed("ls " + appDir).split("\n")) {
403             if (basename.endsWith(".apk") || basename.endsWith(".dm")) {
404                 String path = appDir + "/" + basename;
405                 damageFileAgainstBlockDevice(path, kTargetOffset);
406 
407                 // Retry is sometimes needed to pass the test. Package manager may have FD leaks
408                 // (see b/122744005 as example) that prevents the file in question to be evicted
409                 // from filesystem cache. Forcing GC workarounds the problem.
410                 int retry = 5;
411                 for (; retry > 0; retry--) {
412                     dropCaches();
413                     if (!canReadByte(path, kTargetOffset)) {
414                         break;
415                     }
416                     try {
417                         CLog.d("lsof: " + expectRemoteCommandToSucceed("lsof " + apkPath));
418                         Thread.sleep(1000);
419                         String pid = expectRemoteCommandToSucceed("pidof system_server");
420                         mDevice.executeShellV2Command("kill -10 " + pid);  // force GC
421                     } catch (InterruptedException e) {
422                         Thread.currentThread().interrupt();
423                         return;
424                     }
425                 }
426                 assertTrue("Read from " + path + " should fail", retry > 0);
427             }
428         }
429     }
430 
verifyInstalledFiles(String... filenames)431     private void verifyInstalledFiles(String... filenames) throws DeviceNotAvailableException {
432         String apkPath = getApkPath(TARGET_PACKAGE);
433         String appDir = apkPath.substring(0, apkPath.lastIndexOf("/"));
434         // Exclude directories since we only care about files.
435         HashSet<String> actualFiles = new HashSet<>(Arrays.asList(
436                 expectRemoteCommandToSucceed("ls -p " + appDir + " | grep -v '/'").split("\n")));
437 
438         HashSet<String> expectedFiles = new HashSet<>(Arrays.asList(filenames));
439         assertEquals(expectedFiles, actualFiles);
440     }
441 
damageFileAgainstBlockDevice(String path, long offsetOfTargetingByte)442     private void damageFileAgainstBlockDevice(String path, long offsetOfTargetingByte)
443             throws DeviceNotAvailableException {
444         assertTrue(path.startsWith("/data/"));
445         ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data");
446         ArrayList<String> args = new ArrayList<>();
447         args.add(DAMAGING_EXECUTABLE);
448         if ("f2fs".equals(mountPoint.type)) {
449             args.add("--use-f2fs-pinning");
450         }
451         args.add(mountPoint.filesystem);
452         args.add(path);
453         args.add(Long.toString(offsetOfTargetingByte));
454         expectRemoteCommandToSucceed(String.join(" ", args));
455     }
456 
getApkPath(String packageName)457     private String getApkPath(String packageName) throws DeviceNotAvailableException {
458         String line = expectRemoteCommandToSucceed("pm path " + packageName + " | grep base.apk");
459         int index = line.trim().indexOf(":");
460         assertTrue(index >= 0);
461         return line.substring(index + 1);
462     }
463 
getFileSizeInBytes(String packageName)464     private long getFileSizeInBytes(String packageName) throws DeviceNotAvailableException {
465         return Long.parseLong(expectRemoteCommandToSucceed("stat -c '%s' " + packageName).trim());
466     }
467 
dropCaches()468     private void dropCaches() throws DeviceNotAvailableException {
469         expectRemoteCommandToSucceed("sync && echo 1 > /proc/sys/vm/drop_caches");
470     }
471 
canReadByte(String filePath, long offset)472     private boolean canReadByte(String filePath, long offset) throws DeviceNotAvailableException {
473         CommandResult result = mDevice.executeShellV2Command(
474                 "dd if=" + filePath + " bs=1 count=1 skip=" + Long.toString(offset));
475         return result.getStatus() == CommandStatus.SUCCESS;
476     }
477 
expectRemoteCommandToSucceed(String cmd)478     private String expectRemoteCommandToSucceed(String cmd) throws DeviceNotAvailableException {
479         CommandResult result = mDevice.executeShellV2Command(cmd);
480         assertEquals("`" + cmd + "` failed: " + result.getStderr(), CommandStatus.SUCCESS,
481                 result.getStatus());
482         return result.getStdout();
483     }
484 
expectRemoteCommandToFail(String cmd)485     private void expectRemoteCommandToFail(String cmd) throws DeviceNotAvailableException {
486         CommandResult result = mDevice.executeShellV2Command(cmd);
487         assertTrue("Unexpected success from `" + cmd + "`: " + result.getStderr(),
488                 result.getStatus() != CommandStatus.SUCCESS);
489     }
490 
491     private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> {
InstallMultiple()492         InstallMultiple() {
493             super(getDevice(), getBuild());
494         }
495 
addFileAndSignature(String filename)496         InstallMultiple addFileAndSignature(String filename) {
497             try {
498                 addFile(filename);
499                 addFile(filename + ".fsv_sig");
500             } catch (FileNotFoundException e) {
501                 fail("Missing test file: " + e);
502             }
503             return this;
504         }
505     }
506 }
507