• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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