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 android.appsecurity.cts; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import static org.junit.Assume.assumeTrue; 23 24 import android.platform.test.annotations.RestrictedBuildTest; 25 26 import com.android.tradefed.device.DeviceNotAvailableException; 27 import com.android.tradefed.device.ITestDevice; 28 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 29 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 30 import com.android.tradefed.util.FileUtil; 31 import com.android.tradefed.util.ZipUtil; 32 33 import com.google.common.truth.Expect; 34 35 import org.junit.AfterClass; 36 import org.junit.Before; 37 import org.junit.Ignore; 38 import org.junit.Rule; 39 import org.junit.Test; 40 import org.junit.rules.TestRule; 41 import org.junit.runner.Description; 42 import org.junit.runner.RunWith; 43 import org.junit.runners.model.Statement; 44 45 import java.io.File; 46 import java.io.IOException; 47 import java.io.InputStream; 48 import java.net.URISyntaxException; 49 import java.util.ArrayList; 50 import java.util.Collection; 51 import java.util.Enumeration; 52 import java.util.HashMap; 53 import java.util.Iterator; 54 import java.util.List; 55 import java.util.Map; 56 import java.util.Set; 57 import java.util.regex.Pattern; 58 import java.util.zip.ZipEntry; 59 import java.util.zip.ZipException; 60 import java.util.zip.ZipFile; 61 62 /** 63 * Tests for APEX signature verification to ensure preloaded APEXes 64 * DO NOT signed with well-known keys. 65 */ 66 @RunWith(DeviceJUnit4ClassRunner.class) 67 public class ApexSignatureVerificationTest extends BaseHostJUnit4Test { 68 69 private static final String TEST_BASE = "ApexSignatureVerificationTest"; 70 private static final String TEST_APEX_SOURCE_DIR_PREFIX = "tests-apex_"; 71 private static final String APEX_PUB_KEY_NAME = "apex_pubkey"; 72 73 private static final Pattern WELL_KNOWN_PUBKEY_PATTERN = Pattern.compile( 74 "^apexsigverify\\/.*.avbpubkey"); 75 76 private static boolean mHasTestFailure; 77 78 private static File mBasePath; 79 private static File mWellKnownKeyStorePath; 80 private static File mArchiveZip; 81 82 private static Map<String, String> mPreloadedApexPathMap = new HashMap<>(); 83 private static Map<String, File> mLocalApexFileMap = new HashMap<>(); 84 private static Map<String, File> mExtractedTestDirMap = new HashMap<>(); 85 private static List<File> mWellKnownKeyFileList = new ArrayList<>(); 86 private ITestDevice mDevice; 87 88 @Rule 89 public final Expect mExpect = Expect.create(); 90 91 @Before setUp()92 public void setUp() throws Exception { 93 mDevice = getDevice(); 94 if (mBasePath == null && mWellKnownKeyStorePath == null 95 && mExtractedTestDirMap.size() == 0) { 96 mBasePath = FileUtil.createTempDir(TEST_BASE); 97 mBasePath.deleteOnExit(); 98 mWellKnownKeyStorePath = FileUtil.createTempDir("wellknownsignatures", mBasePath); 99 mWellKnownKeyStorePath.deleteOnExit(); 100 pullWellKnownSignatures(); 101 getApexPackageList(); 102 pullApexFiles(); 103 extractApexFiles(); 104 } 105 } 106 107 @AfterClass tearDownClass()108 public static void tearDownClass() throws IOException { 109 if (mArchiveZip == null && mHasTestFailure) { 110 // Archive all operation data and materials in host 111 // /tmp/ApexSignatureVerificationTest.zip 112 // in case the test result is not expected and need to debug. 113 mArchiveZip = ZipUtil.createZip(mBasePath, mBasePath.getName()); 114 } 115 } 116 117 @Rule 118 public final OnFailureRule mDumpOnFailureRule = new OnFailureRule() { 119 @Override 120 protected void onTestFailure(Statement base, Description description, Throwable t) { 121 mHasTestFailure = true; 122 } 123 }; 124 125 @Test testApexIncludePubKey()126 public void testApexIncludePubKey() { 127 for (Map.Entry<String, File> entry : mExtractedTestDirMap.entrySet()) { 128 final File pubKeyFile = FileUtil.findFile(entry.getValue(), APEX_PUB_KEY_NAME); 129 130 assertWithMessage("apex:" + entry.getKey() + " does not contain pubkey").that( 131 pubKeyFile.exists()).isTrue(); 132 } 133 } 134 135 /** 136 * Assert that the preloaded apexes are secure, not signed with wellknown keys. 137 * 138 * Debuggable aosp or gsi rom could not preload official apexes module allowing. 139 * 140 * Note: This test will fail on userdebug / eng devices, but should pass 141 * on production (user) builds. 142 */ 143 @SuppressWarnings("productionOnly") 144 @RestrictedBuildTest 145 @Test testApexPubKeyIsNotWellKnownKey()146 public void testApexPubKeyIsNotWellKnownKey() { 147 for (Map.Entry<String, File> entry : mExtractedTestDirMap.entrySet()) { 148 final File pubKeyFile = FileUtil.findFile(entry.getValue(), APEX_PUB_KEY_NAME); 149 final Iterator it = mWellKnownKeyFileList.iterator(); 150 151 assertThat(pubKeyFile).isNotNull(); 152 153 while (it.hasNext()) { 154 final File wellKnownKey = (File) it.next(); 155 mExpect.withMessage( 156 entry.getKey() + " must not use well known pubkey " 157 + wellKnownKey.getName()) 158 .that(areKeysMatching(pubKeyFile, wellKnownKey)).isFalse(); 159 } 160 } 161 } 162 163 @Ignore 164 @Test testApexPubKeyMatchPayloadImg()165 public void testApexPubKeyMatchPayloadImg() { 166 // TODO(b/142919428): Need more investigation to find a way verify apex_paylaod.img 167 // was signed by apex_pubkey 168 } 169 extractApexFiles()170 private void extractApexFiles() { 171 final String subFilesFilter = "\\w+.*"; 172 173 try { 174 for (Map.Entry<String, File> entry : mLocalApexFileMap.entrySet()) { 175 final String testSrcDirPath = TEST_APEX_SOURCE_DIR_PREFIX + entry.getKey(); 176 File apexDir = FileUtil.createTempDir(testSrcDirPath, mBasePath); 177 apexDir.deleteOnExit(); 178 ZipUtil.extractZip(new ZipFile(entry.getValue()), apexDir); 179 180 assertThat(apexDir).isNotNull(); 181 182 mExtractedTestDirMap.put(entry.getKey(), apexDir); 183 184 assertThat(FileUtil.findFiles(apexDir, subFilesFilter)).isNotNull(); 185 } 186 } catch (IOException e) { 187 throw new AssertionError("extractApexFile IOException" + e); 188 } 189 } 190 getApexPackageList()191 private void getApexPackageList() { 192 Set<ITestDevice.ApexInfo> apexes; 193 try { 194 apexes = mDevice.getActiveApexes(); 195 for (ITestDevice.ApexInfo ap : apexes) { 196 // Compressed APEXes on /system are decompressed to /data/apex/decompressed 197 if (!ap.sourceDir.startsWith("/data/apex/active")) { 198 mPreloadedApexPathMap.put(ap.name, ap.sourceDir); 199 } 200 } 201 202 assumeTrue("No active APEX packages or all APEX packages have been already updated", 203 mPreloadedApexPathMap.size() > 0); 204 } catch (DeviceNotAvailableException e) { 205 throw new AssertionError("getApexPackageList DeviceNotAvailableException" + e); 206 } 207 } 208 getResourcesFromJarFile(final File file, final Pattern pattern)209 private static Collection<String> getResourcesFromJarFile(final File file, 210 final Pattern pattern) { 211 final ArrayList<String> candidateList = new ArrayList<>(); 212 ZipFile zf; 213 try { 214 zf = new ZipFile(file); 215 assertThat(zf).isNotNull(); 216 } catch (final ZipException e) { 217 throw new AssertionError("Query Jar file ZipException" + e); 218 } catch (final IOException e) { 219 throw new AssertionError("Query Jar file IOException" + e); 220 } 221 final Enumeration e = zf.entries(); 222 while (e.hasMoreElements()) { 223 final ZipEntry ze = (ZipEntry) e.nextElement(); 224 final String fileName = ze.getName(); 225 final boolean isMatch = pattern.matcher(fileName).matches(); 226 if (isMatch) { 227 candidateList.add(fileName); 228 } 229 } 230 try { 231 zf.close(); 232 } catch (final IOException e1) { 233 } 234 return candidateList; 235 } 236 pullApexFiles()237 private void pullApexFiles() { 238 try { 239 for (Map.Entry<String, String> entry : mPreloadedApexPathMap.entrySet()) { 240 final File localTempFile = File.createTempFile(entry.getKey(), "", mBasePath); 241 242 assertThat(localTempFile).isNotNull(); 243 assertThat(mDevice.pullFile(entry.getValue(), localTempFile)).isTrue(); 244 245 mLocalApexFileMap.put(entry.getKey(), localTempFile); 246 } 247 } catch (DeviceNotAvailableException e) { 248 throw new AssertionError("pullApexFile DeviceNotAvailableException" + e); 249 } catch (IOException e) { 250 throw new AssertionError("pullApexFile IOException" + e); 251 } 252 } 253 pullWellKnownSignatures()254 private void pullWellKnownSignatures() { 255 final Collection<String> keyPath; 256 257 try { 258 File jarFile = new File( 259 this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI()); 260 keyPath = getResourcesFromJarFile(jarFile, WELL_KNOWN_PUBKEY_PATTERN); 261 262 assertThat(keyPath).isNotNull(); 263 } catch (URISyntaxException e) { 264 throw new AssertionError("Iterate well-known key name from jar IOException" + e); 265 } 266 267 Iterator<String> keyIterator = keyPath.iterator(); 268 while (keyIterator.hasNext()) { 269 final String tmpKeyPath = keyIterator.next(); 270 final String keyFileName = tmpKeyPath.substring(tmpKeyPath.lastIndexOf("/")); 271 File outFile; 272 try (InputStream in = getClass().getResourceAsStream("/" + tmpKeyPath)) { 273 outFile = File.createTempFile(keyFileName, "", mWellKnownKeyStorePath); 274 mWellKnownKeyFileList.add(outFile); 275 FileUtil.writeToFile(in, outFile); 276 } catch (IOException e) { 277 throw new AssertionError("Copy well-known keys to tmp IOException" + e); 278 } 279 } 280 281 assertThat(mWellKnownKeyFileList).isNotEmpty(); 282 } 283 areKeysMatching(File pubkey, File wellknownKey)284 private static boolean areKeysMatching(File pubkey, File wellknownKey) { 285 try { 286 return FileUtil.compareFileContents(pubkey, wellknownKey); 287 } catch (IOException e) { 288 throw new AssertionError( 289 "Failed to compare " + pubkey.getAbsolutePath() + " and " 290 + wellknownKey.getAbsolutePath()); 291 } 292 } 293 294 /** 295 * Custom JUnit4 rule that provides a callback upon test failures. 296 */ 297 public abstract class OnFailureRule implements TestRule { OnFailureRule()298 public OnFailureRule() { 299 } 300 301 @Override apply(Statement base, Description description)302 public Statement apply(Statement base, Description description) { 303 return new Statement() { 304 305 @Override 306 public void evaluate() throws Throwable { 307 try { 308 base.evaluate(); 309 } catch (Throwable t) { 310 onTestFailure(base, description, t); 311 throw t; 312 } 313 } 314 }; 315 } 316 317 protected abstract void onTestFailure(Statement base, Description description, Throwable t); 318 } 319 } 320