Add logging when signature is either not found or doesn't pass

verification

Note: InvalidKeyExceptions may be thrown for other reasons (e.g.
signature algorithm not matching), hence the addition of a new Exception
to specify when the public key is missing.

Flag: com.android.net.ct.flags.certificate_transparency_service
Fixes: 384931263
Bug: 378626065
Test: atest NetworkSecurityUnitTests
Change-Id: I67341b1e2d169cf634e45ca84eb24e3a1f6d887e
This commit is contained in:
Bessie Jiang 2024-12-24 13:51:18 +00:00
commit 558445cc9a
4 changed files with 209 additions and 9 deletions

View file

@ -20,8 +20,10 @@ import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFIC
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 org.json.JSONObject;
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 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
}
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;
}

View file

@ -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);
}
}

View file

@ -27,7 +27,6 @@ import androidx.annotation.VisibleForTesting;
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 @@ public class SignatureVerifier {
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());

View file

@ -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 class CertificateTransparencyDownloaderTest {
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 class CertificateTransparencyDownloaderTest {
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 class CertificateTransparencyDownloaderTest {
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,
@ -417,6 +419,148 @@ public class CertificateTransparencyDownloaderTest {
assertNoVersionIsInstalled();
}
@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()
@ -458,7 +602,7 @@ public class CertificateTransparencyDownloaderTest {
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()))