1 /*
2  * Copyright (C) 2020 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.security.cts;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertThrows;
23 import static org.junit.Assert.assertTrue;
24 import static org.junit.Assume.assumeTrue;
25 
26 import android.content.Context;
27 import android.content.pm.PackageManager;
28 import android.os.Build;
29 import android.os.Environment;
30 import android.platform.test.annotations.AppModeFull;
31 import android.platform.test.annotations.RequiresFlagsEnabled;
32 import android.platform.test.annotations.RestrictedBuildTest;
33 import android.platform.test.flag.junit.CheckFlagsRule;
34 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
35 import android.security.FileIntegrityManager;
36 import android.security.Flags;
37 import android.util.Log;
38 
39 import androidx.test.platform.app.InstrumentationRegistry;
40 import androidx.test.runner.AndroidJUnit4;
41 
42 import com.android.compatibility.common.util.AdoptShellPermissionsRule;
43 import com.android.compatibility.common.util.ApiTest;
44 import com.android.compatibility.common.util.CddTest;
45 import com.android.compatibility.common.util.PropertyUtil;
46 
47 import org.junit.After;
48 import org.junit.Before;
49 import org.junit.Rule;
50 import org.junit.Test;
51 import org.junit.runner.RunWith;
52 
53 import java.io.ByteArrayInputStream;
54 import java.io.ByteArrayOutputStream;
55 import java.io.File;
56 import java.io.FileOutputStream;
57 import java.io.IOException;
58 import java.io.InputStream;
59 import java.security.cert.CertificateException;
60 import java.security.cert.CertificateFactory;
61 import java.security.cert.X509Certificate;
62 import java.util.ArrayList;
63 import java.util.Arrays;
64 import java.util.HexFormat;
65 import java.util.List;
66 
67 @AppModeFull
68 @RunWith(AndroidJUnit4.class)
69 public class FileIntegrityManagerTest {
70 
71     private static final String TAG = "FileIntegrityManagerTest";
72     private static final String FILENAME = "test.file";
73     private static final String TEST_FILE_CONTENT = "fs-verity";
74     // Produced by shell command: echo -n 'fs-verity' > t; fsverity digest --compact t
75     private static final String TEST_FILE_DIGEST =
76             "1e5300d45dce778c0aa6ced72954a8a4398d8f5d590c73cb431fe5fe2adbeeed";
77     private static final int MIN_REQUIRED_API_LEVEL = 30;
78 
79     private Context mContext;
80     private FileIntegrityManager mFileIntegrityManager;
81     private CertificateFactory mCertFactory;
82 
83     @Rule
84     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
85 
86     @Rule
87     public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
88             InstrumentationRegistry.getInstrumentation().getUiAutomation(),
89             android.Manifest.permission.SETUP_FSVERITY);
90 
91     @Before
setUp()92     public void setUp() throws Exception {
93         mContext = InstrumentationRegistry.getInstrumentation().getContext();
94         // This feature name check only applies to devices that first shipped with
95         // SC or later.
96         final int firstApiLevel =
97                 Math.min(PropertyUtil.getFirstApiLevel(), PropertyUtil.getVendorApiLevel());
98         if (firstApiLevel >= Build.VERSION_CODES.S) {
99             // Assumes every test in this file asserts a requirement of CDD section 9.
100             assumeTrue("Skipping test: FEATURE_SECURITY_MODEL_COMPATIBLE missing.",
101                     mContext.getPackageManager()
102                     .hasSystemFeature(PackageManager.FEATURE_SECURITY_MODEL_COMPATIBLE));
103         }
104 
105         mFileIntegrityManager = mContext.getSystemService(FileIntegrityManager.class);
106         mCertFactory = CertificateFactory.getInstance("X.509");
107     }
108 
109     @After
tearDown()110     public void tearDown() throws Exception {
111         var files = newSupportedFiles();
112         files.addAll(newUnsupportedFiles());
113         for (var file : files) {
114             if (file.exists()) {
115                 file.delete();
116             }
117         }
118     }
119 
120     @CddTest(requirement="9.10/C-0-3,C-1-1")
121     @Test
testSupportedOnDevicesFirstLaunchedWithR()122     public void testSupportedOnDevicesFirstLaunchedWithR() throws Exception {
123         if (PropertyUtil.getFirstApiLevel() >= MIN_REQUIRED_API_LEVEL) {
124             assertTrue(mFileIntegrityManager.isApkVeritySupported());
125         }
126     }
127 
128     @CddTest(requirement="9.10/C-0-3,C-1-1")
129     @Test
testIsAppSourceCertificateTrusted()130     public void testIsAppSourceCertificateTrusted() throws Exception {
131         boolean isReleaseCertTrusted = mFileIntegrityManager.isAppSourceCertificateTrusted(
132                 readAssetAsX509Certificate("fsverity-release.x509.der"));
133         if (!Flags.deprecateFsvSig()
134                 && mFileIntegrityManager.isApkVeritySupported()) {
135             assertTrue(isReleaseCertTrusted);
136         } else {
137             assertFalse(isReleaseCertTrusted);
138         }
139     }
140 
141     @CddTest(requirement="9.10/C-0-3,C-1-1")
142     @RestrictedBuildTest
143     @Test
testPlatformDebugCertificateNotTrusted()144     public void testPlatformDebugCertificateNotTrusted() throws Exception {
145         boolean isDebugCertTrusted = mFileIntegrityManager.isAppSourceCertificateTrusted(
146                 readAssetAsX509Certificate("fsverity-debug.x509.der"));
147         assertFalse(isDebugCertTrusted);
148     }
149 
readAssetAsX509Certificate(String assetName)150     private X509Certificate readAssetAsX509Certificate(String assetName)
151             throws CertificateException, IOException {
152         InputStream is = mContext.getAssets().open(assetName);
153         return toX509Certificate(readAllBytes(is));
154     }
155 
156     // TODO: Switch to InputStream#readAllBytes when Java 9 is supported
readAllBytes(InputStream is)157     private byte[] readAllBytes(InputStream is) throws IOException {
158         ByteArrayOutputStream output = new ByteArrayOutputStream();
159         byte[] buf = new byte[8192];
160         int len;
161         while ((len = is.read(buf, 0, buf.length)) > 0) {
162             output.write(buf, 0, len);
163         }
164         return output.toByteArray();
165     }
166 
toX509Certificate(byte[] bytes)167     private X509Certificate toX509Certificate(byte[] bytes) throws CertificateException {
168         return (X509Certificate) mCertFactory.generateCertificate(new ByteArrayInputStream(bytes));
169     }
170 
171     @Test
172     @ApiTest(apis = {"android.security.FileIntegrityManager#setupFsVerity",
173             "android.security.FileIntegrityManager#getFsVerityDigest"})
174     @RequiresFlagsEnabled(Flags.FLAG_FSVERITY_API)
testEnableAndMeasureFsVerityByFile()175     public void testEnableAndMeasureFsVerityByFile() throws Exception {
176         var files = newSupportedFiles();
177         for (var file : files) {
178             Log.d(TAG, "testEnableAndMeasureFsVerityByFile: " + file);
179 
180             createTestFile(file);
181             mFileIntegrityManager.setupFsVerity(file);
182 
183             byte[] actualDigest = mFileIntegrityManager.getFsVerityDigest(file);
184             assertThat(actualDigest).isNotNull();
185             assertThat(HexFormat.of().formatHex(actualDigest)).isEqualTo(TEST_FILE_DIGEST);
186         }
187     }
188 
189     @Test
190     @ApiTest(apis = {"android.security.FileIntegrityManager#setupFsVerity"})
191     @RequiresFlagsEnabled(Flags.FLAG_FSVERITY_API)
testFailToEnableUnsupportedLocation()192     public void testFailToEnableUnsupportedLocation() throws Exception {
193         var files = newUnsupportedFiles();
194         for (var file : files) {
195             Log.d(TAG, "testFailToEnableUnsupportedLocation: " + file);
196 
197             createTestFile(file);
198             assertThrows(Exception.class, () -> mFileIntegrityManager.setupFsVerity(file));
199         }
200     }
201 
202     @Test
203     @ApiTest(apis = {"android.security.FileIntegrityManager#setupFsVerity"})
204     @RequiresFlagsEnabled(Flags.FLAG_FSVERITY_API)
testFailToEnableWithOpenedWritableFd()205     public void testFailToEnableWithOpenedWritableFd() throws Exception {
206         var files = newSupportedFiles();
207         for (var file : files) {
208             Log.d(TAG, "testFailToEnableWithOpenedWritableFd: " + file);
209 
210             var fos = new FileOutputStream(file);
211             fos.write(TEST_FILE_CONTENT.getBytes());
212 
213             // With any writable fd, the call will fail.
214             assertThrows(IOException.class, () ->
215                     mFileIntegrityManager.setupFsVerity(file));
216         }
217     }
218 
219     @Test
220     @ApiTest(apis = {"android.security.FileIntegrityManager#getFsVerityDigest"})
221     @RequiresFlagsEnabled(Flags.FLAG_FSVERITY_API)
testMeasureWithoutFsVerity()222     public void testMeasureWithoutFsVerity() throws Exception {
223         var files = newSupportedFiles();
224         for (var file : files) {
225             Log.d(TAG, "testMeasureWithoutFsVerity: " + file);
226 
227             createTestFile(file);
228 
229             byte[] actualDigest = mFileIntegrityManager.getFsVerityDigest(file);
230             assertThat(actualDigest).isNull();
231         }
232     }
233 
createTestFile(File file)234     private void createTestFile(File file) throws IOException {
235         try (var fos = new FileOutputStream(file)) {
236             fos.write(TEST_FILE_CONTENT.getBytes());
237         }
238     }
239 
newSupportedFiles()240     private List<File> newSupportedFiles() {
241         var ceContext = mContext.createCredentialProtectedStorageContext();
242         var deContext = mContext.createDeviceProtectedStorageContext();
243         return new ArrayList<>(Arrays.asList(
244                 new File(ceContext.getDataDir(), FILENAME),
245                 new File(ceContext.getFilesDir(), FILENAME),
246                 new File(deContext.getDataDir(), FILENAME),
247                 new File(deContext.getFilesDir(), FILENAME)));
248     }
249 
newUnsupportedFiles()250     private List<File> newUnsupportedFiles() {
251         return new ArrayList<>(Arrays.asList(
252                 new File(mContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), FILENAME),
253                 new File(mContext.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), FILENAME)));
254     }
255 }
256