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