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