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); 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); 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); 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); 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