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.assertTrue;
22 import static org.junit.Assert.fail;
23 import static org.mockito.ArgumentMatchers.any;
24 import static org.mockito.ArgumentMatchers.anyInt;
25 import static org.mockito.ArgumentMatchers.anyVararg;
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.security.IKeyChainService;
37 
38 import com.android.org.conscrypt.TrustedCertificateStore;
39 import com.android.keychain.ShadowKeyStore;
40 
41 import org.junit.Before;
42 import org.junit.Test;
43 import org.junit.runner.RunWith;
44 import org.mockito.Mock;
45 import org.mockito.MockitoAnnotations;
46 import org.robolectric.Robolectric;
47 import org.robolectric.RobolectricTestRunner;
48 import org.robolectric.RuntimeEnvironment;
49 import org.robolectric.android.controller.ServiceController;
50 import org.robolectric.annotation.Config;
51 import org.robolectric.shadows.ShadowPackageManager;
52 
53 import java.io.ByteArrayInputStream;
54 import java.io.IOException;
55 import java.security.cert.CertificateException;
56 import java.security.cert.CertificateFactory;
57 import java.security.cert.X509Certificate;
58 
59 import javax.security.auth.x500.X500Principal;
60 
61 @RunWith(RobolectricTestRunner.class)
62 @Config(shadows = {
63     ShadowTrustedCertificateStore.class,
64     ShadowKeyStore.class
65 })
66 public final class KeyChainServiceRoboTest {
67     private IKeyChainService.Stub mKeyChain;
68 
69     @Mock
70     private KeyChainService.Injector mockInjector;
71     @Mock
72     private TrustedCertificateStore mockCertStore;
73 
74     /*
75      * The CA cert below is the content of cacert.pem as generated by:
76      * openssl req -new -x509 -days 3650 -extensions v3_ca -keyout cakey.pem -out cacert.pem
77      */
78     private static final String TEST_CA =
79             "-----BEGIN CERTIFICATE-----\n" +
80             "MIIDXTCCAkWgAwIBAgIJAK9Tl/F9V8kSMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n" +
81             "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" +
82             "aWRnaXRzIFB0eSBMdGQwHhcNMTUwMzA2MTczMjExWhcNMjUwMzAzMTczMjExWjBF\n" +
83             "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n" +
84             "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" +
85             "CgKCAQEAvItOutsE75WBTgTyNAHt4JXQ3JoseaGqcC3WQij6vhrleWi5KJ0jh1/M\n" +
86             "Rpry7Fajtwwb4t8VZa0NuM2h2YALv52w1xivql88zce/HU1y7XzbXhxis9o6SCI+\n" +
87             "oVQSbPeXRgBPppFzBEh3ZqYTVhAqw451XhwdA4Aqs3wts7ddjwlUzyMdU44osCUg\n" +
88             "kVg7lfPf9sTm5IoHVcfLSCWH5n6Nr9sH3o2ksyTwxuOAvsN11F/a0mmUoPciYPp+\n" +
89             "q7DzQzdi7akRG601DZ4YVOwo6UITGvDyuAAdxl5isovUXqe6Jmz2/myTSpAKxGFs\n" +
90             "jk9oRoG6WXWB1kni490GIPjJ1OceyQIDAQABo1AwTjAdBgNVHQ4EFgQUH1QIlPKL\n" +
91             "p2OQ/AoLOjKvBW4zK3AwHwYDVR0jBBgwFoAUH1QIlPKLp2OQ/AoLOjKvBW4zK3Aw\n" +
92             "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcMi4voMMJHeQLjtq8Oky\n" +
93             "Azpyk8moDwgCd4llcGj7izOkIIFqq/lyqKdtykVKUWz2bSHO5cLrtaOCiBWVlaCV\n" +
94             "DYAnnVLM8aqaA6hJDIfaGs4zmwz0dY8hVMFCuCBiLWuPfiYtbEmjHGSmpQTG6Qxn\n" +
95             "ZJlaK5CZyt5pgh5EdNdvQmDEbKGmu0wpCq9qjZImwdyAul1t/B0DrsWApZMgZpeI\n" +
96             "d2od0VBrCICB1K4p+C51D93xyQiva7xQcCne+TAnGNy9+gjQ/MyR8MRpwRLv5ikD\n" +
97             "u0anJCN8pXo6IMglfMAsoton1J6o5/ae5uhC6caQU8bNUsCK570gpNfjkzo6rbP0\n" +
98             "wQ==\n" +
99             "-----END CERTIFICATE-----\n";
100 
101     private static final String NON_EXISTING_ALIAS = "alias-does-not-exist-1";
102 
103     private X509Certificate mCert;
104     private String mSubject;
105     private ShadowPackageManager mShadowPackageManager;
106 
107     @Before
setUp()108     public void setUp() throws Exception {
109         MockitoAnnotations.initMocks(this);
110         ShadowTrustedCertificateStore.sDelegate = mockCertStore;
111 
112         mCert = parseCertificate(TEST_CA);
113         mSubject = mCert.getSubjectX500Principal().getName(X500Principal.CANONICAL);
114 
115         final PackageManager packageManager = RuntimeEnvironment.application.getPackageManager();
116         mShadowPackageManager = shadowOf(packageManager);
117 
118         final ServiceController<KeyChainService> serviceController =
119                 Robolectric.buildService(KeyChainService.class).create().bind();
120         final KeyChainService service = serviceController.get();
121         service.setInjector(mockInjector);
122         final Intent intent = new Intent(IKeyChainService.class.getName());
123         mKeyChain = (IKeyChainService.Stub) service.onBind(intent);
124     }
125 
126     @Test
testCaInstallSuccessLogging()127     public void testCaInstallSuccessLogging() throws Exception {
128         setUpLoggingAndAccess(true);
129 
130         mKeyChain.installCaCertificate(TEST_CA.getBytes());
131 
132         verify(mockInjector, times(1)).writeSecurityEvent(
133                 SecurityLog.TAG_CERT_AUTHORITY_INSTALLED, 1 /* success */, mSubject, 0);
134     }
135 
136     @Test
testCaInstallFailedLogging()137     public void testCaInstallFailedLogging() throws Exception {
138         setUpLoggingAndAccess(true);
139 
140         doThrow(new IOException()).when(mockCertStore).installCertificate(any());
141 
142         try {
143             mKeyChain.installCaCertificate(TEST_CA.getBytes());
144             fail("didn't propagate the exception");
145         } catch (IllegalStateException expected) {
146             assertTrue(expected.getCause() instanceof IOException);
147         }
148 
149         verify(mockInjector, times(1)).writeSecurityEvent(
150                 SecurityLog.TAG_CERT_AUTHORITY_INSTALLED, 0 /* failure */, mSubject, 0);
151     }
152 
153     @Test
testCaRemoveSuccessLogging()154     public void testCaRemoveSuccessLogging() throws Exception {
155         setUpLoggingAndAccess(true);
156 
157         doReturn(mCert).when(mockCertStore).getCertificate("alias");
158 
159         mKeyChain.deleteCaCertificate("alias");
160 
161         verify(mockInjector, times(1)).writeSecurityEvent(
162                 SecurityLog.TAG_CERT_AUTHORITY_REMOVED, 1 /* success */, mSubject, 0);
163     }
164 
165     @Test
testCaRemoveFailedLogging()166     public void testCaRemoveFailedLogging() throws Exception {
167         setUpLoggingAndAccess(true);
168 
169         doReturn(mCert).when(mockCertStore).getCertificate("alias");
170         doThrow(new IOException()).when(mockCertStore).deleteCertificateEntry(any());
171 
172         mKeyChain.deleteCaCertificate("alias");
173 
174         verify(mockInjector, times(1)).writeSecurityEvent(
175                 SecurityLog.TAG_CERT_AUTHORITY_REMOVED, 0 /* failure */, mSubject, 0);
176     }
177 
178     @Test
testNoLoggingWhenDisabled()179     public void testNoLoggingWhenDisabled() throws Exception {
180         setUpLoggingAndAccess(false);
181 
182         doReturn(mCert).when(mockCertStore).getCertificate("alias");
183 
184         mKeyChain.installCaCertificate(TEST_CA.getBytes());
185         mKeyChain.deleteCaCertificate("alias");
186 
187         doThrow(new IOException()).when(mockCertStore).installCertificate(any());
188         doThrow(new IOException()).when(mockCertStore).deleteCertificateEntry(any());
189 
190         try {
191             mKeyChain.installCaCertificate(TEST_CA.getBytes());
192             fail("didn't propagate the exception");
193         } catch (IllegalStateException expected) {
194             assertTrue(expected.getCause() instanceof IOException);
195         }
196         mKeyChain.deleteCaCertificate("alias");
197 
198         verify(mockInjector, never()).writeSecurityEvent(anyInt(), anyInt(), anyVararg());
199     }
200 
parseCertificate(String cert)201     private X509Certificate parseCertificate(String cert) throws CertificateException {
202         final CertificateFactory cf = CertificateFactory.getInstance("X.509");
203         return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(cert.getBytes()));
204     }
205 
206     @Test
testBadPackagesNotAllowedToInstallCaCerts()207     public void testBadPackagesNotAllowedToInstallCaCerts() throws Exception {
208         setUpCaller(1000666, null);
209         try {
210             mKeyChain.installCaCertificate(TEST_CA.getBytes());
211             fail("didn't throw the exception");
212         } catch (SecurityException expected) {}
213     }
214 
215     @Test
testNonSystemPackagesNotAllowedToInstallCaCerts()216     public void testNonSystemPackagesNotAllowedToInstallCaCerts()  throws Exception {
217         setUpCaller(1000666, "xxx.nasty.flashlight");
218         try {
219             mKeyChain.installCaCertificate(TEST_CA.getBytes());
220             fail("didn't throw the exception");
221         } catch (SecurityException expected) {}
222     }
223 
224     @Test
testRequestPrivateKeyReturnsNullForNonExistingAlias()225     public void testRequestPrivateKeyReturnsNullForNonExistingAlias() throws Exception {
226         String privateKey = mKeyChain.requestPrivateKey(NON_EXISTING_ALIAS);
227         assertThat(privateKey).isNull();
228     }
229 
230     @Test
testGetCertificateReturnsNullForNonExistingAlias()231     public void testGetCertificateReturnsNullForNonExistingAlias() throws Exception {
232         byte[] certificate = mKeyChain.getCertificate(NON_EXISTING_ALIAS);
233         assertThat(certificate).isNull();
234     }
235 
236     @Test
testGetCaCertificatesReturnsNullForNonExistingAlias()237     public void testGetCaCertificatesReturnsNullForNonExistingAlias() throws Exception {
238         byte[] certificate = mKeyChain.getCaCertificates(NON_EXISTING_ALIAS);
239         assertThat(certificate).isNull();
240     }
241 
setUpLoggingAndAccess(boolean loggingEnabled)242     private void setUpLoggingAndAccess(boolean loggingEnabled) {
243         doReturn(loggingEnabled).when(mockInjector).isSecurityLoggingEnabled();
244 
245         // Pretend that the caller is system.
246         setUpCaller(1000, "android.uid.system:1000");
247     }
248 
setUpCaller(int uid, String packageName)249     private void setUpCaller(int uid, String packageName) {
250         doReturn(uid).when(mockInjector).getCallingUid();
251         mShadowPackageManager.setNameForUid(uid, packageName);
252     }
253 }
254