Log audit events for root CA install/delete.

Bug: 70886042
Test: m -j RunKeyChainRoboTests
Change-Id: I3a22360a29fcb927a1fe506f2130a367576195aa
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();
+        }
+    }
 }