Snap for 4577102 from b4496fefd352cec7f599b511f3ab5f43119d4ab4 to pi-release
Change-Id: I1757d4ddd372d321a1e40801aa1db6d4831de85c
diff --git a/robotests/src/com/android/keychain/KeyChainServiceRoboTest.java b/robotests/src/com/android/keychain/KeyChainServiceRoboTest.java
new file mode 100644
index 0000000..cca6fb2
--- /dev/null
+++ b/robotests/src/com/android/keychain/KeyChainServiceRoboTest.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keychain;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyVararg;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.admin.SecurityLog;
+import android.content.Intent;
+import android.security.IKeyChainService;
+
+import com.android.org.conscrypt.TrustedCertificateStore;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ServiceController;
+import org.robolectric.annotation.Config;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+import javax.security.auth.x500.X500Principal;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
+ shadows = {ShadowTrustedCertificateStore.class})
+public final class KeyChainServiceRoboTest {
+ private IKeyChainService.Stub mKeyChain;
+
+ @Mock
+ private KeyChainService.Injector mockInjector;
+ @Mock
+ private TrustedCertificateStore mockCertStore;
+
+ /*
+ * The CA cert below is the content of cacert.pem as generated by:
+ * openssl req -new -x509 -days 3650 -extensions v3_ca -keyout cakey.pem -out cacert.pem
+ */
+ private static final String TEST_CA =
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIDXTCCAkWgAwIBAgIJAK9Tl/F9V8kSMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n" +
+ "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" +
+ "aWRnaXRzIFB0eSBMdGQwHhcNMTUwMzA2MTczMjExWhcNMjUwMzAzMTczMjExWjBF\n" +
+ "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n" +
+ "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" +
+ "CgKCAQEAvItOutsE75WBTgTyNAHt4JXQ3JoseaGqcC3WQij6vhrleWi5KJ0jh1/M\n" +
+ "Rpry7Fajtwwb4t8VZa0NuM2h2YALv52w1xivql88zce/HU1y7XzbXhxis9o6SCI+\n" +
+ "oVQSbPeXRgBPppFzBEh3ZqYTVhAqw451XhwdA4Aqs3wts7ddjwlUzyMdU44osCUg\n" +
+ "kVg7lfPf9sTm5IoHVcfLSCWH5n6Nr9sH3o2ksyTwxuOAvsN11F/a0mmUoPciYPp+\n" +
+ "q7DzQzdi7akRG601DZ4YVOwo6UITGvDyuAAdxl5isovUXqe6Jmz2/myTSpAKxGFs\n" +
+ "jk9oRoG6WXWB1kni490GIPjJ1OceyQIDAQABo1AwTjAdBgNVHQ4EFgQUH1QIlPKL\n" +
+ "p2OQ/AoLOjKvBW4zK3AwHwYDVR0jBBgwFoAUH1QIlPKLp2OQ/AoLOjKvBW4zK3Aw\n" +
+ "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcMi4voMMJHeQLjtq8Oky\n" +
+ "Azpyk8moDwgCd4llcGj7izOkIIFqq/lyqKdtykVKUWz2bSHO5cLrtaOCiBWVlaCV\n" +
+ "DYAnnVLM8aqaA6hJDIfaGs4zmwz0dY8hVMFCuCBiLWuPfiYtbEmjHGSmpQTG6Qxn\n" +
+ "ZJlaK5CZyt5pgh5EdNdvQmDEbKGmu0wpCq9qjZImwdyAul1t/B0DrsWApZMgZpeI\n" +
+ "d2od0VBrCICB1K4p+C51D93xyQiva7xQcCne+TAnGNy9+gjQ/MyR8MRpwRLv5ikD\n" +
+ "u0anJCN8pXo6IMglfMAsoton1J6o5/ae5uhC6caQU8bNUsCK570gpNfjkzo6rbP0\n" +
+ "wQ==\n" +
+ "-----END CERTIFICATE-----\n";
+
+ private X509Certificate mCert;
+ private String mSubject;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ ShadowTrustedCertificateStore.sDelegate = mockCertStore;
+
+ mCert = parseCertificate(TEST_CA);
+ mSubject = mCert.getSubjectX500Principal().getName(X500Principal.CANONICAL);
+
+ final ServiceController<KeyChainService> serviceController =
+ Robolectric.buildService(KeyChainService.class).create().bind();
+ final KeyChainService service = serviceController.get();
+ service.setInjector(mockInjector);
+ final Intent intent = new Intent(IKeyChainService.class.getName());
+ mKeyChain = (IKeyChainService.Stub) service.onBind(intent);
+ }
+
+ @Test
+ public void testCaInstallSuccessLogging() throws Exception {
+ setUpLoggingAndAccess(true);
+
+ mKeyChain.installCaCertificate(TEST_CA.getBytes());
+
+ verify(mockInjector, times(1)).writeSecurityEvent(
+ SecurityLog.TAG_CERT_AUTHORITY_INSTALLED, 1 /* success */, mSubject);
+ }
+
+ @Test
+ public void testCaInstallFailedLogging() throws Exception {
+ setUpLoggingAndAccess(true);
+
+ doThrow(new IOException()).when(mockCertStore).installCertificate(any());
+
+ try {
+ mKeyChain.installCaCertificate(TEST_CA.getBytes());
+ fail("didn't propagate the exception");
+ } catch (IllegalStateException ignored) {
+ assertTrue(ignored.getCause() instanceof IOException);
+ }
+
+ verify(mockInjector, times(1)).writeSecurityEvent(
+ SecurityLog.TAG_CERT_AUTHORITY_INSTALLED, 0 /* failure */, mSubject);
+ }
+
+ @Test
+ public void testCaRemoveSuccessLogging() throws Exception {
+ setUpLoggingAndAccess(true);
+
+ doReturn(mCert).when(mockCertStore).getCertificate("alias");
+
+ mKeyChain.deleteCaCertificate("alias");
+
+ verify(mockInjector, times(1)).writeSecurityEvent(
+ SecurityLog.TAG_CERT_AUTHORITY_REMOVED, 1 /* success */, mSubject);
+ }
+
+ @Test
+ public void testCaRemoveFailedLogging() throws Exception {
+ setUpLoggingAndAccess(true);
+
+ doReturn(mCert).when(mockCertStore).getCertificate("alias");
+ doThrow(new IOException()).when(mockCertStore).deleteCertificateEntry(any());
+
+ mKeyChain.deleteCaCertificate("alias");
+
+ verify(mockInjector, times(1)).writeSecurityEvent(
+ SecurityLog.TAG_CERT_AUTHORITY_REMOVED, 0 /* failure */, mSubject);
+ }
+
+ @Test
+ public void testNoLoggingWhenDisabled() throws Exception {
+ setUpLoggingAndAccess(false);
+
+ doReturn(mCert).when(mockCertStore).getCertificate("alias");
+
+ mKeyChain.installCaCertificate(TEST_CA.getBytes());
+ mKeyChain.deleteCaCertificate("alias");
+
+ doThrow(new IOException()).when(mockCertStore).installCertificate(any());
+ doThrow(new IOException()).when(mockCertStore).deleteCertificateEntry(any());
+
+ try {
+ mKeyChain.installCaCertificate(TEST_CA.getBytes());
+ fail("didn't propagate the exception");
+ } catch (IllegalStateException ignored) {
+ assertTrue(ignored.getCause() instanceof IOException);
+ }
+ mKeyChain.deleteCaCertificate("alias");
+
+ verify(mockInjector, never()).writeSecurityEvent(anyInt(), anyInt(), anyVararg());
+ }
+
+ private X509Certificate parseCertificate(String cert) throws CertificateException {
+ final CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(cert.getBytes()));
+ }
+
+ private void setUpLoggingAndAccess(boolean loggingEnabled) {
+ doReturn(loggingEnabled).when(mockInjector).isSecurityLoggingEnabled();
+ // Pretend the caller is system.
+ doReturn(1000).when(mockInjector).getCallingUid();
+ }
+}
diff --git a/robotests/src/com/android/keychain/ShadowTrustedCertificateStore.java b/robotests/src/com/android/keychain/ShadowTrustedCertificateStore.java
new file mode 100644
index 0000000..c78f2b4
--- /dev/null
+++ b/robotests/src/com/android/keychain/ShadowTrustedCertificateStore.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keychain;
+
+import com.android.org.conscrypt.TrustedCertificateStore;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.io.IOException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+/**
+ * Delegating shadow for TrustedCertificateStore.
+ */
+@Implements(TrustedCertificateStore.class)
+public class ShadowTrustedCertificateStore {
+ public static TrustedCertificateStore sDelegate;
+
+ @Implementation
+ public void installCertificate(X509Certificate cert) throws IOException, CertificateException {
+ sDelegate.installCertificate(cert);
+ }
+
+ @Implementation
+ public String getCertificateAlias(Certificate cert) {
+ return sDelegate.getCertificateAlias(cert);
+ }
+
+ @Implementation
+ public Certificate getCertificate(String alias) {
+ return sDelegate.getCertificate(alias);
+ }
+
+ @Implementation
+ public void deleteCertificateEntry(String alias)
+ throws IOException, CertificateException {
+ sDelegate.deleteCertificateEntry(alias);
+ }
+}
+
diff --git a/src/com/android/keychain/KeyChainService.java b/src/com/android/keychain/KeyChainService.java
index 4ce8378..7b778cf 100644
--- a/src/com/android/keychain/KeyChainService.java
+++ b/src/com/android/keychain/KeyChainService.java
@@ -16,36 +16,37 @@
package com.android.keychain;
+import static android.app.admin.SecurityLog.TAG_CERT_AUTHORITY_INSTALLED;
+import static android.app.admin.SecurityLog.TAG_CERT_AUTHORITY_REMOVED;
+
import android.app.BroadcastOptions;
import android.app.IntentService;
-import android.content.ContentValues;
+import android.app.admin.SecurityLog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.StringParceledListSlice;
-import android.database.Cursor;
-import android.database.DatabaseUtils;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
-import android.os.Process;
import android.os.UserHandle;
import android.security.Credentials;
import android.security.IKeyChainService;
import android.security.KeyChain;
+import android.security.KeyStore;
import android.security.keymaster.KeymasterArguments;
import android.security.keymaster.KeymasterCertificateChain;
-import android.security.keymaster.KeymasterDefs;
-import android.security.KeyStore;
import android.security.keystore.AttestationUtils;
import android.security.keystore.DeviceIdAttestationException;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.ParcelableKeyGenParameterSpec;
import android.text.TextUtils;
import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
import com.android.keychain.internal.GrantsDatabase;
+import com.android.org.conscrypt.TrustedCertificateStore;
+
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
@@ -53,26 +54,28 @@
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
-import java.security.cert.CertificateException;
+import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
-import java.util.Set;
-import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
-import com.android.org.conscrypt.TrustedCertificateStore;
+import javax.security.auth.x500.X500Principal;
public class KeyChainService extends IntentService {
private static final String TAG = "KeyChain";
/** created in onCreate(), closed in onDestroy() */
- public GrantsDatabase mGrantsDb;
+ private GrantsDatabase mGrantsDb;
+ private Injector mInjector;
public KeyChainService() {
super(KeyChainService.class.getSimpleName());
+ mInjector = new Injector();
}
@Override public void onCreate() {
@@ -98,7 +101,7 @@
checkArgs(alias);
final String keystoreAlias = Credentials.USER_PRIVATE_KEY + alias;
- final int uid = Binder.getCallingUid();
+ final int uid = mInjector.getCallingUid();
return mKeyStore.grant(keystoreAlias, uid);
}
@@ -241,7 +244,7 @@
validateAlias(alias);
validateKeyStoreState();
- final int callingUid = getCallingUid();
+ final int callingUid = mInjector.getCallingUid();
if (!mGrantsDb.hasGrant(callingUid, alias)) {
throw new IllegalStateException("uid " + callingUid
+ " doesn't have permission to access the requested alias");
@@ -251,16 +254,27 @@
@Override public String installCaCertificate(byte[] caCertificate) {
checkCertInstallerOrSystemCaller();
final String alias;
+ String subjectForAudit = null;
try {
final X509Certificate cert = parseCertificate(caCertificate);
+ if (mInjector.isSecurityLoggingEnabled()) {
+ subjectForAudit =
+ cert.getSubjectX500Principal().getName(X500Principal.CANONICAL);
+ }
synchronized (mTrustedCertificateStore) {
mTrustedCertificateStore.installCertificate(cert);
alias = mTrustedCertificateStore.getCertificateAlias(cert);
}
- } catch (IOException e) {
+ } catch (IOException | CertificateException e) {
+ if (subjectForAudit != null) {
+ mInjector.writeSecurityEvent(
+ TAG_CERT_AUTHORITY_INSTALLED, 0 /*result*/, subjectForAudit);
+ }
throw new IllegalStateException(e);
- } catch (CertificateException e) {
- throw new IllegalStateException(e);
+ }
+ if (subjectForAudit != null) {
+ mInjector.writeSecurityEvent(
+ TAG_CERT_AUTHORITY_INSTALLED, 1 /*result*/, subjectForAudit);
}
broadcastLegacyStorageChange();
broadcastTrustStoreChange();
@@ -366,14 +380,28 @@
}
private boolean deleteCertificateEntry(String alias) {
+ String subjectForAudit = null;
+ if (mInjector.isSecurityLoggingEnabled()) {
+ final Certificate cert = mTrustedCertificateStore.getCertificate(alias);
+ if (cert instanceof X509Certificate) {
+ subjectForAudit = ((X509Certificate) cert)
+ .getSubjectX500Principal().getName(X500Principal.CANONICAL);
+ }
+ }
+
try {
mTrustedCertificateStore.deleteCertificateEntry(alias);
+ if (subjectForAudit != null) {
+ mInjector.writeSecurityEvent(
+ TAG_CERT_AUTHORITY_REMOVED, 1 /*result*/, subjectForAudit);
+ }
return true;
- } catch (IOException e) {
+ } catch (IOException | CertificateException e) {
Log.w(TAG, "Problem removing CA certificate " + alias, e);
- return false;
- } catch (CertificateException e) {
- Log.w(TAG, "Problem removing CA certificate " + alias, e);
+ if (subjectForAudit != null) {
+ mInjector.writeSecurityEvent(
+ TAG_CERT_AUTHORITY_REMOVED, 0 /*result*/, subjectForAudit);
+ }
return false;
}
}
@@ -395,7 +423,7 @@
* Returns null if actually caller is expected, otherwise return bad package to report
*/
private String checkCaller(String expectedPackage) {
- String actualPackage = getPackageManager().getNameForUid(getCallingUid());
+ String actualPackage = getPackageManager().getNameForUid(mInjector.getCallingUid());
return (!expectedPackage.equals(actualPackage)) ? actualPackage : null;
}
@@ -523,4 +551,27 @@
sendBroadcastAsUser(intent, UserHandle.of(UserHandle.myUserId()));
}
}
+
+ @VisibleForTesting
+ void setInjector(Injector injector) {
+ mInjector = injector;
+ }
+
+ /**
+ * Injector for mocking out dependencies in tests.
+ */
+ @VisibleForTesting
+ static class Injector {
+ public boolean isSecurityLoggingEnabled() {
+ return SecurityLog.isLoggingEnabled();
+ }
+
+ public void writeSecurityEvent(int tag, Object... payload) {
+ SecurityLog.writeEvent(tag, payload);
+ }
+
+ public int getCallingUid() {
+ return Binder.getCallingUid();
+ }
+ }
}