Modify CTLogger to take in the LogListUpdateStatus object instead of

individually passed parameters

This CL also moves the DataStore updates of the failure count into
CTLogger directly (CTDownloader will only modify the failure count on
complete success).

Note: due to limitations with Mockito (not being able to mock static
classes like the auto-generated CTStatsLog), there is no test class for
CTLoggerImpl.

Bug: 394278886
Bug: 378626065
Flag: com.android.net.ct.flags.certificate_transparency_service
Test: atest NetworkSecurityUnitTests
Change-Id: I4498bfe75e84c6ba1654cfa55912c46836f61e03
This commit is contained in:
Bessie Jiang 2025-02-05 18:29:58 +00:00
commit 782fad0c3a
6 changed files with 99 additions and 192 deletions

View file

@ -35,6 +35,7 @@ import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/** Helper class to download certificate transparency log files. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@ -201,14 +202,9 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
// TODO(b/391327942): parse file and log the timestamp of the log list
if (!updateStatus.isSignatureVerified()) {
updateFailureCount();
Log.w(TAG, "Log list did not pass verification");
mLogger.logCTLogListUpdateStateChangedEvent(
updateStatus.state(),
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0),
updateStatus.signature());
mLogger.logCTLogListUpdateStateChangedEvent(updateStatus);
return;
}
@ -227,47 +223,28 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
mDataStore.store();
} else {
updateFailureCount();
mLogger.logCTLogListUpdateStateChangedEvent(
CTLogListUpdateState.VERSION_ALREADY_EXISTS,
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0),
updateStatus.signature());
updateStatus
.toBuilder()
.setState(CTLogListUpdateState.VERSION_ALREADY_EXISTS)
.build());
}
}
private void handleDownloadFailed(DownloadStatus status) {
Log.e(TAG, "Download failed with " + status);
updateFailureCount();
int failureCount =
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
// Unable to log the signature, as that is dependent on successful downloads
LogListUpdateStatus.Builder updateStatus = LogListUpdateStatus.builder();
if (status.isHttpError()) {
mLogger.logCTLogListUpdateStateChangedEvent(
CTLogListUpdateState.HTTP_ERROR,
failureCount,
status.reason());
updateStatus
.setState(CTLogListUpdateState.HTTP_ERROR)
.setHttpErrorStatusCode(status.reason());
} else {
// TODO(b/384935059): handle blocked domain logging
mLogger.logCTLogListUpdateStateChangedEventWithDownloadStatus(
status.reason(), failureCount);
updateStatus.setDownloadStatus(Optional.of(status.reason()));
}
}
/**
* Updates the data store with the current number of consecutive log list update failures.
*/
private void updateFailureCount() {
int failure_count =
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
int new_failure_count = failure_count + 1;
mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, new_failure_count);
mDataStore.store();
mLogger.logCTLogListUpdateStateChangedEvent(updateStatus.build());
}
private long download(String url) {

View file

@ -20,35 +20,12 @@ package com.android.server.net.ct;
public interface CertificateTransparencyLogger {
/**
* Logs a CTLogListUpdateStateChanged event to statsd, when failure is from DownloadManager.
* Logs a CTLogListUpdateStateChanged event to statsd.
*
* @param downloadStatus DownloadManager failure status why the log list wasn't updated
* @param failureCount number of consecutive log list update failures
* @param updateStatus status object containing details from this update event (e.g. log list
* signature, log list timestamp, failure reason if applicable)
*/
void logCTLogListUpdateStateChangedEventWithDownloadStatus(
int downloadStatus, int failureCount);
/**
* Logs a CTLogListUpdateStateChanged event to statsd without a HTTP error status code.
*
* @param failureReason reason why the log list wasn't updated
* @param failureCount number of consecutive log list update failures
* @param logListSignature signature used during log list verification
*/
void logCTLogListUpdateStateChangedEvent(
CTLogListUpdateState failureReason, int failureCount, String logListSignature);
/**
* Logs a CTLogListUpdateStateChanged event to statsd with an HTTP error status code.
*
* @param failureReason reason why the log list wasn't updated (e.g. DownloadManager failures)
* @param failureCount number of consecutive log list update failures
* @param httpErrorStatusCode if relevant, the HTTP error status code from DownloadManager
*/
void logCTLogListUpdateStateChangedEvent(
CTLogListUpdateState failureReason,
int failureCount,
int httpErrorStatusCode);
void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus);
/**
* Intermediate enum for use with CertificateTransparencyStatsLog.

View file

@ -35,49 +35,56 @@ import android.app.DownloadManager;
/** Implementation for logging to statsd for Certificate Transparency. */
class CertificateTransparencyLoggerImpl implements CertificateTransparencyLogger {
@Override
public void logCTLogListUpdateStateChangedEventWithDownloadStatus(
int downloadStatus, int failureCount) {
logCTLogListUpdateStateChangedEvent(
downloadStatusToFailureReason(downloadStatus),
failureCount,
/* httpErrorStatusCode= */ 0,
/* signature= */ "");
private final DataStore mDataStore;
CertificateTransparencyLoggerImpl(DataStore dataStore) {
mDataStore = dataStore;
}
@Override
public void logCTLogListUpdateStateChangedEvent(
CTLogListUpdateState failureReason, int failureCount, String signature) {
logCTLogListUpdateStateChangedEvent(
localEnumToStatsLogEnum(failureReason),
failureCount,
/* httpErrorStatusCode= */ 0,
signature);
}
public void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus) {
int updateState =
updateStatus
.downloadStatus()
.map(s -> downloadStatusToFailureReason(s))
.orElseGet(() -> localEnumToStatsLogEnum(updateStatus.state()));
int failureCount =
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
@Override
public void logCTLogListUpdateStateChangedEvent(
CTLogListUpdateState failureReason,
int failureCount,
int httpErrorStatusCode) {
logCTLogListUpdateStateChangedEvent(
localEnumToStatsLogEnum(failureReason),
updateState,
failureCount,
httpErrorStatusCode,
/* signature= */ "");
updateStatus.httpErrorStatusCode(),
updateStatus.signature());
}
private void logCTLogListUpdateStateChangedEvent(
int failureReason, int failureCount, int httpErrorStatusCode, String signature) {
int updateState, int failureCount, int httpErrorStatusCode, String signature) {
updateFailureCount();
CertificateTransparencyStatsLog.write(
CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED,
failureReason,
updateState,
failureCount,
httpErrorStatusCode,
signature,
/* logListTimestampMs= */ 0);
}
/**
* Updates the data store with the current number of consecutive log list update failures.
*/
private void updateFailureCount() {
int failure_count =
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
int new_failure_count = failure_count + 1;
mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, new_failure_count);
mDataStore.store();
}
/** Converts DownloadStatus reason into failure reason to log. */
private int downloadStatusToFailureReason(int downloadStatusReason) {
switch (downloadStatusReason) {

View file

@ -61,7 +61,7 @@ public class CertificateTransparencyService extends ICertificateTransparencyMana
dataStore,
new DownloadHelper(context),
new SignatureVerifier(context),
new CertificateTransparencyLoggerImpl()),
new CertificateTransparencyLoggerImpl(dataStore)),
new CompatibilityVersion(
Config.COMPATIBILITY_VERSION,
Config.URL_SIGNATURE,

View file

@ -24,6 +24,8 @@ import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateSt
import com.google.auto.value.AutoValue;
import java.util.Optional;
/** Class to represent the signature verification status for Certificate Transparency. */
@AutoValue
public abstract class LogListUpdateStatus {
@ -34,6 +36,10 @@ public abstract class LogListUpdateStatus {
abstract long logListTimestamp();
abstract int httpErrorStatusCode();
abstract Optional<Integer> downloadStatus();
boolean isSignatureVerified() {
// Check that none of the signature verification failures have been set as the state
return state() != PUBLIC_KEY_NOT_FOUND
@ -54,6 +60,10 @@ public abstract class LogListUpdateStatus {
abstract Builder setLogListTimestamp(long timestamp);
abstract Builder setHttpErrorStatusCode(int httpStatusCode);
abstract Builder setDownloadStatus(Optional<Integer> downloadStatus);
abstract LogListUpdateStatus build();
}
@ -63,6 +73,8 @@ public abstract class LogListUpdateStatus {
return new AutoValue_LogListUpdateStatus.Builder()
.setState(CTLogListUpdateState.UNKNOWN_STATE)
.setSignature("")
.setLogListTimestamp(0L);
.setLogListTimestamp(0L)
.setHttpErrorStatusCode(0)
.setDownloadStatus(Optional.empty());
}
}

View file

@ -20,10 +20,6 @@ import static com.google.common.io.Files.toByteArray;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -50,6 +46,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@ -67,6 +64,7 @@ import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.util.Base64;
import java.util.Optional;
/** Tests for the {@link CertificateTransparencyDownloader}. */
@RunWith(JUnit4.class)
@ -74,6 +72,8 @@ public class CertificateTransparencyDownloaderTest {
@Mock private DownloadManager mDownloadManager;
@Mock private CertificateTransparencyLogger mLogger;
private ArgumentCaptor<LogListUpdateStatus> mUpdateStatusCaptor =
ArgumentCaptor.forClass(LogListUpdateStatus.class);
private PrivateKey mPrivateKey;
private PublicKey mPublicKey;
@ -207,14 +207,12 @@ public class CertificateTransparencyDownloaderTest {
mContext,
makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_INSUFFICIENT_SPACE));
assertThat(
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
.isEqualTo(1);
verify(mLogger, times(1))
.logCTLogListUpdateStateChangedEventWithDownloadStatus(
DownloadManager.ERROR_INSUFFICIENT_SPACE,
/* failureCount= */ 1);
.logCTLogListUpdateStateChangedEvent(
LogListUpdateStatus.builder()
.setDownloadStatus(
Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
.build());
}
@Test
@ -256,14 +254,12 @@ public class CertificateTransparencyDownloaderTest {
makeMetadataDownloadFailedIntent(
mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
assertThat(
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
.isEqualTo(1);
verify(mLogger, times(1))
.logCTLogListUpdateStateChangedEventWithDownloadStatus(
DownloadManager.ERROR_INSUFFICIENT_SPACE,
/* failureCount= */ 1);
.logCTLogListUpdateStateChangedEvent(
LogListUpdateStatus.builder()
.setDownloadStatus(
Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
.build());
}
@Test
@ -309,14 +305,12 @@ public class CertificateTransparencyDownloaderTest {
makeContentDownloadFailedIntent(
mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
assertThat(
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
.isEqualTo(1);
verify(mLogger, times(1))
.logCTLogListUpdateStateChangedEventWithDownloadStatus(
DownloadManager.ERROR_INSUFFICIENT_SPACE,
/* failureCount= */ 1);
.logCTLogListUpdateStateChangedEvent(
LogListUpdateStatus.builder()
.setDownloadStatus(
Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
.build());
}
@Test
@ -352,30 +346,10 @@ public class CertificateTransparencyDownloaderTest {
mCertificateTransparencyDownloader.onReceive(
mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
assertThat(
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
.isEqualTo(1);
verify(mLogger, times(1))
.logCTLogListUpdateStateChangedEvent(
CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND,
/* failureCount= */ 1,
"");
verify(mLogger, never())
.logCTLogListUpdateStateChangedEvent(
eq(CTLogListUpdateState.SIGNATURE_NOT_FOUND),
anyInt(),
anyString());
verify(mLogger, never())
.logCTLogListUpdateStateChangedEvent(
eq(CTLogListUpdateState.SIGNATURE_INVALID),
anyInt(),
anyString());
verify(mLogger, never())
.logCTLogListUpdateStateChangedEvent(
eq(CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED),
anyInt(),
anyString());
.logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
assertThat(mUpdateStatusCaptor.getValue().state())
.isEqualTo(CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND);
}
@Test
@ -398,30 +372,10 @@ public class CertificateTransparencyDownloaderTest {
mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
// Assert
assertThat(
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
.isEqualTo(1);
verify(mLogger, times(1))
.logCTLogListUpdateStateChangedEvent(
CTLogListUpdateState.SIGNATURE_INVALID,
/* failureCount= */ 1,
""); // Should be empty b/c invalid key exception thrown
verify(mLogger, never())
.logCTLogListUpdateStateChangedEvent(
eq(CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED),
anyInt(),
anyString());
verify(mLogger, never())
.logCTLogListUpdateStateChangedEvent(
eq(CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND),
anyInt(),
anyString());
verify(mLogger, never())
.logCTLogListUpdateStateChangedEvent(
eq(CTLogListUpdateState.SIGNATURE_NOT_FOUND),
anyInt(),
anyString());
.logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
assertThat(mUpdateStatusCaptor.getValue().state())
.isEqualTo(CTLogListUpdateState.SIGNATURE_INVALID);
}
@Test
@ -444,31 +398,14 @@ public class CertificateTransparencyDownloaderTest {
mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
// Assert
assertThat(
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
.isEqualTo(1);
verify(mLogger, never())
.logCTLogListUpdateStateChangedEvent(
eq(CTLogListUpdateState.SIGNATURE_NOT_FOUND),
anyInt(),
anyString());
verify(mLogger, never())
.logCTLogListUpdateStateChangedEvent(
eq(CTLogListUpdateState.SIGNATURE_INVALID),
anyInt(),
anyString());
verify(mLogger, never())
.logCTLogListUpdateStateChangedEvent(
eq(CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND),
anyInt(),
anyString());
byte[] signatureBytes = Base64.getDecoder().decode(toByteArray(metadataFile));
verify(mLogger, times(1))
.logCTLogListUpdateStateChangedEvent(
CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED,
/* failureCount= */ 1,
new String(signatureBytes, StandardCharsets.UTF_8));
.logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
assertThat(statusValue.state())
.isEqualTo(CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED);
assertThat(statusValue.signature())
.isEqualTo(new String(signatureBytes, StandardCharsets.UTF_8));
}
@Test
@ -485,16 +422,13 @@ public class CertificateTransparencyDownloaderTest {
mCertificateTransparencyDownloader.onReceive(
mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
assertThat(
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
.isEqualTo(1);
byte[] signatureBytes = Base64.getDecoder().decode(toByteArray(metadataFile));
verify(mLogger, times(1))
.logCTLogListUpdateStateChangedEvent(
CTLogListUpdateState.VERSION_ALREADY_EXISTS,
/* failureCount= */ 1,
new String(signatureBytes, StandardCharsets.UTF_8));
.logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.VERSION_ALREADY_EXISTS);
assertThat(statusValue.signature())
.isEqualTo(new String(signatureBytes, StandardCharsets.UTF_8));
}
@Test