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