1 /*
2  * Copyright (C) 2018 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 com.android.keychain;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertTrue;
23 import static org.junit.Assert.fail;
24 import static org.mockito.Mockito.any;
25 import static org.mockito.Mockito.anyInt;
26 import static org.mockito.Mockito.doReturn;
27 import static org.mockito.Mockito.doThrow;
28 import static org.mockito.Mockito.never;
29 import static org.mockito.Mockito.times;
30 import static org.mockito.Mockito.verify;
31 import static org.robolectric.Shadows.shadowOf;
32 
33 import android.app.admin.SecurityLog;
34 import android.content.Intent;
35 import android.content.pm.PackageManager;
36 import android.net.Uri;
37 import android.security.AppUriAuthenticationPolicy;
38 import android.security.IKeyChainService;
39 
40 import com.android.org.conscrypt.TrustedCertificateStore;
41 
42 import org.junit.Before;
43 import org.junit.Test;
44 import org.junit.runner.RunWith;
45 import org.mockito.Mock;
46 import org.mockito.MockitoAnnotations;
47 import org.robolectric.Robolectric;
48 import org.robolectric.RobolectricTestRunner;
49 import org.robolectric.RuntimeEnvironment;
50 import org.robolectric.android.controller.ServiceController;
51 import org.robolectric.annotation.Config;
52 import org.robolectric.shadows.ShadowPackageManager;
53 
54 import java.io.ByteArrayInputStream;
55 import java.io.IOException;
56 import java.security.KeyStore;
57 import java.security.cert.CertificateException;
58 import java.security.cert.CertificateFactory;
59 import java.security.cert.X509Certificate;
60 
61 import javax.security.auth.x500.X500Principal;
62 
63 @RunWith(RobolectricTestRunner.class)
64 @Config(shadows = {
65     ShadowTrustedCertificateStore.class,
66 })
67 public final class KeyChainServiceRoboTest {
68 
69     private static final String DEFAULT_KEYSTORE_TYPE = "BKS";
70 
71     private IKeyChainService.Stub mKeyChain;
72 
73     @Mock
74     private KeyChainService.Injector mockInjector;
75     @Mock
76     private TrustedCertificateStore mockCertStore;
77 
78     /*
79      * The CA cert below is the content of cacert.pem as generated by:
80      * openssl req -new -x509 -days 3650 -extensions v3_ca -keyout cakey.pem -out cacert.pem
81      */
82     private static final String TEST_CA =
83             "-----BEGIN CERTIFICATE-----\n" +
84             "MIIDXTCCAkWgAwIBAgIJAK9Tl/F9V8kSMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n" +
85             "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" +
86             "aWRnaXRzIFB0eSBMdGQwHhcNMTUwMzA2MTczMjExWhcNMjUwMzAzMTczMjExWjBF\n" +
87             "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n" +
88             "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" +
89             "CgKCAQEAvItOutsE75WBTgTyNAHt4JXQ3JoseaGqcC3WQij6vhrleWi5KJ0jh1/M\n" +
90             "Rpry7Fajtwwb4t8VZa0NuM2h2YALv52w1xivql88zce/HU1y7XzbXhxis9o6SCI+\n" +
91             "oVQSbPeXRgBPppFzBEh3ZqYTVhAqw451XhwdA4Aqs3wts7ddjwlUzyMdU44osCUg\n" +
92             "kVg7lfPf9sTm5IoHVcfLSCWH5n6Nr9sH3o2ksyTwxuOAvsN11F/a0mmUoPciYPp+\n" +
93             "q7DzQzdi7akRG601DZ4YVOwo6UITGvDyuAAdxl5isovUXqe6Jmz2/myTSpAKxGFs\n" +
94             "jk9oRoG6WXWB1kni490GIPjJ1OceyQIDAQABo1AwTjAdBgNVHQ4EFgQUH1QIlPKL\n" +
95             "p2OQ/AoLOjKvBW4zK3AwHwYDVR0jBBgwFoAUH1QIlPKLp2OQ/AoLOjKvBW4zK3Aw\n" +
96             "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcMi4voMMJHeQLjtq8Oky\n" +
97             "Azpyk8moDwgCd4llcGj7izOkIIFqq/lyqKdtykVKUWz2bSHO5cLrtaOCiBWVlaCV\n" +
98             "DYAnnVLM8aqaA6hJDIfaGs4zmwz0dY8hVMFCuCBiLWuPfiYtbEmjHGSmpQTG6Qxn\n" +
99             "ZJlaK5CZyt5pgh5EdNdvQmDEbKGmu0wpCq9qjZImwdyAul1t/B0DrsWApZMgZpeI\n" +
100             "d2od0VBrCICB1K4p+C51D93xyQiva7xQcCne+TAnGNy9+gjQ/MyR8MRpwRLv5ikD\n" +
101             "u0anJCN8pXo6IMglfMAsoton1J6o5/ae5uhC6caQU8bNUsCK570gpNfjkzo6rbP0\n" +
102             "wQ==\n" +
103             "-----END CERTIFICATE-----\n";
104 
105     private static final String NON_EXISTING_ALIAS = "alias-does-not-exist-1";
106 
107     private static final String TEST_PACKAGE_NAME_1 = "com.android.test";
108     private static final Uri TEST_URI_1 = Uri.parse("test.com");
109     private static final String TEST_ALIAS_1 = "testAlias";
110     private static final String CREDENTIAL_MANAGER_PACKAGE = "com.android.cred.mng.pkg";
111     private static final AppUriAuthenticationPolicy AUTHENTICATION_POLICY =
112             new AppUriAuthenticationPolicy.Builder()
113                     .addAppAndUriMapping(TEST_PACKAGE_NAME_1, TEST_URI_1, TEST_ALIAS_1)
114                     .build();
115 
116     private X509Certificate mCert;
117     private String mSubject;
118     private ShadowPackageManager mShadowPackageManager;
119 
120     @Before
setUp()121     public void setUp() throws Exception {
122         MockitoAnnotations.initMocks(this);
123         ShadowTrustedCertificateStore.sDelegate = mockCertStore;
124 
125         mCert = parseCertificate(TEST_CA);
126         mSubject = mCert.getSubjectX500Principal().getName(X500Principal.CANONICAL);
127 
128         final PackageManager packageManager = RuntimeEnvironment.application.getPackageManager();
129         mShadowPackageManager = shadowOf(packageManager);
130 
131         final ServiceController<KeyChainService> serviceController =
132                 Robolectric.buildService(KeyChainService.class);
133         final KeyChainService service = serviceController.get();
134         service.setInjector(mockInjector);
135         doReturn(KeyStore.getInstance(DEFAULT_KEYSTORE_TYPE))
136                 .when(mockInjector).getKeyStoreInstance();
137         serviceController.create().bind();
138 
139         final Intent intent = new Intent(IKeyChainService.class.getName());
140         mKeyChain = (IKeyChainService.Stub) service.onBind(intent);
141     }
142 
143     @Test
testCaInstallSuccessLogging()144     public void testCaInstallSuccessLogging() throws Exception {
145         setUpLoggingAndAccess(true);
146 
147         mKeyChain.installCaCertificate(TEST_CA.getBytes());
148 
149         verify(mockInjector, times(1)).writeSecurityEvent(
150                 SecurityLog.TAG_CERT_AUTHORITY_INSTALLED, 1 /* success */, mSubject, 0);
151     }
152 
153     @Test
testCaInstallFailedLogging()154     public void testCaInstallFailedLogging() throws Exception {
155         setUpLoggingAndAccess(true);
156 
157         doThrow(new IOException()).when(mockCertStore).installCertificate(any());
158 
159         try {
160             mKeyChain.installCaCertificate(TEST_CA.getBytes());
161             fail("didn't propagate the exception");
162         } catch (IllegalStateException expected) {
163             assertTrue(expected.getCause() instanceof IOException);
164         }
165 
166         verify(mockInjector, times(1)).writeSecurityEvent(
167                 SecurityLog.TAG_CERT_AUTHORITY_INSTALLED, 0 /* failure */, mSubject, 0);
168     }
169 
170     @Test
testCaRemoveSuccessLogging()171     public void testCaRemoveSuccessLogging() throws Exception {
172         setUpLoggingAndAccess(true);
173 
174         doReturn(mCert).when(mockCertStore).getCertificate("alias");
175 
176         mKeyChain.deleteCaCertificate("alias");
177 
178         verify(mockInjector, times(1)).writeSecurityEvent(
179                 SecurityLog.TAG_CERT_AUTHORITY_REMOVED, 1 /* success */, mSubject, 0);
180     }
181 
182     @Test
testCaRemoveFailedLogging()183     public void testCaRemoveFailedLogging() throws Exception {
184         setUpLoggingAndAccess(true);
185 
186         doReturn(mCert).when(mockCertStore).getCertificate("alias");
187         doThrow(new IOException()).when(mockCertStore).deleteCertificateEntry(any());
188 
189         mKeyChain.deleteCaCertificate("alias");
190 
191         verify(mockInjector, times(1)).writeSecurityEvent(
192                 SecurityLog.TAG_CERT_AUTHORITY_REMOVED, 0 /* failure */, mSubject, 0);
193     }
194 
195     @Test
testNoLoggingWhenDisabled()196     public void testNoLoggingWhenDisabled() throws Exception {
197         setUpLoggingAndAccess(false);
198 
199         doReturn(mCert).when(mockCertStore).getCertificate("alias");
200 
201         mKeyChain.installCaCertificate(TEST_CA.getBytes());
202         mKeyChain.deleteCaCertificate("alias");
203 
204         doThrow(new IOException()).when(mockCertStore).installCertificate(any());
205         doThrow(new IOException()).when(mockCertStore).deleteCertificateEntry(any());
206 
207         try {
208             mKeyChain.installCaCertificate(TEST_CA.getBytes());
209             fail("didn't propagate the exception");
210         } catch (IllegalStateException expected) {
211             assertTrue(expected.getCause() instanceof IOException);
212         }
213         mKeyChain.deleteCaCertificate("alias");
214 
215         verify(mockInjector, never()).writeSecurityEvent(anyInt(), anyInt(), any());
216     }
217 
parseCertificate(String cert)218     private X509Certificate parseCertificate(String cert) throws CertificateException {
219         final CertificateFactory cf = CertificateFactory.getInstance("X.509");
220         return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(cert.getBytes()));
221     }
222 
223     @Test
testBadPackagesNotAllowedToInstallCaCerts()224     public void testBadPackagesNotAllowedToInstallCaCerts() throws Exception {
225         setUpCaller(1000666, null);
226         try {
227             mKeyChain.installCaCertificate(TEST_CA.getBytes());
228             fail("didn't throw the exception");
229         } catch (SecurityException expected) {}
230     }
231 
232     @Test
testNonSystemPackagesNotAllowedToInstallCaCerts()233     public void testNonSystemPackagesNotAllowedToInstallCaCerts()  throws Exception {
234         setUpCaller(1000666, "xxx.nasty.flashlight");
235         try {
236             mKeyChain.installCaCertificate(TEST_CA.getBytes());
237             fail("didn't throw the exception");
238         } catch (SecurityException expected) {}
239     }
240 
241     @Test
testRequestPrivateKeyReturnsNullForNonExistingAlias()242     public void testRequestPrivateKeyReturnsNullForNonExistingAlias() throws Exception {
243         String privateKey = mKeyChain.requestPrivateKey(NON_EXISTING_ALIAS);
244         assertThat(privateKey).isNull();
245     }
246 
247     @Test
testGetCertificateReturnsNullForNonExistingAlias()248     public void testGetCertificateReturnsNullForNonExistingAlias() throws Exception {
249         byte[] certificate = mKeyChain.getCertificate(NON_EXISTING_ALIAS);
250         assertThat(certificate).isNull();
251     }
252 
253     @Test
testGetCaCertificatesReturnsNullForNonExistingAlias()254     public void testGetCaCertificatesReturnsNullForNonExistingAlias() throws Exception {
255         byte[] certificate = mKeyChain.getCaCertificates(NON_EXISTING_ALIAS);
256         assertThat(certificate).isNull();
257     }
258 
259     @Test
testHasCredentialManagementApp_noManagementApp_returnsFalse()260     public void testHasCredentialManagementApp_noManagementApp_returnsFalse() throws Exception {
261         setUpSystemCaller();
262         assertFalse(mKeyChain.hasCredentialManagementApp());
263     }
264 
265     @Test
testGetCredentialManagementAppPackageName_noManagementApp_returnsNull()266     public void testGetCredentialManagementAppPackageName_noManagementApp_returnsNull()
267             throws Exception {
268         setUpSystemCaller();
269         assertThat(mKeyChain.getCredentialManagementAppPackageName()).isNull();
270     }
271 
272     @Test
testGetCredentialManagementAppPolicy_noManagementApp_returnsNull()273     public void testGetCredentialManagementAppPolicy_noManagementApp_returnsNull()
274             throws Exception {
275         setUpSystemCaller();
276         assertThat(mKeyChain.getCredentialManagementAppPolicy()).isNull();
277     }
278 
279     @Test
testGetPredefinedAliasForPackageAndUri_noManagementApp_returnsNull()280     public void testGetPredefinedAliasForPackageAndUri_noManagementApp_returnsNull()
281             throws Exception {
282         setUpSystemCaller();
283         assertThat(mKeyChain.getPredefinedAliasForPackageAndUri(TEST_PACKAGE_NAME_1,
284                 TEST_URI_1)).isNull();
285     }
286 
287     @Test
testHasCredentialManagement_hasManagementApp_returnsTrue()288     public void testHasCredentialManagement_hasManagementApp_returnsTrue() throws Exception {
289         setUpSystemCaller();
290         mKeyChain.setCredentialManagementApp(CREDENTIAL_MANAGER_PACKAGE, AUTHENTICATION_POLICY);
291 
292         assertTrue(mKeyChain.hasCredentialManagementApp());
293     }
294 
295     @Test
testGetCredentialManagementAppPackageName_hasManagementApp_returnsPackageName()296     public void testGetCredentialManagementAppPackageName_hasManagementApp_returnsPackageName()
297             throws Exception {
298         setUpSystemCaller();
299         mKeyChain.setCredentialManagementApp(CREDENTIAL_MANAGER_PACKAGE, AUTHENTICATION_POLICY);
300 
301         assertThat(mKeyChain.getCredentialManagementAppPackageName())
302                 .isEqualTo(CREDENTIAL_MANAGER_PACKAGE);
303     }
304 
305     @Test
testGetCredentialManagementAppPolicy_hasManagementApp_returnsPolicy()306     public void testGetCredentialManagementAppPolicy_hasManagementApp_returnsPolicy()
307             throws Exception {
308         setUpSystemCaller();
309         mKeyChain.setCredentialManagementApp(CREDENTIAL_MANAGER_PACKAGE, AUTHENTICATION_POLICY);
310 
311         assertThat(mKeyChain.getCredentialManagementAppPolicy()).isEqualTo(AUTHENTICATION_POLICY);
312     }
313 
314     @Test
testGetPredefinedAliasForPackageAndUri_hasManagementApp_returnsCorrectAlias()315     public void testGetPredefinedAliasForPackageAndUri_hasManagementApp_returnsCorrectAlias()
316             throws Exception {
317         setUpSystemCaller();
318         mKeyChain.setCredentialManagementApp(CREDENTIAL_MANAGER_PACKAGE, AUTHENTICATION_POLICY);
319 
320         assertThat(mKeyChain.getPredefinedAliasForPackageAndUri(TEST_PACKAGE_NAME_1, TEST_URI_1))
321                 .isEqualTo(TEST_ALIAS_1);
322     }
323 
324     @Test
testRemoveCredentialManagementApp_hasManagementApp_removesManagementApp()325     public void testRemoveCredentialManagementApp_hasManagementApp_removesManagementApp()
326             throws Exception {
327         setUpSystemCaller();
328 
329         mKeyChain.removeCredentialManagementApp();
330 
331         assertFalse(mKeyChain.hasCredentialManagementApp());
332         assertThat(mKeyChain.getCredentialManagementAppPackageName()).isNull();
333         assertThat(mKeyChain.getCredentialManagementAppPolicy()).isNull();
334     }
335 
setUpLoggingAndAccess(boolean loggingEnabled)336     private void setUpLoggingAndAccess(boolean loggingEnabled) {
337         doReturn(loggingEnabled).when(mockInjector).isSecurityLoggingEnabled();
338 
339         // Pretend that the caller is system.
340         setUpCaller(1000, "android.uid.system:1000");
341     }
342 
setUpSystemCaller()343     private void setUpSystemCaller() {
344         setUpCaller(1000, "android.uid.system:1000");
345     }
346 
setUpCaller(int uid, String packageName)347     private void setUpCaller(int uid, String packageName) {
348         doReturn(uid).when(mockInjector).getCallingUid();
349         mShadowPackageManager.setNameForUid(uid, packageName);
350     }
351 }
352