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