Merge "Add logging when signature is either not found or doesn't pass verification" 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 79123ee..502c449 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)
@@ -202,15 +205,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()))