Merge "Implement killswitch mechanism for Android CT" into main
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index 4584212..002ad9a 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -20,8 +20,10 @@
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DOWNLOAD_CANNOT_RESUME;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_UNKNOWN;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_TOO_MANY_REDIRECTS;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_UNKNOWN;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__PENDING_WAITING_FOR_WIFI;
 
@@ -47,6 +49,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
 
 /** Helper class to download certificate transparency log files. */
 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@@ -221,15 +224,42 @@
         }
 
         boolean success = false;
+        boolean failureLogged = false;
+
         try {
             success = mSignatureVerifier.verify(contentUri, metadataUri);
+        } catch (MissingPublicKeyException e) {
+            if (updateFailureCount()) {
+                failureLogged = true;
+                mLogger.logCTLogListUpdateFailedEvent(
+                        CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND,
+                        mDataStore.getPropertyInt(
+                            Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0)
+                );
+            }
+        } catch (InvalidKeyException e) {
+            if (updateFailureCount()) {
+                failureLogged = true;
+                mLogger.logCTLogListUpdateFailedEvent(
+                        CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
+                        mDataStore.getPropertyInt(
+                            Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0)
+                );
+            }
         } catch (IOException | GeneralSecurityException e) {
             Log.e(TAG, "Could not verify new log list", e);
         }
