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