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