+
         if (!success) {
             Log.w(TAG, "Log list did not pass verification");
 
-            // TODO(b/384931263): add logging for failed signature verification
+            // Avoid logging failure twice
+            if (!failureLogged && updateFailureCount()) {
+                mLogger.logCTLogListUpdateFailedEvent(
+                        CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
+                        mDataStore.getPropertyInt(
+                            Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
+            }
             return;
         }
 
diff --git a/networksecurity/service/src/com/android/server/net/ct/MissingPublicKeyException.java b/networksecurity/service/src/com/android/server/net/ct/MissingPublicKeyException.java
new file mode 100644
index 0000000..80607f6
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/MissingPublicKeyException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 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.server.net.ct;
+
+/**
+ * An exception thrown when the public key is missing for CT signature verification.
+ */
+public class MissingPublicKeyException extends Exception {
+
+    public MissingPublicKeyException(String message) {
+        super(message);
+    }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
index 0b775ca..96488fc 100644
--- a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -27,7 +27,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
 import java.security.KeyFactory;
 import java.security.PublicKey;
 import java.security.Signature;
@@ -74,9 +73,10 @@
         mPublicKey = Optional.of(publicKey);
     }
 
-    boolean verify(Uri file, Uri signature) throws GeneralSecurityException, IOException {
+    boolean verify(Uri file, Uri signature)
+            throws GeneralSecurityException, IOException, MissingPublicKeyException {
         if (!mPublicKey.isPresent()) {
-            throw new InvalidKeyException("Missing public key for signature verification");
+            throw new MissingPublicKeyException("Missing public key for signature verification");
         }
         Signature verifier = Signature.getInstance("SHA256withRSA");
         verifier.initVerify(mPublicKey.get());
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index 3a359f4..25f0dc1 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -16,6 +16,8 @@
 package com.android.server.net.ct;
 
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -220,7 +222,7 @@
     public void testDownloader_publicKeyDownloadFail_failureThresholdNotMet_doesNotLog()
                 throws Exception {
         long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
-        // Set the failure count to just below the threshold
+        // Set the failure count to well below the threshold
         mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
         setFailedDownload(
                 publicKeyId, // Failure cases where we give up on the download.
@@ -294,7 +296,7 @@
     public void testDownloader_metadataDownloadFail_failureThresholdNotMet_doesNotLog()
                 throws Exception {
         long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
-        // Set the failure count to just below the threshold
+        // Set the failure count to well below the threshold
         mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
         setFailedDownload(
                 metadataId,
@@ -379,7 +381,7 @@
     public void testDownloader_contentDownloadFail_failureThresholdNotMet_doesNotLog()
                 throws Exception {
         long contentId = mCertificateTransparencyDownloader.startContentDownload();
-        // Set the failure count to just below the threshold
+        // Set the failure count to well below the threshold
         mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
         setFailedDownload(
                 contentId,
@@ -419,6 +421,148 @@
 
     @Test
     public void
+            testDownloader_contentDownloadSuccess_noSignatureFound_failureThresholdExceeded_logsSingleFailure()
+                    throws Exception {
+        File logListFile = makeLogListFile("456");
+        File metadataFile = sign(logListFile);
+        mSignatureVerifier.setPublicKey(mPublicKey);
+        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+        setSuccessfulDownload(metadataId, metadataFile);
+        long contentId = mCertificateTransparencyDownloader.startContentDownload();
+        setSuccessfulDownload(contentId, logListFile);
+        // Set the failure count to just below the threshold
+        mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+                Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+        // Set the public key to be missing
+        mSignatureVerifier.resetPublicKey();
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeDownloadCompleteIntent(contentId));
+
+        assertThat(mDataStore.getPropertyInt(
+                Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+                        .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+        verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
+                CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND,
+                Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
+        );
+        verify(mLogger, never()).logCTLogListUpdateFailedEvent(
+                eq(CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION),
+                anyInt()
+        );
+    }
+
+    @Test
+    public void
+            testDownloader_contentDownloadSuccess_wrongSignatureAlgo_failureThresholdExceeded_logsSingleFailure()
+                    throws Exception {
+        // Arrange
+        File logListFile = makeLogListFile("456");
+        File metadataFile = sign(logListFile);
+
+        // Set the key to be deliberately wrong by using diff algorithm
+        KeyPairGenerator instance = KeyPairGenerator.getInstance("EC");
+        mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+
+        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+        setSuccessfulDownload(metadataId, metadataFile);
+        long contentId = mCertificateTransparencyDownloader.startContentDownload();
+        setSuccessfulDownload(contentId, logListFile);
+
+        // Set the failure count to just below the threshold
+        mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+                Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+        // Act
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeDownloadCompleteIntent(contentId));
+
+        // Assert
+        assertThat(mDataStore.getPropertyInt(
+                Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+                        .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+        verify(mLogger, never()).logCTLogListUpdateFailedEvent(
+                eq(CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND),
+                anyInt()
+        );
+        verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
+                CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
+                Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
+        );
+    }
+
+    @Test
+    public void
+            testDownloader_contentDownloadSuccess_signatureNotVerified_failureThresholdExceeded_logsSingleFailure()
+                    throws Exception {
+        // Arrange
+        File logListFile = makeLogListFile("456");
+        File metadataFile = sign(logListFile);
+
+        // Set the key to be deliberately wrong by using diff key pair
+        KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+        mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+
+        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+        setSuccessfulDownload(metadataId, metadataFile);
+        long contentId = mCertificateTransparencyDownloader.startContentDownload();
+        setSuccessfulDownload(contentId, logListFile);
+
+        // Set the failure count to just below the threshold
+        mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+                Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+        // Act
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeDownloadCompleteIntent(contentId));
+
+        // Assert
+        assertThat(mDataStore.getPropertyInt(
+                Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+                        .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+        verify(mLogger, never()).logCTLogListUpdateFailedEvent(
+                eq(CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND),
+                anyInt()
+        );
+        verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
+                CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
+                Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
+        );
+    }
+
+    @Test
+    public void
+            testDownloader_contentDownloadSuccess_wrongSignature_failureThresholdNotMet_doesNotLog()
+                    throws Exception {
+        File logListFile = makeLogListFile("456");
+        File metadataFile = sign(logListFile);
+        // Set the public key wrong, so signature verification fails
+        mSignatureVerifier.setPublicKey(mPublicKey);
+        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+        setSuccessfulDownload(metadataId, metadataFile);
+        long contentId = mCertificateTransparencyDownloader.startContentDownload();
+        setSuccessfulDownload(contentId, logListFile);
+        // Set the failure count to well below the threshold
+        mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
+
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeDownloadCompleteIntent(contentId));
+
+        assertThat(mDataStore.getPropertyInt(
+                Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+                        .isEqualTo(1);
+        verify(mLogger, never()).logCTLogListUpdateFailedEvent(
+                eq(CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND),
+                anyInt()
+        );
+        verify(mLogger, never()).logCTLogListUpdateFailedEvent(
+                eq(CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION),
+                anyInt()
+        );
+    }
+
+    @Test
+    public void
             testDownloader_contentDownloadSuccess_installFail_failureThresholdExceeded_logsFailure()
                     throws Exception {
         File logListFile = makeLogListFile("456");
@@ -458,7 +602,7 @@
         setSuccessfulDownload(metadataId, metadataFile);
         long contentId = mCertificateTransparencyDownloader.startContentDownload();
         setSuccessfulDownload(contentId, logListFile);
-        // Set the failure count to just below the threshold
+        // Set the failure count to well below the threshold
         mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
         when(mCertificateTransparencyInstaller.install(
                         eq(Config.COMPATIBILITY_VERSION), any(), anyString()))